dex文件格式
dex文件中的数据结构
类型 | 含义 |
---|---|
u1 | 等同uint8_t,表示1字节的无符号数 |
u2 | 等同于uint16_t,表示2字节的无符号数 |
u4 | 等同于uint32_t,表示4字节的无符号数 |
u8 | 等同于uint64_t,表示8字节的无符号数 |
sleb128 | 有符号LEB128,可变长度1~5字节 |
uleb128 | 无符号LEB128,可变长度1~5字节 |
uleb128p1 | 无符号LEB128值加1,可变长度1~5字节 |
每个LB125由1~5个字节组成,所有的字节组合在一起表示一个32位的数据。每个字节只有7位有效位,如果第一个字节的最高位为1,表示LEB128需要使用到第2个字节,如果LEB第二个字节的最高位为1,表示会使用到第3个字节,以此类推。LEB128最多使用5个字节,如果读取5个字节后下一个字节最高位仍为1,则表示该dex文件无效。
以字符序列“c0 83 92 25”为例,计算它的uleb128的值:
第1个字节0xc0 > 0x7c ,表示会用到第2个字节。result = 0x0c & 0x7f
第2个字节0x83 > 0x7c,表示会用到第3个字节。result = result + 0x83 & 0x7f << 7
第3个字节0x92 > 0x7c,表示会用到第4个字节。result = result + 0x92 & 0x7c << 14
第4个字节0x25 < 0x7c,表示到了结尾。result = result + 0x25 & 0x7c << 21
计算结果为0x40 + 0x180 + 0x48000 +0x4a00000 = 0x4a481c0
以字符序列“d1 c2 b3 40”为例,计算它的sleb128值。
result = 0xd1 & 0x7f
result=result + 0xc2 & 0x7f << 7
result=result + b3 & 0x7f << 14
result=(result + 40 & 0x7f <<21)<<4)>>4
dex文件整体结构
dex文件由7个部分组成。dex header为dex文件头,它指定了dex文件的一些属性,并记录了其他6部分数据结构在dex文件中的物理偏移。string_ids
和 class_def
结构可以裂解为索引结构区,真实的数据存放在data中。最后的link_data
为静态链接数据区,对于目前生成的dex文件而言,它始终为空。
dex head |
---|
string_ids |
type_ids |
proto_ids |
field_ids |
method_ids |
class_def |
data |
link_data |
未经过优化的dex文件结构表示如下:
struct DexFile {
DexHeader header;
DexStringId StringIds[StringIdsSize];
DexTypeId TypeIds[typeIdsSize];
DexProtoId ProtoIds[protoIdsSize];
DexFieldId FieldIds[fieldIdsSize];
DexMethodId MethodIds[methodIdsSize];
DexClassDef Data[];
DexLink LinkData;
}
DexFile结构的声明在Android系统源码dalvik\libdex\DexFile.h文件中。
dex文件头
字段名称 | 偏移量字节 | 长度(byte) | 字段描述 |
---|---|---|---|
magic[8] | 0x0 | 0x8 | dex版本标识 |
checksum | 0x8 | 0x4 | alder32算法, 去除了magic和checksum 字段之外的所有内容的校验码 |
signature | 0xc | 0x14 | sha-1签名, 去除了magic、checksum和 signature字段之外的所有内容的签名 |
fileSize | 0x20 | 0x4 | 整个dex的文件大小 |
headerSize | 0x24 | 0x4 | 整个dex文件头的大小 (固定大小为0x70) |
endianTag | 0x28 | 0x4 | 字节序 (大尾方式、小尾方式) 默认为小尾方式 <--> 0x12345678 |
linkSize | 0x2c | 0x4 | 链接段的大小, 默认为0表示静态链接 |
linkOff | 0x30 | 0x4 | 链接段开始偏移 |
mapOff | 0x34 | 0x4 | DexMapList的文件偏移 |
stringIdsSize | 0x38 | 0x4 | DexStringId个数 |
stringIdsOff | 0x3c | 0x4 | DexStringId偏移 |
typeIdsSize | 0x40 | 0x4 | DexTypeId个数 |
typeIdsOff | 0x44 | 0x4 | DexTypeId偏移 |
protoIdsSize | 0x48 | 0x4 | DexProtoId个数 |
protoIdsOff | 0x4c | 0x4 | DexProtoId偏移 |
fieldIdsSize | 0x50 | 0x4 | DexFieldId个数 |
fieldIdsOff | 0x54 | 0x4 | DexFieldId偏移 |
methodIdsSize | 0x58 | 0x4 | DexMethodId个数 |
methodIdsOff | 0x5c | 0x4 | DexMethodId偏移 |
classDefsSize | 0x60 | 0x4 | DexClassDef个数 |
classDefsOff | 0x64 | 0x4 | DexClassDef偏移 |
dataSize | 0x68 | 0x4 | 数据段大小 |
dataOff | 0x6c | 0x4 | 数据段偏移 |
DexHeader结构下面的数据为“索引结构区”与“数据区”。
“索引结构区”中各数据结构的偏移地址都是从DexHeader结构的stringIdsOff~classDefsOff字段的值指定的,它们并非真正的类数据,而是指向dex文件的data数据区的偏移或数据结构索引。
DexHeader结构声明如下:
struct DexHeader {
u1 magic[8];
u4 checksum;
u1 signature[kSHA1DigestLen];
u4 fileSize;
u4 headerSize;
u4 endianTag;
u4 linkSize;
u4 linkOff;
u4 mapOff;
u4 stringIdsSize;
u4 stringIdsOff;
u4 typeIdsSize;
u4 typeIdsOff;
u4 protoIdsSize;
u4 protoIdsOff;
u4 fieldIdsSize;
u4 fieldIdsOff;
u4 methodIdsSize;
u4 methodIdsOff;
u4 classDefsSize;
u4 classDefsOff;
u4 dataSize;
u4 dataOff;
};
dex文件结构分析
DexMapItem
Dalvik虚拟机解析dex文件的内容,最终将其映射成DexMapList数据结构,DexHeader结构的mapOff字段指明了DexMapList结构在dex文件中的偏移,它的声明如下。
struct DexMapList {
u4 size; /* DexMapItem的个数 */
DexMapItem list[1]; /* DexMapItem的结构 */
};
size字段表示接下来有多少个DexMapItem结构,DexMapItem的结构声明如下。
struct DexMapItem {
u2 type; /* kDexType开头的类型 */
u2 unused; /*未使用,用于字节对齐 */
u4 size; /* 指定类型的个数 */
u4 offset; /* 指定类型的文件偏移 */
};
type字段为一个枚举常量,如下所示,通过类型名称很容易判断它的具体类型。
enum {
kDexTypeHeaderItem = 0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
};
以hello.dex为例,用winhex打开文件。
在偏移0x34处找到mapOff,即MapList的偏移:0x0290
读取0x290处的一个双字值为0x0d,表明接下来会有13个DexMapItem。(此处占四个字节?)
根据前文中提到的DexMapItem结构,一个DexMapItem占12个字节,1-2个字节表示类别,5-8个字节表示个数,9-12个字节表示偏移量。
所有DexMapItem如下表:
类型 | 个数 | 偏移 |
---|---|---|
kDexTypeHeaderItem | 0x1 | 0x0 |
kDexTypeStringIdItem | 0x10 | 0x70 |
kDexTypeTypeIdItem | 0x7 | 0xb0 |
kDexTypeProtoIdItem | 0x4 | 0xcc |
kDexTypeFieldIdItem | 0x1 | 0xfc |
kDexTypeMethodIdItem | 0x5 | 0x104 |
kDexTypeClassDefItem | 0x1 | 0x12c |
kDexTypeCodeItem | 0x3 | 0x1b4 |
kDexTypeTypeList | 0x3 | 0x1ca |
kDexTypeStringDataItem | 0x10 | 0x16c |
kDexTypeDebugInfoItem | 0x3 | 0x267 |
kDexTypeClassDataItem | 0x1 | 0x27b |
kDexTypeMapList | 0x1 | 0x290 |
kDexTypeHeaderItem描述了DexHeader部分,它占用了文件0x70个字节的空间。而接下来的kDexTypeStringIdItem~kDexTypeClassDataItem与DexHeader当中对应的类型个数字段的值是相同的。
kDexTypeStringIdItem
比如kDexTypeStringIdItem对应了DexHeader的stringIdsSize与stringIdsOff字段,表明了在0
x70偏移处,有0x10个DexStringId对象。DexStringId结构的声明如下。
struct DexStringId {
u4 stringDataOff; /* 字符串数据偏移 */
};
DexStringId结构只有一个stringDataOff字段,直接指向字符串数据,从0x70开始,有0x10个DexStringId对象,一个对象是4个字节代表字符串偏移。一共16个字段如下:
序号 | 偏移 | 字符串 |
---|---|---|
0x0 | 0x1ca | <init> |
0x1 | 0x1d2 | Hello.java |
0x2 | 0x1de | I |
0x3 | 0x1e1 | III |
0x4 | 0x1e6 | LHello; |
0x5 | 0x1ef | Ljava/io/PrintStream; |
0x6 | 0x206 | Ljava/lang/Object; |
0x7 | 0x21a | Ljava/lang/System; |
0x8 | 0x22e | V |
0x9 | 0x231 | VI |
0xa | 0x235 | VL |
0xb | 0x392 | [Ljava/lang/String; |
0xc | 0x24e | foo |
0xd | 0x253 | main |
0xe | 0x259 | out |
0xf | 0x25e | println |
上表中的字符串并非普通的ascii字符串,他们是有MUTF-8编码表示的。MUTF-8含义为Modified UTF-8。
kDexTypeTypeIdItem
他对应DexHeader中的typeIdsSize和typeIdsOff字段,指向的结构体为:
struct DexTypeId {
u4 descriptorIdx; /* 指向DexStringId列表的索引 */
};
descriptorIdx
是指向DexStringId列表的索引 ,他对应的字符串代表具体类的类型,我们根据上面字段可知:从0xb0起有0x7个DexTypeId结构:
类型索引 | 字符串索引 | 字符串 |
---|---|---|
0 | 0x2 | I |
1 | 0x4 | LHello; |
2 | 0x5 | Ljava/io/PrintStream; |
3 | 0x6 | Ljava/lang/Object; |
4 | 0x7 | Ljava/lang/System; |
5 | 0x8 | V |
6 | 0xb | [Ljava/lang/String; |
kDexTypeProtoIdItem
对应DexHeader中的protoIdsSize与protoIdsOff字段,声明如下:
struct DexProtoId {
u4 shortyIdx; /* 指向DexStringId列表的索引 */
u4 returnTypeIdx; /* 指向DexTypeId列表的索引 */
u4 parametersOff; /* 指向DexTypeList的偏移 */
};
他是一个方法的声明结构体,shortyIdx为方法声明字符串,returnTypeIdx为方法返回类型字符串,parametersOff指向一个DexTypeList的结构体存放了方法的参数列表 。DexTypeList声明如下:
struct DexTypeList{
u4 size; /* 接下来DexTypeItem的个数 */
DexTypeItem list[1]; /* DexTypeItem结构 */
};
DexTypeItem声明如下:
struct DexTypeItem {
u2 typeIdx; /* 指向DexTypeId列表的索引 */
};
从0xcc开始,有4个DexProtoId,如表:
DexProtoId结构表
索引 | 方法声明 | 返回类型 | 参数列表 |
---|---|---|---|
0 | III |
I |
2个参数I 、I
|
1 | V |
V |
无参数 |
2 | VI |
V |
1个参数I
|
3 | VL |
V |
1个参数[Ljava/lang/String;
|
通过上表可以发现,方法声明由返回类型与参数列表组成,并且返回类型位于参数列表的前面。
kDexTypeFieldIdItem
对应DexHeader中的fieldIdsSize和fieldIdsOff字段,指向DexFieldId结构体 :
struct DexFieldId {
u2 classIdx; /* 类的类型,指向DexTypeId列表索引 */
u2 typeIdx; /* 字段类型,指向DexTypeId列表索引 */
u4 nameIdx; /* 字段名,指向DexStringId列表 */
};
DexFieldId结构体中的数据全部是索引值,指明了字段所在的类,字段的类型以及字段名,从0xfc开始共有1个DexFieldId结构。结果如图:
类类型 | 字段类型 | 字段名 |
---|---|---|
Ljava/lang/System; |
Ljava/io/PrintStream; |
out |
kDexTypeMethodIdItem
它对应DexHeader中的methodIdsSize与methodIdsOff字段,指向的结构体DexMethodId
struct DexMethodId {
u2 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u2 protoIdx; /* 声明类型,指向DexProtoId列表索引 */
u4 nameIdx; /* 方法名,指向DexStringId列表的索引 */
};
数据也是索引,指明了方法所在的类,方法声明和方法名。从0x104有0x5个kDexTypeMethodIdItem
类类型 | 方法声明 | 方法名 |
---|---|---|
LHello |
V |
<init> |
LHello |
III |
foo |
LHello |
VL |
main |
Ljava/io/PrintStream; |
VI |
println |
Ljava/lang/Object; |
V |
<init> |
kDexTypeClassDefItem
对应DexHeader中的classDefsSize和classDefsOff字段,指向结构体DexClassDef
struct DexClassDef {
u4 classIdx; /* 类的类型,指向DexTypeId列表索引 */
u4 accessFlags; /* 访问标志 */
u4 superclassIdx; /* 父类的类型,指向DexTypeId列表的索引 */
u4 interfacesOff; /* 实现了哪些接口,指向DexTypeList结构的偏移 */
u4 sourceFileIdx; /* 源文件名,指向DexStringId列表的索引 */
u4 annotationsOff; /* 注解,指向DexAnnotationsDirectoryItem结构的偏移 */
u4 classDataOff; /* 指向DexClassData结构的偏移 */
u4 staticValuesOff; /* 指向DexEncodedArray结构的偏移 */
};
-
classIdx
:索引值,表明类的类型 -
accessFlags
:类的访问标志,它是以ACC_
开头的一个枚举值 -
superclassIdx
:父类类型索引值, -
interfacesOff
:如果类中含有接口声明或实现,字段会指向1个DexTypeList结构,否则这里为0 -
sourceFileIdx
:字符串索引值,表示类所在的源文件名称 -
annotationsOff
:指向注解目录结构 -
classDataOff
:指向DexClassData结构,这是数据部分 -
staticValuesOff
:指向DexEncodedArray结构,记录了类中的静态数据
DexClassData
struct DexClassData {
DexClassDataHeader header; //指向DexClassDataHeader,字段和方法个数
DexField* staticFields; //静态字段
DexField* instanceFields; //实例字段
DexMethod* directMethods; //直接方法
DexMethod* virtualMethods;//虚方法
};
DexClassDataHeader
记录了当前类中的字段和方法的数目,声明如下:
struct DexClassDataHeader {
u4 staticFieldsSize; //静态字段个数
u4 instanceFieldsSize;//实例字段个数
u4 directMethodsSize;//直接方法个数
u4 virtualMethodsSize;//虚方法个数
};
DexField
描述了字段类型与访问标志,声明如下:
struct DexField {
u4 fieldIdx; /* 指向DexFieldId列表的索引 */
u4 accessFlags; /* 访问标志 */
};
DexMethod
struct DexMethod {
u4 methodIdx; /* 指向DexMethodId列表的索引 */
u4 accessFlags; /* 访问标志 */
u4 codeOff; /* 指向DexCode结构的偏移 */
};
codeOff字段指向一个DexCode结构体,声明如下:
/dalvik/libdex/DexFile.h
struct DexCode {
u2 registersSize;//使用寄存器个数
u2 insSize;//参数个数
u2 outsSize;//调用其他方法时使用的寄存器个数
u2 triesSize;//try/catch个数
u4 debugInfoOff;//指向调试信息的偏移
u4 insnsSize;//指令集个数,以2字节为单位
u2 insns[1];//指令集
/* followed by optional u2 padding */
/* followed by try_item[triesSize] */
/* followed by uleb handlersSize */
/* followed by catch_handler_item[handlersSize] */
};