ARTICLE DETAIL

资讯详情

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

嵌入式Linux必备:aarch64-linux-gnu-gcc交叉编译工具链全解析

嵌入式Linux必备:aarch64-linux-gnu-gcc交叉编译工具链全解析 1. 为什么嵌入式开发绕不开aarch64-linux-gnu-gcc1.1 从“在电脑上编译、在板子上运行”说起做嵌入式开发的人基本都逃不过交叉编译这个词。简单来说交叉编译就是在一台架构不同的电脑上编译出目标机器能运行的二进制程序。我们日常用的PC几乎都是x86_64架构而嵌入式开发板大多数是ARM架构比如树莓派、RK3588、全志系列、飞凌或正点原子的各种开发板。如果你直接在板子上跑gcc也不是不行但板子的CPU性能通常比较弱编一个大点的Linux内核或Qt应用几个小时甚至一天就过去了。而在PC上做完交叉编译几分钟就能出结果这效率差距是肉眼可见的。aarch64-linux-gnu-gcc这个名字看起来很绕其实拆开就明白了aarch64是ARM的64位指令集架构linux表示目标系统是Linuxgnu表示用的是GNU工具链体系gcc自然就是编译器本体。所以它的含义非常明确——在x86主机上编译出能够在ARM 64位Linux系统上运行的程序。这篇文章主要写给那些刚接触嵌入式Linux开发、正准备搭环境拉代码编译的朋友。网上教程很多很杂有的过时了有的只是APT装个包就完事至于装完怎么验证、编出来的程序为什么在板子上跑不起来、遇到奇怪报错怎么定位基本没人讲清楚。我会完整走一遍安装流程把那些踩过才知道的坑一并交代清楚。1.2 工具链的完整组成和原理很多人以为工具链就是一堆gcc命令其实不止。一个完整的交叉编译工具链包含编译器、汇编器、链接器、库文件、头文件以及调试工具和二进制分析工具。用APT安装的gcc-aarch64-linux-gnu只是一个编译器前端真正干活的时候还要配合binutils-aarch64-linux-gnu里的as、ld以及libc6-dev-arm64-cross提供的标准C库头文件和二进制库。整个编译过程其实分为四步预处理、编译、汇编和链接。预处理阶段会展开宏和头文件编译阶段把C/C源码翻译成汇编汇编阶段生成机器指令的目标文件链接阶段把目标文件和库文件最终绑定成可执行文件。交叉编译和本地编译的区别主要就体现在工具链所面向的架构不同。aarch64-linux-gnu-gcc生成的是AArch64指令集的机器码它做的事情和gcc生成x86_64机器码没有本质差别只是目标平台不同。有一点要特别注意工具链内部涉及三套环境——build构建环境、host运行环境、target目标环境。对交叉编译来说build是你的PCx86_64host也是你的PCtarget才是ARM开发板。理解不了这一层后面配置CMake时很容易把自己绕晕。2. 安装前必须想清楚的三个问题2.1 你的目标系统是32位还是64位这是最容易被新手忽略的问题。ARM架构目前能看到三种形态armel32位软浮点、armhf32位硬浮点、aarch6464位。如果开发板是树莓派较早的型号、全志H3或者其他32位平台那你需要的是arm-linux-gnueabihf-gcc如果板子是RK3588、树莓派4B/5、香橙派5这类64位平台才用aarch64-linux-gnu-gcc。怎么判断最简单的办法是开机后在板子终端执行uname -m输出aarch64就是64位输出armv7l就是32位。这个信息直接决定了你装的工具链到底对不对。装错链子恨终身编译出来的程序拷贝到板子上只会报Exec format error。2.2 选择系统自带源还是官方独立工具链Ubuntu和Debian的软件源里都有交叉编译工具链直接APT安装非常方便这也是多数人入门时的首选。但如果你要编译大型项目比如完整版的Qt、GStreamer、Linux内核系统源里的工具链版本可能偏老或者缺少某些架构相关的优化选项这时候就必须考虑从Linaro或ARM官网下载最新版本的工具链。Linaro是一家专门为ARM生态做工具链适配的组织它发布的aarch64-linux-gnu-gcc工具链通常比发行版源里的版本更新优化也更激进。ARM官网也提供官方的GNU工具链但需要注册登录才能下载稍显繁琐。从Linaro下载的压缩包解压后把bin目录加入PATH就能用完全不依赖系统包管理器也不会和系统自带的gcc冲突。2.3 你的构建主机系统位宽和依赖情况建议用64位的Ubuntu 20.04 LTS或22.04 LTS做交叉编译环境32位主机虽然理论上可以但很多依赖库和工具链已经逐步放弃32位支持了。装工具链之前先把基础的构建依赖装上不然编译中后期才报缺库排查起来很伤神sudo apt update sudo apt install -y build-essential git wget file tree先把这些装好后面验证工具链、编译测试程序的时候就不会被环境问题干扰。3. 保姆级安装实操全流程3.1 方案一APT直接安装新手推荐用APT安装是最省力的方案完全自动处理依赖关系系统源里预编译好的版本稳定可靠。打开终端执行sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu如果想完整一点把binutils和C库也一起装sudo apt install -y binutils-aarch64-linux-gnu libc6-dev-arm64-cross安装完成后验证一下版本aarch64-linux-gnu-gcc --version如果出现类似gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04)的输出说明安装成功。注意APT源里附带的gcc本身就是完整的交叉编译器前端不要再额外装gcc-arm-linux-gnueabihf除非你确实需要32位目标版本。3.2 方案二Linaro工具链适合编译大型项目从Linaro官方下载最新工具链需要先确认自己的主机架构。x86_64主机选x86_64版本的aarch64-linux-gnu工具链压缩包。下载后用tar解压到/opt目录下sudo mkdir -p /opt/aarch64-linux-gnu sudo tar xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt解压后的目录名可能是gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu里面的bin目录就有aarch64-linux-gnu-gcc等一堆工具。为了后续使用方便把它加进PATHexport PATH/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH如果不想每次开终端都手动export可以把这行追加到~/.bashrc末尾。3.3 环境变量怎么配才不踩坑很多教程会让你配CROSS_COMPILE环境变量这在编译U-Boot和Linux内核时是必需的export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm64注意CROSS_COMPILE后面一定要带个短横线这样Makefile才能拼出aarch64-linux-gnu-gcc这样的完整命令。ARCHarm64是告诉内核构建系统目标架构是64位ARM。这两个变量是内核和Bootloader编译的敲门砖不配或者配错Makefile就会默认用本机的gcc然后编出一堆x86_64的.o文件最后链接的时候报一堆莫名其妙的错误。对于CMake工程通常是用工具链文件的方式指定编译器而不是依赖环境变量。下面这份交叉编译工具链文件可以直接保存为aarch64-toolchain.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)其中CMAKE_FIND_ROOT_PATH指向的是目标系统的根目录也就是交叉编译时用来查找库和头文件的路径。这里设置为/usr/aarch64-linux-gnu是因为Ubuntu的交叉编译库默认装在这个位置。三个MODE设置的含义分别是不查找主机上的可执行程序只查找目标系统根目录下的库和头文件这是防止交叉编译时误用本机库的关键。3.4 最简单可靠的验证方法装完之后应该先写个最小的C程序验证工具链是否真的能工作。我在/tmp下建一个测试目录写一个hello.c#include stdio.h int main(void) { printf(Hello, AArch64!\n); return 0; }然后交叉编译aarch64-linux-gnu-gcc -o hello hello.c不要急着一跑本地是x86_64机器执行它会直接报Exec format error。正确做法是用file查看生成的文件类型file hello如果输出里包含ELF 64-bit LSB executable, ARM aarch64说明编译成功且目标架构正确。想确认程序逻辑没问题可以用QEMU用户态模拟执行sudo apt install -y qemu-user ./hello这一下如果输出Hello, AArch64!说明从编译到运行整个链路完全没问题。4. 高频报错与排查技巧实录4.1 明明装好了却提示Command not found最典型的现象是执行aarch64-linux-gnu-gcc --version时报command not found但之前明明安装成功了。九成原因都是PATH没配置好尤其用Linaro方式安装时最容易出这个问题。打开终端后先检查一下编译器的实际路径which aarch64-linux-gnu-gcc find / -name aarch64-linux-gnu-gcc 2/dev/null如果find能找到但which找不到那基本就可以确定是PATH的问题。把工具链的bin目录加到PATH里或者直接用绝对路径调用。还有一个隐蔽的问题是APT安装后命令名可能不是标准的aarch64-linux-gnu-gcc而是带版本号的后缀。用ls /usr/bin/aarch64*看一下实际有哪些命令如果有多个版本手动软链接一个过去就行。4.2 运行时提示No such file or directory但文件明明存在这个报错特别容易把新手带偏。你在编译主机上执行file看可执行文件确认是aarch64架构拿到板子上执行却提示No such file or directory。注意这个提示并不是说文件不存在而是说找不到动态链接器。32位ARM程序的链接器路径通常指向/lib/ld-linux-armhf.so.364位程序指向/lib/ld-linux-aarch64.so.1。如果板子的文件系统里没有这个文件或者路径不对内核加载器就会返回这个错误。排查方法很简单在主机上用readelf查看程序的解释器路径aarch64-linux-gnu-readelf -l hello | grep interpreter如果输出显示需要/lib/ld-linux-aarch64.so.1那就到板子上确认一下这个文件是否存在。如果不存在要么重新安装libc库要么在编译时加上-static做静态链接。静态编译的二进制不依赖动态链接器直接就能跑但体积会大不少。做产品的话建议还是用动态编译把库配好方便后续更新维护。4.3 编译时找不到头文件的Fatal error处理交叉编译时最常出现的几个错误一个是stdio.h找不到一个是string.h找不到还有各种库文件找不到。这种情况基本可以断定是C库没装好。用APT方式安装时必须确认libc6-dev-arm64-cross装上了。它负责提供ARM架构的C标准库头文件和二进制文件没有这层依赖gcc编译器本体就算装在完美也编不了任何带标准库的程序。如果你用的不是APT源里的工具链就要手动指定sysroot。sysroot可以理解为目标系统的根文件系统在主机上的映射编译器在里面找头文件和库。Linaro工具链一般自带sysroot在源码目录下有个aarch64-linux-gnu/libc目录。使用时要加参数aarch64-linux-gnu-gcc --sysroot/opt/aarch64-linux-gnu/aarch64-linux-gnu/libc -o hello hello.c也可以用环境变量统一指定但每次命令行加参数更直观不容易把不同项目的配置混在一起。4.4 CMake配置后编译报“does not contain a CMakeCache file”这个问题的根源在于CMake的构建缓存和工具链文件不能混用。如果你先在没有指定工具链文件的情况下跑过一次cmake再切换工具链重新cmake新配置会和旧的CMakeCache.txt冲突导致各种诡异报错。解决办法是删掉build目录重新来rm -rf build mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../aarch64-toolchain.cmake .. make还有个常见问题是忘记设置CMAKE_FIND_ROOT_PATH_MODE_PROGRAM为NEVER导致CMake在找主机工具时误用了目标板的路径进而报错找不到编译器。具体表现可能是在CMake的日志中出现could NOT find a C compiler。遇到这类问题把工具链文件按前面给的模板写一遍再清空build目录重新配置基本都能解决。4.5 链接阶段报undefined reference的应对思路链接时的undefined reference往往和编译时有关常见原因是你引用的第三方库没有用交叉编译器重新编译。以前在x86上编出来的.so库是x86_64格式交叉链接器当然不认。开了几个项目后发现很多人把目标板的库直接拷到主机上然后指定链接路径这是典型的操作误区。正确做法是拿到库的源码在编译主机上用aarch64-linux-gnu-gcc重新编译一遍得到ARM架构的库版本再参与链接。还有一个隐蔽点Ubuntu各版本把库文件放在/usr/aarch64-linux-gnu/lib下但libpthread.so、libm.so这些系统库的链接形式可能是指向libc.so.6的软链接如果软链接断了链接器会报找不到。这种情况下用dpkg -S查一下哪些包包含缺失的库文件重新安装对应包就能恢复。4.6 常见问题速查表现象可能原因解决方案command not foundPATH未配置配置PATH或使用绝对路径Exec format error在x86主机直接执行ARM程序用file检查后拷贝到板子运行No such file or directory缺少动态链接器或库readelf查看interpreter路径补装库或静态编译stdio.h not found缺少libc-dev-arm64-cross安装对应C库开发包CMakeCache冲突工具链切换后缓存残留删build目录重新配置undefined reference第三方库未交叉编译用目标架构源码重新编译库内核编译缺少scripts内核源码未清理make distclean后重新配置4.7 一个真实踩坑经历我差点以为是工具链坏了有一年帮朋友交叉编译一个带OpenSSL的应用程序编译阶段一切正常链接时却反复报libcrypto.a的architecture mismatch。刚开始怀疑是工具链版本问题换了好几个版本结果都一样。后来用aarch64-linux-gnu-ar t libcrypto.a看了下里面的目标文件发现全部是x86_64的。原来朋友图省事从网上直接下载了一个预编译的OpenSSL库那个库是给本机用的。平时编译x86程序根本没发现问题直到我做交叉编译所有隐藏的问题一次性暴露出来了。这个经历说明一个道理交叉编译环境里库的架构一致性比什么都重要。拿到任何库文件第一件事不是急着配置路径而是用file检查格式。养成这个习惯能帮你避开很多莫名其妙的坑。5. 编译内核时的环境准备注意事项5.1 内核构建的特殊性交叉编译工具链装好了很多人的下一个目标是编译Linux内核。内核编译和普通应用程序编译有不少区别最典型的是需要额外的工具处理设备树DTS文件。设备树源文件需要dtc编译器转换成dtb二进制内核构建系统会自动调用scripts/dtc下的工具来做这件事。还有一个内核特有的问题是很多外部模块需要依赖内核源码的头文件。如果你要编译一个外部内核模块光有工具链还不够必须先在源码目录下执行make prepare和make modules_prepare把生成的头文件和符号文件准备好。这一步失败了后面编译任何模块都会报找不到generated/autoconf.h之类的错误。5.2 内核编译常用的配置命令用最精简的方式走一遍export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig make -j$(nproc) Image dtbsARCH和CROSS_COMPILE两个变量必须设置这是Makefile判断目标架构和交叉编译器的依据。defconfig会生成以当前架构默认配置为基础的.config文件。如果目标板有自己的配置比如rockchip_linux_defconfig那就要用对应的配置项替换defconfig。编译产物在arch/arm64/boot/Image和一系列dtb文件。内核镜像编出来之后可以直接用fastboot或dd写入板子分区但具体刷机方式因板而异务必先查清楚。由于现代内核会用到主机端的host工具所以主机上也要装好bison、flex、libssl-dev等构建依赖。少了这些编译会在非常靠前的阶段就停下来报错信息可能是You must install flex或unable to find openssl headers。逐个装齐就好不算复杂。6. 实际交叉编译一个C项目并部署到板子6.1 规范的工程目录结构为了演示完整流程我建一个简单的LED控制项目包含设备树无关的测试逻辑ledctrl/ ├── CMakeLists.txt ├── src/ │ ├── main.c │ └── gpio_util.c └── include/ └── gpio_util.hCMakeLists.txt内容如下cmake_minimum_required(VERSION 3.16) project(ledctrl C) set(CMAKE_C_STANDARD 11) include_directories(include) add_executable(ledctrl src/main.c src/gpio_util.c)这个简单的工程结构适合刚开始接触交叉编译的人。拿实际项目练手时建议先建一个类似的目录骨架把源文件和头文件规范分开比全部塞在一个目录里清爽得多也方便后面写CMake或Makefile。6.2 用工具链文件组织CMake构建在项目根目录下放好前面写的aarch64-toolchain.cmake然后执行mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../aarch64-toolchain.cmake .. make整个构建过程如果没报错build目录下就会生成ledctrl可执行文件。我再强调一遍重点是用file检查file ledctrl如果看到ELF 64-bit LSB executable, ARM aarch64说明这份文件已经可以在ARM 64位Linux上运行了。6.3 拷贝到开发板执行的正确做法用scp或者U盘把ledctrl拷贝到开发板后用root权限执行。程序里如果直接操作GPIO一般需要root权限访问/dev/mem所以执行方式通常是sudo ./ledctrl如果程序是纯计算逻辑不需要操作硬件普通用户也能跑。首次部署时建议在板子上先跑一下ldd ./ledctrl确认动态库依赖都满足否则会启动失败且报错不明显。部署这一步经常出问题的是目标板文件系统缺少某些共享库拿ldd检查一遍就能快速定位。7. 工具链的进阶配置与调试技巧7.1 利用汇编文件快速验证目标架构有时候想快速验证工具链是否工作正常不用写完整C程序可以直接内联汇编生成一个最简单的目标文件echo mov x0, #42; ret | aarch64-linux-gnu-as -o test.o - aarch64-linux-gnu-objdump -d test.o如果objdump能正确反汇编并显示mov x0, #0x2a和ret说明汇编器工作正常。这个技巧在面对工具链整体怀疑时会很有用能帮你区分问题出在编译器、汇编器还是后续的链接阶段。7.2 用gdb和gdbserver做远程调试嵌入式开发里代码调试一直是很费神的部分。交叉编译工具链里自带的aarch64-linux-gnu-gdb可以配合开发板上的gdbserver做远程调试。在开发板上启动gdbserver监听某个端口比如2345gdbserver :2345 ./ledctrl然后在PC主机上执行aarch64-linux-gnu-gdb ledctrl (gdb) target remote 板子IP:2345连接成功之后就可以像本地调试一样打断点、单步执行、查看寄存器了。有一点要提醒编译可执行文件时加上-g选项调试信息才会保留。另外开发板上的gdbserver版本要和主机端gdb版本尽量一致跨版本容易出兼容性问题。7.3 静态编译与动态编译的取舍前面多次提过静态编译这里把取舍讲透。静态编译-static最大优点是部署简单二进制拷贝到板子上直接能跑不需要考虑动态库依赖。缺点是二进制体积显著变大而且如果程序用了NSSName Service Switch这类依赖动态加载机制的库即使静态编译也可能出现域名解析异常。动态编译则正好相反体积小加载快升级库时程序自动受益但部署时必须保证目标板有配套的库文件。具体的判断标准是程序规模小、依赖库少优先静态编译程序用了很多系统库或者设备上有包管理器可以直接装库优先动态编译。8. 我个人的工具链版本选择心得文章写到这里整个交叉编译工具链的安装和使用流程就全部走完了。最后分享一个比较个人化的建议如果做产品开发尽量跟生产环境保持一致的系统版本和工具链版本不要频繁升级。交叉编译里的问题很多时候不是你不会用而是环境版本不一致导致的隐性不兼容。改一行库的版本可能就引出几十个编译错误排查起来非常耗时。我个人的习惯是每个长期项目固定一个工具链版本并且把工具链下载地址、安装方式、测试方法写进项目的README里。这样过几个月换电脑或者新同事加入时照着文档十分钟就能把环境重建出来不用凭记忆补一堆细节。如果只是自己学习验证APT源里的版本完全够用。如果是商业量产或者对性能有极致要求Linaro的版本一般更新编译器对最新ARM处理器的调度优化更好实测下来同样的代码性能可能有5%到10%的差距。选型没有绝对的对错适合自己项目的阶段和节奏就好。
返回列表