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$InnerClassA
、ClassName$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 | .method private test(I)V |
结合 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,v2
,move-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 | private void test() { |
上面的 Java 代码逻辑很简单——一个很简单的 if 语句,为了在 Smali 中看的更清楚,我只做了字符串赋值操作。下面是对应的 Smali 代码:
1 | .method private test()V |
仔细观察可以发现:
- 属性操作:
属性操作的分为:取值(get)和赋值(put)
目标类型分为:数组(array)、实例(instance)和静态(static)三种,对应的缩写前缀就是 a、i、s
长度类型分为:默认(什么都不写)、wide
(宽,64 位)、object
(对象)、boolean
、byte
、char
、short
(后面几种就不解释了,和 Java 一致)
指令格式:[指令名] [源寄存器], [目标字段所在对象寄存器], [字段指针]
,示例代码如下,操作是为 int 型的类成员变量mIntA
赋值为100
:
1 | const/16 v0, 0x64 |
下面列出用于实例字段的指令,其中 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 | private String mStringA; |
对应的 Smali 代码如下:
1 | # instance fields |
根据 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 相关的文章中进一步介绍!