Smali 实质上就是 Java 字节码
概述
前提: 会java 会java 会java 重要的事情说三遍
Smali 实质上就是 Java 字节码,一句 Java 代码会对应多句 Smali 代码
例子:1
System.out.println("Hello World");
写为 Smali 就是:
1 | # 获取System类中的out字段,存到v0中 |
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 | .method 描述符 方法名(参数类型)返回类型 |
其中参数类型可以有0个或多个,返回类型必须是一个,当要表达多个参数类型时,只需简单地将它们连接到一起,例如:
(int, int, String)
表示为 (IILjava/lang/String;)
调用方法
Smali中必须以非常详细的形式指定要调用的方法,包括类名、方法名、参数类型和返回类型,其具体形式是:
类名->方法名(参数类型)返回类型
1 | System.out.println("Hello world"); |
1 | invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V |
指令名称 | 含义 |
---|---|
invoke-virtual | 调用虚方法 |
invoke-direct | 直接调用方法 |
invoke-static | 调用静态方法 |
invoke-super | 调用父类方法 |
invoke-interface | 调用接口方法 |
使用语法是:invoke-xxxxxx {参数列表}, 类名->方法名(参数类型)返回类型
所以当你在Smali代码中看到invoke开头的指令,就可以直接确定这句代码用于调用某个方法,至于invoke-后面跟着的单词,取决于它要调用的方法的类型。
调用虚方法
虚方法其实是Java多态中的一个概念,大家应该知道Java中子类可以重写父类中可被继承的非final方法,调用这些方法时,都需要使用invoke-virtual指令,才能实现多态的特性,例如下面代码:
1 | Object obj = "123"; |
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; |
当一个字段是 static
和 final
(静态常量)且类型为基本类型, 可以直接为他赋值:
1 | .field public static final ID:I = 0x7f0a0001 |
字段包含注解:1
2
3.field XXXXX
{注解列表}
.end field
引用字段
和调用方法类似,在引用一个字段时,也需要详细形式指定字段:
类名->字段名:字段类名
例如:
1 | System.out.println("Hello world") |
1 | # 在调用 println 方法前需要现将 System 类的字段 out 放到寄存器 v0 中 |
寄存器
v0、v1、v2、p0、p1、p2之类的标识符,这些都代表了寄存器。你可以把它认为是变量,或者是暂时存放东西的地方。
例子:
- 有一个静态方法
abc(String)
,如果你要在 Java 方法中调用这个方法,直接输入abc("Hello");
就行了。 - 而在Smali中,你不能直接把字符串参数传递给方法,你需要一个寄存器(比如v0),先把”Hello”放到v0中,然后再调用abc方法,并告诉它你需要的参数在v0里面,自己去拿吧。
1 | # 定义一个字符串常量"Hello"放到v0中 |
寄存器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,v0
和 v1
不会初始化。
普通寄存器 | 参数寄存器 | 初始化 |
---|---|---|
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 | public class Main{ |
test1()
和 test2()
的唯一区别就是一个是静态一个非静态
test1() 的 smali1
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() 的 smali1
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_ |