您的位置:首页 > 其它

dalvik VM的解释器分析

2016-07-25 17:29 621 查看
以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(oldValue.all, newValue.all,
             &thread->interpBreak.all) != 0);
};
#endif
} while (dvmQuasiAtomicCas64(oldValue.all, newValue.all,
&thread->interpBreak.all) != 0);
}
循环的目的只是为了做原子同步。

其中,newValue.ctl.curHandlerTable取mainHandlerTable与altHandlerTable中的一个。mainHandlerTable的对应kSubModeNormal,表示不加dvmCheckBefore调用的实现;altHandlerTable就是加dvmCheckBefore的调用。

而curHandleTable就是解释器的入口函数地址。

mainHandlerTable与altHandlerTable的值来自./vm/Thread.cpp的allocThread函数:
#ifndef DVM_NO_ASM_INTERP
thread->mainHandlerTable = dvmAsmInstructionStart;
thread->altHandlerTable = dvmAsmAltInstructionStart;
thread->interpBreak.ctl.curHandlerTable = thread->mainHandlerTable;
#endif
dvmAsmInstructionStart和dvmAsmAltInstructionStart的定义在(vm/mterp/out/InterpAsm-armv7-a-neon.S 这是主流设备的架构)
368     .global dvmAsmInstructionStart
369     .type   dvmAsmInstructionStart, %function
370 dvmAsmInstructionStart = .L_OP_NOP
.....
9684     .global dvmAsmAltInstructionStart
9685     .type   dvmAsmAltInstructionStart, %function
9686     .text
9687
9688 dvmAsmAltInstructionStart = .L_ALT_OP_NOP


当完成这些操作后,dvm就会调用到函数dvmMterpStd。

这里还需要解释下,handleTable是什么样的概念,以及与dvmMterpStd有何不同。
dvmMterpStd是解释器的入口函数,而handleTable则是一个表,与dalvik字节码对应的一个表。dalvik字节码由两部分组成:操作代码(opcode)和操作数(operand)组成。
opcode由1个字节组成,operand则可以是0~5个更多。所以opcode最大是255。比如:
mov v1, v2
该字节码就是令v1 = v2。mov是opcode的助记符,v1, v2就是operand。

所以,一个opcode对应一段汇编代码,然后将这些汇编代码按照opcode的值顺序排列起来,就成了handleTable了。当我们执行时,只需要取出opcode对应的汇编码地址,就可以直接跳转并执行了。

详细的实现,厚文中介绍。

dvmMterpStd

dvmMterpStd函数通过调用dvmMterpStdRun以真正进入到解释器入口中。

如何读懂VM 解释器的汇编码?

你需要了解基本的ARM汇编。如果不了解可以从网上了解下,只需要一些入门的知识即可。

我们所阅读的汇编代码,主要在vm/mterp/out/InterpAsm-armv7-a-neon.S文件中。

HandlerTable

mainHandlerTable

VM汇编解释器中的核心代码是每个dalvik opcode对应的汇编代码段组成的handle表。这个表上面我们已经初步介绍了,现在我们介绍下其具体的实现。大家看一个例子:

/* ------------------------------ */
.balign 64
.L_OP_MOVE: /* 0x01 */
/* File: armv6t2/OP_MOVE.S */
/* for move, move-object, long-to-int */
/* op vA, vB */
mov     r1, rINST, lsr #12          @ r1<- B from 15:12
ubfx    r0, rINST, #8, #4           @ r0<- A from 11:8
FETCH_ADVANCE_INST(1)               @ advance rPC, load rINST
GET_VREG(r2, r1)                    @ r2<- fp
GET_INST_OPCODE(ip)                 @ ip<- opcode from rINST
SET_VREG(r2, r0)                    @ fp[A]<- r2
GOTO_OPCODE(ip)                     @ execute next instruction


第一行,".balign 64" 表示代码从64字节处对齐。这条伪指令表形成的关键。所有的opcode对应的汇编码都是从64字节处对齐的,也就是说,opcode对应的汇编码,其最大大小是64字节。由此,handleTable就是一个64*opcode_count的数组,只要用 handleTableStart + opcode*64就可以得到opcode对应的地址了。
.L_OP_MOVE: 这个是标签,标签的取名是用 .L_OP_<opcode助记符大写> 组成的
GOTO_OPCODE(ip):该宏的作用,是跳转到下条指令

