构造Dex文件
Dex文件就是Dalvik可执行文件,实际上它就是一个优化后的java字节码文件,因此构造这类文件需要先写个java文件
Pino.java
public class Pino {
public static void main(String args[]) {
System.out.println("Hello World");
}
}
然后编译
javac Pino.java
之后得到了Pino.class文件,之后我们用dx工具,该工具需要安装Android SDK才能有的工具
dex --dex --output=Pino.dex Pino.class
这样就得到了一个dex文件了,之后我们利用010editor工具来进行分析。
Dex文件整体结构
那我们从头开始分析
Dex文件分析
首先,我们来看一下Dex文件头的结构体
struct DexHeader {
u1 magic[8]; /* dex的魔数 */
u4 checksum; /* 校验和 */
u1 signature[kSHA1DigestLen]; /* SHA-1哈希值*/
u4 fileSize; /* dex文件的大小 */
u4 headerSize; /* dex文件头的大小 */
u4 endianTag; /* 字节序标记 */
u4 linkSize; /* 链接段大小 */
u4 linkOff; /* 链接段偏移 */
u4 mapOff; /* DexMapList的文件偏移 */
u4 stringIdsSize; /* DexStringId的个数 */
u4 stringIdsOff; /* DexStringId的偏移 */
u4 typeIdsSize; /* DexTypeId的个数 */
u4 typeIdsOff; /* DexTypeId的偏移 */
u4 protoIdsSize; /* DexProtoId的个数 */
u4 protoIdsOff; /* DexStringId的偏移 */
u4 fieldIdsSize; /* DexFieldId的个数 */
u4 fieldIdsOff; /* DexFieldId的偏移 */
u4 methodIdsSize; /* DexMethodId的个数 */
u4 methodIdsOff; /* DexMethodId的偏移 */
u4 classDefsSize; /* DexClassDef的个数 */
u4 classDefsOff; /* DexClassDef的偏移 */
u4 dataSize; /* 数据段的大小 */
u4 dataOff; /* 数据段的偏移 */
};
magic[8] 由8个u1类型的数据,内容是“64 65 78 0A 30 33 35 00”,u1就是1个字节的无符号数,这个是dex文件的标志,用来识别dex文件的。
checksum 没什么好说的,就是dex文件的校验和
signature[kSHA1DigestLen],就是整个dex文件进行SHA-1哈希计算得到的字符串,一般来说20个字节
fileSize, 整个dex文件的大小
headerSize, dex文件的头的大小
endianTag, dex文件的字节序标记,用于指定dex文件运行环境的cpu,预设值为0x12345678,在010editor的话就是“78 56 34 12”(小端序)
linkSize和linkOff, 链接段的大小和文件偏移,通常为0,linkSize为0表示静态链接
mapOff, DexMapList的文件偏移
-
stringIdsSize和stringIdsOff,这两个是DexStringId的个数和文件偏移
这里stringIdSize的值为0E,10进制就是14,也就是说这个dex文件的字符串的个数为14个,文件偏移是70,我们到70的位置看一下
蓝色部分就是DexStringId的内容了,每个字符串4字节,总共14个,我们先看一下第一组“76 01 00 00”,这个值并不是字符串的具体内容,而是字符串所在位置的文件偏移,我们去看一下176h这个位置
蓝色部分我一共选中了8个字节,其中第一个字节06代表的是之后多少个字节属于字符串,也就是“3C 69 6E 69 74 3E”,而最后一个字节的00其实是字符串结尾的空字节,但是计数的时候并没有算上而已,总结一下这个dex文件中所有的字符串如下:
序号 | 字符串 |
---|---|
0 | <init> |
1 | Hello World |
2 | LPino; |
3 | Ljava/io/PrintStream; |
4 | Ljava/lang/Object; |
5 | Ljava/lang/String; |
6 | Ljava/lang/System; |
7 | Pino.java |
8 | V |
9 | VL |
10 | [Ljava/lang/String; |
11 | main |
12 | out |
13 | println |
-
typeIdSize和typeIdOff,就是类的类型个数和文件偏移,可以根据之前字符串的进行类比
typeIdSize的值为07,也就是说由7个类型,typeIdOff的值是A8h,我们到A8的位置看一下
蓝色选中的部分都是类型,但是这个一种数据结构
struct DexTypeId {
u4 descriptorIdx; //指向DexStringId列表的索引
}
先看一下第一个4字节的值“02 00 00 00 ”,对照之前我们整理的字符串的表格,就是LPino;即Pino类型的,整理一下所有的类型,如下
序号 | 类的类型 |
---|---|
0 | LPino; |
1 | Ljava/io/PrintStream; |
2 | Ljava/lang/Object; |
3 | Ljava/lang/String; |
4 | Ljava/lang/System; |
5 | V |
6 | [Ljava/lang/String; |
-
protoIdSize和protoIdOff,这两个是方法原型的个数和位置偏移
这里数量就是3,位置偏移为C4,跟过去看下
蓝色选中的部分就是所有的方法原型的结构了,这里又涉及到了一个新的数据结构
struct DexProtoId {
u4 shortyIdx; //指向DexStringId列表的索引
u4 returntypeIdx; //指向DexTypeId列表的索引
u4 parameterOff; //指向DexTypeList列表的位置偏移
}
这三个属性分别是第一个是方法声明的字符串,第二个是方法的返回类型,第三个是方法的参数列表,其中DexTypeList是新的数据结构
struct DexTypeList {
u4 size; //DexTypeItem的个数
DexTypeItem list[1];
}
struct DexTypeItem {
u2 typeIdx; //指向DexTypeId列表的索引
}
回过头来看一下蓝色部分,12个字节,第一个4字节为8,说明DexStringId列表的索引是8,也就是V,第二个4字节是5,也就是V,最后一个是0,也就是没有参数,第一个方法就是void (),整理一下其他的如下:
序号 | 方法原型 |
---|---|
0 | void() |
1 | void (java.lang.String) |
2 | void (java.lang.String[]) |
-
fieldIdSize和fieldIdOff这两个是字段的数量和位置偏移
这里字段数是1,位置偏移为E8,字段也有新的数据结构
struct DexFieldId{
u2 classIdx; /*类的类型,指向DexTypeId列表的索引*/
u2 typeIdx; /*字段类型,指向DexTypeId列表的索引*/
u4 nameIdx; /*字段名,指向DexStringId列表的索引*/
}
也就是一个DexFieldId是8个字节
classIdx的值是4,也就是Ljava/lang/System;,typeIdx的值是1,也就是Ljava/io/PrintStream;,nameIdx的值是C,也就是out,总结一下字段如下:
序号 | 字段 |
---|---|
0 | java.io.PrintStream java.lang.System.out |
-
methodIdSize和methodIdOff这两个分别是方法的数量和位置偏移
fieldIdSize的值是4,也就是有4个方法,fieldIdOff是F0,跟过去看下
新的数据结构如下:
struct DexMethodId{
u2 classIdx; /*类的类型,指向DexTypeId列表的索引*/
u2 protoIdx; /*声明类型,指向DexProtoId列表的索引*/
u4 nameIdx; /*方法名,指向DexStringId列表的索引*/
}
也就是说每个DexMethodId占8个字节,第一个8字节中的classIdx的值是0,也就是LPino;,protoIdx的值也是0,也就是void(),第三nameIdx也是0,也就是<init>,综合起来就是void Pino.<init>(),整理一下所有的方法如下:
序号 | 方法 |
---|---|
0 | void Pino.<init>() |
1 | void Pino.main(java.lang.String[]) |
2 | void java.io.PrintStream.println(java.lang.String) |
3 | void java.lang.Object.<init>() |
-
classDefsSize和classDefsOff是类的定义的数量和位置偏移
classDefsSize的值为1,说明就定义了一个类,然后在到110h的位置看看,但是这里还是有新的结构体
struct DexClassDef{
u4 classIdx; /*类的类型,指向DexTypeId列表的索引*/
u4 accessFlags; /*访问标志,就是表示是public还是private等等*/
u4 superclassIdx; /*父类类型,指向DexTypeId列表的索引*/
u4 interfacesOff; /*接口,指向DexTypeList的偏移*/
u4 sourceFileIdx; /*源文件名,指向DexStringId列表的索引*/
u4 annotationsOff; /*注解,指向DexAnnotationsDirectoryItem结构*/
u4 classDataOff; /*指向DexClassData结构的偏移*/
u4 staticValuesOff; /*指向DexEncodedArray结构的偏移*/
}
上面的数据结构28个字节,内容的话看注释也能看懂,我们直接上实例,在这里,classIdx是1,也就是LPino;,第二个accessFlags是1,也就是public,第三个superclassIdx是2,也就是父类是java.lang.Object,第四个interfacesOff是0,就是没有,第五个是sourceFileIdx是7,也就是Pino.java,第六个是annotationOff,是0,没有,第七个classData是22D,也就是DexClassData的偏移是22D,我们先来看看DexClassData的结构体
struct DexClassData{
DexClassDataHeader header; /*指定字段与方法的个数*/
DexField* staticFields; /*静态字段,DexField结构*/
DexField* instanceFields; /*实例字段,DexField结构*/
DexMethod* directMethods; /*直接方法,DexMethod结构*/
DexMethod* virtualMethods; /*虚方法,DexMethod结构*/
}
这里面又涉及到了其他三种结构体
struct DexClassDataHeader{
u4 staticFieldsSize; /*静态字段个数*/
u4 instanceFieldsSize; /*实例字段个数*/
u4 directMethodsSize; /*直接方法个数*/
u4 virtualMethodsSize; /*虚方法个数*/
}
struct DexField{
u4 fieldIdx; /*指向DexFieldId的索引*/
u4 accessFlags; /*访问标志*/
}
struct DexMethod{
u4 methodIdx; /*指向DexMethodId的索引*/
u4 accessFlags; /*访问标志*/
u4 codeOff; /*指向DexCode结构的偏移*/
}
这里需要注意的一点的就是这里的u4并不是值4字节,而是值uleb128的类型,具体是什么可以自行百度。
现在我们再去22D的位置看看
从这里可以判断姿态字段0个,实例字段0个,直接方法2个,虚方法0个。因为staticFields和instanceFields都是0个,所以直接从directMethods来看了,methodIdx为0,也就是void Pino.<init>(),accessFlags的值为“81 80 04”,这个是uleb128编码的,转换为16进制的话就是10001h,对照一下DexFile.h文件,知道方法是ACC_PUBLIC和ACC_CONSTRUCTOR
表示这个方法是public的并且是构造方法,然后是codeOff的值是“B0 02”,转换为16进制就是130h。第二个函数的methodIdx的值是1,也就是void Pino.main(java.lang.String[]),accessFlags的值是09h,也就是ACC_PUBLIC和ACC_STATIC,codeOff的值是“CB 02”,转换为16进制就是14Bh,也就是位置偏移在14Bh处。