跳到主要内容

CMake 工具链与交叉编译

交叉编译(Cross-compilation)是指在一个平台上编译生成另一个平台可执行程序的过程。本章将介绍如何使用 CMake 工具链文件实现交叉编译。

什么是交叉编译?

应用场景

交叉编译在以下场景中非常重要:

  1. 嵌入式开发:在 PC 上编译 ARM、MIPS 等嵌入式平台的程序
  2. 移动开发:编译 Android 或 iOS 应用
  3. 跨平台发布:在 Linux 上编译 Windows 程序
  4. 性能优化:在强大的开发机上为资源受限的目标平台编译

交叉编译的挑战

交叉编译需要解决以下问题:

  • 指定目标平台的编译器
  • 设置目标平台的系统根目录(sysroot)
  • 配置目标平台的头文件和库路径
  • 处理平台特定的编译选项

工具链文件

什么是工具链文件?

工具链文件(Toolchain File)是一个 CMake 脚本文件,用于设置交叉编译所需的变量。当 CMake 配置项目时,通过 CMAKE_TOOLCHAIN_FILE 变量指定工具链文件。

基本结构

一个简单的工具链文件包含以下内容:

# 目标系统名称
set(CMAKE_SYSTEM_NAME Linux)

# 目标处理器架构
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定编译器
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# 指定 sysroot(可选)
set(CMAKE_SYSROOT /path/to/sysroot)

# 搜索路径设置
set(CMAKE_FIND_ROOT_PATH /path/to/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)

使用工具链文件

命令行指定

# 使用工具链文件配置项目
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake

# 构建项目
cmake --build build

使用预设

CMakePresets.json 中指定工具链文件:

{
"version": 6,
"configurePresets": [
{
"name": "arm-linux",
"displayName": "ARM Linux",
"toolchainFile": "${sourceDir}/toolchains/arm-linux.cmake"
}
]
}

然后使用预设:

cmake --preset arm-linux

常见交叉编译场景

ARM Linux 交叉编译

toolchains/arm-linux.cmake

# 目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 编译器(使用交叉编译工具链)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# Sysroot 路径(包含目标系统的头文件和库)
set(CMAKE_SYSROOT /opt/arm-sysroot)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})

# 搜索模式
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # 程序在主机上找
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) # 包在 sysroot 中找

ARM64 (AArch64) Linux 交叉编译

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/aarch64-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)

Android 交叉编译

toolchains/android.cmake

set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_SYSTEM_VERSION 21) # Android API 级别
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a) # CPU 架构
set(CMAKE_ANDROID_NDK /path/to/android-ndk)

# 编译器由 NDK 自动选择
set(CMAKE_C_COMPILER ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang)
set(CMAKE_CXX_COMPILER ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang++)

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)

更简单的方式:使用 NDK 自带的工具链文件

cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21

Windows 交叉编译(从 Linux)

使用 MinGW-w64 交叉编译 Windows 程序:

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

# MinGW-w64 编译器
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) # Windows 资源编译器

# Windows 目标
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)

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)

iOS 交叉编译

set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_SYSTEM_PROCESSOR arm64)

# iOS SDK 路径
execute_process(
COMMAND xcrun --sdk iphoneos --show-sdk-path
OUTPUT_VARIABLE IOS_SDK_PATH
OUTPUT_STRIP_TRAILING_WHITESPACE
)

set(CMAKE_SYSROOT ${IOS_SDK_PATH})

# 编译器
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)

# iOS 特定标志
set(CMAKE_C_FLAGS "-arch arm64 -miphoneos-version-min=14.0 -fobjc-arc")
set(CMAKE_CXX_FLAGS "-arch arm64 -miphoneos-version-min=14.0 -fobjc-arc")

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)

WebAssembly (Emscripten)

set(CMAKE_SYSTEM_NAME Emscripten)

# Emscripten 编译器
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)

