本文假定所使用的是 Debian 系的系统,且使用 apt 作为包管理器。对于其他的系统/其他的包管理器,可能需要做相应的调整。
Always remember: Google is your best friend!

1. 交叉编译的基本概念

通常我们写的程序是在本机上编译运行的,这样的程序称为本地程序。而在开发嵌入式系统,或者运行平台和本地不一致的时,我们需要将程序编译成目标平台上可以运行的程序,这样的程序称为交叉编译程序。

一般来说,编译器识别平台所用的是一个三元组,即 arch-vendor-os,例如我们通常的 x86,我们可以使用 clang -v 来查看

1
2
3
4
5
6
7
8
9
$ clang -v
Debian clang version 14.0.6
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/12
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/12
Candidate multilib: .;@m64
Selected multilib: .;@m64

可以看到,当前的机器三元组为 x86_64-pc-linux-gnu,即 arch=x86_64vendor=pcos=linux-gnu

交叉编译中,另外一个重要的概念是 build-host-target,即编译器的构建平台、主机平台和目标平台。

  • build: 构建平台,用来构建编译器的平台。
  • host: 主机平台,即执行编译器的平台。
  • target: 目标平台,即编译器生成的程序所运行的平台。

例如,我们在 x86_64 的机器上编译一个 arm 的程序,那么 build=x86_64host=x86_64target=arm

有以下几种比较常见的搭配:

  1. build==host==target:即本地编译,编译器和程序运行在同一平台。
  2. build==host!=target:即交叉编译,编译器和程序运行在不同平台。
  3. build!=host==target:即交叉编译工具链,编译器运行在不同平台,但生成的程序运行在本地。

2. 交叉编译的基本流程

当然通常情况下,上述的情况我们遇到最多的是第二种,即交叉编译。下面我们以交叉编译一个 arm 的程序为例,来介绍交叉编译的基本流程。

2.1. 安装交叉编译工具链

Linux 中,大部分的交叉工具都是前缀的区别,换言之,就是一个 arch-vendor-os 的前缀。例如 arm64gcc,前缀为 aarch64-linux-gnu-armgcc,前缀为 arm-linux-gnueabi-

对于 Debian 系的系统,我们可以使用 apt 来安装交叉编译工具链,例如:

1
sudo apt install binutils-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu -y

其中的 binuils 是一些工具,例如 asld 等,gcc 是编译器,同时 g++gdb 等也可以安装。

2.2. 目标库的安装

很多时候我们所编写的程序会依赖一些其他的库,最基础的 libc,还可能有 libmlibpthread 等。这些库在不同的平台上可能有不同的实现,所以我们需要安装目标平台的库。

一般对于 Debian 系,默认只启用本地的库,所以我们需要手动安装目标平台的库,以 aarch64 为例

1
sudo dpkg --add-architecture arm64

之后是设置对应的镜像源,这里以 Ubuntu 为例,因为 Ubuntuarm 的镜像组织形式和默认的差别较大,所以我们需要手动设置

2.2.1. 对于 Ubuntu 22.04 即之前的

对于这个版本,大部分仍然采用的是 sources.list 单行的形式,所以我们可以直接修改 /etc/apt/sources.list 文件,添加对应的源

例如对于 Ubuntu 18.04,采用清华的镜像源,对应的 /etc/apt/sources.list 文件如下

1
2
3
4
5
6
7
8
9
10
11
deb [arch=amd64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb [arch=amd64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb [arch=amd64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb [arch=amd64] http://security.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse


deb [arch=arm64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic main restricted universe multiverse
deb [arch=arm64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-updates main restricted universe multiverse
deb [arch=arm64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-backports main restricted universe multiverse
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted universe multiverse
# deb [arch=arm64] http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/ bionic-security main restricted universe multiverse

请务必为之前的本地源添加 arch=amd64 的标记,因为 Ubuntuarm 部分的放在了 ubuntu-ports 下,不添加的话 apt 会尝试去 ubuntu-ports 下寻找 amd64 的包,导致错误。

2.2.2. 对于 Ubuntu 22.04 之后的

在这后面的版本中,Ubuntu 采用了 DEB822 的格式,我们可以直接新建一个 /etc/apt/sources.list.d/aarch64-cross-compile.sources 的源文件,内容如下

这里以 Ubuntu 24.04 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Types: deb
URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Architectures: arm64

# Types: deb-src
# URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports
# Suites: noble noble-updates noble-backports
# Components: main restricted universe multiverse
# Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Architectures: arm64

在按照上面的步骤添加源之后,我们可以使用 apt 来刷新源并安装对应的库,例如我们需要安装对应的 libc

1
2
sudo apt update 
sudo apt install libc6:arm64

2.3. 对于不在官方源中的库

有时候我们并不一定能够从官方源中找到我们需要的库,这时候我们可能需要自行去编译这些库,一般来说,大部分开源的库都会提供对应的 configure 脚本,我们可以使用这个脚本来编译对应的库。

例如,我们可以使用:

1
2
3
./configure --host=aarch64-linux-gnu --prefix=/opt/aarch64/sysroot/usr
make
sudo make install

这样会将库安装到 /opt/cross/aarch64-linux-gnu 下,我们可以使用 --prefix 来指定安装的位置。

2.4. 编译我们的程序

在安装好了交叉编译工具链和目标库之后,我们就可以开始编译我们的程序了,例如我们有一个简单的 hello.c 程序

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int extern_lib();

int main() {
printf("Hello, World!\n");
printf("The result of extern_lib is %d\n", extern_lib());
return 0;
}

其中的 extern_lib 假设放在 /opt/aarch64/sysroot/usr/lib 下,我们可以使用如下的命令来编译

1
aarch64-linux-gnu-gcc hello.c -o hello --sysroot=/opt/aarch64/sysroot -lextern

这里的 --sysroot 用来指定我们的目标库的位置,这样编译器就会去这个位置寻找对应的库,具体来说,编译器的查找路径为

默认 --sysroot=<dir>
头文件搜索路径 /usr/include <dir>/usr/include
依赖库搜索路径 /usr/lib <dir>/usr/lib

通过 sysroot 能够很方便的指定我们的目标库的位置,这样我们就可以很方便的编译我们的程序了。

3. 对于自己写的库

假设你使用的是 GNU Make 作为你的构建系统,那么你可以参考我的方式,来管理交叉编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CC := gcc
CFLAGS := ... # 你的编译选项
# 一些其他要用的 binutils
OBJCOPY := objcopy
...

# 交叉编译工具链
CROSS_COMPILE ?= # 这里留空,表示本地编译
SYSROOT ?= # 这里留空,表示本地编译

ifeq ($(CROSS_COMPILE),)
CC := $(CC)
else
CC := $(CROSS_COMPILE)$(CC)
OBJCOPY := $(CROSS_COMPILE)$(OBJCOPY)
...
CFLAGS += --sysroot=$(SYSROOT)
endif

这样默认情况下,我们的编译器是本地的,如果我们需要交叉编译,只需要设置 CROSS_COMPILESYSROOT 即可。

因为大部分交叉编译工具仅仅是前缀的区别,所以我们可以很方便的通过设置 CROSS_COMPILE 来实现交叉编译。

例如,对于 aarch64 的编译,可以使用 make CROSS_COMPILE=aarch64-linux-gnu- 来完成

4. 最后

当然,交叉编译的坑还有很多,这里只是一个简单的入门,更多的内容还需要自己去摸索,希望这篇文章能够帮助到你。

参考

  1. OSDev Target Triplet
  2. 清华大学开源软件镜像站
  3. DEB822 格式