十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

ARM交叉编译实战:从工具链选型到国产化适配全攻略

ARM交叉编译实战:从工具链选型到国产化适配全攻略 1. 为什么我熬了一个通宵才彻底搞清楚“代码不认识CPU”这件事先说个真实经历。以前我在做嵌入式设备时在一台x86的Ubuntu服务器上写好了一整套C语言网络服务程序本地编译、本地运行一切正常。结果要把程序部署到一块ARM开发板上直接拿gcc编出来的二进制扔过去终端回了一句cannot execute binary file: Exec format error。我当时第一反应是“文件传坏了”反复传了四五遍差点把网线都换了。后来才明白问题根本不在传输而是我编译出来的程序是x86指令集ARM处理器的内核根本不认这串指令。这个经历引出了今天要聊的两个关键词ARM架构和交叉编译。ARM和x86是两种完全不同的CPU指令集架构而交叉编译就是在一台架构A的机器上编译出能在架构B上运行的程序。结合热搜词里大量出现的arm交叉编译、qt5.12.10交叉编译、飞腾arm交叉编译、银河麒麟这些内容可以判断很多人正在做嵌入式开发、国产化适配或者边缘计算设备的移植工作这正是当前开发圈里需求量非常大的技术方向。这篇内容我会把自己从“只知道交叉编译这个词”到“能独立把Redis、Qt这类重量级项目交叉编译到ARM平台”的整个过程复盘一遍把工具链选型、环境搭建、常见报错和排查方法全部写清楚。无论是刚接触嵌入式的新手还是已经被交叉编译折磨过的老手这篇文章都能给你一些参考。1.1 ARM和x86到底差在哪不只是“字长”的区别很多人刚接触ARM的时候第一反应是“ARM是精简指令集x86是复杂指令集”。这话没错但如果你只停留在这一层很多东西还是想不通。我个人的理解是ARM和x86的差异体现在指令的编码方式、寄存器数量、内存访问模型、字节序支持等多个维度而这些差异最终决定了同一个二进制文件在这两种平台上无法互通。举一个直观的例子x86的指令长度是可变的短的只有1个字节长的可以达到15个字节这种设计让x86在几十年的演进中保持了向后兼容但也带来了指令解码复杂的问题。ARM的指令则固定长度早期ARM指令集每条指令都是4字节后来Thumb指令集压缩到2字节这样处理器在取值和流水线处理上更简单功耗也更低。所以同样是跑一个加法运算你在x86上写add eax, ebx在ARM上可能是ADD R0, R1, R2机器码完全不一样。还有一个很多人忽略的点是字节序。ARM架构本身是支持大小端切换的但大多数ARM Linux系统跑的是小端模式。x86则固定小端。所以如果你在交叉编译时没有注意字节序的配置或者项目代码里用了网络字节序转换的函数很容易出现“编译过了运行结果却是错的”这种诡异问题。另外浮点运算的ABI应用二进制接口也是个大坑。ARM平台有hard float和soft float之分也就是用硬件浮点单元还是用软件模拟浮点运算。如果你的工具链是arm-linux-gnueabihfhf表示hard float但目标系统里的库却是用soft float编译的链接时就会报一堆无法找到库或者无法解析符号的错误。这种问题不是代码逻辑问题而是ABI不匹配光看代码根本看不出来。1.2 为什么不能直接在ARM板子上编译非要交叉编译有人会问既然ARM板子跑的是Linux系统那我直接在板子上装个gcc然后本地编译不就行了理论上可行但实际工程里几乎没人这么做原因有三。第一性能差距悬殊。ARM开发板往往没有太强的算力一台树莓派级别的板子编译一个Qt项目可能需要几个小时而用PC交叉编译可能只需要十几分钟。我在RK3576的开发板上实测过编译一个中等规模的C项目板载编译需要接近两小时交叉编译在PC上只要七八分钟。这个时间差在生产环境里意味着迭代效率的翻倍提升。第二存储空间和内存不够用。交叉编译工具链虽然也占空间但你只需要在PC上安装一份。如果要在板子上编译你需要把完整的工具链、系统头文件、依赖库全部放到板子的存储里很多板子的eMMC只有8GB或者16GB跑一个Android构建级别的编译任务存储直接告急。第三项目依赖的复杂性。很多项目的构建过程需要生成代码、执行脚本、跑测试程序这些步骤假设你处于一个开发环境里。如果目标板环境太精简缺了Perl、Python、Make等工具构建过程很容易中断。交叉编译则可以让开发环境与运行环境分离各干各的活。所以交叉编译不是“可选项”而是嵌入式开发和国产化适配中的“必选项”。理解了这个前提接下来就好办了。2. 工具链选型五套交叉编译器里我最后只留下了两套交叉编译工具链的选择是整个环节里最影响成败的因素之一。很多新手一上来就在网上搜索“交叉编译工具”然后找个版本下载下来结果编译出来的程序要么在板子上跑不了要么一运行就段错误半天排查不出问题。我刚开始那会儿就吃过这个亏后来把市面上常见的工具链都试了一圈才总结出一套靠谱的选型方法。2.1 交叉编译器名字里的玄机交叉编译器的命名非常有规律看懂名字就能知道这套工具链的适用场景。拿最常见的arm-linux-gnueabihf-gcc来说拆开来看arm目标架构是ARMlinux目标系统是Linuxgnu使用的C库是GNU的glibceabi嵌入式应用二进制接口hf使用硬件浮点再比如aarch64-linux-gnu-gccaarch64表示目标是64位ARM架构也就是常说的ARMv8-A及以上的芯片比如飞腾、鲲鹏、RK3588这些。如果是32位ARM Cortex-A系列一般用arm-linux-gnueabihf。这里必须强调一下32位和64位的工具链不能混用。我之前犯过一个经典错误目标板子是RK356864位处理器但我用了一套32位的工具链去编译结果程序确实能跑但只识别到4GB内存中的一部分而且一加载大文件就崩溃。后来查明原因我用的工具链是arm-linux-gnueabihf对应的是32位用户态环境系统的内核虽然是64位的但用户态的rootfs如果也是64位那你的32位程序就需要系统里有对应的32位动态库支持否则直接报No such file or directory而这个报错往往让人误以为是文件缺失实际是动态链接器路径不对。2.2 sysroot交叉编译最容易忽视的“第二根拐杖”交叉编译和本地编译最大的区别在于本地编译时编译器自动从/usr/include和/usr/lib下拉取头文件和库而交叉编译时你不能让它去宿主机的目录里找因为那些是x86版本的头文件和库。所以必须通过--sysroot参数指定目标系统的根文件系统路径。sysroot里面应该装有目标系统对应的头文件、动态库、静态库最好直接从开发板上拷贝一份干净的/usr目录出来或者从官方提供的rootfs镜像里解压出来。我一般习惯叫它sysroot-arm然后通过环境变量统一指定export SYSROOT/opt/sysroot-arm export CROSS_COMPILEaarch64-linux-gnu- export CC${CROSS_COMPILE}gcc export CXX${CROSS_COMPILE}g export CFLAGS--sysroot$SYSROOT -marcharmv8-a export LDFLAGS--sysroot$SYSROOT这样配置之后编译器去sysroot里找头文件和库不会污染宿主机的环境。很多项目编译失败查到最后就是sysroot没配编译器找到了宿主机上的x86头文件导致一堆类型不匹配或者宏定义冲突的报错。2.3 我最终留下的两套组合根据不同的目标场景我长期使用了以下两套目标平台工具链适用场景32位ARM Cortex-A7/A9/A53arm-linux-gnueabihf-gccgcc 9.3.0老旧工业设备、树莓派2B等64位ARMv8 Cortex-A53/A72/A76aarch64-linux-gnu-gccgcc 10.3.0RK3568、RK3576、飞腾、鲲鹏等在选择具体版本号时有个经验不要一味的追求最新版gcc。因为目标板上的系统库可能是用旧版gcc编译的新版gcc生成的二进制在某些ABI细节上可能不兼容导致链接或者运行时出问题。我一般会先用目标系统自带的gcc版本信息来反推工具链版本可以在板子上执行gcc --version如果板子太精简装不了gcc就看系统镜像的发布说明。把工具链版本和系统库的“代差”控制在一两个大版本以内是最稳妥的方案。3. 实战把Redis和Qt 5.12.10交叉编译到ARM板上的全过程理论说再多不如跑通一个真实项目。这里我挑两个有代表性的项目来讲一个是Redis典型的C项目构建过程简单非常适合练手另一个是Qt 5.12.10构建系统复杂依赖庞大是交叉编译里非常典型的“硬骨头”。把这俩搞定基本可以覆盖大多数项目的编译场景。3.1 Redis的交叉编译五分钟就能跑通但细节里有魔鬼Redis的构建过程基于Makefile而且它本身对交叉编译的适配做得比较好所以可以直接通过指定CC环境变量来完成。我以Redis 6.2.x版本为例步骤如下make distclean make CCaarch64-linux-gnu-gcc MALLOClibc -j$(nproc)指定MALLOClibc是因为Redis默认使用jemalloc而jemalloc在交叉编译时经常需要额外配置直接用libc的malloc实现可以避免这个麻烦。编译完成后在src/目录下会生成redis-server和redis-cli两个可执行文件。这时候最关键的一步是验证二进制文件的架构file src/redis-server看到输出中包含ARM aarch64字样说明编译成功。接着把这个文件拷贝到板子上要注意用二进制方式传输别用ASCII模式特别是从Windows传文件时容易踩坑。在板子上运行./redis-server --port 6379如果出现Fatal error, cant open config file这类报错别慌那不是交叉编译的问题是工作目录和配置文件路径的问题指定redis.conf的绝对路径即可。Redis的交叉编译有3个常见坑我全部踩过第一个坑是make默认使用宿主机的cc导致编译出来的还是x86的二进制。解决办法是明确指定CC变量同时建议把AR变量也指到对应的aarch64-linux-gnu-ar否则某些目标文件的打包步骤会调用宿主机的ar生成的对象格式虽然通常没问题但偶尔会触发奇怪的警告。第二个坑是MALLOC没有指定导致尝试编译jemalloc时因为缺少交叉编译环境而报错。直接指定MALLOClibc可以绕过。第三个坑是在编译过程中系统缺少aarch64-linux-gnu-pkg-config时某些依赖库的查找会失败。这个在纯Redis项目里不常见但如果你编译的是带TLS支持的Redis就需要注意安装对应的交叉编译版OpenSSL并设置PKG_CONFIG_PATH环境变量指向交叉编译库的.pc文件目录。3.2 Qt 5.12.10的交叉编译依赖管理是一场持久战Qt的交叉编译比Redis复杂得多因为Qt本身分很多模块还有大量的第三方依赖。我以RK3576平台为例跑过一次完整的Qt 5.12.10移植整个过程可以分为四个阶段。第一阶段准备依赖库。至少要确保sysroot里有这些库的开发包libfontconfig1-dev、libdbus-1-dev、libxcb1-dev、libxkbcommon-dev、libgl1-mesa-dev等。这些库如果缺失Qt的configure阶段就会检测不到对应的模块导致Qt编译出来功能不完整。第二阶段配置Qt源码。在Qt源码目录下执行mkdir build-arm cd build-arm ../configure \ -prefix /usr/local/Qt5.12.10-arm \ -xplatform linux-aarch64-gnu-g \ -release \ -opensource \ -confirm-license \ -nomake examples \ -nomake tests \ -skip qtwayland \ -no-opengl \ -sysroot $SYSROOT \ -device-option CROSS_COMPILEaarch64-linux-gnu-这里有几个参数需要注意。-xplatform linux-aarch64-gnu-g告诉Qt构建系统使用哪个平台描述文件如果你的工具链不在Qt默认支持列表里需要自己写一个.qmake.conf文件放到qtbase/mkspecs/目录下。-no-opengl是很多驱动不支持GPU加速时的折中方案如果目标板上有Mali GPU并安装了对应的用户态驱动可以考虑打开OpenGL支持。第三阶段编译和安装。Qt的构建时间比较长我在高性能PC上全核编译也需要40分钟左右。编译完后执行make install把Qt库和头文件安装到指定的-prefix目录下。如果后续需要把Qt库打包到板子上可以直接拷贝整个安装目录并在板子上设置LD_LIBRARY_PATH环境变量export LD_LIBRARY_PATH/usr/local/Qt5.12.10-arm/lib:$LD_LIBRARY_PATH第四阶段交叉编译使用Qt的应用程序。这里同样需要指定交叉工具链和Qt安装路径。一个典型的方式是使用Qt自带的qmake$SYSROOT/usr/local/Qt5.12.10-arm/bin/qmake your_project.pro make上述过程中我遇到的最大问题是libGL相关库的缺失。因为目标板上的GPU用户态驱动往往是以二进制形式提供的如果sysroot里没有对应的libGL.soQt的配置阶段就会报opengl library not found。我的解决办法是先从板子上拷贝/usr/lib/aarch64-linux-gnu/下的GPU驱动库到sysroot里再重新跑configure这样Qt就能正确识别OpenGL模块。3.3 名称服务问题cannot find -lxxx的排查思路交叉编译链接阶段最常见的一类报错是cannot find -lxxx。这个报错的含义是链接器找了指定的静态库或动态库但在搜索路径里没有找到。我一般按照下面四条线索排查确认sysroot里是否真的有对应的库文件注意库的架构必须是目标架构可以用file命令查看。确认库文件的版本号名称与库搜索名是否对应比如libssl.so.1.1和libssl.so的关系通常通过符号链接来匹配。确认链接器的搜索路径是否正确通过-L参数或者LIBRARY_PATH环境变量设置。确认库的架构与编译目标一致不要出现aarch64的工具链却链接了armhf的库。很多这类问题并不是“缺少库”而是路径配置没到位白白浪费了大量时间。我给这种做法取了个名字叫“先看格式再看路径最后才怀疑缺失”理由是格式错误和路径错误发生的概率远高于库缺失。4. 绕不开的国产化环境飞腾芯片、银河麒麟系统下的ARM交叉编译热搜词里出现了一大串国产化相关的关键词比如飞腾arm交叉编译、银河麒麟 ssh 10.3 rpm升级包arm、kylin linux advanced server v10 sp1 for arm下载。这说明国产化平台的软件适配已经是一个十分常见的开发任务了。我自己也帮人做过基于飞腾FT-2000/64平台的程序迁移这里有一套方法论可以分享。4.1 飞腾与麒麟环境的交叉编译特殊性飞腾处理器兼容ARMv8指令集这意味着在工具链层面你可以继续使用aarch64-linux-gnu-这一套交叉编译工具。但有一个很容易被忽略的问题银河麒麟系统的库版本与Ubuntu/CentOS的库版本有差异特别是glibc的版本。如果你的sysroot是从Ubuntu 20.04上拷贝的编译出来的程序在银河麒麟上运行时报GLIBC_2.34 not found之类的错误这就说明宿主机sysroot的glibc比目标系统新程序里引用了一些目标系统不存在的符号。稳妥的做法是直接从目标系统上获取一套完整的sysroot。具体操作是# 在目标飞腾麒麟的机器上执行 tar -czf sysroot-arm64.tar.gz /usr /lib /lib64 /etc/ld.so.conf.d # 把压缩包拷回开发主机 # 解压到指定目录 mkdir /opt/kylin-sysroot tar -xzf sysroot-arm64.tar.gz -C /opt/kylin-sysroot注意压缩时不能只打包/usr有些库和动态链接器的配置信息在/lib和/etc下面缺了会导致动态链接器找不到库搜索路径。我在第一次做sysroot时漏掉了/etc/ld.so.conf.d结果程序在板子上运行时找不到libstdc.so.6排查了很久才发现是sysroot里缺了库搜索路径的配置文件。4.2 国产化平台上的动态链接器路径问题交叉编译完的程序放到飞腾麒麟上运行时如果系统提示No such file or directory但你明确文件存在且权限没问题那大概率是动态链接器的路径不对。在ARM64平台上动态链接器的默认路径通常是/lib/ld-linux-aarch64.so.1。如果你的程序是拿交叉工具链默认配置编译的动态链接器的路径会被指定为工具链前缀目录下的某个路径比如/opt/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1但目标系统上并不存在这个路径。查看一个程序依赖的动态链接器路径readelf -l your_program | grep interpreter如果输出里的路径在目标板上不存在有两种解决办法第一种用patchelf修改程序的动态链接器路径patchelf --set-interpreter /lib/ld-linux-aarch64.so.1 your_program第二种在目标板上创建符号链接把编译时指定的路径指到实际存在的路径。不过这种方案不太优雅也不利于程序的长期维护我更推荐第一种。另外一个常见问题是rpath。程序在编译时如果没指定-rpath动态库的搜索顺序是先找LD_LIBRARY_PATH再查系统的ld缓存。当你把程序连同动态库一起拷贝到目标板的自定义目录时如果没设置LD_LIBRARY_PATH程序就会提示找不到库。这时候用一个启动脚本统一设置环境变量是最省心的方式。4.3 处理SSH等系统组件的ARM升级包热搜词里还出现了银河麒麟 ssh 10.3 rpm升级包arm这实际上是一种针对ARM架构的RPM包迁移操作。如果你在麒麟系统上需要升级openssh不能直接拿x86平台的rpm包硬装必须找aarch64架构的rpm包。在系统信息不明确时可以先执行uname -m输出aarch64就说明系统是ARM64架构。然后用rpm -ivh安装包但要注意依赖关系如果缺了依赖建议用yum localinstall如果系统仓库配置正常来自动解析依赖。比较让我头疼的是有些国产系统的软件源维护得并不到位某些软件包只在指定的发行版本里有。这种情况下可以尝试把这些包从目标系统的安装光盘中提取出来或者找同版本的其他ARM发行版仓库补包。在动手之前一定要做快照或备份系统升级失败导致ssh连不上只能跑机房的情况我遇到过不止一次。5. 拿到编译产物后的三道检查file、readelf、ldd一个都不能少交叉编译完成不意味着万事大吉恰恰相反真正的调试验证工作才刚开始。很多开发者在PC上看到编译成功就兴冲冲地把二进制打包发过去结果一运行就崩来回折腾浪费大量时间。我在交付二进制之前一定会做三道检查这三道检查可以拦截掉大部分常见的“运行不了”问题。5.1 file命令一眼看穿二进制真面目file命令可以快速识别二进制文件的目标架构和ABI信息。一个合格的ARM64可执行文件的输出长这样$ file redis-server redis-server: 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如果输出显示的是x86-64那说明工具链根本没有生效可能是在Makefile里被覆盖了CC变量或者用了缓存的对象文件。如果显示的是ARM aarch64但interpreter路径和你预期的不一致那就要接着用readelf来查看详细信息。5.2 readelf命令深入ELF文件内部readelf可以查看ELF文件的程序头、段信息、依赖库等。我最常用的两个参数是readelf -d your_program | grep NEEDED readelf -l your_program | grep interpreterNEEDED列出了程序依赖的所有动态库如果里面出现了一些目标系统上不存在的库那在目标板上运行大概率会失败。观察到这些情况需要在sysroot里补装对应的库然后重新链接。interpreter这行就是动态链接器的路径。我在前面已经提到过这里再强调一遍这行路径必须是在目标系统上真实存在的。很多时候file命令显示正常系统也提示找不到文件问题就出在这里。5.3 ldd命令动态库依赖的最后一环在开发主机上你可以用交叉工具链的ldd或aarch64-linux-gnu-ldd来检查动态库依赖查看哪些库找不到aarch64-linux-gnu-ldd your_program不过要提醒一下交叉工具链的ldd并不能真正模拟目标系统的运行环境它只是根据sysroot里的库信息来猜测依赖。最靠谱的验证方式是把程序放到目标板上用目标系统自带的ldd来检查ldd ./your_program如果输出中有些行显示not found说明程序依赖的某个库在目标系统上不存在。这时候要么把库文件一并打包过去要么检查是否链接了冗余的库。一个实战经验是在我的工作流程里这三道检查通常不会超过5分钟但能省下大量远程联调的时间。我曾见过同事把一个没检查过动态链接器路径的程序发给了现场实施人员结果在现场折腾了整整一天最后发现问题只是interpreter路径不对改一下就跑起来了。这种低级但致命的失误完全可以靠检查环节来规避。5.4 把程序打包成可分发版本的一个小技巧最后分享一个打包的技巧。嵌入式设备的程序分发往往需要连同动态库一起打包但盲目地拷贝/lib下所有库是不现实的。正确的做法是先用ldd确定程序依赖的库清单然后把它们集中拷贝到一个固定目录比如/opt/app/libs下再用patchelf把程序的rpath设置为$ORIGIN/libs。这样程序在移动时只要保持目录结构不变就能正确找到依赖库。我用这个方案交付过好几个项目客户反馈都很好因为他们不需要懂Linux库依赖知识拿到程序包解压后直接运行即可。这虽然是一个很小的工程细节但在实际部署中带来的便利是巨大的。
返回列表