在 Termux 里运行一个为桌面 Linux 编译的二进制文件,常常以 Exec format error 或 libc.so.6: cannot open shared object file 收场。传统解法是用 proot-distro 装一整套 Debian。但如果只是想跑一两个 glibc 工具,还有一条更轻的路——glibc-runner。要理解它为什么更快、更省,得先看清问题出在哪一层。
问题的根源:两套 libc
Termux 跑在 Android 上,用的是 Android 的 Bionic libc;而桌面 Linux 的二进制链接的是 GNU C 库(glibc)。两者在 ABI、符号实现、动态链接器路径上都不一样。
一个 glibc 程序启动时,内核会去加载它 ELF 头里写死的解释器——在 ARM64 上通常是 /lib/ld-linux-aarch64.so.2。Termux 里根本没有这个文件,加载第一步就失败;即便绕过,它接着要找的 libc.so.6 也是 glibc 的版本,Bionic 顶替不了。问题的本质不在内核,而在用户态缺了正确的链接器和库。
proot 的解法,以及它的代价
proot-distro 的思路是”补一整套系统”:装下完整的 Debian/Ubuntu rootfs,再用 proot 把它挂成根目录。proot 无需 root 权限,靠的是 ptrace——它拦截目标进程的每一次系统调用,在用户态把 /lib、/etc 这类路径重写到 rootfs 内部,再放行。
代价因此有两层:
- 空间:一套完整发行版动辄数百 MB 到数 GB;
- 性能:每个 syscall 都要陷入 proot 兜一圈再返回,在 syscall 密集的负载下,这层拦截的开销相当可观。
换句话说,为了跑一个 glibc 程序,proot 把整套 Linux 系统连同一层 syscall 翻译都搬了进来——这远超实际所需。
glibc-runner 的思路:只补缺口,不做模拟
glibc-runner 是 Termux 社区的工具,它不模拟系统,而是精确地把 glibc 程序缺的那块补齐:
- 提供预编译的 glibc 库文件(
libc.so.6、ld-linux-aarch64.so.2等); - 通过 glibc 自己的动态链接器启动目标程序,并用
LD_LIBRARY_PATH把库搜索路径指向这套 glibc,确保程序加载到的是 glibc 而非 Bionic; - 程序运行起来后,系统调用直接发往 Android 内核,没有 proot 那层 ptrace 拦截。
关键在第 3 点。Android 内核本身就是 Linux 内核,绝大多数 syscall 它都认。既然 glibc 程序真正缺的只是”正确的库 + 正确的链接器”,那么把这两样补上、让 syscall 直达内核,整个虚拟化层就被省掉了。
它的全部优势都源自这一点:
- 轻量——只需 glibc 库(约 10–20MB),不必拉完整发行版;
- 高效——syscall 直达内核,无 proot 中转;
- 简单——仍是单一 Termux 环境,无需管理容器。
而它的边界同样源自这一点:glibc-runner 只补了”库”这一层,凡是程序还指望底下存在一套完整 Linux 系统的地方,它都无能为力。这一点在最后一节会再回到。
实际操作
1. 安装
# 安装 glibc-repo(添加 glibc-packages 仓库源)pkg install -y glibc-repo
# 刷新包列表pkg update
# 安装 glibc-runnerpkg install -y glibc-runner
# 验证grun --versionglibc-repo 是元包,安装后会在 $PREFIX/etc/apt/sources.list.d/ 写入 glibc-packages 仓库;必须先装它并 pkg update,否则 glibc-runner 不可见。grun 就是用来运行 glibc 二进制的命令。
2. 基本用法
# 直接运行grun ./myapp --help
# 传参grun ./myapp arg1 arg2
# 配合管道echo "test" | grun ./myapp3. 包装脚本(推荐)
为省去每次手敲 grun,可包一层脚本:
cat > $PREFIX/bin/myapp << 'EOF'#!/data/data/com.termux/files/usr/bin/shexec grun /path/to/real/myapp "$@"EOF
chmod 755 $PREFIX/bin/myappmyapp --help4. 运行前先确认 ELF 类型
承接前面的原理——只有 ARM64 的 glibc ELF 才能这样跑,所以运行前值得先验明正身:
# 架构与类型file ./myapp# 期望:ELF 64-bit LSB executable, ARM aarch64, ...
# 动态链接器readelf -l ./myapp | grep interpreter# 期望含:/lib/ld-linux-aarch64.so.2 或类似也可以手动核对 ELF 头:
# magic(应为 7f454c46)od -An -tx1 -N4 ./myapp | tr -d ' \n'
# e_machine 字段(offset 18,ARM64 = 0xb7)od -An -tx1 -j18 -N1 ./myapp | tr -d ' \n'5. 处理子进程
如果程序会拉起其他 glibc 二进制作为子进程,这些子进程同样绕不开链接器问题,得各自包一层:
mv vendor/tool vendor/tool.realcat > vendor/tool << 'EOF'#!/data/data/com.termux/files/usr/bin/shexec grun "$(dirname "$0")/tool.real" "$@"EOFchmod 755 vendor/tool若 Termux 自带等价的原生工具,直接替换更省事——原生工具走 Bionic,连 grun 都不必:
pkg install -y ripgreprm -f vendor/rgln -s $(command -v rg) vendor/rg6. 环境变量
# 运行前设置export MY_CONFIG=/path/to/configgrun ./myapp
# 或写进包装脚本cat > $PREFIX/bin/myapp << 'EOF'#!/data/data/com.termux/files/usr/bin/shexport MY_CONFIG=/path/to/configexec grun /path/to/real/myapp "$@"EOF自动化脚本模板
把上述步骤串起来,得到一个可复用的安装脚本:
#!/data/data/com.termux/files/usr/bin/bashset -euo pipefail
readonly APP_NAME="myapp"readonly APP_BINARY="/path/to/glibc/binary"readonly WRAPPER_PATH="$PREFIX/bin/$APP_NAME"
# 检查 Termux 环境if [ ! -d "$PREFIX" ]; then echo "Error: Must run in Termux" exit 1fi
# 安装 glibc-runnerecho "Installing glibc-runner..."pkg install -y glibc-repopkg updatepkg install -y glibc-runner
# 验证二进制文件if [ ! -f "$APP_BINARY" ]; then echo "Error: Binary not found: $APP_BINARY" exit 1fi
# 检查 ELF magicmagic=$(od -An -tx1 -N4 "$APP_BINARY" 2>/dev/null | tr -d ' \n')if [ "$magic" != "7f454c46" ]; then echo "Error: Not a valid ELF file" exit 1fi
# 创建包装脚本cat > "$WRAPPER_PATH" << EOF#!/data/data/com.termux/files/usr/bin/shexec grun "$APP_BINARY" "\$@"EOFchmod 755 "$WRAPPER_PATH"
# 验证安装echo "Verifying installation..."grun "$APP_BINARY" --version"$WRAPPER_PATH" --version
echo "Installation complete!"echo "Run with: $APP_NAME"用数据印证
如果”省掉虚拟化层”的推断成立,差距应当能直接量出来。在一台 ARM64 Android 设备上跑同一个中等大小的 glibc 应用,对比如下:
| 方案 | 启动时间 | 内存占用 | 磁盘占用 |
|---|---|---|---|
| proot-distro (Debian) | ~3.5s | ~180MB | ~850MB |
| glibc-runner | ~1.2s | ~120MB | ~15MB(仅 glibc) |
启动提速约 65%、内存降约 33%、磁盘降约 98%——磁盘那一项的悬殊,正对应”一套完整发行版”与”一组库文件”的体量之差;启动与内存的差距,则来自缺席的那层 ptrace 拦截。
边界由原理决定
正因为 glibc-runner 只补”库”这一层,它的限制几乎可以从原理直接推出:
- 复杂依赖会失灵:程序若依赖大量 glibc 专有库,或需要 systemd、dbus 这类系统服务,光有库不够,可能仍得回到 proot。
- 混用带来排查成本:glibc 与 Bionic 在同一环境共存,一旦出问题,定位往往比单一环境更费劲。
- syscall 行为可能有差异:syscall 虽直达内核,但 Android 内核在某些调用上的行为未必和标准 Linux 完全一致,边角场景需实测。
对应地,它的最佳适用场景是:运行单个或少量 glibc 二进制(闭源工具、预编译开发工具等)、不需要发行版级别的包管理、且对性能与空间敏感的情况。
故障排查
Exec format error,提示找不到动态链接器。
多半不是 ARM64 架构。用 file 核实:
file ./myapp# 期望:ELF 64-bit LSB executable, ARM aarch64, ...error while loading shared libraries: libc.so.6。
glibc 库缺失或损坏,重装即可:
pkg reinstall glibc-runnergrun --version子进程调用失败。
排查应用内部拉起的子进程是否也是 glibc 二进制,是则按前文为它们逐一包 grun:
# 列出应用目录下所有 ELFfind /path/to/app -type f -exec file {} \; | grep ELF小结
glibc-runner 给出的启示,是把问题精确地定位到”用户态缺少正确的库与链接器”这一层,并只在这一层动手——补齐库、让 syscall 直达内核,从而绕开 proot 整套虚拟化的体量与开销。对于只需运行少量 glibc 二进制的场景,它比 proot-distro 更轻、更快;而一旦应用真正依赖一套完整的 Linux 系统,proot 仍是更稳妥的退路。
参考资源
- termux-pacman/glibc-packages - glibc-runner 官方仓库
- glibc-packages Wiki - 详细文档
- Termux Wiki - Termux 官方文档