Smali简介和实现类外调用父类方法

前言

通常基于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方法,这里就做一下记录。

猜你喜欢

转载自blog.csdn.net/xingzhong128/article/details/80916069