查看原文
其他

Android 15 上适配 16K Page Size 的填坑思路,以 IJKPlayer 为例子

鸿洋
2024-08-30

The following article is from GSYTech Author 恋猫de小郭

其实这应该是适配 「Android 15 上 16K Page Size 」相关内容的第三篇,为什么会有第三篇呢?还是因为前两篇之后,有些人还是觉得,对于如何适配这件事不是很理解,刚好上一篇讲解「快速适配 16K Page Size」 的时候,就留下了一个“玄学”的问题,那么本篇就用具体的例子来进行「填坑」,算是对于 16K Page Size 这个话题的收尾。
如果没有看过前两期的,可以回顾下:
  • Android 15 上 16K Page Size 为什么是最坑
  • Android 15 之如何快速适配 16K Page Size

1适配


这里再再再简单介绍下,正常的适配思路应该是:
  • 确定 so 是否 16K 对齐,可以通过前两篇文章里的脚本或者 readelf 工具进行判断,例如 readelf 的到 4000(16384),就认为对齐。
  • 代码是否在 mmap / sysconf 写死了 4096(0x1000)
当然,并不是所有写死 4096(0x1000) 的地方就有问题,就算是 mmap ,也只是要求 offset 对齐:offset must be a multiple of the page size as returned by sysconf(_SC_PAGE_SIZE)
     void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);


强调这个,是因为最近真的有人问我,为什么我改了 xxx 4096 ,但是没效果


具体场景具体区分,在代码不存在问题后,就可以修改编译配置,如:
  • NDK r27 之前,Android.mk 增加 LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 
  • NDK r27 开始,Application.mk 配置 APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
如果是 CMakeLists.txt ,则:
  • 3.13 之前:target_link_libraries(a4ijkplayer "-Wl,-z,max-page-size=16384")
  • 3.13 之后:target_link_options(a4ijkplayer PRIVATE "-Wl,-z,max-page-size=16384")
基本上正常适配就是这么简单,在有源码的前提下,重新编译后就可以正常运行到 16K Page Size 的机器。

而今天后面要讲的,则是不正常的情况,就是你按照上面的调整之后,依然会有 SIGNAL Crash 的填坑思路,大多都是因为「陈年的代码,上古的工具链,复杂的引用」所造成的问题。

2问题
本次就用 IJKPlayer 的适配编译作为例子,为什么用 IJKPlayer 做例子?因为它的链路够复杂,有源码,「代码够老」,需求也够大。
首先你肯定是需要一个能跑起来的环境,这也是适配 16K Page Size 的基础:
  • 有源码
  • 能支持编译的环境

默认 IJKPlayer 推荐使用 android-ndk-r10e ,关于环境配置的问题这里就不多赘述,毕竟这不是本篇的重点,需要的可以看:https://github.com/CarGuo/GSYVideoPlayer/blob/master/doc/BUILD_SO.md#macos-%E7%8E%AF%E5%A2%83%E4%B8%8B%E6%96%B0%E9%85%8D%E7%BD%AE%E4%BB%8B%E7%BB%8D


对于使用  android-ndk-r10e 编译,并且不做「任何适配」的 arm64 so 库,我们直接把 ijkplayer 运行到 16K Page Size 的模拟器,你大致会看到类似如下所示的 crash :


可能有人就发现,这个并不是一个典型的不对齐报错,一般情况下的 16K 不对齐报错,应该是类似下方这种提示:
java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH ···· (new hash type from the future?)



那么我们通过  readelf 工具,对刚刚出错的  libijksdl.so 进行简单分析,可以看到两个 LOAD 段的 Align 是 10000(65536) ,也就是 64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的,所以前面 crash 的时候,并没有提示 so 不对齐,而是在某段代码执行时出现 crash。


如果我们通过 ./aarch64-linux-android-addr2line -f -e 去看出现问题的方法和位置,大概就会看到出现异常的地方会是 g_jvm = vm 这段,这时候就会出现让人「懵圈」的情况,为什么这样的赋值会导致 SIGNAL 11 code2的异常?


