为什么要讲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代码,能够使用的寄存器数量就更加有限。
面对这种情况,你想会怎么做呢?我们需要考虑两个原则:
-
寄存器只能共用;
-
尽量给频繁访问的变量分配一个临时的寄存器来使用。
这个算法的基本原理就是这样:
-
做一个寄存器池,哪个变量用到了寄存器,就临时分配给它;
-
如果池中的寄存器用完了,就解除一些变量和寄存器的绑定,归还到寄存器池中;
-
如果能够明确的知道一个变量不再使用了,就及时解除它与寄存器的绑定,归还到寄存器池中;
-
对于一些不是必须寄存器参与的操作,比如读操作,可以直接从内存中读取对应的值;
-
进入基本块时,接触所有变量与寄存器的绑定,这就可以释放更多的寄存器来供先不使用,又可避免寄存器与内存数据可能存在的不一致问题
dvmCompileTrace是jit编译的入口函数。这个函数完成编译的过程,大致如下:
-
dvmCompileTrace 该函数的前半部分,将代码分解成若干个BasicBlock,并建立其他们的关系
-
dvmInitializeSSAConversion SSA的分析
-
dvmCompilerMIR2LIR 将MIR转变为LIR。MIR表示一个dalvik指令;LIR表示一个机器码指令,MIR2LIR的过程是将dalvik字节码翻译成机器码的过程;
-
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指令格式 一种指令的格式,总是固定,因此,把其中固定的位的值填入,把操作数的位置用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拷贝到生成代码的内存中。
-
ArmEncodingMap
*encoder
=
&EncodingMap
[lir
-
>opcode
]
;
-
u4 bits
= encoder
-
>skeleton
;
//获取指令骨架
-
int i
;
-
for
(i
=
0
; i
<
4
; i
++
)
{
//循环操作数,填充bits,得到最后编码的指令
-
u4 operand
;
-
u4 value
;
-
operand
= lir
-
>operands
[i
]
;
-
switch
(encoder
-
>fieldLoc
[i
].
kind
)
{
-
case kFmtUnused
:
-
break
;
-
case kFmtFPImm
:
-
value
=
(
(operand
&
0xF0
)
>>
4
)
<< encoder
-
>fieldLoc
[i
].
end
;
-
value
|
=
(operand
&
0x0F
)
<< encoder
-
>fieldLoc
[i
].
start
;
-
bits
|
= value
;
-
break
;
-
case kFmtBrOffset
:
-
value
=
(
(operand
&
0x80000
)
>>
19
)
<<
26
;
-
value
|
=
(
(operand
&
0x40000
)
>>
18
)
<<
11
;
-
value
|
=
(
(operand
&
0x20000
)
>>
17
)
<<
13
;
-
value
|
=
(
(operand
&
0x1f800
)
>>
11
)
<<
16
;
-
value
|
=
(operand
&
0x007ff
)
;
-
bits
|
= value
;
-
break
;
-
case kFmtShift5
:
-
value
=
(
(operand
&
0x1c
)
>>
2
)
<<
12
;
-
value
|
=
(operand
&
0x03
)
<<
6
;
-
bits
|
= value
;
-
break
;
-
case kFmtShift
:
-
value
=
(
(operand
&
0x70
)
>>
4
)
<<
12
;
-
value
|
=
(operand
&
0x0f
)
<<
4
;
-
bits
|
= value
;
-
break
;
-
case kFmtBWidth
:
-
value
= operand
-
1
;
-
bits
|
= value
;
-
break
;
-
case kFmtLsb
:
-
value
=
(
(operand
&
0x1c
)
>>
2
)
<<
12
;
-
value
|
=
(operand
&
0x03
)
<<
6
;
-
bits
|
= value
;
-
break
;
-
case kFmtImm6
:
-
value
=
(
(operand
&
0x20
)
>>
5
)
<<
9
;
-
value
|
=
(operand
&
0x1f
)
<<
3
;
-
bits
|
= value
;
-
break
;
-
case kFmtBitBlt
:
-
value
=
(operand
<< encoder
-
>fieldLoc
[i
].
start
)
&
-
(
(
1
<<
(encoder
-
>fieldLoc
[i
].
end
+
1
)
)
-
1
)
;
-
bits
|
= value
;
-
break
;
-
case kFmtDfp
:
{
-
assert
(DOUBLEREG
(operand
)
)
;
-
assert
(
(operand
&
0x1
)
==
0
)
;
-
int regName
=
(operand
& FP_REG_MASK
)
>>
1
;
-
/* Snag the 1-bit slice and position it */
-
value
=
(
(regName
&
0x10
)
>>
4
)
<<
-
encoder
-
>fieldLoc
[i
].
end
;
-
/* Extract and position the 4-bit slice */
-
value
|
=
(regName
&
0x0f
)
<<
-
encoder
-
>fieldLoc
[i
].
start
;
-
bits
|
= value
;
-
break
;
-
}
-
case kFmtSfp
:
-
assert
(SINGLEREG
(operand
)
)
;
-
/* Snag the 1-bit slice and position it */
-
value
=
(operand
&
0x1
)
<<
-
encoder
-
>fieldLoc
[i
].
end
;
-
/* Extract and position the 4-bit slice */
-
value
|
=
(
(operand
&
0x1e
)
>>
1
)
<<
-
encoder
-
>fieldLoc
[i
].
start
;
-
bits
|
= value
;
-
break
;
-
case kFmtImm12
:
-
case kFmtModImm
:
-
value
=
(
(operand
&
0x800
)
>>
11
)
<<
26
;
-
value
|
=
(
(operand
&
0x700
)
>>
8
)
<<
12
;
-
value
|
= operand
&
0x0ff
;
-
bits
|
= value
;
-
break
;
-
case kFmtImm16
:
-
value
=
(
(operand
&
0x0800
)
>>
11
)
<<
26
;
-
value
|
=
(
(operand
&
0xf000
)
>>
12
)
<<
16
;
-
value
|
=
(
(operand
&
0x0700
)
>>
8
)
<<
12
;
-
value
|
= operand
&
0x0ff
;
-
bits
|
= value
;
-
break
;
-
default
:
-
assert
(
0
)
;
-
}
-
}
-
//指令的长度不一样,所以,要拷贝的指令数据也不一样。
-
if
(encoder
-
>size
==
2
)
{
-
*bufferAddr
++
=
(bits
>>
16
)
&
0xffff
;
-
}
-
*bufferAddr
++
= bits
&
0xffff
;
-
}
-
return kSuccess
;
本人很佩服这个代码的设计者,这是大师的手笔!
跳转和PC相关的指令
对于跳转指令和基于PC的ldr指令,他们都需要计算相对偏移,这个时候,就用要到LIR的target和offset两个成员了。代码很长,我只摘取了一部分。大家需要注意,这部分代码是对指令做预处理,最终还是上面章节提到的方法来生成指令。
-
if
(lir
-
>opcode
== kThumbLdrPcRel
||
-
lir
-
>opcode
== kThumb2LdrPcRel12
||
-
lir
-
>opcode
== kThumbAddPcRel
||
-
(
(lir
-
>opcode
== kThumb2Vldrd
)
&&
(lir
-
>operands
[
1
]
== r15pc
)
)
||
-
(
(lir
-
>opcode
== kThumb2Vldrs
)
&&
(lir
-
>operands
[
1
]
== r15pc
)
)
)
{
-
ArmLIR
*lirTarget
=
(ArmLIR
*
) lir
-
>generic.
target
;
-
intptr_t pc
=
(lir
-
>generic.
offset
+
4
)
& ~
3
;
-
intptr_t target
= lirTarget
-
>generic.
offset
;
-
int delta
= target
- pc
;
-
if
(delta
&
0x3
)
{
-
ALOGE
(
"PC-rel distance is not multiples of 4: %d", delta
)
;
-
dvmCompilerAbort
(cUnit
)
;
-
}
-
if
(
(lir
-
>opcode
== kThumb2LdrPcRel12
)
&&
(delta
>
4091
)
)
{
-
if
(cUnit
-
>printMe
)
{
-
ALOGD
(
"kThumb2LdrPcRel12@%x: delta=%d", lir
-
>generic.
offset,
-
delta
)
;
-
dvmCompilerCodegenDump
(cUnit
)
;
-
}
-
return kRetryHalve
;
-
}
else
if
(delta
>
1020
)
{
-
if
(cUnit
-
>printMe
)
{
-
ALOGD
(
"kThumbLdrPcRel@%x: delta=%d", lir
-
>generic.
offset,
-
delta
)
;
-
dvmCompilerCodegenDump
(cUnit
)
;
-
}
-
return kRetryHalve
;
-
}
-
if
(
(lir
-
>opcode
== kThumb2Vldrs
)
||
(lir
-
>opcode
== kThumb2Vldrd
)
)
{
-
lir
-
>operands
[
2
]
= delta
>>
2
;
-
}
else
{
-
lir
-
>operands
[
1
]
=
(lir
-
>opcode
== kThumb2LdrPcRel12
)
?
-
delta
: delta
>>
2
;
-
}
-
}
-
.....
最终,将计算出的delta偏移,放入到operands中。因为不同跳转指令预留的长度不一样,所以必须做长度处理。如果偏移过大,就必须再次处理源码使之满足条件。
预处理指令
还有一些LIR对象,不是映射到具体的机器码,而是用作一些特殊的用途,比如做对齐代码。比如下面的代码
-
if
(lir
-
>opcode
<
0
)
{
-
if
(
(lir
-
>opcode
== kArmPseudoPseudoAlign4
)
&&
-
/* 1 means padding is needed */
-
(lir
-
>operands
[
0
]
==
1
)
)
{
-
*bufferAddr
++
= PADDING_MOV_R5_R5
;
-
}
-
continue
;
-
}
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的格式
-
typedef struct MIR {
-
DecodedInstruction dalvikInsn ;
-
unsigned int width ;
-
unsigned int offset ;
-
struct MIR *prev ;
-
struct MIR *next ;
-
struct SSARepresentation *ssaRep ;
-
int OptimizationFlags ;
-
int seqNum ;
-
union {
-
// Used by the inlined insn from the callee to find the mother method
-
const Method *calleeMethod ;
-
// Used by the inlined invoke to find the class and method pointers
-
CallsiteInfo *callsiteInfo ;
-
} meta ;
-
} 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指令为例,他的实现代码如下:
-
static
bool handleFmt12x
(CompilationUnit
*cUnit, MIR
*mir
)
-
{
-
Opcode opcode
= mir
-
>dalvikInsn.
opcode
;
-
RegLocation rlDest
;
-
RegLocation rlSrc
;
-
RegLocation rlResult
;
-
....
-
if
(mir
-
>ssaRep
-
>numUses
==
2
)
-
rlSrc
= dvmCompilerGetSrcWide
(cUnit, mir,
0,
1
)
;
-
else
-
rlSrc
= dvmCompilerGetSrc
(cUnit, mir,
0
)
;
-
if
(mir
-
>ssaRep
-
>numDefs
==
2
)
-
rlDest
= dvmCompilerGetDestWide
(cUnit, mir,
0,
1
)
;
-
else
-
rlDest
= dvmCompilerGetDest
(cUnit, mir,
0
)
;
-
-
switch
(opcode
)
{
-
case OP_MOVE
:
-
case OP_MOVE_OBJECT
:
-
storeValue
(cUnit, rlDest, rlSrc
)
;
-
break
;
-
}
-
.....
rlDest表示的目标寄存器,rlSrc表示的是源寄存器。RegLocation这个对象,表示的是一个寄存器,包含了CPU寄存器的信息,也包含了dalvik寄存器的信息,至于是怎么分配CPU寄存器给dalvik寄存器,我们会在下篇中详细描述。
storeValue的功能是非常简单的,即实现寄存器间数据拷贝。但是实现起来情况就非常的复杂,因为寄存器的类型可以有很多种:
-
kLocPhysReg: 已经分配了CPU寄存器,并且寄存器的值被更新;
-
kLocDalvikFrame: 该值还是存储在dalvik的Frame上(即线程的堆栈上),没有分配对应的CPU寄存器,也没有载入到寄存中;
-
kLocRetval: 这是一个放在Thread中内存地址,表示返回值的地址,return和move-return指令都要用到
-
kLocSpill: 没有用到,忽略。
那么,src和dest一共有6中组合(src和dest不能同时为kLocRetVal,故是2X3种组合),这6种组合产生的代码都不一样。
其中,kLocPhysRes表示真实可用的寄存器,kLocDalvikFrame和kLocRetval都是必须访问内存的。
下面,在注释中详细说明每种情况的用法:
-
static
void storeValue
(CompilationUnit
*cUnit, RegLocation rlDest,
-
RegLocation rlSrc
)
-
{
-
LIR
*defStart
;
-
LIR
*defEnd
;
-
assert
(
!rlDest.
wide
)
;
-
assert
(
!rlSrc.
wide
)
;
-
dvmCompilerKillNullCheckedLoc
(cUnit, rlDest
)
;
-
rlSrc
= dvmCompilerUpdateLoc
(cUnit, rlSrc
)
;
-
rlDest
= dvmCompilerUpdateLoc
(cUnit, rlDest
)
;
-
if
(rlSrc.
location
== kLocPhysReg
)
{
-
if
(dvmCompilerIsLive
(cUnit, rlSrc.
lowReg
)
||
-
(rlDest.
location
== kLocPhysReg
)
)
{
-
// Src is live or Dest has assigned reg.
-
rlDest
= dvmCompilerEvalLoc
(cUnit, rlDest, kAnyReg,
false
)
;
-
//这种情况,src和dst都是寄存器,生成 mov rDst, rSrc的指令
-
genRegCopy
(cUnit, rlDest.
lowReg, rlSrc.
lowReg
)
;
-
}
else
{
-
// Just re-assign the registers. Dest gets Src's regs
-
//src分配了寄存器,但是dest没有,于是把src寄存器当作dest寄存器使用,不产生机器代码
-
rlDest.
lowReg
= rlSrc.
lowReg
;
-
dvmCompilerClobber
(cUnit, rlSrc.
lowReg
)
;
-
}
-
}
else
{
-
// Load Src either into promoted Dest or temps allocated for Dest
-
//src没有分配寄存器,但是dest有,于是,直接从src的内存中读取值到dest中
-
rlDest
= dvmCompilerEvalLoc
(cUnit, rlDest, kAnyReg,
false
)
;
-
loadValueDirect
(cUnit, rlSrc, rlDest.
lowReg
)
;
-
}
-
-
// Dest is now live and dirty (until/if we flush it to home location)
-
//把dest寄存器(已经分配了CPU寄存器)设置类live和dirty。live表示当前的dalvik寄存器与
-
//CPU寄存器处于绑定和被使用状态;dirty表示寄存器的值要新内存的值,在必要时,需要将新值写入内存
-
dvmCompilerMarkLive
(cUnit, rlDest.
lowReg, rlDest.
sRegLow
)
;
-
dvmCompilerMarkDirty
(cUnit, rlDest.
lowReg
)
;
-
-
-
if
(rlDest.
location
== kLocRetval
)
{
-
//如果dest是返回值,需要从Thread::interpSave.retval中读取值并写如到指定寄存器中
-
storeBaseDisp
(cUnit, rSELF,
offsetof
(Thread, interpSave.
retval
),
-
rlDest.
lowReg, kWord
)
;
-
dvmCompilerClobber
(cUnit, rlDest.
lowReg
)
;
-
}
else
{
-
//都没有寄存器了,需要进行内存对内存的操作
-
dvmCompilerResetDefLoc
(cUnit, rlDest
)
;
-
if
(dvmCompilerLiveOut
(cUnit, rlDest.
sRegLow
)
)
{
-
defStart
=
(LIR
*
)cUnit
-
>lastLIRInsn
;
-
int vReg
= dvmCompilerS2VReg
(cUnit, rlDest.
sRegLow
)
;
-
storeBaseDisp
(cUnit, rFP, vReg
<<
2, rlDest.
lowReg, kWord
)
;
-
dvmCompilerMarkClean
(cUnit, rlDest.
lowReg
)
;
-
defEnd
=
(LIR
*
)cUnit
-
>lastLIRInsn
;
-
dvmCompilerMarkDef
(cUnit, rlDest, defStart, defEnd
)
;
-
}
-
}
-
}
我们看genRegCopy函数的实现,这个实现了代码的生成
-
static ArmLIR
* genRegCopyNoInsert
(CompilationUnit
*cUnit,
int rDest,
int rSrc
)
-
{
-
ArmLIR
* res
;
-
ArmOpcode opcode
;
-
res
=
(ArmLIR
*
) dvmCompilerNew
(
sizeof
(ArmLIR
),
true
)
;
-
if
(LOWREG
(rDest
)
&& LOWREG
(rSrc
)
)
-
opcode
= kThumbMovRR
;
-
else
if
(
!LOWREG
(rDest
)
&&
!LOWREG
(rSrc
)
)
-
opcode
= kThumbMovRR_H2H
;
-
else
if
(LOWREG
(rDest
)
)
-
opcode
= kThumbMovRR_H2L
;
-
else
-
opcode
= kThumbMovRR_L2H
;
-
-
res
-
>operands
[
0
]
= rDest
;
-
res
-
>operands
[
1
]
= rSrc
;
-
res
-
>opcode
= opcode
;
-
setupResourceMasks
(res
)
;
-
if
(rDest
== rSrc
)
{
-
res
-
>flags.
isNop
=
true
;
-
}
-
return res
;
-
}
-
-
static ArmLIR
* genRegCopy
(CompilationUnit
*cUnit,
int rDest,
int rSrc
)
-
{
-
ArmLIR
*res
= genRegCopyNoInsert
(cUnit, rDest, rSrc
)
;
-
dvmCompilerAppendLIR
(cUnit,
(LIR
*
)res
)
;
-
return res
;
-
}
genRegCopyNoInsert 函数,首先生成一个ArmLIR对象,然后设置它的opcode, 最后填入操作数。
需要注意的是,虽然都是生成mov rd, rs这样的语句,但是汇编语言与实际机器码是1:N的关系,一条汇编助记符可以根据操作数的不一样,生成不同的机器码。
其他的dalvik指令,他们的框架结构都是类似的,只有具体的代码结构是不一样的。