Morrison.J Android Dev Engineer

Android .dex file format

2017-01-16
Jasper

本文记录对Android官方文档对 .dex 文件格式的解释,以帮助反编译apk。

Android apk 中,包含一个.dex文件,Android 6默认包含 .odex 文件,同时向下支持 .dex 格式,Android7具有了新的处理机制,除了核心apk外依然保持.dex文件。 .odex文件其实是.dex文件经过dex2oat后得到的”.oat file” (the AOT binary for the .dex file),Android采用JIT/AOT编译技术,如果.oat文件可用, 可以直接进行“Apk run”,而不要重新从字节码(.dex)编译成特定Android机器的机器码,如果.oat文件不可用,则采用JIT编译器得到prifile cache,AOT daemon根据profile cache重新编译method的字节码得到机器码。 在framework中,我们也能看到.oat文件,也属于the AOT binary file.
关于oat文件格式,参考Android ELF文件格式,以及源码art目录下面的内容。

ART and Dalvik

ART and Dalvik 概要

图一:ART and Dalvik abstract

大意是:ART和Dalvik是服务于Android应用和Android系统服务的,它们的开发初衷是应用于Android平台, ART和Dalvik能同时运行Dex bytecode,ART还支持Dalvik Executable instruction format(Dalvik的二进制可执行格式,也叫oat格式)。 但是,有些能在Dalvik上运行的技术不一定被ART兼容。

本文讨论的内容主要是Dalvik Executable format(.dex .odex)。

ART 特性

AOT compilation

Ahead-of-time compilation. 大意是:AOT提前编译可以在安装apk的时候,采用内置于Android机器中的dex2oat工具提前编译DEX(.dex)文件(一个或者多个),得到单个.odex文件。 可执行文件格式相比于字节码能赋予apk更优秀的性能。但是,AOT技术要求DEX文件不能是无效的DEX文件。有一些预处理工具可能会产生无效的DEX文件, 虽然能被Dalvik容忍,但在AOT技术中将不会编译出.odex文件。

改进的GC

GC有时候会妨碍app表现,导致界面卡顿,或者触摸反应迟钝,或者其它问题。 改进的GC采用并发机制,改进算法,更快更及时,极少出现在用户使用过程中出现大批量的GC情况。 还能合理减少Android应用的内存占用和碎片。

开发与调试更方便

支持更多的调试特性,输出更多更详细的调试信息。

Dalvik Executable format — .dex

基本数据格式说明

参考链接里面有表格,访问查看。特地声明LEB128数据格式。

.dex-file-type-leb128

图二:.dex 文件中leb128格式说明

大意是:LEB128是小端数据格式,用于表示有符号和无符号的32-bit整数。LEB128遵循DWARF3(代码调试格式标准3,支持C/C++/Java)规范,在.dex文件中,目前只用于编码32-bit数。

每个LEB128数据包含1~5个字节,由此代表一个32-bit的数据值。每一个字节(除了最后一个字节)都有一个有符号位,位于字节的最高一位(0~7的第7位),剩余的7位为数据有效位。这样的一个字节或者多个字节的组合就形成了一个LEB128数值。对于一个LEB128的有符号数据(sleb128),最后一个字节的最高位被拓展成为这个LEB128数值的最终有符号信息,1表示负数,0表示正数。对于一个无符号的LEB128数值(uleb128),最后一个字节的最高位,无论是1还是0,始终看做0,以此表示无符号。

uleb128的变体uleb128p1,规则是:uleb128p1 + 1 = uleb128,uleb128的plus one版本,只包含一个负数-1。有符号数-1(也就是无符号数0xffffffff),就可以使用uleb128p1类型0x00来表示了,是不是节省了空间?

如图二:编码0x00,占用一个字节,如果声明为sleb128 = 0, 最高位是0,表示正数;如果声明为uleb128 = 0,最高位始终是0; 如果声明为uleb128p1 = uleb128 - 1 = 0 - 1 = -1.

Encoded Sequence As sleb128 As uleb128 As uleb128p1
00 0 0 -1
01 1 1 0
7f -1 127 126
80 7f -128 16256 16255

