ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt 5.14.2 aarch64 静态交叉编译实战:从工具链到部署

Qt 5.14.2 aarch64 静态交叉编译实战:从工具链到部署 1. 为什么值得折腾 Qt 5.14.2 的 aarch64 静态交叉编译如果你手上有 Orange Pi、树莓派这类 aarch64 开发板又打算把 Qt 程序直接跑在上面迟早会撞上同一个问题板子上没有完整的桌面环境动态库版本和开发机对不上程序拷过去一跑就报cannot mix incompatible Qt library或者干脆找不到.so。动态链接这条路在嵌入式场景里特别容易翻车因为目标板的系统镜像往往是精简过的缺哪个库你根本猜不到。静态交叉编译就是来解决这个痛点的。它的核心思路是在 x86_64 的开发机上用一套面向 aarch64 的交叉编译工具链把 Qt 库本身和你的应用程序全部编译成静态库最后链接出一个几乎不依赖目标板系统库的可执行文件。拷过去直接./app就能跑不用在板子上装 Qt、不用配LD_LIBRARY_PATH、不用担心库版本冲突。代价是编译过程比较磨人Qt 的 configure 参数多、依赖链条长第一次搞基本都要踩几个坑。这篇手册面向的是需要在 aarch64 平台上部署 Qt 应用的开发者尤其是做嵌入式 HMI、工业控制面板、边缘计算设备这类场景的朋友。我会从工具链准备一路讲到最终验证把 Qt 5.14.2 静态交叉编译的完整流程拆开包括 configure 参数为什么这么选、依赖库怎么处理、编译报错怎么排查。Qt 5.14.2 这个版本选得也有讲究——它是 Qt 5.14 系列的最后一个补丁版本稳定性经过大量项目验证同时保留了完整的qt-everywhere-src源码包结构适合做静态裁剪。需要提前说明的是静态编译 Qt 不是把-static一加就完事。Qt 内部大量使用插件机制platform plugins、imageformats、sqldrivers 等静态链接时这些插件必须显式导入否则程序跑起来会提示找不到平台插件。这是新手最容易卡住的地方后面会专门用一节讲清楚。2. 环境准备与工具链选型2.1 开发机环境的基本要求开发机我建议用 Ubuntu 20.04 或 22.04 的 x86_64 版本磁盘至少留出 40GB 空闲空间。原因很直接Qt 5.14.2 完整源码解压后接近 3GB编译中间产物加上静态库一个配置下来轻松超过 15GB如果你还想同时保留动态版本做对比空间需求翻倍。内存方面 8GB 是底线16GB 会舒服很多因为make -j并行编译时每个编译单元都吃内存Qt 的qtbase模块里有些大文件比如qpainter.cpp单个编译就能吃掉 1GB 以上。系统依赖包这块Ubuntu 下先装一批基础工具sudo apt update sudo apt install -y build-essential perl python3 git \ libgl1-mesa-dev libglu1-mesa-dev \ libxkbcommon-dev libxkbcommon-x11-dev \ libfontconfig1-dev libfreetype6-dev \ libssl-dev libdbus-1-dev \ bison flex gperf这些包的作用要理解清楚不能无脑装。libgl1-mesa-dev和libglu1-mesa-dev是给 Qt 的 OpenGL 模块用的即使你做的是无界面的嵌入式程序Qt 的qtbase在 configure 阶段也会检测 GL 头文件缺了会直接报错退出。libxkbcommon-dev是键盘映射相关libfontconfig1-dev和libfreetype6-dev负责字体渲染嵌入式设备上如果要用中文显示字体这块必须处理好。libssl-dev是网络模块的依赖libdbus-1-dev是 Linux 下进程间通信的基础。注意不要图省事直接apt install qt5-default那是给本机动态开发用的和交叉编译完全是两码事装了反而可能干扰环境变量。2.2 aarch64 交叉编译工具链的选择工具链是整件事的地基选错了后面全是坑。aarch64 的交叉编译工具链主流有几个来源Linaro 的gcc-linaro-aarch64-linux-gnu、ARM 官方的gcc-arm-*-aarch64-none-linux-gnu、以及各芯片厂商如瑞芯微、全志随 SDK 提供的定制工具链。我的建议是优先用 ARM 官方或 Linaro 的通用工具链版本选 GCC 9 或 GCC 10。原因在于 Qt 5.14.2 的代码对 GCC 版本有一定要求GCC 7 以下编译某些 C14 特性会出问题GCC 11 以上又可能因为更严格的语法检查报一堆警告甚至错误。GCC 9/10 是经过大量项目验证的甜点区间。下载后解压到/opt目录然后配置环境变量export TOOLCHAIN_PATH/opt/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu export PATH$TOOLCHAIN_PATH/bin:$PATH export CROSS_COMPILEaarch64-none-linux-gnu- export CC${CROSS_COMPILE}gcc export CXX${CROSS_COMPILE}g export AR${CROSS_COMPILE}ar export LD${CROSS_COMPILE}ld export STRIP${CROSS_COMPILE}strip验证工具链是否可用aarch64-none-linux-gnu-gcc -v能正常打印版本信息就说明路径配对了。这里有个细节CROSS_COMPILE变量末尾的短横线不能少很多构建脚本会用它拼接工具名少了短横线会拼出aarch64-none-linux-gnugcc这种错误路径。2.3 目标板系统库的提取静态编译虽然叫“静态”但并不是所有东西都能静态链接。libc、libpthread、libdl这些系统级库Qt 默认还是动态链接的除非你用-static全静态但那会带来更多麻烦比如 DNS 解析问题。所以你需要把目标板上的系统库和头文件同步到开发机让交叉编译器能找到它们。最稳妥的做法是从目标板的系统镜像里提取sysroot。如果你用的是 Orange Pi 或树莓派可以直接从官方镜像的根文件系统里拷贝/usr/lib、/lib、/usr/include这几个目录。更规范的方式是用rsync从运行中的板子上同步mkdir -p /opt/aarch64-sysroot rsync -avz --safe-links rootboard-ip:/lib /opt/aarch64-sysroot/ rsync -avz --safe-links rootboard-ip:/usr/lib /opt/aarch64-sysroot/usr/ rsync -avz --safe-links rootboard-ip:/usr/include /opt/aarch64-sysroot/usr/同步完成后在 configure 时通过-sysroot参数指向这个目录。这样交叉编译器在链接时就会优先去 sysroot 里找库而不是去开发机的 x86_64 库目录里找。实操心得sysroot 里的库版本必须和目标板实际运行的版本一致。我踩过一次坑开发机上同步的是旧镜像的库结果编译出来的程序在板子上跑一调用某个系统函数就段错误排查了半天才发现是libstdc版本对不上。同步完 sysroot 后建议用aarch64-none-linux-gnu-readelf -a检查一下关键库的版本号。3. Qt 5.14.2 源码配置与静态编译参数详解3.1 源码获取与目录规划Qt 5.14.2 的源码包可以从 Qt 官方归档站点下载qt-everywhere-src-5.14.2.tar.xz。这个包包含了 Qt 所有模块的源码解压后大概 2.8GB。我习惯把源码放在/opt/qt-src下编译输出目录单独放在/opt/qt-build这样源码目录保持干净重新配置时直接删掉 build 目录就行不用重新解压。mkdir -p /opt/qt-src /opt/qt-build cd /opt/qt-src tar -xf qt-everywhere-src-5.14.2.tar.xz cd /opt/qt-buildQt 的构建系统支持“影子构建”shadow build也就是在源码目录之外的地方执行 configure 和 make所有中间产物都生成在 build 目录里。这个机制一定要用否则源码目录会被编译产物污染想重新配置就得重新解压。3.2 configure 参数逐项拆解configure 是整个过程的核心参数选对了后面省一半力气。下面是我实际项目里用的一套配置先贴出来再逐条解释/opt/qt-src/qt-everywhere-src-5.14.2/configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -opensource -confirm-license \ -release -static \ -nomake examples -nomake tests \ -no-opengl \ -no-xcb \ -no-eglfs \ -no-linuxfb \ -no-kms \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre \ -no-iconv \ -no-cups -no-printdialog -no-printsupport \ -no-sql-sqlite \ -no-feature-dbus \ -skip qtwebengine -skip qtwebview \ -skip qtquickcontrols -skip qtquickcontrols2 \ -skip qtmultimedia -skip qtsensors \ -skip qtlocation -skip qtwayland \ -sysroot /opt/aarch64-sysroot \ -platform linux-g \ -xplatform linux-aarch64-gnu-g \ -device-option CROSS_COMPILEaarch64-none-linux-gnu- \ -v-prefix指定安装路径编译完成后make install会把所有静态库、头文件、qmake 等工具装到这个目录。建议路径里带上版本号和架构方便以后同时维护多个 Qt 版本。-static是静态编译的总开关它会同时影响 Qt 库本身的编译方式和最终链接行为。-release关掉调试符号减小库体积嵌入式场景基本都用 release。-nomake examples -nomake tests跳过示例和测试代码的编译能省下大量时间。Qt 的 examples 目录里有几千个示例工程全编译一遍可能要多花一两个小时而且对最终产物没有任何影响。-no-opengl这个参数要重点说。如果你的目标板没有 GPU或者你的应用不需要 OpenGL 渲染一定要关掉。Qt 默认会尝试编译 OpenGL 相关模块如果 sysroot 里没有对应的 GL 库configure 阶段就会报错。关掉之后 Qt 会使用纯软件渲染对于普通的界面程序完全够用。-no-xcb关掉 X11 平台插件。嵌入式设备通常跑的是 framebuffer 或者 EGLFS不需要 X11。关掉它能减少对 X11 库的依赖编译出来的程序体积也更小。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre这一组是让 Qt 使用自带的第三方库源码来编译而不是去链接系统库。静态编译时强烈建议这么做因为系统库的静态版本不一定有而且版本兼容性不好控制。Qt 自带的这些库版本是经过测试的用起来最稳。-no-iconv关掉字符集转换库依赖。如果你的应用不需要处理非 UTF-8 的文本编码关掉它能少一个依赖。-no-cups -no-printdialog -no-printsupport这一组是打印相关的嵌入式设备基本用不到关掉能显著减小体积。-skip qtwebengine必须加。QtWebEngine 是基于 Chromium 的编译它需要极其庞大的依赖和超长时间而且静态编译 QtWebEngine 几乎是不可能完成的任务。类似的还有qtwayland、qtmultimedia这些重型模块用不到就全部 skip。-sysroot指向前面准备的 sysroot 目录让交叉编译器在正确的位置找系统库。-xplatform linux-aarch64-gnu-g指定目标平台。这个值对应qtbase/mkspecs/linux-aarch64-gnu-g/qmake.conf文件Qt 源码里自带了这个 mkspec但里面的工具链前缀可能需要根据你实际用的工具链调整。打开这个文件检查一下cat /opt/qt-src/qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-aarch64-gnu-g/qmake.conf确认里面的QMAKE_CC、QMAKE_CXX、QMAKE_AR等变量指向的是你工具链里的实际路径。如果工具链前缀不是aarch64-linux-gnu-需要手动改这个文件或者通过-device-option CROSS_COMPILE覆盖。3.3 静态编译插件的处理策略静态链接 Qt 时插件不会自动被链接进可执行文件这是静态编译和动态编译最大的行为差异。动态编译时Qt 在运行时去plugins目录扫描并加载插件静态编译时没有这个目录所有插件必须在编译期显式导入。Qt 提供了一个宏Q_IMPORT_PLUGIN来解决这个问题。以平台插件为例如果你用的是 EGLFS需要在main.cpp里加#include QtPlugin Q_IMPORT_PLUGIN(QEglFSIntegrationPlugin)但更省事的做法是在 qmake 工程文件里用QTPLUGIN变量QTPLUGIN qeglfs qlinuxfb qminimalqmake 会自动生成对应的导入代码。对于图像格式插件比如你要显示 PNG 图片QTPLUGIN qpng qjpeg这里有个容易忽略的点QTPLUGIN里写的名字是插件库的文件名去掉lib前缀和.a后缀。比如libqpng.a对应的名字就是qpng。如果名字写错了链接时会报undefined reference但错误信息不会直接告诉你插件名错了需要自己排查。常见问题程序编译链接都通过了拷到板子上运行却提示This application failed to start because no Qt platform plugin could be initialized。这就是平台插件没导入的典型症状。解决办法是在main.cpp里显式Q_IMPORT_PLUGIN或者在 pro 文件里加QTPLUGIN qeglfs根据你实际用的平台插件调整。4. 完整编译流程与实操记录4.1 执行 configure 与常见报错处理配置命令准备好之后在 build 目录下执行。configure 过程大概需要 5 到 15 分钟取决于机器性能。它会检测各种依赖、生成 Makefile。这个过程如果有问题会直接报错退出所以一定要盯着输出。最常见的报错是找不到某个系统库的头文件。比如ERROR: Feature fontconfig was enabled, but the pre-condition libs.fontconfig failed.这说明 sysroot 里缺少 fontconfig 的开发文件。解决办法有两个要么从目标板同步对应的头文件过来要么在 configure 里加-no-fontconfig关掉这个特性。嵌入式场景如果不需要复杂的字体配置直接关掉是最省事的。另一个高频报错是工具链检测失败ERROR: Cannot compile a simple Qt program. Check your compiler installation.这通常是-xplatform指定的 mkspec 里的工具链路径不对或者环境变量没配好。检查qmake.conf里的路径确认aarch64-none-linux-gnu-g能在 PATH 里找到。configure 成功后会输出一份配置摘要列出哪些特性被启用、哪些被禁用。这份摘要要仔细看一遍确认关键特性比如你需要的模块没有被意外关掉。摘要最后会提示你运行make。4.2 并行编译与资源控制编译阶段是整个流程里最耗时的Qt 5.14.2 在 8 核机器上大概需要 40 分钟到 1 小时。用make -j并行编译能大幅缩短时间但并行度不是越高越好。经验公式是CPU 核心数 1比如 8 核就用-j9。设太高会导致内存不足编译进程被 OOM killer 杀掉反而更慢。make -j9 21 | tee build.log用tee把输出同时写到日志文件方便出错后回溯。编译过程中如果某个模块报错make 会停下来但已经编译好的模块不会重编修好问题后重新执行 make 会从断点继续。编译过程中可能遇到的典型错误错误现象原因解决办法undefined reference to __atomic_*工具链缺少原子操作支持在 configure 加-no-feature-atomics或链接-latomicfatal error: bits/wordsize.h: No such filesysroot 头文件不完整从目标板同步/usr/include完整目录error: numeric_limits is not a member of std缺少#include limits给对应源文件打补丁或换 GCC 版本cannot find -lstdc工具链的 C 库路径没配检查qmake.conf里的QMAKE_LIBDIR这些错误里__atomic_*那个特别常见。aarch64 架构下 GCC 对某些原子操作需要显式链接libatomic。解决办法是在qmake.conf里加一行QMAKE_LFLAGS -latomic4.3 make install 与产物验证编译完成后执行安装make install所有产物会装到-prefix指定的目录。安装完成后重点检查几个东西ls /opt/qt-5.14.2-aarch64-static/bin/应该能看到qmake、moc、uic、rcc这些工具。注意这些工具本身是 x86_64 的可执行文件因为它们在开发机上运行但它们生成的代码和链接的库是面向 aarch64 的。这是交叉编译的正常状态不要误以为装错了。检查静态库ls /opt/qt-5.14.2-aarch64-static/lib/应该能看到libQt5Core.a、libQt5Gui.a、libQt5Widgets.a等。用file命令确认架构file /opt/qt-5.14.2-aarch64-static/lib/libQt5Core.a输出里应该包含aarch64字样。如果显示x86-64说明 configure 时-xplatform没生效需要回去检查。4.4 用编译好的 Qt 构建测试程序装好之后用这个 Qt 编译一个最小测试程序验证整条链路。写一个简单的main.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello aarch64 static Qt); label.show(); return app.exec(); }pro 文件QT core gui widgets TARGET helloqt TEMPLATE app SOURCES main.cpp QTPLUGIN qeglfs qlinuxfb qminimal用交叉编译版的 qmake 生成 Makefile/opt/qt-5.14.2-aarch64-static/bin/qmake helloqt.pro make编译出来的helloqt用file检查应该是 aarch64 架构。用ldd检查动态依赖aarch64-none-linux-gnu-ldd helloqt理想情况下只依赖libc.so.6、libm.so.6、libpthread.so.0、libdl.so.2这几个系统库Qt 相关的库全部静态链接进去了。如果ldd输出里还有libQt5Core.so之类的说明静态链接没生效检查 pro 文件里是不是漏了-static或者 qmake 用错了版本。把helloqt拷到目标板直接运行scp helloqt rootboard-ip:/root/ ssh rootboard-ip /root/helloqt如果板子上有 framebuffer 设备应该能看到一个显示文字的窗口。如果报平台插件错误检查QTPLUGIN里有没有加上对应平台的插件名。5. 踩坑记录与排查速查5.1 静态编译特有的问题静态编译 Qt 有几个动态编译不会遇到的坑这里集中说一下。第一个是插件导入遗漏。前面已经强调过但实际项目里还是经常漏。特别是当你的程序用到QSqlDatabase、QImageReader这些依赖插件的类时如果对应的 sqldrivers 或 imageformats 插件没导入运行时会静默失败——QImageReader::supportedImageFormats()返回空列表或者QSqlDatabase::drivers()返回空。排查方法是检查 pro 文件里的QTPLUGIN是否覆盖了所有用到的插件类型。第二个是资源文件路径。动态编译时Qt 的资源系统qrc在运行时从可执行文件内部读取这个没问题。但如果你用了外部资源文件比如QFile读取一个绝对路径的配置文件静态编译不会改变这个行为路径该错还是错。这个不算静态编译的锅但容易混淆。第三个是字体渲染。静态编译时如果用了-qt-freetype字体引擎是编译进去的但字体文件本身.ttf还是需要在目标板上存在。嵌入式设备如果没装中文字体中文会显示成方块。解决办法是把字体文件打包进 qrc 资源或者确保目标板上有/usr/share/fonts目录并放了字体。5.2 目标板运行时的依赖检查程序拷到板子上跑不起来第一步永远是检查动态依赖aarch64-none-linux-gnu-readelf -d helloqt | grep NEEDED这会列出所有动态依赖的库。如果某个库在板子上不存在程序会直接报not found。常见的缺失库包括libstdc.so.6C 标准库、libgcc_s.so.1GCC 运行时。这两个库在精简的嵌入式系统里经常被裁掉。解决办法是在编译时加-static-libstdc -static-libgcc把这两个库也静态链接进去。在 qmake 里这样写QMAKE_LFLAGS -static-libstdc -static-libgcc这样编译出来的程序对系统库的依赖就只剩libc和libm了基本任何 Linux 系统都能跑。5.3 常见问题速查表问题可能原因排查方向configure 报找不到 GL 头文件没装 mesa 开发包或没关 opengl装libgl1-mesa-dev或加-no-openglmake 报undefined reference to __atomic_*缺少 libatomicqmake.conf 加-latomic程序运行提示 no Qt platform plugin平台插件没导入pro 文件加QTPLUGIN qeglfs程序运行提示 cannot mix incompatible Qt library链接了动态 Qt 库检查 qmake 版本和 pro 文件-static中文显示为方块缺少中文字体目标板装字体或打包进 qrc编译到一半被 killed内存不足降低make -j并行度qmake 生成的 Makefile 里工具链是 x86-xplatform没生效检查 mkspec 路径和 qmake.conf静态库体积过大没做裁剪用-skip关掉不需要的模块5.4 体积优化与裁剪技巧静态编译出来的可执行文件动辄几十 MB对于存储空间紧张的嵌入式设备来说是个问题。几个有效的裁剪手段用-no-feature-*关掉不需要的特性。比如你的程序不用拖拽功能可以加-no-feature-draganddrop不用剪贴板加-no-feature-clipboard。Qt 的特性开关非常多可以在qtbase/src/corelib/global/qconfig.h里看到完整列表。编译完成后用strip去掉符号表aarch64-none-linux-gnu-strip helloqt这一步通常能减小 30% 到 50% 的体积。如果还嫌大可以用-Os优化体积而不是-O2优化速度。在 qmake.conf 里改QMAKE_CFLAGS_RELEASE和QMAKE_CXXFLAGS_RELEASE把-O2换成-Os。代价是运行速度会慢一些但对于界面程序来说通常感知不明显。最后链接时加-Wl,--gc-sections让链接器丢弃未使用的代码段。配合编译时的-ffunction-sections -fdata-sections使用效果更好。这几个参数加在QMAKE_LFLAGS里就行。我在实际项目里用这套组合把一个带界面的 Qt 程序从 45MB 压到了 12MB对于 512MB 存储的板子来说完全够用了。当然具体能压到多少取决于你用了多少 Qt 模块模块越少压缩空间越大。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进