安卓Native层函数注册

当执行一个 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
#include <jni.h>
#include <stdio.h>

JNIEXPORT void JNICALL
Java_com_example_MyClass_sayHello(JNIEnv* env, jobject obj) {
printf("Hello from C!\n");
}

逆向

在Java层可以看到Native层注册的函数

image

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

image

动态注册

在本地库加载时(通常在 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");
}

// Java 声明(实例方法)
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
// file: native_reg.c
#include <jni.h>
#include <stdio.h>
#include <string.h>

// 如果是 C++ 编译器,防止函数名被 mangling
#ifdef __cplusplus
extern "C" {
#endif

// -------------------- 本地实现函数 --------------------
// 实例方法(对应 Java 的非 static native 方法)
static void native_sayHello(JNIEnv* env, jobject thiz) {
// 简单打印 —— 注意:Android 上用 printf 可能看不到,建议用 __android_log_print
printf("native_sayHello invoked\n");
}

// 实例方法:带返回值与参数
static jint native_add(JNIEnv* env, jobject thiz, jint a, jint b) {
return a + b;
}

// 静态方法(对应 Java 的 static native 方法)注意第二个参数类型是 jclass
static jstring native_staticEcho(JNIEnv* env, jclass clazz, jstring js) {
const char* s = (*env)->GetStringUTFChars(env, js, NULL);
if (s == NULL) { // OOM or other error
return NULL;
}
// 这里可以处理字符串,演示直接返回一个新字符串
jstring ret = (*env)->NewStringUTF(env, s);
(*env)->ReleaseStringUTFChars(env, js, s);
return ret;
}

// -------------------- 方法表:Java 名称、签名、C 函数指针 --------------------
static JNINativeMethod methods[] = {
// { "Java 方法名", "JNI 签名", (void*)本地函数指针 }
{"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]);

// -------------------- 注册函数(封装 RegisterNatives) --------------------
static int register_native_methods(JNIEnv* env) {
// 注意:这里的类路径使用斜杠,不使用点号
const char* kClassPathName = "com/example/MyClass";
jclass clazz = (*env)->FindClass(env, kClassPathName);
if (clazz == NULL) {
// 如果 FindClass 失败,打印并清理异常(如果有)
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
return JNI_FALSE;
}

// RegisterNatives 返回 < 0 表示失败
if ((*env)->RegisterNatives(env, clazz, methods, methods_count) < 0) {
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
return JNI_FALSE;
}

// 如果你需要长期引用这个 jclass,应该创建全局引用:
// jclass globalClazz = (*env)->NewGlobalRef(env, clazz);
// 然后存储 globalClazz 供以后使用,并在卸载时 DeleteGlobalRef。
return JNI_TRUE;
}

// -------------------- JNI_OnLoad:库被 System.loadLibrary 时 JVM 调用 --------------------
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;

// 获取 JNIEnv 指针 —— 注意线程安全与返回值检查
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // 表示加载失败
}

// 注册 native 方法(通常在这里做,但 Android 情况见后文)
if (!register_native_methods(env)) {
return JNI_ERR; // 注册失败,返回错误使加载失败
}

// 返回所支持的 JNI 版本
return JNI_VERSION_1_6;
}

#ifdef __cplusplus
} // extern "C"
#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;

// 1. 获取 JNIEnv 指针
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

// 2. 调用自己写的注册函数(可选)
if (!register_native_methods(env, "com/example/MyClass", methods, sizeof(methods) / sizeof(methods[0]))) {
return JNI_ERR;
}

// 3. 返回 JNI 版本号(必须)
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; // Java 方法名
const char* signature; // Java 方法签名(描述参数与返回值类型)
void* fnPtr; // 对应的 C 函数指针
} 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[] -> [IString[] -> [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函数

image

到了native层,可以发现大不同了

首先可以看到JNI_Onload函数,看不到静态注册格式命名的函数名了

image

那么应该怎样看动态注册了哪些函数呢

还记得吗,在源码中,我们是写了一个“函数表”的

image

在IDA中,如果没去符号表,是可以看到的

我们点进去,就可以看到动态注册了哪些函数了

image

image