20、CMake进阶:交叉编译工具链文件

说到交叉编译,我估计不少朋友都在这上面栽过跟头。明明本地编译好好的,一放到开发板上就各种报错。嗯,说白了就是编译器用错了。今天我们就来聊聊CMake里最核心的一个东西——工具链文件(toolchain.cmake)。

为什么需要工具链文件?

你想想看,我们在PC上开发嵌入式程序,PC的CPU是x86_64,而目标板可能是ARM Cortex-M4或者RISC-V。这两个架构的指令集完全不同。本地编译器生成的是x86的机器码,放到ARM上当然跑不了。

所以我们需要告诉CMake三件事:

  • 用什么编译器(arm-none-eabi-gcc还是riscv64-unknown-elf-gcc)
  • 目标系统是什么(Linux?FreeRTOS?裸机?)
  • 目标架构是什么(ARMv7-M?Cortex-A53?)

这些信息,就写在toolchain.cmake里。

核心变量:CMAKE_SYSTEM_NAME 和 CMAKE_SYSTEM_PROCESSOR 是工具链文件的灵魂。前者告诉CMake目标操作系统,后者告诉CMake目标CPU架构。

一个典型的工具链文件长什么样?

我在项目中写过不下20个工具链文件,从STM32到全志V3s再到树莓派。结构其实大同小异。来看一个ARM Cortex-M4裸机开发的例子:

# 文件名: arm-none-eabi-gcc.cmake

# 目标系统:裸机(没有操作系统)
set(CMAKE_SYSTEM_NAME Generic)

# 目标处理器架构
set(CMAKE_SYSTEM_PROCESSOR cortex-m4)

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

# 指定编译器路径(可选,如果不在PATH里)
set(TOOLCHAIN_PATH /opt/gcc-arm-none-eabi-10.3-2021.10/bin)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/arm-none-eabi-g++)

# 指定链接器
set(CMAKE_LINKER ${TOOLCHAIN_PATH}/arm-none-eabi-ld)

# 指定汇编器
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PATH}/arm-none-eabi-gcc)

# 指定归档工具
set(CMAKE_AR ${TOOLCHAIN_PATH}/arm-none-eabi-ar)

# 指定目标文件工具
set(CMAKE_OBJCOPY ${TOOLCHAIN_PATH}/arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP ${TOOLCHAIN_PATH}/arm-none-eabi-objdump)

# 设置编译器标志
set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Os -ffunction-sections -fdata-sections" CACHE STRING "C flags")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "C++ flags")

# 设置链接器标志
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections -Wl,-Map=output.map" CACHE STRING "Linker flags")

# 告诉CMake不要尝试链接测试程序(交叉编译时经常失败)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

我的经验:最后一行CMAKE_TRY_COMPILE_TARGET_TYPE特别重要。CMake在配置阶段会尝试编译一个小程序来检测编译器是否可用。但在交叉编译环境下,这个测试程序链接时会因为没有系统库而失败。设置成STATIC_LIBRARY后,CMake只编译不链接,就能顺利通过了。

CMAKE_SYSTEM_NAME 的常见取值

这个变量决定了CMake使用哪套系统库和链接规则。我整理了一张表:

取值 适用场景 示例
Generic 裸机、RTOS(FreeRTOS、RT-Thread) STM32、ESP32裸机
Linux 嵌入式Linux系统 树莓派、全志H3、i.MX6
Windows Windows桌面/嵌入式 Windows IoT
Android Android NDK开发 手机、平板
Darwin macOS/iOS Apple设备

我个人习惯:只要目标板没有完整的Linux系统,一律用Generic。哪怕你跑的是FreeRTOS,也算Generic。因为CMake的Linux模式会默认链接glibc,这在裸机环境下根本不存在。

CMAKE_SYSTEM_PROCESSOR 的配置技巧

这个变量告诉CMake目标CPU的架构。常见取值:

  • cortex-m0cortex-m3cortex-m4cortex-m7:ARM Cortex-M系列
  • cortex-a53cortex-a72:ARM Cortex-A系列
  • armv7-aarmv8-a:ARM架构版本
  • riscv32riscv64:RISC-V架构
  • i686x86_64:x86架构

注意:CMAKE_SYSTEM_PROCESSOR的值会影响CMake的一些内置变量,比如CMAKE_SIZEOF_VOID_P(指针大小)。如果你设错了,可能会导致内存布局计算错误。我曾经在移植一个项目时,把cortex-m4写成了cortex-m3,结果指针大小被误判为4字节(实际是4字节没错),但浮点ABI的检测出了问题,折腾了我一整天。

如何让CMake使用工具链文件?

有两种方式:

方式一:命令行指定

cmake -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi-gcc.cmake ..