接着你可能会怀疑是不是  static 的问题,如果你将代码里的所有 static 声明去除,也许就会看到出错的位置变了,但是还是和「静态变量」或者「全局声明」有关系,然后你继续改,它继续换位置错,最后甚至报错可能会去到 ndk 的源码层,然后你又会陷入停滞。
也许这时候,你开始怀疑对齐是不是有问题,然后 ./aarch64-linux-android-readelf -SW 去看 Section 的时候,就会发现 .bss .data 的分配又貌似没问题,然后继续陷入僵局。


事实上,在这个排查问题,我们不应该被报错地址带偏,首先 GCC 本身的警告和错误的指向就不一定靠谱,而且在这目前这个情况下,它不支持表达性诊断 ,所以我们应该回到正确的思路上,通过 ./aarch64-linux-android-strings -a 我们也确定了用之前我们的 NDK r10e 编译时使用的是 GCC 这一点 。


我们按照正确的逻辑思考,首先动态库在 4K Page Size 下正常运行,然后在 16K Page Size 下出现 SIGNAL 11 code2 的寻址异常,并且代码里没有 mmap 、sysconf 等写死 4096(0x1000) 的地方,那么问题肯定是出现在编译工具链路上。
毕竟 NDK r10e 已经是 2015 年的产物,所以我们有理由怀疑,这里编译的 10000 其实并不是真的 64K 对齐,因为:
虽然 AArch64 编译器默认生成 ELF 文件部分与 64K 对齐,从而兼容所有 AArch64 机器,但如果工具出现 bug ,例如旧版本 patchelf 操作的二进制文件,或自定义编译器标志不对,就可能会导致某些二进制的部分仅与 4K 对齐。


3升级 NDK


我们这里先选择将 NDK 工具升级到 r21 版本,至于为什么先选 r21,大致还是因为越后面的升级成本越高,具体后面我们会介绍,后面会列出不同版本的升级概述,这里我们先看 r21 版本编译后的编译情况。
想通过 r21 编译,对于 IJKPlayer ,理论上你会遇到以下问题:
  • tools/do-detect-env.sh 文件下增加你想支持的 ndk 版本:11*|12*|13*|14*|15*|16*|21*|22*|25*|26*|27*)
  • Bad file descriptor error: invalid argument '-std=c99 :此时你需要在所有 Android.mk 下将所有的 LOCAL_CFLAGS += -std=c99 去掉,因为 GCC 早在 r18 就被移除了
  • Invalid NDK_TOOLCHAIN_VERSION value: 4.9 :同样是因为 GCC 已经移除,所以需要将 Application.mk 下的 NDK_TOOLCHAIN_VERSION=4.9 移除
  • APP_STL := stlport_static is not supported :同样在 r18 的时候,stlport 也已经被移除,所以我们需要将 Application.mk 下的 APP_STL 修改为 APP_STL := c++_static ,采用 libc++ 实现
  • 从 r21开始 ndk-bundle/build/tools/make-standalone-toolchain.sh 中会调用 ndk-bundlel/build/tools/make-standalone-toolchain.py ,所以需要把  tools/do-compile-ffmpeg.sh 下对应的 FF_ANDROID_PLATFORM 修改为  FF_ANDROID_PLATFORM=android-21 ,同理还是有  tools/do-compile-openssl.sh
基本上修改完成后,就可以使用 NDK 打包,之后我们通过 readelf 再看打包出来的 so ,可以看到此时 Align 是 1000(4096) ,因为我们此时还没有做适配调整,所以打出来的 so 都是 4K对齐
可以看到


如果此时我们在 16K 模拟器上直接运行 so ,就可以看到类似 empty/missing DT_HASH 这样的不对齐报错了,所以接下来我们可以开始修改编译配置,让 so 支持 16K 对齐:
ijkmedia/ijkj4a/Android.mkijkmedia/ijkplayer/Android.mkijkmedia/ijksdl/Android.mkijkmedia/ijkyuv/Android.mkijkmedia/ijksoundtouch/Android.mkijkmedia/ijksoundtouch/source/Android-lib/jni/Android.mkijkmedia/ijksoundtouch/source/Android-lib/jni/Application.mkijkprof/android-ndk-profiler-dummy/jni/Android.mk
需要修改的文件具体为上面的位置,大概修改就是类似如下所示,主要是添加  LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384


