smali基础语法
基础知识
在Java字节码中,寄存器都是32位的,能够支持任何类型,64位类型(Long/Double)用两个寄存器表示,寄存器的命名方式有p命名法和v命名法。p命名法通常用与表示函数参数,比如p0、p1等,v命名法用于表示函数内部的变量,比如v0、v1等。
数据类型有两种,基本数据类型和引用类型。
数据类型 | Java文件中的数据表示方式 |
---|---|
V | void |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
引用类型中的对象类型通常以“Lpackage/name/ObjectName;”的形式表示。其中L表示这是一个对象类型,“;”标识对象名称的结束,中间的package/name表示对象的包名,ObjectName表示对象名。最终这样的形式相比于Java源代码中package.name.ObjectName类的完整表示,比如com.r0ysue.demo02.MainActivity类,最终表示”Lcom/r0ysue/demo02/MainActivity;”。 | |
数组通常只需要在类型的前面加上“[”即可。比如,整型数据int []用smali语言的形式就表示[I,对象数组也是类似,[Ljava/lang/String表示一个String字符串的数组类型。 | |
Smali文件中,“#”用于注释,类似于编写Java代码时的“//”符号。 |
看一个smali文件
打开smali文件,最上面三行描述当前类的一些信息格式如下
1 | .class <访问权限> [关键修饰字] <类名>; |
MainActivity.smali与MainActivity.java文件对比会发现.class关键字后跟着的是类名,public表示MainActivity这个类的属性。
.super后跟着的是类的父类,与Java中的extends关键字后跟着的AppCompatActivity类一致。
.source关键字后跟着的是当前类的源文件名。要注意的是文件名可以通过ProGUard优化器去除掉。
1 | // MainActivity.smali |
一个Java类主要由变量和函数构成,其中变量使用.field关键字声明,变量又分为静态变量和示例实例变量。MainActivity中有一个变量成员是String类型的total字段。其在smali文件中的声明,.field关键字后跟着成员变量的访问权限,与Java语言相同,访问权限可能是public,protect,private;在访问权限后实际上还可能存在一些修饰的关键字,如static、final等。变量存在static修饰词,那变量就是静态变量;修饰关键词后,剩下字段名称与对应的字段类型了,以<字段名>:<字段类型>的格式存在。
1 | // MainActivity.smali |
最后是函数。smali文件中,函数的声明以.method关键词开始,以.end method关键词结束,格式如下
1 | .method <访问权限> [修饰关键字] <方法原型> |
观察发现,在.method关键词之后声明了访问权限与修饰关键字,其中访问权限与修饰关键词与变量相同,在这些关键字后跟着函数完整的签名,函数的签名主要由函数名、参数签名以及返回值类型唯一决定。
此外,在函数声明的内容中还存在.locals、.param与.line等关键词。其中.locals关键词用于表示函数中非参数的变量的多少。有些编译器可能会使用.registers关键词,如果是.register关键词则表示函数中使用寄存器的数量,包括所有的p寄存器和v寄存器的数量,如Jadx就是使用.register关键词。.param关键词用于声明方法中的参数名;.line参数则保存着相应代码在Java源文件中行号信息,这个信息也可以通过ProGuard优化器去掉。
1 | .method protected onCreate(Landroid/os/Bundle;)V |
1 | protected void onCreate(Bundle savedInstanceState) { |
函数声明中,除了关键词剩下的就是函数的真实机器指令。smali指令很多,这里介绍几个重要的指令。
smali指令大致可以分为常量操作指令、方法调用指令、移位指令和分支判断指令等。
- 常量操作指令
主要是const相关指令,通常用于声明一个常量,const关键词后可能存在常量类型相关的关键词,如const-wide/16 v5, 0x3e8
定义一个wide类型(64位long)的0x3e8保存到v5寄存器中,而/16指从指令的第16个字节开始的常量池索引。 - 方法调用指令是以incoke关键词开头的一类指令,通常用于调用外部函数,其基本格式为
invoke-kind{vA,vB,vC},method@DDDD
。method@DDDD表示函数的引用;vA,vB,vC则表示函数的参数,其定义顺序与调用函数的参数一一对应;kind则代表被调用的类型,主要有static、virtual、super(被调用的函数是静态函数、正常的函数和父类函数等)。如invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V
指令对应Java文件中的super.onCreat(savedInstanceState);,代表调用MainActivity父类的onCreate(savedInstanceState)函数,第一个参数p0代表this指针,第二个参数p1代表savedInstanceState参数; - 移位指令是关于move的相关指令,通常用于赋值。move关键字后也可能会存在寄存器类型的关键词,如
move-result-object v0
指令就表示将上一步的函数返回值保存到v0寄存器中。 - 分支判断指令以if关键词为标志,用于比较一个寄存器中的值与目标寄存器中的值,与Java中的if语句对应,指令格式为if-[test]v1,v2,[condition]。
1
2
3
4
5
6
7
8void testif(int b){
int a=1;
if(b<a){
Log.d("r0ysue666","a is greater than b");
}else {
Log.d("r0ysue666","a is less than b");
}
}smali1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30.method testif(I)V
.locals 3
.param p1, "b" # I
.line 47
const/4 v0, 0x1
.line 48
.local v0, "a":I
const-string v1, "r0ysue666"
if-ge p1, v0, :cond_0
.line 49
const-string v2, "a is greater than b"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.line 51
:cond_0
const-string v2, "a is less than b"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 53
:goto_0
return-void
.end methodif-ge p1, v0, :cond_0
指令用于判断p1寄存器与v0寄存器中值的大小,而if关键词后的ge则可以认为是greater than的缩写,表示如果p1寄存器中的值大于v0寄存器中的值,则跳转到:cond_0标记的地方。若仔细观察,可发现p1寄存器中存储的是参数b,v0寄存器中存储着1这个变量,最终的实际意义就是:如果p1寄存器中的值大于1,跳转到cond_0所指示的位置,并调用Log.d()函数。其中Log.d()函数第二个参数的内容为”a is less than or equal b”,对应Java部分的else分支。