ARTICLE DETAIL

资讯详情

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

Qt 5.14.2 aarch64架构静态交叉编译实战指南

Qt 5.14.2 aarch64架构静态交叉编译实战指南 前阵子接手一个设备适配的活一块 aarch64 架构的 ARM 板需要把老项目的 Qt 界面原样跑起来。现场 rootfs 非常精简系统的 libstdc 版本跟主机库对不上装个桌面组件又怕把运行环境搞乱。思来想去最靠谱的方案就是直接把 Qt 5.14.2 源码做成交叉编译的静态库打包出一个不依赖目标板环境、拷过去就能跑的可执行文件。这次把整个从零搭建的过程整理出来工具链选择、configure 参数、mkspecs 改造、常见报错都逐一展开给正在折腾同类问题的朋友当个参考。这篇文章适合两类人一类是刚接触交叉编译需要一份能对照执行的完整流程另一类是已经在编译 Qt但卡在某个链接报错或者目标板运行崩溃上想找排查思路的。全文基于 Ubuntu 20.04 主机 aarch64 目标板 Qt 5.14.2 源码这套组合不同版本的小版本号有差异但核心思路是通用的。1. 方案选型为什么是 Qt 5.14.2 静态交叉编译1.1 版本选择的三个理由先说版本。很多人上来就问“为什么不用 Qt 6”答案很简单大量存量项目的界面代码是基于 QtWidgets 和 QML 的旧接口风格写的直接迁移 Qt 6 要处理 QRegExp 移除、OpenGL 模块拆分等一堆兼容性问题成本远大于收益。Qt 5.14.2 是 5.14 LTS 系列的最后一个补丁版本修复了此前的稳定性问题同时保留了经典的接口习惯对嵌入式 BSP 厂商的适配也最成熟。第二个理由是 5.14.2 的源码包在下载和编译层面非常规整。源码解压后直接就是一个 qt-everywhere-src 结构你只需要关心 qtbase、qtdeclarative、qtmultimedia 等真正用到的模块不需要像更老版本那样还得分模块单独下载。配合 -skip 参数还能把不需要的模块直接跳过缩短整体编译时间。第三个理由是静态编译在嵌入式交付场景里的优势太明显。目标板的 rootfs 经常是裁剪过的缺 libicu、缺 libxcb 这类库是常态。把 Qt 主体编译成静态库最终可执行文件里就带了 Qt 自身的实现只需要目标系统提供最基本的 glibc 和内核接口稳定性大幅提升。提示5.14 已经进入 EOL 阶段如果是新立项项目更推荐基于 5.15 或者 6.x 做评估。但存量项目的组件维护上5.14.2 仍是性价比很高的选择。1.2 静态编译的边界哪些能静态哪些不能“静态编译”这个词经常被理解成“完全不依赖任何动态库”这在真实场景里很难实现。以 glibc 为例虽然 gcc 可以通过 -static 让应用静态链接 glibc但静态 glibc 在域名解析、NSS 用户查询、locale 切换上有一堆隐藏坑很多路由器和嵌入式设备上还会因为静态 glibc 的 __nss 配置出现问题。所以更合理的做法是Qt 库本身静态编译C/C 运行时库看情况静态链接系统级的关键库维持动态链接。Qt 的 configure 里 -static 参数负责前者链接器的 -static-libgcc -static-libstdc 负责后者。这样最后生成的二进制文件Qt 代码全在里面但又不会因为把 glibc 硬塞进去导致运行时 DNS 解析异常。我实际测试下来最终产物的体积会从原本带一堆 .so 的几十 MB变成一个 3~8 MB 的单文件这对无盘启动、SD 卡部署、OTA 在线升级都非常友好。1.3 目标板运行环境分析编译前先搞清楚目标板的环境比直接敲 configure 重要得多。至少需要确认三件事CPU 架构是 armv8-a 还是 armv8.2-a这决定了 gcc 的 -march 选择rootfs 是 glibc 还是 musl这决定了运行时是否可以用静态链接显示设备是 LCD framebuffer 还是带 GPU 的 DRM/KMS 节点这决定了 QPA 插件选 linuxfb 还是 eglfs。拿 linuxfb 来说它不依赖 GPU 驱动只要内核里打开了 /dev/fb0 节点就能跑这对很多工业级板卡来说是最稳的兜底方案。如果板子带 Mali 或 PowerVR GPU并且你手里有对应的闭源驱动库那 eglfs 的显示效果会更好但编译复杂度高一个量级。我的经验是第一步先用 linuxfb 跑通全流程后续再考虑 eglfs 优化。2. 交叉编译环境准备2.1 交叉编译工具链的安装与验证工具链是整套流程的地基。这里我选用 Ubuntu 软件源里自带的 gcc-aarch64-linux-gnu版本是 9.x虽然不算新但胜在稳定、无需额外配置和 Qt 5.14.2 的兼容性也已经被大量项目验证过。安装命令很简单sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu装完建议立刻检查两个关键文件是否存在aarch64-linux-gnu-gcc -v aarch64-linux-gnu-g -v如果提示找不到可以检查一下 /usr/bin/ 目录下有没有 aarch64-linux-gnu-gcc-9 这类带版本号的符号链接把它们软链成不带版本号的名称就行。另外有条件的建议安装 gdb-multiarch后面调试目标板程序会用到。工具链自带的 sysroot 位于 /usr/aarch64-linux-gnu/ 目录里面包含了最基本的 C/C 库和头文件。编译 Qt 时Qt 的 configure 脚本会尝试检测这个路径下的内容所以建议先看看里面有没有 libuuid、libdrm 等头文件。如果没有后边配置的时候直接用 -no-* 或 -qt-* 参数绕开相关功能避免 configure 半路报错。2.2 第三方依赖库的准备Qt 除了自身的模块还会依赖一些第三方的库比如 zlib、libpng、libjpeg、sqlite、openssl。在交叉编译的场景下这些库的处理方式有两种一是让 Qt 源码内嵌编译configure 参数 -qt-zlib、-qt-libpng 这种二是预先编译静态库放到 sysroot 里再让 Qt 去链接。我更推荐第一种方式。Qt 源码包里本身就携带了这些库的源码副本位于 qtbase/src/3rdparty/用 -qt-* 参数可以直接把它们编入 Qt 静态库省去单独编译的麻烦也避免版本不匹配导致的各种诡异问题。只有 openssl 比较特殊一个是 Qt 源码里自带的版本通常偏老另一个是 openssl 的功能和目标板的系统安全策略有关系所以一般单独用工具链编译出静态 libssl.a、libcrypto.a然后通过 -openssl-linked 让 Qt 去链接。如果非要自己编译 openssl参考命令./Configure linux-aarch64 no-shared --prefix/opt/ssl-aarch64-static make -j$(nproc) make install这里注意 Configure 的参数是 linux-aarch64不是 linux-armv4编译完会生成 libssl.a 和 libcrypto.a。后续 Qt configure 时需要通过 OPENSSL_LIBS 手动指定库路径。2.3 目录规划与 sysroot 思路交叉编译容易搞乱的主要原因是把目标板的库和主机 x86_64 的库混在一起。为了避免这种混乱我习惯先固定一套目录结构源码目录/opt/src/qt-everywhere-src-5.14.2Qt 安装前缀/opt/qt-aarch64-static第三方库编译输出/opt/aarch64-libs在 configure 里 -prefix 指定安装目录后Qt 的 qmake、头文件、库文件都会统一装到这个目录下后续编译应用时只需要依赖这个目录即可。sysroot 的概念可以简单理解成“目标板文件系统的一个切片”工具链编译任何程序时头文件和库的搜索路径都指向这个切片而不是主机的 /usr/include 和 /usr/lib。验证工具链 sysroot 是否正确的快速命令echo #include stdio.h int main(){puts(ok);return 0;} | aarch64-linux-gnu-gcc -x c - -o /tmp/test-arm file /tmp/test-arm如果输出是 “ELF 64-bit LSB executable, ARM aarch64” 就说明工具链工作正常。3. Qt 源码配置与编译实操3.1 解压源码并准备 mkspecs 配置从 Qt 官网下载 qt-everywhere-src-5.14.2.tar.xz解压后先别急着执行 configure先检查一个关键目录qtbase/mkspecs/ 下有没有 linux-aarch64-gnu-g。理论上 Qt 5.14 自带了这个文件但某些裁剪过的源码包或者从镜像站下载的版本可能没有。如果没有最快的方式是复制 linux-arm-gnueabi-g 这个目录再改名cp -r qtbase/mkspecs/linux-arm-gnueabi-g qtbase/mkspecs/linux-aarch64-gnu-g然后修改 qmake.conf把编译器前缀改成 aarch64-linux-gnu-。这一步的本质是告诉 Qt目标板的 C/C 编译器是哪个、链接器是哪个、目标架构的参数是什么。改错一个字母后边编译出来的库就可能在链接阶段报 “unknown target” 或者 “selected processor does not support” 之类的问题。修改完成后建议额外在 qmake.conf 中加上静态链接器选项QMAKE_LFLAGS -static-libgcc -static-libstdc这样后面给 Qt 写代码时就不需要每个工程都手动加这两个标志了。3.2 configure 完整命令逐段拆解配置命令是全流程的核心。下面给出我实际使用的一套完整命令每一段的关键参数都做了说明。cd /opt/src/qt-everywhere-src-5.14.2 ./configure \ -prefix /opt/qt-aarch64-static \ -xplatform linux-aarch64-gnu-g \ -static \ -release \ -no-opengl \ -no-gtk \ -no-xcb \ -linuxfb \ -qpa linuxfb \ -qt-zlib \ -qt-libpng \ -qt-libjpeg \ -qt-sqlite \ -sql-sqlite \ -no-dbus \ -no-icu \ -nomake examples \ -nomake tests \ -nomake tools \ -skip qt3d \ -skip qtcanvas3d \ -skip qtdeclarative \ -skip qtdoc \ -skip qtlocation \ -skip qtmultimedia \ -skip qtsensors \ -skip qtserialbus \ -skip qtsvg \ -skip qttools \ -skip qtwayland \ -skip qtwebengine \ -skip qtwebsockets \ -skip qtxmlpatterns \ -opensource \ -confirm-license几个容易踩坑的参数逐个说-xplatform指定目标平台配置必须和你准备的 mkspecs 目录名字一致。如果写成 linux-generic-g编译器路径需要手动通过 qmake.conf 指定反而更绕。-static让 Qt 库编译成静态 lib 而不是动态 .so。这是整条链路的核心目标后面编译应用程序时如果发现 Qt 库还是以 .so 形式链接多半就是这个参数没有生效。-no-opengl -no-xcb -linuxfb组合是为了让 Qt 的 QPA 平台插件落到 framebuffer 上。Qt 5.14 的 QPA 默认会因为找不到 OpenGL 头文件而编译失败显式禁用能规避这个问题。如果你的板子需要 eglfs则需要换成 -eglfs并且还要额外配置 GPU 厂商提供的 EGL/GLES 库复杂度完全不在一个量级。-qt-zlib -qt-libpng 等参数让 Qt 使用内置的第三方库源码省去单独交叉编译这些库的环节。如果目标板系统里有特定版本要求可以不用这些参数改为自己编译后通过 -I 和 -L 指定路径。-skip 系列参数用于跳过不需要的 Qt 模块。特别是 qtwebengine这个模块体积大、编译时间长而且依赖大量系统库静态编译几乎不可能直接跳过是明智的选择。3.3 常见 configure 报错与解决configure 阶段最容易闪退的是 ICU 检测。Qt 默认会尝试链接 libicu如果 sysroot 里没有对应的头文件和库就会报错。 -no-icu 参数可以规避。但对字符编码处理要求比较高的项目比如需要处理 GBK 历史数据建议还是先用工具链编译一个静态 icu再让 Qt 链接这样 QTextCodec 的功能更完整。另一个高频问题是 “The specified target architecture is not recognized”。这通常是因为 mkspecs 目录里的 qmake.conf 写错了架构名或者 -xplatform 参数与实际目录名不匹配。解决办法是回到 3.1 节确认目录名逐字对比。configure 成功后会生成 config.summary建议把输出保存下来做备份。后续如果出现莫名其妙的链接错误回头核对 config.summary 里的 feature 开启情况往往能直接定位。3.4 make 编译的并行度与时间预估configure 通过后进入编译阶段make -j$(nproc)这里要给个忠告不要无脑把 -j 拉满。Qt 全量编译对内存的消耗非常夸张我用的是 64 GB 内存的机器-j16 编译时峰值占用能到 20 GB 以上。如果你的机器只有 8 GB 内存建议 -j4 甚至 -j2否则编译中途会因为 OOM 直接崩掉前功尽弃。整个编译时长取决于机器性能和剪裁程度。我这边裁剪掉大部分模块后单 qmake qtbase qtdeclarative 大约需要一小时左右。如果是完整编译4 小时起步是正常的。编译过程中如果看到颜色警告、格式提示这类输出不用紧张那是编译器在打印警告信息不影响最终产物。真正的错误一般是带 “error:” 前缀并且会在最后一次 console output 中显示具体文件路径。这里强烈建议保留完整构建日志格式如下./configure /tmp/qt_configure.log 21 make /tmp/qt_make.log 21出问题时直接 grep “error” 就能看到所有异常点。3.5 安装与产物验证编译完成后执行安装make install安装完成后检查一下安装目录结构ls /opt/qt-aarch64-static/lib/libQt5Core.a ls /opt/qt-aarch64-static/plugins/platforms/libqlinuxfb.a这两条命令的结果分别是 Qt5Core 静态库文件和 linuxfb 平台插件的静态归档只有看到这两种文件说明静态编译基本成功。再用官方自带的最小工程验证一次cat EOF /tmp/test.pro QT widgets SOURCES main.cpp EOFmain.cpp 写一个最简单的 QApplication QLabel 显示。回到 /tmp 后执行/opt/qt-aarch64-static/bin/qmake test.pro make file test如果 file test 输出显示 aarch64 ELF 且动态依赖几乎没有只依赖 ld-linux-aarch64.so.1 和 libc.so.6 之类的系统库那就说明全链路已经通了。4. 部署到目标板与常见问题排查4.1 目标板运行黑屏的排查路径程序拷到目标板执行后如果出现黑屏或没有任何窗口第一大嫌疑就是 framebuffer 设备没对上。linuxfb 插件默认使用 /dev/fb0但有些板子的 LCD 节点是 /dev/fb1。可以先用板上的 shell 确认ls /dev/fb*如果确实不是 /dev/fb0运行时指定./test -platform linuxfb:fb/dev/fb1第二嫌疑是权限不够。部分板子的 /dev/fb0 权限是 root:video普通用户无法直接读写。要么把执行用户加进 video 组要么直接 root 运行测试业务上再按最小权限原则收拢。字体渲染黑屏是另一类高频问题。linuxfb 下 Qt 默认走 fontconfig但精简单板系统里可能没有 fontconfig 和字体文件。即使你的程序没有显式显示文字Qt 内部也会加载默认字体加载失败时窗口内容就全部空白。简单粗暴的解决办法是把一个 .ttf 字体文件放到目标板 /usr/share/fonts/ 下或者在运行时通过环境变量指到字体目录export QT_QPA_FONTDIR/usr/share/fonts/4.2 编译阶段的常见报错速查表整理一下整个过程中最容易碰到的几个问题以及对应的定位思路症状概率原因解决办法configure 报 “Host machine does not support pkg-config”sysroot 缺少 pkg-config 文件安装 pkg-config-aarch64-linux-gnu 或设置 PKG_CONFIG_LIBDIR链接报找不到 “-lGL”没禁用 OpenGL确认 configure 里 -no-opengl 已经生效链接报 “cannot find -lstdc”qmake.conf 中静态库标志没加成在 qmake.conf 中补 QMAKE_LFLAGS运行时提示 “qt.qpa.plugin: Could not find the Qt platform plugin”QPA 插件未部署确认 /opt/qt-aarch64-static/plugins/platforms/ 下有没有 libqlinuxfb.a或者运行时用 -platform linuxfb 强制指定程序一启动就 SIGILL工具链 -march 参数比目标板 CPU 特性高检查 qmake.conf 中 QMAKE_CFLAGS 是否加了 -mcpu 或 -march4.3 静态链接的一个特殊坑Qt 模块初始化丢失最后分享一个特别容易忽略的坑。Qt 的某些模块比如 QtPlugin 或者图像格式插件依赖静态注册机制如果把它编成了静态库而你在 main 函数里没有调用对应的 Q_IMPORT_PLUGIN链接器会因为“符号被优化掉”而把插件代码从最终二进制中剔除运行时就会出现“no such file or directory”或者“plugin is not loaded”的错误。解决办法有两个要么在代码里显式导入插件#include QtPlugin Q_IMPORT_PLUGIN(QGifPlugin) Q_IMPORT_PLUGIN(QJpegPlugin) Q_IMPORT_PLUGIN(QSqliteDriverPlugin)要么在 .pro 文件里通过 QTPLUGIN 声明需要链接的插件。这个问题最容易出现在用动态版本调试正常、切到静态后功能突然消失的场景。另外一个场景是 openssl。如果你在 configure 里开启 -openssl-linked编译时也链接了 libssl.a、libcrypto.a但程序跑起来后 HTTPS 请求失败先查一下目标板 /etc/ssl/certs/ 下有没有 ca-certificates.crt。静态链接的 openssl 不会自动带上证书路径这是纯操作系统层面的配置问题跟我们按 Qt 本身无关。4.4 编译产物瘦身与小技巧静态编译的大体积是绕不开的话题。同一套代码动态编译产物可能不到 1 MB静态编译直接到 8 MB。对带宽和存储不敏感的场景可以不用管但如果要放进 boot 分区就需要处理一下。一个有效的手段是用 strip 去掉符号表aarch64-linux-gnu-strip test另一个需要关注的选项是编译优化级别。configure 默认的 -release 模式已经开了 -O2如果还想再激进一点可以在 qmake.conf 里改成 -O3但收益通常只在 CPU 密集场景才有感知建议先不折腾。还有一点编入 Qt 静态库的依赖项比如 zlib、sqlite如果目标板系统里也已经有了这些动态库那么静态程序运行时会优先使用系统动态库版本还是使用内置的静态版本这取决于链接顺序。实际测试中 Qt 静态库会优先使用自己内置的实现这也是为什么 -qt-zlib 能大幅降低对目标板环境的依赖。5. 从这套方案里沉淀下来的经验回头再看整个从零搭建的过程真正的难点其实不是参数怎么敲而是工程化思维提前确认目标板环境剪裁模块规范目录保存日志每一步都留好后路。Qt 交叉编译这种活最难的不是第一次跑通而是换了块板子、换了个工具链版本后你还知道自己当时是怎么弄出来的。我个人的做法是把整个 configure 命令、mkspecs 改动、依赖库编译命令全部写进一个 setup.sh 脚本丢到项目仓库里。下一次适配新板子只需要改改编译器前缀和目标架构参数就能在半小时内完成一次干净的构建。交叉编译这个事情没有太多玄学按照版本、工具链、配置参数、部署调试这四个维度逐层推进走通一次之后后面的板子适配基本都是复制粘贴加微调。希望这份手册能帮你少走一些我踩过的坑。
返回列表