Android 软件安全与逆向分析第三章:进入 Android Dalvik 虚拟机(内含 smali 汇编学习)

Android 软件安全与逆向分析第三章:进入 Android Dalvik 虚拟机

虽然 Android 平台使用 Java 语言来开发应用程序,但 Android 程序却不是运行在标准 Java 虚拟机上的。可能是为了解决移动设备上软件运行效率的问题,也可能是为了规避与 Oracle 公司的版权纠纷。Google 为 Android 平台专门设计了一套虚拟机来运行 Android 程序,它就是 Dalvik Virtual Machine(Dalvik 虚拟机)。本章将讨论 Dalvik 虚拟机的特性及基于 Dalvik 字节码的汇编语言知识。

3.1 Dalvik 虚拟机的特点——掌握 Android 程序的运行原理

3.1.1 Dalvik 虚拟机概述

Dalvik 虚拟机作为 Android 平台的核心组件,用有如下几个特点:

  • 体积小,占用内存空间小;
  • 专有的 DEX 可执行文件格式,体积更小,执行速度更快;
  • 常量池采用 32 位索引值,寻址类方法名、字段名、常量更快;
  • 基于寄存器架构,并拥有一套完整的指令系统;
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;
  • 所有的 Android 程序都运行在 Android 系统进程里,每个进程对应着一个 Dalvik 虚拟机实例。

3.1.2 Dalvik 虚拟机与 Java 虚拟机的区别

  1. Java 虚拟机运行的是 Java 字节码,Dalvik 虚拟机运行的是 Dalvik 字节码
  2. Dalvik 可执行文件(Dex)体积更小
  3. Java 虚拟机与Dalvik 虚拟机架构不同

dx 工具负责将 Java 字节码转换为 Dalvik 字节码,它将 Java 类文件重新排列,重组常量池,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时出现重复的文件加载与解析过程。

Java 虚拟机基于栈架构,使用求值栈来传递参数(零地址形式的指令集):

例如:iload_1

i 是指令前缀,表示操作类型为 int 类型,load 表示将局部变量存入 Java 栈,下划线右边的数字代表要操作具体哪个局部变量,索引值从 0 开始计数,这里的 1 表示将第二个 int 类型的局部变量进栈。

Dalvik 虚拟机基于寄存器架构,调用栈维护一份寄存器列表,寄存器的数量在方法结构体的 registers 字段中给出,Dalvik 虚拟机会根据这个值来创建一份虚拟的寄存器列表。

指令示例:add-int v0, v3, v4(smali)

3.1.3 Dalvik 虚拟机是如何执行程序的

​ Android 系统的架构采用分层思想,好处是拥有减少各层之间的依赖性、便于独立分发、容易收敛问题和错误等优点。

​ Android 系统由 Linux 内核、函数库、Android 运行时、应用程序框架以及应用程序组成。Dalvik 虚拟机属于 Android 运行时环境,它与一些核心库共同承担 Android 应用程序的运行工作。

​ Android 系统启动加载完内核后,第一个执行的是 init 进程,init 进程会首先进行设备的初始化工作,然后读取 inic.rc 文件并启动系统中的重要外部程序 ZygoteZygote 是 Android 所有进程的孵化器进程,它启动后会首先初始化 Dalvik 虚拟机,然后启动 system_server 并进入 Zygote 模式,通过 socket 等候命令。当执行一个 Android应用程序时,system_server 进程通过 socket 方式发送命令给 Zygote,Zygote 收到命令后通过 fork 自身创建一个 Dalvik 虚拟机的实例来执行应用程序的入口函数,由此完成程序的启动。

Zygote 提供了三种创建进程的方法:

  • fork(),创建一个 Zygote 进程
  • forkAndSpecialize(),创建一个非 Zygote 进程
  • forkSystemServer(),创建一个系统服务进程

其中,Zygote 进程可以再 fork() 出其它进程,非 Zygote 进程则不能 fork 其他进程,而系统服务进程在终止后它的子进程也必须终止

​ 进程 fork() 成功后,Dalvik 虚拟机首先通过 loadClassFromDex() 函数完成类的装载工作,每个类被成功解析后都会拥有一个 ClassObject 类型的数据结构存储在运行时环境中,虚拟机使用 gDvm.loadClasses 全局哈希表来存储与查询所有装载进来的类,随后字节码验证器使用 dvmVerifyCodeFlow() 函数对装入的代码进行校验,接着虚拟机调用 FindClass() 函数查找并装载 main 方法类,随后调用 dvmInterpret() 函数初始化解释器并执行字节流。

3.1.4 关于 Dalvik 虚拟机 JIT(即时编译)

JIT(Just-in-time Compilation,即时编译),又称动态编译,是一种通过在运行时将字节码翻译为机器码的技术,使得程序执行速度更快。

主流的 JIT 包含两种字节码编译方式:

  • method 方式:以函数或方法为单位进行编译
  • trace 方式:以 trace 为单位进行编译

​ trace 方式编译能够快速地获取 “热路径”(函数中一般很少按顺序执行,多数的代码都被分成了好几条执行路径,其中的有些路径在实际运行过程中经常被在执行,这些路径被称为“热路径”,反之则被称为“冷路径”)代码,使用更短的时间与更少的内存来编译代码。

3.2 Dalvik 汇编语言基础为分析 Android 程序做准备

Dalvik 虚拟机为自己专门设计了一套指令集,并且制定了自己的指令格式与调用规范。我们将 Dalvik 指令集组成的代码称为 Dalvik 汇编代码,将这种语言称为 Dalvik 汇编语言(Dalvik 汇编语言并不是正式的语言,只是描述 Dalvik 指令集代码的一种称呼)

3.2.1 Dalvik 指令格式

一段 Dalvik 汇编代码由一系列 Dalvik 指令组成,指令语法由指令的位描述符与指令格式标识来决定。位描述符约定如下:

  • 每 16 位的字采用空格分隔开来
  • 每个字母表示四位,每个字母按顺序从高字节开始,排列到低字节。每四位之间可能使用竖线 “ | ” 来表示不同的内容
  • 顺序采用 A~Z 的单个大写字母作为一个 4 位的操作码,op 表示一个 8 位的操作码
  • “ Ø ” 来表示这字段所有的位为 0 值

指令示例:A|Gop BBBB F|E|D|C

​ 这个指令由三个16位的字组成,第一个 16 位的高八位由 A 与 G 组成,低字节由操作码 op 组成。第二个 16 位由 BBBB 组成,表示一个 16 位的偏移值。第三个16 位分别由 F、E、D、C 共四个 4 字节组成,在这里它们单独表示寄存器参数。

​ 单独使用位标识还无法确定一条指令,必须通过指令格式来指定指令的格式编码。它的约定如下:

  • 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母
  • 第一个数字是表示指令有多少个 16 位的字组成
  • 第二个数字是表示指令最多使用寄存器的个数。特殊标记 “ r ” 标识使用一定范围内的寄存器
  • 第三个字母为类型码,表示指令用到的额外数据的类型。

还有一种特殊的情况是末尾可能会多出另一个字母,如果是字母 s 表示指令采用静态链接,如果是字母 i 表示指令应该被内联处理。

指令格式标识类型码:

助记符 位大小 说明
b 8 8 位有符号立即数
c 16,32 常量池索引
f 16 接口常量(仅对静态链接格式有效)
h 16 有符号立即数(32 位或 64 位数的高值位,低值位为 0)
i 32 立即数,有符号整数或 32 位浮点数
l 64 立即数,有符号整数或 64 位双精度浮点数
m 16 方法常量(仅对静态链接格式有效)
n 4 4 位的立即数
s 16 短整型立即数
t 8,16,32 跳转、分支
x 0 无额外数据

指令格式标识示例:22x

​ 第一个数字 2 表示指令有两个 16 位字组成,第二个数字 2 表示指令使用到 2 个寄存器,第三个字母 x 表示没有使用到额外的数据。

另外,Dalvik 指令对语法做了一些说明,它约定如下:

  • 每条指令从操作码开始,后面紧跟参数,参数个数不定,每个参数之间采用逗号分开
  • 每条指令的参数从指令第一部分开始,op 位于低 8 位,高 8 位可以是一个 8 位的参数,也可以两个 4 位的参数,还可以为空,如果指令超过 16 位,则后面部分依次作为参数
  • 如果参数采用 “ vX ” 的方式表示,表明它是一个寄存器。这里采用 v 而不是 r 是为了避免与基于该虚拟机架构本身的寄存器命名产生冲突,如 ARM 架构寄存器命名采用 r 开头
  • 如果参数采用 “ #+X ”的方式表示,表明它是一个常量数字
  • 如果参数采用 “ +X ”的方式表示,表明它是一个相对指令的地址偏移
  • 如果参数采用 “kind@X ” 的方式表示,表明它是一个常量池索引值。其中 kind 表示常量池类型,它可以是 “ string ”(字符串常量池索引)、“ type ”(类型常量池索引)、“ field ”(字段常量池索引)或者 “ meth ”(方法常量池索引)

3.2.3 了解 Dalvik 寄存器

​ Dalvik 将部分寄存器映射到了 ARM 寄存器上,还有一部分则通过调用栈进行模拟,Dalvik 中用到的寄存器都是 32 位的,64 位类型用 2 个相邻寄存器表示。

​ 每个函数都在函数头部使用 .registers 指令指定函数使用到的寄存器数目,当虚拟机执行到这个函数时,会根据寄存器的数目分配适当的栈空间用来存放寄存器实际的值,虚拟机通过处理字节码,对寄存器进入读写操作,其实都是在写栈空间。Android SDK 中有一个名为 dalvik.bytecode.Opcodes 的接口,它定义了一份完整的 Dalvik 字节码列表。处理这些字节码的函数位一个宏 HANDLE_OPCODE(),这份 Dalvik 字节码列表中每个字节码的处理过程可以在 Android 源码的 dalvik\vm\mterp\c 目录中找到。

3.2.4 两种不同的寄存器表示方法——v 命名法与 p 命名法

​ v 命名法采用以小写字母 “ v ” 开头的方式表示函数中用到的局部变量与参数,所有的寄存器命名从 v0 开始,依次递增。

​ p 命名法对函数的局部变量寄存器命名没有影响,它的命名规则是:函数中引入的参数命名从 p0 开始,依次递增。

​ 使用 p 命名法表示的 Dalvik 汇编代码,通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器。

3.2.5 Dalvik 字节码的类型、方法与字段表示方法

  1. 类型

​ Dalvik 字节码只有两种类型,基本类型引用类型,除了对象与数组属于引用对象外,其他的 Java 类型都是基本类型。

Dalvik 字节码类型描述符:

语法 含义
V void,只用于返回类型
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L Java 类类型
[ 数组类型

​ L 类型可以表示 Java 类型中的任何类。这些类在 Java 代码中以 package.name.ObjectName 方式引用,到了 Dalvik 汇编代码中,它们以 Lpackage/name/ObjectName;形式表示,ObjectName 表示对象的名称,最后的分号表示对象名结束。

​ [ 类型可以表示所有基本类型的数组。[ 后面紧跟基本类型描述符,如 [i 表示一个整型一维数组,相当于 Java 中的 int[]。多个 [ 在一起的时候可表示多维数组。L 与 [ 可以同时使用用来表示对象数组。

  1. 方法

​ Dalvik 使用方法名、类型参数与返回值来详细描述一个方法,方法格式如下:

Lpackage/name/ObjectName;->MethodName(III)Z

​ 在这个例子中,Lpackage/name/ObjectName;- 应该理解为一个类型,MethodName 为具体的方法名,(III)Z 是方法的签名部分,其中括号内的 III 为方法的参数(在此为三个整型参数),Z 表示方法的返回类型(boolean 类型)

​ BakSmali 生成的方法代码以 .method 指令开始,以 .end method 指令结束,根据方法类型的不同,在方法指令开始前可能会用井号加以注释。如 # virtual methods 表示这是一个虚方法,# direct methods 表示这是一个直接方法。

  1. 字段

字段与方法很相似,只是字段没有方法签名域中的参数与返回值,取而代之的是字段的类型。字段的格式如下:

Lpackage/name/ObjectName;->FieldNam:Ljava/lang/String;

​ 字段由类型(Lpackage/name/ObjectName;)、字段名(FieldName)与字段类型(Ljava/lang/String;)组成。其中字段名与字段类型中间冒号隔开。

​ BakSmali 生成的字段代码以 .field 指令开头,根据字段类型的不同,在字段指令开始前可能会用井号加以注释。

3.3 Dalvik 指令集(smali 汇编)

3.3.1 指令特点

Dalvik 指令集在调用格式上模仿了 C 语言的调用约定。Dalvik 指令的语法与助词符有如下特点:

  • 参数采用从目标到源的方式
  • 根据字节码的大小与类型不同,一些字节码添加了名称后缀以消除歧义
    • 32位常规类型的字节码未添加任何后缀
    • 64 位常规类型的字节码添加 -wide 后缀
    • 特殊类型的字节码根据具体类型添加后缀。它们可以是 -boolean、-byte、-char、-short、-int、-long、-float、-double、-object、-string、-class、-void 之一
  • 根据字节码的布局与选项不同,一些字节码添加了字节码后缀以消除歧义。这些后缀通过在字节码的主名称后添加 / 来分隔开
  • 在指令集的描述中,宽度值中每个字母表示宽度为 4 位

示例:move-wide/from 16 vAA, vBBBB

move 为基础字节码(base opcode)。标识这是基本操作。

wide 为名称后缀(name suffix)。标识指令操作的数据宽度(64 位)

from 16 为字节码后缀(opcode suffix)。标识源为一个 16 位的寄存器引用变量

vAA 为目的寄存器。它始终在源的前面,取值范围为 v0~v255

vBBBB 为源寄存器。取值范围为 v0~v65535

3.3.2 空指令操作

​ 空操作指令的助记符为 nop。它的值为 00,通常 nop 指令被用来作对齐代码之用,无实际操作。

3.3.3 数据操作指令

​ 数据操作指令为move。move指令的原型为move destination, source或move destination, move指令根据字节码的大小与类型不同,后面会跟上不同的后缀。

move vA, vB 将 vB 寄存器的值赋给 vA 寄存器,源寄存器与目的寄存器都为 4 位。

move/from16 vAA, vBBBB 将 vBBBB 寄存器的值赋给 vAA 寄存器,源寄存器为16 位,目的寄存器为 8 位。

move/16 vAAAA, vBBBB 将 vBBBB 寄存器的值赋给 vAAAA 寄存器,源寄存器与目的寄存器都为16位。

move-wide vA, vB 为 4 位的寄存器对赋值。源寄存器与目的寄存器都为4位。

move-wide/from16 VAA, vBBBBmove-wide/16 vAAAA, vBBBB 实现与 move-wide 相同。

move-object vA, vB 为对象赋值。源寄存器与目的寄存器都为 4 位。

move-object/from16 vAA, vBBBB 为对象赋值。源寄存器为 8 位,目的寄存器为 16 位。 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 寄存器。这条指令必须是异常发生时的异常处理器的一条指令。否则的话,指令无效。

3.3.4 返回指令

​ 返回指令指的是函数结尾时运行的最后一条指令。它的基础字节码为 return,共有以下四条返回指令:

return-void 表示函数从一个 void 方法返回。

return vAA 表示函数返回一个 32 位非对象类型的值,返回值寄存器为 8 位的寄存器 vAA。

return-wide vAA 表示函数返回一个 64 位非对象类型的值。返回值为 8 位的寄存器对 vAA。

return-object vAA 表示函数返回一个对象类型的值。返回值为 8 位的寄存器 vAA。

3.3.5 数据定义指令

​ 数据定义指令用来定义程序中用到的常量、字符串、类等数据。它的基础字节码为 const。

const/4 vA, #+B 数值符号扩展为 32 位后赋给寄存器vA。

const/16 vAA, #+BBBB 将数值符号扩展为 32 位后赋给寄存器 vAA。

const vAA, #+BBBBBBBB 将数值赋给寄存器 vAA。

const/high16 vAA, #+BBBB0000 将数值右边零扩展为 32 位后赋给寄存器VAA。

const-wide/16 vAA, #+BBBB 将数值符号扩展为 64 位后赋给寄存器对 vAA。

const-wide/32 VAA, #十BBBBBBBB 将数值符号扩展为 64 位后赋给寄存器对 vAA。

const-wide vAA, #+BBBBBBBBBBBBBBBB 将数值赋给寄存器对 vAA。

const-wide/high16 vAA, #+BBBB000000000000 将数值右边零扩展为 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。这条指令占用两个字节,值为0x00ff。

3.3.6 锁指令

​ 锁指令多用在多线程程序中对同一对象的操作。Dalvik 指令集中有两条锁指令。

monitor-enter vAA 为指定的对象获取锁。

monitor-exit vAA 释放指定的对象的锁。

3.3.7 实例操作指令

​ 与实例相关的操作包括实例的类型转换、检查及新建等。

check-cast vAA, type@BBBB 将vAA寄存器中的对象引用转换成指定的类型,如果失败会抛出 ClassCastException 异常。如果类型 B 指定的是基本类型,对于非基本类型的 A 来说,运行时始终会失败。

instance-ofvA, 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 相同,只是寄存器值与指令的索引取值范围更大(Android 4.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 相同,只是寄存器值与指令的索引取值范围更大(Android 4.0 中新增的指令)。

3.3.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 是第一个参数寄存器,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-array/jumbo {vCCCC .. vNNNN}, type@BBBBBBBB 指令功能与 filled-new-array/range {vcccc .. vNNNN}, type@BBBB 相同,只是索引取值范围更大 (Android 4.0中新增的指令)。

arrayop vAA, vBB, vCC 对 vBB 寄存器指定的数组元素进入取值与赋值。寄存器指定数组元素索引,vAA 寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令,元素赋值使用 aput 类指令,根据数组中存储的类型指令后面会紧跟不同的指令后缀,指令列表有 aget、aget-wide、aget-obJ ect、aget-boolean、aget-byte、aget-char、aget-short、aput、aput-wide、aput-object、aput-boolean、aput-byte、aput-char、aput-short

3.3.9 异常指令

​ Dalvik指令集中有一条指令用来抛出异常:

throw vAA抛出 vAA 寄存器中指定类型的异常。

3.3.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-lt 如果 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-eq 如果 vAA 为 0 则跳转。Java 语法表示为 if(!vAA)

  • if-ne 如果 vAA 不为 0 则跳转。Java 语法表示为 if(vAA)

  • if-lt 如果 vAA 小于 0 则跳转。Java 语法表示为 if(vAA < 0)

  • if-ge 如果 vAA 大于等于 0 则跳转。Java 语法表示为 if(vAA >= 0)

  • if-gt 如果 vAA 大于 0 则跳转。Java 语法表示为 if(vAA > 0)

  • if-le 如果 vAA 小于等于 0 则跳转。Java 语法表示为 if(vAA <= 0)

3.3.11 比较指令

​ 比较指令用于对两个寄存器的值(浮点型或长整型)进行比较。它的格式为 cmpkind vAA, vBB, vcc,其中 vBB 寄存器与 vCC 寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到 vAA 寄存器。Dalvik 指令集中共有 5 条比较指令:

cmpl-float 比较两个单精度浮点数。如果 vBB 寄存器大于 vCC 寄存器,则结果为 -1,相等则结果为0,小于的话结果为1。

cmpg-float 比较两个单精度浮点数。如果 vBB 寄存器大于 vCC 寄存器,则结果为1,相等则结果为0,小于的话结果为1。

cmpl-double 比较两个双精度浮点数。如果 vBB 寄存器对大于 vCC 寄存器对。则结果为 -1,相等则结果为 0,小于的话结果为 1。

cmpg-double 比较两个双精度浮点数。如果 vBB 寄存器对大于 vCC 寄存器对,则结果为 1,相等则结果为 0,小于的话结果为 -1。

cmp-long 比较两个长整型数。如果 vBB 寄存器大于 vCC 寄存器,则结果为 1,相等则结果为 0,小于的话结果为· -1。

3.3.12 字段操作指令

​ 字段操作指令用来对对象实例的字段进入读写操作。字段的类型可以是 Java 中有效的数据类型。对普通字段与静态字段操作有两种指令集,分别是 iinstanceop VA, VB, field@CCCCsstaticop vAA, field@BBBB

​ 普通字段指令的指令前缀为 i,如对普通字段读操作使用 iget 指令,写操作使用 iput 指令;静态字段的指令前缀为 s,如对静态字段读操作使用 sget 指令,写操作使用 sput 指令。

​ 根据访问的字段类型不同,字段操作指令后面会紧跟字段类型的后缀,如 iget-byte 指令表示读取实例字段的值类型为字节类型,iput-short 指令表示设置实例字段的值类型为短整形。两类指令操作结果都是一样,只是指令前缀与操作的字段类型不同。

​ 普遍字段操作指令有:iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、 iget-short、iput、iput-wide、iput-object、iput-boolean、iput-byte、iput-char、iput-short

​ 静态字段操作指令有:sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、 sget-short、sput、sput-wide、sput-obJ ect、sput-boolean、sput-byte、sput-char、sput-short

​ 在 Android 4.0 系统中,Dalvik 指令集中增加了 iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCCCsstaticop/jumbo vAAAA, field@BBBBBBBB 两类指令,它们与上面介绍的两类指令作用相同,只是在指令中增加了jumbo 字节码后缀,且寄存器值与指令的索引取值范围更大。

3.3.13 方法调用指令

​ 方法调用指令负责调用类实例的方法。它的基础指令为 invoke,方法调用指令有 invoke-kind {vC, vD, vE, vF, vG},meth@BBBBinvoke-kind/range {vCCCC .. vNNNN}, meth@BBBB 两类,两类指令在作用上并无不同,只是后者在设置参数寄存器时使用了 range 来指定寄存器的范围。根据方法类型的不同,共有如下五条方法调用指令:

invoke-virtualinvoke-virtual/range 调用实例的虚方法。

invoke-superinvoke-super/range 调用实例的父类方法。

invoke-directinvoke-direct/range 调用实例的直接方法。

invoke-staticinvoke-static/range 调用实例的静态方法。

invoke-interfaceinvoke-interface/range 调用实例的接口方法。

​ 在 Android 4.0 系统中,Dalvik 指令集中增加了 invoke-kind/jumbo {vcccc .. vNNNN}, meth@BBBBBBBB 这类指令,它与上面介绍的两类指令作用相同,只是在指令中增加了 jumbo 字节码后缀,且寄存器与指令的索引取值更大。

​ 方法调用指令的返回值必须使用 move-result* 指令来获取。如下面两条指令:

1
2
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0

3.3.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 将整型转换为短整型。

3.3.15 数据运算指令

​ 数据运算指令包括算术运算指令与逻辑运算指令。算术运算指令主要进行数值间如加、减、乘、除、模、移位等运算,逻辑运算指令主要进行数值间与、或、非、异或等运算。数据运算指令有如下四类(数据运算时可能是在寄存器或寄存器对间进行,下面的指令作用讲解时使用寄存器来描述):

binop vAA, vBB, vcc 将 vBB 寄存器与 vCC 寄存器进行运算,结果保存到 vAA 寄存器。

binop/2addrvA, VB将 vA 寄存器与 vB 寄存器进行运算,结果保存到 vA 寄存器。

binop/lit16 VA, VB, #+CCCC 将 vB 寄存器与常量 CCCC 进行运算,结果保存到 vA 寄存器。

binop/lit8 vAA, vBB, #+cc将 vBB 寄存器与常量 CC 进行运算,结果保存到 vAA 寄存器。

​ 后面3类指令比第 1 类指令分别多出了 2addr、1it16、lit8 等指令后缀。四类指令中基础字节码相同的指令执行的运算操作是类似的,第 1 类指令中,根据数据的类型不同会在基础字节码后面加上数据类型后缀,如 -int 或 -long 分别表示操作的数据类型为整型与长整型。第 1 类指令可归类如下:

add-type vBB 寄存器与 vCC 寄存器值进行加法运算(vBB + vCC)。

sub-type vBB 寄存器与 vCC 寄存器值进行减法运算(vBB · vCC)。

mul-type vBB 寄存器与 vCC 寄存器值进行乘法运算(vBB * 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。后面 3 类指令与之类似,此处不再列出。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2022-2023 Syclover.Kama
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信