JNI運行錯誤-符號未定義

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

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

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

問題還原

這里用個簡單的demo還原下問題,JNI部分調(diào)用c庫里面的getString函數(shù)返回字符串:

const char *getString(); // 這個函數(shù)的定義在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去調(diào)用到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"...

這樣奔潰其實挺常見的,因為編譯的時候已經(jīng)通過了,證明編譯的時候是可以找到這個符號的,但是運行的時候沒有找到,無非是so沒有導(dǎo)入到apk里面,解壓apk發(fā)現(xiàn)的確如此:

~/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仍未導(dǎo)入apk

難道是gradle配置沒有起作用?解壓apk之后發(fā)現(xiàn)libdemo.so是有導(dǎo)入的:

~/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里面找到,最好找到對應(yīng)cpu架構(gòu)的目錄下的工具。我編譯的是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依賴

其實之后我就在這里卡了很久,感覺哪里都對就結(jié)果不對。后面到處搜索也沒有找到有人遇到類似的情況。后面是在用readelf分析發(fā)現(xiàn)它的依賴有些詭異:

~/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,而且還依賴了它自己。

當(dāng)時我就震驚了,還能有這種操作?

反復(fù)查看cmake配置的依賴配置,沒有發(fā)現(xiàn)問題:

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的幾個名字

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

realname

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

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

linkname

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

gcc main.c -L. -lfoo

soname

soname顧名思義就是so的名字,它可以在編譯的時候用?Wl,?soname,$(soname)指定,-Wl,表示后面的參數(shù)將傳給link程序ld:

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

Soname會被記錄在so的二進(jìn)制數(shù)據(jù)中,我們可以用readelf命令查看:

readelf  -d libfoo.so.0.0.0

Dynamic section at offset 0xf18 contains 25 entries:
  標(biāo)記        類型                         名稱/值
 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庫,然后創(chuàng)建一個軟連接libfoo.so指向它,接著用foo這個linkname指定這個軟鏈接去編譯demo。

最后使用ldd查看demo的依賴,發(fā)現(xiàn)它依賴的是libfoo.so.0這個soname而不是編譯的時候使用的libfoo.so。用readelf查看demo也能看到:

$ readelf -d demo

Dynamic section at offset 0xf10 contains 25 entries:
  標(biāo)記        類型                         名稱/值
 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的二進(jìn)制信息。于是如果這個時候執(zhí)行demo的話就會報找不到libfoo.so.0的問題:

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

問題原因

好了,現(xiàn)在回到我們的問題。最后我們分析到libdemo.so的soname居然是libnative-lib.so,那么原因很容易猜到就是?Wl,?soname指定錯了。

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

然后這里還有一個坑,我一開始是直接報?Wl,?soname,libnative-lib.so這段給刪掉了,因為使用gcc的時候如果沒有指定,會自動把realname當(dāng)做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。而如果需要導(dǎo)入外部的so一般也是用的第三方的,他們也很難出這種低級問題。就算像我這樣的需求自己寫個外部的so導(dǎo)入,干這活的一般也是個成熟的c/c++的程序員。也就我這種半桶水還啥都要自己干的苦逼會遇到。

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

推薦閱讀更多精彩內(nèi)容