介绍
JNI(Java Native Interface)是一种允许Java程序与本地代码(如C或C++)互操作的接口技术。通过JNI,Java程序能够调用本地代码,实现性能和功能上的优化,克服Java在某些场景下的内存管理和执行效率瓶颈。它使得开发者可以在Java应用中集成底层操作系统功能或使用已存在的高效本地库,从而提升应用的执行速度或访问硬件资源的能力。
JNI 基本知识
本地库生命周期
阶段 | 触发条件 | 关键函数 | 用途 |
---|---|---|---|
加载阶段 | System.loadLibrary | JNI_OnLoad | 注册动态方法/初始化资源 |
运行阶段 | Java调用native方法 | - | 业务逻辑执行 |
卸载阶段 | 类加载器回收 | JNI_OnUnload | 释放内存/关闭句柄 |
Native方法命名规范
动态链接器根据特定规则查找Java本地方法对应的Native函数,方法名遵循如下规则:
- 函数名带有Java_前缀
- 后跟以下划线完整类名,类的包名分隔符.以_来代替
- 后跟函数名
- 对于重载的本地方法,会使用双下划线(__)来分隔参数签名
命名结构:Java_ {全限定类名}_ {方法名}, 示例:Java_com_example_Encryptor_encryptData
重载处理:对重载方法追加双下划线 __
和参数签名缩写, 示例:encrypt__Ljava_lang_String_2
Native方法参数
- 第一个参数是JNI接口指针,
JNIEnv *
- 第二个参数是Java对象,具体值取决于当前方法是静态方法还是实例方法,若是静态方法,则表示类对象,若是实例方法,则表示实例对象
- 其余参数与定义本地方法时的参数一 一对应
Java方法声明
public native void process(
int count, // -> jint
String data, // -> jstring
byte[] buffer // -> jbyteArray
);
C++函数实现
JNIEXPORT void JNICALL Java_Processor_process(
JNIEnv *env, // JNI环境指针
jobject obj, // 调用对象实例
jint count, // 对应Java int
jstring jstr, // 对应Java String
jbyteArray jarray // 对应Java byte[]
) {
/* 实现代码 */
}
JNI数据类型
Java类型 | 原生类型 | 原生类型 |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | not applicable |
类型签名
类型签名 | Java类型 |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
void | void |
例如:Java函数声明是long f (int n, String s, int[] arr);
,那么他的类型是(ILjava/lang/String;[I)J
JNI结构函数表
具体可以参考jni.h头文件中的JNINativeInterface 定义, 这里只简单说明几个接口函数。
函数接口 | 说明 |
---|---|
GetObjectClass | 返回某个对象的类型对象 |
GetMethodID | 返回某个类或接口的实例方法ID |
RegisterNatives | 为指定类型注册本地方法 |
UnregisterNatives | 取消注册某个类中所有的本地方法 |
Call |
在本地方法中调用Java对象的实例方法 |
第一个JNI程序
目标
- 实现Java调用Native函数
- 实现Native函数调用Java实例函数
- 实现Java调用JNI动态注册的Native函数
代码实现
Main.java
public class Main {
static {
System.loadLibrary("test");
}
private native int add(int a, int b);
private native int sub(int a, int b);
private native void foo();
private void sayHi() {
System.out.println("Hello World");
}
public static void main(String[] args) {
Main mainObj = new Main();
System.out.println(mainObj.add(1, 2));
System.out.println(mainObj.sub(2, 1));
mainObj.foo();
}
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(test)
set(PRODUCT_NAME ${PROJECT_NAME})
set(CMAKE_CXX_STANDARD 17)
set(JAVA_HOME $ENV{JAVA_HOME})
include_directories(${JAVA_HOME}/include)
include_directories(${JAVA_HOME}/include/win32)
link_directories(${JAVA_HOME}/lib)
message(STATUS JAVA_HOME:${JAVA_HOME})
aux_source_directory(. SRC_LIST)
add_library(${PRODUCT_NAME} SHARED ${SRC_LIST})
使用javah -jni Main
自动生成Main类的JNI头文件
Main.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Main */
#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Main
* Method: add
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_Main_add
(JNIEnv *, jobject, jint, jint);
/*
* Class: Main
* Method: foo
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Main_foo
(JNIEnv*, jobject);
#ifdef __cplusplus
}
#endif
#endif
Main.cpp
#include "Main.h"
JNIEXPORT jint JNICALL Java_Main_add(JNIEnv* env, jobject obj, jint a, jint b)
{
return a + b;
}
JNIEXPORT void JNICALL Java_Main_foo(JNIEnv* env, jobject obj)
{
jclass clazz = env->GetObjectClass(obj);
jmethodID sayHi = env->GetMethodID(clazz, "sayHi", "()V");
env->CallVoidMethod(obj, sayHi);
}
JNIEXPORT jint JNICALL Main_sub(JNIEnv* env, jobject obj, jint a, jint b)
{
printf("Java_Main_sub\n");
return a - b;
}
static const JNINativeMethod methods[] = {
{"sub", "(II)I", (void*)Main_sub},
};
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
printf("JNI_OnLoad\n");
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK)
{
return JNI_ERR;
}
jclass mainClass = env->FindClass("Main");
if (mainClass == NULL)
{
return JNI_ERR;
}
// 注册本地方法
int numMethods = sizeof(methods) / sizeof(methods[0]);
if (env->RegisterNatives( mainClass, methods, numMethods) < 0)
{
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
运行结果
JNI_OnLoad // 加载库时调用JNI_OnLoad
3 // add(1, 2) 的结果
Java_Main_sub // 调用动态注册的sub方法
1 // sub(2, 1) 的结果
Hello World // foo函数调用Java的sayHi方法
执行流程解析:
- 加载库: JVM加载test库,触发JNI_OnLoad执行,动态注册sub方法。
- 调用add: mainObj.add(1, 2)调用Java_Main_add函数。
- 返回计算结果3。
- 调用sub: mainObj.sub(2, 1)调用动态注册的Main_sub函数, 打印"Java_Main_sub"。
- 返回计算结果1。
- Java_Main_foo函数调用Java的sayHi方法
代码安全加固策略
Java字节码因其高度可读性和易反编译特性,导致关键业务逻辑和敏感算法面临被逆向分析的风险。通过JNI将核心功能迁移到C/C++实现的本地库中,可显著提高攻击者的逆向门槛:
- 逆向难度提升:本地库的二进制代码比Java字节码更难反编译
- 调试障碍增加:需要专业的底层调试工具(如IDA Pro、GDB)
- 分析成本激增:逆向工程师需要同时精通Java和底层汇编语言
- 攻击面转移:从Java层的逆向转向底层的二进制逆向
此时可直接使用Virbox Protector成熟的代码保护工具进行加固。该工具提供多种保护方案(包括Native库代码保护和虚拟化保护VME等),通过组合使用能有效保护核心代码不被轻易泄露或分析。