天天看点

通过dexdump来学习DEX文件格式Class的定义

一、dexdump简介

dexdump是android提供的一个dex文件查看工具,在4.4之前的版本上,我们可以在dalvik的dexdump目录找到源码。这个工具简单而且全面。通过学习这个工具的源码,我们可以很快的对dex文件有一个全面的了解。

首先看下dexdump的命令行参数:

dexdump: [-c] [-d] [-f] [-h] [-i] [-l layout] [-m] [-t tempfile]

dexfile…

-c : verify checksum and exit

-d : disassemble code sections

-f : display summary information from file header

-h : display file header details

-i : ignore checksum failures

-l : output layout, either ‘plain’ or ‘xml’

-m : dump register maps (and nothing else)

-t : temp file name (defaults to /sdcard/dex-temp-*)

参数都很浅显易懂,没有什么好说的。因为dex文件是多个class的集合,因此我们只要看每个class是怎样定义的即可。下面这个例子是一个最简单的class,没有任何field,只要一个无代码的方法,dump出的结果如下:

Class #110            -
  Class descriptor  : 'Landroid/widget/Filterable;'
  Access flags      : x0601 (PUBLIC INTERFACE ABSTRACT)
  Superclass        : 'Ljava/lang/Object;'
  Interfaces        -
  Static fields     -
  Instance fields   -
  Direct methods    -
  Virtual methods   -
    #0              : (in Landroid/widget/Filterable;)
      name          : 'getFilter'
      type          : '()Landroid/widget/Filter;'
      access        : x0401 (PUBLIC ABSTRACT)
      code          : (none)

  source_file_idx   :  (Filterable.java)
           

可以看到,dexdump按照类的定义,清晰的列出了其结构和内容。

下面看一个对field的dump(static和instance field的dump是一样的)

#1              : (in Landroid/app/Activity;)
      name          : 'DEFAULT_KEYS_DIALER'
      type          : 'I'
      access        : x0019 (PUBLIC STATIC FINAL)
           

给出了field的name, type, access这些定义。

在看method的定义

#12              : (in Landroid/widget/ArrayAdapter;)
      name          : 'createViewFromResource'
      type          : '(ILandroid/view/View;Landroid/view/ViewGroup;I)Landroid/view/View;'
      access        :  (PRIVATE)
      code          -
      registers     : 
      ins           : 
      outs          : 
      insns size    :  -bit code units
18abd4:                                        |[18abd4] android.widget.ArrayAdapter.createViewFromResource:(ILandroid/view/View;Landroid/view/ViewGroup;I)Landroid/view/View;
18abe4:  1f00                              |: if-nez v9, 001f // +001f
....
18abec:                                    |: const/ v6, #int 0 // #0
.....
18ac60:  6bde                          |: invoke-virtual {v3, v5}, Landroid/widget/TextView;.setText:(Ljava/lang/CharSequence;)V // [email protected]
18ac66: 28dd                                   |: goto  // -
      catches       : 
         - 
          Ljava/lang/ClassCastException; -> 
         - 
          Ljava/lang/ClassCastException; -> 
      positions     : 
         line=
         line=
         line=
         line=
....
         line=
         line=
      locals        : 
         -  reg= this Landroid/widget/ArrayAdapter; 
         -  reg= item Ljava/lang/Object; TT;
...
         -  reg= parent Landroid/view/ViewGroup; 
         -  reg= resource I 
           

因为例子很长,且我们关心的是method的格式,所以我省略了很多细节内容。可以清楚的看到,dump出的内容包括method的信息头(名字、类型、权限)、代码段的信息(大小、寄存器总数、参数个数等)、代码段内容、catch内容、源码与字节码的行对应关系、寄存器与变量的对应关系。

其中行号对应与变量对应关系是调试、抛出异常时必须的内容。这两部分内容开始的位置都是基于本方法代码的开头位置,通过字节码的偏移范围来实现的。

如何在调用dexdump时加上-h选项,就会得到关于class header的信息,这些信息是更加原始的关于class定义的信息。例如下面的例子

