DAVLIK JIT编译实现 (上)

Dalvik的JIT编译流程 & ART的dex2oat流程 一.Dalvik的JIT编译流程 Dalvik JIT是一个tracing JIT(也叫trace-based JIT),其中间表示分为两种,MIR与LIR。MIR与LIR节点各自形成链表,分别被组织在BasicBlock与CompilationUnit中。 编译流程是:  0、创建CompilationUnit对象(一个trace对应一个CompilationUnit)来存放一次编译中需要 阅读详情

为什么要讲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指令,他们的框架结构都是类似的,只有具体的代码结构是不一样的。


JIT(Just-In-Time Compilation,即时编译)技术 JIT(即时编译)技术是一种在程序运行时动态生成机器码的编译方法,结合了解释执行的灵活性和静态编译的高效性。其核心原理包括动态编译、利用运行时信息进行优化以及缓存重用。JIT的工作流程分为解释执行、热点检测与触发编译、编译与优化、执行与缓存管理四个阶段。JIT广泛应用于Java虚拟机、JavaScript引擎、动态语言运行时和高性能计算等领域,具有性能接近原生代码、跨平台兼容性和动态适应能力等优势,但也面临启动延迟、内存占用和复杂保护对抗等挑战。未来,JIT技术将朝着智能化优化、跨平台支持和与AOT结合的方 阅读详情

相关推荐

掌握Java和Android虚拟机

​我们知道的虚拟机有很多,运行Java的JVM虚拟机,运行Android程序的Davlik和Art虚拟机,运行C#的CLR虚拟机,那么什么是虚拟机呢,虚拟机的作用又是什么呢?运行JavaScript的v8引擎或者运行Python的引擎是否也是虚拟机呢?带着这几个问题,我们开始对虚拟机的学习。 虽然现在很多人都认为运行JavaScript的V8或运行Python的VirtualEnv,都不是虚拟机,而是解释器,主要原因是因为V8或者VirtualEnv不仅仅能执行字节码文件,还能将源文件编译成字节码文件,而传

子者不语的专栏 2185

Dalvik VM的JIT编译器的资料堆积(dumping...work in progress)

Android的Dalvik VM 在去年下半年新增了JIT编译器,应该能提高Android上的Java程序的性能。暂时只支持armv5te、armv5te-vfp和armv7-a这三种CPU。去年11月在android-platform 上的一帖提到: Nov 17 2009, 6:53 am From: Bill Buzbee Date: Mon, 16 Nov 2009 14:53

fishmai的专栏 1229

Python 爬虫高级面试真题_2

摘要:本文总结了移动安全领域12个常见问题的技术原理与解决方案。主要内容包括:Frida动态分析工具的实现原理、iOS应用保护机制及突破方法、inline hook技术原理、Davlilk与ART虚拟机区别、Frida反调试方法、Dex文件方法数限制原理、安卓加固与脱壳技术、瑞数动态Cookie反爬处理、绕过Frida反调试技巧、安卓内核编译修改traceid、APK反编译流程,以及指令抽取脱壳思路。这些问题涵盖了移动应用安全分析、逆向工程和系统底层技术等多个方面,为安全研究人员提供了实用的技术参考。

CXY00000的博客 1659

深入浅出 JIT 编译器

深入浅出 JIT 编译器 https://developer.ibm.com/zh/articles/j-lo-just-in-time/ 理解 Java 底层机制并且在 Java 性能优化领域打开更广的视野 收藏赞 李伟军, 宋翰瀛, 杨翔宇 发布: 2016-05-31 JIT 简介 JIT 是 just in time 的缩写, 也就是即时编译编译器。使用即时编译器技术,能够加速 Java 程序的执行速度。下面,就对该编译器技术做个简单的讲解。 首先,我们大家都知道,通常通过 jav..

cxu123321的博客 473

Android JIT编译器深度剖析与实践

本文还有配套的精品资源,点击获取 简介:Android平台上的JIT编译器通过在程序运行时将字节码转换为机器码来提升应用程序性能。该编译器利用各种优化策略,如死代码消除和循环展开,同时在有限的内存资源中进行智能的代码管理。通过“热方法探测”技术来确定代码编译的优先级,以及与垃圾回收机制协作以保障应用的稳定运行。本源代码包可能包含了Android JIT编译器的C语言实现细节...

weixin_42400643的博客 1278

Oracle VM VirtualBox虚拟机网络配置与静态IP设置实战

本文详细讲解了Oracle VM VirtualBox虚拟机的网络配置,特别是如何设置静态IP以实现稳定访问。文章对比了NAT、桥接、仅主机等网络模式,并提供了从VirtualBox管理界面到Linux系统内部的完整配置步骤,帮助用户解决IP变动问题,构建可靠的本地开发测试环境。

