Smali 基础知识

Smali 是什么?

简介

Smali 是用于 Dalvik(Android 虚拟机)的反汇编程序实现,汇编工具(将 Smali 代码汇编为 dex 文件)为 smali.jar,与之对应的 baksmali.jar 则是反汇编程序(下载地址),官方所说的基于 Jasmin/dedexer 语法,实际根不知道是什么鬼……

Smali 支持注解、调试信息、行数信息等基本 Java 的基本特性,可以说是很接近 Java 编译在 JVM 上的中间语言了,一般用来做 Android 程序的逆向工程,还可以。。搞搞小名堂

个人认为 Smali 只是用于做反汇编的一种语言实现,如果可以,自己也能定义一套这样的语言,实现反汇编的效果

Smali 基础

下面的内容涉及一些 Smali 编程的结构和基本语法,这些基本语法,在使用 Smali 修改 App 逻辑时需要用到

Smali 文件结构

一个 Smali 文件对应的是一个 Java 的类,更准确的说是一个.class 文件,如果有内部类,需要写成ClassName$InnerClassAClassName$InnerClassB…这样的形式

基本类型

类型关键字 对应 Java 中的类型说明
V void,只能用于返回类型
Z boolean
B byte
S short
C char
I int
J long (64 bits)
F float
D double (64 bits)

对象

Object 类型,即引用类型的对象,在引用时,使用 L 开头,后面紧接着的是完整的包名,比如:java.lang.String对应的 Smali 语法则是Ljava/lang/String

数组

