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虛擬機》

相關文章
相關標籤/搜索