ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Smali 语法基础

2026/9/12 21:31:05 拓冰建站 浏览量
Smali 语法基础 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表示thisp1、p2……依次表示方法参数静态方法没有thisp0就是第一个参数long和double占两个寄存器。类型描述符方法签名和字段类型中使用的是类型描述符而不是 Java 类名Java 类型描述符voidVbooleanZbyteBshortScharCintIlongJfloatFdoubleD引用类型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{privatestaticfinalStringahello;}对应 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{publicstaticStringaa;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(){returnhello;}}对应 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: ListString 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 位如 int0xA 即十进制 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 → 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[]namesnewString[2];names[0]a;Stringfirstnames[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-lengtharray-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通常是反汇编工具自动生成的友好名字你可以在指令后用:标签自定义。无条件跳转 gotogoto :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表示 zeroif-nez常用来判断对象是否非空。示例——Java 的if语句if(num0){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 循环完整示例形式一先判断后执行正常 whileJavainti0;// i 存放于 v0while(i3){log(i i);i;}对应 smali进入循环前先判断一次防止一次都不执行# int i 0i 放 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-whileJavainti0;do{log(i i);i;}while(i3);对应 smali循环体在前跳过首次条件判断# int i 0i 放 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 循环完整示例Javafor(inti0;i3;i){log(i i);}对应 smali编译器通常会把for转成先判断 循环体 递增 回跳的 while 结构与上面的 while 完全一致# int i 0i 放 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比较两个 longvA vB 得 1 得 0 得 -1cmpg-float/cmpg-double比较 float/double处理 NaN 时按更大处理得 1cmpl-float/cmpl-double同上但 NaN 视为更小得 -1示例# long a(longlong 占 2 组寄存器 v0/v1、v2/v3) cmp-long v4, v0, v2 # 比较结果放入 v4 if-ltz v4, :cond_less # 若 v4 0即 v0 v2则跳转packed-switch / sparse-switchswitch 语句#define 整数开关放在 v0 中 # packed-switchcase 值连续 packed-switch v0, :pswitch_data_0 # sparse-switchcase 值不连续 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-eqz0 跳转改成if-nez!0 跳转就能反转判断结果。十一、字段的读取与赋值字段读写指令按实例/静态和数据类型组合数据类型实例读取 / 写入静态读取 / 写入引用类型iget-object/iput-objectsget-object/sput-objectintboolean/byte/char/short 同理igetiget-boolean等/iputsget/sputlong / 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/shortreturn-wide v0返回 long/doublereturn-object v0返回引用类型对象十三、综合实战Hello WorldJava 代码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汇编安卓安全分类逆向工程 / 安卓安全