Smali 語法解析 —— 類

秉心說TM發表於2018-12-13

上一篇學習了 Smali 的數學運算,條件判斷和迴圈,接下來學習類的基本使用,包括介面,抽象類,內部類等等。

直接上程式碼吧,抽象類 Car.java :

public abstract class Car {

    protected String brand;

    abstract void run();
}
複製程式碼

介面 IFly.java

public interface IFly {
    void fly();
}
複製程式碼

BMW.java

public class BMW extends Car implements IFly{

    private String brand = "BMW";

    @Override
    void run() {
        System.out.println(brand + " run!");
    }

    @Override
    public void fly() {
        System.out.println("I can fly!");
    }

    public static void main(String[] args){
        BMW bmw=new BMW();
        bmw.run();
        bmw.fly();
    }
}
複製程式碼

javac 編譯之後使用 dx 生成 dex 檔案,和以往不同的是,這次是三個 class 檔案生成一個 dex,具體命令如下:

dx --dex --output=BMWCar.dex Car.class IFly.class BMW.class
複製程式碼

最後再使用 baksmali 生成 Smali 檔案:

baksmali d BMWCar.dex
複製程式碼

可以看到在當前目錄 out 資料夾下生成了三個檔案 Car.smali IFly.smaiiBMW.smali。下面逐一進行分析。

抽象類

Car.smali :

.class public abstract LCar; // 表明為抽象類
.super Ljava/lang/Object;
.source "Car.java"


# instance fields
.field protected brand:Ljava/lang/String; // proteced String brand


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

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

    return-void
.end method


# virtual methods
.method abstract run()V // abstract void run()
.end method
複製程式碼

抽象類的表示沒有什麼特殊的地方,第一行中會使用 abstract 來宣告。

介面

IFly.smali

.class public interface abstract LIFly; // 表明是介面
.super Ljava/lang/Object;
.source "IFly.java"


# virtual methods
.method public abstract fly()V // abstract void fly()
.end method
複製程式碼

介面使用 interface 宣告。從上面的 smali 程式碼也可以看到介面中的方法預設是 abstract 修飾的。

實現類

BMW.smali :

.class public LBMW;
.super LCar;    // 父類是 Car
.source "BMW.java"

# interfaces
.implements LIFly; // 實現了介面 IFly


# instance fields
.field private brand:Ljava/lang/String; // priva String brand


# direct methods
.method public constructor <init>()V
    .registers 2

    .prologue
    .line 1
    invoke-direct {p0}, LCar;-><init>()V

    .line 3
    const-string v0, "BMW"

    iput-object v0, p0, LBMW;->brand:Ljava/lang/String; // String brand = "BMW";

    return-void
.end method

