专注虚拟机与编译器研究

第3篇-CallStub新栈帧的创建

在前一篇文章 第2篇-JVM虚拟机这样来调用Java主类的main()方法 中我们介绍了在call_helper()函数中通过函数指针的方式调用了一个函数,如下:

StubRoutines::call_stub()(
         (address)&link,
         result_val_address,              
         result_type,
         method(),
         entry_point,
         args->parameters(),
         args->size_of_parameters(),
         CHECK
);

其中调用StubRoutines::call_stub()函数会返回一个函数指针,查清楚这个函数指针指向的函数的实现是我们这一篇的重点。调用的call_stub()函数的实现如下:

源代码位置:openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

static CallStub  call_stub() { 
    return CAST_TO_FN_PTR(CallStub, _call_stub_entry); 
}

call_stub()函数返回一个函数指针,指向依赖于操作系统和CPU架构的特定的函数,原因很简单,要执行native代码,得看看是什么CPU架构以便确定寄存器,看看什么OS以便确定ABI。

其中CAST_TO_FN_PTR是宏,具体定义如下:

源代码位置:/src/share/vm/runtime/utilities/globalDefinitions.hpp
#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))

对call_stub()函数中的实现进行宏替换和展开后会变为如下的形式:

static CallStub call_stub(){
    return (CallStub)( castable_address(_call_stub_entry) );
}

CallStub的定义如下:

源代码位置:openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp

typedef void (*CallStub)(
    // 连接器
    address   link,    
    // 函数返回值地址
    intptr_t* result, 
    //函数返回値类型 
    BasicType result_type, 
    // JVM内部所表示的Java方法对象
    Method*   method, 
    // JVM调用Java方法的例程入口。JVM内部的每一段
    // 例程都是在JVM启动过程中预先生成好的一段机器指令。
    // 必须通过执行本例程来调用Java方法, 
    // 即需要先执行机器指令,然后才能跳转到Java方法
    // 字节码所对应的机器指令去执行
    address   entry_point, 
    intptr_t* parameters,
    int       size_of_parameters,
    TRAPS
); 

如上定义了一种函数指针类型,指向的函数声明了8个形式参数。 

在call_stub()函数中调用的castable_address()函数定义在globalDefinitions.hpp文件中,具体实现如下:

inline address_word  castable_address(address x)  { 
    return address_word(x) ; 
}

address_word是一定自定义的类型,在globalDefinitions.hpp文件中的定义如下:

typedef   uintptr_t     address_word;

其中uintptr_t也是一种自定义的类型,在Linux内核的操作系统下使用的是globalDefinitions_gcc.hpp文件中的定义,具体定义如下:

typedef  unsigned long int  uintptr_t;

这样call_stub()函数其实等同于如下的实现形式:

static CallStub call_stub(){
    return (CallStub)( unsigned int(_call_stub_entry) );
}

将_call_stub_entry强制转换为unsigned int类型,然后又强制转换为CallStub类型。CallStub是一个函数指针,所以_call_stub_entry应该也是一个函数指针,而不应该是一个普通的无符号整数。  

在call_stub()函数中,_call_stub_entry的定义如下:

address StubRoutines::_call_stub_entry = NULL; 

_call_stub_entry的初始化逻辑在openjdk/hotspot/src/cpu/x86/vm/stubGenerator_x86_64.cpp文件下的generate_initial()函数中,调用链如下:

JavaMain()                          java.c
InitializeJVM()                     java.c
JNI_CreateJavaVM()                  jni.cpp
Threads::create_vm()                thread.cpp
init_globals()                      init.cpp
stubRoutines_init1()                stubRoutines.cpp
StubRoutines::initialize1()         stubRoutines.cpp
StubGenerator_generate()            stubGenerator_x86_64.cpp
StubGenerator::StubGenerator()      stubGenerator_x86_64.cpp
StubGenerator::generate_initial()   stubGenerator_x86_64.cpp

其中的StubGenerator类定义在openjdk/hotspot/src/cpu/x86/vm目录下的stubGenerator_x86_64.cpp文件中,这个文件中的generate_initial()函数会初始化call_stub_entry变量,如下:

StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);

现在我们终于找到了函数指针指向的函数的实现逻辑,这个逻辑是通过调用generate_call_stub()函数来实现的。

不过经过查看后我们发现这个函数指针指向的并不是一个C++函数,而是一个机器指令片段,我们可以将其看为C++函数经过C++编译器编译后生成的机器指令片段即可。在generate_call_stub()函数中有如下调用语句:

__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);

这两段代码直接生成机器指令,不过为了查看机器指令,我们借助了HSDB工具将其反编译为可读性更强的汇编指令。如下:

push   %rbp         
mov    %rsp,%rbp 
sub    $0x60,%rsp 

这3条汇编是非常典型的开辟新栈帧的指令。
可在代码中嵌入如下代码打印一个区间内生成的机器指令,如下:

 address start = __ pc();
__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);

 address end =  __ pc();
 Disassembler::decode(start, end);

