欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)
我的github地址:https://github.com/jeasonlzy
Class文件:即java的字节码文件,java源码文件编译后生成了字节码文件,然后被jvm执行,字节码文件中有一个非常重要的区域是常量池,编译的过程中,字节码文件并不会保存方法和字段的最终内存布局信息,也就是说,方法和字段并不像C/C++那样被编译成地址,jvm在加载Class文件的时候,需要从常量池获取对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址中【参考:深入理解Java虚拟机-JVM高级特性与最佳实践】。一个字节码文件中,除了方法体中的内容被编译为字节码指令外,大部分的信息都保存在常量池中,通过索引来访问,包括类的名称,类的字段,类的继承关系,类中方法的定义等。
Dex文件:Dalvik Executable format
,即Dalvik可执行文件格式。实际上在5.0之前的设备上,第一次打开应用时会执行dexopt
,即dex优化,这个过程会生成odex
文件,以后每次都直接加载优化过后的odex
文件(2.x的机子上这个过程非常慢,经常导致应用第一次启动时黑屏,甚至ANR);在5.0及以后,Android不再使用Dalvik,新的虚拟机为ART,不过dex仍然是必须的,ART也会进行dex优化,名为dex2oat
,这个过程和Dalvik不一样,是在安装时进行的,所以5.0及以后的设备安装应用的过程会比较耗时。dexopt
和dex2oat
不在本文讨论范围。
那么,dex文件和class文件有什么区别呢?
准备一个可用于测试的dex文件
1.创建一个文件Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello Dex");
}
}
2.编译成class文件
执行命令 : javac Hello.java
编译完成后 ,目录下生成 Hello.class
文件 。可以使用命令 java Hello
来测试下 ,会输出代码中的 “ Hello Dex” 的字符串。
3.编译成 dex 文件
编译工具在 Android SDK 的路径如下 ,如 ./sdk/build-tools/25.0.1/dx
,可以将该目录设置为PATH环境变量,以后使用dx命令就不用进入该目录下了。
执行命令 : dx –dex –output=Hello.dex Hello.class
编译成功后会生成 Hello.dex 文件
4.运行测试dex文件
测试这个文件的时候,需要手机有root权限,或者使用模拟器,命令如下:
adb root
adb push Hello.dex /sdcard/
adb shell
shell@cancro:/ $ dalvikvm -cp /sdcard/Hello.dex Hello
Hello Dex
我们发现命令行打印出了我们的Hello Dex
,到这里,我们准备的dex文件就生成完成,并进行了测试。
如果需要查看dex的二进制文件格式,可以使用vim -b Hello.dex
命令打开它,此命令必须添加-b 选项表示使用二进制格式打开,然后在命令模式下输入 :%!xxd 命令就可以转化16进制的表示方式。
最后得到的完成结果如图,这就是整个dex文件的16进制数据显示结果,不同的区域被我用颜色做了区分,先整体有个印象。具体每个字节表示什么意思,我们后续一点点分析,想想要一个一个字节,并且是人工读这个dex文件,有没有很激动呢!
此外,还有两个非常重要的概念需要了解一下:
即字节顺序,分为大端序、小端序和混合序。详细可以参考维基百科,这里以Dex文件结构简单说一下,从freeline的产出文件中拿到一个classes.dex文件,查看大小为12296
字节,十六进制是0x3008
,如果以大端序存储,应该为00 00 30 08
,小端序应该为08 30 00 00
(数据以8bit为单位存储)。
他的源码实现在AOSP中:libcore/dex/src/main/java/com/android/dex/Leb128.java
Little-Endian Base 128,这里有些例子,可以帮助你理解这个编码规则。简单点说就是数据可变长度的编码方式,在dex文件中,使用1-5位字节来编码32位整数。数据存储方式也是小端序,如果第一个字节的最高位是1,则继续读下一个字节,依次读取,后面7bits是有效数据。将多个字节的该7bits从低到高组合起来就是所表示的整数。并且最多只能读5个字节,如果第5个字节还是1则dex无效,存储格式如下:
Leb128有3种类型:
- sleb128(signed LEB128),有符号数分成了正数和负数,在计算机的存储中都是以补码存储,正数和上述无符号数一样的处理,负数的处理会有些区别。编码序列的最后1位表示值的符号(上图的bit13),1表示负数
- uleb128(unsigned LEB128),将无符号整数写成二进制形式,从低位到高位7个bits为一个整体组合成一个字节,在该字节最高位填入上述所说的标识信息。
- uleb128p1(uleb128 的值减一), 是将其当做uleb128
编码的值进行解码,然后再减一。解码时要减一,那么反过来编码时就要加一
举个��,被leb128
编码后的值80 7f
,二进制存储方式为1000 0000 0111 1111
,解码方式分以下三种:
sleb128
:总共有两个字节,编码序列的最后一位为1,表示这是一个负数,真实值的二进制编码(补码)为-11111 1000 0000
,原码为-1000 0000
,也即-128
。
uleb128
:这个比较简单,分别取掉两个字节的最高位,结果为000 0000 111 1111
,真实值也就是111 1111 000 0000
,也即16256
uleb128p1
:这个的值就是uleb128
的值减1。
AOSP
中提供了解码leb128
的c
和java
代码。
一共有如下几个:
dalvik/libdex/DexFile.h
dalvik/libdex/DexFile.cpp
dalvik/libdex/DexClass.h
MUTF-8编码格式:libcore\dex\src\main\Java\com\android\dex\Mutf8.java
Leb128编码格式:libcore/dex/src/main/java/com/android/dex/Leb128.java
从官方文档中可以看到,一个.dex
文件主要分为3层:文件头、索引区、数据区。
数据结构如下:
struct DexFile {
DexHeader Header;
DexStringId StringIds[stringIdsSize];
DexTypeId TypeIds[typeIdsSize];
DexProtoId ProtoIds[protoIdsSize];
DexFieldId FieldIds[fieldIdsSize];
DexMethodId MethodIds[methodIdsSize];
DexClassDef ClassDefs[classDefsSize];
DexData Data[];
DexLink LinkData;
};
后面再一步步仔细分析,先简单说一下:头信息中存储了文件的一些概要信息,比如文件大小、版本、校验信息、还有string
的数量及string_ids
在文件中的位置、type
的数量以及type_ids
在文件中的位置等等。
根据头信息中的数据可以找到各种索引区的位置,然后在索引区的数据中可以找到当前类型数据在文件中的存储位置。比如下面Hello.dex
中,从头信息中可以知道有14个string
以及string_ids
的位置,解析string_id
可以得到字符串的位置。
掌握上面的知识后,我们就可以结合官方文档和AOSP源码来解析一个.dex
文件了。
数据结构如下,每个参数的含义使用注释标明:
ubyte 8-bit unsinged int
uint 32-bit unsigned int, little-endian;
alignment: 4 bytes
struct header_item {
ubyte[8] magic; //魔术,用来识别.dex文件,绝大多数的.dex文件值为dex\n035\0
unit checksum; //除magic和checksum外所有字节的adler32值,用于检测文件的完整性
ubyte[20] siganature; //除magic、checksum、signature外所有字节的SHA-1值,用于唯一的标识文件
uint file_size; //文件大小,即 0x2dc = 732字节,和我们在电脑上看的大小一致
uint header_size; //header_item的大小,固定为0x70也就是112字节
unit endian_tag; //大小端标记,dex固定为 78563412 = 0x12345678,即小端序
uint link_size; //保留字段,链接部分的大小,如果此文件没有静态链接,则为0
uint link_off; //保留字段,并没有用到,值为0
uint map_off; //必定为非0值,map_item 的偏移地址,详细看下面的介绍。
uint string_ids_size; //string的数量,可以为0
uint string_ids_off; //string_ids列表的位置,可以为0
uint type_ids_size; //type的数量,可以为0,最大值为65535
uint type_ids_off; //type_ids列表的位置,可以为0
uint proto_ids_size; //proto_type的数量,最大值为65535
uint proto_ids_off; //proto_ids列表的位置,可以为0
uint method_ids_size; //method的数量,可以为0
uint method_ids_off; //method_ids列表的位置,可以为0
uint class_defs_size; //类定义(class definitions)的数量,可以为0
uint class_defs_off; //类定义列表的位置
uint data_size; //数据区大小
uint data_off; //数据区的位置
}
上面参数中提到了adler32算法,详细的可以点击这里adler32查看。
把上面的参数对应到dex中的每个字节,可以得到如下的图片结果:
这里面我们可以关注下几组以_size
和_off
结尾的参数,我们根据他们的数值可以很方便的在dex文件中定位每个区域在dex文件中所处的位置和占用大小,后续中关于每个区域的详细分析,都会用到这些字段。
我们也可以将结果做成表格的形式,结果如下:
name | value | meaning |
---|---|---|
magic | 0x6465 780a 3033 3500 | dex\n035\0 |
checksum | 0xff5d d693 | |
signature | 0xdbc2 9650 a0a6 ce59 cf26 0532 b7b7 60e6 c99d 47d0 | |
file_size | 0xdc02 0000 | 732(符合文件实际大小) |
header_size | 0x7000 0000 | 112(固定大小) |
endian_tag | 0x7856 3412 | 12345678(小端序) |
link_size | 0x0000 0000 | 0 |
link_off | 0x0000 0000 | 0 |
map_off | 0x3c02 0000 | 572 |
string_ids_size | 0x0e00 0000 | 14 |
string_ids_off | 0x7000 0000 | 112 |
type_ids_size | 0x0700 0000 | 7 |
type_ids_off | 0xa800 0000 | 168 |
proto_ids_size | 0x0300 0000 | 3 |
proto_ids_off | 0xc400 0000 | 196 |
field_ids_size | 0x0100 0000 | 1 |
field_ids_off | 0xe800 0000 | 232 |
method_ids_size | 0x0400 0000 | 4 |
method_ids_off | 0xf000 0000 | 240 |
class_defs_size | 0x0100 0000 | 1 |
class_defs_off | 0x1001 0000 | 272 |
data_size | 0xac01 0000 | 428 |
data_off | 0x3001 0000 | 304 |
这里面有个比较特殊的 map_off
,他指向的数据结构是map_list
,这块区域属于data
区,所以 map_off
值要大于等于data_off
,详细描述如下:
定义位置 : data 区
引用位置 : header 区
ushort 16-bit unsigned int, little-endian
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct map_list {
uint size; //表示当前数据后面有 size 个 map_item
map_item list [size]; //真正的数据
}
struct map_item {
ushort type; //该 map_item 的类型,取值是下面表格中的一种,也是在官方文档中,摘要如下
ushort unuse; //对齐字节的,没有其他作用
uint size; //表示再细分此 item , 该类型的个数
uint offset; //第一个元素的针对文件初始位置的偏移量
}
我们根据map_off
的偏移量可以在dex文件中找到map_list
的区域如下图:
针对这个格式我们一点点解析,首先是map_list
结构,前4字节为size,即图中红色0d00 0000
表示,maplist->size = 0x0d(13)
,表示当前共有13个map_item
。根据上面定义的数据结构可知,每个map_item
占12个字节,就是上图中黄蓝相间的数据,我们解析完成后得到如下表格:
这个表格中每个type
所对应的types' meaning
是如何得来的,详细都在官方文档 Type Codes中的这个映射关系表,请自行查看。
index | address | type | size | offset | types’ meaning | name in header |
---|---|---|---|---|---|---|
1 | 0x0240 | 0x0000 | 0x0001 | 0x0000 | TYPE_HEADER_ITEM | |
2 | 0x024C | 0x0001 | 0x000e | 0x0070 | TYPE_STRING_ID_ITEM | string_ids_off |
3 | 0x0258 | 0x0002 | 0x0007 | 0x00a8 | TYPE_TYPE_ID_ITEM | type_ids_off |
4 | 0x0264 | 0x0003 | 0x0003 | 0x00c4 | TYPE_PROTO_ID_ITEM | proto_ids_off |
5 | 0x0270 | 0x0004 | 0x0001 | 0x00e8 | TYPE_FIELD_ID_ITEM | field_ids_off |
6 | 0x027C | 0x0005 | 0x0004 | 0x00f0 | TYPE_METHOD_ID_ITEM | method_ids_off |
7 | 0x0288 | 0x0006 | 0x0001 | 0x0110 | TYPE_CLASS_DEF_ITEM | class_defs_off |
8 | 0x0294 | 0x2001 | 0x0002 | 0x0130 | TYPE_CODE_ITEM | data_off |
9 | 0x02A0 | 0x1001 | 0x0002 | 0x0168 | TYPE_TYPE_LIST | |
10 | 0x02AC | 0x2002 | 0x000e | 0x0176 | TYPE_STRING_DATA_ITEM | |
11 | 0x02B8 | 0x2003 | 0x0002 | 0x0221 | TYPE_DEBUG_INFO_ITEM | |
12 | 0x02C4 | 0x2000 | 0x0001 | 0x022d | TYPE_CLASS_DATA_ITEM | |
13 | 0x02D0 | 0x1000 | 0x0001 | 0x023c | TYPE_MAP_LIST |
到这里我们发现map_list
里面描述的内容,有一部分跟header_item
里面描述的内容相同。但 map_list
描述的似乎更为全面些,实际上,map_list
是整个dex文件的按区域的有序索引,他的用意是用更方便和更简单的形式来遍历整个dex文件。
到此,header
部分描述完毕 ,它包括描述.dex
文件的信息、各索引区偏移信息、data 区的偏移信息、一个map_list
结构。map_list
里除了对索引区和数据区的偏移地址又一次描述,更有其他更详细信息的描述。
从Header中可以知道string_ids
区的位置(0x0070),这个区中存储的是string_id_item
的列表,string_id_item
中存储的是一个名为string_data_off
的uint
类型值,这个值表示对应的string_data_item
在文件中的位置,详情如下:
uint , 32-bit unsigned int , little-endian
alignment: 4 bytes
struct string_id_item {
uint string_data_off; //指向对应的`string_data_item`在文件中的位置
}
uleb128 : unsigned LEB128, valriable length
ubyte: 8-bit unsinged int
alignment: none (byte-aligned)
struct string_data_item {
uleb128 utf16_size; //字符串长度
ubyte data; //字符串的内容,MUTF-8格式
}
需要注意的是,data中的数据是MUTF-8格式的,关于MUTF-8编码格式可以自行点击看看,关于他的源码位于AOSP中的libcore\dex\src\main\Java\com\android\dex\Mutf8.java
。简单说就是如下特性:
MUTF-8 编码:
1. 使用 1~3 字节编码长度
2. 大于 16 位的 Unicode 编码 U+10000~U+10FFFF 使用 3 字节来编码
3. U+0000 采用 2 字节编码
4. 采用空字符00
作为结尾
5. 第一个字节存放字节个数(不包含自已)
所以这里我们可以根据header中的string_ids_off
=0x70和string_ids_size
=0x0e,在dex文件中找到如下区域:
.dex 里 string_ids_item 的二进制
从数据结构和实际数据中都可以看出,该区域是四字节对齐的,我们再根据这个表中的索引,可以找到如下区域:
.dex 里 string_data_item 的二进制,图中背景色绿、紫、蓝相间的区域,与上面的string_id_item
一一对应
我们可以将图中的数据整理出来得到如下表格:
index | string_data_off | utf16_size | data | string |
---|---|---|---|---|
0 | 0x176 | 0x06 | 0x3c 69 6e 69 74 3e 00 | |
1 | 0x17e | 0x09 | 0x48 65 6c 6c 6f 20 44 65 78 00 | Hello Dex |
2 | 0x189 | 0x0a | 0x48 65 6c 6c 6f 2e 6a 61 76 61 00 | Hello.java |
3 | 0x195 | 0x07 | 0x4c 48 65 6c 6c 6f 3b 00 | LHello; |
4 | 0x19e | 0x15 | 0x4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 00 | Ljava/io/PrintStream; |
5 | 0x1b5 | 0x12 | 0x4c 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 3b 00 | Ljava/lang/Object; |
6 | 0x1c9 | 0x12 | 0x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 00 | Ljava/lang/String; |
7 | 0x1dd | 0x12 | 0x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 3b 00 | Ljava/lang/System; |
8 | 0x1f1 | 0x01 | 0x56 00 | V |
9 | 0x1f4 | 0x02 | 0x56 4c 00 | VL |
10 | 0x1f8 | 0x13 | 0x5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 00 | [Ljava/lang/String; |
11 | 0x20d | 0x04 | 0x6d 61 69 6e 00 | main |
12 | 0x213 | 0x03 | 0x6f 75 74 00 | out |
13 | 0x218 | 0x07 | 0x70 72 69 6e 74 6c 6e 00 | println |
string 里的各种标志符号 ,诸如 L , V , VL , [ 等在 .dex 文件里有特殊的意思 ,参考 官方文档dex-format中的String Syntax章节。
而string_ids
的终极目标就是找到这些字符串。当我们使用二进制编辑器打开 .dex 文件时,他会默认使用ASCII码去翻译对应的数据,如果我们的数据都是ASCII中能表示的,就会发现我们能看的懂,而不是乱码,刚刚的分析流程,就是看看编译器是如何找到它们的。此后的type-ids,method_ids等也会引用到这一片熟悉的字符串。
包括 class 类型,数组类型(array types)和基本类型(primitive types)。本区域里的元素格式为 type_id_item
,结构描述如下:
alignment: 4 bytes
struct type_id_item {
uint descriptor_idx; //string_ids 里的 index 序号
}
type_ids_item
里面 descriptor_idx
的值的意思,是 string_ids
里的 index
序号,是用来描述此 type 的字符串。
根据 header 里 type_ids_size
= 0x07,type_ids_off
= 0xa8 , 找到对应的二进制描述区如下。
根据 type_ids_item
的描述,整理出表格如下 。因为 type_ids_item
-> descriptor_idx
里存放的是 指向 string_ids
的 index 号,所以我们也能得到该 type 的字符串描述。
index | descriptor_idx | string |
---|---|---|
0 | 0x03 = 3 | LHello; |
1 | 0x04 = 4 | Ljava/io/PrintStream; |
2 | 0x05 = 5 | Ljava/lang/Object; |
3 | 0x06 = 6 | Ljava/lang/String; |
4 | 0x07 = 7 | Ljava/lang/System; |
5 | 0x08 = 8 | V |
6 | 0x0a = 10 | [Ljava/lang/String; |
proto 的意思是 method prototype
代表 java 语言里的一个 method 的原型(返回类型 + 参数列表)。proto_ids
里的元素为 proto_id_item
, 结构如下 。
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct proto_id_item{
uint shorty_idx;
uint return_type_idx;
uint parameters_off;
}
shorty_idx,跟 type_ids
一样 ,它的值是一个 string_ids
的 index 号,最终是一个简短的字符串描述,用来说明该 method 原型。
return_type_idx,它的值是一个 type_ids
的 index 号 ,表示该 method 原型的返回值类型。
parameters_off, 后缀 off 是 offset,指向 method 原型的参数列表 type_list
; 若 method 没有参数,值为 0 。参数列表的格式是 type_list
,结构从逻辑上如下描述 。
uint 32-bit unsigned int, little-endian
ushort 16-bit unsigned int, little-endian
alignment: 4 bytes
struct type_list {
uint size;
ushort type_idx[size];
}
size
表示参数的个数 ;
type_idx
是对应参数的类型 ,它的值是一个 type_ids
的 index 号 ,跟 return_type_idx
是同一个品种的东西。
header 里 proto_ids_size
= 0x03 , proto_ids_off
= 0xc4 , 它的二进制描述区如下 :
根据映射关系,找到data区的数据如下红色表示的数据:
最后整理成表格如下:
index | shorty_idx | shorty string | return_type_idx | return string | parameters_off | parameters size | parameters string |
---|---|---|---|---|---|---|---|
0 | 0x08 | V | 0x05 | V | 0x0000 | 0 | - |
1 | 0x09 | VL | 0x05 | V | 0x0168 | 1 | Ljava/lang/String; |
2 | 0x09 | VL | 0x05 | V | 0x0170 | 1 | [Ljava/lang/String; |
可以看出,有 3 个 method原型,返回值都为 void
本区的元素格式是 field_id_item
,逻辑结构描述如下:
ushort 16-bit unsigned int, little-endian
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct filed_id_item{
ushort class_idx;
ushort type_idx;
uint name_idx;
}
class_idx, 表示本 field 所属的 class 类型, class_idx
的值是 type_ids
的一个 index,并且必须指向一个 class 类型 。
type_idx,表示本 field 的类型,它的值也是 type_ids 的一个 index 。
name_idx,表示本 field 的名称,它的值是 string_ids 的一个 index 。
header 里 field_ids_size = 1 , field_ids_off = 0xe8 。说明本 .dex 只有一个 field ,这部分的二进制描述如下,为了能看到字节序号,我把上面的protp_ids
区也截图出来了
整理成表格如下:
index | class_idx | class | type_idx | type | name_idx | name |
---|---|---|---|---|---|---|
0 | 0x04 | Ljava/lang/System; | 0x01 | Ljava/io/PrintStream; | 0x0c | out |
method_ids
的元素格式是 method_id_item
, 结构跟 fields_ids
很相似,结构如下:
ushort 16-bit unsigned int, little-endian
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct filed_id_item{
ushort class_idx;
ushort proto_idx;
uint name_idx;
}
class_idx
, 和 name_idx
跟 fields_ids
是一样的 。
class_idx,表示本 method 所属的 class 类型 , class_idx
的值是 type_ids
的一个 index , 并且必须指向一 个 class 类型 。
proto_idx, 描述该 method 的原型 ,指向 proto_ids
的一个 index 。
name_idx,表示本 method 的名称 ,它的值是 string_ids
的一个 index 。
header 里 method_ids_size
= 0x04 , method_ids_off
= 0xf0 。本部分的二进制描述如下 :
整理成表格如下:
index | class_idx | class | proto_idx | proto | name_idx | name |
---|---|---|---|---|---|---|
0 | 0x00 | LHello; | 0x00 | 0x00 | ||
1 | 0x00 | LHello; | 0x02 | 0x0b | main | |
2 | 0x01 | Ljava/io/PrintStream; | 0x01 | 0x0d | println | |
3 | 0x02 | Ljava/lang/Object; | 0x00 | 0x00 |
我们结合proto_ids
的方法原型,再次整理可以得到如下表格:
index | class | proto | name | return | paramters size | paramters |
---|---|---|---|---|---|---|
0 | LHello; | V | V | 0 | ||
1 | LHello; | VL | main | V | 1 | [Ljava/lang/String; |
2 | Ljava/io/PrintStream; | VL | println | V | 1 | Ljava/lang/String; |
3 | Ljava/lang/Object; | V | V | 0 |
至此 ,索引区的内容描述完毕,包括 string_ids
,type_ids
,proto_ids
,field_ids
,method_ids
。每个索引 区域里存放着指向具体数据的偏移地址,或者存放的数据是其它索引区域里面的 index
号。
从字面意思解释,class_defs
区域里存放着 class definitions
,class 的定义。它的结构较 .dex
区都要复杂些, 因为有些数据都直接指向了 data 区里面。
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct class_def_item {
uint class_idx;
uint access_flags;
uint superclass_idx;
uint interfaces_off;
uint source_file_idx;
uint annotations_off;
uint class_data_off;
uint static_value_off;
}
struct annotations_directory_item {
uint class_annotations_off;
uint fields_size;
uint annotated_methods_size;
uint annotated_parameters_size;
field_annotation field_annotations[fields_size]; //optional
method_annotation method_annotations[methods_size]; //optional
parameter_annotation parameter_annotations[parameters_size]; //optional
}
struct field_annotation {
uint field_idx;
uint annotations_off;
}
struct method_annotation {
uint method_idx;
uint annotations_off;
}
struct parameter_annotation {
uint method_idx;
uint annotations_off;
}
struct annotation_set_ref_list {
uint size;
uint annotation_set_ref_item[size];
}
各参数含义如下:
type_ids
的一个index。值必须是一个class类型,不能是数组类型或者基本类型。string_ids
的一个index。若此项信息缺失,此项值赋值为NO_INDEX=0xffff ffff。annotations_direcotry_item
。若没有此项内容,值为0。class_data_item
。若没有此项内容,值为0。该结构里有很多内容,详细描述该class的field,method,method里的执行代码等信息,后面有一个比较大的篇幅来讲述class_data_item
。encoded_array_item
。若没有此项内容,值为0。header 里 class_defs_size
= 0x01 , class_defs_off
= 0x 0110 。只有一个类,则此段二进制描述为 :
整理成表格如下:
index | class_idx | access_flags | superclass_idx | interface_off | source_file_idx | annotations_off | class_data_off | static_value_off |
---|---|---|---|---|---|---|---|---|
数据 | 0 | 0x00 | 0x01 | 0x02 | 0x00 | 0x02 | 0x00 | 0x022d |
描述 | 0 | LHello; | ACC_PUBLIC | Ljava/lang/Object; | - | Hello.java | - | 后面介绍 |
class_data_off
指向 data 区里的 class_data_item
结构,class_data_item
里存放着本 class 使用到的各种数据,下面是 class_data_item
的逻辑结构 :
uleb128 unsigned little-endian base 128
alignment: none (byte-aligned)
struct class_data_item{
uleb128 static_fields_size; //静态字段
uleb128 instance_fields_size; //实例字段
uleb128 direct_methods_size; //直接方法(private或者构造方法)
uleb128 virtual_methods_size; //虚方法(非private、static、final,非构造方法)
encoded_field static_fields[static_fields_size]; //静态字段
encoded_field instance_fields[instance_fields_size]; //实例字段
encoded_method direct_methods[direct_method_size]; //直接方法
encoded_method virtual_methods[virtual_methods_size]; //虚方法
}
struct encoded_field{
uleb128 filed_idx_diff;
uleb128 access_flags;
}
struct encoded_method{
uleb128 method_idx_diff;
uleb128 access_flags;
uleb128 code_off;
}
各参数含义如下:
methd_idx
表示它的值是method_ids
的一个index,后缀_diff
表示它是于另外一个method_idx
的一个差值,就是相对于encoded_method[]
数组里上一个元素的method_idx
的差值。其实encoded_filed->field_idx_diff
表示的也是相同的意思,只是编译出来的Hello.dex
文件里没有使用到class filed
所以没有仔细讲,详细的参考官网文档。public
、private
、static
、final
等。data
区的偏移地址,目标是本method的代码实现。被指向的结构是 code_item
,有近10项元素,后面再详细解释。现在,我们根据class_def_item
-> class_data_off
= 0x022d,得到如下图数据区域中红色部分所示,为了能看清整体,我将图片多截出来了很多。
将红色部分的数据按上面的结构体整理成表格如下:
element | value |
---|---|
static_fields_size | 0x00 |
instance_fields_size | 0x00 |
direct_methods_size | 0x02 |
vitual_methods_size | 0x00 |
static_fields[ ] | 由于static_fields_size值为0,所以该项没有值 |
instance_fields[ ] | 由于instance_fields_size值为0,所以该项没有值 |
direct_methods[ ] | 0x00 0x81 80 04 0xb0 02 0x01 0x09 0xc8 02 00 |
vitual_methods[ ] | 由于vitual_methods_size值为0,所以该项没有值 |
以上表格解释如下:名称为 LHello;
的 class 里只有 2 个 direct methods
。direct_methods
里的值都是uleb128
编码的原始二进制值。按照direct_methods
对应的数据格式encoded_method
,我们再整理一次这 2 个 method
的描述,得到结果如下表格所描述。
directive_method | son-element | value | meaning |
---|---|---|---|
direct_method[0] | method_idx_diff | 0x00 | Lhello; -> ()V |
access_flags | 0x81 80 04 = 0x10001 | ACC_PUBLIC ¦ ACC_CONSTRUCTOR | |
code_off | 0xb0 02 = 0x0130 | ||
direct_method[1] | method_idx_diff | 0x01 | LHello; -> main([Ljava/lang/String;)V |
access_flags | 0x09 = 0x09 | ACC_PUBLIC ¦ ACC_STATIC | |
code_off | 0xc8 02 = 0x0148 |
可以发现,得到的2个 method 一个是
,一个是 main
,main
方法我们都知道,是我自己定义的,那么
方法就是编译器在编译时为我们自动生成的,关于什么是
方法,详细看这里。
到这里我们发现还有code_off
仍然没有解析,那我们还要继续深入,code_off
指向区域的数据结构是code_item
如下:
ushort 16-bit unsigned int, little-endian
uint 32-bit unsigned int, little-endian
alignment: 4 bytes
struct code_item {
ushort registers_size;//本段代码使用到的寄存器数目
ushort ins_size; //传入当前method的参数数量,后面的结果中默认的构造方法中这个值是1,原因是有个this,静态方法没this
ushort outs_size; //本段代码调用其它method时需要的参数个数
ushort tries_size; //代码块中异常处理的数量,结构为try_item
uint debug_info_off; //偏移地址,指向本段代码的debug信息存放位置,是一个debug_info_item结构
uint insns_size; //指令列表的大小,以16-bit为单位。insns是instructions的缩写
ushort insns[insns_size]; //指令列表
ushort paddding; // optional,值为0,用于对齐字节
try_item tries[tyies_size]; // optional,用于处理java中的exception,常见的语法有try catch
encoded_catch_handler_list handlers; // optional,用于处理java中的exception,常见的语法有try catch
}
alignment: none (byte-aligned)
struct debug_info_item {
uleb128 line_start; //状态机的初始值,并不代表实际值
uleb128 parameters_size; //编码的参数名称的数量
uleb128p1 parameter_names[parameters_size]; //参数名的索引
}
struct try_item {
uint start_addr;
ushort insn_count;
ushort handler_off;
}
struct encoded_catch_handler_list {
uleb128 size;
encoded_catch_handler list[size];
}
struct encoded_catch_handler {
sleb128 size;
encoded_type_addr_pair handlers[abs(size)];
uleb128 catch_all_addr; //optional
}
struct encoded_type_addr_pair {
uleb128 type_idx;
uleb128 addr;
}
上传参数中,末尾的3项标志为optional,表示可能有,也可能没有,根据具体的代码来。
到这里,我相信大多数人有点晕了,所以我先捋一遍我们是怎么一步一步走到这里来的:
Hello.dex
里只有一个class,就是LHello;
。class_defs
区,这里面其实是class_def_item
结构。这个结构里描述了LHello;
的各种信息,诸如名称,superclass,accessflag,interface等。class_def_item
里有一个元素class_data_off
,指向data区里的一个class_data_item
结构,用来描述class使用到的各种数据。自此以后的结构都归于data区了。class_data_item
结构,里描述值着class里使用到的static_field
,instance_field
,direct_method
,virtual_method
的数目和描述。例如Hello.dex
里,只有2个direct_method
,其余的field和method的数目都为0。描述direct_method
的结构叫做encoded_method
,是用来详细描述某个method的。encoded_method
结构,描述某个method的method类型,包含access_flags
和code_off
偏移地址,code_off
指向的数据类型为code_item
。code_item,code_item
结构里描述着某个method的具体实现。现在离成功还有最后一步
根据得到的direct_method[0]->code_off=0x130
和direct_method[1]->code_off=0x148
,我们再dex文件中找到如下区域,红色代表direct_method[0]
,蓝色代表direct_method[1]
:
继续根据结构体得到如下表格,后面的三个可选参数,在我们的这个例子中并没有,所以表格中没有列出。
index | method | registers_size | ins_size | outs_size | tries_size | debug_info_off | insns_size | insns |
---|---|---|---|---|---|---|---|---|
0 | 0x0001 | 0x0001 | 0x0001 | 0x0000 | 0x0221 | 0x0004 | 0x1070 0003 0000 000e | |
1 | main | 0x0003 | 0x0001 | 0x0002 | 0x0000 | 0x226 | 0x0008 | 0x0062 0000 011a 0001 206e 0002 0010 000e |
这里我们又发现了一个debug_info_off
的参数,他对应的结构体在上面已经给出了定义,所指向的区域我也在上面的最后面用绿色标识出来了,这个区域的数据主要用于debug
相关,在此就不做过多讲解了。
现在我们终于将整个dex文件重头到尾全部读取完毕了,这样就得到了文章开头的那张分颜色区域标识dex文件的大图,当然图片中的颜色并没有和讲解中的一致,主要是为了区分不同的区域。现在回头看看这张图片,整个dex文件的结构清晰无比,不过我们也发现了一个现象,其中地址0x016e
的两个字节和地址0x023b
的一个字节,他们的值都是0x00
,但是整个dex文件都没有使用到他们,初步估计是为了字节对齐而来的,也从另一方面反映了dex
的data
区的数据并不是连续的,我们要读取data
区的数据还是需要老老实实的按偏移地址去找,只有这样才能确保我们的数据不会读错。
经过了第二部分的阅读,我们知道了整个dex文件的结构,也知道了我们的方法具体在dex文件中存放的位置,就是我们上面最后分析的code_item
方法指令区,那么这些指令到底代表了什么意思呢,代码是如何被虚拟机执行的,我们接下来就对我们得到的两个方法指令做详细的解释。
以下来自百度百科:
Smali、Baksmali是指安卓系统里的Java虚拟机(Dalvik)所使用的一种.dex格式文件的汇编器,反汇编器。其语法是一种宽松式的Jasmin/dedexer语法,而且它实现了.dex格式所有功能(注解,调试信息,线路信息等)。
简单的说,smali就是Davlik的寄存器语言,是Dalvik VM内部执行的核心代码。它有自己的一套语法,关于详细的smali语法介绍,可以参考以下三篇文章:
APK反编译之一:基础知识
Android逆向之路:深入理解Davilk字节码指令及Smali文件
Smali–Dalvik虚拟机指令语言
后面分析的过程中,会用到smali语法,并且要使用AOSP中的两个官方文档,先摆出来如下(goole文档自行科学上网):
instruction formats
dalvik bytecode
1070 0003 0000 000e
首先我们要知道,dvm指令是16位的,读取字节的时候,先读低8位,再读高8位。
70
,查阅文档dalvik bytecode
中得知语法格式为invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
,指令格式为35c
,并且能知道详细的含义:70: invoke-direct
。instruction formats
中查询35c
,得知格式为A|G|op BBBB F|E|D|C
,而我们这里A=1
,op=70
,所以语法为[A=1] op {vC}, kind@BBBB
,这里的kind
对照上一步的语法可知,kind
代表meth
,即BBBB
是method_ids
中的索引。method_ids
中找到BBBB=0x0000
的方法,最后翻译一下,1070 0003 0000
的含义就是:invoke-direct {v0} Ljava/lang/Object;->()V
,到这里第一个指令共计6个字节读取完毕。0e
,查询文档instruction formats
得知的含义是return-void
,并且指令格式10x
。instruction formats
中查询10x
,得知格式为ØØ|op
,发现什么也不做,第一个方法的所有指令读取完毕。综合上面的信息都可以写出<init>
方法的smali
语法定义:
.method public constructor <init>()V
.registers 1
invoke-direct { v0 }, Ljava/lang/Object;->()V
return-void
.end method
指令为:0062 0000 011a 0001 206e 0002 0010 000e
62
,查阅文档得知语法格式为sstaticop vAA, field@BBBB
,指令格式为21c
,并且能知道详细的含义:62: sget-object
。instruction formats
中查询21c
,得知格式为AA|op BBBB
,对应语法op vAA, field@BBBB
,在field_ids
中找到BBBB=0x0000
的字段。0062 0000
翻译如下:sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
。1a
,查阅文档得知语法格式为const-string vAA, string@BBBB
,指令格式为21c
。instruction formats
中查询21c
,得知格式为AA|op BBBB
,对应语法op vAA, string@BBBB
,在string_ids
中找到BBBB=0x0001
的字段。011a 0001
翻译如下:const-string v1, "Hello Dex"
。6e
,查阅文档得知语法格式为invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
,指令格式为35c
,并且能知道详细的含义:6e: invoke-virtual
。instruction formats
中查询35c
,得知格式为A|G|op BBBB F|E|D|C
,而我们这里A=2
,op=6e
,所以语法为[A=2] op {vC, vD}, kind@BBBB
,这里的kind
对照上一步的语法可知,kind
代表meth
,即BBBB
是method_ids
中的索引,在method_ids
中找到BBBB=0x0002
的方法。206e 0002 0010
翻译如下:invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
。000e
翻译为return-void
,不在多说。综合上面的信息都可以写出main
方法的smali
语法定义:
.method public static main([Ljava/lang/String;)V
.registers 3
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello Dex"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 4
return-void
.end method
BBBB=0xFFFF
(65535),方法指令的索引最大只有65535,如果超过这个方法数限制,就会找不到方法了,这也就是我们当项目很大的时候,常见的64k方法限制的根源所在。使用Android SDK中的dexdump
工具也可以看到.dex
文件的详细信息:
执行以下命令
dexdump -d Hello.dex
得到信息如下,发现dump的结果发现和我们自己读的结果完全一致。
Processing 'Hello.dex'...
Opened 'Hello.dex', DEX version '035'
Class #0 -
Class descriptor : 'LHello;'
Access flags : 0x0001 (PUBLIC)
Superclass : 'Ljava/lang/Object;'
Interfaces -
Static fields -
Instance fields -
Direct methods -
#0 : (in LHello;)
name : ''
type : '()V'
access : 0x10001 (PUBLIC CONSTRUCTOR)
code -
registers : 1
ins : 1
outs : 1
insns size : 4 16-bit code units
000130: |[000130] Hello.:()V
000140: 7010 0300 0000 |0000: invoke-direct {v0}, Ljava/lang/Object;.:()V // method@0003
000146: 0e00 |0003: return-void
catches : (none)
positions :
0x0000 line=1
locals :
0x0000 - 0x0004 reg=0 this LHello;
#1 : (in LHello;)
name : 'main'
type : '([Ljava/lang/String;)V'
access : 0x0009 (PUBLIC STATIC)
code -
registers : 3
ins : 1
outs : 2
insns size : 8 16-bit code units
000148: |[000148] Hello.main:([Ljava/lang/String;)V
000158: 6200 0000 |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0000
00015c: 1a01 0100 |0002: const-string v1, "Hello Dex" // string@0001
000160: 6e20 0200 1000 |0004: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0002
000166: 0e00 |0007: return-void
catches : (none)
positions :
0x0000 line=3
0x0007 line=4
locals :
Virtual methods -
source_file_idx : 2 (Hello.java)
进过这篇文章的分析,我们知道了dex文件的结构组成,以及不同的区域是如何划分的,他们是如何分工的,最后我们手动读取了一遍指令,了解了每个方法是如何通过指令被执行的,并且找到了64k方法限制的根源。相信这些都会加深对dex文件的认识,dex的这些结构搞清楚了,再反过来看市面上的apk加固,也是对dex文件的修改,让别人无法通过反编译获取真正的代码。就说这么多了。
其他参考资料
一篇胎死腹中的Android文章——Dex文件结构解析
实例分析dex 文件格式-20140323