DAVLIK JIT编译实现 (上)

本文深入探讨了Dalvik虚拟机的JIT编译实现,包括编译步骤、LIR(低级中间表示)的生成、BasicBlock与MIR(中间寄存器)的创建,以及MIR到LIR的转换。文章强调了JIT编译在性能优化中的关键作用,特别是Dalvik到ART的演进过程,并以ARM架构为例,详细解释了LIR的构造及其转化为机器码的过程。
摘要由CSDN通过智能技术生成

为什么要讲dalvik jit的编译实现呢?首先,为什么是dalvik?虽然dalvik已经在L版本后不再出现在Android系统中,但是ART是从dalvik继承而来,ART中有很多Dalvik的痕迹。在很多方面,ART都继承了dalvik的思想,如果不深入了解dalvik,对ART的了解也是不全面的。另外,dalvik相对与ART,逻辑结构更加简单和清晰,很多方面都可以看作ART的雏形和蓝本,所以,对Dalvik进行深入研究,可以更容易理解ART。第三,虽然dalvik已经退出Android两年时间,但是仍有一些KK版本的机型存在,研究dalvik的代码,也有助于我们提升这些老机型的性能。

基于这些考虑,我深入研究了JIT的编译实现代码。

那么,为什么研究JIT的编译器呢?JVM中,如日中天的是Hotspot,而Hotspot的威力,主要来自将java的字节码翻译成机器码直接执行;而ART强于dalvik的一个很重要的一点,是将dalvik字节码提前翻译成机器码来执行。由此可见,对于虚拟机来说,将虚拟指令翻译为CPU指令,对于性能有着至关重要的影响。作为建筑在安卓系统上的公司,对于这样重要和核心的部件,给予再多的重视也是值得的。

实际上,安卓ART在编译器上的进步也非常大,L版本的Quick编译器,是从dalvik借鉴和改造而来,总体结构与KK的jit很类似;M版本中,引入了optimized compiler,与Quick编译器共存,而且已经开始使用optimized compiler了;在N版本中,已经删除了Quick编译器,而且OptimizedCompiler也经过大幅改进。高通公司,也在ART的编译器中加入了一些自己的优化,因为我们看不到源码,所以无从了解他们都做了些什么。

所以,对于我们来说,了解ART的编译系统,并掌握它们,是有非常积极的意义。

那么,我们先从dalvik开始吧。

编译的步骤

对于jit的入口,我就不详细讲了,有兴趣的,请参考  DALVIK JIT 入口分析

请大家把自己想象成JIT编译器,你被要求,输入一组dalvik 字节码,然后输出一组可以执行的机器码。你会怎么做? 最简单的做法,就是将一条dalvik字节码,翻译成对应的机器码,不就可以了! 如果你这么想,恭喜你!答对来。不错,dalvik的基本思路就是这样,一条dalvik指令,翻译成若干条机器指令。当然,还有很多困难,其中有两大问题:

  • 有大量的if-else, switch-case和for,while循环,这些东西将代码分成了不同的块,块内部的指令与不同块的指令,他们的紧密度是不一样的,不能同等对待;

  • 怎么把dalvik的虚拟寄存器与CPU的实际寄存器对应起来?

解决第一个问题,必须把代码分解成不同的基本块(BasicBlock),每个基本块内只能包括非跳转的代码,基本块的结束指令应该是if-eq/ne/le/ge/gt/lt等,packed/sparse-switch,goto,invoke,return这些指令。而且不同BasicBock直接还有逻辑关系,即token和fallthrougth。比如,一个basicblock的最后一条指令是if-eq,当条件满足时,代码的执行流程走到token块,而条件不满足时,代码的执行流程走向fallthrougth块。

