编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
        • 12.1 案例引入
          • 12.1.1 ARM 黑屏
          • 12.1.2 根因分析
          • 12.1.3 三大问题
          • 12.1.4 ELF 探秘
        • 12.2 交叉编译原理
          • 12.2.1 编译链路
          • 12.2.2 Qt 特殊性
          • 12.2.3 工具链内幕
        • 12.3 构建 Qt
          • 12.3.1 sysroot 准备
          • 12.3.2 设备编译
          • 12.3.3 CMake 集成
        • 12.4 设备部署
          • 12.4.1 最小依赖检查
          • 12.4.2 Qt 插件部署
          • 12.4.3 自包含打包
          • 12.4.4 部署验证
        • 12.5 OTA 远程更新
        • 12.7 新手陷阱
        • 12.8 训练题
        • 12.9 速查表
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

交叉编译与部署

# 第 12 章 交叉编译与部署

本章定位:从 x86 开发机到 ARM 设备点亮屏幕。x86 上编译通过的代码拷贝到 ARM 板上黑屏无反应——90% 是交叉编译链没搭对。本章覆盖 toolchain 搭建、Qt 源码交叉编译、CMake 嵌入式配置、sysroot 构建与最小依赖部署,以及 OTA 热更新——让 QML 应用从"能编译"到"能部署在产线上"。

# 目录介绍

  • 12.1 案例引入
    • 12.1.1 ARM 黑屏
    • 12.1.2 根因分析
    • 12.1.3 三大问题
  • 12.2 交叉编译原理
    • 12.2.1 编译链路
    • 12.2.2 Qt 特殊性
  • 12.3 构建 Qt
    • 12.3.1 sysroot 准备
    • 12.3.2 设备编译
    • 12.3.3 CMake 集成
  • 12.4 设备部署
    • 12.4.1 最小依赖检查
    • 12.4.2 Qt 插件部署
    • 12.4.3 自包含打包
  • 12.5 OTA 远程更新
  • 12.6 树莓派部署
  • 12.7 新手陷阱
  • 12.8 训练题
  • 12.9 速查表

# 12.1 案例引入

# 12.1.1 ARM 黑屏

某车机工程师在 Ubuntu x86 上开发了一个仪表盘 QML 应用——运行完美,FPS 60。拷贝到 ARM Cortex-A53(Raspberry Pi 4)上,满心期待地敲下 ./myapp -platform eglfs,结果是:

-bash: ./myapp: No such file or directory

但 ls -la ./myapp 显示文件确实存在:

-rwxr-xr-x 1 root root 245K Jun 24 10:00 ./myapp

三连排查:

错误信息 根因 修复
No such file or directory(文件存在) ELF 的 interpreter(动态链接器)路径在 ARM 上不存在:/lib/ld-linux-armhf.so.3 在 sysroot 中安装匹配的 glibc
error while loading shared libraries: libQt6Quick.so.6 未部署 Qt .so 库 拷贝 lib/ 目录
QQmlApplicationEngine failed to load component QML plugins 缺失:platforms/libqeglfs.so 未部署 拷贝 plugins/ 目录

# 12.1.2 根因分析

每条错误对应一个部署缺口:

① "No such file or directory"(文件存在):
  x86 ELF: INTERP /lib64/ld-linux-x86-64.so.2
  ARM ELF: INTERP /lib/ld-linux-armhf.so.3
  → ARM 板上没有 /lib/ld-linux-armhf.so.3 → bash 报"文件不存在"

② "libQt6Quick.so.6 not found":
  ldd ./myapp → 列出 50+ 个 .so 依赖
  → 部署时忘了拷贝 Qt 的运行库

③ "QQmlApplicationEngine failed":
  QML 依赖的 QPA 插件不在部署目录
  → 部署时忘了 plugins/platforms/libqeglfs.so

# 12.1.3 三大问题

问题 在哪节回答
怎么在 x86 上编译出 ARM 二进制?sysroot 是什么? §12.2 + §12.3
编译完后拷贝哪些文件到 ARM 板上才能跑? §12.4
已经部署到产线的设备怎么静默升级 QML? §12.5

# 12.1.4 ELF 探秘

"文件存在但 bash 报 No such file or directory"——这句话背后是 ELF 文件格式和内核加载流程。

ELF Header 里的 INTERP 字段

每个动态链接的 ELF 可执行文件里嵌了一个解释器路径,告诉内核"用这个程序来加载我":

# 查看 x86 和 ARM 二进制的 INTERP 差异
readelf -l build-x86/dashboard | grep INTERP
# INTERP    /lib64/ld-linux-x86-64.so.2    ← x86 的动态链接器

aarch64-linux-gnu-readelf -l build-arm/dashboard | grep INTERP
# INTERP    /lib/ld-linux-aarch64.so.1     ← ARM64 的动态链接器

内核加载 ELF 的三步流程:

execve("./myapp") → 内核:
  1. 读 ELF Header → 找到 INTERP: /lib/ld-linux-aarch64.so.1
  2. 尝试打开 /lib/ld-linux-aarch64.so.1  ← ARM 板上这个文件存在吗?
     ├── 存在 → 加载 ld.so → ld.so 再加载 libc, libQt6*.so ...
     └── 不存在 → 返回 ENOENT ("No such file or directory")
                              ↑ 这个错误消息说的是 解释器 不存在,
                              不是说 可执行文件 不存在!

为什么 bash 的报错如此误导?

bash 调用 execve() 失败后,它只知道系统返回了 ENOENT——但 execve 不区分"可执行文件没找到"和"解释器没找到"。两者返回同一个错误码。bash 别无选择,只能说 No such file or directory。

RPATH 与 RUNPATH——内嵌的库搜索路径

ELF 里除了 INTERP,还可以嵌入库搜索路径,让部署不依赖 LD_LIBRARY_PATH:

# 查看当前二进制的 RPATH/RUNPATH
readelf -d build-arm/dashboard | grep -E "RPATH|RUNPATH"

# 编译时设置——让 Qt .so 的搜索路径内嵌在 ELF 里
cmake -B build-arm \
  -DCMAKE_INSTALL_RPATH="/opt/dashboard/lib" \      # 内嵌搜索路径
  -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON

# 效果: 不需要 start.sh 设 LD_LIBRARY_PATH,直接 ./myapp 就能跑
搜索机制 设置方式 优先级 是否绕过
DT_RPATH 编译时 -Wl,-rpath,/opt/app/lib 高 需要用 chrpath 修改
DT_RUNPATH CMake INSTALL_RPATH 中 LD_LIBRARY_PATH 可覆盖
LD_LIBRARY_PATH 运行时环境变量 中高 受 secure-execution 限制
/etc/ld.so.conf 系统配置 低 仅系统目录

# 12.2 交叉编译原理

# 12.2.1 编译链路

x86 开发机 (Host)                          ARM 设备 (Target)
┌─────────────────────────┐               ┌─────────────────────────┐
│ gcc → ELF (x86-64)      │               │ 需要 ARM aarch64 ELF    │
│  × 不能在 ARM 跑         │               │ 从 Host 拷贝过来         │
└─────────────────────────┘               └─────────────────────────┘
            ↓
 arm-linux-gnueabihf-gcc  ← 交叉编译器
 (或 aarch64-linux-gnu-gcc)
            ↓
   生成 ARM ELF → 拷贝到 ARM 板 → ./myapp

交叉编译的三要素:

要素 说明 例子
交叉编译器 运行在 x86、输出 ARM 指令的工具链 arm-linux-gnueabihf-gcc / aarch64-linux-gnu-gcc
sysroot ARM 根文件系统的镜像——含所有头文件和 .so /home/user/raspi-sysroot/
目标设备配置 Qt 的 -device 参数——对应 ARM 板的 mkspecs linux-rasp-pi4-g++ / linux-imx8-g++

sysroot 的本质——它是 ARM 板的完整文件系统副本:

sysroot/
├── usr/
│   ├── include/          ← 交叉编译时 `#include <xxx>` 从这里找
│   │   ├── stdio.h
│   │   ├── GLES2/gl2.h
│   │   └── ...
│   └── lib/
│       ├── arm-linux-gnueabihf/
│       │   ├── libc.so.6       ← 交叉链接时从这里找
│       │   ├── libGLESv2.so
│       │   └── ...
│       └── ...
├── lib/
│   └── ld-linux-armhf.so.3     ← ARM 的动态链接器
└── opt/vc/                      ← Raspberry Pi 专有:GPU 库
    ├── include/
    └── lib/

# 12.2.2 Qt 特殊性

Qt 编译过程除了 gcc/g++,还依赖 Host 端工具——它们运行在 x86 上、但为 ARM 目标生成代码:

工具 运行位置 作用
moc Host(x86) 扫描 Q_OBJECT 宏 → 生成 moc_*.cpp
rcc Host(x86) 编译 .qrc 资源文件 → qrc_*.cpp
qmlcachegen Host(x86) 预编译 .qml → .qmlc 字节码
uic Host(x86) .ui → C++ 代码

结论:编译 Qt 时需要两套工具——Host 工具用 x86 编译器、Target 代码用 ARM 交叉编译器。CMake 的 CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER 正是告诉构建系统"程序在 Host 找,库在 Target sysroot 找"。

# 12.2.3 工具链内幕

把 aarch64-linux-gnu-gcc 的编译过程拆开,可以看到 sysroot 是如何被注入的:

# 交叉编译器实际上是一组具有相同前缀的程序
ls /usr/bin/aarch64-linux-gnu-*
# aarch64-linux-gnu-gcc      ← C 编译器
# aarch64-linux-gnu-g++      ← C++ 编译器
# aarch64-linux-gnu-ld       ← 链接器
# aarch64-linux-gnu-objdump  ← 反汇编
# ...

# 编译一个 .cpp 时的实际步骤
aarch64-linux-gnu-g++ -c main.cpp -o main.o \
  --sysroot=/home/user/raspi-sysroot \        # ← gcc 内部参数——所有路径前加 sysroot
  -I/usr/include                              # → 实际找 /home/user/raspi-sysroot/usr/include

aarch64-linux-gnu-ld main.o -o myapp \
  --sysroot=/home/user/raspi-sysroot \
  -L/usr/lib/aarch64-linux-gnu                 # → /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu
  -L/usr/lib                                  # → /home/user/raspi-sysroot/usr/lib
  -lQt6Core

sysroot 下的多架构路径陷阱

ARM64 Debian/Ubuntu 系统使用 multiarch 路径——库不在 usr/lib/ 下,而是 usr/lib/aarch64-linux-gnu/:

rsync ARM板 → 本地 sysroot 后:
  /home/user/raspi-sysroot/usr/lib/              ← 没有 .so!
  /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu/  ← .so 都在这里

如果编译时报 cannot find -lGLESv2,不是库不存在——是 gcc 没找到 multiarch 子目录。需要显式指定:

./configure -sysroot /home/user/raspi-sysroot \
  -L /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu \  # ← 显式添加
  -I /home/user/raspi-sysroot/usr/include/aarch64-linux-gnu

工具链的三层库搜索优先级:

1. --sysroot 下的路径(交叉编译专属)
   /home/user/raspi-sysroot/usr/lib/aarch64-linux-gnu/
   /home/user/raspi-sysroot/usr/lib/
   /home/user/raspi-sysroot/lib/

2. gcc 内部 multilib 路径(编译时嵌入的工具链默认路径)
   /usr/lib/gcc-cross/aarch64-linux-gnu/12/

3. Host 系统路径(如果 CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 设为 BOTH)
   /usr/lib/x86_64-linux-gnu/  ← 危险:x86 的库链接到 ARM 二进制!

# 12.3 构建 Qt

# 12.3.1 sysroot 准备

方案 A:从 ARM 板 rsync 完整根文件系统:

# 在 x86 开发机上
mkdir -p ~/raspi-sysroot
rsync -avz pi@192.168.1.100:/lib     ~/raspi-sysroot/
rsync -avz pi@192.168.1.100:/usr/lib ~/raspi-sysroot/usr/
rsync -avz pi@192.168.1.100:/usr/include ~/raspi-sysroot/usr/
rsync -avz pi@192.168.1.100:/opt/vc  ~/raspi-sysroot/opt/    # 树莓派专有 GPU 库

# 修正符号链接——rsync 之后可能有绝对路径链接指向 /
# 需要用脚本把它们改为相对路径
wget https://raw.githubusercontent.com/riscv/riscv-poky/master/scripts/sysroot-relativelinks.py
python3 sysroot-relativelinks.py ~/raspi-sysroot

方案 B:用预构建的交叉编译工具链 + sysroot(各厂商提供):

平台 工具链
Raspberry Pi 4 arm-linux-gnueabihf (32-bit) / aarch64-linux-gnu (64-bit)
i.MX 6/8 NXP 官方 Yocto SDK 中的 aarch64-poky-linux-gcc
STM32MP1 ST 官方 OpenSTLinux SDK
通用 ARM Linaro 或 bootlin 预编译工具链

# 12.3.2 设备编译

# ① 下载 Qt 源码
wget https://download.qt.io/official_releases/qt/6.5/6.5.3/single/qt-everywhere-src-6.5.3.tar.xz
tar xf qt-everywhere-src-6.5.3.tar.xz && cd qt-everywhere-src-6.5.3

# ② 配置——指定 device + sysroot + 交叉编译器
./configure \
  -release \
  -opengl es2 \
  -eglfs \
  -device linux-rasp-pi4-g++ \                       # 设备 mkspec
  -device-option CROSS_COMPILE=aarch64-linux-gnu- \   # 交叉编译器前缀
  -sysroot /home/user/raspi-sysroot \                 # ARM 根文件系统
  -prefix /usr/local/qt6-arm \                        # 安装路径(在 ARM 板上)
  -extprefix /home/user/qt6-arm-host \                # Host 上的暂存路径
  -nomake examples -nomake tests \                    # 不编译示例/测试
  -skip qtwebengine \                                 # 嵌入式不需要 WebEngine
  -no-feature-wayland-server

# ③ 编译
make -j$(nproc)     # 约 40 分钟(取决于 CPU)
make install

# ④ 验证——查看生成的文件
file /home/user/qt6-arm-host/lib/libQt6Core.so
# → ELF 64-bit LSB shared object, ARM aarch64, ...

常用设备配置:

设备 -device 参数 编译器前缀
Raspberry Pi 4 (64-bit) linux-rasp-pi4-g++ aarch64-linux-gnu-
Raspberry Pi 3 (32-bit) linux-rasp-pi3-g++ arm-linux-gnueabihf-
i.MX8 linux-imx8-g++ aarch64-poky-linux-
STM32MP1 linux-stm32mp1-g++ arm-ostl-linux-gnueabi-

# 12.3.3 CMake 集成

# toolchain-arm64.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(CMAKE_C_COMPILER   aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)

set(CMAKE_SYSROOT /home/user/raspi-sysroot)

# 程序(moc/rcc)在 Host 找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 库在 Target sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# 头文件在 Target sysroot 找
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(DashboardApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# Qt6 路径——交叉编译时指向 ARM 版的 Qt
set(CMAKE_PREFIX_PATH /home/user/qt6-arm-host)

find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2)

qt_add_executable(dashboard main.cpp)
qt_add_qml_module(dashboard
    URI Dashboard
    VERSION 1.0
    QML_FILES main.qml SpeedGauge.qml RPMGauge.qml
    RESOURCES resources.qrc
)

# 部署 QML 运行时文件
qt_generate_deploy_qml_app_script(
    TARGET dashboard
    OUTPUT_SCRIPT deploy_script
)
install(SCRIPT ${deploy_script})

编译三连:

# x86 开发调试
cmake -B build-x86 -DCMAKE_PREFIX_PATH=/usr/lib/qt6
cmake --build build-x86

# ARM 交叉编译
cmake -B build-arm \
  -DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake \
  -DCMAKE_PREFIX_PATH=/home/user/qt6-arm-host
cmake --build build-arm -j$(nproc)

# 打包部署
cmake --install build-arm --prefix /tmp/dashboard-package

# 12.4 设备部署

# 12.4.1 最小依赖检查

# 在 x86 开发机上用交叉 ldd 检查 ARM 二进制的依赖
aarch64-linux-gnu-readelf -d build-arm/dashboard | grep NEEDED
# → libQt6Quick.so.6, libQt6Qml.so.6, libQt6Core.so.6, ...

# 或用交叉编译工具链的 ldd
aarch64-linux-gnu-ldd build-arm/dashboard

部署文件的三类来源:

来源 文件 部署到
Qt 安装目录(交叉编译产物) lib/libQt6*.so /opt/dashboard/lib/
Qt plugins plugins/platforms/libqeglfs.so /opt/dashboard/plugins/platforms/
ARM sysroot lib/ld-linux-aarch64.so.1、libc.so.6 等系统库 ARM 板已自带,不需部署

# 12.4.2 Qt 插件部署

QML 应用依赖以下插件,任何一个缺失都会导致运行时崩溃:

/opt/myapp/
├── bin/myapp                         ← 可执行文件
├── plugins/
│   ├── platforms/
│   │   └── libqeglfs.so              ← QPA 插件(EGLFS 后端)——必须
│   ├── imageformats/
│   │   ├── libqjpeg.so               ← JPEG 图片解码——如果有 Image
│   │   ├── libqpng.so                ← PNG 图片解码
│   │   └── libqsvg.so                ← SVG 图标——如果有
│   ├── egldeviceintegrations/
│   │   └── libqeglfs-kms-integration.so  ← eglfs KMS 集成
│   └── qmltooling/
│       └── libqmldbg_*.so            ← 调试工具——生产环境可省略
├── qml/
│   ├── QtQuick/                      ← QML 模块
│   ├── QtQuick/Controls/             ← Controls 模块
│   ├── QtQuick/Layouts/              ← Layouts 模块
│   └── QtQuick/Shapes/               ← Shapes 模块(如果用了)
├── lib/
│   ├── libQt6Core.so.6
│   ├── libQt6Gui.so.6
│   ├── libQt6Quick.so.6
│   ├── libQt6Qml.so.6
│   ├── libQt6QuickControls2.so.6
│   └── ...
└── qml/                              ← 应用的 QML 文件 + .qmlc 缓存
    ├── main.qml
    ├── SpeedGauge.qml
    └── ...

Qt 6 的自动部署脚本——qt_generate_deploy_qml_app_script 自动生成部署脚本,拷贝所有需要的 QML 模块:

# 运行 CMake 生成的部署脚本
cmake --install build-arm --prefix /tmp/package

# 或手动执行
cd build-arm
./.qt/deploy_qml_imports.sh /tmp/package/qml

# 12.4.3 自包含打包

方案 A:LD_LIBRARY_PATH + 启动脚本:

#!/bin/sh
# start.sh
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
export LD_LIBRARY_PATH=$SCRIPT_DIR/lib:$LD_LIBRARY_PATH
export QT_PLUGIN_PATH=$SCRIPT_DIR/plugins
export QML_IMPORT_PATH=$SCRIPT_DIR/qml
export QT_QPA_PLATFORM=eglfs
exec $SCRIPT_DIR/bin/myapp "$@"

方案 B:AppImage 自包含——整个应用 + Qt 库打包成一个文件:

# ① 用 linuxdeployqt 打包
linuxdeployqt ./bin/myapp \
  -qmldir=./qml \
  -bundle-non-qt-libs \
  -extra-plugins=platforms/libqeglfs.so

# ② 生成 .AppImage(可选)
appimagetool ./AppDir MyApp.arm64.AppImage

方案 C:静态链接——编译 Qt 时加 -static,应用二进制包含所有 Qt 代码:

./configure -static -release -opengl es2 -eglfs ...
# 编译产物:myapp 一个文件 = 可执行文件 + Qt + QML 引擎
# 缺点:文件大(20-50 MB)、不能热更新 QML 模块

# 12.4.4 部署验证

部署到 ARM 板后、首次启动前,用 五步验证法 避免"盲启动→崩溃→猜原因"的循环:

# Step 1: 验证 INTERP——解释器路径在 ARM 板上存在吗?
ssh root@192.168.1.100 'ls -la /lib/ld-linux-aarch64.so.1'
# → 不存在?→ 说明 glibc 未安装或 sysroot 路径不匹配
# → readelf -l myapp | grep INTERP 确认二进制里写的路径
# → 路径 /lib/ld-linux-aarch64.so.1 vs 实际 /lib/ld-linux-armhf.so.3?

# Step 2: 验证 NEEDED——所有 .so 依赖都在部署目录里吗?
ssh root@192.168.1.100 << 'EOF'
  for lib in $(readelf -d /opt/dashboard/bin/dashboard | grep 'Shared lib' | awk '{print $NF}' | tr -d '[]'); do
    found=$(find /opt/dashboard/lib -name "$lib" 2>/dev/null)
    if [ -z "$found" ]; then
      echo "❌ 缺失: $lib"
    fi
  done
EOF
# → 有输出?→ 部署目录少了某个 .so → 补全

# Step 3: 验证 QPA 插件——EGLFS 后端存在吗?
ssh root@192.168.1.100 'ls /opt/dashboard/plugins/platforms/libqeglfs.so'
# → 不存在?→ 严格按 §12.4.2 的目录树补全 plugins/

# Step 4: 验证 GPU 设备权限——QML 能打开 /dev/dri/card0 吗?
ssh root@192.168.1.100 'ls -la /dev/dri/card0'
# → crw-rw---- 1 root video?→ sudo usermod -aG video myapp-user

# Step 5: 验证 QML 模块——import 路径完整吗?
ssh root@192.168.1.100 'find /opt/dashboard/qml -name "qmldir"'
# → 应该看到 QtQuick/qmldir, QtQuick/Controls/qmldir 等
# → 缺失?→ 重新运行 qt_generate_deploy_qml_app_script

strace 终极法——当以上都通过但仍崩溃时

ssh root@192.168.1.100 << 'EOF'
  strace -f -e trace=openat,open,stat -o /tmp/start.log \
    /opt/dashboard/start.sh
  # 在日志里搜索 ENOENT——找到第一个"文件不存在"的错误
  grep ENOENT /tmp/start.log | head -5
EOF
# → openat(AT_FDCWD, "libQt6Core.so.6", ...) = -1 ENOENT
# → ld.so 在搜索 libQt6Core.so.6 但没找到——库里部署了但路径不对
# → 检查 LD_LIBRARY_PATH 是否设对了

# 12.5 OTA 远程更新

增量更新策略——只更新 QML 文件 + .qmlc 缓存,不动 C++ 二进制:

# 服务端:准备更新包
tar czf update.tar.gz \
  main.qml \
  SpeedGauge.qml \
  RPMGauge.qml \
  *.qmlc

