交叉编译小结

在公司开发嵌入式软件的过程中,经常会用到一些在嵌入式单板上的小工具,具体举例为 gdb,tmux,tcpdump,nm,ltrace 等,由于嵌入式单板的闪存、内存空间都较小,我们不会在构建 rootfs 时把业务无关的二进制带上,更不会在单板上直接部署一套完整的工具链,为此,我们识别出这样一种需求:为嵌入式单板构建常见的工具二进制。

接下来,我们分几个小节,试图阐述清楚交叉编译的来龙去脉和我所认为的最佳实践。

编译原理知识回顾

首先让我们回到大学编译原理的课程。从一个 hello.c 文件到 hello 二进制,需要经过“预处理 - 编译 - 汇编 - 链接”几道工序,如下图所示。 https://i.imgur.com/1UgBkid.png

而我们在敲下 gcc hello.c 的时候,gcc 此时的作用是一个脚本,帮我们把上图的所有工序按顺序一一执行。学习过该课程的同学大都知道,这中间的每个步骤我们都可以手动敲相关命令按步骤执行,这些步骤所对应的一整套工具是一个本文涉及到的一个重要概念:构建工具链。我们在源码级构建的时候经常遇到的一个名词“toolchain”既为此意。

交叉工具链 & 命名规则

我们姑且认为,工具链是我们对 src -> bin 过程使用到的工具集合的抽象,那么何谓交叉工具链?实际上这个概念并不会很复杂,交叉工具链帮我们做到的是另一件事 src -> bin(other platform),也就是说,交叉工具链会帮助我们把源代码 " 翻译”成其他平台的二进制,仅此而已。

在使用交叉工具链的时候,我们通常最关注的是其中名字结尾为 gcc 的工具,这个工具的前面会加上长长的一串前缀,比如

arm-none-linux-gnueabi-

aarch64-linux-musl-

这里实际上指代的是目标二进制的平台、abi、libc 实现等,并没有固定的规范,在一个常见的开源交叉构建软件 cross-ng 中,命名规则为 arch-vendor-kernel-system

交叉编译之三元组

在交叉编译中,我们经常遇到 buildhosttarget 这三个术语及其组合。理解这三个概念并不难,但如果现在不完全明白,没关系,实践中遇到之后再回头看会更加清楚。 build 与 host 不同是交叉编译器;build 与 target 不同是交叉编译链;三者都相同则为本地编译。 以下是几个例子

  1. 指定:- -build=X86, - -host=X86, - -target=X86 使用 X86 下构建 X86 的 gcc 编译器,编译出能在 X86 下运行的程序。
  2. 指定:- -build=X86, - -host=X86, - -target=MIPS 在 X86 下交叉编译出能在 MIPS 下运行的可执行程序。
  3. 指定:- -build=X86, - -host=MIPS, - -target=X86 在 X86 下构建 gcc 交叉编译器,在 MIPS 上运行 gcc 交叉编译器,编译出能在 ARM 上运行的可执行程序。
  4. 指定:- -build=X86, - -host=ARM, - -target=MIPS 在 X86 下构建 gcc 交叉编译器,在 ARM 上运行 gcc 交叉编译器,编译出能在 MIPS 运行的可执行程序。

交叉工具链的选择

在交叉编译过程中,我们面临许多选择,类似于平时构建时的选择。我们需要决定是自己构建完整的工具链,还是使用平台预先提供的二进制工具链?是选择静态链接还是动态链接?是使用 gcc 还是 clang?

我按照以下几个维度将其分类:

  1. 从零构建出 rootfs+toolchain 的选择:buildroot,crosstool-ng,openwrt(没错,openwrt 也是一个可用选项 XD)
  2. 提供好预编译的 toolchain,或者自己编译 toolchain,可以直接编译出二进制:musl-cross-make,linaro,bootlin(之前我一般选择这个)
  3. qemu 模拟对应平台的 cpu,然后本地构建(这个不多解释,主打一个 brutal)

在折腾了几个工具的交叉构建之后,结合自身的使用场景,我认为第三种方案最有价值,这个方案需要使用一个 docker 镜像,叫做 multiarch,它可以让我们用一行 docker 命令起一个异构的 qemu 镜像,共享目录之类的功能也十分方便。

实际上我们都知道,qemu 模拟异构 cpu 会极大的浪费 cpu 性能,那我为什么依然认为它好用呢?实际上好用的不是这个 multiarch,而是 alpine,这个发行版让我们跳过复杂的调试、思考、找源码等过程,直达构建!它提供了大部分常见依赖的 header+static library(musl),天然对交叉编译 + 静态构建友好,一次构建,到处运行。

对了,如果想使用第三方提供的 toolchain,需要指定一些常用的工具,这里我抄袭提供一个脚本,放到一个 .sh 文件中 source 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export CLFS_ABI="[From Chart]"
export CLFS_TARGET="[target triplet]"
echo export CC=\""${CLFS_TARGET}-gcc --sysroot=${CLFS}/targetfs\"" >> ~/.bashrc
echo export CXX=\""${CLFS_TARGET}-g++ --sysroot=${CLFS}/targetfs\"" >> ~/.bashrc
echo export AR=\""${CLFS_TARGET}-ar\"" >> ~/.bashrc
echo export AS=\""${CLFS_TARGET}-as\"" >> ~/.bashrc
echo export LD=\""${CLFS_TARGET}-ld --sysroot=${CLFS}/targetfs\"" >> ~/.bashrc
echo export RANLIB=\""${CLFS_TARGET}-ranlib\"" >> ~/.bashrc
echo export READELF=\""${CLFS_TARGET}-readelf\"" >> ~/.bashrc
echo export STRIP=\""${CLFS_TARGET}-strip\"" >> ~/.bashrc
echo export LDFLAGS=\"-static\" >> ~/.bashrc

构建实战

有了上面描述的知识,并不代表我们的交叉构建之旅将一帆风顺,因为我们面对的是 linux 平台几十年的风云变幻,不过好在大部分软件都在持续更新,遇到问题见招拆招即可。

接下来我会提供几个常用工具的交叉静态构建案例,读者有需要可以尝试自行构建。

ipmitool

首先我们下载好源码,解压到 ipmitool 目录下。

使用 docker 打开一个 alpine 虚拟机: docker run -it --rm -v .:/cross multiarch/alpine:amd64-latest-stable /bin/sh

安装必要的依赖: apk update && apk add alpine-sdk libtool autoconf automake openssl-dev openssl-libs-static readline-dev readline-static ncurses-static ncurses-dev

以下是构建步骤:

1
2
3
4
5
6
7
./bootstrap # 需要alpine-sdk libtool autoconf automake
LDFLAGS='-static' LIBS='-lncursesw' ./configure  --enable-intf-lanplus # LDFLAGS是核心,这一步和容易报错,需要仔细看configure脚本中的help与报错内容,还有,这里注意不要添加-lreadline,不然会报错
LDFLAGS='-static' LIBS='-lncursesw' make -j17 # 不解释
# 注意,由于ipmitool构建脚本自身的问题,此时构建出来的二进制能用,但并非静态构建,因此我们需要调整构建命令,使其变为静态链接的二进制。
cd src
/bin/sh ../libtool --silent  --tag=CC   --mode=link gcc  -g -O2 -Wall -Wextra -std=gnu11 -pedantic -Wformat -Wformat-nonliteral  -static -o ipmitool ipmitool.o ipmishell.o ../lib/libipmitool.la plugins/libintf.la -lreadline -lncursesw -lcrypto
strip ipmitool

gdb

1
2
3
4
5
6
7
8
# 以16.2为例
apk update && apk add alpine-sdk libtool autoconf automake openssl-dev openssl-libs-static readline-dev readline-static ncurses-static ncurses-dev
apk add gmp-dev mpfr-dev
cp /usr/lib/libncursesw.a /usr/lib/libcurses.a
sed -i -e 's/^CC_LD.*$/& -all-static/g' gdb/Makefile.in

../configure --disable-sim --disable-gdbserver --disable-gdbsupport --disable-tui --with-python=no
make -j17

其他语言

Go 和 Rust 是新兴语言的代表,他们在交叉构建方面各自规定了一套通用方案,学习和解决问题都比较简单,待有空补充。

0%