Native 方法的探究有了小进展~~

卧槽。痛苦的一天。

因为需要读取 jar 文件,还用 C++ 语言QAQAQAQ,没有现成的库……我又不可能直接去解析……QAQ,于是就想到直接调用 Hotspot 写好的现成的 jar 文件库(我好聪明,逃。
然后就开始了日狗的一天。啥都没干,就一直在干动态库(。mac 的动态库 dylib 实在是气死了。但是这个设计也真是让人眼前一亮啊。学到了不少东西。

一开始只想要调用 Java 的 Native 方法

想写一个小型 jvm 的话,(虽然是玩具,不过我是不可能仅仅局限于只是跑一个 hello world 这种的。自然,想要打造一个运行时环境,那么就必须能够和原生java挂钩。这是目标,同样也是底线。不能让步,如果写完了之后连 java 的类库都不能调,可以狗带了。比如说日后我要是想要实现一个 C 语言的玩具编译器的话,那么不可能仅仅调用我自己底层写的 printf,一定要调用 stdio.h 中的 printf 才好啊。但是这样的话,就肯定涉及解释执行 C 宏了。因为 stdio.h 全都是宏,大家也不是不知道。当然……就我这辣鸡水平估计是要弃坑了……不过这是一年以后的话题了。我们先放着。和这一样,如果要写一个小型 jvm,那么就必须要支持 java 类库的调用才行。要不然就只能跑自己 XJB 写的不带任何库的 java 文件,一点意义也没有。因而,必须要调用 Native 方法。但是,这时就产生了一个问题啊。这个 Native 方法,每个 Native 是不是都是纯函数呢?这是个很重要的话题呢。如果不纯,我即使强行从动态库中把他们扒出来,调用也一定会出错。因为可能这个 Native 方法还涉及到他周边的上下文环境,而我只调用了这个方法,那么肯定会出错的。这个 blog 就是一个栗子。。因为要拆包 jar 文件,我模仿 hotspot 的调用方式调用了一波 jre lib,于是代码如下:

1
2
3
4
5
6
7
8
9
string path = "<YourOpenJDK>/build/macosx-x86_64-normal-server-slowdebug/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home/jre/lib/"; // 自己编译的 jdk。如果是你的,应该路径(Mac OS)是 /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/
string jvm_path = path + "server/";
string target = path + "libzip.dylib"; // java 中,java.util.ZipFile 类的 native 文件最后全被打包成了 jre/lib 中的 libzip.dylib。在 linux 上可能是 libzip.so 吧。我们将要解包,并且调用里边 sun 公司开发者写好的 Zip_Open 函数,这个函数可以解开一个 zip 包并且读文件。源代码在 <YourOpenJDK>/jdk/src/share/native/java/util/zip/zip_util.c 中。
// [1] 占位符,这里一会要插入代码
void *handle = ::dlopen((target).c_str(), RTLD_LAZY); // posix 的 <dlfcn.h> 头文件,这个头文件是 <DynamicLibaryFunCtioN> 的缩写。即动态库 utils。而 dlopen 函数则根据你给定的库路径,使用 RTLD_LAZY 宏采用 lazy load 的策略——即再用到此动态库的时候再解开。懒人策略。
cout << (unsigned long)handle << endl; // 测试。如果是 0 则说明错误。
if (handle == nullptr) cout << dlerror(); // 如果错误输出为啥错。
void *fun = ::dlsym(handle, "ZIP_Open"); // dlsym 方法,在库中选择一个函数 ZIP_Open,然后传回地址。
cout << (unsigned long)fun << endl; // 同上,输出 0 则说明错误。

emmm。然后 mac 的动态库给我报错

1
2
3
dyld: Library not loaded: @rpath/libjvm.dylib
Referenced from: <此执行文件名>
Reason: image not found

这一段非常恶心。本质上原因是:由于你这个调用的动态库引用了别的动态库,如图:

1
2
3
4
5
6
7
<YourOpenJdk>/build/macosx-x86_64-normal-server-slowdebug/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home/jre/lib> otool -L libzip.dylib
libzip.dylib:
@rpath/libzip.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
@rpath/libjava.dylib (compatibility version 1.0.0, current version 1.0.0)
@rpath/libjvm.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)

我们能够发现这个动态库 libzip.dylib 引用了一堆动态库,有两个 libz.1.dylib 和 libSystem.B.dylib 是绝对路径,那么系统绝对不可能找不着。关键在于另三个:前边都有个 @rpath。
这个 @rpath 是执行的路径。貌似没法直接输出,只能通过强行指定才行。有的大神用 otool -l libzip.dylib(注意 l 小写) 然后得到了如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
Load command 11
cmd LC_LOAD_DYLIB
cmdsize 48
name @rpath/libjvm.dylib (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 1.0.0
compatibility version 1.0.0
Load command 12
cmd LC_LOAD_DYLIB
cmdsize 56
name /usr/lib/libSystem.B.dylib (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 1238.60.2
compatibility version 1.0.0
Load command 13
cmd LC_RPATH // 这里!
cmdsize 32
path @loader_path/. (offset 12)
...

大概能看出来后边的 LC_RPATH 的指定位置,是 @loader_path。也就是,应该是你的可执行文件调用这个库所在的位置。比如说,我在 /usr/tmp/haha 这个可执行文件中调用 <YourOpenJdk>/build/macosx-x86_64-normal-server-slowdebug/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home/jre/lib/libzip.dylib,那么八成这个 @loader_path 只是 /usr/tmp/haha。然后由于 @rpath@loader_path/.,那么就一样。于是 libzip.dylib 引用的 libjvm.dylib 等共三个的目录变成了 /usr/tmp/libjvm.dylib……应该是这样。为啥说是应该,因为我也没有验证过,存在错误的可能。不过八成是真的(逃。但是总之你是肯定索引不到真正的 libjvm.dylib 就是了,哈哈。
于是我们要引入 mac 的一个工具:install_name_tool。这个工具可以在可执行文件/动态库中加入 @rpath。由于我们正在写程序,所以不可能通过程序把自己设置 rpath。。。所以,我们只能在程序内去修改 libzip.dylib 的 rpath 了。那么我们在刚才的 [1] 处添加几行代码:

1
2
string cmd = "install_name_tool -add_rpath " + jvm_path + " " + target;
system(cmd.c_str()); // 执行:install_name_tool -add_rpath <Your Real JVM Path> <Need Modified Lib/Execute File>

这样就可以成功了 QAQ。我说的比较容易,其实花了我将近半天才弄完QAQ。

调用不纯的 Native 方法是一个巨坑。

到这一步能够解析动态库了,那么我们就开始搜刮里边的 sun 公司写好的函数了~于是加入下边的代码来 get 那个 ZIP_Open 函数~

1
2
3
4
5
6
7
char *error_msg = nullptr;
typedef void ** (JNICALL *ZIPOPEN) (const char *name, char **pmsg); // jdk native 函数中 ZIP_Open 的定义。
typedef void * jzfile; // 意为 jar/zip file 的句柄
ZIPOPEN zip_open = (ZIPOPEN)fun; // 函数指针强转。
jzfile *zip = zip_open("<YourOpenJdk>/build/macosx-x86_64-normal-server-slowdebug/jdk/lib/jce.jar", &error_msg); // 随手解压一个 jar 文件
cout << (unsigned long)zip << endl; // 不是 0 则没问题。

然后就开始了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
# To suppress the following error report, specify this argument
# after -XX: or in .hotspotrc: SuppressErrorAt=/os.hpp:203
......
无限循环......

Oh my god。
试了无数遍都这样,我是崩溃的!!
卧槽,后来仔细想了想……这一段错误提示是不是在 jvm 中的啊???
然后我在 /hotspot/src 下 grep 了一发,发现这两句出现在 hotspot/src/share/vm/utilities/debug.cpp 下……
然而!然而!!
我调用的 ZIP_Open 函数分明是 jdk 中的 Native 函数啊!!jdk 和 hotspot 是两个模块互不干扰,而且我就是写了一段 cpp 代码,根本没用到 jvm 啊!!
emmmm。细心的小伙伴肯定发现了。刚才的 libzip.dylib 引用了 libjvm.dylib。
那只能说明一点!!
(双眼一亮)
ZIPOpen 函数内部肯定检测了 JVM 的启动!!
是的就是这样。查阅源码之后我们能看到内部的各种检测自家的 JVM 启没启动的代码……
所以我们必须启动一个虚拟机才行。调用 JNI:
在整个代码的前部加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int res;
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
// 设置初始化参数:
options[0].optionString = const_cast<char *>("-Djava.compiler=NONE"); // ISO-C++11 不允许把 const char* 转为 char*。
// classpath 有多个的时候,Windows 用 ";" 分割。 unix 用 ":" 分割。
options[1].optionString = const_cast<char *>("-Djava.class.path=.");
// 用于跟踪运行时信息
options[2].optionString = const_cast<char *>("-verbose:jni");
// 版本号不能设漏
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 2; // 想要 JVM 的输出调试信息的话,请改为 3。
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
// 初始化虚拟机
res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (res == JNI_ERR) {
cerr << "Can't create Java VM!" << endl;
exit(1);
}

然后就可以了!输出就是正常的了。不过我们在编译的时候也要用 install_name_tools:

1
2
3
4
> clang++ -I/usr/include/jni -L/<YourOpenJdk>/build/macosx-x86_64-normal-server-slowdebug/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home/jre/lib/server/ <thisFile>.cc -o <a.out> -ljvm
> install_name_tool -add_rpath /<YourOpenJdk>/build/macosx-x86_64-normal-server-slowdebug/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home/jre/lib/server <a.out>
> ./a.out
# 输出即正常!

这说明,如果要自己写的话,Native 方法也要自己实现,不能调用人家的啊……(