ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

aarch64 Qt静态交叉编译全流程:从工具链到target机部署

aarch64 Qt静态交叉编译全流程:从工具链到target机部署 我前阵子接到一个活儿一台aarch64的工控机系统里没有gcc更没有Qt运行库文件系统又是只读的想往上面跑一个带界面的Qt程序。折腾到最后最靠谱的方案就是静态交叉编译——在x86的开发机上用aarch64交叉工具链把Qt 5.14.2和应用程序一起编译成一个不依赖目标设备任何库的ELF可执行文件拷过去直接跑。这篇就把整个环境从零搭建到最终出包的过程完整记录下来。里面包含工具链与sysroot的选型、依赖库怎么处理、configure参数逐条拆解、编译和链接阶段的经典报错排查以及最后在目标机上验证产物时容易栽进去的几个坑。想给ARM64设备交付Qt软件的朋友这份手册可以直接照着抄。1. 动手之前版本、工具链与目标架构三件事先定死1.1 为什么这个场景非选 Qt 5.14.2 不可交叉编译最怕的就是版本选错、选太新资料少、依赖变化大。Qt 5.14.2属于5.14系列稳定性在嵌入式圈子里口碑不错很多工控板卡厂商的BSP里默认带的都是这一条线的Qt库。它对Linux平台抽象层QPA里的linuxfb、eglfs这些后端支持非常成熟做车载屏、工控HMI、瘦客户机界面都够用。从技术角度看5.14.2的源码包里自带了zlib、libpng、libjpeg、harfbuzz、pcre2、sqlite等大量第三方库可以通过-qt-*系列参数让Qt直接用源码树里那份避免外部依赖链越拉越长。这一点对静态编译来说简直是救命稻草。相比之下Qt 6.x在交叉编译时要处理的CMake工具链配置、python依赖明显更重而Qt 5.15虽然还在补丁期但5.14.2的资料量和稳定性更稳妥尤其是网上能搜到的aarch64下踩坑经验绝大多数都基于5.14或5.15。另外要明白Qt官方不提供aarch64 Linux离线安装包你想在ARM设备上装Qt基本只有两条路要么在设备上现场编译要么就是在x86主机上交叉编译。设备上现场编译一个完整Qt耗时太长且会污染只读文件系统交叉编译是唯一符合工程交付的做法。1.2 交叉工具链系统源里的 aarch64-gcc 就够用交叉工具链我推荐直接用Ubuntu自带的干净又好卸载sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu aarch64-linux-gnu-g --version这个包安装后编译器、链接器、ar、strip、objcopy全部带上前缀aarch64-linux-gnu-sysroot自动指向/usr/aarch64-linux-gnu。比如你搜索一个arm64版的库会出现在/usr/aarch64-linux-gnu/lib和/usr/aarch64-linux-gnu/include下面而不是宿主机自己的/usr/lib和/usr/include。这条隔离规则非常重要交叉编译的依赖库必须装到工具的sysroot里绝不能让configure脚本搜到x86宿主机上的头文件和库否则后续会出现各种匪夷所思的“Wrong ELF class”或头文件版本错乱问题。关于工具链版本Ubuntu 20.04/22.04自带的gcc 9/10/11系列编译Qt 5.14.2一点问题都没有不需要去折腾Linaro或ARM官方工具链。只有一种情况例外——目标设备里的glibc版本特别老比如老板子还是2.17的那才需要考虑用Linaro提供的早期工具链。绝大多数人都遇不到这个场景系统源方案最省心。1.3 目标架构先确认好别编译到一半才后悔拿到目标设备第一件事在设备上执行uname -m输出是aarch64就说明是ARMv8/AArch64 64位架构配aarch64-linux-gnu-g没问题。如果看到的却是armv7l那是32位ARM工具链要换成arm-linux-gnueabihf-Qt的mkspec也完全不同。这个判断务必在开头做掉不然编译了半天最后拷到设备上“Illegal instruction”或者“Exec format error”心态直接崩。另外交叉编译和普通编译的用途差别要搞清楚普通编译在x86上生成x86二进制静态编译在x86上生成一个不带动态依赖的x86二进制而“静态交叉编译”是两者叠加生成的aarch64二进制在目标设备上不依赖任何额外的so文件。后面所有工作都是围绕“静态”和“aarch64”这两个词展开的。2. 依赖库地基一个-qt-参数能带过绝不手工造轮子2.1 哪些库要预先交叉编译哪些直接用Qt自带的网上很多Qt交叉编译教程第一步就让你去编译zlib、libpng、libjpeg、sqlite、openssl看得人头皮发麻。实际上Qt 5.14.2非常贴心地提供了“嵌入式小依赖”策略configure的时候加这几个参数-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz含义是让Qt在编译时直接使用qtbase源码里src/3rdparty目录下的这些库源码编译成Qt静态库的一部分。这样一来你完全不需要提前在sysroot里准备它们的aarch64版本也不用管它们自己的交叉编译配置。这是整个搭建流程里最省时间的一步。我整理了一个依赖处理总表建议照着这个思路走库/组件处理方式原因说明zlib、libpng、libjpeg、freetype、harfbuzz使用-qt-*参数启用源码内构建Qt自带这些库的完整源码避免外部交叉编译openssl-no-openssl暂时禁用静态编译openssl要处理libcrypto的依赖闭环对纯界面应用收益不大xcb、X11-no-xcb禁用目标设备是无桌面嵌入式环境不需要X WindowOpenGL/EGL-no-opengl禁用如果做2D界面或linuxfb显示不依赖GPU驱动栈libuuid手动交叉编译或裁剪特性Qt的configure有时会探测系统uuid库sysroot里没头文件就报错sqlite默认走-qt-sqlite源码内构建Qt自带sqlite插件不开外部版本反而省事2.2 唯一可能需要手动编译的libuuid我在实际操作中遇到configure阶段明确报uuid/uuid.h: No such file or directory这是Qt的configure脚本探测libuuid特性时找不到包导致的。网上最简单的说法是“sudo apt install libuuid-dev:arm64”但在我这边这条命令把arm64包装到了宿主机的multiarch目录与交叉工具链的sysroot对不上反而引出新的头文件混乱。当时我的处理方式是下载e2fsprogs源码只交叉编译其中的libuuid并安装到sysrootwget https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/.../e2fsprogs-1.47.0.tar.gz tar xf e2fsprogs-1.47.0.tar.gz cd e2fsprogs-1.47.0 export PKG_CONFIG_LIBDIR/usr/aarch64-linux-gnu/usr/local/lib/pkgconfig ./configure --hostaarch64-linux-gnu --prefix/usr/aarch64-linux-gnu/usr/local make -C lib/uuid -j$(nproc) make -C lib/uuid install这样/usr/aarch64-linux-gnu/usr/local/include/uuid/uuid.h和静态库/usr/aarch64-linux-gnu/usr/local/lib/libuuid.a就位Qt configure再探测时就能顺利通过。如果你的目标环境压根不需要uuid这个feature也可以尝试在configure参数里通过-no-feature-libuuid直接关掉探测但不同小版本支持情况有差异我建议手动编译libuuid最保险。2.3 如果以后要启用 openssl提前留好编译位这次先把openssl禁用了但很多项目跑着跑着就要上HTTPS到时候再回来编译openssl也来得及。openssl的交叉编译命令非常简单./Configure linux-generic64 no-shared --prefix/usr/aarch64-linux-gnu/usr/local --cross-compile-prefixaarch64-linux-gnu- make -j$(nproc) make install编译出的libssl.a和libcrypto.a直接进入sysroot然后重新配置Qt时把-no-openssl换成-openssl-linked并确认PKG_CONFIG_LIBDIR指向sysroot的pkgconfig目录就可以在QtNetwork里启用HTTPS了。这个步骤我放后面做前期不碰它确保整体链路最短。3. configure参数逐条拆解静态、平台、模块裁剪缺一不可3.1 一条能跑通的完整configure命令进入qt-everywhere-src-5.14.2源码目录后我建议先建一个build目录源码树保持干净方便后续查问题或重新配置mkdir -p /opt/qt-build/build-aarch64 cd /opt/qt-build/build-aarch64 /path/to/qt-everywhere-src-5.14.2/configure \ -prefix /opt/qt-5.14.2-aarch64 \ -xplatform linux-aarch64-gnu-g \ -static \ -release \ -opensource -confirm-license \ -no-opengl \ -no-xcb \ -no-feature-xcb \ -skip qtwebengine \ -skip qtwayland \ -skip qtscript \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz \ -no-openssl \ -nomake examples -nomake tests -nomake tools \ -sysroot /usr/aarch64-linux-gnu你可能会问为什么在源码目录外建build目录还能跑configure因为Qt 5.14支持影子构建生成的中间文件和源码分离这对交叉编译来说非常友好。如果这个命令执行到一半想改参数不用从头解压把这个build目录删掉重建就行。3.2 这条命令里每一个关键参数背后的逻辑-prefix /opt/qt-5.14.2-aarch64指定最终安装路径。注意它是“从sysroot角度看”的安装路径后面qmake生成的应用Makefile会引用这个前缀下的库。整个安装目录最后可以直接拷贝或打包分发。-xplatform linux-aarch64-gnu-g这是整个交叉编译的核心。linux-aarch64-gnu-g对应源码中qtbase/mkspecs/linux-aarch64-gnu-g目录里面有个qmake.conf文件描述了要用的编译器前缀。Qt的configure拿到这个参数后所有内部编译、链接、打包步骤都会使用aarch64-linux-gnu-gcc和aarch64-linux-gnu-g。-static静态编译开关。它会影响Qt自身的构建方式最终生成的libQt5Core.a、libQt5Gui.a等全部是静态库并且qmake在生成应用Makefile时也会自动加上-static链接标志。-no-opengl -no-xcb -no-feature-xcb嵌入式设备没有X Window和OpenGL去掉它们可以砍掉一大批依赖检查。其中-no-xcb关闭xcb插件构建-no-feature-xcb关闭QtGui库里对xcb支持特性的编译。对纯Qt Widgets程序跑linuxfb来说这两个都不需要。-skip qtwebengineqtwebengine是Chromium内核交叉编译aarch64版本需要下载大量第三方工具链且编译时间以天计99%的嵌入式界面项目用不到直接从构建列表里剔除。-sysroot /usr/aarch64-linux-gnu显式告诉configure系统库和头文件的根目录在这里。如果你不写工具链默认会用自己的sysroot有可能把编译器自带的C头文件目录和系统库目录混在一起configure探测结果会很随机。显式指定后所有头文件搜索和库搜索均以该目录为根。3.3 mkspec找不到时自己造一个qmake.conf 修改法我遇到过一次configure报Could not find qmake spec linux-aarch64-gnu-g的情况原因是下载的源码包被裁剪过mkspecs目录不完整。此时最简单的做法是复制一个近似的spec然后改成arm64cp -r /path/to/qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-arm-gnueabi-g \ /path/to/qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-aarch64-my-g然后编辑这个新目录下的qmake.conf把里面的编译器命令改成aarch64版本QMAKE_CC aarch64-linux-gnu-gcc QMAKE_CXX aarch64-linux-gnu-g QMAKE_LINK aarch64-linux-gnu-g QMAKE_LINK_SHLIB aarch64-linux-gnu-g QMAKE_AR aarch64-linux-gnu-ar cqs QMAKE_STRIP aarch64-linux-gnu-strip QMAKE_OBJCOPY aarch64-linux-gnu-objcopy QMAKE_NM aarch64-linux-gnu-nm这里特别容易踩的坑是QMAKE_AR没改导致Qt打包静态库时用的还是宿主x86的ar工具。虽然静态库.a的格式是统一的但在configure阶段有些特性测试会调用ar去创建测试库命令名前缀不对就会找不到工具或构建出宿主格式的归档文件。所以五个工具全都要换成aarch64前缀。改完后把configure命令里的-xplatform linux-aarch64-gnu-g改成-xplatform linux-aarch64-my-g其余不变configure就能继续跑。3.4 模块裁剪少编译一个模块少踩一片坑Qt整个仓库有几十个模块交叉编译全量编译会带来几个后果时间暴涨、磁盘占用暴涨、第三方依赖变多、报错概率变高。我的裁剪思路是只保留Qt应用最常用的基础模块其他的全部skip。实际需要保留的模块有qtbase、qtdeclarative、qttools可选、qtsvg如果需要矢量图。如果是纯Widgets程序连qtdeclarative都可以不要。常见可以放心跳过的模块和理由模块为什么跳过qtwebengineChromium交叉编译复杂度极高时间成本大qtwayland无Wayland显示服务环境就用不上qtscript已废弃Qt6彻底移除qtquick3d高性能3D场景非必需qtdoc、qtqa、qtrepotools文档、测试、仓库工具生产环境无用裁剪后configure阶段会打印一张“Modules in configuration”列表自己检查一下QPA backends里有没有linuxfb这一步非常重要后面目标机上能不能出画面全靠它。4. make构建与安装耗时、提速和编译期错误现场4.1 编译命令、核数与安装路径验证configure正常结束并打印Qt is now configured for building后开始编译make -j$(nproc) 21 | tee build.lognproc在8核机器上会变成8内存如果不到8G建议手动-j4否则编译过程中的并行g进程容易把内存撑爆直接导致“internal compiler error: Killed”。编译Qt的静态库时每个编译单元都带大量模板实例化和内联展开单个g进程能吃掉1~2G内存所以内存比核心数更容易成为瓶颈。我第一次全量编译大约花了1个半小时机器是8核16G。编译期间如果某个模块因为第三方库缺少而失败不需要重头再来直接在configure命令里把该模块加入-skip列表重新建一个build目录配置并编译即可。编译结束后安装make install检查安装结果是否真实可用的命令是看/opt/qt-5.14.2-aarch64/bin/qmake是否存在并且执行/opt/qt-5.14.2-aarch64/bin/qmake -query输出里QT_HOST_PREFIX对应的如果是/opt/qt-5.14.2-aarch64QT_TARGET_ARCH如果是aarch64就算安装成功。注意qmake -query输出里的QT_HOST_*和QT_TARGET_*两个命名空间交叉编译后它们并不相同这是边界感很重要的一次体现。4.2 编译期高频报错与现场修复记录编译期不是所有错误都来自模块源码本身我遇到最多的是configure在探测阶段埋下的隐患真正的编译阶段反而相对顺利。如果你在configure阶段看到这类报错ERROR: Feature system-zlib was enabled, but the system library is not available这是configure认为你声明了使用系统zlib但sysroot里没有。解决办法是给configure加上-qt-zlib所有依赖库统一走源码内构建这类错误就从根本上消除了。类似地凡是报“system-xxx is enabled but not available”的就改用对应的-qt-xxx参数。如果报错出现在具体模块源码编译过程中先去查这个模块依赖了哪个外部库。比如qtimageformats模块如果依赖外部的webp、tiff库而你没有在sysroot里补上它就会中途失败。既然不需要这些图像格式直接在configure命令里加-skip qtimageformats把它跳过去比去费劲交叉编译一堆图像解码库划算得多。4.3 链接阶段的经典报错静态库顺序与-Wl,--start-groupQt自带的模块通常能顺利链接但当你自己的程序逐渐变大引入的第三方静态库会带来一系列链接顺序问题。静态库链接的规则是“谁依赖谁谁就排在前面被依赖的库永远排在后面”。比如你的程序用到了libpng而libpng内部又依赖zlib链接命令行顺序必须至少是-lpng -lz如果写成-lz -lpng多数情况下链接器还是能过的因为现代链接器有垃圾回收和组扫描机制但一旦涉及时序复杂的库比如libxml2依赖lzma、zlib、iconv多个库单靠人工排顺序就非常痛苦。这时候可以用链接器的组扫描参数一次解决LIBS -Wl,--start-group -lz -lpng -ljpeg -Wl,--end-group--start-group包围的库会被链接器反复扫描直到所有符号引用都满足循环依赖也能解开。Qt静态库之间偶尔也会出现这种互相引用如果你在链接应用时看到大量未定义符号但又确定这些符号确实存在某个libQt开头的库里就用组扫描包一圈。注意这个参数不要滥用把整条命令行都包进去会让链接时间显著变长。5. 交叉编译第一个Qt窗口程序并验证产物5.1 用交叉qmake生成工程Makefile交叉安装完成后编译应用程序基本就是水到渠成。先写一个最小验证程序main.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(hello aarch64 static); label.resize(320, 200); label.show(); return app.exec(); }工程文件hello.proQT core gui widgets TARGET hello_aarch64 TEMPLATE app SOURCES main.cpp构建时不要直接输入qmake要明确用交叉编译后的那个qmake否则系统原有的x86版qmake会跑来做宿主构建/opt/qt-5.14.2-aarch64/bin/qmake hello.pro make -j$(nproc)如果你在工程里加入了第三方静态库路径记得在.pro里写INCLUDEPATH /usr/aarch64-linux-gnu/usr/local/include LIBS -L/usr/aarch64-linux-gnu/usr/local/lib5.2 file、ldd、readelf 三条命令看穿产物编译完成后验证静态交叉编译是否成功有三板斧file hello_aarch64正常输出应该是ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked这里看到ARM aarch64说明架构正确看到statically linked说明静态链接生效。如果输出里出现dynamically linked说明.pro中少了静态标志回到工程文件添加QMAKE_LFLAGS -static再执行ldd hello_aarch64静态二进制会直接提示“不是动态可执行文件”或“statically linked”说明它已经不再依赖任何so拷到目标设备上运行不需要带任何Qt库。最后用aarch64-linux-gnu-readelf -h hello_aarch64确认ELF头的Machine字段是AArch64这一步可以作为发布前的自动化检查脚本。5.3 静态插件导入漏了它窗口根本起不来这里有个非常隐蔽的坑Qt把很多功能做成插件比如linuxfb平台插件、png格式插件、字体引擎插件。动态编译时这些插件以so形式放在plugins/目录下运行时按需加载静态编译时插件被编成了.a静态库但不会自动进入你的可执行文件必须显式声明。最简单的做法是在.pro里声明QTPLUGIN qlinuxfb qgif或者更直接地在main.cpp里手动导入#include QtPlugin Q_IMPORT_PLUGIN(qlinuxfb)如果没有这一步程序在目标机上会报This application failed to start because no Qt platform plugin could be initialized.这个错误信息几乎可以断定是静态插件没链进去而不是系统配置问题。处理完插件链接后应用运行前还要设置QPA平台export QT_QPA_PLATFORMlinuxfb ./hello_aarch64linuxfb模式直接写framebuffer节点/dev/fb0不需要X Server、Wayland或任何窗口管理器这是无桌面嵌入式设备上的最佳选择。6. 踩坑实录从configure到目标机运行的完整排查链路6.1 坑一uuid/uuid.h not found——configure探针的“误伤”第一个让我头疼的报错出现在configure阶段checking for uuid/uuid.h... no随后编译直接中断提示找不到uuid/uuid.h。其实我的程序从头到尾没用过uuid库但Qt的configure脚本把这个feature默认置为有系统依赖。排查路径是这样的打开config.log搜索uuid/uuid.h看到失败记录是编译器尝试includeuuid/uuid.h但路径不存在。检查sysroot的/usr/aarch64-linux-gnu/usr/local/include确实没有uuid头文件。决定按2.2节的方式编译libuuid进sysroot让探查通过。这个坑提醒我Qt的configure是一个“特性全探测”脚本它会检测系统能力来决定启用哪些功能交叉编译环境下这些探测全部依赖sysroot内容的完整性。与其在报错后一个个补库不如一开始就确定好sysroot里要有什么并在configure命令里裁剪掉不需要的feature。6.2 坑二undefined reference to inflate——静态链接顺序造成的假象链接自己的应用时遇到一堆未定义引用libQt5Core.a(qzlib.cpp.o): In function QZlibStream::decompress: undefined reference to inflate明明加了-lz为什么还是找不到问题出在链接顺序上Qt的.a静态库在命令行中的位置靠前它内部的inflate符号解析时链接器还没扫描到后面的-lz自然报未定义。我当时的解决方式是在.pro文件里使用组扫描参数把Qt库和zlib放在同一个组里QMAKE_LFLAGS -static LIBS -Wl,--start-group -lQt5Widgets -lQt5Gui -lQt5Core -lz -ldl -lpthread -Wl,--end-group静态库互相依赖的循环关系用组扫描一次解决。这条经验之后我处理所有第三方静态库都很通用凡是链接阶段出现“明明有符号但解析不了”第一反应就是检查链接顺序或者干脆上组扫描。6.3 坑三目标机上段错误还是Could not load platform plugin把hello_aarch64拷贝到目标设备后第一次运行并没有出现窗口而是报错This application failed to start because no Qt platform plugin could be initialized.我的第一反应是检查plugins/platforms目录是否存在。但静态编译的二进制根本没有目录概念问题在于我没把linuxfb插件静态导入。加入Q_IMPORT_PLUGIN(qlinuxfb)后错误变成了Segmentation fault这就更不能忍了。排查链路是这样的先确认CPU架构目标设备uname -m输出必须是aarch64符合预期。用aarch64-linux-gnu-readelf -h hello_aarch64确认ELF Machine字段是AArch64。检查目标设备上是否有/dev/fb0嵌入式设备如果内核没开fbdev或分辨率不对linuxfb插件初始化会失败。执行ls /dev/fb*发现/dev/fb0确实存在。在目标机上加调试输出export QT_DEBUG_PLUGINS1运行后打印全插件加载过程最后定位到“Cannot open display”并不是dispay问题而是没有指定QPA平台。最终设置export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_DRM1 ./hello_aarch64窗口正常显示。这个坑说明静态Qt程序运行时的环境变量依然很重要静态只解决“依赖库”问题不解决“运行时设备配置”问题。6.4 config.log定位问题比搜索引擎更直接的唯一真源凡是configure阶段出问题不管报错多吓人我的第一动作永远是grep -n error\|Error\|ERROR\|not found\|No such config.log | head -50config.log会记录configure期间执行的每一条测试命令及其输出你看到的报错信息往往只是它最后搭起的冰山一角。比如编译xcb相关feature失败时屏幕只显示一句“Basic XLib functionality test failed”但config.log里能看到具体的编译器报错是GL/gl.h not found还是xcb.h not found从而决定是补库还是禁feature。另外强烈建议在configure前设置环境变量export PKG_CONFIG_LIBDIR/usr/aarch64-linux-gnu/usr/local/lib/pkgconfig:/usr/aarch64-linux-gnu/lib/aarch64-linux-gnu/pkgconfig export PKG_CONFIG_SYSROOT_DIR/usr/aarch64-linux-gnu这样pkg-config只会在sysroot里找.pc文件不会探测到宿主x86的依赖环境。很多明明交叉编译了某个库configure却依然说找不到的灵异事件多半就是PKG_CONFIG_LIBDIR没设置对。7. 环境搭好之后还能做什么当这套交叉编译环境完整跑通后你得到的其实不只是Qt的编译器工具链而是一个完整的aarch64静态编译平台。同一个sysroot、同一套aarch64-linux-gnu-前缀工具链、同一个pkg-config配置可以用来交叉编译大量在ARM64服务器或嵌入式设备上运行的C/C程序。比如nginx这类高性能组件交叉编译时只需指定编译器./configure --with-ccaarch64-linux-gnu-gcc --with-ld-opt-static --without-http_rewrite_module这里的依赖逻辑和Qt完全一致静态链接时编译器找头文件从/usr/aarch64-linux-gnu下的路径找pkg-config配置指向sysroot最后用file命令验证。甚至phantomjs这类缺少官方aarch64预编译二进制的项目只要你能拿到对应的WebKit源码并愿意花时间去适配这套环境也是基础。个人觉得最值回票价的其实是省下的时间成本。初次搭建静态交叉编译Qt环境从工具链选型到最终目标机跑通我前后花了两三天大部分时间都耗在configure探测和插件导入上。环境搭好后后续每个Qt工程从写代码到产出aarch64静态二进制基本十分钟以内就能完成。以后再接ARM设备上的界面项目开发、编译、部署、验证的路径就固化下来了不需要每次都从零开始摸黑。
返回列表