DEX文件结构解析:从头文件到类定义的深入分析
字数 5103 2025-09-23 19:27:38

DEX 文件结构解析:从头文件到类定义的深入分析

1. DEX 文件概述

DEX(Dalvik Executable)是 Android 系统的核心可执行文件格式,专为 Android 平台设计,用于存储和执行应用程序代码。相较于标准的 Java 字节码(.class 文件),DEX 文件具有体积更小、内存占用更少、加载速度更快、类查找更快等优点,使其更适合移动设备环境。

生成方式:Java 代码 (.java) 先编译为 Java 字节码 (.class),然后通过 dxd8 工具将多个 .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 文件由以下部分组成,按顺序排列:

  1. Dex Header:文件头,包含元数据并指向其他部分的偏移量。
  2. String Table:字符串索引表 (string_ids)。
  3. Type Table:类型索引表 (type_ids)。
  4. Proto Table:方法原型索引表 (proto_ids)。
  5. Field Table:字段索引表 (field_ids)。
  6. Method Table:方法索引表 (method_ids)。
  7. Class Def Table:类定义表 (class_defs)。
  8. Data Section:数据区,包含上述索引表实际指向的字符串、代码、调试信息等所有具体数据。
  9. 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 对:这是文件头的核心作用。每一对(如 stringIdsSizestringIdsOff)指明了文件中一个关键区块(string_ids有多少个条目以及从文件的哪个偏移地址开始读取。解析器通过读取文件头,再根据这些指针跳转到相应位置进行解析。

4. 各索引表解析

4.1. string_ids (字符串索引表)

  • 作用:存储 DEX 文件中用到的所有字符串的偏移指针
  • 结构:一个 DexStringId 数组,每个条目 (u4 string_data_off) 是一个指向数据区中实际字符串存储位置的偏移量。
  • 解析流程
    1. header->stringIdsOff 找到 string_ids 表起始位置。
    2. 按索引读取 DexStringId,得到 string_data_off
    3. 跳转到 string_data_off 指向的数据区位置。
    4. 该位置以一个 uleb128 编码的整数开头,表示字符串的长度
    5. 紧接着是以 MUTF-8 编码的字符串数据,以空字符 \0 结尾。
  • 示例:第一个 DexStringIdstring_data_off0x02DA,在此偏移处读取到长度 0x08,随后是 8 字节的字符串数据 <clinit>

4.2. type_ids (类型索引表)

  • 作用:存储 DEX 文件中引用的所有类型(类、接口、数组、基本类型)。
  • 结构:一个 DexTypeId 数组,每个条目 (u4 descriptor_idx) 是一个索引值,指向 string_ids 表中的某个字符串。
  • 解析流程
    1. header->typeIdsOff 找到 type_ids 表。
    2. 读取 descriptor_idx
    3. descriptor_idx 为索引,去 string_ids 表中找到对应的字符串描述符(如 "I" 表示 int"Ljava/lang/String;" 表示 String 类)。
  • 示例:第一个 DexTypeIddescriptor_idx5,对应 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)。
  • 解析流程
    1. 找到 proto_ids 表。
    2. 解析 shorty_idxreturn_type_idx
    3. 如果 parameters_off 不为 0,则跳转到该偏移,读取 DexTypeList(包含一个 size 和一个 DexTypeItem[size] 数组,每个 DexTypeItem 包含一个指向 type_ids 的索引)。
  • 示例:一个 proto_idshorty_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 结构,该方法的具体字节码指令、寄存器信息、调试信息等都存储于此)。

示例类解析流程

  1. classIdx -> type_ids -> string_ids -> "LHelloWorld2;"
  2. accessFlags = 0x1 -> ACC_PUBLIC
  3. superclassIdx -> type_ids -> string_ids -> "Ljava/lang/Object;"
  4. classDataOff -> 0x4A1,跳转到此处的 class_data_item
  5. 读取 class_data_item
    • static_fields_size = 1
      • 解析 encoded_fieldfield_idx -> field_ids[1] -> String HelloWorld2.baccess_flags -> STATIC
    • instance_fields_size = 1
      • 解析 encoded_fieldfield_idx -> field_ids[0] -> int HelloWorld2.a
    • direct_methods_size = 3
      • 解析 encoded_methodmethod_idx -> method_ids[0] -> void HelloWorld2.<clinit>()access_flags -> STATIC | CONSTRUCTOR
      • ...(其他直接方法,如构造函数 <init>
    • virtual_methods_size = 1
      • 解析 encoded_methodmethod_idx -> method_ids[?] -> int HelloWorld2.getNumber(int, int)access_flags -> PUBLIC
      • code_off -> 指向 DexCode 结构,其中包含 getNumber 方法的实际字节码指令(如 add-int 等)。

6. 总结

DEX 文件的结构设计高效且紧凑:

  1. 分层索引:通过文件头定位多个索引表,索引表又指向数据区的具体内容。这种设计避免了重复数据的存储(如相同的字符串、方法签名只需存一份)。
  2. 逻辑分离method_ids 等表存储引用信息,而 class_defsclass_data_item 通过 code_off 管理实现信息(字节码)。
  3. 紧凑编码:大量使用 uleb128 等变长编码节省空间。
  4. 解析顺序:解析 DEX 文件通常遵循 Header -> string_ids -> 其他索引表 -> class_defs -> class_data_item -> DexCode 的顺序,逐步解析并建立相互关联。

理解 DEX 文件结构是进行 Android 应用逆向分析、安全审计、性能优化和深入理解 Android 运行时机制的基础。


参考链接

DEX 文件结构解析:从头文件到类定义的深入分析 1. DEX 文件概述 DEX (Dalvik Executable)是 Android 系统的核心可执行文件格式,专为 Android 平台设计,用于存储和执行应用程序代码。相较于标准的 Java 字节码(.class 文件),DEX 文件具有 体积更小、内存占用更少、加载速度更快、类查找更快 等优点,使其更适合移动设备环境。 生成方式 :Java 代码 ( .java ) 先编译为 Java 字节码 ( .class ),然后通过 dx 或 d8 工具将多个 .class 文件合并、优化并转换为一个 .dex 文件。 样例代码 : 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 语言视角) : 注意 :文档中提到的 u4 等类型在某些上下文中可能指 uleb128 变长编码(占 1-5 字节),而非固定 4 字节。 3. Dex Header(文件头)详解 文件头是 DEX 文件的起点和目录,固定为 0x70 字节。其结构定义如下: 关键字段说明 : 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 数组,每个条目包含类的所有元信息: 解析核心 - 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_PUBLIC superclassIdx -> 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 运行时机制的基础。 参考链接 : Android 官方 DEX 格式文档 看雪论坛相关文章 看雪论坛相关文章