另外,还需要在  tools/do-compile-ffmpeg.sh  tools/do-compile-openssl.sh 下添加FF_EXTRA_LDFLAGS="-W1,-z,max-page-size=16384" 来让 ffmpeg 也构建为 16K :


修改后重新构建 so,此时我们用  readelf 再看打包出来的 so ,嗯,可以看到 Align 是 4000(16384) ,也就是 16K 对齐的状态了。


但是当我们再将项目运行到 16K 模拟器时,发现项目还是依旧 crash ,虽然报错的地址和位置变了,但是依然还是和「静态变量」还有「全局变量」有关系,这就让人很无奈了,难道真的只能升级到 r27?
但是升级 r27 的成本无疑难以接受,所以接着我们继续尝试升级到 r22
升级到 r22 ,你可能会遇到类似如下的提示错误提示,那么你可以添加 -WI,-Bsymbolic 来解决问题:
ld: error: relocation R_AARCH64_ADR_PREL_PG_HI21 cannot be used against symbol ff_cos_32; recompile with -fPIC


至于为什么,其中 -Wl,-Bsymbolic Wl 表示将紧跟其后的参数,传递给连接器 ld,而 Bsymbolic 表示强制采用本地的全局变量定义,这样就不会出现类似「全局变量被同名定义给覆盖」的问题,理论上 -Bsymbolic 与 - pie 比较接近。


至于为什么我会找到这个 fix ,其实是在 ExoPlayer 支持 extension 编译 ffmpeg 的 issue 看到:https://github.com/google/ExoPlayer/issues/9933
此时再重新打包所有 so ,再运行到 16K 模拟器,发现终于可以正常播放视频了,我们再看 r22 下的,已经是 clang 11.0.5 的版本。


那么 r22 对比 r21 又有什么变化呢?为什么 r22 构建之后的包就可以正常适配 16K ?
我们看 r22 的更新说明,有几个关键变动:
  • GNU binutils 已弃用,将在即将发布的 NDK 版本中被删除,包括 GNU assembler (as) ,如果使用 as 请转为 clang
  • LLD 现在是默认链接器
  • libc++ 升级
  • llvm-ar 代替 ar
  • llvm-strip 代替 strip
  • make 升级到 4.3
这里简洁地介绍一些设定:
  • ar:用于生成静态库,类似打包器
  • as: 汇编器,变为 ‌.o 文件
  • strip: 裁剪符号,瘦身
  • LLD:llvm 的链接器
在 r22 之前,对于 arm64 默认的 NDK 链接器是 ld.bfd,其他架构是 ld.gold,而 r22 将默认链接器切换为 ld.lld


我们通过 readelf -p .comment 可以简单看到当前 so 编译时附带的一些信息,例如 clang version,LLD version 等。


另外,在 NDK r14 开始,其实 so 一般可以查看到用的是什么版本的 ndk ,我们可以先通过 readelf --sections -W找到 .note.android.ident 的 Section number , 然后就可以通过  readelf -x num 看到 so 包含的 ndk version:
# 得到 1 这个 num
./aarch64-linux-android-readelf --sections -W  xxx.so
# -x 1 看这个 num 下的内容
./aarch64-linux-android-readelf -x 1  xxx.so


另外,曾经还遇到过 p_offset + p_filesz > file_size 的情况,这种时候基本也是工具链的问题,当时好像也是通过 llvm-strip 代替 GNU strip 解决了问题。


因为有的人可能并不知道自己的 so 是用什么编译,所以这部分「啰嗦」也可以帮助你更好了解你的 native library 的相关信息。
此外,可以看到 21 上,典型的 bdf-ld/gold linker 上,对于 elf 一般都是分两段,.data .bss 一般在第二段 RW ;而在 22 上, LLD 上 .data .bss 被单独放在一个 LOAD segments,并且还多了 PHDR 程序头。


