The quiter you become,the more you are able to hear!

Dex文件格式

Author: geneblue

Blog: https://geneblue.github.io/

为了更好了解DEX文件格式,写了个java版的DEX解析器,可以打印出一个dex文件中的字符串,字段和类中调用了哪些方法。具体代码见这里

0X01 准备工作

java文件为Hello.java:

public class Hello {
   public int foo(int a, int b) {
       return (a + b) * (a - b);
   }

   public static void main(String[] argc) {
       Hello hello = new Hello();
       System.out.println(hello.foo(5, 3));
   }
}

将java文件编译成class字节码文件,然后将class字节码转换成dex字节码文件,命令如下:

javac -source 1.6 -target 1.6 Hello.java //这里需要指定JDK的版本号,不然会出错

dx.bat --dex --output=Hello.dex Hello.class

0X02 整体分析

DEX文件的结构图如下:

dex

从上图可以看出DEX文件结构共包含9个部分,MapSection是包含在DataSection中的。

将生成的Hello.dex文件用任意的16进制编辑器打开后就会得到如下的编码:

dex

dex

0X03 详细分析

DexHeader

字段名称 偏移值 长度 描述
magic 0x0 8 魔数字段,值为“dex/n035/0”标识文件类型 目前android dex文件版本为035
signature 0xC 20 SHA-1签名值
file_size 0x20 4 此dex文件的大小
header_size 0x24 4 dex文件头长度,035版本是0x70个字节
endian_tag 0x28 4 表明数据存储方式是大端还是小端,默认值为0x78563412
link_size 0x2C 4 连接段的大小,如果为0就表示是静态连接。
link_of 0x30 4 连接段的开始位置,从本文件头开始算起。如果连接段的大小为0,这里也是0
map_off 0x34 4 map数据基地址
string_ids_size 0x38 4 字符串列表的字符串个数。
string_ids_off 0x3C 4 字符串列表表基地址。
type_ids_size 0x40 4 类型列表里类型个数。
type_ids_off 0x44 4 类型列表基地址。
proto_ids_size 0x48 4 原型列表里原型个数。
proto_ids_off 0x4C 4 原型列表基地址。
field_ids_size 0x50 4 字段列表里字段个数。
field_ids_off 0x54 4 字段列表基地址。
method_ids_size 0x58 4 方法列表里方法个数。
method_ids_off 0x5C 4 方法列表基地址。
class_defs_size 0x60 4 类定义类表中类的个数。
class_defs_off 0x64 4 类定义列表基地址。
data_size 0x68 4 数据段的大小,必须以4字节对齐。
data_off 0x6C 4 数据段基地址。

需要注意的是DexHeader中描述的从DexStringTable到DexClassDefTable的size并不是指这个Table的字节数,而是这些Table中各个量的个数。

checksum字段是从0x12处开始至dex文件尾的校验和,采用的是adler32算法。

signature字段是从0x32处开始至dex文件尾的sha-1签名

DexStringTable

从上面的DexHeader结构处的具体值可以看出,DexStringTable的描述数据是 10 00 00 00 70 00 00 00,也就是存在0x10个类型的值,DexStringTable的偏移地址是0x70 在此表中并未储存任何数据,只是保存了这些字符串数据在Data段中的偏移地址。每4个字节表示一个字符串地址,其结构声明DexStringId为:

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

从数据中可以看出,第一个字符串的偏移地址是0x1CA,即从该地址处开始使用MUTF-8编码方式开始解读字符串,关于MUTF-8编码是Android中自定义的一种编码方式。对于一个字符串由两部分编码组成uleb128和utf-8,uleb128编码指定该字符串中含有多少个字符,utf-8编码使用1-3个字节表示一个字符。

通过分析一个较大的dex文件的字符串信息可知,字符串在dex文件中存储是按照字典顺序排列的,通常在开始处是换行符和空格符之类的,注意在dex中排列在一起的多个空格符被当作一个字符串(多个空格组成的字符串)来看待

DexProtoTable

这里存放着方法声明的结构体,根据DexHeader中提供的数据04 00 00 00 CC 00 00 00 可知,共有0x04个方法声明,此表的偏移地址是0xcc。结构体信息如下:

  struct DexProtoId{
   u4 shortIdx;        //方法声明字符串,指向DexStringId列表的索引
   u4 returnTypeIdx;   //方法返回类型字符串,指向DexTypeId列表的索引
   u4 parametersOff;   //方法的参数列表,指向DexTypeList的偏移,这是一个结构体,当指向的偏移地址为0时表示
  };                      //没有参数信息

