一、前言

在對某個 apk 文件進行代碼注入的時候,我們面對的往往是反編譯后的 smali 代碼,而不是直接的 Java 源碼文件,因而了解 smali 語法基礎還是很有必要的。在這里先介紹下 Dalvik 虛擬機:Dalvik 是 Google 專門為 Android 平臺設計的虛擬機。雖然 Android 程序可以使用 Java 語言來進行開發,但 Dalvik VM 和 Java VM 是兩款不同的虛擬機。Dalvik VM 基于寄存器,而 Java VM 基于棧 。Dalvik VM 有專門的文件執行格式 dex (Dalvik Executable),而 Java VM 則執行的是 Java 字節碼。DVM 比 JVM 速度更快,占用的空間更少。

二、smali文件結構

下面的 smali 代碼取自某個測試 demo(通過 apktool 反編譯 .apk 文件獲取,這里先對 smali 語法格式進行介紹),目的是先對 smali 的文件內容結構有個大概的了解,有利于后面對語法細節講解的時候有個整體把握。

.class public abstract Lcom/happy/learnsmali/BaseActivity;.super Landroidx/appcompat/app/AppCompatActivity;.source "BaseActivity.kt" # interfaces.implements Lcom/happy/learnsmali/action/ActivityAction;.implements Lcom/happy/learnsmali/action/ClickAction;.implements Lcom/happy/learnsmali/action/HandlerAction;.implements Lcom/happy/learnsmali/action/BundleAction;.implements Lcom/happy/learnsmali/action/KeyboardAction;  # annotations.annotation system Ldalvik/annotation/MemberClasses;    value = {        Lcom/happy/learnsmali/BaseActivity$Companion;,        Lcom/happy/learnsmali/BaseActivity$OnActivityCallback;    }.end annotation .annotation system Ldalvik/annotation/SourceDebugExtension;    value = "SMAPBaseActivity.ktKotlin*S Kotlin*F+ 1 BaseActivity.ktcom/happy/learnsmali/BaseActivity+ 2 fake.ktkotlin/jvm/internal/FakeKt*L1#1,179:11#2:180*E".end annotation # static fields.field public static final Companion:Lcom/happy/learnsmali/BaseActivity$Companion; .field public static final RESULT_ERROR:I = -0x2  # instance fields.field private final activityCallbacks$delegate:Lkotlin/Lazy;  # direct methods.method public static synthetic $r8$lambda$mAxgPA6JBXhjuhBfNvUeqmKUmlk(Lcom/happy/learnsmali/BaseActivity;Landroid/view/View;)V    .locals 0     invoke-static {p0, p1}, Lcom/happy/learnsmali/BaseActivity;->initSoftKeyboard$lambda-0(Lcom/happy/learnsmali/BaseActivity;Landroid/view/View;)V     return-void.end method .method static constructor ()V    .locals 2     new-instance v0, Lcom/happy/learnsmali/BaseActivity$Companion;     const/4 v1, 0x0     invoke-direct {v0, v1}, Lcom/happy/learnsmali/BaseActivity$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V     sput-object v0, Lcom/happy/learnsmali/BaseActivity;->Companion:Lcom/happy/learnsmali/BaseActivity$Companion;     return-void.end method .method public constructor ()V    // ....end method

上面的代碼中,如果你剛開始接觸 smali 代碼,看得是一頭霧里云里的話那是正常的,下面我將進行解析,讀懂這些符號的含義有利于在我們反編譯 apk 進行注入代碼的時候達到事半功倍的效果。

smali中的繼承、接口、包信息

首先我們先看看開頭的幾行:

.class public abstract Lcom/happy/learnsmali/BaseActivity; // .class 表示類路徑 包名+類名.super Landroidx/appcompat/app/AppCompatActivity;           // .super 表示父類的路徑.source "BaseActivity.kt"                                   // 表示源碼文件名 # interfaces.implements Lcom/happy/learnsmali/action/ActivityAction;.implements Lcom/happy/learnsmali/action/ClickAction;.implements Lcom/happy/learnsmali/action/HandlerAction;.implements Lcom/happy/learnsmali/action/BundleAction;.implements Lcom/happy/learnsmali/action/KeyboardAction;  # annotations.annotation system Ldalvik/annotation/MemberClasses;    value = {        Lcom/happy/learnsmali/BaseActivity$Companion;,        Lcom/happy/learnsmali/BaseActivity$OnActivityCallback;    }.end annotation

1-3行定義基本信息:表示有源文件 BaseActivity.kt 反編譯得到的 smali 文件(第三行),文件路徑位于 com/happy/learnsmali/(第二行),繼承于 androidx/appcompat/app/AppCompatActivity(第三行)。

5-9行定義接口信息:表示 BaseActivity 類實現的接口類有:

  • com/happy/learnsmali/action/ActivityAction
  • com/happy/learnsmali/action/ClickAction
  • com/happy/learnsmali/action/HandlerAction
  • com/happy/learnsmali/action/BundleAction
  • com/happy/learnsmali/action/KeyboardAction

11-16行定義內部類:表示 BaseActivity 類有兩個內部類 -- Companion 和 OnActivityCallback。

分析完 smali 開頭的文件信息,我們可以據此可以構造出 java 代碼:

class BaseActivity extends AppCompatActivity    implements ActivityAction, ClickAction, HandlerAction, BundleAction, KeyboardAction {     class Companion {        // ...    }     class OnActivityCallback {        // ...    }}

其他方法

# virtual methods   //Representation is a virtual method.method protected onCreate(Landroid/os/Bundle;)V    .locals 1    .param p1, "savedInstanceState"    # Landroid/os/Bundle;     .line 10    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V     .line 11    const/high16 v0, 0x7f050000     invoke-virtual {p0, v0}, Lcom/justart/samlidemo/MainActivity;->setContentView(I)V     .line 12    return-void.end method
  • 方法以 .method 開始,以 .end method 結束;
  • 位于第一行的最后 V 表示返回類型為 void;
  • 方法參數 Landroid/os/Bundle; 表示方法 onCreate() 的參數為 Bundle 類型;
  • . param 表示方法的參數名稱為 savedInstanceState;
  • 最后 return-void 表示返回的值類型為 void;

三、數據類型

