干过嵌入式Linux开发的同行,多半都碰到过这个场景:交付给客户的ARM工控板上,系统裁剪得很干净,没有包管理器,甚至没有网络,却得跑一个带界面的Qt程序。这时候拿着动态链接的Qt往板子上一扔,缺库缺到怀疑人生,直接让人想转行。我最近正好完整走了一遍Qt 5.14.2在aarch64架构下的静态交叉编译流程,从工具链准备到configure参数一个个抠,再到最后把程序丢到目标板上跑起来,踩了不少坑,也把整个路径彻底捋顺了。这篇东西跟网上那些“三步搞定”的速食教程完全不是一回事,我尽量把每一步为什么这么干、遇到报错怎么解都写清楚,适合正在给ARM64设备做界面程序、或者手里有勘智、瑞芯微、飞腾这类国产化板子又不想被动态库折腾的同行参考。
先说结论:静态交叉编译不同等于把源码拿来直接configure一把过,真正的功夫全花在依赖裁剪、工具链配置、插件装载这三个地方。把这三件事搞清楚,Qt静态编译没有那么玄乎。
1. 为什么偏偏选Qt 5.14.2,还要坚持做静态交叉编译
1.1 版本选择:5.14.2到底香在哪里
现在聊Qt版本,很多人第一反应是Qt 6,但嵌入式领域完全不是这么回事。我把选型逻辑摆出来,你们就懂为什么到今天还有大批项目咬死Qt 5.14.2不放。
Qt 5.14.2是Qt 5系列中后期的LTS版本,发布时间是2020年初,正处在Qt 5体系最成熟、同时又足够新的位置上。相比Qt 5.12,它对C++17支持更好,QML相关模块的稳定性明显提升;相比Qt 5.15,它最大的优势是源码包不需要在线账号就能从官方镜像直接下载,省去一堆注册、登录的麻烦。而在国产化开发场景里,源码拿不到或者下载过程繁琐,往往是劝退很多工程师的第一道坎。
更重要的是,Qt 5.14.2对aarch64架构的支持已经非常成熟,linuxfb、eglfs这些嵌入式平台插件都在这个版本里处于稳定状态。Qt 6.x虽然架构更先进,但很多老的qmake工程迁移成本极高,第三方模块的兼容性也没跟上,在资源受限的嵌入式板子上做静态裁剪,5.14.2的保守和成熟反而是优点。
1.2 静态链接 vs 动态链接:不是拍脑袋决定的
静态交叉编译,从名字就能看出来,关键是把运行依赖直接编进二进制里,目标系统上不需要额外安装Qt运行库。为什么坚持这么干,最核心的还是嵌入式交付场景的痛点。
动态链接的优势是体积小、省内存,几个程序可以共享一份Qt库。但这个优势的前提是目标系统上的运行环境你能控制得住。实际项目里,板子上的系统要么是裁剪过的Buildroot,要么是一个很老的rootfs,或者干脆是客户那边我们把控不了的系统。动态链接意味着Qt的依赖链——比如libQt5Core.so依赖libicu,libQt5Gui.so依赖libpng、libjpeg、freetype——每一环都得跟着部署,少一个就启动失败。而且动态运行库对glibc版本敏感,编译机上好好的,拷到目标板上就报版本不兼容,这种事情太常见了。
静态编译付出的代价也明显:可执行文件动不动几十上百兆,内存占用和启动速度也会受影响。但在大部分工控、医疗、电力设备上,存储空间以GB计,内存动辄512MB起步,这点体积根本不叫事。换来的是部署时一个二进制文件加一个字体目录就能跑,再也不用被动态库依赖链折磨,思路一下就清爽了。这就是为什么嵌入式交付场景里,Qt静态编译是业内默认的高级玩法。
2. 准备工作:目标架构分析、工具链选型与sysroot搭建
2.1 先搞清楚目标板子,别上来就无脑编译
很多教程一上来就让你装工具链、下源码、跑configure,这是本末倒置。交叉编译的第一步,一定是先弄清楚目标板子的底细,因为这直接决定你后面所有配置参数。
连上目标板或拿到rootfs后,先用这几条命令摸底:
uname -a cat /proc/cpuinfo ldd --version | head -n 1 ls /lib/aarch64-linux-gnu/ | head这一步的核心信息有三个:确认CPU是64位ARM架构、确认内核位数、确认glibc版本。glibc版本尤其关键,后面会反复提到,因为静态编译的Qt程序最终仍可能动态链接glibc,如果你用的工具链自带glibc版本比目标板上的新,程序跑到目标板上分分钟报版本不兼容。
另外还要确认目标板有没有图形加速、有没有X11或Wayland。大多数嵌入式Qt场景根本不跑完整桌面,直接通过fbdev或者DRM/KMS访问显示,这就决定了Qt的平台插件选linuxfb还是eglfs。如果没有GPU,configure的时候老老实实加-no-opengl,别想着硬上。
2.2 交叉编译工具链:Linaro还是厂商定制
工具链是静态交叉编译的地基,选错后面全是坑。我试过几类方案,把经验直接分享出来。
首选方案是自己用crosstool-ng定制工具链。这个方案最灵活,可以精确指定gcc版本、glibc版本和编译优化参数,但缺点是费时费力,光编译一个工具链就得几小时。如果是长期做一个系列的arm64项目,定制工具链值得投资。
次选方案是直接用系统的Linaro工具链,Ubuntu上装aarch64-linux-gnu-gcc就行,一条apt install gcc-aarch64-linux-gnu搞定。这个方案的优势是方便,缺点是glibc版本可能和目标板不一致,特别是目标板如果还在跑老系统,基本必踩glibc不兼容的坑。这种情况我建议去Linaro官网下载带配套sysroot的GCC工具链压缩包,版本控制更精确。
如果是瑞芯微、飞腾、全志这类原厂板子,很多厂商会提供自家定制工具链,这种和板子上的系统的匹配度最高,优先用原厂方案。判断工具链好不好的标准很简单:用工具链编译一个hello world,丢到板子上能不能跑。跑不起来,说明工具链和目标系统根本不匹配,别抱侥幸心理继续往后走。
2.3 sysroot的本质:把目标板文件系统搬到编译机上
sysroot这个概念对静态编译新手来说经常是拦路虎。通俗地说,交叉编译时必须让编译器知道你链接的库头文件和库文件在哪,sysroot就是指定这个搜索根的。编译器在sysroot目录下找usr/include和usr/lib,就像本机编译时找系统的/usr一样。
Linaro工具链自带了一个基础sysroot,里面包含基本的glibc、libstdc++等。但做Qt静态编译,我们通常需要把目标板上的库也扩展进去,特别是后面要链接的第三方依赖。更直接的做法是直接从目标板上同步一份完整的lib和usr目录过来:
mkdir -p /opt/arm64-sysroot rsync -avz root@target-board:/lib/ /opt/arm64-sysroot/lib/ rsync -avz root@target-board:/usr/lib/ /opt/arm64-sysroot/usr/lib/ rsync -avz root@target-board:/usr/include/ /opt/arm64-sysroot/usr/include/这个目录在整个编译过程中承担两个作用:一是作为交叉编译器的搜索根,二是存放你额外编译出来的静态库文件。我习惯把自家编译的所有libxxx.a统一放到/opt/arm64-sysroot/usr/local/lib下,既不会被系统文件污染,又方便统一管理。
有一点必须提醒:sysroot里的动态库版本优先级很敏感,静态编译时如果系统里有同名.so和.a,链接器默认优先用.so,这样会打破全静态的计划。所以后面处理依赖库时,我会刻意告诉链接器优先找.a,这个细节在后面pro配置章节详细说。
3. Qt源码下载、configure配置与静态编译实操
3.1 源码准备:选对包,省掉一半烦恼
Qt源码包长什么样也是有讲究的。从官方下载页面可以拿到qt-everywhere-src-5.14.2.tar.xz,这个包体积大约500多MB,解压之后超过2GB,包含了Qt所有模块源码。如果你只想编译必要的模块,也可以只下载qtbase、qtdeclarative等分模块包,但实际操作中我建议直接用everywhere包,省得后面因为缺少某个模块函数引用浪费大量时间。
下载完成后解压到工作目录:
tar -xf qt-everywhere-src-5.14.2.tar.xz mkdir -p /opt/qt-build && cd /opt/qt-build单独建一个编译目录非常重要。Qt支持shadow build,也就是源码目录不动,编译产物全部放在编译目录里,这样万一配置错了可以直接删掉编译目录重新来,不用把整个源码再解压一遍。这条经验在反复调configure参数的时候能帮你节省大量时间。
3.2 configure关键参数逐项拆解:每一条都别乱选
整个Qt静态交叉编译流程里,configure参数配置是最核心的一步,网上很多教程直接贴一大段命令让你照着敲,但根本不解释每个参数是干嘛的,出了问题完全没法调。我说一下我最终敲定的配置,以及每个参数背后的逻辑。
先看完整命令:
../qt-everywhere-src-5.14.2/configure \ -opensource -confirm-license \ -static \ -prefix /opt/Qt5.14.2-aarch64 \ -xplatform linux-aarch64-gnu-g++ \ -release \ -nomake examples -nomake tests \ -skip qtwebengine \ -skip qt3d \ -skip qtcanvas3d \ -skip qtpurchasing \ -no-opengl \ -linuxfb \ -qpa linuxfb \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre -qt-harfbuzz \ -no-feature-cups \ -no-feature-printdialog \ -sql-sqlite \ -openssl-linked \ -I/opt/arm64-sysroot/usr/local/include \ -L/opt/arm64-sysroot/usr/local/lib逐项说明:
-static是整个编译的核心开关,告诉Qt构建系统不生成.so动态库,而是全部生成.a静态库。这一步不设的话,后面所有的努力都白费。
-xplatform linux-aarch64-gnu-g++是交叉编译的另一个核心开关。Qt的构建系统区分了-platform(编译Qt时用的平台,也就是x86 Linux)和-xplatform(目标平台,也就是aarch64 Linux)。这里填写的linux-aarch64-gnu-g++对应Qt源码目录qtbase/mkspecs/下的一个子目录名,如果该目录不存在,说明你的工具链命名方式不匹配,需要去mkspecs里找合适的目录或者手动新建一个。
-prefix指定安装路径,没什么好说的,只是要注意后面程序运行时和qmake生成的Makefile里都会记录这个路径信息,所以定下来就不要轻易改,否则需要整个重新编译。
-release去掉调试符号,静态编译的二进制本来就大,不设release的话体积直接失控。
-nomake examples -nomake tests:跳过示例和测试模块,这些模块在交叉编译场景下意义不大,而且要编很久,直接砍掉。
-skip系列是裁剪模块的关键。qtwebengine(Chromium内核)、qt3d、qtcanvas3d这些体积巨大、依赖复杂的模块,静态编译时基本用不上。特别是qtwebengine,交叉编译Chromium的代价在业界是出了名的恐怖,除非你必须用QWebEngine,否则无论如何都建议skip掉。qtpurchasing是应用内购买模块,在嵌入式离线场景下毫无用处。
-no-opengl:如果目标板没有GPU,这里直接关掉。开OpenGL不只是性能问题,还会引入libGL.so等一堆系统依赖,与全静态的初衷背道而驰。如果你的板子有GPU而且必须走GPU渲染,这一段配置会复杂得多,得换成-opengl es2,同时还需要EGL库支持,建议单独开一篇专题才讲得完。
-linuxfb是启用Linux framebuffer平台插件,这是嵌入式Qt在没有X11/Wayland环境下的主要显示后端。它直接操作/dev/fb0做显示输出,对硬件要求最低。-qpa linuxfb则是把linuxfb设为默认的平台插件。
-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype -qt-pcre -qt-harfbuzz这一串是静态编译的精髓:强制Qt使用源码包自带的第三方库,而不是链接系统的动态版本。如果这里不加这些参数,configure会去系统里找对应依赖库,找到的往往是动态库,导致静态编译产出的文件仍然依赖外部.so,部署时前功尽弃。用-qt-*系列把这些依赖变成Qt内部静态实现,应用部署时就只需要一个二进制文件。
-no-feature-cups -no-feature-printdialog:CUPS打印支持和打印对话框在大多数嵌入式设备上用不到,在configure中裁剪掉能减少不少编译时间。Qt的特性裁剪系统是一大利器,-no-feature-*比-skip粒度更细,可以裁到某个具体功能模块。如果对最终体积非常敏感,可以研究一下qtbase的feature清单,把用不到的全裁掉。但配置特性裁剪要特别注意,裁过头会导致编译哪些模块因为依赖缺失而报错,建议新手先用保守裁剪,稳了再往死里瘦身。
-sql-sqlite:启用SQLite数据库支持。嵌入式设备上经常要本地存数据,SQLite是标配。
-openssl-linked:如果程序需要走HTTPS,OpenSSL是绕不开的。这个参数表示静态链接OpenSSL库,前提是之前交叉编译好了OpenSSL静态库,并且用-I和-L把路径指定给了QMake。如果程序不涉及加密网络通信,建议先不加这个参数,等编译跑通后再考虑,减少出错点。
configure结束之后,最后几行会输出一个汇总表,列出Qt检测到的支持模块和环境信息。这一步一定要耐心检查。特别是它会明确告诉你是否启用了静态编译、是否识别出aarch64交叉编译环境、依赖库是qt还是system。万一有个关键的库检测为system,趁现在赶紧回头改参数重跑configure,比编译到一半再发现要省事得多。
3.3 编译与安装:等待时间,正好做这些事
configure通过后,就进入编译环节:
make -j$(nproc)这一步会持续相当一段时间,取决于机器性能,一般在40分钟到两小时之间。Qt 5.14.2的qmake构建系统在交叉编译时并不是每次都完美支持多进程并行,如果编译过程中出现随机报错,先不要怀疑代码问题,换成make -j2甚至单线程跑一下试试。并行编译在某些源文件之间竞争同一份生成文件时可能触发诡异的问题,降级线程数往往能秒解。
编译完成后安装到指定前缀:
make install安装成功后,检查一下关键产物:
file /opt/Qt5.14.2-aarch64/lib/libQt5Core.a /opt/Qt5.14.2-aarch64/bin/qmake -v如果此时看到libQt5Core.a的架构是ARM aarch64,说明核心编译已经成功。一个很容易被忽略的细节是,make install后qtbase的mkspecs目录也会被复制到安装目录下,后续写pro文件时qmake会从安装目录读取交叉编译的规格设置,所以整个Qt安装目录在后续使用中不要随意挪动。
4. 依赖库处理与pro工程配置:把“静态”贯彻到底
4.1 系统依赖库:能内嵌就内嵌,非内嵌就单独编静态
前面configure里用了-qt-*系列参数,大部分图片、字体、压缩库都已经变成Qt内部代码了。但仍有一些依赖是Qt没有预置的,最常见的就是openssl。
静态编译openssl的流程比较典型,可以作为处理其他依赖库的样板。下载openssl源码,交叉编译配置:
../openssl-1.1.1k/Configure \ linux-aarch64 \ no-shared \ --prefix=/opt/arm64-sysroot/usr/local \ --cross-compile-prefix=aarch64-linux-gnu-no-shared和Qt configure里的-static是同一个思路,只生成.a静态库。编译完成后,libcrypto.a、libssl.a会被安装到sysroot的usr/local/lib目录里。这样,Qt configure里的-I和-L参数就能找到它们了。
这里有一个非常关键的经验:如果最终程序还需要链接第三方静态库,而这个库又引用了第四方的符号,那么所有的.a文件之间的链接顺序不是自由的。以openssl为例,libssl.a依赖libcrypto.a,在最终链接应用时,-lssl必须写在-lcrypto前面。GNU链接器是逐个扫描静态库解析符号的,顺序反了,链接器在扫libssl.a时找不到libcrypto.a中的符号,直接报undefined reference。这种问题在动态链接时代完全不存在,但在静态编译领域是头号坑,后面pro配置时会专门展示格式。
如果目标板系统的glibc比较老,比如2.28,而编译机的glibc是2.35,仅靠工具链会导出过新的符号,静态Qt程序拷到板子上一跑就报version GLIBC_2.34 not found。解决思路是改用低版本glibc的工具链。这时候Linaro老版本GCC工具链或者crosstool-ng自制的工具链价值就体现出来了。也可以用musl libc替代glibc做纯静态编译,Qt官方没有正式支持musl,但社区有大量成功案例。要注意的是,即使Qt本身静态了,很多静态库内部仍可能通过dlopen在运行时加载动态库,这在最终程序里很难排查,建议在目标板上用readelf -d检查NEEDED字段,凡是标记了libc.so.6之外的东西都要仔细审查。
4.2 让qmake记住交叉编译环境
Qt安装完之后,我们通常不直接敲gcc编译程序,而是通过qmake生成Makefile。此时必须确保qmake生成的是交叉编译规则。Qt安装目录下的bin/qmake会自动读取同一目录下../mkspecs里的平台配置,所以我们直接用安装目录下的qmake就行,它会默认用configure时指定的aarch64交叉编译规则。
如果项目里需要额外的头文件和库文件搜索路径,不用每次都改全局环境变量,直接在pro文件里配置即可。
4.3 应用项目的pro文件:静态链接里的几个关键点
一个最简单的静态Qt Widgets应用,pro文件相对标准写法其实只需要少数几行补充:
QT += core gui widgets TARGET = app_name TEMPLATE = app CONFIG += static CONFIG -= dynamic SOURCES += main.cpp mainwindow.cpp # 如果用到OpenSSL等第三方静态库,手动指定链接顺序 LIBS += -lssl -lcryptoCONFIG += static的作用是告诉qmake生成链接静态库的Makefile规则,不加的话即使Qt库本身是静态的,qmake也可能按动态链接的方式来处理。
真正的难点在插件装载。Qt的静态程序启动时,平台插件不会像动态编译那样自动从plugins/platforms/目录加载,它必须在编译阶段就把插件代码链接进二进制,并在初始化时手动导入。以linuxfb平台为例,需要在main函数里加上:
#include <QtPlugin> Q_IMPORT_PLUGIN(qlinuxfb) int main(int argc, char *argv[]) { QApplication app(argc, argv); // ... }如果忘了写这一步,程序在目标板上会提示could not find a Qt platform plugin然后就退出了。这一步可以算静态Qt部署最容易踩的坑,很多新手对着这个报错半天找不到原因。除了平台插件,如果还用了QPA相关的图像格式插件、字体插件,也需要用同样的方式导入。我在pro文件里写这段的常用做法是把Q_IMPORT_PLUGIN单独整理到一个插件装载头文件里,后面维护起来方便得多。
pro文件里再加一个注意点就是,静态链接时如果出现一堆undefined reference错误,并且报错集中在Qt库内部,多半是库的链接顺序问题。Qt官方对静态编译的字符串库处理有一整套规则,通常按Qt5Core、Qt5Gui、Qt5Widgets的顺序排,但实际上qmake生成Makefile时会处理大部分排序,真正需要我们手动干预的是第三方库。原则就是从底层依赖库开始往回排,比如libssl依赖libcrypto,那么-lssl放前-lcrypto放后。
4.4 裁剪二进制体积:strip是个老朋友
编译完成后,二进制体积通常很大,一个有Qt Widgets界面的程序不加处理轻松超过30MB,如果QML的东西多,更大。这时候用交叉工具链里的strip工具去掉符号表:
aarch64-linux-gnu-strip app_namestrip之后通常能砍掉三分之一的体积。注意别strip过头,有时候需要留着符号表排查问题,我习惯在正式发布前才strip,调试阶段保持符号完整。
如果想进一步减小体积,可以在configure阶段做更激进的特性裁剪。比如不用的模块加上-skip,不用的特性加-no-feature-*。另外,-optimize-size这个参数能指示编译器优先优化体积而非速度,对存储空间紧张的项目值得考虑。总之路子很多,但前提是先把整个链路跑通,优化放到第二步。
5. 部署到目标板与运行验证:最后一公里往往最考验人
5.1 静态程序部署,到底要带哪些东西
在目标板上运行静态Qt程序,人们常说要“一个文件跑天下”,严格说来不够准确。我在实际部署过程中把所需文件分为三类,缺一不可。
第一类是程序本体,也就是静态链接出来的可执行文件。这个文件已经包含Qt库和第三方静态库,一般不再需要额外拷Qt相关的东西。
第二类是系统运行库。尽管Qt库静态了,但glibc、libstdc++等底层库通常还是动态链接的。如果目标板的rootfs本来就有这些库,什么都不用拷;但如果是从零搭建的极简rootfs,至少要确保/lib/aarch64-linux-gnu/下存在libc.so.6、libm.so.6、libstdc++.so.6、libgcc_s.so.1。这一点可以用readelf -d验证:
aarch64-linux-gnu-readelf -d app_name | grep NEEDED如果输出里有libc.so.6等系统库,属于正常范围。如果输出里出现了libQt5Core.so或者libpng之类的Qt或第三方库,说明你的静态编译并不彻底,回去检查configure参数是不是漏了-static或-qt-*相关选项。
第三类是运行资源,最容易被忽略的是字体文件。Qt在linuxfb下运行,没有X11的fontconfig机制,默认找字体的路径通常可以参考QT_QPA_FONTDIR环境变量指定的目录,默认路径包括/usr/share/fonts和/usr/lib/fonts。如果目标板上没有中文字体,程序一启动,界面上全是豆腐块方块。解决办法很朴素,从任意一台Linux机器上拷贝一个中文字体文件(文泉驿或者Noto Sans CJK都行)放到目标板的/usr/share/fonts/目录下,或者干脆放在程序同一目录下,然后用环境变量指定:
export QT_QPA_FONTDIR=/app/fonts还有时区数据。如果程序里用了QDateTime做时间转换,目标板的/usr/share/zoneinfo目录不能为空,否则时间会显示成UTC或者直接报错。这也是新装机最容易忽略的一个隐形坑。
5.2 常见问题排查速查表
把我在整个过程中遇到的高频问题整理成一张表,方便大家直接对应排查:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 启动后提示could not find a Qt platform plugin | 静态编译未导入平台插件 | 在main函数加Q_IMPORT_PLUGIN(qlinuxfb),重新编译 |
| 报error while loading shared libraries: libssl.so.1.1 | openssl被当成动态库链接 | 重新用no-shared编译openssl,configure时用-openssl-linked |
| 报version GLIBC_2.34 not found | 工具链glibc版本高于目标板 | 换用低版本glibc工具链,或改用musl工具链 |
| 运行到一半崩溃,gdb显示动态加载? | 静态库内部用了dlopen动态加载 | 尽量把依赖静态库的dlopen关闭,或用"full static + musl"方案 |
| 显示框全是方框 | 缺少中文字体 | 拷贝字体文件并设置QT_QPA_FONTDIR环境变量 |
| 触摸点击位置偏移 | 触摸屏校准参数缺失 | 使用-plugin evdevtouch并检查tslib校准文件 |
| 程序体积巨大 | 静态链接且未优化 | 用strip、特性裁剪、-optimize-size重新编译 |
| 链接时报重复定义错误 | 同一静态库被多次链接 | 检查pro文件及LIBS顺序,去重后重连 |
这张表总结了绝大多数卡脖子的位置,值得收藏备用。
5.3 三个印象深刻的“非典型”问题
除了表格里的常规坑,我还碰到过三个比较独特的疑难杂症,这里展开说说排查过程,帮助大家建立自己的debug思路。
第一个是链接器报skipping incompatible /opt/Qt5.14.2-aarch64/lib/libQt5Widgets.a when searching for -lQt5Widgets。这个报错表面上看着像库文件架构不兼容,其实本质是编译器找错了库路径。原因是我在pro文件里手动指定了一个编译机自带的Qt库路径,抢在了交叉编译Qt库路径前面。排查方式就是用aarch64-linux-gnu-ar t检查库里的object文件架构,确认库本身没问题后再去查路径优先级,结果发现是LIBRARY_PATH环境变量残留导致的。解决方法是把pro文件里的LIBS路径规范化,保证-L/opt/Qt5.14.2-aarch64/lib在第一位。
第二个是configure时qtbase模块自动检测到xcb,然后动态链接了一堆X11库。虽然configure参数里加了-no-feature-xcb,但Qt底层还是会自动检测系统里是否装了xcb相关头文件,一旦检测到就尝试链接。这在静态编译时是个大坑,因为最终目标板上根本没有X11环境。排查方案是configure之后检查config.summary文件里xcb的状态,如果显示enabled或auto,直接检查环境变量PKG_CONFIG_PATH是不是指向了编译机上的包配置路径,把这些路径清空后再重新configure。
第三个是程序在目标板上启动后黑屏,但进程活着。排查了半天发现linuxfb后端打开/dev/fb0失败,缺少访问权限或者帧缓冲设备节点根本没建立。这个严格来说不是Qt问题,而是目标板系统初始化脚本没把/dev/fb0节点创建好。我的经验是部署时写一个启动脚本,启动前先检查设备节点:
[ -c /dev/fb0 ] || mknod /dev/fb0 c 29 0 chmod 666 /dev/fb0这段算是嵌入式Linux非常经典的初始化问题,很多从纯应用开发转过来的人容易忽略。
6. 我再啰嗦几句经验
整个Qt 5.14.2 aarch64静态交叉编译流程走下来,最大的感悟是:静态编译不是配置几个参数那么简单,它是一个从工具链到configure再到部署的全局工程。工具链决定了底层ABI兼容性,configure决定了依赖关系,部署决定了运行环境,三者一环扣一环,哪一环出了问题,整个链路都得回去排查。如果你正卡在某一环,希望这篇手册能帮你少走弯路。
最后分享一个我习惯的核验方法:完成静态编译后,把程序拷贝到目标板上之前,先在编译机上用交叉工具链的file、readelf查一遍文件架构和动态依赖,然后在目标板上执行前用qemu-aarch64在编译机上跑一次功能测试。虽然qemu模拟性能和真机差得多,但能提前把启动崩溃和动态库问题暴露在开发阶段,比板子上一遍遍刷系统高效太多。这个方法在我后来几个项目的交付阶段帮了大忙,也顺手推荐给你们。