# 客户端:OTA 守护进程
#!/bin/sh
# ota_update.sh
wget -O /tmp/update.tar.gz https://ota.example.com/v1.2.3/update.tar.gz
sha256=$(sha256sum /tmp/update.tar.gz | awk '{print $1}')
expected=$(curl -s https://ota.example.com/v1.2.3/sha256.txt)

if [ "$sha256" != "$expected" ]; then
    echo "校验失败,放弃更新"
    exit 1
fi

# 备份旧版本
cp -r /opt/myapp/qml /opt/myapp/qml.bak

# 解压新版本
tar xzf /tmp/update.tar.gz -C /opt/myapp/qml/

# 重启应用
systemctl restart myapp.service

# 如果应用启动失败——回滚
if ! systemctl is-active --quiet myapp.service; then
    rm -rf /opt/myapp/qml
    mv /opt/myapp/qml.bak /opt/myapp/qml
    systemctl restart myapp.service
fi

完整 OTA 更新架构:

云端 OTA Server
  ├── /v1.2.3/
  │   ├── update.tar.gz        ← QML + .qmlc 更新包
  │   ├── sha256.txt            ← 校验和
  │   └── changelog.txt
  └── /v1.2.2/ (上一版本——用于回滚)

车载 ARM 设备
  ├── ota_client.service        ← systemd 定时任务
  │   └── 每小时检查一次 OTA 服务器
  ├── ota_update.sh             ← 下载 → 校验 → 更新 → 回滚
  └── myapp.service             ← QML 应用

OTA 安全三件套:

安全措施 实现
签名验证 gpg --verify update.tar.gz.sig update.tar.gz
加密传输 HTTPS + mTLS 双向认证
完整性校验 sha256sum 逐包校验
原子替换 mv 是原子操作——不出现"一半旧一半新"的状态
故障回滚 双槽位 A/B 分区更新——一个分区失败立刻切到另一个

A/B 分区原子切换——零停机更新

shell 脚本的 mv 回滚有一个致命缺陷:如果应用在启动过程中崩了,systemd 会无限重启它——不是"回滚到旧版",而是"反复崩溃"。A/B 分区从根本上解决:

设备 eMMC 分区表:
  /dev/mmcblk0p1  →  boot (bootloader)
  /dev/mmcblk0p2  →  slot_a (/opt/dashboard_a)  ← 当前运行版本 v1.2.2
  /dev/mmcblk0p3  →  slot_b (/opt/dashboard_b)  ← 空闲槽位
  /dev/mmcblk0p4  →  data  (用户数据——不随 OTA 改变)

OTA 更新流程(双槽位):
  1. 下载 v1.2.3 → 解压到 /opt/dashboard_b (不在当前运行的槽位)
  2. 校验完整性(sha256 + 签名)
  3. 原子切换:
     # bootloader 环境变量
     fw_setenv boot_slot b              ← 写 U-Boot 环境变量
     fw_setenv boot_attempts 3          ← 最多尝试 3 次启动
     sync && reboot
  4. 设备重启 → U-Boot 读 boot_slot → 挂载 slot_b
  5. 启动 v1.2.3:
     ├── 启动成功 → systemd 通知 bootloader → boot_attempts = 0 (确认成功)
     └── 启动失败(3次内)→ bootloader 自动切回 slot_a → 启动旧版 v1.2.2

A/B 切换的 systemd 集成:

# /etc/systemd/system/dashboard.service
[Unit]
Description=Dashboard HMI
After=multi-user.target
OnFailure=dashboard-fallback.service  ← 崩溃时触发回滚

[Service]
ExecStart=/opt/dashboard/start.sh
Restart=on-failure
RestartSec=2s

# 健康检查——启动 30 秒后验证
ExecStartPost=/usr/bin/health-check.sh
# health-check.sh:
#   sleep 30
#   if ! pgrep dashboard; then
#     fw_setenv boot_slot a && reboot   # 切回旧槽位
#   fi
方案 回滚速度 回滚可靠性 实现复杂度
mv 回滚 < 1s(应用重启) ⚠️ 应用崩在 systemd 循环里无效 低
A/B 分区 ~5s(reboot) ✅ bootloader 级保证 中(需分区 + U-Boot)

完整操作脚本——从交叉编译到 ARM 板点亮屏幕:

#!/bin/bash
# deploy-to-pi4.sh — Raspberry Pi 4 一键部署

set -e
PI_IP=192.168.1.100
PI_USER=pi

echo "=== Step 1: 交叉编译 ==="
cmake -B build-arm \
  -DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake \
  -DCMAKE_PREFIX_PATH=/home/user/qt6-arm-host
cmake --build build-arm -j4

echo "=== Step 2: 安装到临时目录 ==="
cmake --install build-arm --prefix /tmp/dashboard-pkg

echo "=== Step 3: 拷贝 Qt 运行库 ==="
cp -r /home/user/qt6-arm-host/lib/libQt6*.so* /tmp/dashboard-pkg/lib/
cp -r /home/user/qt6-arm-host/plugins /tmp/dashboard-pkg/
cp -r /home/user/qt6-arm-host/qml /tmp/dashboard-pkg/

echo "=== Step 4: 创建启动脚本 ==="
cat > /tmp/dashboard-pkg/start.sh << 'EOF'
#!/bin/sh
DIR=$(dirname "$(readlink -f "$0")")
export LD_LIBRARY_PATH=$DIR/lib
export QT_PLUGIN_PATH=$DIR/plugins
export QML_IMPORT_PATH=$DIR/qml
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_HIDECURSOR=1
exec $DIR/bin/dashboard
EOF
chmod +x /tmp/dashboard-pkg/start.sh

echo "=== Step 5: 上传到 ARM 板 ==="
rsync -avz /tmp/dashboard-pkg/ $PI_USER@$PI_IP:/opt/dashboard/

echo "=== Step 6: 在 ARM 板上启动 ==="
ssh $PI_USER@$PI_IP "sudo systemctl stop dashboard; sleep 1"
ssh $PI_USER@$PI_IP "/opt/dashboard/start.sh &"

echo "✅ 部署完成——检查仪表盘是否点亮屏幕"

跨章知识串连——这个部署脚本看似简单,背后串起了全书关键点:

脚本步骤 背后原理 参考章节
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm64.cmake 交叉编译 toolchain 的 sysroot + 编译器前缀 §12.2 + §12.3
plugins/libqeglfs.so 部署 EGLFS 后端——ARM 无桌面环境的唯一渲染路径 §13.3 EGLFS
QT_QPA_PLATFORM=eglfs QPA 平台抽象层——跳过 X11/Wayland,直连 DRM/KMS §13.2.2 QPA层
QT_QPA_EGLFS_HIDECURSOR=1 嵌入式默认无鼠标——隐藏光标 §13.3.3 输入设备
LD_LIBRARY_PATH 动态链接器搜索路径——内嵌 RPATH 更优但需编译时设 §12.1.4 ELF探秘
rsync 同步部署 sysroot 的来源就是 ARM 板的 rsync 镜像 §12.3.1 sysroot

部署失败的"五分钟排障法"

当 6 步脚本全部顺利执行但屏幕仍然黑屏——按这个顺序逐个排查,不跳步骤:

Step 1: 确认二进制是 ARM ELF
  file /opt/dashboard/bin/dashboard
  → 必须是 "ELF 64-bit LSB executable, ARM aarch64" 而不是 "x86-64"
  → 如果看到 x86-64 → toolchain 文件没生效,CMake 用了 Host 编译器

Step 2: 确认 EGLFS 可以初始化 GPU
  QT_LOGGING_RULES="qt.qpa.eglfs*=true" ./start.sh
  → 日志里有 "EGL display opened" → GPU OK
  → 日志里有 "Could not open egl display" → GPU 驱动问题 → §13.6.3

Step 3: 确认 QML engine 可以加载主文件
  QML_IMPORT_TRACE=1 ./start.sh
  → 看到 "loaded main.qml" → QML OK
  → 看到 "import QtQuick not found" → qml/ 目录不完整 → §12.4.2

Step 4: strace 跟踪文件打开
  strace -f -e openat ./start.sh 2>&1 | grep -E "ENOENT|qml|so"
  → 搜索第一个 ENOENT——就是缺失的文件
  → 补全 → 重新验证

# 12.7 新手陷阱

# 陷阱 说明 修复
1 用 x86 gcc 编译的二进制直接拷到 ARM ELF 格式不兼容——No such file or directory 必须用交叉编译器
2 sysroot 没做 relativelinks 链接指向 /lib/...(ARM 板绝对路径)→ Host 上找不到 sysroot-relativelinks.py 脚本修正
3 部署时忘记 plugins/platforms/libqeglfs.so QML 加载时找不到 QPA 插件→启动崩溃 部署脚本中显式列出 plugins 依赖
4 qml/ 目录下的 QML 模块没拷贝 QML import 路径缺失→运行时模块加载失败 用 qt_generate_deploy_qml_app_script 自动生成
5 OTA 更新没有备份+回滚 新 QML 有 bug → 设备变砖 双槽位更新 + systemd 健康检查

# 12.8 训练题

写一个 Raspberry Pi 4 的 toolchain 文件

要求:aarch64 架构,sysroot 在 /opt/raspi-sysroot,编译器前缀 aarch64-linux-gnu-。

参考答案
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER   aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/raspi-sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

# 12.9 速查表

概念 一句话
交叉编译器 运行在 x86、输出 ARM 指令:aarch64-linux-gnu-gcc
sysroot ARM 根文件系统镜像——含所有头文件和 .so
-device Qt configure 的设备参数——linux-rasp-pi4-g++
CMAKE_TOOLCHAIN_FILE CMake 交叉编译配置——指定编译器 + sysroot
CMAKE_FIND_ROOT_PATH_MODE 程序/库/头文件的查找范围控制
ldd / readelf -d ARM 二进制依赖检查
qt_generate_deploy_qml_app_script Qt 6 自动生成部署脚本——拷贝 QML 模块
rsync 文件同步——sysroot 构建 + 部署上传
LD_LIBRARY_PATH 运行时库搜索路径——嵌入式必备
OTA 远程静默升级——QML 热更新 + A/B 槽位回滚

核心哲学:

交叉编译 = 正确的 toolchain + 完整的 sysroot + 匹配的 device mkspec。
部署 = ldd 检查 + plugins 全部拷贝 + 启动脚本设 LD_LIBRARY_PATH。
缺任何一环 = ARM 板上的"No such file or directory"。

上次更新: 2026/07/12, 19:39:51
自定义SceneGraph节点
嵌入式渲染后端

← 自定义SceneGraph节点 嵌入式渲染后端→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式