前言
学习这块的主要目的还是想知道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 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 |
|
这里是从dex文件中提取出函数的简要声明,onCreate的简单声明为VL,然后根据声明,放入参数。首先放入this参数,然后根据后续参数的类型,将参数放入相应位置。和dalvik虚拟机流程类似。
sub_3FE5C
sub_D930完成了虚拟寄存器栈的构建并放入参数后,调用了sub_3FE5C。sub_3FE5C主要负责初始化解释器的一些状态,主要是InterpState结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
数字壳解释器模型
sub_D930和sub_3FE5C完成了解释器的准备工作。sub_3FF5C负责解释执行。数字壳的解释器模型就是上面提到的那种最直观的模型,循环取指令,然后判断指令类型,去相应分支执行,执行完成后,再返回到switch执行下条指令。
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 |
|
数字壳invoke-super指令分析
invoke-super指令包含了函数调用的过程,可以看到dalvik解释器虚拟寄存器栈是比较复杂的,设计很多数据结构。目前为止数字壳的相关结构中,并未发现类似的结构。所以想分析下数字壳的解释器是如何处理函数调用的。调试分析后发现,数字壳的解释器其实并未实现真正的函数调用,它是通过调用jni中的CallVoidMethod方法来实现函数调用。
处理invoke-super的handler为sub_4878C,流程如下:
- 提取invoke-super指令中包含的MethodID,从dex中获取到DexMethodId结构,获取函数名。
- 从DexMethodId中获取ClassId
- 利用获取的ClassId可以获取到类名,利用jni->FindClass获取类
- 利用jni->GetMethod获取函数
- 准备函数参数
- 利用jni->CallVoidMehtod调用函数,实现invoke-method
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 |
|
数字壳比较复杂,通过分析可以学到很多东西,比如各种反调试,linker,适配很多版本的动态加载,解释器等,感谢数字公司提供的免费加固。笔记如果有错误,欢迎指正,也希望大佬们可以交流下其他vmp的实现思路。