Android DEX 文件格式解析详解
本文档基于 l0neman 的博客 和 Android 9.0.0_r3 系统源码整理 & 一些个人理解
前言
Java 代码文件在经过 javac 编译器编译后会产出 .class 格式的 Java 虚拟机可执行的字节码文件.dex,而 DEX 文件则是 Android SDK 编译 Java 代码后的产物(Android SDK 使用 dx 或 d8 编译器将 .class 文件编译为 .dex 文件)。
了解 DEX 文件结构是理解 Android 虚拟机原理的基础,同时也是学习 Android 逆向工程的基础。
DEX 文件的文件后缀为 .dex,是 Android 虚拟机的可执行文件。
基本数据类型
首先说明组成 DEX 文件结构中的基本类型,包含原生类型和 uleb128 类型:
| 名称 | 说明 |
|---|---|
| byte | 8 位有符号整数 |
| ubyte | 8 位无符号整数 |
| short | 16 位有符号整数,小端字节序 |
| ushort | 16 位无符号整数,小端字节序 |
| int | 32 位有符号整数,小端字节序 |
| uint | 32 位无符号整数,小端字节序 |
| long | 64 位有符号整数,小端字节序 |
| ulong | 64 位无符号整数,小端字节序 |
| sleb128 | 有符号 LEB128,可变长度 |
| uleb128 | 无符号 LEB128,可变长度 |
| uleb128p1 | 无符号 LEB128 + 1,可变长度 |
简单记忆:
dex中, byte 、short、int、long 和ubyte\uint\ushort\ulong ,带u的是无符号整数,不带u的是有符号整数,(无符号的表示正数和0,有符号的表示正负数)刚好相反
在系统源码 DexFile.h 的对应类型别名定义如下:
// DexFile.h
typedef uint8_t u1;
typedef uint16_t u2;
typedef uint32_t u4;
typedef uint64_t u8;
typedef int8_t s1;
typedef int16_t s2;
typedef int32_t s4;
typedef int64_t s8;
LEB128 类型
LEB128 类型是 DEX 文件中的基础类型之一,该类型格式借鉴了 DWARF3 规范。在 DEX 文件中,LEB128 仅用于对 32 位数字进行编码。
LEB128 全称为"Little-Endian Base 128",表示任意有符号或无符号整数的可变长度编码。
每个 LEB128 编码值由 1-5 个字节组成,共同表示一个 32 位的值,设计 LEB128 的目的是为了节省内存。
表示方法是:每个字节的首位为标志位,为 0 说明这个字节是 LEB128 类型的最后一个字节,为 1 则表示后面还有若干字节,然后将每个字节去除标志位后的 7 位组合成一个 32 位数字。
uleb128
对于无符号的 uleb128 类型,使用两个字节表示十进制数字 4176 的示例如下:
11010000 00100000
去除标志位后:
1010000 0100000
组合后使用便于阅读的大端格式表示为:
00010000 01010000
1000001010000 转化为十进制的值为 4176。
sleb128
对于有符号的 sleb128 类型,最后一个字节的除了标志位的最高位将为符号位,将进行符号扩展。
例如下面使用两个字节的 sleb128 表示有符号数:
11010000 01100000
去除标志位后:
1010000 1100000
组合后使用便于阅读的大端格式表示为:
00110000 01010000
由于符号位为 1,说明表示负数,则使用补码计算:
首先求反码:00001111 10101111
然后 +1 -> 00001111 10110000
111110110000 的十进制结果为 4016,添加负号为 -4016。
uleb128p1
uleb128p1 用于表示一个有符号值,它的编码方法为 uleb128 的值加 1,那么解码时首先以 uleb128 格式解析值,然后减去 1,主要是为了表示 -1 这个常用数而节省空间。
LEB128 类型的一些典型值:
| Binary | Hex | sleb128 | uleb128 | uleb128p1 |
|---|---|---|---|---|
| 00000000 | 00 | 0 | 0 | -1 |
| 00000001 | 01 | 1 | 1 | 0 |
| 01111111 | 7f | -1 | 127 | 126 |
| 10000000 01111111 | 80 7f | -128 | 16256 | 16255 |
DEX 文件结构图
DEX 文件的总体结构可以用以下示意图表示:
+------------------------+
| header_item | 文件头 (0x70 字节)
+------------------------+
| string_ids | 字符串ID列表
+------------------------+
| type_ids | 类型ID列表
+------------------------+
| proto_ids | 方法原型ID列表
+------------------------+
| field_ids | 字段ID列表
+------------------------+
| method_ids | 方法ID列表
+------------------------+
| class_defs | 类定义列表
+------------------------+
| call_site_ids | 调用站点ID列表(可选)
+------------------------+
| method_handle_ids | 方法句柄ID列表(可选)
+------------------------+
| data | 数据区域
| | ├─ string_data_item
| | ├─ type_list
| | ├─ code_item
| | ├─ class_data_item
| | ├─ map_list
| | └─ debug_info_item
+------------------------+
DEX 文件结构
DEX 文件的总体结构包含以下主要部分:
- header_item - 文件头部,包含文件的基本信息和各个段的偏移量
- string_ids - 字符串标识符列表
- type_ids - 类型标识符列表
- proto_ids - 方法原型标识符列表
- field_ids - 字段标识符列表
- method_ids - 方法标识符列表
- class_defs - 类定义列表
- map_list - 数据区段映射列表
- code_item - 代码项
- debug_info_item - 调试信息项
DEX 结构详细说明
header_item
DEX 文件的头部具有一个 header_item 结构,它存放了整个 DEX 文件的元信息,描述了一个 DEX 文件的概要结构。
struct header_item {
/* 魔数 */
u1 magic[8];
/* 文件剩余内容(除 magic 和此字段之外的所有内容)的 adler32 校验和 */
u4 checksum;
/* 文件剩余内容的 SHA-1 签名(哈希),用于对文件进行唯一标识 */
u1 signature[20];
/* 整个文件(包括标头)的大小,以字节为单位 */
u4 file_size;
/* 标头(整个区段)的大小,以字节为单位 */
u4 header_size;
/* 字节序标记 */
u4 endian_tag;
/* 链接区段的大小 */
u4 link_size;
/* 从文件开头到链接区段的偏移量 */
u4 link_off;
/* 映射列表的偏移量 */
u4 map_off;
/* string_id_item 的个数 */
u4 string_ids_size;
/* string_id_item 列表的偏移量 */
u4 string_ids_off;
/* type_id_item 的个数 */
u4 type_ids_size;
/* type_id_item 列表的偏移量 */
u4 type_ids_off;
/* proto_id_item 的个数 */
u4 proto_ids_size;
/* proto_id_item 列表的偏移量 */
u4 proto_ids_off;
/* field_id_item 的个数 */
u4 field_ids_size;
/* field_id_item 列表的偏移量 */
u4 field_ids_off;
/* method_id_item 的个数 */
u4 method_ids_size;
/* method_id_item 列表的偏移量 */
u4 method_ids_off;
/* class_def_item 的个数 */
u4 class_defs_size;
/* class_def_item 列表的偏移量 */
u4 class_defs_off;
/* 数据区段的大小,以字节为单位 */
u4 data_size;
/* 数据区段开始的偏移量 */
u4 data_off;
};
string_ids
字符串标识符列表,存储所有字符串的索引信息。每个 string_id_item 结构包含:
struct string_id_item {
u4 string_data_off; // 字符串数据的偏移量
};
对应的字符串数据结构为:
struct string_data_item {
uleb128 utf16_size; // 字符串的UTF-16代码单元数量
ubyte data[utf16_size]; // 字符串数据,以MUTF-8格式编码,以0x00结尾
};
MUTF-8(Modified UTF-8)编码规则:
- 空字符(U+0000)编码为 0xC0 0x80
- 其他字符按标准 UTF-8 编码
- 字符串以 NULL 字节(0x00)结尾
type_ids
类型标识符列表,存储所有类型的索引信息。每个 type_id_item 结构包含:
struct type_id_item {
u4 descriptor_idx; // 指向 string_ids 列表的索引,表示类型描述符
};
proto_ids
方法原型标识符列表,描述方法的签名信息。每个 proto_id_item 结构包含:
struct proto_id_item {
u4 shorty_idx; // 指向 string_ids 的索引,表示方法的简短描述符
u4 return_type_idx; // 指向 type_ids 的索引,表示返回值类型
u4 parameters_off; // 指向参数列表的偏移量,如果没有参数则为 0
};
field_ids
字段标识符列表,存储所有字段的索引信息。每个 field_id_item 结构包含:
struct field_id_item {
u2 class_idx; // 字段所属类的索引,指向 type_ids
u2 type_idx; // 字段类型的索引,指向 type_ids
u4 name_idx; // 字段名称的索引,指向 string_ids
};
method_ids
方法标识符列表,存储所有方法的索引信息。每个 method_id_item 结构包含:
struct method_id_item {
u2 class_idx; // 方法所属类的索引
u2 proto_idx; // 方法原型的索引
u4 name_idx; // 方法名称的索引
};
class_defs
类定义列表,包含类的详细信息。每个 class_def_item 结构包含:
struct class_def_item {
u4 class_idx; // 类的类型索引
u4 access_flags; // 访问标志
u4 superclass_idx; // 父类的类型索引
u4 interfaces_off; // 接口列表的偏移量
u4 source_file_idx; // 源文件名的字符串索引
u4 annotations_off; // 注解的偏移量
u4 class_data_off; // 类数据的偏移量
u4 static_values_off; // 静态字段初始值的偏移量
};
call_site_ids
调用站点标识符列表(用于 invokedynamic 指令)。每个 call_site_id_item 结构包含:
struct call_site_id_item {
u4 call_site_off; // 调用站点数据的偏移量
};
map_list
数据区段映射列表,描述文件中各个数据段的位置和大小:
struct map_list {
u4 size; // 列表中的条目数量
map_item list[size]; // 映射项列表
};
struct map_item {
u2 type; // 项目类型,例如 TYPE_HEADER_ITEM
u2 unused; // 未使用,必须为零
u4 size; // 在指定偏移量处找到的项目计数
u4 offset; // 从文件开头到项目的偏移量
};
type_list
类型列表,用于表示方法参数列表或接口列表:
struct type_list {
u4 size; // 列表中的条目数量
type_item list[size]; // 类型项列表
};
struct type_item {
u2 type_idx; // 类型的索引,指向 type_ids
};
class_data_item
类数据项,包含类的字段和方法的详细信息:
struct class_data_item {
uleb128 static_fields_size; // 静态字段的数量
uleb128 instance_fields_size; // 实例字段的数量
uleb128 direct_methods_size; // 直接方法的数量
uleb128 virtual_methods_size; // 虚方法的数量
encoded_field static_fields[static_fields_size]; // 静态字段
encoded_field instance_fields[instance_fields_size]; // 实例字段
encoded_method direct_methods[direct_methods_size]; // 直接方法
encoded_method virtual_methods[virtual_methods_size]; // 虚方法
};
struct encoded_field {
uleb128 field_idx_diff; // 字段索引与前一个字段索引的差值
uleb128 access_flags; // 字段的访问标志
};
struct encoded_method {
uleb128 method_idx_diff; // 方法索引与前一个方法索引的差值
uleb128 access_flags; // 方法的访问标志
uleb128 code_off; // 指向 code_item 的偏移量,如果为 0 则表示抽象或本地方法
};
code_item
代码项,包含方法的字节码和其他相关信息:
struct code_item {
u2 registers_size; // 此代码使用的寄存器数量
u2 ins_size; // 方法的传入参数的字数
u2 outs_size; // 此代码进行方法调用所需的传出参数空间的字数
u2 tries_size; // 此实例的 try_item 数量,可以为零
u4 debug_info_off; // 从文件开头到此代码的调试信息序列的偏移量
u4 insns_size; // 指令列表的大小(以 16 位代码单元为单位)
u2 insns[insns_size]; // 字节码的实际数组
u2 padding; // (可选) 使 tries 4 字节对齐的填充
try_item tries[tries_size]; // (可选) 异常处理器信息的数组
encoded_catch_handler_list handlers; // (可选) 异常处理器数据
};
struct try_item {
u4 start_addr; // 覆盖代码的起始地址
u2 insn_count; // 覆盖的 16 位代码单元的数量
u2 handler_off; // 到关联的 encoded_catch_handler 的字节偏移量
};
debug_info_item
调试信息项,包含源代码行号、局部变量名等调试信息:
struct debug_info_item {
uleb128 line_start; // 对应于此序列的初始行号
uleb128 parameters_size; // 参数名称的数量
uleb128p1 parameter_names[parameters_size]; // 参数的字符串索引
// 后续为调试字节码序列
};
DEX 文件解析实例
构建 DEX 文件
首先创建一个简单的 Java 类:
// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
编译和转换为 DEX:
javac Hello.java
dx --dex --output=Hello.dex Hello.class
执行 DEX 文件
在 Android 设备上执行:
adb push Hello.dex /data/local/tmp/
adb shell dalvikvm -cp /data/local/tmp/Hello.dex Hello
解析 DEX 文件
打开 DEX 文件
FILE *fp = fopen("Hello.dex", "rb");
if (fp == NULL) {
printf("无法打开文件\n");
return -1;
}
解析 header_item

4754

被折叠的 条评论
为什么被折叠?



