一: 前言
文件数据分析离不开结构分析, 而结构定义权威参考当然是DexFile.h
/dalvik/libdex/DexFile.h
https://github.com/plum-umd/dexdump 有代码可以参考
二: 准备:
- 源文件java
$ cat hello.java
public class hello
{
public static void main(String[] argv)
{
System.out.println("hello world");
}
}
编译为class
$ java -c hello.java将class 变为dex 文件
$ dx –dex –output=hello.dex hello.class用davikvm 执行文件
将文件上传到手机: adb push hello.dex /data
登陆手机控制台,执行: davikvm -cp /data/hello.hex
-cp 是class path的简写
三. dex 文件格式分析.
3.0 文件整体结构
struct DexFile
{
DexHeader Header; //定义了各表的偏移地址及元素个数
DexStringId StringIds[stringIdsSize]; //修改过的utf8字符串定义
DexTypeId TypeIds[typeIdsSize]; //变量类型定义
DexProtoId ProtoIds[protoIdsSize]; //函数原型定义
DexFieldId FieldIds[fieldIdsSize]; //成员变量定义
DexMethodId MethodIds[methodIdsSize]; //成员方法定义
DexClassDef ClassDefs[classDefsSize]; //类定义
DexData Data[]; //数据,
DexLink LinkData; //链接数据
};
与java ClassFile 相比, 他把表的长度都集中到头部进行统一管理.
可以简单划分为3个部分,头部区,索引区,数据区
中间的从DexStringId 开始, 到DexClassDef[classDefsSize]为止都是索引区
在DexFile.h 中, DexFile 的结构定义略有差别, 它是为内存使用而定义的.
它能够更好的管理和使用文件中的数据.并且还有一些扩充. 这里是简单的hello分析
高级功能暂不涉及.内存中对应的项正是文件中相应项的映射.
typedef struct DexFile {
/* directly-mapped "opt" header */
const DexOptHeader* pOptHeader;
/* pointers to directly-mapped structs and arrays in base DEX */
const DexHeader* pHeader;
const DexStringId* pStringIds;
const DexTypeId* pTypeIds;
const DexFieldId* pFieldIds;
const DexMethodId* pMethodIds;
const DexProtoId* pProtoIds;
const DexClassDef* pClassDefs;
const DexLink* pLinkData;
/*
* These are mapped out of the "auxillary" section, and may not be
* included in the file.
*/
const DexClassLookup* pClassLookup;
DexIndexMap indexMap;
const void* pRegisterMapPool; // RegisterMapClassPool
/* points to start of DEX file data */
const u1* baseAddr;
/* track memory overhead for auxillary structures */
int overhead;
/* additional app-specific data structures associated with the DEX */
//void* auxData;
} DexFile;
下面具体看看那些结构元素.
3.1 头部信息0x70 bytes
struct DexHeader
{
u1 magic[8]; //field_0,1 ;dex 版本标识,"dex.035"
u4 checksum; //field_2, ;adler32 检验
u1 signature[20]; //f_3,4,5,6,7 ;SHA-1 哈希值,20个字节
u4 fileSize; //field_8 ;整个 dex 文件大小
u4 headerSize; //field_9 ;DexHeader 结构大小,0x70
u4 endianTag; //field_10 ;字节序标记,小端 "0x12345678",大端"0x78563412"
u4 linkSize; //field_11 ;链接段大小
u4 linkOff; //field_12 ;链接段偏移
u4 mapOff; //field_13 ;DexMapList 的偏移
u4 stringIdsSize; //field_14 ;DexStringId 的个数
u4 stringIdsOff; //field_15 ;DexStringId 的偏移 字符串
u4 typeIdsSize; //field_16 ;DexTypeId 的个数
u4 typeIdsOff; //field_17 ;DexTypeId 的偏移 类型
u4 protoIdsSize; //field_18 ;DexProtoId 的个数
u4 protoIdsOff; //field_19 ;DexProtoId 的偏移 声明
u4 fieldIdsSize; //field_20 ;DexFieldId 的个数
u4 fieldIdsOff; //field_21 ;DexFieldId 的偏移 字段
u4 methodIdsSize; //field_22 ;DexMethodId 的个数
u4 methodIdsOff; //field_23 ;DexMethodId 的偏移 方法
u4 classDefsSize; //field_24 ;DexClassDef 的个数
u4 classDefsOff; //field_25 ;DexClassDef 的偏移 类
u4 dataSize; //field_26 ;数据段的大小
u4 dataOff; //field_27 ;数据段的偏移
};
共28个DWORD, 0x70bytes
0000000: 6465 780a 3033 3500 db5e 64f4 14f0 bf23 dex.035..^d....#
0000010: b4c7 881c 1f88 a158 a1e7 a7ca 54fe a2d5 .......X....T...
//magic : 'dex.035'
//checksum : f4645edb
//signature : 14f0...a2d5
0000020: e002 0000 7000 0000 7856 3412 0000 0000 ....p...xV4.....
//file_size : 736(0x02e0), 文件长度.
//header_size : 112(0x70) , header 大小
//endian_tag : 0x12345678 小端, 显然
//link_size : 0
0000030: 0000 0000 4002 0000 0e00 0000 7000 0000 ....@.......p...
//link_off : 0 (0x000000) ,
//查资料知,link_size,link_off 在静态链接时有值,这里是动态链接,故为0
//map_off : 0x0240 , map表偏移
//string_ids_size : 14 , 14 个string, 见后.
//string_ids_off : 112 (0x000070),从偏移0x70开始,紧接头部
0000040: 0700 0000 a800 0000 0300 0000 c400 0000 ................
//type_ids_size : 7 , 7 个type(类型), 从0xa8开始
//type_ids_off : 168 (0x0000a8)
//proto_ids_size : 3 , 3 个proto(函数原型),从0xc4
//proto_ids_off : 196 (0x0000c4)
0000050: 0100 0000 e800 0000 0400 0000 f000 0000 ................
//field_ids_size : 1 , 1 个field(字段),从0xe8开始
//field_ids_off : 232 (0x0000e8)
//method_ids_size : 4 , 4 个method(方法),从0xf0开始
//method_ids_off : 240 (0x0000f0)
0000060: 0100 0000 1001 0000 b001 0000 3001 0000 ............0...
//class_defs_size : 1 , 1 个类定义, 从0x110开始
//class_defs_off : 272 (0x000110)
//data_size : 432 , 数据,从0x130 开始
//data_off : 304 (0x000130)
找到数据所在, 理解数据含义才算理解.
下面是14个strings
3.2 DexStringId
typedef struct _DexStringId
{
u4 stringDataOff; // 指向MUTF-8 字符串的偏移(修改过的utf8字符串)
}DexStringId, *PDexStringId;
我们看到,它索引的是地址偏移.占4个bytes
从0x70到0xa7, 因为0xa8是type_ids_off, 每个string 占4byte, 14个string
从0x70 正好到0xa7
0000070: 7601 0000 7e01 0000 8701 0000 9e01 0000 v...~...........
0000080: b201 0000 c601 0000 da01 0000 dd01 0000 ................
0000090: e101 0000 f601 0000 0402 0000 1002 0000 ................
00000a0: 1602 0000 1b02 0000
顺便把string 也拿过来吧. 这些string, 位于数据区了!
0000170: 0100 0000 0600 063c 696e 6974 3e00 074c .......<init>..L
0000180: 6865 6c6c 6f3b 0015 4c6a 6176 612f 696f hello;..Ljava/io
0000190: 2f50 7269 6e74 5374 7265 616d 3b00 124c /PrintStream;..L
00001a0: 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 java/lang/Object
00001b0: 3b00 124c 6a61 7661 2f6c 616e 672f 5374 ;..Ljava/lang/St
00001c0: 7269 6e67 3b00 124c 6a61 7661 2f6c 616e ring;..Ljava/lan
00001d0: 672f 5379 7374 656d 3b00 0156 0002 564c g/System;..V..VL
00001e0: 0013 5b4c 6a61 7661 2f6c 616e 672f 5374 ..[Ljava/lang/St
00001f0: 7269 6e67 3b00 0c68 656c 6c6f 2077 6f72 ring;..hello wor
0000200: 6c64 0a00 0a68 656c 6c6f 2e6a 6176 6100 ld...hello.java.
0000210: 046d 6169 6e00 036f 7574 0007 7072 696e .main..out..prin
0000220: 746c 6e00 0100 070e 0004 0100 070e 7800 tln...........x.
我们看到
0. 00000176->06, 后面跟6字节
1. 0000017e->07, 后面跟7字节Lhello;
2. 00000187->15, 后面跟0x15字节Ljava/io/PrintStream;
3. 0000019e->12, 后面跟0x12字节Ljava/lang/Object;
4. 000001b2->12, 后面跟0x12字节Ljava/lang/String;
5. 000001c6->12, 后面跟0x12字节Ljava/lang/System;
6. 000001da->01, 后面跟0x1字节V
7. 000001dd->02, 后面跟0x2字节VL
8. 000001e1->18, 后面跟0x13字节[Ljava/lang/String;
9. 000001f6->0c, 后面跟0xc字节hello world
10. 00000204->0a, 后面跟0xa字节hello.java
11. 00000210->04, 后面跟0x4字节main
12. 00000216->03, 后面跟6字节out
13. 0000021b->07, 后面跟6字节println
所以说mutf8 是说前面是长度,后面是utf8字符串,前面的长度是leb128编码.
leb128是little endian base 128, 是一种变长编码,只用低7位表示有效数据,
最高位为1表示有后续byte, 为0表示无后续byte. 最多占用5byte,表示一个32位数.
用leb128编码长度更灵活.
3.3 DexTypeId
typedef struct _DexTypeId
{
u4 descriptorIdx; // 指向 DexStringId 列表的索引
}DexTypeId, *PDexTypeId;
7个类型,0xa8始,0xe8止
它索引的是个序号,如果字符串不是很多,4byte数据高位肯定是0.
00000a0: 0100 0000 0200 0000 ................
00000b0: 0300 0000 0400 0000 0500 0000 0600 0000 ................
00000c0: 0800 0000
分别是
0. ->1 “Lhello;”
1. ->2 “Ljava/io/PrintStream;
2. ->3 “Ljava/lang/Object;”
3. ->4 “Ljava/lang/String;”
4. ->5 “Ljava/lang/System;”
5. ->6 “V”
6. ->8 “[Ljava/lang/String;”
共7个字符串,已经把参考字符串copy过来了
3.4 DexProtoId
typedef struct _DexProtoId
{
u4 shortyIdx; // 方法声明字符串,指向 DexStringId
u4 returnTypeIdx; // 方法返回类型字符串,指向 DexTypeId
u4 parametersOff; // 方法的参数列表,指向 DexTypeList
}DexProtoId, *PDexProtoId;
函数原型既有字符串索引,索引名字,又有序号索引,索引返回值类型,还有函数参数列表索引,该索引是一个地址偏移量,指向一个DexTypeList结构,DexTypeList 结构数据位于数据区了,更详细的见后述.
3 个proto(函数原型)
00000c0: 0600 0000 0500 0000 0000 0000 ................
\0.
6 -> “V”
5 -> 6 -> “V”
0 -> 无参数
00000d0: 0700 0000 0500 0000 6801 0000 0700 0000 ........h.......
\1.
7 -> “VL”
5 -> 6 -> “V”
0x168 ->参数0x168 -> Ljava/lang/String
\2.
7 -> “VL”
5 -> 6 -> “V”
0x170 ->参数0x170 -> [Ljava/lang/String
00000e0: 0500 0000 7001 0000 ....p...........
info: 参数列表
typedef struct DexTypeList {
u4 size; /* #of entries in list */
DexTypeItem list[1]; /* entries */ ,实际上是跟size 个项
} DexTypeList;
typedef struct DexTypeItem {
u2 typeIdx; /* index into typeIds */
} DexTypeItem;
0000160: 0100 0000 0300 0000 n …………..
0x168处参数列表只有1个, 03 -> 04 -> Ljava/lang/String;
0000170: 0100 0000 0600
0x170处参数列表只有1个, 06 -> 08 -> [Ljava/lang/String;
这里,我要强调一下这个参数表的转换过程!
上一个gdb脚本, 分析一下内存结构.
set $j=((DexTypeList *)(pDexFile->baseAddr + pDexFile->pProtoIds[2].parametersOff))->list[0].typeIdx
set $k= pDexFile->pTypeIds[$j].descriptorIdx
p pDexFile->baseAddr + pDexFile->pStringIds[$k].stringDataOff+1
下面分析这个脚本
假如dex文件被加载到内存, pDexFile 结构均指向正确数据.
pDexFile->pProtoIds[2] 为第二个函数原型
pDexFile->pProtoIds[2].parametersOff 为第2个函数原型的参数列表的偏移量
pDexFile->baseAddr + pDexFile->pProtoIds[2].parametersOff, 为第2个函数原型的参数列表在内存中位置
这个位置是用u4定义的,把它强制转化为(DexTypeList *)
((DexTypeList *)(pDexFile->baseAddr + pDexFile->pProtoIds[2].parametersOff))->list[0].typeIdx
取出第2个函数原型参数列表的第一个参数的typeIdx
set $k= pDexFile->pTyp[$j].descriptorIdx
把typeIdx转换为stringIds index. 并付给$k
p pDexFile->baseAddr + pDexFile->pStringIds[$k].stringDataOff+1
简单描述为: 取到$k字符串的内存地址,加1是为了跳过第一个长度字节(已假定字符串不超过128).
则后面的utf8字符串会被打印出来.
3.5 DexFieldId
typedef struct _DexFieldId
{
u2 classIdx; // 类的类型,索引 DexTypeId 列表
u2 typeIdx; // 字段的类型,索引 DexTypeId 列表
u4 nameIdx; // 字段名,索引 DexStringId 列表
}DexFieldId, *PDexFieldId;
成员变量的索引定义了所属类的索引,类型的所以,名称的索引.
这里只用了2个bytes来索引类,2个byte来索引类型,节省了空间.也要求类型个数不能超过65536个. 这个一般能满足.
1 个field(字段),从0xe8开始
00000e0: 0400 0100 0c00 0000 ....p...........
classIdx : 4 -> 5 -> “Ljava/lang/System;”
typeIdx : 1 -> 2 -> “Ljava/io/PrintStream;”
nameIdx : 12 -> “out”
故为Ljava/lang/System 类out 字段, 类型Ljava/io/PrintStream;
3.6 DexMethodId
struct field_id_item
{
u2 classIdx; // 类的类型,索引 DexTypeId 列表
u2 protoIdx; // 声明的类型,索引 DexProtoId 列表
u4 nameIdx; // 方法名,索引 DexStringId 列表
}
成员函数定义了所属类的索引,所属函数原型的索引及名称索引, 跟成员变量很相似,它也要求类的个数及函数原型的个数不能超过65536, 因为它也是用2byte来索引的.
4 个method(方法),从0xf0开始
00000f0: 0000 0000 0000 0000 0000 0200 0b00 0000 ................
0. void hello()
1.
0000100: 0100 0100 0d00 0000 0200 0000 0000 0000 ................
3.7 DexClassDef
struct DexClassDef
{
u4 classIdx; // 类的类型,指向 DexTypeId 列表的索引
u4 accessFlags; // 访问标志
u4 superclassIdx; // 父类类型,指向 DexTypeId 列表的索引
u4 interfacesOff; // 接口,指向 DexTypeList 的偏移,否则为0
u4 sourceFileIdx; // 源文件名,指向 DexStringId 列表的索引
u4 annotationsOff; // 注解,指向 DexAnnotationsDirectoryItem 结构,或者为 0
u4 classDataOff; // 指向 DexClassData 结构的偏移,类的数据部分
u4 staticValuesOff; // 指向 DexEncodedArray 结构的偏移,记录了类中的静态数据,主要是静态方法
}
类定义比较复杂,因为它的索引结构包含了8个字段.
本类和父类的名称用类型序号表示,访问标志用一个整数表示.
一个类一般会对应一个源文件,源文件名用一个字符串序号表示,接口用dexTypelist偏移表示,前面说过.这个也是指向数据区的. 另外还有三个指向数据区的偏移:
标注的偏移,静态数据的偏移,这两个先不做分析,还有一个重要的偏移是类数据的偏移. 分析见后.
1 个类定义, 从0x110开始
0000110: 0000 0000 0100 0000 0200 0000 0000 0000 ................
0000120: 0a00 0000 0000 0000 2f02 0000 0000 0000 ......../.......
class_idx : 0
access_flags : 1 (0x0001)
superclass_idx : 2
interfaces_off : 0 (0x000000)
source_file_idx : 10
annotations_off : 0 (0x000000)
class_data_off : 559 (0x00022f)
staticValuesOff : 0 (0x000000)
class_data 会对应到如下信息, 见后
static_fields_size : 0
instance_fields_size: 0
direct_methods_size : 2
virtual_methods_size: 0
3.8 数据区(0x130开始)
!:codeItem,0x02项,偏移0x0130
0000130: 0100 0100 0100 0000 2302 0000 0400 0000 ........#.......
0000140: 7010 0300 0000 0e00 0300 0100 0200 0000 p...............
0000150: 2802 0000 0800 0000 6200 0000 1a01 0900 (.......b.......
0000160: 6e20 0200 1000 0e00 n ..............
code区现在先不做分析
!:typeList,0x02项,偏移0x0168
0000160: 0100 0000 0300 0000 n ..............
0000170: 0100 0000 0600 .......<init>..L
前面已经分析过了. typelist 还是直接映射的,先定义大小,每个ITEM大小用2byte来索引
!:stringDataItem,0x0e项,偏移0x0176
0000170: 063c 696e 6974 3e00 074c .......<init>..L
0000180: 6865 6c6c 6f3b 0015 4c6a 6176 612f 696f hello;..Ljava/io
0000190: 2f50 7269 6e74 5374 7265 616d 3b00 124c /PrintStream;..L
00001a0: 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 java/lang/Object
00001b0: 3b00 124c 6a61 7661 2f6c 616e 672f 5374 ;..Ljava/lang/St
00001c0: 7269 6e67 3b00 124c 6a61 7661 2f6c 616e ring;..Ljava/lan
00001d0: 672f 5379 7374 656d 3b00 0156 0002 564c g/System;..V..VL
00001e0: 0013 5b4c 6a61 7661 2f6c 616e 672f 5374 ..[Ljava/lang/St
00001f0: 7269 6e67 3b00 0b68 656c 6c6f 2077 6f72 ring;..hello wor
0000200: 6c64 000a 6865 6c6c 6f2e 6a61 7661 0004 ld..hello.java..
0000210: 6d61 696e 0003 6f75 7400 0770 7269 6e74 main..out..print
0000220: 6c6e 00
字符串数据前面分析过,为mutf8格式, 字符串的长度u4已经采用了leb128编码. 从空间占用上整体会减小.不过先不要嘲笑它减小不了多少空间.
!:debugInfoItem,0x02项,偏移0x0223
0000220: 01 0007 0e00 0501 0007 0e78 0000 ln...........x..
debug 信息现在不做分析!
!:classDataItem,0x01项,偏移022f
0000220: 00 ln...........x..
0000230: 0002 0000 8180 04b0 0201 09c8 0200 0000 ................
typedef struct DexClassData {
DexClassDataHeader header;
DexField* staticFields;
DexField* instanceFields;
DexMethod* directMethods;
DexMethod* virtualMethods;
} DexClassData;
typedef struct DexClassDataHeader {
u4 staticFieldsSize;
u4 instanceFieldsSize;
u4 directMethodsSize;
u4 virtualMethodsSize;
} DexClassDataHeader;
但这是uleb128编码长度. 结果如下,见后面分析
staticFieldsSize :00
instanceFieldsSize :00
directMethodsSize :02 2个方法
virtualMethodsSize :00
//类数据成员方法结构定义
typedef struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4 accessFlags;
u4 codeOff; /* file offset to a code_item */
} DexMethod;
但对应数据仍然采用uleb128编码
类数据结构定义了类的成员方法索引,访问标志和代码偏移
类数据的大小,索引,访问标志及偏移地址都采用了uleb128编码来代替直接的u4编码
0.
methodIdx -> uleb128(00) -> 0x0
accessFlag -> uleb128(81 80 04) -> 0x010001
codeOff -> uleb128(b0 02) -> 0x130
1.
methodIdx -> uleb128(01) -> 0x1
accessFlag -> uleb128(09) -> 0x09
codeOff -> uleb128(c8 02 )-> 0x0148
3.9 map 数据, 从0x240 开始, 到文件尾.
struct DexMapList
{
u4 size; // DexMapItem 的个数
DexMapItem list[1]; // DexMapItem 结构
};
struct DexMapItem
{
u2 type; // kDexType 类型变量
u2 unused; // 未使用,用于对齐
u4 size; // 指定类型的个数
u4 offset; // 指定类型数据的文件偏移
};
typedef enum
{
kDexTypeHeaderItem = 0x0000, // 对应 DexHeader
kDexTypeStringIdItem = 0x0001, // 对应 stringIdsSize 与 stringIdsOff 字段
kDexTypeTypeIdItem = 0x0002, // 对应 typeIdsSize 与 typeIdsOff 字段
kDexTypeProtoIdItem = 0x0003, // 对应 protoIdsSize 与 protoIdsOff 字段
kDexTypeFieldIdItem = 0x0004, // 对应 fieldIdsSize 与 fieldIdsOff 字段
kDexTypeMethodIdItem = 0x0005, // 对应 methodIdsSize 与 methodIdsOff 字段
kDexTypeClassDefItem = 0x0006, // 对应 classDefsSize 与 classDefsOff 字段
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodeArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006
}kDexType;
0000240: 0d00 0000 0000 0000 0100 0000 0000 0000 ................
!:有0xd个Map项, DexHeader,1项,偏移为0
0000250: 0100 0000 0e00 0000 7000 0000 0200 0000 ........p.......
!:stringId,0x0e项,偏移为0x70,
!:typeId, 0x07项,偏移为0xa8
0000260: 0700 0000 a800 0000 0300 0000 0300 0000 ................
!:protoId,0x3项,偏移为0xc4
0000270: c400 0000 0400 0000 0100 0000 e800 0000 ................
!:fieldId,0x1项,偏移为0xe8
0000280: 0500 0000 0400 0000 f000 0000 0600 0000 ................
!:methodId,0x4项,偏移0xf0
!:classDef,0x1项,偏移0x110
0000290: 0100 0000 1001 0000 0120 0000 0200 0000 ......... ......
!:codeItem,0x02项,偏移0x0130
00002a0: 3001 0000 0110 0000 0200 0000 6801 0000 0...........h...
!:typeList,0x02项,偏移0x0168
00002b0: 0220 0000 0e00 0000 7601 0000 0320 0000 . ......v.... ..
!:stringDataItem,0x0e项,偏移0x0176
!:debugInfoItem,0x02项,偏移0x0223
00002c0: 0200 0000 2302 0000 0020 0000 0100 0000 ....#.... ......
!:classDataItem,0x01项,偏移022f
00002d0: 2f02 0000 0010 0000 0100 0000 4002 0000 /...........@...
typeMapList,0x01项,偏移0x0240
前面的map项与头定义是重复的,后面的map项对数据区又进一步进行了划分.
从0x130开始,到0x240止.又多了codeItem, typeList, stringDataItem,
debugInfoItem, classDataItem
至此hello.dex 才算分析完毕! 来点总结发言吧.
dex文件格式分为三个部分,头部,索引区,数据区.
以常量字符串为基础,定义了字符串表.
名称,类型以字符串序号为索引,
类型表是类型的数组.
参数列表定义的是size, 类型表索引,参数列表在数据区.
函数原型表元素包含了函数名,返回类型,参数列表.
成员变量表项包含了类名,类型及名称
成员函数表项包含了类名,函数原型,及名称.
类定义表项主要包含了类名,父类名,访问标志.类数据偏移. 还有类所在的文件名,接口,注释,静态数据等较次要信息.
类数据由它的头部各size定义及对应的数据实体构成.
在hello.dex中重点关注了dexMethod 实体,它由methodIdx, 访问标志及代码偏移来构成.
它们就是这样以表格的形式,被一层层定义和索引,
最后map表,指出了整个文件的不同区域划分!