对于第二个问题,你可以先这样思考: 所有的变量,都必须先赋值再使用,如果不赋值,那么变量的值就是未知、不确定的;一个变量可能会被多次赋值,也许会改变它的用途。同样的,在dalvik字节码中,虚拟寄存器(v0~vN),必须先写入值,才能读。而且一个寄存器,可能用作不同的用途,甚至用在不同的类型上。比如一个寄存器v1,他可能被先被当作一个整数来使用;运行到其他代码块中,可能被当作一个object来使用;甚至还会被当作一个float类型。在dalvik字节码中,虚拟寄存器不像变量,它们没有字面意思,可以毫无顾忌的改变他们的含义和用途。

为了解决寄存器在使用上的交叉和共用情况,我们必须引入一个SSA处理,SSA是Static Single Assignment的缩写。它的基本思路是:如果一个变量被赋值,就把它作为一个新的变量来对待 。当被当作一个新的变量后,就不存在混淆的情况。

Dalvik JIT是基于trace热点编译的,这就意味着,编译的对象不是整个method,而是method中的一部分代码,dalvik会在机器码和解释器之间来回跳转。这种情况,就必须要求dalvik的jit代码,遵循与解释器一样的堆栈规则和CPU寄存器的使用规则,那么,对于jit代码,能够使用的寄存器数量就更加有限。

面对这种情况,你想会怎么做呢?我们需要考虑两个原则:

  1. 寄存器只能共用;

  2. 尽量给频繁访问的变量分配一个临时的寄存器来使用。

这个算法的基本原理就是这样:

  1. 做一个寄存器池,哪个变量用到了寄存器,就临时分配给它;

  2. 如果池中的寄存器用完了,就解除一些变量和寄存器的绑定,归还到寄存器池中;

  3. 如果能够明确的知道一个变量不再使用了,就及时解除它与寄存器的绑定,归还到寄存器池中;

  4. 对于一些不是必须寄存器参与的操作,比如读操作,可以直接从内存中读取对应的值;

  5. 进入基本块时,接触所有变量与寄存器的绑定,这就可以释放更多的寄存器来供先不使用,又可避免寄存器与内存数据可能存在的不一致问题

dvmCompileTrace是jit编译的入口函数。这个函数完成编译的过程,大致如下:

  1. dvmCompileTrace 该函数的前半部分,将代码分解成若干个BasicBlock,并建立其他们的关系

  2. dvmInitializeSSAConversion SSA的分析

  3. dvmCompilerMIR2LIR 将MIR转变为LIR。MIR表示一个dalvik指令;LIR表示一个机器码指令,MIR2LIR的过程是将dalvik字节码翻译成机器码的过程;

  4. dvmCompilerAssembleLIR 将LIR转变为真正的字节码

循环的处理会更加复杂,dalvik jit中是单独处理的,我们也将单独说明。

Assemble LIR

首先介绍LIR转变为机器码的过程,是因为这部分比较简单,容易理解。从这里还可以破除对机器码的恐惧。读完本章后,你会觉得机器码一点都不神秘。

我需要说明的是,我只介绍ARM架构,因为这是唯一具有现实意义的架构。

LIR是什么东东?

LIR的定义如下,

typedef struct LIR {
    int offset;
    struct LIR *next;
    struct LIR *prev;
    struct LIR *target;
} LIR;

一个LIR代表一个机器码指令。

  • offset:就是这条指令在整个代码中的偏移。这个很重要,相当于代码的地址,需要相对跳转时,要用它来计算跳转的偏移;

  • target:指向要跳过去的指令。只要跳转指令才会设置target,target的offset和本LIR的offset的差值,就是跳转的偏移。

这是公共的LIR,对于ARM平台,用到的是ArmLIR:

/*
 * Each instance of this struct holds a pseudo or real LIR instruction:
 * - pseudo ones (eg labels and marks) and will be discarded by the assembler.
 * - real ones will be assembled into Thumb instructions.
 *
 * Machine resources are encoded into a 64-bit vector, where the encodings are
 * as following:
 * - [ 0..15]: general purpose registers including PC, SP, and LR
 * - [16..47]: floating-point registers where d0 is expanded to s[01] and s0
 *   starts at bit 16
 * - [48]: IT block
 * - [49]: integer condition code
 * - [50]: floatint-point status word
 */
