zoukankan      html  css  js  c++  java
  • 第38篇解释方法之间的调用小实例

    这一篇我们介绍一下解释执行的main()方法调用解析执行的add()方法的小实例,这个例子如下:

    package com.classloading;
    
    public class TestInvokeMethod {
    	public int add(int a, int b) {
    		return a + b;
    	}
    
    	public static void main(String[] args) {
    		TestInvokeMethod tim = new TestInvokeMethod();
    		tim.add(2, 3);
    	}
    }
    

    通过Javac编译器编译为字节码文件,如下: 

    Constant pool:
       #1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
       #2 = Class              #17            // com/classloading/TestInvokeMethod
       #3 = Methodref          #2.#16         // com/classloading/TestInvokeMethod."<init>":()V
       #4 = Methodref          #2.#18         // com/classloading/TestInvokeMethod.add:(II)I
       #5 = Class              #19            // java/lang/Object
       #6 = Utf8               <init>
       #7 = Utf8               ()V
       #8 = Utf8               Code
       #9 = Utf8               LineNumberTable
      #10 = Utf8               add
      #11 = Utf8               (II)I
      #12 = Utf8               main
      #13 = Utf8               ([Ljava/lang/String;)V
      #14 = Utf8               SourceFile
      #15 = Utf8               TestInvokeMethod.java
      #16 = NameAndType        #6:#7          // "<init>":()V
      #17 = Utf8               com/classloading/TestInvokeMethod
      #18 = NameAndType        #10:#11        // add:(II)I
      #19 = Utf8               java/lang/Object
    {
      public com.classloading.TestInvokeMethod();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
    
      public int add(int, int);
        descriptor: (II)I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=3
             0: iload_1
             1: iload_2
             2: iadd
             3: ireturn
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=2, args_size=1
             0: new           #2                  // class com/classloading/TestInvokeMethod
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: aload_1
             9: iconst_2
            10: iconst_3
            11: invokevirtual #4                  // Method add:(II)I
            14: pop
            15: return
    }

    下面分几部分介绍调用相关的内容。

    1、C++函数调用main()方法

    现在我们从字节码索引为8的aload_1开始看,此时的栈帧状态如下:

    由于aload_1的tos_out为atos,所以在栈顶缓存的寄存器中会缓存有TestInvokeMethod实例的地址,当执行iconst_2时,会从atos进入。iconst_2指令的汇编如下: 

    //  aep
    push   %rax
    jmpq   // 跳转到下面那条指令执行
    
    // ...
    
    mov    $0x2,%eax // 指令的汇编代码
    

    由于iconst_2的tos_out为itos,所以在进入下一个指令时,会从iconst_3的tos_int为itos中进入,如下:  

    // iep
    push   %rax
    
    mov    $0x3,%eax
    

    接下来就是执行invokevirtual字节码指令了,此时的2已经压入了表达式栈,而3在%eax寄存器中做为栈顶缓存,但是invokevirtual的tos_in为vtos,所以从invokevirtual字节码指令的iep进入时会将%eax寄存器中的值也压入表达式栈中,最终的栈状态如下图所示。

     

    2、main()方法调用add()方法

    invokevirtual字节码指令在执行时,假设此字节码指令已经解析完成,也就是对应的ConstantPoolCacheEntry中已经保存了方法调用相关的信息,则执行的相关汇编代码如下:

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

    执行完如上的代码后,已经向相关的寄存器中存储了相关的值。相关的寄存器状态如下:

    rbx: 存储的是ConstantPoolCacheEntry::_f2属性的值
    rcx: 就是调用实例方法时的第一个参数this
    rdx: 存储的是ConstantPoolCacheEntry::_flags属性的值
    

    栈的状态如下图所示。

    需要注意的是return address也是一个例程的地址,是TemplateInterpreter::invoke_return_entry一维数组中类型为整数对应的下标存储的那个地址,因为调用add()方法返回的是整数类型。如何得出add()方法的返回类型呢?是从ConstantPoolCacheEntry的_flags的TosState中得出的。

    下面继续看invokevirtual字节码指令将要执行的汇编代码,如下:

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

    执行Method::_from_interpretered_entry例程,这个例程在之前详细介绍过,执行完成后会为add()方法创建栈帧,此时的栈状态如下图所示。

    执行iload_0与iload_1指令,由于连续出现了2个iload,所以是_fast_iload2,汇编如下:

    movzbl  0x1(%r13),%ebx
    neg     %rbx
    mov     (%r14,%rbx,8),%eax
    push    %rax
    movzbl  0x3(%r13),%ebx
    neg     %rbx
    mov     (%r14,%rbx,8),%eax
    

    注意,只有第1个变量压入了栈,第2个则存储到%eax中做为栈顶缓存。 

    调用iadd指令,由于tos_in为itos,所以汇编如下:

    mov    (%rsp),%edx
    add    $0x8,%rsp
    add    %edx,%eax
    

    最后结果缓存在%eax中。 

    3、退出add()方法

    执行ireturn字节码指令进行add()方法的退栈操作。对于实例来说,执行的相关汇编代码如下:

    // 将JavaThread::do_not_unlock_if_synchronized属性存储到%dl中
    0x00007fffe101b770: mov    0x2ad(%r15),%dl
    // 重置JavaThread::do_not_unlock_if_synchronized属性值为false
    0x00007fffe101b777: movb   $0x0,0x2ad(%r15)
     
    // 将Method*加载到%rbx中
    0x00007fffe101b77f: mov    -0x18(%rbp),%rbx
    // 将Method::_access_flags加载到%ecx中
    0x00007fffe101b783: mov    0x28(%rbx),%ecx
    // 检查Method::flags是否包含JVM_ACC_SYNCHRONIZED
    0x00007fffe101b786: test   $0x20,%ecx
    // 如果方法不是同步方法,跳转到----unlocked----
    0x00007fffe101b78c: je     0x00007fffe101b970

    unlocked处的汇编实现如下:  

    // 将-0x8(%rbp)处保存的old stack pointer(saved rsp)取出来放到%rbx中
    0x00007fffe101bac7: mov    -0x8(%rbp),%rbx
     
    // 移除栈帧
    // leave指令相当于:
    //     mov %rbp, %rsp
    //     pop %rbp
    0x00007fffe101bacb: leaveq 
    // 将返回地址弹出到%r13中
    0x00007fffe101bacc: pop    %r13
    // 设置%rsp为调用者的栈顶值
    0x00007fffe101bace: mov    %rbx,%rsp
    0x00007fffe101bad1: jmpq   *%r13

    执行leaveq指令进行退栈操作,此时的栈状态如下图所示。

    然后我们就要弹出返回地址,跳转到TemplateInterpreter::invoke_return_entry数组中保存的相关地址去执行对应的例程了。

    4、执行返回例程

    对于实例来说,传递的state为itos时生成的汇编代码如下:

    // 将-0x10(%rbp)存储到%rsp后,置空-0x10(%rbp)
    0x00007fffe1006ce0: mov    -0x10(%rbp),%rsp   // 更改rsp
    0x00007fffe1006ce4: movq   $0x0,-0x10(%rbp)   // 更改栈中特定位置的值
    // 恢复bcp和locals,使%r14指向本地变量表,%r13指向bcp
    0x00007fffe1006cec: mov    -0x38(%rbp),%r13
    0x00007fffe1006cf0: mov    -0x30(%rbp),%r14
     // 获取ConstantPoolCacheEntry的索引并加载到%ecx
    0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx     
    
     // 获取栈中-0x28(%rbp)的ConstantPoolCache并加载到%ecx
    0x00007fffe1006cf9: mov    -0x28(%rbp),%rbx   
    // shl是逻辑左移,获取字偏移
    0x00007fffe1006cfd: shl    $0x2,%ecx           
    // 获取ConstantPoolCacheEntry中的_flags属性值
    0x00007fffe1006d00: mov    0x28(%rbx,%rcx,8),%ebx
    // 获取_flags中的低8位中保存的参数大小
    0x00007fffe1006d04: and    $0xff,%ebx          
    
    // lea指令将地址加载到内存寄存器中,也就是恢复调用方法之前栈的样子
    0x00007fffe1006d0a: lea    (%rsp,%rbx,8),%rsp  
    
    // 跳转到下一指令执行
    0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx  
    0x00007fffe1006d13: add    $0x3,%r13
    0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
    0x00007fffe1006d21: jmpq   *(%r10,%rbx,8)
      
    

    如上的汇编代码也是执行的退栈操作,最主要的就是把在调用解释执行方法时压入的实参从栈中弹出,接着就是执行main()方法中invokevirtual中的下一条指令pop。此时的栈状态如下图所示。

    需要注意的是,此时的栈顶缓存中存储着调用add()方法的执行结果,那么在跳转到下一条指令pop时,必须要从pop的iep入口进入,这样就能正确的执行下去了。 

    5、退出main()方法 

    当执行pop指令时,会从iep入口进入,执行的汇编代码如下:

    // iep
    push   %rax
    
    // ...
    
    add    $0x8,%rsp

    由于main()方法调用add()方法不需要返回结果,所以对于main()方法来说,这个结果会从main()方法的表达式栈中弹出。下面接着执行return指令,这个指令对应的汇编代码如下:

    // 将JavaThread::do_not_unlock_if_synchronized属性存储到%dl中
    0x00007fffe101b770: mov    0x2ad(%r15),%dl
    // 重置JavaThread::do_not_unlock_if_synchronized属性值为false
    0x00007fffe101b777: movb   $0x0,0x2ad(%r15)
    
    // 将Method*加载到%rbx中
    0x00007fffe101b77f: mov    -0x18(%rbp),%rbx
    // 将Method::_access_flags加载到%ecx中
    0x00007fffe101b783: mov    0x28(%rbx),%ecx
    // 检查Method::flags是否包含JVM_ACC_SYNCHRONIZED
    0x00007fffe101b786: test   $0x20,%ecx
    // 如果方法不是同步方法,跳转到----unlocked----
    0x00007fffe101b78c: je     0x00007fffe101b970

    main()方法为非同步方法,所以跳转到unlocked,在unlocked逻辑中会执行一些释放锁的逻辑,对于我们本实例来说这不重要,我们直接看退栈的操作,如下:

    // 将-0x8(%rbp)处保存的old stack pointer(saved rsp)取出来放到%rbx中
    0x00007fffe101bac7: mov    -0x8(%rbp),%rbx
    
    // 移除栈帧
    // leave指令相当于:
    //     mov %rbp, %rsp
    //     pop %rbp
    0x00007fffe101bacb: leaveq 
    // 将返回地址弹出到%r13中
    0x00007fffe101bacc: pop    %r13
    // 设置%rsp为调用者的栈顶值
    0x00007fffe101bace: mov    %rbx,%rsp
    0x00007fffe101bad1: jmpq   *%r13 

    最后的栈状态如下图所示。

    其中的return address是C++语言的返回地址,接下来如何退出如上的一些栈帧及结束方法就是C++的事儿了。

    公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流 

     

      

      

      

      

  • 相关阅读:
    JSP脚本指令
    JSP编译指令——page、include
    c++基础(三):多态
    c++基础(一):数据类型和结构
    c++基础(二):成员he派生类
    python小算法(二)
    python的内存管理
    初识java之Mina(一)
    python的小爬虫的基本写法
    python小算法(一)
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15523953.html
Copyright © 2011-2022 走看看