第31篇-方法調用指令之invokevirtual


invokevirtual字節碼指令的模板定義如下:

def(Bytecodes::_invokevirtual       , ubcp|disp|clvm|____, vtos, vtos, invokevirtual       , f2_byte      );

生成函數為invokevirtual,傳遞的參數為f2_byte,也就是2,如果為2時,ConstantPoolCacheEntry::indices中取[b2,b1,index]的b2。調用的TemplateTable::invokevirtual()函數的實現如下:

void TemplateTable::invokevirtual(int byte_no) {
  prepare_invoke(byte_no,
                 rbx,    // method or vtable index
                 noreg,  // unused itable index
                 rcx,    // recv
                 rdx);   // flags

  // rbx: index
  // rcx: receiver
  // rdx: flags
  invokevirtual_helper(rbx, rcx, rdx);
} 

先調用prepare_invoke()函數,后調用invokevirtual_helper()函數來生成invokevirtual字節碼指令對應的匯編代碼。

1、TemplateTable::prepare_invoke()函數

調用TemplateTable::prepare_invoke()函數生成的匯編代碼比較多,所以我們分三部分進行查看。

第1部分:

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直接執行方法調用操作。

第2部分:

// 調用InterpreterRuntime::resolve_invoke()函數,因為指令還沒有連接
// 將bytecode為182的指令移動到%ebx中
0x00007fffe1021fb9: mov    $0xb6,%ebx 

// 通過調用MacroAssembler::call_VM()函數來調用
// InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode)函數
// 進行方法連接
0x00007fffe1021fbe: callq  0x00007fffe1021fc8  
0x00007fffe1021fc3: jmpq   0x00007fffe1022046   // 跳轉到----E----
// 准備第2個參數,也就是bytecode
0x00007fffe1021fc8: mov    %rbx,%rsi 
0x00007fffe1021fcb: lea    0x8(%rsp),%rax
0x00007fffe1021fd0: mov    %r13,-0x38(%rbp)
0x00007fffe1021fd4: mov    %r15,%rdi
0x00007fffe1021fd7: mov    %rbp,0x200(%r15)
0x00007fffe1021fde: mov    %rax,0x1f0(%r15)
0x00007fffe1021fe5: test   $0xf,%esp
0x00007fffe1021feb: je     0x00007fffe1022003
0x00007fffe1021ff1: sub    $0x8,%rsp
0x00007fffe1021ff5: callq  0x00007ffff66ac528
0x00007fffe1021ffa: add    $0x8,%rsp
0x00007fffe1021ffe: jmpq   0x00007fffe1022008
0x00007fffe1022003: callq  0x00007ffff66ac528
0x00007fffe1022008: movabs $0x0,%r10
0x00007fffe1022012: mov    %r10,0x1f0(%r15)
0x00007fffe1022019: movabs $0x0,%r10
0x00007fffe1022023: mov    %r10,0x200(%r15)
0x00007fffe102202a: cmpq   $0x0,0x8(%r15)
0x00007fffe1022032: je     0x00007fffe102203d
0x00007fffe1022038: jmpq   0x00007fffe1000420
0x00007fffe102203d: mov    -0x38(%rbp),%r13
0x00007fffe1022041: mov    -0x30(%rbp),%r14
0x00007fffe1022045: retq   
// 結束MacroAssembler::call_VM()函數的調用

// **** E ****
// 將invokevirtual x中的x加載到%edx中,也就是ConstantPoolCacheEntry的索引
0x00007fffe1022046: movzwl 0x1(%r13),%edx   

// 將ConstantPoolCache的首地址存儲到%rcx中  
0x00007fffe102204b: mov    -0x28(%rbp),%rcx   

 // %edx中存儲的是ConstantPoolCacheEntry index,轉換為字偏移
0x00007fffe102204f: shl    $0x2,%edx    

方法連接的邏輯和之前介紹的字段的連接邏輯類似,都是完善ConstantPoolCache中對應的ConstantPoolCacheEntry添加相關信息。

調用InterpreterRuntime::resolve_invoke()函數進行方法連接,這個函數的實現比較多,我們在下一篇中詳細介紹。連接完成后ConstantPoolCacheEntry中的各個項如下圖所示。

所以對於invokevirtual來說,通過vtable進行方法的分發,在ConstantPoolCacheEntry中,_f1字段沒有使用,而對_f2字段來說,如果調用的是非final的virtual方法,則保存的是目標方法在vtable中的索引編號,如果是virtual final方法,則_f2字段直接指向目標方法的Method實例。

第3部分:

// **** 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  

TemplateInterpreter::invoke_return_entry保存了一段例程的入口,這段例程在后面會詳細介紹。

