专注虚拟机与编译器研究

第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,拉你入虚拟机群交流
  

  

  

 

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

posted on 2021-12-29 14:37  鸠摩(马智)  阅读(644)  评论(0编辑  收藏  举报

导航