解码sleb128 0x807f:
80 7f 取低7bits = 0000000_1111111 转换成小端序 = 1111111_0000000 组合成8bits = 111111_10000000 = 11111111_10000000(有符号数,高位是1则补1) 取反加一得 -00000000_10000000 = -0x80 = -128
编码sleb128 -128:
-128 = 128的补码 = 10000000的补码 = 10000000(0x80) = 1_10000000 = 1111111_0000000(组合成7bits,有符号数高位是1则补1) = 0000000_1111111(小端序) = 10000000_01111111(拓展成8bits的leb128格式) = 0x807f

解码uleb128 0x807f:
80 7f 取低7bits = 0000000_1111111 转换成小端序 = 1111111_0000000 组合成8bits = 111111_10000000 = 00111111_10000000(无符号数补0) = 0x3f80 = 16256
编码uleb128 16256:
16256 = 0x3f80 = 00111111_10000000 = 00_1111111_0000000 = 组合成8bits = 10000000_01111111(转换成小端序) = 0x807f

文件布局

dalvik/libdex/DexFile.h

/*
 * These match the definitions in the VM specification.
 */
typedef uint8_t             u1;
typedef uint16_t            u2;
typedef uint32_t            u4;
typedef uint64_t            u8;
typedef int8_t              s1;
typedef int16_t             s2;
typedef int32_t             s4;
typedef int64_t             s8;

/*
 * Structure representing a DEX file.
 *
 * Code should regard DexFile as opaque, using the API calls provided here
 * to access specific structures.
 */
struct DexFile {
......
    /* pointers to directly-mapped structs and arrays in base DEX */
    const DexHeader*    pHeader; 
    const DexStringId*  pStringIds;	// string identifiers list.
    const DexTypeId*    pTypeIds;	// type identifiers list.
    const DexFieldId*   pFieldIds;	// method prototype identifiers list. 
    const DexMethodId*  pMethodIds;	// field identifiers list.
    const DexProtoId*   pProtoIds;	// method identifiers list.
    const DexClassDef*  pClassDefs;	// class definitions list.
    const DexLink*      pLinkData;	// data used in statically linked files.
......
     /* points to start of DEX file data */
     const u1*           baseAddr;	// data
......
};

dalvik/libdex/DexFile.cpp

265 /*
266  * Set up the basic raw data pointers of a DexFile. This function isn't
267  * meant for general use.
268  */
269 void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data) {
270     DexHeader *pHeader = (DexHeader*) data;
271 
272     pDexFile->baseAddr = data;
273     pDexFile->pHeader = pHeader;
274     pDexFile->pStringIds = (const DexStringId*) (data + pHeader->stringIdsOff);
275     pDexFile->pTypeIds = (const DexTypeId*) (data + pHeader->typeIdsOff);
276     pDexFile->pFieldIds = (const DexFieldId*) (data + pHeader->fieldIdsOff);
277     pDexFile->pMethodIds = (const DexMethodId*) (data + pHeader->methodIdsOff);
278     pDexFile->pProtoIds = (const DexProtoId*) (data + pHeader->protoIdsOff);
279     pDexFile->pClassDefs = (const DexClassDef*) (data + pHeader->classDefsOff);
280     pDexFile->pLinkData = (const DexLink*) (data + pHeader->linkOff);
281 }

以上代码可以发现,DexFile中的数据由pHeader和data基地址决定。pHeader中定义了其它块的偏移地址。 dalvik/libdex/DexFile.h

 255 /*
 256  * Direct-mapped "header_item" struct.
 257  */
 258 struct DexHeader {
 259     u1  magic[8];           /* includes version number */
 260     u4  checksum;           /* adler32 checksum */
 261     u1  signature[kSHA1DigestLen]; /* SHA-1 hash */
 262     u4  fileSize;           /* length of entire file */
 263     u4  headerSize;         /* offset to start of next section */
 264     u4  endianTag;
 265     u4  linkSize;
 266     u4  linkOff;
 267     u4  mapOff;
 268     u4  stringIdsSize;
 269     u4  stringIdsOff;
 270     u4  typeIdsSize;
 271     u4  typeIdsOff;
 272     u4  protoIdsSize;
 273     u4  protoIdsOff;
 274     u4  fieldIdsSize;
 275     u4  fieldIdsOff;
 276     u4  methodIdsSize;
 277     u4  methodIdsOff;
 278     u4  classDefsSize;
 279     u4  classDefsOff;
 280     u4  dataSize;
 281     u4  dataOff;
 282 };

