1. 交叉编译这件事,先弄清楚再动手
先说一个很多人踩过的问题:不少新手在 Ubuntu 20.04 上执行apt install gcc-aarch64-linux-gnu之后,以为就万事大吉了,结果写个 hello world 编译时,要么报aarch64-linux-gnu-gcc: command not found,要么编译出来一运行就提示Exec format error,还有的干脆连头文件都找不到。这个工具链本身安装不复杂,但周围的环境配不齐、路径没对上、依赖库缺失,才是真正让人头大的地方。
先解释清楚什么叫交叉编译。你在一台 x86 架构的电脑上装 Ubuntu 20.04,然后要编译出能在 ARM 64 位设备上运行的程序,这个编译过程就叫交叉编译。平时编译本机程序,gcc编出来的可执行文件给本机 CPU 用;但 ARM 开发板、树莓派、飞腾盒子这类设备,它们的指令集跟 x86 不一样,不能直接拿 x86 上编好的二进制文件去跑。你需要一个跑在 x86 上、但生成 ARM 指令集代码的编译器,这就是 aarch64-linux-gnu-gcc 存在的意义。
这个工具链适合谁用?做嵌入式开发的、玩树莓派这类 ARM 单板电脑的、在 ARM 云主机上部署服务的、还有搞物联网网关程序移植的,都会用到。它的价值一句话就能说明白:让你不用在 ARM 设备上装一整套编译环境,直接在性能强的 x86 电脑上批量出 ARM 二进制文件,再拷过去部署就行。
标题里既然提到了 Ubuntu 20.04,我得先说一个前提:Ubuntu 20.04 的 apt 源里确实有 aarch64 交叉编译工具链,版本是 9.3.0,对应 GCC 9 系列。这个版本对绝大多数嵌入式场景都够用了,尤其是给 Linux 内核、驱动模块、C/C++ 应用做交叉编译,稳定性很好。如果你需要 GCC 10 以上或者 Clang,那就得手动装,后面我会讲。
2. 安装前的环境准备,这些细节别忽略
2.1 确认系统架构与版本
在安装之前,先确认你的 Ubuntu 20.04 确实是 x86_64 架构。虽然大多数 PC 都是 x86_64,但也有少部分情况是在 ARM 机器上装了 Ubuntu,那样的话你根本不需要交叉编译工具链,直接装原生 gcc 就行。
打开终端执行:
uname -m输出x86_64,说明你的机器是 64 位 x86 架构。如果是aarch64,那你本身就是 ARM 平台,用不着交叉编译,后面内容可以不用看了。
再确认一下系统版本:
lsb_release -a确保是 Ubuntu 20.04.x。虽然这套安装方法在 18.04、22.04 上也能用,但 apt 源里的包版本会有差异,报错信息也可能不一样,所以最好先确认清楚。
2.2 更新软件源和系统
这一步很多人直接跳过,但实际踩坑概率很高。Ubuntu 20.04 默认的 apt 源在国内网络环境下常常很慢,或者干脆超时,导致安装中断。你至少应该先执行:
sudo apt update如果发现源连接超时或者 404,那就需要换源了。把/etc/apt/sources.list里的archive.ubuntu.com和security.ubuntu.com替换成国内镜像,比如清华源、阿里源、中科大源。具体操作是用sed命令或者直接编辑文件:
sudo sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list sudo sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list sudo apt update这里我从实际经验说一句:不要为了追求新版本去添加什么 PPA 源。Ubuntu 20.04 官方源里自带的交叉编译工具链已经足够稳定,PPA 反而会引入依赖冲突,后面排错非常头疼。
2.3 判断系统是多架构支持还是单架构
Ubuntu 20.04 支持多架构(multiarch),也就是说你可以同时安装 amd64 和 arm64 的软件包。交叉编译的时候,经常需要安装 arm64 版本的库文件,这时候就要用到dpkg --add-architecture。
先看一下当前支持哪些架构:
dpkg --print-architecture dpkg --print-foreign-architectures第一行输出一般是amd64,第二行如果为空,说明你还没有添加其他架构。如果你想编译的程序依赖了很多第三方库,比如 OpenSSL、libpcre,那通常需要添加 arm64 架构支持:
sudo dpkg --add-architecture arm64 sudo apt update不过要注意:这个操作不是必须的。如果你只是用纯 C/C++ 写的小工具、静态链接库程序,那么装完工具链直接用就行。但如果你要链接系统库,比如libc.so.6,那就大概率需要安装 arm64 版本的依赖库。这一块很容易被忽略,后面编译报错的时候会专门讲。
3. 快速安装 aarch64-linux-gnu-gcc 工具链
3.1 通过 apt 直接安装(最推荐的方式)
Ubuntu 20.04 官方源里已经有打包好的交叉编译工具链,安装命令非常简单。打开终端执行:
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu这会把 C 编译器和 C++ 编译器一起装好。安装完成后,可执行文件在/usr/bin/下,名字分别是:
aarch64-linux-gnu-gccaarch64-linux-gnu-g++aarch64-linux-gnu-gcc-9(带版本号的后缀)
验证是否安装成功:
aarch64-linux-gnu-gcc --version正常情况下会输出类似这样的信息:
aarch64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 Copyright (C) 2019 Free Software Foundation, Inc.如果你只需要 C 编译器,装gcc-aarch64-linux-gnu就行;需要 C++ 的话,g++-aarch64-linux-gnu也要一起装,因为很多项目用 CMake 配置时会同时检测 CC 和 CXX 两个变量。
3.2 确认工具链相关的全部组件
有时候光装gcc还不够,编译 Linux 内核、驱动模块或者某些系统级程序,还需要 binutils、glibc 的 arm64 版本。apt 安装gcc-aarch64-linux-gnu的时候其实会把这些核心依赖一起拉下来,但为了保险起见,你可以额外确认一下:
sudo apt install binutils-aarch64-linux-gnu libc6-dev-arm64-crossbinutils-aarch64-linux-gnu提供汇编器、链接器,对应可执行文件是aarch64-linux-gnu-as和aarch64-linux-gnu-ld。libc6-dev-arm64-cross提供 ARM 64 位的 C 标准库头文件和静态库,这个尤其重要,因为很多时候编译报错fatal error: stdio.h: No such file or directory,就是因为缺少这个包。
这里有我实际遇到的一个情况:有一次我在 Ubuntu 20.04 上只执行了sudo apt install gcc-aarch64-linux-gnu,然后写了个最简单的 C 程序交叉编译,结果报错找不到crt1.o。我当时很懵,gcc都装上了,怎么会缺启动文件?后来发现就是因为libc6-dev-arm64-cross没有装。apt 在安装gcc时理论上会把它作为依赖装好,但如果你之前手动卸载过或者依赖关系出了问题,就需要单独补装。
3.3 快速测试:写一个 hello world 交叉编译
安装完之后,先做个最简单的验证。创建一个测试文件:
cat > hello.c << 'EOF' #include <stdio.h> int main(void) { printf("Hello, AArch64!\n"); return 0; } EOF用交叉编译器编译:
aarch64-linux-gnu-gcc hello.c -o hello_arm64然后看一下生成的文件类型:
file hello_arm64正常输出:
hello_arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, not stripped看到ARM aarch64就说明编译成功了。此时你在 x86 机器上直接运行它,系统会提示Exec format error,这是正常的,因为你没有 ARM 的执行环境。你可以用qemu-aarch64来模拟运行,后面会提到。
这一步测试非常重要。很多人的工具链装上之后,第一个程序就编译失败,然后开始怀疑是不是安装有问题,但实际上问题出在编译参数或者环境变量上。先跑通 hello world,再往下深入,效率高得多。
4. 依赖库与链接问题:交叉编译里最容易翻车的地方
4.1 为什么有时候需要安装 arm64 版本的依赖库
很多初学者以为装了交叉编译器,就能像本机 gcc 一样随便编译任何程序。其实不是。当你交叉编译一个用到了第三方库的程序,比如链接 OpenSSL、libpcre、libz,你需要的不是本机的 x86 版本库,而是 arm64 架构的.so文件。如果在编译时指定了-lssl,但系统里没有 arm64 版本的 libssl,链接器就会报cannot find -lssl。
当年我在编译一个 ARM 设备上的网络服务程序时,就因为libssl-dev没有装 arm64 版本,折腾了一下午。后来把 arm64 架构支持打开、装好libssl-dev:arm64,问题才解决。
具体操作如下。首先添加 arm64 架构支持:
sudo dpkg --add-architecture arm64 sudo apt update然后安装某个具体库的 arm64 版本,比如:
sudo apt install libssl-dev:arm64这个:arm64后缀很关键。它告诉 apt 你安装的是 arm64 架构的版本,而不是默认的 amd64 版本。
4.2 链接器搜索路径问题
用交叉编译器链接库的时候,你可能会遇到明明装了 arm64 库,却仍然找不到的情况。这是因为交叉编译器的默认搜索路径和本机 gcc 不一样。你可以用下面这个命令查看:
aarch64-linux-gnu-gcc -print-sysroot如果没有输出,说明没有设置 sysroot。再看看搜索路径:
aarch64-linux-gnu-gcc -print-search-dirs一般情况下,交叉编译器会在/usr/aarch64-linux-gnu这个目录下搜索头文件和库文件。所以当你手动安装 arm64 库时,要确认库文件是否放在了正确位置。
如果你需要指定额外的头文件或库文件路径,用这些参数:
aarch64-linux-gnu-gcc -I/path/to/arm64/include -L/path/to/arm64/lib program.c -o program-I指定头文件搜索路径,-L指定库文件搜索路径。这种方式适合那些没有打包成 deb 的第三方库,比如你自己从源码编译出来的 arm64 版本。
4.3 静态链接与动态链接的选择
在 ARM 设备上部署程序时,你经常要面对一个选择:静态链接还是动态链接?
动态链接的话,生成的可执行文件体积小,但要求目标 ARM 设备上有对应的.so动态库。如果设备系统很精简,缺少某些库,程序运行时会提示error while loading shared libraries。比如:
./hello_arm64: error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory这时候你有两个选择:一个是把库文件也一起拷到 ARM 设备上,另一个是改用静态链接。静态链接会把库代码直接编进可执行文件里,生成的文件会大不少,但部署简单,拷过去就能跑。
静态链接的命令是:
aarch64-linux-gnu-gcc hello.c -o hello_arm64_static -static用file命令看一眼就能区分:
file hello_arm64_static静态链接的file输出里不会有interpreter相关字段,而是显示statically linked。对于绝大多数部署到嵌入式设备上的工具程序,我个人的建议是能用静态链接就用静态链接,省心。
不过有一类情况不能静态链接:如果你在编译动态库(.so)或者需要 dlopen 插件的程序,那就得用动态链接。这个要看具体场景。
5. 手动安装与版本切换:当你需要更新的 GCC 时
apt 方式装的是 GCC 9.3.0,大部分场景都够用。但如果说你需要 GCC 10、GCC 11,或者需要带特定编译选项的定制版工具链,那就得手动安装了。这一节不讲什么从源码编译整个 GCC 工具链的方法,那个耗时太长而且容易出问题,我只讲两种更实用的方式。
5.1 使用 Linaro 工具链
Linaro 是一个专门做 ARM 工具链的机构,它们发布的 aarch64-linux-gnu-gcc 工具链是很多嵌入式团队的实际选择。下载地址可以到 Linaro 官网的 releases 页面查找,文件名通常类似gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz。
下载并解压:
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz解压出来的目录里有bin文件夹,里面就是工具链的可执行文件。把这个目录加到PATH环境变量里:
export PATH=$PATH:/path/to/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin这种方式的优势是版本明确、不带 Ubuntu 的定制补丁,适合做嵌入式 SDK 出厂的团队,因为这样可以保证所有开发者用的编译器版本完全一致。劣势也是有的,就是它没有和系统包管理集成,升级、卸载都要手动处理。
5.2 用 update-alternatives 管理多个版本的 GCC
如果你机器上既有 apt 装的 GCC 9.3.0,又手动装了其他版本,为了防止混淆,建议用update-alternatives来管理。这样你可以在多个工具链之间快速切换。
注册两个版本:
sudo update-alternatives --install /usr/bin/aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-gnu-gcc-9 100 sudo update-alternatives --install /usr/bin/aarch64-linux-gnu-gcc aarch64-linux-gnu-gcc /opt/gcc-linaro-7.5.0/bin/aarch64-linux-gnu-gcc 80切换版本:
sudo update-alternatives --config aarch64-linux-gnu-gcc它会列出所有已注册的版本,输入序号即可切换。这个方法在同时维护多个项目、不同项目要求不同编译器版本的时候特别有用。我见过不少团队因为编译器版本不一致,导致同一个代码在不同人机器上编译结果都不一样,最后排查半天发现是 GCC 版本差异。用update-alternatives管好版本,这种问题能消灭一大半。
6. 常见错误与解决方案速查
以下这些错误都是在 Ubuntu 20.04 上安装和使用 aarch64-linux-gnu-gcc 时最常见的,我按出现频率排个序,并附上排查思路。
6.1 错误一:command not found
你在终端敲aarch64-linux-gnu-gcc,系统提示command not found。这个错误通常有一个很隐蔽的原因:虽然包是装上了,但是你的 shell 环境没有刷新。有时候执行完sudo apt install,然后直接开新终端运行,却还是提示找不到命令。原因是 shell 的 hash 缓存还停留在旧状态。可以先执行:
hash -r或者直接重启终端。如果还不行,检查一下命令路径:
ls /usr/bin/aarch64-linux-gnu-gcc*如果确实没有,说明包没装成功,重装一次。还有一种情况是权限问题,有些人安装时用了su切换到 root,退出后又用普通用户执行,导致 PATH 里没有/usr/bin。普通用户的PATH一般包含/usr/bin,但保不齐你改过~/.bashrc,把 PATH 覆盖了。可以用echo $PATH查看。
6.2 错误二:找不到头文件 stdio.h、stdlib.h
编译 hello world 都报错:
hello.c:1:10: fatal error: stdio.h: No such file or directory这基本就是libc6-dev-arm64-cross没装好。这个包提供了 arm64 版本的 C 标准库头文件,路径在/usr/aarch64-linux-gnu/include/下。安装命令:
sudo apt install libc6-dev-arm64-cross装完之后再编译。如果还是报错,用dpkg -l | grep libc6-dev看看包是否存在,确认安装路径是否正确。
6.3 错误三:找不到 crt1.o 或者 crti.o
编译时报:
/usr/bin/aarch64-linux-gnu-ld: cannot find crt1.o: No such file or directory这同样和libc6-dev-arm64-cross有关。crt1.o、crti.o、crtn.o这些是 C 运行时启动文件,放在/usr/aarch64-linux-gnu/lib/或者/usr/lib/aarch64-linux-gnu/目录里。检查一下:
find /usr -name "crt1.o" 2>/dev/null如果确实没有,说明libc6-dev-arm64-cross安装有问题,重装:
sudo apt install --reinstall libc6-dev-arm64-cross6.4 错误四:cannot find -lxxx(找不到某个库)
比如:
/usr/bin/aarch64-linux-gnu-ld: cannot find -lssl这就是前面说的依赖库问题。你要先确认是不是有 arm64 版本的 libssl 库存在:
find /usr/aarch64-linux-gnu -name "libssl*"如果没有,就装:
sudo dpkg --add-architecture arm64 sudo apt update sudo apt install libssl-dev:arm64注意一个细节:有些库即便安装了 arm64 版本,文件名后缀也可能是.so,直接放到/usr/aarch64-linux-gnu/lib/。但链接器搜索的时候不仅要文件名对得上,还要注意软链接是否齐全。比如libssl.so应该是指向具体版本号的软链接,如果只有libssl.so.1.1而libssl.so不存在,链接器照样找不到。这种情况可以手动创建软链接:
sudo ln -s /usr/aarch64-linux-gnu/lib/libssl.so.1.1 /usr/aarch64-linux-gnu/lib/libssl.so6.5 错误五:gnu/stubs-32.h 相关报错
编译时有同学遇到过这样的报错:
/usr/include/gnu/stubs.h:7:11: fatal error: gnu/stubs-32.h: No such file or directory这个错误通常发生在编译 32 位 armel 或者 armhf 时,但你用的是 aarch64(64位),一般不会出现。但如果你确实遇到了,说明你可能是混用了环境变量或者工具链冲突。排查思路是先确认你编译的目标架构是否正确,是否在编译命令里加了多余的-m32参数。如果没加,那可能是你的工具链安装和被PATH里其他工具覆盖了,用which aarch64-linux-gnu-gcc确认用的是哪个路径下的编译器。
6.6 错误六:编译出来的程序在 ARM 设备上无法运行
这是最隐蔽的问题。你在 x86 上交叉编译成功了,file命令也显示是 ARM aarch64,但拷到 ARM 设备上运行,提示类似:
./program: not found注意,这里不是 "No such file or directory",而是 "not found"。其实真正的原因是动态链接器路径不对。很多嵌入式 Linux 系统的动态链接器不在/lib/ld-linux-aarch64.so.1,而是放在别的位置。这时候需要给编译器指定正确的 sysroot,或者在编译时用-Wl,--dynamic-linker指定路径:
aarch64-linux-gnu-gcc hello.c -o hello_arm64 -Wl,--dynamic-linker=/lib/ld-linux-aarch64.so.1还有一个坑:如果你的 ARM 设备里的系统库版本比较老,而你编译时用的 glibc 版本太新,运行时会报:
./program: /lib/aarch64-linux-gnu/libm.so.6: version GLIBC_2.29 not found这种情况只能降低编译端的 glibc 版本,或者改用静态链接。
6.7 错误速查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| command not found | 没安装成功 / shell 缓存 / PATH 问题 | 重装;hash -r;检查 PATH |
| stdio.h no such file | 缺少 libc6-dev-arm64-cross | apt install libc6-dev-arm64-cross |
| crt1.o not found | libc6-dev-arm64-cross 损坏或缺失 | 重装该包 |
| cannot find -lxxx | 缺少 arm64 版本依赖库 | dpkg --add-architecture arm64后安装:arm64库 |
| gnu/stubs-32.h 报错 | 工具链混用或架构参数错误 | 检查 PATH 和编译参数 |
| ARM 设备上 not found | 动态链接器路径不对 | -Wl,--dynamic-linker指定路径 |
| GLIBC_2.xx not found | 编译端 glibc 过新 | 静态链接或降低 SDK 版本 |
7. 配合 CMake 使用:让交叉编译体系化
命令行方式编译单个文件很简单,但真实项目肯定是要用构建系统的。这里简单说一下怎么在 CMake 里配置交叉编译。
创建一个工具链文件aarch64-linux-gnu.cmake:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)配置时指定工具链文件:
mkdir build && cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=../aarch64-linux-gnu.cmake make这里有一个细节要注意:CMAKE_FIND_ROOT_PATH要指向工具链的 sysroot 路径。如果留空,CMake 可能会去/usr/lib/x86_64-linux-gnu下找库文件,那就全乱了。MODE_PROGRAM NEVER的意思是,查找程序可执行文件时不要到 sysroot 里找,不然 CMake 可能会找到 ARM 版本的辅助工具然后在 x86 上执行,直接报错。
这段配置是我用了很多年验证过的,可以直接抄作业。项目复杂的话,还可以在工具链文件里加CMAKE_EXE_LINKER_FLAGS、CMAKE_C_FLAGS这些变量,但最基础的就是上面这些。
8. 把交叉编译环境再往前推一步
安装工具链只是第一步,真正做嵌入式开发,还需要考虑东西还挺多的。比如你编出来的程序在 ARM 设备上跑,总要有一个调试手段。gdb 的交叉调试版本是gdb-multiarch,Ubuntu 20.04 上可以直接安装:
sudo apt install gdb-multiarch然后用它配合硬件调试器或者 qemu 调试 ARM 程序。我自己用 qemu 模拟运行 ARM 程序的经验是,装一个qemu-user-static,可以用 user mode 直接跑 ARM 可执行文件:
sudo apt install qemu-user-static ./hello_arm64在 x86 机器上直接执行 ARM 程序,这玩意儿还是挺神奇的。很多时候你不需要 ARM 开发板就能快速验证程序的逻辑正确性,比如跑单元测试、处理数据文件的工具,qemu-user 完全够用。
还有一点需要注意:如果你的项目同时维护多个目标平台,建议把交叉编译环境写成一个初始化脚本,放在项目仓库里。这样新同事拉到代码后,一条命令就能把环境准备好,不用从零开始踩坑。脚本内容大概就是执行 apt 安装、导出 PATH、创建 CMake 工具链文件这些操作。
我在实际工作中见过很多次这样的情况:两三个同事各自在自己的电脑上尝试装环境,每个人遇到的问题都不一样,前前后后折腾了一两天。后来我把环境初始化脚本写好,提交到代码仓库,整个团队再也没为环境问题烦心过。这个建议可能比工具链本身的安装技巧更有价值。
最后说个小贴士:现在很多云开发环境自带 arm 交叉编译器,但你本地有一份可用的工具链仍然是刚需,因为大量验证工作、编译脚本的调试、CI/CD 的本地复现,都需要它。把这一套环境配好、用熟,对做 ARM 相关开发的人来说,是一笔非常划算的投资。