前言 ==
相信大家都熟悉dex文件,把一个apk给解压缩,就会得到一堆dex文件,但是这些dex文件是怎么来的,又有什么用,为什么这样设计,有进行思考过吗
俗话说知其然,知其所以然,本篇文章开始探究一下这些底层实现细节。
正文 ==
不同的虚拟机
JVM
JVM是Java Virtual Machine的简称,即Java虚拟机,它本质是一层软件抽象,在这之上才可以运行Java程序。Java文件经过编译后会生成JVM字节码,和C语言编译后生成的汇编语言不同,C编译成的汇编语言可以直接在硬件上跑,但是Java编译生成的字节码是在JVM上跑,需要由JVM把字节码翻译成机器指令。
也是由于这个JVM在操作系统上屏蔽了底层实现的差异,从而有了Java的跨平台特性。
DVM
DVM是Dalvik Virtual Machine的简称,是Android4.4及以前使用的虚拟机,所有Android程序都运行在Android系统进程中,每个进程对应着一个Dalvik虚拟机实例。
JVM和DVM都提供了对对象生命周期管理,堆栈管理,安全和异常管理及垃圾回收等重要功能。
但是DVM却不能和JVM一样能直接运行Java字节码,它只能运行.dex文件,而这个.dex文件则是由Java字节码通过Android的dx工具生成的文件。
ART
ART是Android Runtime,在Android5.0开始使用ART虚拟机来替代Dalvik虚拟机,为什么Google要换Android程序运行的虚拟机呢 因为ART虚拟机更优秀。
前面说了Dalvik虚拟机会在APP打开时去运行.dex文件,而这个是实时的,也就是JIT特性(Just In Time),这也就会导致在启动APP时会先将.dex文件转换成机器码,这就导致了APP启动慢的问题。
而ART虚拟机有个很好的特性叫做AOT(ahead of time),这个特性可以在安装APK的时候将dex直接处理成可直接供ART虚拟机使用的机器码,ART虚拟机将.dex文件转换成可直接运行的.oat文件,而且ART虚拟机天生支持多dex,所以ART虚拟机可以很大提升APP的冷启动速度。
除了这个优点外,ART还提升了GC速度,提供功能更全面的Debug特性,但是缺点也就是APK安装速度慢,占用的空间多。
生成和查看dex文件
前面说了dex文件是给Android手机的虚拟机来使用的,所以我们来看看如何生成和查看一个dex文件。
先编写一个简单的.java文件:
public class HelloWorld {
int a = 0;
static String b = "HelloDalvik";
public int getNumber(int i, int j) {
int e = 3;
return e + i + j;
}
public static void main(String[] args) {
int c = 1;
int d = 2;
HelloWorld helloWorld = new HelloWorld();
String sayNumber = String.valueOf(helloWorld.getNumber(c, d));
System.out.println("HelloDex!" + sayNumber);
}
}
然后使用javac命令来编译.java文件为.class,注意这里必须使用Java 8,而不能使用Java 11,如下图专门使用Java 8编译的结果(原来Windows环境变量是Java 11,后续的dex解析有误):
有了.class文件后,就是Android的dx工具,该工具一般在下面目录:
//也就是sdk目录下的build-tools文件夹中
D:\Users\wayee\AppData\Local\Android\sdk\build-tools\30.0.3\dx.bat
使用dx工具对.class文件进行处理:
然后会生成一个.dex文件,直接打开这个dex文件它是十六进制编码的文件,看不出任何有用信息,这时就需要一个专门来看这个的工具,这里推荐使用 010 Editor 这个工具,直接把.dex文件拖入工具:
注意这里选择的模板就是DEX.bt,然后就可以按照DEX的格式来分析这些字节是什么意思了,所以看懂dex文件必须要了解DEX文件的格式。
Dex文件格式
看到这里就必须要清楚一个基本概念了,也就是平时使用Java编写的文件,这里给编译打包成dex文件,那这个dex文件就必须要包含这个Java文件的所有信息,那是按照Java文件顺序一行一行保存为字节码还是其他什么方式呢
所以想知道编译器是如何在编译Java文件后保存信息的,就必须要清楚Dex文件格式。
我们可以直接在刚刚010 Editor软件中看到Dex.bt即Dex文件的格式,其格式如下:
当然也可以去Android源码官网看一下Dex的格式:
看了上面dex文件的格式,其大致可以分为3个区域,分别是文件头、索引区和数据区,那我们就来挨个分析这几个区域有什么作用,以及是如何保存编译后的Java文件。
header 文件头
头文件它包含了这个dex文件的几乎所有信息,所以它的信息非常多,其格式如下:
然后这时就直接点击010 Editor下面的dex\_header部分:
其中上面的红框就是文件头的数据,而下面的红框就是文件头的格式,我们来挨个分析一下。
1、Magic value,即魔数,这个就是用来失败dex这种文件的,可以判断当前的dex文件是否有效,其值是固定死的:
转换成ASCII也就是dex.035, 所以凡是dex文件都是这个开头,否则就是错误的dex文件。
2、checksum,dex文件的校验和,它可以判断dex文件是否损坏或者篡改,占用4个字节,注意这里是采用小字节序的编码方式,即低位上存储的就是低字节内容,可以看一下:
会发现这里的值和二进制保存是相反的。
3、SHA1签名,也就是把整个dex文件用SHA-1签名得到的一个值,占用20个字节。
4、fileSize,整个文件的大小,占用4个字节,看一下值这里是:
十进制是1204,换算成16进制就是4B4,我们再来看看这个dex文件的长度:
这里长度也是4B4。
5、headerSize,表示头结构的大小,占用4个字节,这个就不截图看了。
6、endian\_tag,表示字节序,这里具体的值就2个,标准的.dex格式采用小段字节序,但具体实现可能会选择执行字节交换,所以这个改变就由这个tag来判断。
7、linkSize和linkOff,这2个字段指定了链接段的大小和文件偏移,通常情况下他们都是0,linkSize为0表示为静态链接。
从这里开始就会发现有off这个字段,这是啥意思呢,其实也就是文件偏移量,也就是从这个文件第多少位置开始表示的值。
8、mapOff,这个字段表示DexMapList的文件偏移,这里我们先不多介绍,后面再说,这里值是:
换成16进制就是414h。
9、stringIdsSize和stringIdsOff,这2个字段指定了dex文件中所有用到的字符串的个数和位置偏移,注意这里指的是位置偏移,而不是真正的字符串值。
我们来看看size是多少:
会发现一共有28个字符串,而其值的偏移从112开始,而这个112是不是有点熟悉,112是整个dex头的大小,也就说明在头部之后第一部分就是字符串索引,这里之所以叫做索引也很合理,从112开始的n个字节保存的是程序用到的字符串的偏移量,注意这里不是字符串,只是各个字符串的偏移量。
这时你可能会疑惑,这28个字符串的偏移量该如何存放以及值是多少,我们完全不用担心,还是打开010 Editor软件,选中数据结构是dex\_string\_ids即可:
会发现从70h开始,开始保存的每个字符串的偏移量,而这个偏移量对应的就是最后面部分的值,我们还是拿第一个字符串来说:
会发现这里的偏移量,我们转到偏移量会发现:
这里是用了10个字节来保存了一个字符串,这个字符串是"clinit",我们暂时不考虑这个字符串是啥意思,这里采用了一种叫做uleb的数据结构,来动态保存字符串长度,这里我们暂时不考虑细节。
其实从这个字符串保存的方法来看,我们已经能大概看出是如何保存的了。首先在头部保存字符串大小,以及字符串索引的偏移量,然后再遍历索引找到每个字符串。
比如上面我们的代码,在这里保存的字符串是如下:
10、typeIds和typeIdsOff,有了上面字符串保存的逻辑,这个就是类的类型的数量和位置偏移,也都是占用4个字节,我们还是来看看值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kW77mc6C-1653642835067)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b0eb703dded4459a2e3a9b282327f8f~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
一共用到了9个类型,但是注意这里就没必要像保存字符串一样了,记录每个类型的偏移量,再去偏移的地方取值,这里类型的描述符已经在前面字符串变量中都进行描述过了,所以这里保存的是字符串的索引,我们来看看:
找到上面对应的偏移位置,我们发现第一个类型值是0x5,然后我们再去前面的字符串索引找到下标为5的字符串:
会发现这里的值是I,也就是第一个类型,依次类推,所有的类型如下:
会发现这几个类型的字符串描述在前面字符串列表中都保存过了,这样设计也可以减小查询操作、节省内存。
11、protoIdSize和protoIdOff,这个表示的是方法原型的个数和位置偏移,会发现上面dex文件中有7个方法原型,这里图就不截了,来看一下这7个方法原型都保存了哪些数据:
其实不难理解,想表示一个方法原型不外乎就是方法名、返回值和参数,其中参数可能是多个,所以会有多个类型索引,这里具体的数据结构就不细说了,大体意思理解即可,来看看7个方法原型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M9aNGJ56-1653642835081)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0de94620959b4b9e92178e7999594b2b~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
这里的方法也就是前面java文件中有使用到的。
12、fieldSize和fieldOff,这2个字段就比较简单了,表示java文件中字段的信息,从头部数据结构中会发现有3个字段,我们直接看一下字段索引的数据:
会发现到这里时,信息表示就变的简单了,因为你想表示一个字段,不外乎就是类型、其类的类型、以及自己描述的字符串,而由于前面我们已经得到了字符串索引和类型索引,所以这里数据结构中的值直接使用前面定义过的索引即可。
还是看一下定义的所有字段的值:
13、methodSize和methodOff,这2个字段就比较熟悉了,表示了方法,而方法的表示也是需要几个要点,比如方法所在的类、方法的声明以及方法名,而类在之前类型索引定义过了,方法声明也声明过了,以及方法名也就是之前定义的字符串索引,所以这里我们就不细看其数据结构了,直接看一下我们前面写的java文件有多少个方法:
这里一共有10个方法。
14、classDefsSize和classDefsOff,这2个字段表示类定义的相关信息,类的信息就比较多了,包括类的修饰符、父类、接口、注解、静态元素等等,我们也还是通过010 Editor来看一下class都保存了哪些信息:
可以发现还是有不少信息的。
到这里我们基本就可以把一个类的信息都整清楚了,我们使用一张图来表示:
上图虽然只是表示了文件头的信息,但是我们知道有了这些文件头的信息,根据偏移量便可以获取到其保存的值。
Dex文件格式总结
看了文件头的定义,并且明白其值的意义,便也就熟悉了整个Dex格式的保存原理,我们这里看一张图:
这里除了文件头还有索引区和数据区,其中索引区的偏移量已经在文件头中定义,而数据区则保存着类的定义以及索引区中的数据,而最下面的链接数据区则是一些静态库或者动态库的链接。
总结==
本篇内容有点多,但是还是很好理解的,首先就是虚拟机,在Android系统的虚拟机需要读取dex文件,而这个dex文件是由我们编写的.java文件编译而来,所以dex文件应当保存.java文件的所有信息。
而保存这些信息的方法就像是文件头保存大致地址,索引区保存具体地址,数据区是真的地方,通过这种方式就可以完整的保存一个java文件的信息。
想要了解更多Anrloid相关知识可以点击下方课堂链接 https://edu.51cto.com/course/32703.html Android课
标签:Dex,dex,Java,字节,文件,虚拟机,详解,字符串,Android From: https://blog.51cto.com/u_16163480/6959108