Dalvik虚拟机是如何加载Dex

2024-03-14 09:38
文章标签 加载 虚拟机 dalvik dex

本文主要是介绍Dalvik虚拟机是如何加载Dex,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

0x00 Dalvik虚拟机是如何执行程序Dex的?

这里写图片描述

解析完dex结构之后,我就比较好奇dalvik虚拟机是如何加载并执行dex的?

davlik是基于寄存器的虚拟机,其从源代码到可执行文件中的与java编译有所不同,多了一步使用dx工具将class文件压缩成Dalvik字节码

对比jar和apk文件格式的区别:

0x01 dalvik相对于java虚拟机的优点:

代码密度小,运行效率高,节省资源。
常量池只使用32位的索引
有内存限制
默认栈大小是12KB(3个页,每页4KB)
堆默认启动大小为2MB,默认最大值为16MB
堆支持的最小启动大小为1MB,支持的最大值为1024MB
堆和栈参数可以通过-Xms-Xmx修改

0x02 dalvik如何加载并执行dex

通过mmap函数将类加载到内存,然后通过读写操作访问dex,然后解析dex文件内容并加载其中的类到哈希表中。并通过dexFileParse函数对其进行分析

映射dex到内存--》加载class--》Dalvik解释器解释执行

2.1 映射dex到内存

总的来说,dex文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行哈希并生成索引。

2.2加载dex

在对文件解析完成后就要加载Class的具体内容了!在Dalvik中,由ClassObject 这个数据结构负责存放加载的信息。还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。

typedef struct Object {ClassObject* clazz;  // ClassObject类型对象Lock lock;           // 锁对象
} Object;

如下图所示,加载过程会在内存中alloc几个区域,分别存放directMethods, virtualMethods, sfields, ifields。这些信息正是从dex 文件的数据区中读取。

struct Field {ClassObject* clazz;    //所属类型const char* name;      // 变量名称const char* signature; // 如“Landroid/os/Debug;”u4 accessFlags;        // 访问标记#ifdef PROFILE_FIELD_ACCESSu4 gets;u4 puts;#endif
};

首先会读取Class的详细信息,从中获知directMethod, virtualMethod, sfield, ifield等的信息,然后再读取。下图为加载完成后的示意。 这里并未介绍加载的每个细节,感兴趣的同学可通过此二图自行分析。

还请大家注意的是在ClassObject结构中有个名为super的成员。通过super成员来指向它的超类。

而dex加载整体流程如下:

待得到class索引后,实际的加载由loadClassFromDex来完成。首先它会读取class的具体数据,分别加载directMethod, virtualMethod, ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。

2.3 dalvik解释器分析

1.dalvik解释器解释指令前的准备工作
2.dalvik解释器的模型
3.invoke-super指令实例分析

dalvik解释器解释指令前的准备工作
从外部进入解释器的调用链如下:
dvmCallMethod -> dvmCallMethodV -> dvmInterpret

这三个函数是在解释器取指令,选分支之前被调用,主要负责一些准备工作,包括分配虚拟寄存器,放入参数,初始化解释器参数等。其中dvmCallMethod,直接调用了dvmCallMethodV.下面分析下后两个函数。

dvmCallMethodV
dalvik虚拟机是基于寄存器架构的,可想而知,在具体执行函数之前,首先要做的就是分配好虚拟寄存器空间,并且将函数所需的参数,放入虚拟寄存器中。主要流程:

1.取出函数的简单声明,如onCreate函数的简单声明为:VL
2.分配虚拟寄存器栈
3.放入this参数,根据参数类型放入申明中的参数
4.如果方法是native方法,直接跳转到method->nativeFunc执行
5.如果方法是java方法,进入dvmInterpret解释执行

