ARTICLE DETAIL

资讯详情

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

Qt 5.14.2 aarch64 静态交叉编译实战手册

Qt 5.14.2 aarch64 静态交叉编译实战手册 Qt 5.14.2 在嵌入式圈子里算是个挺特殊的存在。它不像 5.12 那样被当成长期支持版本反复打磨也不像 5.15 之后那样门槛越来越高但偏偏有大量存量项目卡在这个版本上——板子上的 BSP 是它客户老产品的 SDK 是它甚至一些工业 HMI 的固件就是用它编出来的。问题在于这些项目大多跑在 aarch64 的板子上而板子的存储和运行环境往往很紧动态链接的 Qt 库一套下来动辄上百兆拷贝、升级、版本对齐都很麻烦。于是我这些年反复做的一件事就是给 Qt 5.14.2 做aarch64 静态交叉编译把整套 Qt 打进一个可执行文件里丢到板子上就能跑。这篇手册适合谁看如果你手上有一块 aarch64 的开发板或者国产化设备需要把一个 Qt 界面程序部署上去又不希望板子上再装一堆 so 库那这篇就是给你写的。我会从工具链选型一路讲到 configure 参数逐条拆解、依赖库的编译顺序、静态插件的处理再到实际跑起来之后踩到的坑。整个过程我在不同宿主机上复现过很多次下面的步骤都是能照着抄的但我会把每个选择的理由讲清楚这样你遇到变体环境时也能自己判断怎么改。1. 先想清楚为什么非要走静态交叉编译这条路1.1 静态链接在 aarch64 设备上到底解决了什么先说结论静态编译的核心价值不是省空间这么简单而是把运行时的依赖不确定性降到最低。动态链接的 Qt 程序在开发机上跑得好好的拷到板子上经常因为libQt5Core.so.5找不到、版本对不上、或者libstdc的 GLIBCXX 符号版本不够而直接崩掉。板子上的根文件系统往往是裁剪过的你要么手动塞库要么在板子上再装一遍 Qt两条路都很难受。静态编译之后Qt 的那些库QtCore、QtGui、QtWidgets、QtNetwork 等等全部以.a的形式被链接进最终的可执行文件运行时不再需要这些 so。整个程序变成一个文件拷贝、升级、回滚都变得极其简单。我做过一个对比同一套界面程序动态版本部署包含 Qt 库大概 120MB静态版本单个可执行文件大概 25MB而且板子上不需要动任何系统目录。当然静态链接不是没有代价。最大的代价是libc 仍然是动态的——除非你做全静态链接-static连 libc 一起静态那个坑更深否则 glibc 的版本匹配问题依然存在。所以静态编译解决的是Qt 层依赖不是所有依赖。这一点很多教程含糊带过实际用起来才发现被坑。1.2 aarch64 交叉编译的典型使用场景aarch64 交叉编译最常见的使用场景有几类。一类是国产化替代项目设备用的是 ARM64 处理器宿主机是 x86_64 的 Ubuntu两者架构不同必须在宿主机上编出能在目标板运行的二进制。另一类是嵌入式网关或工业控制器设备算力有限不适合在上面跑完整编译环境只能交叉编译。我最近接触的一个场景是这样的设备上要跑一个本地服务有人在上面移植了 aarch64 版的 nginx用来做局域网内的静态资源分发和诊断页面同时又要有一个图形化的本地面板做状态展示和参数配置。这种组合里Qt 程序如果做成动态链接就要在设备上维护一套 Qt 运行库和 nginx 共用同一套根文件系统时库的版本管理会变得很乱做成静态可执行文件之后Qt 那部分就完全独立了nginx 的服务目录、配置目录怎么改都不会影响界面程序。所以判断标准很直白终端数量多、升级频繁、根文件系统不方便改动走静态只有一两台设备、开发调试为主、有完整的包管理动态更省事。1.3 整体方案与关键取舍整体方案可以拆成五步准备宿主机和 aarch64 交叉工具链编译 Qt 依赖的第三方静态库给 Qt 写或改一份 aarch64 的 mkspec用配置好的参数跑 configure最后 make 并安装。听起来不复杂但每一步都有关键取舍。第一个取舍是工具链选哪家。市面上常见的有 ARM 官方历史版本、Linaro 的 GCC 发行版、Bootlin 提供的工具链以及自己用 crosstool-ng 构建。选错工具链的后果是后面 Qt 编译期报一堆莫名其妙的内联汇编或者头文件错误。第二个取舍是用 Qt 自带的第三方库还是系统库。静态编译场景下我强烈建议尽可能用 Qt 自带的-qt-zlib、-qt-libpng之类因为它们会跟着 Qt 一起被静态化避免外部静态库的依赖传递问题。第三个取舍是平台插件怎么选嵌入式上一般是linuxfb或eglfs而不是桌面上的xcb。下面我就按这个顺序一步步展开。2. 宿主机环境与 aarch64 工具链怎么选、怎么验2.1 宿主机的选择与基础依赖清单宿主机我一般用 Ubuntu 18.04 或 20.04 的 x86_64 版本这两个版本的 glibc 比较老编出来的东西兼容性更好用太新的发行版比如 22.04 之后的交叉工具链和 Qt 编译脚本偶尔会遇到 gcc 版本相关的告警。内存方面静态编译 Qt 是吃资源的我一般给虚拟机配 8GB 以上最好 16GB不然并行编译的时候会 OOM。基础依赖用 apt 装一批就够sudo apt update sudo apt install -y build-essential perl python3 \ git bison flex gperf \ libncurses5-dev texinfo \ pkg-config wget这里面bison、flex、gperf是 Qt 编译过程中生成解析器需要的少一个都会在中途configure或make时报错。perl和python3是 Qt 的一部分构建脚本依赖。libncurses5-dev主要是为了后面可能要用的其他工具。提示不要图省事用 root 直接操作也不要把 Qt 编到/usr/local。我习惯在$HOME下建一个work目录所有交叉编译产物都在里面出问题直接删目录重来不会污染系统。2.2 交叉工具链的挑选与验证方法工具链这块我踩过最多的坑。早期用过一个来源不明的 GCC结果编译 Qt 的时候qatomic里的内联汇编直接不识别报了一屏错。后来固定用两类一是 Linaro 针对 aarch64 的 GCC 发行版二是 Bootlin 编译好的工具链。现在更常用的是自己用 crosstool-ng 构建或者直接用目标板 BSP 里附带的那套 toolchain。如果有 BSP 自带的工具链优先用它因为它的 glibc 版本和目标板根文件系统是匹配的你能省掉后面最难啃的 sysroot 问题。拿到工具链之后第一件事是验证它能不能用。把工具链解压加入 PATH然后export PATH/opt/toolchains/gcc-aarch64-linux-gnu/bin:$PATH aarch64-linux-gnu-gcc -v输出里要能看到Target: aarch64-linux-gnu并且 gcc 版本、glibc 版本都正常打印。再写个小程序验证链接echo int main(){return 0;} t.c aarch64-linux-gnu-gcc t.c -o t.elf file t.elffile应该输出ELF 64-bit LSB executable, ARM aarch64。注意前缀名有的工具链前缀是aarch64-none-linux-gnu-有的是aarch64-linux-gnu-后面写 mkspec 时要和实际一致我见过有人复制别人的配置忘了改前缀编了半天报找不到编译器。2.3 目录规划与 sysroot 的处理思路目录规划看起来是小事但静态交叉编译里它直接影响后面参数怎么写。我的习惯是这样~/work/ ├── toolchain/ # 交叉工具链 ├── sysroot/ # 目标系统的头文件和库可选 ├── src/ # 源码Qt 和第三方库 ├── build/ # Qt 的 out-of-source 构建目录 └── install/ # 安装目录aarch64-qt-5.14.2-staticsysroot 是这里最关键的概念。简单说它就是目标板根文件系统的一个副本包含目标板的/usr/include、/usr/lib。交叉编译时编译器去 sysroot 里找第三方库的头和.a/.so。如果你有 BSP 提供的完整 sysroot一定要用配合--sysroot参数能避免大量找不到 libc 头文件或者链接到了宿主机 x86 库的问题。如果没有现成 sysroot那就退一步尽量用 Qt 自带的第三方库少依赖系统库然后把第三方库统一装到install/下的一个前缀里通过-I和-L单独指向。我后面讲依赖库编译时用的就是这种自建前缀的方式不依赖完整 sysroot。注意sysroot 和工具链的 glibc 版本要能对上否则你会看到error: __GLIBC_2.28 not found这种链接错误或者运行时提示version GLIBC_2.XX not found。这不是 Qt 的问题是基础库版本不匹配先解决它再往下走。3. 静态依赖库的编译顺序与关键参数3.1 依赖关系梳理为什么顺序不能乱Qt 的第三方依赖是有明确依赖关系的。比方说libpng依赖zlibfreetype可能依赖zlib和libpng取决配置libjpeg独立openssl独立。如果你先编 libpng 再编 zliblibpng 的 configure 会找不到 zlib 头文件要么报错要么悄悄编成不依赖 zlib 的版本结果就是后来 Qt 链接图像格式的时候出问题。我总结的编译顺序是zlib → libpng → libjpeg → freetype → openssl。其中前四个是图像和字体相关openssl 是网络相关。其实如果你愿意用 Qt 自带的-qt-zlib等前四个都可以不编Qt 源码树里src/3rdparty/下都有。这也是我推荐的默认做法。但是有两个例外必须单独编OpenSSLQt 不带且需要匹配版本和freetype当目标板字体渲染有特殊需求时。所以下面我重点讲这两个的交叉编译其余说明用 Qt 自带的理由。3.2 freetype 与 openssl 的交叉编译实操先装到一个统一前缀比如~/work/install/aarch64-deps。freetype 编译大致这样cd ~/work/src/freetype-2.10.4 ./configure --hostaarch64-linux-gnu \ --prefix$HOME/work/install/aarch64-deps \ --without-harfbuzz \ --without-bzip2 \ --disable-shared --enable-static make -j$(nproc) make install--host是告诉 autotools 这是交叉编译别用宿主机编译器。--disable-shared只出静态库。编完检查install/aarch64-deps/lib下是不是有libfreetype.a用aarch64-linux-gnu-ar t libfreetype.a确认架构对。OpenSSL 的编译方式不太一样它不用 configure 而用 Configurecd ~/work/src/openssl-1.1.1w ./Configure linux-aarch64 \ --prefix$HOME/work/install/aarch64-deps \ --openssldir$HOME/work/install/aarch64-deps/ssl \ no-shared no-tests no-comp \ CCaarch64-linux-gnu-gcc make -j$(nproc) make install_sw注意linux-aarch64是 OpenSSL 自带的目标配置名不要自己瞎写。no-shared出静态库no-tests跳过测试省时间。Qt 5.14.2 对 OpenSSL 1.1.1 系列支持最好如果你手上是 3.x链接时可能会出现 API 变更导致编译失败需要额外的补丁这一点要有心理准备。提示make install_sw比make install快很多它只装软件相关部分不装文档。交叉编译场景下我们根本不需要文档。3.3 静态库的坑-fPIC 和依赖传递静态库有两个坑必须提前说。第一是-fPIC。如果你的 Qt 程序最终要编成共享库或者要满足某些链接器的 PIE 要求第三方静态库必须是位置无关代码。有些库的 configure 默认不带-fPIC需要在 CFLAGS 里手动加export CFLAGS-fPIC export CXXFLAGS-fPIC第二是依赖传递。静态库本身不记录它依赖谁不像动态库有 NEEDED 字段所以你在链接 Qt 程序时链接器需要知道所有静态库的依赖顺序。这也是为什么我推荐用-qt-zlib这类方式让 Qt 自己管理这些静态库的内部依赖你不需要在.pro里手写一长串LIBS -lz -lpng ...。还有一点静态库的编译工具链要和 Qt 一致。不要用宿主机的 gcc 编出 aarch64 的库也不要拿 x86 的.a混进来。验证方式很简单aarch64-linux-gnu-objdump -f libxxx.a | head看架构是不是aarch64。4. Qt 5.14.2 的 mkspec 与 configure 参数逐条拆解4.1 给 aarch64 写一份 mkspecQt 5.14.2 的跨平台配置走的是-device或-xplatform机制。我习惯直接在 Qt 源码树的qtbase/mkspecs/devices/下建一个目录比如linux-aarch64-gnu-g里面放一个qmake.confMAKEFILE_GENERATOR UNIX CONFIG incremental QMAKE_INCREMENTAL_STYLE sublib include(../common/linux.conf) include(../common/gcc-base-unix.conf) include(../common/g-unix.conf) QT_QPA_DEFAULT_PLATFORM linuxfb 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_AR aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY aarch64-linux-gnu-objcopy QMAKE_NM aarch64-linux-gnu-nm -P QMAKE_STRIP aarch64-linux-gnu-strip load(qt_config)这里有几个点要解释。QT_QPA_DEFAULT_PLATFORM linuxfb决定了 Qt 默认用哪个平台插件嵌入式无 X11 的场景就用linuxfb如果你的板子有 EGL 和 GPU可以改成eglfs。QMAKE_AR后面的cqs是归档选项别漏了。所有工具的前缀必须和实际工具链一致。如果你不想改 Qt 源码树也可以用-device-option CROSS_COMPILEaarch64-linux-gnu-配合已有的 device 配置但那样对参数的控制不如自己写 mkspec 灵活。4.2 configure 命令的完整形态与参数含义下面这份是我反复验证过能跑通的 configure 命令假设第三方依赖装在~/work/install/aarch64-depscd ~/work/build/qt-5.14.2-static ~/work/src/qt-everywhere-src-5.14.2/configure \ -prefix /opt/aarch64-qt-5.14.2-static \ -opensource -confirm-license \ -release -static \ -device linux-aarch64-gnu-g \ -device-option CROSS_COMPILEaarch64-linux-gnu- \ -sysroot $HOME/work/install/aarch64-deps \ -no-gcc-sysroot \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre -qt-harfbuzz \ -openssl-linked \ -I $HOME/work/install/aarch64-deps/include \ -L $HOME/work/install/aarch64-deps/lib \ -nomake examples -nomake tests \ -no-opengl \ -no-icu -no-iconv \ -no-xcb -no-glib \ -skip qtwebengine -skip qtwebview \ -no-feature-vulkan \ -sql-sqlite -qt-sqlite \ -reduce-exports \ -v参数很多我挑关键的讲。-static是核心生成.a而不是.so。-release关掉调试信息减小体积。-qt-*系列让 Qt 编译自己的第三方库副本避免外部依赖。-openssl-linked让 Qt 在链接阶段就把 OpenSSL 静态链进去而不是运行时动态加载。-no-opengl是重要取舍。如果你的程序不需要 OpenGL关掉它能让编译顺利很多因为交叉编译 OpenGL 常常牵扯到 EGL、Mesa 等一堆系统库。如果确实需要改成-opengl es2并确保 sysroot 里有对应的 EGL 头文件和库。-no-icu -no-iconv是裁剪ICU 很大静态链接进去会让可执行文件爆炸性增长iconv 在纯英文或固定编码场景下也不需要。-skip qtwebengine建议无脑加上。QtWebEngine 实际上就是打包的 Chromium交叉编译它是一场噩梦编一次好几个小时还经常失败静态链接更是不可能Chromium 的架构不支持简单静态化。如果你的界面程序需要 Web 渲染用 QWebView 走 QtWebKit 或者换方案别碰 WebEngine。4.3 配置阶段的自检与常见报错configure 完成之后会打印一个长长的配置摘要Build options和Qt modules。一定要把这部分截图或存下来因为后面编译出问题第一个要确认的就是某个模块是不是真的被启用了。假如看到QPA plugins里没有linuxfb说明你的 mkspec 里QT_QPA_DEFAULT_PLATFORM没生效检查 include 顺序和拼写。如果OpenSSL那一行是no说明-openssl-linked没找到你的库检查-I和-L路径以及libssl.a、libcrypto.a是否在里面。常见配置报错还有Project ERROR: Unknown module(s) in QT: xxx一般是某个 skip 掉的模块被别的模块依赖还有Cannot find libz.a那是-qt-zlib和外部-lz混用了检查有没有同时加-system-zlib。提示-v参数会让 configure 输出详细过程出错时非常有用。我建议整个配置阶段都带着它出问题一眼能看到是哪一步失败。5. 编译、安装与静态链接验证的完整实操5.1 make 阶段的资源管理与时间预期configure 通过之后就是make。这一步最耗时也最容易因为内存不足失败。我的做法是控制并行度make -j$(nproc) 21 | tee build.log如果机器内存小于 16GB把-j调成$(nproc)/2否则链接阶段某个大模块比如 QtWebKit 或者 QtGui可能会 OOM。整个 Qt 5.14.2 静态编译在 8 核 16GB 的机器上大概要 40 分钟到 1.5 小时具体看裁剪程度。编的过程中可以用tail -f build.log观察。如果中途失败别急着重跑整个makemake本身是增量的改了相关文件之后重新make会从失败的地方继续。但如果失败和 configure 选项有关那就得make distclean重来——这也是我强调用 out-of-source 构建的原因源码目录干干净净重建成本低。make成功之后make install产物会装到-prefix指定的/opt/aarch64-qt-5.14.2-static需要写权限或者你把它换成$HOME/work/install/...。装完之后最该检查的是lib目录应该全是.a文件没有.so。如果发现.so说明-static没生效或者某个模块没被静态化。5.2 用一个小程序验证静态链接是否成立验证环节我写一个最小程序只用一个 QLabel看它能不能静态编出来并且在 aarch64 上跑。QT core gui widgets TARGET statictest TEMPLATE app CONFIG static SOURCES main.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(hello aarch64); label.show(); return app.exec(); }编译时不走qmake直接用一个交叉编译版的 qmake 生成 Makefile/opt/aarch64-qt-5.14.2-static/bin/qmake statictest.pro make file statictestfile输出要是ARM aarch64。再用aarch64-linux-gnu-readelf -d statictest看动态依赖理想情况下只有libc.so.6、libm.so.6、libpthread.so.0之类的基础库不应该出现任何 libQt5.so*。如果出现了说明链接到了动态 Qt检查 qmake 用的是不是刚编出来的那个。注意静态编译之后Qt 的插件平台插件、图像格式插件默认不会自动链进去需要在代码里显式导入。这点很多人第一次用会懵程序能编出来但一跑就报This application failed to start because no Qt platform plugin could be initialized。5.3 静态插件的处理与部署到 aarch64 设备静态插件有两种处理方式。一种是在代码里手动导入#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin) Q_IMPORT_PLUGIN(QJpegPlugin)另一种是在.pro里用QTPLUGINQTPLUGIN qlinuxfb qjpeg我一般用第一种因为可控性强出问题的时候清楚知道哪个插件被链进来了。平台插件名要和你实际启用的 QPA 一致linuxfb对应QLinuxFbIntegrationPlugineglfs对应QEglFSIntegrationPlugin。部署到设备时把静态可执行文件拷过去设置运行环境就行。linuxfb 场景下通常需要指定 framebuffer 设备export QT_QPA_PLATFORMlinuxfb export QT_QPA_FB_DRM1 ./statictest -platform linuxfb如果板子上同时跑着 nginx 做本地服务要注意 framebuffer 设备/dev/fb0不能被两个程序抢占Qt 界面程序和别的图形程序不能同时用同一个 fb 设备。这种资源冲突在静态部署时反而更明显因为程序独立了没人帮你协调。权限方面访问/dev/fb0一般要 root 或者在video组部署脚本里加个chmod或者用 systemd 指定用户比每次手动 sudo 更靠谱。6. 踩过的坑编译期与运行期问题排查实录6.1 编译阶段的典型错误与分析路径编译阶段的错误基本集中在三类找不到头文件、找不到库、架构不匹配。找不到头文件错误形如fatal error: zlib.h: No such file or directory。原因通常是-I路径没指对或者依赖库压根没编译成功。排查方法先在 shell 里手动确认ls $HOME/work/install/aarch64-deps/include/zlib.h再看 configure 里-I是不是这个路径。注意有些库把头文件装到include/xxx/子目录需要额外加一级。找不到库错误形如cannot find -lssl。先确认libssl.a在-L目录下文件名对不对然后确认你用的是.a不是.so。有时候库文件在但链接器只认.so那是因为-L路径下同时存在两种而-static又没传到位。架构不匹配错误形如file in wrong format或者Relocations in generic ELF (EM: 62)。这是最隐蔽也最常见的意味着你链接了一个 x86_64 的库。原因往往是某个库的 configure 没识别出交叉编译用宿主机 gcc 编了。用file或者readelf -h逐个检查.a文件就能定位。我在一篇讲 aarch64 移植的文章里见过类似问题别人编译 nginx 的 aarch64 版时也是因为混进了 x86 库导致链接失败。6.2 运行阶段的典型问题与定位手法运行阶段最常见的是插件找不到。前面说的no Qt platform plugin could be initialized就是这个。解决办法是Q_IMPORT_PLUGIN或者确保QTPLUGIN生效。确认方法strings statictest | grep -i linuxfb能找到相关字符串说明插件被链进去了。第二个常见问题是字体不显示或者显示成方块。静态编译时如果没带-qt-freetype或者 freetype 字体路径没配好Qt 找不到字体文件。解决办法是在程序启动前设置QFontDatabase::addApplicationFont(/path/to/font.ttf)或者把字体文件一起打包运行时加载。第三个是 glibc 版本不匹配运行时提示version GLIBC_2.29 not found。这说明你的工具链比目标板的 glibc 新。这是静态编译解决不了的问题只能换工具链或者升级目标板 glibc。这个坑我在讲 aarch64 服务移植的内容里也反复看到不是 Qt 独有。6.3 问题速查表现象常见原因排查与解决找不到头文件-I路径错误或依赖未编手动ls确认路径重新编依赖找不到 -lxxx库名或路径不对或只有 .so确认.a存在检查-L与-staticwrong format / EM:62混入 x86_64 库file逐个检查.a重编问题库no Qt platform plugin静态插件未导入加Q_IMPORT_PLUGIN或QTPLUGIN字体显示方块freetype 或字体路径问题手动加载字体文件GLIBC 版本不匹配工具链 glibc 过新换工具链或升级目标板 glibc编译过程 OOM并行度太高降低make -j数值程序启动段错误库顺序或 ABI 不匹配检查工具链与依赖 ABI 是否一致这张表是我这些年反复用到的遇到问题的时候先按表对号入座能省不少时间。7. 一些实操心得与后续可扩展的方向静态交叉编译这件事最深的体会是把不确定性前置。工具链的 glibc 版本、依赖库的架构、configure 参数的含义这些问题在开始make之前就要确认清楚而不是等到链接报错再去查。我后来的习惯是每编完一个依赖库就用aarch64-linux-gnu-objdump -f检查一次架构把问题挡在前面。关于裁剪我的建议是先跑通一个最小可用配置再逐步加模块。一上来就把参数堆满出错了根本不知道是哪个选项的问题。我的最小配置就是-no-opengl -no-icu -no-iconv -no-xcb -skip qtwebengine这套能覆盖 80% 的嵌入式界面场景。等这套跑通了再按需加 OpenGL、加网络、加数据库。如果你后续要给多个不同的 aarch64 设备做部署可以考虑把整个编译配置脚本化包括工具链路径、依赖版本、configure 参数都固化到脚本里每次换设备改几个变量就行。我目前就是这么做的一个build.sh加一份config.env从零到出可执行文件基本是自动化流程比手动敲一遍稳得多。另外如果设备上还有 nginx 之类的服务在跑记得把 Qt 程序对 framebuffer、串口、GPIO 这些独占资源的占用情况和服务的资源规划一起考虑不然单独跑都没事一起跑就互相干扰。
返回列表