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阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容