29. 8.1 交叉编译基础

引言:当施工队要去外地盖楼

在前面的章节里,我们的 CMake “施工队长”一直驻扎在本地,给各支小队(Target)分配任务、管理材料(源文件)、监督工艺(编译选项),最后把大楼完完整整地在本地建了起来。但现实世界里的建筑工程,往往不会只在总部附近开工——有时候,你需要在遥远的另一座城市,甚至另一个国家,按照当地的规范和气候条件盖一栋楼。

在 C++ 开发中,这种”异地施工”就是交叉编译(Cross Compilation)。你在自己的 x86_64 Windows 或 Linux 笔记本上写代码、跑 CMake,但最终生成的可执行文件却要运行在 ARM 开发板、嵌入式 Linux 设备,甚至是 iOS 手机上。这时候,施工队长不能只会说本地方言了,他得带上”翻译”和”异地施工许可证”——也就是我们今天要讲的工具链文件(Toolchain File)

一、交叉编译:到底在”交叉”什么?

1.1 宿主机与目标机的区别

要理解交叉编译,首先得分清两个角色:

  • 宿主机(Host):就是你当下正在使用的电脑。它运行着操作系统、CMake、IDE,以及交叉编译工具链本身。施工队长坐镇的地方。
  • 目标机(Target):最终运行你程序的设备。它可能有完全不同的处理器架构(比如从 x86 到 ARM)、不同的操作系统(比如从 Windows 到嵌入式 Linux),甚至根本没有编译环境。

所谓”交叉”,就是指编译行为发生在一台机器上,而生成的二进制文件运行在另一台机器上。如果两者是同一台机器(或同架构同系统),那就是大家最熟悉的本地编译(Native Compilation)

1.2 什么时候需要交叉编译?

交叉编译在以下场景几乎是唯一的选择:

  • 嵌入式开发:ARM、RISC-V 开发板的资源有限,无法在上面直接编译大型项目。
  • 移动平台:iOS 和 Android 应用必须在 PC 上编译,再部署到手机。
  • 异构服务器:在 x86 服务器上为 ARM 云服务器构建发布包。
  • 历史兼容:为老旧或特殊的硬件平台维护软件,而手头没有对应的开发机。

二、工具链文件:异地施工的”施工许可证”

在本地编译时,CMake 很”懒”——它会自动去系统 PATH 里找 gcc、g++,用默认的系统头文件和库路径。但一旦进入交叉编译场景,CMake 就”蒙圈”了:它不知道该用哪个编译器,不知道目标系统的头文件和库在哪里,甚至不知道目标系统叫什么名字。

这时候,我们就需要一份工具链文件(Toolchain File),通常是一个以 .cmake 结尾的脚本。它就像施工队的异地施工许可证,明确告诉 CMake:

  1. 我们要去哪个城市施工?(目标系统与架构)
  2. 当地的施工设备和标准是什么?(编译器、链接器路径)
  3. 当地的建材市场在哪里?(头文件、库的搜索路径)

2.1 工具链文件的规范

工具链文件本质上是一个普通的 CMake 脚本,但它有一个重要的使用约定:

  • 文件中不应包含 project()add_executable() 等项目构建指令——它只负责配置工具链环境。
  • 通常通过命令行参数 -DCMAKE_TOOLCHAIN_FILE=路径 指定给 CMake。
  • 工具链文件会在 CMake 处理项目 CMakeLists.txt最早阶段被执行,甚至早于 project() 命令。

2.2 完整结构一览

一个规范的交叉编译工具链文件通常包含以下五个部分:

  1. 头部说明:注释说明该工具链的目标平台。
  2. 系统信息声明:设置 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_PROCESSOR 等。
  3. 编译器与工具指定:设置 C/C++ 编译器、链接器、归档器等完整路径。
  4. 查找路径与根目录配置:设置 CMAKE_SYSROOTCMAKE_FIND_ROOT_PATH 及查找模式。
  5. 额外编译标志(可选):如处理器型号微调标志 -mcpu=cortex-a53 等。

三、系统信息的强制设置

3.1 CMAKE_SYSTEM_NAME:触发交叉编译模式的关键开关

在工具链文件中,设置 CMAKE_SYSTEM_NAME 是告诉 CMake”我们要交叉编译”的最关键一步。只要这个变量被显式设置,CMake 就会认为自己处于交叉编译模式,并自动设置内部变量 CMAKE_CROSSCOMPILINGTRUE

常见的取值包括:

  • Linux:目标系统是 Linux(包括嵌入式 Linux)。
  • Windows:目标系统是 Windows。
  • Darwin:目标系统是 macOS/iOS。
  • Android:目标系统是 Android(也可通过 NDK 工具链简化)。
  • Generic:用于裸机(Bare-metal)嵌入式开发,没有操作系统。
