3. DEX 文件头与类索引解析

DEX 文件头与类索引解析 学习卡片

DEX 文件头与索引结构解析

DEX 文件是 Android 应用的核心字节码容器,其头部固定 112 字节,随后依次排列 string_ids、type_ids、proto_ids、field_ids、method_ids 和 class_defs 等索引表。理解这些结构的偏移与数量关系,是手动提取类名与方法签名的前提。

DEX 文件头与索引表布局(偏移单位:字节) header_item (0x00 ~ 0x6F, 112 bytes) magic[8] checksum signature[20] file_size header_size endian_tag link_size link_off map_off string_ids_size string_ids_off type_ids_size type_ids_off proto_ids_size proto_ids_off field_ids_size field_ids_off method_ids_size method_ids_off class_defs_size string_ids[] 每个条目 4 字节 存储 string_data_off 指向 MUTF-8 字符串 索引 i → 第 i 个字符串 type_ids[] 每个条目 4 字节 存储 descriptor_idx 指向 string_ids 中的类型描述符 如 "Ljava/lang/String;" class_defs[] 每个条目 32 字节 class_idx → type_ids superclass_idx / interfaces_off class_data_off → 方法/字段 method_ids[] 每个条目 8 字节 class_idx (2B) + proto_idx (2B) + name_idx (4B) proto_ids[] 每个条目 12 字节 shorty_idx + return_type_idx + parameters_off class_data_off 解析出 direct_methods / virtual_methods dexdump 验证对照 dexdump -f classes.dex 对比 class_defs 数量 对比 method_ids 签名

手动解析关键步骤

使用 Python 的 struct 模块按小端序读取 DEX 文件。头部字段的偏移与长度如下表所示:

字段偏移长度说明
magic0x008固定为 "dex\n035\0" 或 "dex\n037\0"
checksum0x084Adler-32 校验和
signature0x0C20SHA-1 签名
file_size0x204整个 DEX 文件大小
header_size0x244固定 0x70 (112)
string_ids_size0x384字符串索引数量
string_ids_off0x3C4字符串索引表偏移
type_ids_size0x404类型索引数量
type_ids_off0x444类型索引表偏移
proto_ids_size0x484原型索引数量
proto_ids_off0x4C4原型索引表偏移
method_ids_size0x584方法索引数量
method_ids_off0x5C4方法索引表偏移
class_defs_size0x604类定义数量
class_defs_off0x644类定义表偏移

完整解析脚本

import struct

def read_uleb128(data, offset):
    """读取 ULEB128 编码,返回 (值, 新偏移)"""
    result = 0
    shift = 0
    while True:
        byte = data[offset]
        offset += 1
        result |= (byte & 0x7F) << shift
        if (byte & 0x80) == 0:
            break
        shift += 7
    return result, offset

def parse_dex(filepath):
    with open(filepath, 'rb') as f:
        data = f.read()

    # 1. 解析头部
    magic = data[0:8]
    assert magic[:4] == b'dex\n', "不是有效的 DEX 文件"
    file_size = struct.unpack_from('<I', data, 0x20)[0]
    string_ids_size = struct.unpack_from('<I', data, 0x38)[0]
    string_ids_off  = struct.unpack_from('<I', data, 0x3C)[0]
    type_ids_size   = struct.unpack_from('<I', data, 0x40)[0]
    type_ids_off    = struct.unpack_from('<I', data, 0x44)[0]
    proto_ids_size  = struct.unpack_from('<I', data, 0x48)[0]
    proto_ids_off   = struct.unpack_from('<I', data, 0x4C)[0]
    method_ids_size = struct.unpack_from('<I', data, 0x58)[0]
    method_ids_off  = struct.unpack_from('<I', data, 0x5C)[0]
    class_defs_size = struct.unpack_from('<I', data, 0x60)[0]
    class_defs_off  = struct.unpack_from('<I', data, 0x64)[0]

    print(f"[*] DEX 版本: {magic[4:7].decode()}")
    print(f"[*] 文件大小: {file_size} bytes")
    print(f"[*] string_ids: {string_ids_size} @ 0x{string_ids_off:x}")
    print(f"[*] type_ids:   {type_ids_size} @ 0x{type_ids_off:x}")
    print(f"[*] proto_ids:  {proto_ids_size} @ 0x{proto_ids_off:x}")
    print(f"[*] method_ids: {method_ids_size} @ 0x{method_ids_off:x}")
    print(f"[*] class_defs: {class_defs_size} @ 0x{class_defs_off:x}")

    # 2. 解析 string_ids → 字符串列表
    strings = []
    for i in range(string_ids_size):
        str_off = struct.unpack_from('<I', data, string_ids_off + i * 4)[0]
        # string_data_item: uleb128 utf16_size + MUTF-8 数据
        utf16_len, pos = read_uleb128(data, str_off)
        end = data.index(b'\x00', pos)
        s = data[pos:end].decode('utf-8', errors='replace')
        strings.append(s)

    # 3. 解析 type_ids → 类型描述符
    types = []
    for i in range(type_ids_size):
        descriptor_idx = struct.unpack_from('<I', data, type_ids_off + i * 4)[0]
        types.append(strings[descriptor_idx])

    # 4. 解析 proto_ids → 方法原型
    protos = []
    for i in range(proto_ids_size):
        base = proto_ids_off + i * 12
        shorty_idx, return_type_idx, params_off = struct.unpack_from('<III', data, base)
        protos.append({
            'shorty': strings[shorty_idx],
            'return_type': types[return_type_idx],
            'params_off': params_off
        })

    # 5. 解析 method_ids → 方法签名
    methods = []
    for i in range(method_ids_size):
        base = method_ids_off + i * 8
        class_idx, proto_idx, name_idx = struct.unpack_from('<HHI', data, base)
        methods.append({
            'class': types[class_idx],
            'name': strings[name_idx],
            'proto': protos[proto_idx]
        })

    # 6. 解析 class_defs → 类名
    classes = []
    for i in range(class_defs_size):
        base = class_defs_off + i * 32
        class_idx = struct.unpack_from('<I', data, base)[0]
        classes.append(types[class_idx])

    return strings, types, protos, methods, classes

if __name__ == '__main__':
    strings, types, protos, methods, classes = parse_dex('classes.dex')
    print(f"\n[+] 共解析 {len(classes)} 个类:")
    for c in classes[:10]:
        print(f"    {c}")
    print(f"\n[+] 共解析 {len(methods)} 个方法,前 10 个:")
    for m in methods[:10]:
        params = m['proto']['shorty'][1:]
        print(f"    {m['class']}->{m['name']}({params}) : {m['proto']['return_type']}")

与 dexdump 输出对照验证

运行 dexdump -f classes.dex 可查看头部摘要,dexdump -d classes.dex 输出完整类与方法列表。将脚本输出的 class_defs_size 与 dexdump 头部中的 class_defs_size 对比,将方法签名与 dexdump 的 Class descriptor 和 name 字段逐条核对。

验证要点:
1. string_ids_size 与 dexdump 头部 string_ids_size 一致;
2. 类名数量等于 class_defs_size;
3. 方法签名中的参数类型与返回类型与 dexdump 的 proto 行一致;
4. 若 magic 为 dex\n037\0,需注意 DEX 版本 037 新增的 call_site_ids 等字段不影响本节解析逻辑。
常见错误:
- 偏移读取时未使用小端序 <,导致数值异常;
- 字符串解析时未处理 ULEB128 长度前缀,直接按固定长度读取会错位;
- 方法签名中 class_idx 和 proto_idx 为 2 字节,name_idx 为 4 字节,混用会导致索引错乱。