第33篇-方法調用指令之invokeinterface

  • 2021 年 10 月 29 日
  • 筆記

invokevirtual位元組碼指令的模板定義如下: 

def(Bytecodes::_invokeinterface     , ubcp|disp|clvm|____, vtos, vtos, invokeinterface     , f1_byte      );

可以看到指令的生成函數為TemplateTable::invokeinterface(),在這個函數中首先會調用TemplateTable::prepare_invoke()函數,TemplateTable::prepare_invoke()函數生成的彙編程式碼如下:

第1部分:

0x00007fffe1022610: mov    %r13,-0x38(%rbp)
0x00007fffe1022614: movzwl 0x1(%r13),%edx
0x00007fffe1022619: mov    -0x28(%rbp),%rcx
0x00007fffe102261d: shl    $0x2,%edx
// 獲取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices
0x00007fffe1022620: mov    0x10(%rcx,%rdx,8),%ebx


// 獲取ConstantPoolCacheEntry中indices[b2,b1,constant pool index]中的b1
// 如果已經連接,那這個b1應該等於185,也就是invokeinterface指令的操作碼
0x00007fffe1022624: shr    $0x10,%ebx
0x00007fffe1022627: and    $0xff,%ebx
0x00007fffe102262d: cmp    $0xb9,%ebx
// 如果invokeinterface已經連接就跳轉到----resolved----
0x00007fffe1022633: je     0x00007fffe10226d2 

彙編程式碼的判斷邏輯與invokevirutal一致,這裡不在過多解釋。

第2部分:

由於方法還沒有解析,所以需要設置ConstantPoolCacheEntry中的資訊,這樣再一次調用時就不需要重新找調用相關的資訊了。生成的彙編如下:

// 執行如下彙編程式碼時,表示invokeinterface指令還沒有連接,也就是ConstantPoolCacheEntry中
// 還沒有保存調用相關的資訊
  
// 通過調用call_VM()函數生成如下彙編,通過這些彙編
// 調用InterpreterRuntime::resolve_invoke()函數
// 將bytecode存儲到%ebx中
0x00007fffe1022639: mov    $0xb9,%ebx           
// 通過MacroAssembler::call_VM()來調用InterpreterRuntime::resolve_invoke()
0x00007fffe102263e: callq  0x00007fffe1022648   
0x00007fffe1022643: jmpq   0x00007fffe10226c6
0x00007fffe1022648: mov    %rbx,%rsi
0x00007fffe102264b: lea    0x8(%rsp),%rax
0x00007fffe1022650: mov    %r13,-0x38(%rbp)
0x00007fffe1022654: mov    %r15,%rdi
0x00007fffe1022657: mov    %rbp,0x200(%r15)
0x00007fffe102265e: mov    %rax,0x1f0(%r15)
0x00007fffe1022665: test   $0xf,%esp
0x00007fffe102266b: je     0x00007fffe1022683
0x00007fffe1022671: sub    $0x8,%rsp
0x00007fffe1022675: callq  0x00007ffff66ae13a
0x00007fffe102267a: add    $0x8,%rsp
0x00007fffe102267e: jmpq   0x00007fffe1022688
0x00007fffe1022683: callq  0x00007ffff66ae13a
0x00007fffe1022688: movabs $0x0,%r10
0x00007fffe1022692: mov    %r10,0x1f0(%r15)
0x00007fffe1022699: movabs $0x0,%r10
0x00007fffe10226a3: mov    %r10,0x200(%r15)
0x00007fffe10226aa: cmpq   $0x0,0x8(%r15)
0x00007fffe10226b2: je     0x00007fffe10226bd
0x00007fffe10226b8: jmpq   0x00007fffe1000420
0x00007fffe10226bd: mov    -0x38(%rbp),%r13
0x00007fffe10226c1: mov    -0x30(%rbp),%r14
0x00007fffe10226c5: retq   

// 結束MacroAssembler::call_VM()函數
// 將invokeinterface x中的x載入到%edx中
0x00007fffe10226c6: movzwl 0x1(%r13),%edx
// 將ConstantPoolCache的首地址存儲到%rcx中
0x00007fffe10226cb: mov    -0x28(%rbp),%rcx
// %edx中存儲的是ConstantPoolCacheEntry項的索引,轉換為位元組偏移,因為
// 一個ConstantPoolCacheEntry項佔用4個字 0x00007fffe10226cf: shl $0x2,%edx

與invokevirtual的實現類似,這裡仍然在方法沒有解釋時調用InterpreterRuntime::resolve_invoke()函數進行方法解析,後面我們也詳細介紹一下InterpreterRuntime::resolve_invoke()函數的實現。

在調用完resolve_invoke()函數後,會將調用相信的資訊存儲到CallInfo實例info中。所以在調用的InterpreterRuntime::resolve_invoke()函數的最後會有如下的實現:

