Smali语法

Smali 实质上就是 Java 字节码

概述

前提: 会java 会java 会java 重要的事情说三遍

Smali 实质上就是 Java 字节码,一句 Java 代码会对应多句 Smali 代码

例子:

1
System.out.println("Hello World");

写为 Smali 就是:

1
2
3
4
5
6
7
8
9
# 获取System类中的out字段,存到v0中
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

# 把"Hello World"存到v1中
const-string v1, "Hello World"

# 调用虚方法println,传入参数v0, v1
# 相当于v0.println(v1)
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

Smali 文件

通常一个java文件中,可以定义多个类(含匿名内部类), 但一个 smali 只能定义一个类,smali 文件内容的格式如下:

  • .class 修饰符类名
  • .super 父类的类名
  • .source 源文件名
  • 实现的接口内容
  • 注解列表
  • 字段列表
  • 方法列表

详解

内容体 说明
.class 修饰符类名 public、private、protected、static、final等,和Java中的差不多,另外对于类还有interface和enum来表示这个类是一个接口或者枚举类
.super 父类的类名 L包名路径/类名;,例如Android中的TextView类,它的包名是android.widget,如果你要在Smali中表示这个类,就要写成Landroid/widget/TextView
.source 源文件名 编译这个类的java文件名,如Main.java,仅用于debug删了也没影响
接口语法 .implements 接口类名 可以有0个或者多个,表示这个类实现了哪些接口
注解 Java代码中@XXX之类的代码,例如比较常见的@Override@Nullable@NonNull Smali语法是 .annotation xx xxxxxx .end annotation 类似模板语法涵括到其中。关键词 annotation
字段 字段就是 field,方法就是 method

类型

基本类型

其中除了boolean对应Z,long对应J,其它都是对应首个字母大写,还是很好记的

Java Smali
void V
boolean Z (不同)
byte B
short S
char C
int I
long J (不同)
float F
double D

数组

