深入理解Android:Java虚拟机ART 读书笔记 以下内容均来自书中内容 建议看原书哦

3.1 Dex文件格式总览

ARM CPU通用寄存器比较多,Class格式的文件在移动设备上不能扬长避短,invokevirtual指令的时候,我们看到Class文件中指令码执行的时候需要存取操作数栈(operand stack)。而在移动设备上,由于ARM的CPU有很多通用寄存器,Dex中的指令码可以利用它们来存取参数。显然,寄存器的存取速度比位于内存中的操作数栈的存取速度要快得多。

字节码文件的创建

一个Class文件对应一个Java源码文件,而一个Dex文件可对应多个Java源码文件。开发者开发一个Java模块(不管是Jar包还是Apk)时:

在PC平台上,该模块包含的每一个Java源码文件都会对应生成一个同文件名(不包含后缀)的.class文件。这些文件最终打包到一个压缩包(即Jar包)中。

而在Android平台上,这些Java源码文件的内容最终会编译、合并到一个名为classes.dex的文件中。不过,从编译过程来看,Java源文件其实会先编译成多个.class文件,然后再由相关工具将它们合并到Jar包或Apk包中的classes.dex文件中。

这样的优点可能有:

  1. 虽然Class文件通过索引方式能减少字符串等信息的冗余度,但是多个Class文件之间可能还是有重复字符串等信息。而classes.dex由于包含了多个Class文件的内容,所以可以进一步去除其中的重复信息。
  2. 如果一个Class文件依赖另外一个Class文件,则虚拟机在处理的时候需要读取另外一个Class文件的内容,这可能会导致CPU和存储设备进行更多的I/O操作。而classes.dex由于一个文件就包含了所有的信息,相对而言会减少I/O操作的次数。

字节序

Java平台上,字节序采用的是Big Endian,class文件也是,而Android平台上的Dex文件默认的字节序是Little Endian(这可能是因为ARM CPU(也包括X86CPU)采用的也是Little endian字节序的原因吧)。

Java ByteBuffer类提供了一个非常简单API,它可以很方便处理不同字节序的问题

public static void testEndian(){ byte[] content = new byte[] {0x01,0x02,0x03,0x04};//内容 int littleEndianExpectedValue = (0x04<<24)|(0x03<<16)|(0x02<<8)| (0x01<<0); //按LittleEndian方式解析得到的期望值 int bigEndianExpectedValue = (0x01<<24)|(0x02<<16)|(0x03<<8)| (0x04<<0); //按BigEndian方式解析得到的期望值 //创建一个ByteBuffer(java.nio包中),并设置字节序BigEndian ByteBuffer byteBuffer = ByteBuffer.wrap(content); byteBuffer.order(ByteOrder.BIG_ENDIAN); int readValue = byteBuffer.getInt(); assert(readValue==bigEndianExpectedValue); //比较readValue和bigEndianExpectedValue byteBuffer.rewind(); //ByteBuffer回滚到第一个字节以重新读取其内容。 byteBuffer.order(ByteOrder.LITTLE_ENDIAN); //这次设置字节序为Little Endian, readValue = byteBuffer.getInt(); assert(readValue==bigEndianExpectedValue); //比较readValue和littleEndianExpectedValue }

新增LEB128数据类型

LEB128是Little Endian Based 128的缩写,其唯一功能就是用于表示32比特位长度的数据。传统的int型数据是32位长,比如0这个int型数据需要4个字节。但是如果使用LEB128格式的话,0这个数只要1个字节就可以表示了。

SLEB128:Signed LEB128,有符号的整数。结尾字节的第6位用于表示是否为负数。ULEB128:Unsigned LEB128,无符号的整数。将所有字节的7位数据经过移位等组合成一个无符号32位数据。

3.2 认识Dex文件

header_item是Dex文件头结构 偏移量也是一种形式的索引。

insns_size和insns数组:指令码数组的长度和指令码的内容。Dex文件格式中JVM指令码长度为2个字节,而Class文件中JVM指令码长度为1个字节。

3.3 Dex指令码介绍

Dex指令码的条数和Class指令码差不多,都不超过255条,但是Dex文件中存储函数内容的insns数组(位于code_item结构体里)却比Class文件中存储函数内容的code数组(位于Code属性中)解析起来要有难度。其中一个原因是Android虚拟机在执行指令码的时候不需要操作数栈,所有参数要么和Class指令码一样直接跟在指令码后面,要么就存储在寄存器中。对于参数位于寄存器中的指令,指令码就需要携带一些信息来表示该指令执行时需要操作哪些寄存器。

insns的组织形式

函数的内容存储在insns数组里,该数组元素的类型是ushort,而ushort为两个字节长。

Dex指令码与第一个参数混在一起构成了一个双字节元素存储在insns内。在这个双字节中,低8位才是指令码,高8位是参数。笔者称这种双字节元素为[参数+操作码组合]。

[参数+操作码组合]后的下一个ushort双字节元素可以是新一组的[参数+操作码组合],也可以是[纯参数组合]。

参数组合的格式也有要求,不同的字符代表不同的参数,参数的比特位长度又是由字符的个数决定。比如AA表示一个参数,这个参数占8位,而其中每一个A都代表4位比特长。

参数的长度由对应字符的个数决定,1个字符占据4个比特。比如:A表示一个占4比特的参数,AA代表一个占8比特的参数,AAAA代表一个16比特长的参数。

代表一个特殊的参数,该参数取值为0。比如øø表示这样一个参数,这个参数长度为8位,每位的取值都是0。

指令码描述规则

第一列叫Format,指明指令码和参数的存储格式

第二列叫Format ID,简称ID,其内容包含两个数字和一到多个后缀字符。其中,第一个数字表示一条完整的指令(即执行该指令需要的指令码和参数)包含几个ushort元素,第二个数字表示这条指令将用到几个寄存器。

第三列则具体展示了各个参数的用法。

由第一列Format可知一共有A、BBBB、C、D、E、F、G7个参数。每个参数的位长由代表该参数的字符的个数决定。即,除了BBBB是16位长之外,其他6个参数都是4位。Format同时还指明了这7个参数位于ushort元素中的位置。

由第二列ID的“35c”可知,这种类型的指令需要3个ushort元素,并且需要5个寄存器。

第三列给出符合第二列ID格式的指令的具体表现形式。其中,[A=x]表示A参数取值为x。“vC”表示某个寄存器,其编号是C的值,“kind@BBBB”表示BBBB为指向xxx_ids的索引。另外,“{}”花括号表示该指令执行时候需要操作的一组寄存器。

此处简单介绍00021e处指令码"1200"该如何解析。

  • "1200"是指令码的内容。它是16进制,分为"12"和"00",分别构成两个字节。因为该dex文件是Little Endian字节序。所以"12"是低8位,"00"是高八位。
  • 操作码+参数组合的话,操作码位于低8位,所以此处可知操作码为"12"。通过查询https://source.android.com/devices/tech/dalvik/dalvik-bytecode可知,该指令的标准格式为"1211n",对应的助记符为"const/4 vA,#+B"。
  • 接下来需要解析这个参数。这需要利用解析格式(Format)"11n"。查询官方文档,11n对应的指令码+参数组合的格式为"B|A|op"。所以,"1200"中,"B|A"取值为"0|0",即B为0,A也为0。
  • 有了这些信息,"const/4 vA,#+B"就可解析为"const/4 v0,#+0"。

todo:编写一个Dex文件格式的解析程序

Dex文件格式Android官方介绍 https://source.android.com/devices/tech/dalvik/dex-format

Dex指令码格式Android官方介绍 https://source.android.com/devices/tech/dalvik/dalvik-bytecode

https://source.android.com/devices/tech/dalvik/instruction-formats