上一篇學習了 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.smaii
和 BMW.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
類中出現了幾個還沒遇到過的指令,逐一分析一下:
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 方法 |
iput-object v0, p0, LBMW;->brand:Ljava/lang/String;
v0
中儲存的是 BMW
字串,賦給 p0
的 brand 欄位。
通用用法:
iinstanceop vA, vB, field@CCCC
複製程式碼
vA
可以是源暫存器,也可以是目的暫存器。當使用 iput
命令時, vA
就是源暫存器,使用 iget
命令時, vA
就是目的暫存器。
此命令還有一個同樣用法的變種 sput
和 sget
,從名字就可以看出來是對靜態欄位的操作,i
是對例項欄位的操作。
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
儲存的是建構函式的引數,即外部類的引用。這個建構函式一共執行了兩步:
- 把外部類的引用賦值給
this$0
- 執行內部類的
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 題解,歡迎關注!