dalvik字节码初识

最近在看《深刻理解Android: Java虚拟机ART》,说实话,这本书的内容仍是很深的,对于我来讲就像一个小学生在作初中的数学题同样。在看第三章深刻理解Dex文件格式的时候,其中最后一部分讲“指令码描述规则”,没有看懂(怪本身水平过低),最后经过个人不懈努力,终于看懂了,下面记录一下这个过程,但愿对后面的新手有所帮助。html

要想将这部份内容讲懂,那确定要结合具体的例子,下面就经过最简单的一个例子来说解这个过程:java

从.java到.class

首先咱们先贴出一段在《深刻理解java虚拟机》这本书中常常用到的java代码:android

public class TestClass {
    private int m;

    public int inc() {
        return m + 1;
    }

}
复制代码

而后咱们经过javac编译成.class文件数组

javac TestClass
复制代码

编译完成后,会在当前目录生成TestClass.class文件。bash

从.class到.dex

在android运行时环境中,须要将.class文件进行翻译、重构、解释、压缩等操做生成.dex文件,这个时候须要借助工具dx.bat,这个工具位于sdk根目录/build-tools/任意版本里面,例如个人是在E:\Java\Sdk\build-tools\28.0.3下面(好比你是用的是cmd命令,须要切换至该目录下)。经过以下命令将.class文件编译成.dex文件:函数

dx --dex --output=TestClass.dex TestClass.class
注意:这个命令中TestClass.class文件须要放置在当前目录下
复制代码

执行完成后,咱们会在当前目录下看到TestClass.dex文件。接下来咱们须要经过dexdump工具来反编译TestClass.dex文件(注:dexdump工具和javap工具做用相似,只是javap是反编译的.class文件)工具

dexdump -d TestClass.dex
复制代码

执行结果以下ui