DexTypeList的结构信息如下:

   struct DexTypeList{
   u4 size;                  //表示DexTypeItem的个数,也就是参数的个数
   DexTypeItem list[1];   //这是个结构
   };

DexTypeItem的结构信息如下:

  struct DexTypeItem{
   u2 typeIdx;               //指向DexTypeId列表的序号,这里的序号值仅跟着上述结构中的size地址后
  };

上面数据段中的红色区域就是方法声明的参数列表信息,共计有3个参数列表,可以看出这里对数据做了对齐处理。

DexFieldTable

这个表中记录的数据全部是字段的索引值,指明了字段所在的类,字段的类型以及字段名。其结构如下:

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

DexMethodTable

这个表和DexFieldTable表基本相同,记录的数据也都是索引值,指明了方法所在的类,方法的声明和方法名。其结构如下:

 struct  DexMethodId{
        u2  classIdx;            //类的类型,指向DexTypeId列表的索引
        u2  protoldx;            //声明的类型,指向DexProtoId列表的索引
        u4  nameIdx;         //字段名,指向DexStringId列表的索引
  };

DexClassDefTable

DexHeader中给出该表的数据描述是01 00 00 00 2C 01 00 00 ,说明该表存在一个类定义结构,偏移地址是0x01c2。该表中存放的结构体为DexClassDef,共计32个byte:

 struct DexClassDef{
     u4  classIdx;        //类的类型,指向DexTypeId列表的索引
     u4  accessFlags;     //访问标志
     u4  superclassIdx;   //父类类型,指向DexTypeId列表的索引
     u4  interfacesOff;   //接口,指向DexTypeList的偏移
     u4  sourceFileIdx;   //源文件名,指向DexStringId列表的索引
     u4  annotationsOff;  //注解,指向DexAnnotationsDirectoryItem结构,
     u4  classDataOff;    //指向DexClassData结构的偏移,类的数据部分
     u4  statiValuesOff;  //指向DexEncodedArray结构的偏移,记录类中的静态数据,为0表示无静态数据
 };

accessFlags字段是类的访问标志,它是以ACC_开头的一个枚举值。 interfacesOff字段会指向一个DexTypeList结构,否则值为0 DexClassData结构的声明为,结构大小最多56byte:

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

DexClassDataHeader结构声明为,结构大小最多为16byte:

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

注意:DexClassData结构和DexClassDataHeader结构均定义在DexClass.h文件中,该文件中所有结构的u4类型的字段其实都是uleb128类型的。 DexField结构描述了字段的类型和访问标志,其声明为:

        struct  DexField{
               u4  fieldIdx;              //指向DexFieldId的索引
               u4  accessFlags;      //访问标志,与DexClassDef结构中accessFlags字段的类型相同
         };

DexMethod结构描述方法的原型,名称,访问标志和代码的数据块,其声明为:

        struct  DexMethod{
               u4   methodIdx;        //这是一个相对于上一个方法的偏移值
               u4  accessFlags;        //访问表示,与上述相同
               u4  codeOff;              //指向DexCode结构的偏移
        } ;

需要注意的是methodIdx字段,在dex文件中类中的方法是按照字母表的顺序排列的,methodIdx字段并不是指向DexMethodId的索引,它只是相对于上一个方法的偏移值,比如说:第一个方法此处值为1表明是DexMethodIdx[1],第二个方法此处值也是1,这个1是相对于第一个方法的偏移表明是DexMethodIdx[2]。

DexCode结构描述了方法详尽的信息和方法中指令的内容,其声明为:

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

详细的指令参见delavik opcode。

DexMapSection

最终来看看,该段中放了什么东西。在DexHeader中给出该段的偏移地址0x0290,该段其实是一个映射表,虚拟机在解析dex文件时会将其映射成DexMapList数据结构。其结构是:

        struct DexMapList{
               u4  size;                           //表示下面会存储多少个DexMapItem结构
               DexMapItem  list[1];     //DexMapItem结构
         };

DexMapItem的结构声明为:

         struct DexMapItem{
                u2  type;            //枚举常量,对应dex中数据类型
                u2  unused;       //未使用,便于字节对齐
                u4  size;             //指定类型的个数
                u4  offset;         //指定类型数据的文件偏移
         };

type枚举常量的具体表示如下:

 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;
    kDexTypeEncodeArrayItem              = 0x2005;
    kDexTypeAnnotationsDirectoryItem     = 0x2006;
  };

0X04 附录