Dex文件格式与解析

Dex文件格式与解析

1. Dex文件格式

Dex文件由dx(sdk里面)工具将class文件,整体打包而生成。

$ javac HelloWorld.java
$ dx --dex --output=HelloWorld.dex HelloWorld.class

1.1 总揽

9大部分组成。

组成
Header
Strings
Types
Prototypes
Fields
Methods
Classes
Data
LinkData

说明:u1表示1个字节,u2表示两个字节,依次类推。
u1-u8的定义:

// mac 64位上的标准c定义
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

// Android源代码中的定义
typedef uint8_t u1;
typedef uint16_t u2;
typedef uint32_t u4;
typedef uint64_t u8;

1.2 Header结构

struct DexHeader {
    u1 magic[8]; // 魔数固定 dex 035。035是版本号。 16进制:64 65 78 0A 30 33 35 00
    u4 checksum;  // 校验和
    u1 signature[20]; // 签名
    u4 fileSize;           // 文件大小
    u4 headerSize;         // header的大小 112
    u4 endianTag; // 字节序标记,用于指定dex运行环境的cpu,预设值为0x12345678, 即小字节序:78 56 34 12
    u4 linkSize; // 链接段大小 一直为0
    u4 linkOff; // 链接段的偏移 为0x0
    u4 mapOff; // DexMapList文件的偏移
    u4 stringIdsSize; // 字符串个数
    u4 stringIdsOff; // 字符串的偏移  对应struct DexStringId
    u4 typeIdsSize; // 类型数量
    u4 typeIdsOff; // 类型偏移 对应struct DexTypeId
    u4 protoIdsSize; // 方法原型的个数
    u4 protoIdsOff;  // 偏移 对应struct DexProtoId
    u4 fieldIdsSize; // 字段个数
    u4 fieldIdsOff; // 字段偏移 对应struct DexFieldId
    u4 methodIdsSize; // 方法个数
    u4 methodIdsOff; // 方法偏移 对应struct DexMethodId
    u4 classDefsSize; // 类的个数
    u4 classDefsOff; // 类的偏移 对应struct DexClassDef
    u4 dataSize; // 数据段的大小
    u4 dataOff; // 数据段偏移
};

1.3 stringIdsSize和stringIdsOff

stringIdsSize:记录了整个dex文件中有多少个字符串,dex里面的文件不会重复。
stringIdsOff:记录字符串在dex文件中的偏移位置。即第一个字符串的位置。

比如上面的图片中:
stringIdsSize=1C 00 00 00, 16进制的1C=28(10进制),表示此dex文件中有28个字符串。
stringIdsOff=70 00 00 00,表示:字符串偏移到0x70的位置(即从文件开始数112个字节位),也就是070h:B2 02 00 00。

stringIdsOff存放的不是真正的字符串,实际的字符串存放在data段里面,这里表示的是实际的字符串偏移位置。

stringIdsOff的结构如下:

struct DexStringId {
    u4 stringDataOff;  // 真实的字符串位置
};

寻找真实字符串的步骤:先找到字符串的偏移位置stringIdsOff,然后通过stringIdsOff找到真实的字符串。

比如:上面的dex文件有28个字符串,我们找第一个字符串。
第一个字符串的偏移位置是stringIdsOff=70 00 00 00,也就是70h的位置B2 02 00 00,然后再找到B2 02 00 00在文件中的位置(注意小字节序实际是02B2H的位置) 02B2H的位置是:08,表示的是后面8个字节组合起来就是真正的字符串了,即:3C 63 6C 69 6E 69 74 3E 00, 注意后面有个00,表示字符串的结尾(\0)。根据16进制查找ascii码表 可以得到:<clinit>

第二个字符串,紧接其后,06表示后面6个字节表示第二个字符串,3C 69 6E 69 74 3E 00查表可知是<init>。或者也可以共同过stringIdsOff来找。
字符串的偏移地址是:0x70,第二个就是0x70 + stringDataOff的长度4,即0x74得到第二个字符串的data偏移地址:BC 02 00 000x02BC,然后在到0x2bc找到真实的字符串即:06 3C 63 6C 69 6E 69 74 3E 00

28个字符串

