ARTICLE DETAIL

资讯详情

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

arm-linux-gcc交叉编译工具链:安装、参数与排错实战

arm-linux-gcc交叉编译工具链:安装、参数与排错实战 1. 交叉编译这件事先把底层逻辑想透搞嵌入式 Linux 的朋友工作台上迟早会摆上arm-linux-gcc这条工具链。我见过太多人第一次拿到开发板插上串口、连上网线然后下意识地在板子上的终端里敲了个gcc hello.c -o hello编译倒是过了运行起来慢得像蜗牛稍微大一点的工程直接卡死到怀疑人生。这个场景基本上每个做嵌入式Linux的人都经历过也是引出交叉编译这个概念最自然的入口。一句话概括交叉编译在一台性能充沛、工具齐全的机器上生成另一台资源受限、指令集不同的机器能跑的二进制文件。前者叫宿主机通常是 x86_64 架构的 PC 或者虚拟机后者叫目标机通常是 ARM 架构的开发板。arm-linux-gcc 就是干这件事的编译器它本质上是一个 GCC 的前端封装背后配套了针对 ARM 的汇编器、链接器、C 库和头文件。这套东西合起来叫交叉编译工具链。那内容适合谁看如果你刚开始学嵌入式开发被工具链的安装和一堆参数搞得头大这篇文章能让你少走一两周的弯路。如果你已经能编译出程序但总在板子上遇到not found、Illegal instruction、段错误这类问题第三、六章大概率能直接对上你的症状。如果你是要给团队搭一套统一的编译环境第七部分的工程化经验可以直接抄。1.1 为什么不能让板子自己编译自己很多人第一反应是把 GCC 装到板子上不就完了吗技术上确实可行这条路叫本地编译Yocto、Buildroot 也能给你生成带完整工具链的根文件系统。但真到了工程上这么干的人不多原因很实在。开发板的算力瓶颈是硬伤。一块 Cortex-A7 主频 800MHz 的板子编译一个稍大的 C 文件可能比你在 PC 上慢二三十倍。整个 Linux 内核编译下来PC 上十几分钟板子上可能是一夜中间还可能因为内存不足被 OOM Killer 干掉。板子的存储也吃不消一套完整的 GCC 加 binutils 加 glibc 的头文件和静态库轻轻松松吃掉一两个 GB而板载 eMMC 或 NAND 往往只有几百兆到几 G还得留给根文件系统。还有一层是可复现性。本地编译要求每块板子都装一遍工具链版本稍有差异出来的二进制行为就可能不一样。交叉编译把编译环境收敛到宿主机一台机器上版本、参数、依赖全部固定团队里谁编译结果都一致出了问题也好定位。这一点在量产项目里比性能差异更重要。所以交叉编译不是为了炫技是被资源和工程管理逼出来的最优解。1.2 arm-linux-gcc 这个叫法到底指什么严格来说arm-linux-gcc不是一个官方的标准名字它是国内嵌入式圈子里长期沿用下来的俗称。你看到它的时候实际指代的可能是好几种不同的东西。按照 GNU 的三元组命名规范一个完整的交叉编译器名字应该是架构-厂商-操作系统-ABI比如arm-linux-gnueabihf-gcc。这里arm是目标架构linux是目标操作系统gnueabihf是 ABI 描述意思是 GNU EABI 加上硬浮点。而arm-linux-gcc这种省略了中间厂商字段的写法在早期工具链里非常普遍芯片原厂或开发板厂商把工具链编译好打包把主程序统一命名为arm-linux-gcc再配上一堆arm-linux-ld、arm-linux-objcopy、arm-linux-readelf之类的同名前缀工具打包成一个 tar.gz 发出来。这种厂商定制工具链的好处是开箱可用版本和板子上的内核、根文件系统是配套的坏处是版本往往很老很多还停留在 GCC 4.4、4.6 那个年代对 C11、C11 的支持不完整编译现代代码会踩到一堆兼容性问题。所以当我说 arm-linux-gcc 时请你在心里自动做一次映射它是一套以 arm-linux- 为前缀的交叉工具链的统称具体版本和 ABI 要看实际拿到的那一份。1.3 三条获取路线的取舍拿到工具链的途径主要有三条我在不同项目里都用过各有适用场景。路线获取方式优点局限包管理器安装apt 装gcc-arm-linux-gnueabihf一条命令搞定版本新依赖自动处理前缀名带 hf与老教程对不上不保证和目标板 glibc 匹配厂商预编译包解压原厂提供的 tar.gz与板子环境严格配套开箱即用版本偏老32 位二进制依赖兼容库源码自构建crosstool-NG / Buildroot版本、ABI、C 库全可控首次构建耗时一到数小时配置项多我的建议是这样学习阶段或者写 demo直接用包管理器装最快看到效果做正式产品优先用板卡原厂配套的那一份出了问题主要责任在配套版本上好排查如果项目对 glibc 版本有硬要求或者目标平台比较冷门没有现成工具链再上 crosstool-NG 自己构建。顺便说一句经常有人问起那些可视化 IDE 能不能直接编嵌入式硬件代码、要不要折腾一些老牌开发环境答案是工具链才是地基IDE 只是外壳地基不对外壳再花哨编译出来的东西上板一样跑不起来。2. 安装前的宿主机环境准备很多人一上来就tar -xvf解压工具链结果执行时报No such file or directory明明文件就在眼前。这种诡异现象的根子基本都出在环境准备这一步我把它单独拎出来讲因为它是最容易翻车、又最容易被忽略的环节。2.1 系统版本与硬件资源的取舍宿主机推荐用 Ubuntu 20.04 或 22.04 的 LTS 版本服务器版和桌面版都行。选 LTS 的理由是软件源稳定工具链包和依赖库的版本不会突然变。Debian 系的其他发行版也可以逻辑一样只是包名细节有差别。如果你更习惯国产 Linux 发行版同样可行只要它能提供标准的 glibc 和多架构支持交叉编译本身和发行版关系不大真正相关的是宿主机上有没有 32 位运行库。硬件上编译工具链本身对配置要求不高双核 4G 内存足够。但如果你的工作流里还要顺带编译内核、根文件系统、Buildroot 一整套建议至少 8G 内存、SSD 硬盘并且给工作分区留出 100G 以上空间。交叉编译出来的中间产物体积惊人一个内核源码树加上编译输出几个 G 是常态。虚拟机是更常见的形态VMware、VirtualBox、Hyper-V 都可以。这里有个细节值得提务必把虚拟机配置成桥接或 NAT 且能 ssh 到开发板因为后面调试阶段你会频繁在宿主机和板子之间传文件网络不通会非常痛苦。共享文件夹方案不推荐编译大量小文件时性能掉得厉害还容易因为文件系统权限映射问题出各种古怪报错。2.2 32 位兼容运行库最容易翻车的地方这是重中之重。厂商标配的那些 arm-linux-gcc绝大多数是 32 位的 x86 可执行文件。为什么因为它们编译的年代32 位宿主环境还是主流而且 32 位版本能在 64 位系统上通过兼容层跑反过来不行。于是发行方就统一按 32 位打包了。问题在于现在的 64 位 Ubuntu 默认不装 32 位运行库。你解压完工具链敲arm-linux-gcc -v系统会告诉你找不到这个文件但ls又能看到它。这时候不要怀疑人生用file命令看一眼就清楚了file /opt/toolchain/bin/arm-linux-gcc # 输出类似ELF 32-bit LSB executable, Intel 80386, ...看到32-bit Intel 80386就说明缺的是 32 位加载器。解决办法是装多架构支持sudo dpkg --add-architecture i386 sudo apt update sudo apt install libc6:i386 libncurses5:i386 libstdc6:i386 zlib1g:i386老一点的经验贴里会写apt install ia32-libs这个包早就在现代发行版里被拆分了硬装大概率提示找不到。另外lib32z1这个包名也常见于 Ubuntu它和zlib1g:i386提供的是同类功能实际装哪个看你的发行版源里有什么。装完之后再执行arm-linux-gcc -v如果输出了版本和配置信息这一步就过了。注意sudo dpkg --add-architecture i386之后如果apt update报 404通常是软件源里没有启用 i386 架构的仓库镜像检查/etc/apt/sources.list里对应条目是否带了[archamd64,i386]这样的限制条件删掉限制即可。2.3 安装路径与目录规划工具链放哪儿是个看着小、影响后续使用体验的问题。我的习惯是统一放在/opt下面按厂商-版本命名比如/opt/arm-linux-4.4.3或者/opt/gcc-arm-10.3-x86_64-arm-none-linux-gnueabihf。理由有几条。第一/opt的语义就是第三方可选软件包和系统自带的/usr/bin分开将来换版本、删旧版本都干净不会污染系统目录。第二不要把工具链解压到/usr/local下面虽然很多老教程这么写但当你同时维护两三个不同版本的工具链时/usr/local里会乱成一锅粥还容易和包管理器装的东西冲突。第三路径里不要有空格和中文交叉编译时的 Makefile、脚本对空格极其敏感一个空格能让你排查半天。命名建议里带上版本号因为 ARM 的工具链版本差异真的会导致编译结果不同。你写项目文档时明确记录用的是哪一套工具链、从哪来的、什么版本比什么都重要。我见过团队里两个人编译同一个工程一个人用的是 4.4.3另一个人用的是 4.9.4结果一个链接报符号找不到、一个正常查了两天才发现版本不一致。空间占用要有心理准备。以常见的厂商标配工具链为例解压后大约 300M 到 1G 不等如果是从源码构建的完整工具链包含所有多库版本2G 以上很正常。3. arm-linux-gcc 的安装实操三条路环境备好了下面进入正题。我把三条路线分别走一遍你可以根据自己的情况选一条。为了表述方便后面统一用arm-linux-gcc指代编译器主程序实际操作时请替换成你手上工具链的真实名字。3.1 路线一包管理器直接装五分钟见效Ubuntu 和 Debian 都有现成的交叉编译器包包名就是命令名前缀sudo apt update sudo apt install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf装完之后命令名是arm-linux-gnueabihf-gcc。想让老教程里的arm-linux-gcc也能用做个软链接或者写个CROSS_COMPILE变量就行export CROSS_COMPILEarm-linux-gnueabihf-这个变量的用法后面会讲它是内核和 U-Boot 构建系统约定的标准写法ARCH配CROSS_COMPILE就能让make自动找对编译器前缀。验证安装arm-linux-gnueabihf-gcc -v能看到版本信息就成了。这种方式的工具链是 64 位的不需要装 32 位兼容库省了一堆麻烦。缺点是名字和教程对不上且默认是硬浮点 ABI如果你的板子是软浮点老环境编出来的程序跑不了。判断板子用哪种 ABI最直接的办法是在板子上执行readelf -h /bin/ls | grep Flags或者看根文件系统里的动态链接器名字/lib/ld-linux-armhf.so.3是硬浮点/lib/ld-linux.so.3是软浮点。名字里的hf就是 hard float 的意思一眼可辨。3.2 路线二厂商预编译包手动部署这是最符合arm-linux-gcc这个叫法的场景也是嵌入式教程里出现频率最高的方式。拿到压缩包之后sudo mkdir -p /opt/toolchain sudo tar -xvf arm-linux-gcc-4.4.3.tar.gz -C /opt/toolchain注意压缩包内部往往自带一层目录结构解压后可能是/opt/toolchain/usr/local/arm/4.4.3/需要你再把真正的工具链目录挪到你规划的路径下或者干脆把它自带的路径也加入 PATH。这一步不要偷懒先tar -tzf看一眼包内结构再决定解压位置tar -tzf arm-linux-gcc-4.4.3.tar.gz | head -20解压后的目录里通常有bin、lib、include、libexec、share这几层。真正的可执行文件在bin下lib下放的是 ARM 版本的库和头文件给目标程序用的libexec下是 GCC 内部的 cc1、collect2 之类的辅助程序share里是文档和部分可选库。搞清楚这个结构后面配--sysroot的时候心里就有数了。还有一种常见情况是压缩包里是个.tar.bz2或者.tar.xz解压参数换成-xjf、-xJf就行别死记-xvf先看扩展名。3.3 路线三源码自建工具链把控制权拿回来当现成的工具链都不合适就得自己构建。crosstool-NG 是这条路上最成熟的工具配置项清晰社区文档也多。# 拉取源码后进入目录 ./configure --prefix/opt/crosstool-ng make sudo make install # 初始化配置 mkdir -p ~/toolchain-build cd ~/toolchain-build ct-ng list-samples | grep arm-linux ct-ng arm-unknown-linux-gnueabi ct-ng menuconfigmenuconfig里要重点关注的几项Target options里设置架构和 CPU 型号、浮点 ABIToolchain options里选 binutils、GCC、glibc 的版本C-library里决定用 glibc 还是 musl 还是 uClibc。选 glibc 兼容性最好但体积大musl 小巧轻量但在某些依赖 glibc 专有接口的程序上会缺功能需要你根据项目需求权衡。配置完之后ct-ng build构建时间取决于宿主机性能双核 4G 的虚拟机大概 1 到 3 小时。构建过程中最常卡在下载源码包这一步如果网络不稳可以提前把源码包下好放进~/.build/src目录crosstool-NG 会优先用本地已有的包。构建完成后工具链在~/x-tools/arm-unknown-linux-gnueabi/bin/下面把它加进 PATH 就能用了。提示Buildroot 也能顺带生成工具链如果你本来就要用 Buildroot 构建整个根文件系统在Toolchain菜单里选Build toolchain一次构建同时得到工具链和根文件系统两者版本天然匹配这是省事的做法。3.4 环境变量配置与结果验证三条路线走到最后都要做同一件事让系统能找到这套工具链。有三种做法。临时生效只在当前终端有效适合临时测试export PATH/opt/toolchain/bin:$PATH用户级永久生效写进~/.bashrcecho export PATH/opt/toolchain/bin:$PATH ~/.bashrc source ~/.bashrc全局生效适合团队共用一台编译服务器写进/etc/profile.d/sudo tee /etc/profile.d/arm-toolchain.sh /dev/null EOF export PATH/opt/toolchain/bin:$PATH export ARCHarm export CROSS_COMPILEarm-linux- EOF这里多导出了两个变量ARCH和CROSS_COMPILE。它们对内核、U-Boot、BusyBox 这些用 Kbuild 系统的项目来说是必需的不设这两个变量make会用宿主机默认的 gcc 去编 ARM 代码速度上来了但结果全错。养成习惯搭环境时顺手导出后面能省不少事。验证四连击四条都过就没问题了which arm-linux-gcc # 1. 能否找到 arm-linux-gcc -v # 2. 版本是否正常输出 arm-linux-gcc -dumpmachine # 3. 目标三元组应为 arm-linux-gnueabi(hf) echo int main(){return 0;} t.c arm-linux-gcc t.c -o t file t # 4. 实际编译并验证产物架构第四步的file t输出应该包含ELF 32-bit LSB executable, ARM看到 ARM 字样才算真的成功。这一步很多人跳过结果装了个 x86 版本的交叉编译器编译出来还是 x86 程序上板自然跑不了。花十秒验证一次省一整天排查。4. 编译参数详解参数选错比装错还坑工具链装好只是开始真正决定程序能不能在板子上跑起来、跑得快不快的是编译参数。这一块我把最关键的几组拆开讲包括它们背后的原理。4.1 架构与指令集参数-march和-mcpu是一对容易混淆的参数。-march指定的是指令集架构版本决定了编译器可以使用哪些指令取值范围是armv5te、armv6、armv7-a、armv8-a这类。-mcpu指定的是具体的处理器型号比如cortex-a7、cortex-a9、cortex-a53它隐含了对应的架构版本和流水线特性编译器会据此做指令调度优化。实践中常用的组合arm-linux-gcc -marcharmv7-a -mtunecortex-a9 -c main.c -o main.o这里我用-mtune而不是-mcpu原因是-mcpu会同时影响可用指令集和调度优化如果你的代码需要在同架构不同型号的板子上都能跑用-march固定指令集底线用-mtune指定调度目标兼容性更好。反过来说如果你非常确定只跑某一块板子-mcpucortex-a9一步到位编译器会放开该型号的所有指令性能略好。生成的二进制最低运行架构可以用readelf -A看输出里的Tag_CPU_arch就是实际记录的架构版本。如果它比你的板子高上板会直接报Illegal instruction。4.2 浮点 ABIARM 交叉编译最经典的深坑浮点这块是 ARM 交叉编译绕不开的坎-mfloat-abi有三个取值行为完全不同。soft表示完全不用硬件浮点单元所有浮点运算由软件函数模拟跑得慢但兼容性最好任何 ARM 核都能跑。softfp表示使用 FPU 指令做运算但函数调用的参数传递仍然走整数寄存器这样能兼容软浮点的调用约定同时享受硬件计算的速度。hard表示既用 FPU 指令参数传递也走 FPU 寄存器性能最好但和 soft/softfp 编译出来的模块不能直接链接在一起。-mfpu指定具体的浮点单元型号常见的有vfpv3、vfpv3-d16、vfpv4、neon-vfpv4。以 Cortex-A9 为例它带的是 VFPv3-D16 加 NEON那么配置就是arm-linux-gcc -marcharmv7-a -mfpuneon-vfpv4 -mfloat-abihard -O2 main.c -o main或者更保守一点不带 NEONarm-linux-gcc -marcharmv7-a -mfpuvfpv3-d16 -mfloat-abihard -O2 main.c -o main怎么确认板子支持哪种在板子上执行cat /proc/cpuinfo | grep Features输出里如果有vfpv3、neon、asimd这些字段就说明对应的硬件单元存在。再结合根文件系统动态链接器的名字ld-linux-armhf.so.3还是ld-linux.so.3确认 ABI 是 hard 还是 soft。这里的坑在于工具链名字里的 hf 和你编译时加的 -mfloat-abi 必须是同一套。如果你的工具链是arm-linux-gnueabihf它默认就是 hard你不需要也不应该再额外加-mfloat-abisoft那样会得到一个自相矛盾的产物。反之如果工具链是arm-linux-gnueabi无 hf它默认是 softfp你想用 hard 就得换个工具链光加参数是没用的因为工具链配套的 C 库本身就是按 softfp 编的。4.3 链接参数与库搜索路径编译阶段顺了链接阶段的报错往往更隐蔽。最典型的一条是warning: libm.so.6, needed by libfoo.so, not found (try using -rpath or -rpath-link)这是说链接器在找libm.so.6的时候没找到虽然旁边提示的是警告但它经常紧接着引发一堆undefined reference to sqrt之类的错误。原因通常是工具链的 sysroot 路径没被正确识别。解决办法是用--sysroot显式告诉编译器去哪里找目标系统的头文件和库arm-linux-gcc --sysroot/opt/toolchain/arm-linux-gnueabihf/libc hello.c -o hello这个libc目录是工具链自带的、专门给目标系统用的 C 库和头文件和宿主机自己的 glibc 完全是两套东西千万别混。你在/usr/include下看到的头文件是给 x86 用的拿它编 ARM 程序必然出错。如果是链接第三方动态库可以用-L指定搜索路径用-Wl,-rpath-link补充运行时依赖的搜索路径arm-linux-gcc main.o -L./lib -lfoo -Wl,-rpath-link,/opt/toolchain/lib -o app-rpath-link和-rpath的区别值得记一下前者只影响链接阶段的依赖解析不会写进产物后者会把路径写进 ELF 的DT_RUNPATH段影响运行时查找。板子上如果库不在默认搜索路径里运行时就要靠LD_LIBRARY_PATH或者写死的 rpath 来定位。写 rpath 时要小心宿主机上的绝对路径在板子上是不存在的写成/usr/lib这类板子上真实存在的路径才对。5. 从单文件到完整工程把流程跑通参数讲完了来点能直接上手的。这一章从最简单的 hello world 开始逐步走到多文件工程最后上板验证。5.1 单文件编译与产物检查先写一个最基础的测试程序别小看它它同时验证了工具链、C 库、链接器三件事/* hello.c */ #include stdio.h int main(void) { printf(hello from arm\n); return 0; }编译arm-linux-gcc hello.c -o hello如果这一步报stdio.h: No such file or directory说明工具链的 sysroot 没找对报undefined reference to printf说明链接阶段没找到 C 库。两个报错对着前面的 4.3 节处理就行。成功后立刻检查产物file hello # hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), # dynamically linked, interpreter /lib/ld-linux-armhf.so.3, ... readelf -d hello | head -20 # 看依赖了哪些动态库 size hello # 看代码段、数据段占用file那行输出里的三个信息很关键ARM确认架构对了dynamically linked说明是动态链接interpreter后面那一串是板子上必须具备的动态链接器。三个都得和你的板子对得上缺一个上板就报not found。5.2 多文件工程与 Makefile 写法实际项目肯定不止一个文件。假设目录结构是这样project/ ├── inc/ │ ├── calc.h │ └── log.h ├── src/ │ ├── calc.c │ ├── log.c │ └── main.c └── Makefile一份可用的 Makefile 长这样CROSS_COMPILE ? arm-linux- CC : $(CROSS_COMPILE)gcc CFLAGS : -Wall -O2 -g -Iinc -marcharmv7-a -mfpuvfpv3-d16 -mfloat-abihard LDFLAGS : -lm TARGET : app SRCS : $(wildcard src/*.c) OBJS : $(SRCS:.c.o) $(TARGET): $(OBJS) $(CC) $^ $(LDFLAGS) -o $ %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(TARGET) .PHONY: clean几点说明。CROSS_COMPILE ?用?是留了个后门别人可以在命令行覆盖它make CROSS_COMPILEarm-linux-gnueabihf-就能换工具链。CFLAGS里-Iinc指定头文件目录-g保留调试信息-O2开优化。LDFLAGS的-lm链接数学库因为浮点函数需要它。wildcard和patsubst这种写法让我不用手动维护源文件列表加文件不用改 Makefile工程大了很省心。$^是全部依赖$是第一个依赖$是目标这三个自动变量记住能少写很多重复内容。编译后检查每个目标文件的架构一致性arm-linux-gcc -c src/calc.c -o /tmp/calc.o readelf -A /tmp/calc.o | grep -E CPU_arch|VFP_args|FP_arch输出里Tag_ABI_VFP_args: VFP registers表示是硬浮点Tag_FP_arch: VFPv3-D16和你-mfpu的设置对应。如果某个目标文件的这些属性和其他文件不一致链接时就会报 ABI 不兼容这时候就用这条命令逐个排查。5.3 静态库、动态库与体积优化工程模块化之后公共代码通常会打成库。静态库的做法arm-linux-gcc -c src/log.c -o log.o arm-linux-ar rcs liblog.a log.o注意arm-linux-ar不要用宿主机默认的ar虽然通常也能用但严格来说应该用工具链配套的那个避免格式差异。链接静态库arm-linux-gcc src/main.c -L. -llog -o app动态库则要加-fPIC和-sharedarm-linux-gcc -fPIC -c src/log.c -o log.o arm-linux-gcc -shared -o liblog.so log.o动态库的运行时查找路径是常见坑。板子上如果没有把liblog.so放进/lib或/usr/lib程序启动会报error while loading shared libraries: liblog.so: cannot open shared object file。三种处理方式把库拷进板子的标准库目录在启动脚本里导出LD_LIBRARY_PATH/opt/app/lib编译时写死-Wl,-rpath,/opt/app/lib。生产环境我倾向第三种路径固定、不依赖环境变量启动可控。体积优化方面-Os优化体积-ffunction-sections -fdata-sections配合-Wl,--gc-sections扔掉未使用的函数和数据最后arm-linux-strip app剥掉符号表。一套下来一个几兆的可执行文件能压到几百 K对存储紧张的板子很有意义。arm-linux-gcc -Os -ffunction-sections -fdata-sections \ -marcharmv7-a -mfpuvfpv3-d16 -mfloat-abihard \ src/*.c -Wl,--gc-sections -o app arm-linux-strip app注意strip之后就没办法用 gdb 调试了符号表没了栈回溯只能看到裸地址。调试版本记得单独保留带符号的产物用make的 debug 目标分别输出别把发布版本覆盖掉。5.4 上板验证与运行环境确认编译产物传到板子上传输方式看你的开发环境scp、tftp、NFS 挂载、U 盘都行。scp 最省事scp app root192.168.1.100:/opt/app/上板后先加执行权限再运行chmod x app ./app。如果启动报-sh: ./app: not found但文件明明在这九成不是找不到文件而是找不到程序需要的动态链接器。用readelf -l app | grep interpreter看它要求哪个链接器再去板子上ls /lib/ld-linux*确认存在。名字对不上就是 ABI 不匹配回头检查你的-mfloat-abi和板子根文件系统是不是一套。这个报错太有迷惑性我第一次遇到的时候折腾了小半天记录下来希望能帮你省点时间。如果报Illegal instruction那就是指令集或浮点指令超出板子能力范围了用readelf -A看产物的架构属性和/proc/cpuinfo里的实际情况对一下把-march调低或者-mfpu换成更基础的型号。6. 典型报错与排查技巧实录这一章是我自己踩过的坑汇总按阶段分类整理附上排查思路。我把它们做成速查表放在最后方便对照。6.1 编译期报错症状一arm-linux-gcc: No such file or directory但文件确实存在。这是 32 位运行库缺失的经典表现用file确认工具链是 32 位 ELF然后按 2.2 节装 i386 依赖库。另一种可能是 PATH 没配对用绝对路径试一次/opt/toolchain/bin/arm-linux-gcc -v能跑就说明是环境变量问题。症状二fatal error: stdio.h: No such file or directory。头文件找不到通常是 sysroot 路径不对或者工具链的目录被搬动过导致内部相对路径失效。工具链对自身路径很敏感很多老工具链在内部硬编码了安装路径挪个位置就可能找不到自己的头文件。所以解压位置定好之后就别再挪了非要挪就重新解压一次比修路径省事。症状三error: unknown type name xxx大量出现。常见于用老旧工具链GCC 4.4 那个年代编译用了 C99、C11 特性的新代码。老工具链默认的 C 标准版本很旧可以试试加-stdgnu99或-stdgnu11强制指定但有些新特性是真的不支持只能改代码或者换工具链。6.2 链接期报错症状一undefined reference to sqrt。数学库没链接加-lm。注意-lm要放在源文件或目标文件后面链接器从左到右解析顺序反了同样找不到符号。这个顺序问题在链接多个静态库时特别容易犯规则是被依赖的库放在依赖它的库后面。症状二cannot find -lxxx。库不存在或路径不对。先用arm-linux-gcc -print-search-dirs看编译器的默认搜索路径再用find /opt/toolchain -name libxxx*在工具链目录下找库文件。找不到就说明这个库没有 ARM 版本需要自己交叉编译一份。症状三error: ... uses VFP register arguments, ... does not。典型的浮点 ABI 混用一部分目标文件是 hard 浮点编的另一部分是 softfp。用readelf -A逐个查找到那个不一致的重新编译。第三方预编译的.a文件经常有这个坑因为它们大多按 softfp 编译和你的 hard 工程混不到一起。6.3 运行期报错症状一not found文件存在。前面说过是动态链接器缺失或名字不匹配用readelf -l确认需求和板子上实际有的对比。症状二error while loading shared libraries: libxxx.so.6: cannot open shared object file。库缺失或搜索路径不对。用LD_LIBRARY_PATH临时验证确认库存在之后就把它放到标准路径或者写 rpath。症状三GLIBC_2.29 not found。这是一个特别典型的版本倒挂问题你用的工具链带的 glibc 比板子上的新程序里引用了新版才有的符号板子上找不到。解决办法有几个方向换用和板子 glibc 版本匹配的工具链把板子上的 C 库升级风险大可能影响系统其他程序或者干脆静态链接-static把 C 库打进可执行文件。静态链接是最省事的应急方案代价是体积膨胀而且如果程序用了dlopen之类的动态加载功能会受限制。症状四Bus error或莫名的Segmentation fault。如果代码在 x86 上跑得好好的交叉编译后就崩优先怀疑内存对齐问题。ARM 对未对齐访问比 x86 严格得多x86 上能容忍的写法在 ARM 上直接崩。用-fsanitizeaddress需要工具链支持老的 glibc 可能没有退而求其次的做法是逐个指针操作检查或者用-maligned-data之类的参数试着规避。6.4 排查速查表报错关键词出现阶段大概率原因处理方向No such file or directory执行工具链缺 32 位运行库装 libc6:i386 等依赖stdio.h 找不到编译sysroot 路径错误加 --sysroot 或检查工具链路径undefined reference链接库顺序或库缺失调 -l 顺序补 -lmcannot find -lxxx链接库路径不含加 -L或自己编库VFP register arguments链接浮点 ABI 混用readelf -A 找不一致的目标文件not found文件在运行动态链接器不匹配检查 -mfloat-abi 与根文件系统cannot open shared object运行动态库缺失LD_LIBRARY_PATH 或 rpathGLIBC_x.xx not found运行glibc 版本倒挂换工具链或静态链接Illegal instruction运行指令集/浮点超出降低 -march 或换 -mfpuSegmentation fault运行未对齐访问等检查指针和结构体对齐提示遇到奇怪问题第一件事永远是用file和readelf把产物的架构、ABI、依赖全部看清楚。交叉编译的问题八成能在 ELF 头里找到答案这是我用下来最有价值的一条经验。6.5 几条不写进文档但很实用的经验第一永远保留一份带-g的调试版本。发布版 strip 到极致没问题但调试版必须同时产出用make debug和make release两个目标分别管理。板子上出问题的时候有符号表和无符号表的排查效率差十倍。第二建立自己的最小验证工程。我的习惯是维护一个 hello.c 加一个带浮点运算和 pthread 的小 demo每次换工具链、换板子先编这两个跑通再动正式代码。这能把工具链问题和业务代码问题隔离开省大量时间。第三工具链版本写进项目 README。具体到从哪里获取、什么版本号、解压到哪个路径、需要的环境变量。团队协作时这条比写多少注释都有用。第四遇到 glibc 版本问题优先考虑静态链接兜底。不要一上来就想着升级板子上的 C 库那东西牵扯整个根文件系统改动风险远大于收益。第五-Wall从第一天就打开。交叉编译下的未定义行为比本地编译更危险因为很多在 x86 上看起来没事的写法在 ARM 上会直接暴露成崩溃。编译警告是免费的代码审查别关掉。7. 工程化补充让工具链真正服务项目单次编译跑通只是及格线要让它稳定支撑项目开发还有几件事值得做。7.1 与主流构建系统集成手写 Makefile 适合小工程稍大一点的项目会上 CMake 或 Autotools。CMake 集成交叉编译的标准做法是写一个 toolchain 文件# arm-toolchain.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CROSS_PREFIX /opt/toolchain/bin/arm-linux-) set(CMAKE_C_COMPILER ${CROSS_PREFIX}gcc) set(CMAKE_CXX_COMPILER ${CROSS_PREFIX}g) set(CMAKE_FIND_ROOT_PATH /opt/toolchain/arm-linux/libc) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)使用mkdir build-arm cd build-arm cmake -DCMAKE_TOOLCHAIN_FILE../arm-toolchain.cmake .. make -j$(nproc)CMAKE_FIND_ROOT_PATH_MODE_*这三行很关键。它们限制 CMake 只在目标 sysroot 里找库和头文件不去宿主机系统目录里翻。不加这几行find_package很可能找到 x86 版本的库链接出一堆莫名其妙的错误。Autotools 的项目就靠环境变量传参./configure --hostarm-linux --prefix/opt/app \ CCarm-linux-gcc CXXarm-linux-g \ --disable-shared --enable-static--host是关键它告诉 configure 这是交叉编译会自动切换掉那些不能在宿主机上运行的目标程序检查。--disable-shared生成纯静态省掉板子上放一大堆.so的麻烦。7.2 多版本共存与团队环境统一项目多了之后手上同时有几套工具链是常态。用 3.4 节的profile.d方案会打架更优雅的做法是封装一个环境切换脚本# /opt/tc-select.sh case $1 in a7) export PATH/opt/toolchain-a7/bin:$PATH ;; a53) export PATH/opt/toolchain-a53/bin:$PATH ;; *) echo usage: source tc-select.sh {a7|a53}; return ;; esac export ARCHarm export CROSS_COMPILEarm-linux- arm-linux-gcc -dumpmachine用source tc-select.sh a7切换末尾那句会打印当前工具链的目标三元组避免切错还不自知。团队环境统一方面最省事的办法是用 Docker 把工具链和依赖一起打包新同事拉个镜像就能开工不用再走一遍 32 位兼容库那些坑。这个方案我推过两个团队效果都很好尤其是需要同时维护三四个老项目的场景。7.3 什么时候该考虑换掉老工具链厂商标配的 arm-linux-gcc 能用但用久了会遇到天花板。几个信号值得注意代码里想用 C11 的std::thread编不过第三方库的编译要求 GCC 4.8 以上-fsanitize系列排查工具用不了编译大工程时链接时间离谱地长。这些都是老工具链的常见局限。可以考虑迁移到更新的arm-linux-gnueabihf-gccUbuntu 源里就有或者用 crosstool-NG 构建一个版本合适的。迁移前必须确认板子的 glibc 版本因为这决定了你能用多新的工具链一个实用的判断标准是工具链自带 glibc 的版本号不能高过板子上 glibc 的版本号。板子上查版本ldd --version # 直接看 glibc 版本如果板子是 glibc 2.23工具链最好选 2.23 或更低版本配套的实在要用新版工具链静态链接兜底。迁移过程中最容易出问题的就是那些预编译的第三方.a库它们往往是按老 ABI 编的和新的硬浮点工具链混不到一起需要一并重新编译。8. 最后聊几句实际使用的心得这套工具链我已经在很多个项目里来回折腾过从最早照着教程一步步敲到后来给团队搭统一的编译镜像踩的坑基本都在前面几章里了。有几点体会是真金白银换来的ABI 是交叉编译的命门架构、浮点、C 库版本三个维度任意一个不匹配程序就上不了板而且报错信息往往指着错误的方向工具链路径定好就别动很多老工具链内部有硬编码路径挪一次可能就是几个小时的排查编译产物一定用 file 和 readelf 验一遍别嫌麻烦这十秒能省掉上板后一整天的心慌。还有一个容易被忽略的点arm-linux-gcc这套命名确实承载了一代嵌入式人的记忆博客、教程、老项目里到处是它。但现在的工程实践里更规范的arm-linux-gnueabihf-gcc正在成为主流新的项目没必要为了跟老教程对齐硬凑旧名字。工具链选型看的是 ABI 匹配和版本合适不是看名字顺不顺口。理解了这一层你在面对任何新平台、新工具链的时候都能用同一套思路快速上手先搞清楚目标平台的架构和 ABI再找匹配的工具链然后file加readelf验证到底。这套方法比记住任何一条具体命令都管用。
返回列表