<<Android軟件安全權威指南>>筆記
第三章 Dalvik可執行格式與字節碼規範
早期Android是Dalvik虛擬機,Android4.4以後,引入ART(Android Runtime)虛擬機
JIT(Just-In-Time)即時編譯,AOT(Ahead-Of-Time)預先編譯
ART提供對Dalvik的可執行格式與字節碼的支持
Dalvik虛擬機
特點
專有DEX(Dalvik Executable)可執行文件格式
常量池32位索引
基於寄存器架構,完整指令系統
Dalvik虛擬機與Java虛擬機區別
字節碼
Java源文件 -> Java字節碼保存在class文件中 -> Java虛擬機執行
Java源文件 -> Java字節碼保存在class文件中 -> Dalvik字節碼保存在DEX可執行文件 -> Dalvik虛擬機執行
常量池
dx重新排列Java類文件,消除冗餘信息,dx將java文件轉換爲DEX文件
壓縮常量池,重複字符串和常量只會出現一次
虛擬機架構
Java虛擬機基於棧,Dalvik虛擬機基於寄存器
實例
public class Hello {
public int foo(int a, int b) {
return (a + b) * (a - b);
}
public static void main(String[] args) {
Hello hello = new Hello();
System.out.println(hello.foo(5, 3));
}
}
編譯文件,指定版本
javac -source 1.7 -target 1.7 Hello.java
生成DEX文件
dx --dex --output=Hello.dex Hello.class
javap反編譯Hello.class,查看foo函數的Java字節碼
javap -c -classpath . Hello
得到
public int foo(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: iload_1
4: iload_2
5: isub
6: imul
7: ireturn
java字節碼共佔8字節,每條指令沒有參數
Java虛擬機指令集爲零地址形式的指令集,源參數和目標參數都是隱含的,Java虛擬機提供求值棧的數據結構。
Java程序每一個線程在執行時都有一個PC計數器和一個Java棧
PC計數器以字節爲單位記錄當前運行位置與方法開頭的偏移量,PC計數器只對當前方法有效,Java虛擬機通過PC值來取指令執行。
Java棧記錄Java方法調用的活動記錄,以幀frame爲單位保存線程的運行狀態,每調用一個新方法分配新棧幀壓棧,方法返回則彈出撤出對應棧幀。
每個棧幀包括:局部變量區、操作數棧、其他
最左邊是PC中的偏移量,最多支持0xff條指令
以iload_1
爲例分析
i
是前綴,可以是f
float,l
long,d
double
load
是指令,表示將局部變量存入java棧
_1
數字表示對哪個局部變量進行操作,從0開始計數
使用dexdump,查看foo的Dalvik字節碼
dexdump -d Hello.dex
得到
Virtual methods -
#0 : (in LHello;)
name : 'foo'
type : '(II)I'
access : 0x0001 (PUBLIC)
code -
registers : 5
ins : 3
outs : 0
insns size : 6 16-bit code units
000198: |[000198] Hello.foo:(II)I
0001a8: 9000 0304 |0000: add-int v0, v3, v4
0001ac: 9101 0304 |0002: sub-int v1, v3, v4
0001b0: b210 |0004: mul-int/2addr v0, v1
0001b2: 0f00 |0005: return v0
catches : (none)
positions :
0x0000 line=3
locals :
0x0000 - 0x0006 reg=2 this LHello;
Dalvik指令簡潔,只用了4條
Dalvik虛擬機運行也爲每一個線程維護一個PC和一個調用棧,不同的是,該調用棧維護一個寄存器列表,寄存器的數量在方法結構體的registers
字段給出,根據這個值創建一個虛擬寄存器列表
虛擬機的執行流程
Android系統從下到上爲Linux內核,函數庫,Android運行時環境,框架,應用
Android系統啓動後,立即執行init進程,完成設備初始化工作,再讀取init.rc文件啓動系統中重要外部程序Zygote(我查字典後才知道Zygote是受精卵的意思…還真的挺貼切的)
Zygote是Android系統中所有進程的孵化器進程。
Zygote啓動後,初始化Dalvik虛擬機,再啓動system_server進程進入Zygote模式,通過socket等待命令下達。
執行一個Android應用程序的時候,system_server進程通過Binder IPC方式將命令發送給Zygote,收到命令後,通過fork自身創建一個Dalvik虛擬機實例來執行應用的入口函數,完成程序自啓動。
Zygote三種創建進程的方法
fork()
創建一個Zygote進程,這種方法實際上不會被調用forkAndSpecialize()
創建一個非Zygote進程forkSystemServer()
創建一個系統服務進程
系統服務進程終止後子進程也終止。Zygote進程可以再分爲其他進程,非Zygote進程不能再分。
進程fork後,執行交給Dalvik虛擬機
先通過loadClassFromDex()
函數裝載類,每個類成功解析後,都會獲得運行時環境中的一個ClassObject
類型的數據結構存儲,虛擬機使用gDvm.loadedClasses
全局散列表來存儲和查詢所有裝載進來的類。接下來字節碼驗證器使用dvmVerifyCodeFlow()
函數對裝入的代碼進行校驗,虛擬機調用FindClass()
函數查找並裝載main方法類,最後虛擬機調用dvmInterpret()
函數來初始化解釋器並執行字節碼流
虛擬機執行方式
即時編譯JIT,又叫動態編譯,在運行時將字節碼翻譯爲機器碼使程序執行速度加快。
主流JIT包括兩種字節碼編譯方式
method
方式:以函數方法爲單位編譯trace
方式:以trace爲單位進行編譯
trace方式:在函數中,只有少數代碼是順序執行的,多數代碼有好幾條執行路徑,其中一些路徑很少執行,稱爲冷路徑,執行頻繁的稱爲熱路徑,使用trace編譯能快速獲取熱路徑的代碼。
Dalvik虛擬機默認採用trace方式編譯代碼,同時支持JIT
Dalvik語言基礎
Dalvik彙編語言
基於寄存器的設計,方法在內存創建以後擁有固定大小的棧幀,棧幀空間取決於方法中寄存器數目,運行時數據和代碼在DEX文件中
指令流以16位無符號整型爲存儲單元
Dalvik指令格式
指令的位描述和指令格式標識
位描述
- 每16位的字用空格分開
- 每個字母表示4位,每個字母按順序從高字節到低字節排序,每4位用|分隔
- 順序採用大寫字母A-Z表示4位操作碼,op表示8位
- 表示字段所有位的值爲0
A|G|op BBBB F|E|D|C
爲例,空格將其分爲3個部分,每個部分16位
第一個部分A|G|op
,高8位由A和G組成,低字節op。
第二個部分BBBB
表示一個16位的偏移量
第三個部分F|E|D|C
分別表示寄存器參數
指令格式標識
DEX反彙編工具
目前主流DEX文件反彙編工具:Android官方dexdump
,第三方baksmali
兩種反編譯代碼結構大致相同,寄存器命名有不同,dexdump
使用的是以v開頭的寄存器,baksmali
使用的是以v和p開頭的寄存器。
Dalvik寄存器
基於寄存器架構,設計之初爲ARM架構,Dalvik將部分寄存器映射到ARM寄存器上,還有一部分通過調用棧進行模擬。
Dalvik使用的寄存器都是32位的,64位類型用相鄰兩個寄存器表示
語法格式爲op vAAAA, vBBBB
,則最大值爲216-1,即65535,從0開始,取值爲v0-v65535
每個函數在頭部用.registers
指令指定所使用的寄存器數目,當虛擬機執行到這個函數的時候,會根據寄存器數目分配適當的棧空間。
虛擬機通過處理字節碼對寄存器進行的讀寫操作實際上都是對棧空間進行讀寫操作。
fp爲ARM寄存器棧幀寄存器,取idx的寄存器值
寄存器命名法
兩種寄存器表示方法:v命名法和p命名法
假設一個函數使用M個寄存器,函數有N個參數。則參數使用最後的N個寄存器,局部變量用從v0開始的M-N個寄存器。
以foo爲例子,一共使用5個寄存器,2個顯式的整型參數。
該函數是Hello類的非靜態方法,調用時會傳入一個隱式的Hello對象引用。所以實際上有三個參數,局部變量使用前兩個寄存器,參數使用後三個寄存器。
# virtual methods
.method public foo(II)I
.registers 5
.prologue
.line 3
add-int v0, p1, p2
sub-int v1, p1, p2
mul-int/2addr v0, v1
return v0
.end method
v命名法是所有的寄存器都是v+數字。
p命名法是局部變量使用的寄存器爲v+數字,參數使用的寄存器爲p+數字。可以通過前綴判斷寄存器是局部變量寄存器還是參數寄存器。
Dalvik字節碼
類型
基本類型和引用類型,這兩種類型表示Java語音的全部類型。
Java中的對象和數組都屬於引用對象,其他Java類型爲基本類型。
Dalvik字節碼類型描述符
語法 | 含義 |
---|---|
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位類型,使用相鄰兩個寄存器存儲。
L
表示Java類型中的任何類,Java代碼中表示爲package.name.ObhectName
,在Dalvik彙編代碼中以Lpackage/name/ObjectName;
表示
[
表示基本類型數組,多個表示多維數組,例如[[I
。最大維數爲255
L
和[
可以同時使用
方法
Dalvik用方法名,類型參數,返回值描述一個方法。例如
Lpackage/name/ObjectName;->MethodName(III)Z
括號(III)
爲方法的參數,這裏意思爲三個int類型,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
結束
#
爲註釋
字段
字段的格式
Lpackage/name/ObjectName;->FiledName:Ljava/lang/String;
類型Lpackage/name/ObjectName;
,字段名FiledName
,字段類型Ljava/lang/String;
。
baksmali生成字段代碼以.filed
開頭
Dalvik指令集
Android4.4以前,可以在dalvik/libdex/DexOpcodes.h
中找到完整Dalvik指令集
ART主導系統,可以在art/runtime/dexinstructionlist.h
中找到系統支持的完整的指令集定義。
Dalvik指令集使用單字節的指令助記符
指令類型
參數採用從dest到src的方式
爲一些字節碼添加名稱後綴消除歧義
- 32位常規類型的字節碼未添加任何後綴
- 64位常規類型的字節碼添加
-wide
後綴 - 特殊類型的字節碼,具體類型後綴,除了基本類型外,還有
-string
,-class
,-object
- 斜槓後綴
- 寬度值中的每個字母表示4位的寬度
例子:move-wide/from16 vAA, vBBBB
move
爲基礎字節碼,-wide
爲名稱後綴,表示指令操作的數據寬度爲64位,from16
爲字節碼後綴,表示源爲一個16位寄存器引用變量,vAA
爲dest寄存器,vBBBB
爲src寄存器
空操作指令
nop
,值爲00,通常用於對齊代碼,無操作
數據操作指令
原型爲move dest, src
,有不同後綴
返回指令
指函數運行結束時運行的最後一條指令,基礎字節碼爲return
數據定義指令
定義程序用到的常量,字符串,類等類型,基礎字節碼爲const
鎖指令
多線程程序對同一對象的操作中
monitor-enter vAA
,爲指定對象獲取鎖
monitor-exit vAA
,釋放指定對象的鎖
實例操作指令
包括類型轉換,檢查,創建等
check-cast vAA, type@BBBB
,用於將vAA寄存器中對象引用轉換爲指定的類型,失敗拋出ClassCastException異常。
instance-of vA, vB, type@CCCC
,用於判斷vB寄存器中的對象引用是否可以轉換爲指定類型,如果可以vA賦值1,否則賦值0
new-instance vAA, type@BBBB
,用於構造一個指定類型對象的新實例,對象引用賦值給vAA寄存器,type指定類型不能是數組
數組操作指令
獲取數組長度,新建數組,數組賦值,數組元素取值與賦值
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類型和vA大小的數組並填充數組內容,vA爲隱含使用的
filled-new-array/range {vCCCC, ..., vNNNN}, type@BBBB
,該指令與上一條功能相同,使用range指定取值範圍
arrayop vAA, vBB, vCC
,用於對vBB寄存器指定的數組元素進行取值和賦值,vCC中爲索引,vAA爲取得值或賦的值,,讀取用aget
,賦值用aput
異常指令
throw vAA
,拋出vAA中指定類型的異常
跳轉指令
三種跳轉:無條件跳轉goto
,分支跳轉switch
,條件跳轉if
packed-switch vAA, +BBBBBBBB
,vAA爲switch分支判斷的值,BBBBBBBB
指向一個packed-switch-payload
格式偏移表。
if-test vA, vB, +CCCC
條件跳轉,比較vA和vB的值,比較結果滿足則跳轉到CCCC指定的偏移處,test
有好幾種類型
eq:==, ne:!=, lt:<, ge:>=, gt:>, le:<=
eqz:==0, nez:!=0, ltz:<0, gez:>=0, gtz:>0, lez:<=0
比較指令
對兩個寄存器的值,浮點或者長整型進行比較,格式爲cmpkind vAA, vBB, vCC
,其中vBB和vCC是比較的兩個寄存器,比較的結果放在vAA中。共有5條比較指令
cmpl-float
比較單精度浮點數,vBB>vCC則vAA=-1,vBB=vCC則vAA=0,vBB<vCC則vAA=1
cmpg-float
cmpl-double
cmpg-double
cmp-long
字段操作指令
對對象實例的字段進行讀寫
普通字段:iinstanceop vA, vB, fileld@CCCC
,前綴爲i
靜態字段:sstaticop vAA, field@BBBB
,前綴爲s
後面加上字段類型後綴,例如iget-byte
,sget-wide
方法調用指令
調用類實例的方法,基礎指令爲invoke
invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
invoke-kind/range {vCCCC,...,vNNN}, meth@BBBB
方法類型不同,有5種
invoke-virtual
實例的虛方法
invoke-super
實例的父類方法
invoke-direct
實例的直接方法
invoke-static
實例的靜態方法
invoke-interface
實例的接口方法
返回值必須用move-result*
指令獲取
數據轉換指令
將一種類型的數值轉換爲另一種類型的數值,格式unop vA, vB
,vB中放需要轉換的數據,轉換結果放在vA中。
neg-[type]
求補
not-[type]
求反
XXX-to-XXX
一個類型到另一個類型
數據運算指令
算術運算指令和邏輯運算指令,加減乘除模移位,與或非異或
數據運算指令有4類
binop vAA, vBB, vCC
將寄存器vBB和vCC進行運算,結果放在vAA
binop/2addr vA, vB
將vA與vB進行運算,結果在vA
binop/lit16 vA, vB, #+CCCC
vB與常量CCCC進行運算,結果在vA
binop/lit8 vAA, vBB, #+CC
同上
運算+數據類型
add, sub, mul, div, rem(就是mod), and, or, xor, shl, shr(>>), ushr(>>>)
以上就是所以支持指令,Android4.0以後擴充了一部分指令,助記符後添加jumbo
Dalvik指令練習
編寫smali文件
新建HelloWorld.smali
文件,模版框架如下
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 4
.prologue
return-void
.end method
添加指令,在.prologue
後添加
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 4
.prologue
nop
nop
nop
nop
#數據定義
const/16 v0, 0x8
const/4 v1, 0x5
const/4 v2, 0x3
#數組操作
new-array v0, v0, [I
array-length v1, v0
#實例操作
new-instance v1, Ljava/lang/StringBuilder;
#方法調用
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
#跳轉指令
if-nez v0, :cond_0
goto :goto_0
:cond_0
#數據轉換
int-to-float v2, v2
#數據運算
add-float v2, v2, v2
#比較指令
cmpl-float v0, v2, v2
#字段操作
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello world"
#方法調用
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
:goto_0
return-void
.end method
編譯smali文件
java -jar smali.jar a HelloWorld.smali
會得到out.dex
文件
測試運行
啓動Android運行環境,cmd執行
adb push out.dex /sdcard/
將文件輸入sdcard目錄中
adb shell dalvikvm -cp /sdcard/out.dex HelloWorld
會看到輸出Hello World