.method public static main([Ljava/lang/String;)V // main 方法
    .registers 2

    .prologue
    .line 16
    new-instance v0, LBMW; // 新建 BMW 物件,並將其引用存入 v0

    invoke-direct {v0}, LBMW;-><init>()V // 執行物件的建構函式

    .line 17
    invoke-virtual {v0}, LBMW;->run()V // 執行 run() 方法

    .line 18
    invoke-virtual {v0}, LBMW;->fly()V // 執行 fly() 方法

    .line 19
    return-void
.end method


# virtual methods
.method public fly()V  // fly() 方法
    .registers 3

    .prologue
    .line 12
    // 下面三行位元組碼執行了 System.out.println("I can fly!");
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    const-string v1, "I can fly!"

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

    .line 13
    return-void
.end method

.method run()V  // run() 方法
    .registers 4

    .prologue
    .line 7
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; // 獲取 Sysem.out 物件

    new-instance v1, Ljava/lang/StringBuilder; // 新建 StringBuilder 物件,並將其引用存到 v1

    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V // 初始化

    iget-object v2, p0, LBMW;->brand:Ljava/lang/String; // 獲取當前類的 brand 物件

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; // append()

    move-result-object v1 // 將上一步中執行 append() 返回的物件賦給 v1,這裡指的是 StringBuilder 物件

    const-string v2, " run!"

    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; // append()

    move-result-object v1 // 這裡的 v1 儲存的仍然是 StringBuilder 物件的引用

    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; // toString()

    move-result-object v1 // 這裡 v1 儲存的是 StringB.toString() 執行的結果

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

    .line 8
    return-void
.end method
複製程式碼

BMW 類中出現了幾個還沒遇到過的指令,逐一分析一下:

  1. invoke-direct {p0}, LCar;-><init>()V

p0 儲存的是當前類的引用,這句話表示執行當前類中 Car 物件的 init() 方法。

通用表示:

invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB

表示呼叫指定的方法,之後通常會跟一句 move-result* 來獲取方法的返回值。例如:

invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

move-result-object v1
複製程式碼

第一句中執行了 StringBuilder.toString() 方法,返回值是一個 String 物件,緊接著後面的 move-result-object 將返回的 String 物件引用存在了 v1 中。

invoke-virtual 指呼叫正常的虛方法(不是 private,static,final,也不是建構函式)。除此之外,還有一些方法呼叫指令,在下面的表格中列舉出來:

指令 說明
invoke-virtual 呼叫正常的虛方法
invoke-super 呼叫最近超類的虛方法
invoke-direct 呼叫 private 方法或建構函式
invoke-static 呼叫 static 方法
invoke-interface 呼叫 interface 方法
  1. iput-object v0, p0, LBMW;->brand:Ljava/lang/String;

v0 中儲存的是 BMW 字串,賦給 p0 的 brand 欄位。

通用用法:

iinstanceop vA, vB, field@CCCC
複製程式碼

vA 可以是源暫存器,也可以是目的暫存器。當使用 iput 命令時, vA 就是源暫存器,使用 iget 命令時, vA 就是目的暫存器。

此命令還有一個同樣用法的變種 sputsget,從名字就可以看出來是對靜態欄位的操作,i 是對例項欄位的操作。

  1. new-instance v1, Ljava/lang/StringBuilder;

建立一個 StringBuilder 物件,並將其引用存在 v1 暫存器。

通用用法:

new-instance vAA, type@BBBB
複製程式碼

注意這裡的 type 不能是陣列型別。陣列使用的是 new-array 指令:

new-array vA, vB, type@CCCC
複製程式碼

run() 方法的 smali 程式碼中我們也學習到了一個小知識點。我們都知道在 Java 中,使用 = 進行字串拼接是很低效的,run() 方法中執行的是 System.out.println(brand + " run!");,然而虛擬機器並沒有傻傻的使用 = 去拼接,而是自動使用 StringBuilder 去拼接,提高執行效率。

內部類

內部類大家應該都不陌生,在 Android 開發中使用最多的要數匿名內部類了,除此之後,還有靜態內部類,成員內部類等。下面的 Outer.java 中便使用了這三種內部類:

public class Outer {
    
    // 成員內部類
    private class Inner {
        private void in() {
            System.out.println("I am inner class.");
        }
    }

    // 靜態內部類
    private static class StaticInner {
        private void staticIn() {
            System.out.println("I am static inner class.");
        }
    }

    public static void main(String[] args) {

        Outer outer = new Outer();

        Inner inner = outer.new Inner();
        inner.in();

        StaticInner staticInner = new StaticInner();
        staticInner.staticIn();

        // 匿名內部類
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}
複製程式碼

還記得內部類的使用方法嗎?靜態內部類可以直接使用 new 關鍵字進行例項化,如 new StaticInner()。而成員內部類則不行,必須通過外部類來初始化,如 out.new Inner()javac 編譯之後會生成如下四個 class 檔案:

  • OUter.class
  • Outer$Inner.clss
  • Outer$StaticInner.class
  • Outer$1.class

同樣,使用 baksmali 反彙編之後也會生成四個 smali 檔案。

首先來看外部類 Outer.smali

.class public LOuter;
.super Ljava/lang/Object;
.source "Outer.java"


# annotations 系統自動新增的註解,表示內部類列表
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        LOuter$StaticInner;,
        LOuter$Inner;
    }
.end annotation


# direct methods
.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 // main() 方法
    .registers 4

    .prologue
    const/4 v2, 0x0

    .line 17
    new-instance v0, LOuter; // 建立外部類 Outer 物件,存入 v0

    invoke-direct {v0}, LOuter;-><init>()V // 執行 Outer 物件的建構函式

    .line 19
    new-instance v1, LOuter$Inner; // 建立內部類 Outer$Inner 物件,存入 v1

    // 獲取外部類物件的引用
    invoke-virtual {v0}, Ljava/lang/Object;->getClass()Ljava/lang/Class;

    // 執行內部類 Out$Inner 的建構函式,注意這裡執行的不是無參構造,後面具體說明
    invoke-direct {v1, v0, v2}, LOuter$Inner;-><init>(LOuter;LOuter$1;)V

    .line 20
    # invokes: LOuter$Inner;->in()V 
    // 執行 Outer$Inner 的 access$100() 方法,這個方法是自動生成的,
    // 實際執行的就是 in() 方法
    invoke-static {v1}, LOuter$Inner;->access$100(LOuter$Inner;)V

    .line 22
    new-instance v0, LOuter$StaticInner; // 建立靜態內部類 Out$StaticInner 物件

    // 執行靜態內部類的建構函式,這裡也是有參構造
    invoke-direct {v0, v2}, LOuter$StaticInner;-><init>(LOuter$1;)V

    .line 23
    # invokes: LOuter$StaticInner;->staticIn()V
    // 執行 Outer$StaticInner 的 access$300() 方法,這個方法是自動生成的,
    // 實際執行的就是 staticIn() 方法
    invoke-static {v0}, LOuter$StaticInner;->access$300(LOuter$StaticInner;)V

    .line 25
    new-instance v0, Ljava/lang/Thread; // 建立 Thread 物件

    new-instance v1, LOuter$1; // 建立 Out$1 物件

    invoke-direct {v1}, LOuter$1;-><init>()V // 執行 Out$1 物件的建構函式

    invoke-direct {v0, v1}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V // 執行 Thread 物件的建構函式

    .line 30
    invoke-virtual {v0}, Ljava/lang/Thread;->start()V // 執行 Thread.start() 方法

    .line 31
    return-void
.end method
複製程式碼

看完外部類 Outer 的 smali 程式碼,我們發現每個內部類初始化時執行的都是有參構造,但是我們並沒有顯示的宣告任何有參構造。我們從內部類的 smali 程式碼中找找答案。

成員內部類

Out$Inner.smali

.class LOuter$Inner;
.super Ljava/lang/Object;
.source "Outer.java"


# annotations 
# EnclosingClass 註解,系統自動生成,value 值代表其作用範圍
.annotation system Ldalvik/annotation/EnclosingClass;
    value = LOuter;
.end annotation

# InnerClass 註解,系統自動生成,表示內部類
.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x2 // private
    name = "Inner"
.end annotation


# instance fields
# synthetic 表示 this$0 是“合成” 的,並非原始碼中就有的
.field final synthetic this$0:LOuter;


# direct methods
# 有參構造
.method private constructor <init>(LOuter;)V
    .registers 2

    .prologue
    .line 3
    iput-object p1, p0, LOuter$Inner;->this$0:LOuter;

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

    return-void
.end method

# 有參構造
.method synthetic constructor <init>(LOuter;LOuter$1;)V
    .registers 3

    .prologue
    .line 3
    invoke-direct {p0, p1}, LOuter$Inner;-><init>(LOuter;)V

    return-void
.end method

# 編譯器生成的 access$100() 方法
.method static synthetic access$100(LOuter$Inner;)V
    .registers 1

    .prologue
    .line 3
    invoke-direct {p0}, LOuter$Inner;->in()V // 呼叫 in() 方法

    return-void
.end method

// in() 方法
.method private in()V
    .registers 3

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

    const-string v1, "I am inner class."

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

    .line 6
    return-void
.end method
複製程式碼

重點看一下成員內部類的有參建構函式:

.method private constructor <init>(LOuter;)V
    .registers 2

    .prologue
    .line 3
    iput-object p1, p0, LOuter$Inner;->this$0:LOuter;

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

    return-void
.end method
複製程式碼

有參建構函式的引數是 LOuter;p0 儲存的是當前類的引用,p1 儲存的是建構函式的引數,即外部類的引用。這個建構函式一共執行了兩步:

  1. 把外部類的引用賦值給 this$0
  2. 執行內部類的 init() 方法

由此可以,成員內部類是持有外部類的引用的,這也就解釋了為什麼成員內部類可以呼叫外部類的屬性和方法。

再回頭看一下 Outer.smali 中成員內部類 Outer$Inner 的初始化過程:

const/4 v2, 0x0
invoke-direct {v1, v0, v2}, LOuter$Inner;-><init>(LOuter;LOuter$1;)V
複製程式碼

發現呼叫的並不是上面那個建構函式,而是一個兩個引數的建構函式:

.method synthetic constructor <init>(LOuter;LOuter$1;)V
    .registers 3

    .prologue
    .line 3
    invoke-direct {p0, p1}, LOuter$Inner;-><init>(LOuter;)V

    return-void
.end method
複製程式碼

這個建構函式中還是呼叫了 init(LOuter;) 函式,並且也沒有使用 LOuter$1 引數。這個 LOuter$1 引數是什麼呢?看到最後就知道,這是匿名內部類的引用。但是在實際呼叫中傳入的是 0x0, 關於傳遞這個引數的意義,不知道有沒有讀者知道,可以討論一下。

緊接著編譯器為成員內部類自動生成了一個靜態方法:

.method static synthetic access$100(LOuter$Inner;)V
    .registers 1

    .prologue
    .line 3
    invoke-direct {p0}, LOuter$Inner;->in()V // 呼叫 in() 方法

    return-void
.end method
複製程式碼

方法引數是 p0 儲存的當前類的引用,然後通過 p0 直接呼叫 in() 方法。同樣,在外部類中呼叫成員內部類的 in() 方法,是通過 invoke-static 呼叫這個靜態方法來執行的,如下所示:

# invokes: LOuter$Inner;->in()V
invoke-static {v1}, LOuter$Inner;->access$100(LOuter$Inner;)V
複製程式碼

靜態內部類

Outer$StaticInner.smali

.class LOuter$StaticInner;
.super Ljava/lang/Object;
.source "Outer.java"


# annotations
# EnclosingClass 註解,系統自動生成,value 值代表其作用範圍
.annotation system Ldalvik/annotation/EnclosingClass;
    value = LOuter;
.end annotation

# InnerClass 註解,系統自動生成,表示內部類
.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0xa # private static
    name = "StaticInner"
.end annotation


# direct methods
# 無參構造
.method private constructor <init>()V
    .registers 1

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

    return-void
.end method

# 有參構造
.method synthetic constructor <init>(LOuter$1;)V
    .registers 2

    .prologue
    .line 9
    invoke-direct {p0}, LOuter$StaticInner;-><init>()V

    return-void
.end method

# 編譯器生成的 static 方法
.method static synthetic access$300(LOuter$StaticInner;)V
    .registers 1

    .prologue
    .line 9
    invoke-direct {p0}, LOuter$StaticInner;->staticIn()V

    return-void
.end method

// staticIn() 方法
.method private staticIn()V
    .registers 3

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

    const-string v1, "I am static inner class."

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

    .line 12
    return-void
.end method
複製程式碼

結構與成員內部類基本相似,有兩點區別。

第一, accessFlags 的不同。在系統生成的 InnerClass 註解中有 accessFlags 值。成員內部類 Outer$Inner 值為 0x2,表示 private,靜態內部類 Outer$StaticInner 值為 0xa,表示 private static。關於 accessFlags 的表示方式,在我的另一篇文章 Class 檔案格式詳解 中有具體介紹。

第二,靜態內部類不持有外部類的引用。Outer$StaticInner.smali 中並沒有定義 this$0 欄位,有參構造中的引數也不包含外部類引用。所以,這也驗證了靜態內部類只能呼叫外部類的靜態屬性和靜態方法。

外部類對靜態內部類方法的呼叫也是自動生成了一個靜態方法,再通過這個靜態方法來呼叫,如下所示:

# invokes: LOuter$StaticInner;->staticIn()V
invoke-static {v0}, LOuter$StaticInner;->access$300(LOuter$StaticInner;)V
複製程式碼

匿名內部類

Outer$1.smali

.class final LOuter$1; // 匿名內部類是 final 的
.super Ljava/lang/Object;
.source "Outer.java"

# interfaces
.implements Ljava/lang/Runnable; // 實現了 Runnable 介面


# annotations
# 作用域在 main() 方法中
.annotation system Ldalvik/annotation/EnclosingMethod;
    value = LOuter;->main([Ljava/lang/String;)V
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x8
    name = null
.end annotation


# direct methods
# 無參構造
.method constructor <init>()V
    .registers 1

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

    return-void
.end method


# virtual methods
# run() 方法
.method public run()V
    .registers 3

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

    invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String;

    move-result-object v1

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

    .line 29
    return-void
.end method
複製程式碼

看起來並沒有什麼特別的地方,run() 方法中執行的 System.out.println(Thread.currentThread().getName()); 也很容易看懂。只有一個無參構造。對,只有一個無參構造,如果你的 Java 基礎學的還可以的話,應該記得匿名內部類會持有外部類的引用,可以這裡為什麼只有一個無參構造呢?別忘了,這裡是 main() 方法,是 static 方法。靜態方法中只能引用類中的靜態屬性。如果換成一個普通方法,生成的 smali 程式碼中肯定會有 this$0 欄位和有參構造。在 Outer.java 中新增如下程式碼:

    public void test(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
複製程式碼

生成的 smali 程式碼中多了一個檔案 Outer$2.smali

.class LOuter$2;
.super Ljava/lang/Object;
.source "Outer.java"

# interfaces
.implements Ljava/lang/Runnable;


# annotations
.annotation system Ldalvik/annotation/EnclosingMethod;
    value = LOuter;->test()V
.end annotation

.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = null
.end annotation


# instance fields
.field final synthetic this$0:LOuter; // 外部類引用


# direct methods
# 有參構造
.method constructor <init>(LOuter;)V
    .registers 2

    .prologue
    .line 34
    iput-object p1, p0, LOuter$2;->this$0:LOuter;

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

    return-void
.end method


# virtual methods
.method public run()V
    .registers 3

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

    invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread;

    move-result-object v1

    invoke-virtual {v1}, Ljava/lang/Thread;->getName()Ljava/lang/String;

    move-result-object v1

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

    .line 38
    return-void
.end method
複製程式碼

注意看兩處新增註釋的地方,這就驗證了匿名內部類持有外部類的引用的說法。

關於 Smali 的學習就到這裡了,在這個過程中,也驗證了一些我們曾經熟知的知識點,加深了我們對 Java 的理解。當然在 Android 逆向過程中,我們碰到的要比這裡說的複雜的多,這就需要我們積累足夠的經驗了。下一篇,正式進入 Android 領域了,介紹一些常用的 Android 逆向工具。

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

Smali 語法解析 —— 類

相關文章