前言
学习这一块的主要目的是想知道vmp如何实现,如何与系统本身的虚拟机合作,所以简单地学习Dalvik对数字公司的解释器进行了比较和分析。笔记结构如下:
- dalvik解释器分析
- dalvik解释器解释指令前的准备工作
- dalvik解释器模型
- invoke-super指令实例分析
- 数字壳解释器分析
- 解释器解释指令前的准备工作
- 解释器模型
- invoke-super指令实例分析
dalvik解释器分析
dalvik解释器解释指令前的准备工作
从外部进入解释器的调用链如下: dvmCallMethod -> dvmCallMethodV -> dvmInterpret
这三个函数在解释器获取指令和选择分支之前被调用,主要负责一些准备工作,包括分配虚拟寄存器、放入参数、初始化解释器参数等。dvmCallMethod,直接调用了dvmCallMethodV.下面分析下两个函数。
dvmCallMethodV
dalvik虚拟机是基于寄存器架构的。可以想象,在具体执行函数之前,首先要做的是分配虚拟寄存器空间,并将函数所需的参数放入虚拟寄存器中。主要流程:
- 取出函数的简单声明,如onCreate函数的简单声明如下:VL
- 虚拟寄存器存器栈
- 放入this参数,根据参数类型放入申报中的参数
- 如果方法是native方法,直接跳转method->nativeFunc执行
- 如果方法是java方法,进入dvmInterpret解释执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
dvmInterpret
dvmInterpret作为虚拟机的入口,主要做了如下工作:
- 初始化解释器的执行环境。主要是对解释器的变量进行初始化,如将要执行方法的指针,当前函数栈的指针,程序计数器等。
- 判断将要执行的方法是否合法
- JIT环境的设置
- 根据系统参数选择解释器(Fast解释器或者Portable解释器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
dalvik解释器流程分析
dalvik解释器有两种:Fast解释器,Portable解释器。选择分析Portable解释器,因为Portable解释器的可读性更好。在分析前,先看下Portable解释器的模型。
Thread Code技术
实现解释器的一个常见思路如下代码,循环取指令,然后判断指令类型,去相应分支执行,执行完成后,再返回到switch执行下条指令。
1 2 3 4 5 6 7 8 9 |
|
但是当每次执行一条指令,都需要重新判断下条指令类型,然后选择switch分支,这是个昂贵的开销。Dalvik为了解决这个问题,引入了Thread Code技术。简单的说就是在执行函数之前,建立一个分发表GOTO_TABLE,每条指令在表中有一个对应条目,条目里存放的就是处理该条指令的handler地址。比如invoke-super指令,它的opcode为6f,那么处理该条指令的handler地址就是:GOTO_TABLE[6f].那么在每条指令的解释程序末尾,都可以加上取指动作,然后goto到下条指令的handler。
dvmInterpretPortable源码分析
dvmInterpretPortable是Portable型虚拟机的具体实现,流程如下
- 初始化一些关于虚拟机执行环境的变量
- 初始化分发表
- FINISH(0)开始执行指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
invoke-super指令实例分析
invoke-super这条指令的handler如下:
1 2 3 4 5 6 7 8 9 |
|
invokeSuper这个标签定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
|
解析完要调用的方法后,跳转到invokeMethod结构来执行函数调用,invokeMethod为要调用的函数创建虚拟寄存器栈,新的寄存器栈和之前的栈是由重叠的。然后重新设置解释器执行环境的参数,调用FINISH(0)执行函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
数字壳解释器分析
数字壳解释执行前的准备工作
进入解释器的流程为onCreate->sub_D930->sub_3FE5C->sub_3FF5C。sub_3FF5C真正的解释器入口,sub_D930和sub_3FE5C负责执行前的准备工作。这部分准备工作和dalvik解释器的准备工作类似。
sub_D930
sub_D930分为两部分,调用sub_66BD4之前为第一部分,之后为第二部分。这两部分主要做的事情如下:
- 第一部分
- jni的一些初始化工作,FindClass,GetMethodID之类的工作
- 利用java.lang.Thread.getStackTrace获取到调用当前方法的类的类名以及函数名
- 第二部分
- 调用sub_66BD4获取一些全局信息,以及待解释函数的信息
- 构建解释器的虚拟寄存器栈
- 解析待解释函数的简单声明,将函数参数放入虚拟寄存器
主要分析第二部分,首先引入一些数据结构,这类数据结构是动态分析出来的,有些字段的含义还不清楚标记为unkonw。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
sub_66BD4返回的是指向GlobalInfo结构的指针。这个全局信息里面包含了dex有关的信息和待解释函数的信息。有了这个信息就可以构建解释器所需的虚拟寄存器栈,完成准备工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
通过创建并初始化StackInfo结构,就完成了虚拟寄存器栈的创建,可以看到这里分配了两个虚拟寄存器栈。后面调试发现主要使用的是第二个虚拟寄存器栈。猜测这两个虚拟寄存器栈和dalvik拥有两个虚拟寄存器栈一样的原因一样,是一个用来执行native方法,一个执行java方法。
创建完虚拟寄存器栈的下一步工作就是放入函数参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |