ARTICLE DETAIL

资讯详情

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

ARM架构与交叉编译实战:从工具链选型到嵌入式部署

ARM架构与交叉编译实战:从工具链选型到嵌入式部署 你有没有遇过这种情况手边是一台 x86 架构的 Ubuntu 20.04 电脑开发板却是 ARM 的刚写好一个 C 程序想在板子上跑结果直接拿系统的 gcc 编了一下拷上去执行就报Exec format error。其实原因不复杂——你用的是 x86 的编译器编出来的二进制基于 x86 指令集ARM 处理器根本读不懂。这套东西行业里叫ARM 架构与交叉编译。所谓交叉编译就是在一台 CPU 架构与目标机器不同的主机上编译出目标机能够直接运行的二进制。ARM 架构则是目前嵌入式领域应用最广泛的芯片体系手机、路由器、电视盒子、智能门锁、各类开发板背后几乎都是它。这篇文章我会从 ARM 架构的基础差异讲起说清楚交叉编译的核心机制、工具链怎么选、参数怎么配然后带你从 hello world 一路走到 Qt 5.12.10、OpenSSL、strongswan 这些真实项目里高频出现的场景。适合刚入门嵌入式 Linux、想把程序从 x86 电脑搬到 ARM 开发板的同学也适合被工具链报错和链接库折腾过、想真正弄明白背后原理的人。1. 先搞明白ARM 架构与交叉编译到底解决什么问题1.1 ARM 架构不是一块芯片而是一整套体系很多人第一次接触 ARM 的时候会以为 ARM 就是某个具体的芯片型号比如“我手上这块板子是 ARM 的”。其实 ARM 是一家做 IP 授权的公司自己不直接生产芯片而是把指令集架构授权给高通、联发科、全志、瑞芯微、博通这些芯片厂商。所以你会看到同样是 ARM 平台树莓派、RK3399、STM32 的内部实现差异巨大但它们底层的指令集规范是同源的编译出来的机器码都遵循 ARM 定义的规则。从应用场景上ARM 家族一般分成三条线Cortex-A面向应用处理器运行 Linux、Android性能最强比如树莓派里的 SoC、手机主控。Cortex-R面向实时控制场景常用于汽车电子、工业控制。Cortex-M面向微控制器功耗和成本极低典型代表就是 STM32 单片机。从指令集维度看ARMv7 及之前的版本基本都是 32 位ARMv8 开始引入 64 位的 AArch64 执行状态。这个 32 位和 64 位的区别会直接决定你选哪一套交叉编译工具链。很多老开发板比如 NanoPi NEO、树莓派 2B默认跑的是 32 位 ARMv7 系统而 RK3399、树莓派 4B 的 64 位系统就是 AArch64。拿到板子第一步不是急着编译而是先确认它是什么架构、什么 ABI这一步错后面全错。1.2 为什么不能拿着 x86 的编译器直接编原因一句话CPU 不认识对方的机器码。x86 和 ARM 采用的是完全不同的指令集体系x86 是复杂指令集 CISC指令长度可变、指令功能复合ARM 是精简指令集 RISC指令定长、指令语义更简单。C/C 源码经过编译器处理后会先变成目标 ISA指令集架构的汇编指令再被汇编器转成对应的机器码。换一个 ISA目标 CPU 就完全没有办法解释执行这些机器码。那为什么不直接在开发板上编译呢两个原因。第一是性能问题嵌入式设备的 CPU 和大内存远不如 x86 工作站编译一个 Qt 或者 llama.cpp 这种体量的项目在板子上可能要跑一晚上主机上几分钟十几分钟就能完成。第二是很多嵌入式板子根本没装完整的编译工具链你连gcc都未必找得到。就算装了内存不够也会把编译直接 OOM 掉。所以交叉编译就成了嵌入式 Linux 开发的日常工作。你需要的其实是三样东西交叉编译器、交叉汇编器、交叉链接器以及目标机对应的头文件和库。它们组合在一起就是一套完整的交叉编译工具链。2. 交叉编译工具链选型、安装与核心参数2.1 工具链三段式命名规则看名字就知道能不能用交叉编译工具链的命名是有规则的基本上遵循“目标架构-目标系统-C库/ABI”这个三段式。以最常见的arm-linux-gnueabihf-gcc为例拆开看arm目标架构是 ARM一般在 32 位场景下出现。linux目标操作系统是 Linux。gnueabihfC 库用的是 glibc并且遵循 EABI嵌入式应用二进制接口hf代表 hard-float硬浮点。如果你看到的是aarch64-linux-gnu-gcc那就是 64 位 ARMv8 的工具链aarch64就是 AArch64 的代号。还有一类叫arm-none-eabi-gcc的none表示没有操作系统常用于裸机开发比如 STM32 这类 Cortex-M 单片机的编译和嵌入式 Linux 是不同的分支。这里有个非常容易踩的坑gnueabi和gnueabihf看着只差两个字母编出来的程序能不能在目标板上运行完全是两回事。下面这个表是常见的工具链选型参考工具链名称目标平台典型应用场景arm-linux-gnueabi-gcc32 位 ARM软浮点老 ARM 板、armel 系统、不带 VFP/NEON 的芯片arm-linux-gnueabihf-gcc32 位 ARM硬浮点树莓派 32 位官方系统、大部分主流嵌入式 Linux 开发板aarch64-linux-gnu-gcc64 位 ARMv8树莓派 64 位系统、RK3399/RK3588 等高性能平台arm-none-eabi-gcc无操作系统裸机STM32、Cortex-M 单片机开发选错了很容易出现两种情况要么编译时直接报浮点 ABI 不兼容要么编出来的程序拷到板子上跑起来就Illegal instruction崩溃。后面第 5 章我详细说怎么排查。2.2 Ubuntu 上一条命令装好工具链但版本坑要注意Debian/Ubuntu 的软件仓库里直接有打包好的交叉编译工具链不用自己跑去官网下载。我用的最多的是这两个命令# 32 位 ARMv7 硬浮点工具链 sudo apt install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf binutils-arm-linux-gnueabihf # 64 位 ARMv8 工具链 sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu binutils-aarch64-linux-gnu装完之后可以验证一下arm-linux-gnueabihf-gcc --version which arm-linux-gnueabihf-gccUbuntu 20.04、22.04、24.04 都能直接装但你得留个心眼不同 Ubuntu 版本自带的交叉编译器版本不一样比如 20.04 默认是 GCC 924.04 可能已经是 GCC 13。编译器版本差异带来的最大问题不是语法而是生成的二进制依赖的 glibc 版本。如果你在 Ubuntu 24.04 上用aarch64-linux-gnu-gcc编出来的程序拿到一个 glibc 版本较老的板子系统上运行会报类似version GLIBC_2.34 not found的错误。这种情况要么升级板子的系统要么换老版本的工具链要么直接把程序静态编译把 libc 一起打包进二进制。2.3 编译参数背后的意义编出来但跑不起来的最常见原因交叉编译的几个关键参数很多人知道要加但完全不清楚为什么。这里按重要性排序讲一下。-march指定目标 CPU 的架构级别。比如-marcharmv7-a表示针对 ARMv7-A 架构优化。不指定的时候编译器会用一个比较保守的默认值但如果板子的 CPU 支持新特性正确指定能让性能提升不少。-mfpu指定浮点运算单元。ARM 平台的浮点运算可以走 VFP、NEON 或纯软件模拟像树莓派等多数板子都带 NEON配置参数时可以写-mfpuneon。如果芯片压根不支持 NEON你却编译了带 NEON 指令的二进制运行时会直接崩溃。-mfloat-abi指定浮点 ABI 模式可选soft、softfp、hard。这个参数必须和工具链以及目标系统一致。硬浮点模式下浮点参数直接用 VFP/NEON 寄存器传递速度快软浮点模式下浮点运算交给通用寄存器模拟兼容性好但性能差。如果目标系统是 armhf 硬浮点你用软浮点工具链编出来的程序链接动态库时会报浮点 ABI 不匹配。除了这三个还有两个工程里绕不开的--sysroot和-Wl,-rpath。--sysroot的作用是告诉编译器“目标机的根文件系统在哪个目录”编译器会到sysroot/usr/include找头文件、到sysroot/usr/lib找库而不是去宿主机的/usr目录翻 x86 的库。-Wl,-rpath则是把动态库搜索路径写进编译后的二进制里这样运行时不设LD_LIBRARY_PATH也能找到库。这两个参数是解决交叉编译“编过了但跑不起来”问题的核心后面实操章节我会带你看具体效果。3. 实操从 hello world 到工程级交叉编译3.1 第一步编一个能在 ARM 板子上跑的 hello world先说最简单的场景。写一个 hello.c#include stdio.h int main() { printf(hello arm\n); return 0; }要编出 ARM 版本直接用交叉编译器编译注意这里建议先加-static做静态链接arm-linux-gnueabihf-gcc -static -o hello hello.c编完用file命令看一眼产物格式file hello正常情况下会看到类似这样的输出ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped看到ARM和statically linked就说明编译目标是正确的。这时候把 hello 拷到开发板上chmod x hello后直接执行就能打印出hello arm。这里我强烈建议第一次交叉编译时先试静态链接。因为静态链接把所有依赖的 C 库代码都打进了二进制不依赖板子上的 glibc 版本可以排除掉很多环境问题。不过要心里有数静态编译的 hello world 大概 700KB 左右比动态版大得多而且如果工程里用到 NSS 这类需要动态加载模块的库静态链接会产生各种奇怪问题。所以静态版只是用来验证工具链是否可用真实项目大多还是动态链接。再看动态编译的情况arm-linux-gnueabihf-gcc -o hello_dyn hello.c file hello_dyn readelf -d hello_dyn你会看到动态版依赖libc.so.6而且程序解释器的路径通常指向/lib/ld-linux-armhf.so.3。如果目标板系统的动态链接器不在这个路径运行就会报No such file or directory这个坑我后面专门讲。3.2 带第三方库的工程-I、-L和 sysroot 怎么配合实际项目里很少只依赖 libc可能还要链接 libcurl、libssl、libsqlite3 之类的东西。很多人第一反应是像本机编译一样用-L/usr/lib指定库路径结果编译器把 x86 的库拿过来了链接报错或者运行崩溃非常难受。交叉编译时头文件和库必须来自目标机的 sysroot。正确做法是这样的从开发板上把根文件系统整体拷到一个本地目录比如/opt/arm-sysroot。rsync -avz root板子IP:/ /opt/arm-sysroot/注意这一步可能要排除/proc、/sys、/dev这些虚拟文件系统不然拷出来的 sysroot 会很脏。编译时指定--sysrootarm-linux-gnueabihf-gcc --sysroot/opt/arm-sysroot -o app app.c -lcurl编译器拿到--sysroot/opt/arm-sysroot之后头文件会自动去/opt/arm-sysroot/usr/include找库会自动去/opt/arm-sysroot/usr/lib找。这比手动-I、-L可靠得多因为你手动指定的路径很容易混入宿主机上的 x86 库而 sysroot 是个封闭目录里面全是目标板的东西从源头上杜绝了架构错配。如果项目用 Makefile推荐把工具链和参数统一写在变量里CROSS_COMPILE arm-linux-gnueabihf- CC $(CROSS_COMPILE)gcc SYSROOT /opt/arm-sysroot CFLAGS --sysroot$(SYSROOT) -marcharmv7-a -mfpuneon -mfloat-abihard LDFLAGS --sysroot$(SYSROOT) -Wl,-rpath,/usr/local/lib app: app.c $(CC) $(CFLAGS) -o $ $^ $(LDFLAGS) -lcurl3.3 工程化方案CMake 交叉编译文件怎么写现在很多项目用 CMake交叉编译时不需要改源码只需要提供一个 toolchain 文件。我常用的写法是这样文件名叫toolchain-armhf.cmakeset(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g) set(CMAKE_SYSROOT /opt/arm-sysroot) set(CMAKE_FIND_ROOT_PATH /opt/arm-sysroot) 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 -DCMAKE_TOOLCHAIN_FILEtoolchain-armhf.cmake .. make -j$(nproc)这里有几个地方要解释清楚。CMAKE_SYSTEM_NAME设为 Linux 就是因为 CMake 检测到目标系统不是宿主机系统才会启用交叉编译模式。CMAKE_FIND_ROOT_PATH告诉 CMake 去哪儿找目标机的库和头文件。CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER表示查找程序时还是用宿主机的工具比如查找sed、awk这些辅助工具LIBRARY和INCLUDE设为ONLY表示找库和头文件时只能在 sysroot 目录里找绝不能跑去宿主机/usr/lib/x86_64-linux-gnu翻这是避免交叉编译工程“串味”的关键。实际踩过的坑是如果漏掉CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLYCMake 很可能在宿主机上找到了某个 x86 库连接阶段报一堆skipping incompatible ...的警告看起来像没装库实际是架构不匹配。4. 高频实战场景Qt、OpenSSL、strongswan 的交叉编译4.1 Qt 5.12.10 交叉编译环境搭建要点Qt 是嵌入式 Linux 图形界面开发绕不开的框架也是交叉编译里比较痛苦的环节。热搜里看到很多人搜ubuntu-20.04 安装 qt 交叉编译环境我在 Ubuntu 20.04 上配过 Qt 5.12.10这里把核心流程和坑位说一下。首先准备三样东西Qt 源码包qt-everywhere-src-5.12.10.tar.xz目标板 sysroot至少要包含完整的/usr和/lib交叉工具链前面装好的arm-linux-gnueabihf就行解压后进入源码目录执行 configure./configure -prefix /opt/qt5.12.10-arm \ -xplatform linux-arm-gnueabihf-g \ -sysroot /opt/arm-sysroot \ -opensource -confirm-license \ -no-opengl -nomake examples -nomake tests解释下几个关键参数。-prefix指定编译产物的安装目录之后用来编应用的 qmake 就在这个目录里。-xplatform是 Qt 专门为交叉编译设计的平台参数必须写成linux-arm-gnueabihf-g如果写成宿主机平台整个编译过程都会用宿主的 g编出来的 Qt 库在 ARM 板上一跑就崩。-sysroot指定目标机根文件系统。-no-opengl是为了省事如果板子 GPU 支持 ES2可以用-opengl es2但服务器上调试环境一般先关掉。配置通过之后编译安装make -j4 make install这一步非常耗时我实测 Qt 5.12.10 完整编译在 8 核机器上也要一个小时以上内存建议至少 8G。编译过程中如果报缺少zlib、libstdc之类的头文件多半是 sysroot 里没有对应的开发包需要在板子上apt install对应-dev包后再重新同步 sysroot。应用层编译时千万不要用系统自带的 qmake 去编 ARM 应用要用刚编译出来的/opt/qt5.12.10-arm/bin/qmake否则链接的全是 x86 的 Qt 库报错千奇百怪。把编译出来的整个 Qt 库目录拷到板子上运行前设置LD_LIBRARY_PATH/opt/qt5.12.10-arm/lib这个环境就彻底配通了。4.2 OpenSSL 与 strongswanautoconf/Configure 类项目的交叉编译OpenSSL 和 strongswan 这类老牌开源项目交叉编译的套路稍微不一样。Qt 用的是自己那套 xplatform 机制而 OpenSSL 用的是Configure脚本strongswan 用的是 autoconf 体系。但它们底层逻辑一样告诉构建系统目标平台是谁。OpenSSL 的交叉编译命令大致是这样的./Configure linux-armv4 \ -marcharmv7-a -mfpuneon -mfloat-abihard \ --cross-compile-prefixarm-linux-gnueabihf- \ shared no-asmlinux-armv4是 OpenSSL 自带的平台名表示 32 位 ARM 架构虽然写了 armv4但实际支持到 ARMv7。--cross-compile-prefix指定交叉工具链前缀OpenSSL 会自动在后面补全gcc、ld、ar等命令。no-asm表示关闭汇编优化如果你没有针对目标芯片仔细核对过汇编代码建议先关掉避免编出跑不了的二进制。strongswan 的交叉编译用 autoconf 的套路./configure \ --hostarm-linux-gnueabihf \ --prefix/usr/local \ CCarm-linux-gnueabihf-gcc \ CFLAGS-marcharmv7-a -mfpuneon -mfloat-abihard --sysroot/opt/arm-sysroot这里--host是最关键的参数。autoconf 体系里--build表示编译机--host表示目标机交叉编译的本质就是让--host跟目标板一致、--build保持为宿主机。很多人设了--buildarmxxx反而编出幺蛾子就是没搞明白这两个参数的分工。如果目标机系统是 64 位--hostaarch64-linux-gnu即可。4.3 顺带聊聊 llama.cpp 在 ARM 设备上的编译llama.cpp 这两年很火不少人想把它弄到 ARM 开发板上跑本地大模型。它本身就是为本地推理设计的支持 ARM 上的 NEON 优化但如果直接在本机用 x86 编译器编了再拷到 ARM 板子照样跑不起来还是绕不开交叉编译。llama.cpp 用 CMake 构建理论上一个 toolchain 文件就能解决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 /opt/arm64-sysroot) 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 .. -DCMAKE_TOOLCHAIN_FILEtoolchain-aarch64.cmake -DGGML_NATIVEOFFGGML_NATIVE不开的话构建系统会针对编译机做-marchnative优化生成 x86 特有指令拷到 ARM 上直接非法指令崩溃。还有一个经验是如果目标是 32 位 ARMv7llama.cpp 编译需要确认芯片是否支持 NEON老一点的 Cortex-A7 跑起来性能会很挣扎AArch64 就舒服很多现代 64 位 ARM 芯片基本都有完整的 NEON 支持。5. 常见问题与排查技巧实录5.1 三个高频编译报错与排查思路交叉编译遇到报错很多新手第一反应是去搜索引擎复制粘贴但效率其实很低。我习惯按照下面的顺序排查命中率很高报错特征可能原因排查命令cannot find -lXXX没有该库或-L指定的路径不对find /opt/arm-sysroot -name libXXX*fatal error: XXX.h: No such file or directory头文件路径指向宿主机或 sysroot 缺开发包检查--sysroot、-I确认 sysroot 里有对应头文件拷贝到板子后报No such file or directory动态链接器路径不存在或二进制和系统架构不符file 二进制、readelf -l 二进制查看 INTERP第一类报错大多是库没装全去 sysroot 里找一下库文件是否存在。特别提醒有些库只装到了宿主机没进 sysroot这时候要在板子上装好对应包再同步一次 sysroot。第二类多半是头文件找错了地方检查编译命令里有没有手动加-I/usr/include这类路径如果有果断删掉让编译器只从 sysroot 里找。第三类最迷惑明明文件存在却报No such file or directory其实这不是文件不存在而是 ELF 里指定的解释器ld-linux路径不存在。用readelf -l一看便知。5.2 armel 与 armhf、硬浮点和软浮点混用的后果很严重交叉编译最隐蔽的问题不是编译不过而是编译过了、运行崩溃。最典型的就是 armel 和 armhf 混用。armel 对应软浮点 EABIarmhf 对应硬浮点 EABI这两个 ABI 浮点参数的传递方式完全不一样。如果用 32 位软浮点工具链arm-linux-gnueabi-gcc编出的程序放到 armhf 硬浮点系统上运行动态库加载阶段就会报浮点 ABI 不匹配因为板子上的 libc.so.6 是硬浮点编译的期望浮点参数从 VFP 寄存器传入而你的程序却用通用寄存器传参。反过来用硬浮点工具链编程序放软浮点系统问题更严重可能在初始化阶段就非法指令崩溃。确认一个交叉编译产物是否硬浮点可以这么做arm-linux-gnueabihf-readelf -A hello | grep Tag_ABI_VFP_args如果输出Tag_ABI_VFP_args: VFP registers说明是硬浮点如果没有这行或写着Tag_ABI_VFP_args: (none)就是软浮点。另外还要注意在 64 位处理器上运行 32 位 ARM 程序需要内核开启CONFIG_COMPAT兼容选项有些精简内核没开32 位程序直接报Exec format error。5.3 动态库运行时找不到不一定要改代码程序编译成功后在板子上运行时提示error while loading shared libraries: libxxx.so.1: cannot open shared object file这问题很常见。首先确认库文件有没有拷到板子上其次看搜索路径。最粗暴的方式是设置环境变量export LD_LIBRARY_PATH/opt/mylibs:$LD_LIBRARY_PATH ./app但如果每次启动都要手动 export不优雅。更好的办法是在链接阶段把路径写进二进制这样运行时不依赖系统环境arm-linux-gnueabihf-gcc -Wl,-rpath,/opt/mylibs -o app app.c -lxxx-Wl表示把后续参数传给链接器-rpath就是运行时库搜索路径。这里有个值得注意的陷阱-rpath指定的路径会原样写进二进制如果路径相对于板子是/opt/mylibs那板子上的库就也得放在这个目录不能随意改。如果想让路径相对可执行文件位置做偏移也可以考虑$ORIGIN变量但对于嵌入式场景固定路径往往更直观。还有一类情况程序编译时链接的是静态库但运行时报找不到动态库这通常是库依赖关系没理清。比如-lssl实际还会依赖libcrypto.so你只拷了 libssl 没拷 libcrypto就会有这种“明明编过了运行时却报缺库”的现象。排查时可以现场执行ldd app看依赖列表对比板子/usr/lib下的文件。5.4 我的一个避坑习惯最后分享一个我自己踩了不少次坑之后养成的习惯拿到一块新 ARM 板子第一件事不是跑程序而是先搞清楚它到底用的是什么系统、什么 ABI、是 32 位还是 64 位。在板子上执行uname -a看内核架构执行file /bin/ls看 rootfs 的二进制格式执行ldd --version看 glibc 版本。把这三样确认清楚了再去选工具链、配 sysroot交叉编译的成功率会提升一个量级。我现在每次交叉编译完都会顺手对生成的二进制做三件事file看架构readelf -A看 ABI 和浮点标识readelf -d看动态依赖和解释器路径。这三条命令加起来不到十秒但能帮我避免无数个“明明编译不过其实是参数不对”的无效调试。希望这篇文章能让你少走一点弯路把精力花在真正有意思的业务代码上。
返回列表