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文件中所处的位置和占用大小,后续中关于每个区域的详细分析,都会用到这些字段。

我们也可以将结果做成表格的形式,结果如下:

namevaluemeaning
magic0x6465 780a 3033 3500dex\n035\0
checksum0xff5d d693
signature0xdbc2 9650 a0a6 ce59 cf26 0532 b7b7 60e6 c99d 47d0
file_size0xdc02 0000732(符合文件实际大小)
header_size0x7000 0000112(固定大小)
endian_tag0x7856 341212345678(小端序)
link_size0x0000 00000
link_off0x0000 00000
map_off0x3c02 0000572
string_ids_size0x0e00 000014
string_ids_off0x7000 0000112
type_ids_size0x0700 00007
type_ids_off0xa800 0000168
proto_ids_size0x0300 00003
proto_ids_off0xc400 0000196
field_ids_size0x0100 00001
field_ids_off0xe800 0000232
method_ids_size0x0400 00004
method_ids_off0xf000 0000240
class_defs_size0x0100 00001
class_defs_off0x1001 0000272
data_size0xac01 0000428
data_off0x3001 0000304

这里面有个比较特殊的 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中的这个映射关系表,请自行查看。

indexaddresstypesizeoffsettypes’ meaningname in header
10x02400x00000x00010x0000TYPE_HEADER_ITEM
20x024C0x00010x000e0x0070TYPE_STRING_ID_ITEMstring_ids_off
30x02580x00020x00070x00a8TYPE_TYPE_ID_ITEMtype_ids_off
40x02640x00030x00030x00c4TYPE_PROTO_ID_ITEMproto_ids_off
50x02700x00040x00010x00e8TYPE_FIELD_ID_ITEMfield_ids_off
60x027C0x00050x00040x00f0TYPE_METHOD_ID_ITEMmethod_ids_off
70x02880x00060x00010x0110TYPE_CLASS_DEF_ITEMclass_defs_off
80x02940x20010x00020x0130TYPE_CODE_ITEMdata_off
90x02A00x10010x00020x0168TYPE_TYPE_LIST
100x02AC0x20020x000e0x0176TYPE_STRING_DATA_ITEM
110x02B80x20030x00020x0221TYPE_DEBUG_INFO_ITEM
120x02C40x20000x00010x022dTYPE_CLASS_DATA_ITEM
130x02D00x10000x00010x023cTYPE_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一一对应

我们可以将图中的数据整理出来得到如下表格:

indexstring_data_offutf16_sizedatastring
00x1760x060x3c 69 6e 69 74 3e 00<init>
10x17e0x090x48 65 6c 6c 6f 20 44 65 78 00Hello Dex
20x1890x0a0x48 65 6c 6c 6f 2e 6a 61 76 61 00Hello.java
30x1950x070x4c 48 65 6c 6c 6f 3b 00LHello;
40x19e0x150x4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 00Ljava/io/PrintStream;
50x1b50x120x4c 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 3b 00Ljava/lang/Object;
60x1c90x120x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 00Ljava/lang/String;
70x1dd0x120x4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 3b 00Ljava/lang/System;
80x1f10x010x56 00V
90x1f40x020x56 4c 00VL
100x1f80x130x5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 00[Ljava/lang/String;
110x20d0x040x6d 61 69 6e 00main
120x2130x030x6f 75 74 00out
130x2180x070x70 72 69 6e 74 6c 6e 00println

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[] 类型 。
indexdescriptor_idxstring
00x03 = 3LHello;
10x04 = 4Ljava/io/PrintStream;
20x05 = 5Ljava/lang/Object;
30x06 = 6Ljava/lang/String;
40x07 = 7Ljava/lang/System;
50x08 = 8V
60x0a = 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区的数据如下红色表示的数据:

最后整理成表格如下:

indexshorty_idxshorty stringreturn_type_idxreturn stringparameters_offparameters sizeparameters string
00x08V0x05V0x00000-
10x09VL0x05V0x01681Ljava/lang/String;
20x09VL0x05V0x01701[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区也截图出来了

整理成表格如下:

indexclass_idxclasstype_idxtypename_idxname
00x04Ljava/lang/System;0x01Ljava/io/PrintStream;0x0cout

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 。本部分的二进制描述如下 :

整理成表格如下:

indexclass_idxclassproto_idxprotoname_idxname
00x00LHello;0x000x00<init>
10x00LHello;0x020x0bmain
20x01Ljava/io/PrintStream;0x010x0dprintln
30x02Ljava/lang/Object;0x000x00<init>

我们结合proto_ids的方法原型,再次整理可以得到如下表格:

indexclassprotonamereturnparamters sizeparamters
0LHello;V<init>V0
1LHello;VLmainV1[Ljava/lang/String;
2Ljava/io/PrintStream;VLprintlnV1Ljava/lang/String;
3Ljava/lang/Object;V<init>V0

至此 ,索引区的内容描述完毕,包括 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 。只有一个类,则此段二进制描述为 :

整理成表格如下:

indexclass_idxaccess_flagssuperclass_idxinterface_offsource_file_idxannotations_offclass_data_offstatic_value_off
数据00x000x010x020x000x020x000x022d
描述0LHello;ACC_PUBLICLjava/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,得到如下图数据区域中红色部分所示,为了能看清整体,我将图片多截出来了很多。

将红色部分的数据按上面的结构体整理成表格如下:

elementvalue
static_fields_size0x00
instance_fields_size0x00
direct_methods_size0x02
vitual_methods_size0x00
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_methodson-elementvaluemeaning
direct_method[0]method_idx_diff0x00Lhello; -> ()V
access_flags0x81 80 04 = 0x10001ACC_PUBLIC ¦ ACC_CONSTRUCTOR
code_off0xb0 02 = 0x0130
direct_method[1]method_idx_diff0x01LHello; -> main([Ljava/lang/String;)V
access_flags0x09 = 0x09ACC_PUBLIC ¦ ACC_STATIC
code_off0xc8 02 = 0x0148

可以发现,得到的2个 method 一个是 <init>,一个是 mainmain方法我们都知道,是我自己定义的,那么<init>方法就是编译器在编译时为我们自动生成的,关于什么是<init>方法,详细看这里

到这里我们发现还有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]

继续根据结构体得到如下表格,后面的三个可选参数,在我们的这个例子中并没有,所以表格中没有列出。

indexmethodregisters_sizeins_sizeouts_sizetries_sizedebug_info_offinsns_sizeinsns
0<init>0x00010x00010x00010x00000x02210x00040x1070 0003 0000 000e
1main0x00030x00010x00020x00000x2260x00080x0062 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 <init>方法

<init>方法就是默认的构造方法,指令为: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;-><init>()V,到这里第一个指令共计6个字节读取完毕。
  4. 第二个指令先读0e,查询文档instruction formats得知的含义是return-void,并且指令格式10x
  5. 在文档instruction formats中查询10x,得知格式为ØØ|op,发现什么也不做,第一个方法的所有指令读取完毕。

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

.method public constructor <init>()V
    .registers 1
    invoke-direct { v0 }, Ljava/lang/Object;-><init>()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          : '<init>'
      type          : '()V'
      access        : 0x10001 (PUBLIC CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 1
      outs          : 1
      insns size    : 4 16-bit code units
000130:                   |[000130] Hello.<init>:()V
000140: 7010 0300 0000    |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值