最近在看《深刻理解Android: Java虛擬機ART》,說實話,這本書的內容仍是很深的,對於我來講就像一個小學生在作初中的數學題同樣。在看第三章深刻理解Dex文件格式的時候,其中最後一部分講「指令碼描述規則」,沒有看懂(怪本身水平過低),最後經過個人不懈努力,終於看懂了,下面記錄一下這個過程,但願對後面的新手有所幫助。html
要想將這部份內容講懂,那確定要結合具體的例子,下面就經過最簡單的一個例子來說解這個過程:java
首先咱們先貼出一段在《深刻理解java虛擬機》這本書中常常用到的java代碼:android
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
複製代碼
而後咱們經過javac編譯成.class文件數組
javac TestClass
複製代碼
編譯完成後,會在當前目錄生成TestClass.class文件。bash
在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
咱們就只介紹 000124:
處指令碼該如何解析。編碼
d800 0001
。 看到這個指令的第一個問題確定是這個是怎麼來的(如何生成的)只有搞明白如何來的才能去分析它。
首先咱們看以下箭頭指示
inc()
方法嗎,可是這個方法在dex文件中的表示涉及到官方文檔中
文件板式,因此咱們須要簡單的介紹一下dalvik的文版樣式(這裏我只簡單介紹這個inc()方法是怎麼來的,感興趣的同窗能夠去看官方文檔):
首先咱們看一下文版樣式中class_defs
,class_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
,它由d8
和00
兩個字節構成,根據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
中的高八位00
,op
表示d800
中的低八位d8
; CC|BB
即表示01|00
。
至此,咱們終於瞭解了d800 0001
所表明的含義以及在官方文檔中表格的對應關係。
有的讀者可能想了解指令碼d800 0001
對應的具體操做,因爲不是本章重點,因此這裏只簡單提一下,d8:add-int/lit8
指令根據描述其實就是執行+
這個二目運算,而後根聽說明中的內容:對指定寄存器(第一個參數這裏爲0
)和字面量(第二個參數這裏是1
)執行指定的二元運算,並將結果存儲到目標寄存器中。(具體的vX
,#+X
所表明的含義參考語法)。
-《深刻理解java虛擬機》