CMake 工具链与交叉编译
交叉编译(Cross-compilation)是指在一个平台上编译生成另一个平台可执行程序的过程。本章将介绍如何使用 CMake 工具链文件实现交叉编译。
什么是交叉编译?
应用场景
交叉编译在以下场景中非常重要:
- 嵌入式开发:在 PC 上编译 ARM、MIPS 等嵌入式平台的程序
- 移动开发:编译 Android 或 iOS 应用
- 跨平台发布:在 Linux 上编译 Windows 程序
- 性能优化:在强大的开发机上为资源受限的目标平台编译
交叉编译的挑战
交叉编译需要解决以下问题:
- 指定目标平台的编译器
- 设置目标平台的系统根目录(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()
小结
本章我们学习了:
- 交叉编译基础:概念和应用场景
- 工具链文件:基本结构和使用方法
- 常见场景:ARM、Android、Windows、iOS、WebAssembly
- 关键变量:系统、编译器、查找路径配置
- 高级配置:多架构支持、依赖处理
- 测试和验证:QEMU 模拟、目标设备测试
交叉编译是嵌入式开发和跨平台发布的关键技术,掌握工具链文件的使用能让你轻松应对各种目标平台。
练习
- 创建一个 ARM Linux 交叉编译工具链文件
- 使用 MinGW 从 Linux 编译一个 Windows 程序
- 配置 Android NDK 交叉编译环境
- 使用 QEMU 测试交叉编译的程序