作者 crosskernel@gmail.com
5.3 Portable Interpreter
最初的几个andoid版本里,dalvik的解释器是用c写的。这种解释器执行速度较慢,但可读性较强,移植性好,在以后Android版本里尽管实现了汇编优化的解释器,但这种portable解释器依然存在。在Android向某个全新架构的处理器上移植时,是没有对应的汇编解释器的,这时portable的价值就体现出来了。
该解释器的核心是一个handler数组,定义如下:
#define DEFINE_GOTO_TABLE(_name) \
static const void* _name[kNumDalvikInstructions] = { \
…
H(OP_MOVE_WIDE_FROM16), \
H(OP_MOVE_WIDE_16), \
H(OP_MOVE_OBJECT), \
H(OP_MOVE_OBJECT_FROM16), \
H(OP_MOVE_OBJECT_16), \
H(OP_MOVE_RESULT), \
H(OP_MOVE_RESULT_WIDE), \
H(OP_MOVE_RESULT_OBJECT), \
H(OP_MOVE_EXCEPTION),
…
\ }
该数组handlerTable存放着每个操作码的handler地址,每遇到一个操作码就跳到这个数组里取出handler来执行该操作码。
而每条操作码的handler结构被定义如下:
HANDLE_OPCODE(OP_XXX)
FINISH(…);
OP_END
其中HANDLE_OPCODE(OP_XXX)被定义成该段handler标号,handlerTable对应项指向这个地址,而每个操作码执行完,都通过FINISH(…)取出下一个操作码,然后跳入该操作码对应的handler中:
# define FINISH(_offset) { \
//将PC指向下一条字节码
ADJUST_PC(_offset); \
//取出字节码到inst
inst = FETCH(0); \
…
//“INST_INST(inst)”即为指令编号,根据这个编号索引handlerTable的位置
goto *handlerTable[INST_INST(inst)]; \
}
其中,ADJUST_PC(_offset)是将操作码代码段的pc指针指向下一个操作码,FETCH(0)的作用是取出这个操作码。最后跳入下一个操作码的handler
5.4 ASM Interpreter
Dalvik默认的解释器就是这种汇编优化的解释器,根据不同CPU架构有不同的实现,本文主要讨论的是arm V7架构的实现。
5.4.1 基本结构
跟portbale解释器一样,ASM解释器也是一个由不同字节码解释器组成的大数组,这是ASM Interpreter的mainhandler,在正常运行时使用,其实现在文件dalvik/vm/mterp/out/InterpAsm-armv7-a-neon.S”中:
mainHandler大数组定义如下:
//大数组的基地址:dvmAsmInstructionStart。即为编号为零的NOP指令的的地址
dvmAsmInstructionStart = .L_OP_NOP
//代码段
.text
/*偏移量64,每个handler 64 byte,不够的话再跳的其他地方,但要保证每个handler 64字节的入口*/
.balign 64
.L_OP_NOP: /* 0x00 */
/* File: armv5te/OP_NOP.S */
…
//64字节对齐,第二个字节码MOVE的handler
.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
…
.balign 64
.size dvmAsmInstructionStart, .-dvmAsmInstructionStart
.global dvmAsmInstructionEnd
//mainhandler数组结束
dvmAsmInstructionEnd:
对于有些字节码不能用64字节完成其handler实现,asm解释器将其余实现放在代码段:dvmAsmSisterStart里。
ALThandler
ASM Interpreter的还有一个ALThandler,在JIT和debugger时使用,其实现也在文件dalvik/vm/mterp/out/InterpAsm-armv7-a-neon.S”中:
//全局变量dvmAsmAltInstructionStart
.global dvmAsmAltInstructionStart
.type dvmAsmAltInstructionStart, %function
//代码段
.text
//ALThandler数组也是跟字节码指令一一对应
dvmAsmAltInstructionStart = .L_ALT_OP_NOP
//也是64字节对齐
.balign 64
.L_ALT_OP_NOP: /* 0x00 */
…
.balign 64
.L_ALT_OP_MOVE: /* 0x01 */
…
.balign 64
//详细分析一个ALT handler的结构,其余ALThandler类似
.L_ALT_OP_IF_GEZ: /* 0x3b */
//把线程结构的breakFlags放到r3
ldrb r3, [rSELF, #offThread_breakFlags]
adrl lr, dvmAsmInstructionStart + (59 * 64)
ldr rIBASE, [rSELF, #offThread_curHandlerTable]
/*检查breakFlags是否为0,如为0直接跳到mainhandler,lr为mainhandler数组里对应地址*/
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
//breakFlags被置为,需要进一步到dvmCheckBefore检查
b dvmCheckBefore @ (dPC,dFP,self) tail call
…
.balign 64
//althander数组长度
.size dvmAsmAltInstructionStart, .-dvmAsmAltInstructionStart
.global dvmAsmAltInstructionEnd
//althander数组结束
dvmAsmAltInstructionEnd:
Handler的启用
在一个dalvik创建之初,在线程的管理结构里记录下该handler的地址。
static Thread* allocThread(int interpStackSize)
{ …
//“mainHandlerTable”偏移值为88,即为:“offThread_mainHandlerTable”
thread->mainHandlerTable = dvmAsmInstructionStart;
// “altHandlerTable”即为“dvmAsmAltInstructionStart;”
thread->altHandlerTable = dvmAsmAltInstructionStart;
//“interpBreak.ctl.curHandlerTable”偏移值为40,即为:“offThread_curHandlerTable”
thread->interpBreak.ctl.curHandlerTable = thread->mainHandlerTable;
…
}
5.4.2 运行时模型与基本操作
Asm解释器定义了专门的寄存器来对应Dalvik虚拟机模型
// rPC指向dalvik操作码的地址
#define rPC r4
//fFP指向dalvik的帧,这是在编译时确定下来的寄存器组
#define rFP r5
//rSELF指向当前线程的struct Thread结构。
#define rSELF r6
// rINST为当前指令
#define rINST r7
// rIBASE指向字节码handler大数组的基地址
#define rIBASE r8
基本操作
//把以当前操作码为基址,偏移量为_countX2,开始的无符号半字加载到寄存器_reg中
#define FETCH(_reg, _count) ldrh _reg, [rPC, #((_count)*2)]
//把rPC和rFP从当前线程的struct Thread结构里取出来
#define LOAD_PC_FP_FROM_SELF() ldmia rSELF, {rPC, rFP}
//把以_vreg为索引的寄存器加载在_reg中,_vreg的索引值以rFP为基准
#define GET_VREG(_reg, _vreg) ldr _reg, [rFP, _vreg, lsl #2]
//从“struct InterpSaveState”取出字节码指令地址跟帧地址放入rPC和rFP
#define LOAD_PC_FP_FROM_SELF() ldmia rSELF, {rPC, rFP}
//跳到_reg字节码对应的handler,因为是64字节对齐,所以lsl#6
#define GOTO_OPCODE(_reg) add pc, rIBASE, _reg, lsl #6
5.4.3 ASM Interpreter入口
“dvmMterpStdRun”是解释器入口,不同的解释器有着不同的实现,对于ASM Interpreter,不仅要满足“C to ASM”调用规范,而且承接好DVM虚拟机Context, 其实现如下:
//ASM版解释器入口
dvmMterpStdRun:
#define MTERP_ENTRY1 \
.save {r4-r10,fp,lr}; \
stmfd sp!, {r4-r10,fp,lr}
#define MTERP_ENTRY2 \
.pad #4; \
sub sp, sp, #4
/*引用上文定义宏,保存r4-r10,fp,lr 寄存器*/
.fnstart
MTERP_ENTRY1
MTERP_ENTRY2
/* 保存栈指针 */
str sp, [r0, #offThread_bailPtr]
/* r0里存放当前线程的“struct Thread” */
mov rSELF, r0
/* 从当前线程“struct Thread”的从“struct InterpSaveState”取出字节码地址放入fPC,帧地址放入rFP */
LOAD_PC_FP_FROM_SELF()
/* rIBASE 就是handler数组的地址*/
ldr rIBASE, [rSELF, #offThread_curHandlerTable] @ set rIBASE
#if defined(WITH_JIT)
/*jit功能enable时的处理: */
.LentryInstr:
/* Entry is always a possible trace start */
/*把“struct Thread的“ pJitProfTable;”放入r0,JitProfTable是用来统计热点的阀值表*/
ldr r0, [rSELF, #offThread_pJitProfTable]
//取出当前指令
FETCH_INST()
mov r1, #0 @ prepare the value for the new state
str r1, [rSELF, #offThread_inJitCodeCache] @ back to the interp land
/*如果“ pJitProfTable”为零就表示没有热点检测,自然就没有jit这回事了*/
cmp r0,#0 @ is profiling disabled?
#if !defined(WITH_SELF_VERIFICATION)
/*入口也是种跳转,去检测是否热点,是否需要jit*/
bne common_updateProfile @ profiling is enabled
#else
…
#endif
/*这是jit eable时有效的编译,第一条字节码已经被取出到rINST,把rINST里的指令编码取出来放到寄存器ip里 */
1:
GET_INST_OPCODE(ip)
/*跟据寄存器ip的值,跳转到第一条字节码对应的handler,至此asm interpreter启动了,以后每条字节码都会取出其后的字节码,并跳入对应的handler */
GOTO_OPCODE(ip)
#else
/* start executing the instruction at rPC */
FETCH_INST() @ load rINST from rPC
GET_INST_OPCODE(ip) @ extract opcode from rINST
GOTO_OPCODE(ip) @ jump to next instruction
#endif