typedef struct ArmLIR {
    LIR generic;
    ArmOpcode opcode;
    int operands[4];            // [0..3] = [dest, src1, src2, extra]
    struct {
        bool isNop:1;           // LIR is optimized away
        bool insertWrapper:1;   // insert branch to emulate memory accesses
        unsigned int age:4;     // default is 0, set lazily by the optimizer
        unsigned int size:3;    // bytes (2 for thumb, 2/4 for thumb2)
        unsigned int unused:23;
    } flags;
    int aliasInfo;              // For Dalvik register & litpool disambiguation
    u8 useMask;                 // Resource mask for use
    u8 defMask;                 // Resource mask for def
} ArmLIR;

注释说的比较明白,不过即便你英文好,我也要解释下:

  • generic 说明ArmLIR继承自LIR,明明用C++编译,也不知道dalvik用还用这么土的方法

  • opcode: Arm机器指令的操作码,这个是0~kArmLast的范围,用作一个数组的索引,这个数组是生成机器码的关键,后面介绍

  • operands: 操作数,arm指令最多4个操作数,不是所有都用到。其他的我们暂时忽略。

opcode和operands是生成机器码的关键,但是需要借助一个数组:EncodingMap

EncodingMap

这个数组的结构是ArmEncodingMap:

typedef struct ArmEncodingMap {
    u4 skeleton;
    struct {
        ArmEncodingKind kind;
        int end;   /* end for kFmtBitBlt, 1-bit slice end for FP regs */
        int start; /* start for kFmtBitBlt, 4-bit slice end for FP regs */
    } fieldLoc[4];
    ArmOpcode opcode;
    int flags;
    const char* name;
    const char* fmt;
    int size;
} ArmEncodingMap;

这是dalvik jit编译器很讨巧的实现:

  • skeleton: 这是指令的骨架。我们知道,ARM的指令,其结构如下图:

    Arm指令格式Arm指令格式  一种指令的格式,总是固定,因此,把其中固定的位的值填入,把操作数的位置用0空出,就可以得到一个skeleton。我们只要把操作数填入到这个skeleton中,就得到了一个机器码;

  • fieldLoc: 对应ArmLIR中的operands,描述这个操作数:1)应该放在skeleton的什么位;2)占用多少位;3)操作数是否需要除4(地址要4字节对齐,只需要存储4的倍数即可);4)是否需要shift,及shift多少;5)其他信息。总之,说明操作数应该如何被填到skeleton中。你可以看ArmEncodingKind的定义了解具体有多少种填充方法

  • size: 指令的长度,只能是2或者4。因为Thumb2指令格式只有这两种长度其他的域可以忽略。

EncodingMap数组以ArmLIR的opcode为索引,拿到一个ArmEncodingMap数据就可以填充了。

dvmCompilerAssembleLIR

核心的实现代码是assembleInstructions。我们首先看看除了跳转代码或者基于PC的代码之外的转换实现

注意:下面的代码都摘自函数assembleInstructions. 完整的代码可以参见源码

普通指令的生成