switch (info.call_kind()) {
  case CallInfo::direct_call: // 直接調用
    cache_entry(thread)->set_direct_call(
          bytecode,
          info.resolved_method());
    break;
  case CallInfo::vtable_call: // vtable分派
    cache_entry(thread)->set_vtable_call(
          bytecode,
          info.resolved_method(),
          info.vtable_index());
    break;
  case CallInfo::itable_call: // itable分派
    cache_entry(thread)->set_itable_call(
          bytecode,
          info.resolved_method(),
          info.itable_index());
    break;
  default:  ShouldNotReachHere();
}

之前已經介紹過vtable分派,現在看一下itable分派。

當為itable分派時,會調用set_itable_call()函數設置ConstantPoolCacheEntry中的相關資訊,這個函數的實現如下:

void ConstantPoolCacheEntry::set_itable_call(
 Bytecodes::Code   invoke_code,
 methodHandle      method,
 int               index
) {

  InstanceKlass* interf = method->method_holder();
  // interf一定是介面,method一定是非final方法
  set_f1(interf); // 對於itable,則_f1為InstanceKlass
  set_f2(index);
  set_method_flags(as_TosState(method->result_type()),
                   0,  // no option bits
                   method()->size_of_parameters());
  set_bytecode_1(Bytecodes::_invokeinterface);
}

ConstantPoolCacheEntry中存儲的資訊為:

  • bytecode存儲到了_f2欄位上,這樣當這個欄位有值時表示已經對此方法完成了解析;
  • _f1欄位存儲聲明方法的介面類,也就是_f1是指向表示介面的Klass實例的指針;
  • _f2表示_f1介面類對應的方法表中的索引,如果是final方法,則存儲指向Method實例的指針。

解析完成後ConstantPoolCacheEntry中的各個項如下圖所示。

第3部分:

如果invokeinterface位元組碼指令已經解析,則直接跳轉到resolved執行,否則調用resolve_invoke進行解析,解析完成後也會接著執行resolved處的邏輯,如下:

// **** resolved ****
// resolved的定義點,到這裡說明invokeinterface位元組碼已經連接


// 執行完如上彙編後暫存器的值如下:
// %edx:ConstantPoolCacheEntry index
// %rcx:ConstantPoolCache

// 獲取到ConstantPoolCacheEntry::_f1
// 在計算時,因為ConstantPoolCacheEntry在ConstantPoolCache
// 之後保存,所以ConstantPoolCache為0x10,而
// _f1還要偏移0x8,這樣總偏移就是0x18
0x00007fffe10226d2: mov    0x18(%rcx,%rdx,8),%rax  
// 獲取ConstantPoolCacheEntry::_f2屬性
0x00007fffe10226d7: mov    0x20(%rcx,%rdx,8),%rbx
// 獲取ConstantPoolCacheEntry::_flags屬性
0x00007fffe10226dc: mov    0x28(%rcx,%rdx,8),%edx


// 執行如上彙編後暫存器的值如下:
// %rax:ConstantPoolCacheEntry::_f1
// %rbx:ConstantPoolCacheEntry::_f2
// %edx:ConstantPoolCacheEntry::_flags

// 將flags移動到ecx中
0x00007fffe10226e0: mov    %edx,%ecx
// 從ConstantPoolCacheEntry::_flags中獲取參數大小
0x00007fffe10226e2: and    $0xff,%ecx  
// 讓%rcx指向recv           
0x00007fffe10226e8: mov    -0x8(%rsp,%rcx,8),%rcx 
// 暫時用%r13d保存ConstantPoolCacheEntry::_flags屬性
0x00007fffe10226ed: mov    %edx,%r13d  
// 從_flags的高4位保存的TosState中獲取方法返回類型           
0x00007fffe10226f0: shr    $0x1c,%edx
// 將TemplateInterpreter::invoke_return_entry地址存儲到%r10
0x00007fffe10226f3: movabs $0x7ffff73b63e0,%r10
// %rdx保存的是方法返回類型,計算返回地址
// 因為TemplateInterpreter::invoke_return_entry是數組,
// 所以要找到對應return type的入口地址
0x00007fffe10226fd: mov    (%r10,%rdx,8),%rdx
// 獲取結果處理函數TemplateInterpreter::invoke_return_entry的地址並壓入棧中
0x00007fffe1022701: push   %rdx                   

// 恢復ConstantPoolCacheEntry::_flags中%edx
0x00007fffe1022702: mov    %r13d,%edx   
// 還原bcp          
0x00007fffe1022705: mov    -0x38(%rbp),%r13

在TemplateTable::invokeinterface()函數中首先會調用prepare_invoke()函數,上面的彙編就是由這個函數生成的。調用完後各個暫存器的值如下:

rax: interface klass (from f1)
rbx: itable index (from f2)
rcx: receiver
rdx: flags

然後接著執行TemplateTable::invokeinterface()函數生成的彙編片段,如下:

第4部分:

// 將ConstantPoolCacheEntry::_flags的值存儲到%r14d中
0x00007fffe1022709: mov    %edx,%r14d
// 檢測一下_flags中是否含有is_forced_virtual_shift標識,如果有,
// 表示調用的是Object類中的方法,需要通過vtable進行動態分派
0x00007fffe102270c: and    $0x800000,%r14d
0x00007fffe1022713: je     0x00007fffe1022812  // 跳轉到----notMethod----

// ConstantPoolCacheEntry::_flags存儲到%eax
0x00007fffe1022719: mov    %edx,%eax
// 測試調用的方法是否為final
0x00007fffe102271b: and    $0x100000,%eax
0x00007fffe1022721: je     0x00007fffe1022755 // 如果為非final方法,則跳轉到----notFinal----


// 下面彙編程式碼是對final方法的處理

// 對於final方法來說,rbx中存儲的是Method*,也就是ConstantPoolCacheEntry::_f2指向Method*
// 跳轉到Method::from_interpreted處執行即可
0x00007fffe1022727: cmp    (%rcx),%rax
// ... 省略統計相關的程式碼
// 設置調用者棧頂並存儲
0x00007fffe102274e: mov    %r13,-0x10(%rbp)
// 跳轉到Method::_from_interpreted_entry
0x00007fffe1022752: jmpq   *0x58(%rbx)   // 調用final方法


// **** notFinal ****

// 調用load_klass()函數生成如下2句彙編
// 查看recv這個oop對應的Klass,存儲到%eax中
0x00007fffe1022755: mov    0x8(%rcx),%eax  
// 調用decode_klass_not_null()函數生成的彙編   
0x00007fffe1022758: shl    $0x3,%rax  

      
// 省略統計相關的程式碼

// 調用lookup_virtual_method()函數生成如下這一句彙編
0x00007fffe10227fe: mov    0x1b8(%rax,%rbx,8),%rbx

// 設置調用者棧頂並存儲
0x00007fffe1022806: lea    0x8(%rsp),%r13
0x00007fffe102280b: mov    %r13,-0x10(%rbp)

// 跳轉到Method::_from_interpreted_entry
0x00007fffe102280f: jmpq *0x58(%rbx) 

如上彙編包含了對final和非final方法的分派邏輯。對於final方法來說,由於ConstantPoolCacheEntry::_f2中存儲的就是指向被調用的Method實例,所以非常簡單;對於非final方法來說,需要通過itable實現動態分派。分派的關鍵一個彙編語句如下:

mov    0x1b8(%rax,%rbx,8),%rbx

如上是vtable的動態分派邏輯,這個分派邏輯比較簡單,之前也介紹過,這裡不再介紹。

如果跳轉到notMethod後,那就需要通過itable進行方法的動態分派了,我們看一下這部分的實現邏輯:

第5部分:

// **** notMethod ****

// 讓%r14指向本地變數表
0x00007fffe1022812: mov    -0x30(%rbp),%r14  
// %rcx中存儲的是receiver,%edx中保存的是Klass
0x00007fffe1022816: mov    0x8(%rcx),%edx  
// LogKlassAlignmentInBytes=0x03,進行對齊處理
0x00007fffe1022819: shl    $0x3,%rdx

// 如下程式碼是調用如下函數生成的:
__ lookup_interface_method(rdx, // inputs: rec. class
rax, // inputs: interface
rbx, // inputs: itable index
rbx, // outputs: method
r13, // outputs: scan temp. reg
no_such_interface);

 
// 獲取vtable的起始地址  
// %rdx中存儲的是recv.Klass,獲取Klass中vtable_length屬性的值
0x00007fffe10228c1: mov    0x118(%rdx),%r13d  

// %rdx:recv.Klass,%r13為vtable_length,最後r13指向第一個itableOffsetEntry
// 加一個常量0x1b8是因為vtable之前是InstanceKlass
// 其中base=%rdx=recv_klass,index=%r13=scan_temp,scala=8=times_vte_scale,disp=0x1b8=vtable_base
0x00007fffe10228c8: lea    0x1b8(%rdx,%r13,8),%r13 
// 其中base=%rdx=recv_klass,index=%rbx=itable_index,scala=8=Address::times_ptr,disp=itentry_off
0x00007fffe10228d0: lea    (%rdx,%rbx,8),%rdx   

// 獲取itableOffsetEntry::_interface並與%rax比較,%rax中存儲的是要查找的介面
0x00007fffe10228d4: mov    0x0(%r13),%rbx
0x00007fffe10228d8: cmp    %rbx,%rax
// 如果相等,則直接跳轉到---- found_method ----
0x00007fffe10228db: je     0x00007fffe10228f3