weixin_28365523的博客 511

热修复设计之热修复原理(三)

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 本篇文章将先从热修复原理来介绍热修复设计: Android 热修复 在Android的热修复中主要用来替换类,资源,so的过程; Java 虚拟机 栈架构指令集的主要缺点是执行速度相对来说稍微慢一些;基于堆栈的机器需要更多指令,(内存) And...

AndroidAlvin的博客 184

Arduino程序设计(十)8×8 共阴极LED点阵显示(MAX7219)

本文主要介绍8×8 LED点阵显示实验,分别是:1、介绍MAX7219点阵模块;2、点阵显示部分图形;3、点阵显示数字 0~9 及 26 个英文字母。1、通过上述两个点阵实验,了解了8*8点阵显示的基本原理;2、通过SPI通信控制点阵显示,学会了Arduino用SPI通信方式控制外设。

Alice的博客 1万+

JIT AOT Davlik ART

一、 JIT 是just In time compile的缩写,就是“即时编译技术”,用于Davlik虚拟机 AOT "Ahead of Time"的缩写,提前编译,运行前编译,一般在安装时就将字节码翻译成机器码 dalvik这个大家肯定很熟悉 Dalvik是Google公司设计用于Android平台的Java虚拟机 ,使用dex,odex(优化后) Art是(Android runt...

wangwei6227的博客 355

利用netmq在相机应用于unity之间传输图像数据

Unity工业相机接入方案解析 本文探讨了在Unity中接入不同品牌工业相机的技术方案。研究发现: Basler相机可直接调用官方DLL(pylonc.net.dll)实现接入 海康和大恒相机因驱动DLL依赖C++组件,无法直接调用 解决方案:通过独立进程(相机应用程序)与Unity进行通信,采用NetMQ实现图像数据传输 技术实现要点: 相机端:使用PublisherSocket发布JPEG图像帧 Unity端:通过SubscriberSocket订阅图像数据 优化:设置高水位线控制缓存帧数,避免数据堆积

weixin_46450859的博客 417

URDF模型转换实战:解决SolidWorks到Gazebo的五大常见问题

本文详细解析了从SolidWorks导出URDF模型到Gazebo仿真过程中常见的五大问题及解决方案,包括插件安装、坐标系建立、Link与Joint配置等关键步骤。通过实战经验分享,帮助机器人仿真工程师高效完成模型转换,确保Gazebo仿真准确性。

weixin_29052717的博客 445

面试:Art虚拟机和Davlik虚拟机简要对比

Android 4.4以后推出了ART虚拟机,它与Davlik虚拟机最大的不同在于,系统在安装应用时会对其进行预编译一次,将字节码转换成机器指令,以后每次启动不会重新编译,直接运行本地机器码。 直接上图: Art与Davlik加载过程.png Java虚拟机 首先先来说一下java虚拟机,不管是Art虚拟机还是Davlik虚拟机,首先第一步需要得到.class字节码。 Java虚拟机加载的是.class字节码,所以需要将.java文件编译成.class文件; 它是基于栈结构的

王温暖的博客 2074

BrepNet实战:5分钟搞定三维CAD模型的加工特征识别(附Python代码)

本文详细介绍了BrepNet在三维CAD模型加工特征识别中的实战应用,通过Python代码演示了从环境搭建到模型训练的全流程。BrepNet基于边界表示(BRep)的深度学习框架,能够高效识别加工特征,显著提升工业设计和智能制造领域的效率。文章还提供了性能优化和部署建议,帮助开发者快速实现AI驱动的几何特征识别。

weixin_29044713的博客 509

Android 虚拟机 Davlik

Dalvik虚拟机是Google的用于移动设备的Android平台的一个主要部分。虚拟机可运行Java平台应用程序,这些应用程序被转换成紧凑的Dalvik可执行格式(.dex),该格式适合内存和处理器速度受限的系统。Dalvik虚拟机的作者是丹伯恩斯坦(Dan Bornstein)。与 大多数虚拟机和真正的Java虚拟机不同,前者是栈机(stack machine),而Dalvik VM...

weixin_30872733的博客 84

车窗标定工作流程

标定简单来说就是修改防夹参数来适配对应的车型,所有的防夹的参数在vers_userdef.h文件中定义。#标定内容。

qq_21274991的博客 4272

OpenManus本地部署实战:从源码编译到浏览器路径优化全解析

本文详细解析了OpenManus本地部署的全过程,从源码编译到浏览器路径优化。涵盖环境准备、源码获取、模型配置、依赖安装等关键步骤,特别针对Windows系统下的chrome.exe路径修改提供了实用解决方案,帮助开发者高效完成部署并优化性能。

weixin_28826545的博客 381

android 中java 虚拟机模型研究与优化,掌握Java和Android虚拟机

我们知道的虚拟机有很多,运行Java的JVM虚拟机,运行Android程序的Davlik和Art虚拟机,运行C#的CLR虚拟机,那么什么是虚拟机呢,虚拟机的作用又是什么呢?运行JavaScript的v8引擎或者运行Python的引擎是否也是虚拟机呢?带着这几个问题,我们开始对虚拟机的学习。虽然现在很多人都认为运行JavaScript的V8或运行Python的VirtualEnv,都不是虚拟机,而是...

weixin_39851279的博客 255

LVGL core group焦点分组管理(lv_group.c)

lv_group_t是用于焦点管理的,将需要获取焦点的对像加入到同一个分组内,再将其通过挂到指定的事件驱动中,就可以控制焦点了。而被加入的对象又可以通过焦点状态事件和事件来控制对象的焦点逻辑了。创建好的分组通过接口挂事件驱动中,这样就可以指定一个自定义的焦点分组了。;) {if(!} }友情提示:无论是接口还是都是单例的,即被设置后前一个被设置的值就失效了;此方案可以保证焦点的唯一 性,但也一定要记住要在适合的位置设置回来,以防丢失焦点。

