Smali 语法基础
2026/9/12 21:30:50 网站建设 项目流程

Smali 语法基础

Smali 是 DEX 字节码的汇编语言表示:APK 中的 DEX 文件经 baksmali/apktool 反汇编后得到.smali文件,每条 smali 指令与 DEX 字节码一一对应、语义等价。修改 smali 后可以重新汇编成 DEX 并打包回 APK——这也是修改 App 功能(去广告、改会员校验等)的基本手段。

本文按"类 → 字段 → 方法 → 调用 → 对象 → 数据 → 数组 → 异常 → 跳转 → 字段读写 → 综合实战"的顺序整理常用语法。


一、基本语法规则

注释

#开头:

# 这是一行注释

寄存器:v 寄存器与 p 寄存器

Smali 是面向寄存器的语言,所有数据都放在寄存器中操作,寄存器命名为v0v1v2……

方法中可用寄存器数量由两条伪指令之一声明:

.locals 2 # 方法内自定义(局部)寄存器有 2 个:v0、v1 .registers 3 # 方法总共使用 3 个寄存器(含参数寄存器),二选一即可

参数寄存器以p命名,与v寄存器的换算关系:

  • 非静态方法p0表示thisp1p2……依次表示方法参数;
  • 静态方法:没有thisp0就是第一个参数;
  • longdouble占两个寄存器。

类型描述符

方法签名和字段类型中使用的是类型描述符,而不是 Java 类名:

Java 类型描述符
voidV
booleanZ
byteB
shortS
charC
intI
longJ
floatF
doubleD
引用类型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 method

Java 代码:

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 method

Java 代码:

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>()V

2. 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 method

3. 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 method

4. 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;)Z

5. 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 v0

6. 接收方法返回值

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(占两个连续寄存器)

longdouble是 64 位数据,占用两个相邻寄存器:

const-wide v0, 0x1L # 占用 v0 和 v1 两个寄存器

对应的变体还有const-wide/16const-wide/32const-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 → vCint 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/doublearr[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 method

throw 显式抛出异常

# 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 结构)。读懂.catchmove-exception,才能在 Hook/分析时准确定位"哪里抛异常、异常被谁吃掉"。


十、跳转指令(控制流)

跳转指令用于改变程序的执行顺序,实现if / elseswitchwhilefor等控制流。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, :lvA == vB 时跳转if (a == b)
if-ne vA, vB, :lvA != vB 时跳转if (a != b)
if-lt vA, vB, :lvA < vB 时跳转if (a < b)
if-ge vA, vB, :lvA >= vB 时跳转if (a >= b)
if-gt vA, vB, :lvA > vB 时跳转if (a > b)
if-le vA, vB, :lvA <= vB 时跳转if (a <= b)
if-eqz vA, :lvA == 0 时跳转if (a == 0)
if-nez vA, :lvA != 0 时跳转if (a != null)
if-ltz vA, :lvA < 0 时跳转if (a < 0)
if-gez vA, :lvA >= 0 时跳转if (a >= 0)
if-gtz vA, :lvA > 0 时跳转if (a > 0)
if-lez vA, :lvA <= 0 时跳转if (a <= 0)
  • if-test系列比较两个寄存器
  • if-testz系列只比较一个寄存器与 0z表示 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-void

while 循环完整示例

形式一:先判断后执行(正常 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-void

for 循环完整示例

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 比较指令

longdoublefloat等不能直接用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-objectsget-object/sput-object
int(boolean/byte/char/short 同理)igetiget-boolean等)/iputsget/sput
long / doubleiget-wide/iput-widesget-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是静态方法,p0String[]参数;.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汇编安卓安全

分类:逆向工程 / 安卓安全

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询