下一節,介紹Dalvik指令集Android Dalvik虛拟機之Dalvik指令集-Smali彙編解析
Dalvik虛拟機為自己專門設計了一套指令集,并且制定了自己的指令格式與調用規範。我們将Dalvik指令集組成的代碼稱為Dalvik彙編代碼,将這種代碼表示的語言稱為Dalvik彙編語言(Dalvik彙編語言并不是正式的語言,隻是描述Dalvik指令集代碼的一種稱呼)。
1. Dalvik指令格式
一段Dalvik彙編代碼由一系列Dalvik指令組成,指令文法由指令的位描述與指令格式辨別來決定。位描述約定如下:
- 每16位的字采用空格分隔開來。
- 每個字母表示4位,每個字母按順序從高位元組開始,排列到低位元組。每4位之間可能使有豎線“|”來表示不同的内容。
- 順序采用A~Z的單個大寫字母作為一個4位的操作碼,op表示一個8位的操作碼。
- “Ø”來表示這字段所有位為0值。
以指令格式“A|G|op BBBB F|E|D|C”為例:
指令中間有兩個空格,每個分開的部分大小為16位,是以這條指令由三個16位的字組成。第一個16位是“A|G|op”,高8位由A與G組成,低位元組由操作碼op組成。第二個16位由BBBB組成,它表示一個16位的偏移值。第三個16位分别由F,E,D,C共四個4位組成,在這裡它們表示寄存器參數。
單獨使用位辨別還無法确定一條指令,必須通過指令格式辨別來指定指令的格式編碼。它的約定如下:
- 指令格式辨別大多由三個字元組成,前兩個是數字,最後一個是字母。
- 第一個數字是表示指令有多少個16位的字組成。
- 第二個數字是表示指令最多使用寄存器的個數。特殊标記“r”辨別使用一定範圍内的寄存器。
- 第三個字母為類型碼,表示指令用到的額外資料的類型。取值見下表。
還有一種特殊的情況是末尾可能會多出另一個字母,如果是字母 s 表示指令采用靜态連結,如果是字母 i 表示指令應該被内聯處理。指令格式辨別的類型碼如下:
助記符 | 位大小 | 說明 |
b | 8 | 8位有符号立即數 |
c | 16,32 | 常量池索引 |
f | 16 | 接口常量(僅對靜态連結格式有效) |
h | 16 | 有符号立即數(32位或64位數的高值位,低值位為0) |
i | 32 | 立即數,有符号整數或32位浮點數 |
m | 16 | 方法常量(僅對靜态連結格式有效) |
n | 4 | 4位的立即數 |
s | 16 | 短整型立即數 |
t | 8,16,32 | 跳轉,分支 |
x | 無額外資料 |
以指令格式辨別 22x 為例:
第一個數字2表示指令有兩個16位字組成,第二個數字2表示指令使用到2個寄存器,第三個字母x表示沒有使用到額外的資料。
另外,Dalvik指令對文法做了一些說明,它約定如下:
- 每條指令從操作碼開始,後面緊跟參數,參數個數不定,每個參數之間采用逗号分開。
- 每條指令的參數從指令第一部分開始,op位于低8位,高8位可以是一個8位的參數,也可以是兩個4位的參數,還可以為空,如果指令超過16位,則後面部分依次作為參數。
- 如果參數采用“vX”的方式表示,表明它是一個寄存器,如v0,v1等。這裡采用v而不用r是為了避免與基于該 虛拟機架構本身的寄存器命名産生沖突,如ARM架構寄存器命名采用r開頭。
- 如果參數采用“#+X”的方法表示,表明它是一個常量數字。
- 如果參數采用“+X”的方式表示,表明它是一個相對指令的位址偏移。
- 如果參數采有“[email protected]”的方式表示,表明它是一個常量池索引值。其中kind表示常量池類型,它可以是“string”(字元串常量池索引),“type”(類型常量池索引),“field”(字段常量池索引)或者“meth”(方法常量池索引)。
以指令 “op vAA, [email protected]” 為例:指令用到了1個寄存器參數 vAA,并且還附加了一個字元串常量池索引 [email protected],其實這條指令格式代表着 const-string 指令。
2. DEX檔案反彙編工具
目前DEX可執行檔案主流的反彙編工具有:BakSmali與Dedexer。兩者的反彙編效果都不錯,在文法上也有着很多的相似處。下面通過代碼對比兩者的文法差異,測試代碼采用上一節的Hello.java,首先使用dx工具生成Hello.dex檔案,然後在指令提示符下輸入以下指令使用baksmali.jar反彙編 Hello.dex:
$ java -jar baksmali.jar -o baksmaliout Hello.dex
指令成功執行會在baksmaliout目錄下生成Hello.smali檔案,使用文本編輯器打開它:
# virtual methods
.method public foo(II)I
.registers 5
.parameter
.parameter
.prologue
.line 3
add-int v0, p1, p2
sub-int v1, p1, p2
mul-int/2addr v0, v1
return v0
.end method
執行以下指令使用ddx.jar(Dedexer的jar檔案)反彙編Hello.dex:
$ java -jar ddx.jar -d ddxout Hello.dex
指令成功執行後,會在ddxout目錄下生成Hello.ddx檔案,使用文本編輯器打開它,foo()函數代碼如下:
.method public foo(II)I
.limit registers 5
; this: v2 (LHello;)
; parameter[0] : v3 (I)
; parameter[1] : v4 (I)
.line 3
add-int v0, v3, v4
sub-int v1, v3, v4
mul-int/2addr v0,v1
return v0
.end method
兩種反彙編代碼大體的結構組織是一樣的,在方法名,字段類型與代碼指令序列上它們保持一緻,具體的差異表現在一些文法細節上。對比之下,可以發現如下不同點:
- 前者使用.registers指令指定函數用到的寄存器數目,後者在.registers指令前加了limit字首。
- 前者使寄存器p0作為this引用,後者使用寄存器v2作為this引用。
- 前者使用一條.parameter指令指定函數一個參數,後者則使用parameter數組指定參數寄存器。
- 前者使用.prologue指令指定函數代碼起始處,後者卻沒有。
- 兩者寄存器表示法不同,前者使用p命名法,後者使用v命名法。
BakSmali提供反彙編功能的同時,還支援使用Smali工具打包反彙編代碼重新生成dex檔案,這個功能被廣泛應用于apk檔案的修改,更新檔,破解等場合,因而更加受到開發人員的青睐。本系列blog預設都将采用Smali文法格式。
3. 了解Dalvik寄存器
Dalvik虛拟機基于寄存器架構,在代碼中大量地使用到了寄存器。Dalvik虛拟機是作用于特定架構的CPU上運作的,在設計之初采用了ARM架構,ARM架構的CPU本身內建了多個寄存器,Dalvik将部分寄存器映射到了ARM寄存器上,還有一部分則通過調用棧進行模拟。注意:Dalvik中用到的寄存器都是32位的,支援任何類型,64位類型用2個相鄰寄存器表示。(這節具體的内容還是看書吧!非蟲的書)
4. 兩種不同的寄存器表示方法——v命名法與p命名法
前面曾多次提到v命名法與p命名法,它們是Dalvik位元組碼中兩種不同的寄存器表示方法。下面我們來看看,它們在表現上有一些什麼樣的差別。
假設一個函數使用到M個寄存器,并且該函數有N個參數,根據Dalvik虛拟機參數傳遞方式中的規定:參數使用最後的N個寄存器,局部變量使用從v0開始的前(M-N)個寄存器。如前面的小節中,foo()函數使用到了5個寄存器,2個顯式的整形參數,其中foo()函數是Hello類的非靜态方法,函數被調用時會傳入一個隐式的Hello對象引用,是以,實際傳入的參數數量是3個。根據傳參規則,局部變量将使用前2個寄存器,參數會使用後3個寄存器。
v命名法采用以小寫字母“v”開頭的方式表示函數中用到的局部變量與參數,所有的寄存器命名從v0開始,依次遞增。對于foo()函數,v命名法會用到v0,v1,v2,v3,v4等五個寄存器,v0與v1用來表示函數的局部變量寄存器,v2表示被傳入的Hello對象的引用,v3與v4分别表示兩個傳入的整形參數。
p指令法對函數的局部變量寄存器命名沒有影響,它的命名規則是:函數中引入的參數命名從p0開始,依次遞增。對于foo()函數,p命名法會用到v0,v1,p0,p1,p2等五個寄存器,v0與v1同樣用來表示函數的局部變量寄存器,p0表示被傳入的Hello對象的引用,p1與p2分别表示兩個傳入的整形參數。
對于有M個寄存器及N個參數的函數foo()來說,v命名法與p命名法的表現形式如下表所示。通過觀察可以發現,使用p命名法表示的Dalvik彙編代碼,通過寄存器的字首更容易判斷寄存器到底是局部變量寄存器還是參數寄存器,在Dalvik彙編代碼較長,使用寄存器較多的情況下,這種優勢将更加明顯。表:v命名法與p命名法:
v命名法 | p命名法 | 寄存器含義 |
v0 | v0 | 第一個局部變量寄存器 |
v1 | v1 | 第二個局部變量寄存器 |
... | ... | 中間的局部變量寄存器依次遞增 |
vM-N | p0 | 第一個參數寄存器 |
... | ... | 中間的參數寄存器分别依次遞增 |
vM-1 | pN-1 | 第N個參數寄存器 |
5. Dalvik位元組碼的類型,方法與字段表示方法
Dalvik位元組碼有着一套自己的類型,方法與字段表示方法,這些方法與Dalvik虛拟機指令集一起組成了一條條的Dalvik彙編代碼。
5.1. 類型
Dalvik位元組碼隻有兩種類型,基本類型與引用類型。Dalvik使用這兩種類型來表示Java語言的全部類型,除了對象與數組屬于引用對象外,其他的Java類型都是基本類型。BakSmali嚴格遵守了DEX檔案格式中的類型描述符定義。類型描述符對照如下表:
文法 | 含義 |
V | void,隻用于傳回值類型 |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | Java類類型 |
[ | 數組類型 |
每個Dalvik寄存器都是32位大小,對于小于或等于32位長度的類型來說,一個寄存器就可以存放該類型的值。而像J,D等64位的類型,它們的值是使用相鄰兩個寄存器來存儲的,如 v0 與 v1,v3 與 v4等。
L類型可以表示Java類型中的任何類。這些類在Java代碼中以 package.name.ObjectName方式引用,到了Dalvik彙編代碼中,它們以Lpackage/name/ObjectName; 形式表示,注意最後有個分号,L表示後面跟着一個Java類,package/name/表示對象所在的包,ObjectName表示對象的名稱,最後的分号表示對象名結束。例如:Ljava/lang/String;相當于java.lang.String。
[類型可以表示所有基本類型的數組。[後面緊跟基本類型描述符,如 [I 表示一個整型一維數組,相當于Java中的int[]。多個[在一起時可用來表示多元數組,如 [[I 表示int[][],[[[I表示 int[][][]。注意多元數組的維數最大為255個。L與[ 可以同時使用用來表示對象數組。如 [Ljava/lang/String;就表示Java中的字元串數組。
5.2. 方法
方法的表現形式比類名要複雜一些,Dalvik使用方法名,類型參數與傳回值來較長的描述一個方法。這樣做一方面有助于Dalvik虛拟機在運作時從方法表中快速地找到正确的方法,另一方面,Dalvik虛拟機也可以使用它們來做一些靜态分析,比如Dalvik位元組碼的驗證與優化。方法格式如下:
Lpackage/name/ObjectName;->Methodname(III)Z
在這個例子中,Lpackage/name/ObjectName;應該了解為 個類型,MethodName為具體的方法名,(III)Z是方法的簽名部分,其中括号 内的III為方法的參數(在此為三個整型參數),Z表示方法的傳回類型(boolean類型)。
下面是一個更為複雜的例子:
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
按照上面的知識,将其轉換成Java形式的代碼應該為:
String method(int, int[][], int, String, Object[])
BakSmali生成的方法代碼 .method指令開始,以 .end method指令結束,根據方法類型的不同,在方法指令開始前可能會用“#”号加以注釋。如“# virtual methods”表示這是一個虛方法,“#direct methods”表示這是一個直接方法。
5.3. 字段
字段與方法很相似,隻是字段沒有方法簽名域中的參數與傳回值,取而代之的是字段的類型。同樣,Dalvik虛拟機定位字段與位元組碼靜态分析時會用到它。字段的格式如下:
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
字段由類型(Lpackage/name/ObjectName;),字段名(FieldName)與字段類型(Ljava/lang/String;)組成。其中字段名與字段類型中間用冒号“:”隔開。
BakSmali生成的字段代碼以 .field指令開頭,根據字段類型的不同,在字段指令的開始可能會用“#”号加以注釋,如:“# instance fields”表示這是一個執行個體字段,“#static fields”表示這是一個靜态字段。
原文:https://my.oschina.net/fhd/blog/365337