Smali中通过在类型前面加[来表示该类型的数组,例如[I表示int[],[Ljava/lang/String;表示String[],如果要表示多维数组,只需要增加[的数量,例如[[I表示二维数组int[][]

例子:

  • [I 表示 int[]
  • [Ljava/lang/String; 表示 String[]

引用类型

在Smali中都是用L包名路径/类名;表示,例如Android中的TextView类,它的包名是 android.widget,如果你要在Smali中表示这个类,就要写成 Landroid/widget/TextView

  • 包名 android.widget
  • Smali Landroid/widget/TextView

方法

Smali中定义方法的语法是:

1
2
3
.method 描述符 方法名(参数类型)返回类型
方法代码...
.end method

其中参数类型可以有0个或多个返回类型必须是一个,当要表达多个参数类型时,只需简单地将它们连接到一起,例如:

(int, int, String) 表示为 (IILjava/lang/String;)

调用方法

Smali中必须以非常详细的形式指定要调用的方法,包括类名、方法名、参数类型和返回类型,其具体形式是:

类名->方法名(参数类型)返回类型

1
2
System.out.println("Hello world");
// 其中out是System的一个静态字段,它的类型是PrintStream,println是PrintStream中的一个方法
1
2
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
# 后面那部分正是方法println的完整表达形式
指令名称 含义
invoke-virtual 调用虚方法
invoke-direct 直接调用方法
invoke-static 调用静态方法
invoke-super 调用父类方法
invoke-interface 调用接口方法

使用语法是:invoke-xxxxxx {参数列表}, 类名->方法名(参数类型)返回类型

所以当你在Smali代码中看到invoke开头的指令,就可以直接确定这句代码用于调用某个方法,至于invoke-后面跟着的单词,取决于它要调用的方法的类型。

调用虚方法

虚方法其实是Java多态中的一个概念,大家应该知道Java中子类可以重写父类中可被继承的非final方法,调用这些方法时,都需要使用invoke-virtual指令,才能实现多态的特性,例如下面代码:

1
2
Object obj = "123";
obj.equals("456");
1
invoke-virtual {v0, v1}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z

表面上看是调用Object的equals方法,但是由于obj实际上是字符串“123”,而字符串类String中重写了equals方法,所以虚拟机最后调用的是String的equals方法。

直接调用方法

由于调用虚方法时,虚拟机需要先查找该方法是否被重写,而对于那些无法被重写的方法,查找显得是在浪费时间,所以使用invoke-direct指令来提高效率,其通常用于final方法、private方法、构造方法。

调用静态方法

调用static方法时,就使用invoke-static。

调用父类方法

在子类中,如果它已经重写了父类的XX方法,而又想调用父类的XX方法时,可通过super.XX()来调用,其对于的指令就是 invoke-super

调用接口方法

invoke-xxxxxx {参数列表}, 类名->方法名(参数类型)返回类型

如果类名对应的类是个接口,那么xxxxxx就得写 interface

字段

Smali中定义方法的语法是:

1
.field 描述符 字段名:字段类型

例如:

1
public String text;
1
.field public text:Ljava/lang/String;

当一个字段是 staticfinal (静态常量)且类型为基本类型, 可以直接为他赋值:

1
.field public static final ID:I = 0x7f0a0001

字段包含注解:

1
2
3
.field XXXXX
{注解列表}
.end field

引用字段

和调用方法类似,在引用一个字段时,也需要详细形式指定字段:

类名->字段名:字段类名

例如:

1
System.out.println("Hello world")
1
2
3
4
5
# 在调用 println 方法前需要现将 System 类的字段 out 放到寄存器 v0 中
# 也就是下面这句代码:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

# 后面那部分正是字段 out 的完整表达形式

寄存器

v0、v1、v2、p0、p1、p2之类的标识符,这些都代表了寄存器。你可以把它认为是变量,或者是暂时存放东西的地方。

例子:

  • 有一个静态方法 abc(String),如果你要在 Java 方法中调用这个方法,直接输入 abc("Hello"); 就行了。
  • 而在Smali中,你不能直接把字符串参数传递给方法,你需要一个寄存器(比如v0),先把”Hello”放到v0中,然后再调用abc方法,并告诉它你需要的参数在v0里面,自己去拿吧。
1
2
3
4
# 定义一个字符串常量"Hello"放到v0中
const-string v0, "Hello"
# 调用abc方法,需要的参数放在v0中
invoke-static {v0}, LXX;->abc(Ljava/lang/String;)V

寄存器v0、v1、v2后面的数字也不是随便写的,需要在方法的开头用.registers N来指定寄存器的数量,然后才可以使用寄存器v0到v(N-1)。就像C中申请内存同理

参数寄存器

上面说的都是普通寄存器vN,另外Smali还特意定义了一种参数寄存器pN,用于存放这个方法传入的参数的值。

如果一个方法有n个寄存器,有m个参数,那么n必须大于等于m,并且n个寄存器的后面m个是参数寄存器,举个例子:

某个静态方法abc(int, int, int),它一共有3个参数,如果它一共有5个寄存器(通过.registers N定义,N不能小于3)。

普通寄存器 对应参数寄存器
v0
v1
v2 p0
v3 p1
v4 p2

当我调用 abc(11, 22, 33) 时,p0 中的值初始化为11,p1 中的值初始化为22,p2 中的值初始化为33,v0v1 不会初始化。

普通寄存器 参数寄存器 初始化
v0
v1
v2 p0 11
v3 p1 22
v4 p2 33

当把寄存器数量改成6(.registers 6),寄存器就会变成下表所示:

普通寄存器 参数寄存器 初始化
v0
v1
v2
v3 p0 11
v4 p1 22
v5 p2 33

隐藏的参数

非静态方法,它的参数寄存器数量参数多了一个,p0会固定用于表示当前类实例(this),从p1开始才是真正的参数,Java代码如下

1
2
3
4
5
6
7
8
9
10
11
12
public class Main{

static void test1(String s, int i){
System.out.println(s);
System.out.println(i);
}

void test2(String s, int i){
System.out.println(s);
System.out.println(i);
}
}

test1()test2() 的唯一区别就是一个是静态一个非静态

test1() 的 smali

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.method static test1(Ljava/lang/String;I)V
.registers 3

.prologue
.line 4
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v0, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

.line 5
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v0, p1}, Ljava/io/PrintStream;->println(I)V

.line 6
return-void
.end method

test2() 的 smali

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.method test2(Ljava/lang/String;I)V
.registers 4

.prologue
.line 9
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v0, p1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

.line 10
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v0, p2}, Ljava/io/PrintStream;->println(I)V

.line 11
return-void
.end method

两个方法都是依次打印出两个参数,test1()中打印第一个参数用的是p0,打印第二个参数用的是p1;对照下test2()中则分别用的是p1和p2。

附:常见smali对应语义表

后面有空会继续完善

基础语法参照表

语法 语义
.field private isFlag:z 定义变量
.method 方法
.parameter 方法参数
.prologue 方法开始
.line 12 此方法位于第12行
invoke-super 调用父函数
const/high16 v0, 0x7fo3 把0x7fo3赋值给v0
invoke-direct 调用函数
return-void 函数返回void
.end method 函数结束
new-instance 创建实例
iput-object 对象赋值
iget-object 调用对象
invoke-static 调用静态函数

条件跳转分支表

语法 语义
if-eq vA, vB, :cond_ 如果vA等于vB则跳转到:cond_
if-ne vA, vB, :cond_ 如果vA不等于vB则跳转到:cond_
if-lt vA, vB, :cond_ 如果vA小于vB则跳转到:cond_
if-ge vA, vB, :cond_ 如果vA大于等于vB则跳转到:cond_
if-gt vA, vB, :cond_ 如果vA大于vB则跳转到:cond_
if-le vA, vB, :cond_ 如果vA小于等于vB则跳转到:cond_
if-eqz vA, :cond_ 如果vA等于0则跳转到:cond_
if-nez vA, :cond_ 如果vA不等于0则跳转到:cond_
if-ltz vA, :cond_ 如果vA小于0则跳转到:cond_
if-gez vA, :cond_ 如果vA大于等于0则跳转到:cond_
if-gtz vA, :cond_ 如果vA大于0则跳转到:cond_
if-lez vA, :cond_ 如果vA小于等于0则跳转到:cond_