当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。
注册分为 静态注册 和 动态注册两种。默认的实现方式即静态注册
静态注册
Java 方法与 C 函数通过名字规则自动对应,C/C++ 的函数名必须严格遵循命名规范:
1
| Java_包名_类名_方法名(JNIEnv* env, jobject obj, ...)
|
例子
1 2 3 4 5 6 7 8
| package com.example;
public class MyClass { public native void sayHello(); static { System.loadLibrary("native-lib"); } }
|
native代码
1 2 3 4 5 6 7
|
JNIEXPORT void JNICALL Java_com_example_MyClass_sayHello(JNIEnv* env, jobject obj) { printf("Hello from C!\n"); }
|
逆向
在Java层可以看到Native层注册的函数

在Native层中,可以看到注册的函数,遵循静态注册的命名规则

动态注册
在本地库加载时(通常在 JNI_OnLoad()),或者在运行时由 C/C++ 代码主动调用 RegisterNatives(),把一组 Java 方法名 + 方法签名 + 本地函数指针 映射注册到 JVM,从而让 Java 代码调用这些本地实现。
它不依赖函数名的固定命名规则(Java_package_Class_method)手动把Java方法和C函数的对应关系注册到JVM
JNI_OnLoad(JavaVM* vm, void* reserved) 在 native-lib 被 System.loadLibrary() 时调用
例子
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.example;
public class MyClass { static { System.loadLibrary("native-lib"); }
public native void sayHello(); public native int add(int a, int b); public static native String staticEcho(String s); }
|
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include <jni.h> #include <stdio.h> #include <string.h>
#ifdef __cplusplus extern "C" { #endif
static void native_sayHello(JNIEnv* env, jobject thiz) { printf("native_sayHello invoked\n"); }
static jint native_add(JNIEnv* env, jobject thiz, jint a, jint b) { return a + b; }
static jstring native_staticEcho(JNIEnv* env, jclass clazz, jstring js) { const char* s = (*env)->GetStringUTFChars(env, js, NULL); if (s == NULL) { return NULL; } jstring ret = (*env)->NewStringUTF(env, s); (*env)->ReleaseStringUTFChars(env, js, s); return ret; }
static JNINativeMethod methods[] = { {"sayHello", "()V", (void*)native_sayHello}, {"add", "(II)I", (void*)native_add}, {"staticEcho", "(Ljava/lang/String;)Ljava/lang/String;", (void*)native_staticEcho} };
static const int methods_count = sizeof(methods) / sizeof(methods[0]);
static int register_native_methods(JNIEnv* env) { const char* kClassPathName = "com/example/MyClass"; jclass clazz = (*env)->FindClass(env, kClassPathName); if (clazz == NULL) { if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } return JNI_FALSE; }
if ((*env)->RegisterNatives(env, clazz, methods, methods_count) < 0) { if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } return JNI_FALSE; }
return JNI_TRUE; }
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }
if (!register_native_methods(env)) { return JNI_ERR; }
return JNI_VERSION_1_6; }
#ifdef __cplusplus } #endif
|
注意,这里的register_native_methods并不是必须要写的,它的作用只是让代码结构更清晰
一个典型的辅助函数
1 2 3 4 5 6 7 8 9 10 11
| static int register_native_methods(JNIEnv* env, const char* className, JNINativeMethod* methods, int numMethods) { jclass clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_FALSE; } if ((*env)->RegisterNatives(env, clazz, methods, numMethods) < 0) { return JNI_FALSE; } return JNI_TRUE; }
|
而Jni_Onload是必须的
在Java层执行system.loadLibrary的时候被Jvm自动调用,当 native 库被加载时,完成初始化,比如注册 native 函数。
JNI_OnLoad的标准写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }
if (!register_native_methods(env, "com/example/MyClass", methods, sizeof(methods) / sizeof(methods[0]))) { return JNI_ERR; }
return JNI_VERSION_1_6; }
|
在动态注册中,核心函数是
1
| jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods);
|
注意,这个methods参数,是一个数组,类型为
1 2 3 4 5
| typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
|
如果不传这个表,RegisterNatives()就不知道在注册什么东西
所以也就是说,从逻辑上来看,方法表是一个必须的东西
方法表的结构如下
1 2 3 4
| static JNINativeMethod methods[] = { {"sayHello", "()V", (void*)native_sayHello}, {"add", "(II)I", (void*)native_add} };
|
“sayHello”这个字段表示函数名
“()V”这个字段表示void
(void*)native_sayHello字段表示函数地址
第二个字段的常见签名方法如下
Java 类型 -> JNI 签名
-
void -> V
-
boolean -> Z
-
byte -> B
-
char -> C
-
short -> S
-
int -> I
-
long -> J
-
float -> F
-
double -> D
-
Object -> Lfull/package/ClassName; (包名用 /,例如 Ljava/lang/String;)
-
int[] -> [I、String[] -> [Ljava/lang/String;
举例:
-
void foo() -> ()V
-
int sum(int a, int b) -> (II)I
-
String process(String s, int[] arr) -> (Ljava/lang/String;[I)Ljava/lang/String;
逆向
同样可以看到注册了个native函数

到了native层,可以发现大不同了
首先可以看到JNI_Onload函数,看不到静态注册格式命名的函数名了

那么应该怎样看动态注册了哪些函数呢
还记得吗,在源码中,我们是写了一个“函数表”的

在IDA中,如果没去符号表,是可以看到的
我们点进去,就可以看到动态注册了哪些函数了

