十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Qt 5.14.2 aarch64 静态交叉编译从零到部署实战

Qt 5.14.2 aarch64 静态交叉编译从零到部署实战 这两年国产化替代的节奏大家都有体会身边不少嵌入式项目从 x86 迁移到 ARM 架构。我手上这个数据采集终端就是典型例子目标平台是 aarch64 架构的飞腾处理器系统用的银河麒麟 V10。刚开始项目赶进度图省事直接在板子上装 Qt 开发环境开发调试确实快可到了交付阶段才发现动态链接版本的 Qt 应用在客户现场各种“水土不服”——有的机器缺 libQt5Core.so.5有的版本被系统自带的 Qt 串了台还有的根本没法在纯内网环境里临时补依赖。被坑了几次之后我下决心把整个工具链切到 Qt 5.14.2 aarch64 静态交叉编译。折腾了大概两周时间过程中踩了不少坑也把周围同事经常问的问题整理了一遍。今天这篇就把从零开始的完整流程、参数选型逻辑、报错排查经验一次说清楚。文章覆盖了为什么选静态交叉编译、宿主机环境与 sysroot 准备、Qt 源码 configure 参数逐条拆解、应用工程的静态链接与目标板部署最后还整理了高频报错的速查表。适合正在做嵌入式 Linux、国产化平台适配、或者被动态库依赖搞到崩溃的 Qt 开发者参考。1. 为什么要折腾静态交叉编译——先搞清楚需求和选型1.1 什么场景下需要静态交叉编译先说结论静态交叉编译不是所有 Qt 项目的“最优解”但它能解决动态链接方案里最头疼的那几个问题。如果你遇到下面任一场景就可以认真考虑切到静态编译。第一种目标环境是纯内网或者没有包管理权限。很多工控项目、军工项目、电力项目现场根本没有外网客户也不允许动系统的 /usr/lib。动态链接方案一旦缺了某个 .so你只能手动拷贝依赖拷贝过程中还要盯着符号链接漏一个、错一个都够你喝一壶。静态编译把这个麻烦直接抹掉了程序跑起来只依赖 glibc 这几个基础系统库。第二种目标板的 rootfs 很小内存和磁盘都紧张。Qt 动态库全家桶动不动几百 MB还得带一堆插件目录、翻译目录、字体目录。静态编译的代价是单个可执行文件会膨胀到十几到几十 MB但整体占用可控得多拷过去一个文件就能跑对部署脚本和运维人员都非常友好。第三种也是我最看重的就是“程序必须自包含”。动态链接方案里Qt 库版本、依赖库版本、甚至环境变量 QT_QPA_PLATFORM 没设对都能导致程序跑不起来。静态编译后运行时只认自己编进去的那份 Qt不会再跟系统里其他版本打架。交付现场出问题的概率凭空就降了一大截。1.2 为什么选 Qt 5.14.2 和 aarch64aarch64 这个选型不用多解释现在嵌入式、工控、国产化平台基本上都是 ARMv8 64 位天下的飞腾、鲲鹏、瑞芯微这些主流芯片跑的操作系统核心就是 aarch64。托管环境里 uname -m 输出 aarch64基本就是你未来要部署的目标。Qt 版本我锁在 5.14.2主要三个理由。第一它是 Qt 5 时代非常成熟稳定的一个版本bug 修复比较完善网上资料也最齐全。第二它对老一些的交叉编译工具链兼容性极好我在 gcc 7.5、gcc 9.3 上都编过基本没什么幺蛾子。第三这个版本是开源社区里“静态编译 嵌入式”讨论最充分的一个版本你踩坑时大概率能搜到前人留下的解决方案。Qt 5.15 虽然是 LTS但开源版不再提供离线安装包源码包也带上了商业版限制Qt 6 对老嵌入式环境更不友好。对于追求稳定、不太需要新特性的嵌入式项目Qt 5.14.2 是性价比很高的选择。1.3 静态 vs 动态、交叉 vs 本机的组合怎么选把这两对维度拆开看组合逻辑其实很清楚。本机编译、动态链接开发体验最好编译快运行时报错直观但不适合交付目标机和开发机不可能完全一致。本机编译、静态链接部署简单但如果目标板性能弱在板子上编 Qt 能等到崩溃我试过一次一条 make 跑了两小时还没完。交叉编译、动态链接编译速度快可以较方便地定制 Qt 功能但部署时依然要处理依赖省不了多少事。交叉编译、静态链接流程最复杂但部署最干净、运行时最可控。我最终选的这条路代价是 configure 参数要抠得细、sysroot 要备得全换来的是交付现场真正意义上的“拷贝即运行”。2. 环境准备工具链、Sysroot、依赖库一个都不能少2.1 宿主机环境与目标平台确认先说我的宿主机环境x86_64 的 Ubuntu 20.04内存 32GB磁盘剩余 50GB 以上。Qt 全量静态编译包含 webengine 这种大块头除外大概要占 15~20GB 的临时空间磁盘不够的提前清理。动手之前一定要先拿到目标板的准确信息。我在板子上依次执行了这几条命令输出记下来uname -m # 输出aarch64 cat /etc/os-release # 输出银河麒麟 V10 相关版本信息 ldd --version # 输出 glibc 版本例如 2.28 或 2.31这里面 glibc 版本尤其关键。交叉编译出来的程序最终运行在目标板上glibc 是没法静态链接进去的它是系统底层的 C 运行库。如果宿主机工具链的 glibc 版本新于目标板的 glibc编出来的程序大概率会报GLIBC_2.xx not found。所以工具链的选择优先用目标板厂商配套提供的交叉编译工具链其次才考虑通用工具链。如果拿不到原厂工具链就尽量选 glibc 版本接近目标板的 gcc。另外我在宿主机上还装了qemu-user-static这个对后来调试非常重要。它能让你在 x86 的宿主机上直接运行 aarch64 的可执行文件快速验证程序能不能正常启动不用反复往板子上拷贝。2.2 交叉编译工具链的选择与安装工具链这块我踩过一个大坑值得单独说。最开始我用 apt 直接装的g-aarch64-linux-gnu版本是 gcc 9.4编出来的程序在板子上跑就报GLIBCXX_3.4.28 not found。排查了半天发现是工具链自带的 libstdc 版本比目标板系统高静态链接 Qt 时把高版本符号也带进程序里了。后面换成目标板厂商 SDK 里自带的交叉工具链版本是 gcc 7.5glibc 版本为 2.28与目标板完全一致问题立刻消失。所以我的建议是如果能从目标板厂商那里拿到交叉工具链优先用原厂版本匹配能帮你省下一大堆莫名奇妙的运行时报错。如果确实没有原厂工具链用 apt 装的话推荐这样搞sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu aarch64-linux-gnu-g --version注意 apt 自带的这套工具链缺不少头文件和链接库后面 sysroot 阶段需要手动补。也可以去 Linaro 官网下载gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz解压后直接把 bin 目录加到 PATH 里不污染系统的包管理。装好之后做个最基础的验证写一个 hello.c 编译试试aarch64-linux-gnu-gcc hello.c -o hello_aarch64 file hello_aarch64 # 输出ELF 64-bit LSB executable, ARM aarch64看到ARM aarch64字样工具链就算通了。接着用 qemu-aarch64 在宿主机上运行验证qemu-aarch64 ./hello_aarch64能打印出 hello说明工具链和模拟器都正常后面调试 Qt 程序就方便了。2.3 准备 sysroot 和必要的第三方依赖库交叉编译最麻烦的不是编译器本身而是 sysroot。通俗讲sysroot 就是目标板的“头文件和库文件快照”编译器需要用它来找到目标板上应该链接的 C 库、系统库和第三方库。两个来源一是厂商 SDK 自带 sysroot省事但可能版本旧二是自己从目标板拷贝。我是用拷贝方式做的步骤如下# 在目标板上执行打包系统库和头文件 tar --exclude/proc --exclude/sys --exclude/dev \ -czf sysroot.tar.gz /lib /usr/include /usr/lib拷到宿主机后解压整理成一个目录比如/opt/sysroot-aarch64。这里有几个关键点都是我实际踩过坑后总结的。拷贝时一定要保留符号链接So 库之间的 .so - .so.x 链接断掉后续链接阶段会报cannot find -lxxx。用 tar 打包再解压天然能保留符号链接比直接cp -r安全得多。解压之后建议清掉一些用不到的目录比如/usr/lib/debug、/usr/share/doc、/usr/lib/gcc里的旧版本残留能省不少空间。Qt 静态编译如果依赖了 OpenSSL、ICU、zlib 这些第三方库优先使用目标板 sysroot 里已有的版本。Qt configure 会尝试检测 sysroot 下是否有这些库有就直接用系统版本没有就只能用 Qt 自带源码编译或者手动交叉编译静态库放进去。我为了避免麻烦configure 时明确指定了-no-opencv之类的选项把不需要的依赖全部关掉OpenSSL 用 sysroot 里系统自带的版本。3. Qt 5.14.2 源码编译configure、make 与参数详解3.1 下载源码并整理目录Qt 5.14.2 的源码包叫qt-everywhere-src-5.14.2.tar.xz大概 500MB 左右。如果你在开发环境能访问外网直接用 wget 从 Qt 官方 archive 下载如果纯内网就在有网的机器上下好再拷贝到宿主机。下载地址就不贴了搜索引擎搜qt-everywhere-src-5.14.2第一个结果就是。下载完成后解压强调一句不要直接在源码根目录下执行 configure 和 make。Qt 官方其实支持源码内构建但会污染源码目录后续想切换配置、重新编译就会很痛苦。我一直用的“独立构建目录”方式干净、可重复也方便保留多个配置。mkdir -p ~/qt-5.14.2-build cd ~/qt-5.14.2-build ../qt-everywhere-src-5.14.2/configure [参数...]源码目录和构建目录放到一起注意路径里别有中文和空格Qt 的构建脚本对路径挺敏感的。磁盘至少留 30GB静态编译中间文件非常多别卡在No space left on device这种低端错误上。3.2 configure 关键参数逐一拆解configure 是 Qt 构建的灵魂参数选对了后面所有问题都会少一半。下面是我这次使用的核心参数逐一解释参数背后的逻辑。../qt-everywhere-src-5.14.2/configure \ -static \ -prefix /opt/Qt5.14.2-aarch64-static \ -xplatform linux-aarch64-gnu-g \ -opensource -confirm-license \ -nomake examples -nomake tests \ -no-opengl \ -linuxfb \ -no-icu \ -qt-zlib -qt-libpng -qt-libjpeg \ -no-feature-presentation \ -no-feature-printsupport \ -skip qtwebengine -skip qt3d逐个拆解-static是本次编译的核心告诉 Qt 把自身库编成静态库后续应用链接时全部静态合并进可执行文件。如果不加默认是-shared那和普通动态装没区别。-prefix /opt/Qt5.14.2-aarch64-static指定安装路径。注意这个参数只影响 make install 的拷贝目标编译过程本身不依赖这个路径。建议设一个带架构标识的目录方便以后 x86、aarch64 版本共存。-xplatform linux-aarch64-gnu-g是指定交叉编译 mkspec 的关键参数。Qt 源码里预置了qtbase/mkspecs/linux-aarch64-gnu-g它告诉构建系统“编译器前缀是 aarch64-linux-gnu-目标是 ARM 64 位”。本地编译用-platform linux-g交叉编译必须用-xplatform这个区别很关键别搞混。-opensource -confirm-license就是确认开源协议不写会卡在交互确认步骤。-nomake examples -nomake tests不编译示例和测试代码。静态编译本身就慢这几百个例子编进去纯属浪费时间。-no-opengl是因为我的目标板没有 GPU也没有 OpenGL 驱动。如果你目标板有 GPU 且需要 QML 渲染要保留 OpenGL纯 Widgets 应用可以大胆关掉。开着反而会去检测 sysroot 里是否包含 libGL 等库缺了就报错。-linuxfb启用 framebuffer 平台插件。嵌入式板子没有 X11/Wayland 显示服务时Qt 程序直接往/dev/fb0上画这是最常用的“裸奔”显示方案。如果你的板子跑在带桌面的系统上需要 XCB 插件那就要保留-xcb相关选项。我这次主要是 linuxfb 场景。-no-icu关闭 ICU。ICU 是处理 Unicode 的库功能强大但体积大、编译极慢。只做中文和常见字符显示的话Qt 自带的基础 Unicode 支持完全够用。省掉的编译时间非常可观。-qt-zlib -qt-libpng -qt-libjpeg使用 Qt 自带的 zlib、png、jpeg 库。这三个是基础图片编解码依赖与其去 sysroot 里找版本匹配的库不如直接用 Qt 内置源避免版本冲突。-no-feature-presentation -no-feature-printsupport是裁剪特性。Qt 5.14.2 支持很多用不到的功能模块比如 presentation 和打印支持。裁剪能缩小最终静态库体积也能减少编译时间。不过这一项需要你对项目需求心里有数不确定的就别瞎裁。-skip qtwebengine -skip qt3d跳过重型模块。WebEngine 内嵌 Chromium编译时间极长体积巨大嵌入式场景大多数用不到Qt3D 也同理。这两个不开configure 和 make 速度能快一个数量级。configure 结束后仔细看输出的 summary确认几个关键项Build type: static、Architecture: aarch64、Platform plugin: linuxfb。如果这些不一致回头改参数重新 configure。3.3 make、安装与编译耗时configure 成功后直接make -j8。并行编译数量可以参考宿主机的 CPU 核数我是 8 核 16 线程的机器-j8比较稳定开-j16偶尔会出现内存峰值过高。静态编译机器的内存建议至少 16GB低于这个数请调低并行度。编译时长这个事得说清楚全量 Qt 5.14.2没跳过重量级模块静态编译在我的机器上跑了近 50 分钟。用了上面的裁剪参数之后实际只花了 20 分钟左右。如果你的机器性能弱一些耐心等待即可中间不要中断也不建议 make 到一半改参数重来Qt 的增量构建有时不靠谱。编译完成后安装make install安装到/opt/Qt5.14.2-aarch64-static后验证一下成果/opt/Qt5.14.2-aarch64-static/bin/qmake -v # 输出应该类似QMake version 3.1Using Qt version 5.14.2 file /opt/Qt5.14.2-aarch64-static/bin/qmake # 输出ELF 64-bit LSB executable, ARM aarch64看到ARM aarch64说明这套 Qt 确实是给目标架构编的。此时整个 Qt 静态库已经躺在/opt/Qt5.14.2-aarch64-static/lib/下后缀基本都是.a。4. 应用工程静态链接与目标板部署的硬核细节4.1 用交叉 qmake 编译应用Qt 安装成功只是第一步真正写代码、编工程、跑通部署才是完整的流程。先说怎么编译一个普通 Qt Widgets 工程。假设你的工程目录结构是myapp/ ├── myapp.pro ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h直接用新安装的交叉 qmake 生成 Makefile然后 makeexport PATH/opt/Qt5.14.2-aarch64-static/bin:$PATH cd myapp qmake myapp.pro make -j8qmake 会在 Makefile 里自动带上交叉编译工具链和 Qt 静态库路径不需要额外写死。编出来的可执行文件用 file 验证file myapp # 输出ELF 64-bit LSB executable, ARM aarch64, dynamically linked ...注意一个细节即使 Qt 是静态链接可执行文件仍然会动态链接 glibc 等基础系统库所以 file 输出里还有dynamically linked字样这是正常的。后续部署时只需要保证目标板的 glibc 版本兼容即可。如果你用的是 Qt Creator需要在工具链设置里新增一个 KitCompiler 选 aarch64-linux-gnu-gQt Version 选 /opt 下安装的 qmakeCMake 或 qmake 按自己习惯。配置好之后 Qt Creator 就能像本地开发一样远程部署、调试体验会好很多。4.2 静态链接的坑ICU、OpenSSL、插件系统静态链接远比动态链接“挑剔”这里重点说三个最让我头疼的坑。第一个是 ICU。如果 configure 时开了 ICU静态编译后你的可执行文件里会带着 ICU 的数据文件体积直接翻倍而且 ICU 版本要跟目标板系统的 libicu 完全对齐否则运行时各种诡异的字符处理问题。我这次的方案是-no-icu规避了这个问题。代价是某些依赖 ICU 的高级字符串处理接口不可用但对普通业务来说影响可以忽略。第二个是 OpenSSL。Qt 的 SSL 模块如果开启了 OpenSSL 支持静态链接时它会把 libcrypto 和 libssl 静态库都合并进程序。这对部署有利但要注意 OpenSSL 的版本必须与目标板系统兼容尤其是证书链路径。我这次目标板上的 OpenSSL 版本是 1.1.xsysroot 里也是 1.1.x才稳。如果版本不一致建议在 sysroot 里就地编译一份静态 OpenSSL 放进去再让 Qt 链接它。第三个是 Qt 插件系统这是静态链接里最隐蔽的坑。动态编译时平台插件libqlinuxfb.so是独立文件Qt 运行时按路径去加载静态编译时插件已经被编进可执行文件了但 Qt 的插件机制默认还是会尝试“动态加载”结果就是运行时报could not find the Qt platform plugin linuxfb。解决方式是在应用代码里显式导入插件#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)把这个放任意一个 .cpp 文件里重新编译插件就会通过静态链接机制被带进可执行文件。同理如果你需要 JPEG、PNG 图片插件也要这样导入。这个知识点常规 Qt 教程很少讲我当初也是查了很多资料才转过弯来。4.3 部署到目标板库文件、插件与平台插件处理静态编译之后部署逻辑就变得很简单了。把可执行文件拷贝到目标板任意目录chmod x然后设置几个环境变量就能跑export QT_QPA_PLATFORMlinuxfb export QT_QPA_FONTDIR/usr/share/fonts ./myapp如果你的程序是跑在有 framebuffer 的嵌入式环境QT_QPA_PLATFORMlinuxfb是必须的否则 Qt 找不到显示后端。QT_QPA_FONTDIR指定字体目录不设的话中文字符显示出来全是方块。如果目标板实际有桌面环境比如银河麒麟 V10 桌面版那就要用 XCB 后端。你需要在 configure 时保留 xcb 支持并静态链接相关 X11 库部署时再把 X11 相关的动态库一起拷过去。不过这样就绕回了动态部署的老路所以我个人建议跨平台交付的嵌入式项目能走 linuxfb 或者 eglfs 就走别贪 XCB 的花哨功能。还有一个细节静态编译后程序体积普遍不小一个 Widgets 程序 15MB 很正常。如果客户对体积有要求建议用strip命令瘦身aarch64-linux-gnu-strip myapp体积能压缩一半以上。千万不要忘记这个步骤我头一次发布时忘了 strip80MB 的二进制拷到客户机器上被吐槽了半天。5. 实战排查交叉编译高频报错与对策5.1 版本冲突cannot mix incompatible Qt library (version ex50601) with this library这个报错我见过太多次了尤其是把 Linux 桌面开发的 Qt 代码拿到交叉环境里重新编译时最容易出现。仅从报错文本看ex50601里的50601表示 Qt 版本 5.6.1而ex前缀是动态库版本格式。背后的机制是程序链接时编译器和链接器发现某个目标文件是用 Qt 5.6.1 的头文件、链接路径编译出来的但当前链接的是 Qt 5.14.2 的库版本不同导致 ABI 不兼容链接器直接拒绝。解决办法也很直接。第一清理旧的构建产物包括 Makefile、.o、.moc 文件彻底重新构建。QMake 生成的 Makefile 里会缓存很多路径旧工程的缓存不删干净即使换了 Qt 版本Makefile 还是会指向旧路径。第二检查 .pro 文件里有没有硬编码的INCLUDEPATH或LIBS指向了系统 Qt 路径把这些路径改成新的交叉编译 Qt 路径。第三确认系统环境变量LD_LIBRARY_PATH没有被 x86 版本的 Qt 动态库污染有时候这个变量没清干净运行时程序会加载到两个不同版本的 Qt 库同样报错。5.2 平台插件找不到could not find the Qt platform plugin linuxfb这个报错在静态编译环境里几乎必现原因我在 4.2 节已经提过Qt 具备插件机制插件未显式导入。你需要在程序入口文件加上Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)然后重新编译。但有些时候加了这个宏还是报错我遇到过两种奇葩情况。第一种configure 时没加-linuxfb编译出来的 Qt 本身就没有 linuxfb 插件你代码里写Q_IMPORT_PLUGIN也会编译报错。回到 configure 参数那一步把-linuxfb加上重新编 Qt。第二种目标板上有多个 framebuffer 设备比如有 HDMI 和 LVDSQt 默认选 fb0但 fb0 不一定有人插屏。这时候可以用环境变量指定export QT_QPA_FB_DEVICE/dev/fb1 ./myapp调试插件加载问题时加QT_DEBUG_PLUGINS1能看到 Qt 尝试加载哪些插件、失败原因是什么这个开关帮我省了很多排查时间。5.3 其他常见问题速查表报错信息或现象根因与对策qmake: command not found交叉 qmake 没加进 PATH或者/opt/Qt5.14.2-aarch64-static/bin不存在。检查安装路径并重新 export PATH。GLIBCXX_3.4.28 not found交叉工具链的 libstdc 版本比目标板的 glibc 新。换用版本匹配的工具链或者用目标板 SDK 自带工具链。cannot find -lGL目标板没有 OpenGL 库或 sysroot 里没有 OpenGL 头文件和库。如果应用不需要 OpenGLconfigure 加-no-opengl如果确实需要必须交叉编译 OpenGL 相关依赖进 sysroot。程序运行起来中文全是方块Qt 静态编译后字体路径不对。设置QT_QPA_FONTDIR指向板子上实际字体目录确认目录里有中文字体文泉驿、Noto Sans CJK 之类。Segmentation fault启动即崩最可能是 sysroot 与目标板不一致库的头文件和板子真实库版本有差异。重新生成 sysroot务必保留符号链接。也可以先在宿主机用 qemu 跑一下快速定位崩溃位置。可执行文件在 x86 机器上显示cannot execute binary file这是正常的你编的是 aarch64 程序在 x86 上必须用 qemu-aarch64 跑或者拷贝到目标板跑。make 时报No rule to make target ...常见于源码目录和构建目录混用之后源码目录里有上次生成的 Makefile 残留。建议 rm -rf 构建目录重新从源码目录 configure。这套流程我前后完整跑过三遍。第一次用 apt 的通用工具链在 sysroot 阶段折腾了两天最终因为 GLIBCXX 版本问题失败第二次换成了目标板厂商 SDK 里的工具链还是在插件导入上卡了半天第三次终于把 configure 参数、sysroot 生成、插件导入这些环节全部理顺一个下午时间就完成了从源码到目标板可运行程序的全部流程。最后再分享一个小经验把 configure 参数、环境变量、编译命令都写成一个 shell 脚本保存下来命名带上 Qt 版本和架构标识比如build_qt_5142_aarch64_static.sh。后续无论是换机器、换同事还是版本升级只需要改脚本里的版本号和路径就能在几小时内复现整套环境。我第一次就是吃了“靠记忆敲 configure 参数”的亏第二次重建环境时很多参数细节已经记不清了翻命令历史一条条捞非常痛苦。写在脚本里这条经验值得每一位做交叉编译的工程师提前收藏。
返回列表