盒子
盒子
文章目录
  1. SONAME缺失
    1. so的几个名字
      1. realname
      2. linkname
      3. soname
    2. patchelf
  2. so的版本号问题
    1. so的搜索路径
    2. so缓存

Android so链接的一些坑

SONAME缺失

前几天遇到了个比较诡异的链接问题,分析下来感觉挺有意思的。

背景是我们导入了供应商给的几个so,编译成功之后在机器上运行出现链接报错:

1
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的信息:

1
2
3
4
5
6
7
8
# 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配置上看他们的配置方式是一样的:

1
2
3
4
5
6
7
8
9
10
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的区别:

可以看到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参数指定:

1
gcc -shared -o $(realname) …

linkname

linkname是在链接时使用的,用-l参数指定例如下面的foo就是linkname。我们在这里不需要填so文件的名字,gcc会自动为linkname补上lib和.so,去链接lib$(name).so

1
gcc main.c -L. -lfoo

另外我们在java里面加载so填的也是linkname:

1
System.loadLibrary("Demo");

soname

soname顾名思义就是so的名字,它可以在编译的时候用−Wl,−soname,${soname}指定,-Wl,表示后面的参数将传给link程序ld:

1
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到安卓机器上去运行:

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

然后再把修改后的libcjson.so用adb pull回来重新编译app。运行之后可以发现前面的报错的确没有了,证明的确是SONAME缺失导致的。

so的版本号问题

但是却出现了其他的报错:

1
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:

1
2
3
4
5
6
7
8
# 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:

1
2
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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到应用的内部私有目录:

1
2
3
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