Ghost Downloader Android 原生二进制执行方案解析:linker64 绕过 W^X 限制与 FFmpeg 预打包约束
【免费下载链接】Ghost-Downloader-3The only downloader you need. 下载器的集大成者。项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost-Downloader-3
本篇文章以 Ghost Downloader 架构决策记录(ADR)docs/adr/0005-android-native-binary-exec-via-linker64.md 为核心,系统讲解 Android 10+ 平台下 W^X 内存保护与 SELinux 对可执行文件的限制,以及项目如何通过/system/bin/linker64间接执行运行时热安装的 N_m3u8DL-RE 二进制,同时解释为何 FFmpeg 必须预打包在nativeLibraryDir中。读完本文,你将理解 Android 平台上"下载型应用"分发原生可执行文件的底层约束,掌握 linker64 调用技巧、app_process方案的取舍理由,以及该方案对 APK 体积(约 20 MB)和更新策略的实际影响。
一、问题背景:Android 10+ 的 W^X 与 SELinux 双重限制
Android 从版本 10(API 29)开始强制执行W^X(Write XOR Execute)安全策略:位于 APK 的nativeLibraryDir目录之外的文件,一律不能被当作可执行文件直接运行。同时,SELinux 的 exec 权限检查也进一步收紧了对应用私有目录中二进制文件的执行限制。
这对 Ghost Downloader 这类"下载器的集大成者"提出了一个现实问题:项目在 Android 端依赖若干第三方原生工具链(如 HLS/DASH 流媒体下载工具 N_m3u8DL-RE、视频混流工具 FFmpeg),这些二进制要么随 APK 预打包,要么在运行时从网络获取。如果采用"运行时下载到应用私有目录再直接执行"的思路,在 Android 10+ 上会直接触碰 W^X 与 SELinux 红线而失败。
从仓库源码可以印证这一平台差异的存在:app/platform/android.py 中通过hasattr(sys, "getandroidapilevel")判定IS_ANDROID,并通过jnius反射ApplicationInfo.nativeLibraryDir获取原生库目录(app/platform/android.py)。该目录正是 Android 允许执行二进制的唯一位置。
二、核心方案:通过/system/bin/linker64 <path>间接执行
ADR 给出的解决方案是:下载得到的原生二进制(N_m3u8DL-RE)必须通过/system/bin/linker64 <path>方式调用,以绕过 SELinux 的 exec 限制。
其原理是:linker64是 Android 系统自带的动态链接器,由系统域(system domain)授权,位于受信任的系统路径。将可执行文件作为linker64的参数传入后,二进制本身不再以"被 exec 的对象"身份接受 SELinux 检查,而是作为链接器加载的动态对象被解析执行,从而规避了对应用私有目录文件的 exec 拒绝。ADR 明确记录该方案已在真机(on-device)上验证通过。
这一方案意味着 Ghost Downloader 可以安全地把 N_m3u8DL-RE 的下载产物放在 APK 之外的目录(如应用数据目录),运行时通过类似subprocess的方式以linker64 <path>形式拉起,而不是把二进制预打包进 APK。
为什么不能直接用app_process?
ADR 同时记录了一个被否决的候选方案:使用app_process替代linker64。否决理由是:app_process面向 Java 类加载场景设计,需要完整的 Java 类加载环境,并不适合加载纯原生(plain native)可执行文件。linker64作为纯原生二进制的系统链接器,才是与 N_m3u8DL-RE 这类 C/C++ 编译产物匹配的入口。
三、FFmpeg 必须预打包:N_m3u8DL-RE 内部 exec 的隐蔽陷阱
ADR 强调了另一个关键约束:FFmpeg 必须保持预打包在nativeLibraryDir中(通过gd3ffmpeg这个 python-for-android(p4a)recipe 实现)。
原因在于 N_m3u8DL-RE 的内部实现:它下载完 HLS/DASH 分片后,会以子进程方式内部 exec 调用 ffmpeg来完成音视频混流(muxing)与 AES-128 解密(这一点也在 docs/adr/0002-minimal-lgpl-self-built-ffmpeg.md 中有所呼应)。关键在于:
- N_m3u8DL-RE 的内部 exec 调用不会经过我们的 linker64 包装;
- 如果 ffmpeg 位于
nativeLibraryDir之外,N_m3u8DL-RE 的内部 exec 会静默失败(fails silently),用户只会得到没有混流结果的残缺输出,而不会有任何明显报错。
因此,"用 linker64 包装解决所有二进制执行问题"的通用思路在此不成立:linker64 方案只适用于由 Ghost Downloader 自己发起的子进程调用;对于 N_m3u8DL-RE 内部再派生的 ffmpeg 子进程,唯一可靠的路径就是把 ffmpeg 放进系统允许执行的nativeLibraryDir。
四、收益与边界:只有 N_m3u8DL-RE 可以运行时热安装
综合以上分析,ADR 得出的最终边界是:
| 二进制 | 分发方式 | 原因 |
|---|---|---|
| FFmpeg / ffprobe | 预打包进nativeLibraryDir(p4a recipegd3ffmpeg) | N_m3u8DL-RE 内部 exec 不经过 linker64 包装,必须保证可执行 |
| N_m3u8DL-RE | 运行时热安装(linker64 间接执行) | 体积约 20 MB,且独立于应用发版更新 |
这样做的直接收益是APK 体积节省约 20 MB——N_m3u8DL-RE 不再随 APK 分发,用户需要时才下载。同时 N_m3u8DL-RE 的版本更新节奏与 Ghost Downloader 主应用解耦,无需为了升级下载器而整体更新 APK。
ADR 同时明确记录:该热安装实现当前处于延后(deferred)状态。从仓库当前源码也可以看到这一状态的印证:
- features/m3u8_pack/config.py 中
M3U8Runtime.path()在 Android 平台返回nativeLibraryDir()/libnm3u8dlre.so,即当前实现仍从nativeLibraryDir读取 N_m3u8DL-RE; - 同一文件中
canInstall = not IS_ANDROID(features/m3u8_pack/config.py),说明 Android 端尚不支持运行时一键安装; - compose/build-scripts/fetch_android_libs.sh 构建脚本仍将 N_m3u8DL-RE 的 android-bionic-arm64 产物下载后重命名为
libnm3u8dlre.so放入jniLibs/arm64-v8a,随 APK 预打包。
可见 ADR 描述的 linker64 热安装是既定目标方向,而当前仓库的 Android 构建链路仍采用"全部预打包"的过渡实现。
五、被否决方案对比:为什么不全量打包?
ADR 记录了两种被考虑过的替代方案,其取舍逻辑值得借鉴:
方案一:将所有二进制打包进nativeLibraryDir——被否决。虽然这是最直接、最符合 Android 平台惯例的做法,但它不符合长期目标:N_m3u8DL-RE 体积约 20 MB,且更新独立于应用;全量打包既让 APK 膨胀,又迫使任何下载器版本升级都必须走完整的应用更新流程。这正是"预打包 ffmpeg + 热安装 N_m3u8DL-RE"混合策略的动机来源。
方案二:使用app_process替代linker64——被否决。app_process需要 Java 类加载机制,适用于执行 Java/Android 框架代码,对纯原生可执行文件并不合适,因此选定linker64。
六、仓库实现印证:Android 平台二进制的实际解析路径
ADR 决策在仓库多个模块中得到落实,可以从源码层面完整还原 Android 端二进制解析链路:
1. 统一获取nativeLibraryDir:app/platform/android.py 通过jnius反射org.kivy.android.PythonActivity的mActivity,调用getApplicationInfo().nativeLibraryDir获取该目录,并用lru_cache缓存结果。
2. M3U8 下载引擎(N_m3u8DL-RE):features/m3u8_pack/config.py 在 Android 上解析nativeLibraryDir()/libnm3u8dlre.so作为可执行路径;features/m3u8_pack/task.py 在任务运行时通过asyncio.create_subprocess_exec(execPath, *command, ...)拉起该二进制,若路径不存在则抛出"N_m3u8DL-RE 未安装"的TaskError。
3. FFmpeg / ffprobe:features/ffmpeg_pack/config.py 同样在 Android 上从nativeLibraryDir读取libffmpeg.so与libffprobe.so,与 ADR"FFmpeg 必须预打包"的结论一致。
4. 同类处理的 QuickJS:Android 端 JavaScript 引擎同样遵循该模式,features/yt_dlp_pack/config.py 从nativeLibraryDir读取libqjs.so。
5. 构建侧的一体化打包:compose/build-scripts/fetch_android_libs.sh 统一从上游拉取 Android arm64 预编译产物(FFmpeg/ffprobe、N_m3u8DL-RE、QuickJS-NG),并全部重命名为lib*.so落入jniLibs/arm64-v8a——这正体现了当前"以nativeLibraryDir为唯一可执行边界"的构建约束,也为未来 N_m3u8DL-RE 切出预打包、改走 linker64 热安装预留了清晰的改造点。
七、小结
这条 ADR 浓缩了 Android 平台原生二进制分发的核心矛盾:W^X 与 SELinux 只认nativeLibraryDir,而热安装与体积优化又希望把大体积工具移出 APK。Ghost Downloader 给出的答案是一套混合策略:可被自己包装调用的 N_m3u8DL-RE 走linker64间接执行以换取约 20 MB 的 APK 节省,而会被子进程内部 exec 的 FFmpeg 则坚守nativeLibraryDir预打包以保证混流链路可靠。该决策对任何在 Android 上分发原生工具的下载类应用都具有直接参考价值。
【免费下载链接】Ghost-Downloader-3The only downloader you need. 下载器的集大成者。项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost-Downloader-3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考