在前一篇 第4篇-JVM終於開始調用Java主類的main()方法啦 介紹了通過callq調用entry point,不過我們並沒有看完generate_call_stub()函數的實現。接下來在generate_call_stub()函數中會處理調用Java方法后的返回值,同時還需要執行退棧操作,也就是將棧恢復到調用Java方法之前的狀態。調用之前是什么狀態呢?在 第2篇-JVM虛擬機這樣來調用Java主類的main()方法 中介紹過,這個狀態如下圖所示。
generate_call_stub()函數接下來的代碼實現如下:
// 保存方法調用結果依賴於結果類型,只要不是T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE,都當做T_INT處理
// 將result地址的值拷貝到c_rarg0中,也就是將方法調用的結果保存在rdi寄存器中,注意result為函數返回值的地址
__ movptr(c_rarg0, result);
Label is_long, is_float, is_double, exit;
// 將result_type地址的值拷貝到c_rarg1中,也就是將方法調用的結果返回的類型保存在esi寄存器中
__ movl(c_rarg1, result_type);
// 根據結果類型的不同跳轉到不同的處理分支
__ cmpl(c_rarg1, T_OBJECT);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(c_rarg1, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
// 當邏輯執行到這里時,處理的就是T_INT類型,
// 將rax中的值寫入c_rarg0保存的地址指向的內存中
// 調用函數后如果返回值是int類型,則根據調用約定
// 會存儲在eax中
__ movl(Address(c_rarg0, 0), rax);
__ BIND(exit);
// 將rsp_after_call中保存的有效地址拷貝到rsp中,即將rsp往高地址方向移動了,
// 原來的方法調用實參argument 1、...、argument n,
// 相當於從棧中彈出,所以下面語句執行的是退棧操作
__ lea(rsp, rsp_after_call); // lea指令將地址加載到寄存器中
這里我們要關注result和result_type,result在調用call_helper()函數時就會傳遞,也就是會指示call_helper()函數將調用Java方法后的返回值存儲在哪里。對於類型為JavaValue的result來說,其實在調用之前就已經設置了返回類型,所以如上的result_type變量只需要從JavaValue中獲取結果類型即可。例如,調用Java主類的main()方法時,在jni_CallStaticVoidMethod()函數和jni_invoke_static()函數中會設置返回類型為T_VOID,也就是main()方法返回void。
生成的匯編代碼如下:
// 棧中的-0x28位置保存result
mov -0x28(%rbp),%rdi
// 棧中的-0x20位置保存result type
mov -0x20(%rbp),%esi
cmp $0xc,%esi // 是否為T_OBJECT類型
je 0x00007fdf450007f6
cmp $0xb,%esi // 是否為T_LONG類型
je 0x00007fdf450007f6
cmp $0x6,%esi // 是否為T_FLOAT類型
je 0x00007fdf450007fb
cmp $0x7,%esi // 是否為T_DOUBLE類型
je 0x00007fdf45000801
// 如果是T_INT類型,直接將返回結果%eax寫到棧中-0x28(%rbp)的位置
mov %eax,(%rdi)
// -- exit --
// 將rsp_after_call的有效地址拷到rsp中
lea -0x60(%rbp),%rsp
為了讓大家看清楚,我貼一下在調用Java方法之前的棧幀狀態,如下:
由圖可看到-0x60(%rbp)地址指向的位置,恰好不包括調用Java方法時壓入的實際參數argument word 1 ... argument word n。所以現在rbp和rsp就是圖中指向的位置了。
接下來恢復之前保存的caller-save寄存器,這也是調用約定的一部分,如下:
__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);
__ ldmxcsr(mxcsr_save);
生成的匯編代碼如下:
mov -0x58(%rbp),%r15
mov -0x50(%rbp),%r14
mov -0x48(%rbp),%r13
mov -0x40(%rbp),%r12
mov -0x38(%rbp),%rbx
ldmxcsr -0x60(%rbp)
在彈出了為調用Java方法保存的實際參數及恢復caller-save寄存器后,繼續執行退棧操作,實現如下:
// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);
// return
__ pop(rbp);
__ ret(0);
生成的匯編代碼如下:
// %rsp加上0x60,也就是執行退棧操作,也就相
// 當於彈出了callee_save寄存器和壓棧的那6個參數
add $0x60,%rsp
pop %rbp
// 方法返回,指令中的q表示64位操作數,就是指
// 的棧中存儲的return address是64位的
retq
記得在之前 第3篇-CallStub新棧幀的創建時,通過如下的匯編完成了新棧幀的創建:
push %rbp
mov %rsp,%rbp
sub $0x60,%rsp
現在要退出這個棧幀時要在%rsp指向的地址加上$0x60,同時恢復%rbp的指向。然后就是跳轉到return address指向的指令繼續執行了。
為了方便大家查看,我再次給出了之前使用到的圖片,這個圖是退棧之前的圖片:
退棧之后如下圖所示。
至於paramter size與thread則由JavaCalls::call_hlper()函數負責釋放,這是C/C++調用約定的一部分。所以如果不看這2個參數,我們已經完全回到了本篇給出的第一張圖表示的棧的樣子。
上面這些圖片大家應該不陌生才對,我們在一步步創建棧幀時都給出過,現在怎么創建的就會怎么退出。
之前介紹過,當Java方法返回int類型時(如果返回char、boolean、short等類型時統一轉換為int類型),根據Java方法調用約定,這個返回的int值會存儲到%rax中;如果返回對象,那么%rax中存儲的就是這個對象的地址,那后面到底怎么區分是地址還是int值呢?答案是通過返回類型區分即可;如果返回非int,非對象類型的值呢?我們繼續看generate_call_stub()函數的實現邏輯:
// handle return types different from T_INT
__ BIND(is_long);
__ movq(Address(c_rarg0, 0), rax);
__ jmp(exit);
__ BIND(is_float);
__ movflt(Address(c_rarg0, 0), xmm0);
__ jmp(exit);
__ BIND(is_double);
__ movdbl(Address(c_rarg0, 0), xmm0);
__ jmp(exit);
對應的匯編代碼如下:
// -- is_long --
mov %rax,(%rdi)
jmp 0x00007fdf450007d4
// -- is_float --
vmovss %xmm0,(%rdi)
jmp 0x00007fdf450007d4
// -- is_double --
vmovsd %xmm0,(%rdi)
jmp 0x00007fdf450007d4
當返回long類型時也存儲到%rax中,因為Java的long類型是64位,我們分析的代碼也是x86下64位的實現,所以%rax寄存器也是64位,能夠容納64位數;當返回為float或double時,存儲到%xmm0中。
統合這一篇和前幾篇文章,我們應該學習到C/C++的調用約定以及Java方法在解釋執行下的調用約定(包括如何傳遞參數,如何接收返回值等),如果大家不明白,多讀幾遍文章就會有一個清晰的認識。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流