Class #0 header:
class_idx           : 
access_flags        :  ()
superclass_idx      : 
interfaces_off      :  ()
source_file_idx     : 
annotations_off     :  ()
class_data_off      :  ()
static_fields_size  : 
instance_fields_size: 
direct_methods_size : 
virtual_methods_size: 
           

这些信息对我们了解class在dex中的定义很有帮助。

二、dexdump的入口代码及头部信息

dexdump的全部实现是在dalvik/dexdump/DexDump.cpp文件。当然,它大量使用了dalvik内部的头文件和库文件,这样即节省了很多代码,又能保证与dalvik同步。

入口函数是main,main函数之前处理参数部分很简单,我们省略不看。在main函数结尾处,有如下代码:

int result = ;
    while (optind < argc) {
        result |= process(argv[optind++]);
    }
           

dexdump可同时dump多个文件,主要的函数是process。

process函数调用dexOpenAndMap函数,打开并将文件映射到内存中,然后调用dexFileParse函数,得到一个DexFile对象,最后调用processDexFile来dump具体内容。

dexOpenAndMap函数可以接受以.zip/.jar/.apk为结尾的文件。对于这些文件,dexdump会先对他们进行解压(通过dexUnzipToFile)将其中的classes.dex文件提取出来后,在打开并映射。

dexFileParse函数主要是将文件内容加载到一个DexFile对象,这只是初步加载,将文件头及基本信息加载即可。dexdump不仅可以处理dex文件,也可以处理经过优化后的odex文件。odex文件的具体格式在下期内容详细介绍,这里我们只关注dex文件。odex与dex的区别不在于文件后缀名,而是文件开头的魔数。dex的魔数是DEX_MAGIC, 即”dex\n”,而odex的魔数是DEX_OPT_MAGIC,即”dey\n”。

关于DexFile的详细内容,见下面小节dumpFileHeader.

processDexFile函数调用了几个重要的函数:

  • dumpStrings: dump字符串池
  • dumpRegisterMaps: dump 寄存器与对象的对应关系
  • dumpFileHeader: dump dex文件头
  • dumpOptDirectory: dump odex的相关内容
  • dumpClass: dump 一个类

dumpFileHeader

dexdump -f 参数可以dump出dex文件的骨架。其中最开始的几行就是文件头,就是dex文件的头部信息。

下面的例子是dexdump -f framework.jar的结果(头部部分)

Processing 'out/target/product/xxx/system/framework/framework.jar'...
Opened 'out/target/product/mocha/system/framework/framework.jar', DEX version '035'
DEX file header:
magic               : 'dex\n035\0'
checksum            : 2f03ef9e
signature           : bfb4...2303
file_size           : 
header_size         : 
link_size           : 
link_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     : 
class_defs_off      :  ()
data_size           : 
data_off            :  ()
           

下面我们来解释下每个部分的内容:

magic 部分表示这是一个dex文件,版本为035。

从string_ids_size, string_ids_off开始到最后method_ids_size和method_ids_off都是各个常量池的大小和偏移。

为了更清楚了解个部分的内容,我们看DexFile的结构:

struct DexFile {
    /* directly-mapped "opt" header */
    const DexOptHeader* pOptHeader;

    /* pointers to directly-mapped structs and arrays in base DEX */
    const DexHeader*    pHeader;
    const DexStringId*  pStringIds;
    const DexTypeId*    pTypeIds;
    const DexFieldId*   pFieldIds;
    const DexMethodId*  pMethodIds;
    const DexProtoId*   pProtoIds;
    const DexClassDef*  pClassDefs;
    const DexLink*      pLinkData;

    /*
     * These are mapped out of the "auxillary" section, and may not be
     * included in the file.
     */
    const DexClassLookup* pClassLookup;
    const void*         pRegisterMapPool;       // RegisterMapClassPool

    /* points to start of DEX file data */
    const u1*           baseAddr;

    /* track memory overhead for auxillary structures */
    int                 overhead;

    /* additional app-specific data structures associated with the DEX */
    //void*               auxData;
};
           

