ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ubuntu 20.04 上 aarch64 交叉编译工具链安装与排错实战指南

Ubuntu 20.04 上 aarch64 交叉编译工具链安装与排错实战指南 1. 为什么 Ubuntu 20.04 上装 aarch64 交叉工具链这么容易翻车先交代一下背景。我平时主要做 ARM 平台上的嵌入式开发宿主机是 Ubuntu 20.04目标平台是 RK3588 这类 ARMv8 板子。刚开始接触交叉编译时我以为“装个交叉编译器”就是apt install gcc-aarch64-linux-gnu一把梭结果一编译就报错而且错得五花八门——有的说找不到头文件有的说动态链接器路径不对甚至还有直接提示gcc: fatal error: cannot compute C compiler version的。后来摸索了一段时间才把整套工具链的安装、配置和排错路径理顺。这篇东西不是官方文档的复读是我自己反复踩坑之后的记录。里面写的每一步、每一个报错基本都能在真实环境中复现尤其是那些“装完了但还是用不了”的隐蔽问题我会把排查过程和原理一起讲清楚希望能帮你省掉我当初浪费的那几天。先说一个很重要的认知在 Ubuntu 20.04 上装 aarch64 交叉编译工具链核心其实不是“装”这个动作而是“让工具链找到正确的 sysroot 和头文件搜索路径”。很多人装完gcc-aarch64-linux-gnu之后随便写个 hello.c 一编就过就以为大功告成等到真去编译带依赖的工程时才发现根本不是那么回事。2. 快速安装两条路线按需选择2.1 路线一一条 apt 命令装完基础工具链如果你是第一次接触交叉编译或者只是临时需要在 x86_64 的 Ubuntu 上编一个 ARM64 的静态程序那么官方源里的软件包是最稳妥的选择。Ubuntu 20.04Focal的官方源里已经收录了完整的 aarch64 交叉编译工具链不需要添加任何第三方 PPA。sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu这两条命令会安装aarch64-linux-gnu-gccC 交叉编译器aarch64-linux-gnu-gC 交叉编译器配套的cpp、gcc-ar、gcc-nm、gcc-ranlib等工具基础 C/C 运行库的头文件和库文件装完之后查一下版本确认可用aarch64-linux-gnu-gcc --version如果你看到类似aarch64-linux-gnu-gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0的输出就说明编译器本体已经装好了。但这里有一个非常容易忽略的点官方源里的这个包C 库头文件是不完整的。它依赖libc6-dev-arm64-cross而 Ubuntu 20.04 的libc6-dev-arm64-cross版本可能是 2.31 的某个修订版这套 C 库的头文件相对完整但如果你要编依赖libssl-dev、libz-dev这类第三方库的程序就需要额外安装对应的arm64版本开发包否则编译时直接报fatal error: openssl/ssl.h: No such file or directory。2.2 路线二手动部署官方 Linaro/GCC 工具链如果你对编译器版本有要求比如项目指定要用 GCC 10 或更高版本或者你需要带aarch64-linux-gnu-gdb调试器、aarch64-linux-gnu-objdump等全套 binutils那我建议你直接下载 ARM 官方提供的预编译工具链而不是依赖 apt。目前比较常用的是 ARM 的 GNU Toolchain 页面developer.arm.com/downloads/-/arm-gnu-toolchain-downloads选择AArch64 GNU/Linux target (aarch64-none-linux-gnu)或AArch64 GNU/Linux target with hard float (aarch64-none-linux-gnu)_x86_64的 tarball。下载解压后把bin目录加入PATHwget https://developer.arm.com/-/media/Files/downloads/gnu/11.2-2022.02/binrel/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu.tar.xz tar -xf gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu.tar.xz export PATH$PATH:$PWD/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin手动部署的工具链编译器前缀是aarch64-none-linux-gnu-和 apt 版的前缀aarch64-linux-gnu-不一样所以 Makefile 里的CROSS_COMPILE变量要写对。这个也是新手最容易搞混的地方——你明明装好了但 make 的时候报command not found先检查一下前缀是否匹配。两种路线的区别我用一张表总结一下对比项apt 安装手动部署 ARM 官方工具链安装命令sudo apt install gcc-aarch64-linux-gnu下载解压 tar.xz编译器前缀aarch64-linux-gnu-aarch64-none-linux-gnu-GCC 版本9.4.0较老11.2或更新自带 sysroot有但第三方库需另装自带较完整 sysroot适合场景快速验证、简单编译复杂工程、进阶调试后续升级apt upgrade 即可手动替换目录我个人建议优先用 apt除非你的项目明确需要新版 GCC。为什么因为 apt 版和 Ubuntu 的库打包体系是绑定的。你装第三方开发库时直接apt install libssl-dev:arm64就能装到交叉编译需要的 sysroot 里路径都是标准的/usr/aarch64-linux-gnu。而手动部署的工具链要自己把各类依赖库塞进它的 sysroot工作量反而更大。3. 安装完成只是开始环境变量与 sysroot 路径装完交叉编译器你还需要告诉系统“这个编译器该去哪里找目标平台的库”。这里涉及一个核心概念sysroot。通俗地讲sysroot 就是目标设备比如 ARM 板子上根目录的一个镜像里面放着头文件、C 运行库、动态链接器、各种 .so 文件。交叉编译器在编译时必须基于这个 sysroot而不是本机的/usr/include、/usr/lib——因为本机是 x86_64 的库ARM 的可执行文件根本用不了。apt 版工具链的 sysroot 路径是/usr/aarch64-linux-gnu。你可以看一下这个目录下有什么ls /usr/aarch64-linux-gnu正常情况下你会看到bin、include、lib等子目录其中lib里还有ld-linux-aarch64.so.1、libc.so.6这类目标平台的核心运行库。关键点来了交叉编译器默认会使用/usr/aarch64-linux-gnu作为 sysroot但你用常规的 gcc 参数编译时它还会去查找默认的头文件搜索路径。如果 Makefile 里硬编码了-I/usr/include或者-L/usr/lib那你就会看到类似“头文件找到但架构不匹配”的诡异报错。为了避免这个问题建议编译时显式指定 sysrootaarch64-linux-gnu-gcc --sysroot/usr/aarch64-linux-gnu -o hello hello.c如果是 C 工程记得把库路径也指过去aarch64-linux-gnu-g --sysroot/usr/aarch64-linux-gnu -I/usr/aarch64-linux-gnu/include -L/usr/aarch64-linux-gnu/lib -o hello hello.cpp另外建议给编译器配置一对符号链接方便 Makefile 统一引用名字sudo ln -s /usr/bin/aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-gnu-cc sudo ln -s /usr/bin/aarch64-linux-gnu-g /usr/bin/aarch64-linux-gnu-c很多内核模块、U-Boot 的构建脚本里写的是CCaarch64-linux-gnu-cc不建这个软链接你会在 make 的第一步就收到一堆No such file or directory的提示。4. 验证工具链写个 hello world交叉编译并检查产物装没装好不靠版本号靠能不能编出能在目标平台运行的程序。我们做一个最简单也最有效的验证流程。先在宿主机上建一个测试目录mkdir ~/arm64-test cd ~/arm64-test写一个简单的 C 文件#include stdio.h int main(void) { printf(Hello, aarch64!\n); return 0; }然后交叉编译aarch64-linux-gnu-gcc -o hello hello.c下一步是验证产物确实不是本机可执行的。用file命令看格式file hello你应该看到类似这样的输出hello: 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说明是 ARM64 架构的 ELF。interpreter /lib/ld-linux-aarch64.so.1说明它是动态链接的运行时需要目标设备上有这个动态链接器。如果你在 x86_64 的 Ubuntu 上直接执行./hello会收到一个为人熟知的报错bash: ./hello: cannot execute binary file: Exec format error看到这个报错不要慌这恰恰说明你交叉编译成功了。因为你是在 x86_64 上尝试执行 ARM64 的程序内核直接拒绝执行。要真正跑这个程序要么拷贝到 ARM 板子上要么用 qemu-aarch64 用户态模拟sudo apt install qemu-user qemu-aarch64 ./hello如果输出Hello, aarch64!那说明这个 hello world 从编译到运行链路全部打通了。5. 常见错误一fatal error: bits/libc-header-start.h: No such file or directory这个报错几乎是嵌入式新手必遇的第一个拦路虎。错误输出长这样aarch64-linux-gnu-gcc -o test test.c test.c:1:10: fatal error: bits/libc-header-start.h: No such file or directory 1 | #include stdio.h | ^~~~~~~~~ compilation terminated.为什么会出现这个错核心原因是 apt 安装的gcc-aarch64-linux-gnu不会自动把 C 库头文件装全。bits/libc-header-start.h这个文件实际属于libc6-dev-arm64-cross这个包而不是 gcc 包本身。所以在便编译的早期阶段编译器去系统头文件搜索路径里找不到这个文件就会报错。解决办法很简单把缺失的依赖补上sudo apt install libc6-dev-arm64-cross装完后你在/usr/aarch64-linux-gnu/include目录下应该能看到gnu/stubs.h、bits/libc-header-start.h等文件。但如果你装完了还是报同样的错可以手动检查一下头文件搜索路径aarch64-linux-gnu-gcc -v -E -x c /dev/null 21 | grep ^ 这条命令会把交叉编译器默认的头文件搜索路径逐行打印出来。正常情况下你会看到/usr/aarch64-linux-gnu/include被列在 C 库头文件搜索路径里。如果没有大概率是你系统里有多个版本的 GCC 或者手动部署过交叉工具链把路径配置搞乱了。这时候直接显式指定--sysroot是最快的规避手段。6. 常见错误二cannot find -lgcc / cannot find -lc这个报错通常在编译稍微复杂一点的程序时出现。报错信息长这样/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: cannot find -lgcc collect2: error: ld returned 1 exit status这个问题的本质是编译器找到了但它默认链接的库搜索路径里没有libgcc.a或者libc.a。原因是 libgcc 是编译器自带的库交叉版本不一定随 gcc 包一并安装到默认库路径下。排查思路是这样第一步安装gcc-9-aarch64-linux-gnu-base和libgcc-9-dev-arm64-cross把 libgcc 补上sudo apt install gcc-9-aarch64-linux-gnu-base libgcc-9-dev-arm64-cross第二步确认 libgcc.a 的真实路径find /usr -name libgcc.a 2/dev/null | grep aarch64我遇到过的情况是libgcc.a 实际在/usr/lib/gcc-cross/aarch64-linux-gnu/9/下但系统默认搜索路径却漏掉了它。这时候可以用-B参数把 GCC 子目录加进搜索路径aarch64-linux-gnu-gcc -B/usr/lib/gcc-cross/aarch64-linux-gnu/9/ -o test test.c不过更推荐的做法是检查一下你的 Makefile 或者编译命令里是不是用了-nostdlib或-nodefaultlibs。有些嵌入式工程为了精简体积在某个环节会加这些参数结果把默认 libgcc 也去掉了但后面又没手动链进去。正确做法是在链接命令尾部自动补一句-lgcc -lc比如aarch64-linux-gnu-gcc -nostdlib -o test test.o -lgcc -lclibgcc 里面包含了一些编译期辅助函数比如__aeabi_uidiv除法、__aeabi_ldivmod长除法这些链接裸机或内核代码时必须显式拉进来否则就是 cannot find 或者 undefined reference。7. 常见错误三动态链接器路径不对导致 target 上无法运行这种情况最坑因为它不会在编译时报任何错误你编出来一个看似正常的 ARM64 可执行文件拷到板子上运行时却得到/lib/ld-linux-aarch64.so.1: No such file or directory如果你是在一个精简的 ARM64 根文件系统比如用 busybox 构建的最小系统上测试可能会遇到这个问题因为系统里的实际动态链接器并不在那个默认路径。先检查编译后的可执行文件依赖了哪个动态链接器readelf -l hello | grep interpreter输出可能是INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238 0x0000000000000019 0x0000000000000019 R 0x8或者直接看readelf -l hello | grep interpreter -A1如果显示[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]而你的目标系统上该文件存在于/lib64/ld-linux-aarch64.so.1或其它自定义路径那么编译时就需要用-Wl,--dynamic-linker把它改成正确的路径aarch64-linux-gnu-gcc -Wl,--dynamic-linker/lib64/ld-linux-aarch64.so.1 -o hello hello.c这个方法在移植到特定嵌入式 rootfs 时特别常用。很多用 Yocto 或 Buildroot 构建的系统rootfs 的目录布局都和 Ubuntu 不一样总不能为了一个程序去改整个文件系统吧直接编译时指定动态链接器路径反而干脆利落。另外还可能出现一种情况是一切路径都正确、但在板子上运行仍然报No such file or directory——这时候别急着怪交叉编译器。先检查一下宿主机上交叉编译出来的程序是被file识别为正确的架构然后检查目标板子上的内核是否支持 ARM64 架构废话不支持的话根本启动不了。更有可能的是目标 rootfs 里缺少必要的共享库比如ldd hello # 在板子上执行如果在板子上执行提示not a dynamic executable那说明你可能不小心把静态链接和动态链接搞混了或者hello不是真正的动态链接产物。8. 常见错误四头文件、库文件架构不匹配导致的诡异编译错误这类问题的特点是头文件找到了但后面跟了一堆莫名其妙的undefined reference或者relocation truncated to fit。根本原因通常是——把 x86_64 的开发库头文件暴露给了 ARM 编译器或者反过来。举个例子。你为了图省事在编译 C 工程时加了-I/usr/include/jsoncpp这个目录下是 x86_64 平台的 JsonCpp 头文件虽然源码头文件能读进去但它内部声明的某些类型与交叉编译器内置类型不一致最终链接时就会有一堆和std::相关的莫名报错。正确做法是对于交叉编译工程所有第三方依赖都要安装 ARM64 版本。Ubuntu 20.04 的 apt 支持多架构安装sudo dpkg --add-architecture arm64 sudo apt update sudo apt install libssl-dev:arm64 libz-dev:arm64dpkg --add-architecture arm64这个命令的作用是告诉 apt除了 amd64我还要安装 arm64 架构的软件包。执行后libssl-dev:arm64这类带架构后缀的包才能被 apt 解析。装完 arm64 版本的开发库后相关头文件和库会安装到/usr/aarch64-linux-gnu/include和/usr/aarch64-linux-gnu/lib交叉编译器在默认情况下是能感知到这些路径的因为 apt 版工具链默认 sysroot 就是/usr/aarch64-linux-gnu。不过有些第三方库的头文件结构比较特殊还需要手动加-I参数。这时候建议在编译命令的第一行加上-v把实际搜索的头文件路径打出来一旦路径里出现/usr/include且后面没跟/aarch64-linux-gnu就要警惕架构污染了。9. 常见错误五libstdc.so.6: cannot open shared object file这个报错通常出现在目标板子运行时而不是编译时。程序已经和宿主机上的交叉编译产物一样能跑了但动态加载器找不到libstdc.so.6。这种情况比分两种拆开来看一是程序是动态链接的但 target 根文件系统里没有 libstdc。排查方法是在板子上执行find / -name libstdc.so.6 2/dev/null如果找不到那就说明你的 rootfs 里压根没有 C 标准库。如果你用的是 Buildroot 或 Yocto 构建的根文件系统可以在配置里开启 C 支持如果是手工搭建的 rootfs可以直接从宿主机交叉工具链里拷sudo cp /usr/aarch64-linux-gnu/lib/libstdc.so.6* /你的目标rootfs/usr/lib/注意最好用arm64架构的 libstdc别把 x86_64 系统的拷过去。可以通过file验证架构file /usr/aarch64-linux-gnu/lib/libstdc.so.6拆开来看二是程序在编译时链接的 libstdc 路径与运行时不符。如果你平时会用-L参数指定库搜索路径比如有些手动部署的工具链放在/opt/toolchain/lib链接时用了/opt/toolchain/lib/libstdc.so.6运行时却去/usr/lib找就会找不到。这时建议在链接时用-Wl,-rpath把库的搜索路径写进最终可执行文件里aarch64-linux-gnu-g -o app app.o -L/opt/toolchain/lib -lstdc -Wl,-rpath,/opt/toolchain/lib我见过很多搞嵌入式的人在开发主机上程序跑得好好的一部署到板子就报 libstdc not found最后查半天发现是 rootfs 里压根没有。说实话交叉编译的“最后一公里”经常就死在这种看似不搭边的运行环境问题上。10. 实战案例给 CMake 工程配置交叉编译很多新手直接用 gcc 命令编译几个文件还好一旦工程大了必然要上 CMake。给 CMake 写工具链文件也是交叉编译的经典步骤一不小心就会踩坑。先写一个工具链文件比如aarch64-linux-gnu.cmakeset(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)这里三个MODE变量的设置非常关键解释一下CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER查找程序时不去交叉根目录找因为编译期用的工具比如 cmake、编译器自身都是 x86_64 的必须在主机上找。CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY查找库时只能从交叉根目录找禁止找到宿主机 x86_64 的.so和.a。CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY查找头文件同理。如果你漏了后面这两行CMake 很容易通过find_package找到宿主机/usr/lib/x86_64-linux-gnu下的库然后编出来的程序架构全乱。使用时mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../aarch64-linux-gnu.cmake .. make添加一个额外提示CMAKE_FIND_ROOT_PATH可以指定多个路径如果你们团队的库统一安装在/opt/arm64-libs下可以写成set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu /opt/arm64-libs)这样 CMake 会在这两个目录下按顺序查找库和头文件。11. 可能被忽略但必须注意的隐性坑11.1 CC 环境变量与 Makefile 的交叉编译逻辑有些工程 Makefile 写得很“标准”已经支持CROSS_COMPILE变量。只要你这样传递make CROSS_COMPILEaarch64-linux-gnu-它就会自动调用aarch64-linux-gnu-gcc。但如果你遇到的代码库写死了gcc那你只能手动改 Makefile 或构造一个 shimmkdir -p ~/bin printf #!/bin/bash\nexec aarch64-linux-gnu-gcc $\n ~/bin/gcc chmod x ~/bin/gcc export PATH~/bin:$PATH这个方案不太优雅但是很多老嵌入式工程确实只能这样绕。另一种更干净的办法是直接改 Makefile 里的CCCC aarch64-linux-gnu-gcc如果工程里分别用了CC、AR、LD等变量则要一并替换AR aarch64-linux-gnu-ar LD aarch64-linux-gnu-ld OBJCOPY aarch64-linux-gnu-objcopy11.2 32 位依赖和 64 位目标的混淆Ubuntu 20.04 上如果同时装了 i386 体系的多架构库偶尔会出现“某库路径在两个架构下都有”的情况。交叉编译时编译器只看自己的 sysroot所以一般不会被宿主机 i386 或 amd64 干扰。但如果你用 apt 装了libc6-dev-i386别慌它不影响。真正要防的是在 CMake 或 Makefile 里直接写死/usr/lib。这种硬编码路径才是交叉编译的头号杀手。11.3 使用 Clang 作为交叉编译前端的替代方案如果你觉得 GCC 系交叉工具链某些报错看不懂也可以试试 Clang。Ubuntu 20.04 的 Clang 对交叉编译支持得不错:sudo apt install clang lld clang --targetaarch64-linux-gnu -gcc-toolchain /usr -o hello hello.c加粗显示一下--targetaarch64-linux-gnu是给 Clang 指定目标平台的关键参数。Clang 本身是天然支持多目标的但需要找到对应的 sysroot 和 GCC 工具链中的库文件。这个方案在交叉编译 C 依赖较多的代码时有时反而更省心。12. 安装后的一套速查命令把自己常用的一套交叉工具链速查命令放在下面能帮你快速定位问题# 查看交叉编译器版本和默认目标 aarch64-linux-gnu-gcc -v 21 | grep Target # 查看头文件搜索路径 aarch64-linux-gnu-gcc -v -E -x c /dev/null 21 | grep ^ # 查看库搜索路径 aarch64-linux-gnu-gcc -print-search-dirs # 查看 sysroot 路径 aarch64-linux-gnu-gcc -print-sysroot # 查看默认库路径 aarch64-linux-gnu-gcc -print-file-namelibc.so # 检查可执行文件依赖哪些动态库 aarch64-linux-gnu-readelf -d hello | grep NEEDED # 检查可执行文件解释器路径 aarch64-linux-gnu-readelf -l hello | grep interpreter这组命令里面有相当一部分是诊断“编译出来的程序能不能在目标系统跑”的关键手段。我在实际调试板子上的程序加载失败时几乎全靠 readelf 这一条来确定是不是动态库依赖问题。13. 最后的实际体会交叉编译工具链本身并不神秘本质就是“一个在 x86 上运行的编译器编译出 ARM 架构的机器码”。而 Ubunutu 20.04 上这套gcc-aarch64-linux-gnu的真正难点不是装而是装完之后的 sysroot、依赖库、路径配置这些容易被忽略的工程化问题。我自己的习惯是每次新搭一台开发环境先花十分钟从头跑一遍 hello world 验证再往里加工程化的依赖——这个土办法虽然看起来慢但在后面用起来反而最省时间。还有一个心得很想分享很多人在交叉编译遇到诡异报错时第一反应是上网复制粘贴命令或者去找所谓的“魔法参数”但其实大多数问题都能靠-v和readelf这两条最基本的命令定位出来。与其迷信某条命令能解决一切不如先搞清楚你的交叉编译器到底在搜哪些路径再根据报错信息逐项核对。上面这些都是我在 Ubuntu 20.04 上折腾交叉编译的真实记录覆盖面从安装命令、验证方式到几个最常见的报错排查应该能帮你少走不少弯路。如果你在配置过程中还遇到其他报错建议先跑一遍那组速查命令把编译器的实际搜索路径和产物状态摸清楚然后再针对性地查会比盲目折腾高效得多。
返回列表