DEX文件结构解析:从头文件到类定义的深入分析
字数 5103 2025-09-23 19:27:38
DEX 文件结构解析:从头文件到类定义的深入分析
1. DEX 文件概述
DEX(Dalvik Executable)是 Android 系统的核心可执行文件格式,专为 Android 平台设计,用于存储和执行应用程序代码。相较于标准的 Java 字节码(.class 文件),DEX 文件具有体积更小、内存占用更少、加载速度更快、类查找更快等优点,使其更适合移动设备环境。
生成方式:Java 代码 (.java) 先编译为 Java 字节码 (.class),然后通过 dx 或 d8 工具将多个 .class 文件合并、优化并转换为一个 .dex 文件。
javac HelloWorld2.java
dx --dex --output=./HelloWorld2.dex HelloWorld2.class
样例代码:
public class HelloWorld2 {
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;
HelloWorld2 helloWorld = new HelloWorld2();
String sayNumber = String.valueOf(helloWorld.getNumber(c, d));
System.out.println("HelloDex!" + sayNumber);
}
}
2. DEX 文件整体结构
一个 DEX 文件由以下部分组成,按顺序排列:
- Dex Header:文件头,包含元数据并指向其他部分的偏移量。
- String Table:字符串索引表 (
string_ids)。 - Type Table:类型索引表 (
type_ids)。 - Proto Table:方法原型索引表 (
proto_ids)。 - Field Table:字段索引表 (
field_ids)。 - Method Table:方法索引表 (
method_ids)。 - Class Def Table:类定义表 (
class_defs)。 - Data Section:数据区,包含上述索引表实际指向的字符串、代码、调试信息等所有具体数据。
- Map List:映射列表,可选区块,详细列出了文件各个部分的位置和大小,用于快速遍历。
核心数据结构 (C 语言视角):
struct DexFile {
const DexHeader* pHeader; // DEX 文件头
const DexStringId* pStringIds; // 字符串索引数组
const DexTypeId* pTypeIds; // 类型索引数组
const DexFieldId* pFieldIds; // 字段索引数组
const DexMethodId* pMethodIds; // 方法索引数组
const DexProtoId* pProtoIds; // 原型索引数组
const DexClassDef* pClassDefs; // 类定义数组
const DexLink* pLinkData; // 链接数据
const void* baseAddr; // 指向 DEX 文件起始地址
// ... 其他辅助字段 ...
};
注意:文档中提到的 u4 等类型在某些上下文中可能指 uleb128 变长编码(占 1-5 字节),而非固定 4 字节。
3. Dex Header(文件头)详解
文件头是 DEX 文件的起点和目录,固定为 0x70 字节。其结构定义如下:
struct DexHeader {
u1 magic[8]; // 魔数,标识文件类型: "dex\n035\0" 或 {0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00}
u4 checksum; // 除 magic 和本字段外,文件内容的 Adler-32 校验和
u1 signature[kSHA1DigestLen]; // 除 magic, checksum 和本字段外,文件内容的 SHA-1 签名 (20 字节)
u4 fileSize; // 整个 DEX 文件的大小(字节)
u4 headerSize; // 本头结构的大小(始终为 0x70)
u4 endianTag; // 字节序标记,小端为 0x12345678
u4 linkSize; // 链接段大小(通常为 0)
u4 linkOff; // 链接段偏移(通常为 0)
u4 mapOff; // MapList 的偏移量
u4 stringIdsSize; // string_ids 中的元素个数
u4 stringIdsOff; // string_ids 的偏移量
u4 typeIdsSize; // type_ids 中的元素个数
u4 typeIdsOff; // type_ids 的偏移量
u4 protoIdsSize; // proto_ids 中的元素个数
u4 protoIdsOff; // proto_ids 的偏移量
u4 fieldIdsSize; // field_ids 中的元素个数
u4 fieldIdsOff; // field_ids 的偏移量
u4 methodIdsSize; // method_ids 中的元素个数
u4 methodIdsOff; // method_ids 的偏移量
u4 classDefsSize; // class_defs 中的元素个数
u4 classDefsOff; // class_defs 的偏移量
u4 dataSize; // 数据区的大小
u4 dataOff; // 数据区的偏移量
};
关键字段说明:
magic:唯一标识这是一个有效的 DEX 文件。checksum&signature:用于验证文件完整性和真实性,防止篡改。endianTag:规定文件中整数字段的字节序(小端)。*Size/*Off对:这是文件头的核心作用。每一对(如stringIdsSize和stringIdsOff)指明了文件中一个关键区块(string_ids)有多少个条目以及从文件的哪个偏移地址开始读取。解析器通过读取文件头,再根据这些指针跳转到相应位置进行解析。
4. 各索引表解析
4.1. string_ids (字符串索引表)
- 作用:存储 DEX 文件中用到的所有字符串的偏移指针。
- 结构:一个
DexStringId数组,每个条目(u4 string_data_off)是一个指向数据区中实际字符串存储位置的偏移量。 - 解析流程:
- 从
header->stringIdsOff找到string_ids表起始位置。 - 按索引读取
DexStringId,得到string_data_off。 - 跳转到
string_data_off指向的数据区位置。 - 该位置以一个
uleb128编码的整数开头,表示字符串的长度。 - 紧接着是以
MUTF-8编码的字符串数据,以空字符\0结尾。
- 从
- 示例:第一个
DexStringId的string_data_off为0x02DA,在此偏移处读取到长度0x08,随后是 8 字节的字符串数据<clinit>。
4.2. type_ids (类型索引表)
- 作用:存储 DEX 文件中引用的所有类型(类、接口、数组、基本类型)。
- 结构:一个
DexTypeId数组,每个条目(u4 descriptor_idx)是一个索引值,指向string_ids表中的某个字符串。 - 解析流程:
- 从
header->typeIdsOff找到type_ids表。 - 读取
descriptor_idx。 - 以
descriptor_idx为索引,去string_ids表中找到对应的字符串描述符(如"I"表示int,"Ljava/lang/String;"表示String类)。
- 从
- 示例:第一个
DexTypeId的descriptor_idx为5,对应string_ids[5]的字符串是"I"。
4.3. proto_ids (原型索引表)
- 作用:描述方法的原型(返回类型 + 参数列表),用于复用方法签名。
- 结构:
DexProtoId数组,每个条目包含:u4 shorty_idx:指向string_ids,一个简短的签名描述(如"III"表示int (int, int))。u4 return_type_idx:指向type_ids,表示返回类型。u4 parameters_off:指向数据区的一个DexTypeList,描述参数列表(可为 0)。
- 解析流程:
- 找到
proto_ids表。 - 解析
shorty_idx和return_type_idx。 - 如果
parameters_off不为 0,则跳转到该偏移,读取DexTypeList(包含一个size和一个DexTypeItem[size]数组,每个DexTypeItem包含一个指向type_ids的索引)。
- 找到
- 示例:一个
proto_id的shorty_idx指向"III",return_type_idx指向int,参数列表包含两个int类型。最终解析出方法原型:int (int, int)。
4.4. field_ids (字段索引表)
- 作用:描述字段的引用信息(在哪个类、什么类型、叫什么名字)。
- 结构:
DexFieldId数组,每个条目包含:u2 class_idx:指向type_ids,表示字段所属的类。u2 type_idx:指向type_ids,表示字段的类型。u4 name_idx:指向string_ids,表示字段的名称。
- 解析流程:直接组合三个索引指向的内容即可。
- 示例:
class_idx->"LHelloWorld2;",type_idx->"I",name_idx->"a"。合并得到字段:int HelloWorld2.a。
4.5. method_ids (方法索引表)
- 作用:描述方法的引用信息(在哪个类、什么原型、叫什么名字)。
- 结构:
DexMethodId数组,每个条目包含:u2 class_idx:指向type_ids,表示方法所属的类。u2 proto_idx:指向proto_ids,表示方法的原型(返回类型+参数)。u4 name_idx:指向string_ids,表示方法的名称。
- 解析流程:组合三个索引指向的内容。
- 示例:
class_idx->"LHelloWorld2;",proto_idx->"void ()",name_idx->"<clinit>"。合并得到方法:void HelloWorld2.<clinit>()(静态初始化器)。
5. class_defs (类定义表) 详解
这是解析 DEX 文件的最终目标,它定义了类的具体结构。
- 结构:一个
DexClassDef数组,每个条目包含类的所有元信息:struct DexClassDef { u4 classIdx; // 指向 type_ids,表示本类的类型 u4 accessFlags; // 访问标志(如 public, final) u4 superclassIdx; // 指向 type_ids,表示父类 u4 interfacesOff; // 指向 DexTypeList,表示实现的接口(可为 0) u4 sourceFileIdx; // 指向 string_ids,表示源文件名(可为 0) u4 annotationsOff; // 指向注解信息(可为 0) u4 classDataOff; // **关键**:指向数据区的 class_data_item 结构 u4 staticValuesOff; // 指向静态变量的初始值(可为 0) }; - 解析核心 -
class_data_item:
class_dataOff指向的class_data_item结构包含了类的具体成员信息,它以紧凑的uleb128格式存储:uleb128 static_fields_size:静态字段个数。uleb128 instance_fields_size:实例字段个数。uleb128 direct_methods_size:直接方法个数(构造方法、私有方法、静态方法)。uleb128 virtual_methods_size:虚方法个数。- 随后是字段和方法列表:
- 字段列表:由
encoded_field组成,包含field_idx(指向field_ids)和访问标志access_flags。 - 方法列表:由
encoded_method组成,包含method_idx(指向method_ids)、访问标志access_flags以及至关重要的code_off(指向数据区的DexCode结构,该方法的具体字节码指令、寄存器信息、调试信息等都存储于此)。
- 字段列表:由
示例类解析流程:
classIdx->type_ids->string_ids->"LHelloWorld2;"accessFlags=0x1->ACC_PUBLICsuperclassIdx->type_ids->string_ids->"Ljava/lang/Object;"classDataOff->0x4A1,跳转到此处的class_data_item- 读取
class_data_item:static_fields_size= 1- 解析
encoded_field:field_idx->field_ids[1]->String HelloWorld2.b,access_flags->STATIC
- 解析
instance_fields_size= 1- 解析
encoded_field:field_idx->field_ids[0]->int HelloWorld2.a
- 解析
direct_methods_size= 3- 解析
encoded_method:method_idx->method_ids[0]->void HelloWorld2.<clinit>(),access_flags->STATIC | CONSTRUCTOR - ...(其他直接方法,如构造函数
<init>)
- 解析
virtual_methods_size= 1- 解析
encoded_method:method_idx->method_ids[?]->int HelloWorld2.getNumber(int, int),access_flags->PUBLIC code_off-> 指向DexCode结构,其中包含getNumber方法的实际字节码指令(如add-int等)。
- 解析
6. 总结
DEX 文件的结构设计高效且紧凑:
- 分层索引:通过文件头定位多个索引表,索引表又指向数据区的具体内容。这种设计避免了重复数据的存储(如相同的字符串、方法签名只需存一份)。
- 逻辑分离:
method_ids等表存储引用信息,而class_defs和class_data_item通过code_off管理实现信息(字节码)。 - 紧凑编码:大量使用
uleb128等变长编码节省空间。 - 解析顺序:解析 DEX 文件通常遵循
Header->string_ids-> 其他索引表 ->class_defs->class_data_item->DexCode的顺序,逐步解析并建立相互关联。
理解 DEX 文件结构是进行 Android 应用逆向分析、安全审计、性能优化和深入理解 Android 运行时机制的基础。
参考链接: