Smali 語法解析——Hello World

經過上一篇文章的學習,咱們都知道了 Java 代碼是如何執行的。Java 編譯器將 .java 源文件編譯爲 .class 字節碼文件,JVM(Java虛擬機)將字節碼解釋爲機器代碼最終在目標機器上執行。而在 Android 中,代碼是如何執行的呢 ?首先看下面這張圖:java

JVM VS DVM

這裏的 DVM 指的是 DalviK VM 。在 Android 中,Java 類被打包生成固定格式的 DEX 字節碼文件,DEX 字節碼通過 Dalvik 或者 ART 轉換爲原生機器碼,進而執行。DEX 字節碼是獨立於設備架構的。android

Dalvik 是一個基於 JIT(即時)的編譯引擎。使用 Dalvik 是有缺點的,所以從 Android4.4(kitkat)開始引入了 ART 做爲運行時,從 Android5.0(Lollopop)開始就徹底替代了 Dalvik。Android7.0 增長了一個即時型編譯器,給 Android 運行時(ART)提供了代碼分析,提高了 Android app運行時的表現。關於 Dalvik 和 Art 的具體分析,能夠閱讀我以前的一篇譯文 走近 Android 運行時: DVM VS ARTgit

上圖中還能夠看到,JVM 的執行是 Stack-based , 基於棧幀的,而 Dalvik 虛擬機是 Register-based,基於寄存器的。這點須要記住,在後面的 Smali 語法分析中很重要。說到 Smali,那麼什麼是 Smali呢?用過 apktool 的朋友確定都不陌生,apktool d xxx.apk 反編譯 apk 以後,生成的文件夾之中會有 smali 文件夾,裏面就包含了該 apk 的全部代碼,均以 .smali 文件形式保存。關於 Smali ,在 Android 官網中並沒有相關介紹,它應該出自 JesusFreke 的開源項目 smali,在 README 中是這樣介紹的:github

smali/baksmali is an assembler/disassembler for the dex format used by dalvik, Android's Java VM implementation. The syntax is loosely based on Jasmin's/dedexer's syntax, and supports the full functionality of the dex format (annotations, debug info, line info, etc.) 複製代碼

大體翻譯一下, smali/baksmali 是針對 dalvik 使用的 dex 格式的彙編/反彙編器。它的語法基於 Jasmin's/dedexer,支持 dex 格式的全部功能(註釋,調試信息,行信息等等)。所以咱們能夠認爲 smali 和 Dalvik 字節碼文件是等價的。事實上,Apktool 也正是調用這個工程生成的 jar 包來進行反編譯生成 smali 代碼的。對生成的 smali 代碼進行修改以後再重打包,就能夠修改 apk 中的邏輯了。所以,能閱讀 smali 代碼對咱們進行 android 逆向十分重要。數組

Smali 文件生成

下面仍然以以前的 Hello.java 爲例:sass

public class Hello {

    private static String HELLO_WORLD = "Hello World!";

    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
複製代碼

javac 生成 Hello.class 文件,而後經過 Sdk 自帶的 dx 工具生成 Hello.dex 文件,命令以下:bash

dx --dex --output=Hello.dex  Hello.class
複製代碼

dx 工具位於 Sdk 的 build-tools 目錄下,可添加至環境變量方便調用。dx 也支持多 Class 文件生成 dex。微信

dexsmali 使用的工具是 baksmali.jar ,最新版本是 2.2.5點擊下載,使用命令以下:架構

java -jar baksmali-2.2.5.jar d hello.dex
複製代碼

執行完成後,會在當前目錄生成 out 文件夾,文件夾內包含生成的 smali 文件。app

Smali 詳細解析

咱們首先看一下生成的 Hello.smali 文件內容:

.class public LHello;
.super Ljava/lang/Object;
.source "Hello.java"


# static fields
.field private static HELLO_WORLD:Ljava/lang/String;


# direct methods
.method static constructor <clinit>()V
    .registers 1

    .prologue
    .line 3
    const-string v0, "Hello World!"

    sput-object v0, LHello;->HELLO_WORLD:Ljava/lang/String;

    return-void
.end method

.method public constructor <init>()V
    .registers 1

