当前位置: 代码迷 >> Android >> 卷2 Dalvik与Android源码分析 第五章 Interpreter与JIT 5.1 解释器编译结构、5.2dalvik寄存器编译模型 图书版试读-请勿转发
  详细解决方案

卷2 Dalvik与Android源码分析 第五章 Interpreter与JIT 5.1 解释器编译结构、5.2dalvik寄存器编译模型 图书版试读-请勿转发

热度:35   发布时间:2016-04-28 06:36:30.0
卷二 Dalvik与Android源码分析 第五章 Interpreter与JIT 5.1 解释器编译结构、5.2dalvik寄存器编译模型 图书版试读--请勿转发

作者 [email protected]


第五章 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还可做为他用




  相关解决方案