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 : 0x0601 (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 : 0x0401 (PUBLIC ABSTRACT)
code : (none)
source_file_idx : 12206 (Filterable.java)
可以看到,dexdump按照类的定义,清晰的列出了其结构和内容。
下面看一个对field的dump(static和instance field的dump是一样的)
#1 : (in Landroid/app/Activity;)
name : 'DEFAULT_KEYS_DIALER'
type : 'I'
access : 0x0019 (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 : 0x0002 (PRIVATE)
code -
registers : 12
ins : 5
outs : 4
insns size : 66 16-bit code units
18abd4: |[18abd4] android.widget.ArrayAdapter.createViewFromResource:(ILandroid/view/View;Landroid/view/ViewGroup;I)Landroid/view/View;
18abe4: 3909 1f00 |0000: if-nez v9, 001f // +001f
....
18abec: 1206 |0004: const/4 v6, #int 0 // #0
.....
18ac60: 6e20 6bde 5300 |003e: invoke-virtual {v3, v5}, Landroid/widget/TextView;.setText:(Ljava/lang/CharSequence;)V // method@de6b
18ac66: 28dd |0041: goto 001e // -0023
catches : 2
0x0009 - 0x0011
Ljava/lang/ClassCastException; -> 0x002a
0x0021 - 0x0029
Ljava/lang/ClassCastException; -> 0x002a
positions :
0x0000 line=370
0x0002 line=371
0x0009 line=377
0x000d line=379
....
0x0032 line=386
0x003a line=394
locals :
0x0000 - 0x0000 reg=7 this Landroid/widget/ArrayAdapter;
0x0015 - 0x001b reg=2 item Ljava/lang/Object; TT;
...
0x0000 - 0x0042 reg=10 parent Landroid/view/ViewGroup;
0x0000 - 0x0042 reg=11 resource I
因为例子很长,且我们关心的是method的格式,所以我省略了很多细节内容。可以清楚的看到,dump出的内容包括method的信息头(名字、类型、权限)、代码段的信息(大小、寄存器总数、参数个数等)、代码段内容、catch内容、源码与字节码的行对应关系、寄存器与变量的对应关系。
其中行号对应与变量对应关系是调试、抛出异常时必须的内容。这两部分内容开始的位置都是基于本方法代码的开头位置,通过字节码的偏移范围来实现的。
如何在调用dexdump时加上-h选项,就会得到关于class header的信息,这些信息是更加原始的关于class定义的信息。例如下面的例子
Class #0 header:
class_idx : 6
access_flags : 17 (0x0011)
superclass_idx : 5731
interfaces_off : 0 (0x000000)
source_file_idx : 25093
annotations_off : 5329644 (0x5152ec)
class_data_off : 8673220 (0x8457c4)
static_fields_size : 233
instance_fields_size: 0
direct_methods_size : 1
virtual_methods_size: 0
这些信息对我们了解class在dex中的定义很有帮助。
dexdump的全部实现是在dalvik/dexdump/DexDump.cpp文件。当然,它大量使用了dalvik内部的头文件和库文件,这样即节省了很多代码,又能保证与dalvik同步。
入口函数是main,main函数之前处理参数部分很简单,我们省略不看。在main函数结尾处,有如下代码:
int result = 0;
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函数调用了几个重要的函数:
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 : 9105424
header_size : 112
link_size : 0
link_off : 0 (0x000000)
string_ids_size : 85794
string_ids_off : 112 (0x000070)
type_ids_size : 6685
type_ids_off : 343288 (0x053cf8)
proto_ids_size : 12195
proto_ids_off : 370028 (0x05a56c)
field_ids_size : 39764
field_ids_off : 516368 (0x07e110)
method_ids_size : 60182
method_ids_off : 834480 (0x0cbbb0)
class_defs_size : 5454
class_defs_off : 1315936 (0x141460)
data_size : 7614960
data_off : 1490464 (0x16be20)
下面我们来解释下每个部分的内容:
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[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;
};
下面用一个表详细说明下各个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的定义主要通过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索引获得的。
从dexGetClassData可以得到一个Encoded数据。这个Encodede是针对LEB128编码而言的。
例如有如下序列:
序列 | 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 |
函数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值,都是对应的常量表的索引。
在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[1]; /* entries */
};
这个数据结构保存了每个参数的类型的typeIdx,我们可以借此得到所有参数信息。
使用函数dexProtoGetMethodDescriptor可以将TypeList转换为可以阅读的参数签名。
通过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[1];
/* followed by optional u2 padding */
/* followed by try_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
};
代码的数据可以通过dumpBytecodes函数进行反汇编。
dumpCatches函数用于dump try-catch信息。
DexTry数据是Try的数据结构,这不是LEB128数据结构。通过dexGetTries方法获得,获取方法是DexCode::insns + pCode->insnsSize处的数据。
DexTry的结构是
/*
* Direct-mapped "try_item".
*/
struct DexTry {
u4 startAddr; /* start address, in 16-bit code units */
u2 insnCount; /* instruction count, in 16-bit code units */
u2 handlerOff; /* offset in encoded handler data to handlers */
};
访问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 */
};
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编码,其结构如下:
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,有几种取值:
line和address的对应关系非常容易理解。
注意:这些opcode与offsets都是交错存储在debug info数据中的。在解析debug info数据过程中,存在一个当前address, 当前line与当前local的概念,当前line与当前address总是相互对应的,而当前local总是与当前address对应的。
对于寄存器的对应关系,有几点需要注意: