0x01 Dalvik虚拟机概述

Dalvik虚拟机是Goole为Android平台专门设计的一套专门用来运行Android程序的虚拟机,是Android平台的核心组件。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

0x02 Dalvik虚拟机的特点

  • 体积小,占用内存空间小;

  • 专有的DEX可执行文件格式,体积更小,执行速度更快;

  • 常量池采用32位索引值,寻之类方法名、字段名、常量更快;

  • 基于寄存器架构,并拥有一套完整的指令系统;

  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;

  • 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。

0x03 Dalvik虚拟机与Java虚拟机的区别

  • Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。

  • Dalvik可执行文件体积更小。与Java相比,相同的字符串、常量在DEX文件中只会出现一次。

  • Jave虚拟机基于栈架构,需要频繁从栈上读取或写入数据,耗费大量CPU时间;Dalvik虚拟机基于寄存器架构,数据通过寄存器直接传递,速度比栈方式快很多。

0x04 Dalvik虚拟机工作原理

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

Android系统由 Linux内核、函数库、Android运行时、应用程序框架以及应用程序 组成。

从上图可以看出,Dalvik虚拟机属于Android运行时环境,与一些核心库共同承担Android应用程序的运行工作。

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

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

  • fork(),创建一个Zygote进程;

  • forkAddSpecialize();创建一个非Zygote进程;

  • forkSystemServer();创建一个系统服务进程。

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

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

0x05 Dalvik虚拟机JIT

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

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

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

  • trace方式:以trace为单位进行编译,用更短的时间与更少的内存来编译代码。

目前,Dalvik虚拟机默认采用trace方式编译代码,同时也支持采用method方式来编译。

0x06 Dalvik指令格式

Dalvik汇编代码由一系列Dalvik指令组成,指令语法由指令的位描述与指令格式标识来决定。单独使用位标识还无法确定一条指令,必须通过指令格式标识来指定指令的格式编码。

1、位描述

  • 每16位的字采用空格分割开来;

  • 每个字母表示四位,每个字母从高字节开始,排列到低字节。每四位之间可能使用“|”来表示不同内容;

  • 顺序采用A~Z的单个大写字母作为一个4位的操作码,op表示一个8位的操作码

  • Ø”来表示这字段所有位为0

以指令格式“A|G|op BBBB F|E|D|C”为例:

指令中间有两个空格,每个分开的部分大小为16位,所以这条指令由三个16位的字组成。第一个16位是“A|G|op”,高8位由AG组成,低字节由操作码op组成。第二个16位由“BBBB”组成,它表示一个16位的偏移值。第三个16位分别由FEDC共四个4字节组成,在这里它们表示寄存器参数。

2、指令格式标识描述

  • 大多由三个字符组成,前两个是数字,最后一个是字母;

  • 第一个数字是表示指令有多少个16位的字组成;

  • 第二个数字是表示指令最多使用寄存器的个数,特殊标记“r”标识使用一定范围内的寄存器。

  • 第三个字母为类型码,表示指令用到的额外数据的类型。

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

3、指令格式标识的类型码

助记符 位大小 说明
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表示没有使用到额外的数据。

4、Dalvik指令对语法上的一些说明

  • 每条指令从操作码开始,后面紧跟参数,参数个数不定,每个参数之间采用逗号分开;

  • 每条指令的参数从指令第一部分开始,op位于低8位,高8位可以是一个8位的参数,也可以是两个4位的参数,还可以为空,如果指令超过16位,则后面部分依次做参数;

  • 如果参数采用“vX”的方式表示,表明它是一个寄存器;

  • 如果参数采用“#+X”的方式表示,表明它是一个常量数字;

  • 如果参数采用“+X”的方式表示,表明它是一个相对指令的地址偏移;

  • 如果参数采用“kind@X”的方式表示,表明它是一个常量池索引值。kind表示常量池类型,可以是“string”(字符串常量池索引)、“type”(类型常量池索引)、“field”(字段常量池索引)或者“meth”(方法常量池索引)。

以指令“op vAA,string@BBBB”为例:

指令用到了1个寄存器参数vAA,并且还附加了一个字符串常量池索引string@BBBB,其实这条指令格式代表着const-string指令。

0x07 Dalvik寄存器

1、Dalvik寄存器简介

Dalvik中用到的寄存器都是32位的,支持任何类型,64位类型用2个相邻寄存器表示。

Dalvik虚拟机最大支持65536个虚拟寄存器。每个函数在函数头部使用.registers指令指定函数用到的寄存器数目,虚拟机执行到这个函数时,根据寄存器的数目分配适当的栈空间,用来存放寄存器实际的值。

2、寄存器表示方法

寄存器表示方法分为v命名法p命名法

假如一个函数使用到M个寄存器,并且该函数有N个参数,根据Dalvik虚拟机参数传递方式的规定:参数使用最后的N个寄存器中,局部变量使用从v0开始的前M-N个寄存器。

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

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

3、v命名法和p命名法比较

v命名法 p命名法 寄存器含义
v0 v0 第一个局部变量寄存器
v1 v1 第二个局部变量寄存器
中间的局部变量寄存器依次递增且名称相同
vM-N p0 第一个参数寄存器
中间的参数寄存器分别依次递增
vM-1 pN-1 第N个参数寄存器

0x08 Dalvik字节码

1、类型

Dalvik字节码只有两种类型:基本类型与引用类型。Dalvik使用这两种类型来表示Java语言的全部类型,除了对象与数组属于引用对象外,其他的Java类型都是基本类型。

2、Dalvik字节码类型描述符

语法 含义
V void,只用于返回值类型
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L Java类类型
[ 数组类型
  • L类型可以表示Java类型中的任何类。

  • [类型可以表示所有基本类型的数组,多个[在一起时可以用来表示多维数组,多维数组的维数最大为255个。

  • L[可以同时使用用来表示对象数组。

3、方法

Dalvik使用方法名、类型参数与返回值来详细描述一个方法。这样做一方面有助于Dalvik虚拟机在运行时从方法表中快速地找到正确的方法,另一方面,Dalvik虚拟机也可以使用它们来做一些静态分析,比如Dalvik字节码的验证与优化。

举例说明一下:

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

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

BakSmali生成的方法代码以.method指令开始,以.end method指令结束。“# virtual methods”表示这是一个虚方法,“# direct methods”表示这是一个直接方法。

4、字段

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

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

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

BakSmali生成的字段代码以.field指令开始。“# instance fields” 表示这是一个实例字段,“# static fields” 表示这是个静态字段。