数组定义比较有意思,一维数组在类型的左边加一个方括号,比如:[I等同于 Java 的int[],每多一维就加一个方括号,最多可以设置 255 维。。。

方法声明及调用

官方 Wiki 中给出的 Smali 引用方法的模板如下:

1
Lpackage/name/ObjectName;->MethodName(III)Z

第一部分Lpackage/name/ObjectName;用于声明具体的类型,以便 JVM 寻找

第二部分MethodName(III)Z,其中 MethodName 为具体的方法名,()中的字符,表示了参数数量和类型,即 3 个 int 型参数,Z 为返回值的类型,即返回 Boolean 类型

由于方法的参数列表没有使用逗号这样的分隔符进行划分,所以只能从左到右,根据类型定义来区分参数个数,这一点需要比较仔细来观察

如果需要调用构造方法,则 MethodName 为:<init>

寄存器声明及使用

在 Smali 中,如果需要存储变量,必须先声明足够数量的寄存器,1 个寄存器可以存储 32 位长度的类型,比如 Int,而两个寄存器可以存储 64 位长度类型的数据,比如 Long 或 Double

声明可使用的寄存器数量的方式为:.registers N,N 代表需要的寄存器的总个数,同时,还有一个关键字.locals,它用于声明非参数的寄存器个数(包含在 registers 声明的个数当中),也叫做本地寄存器,只在一个方法内有效,但不常用,一般使用 registers 即可

示例:

1
2
3
4
5
6
7
8
9
.method private test(I)V
.registers 4 # 声明总共需要使用 4 个寄存器

const-string v0, "LOG" # 将 v0 寄存器赋值为字符串常量"LOG"

move v1, p1 # 将 int 型参数的值赋给 v1 寄存器

return-void
.end method

结合 Dalvik 常用的指令进行操作,即可实现一些需要的功能

那么,如何确定需要使用的寄存器的个数?

由于非 static 方法,需要占用一个寄存器以保存 this 指针,那么这类方法的寄存器个数,最低就为 1,如果还需要处理传入的参数,则需要再次叠加,此时还需要考虑 Double 和 Float 这种需要占用两个寄存器的参数类型,举例来看:

如果一个 Java 方法声明如下:

1
myMethod(int p1, float p2, boolean p3)

那么对应的 Smali 则为:

1
method LMyObject;->myMethod(IJZ)V

此时,寄存器的对应情况如下:

寄存器名称 对应的引用
p0 this
p1 int 型的 p1 参数
p2, p3 float 型的 p2 参数
p4 boolean 型的 p3 参数

那么最少需要的寄存器个数则为:5

如果方法体内含有常量、变量等定义,则需要根据情况增加寄存器个数,数量只要满足需求,保证需要获取的值不被后面的赋值冲掉即可,方法有:存入类中的字段中(存入后,寄存器可被重新赋值),或者长期占用一个寄存器

Dalvik 指令集

如果需要使用 Smali 编写程序,还需要掌握常用的 Dalvik 虚拟机指令,其合集称为 Dalvik 指令集。这些指令有点类似 x86 汇编的指令,但指令更多,使用也非常简单方便。最详尽的介绍,可以参考 Android 官方的 Dalvik 相关文档:https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions

一般的指令格式为:[op]-[type](可选)/[位宽,默认 4 位] [目标寄存器],[源寄存器](可选),比如:move v1,v2move-wide/from16 v1,v2

这里也列举一些常用的指令,并结合 Smali 进行说明:

  • 移位操作:

此类操作常用于赋值

指令 说明
move v1,v2 将 v2 中的值移入到 v1 寄存器中(4 位,支持 int 型)
move/from16 v1,v2 将 16 位的 v2 寄存器中的值移入到 8 位的 v1 寄存器中
move/16 v1,v2 将 16 位的 v2 寄存器中的值移入到 16 位的 v1 寄存器中
move-wide v1,v2 将寄存器对(一组,用于支持双字型)v2 中的值移入到 v1 寄存器对中(4 位,猜测支持 float、double 型)
move-wide/from16 v1,v2 将 16 位的 v2 寄存器对(一组)中的值移入到 8 位的 v1 寄存器中
move-wide/16 v1,v2 将 16 位的 v2 寄存器对(一组)中的值移入到 16 位的 v1 寄存器中
move-object v1,v2 将 v2 中的对象指针移入到 v1 寄存器中
move-object/from16 v1,v2 将 16 位的 v2 寄存器中的对象指针移入到 v1(8 位)寄存器中
move-object/16 v1,v2 将 16 位的 v2 寄存器中的对象指针移入到 v1(16 位)寄存器中
move-result v1 将这个指令的上一条指令计算结果,移入到 v1 寄存器中(需要配合 invoke-static、invoke-virtual 等指令使用)
move-result-object v1 将上条计算结果的对象指针移入 v1 寄存器
move-result-wide v1 将上条计算结果(双字)的对象指针移入 v1 寄存器
move-exception v1 将异常移入 v1 寄存器,用于捕获 try-catch 语句中的异常
  • 返回操作:

用于返回值,对应 Java 中的 return 语句

指令 说明
return-void 返回 void,即直接返回
return v1 返回 v1 寄存器中的值
return-object v1 返回 v1 寄存器中的对象指针
return-wide v1 返回双字型结果给 v1 寄存器
  • 常量操作:

用于声明常量,比如字符串常量(仅声明,String a = “abc”这种语句包含声明和赋值)

指令 说明
const(/4、/16、/hight16) v1 xxx 将常量 xxx 赋值给 v1 寄存器,/后的类型,需要根据 xxx 的长度选择
const-wide(/16、/32、/hight16) v1 xxx 将双字型常量 xxx 赋值给 v1 寄存器,/后的类型,需要根据 xxx 的长度选择
const-string(/jumbo) v1 “aaa” 将字符串常量”aaa”赋给 v1 寄存器,过长时需要加上 jumbo
const-class v1 La/b/TargetClass 将 Class 常量 a.b.TargetClass 赋值给 v1,等价于 a.b.TargetClass.class
  • 调用操作:

用于调用方法,基本格式:invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB,其中,BBBB 代表方法引用(参见上面介绍的方法定义及调用),vC~G 为需要的参数,根据顺序一一对应

指令 说明
invoke-virtual 用于调用一般的,非 private、非 static、非 final、非构造函数的方法,它的第一个参数往往会传 p0,也就是 this 指针
invoke-super 用于调用父类中的方法,其他和 invoke-virtual 保持一致
invoke-direct 用于调用 private 修饰的方法,或者构造方法
invoke-static 用于调用静态方法,比如一些工具类
invoke-interface 用于调用 interface 中的方法
  • 判断操作:

判断操作用来比较一个寄存器中的值,是否与目标寄存器中的值相等或不等,对应 Java 中的 if 语句,格式为:if-[test] v1,v2, [condition],其衍生操作还有专门与 0 进行比较的if-[test]z v1, [condition],其中[condition]为符合判断结果后的跳转条件,需要提前定义好。判断操作也通常和 goto 配合使用,用来实现循环或者 if-else 语句

指令 说明
if-eq v1,v2 判断两个寄存器中的值是否相等
if-ne v1,v2 判断两个寄存器中的值是否不相等
if-lt v1,v2 判断 v1 寄存器中的值是否小于 v2 寄存器中的值(lt == less than)
if-ge v1,v2 判断 v1 寄存器中的值是否大于或等于 v2 寄存器中的值(ge == great than or equals)
if-gt v1,v2 判断 v1 寄存器中的值是否大于 v2 寄存器中的值(gt == great than)
if-le v1,v2 判断 v1 寄存器中的值是否小于或等于 v2 寄存器中的值(le == less than or equals)

需要注意的是,在 Java 中编写的 if 语句,往往在对应的 Smali 中,会变成相反的判断逻辑,如下面所示:

1
2
3
4
5
6
7
8
9
10
private void test() {
int a = 0;
int b = 1;
String result;
if (a > b) {
result = "a great than b";
} else {
result = "a less than or equals b";
}
}

上面的 Java 代码逻辑很简单——一个很简单的 if 语句,为了在 Smali 中看的更清楚,我只做了字符串赋值操作。下面是对应的 Smali 代码:

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
.method private test()V
.registers 4

.line 24
const/4 v0, 0x0

.line 25
.local v0, "a":I
const/4 v1, 0x1

.line 27
.local v1, "b":I
if-le v0, v1, :cond_7

.line 28
const-string v2, "a great than b"

.line 28
.local v2, "result":Ljava/lang/String;
goto :goto_9

.line 30
.end local v2 # "result":Ljava/lang/String;
:cond_7
const-string v2, "a less than or equals b"

.line 32
.restart local v2 # "result":Ljava/lang/String;
:goto_9
return-void
.end method

仔细观察可以发现:

if 判断

  • 属性操作:

属性操作的分为:取值(get)和赋值(put)

目标类型分为:数组(array)、实例(instance)和静态(static)三种,对应的缩写前缀就是 a、i、s

长度类型分为:默认(什么都不写)、wide(宽,64 位)、object(对象)、booleanbytecharshort(后面几种就不解释了,和 Java 一致)

指令格式:[指令名] [源寄存器], [目标字段所在对象寄存器], [字段指针],示例代码如下,操作是为 int 型的类成员变量mIntA赋值为100

1
2
3
const/16 v0, 0x64

iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I

下面列出用于实例字段的指令,其中 i 都可以换成 a 或者 s,分别用于操作数组字段或者静态字段

指令 说明
iget 取值,用于操作 int 这种的值类型
iget-wide 取值,用于操作 wide 型字段
iget-object 取值,用于操作对象引用
iget-boolean 取值,用于操作布尔类型
iget-byte 取值,用于操作字节类型
iget-char 取值,用于操作字符类型
iget-short 取值,用于操作 short 类型
iput 赋值,用于操作 int 这种的值类型
iput-wide 赋值,用于操作 wide 型字段
iput-object 赋值,用于操作对象引用
iput-boolean 赋值,用于操作布尔类型
iput-byte 赋值,用于操作字节类型
iput-char 赋值,用于操作字符类型
iput-short 赋值,用于操作 short 类型

举例:

以下 Java 代码是进行的是最基本的类成员变量的赋值、取值操作

1
2
3
4
5
6
7
8
9
10
11
private String mStringA;
private int mIntA;
private Activity mActivityA;

public void fieldTest() {
mStringA = "Put String to mStringA";
mIntA = 100;
mActivityA = this;

int len = mStringA.length();
}

对应的 Smali 代码如下:

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
# instance fields
.field private mActivityA:Landroid/app/Activity;

.field private mIntA:I

.field private mStringA:Ljava/lang/String;

# virtual methods
.method public fieldTest()V
.registers 2

.line 55
const-string v0, "Put String to mStringA"

iput-object v0, p0, Lcom/coderyuan/smali/MainActivity;->mStringA:Ljava/lang/String;

.line 56
const/16 v0, 0x64

iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I

.line 57
iput-object p0, p0, Lcom/coderyuan/smali/MainActivity;->mActivityA:Landroid/app/Activity;

.line 59
iget-object v0, p0, Lcom/coderyuan/smali/MainActivity;->mStringA:Ljava/lang/String;

invoke-virtual {v0}, Ljava/lang/String;->length()I

move-result v0

.line 60
.local v0, "len":I
return-void
.end method

根据 Java 和 Smali 代码的对比,值得注意的是,Smali 获取类成员变量的方法,比较接近函数调用,只不过没有函数调用时的参数

  • 其他指令:

除以上介绍的几种基本的 Dalvik 指令外,Dalvik 还支持值类型转换(如:int 转 float,double 转 float 等)、基本运算(数学运算、逻辑运算、自增)两种指令集,这里只列举一些常用的指令,其他的可以参考上面提到的 Google 官方文档

指令 说明
add-int/lit8 v1, v2, 0x1 给 v2 寄存器+1,并存入 v1 寄存器(注意:lit8 是对要加的常量的长度限制,如果不写,则为 4 位,还可选择 lit16,即 16 位)
add-int/2addr v1, v2 将 v1、v2 寄存器中的值相加,并赋值给 v1 寄存器
float-to-int v1, v2 将 v2 寄存器中的 float 类型值转换为 int 类型,并赋值给 v1 寄存器

Smali 能干什么?

虽然我们了解了 Smali 的基本语法,但一般不会直接编写 Smali 来进行功能开发,这样成本过高,而了解 Smali 的目的,是为了做 Android 的逆向工程,如:分析 APP 的原理、漏洞检测,当然,也可以对一些 APP 做一些小改动(最好不要做一些伤天害理、违法乱纪、损人不利己的事),具体如何进行逆向,我将在下一篇 Smali 相关的文章中进一步介绍!