Java 字节码与Smali 语法基础实战案例
3.1 Java 字节码与 DEX 字节码
3.1.1 Java 字节码(.class)简介
Java 字节码是 Java 源代码编译后的中间表示形式,运行在 Java 虚拟机(JVM)上。
编译流程:
Java 源码 (.java)
↓ javac
Java 字节码 (.class)
↓ JVM
机器码执行
Java 字节码特点:
- 平台无关的中间代码
- 基于栈的虚拟机(Stack-based VM)
- 指令集相对简单
- 面向对象设计
3.1.2 DEX 字节码(.dex)简介
DEX(Dalvik Executable)是 Android 平台上的字节码格式,针对移动设备进行了优化。
转换流程:
Java 源码 (.java)
↓ javac
Java 字节码 (.class)
↓ dx/d8
DEX 字节码 (.dex)
↓ Dalvik/ART VM
机器码执行
DEX 字节码特点:
- 多个 .class 文件合并为一个 .dex
- 基于寄存器的虚拟机(Register-based VM)
- 指令集更紧凑
- 优化了内存使用
3.1.3 Dalvik 虚拟机与 JVM 的区别
| 特性 | JVM | Dalvik VM |
|---|---|---|
| 字节码格式 | .class | .dex |
| 虚拟机类型 | 栈式虚拟机 | 寄存器虚拟机 |
| 文件组织 | 每个类一个文件 | 多个类合并到一个文件 |
| 指令集 | 基于栈操作 | 基于寄存器操作 |
| 内存占用 | 较大 | 较小(优化后) |
| 执行方式 | 解释执行/JIT | 解释执行/JIT/AOT |
3.1.4 指令集映射关系
Java 字节码示例:
// Java 源码 int a = 10; int b = 20; int c = a + b;
对应的 Java 字节码(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 // 弹出两个值,相加,结果压入栈 9: istore_3 // 弹出栈顶值,存储到局部变量 3 (c)
对应的 Smali 代码:
const/16 v0, 0xa # 将 10 加载到寄存器 v0 const/16 v1, 0x14 # 将 20 加载到寄存器 v1 add-int v2, v0, v1 # v2 = v0 + v1
关键区别:
- JVM:使用栈,需要 push/pop 操作
- Dalvik:使用寄存器,直接操作寄存器
3.2 Smali 语法基础
3.2.1 寄存器系统
Smali 使用寄存器来存储局部变量和方法参数,这是与 Java 字节码最大的区别。
寄存器命名规则
局部变量寄存器:
v0,v1,v2, …vN:局部变量寄存器- 从 0 开始编号
- 通常用于存储方法内的局部变量
参数寄存器:
p0,p1,p2, …pN:参数寄存器p0在非静态方法中通常是this引用p1,p2, … 是方法的实际参数
寄存器数量限制:
- 单个方法最多可以使用 65536 个寄存器(理论值)
- 实际使用中,寄存器数量受方法复杂度限制
- 寄存器数量过多会导致编译失败
寄存器使用示例
Java 代码:
public int add(int a, int b) {
int result = a + b;
return result;
}
对应的 Smali 代码:
.method public add(II)I
.registers 4 # 使用 4 个寄存器
# 寄存器分配:
# p0 = this (非静态方法的 this 引用)
# p1 = a (第一个参数)
# p2 = b (第二个参数)
# v0 = result (局部变量)
.prologue
.line 1
add-int v0, p1, p2 # v0 = p1 + p2
return v0 # 返回 v0
.end method⚠️ 重要提示:
- 参数寄存器从
p0开始,不是p1 - 在非静态方法中,
p0是this,实际参数从p1开始 - 在静态方法中,参数从
p0开始
3.2.2 数据类型
基本类型
| Java 类型 | Smali 类型 | 大小 | 说明 |
|---|---|---|---|
boolean | Z | 1 字节 | 布尔值 |
byte | B | 1 字节 | 字节 |
short | S | 2 字节 | 短整型 |
char | C | 2 字节 | 字符 |
int | I | 4 字节 | 整型 |
long | J | 8 字节 | 长整型 |
float | F | 4 字节 | 单精度浮点 |
double | D | 8 字节 | 双精度浮点 |
void | V | - | 无返回值 |
引用类型
类类型:
Ljava/lang/String; # String 类 Landroid/app/Activity; # Activity 类
数组类型:
[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 方法调用约定
Smali 中有多种方法调用指令,用于不同的调用场景。
invoke-virtual(虚方法调用)
用途: 调用实例方法,支持多态
语法:
invoke-virtual {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
// 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(静态方法调用)
用途: 调用静态方法
语法:
invoke-static {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
// 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 结果存储到 v2
invoke-direct(直接方法调用)
用途: 调用构造函数、私有方法、final 方法
语法:
invoke-direct {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
// 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(接口方法调用)
用途: 调用接口方法
语法:
invoke-interface {参数寄存器}, 接口名;->方法名(参数类型)返回类型
invoke-super(父类方法调用)
用途: 调用父类方法
语法:
invoke-super {参数寄存器}, 父类名;->方法名(参数类型)返回类型
调用指令对比表:
| 指令 | 用途 | 多态支持 | 性能 |
|---|---|---|---|
invoke-virtual | 实例方法 | ✅ | 较慢 |
invoke-static | 静态方法 | ❌ | 快 |
invoke-direct | 构造函数/私有方法 | ❌ | 快 |
invoke-interface | 接口方法 | ✅ | 最慢 |
invoke-super | 父类方法 | ❌ | 快 |
3.2.4 条件跳转指令
条件跳转指令用于实现 if-else、循环等控制流。
基本条件跳转
相等比较:
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-* 系列(方法调用)
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-* 系列(数据移动)
常用 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 位立即数 (-8 到 7) const/16 vA, #+B # vA = 符号扩展的 16 位立即数 (-32768 到 32767) const vA, #+B # vA = 32 位立即数 const-wide/16 vA, #+B # vA = 符号扩展的 16 位立即数 (long) const-wide/32 vA, #+B # vA = 符号扩展的 32 位立即数 (long) const-wide vA, #+B # vA = 64 位立即数 (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 其他常用指令
算术运算
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
数组操作
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 # 静态字段 .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 块的开始和结束catch指令指定捕获的异常类型和处理位置move-exception将异常对象移动到寄存器
3.5 工具使用:字节码转换与分析
3.5.1 使用 javac 编译 Java 源码
基本用法:
# 编译单个文件 javac HelloWorld.java # 编译多个文件 javac *.java # 指定输出目录 javac -d build/ src/**/*.java # 指定 classpath javac -cp libs/*.jar src/**/*.java
示例:
# 创建测试文件
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 转换为 DEX
dx 工具(旧版,Android SDK Build Tools < 28):
# 转换单个 .class 文件 dx --dex --output=classes.dex Test.class # 转换整个目录 dx --dex --output=classes.dex build/classes/ # 包含外部库 dx --dex --output=classes.dex --libs=android.jar build/classes/
d8 工具(新版,Android SDK Build Tools >= 28):
# 转换 .class 文件 d8 Test.class --output . # 转换整个目录 d8 build/classes/**/*.class --output . # 指定 Android API 级别 d8 --lib android.jar --output . build/classes/**/*.class
验证 DEX 文件:
# 使用 dexdump 查看 DEX 内容 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/
查看反编译结果:
# 查看目录结构 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
验证编译结果:
# 使用 dexdump 验证 dexdump -d classes_new.dex | head -50
3.5.5 使用文本编辑器阅读和修改 Smali
推荐编辑器:
- VS Code + Smali 语法高亮插件
- Sublime Text + Smali 插件
- Android Studio(内置 Smali 支持)
VS Code 配置:
// .vscode/settings.json
{
"files.associations": {
"*.smali": "smali"
}
}安装 Smali 插件:
- 打开 VS Code
- 搜索 “Smali” 插件
- 安装 “Smali” 或 “Smali Language Support”
3.6 代码对照:Java → 字节码 → Smali
3.6.1 示例 1:字符串拼接
Java 源码:
public class StringTest {
public static String concat(String a, String b) {
return a + b;
}
}
Java 字节码(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. 创建 StringBuilder 实例
new-instance v0, Ljava/lang/StringBuilder;
# v0 = new StringBuilder()
# 2. 调用构造函数
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# StringBuilder(v0).<init>()
# 3. 调用 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. 调用 append 方法,添加第二个字符串
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# v0.append(p1)
move-result-object v0
# v0 = 返回值
# 5. 调用 toString 方法
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
# v0.toString()
move-result-object v0
# v0 = 返回的字符串
# 6. 返回结果
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 字节码:
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,跳转到 :cond_0 if-gt p0, p1, :cond_0 # if (p0 > p1) goto :cond_0 # 2. 如果条件不满足(a <= b),执行这里,返回 a return p0 # return p0 # 3. 标签:如果 a > b,跳转到这里 :cond_0 # 4. 返回 b return p1 # return p1
3.6.3 示例 3:循环
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 循环开始
.end method逐行解释:
# 1. 初始化 result = 0 const/4 v0, 0x0 # v0 = 0 # 2. 初始化 i = 0 const/4 v1, 0x0 # v1 = 0 # 3. 循环标签 :goto_0 # 4. 检查循环条件:if (i < n) if-ge v1, p0, :cond_0 # if (v1 >= p0) goto :cond_0 (如果 i >= n,退出循环) # 注意:if-ge 是 "if greater or equal",所以这里是反向逻辑 # 5. 如果循环结束,返回 result return v0 # return v0 # 6. 循环体标签 :cond_0 # 7. 执行循环体:result += i add-int/2addr v0, v1 # v0 = v0 + v1 # 8. i++ add-int/lit8 v1, v1, 0x1 # v1 = v1 + 1 # 9. 跳回循环开始 goto :goto_0 # goto :goto_0
3.7 实战案例:修改 Smali 代码绕过验证
3.7.1 创建测试应用
步骤 1:编写 Java 代码
创建 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 或命令行编译 # 这里假设已经编译为 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 分析验证逻辑
关键代码分析:
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
const-string v0, "admin123" # v0 = "admin123"
# 调用 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 中的调用:
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),跳转到失败分支
# 如果返回 true (非0),继续执行成功分支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:修改条件判断,反转逻辑
在 onCreate 方法中:
# 原始代码 if-eqz v0, :cond_0 # if (v0 == 0) goto cond_0 # 修改为:始终跳转到成功分支 # if-eqz v0, :cond_0 goto :cond_1 # 直接跳转到成功分支(需要添加标签) # 或者修改条件判断 if-nez v0, :cond_0 # if (v0 != 0) goto cond_0 (反转逻辑)
方法 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 # 运行应用,验证绕过效果
3.7.6 验证修改效果
预期结果:
- ✅ 无论输入什么密码,都应该显示 “Login Success!”
- ✅ checkPassword 方法始终返回 true
- ✅ 应用正常运行,无崩溃
如果修改失败:
- 检查 Smali 语法是否正确
- 检查寄存器使用是否正确
- 检查方法签名是否匹配
- 查看 logcat 日志排查错误
3.8 常见问题与解决方案
3.8.1 寄存器相关问题
问题 1:寄存器数量限制导致编译失败
症状:
Error: Invalid register: v65536
原因:
- 寄存器数量超过了限制
- 寄存器编号错误
解决方案:
- 检查
.registers指令声明的寄存器数量 - 确保使用的寄存器编号在声明范围内
- 减少局部变量的使用
示例:
# 错误:声明了 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:方法参数寄存器编号错误
症状:
方法调用时参数传递错误
原因:
- 在非静态方法中,
p0是this,参数从p1开始 - 在静态方法中,参数从
p0开始
解决方案:
# 非静态方法
.method public test(Ljava/lang/String;I)V
.registers 3
.param p1, "str" # 第一个参数是 p1(p0 是 this)
.param p2, "num" # 第二个参数是 p2
.end method
# 静态方法
.method public static test(Ljava/lang/String;I)V
.registers 2
.param p0, "str" # 第一个参数是 p0
.param p1, "num" # 第二个参数是 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
# 正常流程继续
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 代码修改问题
问题:修改后逻辑错误
症状:
应用崩溃或行为异常
解决方案:
- 仔细分析原始逻辑
- 确保修改后的逻辑完整
- 注意寄存器的一致性
- 使用 logcat 查看错误日志
调试技巧:
# 添加日志输出(需要导入 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 本章总结
3.9.1 知识点回顾
- Java 字节码 vs DEX 字节码
- JVM 使用栈,Dalvik 使用寄存器
- DEX 格式更紧凑,适合移动设备
- Smali 语法基础
- 寄存器系统(v0-vN, p0-pN)
- 数据类型表示
- 方法调用约定
- Smali 指令集
- invoke-* 系列:方法调用
- if-* 系列:条件跳转
- move-* 系列:数据移动
- const-* 系列:常量加载
- 类、方法、字段定义
- 访问修饰符
- 异常处理
3.9.2 实践要点
- ✅ 理解寄存器系统是学习 Smali 的关键
- ✅ 掌握指令集,能够阅读 Smali 代码
- ✅ 能够修改 Smali 代码实现逻辑绕过
- ✅ 注意参数寄存器的编号规则
3.9.3 下一步学习
- 第 4 章:使用 apktool 和 jadx 进行静态分析
- 第 6 章:学习动态调试技术
- 第 7 章:使用 Frida 进行动态 Hook
附录:Smali 指令速查表
数据移动指令
| 指令 | 说明 | 示例 |
|---|---|---|
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" |
方法调用指令
| 指令 | 说明 | 示例 |
|---|---|---|
invoke-virtual | 虚方法调用 | invoke-virtual {v0}, Ljava/lang/String;->length()I |
invoke-static | 静态方法调用 | invoke-static {}, Ljava/lang/System;->currentTimeMillis()J |
invoke-direct | 直接方法调用 | invoke-direct {v0}, Ljava/lang/String;-><init>()V |
条件跳转指令
| 指令 | 说明 | 示例 |
|---|---|---|
if-eq vA, vB, :label | 相等跳转 | if-eq v0, v1, :equal |
if-ne vA, vB, :label | 不等跳转 | if-ne v0, v1, :not_equal |
if-lt vA, vB, :label | 小于跳转 | if-lt v0, v1, :less |
if-gt vA, vB, :label | 大于跳转 | if-gt v0, v1, :greater |
本章完成! 🎉
现在你已经掌握了 Smali 语法的基础知识,能够阅读和修改 Smali 代码。在下一章中,我们将学习如何使用工具进行静态分析实战。
到此这篇关于Java 字节码与 Smali 语法基础的文章就介绍到这了,更多相关Java 字节码与 Smali 语法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
maven打包web项目时同时打包为war和jar文件的方法
本篇文章主要介绍了maven打包web项目时同时打包为war和jar文件的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2017-10-10


最新评论