
1. 为什么我坚持用 Qt 5.14.2 做静态交叉编译1.1 静态链路到底解决什么问题前阵子接了一个 ARM64 工控板的界面项目板子存储空间不大系统还是裁剪过的没有包管理器更没有 x86 开发机上那种装个 Qt 就能跑的便利条件。以前在板子上跑 Qt 程序最怕的就是那一连串libQt5Core.so.5: cannot open shared object file的报错Qt 的动态库少说也有十几二十个拷少了缺这个拷多了盘不够版本一不对整个程序直接罢工。静态交叉编译把这个问题一次性解决了Qt 的 Core、Gui、Widgets 模块和平台插件在编译期就全部揉进可执行文件里最后拿到板子上的就是一个独立的 ELF 文件。没有运行时依赖链没有库版本地狱拷走就能跑。对 HMI 类应用来说这个价值太实在了。当然静态不是免费的午餐。编译时间变长、可执行文件体积变大、部分动态特性会受限制但对一个追求一个文件上板即跑的嵌入式场景来说这些代价完全值得。1.2 版本选型5.14.2 是 Qt5 时代最稳的锚点肯定有人会问现在 Qt 6 都出了那么久为什么还盯着 5.14.2我是这么看的。Qt 5.15 是 Qt5 最后一个 LTS但开源版源码包的获取方式在 5.15 之后变得绕而很多工业项目恰恰需要干净可控的开源 qmake 构建链路不想在其他环节上牵扯太多精力。Qt 6 全面转向 CMake构建哲学变化很大对既有 Qt5 代码库的迁移本身就是一个大工程没必要为了追新把正在运行的项目推倒重来。5.14.2 本身也非常成熟。它支持 C11/14qmake 工作流稳定第三方资料全网到处都是遇到的坑几乎都有前人踩过适合做交叉编译这种一步错步步错的工程。我在这篇文章里就用 5.14.2 作为基准版本完整跑一遍从零搭建静态交叉编译环境的流程把这个事情彻底说透。1.3 整体架构先说清楚做交叉编译之前脑子里必须有这样一幅图开发主机x86_64 Linux推荐 Ubuntu 20.04 或 22.04交叉工具链aarch64-linux-gnu-gcc / g编译出的程序跑在 ARM64 上sysroot目标板根文件系统的镜像目录里面放着 ARM64 版 libc、内核头文件等Qt 源码qtbase-everywhere-src-5.14.2只取 base 模块就能支撑 Qt Widgets 应用目标形态Qt 静态库 静态平台插件 你的业务代码全部链进一个 ARM64 可执行文件理解了这张图后面每一步你都知道自己在干嘛而不是机械地复制命令。2. 工具链与 sysroot比 Qt 本身更能决定成败2.1 交叉工具链的安装交叉编译的起点不是 Qt而是能不能用正确的编译器编出能在 ARM64 上运行的程序。Ubuntu 官方仓库直接提供了完整的 aarch64 交叉工具链安装很简单sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu这条命令会把aarch64-linux-gnu-gcc、aarch64-linux-gnu-g、aarch64-linux-gnu-ld、aarch64-linux-gnu-ar等一系列工具都装好。验证一下版本aarch64-linux-gnu-g --version如果能正常打印版本号说明工具链本身没问题。我这里故意用 Ubuntu 自带的工具链而不是去折腾 Linaro 或者板厂定制 SDK原因是通用性最高踩坑最少。板厂 SDK 往往绑定了特定内核和交叉编译器版本适合做量产固件不适合我们这种自己搭 Qt 环境的场景。2.2 sysroot 的三种来源sysroot 是交叉编译最容易翻车的地方它表示目标系统在主机上的一个影子目录。编译 Qt 时头文件、库文件、链接脚本都要从这个影子目录里找而不是从宿主机找。我实际用过三种方式各有利弊直接使用 Ubuntu 多架构目录安装libc6-dev-arm64-cross后/usr/aarch64-linux-gnu就是一个可用的 sysroot里面有 ARM64 版 libc 静态库、动态库和头文件。这是最简单的起步方式适合验证流程。用 debootstrap 拉一个完整的 arm64 rootfssudo debootstrap --archarm64 bullseye /opt/sysroot-arm64这种方式可以得到一个纯正的目标系统根目录还能用 chroot 进去装额外的开发包比较接近目标板真实环境。复用板厂 SDK 里的镜像目录很多开发板 BSP 自带一个 rootfs直接把里面的/usr和/lib抽出来用。这种方式最贴近最终部署环境但目录可能比较杂乱。对于第一次尝试的人我建议用第一种干净利落sudo apt install libc6-dev-arm64-cross装完之后确认/usr/aarch64-linux-gnu目录存在并且里面有lib、usr/include、usr/lib等子目录。2.3 验证工具链和 sysroot 是否匹配工具链和 sysroot 能不能一起干活用一个小测试就能验证。写一个空的测试文件echo int main() { return 0; } test.cpp aarch64-linux-gnu-g test.cpp -o test_arm64 file test_arm64file输出里如果出现ELF 64-bit LSB executable, ARM aarch64就说明编译器能编译出 ARM64 可执行文件。再进一步把 sysroot 显式传进去aarch64-linux-gnu-g test.cpp -o test_arm64 --sysroot/usr/aarch64-linux-gnu这一步能正常通过说明头文件和库文件的搜索路径都对得上后面给 Qt configure 传-sysroot参数时心里就有底了。还有一个细节某些发行版的交叉编译器会在链接时寻找ld-linux-aarch64.so.1这样的动态链接器如果 sysroot 里只有lib/ld-linux-aarch64.so.1而没有对应的符号链接链接可能报cannot find /lib/ld-linux-aarch64.so.1。遇到这种情况检查一下 sysroot 的 lib 目录手动补上符号链接即可。3. Qt 源码准备mkspec 与 configure 就是这场戏的剧本3.1 只取 qtbase 而不是把整个 everywhere 源码拿下来很多人一上来就下载qt-everywhere-src-5.14.2.tar.xz那个包包含所有模块解压后十几个 G编译起来时间长得让人怀疑人生。如果你只是做 Qt Widgets 程序完全没必要。单独的qtbase-everywhere-src-5.14.2.tar.xz就包含 QtCore、QtGui、QtWidgets、QtNetwork 这些最核心的模块足以覆盖绝大多数 HMI 场景。wget https://download.qt.io/archive/qt/5.14/5.14.2/qtbase-everywhere-src-5.14.2.tar.xz tar xf qtbase-everywhere-src-5.14.2.tar.xz cd qtbase-everywhere-src-5.14.2先用 qtbase 打通整个链路如果后面确实需要 Qt Charts、Qt Multimedia 这类模块再用相同的 configure 思路叠加编译复杂度可控得多。3.2 自定义 linux-aarch64-gnu-g mkspecQt 编译目标平台的行为由 mkspec 定义它告诉 qmake 和 configure编译器叫什么、链接器怎么用、cflags 应该加什么。Qt 5.14.2 自带的 mkspecs 里有 32 位 ARM 的linux-arm-gnueabi-g没有现成的 aarch64 版本所以我们要拷贝一份再改。cp -r qtbase/mkspecs/linux-arm-gnueabi-g qtbase/mkspecs/linux-aarch64-gnu-g然后修改qtbase/mkspecs/linux-aarch64-gnu-g/qmake.conf改成下面这样MAKEFILE_GENERATOR UNIX TARGET_PLATFORM unix TEMPLATE app CONFIG incremental QMAKE_INCREMENTAL_STYLE sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g-unix.conf) 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_LINK_C aarch64-linux-gnu-gcc QMAKE_LINK_C_SHLIB aarch64-linux-gnu-gcc QMAKE_AR aarch64-linux-gnu-ar cqs QMAKE_NM sh -c aarch64-linux-gnu-nm -P \$(basename \$0) QMAKE_STRIP aarch64-linux-gnu-strip QMAKE_RANLIB aarch64-linux-gnu-ranlib QMAKE_OBJCOPY aarch64-linux-gnu-objcopy QMAKE_CFLAGS -fPIC -O2 -marcharmv8-a QMAKE_CXXFLAGS -fPIC -O2 -marcharmv8-a load(qt_config)这里的关键点QMAKE_CC/QMAKE_CXX指向交叉编译器别写错名字-marcharmv8-a是 ARM64 的基础指令集如果你的目标芯片是 Cortex-A53、A72 这类常见核心这个参数没问题如果用到特定 SoC 的优化指令可以换成-mcpucortex-a53之类的QMAKE_AR用cqs参数静态库打包时安静且覆盖索引这是交叉编译静态 Qt 的一个细节少了可能导致链接时符号表找不到-fPIC对静态库不是强制要求但保留它能让部分底层次序处理更省心性能损失也几乎可以忽略3.3 configure 参数逐个拆解mkspec 备好之后进入源码根目录执行 configure。这是整个手册的核心我先把完整命令贴出来再逐个解释为什么这么写./configure \ -prefix /opt/Qt5.14.2/5.14.2/aarch64-static \ -extprefix $HOME/qt-aarch64-static-install \ -xplatform linux-aarch64-gnu-g \ -release -static -optimize-size \ -opensource -confirm-license \ -make libs \ -nomake tests -nomake examples \ -no-opengl -no-xcb \ -no-dbus -no-glib -no-icu -no-iconv \ -no-openssl -no-cups \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz \ -linuxfb参数含义-prefixQt 在目标板上期望的安装路径静态编译下主要影响 qmake 生成的路径变量-extprefixQt 实际安装到开发主机上的位置交叉编译时这里很关键-xplatform指定我们刚创建的 mkspec 名称-static生成静态库这是本文的最终目标-release -optimize-size编译发布版优先优化体积-make libs只构建库不构建工具和插件之外的东西-nomake tests -nomake examples砍掉测试和示例大幅缩短编译时间-no-opengl不使用 OpenGL前提是你的应用不需要 QML/QtQuick-linuxfb启用 Linux Framebuffer 平台插件这是无 X11、无 Wayland 环境下跑 Widgets 程序的方案-prefix和-extprefix是交叉编译最容易混淆的一对。简单说-prefix写的是将来 Qt 在目标板上被放在哪里-extprefix写的是现在 Qt 在开发机上被安装到哪里。由于静态编译最终不需要向目标板部署 Qt 库-prefix更多是让 qmake 生成的配置信息保持合理真正落地的是-extprefix。-no-opengl这个开关值得多说一句。如果目标板没有 GPU 或你不需要 QtQuick加这个可以避免 sysroot 里必须准备 EGL/GLES 头文件和库的麻烦。很多嵌入式板子的显卡驱动只有二进制闭源 so静态交叉编译时链接这些库会遇到一堆意料之外的问题能躲就躲。如果确实需要 OpenGL后面我会在进阶章节讲怎么处理。依赖库的处理我特意用了-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz意思是让 Qt 使用源码树里自带的这些第三方库并将其静态编译进来。这一步省掉了一大堆单独交叉编译依赖库的苦力活。不要一上来就去追求-system-zlib之类的系统库链接那个看起来很专业实际上每个库都要你手动交叉编译任何一个版本不匹配都会把 configure 卡死。3.4 首次进入 configure 时应该看到什么执行 configure 后脚本会花几分钟做编译探测。最后如果能看到类似这样的输出基本说明环境配置成功了Qt is now configured for building. Just run make once to create all the build artifacts.如果中途出现ERROR:开头的行不要慌把错误信息里提到的文件路径和缺失组件名记下来多半是 sysroot 里缺了某个头文件或库。最常见的错误是crt1.o: No such file or directory这是因为libc6-dev-arm64-cross没装或 sysroot 路径不对。看到这个错误先检查 sysroot而不是重新去改 configure 参数。4. 跑 make 和安装一套可以直接照抄的实测命令4.1 编译前最后检查在我实际执行 make 之前习惯性做三件事确认当前 shell 里aarch64-linux-gnu-g在 PATH 中确认$HOME/qt-aarch64-static-install目录可写确认磁盘剩余空间大于 5GB静态编译的中间文件并不小df -h . which aarch64-linux-gnu-g第三点很容易被忽略。Qt 静态编译的中间 .o 文件、.a 文件加起来体积很大如果过编译的时候磁盘写满报的错误会很莫名其妙而且排查起来浪费时间。4.2 正式执行 make一切就绪后直接跑make -j$(nproc)-j$(nproc)能让所有 CPU 核心都参与编译。但这里有个教训如果你的机器内存小于 8GB别盲目开满核心。Qt 编译是典型的高内存消耗任务16 核机器上-j16很可能把内存吃满导致 OOM编译器被内核杀掉终端上只剩一行Killed。遇到这种情况先用-j4甚至-j2跑稳比快重要。编译 qtbase 静态版在我的机器上大概需要二十分钟到四十分钟取决于 CPU 性能。中断之后重新执行make是可以续传的它只重建上次未完成的部分这一点 Qt 的构建系统做得很老实不需要每次从头来。4.3 make install 与产物验证编译完成后make install安装完成后验证 Qt 的交叉 qmake 是否可用$HOME/qt-aarch64-static-install/bin/qmake -v正常输出类似于QMake version 3.1 Using Qt version 5.14.2 in /home/you/qt-aarch64-static-install我这里qmake -v显示的路径是$HOME/qt-aarch64-static-install也就是-extprefix指定的安装目录。看到这个输出说明 Qt 静态库已经成功构建并安装。再检查一下静态库是不是真的静态的ls $HOME/qt-aarch64-static-install/lib/libQt5Widgets.a能列出libQt5Widgets.a就说明 Qt Widgets 是以静态库形式存在的。4.4 编译中遇到的三个高频报错报错 1crt1.o: No such file or directory原因sysroot 里缺少目标系统的 C 运行时启动文件。多半是libc6-dev-arm64-cross没装全或者-sysroot参数指向了空目录。先确认/usr/aarch64-linux-gnu/usr/lib/aarch64-linux-gnu/crt1.o是否存在。报错 2GL/gl.h: No such file or directory原因sysroot 里没有 OpenGL 头文件。如果你根本不需要 OpenGL把 configure 里的-no-opengl加上如果确实需要需要把板子的 EGL/GLES 开发头文件放进 sysroot。报错 3cannot find -lGL原因链接阶段找不到 OpenGL 库。同上要么去掉 OpenGL 依赖要么提供正确的静态libGL.a或链接路径。这条报错的坑在于有时候 configure 阶段探测时没有 OpenGL 也让你过了实际链接 app 时却要求-lGL这种情况要在.pro文件里显式关闭相关模块或补全库路径。这三个是我实战中遇到频率最高的把 sysroot 和 OpenGL 这两个根源解决掉其他报错基本都能顺藤摸瓜查出来。5. 编译第一个静态 Qt 程序从 qmake 到目标板5.1 一个最小的 Qt Widgets 工程环境搭好之后用一个小工程验证全套链路。先准备myapp.proQT core widgets TARGET myapp TEMPLATE app CONFIG c11 SOURCES main.cpp QTPLUGIN qlinuxfb再准备main.cpp#include QApplication #include QLabel #include QtPlugin Q_IMPORT_PLUGIN(QFbIntegrationPlugin) int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello ARM64 Qt); label.resize(320, 120); label.show(); return app.exec(); }这段代码里Q_IMPORT_PLUGIN(QFbIntegrationPlugin)非常关键。静态链接的 Qt 不会在运行时去/plugins/platforms目录动态加载libqlinuxfb.so平台插件必须在编译期直接链进可执行文件。这个宏就是告诉编译器把QFbIntegrationPlugin这个平台的符号导入进来。如果漏掉这行编译出来的程序在板子上运行时会报This application failed to start because no Qt platform plugin could be initialized.所以先别问为什么这么写记住静态 Qt 工程必须用Q_IMPORT_PLUGIN手动引入你需要的平台插件。这部分是静态交叉编译和动态编译最显著的区别。5.2 用交叉 qmake 构建应用export PATH$HOME/qt-aarch64-static-install/bin:$PATH cd myapp qmake myapp.pro make这里有个容易踩的坑export PATH之后qmake不一定真的是新装的交叉 qmake。先用which qmake确认which qmake # 期望看到 /home/you/qt-aarch64-static-install/bin/qmake如果 which 结果指向系统自带的 Qt5 qmake那是 x86_64 的编译出来的程序在板子上根本跑不了。这类问题比编译报错更隐蔽因为它能正常编译通过只有在板子上跑的时候才会发现不是 ARM64 架构。构建完成后file myapp输出里应该有ELF 64-bit LSB executable, ARM aarch64, statically linked看到static关键字说明 Qt 是静态链进去的。如果你还额外用了-static-libstdc之类的选项这里也可能会显示statically linked。当然如果系统 glibc 是动态链入的这里可能显示部分动态标志后面我会解释这是有意的选择。5.3 部署到目标板的运行验证把myapp拷到 ARM64 板子上方式可以是 U 盘、UART 传文件或者网络。我的习惯是先用scpscp myapp root192.168.1.100:/root/然后 ssh 到板子上执行chmod x /root/myapp /root/myapp -platform linuxfb加-platform linuxfb是显式指定使用 Linux Framebuffer 插件避免 Qt 自动探测平台时出岔子。如果板子的/dev/fb0存在且帧缓冲初始化正常屏幕上就能看到那个 Hello ARM64 Qt 的窗口。这个验证页面虽然简单但它把工具链、sysroot、mkspec、configure、静态编译、插件导入、目标板运行验证整条链路都串起来了。第一次跑通的那一刻值得记录一下时间点因为后面所有业务逻辑都是在这个基础之上展开的。5.4 部署时还要注意什么静态编译解决了 Qt 库的依赖问题但有一些运行时数据依然不是静态的时区数据、ssl 证书这些系统级文件仍然依赖目标板中文字体库不会自动进二进制目标板没有对应字体会出现方块字输入设备支持如果要使用触摸屏一般要另外交叉编译 tslib 或 evdev 相关支持这些不是 Qt 静态编译能一揽子解决的需要在部署阶段单独处理。我在下一节里专门讲字体和体积这两个最常被问到的日常问题。6. 静态包说大不大说小不小优化与进阶实战6.1 体积瘦身三板斧静态编译最直观的代价就是体积。一个最简单的 Qt Widgets 程序编译出来动辄十几兆。瘦身可以从以下几个方面入手第一strip。安装 Qt 时已经带上了交叉 strip 工具aarch64-linux-gnu-strip --strip-unneeded myapp这一条命令通常能把可执行文件从 16MB 压到 12MB 左右效果非常直接。第二configure 阶段控制模块和特性。-nomake tests -nomake examples只是砍掉了额外工程真正影响库体积的是 Qt 的特性模块。可以在 configure 里追加-no-feature-gif -no-feature-ftp -no-feature-dns-lookup这类特性开关按需裁剪Qt 5.14.2 提供了非常细粒度的-no-feature-*选项。不过我要提醒一句刚开始别裁太狠有些功能看起来没用实际代码里可能间接依赖等编译报错再去排查很费劲。建议先全量编译跑通再逐步裁剪。第三-optimize-size参数在 configure 里已经加入它让编译器按照-Os级别优化在可执行文件体积和运行速度之间取了一个偏体积的平衡点。如果你对性能要求更高可以把-optimize-size改成 -optimize-ful...6.2 中文字体和缺字方块问题很多人费劲把静态 Qt 程序跑到板子上结果界面上所有中文都变成了方块。这不是代码 bug而是目标板系统里没有可用的中文字体文件。几个解决思路把字体内嵌进 Qt 资源文件。把NotoSansCJKsc-Regular.otf或一个精简的 ttf 放进.qrc程序启动时用QFontDatabase::addApplicationFont(:/fonts/your_font.ttf)注册再用对应字体族名设置全局字体。这是最稳妥的方案不依赖目标板的文件系统。给目标板补字体目录。把字体的拷贝到板子的/usr/share/fonts下设置环境变量QT_QPA_FONTDIR/usr/share/fonts。如果 Qt 编译时带了 fontconfig这一步更自然但我通常不在静态编译里引入 fontconfig因为它是又一个要交叉编译的依赖。纯英文界面。如果业务允许直接不做中文字体支持省掉体积也省掉麻烦。我个人强烈推荐第一种字体内嵌资源因为静态编译的意义就在于一个文件走天下字体放进资源文件才符合这个精神。6.3 真正的全静态和 glibc 的纠缠这篇文章标题叫静态交叉编译但我要诚实地告诉你Qt 本身的静态只是绝大部分静态。因为链接器默认会把目标板系统的 libc、libstdc 作为动态依赖保留。要验证是否是全静态在板子上跑ldd ./myapp如果输出全是statically linked说明连 glibc 也链进去了如果还有几条libc.so.6、libstdc.so.6说明系统库还是动态的。很多嵌入式板子的系统里自带 glibc 和 libstdc 动态库所以 Qt 静态 系统库动态这个混合状态完全能跑而且我建议保持这种状态。为什么会这样因为想让二进制完全静态需要在编译时加-static并确保 sysroot 里有齐全的 libc 静态库像libc.a、libm.a这些。全静态链接少数 glibc 版本会出现 DNS 解析问题比如程序里用QNetworkAccessManager访问域名时卡住不动排查起来非常难受。如果确实需要尽可能静的二进制可以在.pro里追加QMAKE_LFLAGS -static-libgcc -static-libstdc先把 gcc 和 C 标准库静态链进剩下 libc 保持动态。这是一个很好的折中方案既能减少目标板的依赖又规避了全静态 glibc 的 DNS 隐患。6.4 给 Qt 加入 OpenSSL 支持扩展很多 HMI 项目后期都会遇到 HTTPS 请求的需求比如对接云平台、设备管理接口。Qt 的QNetworkAccessManager要支持 HTTPS依赖 OpenSSL 库。静态编译场景下有两种处理方式一是-no-openssl程序只能走 HTTP开发测试够用但上线不够安全。二是交叉编译 OpenSSL 并让 Qt 静态链接它。大致步骤wget https://www.openssl.org/source/openssl-1.1.1t.tar.gz tar xf openssl-1.1.1t.tar.gz cd openssl-1.1.1t ./Configure linux-aarch64 no-shared no-asm --prefix/usr/aarch64-linux-gnu make -j$(nproc) sudo make install然后回到 Qt 的 configure把原来的-no-openssl换成-openssl-linked -I/usr/aarch64-linux-gnu/include -L/usr/aarch64-linux-gnu/libno-shared保证了 OpenSSL 以静态库形式编译这样 Qt 才能把它链进最终的可执行文件。注意这里没加-no-asm如果交叉编译器对某些汇编指令支持不好再加上去。这一步会显著增加编译时间也需要目标板的内核支持足够的新特性。作为扩展方案我只在确有 HTTPS 需求时才会引入绝大多数纯本地的 HMI 界面用不上。6.5 一个实际的体积参考数据我把一个带 QWidget、QPushButton 和 QLabel 的测试程序以静态方式编译后做了简单测量状态体积正常编译未 strip约 18MBstrip 之后约 13MB裁剪部分 Qt feature 后约 11MB不同模块使用情况差异很大如果你只用 QtCore 不用 Widgets体积会小很多。这个数据仅供参考但它能帮你提前预估目标板的存储占用。我在实际交付这类项目时还有一个习惯版本号和时间戳在编译阶段写进程序里方便后面在板子上用myapp --version确认固件版本。这个习惯在静态编译工程里尤其有用因为二进制完全独立没有任何库版本信息可查唯一的追溯依据就是你编译进去的那几个数字。