Android Dex文件结构解析

本文详细介绍了Android的Dex文件结构,从header开始,包括DexMapList、DexStringId、DexTypeId、DexProtoId、DexFieldId、DexMethodId和DexTypeClassDefItem等关键区段,阐述了每个区段的作用和数据结构,揭示了DEX文件如何高效存储类名、方法名等信息,以适应移动设备的需求。

Java源文件通过Java编译器生成CLASS文件,再通过dx工具转换为classes.dex文件。
DEX文件从整体上来看是一个索引的结构,类名、方法名、字段名等信息都存储在常量池中,这样能够充分减少存储空间,相较于Java字节码文件更适合手机设备。

DEX文件的相关结构声明定义在/dalvik/libdex/DexFile.h文件中,下面我们先来看一下DEX文件中使用到的数据结构。

表1 dex文件使用到的数据结构
类型含义
u1等同于uint8_t,表示1字节的无符号数
u2等同于uint16_t,表示2字节的无符号数
u4等同于uint32_t,表示4字节的无符号数
u8等同于uint64_t,表示8字节的无符号数
sleb128有符号LEB128,可变长度1~5字节
uleb128无符号LEB128,可变长度1~5字节
uleb128p1无符号LEB128加1,可变长度1~5字节

DEX文件的基本结构如下图所示:

header
string_ids
type_ids
proto_ids
field_ids
method_ids
class_def
data
link_data
图1 DEX文件结构

1. header(基本信息)

header是DEX文件头,包含magic字段、alder32校验值、SHA-1哈希值、string_ids的个数以及偏移地址等。DEX文件的头结构很固定,占用0x70个字节,具体定义代码如下所示(摘自DexFile.h):

/*
 * Direct-mapped "header_item" struct.
 */


struct DexHeader {
    u1  magic[8];           /* includes version number */  
    u4  checksum;           /* adler32 checksum */  
    u1  signature[kSHA1DigestLen]; /* SHA-1 hash */
    u4  fileSize;           /* length of entire file */
    u4  headerSize;         /* offset to start of next section */
    u4  endianTag;
    u4  linkSize;
    u4  linkOff;
    u4  mapOff;
    u4  stringIdsSize;
    u4  stringIdsOff;
    u4  typeIdsSize;
    u4  typeIdsOff;
    u4  protoIdsSize;
    u4  protoIdsOff;
    u4  fieldIdsSize;
    u4  fieldIdsOff;
    u4  methodIdsSize;
    u4  methodIdsOff;
    u4  classDefsSize;
    u4  classDefsOff;
    u4  dataSize;
    u4  dataOff;
};

magic[8]:共8个字节。目前为固定值dex\n035。

checksum:文件校验码,使用alder32算法校验文件除去magic、checksum外余下的所有文件区域,用于检查文件错误。

signature:使用 SHA-1算法hash除去magic,checksum和signature外余下的所有文件区域 ,用于唯一识别本文件 。

fileSize:DEX文件的长度。

headerSize:header大小,一般固定为0x70字节。

endianTag:指定了DEX运行环境的cpu字节序,预设值ENDIAN_CONSTANT等于0x12345678,表示默认采用Little-Endian字节序。


linkSizelinkOff:指定链接段的大小与文件偏移,大多数情况下它们的值都为0。link_size:LinkSection大小,如果为0则表示该DEX文件不是静态链接。link_off用来表示LinkSection距离DEX头的偏移地址,如果LinkSize为0,此值也会为0。

mapOff:DexMapList结构的文件偏移。

stringIdsSizestringIdsOff:DexStringId结构的数据段大小与文件偏移。

typeIdsSizetypeIdsOff:DexTypeId结构的数据段大小与文件偏移。

protoIdsSizeprotoIdsSize:DexProtoId结构的数据段大小与文件偏移。

fieldIdsSizefieldIdsSize:DexFieldId结构的数据段大小与文件偏移。

methodIdsSizemethodIdsSize:DexMethodId结构的数据段大小与文件偏移。

classDefsSizeclassDefsOff:DexClassDef结构的数据段大小与文件偏移。

dataSizedataOff:数据段的大小与文件偏移。


下面我们来看某apk中classes.dex的解析结果,确实与上面的结构一致:

image

2.DexMapList区段(大纲)

Dalvik虚拟机解析DEX文件的内容,最终将其映射成DexMapList数据结构,它实际上包含所有其他区段的结构大纲。DexHeader中的mapOff字段指明了DexMapList结构在DEX文件中的偏移。具体定义代码如下所示:

struct DexMapList {
    u4 size;               /* DexMapItem的个数 */
    DexMapItem list[1];    /* DexMapItem的结构 */
};

struct DexMapItem {   
    u2 type;      /* kDexType开头的类型 */
    u2 unused;  /* 未使用,用于字节对齐 */
    u4 size;    /* type指定类型的个数,它们在dex文件中连续存放 */
    u4 offset;  /* 指定类型数据的文件偏移 */
};

/* type字段为一个枚举常量,通过类型名称很容易判断它的具体类型。 */
/* map item type codes */
enum {
    kDexTypeHeaderItem               = 0x0000,
    kDexTypeStringIdItem             = 0x0001,
    kDexTypeTypeIdItem               = 0x0002,
    kDexTypeProtoIdItem              = 0x0003,
    kDexTypeFieldIdItem              = 0x0004,
    kDexTypeMethodIdItem             = 0x0005,
    kDexTypeClassDefItem             = 0x0006,
    kDexTypeMapList                  = 0x1000,
    kDexTypeTypeList                 = 0x1001,
    kDexTypeAnnotationSetRefList     = 0x1002,
    kDexTypeAnnotationSetItem        = 0x1003,
    kDexTypeClassDataItem            = 0x2000,
    kDexTypeCodeItem                 = 0x2001,
    kDexTypeStringDataItem           = 0x2002,
    kDexTypeDebugInfoItem            = 0x2003,
    kDexTypeAnnotationItem           = 0x2004,
    kDexTypeEncodedArrayItem         = 0x2005,
    kDexTypeAnnotationsDirectoryItem = 0x2006,
};

下面我们来看一下010Editor对某classes.dex文件的解析出的DexMapList结构。上面DexMapList结构中的size字段表示list数组的成员个数,即DexMapItem结构的数量:图中是11h,表示共有17个DexMapItem结构,与图中的list数组大小相符。

image

然后我们再来看下DexMapItem的结构。例如对于下图中的DexMapItem的第一项来说,type等于0说明其是kDexTypeHeaderItem类型的结构;unused一般都为0;size为1代表该结构仅有一个,即只有一个Dex文件头;offset为0代表Dex文件头从0h开始。

image

最后我们将所有DexMapItem结构整理成下表:

类型(type)个数(size)偏移(offset)
kDexTypeHeaderItem(0x0000)0x10x0
kDexTypeStringIdItem(0x0001)0xA1150x70
kDexTypeTypeIdItem(0x0002)0x1D380x284C4
kDexTypeProtoIdItem(0x0003)0x25050x2F9A4
kDexTypeFieldIdItem(0x0004)0x9FB90x4B5E0
kDexTypeMethodIdItem(0x0005)0xC3440x9B3A8
kDexTypeClassDefItem(0x0006)0x189D0xFCDC8
kDexTypeAnnotationSetItem(0x1003)0x10E00x12E168
kDexTypeCodeItem(0x2001)0x96DB0x138A34
kDexTypeAnnotationsDirectoryItem(0x2006)0xCE60x4A3EEC
kDexTypeTypeList(0x1001)0x16200x4B9894
kDexTypeStringDataItem(0x2002)0xA1150x4C74CA
kDexTypeDebugInfoItem(0x2003)0x8FCC0x5C8544
kDexTypeAnnotationItem(0x2004)0x101C0x63FBC1
kDexTypeEncodedArrayItem(0x2005)0x10E0x653536
kDexTypeClassDataItem(0x2000)0x184F0x65B97A
kDexTypeMapList(0x1000)0x10x6B8828

可以看出,其中区段的offset与header中的off是完全相等的。


3.DexStringId区段(字符串)

struct DexStringId {
    u4 stringDataOff;   /* 字符串数据偏移 */
}

DexStringId结构只有一个stringDataOff字段,直接指向字符串数据。这个区段中包含了DEX文件中用到的所有字符串。

4.DexTypeId区段(类名/类型名称字符串)

struct DexTypeId {
    u4 descriptorIdx;    /* 指向 DexStringId列表的索引 */
};

