1、先来了解一下dalvik虚拟机:
dalvik虚拟机是Android 5.0以前用于运行安卓应用的虚拟机,从 Android 4.4 开始,Google 开始引入了全新的虚拟机 ART(Android Runtime),直到Android5.0开始ART虚拟机就替代了dalvik虚拟机。既然dalvik虚拟机被ART虚拟机替代了,那我们还有学的必要吗?ART 是向下兼容的,ART虚拟机对DEX字节码的运行是兼容的,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。但是Dalvik 采用的一些技术并不适用于 ART,但不妨碍我们了解和学习dalvik字节码。
2、我们再来了解一下dalvik寄存器和寄存器的命名方法:
Dalvik寄存器中的寄存器都是32位大小,支持所有类型,对于小于或等于32位的类型,使用一个寄存器就可以了;对于64位(long和double)类型,需要使用两个相邻的寄存器来存储。
寄存器的命名方法有两种:v命名法和p命名法:
v命名法:局部变量寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。
p命名法:函数参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。
特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this",p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)
寄存器赋值:
const/4 p5, 0x1 //p5赋值1
const/16 v0, 0xa //v0赋值10,在16进制里a表示10
const/4和const/16是表示定义一个4位和16位值,这里只是修饰一下,记住Dalvik寄存器是32位寄存器,这只是表示32位寄存器中存的值是4位和16位而已,不要被这个给影响了。
3、再来了解一下dalvik字节码的类型与Java数据类型的对应关系:
数据类型对应:
smali类型 | java类型 | 注释 |
V | void | 无返回值 |
Z | boolean | 布尔值类型,返回0或1 |
B | byte | 字节类型,返回字节 |
S | short | 短整数类型,返回数字 |
C | char | 字符类型,返回字符 |
I | int | 整数类型,返回数字 |
J | long (64位 需要2个寄存器存储) | 长整数类型,返回数字 |
F | float | 单浮点类型,返回数字 |
D | double (64位 需要2个寄存器存储) | 双浮点类型,返回数字 |
string | String | 文本类型,返回字符串 |
Lxxx/xxx/xxx | object | 对象类型,返回对象 |
long类型和double类型都是64位,需要两个寄存器来存储,有时在smali代码中,原本从0到n的寄存器在中间少了一个,那就有可能是其中有数据是long类型或者double类型的,在赋值代码中虽然用到了两个寄存器,但隐藏了其中一个。
4、了解smali和Java之间的转换流程:
xxx.java ==> xxx.class ==> 使用dx工具将.class打包成.dex文件 ==> 使用baksmali工具将.dex文件反编译成.smali文件 ==> 反编译成smali文件后对该文件进行修改成功 ==> 使用samli工具将.smali文件打包成.dex文件(回编译)
5、了解dalvik方法:
//一个私有、静态、不可变的方法 方法名
.method private static final onCreate$lambda-2([Ljava/lang/String;)Z//(这里面是方法的参数)这里是方法返回值类型,表示布尔值类型,返回假或真
.method表示定义了一个方法;
private、static、final表示该方法是一个一个私有、静态、不可变的方法;
onCreate$lambda-2是该方法定义的方法名,方法名后面括号中是该方法的参数;
[Ljava/lang/String;该参数表示接收字符串类型的一维数组,其中[表示一维数组,在smali的方法参数中每一个[就表示一维数组,比如[[就表示二维数组;java中类是一种数据类型,类用L表示,L后面接的是完整类名,也就是包名和类名。
6、了解dalvik字段:
dalvik创建字段需要指定字段的访问标志(修饰符)、字段名、字段类型和初始值等信息。下面是一个示例字段的创建代码:
.field private static count:I = 0
上述代码创建了一个名为count的私有静态字段,类型为整型,初始值为0。
1、dalvik指令集格式:
基础字节码-名称后缀/字节码后缀 目的寄存器 源寄存器/常量
Dalvik指令集中参数采用从源寄存器到目标寄存器的方式。
根据dalvik字节码的大小与类型不同,可能会添加名称后缀以消除岐义。具体来说,字节码的类型可以分为常规类型和特殊类型,根据类型的不同添加的后缀也不同,常规类型的字节码不添加任何后缀,而特殊类型的字节码根据具体类型添加后缀,可能是以下几种后缀之一:
常规类型的字节码主要有以下几种:
此外,根据字节码的大小与布局的不同,也可能添加字节码后缀以消除岐义。这些后缀通过在字节码主名称后添加斜杠“/”来分隔开。例如,一些指令可能会添加以下后缀:
-wide:64位宽度
-from16:源为16位寄存器引用
-to16:目标为16位寄存器引用
-range:指示该指令的操作数是一个范围。
32位常规类型的字节码没有添加任何后缀。
64位常规类型的字节码添加 -wide后缀。
例如这条指令:“move-wide/from16 vAA, vBBBB”:
2、dalvik指令集的使用:
空操作指令
数据操作指令
move指令的三种作用:
第一种作用:进行赋值操作
第二种作用:move-result接收方法返回值操作
第三种作用:处理异常的操作
move指令集的使用:
move vx, vy 指令将寄存器vy中的值移动到寄存器vx中。例如,move v1, v2表示将寄存器v2中的值移动到寄存器v1中,源寄存器与目的寄存器都为4位。
move/from16 vx, vy 指令将16位寄存器vy中的值移动到寄存器vx中。例如,move/from16 v1, v2表示将16位寄存器v2中的值移动到寄存器v1中,源寄存器为16位,目的寄存器为8位(范围必须在256以内)。注意,寄存器中存储的是值的引用,也可以理解为存储的是值的地址。
move-wide/from16 vx, vy 指令用于移动long/double类型的数据,将其从16位寄存器vy移动到范围必须在256内(8位)的寄存器vx中。例如,move-wide/from16 v1, v2表示将16位寄存器v2中的long/double类型的值移动到8位寄存器v1中。注意,寄存器中存储的是值的引用,也可以理解为存储的是值的地址。
move/16 vx, vy 指令将寄存器vy的值移动到寄存器vx中。例如,move/16 v1,v2表示将16位寄存器v2中的值移动到16位寄存器v1中,源寄存器与目的寄存器都为16位。
move-object vx, vy 指令将一个对象引用从寄存器vy移动到寄存器vx中。例如,move-object v1, v2表示将对象引用从寄存器v2移动到寄存器v1中。
move-object/from16 vx, vy 指令将对象引用从16位寄存器vy移动到寄存器vx中。例如,move-object/from16 v1, v2表示将对象引用从16位寄存器v2移动到寄存器v1中。
move-result vx 操作将上一条指令的返回值存储到指令中的目标寄存器vx中。例如,move-result v1表示将上一条指令的返回值移动到寄存器v1中。
move-result-wide vx 操作与move-result类似,但是用于保存长整数或双精度浮点数类型的返回值。例如,move-result-wide v1表示将长整数或双精度浮点数类型的返回值移动到寄存器v1中。
move-result-object vx 操作与move-result类似,但是用于保存引用类型的返回值。例如,move-result-object v1表示将引用类型的返回值移动到寄存器v1中。
move-exception vx 操作用于捕获异常对象并将其移动到指定寄存器vx中。例如,move-exception v1表示将异常对象移动到寄存器v1中。
返回指令(重点)
return-void:用于表示函数从一个void方法中返回,返回值为空。例如,return-void表示从当前方法中返回值为空。
return vx:用于返回一个整数类型的值,该值存储在寄存器vx中。例如,return v1表示从当前方法中返回寄存器v1中存储的整数类型的值。
return-wide vx:用于返回一个长整数或双精度浮点数类型的值,该值存储在寄存器vx和vx+1中。例如,return-wide v1表示从当前方法中返回寄存器v1和v2中存储的长整数或双精度浮点数类型的值。
return-object vx:用于返回一个对象引用类型的值,该值存储在寄存器vx中。例如,return-object v1表示从当前方法中返回寄存器v1中存储的对象引用类型的值。
return-boolean vx:用于返回一个布尔类型的值,该值存储在寄存器vx中。例如,return-boolean v1表示从当前方法中返回寄存器v1中存储的布尔类型的值。
return-byte vx:用于返回一个字节类型的值,该值存储在寄存器vx中。例如,return-byte v1表示从当前方法中返回寄存器v1中存储的字节类型的值。
return-char vx:用于返回一个字符类型的值,该值存储在寄存器vx中。例如,return-char v1表示从当前方法中返回寄存器v1中存储的字符类型的值。
return-short vx:用于返回一个短整数类型的值,该值存储在寄存器vx中。例如,return-short v1表示从当前方法中返回寄存器v1中存储的短整数类型的值。
数据定义指令(重点)
const/4 vA,#+B:用于将一个4位的数值符号扩展为32后赋值给寄存器vA。其中,vA表示目标寄存器,#+B表示常量值,取值范围为-8到7。
const/16 vAA, #+BBBB:用于将一个16位的数据符号扩展为32位后赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBB表示常量值,取值范围为-32768到32767。
const vAA, #+BBBBBBBB:用于将一个32位的数值赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBBBBBB表示常量值,取值范围为-2147483648到2147483647。
const/high16 vAA, #+BBBB0000:用于将一个高16位的数值以低16位为零的方式扩展为32位后赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBB0000表示常量值的高16位。
const-wide/16 vAA,#+BBBB:用于将16位的数值符号扩展为64位后赋值给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器(因为dalvik寄存器是32位的,所以存储64位数据需要两个寄存器存储),#+BBBB表示常量值,取值范围为-32768到32767。
const-wide/32 vAA, #+BBBBBBBB:用于将32位的数值符号扩展为64位后赋值给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBBBBBB表示常量值。
const-wide vAA, #+BBBBBBBBBBBBBBBB:用于将64位的数值赋给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBBBBBBBBBBBBBB表示常量值。
const-wide/high16 vAA, #+BBBB000000000000:用于将一个高16位的数值以其他位为零的方式扩展为64位后赋给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBB000000000000表示常量值的高16位。
const-string vAA,string@BBBB:用于将字符串常量引用存储到寄存器vAA中,通过字符串ID或字符串。其中,vAA表示目标寄存器,string@BBBB表示字符串在常量池中的索引。
const-string/jumbo vAA, string@BBBBBBBB:与const-string类似,但用于存储较长的字符串常量引用。
const-class vAA,type@BBBB:用于将类对象常量存储到寄存器vAA中,通过类型ID或类型(如Object.class)。其中,vAA表示目标寄存器,type@BBBB表示类在常量池中的索引。
const-class/jumbo vAAAA, type@BBBBBBBB:与const-class类似,但const-class/jumbo指令占用两个字节,值为0xooff,并且其操作数比常规的const-class指令操作数更大。这是Android4.0中新增的指令。
实例操作指令
new-instance vAA, type@BBBB:根据类型或者类型ID新建一个对象实例,并将新建的对象的引用存入目标寄存器vAA中。
instance-of vA, vB, type@CCCC:检查vB寄存器中的对象引用是否是type@CCCC对应类型的实例,如果是vA寄存器存入非零值,否则vA寄存器存入零。
check-cast vAA, type@BBBB:检查vAA寄存器中的对象引用是否可以转换成type@BBBB对应类型的实例。如不可转换,抛出ClassCastException异常,否则继续执行。另外还有一点需要注意,该指令只能用于转换成引用类型,不能用于转换成基本类型。
instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC:指令功能与“instance-of vA, vB, type@CCCC”相同,只是寄存器值与指令的索引取值范围更大。
new-instance/jumbo vAAAA, type@BBBBBBBB:指令功能与“new-instance vAA, type@BBBB”相同,只是寄存器值与指令的索引取值范围更大。
数组操作指令
array-length vA, vB:计算vB寄存器中数组引用的元素长度并将长度存入vA寄存器。
new-array vA, vB, type@CCCC:根据指定类型或类型ID(type@CCCC)与大小(vB寄存器存入数组的长度)构造一个数组,并将数组的引用赋给vA寄存器。
filled-new-array {vC, vD, vE, vF, vG}, type@BBBB:根据指定类型或类型ID(type@BBBB)与大小(vA)构造一个数组并填充数组内容,vA寄存器是隐含使用的,除了指定数组的大小外还指定了参数的个数,vC~vG是使用到的参数寄存器序号。指令会将寄存器 vC 到 vG 中的值填充到新数组中,并将新数组引用保存到寄存器 vA 中。还有一点需要注意,filled-new-array 指令创建的数组是一个新的对象,它与原数组没有任何关系。另外,该指令只能用于创建引用类型的数组,不能用于创建基本类型的数组。
filled-new-array/range {vCCCC .. vNNNN}, type@BBBB:filled-new-array/range 指令与 filled-new-array 指令非常类似,只是它的参数寄存器是以范围形式给出的,vCCCC 和 vNNNN 分别表示参数寄存器的起始位置和结束位置,type@BBBB 表示数组元素的类型。vC 是 filled-new-array/range 指令的第一个参数寄存器,表示数组大小。N=A+C-1 表示参数寄存器的结束位置,其中 A 表示数组大小,C 表示第一个参数寄存器的编号。因为 filled-new-array/range 指令的参数寄存器是连续的一段,所以 N 的值等于 A+C-1,即参数寄存器的结束位置。例如,如果 filled-new-array/range {v2 .. v5}, type@BBBB 指令被执行,那么数组大小为 v2 中存储的整数值,参数寄存器的编号为 v2、v3、v4、v5,因此 N=A+C-1=4+v2-1=3+v2。
fill-array-data vAA, 偏移量:用指定的字面量数组数据填充到目标数组中,字面量数组数据的位址是当前指令位址加偏移量的和。该指令填充的数组类型必须为基本类型的数组和字符串数组,其中基本类型包括 boolean、byte、short、char、int、float 和 double。填充数组时,字面量数组数据的每个元素都必须与目标数组的元素类型相同。另外,该指令只能用于填充已经创建的目标数组,不能用于创建新的数组对象。
举个例子,假设有如下代码:
...
fill-array-data v0, :array_0 // 将字面量数组数据填充到 v0 寄存器所代表的数组中,:array_0为字面量数组数据的偏移地址(偏移量)
...
:array_0
.array-data 2 // 表示每个数组元素占用两个字节
0x14ebs // 数组元素值为 0x14eb(short 类型)
0x15f0s // 数组元素值为 0x15f0(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
0x15c7s // 数组元素值为 0x15c7(short 类型)
...
异常指令
throw vAA:抛出异常对象,异常对象的引用在vAA寄存器。
跳转指令(重点)
无条件跳转指令
分支跳转指令
举个例子,假设有如下代码:
Java代码:
switch (this.mType) {
case 1:
return Math.signum(0.5d - (getP(d) % 1.0d));
case 2:
abs = Math.abs((((getP(d) * 4.0d) + 1.0d) % 4.0d) - 2.0d);
break;
case 3:
return (((getP(d) * 2.0d) + 1.0d) % 2.0d) - 1.0d;
case 4:
abs = ((getP(d) * 2.0d) + 1.0d) % 2.0d;
break;
case 5:
return Math.cos(this.PI2 * getP(d));
case 6:
double abs2 = 1.0d - Math.abs(((getP(d) * 4.0d) % 4.0d) - 2.0d);
abs = abs2 * abs2;
break;
default:
return Math.sin(this.PI2 * getP(d));
}
smali代码:
packed-switch v0, :pswitch_data_5e # 带着v0寄存器中的值跳转到索引表偏移量:pswitch_data_5e位址寻找对应的case指令偏移量的索引
.line 120
# default分支语句
iget-wide v0, p0, Landroidx/constraintlayout/motion/utils/Oscillator;->PI2:D
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr v0, p1
invoke-static {v0, v1}, Ljava/lang/Math;->sin(D)D
move-result-wide p1
return-wide p1
.line 132
:pswitch_17 # case 6
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr p1, v1
rem-double/2addr p1, v1
sub-double/2addr p1, v3
invoke-static {p1, p2}, Ljava/lang/Math;->abs(D)D
move-result-wide p1
sub-double p1, v5, p1
mul-double/2addr p1, p1
:goto_25 # 所有case的出口,也可以说是在case中执行break后跳转到此处
sub-double/2addr v5, p1
return-wide v5
.line 130
:pswitch_27 # case 5
iget-wide v0, p0, Landroidx/constraintlayout/motion/utils/Oscillator;->PI2:D
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr v0, p1
invoke-static {v0, v1}, Ljava/lang/Math;->cos(D)D
move-result-wide p1
return-wide p1
.line 128
:pswitch_33 # case 4
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr p1, v3
add-double/2addr p1, v5
rem-double/2addr p1, v3
goto :goto_25 # 跳转到偏移量为goto_25的目标位址
.line 126
:pswitch_3b # case 3
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr p1, v3
add-double/2addr p1, v5
rem-double/2addr p1, v3
sub-double/2addr p1, v5
return-wide p1
.line 124
:pswitch_44 # case2
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
mul-double/2addr p1, v1
add-double/2addr p1, v5
rem-double/2addr p1, v1
sub-double/2addr p1, v3
invoke-static {p1, p2}, Ljava/lang/Math;->abs(D)D
move-result-wide p1
goto :goto_25 # 跳转到偏移量为goto_25的目标位址
:pswitch_51 # case 1
const-wide/high16 v0, 0x3fe0000000000000L # 0.5
.line 122
invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D
move-result-wide p1
rem-double/2addr p1, v5
sub-double/2addr v0, p1
invoke-static {v0, v1}, Ljava/lang/Math;->signum(D)D
move-result-wide p1
return-wide p1
:pswitch_data_5e
.packed-switch 0x1 # 索引表,case常量从1开始,依次递增
:pswitch_51 # case 1
:pswitch_44 # case 2
:pswitch_3b # case 3
:pswitch_33 # case 4
:pswitch_27 # case 5
:pswitch_17 # case 6
.end packed-switch
举个例子,假设有如下代码:
Java代码:
switch (transit) {
case 4097:
return 8194;
case 4099:
return 4099;
case 8194:
return 4097;
default:
return 0;
}
smali代码:
sparse-switch p0, :sswitch_data_e # sparse-switch指令,传入寄存器p0的值作为查找表的索引,:sswitch_data_e是查询表的偏移量
.line 2025 # 源代码的行号
:goto_4 # 这里是所有case的出口,如果case常量没匹配上查询表中的值,那么v0寄存器中的值就为0,这里就返回零,也就是default下的代码return 0;
return v0 # 返回v0寄存器中的值
.line 2016 # 源代码的行号
:sswitch_5 # 标签,对应表中的第一个case常量0x1001
const/16 v0, 0x2002 # 将0x2002赋值给v0寄存器
.line 2017 # 源代码的行号
goto :goto_4 # 无条件跳转到:goto_4标签处执行
.line 2019 # 源代码的行号
:sswitch_8 # 标签,对应表中的第三个case常量0x2002
const/16 v0, 0x1001 # 将0x1001赋值给v0寄存器
.line 2020 # 源代码的行号
goto :goto_4 # 无条件跳转到:goto_4标签处执行
.line 2022 # 源代码的行号
:sswitch_b # 标签,对应表中的第二个case常量0x1003
const/16 v0, 0x1003 # 将0x1003赋值给v0寄存器
goto :goto_4 # 无条件跳转到:goto_4标签处执行
.line 2014 # 源代码的行号
:sswitch_data_e # 标签,作为查询表的偏移量
.sparse-switch # 表示接下来是一个查询表
0x1001 -> :sswitch_5 # case常量0x1001跳转到标签:sswitch_5处执行
0x1003 -> :sswitch_b # case常量0x1003跳转到标签:sswitch_b处执行
0x2002 -> :sswitch_8 # case常量0x2002跳转到标签:sswitch_8处执行
.end sparse-switch # 标志着查询表的结束
条件跳转指令
if-eq vA, vB, 目标:如果vA == vB,跳转到目标。
if-ne vA, vB, 目标:如果vA != vB,跳转到目标。
if-lt vA, vB, 目标:如果vA < vB,跳转到目标。
if-ge vA, vB, 目标:如果vA >= vB,跳转到目标。
if-gt vA, vB, 目标:如果vA > vB,跳转到目标。
if-le vA, vB, 目标:如果vA <= vB,跳转到目标。
if-eqz vA, 目标:如果vA == 0,跳转到目标。
if-nez vA, 目标:如果vA != 0,跳转到目标。
if-ltz vA, 目标:如果vA < 0,跳转到目标。
if-gez vA, 目标:如果vA >= 0,跳转到目标。
if-gtz vA, 目标:如果vA > 0,跳转到目标。
if-lez vA, 目标:如果vA <= 0,跳转到目标。
比较指令
cmpl-float vAA,vBB,vCC:比较vBB和vCC的float值并在vAA存入int型返回值。非数值默认为小于,如果参数为非数值将返回-1。如果vBB寄存器大于vCC寄存器,则结果为-1,相等则结果为0,小于则结果为1。
cmpg-float vAA,vBB,vCC:比较vBB和vCC的float值并在vAA存入int型返回值。非数值默认为大于,如果参数为非数值将返回1。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于则结果为-1。
cmpl-double vAA,vBB,vCC:比较vBB和vCC的double值并在vAA存入int型返回值。非数值默认为小于,如果参数为非数值将返回-1。如果vBB,vBB+1寄存器的值大于vCC,vCC+1寄存器的值,则结果为-1,相等则结果为0,小于则结果为1。
cmpg-double vAA,vBB,vCC:比较vBB和vCC的double值并在vAA存入int型返回值。非数值默认为大于,如果参数为非数值将返回1。如果vBB,vBB+1寄存器的值大于vCC,vCC+1寄存器的值,则结果为1,相等则结果为0,小于则结果为-1。
cmp-long vAA,vBB,vCC:比较vBB和vCC的long值并在vAA存入int型返回值。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小则结果为-1。
字段操作指令
iput-object vAA,vBB,字段ID:根据字段ID将vAA寄存器的值存入实例的对象引用字段,vBB寄存器中是该实例的引用。例如:
iput-object v0, p0, Lbin/mt/apksignaturekillerplus/HookApplication;->appPkgName:Ljava/lang/String;
可以看到例子中将v0寄存器中Ljava/lang/String;类型的值(实例的对象引用)存入到p0寄存器中,p0寄存器中是Lbin/mt/apksignaturekillerplus/HookApplication;类型下字段名为appPkgName的实例字段,并且实例字段appPkgName的存储值类型为Ljava/lang/String;
iput-boolean vAA,字段ID:根据字段ID将vAA寄存器的值存入实例的boolean型字段,vBB寄存器中是该实例的引用。例如:
iput-boolean p1, p0, Lbin/mt/plugin/api/translation/TranslationEngine$Configuration;->acceptTranslated:Z
可以看到例子中将p1寄存器中boolean型的值存入到p0寄存器中,p0寄存器中是Lbin/mt/plugin/api/translation/TranslationEngine$Configuration;类型下字段名为acceptTranslated的实例字段,并且实例字段acceptTranslated的存储值类型为boolean。
iput-wide vAA,vBB,字段ID:根据字段ID将vAA,vAA+1寄存器的值存入实例的double/long型字段,vBB寄存器中是该实例的引用。例如:
iput-wide v1, p0, Lcom/alipay/android/phone/mrpc/core/l;->e:J
可以看到例子中将v1,v2寄存器中long型的值存入到p0寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/l;类型下字段名为e的实例字段,并且实例字段e的存储值类型为long。
iput vAA,vBB,字段ID:根据字段ID将vAA寄存器的值存入实例的int型字段,vBB寄存器中是该实例的引用。例如:
iput v0, p0, Lcom/alipay/android/phone/mrpc/core/e;->a:I
可以看到例子中将v0寄存器中int型的值存入到p0寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/e;类型下字段名为a的实例字段,并且实例字段a的存储值类型为boolean。
iget-object vAA,vBB,字段ID:根据字段ID读取一个实例的对象引用字段到vAA,vBB寄存器中是该实例的引用。例如:
iget-object v4, p0, Lbin/mt/apksignaturekillerplus/HookApplication;->appPkgName:Ljava/lang/String;
可以看到例子中将p0寄存器中Ljava/lang/String;类型的值(实例的对象引用)存入到v4寄存器中,p0寄存器中是Lbin/mt/apksignaturekillerplus/HookApplication;类型下字段名为appPkgName的实例字段,并且实例字段appPkgName的存储值类型为Ljava/lang/String;
iget-boolean vAA,vBB,字段ID:根据字段ID读取实例的boolean型字段到vAA,vBB寄存器中是该实例的引用。例如:
iget-boolean v1, p0, Lbin/mt/plugin/api/translation/TranslationEngine$ConfigurationBuilder;->acceptTranslated:Z
可以看到例子中将p0寄存器中boolean型的值存入到v1寄存器中,p0寄存器中是Lbin/mt/plugin/api/translation/TranslationEngine$ConfigurationBuilder;类型下字段名为acceptTranslated的实例字段,并且实例字段acceptTranslated的存储值类型为boolean。
iget-wide vAA,vBB,字段ID:根据字段ID读取实例的double/long型字段到vAA,vAA+1,vBB寄存器中是该实例的引用。例如:
iget-wide v3, p0, Lcom/alipay/android/phone/mrpc/core/l;->g:J
可以看到例子中将p0寄存器中long型的值存入到v3,v4寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/l;类型下字段名为g的实例字段,并且实例字段g的存储值类型为long。
iget vAA,vBB,字段ID:根据字段ID读取实例的int型字段到vAA,vBB寄存器中是该实例的引用。例如:
iget v1, v8, Landroid/util/TypedValue;->data:I
可以看到例子中将v8寄存器中int型的值存入到v1寄存器中,v8寄存器中是Landroid/util/TypedValue;类型下字段名为data的实例字段,并且实例字段data的存储值类型为int。
sput-object vAA,字段ID:根据字段ID将vAA寄存器中的对象引用赋值到对象引用静态字段。例如:
sput-object v0, Lbin/tools/inject/InjectedLog;->TIME_FORMAT1:Ljava/text/SimpleDateFormat;
可以看到例子中将v0寄存器中Ljava/text/SimpleDateFormat;类型的值(对象引用)存入到Lbin/tools/inject/InjectedLog;类型下的存储值为Ljava/text/SimpleDateFormat;类型的对象引用静态字段TIME_FORMAT1中。
sput-boolean vAA,字段ID:根据字段ID将vAA寄存器中的值赋值到boolean型静态字段。例如:
sput-boolean v6, Lcom/alipay/sdk/cons/a;->r:Z
可以看到例子中将v6寄存器中boolean类型的值存入到Lcom/alipay/sdk/cons/a;类型下的存储值为boolean类型的静态字段r中。
sput-wide vAA,字段ID:根据字段ID将vAA,vAA+1寄存器中的值赋值到double/long型静态字段。例如:
sput-wide v0, Lcom/alipay/sdk/app/PayTask;->i:J
可以看到例子中将v0,v1寄存器中long类型的值存入到Lcom/alipay/sdk/app/PayTask;类型下的存储值为long类型的静态字段i中。
sput vAA,字段ID:根据字段ID将vAA寄存器中的值赋值到int型静态字段。例如:
sput v0, Lcom/google/android/material/appbar/AppBarLayout;->DEF_STYLE_RES:I
可以看到例子中将v0寄存器中int类型的值存入到Lcom/google/android/material/appbar/AppBarLayout;类型下的存储值为int类型的静态字段DEF_STYLE_RES中。
sget-object vAA,字段ID:根据字段ID读取静态对象引用字段到vAA。例如:
sget-object v17, Ljava/lang/System;->out:Ljava/io/PrintStream;
可以看到例子中将Ljava/lang/System;类型下的静态字段out中的Ljava/io/PrintStream;类型的值(对象引用)存入v17寄存器中。
sget-boolean vAA,字段ID:根据字段ID读取静态boolean型字段到vAA。例如:
sget-boolean v1, Lbin/mt/plus/Features;->ۘf:Z
可以看到例子中将Lbin/mt/plus/Features;类型下的静态字段f中的boolean类型的值存入v1寄存器中。
sget-wide vAA,字段ID:根据字段ID读取静态double/long型字段到vAA,vAA+1。例如:
sget-wide v2, Lcom/alipay/android/phone/mrpc/core/b;->a:J
可以看到例子中将Lcom/alipay/android/phone/mrpc/core/b;类型下的静态字段a中的long类型的值存入v2,v3寄存器中。
sget vAA,字段ID:根据字段ID读取静态int型字段到vAA。例如:
sget v0, Landroid/os/Build$VERSION;->SDK_INT:I
可以看到例子中将Landroid/os/Build$VERSION;类型下的静态字段SDK_INT中的int类型的值存入v0寄存器中。
invoke-virtual {参数},方法名:调用带参数的实例的虚方法(虚方法相当于Java中的普通方法)。
invoke-virtual/range {vAA .. vBB},方法名:调用以寄存器范围为参数的虚方法。该指令第一个寄存器和寄存器的数量将会传递给方法。该指令与普通的invoke-virtual指令相比,可以同时传递多个参数,提高了效率。例如:
sget-object v17, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string/jumbo v18, "PmsHook success."
invoke-virtual/range {v17 .. v18}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
在这条指令中,寄存器范围为{v17 .. v18},调用java.io.PrintStream类的println(String)方法,该方法需要一个参数,参数类型为java.lang.String,并且不返回任何值。该指令的第一个寄存器为v17,寄存器的数量为2,因此v17和v18两个寄存器都将作为参数传递给方法,其中v18存储了传递给println()方法的参数值,v17则存储了PrintStream对象的引用。在执行该指令之前,需要先将参数值存储在寄存器v18中,将PrintStream对象的引用存储在寄存器v17中。该虚方法的作用是将参数值打印到控制台上。在Dalvik字节码中,方法的参数和返回值都是通过寄存器进行传递的。
invoke-super {参数},方法名:调用带参数的直接父类的虚方法。
invoke-super/range {vAA .. vBB},方法名:调用以寄存器范围为参数的直接父类的虚方法。该指令第一个寄存器和寄存器的数量将会传递给方法。
invoke-direct {参数},方法名:不解析直接调用带参数的方法,例如构造方法、静态语句块。
invoke-direct/range {vAA .. vBB},方法名:不解析直接调用以寄存器范围为参数的方法。该指令第一个寄存器和寄存器的数量将会传递给方法。
invoke-static {参数},方法名:调用带参数的静态方法。
invoke-static/range {vAA .. vBB},方法名:调用以寄存器范围为参数的静态方法。该指令第一个寄存器和寄存器的数量将会传递给方法。
invoke-interface {参数},方法名:调用带参数的接口方法。
invoke-interface/range {vAA .. vBB},方法名:调用以寄存器范围为参数的接口方法。该指令第一个寄存器和寄存器的数量将会传递给方法。
数据转换指令
neg-int:对整型数求补。取补指令是按位取反后加1,即将二进制数中的0变为1,1变为0,然后再加1。在Dalvik指令集中,取补指令的格式为“neg vA, vB”,表示将vB寄存器中的值按位取反后加1后存储到vA寄存器中。
not-int:对整型数求反。取反指令是按位取反,即将二进制数中的0变为1,1变为0。在Dalvik指令集中,取反指令的格式为“not vA, vB”,表示将vB寄存器中的值按位取反后存储到vA寄存器中。
neg-long:对长整型数求补。
not-long:对长整型数求反。
neg-float:对单精度浮点型数求补。
neg-double:对双精度浮点型数求补。
int-to-byte:将整型数转换为字节型。格式为“int-to-byte vA, vB”,表示将vB寄存器中的整型数转换为字节型后存放到vA寄存器中。需要注意的是,由于字节型数的范围是-128到127,因此在转换时可能会出现溢出情况。
int-to-char:将整型数转换为字符型。格式为“int-to-char vA, vB”,表示将vB寄存器中的整型数转换为字符型后存放到vA寄存器中。因为char类型是无符号的,大小为两个字节,所以可以表示的最大值就是2^16-1=65535。字符型数的范围是0到65535,在转换时可能会出现溢出情况。
int-to-short:将整型数转换为短整型。格式为“int-to-short vA, vB”,表示将vB寄存器中的整型数转换为短整型后存放到vA寄存器中。短整型数的范围是-32768到32767,因此在转换时可能会出现溢出情况。
long-to-int:将长整型数转换为整型数。格式为“long-to-int vA, vB”,表示将vB寄存器中的长整型数转换为整型数后存放到vA寄存器中。需要注意的是,由于长整型数的范围比整型数大,因此在转换时可能会出现溢出情况。
long-to-float:将长整型数转换为单精度浮点型数。格式为“long-to-float vA, vB”,表示将vB寄存器中的长整型数转换为单精度浮点型数后存放到vA寄存器中。
long-to-double:将长整型数转换为双精度浮点型数。格式为“long-to-double vA, vB”,表示将vB寄存器中的长整型数转换为双精度浮点型数后存放到vA寄存器中。
float-to-int:将单精度浮点型数转换为整型数。格式为“float-to-int vA, vB”,表示将vB寄存器中的单精度浮点型数转换为整型数后存放到vA寄存器中。需要注意的是,由于单精度浮点型数的范围比整型数大,因此在转换时可能会出现溢出情况。
float-to-long:将单精度浮点型数转换为长整型数。格式为“float-to-long vA, vB”,表示将vB寄存器中的单精度浮点型数转换为长整型数后存放到vA寄存器中。
float-to-double:将单精度浮点型数转换为双精度浮点型数。格式为“float-to-double vA, vB”,表示将vB寄存器中的单精度浮点型数转换为双精度浮点型数后存放到vA寄存器中。
double-to-int:将双精度浮点型数转换为整型数。格式为“double-to-int vA, vB”,表示将vB寄存器中的双精度浮点型数转换为整型数后存放到vA寄存器中。需要注意的是,由于双精度浮点型数的范围比整型数大,因此在转换时可能会出现溢出情况。
double-to-long:将双精度浮点型数转换为长整型数。格式为“double-to-long vA, vB”,表示将vB寄存器中的双精度浮点型数转换为长整型数后存放到vA寄存器中。
double-to-float:将双精度浮点型数转换为单精度浮点型数。格式为“double-to-float vA, vB”,表示将vB寄存器中的双精度浮点型数转换为单精度浮点型数后存放到vA寄存器中。需要注意的是,由于单精度浮点型数的范围比双精度浮点型数小,因此在转换时可能会出现精度损失的情况。
int-to-byte:将整型数转换为字节型。格式为“int-to-byte vA, vB”,表示将vB寄存器中的整型数转换为字节型后存放到vA寄存器中。
int-to-char:将整型数转换为字符型。格式为“int-to-char vA, vB”,表示将vB寄存器中的整型数转换为字符型后存放到vA寄存器中。
int-to-short:将整型数转换为短整型。格式为“int-to-short vA, vB”,表示将vB寄存器中的整形数转换为短整形后存放到vA寄存器中。
数据运算指令
add-type:进行加法运算。格式为“add-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数相加,结果存放到vA寄存器中。
sub-type:进行减法运算。格式为“sub-int vA, vB, vC”,表示将vB寄存器中的整型数减去vC寄存器中的整型数,结果存放到vA寄存器中。
mul-type:进行乘法运算。格式为“mul-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数相乘,结果存放到vA寄存器中。
div-type:进行除法运算。格式为“div-int vA, vB, vC”,表示将vB寄存器中的整型数除以vC寄存器中的整型数,结果存放到vA寄存器中。
rem-type:进行取余运算。格式为“rem-int vA, vB, vC”,表示将vB寄存器中的整型数除以vC寄存器中的整型数的余数,结果存放到vA寄存器中。
and-type:进行按位与运算。格式为“and-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位与运算,结果存放到vA寄存器中。
or-type:进行按位或运算。格式为“or-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位或运算,结果存放到vA寄存器中。
xor-type:进行按位异或运算。格式为“xor-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位异或运算,结果存放到vA寄存器中。
shl-type:有符号左移。格式为“shl-type vA, vB, vC”,表示将vB寄存器中的有符号整型数左移vC位,结果存放到vA寄存器中。左移时,低位补0,高位舍弃。
shr-type:有符号右移。格式为“shr-type vA, vB, vC”,表示将vB寄存器中的有符号整型数右移vC位,结果存放到vA寄存器中。右移时,高位补符号位,低位舍弃。
ushr-type:无符号右移。格式为“ushr-type vA, vB, vC”,表示将vB寄存器中的无符号整型数右移vC位,结果存放到vA寄存器中。右移时,高位补0,低位舍弃。
无论是普通类、抽象类、接口类还是内部类,在反编译出来的代码中,它们都是以单独的smali文件进行存放的。每个smali文件中的语句都遵循着一套语法规范,这里想要讲解的正是这套smali文件中的语法规范。
Smali中开头的包信息以及包信息格式:
.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>
例如:
.class public interface abstract Landroid/support/v4/os/IResultReceiver;
// 这个类是android.support.v4.os这个包下名为IResultReceiver的接口类
.super Ljava/lang/Object; // 该类继承了父类java.lang.Object
.source "IResultReceiver.java" // 这个smali文件在还是Java文件时,文件名为IResultReceiver.java
静态字段定义
**.field <访问权限> static [修饰关键字] <字段名>:<字段类型>**
例如:
.field private static final PARAMETER_BUFFER:Ljava/lang/ThreadLocal;
实例字段定义(java中的普通字段)
相比起静态字段定义,实例字段就少了一个静态修饰符static而已,格式:
.field <访问权限> [修饰关键字] <字段名>:<字段类型>
例如:
.field public final mConstructorArgs:[Ljava/lang/Object;
直接方法定义
# direct methods
.method <访问权限> [修饰关键字] <方法名称、参数还有返回值>
<.registers> // 指定了方法中寄存器的总数,这个数量是参数寄存器和本地寄存器的总和。
<.local> // 这个指令表明方法中非参数寄存器(本地寄存器)的个数
[.param] // 表明了方法的参数,每个.param指令表示一个参数,方法使用了几个参数就有几个.param指令。
[.prologue] // 表明了方法中代码的开始处
[.line] // 表名了该处代码在源代码中的行号
<代码体>
.end method
例如:
.method public static spatialSampling(Landroid/gesture/Gesture;IZ)[F
// 定义一个公共静态方法,返回一个浮点数数组
// 方法名为spatialSampling,参数为Gesture对象、整型bitmapSize和布尔型keepAspectRatio
.locals 0
// 定义本地寄存器数为0
.param p0, "gesture" # Landroid/gesture/Gesture;
// 定义一个参数p0,类型为Gesture,表示手势对象
.param p1, "bitmapSize" # I
// 定义一个参数p1,类型为整型,表示位图大小
.param p2, "keepAspectRatio" # Z
// 定义一个参数p2,类型为布尔型,表示是否保持宽高比
new-instance p0, Ljava/lang/RuntimeException;
// 创建一个RuntimeException对象,将其引用赋值给p0
invoke-direct {p0}, Ljava/lang/RuntimeException;->()V
// 调用RuntimeException对象的构造方法,初始化该对象
throw p0
// 抛出RuntimeException对象
.end method
// 方法结束
虚方法(java中的普通方法)
# virtual methods
.method <访问权限> [修饰关键字] <方法名称、参数还有返回值>
<.registers> // 指定了方法中寄存器的总数,这个数量是参数寄存器和本地寄存器的总和。
<.local> // 这个指令表明方法中非参数寄存器(本地寄存器)的个数
[.param] // 表明了方法的参数,每个.param指令表示一个参数,方法使用了几个参数就有几个.param指令。
[.prologue] // 表明了方法中代码的开始处
[.line] // 表名了该处代码在源代码中的行号
<代码体>
.end method
除了顶头的注释和直接方法不同,其他没什么区别,我们来看一个例子,例如:
# virtual methods
// 定义虚方法
.method public final createConnectionKeepAliveStrategy()Lorg/apache/http/conn/ConnectionKeepAliveStrategy;
// 方法名为createConnectionKeepAliveStrategy,返回值类型为ConnectionKeepAliveStrategy对象
// final关键字表示该方法不能被子类重写
.locals 1
// 定义本地寄存器数为1
new-instance v0, Lcom/alipay/android/phone/mrpc/core/f;
// 创建一个com.alipay.android.phone.mrpc.core.f类的实例对象,并将其引用赋值给v0
invoke-direct {v0, p0}, Lcom/alipay/android/phone/mrpc/core/f;->(Lcom/alipay/android/phone/mrpc/core/d;)V
// 调用v0对象的构造方法,将p0对象作为参数传入。
// 这里要注意v0寄存器中存放的是com.alipay.android.phone.mrpc.core.f类的实例对象,也可以说是java中的this。
return-object v0
// 将v0对象返回
.end method
// 方法结束
接口
# interfaces
.implements <接口名>
例如:
# interfaces
.implements Ljava/lang/reflect/InvocationHandler;
// 当前类实现了接口Ljava/lang/reflect/InvocationHandler;
注解
在讲注解之前先解释一下什么是类型签名:在Java中,每个类、接口、数组、枚举、注解和基本类型都有一个唯一的类型签名。类型签名是一个字符串,用来描述该类型的全限定名、类型参数、维度等信息。类型签名的格式如下:
::= | |
::= L ; *
::= [
::= T ;
::= *
::= |
::= ? ?
::= extends | super
::= |
下面是类型签名的详细解释:
:表示一个类型签名。它可以是
、
或
。
:表示一个类类型。它的格式为L ; *
,其中
是类的全限定名,使用斜杠(/)分隔包名和类名,例如java/lang/String
。
表示类的泛型参数,可以有多个。
:表示一个数组类型。它的格式为[
,其中
是数组元素的类型。
:表示一个类型变量。它的格式为T ;
,其中
是类型变量的标识符。
:表示一个或多个类型参数。它的格式为*
,即可以有零个或多个类型参数。
:表示一个类型参数。它可以是
或
。
:表示一个通配符类型参数。它的格式为? ?
,其中
表示通配符的上界或下界。
:表示通配符的上界或下界。它可以是extends
或super
,表示通配符的上界或下界是某个参考类型。
:表示一个参考类型。它可以是
或
。在Smali代码中,类型签名的格式与Java基本一致,只是用L来表示类类型,用[来表示数组类型,用T来表示类型变量。例如,类型签名Ljava/lang/Deprecated表示的是Java内置类Deprecated的类型签名。
注解的格式:
# annotation
.annotation [注解属性] <注解类名>
[注解字段 = 值]
.end annotation
在Smali中,可以使用注解来提供额外的信息和指令给编译器和虚拟机。
以下是Smali注解的一些常见规范和详解:
.annotation
:用于声明一个注解类型。语法为.annotation {}
,其中
可以是public
、protected
、private
或package
,
是注解类型的全限定名,
是注解的元素。.subannotation
:用于声明一个嵌套注解类型。语法为.subannotation {}
,与.annotation
类似。.end annotation
:用于结束一个注解类型的声明。.enum
:用于声明一个枚举类型。语法为.enum {}
,其中
可以是public
、protected
、private
或package
,
是枚举类型的全限定名,
是枚举的元素。.end enum
:用于结束一个枚举类型的声明。.field
:用于为字段添加注解。语法为.field {}
,其中
是字段的声明,
是字段的注解。.method
:用于为方法添加注解。语法为.method {}
,其中
是方法的声明,
是方法的注解。.parameter
:用于为方法参数添加注解。语法为.parameter
,其中
是参数的索引,从0开始,
是参数的注解。.prologue
:用于指定方法的前导代码。.line
:用于指定源代码中的行号。.end method
:用于结束一个方法的声明。.catch
:用于捕获异常。语法为.catch {}
,其中
是异常类型的全限定名,
是捕获异常后的处理代码。.catchall
:用于捕获所有异常。语法为.catchall {}
,其中
是捕获异常后的处理代码。.locals
:用于指定方法中的本地变量数量。语法为.locals
,其中
是本地变量的数量。.end
:用于结束一个代码块或注解的声明。注解的作用范围可以是类、方法或者字段。
如果注解的作用范围是类,".annotation"指令会直接定义在smali文件中,例如:
.class public interface abstract annotation Ll/۬ۘ;
.super Ljava/lang/Object;
.source "H5TW"
# annotation
// 定义注解
.annotation runtime Ljava/lang/annotation/Retention;
// 定义一个运行时注解Retention
// runtime表示该注解在运行时可见,即可以通过反射获取该注解信息
// Ljava/lang/annotation/Retention表示Retention注解的类型签名
value = .enum Ljava/lang/annotation/RetentionPolicy;->CLASS:Ljava/lang/annotation/RetentionPolicy;
// 枚举类型,表示Retention注解的保留策略为CLASS
// .enum表示定义一个枚举类型
// Ljava/lang/annotation/RetentionPolicy表示RetentionPolicy枚举的类型签名
// ->表示枚举类型的访问符
// CLASS表示RetentionPolicy枚举的一个值,表示注解保留在类文件中,但在运行时不可见
.end annotation
// 注解定义结束
如果是方法或字段,".annotation"指令则会包含在方法或字段中定义。
注解方法,例如:
.method public onTouch(Landroid/view/View;Landroid/view/MotionEvent;)Z
.locals 0
.annotation build Landroid/annotation/SuppressLint;
// 定义一个编译时注解SuppressLint
// build表示该注解是Android SDK提供的注解
// Landroid/annotation/SuppressLint表示SuppressLint注解的类型签名
value = {
"ClickableViewAccessibility"
}
// 定义注解的属性value,表示需要忽略的lint警告类型
// "ClickableViewAccessibility"表示需要忽略的lint警告类型,即点击事件缺少无障碍支持
.end annotation
// 注解定义结束
注解字段,例如:
.field public top:F
.annotation runtime Ljava/lang/Deprecated;
// 定义一个运行时注解Deprecated
// runtime表示该注解在运行时可见,即可以通过反射获取该注解信息
// Ljava/lang/Deprecated表示Deprecated注解的类型签名
.end annotation
// 注解定义结束
.end field