索引号 长度 字符串
0 8 <clinit>
1 6 <init>
2 11 HelloDalvik
3 9 HelloDex!
4 15 HelloWorld.java
5 1 I
6 3 III
7 1 L
8 12 LHelloWorld;
9 2 LI
10 2 LL
11 21 Ljava/io/PrintStream;
12 18 Ljava/lang/Object;
13 18 Ljava/lang/String;
14 25 Ljava/lang/StringBuilder;
15 18 Ljava/lang/System;
16 1 V
17 2 VL
18 19 [Ljava/lang/String;
19 1 a
20 6 append
21 1 b
22 9 getNumber
23 4 main
24 3 out
25 7 println
26 8 toString
27 7 valueOf

1.4 typeIdsSize和typeIdsOff

typeIdsSize:记录了dex文件中的类型
typeIdsOff:偏移地址,指向的是struct DexTypeId结构

struct DexTypeId {
    u4 descriptorIdx; // 表示的是上面28个字符串的索引(序号)
};

descriptorIdx:表示的是上面28个字符串的索引(序号), 假如descriptorIdx的值是1,那么根据上面的分析,第一个(索引0)字符串是<clinit>,第二个(索引1)是<init>,那么就是指代<init>

解析举例:
根据上面的图片找到typeIdsSize=09 00 00 00,即有9个typeId,typeIdsOff=E0 0 00 00偏移地址是:0x00E0,然后找到0x00E005 00 00 00, 16进制转换为10进制就是5,也就是说,第一个type指向的是字符串第5个,查找上面的表格可知类型是I,即java中的int。

9个type

序号 字符串索引 结果 java中的含义
0 5 I int
1 8 LHelloWorld; class HelloWorld
2 11 Ljava/io/PrintStream; class PrintStream
3 12 Ljava/lang/Object; class Object
4 13 Ljava/lang/String; class String
5 14 Ljava/lang/StringBuilder; class StringBuilder
6 15 Ljava/lang/System; class System
7 16 V void
8 18 [Ljava/lang/String; class String 数组,new String[]

1.5 protoIdsSize和protoIdsOff

protoIdsSize:方法原型的个数
protoIdsOff:偏移, 结构是struct DexProtoId

struct DexProtoId {
    u4 shortyIdx;       // 指向strings的索引号, 同type类似。
    u4 returnTypeIdx;      // 指向type的索引号 表示的是返回类型
    u4 parametersOff;      // 参数列表的偏移  表示的是方法的参数列表
};

parametersOff:指向的数据结构

struct DexTypeList {
    u4 size;               // 表示下面DexTypeItem的个数
    DexTypeItem list[1];   // DexTypeItem的数据格式
};


struct DexTypeItem {
    u2 typeIdx;  // 指向types的索引号
};

解析步骤举例:

  1. 在header中找到protoIdsSize=07 00 00 00, 表示有7个方法原型
  2. 找到protoIdsOff=04 01 00 00,表示第一个的偏移位置是0x104
  3. 找到0x104的位置06 00 00 00 00 00 00 00 94 02 00 00,因为DexProtoId是有3个u4组成所以是12个字节
    shortyIdx=06 00 00 00,即表示是28个字符串中的第6个,III
    returnTypeIdx=00 00 00 00,即9个type中的第1个(索引0),I
    parametersOff= 94 02 00 00,表示参数列表偏移地址是0x294,即第一个struct DexTypeList的位置。
  4. 找到0x29402 00 00 00即10进制的2,表示后面有2个struct DexTypeItem每个占2个字节。 所以整个struct DexTypeList的值就是02 00 00 00 00 00 00 00两个分别是00 0000 00表示9个types的第1个,分别是II ,所以整合起来就是返回I(II),即int (int int)。

DexProtoId.shortyIdx

序号 字符串的索引号
0 6 III
1 7 L
2 9 LI
3 10 LL
4 16 V
5 17 VL
6 17 VL

DexProtoId.returnTypeIdx

序号 types的索引号
0 0 I
1 4 Ljava/lang/String;
2 4 Ljava/lang/String;
3 5 Ljava/lang/StringBuilder;
4 7 V
5 7 V
6 7 V

DexProtoId.parametersOff

序号 size DexTypeItem(type索引号)
0 2 0 和0 I 和 I
1 0 ()
2 1 0 I
3 1 4 Ljava/lang/String;
4 0 ()
5 1 4 Ljava/lang/String;
6 1 8 [Ljava/lang/String;

综上可以得到方法原型(删去函数名字和变量名):

7个proto

序号
0 int (int int)
1 String ()
2 String (int)
3 StringBuilder (StringBuilder)
4 void ()
5 void (String)
6 void (String[])

1.6 fieldIdsSize和fieldIdsOff

fieldIdsSize: dex文件中字段的个数,4个字节
fieldIdsOff:字段的偏移地址。指向的是struct DexFieldId的结构,每一个结构占8个字节

struct DexFieldId {
    u2 classIdx;        // 9个type的索引
    u2 typeIdx;          // 9个type的索引
    u4 nameIdx;          // 28个字符串的索引
};

分析举例:

  1. 在header中找到fieldIdsSize=03 00 00 00,表示有3个字段
  2. fieldIdsOff=58 01 00 00,表示字段的偏移地址是0x158
  3. 找到0x158的位置01 00 00 00 13 00 00 00,即DexFieldId的值。 所以第一个字段:
    struct DexFieldId {
     u2 classIdx = 01 00 ;        // 9个type的索引的第2个(索引1)
     u2 typeIdx = 00 00;          // 9个type的索引的第1个(索引0)
     u4 nameIdx = 13 00 00 00;    // 28个字符串的索引的第20个(索引19), 13 00 00 00 表示的是16进制
    };
    
    查上面的各个表可知,第一个字段是:
    struct DexFieldId {
     u2 classIdx = LHelloWorld ;        // 9个type的索引的第2个(索引1)
     u2 typeIdx = I;          // 9个type的索引的第1个(索引0)
     u4 nameIdx = a;    // 28个字符串的索引的第20个(索引19)
    };
    
    整合起来的意思是:class HelloWorld中有一个int a字段。目前还不知道访问限制。
    class HelloWorld {
     int a;
    }
    

3个Field

序号 结果
0 int HelloWorld.a
1 String HelloWorld.b
3 PrintStream java.lang.System.out

1.7 methodIdsSize和methodIdsOff

methodIdsSize: 方法个数 4个字节 methodIdsOff: 方法偏移 4个字节, 对应struct DexMethodId 每个占8个字节

struct DexMethodId {
    u2 classIdx; // 9个type的索引
    u2 protoIdx;  // 7个proto索引
    u4 nameIdx;    //28个字符串的索引
};

解析实例:

  1. header中找到methodIdsSize=0A 00 00 00即10个
  2. methodIdsOff=70 01 00 00即偏移位置是0x170
  3. 跳到0x170的位置,第一个DexMethodId是01 00 04 00 00 00 00 00。按长度分配。 ```c struct DexMethodId { u2 classIdx = 01 00; // 9个type的索引,即第2个(索引1) u2 protoIdx = 04 00; // 7个proto索引,即第5个(索引4) u4 nameIdx = 00 00 00 00; //28个字符串的索引,即第1个(索引0) };
查上面的各个表可知:
```c 
struct DexMethodId {
    u2 classIdx = LHelloWorld; // 9个type的索引,即第2个(索引1)
    u2 protoIdx = void();  // 7个proto索引,即第5个(索引4)
    u4 nameIdx = `<clinit>;    //28个字符串的索引,即第1个(索引0)
};

还原成java即:

class HelloWorld {

    // 这是编译器自动添加的,就是静态构造函数。
    void `<clinit>`() {

    }
}

10个方法

序号 方法
0 void <clinit> ()
1 void <linit> ()
2 int getNumber(int, int)
3 void main (String[])
4 void PrintStream.println(String)
5 void Object.<init> ()
6 String String.valueOf (int)
7 void StringBuilder.<init> ()
8 StringBuilder StringBuild.append (String)
9 String StringBuild.toString ()

1.8 classDefsSize和classDefsOff

classDefsSize: 类的个数 4个字节 classDefsOff: 类的偏移位置,4个字节, 对应struct DexClassDef32个字节

struct DexClassDef {
    u4 classIdx;      // 9个type的索引
    u4 accessFlags; // 访问限制, public private default protected
    u4 superclassIdx;     // 9个type的索引, 父类
    u4 interfacesOff;      // DexTypeList的偏移地址, 对应interface
    u4 sourceFileIdx;      // 28个string
    u4 annotationsOff;     // 注解的偏移地址 DexAnnotationsDirectoryItem
    u4 classDataOff;       /* file offset to class_data_item */
    u4 staticValuesOff;    // 静态字段 DexEncodedArray
};

类的结构比较复制。 子结构后面涉及到了子列出。

分析步骤:

  1. 在header中找到classDefsSize=01 00 00 00即只有一个类
  2. 紧接着就是classDefsOff=C0 01 00 00即是第一个类的偏移地址0x1C0
  3. 然后跳转到0x1C0的位置,struct DexClassDef是32个字节所以01 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 F8 03 00 00 00 00 00 00
struct DexClassDef {
    u4 classIdx = 01 00 00 00;      // 9个type的索引
    u4 accessFlags = 01 00 00 00; // 访问限制, public private default protected
    u4 superclassIdx = 03 00 00 00;     // 9个type的索引, 父类
    u4 interfacesOff = 00 00 00 00;      // DexTypeList的偏移地址, 对应interface 偏移地址为0,表示没有
    u4 sourceFileIdx = 04 00 00 00;      // 28个string
    u4 annotationsOff = 00 00 00 00;     // 注解的偏移地址 DexAnnotationsDirectoryItem 偏移地址为0,表示没有
    u4 classDataOff = F8 03 00 00;       /* file offset to class_data_item */
    u4 staticValuesOff = 00 00 00 00;    // 静态字段 DexEncodedArray, 偏移地址为0,表示没有
};

查上面的表可知:

struct DexClassDef {
    u4 classIdx = LHelloWorld;      // 9个type的索引
    u4 accessFlags = public; // 访问限制, public private default protected
    u4 superclassIdx = Ljava/lang/Object;     // 9个type的索引, 父类
    u4 interfacesOff = ;      // DexTypeList的偏移地址, 对应interface 偏移地址为0,表示没有
    u4 sourceFileIdx = HelloWorld.java;      // 28个string
    u4 annotationsOff = ;     // 注解的偏移地址 DexAnnotationsDirectoryItem 偏移地址为0,表示没有
    u4 classDataOff = F8 03 00 00;      // DexClassData结构
    u4 staticValuesOff = ;    // 静态字段 DexEncodedArray, 偏移地址为0,表示没有
};

访问限制枚举:

// dalvik/libdex/DexFile.h 
ACC_PUBLIC       = 0x00000001,       // class, field, method, ic
ACC_PRIVATE      = 0x00000002,       // field, method, ic
ACC_PROTECTED    = 0x00000004,       // field, method, ic
ACC_STATIC       = 0x00000008,       // field, method, ic
ACC_FINAL        = 0x00000010,       // class, field, method, ic
ACC_SYNCHRONIZED = 0x00000020,       // method (only allowed on natives)
ACC_SUPER        = 0x00000020,       // class (not used in Dalvik)
ACC_VOLATILE     = 0x00000040,       // field
ACC_BRIDGE       = 0x00000040,       // method (1.5)
ACC_TRANSIENT    = 0x00000080,       // field
ACC_VARARGS      = 0x00000080,       // method (1.5)
ACC_NATIVE       = 0x00000100,       // method
ACC_INTERFACE    = 0x00000200,       // class, ic
ACC_ABSTRACT     = 0x00000400,       // class, method, ic
ACC_STRICT       = 0x00000800,       // method
ACC_SYNTHETIC    = 0x00001000,       // field, method, ic
ACC_ANNOTATION   = 0x00002000,       // class, ic (1.5)
ACC_ENUM         = 0x00004000,       // class, field, ic (1.5)
ACC_CONSTRUCTOR  = 0x00010000,       // method (Dalvik only)
ACC_DECLARED_SYNCHRONIZED = 0x00020000,       // method (Dalvik only)
ACC_CLASS_MASK =
        (ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT
                | ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM),
    ACC_INNER_CLASS_MASK =
        (ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC),
    ACC_FIELD_MASK =
        (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
                | ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC | ACC_ENUM),
    ACC_METHOD_MASK =
        (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
                | ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS | ACC_NATIVE
                | ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC | ACC_CONSTRUCTOR
                | ACC_DECLARED_SYNCHRONIZED),

由于接口 注解 静态字段没有这里我们值分析classDataOff。

classDataOff=F8 03 00 00,即表示这个classData的偏移地址是0x03F8。

注意:DexClassData结构的字节数是uleb128类型的,表示1-5个字节,需要转换,dalvik/libdex/Leb128.h

struct DexClassData {
    DexClassDataHeader header; // 头 
    DexField*          staticFields; // 静态字段 
    DexField*          instanceFields; // 实例字段 
    DexMethod*         directMethods; //  直接方法
    DexMethod*         virtualMethods;  // 虚方法
   };


struct DexClassDataHeader {
        u4 staticFieldsSize; // 静态字段数 
        u4 instanceFieldsSize; // 实例字段数 
        u4 directMethodsSize; // 直接方法
        u4 virtualMethodsSize; // 虚方法
    };

Leb128转换规则:

最高位1:表示有后续字节 为0:表示没有后续字节
转换时去掉最高位,从低到高从新组合。
比如:0000 0001
去掉最高位: 000 0001
结果还是1

根据数据转换格式后可知:

struct DexClassDataHeader {
        u4 staticFieldsSize = 0x1; // 静态字段数 
        u4 instanceFieldsSize = 0x1; // 实例字段数 
        u4 directMethodsSize = 0x3; // 直接方法
        u4 virtualMethodsSize = 0x1; // 虚方法
    };
struct DexField {
   u4 fieldIdx;   // 指向字段索引号
   u4 accessFlags; // 访问表示
};

DexClassDataHeader实际占用了4个字节,所以staticFields紧接其后01即1,和08即8。

struct DexField {
   u4 fieldIdx = String HelloWorld.b;   // 指向字段索引号, 
   u4 accessFlags = ACC_STATIC; // 访问表示
};

然后是instanceFields:00 和 00。

struct DexField {
   u4 fieldIdx = int HelloWorld.a;   // 指向字段索引号0
   u4 accessFlags = ; // 
};

接着是DexMethod directMethods。

struct DexMethod {
        u4 methodIdx;   // 方法索引
         u4 accessFlags;
         u4 codeOff;      // 代码放置的偏移位置 DexCode结构
     };

接着instanceFields后面是00 88 80 04...。 表示methoIdx是0,即void <clinit> ()
后面的数据需要使用leb128转换,因为0x88的二进制是1000 1000最高位为1,说明后面还有字节,继续看0x80,1000 0000后面还有,在看0x04没有了, 表明accessFlags是3个字节组成,即0x88 80 04,我们转换一下10001000 10000000 00000100,去掉每一个字节的高位0001000 0000000 0000100, 然后按小字节序排列。得到0000100 000000 0001000,即16进制的0x10008,也就是ACC_STATIC和ACC_CONSTRUCTOR。
codeOff:E0 03。也需要转换,即0x1E0,表示code放的位置。

struct DexCode {
    u2 registersSize; // 需要寄存器的个数
    u2 insSize; // 需要的参数个数
    u2 outsSize; // 调用其他函数的参数个数
    u2 triesSize; //  DexTry 的个数 try catch
    u4 debugInfoOff;   // 偏移地址,指向本段代码的 debug 信息存放位置,是一个 debug_info_item 结构
    u4 insnsSize;      // 指令列表的大小,以 16-bit 为单位。 insns 是 instructions 的缩写
    u2 insns[1]; // 指令集
};

0x1E0: 01 00 00 00 00 00 00 00 D8 03 00 00 05 00 00 00 1A...0E 00

struct DexCode {
    u2 registersSize = 1; // 需要寄存器的个数
    u2 insSize = 0; // 需要的参数个数
    u2 outsSize = 0; // 调用其他函数的参数个数
    u2 triesSize = 0; //  DexTry 的个数 try catch
    u4 debugInfoOff = 0x03D8;   // 偏移地址,指向本段代码的 debug 信息存放位置,是一个 debug_info_item 结构
    u4 insnsSize = 5;      // 指令列表的大小,以 16-bit 为单位。 insns 是 instructions 的缩写, 最大为65535
    u2 insns[1] = ; // 指令集,参考https://source.android.com/devices/tech/dalvik/dalvik-bytecode
};

至此已经分析完了一个directMethod还有2个以及virtualMethod方法,自行尝试分析。

附:

public class HelloWorld {  
    int a = 0;  
    static String b = "HelloDalvik";  

    public int getNumber(int i, int j) {  
        int e = 3;  
        return e + i + j;  
    }  

    public static void main(String[] args) {  
        int c = 1;  
        int d = 2;  
        HelloWorld helloWorld = new HelloWorld();  
        String sayNumber = String.valueOf(helloWorld.getNumber(c, d));  
        System.out.println("HelloDex!" + sayNumber);  
    }  
}

参考:

  1. https://blog.csdn.net/sinat_18268881/article/details/55832757
  2. http://androidxref.com/9.0.0_r3/xref/dalvik/libdex/DexFile.h
  3. https://nanshihui.github.io/2017/03/30/dex%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90/
声明:原创文章,版权所有,转载请注明出处,https://litets.com。