descriptorIdx为指向DexStringId列表的索引,它对应的字符串代表了具体类的类型(DEX文件中用到的所有基本数据类型和类的名称)。如下图中的第一项值为0xAEB,表示其是DexStringId中第0xAEB(2795)项;而第8项值为0x1969,表示其是DexStringId中第0x1969(6505)项。经过我们的验证,以上分析是正确的。

image

image

image

5.DexProtoId区段(方法声明=返回类型 + 参数列表)

struct DexProtoId {
    u4 shortyIdx;   /* 指向DexStringId列表的索引 */
    u4 returnTypeIdx;   /* 指向DexTypeId列表的索引 */
    u4 parametersOff;   /* 指向DexTypeList的偏移 */
}

struct DexTypeList {
    u4 size;             /* 接下来DexTypeItem的个数 */
    DexTypeItem list[1]; /* DexTypeItem结构 */
};

struct DexTypeItem {
    u2 typeIdx;    /* 指向DexTypeId列表的索引 */
};

下面结合实例进行分析:

  • DexProtoId

    • shortyIdx:方法声明字符串,具体而言是由方法的返回类型与参数列表组成的一个字符串,并且返回类型位于参数列表的前面。如“III”“V”“VI”“VL”等。在下图的三个方法声明中分别为B、BL、DL。

    • returnTypeIdx:方法返回类型,指向DexTypeId列表。下图的分别为byte、byte、double。

    • parametersOff:指向一个DexTypeList结构体,存放了方法的参数类型。下图分别为0、0x4BA78C、0x4BA7BC。值为0表示参数为void。

  • DexTypeList

    • size:DexTypeItem的个数,即参数的数量。下图分别为?、1、1,表示后两个方法都只有一个参数。
    • list:指向size个DexTypeItem项,每一项代表方法的一个参数。
  • DexTypeItem

    • typeIdx:指向DexTypeId列表,最终指向参数类型的字符串。如第三图61h(97)项在DexTypeId列表中正好指向”Landroid/content/Context;”类型字符串。

image

image

image

image

6.DexFieldId区段(字段)

DexFieldId结构中的数据全部是索引值,指明了字段所在的类、字段的类型以及字段名。

struct DexFieldId {
    u2 classIdx;   /* 类的类型,指向DexTypeId列表的索引 */
    u2 typeIdx;    /* 字段类型,指向DexTypeId列表的索引 */
    u4 nameIdx;    /* 字段名,指向DexStringId列表的索引 */
};

如下图,可以看到字段所属类名为MTT.ThirdAppInfoNew,字段类型为int,字段名为iCoreType。

image

7.DexMethodId区段(方法)

DexMethodId结构中的数据全部是索引值,指明了方法所在的类、方法的声明以及方法名。

struct DexMethodId {
    u2 classIdx;  /* 类的类型,指向DexTypeId列表的索引 */
    u2 protoIdx;  /* 声明类型,指向DexProtoId列表的索引 */
    u4 nameIdx;   /* 方法名,指向DexStringId列表的索引 */
};

如下图,可以看到方法所属类为MTT.ThirdAppInfoNew,方法声明为V,方法名为。

image

8.DexTypeClassDefItem(类定义)

struct DexClassDef {
    u4 classIdx;    /* 类的类型,指向DexTypeId列表的索引 */
    u4 accessFlags; /* 访问标志 */
    u4 superclassIdx;  /* 父类类型,指向DexTypeId列表的索引 */
    u4 interfacesOff; /* 接口,指向DexTypeList的偏移 */
    u4 sourceFileIdx; /* 源文件名,指向DexStringId列表的索引 */
    u4 annotationsOff; /* 注解,指向DexAnnotationsDirectoryItem结构 */
    u4 classDataOff;   /* 指向DexClassData结构的偏移 */
    u4 staticValuesOff;  /* 指向DexEncodedArray结构的偏移 */
};

