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-m0、cortex-m3、cortex-m4、cortex-m7:ARM Cortex-M系列
- cortex-a53、cortex-a72:ARM Cortex-A系列
- armv7-a、armv8-a:ARM架构版本
- riscv32、riscv64:RISC-V架构
- i686、x86_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自带的工具)应该在本地找。
工具链文件的核心逻辑
说了这么多,我画了一张图帮你理清思路:
避坑指南
我这些年踩过的坑,都列在这里了:
- 编译器路径带空格: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