Smali 语法基础
Smali 是 DEX 字节码的汇编语言表示:APK 中的 DEX 文件经 baksmali/apktool 反汇编后得到.smali文件,每条 smali 指令与 DEX 字节码一一对应、语义等价。修改 smali 后可以重新汇编成 DEX 并打包回 APK——这也是修改 App 功能(去广告、改会员校验等)的基本手段。
本文按"类 → 字段 → 方法 → 调用 → 对象 → 数据 → 数组 → 异常 → 跳转 → 字段读写 → 综合实战"的顺序整理常用语法。
一、基本语法规则
注释
以#开头:
# 这是一行注释寄存器:v 寄存器与 p 寄存器
Smali 是面向寄存器的语言,所有数据都放在寄存器中操作,寄存器命名为v0、v1、v2……
方法中可用寄存器数量由两条伪指令之一声明:
.locals 2 # 方法内自定义(局部)寄存器有 2 个:v0、v1 .registers 3 # 方法总共使用 3 个寄存器(含参数寄存器),二选一即可参数寄存器以p命名,与v寄存器的换算关系:
- 非静态方法:
p0表示this,p1、p2……依次表示方法参数; - 静态方法:没有
this,p0就是第一个参数; long和double占两个寄存器。
类型描述符
方法签名和字段类型中使用的是类型描述符,而不是 Java 类名:
| Java 类型 | 描述符 |
|---|---|
| void | V |
| boolean | Z |
| byte | B |
| short | S |
| char | C |
| int | I |
| long | J |
| float | F |
| double | D |
| 引用类型 | L全包名/类名;,如Ljava/lang/String; |
| 数组 | [+ 元素类型,如[I、[Ljava/lang/String; |
方法签名格式为:(参数1类型参数2类型)返回值类型,例如(Ljava/lang/String;I)V表示参数为 String、int,无返回值。
二、类声明
格式:
.class 权限修饰符 类名; .super 父类全类名;示例:
.class public Lcom/test/Test; .super Ljava/lang/Object;注意类名必须是L开头、;结尾的描述符形式。
三、字段声明
普通字段
格式:
.field 权限修饰符 静态修饰符 变量名:变量类型描述符;示例:
.field public a:Ljava/lang/String; .field private static b:I常量(带初始值的字段)
加上final并在类型后用=给出初始值:
.field 权限修饰符 static final 变量名:类型描述符; = 常量值Java 代码:
publicclassTest{privatestaticfinalStringa="hello";}对应 smali:
.class public LTest; .super Ljava/lang/Object; .field private static final a:Ljava/lang/String; = "hello"四、方法声明
格式:
.method 权限修饰符 静态修饰符 方法名(参数类型)返回值类型 # 方法体 .end method示例:
.method public testMethod(Ljava/lang/String;)V .locals 0 return-void .end method构造方法<init>
构造方法名固定为<init>,声明时带constructor关键字:
.method 权限修饰符 constructor <init>(参数类型)返回值类型 # 方法体 .end methodJava 代码:
publicclassTest{publicTest(Stringa){}}对应 smali:
.class public LTest; .super Ljava/lang/Object; .method public constructor <init>(Ljava/lang/String;)V .registers 2 invoke-direct {p0}, Ljava/lang/Object;-><init>()V # 调用父类构造方法 return-void .end method静态代码块<clinit>
静态代码块对应固定方法名<clinit>,用static constructor声明,在类加载时执行一次:
.method static constructor <clinit>()V # 方法体 .end methodJava 代码:
publicclassTest{publicstaticStringa="a";static{}}对应 smali(静态字段的赋值放在<clinit>中,配合sput-object):
.class public LTest; .super Ljava/lang/Object; .field public static a:Ljava/lang/String; .method static constructor <clinit>()V .registers 1 const-string v0, "a" sput-object v0, LTest;->a:Ljava/lang/String; return-void .end method五、方法的调用:五种 invoke
| 指令 | 用途 |
|---|---|
invoke-virtual | 调用非私有实例方法(普通 public/protected 成员方法,虚分派) |
invoke-direct | 调用构造方法<init>和私有方法 |
invoke-static | 调用静态方法 |
invoke-super | 调用父类的方法 |
invoke-interface | 调用接口方法 |
通用格式:
invoke-类型 {寄存器列表}, 方法所属类;->方法名(参数类型)返回值类型1. invoke-direct:构造方法与私有方法
invoke-direct {p0}, Ljava/lang/Object;-><init>()V2. invoke-virtual:普通实例方法
Java 代码:
publicclassTest{publicTest(Stringa){this.getName();}publicStringgetName(){return"hello";}}对应 smali:
.class public LTest; .super Ljava/lang/Object; .method public constructor <init>(Ljava/lang/String;)V .registers 2 invoke-direct {p0}, Ljava/lang/Object;-><init>()V invoke-virtual {p0}, LTest;->getName()Ljava/lang/String; # 调用普通成员方法 return-void .end method # 声明 getName 方法 .method public getName()Ljava/lang/String; .registers 1 const-string v0, "hello" # 定义局部字符串常量 return-object v0 # 返回对象 .end method3. invoke-super:调用父类方法
Java 代码:
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);}对应 smali:
.method protected onCreate(Landroid/os/Bundle;)V .registers 2 invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V return-void .end method4. invoke-interface:调用接口方法
调用的是接口声明的方法时必须使用invoke-interface,方法所属类写接口类型:
# Java: List<String> list = new ArrayList<>(); # list.add("hello"); new-instance v0, Ljava/util/ArrayList; invoke-direct {v0}, Ljava/util/ArrayList;-><init>()V const-string v1, "hello" invoke-interface {v0, v1}, Ljava/util/List;->add(Ljava/lang/Object;)Z5. invoke-static:调用静态方法
# Java: int n = Integer.parseInt("123"); const-string v0, "123" invoke-static {v0}, Ljava/lang/Integer;->parseInt(Ljava/lang/String;)I move-result v06. 接收方法返回值
invoke-*只负责调用,返回值必须紧跟一条move-result系列指令接收:
# 第一步:调用方法 invoke-virtual {p0}, LTest;->getName()Ljava/lang/String; # 第二步:接收返回值 move-result-object v0 # 引用类型用 move-result-object # move-result v0 # int/boolean 等基本类型 # move-result-wide v0 # long/double六、创建对象
对象创建分两步:先 new 出实例,再调用构造方法,二者缺一不可:
# 1. 声明实例 new-instance v0, LTest; # 2. 调用构造方法(若构造方法有参数,需提前准备好寄存器,一并传入) invoke-direct {v0}, LTest;-><init>()V对应 Java 的new Test()。
注意:
new-instance与后续<init>调用之间不能有分支跳转,未初始化的对象不能被使用。
七、数据的定义
字符串与类对象
const-string v0, "hello" # 字符串:将 "hello" 放入 v0 const-class v0, LGoActivity; # 类对象:将 GoActivity.class 放入 v0数值常量:const 系列(占一个寄存器)
高位默认为符号位,按能容纳的位宽选择指令:
const/4 v0, 0x2 # 4 位半字节,取值范围 -8 ~ 7 const/16 v0, 0xABCD # 16 位,取值范围 -32768 ~ 32767(如 short) const v0, 0xA # 32 位(如 int),0xA 即十进制 10 const/high16 v0, 0x10000 # 只存高 16 位,低 16 位自动补 0宽数据:const-wide(占两个连续寄存器)
long、double是 64 位数据,占用两个相邻寄存器:
const-wide v0, 0x1L # 占用 v0 和 v1 两个寄存器对应的变体还有const-wide/16、const-wide/32、const-wide/high16。
八、数组操作
创建数组 new-array / new-array/jumbo
先创建数组new-array(目标寄存器, 长度寄存器, 元素类型),再逐个填元素:
# Java: int[] arr = new int[3]; const/4 v0, 0x3 # 数组长度 new-array v0, v0, [I # 创建 int[3],引用放入 v0 # 元素占 long/double 时用 new-instance/new-array-wide(实际是 new-array,类型写 J/D) const/4 v0, 0x3 new-array v0, v0, [J # 创建 long[3]数组长度超过0xFFFF(65535) 时用new-array/jumbo。
注意:
new-array的目标寄存器与长度寄存器不能是同一个——上面示例第一个是目标v0、第二个是长度v0,是合法的;更推荐用v0存长度、v1存数组,避免混淆。
const/16 v0, 0x3 # 长度放 v0 new-array v1, v0, [I # 数组放 v1数组读写 aget / aput
| 指令 | 语义 | Java 对应 |
|---|---|---|
aget vC, vA, vB | 读取vA[索引vB]的 int → vC | int x = arr[0]; |
aget-object vC, vA, vB | 读取数组中的对象(引用) | String s = arr[0]; |
aget-wide vC, vA, vB | 读取 long/double 元素 | long x = arr[0]; |
aput vA, vB, vC | 把 vC 的值写入vA[索引 vB] | arr[0] = 10; |
aput-object vA, vB, vC | 把对象 vC 写入数组 | arr[0] = s; |
aput-wide vA, vB, vC | 写入 long/double | arr[0] = 100L; |
记忆方法:get读 /put写;-object处理引用,-wide处理 long/double;前两个寄存器是(数组, 索引)。对象数组实例化后每个元素默认是 null,使用前要逐个赋值。
完整示例——读写一个String[]:
String[]names=newString[2];names[0]="a";Stringfirst=names[0];对应 smali:
# String[] names = new String[2]; const/4 v0, 0x2 new-array v0, v0, [Ljava/lang/String; # String[2] 放入 v0 # names[0] = "a"; const/4 v1, 0x0 const-string v2, "a" aput-object v0, v1, v2 # v0[0] = v2 # String first = names[0]; const/4 v1, 0x0 aget-object v3, v0, v1 # v3 = v0[0]数组长度 array-length
array-length vC, vA # vC = int 数组 vA 的长度(Java: int n = arr.length;)填充数组 fill-array-data
静态初始化数组时用fill-array-data,数据写在方法末尾的.array-data中:
const/16 v0, 0x3 new-array v0, v0, [I fill-array-data v0, :array_0 # 用 :array_0 的表填充 :array_0 .array-data 4 # 每个元素占 4 字节(int) 0x1 0x2 0x3 .end array-data九、异常处理
try / catch 结构
Smali 中异常处理通过.catch伪指令声明,放在 try 块起点到异常处理器跳转之间:
.method public test()V .registers 2 # 声明 try 范围:从 :try_start_0 到 :try_end_0,抛出的 TimeoutException 交给 :catch_0 .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_0 :try_start_0 # 可能抛异常的操作 invoke-static {}, Lcom/test/Net;->request()V goto :try_end_0 :catch_0 # 异常处理器 move-exception v0 # 捕获的异常对象放入 v0 const-string v1, "IO error" # 处理... :try_end_0 return-void .end method.catch语法:
.catch 异常类型; {:try_start_标签 .. :try_end_标签} :处理标签- 异常类型:要捕获的异常全类名(如
Ljava/io/IOException;)。捕获所有异常(Java 的catch (Exception e))通常写Ljava/lang/Exception;; - try 范围:
{:start .. :end}是两个标签之间,范围是半开区间(含 start,不含 end); - 处理标签:异常发生时跳转到的处理器位置。
move-exception 获取异常对象
进入异常处理器后,第一件事通常就是move-exception vX,把抛出的异常对象放进寄存器:
.catch Ljava/lang/Exception; {:ts_0 .. :te_0} :handler :ts_0 invoke-static {}, Lcom/test/Test;->risky()I goto :done :handler move-exception v0 # v0 = 捕获到的 Throwable invoke-virtual {v0}, Ljava/lang/Throwable;->printStackTrace()V goto :done :done return-void注意:
move-exception必须紧跟在异常处理器的起始位置(.catch指向的那个标签处),且每个 .catch 处理器只执行一次。
同时捕获多个异常
Java 中:
try{risky();}catch(IOExceptione){// ...}catch(RuntimeExceptione){// ...}smali 用多条.catch(范围相同、处理器不同):
.method public test()V .registers 2 .catch Ljava/io/IOException; {:try_start_0 .. :try_end_0} :catch_io .catch Ljava/lang/RuntimeException; {:try_start_0 .. :try_end_0} :catch_rt :try_start_0 invoke-static {}, Lcom/test/Net;->risky()V goto :try_end_0 :catch_io move-exception v0 # 处理 IOException goto :try_end_0 :catch_rt move-exception v0 # 处理 RuntimeException :try_end_0 return-void .end methodthrow 显式抛出异常
# Java: throw new UnsupportedOperationException(); new-instance v0, Ljava/lang/UnsupportedOperationException; invoke-direct {v0}, Ljava/lang/UnsupportedOperationException;-><init>()V throw v0逆向场景:smali 中经常能看到反编译器为"加固/校验"生成的 try-catch 包裹,或把某段关键逻辑放在 finally(编译成 try-finally 结构)。读懂
.catch与move-exception,才能在 Hook/分析时准确定位"哪里抛异常、异常被谁吃掉"。
十、跳转指令(控制流)
跳转指令用于改变程序的执行顺序,实现if / else、switch、while、for等控制流。Smali 中跳转目标用标签(label)表示,如:cond_0、:goto_0,通常是反汇编工具自动生成的友好名字,你可以在指令后用:标签自定义。
无条件跳转 goto
goto :goto_0 # 无条件跳到标签处 goto/16 :goto_0 # 跳转范围更大(16 位偏移) goto/32 :goto_0 # 跳转范围最大(32 位偏移) :goto_0 return-void条件跳转 if-test / if-testz
| 指令 | 语义 | Java 对应 |
|---|---|---|
if-eq vA, vB, :l | vA == vB 时跳转 | if (a == b) |
if-ne vA, vB, :l | vA != vB 时跳转 | if (a != b) |
if-lt vA, vB, :l | vA < vB 时跳转 | if (a < b) |
if-ge vA, vB, :l | vA >= vB 时跳转 | if (a >= b) |
if-gt vA, vB, :l | vA > vB 时跳转 | if (a > b) |
if-le vA, vB, :l | vA <= vB 时跳转 | if (a <= b) |
if-eqz vA, :l | vA == 0 时跳转 | if (a == 0) |
if-nez vA, :l | vA != 0 时跳转 | if (a != null) |
if-ltz vA, :l | vA < 0 时跳转 | if (a < 0) |
if-gez vA, :l | vA >= 0 时跳转 | if (a >= 0) |
if-gtz vA, :l | vA > 0 时跳转 | if (a > 0) |
if-lez vA, :l | vA <= 0 时跳转 | if (a <= 0) |
if-test系列比较两个寄存器;if-testz系列只比较一个寄存器与 0(z表示 zero),if-nez常用来判断对象是否非空。
示例——Java 的if语句:
if(num>0){log("positive");}else{log("non-positive");}对应 smali(注意"不满足条件则跳过正分支"的写法,条件方向是反的):
# int num 存放在 v0 中 if-lez v0, :cond_else # if num <= 0 跳到 else # 正分支:num > 0 const-string v1, "positive" invoke-static {v1}, Lcom/test/Test;->log(Ljava/lang/String;)V goto :cond_end :cond_else # else 分支 const-string v1, "non-positive" invoke-static {v1}, Lcom/test/Test;->log(Ljava/lang/String;)V :cond_end return-voidwhile 循环完整示例
形式一:先判断后执行(正常 while)
Java:
inti=0;// i 存放于 v0while(i<3){log("i = "+i);i++;}对应 smali(进入循环前先判断一次,防止一次都不执行):
# int i = 0(i 放 v0,常量 3 放 v1) const/4 v0, 0x0 const/4 v1, 0x3 :loop_cond # 判断 i < 3,不成立则跳出 if-ge v0, v1, :loop_end # 循环体:log("i = " + i) const-string v2, "i = " invoke-static {v2, v0}, Lcom/test/Test;->log(Ljava/lang/String;I)V # i++ add-int/lit8 v0, v0, 0x1 goto :loop_cond :loop_end return-void形式二:先执行后判断(do-while)
Java:
inti=0;do{log("i = "+i);i++;}while(i<3);对应 smali(循环体在前,跳过首次条件判断):
# int i = 0(i 放 v0,常量 3 放 v1) const/4 v0, 0x0 const/4 v1, 0x3 :loop_body # 循环体 const-string v2, "i = " invoke-static {v2, v0}, Lcom/test/Test;->log(Ljava/lang/String;I)V # i++ add-int/lit8 v0, v0, 0x1 # 执行后再判断:i < 3 则继续 if-lt v0, v1, :loop_body return-voidfor 循环完整示例
Java:
for(inti=0;i<3;i++){log("i = "+i);}对应 smali(编译器通常会把for转成"先判断 + 循环体 + 递增 + 回跳"的 while 结构,与上面的 while 完全一致):
# int i = 0(i 放 v0,常量 3 放 v1) const/4 v0, 0x0 const/4 v1, 0x3 :for_cond # 条件判断:i < 3 不成立则跳出 if-ge v0, v1, :for_end # 循环体 const-string v2, "i = " invoke-static {v2, v0}, Lcom/test/Test;->log(Ljava/lang/String;I)V # 递增 i++(即 for 的第三段) add-int/lit8 v0, v0, 0x1 goto :for_cond :for_end return-void逆向读循环的两个诀窍:① 先找循环条件判断所在的标签和"不符合才跳出的那条
if-*",它就对应while/for的条件;②add-int/lit8这类"自增/自减"指令紧跟循环体结尾,是找循环变量的标记。
compare 比较指令
long、double、float等不能直接用if-test比较,需要先用cmp指令把比较结果放进一个寄存器,再配合if-testz判断:
| 指令 | 语义 |
|---|---|
cmp-long vC, vA, vB | 比较两个 long:vA > vB 得 1,== 得 0,< 得 -1 |
cmpg-float/cmpg-double | 比较 float/double;处理 NaN 时按"更大"处理,得 1 |
cmpl-float/cmpl-double | 同上,但 NaN 视为更小,得 -1 |
示例:
# long a(long+long 占 2 组寄存器 v0/v1、v2/v3) cmp-long v4, v0, v2 # 比较,结果放入 v4 if-ltz v4, :cond_less # 若 v4 < 0(即 v0 < v2)则跳转packed-switch / sparse-switch:switch 语句
#define 整数开关放在 v0 中 # packed-switch(case 值连续) packed-switch v0, :pswitch_data_0 # sparse-switch(case 值不连续) sparse-switch v0, :sswitch_data_0对应的跳转表(payload)放在方法末尾,一旦执行到它:
:pswitch_data_0 .packed-switch 1 # case 起始值 :pswitch_1 # case 1 的目标 :pswitch_2 # case 2 的目标 :pswitch_3 # case 3 的目标 .end packed-switch :sswitch_data_0 .sparse-switch 1 -> :sswitch_1 # case 1 5 -> :sswitch_5 # case 5 10 -> :sswitch_10 # case 10 .end sparse-switch改造 App 时"开关跳转"很常见:如果某个
switch决定走哪个分支,可以改跳转表里的目标标签,或直接goto到想要的分支。逆向中常见的"改会员判断"常落在if-eqz/-nez上——把if-eqz(==0 跳转)改成if-nez(!=0 跳转)就能反转判断结果。
十一、字段的读取与赋值
字段读写指令按"实例/静态"和"数据类型"组合:
| 数据类型 | 实例读取 / 写入 | 静态读取 / 写入 |
|---|---|---|
| 引用类型 | iget-object/iput-object | sget-object/sput-object |
| int(boolean/byte/char/short 同理) | iget(iget-boolean等)/iput | sget/sput |
| long / double | iget-wide/iput-wide | sget-wide/sput-wide |
记忆方法:i开头 = instance 实例字段,s开头 = static 静态字段;get读,put写。
示例:
# 读取实例字段:String s = this.a; iget-object v0, p0, LTest;->a:Ljava/lang/String; # 写入实例字段:this.a = "hello"; const-string v0, "hello" iput-object v0, p0, LTest;->a:Ljava/lang/String; # 读取静态字段:int b = Test.b; sget v0, LTest;->b:I # 写入静态字段:Test.b = 10; const/16 v0, 0xa sput v0, LTest;->b:I十二、返回指令
| 指令 | 用途 |
|---|---|
return-void | 无返回值(对应void方法) |
return v0 | 返回 int/boolean/byte/char/short |
return-wide v0 | 返回 long/double |
return-object v0 | 返回引用类型对象 |
十三、综合实战:Hello World
Java 代码:
classTest{publicstaticvoidmain(String[]args){System.out.println("hello pangshu");}}对应完整 smali:
.class public LTest; .super Ljava/lang/Object; .method public constructor <init>()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # 程序入口 .method public static main([Ljava/lang/String;)V .registers 3 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; # 取得静态字段 System.out const-string v1, "hello pangshu" # 准备字符串参数 invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V return-void .end method说明:main是静态方法,p0为String[]参数;.registers 3表示 v0、v1 两个局部寄存器加上参数寄存器共 3 个。
附:常见低级错误清单
对照排查,可以避开手写/修改 smali 时 90% 的汇编报错:
.end method(不是methed);- 构造方法是
<init>、静态代码块是<clinit>(不是<int>); - 类型描述符
Ljava/lang/Object;、Ljava/lang/String;大小写不能错,且L开头;结尾; new-instance后必须紧跟invoke-direct调<init>;- 调用有返回值的方法后,要用
move-result(-object/-wide)接收; long/double占两个寄存器,注意后续寄存器编号要跳过;- 静态字段用
sget/sput,实例字段用iget/iput,接口方法用invoke-interface; - 跳转的标签(
:cond_0等)必须在同一方法内定义,且条件跳转的方向是"满足则跳",写if/else逻辑时判断条件通常要反过来; aget/aput是读/写,new-array前要const长度,对象数组元素要先逐个赋值(默认 null);.catch的 try 范围是半开区间,move-exception必须紧跟在异常处理标签处,且每个处理器只能取一次异常。
标签:Android逆向SmaliDEXapktool汇编安卓安全
分类:逆向工程 / 安卓安全