Java 字節(jié)碼與Smali 語法基礎(chǔ)實戰(zhàn)案例
3.1 Java 字節(jié)碼與 DEX 字節(jié)碼
3.1.1 Java 字節(jié)碼(.class)簡介
Java 字節(jié)碼是 Java 源代碼編譯后的中間表示形式,運行在 Java 虛擬機(JVM)上。
編譯流程:
Java 源碼 (.java)
↓ javac
Java 字節(jié)碼 (.class)
↓ JVM
機器碼執(zhí)行
Java 字節(jié)碼特點:
- 平臺無關(guān)的中間代碼
- 基于棧的虛擬機(Stack-based VM)
- 指令集相對簡單
- 面向?qū)ο笤O(shè)計
3.1.2 DEX 字節(jié)碼(.dex)簡介
DEX(Dalvik Executable)是 Android 平臺上的字節(jié)碼格式,針對移動設(shè)備進行了優(yōu)化。
轉(zhuǎn)換流程:
Java 源碼 (.java)
↓ javac
Java 字節(jié)碼 (.class)
↓ dx/d8
DEX 字節(jié)碼 (.dex)
↓ Dalvik/ART VM
機器碼執(zhí)行
DEX 字節(jié)碼特點:
- 多個 .class 文件合并為一個 .dex
- 基于寄存器的虛擬機(Register-based VM)
- 指令集更緊湊
- 優(yōu)化了內(nèi)存使用
3.1.3 Dalvik 虛擬機與 JVM 的區(qū)別
| 特性 | JVM | Dalvik VM |
|---|---|---|
| 字節(jié)碼格式 | .class | .dex |
| 虛擬機類型 | 棧式虛擬機 | 寄存器虛擬機 |
| 文件組織 | 每個類一個文件 | 多個類合并到一個文件 |
| 指令集 | 基于棧操作 | 基于寄存器操作 |
| 內(nèi)存占用 | 較大 | 較?。▋?yōu)化后) |
| 執(zhí)行方式 | 解釋執(zhí)行/JIT | 解釋執(zhí)行/JIT/AOT |
3.1.4 指令集映射關(guān)系
Java 字節(jié)碼示例:
// Java 源碼 int a = 10; int b = 20; int c = a + b;
對應(yīng)的 Java 字節(jié)碼(javap -c):
0: bipush 10 // 將 10 壓入棧 2: istore_1 // 彈出棧頂值,存儲到局部變量 1 (a) 3: bipush 20 // 將 20 壓入棧 5: istore_2 // 彈出棧頂值,存儲到局部變量 2 (b) 6: iload_1 // 加載局部變量 1 (a) 到棧 7: iload_2 // 加載局部變量 2 (b) 到棧 8: iadd // 彈出兩個值,相加,結(jié)果壓入棧 9: istore_3 // 彈出棧頂值,存儲到局部變量 3 (c)
對應(yīng)的 Smali 代碼:
const/16 v0, 0xa # 將 10 加載到寄存器 v0 const/16 v1, 0x14 # 將 20 加載到寄存器 v1 add-int v2, v0, v1 # v2 = v0 + v1
關(guān)鍵區(qū)別:
- JVM:使用棧,需要 push/pop 操作
- Dalvik:使用寄存器,直接操作寄存器
3.2 Smali 語法基礎(chǔ)
3.2.1 寄存器系統(tǒng)
Smali 使用寄存器來存儲局部變量和方法參數(shù),這是與 Java 字節(jié)碼最大的區(qū)別。
寄存器命名規(guī)則
局部變量寄存器:
v0,v1,v2, …vN:局部變量寄存器- 從 0 開始編號
- 通常用于存儲方法內(nèi)的局部變量
參數(shù)寄存器:
p0,p1,p2, …pN:參數(shù)寄存器p0在非靜態(tài)方法中通常是this引用p1,p2, … 是方法的實際參數(shù)
寄存器數(shù)量限制:
- 單個方法最多可以使用 65536 個寄存器(理論值)
- 實際使用中,寄存器數(shù)量受方法復(fù)雜度限制
- 寄存器數(shù)量過多會導(dǎo)致編譯失敗
寄存器使用示例
Java 代碼:
public int add(int a, int b) {
int result = a + b;
return result;
}
對應(yīng)的 Smali 代碼:
.method public add(II)I
.registers 4 # 使用 4 個寄存器
# 寄存器分配:
# p0 = this (非靜態(tài)方法的 this 引用)
# p1 = a (第一個參數(shù))
# p2 = b (第二個參數(shù))
# v0 = result (局部變量)
.prologue
.line 1
add-int v0, p1, p2 # v0 = p1 + p2
return v0 # 返回 v0
.end method?? 重要提示:
- 參數(shù)寄存器從
p0開始,不是p1 - 在非靜態(tài)方法中,
p0是this,實際參數(shù)從p1開始 - 在靜態(tài)方法中,參數(shù)從
p0開始
3.2.2 數(shù)據(jù)類型
基本類型
| Java 類型 | Smali 類型 | 大小 | 說明 |
|---|---|---|---|
boolean | Z | 1 字節(jié) | 布爾值 |
byte | B | 1 字節(jié) | 字節(jié) |
short | S | 2 字節(jié) | 短整型 |
char | C | 2 字節(jié) | 字符 |
int | I | 4 字節(jié) | 整型 |
long | J | 8 字節(jié) | 長整型 |
float | F | 4 字節(jié) | 單精度浮點 |
double | D | 8 字節(jié) | 雙精度浮點 |
void | V | - | 無返回值 |
引用類型
類類型:
Ljava/lang/String; # String 類 Landroid/app/Activity; # Activity 類
數(shù)組類型:
[I # int[] [[I # int[][] [Ljava/lang/String; # String[]
完整類型示例:
// Java 代碼 String[] names; int[][] matrix;
# Smali 代碼 .field names:[Ljava/lang/String; .field matrix:[[I
3.2.3 方法調(diào)用約定
Smali 中有多種方法調(diào)用指令,用于不同的調(diào)用場景。
invoke-virtual(虛方法調(diào)用)
用途: 調(diào)用實例方法,支持多態(tài)
語法:
invoke-virtual {參數(shù)寄存器}, 類名;->方法名(參數(shù)類型)返回類型
示例:
// Java 代碼 String str = "Hello"; int len = str.length();
# Smali 代碼
const-string v0, "Hello"
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1 # 將返回值存儲到 v1
invoke-static(靜態(tài)方法調(diào)用)
用途: 調(diào)用靜態(tài)方法
語法:
invoke-static {參數(shù)寄存器}, 類名;->方法名(參數(shù)類型)返回類型
示例:
// Java 代碼 int max = Math.max(10, 20);
# Smali 代碼
const/16 v0, 0xa # 10
const/16 v1, 0x14 # 20
invoke-static {v0, v1}, Ljava/lang/Math;->max(II)I
move-result v2 # max 結(jié)果存儲到 v2
invoke-direct(直接方法調(diào)用)
用途: 調(diào)用構(gòu)造函數(shù)、私有方法、final 方法
語法:
invoke-direct {參數(shù)寄存器}, 類名;->方法名(參數(shù)類型)返回類型
示例:
// Java 代碼
String str = new String("Hello");
# Smali 代碼
new-instance v0, Ljava/lang/String;
const-string v1, "Hello"
invoke-direct {v0, v1}, Ljava/lang/String;-><init>(Ljava/lang/String;)V
invoke-interface(接口方法調(diào)用)
用途: 調(diào)用接口方法
語法:
invoke-interface {參數(shù)寄存器}, 接口名;->方法名(參數(shù)類型)返回類型
invoke-super(父類方法調(diào)用)
用途: 調(diào)用父類方法
語法:
invoke-super {參數(shù)寄存器}, 父類名;->方法名(參數(shù)類型)返回類型
調(diào)用指令對比表:
| 指令 | 用途 | 多態(tài)支持 | 性能 |
|---|---|---|---|
invoke-virtual | 實例方法 | ? | 較慢 |
invoke-static | 靜態(tài)方法 | ? | 快 |
invoke-direct | 構(gòu)造函數(shù)/私有方法 | ? | 快 |
invoke-interface | 接口方法 | ? | 最慢 |
invoke-super | 父類方法 | ? | 快 |
3.2.4 條件跳轉(zhuǎn)指令
條件跳轉(zhuǎn)指令用于實現(xiàn) if-else、循環(huán)等控制流。
基本條件跳轉(zhuǎn)
相等比較:
if-eq vA, vB, :label # if (vA == vB) goto label if-ne vA, vB, :label # if (vA != vB) goto label
大小比較:
if-lt vA, vB, :label # if (vA < vB) goto label if-le vA, vB, :label # if (vA <= vB) goto label if-gt vA, vB, :label # if (vA > vB) goto label if-ge vA, vB, :label # if (vA >= vB) goto label
零值比較:
if-eqz vA, :label # if (vA == 0) goto label if-nez vA, :label # if (vA != 0) goto label if-ltz vA, :label # if (vA < 0) goto label if-lez vA, :label # if (vA <= 0) goto label if-gtz vA, :label # if (vA > 0) goto label if-gez vA, :label # if (vA >= 0) goto label
示例:
// Java 代碼
if (a == b) {
return true;
} else {
return false;
}
# Smali 代碼 if-eq p1, p2, :equal # if (a == b) goto equal const/4 v0, 0x0 # v0 = false return v0 # return false :equal const/4 v0, 0x1 # v0 = true return v0 # return true
3.3 Smali 指令集詳解
3.3.1 invoke-* 系列(方法調(diào)用)
invoke-virtual
完整示例:
// Java 代碼
public void test() {
String str = "Hello";
int len = str.length();
System.out.println(len);
}
.method public test()V
.registers 3
.prologue
const-string v0, "Hello" # v0 = "Hello"
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1 # v1 = str.length()
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(I)V
return-void
.end methodinvoke-static
// Java 代碼 int result = Math.max(10, 20);
const/16 v0, 0xa # v0 = 10
const/16 v1, 0x14 # v1 = 20
invoke-static {v0, v1}, Ljava/lang/Math;->max(II)I
move-result v2 # v2 = Math.max(10, 20)
3.3.2 if-* 系列(條件判斷)
完整 if-else 示例
// Java 代碼
public int compare(int a, int b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
.method public compare(II)I
.registers 3
.param p1, "a" # I
.param p2, "b" # I
.prologue
if-gt p1, p2, :check_less # if (a > b) goto check_less
const/4 v0, 0x1 # v0 = 1
return v0 # return 1
:check_less
if-lt p1, p2, :equal # if (a < b) goto equal
const/4 v0, -0x1 # v0 = -1
return v0 # return -1
:equal
const/4 v0, 0x0 # v0 = 0
return v0 # return 0
.end method3.3.3 move-* 系列(數(shù)據(jù)移動)
常用 move 指令:
move vA, vB # vA = vB (32位) move-wide vA, vB # vA = vB (64位,用于 long/double) move-object vA, vB # vA = vB (對象引用) move-result vA # vA = 方法返回值 (32位) move-result-wide vA # vA = 方法返回值 (64位) move-result-object vA # vA = 方法返回值 (對象) move-exception vA # vA = 異常對象
示例:
// Java 代碼 int a = 10; int b = a; int c = getValue();
const/16 v0, 0xa # v0 = 10 (a)
move v1, v0 # v1 = v0 (b = a)
invoke-static {}, Lcom/example/Test;->getValue()I
move-result v2 # v2 = getValue() (c)
3.3.4 const-* 系列(常量加載)
常用 const 指令:
const/4 vA, #+B # vA = 符號擴展的 4 位立即數(shù) (-8 到 7) const/16 vA, #+B # vA = 符號擴展的 16 位立即數(shù) (-32768 到 32767) const vA, #+B # vA = 32 位立即數(shù) const-wide/16 vA, #+B # vA = 符號擴展的 16 位立即數(shù) (long) const-wide/32 vA, #+B # vA = 符號擴展的 32 位立即數(shù) (long) const-wide vA, #+B # vA = 64 位立即數(shù) (long) const/high16 vA, #+B # vA = 0xBBBB0000 (高16位) const-string vA, string # vA = 字符串引用 const-class vA, type # vA = 類對象引用
示例:
// Java 代碼 int a = 10; int b = 1000; long c = 100000L; String str = "Hello";
const/16 v0, 0xa # v0 = 10 const/16 v1, 0x3e8 # v1 = 1000 const-wide/32 v2, 0x186a0 # v2 = 100000L const-string v4, "Hello" # v4 = "Hello"
3.3.5 其他常用指令
算術(shù)運算
add-int vA, vB, vC # vA = vB + vC sub-int vA, vB, vC # vA = vB - vC mul-int vA, vB, vC # vA = vB * vC div-int vA, vB, vC # vA = vB / vC rem-int vA, vB, vC # vA = vB % vC
邏輯運算
and-int vA, vB, vC # vA = vB & vC or-int vA, vB, vC # vA = vB | vC xor-int vA, vB, vC # vA = vB ^ vC shl-int vA, vB, vC # vA = vB << vC shr-int vA, vB, vC # vA = vB >> vC ushr-int vA, vB, vC # vA = vB >>> vC
數(shù)組操作
new-array vA, vB, type # vA = new type[vB] array-length vA, vB # vA = vB.length aget vA, vB, vC # vA = vB[vC] aput vA, vB, vC # vB[vC] = vA
字段操作
iget vA, vB, field # vA = vB.field iput vA, vB, field # vB.field = vA sget vA, field # vA = static_field sput vA, field # static_field = vA
3.4 Smali 中的類、方法和字段
3.4.1 類定義
Java 代碼:
package com.example;
public class MyClass {
// ...
}
Smali 代碼:
.class public Lcom/example/MyClass; .super Ljava/lang/Object; .source "MyClass.java"
類修飾符:
.class public final Lcom/example/MyClass; # public final class .class public abstract Lcom/example/MyClass; # public abstract class .class public Lcom/example/MyClass; # public class
3.4.2 方法定義
Java 代碼:
public int add(int a, int b) {
return a + b;
}
Smali 代碼:
.method public add(II)I
.registers 4
.param p1, "a" # I
.param p2, "b" # I
.prologue
.line 1
add-int v0, p1, p2
return v0
.end method方法修飾符:
| Java 修飾符 | Smali 表示 |
|---|---|
public | .method public |
private | .method private |
protected | .method protected |
static | .method public static |
final | .method public final |
synchronized | .method public synchronized |
3.4.3 字段定義
Java 代碼:
public class MyClass {
private int value;
public static String name;
}
Smali 代碼:
.class public Lcom/example/MyClass; .super Ljava/lang/Object; # 實例字段 .field private value:I # 靜態(tài)字段 .field public static name:Ljava/lang/String;
字段修飾符:
| Java 修飾符 | Smali 表示 |
|---|---|
public | .field public |
private | .field private |
protected | .field protected |
static | .field public static |
final | .field public final |
3.4.4 異常處理
Java 代碼:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
e.printStackTrace();
}
Smali 代碼:
.method public test()V
.registers 3
.prologue
.line 1
:try_start_0
const/16 v0, 0xa
const/4 v1, 0x0
div-int v0, v0, v1
:try_end_0
.catch Ljava/lang/ArithmeticException; {:try_start_0 .. :try_end_0} :catch_0
return-void
:catch_0
move-exception v0
invoke-virtual {v0}, Ljava/lang/ArithmeticException;->printStackTrace()V
return-void
.end method?? 重要提示:
try_start_X和try_end_X標記 try 塊的開始和結(jié)束catch指令指定捕獲的異常類型和處理位置move-exception將異常對象移動到寄存器
3.5 工具使用:字節(jié)碼轉(zhuǎn)換與分析
3.5.1 使用 javac 編譯 Java 源碼
基本用法:
# 編譯單個文件 javac HelloWorld.java # 編譯多個文件 javac *.java # 指定輸出目錄 javac -d build/ src/**/*.java # 指定 classpath javac -cp libs/*.jar src/**/*.java
示例:
# 創(chuàng)建測試文件
cat > Test.java << 'EOF'
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
EOF
# 編譯
javac Test.java
# 查看生成的 .class 文件
ls -la Test.class3.5.2 使用 dx/d8 轉(zhuǎn)換為 DEX
dx 工具(舊版,Android SDK Build Tools < 28):
# 轉(zhuǎn)換單個 .class 文件 dx --dex --output=classes.dex Test.class # 轉(zhuǎn)換整個目錄 dx --dex --output=classes.dex build/classes/ # 包含外部庫 dx --dex --output=classes.dex --libs=android.jar build/classes/
d8 工具(新版,Android SDK Build Tools >= 28):
# 轉(zhuǎn)換 .class 文件 d8 Test.class --output . # 轉(zhuǎn)換整個目錄 d8 build/classes/**/*.class --output . # 指定 Android API 級別 d8 --lib android.jar --output . build/classes/**/*.class
驗證 DEX 文件:
# 使用 dexdump 查看 DEX 內(nèi)容 dexdump -d classes.dex | head -50
3.5.3 使用 baksmali 反編譯為 Smali
安裝 baksmali:
# 下載 baksmali wget https://github.com/JesusFreke/smali/releases/download/v2.5.2/baksmali-2.5.2.jar # 或使用 Homebrew (macOS) brew install smali
基本用法:
# 反編譯 DEX 文件 java -jar baksmali-2.5.2.jar d classes.dex -o smali_output/ # 指定 API 級別(重要!) java -jar baksmali-2.5.2.jar d classes.dex -o smali_output/ --api-level 28 # 反編譯 APK 中的 DEX java -jar baksmali-2.5.2.jar d app.apk -o smali_output/
查看反編譯結(jié)果:
# 查看目錄結(jié)構(gòu) tree smali_output/ # 查看特定類的 Smali 代碼 cat smali_output/com/example/Test.smali
3.5.4 使用 smali 編譯回 DEX
基本用法:
# 編譯 Smali 目錄為 DEX java -jar smali-2.5.2.jar a smali_output/ -o classes_new.dex # 指定 API 級別 java -jar smali-2.5.2.jar a smali_output/ -o classes_new.dex --api-level 28
驗證編譯結(jié)果:
# 使用 dexdump 驗證 dexdump -d classes_new.dex | head -50
3.5.5 使用文本編輯器閱讀和修改 Smali
推薦編輯器:
- VS Code + Smali 語法高亮插件
- Sublime Text + Smali 插件
- Android Studio(內(nèi)置 Smali 支持)
VS Code 配置:
// .vscode/settings.json
{
"files.associations": {
"*.smali": "smali"
}
}安裝 Smali 插件:
- 打開 VS Code
- 搜索 “Smali” 插件
- 安裝 “Smali” 或 “Smali Language Support”
3.6 代碼對照:Java → 字節(jié)碼 → Smali
3.6.1 示例 1:字符串拼接
Java 源碼:
public class StringTest {
public static String concat(String a, String b) {
return a + b;
}
}
Java 字節(jié)碼(javap -c):
public static java.lang.String concat(java.lang.String, java.lang.String);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_1
12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
Smali 代碼:
.class public LStringTest;
.super Ljava/lang/Object;
.method public static concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.registers 4
.param p0, "a" # Ljava/lang/String;
.param p1, "b" # Ljava/lang/String;
.prologue
.line 3
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method逐行解釋:
# 1. 創(chuàng)建 StringBuilder 實例
new-instance v0, Ljava/lang/StringBuilder;
# v0 = new StringBuilder()
# 2. 調(diào)用構(gòu)造函數(shù)
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# StringBuilder(v0).<init>()
# 3. 調(diào)用 append 方法,添加第一個字符串
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# v0.append(p0)
move-result-object v0
# v0 = 返回值(StringBuilder 對象)
# 4. 調(diào)用 append 方法,添加第二個字符串
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# v0.append(p1)
move-result-object v0
# v0 = 返回值
# 5. 調(diào)用 toString 方法
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
# v0.toString()
move-result-object v0
# v0 = 返回的字符串
# 6. 返回結(jié)果
return-object v0
# return v03.6.2 示例 2:條件判斷
Java 源碼:
public class ConditionTest {
public static int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
}
Java 字節(jié)碼:
public static int max(int, int);
Code:
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_0
6: ireturn
7: iload_1
8: ireturn
Smali 代碼:
.method public static max(II)I
.registers 2
.param p0, "a" # I
.param p1, "b" # I
.prologue
.line 3
if-gt p0, p1, :cond_0 # if (a > b) goto cond_0
return p0 # return a
:cond_0
return p1 # return b
.end method逐行解釋:
# 1. 比較 a 和 b,如果 a > b,跳轉(zhuǎn)到 :cond_0 if-gt p0, p1, :cond_0 # if (p0 > p1) goto :cond_0 # 2. 如果條件不滿足(a <= b),執(zhí)行這里,返回 a return p0 # return p0 # 3. 標簽:如果 a > b,跳轉(zhuǎn)到這里 :cond_0 # 4. 返回 b return p1 # return p1
3.6.3 示例 3:循環(huán)
Java 源碼:
public class LoopTest {
public static int sum(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
}
Smali 代碼:
.method public static sum(I)I
.registers 3
.param p0, "n" # I
.prologue
.line 3
const/4 v0, 0x0 # v0 = result = 0
const/4 v1, 0x0 # v1 = i = 0
:goto_0
if-ge v1, p0, :cond_0 # if (i < n) goto cond_0
return v0 # return result
:cond_0
add-int/2addr v0, v1 # result += i
add-int/lit8 v1, v1, 0x1 # i++
goto :goto_0 # goto 循環(huán)開始
.end method逐行解釋:
# 1. 初始化 result = 0 const/4 v0, 0x0 # v0 = 0 # 2. 初始化 i = 0 const/4 v1, 0x0 # v1 = 0 # 3. 循環(huán)標簽 :goto_0 # 4. 檢查循環(huán)條件:if (i < n) if-ge v1, p0, :cond_0 # if (v1 >= p0) goto :cond_0 (如果 i >= n,退出循環(huán)) # 注意:if-ge 是 "if greater or equal",所以這里是反向邏輯 # 5. 如果循環(huán)結(jié)束,返回 result return v0 # return v0 # 6. 循環(huán)體標簽 :cond_0 # 7. 執(zhí)行循環(huán)體:result += i add-int/2addr v0, v1 # v0 = v0 + v1 # 8. i++ add-int/lit8 v1, v1, 0x1 # v1 = v1 + 1 # 9. 跳回循環(huán)開始 goto :goto_0 # goto :goto_0
3.7 實戰(zhàn)案例:修改 Smali 代碼繞過驗證
3.7.1 創(chuàng)建測試應(yīng)用
步驟 1:編寫 Java 代碼
創(chuàng)建 LoginActivity.java:
package com.example.test;
import android.app.Activity;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends Activity {
private static final String CORRECT_PASSWORD = "admin123";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 簡化:直接驗證
if (checkPassword("admin123")) {
showMessage("Login Success!");
} else {
showMessage("Login Failed!");
}
}
private boolean checkPassword(String password) {
return password.equals(CORRECT_PASSWORD);
}
private void showMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}步驟 2:編譯為 APK
# 使用 Android Studio 或命令行編譯 # 這里假設(shè)已經(jīng)編譯為 app.apk
3.7.2 反編譯 APK
步驟 1:使用 apktool 反編譯
apktool d app.apk -o app_decompiled
步驟 2:查看 Smali 代碼
cat app_decompiled/smali/com/example/test/LoginActivity.smali
反編譯后的 Smali 代碼:
.class public Lcom/example/test/LoginActivity;
.super Landroid/app/Activity;
.field private static final CORRECT_PASSWORD:Ljava/lang/String; = "admin123"
.method protected onCreate(Landroid/os/Bundle;)V
.registers 3
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.prologue
.line 12
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 15
const-string v0, "admin123"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->checkPassword(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :cond_0
.line 16
const-string v0, "Login Success!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
:goto_0
return-void
:cond_0
.line 18
const-string v0, "Login Failed!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
goto :goto_0
.end method
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
const-string v0, "admin123"
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
return v0
.end method
.method private showMessage(Ljava/lang/String;)V
.registers 3
.param p1, "message" # Ljava/lang/String;
.prologue
.line 27
const/4 v0, 0x0
invoke-static {p0, p1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
return-void
.end method3.7.3 分析驗證邏輯
關(guān)鍵代碼分析:
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
const-string v0, "admin123" # v0 = "admin123"
# 調(diào)用 password.equals("admin123")
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0 # v0 = 返回值(true/false)
return v0 # return v0
.end method在 onCreate 中的調(diào)用:
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->checkPassword(Ljava/lang/String;)Z
move-result v0 # v0 = checkPassword 的返回值
if-eqz v0, :cond_0 # if (v0 == 0) goto cond_0
# 如果返回 false (0),跳轉(zhuǎn)到失敗分支
# 如果返回 true (非0),繼續(xù)執(zhí)行成功分支3.7.4 修改 Smali 代碼繞過驗證
方法 1:修改 checkPassword 方法,始終返回 true
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
# 原始代碼(注釋掉)
# const-string v0, "admin123"
# invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
# move-result v0
# return v0
# 修改:直接返回 true
const/4 v0, 0x1 # v0 = true
return v0 # return true
.end method方法 2:修改條件判斷,反轉(zhuǎn)邏輯
在 onCreate 方法中:
# 原始代碼 if-eqz v0, :cond_0 # if (v0 == 0) goto cond_0 # 修改為:始終跳轉(zhuǎn)到成功分支 # if-eqz v0, :cond_0 goto :cond_1 # 直接跳轉(zhuǎn)到成功分支(需要添加標簽) # 或者修改條件判斷 if-nez v0, :cond_0 # if (v0 != 0) goto cond_0 (反轉(zhuǎn)邏輯)
方法 3:直接修改 onCreate,移除驗證
.method protected onCreate(Landroid/os/Bundle;)V
.registers 3
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.prologue
.line 12
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
# 移除所有驗證代碼,直接顯示成功消息
const-string v0, "Login Success!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
return-void
.end method3.7.5 回編譯并測試
步驟 1:回編譯 APK
apktool b app_decompiled -o app_modified.apk
步驟 2:簽名 APK
# 生成密鑰庫(如果還沒有)
keytool -genkey -v -keystore my-release-key.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias my-key-alias
# 簽名 APK
apksigner sign --ks my-release-key.jks \
--ks-key-alias my-key-alias \
app_modified.apk或者使用 jarsigner(舊方法):
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore my-release-key.jks \
app_modified.apk my-key-alias
步驟 3:對齊 APK(可選但推薦)
zipalign -v 4 app_modified.apk app_modified_aligned.apk
步驟 4:安裝測試
# 卸載舊版本(如果已安裝) adb uninstall com.example.test # 安裝修改后的 APK adb install app_modified_aligned.apk # 運行應(yīng)用,驗證繞過效果
3.7.6 驗證修改效果
預(yù)期結(jié)果:
- ? 無論輸入什么密碼,都應(yīng)該顯示 “Login Success!”
- ? checkPassword 方法始終返回 true
- ? 應(yīng)用正常運行,無崩潰
如果修改失?。?/strong>
- 檢查 Smali 語法是否正確
- 檢查寄存器使用是否正確
- 檢查方法簽名是否匹配
- 查看 logcat 日志排查錯誤
3.8 常見問題與解決方案
3.8.1 寄存器相關(guān)問題
問題 1:寄存器數(shù)量限制導(dǎo)致編譯失敗
癥狀:
Error: Invalid register: v65536
原因:
- 寄存器數(shù)量超過了限制
- 寄存器編號錯誤
解決方案:
- 檢查
.registers指令聲明的寄存器數(shù)量 - 確保使用的寄存器編號在聲明范圍內(nèi)
- 減少局部變量的使用
示例:
# 錯誤:聲明了 3 個寄存器,但使用了 v3
.method test()V
.registers 3
const/4 v3, 0x1 # 錯誤!v3 超出范圍
.end method
# 正確:聲明足夠的寄存器
.method test()V
.registers 4
const/4 v3, 0x1 # 正確
.end method問題 2:方法參數(shù)寄存器編號錯誤
癥狀:
方法調(diào)用時參數(shù)傳遞錯誤
原因:
- 在非靜態(tài)方法中,
p0是this,參數(shù)從p1開始 - 在靜態(tài)方法中,參數(shù)從
p0開始
解決方案:
# 非靜態(tài)方法
.method public test(Ljava/lang/String;I)V
.registers 3
.param p1, "str" # 第一個參數(shù)是 p1(p0 是 this)
.param p2, "num" # 第二個參數(shù)是 p2
.end method
# 靜態(tài)方法
.method public static test(Ljava/lang/String;I)V
.registers 2
.param p0, "str" # 第一個參數(shù)是 p0
.param p1, "num" # 第二個參數(shù)是 p1
.end method3.8.2 異常處理問題
問題:try-catch 塊表示方式
Java 代碼:
try {
// code
} catch (Exception e) {
// handle
}
Smali 表示:
:try_start_0
# try 塊代碼
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
# 正常流程繼續(xù)
return-void
:catch_0
move-exception v0
# catch 塊代碼
return-void?? 重要提示:
try_start_X和try_end_X必須配對catch指令必須在try_end_X之后move-exception必須在 catch 塊的第一條指令
3.8.3 編譯和回編譯問題
問題 1:baksmali 反編譯失敗
癥狀:
Error: Invalid DEX file
解決方案:
- 檢查 DEX 文件是否損壞
- 使用正確的 API 級別:
--api-level 28 - 更新 baksmali 到最新版本
問題 2:smali 編譯失敗
癥狀:
Error: Invalid register Error: Invalid instruction
解決方案:
- 檢查 Smali 語法是否正確
- 檢查寄存器使用是否超出范圍
- 檢查指令格式是否正確
- 查看詳細的錯誤信息定位問題
問題 3:回編譯后 APK 無法安裝
癥狀:
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
解決方案:
- APK 必須簽名才能安裝
- 使用
apksigner或jarsigner簽名 - 使用
zipalign對齊 APK
# 完整流程 apktool b app_decompiled -o app_unsigned.apk apksigner sign --ks keystore.jks app_unsigned.apk zipalign -v 4 app_unsigned.apk app_final.apk
3.8.4 代碼修改問題
問題:修改后邏輯錯誤
癥狀:
應(yīng)用崩潰或行為異常
解決方案:
- 仔細分析原始邏輯
- 確保修改后的邏輯完整
- 注意寄存器的一致性
- 使用 logcat 查看錯誤日志
調(diào)試技巧:
# 添加日志輸出(需要導(dǎo)入 Log 類)
const-string v0, "TAG"
const-string v1, "Debug message"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
3.9 本章總結(jié)
3.9.1 知識點回顧
- Java 字節(jié)碼 vs DEX 字節(jié)碼
- JVM 使用棧,Dalvik 使用寄存器
- DEX 格式更緊湊,適合移動設(shè)備
- Smali 語法基礎(chǔ)
- 寄存器系統(tǒng)(v0-vN, p0-pN)
- 數(shù)據(jù)類型表示
- 方法調(diào)用約定
- Smali 指令集
- invoke-* 系列:方法調(diào)用
- if-* 系列:條件跳轉(zhuǎn)
- move-* 系列:數(shù)據(jù)移動
- const-* 系列:常量加載
- 類、方法、字段定義
- 訪問修飾符
- 異常處理
3.9.2 實踐要點
- ? 理解寄存器系統(tǒng)是學(xué)習 Smali 的關(guān)鍵
- ? 掌握指令集,能夠閱讀 Smali 代碼
- ? 能夠修改 Smali 代碼實現(xiàn)邏輯繞過
- ? 注意參數(shù)寄存器的編號規(guī)則
3.9.3 下一步學(xué)習
- 第 4 章:使用 apktool 和 jadx 進行靜態(tài)分析
- 第 6 章:學(xué)習動態(tài)調(diào)試技術(shù)
- 第 7 章:使用 Frida 進行動態(tài) Hook
附錄:Smali 指令速查表
數(shù)據(jù)移動指令
| 指令 | 說明 | 示例 |
|---|---|---|
move vA, vB | 移動 32 位值 | move v0, v1 |
move-wide vA, vB | 移動 64 位值 | move-wide v0, v2 |
move-object vA, vB | 移動對象引用 | move-object v0, v1 |
move-result vA | 移動方法返回值 | move-result v0 |
常量加載指令
| 指令 | 說明 | 示例 |
|---|---|---|
const/4 vA, #+B | 加載 4 位常量 | const/4 v0, 0x5 |
const/16 vA, #+B | 加載 16 位常量 | const/16 v0, 0x100 |
const-string vA, string | 加載字符串 | const-string v0, "Hello" |
方法調(diào)用指令
| 指令 | 說明 | 示例 |
|---|---|---|
invoke-virtual | 虛方法調(diào)用 | invoke-virtual {v0}, Ljava/lang/String;->length()I |
invoke-static | 靜態(tài)方法調(diào)用 | invoke-static {}, Ljava/lang/System;->currentTimeMillis()J |
invoke-direct | 直接方法調(diào)用 | invoke-direct {v0}, Ljava/lang/String;-><init>()V |
條件跳轉(zhuǎn)指令
| 指令 | 說明 | 示例 |
|---|---|---|
if-eq vA, vB, :label | 相等跳轉(zhuǎn) | if-eq v0, v1, :equal |
if-ne vA, vB, :label | 不等跳轉(zhuǎn) | if-ne v0, v1, :not_equal |
if-lt vA, vB, :label | 小于跳轉(zhuǎn) | if-lt v0, v1, :less |
if-gt vA, vB, :label | 大于跳轉(zhuǎn) | if-gt v0, v1, :greater |
本章完成! ??
現(xiàn)在你已經(jīng)掌握了 Smali 語法的基礎(chǔ)知識,能夠閱讀和修改 Smali 代碼。在下一章中,我們將學(xué)習如何使用工具進行靜態(tài)分析實戰(zhàn)。
到此這篇關(guān)于Java 字節(jié)碼與 Smali 語法基礎(chǔ)的文章就介紹到這了,更多相關(guān)Java 字節(jié)碼與 Smali 語法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java之scan.next()與scan.nextline()函數(shù)的使用及區(qū)別
這篇文章主要介紹了java之scan.next()與scan.nextline()函數(shù)的使用及區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
SpringBoot集成Redis數(shù)據(jù)庫,實現(xiàn)緩存管理
SpringBoot2 版本,支持的組件越來越豐富,對Redis的支持不僅僅是擴展了API,更是替換掉底層Jedis的依賴,換成Lettuce。 本案例需要本地安裝一臺Redis數(shù)據(jù)庫。下面就來看下集成Redis的步驟2021-06-06
java 二進制數(shù)據(jù)與16進制字符串相互轉(zhuǎn)化方法
今天小編就為大家分享一篇java 二進制數(shù)據(jù)與16進制字符串相互轉(zhuǎn)化方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07
Spring Cloud OpenFeign REST服務(wù)客戶端原理及用法解析
這篇文章主要介紹了Spring Cloud OpenFeign REST服務(wù)客戶端原理及用法解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習或者工作具有一定的參考學(xué)習價值,需要的朋友可以參考下2020-10-10
MyBatis?Generator?ORM層面的代碼自動生成器(推薦)
Mybatis?Generator是一個專門為?MyBatis和?ibatis框架使用者提供的代碼生成器,也可以快速的根據(jù)數(shù)據(jù)表生成對應(yīng)的pojo類、Mapper接口、Mapper文件,甚至生成QBC風格的查詢對象,這篇文章主要介紹了MyBatis?Generator?ORM層面的代碼自動生成器,需要的朋友可以參考下2023-01-01
SpringMVC+Mybatis二維碼實現(xiàn)多平臺付款(附源碼)
本文主要實現(xiàn)微信支付寶等支付平臺合多為一的二維碼支付,并且實現(xiàn)有效時間內(nèi)支付有效,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08