Bitfield, string and constant definitions

8字节的magic

用来识别Dex文件。

ubyte[8] DEX_FILE_MAGIC = { 0x64 0x65 0x78 0x0a 0x30 0x33 0x37 0x00 }
                        = "dex\n037\0"

\n\0用来防止错误的识别dex字符串和版本来。这里的037就是前面注释的/* includes version number */.

Note: 至少由两个版本号被使用,比如,009用于Android平台的M3版本,013用于Android平台的M5版本。 037版本是Android7.0加入的,有早期版本035。它们的区别在于,037添加了默认的方法,并修改了invoke方法去支持默认方法的调用。

特地声明:由于在旧的Android版本中,Dalvik存在bug,所以弃用了036版本,其不会出现在任何的Android版本中。

字节序的确定

uint ENDIAN_CONSTANT = 0x12345678; // 大端序
uint REVERSE_ENDIAN_CONSTANT = 0x78563412; // 小端序,需要将值进行反转。

这两个宏被嵌入到header_item,也就是DexHeader结构体中的endian_tag变量。

非索引值

uint NO_INDEX = 0xffffffff; // == -1 if treated as a signed int

这个宏被嵌入到class_def_item and debug_info_item, 使用一个uint表示的最大值,并以此表示非索引值,相当于索引变量清空,如果赋给一个有符号int,将 == -1.

访问标志定义

access_flags definitions

embedded in class_def_item, encoded_field, encoded_method, and InnerClass

即:被嵌入到类、字段、方法、内部类中,声明以上内容的可访问属性。使用位域的方式定义可访问属性,具体见官方表格。 关于具体的access_flags,可以参考SE7的access_flags官方说明

拓展的UTF-8编码

MUTF-8 (Modified UTF-8) Encoding 与 UTF-8 的区别:

  • Only the one-, two-, and three-byte encodings are used.
    使用不超过3个字节进行编码。
  • Code points in the range U+10000 … U+10ffff are encoded as a surrogate pair, each of which is represented as a three-byte encoded value.
    U+10000 … U+10ffff 的值将会使用两个3字节的MUTF-8值表示。
  • The code point U+0000 is encoded in two-byte form.
    U+0000会被编码成2个字节的MUTF-8值。
  • A plain null byte (value 0) indicates the end of a string, as is the standard C language interpretation.
    采用单字节的0表示字符串的空结尾,这个与标准C是相同的。

