第 3 章:深入理解 Dex 文件格式

本文最后更新于:1 年前

Reference


Dex 文件格式:

  • Dex 文件是 Android 平台上 Java 源码文件经编译和处理后得到的字节码文件。
  • 重复造轮子不直接使用 Class 文件格式的原因:Android 系统主要针对移动设备内存、存储空间相对 PC 平台而言较小,且主要使用 ARM 的 CPU 通用寄存器更多,寄存器的存取速度比位于内存中的操作数栈的存取速度要快得多,Dex 中的指令码也叫操作码可以利用寄存器来存取参数。
  • Dex 和 Class 文件格式对比:
    • 最终产物不同:
      • PC 平台:每一个 Java 源码文件都会对应生成一个同文件名的 Class 文件,生成的所有文件会打包到一个压缩包 Jar 包
      • Android 平台:每一个 Java 源码文件都会对应生成一个同文件名的 Class 文件,生成的所有文件会合并成一个 classes.dex 文件
        优:
        • 进一步去除冗余信息多个 Class 文件之间可能还是有重复字符串等信息
        • 减少 CPU 和存储设备之间 I/O 操作的次数一个 Class 文件可能依赖另外一个 Class 文件
    • 字节序不同但是一个字节内的比特顺序都一样
      • Class 文件的字节序是 Big Endian。
      • Dex 文件默认的字节序是 Little Endian ARM CPU、X86 CPU 采用的也是 Little Endian,保持一致了
    • Dex 新增的 LEB128Little Endian Based 128 数据类型:
      • 唯一功能是用于表示 32 位长度的数据。
      • 进一步减少空间占用:
        • 每个字节的第 7 位数据用于表示这个 LEB128 数据是否结束所以小值可以就占 1 个字节,大值最多用 5 个字节:1 表示非结尾字节,0 表示结尾字节
        • 每个字节的前 7 位数据按顺序组合为一个 32 位的数据。
      • SLEB128:有符号的整数,结尾字节的第 6 位用于表示是否为负数。
      • ULEB128:无符号的整数。
        • ULEB128p1:无符号的整数,值为 ULEB128 - 1,使得 -1 这个负数只要 1 个字节就可以表示。
    • 【相同点】数据类型的字符串描述:
      • 原始数据类型: BCDFIJSZ,分别表示 Java 类型的 bytechardoublefloatintlongshortboolean
      • V 表示 void,只能用于表示方法的返回值类型。
      • 引用数据类型: L类的全路径名;
      • 数组:[数据类型
    • Class 的 MethodDescriptor 和 Dex 的 ShortyDescriptor简短描述,描述方法的参数和返回值信息
      • MethodDescriptor:(参数类型)返回值类型
      • ShortyDescriptor:返回值类型 参数类型
        • ShortyDescriptor -> ShortyReturnType (参数类型)*
        • ShortyReturnType -> V | shortyFieldType
        • shortyFieldType -> B | C | D | F | I | J | S | Z | L
          【引入问题】引用数据类型用 L 表示将无法确定参数或返回值的具体类型。
          【解决问题】Dex 提供了其他数据表明具体类型。

Dex 文件格式概貌[1]

类型名称描述
header_itemheaderDex 文件头
string_id_item[]string_ids字符串信息
type_id_item[]type_ids类型信息
proto_id_item[]proto_ids方法原型信息
field_id_item[]field_ids成员变量信息变量名、类型等
method_id_item[]method_ids成员方法信息方法名、参数和返回值类型等
class_def_item[]class_defs类信息
call_site_id_item[]call_site_ids调用站点信息
method_handle_item[]method_handles方法句柄信息
ubyte[]data数据通过成员变量 xx_off 指向文件的某个位置
ubyte[]link_data保留

header_item:

类型名称描述
ubyte[8]magic魔数 0x6465780A30333500
uintchecksum校验和不包括 magic 和 checksum 自己
ubyte[20]signature签名信息不包括 magic、checksum 和 signature 自己
uintfile_size文件的大小
uint = 0x70header_size文件头的大小
uintendian_tag字节序默认是 0x12345678,Little Endian 格式;如果是 0x78563412,表示 Big Endian 格式
uintlink_size链接区段的大小如果此文件未进行静态链接,则该值为 0
uintlink_off从文件开头到链接区段的偏移量
uintmap_off从文件开头到映射项的偏移量
uintstring_ids_size字符串标识符列表中的字符串数量
uintstring_ids_off从文件开头到字符串标识符列表的偏移量
uinttype_ids_size类型标识符列表中的元素数量
uinttype_ids_off从文件开头到类型标识符列表的偏移量
uintproto_ids_size原型标识符列表中的元素数量
uintproto_ids_off从文件开头到原型标识符列表的偏移量
uintfield_ids_size字段标识符列表中的元素数量
uintfield_ids_off从文件开头到字段标识符列表的偏移量
uintmethod_ids_size方法标识符列表中的元素数量
uintmethod_ids_off从文件开头到方法标识符列表的偏移量
uintclass_defs_size类定义列表中的元素数量
uintclass_defs_off从文件开头到类定义列表的偏移量
uintdata_sizedata 区段的大小该数值必须是 sizeof(uint) 的偶数倍
uintdata_off从文件开头到 data 区段开头的偏移量

code_item:

类型名称描述
ushortregisters_size需要使用的寄存器虚拟寄存器,非物理寄存器数量
ushortins_size方法的输入参数需要占用的字数以双字节为单位。在 art 优化器中,ins_size 是方法的输入参数的个数,也是输入参数占据虚拟寄存器的个数
ushortouts_size方法内部调用时,输出参数需要占用的字数以双字节为单位
ushorttries_sizetry_item 的数量
uintdebug_info_off从文件开头到此代码的调试信息(行号 + 局部变量信息)序列的偏移量如果没有任何信息,该值为 0。该偏移量(如果为非零值)应该是到 data 区段中某个位置的偏移量。数据格式由 debug_info_item 指定
uintinsns_size指令码数组的大小Dex 文件格式中指令码的长度还是 1 个字节,但是与第一个参数混在一起构成了一个双字节元素
ushort[insns_size]insns指令码数组。insns 数组中的代码格式由随附文档 Dalvik 字节码指定有一些内部结构倾向于采用四字节对齐方式
ushort(可选)= 0padding使 tries 实现四字节对齐的两字节填充只有 tries_size 为非零值且 insns_size 是奇数时,此元素才会存在
try_item[tries_size](可选)triestry 语句的内容只有 tries_size 为非零值时,此元素才会存在
encoded_catch_handler_list(可选)handlerscatch 语句的内容只有 tries_size 为非零值时,此元素才会存在

其余数据结构查询项和相关结构

Dex 指令码:

  • 解析 Dex 文件中存储方法内容的 insns 数组位于 code_item 结构体内比 解析 Class 文件中存储方法内容的 code 数组位于 Code 属性中难,因为操作所需的参数如果不跟在指令码后面和 Class 指令码一样,就存储在寄存器中不使用操作数栈,对于存储在寄存器中的参数,指令码需要携带一些信息来表示该指令执行时需要操作哪些寄存器。
  • insns 指令码数组的元素两个字节长的数据结构:

    元素有两种结构:
    • 参数 + 指令码:低 8 位是指令码,高 8 位是参数。
    • 纯参数跟在参数 + 指令码后面
      【参数格式】
      • 不同字符代表不同的参数。
      • 参数的长度由字符个数决定每个字符占 4 位
      • 一个 Ø 代表 4 位的 0
  • 指令码描述:
    • 规则:
      • 第一列 Format:指令码和参数的存储格式。
      • 第二列 Format ID:两个数字 + 一到多个后缀字符。
        • 两个数字:第一个数字表示一条完整的指令执行该指令需要的指令码和参数需要的 ushort 元素个数,第二个数字表示这条指令将用到几个寄存器。
        • 后缀字符:以半助记符的形式表示该格式编码的任何其他数据类型[2]
      • 第三列 语法[3]:具体展示了各个参数的用法有特殊字符
    • 以 AA|op BBBB 为例:
      • Format:
        • AA|op BBBB:一个 8 位的参数 + 指令码 + 一个 16 位的参数。
      • Format ID:
        • 22x:需要 insns 数组中的两个元素 + 需要两个寄存器 + 无额外数据。
        • 22t:需要 insns 数组中的两个元素 + 需要两个寄存器 + 分支目标。
        • 22s:需要 insns 数组中的两个元素 + 需要两个寄存器 + 有符号立即数(短整型)。
        • 22h:需要 insns 数组中的两个元素 + 需要两个寄存器 + 有符号立即数 hat(32 位或 64 位值的高阶位,低阶位全为 0)。
        • 22c:需要 insns 数组中的两个元素 + 需要两个寄存器 + 常量池索引。
      • 语法:
        • op vAA, vBBBB:寄存器 A + 寄存器 B
        • op vAA, +BBBB:寄存器 A + 相对指令地址偏移量 B
        • op vAA, #+BBBB:寄存器 A + 常量值 B
        • op vAA, #+BBBB0000:寄存器 A + 常量值 B
        • op vAA, #+BBBB000000000000:寄存器 A + 常量值 B
        • op vAA, type@BBBB:寄存器 A + 指向 type_ids 数组的索引 B
        • op vAA, field@BBBB:寄存器 A + 指向 field_ids 数组的索引 B
        • op vAA, method_handle@BBBB:寄存器 A + 指向 method_ids 数组的索引 B
        • op vAA, proto@BBBB:寄存器 A + 指向 proto_ids 数组的索引 B
        • op vAA, string@BBBB:寄存器 A + 指向 string_ids 数组的索引 B

dexdump 工具:

  • 解析 dex 文件:把 insns 数组的内容翻译成对应的助记符,并解析其中的参数。
  • 使用方法:
    1. 准备一个 dex 文件比如 D:\classes3.dex
    2. 进入 dexdump 目录比如 C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\33.0.2\
    3. 打开命令行工具。
    4. 执行命令:
      dexdump.exe -d D:\classes3.dex
  • 解析红框中的指令码:
    • 确定指令码:1a00,先按字节划分为两个元素 1a00,因为 Dex 文件是 Little Endian,所以 【1a 是指令码】低 8 位,00 是参数高 8 位
    • 确定参数:

      经查询,1a 的 Format ID 是 21c需要 insns 数组中的两个元素 + 需要一个寄存器 + 常量池索引,助记符是 const-string vAA, string@BBBB

      经查询,21c 的 Format 是 AA|op BBBB一个 8 位的参数 + 指令码 + 一个 16 位的参数,语法是 op vAA, string@BBBB寄存器 A + 指向 string_ids 数组的索引 B,对应 1a00 1d00,【寄存器 0 + 指向 string_ids 数组的索引 001d】。

TODO:自己动手编写一个 Dex 文件格式的解析程序

参考