ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Qt 5.14.2 aarch64静态交叉编译实战:从configure到部署全流程解析

Qt 5.14.2 aarch64静态交叉编译实战:从configure到部署全流程解析 把 Qt5.14.2 以静态方式交叉编译到 aarch64 架构这件事听起来像一条命令实际上踩起来全是坑。前阵子我接手一个嵌入式 Linux 项目目标板是 ARMv8 开发板要在上面跑一个 Qt 客户端既没有桌面环境也不能依赖太多动态库。最后我选择了 Qt5.14.2 aarch64 交叉编译器 全静态链接的方案前后折腾了几个周末踩过了工具链不匹配、mkspec 找不到、第三方库链接顺序混乱、运行时报平台插件缺失这些典型的坑才把流程完全跑通。这篇文章就是一遍踩坑换来的完整操作手册写给后面要接手同样工作的人。不管你是嵌入式小白还是已经编译过 Qt 但没试过静态交叉编译的老手照着这篇文章的思路走至少能少走一半弯路。我会把“为什么这么做”和“怎么一步步做”都讲清楚而不是贴一堆命令让你自己试错。1. 动手之前先搞清楚静态交叉编译要解决什么问题1.1 为什么最后选择了“静态交叉编译”很多人第一次接触 Qt 嵌入式开发时会有一个错觉直接把 Qt 源码拿到开发板上去编译不就是“原生编译”了吗确实可行但现实往往不允许。开发板的 CPU 性能通常比较弱在板子上编译完整的 Qt 库动辄两三个小时甚至更久而且内存稍小的板子在并行编译时很容易 OOM。开发过程中每改一次 Qt 配置要重新编一遍那个等待时间会让人绝望。交叉编译的做法是在一台高性能的 x86 宿主机上用目标架构的交叉编译器生成 aarch64 的程序和库再把编好的产物拷贝到开发板上。这样编译一次 Qt 库只需要十几分钟到半小时效率完全不在一个量级。至于“静态”和“动态”的选择我在这两者之间也纠结过。动态编译出的可执行文件很小但部署时要跟着拷贝一堆 Qt 的 .so 文件。如果开发板的 rootfs 里恰好少了某个依赖库或者开发板上的 glibc 版本和宿主机编译环境不一致程序跑起来就会出现各种诡异的问题比如缺符号、段错误、加载不到平台插件等。静态编译会把 Qt 库直接编进可执行文件部署时只要拷一个文件运维成本非常低。静态编译的代价也很明确可执行文件体积明显变大纯 Widgets 的小程序一般也有 8MB 到 20MB首次编译 Qt 库因为要生成全套静态库时间会比动态编译略长还有一点必须注意静态链接意味着你选择的 Qt 许可证合规性需要自己确认如果项目是商业闭源软件使用 Qt 的 LGPL 条款时对静态链接有额外的要求这一点建议提前和法务确认不要等产品发布前再补救。1.2 从零开始的全局路线图在正式动手之前先把整个流程在脑子里画出来后面每一步才知道自己处在哪个位置。我当时给自己列了一个非常简单粗暴的路线图在宿主机安装 aarch64 交叉编译工具链。下载 Qt 5.14.2 源码包解压到固定目录。在源码根目录运行 configure指定静态编译、release模式、aarch64 目标平台。执行 make 编译 Qt 全套静态库。用编译出的 qmake 配置业务工程编译生成最终可执行文件。把可执行文件部署到开发板解决运行环境问题。这个顺序不能乱。尤其是 configure 阶段路径和参数错了后面 re-run 一遍会生成一堆残留配置非常恶心。我建议每一步做完都做一次简单的验证比如工具链装没装好、qmake 是不是指向了正确路径再进入下一步。看似多花了几分钟实际上能帮你节约很多排错时间。2. 搭建交叉编译环境2.1 安装交叉编译器与依赖工具我宿主机用的是 Ubuntu 20.04交叉编译器直接通过 apt 安装没花太多精力。常见的 aarch64 交叉工具链就是 gcc-aarch64-linux-gnu安装命令如下sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu安装完验证一下版本确保工具链可用aarch64-linux-gnu-gcc --version编译 Qt 除了编译器之外还需要一堆辅助工具包括 make、perl、python3。Qt 的构建系统在解析工程和生成部分源代码时需要 perl如果你的机器上没有装会在 configure 阶段直接报错。Ubuntu 下可以把依赖一次性装齐sudo apt install -y make perl python3 libclang-devlibclang 那一步不是必须的它主要是给 Qt Creator 的代码模型用的纯命令行编译可以不管。我装它的原因是为了后续需要打开 Qt Creator 查看源码时避免缺依赖如果你不打算用 IDE可以跳过。另外提一句如果你在 Windows 上想搞 aarch64 交叉编译需要安装 WSL 或者专门的交叉编译工具链比如 Linaro 的 aarch64-linux-gnu。我这边所有的操作都是在 WSL 的 Ubuntu 环境里完成的步骤和 Linux 宿主没有区别所以这篇文章以 Ubuntu 为例展开。2.2 目标sysroot怎么准备以及什么时候可以不准备sysroot 是交叉编译里的一个概念简单理解就是一个“迷你根文件系统”里面放着目标板上的头文件和库文件交叉编译器去这个目录里找标准库和依赖项而不是去宿主机的 /usr 里找避免把 x86 的库错误地链接进来。Ubuntu 的 aarch64 交叉编译器默认带了一套自己的 aarch64 libc 头文件和静态库如果你只编译一个纯静态的可执行文件不需要额外准备 sysroot 也能通过。也就是说你不会在链接阶段遇到找不到 libc 的问题。但如果你要链接项目自己的第三方库比如 libgpiod、libsqlite、libcurl 这些就必须把目标板对应架构的 .so 或 .a 文件放进 sysroot或者通过 QMAKE_LIBDIR 指定。我当时没有用第三方 C 库只用 Qt 自带的模块所以做了一个取巧的决定不单独制作 sysroot直接靠交叉编译器自带的 aarch64 运行时。这样省去了一步但我也建议你在做之前先确认业务工程有没有额外的链接依赖如果有还是老老实实从目标板上拷贝一份 /lib、/usr/include、/usr/lib 过来用 Qt 的 configure 参数指定-sysroot /path/to/your/rootfs这样在 configure 时Qt 检测 OpenSSL、ICU 等可选依赖时会去这个 sysroot 里找检测结果也更符合目标板的实际情况。2.3 下载Qt 5.14.2源码与目录规划地址方面Qt 5.14.2 源码可以从 Qt 官网 archive 下载也可以找国内镜像站拉取。源码包的典型名字是 qt-everywhere-src-5.14.2.tar.xz体积大概 500MB 左右。下载完之后我习惯把源码和编译产物分开目录放置避免以后想清理时找不到哪些是构建生成的。我当时规划的目录如下/opt/qt-everywhere-src-5.14.2 # Qt 源码包解压目录 /opt/qt-aarch64-static # 安装目录prefix源码目录解压完成后先不要急着 configure。检查一下目录里是否有 configure 可执行文件以及你的磁盘空间是否足够。静态编译 Qt 需要的临时空间比最终安装体积大不少建议至少留出 10GB 空闲空间。我把编译过程中产生的临时文件也放在源码目录里没有刻意改 OUTDIR因此磁盘不足会在 make 中途报错那时候清理起来很被动不如提前确认。3. Qt 5.14.2 的 configure 与构建流程3.1 值得逐个说清楚的configure参数configure 这一步是整个交叉编译最关键的地方参数设置不对后面编译出来的 Qt 要么还是 x86 的产物要么链接阶段缺模块。先把常用参数的含义说清楚你才能根据自己项目需求做增删。第一个必须指定的是 -xplatform。Qt 的 mkspec 机制里不同的目标平台对应不同的编译配置aarch64 对应的规范名是 linux-aarch64-gnu-g。指定了它Qt 才会调用 aarch64-linux-gnu-gcc 作为编译器。如果你漏了这一步默认会走宿主机平台编出来的是 x86 版本相当于白干。第二个是 -static。这个参数告诉 Qt 构建静态库而不是动态的 .so。单独加这个参数还不够还要配合 -release 来关闭 debug 构建减少冗余符号和体积。第三个是 -prefix 和 -extprefix。前者是安装后文件路径后者是交叉编译时实际落地路径。两者在很多情况下可以设为同一个目录。如果你设的 install prefix 指向开发板将来要用的路径比如 /opt/qt但宿主机上不想真安装到那个路径就另外指定 extprefix。我直接统一指到宿主机的 /opt/qt-aarch64-static简单省事。第四个是模块裁剪参数。Qt 5.14 的源码有大量模块很多在嵌入式项目里根本用不到比如 qtwebengine编译一次极其耗时必须跳过。我常用的裁剪参数是-skip qtwebengine -skip qtwebview -skip qt3d -skip qtcanvas3d -skip qtcharts下面安排一个更具体的表格把 configure 阶段最核心的参数按“参数/作用/我的建议”三列展示方便查阅参数作用我的建议-static生成静态库而不是动态库必须加不加就是动态编译-release编译 release 版本必须加避免 debug 符号膨胀-xplatform linux-aarch64-gnu-g指定目标平台为 aarch64 Linux必须加这是交叉编译的核心-opensource -confirm-license接受开源协议必须加否则会卡在协议确认-no-opengl不支持 OpenGL嵌入式无 GPU 场景建议关闭-no-dbus不支持 D-Bus嵌入式单进程应用可关闭-no-xcb不依赖 X11 窗口系统脱离桌面环境时必须关闭-no-feature-xcb同上进一步禁用 xcb 功能同上-nomake examples -nomake tests不编译示例和测试建议开启节省大量时间-skip qtwebengine跳过 WebEngine 模块必须跳过编译它量太大3.2 可直接抄的完整配置命令结合上面的参数表我当时最终使用的 configure 命令如下你可以直接复制修改后使用cd /opt/qt-everywhere-src-5.14.2 ./configure \ -prefix /opt/qt-aarch64-static \ -extprefix /opt/qt-aarch64-static \ -xplatform linux-aarch64-gnu-g \ -opensource -confirm-license \ -release \ -static \ -no-opengl \ -no-dbus \ -no-xcb \ -no-feature-xcb \ -nomake examples \ -nomake tests \ -skip qtwebengine \ -skip qtwebview \ -skip qt3d \ -skip qtcanvas3d \ -skip qtcharts \ -skip qtdatavis3d \ -skip qtgamepad \ -skip qtpurchasing \ -make libs \ -make tools这里重点解释一下 -make libs 和 -make tools。不加这两个参数Qt 默认会编译一部分模块加上之后更明确地告诉构建系统只要库和工具不要 examples、tests 这些对嵌入式没有意义的东西。如果你需要保留 Qt Widgets 界面千万不要加 -no-gui。我这边是纯 Widgets 应用所以保留默认的 gui 模块只关掉 opengl 和 xcb。这样编译出的 Qt 仍然包含 QWidget、QPainter 等基础绘图能力UI 界面照常能用。configure 过程中会打印一堆模块检测结果。你需要重点看的几个值Platform 是否显示为 linux-aarch64-gnu-g以及 是否生成了 qmake路径是不是在 qtbase/bin 下面。如果这里显示的是 x86_64,就说明 -xplatform 参数没生效立刻中断排查不要往下走。3.3 mkspec原理qmake 为什么能识别 aarch64顺着前面的 configure 参数聊一聊 mkspec。Qt 的构建系统非常依赖 mkspec它其实是一组配置文件的集合定义了一套编译规则包括“我用什么编译器”“归档工具是谁”“链接时的参数是什么”。Qt 源码的 qtbase/mkspecs/ 目录下放了几乎所有平台的定义。linux-aarch64-gnu-g 这个条目下有一个 qmake.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如果交叉编译器不在 PATH 里或者工具链安装到了非标准位置configure 时 Qt 找不到 aarch64-linux-gnu-g就会报错。这时有两个办法一是把工具链的 bin 目录加进 PATH我推荐的做法二是直接修改 qtbase/mkspecs/linux-aarch64-gnu-g/qmake.conf 里的编译器路径改成绝对路径。我个人建议用 PATH 的方式因为不仅 configure 需要后面业务工程编译的时候qmake 也会自动调用同样的编译器。如果你只改了 mkspec 里的绝对路径业务工程里假如又用了别的交叉工具还是可能对不上。3.4 开始编译与产物确认configure 通过之后剩下的就是等待。执行make -j$(nproc)这里建议别盲目用 -j16 或更多如果你宿主机内存不大并行编译很容易 OOM。我一开始用 -j8 在 16GB 内存机器上编译连续两次在最后阶段报内存不足。把并行数降到 4 之后才稳定跑完。所以如果有内存焦虑直接用 -j4 更稳妥。编译完成后执行安装make install装完后检查一下静态库是否真的生成ls /opt/qt-aarch64-static/lib | grep libQt5Core静态库的标志是结尾带 .a例如 libQt5Core.a。如果看到的是 .so说明 configure 的 -static 参数没有生效或者 configure 没重新运行需要退回去重来。qmake 工具在安装后会出现在 /opt/qt-aarch64-static/bin/qmake。这个 qmake 是宿主机架构的可执行文件这点别搞混它运行在 x86 Ubuntu 上用于生成 Makefile生成的 Makefile 里记录的是交叉编译规则所以最终产物是 aarch64 的。理解这一点后面看到 qmake 能跑起来但编译出来的是 ARM 程序就不会觉得奇怪了。4. 业务工程如何接入这套交叉编译环境4.1 用编译出来的qmake而不是系统里的qmakeQt 安装好之后最容易犯的一个错误是调用了系统原有的 qmake而不是 /opt/qt-aarch64-static/bin/qmake。系统 qmake 通常指向 /usr/bin/qmake对应的是宿主机 Qt 版本和 x86 编译器用它生成 Makefile交叉编译自然就不成立了可能还会出现“Qt 5.14.2 not found”这类错误。我强烈建议把编译出的 Qt 的 bin 目录加入 PATH并且把 PATH 的优先级放最前面export PATH/opt/qt-aarch64-static/bin:$PATH export QMAKESPEClinux-aarch64-gnu-g然后验证which qmake qmake -v只要输出里能看到 aarch64 相关路径或者 Qt 5.14.2 的安装路径说明环境变量生效了。QMAKESPEC 其实多数情况下不用手动指定因为 qmake 编译进 Makefile 时会带上当前的 mkspec但我习惯显式设置避免以后某个终端窗口忘记加载配置。4.2 最小可执行工程示例为了验证环境是否正常我先写了一个最简单的 hello 工程杜绝 UI 干扰先把链路打通。main.cpp 如下#include QApplication #include QLabel int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label(Hello aarch64 Qt); label.resize(320, 200); label.show(); return app.exec(); }hello.pro 如下QT core gui widgets TARGET hello TEMPLATE app SOURCES main.cpp编译命令很简单qmake hello.pro make生成 hello 可执行文件后用 file 命令看一下架构file hello输出里如果包含 “ELF 64-bit LSB executable, ARM aarch64” 并且静态链接说明整个交叉编译链路已经通了。如果看到 “x86-64”肯定是编译器指向出错了优先检查 PATH 和 mkspec。4.3 第三方静态库的链接与顺序问题实际项目不太可能只用 Qt如果工程里还引用了 OpenSSL、zlib 之类的外部库静态链接就要格外注意库的顺序。静态链接和动态链接不同链接器处理 .a 归档文件时是从左到右扫描的如果一个静态库 A 引用另一个静态库 B 里的符号A 必须排在 B 前面否则链接器在扫描 A 时还没把 B 加入符号表会直接报 undefined reference。这个坑非常经典我自己也踩过而且是在最后集成功能的时候才暴露定位起来很费劲。在 .pro 里我的写法通常是INCLUDEPATH /path/to/third/libs/include LIBS -L/path/to/third/libs/lib \ -lcurl \ -lssl \ -lcrypto \ -lz需要特别注意的是如果 -lcurl 依赖 -lssl -lcrypto这三个库的顺序就不能随意调整一般把依赖方放前面被依赖方放后面。如果出现一堆 undefined reference 但是看不出规律别急着怀疑编译器先试试把库顺序倒过来或者重复写一遍被依赖的库。这是静态链接独有的现象新手很容易在这里浪费大量时间。5. 部署到开发板的运行细节5.1 静态单文件部署三板斧静态编译最大的福利是部署极简核心就三步。第一步确认可执行文件确实是静态链接。用 readelf 或者 file 工具检查readelf -l hello | grep INTERP静态可执行文件通常没有 INTERP 段因为没有动态解释器。我常用的另一个命令是 ldd它会提示 “not a dynamic executable”这就代表静态链接成功了。第二步把文件拷贝到开发板。如果开发板和宿主机在同一个局域网直接用 scp 就可以scp hello usertarget_board_ip:/home/user/第三步给可执行文件加执行权限然后运行chmod x hello ./hello如果你在调试界面程序还需要确认显示环境。如果开发板有 framebuffer 设备可以指定 Qt 使用 linuxfb 插件如果只是无头环境测试可以用 offscreen 插件。5.2 静态编译和动态交叉编译怎么选我在项目刚开始时对比过静态和动态两种方案最后的结论是如果你非常在意可执行文件大小并且板子的 rootfs 可控动态编译确实是更轻盈的选择但如果你希望部署流程足够简单、板子的环境不可控静态编译几乎是唯一的省心方案。下面把两者的差异整理成表方便判断对比项静态编译动态交叉编译可执行文件体积较大通常 8MB 起较小通常几百 KB 到几 MB部署步骤拷贝单文件即可需要同时拷贝多个 .so 并设置搜索路径运行时依赖基本不依赖系统 Qt 库依赖目标板动态库存在且版本匹配升级 Qt 的影响重新编译业务工程即可可能需要整体替换动态库调试复杂度相对简单问题多集中在编译期运行期可能出现缺符号等问题许可证合规需要特别注意合规要求动态链接相对宽松对比下来静态编译最适配的是工业控制、电力监测、手持终端这类对部署稳定性要求很高的嵌入式场景而动态方案更适合设备厂商自己维护完整 rootfs 的环境。5.3 平台插件缺失这个经典坑跑静态 Qt 程序我遇到最多的问题就是运行时报错could not find or load the Qt platform plugin linuxfb Available platform plugins are: offscreen.原因是 Qt 的图形平台插件QPA默认是动态加载的。静态编译时插件对应的代码并没有自动链进可执行文件需要你在 .pro 文件里显式声明使用哪些插件。这里和前缀环境变量 QT_QPA_PLATFORM_PLUGIN_PATH 没有太大关系因为根本没编译进去设了路径也找不到。解决办法有两个。第一最推荐的方式在 .pro 中声明插件QTPLUGIN qlinuxfb同时可以加上CONFIG static这样 Qt 会尝试在链接阶段把 linuxfb 插件以静态方式合并进来。如果这样做了之后仍然找不到插件那就需要代码里手动导入插件。在 main.cpp 顶部加上#include QtPlugin Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)之后再指定运行时的平台插件export QT_QPA_PLATFORMlinuxfb ./hello这一点如果不提前处理交叉编译就算成功了最终在板子上也跑不起来而且报错信息对新手非常不友好。你在网上搜 Qt QPA platform plugin path 相关话题时大概率看到的就是这个问题。静态编译时先把插件声明好能省掉一把眼泪。6. 编译、链接、运行三阶段的坑6.1 编译阶段容易遇到的问题编译阶段最烦的是“编译器串台”。症状是 configure 阶段正常make 到一半突然报错提示头文件里有大量 x86 相关的系统类型定义。这个问题的根源一般有两个一是 qmake 生成 Makefile 时CC/CXX 变量不是 aarch64 交叉编译器二是业务工程里自己添加的 INCLUDEPATH 误把宿主机 /usr/include 加了进来。排查思路也很简单打开 Makefile看 CC 和 CXX 对应的路径。如果出现的是 gcc 或 cc而不是 aarch64-linux-gnu-gcc说明 qmake 的 mkspec 没摘干净。可以用下面的变量强制覆盖qmake hello.pro \ QMAKE_CCaarch64-linux-gnu-gcc \ QMAKE_CXXaarch64-linux-gnu-g另外一个常见报错是 “cannot find -lGL”这通常是因为 Qt 的 xcb 或 OpenGL 检测没有被完全关掉。检查 configure 参数里是否有 -no-opengl -no-xcb如果有但还报可能需要修改 mkspec 里的 QMAKE_LIBS_OPENGL把 -lGL 直接清空。我在关闭了 opengl 之后便没有再遇到这个问题但它确实烦人。6.2 链接阶段容易遇到的问题链接阶段最典型的是 undefined reference原因分三类。第一类是没有链接对应模块比如用了 QPainter 却只在 .pro 里写了 QT core gui没有加 widgets。Qt 5 里 QWidget 相关类在 Widgets 模块必须显式写 QT widgets。第二类是第三方库顺序问题在上一节已经说过这里不再重复。第三类是 GCC 版本和库不一致导致的符号问题。如果你的交叉编译器版本太老而定制的 sysroot 是从较新系统上拷贝的glibc 里有新符号老编译器不认识链接时会报缺某个版本的 GLIBC_XX。解决方法是把工具链版本和 rootfs 来源统一尽量使用同一时期发布的版本。Ubuntu 20.04 自带的 gcc-9 配合 aarch64 libc比较稳定这也是我推荐在 Ubuntu 20.04 上操作的原因。6.3 运行阶段容易遇到的问题运行阶段的坑集中在两个方向。一个是显示环境。开发板如果没有显示屏程序运行时指定 offscreen 平台可以验证逻辑但看不到界面如果板子有 framebuffer 设备需要检查 /dev/fb0 是否存在且应用有访问权限。很多时候程序默默退出没有任何错误信息就是因为平台插件初始化失败被当作普通崩溃退出了。另一个是字体缺失。Qt 在嵌入式环境中默认字体往往找不到界面上所有中文都显示成方框。解决方法一般是把目标板上已有的中文字体文件路径通过环境变量指定给 Qt或者直接在代码里用 QFontDatabase 加载指定路径的字体文件。这个问题的排查比较隐蔽因为程序不报错只是显示效果异常。建议在集成 UI 前先验证文字渲染别等到最后才发现全是方块。6.4 Qt aarch64静态编译问题速查表为了方便你做快速排查我把这段时间遇到的高频问题整理成一张速查表现象可能原因处理办法configure 检测平台为 x86_64-xplatform 参数未生效或拼写错误重新运行 configure核对 linux-aarch64-gnu-gmake 产出的库是 .so 而不是 .a未加 -static 或 configure 被跳过清空构建目录重新 configure 后再 make编译到一半报 GNU C 头文件不匹配编译器串台用了宿主 gcc检查 Makefile 的 CC/CXX用 qmake 变量强制覆盖链接提示找不到 -lGLOpenGL 未被彻底关闭加 -no-opengl并调整 qmake.conf 中 QMAKE_LIBS_OPENGL可执行文件是 x86 架构PATH 里的 qmake 用的是系统 Qt修改 PATH确认 qmake 在 /opt/qt-aarch64-static/bin运行时报找不到 linuxfb 插件插件未静态链入.pro 加 QTPLUGIN qlinuxfb或代码 Q_IMPORT_PLUGIN程序运行秒退且无输出QPA 插件初始化失败先试 QT_QPA_PLATFORMoffscreen逐层定位中文全部显示为方框缺少字体文件指定字体文件路径或用 QFontDatabase 加载make 过程 OOM并行编译任务过多降低 -j 参数比如 -j4上面这张表不敢说覆盖所有场景但基本是静态交叉编译链路里最常出现的类型。遇到问题先照着表里对号入座不能解决再深入到日志里看比盲目重编要高效得多。最后再说一个我个人的习惯不管工程大小先做一个“最小闭环”再集成业务代码。意思是先用一个空窗口程序把“宿主机编译—板子运行”这条链路跑通然后再往里面加业务模块。很多人在最开始一上来就编译完整项目失败之后根本分不清是 Qt 环境问题还是业务代码问题。先让 hello 跑起来后面的所有报错就都有了明确参照物排查速度会快很多。
返回列表