代码不算长,第一步是得到ArmEncodingMap;然后,先得到skeleton到bits;第三,循环处理每个操作数,把他们填到bits中;最后,将bits拷贝到生成代码的内存中。


  
  
  1. ArmEncodingMap *encoder = &EncodingMap [lir - >opcode ] ;
  2. u4 bits = encoder - >skeleton ; //获取指令骨架
  3. int i ;
  4. for (i = 0 ; i < 4 ; i ++ ) { //循环操作数,填充bits,得到最后编码的指令
  5. u4 operand ;
  6. u4 value ;
  7. operand = lir - >operands [i ] ;
  8. switch (encoder - >fieldLoc [i ]. kind ) {
  9. case kFmtUnused :
  10. break ;
  11. case kFmtFPImm :
  12. value = ( (operand & 0xF0 ) >> 4 ) << encoder - >fieldLoc [i ]. end ;
  13. value | = (operand & 0x0F ) << encoder - >fieldLoc [i ]. start ;
  14. bits | = value ;
  15. break ;
  16. case kFmtBrOffset :
  17. value = ( (operand & 0x80000 ) >> 19 ) << 26 ;
  18. value | = ( (operand & 0x40000 ) >> 18 ) << 11 ;
  19. value | = ( (operand & 0x20000 ) >> 17 ) << 13 ;
  20. value | = ( (operand & 0x1f800 ) >> 11 ) << 16 ;
  21. value | = (operand & 0x007ff ) ;
  22. bits | = value ;
  23. break ;
  24. case kFmtShift5 :
  25. value = ( (operand & 0x1c ) >> 2 ) << 12 ;
  26. value | = (operand & 0x03 ) << 6 ;
  27. bits | = value ;
  28. break ;
  29. case kFmtShift :
  30. value = ( (operand & 0x70 ) >> 4 ) << 12 ;
  31. value | = (operand & 0x0f ) << 4 ;
  32. bits | = value ;
  33. break ;
  34. case kFmtBWidth :
  35. value = operand - 1 ;
  36. bits | = value ;
  37. break ;
  38. case kFmtLsb :
  39. value = ( (operand & 0x1c ) >> 2 ) << 12 ;
  40. value | = (operand & 0x03 ) << 6 ;
  41. bits | = value ;
  42. break ;
  43. case kFmtImm6 :
  44. value = ( (operand & 0x20 ) >> 5 ) << 9 ;
  45. value | = (operand & 0x1f ) << 3 ;
  46. bits | = value ;
  47. break ;
  48. case kFmtBitBlt :
  49. value = (operand << encoder - >fieldLoc [i ]. start ) &
  50. ( ( 1 << (encoder - >fieldLoc [i ]. end + 1 ) ) - 1 ) ;
  51. bits | = value ;
  52. break ;
  53. case kFmtDfp : {
  54. assert (DOUBLEREG (operand ) ) ;
  55. assert ( (operand & 0x1 ) == 0 ) ;
  56. int regName = (operand & FP_REG_MASK ) >> 1 ;
  57. /* Snag the 1-bit slice and position it */
  58. value = ( (regName & 0x10 ) >> 4 ) <<
  59. encoder - >fieldLoc [i ]. end ;
  60. /* Extract and position the 4-bit slice */
  61. value | = (regName & 0x0f ) <<
  62. encoder - >fieldLoc [i ]. start ;
  63. bits | = value ;
  64. break ;
  65. }
  66. case kFmtSfp :
  67. assert (SINGLEREG (operand ) ) ;
  68. /* Snag the 1-bit slice and position it */
  69. value = (operand & 0x1 ) <<
  70. encoder - >fieldLoc [i ]. end ;
  71. /* Extract and position the 4-bit slice */
  72. value | = ( (operand & 0x1e ) >> 1 ) <<
  73. encoder - >fieldLoc [i ]. start ;
  74. bits | = value ;
  75. break ;
  76. case kFmtImm12 :
  77. case kFmtModImm :
  78. value = ( (operand & 0x800 ) >> 11 ) << 26 ;
  79. value | = ( (operand & 0x700 ) >> 8 ) << 12 ;
  80. value | = operand & 0x0ff ;
  81. bits | = value ;
  82. break ;
  83. case kFmtImm16 :
  84. value = ( (operand & 0x0800 ) >> 11 ) << 26 ;
  85. value | = ( (operand & 0xf000 ) >> 12 ) << 16 ;
  86. value | = ( (operand & 0x0700 ) >> 8 ) << 12 ;
  87. value | = operand & 0x0ff ;
  88. bits | = value ;
  89. break ;
  90. default :
  91. assert ( 0 ) ;
  92. }
  93. }
  94. //指令的长度不一样,所以,要拷贝的指令数据也不一样。
  95. if (encoder - >size == 2 ) {
  96. *bufferAddr ++ = (bits >> 16 ) & 0xffff ;
  97. }
  98. *bufferAddr ++ = bits & 0xffff ;
  99. }
  100. return kSuccess ;

