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
2
3
.class <访问权限> [关键修饰字] <类名>;
.super <父类名>;
.source <源文件名>;

  MainActivity.smali与MainActivity.java文件对比会发现.class关键字后跟着的是类名,public表示MainActivity这个类的属性。
  .super后跟着的是类的父类,与Java中的extends关键字后跟着的AppCompatActivity类一致。
  .source关键字后跟着的是当前类的源文件名。要注意的是文件名可以通过ProGUard优化器去除掉。

1
2
3
4
5
6
7
8
// MainActivity.smali
.class public Lcom/r0ysue/demo02/MainActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MainActivity.java"

//MainActivity.java
package com.r0ysue.demo02;
public class MainActivity extends AppCompatActivity {...}

  一个Java类主要由变量和函数构成,其中变量使用.field关键字声明,变量又分为静态变量和示例实例变量。MainActivity中有一个变量成员是String类型的total字段。其在smali文件中的声明,.field关键字后跟着成员变量的访问权限,与Java语言相同,访问权限可能是public,protect,private;在访问权限后实际上还可能存在一些修饰的关键字,如static、final等。变量存在static修饰词,那变量就是静态变量;修饰关键词后,剩下字段名称与对应的字段类型了,以<字段名>:<字段类型>的格式存在。

1
2
3
4
5
6
// MainActivity.smali
# instance fields
.field private total:Ljava/lang/String;

// MainActivity.java
private String total = "hello";

  最后是函数。smali文件中,函数的声明以.method关键词开始,以.end method关键词结束,格式如下

1
2
3
4
5
6
.method <访问权限> [修饰关键字] <方法原型>
<.locals/.registers>
[.param]
[.line]
<代码>
.end method

  观察发现,在.method关键词之后声明了访问权限与修饰关键字,其中访问权限与修饰关键词与变量相同,在这些关键字后跟着函数完整的签名,函数的签名主要由函数名、参数签名以及返回值类型唯一决定。
  此外,在函数声明的内容中还存在.locals、.param与.line等关键词。其中.locals关键词用于表示函数中非参数的变量的多少。有些编译器可能会使用.registers关键词,如果是.register关键词则表示函数中使用寄存器的数量,包括所有的p寄存器和v寄存器的数量,如Jadx就是使用.register关键词。.param关键词用于声明方法中的参数名;.line参数则保存着相应代码在Java源文件中行号信息,这个信息也可以通过ProGuard优化器去掉。

1
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
31
32
33
34
35
36
37
.method protected onCreate(Landroid/os/Bundle;)V
.locals 7
.param p1, "savedInstanceState" # Landroid/os/Bundle;

# invoke指令
.line 20
invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V

.line 21
sget v0, Lcom/r0ysue/demo02/R$layout;->activity_main:I

invoke-virtual {p0, v0}, Lcom/r0ysue/demo02/MainActivity;->setContentView(I)V

.line 23
new-instance v0, Ljava/util/Timer;

invoke-direct {v0}, Ljava/util/Timer;-><init>()V

iput-object v0, p0, Lcom/r0ysue/demo02/MainActivity;->mTimer:Ljava/util/Timer;

.line 24
iget-object v1, p0, Lcom/r0ysue/demo02/MainActivity;->mTimer:Ljava/util/Timer;

new-instance v2, Lcom/r0ysue/demo02/MainActivity$1;

invoke-direct {v2, p0}, Lcom/r0ysue/demo02/MainActivity$1;-><init>(Lcom/r0ysue/demo02/MainActivity;)V

# const指令
const-wide/16 v3, 0x0

const-wide/16 v5, 0x3e8

invoke-virtual/range {v1 .. v6}, Ljava/util/Timer;->scheduleAtFixedRate(Ljava/util/TimerTask;JJ)V

.line 32
return-void
.end method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// 这里调用你的函数
fun(50, 30);
Log.d("r0ysue.string",fun("LoWeRcAsE Me!!!!!"));
}
}, 0, 1000); // 0 毫秒后开始执行,然后每隔 1000 毫秒执行一次
}

  函数声明中,除了关键词剩下的就是函数的真实机器指令。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
    8
    void 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");
    }
    }
    1
    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 method
    smaliif-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分支。