generate_fixed_frame() 与dispatch

2022-06-20  本文已影响0人  程序员札记

在从generate_normal_entry()函数调用generate_fixed_frame()函数时的栈与寄存器的状态如下:

栈的状态如下图所示。

image.png

各个寄存器的状态如下所示。

rax: return address     // %rax寄存器中存储的是返回地址r
rbx: Method*            // 要执行的Java方法的指针
r14: pointer to locals  // 本地变量表指针
r13: sender sp          // 调用者的栈顶

调用的generate_fixed_frame()方法的实现如下:
源代码位置:src/cpu/x86/vm/templateInterpreter_x86_64.cpp

// Generate a fixed interpreter frame. This is identical setup for
// interpreted methods and for native methods hence the shared code.
void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
  // initialize fixed part of activation frame
  __ push(rax);             // save return address    把返回地址紧接着局部变量区保存
  __ enter();               // save old & set new rbp 进入固定桢
  __ push(r13);             // set sender sp          保存调用者的栈顶地址
  __ push((int)NULL_WORD);  // leave last_sp as null
  __ movptr(r13, Address(rbx, Method::const_offset()));      // 获取ConstMethod*并保存到r13中
  __ lea(r13, Address(r13, ConstMethod::codes_offset()));    // 保存字节码的地址到r13中
  __ push(rbx);             // 保存Method*的地址到堆栈上

  // ProfileInterpreter的默认值为true,表示需要对方法进行信息编译
  if (ProfileInterpreter) {
    Label method_data_continue;
    // MethodData结构基础是ProfileData,记录函数运行状态下的数据
    // MethodData里面分为3个部分,一个是函数类型等运行相关统计数据,一个是参数类型运行相关统计数据,
    // 还有一个是extra扩展区保存着deoptimization的相关信息
    // 获取Method中的_method_data属性的值并保存到rdx中
    __ movptr(rdx, Address(rbx, in_bytes(Method::method_data_offset())));
    __ testptr(rdx, rdx);
    __ jcc(Assembler::zero, method_data_continue);
    // 执行到这里,说明_method_data已经进行了初始化,通过MethodData来获取_data属性的值并存储到rdx中
    __ addptr(rdx, in_bytes(MethodData::data_offset()));
    __ bind(method_data_continue);
    __ push(rdx);      // set the mdp (method data pointer)
  } else {
    __ push(0);
  }

  __ movptr(rdx, Address(rbx, Method::const_offset()));          // 获取ConstMethod*存储到rdx
  __ movptr(rdx, Address(rdx, ConstMethod::constants_offset())); // 获取ConstantPool*存储到rdx
// 获取ConstantPoolCache*并存储到rdx
  __ movptr(rdx, Address(rdx, ConstantPool::cache_offset_in_bytes())); 
  __ push(rdx); // 保存ConstantPoolCache*到堆栈上
  __ push(r14); // 保存第1个参数的地址到堆栈上

  if (native_call) {
    __ push(0); // no bcp
  } else {
    __ push(r13); // 保存Java方法字节码地址到堆栈上,注意上面对r13寄存器的值进行了更改
  }
  __ push(0); // reserve word for pointer to expression stack bottom
  __ movptr(Address(rsp, 0), rsp); // set expression stack bottom //在rsp的地址保存rsp的值
}  

生成的汇编代码如下:

<pre class="brush:csharp;gutter:false;" style="overflow: auto; font-family: &quot;Courier New&quot; !important; font-size: 12px !important; box-sizing: border-box; margin: 5px 0px; width: 890px; white-space: pre; border-radius: 3px; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); padding: 5px; color: rgb(0, 0, 0);">0x00007fffed01b254: push   %rax
0x00007fffed01b255: push   %rbp
0x00007fffed01b256: mov    %rsp,%rbp
0x00007fffed01b259: push   %r13
0x00007fffed01b25b: pushq  $0x0
0x00007fffed01b260: mov    0x10(%rbx),%r13
0x00007fffed01b264: lea    0x30(%r13),%r13 // lea指令获取内存地址本身
0x00007fffed01b268: push   %rbx
0x00007fffed01b269: mov    0x18(%rbx),%rdx
0x00007fffed01b26d: test   %rdx,%rdx
0x00007fffed01b270: je     0x00007fffed01b27d
0x00007fffed01b276: add    $0x90,%rdx
0x00007fffed01b27d: push   %rdx
0x00007fffed01b27e: mov    0x10(%rbx),%rdx
0x00007fffed01b282: mov    0x8(%rdx),%rdx
0x00007fffed01b286: mov    0x18(%rdx),%rdx
0x00007fffed01b28a: push   %rdx
0x00007fffed01b28b: push   %r14
0x00007fffed01b28d: push   %r13
0x00007fffed01b28f: pushq  $0x0
0x00007fffed01b294: mov    %rsp,(%rsp)