以上的第一和第二项表明:MUTF-8可以表示UTF-16的所有值,间接地替代了Unicode编码。
以上的第三和第四项表明:MUTF-8可以在字符串中包含U+0000值,同时单个字节0值还能表示字符串的结尾。(这里的U+0000实际用MUTF-8表示的两个字节,具体是什么?

这里的U+0000的特殊意义在于,使用strcmp()函数比较字符串时,当读到U+0000并不意味着字符串的结束。所以,比较MUTF-8字符串相等性的最好方法是逐个字节解码,然后解码后的值。(当然,更聪明的方法也是可以的。)

打算进一步了解字符编码,请查阅The Unicode Standard. MUTF-8更接近于CESU-8,尽管它很少被人知晓。

encoded_value的编码规则

encoded_value是指任意结构化的数据value,而不是上面描述的基本数据类型。如何表达结构化的数据value,就是encoded_value编码规则。 这种数据类型被嵌入到注解元素和编码数组中。
表达一个encoded_value(下面简称value),是用一个ubyte和一个ubyte[]表示的。
单个ubyte == (value\_arg << 5) | value\_type
高三位通常用于表示value所占用的ubyte个数,低5位表示value的类型。比如:0x00表示后续的value是一个VALUE_BYTE类型的encoded_value。 对于VALUE_ARRAY和VALUE_ANNOTATION,不单单是一个ubyte[size-1]数组,官方文档中给出了详细的结构说明。另外,目前,我还不知道这些结构化数据类型的用途。

String syntax

在一个.dex文件中,string可以有很多表现形式。下面的BNF-style风格的说明就展示了类似的情况。
SimpleName
所有的Unicode非代理字符,以及所有的Unicode拓展字符。官方文档中采用Unicode的形式进行说明,实际上,在.dex文件中,它们都将被转换为MUTF-8字符。
MemberName
used by field_id_item and method_id_item
FullClassName
代表一个类的名称,包含这一个可选的包前缀’/’符号。(不懂这个包前缀是什么意思呢!)
TypeDescriptor
类型说明符,可以代表很多类型,包括原函数、类、数组和void。
ShortyDescriptor
短说明符用于表示一个原型函数,包括返回值和参数,除非在各种类型引用之间没有区别(同一个类/同一个数组)。实际上,所有的参数类型都可以使用一个’L’短说明符来表示。

相关结构体和项的说明

Items and related structures
这里复制一份网络的整理信息,就不重复了。
参考自Android Dex文件结构解析

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字节序。
linkSize和linkOff:指定链接段的大小与文件偏移,大多数情况下它们的值都为0。link_size:LinkSection大小,如果为0则表示该DEX文件不是静态链接。link_off用来表示LinkSection距离DEX头的偏移地址,如果LinkSize为0,此值也会为0。
mapOff:DexMapList结构的文件偏移。
stringIdsSize和stringIdsOff:DexStringId结构的数据段大小与文件偏移。
typeIdsSize和typeIdsOff:DexTypeId结构的数据段大小与文件偏移。
protoIdsSize和protoIdsSize:DexProtoId结构的数据段大小与文件偏移。
fieldIdsSize和fieldIdsSize:DexFieldId结构的数据段大小与文件偏移。
methodIdsSize和methodIdsSize:DexMethodId结构的数据段大小与文件偏移。
classDefsSize和classDefsOff:DexClassDef结构的数据段大小与文件偏移。
dataSize和dataOff:数据段的大小与文件偏移。

头部信息共21*4 + 8 + 20 = 112个字节。

hexdump -C classes.dex -n 112
00000000  64 65 78 0a 30 33 35 00  aa 02 91 06 2d a0 f9 e8  |dex.035.....-...|
00000010  51 72 f9 a0 bd a0 d7 8e  ef 99 25 56 71 32 b9 93  |Qr........%Vq2..|
00000020  50 44 10 00 70 00 00 00  78 56 34 12 00 00 00 00  |PD..p...xV4.....|
00000030  00 00 00 00 8c 43 10 00  20 2a 00 00 70 00 00 00  |.....C.. *..p...|
00000040  09 07 00 00 f0 a8 00 00  9d 0a 00 00 14 c5 00 00  |................|
00000050  3c 0d 00 00 70 44 01 00  a2 26 00 00 50 ae 01 00  |<...pD...&..P...|
00000060  39 04 00 00 60 e3 02 00  d0 d9 0c 00 80 6a 03 00  |9...`........j..|

举个例子,查看文件的大小,从第32个字节开始(最小是第0个),上图为50 44 10 00,转换成大端序得:0x00104450 = 1066064 验证一下:

du -b classes.dex 
1066064	classes.dex

下面用一张表格粗略地整理header信息,字节序保持不变。

表1.header信息表

address name type value
00 magic ubyte[8] 64 65 78 0a 30 33 35 00
00+8 checksum uint aa 02 91 06
00+8+4 signature ubyte[20] 2d a0 f9 e8 51 72 f9 a0 bd a0 d7 8e ef 99 25 56 71 32 b9 93
20 file_size uint 50 44 10 00
20+4 header_size uint 70 00 00 00
20+4+4 endian_tag uint 78 56 34 12
20+4+4+4 link_size uint 00 00 00 00
30 link_off uint 00 00 00 00
30+4 map_off uint 8c 43 10 00
30+4+4 string_ids_size uint 20 2a 00 00
30+4+4+4 string_ids_off uint 70 00 00 00
40 type_ids_size uint 09 07 00 00
40+4 type_ids_off uint f0 a8 00 00
40+4+4 proto_ids_size uint 9d 0a 00 00
40+4+4+4 proto_ids_off uint 14 c5 00 00
50 field_ids_size uint 3c 0d 00 00
50+4 field_ids_off uint 70 44 01 00
50+4+4 method_ids_size uint a2 26 00 00
50+4+4+4 method_ids_off uint 50 ae 01 00
60 class_defs_size uint 39 04 00 00
60+4 class_defs_off uint 60 e3 02 00
60+4+4 data_size uint d0 d9 0c 00
60+4+4+4 data_off uint 80 6a 03 00

map_off

data_off + data_size = 0x036a80 + 0x0cd9d0 = 0x104450
map_off = 0x10438c < data_off

正如官方文档所言,map_off处于data section中。map_list记录了整个dex文件拥有哪些Item Type,以及它们各自在文件中的偏移地址。 参考下面的一张来自网络的图片,map_off的第一个uint是0D 00 00 00,共12个Item type的偏移信息被记录在这里。放置这个map_list的目的是 可以更快速的迭代整个dex文件。比方说,我们需要获得String:

  • 需要先找到string_ids_off(第一步),再根据size把所有的string_ids(其实就是String的偏移地址)拿到(第二步),然后根据ids去data section中取String(第三步);
  • 如果有map_list,找到map_off(第一步),就可以定位到string_data_item(maplist中的类型值是0x2002),得到String的个数和偏移地址(第二步),从偏移地址取String(第三步)。

好像都需要三步,没有什么变化呀!但是,如果是迭代dex文件,后续获取其它类型数据时,不再需要取map_off了(省了一步),由此可见,map_list的作用相当于减少了1/3的处理时间,这使得Android系统在加载dex文件时获得优异的性能提升。
从下图可以得到验证,02 20 00 00,转换成大端序为0x2002

尽管文前的参考链接中包含dex文件的分析图解,但我还是把它放下来,方便参考查阅:
dex-hexdump-info

string_ids

map_off中已经阐述,是string的偏移地址。对应地址中存放string_data_item,对于每一个string_data_item,第一个uleb128表示这个string的长度。

type_ids

type_ids_off是type_ids的偏移地址,从这个偏移地址开始,保存着多个string_id,每一个string_id应当属于TypeDescriptor的其中一个偏移量。
特别声明:这篇文章提到的string_id,string_ids是对于特定对象而言的。比如:对于type_item的string_id,就是type_item字符串列表中的偏移量,0x02表示第2个类型描述符;对于string_data_item中的string_id,指定了其中某个处于string_data_item中string在dex文件中的偏移地址。
所有在当前dex文件用到的类型描述符的偏移量都要在type_ids_off中声明。

proto_ids

就是符合原型结构ShortyReturnType (ShortyFieldType)*的类型描述符的string_ids(注意前面对string_id的特别声明)。

field_ids

举个例子吧。请看上面的图:Android DEX 文件格式,和整理的表1.header信息表。 dex文件偏移地址0x50处,指明了field_id的个数是1,偏移地址是0xFC。得到filed_id的具体信息:
04 00 02 00 0E 00 00 00,所以这个filed属于某个类,类的class_id = 0x04,声明的类型的偏移量是0x02(具体见type_item),filed的名称id的string_id是0x0E(0x0E在string_ids_item中 == 59 02 00 00 == 0x259,也就是说,这个filed的名称在dex文件中的偏移地址是0x259 == 03 6F 75 74 00)。03 6F 75 74 00属于string_data_item = uleb128 + ubyte[],所以是一个拥有0x03个字符的字符串out.所以,这个filed就是一个字段,名称是out。实际上是System.out.println中的out,out在Java中属于字段。

其它类型的理解就不记录了,官方文档解释的很好。比对一下就好了。

.odex/.oat 格式

即为Android ELF文件格式,是通过dex2oat程序得到的,dex2oat需要依赖很多库才能把一个dex文件转换成包含特定机器码的ELF文件。
比如:

dex2oat-cmdline = --runtime-arg -Xms64m --runtime-arg -Xmx64m --image-classes=out/target/product/generic_a15/argument_for_cust_package/preloaded-classes --dex-file=out/target/product/generic_a15/system/framework/core-oj.jar --dex-file=out/target/product/generic_a15/system/framework/core-libart.jar --dex-file=out/target/product/generic_a15/system/framework/conscrypt.jar --dex-file=out/target/product/generic_a15/system/framework/okhttp.jar --dex-file=out/target/product/generic_a15/system/framework/core-junit.jar --dex-file=out/target/product/generic_a15/system/framework/bouncycastle.jar --dex-file=out/target/product/generic_a15/system/framework/ext.jar --dex-file=out/target/product/generic_a15/system/framework/framework.jar --dex-file=out/target/product/generic_a15/system/framework/telephony-common.jar --dex-file=out/target/product/generic_a15/system/framework/voip-common.jar --dex-file=out/target/product/generic_a15/system/framework/ims-common.jar --dex-file=out/target/product/generic_a15/system/framework/apache-xml.jar --dex-file=out/target/product/generic_a15/system/framework/org.apache.http.legacy.boot.jar --dex-file=out/target/product/generic_a15/system/framework/hwEmui.jar --dex-file=out/target/product/generic_a15/system/framework/hwTelephony-common.jar --dex-file=out/target/product/generic_a15/system/framework/hwframework.jar --dex-file=out/target/product/generic_a15/system/framework/org.simalliance.openmobileapi.jar --dex-file=out/target/product/generic_a15/system/framework/org.ifaa.android.manager.jar --dex-file=out/target/product/generic_a15/system/framework/hwaps.jar --dex-file=out/target/product/generic_a15/system/framework/hwcustEmui.jar --dex-file=out/target/product/generic_a15/system/framework/hwcustTelephony-common.jar --dex-file=out/target/product/generic_a15/system/framework/hwcustframework.jar --dex-location=/system/framework/core-oj.jar --dex-location=/system/framework/core-libart.jar --dex-location=/system/framework/conscrypt.jar --dex-location=/system/framework/okhttp.jar --dex-location=/system/framework/core-junit.jar --dex-location=/system/framework/bouncycastle.jar --dex-location=/system/framework/ext.jar --dex-location=/system/framework/framework.jar --dex-location=/system/framework/telephony-common.jar --dex-location=/system/framework/voip-common.jar --dex-location=/system/framework/ims-common.jar --dex-location=/system/framework/apache-xml.jar --dex-location=/system/framework/org.apache.http.legacy.boot.jar --dex-location=/system/framework/hwEmui.jar --dex-location=/system/framework/hwTelephony-common.jar --dex-location=/system/framework/hwframework.jar --dex-location=/system/framework/org.simalliance.openmobileapi.jar --dex-location=/system/framework/org.ifaa.android.manager.jar --dex-location=/system/framework/hwaps.jar --dex-location=/system/framework/hwcustEmui.jar --dex-location=/system/framework/hwcustTelephony-common.jar --dex-location=/system/framework/hwcustframework.jar --oat-symbols=out/target/product/generic_a15/symbols/system/framework/arm64/boot.oat --oat-file=out/target/product/generic_a15/system/framework/arm64/boot.oat --oat-location=/system/framework/arm64/boot.oat --image=out/target/product/generic_a15/system/framework/arm64/boot.art --base=0x70000000 --instruction-set=arm64 --instruction-set-features=default --android-root=out/target/product/generic_a15/system --compiler-filter=speed --include-patch-information --runtime-arg -Xnorelocate --no-generate-debug-info

Android7+中,使用JIT技术,在安装中并不执行speed模式的dex2oat,而是直接加载.dex文件启动app,然后根据需要执行JIT优化。在设备休眠或者充电时,执行speed模式的dex2oat优化,具体参考官方AOSP说明。

参考资源


Similar Posts

Comments