0x1 smali概述
Dalvik 虚拟机 (Dalvik VM) 是 Google 专门为 Android 平台设计的一套虚拟机.区别于标准 Java 虚拟机 JVM 的 class 文件格式,Dalvik VM 拥有专属的 DEX 可执行文件格式和指令集代码.smali和 baksmali 则是针对 DEX 执行文件格式的汇编器和反汇编器,反汇编后 DEX 文件会产生.smali 后缀的代码文件,smali 代码拥有特定的格式与语法,smali 语言是对Dalvik 虚拟机字节码的一种解释.关于smali语法的格式,在AOSP/Dalvik/docs 目录下提供了一份文档 instruction-formats.html, 里面详细列举了 Dalvik 虚拟机字节码指令的所有格式,网上已经有了翻译后的中文文档.
0x2 使用smali和baksmali
smali 和 baksmali 这两个工具汇编和反汇编DEX 文件的使用非常简单,我们使用
baksmali.jar 反汇编 HelloWorld.dex 只需输入以下命令:
java -jar baksmali.jar -o HelloWorldOutHelloWorld.dex
命令执行成功后会在 HelloWorldOut 目录下生成相应 smali 文件,我们在修改完 smali
代码后,使用 smali.jar 重新汇编成 HelloWorld.dex,输入命令:
java -jar smali.jar -o HelloWorld.dexHelloWoldOut
我们只需把生成的 HelloWorld.dex 通过adb 命令 push 到手机上,并使用 dalvikvm 命令
便可以运行这个 DEX 文件,执行的命令如下:
adb push HelloWorld.dex /data/local/
adb shell dalvikvm -cp/data/local/HelloWorld.dex HelloWorld
0x3 Dalvik字节码
(1).类型
每个 Dalvik 寄存器都是 32 位大小, 对于小于或者等于 32 位长度的类型来说, 一个寄存器就可以存放该类型的值, 而像 J、 D 等 64 位的类型, 它们的值是使用相邻两个寄存器来存储的,如 v0 与 v1、v3 与 v4 等.Java 中的对象在 smali 中以 Lpackage/name/ObjectName;的形式表示.前面的 L 表示这是一个对象类型,package/name/表示该对象所在的包,ObjectName是对象的名字,”;”表示对象名称的结束. 相当于 java 中的 package.name.ObjectName. 例 如: Ljava/lang /String;相当于java.lang.String.”[“类型可以表示所有基本类型的数组.[I 表示一个整型一维数组,相当于 java 中的int[].对于多维数组,只要增加[就行了,[[I 相当于 int[][],[[[I 相当于 int[][][].注意每一维的最多 255 个.对象数组的表示:[Ljava/lang /String;表示一个 String 对象数组.
(2).方法
方法调用的表示格式:
Lpackage/name/ObjectName;->MethodName(III)Z.Lpackage/name/ObjectName;
表示类型,MethodName 是方法名,III 为参数(在此是 3 个整型参数) ,Z 是返回类型(bool 型) .函数的参数是一个接一个的,中间没有隔开.一个更复杂的例子:method(I[[IILjava/lang /String;[Ljava/lang /Object;)Ljava/lang/String;在 java 中则为:String method(int, int[][], int, String, Object[])
(3).字段
字段,即 java 中类的成员变量,表示格式:
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; 即包名, 字段名和字段类型,字段名与字段类型是以冒号”:”分隔.
(4). 两种不同的寄存器表示法
在 Dalvik 虚拟机字节码中寄存器的命名法中主要有 2 种:v 命名法和 p 命名法.假设一个函数使用到 M 个寄存器,并且该函数有 N 个入参,根据 Dalvik 虚拟机参数传递方式中的规定:入参使用最后的 N 个寄存器中,局部变量使用从 v0 开始的前 M-N 个寄存器.比如,某函数 A 使用了 5 个寄存器, 2 个显式的整形参数, 如果函数 A 是非静态方法, 函数被调用时会传入一个隐式的对象引用, 因此实际传入的参数个数是 3 个. 根据传参规则, 局部变量将使用前 2 个寄存器,参数会使用后 3 个寄存器.
v 命名法采用小写字母”v”开头的方式表示函数中用到的局部变量与参数,所有的寄存器命名从 v0 开始,依次递增.对于上文的函数 A,v 命名法会用到 v0、v1、v2、v3、v4等 5 个寄存器,v0 与 v1 表示函数 A 的局部变量,v2 表示传入的隐式对象引用,v3 与 v4 表示实际传入的 2 个整形参数.
P 命名法对函数的局部变量寄存器命名没有影响,它的命名规则是:函数的入参从 p0开始命名,依次递增.对于上文的函数 A,p 命名法会用到 v0、v1、p0、p1、p2 等 5 个寄存器,v0 与 v1 表示函数 A 的局部变量,p0 表示传入的隐式对象引用,p1 与 p2 表示实际传入的 2 个整形参数. 此时, p0、 p1、 p2 实际上分别表示 v2、 v3、 v4, 只是命名不一样而已.
在实际的 Smali 文件中, 几乎都是使用了 p 命名法, 主要原因是使用 p 命名法能够通过寄存器的名字前缀就能很容易判断寄存器到底是局部变量还是函数的入参.初次学习 smali语法时容易对寄存器 p0 表示的意义出现混乱,这主要体现在静态方法和非静态方法中.其实只要理解 p 命名法的定义后就可以很清楚的理解.在 smali 语法中,在调用非静态方法时需要传入该方法所在对象的引用,因此此时 p0 表示的是传入的隐式对象引用,从 p1 开始才是实际传入的入参. 但是在调用静态方法时, 由于静态方法不需要构建对象的引用, 因而也就不需要传入该方法所在对象的引用,因此此时从 p0 开始就是实际传入的入参.
在 Dalvik 指令中使用”v 加数字”的方法来索引寄存器,如:v0、v1、v15、v255,但每条指令使用的寄存器索引范围都有限制(因为 Dalvik 指令字节码必须字节对齐) ,这里我们使用一个大写字母来表示 4 位数据宽度的取值范围,如:指令 move vA, vB,目的寄存器 vA可使用 v0 ~ v15 的寄存器,源寄存器 vB 可以使用 v0 ~ v15 寄存器.指令move/from16 vAA,vBBBBB,目的寄存器 vAA 可使用 v0 ~ v255的寄存器,源寄存器 vB 可以使用 v0 ~ v65535 寄存器.简而言之,当目的寄存器和源寄存器中有一个寄存器的编号大于 15 时,即需要加上/from16 指令才能得到正确运行.初次学习 Smali 语法时也容易对这一点不能理解,不注意就会导致 Smali 文件汇编为 dex 文件的时候出现编译错误. 比如, 按照前面总结的 p 命名法,当 p0 实际表示的寄存器编号大于 15 时,此时 Smali 语句 move v0,p0 就会编译出错.
0x4 实例
//Hello.java
public class Hello
{
publicint foo(int a, int b)
{
return(a + b) * (a - b);
}
publicstatic void main(String[] argc)
{
Hellohello = new Hello();
System.out.println(hello.foo(5,3));
}
}
//Hello.smali
.class public LHello; #class的名字
.super Ljava/lang/Object; #这个类继承的对象
.source "Hello.java" #java文件名
# direct methods #直接方法
.method public constructor <init>()V #class的构造函数
.registers 1 #寄存器数
.prologue #然而并没有什么卵用 -_-!
.line 1 #行号
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
#调用Object的构造方法,p0相当于"this"指针
return-void #返回空
.end method
.method public static main([Ljava/lang/String;)V #main方法实现
.registers 5
.parameter
.prologue
.line 10
new-instance v0, LHello; #new一个对象
invoke-direct {v0}, LHello;-><init>()V #调用Hello类的构造函数,参数是v0
.line 11
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream; #读取输出值到v1
const/4 v2, 0x5 #读4位立即数0x5到v2
const/4 v3, 0x3 #读4位立即数0x3到v3
invoke-virtual {v0, v2, v3}, LHello;->foo(II)I #调用foo函数,传递参数v0,v2,v3
move-result v0 #调用返回值到v0
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V #调用虚方法,参数v1,v0
.line 12
return-void
.end method
# virtual methods #foo方法
.method public foo(II)I
.registers 5
.parameter
.parameter
.prologue
.line 5
add-int v0, p1, p2 #计算p1+p2到v0
sub-int v1, p1, p2 #计算p1-p2到v1
mul-int/2addr v0, v1 #计算v0*v1到v0
return v0 #返回v0
.end method