void dvmCallMethodV(Thread* self, const Method* method, Object* obj, bool fromJni, JValue* pResult, va_list args)
{//取出方法的简要声明const char* desc = &(method->shorty[1]); // [0] is the return type.int verifyCount = 0;ClassObject* clazz;u4* ins;//访问权限检查,以及分配函数调用栈,在栈中维护了一份虚拟寄存器列表。clazz = callPrep(self, method, obj, false);if (clazz == NULL)return;/* "ins" for new frame start at frame pointer plus locals *///指向第一个参数ins = ((u4*)self->interpSave.curFrame) + (method->registersSize - method->insSize);//放入this指针,到第一个参数。/* put "this" pointer into in0 if appropriate */if (!dvmIsStaticMethod(method)) {*ins++ = (u4) obj;verifyCount++;}//根据后续参数的类型,放入后续参数while (*desc != '\0') {switch (*(desc++)) {case 'D': case 'J': {u8 val = va_arg(args, u8);memcpy(ins, &val, 8);       // EABI prevents direct storeins += 2;verifyCount += 2;break;}case 'F': {/* floats were normalized to doubles; convert back */float f = (float) va_arg(args, double);*ins++ = dvmFloatToU4(f);verifyCount++;break;}case 'L': {     /* 'shorty' descr uses L for all refs, incl array */void* arg = va_arg(args, void*);assert(obj == NULL || dvmIsHeapAddress(obj));jobject argObj = reinterpret_cast<jobject>(arg);if (fromJni)*ins++ = (u4) dvmDecodeIndirectRef(self, argObj);else*ins++ = (u4) argObj;verifyCount++;break;}default: {/* Z B C S I -- all passed as 32-bit integers */*ins++ = va_arg(args, u4);verifyCount++;break;}}}//如果是本地方法,就直接跳转到本地方法,若是java方法,进入解释器,解释执行。if (dvmIsNativeMethod(method)) {TRACE_METHOD_ENTER(self, method);/** Because we leave no space for local variables, "curFrame" points* directly at the method arguments.*/(*method->nativeFunc)((u4*)self->interpSave.curFrame, pResult,method, self);TRACE_METHOD_EXIT(self, method);} else {dvmInterpret(self, method, pResult);//解释器的入口}dvmPopFrame(self);
}

dvmInterpret
dvmInterpret作为虚拟机的入口,主要做了如下工作:

1.初始化解释器的执行环境。主要是对解释器的变量进行初始化,如将要执行方法的指针,当前函数栈的指针,程序计数器等。
2.判断将要执行的方法是否合法(是否初始化或者error)
3.JIT环境的设置
4.根据系统参数选择解释器(Fast解释器或者Portable解释器)

void dvmInterpret(Thread* self, const Method* method, JValue* pResult)
{//解释器的状态InterpSaveState interpSaveState;ExecutionSubModes savedSubModes;#if defined(WITH_JIT)double calleeSave[JIT_CALLEE_SAVE_DOUBLE_COUNT];
#endif//保存之前的解释器状态,并将新的状态和之前的状态连接起来(链表)interpSaveState = self->interpSave;self->interpSave.prev = &interpSaveState;/** Strip out and save any flags that should not be inherited by* nested interpreter activation.*/savedSubModes = (ExecutionSubModes)(self->interpBreak.ctl.subMode & LOCAL_SUBMODE);if (savedSubModes != kSubModeNormal) {dvmDisableSubMode(self, savedSubModes);}
#if defined(WITH_JIT)dvmJitCalleeSave(calleeSave);
#endif#if defined(WITH_TRACKREF_CHECKS)self->interpSave.debugTrackedRefStart =dvmReferenceTableEntries(&self->internalLocalRefTable);
#endifself->debugIsMethodEntry = true;
#if defined(WITH_JIT)/* Initialize the state to kJitNot */self->jitState = kJitNot;
#endif/初始化解释器的执行环境self->interpSave.method = method;  //初始化执行的方法self->interpSave.curFrame = (u4*) self->interpSave.curFrame; //初始化函数调用栈self->interpSave.pc = method->insns;  //初始化程序计数器//检查方法是否为本地方法assert(!dvmIsNativeMethod(method));//方法的类是否初始化if (method->clazz->status < CLASS_INITIALIZING || method->clazz->status == CLASS_ERROR){ALOGE("ERROR: tried to execute code in unprepared class '%s' (%d)",method->clazz->descriptor, method->clazz->status);dvmDumpThread(self, false);dvmAbort();}// 选择解释器typedef void (*Interpreter)(Thread*);Interpreter stdInterp;if (gDvm.executionMode == kExecutionModeInterpFast)stdInterp = dvmMterpStd;
#if defined(WITH_JIT)else if (gDvm.executionMode == kExecutionModeJit ||gDvm.executionMode == kExecutionModeNcgO0 ||gDvm.executionMode == kExecutionModeNcgO1)stdInterp = dvmMterpStd;
#endifelsestdInterp = dvmInterpretPortable;//设置为Portable解释器// Call the interpreter(*stdInterp)(self);*pResult = self->interpSave.retval;/* Restore interpreter state from previous activation */self->interpSave = interpSaveState;
#if defined(WITH_JIT)dvmJitCalleeRestore(calleeSave);
#endifif (savedSubModes != kSubModeNormal) {dvmEnableSubMode(self, savedSubModes);}
}

