使用 qemu 构建 multiarch 调试环境

最近需要一个环境调试一些非 x86 的二进制和学习一下其他体系架构的汇编,最基本的思路是 qemu 启动 qcow2 虚拟机(如果需要模拟调试路由器等设备,可能需要这种办法,现成的 qcow2 镜像可以从类似 https://people.debian.org/~jcowgill/qemu-mips/ 的地方找到),但是我觉得有点太麻烦了, 理论上学习汇编其实只需要一个干净的 rootfs 和 runtime 就行了。

本文将简要讨论在 qemu 的支持下,通过 debootstrap + chroot / 容器等方法构建其他指令集架构的系统基本环境,并使用 gdb-multiarch 等工具进行交叉编译、调试。

使用 debootstrap + chroot

debootstrap+chroot 是 Debian 用户常用于快速构建基本 rootfs 的工具,也可以用来构建其他架构(Debian 支持的架构非常多,常见一点的基本都有官方支持),另外 Debian wiki 也提到可以使用multistrap,区别是可以使用配置文件,理论上更加灵活但是我目前的应用场景不需要了,参见:https://wiki.debian.org/EmDebian/CrossDebootstrap

流程很简单,唯一需要注意的是,跨架构 debootstrap 需要将 --foreign--second-stage 两步分开,简单地说就是需要 qemu 参与 stage2,所以我们要把 qemu 丢进 chroot 的目录里,参见:https://askubuntu.com/questions/287789/what-is-debootstrap-second-stage-for

另外 qemu-debootstrap是一个上述过程的 wrapper 免去了手动分开两个 stage 的步骤,大同小异,不在此赘述。

apt install binfmt-support qemu qemu-user-static debootstrap
mkdir debian-mips
debootstrap --arch=mips --foreign buster ./debian-mips http://mirrors.uestc.cn/debian # 随便换个什么速度快的镜像源
cp `which qemu-mips-static` ./debian-mips/usr/bin/
chroot ./debian-mips /debootstrap/debootstrap --second-stage

进入 chroot 环境后使用 hexdump 可以读取一个可执行文件的文件头(ELF 的 magic number 是 0x7f 0x45 0x4c 0x46),可以观察到 MIPS 和 amd64 的大小端不一样导致读到的字节顺序是不同的,同时也可以确认我们的 chroot 确实成功运行着其他架构的二进制。

第二次进入 chroot 环境时则不需要再加 --second-stage  参数,遇到提示类似 is /dev/pts mounted 的时候手动执行以下 mount -t devpts devpts /dev/pts,如果需要 procfs 、sysfs 也需要手动 mount 一次,如果不想手动处理这些问题,可以借助 systemd-nspawn:

apt install systemd-container
systemd-nspawn -D debian-mips/

运作基础:binfmt_misc

这一切运作的基础是 binfmt_misc (https://zh.wikipedia.org/wiki/Binfmt_misc) ,一个由内核提供、根据判断 magic number 判断文件类型而使用指定打开方式的机制(如指定 Java 程序直接使用 JVM 虚拟机打开),是一个内核模块,所有注册过的信息会保存至 /proc/sys/fs/binfmt_misc 目录下,部分发行版不需要手动 mount,如 Debian 的 binfmt-suppport 包含了几个启动脚本和 systemd-unit,binfmt-support 也在 qemu-user-static 的推荐里,可见 Debian 自动注册了相关的 magic number 到 qemu:

-> # cat /proc/sys/fs/binfmt_misc/qemu-mips
enabled
interpreter /usr/bin/qemu-mips-static
flags: OCF
offset 0
magic 7f454c4601020100000000000000000000020008
mask ffffffffffffff00fefffffffffffffffffeffff

如果发行版没有帮你做这些步骤,可能需要手动操作:

modprobe binfmt_misc
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc

另外注意到在 Debian 中,flag 字段是 OCF, 其中的 F 会禁用 lazy spawn 特性,让 binfmt_misc 在 mount namespace 环境下正常工作,详细参考:https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html

但是,即使告诉了系统使用 qemu 执行跨架构的 ELF,没有 chroot 环境直接执行依然是会报错的,提示是:/lib/ld.so.1: No such file or directory,这个报错的意思实际是 ld 找不到需要的 ld.so 和 libc.so(类似的报错同样会出现在没有安装 i386 运行库的 amd64 系统执行 i386 ELF 的时候)。

readelf 可见 ELF 中写死了 interpreter 的路径,因此不能简单地通过 LD_PRELOAD 在 x64 执行 MIPS 的 ELF,如:

因此我们需要使用  -L /usr/path/to/multilib 告诉 qemu ELF interpreter 的位置(需要安装对应架构的 crossbuild-essential 包):
-> # qemu-mips-static -L /usr/mips-linux-gnu ./hello.o
Hello, World!#

如果是完全静态链接的 ELF,因为根本不存在寻找 ld.so 和动态链接的过程,指定了 qemu 作为解释器后可以直接在 x64 下执行。

使用 Docker

chroot 简单轻量,但需要自己处理 mount procfs 之类的问题,如果目标架构(如 ARMv8)已经有现成的 image 可用,也可以使用 docker,可参见:https://www.stereolabs.com/docs/docker/building-arm-container-on-x86/

系统中存在 qemu-user-static 则 Docker 可以直接开箱即用的使用其他架构的 image:

apt install qemu binfmt-support qemu-user-static  
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes  # 不必要,取决于发行版
docker run -it arm64v8/ubuntu

更多的 image 可以在类似 https://hub.docker.com/_/debian 的页面中找到,且注意到上一节中,Debian 的 binfmt-support 已经注册过,这使得 Debian 使用 Docker 打开跨架构二进制不需要执行上述步骤的第二步。

交叉编译和调试

qemu-user-static 提供的二进制翻译效率很差(顾名思义,qemu-user-static 是一个用户态的二进制翻译,跨架构也无法使用类似 KVM 的指令级加速),因此如果需要编译大型 ELF,不要在 chroot 内直接编译,而是使用交叉编译,如在 x64 上编译 i386:

apt install gcc-multilib
apt install libc6-dev-i386
gcc -m32 hello.c -o i386.o

又如直接在 x64 编译其他 target 的二进制:

apt install crossbuild-essential-arm64
aarch64-linux-gnu-gcc -o arm.o hello.c
对于最基本的静态调试,binutils 包提供的命令诸如 objdumpreadelf 等可以正常使用(其实这些纯静态的东西在 x64 host 上也是可以正常用的),但如果想使用 gdb 进行动态调试会遇到问题,qemu 的用户态模拟没有实现诸如 ptrace 的各种系统调用,根据 https://reverseengineering.stackexchange.com/questions/8829/cross-debugging-for-arm-mips-elf-with-qemu-toolchain,一个可行的办法是在 host 上使用 gdb-multiarch 并使用 qemu 内置 gdb stub:
-> # qemu-aarch64-static -g 2333 -L /usr/aarch64-linux-gnu ./arm.o &
[1] 27219

-> # gdb-multiarch
GNU gdb (Debian 8.2.1-2+b3) 8.2.1
···(略过一大堆输出)

(gdb) set arch aarch64
The target architecture is assumed to be aarch64
(gdb) target remote localhost:2333
Remote debugging using localhost:2333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x0000004000814040 in ?? ()
(gdb) run
The "remote" target does not support "run".  Try "help target" or "continue".
(gdb) c
Continuing.
Hello, World![Inferior 1 (Remote target) exited normally]
(gdb) [1]  + 27219 done       qemu-aarch64-static -g 2333 -L /usr/aarch64-linux-gnu ./arm.o
Quit
(gdb) quit

实际使用中发现,remote target 的 gdb 功能上有一些缺失(无法使用 run 命令等),断点的工作也不太正常(我百思不得其解为什么 continue 一下程序直接跑到最后了,我的断点呢,我放这儿这么大一个断点呢.jpg),最后发现可能还是需要折腾一下 qemu-system。

使用 qemu-system

如上所述,从 https://people.debian.org/~jcowgill/qemu-mips/ 下载 kernel、initrd、rootfs (qcow2),注意区分带 el 后缀的是小端序,不带的是大端序,CPU 类型和 kernel / rootfs 必须一致,如遇卡死等问题 Ctrl-A + X 可以直接 kill qemu:

qemu-system-mips64 \
    -M malta \
    -cpu MIPS64R2-generic \
    -m 2G \
    -nographic\
    -append 'root=/dev/vda console=ttyS0 net.ifnames=0 nokaslr' \
    -netdev user,id=user.0 \
    -device virtio-net,netdev=user.0 \
    -kernel /root/debian-qemu/vmlinux-* \
    -initrd /root/debian-qemu/initrd.img-* \
    -drive file=$(echo /root/debian-qemu/debian-*.qcow2),if=virtio

虽然 qemu-system 的效率令人发指的低,gdb 跑的很慢(run 花了半分钟才开始跑),但是不需要思考为什么我的断点就这么被跳过了。不考虑文件系统共享之类的问题其实也没有最初想得那么复杂,不过有各种小问题(比如 serial 的 console size )用着还是比较蛋疼,且 qemu-system 启动大约要一分钟左右,如无特殊需要我还是会优先考虑用容器的方式。


本文链接:https://www.starduster.me/2020/09/29/build-multiarch-environment-with-qemu/
本站基于 Creactive Commons BY-NC-SA 4.0 License 允许并欢迎您在注明来源和非商业使用前提下自由地对本文进行复制、分享或基于本文进行创作。
请注意:受限于笔者水平,本站内容可能存在主观臆断或事实错误,文中信息也可能因时间推移而不再准确,在此提醒读者结合自身判断谨慎地采纳。

一条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据