zoukankan      html  css  js  c++  java
  • 第33篇-方法调用指令之invokeinterface

    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字节码指令

  • 相关阅读:
    变量、内存区域、MDK文件(map、htm)
    全双工与半双工的区别
    4G网络 LTE、 FDD 和TD网络格式区别
    国内4G频段划分
    Nordic老版官网介绍(2018-11-30停止更新)
    无线通信模组产业链及竞争格局分析
    LBS 与 GPS 定位之间的区别
    99%的人都理解错了HTTP中GET与POST的区别(转自知乎)
    goto 的用法
    C语言字节对齐 __align(),__attribute((aligned (n))),#pragma pack(n)
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15479434.html
Copyright © 2011-2022 走看看