内容纲要

dex文件

dex文件概述

Dalvik 是 Google 设计的用于 Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在Android 4.4推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。

dex文件生成

.java->.class

image-20220718035343636

.class->.dex

上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。

dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的SDK根目录/build-tools/任意版本 里面。

dex文件格式

整体布局

image-20220718035807463

文件头

img

magic 一般是常量,用来标记 DEX 文件,它可以分解为:

文件标识 dex + 换行符 + DEX 版本 + 0

eg:字符串格式为 dex\n035\0,十六进制为 0x6465780A30333500

checksum 是对去除 magicchecksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。

signature 是对除去 magicchecksumsignature 以外的文件部分作 sha1 得到的文件哈希值。

endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678

其余部分分别标记了 DEX 文件中其他各个数据结构的个数和其在数据区的偏移量。根据偏移量我们就可以轻松的获得各个数据结构的内容。

索引区(id区)

id 区存储着字符串,type,prototype,field, method 资源的真正数据在文件中的偏移量,我们可以根据 id 区的偏移量去找到该 id 对应的真实数据。

img

image-20220718041837969

headerdata 之间都是偏移量数组,并不存储真实数据,所有数据都存在 data 数据区,根据其偏移量区查找。

字符串id区域

image-20220718040251706

类型id区

image-20220718040405091

类型id指向字符串id区位置,字符串id的位置为该类型的文字字符串描述

方法原型id区

image-20220718040657170

成员id区

image-20220718040808921

方法id区

image-20220718040920193

类定义区(class_def)

image-20220718041031217

class_def 是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息

classIdx : 指向 type_ids ,表示类信息

accessFlags : 访问标识符

superclassIdx : 指向 type_ids ,表示父类信息

interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息

sourceFileIdx : 指向 string_ids ,表示源文件名称

annotationOff : 注解信息

classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分

staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据

classDataOff

它包含了一个类的核心数据。在 Android 源码中定义为 DexClassData

struct DexClassData {
    DexClassDataHeader header;
    DexField*          staticFields;
    DexField*          instanceFields;
    DexMethod*         directMethods;
    DexMethod*         virtualMethods;
};

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

在读取的时候要注意这里的数据是 LEB128 类型。它是一种可变长度类型,每个 LEB128 由 1~5 个字节组成,每个字节只有 7 个有效位。如果第一个字节的最高位为 1,表示需要继续使用第 2 个字节,如果第二个字节最高位为 1,表示需要继续使用第三个字节,依此类推,直到最后一个字节的最高位为 0,至多 5 个字节。除了 LEB128 以外,还有无符号类型 ULEB128。

那么为什么要使用这种数据结构呢?我们都知道 Java 中 int 类型都是 4 字节,32 位的,但是很多时候根本用不到 4 个字节,用这种可变长度的结构,可以节省空间。对于运行在 Android 系统上来说,能多省一点空间肯定是好的。

header 部分定义了各种字段和方法的个数,后面跟着的分别就是 静态字段 (staticFields)、实例字段(instanceFieldsSize) 、直接方法 (directMethodsSize)、虚方法(virtualMethodsSize) 的具体数据了。字段用 DexField 表示,方法用 DexMethod 表示。

image-20220718043252073

image-20220718043303477

DexCode

DexCode 是用来存储方法的详细信息以及其中的指令的。

struct DexCode {
    u2  registersSize;  // 寄存器个数
    u2  insSize;        // 参数的个数
    u2  outsSize;       // 调用其他方法时使用的寄存器个数
    u2  triesSize;      // try/catch 语句个数
    u4  debugInfoOff;   // debug 信息的偏移量
    u4  insnsSize;      // 指令集的个数
    u2  insns[1];       // 指令集
    /* followed by optional u2 padding */  // 2 字节,用于对齐
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};
案例

main() 方法对应的 DexCode 十六进制表示为 :

03 00 01 00 02 00 00 00 79 02 00 00 08 00 00 00
62 00 01 00 62 01 00 00 6E 20 03 00 10 00 0E 00

使用的寄存器个数是 3 个。参数个数是 1 个,就是 main() 方法中的 String[] args。调用外部方法时使用的寄存器个数为 2 个。指令个数是 8 。

终于说到指令了,main() 函数中有 8 条指令,就是上面十六进制中的第二行。尝试来解析一下这段指令。Android 官网就有 Dalvik 指令的相关介绍,链接

第一个指令 62 00 01 00,查询文档 62 对应指令为 sget-object vAA, field@BBBBAA 对应 00 , 表示 v0 寄存器。BBBB 对应 01 00 ,表示 field_ids 中索引为 1 的字段,根据前面的解析结果该字段为 Ljava/lang/System;->out;Ljava/io/PrintStream,整理一下,62 00 01 00 表示的就是:

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

接着是 62 01 00 00。还是 sget-object vAA, field@BBBB, AA 对应 01BBBB 对应 0000, 使用的是 v1 寄存器,field 位 field_ids 中索引为 0 的字段,即 LHello;->HELLO_WORLD;Ljava/lang/String,该句完整指令为:

sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

接着是 6E 20 03 00, 查看文档 6E 指令为 invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB6E 后面一个十六位 2 表示调用方法是两个参数,那么 BBBB 就是 03 00,指向 method_ids 中索引为 3 方法。根据前面的解析结果,该方法就是 Ljava/io/PrintStream;->println(Ljava/lang/String;)V。完整指令为:

invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

最后的 0E,查看文档该指令为 return-void,到这 main() 方法就结束了。

将上面几句指令放在一起:

62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
OE OO : return-void

这就是 main() 方法的完整指令了。

参考资料

https://tech.youzan.com/qian-tan-android-dexwen-jian/

代码详解:https://juejin.cn/post/6844903847647772686

img

img10

img