资讯详情

资讯详情

aarch64嵌入式Qt静态交叉编译实战:从零构建单文件部署方案

做嵌入式设备最烦的事情之一就是应用部署。之前项目用的目标板是一块aarch64处理器的Linux板子存储空间紧张既不想在板子上放一堆Qt动态库又希望应用能单独拷贝、单独运行。为了这个目标我在x86的Ubuntu 20.04开发机上用交叉编译工具链把Qt 5.14.2编译成静态库再用这套静态库把自己的Qt Widgets应用链接成单个可执行文件直接丢到板子上就能跑。整个过程从零开始工具链搭建、configure参数调优、静态插件引入、目标板运行验证每一步都踩过坑。这篇文章就是完整的从零搭建手册适合要在ARM 64位嵌入式Linux平台上交付Qt界面应用、又不想依赖板载动态库的工程师参考。1. 为什么要在aarch64上做静态交叉编译1.1 静态编译解决的实际问题我手里这块目标板是aarch64架构的Linux系统存储空间非常紧张。最初试过在板子上直接放Qt动态库libQt5Core.so.5、libQt5Gui.so.5、libQt5Widgets.so.5再加上平台插件和依赖一套下来接近300MB对板子的eMMC压力非常大。而且动态库方案还容易踩版本冲突的坑板子系统自带的Qt版本和应用依赖的Qt版本不一致跑起来会出现各种莫名其妙的问题尤其是API行为差异导致的崩溃排查起来非常痛苦。改成静态交叉编译之后得到的是一个完全独立的可执行文件Qt相关代码全部打进去了部署就是scp一个文件到板子上的事。我们的实际应用场景下最终可执行文件大小在15MB左右比动态库方案整体体积反而更可控。静态链接还顺带解决了一个动态库方案里很隐晦的问题板子上的libQt5Core.so如果被系统更新或者误操作替换成新版本应用可能直接启动崩溃。静态编译没有动态库可以替换完全不存在这个顾虑。1.2 静态编译的本质不是全静态这里必须澄清很多新手容易误解的概念。Qt的静态编译也就是configure时的-static参数只代表Qt库以静态方式链接进可执行文件并不代表生成了一个完全不依赖外部库的ELF。链接器默认情况下依然会把libc、libstdc、libpthread这些基础C/C运行时动态链接进来。所以实际部署时目标板上仍然需要有一个可用的glibc运行时环境。注意不少第一次做静态Qt的人会惊讶地发现编译出来的程序还依赖libc.so.6以为哪里配置错了。这是正常现象。嵌入式环境里我强烈建议保留glibc动态链接。原因有两个一是glibc完全静态化会遇到NSS模块的问题DNS解析、用户/组查询这些系统功能在完全静态化的程序里会出各种诡异故障二是目标板本身就有glibc没必要为了省下这几百KB去承担兼容性风险。如果你确实需要做到完全单文件静态只能换musl工具链比如aarch64-linux-musl-gcc。Qt 5.14.2可以基于musl编译但需要额外补丁而且资料少、坑多不适合新手挑战。我在实际项目里一直用Qt静态加glibc动态这个组合稳定性经过长期验证是最推荐的做法。1.3 为什么选Qt 5.14.2这个版本版本选择上我对比过Qt 5.12、5.14、5.15三个分支。5.12相对偏老对较新的aarch64平台特性支持不够完整。5.15.2虽然是LTS但开源下载入口比较别扭非商业用户拿不到在线安装包源码编译也要手动找地址。5.14.2属于5.14分支的修复版本在download.qt.io上能直接找到完整的源码包下载基本没有门槛。更重要的是5.14之后的QPA架构已经非常成熟对linuxfb、eglfs这类嵌入式平台插件的支持很稳定Qt Widgets应用的静态链接方案被大量商业产品验证过。对纯Widgets界面需求来说5.14.2完全够用。2. 编译环境准备工具链、系统依赖和源码获取2.1 开发机系统选择开发机我用的Ubuntu 20.04 x86_64。推荐20.04的原因很简单这个版本仓库里自带的gcc-aarch64-linux-gnu版本是9.3和Qt 5.14.2官方推荐的编译器兼容性最好。我也在Ubuntu 22.04上试过工具链自动变成GCC 11.x编译过程中会出现一些配置检测差异主要是宏定义和行为变化需要额外处理。如果想少踩坑直接用20.04最省事。需要安装的包sudo apt update sudo apt install -y build-essential \ gcc-aarch64-linux-gnu g-aarch64-linux-gnu \ libgl1-mesa-dev libxkbcommon-dev libfontconfig1-dev这里有个需要说清楚的细节libgl1-mesa-dev、libxkbcommon-dev这些库装的是宿主机上的x86版本它们的作用是帮助configure阶段做一些特性检测。交叉编译出来的Qt静态库并不会去链接宿主机里的这些x86库所以不用担心架构错乱的问题。2.2 交叉工具链安装后的验证装完工具链先验证一下aarch64-linux-gnu-gcc --version再写一个最简程序测试echo int main(){return 0;} test.c aarch64-linux-gnu-gcc test.c -o test file test输出应该显示test: ELF 64-bit LSB executable, ARM aarch64, dynamically linked (uses shared libs), for GNU/Linux 3.7.0看到ARM aarch64字样说明交叉工具链工作正常。这里出现的for GNU/Linux 3.7.0指的是内核API版本不是宿主机内核版本目标板内核版本只要不是特别老一般都能跑。2.3 Qt源码下载与目录规划源码从download.qt.io下载wget https://download.qt.io/archive/qt/5.14/5.14.2/single/qt-everywhere-src-5.14.2.tar.xz这个包大概500MB解压tar xJf qt-everywhere-src-5.14.2.tar.xz mv qt-everywhere-src-5.14.2 ~/qt-src源码目录建议放在纯英文路径下。Qt的构建系统内部有大量路径拼接逻辑路径里带中文、空格或者特殊符号会在链接阶段引发稀奇古怪的问题。我见过有人把源码放在带空格的目录名里结果某些模块就是编不过改路径之后问题立刻消失。3. configure配置与参数交叉编译的关键决策点3.1 我最终使用的完整configure命令configure是整个静态交叉编译过程中最关键的环节直接决定后面编译是否顺利。进入源码目录执行cd ~/qt-src ./configure \ -prefix /opt/qt-5.14.2-aarch64-static \ -opensource -confirm-license \ -static -release \ -xplatform linux-aarch64-gnu-g \ -linuxfb -no-opengl \ -no-icu -no-dbus \ -no-feature-cups -no-feature-printdialog \ -qt-libpng -qt-libjpeg -qt-zlib -qt-pcre \ -sql-sqlite \ -nomake examples -nomake tests \ -skip qtwebengine -skip qt3d -skip qtquick3d \ -skip qtwebview -skip qtscript下面我把每个关键参数背后的原因拆开讲清楚。3.2 三个核心开关static、release与xplatform-static是这次编译的核心诉求让Qt所有模块产出静态库.a文件而不是动态库.so文件。-release表示只生成release版本。静态库如果带debug信息体积会膨胀到完全没法接受的程度比如libQt5Core.a动辄上百MB。而且交叉调试本身步骤繁琐一般不会有人真的去在目标板上单步调试Qt库内部代码。所以release是唯一合理选择。-xplatform linux-aarch64-gnu-g指定目标平台的mkspec名称。configure会去qtbase/mkspecs/linux-aarch64-gnu-g目录读取配置文件。这里有个非常容易混淆的点-platform参数是给宿主平台用的-xplatform才是给目标平台用的。交叉编译必须用-xplatform如果写成了-platform linux-aarch64-gnu-gconfigure会把目标板当成开发机来处理后续所有检测全部错乱。如果configure报错找不到linux-aarch64-gnu-g先检查PATH里有没有aarch64-linux-gnu-g再确认qtbase/mkspecs目录下是否存在这个mkspec目录。Ubuntu装上gcc-aarch64-linux-gnu后Qt 5.14.2自带的mkspec可以直接识别到标准工具链名称正常情况下不需要自定义。3.3 显示后端与模块裁剪的决策逻辑-linuxfb会启用Linux帧缓冲插件。Qt程序通过直接操作/dev/fb0把界面渲染到屏幕上不需要X11、Wayland这些显示服务器是最朴素的嵌入式显示方案。对Widgets应用来说这是最简单可靠的路径。如果目标板带GPU且要跑QML应该考虑-eglfs但那就需要额外交叉编译OpenGL ES库复杂度完全不在一个量级。我的项目是纯Widgets界面linuxfb足够稳定。-no-opengl和-linuxfb是配套的。既然不用eglfsOpenGL直接关掉。如果不关configure会去检测OpenGL相关库在交叉环境下没有现成的arm64版本就会检测失败导致整个配置中断。如果你的应用必须依赖OpenGL那就得提前准备好arm64版的libGL和EGL库再通过-sysroot参数告诉configure去哪个目录找。-no-icu和-no-dbus是我个人非常推荐的裁剪项。ICU是一个体量很大的Unicode处理库如果应用不需要复杂文本排版、不需要QCollator这种本地化排序功能关掉能砍掉大量编译时间和最终体积。D-Bus同理嵌入式板子上的Qt应用如果不需要进程间通信关掉能省掉一个完整的IPC依赖链。-no-feature-cups -no-feature-printdialog把打印支持关掉。绝大多数嵌入式设备根本不需要打印功能这两个选项如果不关configure会去检测cups头文件交叉环境稍微不完整就会报错。3.4 第三方库选型自带还是系统版本-qt-libpng -qt-libjpeg -qt-zlib -qt-pcre这一组参数让Qt使用源码包自带的第三方库而不是链接宿主机上的系统库。这个选择在交叉编译中至关重要宿主机上的libpng、libjpeg、zlib全部是x86架构的如果Qt配置成链接system库链接阶段要么找不到aarch64版本要么错误地把x86版本的静态库链接进来最终程序在目标板上直接段错误。用Qt自带库就从根源上规避了架构不匹配的问题代价是可执行文件体积会大一点但对静态交付来说这点体积完全在可接受范围内。-sql-sqlite让Qt SQL模块提供SQLite驱动插件。注意这里只是把SQLite驱动编进Qt库程序里要用到QSqlDatabase还得按第五节的静态插件方式显式导入这一步是在应用代码里做的不是configure能完成的。3.5 sysroot与目标板库依赖的两种准备思路交叉编译中最终Qt静态库可能还会链接一些目标板上的系统库。这里有两种做法。第一种直接用交叉工具链自带的sysroot也就是/usr/aarch64-linux-gnu目录。这个目录是安装gcc-aarch64-linux-gnu时自动创建的里面有基础libc、libstdc、pthread等运行时但fontconfig、xkbcommon这类系统库基本是缺失的。结果是Qt的某些功能会自动降级或禁用。对纯Widgets程序来说大部分情况下影响不大可以接受。第二种给目标板准备更完整的sysroot。Ubuntu上可以启用arm64架构然后安装对应的交叉版本库sudo dpkg --add-architecture arm64 sudo apt update sudo apt install -y libfontconfig1-dev:arm64 libdbus-1-dev:arm64 libxkbcommon-dev:arm64这些库会安装到/usr/aarch64-linux-gnu目录恰好就是交叉编译器的默认sysroot。但我实测下来有个问题arm64架构的库依赖树比想象中复杂装一个fontconfig可能会连带拉入一堆依赖有的版本会跟工具链自带的sysroot内容冲突。所以我的建议是如果不是确实需要某个系统库来启用Qt的特定功能不要贸然把整个sysroot装满。先用Qt自带库把最小集跑通后面缺什么再补什么。3.6 configure阶段常见的两类失败configure阶段遇到最多的报错是ERROR: Feature xcb was enabled, but the feature xcb_xlib cannot be enabled这类错误通常是显式开启或默认启用了某个功能但交叉环境里它的依赖库没准备好。解决办法很简单用-no-feature-xxx把对应功能关掉或者先把依赖库的arm64版本准备好。另一类常见的提示是The test for linking against libxcb will failQt在交叉环境下检测到没有xcb库时会给出这个提示。如果你不需要X11忽略即可如果要在板子上跑X11环境就必须交叉编译libxcb和它的所有X11依赖库这个工作量确实不算小。4. 编译安装与自定义mkspec4.1 编译过程中的并行度控制configure成功之后开始编译make -j4并行度建议控制在4到8之间。我曾在24核机器上直接跑make -j$(nproc)结果编译内存冲到16GB以上磁盘IO长时间高负载整个系统卡到几乎不可操作。Qt这种大型项目并行度太高反而容易在个别模块上触发竞态条件导致编译中断。整个Qt库在关闭webengine等大模块的前提下-j4大约需要1到2小时这个时间成本可以接受。4.2 安装与验证编译产物编译完成后执行make install默认安装到之前-prefix指定的/opt/qt-5.14.2-aarch64-static。检查安装结果ls /opt/qt-5.14.2-aarch64-static/lib应该能看到一排.a文件libQt5Core.a libQt5Gui.a libQt5Widgets.a libQt5Network.a ...再用自带qmake确认平台信息/opt/qt-5.14.2-aarch64-static/bin/qmake -query输出里有一项QT_SYSROOT:正常情况是空的。这表示qmake没有显式配置sysroot使用的是编译器默认值。这不是错误但心里要有数。4.3 什么时候需要自定义mkspec正常情况下Qt 5.14.2自带的linux-aarch64-gnu-g已经能应对标准Ubuntu工具链。但实际项目里很多开发板厂商提供的是专用工具链名字可能是arm-rockchip830-linux-uclibcgnueabihf之类内置mkspec完全不认识。这时候就需要自定义mkspec。方法是在qtbase/mkspecs下新建一个目录比如linux-aarch64-custom-g里面放两个文件。qmake.conf内容参考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 /opt/toolchain/bin/aarch64-linux-gnu-gcc QMAKE_CXX /opt/toolchain/bin/aarch64-linux-gnu-g QMAKE_LINK /opt/toolchain/bin/aarch64-linux-gnu-g QMAKE_LINK_SHLIB /opt/toolchain/bin/aarch64-linux-gnu-g QMAKE_AR /opt/toolchain/bin/aarch64-linux-gnu-ar cqs QMAKE_OBJCOPY /opt/toolchain/bin/aarch64-linux-gnu-objcopy QMAKE_NM /opt/toolchain/bin/aarch64-linux-gnu-nm -P QMAKE_STRIP /opt/toolchain/bin/aarch64-linux-gnu-strip QMAKE_RANLIB /opt/toolchain/bin/aarch64-linux-gnu-ranlib QMAKE_CFLAGS -marcharmv8-a QMAKE_CXXFLAGS -marcharmv8-a load(qt_config)另一个qplatformdefs.h可以直接软链接到内置mkspec同名文件ln -s ../linux-aarch64-gnu-g/qplatformdefs.h qplatformdefs.hconfigure时把-xplatform linux-aarch64-gnu-g改成-xplatform linux-aarch64-custom-g即可。自定义mkspec的额外好处是可以针对具体芯片指定优化参数比如-marcharmv8.2-a -mtunecortex-a72让编译出来的库和应用更贴合实际硬件。5. 落地验证用静态库交叉编译第一个Qt程序5.1 最小测试项目先建一个测试目录和两个文件。main.cpp#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(aarch64 static Qt works); label.resize(480, 200); label.show(); return app.exec(); }hello.proQT core gui widgets TARGET hello TEMPLATE app SOURCES main.cpp5.2 用交叉qmake编译编译前先临时把交叉qmake放到PATH最前面export PATH/opt/qt-5.14.2-aarch64-static/bin:$PATH cd test qmake hello.pro make如果一切顺利当前目录会生成hello可执行文件。用file确认架构file hello输出正常情况下是hello: ELF 64-bit LSB executable, ARM aarch64, dynamically linked (uses shared libs), for GNU/Linux 3.7.0这里dynamically linked指的是glibc动态链接Qt本身已经是静态的了。想验证这一点用aarch64-linux-gnu-readelf -d hello | grep NEEDED查看依赖项如果只有libc、libstdc、libm、libpthread这些系统库没有libQt5Core.so.5之类说明Qt静态链接成功。如果还想把libstdc也静态进去以减少对目标板libstdc版本的依赖可以在.pro里加QMAKE_CXXFLAGS -static-libgcc -static-libstdc QMAKE_LFLAGS -static-libgcc -static-libstdc这种组合我实际交付时经常用既能避免目标板libstdc版本过旧的问题又不用承担glibc完全静态化带来的NSS风险。5.3 静态插件链接必然要踩的坑直接把编译好的hello放到板子上运行很大概率会看到这样的报错This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. Available platform plugins are: (empty)原因是平台上唯一的linuxfb插件是静态编译的而静态库创建的可执行文件在链接时默认不会把没被引用的静态库目标文件拉进来。平台插件没有进入可执行文件程序启动时自然找不到平台插件。解决办法是显式导入插件。在main.cpp开头加上#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)然后重新编译就正常了。如果程序里用了Qt SQL的SQLite驱动同样要加Q_IMPORT_PLUGIN(QSQLiteDriverPlugin)不想改代码的话也可以在.pro文件里声明QTPLUGIN qlinuxfbqmake会处理插件的链接。但实际项目里我更推荐Q_IMPORT_PLUGIN方式因为插件清单在代码里一目了然多个插件同时存在时更好排查问题。静态插件链接这个问题几乎每个从动态Qt转静态Qt的人都会遇到提前知道就能省下不少排查时间。5.4 部署到目标板并处理运行环境把hello拷到板子上运行./hello -platform linuxfb或者通过环境变量指定export QT_QPA_PLATFORMlinuxfb ./hello如果板子上的/dev/fb0存在且当前用户有权限界面就会出现在屏幕上。实际部署中我遇到过的几种情况列在这里方便对照排查提示Cant open framebuffer device /dev/fb0要么当前用户没有权限要么板子内核没有使能frame buffer设备。前者用root跑或加权限后者需要检查内核配置和启动日志。程序启动后屏幕没有内容可能是帧缓冲格式和Qt预期不一致。可以检查一下QT_QPA_FB_FORCE_FORMAT是否匹配板子屏幕驱动支持的格式。界面文字全部是方框板子系统里没有中文字体或者Qt找不到字体目录。解决办法在下一节细说。6. 后期运维与常见问题字体、依赖库和链接报错6.1 交叉编译OpenSSL与SQLite扩展如果Qt程序要访问HTTPS接口静态Qt库就必须启用SSL支持。默认情况下如果configure时没有检测到OpenSSLQt会编出一个不带SSL功能的版本。程序里调用HTTPS接口时会报qt.network.ssl: QSslSocket: cannot resolve TLSv1_2_client_method解决办法是先把OpenSSL交叉编译到aarch64生成arm64版的libssl.a和libcrypto.a然后重新configure Qt增加以下参数-openssl-linked \ -I/path/to/openssl/aarch64/include \ -L/path/to/openssl/aarch64/lib-openssl-linked表示把OpenSSL用静态链接的方式链进Qt证书库则需要在运行时通过SSL_CERT_FILE环境变量指定到板子上的证书文件。如果你的网络请求不需要验证服务端证书也可以把OpenSSL和证书都打进应用但生产环境我不建议这么做。SQLite驱动插件的问题和平台插件类似即使configure时加了-sql-sqlite应用运行时依然会提示找不到SQLite驱动。解决方案也是用Q_IMPORT_PLUGIN(QSQLiteDriverPlugin)导入。6.2 静态Qt的中文字体问题静态编译后的Qt本身不携带任何字体文件运行时完全依赖目标板系统里的字体。很多精简的嵌入式板子出厂不带中文字体或者根本没有完整的字体目录结果就是Qt界面上的中文全部显示成方框。最直接的解决办法是拷贝一个中文字体到板子上比如文泉驿正黑wqy-zenhei.ttc放到/usr/share/fonts/truetype/目录下然后设置环境变量export QT_QPA_FONTDIR/usr/share/fonts/truetype再运行应用中文就能正常渲染了。如果不想依赖外部字体目录也可以把字体文件打包到应用目录在代码里直接加载#include QFontDatabase QFontDatabase::addApplicationFont(/app/fonts/wqy-zenhei.ttc); QFont font(WenQuanYi Zen Hei); app.setFont(font);这种方式的好处是应用完全自包含哪怕板子上没有任何系统字体也能正常显示文字。6.3 链接阶段典型报错速查整理一份我在实际编译中遇到最多的链接错误对照表遇到问题时可以直接查报错信息原因处理方式cannot find -lGLconfigure没禁用OpenGL链接时找不到libGL回configure加-no-opengl重新编译Qtcannot find -lts检测到tslib但库缺失加-no-tslib或先交叉编译tslibundefined reference to qt_plugin_instance_qlinuxfb缺少静态插件导入用Q_IMPORT_PLUGIN或QTPLUGINcannot mix incompatible Qt library (5.15.x) with this library (5.14.2)编译应用时qmake路径和编译器用的Qt版本不一致检查PATH和qmake路径确保用的是交叉编译出的5.14.2的qmakeunknown module(s) in qt: serialport该模块在当前静态Qt构建中未启用确认configure是否包含serialport必要时重新构建Qt最后一种unknown module(s) in qt是很多人用静态交叉编译时容易困惑的。系统里可能装了另一个Qt版本那里有serialport而你当前用的静态Qt构建时并没有包含这个模块。排查方法很简单先执行qmake -query QT_INSTALL_PREFIX确认当前qmake是不是交叉编译出来的那个然后看/opt/qt-5.14.2-aarch64-static/include下有没有QtSerialPort目录。6.4 把整套交叉编译环境打包复用整个流程跑通之后/opt/qt-5.14.2-aarch64-static就是一套完整的交叉编译SDK。可以把整个目录打包tar czf qt-5.14.2-aarch64-static.tar.gz /opt/qt-5.14.2-aarch64-static以后换到另一台开发机只要解压到相同路径设好交叉工具链和PATH就能直接编译应用不需要重新configure和编译Qt。团队协作时这个方法能做到多人共享一套编译环境省掉每个人各自编译一遍Qt的几十个小时。最后再分享一个排查经验静态交叉编译的Qt版本、工具链版本和目标板内核版本三者之间的关系很微妙。如果程序在板子上运行崩溃或者某些系统API调用行为异常优先检查目标板的glibc版本是否低于编译主机上交叉工具链的glibc版本。用ldd --version在板子上查一下glibc再用aarch64-linux-gnu-gcc -print-file-namelibc.so.6确认交叉编译器链接的glibc路径这个对比能解决很多运行期的疑难杂症。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →