
1. 开篇ARM架构与交叉编译到底解决什么问题今天记录的是ARM架构与交叉编译。这两件事几乎是嵌入式开发和国产化适配绕不过去的一关你在自己的x86笔记本上写完代码最终要跑到ARM架构的板子、盒子或者服务器上怎么办交叉编译就是标准答案。简单说交叉编译是在一台x86的Ubuntu机器上用面向ARM平台编译工具链生成能在ARM CPU上直接运行的可执行文件再通过网络或U盘拷到目标设备上运行。相比在板子上直接装GCC交叉编译不占用目标设备资源、速度更快还能轻松接入CI流水线做到每次提交代码都自动编译出可交付的固件包。这篇内容适合刚接触ARM生态的新手也适合需要在飞腾、瑞芯微这类国产平台上做应用适配的同学参考。我尽量把原理、工具链选型、实际编译步骤和坑都讲透文章最后还会给一份排查速查表让你遇到问题能快速定位。1.1 从x86到ARM差的不只是CPU型号很多人觉得ARM和x86就是“指令集不同”这个说法对但不够具体。ARM用的是RISC精简指令集x86是CISC复杂指令集两者最直观的差异体现在三处第一指令长度和风格。ARM的指令定长、规整很多指令是单周期的这让它在功耗受限的场景下表现很好x86的指令变长、寻址方式丰富单条指令能做很多事但解码电路复杂、功耗更高。第二生态位不同。x86统治PC和服务器ARM统治手机、平板、路由器、智能家电现在ARM服务器也在增长。你手里的路由器、智能音箱、车载中控绝大多数都是ARM内核。第三软件交付方式不一样。x86上常见的做法是直接在目标机器上编译但如果目标是ARM你会发现很多第三方库根本没有现成的ARM版二进制包或者版本不对、动态库依赖缺一堆。这时候交叉编译就成了必须掌握的技能。我最初接触ARM是因为要给一块RK3568的开发板编译应用。当时公司没有ARM服务器也没法在板子上跑完整的编译任务内存才2GB唯一可行的方案就是在x86工作站上配置交叉编译环境批量产出ARM可执行文件后推送到板子测试。1.2 哪些场景离不开交叉编译我梳理了一下日常工作中至少有四类场景绕不开嵌入式开发树莓派、全志、瑞芯微、飞腾等平台应用、驱动、系统镜像都是交叉编译出来的。国产化适配在飞腾ARM指令集兼容或鲲鹏服务器上跑业务定制glibc版本、依赖库基本都走交叉编译。边缘AI部署像SenseVoice-Small这种语音模型要在ARM CPU上做推理光靠Python环境往往不够需要交叉编译ONNX Runtime、FFmpeg、Kaldi相关组件。CI/CD流水线产品需要高频出包时不可能每台构建机都放一块ARM开发板主流做法是x86构建机上挂一套交叉编译工具链和sysroot统一批量构建。上面这些场景里交叉编译不只是“能跑”而是“高效、可复制、可控”。你只要配置好一次环境后续版本的编译流程完全一致出包质量也能稳定跟踪。1.3 这篇文章适合谁如果你是刚接触ARM平台的学生、从纯Web转嵌入式开发的开发者、或者正在做信创适配的系统工程师这篇文章应该能帮到你。前两节讲原理和选型中间两节是可直接抄作业的实操最后一节整理了我在多个项目里踩过的坑和排查方法。我不会把内容写得像官方文档那样干巴巴。所有命令、参数、路径都是从真实项目里提炼出来的你照着做基本能跑通。我也会在关键位置补一句为什么这么做帮你理解背后的逻辑。2. 交叉编译的原理与工具链选型这块是很多教程跳过但实际很关键的部分。你不知道工具链是怎么组织的出了问题就只能瞎试。2.1 一次编译到底经历了什么先回顾一下普通编译的四步预处理展开宏和头文件、编译生成汇编、汇编生成目标文件、链接把目标文件和库合并成可执行文件。交叉编译和本机编译的区别只在“编译”和“链接”这两步用了不同的工具。本机编译用的gcc、ld、ar都是针对x86的而交叉编译需要用一套针对ARM的工具链比如aarch64-linux-gnu-gcc、aarch64-linux-gnu-ld、aarch64-linux-gnu-ar。还有一个概念叫sysroot你可以把它理解成一个袖珍版的ARM根文件系统。这里面有ARM平台的头文件/usr/include和库文件/usr/lib编译时工具链会优先到这里找头文件和动态库而不是到主机自带的/usr/include里找。sysroot版本必须和目标板系统的glibc版本匹配否则编译出来的程序一运行就报GLIBC版本不兼容。提示交叉编译工具链本身是一整套工具不仅包含gcc还包含binutilsld、as、objcopy、readelf、file等和标准C库glibc或uClibc挑选时不要只看编译器版本还要看配套的glibc版本。2.2 工具链前缀aarch64与armv7的区分很多新手第一个困惑就是同样是ARM为什么有的工具链叫aarch64-linux-gnu-gcc有的叫arm-linux-gnueabihf-gcc这里的关键是目标架构和浮点接口。aarch64-linux-gnu-64位ARMv8-A架构对应A53、A72、A55等核心也叫ARMv8。arm-linux-gnueabihf-32位ARMv7架构带硬浮点hf对应Cortex-A7、A9、A15。arm-linux-gnueabi-32位ARMv7软浮点适合没有VFP/NEON单元的老平台。选择时先确认目标系统的用户态位数如果你是64位系统用aarch64如果是32位用户态用arm-linux-gnueabihf。现在绝大部分开发板都是64位系统但很多老产品还在用32位用户态这点最容易看错。看系统的命令是uname -a里面有aarch64或armv7l字样。浮点接口也很关键。硬浮点工具链编译出的程序传参时直接用浮点寄存器VFP/NEON速度更快软浮点则用通用寄存器模拟浮点运算。如果目标板内核没启用NEON或者库是用软浮点编的硬浮点程序跑起来会报Illegal instruction或者链接报错。2.3 主流工具链怎么选我用过几个方案各有特点工具链方案适用场景优点注意事项发行版自带交叉工具链gcc-aarch64-linux-gnu快速验证、学习安装方便apt install就能用版本可能较旧sysroot不完整Linaro GNU Toolchain多版本选择较新覆盖面广支持armv7/armv8需要手动下载解压环境变量配置稍麻烦ARM官方arm-gnu-toolchain最贴近ARM官方版本新、工具完整下载需要ARM账号部分老项目不兼容Yocto/OpenEmbedded生成的工具链完整嵌入式Linux开发sysroot和镜像完全一致构建可复现配置复杂时间长适合企业级项目如果你只是想跑通一个Hello World用发行版自带的工具链就够了如果是要给客户交付产品我建议直接上Linaro或者ARM官方工具链再搭配一个从板子上备份出来的sysroot。2.4 手动安装Linaro工具链Linaro工具链的下载页面通常会提供最新版本的多个包下载时注意区分x86_64宿主和aarch64目标的组合。以Linaro GNU Toolchain为例下载解压后你会看到bin目录里有aarch64-linux-gnu-gcc这样带前缀的可执行文件。我习惯把它们放到/opt/toolchains目录下然后在~/.bashrc里加PATHexport PATH/opt/toolchains/gcc-arm-10.3-x86_64-aarch64-none-linux-gnu/bin:$PATH export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm64这里设置了三个东西PATH用于直接调用工具链命令CROSS_COMPILE和ARCH是给Linux内核和很多Makefile工程用的约定变量内核构建时自动拼接前缀去调用aarch64-linux-gnu-gcc。验证是否安装成功aarch64-linux-gnu-gcc --version能正常输出版本信息说明工具链可以被系统找到。注意Linaro工具链的默认sysroot在解压目录里的aarch64-none-linux-gnu/libc目录下编译时可以用--sysroot参数指定。3. 从零跑通一个ARM交叉编译实例这节我会完整走一遍在x86 Ubuntu下交叉编译C程序的过程包括动态库依赖分析。你最好能跟着敲一遍。3.1 准备一台Ubuntu环境我用的是Ubuntu 22.04理论上20.04及以上的版本都没问题。先安装基础工具链sudo apt update sudo apt install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ g-aarch64-linux-gnu build-essential file安装完成后检查一下aarch64-linux-gnu-gcc --version如果输出版本说明环境可用。这些包来自Ubuntu官方源优点是简单缺点是工具链版本可能比较旧编译复杂项目时可能遇到库不兼容问题但学习足够。3.2 编译第一个ARM程序写一个最简单的C文件命名为hello_arm.c#include stdio.h int main(void) { printf(Hello ARM, compiled on x86.\n); return 0; }编译命令aarch64-linux-gnu-gcc -o hello_arm hello_arm.c这一步会做完整的编译和链接。如果报找不到stdio.h多半是sysroot路径没配置好后面会讲排查方法。编译完成后用file命令看产物file hello_arm正常输出类似hello_arm: 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和dynamically linked说明这确实是一个ARM 64位的动态链接可执行文件。3.3 拷贝到目标板运行将hello_arm通过scp拷贝到开发板scp hello_arm user192.168.1.100:/home/user/然后在开发板上加执行权限并运行chmod x /home/user/hello_arm /home/user/hello_arm这里有一个非常常见的坑如果目标板系统的glibc版本比工具链的glibc版本低运行时会报/lib/aarch64-linux-gnu/libc.so.6: version GLIBC_2.34 not found解决方案有两种换用与目标板系统版本匹配的旧工具链。改成静态编译把C库打进可执行文件里aarch64-linux-gnu-gcc -static -o hello_arm_static hello_arm.c静态编译的产物体积会增大但基本不存在glibc版本问题对部署在“纯净板子”上的工具类程序很实用。3.4 动态库依赖检查当你写稍微复杂的程序用到第三方库时光有编译成功还不够还得保证目标板上有对应的动态库。在x86宿主机上看不了ARM程序的ldd因为ldd默认调用的是宿主动态加载器。用工具链自带的readelf来读取依赖aarch64-linux-gnu-readelf -d hello_arm | grep NEEDED输出会列出程序依赖的.so文件比如0x0000000000000001 (NEEDED) Shared library: [libc.so.6]如果依赖了一个非标准路径的库比如libssl.so.1.1你需要确认目标板上是否有这个库或者把库一并拷贝到板子的/usr/lib目录。我一般会在sysroot里对应路径下准备一份目标板库的备份确保编译和运行时依赖一致。3.5 用CMake管理交叉编译工程当项目文件多起来直接敲gcc命令不现实。CMake是嵌入式项目最常见的构建工具交叉编译需要写一个toolchain文件。创建一个文件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指向工具链的sysrootCMAKE_FIND_ROOT_PATH_MODE_PROGRAM设为NEVER是为了让CMake在查找可执行程序时仍然使用宿主机工具而后两个ONLY表示查找库和头文件时只在sysroot里找。构建时运行mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../aarch64-toolchain.cmake .. make用CMake管理的好处是第三方库的查找、编译选项的传递都规范化了后续接CI也很方便。4. 进阶实战QT交叉编译与ARM端推理部署简单程序跑通后再上两个实际场景一个是GUI应用开发常用的QT交叉编译一个是ARM CPU上的AI语音模型部署。这两个场景也是最近搜索热词里最集中的方向。4.1 QT 5.12.10交叉编译以RK3576为例RK3576是瑞芯微推出的一款ARM平台SoC跑Linux系统很多工业HMI项目用它配QT做界面。QT交叉编译的难点在于QT源码的配置选项非常多且要处理好依赖库。流程大致是在x86 Ubuntu上安装交叉编译基础工具同时确认目标板的Qt运行库版本。下载Qt源码比如qt-everywhere-opensource-src-5.12.10.tar.xz解压。编译前先编译tslib触摸屏依赖因为QT的plugins/platforms下的libqlinuxfb.so和libqtslibplugin.so需要它。配置QT./configure -prefix /opt/qt5.12.10-arm \ -opensource -confirm-license \ -xplatform linux-arm-gnueabi-g \ -device linux-raspberrypi3-g \ -nomake examples \ -nomake tests \ -no-opengl这里-xplatform指定的是交叉编译平台配置文件-device指定目标设备的qmake配置。不同开发板的device配置可能不同比如RK3576有时用linux-rk3576-g需要你从qtbase/mkspecs/devices目录里找。编译安装make -j$(nproc) make install编写目标板的qmake交叉编译配置把编译器指向aarch64-linux-gnu-g把路径指向安装好的arm版Qt库。我踩过的坑有两个一是设备配置文件里默认的编译器路径是arm-linux-gnueabihf-g但我的目标板是64位必须改成aarch64-linux-gnu-g二是--prefix路径如果不写QT默认会装到主机/usr/local下污染本机环境。后来我在所有QT交叉编译工程里都固定用/opt/qt5.12.10-arm这类独立前缀目录再通过环境变量QTDIR指向它整个环境就清晰多了。如果你不想从源码编译也可以直接用Linaro工具链加上RK官方发布的交叉编译SDK里面通常已经把QT依赖的库链好配置好环境变量就能直接qmake。4.2 SenseVoice-Small这类模型在ARM CPU上的部署思路最近很多人在搜“sensevoice-small arm架构cpu部署”这类语音识别模型是典型的边缘AI场景。在ARM CPU上部署和你在x86上跑Python Demo差别很大。我的做法是分四步第一步确认目标平台算力和系统环境。Arm CPU有没有NEON指令、内存多大、系统是否支持docker。如果目标板性能弱优先用C运行时而不是Python。第二步交叉编译推理运行时。用ONNX Runtime为例git clone https://github.com/microsoft/onnxruntime.git cd onnxruntime ./build.sh --config Release \ --build_dir build-arm \ --arm64 \ --cmake_extra_defines CMAKE_TOOLCHAIN_FILE/path/to/aarch64-toolchain.cmake第三步把编译出来的libonnxruntime.so和模型文件、音频处理库一起拷贝到目标板。第四步在目标板上写一个C推理代码完成前处理、推理、后处理最后编译链接时指定交叉工具链。注意交叉编译AI框架时像Protobuf、OpenBLAS这类依赖库也需要用同一工具链编译不能从x86宿主上直接拖.so过去否则会报架构不匹配或者符号缺失。这部分工作量往往比模型本身还大。如果只是快速验证也可以用Arm提供的Python解释器和numpy的ARM版本但性能和部署体验远不如C方案。4.3 飞腾平台与ARM版CentOS适配飞腾CPU兼容ARMv8指令集很多信创项目会直接在上面跑银河麒麟、统信UOS或者ARM版CentOS 7。如果你需要在“目标机上编译”还好如果必须交叉编译要注意三点镜像选择CentOS 7官方镜像里没有ARM版本需要找第三方或厂商提供的ARM架构镜像否则装都装不上。glibc版本CentOS 7默认glibc是2.17很多新工具链编译出的程序依赖GLIBC_2.18以上运行时直接报错。最稳妥的方式是用aarch64-centos7兼容的sysroot编译或者干脆静态编译。第三方库来源EPEL源对aarch64的支持还算可以但有些库仍然没有aarch64包需自行交叉编译且版本尽量和线上环境保持一致。我有一次给飞腾FT-2000/4的机器编译一个网络监控程序工具链用的是Linaro 10.3本地验证一切正常拷过去后报缺libcrypto.so.10。排查后发现是程序依赖OpenSSL 1.0而工具链sysroot里只有OpenSSL 1.1。后来从板子上提取了整个/usr/lib64下的ARM库打包成sysroot再重新编译问题才解决。所以如果你做的是长期项目建议直接做一个和目标板系统完全一致的sysroot不是下载官方默认的轻量sysroot。5. 常见问题排查与实用经验交叉编译的报错千奇百怪但大部分集中在几个点上。这里把我实际遇到过的典型问题和排查思路整理成速查表长期做ARM开发的建议收藏。5.1 头文件找不到报错示例hello_arm.c:1:10: fatal error: stdio.h: No such file or directory原因编译器找不到目标架构的C库头文件。常见于使用发行版自带工具链时只装了gcc-aarch64-linux-gnu没装libc6-dev-arm64-cross。修复sudo apt install libc6-dev-arm64-cross如果是自己下载的工具链检查--sysroot参数是否正确指向工具链内libc目录。5.2 链接阶段库找不到报错示例undefined reference to pthread_create或者cannot find -lpthread原因链接时没找到对应库或者库版本不匹配。排查确认编译命令中是否加了-lpthread。用find在sysroot里搜索libpthread.so是否存在。确认没有用宿主机/usr/lib下的x86库文件。从目标板上拷贝库到sysroot时注意软链接也要一起拷很多.so文件是指向带版本号文件的软链接用scp -r不一定保留链接关系建议用tar打包再解压。5.3 运行时提示架构错误或找不到解释器报错示例-bash: ./hello_arm: cannot execute binary file: Exec format error或者No such file or directory前者说明文件不是目标架构或者目标板本身不是ARM后者更坑文件其实存在但脚本解释器路径不对常见于动态链接程序因为/lib/ld-linux-aarch64.so.1不存在。排查file hello_arm确认ELF架构是aarch64。然后在目标板执行ls -l /lib/ld-linux-aarch64.so.1如果确实没有这个文件要么换静态编译要么检查目标板系统是否完整。5.4 GLIBC版本不匹配报错示例version GLIBC_2.34 not found (required by ./hello_arm)原因编译时使用的工具链glibc版本高于目标板系统glibc版本。修复思路优先降级工具链版本新工具链编出的二进制对旧系统支持很差。或者静态编译绕开glibc兼容问题。或者在一个与目标板系统版本一致的ARM容器里做编译让容器环境模拟目标板。我在实际项目中最后选择了第三种方案用Docker的ARM模拟容器qemu用户态模拟在容器里配置与目标板相同版本的基础镜像再安装编译依赖。这样编出来的程序兼容性最好还能顺手跑测试用例。不过这里不展开qemu的配置了方法大家都能搜到。5.5 快速排查速查表现象可能原因排查命令/修复file显示x86-64用了宿主gcc换成aarch64-linux-gnu-gcc找不到头文件sysroot不完整安装libc6-dev-arm64-cross或检查--sysrootundefined reference第三方库没编译ARM版重新交叉编译依赖库cannot execute binary file架构不匹配file命令核对ELF架构GLIBC版本报错工具链版本过新降级工具链/静态编译静态编译后体积大glibc被整体打包换musl工具链或接受体积运行时报段错误浮点ABI不匹配检查hf/eabi选择是否一致5.6 我的一些避坑心得交叉编译这件事刚上手时会觉得“命令不难坑难填”。我从几个项目里总结了几条经验第一工具链能锁定就锁定。同一个项目不同成员用不同版本的Linaro工具链编出来的行为可能不一致。我建议把工具链固定一个目录版本号写进README甚至用符号链接做版本切换。第二sysroot里的库越完整越好。不要依赖工具链自带的精简sysroot直接用tar从目标板上打包/usr/lib、/usr/include再解压到工具链sysroot目录这样最贴近真实运行环境。第三编译选项统一。比如-O2、-fPIC、-march等选项多人协作时最好写进CMakeLists或者Makefile里的全局变量避免有人单独编译时加上奇怪的flag。第四重视file和readelf这两个命令。它们能让你在最快时间内判断编译产物是不是你要的东西不用等到拷到板子上才发现问题。最后再分享一个小技巧如果你手头没有ARM板子却想验证交叉编译产物能不能跑可以在x86 Linux上配合QEMU的用户态模拟跑起来命令大致是sudo apt install qemu-user-static ./hello_arm在开启了qemu-user-static的x86系统上Linux内核通过binfmt_misc自动识别ARM ELF并用qemu-aarch64执行。我第一次发现这个技巧时省下了不少来回拷贝板子的时间调试依赖库、验证运行时行为都方便很多。交叉编译本身不神秘本质上就是“用一套完整的ARM世界构建工具链在你熟悉的环境里生产能被别处运行的产物”。只要工具链、sysroot、目标系统版本三者保持一致这条路会走得顺利很多。希望这篇记录能帮你少踩几个坑。