什么是dex
dex是Android系统特有的可执行文件,具有应用的全部指令及运行时数据。
.class通过dx工具整合到一个dex,使得各个类都能互相通信,且减小占用的空间,使得结构更加紧凑。
class文件和dex的结构存在本质上的不同。
为什么不使用class格式而采用dex格式
- dex整合所有的class文件压缩在classes.dex当中,所有的字符串常量可能会包含重复的,而整合在一起则可以最大化减少重复的字符串等信息。
- 如果一个class文件依赖另一个class文件则需要加载另一个class文件,这就会造成I/O操作,因为一次性把所有class文件加载,可以使得I/O操作减少。
字节序
计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。
dex格式存储采用的是小端字节序,比如一个16进制数0x2211
,大端字节序跟该数字一致,采用高位字节在前低位字节在后。
而小端字节序则与大端字节序相反,采用高位字节在后低位字节在前,则该数位0x1122
同理0x987654
其小端字节序写做:0x456789
,即人一般读数是按大端字节序读取,而dex的读数是按小端字节序来读取的,其实这一块会由外部设备进行自动转换方便人的阅读,理解即可。
信息描述规则
数据类型描述(Type Descriptor)
数据类型描述说的是用字符串表示不同的数据类型。在这方面,Dex和Class文件格式没有区别。
- 原始数据类型对应的字符串描述为”B”,”C”,”D”,”F”,”I”,”J”,”S”,”Z”,它们分别对应的
Java类型为byte,char,double,float,int,long,short,boolean。 - “V”:表示void,不过只能用于表示函数的返回值类型。
- 引用数据类型的格式为”LClassName;”。此处的ClassName为对应类的全路径名。
- 数组用”[其他类型的描述名”来表示。Dex文件最多支持255维数组。
简短描述(Shorty Descriptor)
用来描述函数的参数和返回值信息,类似Class文件格式的MethodDescriptor。不过,Shorty Descriptor比MethodDescriptor要抠,省略了好些个字符。
定义ShortyDescriptor的描述规则,箭头后面是规则的组成:
- ShortyDescriptor → ShortyReturnType (ShortyFieldType)*
- ()在规则中表示一个Group,*号表示这个Group可以有0或多个
定义ShortyReturnType的描述规则:
ShortyReturnType → ‘V’ | ShortyFieldType
ShortyFieldType → ‘Z’ | ‘B’ | ‘S’ |’C’ | ‘I’ | ‘J’ | ‘F’ | ‘D’ |’L’
定义ShortyFieldType的描述规则,注意,引用类型统一用”L”表示即可
ShortyDescriptor是”返回值类型”+”参数类型”,如果有参数就会带参数类型,没有参数就只有返回值类型。
在ShortyDescriptor的ShortyFieldType中,引用类型只需要用”L”表示,显然,ShortyDescriptor对于那些参数或返回值类型为引用类型的函数将无法区分。不过没关系,Dex中还会提供其他数据来指明参数或返回值的具体类型。这种做法的原因其实还是为了减少字符串的使用。
DexHeader
1 | /* |
注:邓凡平先生主要把dex_header分成
上面的注释也主要围绕这个分发加注
header_item
header_item是一个二进制数据集合,邓凡平先生通过编程语言还原了其结构
magic: 取值必须是字符串“dex\n035\0”,或者byte数组{0x640x650x780x0a 0x300x330x350x00}
checksum: 文件内容的校验和。不包括magic和checksum自己。该字段的内容用于检查文件是否损坏。
signature: 签名信息,不包括magic、checksum和signature。该字段的内容用于检查文件是否被篡改。
file_size: 整个文件的长度,单位为字节,包括所有内容。
header_size: 默认是0x70个字节。
endian_tag: 表示文件内容应该按什么字节序来处理。默认取值为0x12345678,Little Endian格式。如果为Big Endian时,该字段取值为0x78563412。
xxx_id_item: 包括string_id_item、type_id_item、proto_id_item、field_id_item和method_id_item这五种数据结构,它们对应为string_ids、type_ids、proto_ids、field_ids和method_ids数组元素的数据类型。
string_data_item: utf16_size是字符串中字符的个数。Dex文件的字符串采用了变种的UTF8格式,对于英文字符和数字字符而言,都只占一个字节。data是字符串对应的内容。
string_id_item: 类似Class文件的CONSTANT_String类型,它只有一个成员。
string_data_off: 用于指明string_data_item位于文件的位置,也就是索引。
type_id_item: descriptor_idx是指向string_ids的索引。
field_id_item: class_idx和type_idx是指向type_ids的索引,而name_idx是指向string_ids的索引。
method_id_item: class_idx是指向type_ids的索引,proto_idx是指向proto_ids的索引,name_idx是指向string_ids的索引。
proto_id_item: shorty_idx是指向string_ids的索引,return_type_idx是指向type_ids的索引,如果parameters_off不为0,则文件对应的地方存储类型为type_list的结构,用于描述函数参数的类型。
type_item: type_idx是指向type_ids的索引。
type_list: size表示list数组的个数,而list数组元素类型为type_item。函数的每一个参数都对应一个type_item元素。
于proto_id_item: 首先,它的成员域shorty_idx(也就是ShortyDescriptor字符串)已经描述了参数和返回值的类型,但这只是简单描述,比如所有引用类型都用”L”统一表示,所以ShortyDescriptor肯定无法完整描述那种参数或者返回值类型为引用类型的函数。为解决此问题,proto_id_item中的return_type_idx用来描述返回值的数据类型,而参数的类型则通过parameters_off域(如果取值不为0,则表示该函数有参数)指向一个type_list。这个type_list为每个参数都存储了对应的数据类型(通过type_item中的type_idx来索引type_ids中的元素)。
class_def:
class_idx:指向type_ids,代表本类的类型。
superclass_idx:指向type_ids,代表基类类型,如果没有基类则取值为NO_INDEX(值为-1)。
interfaces_off:如果本类实现了某些接口,则interfaces_off指向文件对应位置,那里存储了一个type_list。该type_list的list数组存储了每一个接口类的type_idx索引(参考图3-5和对应的解释)。
source_file_idx:指向string_ids,该类对应的源文件名。
annotations_off:存储和注解有关的信息。
class_data_off:指向文件对应位置,那里将存储更细节的信息,由class_data_item类型来描述。
static_values_off:存储用来初始化类的静态变量的值,静态变量如果没有显示设置初值的话,默认是0或者null。如果有初值的话,初值信息就存储在文件static_values_off的地方,对应的数据结构名为encoded_array_item
一个类的成员变量、成员函数等信息则是通过class_data_off域指向一个名为class_data_item结构体来描述的。
static_fields:类的静态成员信息,元素类型为encoded_field。
instance_fields:类的非静态成员信息,元素类型为encoded_field。
direct_methods:非虚函数信息,元素类型为encoded_method。
virtual_methods:虚函数信息,元素类型为encoded_method。
encoded_field和encoded_method用于描述类的成员变量和成员函数的信息。
encoded_field:field_idx_diff指向field_ids。注意这里是field_idx_diff,它表示除数组里第一个元素的field_idx_diff取值为索引值,该数组后续元素field_idx_diff取值为和前一个索引值的差。access_flags表示成员域的访问标志
encoded_method:method_idx_diff指向method_ids。diff的含义与field_idx_diff一样。access_flags表示该函数的访问标志,code_off指向文件对应位置处,那里有一个类型为code_item的结构体,code_item类似于Class文件的Code属性。
access_flags取值范围
code_item
dex格式当中,我们需要了解的重中之重,因为函数抽取实际上就是把code_item进行抽空(置0)的方式实现的。
registers_size:此函数需要用到的寄存器个数。
ins_size:输入参数所占空间,以双字节为单位。
outs_size:该函数表示内部调用其他函数时,所需参数占用的空间。同样以双字节为单位。
insns_size和insns数组:指令码数组的长度和指令码的内容。Dex文件格式中JVM指令码长度为2个字节,而Class文件中JVM指令码长度为1个字节。
tries_size和tries数组:如果该函数内部有try语句块,则tries_size和tries数组用于描述try语句块相关的信息。注意,tries数组是可选项,如果tries_size为0,则此code_item不包含tries数组。
padding:用于将tries数组(如果有,并且insns_size是奇数长度的话)进行4字节对齐。
handlers:catch语句对应的内容,也是可选项。如果tries_size不为零才有handlers域。
padding则是需要对齐的时候才存在