本人很佩服这个代码的设计者,这是大师的手笔!

跳转和PC相关的指令

对于跳转指令和基于PC的ldr指令,他们都需要计算相对偏移,这个时候,就用要到LIR的target和offset两个成员了。代码很长,我只摘取了一部分。大家需要注意,这部分代码是对指令做预处理,最终还是上面章节提到的方法来生成指令。


  
  
  1. if (lir - >opcode == kThumbLdrPcRel ||
  2. lir - >opcode == kThumb2LdrPcRel12 ||
  3. lir - >opcode == kThumbAddPcRel ||
  4. ( (lir - >opcode == kThumb2Vldrd ) && (lir - >operands [ 1 ] == r15pc ) ) ||
  5. ( (lir - >opcode == kThumb2Vldrs ) && (lir - >operands [ 1 ] == r15pc ) ) ) {
  6. ArmLIR *lirTarget = (ArmLIR * ) lir - >generic. target ;
  7. intptr_t pc = (lir - >generic. offset + 4 ) & ~ 3 ;
  8. intptr_t target = lirTarget - >generic. offset ;
  9. int delta = target - pc ;
  10. if (delta & 0x3 ) {
  11. ALOGE ( "PC-rel distance is not multiples of 4: %d", delta ) ;
  12. dvmCompilerAbort (cUnit ) ;
  13. }
  14. if ( (lir - >opcode == kThumb2LdrPcRel12 ) && (delta > 4091 ) ) {
  15. if (cUnit - >printMe ) {
  16. ALOGD ( "kThumb2LdrPcRel12@%x: delta=%d", lir - >generic. offset,
  17. delta ) ;
  18. dvmCompilerCodegenDump (cUnit ) ;
  19. }
  20. return kRetryHalve ;
  21. } else if (delta > 1020 ) {
  22. if (cUnit - >printMe ) {
  23. ALOGD ( "kThumbLdrPcRel@%x: delta=%d", lir - >generic. offset,
  24. delta ) ;
  25. dvmCompilerCodegenDump (cUnit ) ;
  26. }
  27. return kRetryHalve ;
  28. }
  29. if ( (lir - >opcode == kThumb2Vldrs ) || (lir - >opcode == kThumb2Vldrd ) ) {
  30. lir - >operands [ 2 ] = delta >> 2 ;
  31. } else {
  32. lir - >operands [ 1 ] = (lir - >opcode == kThumb2LdrPcRel12 ) ?
  33. delta : delta >> 2 ;
  34. }
  35. }
  36. .....

最终,将计算出的delta偏移,放入到operands中。因为不同跳转指令预留的长度不一样,所以必须做长度处理。如果偏移过大,就必须再次处理源码使之满足条件。

预处理指令

还有一些LIR对象,不是映射到具体的机器码,而是用作一些特殊的用途,比如做对齐代码。比如下面的代码


  
  
  1. if (lir - >opcode < 0 ) {
  2. if ( (lir - >opcode == kArmPseudoPseudoAlign4 ) &&
  3. /* 1 means padding is needed */
  4. (lir - >operands [ 0 ] == 1 ) ) {
  5. *bufferAddr ++ = PADDING_MOV_R5_R5 ;
  6. }
  7. continue ;
  8. }

LIR opcode小于0的都属于预处理指令,比如kArmPseudoPseudoAlign4,表示下面的代码一定要在4字节对齐处。所以,它为指令加入一个mov r5, r5的空指令,来实现对齐。

BasicBlock及MIR的生成

