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.4和libcrypto.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的区别:
# readelf -d /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libcjson.so
Dynamic section at offset 0x8d90 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000001a (FINI_ARRAY) 0x9d78
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x0000000000000004 (HASH) 0x190
0x000000006ffffef5 (GNU_HASH) 0x4b8
0x0000000000000005 (STRTAB) 0x1110
0x0000000000000006 (SYMTAB) 0x768
0x000000000000000a (STRSZ) 1836 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x9f50
0x0000000000000002 (PLTRELSZ) 360 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x1a08
0x0000000000000007 (RELA) 0x1930
0x0000000000000008 (RELASZ) 216 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x000000006ffffffe (VERNEED) 0x1910
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x183c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
# readelf -d /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libcurl.so
Dynamic section at offset 0x45978 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libcurl.so]
0x000000000000001a (FINI_ARRAY) 0x45768
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x0000000000000004 (HASH) 0x50000
0x000000006ffffef5 (GNU_HASH) 0x638
0x0000000000000005 (STRTAB) 0x504a8
0x0000000000000006 (SYMTAB) 0x8c0
0x000000000000000a (STRSZ) 1932 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x46b48
0x0000000000000002 (PLTRELSZ) 2808 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x49d8
0x0000000000000007 (RELA) 0x2128
0x0000000000000008 (RELASZ) 10416 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x000000006ffffffe (VERNEED) 0x2108
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x1fba
0x000000006ffffff9 (RELACOUNT) 419
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
# readelf -d /data/app/~~Y8XCESOaI01yUY_5GwBPeg==/me.linjw.demo-HHKb0jSb43YhjfSIfuxutw==/lib/arm64/libcrypto.so
Dynamic section at offset 0x20ffc8 contains 32 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [libcrypto.so.1.1]
0x0000000000000010 (SYMBOLIC) 0x0
0x000000000000001a (FINI_ARRAY) 0x1e9560
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x0000000000000004 (HASH) 0x190
0x000000006ffffef5 (GNU_HASH) 0x8888
0x0000000000000005 (STRTAB) 0x2c6f0
0x0000000000000006 (SYMTAB) 0x11d98
0x000000000000000a (STRSZ) 88060 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x2111c8
0x0000000000000002 (PLTRELSZ) 2880 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x7f438
0x0000000000000007 (RELA) 0x444d0
0x0000000000000008 (RELASZ) 241512 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffc (VERDEF) 0x44260
0x000000006ffffffd (VERDEFNUM) 16
0x000000000000001e (FLAGS) SYMBOLIC BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW NODELETE
0x000000006ffffffe (VERNEED) 0x44490
0x000000006fffffff (VERNEEDNUM) 2
0x000000006ffffff0 (VERSYM) 0x41eec
0x000000006ffffff9 (RELACOUNT) 10058
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
0x0000000000000000 (NULL) 0x0
可以看到libcrypto.so和libcurl.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。如果不指定的话soname默认为realname:
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)'
;;
...
解决这个问题除了修改编译配置重新编译之外,如果没有源代码同样可以用patchelf把libcrypto.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
👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