執行完如上的代碼后,已經向相關的寄存器中存儲了相關的值。相關的寄存器狀態如下:

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

棧的狀態如下圖所示。

棧中壓入了TemplateInterpreter::invoke_return_entry的返回地址。 

2、TemplateTable::invokevirtual_helper()函數

調用TemplateTable::invokevirtual_helper()函數生成的代碼如下:

// 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) 

對於final方法來說,其實沒有動態分派,所以也不需要通過vtable進行目標查找。調用時的棧如下圖所示。

如下代碼是通過vtable查找動態分派需要調用的方法入口 。

// **** notFinal ****

// invokevirtual指令調用的如果是非final方法,直接跳轉到這里
// %rcx中存儲的是receiver,用oop來表示。通過oop獲取Klass
0x00007fffe10220c0: mov    0x8(%rcx),%eax  

// 調用MacroAssembler::decode_klass__not_null()函數生成下面的一個匯編代碼
0x00007fffe10220c3: shl    $0x3,%rax       // LogKlassAlignmentInBytes=0x03

// 省略統計相關代碼部分


// %rax中存儲的是recv_klass
// %rbx中存儲的是vtable_index,
// 而0x1b8為InstanceKlass::vtable_start_offset()*wordSize+vtableEntry::method_offset_in_bytes(),
// 其實就是通過動態分派找到需要調用的Method*並存儲到%rbx中
0x00007fffe1022169: mov 0x1b8(%rax,%rbx,8),%rbx

// 設置調用者的棧頂地址並保存
0x00007fffe1022171: lea    0x8(%rsp),%r13
0x00007fffe1022176: mov    %r13,-0x10(%rbp)

// 跳轉到Method::_from_interpreted_entry處執行
0x00007fffe102217a: jmpq   *0x58(%rbx)

理解如上代碼時需要知道vtable方法分派以及vtable在InstanceKlass中的布局,這在《深入剖析Java虛擬機:源碼剖析與實例詳解》一書中詳細介紹過,這里不再介紹。  

跳轉到Method::_from_interpretered_entry保存的例程處執行,也就是以解釋執行運行invokevirtual字節碼指令調用的目標方法,關於Method::_from_interpretered_entry保存的例程的邏輯在第6篇、第7篇、第8篇中詳細介紹過,這里不再介紹。

如上的匯編語句 mov 0x1b8(%rax,%rbx,8),%rbx 是通過調用調用lookup_virtual_method()函數生成的,此函數將vtable_entry_addr加載到%rbx中,實現如下:

void MacroAssembler::lookup_virtual_method(Register recv_klass,
                                           RegisterOrConstant vtable_index,
                                           Register method_result) {
  const int base = InstanceKlass::vtable_start_offset() * wordSize;
  Address vtable_entry_addr(recv_klass,
                            vtable_index,
			    Address::times_ptr,
                            base + vtableEntry::method_offset_in_bytes());
  movptr(method_result, vtable_entry_addr);
}

其中的vtable_index取的就是ConstantPoolCacheEntry::_f2屬性的值。

最后還要說一下,如上生成的一些匯編代碼中省略了統計相關的執行邏輯,這里統計相關的代碼也是非常重要的,它會輔助進行編譯,所以后面我們還會介紹這些統計相關的邏輯。

參考文章:Java字節碼里的invoke操作&&編譯時的靜態綁定與動態綁定

推薦閱讀:

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

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

第3篇-CallStub新棧幀的創建

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

第5篇-調用Java方法后彈出棧幀及處理返回結果

第6篇-Java方法新棧幀的創建

第7篇-為Java方法創建棧幀

第8篇-dispatch_next()函數分派字節碼

第9篇-字節碼指令的定義

第10篇-初始化模板表

第11篇-認識Stub與StubQueue

第12篇-認識CodeletMark

第13篇-通過InterpreterCodelet存儲機器指令片段

第14篇-生成重要的例程

第15章-解釋器及解釋器生成器

第16章-虛擬機中的匯編器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

第19篇-加載與存儲指令(1)

第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)

第21篇-加載與存儲指令之iload、_fast_iload等(3)

第22篇-虛擬機字節碼之運算指令

第23篇-虛擬機字節碼指令之類型轉換

第24篇-虛擬機對象操作指令之getstatic

第25篇-虛擬機對象操作指令之getfield

第26篇-虛擬機對象操作指令之putstatic

第27篇-虛擬機字節碼指令之操作數棧管理指令

第28篇-虛擬機字節碼指令之控制轉移指令

第29篇-調用Java主類的main()方法

第30篇-main()方法的執行

  

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM