第5篇-調用Java方法後彈出棧幀及處理返回結果
- 2021 年 8 月 14 日
- 筆記
在前一篇 第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源碼剖析系列文章!