ARM架构与交叉编译实战:嵌入式ARM开发17天总结
2026/9/12 12:22:22 网站建设 项目流程

系统学习嵌入式ARM开发到今天正好第17天,准备把这个阶段的东西完整梳理一遍,标题就叫DAY17-ARM 架构与交叉编译。说实话,ARM架构这个词我第一次听的时候觉得挺玄乎,实际上把它拆开成指令集架构、微架构、生态三个层面之后就好理解多了;而交叉编译则是嵌入式开发最绕不开的一道坎:你的开发机通常是x86架构,目标板却是ARM架构,怎么让程序在目标板上跑起来,靠的就是一套专门的工具链。这篇文章我不会讲太深的理论,更多是把我实际搭建环境、编译程序、踩坑排查的过程拿出来分享,希望能给同样在学ARM、在做Linux移植、或者刚拿到一块开发板不知道从哪下手的读者一点参考。

1. ARM架构到底在讲什么:指令集、微架构与生态选型

1.1 指令集架构与微架构:能跑什么和怎么跑是两回事

很多东西一说到"ARM架构"就容易混为一谈。实际上ARM架构严格来说指的是指令集架构(Instruction Set Architecture,ISA),它决定了一款CPU"认识"哪些指令、寄存器怎么编排、内存怎么访问、异常怎么处理。而微架构(Microarchitecture)指的是具体怎么实现这些指令,比如流水线几级、缓存多大、乱序执行还是顺序执行。换句话说,ISA是"接口规范",微架构是"内部实现",二者是两码事。

同一套ARM指令集,可以有完全不同的微架构实现。Cortex-A72、Cortex-A76、Cortex-X1这些都是ARMv8-A架构下的不同微架构,但它们都能跑同一份编译好的ARMv8-A指令的程序。反过来,同样叫ARM处理器,如果一个是ARMv7-A、一个是ARMv8-A,那它们能跑的指令就不是同一套,二进制程序通常不能直接互换。这也是为什么"arm架构"这个词在选型时不够精确——你得说清楚具体是哪个架构版本,比如ARMv7、ARMv8、ARMv9,以及在这个架构版本下用的是哪一类核心。

1.2 Cortex-A/R/M怎么选:先想清楚你写的是Linux还是裸机

ARM官方把处理器核心按应用场景分成三大类:Cortex-A(Application,应用处理器)、Cortex-R(Real-time,实时处理器)、Cortex-M(Microcontroller,微控制器)。

Cortex-A系列跑的是复杂应用,最典型的就是Linux、Android这类带MMU的操作系统。你需要跑Linux、跑Qt界面、跑复杂的协议栈,就选Cortex-A,比如Cortex-A53、A72、A78。Cortex-M系列面向裸机或RTOS,比如FreeRTOS、RT-Thread,特点是低功耗、中断响应快、没有MMU(或者说MMU能力非常有限),常见的有Cortex-M0/M3/M4/M7。Cortex-R系列介于两者之间,强调确定性和实时性,常用于汽车、工业控制领域,比如TC387这类芯片里就集成了多核的Cortex-R。

很多初学者拿到一块开发板,上来就想"用Linux",结果发现自己手里的板子是Cortex-M内核,这就非常尴尬。所以选型的第一步不是看芯片牌子,而是先明确自己的软件形态:跑不跑操作系统,跑什么操作系统,需要多大的内存和算力。这决定了你后面所有工具链的选型方向。

1.3 ARM的授权模式对开发者的真实影响

ARM和x86很大的不同在于它的商业模式:ARM本身不生产芯片,它把架构和核心设计授权给芯片厂商(比如高通、联发科、华为、NXP、ST等),厂商拿到授权后可以集成自己的外设和定制逻辑,然后流片生产。

这个模式对普通开发者有什么影响?第一个影响是生态碎片化。同样是ARMv8-A架构,不同厂的芯片外设寄存器完全不同,UART控制器、GPIO控制器、中断控制器(比如GIC)、时钟树,各写各的。你在A厂商的芯片上写的驱动,拿去B厂商的芯片上基本没法直接编译。第二个影响是工具链适配,虽然GCC对ARM的支持很成熟,但如果你要用到芯片厂商的特殊启动流程、固件打包或者调试工具,还得用原厂那一套。第三个影响是部分芯片还会提供自定义指令扩展,不过这类东西在通用工具链里经常不支持,需要专门打补丁,实际项目里我一般不碰,因为一旦用了,后续升级工具链就是灾难。