其中baseAddr的地址是映射后dex文件的开始位置。baseAddr加上上面提到的各种偏移,就是对应数据(pStringIds, pTypeIds等)的地址了。DexHeader的数据结构与dex文件结构是一致的,下面是它的定义:

struct DexHeader {
    u1  magic[];           /* includes version number */
    u4  checksum;           /* adler32 checksum */
    u1  signature[kSHA1DigestLen]; /* SHA- 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;
};
           

下面用一个表详细说明下各个Ids的作用、对应的数据结构、获取函数信息。

Ids类型 作用说明 对应的数据结构 获取函数 说明
stringsIds 保存所有用到的字符串的索引 DexStringId dexGetStringId DexStringId包含 u4 stringDataOff; 该成员指出字符串在常量池中偏移,通过dexGetStringData可以取得真正的字符串。 dexStringById 函数可以直接用string id索引得到字符串值
typeIds 保存class, 基础类型的表 DexTypeId dexGetTypeId, dexStringByTypeIdx DexTypeId 只包含u4 descriptorIdx; 这是一个DexStringId的索引。使用dexStringByTypeIdx可以直接获取对应的字符串。Dex中的Type全部是用java全类型限定名来表示的
protoIds 保存一个method参数及返回值类型的数据的索引 DexProtoId dexGetProtoId DexProtoId包含3个成员:shortyIdx: 返回值参数的短格式(1), 是一个stringsId值; returnTypeIdx:返回值类型,是一个typeId值; parametersOff: 相对与baseAddr的DexTypeList对象的偏移,DexTypeList是以描述每个参数类型的列表对象
fieldId 描述field信息的索引 DexFieldId dexGetFieldId DexFieldId包含3个成员:classIdx: field所属类的typeId值;typeIdx:field类型的typeId值;nameIdx: 名字的stringId值
methodId 描述一个方法的信息索引 DexMethodId dexGetMethodId DexMethodId包含3个成员:classIdx: method所属类的typeId值; protoIdx;method的参数及返回值索引,protoId索引; nameIdx: 名字的stringId值
classDefs 一个类的定义信息 DexClassDef dexGetClassDef 定义了一个类

link和data这两个在源码中很少使用,故不要考虑。

(1)参数的短格式是对函数参数的缩写模式。主要将类的签名缩写为’L’,将数组签名缩写为’[‘,每种用类型只占一个字符。比如有函数签名 “(IJDLandroid/view/View;[I)Landroid/view/View”, 他的短格式就是 LIJDL[。这里第一个字符表示返回值。短格式有助于虚拟机快速处理不需要了解参数具体类型的场合,如计算参数占用空间大小时。

Class的定义

class的定义主要通过dumpClass函数实现。在了解dumpClass之前,我们先看看dumpClassDef函数。

ClassDef是一个类的总体定义,它的结构如下:

/*
 * Direct-mapped "class_def_item".
 */
struct DexClassDef {
    u4  classIdx;           /* index into typeIds for this class */
    u4  accessFlags;
    u4  superclassIdx;      /* index into typeIds for superclass */
    u4  interfacesOff;      /* file offset to DexTypeList */
    u4  sourceFileIdx;      /* index into stringIds for source file name */
    u4  annotationsOff;     /* file offset to annotations_directory_item */
    u4  classDataOff;       /* file offset to class_data_item */
    u4  staticValuesOff;    /* file offset to DexEncodedArray */
};
           

这里包含了class的基本信息。其中classDataOff是具体的class定义的信息。staticValuesOff则是静态数据的信息。

通过调用dexGetClassDef函数可以获取ClassDef数据,这个函数是从DexHeader中的classDefsOff加上具体的class索引获得的。

ClassData

从dexGetClassData可以得到一个Encoded数据。这个Encodede是针对LEB128编码而言的。

LEB128编码规则

