第38篇-解釋方法之間的調用小實例

  • 2021 年 11 月 8 日
  • 筆記

這一篇我們介紹一下解釋執行的main()方法調用解析執行的add()方法的小實例,這個例子如下:

package com.classloading;

public class TestInvokeMethod {
	public int add(int a, int b) {
		return a + b;
	}

	public static void main(String[] args) {
		TestInvokeMethod tim = new TestInvokeMethod();
		tim.add(2, 3);
	}
}

通過Javac編譯器編譯為位元組碼文件,如下: 

Constant pool:
   #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // com/classloading/TestInvokeMethod
   #3 = Methodref          #2.#16         // com/classloading/TestInvokeMethod."<init>":()V
   #4 = Methodref          #2.#18         // com/classloading/TestInvokeMethod.add:(II)I
   #5 = Class              #19            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               add
  #11 = Utf8               (II)I
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               TestInvokeMethod.java
  #16 = NameAndType        #6:#7          // "<init>":()V
  #17 = Utf8               com/classloading/TestInvokeMethod
  #18 = NameAndType        #10:#11        // add:(II)I
  #19 = Utf8               java/lang/Object
{
  public com.classloading.TestInvokeMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class com/classloading/TestInvokeMethod
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_2
        10: iconst_3
        11: invokevirtual #4                  // Method add:(II)I
        14: pop
        15: return
}

下面分幾部分介紹調用相關的內容。

1、C++函數調用main()方法

現在我們從位元組碼索引為8的aload_1開始看,此時的棧幀狀態如下:

由於aload_1的tos_out為atos,所以在棧頂快取的暫存器中會快取有TestInvokeMethod實例的地址,當執行iconst_2時,會從atos進入。iconst_2指令的彙編如下: 

//  aep
push   %rax
jmpq   // 跳轉到下面那條指令執行

// ...

mov    $0x2,%eax // 指令的彙編程式碼

由於iconst_2的tos_out為itos,所以在進入下一個指令時,會從iconst_3的tos_int為itos中進入,如下:  

// iep
push   %rax

mov    $0x3,%eax

接下來就是執行invokevirtual位元組碼指令了,此時的2已經壓入了表達式棧,而3在%eax暫存器中做為棧頂快取,但是invokevirtual的tos_in為vtos,所以從invokevirtual位元組碼指令的iep進入時會將%eax暫存器中的值也壓入表達式棧中,最終的棧狀態如下圖所示。

 

2、main()方法調用add()方法

invokevirtual位元組碼指令在執行時,假設此位元組碼指令已經解析完成,也就是對應的ConstantPoolCacheEntry中已經保存了方法調用相關的資訊,則執行的相關彙編程式碼如下:

0x00007fffe1021f90: mov    %r13,-0x38(%rbp)    // 將bcp保存到棧中
// invokevirtual x中取出x,也就是常量池索引存儲到%edx,
// 其實這裡已經是ConstantPoolCacheEntry的index,因為在類的連接
// 階段會對方法中特定的一些位元組碼指令進行重寫
0x00007fffe1021f94: movzwl 0x1(%r13),%edx 
// 將ConstantPoolCache的首地址存儲到%rcx
 
 
0x00007fffe1021f99: mov    -0x28(%rbp),%rcx    
 
// 左移2位,因為%edx中存儲的是ConstantPoolCacheEntry索引,左移2位是因為
// ConstantPoolCacheEntry佔用4個字
0x00007fffe1021f9d: shl    $0x2,%edx    
        
// 計算%rcx+%rdx*8+0x10,獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
// 因為ConstantPoolCache的大小為0x16位元組,%rcx+0x10定位
// 到第一個ConstantPoolCacheEntry的位置
// %rdx*8算出來的是相對於第一個ConstantPoolCacheEntry的位元組偏移
0x00007fffe1021fa0: mov    0x10(%rcx,%rdx,8),%ebx 
 
// 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b2
0x00007fffe1021fa4: shr    $0x18,%ebx 
 
// 取出indices中含有的b2,即bytecode存儲到%ebx中
0x00007fffe1021fa7: and    $0xff,%ebx    
 
// 查看182的bytecode是否已經連接      
0x00007fffe1021fad: cmp    $0xb6,%ebx    
  
// 如果連接就進行跳轉,跳轉到resolved     
0x00007fffe1021fb3: je     0x00007fffe1022052

我們直接看方法解析後的邏輯實現,如下:

// **** resolved ****
// resolved的定義點,到這裡說明invokevirtual位元組碼已經連接
// 獲取ConstantPoolCacheEntry::_f2,這個欄位只對virtual有意義
// 在計算時,因為ConstantPoolCacheEntry在ConstantPoolCache之後保存,
// 所以ConstantPoolCache為0x10,而
// _f2還要偏移0x10,這樣總偏移就是0x20
// ConstantPoolCacheEntry::_f2存儲到%rbx
0x00007fffe1022052: mov    0x20(%rcx,%rdx,8),%rbx  
 // ConstantPoolCacheEntry::_flags存儲到%edx
