我喜欢在不同的环境下测试我的软件,在奇怪的平台上,有多种不同的实现。每个环境都可以提前暴露bug。C语言对此尤其擅长,因为它可以选择许多不同的编译器,并且可以在任何环境下跑起来。比如我至少可以说出7种不同的C编译器在Debian上。写可移植软件的一个好处是可以在广泛的测试环境上使用,这也是我更倾向于使用标准化平台而不是指定平台的原因之一。
然而,我已经与架构的多样性做了很长时间的斗争了。我的工作和测试几乎全都是在x86上,以及位居第二的ARM上(树莓派等)。大端机很罕见。然而,我最近掌握了一个快速方便的访问许多不同架构的窍门,我甚至不用离开我的电脑:QEMU 用户仿真。Debian及其衍生品对此支持的非常好,而且几乎不需要安装和配置。
交叉编译例子
有许多选择,我最主要的交叉测试架构是PowerPC。它是32位的big endian,而我一般用64位little endian工作,这与我平时使用的完全不匹配。我用一个Debian提供的交叉编译起和qemu-user工具。binfmt的支持尤其丝滑,这也是为什么我经常用它。
# apt install gcc-powerpc-linux-gnu qemu-user-binfmt
binfmt_misc是一个内核模块,可以让Linux知道如何识别任何二进制格式。例如,有一个Wine binfmt,所以Linux程序可以显示地执行windows .exe库。如果是QEMU用户模式,不同架构的二进制文件会被加载进在用户模式配置的QEMU虚拟机中。在用户模式种,没有游客操作系统,代替而来的是,虚拟机会将游客系统调到主机操作系统上。
第一个包给到我的是powerpc-linux-gnu-gcc。前缀是描述指令集和系统ABI的架构组。为了试验它,我有个小的测试程序,会对他的运行环境做检查。
#include int main(void) { char *w = "?"; switch (sizeof(void *)) { case 1: w = "8"; break; case 2: w = "16"; break; case 4: w = "32"; break; case 8: w = "64"; break; } char *b = "?"; switch (*(char *)(int []){1}) { case 0: b = "big"; break; case 1: b = "little"; break; } printf("%s-bit, %s endian\n", w, b); }
当它在本地x86-64上跑时:
$ gcc test.c $ ./a.out 64-bit, little endian
通过QEMU在PowerPC上跑时:
$ powerpc-linux-gnu-gcc -static test.c $ ./a.out 32-bit, big endian
感谢binfmt,让我可以运行起来,尽管PowerPC的库是一个本地的库。只需要一些环境变了的配置,我就可以假装在PowerPC上开发-当然除了仿真性能的损耗。
可是,你可能发觉我偷偷加了一个参数:-static。截止到现在,我只是展示了如何使用静态库。没有可用的动态库的加载器去运行动态链接库。幸运的是,我们通过简单的两步来修复它。第一步是安装PowerPC的动态链接:
# apt install libc6-powerpc-cross
第二步是告诉QEMU到哪里去找到它,不幸的是,它自己找不到。
$ export QEMU_LD_PREFIX=/usr/powerpc-linux-gnu
现在我可以不使用-static了:
$ powerpc-linux-gnu-gcc test.c $ ./a.out 32-bit, big endian
一个可实操的例子:还记得binitools吗?我现在准备好在这个交叉测试平台去跑fuzz-generated测试套件了。
$ git clone https://github.com/skeeto/binitools $ cd binitools/ $ make check CC=powerpc-linux-gnu-gcc ... PASS: 668/668
或者,如果我要经常运行make:
$ export CC=powerpc-linux-gnu-gcc $ make -e check
重复调用:make’s -e flag 传递环境参数,所以我不用每次都在命令行传CC=…
当你为自己的程序建好了一个测试套件之后,考虑到在定制化环境下跑测试的难度。越早跑测试,他们就会跑的越多。我遇到过许多程序,他们有着极其复杂的测试构建,甚至无法在测试套件中启动杀毒。更不必说交叉架构测试了。
依赖?也许可以用Debian multiarch support来安装这些包,但是我还没能弄清楚,你可能需要使用交叉编译器自己构建依赖。
用Go来测试
不必拘泥于C(或C++)。我也成功的用它测试交叉架构下的Go的库和程序。这其实不怎么重要,因为写不可移植的Go比C更难。- 例如:dumb pointer tricks被打上了“unsafe”的标签。可是,Go简化了交叉编译,并且是静态编译的,所以它非常非常简单。一旦你安装了qemu-user-binfmt,它就完全的透明了:
$ GOARCH=mips64 go test
这就是交叉平台测试所需要的一切了。如果因为一些原因binfmt不工作了(WSL)或者你不想安装它,只需要一个额外的步骤(包名example):
$ GOARCH=mips64 go test -c $ qemu-mips64-static example.test
-c选项构建一个测试库,但不去运行它,它允许你选择在哪里和怎样运行它。
如果你希望跟C跳转到相同的代码段,它甚至可以和cgo一起运行:
package main // #include // uint16_t v = 0x1234; // char *hi = (char *)&v + 0; // char *lo = (char *)&v + 1; import "C" import "fmt" func main() { fmt.Printf("%02x %02x\n", *C.hi, *C.lo) }
Go在x86-64上运行:
$ CGO_ENABLED=1 go run example.go 34 12
通过QEMU用户模式:
$ export CGO_ENABLED=1 $ export GOARCH=mips64 $ export CC=mips64-linux-gnuabi64-gcc $ export QEMU_LD_PREFIX=/usr/mips64-linux-gnuabi64 $ go run example.go 12 34
看到它可以运行的这么好,真的很欣慰呀!
另一个维度
尽管多样化,所有的这些架构仍然是运行在相同的操作系统上,Linux,所以他们只是在一个维度上的变化,对于大多数程序,主要目标在x86-64 Linux,PowerPC Linux同样可以这么做,而在x86-64 OpenBSD就是另一块内容了,尽管共享了同一个架构和ABI(SystemV)。跨操作系统的测试仍然需要花时间去安装,配置,保存这些额外的主机。
<测试窝原创译文:译者binger>