1. 项目概述与背景分析
1.1 为什么要做aarch64静态交叉编译
做嵌入式Linux开发的人,迟早会碰上这么一件事。手里拿到一块ARM64的开发板,或者甲方给了指定规格的工控机,然后告诉你在上面跑一个带界面的程序,UI还得过得去,但系统里没有包管理器,或者出厂文件系统特别精简,甚至连libstdc++版本都对不上。这时候你才意识到:动态编译那套思路,在嵌入式场景下经常是灾难。
动态编译下,Qt程序运行起来依赖一堆so:libQt5Widgets.so.5、libQt5Gui.so.5、libQt5Core.so.5,还有平台插件libqlinuxfb.so、字体库、ICU那些零零碎碎。如果在开发机上能用,换到目标板上缺这个缺那个,光是符号找不到的问题就够查半天。更麻烦的是,Qt5.14.2这个版本是基于老的ABI约定编译的,如果目标板的系统gcc版本特别高,旧库可能还能兼容,但反过来,目标板系统太老,glibc版本上不去,那基本就废了,只能走静态这条路。
静态交叉编译的核心思路,一句话总结就是:程序运行时不依赖任何动态链接的Qt库和第三方库,所有依赖全部编进ELF文件里。这样一来,部署的时候只需要一个二进制文件,放到目标板任意目录,权限给上就能直接跑,不需要管目标机上有没有装Qt、有没有对应版本的运行库、环境变量怎么配,全都省了。对于量产设备、多台分发、容器化敏捷部署这种场景,静态编译几乎是唯一靠谱的方案。
1.2 这套方案的适用场景与不适用场景
先说说什么情况下你才需要折腾静态交叉编译。我自己这些年踩过的场景大概是这样的:
- 目标板文件系统极小,比如只有几百MB的busybox系统,包管理器不可用,也没办法单独安装运行时库。
- 需要把同一个可执行文件交付给多个硬件平台,而且这些平台的系统版本、库版本不可控,动态编译没法保证每台机器都能跑。
- 甲方对外发版本时只给了一个二进制,开发环境不能透露,源代码更不能交付,此时静态编译让目标机上没有开发痕迹。
- 程序内含自身更新机制,得把新版本二进制文件直接拉下来覆盖运行,没有任何安装步骤,这种时候动态库的管理成本会让人崩溃。
不适用场景也要说清楚。如果目标板系统是标准的Debian/Ubuntu发行版,而且你能通过apt安装Qt库,那动态编译显然更省事,编译时间短,体积小,调试方便。另外如果用到了某些需要运行时动态加载的插件机制,比如QPluginLoader加载第三方插件,静态编译会让这套机制变得非常麻烦,因为插件本身也得静态链接,做不到边运行边加载。这一点上的取舍,务必先想好再做。
2. 环境准备与工具链选型
2.1 完整工具链清单
静态交叉编译这件事,成败有一半取决于你的工具链是否顺滑。网上很多教程都默认读者已经搭好了交叉编译环境,但新手恰恰就卡在这里。我把我实际验证过的一套组合贴出来,你照着准备就行。
开发机操作系统我建议用Ubuntu 20.04 LTS或者Debian 11,不要用太新的大版本,有时候新系统自带工具链的默认特性会和Qt5.14.2的源码有莫名其妙的小冲突。
| 工具 | 版本/包名 | 说明 |
|---|---|---|
| 交叉编译器 | gcc-aarch64-linux-gnu (9.4) | Ubuntu源里的默认arm64交叉gcc |
| make | 4.2+ | 系统自带即可 |
| CMake | 3.16+ | 构建部分依赖项时需要 |
| Perl | 5.30+ | 编译Qt必须 |
| Python | 3.8+ | Qt构建脚本依赖 |
| 辅助工具 | pkg-config, libtool, autoconf | 编译zlib等依赖库需要 |
Ubuntu上执行安装命令:
sudo apt update sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ make cmake perl python3 python3-pip \ pkg-config libtool autoconf automake \ flex bison gperf装完确认编译器能跑:
aarch64-linux-gnu-gcc --version输出里应该能看到gcc (Ubuntu 9.4.0...这种信息。如果系统源里的版本是11或12,也不一定就不行,但我在Qt 5.14.2上踩到过gcc 12编译出的libstdc++符号不兼容的问题,这里特意用9.4图个稳妥。如果你想用更新一点的Linaro工具链也行,但要注意工具链对应的glibc版本别太新,目标板系统如果比较老,反而会出问题。
2.2 交叉编译的核心原理:sysroot是怎么回事
在说具体编译之前,我得花点篇幅把“sysroot”这个概念讲透,因为大多数做上层应用开发的人第一次接触交叉编译时,都会在这里产生困惑。
你在开发机上编译的是给ARM64板子用的程序,但编译过程中需要找到头文件(stdio.h、stddef.h这些)和库文件(libc.so、libm.so这些)。普通编译时,这些文件在开发机的/usr/include和/usr/lib/x86_64-linux-gnu/下,但那些是x86_64架构的,ARM64的程序不能用。所以交叉编译需要一套专门属于ARM64架构的根文件系统,里面放着ARM64版的头文件、ARM64版的运行库,这套东西就叫sysroot。
GCC在编译时会通过--sysroot参数指定这个根目录的位置,然后它就会在这个目录下查找include和lib,而不是去开发机的系统目录。Ubuntu上的gcc-aarch64-linux-gnu包装好了一套基础的sysroot,在/usr/aarch64-linux-gnu/下,已经包含了ARM64的glibc和基本头文件。你可以在终端里试一下:
aarch64-linux-gnu-gcc -print-sysroot能看到类似/usr/aarch64-linux-gnu的输出。但光有基础运行库不够,Qt还需要zlib、libpng、freetype这些第三方库,你需要把这些库的ARM64版本也编译出来并安装到这个sysroot里,后续Qt的configure才能找到它们。这个思路一定先记牢:先把依赖库装进sysroot,再编译Qt,最后交叉编译你的应用。
2.3 目录规划与源码下载
为了防止整个工作过程变成一团乱麻,我建议你建一个独立的工程目录,所有交叉编译相关的源码和产物都放里面。
mkdir -p ~/qt514-aarch64 cd ~/qt514-aarch64 mkdir -p src sysroot buildsrc:存放所有源码包sysroot:自己扩展的ARM64系统根目录build:编译过程中产生的临时文件和最终产物
源码下载这块,我用的是qt-everywhere-opensource-src-5.14.2.tar.xz。Qt官网现在需要登录才能下载了,我提供一个仍然可用的官方镜像路径:
wget https://download.qt.io/archive/qt/5.14/5.14.2/qt-everywhere-opensource-src-5.14.2.tar.xz这个包是完整源码,包含qtbase、qtdeclarative、qtsvg、qtimageformats等所有模块,静态编译时需要用到的模块它都包全了。
依赖库我列一下:
| 库名 | 版本 | 用途 |
|---|---|---|
| zlib | 1.2.13 | 压缩支持 |
| libpng | 1.6.39 | PNG图片格式 |
| freetype | 2.10.4 | 字体渲染 |
| fontconfig | 2.13.1 | 字体配置与管理 |
| libffi | 3.4.2 | 动态类型系统(部分功能依赖) |
| expat | 2.4.8 | XML解析(fontconfig依赖) |
| openssl | 1.1.1w | SSL/HTTPS支持(可选项) |
这里特意选zlib 1.2.13而不是更新的1.3.x,稳字当头。Qt 5.14.2时代的代码比较古老,新版依赖库有时候会引入不兼容的API变化,导致编译直接失败。后面编译时我再说具体会遇到哪些问题。
3. 依赖库静态编译全流程
3.1 环境变量设置
依赖库编译时频繁需要指定交叉编译器和sysroot路径,我习惯先把这些环境变量放到一个独立的脚本里,每次新起终端先source一下:
cat > cross-env.sh << 'EOF' export ROOT_DIR=~/qt514-aarch64 export SYSROOT=$ROOT_DIR/sysroot export CROSS_COMPILE=aarch64-linux-gnu- export CC=${CROSS_COMPILE}gcc export CXX=${CROSS_COMPILE}g++ export AR=${CROSS_COMPILE}ar export AS=${CROSS_COMPILE}as export LD=${CROSS_COMPILE}ld export RANLIB=${CROSS_COMPILE}ranlib export STRIP=${CROSS_COMPILE}strip export CPP="${CROSS_COMPILE}gcc -E" export NM=${CROSS_COMPILE}nm # 关键:让各库能通过pkg-config找到之前装进sysroot的依赖 export PKG_CONFIG_PATH=$SYSROOT/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR=$SYSROOT export CFLAGS="--sysroot=$SYSROOT" export CXXFLAGS="--sysroot=$SYSROOT" export LDFLAGS="--sysroot=$SYSROOT" EOF注意PKG_CONFIG_SYSROOT_DIR这个变量,它会让pkg-config在返回-I和-L参数时自动前缀上sysroot路径,省去很多手动改参数的麻烦。如果不设置这个,pkg-config返回的路径会是/usr/include/freetype2这种开发机路径,交叉编译时gcc会去开发机上找头文件,结果要么找不到要么找错,各种诡异报错。
这时候还要给sysroot配目录结构:
cd ~/qt514-aarch64 mkdir -p sysroot/usr/include sysroot/usr/lib sysroot/usr/bin sysroot/usr/share这些目录在后续configure时候会被当作-sysroot参数指向的根。
3.2 zlib静态编译
为什么先编zlib?因为Qt的很多组件(特别是PNG解压、数据压缩`)底层依赖它,后面每个库编译时几乎都会碰它。zlib是autotools体系,交叉编译参数比较标准。
cd ~/qt514-aarch64/src wget https://zlib.net/zlib-1.2.13.tar.gz tar zxf zlib-1.2.13.tar.gz cd zlib-1.2.13 source ~/qt514-aarch64/cross-env.sh ./configure --prefix=$SYSROOT/usr --static make -j$(nproc) make install这里有个经验:在configure脚本里指定--static很关键,它会让生成的libz.a是完整静态库,而不是带了动态链接依赖的半拉子库。之前我有一次忘了加这个参数,编出来的libz.a后来链接时才发现里面还有对__aarch64_ldadd这种意外的隐式依赖,排查了半天。
如果configure阶段报“Cannot find a working compiler”,先执行echo $CC确认环境变量生效了,再检查aarch64-linux-gnu-gcc能不能直接编译一个hello.c。交叉编译里90%的工具链问题都能用这个简单方法定位。
3.3 libpng与freetype编译
libpng本身依赖zlib,所以要先确认zlib已经安装进sysroot。编译libpng:
cd ~/qt514-aarch64/src wget https://download.sourceforge.net/libpng/libpng-1.6.39.tar.gz tar zxf libpng-1.6.39.tar.gz cd libpng-1.6.39 source ~/qt514-aarch64/cross-env.sh ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr --enable-static --disable-shared make -j$(nproc) make install--host参数告诉configure脚本目标平台是ARM64,这个参数对autotools项目几乎是标配。--disable-shared表示只要静态库,不生成动态库,节省编译时间也让后续链接少些干扰。
接着编译freetype。freetype是Qt字体渲染的底层支撑,如果不编它,Qt界面上的文字会糊得像一坨浆糊,或者干脆一个汉字都显示不出来。
cd ~/qt514-aarch64/src wget https://download.savannah.gnu.org/releases/freetype/freetype-2.10.4.tar.gz tar zxf freetype-2.10.4.tar.gz cd freetype-2.10.4 source ~/qt514-aarch64/cross-env.sh ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --enable-static --disable-shared \ --without-harfbuzz --without-bzip2 make -j$(nproc) make install这里特意加--without-harfbuzz和--without-bzip2,因为这俩是可选项,但一旦configure探测到开发机上有它们,就会自动启用,进而编译时去sysroot找对应的头文件,找不到就报错。对纯Qt界面来说,不带harfbuzz不会对渲染质量有太大影响,老版本Qt在ARM平台上很多都不用这个。
3.4 fontconfig与expat编译
fontconfig是用来管理字体文件和配置的系统库,Qt的字体查找和匹配最终依赖它。但fontconfig又依赖expat(一个XML解析器),所以得先编译expat。
cd ~/qt514-aarch64/src wget https://github.com/libexpat/libexpat/releases/download/R_2_4_8/expat-2.4.8.tar.gz tar zxf expat-2.4.8.tar.gz cd expat-2.4.8 source ~/qt514-aarch64/cross-env.sh ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --enable-static --disable-shared make -j$(nproc) make install然后是fontconfig本体。fontconfig的configure脚本对cross compile支持得不错,但有个前置条件,必须在编译时把--with-arch或pkg-config路径指对,否则它会跑到开发机x86_64的fontconfig里去找依赖库:
cd ~/qt514-aarch64/src wget https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.13.1.tar.gz tar zxf fontconfig-2.13.1.tar.gz cd fontconfig-2.13.1 source ~/qt514-aarch64/cross-env.sh ./configure --host=aarch64-linux-gnu --prefix=$SYSROOT/usr \ --enable-static --disable-shared \ --with-pkg-config=$SYSROOT/usr/lib/pkgconfig \ --with-expat=$SYSROOT/usr make -j$(nproc) make installfontconfig编译过程中会生成一个fc-cache工具,但那是给x86开发机用的版本还是给ARM板子用的版本,configure脚本会自动判断,不用手动操心。装好之后你会在sysroot里得到一个fontconfig库,后续Qt链接时就能找到。
3.5 openssl编译(可选但强烈建议)
如果程序需要用HTTPS访问网络,Qt的network模块里很多TLS相关功能都依赖openssl。我可以负责任地告诉你,不编openssl,QNetworkAccessManager访问HTTPS时qt会直接报TLS initialization failed,但这在交叉编译里太常见了。为了少被坑,我建议还是把openssl静态编进去。
openssl的交叉编译相对特殊一点,它是用Configure + makefile。我实际用的命令是这样的:
cd ~/qt514-aarch64/src wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz tar zxf openssl-1.1.1w.tar.gz cd openssl-1.1.1w source ~/qt514-aarch64/cross-env.sh ./Configure linux-aarch64 \ --prefix=$SYSROOT/usr --openssldir=$SYSROOT/usr/ssl \ no-shared no-tests no-async make -j$(nproc) make install注意linux-aarch64这个target名称,openssl的Configure脚本对不同架构有不同目标名称,ARM64就是这样叫的。不过也不是所有版本都一样,如果遇到了1.1.1w要求的名称不同,执行./Configure LIST看一下输出列表里有没有linux-aarch64,如果没有就搜索linux-arm相关的名字。1.1.1w是支持的。
如果不需要HTTPS,只是想做一个纯本地界面工具,openssl完全可以跳过,能省掉不小的编译量。
3.6 验证sysroot中的静态库
依赖库全部编译进入sysroot之后,务必做一个整体检查。我在实际工作中见过太多“某库头文件没装上但configure没报错”的情况,结果到链接阶段才暴露出来,那时排查起来比编译前发现痛苦十倍。
ls -la $SYSROOT/usr/lib/libz.a $SYSROOT/usr/lib/libpng.a ls -la $SYSROOT/usr/lib/libfreetype.a $SYSROOT/usr/lib/libfontconfig.a ls -la $SYSROOT/usr/lib/libexpat.a $SYSROOT/usr/lib/libssl.a ls -la $SYSROOT/usr/lib/libcrypto.a再检查头文件:
ls $SYSROOT/usr/include/zlib.h $SYSROOT/usr/include/png.h ls $SYSROOT/usr/include/freetype2/ft2build.h ls $SYSROOT/usr/include/fontconfig/fontconfig.h还有一个更靠谱的验证方式,写一个简单的测试程序交叉编译试试链接:
// test.c #include <stdio.h> #include <zlib.h> #include <png.h> int main(int argc, char *argv[]) { printf("zlib version: %s\n", zlibVersion()); (void)sizeof(png_get_libpng_ver(NULL)); return 0; }aarch64-linux-gnu-gcc --sysroot=$SYSROOT test.c -o test \ -I$SYSROOT/usr/include -L$SYSROOT/usr/lib -lpng -lz如果这一步报链接错误,先检查$SYSROOT/usr/lib下有没有对应库的arch匹配信息:
file $SYSROOT/usr/lib/libz.a输出应该包含ARM aarch64字样,如果是x86-64架构,那说明这个库编错了架构,一切得重来。
提示:我建议把
file检查养成习惯,每次编译完一个库,先file确认架构,再进入下一步。因为有些源码包的configure脚本会自动缓存架构信息,在交叉编译环境下可能探测出错,导致编出错误架构的静态库,这种错位问题最难排查。
4. Qt 5.14.2源码编译与配置
4.1 configure参数逐项解读
Qt源码解压之后,重头戏就是configure那一步。这一步的参数直接决定了整个编译是否能通过、编出来的库是否可用。我先把一套经过验证的完整配置放出来,再逐个参数解释。
cd ~/qt514-aarch64/src tar xf qt-everywhere-opensource-src-5.14.2.tar.xz cd qt-everywhere-opensource-src-5.14.2 mkdir -p ~/qt514-aarch64/build/qt514-aarch64 cd ~/qt514-aarch64/build/qt514-aarch64 source ~/qt514-aarch64/cross-env.sh ../../../src/qt-everywhere-opensource-src-5.14.2/configure \ -prefix /usr/local/qt5.14-aarch64 \ -sysroot $SYSROOT \ -extprefix $ROOT_DIR/build/output \ -opensource -confirm-license \ -release -static \ -no-pch -no-rpath -no-icu -no-opengl \ -skip qtwebengine \ -nomake examples -nomake tests \ -qt-zlib -qt-libpng -qt-freetype \ -qt-pcre -no-glib -no-cups -no-xcb \ -linuxfb -eglfs \ -openssl-linked \ -I$SYSROOT/usr/include/freetype2一句话总结这套参数:静态链接、不依赖目标板运行时、只保留嵌入式Linux需要的平台插件、顺便把网络TLS支持做进去。
下面拆细节。
-prefix /usr/local/qt5.14-aarch64:这是Qt最终安装的目标路径。但要注意,由于交叉编译,安装位置不能直接写目标板路径,所以加了-extprefix $ROOT_DIR/build/output,意思是:make install时文件安装到开发机的build/output目录,但Qt内部记录的路径是/usr/local/qt5.14-aarch64。这种“前缀/扩展前缀”分离的机制是Qt交叉编译特有的,得记清楚。
-sysroot $SYSROOT:指定交叉编译用的根文件系统,这样configure在查找第三方依赖库时先找sysroot,而不是开发机系统。
-release -static:release模式,静态编译。-static这个参数是qtbase的交叉编译配置项,它会让Qt把自己编成.a格式的静态库,后续你链接程序时这些静态库会被编进可执行文件。
-no-pch:关闭预编译头。这个参数对交叉编译很重要,因为PCH在交叉编译环境下缓存命中率低,有时候还会因为头文件路径不同而产生隐蔽错误。关了还省不少磁盘。
-no-rpath:不把rpath编译进库里。静态库没有运行时路径概念,关掉纯粹是为了减少干扰。
-no-icu:ICU(International Components for Unicode)是一套很大的国际化库。Qt 5默认开启ICU来进行Unicode和文本布局处理,但ICU交叉编译极其痛苦,而且体积巨大。关掉之后Qt改用自带的QUnicodeTables,常用的中文显示完全不受影响,只是在某些极复杂的文字布局场景(比如阿拉伯语连写、复杂的Indic文字)上会有欠缺。不搞国际化的嵌入式程序,关掉ICU能看到显著收益。
-no-opengl:如果目标板上没有GPU或者不需要硬件加速OpenGL,这个参数能省一大串依赖。但注意,-eglfs插件本身是和OpenGL相关的,这个我下面会说怎么取舍。
-skip qtwebengine:QtWebEngine是基于Chromium的组件,源码体积大到惊人,编译一次能吃掉十几GB临时文件,而且交叉编译时几乎肯定会有编译错误。不搞浏览器类应用,跳过它最理智。
-nomake examples -nomake tests:不生成示例和测试,省编译时间,也避免示例程序在构建时引发额外的链接依赖问题。
-qt-zlib -qt-libpng -qt-freetype:这三个参数让Qt使用自身源码里捆绑的zlib/png/freetype。其实在静态交叉编译时,最省心的做法是让Qt用自己的第三方库源码,而不是去sysroot里找。理由很简单,Qt自带的这些库版本经过验证,跟Qt源码兼容性最好。sysroot里已经编好的zlib、png、freetype是给Qt之外的依赖链用的,比如fontconfig需要freetype,这条链还是得在sysroot里备着。
-qt-pcre:PCRE是正则表达式引擎,Qt自带源码版,默认就是-qt-pcre,显式写上是为了防止它自动检测开发机上的PCRE然后引起混乱。
-no-glib -no-cups -no-xcb:通用Linux桌面环境相关的东西,嵌入式板子用不上。特别是xcb(X11的客户端协议),普通嵌入式Linux板子根本没跑X server,开着只会让configure去检测一堆X11的头文件,然后报错。目标板如果有显示器、想用帧缓冲,平台插件用linuxfb就够。
-linuxfb -eglfs:这两个是平台插件。linuxfb是简单帧缓冲,不需要GPU、不需要X11,直接在Linux帧缓冲设备上画界面,适合绝大多数嵌入式LCD屏。eglfs则需要OpenGL ES支持,如果你的板子有Mali、Vivante等GPU并且你想用硬件渲染,就保留它。但如果目标板没有GPU内核驱动,openGL ES相关的库可能都没有,eglfs插件虽然编出来了,运行时也可能起不来,到时候再按运行时报错去精简。
-openssl-linked:把openssl静态链接进Qt库。由于openssl已经编成静态库放进sysroot,这里直接链接即可。
-I$SYSROOT/usr/include/freetype2:freetype的头文件默认在/usr/include/freetype2子目录下,Qt自己的configure脚本有时候找不到它,手动指一下节省排查时间。
4.2 configure报错与应对
这一节我实际跑configure时踩过的坑,整理成速查表,你照着处理应该会顺利很多。
| 报错特征 | 原因 | 处理办法 |
|---|---|---|
The test for linking against ICU ... failed | 虽然没指定-icu但configure自动探测 | 显式加-no-icu,不要自动探测 |
The test for linking against libxcb ... failed | X11变量没有完全关闭 | 检查是否漏了-no-xcb,另外确保没有在环境变量里残留-I/usr/include |
Cannot find -lGL | opengl检测失败 | 加-no-opengl,或者开发机上安装libgl-dev做检测用 |
GLES2 support cannot be enabled due to missing OpenGL ES 2.0 | eglfs所需环境不满足 | 如果不需要GPU,直接去掉-eglfs,只留-linuxfb |
fatal error: zlib.h: No such file or directory | sysroot中zlib头文件缺失 | 检查zlib是否真的installed进入了sysroot |
collect2: error: ld returned 1 exit status | 配置测试链接阶段静态链接失败 | 看看是不是依赖库顺序写错,或者某个库编译时漏了--static |
configure全部通过之后会生成config.summary文件,里面会列出最终启用的模块和选项。务必花两分钟看一下这个文件,比如确认Qt Sql、Qt Network、Qt Gui是否启用,平台插件里是否有linuxfb。这一步看清楚了,后面编译完才发现功能缺了要重新配置,代价极大。
4.3 make与make install
configure通过之后,编译过程相对无脑,但时间较长。我的建议是不要盲目把所有核心都用上,嵌入式Linux这种交叉编译在慢速云主机上动辄2-3小时,即使有8核机器,各个模块之间还有依赖顺序,线程开太多反而可能内存吃紧导致OOM。我通常使用:
make -j4编译期间控制台会不断输出qtbase、qtdeclarative、qtimageformats、qtmultimedia等模块的构建日志。连接是否进行到Creating qmake...,如果卡在某个模块超过10分钟不动,多半是出错了,往上翻日志找具体报错。
编完之后执行安装:
make install如果-extprefix配得对,所有产物会安装到~/qt514-aarch64/build/output/目录下。验证一下:
ls ~/qt514-aarch64/build/output/应该能看到bin、lib、plugins、qml等子目录。Qt的安装目录已经生成,但它还带了一个很关键的编译器:bin/qmake,这个qmake是x86开发机上运行的ARM64版本Qt构建工具,后续交叉编译你的应用时,就是要用这个qmake来生成Makefile。它的存在说明交叉Qt环境已经搭建完成。
5. 用静态Qt编译你的第一个aarch64程序
5.1 编译环境准备
到了这一步,依赖库有了,Qt库有了,接下来我们就用一个简单的QWidget程序做验证。这个验证程序几乎就是最小可用的嵌入式GUI程序,写起来简单,跑起来能看出很多基础功能是否正常。
先在工程目录新建一个项目文件:
mkdir -p ~/qt514-aarch64/demo/hello cd ~/qt514-aarch64/demo/hello创建一个hello.pro:
QT += core gui widgets TARGET = hello TEMPLATE = app CONFIG += c++11 SOURCES += main.cpp再创建一个main.cpp:
#include <QApplication> #include <QLabel> int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel label("Hello from aarch64 static Qt"); label.resize(320, 240); label.show(); return app.exec(); }编译时需要使用我们刚刚安装好的qmake。在终端里指定它的完整路径:
export PATH=$ROOT_DIR/build/output/bin:$PATH qmake hello.pro make -j4qmake会读取你当前Shell环境的历史信息,结合.pro文件生成Makefile。如果此前环境变量里的CC、CXX、PATH不一致,改用qmake编自己的工程时可能会混乱。最简单的做法是新开一个干净终端,只设置上面这一行PATH,不加交叉编译的其他环境变量,让qmake自己用内置的交叉编译器配置。
编译完成后用file命令检查得到的hello文件:
file hello输出结果应该类似:
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped看到statically linked,这就说明整个程序已经把Qt库、依赖库、平台插件全部链接进去了。这个二进制再使用aarch64-linux-gnu-strip hello收缩体积,可以从几十MB降到十几MB甚至更小,压缩效果明显。别忘了通很久aarch64-linux-gnu-strip而不是x86的strip,用错会导致二进制损坏。
5.2 运行与调试
把hello这个文件拷贝到目标板的任意用户目录,执行:
chmod +x hello ./hello -platform linuxfb如果不给-platform参数,Qt默认按编译时的头号平台插件去找,但静态编译时所有插件都已经链进可执行文件了,所以还是要用-platform显式指定。如果你的板子LCD分辨率是800x600或者其他值,linuxfb会自动读取内核的帧缓冲信息,通常不需要提前设置。
如果屏幕上出现了“Hello from aarch64 static Qt”标签,恭喜,你的交叉编译全链路已经打通。
出现黑屏或者程序直接崩溃,优先排查四件事:
帧缓冲权限:linuxfb插件需要访问
/dev/fb0。用ls -l /dev/fb0查看权限,普通用户没权限时用root或者加入video组。字体缺失:Qt静态编译时如果找不到系统中文字体,界面上会显示方框。先放几个
.ttf字体文件到板子的/usr/share/fonts目录,或者用QFontDatabase在程序里指定字体路径。如果只是英文测试程序,问题不大。环境变量LT_DEBUG:设置
export QT_DEBUG_PLUGINS=1,运行程序时看它打印的平台插件加载日志,能明确看到它选了哪个插件。linuxfb显示分辨率:有些拿到手的板子,帧缓冲设备的分辨率并不等于它物理屏幕的分辨率,比如内部frame buffer是1920x1080但屏幕实际是800x480,显示会溢出。可以用
fbset查看实际分辨率,或者运行时通过环境变量指定:export QT_QPA_FB_DRM=1(如果内核用的是DRM驱动的fb)。
5.3 静态编译中静态插件的手动配置
一个小坑需要特别提示:静态编译时Qt的插件机制和动态编译完全不同。动态编译时,插件是.so文件放在plugins目录下,运行时按需加载。静态编译时,插件被直接编译进可执行文件,只有在链接时显式加入才能生效。QApplication在运行时是无法动态发现静态插件的,所以要么在代码中用Q_IMPORT_PLUGIN显示的导入,要么在链接时通过Qt的qmake配置加入。以linuxfb插件为例,.pro里需要这样写:
QTPLUGIN += qlinuxfb CONFIG += static这样qmake在链接时会把linuxfb插件编进可执行文件。如果以后你需要支持多个平台插件(比如同时支持linuxfb和eglfs),在静态场景下就得把多个插件都编进去,运行时通过-platform选择。
另一个方式是手动在main.cpp里加插件导入宏:
#include <QtPlugin> Q_IMPORT_PLUGIN(QLinuxFbIntegrationPlugin)两种方式二选一即可,我偏向用qmake的QTPLUGIN方式,简单直观,也不容易漏。
6. 常见问题与排查技巧实录
6.1 我实际踩过的坑
坑一:configure阶段误探测开发机的库
Qt的configure脚本会自动检测开发机上的libdbus、libicu、libgl等库,即使你在命令行用了-no-icu,它还是会去检查,有些时候明明写了-no-glib却仍然报告libglib2.0-dev找不到。解决办法是让configure根本探测不到,一种粗暴且有效的方式是只在干净环境中执行configure,不要过早把大小PATH里的开发机库路径加进CFLAGS。
典型报错:The test for linking against libdbus ... failed。此时我一般直接-no-dbus,关掉阀门。嵌入式板子里需要DBus的场景不多,真需要时再单独构建dbus静态库也不迟。
坑二:qmake路径错乱
qmake编译你的工程时,如果你在终端里设置了CROSS_COMPILE、CC这些环境变量,qmake会尝试用你自己定义的CC来编译,一旦工具链版本与你编译Qt时的工具链不完全一致,链接就会出现各种奇奇怪怪的报错。我把这个坑归为“交叉编译环境变量污染”。最稳的做法:编译应用时,新开一个终端,只设置PATH指向Qt安装目录的bin,不设置其他交叉编译环境变量。
坑三:静态链接时库顺序错误
Linux的静态链接是顺序敏感的:链接器从左往右一次处理每个静态库,如果左边的目标文件引用了右边的库,链接没问题;反过来如果右边的库需要左边库里的符号,链接就会报undefined reference。Qt的qmake在生成Makefile时已经处理好了大部分依赖顺序,但如果你自己写的Makefile或者手动ld命令,就可能踩到。解决方式是用--start-group和--end-group包裹所有库:
aarch64-linux-gnu-g++ main.o -Wl,--start-group -lQt5Widgets -lQt5Gui -lQt5Core -Wl,--end-group这样链接器会多次重复搜索这些库,直到所有符号都解析或者无解。
坑四:openssl版本与Qt的兼容
Qt 5.14.2对openssl 1.1.x支持得最好,如果用了openssl 3.0,虽然大部分接口一致,但在某些TLS握手细节上可能调用旧符号失败。我用openssl 1.1.1w就是图稳,推荐你也用这个版本。如果确实要支持3.0,建议至少用Qt 5.15+或者Qt6。
6.2 快速定位问题的三板斧
交叉编译的问题排查比普通开发麻烦,因为很多报错的上下文被工具链和配置脚本层层包裹。我总结了三板斧,90%的问题都能靠它们定位:
单文件独立编译测试:出现问题先写一个不依赖Qt的最小程序,用交叉编译器直接编译,确认工具链和sysroot基础配置没问题。如果这一步都过不了,问题就不在Qt而在工具链。
查看config.summary:configure之后一定仔细看config.summary,它记录了最终启用的模块、插件和特性。功能缺失时,第一反应应该回去看它,而不是猜。
增加构建输出级别:Qt编译时如果想看到更详细的日志,
make VERBOSE=1会打印所有编译和链接命令,这些命令里的路径和参数一目了然。很多“浪费一小时”的问题,其实就是看到链接命令里路径错了才恍然大悟。
6.3 体积优化技巧
静态编译生成的二进制文件体积通常不菲,一个小小的QWidget程序动辄十几MB。因为静态库把Qt Core、Gui、Widgets整块都拉进去了。对存储紧张的嵌入式设备,这个体积可能吃紧,需要再做一点优化。
- strip符号:这条效果最明显。静态编译后先不strip的文件可能30MB+,
aarch64-linux-gnu-strip --strip-unneeded之后能降到10MB左右。 - 裁剪Qt模块:如果你的程序只用Qt Gui和Widgets,不用Qt Network,编译Qt时可以用
-skip qtnetwork关掉网络模块,体积能进一步缩小。不过这个必须在一开始编译Qt时决定,编译完就改不了了。 - 编译器优化:编译Qt时配置为
-release -O2,编译应用时在.pro里加QMAKE_CXXFLAGS_RELEASE += -Os -s,同样能压低体积。 - 用upx压缩:如果目标板有足够的解压CPU时间,upx可以把二进制压到原来30%~40%,运行时自解压。小型Linux系统上我试过,兼容性没问题,但有些安全合规场景不允许自修改文件,自行权衡。
提示:strip之后程序的调试符号就没了,正式交付前别急着strip,先留在开发机上方便后续用gdb调试。调试和发布流程分开,发布去通过CI脚本自动strip,调试时用编译目录里的未strip版本。
6.4 一个完整的部署示例
假设目标板是RK3399或者树莓派CM4这种ARM64设备,运行一个精简busybox系统,Qt程序部署流程大致是:
- 交叉编译得到
hello二进制。 aarch64-linux-gnu-strip --strip-unneeded hello。- 拷贝
hello到目标板/opt/app/。 - 如果程序需要中文显示,把中文字体文件放到
/opt/app/fonts/,程序里加一行:
QFontDatabase::addApplicationFont("/opt/app/fonts/NotoSansCJK-Regular.ttc"); app.setFont(QFont("Noto Sans CJK SC", 12));- 创建一个简单的启动脚本:
#!/bin/sh export QT_QPA_PLATFORM=linuxfb export QTWEBENGINE_DISABLE_SANDBOX=1 # 如果用了qtwebengine才需要 cd /opt/app exec ./hello- 开机自动运行(systemd或init脚本,根据目标板的系统机制决定)。
整个过程不依赖任何动态库安装,不依赖Qt库路径,一个文件加一个字体,就完整跑起来了。
7. 进阶:把交叉编译能力沉淀为可复用流程
一次编译成功只能算运气好,任何项目上都可能出现需要清理重建的情况。我建议你把这套步骤整理成自动化脚本,至少让configure和make部分可以一键重跑。
我在自己工作流里常用的做法是:
- 写一个
build-all.sh,按顺序执行:编译zlib、libpng、freetype、expat、fontconfig、openssl、Qt主构建。每个步骤用一个独立的函数封装,出错时通过set -e直接中断。 - 每个库的源码包打好tar,解压后固定进入对应目录编译。使用git管理的源码目录不适合这种构建方式,因为Qt源码里的子模块重新拉取会引入版本漂移风险。
- 产物统一输出到
output/目录,记录每次构建用到的工具链版本、configure参数、环境变量,便于追溯。
代码方面给出一个简化的流程框架:
#!/bin/bash set -e source cross-env.sh build_zlib() { cd $ROOT_DIR/src/zlib-1.2.13 ./configure --prefix=$SYSROOT/usr --static make -j4 make install } build_qt() { cd $ROOT_DIR/build/qt514-aarch64 $ROOT_DIR/src/qt-everywhere-opensource-src-5.14.2/configure \ -prefix /usr/local/qt5.14-aarch64 \ -sysroot $SYSROOT \ -extprefix $ROOT_DIR/build/output \ -opensource -confirm-license \ -release -static \ -no-pch -no-rpath -no-icu -no-opengl \ -skip qtwebengine \ -nomake examples -nomake tests \ -qt-zlib -qt-libpng -qt-freetype \ -qt-pcre -no-glib -no-cups -no-xcb \ -linuxfb \ -openssl-linked \ -I$SYSROOT/usr/include/freetype2 make -j4 make install } build_zlib # build_png, build_freetype, build_fontconfig, build_openssl ... build_qt这里面每个函数都应该检查是否已经编译过,或者至少支持手动断点恢复。编译一次Qt全流程动辄两三个小时,如果因为某个临时原因中断就得从头再来,谁都受不了。
8. 最后再分享一个小经验
静态编译这事情,难的不是单个步骤,而是每一步之间微妙的依赖关系。工具链、sysroot、依赖库版本、configure参数,任意一个地方没对齐,最后链接就会出现奇怪的符号错误。我见过很多人在这条路上走了一半放弃,然后转回动态编译,让现场调试变成噩梦。
我实际工作中体会最深的一点:交叉编译环境一旦调通,一定要把构建脚本和版本信息用某种方式固化下来。哪怕只是用一个README记录你用了哪个版本的zlib,哪个版本的Qt源码,哪个版本的gcc,都能让半年后再回来维护的人(很可能就是你自己)少走大量弯路。
如果你是从零开始,跟着这套流程完整走一遍,基本上一个周末的时间就能打通。过程中大概率会遇到和我不同的报错,不要慌,多用file、pkg-config --libs --cflags、make VERBOSE=1这几件工具,把问题拆到最小单元逐步排查,多半能自己定位。
如果Qt版本换成5.15.x,大部分配置参数是兼容的,但需要注意openssl版本和gcc版本匹配,因为Qt5.15对新工具链的支持更友好,到时候可以用更新的版本。换成Qt6的话,配置参数差异较大,建议重新查阅相应文档,但这套依赖库+sysroot+configure的整体思路完全一致,具备很好的参考价值。