0x00007fffe1022057: mov    0x28(%rcx,%rdx,8),%edx 
 // 將flags移動到ecx中
0x00007fffe102205b: mov    %edx,%ecx      
// 從flags中取出參數大小        
0x00007fffe102205d: and    $0xff,%ecx     
 
          
// 獲取到recv,%rcx中保存的是參數大小,最終計算參數所需要的大小為%rsp+%rcx*8-0x8,
// flags中的參數大小對實例方法來說,已經包括了recv的大小
// 如調用實例方法的第一個參數是this(recv)
0x00007fffe1022063: mov    -0x8(%rsp,%rcx,8),%rcx  // recv保存到%rcx 
 
// 將flags存儲到r13中
0x00007fffe1022068: mov    %edx,%r13d              
// 從flags中獲取return type,也就是從_flags的高4位保存的TosState
0x00007fffe102206b: shr    $0x1c,%edx 
 
// 將TemplateInterpreter::invoke_return_entry地址存儲到%r10
0x00007fffe102206e: movabs $0x7ffff73b6380,%r10 
// %rdx保存的是return type,計算返回地址
// 因為TemplateInterpreter::invoke_return_entry是數組,
// 所以要找到對應return type的入口地址
0x00007fffe1022078: mov    (%r10,%rdx,8),%rdx 
// 向棧中壓入返回地址
0x00007fffe102207c: push   %rdx      
 
// 還原ConstantPoolCacheEntry::_flags            
0x00007fffe102207d: mov    %r13d,%edx             
// 還原bcp
0x00007fffe1022080: mov    -0x38(%rbp),%r13  

執行完如上的程式碼後,已經向相關的暫存器中存儲了相關的值。相關的暫存器狀態如下:

rbx: 存儲的是ConstantPoolCacheEntry::_f2屬性的值
rcx: 就是調用實例方法時的第一個參數this
rdx: 存儲的是ConstantPoolCacheEntry::_flags屬性的值

棧的狀態如下圖所示。

需要注意的是return address也是一個常式的地址,是TemplateInterpreter::invoke_return_entry一維數組中類型為整數對應的下標存儲的那個地址,因為調用add()方法返回的是整數類型。如何得出add()方法的返回類型呢?是從ConstantPoolCacheEntry的_flags的TosState中得出的。

下面繼續看invokevirtual位元組碼指令將要執行的彙編程式碼,如下:

// flags存儲到%eax
0x00007fffe1022084: mov    %edx,%eax     
// 測試調用的方法是否為final        
0x00007fffe1022086: and    $0x100000,%eax    
// 如果不為final就直接跳轉到----notFinal----    
0x00007fffe102208c: je     0x00007fffe10220c0     
 
// 通過(%rcx)來獲取receiver的值,如果%rcx為空,則會引起OS異常
0x00007fffe1022092: cmp (%rcx),%rax 
 
// 省略統計相關程式碼部分
 
// 設置調用者棧頂並保存
0x00007fffe10220b4: lea    0x8(%rsp),%r13
0x00007fffe10220b9: mov    %r13,-0x10(%rbp)
 
// 跳轉到Method::_from_interpretered_entry入口去執行
0x00007fffe10220bd: jmpq   *0x58(%rbx) 

執行Method::_from_interpretered_entry常式,這個常式在之前詳細介紹過,執行完成後會為add()方法創建棧幀,此時的棧狀態如下圖所示。

執行iload_0與iload_1指令,由於連續出現了2個iload,所以是_fast_iload2,彙編如下:

movzbl  0x1(%r13),%ebx
neg     %rbx
mov     (%r14,%rbx,8),%eax
push    %rax
movzbl  0x3(%r13),%ebx
neg     %rbx
mov     (%r14,%rbx,8),%eax

注意,只有第1個變數壓入了棧,第2個則存儲到%eax中做為棧頂快取。 

調用iadd指令,由於tos_in為itos,所以彙編如下:

mov    (%rsp),%edx
add    $0x8,%rsp
add    %edx,%eax

最後結果快取在%eax中。 

3、退出add()方法

執行ireturn位元組碼指令進行add()方法的退棧操作。對於實例來說,執行的相關彙編程式碼如下:

// 將JavaThread::do_not_unlock_if_synchronized屬性存儲到%dl中
0x00007fffe101b770: mov    0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb   $0x0,0x2ad(%r15)
 
// 將Method*載入到%rbx中
0x00007fffe101b77f: mov    -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov    0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test   $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je     0x00007fffe101b970

