dalvik VM的解释器分析

本文基于KK的dalvik源码,详细分析了dvmCallMethodV、dvmInterpret和dvmMterpStd的执行过程,探讨了VM解释器的汇编码,包括HandlerTable、寄存器使用、异常处理机制,并对常見指令如const、mov和INVOKE进行了剖析。重点关注了ARM架构下的实现,强调了理解基本ARM汇编的重要性。

以KK的dalvik源码为基础来解析。

使用的源码基于https://github.com/AOKP/dalvik, 可以从https://github.com/AOKP/dalvik/archive/kitkat.zip 下载。

我是在linux下,使用vim + ctags做分析的。

由于ARM架构是使用最多也是最频繁的架构,所以我分析的重点是ARM的汇编如何实现解释器的。所以我在分析过程中会忽略掉与ARM无关的代码。

入口是什么?

从dvmCallMethod开始分析。这个函数是调用一个java method的主要入口函数。
dvmCallMethod->dvmCallMethodV->dvmInterpret->dvmMterpStd->dvmMterpStdRun

这一串调用,最终走到了dvmMterpStdRun函数。这个函数是用汇编写成的,在ARM架构中,实现的文件是vm/mterp/out/InterpAsm-armv7-a-neon.S。这个是Android KK ARM版本中使用的文件,也是我们分析的重点。

说了半天,dvmMterpStdRun是干什么的?其实就是用于解释实现各个dex 指令的,是整个解释的核心。不过,在正式了解这个函数之前,我们要分别了解下dvmCallMethodV,dvmInterpret,dvmMethodStd这3个函数的主要功能和涉及到的数据结构。
这些函数并不直接执行代码,但是却为dvmMterpStdRun准备了运行环境,只有理解了这些运行环境,才能更好的理解解释器的工作原理。

dvmCallMethodV

这个函数的核心作用是创建虚拟栈。
首先,我们了解下,一个普通的java method调用链条中的虚拟栈是怎么组织的。
dvmCallMethodV中完成栈构建的函数在callPrep->dvmPushInterpFrame

普通java method的虚拟栈

DVM为解释器分配的专门的栈,每个java线程有一个。这些信息保存在Thread结构体中。
//vm/interp/InterpState.h
struct InterpSaveState {
    const u2*       pc;         // Dalvik PC
    u4*             curFrame;   // Dalvik frame pointer
    const Method    *method;    // Method being executed
    DvmDex*         methodClassDex;
    JValue          retval;
    void*           bailPtr;
....
    struct InterpSaveState* prev;  // To follow nested activations
} __attribute__ ((__packed__));

//vm/Thread.h
/*
 * Our per-thread data.
 *
 * These are allocated on the system heap.
 */
struct Thread {
    /*
     * Interpreter state which must be preserved across nested
     * interpreter invocations (via JNI callbacks).  Must be the first
     * element in Thread.
     */
    InterpSaveState interpSave;
.....
    /* current limit of stack; flexes for StackOverflowError */
    const u1*   interpStackEnd;
......
    /* start (high addr) of interp stack (subtract size to get malloc addr) */
    u1*         interpStackStart;
......
};

Thread的interpStackStart和interpStackEnd表示虚拟栈的开始和结束范围。注意,栈的增长方向是由高地址向低地址曾长,因此inpterpStackStart >interpStackEnd。
inpterpSave的curFrame记录了当前虚拟栈(Frame)的位置。

我用下面的表来表示虚拟栈的组成部分:



  • StackSaveArea保存的是一个method 栈的信息,如当前的Method, 调用的PC地址,返回值地址等,还有prevFrame,指向Caller的frame地址;
  • curFrame保存当前函数的Frane地址。这个地址不包含StackSaveArea的指针。
  • method的registersSize指出全部寄存器的个数,包括用作参数传递的寄存器个数;
  • method的insSize指出作为参数的寄存器的个数;
  • method的outsSize指出该函数调用其他函数需要的寄存器个数
关于registerSize,insSize和outsSize的关系,以下几点:
  • 参数寄存器 (ins registers)是全部寄存器的一部分,所以,insSize < registerSize;
  • outsSize是调用其他函数需要的寄存器个数,这只是一个参考值。因为一个函数可以调用N个函数,这些被调用的函数的参数个数不一样,所以,outsSize总是取被调用函数中参数最多的那个值作为它的值;
  • 假设函数A调用函数B,A 向它的out register写入调用参数,当进入B函数后,A的out register就变成了B的ins register。这样,B就可以直接访问A传递过来的参数了。
  • ins register是register的一部分,在函数内部是可以参与运算的。因为需要和调用者共用,所以ins register总是位于虚拟栈的最高地址处。
关于参数,还需有注意一点:非静态函数,参数P0被隐含定义为this指针。

dvmCallMethodV的栈特点

dvmCallMethodV要插入一个BreakStackSaveArea对象,这也是个StackSaveArea,用于模拟一个栈,保证调用链条的连续性。

如下图:



dvmInterpret

该函数是解释模式的入口函数。在介绍这个函数之前,需要介绍两个重要的概念:InterpSaveState和ExecutionSubModes