dalvik解释器流程分析
dalvik解释器有两种:Fast解释器,Portable解释器。选择分析Portable解释器,因为Portable解释器的可读性更好。在分析前,先看下Portable解释器的模型。

Thread Code技术
实现解释器的一个常见思路如下代码,循环取指令,然后判断指令类型,去相应分支执行,执行完成后,再返回到switch执行下条指令。

while (*ins) {switch (*ins) {case NOP:break;case MOV:break;......}
}

但是当每次执行一条指令,都需要重新判断下条指令类型,然后选择switch分支,这是个昂贵的开销。Dalvik为了解决这个问题,引入了Thread Code技术。简单的说就是在执行函数之前,建立一个分发表GOTO_TABLE,每条指令在表中有一个对应条目,条目里存放的就是处理该条指令的handler地址。比如invoke-super指令,它的opcode为6f,那么处理该条指令的handler地址就是:GOTO_TABLE[6f].那么在每条指令的解释程序末尾,都可以加上取指动作,然后goto到下条指令的handler。

dvmInterpretPortable源码分析
dvmInterpretPortable是Portable型虚拟机的具体实现,流程如下

1.初始化一些关于虚拟机执行环境的变量
2.初始化分发表
3.FINISH(0)开始执行指令

void dvmInterpretPortable(Thread* self)
{DvmDex* methodClassDex;     // curMethod->clazz->pDvmDexJValue retval;//一些核心的状态const Method* curMethod;    // 要执行的方法const u2* pc;               // 指令计数器u4* fp;                     // 函数栈指针u2 inst;                    // 当前指令/* instruction decoding */u4 ref;                     // 用来表示类的引用u2 vsrc1, vsrc2, vdst;      // 寄存器索引/* method call setup */const Method* methodToCall;bool methodCallRange;//建立分发表DEFINE_GOTO_TABLE(handlerTable);//初始化上面定义的变量curMethod = self->interpSave.method;pc = self->interpSave.pc;fp = self->interpSave.curFrame;retval = self->interpSave.retval;   /* only need for kInterpEntryReturn? */methodClassDex = curMethod->clazz->pDvmDex;if (self->interpBreak.ctl.subMode != 0) {TRACE_METHOD_ENTER(self, curMethod);self->debugIsMethodEntry = true;   // Always true on startup}methodToCall = (const Method*) -1;//取出第一条指令,并且执行FINISH(0);                  /* fetch and execute first instruction *///下面就是定义了每条指令的处理分支。
//NOP指令的处理程序:什么都不做,然后处理下条指令
HANDLE_OPCODE(OP_NOP)FINISH(1);
OP_END
.....

invoke-super指令实例分析
invoke-super这条指令的handler如下:

#define GOTO_invoke(_target, _methodCallRange)                              \do {                                                                    \methodCallRange = _methodCallRange;                                 \goto _target;                                                       \} while(false)HANDLE_OPCODE(OP_INVOKE_SUPER /*vB, {vD, vE, vF, vG, vA}, meth@CCCC*/)GOTO_invoke(invokeSuper, false);
OP_END

invokeSuper这个标签定义如下:

//invoke-super位描述符如下:A|G|op BBBB F|E|D|C
//methodCallRange depending on whether this is a "/range" instruction.
GOTO_TARGET(invokeSuper, bool methodCallRange){Method* baseMethod;u2 thisReg;EXPORT_PC();//7010 0400 0000  opcode 对应的 A|G|OP BBBB CDEF//取出AG的值vsrc1 = INST_AA(inst); //要调用的method索引ref = FETCH(1);//要作为参数的寄存器的索引vdst = FETCH(2);        //取出this寄存器的索引,比如thisReg为3的话,表示第三个寄存器,放的是this参数。if (methodCallRange) {ILOGV("|invoke-super-range args=%d @0x%04x {regs=v%d-v%d}",vsrc1, ref, vdst, vdst+vsrc1-1);thisReg = vdst;} else {ILOGV("|invoke-super args=%d @0x%04x {regs=0x%04x %x}",vsrc1 >> 4, ref, vdst, vsrc1 & 0x0f);thisReg = vdst & 0x0f;}//检查this 是否为空if (!checkForNull((Object*) GET_REGISTER(thisReg)))GOTO_exceptionThrown();//解析要调用的方法baseMethod = dvmDexGetResolvedMethod(methodClassDex, ref);if (baseMethod == NULL) {baseMethod = dvmResolveMethod(curMethod->clazz, ref,METHOD_VIRTUAL);if (baseMethod == NULL) {ILOGV("+ unknown method or access denied");GOTO_exceptionThrown();}}if (baseMethod->methodIndex >= curMethod->clazz->super->vtableCount) {/** Method does not exist in the superclass.  Could happen if* superclass gets updated.*/dvmThrowNoSuchMethodError(baseMethod->name);GOTO_exceptionThrown();}methodToCall = curMethod->clazz->super->vtable[baseMethod->methodIndex];#if 0if (dvmIsAbstractMethod(methodToCall)) {dvmThrowAbstractMethodError("abstract method not implemented");GOTO_exceptionThrown();}
#elseassert(!dvmIsAbstractMethod(methodToCall) ||methodToCall->nativeFunc != NULL);
#endifLOGVV("+++ base=%s.%s super-virtual=%s.%s",baseMethod->clazz->descriptor, baseMethod->name,methodToCall->clazz->descriptor, methodToCall->name);assert(methodToCall != NULL);//调用方法GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);}
GOTO_TARGET_END

解析完要调用的方法后,跳转到invokeMethod结构来执行函数调用,invokeMethod为要调用的函数创建虚拟寄存器栈,新的寄存器栈和之前的栈是由重叠的。然后重新设置解释器执行环境的参数,调用FINISH(0)执行函数

GOTO_TARGET(invokeMethod, bool methodCallRange, const Method* _methodToCall, u2 count, u2 regs)
{      //节选if (!dvmIsNativeMethod(methodToCall)) {/** "Call" interpreted code.  Reposition the PC, update the* frame pointer and other local state, and continue.*/curMethod = methodToCall;     //设置要调用的方法self->interpSave.method = curMethod; methodClassDex = curMethod->clazz->pDvmDex;  pc = methodToCall->insns;     //重置pc到要调用的方法fp = newFp;self->interpSave.curFrame = fp;
#ifdef EASY_GDBdebugSaveArea = SAVEAREA_FROM_FP(newFp);
#endifself->debugIsMethodEntry = true;        // profiling, debuggingILOGD("> pc <-- %s.%s %s", curMethod->clazz->descriptor,curMethod->name, curMethod->shorty);DUMP_REGS(curMethod, fp, true);         // show input argsFINISH(0);                              // jump to method start}

参考

http://blog.csdn.net/VirtualPower/article/details/5715277
https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system/DexFile.java
https://www.jianshu.com/p/14147171a599

解释执行参考:
https://bbs.pediy.com/thread-226214.htm

这篇关于Dalvik虚拟机是如何加载Dex的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/807993

相关文章

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

HotSpot虚拟机的经典垃圾收集器

读《深入理解Java虚拟机》第三版笔记。 关系 Serial、ParNew、Parallel Scavenge、Parallel Old、Serial Old(MSC)、Concurrent Mark Sweep (CMS)、Garbage First(G1)收集器。 如图: 1、Serial 和 Serial Old 收集器 2、ParNew 收集器 3、Parallel Sc

理解java虚拟机内存收集

学习《深入理解Java虚拟机》时个人的理解笔记 1、为什么要去了解垃圾收集和内存回收技术? 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。 2、“哲学三问”内存收集 what?when?how? 那些内存需要回收?什么时候回收?如何回收? 这是一个整体的问题,确定了什么状态的内存可以

虚拟机ubuntu配置opencv和opencv_contrib

前期准备  1.下载opencv和opencv_contrib源码 opencv-4.6.0:https://opencv.org/releases/ opencv_contrib-4.6.0:https://github.com/opencv/opencv_contrib 在ubuntu直接下载或者在window上下好传到虚拟机里都可以 自己找个地方把他们解压,个人习惯在home下新建一

OpenStack创建虚拟机过程

OpenStack创建虚拟机过程 一、在分析OpenStack创建虚拟机的过程之前,先来梳理一下需要用用到哪些组件。 二、每一步都需要去keystone去进行验证,下图有详细的流程。 登录界面或命令行通过RESTful API向keystone获取认证信息。keystone通过用户请求认证信息,并生成auth-token返回给对应的认证请求。界面或命令行通过RESTful API

使用WebP解决网站加载速度问题,这些细节你需要了解

说到网页的图片格式,大家最常想到的可能是JPEG、PNG,毕竟这些老牌格式陪伴我们这么多年。然而,近几年,有一个格式悄悄崭露头角,那就是WebP。很多人可能听说过,但到底它好在哪?你的网站或者项目是不是也应该用WebP呢?别着急,今天咱们就来好好聊聊WebP这个图片格式的前世今生,以及它值不值得你花时间去用。 为什么会有WebP? 你有没有遇到过这样的情况?网页加载特别慢,尤其是那

VMware Fusion Pro 13 Mac版虚拟机 安装Win11系统教程

Mac分享吧 文章目录 Win11安装完成,软件打开效果一、VMware安装Windows11虚拟机1️⃣:准备镜像2️⃣:创建虚拟机3️⃣:虚拟机设置4️⃣:安装虚拟机5️⃣:解决连不上网问题 安装完成!!! Win11安装完成,软件打开效果 一、VMware安装Windows11虚拟机 首先确保自己的mac开启了网络共享。不然虚拟机连不上👀的 1️⃣:准备镜像

SSH连接虚拟机中的Ubuntu 12.0.4

摘要:主要是解决不能使用ssh远程Ubuntu的问题、使用的远程工具是putty、也可以使用xshell、ubunut12.0.4是装在虚拟机中的、不过这个应该没有什么影响。 一:问题的出现 前两天使用VMware装了一个ubuntu12.0.4之后、因为常常使用命令行、又喜欢在虚拟机与实体机中切来切去、感觉很不方便、就想在xp中远程ubuntu、遇到了点小意外、经过一会调试解决成功、把

gazebo 已加载模型但无法显示

目录 写在前面的话问题一:robot_state_publisher 发布机器人信息失败报错一 Error: Error document empty.报错二 .xcaro 文件中有多行注释成功启动 问题二:通过 ros2 启动 gazebo 失败成功启动 问题三:gazebo 崩溃和无法显示模型问题四: 缺少 robot_description 等话题正确的输出 写在前面的话

用了虚拟机后,本机摄像头打不开了(联想电脑thinkpad)

虚拟机有摄像头,我断开了连接,现在本机的摄像头打开就是一个锁 我先把虚拟机的摄像头关了 然后把本机的vm usb关闭了 Win+R),输入services.msc,找到VMware USB Arbitration Service,确保其状态为“关闭 然后打开桌面助手 开启 参考: 联想知识库