2. 交叉编译的本质:为什么x86主机非要另搞一套工具链

2.1 一次编译到处跑的谎言:目标架构才是编译的边界

很多从应用开发转过来的朋友一开始会问这样一个问题:我写的C代码不都是编译成二进制吗?凭什么x86上编出来的程序不能在ARM上跑?原因其实很简单:x86的CPU和ARM的CPU能识别的机器指令不是同一种语言。x86程序编译出来的是x86指令码,ARM处理器根本不认识;反过来也一样。

所谓交叉编译,就是在一种架构的主机上,编译出目标架构上可运行的二进制程序。这里"交叉"两个字指的是"编译工具运行的环境"和"生成程序运行的环境"不一致。如果两者一致,叫"本地编译"(native compile)。日常我们在x86 Linux上用gcc编译x86程序,就是本地编译。嵌入式开发里,目标板通常性能有限、存储有限,不太可能在板上直接跑编译器,所以几乎都是交叉编译。

要把这件事真正理解透,得知道一个关键概念:编译器本身是一个运行在宿主机的程序,但它生成的目标代码必须遵循目标平台的指令集和ABI规则。ABI(Application Binary Interface)比指令集还要细,它规定了函数调用时参数怎么传(寄存器还是栈)、寄存器哪些是调用者保存、结构体怎么布局、浮点数怎么传、动态链接器怎么找库。不同编译器、不同架构的ABI可能完全不同,这也是为什么交叉编译时工具链必须和目标系统精确匹配。

2.2 交叉编译工具链四元组:看懂前缀就懂了一半

用GCC的时候,你会看到工具链名字带一个前缀,比如arm-linux-gnueabihf-gcc。这个前缀不是随便写的,它其实是一个"四元组"的缩写,完整形式是:

arch-vendor-kernel-system
  • arch:目标架构,这里是arm
  • vendor:厂商或工具链来源,常见的有none(无厂商)、unknown、buildroot、linaro等
  • kernel:目标系统运行的操作系统或ABI类型,常见的是linux(Linux系统),也有eabi(嵌入式ABI,常用于裸机)
  • system:C库和浮点ABI,常见的是gnueabi(软浮点)、gnueabihf(硬浮点)、musleabi等

看一个具体前缀arm-linux-gnueabihf,可以读出三层含义:目标是ARM架构,跑的是Linux系统,用的是glibc+硬浮点ABI。至于前面的arm具体指ARMv7还是ARMv8,单纯从前缀还看不出来,需要用arm-linux-gnueabihf-gcc -v或者-dumpmachine查看工具链默认的配置。

理解这个前缀非常实用。你在网上搜"arm交叉编译工具链"会看到各种名字:arm-none-eabi-gcc、arm-none-linux-gnueabi-gcc、arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc。它们分别适用不同场景:

工具链前缀适用场景典型目标
arm-none-eabi-裸机、RTOS,无操作系统Cortex-M系列
arm-none-linux-gnueabi-ARM Linux,软浮点,ARMv5/ARMv7老平台、ARM926
arm-linux-gnueabihf-ARM Linux,硬浮点,ARMv7+Cortex-A7/A8/A9
aarch64-linux-gnu-64位ARM LinuxCortex-A53/A72等

我自己第一次看到这么多前缀的时候头都是大的,后来用了一招就记住了:先明确我的目标板是Cortex-A系列,跑Linux,要浮点性能,那就锁定arm-linux-gnueabihf这一条线。别贪多,先解决一个场景。

2.3 静态链接与动态链接:开发板上能不能跑起来的关键

交叉编译还有一个经常被忽略的问题:程序在目标板上跑不起来,很多情况下不是编译器的问题,而是动态链接器找不到依赖库。

动态链接的意思是,程序本身只记录了"我需要libc.so.6这个动态库",真正运行的时候由系统里的动态链接器(ld-linux-armhf.so.3这一类)去加载。在x86主机上,动态库默认在/lib/x86_64-linux-gnu/usr/lib/x86_64-linux-gnu等路径;而ARM的库路径完全不同,而且CPU架构也不一样,所以你编出来的ARM程序不能在x86主机上直接运行,也不能直接去加载x86的库。