详细的具体原因这里就不再深入展开,这里更多是一个提示:如果出现「玄学」的报错,可能更多来自工具链的问题。


所以目前看来,可以总结下,在 16K Page Size 上,llvm 的整体链路适配可能会比 GNU 更通畅,其实在更高版本的 NDK 里,GNU GCC 系列已经被完全移除,所以总结下:
  • 移除 stlport、gnustl 标准库的,使用 LLVM 的 libc++,如 c++_static、c++_shared
  • 使用较高版本 Clang/LLVM 替代 GCC/Binutils 进行编译适配
  • 低版本下 64k(0x10000) 对齐,其实并不一定对齐


毕竟例如 IJKPlayer 使用的 stlport STL ,已经十多年没更新了,关于页面大小的计算适配,还是在 2007 年的时候;另外 GNU 的 libstdc++ 与 LLVM Clang 编译器配合也不是很好。


为了证实这个问题,我们同样配置下,降级到 r21 ,重新打包,运行后,熟悉的 Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR) 又出现了,所以可以确认,问题核心是出在 NDK 编译链条。

所以,在 16K Page Size 的适配上,只要你没有 mmap 、sysconf 等写死 4096(0x1000) 地方的代码,那么完全就可以怀疑是 NDK 对应的工具链和标准库的问题。

4NDK 版本简介
事实上,如果你关注 NDK 的更新,你就会发现 NDK 的问题并不少,特别是在之前各个版本之间存在较大差异,这里我简单罗列下一些有关的更新:
  • NDK r11
    • 开始建议切换到 Clang
  • NDK r12
    • ndk-build 命令默认使用 Clang
    • make-standalone-toolchain.sh 脚本即将删除,需要尽快计划迁移到  make_standalone_toolchain.py,所以如果你的 ndk 已经删除了,可以在老版本中找到兼容方式:

  • NDK r13
    • GCC 不再受支持,它暂时不会从 NDK 中删除
  • NDK r14
    • GCC 弃用。但未从 NDK 中删除
  • NDK r16
    • 鼓励使用 libc++ 作为 C++ 标准库
  • NDK r17
    • libc++ 现在是 CMake 和独立工具链的默认 STL
    • 删除对 ARMv5 (armeabi)、MIPS 和 MIPS64 的支持
  • NDK r18
    • GCC 已被删除,包括 GNUSTL ,包括 gabi++ 和 stlport 一起被删除。
  • NDK r22
    • GNU binutils 已弃用,将在即将发布的 NDK 版本中被删除,包括 GNU assembler (as) ,如果使用 as 请转为 clang
    • LLD 现在是默认链接器
    • libc++ 升级
    • llvm-ar 代替 ar
    • llvm-strip 代替 strip
    • make 升级到 4.3
  • NDK r23
    • GNU binutils已被删除,GAS 将在下一版本中删除
    • 对 GDB 的支持已终止
    • NDK r23 是最后一个支持非 Neon 的版本
  • NDK r24
    • GNU 汇编器 (GAS) 已被移除
    • 非 Neon 设备不再受支持
  • NDK r27
    • 支持 APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true
我们再直观看 NDK 的 cxx-stl 目录,如下图所示,可以看到,r10 和 r21 的区别还是很大的。


所以升级 NDK 不要一下跨度太大,因为升级的成本真的很高,很容易让人放弃,而逐步升级,直到可以运行的方式会比较合适。

5Clang/LLVM 和 GCC/Binutils
接下来是简单的科普时间,不看科普的到这里其实就结束了,简单普及一些概念,免得不熟悉的情况下大家可能又会有更多疑问。
GCC 是 GNU 开发的编译器套件,是一套遵循 GNU 通用公共许可证(GPL)和 GNU 宽通用公共许可证(LGPL)发布的自由软件,是 GNU 和 Linux 系统的官方编译器。
Binutils( Binary Utilities) ,也就是 GNU 的二进制工具集,比如之前我们提到的 objdump、readelf 这一系列的工具就是 Binutils,而 GNU 就表示它们都是可以自由地使用的 GNU 软件,这些工具的目的是用于操作二进制文件。
二进制文件主要指 *.o 文件和 elf 执行文件,编译源代码的是 gcc,所以 Binutils 不包含 gcc。


