JNI運行錯誤-符號未定義 問題還原 問題分析 so的幾個名字 問題原因

最近在弄ndk的時候遇到了個比較坑的問題,雖然最後發現原因挺低級的,但是的確花了我不少時間去查找,中間的分析手法可能不熟悉c/c++的同學會比較陌生,如果遇到的同樣問題的話會無從下手。這裏把整個分析的流程記錄下來,希望有用。

背景項目分兩個部分,自己編寫的c庫工程,和安卓工程,將它們分離的原因是這個c庫的功能可能在其他的地方也能使用到。

由於項目只是初始階段,爲了驗證流程,我先搭了個簡單的demo框架,用c庫工程編譯出so之後導入到安卓工程。雖然整個代碼比較簡單,但是運行的時候直接就崩潰了,報找不到符號的異常。

問題還原

這裏用個簡單的demo還原下問題,JNI部分調用c庫裏面的getString函數返回字符串:

const char *getString(); // 這個函數的定義在c庫工程編譯出來的so庫裏面

extern "C" JNIEXPORT jstring JNICALL
Java_com_cvte_tv_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    return env->NewStringUTF(getString());
}

c庫的代碼也很簡單,就返回字符串,我們會將它編譯成libdemo.so:

const char* getString() {
    return "Hello world!\n";
}

cmake配置也很簡單,我們的jni編譯了一個libnative-lib.so依賴libdemo.so,java層通過這個libnative-lib.so去調用到libdemo.so裏面的getString:

cmake_minimum_required(VERSION 3.4.1)

add_library(native-lib SHARED native-lib.cpp)

add_library(demo SHARED IMPORTED)

set_target_properties(demo PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/jniLibs/${ANDROID_ABI}/libdemo.so)

target_link_libraries(native-lib  demo)

運行之後報的問題看起來也很簡單:

java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "_Z9getStringv" referenced by "/data/app/com.cvte.tv.ndkdemo-xD9KLsO5Wmh_YGDKRKL5lA==/lib/arm64/libnative-lib.so"...

這樣奔潰其實挺常見的,因爲編譯的時候已經通過了,證明編譯的時候是可以找到這個符號的,但是運行的時候沒有找到,無非是so沒有導入到apk裏面,解壓apk發現的確如此:

~/workspace/NDKDemo/app/build/outputs/apk/debug/app-debug/lib  tree
.
└── arm64-v8a
    └── libnative-lib.so

1 directory, 1 file

這種問題的原因在於jniLibs.srcDirs沒有配置,我的so是放在app/src/main/cpp/jniLibs目錄裏面的,所以在build.gradle裏面添加下面配置即可:


android {
    ...
    sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/cpp/jniLibs']
        }
    }
}

修改完之後滿心歡喜的重新編譯運行,立馬啪啪打臉,依然找不到_Z9getStringv

問題分析

疑點一: so仍未導入apk

難道是gradle配置沒有起作用?解壓apk之後發現libdemo.so是有導入的:

~/workspace/NDKDemo/app/build/outputs/apk/debug/app-debug/lib  tree .
.
└── arm64-v8a
    ├── libdemo.so
    └── libnative-lib.so

1 directory, 2 files

疑點二: so裏面沒有這個符號

難道是libdemo.so裏面的確沒有這個符號?我們可以用nm工具去查看so裏面的符號。這個nm命令可以在ndk裏面找到,最好找到對應cpu架構的目錄下的工具。我編譯的是arm64-v8a的so,可以用aarch64-linux-android下面的nm工具:

~/Library/Android/sdk/ndk/20.0.5594570  ./toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/aarch64-linux-android/bin/nm  ~/workspace/NDKDemo/app/src/main/cpp/jniLibs/arm64-v8a/libdemo.so | grep getString
0000000000000538 T _Z9getStringv

輸出顯示沒毛病,so裏面的確是有_Z9getStringv這個符號的。

疑點三: 詭異的so依賴

其實之後我就在這裏卡了很久,感覺哪裏都對就結果不對。後面到處搜索也沒有找到有人遇到類似的情況。後面是在用readelf分析發現它的依賴有些詭異:

~/Library/Android/sdk/ndk/20.0.5594570  ./toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/aarch64-linux-android/bin/readelf -d ~/workspace/NDKDemo/app/build/outputs/apk/debug/app-debug/lib/arm64-v8a/libnative-lib.so

Dynamic section at offset 0xdd8 contains 26 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libnative-lib.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000000e (SONAME)             Library soname: [libnative-lib.so]
 ...

我們可以看到libnative-lib.so這個庫它不但沒有依賴libdemo.so,而且還依賴了它自己。

當時我就震驚了,還能有這種操作?

反覆查看cmake配置的依賴配置,沒有發現問題:

cmake_minimum_required(VERSION 3.4.1)

add_library(native-lib SHARED native-lib.cpp)

add_library(demo SHARED IMPORTED)

set_target_properties(demo PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/jniLibs/${ANDROID_ABI}/libdemo.so)

target_link_libraries(native-lib  demo)

疑點四: 詭異的SONAME

我也卡了很久一直在cmake裏面找原因,以爲是編譯libnative-lib.so的時候出了問題。後面實在沒有頭緒,無意中用readelf看了下libdemo.so:

