当一个 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
2
$ $ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf
/~/ndk/21.4.7075529/prebuilt/darwin-x86_64/bin/../../../toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf

定义一个名为 readelf 的 alias 方便在 Terminal 中调用 aarch64-linux-android-readelf

1
2
3
4
5
$ alias readelf=`$ANDROID_NDK_HOME/ndk-which --abi arm64-v8a readelf`
$ readelf -v
GNU readelf (GNU Binutils) 2.27.0.20170315
Copyright (C) 2016 Free Software Foundation, Inc.
...

以 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 的数据结构

namesz32-bit, size of "name" field
descsz32-bit, size of "desc" field
type32-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_apindk_versionndk_build_number 三段信息。

readelfstring-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
2
$ ANDROID_NDK_HOME=$ANDROID_HOME/ndk/23.1.7779620
$ 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]  r23b
    [    58]  7779620

Android API 等级怎么不见了呢?
根据 .note.android.ident 的源代码文件 ndk/sources/crt/crtbrand.S 的定义:

1
2
3
note_data:
// int32_t android_api
.long PLATFORM_SDK_VERSION

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 数据结构,可得到如下数据布局图


namesz
descsz
type
name

desc


+0+1+2+3
0x08
0x84
0x01
Andr
oidpad
android_api
ndk_version
ndk_build_number

ps. 注意为该 so 为小端序(Little Endian),可以用 readelf -h libxxx.so 来确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ readelf -h libc++_shared.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x0
Start of program headers: 64 (bytes into file)
Start of section headers: 95168 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 24
Section header string table index: 23

其中 0x000d5a0c +4 (“oid\0” 后面)的一个 32 位 int 15000000 (亦即小端序的 int 0x15)其实就是 Android API,将该 int 值进制转换一下:

1
2
$ printf "%d\n" 0x15
21

可知,其 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
2
3
$ alias objcopy=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-objcopy

$ objcopy --dump-section=.note.android.ident=libc++_shared.so.android.note $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so

用 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
2
$ python3 parse_elfnote.py --ndk=$ANDROID_NDK_HOME \
$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so
----------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
2
3
4
5
6
7
8
9
typedef struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;

extern Elf64_Dyn _DYNAMIC[];

(详解见文档 Dynamic Section

依然通过 readelf 工具可以直接 dump .dynamic section 信息: readelf -d libxxx.so

ps. 此处以 com.google.ar.core-1.38.0 为例。

1
2
$ curl -OL "https://dl.google.com/android/maven2/com/google/ar/core/1.38.0/core-1.38.0.aar"
$ unzip -d core-1.38.0 core-1.38.0.aar "jni/arm*"
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.soliblog.so等),libarcore_sdk_c.so除了系统库之外不依赖其它so。

将这样的依赖信息用 Graphviz Dot 语言 描述出来,如下:

libarcore_sdk_jni.dot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
digraph libarcore_sdk_jni {
libarcore_sdk_jni [label="libarcore_sdk_jni.so"]
libandroid [label="libandroid.so"]
liblog [label="liblog.so"]
libc [label="libc.so"]
libm [label="libm.so"]
libdl [label="libdl.so"]
libarcore_sdk_jni -> { libarcore_sdk_c libandroid liblog libc libm libdl }
libarcore_sdk_c [label="libarcore_sdk_c.so"]
libandroid [label="libandroid.so"]
liblog [label="liblog.so"]
libc [label="libc.so"]
libm [label="libm.so"]
libdl [label="libdl.so"]
libarcore_sdk_c -> { libandroid liblog libc libm libdl }
}

可以得到 libarcore_sdk_jni.so 的 so 依赖 DAG(有向无环图)

上图的 Graphviz Visual Editor 链接

你可将过程写到一个脚本里,即可得到一个解析so的依赖并生成依赖DAG 的工具。

如果不想要在依赖图里画上系统库,可以从 NDK 目录下的 $ANDROID_NDK_HOME/meta/system_libs.json 中获取系统库的list,并将其从digraph 中排除:

1
2
3
4
5
6
import os
import json
android_ndk = os.getenv("ANDROID_NDK_HOME")
with open(os.path.join(android_ndk, 'meta', 'system_libs.json'), mode='rt') as f:
system_libs_json = json.load(f)
system_libs = list(system_libs_json.keys())

如下是从 https://app.bilibili.com/下载的 apk 的部分so 依赖图:

又如从 https://weixin.qq.com/ 下载的 apk 的部分so 依赖图:

ps. 有部份 so 的 Vendor 会将 .dynamic Section 的名字给删掉,导致 readelf -d 读不了,可以使用 readelf -d -–use-dynamic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ readelf -d --use-dynamic iBiliPlayer-bili/lib/arm64-v8a/libmsaoaidauth.so
readelf: Error: no .dynamic section in the dynamic segment

Dynamic section at offset 0x3f6f0 contains 32 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libz.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libstdc++.so]
0x000000000000000e (SONAME) Library soname: [libmsaoaidauth.so]
0x000000000000000c (INIT) 0x12fe4
0x0000000000000019 (INIT_ARRAY) 0x3f020
...

查看 so 的 Build ID

在 Terminal 中使用 file 命令,可以查看 so 的基本信息:

1
2
$ file core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so 
core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[md5/uuid]=c6cb00844756c8d0504c8c15dd4340bc, stripped

可以看到其中有一条BuildID[md5/uuid]=c6cb00844756c8d0504c8c15dd4340bc,这个就是该 so 的唯一编码 Build ID

Build ID 其实是放在 so 的 .note.gnu.build-id NOTE Section 中,通过启用 linker 的 --build-id 选项生成。

1
2
3
4
5
$ readelf --hex-dump=.note.gnu.build-id core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so

Hex dump of section '.note.gnu.build-id':
0x000002d0 04000000 10000000 03000000 474e5500 ............GNU.
0x000002e0 c6cb0084 4756c8d0 504c8c15 dd4340bc ....GV..PL...C@.

Android NDK 会自动添加编译参数 -Wl,--build-id ,用于给最终生成的 so 带上 Build ID

flags.cmake
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
2
3
4
$ file unstripped/arm64-v8a/libijkplayer.so 
unstripped/arm64-v8a/libijkplayer.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=746cfe418a56fe0425ca00a822691f3629fc6a1d, with debug_info, not stripped
$ file stripped/arm64-v8a/libijkplayer.so
stripped/arm64-v8a/libijkplayer.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=746cfe418a56fe0425ca00a822691f3629fc6a1d, stripped

修改上文从 NDK 源码中下载的 parse_elfnote.py, 加上 .note.gnu.build-id 的解析功能:

parse_elfnote.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#...
def main():
#...
with open(file_path, "rb") as obj_file:
#...
(sec_off, sec_size) = get_section_pos(readelf, ".note.gnu.build-id", file_path)

obj_file.seek(sec_off)
sec_data = obj_file.read(sec_size)
print("----------BUILD ID----------")
if len(sec_data) == 0:
logger().warning(".note.gnu.build-id section is empty")
for name, kind, desc in iterate_notes(sec_data):
# NT_GNU_BUILD_ID (3)
if (name, kind) == (b"GNU", 3):
print("".join(format(x, "02x") for x in desc))
else:
logger().warning(
"unrecognized note (name %s, type %d)", repr(name), kind
)

(完整代码见gist

1
2
3
4
5
6
7
$ python3 parse_elfnote.py --ndk=$ANDROID_NDK_HOME core-1.38.0/jni/arm64-v8a/libarcore_sdk_jni.so 
----------ABI INFO----------
ABI_ANDROID_API: 21
ABI_NDK_VERSION: r25
ABI_NDK_BUILD_NUMBER: 8775105
----------BUILD ID----------
c6cb00844756c8d0504c8c15dd4340bc

总结

Android 的原生库(.so)其实一个是 ELF 格式的文件,可以直接通过 readelf 等工具解析 ELF 我们所需要的信息所对应 Section Data。再将解析过程脚本化,方便日常开发工作。

Refs