Android so鏈接的一些坑

SONAME缺失

前幾天遇到了個比較詭異的鏈接問題,分析下來感覺挺有意思的。

背景是我們導入了供應商給的幾個so,編譯成功之后在機器上運行出現鏈接報錯:

06-26 08:10:01.940 25976 25976 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: library "/Users/linjw/workspace/Demo/app/src/main/cpp/../../../libs/arm64-v8a/libcjson.so" not found: needed by /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libDemo.so in namespace classloader-namespace

libcjson.so的確是其中一個so,但可以看到它的運行報錯居然是去找我的開發電腦上的這個路徑:/Users/linjw/workspace/Demo/app/src/main/cpp/../../../libs/arm64-v8a/libcjson.so

這樣的問題首先我們可以在adb shell里面用readelf命令或者在開發電腦里的ndk目錄下找到對應abi的readelf工具看看libDemo.so的信息:

# readelf -d /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libDemo.so

Dynamic section at offset 0x3f6c8 contains 38 entries:
  Tag                Type                 Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [/Users/linjw/workspace/Demo/app/src/main/cpp/../../../libs/arm64-v8a/libcjson.so]
 0x0000000000000001 (NEEDED)             Shared library: [libcurl.so.4]
 0x0000000000000001 (NEEDED)             Shared library: [libcrypto.so.1.1]
 ...

可以看到的確有一個NEEDED配置的是/Users/linjw/workspace/Demo/app/src/main/cpp/../../../libs/arm64-v8a/libcjson.so,但是可以看到其他的像libcurl.so.4libcrypto.so.1.1也是供應商提供的,他們就沒有帶開發電腦的路徑。從CMake配置上看他們的配置方式是一樣的:

set(lib_path ${CMAKE_SOURCE_DIR}/../../../libs)

add_library(cjson SHARED IMPORTED)
set_target_properties(cjson PROPERTIES IMPORTED_LOCATION ${lib_path}/${ANDROID_ABI}/libcjson.so)

add_library(curl SHARED IMPORTED)
set_target_properties(curl PROPERTIES IMPORTED_LOCATION ${lib_path}/${ANDROID_ABI}/libcurl.so)

add_library(crypto SHARED IMPORTED)
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${lib_path}/${ANDROID_ABI}/libcrypto.so)

那么問題就只能出現在他們的so本身,我們繼續用readelf去對比看看這幾個so的區別:

1.png

可以看到libcrypto.solibcurl.so都是帶有SONAME的,但是libcjson.so沒有攜帶。我之前在其他的問題里面遇到過SONAME配錯了導致找不到符號的問題。看起鏈接器在鏈接的時候是使用so的SONAME字段而不是文件名去寫入target的NEEDED字段所以造成了這個問題。

so的幾個名字

這里我們再回顧下so幾個name的作用:

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

另外我們在java里面加載so填的也是linkname:

System.loadLibrary("Demo");

soname

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

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

如前面所見,soname會被記錄在so的二進制數據中。在鏈接目標程序的時候也會將soname填入目標程序的NEEDED字段記錄依賴,如果so里面沒有SONAME字段則將文件路徑打入目標程序的NEEDED字段。在加載目標程序的時候則是根據這個NEEDED去相應目錄加載${NEEDED}這個文件。

patchelf

如果我們有源碼,當然可以修改編譯配置把SONAME加入到libcjson.so,但是這個so是供應商提供的。我們可以先用patchelf工具嘗試給它加上SONAME驗證看看。下載patchelf-0.18.0-aarch64.tar.gz解壓出patchelf直接adb push到安卓機器上去運行:

patchelf --set-soname libcjson.so libcjson.so

然后再把修改后的libcjson.so用adb pull回來重新編譯app。運行之后可以發現前面的報錯的確沒有了,證明的確是SONAME缺失導致的。

so的版本號問題

但是卻出現了其他的報錯:

06-26 08:46:47.737 30092 30092 E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: library "libcrypto.so.1.1" not found: needed by /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libDemo.so in namespace classloader-namespace

這是由于libcrypto.so的SONAME字段是libcrypto.so.1.1,所以libDemo.so在鏈接它之后NEEDED字段填入的也是libcrypto.so.1.1:

# readelf -d /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libDemo.so

Dynamic section at offset 0x3f6c8 contains 38 entries:
  Tag                Type                 Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [/Users/linjw/workspace/Demo/app/src/main/cpp/../../../libs/arm64-v8a/libcjson.so]
 0x0000000000000001 (NEEDED)             Shared library: [libcurl.so.4]
 0x0000000000000001 (NEEDED)             Shared library: [libcrypto.so.1.1]
 ...

