• 第47篇解释执行的Java方法调用native方法小实例


    举个小实例,如下:

    public class TestJNI {
        static {
            // 程序在加载时,自动加载libdiaoyong.so库
            System.loadLibrary("diaoyong"); 
        }
        
        public static native int get();
      
        public static void main(String[] args) {        
            TestJNI.get();
        }
    }
    

    其字节码的实现如下:

    Constant pool:
       #1 = Methodref          #6.#18         // java/lang/Object."<init>":()V
       #2 = Methodref          #5.#19         // TestJNI.get:()I
       #3 = String             #20            // diaoyong
       #4 = Methodref          #21.#22        // java/lang/System.loadLibrary:(Ljava/lang/String;)V
       #5 = Class              #23            // TestJNI
       #6 = Class              #24            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               get
      #12 = Utf8               ()I
      #13 = Utf8               main
      #14 = Utf8               ([Ljava/lang/String;)V
      #15 = Utf8               <clinit>
      #16 = Utf8               SourceFile
      #17 = Utf8               TestJNI.java
      #18 = NameAndType        #7:#8          // "<init>":()V
      #19 = NameAndType        #11:#12        // get:()I
      #20 = Utf8               diaoyong
      #21 = Class              #25            // java/lang/System
      #22 = NameAndType        #26:#27        // loadLibrary:(Ljava/lang/String;)V
      #23 = Utf8               TestJNI
      #24 = Utf8               java/lang/Object
      #25 = Utf8               java/lang/System
      #26 = Utf8               loadLibrary
      #27 = Utf8               (Ljava/lang/String;)V
    {
      // ...
      public static native int get();
        descriptor: ()I
        flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=1, args_size=1
             0: invokestatic  #2                  // Method get:()I
             3: pop
             4: return
      // ...
    }

    native方法get()对应的本地函数的头文件TestJNI.h的实现如下:

    #include <jni.h>
    
    #ifndef _Included_TestJNI
    #define _Included_TestJNI
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv *, jclass);
    
    #ifdef __cplusplus
    }
    #endif
    #endif

    TestJNI.c文件的实现如下:

    #include <stdio.h> 
     
    #include "TestJNI.h" 
     
     
    JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv * env, jclass jc){
      printf("ok!You have successfully passed the Java call c\n");
      return 100; 
    } 
    

    为如上的本地方法生成libdiaoyong.so动态链接库,运行后会输出如下结果:

    ok!You have successfully passed the Java call c
    

    由于native方法本质上是C/C++函数,所以不会有对应的字节码。我们在main()方法中通过invokestatic字节码指令调用native方法,在执行invokestatic字节码之前栈状态如下图所示。

     

    下面我们来简单介绍一下解释执行的main()方法调用native方法get()的具体过程。

    调用的invokestatic字节码指令的汇编如下:

    0x00007fffe101c030: mov    %r13,-0x38(%rbp)
    0x00007fffe101c034: movzwl 0x1(%r13),%edx
    0x00007fffe101c039: mov    -0x28(%rbp),%rcx
    0x00007fffe101c03d: shl    $0x2,%edx
    0x00007fffe101c040: mov    0x10(%rcx,%rdx,8),%ebx
    0x00007fffe101c044: shr    $0x10,%ebx
    0x00007fffe101c047: and    $0xff,%ebx
    0x00007fffe101c04d: cmp    $0xb8,%ebx
    // 检查invokestatic=184的bytecode是否已经连接,如果已经连接就进行跳转 
    0x00007fffe101c053: je     0x00007fffe101c0f2
     
     
    // 调用InterpreterRuntime::resolve_invoke()函数对invokestatic=184的
    // 的bytecode进行连接,因为字节码指令还没有连接
    // ... 省略了解析invokestatic的汇编代码 
     
    // 将invokestatic x中的x加载到%edx中
    0x00007fffe101c0e6: movzwl 0x1(%r13),%edx
    // 将ConstantPoolCache的首地址存储到%rcx中
    0x00007fffe101c0eb: mov    -0x28(%rbp),%rcx
    // %edx中存储的是ConstantPoolCacheEntry项的索引,转换为字偏移
    0x00007fffe101c0ef: shl    $0x2,%edx
     
     
    // 获取ConstantPoolCache::_f1属性的值
    0x00007fffe101c0f2: mov    0x18(%rcx,%rdx,8),%rbx
    // 获取ConstantPoolCache::_flags属性的值
    0x00007fffe101c0f7: mov    0x28(%rcx,%rdx,8),%edx
     
     
    // 从flags中获取return type,也就是从_flags的高4位保存的TosState
    0x00007fffe101c0fb: shr    $0x1c,%edx
    // 将TemplateInterpreter::invoke_return_entry地址存储到%r10
    0x00007fffe101c0fe: movabs $0x7ffff73b5d00,%r10
    // 找到对应return type的invoke_return_entry的地址
    0x00007fffe101c108: mov    (%r10,%rdx,8),%rdx
    // 压入返回地址,这个返回地址就是通过invokestatic指令调用的函数的返回地址
    0x00007fffe101c10c: push   %rdx
     
     
    // 设置调用者栈顶
    0x00007fffe101c10d: lea    0x8(%rsp),%r13
    // 向栈中last_sp的位置保存调用者栈顶
    0x00007fffe101c112: mov    %r13,-0x10(%rbp)
     
    // 跳转到Method::_from_interpretered_entry入口去执行
    0x00007fffe101c116: jmpq   *0x58(%rbx)  

    根据ConstantCachePoolEntry中的信息来获取返回地址TemplateInterpreter::invoke_return_entry并压入栈中,然后就会跳转到Method::_from_interpretered_entry去执行,这个Method::_from_interpretered_entry保存的就是由

    InterpreterGenerator::generate_native_entry()函数生成的例程入口。此时的栈帧状态如下图所示。

    这里需要提示一下,因为使用invokestatic调用的get()方法没有参数,所以在-0x8(%rsp)的位置处并没有本地变量表。我们可以举一个需要本地变量表传递参数的例子,如下:

    public class TestLocalTable {	
    	public void get(int a,int b) {
    		// ...
    	}
    	
    	public static void main(String args[]) {
    		get(1,2);
    	}
    }

    在test()方法中调用实例方法get(),并且传递了2个参数,生成的字节码如下:

     public void test();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: iconst_1
             2: iconst_2
             3: invokevirtual #2                  // Method get:(II)V
             6: return
    

    实际上会在test()方法的表达式栈中压入3个实参,分别是接收者、常数1和常数2,而这3个参数会做为get()方法局部变量表的一部分存在,所以无论是invokevirtual还是invokestatic等字节码指令,在调用时,调用者的表达式栈中已经准备好了实参,这一部分将做为被调用者的局部变量表组成的一部分,这叫栈帧重叠,之前介绍过。

    开始执行native方法的例程,如下:

    // 在调用此例程时,各个寄存器中的值如下:
    // rbx: Method*
    // r13: sender sp
      
    // 将ConstMethod*存储到%rcx中
    0x00007fffe1014c00: mov    0x10(%rbx),%rcx 
    // 将参数的大小存储到%ecx中
    0x00007fffe1014c04: movzwl 0x2a(%rcx),%ecx 
    // 将返回地址弹出到%rax中
    0x00007fffe1014c08: pop    %rax 
      
    // rbx: Method*
    // rcx: size of parameters 通过上面的操作,将参数的大小存储到rcx寄存器中
    // r13: sender sp
    
      
    // 根据%rsp和参数大小计算参数的地址
    // %r14指向局部变量表第一个参数的位置
    // 注意,由于调用的是native方法,所以局部变量表只用来单纯传递参数,
    // 不用考虑本地变量,所以我们只开辟能存储参数大小的局部变量表即可
    0x00007fffe1014c09: lea    -0x8(%rsp,%rcx,8),%r14 
     
    // 为本地调用初始化两个8字节的数据,其中一个保存result_handler,一个保存oop temp
    0x00007fffe1014c0e: pushq  $0x0
    // oop temp对于静态的native方法来说,保存的可能是mirror,
    // 或者native方法调用结果为对象时,保存这个对象
    0x00007fffe1014c13: pushq  $0x0

    由于用来传递参数的局部变量表已经存在于栈中了,所以可通过lea -0x8(%rsp,%rcx,8),%r14汇编指令直接计算局部变量表第1个参数的地址,然后保存到%r14中。

    接下来为native方法生成栈帧,如下:

    0x00007fffe1014c18: push   %rax
      
    0x00007fffe1014c19: push   %rbp
    0x00007fffe1014c1a: mov    %rsp,%rbp
      
    0x00007fffe1014c1d: push   %r13
    0x00007fffe1014c1f: pushq  $0x0
    0x00007fffe1014c24: mov    0x10(%rbx),%r13
    0x00007fffe1014c28: lea    0x30(%r13),%r13
    0x00007fffe1014c2c: push   %rbx
    0x00007fffe1014c2d: mov    0x18(%rbx),%rdx
    0x00007fffe1014c31: test   %rdx,%rdx
    0x00007fffe1014c34: je     0x00007fffe1014c41
    0x00007fffe1014c3a: add    $0x90,%rdx
    0x00007fffe1014c41: push   %rdx
    0x00007fffe1014c42: mov    0x10(%rbx),%rdx
    0x00007fffe1014c46: mov    0x8(%rdx),%rdx
    0x00007fffe1014c4a: mov    0x18(%rdx),%rdx
    0x00007fffe1014c4e: push   %rdx
    0x00007fffe1014c4f: push   %r14
    0x00007fffe1014c51: pushq  $0x0
    0x00007fffe1014c56: pushq  $0x0
    0x00007fffe1014c5b: mov    %rsp,(%rsp)

    执行完如上汇编后的栈帧状态如下图所示。 

     

    接着开辟传参空间,这个空间将会存放native方法对应的本地函数需要的参数,如下:

    // 从栈帧中取出Method*存储到%rbx中
    0x00007fffe1014d87: mov    -0x18(%rbp),%rbx    
    // 获取ConstMethod*存储到%r11中
    0x00007fffe1014d8b: mov    0x10(%rbx),%r11    
     // 将方法参数的大小放到%r11d中 
    0x00007fffe1014d8f: movzwl 0x2a(%r11),%r11d   
    // 将%r11d中的内容左移3位,也就是算出方法参数需要占用的字节数
    0x00007fffe1014d94: shl    $0x3,%r11d      
    // 更新%rsp的值,为方法参数开辟存储参数的空间   
    0x00007fffe1014d98: sub    %r11,%rsp   
    // 对linux系统来说不起作用       
    0x00007fffe1014d9b: sub    $0x0,%rsp           
    // 必须是16字节边界(see amd64 ABI)
    0x00007fffe1014d9f: and    $0xfffffffffffffff0,%rsp 
    

    本地函数Java_TestJNI_get()虽然需要JNIEnv*和jclass参数,但是这2个参数是通过寄存器传递的,所以本实例不需要开辟任何传参空间。

    我们能够看到,一个解释执行的Java方法调用native方法时,需要有局部变量表来给native方法传递参数,然后在调用native方法对应的本地函数时,还需要开辟另外一个传参空间。现在局部变量表已经有值,而新开辟的空间还没有设置对应的值,接着就是调用signature_handler来根据局部变量表中存储的值设置新开辟空间中各个slot的值了。之所以这样做,就是因为解释执行的调用约定和本地函数的调用约定不同,也就是传参的约定不同。

    接下来是执行signature_handler,如下:

    // 调用Method::signature_handler函数
    0x00007fffe1014e40: callq  *%r11   
            
    // 重新获取Method
    0x00007fffe1014e43: mov    -0x18(%rbp),%rbx  
    // 将%rax中的result_handler存储到方法栈帧中,result_handler
    // 是执行signature_handler例程后的返回值,根据方法签名的返回类型获取的
    0x00007fffe1014e47: mov    %rax,0x18(%rbp)
    

    Method实例的第2个附加slot的signature_handler指向的例程用来消除Java解释器栈和C/C++栈调用约定的不同,将位于解析器栈中的参数适配到本地函数使用的C栈。生成的signature_handler与result_handler的例程如下:

    argument handler #56 for: static TestJNI.get()I (fingerprint = 341, 11 bytes generated)
      // 将result_handler的地址存储到%rax中
      0x00007f98e911c85d: movabs $0x7f98e900f1f6,%rax
      0x00007f98e911c867: retq   
    
     --- associated result handler ---
      0x00007f98e900f1f9: retq 
    

    result handler的实现非常简单,因为本地方法根据调用约定,会将int类型的返回值放到%rax中,我们只需要从%rax中获取值即可。

    接下来会执行如下汇编代码:

    // 将Method::access_flags存储到%r11d中
    0x00007fffe1014e4b: mov    0x28(%rbx),%r11d   
    // 判断是否为static本地方法,其中$0x8表示JVM_ACC_STATIC
    0x00007fffe1014e4f: test   $0x8,%r11d        
    // 如果为0,表示是非static方法,要跳转到-- L2 --
    0x00007fffe1014e56: je     0x00007fffe1014e74 
      
     
    // 执行这里代码时,说明方法是static方法
    // 如下4个mov指令将通过Method->ConstMehod->ConstantPool->mirror
    // 获取到java.lang.Class的oop
    0x00007fffe1014e5c: mov    0x10(%rbx),%r11
    0x00007fffe1014e60: mov    0x8(%r11),%r11
    0x00007fffe1014e64: mov    0x20(%r11),%r11
    0x00007fffe1014e68: mov    0x70(%r11),%r11
    // 将mirror存储到栈帧中,也就是oop temp这个slot位置
    0x00007fffe1014e6c: mov    %r11,0x10(%rbp)
    // 将mirror拷到%rsi中作为静态方法调用的第2个参数
    0x00007fffe1014e70: lea    0x10(%rbp),%rsi

    对于实例来说,get()方法是静态方法,所以会将mirror放到栈帧中的oop temp中。

    接下来执行如下汇编:

    // 获取Method::native_function的地址并存储到%rax中
    0x00007fffe1014e74: mov    0x60(%rbx),%rax   
    // %r11中存储的是SharedRuntime::native_method_throw_unsatisfied_link_error_entry()
    0x00007fffe1014e78: movabs $0x7ffff6a08f14,%r11 
    // 判断rax中的地址是否是native_method_throw_unsatisfied_link_error_entry的
    // 地址,如果是说明本地方法未绑定
    0x00007fffe1014e82: cmp    %r11,%rax
    // 如果不等于,即native方法已经绑定,跳转到----L3----
    0x00007fffe1014e85: jne    0x00007fffe1014f1b 
    //  ... 省略查找native_function的逻辑
    
    // 重新获取Method*到%rbx中
    
    0x00007fffe1014f13: mov    -0x18(%rbp),%rbx
    // 获取native_function的地址拷到%rax中
    0x00007fffe1014f17: mov    0x60(%rbx),%rax
    

    我们假设native_function已经存储到了Method实例的对应slot处,那么接下来就直接调用这个本地函数了,如下:

    // 将当前线程的JavaThread::jni_environment放入c_rarg0,也就是%rdi中
    0x00007fffe1014f1b: lea 0x210(%r15),%rdi
    
    // ...
    
    // 调用native_function本地函数
    0x00007fffe1014f4c: callq  *%rax              
      
    // ...
    
    // 如下4行代码是为了保存调用native_function函数后得到的结果,将
    // 结果存储到栈顶
    0x00007fffe1014f51: sub    $0x10,%rsp
    0x00007fffe1014f55: vmovsd %xmm0,(%rsp)
    0x00007fffe1014f5a: sub    $0x10,%rsp
    0x00007fffe1014f5e: mov    %rax,(%rsp)

    在调用native方法时,将JNIEnv*存储到c_rarg0,mirror存储到c_rarg1中,然后调用native方法的本地函数。根据C/C++函数的调用约定,如果返回浮点数,则会存储到%xmm0中,如果是对象或整数等类型,则会存储到%rax中。将%xmm0和%rax中的值压入栈中,最后会执行如下汇编代码:

    // 将栈顶的代表方法调用结果的数据pop到%rax和%xmm0寄存器中
    0x00007fffe101543c: mov    (%rsp),%rax
    0x00007fffe1015440: add    $0x10,%rsp
    0x00007fffe1015444: vmovsd (%rsp),%xmm0
    0x00007fffe1015449: add    $0x10,%rsp
    
    // 获取result_handler存储到%r11中
    0x00007fffe101544d: mov    0x18(%rbp),%r11
    
    0x00007fffe1015451: callq  *%r11           // 调用result_handler处理方法调用结果
     
    0x00007fffe1015454: mov    -0x8(%rbp),%r11 // 获取sender sp,开始恢复上一个Java栈帧
    0x00007fffe1015458: leaveq                 // 相当于指令mov %ebp,%esp和pop %ebp
    0x00007fffe1015459: pop    %rdi            // 获取return address
    0x00007fffe101545a: mov    %r11,%rsp       // 设置sender sp
    0x00007fffe101545d: jmpq   *%rdi           // 跳转到返回地址处继续执行
      

    调用result_handler处理方法调用结果,最终只是执行了retq指令,所以此次的callq和retq指令执行后没有对栈帧产生任何影响。  

    继续执行Interpreter::_invoke_return_entry例程,如下:

    // 将-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 
    // 注意这里会更改%rsp的指向,会将调用方表达式栈(被调用方局部变量表组成的一部分)中压入的、给调用的
    // 方法传递参数的值从表达式栈中弹出去,这样在解释执行的情况下,由调用方完成实参的清理工作
    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()方法中剩余指令了。  

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

      

      

      

     

      

  • 相关阅读:
    ASP.NET刷新页面的六种方法
    web安全攻防实践能力培养引导
    流程控制 ifwhilefor 语句
    迭代器和生成器
    闭包函数装饰器
    元组字典集合及内置方法
    字符编码与文件操作
    字符类型及内置方法
    初识函数
    递归匿名函数常用的内置方法
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15744878.html
Copyright © 2020-2023  润新知