专注虚拟机与编译器研究

第37篇-Interpreter::_invoke_return_entry等例程

我们在之前介绍过return字节码指令的执行逻辑,这个字节码指令只会执行释放锁和退出当前栈帧的操作,但是当控制权转移给调用者时,还需要恢复调用者的栈帧状态,如让%r13指向bcp、%r14指向局部变量表等,另外还需要弹出压入的实参、跳转到调用者的下一个字节码指令继续执行,而这一切操作都是由Interpreter::_return_entry例程负责的。这个例程在之前介绍invokevirtual和invokeinterface等字节码指令时介绍过,当使用这些字节码指令调用方法时,会根据方法的返回类型压入Interpreter::_return_entry一维数组中保存的对应例程地址,这样return字节码指令执行完成后就会执行这段例程。

在invokevirtual和invokeinterface等字节码指令中通过调用如下函数获取对应的例程入口:

address* TemplateInterpreter::invoke_return_entry_table_for(Bytecodes::Code code) {
  switch (code) {
  case Bytecodes::_invokestatic:
  case Bytecodes::_invokespecial:
  case Bytecodes::_invokevirtual:
  case Bytecodes::_invokehandle:
    return Interpreter::invoke_return_entry_table();
  case Bytecodes::_invokeinterface:
    return Interpreter::invokeinterface_return_entry_table();
  default:
    fatal(err_msg("invalid bytecode: %s", Bytecodes::name(code)));
    return NULL;
  }
}

可以看到invokeinterface字节码从Interpreter::_invokeinterface_return_entry数组中获取对应的例程,而其它的从Interpreter::_invoke_return_entry一维数组中获取。如下:

address TemplateInterpreter::_invoke_return_entry[TemplateInterpreter::number_of_return_addrs];
address TemplateInterpreter::_invokeinterface_return_entry[TemplateInterpreter::number_of_return_addrs];
address TemplateInterpreter::_invokedynamic_return_entry[TemplateInterpreter::number_of_return_addrs];

当返回一维数组后,会根据方法返回类型进一步确定例程入口地址。下面我们就看一下这些例程的生成过程。 

TemplateInterpreterGenerator::generate_all()函数中会生成Interpreter::_return_entry入口,如下:

{
    CodeletMark cm(_masm, "invoke return entry points");
    const TosState states[]           = {itos, itos, itos, itos, ltos, ftos, dtos, atos, vtos};
    const int invoke_length           = Bytecodes::length_for(Bytecodes::_invokestatic);     // invoke_length=3
    const int invokeinterface_length  = Bytecodes::length_for(Bytecodes::_invokeinterface);  // invokeinterface=5

    for (int i = 0; i < Interpreter::number_of_return_addrs; i++) { // number_of_return_addrs = 9
       TosState state = states[i]; // TosState是枚举类型
       Interpreter::_invoke_return_entry[i] = generate_return_entry_for(state, invoke_length, sizeof(u2)); 
       Interpreter::_invokeinterface_return_entry[i] = generate_return_entry_for(state, invokeinterface_length, sizeof(u2));
    }
  }

除invokedynamic字节码指令外,其它的方法调用指令在解释执行完成后都需要调用由generate_return_entry_for()函数生成的例程,生成例程的generate_return_entry_for()函数实现如下:

address TemplateInterpreterGenerator::generate_return_entry_for(TosState state, int step, size_t index_size) {

  // Restore stack bottom in case万一 i2c adjusted stack
  __ movptr(rsp, Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize)); // interpreter_frame_last_sp_offset=-2
  // and NULL it as marker that esp is now tos until next java call
  __ movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), (int32_t)NULL_WORD);

  __ restore_bcp();
  __ restore_locals();

  // ...

  const Register cache = rbx;
  const Register index = rcx;
  __ get_cache_and_index_at_bcp(cache, index, 1, index_size);

  const Register flags = cache;
  __ movl(flags, Address(cache, index, Address::times_ptr, ConstantPoolCache::base_offset() + ConstantPoolCacheEntry::flags_offset()));
  __ andl(flags, ConstantPoolCacheEntry::parameter_size_mask);
  __ lea(rsp, Address(rsp, flags, Interpreter::stackElementScale())   ); // 栈元素标量为8
  __ dispatch_next(state, step);

  return entry;
}

根据state的不同(方法的返回类型的不同),会在选择执行调用者方法的下一个字节码指令时,决定要从字节码指令的哪个入口处开始执行。我们看一下,当传递的state为itos(也就是当方法的返回类型为int时)时生成的汇编代码如下:

// 将-0x10(%rbp)存储到%rsp后,置空-0x10(%rbp)
0x00007fffe1006ce0: mov    -0x10(%rbp),%rsp   // 更改rsp
0x00007fffe1006ce4: movq   $0x0,-0x10(%rbp)   // 更改栈中特定位置的值
// 恢复bcp和locals,使%r14指向本地变量表,%r13指向bcp
0x00007fffe1006cec: mov    -0x38(%rbp),%r13
0x00007fffe1006cf0: mov    -0x30(%rbp),%r14
 // 获取ConstantPoolCacheEntry的索引并加载到%ecx
0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx     

 // 获取栈中-0x28(%rbp)的ConstantPoolCache并加载到%ecx
0x00007fffe1006cf9: mov    -0x28(%rbp),%rbx   
// shl是逻辑左移,获取字偏移
0x00007fffe1006cfd: shl    $0x2,%ecx           
// 获取ConstantPoolCacheEntry中的_flags属性值
0x00007fffe1006d00: mov    0x28(%rbx,%rcx,8),%ebx
// 获取_flags中的低8位中保存的参数大小
0x00007fffe1006d04: and    $0xff,%ebx          

// lea指令将地址加载到内存寄存器中,也就是恢复调用方法之前栈的样子
0x00007fffe1006d0a: lea    (%rsp,%rbx,8),%rsp  

// 跳转到下一指令执行
0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx  
0x00007fffe1006d13: add    $0x3,%r13
0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
0x00007fffe1006d21: jmpq   *(%r10,%rbx,8)  

如上汇编代码的逻辑非常简单,这里不再过多介绍。  

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

 

 

 

 

 

  

posted on 2021-11-04 10:10  鸠摩(马智)  阅读(325)  评论(0编辑  收藏  举报

导航