InterpSaveState
InterpSaveState 结构体保存在Thread中,用于存储解释模式中重要的数据结构。它的定义如下: (vm/interp/InterpState.h)
struct InterpSaveState {
    const u2*       pc;         // Dalvik PC
    u4*             curFrame;   // Dalvik frame pointer
    const Method    *method;    // Method being executed
    DvmDex*         methodClassDex;
    JValue          retval;
    void*           bailPtr;
#if defined(WITH_TRACKREF_CHECKS)
    int             debugTrackedRefStart;
#else
    int             unused;        // Keep struct size constant
#endif
    struct InterpSaveState* prev;  // To follow nested activations
} __attribute__ ((__packed__));
  • pc: 保存的是当前正在执行的dalvik代码的地址
  • curFrame: 当前的frame地址,与StackSaveArea中的 curFrame是一致的
  • method: 当前调用的Method对象
  • methodClassDex: 当前method所属的DvmDex对象。这个对象包含了一个Dex文件对象和解析表(ResolvedClasses, ResolvedMethod, ResolvedString和ResolevedField等)
  • retval:返回值
  • bailPtr: 保存的是寄存器sp的地址,用于恢复解释器的堆栈
  • prev:指向上一个InterpSaveState对象。这个链表的建立就是在dvmIntepreter函数中实现的。
解释器要不断的更新这些值,保持这些值与运行状态一致。

ExecutionSubModes
它是一个枚举值,它的定义如下:
/*
 * Execution sub modes, e.g. debugging, profiling, etc.
 * Treated as bit flags for fast access.  These values are used directly
 * by assembly code in the mterp interpeter and may also be used by
 * code generated by the JIT.  Take care when changing.
 */
enum ExecutionSubModes {
    kSubModeNormal            = 0x0000,   /* No active subMode */
    kSubModeMethodTrace       = 0x0001,
    kSubModeEmulatorTrace     = 0x0002,
    kSubModeInstCounting      = 0x0004,
    kSubModeDebuggerActive    = 0x0008,
    kSubModeSuspendPending    = 0x0010,
    kSubModeCallbackPending   = 0x0020,
    kSubModeCountedStep       = 0x0040,
    kSubModeCheckAlways       = 0x0080,
    kSubModeSampleTrace       = 0x0100,
    kSubModeJitTraceBuild     = 0x4000,
    kSubModeJitSV             = 0x8000,
    kSubModeDebugProfile   = (kSubModeMethodTrace |
                              kSubModeEmulatorTrace |
                              kSubModeInstCounting |
                              kSubModeDebuggerActive)
};

整个mode可以分为两类,第一类是kSubModeNormal,这种模式下,解释器正常执行dalvik的字节码;其余可以归结为另一类,这类模式下,在执行dalvik字节码之前,先要调用一个dvmCheckBefore函数,这个函数会根据不同的submode,执行不同的操作,为debug,jit的trace code等功能,提供入口。
这个枚举值记录在Thread::InterpBreak.ctl.subMode中。

他通过调用 dvmDisableSubMode/dvmEnableSubMode -> updateInterpBreak函数来实现对模式的切换。
/*
 * Update interpBreak for a single thread.
 */
void updateInterpBreak(Thread* thread, ExecutionSubModes subMode, bool enable)
{
    InterpBreak oldValue, newValue;
    do {
.....
#ifndef DVM_NO_ASM_INTERP
        newValue.ctl.curHandlerTable = (newValue.ctl.breakFlags) ?
            thread->altHandlerTable : thread->mainHandlerTable;
#endif
    } while (dvmQuasiAtomicCas64(
在C语言里,位左对齐右对齐一般在格式化输出时会用到,主要用于控制数据在输出时的位置。以下是相关介绍: ### 整型数据的左对齐右对齐 通过`printf`函数实现整型数据的左对齐右对齐右对齐是默认方式,在格式说明符`%`和`d`之间添加数字来规定输出宽度,若数字位数小于规定宽度,会在左边补空格;左对齐则需在数字前加`-`号,若数字位数小于规定宽度,会在右边补空格。 示例代码如下: ```c #include <stdio.h> int main() { // 右对齐。数字宽度为10,若不足10,在左边补足空格 printf("%10d\n", 1234); // 左对齐。数字宽度为10,若不足10,在右边补足空格 printf("%-10d\n", 1234); return 0; } ``` ### 不同输出长度的情况 当规定的输出宽度和数字实际位数不同时,有不同的处理方式。若规定宽度小于数字实际位数,会完整输出数字;若规定宽度大于数字实际位数,右对齐在左边补空格,左对齐在右边补空格。 示例代码如下: ```c #include <stdio.h> int main() { // -5是左对齐,输出长度为5。5是右对齐,输出长度为5 printf("%-5d %5d\n", 455, 455); printf("%-5d %5d\n", -123, -123); // 规定宽度小于实际位数,完整输出数字 printf("%-5d %5d\n", 987654, 987654); return 0; } ``` ### 其他数据类型的对齐 除整型外,其他数据类型也能实现左对齐右对齐。例如浮点数(`%f`)、字符串(`%s`)等,方法和整型一致。 示例代码如下: ```c #include <stdio.h> int main() { // 右对齐浮点数,宽度为10 printf("%10f\n", 3.14); // 左对齐浮点数,宽度为10 printf("%-10f\n", 3.14); // 右对齐字符串,宽度为10 printf("%10s\n", "hello"); // 左对齐字符串,宽度为10 printf("%-10s\n", "hello"); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值