Dex文件结构

欢迎大家加入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及以后的设备安装应用的过程会比较耗时。dexoptdex2oat不在本文讨论范围。

那么,dex文件和class文件有什么区别呢?

  1. class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度,让dex文件执行的更快,更节省内存。
  2. dvm的字节码指令是16位,而jvm是8位
  3. dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。

1. 准备工作

准备一个可用于测试的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文件就生成完成,并进行了测试。

  • -cp 是 class path 的缩写 ,后面的 Hello 是要运行的 Class 的名称。详细的可以输入 dalvikvm -help 可以看到 dalvikvm 的帮助文档。
  • 之所以需要root是因为,命令在执行时 dalvikvm 会在 /data/dalvik-cache/ 目录下创建 .dex 文件,所以局需要对目录 /data/dalvik-cache/ 有读、写和执行的权限 ,否则无法达到预期效果。

如果需要查看dex的二进制文件格式,可以使用vim -b Hello.dex命令打开它,此命令必须添加-b 选项表示使用二进制格式打开,然后在命令模式下输入 :%!xxd 命令就可以转化16进制的表示方式。

最后得到的完成结果如图,这就是整个dex文件的16进制数据显示结果,不同的区域被我用颜色做了区分,先整体有个印象。具体每个字节表示什么意思,我们后续一点点分析,想想要一个一个字节,并且是人工读这个dex文件,有没有很激动呢!

此外,还有两个非常重要的概念需要了解一下:

1.1 字节序

即字节顺序,分为大端序、小端序和混合序。详细可以参考维基百科,这里以Dex文件结构简单说一下,从freeline的产出文件中拿到一个classes.dex文件,查看大小为12296字节,十六进制是0x3008,如果以大端序存储,应该为00 00 30 08,小端序应该为08 30 00 00(数据以8bit为单位存储)。

1.2 Leb128

他的源码实现在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中提供了解码leb128cjava代码。

1.3 用到的AOSP的源码

一共有如下几个:

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

2. Dex文件结构

从官方文档中可以看到,一个.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文件了。

2.1 header_item 描述.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里除了对索引区和数据区的偏移地址又一次描述,更有其他更详细信息的描述。

2.2 string_ids 区索引了.dex文件所有的字符串

从Header中可以知道string_ids区的位置(0x0070),这个区中存储的是string_id_item的列表,string_id_item中存储的是一个名为string_data_offuint类型值,这个值表示对应的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等也会引用到这一片熟悉的字符串。

2.3 type_ids 区索引了.dex文件里的所有数据类型

包括 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 的字符串描述。

  • L 表示 class 的详细描述 ,一般以分号表示 class 描述结束 ;
  • V 表示 void 返回类型 ,只有在返回值的时候有效 ;
  • [ 表示数组 ,[Ljava/lang/String; 可以对应到 java 语言里的 java.lang.String[] 类型 。
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;

2.4 proto_ids 区索引了method的原型

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

  • index = 0 的没有参数传入
  • index = 1 的传入一个 String 参数
  • index = 2 的传入一个 String[] 类型的参数

2.5 field_ids 区索引了所有被本.dex文件引用的field

本区的元素格式是 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

2.6 method_ids 区索引了.dex文件里的所有的method

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_idxfields_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_idstype_idsproto_idsfield_idsmethod_ids。每个索引 区域里存放着指向具体数据的偏移地址,或者存放的数据是其它索引区域里面的 index 号。

2.7 class_defs 类定义区

从字面意思解释,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];
}

各参数含义如下:

  1. class_idx描述具体的class类型,值是type_ids的一个index。值必须是一个class类型,不能是数组类型或者基本类型。
  2. access_flags描述class的访问类型,诸如public,final,static等。在官方文档dex-format里“access_flags Definitions”有具体的描述。
  3. superclass_idx,描述supperclass的类型,值的形式跟class_idx一样。
  4. interfaces_off,值为偏移地址,指向class的interfaces,被指向的数据结构为type_list。class若没有
    interfaces,值为0。
  5. source_file_idx,表示源代码文件的信息,值是string_ids的一个index。若此项信息缺失,此项值赋值为NO_INDEX=0xffff ffff。
  6. annotions_off,值是一个偏移地址,指向的内容是该class的注释,位置在data区,格式为annotations_direcotry_item。若没有此项内容,值为0。
  7. class_data_off,值是一个偏移地址,指向的内容是该class的使用到的数据,位置在data区,格式为class_data_item。若没有此项内容,值为0。该结构里有很多内容,详细描述该class的field,method,method里的执行代码等信息,后面有一个比较大的篇幅来讲述class_data_item
  8. static_value_off,值是一个偏移地址,指向data区里的一个列表(list),格式为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;
}

各参数含义如下:

  1. method_idx_diff,前缀methd_idx表示它的值是method_ids的一个index,后缀_diff表示它是于另外一个method_idx的一个差值,就是相对于encoded_method[]数组里上一个元素的method_idx的差值。其实encoded_filed->field_idx_diff表示的也是相同的意思,只是编译出来的Hello.dex文件里没有使用到class filed所以没有仔细讲,详细的参考官网文档。
  2. access_flags,访问权限,比如publicprivatestaticfinal等。
  3. code_off,一个指向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 methodsdirect_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 一个是 ,一个是 mainmain方法我们都知道,是我自己定义的,那么方法就是编译器在编译时为我们自动生成的,关于什么是方法,详细看这里。

到这里我们发现还有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,表示可能有,也可能没有,根据具体的代码来。

到这里,我相信大多数人有点晕了,所以我先捋一遍我们是怎么一步一步走到这里来的:

  1. 一个.dex文件被分成了9个区,详细见“2.Dex文件结构”。其中有一个索引区叫做class_defs,索引了.dex里面用到的class,以及对这个class的描述。Hello.dex里只有一个class,就是LHello;
  2. class_defs区,这里面其实是class_def_item结构。这个结构里描述了LHello;的各种信息,诸如名称,superclass,accessflag,interface等。class_def_item里有一个元素class_data_off,指向data区里的一个class_data_item结构,用来描述class使用到的各种数据。自此以后的结构都归于data区了。
  3. class_data_item结构,里描述值着class里使用到的static_fieldinstance_fielddirect_methodvirtual_method的数目和描述。例如Hello.dex里,只有2个direct_method,其余的field和method的数目都为0。描述direct_method的结构叫做encoded_method,是用来详细描述某个method的。
  4. encoded_method结构,描述某个method的method类型,包含access_flagscode_off偏移地址,code_off指向的数据类型为code_item
  5. code_item,code_item结构里描述着某个method的具体实现。

现在离成功还有最后一步

根据得到的direct_method[0]->code_off=0x130direct_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文件都没有使用到他们,初步估计是为了字节对齐而来的,也从另一方面反映了dexdata区的数据并不是连续的,我们要读取data区的数据还是需要老老实实的按偏移地址去找,只有这样才能确保我们的数据不会读错。

3. 方法指令与smali语法

经过了第二部分的阅读,我们知道了整个dex文件的结构,也知道了我们的方法具体在dex文件中存放的位置,就是我们上面最后分析的code_item方法指令区,那么这些指令到底代表了什么意思呢,代码是如何被虚拟机执行的,我们接下来就对我们得到的两个方法指令做详细的解释。

3.1 smali语法

以下来自百度百科:

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

3.2 方法

方法就是默认的构造方法,指令为:1070 0003 0000 000e

首先我们要知道,dvm指令是16位的,读取字节的时候,先读低8位,再读高8位。

  1. 第一个指令先读70,查阅文档dalvik bytecode中得知语法格式为invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB,指令格式为35c,并且能知道详细的含义:70: invoke-direct
  2. 在文档instruction formats中查询35c,得知格式为A|G|op BBBB F|E|D|C,而我们这里A=1op=70,所以语法为[A=1] op {vC}, kind@BBBB,这里的kind对照上一步的语法可知,kind代表meth,即BBBBmethod_ids中的索引。
  3. method_ids中找到BBBB=0x0000的方法,最后翻译一下,1070 0003 0000的含义就是:invoke-direct {v0} Ljava/lang/Object;->()V,到这里第一个指令共计6个字节读取完毕。
  4. 第二个指令先读0e,查询文档instruction formats得知的含义是return-void,并且指令格式10x
  5. 在文档instruction formats中查询10x,得知格式为ØØ|op,发现什么也不做,第一个方法的所有指令读取完毕。

综合上面的信息都可以写出<init>方法的smali语法定义:

.method public constructor <init>()V
    .registers 1
    invoke-direct { v0 }, Ljava/lang/Object;->()V
    return-void
.end method

3.3 main方法

指令为:0062 0000 011a 0001 206e 0002 0010 000e

  1. 先读62,查阅文档得知语法格式为sstaticop vAA, field@BBBB,指令格式为21c,并且能知道详细的含义:62: sget-object
  2. 在文档instruction formats中查询21c,得知格式为AA|op BBBB,对应语法op vAA, field@BBBB,在field_ids中找到BBBB=0x0000的字段。
  3. 第一个指令0062 0000翻译如下:sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
  4. 先读1a,查阅文档得知语法格式为const-string vAA, string@BBBB,指令格式为21c
  5. 在文档instruction formats中查询21c,得知格式为AA|op BBBB,对应语法op vAA, string@BBBB,在string_ids中找到BBBB=0x0001的字段。
  6. 第二个指令011a 0001翻译如下:const-string v1, "Hello Dex"
  7. 先读6e,查阅文档得知语法格式为invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB,指令格式为35c,并且能知道详细的含义:6e: invoke-virtual
  8. 在文档instruction formats中查询35c,得知格式为A|G|op BBBB F|E|D|C,而我们这里A=2op=6e,所以语法为[A=2] op {vC, vD}, kind@BBBB,这里的kind对照上一步的语法可知,kind代表meth,即BBBBmethod_ids中的索引,在method_ids中找到BBBB=0x0002的方法。
  9. 第三个指令206e 0002 0010翻译如下:invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
  10. 最后一个指令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

我们经过上面的分析后能发现一个很重要很常见的问题,那就是方法指令最大只有4个字节,也就是BBBB=0xFFFF(65535),方法指令的索引最大只有65535,如果超过这个方法数限制,就会找不到方法了,这也就是我们当项目很大的时候,常见的64k方法限制的根源所在。

3.4 dexdump

使用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)

4.总结

进过这篇文章的分析,我们知道了dex文件的结构组成,以及不同的区域是如何划分的,他们是如何分工的,最后我们手动读取了一遍指令,了解了每个方法是如何通过指令被执行的,并且找到了64k方法限制的根源。相信这些都会加深对dex文件的认识,dex的这些结构搞清楚了,再反过来看市面上的apk加固,也是对dex文件的修改,让别人无法通过反编译获取真正的代码。就说这么多了。

其他参考资料
一篇胎死腹中的Android文章——Dex文件结构解析

实例分析dex 文件格式-20140323

如果你觉得好,对你有过帮助,请给我一点打赏鼓励吧,一分也是爱呀!

你可能感兴趣的:(android,dex,android,jvm,java,class)