记录一次chroot折腾
记录一次chroot折腾
事情的起点很简单。我在 Android 上用 KSU 模块搭了一个 Ubuntu chroot 环境,里面跑着一个 MCP 服务。这个服务需要调用 Android 的系统命令,比如 am start 启动 Activity,或者 cmd notification 发通知。
最初的方案是通过 nc 把命令发到 chroot 外面的一个监听脚本,让它在 Android 原生环境里执行,再把结果传回来。能用,但绕了一大圈,延迟高,还容易出问题。
某天我突然想到:chroot 只是换了根目录,内核还是同一个,理论上应该能直接跑 Android 二进制才对。于是试了一下:
/system/bin/cmd activity报错:
/bin/bash: /system/bin/cmd: cannot execute: require初的判断是 ABI 不兼容。chroot 是 Ubuntu,用的 glibc;Android 二进制用的是 bionic。两套 C 库,肯定跑不了吧?这个判断听起来很合理,但完全是错的。
Linux 内核不关心你用什么 C 库。内核只负责加载 ELF、分配内存、调度进程。glibc 和 bionic 都只是用户态的库,内核一视同仁。chroot 也只是换了进程看到的"根目录",内核还是同一个。同一个内核上同时跑 glibc 进程和 bionic 进程,完全没问题。
既然不是 ABI 的问题,那 "required file not found" 到底找不到什么?用 readelf 看一下:
readelf -l /system/bin/cmd | grep interpreter
输出是 `[Requesting program interpreter: /system/bin/linker64]`。这是 Android 的动态链接器。检查一下这个文件,发现它是个符号链接,指向 `/apex/com.android.runtime/bin/linker64`。
那就看看 /apex 里有什么。结果是空的。
问题找到了:/apex 这个目录存在,但里面是空的。linker64 的符号链接指向了一个不存在的路径,所以内核加载 ELF 的时候找不到 interpreter,报 “file not found”。
这就涉及到 Android 的 APEX 机制了。APEX 是 Android 10 引入的模块化系统,把一些核心组件打包成独立模块,在启动时由 apexd 服务挂载到 /apex 下面。关键在于:apexd 的挂载操作是在特定的 mount namespace 里做的。
Linux 的 mount namespace 是一种隔离机制。不同 namespace 里的进程看到的挂载树可以完全不同。同一个路径,在 A namespace 里可能挂载了东西,在 B namespace 里可能是空的。
我扫了一下系统,发现至少有三个不同的 mount namespace。init 进程在一个 namespace 里,/apex 只有空的骨架目录。system_server 在另一个 namespace 里,/apex 下面有完整的内容。而我的 chroot 进程继承的是哪个 namespace,取决于它是怎么启动的。
知道了原因,解决思路就是把正确的 /apex 挂载到 chroot 里。但这一步踩了好几个坑。
第一个尝试是在 KSU 模块的 `service.sh` 里加一行 `mount --bind /apex $UBUNTU/apex`。没用。service.sh 跑在 init 的 namespace 里,绑定的是 init namespace 里的空骨架。
第二个尝试是用 nsenter 进入 system_server 的 namespace 做挂载。日志显示成功了,但 chroot 里还是看不到。因为这个挂载只在 system_server 的 namespace 里生效,我的 chroot 进程在另一个 namespace,看不到这个挂载。
第三个尝试是从 `/proc/$SS_PID/root/apex` 做 rbind。报错 Invalid argument。/proc/PID/root 是个特殊路径,不能直接作为 mount source。
折腾了一圈,我开始重新审视问题。MCP 服务是怎么启动的?是从 Termux 里启动的。Termux 作为一个普通 app,跑在 app 的 namespace 里,而 apexd 的挂载正是在这个 namespace 里做的。也就是说,Termux 能看到完整的 /apex。
那如果从 Termux 里做挂载呢?
su -c "mount --rbind /apex $UBUNTU/apex"
然后进 chroot 检查,文件在了。再试一下执行 cmd,还是报错,但这次不一样了。检查发现是 binder 的问题。cmd 命令需要通过 binder 和 system_server 通信,而 `/dev/binder` 是个符号链接,指向 `/dev/binderfs/binder`,chroot 里的 /dev/binderfs 是空的。
补上这个挂载:
su -c "mount --bind /dev/binderfs $UBUNTU/dev/binderfs"
再试:
/system/bin/cmd activity start -n com.android.settings/.Settings
设置界面弹出来了。搞定。
每次开机都要手动敲两条命令太麻烦,把它加到 Termux 的 .bashrc 里就行。mount 是内核级操作,挂上之后就一直在,不管 Termux 进程死不死,只有重启手机才会清掉。所以开机流程就是:开一次 Termux,启动 MCP 服务,然后 Termux 随便杀,MCP 继续跑,挂载继续在。
这次踩坑的核心教训有几个。“cannot execute: required file not found” 这个报错很有迷惑性,它不是说可执行文件找不到,而是说 interpreter 或者依赖库找不到。遇到这个报错,第一反应应该是 readelf 看 interpreter 路径,而不是怀疑 ABI 兼容性。Mount namespace 的隔离是真实存在的,同一台机器上不同进程看到的文件系统可以完全不同,在 A namespace 里做的挂载 B namespace 里的进程看不到。调试这类问题的关键是搞清楚“谁在哪个 namespace”,然后在正确的 namespace 里做操作。
评论已关闭