LLVM 包含了一系列模块化的编译器组件和工具链,可以在编译、运行、空闲时对程序语言和链接进行优化,并生成代码。
Clang 是基于 LLVM 用 C++ 编译的 C、C++、Objective-C 或 Objective-C++ 编译器,遵循 Apache 2.0 许可协议发布。
Clang 的设计初衷是提供一个可以替代 GCC 的前端编译器,因为 GCC 的发展不符合 Apple 的节奏和需要,同时受限于License,苹果公司无法使用 LLVM 在 GCC 基础上进一步提升代码生成质量,因此苹果公司决定从头编写 C、C++、Objective-C 语言的前端 Clang,以彻底替代GCC。


所以不严谨的说,Clang/LLVM 是一套全新替代 GCC/Binutils 的存在。

那为什么会有这些东西存在?

这是因为很少有软件是直接用标准 C 编写的,大多其实是用类似 GCC C 编写,而 GCC 添加了一些非标准扩展,所以编译时会需要 GCC 的支持,当然或者 Clang 也在某种程度实现了对应的扩展。
所以总结下,不严谨又好理解的说:
Clang/LLVM 和 GCC/Binutils 一般是成对出现,Clang 和 GCC 主要是提供对于拓展的编译支持,而 LLVM 和 Binutils 则是提供对应的工具支持。


当然这里讨论谁好谁坏没有意思,但是 Android 在很早之前,就开始采用 Clang/LLVM 进行编译,例如 Android 8.0 开始就只支持使用 Clang/LLVM 来编译 Android 系统。


从前面的 NDK 版本迭代也可以看到,NDK 一致在致力剥离 GCC/Binutils 相关的内容,而目前 Swift、Rust、Julia 等许多新编程语言都使用 LLVM 作为编译框架,而 LLVM 也是 Mac OS X、iOS、FreeBSD 、 Android 系统的默认编译器。
如果真要说,GCC 比 Clang 支持更多的传统语言和冷门架构,而新兴语言基本使用 LLVM 居多,如 Swift、Rust、Julia 和 Ruby。


6STL


接着我们再介绍 NDK 里的 STL,就是前面我们提到的,将 APP_STL 从  stlport_static 修改为  c++_static,这又是什么东西?
STL(Standard Template Library) 一般就是我们开发中称呼的标准模板库,例如 iterator 、allocator 就和它有关系:
STL 属于是单独开发,然后提交给 C++ 标准委员会进行审议并接纳,但它不是作为 C++ 标准的一部分开发的,也就是前面我们说到的扩展支持,所以它会有 GNU 和 LLVM 不同版本。


STL 更像是一种设计策略,它提供了标准库所期望的最基本功能,例如存储数据序列的能力,以及处理这些序列的能力。
STL 还提供了有用算法和容器的通用实现,例如容器提供了在程序中存储数据然后查找、排序和对该数据执行其他计算的简单方法:
std::sort(container.begin(), container.end());


那么一开始其实 STL 的选择有很多,每种选择下又分为静态和动态支持,如下图可以简单理解为:
  • system(default) 系统默认的 C++运行库
  • gabi++_static 静态链接的方式使用 gabi++
  • gabi++_shared 动态链接的方式使用 gabi++
  • stlport_static 静态链接的方式使用 stlport 版本的 STL
  • stlport_shared 动态链接的方式使用 stlport 版本的 STL
  • gnustl_static 静态链接的方式使用 gnustl 版本的 STL
  • gnustl_shared 动态链接的方式使用 gnustl 版本的 STL
  • c++_static 以静态链接的方式使用 LLVM libc++
  • c++_shared 以动态链接的方式使用 LLVM libc++


