經過上一篇文章的學習,咱們都知道了 Java 代碼是如何執行的。Java 編譯器將 .java
源文件編譯爲 .class
字節碼文件,JVM
(Java虛擬機)將字節碼解釋爲機器代碼最終在目標機器上執行。而在 Android 中,代碼是如何執行的呢 ?首先看下面這張圖:java
這裏的 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 ART 。git
上圖中還能夠看到,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 逆向十分重要。數組
下面仍然以以前的 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。微信
dex
轉 smali
使用的工具是 baksmali.jar
,最新版本是 2.2.5
,點擊下載,使用命令以下:架構
java -jar baksmali-2.2.5.jar d hello.dex
複製代碼
執行完成後,會在當前目錄生成 out
文件夾,文件夾內包含生成的 smali
文件。app
咱們首先看一下生成的 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
, init
和 main
方法。main
方法是咱們本身編寫的,而 clinit
和 init
方法則是 javac 編譯時生成的。下面進行逐一分析:
.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;
: 前綴 s
的 sput
和 sget
指令用於靜態字段的讀寫操做。將寄存器 v0
存儲的字符串引用賦值給 HELLO_WORLD
字段,結合上一句字節碼,這裏完成了靜態變量 HELLO_WORLD
的賦值工做,也驗證了 clinit
方法的確進行了靜態變量的初始化。
return-void
: 表示該方法無返回值
.end method
: 表示方法執行結束
到這裏,clinit
方法就執行結束了。下面分析 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 實例方法或構造函數)。顯然,這裏調用的是默認構造函數。
.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…。在閱讀過程當中遇到不熟悉的指令,均可以在這個頁面進行查找。
最後再介紹一個 java
轉 smali
的快捷方式,在 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 題解,歡迎關注!