交叉编译是在某个系统平台下生成可以在另一个系统平台运行的可执行文件的过程。
交叉编译通常涉及:
目标平台:操作系统类型(windows/linux/macos 或嵌入式平台)
目标 CPU 架构:x86_64/aarch64/riscv 等。
CPU 还会根据型号和支持的指令集版本进一步进行细化,例如 x86 支持 i386/i586/i686,arm 分为 armv7/armv8/armhf 等版本。
软件依赖
一个编译好的软件通常会依赖一些动态库,一个常见的软件可能具备下面的依赖:
$ ldd /usr/bin/mv
linux-vdso.so.1 (0x00007f7881670000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f7881600000)
libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007f78815f5000)
libattr.so.1 => /lib/x86_64-linux-gnu/libattr.so.1 (0x00007f78815ed000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f788140c000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f7881372000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7881672000)
将源码中以来的动态库转为静态库后,一个软件的运行时依赖可能会变为:
$ ldd /usr/bin/gcc
linux-vdso.so.1 (0x00007fb57c202000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb57c003000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb57c204000)
vdso
vdso(Virtual Dynamic Shared Object)是内核的一部分。内核将一部分系统调用导出到用户空间以提高调用效率。
vdso 在程序加载时由系统 强制 进行映射,无论 ldd 中是否显示有 vdso,程序实际上都一定会加载此 so。这部分可以通过查看进程的 /proc/self/maps
文件得到结论。
出于安全的考虑,vdso 每次加载的地址都不固定。例如:
ldd /bin/cat | grep vdso linux-vdso.so.1 (0x00007ffc62be3000) ldd /bin/cat | grep vdso linux-vdso.so.1 (0x00007ffc12ff7000) ldd /bin/cat | grep vdso linux-vdso.so.1 (0x00007ffd3abec000)
Dynamic Linker
/lib64/ld-linux-x86-64.so.2 文件被称为 Dynamic Linker。当程序文件加载后,内核将控制权移交给动态连接器,然后由动态链接器完成动态库的加载。
使用 -Wl,-dynamic-linker
链接选项可以更换动态链接器的名字。
libc
libc 是 C 语言标准库。libc 本身有很多实现,最常见的包括 glic、musl 等。glibc 具有多个组件,例如:
ld-linux-x86-64.so:运行时链接动态库。
libpthread.so:线程库。
libm.so:数学库。
libc.so:主 C 库。
libgcc.a:低级运行时库,提供 CPU 不支持的能力(例如在 32 位 CPU 上提供 64 位运算)。
crti.o/crtn.o:C 语言运行时文件,用于初始化和销毁进程。
总而言之,C 库提供了对系统调用的封装、软件模拟的数学运算、进程初始化和销毁、动态库链接等能力。
musl
musl 是一个维护良好的、代码简洁的 libc 实现,相比 glibc 而言,musl 的代码简洁易懂,编译简单,跨平台能力强,静态编译是常常作为 glibc 的平台。
总结
从上面我们可以知道,一个软件的运行时依赖主要来源于程序本身依赖的动态库和语言本身特定的运行时库(比如 libc 或者 libstdC++)。要进行交叉编译,首先需要尽量减少程序依赖的外部库,其次需要解决对 libc 的依赖。libc 的依赖可以通过将 C 库替换为 musl 来实现。
GCC
gcc 本身不是一个交叉编译工具链,要使用 gcc 进行交叉编译,只需要下载支持目标平台的 gcc 工具链即可。这些工具链可以从 Linux 系统的官方源中获取,或者从网上下载预编译好的 gcc 工具链。在编译时只需要更换编译器即可。
clang
clang 本身是一个交叉编译工具链,支持将目标编译到目标平台。但是 clang 具备下面的问题:
clang 使用系统的 libc 而没有附带的 libc 版本。
目标三元组
clang 将目标平台表示为三元组,其格式为 <arch><sub>-<vendor>-<sys>-<env>
:
arch = x86_64, i386, arm, thumb, mips 等。
sub = for ex. on ARM: v5, v6m, v7a, v7m 等。
vendor = pc, apple, nvidia, ibm 等。
sys = none, linux, win32, darwin, cuda 等。
env = eabi, gnu, android, macho, elf 等。
除了这些外,clang 还可以通过 -mcpu
等参数进一步调整目标平台的平台,例如:
-mcp=<cpu-name>
:例如 x86_64, swift, cortex-a15。-mfpu-<fpu-name>
:例如 SSE3, NENO 等浮点运算单元。-mfloat-abi=<fabi>
:比如 soft, hard。控制了浮点数实现的方式。
sysroot
尽管 clang 本身是一个交叉编译工具链,但是它本身没有提供交叉编译需要的头文件和目标系统动态库等内容。clang 将这些内容称为 sysroot。sysroot 可以从 gcc 的交叉编译工具链中获取,也可以从 docker 中获取。
交叉编译链接器
代码的编译涉及到预处理、编译和链接阶段。其中预处理和编译阶段需要的头文件由 sysroot 提供。编译到目标平台的代码由 llvm-mc 提供原生支持。链接阶段由链接器和 sysroot 支持。
clang 附带了四个 linker:
ld.lld (Unix)
ld64.lld (macOS)
lld-link (Windows)
wasm-ld (WebAssembly)
通过上面的内容,我们可以通过 clang 进行交叉编译:
$ clang++ --target=aarch64-pc-freebsd --sysroot=$HOME/bsd_sysroot -fuse-ld=lld -stdlib=libc++ -o zpipe zpipe.cc -lz
zig:交叉编译的最后一公里
zig 是 zig 语言的编译器,同样也是一个零依赖的、即插即用的 C/C++ 编译器,支持开箱即用的交叉编译。zig 是基于 LLVM 开发的编译器。但是和 clang 不同,zig 通过各种方式简化了交叉编译,从而大大降低了交叉编译的门槛。
和 clang 不同,zig 参考 LLVM 实现了自己的一套 compiler-rt(libgcc),并将其源码附带到工具链中。
zig 通过附带 musl 源码的形式解决了 libc 的依赖问题。
由于 C 语言运行时文件(crti.o, crti.o)的 ABI 非常稳定,所以 zig 直接进行静态编译。
zig 通过预处理工具对 glibc 和 linux 源码分类成平台相关代码和平台无关代码。并生成了三个文件:
如果用户程序没有要求 glibc 版本,则 zig 首先通过查看自己的二进制文件来查找系统的 glibc,如果找不到就通过 /usr/bin/env 的 shebang 查找。最后连接到系统的 glibc 版本。
如果用户请求特定的 glibc 版本,那么 zig 会链接到一个 dummy 库上,然后在目标系统上动态查找符号。
这样,zig 就不与任何系统绑定了。使用 zig 进行交叉编译的方式为:
zig cc hello.c -target x86_64-native -mcpu sandybridge
zig 的三元组和 clang 的三元组不同,需要进行转换。 |
总结
工具链 |
交叉编译方式 |
支持的平台 |
GCC |
下载预编译的工具链。 |
非常多 |
clang |
设置目标三元组和 sysroot。 |
多 |
zig |
直接进行交叉编译 |
少 |