zoukankan      html  css  js  c++  java
  • 第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()方法的执行

      

     

  • 相关阅读:
    前端主页
    配置站点
    前台
    数据库配置
    后台:Django项目创建
    虚拟环境的搭建
    pip安装源
    AngularJS Scope(作用域)
    scala中的匿名函数 ==> 简单示例
    scala中的内部类 ==> 简单示例
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15469382.html
Copyright © 2011-2022 走看看