如果只是往 ARM 板上拷一个 Qt 程序就要连带拖上十几二十个 .so,再到现场发现库版本对不上、平台插件找不到、中文字体变方块,那静态交叉编译这趟路就值得走。这篇文章记录我用 Qt 5.14.2 给 aarch64(典型场景是鲲鹏、飞腾这类 ARMv8 平台)做静态交叉编译的完整过程,从工具链、sysroot 准备到 configure 参数逐项抠,最后把踩过的坑和排查思路一并列出来。想直接抄作业的人可以按步骤走,想弄明白每一步为什么这么干的人,后面也有足够的原理说明。
1. 为什么选择静态交叉编译:先看清需求边界
1.1 动态编译方案在 ARM 板子上暴露的问题
很多团队做 ARM 端 Qt 程序,第一个想到的方案是在目标板上直接安装 Qt 运行库,或者通过交叉编译生成动态库再挨个拷贝。这个做法在小规模原型验证时确实省事,但一旦多部署几台设备、换了几批板卡,问题就来了。
先说库依赖的问题。Qt 5.14.2 的动态库体系相当庞大,光基础模块就是 libQt5Core.so、libQt5Gui.so、libQt5Widgets.so 这一串,往后还有 network、sql、svg、qml 等模块。每次拷贝这些 .so 都要小心翼翼地对应版本,曾经出现过开发机上 Qt 版本是 5.14.2,目标板上却残留着 5.12 的旧库,程序一启动就报"cannot mix incompatible Qt library",排查起来非常痛苦。
再者是平台插件的问题。Qt 程序启动时必须要找到 platform 插件,常见的就是 libqlinuxfb.so 或者 libqeglfs.so。如果只拷了 Qt 运行库而漏了 plugins/platforms 目录,程序直接报"could not find the Qt platform plugin 'linuxfb'"然后退出。这类问题在动态部署时反复出现,几乎每个新手都要踩一遍。
还有一个隐藏问题——glibc 版本。在 aarch64 的某些国产化系统上,板子的操作系统版本可能比较旧,glibc 版本和开发机不一致,动态链接的程序在目标板上可能直接因为符号版本不匹配而拒绝运行。我用 CentOS 7.9 的 aarch64 环境编译出的动态程序,放到一个基于较为精简文件系统的 ARM 板子上,程序启动时找不到某些 GLIBC_XX 版本符号,这类问题比 Qt 库本身的问题更难查。
1.2 静态编译的收益和代价
静态交叉编译最直接的收益就是一个可执行文件打天下。将 Qt 核心库、第三方依赖库(zlib、libpng、openssl 等)全部静态链入最终产物,部署时不再依赖目标板上的任何 Qt 环境。这对量产设备、无人值守的场景极其友好——拷贝一个二进制文件,如果目标系统有基本的文件系统和 root 权限,程序就能跑起来。
但静态编译不是免费的午餐,有几个代价必须清楚:
- 可执行文件体积显著增大。一个最简单的 Qt Widgets 程序,动态编译大概 1MB 左右,静态编译轻松超过 30MB,如果加了 network 和 sql 模块,体积会更大。
- 静态编译可能导致某些插件无法正常加载。Qt 的插件机制默认是通过动态库加载的,静态编译时需要将插件显式编译进库中,并且用 Q_IMPORT_PLUGIN 宏在程序代码里手动导入。这一点最容易被忽略,后面我详细说。
- 许可证问题不能含糊。如果你使用 Qt 的开源版本(LGPLv3 / GPLv3),静态链接就涉及到 LGPL 合规问题——要么提供目标文件以便用户重新链接(通常对嵌入式客户不现实),要么改用商业授权。这一点建议在项目启动前就和法务确认清楚,不要等打包发布前后才想起来。
所以我的建议是:只有在部署环境复杂、设备量多、现场无人值守的场景下才值得静态编译。如果只是一个实验室项目,目标板环境可控,动态编译的迭代速度其实更快。做技术选型时先看清楚需求边界,才能避免为了一劳永逸而承担不必要的维护成本。
2. 交叉编译环境搭建:工具链和 sysroot 的准备工作
2.1 工具链选型:Linaro 还是发行版自带
aarch64 交叉编译工具链的可选方案不少,我实际用下来主流的选择大致有这三种。
第一是 Linaro 提供的 aarch64-linux-gnu-gcc 工具链。Linaro 的工具链比较贴近 ARM 上游,版本更新快,编译器优化选项也比较全,适合从零搭建嵌入式开发环境。我的做法是直接用 Ubuntu 18.04 或 CentOS 7 的仓库安装,比如 Ubuntu 上执行apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu。
第二是使用目标板厂商提供的 SDK 工具链。像飞腾、瑞芯微、地平线这些芯片厂商通常会给一套完整交叉编译工具链,里面已经配置好了 sysroot 和特定版本的编译器。如果项目是绑定某一款 SoC 的,强烈建议优先用厂商工具链,省去大量对齐 ABI 的麻烦。我之前在一个 i.MX8 平台上做 Qt 开发,用 NXP 提供的 yocto SDK,里面的 sysroot 已经包含了目标系统上所需的几乎所有头文件和库,Qt 编译几乎零障碍。
第三是直接在目标系统(或同架构容器)上原生编译。这个方法绕开了交叉编译的诸多坑,但效率低,在性能较差的 ARM 板上编译 Qt 会让人崩溃。曾经在一块四核 A53 的开发板上尝试源码编译 Qt 5.14.2,整整跑了四个多小时。如果不是实在没有条件,尽量不要走这条路。
2.2 sysroot 的精细化管理
交叉编译的本质是:在 x86 的宿主机上,用 aarch64 的编译器,编译目标机器上运行的代码。编译器需要知道目标系统的头文件和库在哪里,这些文件集构成了 sysroot。
一开始我图省事,直接用工具链自带的 sysroot,发现 configure 检测 openssl、libz 等库时总是报找不到或者版本不匹配。后来学乖了,自己动手搭一个干净的 sysroot 目录:
mkdir -p ~/aarch64-sysroot # 从目标板上同步基础系统文件 rsync -avz root@<target-board-ip>:/lib ~/aarch64-sysroot/ rsync -avz root@<target-board-ip>:/usr/lib ~/aarch64-sysroot/usr/ rsync -avz root@<target-board-ip>:/usr/include ~/aarch64-sysroot/usr/如果目标板的系统过于精简,也可以直接用 debootstrap 构建一个最小化的 Debian/Ubuntu rootfs,再往里面装依赖库。这个方法的可重复性更好,适合团队协作。
一个容易踩坑的细节是 symlink 处理。rsync 同步过来的 /lib 下有不少符号链接(比如libc.so.6 -> libc-2.28.so),rsync 默认会保留符号链接本身,这没问题。但某些工具链的 sysroot 在链接时会对符号链接有严格的要求,我在一次配置中就遇到 libc.so 这个编译链接用的文件缺失,导致 gcc 在链接阶段报找不到-lc。解决方法是从目标板上把libc.so这个文本格式的链接脚本或符号链接也一并拷贝过来。
2.3 依赖库的准备思路
Qt 5.14.2 的 configure 阶段会检测大量第三方库,比如 libz、libpng、libjpeg、libssl、libsqlite3 等。如果希望 Qt 静态链接这些库,必须在 sysroot 中准备这些库的静态版本(.a 文件)。
这里有个经验之谈:但凡目标系统上能够通过包管理器安装的库,尽量用目标系统对应的 .deb 或 .rpm 包来提取静态库。比如在 Ubuntu 的 rootfs 环境中,执行:
apt install zlib1g-dev libssl-dev libpng-dev libjpeg-dev libsqlite3-dev这些 dev 包会同时包含头文件和 .a 静态库。相比自己用源码编译第三方库,这种方式既省时间,又能保证 ABI 和目标的系统一致。如果目标板不是 Debian 系,就用对应的 yum/dnf 配置,比如银河麒麟 V10(aarch64)上用yum install zlib-devel openssl-devel也能拿到同样的产物。
我自己维护 sysroot 时,习惯把每类依赖单独放一个子目录,比如$sysroot/usr/lib/aarch64-linux-gnu/下放 Debian 系的库,$sysroot/usr/lib64/下放 RPM 系的库。Qt 的 configure 要支持这种多路径搜索,需要用-sysroot配合-extra-cflags、-extra-ldflags做精细控制,后面配置章节我会给出完整实例。
3. configure 参数逐项解析:静态交叉编译的核心环节
3.1 配置命令全景
Qt 从 5.12 以后就移除了 qmake 里单独配置交叉编译的向导,改用 configure 脚本加参数的方式。Qt 5.14.2 的 configure 其实是一个 Perl 脚本,它会生成 Makefile 和 qmake 配置。下面这条配置命令是我在多个项目上验证过的,以 Qt 5.14.2 源码根目录为基准执行:
./configure \ -prefix /opt/Qt5.14.2-static-aarch64 \ -opensource -confirm-license \ -release \ -static \ -xplatform linux-aarch64-gnu-g++ \ -sysroot ~/aarch64-sysroot \ -nomake examples -nomake tests \ -no-opengl -no-eglfs -no-xcb \ -linuxfb \ -no-feature-remoteobjects \ -no-feature-systemsemaphore \ -qt-zlib -qt-libpng -qt-libjpeg \ -qt-sql-sqlite \ -no-openssl \ -I~/aarch64-sysroot/usr/include \ -L~/aarch64-sysroot/usr/lib/aarch64-linux-gnu \ -no-iconv \ -skip qtwebengine -skip qtwebview -skip qt3d \ -no-use-gold-linker这条命令是我踩了无数坑之后固化下来的版本,下面逐个解释为什么是这些参数。
3.2 关键参数的选择逻辑
-static是这条命令的核心,它告诉 Qt 生成静态库(.a),而不是默认的动态库。加了-static之后,Qt 的工具链(比如 moc、rcc、uic)依然会先编译成宿主机可执行文件,但 Qt 自身的库产物会全部变成 .a 文件。
-xplatform linux-aarch64-gnu-g++指定目标平台。Qt 源码中qtbase/mkspecs/目录下有一系列预定义平台文件,linux-aarch64-gnu-g++就是专门给 aarch64 用的。我一开始偷懒用linux-arm-gnueabi-g++(那是 32 位 ARM 的),结果编译出来一堆 ABI 不兼容的错误。检查 mkspecs 列表时发现linux-aarch64-gnu-g++是最合适的,它内部会正确设置-march=armv8-a等基础参数。
-sysroot指向我们准备的目标系统根目录。Qmake 在编译时会自动在 sysroot 下搜索 include 和 lib,不需要额外在 CFLAGS 里写一堆-I和-L。这里我仍然加了-I和-L,是为了弥补部分系统库存在于usr/lib/aarch64-linux-gnu而不是usr/lib的情况。因为 Qt 的 configure 在检测某些第三方库时,其内部搜索路径并不完全遵循--sysroot的规则,手动补充路径能提高检测成功率。
-qt-zlib -qt-libpng -qt-libjpeg这组参数特别关键。它指示 Qt 使用自己源码包内的第三方库副本,而不是去系统里找。对交叉编译来说,这能显著降低对外部环境的依赖。但是要注意,Qt 自带副本通常是动态库的源码,静态编译时它们会以静态方式链入。实际测试中这组参数在静态编译场景下反而是最稳的,因为在 sysroot 里准备干净的 zlib 静态库并不总是容易。如果系统里已经有一套可用的静态库,改用-system-zlib -system-libpng -system-libjpeg也是可以的,但要注意如果某个库缺失,configure 会直接报错甚至静默降级。建议第一次编译时尽量用-qt-*系列,确保各模块齐全。
-no-opengl -no-eglfs -no-xcb是为了减少对图形栈的依赖。如果目标板的 GPU 支持 EGL/OpenGL,且你需要 QML 的硬件加速,那么-opengl es2 -eglfs是必要的。但如果只是做传统 Widgets 程序,Linux 下用 linuxfb 就够了,没必要引入一长串 EGL 库。这里我添加了-linuxfb,这也是 Qt 5.14.2 中比较通用的 framebuffer 平台插件。
-no-openssl可能有人不理解。正常来说 Qt 的 network 模块需要 SSL 支持,但如果目标板的系统 openssl 版本混乱或静态库缺失,configure 阶段的 SSL 检测会失败。我建议第一次交叉编译时先关掉,确保 Qt 能编译出来。后续需要 HTTPS 时再单独解决 openssl 静态库问题。如果你想开启,需要提前在 sysroot 中准备好 libssl.a 和对应的头文件,并将参数改为-openssl-linked,同时补上-I和-L路径。注意:开了-openssl-linked之后,如果链接阶段提示找不到libcrypto.a,那就是 sysroot 里静态库没装好,用包管理器补装 libssl-dev 即可解决。
-no-iconv是为了避免 glibc 中 iconv 版本不一致的问题。静态编译时 iconv 符号是在 glibc 中的,如果目标板的 glibc 版本比 sysroot 中的新,会导致运行时 iconv 调用失败。关掉这个功能,多数字符编码转换的需求在 Qt 里会被绕开,或者通过其他机制实现。
-skip qtwebengine qtwebview qt3d这几个模块是 Qt 源码树中最大的,交叉编译时经常出幺蛾子。尤其是 Qt WebEngine,它需要 Python、Ninja、Clang 等多个工具的配合,静态交叉编译难度极大。如果你的业务确实需要嵌入式 Web 渲染能力,建议把 Qt WebEngine 单独拉出来研究,不要在主体编译时一起搞。
-no-use-gold-linker也很关键。某些版本的 binutils 中 gold 链接器对 aarch64 静态库的链接支持不够完善,用 BFD 链接器(默认)反而更稳。我遇到过用 gold 链接后在 ARM 板上运行时出现奇怪的段错误,换回 BFD 链接器就恢复正常。这个参数就是为了强制关闭 gold 链接器。
3.3 从 configure 输出中读取有效信息
包装完 configure 之后,不要直接甩手去 make。configure 的终端输出和生成的config.summary文件里包含大量有效信息,值得花几分钟浏览一遍。
重点检查这几项:
- Build 类型:确认显示
Building on: linux、Building for: linux-aarch64-gnu-g++,避免在 x86 上编译出 x86 代码。 - QPA backend 列表:是否包含
linuxfb。如果没出现,说明平台插件没有被正确加入。 - 第三方库状态:zlib、libpng、libjpeg 是否显示为 "qt"(即使用 Qt 自带版本)还是 "no"(根本未检测到)。如果显示 no,说明 configure 没找到对应的依赖,后续某些模块可能无法使用。
- OpenSSL 状态:确认是 "no"(因为我们显式关闭)。如果是其他奇怪的状态,注意看是否因为路径问题误打误撞开启了。
如果这些信息和你预期不符,现在返回去改 configure 参数的成本最低。等 make 编译到一半再发现模块缺失,代价就大多了。
4. 编译、安装及交叉编译自己的 Qt 程序
4.1 并行编译的科学与玄学
configure 通过后,进入编译阶段:
make -j$(nproc) 2>&1 | tee build.log-j后面的并行度不是越大越好。交叉编译时,宿主机 CPU 核心多,但编译器进程是 x86 宿主上运行、输出 aarch64 代码,性能瓶颈主要在内存和磁盘 IO,而不是 CPU 核心数。我曾经在一台 8 核 16G 内存的开发机上用-j16编译,直接 OOM 崩溃。建议初次编译时先-j4,观察内存占用稳定后再调高。实测 8 核机器跑-j8配合-j4的二阶段构建,整个 Qt 编译约耗时 60 到 90 分钟,是可接受的范围。
编译过程中如果出现错误,不要在 output 里海捞。Qt 的构建系统通常会在build.log里记录完整的命令和错误上下文。遇到.cpp: undefined reference to ...,大多数情况是某个 Qt 模块被裁剪了,而另一个模块还在引用它。比如关掉了-no-feature-remoteobjects之后,部分 qml 或 networking 组件还尝试链接相关符号。逐个模块排查即可。
编译失败后,不要傻乎乎地重新 configure 一遍。Qt 的构建系统支持增量编译:
make clean # 清理已经失败的产物 make -j4 # 重新开始如果改了 configure 参数,最好是先执行make distclean,彻底清除之前的配置产物,再重新 configure 和 make。因为 configure 生成的 Makefile 内部缓存了路径和 feature 开关,强制覆盖容易产生诡异问题。
4.2 安装到指定前缀
编译成功后安装:
make install所有产物会被安装到配置时指定的-prefix /opt/Qt5.14.2-static-aarch64目录下。这个目录结构里的几个子目录需要心里有数:
lib/下是 Qt 的静态库(.a 文件)。lib/cmake/下是 CMake 的配置文件,方便用 CMake 构建自己的程序。qml/、plugins/目录会在使用Q_IMPORT_PLUGIN时用到。bin/下是宿主机运行的 qmake 等工具。
安装完成后,需要确认一个关键点:qmake 是否是用于交叉编译的版本。执行:
/opt/Qt5.14.2-static-aarch64/bin/qmake -v输出中应该能看到类似Using Qt version 5.14.2 in /opt/Qt5.14.2-static-aarch64/lib的信息。如果 qmake 指向了宿主机上另一套 Qt,说明环境变量 PATH 有问题。交叉编译时一定不要使用宿主机系统的 qmake,否则即使代码能编译,链接的也是宿主机 x86 库。
4.3 交叉编译 Qt 程序的完整示例
现在写一个最基础的 Qt Widgets 程序来验证工具链。假设源码main.cpp是这样的:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(QStringLiteral("Hello, aarch64 Qt Static!")); label.resize(320, 200); label.show(); return app.exec(); }用 qmake 工程文件:
QT += widgets TARGET = hello_static TEMPLATE = app SOURCES += main.cpp构建命令:
export PATH=/opt/Qt5.14.2-static-aarch64/bin:$PATH export CPATH=~/aarch64-sysroot/usr/include export LIBRARY_PATH=~/aarch64-sysroot/usr/lib/aarch64-linux-gnu mkdir build && cd build qmake ../hello_static.pro make -j4qmake是 Qt 的元构建系统,它根据 .pro 文件生成 Makefile。这里.pro文件省略了目标平台的设置,因为 qmake 本身的 mkspecs 已经设定了 aarch64。如果 qmake 默认指向了宿主机工具链,可以手动执行:
qmake -spec linux-aarch64-gnu-g++ -o Makefile ../hello_static.pro链接成功后,检查生成的二进制文件类型:
file hello_static预期输出:
hello_static: ELF 64-bit LSB executable, ARM aarch64, dynamically linked (uses shared libs) ...注意,虽然file信息显示dynamically linked,那是指它链接了基础的 C/C++ 库(比如 libc、libstdc++),Qt 相关的库应该是全静态的。如果想看到更精确的依赖列表:
aarch64-linux-gnu-readelf -d hello_static | grep NEEDED列表里应该只有libc.so.6、libstdc++.so.6这类系统库,而不会出现libQt5Widgets.so。如果需要连它们也静态化,就得在交叉编译器层面额外处理-static-libgcc -static-libstdc++,这个后面在优化章节会讲。
4.4 板子上跑起来的两个关键细节
在把二进制拷到 ARM 板上之前,先检查两件事。
第一,确认目标板上的 framebuffer 设备存在。Qt 的 linuxfb 插件直接操作/dev/fb0这类 framebuffer 节点。如果板子上用的是 DRM/KMS 显示,通常还需要/dev/dri/card0。检查方式:
ls -l /dev/fb0 /dev/dri/card0如果没有 framebuffer 设备,linuxfb 插件会直接启动失败。这种情况下可以考虑改用-platform eglfs或者-platform wayland,但这些依赖板级图形栈,不在静态 Qt 的范围内。
第二,运行时需要设置环境变量QT_QPA_PLATFORM。如果你在 configure 时只加入了 linuxfb 插件,不显式设置环境变量,Qt 会尝试查找默认插件(通常是 xcb)。目标板大概率没有 X11 环境,所以需要在启动脚本里写入:
export QT_QPA_PLATFORM=linuxfb ./hello_static这一位是静态编译的新手最容易翻车的地方。就算编译链接全过了,不设置或设置错误的 platform 插件,程序一启动就弹could not find the Qt platform plugin "linuxfb"直接退出。
5. 动态特性与静态编译的冲突:插件、QML 和第三方库
5.1 插件机制的静态化处理
Qt 的插件机制在动态编译下运行得很自然——加载 plugin 目录下的 .so 文件。静态编译后,没有独立 .so 文件存在,必须把插件直接编译进可执行文件。
有两种方式可以做到。
方式一,在 .pro 文件中手动 import 需要的插件:
QTPLUGIN += qlinuxfb并同时在代码里引入:
#include <QtPlugin> Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)这个宏会把插件的静态实现链接进来,并注册到 Qt 的插件管理器中。
方式二,在链接时使用 Qt 的qtAddPlugin辅助函数。这个方法多用于 CMake 项目。简单来说,静态插件在 qmake 和 CMake 中都有专门的注册方式,但前提是在 configure 时插件已经被编译进plugins/platforms/目录下的 .a 文件。
如果你发现程序在板子上启动时仍然报找不到插件,十有八九是QTPLUGIN或Q_IMPORT_PLUGIN没有生效。可以检查链接后的二进制,用nm查看是否有插件相关的符号:
aarch64-linux-gnu-nm hello_static | grep linuxfb如果没有输出,说明插件未被链接进去,需要检查插件是否确实被编入plugins/platforms目录,或者Q_IMPORT_PLUGIN代码是否被编译器优化掉了(release 模式常见)。
5.2 QML 模块的裁剪策略
如果你使用 QML 开发界面,QML 模块(如 QtQuick、QtQuick.Controls)在静态编译时会有额外的复杂度。QML 是基于字符串加载的脚本语言,其内置的 C++ 插件同样要静态链接。但 QML 的插件数量远比 Widgets 模块多,逐个Q_IMPORT_PLUGIN不现实。
Qt 5.14.2 提供了一个折中方案:QML 模块可以独立编译,不必静态链入所有 QML 插件。在实际部署时,仍然把 QML 插件目录作为资源文件打包或拷贝到板子的文件系统上。这实际上破坏了"零依赖"的初衷,但 QML 渲染引擎本身对 C++ 插件的依赖比 Widgets 更复杂,静态化的边际收益不高。
我的建议是:如果业务对界面复杂度要求高,且使用了 QML,不要盲目追求静态化。此时可以退一步,将系统库(libc、libstdc++)静态化,而 Qt 库保持动态,这样保证 Qt 本身库的版本可控,又不必处理 QML 插件的静态化负担。刻意追求"全静态"可能会把大量时间耗在 QML 模块的配置上。
5.3 第三方依赖库的静态链接坑
Qt 的第三方依赖(如 openssl、sqlite)在静态编译时也有一套自己的坑。
以 openssl 为例。Qt 的-openssl-linked选项会将程序直接链接到libssl.a和libcrypto.a。但 openssl 的版本和编译选项决定了其导出符号是否符合预期。如果 sysroot 里的 openssl 是用动态库方式安装的(比如libssl.so),那么libssl.a很可能根本不存在。这时需要手动下载 openssl 源码进行交叉编译:
wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar xf openssl-1.1.1w.tar.gz cd openssl-1.1.1w export CC=aarch64-linux-gnu-gcc ./Configure linux-aarch64 no-shared --prefix=$SYSROOT/usr make make install这里的关键参数是no-shared,它强制生成.a静态库。安装后的头文件和库都会进入 sysroot 的/usr目录,这样 Qt 的 configure 才能找到。
再比如 sqlite,如果我们要-qt-sql-sqlite(使用 Qt 自带 SQLite),那就不要额外安装系统 sqlite。但如果业务中有第三方代码需要直接操作 sqlite,而 Qt 又自带一份,可能会遇到符号冲突。此时应该明确谁优先,是让 Qt 使用系统 sqlite(-system-sqlite),还是第三方代码全部走 Qt 的 API。我在一个项目中就是在静态模式下同时链接了 Qt 自带 sqlite 和系统 sqlite,导致链接阶段出现一堆 duplicate symbol,最后不得不让 Qt 改用系统 sqlite 才解决。
6. 常见报错速查与排查思路
6.1 CANNOT MIX INCOMPATIBLE QT LIBRARY
这个报错的原文一般长这样:
qt: fatal: cannot mix incompatible Qt library (version 0x50e0601) with this library (version 0x50c0101)这个0x50e0601和0x50c0101是 Qt 的版本号十六进制编码,分别对应 5.14.6 和 5.12.1(后三位是补丁版本)。出现这个错误的本质是:你的程序在运行时同时加载了两个不同版本的 Qt 库。比如你的程序是 5.12 的 Qt 编译的,但系统 LD_LIBRARY_PATH 里存在 5.14 的 Qt 库,动态加载器先加载了 5.14 的库,然后程序内部尝试调用 5.12 的符号,于是直接冲突。
静态编译后,这个错误通常不会出现,因为你已经把所有 Qt 库固化进了可执行文件。但如果你的程序里还通过dlopen动态加载了某个第三方动态库,而那个动态库链接了另一个版本的 Qt,这个问题就会被重新触发。
排查方法很简单:
ldd hello_static检查是否有多个版本的libQt5Core.so出现在依赖列表中。或者:
readelf -d hello_static | grep RPATH静态编译程序一般不需要 RPATH,如果出现 RPATH 且指向了开发机上某个 Qt 安装目录,要么去掉 RPATH,要么确认目标板上确实存在这个路径。
6.2 COULD NOT FIND THE QT PLATFORM PLUGIN "LINUXFB"
qt.qpa.plugin: could not find the Qt platform plugin "linuxfb" in ""这个报错说明 Qt 运行时在搜索平台插件时找不到libqlinuxfb.so或对应的静态插件。静态编译场景下的原因有两个:一是 configure 时没有加入-linuxfb,导致插件根本未被编译;二是编译了插件却没在代码中通过Q_IMPORT_PLUGIN导入,插件成了"虽然存在但无人认领"的状态。
怎么确认 configure 时是否成功加入 linuxfb?在源码目录下执行:
find qtbase -name "*linuxfb*"如果有一些qlinuxfb*相关的目录和文件,说明有。如果没有,那就重新跑 configure,确保加入了-linuxfb。
如果插件确实存在,检查生成的插件静态库:
ls /opt/Qt5.14.2-static-aarch64/plugins/platforms/正常情况下应该有libqlinuxfb.a。然后确认程序代码里是否已经Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)。缺了这行,就算静态库在,Qt 的插件系统也找不到它。
一个补充技巧:在 QApplication 构造前设置环境变量,强制指定插件搜索路径:
#include <QApplication> #include <QDir> int main(int argc, char *argv[]) { qputenv("QT_QPA_PLATFORM", "linuxfb"); QApplication app(argc, argv); // ... }这样能避免在启动脚本中每次都手动写export QT_QPA_PLATFORM=linuxfb。但注意,如果你需要支持多种显示平台切换,还是建议用环境变量。
6.3 内网环境、无外网条件下的依赖获取
很多 aarch64 嵌入式项目部署在纯内网环境,开发环境也无法访问外网,这会带来依赖库获取的困难。我的经验是提前准备一个"离线依赖包仓库"。
具体做法:在一台能联网的同源系统(比如同版本的 Ubuntu aarch64 环境)上执行:
apt-get download zlib1g-dev libssl-dev libpng-dev libjpeg-dev下载的 .deb 文件可以拷贝到内网开发机上,用dpkg -i逐个安装,或者离线 dpkg 仓库的方式统一管理。如果是 CentOS/麒麟系,用yumdownloader --resolve --destdir=...实现类似效果。
注意:下载的 .deb 必须是 aarch64 架构,而不是 amd64。在 x86 开发机上如果执行apt-get download不带架构参数,拿到的将是 amd64 的包,无法用于交叉编译。交叉编译场景下必须使用dpkg --add-architecture arm64并配合apt-get update,或者直接使用wget从镜像站下载指定架构的 .deb 包。
6.4 链接阶段的一堆 undefined reference 怎么破
交叉编译 Qt 程序时最常见的一类链接错误是大量undefined reference to,而且大部分发生在静态编译后才爆发。原因在于动态链接时库之间的依赖是惰性解析的,而静态链接则要求所有符号在链接阶段全部解析完成,顺序也极其敏感。
我在链接自己的程序时遇到过一堆libQt5Gui.a里引用了libpng的符号,而libpng静态库放在了libQt5Gui.a前面导致没有被正确解析。解决方案是调整链接顺序:把依赖别人的库放在后面。更优雅的解决方式是使用链接器的--start-group和--end-group参数将静态库包起来,让链接器可以循环解析:
aarch64-linux-gnu-g++ main.o \ -Wl,--start-group \ libQt5Widgets.a libQt5Gui.a libQt5Core.a \ libqlinuxfb.a \ -Wl,--end-groupqmake 生成的 Makefile 如果出现这类问题,可以手动在.pro中加入:
QMAKE_LFLAGS += -Wl,--start-group QMAKE_LFLAGS += -Wl,--end-group这个技巧算不上优雅,但能解决 90% 的静态链接顺序问题。另一种根治思路是使用 CMake 替代 qmake,CMake 处理链接顺序的能力比 qmake 强很多,对于大型项目我推荐直接迁移到 CMake。
7. 一次完整复盘的优化清单
7.1 静态编译后的优化思路
Qt 静态编译出来的程序体积不小,如果这是你第一次拿到产物,先别急着降低体积,确保功能正确后再优化。
第一个可以动手的是 strip。发布之前对二进制执行:
aarch64-linux-gnu-strip hello_static通常能减少 30% 到 50% 的体积。我见过静态编译的 Qt 程序从 35MB 降到 19MB 的例子。
第二个思路是裁剪 Qt 模块。Qt 5.14.2 的 configure 支持-no-feature-*参数禁用具体功能,比如-no-feature-draganddrop、-no-feature-concurrent等。如果你想更精细地控制,可以在 configure 时使用:
-qtnamespace MyNamespace -qtlibinfix _lite这两个参数能把所有 Qt 符号封装到指定命名空间和库名中,对减小符号表也有帮助,但会破坏部分第三方库与 Qt 的兼容性,慎用。
第三个思路是将编译器静态运行时也纳入。在链接时加上:
-static-libgcc -static-libstdc++如果交叉编译器本身是 GCC 8 以上的版本,需要确保libstdc++.a存在于工具链目录。加上这两个参数后,可执行文件对libstdc++.so.6的依赖也会消失,这在一些精简的嵌入式 Linux 环境里非常有用。
7.2 静态 Qt 的维护成本与升级建议
静态交叉编译的维护成本比动态编译高不少。每次 Qt 升级,都要重新走一遍 sysroot 依赖核对、configure 调参、编译和验证。版本升级往往伴随 feature 变更,参数含义可能发生变化。
我的建议是:把 configure 命令固化成脚本,纳入版本管理。团队中任何一个成员都能通过脚本复现关键的构建环境,而不是依赖某一个人的记忆。同时,为每一次构建打标签,记录 Qt 版本、工具链版本、sysroot 快照、configure 参数、操作系统的 glibc 版本。当现场出现诡异问题时,这个标签能快速定位到是哪一层环境发生了漂移。
另一个被低估的坑是 glibc 版本。静态编译的程序如果链接了目标板上 glibc 的版本符号(比如GLIBC_2.28),在 glibc 更老的板子上就会报FATAL: kernel too old或者 symbol lookup error。要避免这个问题,最好在配置阶段为 sysroot 清晰地标注 glibc 版本,或者在目标板上准备 glibc 2.17 左右的 sysroot(比如 CentOS 7),这样程序能覆盖更多的 AGING 设备。但反过来,老 sysroot 编译的程序在新系统上运行也可能有兼容性隐患。这个选择需要结合你实际管理的设备型号来决定。
7.3 从实际项目看静态编译是否值得
以我最近的一段项目经历为例。一个工业触控终端,基于飞腾 2000 四核处理器,操作系统是某国产化 Linux,Qt 5.14.2 静态交叉编译。初期花了一周时间把工具链、sysroot、configure 全部搞定,期间踩了 openssl 静态库、linuxfb 插件、QML 插件静态化这几个大坑。但一旦固定好构建脚本,后续的开发和部署变得异常轻松——程序编译好之后就是一个单文件,通过 USB 或者网口拷到设备上,甚至能放到一个 ext4 只读分区里直接运行,不必担心任何动态库缺失。
如果项目处于原型验证阶段,或者目标设备数量只有个位数,那么静态编译的投入产出比确实不高。但如果你面对的是一批配置各异、系统环境不受控的现场设备,静态交叉编译省下的是每一次现场排障的时间和差旅成本。这也是我至今仍然坚持用静态方案的主要原因。
我个人在整理这套手册时最大的体会是:Qt 的静态交叉编译并不神秘,真正消耗时间的全是环境对齐——工具链、sysroot、依赖库、configure 参数四者只要有一个不匹配,后续的报错就会千奇百怪。把每一步的环境版本固定下来,遇到问题时刻保持"先怀疑环境,再怀疑代码"的排查顺序,这套流程就能稳定复现。如果你也准备在 aarch64 平台上做 Qt 静态交叉编译,建议先把 configure 参数表和 sysroot 的依赖清单打印出来贴在工位上,踩坑的时候会发现它们值一半的加班时间。