最近在弄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(major).
(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++的程序員。也就我這種半桶水還啥都要自己干的苦逼會遇到。