# 输出格式
set(CMAKE_EXECUTABLE_SUFFIX ".js")

# 额外标志
set(CMAKE_C_FLAGS "-O2 -s WASM=1")
set(CMAKE_CXX_FLAGS "-O2 -s WASM=1")

更推荐的方式:使用 Emscripten 的工具链文件

# 先激活 Emscripten 环境
source /path/to/emsdk/emsdk_env.sh

# 使用 emcmake 包装器
emcmake cmake -B build
emmake make -C build

关键变量详解

系统相关变量

变量说明常见值
CMAKE_SYSTEM_NAME目标操作系统Linux, Windows, Darwin, Android, iOS
CMAKE_SYSTEM_VERSION目标系统版本Android API 级别等
CMAKE_SYSTEM_PROCESSOR目标处理器架构x86_64, arm, aarch64
CMAKE_SYSROOT系统根目录包含目标系统的 /usr 等

编译器相关变量

# C 编译器
set(CMAKE_C_COMPILER /path/to/gcc)
set(CMAKE_C_COMPILER_TARGET arm-linux-gnueabihf) # Clang 目标三元组

# C++ 编译器
set(CMAKE_CXX_COMPILER /path/to/g++)
set(CMAKE_CXX_COMPILER_TARGET arm-linux-gnueabihf)

# 其他工具
set(CMAKE_AR arm-linux-gnueabihf-ar)
set(CMAKE_LINKER arm-linux-gnueabihf-ld)
set(CMAKE_NM arm-linux-gnueabihf-nm)
set(CMAKE_OBJCOPY arm-linux-gnueabihf-objcopy)
set(CMAKE_OBJDUMP arm-linux-gnueabihf-objdump)
set(CMAKE_RANLIB arm-linux-gnueabihf-ranlib)
set(CMAKE_STRIP arm-linux-gnueabihf-strip)

查找路径模式

# PROGRAM:查找可执行程序
# NEVER:只在主机系统中查找
# ONLY:只在目标系统中查找
# BOTH:在两个系统中查找

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) # CMake 包在目标系统中查找

高级配置

多架构支持

为多个目标架构创建不同的工具链文件:

toolchains/
├── arm-linux.cmake
├── aarch64-linux.cmake
├── x86_64-linux.cmake
└── x86_64-windows.cmake

使用预设管理:

{
"version": 6,
"configurePresets": [
{
"name": "base",
"hidden": true,
"binaryDir": "${sourceDir}/build/${presetName}"
},
{
"name": "arm-linux",
"inherits": "base",
"toolchainFile": "${sourceDir}/toolchains/arm-linux.cmake"
},
{
"name": "aarch64-linux",
"inherits": "base",
"toolchainFile": "${sourceDir}/toolchains/aarch64-linux.cmake"
}
]
}

自动检测 Sysroot

# 尝试自动查找 sysroot
find_path(SYSROOT_PATH
NAMES include/stdio.h
PATHS
/opt/arm-sysroot
/usr/arm-linux-gnueabihf
/opt/arm-linux-gnueabihf
)

if(SYSROOT_PATH)
set(CMAKE_SYSROOT ${SYSROOT_PATH})
set(CMAKE_FIND_ROOT_PATH ${SYSROOT_PATH})
else()
message(WARNING "Sysroot not found, cross-compilation may fail")
endif()

条件性配置

根据目标平台设置不同的编译选项:

# 在工具链文件中设置变量
set(TARGET_EMBEDDED ON)

# 在 CMakeLists.txt 中使用
if(TARGET_EMBEDDED)
# 嵌入式平台特定配置
target_compile_definitions(myapp PRIVATE EMBEDDED_PLATFORM)
target_compile_options(myapp PRIVATE -Os) # 优化大小
endif()

处理依赖库

交叉编译时需要为目标平台编译依赖库:

# 指定预编译库的位置
set(DEPENDENCIES_DIR /opt/arm-libs)

