編寫一個Java JNI的DEMO


0x00 Java部分

首先有一段Java代碼,在main函數中引用了會包含native調用的演示函數。至於使用native的具體場景,相信你已經從其他地方了解,此處不在贅述。

package dxcyber409;

public class Test {
    
    static {
        System.load("D:/test.dll");
    }

    static class Cls {
        private native String f(int i, String s);
        public void test() {
            String s = f(10, "asd");
            System.out.println("Your value:" + s);
        }
    }

    public static void main(String[] args) throws Exception {
        Cls cls = new Cls();
        cls.test();
    }

}

這段代碼有明顯的平台傾向,你可以看出筆者用的是Windows平台,從而加載的是DLL動態鏈接庫。如果你正在使用Unix派系的系統,那么動態鏈接庫的后綴應該是*.so。又或者你不想硬編碼路徑和后綴名,那么可以使用System.loadLibrary函數。

首先靜態代碼塊和靜態類Cls會由JVM進行最優先的加載(執行),隨后的main方法能夠順利執行。當然這段代碼是不能直接運行的,讓我們修復缺失的動態鏈接庫部分。

0x01 JNI的一般寫法

從Java到本地代碼的調用過程可以這樣來描述:Java -> JNI Bridge -> Native Code。由此可知我們需要自己編寫代碼,生成動態運行庫。

為了與JNI Bridge能夠兼容接入,我們還需要一套標准的聲明文件,對於C++這種聲明文件就是.h頭文件。Java SDK套件下的javah命令就提供了這種自動生成操作的支持。

圖1.javah用法幫助

javah命令支持從已經編譯好的class文件中提取出需要實現的native函數接口,然后生成JNI Bridge標准的C++風格.h頭文件。

圖2.Java代碼編譯后的目錄

編譯Java代碼后可以得到class文件,可以在資源管理器中查看一下編譯后的目錄(圖2)。按照Java代碼的結構,和編譯后的路徑編寫javah構建語句。

D:\RTEws\Java\jdk1.8.0_121\bin>javah -d "E:\Workspace\NetBeans\DXCyber409\src\main\java\dxcyber409\jni" -classpath "E:\Workspace\NetBeans\DXCyber409\target\classes" -jni dxcyber409.Test$Cls

在src/.../jni目錄下得到dxcyber409_Test_Cls.h文件,有了這個標准聲明就可以放心編寫C++實現了。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class dxcyber409_Test_Cls */

#ifndef _Included_dxcyber409_Test_Cls
#define _Included_dxcyber409_Test_Cls
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     dxcyber409_Test_Cls
 * Method:    f
 * Signature: (ILjava/lang/String;)D
 */
JNIEXPORT jstring JNICALL Java_dxcyber409_Test_00024Cls_f
  (JNIEnv *, jobject, jint, jstring);

#ifdef __cplusplus
}
#endif
#endif

 

圖3.創建Visual Studio項目

此時當然需要創建一個Visual Studio的動態鏈接庫項目,如圖3。

此外,細心的你會發現dxcyber409_Test_Cls.h包含了jni.h文件,要想通過編譯得把這個文件及其依賴一同包括到項目中(圖4)。簡單的做法就是把Java SDK套裝include目錄下的所有.h頭文件(由於筆者是在win平台,也包括win32目錄下的.h文件),復制一份放到項目源碼目錄下,並在VS項目中包含這些文件(圖5)。

圖4.Java SDK套裝include目錄結構

圖5.完成所有.h頭文件復制的項目源碼目錄

在dxcyber409_Test_Cls.h文件中,由於頭文件是我們自己在源碼目錄提供的,而不是使用標准庫頭文件,因此注意將include <jni> 修改為include "jni.h"。

隨后就是實現該頭文件,創建一個dxcyber409_Test_Cls.cpp文件后編寫一些簡單的代碼。

#include "stdafx.h"
#include "dxcyber409_Test_Cls.h"

JNIEXPORT jstring JNICALL Java_dxcyber409_Test_00024Cls_f
(JNIEnv *env, jobject obj, jint a1, jstring a2)
{
    return a2;  // 拋棄第一個int參數,直接返回第二個String參數
}

隨后直接編譯生成即可,找到生成目錄的DLL,移動到D:\test.dll路徑,DEMO運行成功。

圖6.DEMO運行結果

 

PS.如果出現x86架構和x64架構不兼容的提示,在VS中切換架構重新編譯即可。

java.lang.UnsatisfiedLinkError: E:\Workspace\C++\JavaNative\Debug\JavaNative.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform
    at java.lang.ClassLoader$NativeLibrary.load(Native Method)
    at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
    at java.lang.Runtime.load0(Runtime.java:809)
    at java.lang.System.load(System.java:1086)
    at dxcyber409.Test.<clinit>(Test.java:6)
Exception in thread "main" 

0x02 動態注冊native函數

javah自動生成的頭文件以及函數名稱都很冗余繁瑣,實際可以使用JNI_OnLoad進行動態的函數注冊,就可以免於每次改動都用javah生成新的頭文件。

#include "stdafx.h"
#include <stdlib.h>
#include "jni.h"

JNIEXPORT jstring JNICALL func_test(JNIEnv *env, jobject obj, jint a1, jstring a2)
{
    return a2;
}

JNINativeMethod gMethods[] = {
    {"f", "(ILjava/lang/String;)Ljava/lang/String;", func_test},
};

static jclass myClass;
static const char* const className = "dxcyber409/Test$Cls";

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reversed)
{
    JNIEnv* env = NULL;
    jint result = -1;

    // 從JavaVM中獲取JNIEnv
    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        printf("get env error.");
        return -1;
    }

    // 獲取映射的java類
    myClass = env->FindClass(className);
    if (myClass == NULL) {
        printf("cannot get class:%s\n", className);
        return -1;
    }

    // 通過RegisterNatives方法動態注冊
    if (env->RegisterNatives(myClass, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
        printf("cannot get method:%s\n", gMethods[0].name);
        return -1;
    }

    return JNI_VERSION_1_4;
}

首先把目光聚焦於JNI_OnLoad函數。在調用System.load*時JVM會自動對JNI_OnLoad函數進行回調,此處也正是注冊和初始化native函數庫的最好時機。

在myjni_main.cpp代碼中JNI_OnLoad函數的內部調用軌跡為:獲取JNIEnv->獲取native函數所在類名->調用RegisterNatives函數對gMethods數組所描述的方法映射規則進行注冊。

在0x01中我們使用的dxcyber409_Test_Cls.h和dxcyber409_Test_Cls.cpp已經可以拋棄,代碼所在的文件名可以任意取。至此JNI內部調用的函數名稱和內容已經獲得最大程度的自由。編譯后得到DLL,放到Java代碼可識別的路徑中,運行結果一致。

對於這種動態注冊的方法,能夠避免javah生成的長串類名函數名之外,在攻防安全方面也有許多切入點,而大熱的安卓JNI技術也正是基於JVM標准的JNI技術演變而來。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM