19、工具链与交叉编译:CMAKE_TOOLCHAIN_FILE、系统与处理器检测、交叉编译器设置、sysroot
交叉编译,说白了就是「在A机器上编译出能在B机器上跑的程序」。这事儿在嵌入式开发里天天见。你想想看,你的开发机是x86_64的Ubuntu,但目标板是个ARM Cortex-M4的小芯片,总共才几百KB的Flash——你总不能在那上面跑GCC吧?
我个人习惯,每次接手一个新嵌入式项目,第一件事就是确认工具链。不是随便找个arm-none-eabi-gcc就往上怼,得看清楚是bare-metal还是Linux用户态,是glibc还是uClibc。嗯,这里面的坑,我踩过不少。
19.1 工具链文件:CMAKE_TOOLCHAIN_FILE
CMake里做交叉编译,核心就是CMAKE_TOOLCHAIN_FILE。这个变量指向一个.cmake文件,里面定义了编译器、链接器、目标系统等信息。你不需要在CMakeLists.txt里写死这些,而是通过命令行传入:
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/arm-gcc-toolchain.cmake ..
这个文件里一般写什么?我直接给你看一个我项目里用过的模板:
# arm-gcc-toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
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_SYSTEM_NAME设为Generic表示裸机环境。如果是Linux用户态交叉编译,应该设为Linux。这个设置会影响CMake查找库和头文件的行为。
我曾经犯过一个低级错误:忘了设CMAKE_FIND_ROOT_PATH_MODE_PROGRAM为NEVER,结果CMake跑去宿主机的/usr/bin里找工具,当然找不到。折腾了半小时才发现是这行没写。
19.2 系统与处理器检测
CMake在配置阶段会自动检测宿主系统和目标系统。你可以用以下变量来获取信息:
| 变量 | 说明 | 常见值 |
|---|---|---|
CMAKE_SYSTEM_NAME |
目标系统名称 | Linux, Windows, Darwin, Generic |
CMAKE_SYSTEM_PROCESSOR |
目标处理器架构 | arm, aarch64, x86_64, riscv64 |
CMAKE_HOST_SYSTEM_NAME |
宿主系统名称 | Linux, Windows, Darwin |
CMAKE_HOST_SYSTEM_PROCESSOR |
宿主处理器架构 | x86_64, AMD64 |
你可以在CMakeLists.txt里用条件判断来做平台适配:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(myapp PRIVATE PLATFORM_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Generic")
target_compile_definitions(myapp PRIVATE PLATFORM_BARE_METAL)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64")
target_compile_options(myapp PRIVATE -mthumb -mcpu=cortex-m4)
endif()
小技巧:用CMAKE_SYSTEM_PROCESSOR做匹配时,建议用MATCHES而不是STREQUAL,因为不同工具链报告的字符串可能略有差异。比如有的报armv7l,有的报arm。
19.3 交叉编译器设置
设置交叉编译器,不只是指定gcc和g++的路径。你还需要考虑几个细节:
- 编译器前缀:很多工具链使用统一前缀,比如
arm-none-eabi-。你可以用CMAKE_C_COMPILER直接指定全路径,也可以用CMAKE_C_COMPILER_TARGET配合CMAKE_C_COMPILER_FORCED。 - 链接器标志:交叉编译的链接器标志往往和本地不同。比如嵌入式项目常用
-T linker.ld指定链接脚本。 - 汇编器:别忘了
CMAKE_ASM_COMPILER,很多嵌入式项目需要汇编文件。
我建议你在工具链文件里统一设置这些,而不是散落在各个CMakeLists.txt里:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_C_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${CMAKE_SOURCE_DIR}/linker.ld --specs=nano.specs")
注意:不要在工具链文件里用set(CMAKE_C_FLAGS ...)覆盖原有值,要用${CMAKE_C_FLAGS}追加。否则CMake内部的一些必要标志会被清掉,编译会报一些莫名其妙的问题。
19.4 sysroot:根文件系统
sysroot,就是目标系统的根目录。里面放着目标平台的库文件、头文件、甚至一些配置文件。交叉编译时,编译器需要知道去哪里找这些文件。
设置sysroot有两种方式:
- 在工具链文件里设置:
set(CMAKE_SYSROOT /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH /path/to/sysroot)
- 通过编译器标志传递:有些工具链自带sysroot,比如ARM GCC的
--sysroot选项。CMake会自动检测并传递。
sysroot里一般长这样:
sysroot/
├── lib/
│ ├── libc.a
│ ├── libm.a
│ └── crt0.o
├── usr/
│ ├── lib/
│ │ └── libpthread.a
│ └── include/
│ ├── stdio.h
│ └── stdlib.h
└── etc/
└── ...
我记得有一次,客户给的sysroot里缺了libpthread.so,但代码里用了多线程。链接时疯狂报错,我查了半天才发现是sysroot不完整。从那以后,我每次拿到新sysroot都会先跑一遍find检查关键库文件。
19.5 知识体系结构图
下面这张图总结了工具链与交叉编译的核心流程和关键要素:
19.6 实战建议
说了这么多理论,最后给几条实战建议:
- 工具链文件单独维护:不要和项目代码混在一起。我习惯建一个
cmake/toolchains/目录,里面放各种目标平台的工具链文件。 - 用CMake预设(Presets):CMake 3.19+支持
CMakePresets.json,可以把工具链文件路径、缓存变量都写进去。团队协作时特别方便。 - 验证sysroot:写一个简单的hello world,先手动编译试试。能通过再进CMake流程。这一步能省下大量调试时间。
- 注意浮点ABI:ARM平台有硬浮点和软浮点之分。工具链和sysroot必须匹配,否则链接时一堆undefined reference。
我的习惯:每次新建交叉编译项目,我都会在工具链文件里加一行message(STATUS "Using toolchain: ${CMAKE_C_COMPILER}")。配置时看一眼输出,确认用的是正确的编译器。这招帮我避免过好几次「用了宿主机gcc」的尴尬。
交叉编译这东西,说白了就是「骗」CMake。你告诉它目标系统是ARM Linux,它就会用ARM的编译器、找ARM的库。只要工具链文件和sysroot配对了,剩下的和本地编译没什么两样。嗯,前提是你没踩到我上面说的那些坑。
公众号:蓝海资料掘金营,微信 deep3321