Author: geneblue
Blog: https://geneblue.github.io/
为了更好了解DEX文件格式,写了个java版的DEX解析器,可以打印出一个dex文件中的字符串,字段和类中调用了哪些方法。具体代码见这里
0X01 准备工作
java文件为Hello.java:
public class Hello {
public int foo(int a, int b) {
return (a + b) * (a - b);
}
public static void main(String[] argc) {
Hello hello = new Hello();
System.out.println(hello.foo(5, 3));
}
}
将java文件编译成class字节码文件,然后将class字节码转换成dex字节码文件,命令如下:
javac -source 1.6 -target 1.6 Hello.java //这里需要指定JDK的版本号,不然会出错
dx.bat --dex --output=Hello.dex Hello.class
0X02 整体分析
DEX文件的结构图如下:
从上图可以看出DEX文件结构共包含9个部分,MapSection是包含在DataSection中的。
将生成的Hello.dex文件用任意的16进制编辑器打开后就会得到如下的编码:
0X03 详细分析
DexHeader
字段名称 | 偏移值 | 长度 | 描述 |
magic | 0x0 | 8 | 魔数字段,值为“dex/n035/0”标识文件类型 目前android dex文件版本为035 |
signature | 0xC | 20 | SHA-1签名值 |
file_size | 0x20 | 4 | 此dex文件的大小 |
header_size | 0x24 | 4 | dex文件头长度,035版本是0x70个字节 |
endian_tag | 0x28 | 4 | 表明数据存储方式是大端还是小端,默认值为0x78563412 |
link_size | 0x2C | 4 | 连接段的大小,如果为0就表示是静态连接。 |
link_of | 0x30 | 4 | 连接段的开始位置,从本文件头开始算起。如果连接段的大小为0,这里也是0 |
map_off | 0x34 | 4 | map数据基地址 |
string_ids_size | 0x38 | 4 | 字符串列表的字符串个数。 |
string_ids_off | 0x3C | 4 | 字符串列表表基地址。 |
type_ids_size | 0x40 | 4 | 类型列表里类型个数。 |
type_ids_off | 0x44 | 4 | 类型列表基地址。 |
proto_ids_size | 0x48 | 4 | 原型列表里原型个数。 |
proto_ids_off | 0x4C | 4 | 原型列表基地址。 |
field_ids_size | 0x50 | 4 | 字段列表里字段个数。 |
field_ids_off | 0x54 | 4 | 字段列表基地址。 |
method_ids_size | 0x58 | 4 | 方法列表里方法个数。 |
method_ids_off | 0x5C | 4 | 方法列表基地址。 |
class_defs_size | 0x60 | 4 | 类定义类表中类的个数。 |
class_defs_off | 0x64 | 4 | 类定义列表基地址。 |
data_size | 0x68 | 4 | 数据段的大小,必须以4字节对齐。 |
data_off | 0x6C | 4 | 数据段基地址。 |
需要注意的是DexHeader中描述的从DexStringTable到DexClassDefTable的size并不是指这个Table的字节数,而是这些Table中各个量的个数。
checksum字段是从0x12处开始至dex文件尾的校验和,采用的是adler32算法。
signature字段是从0x32处开始至dex文件尾的sha-1签名
DexStringTable
从上面的DexHeader结构处的具体值可以看出,DexStringTable的描述数据是 10 00 00 00 70 00 00 00,也就是存在0x10个类型的值,DexStringTable的偏移地址是0x70 在此表中并未储存任何数据,只是保存了这些字符串数据在Data段中的偏移地址。每4个字节表示一个字符串地址,其结构声明DexStringId为:
struct DexStringId{
u4 stringDataOff; //字符串数据偏移
};
从数据中可以看出,第一个字符串的偏移地址是0x1CA,即从该地址处开始使用MUTF-8编码方式开始解读字符串,关于MUTF-8编码是Android中自定义的一种编码方式。对于一个字符串由两部分编码组成uleb128和utf-8,uleb128编码指定该字符串中含有多少个字符,utf-8编码使用1-3个字节表示一个字符。
通过分析一个较大的dex文件的字符串信息可知,字符串在dex文件中存储是按照字典顺序排列的,通常在开始处是换行符和空格符之类的,注意在dex中排列在一起的多个空格符被当作一个字符串(多个空格组成的字符串)来看待
DexProtoTable
这里存放着方法声明的结构体,根据DexHeader中提供的数据04 00 00 00 CC 00 00 00 可知,共有0x04个方法声明,此表的偏移地址是0xcc。结构体信息如下:
struct DexProtoId{
u4 shortIdx; //方法声明字符串,指向DexStringId列表的索引
u4 returnTypeIdx; //方法返回类型字符串,指向DexTypeId列表的索引
u4 parametersOff; //方法的参数列表,指向DexTypeList的偏移,这是一个结构体,当指向的偏移地址为0时表示
}; //没有参数信息
DexTypeList的结构信息如下:
struct DexTypeList{
u4 size; //表示DexTypeItem的个数,也就是参数的个数
DexTypeItem list[1]; //这是个结构
};
DexTypeItem的结构信息如下:
struct DexTypeItem{
u2 typeIdx; //指向DexTypeId列表的序号,这里的序号值仅跟着上述结构中的size地址后
};
上面数据段中的红色区域就是方法声明的参数列表信息,共计有3个参数列表,可以看出这里对数据做了对齐处理。
DexFieldTable
这个表中记录的数据全部是字段的索引值,指明了字段所在的类,字段的类型以及字段名。其结构如下:
struct DexFieldId{
u2 classIdx; //类的类型,指向DexTypeId列表的索引
u2 typeIdx; //字段的类型,指向DexTypeId列表的索引
u4 nameIdx; //字段名,指向DexStringId列表的索引
};
DexMethodTable
这个表和DexFieldTable表基本相同,记录的数据也都是索引值,指明了方法所在的类,方法的声明和方法名。其结构如下:
struct DexMethodId{
u2 classIdx; //类的类型,指向DexTypeId列表的索引
u2 protoldx; //声明的类型,指向DexProtoId列表的索引
u4 nameIdx; //字段名,指向DexStringId列表的索引
};
DexClassDefTable
DexHeader中给出该表的数据描述是01 00 00 00 2C 01 00 00 ,说明该表存在一个类定义结构,偏移地址是0x01c2。该表中存放的结构体为DexClassDef,共计32个byte:
struct DexClassDef{
u4 classIdx; //类的类型,指向DexTypeId列表的索引
u4 accessFlags; //访问标志
u4 superclassIdx; //父类类型,指向DexTypeId列表的索引
u4 interfacesOff; //接口,指向DexTypeList的偏移
u4 sourceFileIdx; //源文件名,指向DexStringId列表的索引
u4 annotationsOff; //注解,指向DexAnnotationsDirectoryItem结构,
u4 classDataOff; //指向DexClassData结构的偏移,类的数据部分
u4 statiValuesOff; //指向DexEncodedArray结构的偏移,记录类中的静态数据,为0表示无静态数据
};
accessFlags字段是类的访问标志,它是以ACC_开头的一个枚举值。 interfacesOff字段会指向一个DexTypeList结构,否则值为0 DexClassData结构的声明为,结构大小最多56byte:
struct DexClassData{
DexClassDataHeader header; //指定字段与方法的个数
DexField* staticFields; //静态字段,DexField结构
DexField* instanceFields; //实例字段,DexField结构
DexMethod* directMethods; //直接方法,DexMethod结构
DexMethod* virtualMethods; //虚方法,DexMethod结构
};
DexClassDataHeader结构声明为,结构大小最多为16byte:
sturct DexClassDataHeader{
u4 staticFieldsSize; //静态字段个数
u4 instanceFieldsSize; //实例字段个数
u4 directMethodsSize; //直接方法个数
u4 virualMethodsSize; //虚方法个数
};
注意:DexClassData结构和DexClassDataHeader结构均定义在DexClass.h文件中,该文件中所有结构的u4类型的字段其实都是uleb128类型的。 DexField结构描述了字段的类型和访问标志,其声明为:
struct DexField{
u4 fieldIdx; //指向DexFieldId的索引
u4 accessFlags; //访问标志,与DexClassDef结构中accessFlags字段的类型相同
};
DexMethod结构描述方法的原型,名称,访问标志和代码的数据块,其声明为:
struct DexMethod{
u4 methodIdx; //这是一个相对于上一个方法的偏移值
u4 accessFlags; //访问表示,与上述相同
u4 codeOff; //指向DexCode结构的偏移
} ;
需要注意的是methodIdx字段,在dex文件中类中的方法是按照字母表的顺序排列的,methodIdx字段并不是指向DexMethodId的索引,它只是相对于上一个方法的偏移值,比如说:第一个方法此处值为1表明是DexMethodIdx[1],第二个方法此处值也是1,这个1是相对于第一个方法的偏移表明是DexMethodIdx[2]。
DexCode结构描述了方法详尽的信息和方法中指令的内容,其声明为:
struct DexCode{
u2 registersSize; //使用的寄存器的个数
u2 insSize; //参数的个数
u2 outsSize; //调用其它方法时使用的寄存器的个数
u2 triesSize; //Try/Catch的个数
u4 debugInfoOff; //指向调试信息的偏移
u4 insnsSize; //指令集个数,以2字节为单位
u2 insns[1]; //指令集
};
详细的指令参见delavik opcode。
DexMapSection
最终来看看,该段中放了什么东西。在DexHeader中给出该段的偏移地址0x0290,该段其实是一个映射表,虚拟机在解析dex文件时会将其映射成DexMapList数据结构。其结构是:
struct DexMapList{
u4 size; //表示下面会存储多少个DexMapItem结构
DexMapItem list[1]; //DexMapItem结构
};
DexMapItem的结构声明为:
struct DexMapItem{
u2 type; //枚举常量,对应dex中数据类型
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;
kDexTypeEncodeArrayItem = 0x2005;
kDexTypeAnnotationsDirectoryItem = 0x2006;
};