在進入正題以前,推薦閱讀一下以前的兩篇文章。第一篇是個人一篇譯文 —— 譯文找不到了,就放一下原文吧。html
上面這篇文章簡單比較了 Dalvik 和 Art 。其中的一些細節在個人另外一篇文章 說說方舟編譯器 中也有所說起,你們能夠大體瀏覽一下。android
而後再推薦一篇 Android逆向筆記 —— DEX 文件格式解析,在最後解析 DexCode
部分時,詳細的逐字節的解析了一段 Dalvik 字節碼。你們能夠挑這一段閱讀一下,對 Dalvik 字節碼有一個大概的認識。數組
下面就正式來進入 Dalvik 的世界。緩存
Dalvik
是早期 Android 版本中用於運行安卓應用的虛擬機,由 Dan Bornstein
編寫的,名字來源於他的祖先曾經居住過名叫 Dalvík 的小漁村,村子位於冰島。當年也有一部分業內人士認爲 Dalvik 是 Google 爲了不與 Oracle 的訴訟而誕生的產物。Dalvik 是基於 Apache License 2.0
發佈的。Google 說 Dalvik 是一個清潔室(clean room)的實現,而不是一個在標準 Java 運行環境的改進,這意味着它不繼承標準版本的或開源的 Java 運行環境的版權許可限制。關於這一點,Oracle 和一些專家還在討論中。bash
Dalvik 是解釋執行加上 JIT,每次app運行的時候,它動態的將一部分 Dalvik 字節碼 解釋爲機器碼。隨着 App 的運行,更多的字節碼被編譯和緩存。由於 JIT 只編譯了一部分代碼,它具備更小的內存佔用和更少的設備物理空間佔用。可是,邊解釋邊執行,效率低下,這也是後來 Dalvik 遭到拋棄的緣由。微信
從 Android 4.4 開始,Google 開始引入了全新的虛擬機 ART(Android Runtime)。ART 是基於 AOT 編譯的,因爲安裝應用耗時過程,後期高版本的 Android 系統加入了增強版的 JIT 編譯。Dalvik 在 Android 5.0 中正式被刪除,ART 完成上位。那麼如今來學習 Dalvik 還有必要嗎?其實 ART 是向下兼容的,ART 和 Dalvik 是運行 Dex 字節碼的兼容運行時,所以針對 Dalvik 開發的應用也能在 ART 環境中運做。不過,Dalvik 採用的一些技術並不適用於 ART。所以,Dalvik 虛擬機的部分特性以及 Dalvik 字節碼指令其實和 ART 都是相通的。架構
Dalvik 和 JVM 並不兼容,甚至能夠說徹底是兩套機制。下面來講幾點它們之間的區別。app
咱們都知道 JVM(Java 虛擬機)識別的是 Class 文件,我以前寫過一篇 Class 文件格式詳解,詳細介紹了 Class 文件的二進制結構。JVM 運行的是 Java 字節碼,而 Dalvik 運行的是 Dalvik 字節碼。Dalvik 不識別單個的 Class 文件,而是將全部 Class 文件打包成 DEX 文件格式,經過解釋 DEX 文件來執行字節碼。ide
這樣帶來的直接好處就是 Dalvik 的可執行文件的體積更小。若是你瞭解 Class 文件格式的話,你會知道每一個 Class 文件都有單獨的字符串常量池。若是不一樣的 Class 文件中有相同的字符串,那麼就存在重複存儲的狀況。一樣的,若是一個類引用了其餘類中的方法,相應的方法簽名也會被複制到該類文件中。這樣就會有不少沒必要要的冗餘信息,既浪費內存也影響執行效率。
那麼 DEX 文件是如何解決這個問題的呢?對 DEX 文件結構不瞭解的話,能夠閱讀個人另外一篇文章 Android逆向筆記 —— DEX 文件格式解析。DEX 文件提供了一個統一的共享的常量池,供全部類文件使用,這樣就避免了冗餘信心,減少了文件體積,提升瞭解析效率。
JVM 是基於棧架構的。當程序運行時,Java 虛擬機會頻繁的對棧進行讀寫數據的操做。在這個過程當中,不只會屢次進行指令分派和內存訪問,並且會耗費大量的 CPU 時間,所以,對於資源有限的手機設備來講,是一筆很大的開銷。每調用一個方法,就會分配一個新的棧幀並壓入棧。每從一個方法返回,就彈出相應的棧幀。
Dalvik 是基於寄存器架構的,數據的訪問直接在寄存器之間傳遞。
基於堆棧的機器與基於寄存器的機器誰更有優點一直是個爭論不休的話題。 通常來講,基於堆棧的機器必須使用指令才能從堆棧上的加載和操做數據,所以,相對基於寄存器的機器,它們須要更多的指令才能實現相同的性能。可是基於寄存器機器上的指令必須通過編碼,所以,它們的指令每每更大。
上面這段來自百度。的確,Java 虛擬機的操做碼都是單字節的,其指令字總操做碼個數不超過 256 條。而 Dalvik 指令則長的多的多,數量也多的多。要執行相同的操做,JVM 須要短可是更多的指令,Dalvik 須要長可是更少的指令。Dalvik 的思路是用更長可是更少的指令來減小指令分派和內存訪問,以此提升運行效率。
關於 Dalvik 指令格式,官網 中也有相關介紹。只是官網的介紹實在過於晦澀,看了不少遍才理解。我這裏仍是從實際的 Dalvik 指令來進行分析。把以前分析過的 main()
方法直接拿過來:
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
複製代碼
其 DexCode 以下:
62 00 01 00 62 01 00 00 6E 20 03 00 10 00 0E 00
複製代碼
這裏要注意的是 DEX 文件是小端表示法,低位在前,高位在後。一般低 8 位就是 op 碼,也就是咱們說的操做碼。在上面的例子中,第一個操做碼就是 62
,咱們在 Dalvik 指令集中能夠找到其表明的指令。關於 Dalvik 指令集,Android 開發者網站也作了總結,點我查看。另外,在 Android 4.4 以前的 AOSP 源碼中的 dalvik/libdex/DexOpcodes.h 中也有定義。我這裏直接在官網資源中查找 62
,結果以下圖所示:
操做碼 62
表示的指令是 sget-object
,表示獲取一個靜態對象。僅僅知道了操做碼的含義仍是不夠的,咱們還不知道該條指令的完整格式。注意列表最左側的 21c
,它表示的就是指令的格式。關於指令的格式,Android 官網也作了相關總結,點我查看。一樣的,AOSP 中也有相關定義,位於 Android 4.0 版本中的 dalvik/docs/instruction-formats.html 文件。查一下 21c
的指令格式,以下圖所示:
可得 21c
對應的指令格式爲 AA|op BBBB
。對應的上面的十六進制,作一下對比:
AA|op BBBB -> op vAA kind@BBBB
00|62 0001 -> 62 v00 kind@0001
複製代碼
這樣一看,就很清晰了。該指令一共是兩個 16 位的字,第一個 16 位的低 8 位是操做碼 62,表示 sget-object
,高 8 位表示使用的是 v0
寄存器。第二個 16 位是索引值 1,指向 Dex 中 field_id 部分的第一項,根據以前的解析結果,第一項表示的字段是 Ljava/lang/System;->out;Ljava/io/PrintStream
,整合一下,這個指令的完整格式以下:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
複製代碼
表示獲取靜態字段 PrintStream.out
,並保存在寄存器 v0
中。回頭再看一下 21c
,它的每個字符其實都是有含義的。
2
表示該指令有多少個 16 位的字組成1
表示該指令最多使用多少個寄存器c
爲類型碼,c 表明常量池索引關於類型碼,還有不少種,以下表所示:
助記符 | 位數 | 含義 |
---|---|---|
b | 8 | 有符號當即數(字節) |
c | 1六、32 | 常量池索引 |
f | 16 | 接口常量(僅對靜態連接格式有效) |
h | 16 | 有符號當即數(32 位或 64 位值的高階位,低階位全爲 0) |
i | 32 | 有符號當即數(整型)或 32 位浮點數 |
l | 64 | 有符號當即數(長整型)或 64 位雙精度浮點數 |
m | 16 | 方法常量(僅對靜態連接格式有效) |
n | 4 | 有符號當即數(半字節) |
s | 16 | 有符號當即數(短整型) |
t | 八、1六、32 | 分支目標 |
x | 0 | 無額外數據 |
還有一種特殊狀況指令的末尾會多出一個字母。若是是 s
,表示指令採用靜態連接。若是是字母 i
,表示指令應該被內聯處理。
咱們都知道 Dalvik 虛擬機是基於寄存器架構的,其使用的寄存器都是 32 位的。對於 64 位類型,使用相鄰兩個寄存器來表示。Dalvik 基本都是基於 ARM 架構的,ARM 架構的 CPU 自己就含有必定數量的寄存器,那麼 Dalvik 虛擬機支持多少個寄存器呢?咱們來看一個 move
指令:
move/16 vAAAA, vBBBB
複製代碼
vAAAA vBBBB
,每一個大寫字母表示 4 位,一共就是 2^16 -1
,也就是 65535
個。固然,不可能會有 65535 個真實寄存器。Dalvik 使用的是虛擬寄存器,它會將部分寄存器映射到 ARM 的寄存器上,另一部分經過調用棧進行模擬。
Dalvik 虛擬機爲每個進程維護一個調用棧,這個棧的做用之一就是虛擬寄存器。虛擬機經過處理字節碼對寄存器進行讀寫操做,實際上就是對棧空間進行讀寫。可是在實踐中,一個方法須要 16 個以上的寄存器不太常見,而須要 8 個以上的寄存器卻至關廣泛,所以不少指令僅限於尋址前 16 個寄存器。
那麼寄存器是如何命名的呢?上面的分析中提到過 v0
寄存器,是否是 65535 個寄存器就是 v0 - v65535
呢?實際上,寄存器有兩種命名方式,v 命名法
和 p 命名法
。在介紹它們以前,先來講一些基本概念。如下面這個 add
函數爲例:
public int add(int a,int b){
int c = a+b;
return c;
}
複製代碼
它使用了幾個寄存器?若是不是很肯定,能夠查看其 smali 代碼中的 .registers
字段。答案是 4 個。根據 Dalvik 虛擬機規定,方法參數使用最後面的寄存器。這 4 個寄存器中的最後兩個就是存儲參數 a
和 b
。因爲 add()
是非靜態函數,因此該方法老是會傳入當前對象的引用 this
,因此其實是 3 個參數,佔用最後 3 個寄存器。而剩餘的開頭的寄存器就是局部變量寄存器,在 add()
方法中只有一個局部變量寄存器,用於存儲 a+b
的值,就是第一個寄存器。下面就來看看 v 命名法
和 p 命名法
分別是如何給這 4 個寄存器命名的。
v 命名法其實很簡單,就是上面說的 v0 - v65535
。無論是參數寄存器,仍是局部變量寄存器,一概以 v
開頭。在 add()
函數中,4 個寄存器命名以下:
v0 :
局部變量寄存器,存儲 a+b 的值v1 :
當前引用 thisv2 :
參數寄存器,存儲 a 的值v3 :
參數寄存器,存儲 b 的值p 命名法針對參數寄存器進行了優化,參數寄存器的命名從 p0
開始,使得局部變量寄存器和參數寄存器得以很容易的進行區分。smali 語法中就是用了 p 命名法。咱們來看下 add()
方法的 smali 代碼:
.method public add(II)I
.registers 4
.param p1, "a" # I
.param p2, "b" # I
.prologue
.line 6
add-int v0, p1, p2
.line 7
.local v0, "c":I
return v0 .end method
複製代碼
這樣就很清晰了,4 個寄存器命名以下所示:
v0 :
局部變量寄存器,存儲 a+b 的值p0 :
當前引用 thisp1 :
參數寄存器,存儲 a 的值p2 :
參數寄存器,存儲 b 的值p 命名法
更加已讀,通常都是使用 p 命名法。
在更深刻的瞭解 Dalvik 字節碼前,先來看一下 Dalvik 是如何描述字段和方法的,這也有助於咱們閱讀 smali 代碼。
Dalvik 字節碼中只有兩種類型,基本類型和引用類型。除了對象和數組之外,其餘的全部 Java 類型都是基本類型。這和 JVM 的類型描述符是基本一致的。基本類型都是使用單個字母來表示。數組類型使用 [
表示。除數組之外的引用類型使用 L
加上全限定名錶示。以下表所示:
類型描述符 | 類型 |
---|---|
v | void,只用於返回值類型 |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | 對象類型 |
[ | 數組 |
基本類型都很簡單,就很少說了,下面舉一個引用類型的例子。例如 String
對象,其全限定名是 java/lang/String;
,在 Dalvik 中就表示爲 Ljava/lang/String;
。對於數組,又能夠分爲基本類型數組和引用類型數組,其格式都是 [
加上類型描述符。int[]
就是 [I
,String[]
就是 [java/lang/String;
。多維數組就是多個 [
,例如 int[][]
就是 [[I
。
字段的表示統一用以下格式:
類型;->字段名稱:類型描述符
複製代碼
好比一個 com.test.Test
類中的一個 String
類型的 name
字段,在 Dalvik 中就可表示爲:
Lcom/test/Test;->name:Ljava/lang/String
複製代碼
方法的描述和字段的描述有一些相似,區別在於方法多了一個返回值的描述,其基本格式以下:
類型;->方法名(參數類型描述符)返回值類型描述符
複製代碼
以 com.test.Test
類中的 add()
方法爲例,就是上面用到的兩數相加的函數,其在 Dalvik 中描述爲:
Lcom/test/Test;->add(II)I
複製代碼
add(II)
中的兩個 I 表示兩個 int 類型參數,後面跟的一個 I 表示返回值類型是 int。
有了上面的知識儲備以後,就能夠具體的學習 Dalvik 指令集了。除了以前介紹過的官方文檔和 AOSP 中關於 Dalvik 指令集的整理,我我的常常閱讀的,還有一份國外開發者整理的 Dalvik Opcodes,都是很好的學習資料。我也會基於此版本整理一份完整的中文版 Dalvik 操做碼,可能還須要一段時間才能整理出來,到時候會開源出來。
下面簡單整理一下 Dalvik 指令集。
語法 | 說明 |
---|---|
nop | 空指令,一般用於對齊 |
語法 | 說明 |
---|---|
move vA, vB | 將 vB(4 位) 寄存器的值賦給 vA(4 位) 寄存器 |
move/from vAA, vBBBB | 將 vBBBB(16 位) 寄存器的值賦給 vAA(8 位) 寄存器 |
move-object vA, vB | 將 vB(4 位) 寄存器存儲的對象賦給 vA(4 位) 寄存器 |
move-object/from16 vAA, vBBBB | 將 vBBBB(16 位) 寄存器存儲的對象賦給 vAA(8 位) 寄存器 |
move-result vAA | 將最新的 invoke-kind 的單字非對象結果移到指定的寄存器 vAA 中 |
move-result-wide vAA | 將最新的 invoke-kind 的雙字非對象結果移到指定的寄存器 vAA 中 |
move-result-object vAA | 將最新的 invoke-kind 的對象結果移到指定的寄存器 vAA 中 |
move-exception | 將剛剛捕獲的異常保存到給定寄存器中 |
語法 | 說明 |
---|---|
return-void | 返回 void |
return-vAA | 返回一個 32 位非對象類型 |
return-wide vAA | 返回一個 64 位非對象類型 |
return-object vAA | 返回一個對象類型 |
語法 | 說明 |
---|---|
const/4 vA, #+b | 將給定的字面值(符號擴展爲 32 位)移到指定的寄存器 vA 中 |
const vAA, #+BBBBBBBB | 將給定的字面值移到指定的寄存器 vAA 中 |
const/high16 vAA, #+BBBB0000 | 將給定的字面值(右零擴展爲 32 位)移到指定的寄存器 vAA 中 |
const-wide/16 vAA, #+BBBB | 將給定的字面值(符號擴展爲 64 位)移到指定的寄存器對 vAA 中 |
const-wide vAA, #+BBBBBBBBBBBBBBBB | 將給定的字面值移到指定的寄存器對 vAA 中 |
const-string vAA, string@BBBB | 將經過給定的索引獲取的字符串引用移到指定的寄存器 vAA 中 |
const-class vAA, type@BBBB | 將經過給定的索引獲取的類引用移到指定的寄存器 vAA 中 |
語法 | 說明 |
---|---|
monitor-enter vAA | 獲取指定對象的互斥鎖 |
monitor-exit vAA | 釋放指定對象的互斥鎖 |
語法 | 說明 |
---|---|
check-cast vAA, type@BBBB | 若是給定寄存器 vAA 中的引用不能轉型爲指定的類型,則拋出 ClassCastException |
instance-of vA, vB, type@CCCC | 若是指定的引用是給定類型的實例,則爲給定目標寄存器賦值 1,不然賦值 |
new-instance vAA, type@BBBB | 根據指定的類型構造新實例,並將對該新實例的引用存儲到目標寄存器 vAA 中 |
語法 | 說明 |
---|---|
array-length vA, vB | 獲取寄存器 vB 中數組的長度,並存入寄存器 vA |
new-array vA, vB, type@CCCC | 構造指令類型(type@CCCC) 和指定大小(vB) 的數組,並賦給 寄存器 vA |
filled-new-array {vC, vD, vE, vF, vG}, type@BBBB | 構造指令類型(type@BBBB) 和指定大小的數組,並填充內容 |
語法 | 說明 |
---|---|
throw vAA | 拋出 vAA 寄存器指定的異常 |
語法 | 說明 |
---|---|
goto +AA | 無條件跳轉至指定偏移處,偏移量爲 AA |
if-test vA, vB, +CCCC | 若是兩個給定寄存器的值比較結果符合預期,則跳轉到偏移量 CCCC 處 |
同 if-test
同樣,還有 if-eq
if-ne
if-lt
if-ge
if-gt
if-le
if-testz
if-eqz
if-nez
if-ltz
if-gez
if-ltz
if-gez
if-gtz
if-lez
,這些指令格式都是一致的。
字段操做指令分爲兩類,分別是對於普通字段和靜態字段的操做。
語法 | 說明 |
---|---|
iinstanceop vA, vB, field@CCCC | 對已標識的字段執行已肯定的對象實例字段運算,並將結果加載或存儲到值寄存器中 |
針對不一樣類型的普通字段,有以下命令:
iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short
iput、iput-wide、iput-object、iput-boolean、iput-byte、iput-char、iput-short
複製代碼
語法 | 說明 |
---|---|
sstaticop vAA, field@BBBB | 對已標識的靜態字段執行已肯定的對象靜態字段運算,並將結果加載或存儲到值寄存器中 |
針對不一樣類型的靜態字段,有以下命令:
sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short
sput、sput-wide、sput-object、sput-boolean、sput-byte、sput-char、sput-short
複製代碼
方法調用指令的格式爲 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
, 具體的有以下指令:
語法 | 說明 |
---|---|
invoke-virtual | 調用正常的虛方法(該方法不是 private、static 或 final,也不是構造函數) |
invoke-super | 調用父類方法 |
invoke-direct | 調用非 static 直接方法(也就是說,本質上不可覆蓋的實例方法,即 private 實例方法或構造函數) |
invoke-static | 調用 static 方法 |
invoke-interface | 調用實例的接口方法 |
數據運算指令和數據轉換指令都比較簡單,且數量不少,這裏就不浪費篇幅來寫出來了,感興趣的同窗能夠查閱資料看一下。後續我也會開源一個完整版的 Dalvik 指令集的表格。
本文介紹了 Dalvik 虛擬機的相關知識,比較了 Dalvik 虛擬機和 JVM,後續着重介紹了 Dalvik 指令集。看懂看會 Dalvik 指令對咱們作逆向是頗有幫助的,畢竟想要修改程序邏輯,大部分時間就是在和 smali 代碼打交道。而 smali 代碼就是基於 Dalvik 指令集的。若是你閱讀過 smali 代碼,應該會對上面提到的 Dalvik 指令很熟悉。
最後再放一下 Android 逆向筆記系列的其餘文章,按順序閱讀效果更佳!
文章首發微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解。更多逆向相關知識,掃碼關注我吧!