JNI方法注冊源碼分析(JNI_OnLoad|動態注冊|靜態注冊|方法替換)

icon13-png.png

背景

開發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方法加載

so加載流程.png

在Android中,當程序在Java成運行System.loadLibrary("jnitest");這行代碼后,程序會去載入libjnitset.so文件。于此同時,產生一個Load事件,這個事件觸發后,程序默認會在載入的.so文件的函數列表中查找JNI_OnLoad函數并執行,與Load事件相對,在載入的.so文件被卸載時,Unload事件被觸發。此時,程序默認會去載入的.so文件的函數列表中查找JNI_OnLoad函數并執行,然后卸載.so文件。

需要注意的是JNI_OnLoadJNI_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文件所在目錄

nativeLibraryDirectories.png
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;
}

通過反射獲取出BaseDexClassLoadergetLdLibraryPath/data/app/com.jni.tzx--DSAI2wgWJ3_R3i0iUUeJA==/lib/arm64:/data/app/com.jni.tzx--DSAI2wgWJ3_R3i0iUUeJA==/base.apk!/lib/arm64-v8a

LoadedApkgetClassLoader方法調用ApplicationLoaders.getClassLoader方法創建PathClassLoader

構造PathClassLoaderlibraryDir是來自ApplicationInfo.nativeLibraryDir

ApplicationInfo.png

nativeLibararyDir的賦值主要是在PackageManagerService中,通過apkRoot也就是sourceDir

VMRuntime.is64BitInstructionSet(getPrimaryInstructionSet(info));

再判斷是否為64位,確定是安裝包的lib目錄。最后根據AIB確定nativeLibararyDir

ABI相關初始化流程可以參考:Android中app進程ABI確定過程

nativeLoad

https://android.googlesource.com/platform/dalvik.git/+/android-4.2.2_r1/vm/native/java_lang_Runtime.cpp

/*
 * 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庫加載之后,補充兩個函數分別是:dlopendlsym函數。

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:

JavaVMJNIEnvJNI提供的結構體.

JavaVM 提供了允許你創建和銷毀JavaVMinvokation 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
so方法動態注冊.png
#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/LogisLoggable放方法;

#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

參考文章:

https://blog.csdn.net/fireroll/article/details/50102009

https://www.kancloud.cn/alex_wsc/androids/473614

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容