Rust交叉编译静态二进制

只是为了方便分发。

最近在尝试用Rust写程序。虽然使用Rust编写的程序在运行速度上与C/C++相差无几1,但是Rust程序的编译实在是相当缓慢并且耗费资源。这也就使得对于一些嵌入式平台而言(比如Raspberry Pi), 如果不想芯片冒烟的话,还是要老老实实地交叉编译。之前写一些Go的程序的时候交叉编译就非常方便,通过指定GOOS以及GOARCH环境变量即可2。而Rust则显得有些麻烦,这里3有一份很不错的文档可以供参考,并且其中也提到了对于同架构不同操作系统而言,因为缺乏工具链,除了使用一些可以提供不同环境的CI平台,或是虚拟机之类的工具,很难进行这样的编译。总体来说,要实现直接的二进制分发,要达到两个目标。

首先是静态链接(Static linking),在编译过程中编译器与汇编器只会将代码翻译成目标平台机器码,而最终的可执行文件则通过链接器产生。在链接这一步一般情况会使用动态链接,并链接到当前环境下的基础库。这里无意讨论静态链接和动态链接的优劣,但是一些情况下我们确实需要静态链接。比如我自己写个小程序,放到一些老旧的Linux系统上运行,通常就需要想办法避开glibc的版本问题。

接着是交叉编译(Cross compilation),这一步需要一个能在当前平台执行的编译器,其输出则为目标平台的机器码,并且还需要可以生成目标平台可执行文件的链接器。

那么交叉编译和静态链接的关系又在哪里呢?虽然同为x86_64平台的Unix-like系统,macOS和Linux的二进制格式不一样,libc也不一样,因此在二者之间交叉编译对方平台的二进制文件时,可以选择静态链接以避免动态链接库的版本问题。

对于Rust而言,根据之前提到的文档3,要想编译到常见的x86_64架构的Linux平台,静态链接需要将目标平台设置为x86_64-unknown-linux-musl ,也就是静态链接musl libc来去除对glibc动态链接库的依赖,从而达到生成静态链接二进制的目的。因此,首先通过rustup添加目标工具链

rustup toolchain add x86_64-unknown-linux-musl

然后,在构建的时候使用

cargo build --target x86_64-unknown-linux-musl

就可以编译出一个x86_64架构Linux平台下静态链接的二进制文件。

但是在实际中,这一个方法并不一定能成功。比如,使用常见的数据库ORM框架Diesel4,利用SQLite作为后端,即便使用上面的命令生成的二进制文件也并非是静态链接的。按Diesel的文档,引入依赖后Cargo.toml为:

[dependencies]
diesel = { version = "1.4.4", features = ["sqlite"] }

通过查询得知,Diesel在使用SQLite后端的时候,其底层依赖libsqlite3-sys,而libsqlite3-sys默认情况下是不会静态链接的,根据其文档5,必须指定其feature为「bundled」,即在Cargo.toml 里显式地指定libsqlite3-sys的features:

[dependencies]
diesel = { version = "1.4.4", features = ["sqlite"] }
libsqlite3-sys = { version = "*", features = ["bundled"] }

这样,再使用cargo build的时候在理论上就可以成功地生成静态链接的二进制文件了。

但是事情并没有那么简单,这还只是解决了sqlite库的静态链接问题。再次执行cargo build --target x86_64-unknown-linux-musl,发现这次在libsqlite3-sys的编译时会有如下报错

error occurred: Failed to find tool. Is `musl-gcc` installed?

所以转了一圈,交叉编译的问题还没有解决。于是,仍然需要目标平台的musl-gcc。此时有两个解决方案。

Ziglang

第一,利用自带多个平台musl工具链的zig。zig是一门新的旨在替代C的系统级编程语言6。其特色在于没有隐式控制流,并可以利用编译期间的代码执行实现元编程。其编译器本身也是一个C编译器,并且可以实现不同平台的交叉编译。Zig语言的作者的博客中也提到了zig cc可以作为GCC/Clang的原位替代7。因此,可以通过以下的wrap脚本作为代位C编译器

#!/bin/sh
zig cc  -target x86_64-linux-musl $@

保存为zcc,并chmod +x zcc,此时再设置环境变量export CC=./zcc ,再次执行cargo build --target x86_64-unknown-linux-musl,会发现libsqlite3-sys到编译通过了。然而,此时又出现了新的问题

error: linking with `cc` failed: exit status: 1
[...]
note: ld: library not found for -lcrt0.o

由于链接器并没有使用zig cc,而还是系统带的cc,因此自然也不能链接。那么需要修改链接器,于是找到链接器参数可以通过.cargo/config.toml根据target指定,于是有

$ cat .cargo/config.toml
[target.x86_64-unknown-linux-musl]
linker = "zcc-x64-linux"

接着继续使用cargo构建,这次仍然报错,

ld.lld: error: duplicate symbol: __init_tp

仔细检查错误信息,发现链接的时候,zig cc选择链接自己的musl-libc,而rust这边的rustlib也同样提供了libc的符号,符号冲突,因此链接失败。根据遇到同样问题的这篇文章8的说法,可以在.cargo/config.toml中给rustc加参数使rustc不再链接自带的libc

$ cat .cargo/config.toml
[target.x86_64-unknown-linux-musl]
rustflags = ["-C", "linker-flavor=gcc", "-C", "link-self-contained=no"]
linker = "zcc-x64-linux"

这一方法比较简单,因为zig提供了常见平台的release可以直接下载运行。实际操作中,我并没有成功,链接阶段仍然会报和之前一样的符号冲突的错误,并且尝试了一些方法也没有成功。因此个人使用了第二种方法,这种方法留给读者参考。

musl-gcc

第二种方法就是真正地编译一套目标平台的musl-gcc。单凭自己去弄清楚一堆依赖显然不现实。所幸已经有人提供了简单的编译脚本9。(这一脚本主要面向目标操作系统为Linux的,其他操作系统可以到这里下载预先编译好的编译器。)

$ git clone https://github.com/richfelker/musl-cross-make.git
$ cd musl-cross-make
$ cp config.mak.dist config.mak

此时需要编辑config.mak。之后就可以开始编译了。

$ make
$ make install

编译完成之后,可以export CC=/full/path/to/musl-cross-make/output/bin/x86_64-linux-musl-cc,并且设置.cargo/config.toml为(注意替换路径)

[target.x86_64-unknown-linux-musl]
linker = "/full/path/to/musl-cross-make/output/bin/x86_64-linux-musl-gcc"

即可利用cargo build --target x86_64-unknown-linux-musl编译出一个静态链接的二进制文件了。

如果不想自行编译must-gcc,对于macOS,还有Homebrew版本10,使用方法可以参考这篇博客

个人利用第二个方法成功地编译出了Linux下的静态链接二进制文件。

发表回复

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