Qt 5.14.2在aarch64嵌入式Linux下的静态交叉编译手册

Qt 5.14.2在aarch64嵌入式Linux下的静态交叉编译手册 从事嵌入式 Linux 开发这几年遇到频率最高的需求就是在 x86 编译机上给 aarch64 的 ARM 板子交叉编译 Qt 程序。以前一直用动态链接每次部署前都得把 /opt/Qt 下的一堆 .so 逐一拷到目标板漏了哪个就报 libQt5Widgets.so.5: cannot open shared object file。踩的坑多了我干脆把 Qt 5.14.2 在 aarch64 下的静态交叉编译完整做了一遍现在项目部署动作从拷动态库变成拷一个可执行文件。这篇手册就是把整个从零搭建过程完整记录下来覆盖工具链准备、Qt configure 参数、静态编译要点、部署验证和典型问题排查适合刚开始碰嵌入式 Qt 开发、或者正在被动态库部署搞到头疼的人。我不打算一上来就丢配置命令因为网上很多教程只给参数不解释换个板子就全废了。这次尽量把每个关键决策的背景讲清楚哪怕你用的是 Qt 5.15、Qt 6 或者别的目标架构思路也能复用。整条路线是先明确为什么做静态交叉编译然后把主机环境、交叉工具链和依赖库准备好接着编译 Qt 本体再写一个最小程序部署到板子上验证最后把常见坑和工程化建议整理出来。1. 为什么非要做静态交叉编译先聊聊痛点先说说动态部署有多麻烦。一个典型的 Qt 动态链接程序运行依赖除了 Qt 的 libQt5Core、libQt5Gui、libQt5Widgets还经常带上平台插件、图片格式插件、字体插件和一堆第三方库。目标板文件系统一旦跟编译机不一致程序跑起来就是各种 cannot open shared object file。印象最深的一次现场部署的程序在开发板上正常换到另一块同样型号的板子上就崩了最后发现是某次系统更新把带的 libpng 版本改了Qt 在启动时加载旧插件直接段错误。这种问题定位起来特别耗时往往要反复对比编译环境和目标环境。1.1 动态部署的“运行时地狱”动态链接本身没有错但在嵌入式小批量生产环境里它会把简单的“拷贝文件”变成“同步一整套运行环境”。你要保证目标板的 /usr/lib 里每个库的版本都正确还要保证 Qt 插件目录里的 .so 跟主程序匹配。一旦出了问题新手很难分辨到底是代码 bug、库版本冲突还是链接配置错误。更麻烦的是现场设备往往没有调试工具连 gdb 和 strace 都不一定装得下排查全靠肉眼看 log。1.2 静态编译到底换来了什么收益把 Qt 编成静态库之后最终产出的可执行文件把用到的 Qt 功能、插件和资源全部揉在了一个 ELF 里部署时只需要保证目标 Linux 系统的 libc 等基础库可用。这对产品迭代来说省了太多事。另外静态编译的程序启动时少了动态链接器和按需加载各种 .so 的阶段冷启动会明显变快这对很多带触摸交互的嵌入式设备尤其重要。体积上确实会变大但换来的是跨设备复用能力代价是值得的。1.3 为什么选 Qt 5.14.2Qt 5.14.2 是 Qt 5 分支里的 LTS 版本发布晚、修了不少细节问题社区资料也全。很多工业嵌入式项目现在还在用这个版本无论是裁模块还是定制 mkspec网上能找到的实践经验都要比 Qt 6 丰富。aarch64 作为目标平台Qt 官方在 5.12 之后就正式纳入支持范围5.14.2 的源码包自带 linux-aarch64-gnu-g 的 mkspec这也是我选它的原因。加上生产环境里大量客户现场仍是 Ubuntu 20.04 / gcc 9 组合Qt 5.14.2 对它们的兼容性最稳。2. 环境准备工具链、依赖和 sysroot做交叉编译的第一步是把“能在 x86 上运行、却能产出 aarch64 指令”的这套工具链准备好同时理清目标板系统的根文件系统在哪里。很多人在这一步栽跟头不是 gcc 装不上而是没搞明白 sysroot 和库搜索路径。2.1 主机系统选择与基础依赖我这边统一用 Ubuntu 20.04 x86_64 作为编译机其他发行版思路一样只是包管理命令不同。除了系统自带的基础工具Qt 的 configure 脚本依赖 Perl编译过程需要 gcc/g、make、python3 等。建议在开始前把常用包都装上sudo apt update sudo apt install -y build-essential perl python3 python3-pip cmake \ libglib2.0-dev libxkbcommon-dev这里不需要在主机上装 x11 相关开发库因为后面我们做嵌入式目标板一般不走 X11而且 Qt configure 只会在编译主机工具时用到主机自身的库。装好之后先用 gcc --version 确认主机编译器工作正常这能避免后面一堆莫名其妙的编译错误。2.2 交叉编译工具链的安装Ubuntu 官方源里有现成的 aarch64 交叉工具链安装很简单sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu装完以后工具链统一以 aarch64-linux-gnu- 为前缀包括 aarch64-linux-gnu-gcc、aarch64-linux-gnu-g、aarch64-linux-gnu-readelf 等。简单写个 hello.c 验证一下cat hello.c EOF #include stdio.h int main(void) { printf(aarch64 ok\n); return 0; } EOF aarch64-linux-gnu-gcc hello.c -o hello.arm aarch64-linux-gnu-readelf -h hello.arm | grep Machine正常情况下 Machine 字段应该显示 AArch64。这一步很关键相当于确认整个交叉编译链路能通之后再跑到 Qt 里排查问题就会简单很多。值得注意的是apt 安装的交叉 gcc 版本在我的环境里是 9.3.0这个版本跟 Qt 5.14.2 配合得非常顺不会出现 C 标准库 ABI 冲突的问题。如果你用更新的 gcc 12/13Qt 源码编译时偶尔会报一些和旧代码有关的标准库头文件兼容问题处理起来还要额外花费精力。2.3 外源库的交叉编译能省则省Qt 编译本身会用到一批第三方库常见的是 zlib、libpng、libjpeg、freetype、harfbuzz、xcb 等。我的建议是没有特殊需求时优先用 Qt 自带源码里的第三方库配置参数里以 -qt- 开头的开关就是干这件事的。可以用 -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz 一次性让 Qt 自己去编译这些组件省去单独交叉编译一大串依赖的折磨。如果你的项目需要用 OpenSSL 做 HTTPS或者要用 SQLite / MySQL 数据库那确实得先交叉编译相应的第三方库。以 zlib 为例说明一下独立交叉编译的标准动作wget https://zlib.net/zlib-1.2.13.tar.gz tar xzf zlib-1.2.13.tar.gz cd zlib-1.2.13 export CCaarch64-linux-gnu-gcc ./configure --prefix/usr/aarch64-linux-gnu make -j$(nproc) sudo make install这种交叉编译的关键在于把 --prefix 指到交叉工具链自己的 sysroot 目录里。这样后续编译其他库时编译器能自动从 /usr/aarch64-linux-gnu 下面找到 zlib 的头文件和 .a/.so不会跟主机 x86 的库混在一起。2.4 sysroot 目录设计交叉编译里的 sysroot 可以理解成“目标板文件系统的头文件和库的集合”。用 apt 装的交叉编译器默认 sysroot 是 /usr/aarch64-linux-gnu里面已经带了 aarch64 版本的 libc、libstdc 等基础库。假如你手头有板卡厂商提供的 SDK里面可能自带一套 buildroot 或者 Yocto 的 staging 目录更贴合实际目标板原理是一样的编译时通过 --sysroot 或者编译器默认路径指明头文件和库的搜索位置。我自己习惯在 /opt 下建一个目录结构把独立编译的第三方库统一放进去再在 configure Qt 时引用它。比如/opt/aarch64-sysroot/ ├── usr/ │ ├── include/ │ └── lib/这样做的目的只有一个隔离。主机上的库、目标系统的库、自己编译的库三条路径互不干扰。之后不管是编译 Qt 还是编译上层应用出问题了都能快速定位是哪个路径的库没有对上。3. Qt 5.14.2 的源码配置与编译环境备好之后进入整个手册的核心Qt 源码的 configure 和编译。这里最容易翻车的其实不是 make而是 configure 的参数。参数一旦不对轻则 Qt 编出来缺模块重则目标板上运行直接段错误或无法启动。3.1 源码下载与目录准备Qt 5.14.2 的完整源码包叫 qt-everywhere-src-5.14.2直接从 Qt 官网归档目录下载即可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。源码包里已经包含了 qmake、mkspecs、qtbase、qtdeclarative 等所有模块。要注意的是整个源码树对磁盘空间有一定要求建议预留至少 10GB 空间编译时临时文件较大。3.2 configure 参数逐个捋我实际使用并验证过的完整配置命令如下假设编译器是 apt 的 aarch64-linux-gnu-gcc 9.3.0./configure \ -static \ -release \ -opensource \ -confirm-license \ -xplatform linux-aarch64-gnu-g \ -prefix /opt/qt-5.14.2-aarch64 \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz \ -no-opengl \ -no-dbus \ -no-icu \ -no-cups \ -no-glib \ -qt-xcb \ -skip qtwebengine \ -nomake examples -nomake tests -nomake benchmarks \ -silent下面逐个解释我为什么会这么选-static 是最核心的选项它让 Qt 编译出 .a 静态库而不是 .so 动态库后续链接应用时把所有代码打包进可执行文件。-release 指定生成 release 版本体积更小性能也更好嵌入式环境基本不会用 debug 版。-opensource -confirm-license 表示接受开源协议条款。-xplatform linux-aarch64-gnu-g 是交叉编译的关键开关。它告诉 configure 使用 qtbase/mkspecs/linux-aarch64-gnu-g 这个平台描述文件里面定义了目标架构、编译器、链接器和各类默认参数。如果你用板卡 SDK 里的编译器可能需要在这个文件里调整 QMAKE_CC、QMAKE_CXX、QMAKE_LINK 等变量。-prefix 指定安装路径我习惯用独立的 /opt/qt-5.14.2-aarch64不和主机上其他 Qt 版本混在一起。后面应用编译时的 qmake、mkspec 都从这里取。-qt-zlib、-qt-libpng、-qt-libjpeg、-qt-freetype、-qt-harfbuzz 这几个选项表示使用 Qt 源码内嵌的第三方库而不是去系统里找。这是减少交叉编译复杂度的关键一步前面也说过。-no-opengl 是因为我这边很多目标板没有 GPU也不需要 OpenGL 相关能力。如果你的板子支持 EGL/OpenGL ES可以改成 -opengl es2否则不要贸然开启否则会报缺少 EGL 头文件的错误。-no-dbus、-no-icu、-no-cups、-no-glib 分别关掉 D-Bus、ICU、CUPS 和 GLib 支持。嵌入式设备通常用不到这些关掉可以让编译更快、体积更小也能减少对目标系统库的依赖。-qt-xcb 是让 Qt 自带的 xcb 平台插件链接到 Qt 内部。不过要注意xcb 底层仍然依赖 xcb 库的头文件如果目标板根本没有 X11 环境我会建议改成 -no-xcb之后用 linuxfb 插件跑 framebuffer 模式这样能让整个依赖更干净。对于绝大多数 LCD 屏直连的开发板linuxfb 就够了而且部署时不用考虑 xcb 依赖问题。-skip qtwebengine 跳过 WebEngine 模块。这个模块编译极其耗时依赖 Chromium嵌入式项目一般用不到跳过能省下大量编译时间。-nomake examples -nomake tests -nomake benchmarks 省去编译示例和测试代码。-silent 只输出关键错误信息避免日志刷屏。如果你不确定自己的配置是否合理可以先跑一遍 configure然后查看输出末尾的摘要它会列出启用的模块、插件和特性。发现不需要的大模块在 configure 阶段直接剔除比事后裁剪省事得多。3.3 make 与并行编译configure 通过之后直接编译。机器内存大于 8GB 的话可以用满核心make -j$(nproc)在我的机器上8 核 16 线程完整编译大概需要 25 到 40 分钟。日志建议留一份方便出错时排查make -j$(nproc) 21 | tee build.logmake 过程中如果报错不要盲搜先看输出里最上面的 error 行再去 qtbase/config.log 里查上下文。常见的问题大多是缺少某个开发包、编译器版本不兼容、或者参数写错。有时候 configure 时报错但日志被刷屏了可以用grep error config.log | tail -20来过滤。3.4 安装后的目录冒烟测试编译完成后make install安装完成后检查 /opt/qt-5.14.2-aarch64 下的目录结构正常情况下应该有 bin、include、lib、mkspecs、plugins 等目录。这里有个很直观的判断标准进入 lib 目录看到的应该是一堆 .a 文件而不是 .so 文件。如果是这样说明静态编译成功。再用安装好的 qmake 验证版本/opt/qt-5.14.2-aarch64/bin/qmake -v如果能正常打印 QMake version 和 Qt version说明工具链可用。要特别强调一点这里的 qmake 本身是可以直接运行的因为它在编译时是用主机编译器生成的负责为应用生成 Makefile真正交叉编译的目标是 Qt 的 .a 静态库。4. 用静态 Qt 写第一个目标板程序Qt 编译安装完到了验证阶段。用 qmake 交叉编译一个最简界面程序然后把它丢到 aarch64 板子上跑起来这一步通过整套工具链就算真的闭环了。4.1 qmake 交叉编译先准备一个最简单的工程工程结构就两个文件。main.cpp 内容#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello from aarch64 static Qt); label.show(); return app.exec(); }hello.pro 内容QT widgets CONFIG release TARGET hello_aarch64 TEMPLATE app然后使用交叉编译版 qmake 生成 Makefileexport PATH/opt/qt-5.14.2-aarch64/bin:$PATH cd hello qmake hello.pro make -j$(nproc)由于这个 qmake 是带 aarch64 mkspec 的生成的 Makefile 里会自动使用 aarch64-linux-gnu-g。编译完成后用 file 命令检查file hello_aarch64输出应该包含 AArch64 关键字而不是 x86-64。这步错了后面的部署也就没有意义。4.2 静态链接与插件导入如果程序只用到了 QtWidgets 基础模块上面这样直接 make 通常就能通过。但很多程序会用到图片格式解码比如 PNG、JPEG、或者需要指定平台插件这些是以插件形式存在的。动态链接时插件是 .so放进 plugins 目录自动加载静态链接时插件被编成 .a需要显式导入链接。做法有两种。第一种是在 .pro 文件里加 QTPLUGIN 列表让 qmake 把插件链接进来QTPLUGIN qlinuxfb qpng第二种是在程序源码顶部加 Q_IMPORT_PLUGIN 宏#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)两种方式本质一样都是把插件里注册过的类链接进可执行文件。如果漏了这一步运行时 Qt 可能因为找不到平台插件而直接崩溃报错类似 “could not find or load the Qt platform plugin linuxfb”。这是静态编译特有的坑动态编译基本遇不到。4.3 目标板部署与运行验证把编译产物拷贝到目标板这一步环境不同手段不同但最终目的是让程序文件出现在板子上。假设目标板可以 SSH 访问scp hello_aarch64 root192.168.1.100:/root/板子上先用 readelf 或者 ldd 看看依赖readelf -d hello_aarch64 | grep NEEDED静态 Qt 程序此时应该只剩 libc、libm、libgcc_s、libstdc 这类基础库不再出现 libQt5Widgets.so 之类的条目。这个结果就是静态编译最大的成果。运行前先确认目标板的显示环境。如果板子有 /dev/fb0framebuffer直接export QT_QPA_PLATFORMlinuxfb ./hello_aarch64如果不指定平台Qt 会默认尝试 xcb而很多精简板子根本没有 X11 服务程序会报 “xcb missing” 或 “no display found”。这也是为什么我前面建议在 configure 时关掉 xcb、用 linuxfb 作为默认平台插件的原因。5. 踩坑实录这些问题最让人头疼这部分是我真正觉得有价值的内容。整个静态交叉编译过程不下十次遇到疑难问题有些问题藏得很深光看报错很难定位。梳理几个最典型的希望后来的人少走弯路。5.1 工具链版本与 GLIBC 不匹配最大的隐藏问题Qt 静态编译 ≠ 全静态。Qt 本体确实编译进可执行文件了但最终链接时还是会动态链接到目标系统的 glibc、libstdc、libgcc_s 这些基础库。如果你用新版本主机工具链比如 gcc 12 对应的 glibc 2.34编译拷贝到一个老一点的目标系统glibc 2.28运行时就可能报./hello_aarch64: /lib/aarch64-linux-gnu/libc.so.6: version GLIBC_2.34 not found解决思路有三个。最推荐的是用板卡厂商提供的交叉 SDK它内部工具链与目标板 glibc 版本天然匹配。其次是确认目标板 glibc 版本然后按需安装对应版本的交叉工具链。再就是可以考虑在目标板系统里升级 libc但嵌入式系统牵一发动全身基本不建议。平时验证时可以在板子上执行 ldd --version 看 glibc 版本再拿这个数字对照编译机的 gcc 版本范围。5.2 链接期 undefined reference 问题编译应用时最常见的报错是undefined reference to qt_plugin_import_q... undefined reference to qt_static_plugin_q...这类问题几乎都是插件没导入导致的。静态链接下插件不是按需加载必须作为符号链接进程序。解决方法是前面说的 QTPLUGIN 或 Q_IMPORT_PLUGIN。还有一类 undefined reference 是库顺序问题当你额外链接了 openssl、zlib 等第三方库时调整 -l 参数顺序把更底层的库往后放。5.3 xcb 插件与窗口系统configure 时如果保留 -qt-xcb但主机 sysroot 里没有 xcb 头文件会出现这样的提示Basic XLib functionality test failed! You might need to modify include and library search paths by editing QMAKE_INCDIR_X11 and QMAKE_LIBDIR_X11 in .../mkspecs/linux-aarch64-gnu-g/qmake.conf对纯 framebuffer 的板子我建议直接不折腾 X11重跑 configure 时把 -qt-xcb 换成 -no-xcb。如果确实需要 X11 窗口环境就要单独交叉编译 libxcb 以及它依赖的 xcb-proto、libxau、libxdmcp 等一堆库工作量不小而且最终部署时目标板也要有对应的 X11 库和显示器驱动对大多数嵌入式项目来说性价比很低。5.4 中文字体与国际化程序能跑起来之后很快会碰到的就是界面中文显示成方块。Qt 本身不做字体渲染静态 Qt 也并不会把一个字体文件塞进可执行文件。你需要给目标系统准备中文字体比如文泉驿微米黑放到 /usr/share/fonts 下再配置 fontconfig 或者直接在代码里用 QFontDatabase::addApplicationFont 加载字体文件。如果程序还用了 Qt 的国际化 .qm 翻译文件记得把 .qm 资源放到程序能找到的相对路径或者通过 qrc 资源系统编译进二进制否则切语言时会静默失败界面仍然显示英文。5.5 体积控制strip 与模块裁剪静态编译的可执行文件动辄几十 MB这是很多刚接触的人第一反应会觉得“太大”。实际上可执行文件大不等于运行内存大静态代码页只有在用到时才会被调进内存。如果要压缩存放体积可以在 .pro 里加 QMAKE_LFLAGS_RELEASE 来 strip 符号或者编译完手动执行 aarch64-linux-gnu-strip hello_aarch64。我的一个 40MB 的 Qt 程序 strip 之后能降到 28MB 左右。更彻底的方案是在 configure 阶段裁剪特性比如 -no-feature-xxx但这需要逐个模块去看特性清单适合产品形态非常固定的场景。6. 把交叉编译固化到工程里手册记录的是“从零搭建”的过程但真实团队里不可能每次都手动敲一串 configure 参数。环境捣鼓好之后最好把它固化成脚本和配置文件让队友一条命令就能完成编译。6.1 CMake 工具链文件虽然 Qt 5 的官方构建系统是 qmake但不少项目的上层逻辑已经用了 CMake。给一套可复制的 CMake 工具链文件# toolchain-aarch64.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g) set(CMAKE_FIND_ROOT_PATH /opt/qt-5.14.2-aarch64) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)使用方式mkdir build cd build cmake .. -DCMAKE_TOOLCHAIN_FILE../toolchain-aarch64.cmake make这里 CMAKE_FIND_ROOT_PATH 指向 Qt 安装目录作用是让 CMake 在查找依赖时优先在这个目录下找头文件和库不会误抓 x86 主机上的 Qt。CMAKE_FIND_ROOT_PATH_MODE_* 分别控制程序、库、头文件的搜索策略其中 PROGRAM 设成 NEVER 是为了让 CMake 仍然能用主机上的可执行工具比如给 Qt 项目用的 moc、uic。6.2 自动构建脚本写一个脚本把下载、解压、configure、make、install 一整套动作全部封装起来。下面是简易骨架按需扩展#!/bin/bash set -e QT_VERSION5.14.2 PREFIX/opt/qt-${QT_VERSION}-aarch64 SYSROOT/usr/aarch64-linux-gnu if [ ! -d qt-everywhere-src-${QT_VERSION} ]; then echo source not found, please download and extract first exit 1 fi cd qt-everywhere-src-${QT_VERSION} ./configure \ -static -release -opensource -confirm-license \ -xplatform linux-aarch64-gnu-g \ -prefix ${PREFIX} \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-harfbuzz \ -no-opengl -no-dbus -no-icu -no-cups -no-glib -skip qtwebengine \ -nomake examples -nomake tests make -j$(nproc) make install脚本里把安装前缀和 sysroot 提取成变量后面升级工具链或者换板卡 SDK 时只需要改动一个变量不用再去反复敲一大串参数。团队内部建议把这类脚本放进公司内网共享配上一份 README 说明目标板型号和依赖情况能省掉大量重复沟通成本。6.3 团队协作与版本管理交叉编译环境最忌讳多个同事各自在本地安装不同版本的 Qt万一某个人的环境中多了一个库编译出来交到现场的东西可能就跟别人的行为不一致。比较好的做法是让编译服务器充当唯一的构建环境Qt 静态库安装在固定路径所有应用代码通过 CMake 工具链文件或 qmake 的 mkspec 引用它。最终产出物除了可执行文件还要把依赖的系统库清单和运行环境说明打包进发布文档这样测试和现场的同学也能快速判断问题是不是出在环境差异上。我自己从头搭完这一整套之后最大的感受是Qt 静态交叉编译真正的难点不在 make而在 configure 参数和依赖关系的理清。千万不要指望一套参数能通吃所有目标板先摸清楚目标板的图形栈、glibc 版本和系统库再决定用 linuxfb、eglfs 还是 xcb后面就会顺很多。另外第一次编译时建议全程保留日志出了问题先到 config.log 里搜 error再回来对照参数调比靠记忆盲猜快得多。如果条件允许把编译机换成 NVMe 固态和 16GB 以上内存也能省下不少等编译的时间。