Dex文件结构分析。

本文源码均摘自8.0分支

Dex和class字节码的区别

一句话概括:dex是优化后的class字节码。

在打包的过程中,dx工具会将class转换为dex,只要的操作流程为:

对java类文件进行重新排序,建立统一的常量池,消除冗余的信息。

E.G:

在java中,如果类A和类B都调用到了类C的FunctionC方法,则FunctionC的方法签名会被复制到A和B中。

这也就造成了多个不同的文件可能会同时包含多个相同的方法签名。字符串等常量也类似。

因此dx工具建立了一个统一的资源常量池,将所有class文件二次处理,消除其中的冗余项。

Dex文件的结构从上到下也就是:

名称 含义
dex_header 文件头。包含本dex的基本内容和文件其他区域的索引
string_ids 字符串索引
type_ids 类型的索引
proto_ids 方法声明结构体的索引
field_ids 域的索引
method_ids 方法的索引
class_defs 类的定义
data 数据区
link_data 链接数据区

dex中的数据类型

类型 含义
u1 等同于uint8_t,表示1字节的无符号数
u2 等同于uint16_t,表示2字节的无符号数
u4 等同于uint32_t,表示4字节的无符号数
u8 等同于uinit64_t,表示8字节的无符号数
sleb128 有符号LEB128,可变长度1~5字节
uleb128 无符号LEB128,可变长度1~5字节
uleb128p1 无符号LEB128值加1,可变长度1~5字节

每个LEB128由1~5个字节组成,每个字节只有7个有效位。最高位是一个标志位。

如果LEB128的第一个字节的最高位为1,表示需要用到下一个字节

如果第二个字节的最高位也为1,表示也会会用到第三个字节。

以此类推。如果读取第五个字节后下一个字节最高位仍位1,则表示该dex无效,虚拟机在验证dex的时候就会失败返回。

以下为Leb128.h文件中读取uleb128的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* Reads an unsigned LEB128 value, updating the given pointer to point
* just past the end of the read value. This function tolerates
* non-zero high-order bits in the fifth encoded byte.
*/
DEX_INLINE int readUnsignedLeb128(const u1** pStream) {
const u1* ptr = *pStream;
int result = *(ptr++);

if (result > 0x7f) { // 0x7f = 01111111,大于则表示最高位为1
int cur = *(ptr++); // 下一个字节
result = (result & 0x7f) | ((cur & 0x7f) << 7); // 合并值,前两个字节
if (cur > 0x7f) {
cur = *(ptr++);
result |= (cur & 0x7f) << 14;
if (cur > 0x7f) {
cur = *(ptr++);
result |= (cur & 0x7f) << 21;
if (cur > 0x7f) {
/*
* Note: We don't check to see if cur is out of
* range here, meaning we tolerate garbage in the
* high four-order bits.
*/
cur = *(ptr++);
result |= cur << 28;
}
}
}
}

*pStream = ptr;
return result;
}

读取sleb128:

有符号的LEB128其实是一样的,只是对字节的最高有效位做了符号拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* Reads a signed LEB128 value, updating the given pointer to point
* just past the end of the read value. This function tolerates
* non-zero high-order bits in the fifth encoded byte.
*/
DEX_INLINE int readSignedLeb128(const u1** pStream) {
const u1* ptr = *pStream;
int result = *(ptr++);

if (result <= 0x7f) {
result = (result << 25) >> 25;
} else {
int cur = *(ptr++);
result = (result & 0x7f) | ((cur & 0x7f) << 7);
if (cur <= 0x7f) {
result = (result << 18) >> 18;
} else {
cur = *(ptr++);
result |= (cur & 0x7f) << 14;
if (cur <= 0x7f) {
result = (result << 11) >> 11;
} else {
cur = *(ptr++);
result |= (cur & 0x7f) << 21;
if (cur <= 0x7f) {
result = (result << 4) >> 4;
} else {
/*
* Note: We don't check to see if cur is out of
* range here, meaning we tolerate garbage in the
* high four-order bits.
*/
cur = *(ptr++);
result |= cur << 28;
}
}
}
}

*pStream = ptr;
return result;
}

DexHeader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* 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

文件标识符。 64 65 78 0A 30 33 35 00,换为ascii也就是 dex\n035 最后的4位为版本标识

占用8个字节

checksum

dex文件的校验和,用于判断dex是否被修改以及合法性,占用4字节

计算方式是使用alder32算法计算除magic和checksum之外的所有区域的值。

也就是下面除首部12字节外的所有数据(magic 8位,checksum 4位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Returns the checksum of all but the first 12 bytes of {@code dex}.
*/
public int computeChecksum() throws IOException {
Adler32 adler32 = new Adler32();
byte[] buffer = new byte[8192];
ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe
data.limit(data.capacity());
data.position(CHECKSUM_OFFSET + CHECKSUM_SIZE);
while (data.hasRemaining()) {
int count = Math.min(buffer.length, data.remaining());
data.get(buffer, 0, count);
adler32.update(buffer, 0, count);
}
return (int) adler32.getValue();
}

signature

校验码,用于判断dex是否被修改以及合法性。占用20字节

计算方式是使用SHA1算法计算除magic、checksum、signature之外的所有区域的值,也就是除首部32字节以外的所有数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Returns the signature of all but the first 32 bytes of this dex. The
* first 32 bytes of dex files are not specified to be included in the
* signature.
*/
public byte[] computeSignature() throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError();
}
byte[] buffer = new byte[8192];
ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe
data.limit(data.capacity());
data.position(SIGNATURE_OFFSET + SIGNATURE_SIZE);
while (data.hasRemaining()) {
int count = Math.min(buffer.length, data.remaining());
data.get(buffer, 0, count);
digest.update(buffer, 0, count);
}
return digest.digest();
}

Filesize

文件的总大小。4字节

headerSize

整个dexheader的大小,一般固定为0x70

endianTag

DEX运行环境的cpu字节序,预设值ENDIAN_CONSTANT等于0x12345678,表示默认采用Little-Endian字节序。也就是小端显示。

What is 字节序?

字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处;大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处。

字节序不是由操作系统决定的,而是由cpu架构决定的

大端显示:12345678

低—————–>高

12 34 56 78

小端显示:12345678

低—————–>高

78 56 34 12

之所以需要用到字节序,是因为不同语言的数据存储顺序方式不同。C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而JAVA编写的程序则只采用bigendian方式来存储数据。如果c++的程序想要和java程序联通,那么他们的字节顺序就必须要注意。

简单理解就是:一个数据存储的协议。

其他

  • 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:数据段的大小与文件偏移。

DexMapList

Dalvik虚拟机解析dex文件的内容,最终将其映射为DexMapList数据结构。

DexHeader结构的mapOff字段指明了DexMapList结构在dex文件中的偏移。

每一种数据均是一种DexMapItem类型,存放在DexMapList中。

某一类的DexMapItem包含的数据位:这类数据有多少个,他的文件偏移是多少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* Direct-mapped "map_item".
*/
struct DexMapItem {
u2 type; /* kDexType开头的类型 */
u2 unused; /* 未使用,用于字节对齐 */
u4 size; /* 指定类型的个数 */
u4 offset; /* 指定类型的文件偏移 */
};

/*
* Direct-mapped "map_list".
*/
struct DexMapList {
u4 size; // 有多少个DexMapItem
DexMapItem list[1]; /* entries */
};

这上面的type值有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* map item type codes */
enum {
kDexTypeHeaderItem = 0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeCallSiteIdItem = 0x0007,
kDexTypeMethodHandleItem = 0x0008,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
};

随便建了一个项目,生成debug包,解压dex,拖入010 Editor,得到dex的二进制数据。

以下为DexHeader,总大小为0x70

dexheader

按前面的DexHeader偏移表计算可知,mapoff所在的位置为53~56(signature结束于32字节,mapoff前面有5个4字节的)。一行为16字节,故mapoff的值为:30 64 1D 00,因为是小端显示,所以它的值为0x1d6430。

Tips:直接滚动到屏幕最下面就行了

dex_map

它的第一个字段为12,对应十进制的18,表明接下来会有18个DexMapItem结构(总共就只有20个)

前面4个字节为size(12 00 00 00),后面依次的就是各个DexMapItem的内容。

例如:接下来的来的 00对应type,占用两个字节,表示为header。跟着的00表示unused,再跟着的 01 00 00 00表示size,类型的个数。最后的00 00 00 00 表示偏移。

所以第一行的内容(除开12 00 00 00)为:DexHeader类型,个数为1,偏移为0

注意是小端显示

举个例子,整理一下:

类型 个数 偏移
kDexTypeHeaderItem 0x01 0x0
kDexTypeStringIdItem 0x4A33 0x70
kDexTypeTypeIdItem 0x795 0x1293C

这里只列举了3个,剩下的太多,不写了。

简单校验一下,stringId起始的位置为0x70,数量有0x4A33个,每个占用4字节,也就是0x128cc,加上0x70,最终等于0x1293c,也就是TypeIdde的偏移。表明上面的表格正确。

对于stringId来说,表明在0x70处有0x4A33个String类型。从上面DexHeader的那张图可以发现,0070h位置处的4个字节为C6 8A 13 00,也就表明第一个string的字符串数据位于0x138AC6(小端显示).

dex_string

从右边可以看到,这里连续放着一堆字符串,也就表明这次操作正确了