方式二:在CMakeLists.txt中设置(不推荐)

set(CMAKE_TOOLCHAIN_FILE arm-none-eabi-gcc.cmake)

为什么我不推荐方式二?因为工具链文件必须在第一次运行cmake命令时就生效。如果你在CMakeLists.txt里设置,CMake已经完成了部分检测,这时候再加载工具链文件,很多变量已经缓存了,改起来很麻烦。

一个更完整的例子:嵌入式Linux交叉编译

假设我们要为全志V3s(ARM Cortex-A7)编译程序,运行Buildroot生成的工具链:

# 文件名: arm-buildroot-linux-gnueabihf.cmake

# 目标系统是Linux
set(CMAKE_SYSTEM_NAME Linux)

# 目标处理器
set(CMAKE_SYSTEM_PROCESSOR armv7-a)

# 工具链前缀
set(TOOLCHAIN_PREFIX arm-buildroot-linux-gnueabihf)

# 编译器路径
set(TOOLCHAIN_PATH /opt/buildroot-2023.02/output/host/bin)

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

# 查找工具链中的其他工具
find_program(CMAKE_AR ${TOOLCHAIN_PREFIX}-ar PATHS ${TOOLCHAIN_PATH} NO_DEFAULT_PATH)
find_program(CMAKE_RANLIB ${TOOLCHAIN_PREFIX}-ranlib PATHS ${TOOLCHAIN_PATH} NO_DEFAULT_PATH)
find_program(CMAKE_STRIP ${TOOLCHAIN_PREFIX}-strip PATHS ${TOOLCHAIN_PATH} NO_DEFAULT_PATH)

# 设置sysroot(非常重要!)
set(CMAKE_SYSROOT ${TOOLCHAIN_PATH}/../arm-buildroot-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)

关键点:CMAKE_SYSROOT告诉CMake去哪里找目标系统的头文件和库。CMAKE_FIND_ROOT_PATH_MODE_*变量控制CMake在查找程序、库、头文件时的搜索策略。对于交叉编译,库和头文件应该只在sysroot里找,而程序(比如cmake自带的工具)应该在本地找。

工具链文件的核心逻辑

说了这么多,我画了一张图帮你理清思路:

工具链文件核心逻辑 toolchain.cmake CMAKE_SYSTEM_NAME CMAKE_SYSTEM_PROCESSOR 编译器/链接器路径 Generic Linux 其他系统 cortex-m4 armv7-a riscv64 CMAKE_C_COMPILER CMAKE_CXX_COMPILER CMAKE_LINKER 三个核心配置共同决定了CMake如何生成目标平台的构建系统

避坑指南

我这些年踩过的坑,都列在这里了:

  • 编译器路径带空格:Windows上尤其常见。比如"C:\Program Files\...\gcc.exe"。一定要用双引号包起来,或者用短路径名。
  • 忘记设置CMAKE_SYSROOT:Linux交叉编译时,如果不设置sysroot,CMake会去本地/usr/include找头文件,结果找到的是x86的头文件,编译直接报错。
  • CMAKE_SYSTEM_NAME设错了:我之前有个项目,目标板跑的是FreeRTOS,但我设成了Linux。结果CMake自动链接了pthread和dl库,链接器报了一堆undefined reference。折腾了两天才发现是这里的问题。
  • 缓存污染:如果你第一次cmake时没指定工具链文件,第二次再指定,CMake会报错说编译器不匹配。解决办法是删除build目录重新来。

我的习惯:每个项目我都会在项目根目录放一个cmake/文件夹,里面放所有支持的toolchain.cmake。然后在README里写明怎么用。这样团队里其他人拿到项目,看一眼就知道怎么编译了。

调试工具链文件的小技巧

有时候工具链文件写好了,但CMake配置时还是报错。这时候可以打开CMake的调试模式:

cmake -DCMAKE_TOOLCHAIN_FILE=xxx.cmake --trace .. 2>&1 | grep -i "CMAKE_SYSTEM"

这个命令会打印出所有与CMAKE_SYSTEM相关的变量赋值过程。你可以看到CMake到底有没有正确读取你的设置。

另外,在工具链文件里加message()也是个好办法:

message(STATUS "Using toolchain: ${CMAKE_SYSTEM_NAME} / ${CMAKE_SYSTEM_PROCESSOR}")
message(STATUS "C compiler: ${CMAKE_C_COMPILER}")

这样cmake配置时就能看到你的工具链文件是否被正确加载了。

好了,工具链文件的核心内容就这些。说白了就是告诉CMake三件事:用什么编译器、编译给谁用、目标是什么架构。把这三点搞清楚了,交叉编译就成功了一大半。


公众号:蓝海资料掘金营,微信deep3321