第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()函數生成的彙編程式碼
對於一些異常的處理這裡就不過多介紹了,有興趣的可以看一下相關彙編程式碼的實現。
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
第13篇-通過InterpreterCodelet存儲機器指令片段
第20篇-載入與存儲指令之ldc與_fast_aldc指令(2)
第21篇-載入與存儲指令之iload、_fast_iload等(3)