之前我们介绍过在通过函数指针进行调用之前的栈状态,如下:

那么经过运行如上3条汇编后这个栈状态就变为了如下的状态:

我们需要关注的就是old %rbp和old %rsp在没有运行开辟新栈帧(CallStub()栈帧)时的指向,以及开辟新栈帧(CallStub()栈帧)时的new %rbp和new %rsp的指向。另外还要注意saved rbp保存的就是old %rbp,这个值对于栈展开非常重要,因为能通过它不断向上遍历,最终找到当前线程所有的调用者的栈帧。

下面接着看generate_call_stub()函数的实现,如下:

address generate_call_stub(address& return_address) {
    ...
    address start = __ pc();
 

    const Address rsp_after_call(rbp, rsp_after_call_off * wordSize);
 
    const Address call_wrapper  (rbp, call_wrapper_off   * wordSize);
    const Address result        (rbp, result_off         * wordSize);
    const Address result_type   (rbp, result_type_off    * wordSize);
    const Address method        (rbp, method_off         * wordSize);
    const Address entry_point   (rbp, entry_point_off    * wordSize);
    const Address parameters    (rbp, parameters_off     * wordSize);
    const Address parameter_size(rbp, parameter_size_off * wordSize);
 
    const Address thread        (rbp, thread_off         * wordSize);
 
    const Address r15_save(rbp, r15_off * wordSize);
    const Address r14_save(rbp, r14_off * wordSize);
    const Address r13_save(rbp, r13_off * wordSize);
    const Address r12_save(rbp, r12_off * wordSize);
    const Address rbx_save(rbp, rbx_off * wordSize);
 
    // 开辟新的栈帧
    __ enter();
    __ subptr(rsp, -rsp_after_call_off * wordSize);
 
    //  保存寄存器中的值到栈上
    __ movptr(parameters,   c_rarg5); // parameters
    __ movptr(entry_point,  c_rarg4); // entry_point
 
 
    __ movptr(method,       c_rarg3); // method
    __ movl(result_type,  c_rarg2);   // result type
    __ movptr(result,       c_rarg1); // result
    __ movptr(call_wrapper, c_rarg0); // call wrapper
 
    // 将调用者负责保存的寄存器的值保存到栈上
    __ movptr(rbx_save, rbx);
    __ movptr(r12_save, r12);
    __ movptr(r13_save, r13);
    __ movptr(r14_save, r14);
    __ movptr(r15_save, r15);
 
    const Address mxcsr_save(rbp, mxcsr_off * wordSize);
    {
      Label skip_ldmx;
      __ stmxcsr(mxcsr_save);
      __ movl(rax, mxcsr_save);
      __ andl(rax, MXCSR_MASK);    // Only check control and mask bits
      ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
      __ cmp32(rax, mxcsr_std);
      __ jcc(Assembler::equal, skip_ldmx);
      __ ldmxcsr(mxcsr_std);
      __ bind(skip_ldmx);
    }

    // ... 省略了接下来的操作
}

其中开辟新栈帧的逻辑我们已经介绍过,下面就是将call_helper()函数传递的6个在寄存器中的参数存储到CallStub()栈帧中了,除了存储这几个参数外,还需要存储其它寄存器中的值,因为函数接下来要做的操作是为Java方法准备参数并调用Java方法,我们并不知道Java方法会不会破坏这些寄存器中的值,所以要保存下来,等调用完成后恢复寄存器中的值。

生成的汇编代码如下:

mov      %r9,-0x8(%rbp)
mov      %r8,-0x10(%rbp)
mov      %rcx,-0x18(%rbp)
mov      %edx,-0x20(%rbp)
mov      %rsi,-0x28(%rbp)
mov      %rdi,-0x30(%rbp)
mov      %rbx,-0x38(%rbp)
mov      %r12,-0x40(%rbp)
mov      %r13,-0x48(%rbp)
mov      %r14,-0x50(%rbp)
mov      %r15,-0x58(%rbp)
// stmxcsr是将MXCSR寄存器中的值保存到-0x60(%rbp)中
stmxcsr  -0x60(%rbp)  
mov      -0x60(%rbp),%eax
and      $0xffc0,%eax // MXCSR_MASK = 0xFFC0
// cmp通过第2个操作数减去第1个操作数的差,根据结果来设置eflags中的标志位。
// 本质上和sub指令相同,但是不会改变操作数的值
cmp      0x1762cb5f(%rip),%eax  # 0x00007fdf5c62d2c4 
// 当ZF=1时跳转到目标地址
je       0x00007fdf45000772 
// 将 0x1762cb52(%rip) 加载到MXCSR寄存器中
ldmxcsr  0x1762cb52(%rip)      # 0x00007fdf5c62d2c4  

加载完成这些参数后栈的状态如下图所示。

下一篇我们继续介绍下generate_call_stub()函数中其余的实现。

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

posted on 2021-08-13 09:07  鸠摩(马智)  阅读(1351)  评论(0编辑  收藏  举报

导航