struct DexClassData {
    DexClassDataHeader header; /* 指定字段与方法的个数 */
    DexField* staticFields;    /* 静态字段,DexField结构 */
    DexField* instanceFields;  /* 实例字段,DexField结构 */
    DexMethod* directMethods;  /* 直接方法,DexMethod结构 */
    DexMethod* virtualMethods; /* 虚方法,DexMethod结构 */

struct DexClassDataHeader {
    u4 staticFieldsSize;  /* 静态字段个数 */
    u4 instanceFieldsSize; /* 实例字段个数 */
    u4 directMethodsSize;  /* 直接方法个数 */
    u4 virtualMethodsSize; /* 虚方法个数 */
};

struct DexField {
    u4 fieldIdx;    /* 指向DexFieldId的索引 */
    u4 accessFlags; /* 访问标志 */
};

struct DexMethod {
    u4 methodIdx;   /* 指向DexMethodId的索引 */
    u4 accessFlags; /* 访问标志 */
    u4 codeOff;     /* 指向DexCode结构的偏移 */
};

struct DexCode {
    u2 registersSize;   /* 使用的寄存器个数 */
    u2 insSize;         /* 参数个数 */
    u2 outsSize;        /* 调用其他方法时使用的寄存器个数 */
    u2 triesSize;       /* Try/Catch个数 */
    u4 debugInfoOff;    /* 指向调试信息的偏移 */
    u4 insnsSize;       /*指令集个数,以2字节为单位 */
    u2 insns[1];        /* 指令集 */


  • DexClassDef
    • classIdx:索引值,表明类的类型。下图中值为0x6,指向类型MTT.ThirdAppInfoNew。
    • accessFlags:类的访问标志,它是以ACC_开头的一个枚举值。具体定义可以参考这里。下图中值为0x11,表示同时具有ACC_PUBLIC和ACC_FINAL。
    • superclassIdx:父类类型索引值。下图中值为0x1B13,指向java.lang.Object类。
    • interfacesOff:如果类中含有接口声明或实现,interfaceOff会指向一个DexTypeList结构,否则这里的值为0。图中值为0x4B9894(4954260),指向的DexTypeList结构为java.lang.Cloneable。
    • sourceFileIdx:字符串索引值,表示类所在的源文件名称。图中值为NO_INDEX(0xffffffff),表示该值丢失。
    • annotationsOff:指向注解目录结构,根据类型不同会有注解类、注解方法、注解字段与注解参数,如果类中没有注解,这里的值则为0。图中值为0,表示类中没有注解。
    • classDataOff:指向DexClassData结构,它是类的数据部分。图中为0x65B97A。
    • staticValuesOff:指向DexEncodedArray结构,记录了类中的静态数据。图中为0,表示类中没有静态数据。

image

  • DexClassData

    • header:一个DexClassDataHeader结构,指定字段与方法的个数。如下图中的staticFieldsSize为0,表示没有静态字段;instanceFieldsSize为0xC,表示有12个实例字段;directMethodsSize为0x1,表示有一个直接方法;virtualMethodsSize为0x0,表示没有虚方法。
      image

    • staticFields*:指向一个DexField结构,表示静态字段的类型与访问标志。由于本例没有静态字段,因此该结构无效。

    • directMethods*:指向一个DexField结构,表示实例字段的类型与访问标志。如下图本例中有一个实例字段。
      image
    • directMethods*:指向一个DexMethod结构,表示直接方法的原型、名称、访问标志、代码数据块。。如下图本例中有12个实例字段。 image

    • virtualMethods*:指向一个DexMethod结构,表示虚方法的原型、名称、访问标志、代码数据块。由于本例没有静态字段,因此该结构无效。

  • DexField

    • fieldIdx:指向DexFieldId的索引,表示字段的所属类、字段类型和字段名。
    • accessFlags:访问标志。
  • DexMethod

    • methodIdx:指向DexMethodId的索引,表示方法的所在类、方法的声明和方法名。
    • accessFlags:访问标志。
    • codeOff:指向DexCode结构的偏移,图中为0x138A34。
  • DexCode

    • registersSize:该方法使用的寄存器个数。下图中为3。
    • insSize:该方法的参数个数,对应smali语法中的”.register”指令。下图中为1。
    • outsSize:该方法调用其他方法时,对应smali语法中的”.paramter”指令。例如现在有一个方法,使用了5个寄存器,其中有2个为参数,而该方法调用了另一个方法,后者使用了20个寄存器,那么Dalvik虚拟机在分配时,会在分配自身方法寄存器空间时加上那20个寄存器空间。下图中为1。
    • triesSize:方法中Try/Catch的个数。
    • debugInfoOff:如果dex文件保留了调试信息,debugInfoOff字段会指向它。
    • insnsSize:指令个数,以2字节为单位。
    • insns[1]:真正的指令部分。

image

参考文档

  1. 《Android软件安全与逆向分析》 非虫
  2. 《.dex — Dalvik Executable Format》 AOSP
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值