// **** search ****

// 檢測%rbx中的值是否為NULL,如果為NULL,那就說明receiver沒有實現要查詢的介面
0x00007fffe10228dd: test   %rbx,%rbx
// 跳轉到---- L_no_such_interface ----
0x00007fffe10228e0: je     0x00007fffe1022a8c

0x00007fffe10228e6: add    $0x10,%r13

0x00007fffe10228ea: mov    0x0(%r13),%rbx
0x00007fffe10228ee: cmp    %rbx,%rax
// 如果還是沒有在itableOffsetEntry中找到介面類,
// 則跳轉到search繼續進行查找
0x00007fffe10228f1: jne    0x00007fffe10228dd // 跳轉到---- search ----

// **** found_method ****

// 已經找到匹配介面的itableOffsetEntry,獲取
// itableOffsetEntry的offset屬性並存儲到%r13d中
0x00007fffe10228f3: mov    0x8(%r13),%r13d
// 通過recv_klass進行偏移後找到此介面下聲明的一系列方法的開始位置
0x00007fffe10228f7: mov    (%rdx,%r13,1),%rbx

我們需要重點關注itable的分派邏輯,首先生成了如下彙編:

mov    0x118(%rdx),%r13d 

%rdx中存儲的是recv.Klass,獲取Klass中vtable_length屬性的值,有了這個值,我們就可以計算出vtable的大小,從而計算出itable的開始地址。

接著執行了如下彙編: 

lea    0x1b8(%rdx,%r13,8),%r13

其中的0x1b8表示的是recv.Klass首地址到vtable的距離,這樣最終的%r13指向的是itable的首地址。如下圖所示。

 

後面我們就可以開始循環從itableOffsetEntry中查找匹配的介面了, 如果找到則跳轉到found_method,在found_method中,要找到對應的itableOffsetEntry的offset,這個offset指明了介面中定義的方法的存儲位置相對於Klass的偏移量,也就是找到介面對應的第一個itableMethodEntry,因為%rbx中已經存儲了itable的索引,所以根據這個索引直接定位對應的itableMethodEntry即可,我們現在看如下的2個彙編語句:

lea    (%rdx,%rbx,8),%rdx 
...
mov    (%rdx,%r13,1),%rbx

當執行到如上的第2個彙編時,%r13存儲的是相對於Klass實例的偏移,而%rdx在執行第1個彙編時存儲的是Klass首地址,然後根據itable索引加上了相對於第1個itableMethodEntry的偏移,這樣就找到了對應的itableMethodEntry。  

第6部分:

在執行如下彙編時,各個暫存器的值如下:

rbx: Method* to call
rcx: receiver

生成的彙編程式碼如下:

0x00007fffe10228fb: test   %rbx,%rbx
// 如果本來應該存儲Method*的%rbx是空,則表示沒有找到
// 這個方法,跳轉到---- no_such_method ----
0x00007fffe10228fe: je     0x00007fffe1022987 

// 保存調用者的棧頂指針
0x00007fffe1022904: lea    0x8(%rsp),%r13  
0x00007fffe1022909: mov    %r13,-0x10(%rbp)
// 跳轉到Method::from_interpreted指向的常式並執行
0x00007fffe102290d: jmpq   *0x58(%rbx)  


// 省略should_not_reach_here()函數生成的彙編


// **** no_such_method ****
// 當沒有找到方法時,會跳轉到這裡執行

// 彈出調用prepare_invoke()函數壓入的返回地址
0x00007fffe1022987: pop    %rbx
// 恢復讓%r13指向bcp
0x00007fffe1022988: mov    -0x38(%rbp),%r13
// 恢復讓%r14指向本地變數表
0x00007fffe102298c: mov    -0x30(%rbp),%r14


// ... 省略通過call_VM()函數生成的彙編來調用InterpreterRuntime::throw_abstractMethodError()函數
// ... 省略調用should_not_reach_here()函數生成的彙編程式碼

// **** no_such_interface ****

// 當沒有找到匹配的介面時執行的彙編程式碼
0x00007fffe1022a8c: pop    %rbx
0x00007fffe1022a8d: mov    -0x38(%rbp),%r13
0x00007fffe1022a91: mov    -0x30(%rbp),%r14

// ... 省略通過call_VM()函數生成的彙編程式碼來調用InterpreterRuntime::throw_IncompatibleClassChangeError()函數
// ... 省略調用should_not_reach_here()函數生成的彙編程式碼

對於一些異常的處理這裡就不過多介紹了,有興趣的可以看一下相關彙編程式碼的實現。 

推薦閱讀:

第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()方法的執行

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

第32篇-解析interfacevirtual位元組碼指令