unlocked處的彙編實現如下:  

// 將-0x8(%rbp)處保存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov    -0x8(%rbp),%rbx
 
// 移除棧幀
// leave指令相當於:
//     mov %rbp, %rsp
//     pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop    %r13
// 設置%rsp為調用者的棧頂值
0x00007fffe101bace: mov    %rbx,%rsp
0x00007fffe101bad1: jmpq   *%r13

執行leaveq指令進行退棧操作,此時的棧狀態如下圖所示。

然後我們就要彈出返回地址,跳轉到TemplateInterpreter::invoke_return_entry數組中保存的相關地址去執行對應的常式了。

4、執行返回常式

對於實例來說,傳遞的state為itos時生成的彙編程式碼如下:

// 將-0x10(%rbp)存儲到%rsp後,置空-0x10(%rbp)
0x00007fffe1006ce0: mov    -0x10(%rbp),%rsp   // 更改rsp
0x00007fffe1006ce4: movq   $0x0,-0x10(%rbp)   // 更改棧中特定位置的值
// 恢復bcp和locals,使%r14指向本地變數表,%r13指向bcp
0x00007fffe1006cec: mov    -0x38(%rbp),%r13
0x00007fffe1006cf0: mov    -0x30(%rbp),%r14
 // 獲取ConstantPoolCacheEntry的索引並載入到%ecx
0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx     

 // 獲取棧中-0x28(%rbp)的ConstantPoolCache並載入到%ecx
0x00007fffe1006cf9: mov    -0x28(%rbp),%rbx   
// shl是邏輯左移,獲取字偏移
0x00007fffe1006cfd: shl    $0x2,%ecx           
// 獲取ConstantPoolCacheEntry中的_flags屬性值
0x00007fffe1006d00: mov    0x28(%rbx,%rcx,8),%ebx
// 獲取_flags中的低8位中保存的參數大小
0x00007fffe1006d04: and    $0xff,%ebx          

// lea指令將地址載入到記憶體暫存器中,也就是恢復調用方法之前棧的樣子
0x00007fffe1006d0a: lea    (%rsp,%rbx,8),%rsp  

// 跳轉到下一指令執行
0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx  
0x00007fffe1006d13: add    $0x3,%r13
0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
0x00007fffe1006d21: jmpq   *(%r10,%rbx,8)
  

如上的彙編程式碼也是執行的退棧操作,最主要的就是把在調用解釋執行方法時壓入的實參從棧中彈出,接著就是執行main()方法中invokevirtual中的下一條指令pop。此時的棧狀態如下圖所示。

需要注意的是,此時的棧頂快取中存儲著調用add()方法的執行結果,那麼在跳轉到下一條指令pop時,必須要從pop的iep入口進入,這樣就能正確的執行下去了。 

5、退出main()方法 

當執行pop指令時,會從iep入口進入,執行的彙編程式碼如下:

// iep
push   %rax

// ...

add    $0x8,%rsp

由於main()方法調用add()方法不需要返回結果,所以對於main()方法來說,這個結果會從main()方法的表達式棧中彈出。下面接著執行return指令,這個指令對應的彙編程式碼如下:

// 將JavaThread::do_not_unlock_if_synchronized屬性存儲到%dl中
0x00007fffe101b770: mov    0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized屬性值為false
0x00007fffe101b777: movb   $0x0,0x2ad(%r15)

// 將Method*載入到%rbx中
0x00007fffe101b77f: mov    -0x18(%rbp),%rbx
// 將Method::_access_flags載入到%ecx中
0x00007fffe101b783: mov    0x28(%rbx),%ecx
// 檢查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test   $0x20,%ecx
// 如果方法不是同步方法,跳轉到----unlocked----
0x00007fffe101b78c: je     0x00007fffe101b970

main()方法為非同步方法,所以跳轉到unlocked,在unlocked邏輯中會執行一些釋放鎖的邏輯,對於我們本實例來說這不重要,我們直接看退棧的操作,如下:

// 將-0x8(%rbp)處保存的old stack pointer(saved rsp)取出來放到%rbx中
0x00007fffe101bac7: mov    -0x8(%rbp),%rbx

// 移除棧幀
// leave指令相當於:
//     mov %rbp, %rsp
//     pop %rbp
0x00007fffe101bacb: leaveq 
// 將返回地址彈出到%r13中
0x00007fffe101bacc: pop    %r13
// 設置%rsp為調用者的棧頂值
0x00007fffe101bace: mov    %rbx,%rsp
0x00007fffe101bad1: jmpq   *%r13 

最後的棧狀態如下圖所示。

其中的return address是C++語言的返回地址,接下來如何退出如上的一些棧幀及結束方法就是C++的事兒了。

公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源程式碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流