概述
基于https://blog.csdn.net/luoshengyang/article/details/8852432的分析整理。
涉及dalvik启动过程,运行过程
目标为搞懂虚拟机native中的本质。现在基本都不用dalvik了,在我的8.0机子下persist.sys.dalvik.vm.lib.2=libart.so 表示用art虚拟机,dalvik已经完全不见踪影。
前提杂记
linux下分析:
vim:
:sp可直接分屏,ctrl+w切换分屏,ctrl+w+o只留选中的
:/用来搜索 按n下一个、N上一个 光标选中单词shift+*直接搜索该单词
基于栈的虚拟机字节更小,指令所需多;基于寄存器的字节更长,缓存命中小
RISC精简指令集操作更原子,CISC复杂指令集操作更大,二者都是栈与寄存器运行结构。
android内部C++代码大量使用智能指针管理对象声明周期,防止内存释放问题。开发使用java自动回收
每个进程都有dalvik实例,每个应用进程由Zygote fork出,Zygote进程是由init进程启动,在系统启动时加载所需。
这些被fork出来的Android应用程序进程,一方面是复制了Zygote进程中的虚拟机实例,另一方面是与Zygote进程共享了同一套Java核心库。这样不仅Android应用程序进程的创建过程很快,而且由于所有的Android应用程序进程都共享同一套Java核心库而节省了内存空间。
dalvik启动过程
Linux中所有进程基于init进程,android中init创建名为zygote的进程,这个zygote进程要执行的程序是/system/bin/app_process。其主函数为frameworks/base/cmds/app_process/app_main.cpp中的main
之后的调用栈:
注意这个是按函数划分的,只能表现调用栈,方法实现的位置与该图无关。而uml的时序图可以表现出方法的实现位置(对象、类、所属文件),在同类内的方法调用上镶嵌一个条即可表现出调用栈。
开始说明源码:
startVm前用
JniInvocation统一三个接口
这三个都是调用函数指针,函数指针正是初始化在JniInvocation::Init():
其用dlopen加载默认的虚拟机so(libart.so或libdvm.so),并用dlsym加载函数的实现
AndroidRuntime::startVm 设置参数,调用JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs)
dalvik的JNI_CreateJavaVM在dalvik/vm/Jni.cpp中
|
|
每一个Dalvik虚拟机实例还有一个JNI环境列表,保存在对应的JavaVMExt对象的成员变量envList中。注意,JavaVMExt对象的成员变量envList描述的是一个JNIEnvExt列表,其中,每一个Attach到Dalvik虚拟机中去的线程都有一个对应的JNIEnvExt,用来描述它的JNI环境。有了这个JNI环境之后,我们才可以在Java函数和C/C++函数之间互相调用。
dvmCreateJNIEnv在 dalvik/vm/Jni.c 中
注意这里的JavaVM 与 JNIEnv 都是ndk编程中的,Ext表示拓展,可以强制转化为ndk中的。一般是拓展的父类。在c中实现为同结构体内存布局强转。
|
|
在dalvik\libnativehelper\include\nativehelper\jni.h文件中的定义了JNINativeInterface接口,
在这个文件中static const struct JNINativeInterface gNativeInterface = {…}将函数指针传入JNINativeInterface结构体的实例,该结构体中都是函数指针如jclass (*FindClass)(JNIEnv*, const char*);
jclass 类型就是一个空类*
其变量表示空类的对象的指针。
举一个static jclass FindClass(JNIEnv* env, const char* name)
的实现,在dalvik/vm/Jni.c 中,调用了dvmFindClassNoInit获取jvm的ClassObject*
同时传入的还有classLoader,这些函数的实现是统一在此的。
javaVM的定义:
pVM->funcTable = &gInvokeInterface;
填入函数指针
所以gDvmJni.jniVm = (JavaVM*) pVM
;
所以JavaVM就是函数表…Ext是其拓展
dvmStartup:
static bool dvmInitZygote(void):在dalvik/vm/Init.c中
setpgid(0,0);
调用了系统调用setpgid来设置当前进程,即Zygote进程的进程组ID。注意,在调用setpgid的时候,传递进去的两个参数均为0,这意味着Zygote进程的进程组ID与进程ID是相同的,也就是说,Zygote进程运行在一个单独的进程组里面。
AndroidRuntime.startReg :
函数startReg来注册一些Android核心类的JNI方法。
总结:
1. 创建了一个Dalvik虚拟机实例;
2. 加载了Java核心类及其JNI方法;
3. 为主线程的设置了一个JNI环境;
4. 注册了Android核心类的JNI方法。
Zygote进程为Android系统准备好了一个Dalvik虚拟机实例,以后Zygote进程在创建Android应用程序进程的时候,就可以将它自身的Dalvik虚拟机实例复制到新创建Android应用程序进程中去
可以转化为一般的JavaVM和JNIEnv是因为那俩个类(C++)或直接就是JNIInvokeInterface指针(C)都是第一个元素就是函数表指针。方法为代码不占struct的对象存储。
运行过程
|
|
|
|
选择dalvik解释器,函数dvmInterpretStd在执行之前,需要知道解释器的当前状态,也就是它所要执行的Java函数及其指令入口,以及当前要执行的线程的堆栈。这些信息都用一个InterpState结构体来描述。其中,这个InterpState结构体的成员变量method描述的是要执行的Java函数,成员变量fp描述的是要执行的线程的当前堆栈帧,成员变量pc描述的是要执行的Java函数的入口点。
之后到了指令执行:
与想象中一致,使用while switch逐个执行指令
smali语法:
注意参数、变量都是用寄存器存储使用的,调用函数没有显示的栈帧,在虚拟机中每个函数都对使用到的变量进行分配与回收,对每个线程保存自己的环境比如函数返回:
fp = saveArea->prevFrame
pc = saveArea->savedPc;
可见基本等同于模仿了一个cpu,不过一个指令的功能更为强大,native代码加载于内存中
相当于一个void *
。之后被强制化为(Method*)methodID
可见这个指针应该指向的是一个dex方法描述,内有其运行地址,应该是解析dex后保存在某处的。
|
|
执行。
System.loadLibrary
Dalvik虚拟机在调用一个成员函数的时候,如果发现该成员函数是一个JNI方法,那么就会直接跳到它的地址去执行。也就是说,JNI方法是直接在本地操作系统上执行的
在Dalvik虚拟机中,类加载器除了知道它要加载的类所在的文件路径之外,还知道该类所属的APK用来保存so文件的路径。因此,给定一个so文件名称,一个类加载器可以判断它是否存在自己的so文件目录中。
因此dex的解析加载过程十分重要,为逆向重点。
再来看下dvmClassStartup
https://www.jianshu.com/p/92c2f732d2d6?from=timeline