导语
在前面的章节中,我们构建的项目都是在宿主机(Host)上编译、在宿主机上运行的。然而,现代 C++ 项目的交付场景早已突破了单一桌面平台的界限:你需要为 ARM 嵌入式设备编译网关程序,为 Android 手机编译 Native 库,为树莓派编译服务端,甚至为 WebAssembly 编译能在浏览器中运行的模块。这些场景的共同点是:编译代码的机器 ≠ 运行代码的机器。
这种“在 A 平台生成 B 平台可执行文件”的过程,就是交叉编译(Cross-compilation)。CMake 对交叉编译提供了完善的支持,而掌握它的核心就在于理解工具链文件(Toolchain File)的配置。本节将从零开始,带你建立交叉编译的基本认知,并编写出规范、可复用的工具链文件。
什么是交叉编译?宿主机与目标机的区别
在 CMake 的语境中,涉及交叉编译时会出现三个关键角色,务必先厘清:
- 宿主机(Build / Host):执行编译过程的平台。例如你正在使用的 x86_64 Linux 工作站或 Windows PC。
- 目标机(Target):最终运行生成的可执行文件或库的平台。例如一台 ARM Cortex-A72 的嵌入式板子。
- 构建平台(Build):在 GNU Autotools 传统中,Build 指执行编译的机器,Host 指运行编译产物的机器。CMake 为了简化,通常将编译执行机称为 Host,运行机称为 Target System。
当你运行 gcc main.cpp 时,编译器默认生成的是与当前系统架构一致的二进制文件,这属于本地编译(Native Compilation)。而交叉编译时,你必须显式告诉 CMake:“请使用 arm-linux-gnueabihf-gcc 这个编译器,并且不要把宿主机的系统路径当成查找库和头文件的依据。”
工具链文件:交叉编译的“导航图”
为什么需要工具链文件?
在 Modern CMake 中,交叉编译的推荐做法不是手动修改 CMakeLists.txt 里的变量,而是提供一个独立的工具链文件(Toolchain File),通常以 .cmake 为后缀。它本质上是一个 CMake 脚本,通过 -DCMAKE_TOOLCHAIN_FILE 在配置阶段传入,负责预设所有与目标平台相关的变量。
这种方式的好处显而易见:
- 零侵入:项目的 CMakeLists.txt 不需要为了交叉编译而写一堆
if分支。 - 可复用:同一个工具链文件可以被多个项目共享。
- 可维护:编译器路径、系统根目录等环境差异被隔离在一个文件中。
工具链文件的完整结构
一个规范的交叉编译工具链文件通常包含五个部分:系统信息声明 → 编译器与工具指定 → 系统根目录设置 → 查找路径控制 → 额外标志位。下面是一个面向 ARM Linux(armv7 + hard float)的完整示例:
# cmake/arm-linux-gnueabihf.cmake
# ==========================================
# 1. 系统信息(必须首先设置,影响后续决策)
# ==========================================
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_SYSTEM_VERSION 1) # 通常用于嵌入式裸机或特定内核版本标记
# ==========================================
# 2. 编译器与工具链路径
# ==========================================
# 建议通过环境变量或绝对路径指定,避免依赖 PATH 顺序
set(TOOLCHAIN_PREFIX "/opt/gcc-arm-10.3-2021.07-x86_64-arm-linux-gnueabihf/bin/arm-linux-gnueabihf")
set(CMAKE_C_COMPILER "${TOOLCHAIN_PREFIX}-gcc" CACHE FILEPATH "C compiler")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_PREFIX}-g++" CACHE FILEPATH "C++ compiler")
set(CMAKE_AR "${TOOLCHAIN_PREFIX}-ar" CACHE FILEPATH "Archiver")
set(CMAKE_RANLIB "${TOOLCHAIN_PREFIX}-ranlib" CACHE FILEPATH "Ranlib")
set(CMAKE_LINKER "${TOOLCHAIN_PREFIX}-ld" CACHE FILEPATH "Linker")
set(CMAKE_STRIP "${TOOLCHAIN_PREFIX}-strip" CACHE FILEPATH "Strip")
set(CMAKE_NM "${TOOLCHAIN_PREFIX}-nm" CACHE FILEPATH "Symbol list tool")
set(CMAKE_OBJCOPY "${TOOLCHAIN_PREFIX}-objcopy" CACHE FILEPATH "Objcopy tool")
set(CMAKE_OBJDUMP "${TOOLCHAIN_PREFIX}-objdump" CACHE FILEPATH "Objdump tool")
# ==========================================
# 3. 系统根目录(sysroot)设置
# ==========================================
set(CMAKE_SYSROOT "/opt/arm-sysroot")
# ==========================================
# 4. 查找路径控制(至关重要)
# ==========================================
set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 编译工具(如 protoc)使用宿主机版本
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # 库必须在 sysroot 里找
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # 头文件必须在 sysroot 里找
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) # find_package 只在目标环境查找
# ==========================================
# 5. 编译标志与特性(可选)
# ==========================================
set(CMAKE_C_FLAGS_INIT "-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS_INIT "-march=armv7-a -mfpu=neon-vfpv4 -mfloat-abi=hard")
请注意,上述变量大多使用了 CACHE 强制写入,这是为了确保它们不会被项目内部的 set() 意外覆盖。
系统信息的强制设置
工具链文件中最先被处理的应当是系统信息变量。它们不仅用于生成正确的平台判断逻辑,还会直接影响 CMake 内置模块的行为(例如 FindThreads 或 TestBigEndian)。
CMAKE_SYSTEM_NAME
该变量指定目标系统的操作系统名称。常见取值包括:
Linux:目标为 Linux 系统(含嵌入式 Linux)。Windows:目标为 Windows 系统(配合 MinGW-w64 交叉编译时常用)。Darwin:目标为 macOS/iOS(较少用于交叉编译,多为 Xcode 工具链内部使用)。Android:目标为 Android 平台(通常配合 NDK 使用)。Generic:无操作系统(常用于裸机 / Bare-metal 嵌入式开发)。
一旦设置了 CMAKE_SYSTEM_NAME,CMake 会自动认为当前处于交叉编译模式,此时 CMAKE_CROSSCOMPILING 变量会被置为 TRUE。你可以利用这一点在 CMakeLists.txt 中做条件判断:
if(CMAKE_CROSSCOMPILING)
message(STATUS "当前处于交叉编译模式,目标系统:${CMAKE_SYSTEM_NAME}")
endif()
CMAKE_SYSTEM_PROCESSOR
该变量声明目标 CPU 架构,例如 arm、aarch64、x86_64、i686、wasm32 等。它会影响某些库的条件编译逻辑(如 OpenSSL 或 zlib 会根据架构选择不同的汇编实现),但 CMake 本身并不会验证这个字符串的合法性,因此务必与你使用的编译器前缀保持一致。
CMAKE_SYSTEM_VERSION
对于 Linux 或 Android,这通常对应目标系统的内核版本或 API Level。例如 Android NDK 工具链中常设置为 21、28 等,以表示最低支持的 Android API 等级。
编译器、链接器与归档器的指定
交叉编译的核心是告诉 CMake 使用非默认的工具链。除了最常用的 CMAKE_C_COMPILER 和 CMAKE_CXX_COMPILER 外,你还应当完整配置配套的二进制工具,否则在链接静态库、剥离符号表或生成静态分析报表时可能会遇到工具不匹配的错误。
CMake 中与工具链直接相关的关键变量包括:
CMAKE_C_COMPILER/CMAKE_CXX_COMPILER:C 和 C++ 编译器。CMAKE_AR:归档器,用于生成静态库(.a文件)。- <codeCMAKE_RANLIB:为静态库生成索引,提升链接速度。
CMAKE_LINKER:链接器,虽然大多数场景下 CMake 会调用编译器前端驱动链接,但显式指定有助于某些特殊场景。CMAKE_STRIP:用于剥离调试符号,减小 Release 产物体积。CMAKE_NM:查看符号表。CMAKE_OBJCOPY/CMAKE_OBJDUMP:二进制格式转换与反汇编。
为了让工具链文件更加健壮,推荐通过环境变量动态拼接,而不是硬编码绝对路径:
# 从环境变量读取前缀,提高可移植性
if(NOT DEFINED ENV{CROSS_COMPILE})
message(FATAL_ERROR
"请设置环境变量 CROSS_COMPILE,例如:n"
"export CROSS_COMPILE=arm-linux-gnueabihf-")
endif()
set(CROSS_COMPILE $ENV{CROSS_COMPILE})
set(CMAKE_C_COMPILER "${CROSS_COMPILE}gcc" CACHE FILEPATH "" FORCE)
set(CMAKE_CXX_COMPILER "${CROSS_COMPILE}g++" CACHE FILEPATH "" FORCE)
set(CMAKE_AR "${CROSS_COMPILE}ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "${CROSS_COMPILE}ranlib" CACHE FILEPATH "" FORCE)
查找路径与根目录控制
交叉编译最容易出错的地方,不是编译器找不到了,而是 CMake 错误地链接了宿主机的库。试想一下:如果你在 x86_64 Linux 上编译 ARM 程序,却链接了 /usr/lib/x86_64-linux-gnu/libz.so,最终链接阶段必然报错。
CMAKE_SYSROOT 与 CMAKE_FIND_ROOT_PATH
CMAKE_SYSROOT 相当于给编译器传递了 --sysroot= 参数,它告诉 GCC/Clang:“当你查找系统头文件和标准库时,请把这个目录当成根目录。”而 CMAKE_FIND_ROOT_PATH 则是告诉 CMake 的 find_xxx 命令应该去哪里搜索依赖。
两者通常指向同一个目录,即目标平台的“系统镜像”或“staging 目录”,其中包含了目标架构的 /usr/include、/usr/lib、/lib 等目录结构。
CMAKE_FIND_ROOT_PATH_MODE_* 三种模式
CMake 提供了四个变量来控制交叉编译时的查找行为,每个变量都可设为 NEVER、ONLY 或 BOTH:
- NEVER:只搜索宿主机系统路径(不进入
CMAKE_FIND_ROOT_PATH)。 - ONLY:只搜索
CMAKE_FIND_ROOT_PATH指定的路径。 - BOTH:两者都搜索。
实际工程中,推荐的典型配置如下:
# 程序/可执行工具:通常保留宿主机版本(如代码生成器 protobuf、python 脚本)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 库、头文件、CMake 包:严格限制在目标 sysroot 内,避免宿主污染
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
如果你有一些依赖是通过 find_package 引入的,并且这些依赖也需要交叉编译后的版本,那么务必将它们预先安装到 CMAKE_SYSROOT 对应的目录中,或者使用 CMAKE_PREFIX_PATH 指向交叉编译后的依赖安装目录。
使用工具链文件构建项目
编写好工具链文件后,使用它非常简单。你不需要修改项目内的任何 CMakeLists.txt,只需在配置阶段通过命令行传入即可:
# 1. 创建构建目录
mkdir build-arm && cd build-arm
# 2. 配置:传入工具链文件
cmake ..
-DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-gnueabihf.cmake
-DCMAKE_BUILD_TYPE=Release
# 3. 构建
cmake --build . --parallel $(nproc)
# 4. 验证产物架构(使用交叉工具链的 readelf 或宿主的 file 命令)
file ./my_app
# 输出应包含:ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)...
如果你使用 CMake Presets(我们将在第 12 章详细介绍),还可以将工具链文件路径固化到 CMakePresets.json 中,实现一键切换本地编译与交叉编译。
调试与常见问题
初次编写工具链文件时,可能会遇到以下典型问题:
- 找不到编译器:检查
CMAKE_C_COMPILER是否为绝对路径,或是否已正确添加到PATH环境变量。 - 链接阶段报架构不匹配:几乎总是因为
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY设为BOTH或NEVER,导致链接了宿主机的 x86_64 库。应设为ONLY并确保 sysroot 内有对应库。 - 找不到标准头文件:确认
CMAKE_SYSROOT下是否存在usr/include/stdio.h等标准头文件;如果是裸机开发,可能需要显式指定--specs和裸机 newlib 路径。 - find_package 找不到包:交叉编译时,CMake 不会自动搜索
/usr/lib/cmake。你需要将目标平台预编译好的XXXConfig.cmake安装到 sysroot 的lib/cmake/XXX目录,或设置CMAKE_PREFIX_PATH。
调试时,可以打开 CMake 的详细输出,观察它到底使用了哪些路径和标志:
cmake -B build-arm
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-linux-gnueabihf.cmake
-DCMAKE_VERBOSE_MAKEFILE=ON
-S .
cmake --build build-arm --verbose
小结
交叉编译是现代 C++ 工程走向多平台交付的必经之路,而 CMake 通过工具链文件机制,将平台差异优雅地隔离在项目之外。本节我们掌握了以下核心知识:
- 明确了宿主机(Host)与目标机(Target)的区别;
- 理解了工具链文件的五大组成部分及其书写规范;
- 学会了使用
CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR声明目标平台; - 完整配置了编译器、链接器、归档器等工具链变量;
- 通过
CMAKE_FIND_ROOT_PATH及其模式变量,杜绝了宿主机库污染目标产物的风险。
在下一节中,我们将把这些理论应用到具体平台上,实战演练 Android NDK、iOS、嵌入式 Linux 以及 Windows-to-Linux 等常见交叉编译场景。


没有回复内容