Smali 实质上就是 Java 字节码

概述

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

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

例子:

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

写为 Smali 就是:

# 获取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,其它都是对应首个字母大写,还是很好记的
JavaSmali
voidV
booleanZ (不同)
byteB
shortS
charC
intI
longJ (不同)
floatF
doubleD

数组

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中定义方法的语法是:

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

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

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

调用方法

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

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

System.out.println("Hello world");
// 其中out是System的一个静态字段,它的类型是PrintStream,println是PrintStream中的一个方法
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指令,才能实现多态的特性,例如下面代码:
Object obj = "123";
obj.equals("456");
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中定义方法的语法是:

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

例如:

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

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

.field public static final ID:I = 0x7f0a0001

字段包含注解:

.field XXXXX
    {注解列表}
.end field

引用字段

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

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

例如:

 System.out.println("Hello world")
# 在调用 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里面,自己去拿吧。
# 定义一个字符串常量"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
v2p0
v3p1
v4p2

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

普通寄存器参数寄存器初始化
v0
v1
v2p011
v3p122
v4p233

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

普通寄存器参数寄存器初始化
v0
v1
v2
v3p011
v4p122
v5p233

隐藏的参数

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

.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

.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_
Last modification:July 20, 2020
如果觉得我的文章对你有用,请随意赞赏