    .prologue
    .line 1
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

.method public static main([Ljava/lang/String;)V
    .registers 3

    .prologue
    .line 6
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

    .line 7
    return-void
.end method
複製代碼

文件頭

首先看一下文件頭部分:

.class public LHello;       // 類名
.super Ljava/lang/Object;   // 父類名
.source "Hello.java"        // 源文件名稱
複製代碼

.class 後面是 訪問修飾符和當前類,這裏類名用 LHello 表示。那麼這個 L 表明什麼呢?其實以前的 Class 文件中也出現過這種表示方法,JVM 的字節碼指令和 Dalvik 的字節碼指令有不少地方都是相似的。Java 中分爲基本類型和引用類型,DalviK 對這兩種類型分別有不一樣的描述方法。對於基本類型和 Void 類型,都是用一個大寫字母表示。對於引用類型,使用字母 L 加上對象類型的全限定名來表示。具體規則以下表所示:

Java 類型 類型描述符
char C
byte B
short S
int I
long J
float F
double D
boolean Z
void V
對象 L
數組 [

基本類型的表示很簡單,int 用 I 表示便可。對象的表示,如上圖中父類 Object 的表示方法 Ljava/lang/Object;,再好比 String 類型,就用 Ljava/lang/String 表示。

對於數組,DalviK 有特殊的表示方法 [ 後面跟上數組元素的類型。int[] 的表示方式就是 [I, String[] 的表示方法是 [Ljava/lang/String;。二維數組用 [[ 表示,[[Ljava/lang/String 就是指 String[][],以此類推。

字段表示

# static fields
.field private static HELLO_WORLD:Ljava/lang/String;
複製代碼

smali 中的字段以 .field 開頭,並有 # static field(靜態字段) 或者 # instance field(實例字段) 的註釋。.field 以後分別是 訪問修飾符,字段名稱,冒號以及字段類型描述符。這句 smali 就聲明瞭一個 String 類型名稱爲 HELLO_WORLD 的私有靜態字段。

方法表示

smali 中的方法以 .method 開頭。Hello.smali 中包含了三個方法,clinit , initmain 方法。main 方法是咱們本身編寫的,而 clinitinit 方法則是 javac 編譯時生成的。下面進行逐一分析:

clinit

.method static constructor <clinit>()V
    .registers 1

    .prologue
    .line 3
    const-string v0, "Hello World!"

    sput-object v0, LHello;->HELLO_WORLD:Ljava/lang/String;

    return-void
.end method
複製代碼

clinit 方法會進行靜態變量的初始化,靜態代碼塊的執行等操做,該方法在類被加載的時候調用。逐行分析該方法的執行邏輯:

  • .registers 1 : 該方法須要使用的寄存器數量。以前已經提到,DalviK VM 是基於寄存器的,字節碼可使用的虛擬寄存器個數可達 65536 個,每一個寄存器 32 位,64 位的數據使用相鄰兩個寄存器表示。最終,全部的虛擬寄存器都會被映射到真實的物理寄存器上。通常狀況下,咱們使用字母 v 表示局部變量使用的寄存器,使用字母 p 表示參數所使用的寄存器,且局部變量使用的寄存器排列在前,參數使用的寄存器排列在後。這裏就表示 clinit 方法僅使用了一個寄存器。

  • .prologue : 表示邏輯代碼的開始處

  • .line 3 : 表示 java 源文件中的行數

  • const-string v0, "Hello World!" : 將字符串 Hello World! 的引用移到寄存器 v0 中。

  • sput-object v0, LHello;->HELLO_WORLD:Ljava/lang/String; : 前綴 ssputsget 指令用於靜態字段的讀寫操做。將寄存器 v0 存儲的字符串引用賦值給 HELLO_WORLD 字段,結合上一句字節碼,這裏完成了靜態變量 HELLO_WORLD 的賦值工做,也驗證了 clinit 方法的確進行了靜態變量的初始化。

  • return-void : 表示該方法無返回值

  • .end method : 表示方法執行結束

到這裏,clinit 方法就執行結束了。下面分析 init 方法。

init

.method public constructor <init>()V
    .registers 1

    .prologue
    .line 1
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method
複製代碼

其他各項與 clinit 方法相同,咱們直接看執行的代碼邏輯:

invoke-direct {p0}, Ljava/lang/Object;-><init>()V
複製代碼

invoke-direct 用於調用非 static 直接方法(也就是說,本質上不可覆蓋的實例方法,即 private 實例方法或構造函數)。顯然,這裏調用的是默認構造函數。

main

.method public static main([Ljava/lang/String;)V .registers 3 .prologue .line 6 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

    .line 7
    return-void
.end method
複製代碼

最後是 main 方法,從上述 smali 代碼咱們能夠看到 main 方法使用了 3 個寄存器,無返回值(那是確定的),執行的具體代碼是下面三行:

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;

invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
複製代碼

sget 的用法在 clinit 方法中解釋過,表示靜態字段的讀取。第一句代碼,獲取類 System 的靜態字段 out,其類型是 Ljava/io/PrintStream,並將其引用賦給寄存器 v0。第二句代碼獲取在 clinit 方法中已經初始化的靜態字段 HELLO_WORLD,並將其引用賦給寄存器 v1。第三句中使用了 invoke-virtual 指令,invoke-virtual 調用正常的虛方法(該方法不是 private、static 或 final,也不是構造函數),以後一般會跟上 {}{}之中的第一個寄存器一般是指向當前實例對象,如 v0 就是指向 System.out 對象,後面的內容纔是該方法真正的參數,如 v1{}, 以後就是要執行的方法的描述,如 Ljava/io/PrintStream;->println(Ljava/lang/String;)V ,指的就是 PrintStream 對象的 println 方法。綜上,這三句字節碼執行的就是 System.out.println(HELLO_WORLD);

到這裏,Hello.smali 文件就解析完了。固然,咱們在反編譯過程當中遇到的任何一個 smali 文件確定都要比這個複雜的多。Android 官網也對 Dalvik 字節碼的指令集進行了概括,地址是 source.android.google.cn/devices/tec…。在閱讀過程當中遇到不熟悉的指令,均可以在這個頁面進行查找。

最後再介紹一個 javasmali 的快捷方式,在 IDEA 或者 Android Studo 中安裝插件 java2smali,在 Build 菜單欄下會出現 Compile to smali選項,能夠迅速將 java 代碼轉化成 smali 代碼。在咱們學習 smali 的過程當中,碰到不肯定的內容,能夠先寫好 java 代碼,再轉成 smali 代碼進行對照學習。

最後貼一個完整的帶註釋的 Hello.smali 文件:

.class public LHello; 		// 類名
.super Ljava/lang/Object;   // 父類名
.source "Hello.java"		// 源文件名稱


# static fields // 表示靜態字段 private static String HELLO_WORLD
.field private static HELLO_WORLD:Ljava/lang/String;


# direct methods
.method static constructor <clinit>()V // clinit 方法
    .registers 1 // 使用一個寄存器 v0

    .prologue // 方法開始
    .line 3 // 源代碼行數
    const-string v0, "Hello World!" // 將 "Hello World!"放入寄存器 v0

    // 靜態字段賦值,將寄存器v0存儲的值賦給 HELLO_WORLD 
    sput-object v0, LHello;->HELLO_WORLD:Ljava/lang/String; 

    return-void // 無返回值
.end method // 方法結束

.method public constructor <init>()V // init 方法
    .registers 1 // 使用一個寄存器

    .prologue // 方法開始
    .line 1
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V // 調用構造方法

    return-void // 無返回值
.end method // 方法結束

.method public static main([Ljava/lang/String;)V // main 方法
    .registers 3 // 使用 3 個寄存器

    .prologue // 方法開始
    .line 6
    // 獲取靜態對象,System.out,其類型爲 java.io.PrintStream,賦給 v0
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    // 獲取靜態對象, HELLO_WORLD,其類型爲 java.lang.String,賦給 v1
    sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
    // 執行 v0 所存儲的對象的 println() 方法,v1存儲的是方法的參數
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

    .line 7
    return-void // 無返回值
.end method // 方法結束

複製代碼

下一篇 簡單學習一些常見語法的 smali 表示,好比數學運算,if-else,循環等等。

文章同步更新於微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索