Smali語法
基礎介紹
怎麼得到smali檔案?
apk檔案通過apktool反編譯出來的都有一個smali資料夾,裡面都是以.smali結尾的檔案。
Smali是什麼?
Smali是安卓系統裡的Java虛擬機器(Dalvik)所使用的一種.dex格式檔案的彙編器,而Baksmali是反彙編器。其語法是一種寬鬆式的Jasmin/dedexer語法。
Smali語言其實就是一種面向Dalvik的組合語言(組合語言是一種用於電子計算機、微處理器、微控制器,或其他可程式設計器件的低階語言。在不同的裝置中,組合語言對應著不同的機器語言指令集。使用匯編語言編寫的原始碼,然後通過相應的彙編程式將它們轉換成可執行的機器程式碼。這一過程被稱為彙編過程。),我們用smali語言寫的程式碼通過彙編器就轉換成Dalvik可以執行的dex檔案。
對應的有兩個工具,smali.jar將Smali檔案轉換為dex檔案,baksmali.jar則是將dex檔案轉換為smali檔案。apktool能反編譯出smali檔案就是因為它裡面用了baksmali工具。
為什麼要學習Smali?
smali語法是逆向的基礎,它能幫助我們理解別人程式碼的邏輯(當然我們也可以用dex2jar, 更方便檢視程式碼),但是當我們要修改別人的程式碼邏輯,或者加上自己的邏輯,最終是要修改smali檔案的,如果我們不懂smali語法,就無從下手。
如何學習?
smali的語法細節太多了,各種指令,看的人頭疼,我們其實也不需要全都學會,因為現在有工具幫我們了。
AndroidStudio有個外掛: Java2Smali,方便快捷把java程式碼轉換為smali程式碼。有了這個工具,我們可以結合它在實踐中學習smali語法,同時它也可以幫我們省很多事,當我們想在別人的邏輯中增加程式碼時,我們可以把自己的程式碼先用這個工具轉成smali語句, 然後做微調就行了。
當然我們還是需要學習一些基礎語法,瞭解一些常用指令。
如果想完整學習smali語法,可以看看Android軟體安全與逆向分析這本書,網上的很多資料也是來源於這本書。
語法簡介
Dalvik是基於暫存器結構的,在Dalvik位元組碼中,暫存器都是32位的,能夠支援任何型別,64位型別(Long/Double)用2個暫存器表示;Dalvik位元組碼有兩種型別:原始型別;引用型別(包括物件和陣列)。
原始型別
簡寫 | 含義 |
---|---|
B | byte |
C | char |
D | double (64 bits) |
F | float |
I | int |
J | long (64 bits) |
S | short |
V | void 只能用於返回值型別 |
Z | boolean |
物件型別
形式:Lxxx/yyy/zzz;
L
表示這是一個物件型別
xxx/yyy
是該物件所在的包
zzz
是物件名稱
;
標識物件名稱的結束
如:Ljava/lang/String;
陣列型別
形式:[XXX
[I
表示一個int型的一維陣列,相當於int[]
增加一個維度增加一個[
,如[[I
表示int[][]
陣列每一個維度最多255個;
物件陣列表示也是類似
如:String陣列的表示是[Ljava/lang/String
方法
形式:Lxxx/yyy/zzz;->methodName(Lxxx/yyy/zzz;Lxxx/yyy/zzz;I)Z
Lxxx/yyy/zzz
表示物件的型別
methodName
表示方法名
Lxxx/yyy/zzz;Lxxx/yyy/zzz;I
表示三個引數,兩個物件型別,一個整型(方法的引數是一個接一個的,中間沒有隔開)
Z
返回值型別,這裡是boolean型別
如:Log.i("xxx", msg);
轉換成smali語句之後是
Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
欄位
形式:Lxxx/yyy/zzz;->FieldName:Lxxx/yyy/zzz;
Lxxx/yyy/zzz;
表示物件的型別
FieldName
表示欄位名稱
Lxxx/yyy/zzz
表示欄位型別
如:ff = "aa";
轉換後是 Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String
暫存器和變數
Dalvik變數都是存放在暫存器中的,暫存器的命名方法有兩種,v命名法和p命名法,一般我們都用p命名法。
p命名法:暫存器採用v和p來命名,v表示本地暫存器,p表示引數暫存器。
如果一個非靜態方法有兩個本地變數,有三個引數,需要的暫存器關係如下:
v0 第一個本地暫存器
v1 第二個本地暫存器
p0 // 指代呼叫這個方法的this物件。
p1 第一個引數
p2 第二個引數
p3 第三個引數如果是靜態方法,那麼就不需要this物件了,需要的暫存器是v0, v1, p0, p1, p2。
.registers
使用這個指令指定方法中暫存器的總數
.locals
使用這個指定表明方法中非參暫存器的總數,放在方法的第一行。
常用指令
.field private isFlag:z — 定義變數
.method — 方法
.prologue — 方法開始
.end method — 方法結束
.parameter — 方法引數
.line 12 — 此方法位於第12行
invoke-super — 呼叫父函式
invoke-direct — 呼叫函式
invoke-static — 呼叫靜態函式
const/high16 v0, 0x7f03 — 把0x7f03賦值給v0
return-void — 函式返回void
new-instance — 建立例項
move-result v0 — 將上一個invoke型別的指令操作的非物件結果賦值給v0暫存器
move-result-object v0 — 將上一個invoke型別指令操作的物件賦值給v0暫存器
欄位操作指令
對於例項欄位和靜態欄位有兩類指令:
iget, iput 對例項欄位進行讀,寫
sget, sput 對靜態欄位進行讀,寫
會根據不同型別新增不同的字尾:
iget,iget-object,iget-boolean,iget-byte,iget-char,iget-short
iput,iput-object,iput-boolean,iput-byte,iput-char,iput-short
獲取static fields的指令示例:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
上句中sget-object指令把out這個變數獲取並放到v0暫存器中。
獲取instance fields的指令與static fields的類似,需要指明物件所屬的例項。示例:
iget-object v0, p0, Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String;
上句iget-object指令比sget-object多了一個引數p0,就是該變數所在類的例項,在這裡就是p0即“this”。
條件跳轉指令
“if-eq vA, vB, :cond_x” — 如果vA等於vB則跳轉到:cond_x
“if-ne vA, vB, :cond_x” — 如果vA不等於vB則跳轉到:cond_x
“if-lt vA, vB, :cond_x” — 如果vA小於vB則跳轉到:cond_x
“if-ge vA, vB, :cond_x” — 如果vA大於等於vB則跳轉到:cond_x
“if-gt vA, vB, :cond_x” — 如果vA大於vB則跳轉到:cond_x
“if-le vA, vB, :cond_x” — 如果vA小於等於vB則跳轉到:cond_x
“if-eqz vA, :cond_x” — 如果vA等於0則跳轉到:cond_x
“if-nez vA, :cond_x” — 如果vA不等於0則跳轉到:cond_x
“if-ltz vA, :cond_x” — 如果vA小於0則跳轉到:cond_x
“if-gez vA, :cond_x” — 如果vA大於等於0則跳轉到:cond_x
“if-gtz vA, :cond_x” — 如果vA大於0則跳轉到:cond_x
“if-lez vA, :cond_x” — 如果vA小於等於0則跳轉到:cond_x