作者 crosskernel@gmail.com
第五章 Interpreter与JIT 图书版(5.1-5.2)
解释器是影响虚拟机性能关键因素,最初的Dalvik只有C语言版本的解释器,到汇编实现的ASM解释器。再到进一步将JIT做进解释器。Android不停的提升其Dalvik解释器效率。
5.1 解释器编译结构
对于不同的处理器和指令集,Android有着与之对应的高度优化的Interpreter和JIT实现。为了支持这些不同的架构处理器和指令集,Android使用了灵活的编译结构:
在dalvik/vm/Android.mk里包含 ReconfigureDvm.mk
在ReconfigureDvm.mk里包含Dvm.mk
dalvik/vm/Dvm.mk是悬着指令集的关键,这里跟据环境变量dvm_arch_variant选择指令集的对应实现
对于集成NEON的arm处理器,对应的两个最关键的实现文件是InterpAsm-armv7-a-neon.S和InterpC-armv7-a-neon.cpp:
mterp/out/InterpC-$(dvm_arch_variant).cpp.arm \
mterp/out/InterpAsm-$(dvm_arch_variant).S
makefile里的包含的源文件是InterpC-armv7-a-neon.cpp.arm,实际上没有这个文件,在build/core/binary.mk里面对cpp_arm_sources有着特殊的编译处理。InterpC-armv7-a-neon.cpp.arm对应的就是InterpC-armv7-a-neon.cpp
5.2 dalvik寄存器编译模型
Dalvik是基于寄存器的虚拟机,其寄存器编译模型是以函数为中心的,包括的函数内部寄存器、函数间调用时参数寄存器与结果寄存器的分配与布局。
5.2.1 callee寄存器分配
Dalvik的callee函数寄存器分配规则如下:
对于没有产生调用的函数:
设函数定义的局部变量为n则寄存器0 --- 寄存器(n-1)被依次分配给局部变量
寄存器n被分配给this
设函数参数为m,则寄存器(n+1) --- 寄存器(n+m)被依次分配给局部变量
如果函数运算过程中使用了中间变量,则为中间变量分配寄存器,寄存器号插入在this寄存器的后面
以如下函数为例
public int senix_register(int par1,int par2,int par3) {
int local_var1=1;
int local_var2=2;
local_var1=local_var1+local_var2+par1+par2+par2;
return local_var1;
}
使用“./out/host/linux-x86/bin/dexdump –d .odex”将字节码输出:
name : 'senix_register' //函数名
type : '(III)I'//三个int参数,返回值为也为int
access : 0x0001 (PUBLIC) //属性
code -
registers : 7 //共用了7个寄存器
ins : 4 //输入变量
outs : 0//没有调用其他函数outs为0
insns size : 8 16-bit code units
064efc: |[064efc] com.android.launcher2.LauncherApplication.senix_register:(III)I
064f0c: 1210 |0000: const/4 v0, #int 1 // #1 /*local_var1=1;*/
064f0e: 1221 |0001: const/4 v1, #int 2 // #2 /*local_var2=2;*/
064f10: d802 0403 |0002: add-int/lit8 v2, v4, #int 3 // #03 /*“local_var1+local_var2”被优化掉成操作数“#int 3”,分配一个寄存器v2放置中间结果,这里实际效果是“v2= local_var1+local_var2+par1”*/
064f14: b052 |0004: add-int/2addr v2, v5 /* v2= local_var1+local_var2+par1+par2 */
064f16: 9000 0205 |0005: add-int v0, v2, v5 /* v2= local_var1+local_var2+par1+par2+par1,并且把结果放到寄存器v0中 */
064f1a: 0f00 |0007: return v0 /*以v0返回结果*/
catches : (none)
positions :
0x0000 line=78
0x0001 line=79
0x0002 line=80
0x0007 line=81
locals :
0x0001 - 0x0008 reg=0 local_var1 I //寄存器0分配给local_var1
0x0002 - 0x0008 reg=1 local_var2 I //寄存器1分配给local_var2
0x0000 - 0x0008 reg=3 this Lcom/android/launcher2/LauncherApplication; /*寄存器3存放类对象this,该函数是在class LauncherApplication里添加的*/
0x0000 - 0x0008 reg=4 par1 I //寄存器4分配给输入参数par4
0x0000 - 0x0008 reg=5 par2 I //寄存器5分配给输入参数par5
0x0000 - 0x0008 reg=6 par3 I //寄存器6分配给输入参数par5
5.2.2 caller寄存器分配
Caller函数的寄存器分配规则,首先满足callee函数的寄存器分配规则,但是在这规则之外,在产生调用时,要在callee函数帧的高地址放入调用参数。这些调用参数被callee当成ins。
以如下函数分析:
public int senix_register(int par1,int par2,int par3) {
int local_var1=1;
int local_var2=2;
local_var1=local_var1+local_var2+par1+par2+par2;
return local_var1;
}
public int senix_register_caller(int par1,int par2,int par3) {
int local_var1=1;
int local_var2=senix_register(4,5,6);
local_var1=local_var1+local_var2+par1+par2+par2;
return local_var1;
}
“senix_register_caller(int par1,int par2,int par3)”dump的结果如下:
name : 'senix_register_caller'
type : '(III)I'
access : 0x0001 (PUBLIC)
code -
registers : 9 //共用了9个寄存器
ins : 4 //3个输入参数+this,this不占寄存器
outs : 4//3个输出参数+this,this不占寄存器
insns size : 15 16-bit code units
064f28: |[064f28] com.android.launcher2.LauncherApplication.senix_register_caller:(III)I
064f38: 1210 |0000: const/4 v0, #int 1 // #1
064f3a: 1242 |0001: const/4 v2, #int 4 // #4 /*给参数4分配寄存器2*/
064f3c: 1253 |0002: const/4 v3, #int 5 // #5 /*给参数4分配寄存器3*/
064f3e: 1264 |0003: const/4 v4, #int 6 // #6 /*给参数6分配寄存器2*/
064f40: f840 7400 2543 |0004: +invoke-virtual-quick {v5, v2, v3, v4}, [0074] // vtable #0074/* 调用发生了,3个参数加this */
064f46: 0a01 |0007: move-result v1 /* 把返回值放到v1*/
/*下面为计算操作,参见上一节分析 */
064f48: 9002 0001 |0008: add-int v2, v0, v1
064f4c: b062 |000a: add-int/2addr v2, v6
064f4e: b072 |000b: add-int/2addr v2, v7
064f50: 9000 0207 |000c: add-int v0, v2, v7
064f54: 0f00 |000e: return v0
catches : (none)
positions :
0x0000 line=88
0x0001 line=89
0x0008 line=90
0x000e line=91
locals :
0x0001 - 0x000f reg=0 local_var1 I //寄存器0分配给local_var1
0x0008 - 0x000f reg=1 local_var2 I //寄存器1分配给local_var2
0x0000 - 0x000f reg=5 this Lcom/android/launcher2/LauncherApplication; /*//寄存器5分配给this*/
0x0000 - 0x000f reg=6 par1 I //寄存器6分配给par1
0x0000 - 0x000f reg=7 par2 I //寄存器7分配给par2
0x0000 - 0x000f reg=8 par3 I//寄存器8分配给par3
寄存器v2 v3 v4分配给三个调用参数。到了这里还是不能看清这些outs到底放在哪里。要解决这个问题需分析解释器如何处理“invokeMethod_XXX”指令时是如何安排参数的。
5.2.3 outs的处理
在解释器遇到dalvik函数调用指令的处理器如下:(方便起见,用C解释器分析)
GOTO_TARGET(invokeMethod, bool methodCallRange, const Method* _methodToCall,
u2 count, u2 regs)
{
u4* outs;
int i;
/*
* Copy args. This may corrupt vsrc1/vdst.
*/
/*首先正如注释所说,拷贝参数,这里分为两种情况,如果一个函数参数个数比较多,则变量methodCallRange成真。对于每个参数,都用GET_REGISTER从当前函数帧里取值,再复制给参数在callee函数中的寄存器。*/
if (methodCallRange) {
// could use memcpy or a "Duff's device"; most functions have
// so few args it won't matter much
//在当前栈上给参数分配空间,vsrc1中就是参数个数
outs = OUTS_FROM_FP(fp, vsrc1);
for (i = 0; i < vsrc1; i++)
outs[i] = GET_REGISTER(vdst+i);
} else {
u4 count = vsrc1 >> 4;
//在当前栈上给参数分配空间,count就是参数个数
outs = OUTS_FROM_FP(fp, count);
// This version executes fewer instructions but is larger
// overall. Seems to be a teensy bit faster.
assert((vdst >> 16) == 0); // 16 bits -or- high 16 bits clear
switch (count) {
case 5:
outs[4] = GET_REGISTER(vsrc1 & 0x0f);
case 4:
outs[3] = GET_REGISTER(vdst >> 12);
case 3:
outs[2] = GET_REGISTER((vdst & 0x0f00) >> 8);
case 2:
outs[1] = GET_REGISTER((vdst & 0x00f0) >> 4);
case 1:
outs[0] = GET_REGISTER(vdst & 0x0f);
default:
;
}
}
}
//到了这里,参数分配并初始化完毕。但是仔细分析参数空间的分配OUTS_FROM_FP:
#define OUTS_FROM_FP(_fp, _argCount) \
((u4*) ((u1*)SAVEAREA_FROM_FP(_fp) - sizeof(u4) * (_argCount)))
#define SAVEAREA_FROM_FP(_fp) ((StackSaveArea*)(_fp) -1)
我们发现,不是从当前帧指针_fp往下分配,而是越过StackSaveArea在往下分出空间。这样做的原因是_fp指针是用来索引寄存器用,而一个函数帧里在_fp往下还存在着一个VM-specific internal goop-- struct StackSaveArea。
由此可以可以得出结论
outs其实就是从caller角度看的调用参数,就是callee的ins
outs到ins是拷贝,outs分配的那些寄存器在caller还可做为他用