
1. 为什么要折腾aarch64静态交叉编译1.1 静态链接的实际业务价值先说说我在什么场景下被逼到这条路上去的。去年接了一个边缘网关的项目主控是RK3568四核A55系统跑的是Buildroot裁剪出来的定制Linuxrootfs总共才256MB。甲方提了一个要求应用要能直接在板子上跑不依赖板子上的任何Qt运行环境而且最好是一个单文件。当时第一反应是动态交叉编译链到板子自带的Qt库上。结果一问才知道板子上的系统镜像已经被乙方锁死了连opkg都进不去更别说往上面推Qt的so了。这时候静态编译是唯一出路——把所有Qt模块和第三方依赖全部编进可执行文件拷上去就能跑。我之前一直觉得静态编译是老古董玩法桌面Linux早就动态链接了直到被现实毒打了一次。但真正动手之后才发现Qt生态里做静态交叉编译踩坑数量远超想象。本文把我从零搭环境到最终跑通的完整过程还原出来包括每一个configure参数、每一处qmake.conf的修改、每一次链接报错的排查思路。1.2 版本选型的理由为什么是5.14.2选Qt版本这事建议慎重。Qt官方从5.15开始就不再为开源用户提供二进制源码包里的某个分支的离线安装器了而5.15之后的LTS版本源码包需要商业授权才能下载5.12虽然也是LTS但年代偏老对C17的支持不够漂亮。5.14.2是最后一个让开源用户在普通下载渠道拿到完整源码并稳定运行的老版本之一社区里相关的交叉编译资料也最全。我实测下来5.14.2在aarch64上的gnu mkspec支持比较成熟配合gcc-linaro或系统自带的aarch64-linux-gnu-gcc都能顺利编过。如果你考虑用更新的6.x或者5.15.x当然也可以但很多插件特别是xcb相关的编译错误只能靠自己去fixed5.14.2至少能找到大量已知报错的对应解法。2. 搭建交叉编译环境工具链、Sysroot与依赖梳理2.1 工具链的选择与验证交叉编译的底座是工具链。我当时做了两个方案对比方案工具链来源优点缺点ALinaro gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu老牌稳定与Qt 5.14.2时代吻合版本较老不完全支持新ARM特性BUbuntu仓库 gcc-aarch64-linux-gnugcc 9/10/11升级方便ALSA等库容易配套不同Ubuntu版本的库路径差异大我最后选的是方案A原因只有一个字稳。Linaro的目录结构非常规整bin、lib、aarch64-linux-gnu等目录全部分离后面写sysroot路径时不容易混乱。安装方式也简单mkdir -p /opt/aarch64-toolchain tar xf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt/aarch64-toolchain export PATH/opt/aarch64-toolchain/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH验证工具链能否正常工作的命令很重要不要只看版本号aarch64-linux-gnu-gcc -v echo int main(){return 0;} test.c aarch64-linux-gnu-gcc -static test.c -o test_static file test_staticfile输出里应当有statically linked字样如果连静态链接都过不了后面Qt一定编不出来。2.2 Sysroot的两种搭建路线交叉编译最容易被忽略的就是sysroot。所谓sysroot就是目标设备上的头文件和库文件的一个影子目录让编译器跨架构编译时找得到标准库、依赖库和对应头文件。我见过不少人直接用工具链自带的aarch64-linux-gnu/libc当sysroot结果Qt编译到一半报gnu/stubs-32.h: No such file or directory或者bits/libc-header-start.h: No such file就是因为sysroot里缺了32位兼容头文件。两条路线从目标设备的rootfs直接拷如果设备跑的是Buildroot/Yocto生成目录里通常有完整的target/直接用rsync把target/usr/include、target/usr/lib、target/lib等目录拷出来当sysroot最准确。用工具链自带的sysrootLinaro目录下就有aarch64-linux-gnu/libc它包含一套最小rootfs对纯Qt静态编译基本够用。我个人的建议是优先用方案1省去后面无穷无尽的缺少xxx.h问题。如果实在没有目标rootfs方案2也能跑通但遇到libc库版本不匹配时别慌那是正常现象。2.3 依赖库清单静态编译的全家桶很多人交叉编译Qt只装了gcc和make跑configure时发现zlib、libpng全要现编而且Windows下习惯用预编译库的人在Linux下往往不知道这些依赖库是可以直接用源码编的。这块我建议直接一把梭把所有第三方库统一交叉编译到同一个前缀目录下$SDKROOT /opt/aarch64-sysroot需要准备的库如下zlib基础压缩库libpng图片格式freetype字体渲染harfbuzz文字整形复数脚本支持很关键libjpeg-turboJPEG支持opensslHTTPS、加密libxcb及其附属库如果目标设备有显示服务器需要X协议支持fontconfig字体管理icu国际化如果只需要中文可以关掉减少体积这些库的编译参数有一个共同原则--hostaarch64-linux-gnu --prefix$SDKROOT --enable-static然后make make install。注意不要只编静态库有些Qt模块在检测依赖时如果找不到静态.a文件会直接跳过对应功能。3. 依赖库逐个击破完整编译顺序与参数3.1 zlib、libpng、freetype第一梯队依赖库之间存在编译顺序不能乱来。zlib几乎被所有库依赖必须最先编cd zlib-1.2.11 CCaarch64-linux-gnu-gcc \ CXXaarch64-linux-gnu-g \ ./configure --prefix/opt/aarch64-sysroot --static make make installzlib的configure脚本是shell脚本不是autotools所以不需要--host参数用CC环境变量指定交叉编译器即可。编译完成后检查一下/opt/aarch64-sysroot/lib/libz.a是否存在。然后是libpng它依赖zlibcd libpng-1.6.37 ./configure --hostaarch64-linux-gnu \ --prefix/opt/aarch64-sysroot \ CPPFLAGS-I/opt/aarch64-sysroot/include \ LDFLAGS-L/opt/aarch64-sysroot/lib \ --enable-static --disable-shared make make installfreetype的配置类似但要额外注意它可能自动检测harfbuzz和png。如果harfbuzz还没编就用--without-harfbuzz临时禁用等后面harfbuzz编完再反回来编一次freetype。我建议干脆先临时关掉降低第一轮编译的耦合度。3.2 harfbuzz与icu文字整形和国际化的关键harfbuzz这库很特殊Qt的纹理渲染品质、复杂语言排版全靠它。但harfbuzz的configure脚本会尝试用目标编译器跑测试程序在交叉编译环境下这种测试经常失败。我当时直接加了--with-glibno把glib依赖关掉因为Qt静态编译时并不是真正需要app层面的glib它只被harfbuzz当成选项而已。cd harfbuzz-2.6.4 ./configure --hostaarch64-linux-gnu \ --prefix/opt/aarch64-sysroot \ --enable-static --disable-shared \ CPPFLAGS-I/opt/aarch64-sysroot/include \ LDFLAGS-L/opt/aarch64-sysroot/lib make make installicu其实是我最犹豫要不要编的库。Qt通过aarch64交叉编译时icu可以关闭-no-icu来减少体积和编译时间。但如果你的程序用到QCollator、QTextBoundaryFinder之类的高级文本功能icu还是建议编上。ICU的交叉编译是出了名地麻烦——它的configure需要先编译一个本地版本的工具用于生成数据文件再交叉编译目标版本。如果你看完整篇手册后依然觉得麻烦可以在Qt configure时加-no-icu。3.3 openssl静态里的HTTP/HTTPS基石Qt的QNetworkAccessManager走HTTPS必须依赖openssl。默认动态编译下Qt会自动找系统openssl但静态交叉编译必须手动指出路径。openssl的交叉编译命令非常清晰cd openssl-1.1.1k ./Configure linux-aarch64 \ --prefix/opt/aarch64-sysroot \ --cross-compile-prefixaarch64-linux-gnu- \ no-shared \ no-tests make -j$(nproc) make install值得注意的一点是openssl编译完以后我们把.a库放进sysroot还不行Qt在链接时还会去查找libcrypto.so和libssl.so。如果之前别的库带了共享版本链接器可能优先选择.so。这种时候可以在sysroot的lib目录里只保留.a文件或者建立libssl.so - libssl.a这样的软链来强制静态。实际上Qt通过openssl头文件版本检测后在链接命令里传的是-lssl -lcrypto如果目录里只有libssl.a链接器自然选静态。3.4 xcb相关库Qt GUI在Linux上的地基如果你要编的是带界面的Qt应用而不是纯命令行工具那xcb这一串库就是绕不开的大坑。xcb不是单个库而是一套协议栈xcb-proto生成代码的协议描述文件libxcbxcb-util含xcb-util-image、keysyms、renderutil等xcb-util-wmxcb-util-cursorxcb的编译顺序不能乱xcb-proto要最先装因为它只是perl脚本和协议定义文件后续库在configure时都要找xcb-proto的pkgconfig文件cd xcb-proto-1.13 ./configure --hostaarch64-linux-gnu \ --prefix/opt/aarch64-sysroot make make install装了xcb-proto之后libxcb的configure会去调用pkg-config查找协议版本。交叉编译时需要对PKG_CONFIG_PATH做特殊设置让它到sysroot的lib/pkgconfig目录去找export PKG_CONFIG_PATH/opt/aarch64-sysroot/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR/opt/aarch64-sysrootlibxcb还有很多依赖的第三方库libxdmcp、libxau等建议按官方README里的依赖树一个个编。这套东西虽然繁琐但只要你xcb这层过了后面的Qt configure基本就是一马平川。4. 手写qmake.confQt交叉编译的核心门槛4.1 从官方mkspec开始改不要从零写Qt源码里qtbase/mkspecs/下有大量现成的交叉编译配置。aarch64相关的目录有linux-aarch64-gnu-g但这个目录里的配置是给原生构建用的——它调用的是g而不是aarch64-linux-gnu-g。我们要做的就是复制目录然后修改编译器名称和相关FLAGS。cd qtbase/mkspecs cp -r linux-aarch64-gnu-g linux-aarch64-static-gnu-g改这个目录下两个关键文件qmake.conf和qplatformdefs.h。后者一般不用动重点是qmake.conf。4.2 qmake.conf里必须改动的五个部分打开linux-aarch64-static-gnu-g/qmake.conf核心内容如下# # qmake configuration for cross-compiling static Qt on aarch64 # MAKEFILE_GENERATOR UNIX 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_AR aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY aarch64-linux-gnu-objcopy QMAKE_NM aarch64-linux-gnu-nm QMAKE_STRIP aarch64-linux-gnu-strip # 包含头文件和库文件的路径 QMAKE_CFLAGS -I/opt/aarch64-sysroot/include QMAKE_CXXFLAGS -I/opt/aarch64-sysroot/include QMAKE_LFLAGS -L/opt/aarch64-sysroot/lib # 如果目标libc是musl则加 -static如果是glibc链接时再显式加 # QMAKE_LFLAGS -static load(qt_config)注意几点QMAKE_LINK_SHLIB如果不改成交叉编译器动态库的生成也会调用宿主g一出错就是cannot find crt1.o。QMAKE_AR cqs的cqs参数是静态库打包的标准参数c表示创建、q表示快速追加、s表示建立索引。缺了s后续链接时可能报ar: creating … index相关的问题。QMAKE_LFLAGS中的-L路径要确保sysroot里所有.a库都已就位否则configure阶段检测依赖会失败。4.3 一个经典陷阱-static该不该加在QMAKE_LFLAGS里网上的教程大多会让你在qmake.conf里直接加QMAKE_LFLAGS -static但这句话在交叉编译中是有副作用的——它会尝试把所有库全部静态链接包括libgcc和libstdc。如果你的交叉工具链没有配套的静态libstdc.aLinaro工具链默认有链接时就会报/usr/bin/ld: cannot find -lstdc更好的做法是qmake.conf里不写-static而是在编译完应用后手动在pro文件里针对目标平台添加unix:!macx { QMAKE_LFLAGS -static }或者干脆在make命令里追加make LFLAGS-static我个人建议把-static放在pro文件里这样能保留其它平台动态链接的灵活性只在aarch64静态目标时生效。5. Qt源码configure参数详解与编译5.1 configure的完整命令工具链、sysroot、qmake.conf都准备好之后终于可以进入Qt源码的configure环节了。下面是我最终跑通的完整configure命令逐段解释每部分的意图cd qt-everywhere-src-5.14.2 ./configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -xplatform linux-aarch64-static-gnu-g \ -opensource \ -confirm-license \ -static \ -release \ -nomake examples \ -nomake tests \ -no-opengl \ -no-eglfs \ -no-gtk \ -no-xkbcommon \ -no-cups \ -no-iconv \ -xcb \ -openssl-linked \ -I/opt/aarch64-sysroot/include \ -L/opt/aarch64-sysroot/lib \ -prefix /opt/qt-5.14.2-aarch64-static-static核心开关告诉Qt构建静态库而不是动态库。这会直接影响后续Qt模块的链接方式。-xplatform指定刚才复制好的mkspec目录名。-openssl-linked告诉Qt链接openssl而不是运行时动态加载。-xcb启用xcb平台插件。如果你需要framebuffer或offscreen可以加-linuxfb或-qpa offscreen作为备选。-no-opengl除非目标设备有GPU且你确定要OpenGL渲染否则关掉可以少掉一堆EGL相关的坑。如果确实需要可以用-opengl es2指定。-no-eglfs关闭EGL文件系统后端避免它去检测libEGL。-no-cups嵌入式设备一般没有打印机服务关掉。5.2 configure失败的常见信号与对策configure的日志在config.log里报错信息藏在最底下。我遇到的几个典型情况情况一The test for linking against openssl failed!这是openssl路径没对上。检查-I和-L指向的目录里是否真的有libssl.a、libcrypto.a以及openssl/ssl.h等头文件。另外Qt 5.14.2检测openssl时还会看版本太老的openssl 1.0.2在某些API上会报错建议直接用openssl 1.1.1。情况二The xcb library test failedxcb相关库缺失或者pkg-config路径没设好。回到第3.4节把PKG_CONFIG_PATH和PKG_CONFIG_SYSROOT_DIR导出后再试configure。可以先用pkg-config --libs xcb验证一下。情况三No such file or directory: .../include/c/...sysroot里的C头文件找不到一般是因为工具链自带的libstdc头文件路径不在sysroot的include搜索路径里。直接把工具链的include路径追加到CPLUS_INCLUDE_PATHexport CPLUS_INCLUDE_PATH/opt/aarch64-toolchain/aarch64-linux-gnu/include/c5.3 并行编译与资源控制configure通过后编译是整个流程中最耗时的部分。四核机器上并行度建议控制在-j4到-j8之间太多容易内存爆掉make -j4 make install整个Qt源码包在静态模式下编译一次大概需要40-60分钟视机器性能而定。这段时间不要干等着可以先把目标板上的测试脚本写好。编译完检查/opt/qt-5.14.2-aarch64-static/lib/下是否有.a文件和少量必要的.so文件。6. 编译、安装与静态部署验证6.1 写一个最小验证程序验证整条链路Qt安装到前缀目录后不要急着编译大型项目。先写一个最小程序验证交叉编译链路是通的#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello aarch64 Qt!); label.show(); return app.exec(); }对应的pro文件QT core gui widgets TARGET hello TEMPLATE app CONFIG static QMAKE_LFLAGS -static使用我们编译好的Qt编译这个程序export PATH/opt/qt-5.14.2-aarch64-static/bin:$PATH qmake hello.pro make file hello如果file显示ELF 64-bit LSB executable, ARM aarch64, statically linked说明整条链路已经通了。这一步是整个手册的里程碑任何一步出错现在修都还容易。6.2 进一步验证可执行文件的真实依赖静态链接不代表完全没有动态依赖。file命令显示statically linked后记得再用readelf交叉验证aarch64-linux-gnu-readelf -d hello | grep NEEDED如果输出为空说明没有动态依赖可以放心部署。如果输出了libstdc.so.6或libgcc_s.so.1说明你的工具链可能没带静态libstdc要么回退到自带静态版本的工具链要么把这几个so拷到目标板上去。Linaro 7.5.0工具链自带静态libstdc一般不会出现这个情况但如果用了某些简化版工具链就可能碰到。6.3 部署到目标板的细节静态编译的终极目标是省事但部署时仍有几件小事要注意可执行文件加执行权限chmod x helloQPA平台插件Qt静态编译后默认情况下xcb插件是编译进库里的运行时如果没有X server可以在程序里显式选择qputenv(QT_QPA_PLATFORM, offscreen);或者部署时用-platform offscreen参数启动。字体文件Qt静态并不包含中文字体如果界面要显示中文需要把字体文件如wqy-microhei.ttc放到目标板上并在代码里设置QFont font(WenQuanYi Micro Hei); app.setFont(font);我把一个5MB的wqy-microhei.ttc放进了可执行文件同目录的fonts/下实测中文渲染正常。7. 静态编译后程序链接的经典报错排查7.1 undefined reference toqt_plugin_instance一类的问题这是我最开始踩过、而且很多教程里都没提过的坑。Qt静态编译后插件比如xcb平台插件、图片格式插件都是静态库。如果你的程序只写了#include QApplication链接时经常报undefined reference to qt_plugin_instance_qt_plugin_xcb原因是Qt在静态模式下不会自动注册插件需要在main函数里显式导入#include QtPlugin Q_IMPORT_PLUGIN(QXcbIntegrationPlugin)具体要import哪些插件取决于configure时启用了哪些。开了xcb就import xcb插件开了qjpeg就importQJpegPlugin。如果嫌麻烦可以在pro文件里用QTPLUGIN xcbqmake会自动处理导入。7.2 链接顺序问题导致的一大串undefined reference静态链接有顺序问题。libqxcb.a在libQt5Gui.a之前还是之后直接决定链接成败。链接器是按文件出现顺序从左到右解析符号的如果被依赖的库出现在调用者之前就会报很多undefined reference。解决办法是在pro文件的LIBS里把库的排列顺序整理好或者用链接器参数QMAKE_LFLAGS -Wl,--start-group -lxcb -lQt5Gui -lQt5Core -Wl,--end-group--start-group会让链接器在组内反复搜索符号直到不再有新符号解析出来。这是处理静态库依赖环的官方解法。7.3 运行时的segfault静态Qt与宿主glibc冲突我在把程序拷到目标板子运行时遇到过段错误核心原因是编译机宿主系统的glibc版本高于目标板的glibc。你用的工具链里的libc版本如果比板子上的系统库还新静态编译的部分不受影响但程序初始化时仍可能加载某些动态数据文件导致段错误。最靠谱的排查方式用aarch64-linux-gnu-gdb在目标板上跑一次backtrace或者用strace跟踪启动过程。如果问题锁定在locale相关比如FATAL: kernel too old或cannot set locale就给程序加一行setlocale(LC_ALL, C);7.4 代码里隐藏的动态依赖与静态库的两难静态编译并不意味着所有库都必须静态链接。比如你用了dlopen打开一个第三方.so那这个.so如果依赖了libstdc的动态版就可能出现运行时符号冲突。我的经验是整个程序内部统一用静态链接的Qt但外部Module的加载尽量走独立进程或独立.so边界不要让静态Qt和动态Qt在同一进程里混用。8. 静态编译后的可执行文件瘦身与优化8.1 用strip减小体积一个最简单的hello程序静态编译出来通常在8-15MB左右原因是你把Qt Core、Gui、Widgets全塞进去了。strip一下可以明显瘦身aarch64-linux-gnu-strip hellostrip能去掉符号表但不影响程序运行。对于Qtstrip后体积能减少30%-40%。8.2 裁剪不需要的Qt模块5.14.2的configure里可以关掉一大堆用不到的模块-no-opengl可以减少OpenGL相关代码-no-dbus如果程序不依赖总线-no-printsupport如果没有打印需求-no-qml-debug去掉QML调试支持-no-feature-cups去掉CUPS打印支持甚至-no-feature-textodfwriter、-no-feature-texthtmlparser等稀疏特性开关这些裁剪项可以让静态库的最终体积少掉好几MB编译时间也缩短。但注意不要过度裁剪否则Qt某些类编译时会报feature xxx is disabled到时候反而要回头重编。8.3 使用upx压缩可选如果体积还是超标可以试试upx压缩aarch64-linux-gnu-upx helloupx对ARM64的支持还可以能把二进制压到50%左右。但它有一个隐患某些嵌入式设备的内核可能不允许upx解压时的自修改导致运行时报Text file busy。我建议只在开发阶段用upx正式发布版保持strip后的状态。8.4 轻量级运行时库的权衡这里多说一句静态链接Qt确实占空间但换来的是目标设备不需要任何运行时依赖这在设备系统被锁死的场景下是唯一可行的方案。Qt 5.14.2静态编译出来的hello带简单界面大概12MBstrip后7MB。如果你是那种极度抠空间的嵌入式项目可以考虑换用QLiteHTML或直接上Qt for MCU但那就不是本文讨论的范畴了。9. 把整个流程固化成脚本从手动到半自动9.1 我的build.sh骨架搭完一次之后我意识到这套环境不能在每台新机器上重复手敲一遍。动手写个build.sh固化整个流程后续只要改几个路径变量就能复用#!/bin/bash set -e SDKROOT/opt/aarch64-sysroot TOOLCHAIN/opt/aarch64-toolchain/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu QT_SRCqt-everywhere-src-5.14.2 QT_PREFIX/opt/qt-5.14.2-aarch64-static JOBS$(nproc) export PATH$TOOLCHAIN/bin:$PATH export PKG_CONFIG_PATH$SDKROOT/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR$SDKROOT export CPLUS_INCLUDE_PATH$TOOLCHAIN/aarch64-linux-gnu/include/c # 依赖库编译代码... # ( # cd deps/zlib \ # CCaarch64-linux-gnu-gcc ./configure --prefix$SDKROOT --static \ # make -j$JOBS make install # ) # Qt configure与编译 cd $QT_SRC ./configure \ -prefix $QT_PREFIX \ -xplatform linux-aarch64-static-gnu-g \ -opensource -confirm-license -static -release \ -nomake examples -nomake tests \ -no-opengl -no-eglfs -no-gtk -no-xkbcommon -no-cups \ -xcb -openssl-linked \ -I$SDKROOT/include -L$SDKROOT/lib make -j$JOBS make install把这个脚本放在干净机器上跑基本能做到无人值守编译。不过首次执行时建议还是盯着日志因为新版Ubuntu的头文件路径、perl版本等都可能影响configure的结果。9.2 版本的舞台把mkspec和依赖库版本统一管理真正坑过我的还有一件事依赖库的版本不一致。比如在机器A上编Qt时用的是libpng 1.6.37到机器B上用了1.6.39两者生成的静态库如果混用链接期不一定出错但运行期遇到ABI不兼容就可能崩溃。解决办法是给整个编译环境建一个manifest文件记下所有依赖库的版本Qt: 5.14.2 toolchain: gcc-linaro-7.5.0-2019.12 zlib: 1.2.11 libpng: 1.6.37 freetype: 2.10.2 harfbuzz: 2.6.4 openssl: 1.1.1k libxcb: 1.13以后换机器、换人重新编译严格按这份manifest走就不会被版本差异折磨。10. 一次跨项目的实战从静态Qt到多模块部署10.1 多可执行文件共享一套静态Qt的问题大型项目通常不止一个可执行文件。静态Qt在多可执行文件场景下有个麻烦每个可执行文件都会含有一份完整的Qt代码总体积成倍膨胀。比如你有5个业务程序每个都静态链接了Qt光Qt重复占用就可能达到40MB。我的做法是尽量把多个业务逻辑合并到一个可执行文件里用进程内模块的方式拆分而不是拆成多个二进制。Qt的插件机制在静态下虽然要手动注册但比多个二进制省空间得多。10.2 动态加载的.so业务模块如何链到静态Qt如果你实在无法避免动态加载.so那么这些.so尽量不要依赖Qt。或者更精确地说不要把.so编译成链接静态Qt的格式否则加载时会出现多份Qt单例状态比如qApp指针错乱、信号槽连接失败的诡异问题。如果需要.so里也使用Qt有两个解法把.so编译成动态链接到目标板上的Qt库但这又回到动态依赖的老问题且目标板必须有一套Qt。让.so独立携带Qt依赖但避免与主程序共享静态Qt符号编译.so时加-fvisibilityhidden或者用-Wl,-Bsymbolic隔离符号作用域。我自己在多个项目里实测下来最干净的方案还是一个进程一个静态Qt动态边界只走纯C接口或RPC把Qt对象留在主进程内。10.3 升级维护期的构建缓存策略静态编译很耗时所以构建缓存策略很重要。编译Qt源码时我习惯把$QT_SRC的源码目录和构建目录分开这样改mkspec、改configure参数时可以增量重新编译不会因为改动一个配置就把全部推倒重来mkdir -p build-qt-static cd build-qt-static ../qt-everywhere-src-5.14.2/configure ... make -j4Qt的构建系统支持Shadow Build影子构建源码目录不产生中间文件。我强烈建议所有Qt交叉编译构建都用这种方式后续调试和维护都方便。10.4 我实际用这套流程交付后的一些真实结论最终我在那个RK3568网关项目上交付的是一个12MB的可执行文件加一个2MB的字体文件整个应用目录不到15MB。跑在裁剪后的Buildroot系统上不依赖rootfs里任何库ldd显示not a dynamic executable甚至可以直接扔进initramfs里做单用户模式下的诊断工具。整个过程从零开始第一次完整走通大约用了三个工作日其中超过一半时间花在xcb库编译和链接顺序的排查上。如果严格按照本文的顺序来最快应该能压缩到一整天。11. 写在最后经验之外的建议把这套静态交叉编译流程完整走完我真的建议每个做嵌入式Linux应用的人都能体验一次。它虽然繁琐但能让你彻底理解Qt的模块化编译、交叉工具链的构成、sysroot的作用以及链接器解析符号的顺序逻辑。这些知识在你以后遇到任何换平台跑不起来的问题时都是可以直接迁移的排查武器。我自己的体会是Qt 5.14.2静态交叉编译就像是把能遇到的最难啃的骨头集中啃一遍工具链要会配、依赖库要会编、mkspec要会改、链接问题要会看。等你把这些都打通了再回头看动态编译会觉得轻舟已过万重山。如果你现在也卡在某一步——尤其是xcb那串库或者qmake.conf的配置上——别灰心按文章的排查思路走一遍大概率能找到答案。