尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

aarch64静态交叉编译Qt 5.14.2全流程:从工具链到嵌入式Linux部署

aarch64静态交叉编译Qt 5.14.2全流程:从工具链到嵌入式Linux部署 1. 项目概述与背景分析1.1 为什么要做aarch64静态交叉编译做嵌入式Linux开发的人迟早会碰上这么一件事。手里拿到一块ARM64的开发板或者甲方给了指定规格的工控机然后告诉你在上面跑一个带界面的程序UI还得过得去但系统里没有包管理器或者出厂文件系统特别精简甚至连libstdc版本都对不上。这时候你才意识到动态编译那套思路在嵌入式场景下经常是灾难。动态编译下Qt程序运行起来依赖一堆solibQt5Widgets.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交叉gccmake4.2系统自带即可CMake3.16构建部分依赖项时需要Perl5.30编译Qt必须Python3.8Qt构建脚本依赖辅助工具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等所有模块静态编译时需要用到的模块它都包全了。依赖库我列一下库名版本用途zlib1.2.13压缩支持libpng1.6.39PNG图片格式freetype2.10.4字体渲染fontconfig2.13.1字体配置与管理libffi3.4.2动态类型系统部分功能依赖expat2.4.8XML解析fontconfig依赖openssl1.1.1wSSL/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_COMPILEaarch64-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。编译libpngcd ~/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 --hostaarch64-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 --hostaarch64-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 --hostaarch64-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 --hostaarch64-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。我可以负责任地告诉你不编opensslQNetworkAccessManager访问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 -staticrelease模式静态编译。-static这个参数是qtbase的交叉编译配置项它会让Qt把自己编成.a格式的静态库后续你链接程序时这些静态库会被编进可执行文件。-no-pch关闭预编译头。这个参数对交叉编译很重要因为PCH在交叉编译环境下缓存命中率低有时候还会因为头文件路径不同而产生隐蔽错误。关了还省不少磁盘。-no-rpath不把rpath编译进库里。静态库没有运行时路径概念关掉纯粹是为了减少干扰。-no-icuICUInternational Components for Unicode是一套很大的国际化库。Qt 5默认开启ICU来进行Unicode和文本布局处理但ICU交叉编译极其痛苦而且体积巨大。关掉之后Qt改用自带的QUnicodeTables常用的中文显示完全不受影响只是在某些极复杂的文字布局场景比如阿拉伯语连写、复杂的Indic文字上会有欠缺。不搞国际化的嵌入式程序关掉ICU能看到显著收益。-no-opengl如果目标板上没有GPU或者不需要硬件加速OpenGL这个参数能省一大串依赖。但注意-eglfs插件本身是和OpenGL相关的这个我下面会说怎么取舍。-skip qtwebengineQtWebEngine是基于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-pcrePCRE是正则表达式引擎Qt自带源码版默认就是-qt-pcre显式写上是为了防止它自动检测开发机上的PCRE然后引起混乱。-no-glib -no-cups -no-xcb通用Linux桌面环境相关的东西嵌入式板子用不上。特别是xcbX11的客户端协议普通嵌入式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/freetype2freetype的头文件默认在/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 ... failedX11变量没有完全关闭检查是否漏了-no-xcb另外确保没有在环境变量里残留-I/usr/includeCannot find -lGLopengl检测失败加-no-opengl或者开发机上安装libgl-dev做检测用GLES2 support cannot be enabled due to missing OpenGL ES 2.0eglfs所需环境不满足如果不需要GPU直接去掉-eglfs只留-linuxfbfatal error: zlib.h: No such file or directorysysroot中zlib头文件缺失检查zlib是否真的installed进入了sysrootcollect2: error: ld returned 1 exit status配置测试链接阶段静态链接失败看看是不是依赖库顺序写错或者某个库编译时漏了--staticconfigure全部通过之后会生成config.summary文件里面会列出最终启用的模块和选项。务必花两分钟看一下这个文件比如确认Qt Sql、Qt Network、Qt Gui是否启用平台插件里是否有linuxfb。这一步看清楚了后面编译完才发现功能缺了要重新配置代价极大。4.3 make与make installconfigure通过之后编译过程相对无脑但时间较长。我的建议是不要盲目把所有核心都用上嵌入式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.proQT core gui widgets TARGET hello TEMPLATE app CONFIG c11 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_PLUGINS1运行程序时看它打印的平台插件加载日志能明确看到它选了哪个插件。linuxfb显示分辨率有些拿到手的板子帧缓冲设备的分辨率并不等于它物理屏幕的分辨率比如内部frame buffer是1920x1080但屏幕实际是800x480显示会溢出。可以用fbset查看实际分辨率或者运行时通过环境变量指定export QT_QPA_FB_DRM1如果内核用的是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.summaryconfigure之后一定仔细看config.summary它记录了最终启用的模块、插件和特性。功能缺失时第一反应应该回去看它而不是猜。增加构建输出级别Qt编译时如果想看到更详细的日志make VERBOSE1会打印所有编译和链接命令这些命令里的路径和参数一目了然。很多“浪费一小时”的问题其实就是看到链接命令里路径错了才恍然大悟。6.3 体积优化技巧静态编译生成的二进制文件体积通常不菲一个小小的QWidget程序动辄十几MB。因为静态库把Qt Core、Gui、Widgets整块都拉进去了。对存储紧张的嵌入式设备这个体积可能吃紧需要再做一点优化。strip符号这条效果最明显。静态编译后先不strip的文件可能30MBaarch64-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_PLATFORMlinuxfb export QTWEBENGINE_DISABLE_SANDBOX1 # 如果用了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 VERBOSE1这几件工具把问题拆到最小单元逐步排查多半能自己定位。如果Qt版本换成5.15.x大部分配置参数是兼容的但需要注意openssl版本和gcc版本匹配因为Qt5.15对新工具链的支持更友好到时候可以用更新的版本。换成Qt6的话配置参数差异较大建议重新查阅相应文档但这套依赖库sysrootconfigure的整体思路完全一致具备很好的参考价值。
返回列表