从Android 原生库 (.so) 中里挖掘一些有用的信息
当一个 Android APP 需要集成别的地方来的原生库(.so)时,你可能也会跟我一样会有那么几点疑惑:
- 这个 so 用的什么 NDK 版本编译的?会不会跟项目里其它的so 冲突,尤其项目里使用共享 C++ STL的情况下
ANDROID_STL=c++_shared
,一个应用不能使用多个 C++ 运行时 - 这个 so 目标 Android API 等级是多少?会不会大于项目的
minSdkVersion
? - 这个 so 依赖(链接)其它哪些 so?这些 so 有没有都放进项目里?
- 这个 so 有没有除了用文件哈希之外唯一编号,用来标识崩溃堆栈等?
ps. 本文假定读者有一定 Android Native 开发经验,且理解一些基本的概念。
查看 so 的 NDK 版本信息
通过 readelf
工具查看 Android NDK 编译出来的 so 的 Section headers
里有什么 Android 特有的玩意。
ps. 可以用 ndk-which
找到 NDK 中预编译好的 readelf
:
1 | $ $ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf |
定义一个名为 readelf
的 alias 方便在 Terminal 中调用 aarch64-linux-android-readelf
1 | $ alias readelf=`$ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf` |
以 NDK 中带的 libc++_shared.so
为例,在我本机上路径是$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so
:
1 | $ readelf -WS $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so |
There are 36 section headers, starting at offset 0x5f2640: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .note.gnu.build-id NOTE 0000000000000200 000200 000024 00 A 0 0 4 [ 2] .hash HASH 0000000000000228 000228 004484 04 A 4 0 8 [ 3] .gnu.hash GNU_HASH 00000000000046b0 0046b0 004a70 00 A 4 0 8 [ 4] .dynsym DYNSYM 0000000000009120 009120 00da70 18 A 5 3 8 [ 5] .dynstr STRTAB 0000000000016b90 016b90 01a958 00 A 0 0 1 [ 6] .gnu.version VERSYM 00000000000314e8 0314e8 001234 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 0000000000032720 032720 000040 00 A 5 2 8 [ 8] .rela.dyn RELA 0000000000032760 032760 0125e8 18 A 4 0 8 [ 9] .rela.plt RELA 0000000000044d48 044d48 0022f8 18 AI 4 21 8 [10] .plt PROGBITS 0000000000047040 047040 001770 10 AX 0 0 16 [11] .text PROGBITS 00000000000487b0 0487b0 06daf0 00 AX 0 0 4 [12] .rodata PROGBITS 00000000000b62a0 0b62a0 005d5d 00 A 0 0 16 [13] .eh_frame_hdr PROGBITS 00000000000bc000 0bc000 003d14 00 A 0 0 4 [14] .eh_frame PROGBITS 00000000000bfd18 0bfd18 010880 00 A 0 0 8 [15] .gcc_except_table PROGBITS 00000000000d0598 0d0598 005464 00 A 0 0 4 [16] .note.android.ident NOTE 00000000000d59fc 0d59fc 000098 00 A 0 0 4 [17] .init_array INIT_ARRAY 00000000000d7018 0d6018 000008 08 WA 0 0 8 [18] .fini_array FINI_ARRAY 00000000000d7020 0d6020 000010 08 WA 0 0 8 [19] .data.rel.ro PROGBITS 00000000000d7030 0d6030 006cc8 00 WA 0 0 8 [20] .dynamic DYNAMIC 00000000000ddcf8 0dccf8 0001f0 10 WA 5 0 8 [21] .got PROGBITS 00000000000ddee8 0dcee8 001118 08 WA 0 0 8 [22] .data PROGBITS 00000000000df000 0de000 0000f8 00 WA 0 0 8 [23] .bss NOBITS 00000000000df100 0de0f8 003300 00 WA 0 0 16 [24] .comment PROGBITS 0000000000000000 0de0f8 0000dc 01 MS 0 0 1 [25] .debug_aranges PROGBITS 0000000000000000 0de1d4 0000f0 00 0 0 1 [26] .debug_info PROGBITS 0000000000000000 0de2c4 192c55 00 0 0 1 [27] .debug_abbrev PROGBITS 0000000000000000 270f19 00e1ed 00 0 0 1 [28] .debug_line PROGBITS 0000000000000000 27f106 070a59 00 0 0 1 [29] .debug_str PROGBITS 0000000000000000 2efb5f 088967 01 MS 0 0 1 [30] .debug_loc PROGBITS 0000000000000000 3784c6 1c11ea 00 0 0 1 [31] .debug_macinfo PROGBITS 0000000000000000 5396b0 000034 00 0 0 1 [32] .debug_ranges PROGBITS 0000000000000000 5396e4 064e40 00 0 0 1 [33] .shstrtab STRTAB 0000000000000000 5f24c9 000173 00 0 0 1 [34] .symtab SYMTAB 0000000000000000 59e528 026b68 18 35 4280 8 [35] .strtab STRTAB 0000000000000000 5c5090 02d439 00 0 0 1
可以看到一个名字里带android
的 NOTE Section:.note.android.ident
。
ps. NOTE Section 的数据结构
namesz | 32-bit, size of "name" field |
descsz | 32-bit, size of "desc" field |
type | 32-bit, vendor specific "type" |
name | "namesz" bytes, null-terminated string |
desc | "descsz" bytes, binary data |
(详见文档 Program Header # Note Section)
搜索 Android NDK 相关源码,可以看到 .note.android.ident
section 是由这个 ndk/sources/crt/crtbrand.S 汇编代码文件引入的,其中放了包括 android_api
、ndk_version
和 ndk_build_number
三段信息。
用readelf
的 string-dump 功能将其 dump 出来:
1 | $ readelf --string-dump=.note.android.ident $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so |
String dump of section '.note.android.ident': [ c] Android [ 18] r21e [ 58] 7075529
得到编译这个libc++_shared.so
NDK 的版本是 r21e。
换一个其它 NDK 版本libc++_shared.so
的试试:
1 | $ ANDROID_NDK_HOME=$ANDROID_HOME/ndk/23.1.7779620 |
String dump of section '.note.android.ident': [ c] Android [ 18] r23b [ 58] 7779620
Android API 等级怎么不见了呢?
根据 .note.android.ident
的源代码文件 ndk/sources/crt/crtbrand.S 的定义:
1 | note_data: |
note_data
它是个 int32_t,不被string-dump
识别,换成 hex-dump
再看:
1 | $ readelf --hex-dump=.note.android.ident $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so |
Hex dump of section '.note.android.ident': 0x000d59fc 08000000 84000000 01000000 416e6472 ............Andr 0x000d5a0c 6f696400 15000000 72323165 00000000 oid.....r21e.... 0x000d5a1c 00000000 00000000 00000000 00000000 ................ 0x000d5a2c 00000000 00000000 00000000 00000000 ................ 0x000d5a3c 00000000 00000000 00000000 00000000 ................ 0x000d5a4c 00000000 00000000 37303735 35323900 ........7075529. 0x000d5a5c 00000000 00000000 00000000 00000000 ................ 0x000d5a6c 00000000 00000000 00000000 00000000 ................ 0x000d5a7c 00000000 00000000 00000000 00000000 ................ 0x000d5a8c 00000000 00000000 ........
参考 NOTE Section 数据结构,可得到如下数据布局图
|
|
ps. 注意为该 so 为小端序(Little Endian),可以用 readelf -h libxxx.so
来确认:
1 | $ readelf -h libc++_shared.so |
其中 0x000d5a0c +4
(“oid\0” 后面)的一个 32 位 int 15000000 (亦即小端序的 int 0x15
)其实就是 Android API,将该 int 值进制转换一下:
1 | $ printf "%d\n" 0x15 |
可知,其 Android API 等级 为 21。
综合起来,通过 dump so 文件的 .note.android.ident
section 数据内容,即可得到 Android API 等级和 NDK 版本信息。
直接用 readelf
看是够用了,但如何集成到类似 CI 的环境里,让打包脚本也能用呢?
我们可以用 llvm-objcopy
将.note.android.ident
整个copy 到一个文件中,后续再写一个简单的脚本按 NOTE Section 数据结构解析处理即可。
llvm-objcopy
也在 NDK 工具链中:在我本机的路径是 $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-objcopy
1 | $ alias objcopy=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-objcopy |
用 hex dump 的形式查看 copy出来的文件:
1 | $ xxd libc++_shared.so.android.note |
00000000: 0800 0000 8400 0000 0100 0000 416e 6472 ............Andr 00000010: 6f69 6400 1500 0000 7232 3165 0000 0000 oid.....r21e.... 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000050: 0000 0000 0000 0000 3730 3735 3532 3900 ........7075529. 00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000090: 0000 0000 0000 0000 ........
如果你搜索过 Android NDK 源码的话,会发现其实就在源代码仓库里 ⊙.⊙ 有一个 python 脚本: ndk/parse_elfnote.py, 可以直接用来解析 .note.android.ident
1 | $ python3 parse_elfnote.py --ndk=$ANDROID_NDK_HOME \ |
----------ABI INFO---------- ABI_ANDROID_API: 21 ABI_NDK_VERSION: r21e ABI_NDK_BUILD_NUMBER: 7075529
查看 so 依赖的 so
so 的依赖信息,其实就放在它的 .dynamic
section 里,详见 Shared Object Dependencies。
ps. .dynamic
section 内容数据结构
1 | typedef struct { |
(详解见文档 Dynamic Section )
依然通过 readelf
工具可以直接 dump .dynamic
section 信息: readelf -d libxxx.so
。
ps. 此处以 com.google.ar.core-1.38.0 为例。
1 | $ curl -OL "https://dl.google.com/android/maven2/com/google/ar/core/1.38.0/core-1.38.0.aar" |
1 | $ readelf -d core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so |
Dynamic section at offset 0x17018 contains 31 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libarcore_sdk_c.so] 0x0000000000000001 (NEEDED) Shared library: [libandroid.so] 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x000000000000000e (SONAME) Library soname: [libarcore_sdk_jni.so] 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x0000000000000007 (RELA) 0x7d80 0x0000000000000008 (RELASZ) 192 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) ...
1 | $ readelf -d core-1.38.0/jni/arm64-v8a/libarcore_sdk_c.so |
Dynamic section at offset 0x10018 contains 30 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libandroid.so] 0x0000000000000001 (NEEDED) Shared library: [liblog.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so] 0x0000000000000001 (NEEDED) Shared library: [libm.so] 0x0000000000000001 (NEEDED) Shared library: [libdl.so] 0x000000000000000e (SONAME) Library soname: [libarcore_sdk_c.so] 0x000000000000001e (FLAGS) BIND_NOW 0x000000006ffffffb (FLAGS_1) Flags: NOW 0x0000000000000007 (RELA) 0x5e40 0x0000000000000008 (RELASZ) 168 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffff9 (RELACOUNT) 7
可以看到 libarcore_sdk_jni.so
NEEDED libarcore_sdk_c.so
和其它Android 系统库(libandroid.so
,liblog.so
等),libarcore_sdk_c.so
除了系统库之外不依赖其它so。
将这样的依赖信息用 Graphviz Dot 语言 描述出来,如下:
1 | digraph libarcore_sdk_jni { |
可以得到 libarcore_sdk_jni.so
的 so 依赖 DAG(有向无环图):
你可将过程写到一个脚本里,即可得到一个解析so的依赖并生成依赖DAG 的工具。
如果不想要在依赖图里画上系统库,可以从 NDK 目录下的 $ANDROID_NDK_HOME/meta/system_libs.json
中获取系统库的list,并将其从digraph
中排除:
1 | import os |
如下是从 https://app.bilibili.com/ 中下载的 apk 的部分so 依赖图:
又如从 https://weixin.qq.com/ 下载的 apk 的部分so 依赖图:
ps. 有部份 so 的 Vendor 会将 .dynamic
Section 的名字给删掉,导致 readelf -d
读不了,可以使用 readelf -d -–use-dynamic
1 | $ readelf -d --use-dynamic iBiliPlayer-bili/lib/arm64-v8a/libmsaoaidauth.so |
查看 so 的 Build ID
在 Terminal 中使用 file
命令,可以查看 so
的基本信息:
1 | $ file core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so |
可以看到其中有一条BuildID[md5/uuid]=c6cb00844756c8d0504c8c15dd4340bc
,这个就是该 so 的唯一编码 Build ID
该Build ID
其实是放在 so 的 .note.gnu.build-id
NOTE Section 中,通过启用 linker 的 --build-id
选项生成。
1 | $ readelf --hex-dump=.note.gnu.build-id core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so |
Android NDK 会自动添加编译参数 -Wl,--build-id
,用于给最终生成的 so 带上 Build ID
:
1 | string(APPEND _ANDROID_NDK_INIT_LDFLAGS " -Wl,--build-id=sha1") |
(详见 ndk/build/cmake/flags.cmake)
ps. 这个 Build ID
一般是用来给调试器(如lldb)识别 so debug 符号表用的。通常情况下,带 debug info 的未被 stripped 的 so 和已经被 stripped so 的 Build ID
是一样的。
1 | $ file unstripped/arm64-v8a/libijkplayer.so |
修改上文从 NDK 源码中下载的 parse_elfnote.py
, 加上 .note.gnu.build-id
的解析功能:
1 | #... |
(完整代码见gist)
1 | $ python3 parse_elfnote.py --ndk=$ANDROID_NDK_HOME core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so |
总结
Android 的原生库(.so)其实一个是 ELF 格式的文件,可以直接通过 readelf
等工具解析 ELF 我们所需要的信息所对应 Section Data。再将解析过程脚本化,方便日常开发工作。
Refs
- Static, Shared Dynamic and Loadable Linux Libraries
- Android Code Search
- Executable and Linkable Format
- https://www.sco.com/developers/gabi/latest/ch4.sheader.html
- https://www.sco.com/developers/gabi/latest/ch5.pheader.html
- https://www.sco.com/developers/gabi/latest/ch5.dynamic.html
- https://llvm.org/docs/CommandGuide/llvm-objcopy.html
- https://graphviz.org/doc/info/lang.html
- https://en.wikipedia.org/wiki/Directed_acyclic_graph
- https://developer.android.com/ndk/guides/middleware-vendors
- https://fedoraproject.org/wiki/RolandMcGrath/BuildID
Author: Yrom
Link: https://yrom.net/blog/2023/07/25/useful-info-of-android-so/
License: 知识共享署名-非商业性使用 4.0 国际许可协议