Processing 'TestClass.dex'...
Opened 'TestClass.dex', DEX version '035'
Class #0 -
  Class descriptor  : 'LTestClass;'
  Access flags      : 0x0001 (PUBLIC)
  Superclass        : 'Ljava/lang/Object;'
  Interfaces        -
  Static fields     -
  Instance fields   -
    #0 : (in LTestClass;)
      name          : 'm'
      type          : 'I'
      access        : 0x0002 (PRIVATE)
  Direct methods    -
    #0 : (in LTestClass;)
      name          : '<init>'
      type          : '()V'
      access        : 0x10001 (PUBLIC CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 1
      outs          : 1
      insns size    : 4 16-bit code units
0000f8:                                        |[0000f8] TestClass.<init>:()V
000108: 7010 0200 0000                         |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0002
00010e: 0e00                                   |0003: return-void
      catches       : (none)
      positions     :
        0x0000 line=1
      locals        :
        0x0000 - 0x0004 reg=0 this LTestClass;

  Virtual methods   -
    #0 : (in LTestClass;)
      name          : 'inc'
      type          : '()I'
      access        : 0x0001 (PUBLIC)
      code          -
      registers     : 2
      ins           : 1
      outs          : 0
      insns size    : 5 16-bit code units
000110:                                        |[000110] TestClass.inc:()I
000120: 5210 0000                              |0000: iget v0, v1, LTestClass;.m:I // field@0000
000124: d800 0001                              |0002: add-int/lit8 v0, v0, #int 1 // #01
000128: 0f00                                   |0004: return v0
      catches       : (none)
      positions     :
        0x0000 line=5
      locals        :
        0x0000 - 0x0005 reg=1 this LTestClass;

  source_file_idx   : 4 (TestClass.java)
复制代码

到这,咱们终于拿到了反编译的dex文件了,下面咱们就能够结合具体的内容来说今天的重点字节码格式了,开心 ?:this

dalvik字节码格式介绍

咱们就只介绍 000124: 处指令码该如何解析。编码

指令码标记图
咱们看到此处的指令码为 d800 0001。 看到这个指令的第一个问题确定是这个是怎么来的(如何生成的)只有搞明白如何来的才能去分析它。

首先咱们看以下箭头指示

inc()方法执行区域
这不就是咱们在java代码中写的 inc()方法吗,可是这个方法在dex文件中的表示涉及到官方文档中 文件板式,因此咱们须要简单的介绍一下dalvik的文版样式(这里我只简单介绍这个inc()方法是怎么来的,感兴趣的同窗能够去看官方文档):

首先咱们看一下文版样式中class_defsclass_def里面记录了类中的信息

名称 格式 说明
class_defs class+def_item[] 类定义列表。这些类必须进行排序,以便所指定类的超类和已实现的接口比引用类更早出如今该列表中。此外,对于在该列表中屡次出现的同类名,其定义是无效的

下面咱们看一下class_def_item中的class_data_off

名称 格式 说明
class_data_off unit 从文件开头到此项的关联类数据的偏移量;若是此类没有类数据,则该值为 0(这种状况有可能出现,例如,若是此类是标记接口)。该偏移量(若是为非零值)应该位于 data 区段,且其中的数据应采用下文中“class_data_item”指定的格式,同时全部项将此类做为定义符进行引用。

接下来咱们看一下class_data_item中的两个方法direct_methods和virtual_methods:

名称 格式 说明
direct_methods encoded_method[direct_methods_size] 定义的直接(static、private 或构造函数的任何一个)方法;以一系列编码元素的形式表示。这些方法必须按 method_idx 以升序进行排序。
virtual_methods encoded_method[virtual_methods_size] 定义的虚拟(非 static、private 或构造函数)方法;以一系列编码元素的形式表示。此列表不得包括继承方法,除非被此项所表示的类覆盖。这些方法必须按 method_idx 以升序进行排序。虚拟方法的 method_idx 不得与任何直接方法相同。

从这两个说明中咱们可以知道,direct_methods定义的是(static、private或构造方法),virtual_methods定义的是虚拟(非static、private或构造函数)方法。因此咱们定义的inc()方法应该是在encoded_method[virtual_methods_size]中指定,而后咱们看一下encoded_method:

名称 格式 说明
code_off uleb128 从文件开头到此方法代码结构的偏移量;若是此方法是 abstract 或 native,则该值为 0。该偏移量应该是到 data 区段中某个位置的偏移量。数据格式由下文的“code_item”指定。

这里要说一下uleb128,这个是无符号leb128(“Little-Endian Base 128”),表示无符号整数的可变长度编码。而little-endian表示小端字节序(即低位在前,高位在后)
而后咱们看一下说明中指的code_item

名称 格式 说明
insns ushort[insns_size] 字节码的实际数组。insns 数组中代码的格式由随附文档 Dalvik 字节码指定。请注意,尽管此项被定义为 ushort 的数组,但仍有一些内部结构倾向于采用四字节对齐方式。此外,若是此项刚好位于某个字节序交换文件中,则交换操做将只在单个 ushort 上进行,而不是在较大的内部结构上进行。

ushort:表示16位无符号整数,采用小端字节序。
看说明咱们可知,上面所说的指令码d800 0001就是这里的ushort类型的数据,表示两个字节。
至此,咱们终于说完了指令码d800 0001的由来(详细的说明须要结合官方文档来看),下面咱们就能够分析这个字节码的含义了,哈哈,开心 ( ?:)

解析指令码

前面咱们已经介绍了指令码d800 0001的由来以及它的ushort类型,如今咱们就能够进行解析了。

这里的解析咱们须要结合Dalvik可执行指令格式中关于按位描述的内容进行,首先咱们看官方文档按位描述中的一个例子

这里直接拿例子来讲:
“B|A|op CCCC”格式表示其包含两个 16 位代码单元。第一个字由低 8 位中的操做码和高 8 位中的两个四位值组成;第二个字由单个 16 位值组成
复制代码

而后咱们将d800 0001来与之对应:

首先来看d800,它由d800两个字节构成,根据little-endian字节序,咱们知道d8表示低8位,00表示高8位。而由上面按位描述的例子可知,低八位表示操做码。高八位表示参数

既然咱们知道了操做码是d8,那么咱们如何去查找操做码对应的内容了,这个我当时也是看了好长时间也没看明白,最后终于在字节码集合中找到了操做码的位置,在这个表格中,运算和格式标签中的运算就是咱们要找的操做码,如图:

操做码

而后咱们找到d8这个操做码,

运算和格式 助记符/语法 参数 说明
d8
22b
binop/lit8 vAA,vBB, #+CC
add-int/lit8
A:目标寄存器(8位)
B:源寄存器(8位)
C:有符号整数常量(8位)
对指定的寄存器(第一个参数)和字面值(第二个参数)执行指定的二元运算,并将结果存储到目标寄存器中

这里咱们先不讨论具体的语法命令,咱们先看运算和格式表格中的格式22b,这个在哪里能找到了,就在格式说明中的ID,而后咱们找到22b这个ID,以下表:

格式 ID 语法 包含的重要操做码
AA|op CC|BB 22b op vAA,vBB,#+CC

如今咱们看到了格式AA|op CC|BB,让咱们将指令码d800 0001与这个格式相对应,即AA表示d800中的高八位00op表示d800中的低八位d8; CC|BB 即表示01|00

至此,咱们终于了解了d800 0001所表明的含义以及在官方文档中表格的对应关系。

延伸

有的读者可能想了解指令码d800 0001对应的具体操做,因为不是本章重点,因此这里只简单提一下,d8:add-int/lit8指令根据描述其实就是执行+这个二目运算,而后根听说明中的内容:对指定寄存器(第一个参数这里为0)和字面量(第二个参数这里是1)执行指定的二元运算,并将结果存储到目标寄存器中。(具体的vX#+X所表明的含义参考语法)。

参考文献

-Android Dalvik官方文档

-Dalvik虚拟机字节码和指令集对照表

-《深刻理解java虚拟机》

相关文章
相关标签/搜索