target_include_directories(myapp PRIVATE
${DEPENDENCIES_DIR}/include
)

target_link_directories(myapp PRIVATE
${DEPENDENCIES_DIR}/lib
)

使用 FetchContent 进行交叉编译

使用 FetchContent 下载依赖时,需要确保依赖也为目标平台编译:

include(FetchContent)

# 设置依赖的编译选项
set(FETCHCONTENT_QUIET OFF)

FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
)

FetchContent_MakeAvailable(fmt)

# 链接时确保使用正确的配置
target_link_libraries(myapp PRIVATE fmt::fmt)

测试交叉编译结果

在目标平台上运行

# 复制到目标设备
scp build/myapp user@target:/tmp/

# SSH 到目标设备运行
ssh user@target "/tmp/myapp"

使用 QEMU 模拟

# 安装 QEMU 用户模式模拟器
sudo apt install qemu-user

# 运行 ARM 程序
qemu-arm ./build/myapp

# 运行 AArch64 程序
qemu-aarch64 ./build/myapp

使用 QEMU 进行测试

# 在工具链文件中设置
set(CMAKE_CROSSCOMPILING_EMULATOR "qemu-arm")

# 添加测试
add_test(NAME MyTest COMMAND myapp)

# 测试将通过 QEMU 运行
ctest

常见问题

问题 1:找不到头文件

确保 sysroot 包含正确的头文件:

# 检查 sysroot
ls $CMAKE_SYSROOT/usr/include/stdio.h

# 添加额外的包含路径
target_include_directories(myapp PRIVATE
${CMAKE_SYSROOT}/usr/include
)

问题 2:链接错误

确保库为目标平台编译:

# 检查库的架构
file /path/to/libmylib.a
# 输出应为 ARM 或目标架构

问题 3:find_package 找到错误的库

设置正确的搜索模式:

set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

问题 4:编译器标志不正确

在工具链文件中设置标志:

set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-a53")
set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-a53")

最佳实践

1. 保持工具链文件独立

将工具链文件放在单独的目录:

project/
├── CMakeLists.txt
├── toolchains/
│ ├── arm-linux.cmake
│ └── aarch64-linux.cmake
└── src/

2. 使用版本控制

将工具链文件和 sysroot 位置文档化:

# 工具链文件:ARM Linux 交叉编译
# 目标平台:ARM Cortex-A53, Linux
# 依赖:arm-linux-gnueabihf-gcc >= 9.0
# Sysroot:Ubuntu 20.04 ARM rootfs

3. 验证交叉编译

# 在 CMakeLists.txt 中添加检查
if(CMAKE_CROSSCOMPILING)
message(STATUS "Cross-compiling for ${CMAKE_SYSTEM_NAME} (${CMAKE_SYSTEM_PROCESSOR})")
message(STATUS "Sysroot: ${CMAKE_SYSROOT}")
endif()

4. 处理平台差异

# 在代码中处理平台差异
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
target_compile_definitions(myapp PRIVATE ARM_PLATFORM)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
target_compile_definitions(myapp PRIVATE ARM64_PLATFORM)
endif()

小结

本章我们学习了:

  1. 交叉编译基础:概念和应用场景
  2. 工具链文件:基本结构和使用方法
  3. 常见场景:ARM、Android、Windows、iOS、WebAssembly
  4. 关键变量:系统、编译器、查找路径配置
  5. 高级配置:多架构支持、依赖处理
  6. 测试和验证:QEMU 模拟、目标设备测试

交叉编译是嵌入式开发和跨平台发布的关键技术,掌握工具链文件的使用能让你轻松应对各种目标平台。

练习

  1. 创建一个 ARM Linux 交叉编译工具链文件
  2. 使用 MinGW 从 Linux 编译一个 Windows 程序
  3. 配置 Android NDK 交叉编译环境
  4. 使用 QEMU 测试交叉编译的程序

参考资源