通过源代码结合汇编的方式可以将代码的逻辑看的清清楚楚,最终的栈状态变为了如下的样子:

image.png

左边的栈底与右边的栈顶是连续的,右边的栈中除local variable1、...、local variable n之外都是调用generate_fixed_frame()生成的。而argument word 1、...、argument word n加上右边的栈布局就是调用Java方法的栈布局,可以看到,2个栈帧重用了argument word 1、...、argument word n,而浅灰色与深灰色加起来就是为调用Java方法分配的本地变量表。

下面重新总结一下,如下所示。

image.png image.png

注意rax中保存的值是返回到generate_call_stub()通过__ call(c_rarg1) 来调用generate_normal_entry()函数的返回地址,也就是会继续执行__ call(c_rarg1)下面的代码。

调用完generate_fixed_frame()方法后一些寄存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址</pre>

回顾

image.png

局部变量存放引用,操作数栈来运算

void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
  // initialize fixed part of activation frame
  __ push(rax);                                       // save return address
  __ enter();                                         // save old & set new rbp,
 
 
  __ push(rsi);                                       // set sender sp
  __ push((int32_t)NULL_WORD);                        // leave last_sp as null
  __ movptr(rsi, Address(rbx,Method::const_offset())); // get ConstMethod*
  __ lea(rsi, Address(rsi,ConstMethod::codes_offset())); // get codebase
  __ push(rbx);                                      // save Method*
  if (ProfileInterpreter) {
    Label method_data_continue;
    __ movptr(rdx, Address(rbx, in_bytes(Method::method_data_offset())));
    __ testptr(rdx, rdx);
    __ jcc(Assembler::zero, method_data_continue);
    __ addptr(rdx, in_bytes(MethodData::data_offset()));
    __ bind(method_data_continue);
    __ push(rdx);                                       // set the mdp (method data pointer)
  } else {
    __ push(0);
  }
 
  __ movptr(rdx, Address(rbx, Method::const_offset()));
  __ movptr(rdx, Address(rdx, ConstMethod::constants_offset()));
  __ movptr(rdx, Address(rdx, ConstantPool::cache_offset_in_bytes()));
  __ push(rdx);                                       // set constant pool cache
  __ push(rdi);                                       // set locals pointer
  if (native_call) {
    __ push(0);                                       // no bcp
  } else {
    __ push(rsi);                                     // set bcp
    }
  __ push(0);                                         // reserve word for pointer to expression stack bottom
  __ movptr(Address(rsp, 0), rsp);                    // set expression stack bottom
}

先将return address取出再放入

image.png

开辟新的栈帧,三部分:局部变量表,帧数据,操作数栈

调整参数位置,改变偏移量

计算java方法第一个字节码位置

___ movptr(rsi, Address(rbx,methodOopDesc::const_offset()));
___ lea(rsi, Address(rsi, constMethodOopDesc::codes_offset()));
将methodOop压栈

___ push(rbx);
再继续压几个表,将第一条字节码指令压栈

JVM可以加载java类,也可以加载c/c++等程序库,因此jvm通过native_call判断

if (native_call){
    ___ push(0);
} else {
    ___ push(rsi);
}
image.png

还要压一个操作数栈

局部变量区通常用一个一个slot来索引变量,一个slot能够容纳一个int,reference等类型

变量这一块与C/C++编译后结果几乎完全相同

字节码的执行

在generate_normal_entry()函数中会调用generate_fixed_frame()函数为Java方法的执行生成对应的栈帧,接下来还会调用dispatch_next()函数执行Java方法的字节码。generate_normal_entry()函数中调用的dispatch_next()函数的实现如下:

// 从generate_fixed_frame()函数生成固定桢的时候,如果当前是第一次调用,
// 那么r13指向的是字节码的首地址,即第一个字节码,而step为0。
void InterpreterMacroAssembler::dispatch_next(TosState state, int step) 
{
        // load next bytecode (load before advancing r13 to prevent AGI)
        load_unsigned_byte(rbx, Address(r13, step));
        // 在当前字节码的位置,指针向前移动step宽度,获取地址上的值,这个值即为字节码在转发表中的index,
        // 存储到rbx。step的值由字节码指令和操作数决定。转发表中的index其实就是字节码(范围1~202),
        // 参考void DispatchTable::set_entry(int i, EntryPoint& entry) 方法。
        // advance r13
        increment(r13, step);//自增r13供下一次dispatch使用
        // 返回当前栈顶状态的所有字节码入口点
        dispatch_base(state, Interpreter::dispatch_table(state)); // Interpreter::dispatch_table(state)
}

