在前一篇 第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。
生成的彙編程式碼如下:
mov -0x28(%rbp),%rdi // 棧中的-0x28位置儲存result
mov -0x20(%rbp),%esi // 棧中的-0x20位置儲存result type
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
mov %eax,(%rdi) // 如果是T_INT型別,直接將返回結果%eax寫到棧中-0x28(%rbp)的位置 // -- exit --
lea -0x60(%rbp),%rsp // 將rsp_after_call的有效地址拷到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方法在解釋執行下的呼叫約定(包括如何傳遞引數,如何接收返回值等),如果大家不明白,多讀幾遍文章就會有一個清晰的認識。
推薦閱讀:
第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot原始碼剖析系列文章!