Bigan(安)的博客 3909

Dalvik虚拟机特点

1、Dalvik虚拟机与Java虚拟机区别(1)Java程序经过编译生成的Java字节码保存在class文件中;Dalvik虚拟机运行的是Dalvik字节码,Dalvik字节码由Java字节码转换而来,并被打包到一个DEX可执行文件中。(2)Dalvik可执行文件体积更小,DEX文件中所有类文件共享同一个常量池。(3)Java虚拟机与Dalvik虚拟机构架不同。Java虚拟机基于栈构架(需要更多的...

幼儿园 1656

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

(十一)Android AndFix 热修复原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。 一、so 的插件化 在进行 Android 的热修复讲解之前,先大概讲一下 so 的插件化,这在热修复中需要用到。 一个 so 可以通过路径去加载另外一个 so,从而实现一个 so 库调用另外一个 so 库中的方法。 先来一个被调用的 so,这个s...

孤独灬啸月 676

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Android的.dex、.odex与.oat文件区别

dex与.odex、.oat有什么联系呢?

多媒体开发者 2372

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Android再进阶之从Dalvik到ART虚拟机

众所周知,Android虚拟机不同于Java虚拟机,为了更高的编译速度,没有基于栈,而是基于寄存器。下面就来介绍整个Android虚拟机的演变过程。 自己做个目录。说一下主要的内容,不浪费阅读者的时间。主要讲了DVM到ART的转变。 Dalvik虚拟机的简单介绍,它的运行时堆重点介绍了。 ART虚拟机的简单介绍,它的运行时堆重点介绍了。 GC垃圾回收因为太费时间了,没有说。 区别主要集中介绍了编译的方式的转变,例如:JIT、AOT的引入,以及即时JIT的引入。 Dalvik虚拟机 1.DVM与JVM

weixin_42514162的博客 475

热修复设计之热修复原理(三),最新精心整理Android面试题

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!一个人可以走的很快,但一群人才能走的更远。

2401_84002482的博客 283

Android ART和Dalvik区别小结

先介绍下ART 和 DALVIK ART:Android操作系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Tim

CallmeZhe的博客 1511

Dalvik,ART与ODEX相爱相生

如果你有这样的问题:1.Dalvik和ART的区别2.DEX在Dalvik转化为ODEX和ART中转化为ODEX的过程有上面区别3.multidex在dalvik上起作用,ART上使用的也是multidex么(如果不是的话在application中写入multidex.install会对apk启动造成影响么)如果你比较“懒”,,懒得看老罗的源码分析,,,长篇大论请“简要”看完以下“简要”内容一:D...

c_z_w的博客 770

Dalvik 和 ART

1.Dalvik 虚拟机 Dalvik 虚拟机(Dalvik Virtual Machine ),简称 Dalvik VM 或者 DVM。它是由 Dan Bomstein编写的,名字源于他的祖先居住过的名为Dalvik的小渔村。DVM是Google专门 为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是 一个Java虚拟机(以下简称JVM),至于为什么,下文会给你答案。 1.DVM与JVM的区别 DVM之所以不是一个JVM,主要原因是DVM并没有遵循JVM规.

悟心的专栏 299
上一篇: DALVIK JIT 入口分析
下一篇: ART深入浅出1--虚拟机的启动和初始化
漂流的代码
博客等级 码龄23年 188粉丝 99原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值