那么如果按照分类理解,其实就是:
  • system:默认最小的 C++ 运行库,这样生成的应用体积小,内存占用小,但部分功能将无法支持,只提供默认的一些固定标头。
  • gabi++:不支持 C++ 标准库,提供与默认运行时相同的标头,但加入了对异常处理和 RTTI 的支持。
  • stlport:提供 C++ 的特性支持,它是开源项目 STLPort 的一个 android 移植版本。
  • gnu-libstdc++:GNU 标准 C++ 运行时库,同样支持异常和 RTTI。
  • llvm-libc++:该版本是 LLVM libc++ 的一个移植版本,支持 C++ 的所有特性。
但是如果你现在看官网,其实只可以看到下方的选择,对应前面的 cxx-stl 目录,其实现在就只有 libc++ 和 system 可选,也就是你要完整的 C++ 运行时库,只能用 llvm 的  libc++ ,从 NDK r18 之后  libc++ 是唯一的 STL


对于 system ,其实它属于非完全 STL 版本,system 指的就是 Android 版本里的 /system/lib/libstdc++.so,它能提供基本的 c++ 运行支持, 但是属于删减能力版本。
虽然  system 和  libc++ 都是 LLVM 的 c++ STL ,但是  libc++ 是基于NDK开发时,NDK 里已经编译好的完整库:
例如,如果 NDK 开发的 App 用到 libc++_shared.so ,那么其实整个 .so 会被打包到 APK里,用到libc++_static.a 的情况下,.a 也是被包到 App 中,在发布应用时,完整的 STL 会跟随一起发布,不依赖Android 版本内部的 STL 。


虽然都是 llvm ,但是使用不同版本的 NDK 打包构建后的 App ,可能就使用着不同版本的  libc++ , 所以不同的 NDK 本身对于 16K Page Size 适配问题上,本来就可能存在差异化。
此外,这里有个需要注意的,一个 App 里,所有依赖项最好且只使用同一 STL ,例如:
你存在有依赖使用了 STL 的闭源第三方依赖项,那么你必须使用与依赖项相同的 STL,两个不同的 STL 构建的库可能会存在冲突,例如 std::string 在 libc++ 和 gnustl 就存在差异化。


7最后


从官方对于 NDK 对 16K Page Size 提供的描述看,Android V 其实还是允许 OEM 厂商自己选择 16K 还是 4K ,而其实按照 Page Size 逻辑,16K 的 so 本来就兼容 4K 的设备,因为它 16 本身就是 4 的整数倍,所以我们也不需要同时构建 16K 和 4K 版本的库,只需要 16K 就够了。


另外也有人在问, Google Play 计划明年就要求 16K Page Size 的出处是哪里,对于感兴趣的可以查阅:https://developer.android.com/guide/practices/page-sizes?hl=en


所以通篇看下来,你要做的主要就是:
  • 确保代码里没有 mmap 、sysconf 等写死 4096(0x1000) 地方的代码(本篇第 N 次出现)。
  • 确保不是假对齐,例如低版本 NDK 编译下的 0x10000(65536) 64k 对齐不一定可信。
  • 4K 可运行的情况下, 16K 在确定 so 都是地址对齐的情况下,高度怀疑 NDK 问题。
  • 升级 NDK or 工具链合集,首选升级为 Clang/LLVM 相关。
而升级这些东西,了解文中的各种编译器和工具链基础是非常有必要的,至少你要知道出错的是什么,然后才能知道大概需要调整的方向。
适配 16K 的过程,其实就是一个「自我怀疑」的过程,可以说不同的「古老项目」,可能都存在不同的“玄学”问题,而解决这些问题的核心,是一个费时费力的体力活。
那么,你是否已经开始尝试适配了?什么,你说你没有源码?那么只能祈祷 Google 愿意在落地时折腾出来「混合 Page Size Running Time」 的东西了,不过这个可能性并不高,当然也不是没有,不过非虚拟化的混合 Page Size 的落地成本,着实感人。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

Android Surface截图方法总结
推荐个App:直达开源啦!
Android大脑--systemserver进程



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存