r13指向字节码的首地址,当第1次调用时,参数step的值为0,那么load_unsigned_byte()方法从r13指向的内存中取一个字节的值,取出来的是字节码指令的操作码。增加r13的步长,这样下次执行时就会取出来下一个字节码指令的操作码。

调用的dispatch_table()函数的实现如下:

static address* dispatch_table(TosState state) {
     return _active_table.table_for(state);
}

在_active_table中获取对应栈顶缓存状态的入口地址,_active_table变量定义在TemplateInterpreter类中,如下:

static DispatchTable _active_table; // the active dispatch table (used by the interpreter for dispatch)

DispatchTable类及table_for()等方法的定义如下:

DispatchTable TemplateInterpreter::_active_table;
class DispatchTable VALUE_OBJ_CLASS_SPEC {
public:
/* an entry point for each byte value (also for undefined bytecodes) */
    enum { length = 1 << BitsPerByte };             /* BitsPerByte的值为8 */
private:
/* dispatch tables, indexed by tosca and bytecode */
    address _table[number_of_states][length];       /* number_of_states=9,length=256 */
public:
/*
 * Attributes
 * ...
 */
    address* table_for( TosState state )
    {
        return(_table[state]);
    }


    address* table_for()
    {
        return(table_for( (TosState) 0 ) );
    }


/* ... */
};

address为u_char*类型的别名。_table是一个二维数组的表,维度为栈顶状态(共有9种)和字节码(最多有256个),存储的是每个栈顶状态对应的字节码的入口点。这里由于还没有介绍栈顶缓存,所以可能理解起来并不容易,不过后面传经详细介绍,等介绍完了再看这部分逻辑就比较容易理解了。

InterpreterMacroAssembler::dispatch_next()函数中调用的dispatch_base()方法的实现如下:

void InterpreterMacroAssembler::dispatch_base( TosState state, /* 表示栈顶缓存状态 */
                           address* table,
                           bool verifyoop )
{
/*
 * ...
 * 获取当前栈顶状态字节码转发表的地址,保存到rscratch1
 */
    lea( rscratch1, ExternalAddress( (address) table ) );
/*
 * 跳转到字节码对应的入口执行机器码指令
 * address = rscratch1 + rbx * 8
 */
    jmp( Address( rscratch1, rbx, Address::times_8 ) );
}

比如取一个字节的指令,那么InterpreterMacroAssembler::dispatch_next()函数生成的汇编代码如下 :

// 在generate_fixed_frame()方法中已经让%r13存储了bcp
0x00007fffe1010643: movzbl 0x0(%r13),%ebx // %ebx中存储的是字节码的操作码
// $0x7ffff73ba4a0这个地址指向的是对应state状态下的一维数组,长度为256
0x00007fffe1010648: movabs $0x7ffff73ba4a0,%r10
// 注意%r10中存储的是常量,根据计算公式%r10+%rbx*8来获取指向存储入口地址的地址,
// 通过*(%r10+%rbx*8)获取到入口地址,然后跳转到入口地址执行
0x00007fffe1010652: jmpq *(%r10,%rbx,8)

%r10指向的是对应栈顶缓存状态state下的一维数组,长度为256,其中存储的值为opcode,如下图所示。

image.png

下面的方法显示了对每个字节码的每个栈顶状态都设置入口地址。

void DispatchTable::set_entry(int i, EntryPoint& entry) 
{
        assert(0 <= i && i < length, "index out of bounds");
        assert(number_of_states == 9, "check the code below");
        _table[btos][i] = entry.entry(btos);
        _table[ctos][i] = entry.entry(ctos);
        _table[stos][i] = entry.entry(stos);
        _table[atos][i] = entry.entry(atos);
        _table[itos][i] = entry.entry(itos);
        _table[ltos][i] = entry.entry(ltos);
        _table[ftos][i] = entry.entry(ftos);
        _table[dtos][i] = entry.entry(dtos);
        _table[vtos][i] = entry.entry(vtos);
}

其中的参数i就是opcode,各个字节码及对应的opcode可参考https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-7.html

调用dispatch_next()函数执行Java方法的字节码,其实就是根据字节码找到对应的入口地址来执行,而入口地址就是机器码的入口地址,这个机器码就是根据对应的字节码翻译过来的,这些都会在后面详细介绍

上一篇下一篇

猜你喜欢

热点阅读