# 告诉CMake:我们要为Linux系统交叉编译
set(CMAKE_SYSTEM_NAME Linux)

3.2 CMAKE_SYSTEM_PROCESSOR:目标处理器架构

这个变量告诉 CMake 目标 CPU 的架构名称。CMake 本身不会验证这个字符串的合法性,但它是许多生成表达式和第三方库判断架构的重要依据。常见取值:

  • arm:32 位 ARM(如 armv7)。
  • aarch64:64 位 ARM(ARMv8)。
  • x86_64:64 位 x86。
  • i686:32 位 x86。
  • riscv64:64 位 RISC-V。
set(CMAKE_SYSTEM_PROCESSOR arm)

3.3 CMAKE_SYSTEM_VERSION(可选)

如果你需要针对目标系统的特定版本进行适配(例如 Windows 版本或 Android API Level),可以设置这个变量。对于普通嵌入式 Linux,通常可以省略。

set(CMAKE_SYSTEM_VERSION 1)

四、编译器、链接器与归档器指定

交叉编译工具链通常以特定前缀命名,例如 arm-linux-gnueabihf-gcc。你必须显式告诉 CMake 这些工具的完整路径,否则它会继续寻找系统的默认编译器。

4.1 核心编译器变量

  • CMAKE_C_COMPILER:C 语言编译器。
  • CMAKE_CXX_COMPILER:C++ 编译器。

强烈建议使用绝对路径,避免因为环境变量 PATH 配置不当而找到错误的编译器。

set(CMAKE_C_COMPILER   /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-g++)

4.2 辅助工具链变量

一个完整的交叉编译环境,除了编译器,还需要以下”配角”:

  • CMAKE_AR:归档器,用于生成静态库(.a 文件)。对应 ar 工具。
  • CMAKE_RANLIB:为静态库生成索引,加速链接。对应 ranlib 工具。
  • CMAKE_LINKER:链接器。虽然现代 CMake 通常通过编译器驱动链接,但仍可显式指定 ld
  • CMAKE_STRIP:用于剥离二进制文件中的调试符号,减小发布包体积。
  • CMAKE_OBJCOPY:用于格式转换(如生成二进制烧录文件)。
  • CMAKE_NM:用于查看符号表。
set(CMAKE_AR       /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-ar)
set(CMAKE_RANLIB   /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-ranlib)
set(CMAKE_LINKER   /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-ld)
set(CMAKE_STRIP    /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-strip)
set(CMAKE_OBJCOPY  /opt/cross-pi-gcc/bin/arm-linux-gnueabihf-objcopy)

4.3 重要提醒:时机与缓存

这些变量必须在工具链文件中、project() 命令之前设置。因为 project() 命令会触发编译器检测,如果此时还没有指定交叉编译器,CMake 就会错误地使用系统默认编译器,并将检测结果缓存起来,导致后续即使修改了工具链文件也无效——必须清空构建目录重新配置。

五、查找路径与根目录设置

交叉编译最容易踩的坑之一就是:CMake 在宿主机上找到了头文件和库,但这些文件是给 x86_64 用的,不是给 ARM 用的。链接时就会报各种”格式不兼容”错误。

5.1 CMAKE_SYSROOT 与 CMAKE_FIND_ROOT_PATH

这两个变量是控制”建材市场位置”的核心:

  • CMAKE_SYSROOT:编译器的 --sysroot 参数。它告诉编译器:目标系统的头文件和库都在这个目录下,编译时请把这个目录当作根目录。这直接影响编译器查找系统头文件和动态链接器的行为。
  • CMAKE_FIND_ROOT_PATH:告诉 CMake 的 find_xxx() 命令(如 find_libraryfind_path)应该去哪里搜索。通常它和 CMAKE_SYSROOT 设为同一个路径,但也可以额外添加其他路径。
set(CMAKE_SYSROOT /opt/cross-pi-gcc/arm-linux-gnueabihf/sysroot)
set(CMAKE_FIND_ROOT_PATH /opt/cross-pi-gcc/arm-linux-gnueabihf/sysroot)

5.2 查找模式:三个重要的 MODE 变量

仅仅指定根路径还不够,你还得告诉 CMake:不同类型的东西,应该在哪些范围内查找。这是通过以下三个变量控制的:

  • CMAKE_FIND_ROOT_PATH_MODE_PROGRAM:如何查找可执行程序
  • CMAKE_FIND_ROOT_PATH_MODE_LIBRARY:如何查找库文件
  • CMAKE_FIND_ROOT_PATH_MODE_INCLUDE:如何查找头文件
  • CMAKE_FIND_ROOT_PATH_MODE_PACKAGE:如何查找包配置find_package)。

