问题描述
在Dalvik虚拟机中,在解释方法之前,会调用Stack.cpp中的dvmPushInterpFrame方法进行方法栈的开辟,然后把方法的变量和参数入栈,然后开始dvmInterpretPortable解释执行。那么在ART中是在什么时候进行方法栈的开辟呢。具体是在哪个方法中。ART在编译文件的时候是否只是给出栈空间一个数值,等到运行的时候才开辟呢?既然题主指定了版本,那么参考的源码就是:
lollipop-release版本回答只针对Android 5.0讨论。后面的版本(Android 6.0以及最新的AOSP dev版里)情况已经有了一些变化,但是我不想写在这个回答里了。
首先要留意的是,Android 5.0 "Lollipop"版里的Android Runtime(ART)的执行引擎只有两种工作模式:
- AOT编译执行:代码在安装时或升级时从Dex字节码编译为机器码,到运行时可以直接执行这事先编译好的机器码。同时,这些方法对应的Dex字节码也会保留到运行时,以便AOT编译的代码遇到无法处理的情况时可以退回到解释器执行(“deoptimize”)。
- Android 5.0的AOT编译器只有“Quick”这一个是可用的,所以下面只对它讨论。https://android.googlesource.com/platform/art/+/lollipop-release/compiler/dex/
- 解释执行:代码仍然保持Dex形式,到运行时由解释器来执行。Android 5.0的源码里有两个版本的解释器实现可选择,两者都是用纯C++代码实现的。
- switch版:基本款。用Clang编译时默认用这个版本。https://android.googlesource.com/platform/art/+/lollipop-release/runtime/interpreter/interpreter_switch_impl.cc
- computed goto版:使用了GCC的computed goto扩展的进阶版。用GCC编译时默认用这个版本。https://android.googlesource.com/platform/art/+/lollipop-release/runtime/interpreter/interpreter_goto_table_impl.cc
AOT编译器的情况
由Quick编译器以AOT编译模式生成的机器码,其实就跟C或C++编译出来的机器码一样,直接在native stack(或者叫“C stack”)上分配栈帧空间。
从Linaro做的一组演示稿引用一个例子:

演示稿:
HKG15-300: Art's Quick Compiler: An unofficial overview(打不开请自备工具)
上图中,A64 Assembly的部分就是Quick编译器的AArch64后端对例子中的Java程序(Dex字节码)编译生成的机器码的汇编形式。可以看到,其中第一条指令
sub sp, sp, #48就是在分配栈帧空间。
这个Java方法正好是个很小的叶子方法(leaf method)——不调用其它方法的方法,所以编译出来的机器码在方法入口处的处理比较简单,直接分配栈帧就好了。一般的Java方法的入口会有一系列更复杂的处理,详细可以参考上述演示稿的第7到第17页,写得非常详细。其中第12页演示register spills的部分就包含了分配栈帧空间的指令。
具体这些指令在Android 5.0的Quick编译器里是如何生成出来的,请参考代码中的GenEntrySequence()函数,它负责在Quick编译器中把代码形式从MIR转换为LIR时生成方法入口的处理逻辑:
- ARM后端:https://android.googlesource.com/platform/art/+/lollipop-release/compiler/dex/quick/arm/call_arm.cc#340
- ARM64后端:https://android.googlesource.com/platform/art/+/lollipop-release/compiler/dex/quick/arm64/call_arm64.cc#305
- MIPS后端:https://android.googlesource.com/platform/art/+/lollipop-release/compiler/dex/quick/mips/call_mips.cc#289
- x86后端:https://android.googlesource.com/platform/art/+/lollipop-release/compiler/dex/quick/x86/call_x86.cc#206
解释器的情况
如上文所述,Android 5.0中的ART的解释器有两个版本,都是用C++实现的。两个版本间有共享部分代码。
这解释器在执行的时候,每当要新调用一个由解释器执行的方法,实际要分配两部分栈帧:
- native stack:既然解释器自身是用C++实现的,而且每新调用解释器执行一个方法就会新调用一次C++的解释器入口函数,这里必然会涉及C++代码的栈帧的分配。这就跟一般C++函数调用一样,没啥特别的。
- shadow stack:这是所谓的“解释器栈”。逻辑上说,解释器为每个线程维护了一个专门用于解释器的栈,其中每个栈帧的内容就是Dalvik VM(或者说Dex字节码)所关心的状态,包括Dex层面的虚拟寄存器数组、执行方法对象的指针、当前执行到的虚拟PC(Dex PC)、上一个栈帧的指针,等等。这个解释器栈叫做“shadow stack”,其中每个栈帧的结构叫做ShadowFrame。
上面这两部分栈帧都可以在
EnterInterpreterFromInvoke()这个解释器入口函数得到体现。看看其部分源码:
void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver,
uint32_t* args, JValue* result) {
// ...
// Set up shadow frame with matching number of reference slots to vregs.
ShadowFrame* last_shadow_frame = self->GetManagedStack()->GetTopShadowFrame();
void* memory = alloca(ShadowFrame::ComputeSize(num_regs));
ShadowFrame* shadow_frame(ShadowFrame::Create(num_regs, last_shadow_frame, method, 0, memory));
self->PushShadowFrame(shadow_frame);
// ...
self->PopShadowFrame();
}
首先,调用这个C++函数就很自然会在native stack上创建这个函数的native部分的栈帧;然后,这个函数调用alloca()函数来在native栈帧上额外分配出一块空间来存ShadowFrame栈帧。于是两者就联系起来了:一个Java方法被解释器执行时,其解释器栈帧(ShadowFrame)是被包含在其native栈帧里的。
当解释器完成执行一个Java方法后,其ShadowFrame的空间就随着解释器的native栈帧的空间一起被释放。
======================================
从题主问题揣摩一些情况:
在Dalvik虚拟机中,在解释方法之前,会调用Stack.cpp中的dvmPushInterpFrame方法进行方法栈的开辟,然后把方法的变量和参数入栈,然后开始dvmInterpretPortable解释执行。
这个问法说明,题主
- 读的Dalvik VM源码是比较新的版本——已经从C变成“C风格的C++”了
- 读的是Dalvik VM中的“可移植版”解释器:vm/mterp/portable/entry.cpp,而不是实际部署的Android中通常用的“汇编版”解释器mterp。
Dalvik也是在native stack之外有个独立的解释器栈,栈帧的单元是StackSaveArea+被调用方法的vreg数量。其结构可以参考
vm/interp/Stack.h。
Dalvik VM虽然原本计划在后续发展中把解释器栈融合到native stack中,形成所谓的“mixed stack”(HotSpot VM用的就是这样的mixed stack,解释器栈与native stack融合在同一个栈里),但还没做到那一步Dalvik VM就被ART给替代了。
题主阅读Dalvik VM的源码时要留意mterp与JIT在操纵栈时做法与portable解释器都不完全一样的喔。例如说ARMv5TE版的mterp在分配解释器栈帧时是这样做的:
/*
* Given a frame pointer, find the stack save area.
*
* In C this is "((StackSaveArea*)(_fp) -1)".
*/
#define SAVEAREA_FROM_FP(_reg, _fpreg) \
sub _reg, _fpreg, #sizeofStackSaveArea
.LinvokeArgsDone: @ r0=methodToCall
ldrh r9, [r0, #offMethod_registersSize] @ r9<- methodToCall->regsSize
ldrh r3, [r0, #offMethod_outsSize] @ r3<- methodToCall->outsSize
ldr r2, [r0, #offMethod_insns] @ r2<- method->insns
ldr rINST, [r0, #offMethod_clazz] @ rINST<- method->clazz
@ find space for the new stack frame, check for overflow
SAVEAREA_FROM_FP(r1, rFP) @ r1<- stack save area
sub r1, r1, r9, lsl #2 @ r1<- newFp (old savearea - regsSize)
SAVEAREA_FROM_FP(r10, r1) @ r10<- newSaveArea
另外dvmPushInterpFrame()只在从VM调用解释器时会经过;如果是一个解释执行的Java方法要调用另一个解释执行的Java方法,则不会经过该函数。