第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方法在解釋執行下的調用約定(包括如何傳遞參數,如何接收返回值等),如果大家不明白,多讀幾遍文章就會有一個清晰的認識。

推薦閱讀:

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

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

第3篇-CallStub新棧幀的創建

第4篇-JVM終於開始調用Java主類的main()方法啦

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

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