文章目录
1. Dalvik 指 令格式
instruction-formats.html (下载再用浏览器打开)举了 Dalvik
指令的所有格式。
2. DEX 反汇编工具
目前 主流的 DEX 文件反汇编工具有 Android 官方的 dexdump 和第三方的 baksmali。
dexdump
使用的都是以 “v’ 开头的寄存器, baksmali
则同时使用以 “v” 和 “p”开头的寄存器 ( 这涉及 Dalvik 寄存器的命名法)。 两者的寄存器表示法不同, baksmali
使用的是p 命名法
, dexdump
使用的是 v 命名法
。
Dalvik 虚拟机是基于寄存器架构的, 其代码中使用了大量的寄存器,Dalvik 使用的寄存器都是 32
位的, 支持所有类型。 对 64 位类型,可以用两个相邻的寄存器来表示, Dalvik 虚拟机最多支持65536
个虚拟寄存器[v0, v65535]。
3. p 命名法
p 命名法
解释如下:假设一个函数使用了 M
个寄存器, 且该函数有N
个参数。参数使用最后的 N
个寄存器,局部变量使用从 v0
开始的 M- N
个寄存器。
举例:
假定一个函数使 用了
5
个寄存器和2
个显式的整型参数,该函数是 Hello 类的非静态方法,它在被调用时会传 入一个隐式的 Hello 对象引用(this指针
) ,因此, 实际上传入了3
个 参数。
根据传参规则,局部变量将使用前2个寄存器(v0
,v1
), 参数将使用后3个寄存器(p0
,p1
,p2
), p0
用于表示被传入的 Hello 对象引用。
使用 p 命名法
表示的 Dalvik 汇编代码, 通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器, 所以大多是使用 p 命名法
, 也就是采用smali
语法格式。
4. Dalvik 字节码
Dalvik 字节码有 自己的类型、方法及字段表示方法, 这些内容与Dalvik 虚拟机指令集一起组成了 Dalvik 汇编代码。
4.1 类型
Dalvik 字节码只有两种类型,分别是基本类型
和引用类型
。Dalvik 使用这两种类型来表示Java语言的全部类型。除了对象和 数组属于引用对象,其他的 Java 类 型都属 于基本类型。
Dalvik 字节码类型描述符表:
语法 | 含义 |
---|---|
V | void, 只用 于返回值类型 |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | java类类型 |
[ | 数组类型 |
每个Dalvik 寄存器都是 32 位的。对长度小于或等于 32 位 的类型 来说,只用一个寄存器就可以存放该类型的值, 而对J
、 D
等 64 位的类型来说, 它们的值要使用相邻的两个寄存器来存储,例如 v0与 v1 、v3 与 v4。
L
类型可以表示Java 类型中的任何类。 这些类在 Java 代码中以 package.name.ObjectName
的方式被引用。 在 Dalvik 汇编代码中,它们以 “Lpackage/name/ObjectName;
” 的形 式表示 (注意 :最后有一个分号)。
L
表示后面跟着一个 Java 类, package/name/
表示对象所在的包, ObjectName
表示对象的名称,分号
表示对象名结束。
例如, Ljava/lang/String;
相当于 java.lang. String
( 代码中很容易忘记把.
替换成/
) 。
[
类 型可以表示所有基本类型的数组。[
后 面紧跟基本类型描述符,例如 “[I
” 表示一个整型一维数组, 相当于 Java 中的 int[]
。
多个 “ [
” 放在一起, 可以表示多维数组, 例如 “[[I
” 表示 “int[][]
”、 “[[[I
” 表示 “int[][][]
” 。
需要注意的是,多维数组的最大维数为 255。
L
与 [
可以同时使用,以表示对象数组。 例如, “[Ljava/lang/String;
” 表示 Java 中的字符串数组。
4.2 方法
方法的表现形式要比类型复杂一些。Dalvik 使用方法名、 类型参数与返回值来 详细描述一个方法。一方面, 这样做有助于 Dalvik 虚拟机在运行时从方法表中快速找到正确的方法;另一方面,Dalvik 虚拟机可以使用它们来进行一些静态分析, 例如 Dalvik 字节码的验证与优化。方法的格式如下。
Lpackage/name/ObjectName; ->MethodName(III)Z
“Lpackage/name/ObjectName;
” 应该被理解为一个类型, MethodName
为具体的方法名,(III)Z
是方法的签名 部分,括号内的 III
为方法的参数(在此为三个整型参数), Z
表示方法的返回类型( boolean
类型 ) 。
一个更为复杂的例子如下:
method (I[[IILjava/lang/String; [Ljava/lang/Object;)Ljava/lang/String;
将其转换成 Java 形式的代码, 具体如下:
String method(int,int[][],int,String,Object[])
baksmali
生成的方法代码以 .method
指令开始,以 .end method
指令结束, 根据方法类型的不同,在方法指令前可能会用来添加注释。例如, “# virtual methods
" 表示这是一个虚方法,“# direct methods
” 表示这是一个直接方法。
4.3 字段
字段与方法相似,只是字段没有方法签名域中的参数和返回值, 取代它们的是字段的类型。Dalvik 虚拟机在定位字段与进行字节码静态分析时会用到它。字段的格式如下。
Lpackage/name/ObjectName; ->FieldName:Ljava/lang/String;
字段 由类型(Lpackage/name/ObjectName;
) 、字段名 ( FieldName
) 与字段类型 ( Ljava/lang/String;
) 组成, 字段名与字段类型用冒号
分开。
baksmali
生成的字段代码以 .field
指令开头, 根据字段类型的不同, 在字段指令前可能会"#
"用来添加注释。例如, “#instance fields
” 表示这是一个实例字段, “#static fields
” 表示这是一个静态字段。
5. Dalvik 指令集
对于 Android 4.4 之前的系统,可以在 DexOpcodes.h 中 找到完整的 Dalvik 指令集。
对于 Android 4.4 及之后以 ART 为主导的系统, 可以在dex_instruction.h中找 到系统支持的完整的指令集定义。
从这两份指令集的定义文档 中可以 发现, Dalvik 指令集使用了单字节的指令助记符。
5.1 指令类型
Dalvik 指令在调用格式上模仿了 C 语言的调用 约定。 Dalvik 指令的语法与助词符有如下特点:
- 参数采用从目标 (destination ) 到源 (source ) 的 方式。
- 根据字节码大小与类型的不同, 为一些字节码添加了名称后缀以消除歧义。
- 32 位常规类型的字节码未添加任何后缀。
- 64 位常规类型的字节码添加
-wide
后缀。 - 对特殊类型的字节码, 根据具体类型添加后缀, 可以是 -boolean、 -byte、 -char、 -short、 -int、 -long、 -float、 -double、 -object、 -string、 -class、 -void中的一个。
- 根据字节码布局与选项的不同, 为一些字节码添加了字节码后缀以消除 歧义。这些后缀通过在字节码主名称后添加斜杠来分隔。
- 在指令集的描述中,宽度值中的每个字母都表示
4
位的宽度。
以指令 move-wide/froml6 vAA, vBBBB
为例:
move
为基础字节码 ( base opcode ) , 表示这是一个基本操作;
-wide
为名称后缀 ( name suffix ) , 表示指令操作的数据宽度 (64
位);
from16
为字节码后缀 ( opcode suffix ) , 表示源为一个16位 的寄存 器引用变量;
vAA
为 目的寄存器,它始终在源的前面, 取值范围为v0~v255;
vBBBB
为源寄存器,取值范围为 v0 ~ v65535。
Dalvik 指令集中的大多数指令都使用寄存器作为目的操作数或源操作数。 Dalvik 指令集中的大多数指令都使用寄存器作为目的操作数或源操作数。
A、B、C、D、E、 F、G、H 代表 4
位的数值,可用于表示数值0 -15
或寄存器 v0~vl5
;
AA、BB、CC、DD、EE、 FF、GG、HH 代表 8
位的数值,可用于表示数值0 -255
或寄存器 v0~v255
;
AAAA、BBBB、CCCC、DDDD、EEEE、 FFFF、GGGG、HHHH 代表 16
位的数值,可用于表示数值0 -65535
或寄存器 v0~v65535
;
所以对于v0~v15
,它可以视为vAA
或vBBBB
, 如 move-wide/froml6 v0, v1
。
5.2 空操作指令
空操作指令的助记符为 nop
, 它的值为 00
。 nop 指令通常用于对齐代码,不进行实际操作。
5.3 数据操作指令
数据操作指令为 move
其原型为 move destination, source
。
根据字节码大小与类型的不同,move
指令有不同的后缀, 列举如下。
move vA, vB
指令用于将vB
寄存器的值赋予vA
寄存器,源寄存器与目的寄存器都为4
位。move/froml6 vAA, vBBBB
指令用于将vBBBB
寄存器的值赋予vAA
寄存器,源寄存器为16
位,目的寄存器为8
位。move/16 vAAAA, vBBBB
指令用于将vBBBB
寄存器的值赋予vAAAA
寄存器,源寄存器与目的寄存器都为16
位。move-wide vA, vB
指令用于为4
位的寄存器对
赋值, 源寄存器与目的寄存器都为4
位。move-wide/froml6 vAA, vBBBB
与move-wide/16 vAAAA, vBBBB
指令用于使move-wide
相同。move-object vA, vB
指令用于为对象赋值,源寄存器与目的寄存器都为4
位。mov-object/froml6 vAA, vBBBB
指令用于为对象赋值,源寄存器是16
位的, 目的寄存器是8
位的。move-object/16 vAAAA, vBBBB
指令用于为对象赋值, 源寄存器与目的寄存 器都是16
位的。move-result vAA
指令用于将上一个invoke
类型指令操作的单字非对象结果
赋予vAA
寄存器。move-result-wide vAA
指令用于将上一个invoke
类型指令 操作的双字非对象结果
赋予vAA
寄存器。move-result-object vAA
指令用于将上一个 invoke 类型指令操作的对象结果赋予vAA
寄存器。move-exception vAA
指令用于将一个在运行时发生的异常 保存到vAA
寄存器中。这条指令必须在异常发生时由异常 处理器使用, 否则指令无效。
wide
针对的是类型为 double
或 long
。
5.4 返回指令
返回指令是指函数结 束时运行的最后一条指令, 它的基础字节码为 return
有如下四条返回指令。
return-void
指令表示函数从一个void
方法返回。return vAA
指令表示 函数返 回一个32
位非对象类型的值, 返 回值为8
位 寄存器vAA
。return-wide vAA
指令表示 函数返 回一个64
位非对象类型的值, 返 回值为8
位寄存器对
vAA
。return-object vAA
指令表示函数返回一 个对象类型的值, 返 回值为8
位寄存器vAA
。
5.5 数据定义指令
数据定义指令用于定义程序中用到的常量、字符串、类等数据, 它的基础字节码为 const
列举如下。
const/4 vA, #+B
指令用于将数值符号扩展为32
位后赋予寄存器vA
。const/16 vAA, #+BBBB
指令用于将数值符号扩展为32
位后赋予寄存器vAA
。const vAA, #+BBBBBBBB
指令用于将数值赋予寄存器vAA
。const/highl6 vAA, #+BBBB0000
指令用于将数值右边的0
扩展为32
位后赋予寄存器vAA
。const-wide/16 vAA, #+BBBB
指令用于将数值符号扩展为64
位后赋予寄存器对
vAA
。const-wide/32 vAA, #+BBBBBBBB
指令用于将数值符号扩展为64
位后赋予寄存器对
vAA
。const-wide vAA, #+BBBBBBBBBBBBBBBB
指令用于将数值赋予寄存器对
vAA
。const-wide/highl6 vAA, #+BBBB000000000000
指令用于将数值右边的0
扩展为64
位后赋予寄存器对
vAA
。const-string vAA, string@BBBB
指令用于通过字符串索引构造一个字符串, 并将其赋予寄存器vAA
。const-string/jumbo vAA, string@BBBBBBBB
指令用于通过字符 串索引 (较大 ) 构造一个字符串, 并将其赋予寄存器vAA
。const-class vAA, type@BBBB
指令用于通过类型索引获取一个类 引用, 并将其赋予寄存器vAA
。const-class/jumbo vAAAA, type@BBBBBBBB
指令用于通过给定的类型索引获取一个类引用, 并将其赋予寄存器vAAAA
。 这条指令占用 2 字节, 值为 0x00ff(Android 4.0 中新增的指令 )。
很多人会被const/4
, const/16
搞晕,其实这应该只是davik虚拟机在解析dex文件的时候根据int值进行了优化, 如下例子:
int testInt() {
// smali: const v0, 0x7fffffff
return Integer.MAX_VALUE; // const 默认4字节 32位
}
int test4Int() {
// smali: const/4 v0, 0x1
return 1; // const/4 半个字节 4位
}
int test16Int() {
// smali: const/16 v0, 0x64
return 100; // const/16 2个字节 16位
}
short testSHort() {
// smali: const/16 v0, 0x7fff
return Short.MAX_VALUE;// const/16 2个字节 16位
}
double testDouble() {
// smali: const-wide v0, 0x7fefffffffffffffL # Double.MAX_VALUE
return Double.MAX_VALUE;// const-wide 8个字节 64位
}
double testLong() {
// smali: const-wide/high16 v0, 0x43e0000000000000L # 9.223372036854776E18
return Long.MAX_VALUE;// const-wide 8个字节 64位
}
byte testByte() {
// smali: const/16 v0, 0x7f
return Byte.MAX_VALUE;// const/16 2个字节 16位
}
5.6 锁指令
锁指令多用在多线程程序对 同一对 象的操作中。 Dalvik 指令集中如下有两条锁指令。
monitor-enter vAA
指令用于为指定对象获取锁。monitor-exit vAA
指令用于释放指定对象的锁。
5.7 实例操作指令
与实例相关的操作包括实例的类型转换、检查及创建等,列举如下。
check-cast vAA, type@BBBB
指令用于将vAA
寄存器中的对象引用转换成指定的类型,如果失败会抛出ClassCastException
异常。如果类型B
指定的是基本类型,那么对非基本类型的类型A
来说, 运行将会失败。instance-of vA, vB, type@CCCC
指令用于判断vB
寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA
寄存器赋值1
, 否则为vA
寄存器赋值为0
。new-instance vAA, type@BBBB
指令用于构造一个指定类型对象的新实例, 并将对象引用赋值给vAA
寄存器。类型符type
指定的类型不能是数组类。check-cast/jumbo vAAAA, type@BBBBBBBB
指令的功能与check-cast vAA, type@BBBB
指令相同, 只是前者的寄存器与指令索引的取值范围更大 (Android4.0 中新增的指令 )。instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC
指令的功能与instance-of vA, vB, type@CCCC
指令相同,只是前者的寄存器与指令索引的取值范围更大 (Android4.0 中新增的指令 )。new- instance/ jumbo vAAAA, type@BBBBBBBB
指令的功能与new-instance vAA, type@BBBB
指令相同,只是 前者的寄存器与指令索引的取值范 围更大 (Android4.0 中新增的指令 )。
5.8 数组操作指令
数组操作包括获取数组长度、新建数组、数组赋值、数组元素取值与赋值等, 列举如下。
array-length vA, vB
指令用于获取给定vB
寄存器中数组的长度, 并将值赋予vA
寄存器。 数组长度值就是数组中条目的个数。new-array vA, vB, type@CCCC
指令用于构造指定类型 (type@CCCC
) 和大小 (vB
) 的数组,并将值赋予vA
寄存器。filled-new-array {vC, vD, vE, vF, vG}, type@BBBB
指令用于构造指定类型 (type@BBBB
)和大小 (vA
) 的数 组并填充数组内容。vA
寄存器是隐含使用的,除了指定 数组的大小,还指定了参数的个数。vC~vG
是所使用的参数寄存器序列。filled- new- array/ range {vCCCC …vNNNN}, type@BBBB
指令的功能与filled-new-array {vC,vD, vE, vF, vG}, type@BBBB
指令相同,只是参数寄存器使用range
字节码后缀来指定取值范围。vC
是第 1 个参数寄存器,N= A + C-1
。fill-array-data vAA, +BBBBBBBB
指令用指定的数据来填充数组,vAA
寄存器为数组引用(引用 的必须 是基础类型的数组),在指令后面会紧跟一个数据表。new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC
指令的功能与new-array vA, vB, type@CCCC
指令相同,只是寄存器与指令索引的取值范围更大 (Android4.0 中新增的指令 )。filled -new- a r ray/ jumbo{ vCCCC... vNNNN}, type@BBBBBBBB
指令的功能与filled -new- array/range {vCCCC ... vNNNN}, type@BBBB
指令 相同,只是前者 的指令索引的取值范 围更大( Android 4.0 中新增的指令 ) 。arrayop vAA, vBB, vCC
指令用于对vBB
寄存器指定的数组元素进行取值与赋值。vCC
寄存器用于指定数组元素的索引。vAA
寄存器用于存放读取的或需要设置的数组元素的值。 读取元素时使用aget
类指令, 为元素赋值时使用aput
类指令。 根据数组中存储的类型 指令的不同, 在指令后面会紧跟不同的指令后缀。指令包括 aget 、 aget-wide 、 aget-object 、aget-boolean 、 aget-byte 、 aget-char 、 aget-short 、 aput 、 aput-wide 、 aput-object 、 aputboolean 、 aput-byte 、 aput-char aput-short。
5.9 异常指令
Dalvik 指令集中有一条用于抛出异常的指令, 具体如下。
throw vAA
指令用于抛出vAA
寄存器中指定类型的异常。
5.10 跳转指令
跳转指令用于从当前地址跳转到指定的偏移处。Dalvik 指令集中有三种跳转指令, 分别是无条件跳转指令 goto
、 分支跳转指令 switch
与条件跳转指令 if
。
-
goto +AA
指令用于无条件跳转到指定偏移处, 偏移量AA
不能为0
。 -
goto/16 +AAAA
指令用于无条件跳转到指定偏移处,偏移量AAAA
不能为0
。 -
goto/32 +AAAAAAAA
指令用于无条件跳转到指定偏移处。 -
packed-switch vAA, +BBBBBBBB
分支跳转指令,vAA
寄存器为switch
分支中需要判断的值,BBBBBBBB
指向一个packed-switch-payload
格式的偏移表,表中的值是递增的偏移量。 -
sparse-switch vAA, +BBBBBBBB
分支跳转指令,vAA
寄存器为switch
分支中需要判断的值,BBBBBBBB
指向一个sparse-switch-payload
格式的偏移表, 表中的值是无规律的偏移量。 -
if-test vA, vB, +CCCC
条件跳转指令用于比较vA
寄存器与vB
寄存器的值。 如果比较结果满足,就跳转到CCCC
指定的偏移处。偏移量CCCC
不能为0
。if-test
类型的指令如下:if-eq:
如果vA
等于vB
则跳转, 其 Java 语法表示为“if(vA =vB)”
。if-ne:
如果vA
不等于vB
则跳转, 其 Java 语法表示为“if(vA!=vB)”
。if-It:
如果vA
小于vB
则跳转, 其 Java 语法表示为“if(vA<vB)”
。if-ge:
如果vA
大于等于vB
则跳转, 其 Java 语法表示为“if(vA>=vB)”
。if-gt:
如果vA
大于vB
则跳转,其 Java 语法表示为 “if(vA>vB)
” 。if-le:
如果vA
小于等于vB
则跳转 , 其 Java 语法表示为 “if(vA<=vB)
” 。
-
if-testz vAA, +BBBB
条件跳转指令将vAA
寄存器的值与0
进行 比较。 如果比较结果满足或值为0
, 就跳转到BBBB
指定的偏移处。 偏移量BBBB
不能为0
。if-testz
类型的指令如下。if-eqz:
如果vAA
为0
则跳转,其 Java 语法表示为 “if(!vAA)
” 。if-nez:
如果vAA
不为0
则跳转 , 其 Java 语法表示为 “if(vAA)
” 。if-ltz:
如果vAA
小于0
则跳转, 其 Java 语法表示为 “if(vAA<0)
” 。if-gez:
如果vAA
大于等于0
则跳转, 其 Java 语法表示为 “if(vAA>=0)
” 。if-gtz:
如果vAA
大于0
则跳转, 其 Java 语法表示为 “if(vAA>0)
”。if-lez:
如果vAA
小于等于0
则跳转,其 Java 语法表示为 “if(vAA<=0)
” 。
5.11 比较指令
比较 指令用于对两个寄存器的值 (浮点 型或长整型) 进行 比较, 格式为 cmpkind vAA, vBB,vCC
, 其中 vBB
与 vCC
是需要比 较的两个寄存器或两个寄存器对,比较的 结果将被放到 vAA
寄存器中。 Dalvik 指令集中共有五条比较指令, 列举如下。
cmpl-float
指令用于比较两个单精度浮点数。 如果vBB
寄存器的值大于vCC
寄存器的值,则结果为-1
; 如果vBB
寄存器的值等于vCC
寄存器的值, 则结果为0
; 如果vBB
寄存器的值小于vCC
寄存器的值, 则结果为1
。cmpg-float
指令用于比较两个单精度浮点数。 如果vBB
寄存器的值大于vCC
寄存器的值,则结果为1
; 如果vBB
寄存器的值等于vCC
寄存器的值, 则结果为0
; 如果vBB
寄存器的值小于vCC
寄存器的值, 则结果为-1
。cmpl-double
指令用于比较两个双精度浮点数。 如果vBB
寄存器的值大于vCC
寄存器的值,则结果为-1
; 如果vBB
寄存器的值等于vCC
寄存器的值, 则结果为0
; 如果vBB
寄存器的值小于vCC
寄存器的值, 则结果为1
。cmpg-double
指令用于比较两个双精度浮点数。 如果vBB
寄存器的值大于vCC
寄存器的值,则结果为1
; 如果vBB
寄存器的值等于vCC
寄存器的值, 则结果为0
; 如果vBB
寄存器的值小于vCC
寄存器的值, 则结果为-1
。cmp-long
指令用于比较两个长整型数。 如果vBB
寄存器的值大于vCC
寄存器的值,则结果为1
; 如果vBB
寄存器的值等于vCC
寄存器的值, 则结果为0
; 如果vBB
寄存器的值小于vCC
寄存器的值, 则结果为-1
。
5.12 字段操作指令
字段操作指令用于对对象实例的字段进行读写操作, 字段 的类型可以是 Java 中 有效的数据类型。 对普通字段操作与静态字段操作,有两种指令集 , 分别是 iinstanceop vA, vB, field@CCCC
与 sstaticop vAA, field@BBBB
。
普通字段的指令前缀为 i
, 例如,对普通字段,读操作使用 iget
指令,写操作使用 iput
指令。静态字段的指令前缀为s
,例如,对静态字段, 读操作使用 sget
指令, 写操作使用 sput
指令。
根据访问的字段类型不同, 字段操作指令后面会紧跟字段类型的后缀。例如,iget-byte
指令表示读取实例字段的值的类型为字节型,iput-short
指令表示设置实例字段的值的类型为短整型。
这两类指令的操作结果是一样的 , 只是指令前缀与操作的字段类型不同。
- 普遍字段操作指令有 : iget、 iget-wide、 iget-object、iget-boolean 、 iget-byte 、igetchar 、iget-short 、 iput 、iput-wide 、 iput-object 、iput-boolean 、iput-byte 、iput-char 、iput-short。
- 静态字段操作 指令有 : sget、 sget-wide、 sget-object、sget-boolean 、 sget-byte、 sgetchar 、sget-short 、sput 、 sput-wide 、sput-object 、sput-boolean 、 sput-byte 、sput-char 、sput-short。
在 Android 4.0 中, Dalvik 指令集增加了 iinstanceop/ jumbo vAAAA, vBBBB, field@CCCCCCCC
与sstaticop/jumbo vAAAA, f ield@BBBBBBBB
两类指令。它们的作用与上面介绍的两类指令相同, 并在指令中增加了 jumbo
字节码后缀,且寄存器与指令索引的取 值范围更大。
5.13 方法调用指令
方法调用指令负责调用类实例的方法, 基础指令为 invoke
。
方法调用指令有 invoke-kind {vC,vD, vE,vF,vG}, meth@BBBB
与 invoke-kind/range {vCCCC ... vNNNN} ,meth@BBBB
两类。 这两类指令在作用上并无不同,只是后者在设置参数寄存器时使用 range 来指定寄存器的范围。根据方法类型的不同, 共有如下五条方法调用指令。
invoke-virtual
或invoke-virtual/range
指令用于调用实例的虚方法。invoke-super
或invoke-super/range
指令用于调用实例的父类方法。invoke-direct
或invoke-direct/range
指令用于调用实例的直接方法。invoke-static
或invoke-static/range
指令用于调用实例的静态方法。invoke-interface
或invoke-interface/range
指令用于调用实例的接口方法。
在 Android 4.0 中, Dalvik 指令集增加了 invoke- kind/jumbo {vCCCC…vNNNN}, meth@BBBBBBBB
这类指令。它与上面介绍的两类指令作用相同, 但是在指令中增加了 jumbo
字节码后缀, 且寄存
器与指令索引的取值范围更大。
方法调用指令的返回值必须用 move-result*
指令来获取, 示例如下。
invoke-static{}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0
5.14 数据转换指令
数据转换指令用于将一种类型 的数值 转换成另一种类型的数值, 格式为 unop VA, vB
。在 VB
寄存器或 vB
寄存器对
中存放了需要转换的数据。 转换结果保存在 vA
寄存器或 vA
寄存器对
中。数据转换指令列举如下。
neg-int
指令用于对整型数求补。not-int
指令用于对整型数求反。neg-long
指令用于对长整型数求补。not-long
指令用于对长整型数求反。neg-float
指令用于对单精度浮点型数求补。neg-double
指令用于对双精度浮点型数求补。int-to-long
指令用于将整型数转换为长整型数。int-to-float
指令用于将整型数转换为单精度浮点型数。int-to-double
指令用于将整型数转换为双精度浮点型数。long-to-int
指令用于将长整 型数转换为整型数。long-to-float
指令用于将长整型数转换为单精度浮点型数。long-to-double
指令用于将长整型数转换为双精度浮点型数。float-to-int
指令用于将单精度浮点型数转换为整型数。float-to-long
指令用于将单精度浮点型数转换为长整型数。float-to-double
指令用于将单精度浮点型数转换为双精度浮点型数。double-to-int
指令用于将双精度浮点型数转换为整型数。double-to-long
指令用于将双精度浮点型数转换为长整型数。double-to-float
指令用于将双精度浮点型数转换为单精度浮点型数。int-to-byte
指令用于将整型转换为字节型。int-to-char
指令用于将整型转换为字符串。int-to-short
指令用于将整 型转换为短整型。
5.15 数据运算指令
数据运算指令包括算术运算指令与逻辑运算指令。 算术运算指令主要用于进行数值间的加 、减、 乘、除、 模、 移位等运算, 逻辑运算指令主要用于进行数值间的与、 或、 非、 异或等运算。
数据运算指令有如下四类 (由于数据运算可能在寄存器或寄存器对之间进行,下面讲解指令的作用时均使用寄存器来描述 )。
binop vAA, vBB, vCC
指令用于将vBB
寄存器与vCC
寄存器进行运算, 将运算结果保存到vAA
寄存器中。binop/2addr vA, vB
指令用于将vA
寄存器与vB
寄存器进行运算, 将运算结果保存到vA
寄存器中。binop/litl6 vA, vB, #+CCCC
指令用于将vB
寄存器与常量CCCC
进行运算, 将运算结果保存到vA
寄存器中。binop/lit8 vAA, vBB, #+CC
指令用于将vBB
寄存器与常量CC
进行运算, 将运算结果保存到vAA
寄存器中。
后三类指令比第一类指令分别多出了 2addr、 litl6、 lit8 等指令后缀。在这四类指令中, 基础字节码相同的指令执行的运算是类似的。
在第一类指令中,根据数据类型的不同, 会在基础字节码后面加上不同的后缀,例如 -int
和 -long
分别表示操作的数据类型为整型
和长整型
。第一类指令可归类如下。
add-type:
将vBB
寄存器的值与vCC
寄存器的值进行加法运算 (vBB + vCC
) 。sub-type:
将vBB
寄存器的值与vCC
寄存器的值进行减法运算 (vBB-vCC
) 。mul-type:
将vBB
寄存器的值与vCC
寄存器的值进行乘法运算 (vBB x vCC
) 。div-type:
将vBB
寄存器的值与vCC
寄存器的值进行除法运算 (vBB/vCC
) 。rem-type:
将vBB
寄存器的值与vCC
寄存器的值进行模运算 (vBB % vCC
) 。and-type:
将vBB
寄存器的值与vCC
寄存器的值进行与运算 (vBB AND vCC
) 。or-type:
将vBB
寄存器的值与vCC
寄存器的值进行或运算 (vBB OR vCC
) 。xor-type:
将vBB
寄存器的值与vCC
寄存器的值进行异或运算(vBB XOR vCC
) 。shl-type:
将vBB
寄存器的值 (有符号数)左移vCC
位 (vBB << vCC
) 。shr-type:
将vBB
寄存器的值 (有符号数) 右移vCC
位 (vBB >> vCC
) 。ushr-type:
将vBB
寄存器的值 (无符号数) 右移vCC
位 (vBB >> vCC
) 。
基础字节码后面的 -type
可以是 -int
、 -long
、 -float
、 -double
。 后三类指令与之类似,此处不再列出。
6. Dalvik 指令练习
6.1 编写 smali 文件
新建文本文件 HelloWorld.smali
, 建议使用VSCode的扩展来编辑,可以提示错误 , 写出HelloWorld
类的程序框架,具体如下。
.class public LHelloWorld; #定义类名
.super Ljava/lang/Object; #定义父类
.method public static main([Ljava/lang/String;)V #声明静态main()方法
.registers 4 #程序使用v0, v1, v2寄存器和一个参数寄存器
.prologue #代码起始指令
###空指令
nop
nop
nop
#数据定义指令, v0=8,v1=5,v2=3
const/16 v0, 0x8
const/4 v1, 0x5
const/4 v2, 0x3
#数据操作指令,v1=v2=3
move v1, v2
#数组操作指令,v0=new int[8],v1 = v0.length=8
new-array v0, v0, [I
array-length v1, v0
#实例操作指令,v1=new StringBuilder()
new-instance v1, Ljava/lang/StringBuilder;
#方法调用指令,v1调用StringBuilder构造函数
invoke-direct {v1},Ljava/lang/StringBuilder;-><init>()V
#跳转指令,如果v0!=0,就跳转到cond_0,否则跳转到goto_0
if-nez v0, :cond_0
goto :goto_0
:cond_0
#数据转换指令, v2= (float)3
int-to-float v2, v2
#数据运算指令, v2 = v2+v2 =(float)6
add-float v2, v2, v2
#比较指令, v2==v2所以v0=0
cmpl-float v0, v2, v2
#字段操作指令
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello world" #构造字符串
#方法调用指令, v0 = PrintStream.out, v1="Hello world"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
#返回指令
:goto_0
return-void #返回空值
.end method
6.2 编译 smali 文件
编译 smali
文件时使用的工具是 smali,它的使用说明如下:https://github.com/JesusFreke/smali/wiki/SmaliBaksmali2.2
打开命令提示符窗口, 执行如下命令进行编译。
java -jar smali-2.3.4.jar assemble HelloWorld.smali -o HelloWorld.dex
如果编译没有错误, 会在当前目录下生成 HelloWorld.dex
文件, 否则会提示错误所在的行。
6.3 测试运行
启动 Android 运行环境 ( Android 模拟器和真实的 Android 设备均可), 在命令提示符窗口执
行如下命令,测试运行结果。
adb push HelloWorld.dex /sdcard/
adb shell dalvikvm -cp /sdcard/HelloWorld.dex HelloWorld
结果如下:
7. 16进制转换dex
可使用以下网址: https://armconverter.com/
如我们使用JEB打开一份so:
==>
如果我们想把BEQ
改成BNE
==>
可以看出,直接把D0改成D1即可。