~/Library/Android/sdk/ndk/20.0.5594570  ./toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/aarch64-linux-android/bin/readelf -d ~/workspace/NDKDemo/app/build/outputs/apk/debug/app-debug/lib/arm64-v8a/libdemo.so

Dynamic section at offset 0xdf8 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000000e (SONAME)             Library soname: [libnative-lib.so]
...

它的SONAME居然是libnative-lib.so,問題肯定就是出在這裏了...

so的幾個名字

到了這一步,我們已經找到了問題的原因所在。但是要去解決它的話,我們還需要了解一些基礎知識,這裏也順便普及下。so庫的名字其實分三種realname、linkname和soname。

realname

realname實際上就是so的文件名,一般格式爲lib(name).so.(major).(minor).(revision)例如libcurl.so.4.5.0,我們可以在編譯的時候用-o參數指定:

gcc -shared -o $(realname) ...

linkname

linkname是在鏈接時使用的,用-l參數指定例如下面的foo就是linkname。我們在這裏不需要填so文件的名字,gcc會自動爲linkname補上lib和.so,去鏈接lib$(name).so

gcc main.c -L. -lfoo

soname

soname顧名思義就是so的名字,它可以在編譯的時候用−Wl,−soname,$(soname)指定,-Wl,表示後面的參數將傳給link程序ld。如果不指定的話soname默認爲realname:

gcc -shared -fPIC -Wl,-soname,libfoo.so.0 -o libfoo.so.0.0.0 foo.c

Soname會被記錄在so的二進制數據中,我們可以用readelf命令查看:

readelf  -d libfoo.so.0.0.0

Dynamic section at offset 0xf18 contains 25 entries:
  標記        類型                         名稱/值
 0x00000001 (NEEDED)                     共享庫:[libc.so.6]
 0x0000000e (SONAME)                     Library soname: [libfoo.so.0]
 ...

那它有什麼作用呢,我們可以做個試驗:

$ gcc -shared -fPIC -Wl,-soname,libfoo.so.0 -o libfoo.so.0.0.0 foo.c
$ ln -s libfoo.so.0.0.0 libfoo.so
$ gcc main.c -L. -lfoo -o demo
$ ldd demo
        linux-vdso.so.1 (0xbece4000)
        /usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so => /usr/lib/arm-linux-gnueabihf/libarmmem-v7l.so (0xb6ef5000)
        libfoo.so.0 => not found
        libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6d8f000)
        /lib/ld-linux-armhf.so.3 (0xb6f0a000)

我們先編譯了一個realname爲libfoo.so.0.0.0,soname爲libfoo.so.0的so庫,然後創建一個軟連接libfoo.so指向它,接着用foo這個linkname指定這個軟鏈接去編譯demo。

最後使用ldd查看demo的依賴,發現它依賴的是libfoo.so.0這個soname而不是編譯的時候使用的libfoo.so。用readelf查看demo也能看到:

$ readelf -d demo

Dynamic section at offset 0xf10 contains 25 entries:
  標記        類型                         名稱/值
 0x00000001 (NEEDED)                     共享庫:[libfoo.so.0]
 0x00000001 (NEEDED)                     共享庫:[libc.so.6]
...

也就是說在編譯demo這個程序的時候,會通過linkname找到libfoo.so,它是個軟鏈接實際指向libfoo.so.0.0.0,然後gcc會從libfoo.so.0.0.0裏面讀取soname寫入demo的二進制信息。於是如果這個時候執行demo的話就會報找不到libfoo.so.0的問題:

$ ./demo
./demo: error while loading shared libraries: libfoo.so.0: cannot open shared object file: No such file or directory

問題原因

好了,現在回到我們的問題。最後我們分析到libdemo.so的soname居然是libnative-lib.so,那麼原因很容易猜到就是−Wl,−soname指定錯了。

查看編譯記錄的確是這個問題:由於新版本的ndk已經放棄gcc轉向clang,我前段時間剛好換了電腦下載的是比較新的ndk,裏面找不到熟悉的gcc了而我之前又沒有用過clang。所以編譯的指令是從android studio編譯libnative-lib.so的日誌裏面拷貝修改的。它有很大一坨,又由於粗心,只改了-o 參數和.c文件,沒有修改soname,然後問題就出現了。

然後這裏還有一個坑,我一開始是直接報−Wl,−soname,libnative-lib.so這段給刪掉了,因爲使用gcc的時候如果沒有指定,會自動把realname當做soname,但是clang不會。這個時候編譯出來的so裏面沒有SONAME字段:

$ readelf -d libdemo.so

Dynamic section at offset 0xe08 contains 24 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000001a (FINI_ARRAY)         0x10df8

於是在運行的時候又會報找不到libdemo.so。也就是說在運行的時候查找依賴的原理是:從libnative-lib.so讀到依賴libdemo.so,找到libdemo.so之後還會驗證它的soname對不對,如果你只是realname爲libdemo.so,soname不匹配也是不會去鏈接的。

最後將−Wl,−soname,libdemo.so加回上去問題解決。

事後回想了下,其實這種問題遇到的機率還是比較小的。因爲如果c部分是我們自己寫的,一般也就放到android stduio裏面合成一個so。而如果需要導入外部的so一般也是用的第三方的,他們也很難出這種低級問題。就算像我這樣的需求自己寫個外部的so導入,幹這活的一般也是個成熟的c/c++的程序員。也就我這種半桶水還啥都要自己乾的苦逼會遇到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章