第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,拉你入虚拟机群交流
第47篇-解释执行的Java方法调用native方法小实例的更多相关文章
- 第48篇-native方法调用解释执行的Java方法
举一个native方法调用解释执行的Java方法的实例,如下: public class TestJNI { static { System.load("/media/mazhi/sourc ...
- Java中的native方法
博客引用地址:Java中的native方法 今天花了两个小时把一份关于什么是Native Method的英文文章好好了读了一遍,以下是我依据原文的理解. 一. 什么是Native Method 简单地 ...
- java获取调用当前方法的方法名和行数
java获取调用当前方法的方法名和行数String className = Thread.currentThread().getStackTrace()[2].getClassName();//调用的 ...
- Java本地方法(native方法)的实现
Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能. 可以将 ...
- java 里面的 native 方法
第一篇: 今天花了两个小时把一份关于什么是Native Method的英文文章好好了读了一遍,以下是我依据原文的理解. 一. 什么是Native Method 简单地讲,一个Native Meth ...
- java中的native方法和修饰符(转)
Java中的native修饰符 今天偶然看代码,发现别人有这样写的方法,并且jar里面有几个dll文件,比较奇怪,于是把代码打开,发现如下写法. public native String GSMMod ...
- JNI调用native方法出现 java.lang.UnsatisfiedLinkError: XXXclass.XXXmethod()异常的解决办法
昨天拿到JNI的Android工程Demo,然后把demo整合到开发的主线工程上,发现调用JNI方法一直抛同一个异常 java.lang.UnsatisfiedLinkError: XXXclass. ...
- 【Java基础】8、java中的native方法
native是与C++联合开发的时候用的!java自己开发不用的! 使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用.这些函 ...
- java 主类的main方法调用其他方法
方法1:A a=new test().new A(); 内部类对象通过外部类的实例对象调用其内部类构造方法产生,如下: public class test{ class A{ void fA(){ S ...
随机推荐
- 大数据学习day25------spark08-----1. 读取数据库的形式创建DataFrame 2. Parquet格式的数据源 3. Orc格式的数据源 4.spark_sql整合hive 5.在IDEA中编写spark程序(用来操作hive) 6. SQL风格和DSL风格以及RDD的形式计算连续登陆三天的用户
1. 读取数据库的形式创建DataFrame DataFrameFromJDBC object DataFrameFromJDBC { def main(args: Array[String]): U ...
- MySQL学习(一)——创建新用户、数据库、授权
一.创建用户 1.登录mysql mysql -u root -p 2.创建本地用户>/font> use mysql; //选择mysql数据库 create user 'test'@' ...
- Linux基础命令---ntpq查询时间服务器
ntpq ntpq指令使用NTP模式6数据包与NTP服务器通信,能够在允许的网络上查询的兼容的服务器.它以交互模式运行,或者通过命令行参数运行. 此命令的适用范围:RedHat.RHEL.Ubuntu ...
- ybatis中查询出多个以key,value的属性记录,封装成一个map返回的方法
可以采用值做映射,也可以不采用映射方式 <resultMap id="configMap" type="java.util.Map" > <r ...
- oralce 存储过程传入 record 类型的参数?
先定义一个 package , package中含有一个 record 类型的变量 create or replace package pkg_record is type emp_record is ...
- shell脚本下载网页图片
和大家分享一个shell脚本写的图片抓取器.使用方法:img_downloader.sh.使用时在shell下输入:./img_downloader.sh www.baidu.com -d image ...
- 【Python】【Algorithm】排序
冒泡排序 dic = [12, 45, 22, 6551, 74, 155, 6522, 1, 386, 15, 369, 15, 128, 123, ] for j in range(1, len( ...
- Java RestTemplate传递参数
最近使用Spring 的 RestTemplate 工具类请求接口的时候发现参数传递的一个坑,也就是当我们把参数封装在Map里面的时候,Map 的类型选择. 使用RestTemplate post请求 ...
- apply 和 call 的区别
相同点: 都能够改变方法的执行上下文(执行环境),将一个对象的方法交给另一个对象来执行,并且是立即执行 不同点: call方法从第二个参数开始可以接收任意个参数,每个参数会映射到相应位置的func的参 ...
- linux环境centos
qhost:查看集群 投送到集群qsub -l vf=2G,p=1 work.sh -cwd -V all_section_run.sh 杀死任务 qdel id qstat -u \* |less ...