背景
開發Android
應用時,有時候Java層的編碼不能滿足實際需求,需要通過JNI的方式利用C/C++實現重要功能并生成SO文件,再通過System.loadLibrary()
加載進行調用。常見的場景如:加解密算法、音視頻編解碼、數據采集、設備指紋等。通常核心代碼都封裝在SO文件中,也自然成為“黑客”攻擊的目標對象,利用IDA Pro等逆向工具,可以輕松反編譯未采取任何保護措施的SO文件,生成近似源代碼的C代碼,業務邏輯、核心技術將直接暴露在攻擊者的眼前。進一步造成核心技術泄漏、隱私數據泄漏、業務邏輯惡意篡改等危害。
高級選手可以編譯鏈加固,采用花指令等方案。入門選手可以采用Native方法動態注冊,混淆方名。
文章指在學會使用JNI方法動態注冊,靜態注冊,方法替換,且在這個過程中稍微了解一下native
層的動態庫加載,方法加載等知識。
舉例說明
通過javah
,獲取一組帶簽名函數,然后實現這些函數。對應的native
層的函數是:Java_類名_方法名
,樣式如下:
JNIEXPORT jstring JNICALL Java_com_jni_tzx_utils_JNIUitls_getNameString(JNIEnv *env,jclass type){
return env->NewStringUTF("JNIApplication");
}
JNIEXPORT jint JNICALL
Java_com_jni_tzx_utils_JNIUitls_getNumber(JNIEnv *env,jobject instance,jint num){
return 0;
}
但這樣存在兩個問題:
- 第一個問題是在IDA工具查看so文件,或者執行
nm -D libxxx.so
00000000000006d8 T Java_com_jni_tzx_utils_JNIUitls_getNameString
000000000000073c T Java_com_jni_tzx_utils_JNIUitls_getNumber
0000000000000708 W _ZN7_JNIEnv12NewStringUTFEPKc
U __cxa_atexit@LIBC
U __cxa_finalize@LIBC
我們會得到類似上面的輸出結果。可以明顯的看到該so是對應的那個Java類的那個方法。
- 第二個問題是惡意攻擊者可以得到這個so文件之后,查看這個native方法的參數和返回類型,也就是方法的簽名,然后自己在
Java
層寫個Demo程序,然后構造一個和so文件中對應的native方法,就可以執行這個native
方法,如果有一個校驗密碼或者是獲取密碼的方法是個native
的,那么這個時候就會很容易被攻擊者執行方法后獲取結果。
上面的兩個問題可以看到,如果native
層的函數遵循了這樣的格式,無疑給破解提供的簡單的一種方式。
手動注冊native方法
先看結果:
原有的native
方法:
JNIEXPORT jstring JNICALL Java_com_jni_tzx_MainActivity_stringFromJNI(JNIEnv *env, jobject /* this */) {
return env->NewStringUTF("Hello from C++ dynamic\n");
}
經過動態注冊之后:
0000000000000a2c T JNI_OnLoad
00000000000009f8 W _ZN7_JNIEnv12NewStringUTFEPKc
0000000000000bb8 W _ZN7_JNIEnv15RegisterNativesEP7_jclassPK15JNINativeMethodi
0000000000000b84 W _ZN7_JNIEnv9FindClassEPKc
0000000000000b48 W _ZN7_JavaVM6GetEnvEPPvi
U __android_log_print
U __cxa_atexit@LIBC
U __cxa_finalize@LIBC
U __stack_chk_fail@LIBC
00000000000009c8 T test
test
是動態注冊的方法名,對應com.jni.tzx.MainActivity#stringFromJNI
方法。
手動注冊native方法這個手段其實不太常用,因為它的安全措施不是很強大,但是也可以起到一定的作用。聊這個知識點之前,先了解一下so加載的流程。
so方法加載
在Android中,當程序在Java成運行
System.loadLibrary("jnitest");
這行代碼后,程序會去載入libjnitset.so
文件。于此同時,產生一個Load事件,這個事件觸發后,程序默認會在載入的.so
文件的函數列表中查找JNI_OnLoad
函數并執行,與Load事件相對,在載入的.so文件被卸載時,Unload事件被觸發。此時,程序默認會去載入的.so文件的函數列表中查找JNI_OnLoad
函數并執行,然后卸載.so文件。需要注意的是
JNI_OnLoad
和JNI_OnUnLoad
這兩個函數在.so組件中并不是強制要求的,可以不用去實現。我們可以將
JNI_OnLoad
函數看做構造函數在初始化時候調用,可以將JNI_OnUnLoad
函數看做析構函數在被卸載的時候調用;應用層的
Java
程序需要調用本地方法時,虛擬機在加載的動態文件中定位并鏈接該本地方法,從而得以執行本地方法。這中定位native
方法是按照命名規范來的。如果找不到就會崩潰。
jni方法查找失敗
//這個是找到方法
Process: com.jni.tzx, PID: 1598
java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.jni.tzx.utils.JNIUitls.test() (tried Java_com_jni_tzx_utils_JNIUitls_test and Java_com_jni_tzx_utils_JNIUitls_test__)
at com.jni.tzx.utils.JNIUitls.test(Native Method)
at com.jni.tzx.MainActivity.onCreate(MainActivity.java:24)
加載so文件
//java.lang.System.java
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
//java.lang.Runtime.java
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
}
private static native String nativeLoad(String filename, ClassLoader loader);
so文件所在目錄
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
final class DexPathList {
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
/********部分代碼省略*******/
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
private static File[] splitLibraryPath(String path) {
//System.getProperty("java.library.path")=/system/lib:/system/product/lib
ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
return result.toArray(new File[result.size()]);
}
public String findLibrary(String libraryName) {
//前面添加lib,后面添加.so。具體實現可以參考下面的so名稱獲取
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
String path = new File(directory, fileName).getPath();
if (IoUtils.canOpenReadOnly(path)) {
return path;
}
}
return null;
}
}
//java_lang_System.cpp
static jstring System_mapLibraryName(JNIEnv* env, jclass, jstring javaName) {
ScopedUtfChars name(env, javaName);
if (name.c_str() == NULL) {
return NULL;
}
char* mappedName = NULL;
//#define OS_SHARED_LIB_FORMAT_STR "lib%s.so"
asprintf(&mappedName, OS_SHARED_LIB_FORMAT_STR, name.c_str());
jstring result = env->NewStringUTF(mappedName);
free(mappedName);
return result;
}
通過反射獲取出BaseDexClassLoader
的getLdLibraryPath
是/data/app/com.jni.tzx--DSAI2wgWJ3_R3i0iUUeJA==/lib/arm64:/data/app/com.jni.tzx--DSAI2wgWJ3_R3i0iUUeJA==/base.apk!/lib/arm64-v8a
LoadedApk
的getClassLoader
方法調用ApplicationLoaders.getClassLoader
方法創建PathClassLoader
。
構造PathClassLoader
的libraryDir
是來自ApplicationInfo.nativeLibraryDir
。
nativeLibararyDir
的賦值主要是在PackageManagerService
中,通過apkRoot也就是sourceDir
。
VMRuntime.is64BitInstructionSet(getPrimaryInstructionSet(info));
再判斷是否為64
位,確定是安裝包的lib
目錄。最后根據AIB
確定nativeLibararyDir
。
ABI
相關初始化流程可以參考:Android中app進程ABI確定過程
nativeLoad
/*
* static String nativeLoad(String filename, ClassLoader loader)
* 將指定的完整路徑加載為動態庫 JNI 兼容的方法。 成功或失敗返回 null 失敗消息。
*/
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
JValue* pResult)
{
StringObject* fileNameObj = (StringObject*) args[0];
Object* classLoader = (Object*) args[1];
char* fileName = NULL;
StringObject* result = NULL;
char* reason = NULL;
bool success;
assert(fileNameObj != NULL);
fileName = dvmCreateCstrFromString(fileNameObj);
success = dvmLoadNativeCode(fileName, classLoader, &reason);
if (!success) {
const char* msg = (reason != NULL) ? reason : "unknown failure";
result = dvmCreateStringFromCstr(msg);
dvmReleaseTrackedAlloc((Object*) result, NULL);
}
free(reason);
free(fileName);
RETURN_PTR(result);
}
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Native.cpp
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
char** detail)
{
SharedLib* pEntry;
void* handle;
bool verbose;
/********部分代碼省略***********/
Thread* self = dvmThreadSelf();
ThreadStatus oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
//打開pathName對應的動態鏈接庫,配合dlsym函數使用
handle = dlopen(pathName, RTLD_LAZY);
dvmChangeStatus(self, oldStatus);
if (handle == NULL) {
*detail = strdup(dlerror());
ALOGE("dlopen(\"%s\") failed: %s", pathName, *detail);
return false;
}
/* create a new entry */
SharedLib* pNewEntry;
pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
pNewEntry->pathName = strdup(pathName);
pNewEntry->handle = handle;
pNewEntry->classLoader = classLoader;
dvmInitMutex(&pNewEntry->onLoadLock);
pthread_cond_init(&pNewEntry->onLoadCond, NULL);
pNewEntry->onLoadThreadId = self->threadId;
/* try to add it to the list */
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
if (pNewEntry != pActualEntry) {
ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
pathName, classLoader);
freeSharedLibEntry(pNewEntry);
return checkOnLoadResult(pActualEntry);
} else {
if (verbose)
ALOGD("Added shared lib %s %p", pathName, classLoader);
bool result = true;
void* vonLoad;
int version;
//獲取JNI_OnLoad的地址
vonLoad = dlsym(handle, "JNI_OnLoad");
//這是用javah風格的代碼了,推遲native方法的解析,相當于構造為空不需要進行初始化
if (vonLoad == NULL) {
ALOGD("No JNI_OnLoad found in %s %p, skipping init",
pathName, classLoader);
} else {
/*
* Call JNI_OnLoad. We have to override the current class
* loader, which will always be "null" since the stuff at the
* top of the stack is around Runtime.loadLibrary(). (See
* the comments in the JNI FindClass function.)
*/
OnLoadFunc func = (OnLoadFunc)vonLoad;
Object* prevOverride = self->classLoaderOverride;
self->classLoaderOverride = classLoader;
oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
if (gDvm.verboseJni) {
ALOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
}
//調用JNI_OnLoad,并獲取返回的版本信息
version = (*func)(gDvmJni.jniVm, NULL);
dvmChangeStatus(self, oldStatus);
self->classLoaderOverride = prevOverride;
//對版本進行判斷,這是為什么要返回正確版本的原因。現在一般都是JNI_VERSION_1_6
if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
version != JNI_VERSION_1_6)
{
ALOGW("JNI_OnLoad returned bad version (%d) in %s %p",
version, pathName, classLoader);
/*
* It's unwise to call dlclose() here, but we can mark it
* as bad and ensure that future load attempts will fail.
*
* We don't know how far JNI_OnLoad got, so there could
* be some partially-initialized stuff accessible through
* newly-registered native method calls. We could try to
* unregister them, but that doesn't seem worthwhile.
*/
result = false;
} else {
if (gDvm.verboseJni) {
ALOGI("[Returned from JNI_OnLoad for \"%s\"]", pathName);
}
}
}
if (result)
pNewEntry->onLoadResult = kOnLoadOkay;
else
pNewEntry->onLoadResult = kOnLoadFailed;
pNewEntry->onLoadThreadId = 0;
/*
* Broadcast a wakeup to anybody sleeping on the condition variable.
*/
dvmLockMutex(&pNewEntry->onLoadLock);
pthread_cond_broadcast(&pNewEntry->onLoadCond);
dvmUnlockMutex(&pNewEntry->onLoadLock);
return result;
}
}
-
JNI_OnLoad
需要返回值,但是只能選擇返回后三個JNI_VERSION_1_2
,JNI_VERSION_1_4
,JNI_VERSION_1_6
, 返回上述三個值任意一個沒有區別 ;
返回 JNI_VERSION_1_1 會報錯 :
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
講so
庫加載之后,補充兩個函數分別是:dlopen
和dlsym
函數。
dlopen函數
功能:打開一個動態鏈接庫
包含頭文件:
#include <dlfcn.h>
函數定義:
void * dlopen( const char * pathname, int mode );
函數描述:在
dlopen()
函數以指定模式打開指定的動態連接庫文件,并返回一個句柄給調用進程。通過這個句柄來使用庫中的函數和類。使用dlclose()
來卸載打開的庫。-
mode
:分為這兩種-
RTLD_LAZY
暫緩決定,等有需要時再解出符號; -
RTLD_NOW
立即決定,返回前解除所有未決定的符號; RTLD_LOCAL
-
RTLD_GLOBAL
允許導出符號; RTLD_GROUP
RTLD_WORLD
-
-
返回值:
打開錯誤返回NULL;
成功,返回庫引用;
dlsym函數
函數原型是void* dlsym(void* handle,const char* symbol)
, 該函數在<dlfcn.h>
文件中。
handle
是由dlopen
打開動態鏈接庫后返回的指針,symbol
就是要求獲取的函數的名稱,函數返回值是void*
,指向函數的地址,供調用使用。
so方法延遲解析
上面的代碼說明,JNI_OnLoad
是一種更加靈活,而且處理及時的機制。用javah
風格的代碼,則推遲解析,直到需要調用的時候才會解析。這樣的函數,是dvmResolveNativeMethod(dalvik/vm/Native.cpp)
。
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Native.cpp
/*
* Resolve a native method and invoke it.
* //解析native方法并調用它。
* This is executed as if it were a native bridge or function. If the
* resolution succeeds, method->insns is replaced, and we don't go through
* here again unless the method is unregistered.
*
* Initializes method's class if necessary.
*
* An exception is thrown on resolution failure.
*
* (This should not be taking "const Method*", because it modifies the
* structure, but the declaration needs to match the DalvikBridgeFunc
* type definition.)
*/
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;
/*
* If this is a static method, it could be called before the class
* has been initialized.
*/
if (dvmIsStaticMethod(method)) {
if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz)) {
assert(dvmCheckException(dvmThreadSelf()));
return;
}
} else {
assert(dvmIsClassInitialized(clazz) ||
dvmIsClassInitializing(clazz));
}
/* start with our internal-native methods */
DalvikNativeFunc infunc = dvmLookupInternalNativeMethod(method);
if (infunc != NULL) {
/* resolution always gets the same answer, so no race here */
IF_LOGVV() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
LOGVV("+++ resolved native %s.%s %s, invoking",
clazz->descriptor, method->name, desc);
free(desc);
}
if (dvmIsSynchronizedMethod(method)) {
ALOGE("ERROR: internal-native can't be declared 'synchronized'");
ALOGE("Failing on %s.%s", method->clazz->descriptor, method->name);
dvmAbort(); // harsh, but this is VM-internal problem
}
DalvikBridgeFunc dfunc = (DalvikBridgeFunc) infunc;
dvmSetNativeFunc((Method*) method, dfunc, NULL);
dfunc(args, pResult, method, self);
return;
}
/* now scan any DLLs we have loaded for JNI signatures */
//根據signature在所有已經打開的.so中尋找此函數實現
void* func = lookupSharedLibMethod(method);
if (func != NULL) {
/* found it, point it at the JNI bridge and then call it */
//找到方法,并執行調用
dvmUseJNIBridge((Method*) method, func);
(*method->nativeFunc)(args, pResult, method, self);
return;
}
IF_ALOGW() {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
ALOGW("No implementation found for native %s.%s:%s",
clazz->descriptor, method->name, desc);
free(desc);
}
//拋出java.lang.UnsatisfiedLinkError異常
dvmThrowUnsatisfiedLinkError("Native method not found", method);
}
- 根據方法的
signature
在所有已經打開的.so
中尋找此函數實現;
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Native.cpp
/*
* See if the requested method lives in any of the currently-loaded
* shared libraries. We do this by checking each of them for the expected
* method signature.
*/
static void* lookupSharedLibMethod(const Method* method)
{
if (gDvm.nativeLibs == NULL) {
ALOGE("Unexpected init state: nativeLibs not ready");
dvmAbort();
}
//從已經加載的nativeLibs中輪詢執行findMethodInLib方法尋找method
return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib,
(void*) method);
}
- 對哈希表中的每個條目執行一個函數。如果
func
返回一個非零值,提前終止并返回該值。
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Hash.cpp
/*
* Execute a function on every entry in the hash table.
*
* If "func" returns a nonzero value, terminate early and return the value.
*/
int dvmHashForeach(HashTable* pHashTable, HashForeachFunc func, void* arg)
{
int i, val;
for (i = 0; i < pHashTable->tableSize; i++) {
HashEntry* pEnt = &pHashTable->pEntries[i];
if (pEnt->data != NULL && pEnt->data != HASH_TOMBSTONE) {
val = (*func)(pEnt->data, arg);
if (val != 0)
return val;
}
}
return 0;
}
在講從依賴庫中搜索匹配的方法之前,先看一下Method
結構體的定義:
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/oo/Object.h
struct Method {
ClassObject* clazz;
u4 accessFlags;
u2 methodIndex;
u2 registersSize;
u2 outsSize;
u2 insSize;
const char* name;
DexProto prototype;
const char* shorty;
const u2* insns;
int jniArgInfo;
DalvikBridgeFunc nativeFunc;
bool fastJni;
bool noRef;
bool shouldTrace;
const RegisterMap* registerMap;
bool inProfile;
};
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Native.cpp
/*
* (This is a dvmHashForeach callback.)
* //在依賴庫中搜索匹配的方法
* Search for a matching method in this shared library.
*
* TODO: we may want to skip libraries for which JNI_OnLoad failed.
*/
static int findMethodInLib(void* vlib, void* vmethod)
{
const SharedLib* pLib = (const SharedLib*) vlib;
const Method* meth = (const Method*) vmethod;
char* preMangleCM = NULL;
char* mangleCM = NULL;
char* mangleSig = NULL;
char* mangleCMSig = NULL;
void* func = NULL;
int len;
if (meth->clazz->classLoader != pLib->classLoader) {
ALOGV("+++ not scanning '%s' for '%s' (wrong CL)",
pLib->pathName, meth->name);
return 0;
} else
ALOGV("+++ scanning '%s' for '%s'", pLib->pathName, meth->name);
/*
* First, we try it without the signature.
*/
//把java的native方法的名字進行轉換,生成和javah一致的名字
preMangleCM =
createJniNameString(meth->clazz->descriptor, meth->name, &len);
if (preMangleCM == NULL)
goto bail;
mangleCM = mangleString(preMangleCM, len);
if (mangleCM == NULL)
goto bail;
ALOGV("+++ calling dlsym(%s)", mangleCM);
//dlsym清晰的表明,這里才是獲取函數的地方。
func = dlsym(pLib->handle, mangleCM);
if (func == NULL) {
//獲取native方法的返回類型和參數類型
mangleSig = createMangledSignature(&meth->prototype);
if (mangleSig == NULL)
goto bail;
mangleCMSig = (char*) malloc(strlen(mangleCM) + strlen(mangleSig) +3);
if (mangleCMSig == NULL)
goto bail;
//將native的方法名和方法的返回類型和參數類型通過__進行拼接
sprintf(mangleCMSig, "%s__%s", mangleCM, mangleSig);
ALOGV("+++ calling dlsym(%s)", mangleCMSig);
func = dlsym(pLib->handle, mangleCMSig);
if (func != NULL) {
ALOGV("Found '%s' with dlsym", mangleCMSig);
}
} else {
ALOGV("Found '%s' with dlsym", mangleCM);
}
bail:
free(preMangleCM);
free(mangleCM);
free(mangleSig);
free(mangleCMSig);
return (int) func;
}
從上面看到一般通過VM去尋找*.so
里的native
函數。如果需連續調用很多次,每次度需要尋找一遍,回花很多時間。
此時,C組件開發者可以將本地函數向VM進行注冊,以便能加快后續調用native
函數的效率。可以這么想象一下,假設VM內部一個native
函數鏈表,初始時是空的,在未顯示注冊之前,此native
數鏈表是空的,每次java
調用native
函數之前會首先在此鏈表中查找需要調用的native
函數,如果找到就直接調用,如果未找到,得再通過載入的.so
文件中的函數列表中去查找,且每次Java
調用native
函數都是進行這樣的流程。因此,效率就自然會下降。
為了客服這個問題,我們可以通過在.so
文件載入初始化時,即JNI_OnLoad
函數中,先行將native
函數注冊VM的native
函數鏈表中去,這樣一來,后續每次Java
調用native
函數都會在VM中native
函數鏈表中找到對應的函數,從而加快速度。
優點
簡單明了
so方法動態注冊
這種方式,寫的代碼稍微多點,但好處很明顯,函數映射關系配置靈活,執行效率要比第一種方式高。
先看一個簡單的動態注冊實現Demo:
package com.jni.tzx;
public class MainActivity extends Activity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
/*****部分代碼刪除********/
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native String testNative();
}
對應的natieve-lib.cpp
文件:
注冊navtive
方法之前我們需要了解JavaVM
,JNIEnv
:
JavaVM
和 JNIEnv
是JNI
提供的結構體.
JavaVM
提供了允許你創建和銷毀JavaVM
的invokation interface
。理論上在每個進程中你可以創建多個JavaVM
, 但是Android
只允許創造一個。
JNIEnv
提供了大部分JNI
中的方法。在你的Native
方法中的第一個參數就是JNIEnv
.
JNIEnv
用于線程內部存儲。 因此, 不能多個線程共享一個JNIEnv
. 在一段代碼中如果無法獲取JNIEnv
, 你可以通過共享JavaVM
并調用GetEnv()
方法獲取。
JNINativeMethod
是動態注冊方法需要的結構體:
typedef struct {
const char* name;//在java中聲明的native函數名
const char* signature;//函數的簽名,可以通過javah獲取
void* fnPtr;//對應的native函數名
}JNINativeMethod
#include <jni.h>
#include <string>
//作用:避免編繹器按照C++的方式去編繹C函數
extern "C"
//用來表示該函數是否可導出(即:方法的可見性)
JNIEXPORT jstring
//用來表示函數的調用規范(如:__stdcall)
JNICALL
Java_com_jni_tzx_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {////非static的方法參數類型是jobject instance,而static的方法參數類型是jclass type
std::string hello = "Hello from C++ dynamic\n";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
//Java_com_jni_tzx_MainActivity_test(JNIEnv *env, jobject thiz) {
test(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("Java_com_jni_tzx_MainActivity_test");
}
extern "C"
JNIEXPORT jint JNICALL
//System.loadLibrary方法會調用載入的.so文件的函數列表中查找JNI_OnLoad函數并執行
JNI_OnLoad(JavaVM* vm, void* reserved) {
static JNINativeMethod methods[] = {
{
"testNative", //在java中聲明的native函數名
"()Ljava/lang/String;", //函數的簽名,可以通過javah獲取
(void *)test//對應的native函數名
}
};
JNIEnv *env = NULL;
jint result = -1;
// 獲取JNI env變量
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
// 失敗返回-1
return result;
}
// 獲取native方法所在Java類,包名和類名之間使用“/”風格
const char* className = "com/jni/tzx/MainActivity";
//這個可以找到要注冊的類,提前是這個類已經加載到Java虛擬機中;
//這里說明,動態庫和有native方法的類之間,沒有任何關系。
jclass clazz = env->FindClass(className);
if (clazz == NULL) {
return result;
}
// 動態注冊native方法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return result;
}
// 返回成功
result = JNI_VERSION_1_6;
return result;
}
如果使用C
語言,那么需要有幾個地方進行修改:
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4)
(*env)->FindClass(env, kClassName);
(*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))
FindClass
這個可以找到要注冊的類,提前是這個類已經加載到Java虛擬機
中;
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Jni.cpp
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Misc.cpp
/*
* Find a class by name.
*
* We have to use the "no init" version of FindClass here, because we might
* be getting the class prior to registering native methods that will be
* used in <clinit>.
*
* We need to get the class loader associated with the current native
* method. If there is no native method, e.g. we're calling this from native
* code right after creating the VM, the spec says we need to use the class
* loader returned by "ClassLoader.getBaseClassLoader". There is no such
* method, but it's likely they meant ClassLoader.getSystemClassLoader.
* We can't get that until after the VM has initialized though.
*/
static jclass FindClass(JNIEnv* env, const char* name) {
ScopedJniThreadState ts(env);
//通過檢查堆棧獲取當前正在執行的方法。
const Method* thisMethod = dvmGetCurrentJNIMethod();
assert(thisMethod != NULL);
Object* loader;
Object* trackedLoader = NULL;
//獲取當前的類加載器
if (ts.self()->classLoaderOverride != NULL) {
/* hack for JNI_OnLoad */
assert(strcmp(thisMethod->name, "nativeLoad") == 0);
loader = ts.self()->classLoaderOverride;
} else if (thisMethod == gDvm.methDalvikSystemNativeStart_main ||
thisMethod == gDvm.methDalvikSystemNativeStart_run) {
/* start point of invocation interface */
if (!gDvm.initializing) {
loader = trackedLoader = dvmGetSystemClassLoader();
} else {
loader = NULL;
}
} else {
loader = thisMethod->clazz->classLoader;
}
//獲取類的描述符,它是類的完整名稱(包名+類名),將原來的 . 分隔符換成 / 分隔符。
char* descriptor = dvmNameToDescriptor(name);
if (descriptor == NULL) {
return NULL;
}
//獲取已經加載的類,或者使用類加載加載該類
ClassObject* clazz = dvmFindClassNoInit(descriptor, loader);
free(descriptor);
jclass jclazz = (jclass) addLocalReference(ts.self(), (Object*) clazz);
dvmReleaseTrackedAlloc(trackedLoader, ts.self());
return jclazz;
}
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/oo/Class.cpp
/*
* Find the named class (by descriptor), using the specified
* initiating ClassLoader.
*
* The class will be loaded if it has not already been, as will its
* superclass. It will not be initialized.
*
* If the class can't be found, returns NULL with an appropriate exception
* raised.
*/
ClassObject* dvmFindClassNoInit(const char* descriptor,
Object* loader)
{
assert(descriptor != NULL);
//assert(loader != NULL);
LOGVV("FindClassNoInit '%s' %p", descriptor, loader);
if (*descriptor == '[') {
/*
* Array class. Find in table, generate if not found.
*/
return dvmFindArrayClass(descriptor, loader);
} else {
/*
* Regular class. Find in table, load if not found.
*/
if (loader != NULL) {
return findClassFromLoaderNoInit(descriptor, loader);
} else {
return dvmFindSystemClassNoInit(descriptor);
}
}
}
/*
* Load the named class (by descriptor) from the specified class
* loader. This calls out to let the ClassLoader object do its thing.
*
* Returns with NULL and an exception raised on error.
*/
static ClassObject* findClassFromLoaderNoInit(const char* descriptor,
Object* loader)
{
//ALOGI("##### findClassFromLoaderNoInit (%s,%p)",
// descriptor, loader);
Thread* self = dvmThreadSelf();
assert(loader != NULL);
//是否已經加載過該類
ClassObject* clazz = dvmLookupClass(descriptor, loader, false);
if (clazz != NULL) {
LOGVV("Already loaded: %s %p", descriptor, loader);
return clazz;
} else {
LOGVV("Not already loaded: %s %p", descriptor, loader);
}
char* dotName = NULL;
StringObject* nameObj = NULL;
/* convert "Landroid/debug/Stuff;" to "android.debug.Stuff" */
dotName = dvmDescriptorToDot(descriptor);
if (dotName == NULL) {
dvmThrowOutOfMemoryError(NULL);
return NULL;
}
nameObj = dvmCreateStringFromCstr(dotName);
if (nameObj == NULL) {
assert(dvmCheckException(self));
goto bail;
}
dvmMethodTraceClassPrepBegin();
//調用 loadClass()。 這可能會導致幾個拋出異
//因為 ClassLoader.loadClass()實現最終調用 VMClassLoader.loadClass 看是否引導類加載器可以在自己加載之前找到它。
LOGVV("--- Invoking loadClass(%s, %p)", dotName, loader);
{
const Method* loadClass =
loader->clazz->vtable[gDvm.voffJavaLangClassLoader_loadClass];
JValue result;
dvmCallMethod(self, loadClass, loader, &result, nameObj);
clazz = (ClassObject*) result.l;
dvmMethodTraceClassPrepEnd();
Object* excep = dvmGetException(self);
if (excep != NULL) {
#if DVM_SHOW_EXCEPTION >= 2
ALOGD("NOTE: loadClass '%s' %p threw exception %s",
dotName, loader, excep->clazz->descriptor);
#endif
dvmAddTrackedAlloc(excep, self);
dvmClearException(self);
dvmThrowChainedNoClassDefFoundError(descriptor, excep);
dvmReleaseTrackedAlloc(excep, self);
clazz = NULL;
goto bail;
} else if (clazz == NULL) {
ALOGW("ClassLoader returned NULL w/o exception pending");
dvmThrowNullPointerException("ClassLoader returned null");
goto bail;
}
}
/* not adding clazz to tracked-alloc list, because it's a ClassObject */
dvmAddInitiatingLoader(clazz, loader);
LOGVV("--- Successfully loaded %s %p (thisldr=%p clazz=%p)",
descriptor, clazz->classLoader, loader, clazz);
bail:
dvmReleaseTrackedAlloc((Object*)nameObj, NULL);
free(dotName);
return clazz;
}
RegisterNatives
注冊一個類的一個或者多個native
方法。
clazz
:指定的類,即 native 方法所屬的類methods
:方法數組,這里需要了解一下 JNINativeMethod 結構體nMethods
:方法數組的長度
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Jni.cpp
/*
* Register one or more native functions in one class.
*
* This can be called multiple times on the same method, allowing the
* caller to redefine the method implementation at will.
*/
static jint RegisterNatives(JNIEnv* env, jclass jclazz,
const JNINativeMethod* methods, jint nMethods)
{
ScopedJniThreadState ts(env);
ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(ts.self(), jclazz);
if (gDvm.verboseJni) {
ALOGI("[Registering JNI native methods for class %s]",
clazz->descriptor);
}
for (int i = 0; i < nMethods; i++) {
if (!dvmRegisterJNIMethod(clazz, methods[i].name,
methods[i].signature, methods[i].fnPtr))
{
return JNI_ERR;
}
}
return JNI_OK;
}
老羅的Android之旅:Dalvik虛擬機JNI方法的注冊過程分析
/*
* Register a method that uses JNI calling conventions.
*/
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr)
{
if (fnPtr == NULL) {
return false;
}
// If a signature starts with a '!', we take that as a sign that the native code doesn't
// need the extra JNI arguments (the JNIEnv* and the jclass).
bool fastJni = false;
//如果簽名以'!’時,我們認為這表明native代碼沒有
if (*signature == '!') {
fastJni = true;
++signature;
ALOGV("fast JNI method %s.%s:%s detected", clazz->descriptor, methodName, signature);
}
//檢查methodName是否是clazz的一個非虛成員函數
Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature);
if (method == NULL) {
//檢查methodName是否是clazz的一個虛成員函數。
method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature);
}
//方法沒找到
if (method == NULL) {
dumpCandidateMethods(clazz, methodName, signature);
return false;
}
//確保類clazz的成員函數methodName確實是聲明為JNI方法,即帶有native修飾符
if (!dvmIsNativeMethod(method)) {
ALOGW("Unable to register: not native: %s.%s:%s", clazz->descriptor, methodName, signature);
return false;
}
if (fastJni) {
//一個JNI方法如果聲明為同步方法,即帶有synchronized修飾符
// In this case, we have extra constraints to check...
if (dvmIsSynchronizedMethod(method)) {
// Synchronization is usually provided by the JNI bridge,
// but we won't have one.
ALOGE("fast JNI method %s.%s:%s cannot be synchronized",
clazz->descriptor, methodName, signature);
return false;
}
if (!dvmIsStaticMethod(method)) {
// There's no real reason for this constraint, but since we won't
// be supplying a JNIEnv* or a jobject 'this', you're effectively
// static anyway, so it seems clearer to say so.
ALOGE("fast JNI method %s.%s:%s cannot be non-static",
clazz->descriptor, methodName, signature);
return false;
}
}
//獲得Method對象method,用來描述要注冊的JNI方法所對應的Java類成員函數。
//當一個Method對象method描述的是一個JNI方法的時候,它的成員變量nativeFunc保存的就是該JNI方法的地址,但是在對應的JNI方法注冊進來之前,該成員變量的值被統一設置為dvmResolveNativeMethod。
//函數dvmResolveNativeMethod此時會在Dalvik虛擬內部以及當前所有已經加載的共享庫中檢查是否存在對應的JNI方法。
if (method->nativeFunc != dvmResolveNativeMethod) {
/* this is allowed, but unusual */
ALOGV("Note: %s.%s:%s was already registered", clazz->descriptor, methodName, signature);
}
method->fastJni = fastJni;
//一個JNI方法是可以重復注冊的,無論如何,函數dvmRegisterJNIMethod都是調用另外一個函數dvmUseJNIBridge來繼續執行注冊JNI的操作。
dvmUseJNIBridge(method, fnPtr);
ALOGV("JNI-registered %s.%s:%s", clazz->descriptor, methodName, signature);
return true;
}
//如果我們在Dalvik虛擬機啟動的時候,通過-Xjnitrace選項來指定了要跟蹤參數method所描述的JNI方法,那么函數dvmUseJNIBridge為該JNI方法選擇的Bridge函數就為dvmTraceCallJNIMethod,否則的話,就再通過另外一個函數dvmSelectJNIBridge來進一步選擇一個合適的Bridge函數。
static bool shouldTrace(Method* method) {
const char* className = method->clazz->descriptor;
// Return true if the -Xjnitrace setting implies we should trace 'method'.
if (gDvm.jniTrace && strstr(className, gDvm.jniTrace)) {
return true;
}
// Return true if we're trying to log all third-party JNI activity and 'method' doesn't look
// like part of Android.
if (gDvmJni.logThirdPartyJni) {
for (size_t i = 0; i < NELEM(builtInPrefixes); ++i) {
if (strstr(className, builtInPrefixes[i]) == className) {
return false;
}
}
return true;
}
return false;
}
/*
* Point "method->nativeFunc" at the JNI bridge, and overload "method->insns"
* to point at the actual function.
*/
//它主要就是根據Dalvik虛擬機的啟動選項來為即將要注冊的JNI選擇一個合適的Bridge函數。
void dvmUseJNIBridge(Method* method, void* func) {
method->shouldTrace = shouldTrace(method);
// Does the method take any reference arguments?
method->noRef = true;
const char* cp = method->shorty;
while (*++cp != '\0') { // Pre-increment to skip return type.
if (*cp == 'L') {
method->noRef = false;
break;
}
}
DalvikBridgeFunc bridge = gDvmJni.useCheckJni ? dvmCheckCallJNIMethod : dvmCallJNIMethod;
//選擇好Bridge函數之后,函數dvmUseJNIBridge最終就調用函數dvmSetNativeFunc來執行真正的JNI方法注冊操作。
dvmSetNativeFunc(method, bridge, (const u2*) func);
}
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/oo/Class.cpp
//參數method表示要注冊JNI方法的Java類成員函數
//參數func表示JNI方法的Bridge函數
//參數insns表示要注冊的JNI方法的函數地址。
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,
const u2* insns)
{
ClassObject* clazz = method->clazz;
assert(func != NULL);
/* just open up both; easier that way */
dvmLinearReadWrite(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadWrite(clazz->classLoader, clazz->directMethods);
//當參數insns的值不等于NULL的時候,函數dvmSetNativeFunc就分別將參數insns和func的值分別保存在參數method所指向的一個Method對象的成員變量insns和nativeFunc中
if (insns != NULL) {
/* update both, ensuring that "insns" is observed first */
method->insns = insns;
android_atomic_release_store((int32_t) func,
(volatile int32_t*)(void*) &method->nativeFunc);
} else {
//當insns的值等于NULL的時候,函數dvmSetNativeFunc就只將參數func的值保存在參數method所指向的一個Method對象成員變量nativeFunc中。
/* only update nativeFunc */
method->nativeFunc = func;
}
dvmLinearReadOnly(clazz->classLoader, clazz->virtualMethods);
dvmLinearReadOnly(clazz->classLoader, clazz->directMethods);
}
動態注冊閱讀到這里就可以了。
so方法延遲解析出錯
上面拋出的UnsatisfiedLinkError
也許會有人疑問,為什么是Native method not found
而不是我們開始遇到的崩潰 No implementation found for
。可能是因為系統版本不一致導致的,但因為目前沒有找到Android10
這塊的代碼所有沒有辦法一查到底。
https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/Exception.cpp
void dvmThrowUnsatisfiedLinkError(const char* msg) {
dvmThrowException(gDvm.exUnsatisfiedLinkError, msg);
}
void dvmThrowUnsatisfiedLinkError(const char* msg, const Method* method) {
char* desc = dexProtoCopyMethodDescriptor(&method->prototype);
char* className = dvmDescriptorToDot(method->clazz->descriptor);
dvmThrowExceptionFmt(gDvm.exUnsatisfiedLinkError, "%s: %s.%s:%s",
msg, className, method->name, desc);
free(className);
free(desc);
}
https://android.googlesource.com/platform/dalvik.git/+/android-4.3_r3/vm/Globals.h
/*
* All fields are initialized to zero.
*
* Storage allocated here must be freed by a subsystem shutdown function.
*/
struct DvmGlobals {
/******部分代碼省略*****/
ClassObject* exUnsatisfiedLinkError;
}
DvmGlobals
這個是Dalvik
虛擬機在進行初始化的時候所加載的一些基礎類,它們在Java的Libcore里面定義。
static struct { ClassObject** ref; const char* name; } classes[] = {
/*
* Note: The class Class gets special treatment during initial
* VM startup, so there is no need to list it here.
*/
{ &gDvm.exUnsatisfiedLinkError, "Ljava/lang/UnsatisfiedLinkError;" },
/******部分代碼省略*****/
}
利用JNI_OnLoad替換系統的native方法
目標:嘗試替換android/util/Log
的isLoggable
放方法;
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "tanzhenxing22-jni" // 這個是自定義的LOG的標識
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定義LOGD類型
extern "C"
JNIEXPORT jboolean JNICALL
isLoggable(JNIEnv *env, jclass clazz, jstring tag, jint level) {
LOGD("call isLoggable");
return false;
}
extern "C"
JNIEXPORT jint JNICALL
//System.loadLibrary方法會調用載入的.so文件的函數列表中查找JNI_OnLoad函數并執行
JNI_OnLoad(JavaVM* vm, void* reserved) {
LOGD("JNI_OnLoad");
static JNINativeMethod methodsLog[] = {
{"isLoggable", "(Ljava/lang/String;I)Z", (void *)isLoggable}
};
JNIEnv *env = NULL;
jint result = -1;
// 獲取JNI env變量
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
// 失敗返回-1
return result;
}
// 獲取native方法所在類
const char* classNameLog = "android/util/Log";
jclass clazzLog = env->FindClass(classNameLog);
if (clazzLog == NULL) {
return result;
}
// 動態注冊native方法
if (env->RegisterNatives(clazzLog, methodsLog, sizeof(methodsLog) / sizeof(methodsLog[0])) < 0) {
return result;
}
// 返回成功
result = JNI_VERSION_1_6;
return result;
}
文章到這里就全部講述完啦,若有其他需要交流的可以留言哦~!~!
Demo地址:https://github.com/stven0king/JNIApplication
參考文章: