Android逆向基础之Dalvik虚拟机
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位由A
和G
组成,低字节由操作码op
组成。第二个16位由“BBBB
”组成,它表示一个16位的偏移值。第三个16位分别由F
、E
、D
、C
共四个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
” 表示这是个静态字段。