  • byte:B
  • char:C
  • double:D
  • float:F
  • int:I
  • long:J
  • short:S
  • void:V
  • boolean:Z
  • array:[XXX
  • Object:Lxxx/yyy

相信有 JNI 基礎會對上面的數據類型好明白,這里解析上面的最后兩項:

array:[XXX

在基礎類型前加 [ 表示數組類型,例如 int 數組和 byte 數組為 [I、[B。

Object:Lxxx/yyy

以 L 開頭的類型表示為對象,如 String 對象對應表示為 Ljava/lang/String; (對象類型需要后面跟分號),其中 java/lang 表示 java.lang 包,String 表示該包路徑下的一個對象。

這里可能會有童鞋有疑惑,如果類是使用 Ljava/lang/String; 來表示,那么內部類又應該在 smali 中如何定義呢?可能使用過 Java 反射的童鞋腦海里面閃過 $ 符號。是的,在 smali 語法中同樣是使用 Ljava/lang/String$xxx; 來表示 xxx 是 String 類的內部類。

四、寄存器

Dalvik VM 與 JVM 最大的區別之一就是 Dalvik VM 是基于寄存器的。基于寄存器是什么意思呢?個人理解的是有點類似于匯編語言,通過寄存器來存儲數據、傳遞數據。在 smali 中本地寄存器用 v 開頭的字母 + 數字來表示,如 v0、v1、v2 、...,而參數寄存器則使用 p 開頭 + 數字來表示,如 p1、p2、p3 、...。特別注意的是,p0 參數寄存器不一定是表示第一個參數,在非 static 函數中,p0 表示 this,p1 則表示第一個參數,p2 表示函數中的第二個參數。而在 static 函數中 p0 則才對應第一個參數(因為 Java 的 static 的方法沒有對象的概念)。本地寄存器沒有限制,理論上是可以任意使用的。

五、成員變量

下面繼續介紹有關成員變量的內容:

# static field.field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = "installationId"http://... # instance field.field private _activityPackageName:Ljava/lang/String;

上面定義的 static field 和 instance field 均為成員變量,格式是:

.field pubilc/private [static] [final] varName:<類型>

static field 和 instance field 雖然均為成員變量,但它們還是存在區別的。當然最明顯的區別就是是否與對象相關,static field 是類層面的概念,而 instance field 是對象層面的概念。

出現成員變量,那就意味著有變量的賦值與取值。在 smali 語法中,取值指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object 等,而賦值指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object 等。

iget / iput 分別表示 instance field 成員變量的取值和賦值;
sget / sput 分別表示 static field 成員變量的取值和賦值;
是否為 instance field 還是 static field 成員的取值和賦值指令,根據指令前綴判斷即可。帶 -object 后綴表示操作的是成員變量是對象類型,而不帶該后綴則表示操作的是基本數據類型。特別地,boolean 基本數據類型使用帶 -boolean 后綴。

下面有個例子:

const/4 v0, 0x0 iput-boolean v0, p0, Lcom/disney/xx/XxActivity;->isRunning:Z

在上面的例子中,使用了 v0 本地寄存器,并且把 0x0 傳遞到 v0 本地寄存器,然后第二句使用 iput-boolean 指令把 v0 寄存器中的值傳遞到 com.disney.xx.XxActivity 的成員變量 isRunning。即相當于:this.isRunning = false;(上面提到,在非 static 函數中 p0 表示為 this ,在這里則表示為 com.disney.xx.XxActivity 的對象實例)。

static field 成員變量

sget-object v0, Lcom/disney/xx/XxActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;

操作指令 sget-object 是用來獲取靜態成員變量并保存在緊接的本地參數列表中。在這里,把位于 com.disney.xx.XxActivity 類中的靜態成員 PREFS_INSTALLATION_ID 的值傳遞給本地寄存器 v0。

instance field 成員變量

iget-object v0, p0, Lcom/disney/xx/XxActivity;->_view:Lcom/disney/common/WMWView;

操作指令 iget-object 也是用來獲取類成員變量并保存在緊接的本地參數列表中。這里把 com.disney.xx.XxActivity 類中的對象成員 _view 賦值給本地寄存器 v0 中。

通過觀察上面的 static field 靜態成員變量 和 instance field 類成員變量,可以總結出以下的格式:

<本地寄存器>, [<參數寄存器>], <變量所屬的類變量> ->varName:<變量類型>

put 指令和上面提到的 get 指令格式是類似的,這里可以直接通過看下面的例子:

const/4 v3, 0x0 sput-object v3, p0, Lcom/disney/xx/XxActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;

Java 代碼表示: this.globalIapHandler = null; (null = 0x0)

.local v0, wait:Landroid/os/Message; const/4 v1, 0x2 iput v1, v0, Landroid/os/Message;->what:I

Java 代碼表示: wait.what = 0x2;(wait 是 Message 的實例)

六、函數調用

函數定義的格式:

function (type1type2type3...)RetValue

需要注意的是函數的參數類型需要定義為 smali 語法中的類型,同時參數之間不可以有其他的分隔符,例子如下:

helloSmali ()V

表示 void helloSmali()

helloSmali ([BI)Z

表示 boolean helloSmali(byte[], int)

helloSmali (ZLjava/lang/String;[I[I)V

表示 void helloSmali(boolean, String, int[], int[])

在 smali 中函數和成員變量一樣也分為兩種類型,但不同于成員變量中的 static field 靜態成員變量 和 instance field 類成員變量,函數中的是 direct method 和 virtual method。那么函數的 direct method 和 virtual method 有什么區別呢?簡單來說,direct method 就是 private 函數,而 virtual method 則是 public 和 protect 函數。

所以在調用函數的時候,有 invoke-direct、invoke-virtual,另外還有 invoke-static、invoke-super 以及 invoke-interface 等幾種不同的指令。同時還存在著 invoke-XXX/range 指令,這是參數傳參個數大于 4 個的時候調用的指令。

invoke-static

invoke-static {}, Lcom/disney/xx/UnlockHelper;->unlockCrankypack()Z

invoke-static 表示調用的是類靜態函數。Java 代碼表示為:UnlockHelper.unlockCrankypack(),這里注意到 invoke-static 后緊接著 {},表示的是調用該方法的實例 + 參數列表,由于這個方法既不需要參數,也是類靜態方法,所以 {} 內為空,再看一個例子:

const-string v0, "fmodex" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

這里調用的是 static void System.loadLibrary(String) 來加載 so 庫,而 v0 則表示傳參 fmodex。

invoke-super

表示調用父類方法用的指令,在重載的方法都可以看到。

invoke-direct

表示調用 private 函數的方法,如:

invoke-direct {p0}, Lcom/disney/xx/XxActivity;->getGlobalIapHandler()Lcom/disney/config/GlobalPurchaseHandler;

這里的 GlobalPurchaseHandler getGlobalIapHandler() 表示 getGlobalIapHandler() 是定義在 XxActivity 類中,權限為 private 的方法。

invoke-virtual

表示調用的是 protected 或 public 函數。

sget-object v0, Lcom/disney/xx/XxActivity;->shareHandler:Landroid/os/Handler; invoke-virtual {v0, v3}, Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V

這里的 v0 可以表示為 shareHandler:Landroid/os/Handler,v3 則表示為 removeCallbacksAndMessages 方法的 Ljava/lang/Object; 類型的傳參。

invoke-xxxxx/range

表示當方法參數 >= 5 時,需要在后面加上 /range 。

可能有童鞋會注意到,上面的例子都是在 調用函數 這個操作,貌似沒有取函數返回值的操作?在 smali 代碼中,如果調用的函數返回非 void,那么還需要用到 move-result (返回基本數據類型) 和 move-result-object (返回對象):

const/4 v2, 0x0 invoke-virtual {p0, v2}, Lcom/disney/xx/XxActivity;->getPreferences(I)Landroid/content/SharedPreferences; move-result-object v1

v1 表示調用 this.getPreferences(0) 方法返回的 SharedPreferences 類型的對象。

invoke-virtual {v2}, Ljava/lang/String;->length()I move-result v2

v2 表示 String.length() 返回的 int 基本類型。

七
舉例分析

上面初步對函數變量、方法定義、調用的進行解析,下面通過舉例進一步對 smali 語法進行分析:

.method protected onDestroy()V    .locals 0     .line 79    invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onDestroy()V     .line 80    invoke-virtual {p0}, Lcom/happy/learnsmali/BaseActivity;->removeCallbacks()V     .line 81    return-void.end method

這個是我們熟悉的 onDestroy() 函數。首先我們看到函數內第一句:.locals 0 ,表示在這個函數中用到的本地寄存器的個數,這里因為調用的方法沒有使用到本地本地寄存器,因而本地寄存器的個數為 0。如果我在該方法中添加:this.isExited = true,那么上述方法應該修改為:

.method protected onDestroy()V    .locals 1     .line 79    invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onDestroy()V     .line 80    invoke-virtual {p0}, Lcom/happy/learnsmali/BaseActivity;->removeCallbacks()V     .line 81    const/4 v0, 0x1    iput-boolean v0, p0, Lcom/happy/learnsmali/BaseActivity;->exited:Z     .line 82    return-void.end method

因為修改后的 onDestroy() 函數使用到了一個本地寄存器 v0,所以把 .locals 0 修改為 .locals 1 。另外可能你也會注意到 .line 這個標識符,它表示 smali 這一行代碼在 Java 中對應代碼中所在的位置行號。平常當我們在 Android Studio 上調試程序發生崩潰的時候, logcat 中提示發生崩潰所在的代碼行號也是該值。當然,該標識符不是必須的,但為了方便調試還是建議保留吧。