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

时间:2021-10-22 07:18:44

举个小实例,如下:

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字节码之前栈状态如下图所示。

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

下面我们来简单介绍一下解释执行的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()函数生成的例程入口。此时的栈帧状态如下图所示。

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

这里需要提示一下,因为使用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)

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

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

接着开辟传参空间,这个空间将会存放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,拉你入虚拟机群交流
第47篇-解释执行的Java方法调用native方法小实例  

  

  

  

 

  

第47篇-解释执行的Java方法调用native方法小实例的更多相关文章

  1. 第48篇-native方法调用解释执行的Java方法

    举一个native方法调用解释执行的Java方法的实例,如下: public class TestJNI { static { System.load("/media/mazhi/sourc ...

  2. Java中的native方法

    博客引用地址:Java中的native方法 今天花了两个小时把一份关于什么是Native Method的英文文章好好了读了一遍,以下是我依据原文的理解. 一. 什么是Native Method 简单地 ...

  3. java获取调用当前方法的方法名和行数

    java获取调用当前方法的方法名和行数String className = Thread.currentThread().getStackTrace()[2].getClassName();//调用的 ...

  4. Java本地方法(native方法)的实现

    Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能. 可以将 ...

  5. java 里面的 native 方法

    第一篇: 今天花了两个小时把一份关于什么是Native Method的英文文章好好了读了一遍,以下是我依据原文的理解. 一. 什么是Native Method   简单地讲,一个Native Meth ...

  6. java中的native方法和修饰符(转)

    Java中的native修饰符 今天偶然看代码,发现别人有这样写的方法,并且jar里面有几个dll文件,比较奇怪,于是把代码打开,发现如下写法. public native String GSMMod ...

  7. JNI调用native方法出现 java&period;lang&period;UnsatisfiedLinkError&colon; XXXclass&period;XXXmethod()异常的解决办法

    昨天拿到JNI的Android工程Demo,然后把demo整合到开发的主线工程上,发现调用JNI方法一直抛同一个异常 java.lang.UnsatisfiedLinkError: XXXclass. ...

  8. 【Java基础】8、java中的native方法

    native是与C++联合开发的时候用的!java自己开发不用的! 使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用.这些函 ...

  9. java 主类的main方法调用其他方法

    方法1:A a=new test().new A(); 内部类对象通过外部类的实例对象调用其内部类构造方法产生,如下: public class test{ class A{ void fA(){ S ...

随机推荐

  1. &lbrack;No000039&rsqb;操作系统Operating Systems用户级线程User Threads

    多进程是操作系统的基本图像 是否可以资源不动而切换指令序列? 进程 = 资源 + 指令执行序列 线程: 保留了并发的优点,避免了进程切换代价 实质就是映射表不变而PC 指针变 多个执行序列+ 一个地址 ...

  2. Tri-Training&colon; Exploiting Unlabeled Data Using Three Classifiers

    Abstract – In many practical data mining applications such as web page classification, unlabeled tra ...

  3. jquery闭包的使用

    <div id="divTest"> Test </div> <br /> <hr /> <div id="divT ...

  4. Excel 绘制图表,如何显示横轴的数据范围

    右键点击X坐标轴,然后选中“设置图表区域格式”,然后在“坐标轴选项”--“区域”处设置X轴范围. 备注,这种方式仅使用与第一列时日期时间类型的数据. 应用场景 当,选择有两列数据,第一列为横轴数据,第 ...

  5. Linux服务器集群系统&lpar;三&rpar;--转

    引用地址:http://www.linuxvirtualserver.org/zh/lvs3.html LVS集群中的IP负载均衡技术 章文嵩(wensong@linux-vs.org) 2002 年 ...

  6. centos6&period;4 安装erlang

    erlang官网: http://www.erlang.org 下载程序去年:

  7. Spark&lpar;Hive&rpar; SQL数据类型使用详解&lpar;Python&rpar;

    Spark SQL使用时需要有若干“表”的存在,这些“表”可以来自于Hive,也可以来自“临时表”.如果“表”来自于Hive,它的模式(列名.列类型等)在创建时已经确定,一般情况下我们直接通过Spar ...

  8. 【从0開始Tornado建站】群聊

    群聊的前台主要代码: {%block content%} <!--<p class='text-success h3'>測试版本号,每天凌晨4:00清水,enjoy it~~:-)& ...

  9. highlight高亮

    玩转正则之highlight高亮 2013-10-07 05:16 by 靖鸣君, 584 阅读, 3 评论, 收藏, 编辑 程序员在编写代码的时候少不了和字符串以及“查询”打交道,两者的交集中有一个 ...

  10. Warning&colon; The Copy Bundle Resources build phase contains this target&&num;39&semi;s Info&period;plist file &&num;39&semi;yintingting&lowbar;baisi&sol;Info&period;plist&&num;39&semi;&period;

    处理方法: The INFOPLIST_FILE build setting specifies the name of the Info.plist associated with your tar ...