DEX 文件头部位于 APK(ZIP)中 .dex 条目的 data 区起始处,需先解析 ZIP 结构定位 local header 偏移,再读取其后 data 区;DEX header 固定 112 字节,magic 必为 "dex\n035\0" 等合法值,header_size 必为 0x70,且须按 endian_tag 校验字节序。

DEX 文件头部结构在 APK 中的定位位置
APK 本质是 ZIP,DEX 不是独立文件而是 ZIP 中的条目(entry),常见名如 classes.dex、classes2.dex。直接用 fopen 打开 APK 并 fseek 到固定偏移会失败——ZIP 的 central directory 和 local file header 会干扰原始字节偏移。
必须先解析 ZIP 结构,找到 DEX 条目的 local header offset,再读取其 data 区域起始位置。Android 构建工具(如 aapt2)可能将 DEX 放在 ZIP 的末尾(如 multidex 场景),也可能压缩(尽管实际中 DEX 总是 STORED,即压缩方式为 0)。关键字段在 ZIP local file header 后紧接的 data 区,也就是 DEX 的真实起始。
- 不要假设
classes.dex一定在 ZIP 第一个 entry;需遍历 central directory 获取所有.dex条目 - 检查 ZIP entry 的
compression method字段必须为0(STORED),否则需解压(但 Android 官方工具从不压缩 DEX) - ZIP 中 DEX 数据前可能有额外 padding(极少见),但标准 Android 构建下无 padding;可依赖
compressed size == uncompressed size验证
解析 DEX header 的 C++ 基础内存映射方式
拿到 DEX 数据起始地址后,DEX header 是固定 112 字节(0x70),结构定义在 libdex 或 AOSP 的 dalvik/vm/oo/DexFile.h。C++ 中最稳妥做法是用 struct 按字节对齐显式声明,并禁用编译器自动填充。
#pragma pack(1)
struct DexHeader {
uint8_t magic[8]; // "dex\n035\0" or "dey\n035\0"
uint32_t checksum;
uint8_t signature[20];
uint32_t file_size;
uint32_t header_size; // must be 0x70
uint32_t endian_tag; // 0x12345678 (little) or 0x78563412 (big)
uint32_t link_size;
uint32_t link_off;
uint32_t map_off;
uint32_t string_ids_size;
uint32_t string_ids_off;
uint32_t type_ids_size;
uint32_t type_ids_off;
uint32_t proto_ids_size;
uint32_t proto_ids_off;
uint32_t field_ids_size;
uint32_t field_ids_off;
uint32_t method_ids_size;
uint32_t method_ids_off;
uint32_t class_defs_size;
uint32_t class_defs_off;
uint32_t data_size;
uint32_t data_off;
};
#pragma pack()
注意:endian_tag 决定后续所有多字节字段是否需字节序翻转(Android DEX 全部小端,但字段仍按规范校验);magic 必须严格匹配,含换行与版本号,"dex\n035<p>注意:<code>endian_tag 决定后续所有多字节字段是否需字节序翻转(Android DEX 全部小端,但字段仍按规范校验);magic 必须严格匹配,含换行与版本号,"dex\n035\0" 对应 DEX 035(Android 7.0+ 主流),"dey\n035\0" 表示优化过的 odex(但 APK 内不含 odex)。
"dey\n035<p>注意:<code>endian_tag 决定后续所有多字节字段是否需字节序翻转(Android DEX 全部小端,但字段仍按规范校验);magic 必须严格匹配,含换行与版本号,"dex\n035\0" 对应 DEX 035(Android 7.0+ 主流),"dey\n035\0" 表示优化过的 odex(但 APK 内不含 odex)。" 表示优化过的 odex(但 APK 内不含 odex)。
立即学习“C++免费学习笔记(深入)”;
- 务必用
memcmp(header.magic, "dex\n035\0", 8)校验 magic,不能只比前 3 字符 -
header_size必须等于0x70,否则不是标准 DEX(某些定制 ROM 可能扩展,但兼容性极差) - 读取
uint32_t字段前,若endian_tag != 0x12345678,需手动bswap_32;glibc 提供be32toh/le32toh,或手写宏
常见错误:误把 ZIP 中央目录当 DEX 起始 / 混淆 odex 和 dex
典型错误是直接从 APK 文件开头硬跳 0x20 或 0x70 字节,结果读到 ZIP local file header 的签名(0x04034b50)而非 DEX magic(0x6465780a)。另一个高频坑是把系统 /system/framework/*.odex 的解析逻辑套用到 APK 内 DEX——odex 有额外的 dependency 和 opt header,而 APK 中的 DEX 是纯净的、未优化的。
- 用十六进制查看器打开 APK,搜索
64 65 78 0a 30 33 35 00(即 "dex\n035\0" 十六进制),确认其位置不在文件头,而在某个 ZIP entry data 区内 - 遇到
"dey\n035\0",说明该文件不是 APK 内的 DEX,而是设备上的 odex 或 oat 的组成部分,不可直接按 DEX header 解析 - 某些加固 APK 会加密
classes.dex条目(压缩方式非 0,或数据异或),此时 ZIP 层能识别 entry,但解出数据后 magic 校验失败——这是加固特征,不是解析代码问题
进阶:支持 multi-dex 和 DEX version 037/038
Android 5.0+ 支持 DEX 037("dex\n037\0"),Android 7.0+ 引入 038("dex\n038\0"),主要变化在 map_off 后新增 data_off 和 data_size 字段含义扩展(如支持 call site IDs),但 header 固定仍是 0x70 字节。multi-dex 下,APK 中存在多个 DEX entry,命名规则为 classes.dex、classes2.dex…classesN.dex,需全部提取并单独解析。
- 037/038 的
header_size仍为0x70,无需动态计算 header 长度;version 差异体现在字段语义,不影响 header 解析流程 - 遍历 ZIP central directory 时,用
std::string(entry_name).ends_with(".dex")比硬编码"classes*.dex"更健壮(某些插件化框架用plugin.dex) - 解析
map_off指向的DexMapList时,需根据 DEX version 判断type字段是否支持新项(如kDexTypeCallSiteIdItem),否则越界读取
真正难的不是读 header,而是后续 string_id/type_id 等索引表的间接寻址——每个 offset 都依赖 base_addr + offset,且 offset 可能为 0(表示 null),这些细节一旦漏判,解析就会崩在第一个字符串读取上。



















