早年间第一次在一台 arm64 开发板上部署 Qt 程序,我干过最蠢的一件事:把整棵/usr/lib/aarch64-linux-gnu/qt5目录从板子上另一台“同款”设备拷过去,然后对着“cannot find libQt5XcbQpa.so.5”折腾到半夜。后来换了思路,彻底改走静态交叉编译路线:在 x86 的编译机上用aarch64-linux-gnu-g++交叉工具链把 Qt 5.14.2 做成.a静态库,再链接出单个可执行文件,拷到板子上直接跑,之后再也没被 Qt 的动态库依赖折磨过。
这篇文章就是那套流程的完整复盘。核心是:Ubuntu 20.04 主机上交叉编译 Qt 5.14.2,目标平台 aarch64(即 ARM64),全静态链接。我不只给命令,还会把 configure 里每个参数为什么这么传、静态链接时插件为什么要手动导入、为什么最终二进制体积这么大、目标板 glibc 版本不匹配怎么排查,全部说清楚。适合刚接触交叉编译的读者,也适合被动态库版本地狱搞烦了、想换一种部署方式的嵌入式老人。
1. 为什么是 Qt 5.14.2、aarch64 和静态交叉编译
1.1 版本选择背后的考量
先解释选型。Qt 5.14.2 不是最新版,但做交叉编译,稳定压倒一切。5.14 这个分支的 configure 和 mkspec 对 aarch64 的支持已经完全成熟,网上能查到的踩坑记录也最多,真遇到问题,搜一搜基本都有答案。5.12 是更老的 LTS,也能用,但一些模块的 bug 修复和 aarch64 相关补丁不如 5.14 完整。5.15 后续补丁版本对开源用户来说拿不全,6.x 的构建系统变化又比较大,如果你不是非追新不可,选 5.14.2 是风险最低的方案。
另外一点是这个版本对静态构建特别友好。它的静态库构建路径经过了大量验证,第三方库(zlib、libpng、libjpeg、freetype、harfbuzz、pcre)都可以选择用 Qt 源码树内自带的版本,不需要在 sysroot 里逐个装 ARM64 的开发包,这能省掉一大半环境问题。对于“从零搭建”这件事来说,少一个外部依赖就少一分失败概率。
1.2 静态交叉编译到底解决什么问题
静态和动态的核心区别一句话就能讲明白:动态方案里,可执行文件只记下“我需要 libQt5Widgets.so.5”,运行时再去系统里找;静态方案里,Qt 库的代码在链接阶段直接焊进你的可执行文件,运行时不再找 Qt 的动态库。
在 aarch64 嵌入式场景下,动态方案的痛苦非常典型:
- 板子存储小,但 Qt 动态库动不动几百 MB,加插件更多。
- 每次换板子都要对 Qt 库版本和编译器版本,
cannot find -lQt5XcbQpa.so.5这类报错能追到怀疑人生。 - 动态插件机制在裁剪过的 rootfs 上经常失效,平台插件找不到,程序直接黑屏退出。
- 生产板子往往没有网络,没法临时
apt install补依赖。
静态方案把这些问题整个绕开:最终交付物就是单文件二进制,拷贝即运行。代价是体积大(一个 Qt Widgets 静态程序大概 15~30 MB,strip 之后会小一些)和编译时间长(全量编译一两个小时很正常)。对“部署简单、环境干净”有硬要求的项目,这笔交换是值的。
1.3 这套手册适合谁
严格来说,这套流程适合三类人:第一类,目标是 ARM64 Linux 板子,板子跑精简 rootfs,没有条件装 Qt 运行环境;第二类,程序功能相对固定,不需要在板子上频繁加载外部 Qt 插件,也不需要做动态更新的二次开发;第三类,做交付型项目,希望把“环境搭建”这个不可控因素从交付清单里划掉的人。
反过来,如果你需要在板子上长期做二次开发,每次改代码都重新全量编译一个几十 MB 的静态程序,或者你的程序要按设备动态加载第三方插件,那还是老老实实走动态方案。静态不是银弹,它是用编译期的复杂换运行期的简单。
2. 开始前的环境准备:主机、工具链与 sysroot
2.1 主机环境与基础软件
我这套流程在 Ubuntu 20.04 x86_64 上完整验证过,18.04 也可以,再老的版本建议先升级。主机先装基础工具:
sudo apt update sudo apt install -y build-essential perl wget gitbuild-essential提供宿主机的gcc、g++、make,Qt 的 configure 脚本还要依赖perl,少一个都会在奇怪的地方报错。顺手把git装上,后面拉补丁或者做版本管理工作方便。
这里有个常见的坑:很多人以为交叉编译只需要交叉工具链,结果 configure 一开始就报“The specified compiler is not available”或者make找不到,其实是因为宿主编译环境都没装齐。Qt 构建过程中有一部分工具(moc、rcc、uic、qmake)必须在编译机上运行,它们是用宿主编译器先编译出来的,所以宿主机工具链同样必需。
2.2 安装 aarch64 交叉编译工具链
Ubuntu 软件源里现成的交叉工具链就很够用:
sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu安装完验证一下:
aarch64-linux-gnu-gcc --version # 通常在 9.3.0 或更新版本这个工具链的名字要记清楚:以aarch64-linux-gnu-为前缀。对应 Qt mkspec 里默认的编译器名就是aarch64-linux-gnu-gcc和aarch64-linux-gnu-g++,所以用系统包不会出现“找不到编译器”的问题。这个包会连带安装 ARM64 的 libc、libstdc++、libatomic 等运行库,默认 sysroot 在/usr/aarch64-linux-gnu目录下,这点后面会用到。
如果你的板子处理器比较特殊,比如需要针对某些 ARM 特性做优化,可以用 Linaro 的工具链或者自己编的 GCC,但那属于进阶玩法,这次先不展开。
2.3 sysroot 的三种做法
sysroot 是交叉编译里最容易懵的概念。说白了,它就是“目标板根文件系统的裁剪版”,里面装着目标板上的头文件和库文件,交叉编译器编译时拿它当/usr和/lib的根。有三种常见做法:
第一种,直接用工具链自带的默认 sysroot。Ubuntu 的gcc-aarch64-linux-gnu包在/usr/aarch64-linux-gnu下自带了一套 ARM64 的 libc/libstdc++ 头文件和库,大多数情况够用。本手册的配置里,我们刻意关掉了 xcb、OpenGL、DBus、ICU 这些依赖外部库的功能,所有图像、字体、压缩库都用 Qt 源码自带的,所以外部的 ARM64 依赖只剩 glibc 和 libstdc++,工具链自带的 sysroot 就足够了。
第二种,从目标板把真实运行环境 rsync 出来。当你的程序需要链接板子上特有的、工具链 sysroot 里没有的库时,就得用这个办法:
mkdir -p sysroot/lib sysroot/usr/lib sysroot/usr/include rsync -avz root@<board_ip>:/lib/ ./sysroot/lib/ rsync -avz root@<board_ip>:/usr/lib/ ./sysroot/usr/lib/ rsync -avz --safe-links root@<board_ip>:/usr/include/ ./sysroot/usr/include/注意保持符号链接,rsync 不要加-L,否则一些相对路径的软链会被破坏。然后用--sysroot=/path/to/sysroot指定给编译器。这个方案的好处是和目标板完全一致,坏处是每次板子更新系统都要重新同步。
第三种,用 Buildroot 或 Yocto 生成的 SDK sysroot。项目本身用 Buildroot/Yocto 管理的,直接导出它们的 toolchain 即可,干净、可复现。本手册不依赖它,但如果你的生产构建体系已经在用,直接替换工具链部分就行。
3. 下载源码与 configure 参数决策
3.1 获取 Qt 5.14.2 源码
源码从 Qt 官方 archive 下载,文件名很关键,一定要是qt-everywhere-src而不是qt-everywhere-opensource-src(后者是老版本命名):
wget https://download.qt.io/archive/qt/5.14/5.14.2/qt-everywhere-src-5.14.2.tar.xz tar -xJf qt-everywhere-src-5.14.2.tar.xz cd qt-everywhere-src-5.14.2解压完先别急着 configure,看一下目录里的qtbase/mkspecs/linux-aarch64-gnu-g++这个目录是否存在。Qt 对 aarch64 的支持在 mkspec 层面已经集成了,确认它在,后面用-xplatform linux-aarch64-gnu-g++就不会出“mkspec 不存在”的问题。
3.2 模块裁剪:先砍掉不必要的重量级模块
Qt 5.14.2 完整源码包包含几十个模块。交叉编译静态构建时,如果全部编译,一是耗时长,二是一堆模块根本用不上,三是一些模块在静态交叉场景下依赖特别多、特别容易失败。最典型的就是qtwebengine,它在 aarch64 静态构建下基本是噩梦,直接跳过。
我的建议是只保留核心模块。qtbase是核心,必须留;qtserialport、qtsvg、qtimageformats这类轻量的按需保留;qtwebengine、qtwebview、qt3d、qtcharts、qtdatavis3d、qtmultimedia、qtscript、qtvirtualkeyboard、qtwayland这套重模块,没有明确需求就直接-skip掉。
这里特别提醒一句:很多人配置时图省事,把所有模块都留着,结果编译到一半卡在某个模块的依赖上报错,回头再查 configure 日志,发现问题根本不在 Qt 本身,而是某个可选模块要的库没在 sysroot 里。裁剪模块不是为了省硬盘,是为了把构建成功的概率拉高。
3.3 逐个拆解 configure 关键参数
configure 是整个流程里信息量最大的一步。我把关键参数按组拆开解释,这样你后面按需调整时知道自己在动什么。
-platform linux-g++指定的是编译机上运行的工具链。-xplatform linux-aarch64-gnu-g++指定的是目标平台工具链。两者必须同时给对——-platform决定 qmake、moc、rcc、uic 这些构建期工具用什么编译器生成,-xplatform决定真正的 Qt 库和目标二进制用什么编译器。搞反或者漏掉一个,轻则工具无法在主机上运行,重则编出一堆 x86 的“伪交叉”产物。
-static让 Qt 库以.a静态库形式产出。这里有个容易误解的点:这个参数只决定 Qt 库本身的形态,并不强制最后链接出的应用程序一定是全静态。最终链接时,你还可以选择只静态 Qt 库而保留动态 glibc,这个策略差异我们放到第 5 节细讲。
-release -opensource -confirm-license是常规交代。-release去掉调试信息,体积和速度都更友好;-opensource -confirm-license接受开源协议,省得 configure 交互式问你。
第三方库相关的参数是本手册默认配置的核心:
-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz -qt-pcre这组参数的意思是“用 Qt 源码里自带的第三方库,不要到 sysroot 里找系统库”。交叉编译最怕的是每个依赖都要为 ARM64 单独编译一份,有了这组参数,zlib、libpng、libjpeg、freetype、harfbuzz、pcre 全部由 Qt 自己构建,sysroot 里缺什么都不影响,这是从零搭建能成功的关键。
功能裁剪参数是另一组:
-no-opengl -no-xcb -no-dbus -no-icu -no-glib -no-fontconfig-no-opengl关闭 OpenGL/EGL,因为静态编译默认不需要 GPU 渲染的 framebuffer 应用用不到;-no-xcb关闭 X11 客户端支持;-no-dbus关闭 D-Bus,嵌入式 rootfs 十有八九没跑 D-Bus,不关的话 configure 会尝试找 dbus 头文件;-no-icu关闭 ICU 文本库,省掉一个大依赖;-no-glib避免和 glib 发生关系;-no-fontconfig关闭系统字体配置服务,字体问题我们在第 6 节单独解决。
这组参数换来的是“外部依赖只剩 glibc/libstdc++”的干净状态。如果你的板子确实需要 xcb 或 OpenGL,那 sysroot 要重新准备,不能照抄这组配置,这点后面常见问题里再展开。
4. 执行编译与安装
4.1 完整的 configure 命令
把前面的决策落到命令上,完整配置如下。先创建安装目录,再执行 configure:
sudo mkdir -p /opt/qt-aarch64 sudo chown -R $USER /opt/qt-aarch64 cd qt-everywhere-src-5.14.2 ./configure \ -platform linux-g++ \ -xplatform linux-aarch64-gnu-g++ \ -prefix /opt/qt-aarch64 \ -static \ -release \ -opensource \ -confirm-license \ -nomake examples \ -nomake tests \ -skip qtwebengine \ -skip qtwebview \ -skip qt3d \ -skip qtcharts \ -skip qtdatavis3d \ -skip qtlocation \ -skip qtmultimedia \ -skip qtpurchasing \ -skip qtscript \ -skip qtsensors \ -skip qtspeech \ -skip qttools \ -skip qtvirtualkeyboard \ -skip qtwayland \ -skip qtwebchannel \ -skip qtwebsockets \ -no-opengl \ -no-xcb \ -no-dbus \ -no-icu \ -no-glib \ -no-fontconfig \ -qt-zlib \ -qt-libpng \ -qt-libjpeg \ -qt-freetype \ -qt-harfbuzz \ -qt-pcreconfigure 跑完会输出一大段检测结果。重点看这几个字段:FreeType: yes (bundled copy)、FontConfig: no、ICU: no、Xcb: no。如果看到和预期不一致的项,先别急着 make,回头调整参数。configure 失败时,去config.log里搜error:,大部分原因一眼就能看出来。
4.2 并行编译与内存控制
configure 通过后开始编译:
make -j4-j并发数不要盲目拉满。Qt 静态全量编译非常吃内存,8 GB 内存的机器用-j4比较稳,16 GB 可以试-j8。我见过有人用-j$(nproc)在 8 核心机器上编译,跑到一半进程被内核 OOM 杀掉,白白浪费一小时。用-j4慢是慢点,但能一次跑完。
编译中途如果make报错,修完问题后重新执行同样的make -j4即可,Qt 的构建系统支持断点续编,不需要重新 configure。
整体耗时和机器配置强相关。8 核心 16 GB 内存的机器,按上面的裁剪方案大概 40 到 60 分钟;4 核心老机器可能要两小时。建议编译期间不要跑大型 GUI 程序,免得把内存顶爆。
4.3 安装产物验证
编译完成后安装:
make install装完做两个验证,确认交叉编译结果没搞反:
file /opt/qt-aarch64/bin/qmake # 输出: ELF 64-bit LSB executable, x86-64 # qmake 必须能在 x86 主机上运行 file /opt/qt-aarch64/lib/libQt5Widgets.a # 输出: ELF 64-bit LSB relocatable, ARM aarch64 # 静态库必须是指向 aarch64 的目标文件这个对照很重要:bin/qmake是 x86-64,因为它要在编译机上执行,负责替目标程序生成 Makefile;而lib/下的.a库则是 aarch64 的目标文件,是给目标板用的。如果你看到libQt5Widgets.a显示 x86-64,说明-platform和-xplatform传反了,马上回头重配,别继续往下走。
注意这里make install不需要INSTALL_ROOT,因为安装路径就是-prefix指定的/opt/qt-aarch64,直接装进宿主机目录。等目标板部署时,我们只拷贝最终的可执行文件,不拷贝整棵 Qt 树。
5. 交叉编译第一个静态 Qt 程序
5.1 示例工程与静态插件导入
Qt 库编成静态之后,有一个和动态方案截然不同的关键操作:手动导入插件。动态方案下,Qt 在运行时根据配置去插件目录找libqlinuxfb.so;静态方案下没有这个查找过程,所有插件代码必须在链接时进入可执行文件,这就要靠Q_IMPORT_PLUGIN宏。
建立一个示例工程目录:
mkdir -p ~/hello && cd ~/hellohello.pro文件:
QT += core gui widgets CONFIG += static TARGET = hello TEMPLATE = app SOURCES += main.cpp QTPLUGIN += qlinuxfb qoffscreen qminimalmain.cpp文件:
#include <QApplication> #include <QLabel> #include <QtPlugin> Q_IMPORT_PLUGIN(QMinimalIntegrationPlugin) Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) Q_IMPORT_PLUGIN(QOffscreenIntegrationPlugin) int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello, aarch64 static Qt!"); label.resize(360, 120); label.show(); return app.exec(); }.pro里的QTPLUGIN += qlinuxfb qoffscreen qminimal让 qmake 去链接这三个平台插件的静态库,Q_IMPORT_PLUGIN宏负责把插件的初始化代码拉进来。少了Q_IMPORT_PLUGIN,程序运行时会报“could not find the Qt platform plugin”,这是个非常典型的静态编译坑。
5.2 编译步骤与链接选项
用交叉环境下的 qmake 生成 Makefile,然后编译:
export PATH=/opt/qt-aarch64/bin:$PATH /opt/qt-aarch64/bin/qmake hello.pro make如果链接阶段报cannot find -latomic,说明 aarch64 的 libatomic 静态库没装:
sudo apt install libatomic1-arm64-cross或者在.pro里直接加一行:
QMAKE_LIBS += -latomiclibatomic是 aarch64 上比较常见的一个隐藏依赖,某些原子操作需要它,很多人第一次编完 Qt 却在链接 app 时翻车,基本都是这个原因。
关于最终链接方式,这里说清楚三种选择。第一种是“Qt 静态 + 系统库动态”,也就是默认行为:不额外加链接参数,Qt 的.a会进入二进制,而 libc、libstdc++ 保持动态链接。第二种是半静态,在.pro里加QMAKE_LFLAGS += -static-libgcc -static-libstdc++,把 C/C++ 运行库也打成静态,只剩 glibc 的动态部分。第三种是全静态,加QMAKE_LFLAGS += -static,glibc 也静态链入。
全静态看起来最美,但 glibc 静态链接有边界问题:getaddrinfo、NSS 这些需要运行时动态打开.so的能力在静态链接下会失效,程序跑起来会打印类似“Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries”的警告,涉及网络的程序可能直接解析不了域名。所以我个人最推荐第二种,Qt 静态、C/C++ 运行库静态,但保留 glibc 动态。目标板几乎都带 glibc,兼容性最好,部署只依赖 libc 一个动态库,风险极低。
5.3 用 file 和 readelf 验证静态属性
编译出的二进制在编译机上无法直接运行,但可以先验证结构。file告诉你架构,readelf告诉你动态依赖:
file hello # 输出: ELF 64-bit LSB executable, ARM aarch64 aarch64-linux-gnu-readelf -d hello | grep NEEDED # 默认方式下会看到: libstdc++.so.6, libgcc_s.so.1, libc.so.6, libm.so.6 # 如果加了 -static,这里应该一行 NEEDED 都没有看到ARM aarch64字样,说明编译目标正确;看NEEDED列表,确认没有libQt5Widgets.so.5之类的 Qt 动态依赖,就说明 Qt 已经成功焊进二进制了。体积上,一个带 Widgets 的静态程序大概 15~30 MB,strip 之后能压到 8~15 MB:
aarch64-linux-gnu-strip hello ls -lh hello这个体积在大容量 SD 卡或 eMMC 上完全不是问题。如果你后续要追求极致体积,可以再研究 UPX 压缩,但生产环境我并不推荐 UPX,它会拖慢程序启动,节省的那几 MB 不值得。
6. 部署到目标板:从拷贝到稳定运行
6.1 最简单的部署命令
部署其实就是传一个文件过去:
scp hello root@<board_ip>:/home/root/ ssh root@<board_ip> chmod +x /home/root/hello /home/root/hello -platform linuxfb-platform linuxfb走 Linux framebuffer,直接操作/dev/fb0,不需要 X11、不需要 Wayland,这是精简嵌入式板子最常见的 GUI 运行方式。如果板子没有接显示器或者没有/dev/fb0,可以先用-platform offscreen验证程序能起来,它不依赖任何显示设备。
如果你的程序既有 GUI 又需要无人值守启动,可以在程序里硬编码平台插件,这样连-platform参数都不用带。办法是在 main 函数里调用qputenv("QT_QPA_PLATFORM", "linuxfb"),但注意必须在创建QApplication之前设置:
#include <QApplication> #include <QtPlugin> #include <QByteArray> Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) int main(int argc, char *argv[]) { qputenv("QT_QPA_PLATFORM", "linuxfb"); QApplication app(argc, argv); // ... }6.2 字体等数据资源的处理
因为 configure 时带了-no-fontconfig,目标系统上的字体配置服务完全不参与,Qt 不会自动去/usr/share/fonts搜索字体。如果程序里显示中文或特殊字形,界面上很可能直接出现方块。
解决办法很简单,把需要的字体文件拷到板子上,然后在程序里用QFontDatabase::addApplicationFont加载:
#include <QFontDatabase> int main(int argc, char *argv[]) { qputenv("QT_QPA_PLATFORM", "linuxfb"); QApplication app(argc, argv); QFontDatabase::addApplicationFont("/usr/share/fonts/wqy/wqy-zenhei.ttc"); QFont font("WenQuanYi Zen Hei"); font.setPixelSize(28); app.setFont(font); // ... }没有中文字体需求的话,复制一个DejaVuSans.ttf就够英文界面用了。这个细节在动态方案里基本不用操心,但在静态方案里它决定你看到的是正常文字还是满屏方块。
除了字体,还有两类资源容易漏。第一类是图片、样式表、QML 文件等数据文件,静态编译不会把这些文件嵌入二进制,除非你用了 Qt 资源系统.qrc;第二类是翻译文件.qm,同理。我的习惯是:所有静态二进制配套的数据资源,能塞进.qrc就塞进去,实在要塞外部文件的,在部署清单里单独列一行,防止交付时漏拷贝。
6.3 用 qemu 用户态在主机上先跑一遍
每次部署都往板子 scp 调试太慢了。一个高效的验证手段是用qemu-user在编译机上直接跑 ARM64 二进制:
sudo apt install -y qemu-user然后指定目标 libc 的路径执行:
qemu-aarch64 -L /usr/aarch64-linux-gnu ./hello -platform offscreen如果你的程序是全静态链接的,连-L都不用加:
qemu-aarch64 ./hello -platform offscreen看到窗口内容打印出来或者程序正常退出,说明这个二进制在 ARM64 的 ABI 层面上是健康的。qemu 和真实板子毕竟有差异,最终还是要上板验证,但日常调试阶段用这招能省很多来回拷贝的时间。注意 GUI 程序要用-platform offscreen,linuxfb 在 qemu 下没有/dev/fb0可用。
7. 常见问题与排查技巧实录
7.1 高频错误速查表
这套流程跑了很多次,真正高频的报错就那几十个。整理成速查表,按出现频率排序:
| 报错信息 | 原因 | 处理办法 |
|---|---|---|
cannot find -latomic或cannot find -lglib-2.0 | sysroot 缺少对应静态库 | 安装跨平台库包,如libatomic1-arm64-cross;glib 系库若没必要就-no-glib关掉 |
unknown module(s) in Qt: serialport | configure 时跳过了 qtserialport,或者使用的 Qt 构建里没有该模块 | 不要-skip对应模块重新构建;qtserialport是库模块,静态链接后直接在.pro里QT += serialport即可 |
Could not find the Qt platform plugin "linuxfb" | 静态构建下平台插件未链入可执行文件 | 在源码里Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin),并在.pro里加QTPLUGIN += qlinuxfb |
cannot find -lGL | OpenGL 相关功能开启,但 sysroot 没有 libGL | 确认 configure 带-no-opengl;确有需要时给 sysroot 装 mesa aarch64 静态库 |
qmake: ELF 64-bit LSB executable, x86-64但libQt5Core.a也是 x86-64 | -platform和-xplatform传反 | 用-platform linux-g++ -xplatform linux-aarch64-gnu-g++重配 |
error: cannot run C++ compiler或 configure 中途退出 | 缺少宿主机构建环境或交叉工具链未装 | 安装build-essential与g++-aarch64-linux-gnu |
make: g++: Command not found | 宿主 g++ 不存在 | sudo apt install build-essential |
编译进程被Killed | 内存不足,OOM | 降低-j并发,建议先关浏览器再编译 |
运行时报Illegal instruction | 编译器默认指令集与板子 CPU 不匹配 | 确认板子是否为 armv8-a,过老的板子考虑用老工具链或调整 mkspec 的 march 参数 |
7.2 运行时崩溃与插件相关排查
静态程序出现运行时崩溃,十有八九是插件导入不全,其次是系统资源路径不对。最典型的场景是:程序里只导入了qlinuxfb,却用-platform offscreen启动;或者反过来,界面程序依赖了QOffscreenIntegrationPlugin但忘了Q_IMPORT_PLUGIN,运行时报“No such plugin”。
遇到这类问题,先别急着怀疑 Qt 库本身,按这个顺序排查:
第一,确认启动参数。-platform后面跟的插件名必须在导入列表里。截图验证或者在 main 开头qputenv固定平台,这是最省事的做法。
第二,确认 sysroot 和链接参数。用readelf -d再看一遍NEEDED,如果出现libQt5Core.so.5这种 Qt 动态依赖,说明你链接的库不是我们刚编好的静态库,八成是系统里原来的 qmake 混进来了。务必用/opt/qt-aarch64/bin/qmake,别依赖环境变量里的旧版本。
第三,确认目标板 glibc 版本。用 qemu 用户态测过没问题的二进制,上板后如果报version GLIBC_2.32 not found,说明目标板 glibc 比工具链对应版本老。查内核和 glibc 版本用:
# 目标板上执行 ldd --version如果板子比较老,需要换一个旧版本的交叉工具链重新编译,这是交叉编译里最无奈但也最需要意识到的一个约束:编译机的新工具链,产出的二进制默认要求目标板具备相近或更新的系统库。
7.3 静态方案最容易踩的三个坑
第一个坑是盲目追求全静态。QMAKE_LFLAGS += -static全静态链接后,glibc 的getaddrinfo会需要运行时加载 NSS 模块,静态程序里没有这些模块,DNS 解析就废了。程序里只要用到QNetworkAccessManager,我都不建议全静态,用-static-libgcc -static-libstdc++的半静态方案最合适。如果必须全静态,考虑用 musl 工具链替代 glibc,那是另一套体系,本手册不展开。
第二个坑是只拷二进制不拷数据。静态编译只解决了代码依赖,不解决数据依赖。字体、图片、QSS、QML、翻译文件、配置文件,统统不会自动进来。要么用.qrc资源系统,要么在部署清单里逐项核对。我在项目里吃过这个亏——程序在编译机 qemu 下跑得好好的,上板后界面空白,查了半天发现是字体文件没拷过去。
第三个坑是 configure 参数没有存档。交叉编译最怕的是过两个月要重编一次,却想不起来当时用了哪些参数。我的习惯是把完整 configure 命令写成一个build-qt-aarch64.sh脚本,放到 Qt 源码目录外面,随时可以重新执行。换 Qt 版本时,把脚本里的版本号改一下,拿 diff 做对比,比翻笔记靠谱得多。
最后再分享一个这些年养成的习惯:每次构建都是在同一个干净目录里进行,不依赖~/.bashrc里的环境变量。交叉编译本来变量就多,把编译环境固化在脚本里,才能保证换台电脑也能一键重建。这套 Qt 5.14.2 静态交叉编译的环境,我现在基本能做到从零到出可执行文件,全程不用手动干预。按这份手册走一遍,你也能把“部署 Qt 程序”这件事,从最头疼的环节变成最放心的环节。