我们知道,mainHandlerTable对应的dvmAsmInstructionStart是从.L_OP_NONE开始的,汇编代码按照opcode的值顺序排列下来,每个标签间隔64字节,这样,就形成了一个完整的表。

altHandlerTable的小技巧

altHandlerTable指向的dvmAsmAltInstructionStart,是一组以.L_ALT_OP_<opcode助记符大写> 的标签组成,这些标签也是以64字节对齐的。 altHandlerTable实际上只是实现了一个跳转,比如.L_ALT_OP_MOVE在调用完dvmCheckBefore后,在转而调用.L_OP_MOV的。我们还是以mov指令为例:

/* ------------------------------ */
.balign 64
.L_ALT_OP_MOVE: /* 0x01 */
/* File: armv5te/alt_stub.S */
/*
* Inter-instruction transfer stub.  Call out to dvmCheckBefore to handle
* any interesting requests and then jump to the real instruction
* handler.    Note that the call to dvmCheckBefore is done as a tail call.
* rIBASE updates won't be seen until a refresh, and we can tell we have a
* stale rIBASE if breakFlags==0.  Always refresh rIBASE here, and then
* bail to the real handler if breakFlags==0.
*/
ldrb   r3, [rSELF, #offThread_breakFlags]
adrl   lr, dvmAsmInstructionStart + (1 * 64)
ldr    rIBASE, [rSELF, #offThread_curHandlerTable]
cmp    r3, #0
bxeq   lr                   @ nothing to do - jump to real handler
EXPORT_PC()
mov    r0, rPC              @ arg0
mov    r1, rFP              @ arg1
mov    r2, rSELF            @ arg2
b      dvmCheckBefore       @ (dPC,dFP,self) tail call
重点看
adrl   lr, dvmAsmInstructionStart + (1 * 64)
这句话的意思是lr = dvmAsmInstructionStart + 1 * 64,其结果,就是lr = .L_OP_MOV的地址。

lr是link register的缩小,代表函数返回地址。一般情况下,我们调用一个函数,用bl <func>的形式。bl是branch link的缩写,即跳转到<func>函数入口,然后把返回地址写入lr寄存器。当<func>函数返回时,从lr中读取返回地址。

大家请注意,上面代码最后一句: b dvmCheckBefore,用b指令(branch),则只跳转,不改写lr寄存器的值,那么这个时候,寄存器的值就是.L_OP_MOV的地址,那就是说,当 dvmCheckBefore函数返回后,将直接返回到.L_OP_MOV处继续执行。

这就是dvm的小伎俩。所有的altHandle都是这么处理的。这样,在正式执行指令之前,我们就可以调用dvmCheckBefore做些处理了。

寄存器的使用情况

我们看到有如下定义:

/* single-purpose registers, given names for clarity */
#define rPC     r4
#define rFP     r5
#define rSELF   r6
#define rINST   r7
#define rIBASE  r8


rPC: 是当前正在执行的字节码的地址
rIBASE: 这个是curHandlerTable的地址
rSELF: 当前Thread对象指针
rFP : curFrame的地址
rINST: 当前的字节码值,只有低16位被用到,因为字节码以16位为一个单位。
r0~r3:是用作临时寄存器的
r10 : 作为一个通用寄存器来使用
r12(ip) 用作指令跳转的寄存器
其余的被操作系统使用

几个重要的宏说明

FETCH_INST系列

这些包括FETCH_INST,FETCH_ADVANCE_INST, PREFETCH_ADVANCE_INST,FETCH_ADVANCE_INST_RB这些

他们的作用是获取inst到寄存器rINST或者其他寄存器。
以其中一个为例:
#define FETCH_ADVANCE_INST(_count) ldrh    rINST, [rPC, #((_count)*2)]!
该宏的等价表达式是 rINST = *(short*)(rPC += count*2)。
最后的一个"!"表示rPC在取值后要加上_count*2。

FETCH数据系列

包括FETCH, FETCH_S, FETCH_B宏,"S"表示signed,"B"表示byte。

#define FETCH(_reg, _count)     ldrh    _reg, [rPC, #((_count)*2)]


GOTO_OPCODE系列

包括下面几个宏
#define GOTO_OPCODE(_reg)       add     pc, rIBASE, _reg, lsl #6
#define GOTO_OPCODE_BASE(_base,_reg)  add     pc, _base, _reg, lsl #6
#define GOTO_OPCODE_IFEQ(_reg)  addeq   pc, rIBASE, _reg, lsl #6
#define GOTO_OPCODE_IFNE(_reg)  addne   pc, rIBASE, _reg, lsl #6


GOTO_OPCODE宏,就是 pc = rIBASE + (_reg << 6)
_reg << 6 相当于 _reg *64。rIBASE是curHandleTable的值,_reg的值,必须是opcode的值,这个就是直接跳转到指定的opcode上去。

addeq表示标志寄存器比较标志为0时跳转,addne则表示不为零时跳转。

VREG操作系列

#define GET_VREG(_reg, _vreg)   ldr     _reg, [rFP, _vreg, lsl #2]
#define SET_VREG(_reg, _vreg)   str     _reg, [rFP, _vreg, lsl #2]
这两个是用来获取虚拟寄存器中的值到指定寄存器的函数。

Thread管理相关的宏

/* save/restore the PC and/or FP from the thread struct */
#define LOAD_PC_FROM_SELF()     ldr     rPC, [rSELF, #offThread_pc]
#define SAVE_PC_TO_SELF()       str     rPC, [rSELF, #offThread_pc]
#define LOAD_FP_FROM_SELF()     ldr     rFP, [rSELF, #offThread_curFrame]
#define SAVE_FP_TO_SELF()       str     rFP, [rSELF, #offThread_curFrame]
#define LOAD_PC_FP_FROM_SELF()  ldmia   rSELF, {rPC, rFP}
#define SAVE_PC_FP_TO_SELF()    stmia   rSELF, {rPC, rFP}

/*
* "export" the PC to the stack frame, f/b/o future exception objects.  Must
* be done *before* something throws.
*
* In C this is "SAVEAREA_FROM_FP(fp)->xtra.currentPc = pc", i.e.
* fp - sizeof(StackSaveArea) + offsetof(SaveArea, xtra.currentPc)
*
* It's okay to do this more than once.
*/
#define EXPORT_PC() \
str     rPC, [rFP, #(-sizeofStackSaveArea + offStackSaveArea_currentPc)]

/*
* Given a frame pointer, find the stack save area.
*
* In C this is "((StackSaveArea*)(_fp) -1)".
*/
#define SAVEAREA_FROM_FP(_reg, _fpreg) \
sub     _reg, _fpreg, #sizeofStackSaveArea
上面这些宏都是为了读写Thread对象的成员而准备的。

一些常见指令的实现分析

const指令

const指令是对所有立即数的操作,其中包括3类:数字类(整数、浮点数)、字符串类和class操作。

const数字

const指令包括const/4, const/16, const, const/high16, const_wide, const_wide/32, const_wide/high16这些指令。这些指令的区别只在于数字宽度不一样。
我以const指令为例
/* ------------------------------ */
.balign 64
.L_OP_CONST: /* 0x14 */
/* File: armv5te/OP_CONST.S */
/* const vAA, #+BBBBbbbb */
mov     r3, rINST, lsr #8           @ r3<- AA
FETCH(r0, 1)                        @ r0<- bbbb (low)
FETCH(r1, 2)                        @ r1<- BBBB (high)
FETCH_ADVANCE_INST(3)               @ advance rPC, load rINST
orr     r0, r0, r1, lsl #16         @ r0<- BBBBbbbb
GET_INST_OPCODE(ip)                 @ extract opcode from rINST
SET_VREG(r0, r3)                    @ vAA<- r0
GOTO_OPCODE(ip)                     @ jump to next instruction


const指令的助记符形式是
/* const vAA, #+BBBBbbbb */
要让vAA = +BBBBbbbb值。
按照小端字节的排列顺序,这些值保存在  op|AA  BBBB bbbb。除了"op"外,一个字母表示4位。
可以看到,上面的代码含义是:
r3 = rINST >> 8;
r0 = *(++rPC)
r0 |= *(++rPC) << 16
ip = rINST & 255;
pc = rIBASE + ip * 64


const/string

string是一个java.lang.String对象,需要从解析表中获取,代码如下:
.balign 64
.L_OP_CONST_STRING: /* 0x1a */
/* File: armv5te/OP_CONST_STRING.S */
/* const/string vAA, String@BBBB */
FETCH(r1, 1)                        @ r1<- BBBB
ldr     r2, [rSELF, #offThread_methodClassDex]  @ r2<- self->methodClassDex
mov     r9, rINST, lsr #8           @ r9<- AA
ldr     r2, [r2, #offDvmDex_pResStrings]   @ r2<- dvmDex->pResStrings
ldr     r0, [r2, r1, lsl #2]        @ r0<- pResStrings[BBBB]
cmp     r0, #0                      @ not yet resolved?
beq     .LOP_CONST_STRING_resolve
FETCH_ADVANCE_INST(2)               @ advance rPC, load rINST
SET_VREG(r0, r9)                    @ vAA<- r0
GET_INST_OPCODE(ip)                 @ extract opcode from rINST
GOTO_OPCODE(ip)                     @ jump to next instruction

如果DvmDex的pResStrings已经有解析好的stirng对象,直接取出,否则就调用dvmResolveString来解析。dvmResolveString函数会解析出字符串后再把他们添加到DvmDex的pResStrings中去,下次使用时,就不必再次解析了。

在这里,指令里面包含了字符串的string ID,通过该ID来解析字符串。

const/class

该指令是获取一个class对象,然后写入到指定的虚拟寄存器中
/* ------------------------------ */
.balign 64
.L_OP_CONST_CLASS: /* 0x1c */
/* File: armv5te/OP_CONST_CLASS.S */
/* const/class vAA, Class@BBBB */
FETCH(r1, 1)                        @ r1<- BBBB
ldr     r2, [rSELF, #offThread_methodClassDex]  @ r2<- self->methodClassDex
mov     r9, rINST, lsr #8           @ r9<- AA
ldr     r2, [r2, #offDvmDex_pResClasses]   @ r2<- dvmDex->pResClasses
ldr     r0, [r2, r1, lsl #2]        @ r0<- pResClasses[BBBB]
cmp     r0, #0                      @ not yet resolved?
beq     .LOP_CONST_CLASS_resolve
FETCH_ADVANCE_INST(2)               @ advance rPC, load rINST
SET_VREG(r0, r9)                    @ vAA<- r0
GET_INST_OPCODE(ip)                 @ extract opcode from rINST
GOTO_OPCODE(ip)                     @ jump to next instruction


实现同string非常相似,主要不同在于它调用了dvmResolveClass来解析。

mov指令

mov指令按照不同字宽,包括mov, mov/from16, mov/16, mov/wide, mov/wide16,mov/object, mov/from_object, mov/result, mov/result_wide,mov/exception等。
根据名字,我们就大概知道其功能了。其中mov/result, mov/result_wide,是从Thread.retal中读取对应的值。当return时,解释器会将函数结果存储在Thread::retval变量中。

还是看看mov的例子吧
/* ------------------------------ */
    .balign 64
.L_OP_MOVE: /* 0x01 */
/* File: armv6t2/OP_MOVE.S */
    /* for move, move-object, long-to-int */
    /* op vA, vB */
    mov     r1, rINST, lsr #12          @ r1<- B from 15:12
    ubfx    r0, rINST, #8, #4           @ r0<- A from 11:8
    FETCH_ADVANCE_INST(1)               @ advance rPC, load rINST
    GET_VREG(r2, r1)                    @ r2<- fp[B]
    GET_INST_OPCODE(ip)                 @ ip<- opcode from rINST
    SET_VREG(r2, r0)                    @ fp[A]<- r2
    GOTO_OPCODE(ip)                     @ execute next instruction
实际要达到的效果是 fp[A] = fp[B],fp的地址放在rFP寄存器中。

get/put指令

java中对field的读,对应的是get指令,写,对应的是put指令。get/put分sget/sput和iget/iput。's'表示static即对静态field的读写,'i'表示instance,对对象field的读写。按照数据类型不同,分为iget, iget/wide,iget/object,iget/byte, iget/short,等等。每种get和put都是这样。
我们只要看看iget就知道他们的大致写法了
/* ------------------------------ */
.balign 64
.L_OP_IGET: /* 0x52 */
/* File: armv6t2/OP_IGET.S */
/*
* General 32-bit instance field get.
*
* for: iget, iget-object, iget-boolean, iget-byte, iget-char, iget-short
*/
/* op vA, vB, field@CCCC */
mov     r0, rINST, lsr #12          @ r0<- B
ldr     r3, [rSELF, #offThread_methodClassDex]    @ r3<- DvmDex
FETCH(r1, 1)                        @ r1<- field ref CCCC
ldr     r2, [r3, #offDvmDex_pResFields] @ r2<- pDvmDex->pResFields
GET_VREG(r9, r0)                    @ r9<- fp[B], the object pointer
ldr     r0, [r2, r1, lsl #2]        @ r0<- resolved InstField ptr
cmp     r0, #0                      @ is resolved entry null?
bne     .LOP_IGET_finish          @ no, already resolved
8:  ldr     r2, [rSELF, #offThread_method]    @ r2<- current method
EXPORT_PC()                         @ resolve() could throw
ldr     r0, [r2, #offMethod_clazz]  @ r0<- method->clazz
bl      dvmResolveInstField         @ r0<- resolved InstField ptr
cmp     r0, #0
bne     .LOP_IGET_finish
b       common_exceptionThrown


field的索引保存在CCCC部分,一个字母表示4位。CCCC最大值是65535。
如果field没有解析,还需有调用dvmResolveInstField先解析他,然后在继续运行。由于iget指令需要的代码数超出了64字节,因此用.LOP_IGET_FINISH完成剩余的工作。

INVOKE指令的分析

INVOKE指令是其中比较复杂的指令,它按照参数,可以分成RANGE和不带RANGE的,区别在于用于传递参数的寄存器是否是一个范围;按照类型可以分为: INVOKE_STATIC/DIRECT/VIRTUAL/INTERFACE/SUPER这几种。
其中static, direct, native函数都是非常接近的。它们的主要不同,在于获取被调用method的方法不同。

对于DVM来说,要invoke一个方法,需要经历3个步骤:

取得method
准备参数
准备栈结构

最后跳转到第一条指令并执行

取得method

按照不同的method类型,获取method的方法也不一样

direct/static

这3类结构是一样的,即在dalvik字节码中,保存的都是被调用函数的索引
字节码的形式是
/* op vB, {vD, vE, vF, vG, vA}, class@CCCC */
/* op {vCCCC..v(CCCC+AA-1)}, meth@BBBB */
其中class@CCCC或者meth@BBBB表示的就是method的索引。
用伪码表示就是
if (!self->pDvmDex->pResMethods[BBBB]) {
dvmResolveMethod(class, BBBB, MEHOD_DIRECT);
}
....

这是最简单的形式

virtual/super

根据不同的this的类型,获取的method对象也不一样,所以,它必须先取得method对象的methodIdx,然后再从对象的class的vtable中取得真正的method,才能得到正确的method对象。用伪代码表示就是
/* op vB, {vD, vE, vF, vG, vA}, class@CCCC */

mth = self->pDvmDex->pResMethods[CCCC];
if (mth == NULL) {
mth = dvmResoleMethod(self->inpterState.method->clazz, BBBB, METHOD_VIRTUAL);
}
int mthidx = mth->methodIndex;
mth = this->clazz->vtable[mthidx];


super的实现与virtual很类似,所不同的是,对于super,需要从this->clazz->super->vtable中获取method。

interface

interface method的获取与其他method有很大的不同。在dalvik指令中,给出的method@BBBB这样的id,实际上指向的是interface的method,而我们执行时,需要获取的,却是this所属的class的对应的method。完成这个过程,是通过函数dvmFindInterfaceMethodInCache来实现的。

dvmFindInterfaceMethodInCache->dvmInterpFindInterfaceMethod,他们将解析出的真正method缓存起来,用hash表将interface method,class与对应的真实method关联起来。

首先,我们看看,如何从ineterface的method找到implements class的method。

[b]从ineterface的method找到implements class的method

该功能由函数dvmInterpFindInterfaceMethod来实现的。这个算法的核心是InterfaceEntry结构。每个class都有这样一个结构,叫做iftable。iftable是一个InterfaceEntry结构的数组。这个结构如下:
/*
* Used for iftable in ClassObject.
*/
struct InterfaceEntry {
/* pointer to interface class */
ClassObject*    clazz;

/*
* Index into array of vtable offsets.  This points into the ifviPool,
* which holds the vtables for all interfaces declared by this class.
*/
int*            methodIndexArray;
};

clazz:成员指定了当前一个class implememts其中的一个interface的clazz对象
methodIndexArray:interface的method在implements类中的对应的implement method在vtable中的索引

下面的源代码给出了基本的实现:

/*
* Find the concrete method that corresponds to "methodIdx".  The code in
* "method" is executing invoke-method with "thisClass" as its first argument.
*
* Returns NULL with an exception raised on failure.
*/
Method* dvmInterpFindInterfaceMethod(ClassObject* thisClass, u4 methodIdx,
const Method* method, DvmDex* methodClassDex)
{
Method* absMethod;
Method* methodToCall;
int i, vtableIndex;

/*
* Resolve the method.  This gives us the abstract method from the
* interface class declaration.
*/
absMethod = dvmDexGetResolvedMethod(methodClassDex, methodIdx);
....
/* 找出来对应的iftable */
for (i = 0; i < thisClass->iftableCount; i++) {
if (thisClass->iftable[i].clazz == absMethod->clazz)
break;
}
.....
/*用methodIndex得到vtableIndex */
vtableIndex =
thisClass->iftable[i].methodIndexArray[absMethod->methodIndex];
assert(vtableIndex >= 0 && vtableIndex < thisClass->vtableCount);
methodToCall = thisClass->vtable[vtableIndex];

......
return methodToCall;
}


cache的结构和存储方法
每次都需要resolve interface method和一个for循环来查找InterfaceEntry结构,效率很低,于是,dvm引入了一个hash表来解决这个问题。
cache的定义通过宏ATOMIC_CACHE_LOOKUP来实现。

hash表的定义在Thread::pDevDex::pInterfaceCache。这个数据是AtomicCache结构,这个结构是一个带有线程同步的cache结构。其中thisClass和methodId作为hash key,用于快速查找数据的实现。进一步的信息可以查看宏ATOMIC_CACHE_LOOKUP

准备参数

按照是否是rang,分为common_invokeMethodNoRange和common_invokeMethodRange两个标签。这两个标签的功能都是为了拷贝参数,只是实现方法上有不同。要拷贝参数,都分成两个步骤

分配"outs"空间
执行参数拷贝

看下分配"outs"空间部分
@ prepare to copy args to "outs" area of current frame
movs    r2, rINST, lsr #12          @ r2<- B (arg count) -- test for zero
SAVEAREA_FROM_FP(r10, rFP)          @ r10<- stack save area
FETCH(r1, 2)                        @ r1<- GFED (load here to hide latency)
beq     .LinvokeArgsDone
首先,令r2 = arg_count, 然后取得r10 = rFP - sizeof(StackSaveArea)。然后将参数依次填充到r10之前的内存中去。这里,r10每次要-4. 
我将汇编与伪代码对照起来,就是
.LinvokeNonRange:
rsb     r2, r2, #5                  @ r2<- 5-r2                         |         r2 = 5-r2
add     pc, pc, r2, lsl #4        @ computed goto, 4 instrs each      |         switch(r2) {
bl      common_abort                @ (skipped due to ARM prefetch)     |
5:  and     ip, rINST, #0x0f00          @ isolate A                         |         case 0:
ldr     r2, [rFP, ip, lsr #6]       @ r2<- vA (shift right 8, left 2)   |            ip = rINST & 0x0f00; //vA
mov     r0, r0                      @ nop                               |            r2 = *(rFP + (ip >> 6))
str     r2, [r10, #-4]!             @ *--outs = vA                      |            *(r10 - 4) = r2; r10 -= 4;
4:  and     ip, r1, #0xf000             @ isolate G                         |         case 1:
ldr     r2, [rFP, ip, lsr #10]      @ r2<- vG (shift right 12, left 2)  |            ip = r1 & 0xf0000; //vG
mov     r0, r0                      @ nop                               |            r2 = *(rFP + (ip >> 10));
str     r2, [r10, #-4]!             @ *--outs = vG                      |            *(r10 - 4) = r2; r10 -= 4;
3:  and     ip, r1, #0x0f00             @ isolate F                         |         case 2:
ldr     r2, [rFP, ip, lsr #6]       @ r2<- vF                           |            ip = r1 & 0x0f00
mov     r0, r0                      @ nop                               |            r2 = *(rFP + (ip >> 6));
str     r2, [r10, #-4]!             @ *--outs = vF                      |            *(r10 - 4) = r2; r10 -= 4;
2:  and     ip, r1, #0x00f0             @ isolate E                         |         case 3:
ldr     r2, [rFP, ip, lsr #2]       @ r2<- vE                           |            ip = r1 & 0x00f0;
mov     r0, r0                      @ nop                               |            r2 = *(rFP + (ip >> 2));
str     r2, [r10, #-4]!             @ *--outs = vE                      |            *(r10 - 4) = r2; r10 -= 4;
1:  and     ip, r1, #0x000f             @ isolate D                         |         case 4:
ldr     r2, [rFP, ip, lsl #2]       @ r2<- vD                           |            ip = r1 & 0x000f;
mov     r0, r0                      @ nop                               |            r2 = *(rFP + (ip << 2));
str     r2, [r10, #-4]!             @ *--outs = vD                      |            *(r10 - 4) = r2; r10 -= 4; }


dvm用了一个小技巧:
add     pc, pc, r2, lsl #4
巧妙的跳转到指定的位置,以便隔去那些不需要处理的分支。

准备栈结构


.LinvokeArgsDone标签做最后的步骤,即准备栈并调用函数。

准备栈部分的代码如下: (删除部分关系不大的代码)
.LinvokeArgsDone: @ r0=methodToCall
ldrh    r9, [r0, #offMethod_registersSize]  @ r9<- methodToCall->regsSize
ldrh    r3, [r0, #offMethod_outsSize]  @ r3<- methodToCall->outsSize
ldr     r2, [r0, #offMethod_insns]  @ r2<- method->insns
ldr     rINST, [r0, #offMethod_clazz]  @ rINST<- method->clazz
@ find space for the new stack frame, check for overflow
SAVEAREA_FROM_FP(r1, rFP)           @ r1<- stack save area
sub     r1, r1, r9, lsl #2          @ r1<- newFp (old savearea - regsSize)
SAVEAREA_FROM_FP(r10, r1)           @ r10<- newSaveArea
@    bl      common_dumpRegs
ldr     r9, [rSELF, #offThread_interpStackEnd]    @ r9<- interpStackEnd
sub     r3, r10, r3, lsl #2         @ r3<- bottom (newsave - outsSize)
cmp     r3, r9                      @ bottom < interpStackEnd?
ldrh    lr, [rSELF, #offThread_subMode]
ldr     r3, [r0, #offMethod_accessFlags] @ r3<- methodToCall->accessFlags
blo     .LstackOverflow             @ yes, this frame will overflow stack

@ set up newSaveArea
....
str     rFP, [r10, #offStackSaveArea_prevFrame]
str     rPC, [r10, #offStackSaveArea_savedPc]
<pre name="code" class="cpp" style="font-size: 13.3333339691162px;">1:
    tst     r3, #ACC_NATIVE
    bne     .LinvokeNative

......

    ldrh    r9, [r2]                        @ r9 <- load INST from new PC
    ldr     r3, [rINST, #offClassObject_pDvmDex] @ r3<- method->clazz->pDvmDex
    mov     rPC, r2                         @ publish new rPC

    @ Update state values for the new method
    @ r0=methodToCall, r1=newFp, r3=newMethodClass, r9=newINST
    str     r0, [rSELF, #offThread_method]    @ self->method = methodToCall
    str     r3, [rSELF, #offThread_methodClassDex] @ self->methodClassDex = ...
.....
    mov     rFP, r1                         @ fp = newFp
    GET_PREFETCHED_OPCODE(ip, r9)           @ extract prefetched opcode from r9
    mov     rINST, r9                       @ publish new rINST
    str     r1, [rSELF, #offThread_curFrame]   @ curFrame = newFp
    GOTO_OPCODE(ip)                         @ jump to next instruction
....


还是用r10来做临时变量,可以看到,伪代码是这样的
r9 = method->registerSize;
r2 = method->insns; //code的入口地址
r1 = rFP - sizeof(StackSaveArea);
r1 = r1 - r9 * 4;
r10 = r1 - sizeof(StackSaveArea);

r10->prevFrame = rFP;
r10->savedPc = rPC;

r9 = *r2;
rPc = r9; //即 *method->insns; 第一条指令
rSELF->method = r0; //method to call
rSELF->pDvmDex = method_to_call->clazz->pDvmDex; //需要切换pDvmDex为当前调用函数的pDvmDex,否则很多解析表获取会出错

rFP = r1; //新的frame地址
ip = r9 & 0xff; //取得第一条指令的opcode
rINST = r9;
pc = curHandleTable[ip *64]; //完成跳转


对于native函数的调用,我们将在另外文章中详细描述。

异常处理

抛出异常

异常分为两种,一种是系统运行中,由系统产生的异常,一种是throw指令产生的异常。对于系统产生的异常,比如解析错误等,由内部函数dvmThrowException或者其包装函数产生。

dvmThrowException->dvmThrowChainedException,该函数的基本功能很简单,即创建一个exception对象,然后赋值给Thread::exception成员。这就完成了异常的抛出工作。

对于用户用throw指令抛出的异常,则也是将异常赋值给Thread::exception成员。异常的创建则是由new-instance指令来完成的,而throw指令只负责把对应的exception放入到Thread::exception变量中。

处理异常

异常的处理是紧随者异常抛出的。通常,throw指令在设置完self->exception后,就会调用common_exceptionThrown这个标签,而对于系统异常,如dvmResolveClass函数调用完成后,只要判断返回值是否成功,如果失败,就直接跳转到common_exceptionThrown,去处理异常。

当异常发生时,当前程序流程要立即中断,然后进入到catch和finally块进行处理,这些都是由common_excetptionThrown完成的。

为了方便理解,我直接将其转为伪代码,有兴趣的同学可以看源码(common_exceptionThrown标签的内容)

<span style="font-family:microsoft yahei;"><span style="white-space: pre-wrap;">exception = self->exception;
dvmAddTrackedAlloc(self, exception); //防止GC回收正在处理的exception对象
self->exception = NULL; //情况异常

void *newFP;
ret = dvmFindCatchBlock(self, rPc - self->method->insns, exception, 0, &newFP);
if (self->stackOverflowed) {
dvmCleanupStackOverflow(self, exception);
}

rFP = newFP;
if (ret != 0) //catch到了
{
</span></span> method = (rFP - sizeof(StackSaveArea))->method;
self->method = method;
rPC = method->insns + ret * 2; //指令入口
dvmReleaseTrackedAlloc(exception, self);
rIBASE = self->curHandlerTable;
rINST = *rPC & 0xffff;
ip = rINST & 0xff; //取得opcode
pc = rIBASE + ip * 64;
<span style="font-family: 'microsoft yahei'; white-space: pre-wrap;">} else { //没有catch到
//进一步抛出异常
}</span>dvmFindCatchBlock是整个处理过程的核心,它返回catch(包括finally的处理地址和所在frame的地址。有了这两个信息后,dvm就能重建frame并跳转了。
我们知道,frame上有StackSaveArea,该结构保存了prevFrame和savedPc两个重要成员。prevFrame能让我们遍历整个数据栈,一直遇到一个BreakSaveArea。因为异常的处理不能跨越native函数,即如果A->nativeB->C->D,如果D发生异常,那么遍历到nativeB后就要停止,返回到nativeB函数,让nativeB函数去处理异常,或者继续抛出给A函数处理。

每个method的代码中都包含了catch信息,catch信息由(excetpion, startAddr,code-length, handle addr)组成,只要判断savedPc是否在startAddr和code-length中间,就能知道catch是否找到了。这个由函数dvmFindCatchBlock实现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: