在前一篇文章 第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()函式的實現如下:

來源:/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的定義如下:

原始碼位置:/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的初始化在在/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類定義在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()函式中其餘的實現。

推薦閱讀:

第1篇-關於JVM執行時,開篇說的簡單些

第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot原始碼剖析系列文章!