解决这个问题有几个常用方法。最省事的是编译时加-static做静态链接,这样所有依赖都打进了可执行文件,不依赖目标板上的任何动态库。代价是体积变大,而且静态链接的glibc在某些环境里会有DNS解析、NSS(名称服务)之类的问题,需要留个心眼。更好的做法是准备好目标板的sysroot,也就是目标板的根文件系统的拷贝,编译时用--sysroot指向它,这样编译器、链接器能找到ARM版本的动态库和头文件。发行给客户时,把程序和相应的动态库一起部署,再用rpath或者启动脚本设置库路径。

3. 工具链选型与安装:gcc-arm、ARM Compiler 5.06 怎么选

3.1 三大工具链家族:GCC、ARM Compiler、Clang 各自的脾气

现在主流的ARM交叉编译工具链有三条线:

GCC线是最常见的,开源、免费、文档多,社区支持强。Linaro出品的gcc-arm工具链、Buildroot生成的工具链、各种芯片厂商定制过的工具链,本质上都是GCC的衍生版本。对绝大多数应用场景(跑Linux、写裸机程序),首选GCC就对了。

ARM Compiler线是ARM官方提供的商业编译器,最新版本有ARM Compiler 6(基于Clang/LLVM),老版本是ARM Compiler 5(armcc,基于原来的ARM自家编译器)。在KEIL MDK、ARM Development Studio(ADS)这些IDE里会用到。网上经常搜到的"ARM Compiler 5.06u7"指的是ARM Compiler 5.06 Update 7(build 960),是ARMv7时代非常经典的一个版本,很多老项目还在用它。要注意的是:ARM Compiler 5是单独的许可证软件,和GCC不是一回事,有些朋友下载后不知道怎么装,其实它会作为Keil的插件或者独立工具链安装,但默认的armcc是在Windows下用的居多,Linux下用得少。

Clang/LLVM线是后起之秀,Clang对ARM的支持已经很成熟,尤其在代码生成的性能上经常有惊喜。但它背后的库依赖比较多,普通嵌入式项目里如果不是脑壳疼的编译优化问题,没必要专门切到Clang。

选型的原则就一句话:能用GCC就用GCC,除非你的IDE强制要求ARM Compiler,或者项目里已经有历史包袱(比如那份老固件必须用armcc 5.06编译)。我用过的项目里,90%以上都是GCC一条路走到底。

3.2 Ubuntu 20.04 下安装 gcc-arm 工具链实录

这里以我自己在Ubuntu 20.04上搭建环境的完整过程为例。

方法一:直接用apt安装。Ubuntu的软件源里带了一套工具链,不过版本不一定新:

sudo apt update sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf

装完后验证:

arm-linux-gnueabihf-gcc --version

这个方案的好处是快,缺点是工具链版本由Ubuntu源决定,通常比较老。我这边Ubuntu 20.04源里的gcc-arm-linux-gnueabihf是9.x,对一般项目够用了,但如果你的目标板是新的Cortex-A55/A76这类核心,老工具链的默认调优参数可能不太理想,最好确认一下它默认的arch和cpu。

方法二:用ARM官方提供的最新工具链。ARM在官网上发布预编译的gcc-arm工具链(现在叫Arm GNU Toolchain),是一个独立的tar包,解压就能用。比如我下载过gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf,解压后直接添加bin目录到PATH即可:

wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.07/gcc-arm-none-eabi-10.3-2021.07-x86_64-linux.tar.bz2 tar xjf gcc-arm-10.3-2021.07-x86_64-linux.tar.bz2 export PATH=$PWD/gcc-arm-none-eabi-10.3-2021.07/bin:$PATH

上面这个例子是arm-none-eabi的包,适合裸机。如果你要Linux的包,就去Arm GNU Toolchain下载页面找带linux-gnueabihf名字的版本。下载的时候记得看清名字里是aarch64(64位ARM)还是arm(32位ARM),别下错了。我刚开始就下错过一次,拿着64位的工具链去编译32位目标,结果链接阶段各种cannot find crt1.o,还以为是环境问题,查了半天才发现是工具链架构选错了。

方法三:用Buildroot自己生成工具链。Buildroot可以精确控制工具链的glibc版本、GCC版本、内核头文件版本,一致性最好,但生成过程需要下载源码并编译,一般要半小时以上。适合做产品、需要长期维护的场景,学习阶段没必要上这么重的方案。

3.3 环境变量配置与工具链自检

装完工具链之后,我不会急着写代码,而是先做一轮"自检",确认工具链真能用、目标选项真正确。常用三条命令:

arm-linux-gnueabihf-gcc -v arm-linux-gnueabihf-gcc -dumpmachine arm-linux-gnueabihf-gcc -print-sysroot

第一条会打印完整配置信息,包括"Target:"那一行,以及"Thread model: posix"表示多线程支持。第二条直接输出工具链的目标机器名,看到arm-linux-gnueabihf就对了。第三条打印sysroot路径,如果工具链是在主机上交叉编译的,会显示一个实际存在的目录;如果没有sysroot或者显示空,那后面做动态链接时就要特别注意,说明这个工具链可能没有配套的根文件系统,你链接的目标库需要自己准备。

另外建议把工具链的bin目录写入~/.bashrc~/.zshrc,免得每次开终端都要重新export。但要注意:如果系统里同时装了多个工具链,把两条都写进PATH,同名命令会互相覆盖,实际生效的很可能是后写的那条。我自己的习惯是在每个项目目录下放一个env.sh,按项目来切换:

export PATH=/opt/arm-gnu-toolchain/bin:$PATH export CROSS_COMPILE=arm-linux-gnueabihf-

这样切换项目的时候source env.sh一下,就不会出现"唉我这个命令怎么是x86的gcc在跑"这种诡异问题。

4. 第一个ARM程序与实用调试手段

4.1 从hello.c到ARM可执行文件的完整过程

工具链装好后,最直接的事情就是编译一个程序扔到目标板或者模拟器里跑。过程其实和本地编译一模一样,只是把gcc换成交叉版本的gcc:

cat > hello.c <<EOF #include <stdio.h> int main(void) { printf("hello arm, day 17\n"); return 0; } EOF arm-linux-gnueabihf-gcc -o hello_arm hello.c

就这么一条命令,文件出来了。但二进制是不是ARM格式的?还不能光靠文件扩展名和文件大小判断,得用file命令看:

file hello_arm

正常输出应该是类似:

hello_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, not stripped

看到ARM, EABI5interpreter /lib/ld-linux-armhf.so.3,就说明它是一个32位ARM Linux动态链接的可执行文件。如果你是64位目标,用aarch64-linux-gnu-gcc编译,输出里应该是ELF 64-bit LSB executable, ARM aarch64,interpreter会是/lib/ld-linux-aarch64.so.1

如果要静态链接:

arm-linux-gnueabihf-gcc -static -o hello_arm_static hello.c

静态版本体积会大很多,一般都有几百KB甚至1MB以上,动态版本只有几KB。这也是判断一个程序静态还是动态最快的方法。

4.2 file和readelf:验证架构的黄金组合

光看file还不够,我一般还会用readelf深入看ELF文件的头部信息和依赖:

arm-linux-gnueabihf-readelf -h hello_arm arm-linux-gnueabihf-readelf -d hello_arm | grep NEEDED

第一条会输出ELF头,重点看Machine字段,ARM平台显示ARM,AArch64平台显示AArch64。第二条显示动态依赖库,能看到libc.so.6就说明它依赖C库。

这个习惯非常重要。因为很多时候你拿到一个第三方编译好的.so文件或可执行文件,不知道它是x86还是ARM,用filereadelf一照,立刻现原形。网上经常有人问".so从x86迁移arm文件",其实不叫迁移,正确的做法是拿到ARM平台下重新编译一份。如果你拿到一个二进制文件想确认它能不能在你的ARM板子上用,第一个动作就是file,这是最直接的判断方式。

4.3 用QEMU在x86上先跑起来

手头没有开发板的时候,怎么验证交叉编译的程序能跑?最方便的是QEMU的用户态模拟。Ubuntu下安装:

sudo apt install qemu-user qemu-user-static

装了qemu-user之后,就可以直接在x86主机上运行ARM的二进制:

qemu-arm -L /usr/arm-linux-gnueabihf ./hello_arm

这里的-L参数指定一个模拟的根文件系统路径,用来加载动态链接器和依赖库。如果你用的是apt安装的gcc-arm-linux-gnueabihf工具链,在Ubuntu里通常已经配好了/usr/arm-linux-gnueabihf这个sysroot,直接能用。如果报找不到ld-linux-armhf.so.3,说明-L路径不对,或者工具链没有配套的库目录。

QEMU用户态模拟跑单个程序非常快,但有一个限制:它不是完整的系统模拟,不能直接跑多进程依赖很重的服务(比如复杂的守护进程),也可能遇到一些系统调用不支持。这时候就得用qemu-system-arm完整模拟一台ARM机器,或者干脆上开发板真机。我在学习阶段的习惯是:小工具、单文件程序用qemu-arm跑;涉及设备访问、网络栈、多进程的,就放到板子上真跑。

5. Qt交叉编译环境搭建实战(含OpenSSL)

5.1 Qt交叉编译的核心参数

很多做嵌入式界面开发的朋友都会卡在Qt交叉编译这一步。网上搜"qt5.12.10交叉编译"、"ubuntu-20.04 安装 qt 交叉编译环境",答案很多,但大部分是照着抄的,没有解释参数的含义,遇到编译失败就不知道怎么调。这里讲一遍我的理解。

交叉编译Qt最核心的事情有两个:一是让Qt的configure脚本认出你的目标平台,二是指定一个安装前缀(prefix),让编译产物安装到你希望的目录。

以Qt 5.12.10为例,我的configure命令大概是:

./configure \ -prefix /opt/qt5.12.10-arm \ -opensource \ -confirm-license \ -release \ -xplatform linux-arm-gnueabihf-g++ \ -nomake examples \ -nomake tests \ -no-opengl \ -no-icu \ -qt-libpng \ -qt-libjpeg \ -qt-zlib \ -skip qtwayland \ -skip qtwebengine \ -skip qtscript \ -skip qt3d

解释几个关键参数:

  • -xplatform linux-arm-gnueabihf-g++:这个最核心,它告诉Qt用哪个mkspec。Qt在qtbase/mkspecs目录下预置了很多平台的规格文件。如果你的目标平台不在里面,需要自己复制一个类似的再改。比如复制linux-arm-gnueabi-g++改成linux-arm-gnueabihf-g++,然后修改里面的qmake.conf,指定交叉编译器前缀和合适的qmake配置。
  • -no-opengl:如果你的目标板GPU或者开发环境没配好OpenGL ES,先关掉,免得编译一大堆用不到的模块。等后面确实需要再开。
  • -no-icu:ICU库在嵌入式环境里比较重,而且交叉编译ICU特别麻烦。界面程序如果不需要复杂文本排版(比如中文显示Qt自身处理就够了),可以关掉。
  • -qt-libpng -qt-libjpeg -qt-zlib:让Qt使用自身附带的图片库和压缩库,而不是依赖系统里的版本。这样目标板上可以少装好多依赖。

Qt configure完之后会生成一个qtbase目录和一堆模块目录,然后make -j$(nproc),最后make install。整个编译过程在性能尚可的机器上大概要二十到四十分钟,取决于你配置的模块数量。编译期间占用磁盘空间比较大,我一般预留10GB以上空间,特别是debug版会更大。

5.2 OpenSSL与依赖库的交叉编译

如果Qt程序里要用到HTTPS访问,wave socket加密,那就躲不开OpenSSL。这里有个经典大坑:直接用系统里的OpenSSL库链接是不行的,因为x86平台的openssl库ARM平台用不了;但你也不能只交叉编译openssl,还需要让Qt能找到这个交叉编译版的openssl头文件和库。

交叉编译OpenSSL 1.1.1的典型步骤:

wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar xzf openssl-1.1.1w.tar.gz cd openssl-1.1.1w ./Configure \ linux-armv4 \ shared \ --prefix=/opt/openssl-arm \ --cross-compile-prefix=arm-linux-gnueabihf- make -j$(nproc) make install

关键在linux-armv4这个target,它告诉OpenSSL编译成ARMv4及以上版本的指令集(可以覆盖ARMv7、ARMv8的32位模式)。--cross-compile-prefix则是交叉编译器的前缀。编译完成后,动态库会安装到/opt/openssl-arm/lib,头文件在/opt/openssl-arm/include

然后回到Qt配置,加上:

-openssl-linked \ OPENSSL_LIBS="-L/opt/openssl-arm/lib -lssl -lcrypto" \ OPENSSL_INCDIR=/opt/openssl-arm/include

这里要注意一点:如果openssl编译时带shared,出来的动态库是libssl.so.1.1libcrypto.so.1.1这样的版本化文件,部署到目标板后,程序运行时通常还需要在板子的/etc/ld.so.conf里加上库路径,或者把库放到标准路径下,再执行ldconfig。否则就会在板子上报"libssl.so.1.1 not found"。我最初就是漏了这一步,Qt界面在开发机上看着好好的,拷到板子上直接启动失败,日志显示找不到libssl。

5.3 把程序部署到开发板的注意事项

交叉编译完Qt程序之后,还不能直接把可执行文件和Qt的so库一股脑全拷到板子上。一个能跑起来的Qt程序至少需要三样东西:

第一是Qt运行库。Qt安装到/opt之后,/opt/qt5.12.10-arm/lib下会有几十个动态库,但程序不一定每个都需要。最稳妥的做法是用readelf -d 你的程序 | grep NEEDED查看程序直接依赖的Qt库,然后再逐个检查这些Qt库又依赖什么。也可以用ldd的一个ARM版本来查,但注意x86的ldd无法直接分析ARM程序,需要用arm工具链提供的arm-linux-gnueabihf-ldd,或者用readelf手动追。

第二是平台插件。Qt程序启动的时候需要加载platforms/libqlinuxfb.so或者libqeglfs.so这些插件。很多人交叉编译成功、程序也拷到板子了,结果一跑起来报错"could not find or load the Qt platform plugin 'linuxfb'",就是因为没把qtbase/plugins/platforms目录拷过去,或者没有设置环境变量QT_QPA_PLATFORM_PLUGIN_PATH。我在板子上一般是这样启动程序的:

export QT_QPA_PLATFORM=linuxfb export QT_QPA_PLATFORM_PLUGIN_PATH=/opt/app/plugins/platforms export LD_LIBRARY_PATH=/opt/app/lib:$LD_LIBRARY_PATH ./myapp

第三是字库。Qt界面要显示中文,板上得有一个中文字体文件,常见的有wqy-zenhei.ttc或者DroidSansFallback.ttf,放到程序能找到的字体路径,否则界面上就是方块乱码。

6. 常见问题与排查技巧实录

6.1 经典报错速查表

我把自己这段时间反复踩过的坑整理成一张表,基本都是开发中最高频的:

报错现象原因解决方法
cannot find -lc工具链sysroot里没有对应的C库检查工具链是否有配套sysroot,或安装libc6-dev-armhf-cross
cannot find crt1.o缺少目标平台的启动文件多出现在用了错误架构工具链时,检查前缀是否正确
relocation truncated to fit程序太大地址放不下,或内存布局问题检查链接脚本/内存配置,必要时加-Wl,--no-relax或调整地址
undefined reference to \__aeabi_ddiv``浮点或除法辅助函数缺失链接时加-lm-lgcc,或修改fpu/fpabi参数
error: unrecognized command line option '-mfloat-abi=hard'编译器太老或目标架构不支持硬浮点检查目标是否支持硬浮点;换新版本工具链或去掉改选项
板子上运行报No such file or directory动态链接器路径不对,或文件格式不匹配file检查格式,readelf查interpreter,确保代码是ARM
板子上运行报libxxx.so.1: cannot open shared object file依赖的某个动态库没找到readelf查NEEDED,把对应的库放到LD_LIBRARY_PATH路径下
Qt程序报could not load the Qt platform pluginplatforms插件缺失或环境变量没设置拷贝插件目录,设置QT_QPA_PLATFORM_PLUGIN_PATH
编译很慢交叉编译工具链不支持多核,或文件系统IO慢make加-j$(nproc);用SSD;设CCACHE加速

这里特别想说一下浮点ABI的坑。在32位ARM Linux里,浮点参数传递有两种ABI:软浮点(softfp或者纯soft)和硬浮点(hard)。软浮点用通用寄存器传浮点参数,硬浮点用专用的FPU寄存器传。如果编译时用的选项和目标库/工具链用的选项不一致,链接阶段就会出现一堆奇怪的浮点相关报错。最典型的就是前面列的__aeabi_ddiv找不到,这通常是因为你的代码用了double除法,但浮点支持函数没链接进来,或者softfp与hard混用了。解决办法是确保整个工具链、所有库、所有编译参数里的浮点ABI选项保持一致。判断一个ELF文件用的是哪种浮点ABI,可以用readelf -A,看Tag_ABI_VFP_args的值,1表示硬浮点,0表示软浮点。

6.2 从x86迁移ARM的.so处理套路

实际项目里经常会遇到一个情况:供应商提供了一个预编译的.so,但它是x86的,需要在ARM的板子上用。我踩过几次坑之后总结出一个标准处理流程:

第一步,先判明架构。执行file libxxx.so,看清楚到底是x86、x86_64还是ARM、AArch64。如果是x86系列,很遗憾,一般不能直接在ARM上用,只能找供应商要ARM版,或者拿源码交叉编译。如果供应商说"这个库就是通用的",那基本可以认定对方在糊弄人。

第二步,查看依赖。执行readelf -d libxxx.so | grep NEEDED,看看它依赖哪些外部库。有些.so看起来是ARM的,但依赖了一个系统里不存在的库版本,一样跑不起来。

第三步,安装到目标板并测试。把.so放到板子的库目录,执行ldconfig,然后用ldd(板子上的本地ldd,或者用交叉工具链的arm-linux-gnueabihf-ldd)检查是否所有依赖都满足。注意,在x86开发机上执行x86版本的ldd看ARM库是没意义的,会提示"not a dynamic executable"或者干脆解析不了。

第四步,如果供应商只给了x86版又没有源码,那也不是完全没有解,可以考虑反编译转写或者改用其他方案,但成本都很高。所以我的原则是:所有依赖库从项目第一天就必须要求供应商提供ARM版本,写在需求文档里。这件事越早发现越省事,千万别等到联调阶段才暴露。

6.3 一个老工具链的提醒:ARM Compiler 5.06未安装的迷惑现场

最后专门聊聊很多人会遇到的"ARM Compiler 5.06"问题。这个编译器在MDK、老的开发环境里用得很多,网上有人求下载,有人装了又报"该版本未安装"之类的问题。实际上这是个非常典型的工具链版本识别问题。

ARM Compiler 5.06 Update 7(build 960)是一个比较老的版本,它和MDK的关系非常紧密。如果你在Keil MDK的安装界面里看到"ARM Compiler 5.06u7"选项,勾选并安装之后,编译器的位置一般在Keil的安装目录下的ARM/ARMCC/bin。如果你在命令行里输入armcc显示找不到命令,多半是因为没有把ARMCC/bin目录加入PATH,或者你下载的版本是一个需要单独安装的update包,但并没有正确挂载到MDK里。

建议:如果你真的是在MDK下做STM32这类开发,那ARM Compiler 5.06确实有必要;但如果你是在Ubuntu下做ARM Linux开发,那直接把自己装ARM Compiler的时间省下来,老老实实用GCC。别在一开始就被"arm compiler 5.06u7 download"这种热搜词带跑偏,选型之前先想清楚你的目标平台和开发模式,比什么都重要。

我个人在踩过一轮工具链的坑之后的体会是:交叉编译这件事,工具链选型占三成,理解目标架构占三成,剩下四成全在排错和依赖管理上。不要急着把代码写完编译,先花点时间把filereadelfldd这些基础命令用熟,把工具链前缀的含义搞清楚,遇到问题就从"架构对不对、ABI对不对、依赖全不全"三个角度去排查,你会发现很多看起来玄乎的问题,其实都是可以一步步定位到根因的。

这个“DAY17”的阶段总结就先写到这。接下来我打算把几个外设驱动的交叉编译流程也走一遍,特别是GPIO、SPI、I2C这类子系统怎么写驱动、怎么配合Qt应用做数据采集,到时候再开一篇继续分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询