
1. 为什么要在 aarch64 上折腾 Qt 5.14.2 静态编译如果你手头有一块 Orange Pi CM5、树莓派 CM4 或者任何一块 aarch64 架构的板子想把 Qt 程序直接跑在上面大概率会遇到一个很现实的问题板子上的系统版本五花八门glibc 版本、libstdc 版本、各种依赖库的版本全都不一样。动态链接编出来的可执行文件换一台设备就报GLIBC_2.xx not found这种体验相当糟糕。静态编译就是来解决这个问题的。把 Qt 库、C 运行时、甚至部分系统库全部打进一个可执行文件里拷过去就能跑不依赖目标板上的 Qt 环境。代价是文件体积大、编译时间长、某些依赖 glibc 动态加载的特性会受限但对于嵌入式部署场景来说这个取舍通常是值得的。Qt 5.14.2 是一个 LTS 版本在嵌入式领域用得非常多稳定性经过大量项目验证。aarch64 是当前 ARM 64 位的主流架构Orange Pi、树莓派、瑞芯微 RK3588 这些平台都是这个架构。把这两个组合起来做静态交叉编译是很多嵌入式 Qt 开发者绕不开的一道坎。这篇内容适合谁看已经会用 Linux 基本命令、了解交叉编译概念、手上有 aarch64 目标板或者准备做 aarch64 部署的开发者。如果你完全没接触过交叉编译建议先补一下 gcc 工具链的基本用法否则后面很多步骤会一头雾水。整个流程我会按实际操作的顺序来写包括工具链准备、源码配置、编译踩坑、目标板验证这几个阶段。中间会穿插一些我实际踩过的坑和对应的解决办法这些在官方文档里基本找不到。2. 工具链选型与宿主机环境准备2.1 交叉编译工具链到底选哪个aarch64 的交叉编译工具链有好几个来源常见的有工具链来源特点适用场景Linaro GCC官方维护版本更新及时通用 ARM 开发ARM 官方 GNU ToolchainARM 自家维护优化好深度 ARM 优化Bootlin Toolchain带完整 sysroot开箱即用嵌入式快速上手发行版自带如 Ubuntu 的 gcc-aarch64-linux-gnu安装方便版本偏旧快速验证我个人的选择是Bootlin 的 aarch64 工具链原因是它自带完整的 sysroot里面包含了目标系统需要的头文件和库文件省去了自己构建 sysroot 的麻烦。Bootlin 提供的工具链基于 glibc版本选择上要注意和目标板的 glibc 版本匹配。提示工具链的 glibc 版本必须小于等于目标板的 glibc 版本否则编出来的程序在板子上跑不起来。用ldd --version在目标板上查一下 glibc 版本然后选一个不超过这个版本的工具链。假设目标板是 Ubuntu 20.04 aarch64glibc 是 2.31那就选 Bootlin 里 glibc 2.31 或更低的工具链。如果选了 glibc 2.35 的工具链编出来的程序在板子上会直接报版本不匹配。2.2 宿主机环境的具体配置宿主机我用的是 Ubuntu 20.04 x86_64这是最省事的组合。如果你用 CentOS 7.9也能做但一些依赖包的名称不一样需要自己转换。先装基础依赖sudo apt update sudo apt install -y build-essential git python3 perl \ bison flex gperf libfl-dev \ libxcb1-dev libx11-dev libxext-dev libxrender-dev \ libxi-dev libxkbcommon-dev libxkbcommon-x11-dev \ libfontconfig1-dev libfreetype6-dev \ libssl-dev libglib2.0-dev这些包看着多但每一个都有用。比如libxcb1-dev系列是 Qt 的 xcb 平台插件依赖libfontconfig1-dev和libfreetype6-dev是字体渲染依赖libssl-dev是网络模块的 SSL 支持。少装一个configure 阶段就会报某个 feature 不可用。工具链解压到一个固定路径比如/opt/toolchainsudo mkdir -p /opt/toolchain sudo tar -xf bootlin-aarch64-glibc-2.31.tar.xz -C /opt/toolchain解压后目录结构大概是/opt/toolchain/aarch64--glibc--stable-2021.11-1/bin/里面有一堆aarch64-linux-前缀的工具。把这个 bin 目录加到 PATH 里export PATH/opt/toolchain/aarch64--glibc--stable-2021.11-1/bin:$PATH验证一下aarch64-linux-gcc --version能输出版本信息就说明工具链可用了。2.3 源码包和依赖的下载Qt 5.14.2 的源码包从官方下载页面拿文件名是qt-everywhere-src-5.14.2.tar.xz大概 500 多 MB。下载完之后解压tar -xf qt-everywhere-src-5.14.2.tar.xz cd qt-everywhere-src-5.14.2这里有个细节qt-everywhere-src是完整源码包包含了 Qt 的所有模块。如果你只需要核心模块可以在 configure 的时候用-skip参数跳过不需要的模块能省不少编译时间。比如不需要 WebEngine、不需要 Multimedia就加上-skip qtwebengine -skip qtmultimedia。注意Qt WebEngine 在 aarch64 上交叉编译极其麻烦依赖 Chromium 的构建系统除非你确实需要浏览器内核否则强烈建议跳过。我试过一次光编译就花了六个多小时最后还是因为某个依赖问题失败了。3. configure 阶段的参数设计与原理3.1 静态编译的核心配置项configure 是整个流程里最关键的一步参数配错了后面全是坑。先看一个完整的配置命令./configure \ -prefix /opt/qt5.14.2-aarch64-static \ -opensource -confirm-license \ -release \ -static \ -no-shared \ -nomake examples -nomake tests \ -skip qtwebengine -skip qtmultimedia \ -xplatform linux-aarch64-gnu-g \ -sysroot /opt/toolchain/aarch64--glibc--stable-2021.11-1/aarch64-buildroot-linux-gnu/sysroot \ -no-opengl \ -no-eglfs \ -no-xcb \ -qt-zlib -qt-libpng -qt-libjpeg -qt-freetype \ -no-glib \ -no-cups -no-iconv \ -no-feature-accessibility \ -no-dbus \ -no-gui \ -no-widgets这个配置是给纯命令行程序用的不带 GUI。如果你需要 GUI把-no-gui -no-widgets去掉同时把-no-xcb改成-xcb并且确保 sysroot 里有对应的 xcb 库。逐个解释关键参数-static和-no-shared是静态编译的核心告诉 Qt 只生成静态库不生成动态库。这两个参数必须同时用只写一个可能不生效。-xplatform linux-aarch64-gnu-g指定使用哪个平台配置文件。Qt 源码里qtbase/mkspecs/目录下有一堆平台配置linux-aarch64-gnu-g是给 aarch64 Linux 用的。如果这个目录不存在需要自己从linux-arm-gnueabi-g复制一份改。-sysroot指向工具链的 sysroot 目录Qt 编译时会从这里找头文件和库文件。这个路径必须准确写错了会报找不到头文件。-qt-zlib -qt-libpng -qt-libjpeg -qt-freetype表示使用 Qt 自带的这些库而不是系统库。静态编译时强烈建议用 Qt 自带的因为系统库可能是动态链接的混进去会导致最终可执行文件仍然依赖动态库。-no-glib是因为 glib 在静态编译时容易出问题而且 Qt 对 glib 的依赖不是必须的去掉能减少很多麻烦。3.2 mkspec 文件的定制linux-aarch64-gnu-g这个 mkspec 不一定存在需要检查一下ls qtbase/mkspecs/ | grep aarch64如果没有就从现有的复制一份cp -r qtbase/mkspecs/linux-arm-gnueabi-g qtbase/mkspecs/linux-aarch64-gnu-g然后编辑qtbase/mkspecs/linux-aarch64-gnu-g/qmake.conf把里面的工具链前缀改成 aarch64 的QMAKE_CC aarch64-linux-gcc QMAKE_CXX aarch64-linux-g QMAKE_LINK aarch64-linux-g QMAKE_LINK_SHLIB aarch64-linux-g QMAKE_AR aarch64-linux-ar cqs QMAKE_OBJCOPY aarch64-linux-objcopy QMAKE_NM aarch64-linux-nm -P QMAKE_STRIP aarch64-linux-strip这里的前缀要和工具链实际的前缀一致。Bootlin 的工具链前缀是aarch64-linux-Linaro 的可能是aarch64-linux-gnu-要按实际情况改。3.3 configure 报错的常见原因configure 阶段最常见的报错是找不到某个库或者某个 feature 不可用。比如ERROR: Feature xcb was enabled, but the pre-condition features.thread libs.xcb failed.这说明 sysroot 里没有 xcb 库或者路径不对。解决办法是在 sysroot 里确认一下find /opt/toolchain/.../sysroot -name libxcb*如果没有要么换一个带 xcb 的工具链要么在 configure 里加上-no-xcb关掉这个特性。另一个常见问题是-static和某些模块冲突。比如 Qt 的qtdeclarative模块在静态编译时可能需要额外的配置如果报错可以先-skip qtdeclarative跳过等核心模块编译通过后再单独处理。提示configure 的输出信息很长建议重定向到文件里慢慢看./configure ... 21 | tee configure.log。出问题的时候从后往前翻找到第一个 ERROR 或者 was enabled, but 这样的关键字。4. 编译过程中的资源控制与踩坑处理4.1 并行编译的度怎么把握configure 通过之后用make开始编译。Qt 的编译非常吃资源尤其是内存。一个经验值是每核至少 2GB 内存。如果你的机器是 8 核 16GB那make -j8基本是极限再高就容易 OOM。make -j$(nproc) 21 | tee build.log如果编译过程中出现virtual memory exhausted或者Killed说明内存不够了降低并行度make -j4 21 | tee build.log编译时间方面8 核机器上完整编译 Qt 5.14.2 大概需要 1.5 到 2 小时。如果跳过了 WebEngine 和 Multimedia能缩短到 1 小时左右。4.2 几个高频报错的处理报错一error: numeric_limits is not a member of std这是 GCC 版本和 Qt 源码的兼容性问题。Qt 5.14.2 的某些代码没有显式包含limits头文件在新版 GCC 上会报这个错。解决办法是在报错的文件开头加上#include limits或者更省事的办法在 configure 的时候加上-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT之类的宏定义绕过。不过最稳妥的还是直接改源码找到报错的文件补上头文件。报错二undefined reference to pthread_create静态链接时 pthread 库需要显式链接。在qmake.conf里加上QMAKE_LIBS -lpthread -ldl -lrt或者在 configure 的时候加上-lpthread。报错三cannot find -lGL如果 configure 时没有加-no-opengl编译到 QtGui 模块时会去找 OpenGL 库。aarch64 的 sysroot 里可能没有 GL 库加上-no-opengl跳过即可。如果确实需要 OpenGL需要在 sysroot 里安装对应的库。报错四unknown module(s) in qt: serialport这个报错通常出现在使用 qmake 构建项目时说明 QtSerialPort 模块没有被编译进去。检查 configure 时是否加了-skip qtserialport如果加了就去掉。另外静态编译时模块的依赖关系需要显式声明在.pro文件里加上QT serialport并且在链接时确保libQt5SerialPort.a被正确引用。4.3 编译完成后的安装与验证编译完成后make install安装到-prefix指定的目录。安装完成后检查一下ls /opt/qt5.14.2-aarch64-static/lib/应该能看到一堆.a静态库文件以及libQt5Core.a、libQt5Gui.a这些核心库。如果看到.so文件说明静态编译没生效需要回去检查 configure 参数。再验证一下 qmake/opt/qt5.14.2-aarch64-static/bin/qmake -query输出的QT_INSTALL_PREFIX应该是你设置的路径QT_VERSION应该是 5.14.2。5. 用静态 Qt 构建第一个 aarch64 程序5.1 项目 .pro 文件的写法静态编译的 Qt 项目.pro文件和动态编译基本一样但有几个地方要注意QT core QT - gui CONFIG console CONFIG - app_bundle TARGET hello_aarch64 TEMPLATE app SOURCES main.cpp # 静态编译时需要显式链接的库 LIBS -lpthread -ldl -lrt如果用到 GUIQT core gui widgets LIBS -lpthread -ldl -lrt -lfontconfig -lfreetype-lfontconfig和-lfreetype是字体渲染需要的静态链接时不会自动带上必须手动加。5.2 编译和部署的完整流程用静态 Qt 的 qmake 生成 Makefile/opt/qt5.14.2-aarch64-static/bin/qmake hello_aarch64.pro make -j4编出来的可执行文件用file命令检查一下file hello_aarch64应该显示ELF 64-bit LSB executable, ARM aarch64。再用ldd检查aarch64-linux-ldd hello_aarch64如果是静态编译应该显示not a dynamic executable或者只依赖少数几个系统库。把可执行文件拷到目标板上scp hello_aarch64 usertarget-board:/home/user/在板子上直接运行./hello_aarch64如果能正常输出说明整个流程走通了。5.3 静态编译程序的体积优化静态编译的程序体积通常很大一个简单的 Hello World 可能就有 10MB 以上。如果带 GUI轻松超过 30MB。优化手段有几个strip 符号表aarch64-linux-strip hello_aarch64这一步能去掉调试符号体积能减少 50% 以上。编译时加 -Os在.pro文件里加上QMAKE_CXXFLAGS -Os QMAKE_CFLAGS -Os-Os是优化体积的选项比-O2生成的代码更小。用 upx 压缩如果目标板支持upx --best hello_aarch64upx 能把可执行文件压缩到原来的 30% 左右但运行时需要解压会稍微增加启动时间。嵌入式场景下如果存储空间紧张这个手段值得考虑。注意upx 压缩后的程序在某些安全策略下可能无法运行部署前先在目标板上验证一下。6. 目标板上的运行验证与问题排查6.1 运行时的常见报错静态编译的程序在目标板上运行时最常见的报错是GLIBC_2.xx not found这说明工具链的 glibc 版本高于目标板的 glibc 版本。解决办法是换一个更低版本的工具链重新编译。这个问题没有别的绕过方法只能从工具链层面解决。error while loading shared libraries: libstdc.so.6: cannot open shared object file静态编译时如果 C 运行时没有完全静态链接仍然会依赖libstdc.so.6。在链接时加上-static-libstdc -static-libgccQMAKE_LFLAGS -static-libstdc -static-libgccQFontDatabase: Cannot find font directory静态 Qt 程序在目标板上找不到字体目录。解决办法是在程序启动时设置字体路径QFontDatabase::addApplicationFont(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf);或者把字体文件打包到程序里用QResource加载。6.2 性能与内存的实测数据我在 Orange Pi CM5RK35884GB 内存上实测了一个静态编译的 Qt Widgets 程序数据如下指标动态编译静态编译可执行文件体积2.1 MB28 MB启动时间0.8 秒1.2 秒内存占用空闲45 MB52 MB部署依赖需要 Qt 运行时无启动时间慢了 0.4 秒主要是静态链接的程序在加载时需要做更多的重定位工作。内存占用多了 7MB因为静态库的代码段全部加载到了进程空间。但换来了零依赖部署这个代价完全可以接受。6.3 一个实际项目的部署案例我之前做过一个基于 Qt 的工业数据采集程序跑在 RK3588 板子上需要读取串口数据、显示实时曲线、通过 TCP 上报。用动态编译时每次换板子都要重新装 Qt 运行时非常麻烦。改成静态编译后整个部署流程简化成在开发机上makescp到板子chmod x然后直接运行不需要在板子上装任何 Qt 相关的东西。这个程序用到了 QtCore、QtGui、QtWidgets、QtSerialPort、QtNetwork 这几个模块静态编译后体积 35MBstrip 之后 22MB完全在可接受范围内。串口模块在静态编译时有个小坑QSerialPort的静态插件需要手动导入。在main.cpp里加上#include QtPlugin Q_IMPORT_PLUGIN(QSerialPortPlugin)不加这一行的话运行时会报No such file or directory找不到串口设备。这个坑我踩了整整一个下午才找到原因。7. 几个容易忽略的细节和长期维护建议7.1 静态编译的许可证问题Qt 5.14.2 在开源协议下LGPLv3使用静态编译需要遵守 LGPLv3 的条款。简单说如果你静态链接了 LGPLv3 的 Qt 库你需要提供重新链接的能力或者把目标文件也开源。对于内部使用的项目这个问题不大如果要发布商业软件建议仔细阅读 LGPLv3 的条款或者考虑商业授权。提示Qt 5.14.2 的开源版本是 LGPLv3不是 LGPLv2.1。这两个协议对静态链接的要求不一样LGPLv3 更严格。如果你的项目对许可证敏感建议在动手之前先确认清楚。7.2 编译环境的可复现性交叉编译环境很容易因为工具链版本、系统库版本的变化而变得不可复现。建议把整个环境用 Docker 固化下来FROM ubuntu:20.04 RUN apt update apt install -y build-essential git python3 perl \ bison flex gperf libfl-dev libxcb1-dev libx11-dev \ libxext-dev libxrender-dev libxi-dev libxkbcommon-dev \ libfontconfig1-dev libfreetype6-dev libssl-dev COPY toolchain /opt/toolchain ENV PATH/opt/toolchain/bin:$PATH这样下次换机器或者重装系统直接docker build就能恢复环境不用重新踩一遍坑。7.3 增量编译和模块化构建Qt 完整编译一次要一两个小时如果每次改一点东西都全量编译效率太低。建议把 Qt 的编译和项目的编译分开Qt 库编译一次安装到固定路径之后不再动项目代码用 qmake 单独编译只编译改动的文件如果确实需要重新编译 Qt 的某个模块可以进入对应的子目录单独 makecd qtbase make -j8 make install这样只重编 qtbase不用整个源码树重来。7.4 版本升级的注意事项Qt 5.14.2 之后Qt 5.15 是最后一个 5.x 系列版本。如果将来要升级到 Qt 5.15configure 参数基本兼容但有几个变化Qt 5.15 默认使用 C17需要工具链支持某些模块的依赖关系有调整比如 QtQuick 对 OpenGL 的依赖更强静态编译时-no-opengl可能导致 QtQuick 无法使用升级前建议先在虚拟机里完整走一遍流程确认没问题再迁移到正式环境。7.5 一个实用的调试技巧静态编译的程序在目标板上崩溃时因为没有动态库的符号信息gdb 的堆栈跟踪可能不完整。解决办法是在编译时保留调试符号部署时用strip生成两个版本# 保留符号的版本用于调试 cp hello_aarch64 hello_aarch64.debug # strip 后的版本用于部署 aarch64-linux-strip hello_aarch64崩溃时把hello_aarch64.debug拷到板子上用 gdb 加载gdb ./hello_aarch64.debug (gdb) core-file core (gdb) bt这样能看到完整的调用栈定位问题会快很多。这个技巧在排查段错误时特别有用我靠它解决过好几个偶发崩溃的问题。整个流程走下来最耗时的部分其实是 configure 阶段的参数调试和编译等待。一旦 configure 通过后面的步骤基本都是机械操作。建议第一次做的时候把每一步的命令和输出都记录下来形成自己的操作手册下次换工具链或者换 Qt 版本时能省很多时间。