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技術演變而來。