Android 5.0的ART是如何为方法调用开辟栈空间的?

软件工程师、主攻高级编程语言虚拟机的设计与实现

62 👍 / 10 💬

问题描述

在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编译器的情况

由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时生成方法入口的处理逻辑:

解释器的情况

如上文所述,Android 5.0中的ART的解释器有两个版本,都是用C++实现的。两个版本间有共享部分代码。

这解释器在执行的时候,每当要新调用一个由解释器执行的方法,实际要分配两部分栈帧:

上面这两部分栈帧都可以在

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也是在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方法,则不会经过该函数。