BB可以划分为若干类型,其中,必要重要的是

  • kEntryBlock: 表示jit入口的BB,没有包含任何dalvik字节码,用于生成从解释到jit的入口代码;

  • kDalvikByteCode: 表示dalvik字节码的BB,是由dalvik代码生成的;

  • kExitBlock: jit退出的BB,作用同kEntryBlock

  • kExceptionHandling: 这是用来做异常抛出的BB。当某段dalvik代码产生了异常(这段代码本身或者它调用的其他代码),但是这段的代码又不再try-catch块中,就会产生这样一个BB,用于将异常抛到更高的调用层来处理;

  • kPCReconstruction: 从jit代码退出到解释器的BB。这个BB放在最后。jit代码中,会有return,或者一些异常,比如invoke irtual时this为null,或者check cast不成功等,需要跳转到解释器或者转入到异常处理。这时,就会添加一个异常跳出点,最后汇集到一起处理

  • kChainingCellHot/Normal/Predicated/Singleton/BackwardBranch: 当代码执行流程要跳出jit时,跳出的目标可能是在解释器中,也可能是另外一块jit代码。这时候就需要一段小的stub代码来完成任务,这个任务就放在kChainingCellXXX的BB中。Hot和Normal用在goto跳转中,Hot表示目标是另外一个jit代码,而Noraml则进入了解释模式。Predicated用在invoke中,如果被invoke的method还没有resolved,就需要先resolve;Singletone用在普通已经resolved的函数中。对于native函数,则不需要添加任何的chaining cell; BackwardBranch用于循环,跳回到已经执行过的代码中。

MIR是dalvik bytecode转换后的对象。MIR的生成是和BB一起生成的。BB包含了若干个MIR(只是kDalvikByteCode类型)。

BB之间,用token和fallthrough关系关联起来。

BB的生成分成以下步骤:

  • 生成Entry BB

  • 遍历代码,生成若干个kDalvikByteCode的BB

  • 遍历所有的BB,确定每个BB的token和fallthrough关系

  • 最后增加kPCReconstruction和kExceptionHandling来结束整个BB链条

具体的实现代码在dvmCompileTrace中。具体代码不列举了,我提几个重要的点:

  • parseInsn: 解析dalvik指令的函数,需要先了解dalvik bytecode的格式

  • MIR的格式

    
         
         
    1. typedef struct MIR {
    2. DecodedInstruction dalvikInsn ;
    3. unsigned int width ;
    4. unsigned int offset ;
    5. struct MIR *prev ;
    6. struct MIR *next ;
    7. struct SSARepresentation *ssaRep ;
    8. int OptimizationFlags ;
    9. int seqNum ;
    10. union {
    11. // Used by the inlined insn from the callee to find the mother method
    12. const Method *calleeMethod ;
    13. // Used by the inlined invoke to find the class and method pointers
    14. CallsiteInfo *callsiteInfo ;
    15. } meta ;
    16. } MIR ;
    • dalvikInsn 是解析后的指令,内容包括dalvik bytecode opcode和参数

    • width: dalvik指令的长度

    • offset: 在method code中的偏移

    • ssaRep: SSA的信息,后面我们会详细说明

    • meta:

      • callsiteInfo: invoke是保存的必要信息

      • calleeMethod: 对一些method inline的信息

  • findBlockBoundary:这个是用来寻找BB之间关系的函数,用于计算一个BB的token BB是哪个。

MIR2LIR

函数dvmCompilerMIR2LIR是将MIR转为LIR的主体函数。MIR2LIR是编译中最重要的部分,MIR代表的是dalvik bytecode,而LIR代表的是CPU的机器码,这一部分是完成编译的关键。

我们知道,BB中有好几种类型,比如kEnterEntry,kExitEntry等,这些没有对应的dalvik字节码,所以,会直接生成LIR,进一步生成机器码。而kDalvikByteCode型的BB,则需要将里面的每个MIR生成对应的LIR。

为了让大家对MIR2LIR有个直观的概念,我们可以先看看,一个简单的dalvik指令是怎样被转换为CPU指令的。

Move指令的转换

dvmCompilerMIR2LIR按照指令的格式进行了分组,把同一指令的格式分在一起。比如kFmt10x, 用对应的函数handleFmt10x,这个函数处理了return/void这样的dalvik指令。