每个变量都可以取以下三个值之一:

  • NEVER:只在宿主机常规路径下查找,使用 CMAKE_FIND_ROOT_PATH
  • ONLY:只在 CMAKE_FIND_ROOT_PATH 指定的路径下查找。
  • BOTH:先在根路径下查找,再找宿主机路径。

5.3 为什么程序查找通常设为 NEVER?

这是一个初学者非常容易困惑的点。在交叉编译中:

  • 库和头文件是为目标机准备的,必须来自 sysroot,所以要设为 ONLY
  • 可执行程序(如 CMake 自身在配置阶段调用的工具)通常需要在宿主机上运行,比如代码生成器、 protoc、python 脚本等。如果设为 ONLY,CMake 可能会在 ARM 的 sysroot 里找到一个无法执行的 ARM 版程序。

因此,标准的交叉编译工具链文件配置通常是:

# 可执行程序在宿主机找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# 库、头文件、包配置只在目标根路径下找
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

六、一个完整的工具链文件实战

让我们把上面的知识整合起来,写一份为树莓派(Raspberry Pi)ARM 32 位 Linux 交叉编译的工具链文件:

# file: rpi-toolchain.cmake
# 目标平台:Raspberry Pi (ARM 32-bit, Linux)

# ---------------------------------------------------------
# 1. 系统信息声明
# ---------------------------------------------------------
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# ---------------------------------------------------------
# 2. 编译器与工具链
# ---------------------------------------------------------
set(TOOLCHAIN_PREFIX /opt/cross-pi-gcc/bin/arm-linux-gnueabihf)

set(CMAKE_C_COMPILER   ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)

set(CMAKE_AR      ${TOOLCHAIN_PREFIX}-ar)
set(CMAKE_RANLIB  ${TOOLCHAIN_PREFIX}-ranlib)
set(CMAKE_LINKER  ${TOOLCHAIN_PREFIX}-ld)
set(CMAKE_STRIP   ${TOOLCHAIN_PREFIX}-strip)
set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}-objcopy)

# ---------------------------------------------------------
# 3. 根目录与查找路径
# ---------------------------------------------------------
set(CMAKE_SYSROOT /opt/cross-pi-gcc/arm-linux-gnueabihf/sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_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)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

# ---------------------------------------------------------
# 4. 额外编译标志(可选)
# ---------------------------------------------------------
# 针对树莓派3的ARM Cortex-A53优化
set(CMAKE_C_FLAGS   "${CMAKE_C_FLAGS}   -march=armv8-a -mfpu=neon-fp-armv8" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv8-a -mfpu=neon-fp-armv8" CACHE STRING "" FORCE)

七、使用工具链文件构建项目

写好工具链文件后,使用方式非常简单。在配置阶段通过命令行传入即可:

# 进入项目目录
cd my-project

# 配置:指定工具链文件
cmake -B build-arm 
      -DCMAKE_TOOLCHAIN_FILE=../cmake/rpi-toolchain.cmake 
      -DCMAKE_BUILD_TYPE=Release

# 构建
cmake --build build-arm

注意:一旦构建目录通过某个工具链文件配置过,就不能直接更换工具链文件重新配置(因为编译器检测结果已被缓存)。如果需要切换工具链,必须清空或重建构建目录:

rm -rf build-arm
# 然后重新执行上面的 cmake -B ... 命令

八、验证交叉编译是否生效

构建完成后,你可以通过 file 命令(Linux/macOS)检查生成文件的目标架构:

$ file build-arm/myapp

# 期望输出类似:
# build-arm/myapp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), ...

如果显示的是 x86-64i386,说明交叉编译没有生效,CMake 仍然使用了宿主机的默认编译器。

小结

交叉编译是 C++ 开发中不可避免的高阶场景,而 CMake 通过工具链文件为我们提供了清晰、规范的解决方案。本节的核心要点可以总结为:

  • 交叉编译就是在宿主机上生成目标机可执行文件,必须分清 Host 与 Target。
  • 工具链文件是交叉编译的”施工许可证”,通过 -DCMAKE_TOOLCHAIN_FILE 指定。
  • CMAKE_SYSTEM_NAME 是触发交叉编译模式的开关,必须设置。
  • 编译器、链接器、归档器等工具必须通过绝对路径显式指定,且要在 project() 之前完成。
  • CMAKE_SYSROOTCMAKE_FIND_ROOT_PATH 控制目标系统资源的查找范围,配合 MODE_PROGRAM=NEVERMODE_LIBRARY=ONLY 等设置,避免宿主机构件”污染”交叉编译。

掌握了这些基础,下一节我们就可以带着 CMake 这位”施工队长”,去挑战 Android NDK、嵌入式 Linux、iOS 等真实平台的交叉编译实战了。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……