前言
通常基于Java语言开发程序都是通过调用javac编译器将源代码编译成.class文件,这种文件能够被JVM识别,加载并执行的文件格式(除了常见的java源代码生成的class文件,其他的Scalar、Python和Groovy等语言都可以生成class文件,每个类和接口都单独占据一个class文件)。不过class文件内存占用量大,不适合移动端,采用堆栈的加载模式,文件IO操作多,class只包含一个类,不断的查找新类需要不断做IO操作。
Android的Dalvik和ART都是基于Dex格式的虚拟机实现,它们内部采用了寄存器模式访问数据,比普通的JVM基于栈实现更加高效。Dex格式文件不只能通过Java语言生成,C/C++也可以生成dex文件,通过dx命令生成dex文件。dex文件能够记录整个工程的所有类文件的信息,包括所有的文件,这样查询类的时候就不用多次IO,一次把所有类都加入到内存中。.dex里面记录的是Dalvik实现的虚拟机指令,如果使用baksmali工具就能够把.dex文件中的机器指令反编译成smali代码,也就是说smali代码其实是Dalvik的汇编语言。
基本语法
Smali的语法规则相对于Intel等硬件编译语言要简单的多,懂得Java语法的人很容易就能够将二者的实现一一对应,这里先介绍数据类型的表示,再说明方法调用指令,最后了解一下数据存储和返回指令。
类型定义
类型 | Java类型 | 说明 |
---|---|---|
v | void | 只能用于返回值类型 |
Z | boolean | |
B | byte | |
S | short | |
C | char | |
I | int | |
J | long | 2个寄存器 |
F | float | |
D | double | 2个寄存器 |
在Dalvik虚拟机中寄存器的位数是32位,而long和Double类型都有64为,单独的寄存器无法装下这两种数据类型,需要使用2个寄存器装载
上面定义的都是基础数据类型,现在看一下对象类型,对象类型包含普通对象和数组两大类:
类型名称 | 类型 | 说明 |
---|---|---|
对象类型 | Lpackage/name/ObjectName; | L:表示这是一个对象类型 package/name:该对象所在的包 ;:表示对象名称结束 |
数组 | [primaryType | [I:表示整形的一维int数组,相当于java的int[];对于多维的数组多加[就可以了 |
对象数组 | [Lpackage/name/ObjectName; | [Ljava/lang/String; 表示一个String的对象数组 |
方法调用
Smali中的方法被表示成“对象->方法描述符”形式,其中方法描述符就是“方法名(传入参数描述符)返回参数描述符”,传入和返回参数的描述符参考上面的类型定义,其中传入参数是连接在一起中间没有任何分隔符。
Lpackage/name/ObjectName;->methodName(III)Z
Lpackage/name/ObjectName;:表示类型
methodName:表示方法名
III:表示参数为3个整形数字
上面只是方法的表达方式,真正要调用这些方法还需要使用invoke-xxx指令来执行,invoke指令后面会跟上调用参数,其中最后一个是返回参数,前面的参数都是传入参数。
方法调用指令 | 说明 |
---|---|
invoke-static | 类静态方法的调用,编译时确定 |
invoke-virtual | 虚方法调用,调用的方法运行时确认实际调用,和实例引用的实际对象有关,动态确认的 |
invoke-direct | 没有被覆盖方法的调用,即不用动态根据实例所引用的调用,编译时确认的,一般是private或方法; |
invoke-super | 直接调用父类的虚方法,编译时,静态确认的。 |
invokeinterface | 调用接口方法,调用的方法运行时确认实际调用,即会在运行时才确定一个实现此接口的对象 |
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
上面的例子中由于onCreate方法返回值是void,不需要返回参数,p0,p1都是传入参数,其中p0代表this也就是Activity对象,p1代表Bundle对象,这里的invoke-super指令,等价于super.onCreate(p1);。
字段的表示形式是“对象类型->字段名:字段类型”,最开始的对象类型说明当前字段所属的对象类型,最后的字段类型则是这个字段的对象类型。
Lpackage/name/ObjectName;->fieldName:Ljava/lang/String;
其他指令
方法调用指令 | 说明 |
---|---|
return-void | 表示函数返回void,相当于return; |
return vAA | 表示函数返回一个32位非对象类型的值 |
return-wide vAA | 表示函数返回一个64位非对象类型的值 |
return-object vAA | 表示函数返回一个对象类型的值 |
const/4 vA,#+B | 将数值符号扩展为32位后赋给寄存器vA |
const/16 vAA,#+BBBB | 将数值符号扩展为32位后赋给寄存器vAA |
const vAA,#+BBBBBBBB | 将数值赋给寄存器vAA |
move-object/16 vAAAA,vBBBB | 为对象赋值源寄存器与目的寄存器都为 16 位 |
move-result vAA | 将上一个 invoke 类型指令操作的单字非对象结果赋给 vAA 寄存器 |
move-result-wide vAA | 将上一个invoke类型指令操作的双字非对象结果赋给 vAA 寄存器 |
move-result-objcct vAA | 将上一个 invoke 类型指令操作的对象结果赋给 vAA 寄存器 |
if-eq vA, vB | 如果vA不等于vB则跳转,Java语法表示为 if(vA == vB) |
if-ne vA, vB | 如果vA不等于vB则跳转,Java语法表示为 if(vA != vB) |
if-lt vA, vB | 如果vA小于vB则跳转,Java语法表示为 if(vA < vB) |
if-le vA, vB | 如果vA小于等于vB则跳转,Java语法表示为 if(vA <= vB) |
if-gt vA, vB | 如果vA大于vB则跳转,Java语法表示为 if(vA > vB) |
if-ge vA, vB | 如果vA大于等于vB则跳转,Java语法表示为 if(vA >= vB) |
伪指令
Smali中的寄存器的命名分为两种,V*代表本地寄存器,P*代表传入的参数寄存器
V0 —- 第一个本地寄存器
V1 —- 第二个本地寄存器
....
P0 —- 第一个参数寄存器
P1 —- 第二个参数寄存器
....
除了前面的寄存器,还有专门用来定义方法、变量、代码行数、接口和注解等各种元素。
指令 | 解释 |
---|---|
.class | 当前smali反编译包含的类名 |
.super | 当前类的父类 |
.source | 对应的java源文件 |
.field | 定义变量 |
.end field | 定义变量结束 |
.method | 定义方法 |
.locals | 方法使用的本地寄存器个数 |
.register | 方法使用的寄存器个数 |
.parameter | 方法参数 |
.prologue | 方法开始 |
.line 12 | 此方法位于第12行 |
.end method | 函数结束 |
.implement | 实现的接口类型 |
.annotation | 注解开始 |
.end annotation | 注解结束 |
类外调用父类方法
首先直接使用Java代码编写三个类,父类Person,子类Driver和Student,其中Driver覆盖了父类的say方法,Student没有覆盖父类方法,但是添加了一个静态的Drvier作为参数的say方法,在Main中调用Student.say方法,可见调用的是Driver覆盖了的say方法。
package callsuper;
public class Person {
public void say() {
System.out.println("Hello Person");
}
}
public class Driver extends Person {
public void say() {
System.out.println("Hello Driver");
}
}
public class Student extends Person {
public static void say(Driver driver) {
driver.say();
}
}
public class Main {
public static void main(String[] args) throws Exception {
Driver driver = new Driver();
Student.say(driver);
}
}
这时在命令行中使用普通的javac、java执行,接着在使用dx将.class打包成.dex推送到手机端,使用dalvikvm指令执行。
javac callsuper/**.*
java callsuper.Main
// 输出结果:Hello Driver
dx --dex --output=hello.dex .
adb push hello.dex /sdcard/
// hello.dex: 1 file pushed. 0.0 MB/s (1468 bytes in 0.065s)
dalvikvm -cp Hello.dex callsuper.Main
// 输出结果:Hello Driver
可以看到两者执行的效果是一样的,都是调用了Driver覆盖的方法,如果想要在Student.say()中调用Person.say()方法,可以直接修改smali反汇编的代码,在此之前需要安装dex2jar这个工具。
// 将Hello.dex反编译为smali格式并将smali代码放到test文件夹中
d2j-baksmali -o test Hello.dex
进入test文件夹打开Student.smali文件,
// 当前文件包含了Student类
.class public Lcallsuper/Student;
// Student类继承自Person类
.super Lcallsuper/Person;
// 对应于Student.java源文件
.source "Student.java"
// 系统默认生成的无参数构造函数
.method public constructor <init>()V
.registers 1
.prologue
.line 3
// 这里调用了Person的构造函数
invoke-direct { p0 }, Lcallsuper/Person;-><init>()V
return-void
.end method
// 静态方法,替换之前
.method public static say(Lcallsuper/Driver;)V
// 使用了一个寄存器,也就是Driver这个参数
.registers 1
// 方法开始
.prologue
// 在源文件第6行
.line 6
// 调用drvier.say()方法
invoke-virtual { p0 }, Lcallsuper/Driver;->say()V
.line 7
// 返回
return-void
.end method
// 静态方法替换之后
.method public static say(Lcallsuper/Driver;)V
.registers 1
.prologue
.line 6
invoke-super { p0 }, Lcallsuper/Person;->say()V
.line 7
return-void
.end method
上面的smali代码就是正常编译产生的,由于直接在Java代码中无法从其他类中调换当前对象的super.say()方法,这里可以使用invoke-super替换掉invoke-virtual指令,同时Lcallsuper/Driver;->say()V中的类变成Lcallsuper/Person;->say()V,之后再将smali打包成dex文件执行。
// 将test下的smali代码编译成dex文件
d2j-smali -o Super.dex test
adb push Super.dex /sdcard/
// Super.dex: 1 file pushed. 0.0 MB/s (1468 bytes in 0.114s)
dalvikvm -cp Super.dex callsuper.Main
// 输出结果:Hello Person
从类外部调用它的父类方法看起来似乎挺玄乎,它在Android热修复生成补丁非常有用,Android Robust热补丁修复就使用了这种技术实现在补丁中调用super方法,这里就做一下记录。