在前一篇文章 第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 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()函數,調用鏈如下:
StubGenerator::generate_initial() stubGenerator_x86_64.cpp
StubGenerator::StubGenerator() stubGenerator_x86_64.cpp
StubGenerator_generate() stubGenerator_x86_64.cpp
StubRoutines::initialize1() stubRoutines.cpp
stubRoutines_init1() stubRoutines.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c
其中的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條匯編是非常典型的開辟新棧幀的指令。之前我們介紹過在通過函數指針進行調用之前的棧狀態,如下:
那么經過運行如上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);
// save register parameters
__ 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
// save regs belonging to calling function
__ 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
// 將m32加載到MXCSR寄存器中
ldmxcsr 0x1762cb52(%rip) # 0x00007fdf5c62d2c4
加載完成這些參數后如下圖所示。
下一篇我們繼續介紹下generate_call_stub()函數中其余的實現。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流