Smali 語法解析 —— 類

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

直接上代碼吧,抽象類 Car.java :數組

public abstract class Car {

    protected String brand;

    abstract void run();
}
複製代碼

接口 IFly.javabash

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,具體命令以下:app

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

最後再使用 baksmali 生成 Smali 文件:ide

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 來聲明。post

接口

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 題解,歡迎關注!

相關文章
相關標籤/搜索