但我們導入apk的so名字是libcrypto.so,在安裝目錄只有libcrypto.so找不到libcrypto.so.1.1這個名字的so:

# ls /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/ | grep libcrypto
libcrypto.so

所以比較容易想到的是把libcrypto.so文件名改成libcrypto.so.1.1,在adb shell里面用mv命令修改名字運行時可以的。但代碼工程里面修改so名字再去編譯,實際編譯出來之后仍然報錯。這個時候在安裝目錄甚至都找不到libcrypto.so.1.1

原因就是雖然安卓系統是支持這種加載帶版本后綴的so,但是gradle在編譯apk的時候確是只會將.so后綴的文件打包到apk,所以安裝之后就缺失了這個so。

在Android上庫不是在系統范圍內安裝的它們總是應用程序包的一部分,所以so的版本標記是不必要的,谷歌就把這塊在打包的時候去掉了,但這樣的差異造成了在安卓上使用c/c++庫方面需要對so的版本號進行額外的處理。例如在編譯ffmpeg的時候編譯參數添加--target-os=android最終鏈接的時候就會添加-shared -Wl,-soname,$(SLIBNAME)參數指定soname為不帶版本后綴的SLIBNAME:

# ffmpeg-4.4.2 configure
...
SLIBPREF="lib"
SLIBSUF=".so"
SLIBNAME='$(SLIBPREF)$(FULLNAME)$(SLIBSUF)'
...
# OS specific
case $target_os in
    ...
    android)
        disable symver
        enable section_data_rel_ro
        add_cflags -fPIE
        add_ldexeflags -fPIE -pie
        SLIB_INSTALL_NAME='$(SLIBNAME)'
        SLIB_INSTALL_LINKS=
        SHFLAGS='-shared -Wl,-soname,$(SLIBNAME)'
        ;;
    ...

解決這個問題除了修改編譯配置重新編譯之外,如果沒有源代碼同樣可以用patchelflibcrypto.so的SONAME改成libcrypto.so,不過由于蠻多第三方庫交叉編譯之后都會出現帶版本后綴so文件名和soname的情況,這里我再提供兩個思路。

so的搜索路徑

一個是可以用rpath或者runpath去解決。

安卓默認會按照優先級搜索下面的路徑:

  • so文件的RPATH字段指的的目錄
  • LD_LIBRARY_PATH環境變量指定的目錄
  • so文件的RUNPATH字段指的的目錄
  • 應用的安裝目錄如上面的(/data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/)
  • 系統目錄如/system/lib64/、/vendor/lib64/、/system/apex/com.android.i18n/lib64/等

所以我們可以在CMakeLists.txt對libDemo.so添加如下link參數指定rpath到應用的內部私有目錄:

project("Demo")
...
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-rpath,/data/data/me.linjw.demo/cache")

然后在第一次運行的時候將帶有版本后綴的so拷貝到這個目錄下。然后加載libDemo.so的時候就會先去到這個rpath指的的目錄去搜索NEEDED so。

如果有多個目錄需要指定rpath可以用冒號分割,例如"-Wl,-rpath,/data/data/me.linjw.demo/cache:/data/data/me.linjw.demo/files"

另外從前面的搜索目錄來看,Linux并不會在可執行程序的當前目錄下去搜索so。而rpath還有個$ORIGIN變量它指定的是可執行程序的位置,例如我們寫的一個可執行程序依賴了某個so,可以將rpath指定為$ORIGIN,那么只要so和可執行程序在同一個目錄就能搜索到。

so緩存

另外一個是我們在load libDemo.so之前手動調用System.loadLibrary("crypto")去load libcrypto.so,然后load的時候讀取到SONAME是libcrypto.so.1.1放到緩存里,然后再load libDemo.so查找依賴的時候在緩存里面就能找libcrypto.so.1.1

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

推薦閱讀更多精彩內容

  • 前言 對于經常在服務器上跑程序或安裝程序的朋友,不可避免的會遇到一些問題。 其中最常見的問題就是,像下面這樣 gl...
    名本無名閱讀 1,431評論 0 3
  • qmake 的基本行為受到定義于每個項目中的構建過程的變量聲明的影響。其中一些聲明資源(如頭文件和源文件)對于每個...
    趙者也閱讀 6,511評論 0 2
  • 轉自鏈接1[https://jackwish.net/2016/android-dynamic-linker.ht...
    Ella_Eric閱讀 1,995評論 0 1
  • 嘗試使用官方的server demo和client demo程序https://wiki.openssl.org/...
    ChandlerBing閱讀 9,631評論 0 2
  • Android 6.0 新特性(源自官方文檔) Android 6.0(API 級別 23)除了提供諸多新特性和功...
    karlsu閱讀 7,836評論 0 5