  • 以字节为单位,按照小端规则排列
  • 每字节最高位为标志。如果最高位为1,表示有新字节,如果为0,表示字节序列结束
  • 依次将剩下7位组合,顺序左移7位,生成整数。

例如有如下序列:

序列 A1,B2,C3,04
二进制 1010 0001,1011 0010,1100 0011,0000 0100
去掉最高位 (010 0)(001 0)(11 00)(10 10) (0 001)(1 000) (0100)
并重新组合 4 2 C A 1 8 4
人类阅读习惯 0×481AC24

ClassData的定义

函数dexReadAndVerifyClassData读取并解码ClassData,生成一个DexClassData的数据对象。首先,我们看看解码后的数据结构(包括其子结构):

/* expanded form of a class_data_item header */
struct DexClassDataHeader {
    u4 staticFieldsSize;
    u4 instanceFieldsSize;
    u4 directMethodsSize;
    u4 virtualMethodsSize;
};

/* expanded form of encoded_field */
struct DexField {
    u4 fieldIdx;    /* index to a field_id_item */
    u4 accessFlags;
};

/* expanded form of encoded_method */
struct DexMethod {
    u4 methodIdx;    /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff;      /* file offset to a code_item */
};
/* expanded form of class_data_item. Note: If a particular item is
 * absent (e.g., no static fields), then the corresponding pointer
 * is set to NULL. */
struct DexClassData {
    DexClassDataHeader header;
    DexField*          staticFields;
    DexField*          instanceFields;
    DexMethod*         directMethods;
    DexMethod*         virtualMethods;
};
           

可以看到,主要包含的是field和method的信息。

通过解读dexReadAndVerifyClassData函数,我们可以清晰的看到一个Class的结构信息。Class结构的信息与上述结构的信息,在顺序上是一致的,只不过,对于每个field和每个method, 其中的field index与method index的值,不是绝对值,而是上一个值的差值。比如,对于static field,第一个static field的index是10,那么第二static field的index值是12,那么,在dex文件中,第一个static field值保存的是10,而第二个则是2。static field, instance field, direct method和virtual method,它们的第一个都是一个绝对索引值,而随后的都是相对值。

这里面提到的index值,都是对应的常量表的索引。

Method的参数表示

在dumpMethod的时候,时,根据methodIdx,可以得到Method的数据结构

/*
 * Direct-mapped "method_id_item".
 */
struct DexMethodId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  protoIdx;           /* index into protoIds for method prototype */
    u4  nameIdx;            /* index into stringIds for method name */
};
           

可以看到DexMethodId中的几个成员:classIdx表示method所属类的typeId的索引,而nameIdx则是对应的method名字的stringId的索引。protoIdx则是method的参数与返回值的信息。

protoIdx对应的是protoId的索引。

DexProtoId的定义如下:

/*
 * Direct-mapped "proto_id_item".
 */
struct DexProtoId {
    u4  shortyIdx;          /* index into stringIds for shorty descriptor */
    u4  returnTypeIdx;      /* index into typeIds list for return type */
    u4  parametersOff;      /* file offset to type_list for parameter types */
};
           

DexProtoId的成员已经在前面做个介绍,不必赘述。其中paramtersOff数据结构比较复杂,它指向一个DexTypeList的数据结构,通过函数dexGetProtoParameters可以获取((pDexFile->baseAddr + pProtoId->parametersOff)。

DexTypeList的数据结构如下:

/*
 * Direct-mapped "type_item".
 */
struct DexTypeItem {
    u2  typeIdx;            /* index into typeIds */
};

/*
 * Direct-mapped "type_list".
 */
struct DexTypeList {
    u4  size;               /* #of entries in list */
    DexTypeItem list[];    /* entries */
};
           

这个数据结构保存了每个参数的类型的typeIdx,我们可以借此得到所有参数信息。

使用函数dexProtoGetMethodDescriptor可以将TypeList转换为可以阅读的参数签名。

dumpCode

DexCode

通过dexGetCode可以得到结构DexCode。DexCode的值就是 pDexFile->baseAddr + pDexMethod->codeOff。

DexCode的代码是

/*
 * Direct-mapped "code_item".
 *  * The "catches" table is used when throwing an exception,
 * "debugInfo" is used when displaying an exception stack trace or
 * debugging. An offset of zero indicates that there are no entries.
 */
struct DexCode {
    u2  registersSize;
    u2  insSize;
    u2  outsSize;
    u2  triesSize;
    u4  debugInfoOff;       /* file offset to debug info stream */
    u4  insnsSize;          /* size of the insns array, in u2 units */
    u2  insns[];
    /* followed by optional u2 padding */
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};
           
  • registersSize:该method所有用到的寄存器总数;
  • insSize: 该method的参数。参数的索引是从registersSize - insSize开始的。如果是非static Method,第一个参数是隐藏的this指针
  • outsSize: 该函数调用其他函数用到的寄存器的个数。给虚拟机用的,不用考虑
  • triesSize: try-catch块的个数
  • debugInfoOff: debug信息的偏移,相对于baseAddr
  • insnsSize: 代码的大小,不包括try catch信息,以unsigned short为单位

代码的数据可以通过dumpBytecodes函数进行反汇编。

try-catch信息

dumpCatches函数用于dump try-catch信息。

DexTry数据是Try的数据结构,这不是LEB128数据结构。通过dexGetTries方法获得,获取方法是DexCode::insns + pCode->insnsSize处的数据。

DexTry的结构是

/*
 * Direct-mapped "try_item".
 */
struct DexTry {
    u4  startAddr;          /* start address, in -bit code units */
    u2  insnCount;          /* instruction count, in -bit code units */
    u2  handlerOff;         /* offset in encoded handler data to handlers */
};
           
  • DexTry数组的个数由DexCode::triesSize指定
  • startAddr: try块的开始位置
  • insnCount: try块的个数,2字节为单位 handlerOff: catch处理块的信息的偏移

访问handler部分的数据,需要用到辅助类DexCatchIterator。try-catch块中,catch可以有很多个,最后以finally结束,finally是可选的。不管是catch还是finally,都是通过DexCatchHandler结构来表示的。

handler数据是用LEB128表示的。handlerOff指出handler数据开始处。数据开始处是catch的count,随后是连续的LEB128表示的DexCatchHandler数据。

如果catch的count数为负数,表示存在一个finally块,如果是正数,则表示没有finally块。

DexCatchHandler的结构是

/*
 * Catch handler entry, used while iterating over catch_handler_items.
 */
struct DexCatchHandler {
    u4          typeIdx;    /* type index of the caught exception type */
    u4          address;    /* handler address */
};
           
  • typeIdx是typeIds数组的索引,这个是catch块中Exception类的类型的索引
  • address是catch块内代码的开始地址。这个地址指向DexCode::insns的偏移

    如果最后一个是finally块,typeIdx的取值就是kDexNoIndex(即0xffffffff)

debug info

debug 信息包括dex bytecode地址与源代码的行号对应关系,与bytecode地址与虚拟寄存器类型的对应关系。

dexDexcodeDebugInfo函数是获取debug信息的核心函数。这个函数用两个回调函数来处理行号对应关系(DexDebugNewPositionCb)和寄存器对应关系(DexDebugNewLocalCb)。

该函数调用dexGetDebugInfoStream获得debug信息,其实现是pDexFile->baseAddr + pCode->debugInfoOff。

函数dexDecodeDebugInfo0负责解析debug信息。

在了解debug信息之前,先看看debug信息dump出来后的结果。debug信息dump后,形成positons和locals两个信息。postions的信息例子如下:

positions     : 
        0x0001 line=395
        0x0005 line=396
        0x0009 line=404
        0x000d line=427
        0x0010 line=428
        0x0013 line=429
        0x0014 line=405
        .....
           

左边是address地址,右边源码的line开始地址。

locals的例子如下:

locals        : 
        0x000d - 0x0014 reg=4 res Z 
        0x0015 - 0x0028 reg=1 e Landroid/os/RemoteException; 
        0x0027 - 0x0028 reg=4 res Z 
        0x0029 - 0x003c reg=1 e Ljava/lang/RuntimeException; 
        0x003b - 0x003c reg=4 res Z 
        0x0005 - 0x0053 reg=0 data Landroid/os/Parcel; 
        0x003d - 0x0053 reg=1 e Ljava/lang/OutOfMemoryError; 
        0x004b - 0x0053 reg=2 re Ljava/lang/RuntimeException; 
        ....
           

左边是地址范围,右边是reg的索引、对应的变量名与变量的类型签名。他表示在一定地址范围内,寄存器的类型是什么。实际上寄存器是可以被复用的,一个寄存器在整个函数内部不会一成不变。

debug信息中,行号对应关系与寄存器对应关系的信息是混在一起的。整个信息用LEB128编码,其结构如下:

  • 第一个数字为line start,即该函数对应源码的开始位置;
  • 第二个数字为parameter size,即参数的个数;
  • 从第三个数字起,是paramter size个string index值,表示的参数的名字;
  • 从paramter size + 2个开始,数据分为两类:
    • opcode+data,用一个opcode来表示数据的具体类型,后面根具体数据
    • address/line offsets值,即将bytecode的address值的偏移和line的偏移放在一个数字里面

opcode与offsets的区分是依靠数值 。如果opcode >= 0 && opcode < 0x0a,那么表示数据是opcode + data内容;否则,就表示address/line的offsets。

对于offsets形式,计算的方法是:

u4 adjust_code = opcode - 0x0a; //首先减去最大的opcode值
address += (adjust_code / 15);
line += -4 + (adjust_code % 15);
           

对于opcode,有几种取值:

  • 0x00 (DBG_END_SEQUENCE), 表示debug信息的结束
  • 0x01 (DBG_ADVANCE_PC), data为一个正整数, 表示将address += data;
  • 0x02 (DBG_ADVANCE_LINE), data为一个正整数,表示line += data;
  • 0x03 (DBG_START_LOCAL), 0x04(DBG_START_LOCAL_EXTENDED) , 表示开始一个新的寄存器信息;
  • 0x05 (DBG_END_LOCAL), 表示结束一个寄存器信息;
  • 0x06 (DBG_RESTART_LOCAL), 表示重新开始一个寄存器信息;
  • 其他值(0x07~0x0a) 忽略。data为空。

line和address的对应关系非常容易理解。

注意:这些opcode与offsets都是交错存储在debug info数据中的。在解析debug info数据过程中,存在一个当前address, 当前line与当前local的概念,当前line与当前address总是相互对应的,而当前local总是与当前address对应的。

对于寄存器的对应关系,有几点需要注意:

  • 在函数入口处,负责传递参数的寄存器(索引值最大的几个寄存器):
    • 其数据类型是由定义在Method中的ProtoIdx类型来定义,即通过方法的参数签名来获取这些寄存器的类型信息;
    • 其参数名字,由debug info数据中第二个数字指出paramter size,然后再由paramter size个string index指向stringIds中的索引,从而得到这些参数的名字;
    • 对于非static方法,有一个隐含的this指针,对应的寄存器的名字是”this”,而对应的类型则是当前method所属的类,这个信息没有保存在debug info中,但是可以通过method的信息计算出来;
    • 这个时候,用作参数的寄存器(包括隐含的this指针所在的寄存器),其起始地址都是0;
  • DBG_START_LOCAL,DBG_START_LOCAL_EXTENDED:这两个opcode表示一个新的寄存器信息的开始。他们同时意味者老的寄存器信息的结束:
    • opcode后续的数据,依次是:reg的索引,对应local变量的name的string index,对应的local变量的类型的type index,对应local变量的签名的string index(仅opcode==DBG_START_LOCAL_EXTENDED时);
    • reg的索引表明当前处理的是哪个寄存器。它意味着,reg之前所对应local变量的范围已经结束,同时reg对应的新的变量范围已经开始,即当前 address值是reg之前对应local变量的endAddress,是新变量的startAddress;
  • DBG_END_LOCAL, 后续数据为reg的索引,表示对应的reg变量范围结束,即当前address值是reg对应变量的endAddress
  • DBG_RESTART_LOCAL 后续数据为reg的索引,表示重新设定reg对应变量的范围开始,即当前address值为reg对应变量的startAddress。