move指令的格式kFmt12x,我们在handleFmt12x函数中可以找到,以最简单的move/object指令为例,他的实现代码如下:


  
  
  1. static bool handleFmt12x (CompilationUnit *cUnit, MIR *mir )
  2. {
  3. Opcode opcode = mir - >dalvikInsn. opcode ;
  4. RegLocation rlDest ;
  5. RegLocation rlSrc ;
  6. RegLocation rlResult ;
  7. ....
  8. if (mir - >ssaRep - >numUses == 2 )
  9. rlSrc = dvmCompilerGetSrcWide (cUnit, mir, 0, 1 ) ;
  10. else
  11. rlSrc = dvmCompilerGetSrc (cUnit, mir, 0 ) ;
  12. if (mir - >ssaRep - >numDefs == 2 )
  13. rlDest = dvmCompilerGetDestWide (cUnit, mir, 0, 1 ) ;
  14. else
  15. rlDest = dvmCompilerGetDest (cUnit, mir, 0 ) ;
  16.  
  17. switch (opcode ) {
  18. case OP_MOVE :
  19. case OP_MOVE_OBJECT :
  20. storeValue (cUnit, rlDest, rlSrc ) ;
  21. break ;
  22. }
  23. .....

rlDest表示的目标寄存器,rlSrc表示的是源寄存器。RegLocation这个对象,表示的是一个寄存器,包含了CPU寄存器的信息,也包含了dalvik寄存器的信息,至于是怎么分配CPU寄存器给dalvik寄存器,我们会在下篇中详细描述。

storeValue的功能是非常简单的,即实现寄存器间数据拷贝。但是实现起来情况就非常的复杂,因为寄存器的类型可以有很多种:

  1. kLocPhysReg: 已经分配了CPU寄存器,并且寄存器的值被更新;

  2. kLocDalvikFrame: 该值还是存储在dalvik的Frame上(即线程的堆栈上),没有分配对应的CPU寄存器,也没有载入到寄存中;

  3. kLocRetval: 这是一个放在Thread中内存地址,表示返回值的地址,return和move-return指令都要用到

  4. kLocSpill: 没有用到,忽略。

那么,src和dest一共有6中组合(src和dest不能同时为kLocRetVal,故是2X3种组合),这6种组合产生的代码都不一样。

其中,kLocPhysRes表示真实可用的寄存器,kLocDalvikFrame和kLocRetval都是必须访问内存的。

下面,在注释中详细说明每种情况的用法:


  
  
  1. static void storeValue (CompilationUnit *cUnit, RegLocation rlDest,
  2. RegLocation rlSrc )
  3. {
  4. LIR *defStart ;
  5. LIR *defEnd ;
  6. assert ( !rlDest. wide ) ;
  7. assert ( !rlSrc. wide ) ;
  8. dvmCompilerKillNullCheckedLoc (cUnit, rlDest ) ;
  9. rlSrc = dvmCompilerUpdateLoc (cUnit, rlSrc ) ;
  10. rlDest = dvmCompilerUpdateLoc (cUnit, rlDest ) ;
  11. if (rlSrc. location == kLocPhysReg ) {
  12. if (dvmCompilerIsLive (cUnit, rlSrc. lowReg ) ||
  13. (rlDest. location == kLocPhysReg ) ) {
  14. // Src is live or Dest has assigned reg.
  15. rlDest = dvmCompilerEvalLoc (cUnit, rlDest, kAnyReg, false ) ;
  16. //这种情况,src和dst都是寄存器,生成 mov rDst, rSrc的指令
  17. genRegCopy (cUnit, rlDest. lowReg, rlSrc. lowReg ) ;
  18. } else {
  19. // Just re-assign the registers. Dest gets Src's regs
  20. //src分配了寄存器,但是dest没有,于是把src寄存器当作dest寄存器使用,不产生机器代码
  21. rlDest. lowReg = rlSrc. lowReg ;
  22. dvmCompilerClobber (cUnit, rlSrc. lowReg ) ;
  23. }
  24. } else {
  25. // Load Src either into promoted Dest or temps allocated for Dest
  26. //src没有分配寄存器,但是dest有,于是,直接从src的内存中读取值到dest中
  27. rlDest = dvmCompilerEvalLoc (cUnit, rlDest, kAnyReg, false ) ;
  28. loadValueDirect (cUnit, rlSrc, rlDest. lowReg ) ;
  29. }
  30.  
  31. // Dest is now live and dirty (until/if we flush it to home location)
  32. //把dest寄存器(已经分配了CPU寄存器)设置类live和dirty。live表示当前的dalvik寄存器与
  33. //CPU寄存器处于绑定和被使用状态;dirty表示寄存器的值要新内存的值,在必要时,需要将新值写入内存
  34. dvmCompilerMarkLive (cUnit, rlDest. lowReg, rlDest. sRegLow ) ;
  35. dvmCompilerMarkDirty (cUnit, rlDest. lowReg ) ;
  36.  
  37.  
  38. if (rlDest. location == kLocRetval ) {
  39. //如果dest是返回值,需要从Thread::interpSave.retval中读取值并写如到指定寄存器中
  40. storeBaseDisp (cUnit, rSELF, offsetof (Thread, interpSave. retval ),
  41. rlDest. lowReg, kWord ) ;
  42. dvmCompilerClobber (cUnit, rlDest. lowReg ) ;
  43. } else {
  44. //都没有寄存器了,需要进行内存对内存的操作
  45. dvmCompilerResetDefLoc (cUnit, rlDest ) ;
  46. if (dvmCompilerLiveOut (cUnit, rlDest. sRegLow ) ) {
  47. defStart = (LIR * )cUnit - >lastLIRInsn ;
  48. int vReg = dvmCompilerS2VReg (cUnit, rlDest. sRegLow ) ;
  49. storeBaseDisp (cUnit, rFP, vReg << 2, rlDest. lowReg, kWord ) ;
  50. dvmCompilerMarkClean (cUnit, rlDest. lowReg ) ;
  51. defEnd = (LIR * )cUnit - >lastLIRInsn ;
  52. dvmCompilerMarkDef (cUnit, rlDest, defStart, defEnd ) ;
  53. }
  54. }
  55. }

我们看genRegCopy函数的实现,这个实现了代码的生成


  
  
  1. static ArmLIR * genRegCopyNoInsert (CompilationUnit *cUnit, int rDest, int rSrc )
  2. {
  3. ArmLIR * res ;
  4. ArmOpcode opcode ;
  5. res = (ArmLIR * ) dvmCompilerNew ( sizeof (ArmLIR ), true ) ;
  6. if (LOWREG (rDest ) && LOWREG (rSrc ) )
  7. opcode = kThumbMovRR ;
  8. else if ( !LOWREG (rDest ) && !LOWREG (rSrc ) )
  9. opcode = kThumbMovRR_H2H ;
  10. else if (LOWREG (rDest ) )
  11. opcode = kThumbMovRR_H2L ;
  12. else
  13. opcode = kThumbMovRR_L2H ;
  14.  
  15. res - >operands [ 0 ] = rDest ;
  16. res - >operands [ 1 ] = rSrc ;
  17. res - >opcode = opcode ;
  18. setupResourceMasks (res ) ;
  19. if (rDest == rSrc ) {
  20. res - >flags. isNop = true ;
  21. }
  22. return res ;
  23. }
  24.  
  25. static ArmLIR * genRegCopy (CompilationUnit *cUnit, int rDest, int rSrc )
  26. {
  27. ArmLIR *res = genRegCopyNoInsert (cUnit, rDest, rSrc ) ;
  28. dvmCompilerAppendLIR (cUnit, (LIR * )res ) ;
  29. return res ;
  30. }

genRegCopyNoInsert 函数,首先生成一个ArmLIR对象,然后设置它的opcode, 最后填入操作数。

需要注意的是,虽然都是生成mov rd, rs这样的语句,但是汇编语言与实际机器码是1:N的关系,一条汇编助记符可以根据操作数的不一样,生成不同的机器码。

其他的dalvik指令,他们的框架结构都是类似的,只有具体的代码结构是不一样的。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值