Dex文件是什么
在明白什么是 Dex 文件之前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在Android 4.4推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。
构造Dex文件
Java代码转化为dex文件的流程如下:

可以形象理解为Java源代码编译成.class
文件,然后通过dx
工具生成dex
文件。
从.java到.class
先建一个文件Hello.java
,只是简单的打印一下Hello, world
:
1 | public class Hello { |
进入该文件所在的目录,使用javac
编译这个java文件。
1 | javac Hello.java |
javac 命令执行后会在当前目录生成 Hello.class
文件。
从.class到.dex
上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。
dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的SDK根目录/build-tools/任意版本 里面。使用 dx 工具处理上面生成的Hello.class 文件,在 Hello.class 的目录下使用下面的命令:
1 | dx --dex --output=Hello.dex Hello.class |
执行完成后,会在当前目录下生成一个 Hello.dex 文件。这个 .dex 文件就可以直接在 Android 运行时环境执行。
Dex格式详解
先看下dex
文件的整体布局:

然后用xxd命令打开上边生成的dex
文件。
1 | xxd Hello.dex |
因为数据不长,这里直接贴出来整个Hello.dex
的内容:
1 | 00000000: 6465 780a 3033 3500 555d 340e 86e9 521c dex.035.U]4...R. |
接下来对照这个dex文件的内容一步步解析整个dex文件的格式。
header
在android源码中对dex文件的格式,有了详细的定义:
1 | struct DexHeader { |
其中,u
标识无符号整数,u1
表示8位无符号整数即一个字节,u4
表示32位无符号整数即四个字节。
下面用一个表格对照Hello.dex
对每一个成员含义做一个简单的说明,后边会针对某些字段有一个详细的说明。
成员名称 | 成员长度(字节) | 含义 |
---|---|---|
magic | 8 | 魔数,必须出现在文件开头,标识其文件格式,用8个1字节的无符号数来表示,它可以分解为:文件标识 dex + 换行符 + DEX 版本 + 0, 这里是64 65 78 0a 30 33 35 00 ,表示dex的版本是35 |
checksum | 4 | checksum 是对去除 magic 、 checksum 以外的文件部分作 adler32 算法得到的校验值,用于判断 DEX 文件是否被篡改。这里的值是0e34 5d55 (为啥不是555d 340e ?因为是小端存储,关于大小端参见 大端还是小端) |
signature | 20 | SHA1签名,除magic,checksum和它本身,作为文件的唯一标识。这里的值是86 e9 52 1c b1 0d 57 08 b4 48 3f b8 fe ba cd 1c 22 d3 0f 65 |
fileSize | 4 | dex文件的大小,包括头文件,这里是0000 02e0 |
headerSize | 4 | dex头文件的大小,这里是0000 0070 |
endianTag | 4 | 端存储标记,主要是用来判断大端存储还是小端存储。默认值是1234 5678 ,即小端存储,这里是1234 5678 |
linkSize | 4 | 文件链接段大小,为0则表示静态链接,这里是0000 0000 |
linkOff | 4 | 文件链接段的偏移位置,如果链接段大小为0,则偏移位置也为0,这里是0000 0000 |
mapOff | 4 | DexMapList的文件偏移,这里是0000 0240 ,也即DexMapList的基址是0000 0240 |
stringIdsSize | 4 | dex文件包含的字符串数量,这里是0000 000e |
stringIdsOff | 4 | dex文件字符串偏移位置,这里是0000 0070 |
typeIdsSize | 4 | dex文件类型信息的数量,这里是0000 0007 |
typeIdsOff | 4 | dex文件类型信息的偏移位置,这里是0000 00a8 |
protoIdsSize | 4 | dex文件方法声明的数量,这里是0000 0003 |
protoIdsOff | 4 | dex文件方法声明偏移位置,这里是0000 00c4 |
fieldIdsSize | 4 | dex文件字段信息的数量,这里是0000 0001 |
fieldIdsOff | 4 | dex文件字段信息的偏移位置,这里是0000 00e8 |
methodIdsSize | 4 | dex文件方法的数量,这里是0000 0004 |
methodIdsOff | 4 | dex文件方法的偏移位置,这里是0000 00f0 |
classDefsSize | 4 | dex文件类的数量,这里是0000 0001 |
classDefsOff | 4 | dex文件类信息的偏移位置,这里是0000 0110 |
dataSize | 4 | dex文件数据区的大小,这里是0000 01b0 |
dataOff | 4 | dex文件数据区的偏移位置,这里是0000 0130 |
下面针对部分字段,进一步理解:
1. 验证checksum
通过前面表格,了解到checksum
是对去除 magic
、 checksum
以外的文件部分作 alder32 算法得到的校验值,这里我们先备份一下Hello.dex
文件,然后用UE(UltraEdit)
打开Hello.dex
,删除magic
,checksum
信息,如下:

保存之后,执行如下Python代码:
1 | import zlib |
输出结果是238312789,转为十六进制为0e34 5d55
,正好是该文件的checksum
。
2. 验证signature
类似于在上面验证checksum文件,进一步删除signature
,如图:

保存之后,执行如下Python代码:
1 | import hashlib |
输出结果是86e9521cb10d5708b4483fb8febacd1c22d30f65,正好是删除的signature。
3. 验证fileSize
从备份的Hello.dex
还原dex文件,然后前面表格得出整个dex文件的大小是2e0
即736个字节,我们用ll
命令验证下:

4. 验证headerSize
headerSize占0000 0070
也即112个字节。我们通过DexHeader这个结构体可以算出来:
magic(8个字节)+checksum(4个字节)+signature(20个字节)+fileSize(4个字节)+ … + dataOff(4个字节)=112字节。
string_ids
字符串id区域,这个区域是一个偏移量列表,每个偏移量对应一个真正的字符串资源,每个偏移量占32位,即4个字节。我们可以通过偏移量找到对应的实际字符串数据。
从DexFile.h
中我们可以找到DexStringId
的定义:
1 | struct DexStringId { |
通过注释可以看到,这个区域存的并不是真正的字符串,只是存储了真正字符串的偏移位置,stringIdsSize为0000 000e
即14,stringIdsOff为0000 0070
。我们找到地址0000 0070h
,然后取出后边的4*14个字节:
1 | 00000070: 7601 0000 7e01 0000 8d01 0000 9901 0000 v...~........... |
这里以第一个偏移为例,解释具体每个字符串偏移背后代表的真正字符串的,取出前4个字节,即0000 0176
,然后找到0000 0176h
这个地址:

dex中的字符串采用了一种叫做MUTF-8这样的编码,它是经过传统的UTF-8编码修改的。在MTUF-8中,它的头部存放的是由uleb128编码的字符的个数。所以第一个字节06
表示的含义是字符串的字节数是6个,然后我们往后推6个字节,即3C 69 6E 69 74 3E
,对照ASCII码表含义如下:
字符 | 3C | 69 | 6E | 69 | 74 | 3E |
---|---|---|---|---|---|---|
对应ASCII码 | < | i | n | i | t | > |
即<init>。
其余的13个字符串可以按照这个步骤,依次分析出来,这里就不展开了,给出一个表格如下:
索引 | 偏移值 | 字符个数(十六进制) | 字符串十六进制内容 | 对应ASCII内容 |
---|---|---|---|---|
0 | 0000 0176 | 06 | 3C 69 6E 69 74 3E | <init> |
1 | 0000 017e | 0D | 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21 | Hello, world! |
2 | 0000 018d | 0A | 48 65 6C 6C 6F 2E 6A 61 76 61 | Hello.java |
3 | 0000 0199 | 07 | 4C 48 65 6C 6C 6F 3B | LHello; |
4 | 0000 01a2 | 15 | 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B | Ljava/io/PrintStream; |
5 | 0000 01b9 | 12 | 4C 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 3B | Ljava/lang/Object; |
6 | 0000 01cd | 12 | 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 7E 67 3B | Ljava/lang/String; |
7 | 0000 01e1 | 12 | 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 3B | Ljava/lang/System; |
8 | 0000 01f5 | 01 | 56 | V |
9 | 0000 01f8 | 02 | 56 4C | VL |
a | 0000 01fc | 13 | 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 7E 67 3B | [Ljava/lang/String; |
b | 0000 0211 | 04 | 6D 61 69 6E | main |
c | 0000 0217 | 03 | 6F 75 74 | out |
d | 0000 021c | 07 | 70 72 69 6E 74 6C 6E | println |
type_ids
类型id区域,索引的值对应字符串id区域偏移量列表中的某一项,每一个人偏移也是占4个字节。
从DexFile.h
中我们可以找到DexTypeId
的定义:
1 | /* |
从注释可以看到如果我们要找到某个类型的值,需要先根据类型id列表中的索引值去字符串id列表中找到对应的项,这一项存储的偏移量对应的字符串资源就是这个类型的字符串描述。
typeIdsSize
为0000 0007
,typeIdsOff
为0000 00a8
,我们找到地址为0000 00a8
然后取出后面7*4个字节。
1 | 0300 0000 0400 0000 0500 0000 0600 0000 0700 0000 0800 0000 0a00 0000 |
以第一个偏移0000 0003
为例,即索引为3,查上面的string_ids得到的字符串列表,即为LHello;
。类似地,可以分析出来其余6个:
索引 | 对应string_idx的索引 | 类型 |
---|---|---|
0 | 0000 0003 | LHello; |
1 | 0000 0004 | Ljava/io/PrintStream; |
2 | 0000 0005 | Ljava/lang/Object; |
3 | 0000 0006 | Ljava/lang/String; |
4 | 0000 0007 | Ljava/lang/System; |
5 | 0000 0008 | V |
6 | 0000 000a | [Ljava/lang/String; |
proto_ids
方法原型id区,这个区块是一个方法原型 id 列表。
从DexFile.h
中可以找到其定义:
1 | /* |
各个字段的解释如下:

可以看到,这个数据结构由三个变量组成。第一个shortyIdx它指向的是我们上面分析的DexStringId列表的索引,代表的是方法声明字符串。第二个returnTypeIdx它指向的是 我们上边分析的DexTypeId列表的索引,代表的是方法返回类型字符串。第三个parametersOff指向的是DexTypeList的位置索引,这又是一个新的数据结构了,先说一下这里面 存储的是方法的参数列表。可以看到这三个参数,有方法声明字符串,有返回类型,有方法的参数列表,这基本上就确定了我们一个方法的大体内容。
parametersOff
指向DexTypeList
,我们看下DexTypeList
的数据结构:
1 | struct DexTypeList { |
包含2个字段,第一个是大小说的是DexTypeItem的个数。
那DexTypeItem
又是什么呢?我们再看下其数据结构:
1 | struct DexTypeItem { |
包含一个指向DexTypeId列表的索引,也就是代表参数列表中某一个具体的参数的位置。
protoIdsSize
为0000 0003
,protoIdsOff
为0000 00c4
,找到地址为0000 00c4
然后取出后边3*12个字节(为啥是12,因为每一个proto_id数据结构占12个字节):
1 | 0800 0000 0500 0000 0000 0000 |
先对第一个方法原型进行分析,前四个字节0000 0008
为shorty_ids,对应string_ids的索引,查上面string_ids字符串的表格知其为V
,即方法描述短格式为void()
。中间四个字节0000 0005
为return_type_idx,对应type_ids的索引,查上面的type_ids类型区域表格知其为V
,即返回值类型为void
。最后四个字节为0000 0000
,即代表方法无参数。
在看第二个方法原型。前四个字节0000 0009
为short_ids,对应string_ids的索引,查上面string_ids字符串的表格知其为VL
。中间四个字节为0000 0005
为return_type_idx,对应type_idx的索引,查上面的type_ids类型区域表格知其为V
,即返回值类型为void
。最后四个字节为0000 0168
,找到168h这个地址如下:

这里DexTypeList
数据结构,我们先看前4个字节,代表DexTypeItem
的个数,0000 0001
也就是1,说明只有一个DexTypeItem
,每一个占2个字节,就是00 03
,查看type_ids表格,找到索引为3的,即Ljava/lang/String;
,说明有一个String
类型的参数。第三个方法原型类似,就不展开了。整理三个方法原型如下表格:
索引 | 方法描述短格式 | 返回值类型 | 参数类型 | 方法原型 |
---|---|---|---|---|
0 | V | V | 无参数 | void() |
1 | VL | V | Ljava/lang/String; | void(java.lang.String) |
2 | VL | V | [Ljava/lang/String; | void(java.lang.String[]) |
field_ids
成员id区。这个区域是一个类成员id区域列表。定义如下:
1 | /* |
各字段的解释如下:

这里我们Hello.dex
的fieldIdsSize
为0000 0001
,说明只存在一个DexField,fieldIdOff
为0000 00e8
,找到地址为e8h
然后取出后边8个字节,即04 00 01 00 0c 00 00 00
,其中前2个字节0004
,表示class_idx,表示成员所在的类在类型区域的索引,查表得Ljava/lang/System;
。中间2个字节0001
,表示type_idx,表示该成员自身的类型在类型区域的索引,查表得Ljava/io/PrintStream;
。最后4个字节0000 000c
,表示name_idx,表示该成员的名字在字符串区域的索引,查表得out
。所以Hello.dex
中仅包含一个成员为java.io.PrintStream java.lang.System.out
。
method_ids
方法id区,这个方法是存储方法id的列表。数据格式为:
1 | /* |
解释如下:

methodIdsSize
和methodIdsOff
分表为0000 0004
和0000 00f0
,即该dex文件一共包含4个方法,方法id区的偏移地址为f0h
,我们找到这个地址,然后取出后边4*(2+2+4)个字节,如下:
1 | 0000 0000 0000 0000 |
然后根据DexMethodId
数据结构,查上边的type_ids表格,proto_ids表格以及string_ids表格,这里就不一一展开了,结果整理如下:
序号 | class_idx | proto_idx | name_idx | 方法 |
---|---|---|---|---|
0 | LHello; | void() | <init> | void Hello.<init>() |
1 | LHello; | void(java.lang.String[]) | main | void Hello.main(java.lang.String[]) |
2 | Ljava/io/PrintStream; | void(java.lang.String) | println | void java.io.PrintStream.println(java.lang.String) |
3 | Ljava/lang/Object; | void() | <init> | void java.lang.Object.<init>() |
class_def
类定义区。这个区域存储的是类定义的列表,具体的数据结构如下:
1 | /* |
各字段的含义如下:

在开始分析这个类的结构之前,先看DexFile.h
中定义的一组枚举值:
1 | enum { |
classDefsSize
和classDefsOff
分别为0000 0001
和0000 0110
。即只有一个类定义,其偏移地址为110h
,我们找到该地址,并取出32个字节:
1 | 0000 0000 0100 0000 0200 0000 0000 0000 0200 0000 0000 0000 3102 0000 0000 0000 |
前4个字节代表类的类型,为0000 0000
查表知为LHello;
。接下来4个字节为类的访问权限,为0000 0001
,记得我们上边刚提到的那个枚举值定义吗,1代表ACC_PUBLIC即Public访问权限。接下来4个字节0000 0002
为父类对应的类型,查表知为Ljava/lang/Object;
。然后四个字节0000 0000
为这个类实现的接口在dex文件中的偏移,因为我们这个类没有实现接口,所以这里为0000 0000
。紧接着的四个字节0000 0002
查表知为Hello.java
为该类类源码所在的文件。然后4个字节为该类的注解在文件中的偏移,很显然这个类没有注解,所以为0000 0000
。接下来的4个字节则是表示该类的具体数据在文件中的偏移,这里先不讨论,后边会针对类数据区专门讨论。最后4个字节表示静态成员初始值列表在文件中的偏移,很显然我们这个类没有静态成员。整理一下如下:
类的类型 | 类的权限 | 父类的类型 | 实现的接口 | 类定义所在的文件 | 类注解 | 类具体数据 | 静态成员初始值列表 |
---|---|---|---|---|---|---|---|
Hello | ACC_PUBLIC | java.lang.Object | 无 | Hello.java | 无 | 偏移值0000 0231 |
无 |
总结
本文对dex文件结构进行了一个简单的剖析,让我们对dex文件结构有了一个基本的认识。最后附上一个dex文件结构图以及思维图帮助我们记忆dex文件结构。
dex文件层次结构图:

dex文件结构思维导图: