14、多架构支持:在同一个工具链文件中处理32位与64位、硬浮点与软浮点
嵌入式开发中,一个很现实的问题来了:你的项目可能同时要跑在ARM Cortex-A53(64位)和Cortex-M4(32位)上,或者同一个芯片既有硬浮点单元(FPU)又有软浮点模拟。你总不能为每个变体写一套工具链文件吧?
我个人习惯是:一个工具链文件,通过变量切换搞定所有变体。这样维护成本低,也不容易出错。今天我们就来聊聊怎么在同一个.cmake文件里优雅地处理32/64位、硬浮点/软浮点这些“多胞胎”架构。
14.1 核心思路:用变量做“开关”
说白了,工具链文件就是一个CMake脚本。你完全可以在里面定义一些变量,比如ARCH_BITS、FLOAT_ABI,然后根据这些变量去设置不同的编译器标志、链接器选项、系统根目录等等。
我在项目中遇到过最头疼的情况:客户要求同一套代码,既要编译出ARMv7(32位硬浮点)的版本,又要编译出ARMv8(64位)的版本。如果每个版本都写一个工具链文件,那光是维护就够呛。后来我改成用一个文件,加几个开关,世界清净了。
核心原则:工具链文件只负责“描述目标平台”,不负责“选择目标平台”。选择权交给用户,通过-D参数传入。
14.2 定义架构变量
我们先在工具链文件里定义几个关键变量。这些变量用户可以在命令行里传进来,也可以写在CMakePresets.json里。
# 用户必须传入的变量
set(ARCH_BITS "32" CACHE STRING "Target architecture bits: 32 or 64")
set(FLOAT_ABI "softfp" CACHE STRING "Floating point ABI: soft, softfp, hard")
set(ARM_CPU "cortex-a9" CACHE STRING "Target ARM CPU")
# 根据位数设置编译器前缀
if(ARCH_BITS EQUAL 64)
set(TOOLCHAIN_PREFIX "aarch64-linux-gnu")
else()
set(TOOLCHAIN_PREFIX "arm-linux-gnueabihf")
endif()
嗯,这里要注意:softfp和hard的区别很多人搞混。softfp是函数调用时用软浮点规则传参,但内部计算可以用FPU;hard是全程用FPU寄存器传参和计算。你想想看,如果你的库是softfp编译的,主程序是hard编译的,链接时就会报错——ABI不兼容。
14.3 设置编译器与标志
有了变量,接下来就是根据它们设置具体的编译器和标志。这部分我建议写得细一点,因为踩坑往往就踩在这里。
# 设置编译器
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
# 根据CPU和位数设置通用标志
set(COMMON_FLAGS "-mcpu=${ARM_CPU} -mthumb")
# 根据位数设置额外标志
if(ARCH_BITS EQUAL 64)
list(APPEND COMMON_FLAGS "-march=armv8-a")
else()
list(APPEND COMMON_FLAGS "-march=armv7-a")
endif()
# 根据浮点ABI设置标志
if(FLOAT_ABI STREQUAL "hard")
list(APPEND COMMON_FLAGS "-mfloat-abi=hard -mfpu=neon-vfpv4")
elseif(FLOAT_ABI STREQUAL "softfp")
list(APPEND COMMON_FLAGS "-mfloat-abi=softfp -mfpu=neon-vfpv4")
else()
list(APPEND COMMON_FLAGS "-mfloat-abi=soft")
endif()
set(CMAKE_C_FLAGS "${COMMON_FLAGS}" CACHE STRING "C flags")
set(CMAKE_CXX_FLAGS "${COMMON_FLAGS}" CACHE STRING "C++ flags")
我曾经踩过的坑:有一次我忘了给32位和64位设置不同的-march,结果32位版本编译出来的指令集里混入了AArch64的指令,链接器直接报“relocation truncated to fit”。从那以后,我每次都会显式检查ARCH_BITS对应的-march是否正确。
14.4 处理系统根目录与库路径
多架构支持还有一个大坑:系统根目录(sysroot)和库路径。32位和64位的库通常放在不同目录下,比如/usr/arm-linux-gnueabihf/lib和/usr/aarch64-linux-gnu/lib。
# 设置系统根目录
if(ARCH_BITS EQUAL 64)
set(CMAKE_SYSROOT "/usr/aarch64-linux-gnu/sysroot")
else()
set(CMAKE_SYSROOT "/usr/arm-linux-gnueabihf/sysroot")
endif()
# 设置库搜索路径
set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
# 设置链接器搜索路径
set(CMAKE_LIBRARY_PATH
"${CMAKE_SYSROOT}/usr/lib"
"${CMAKE_SYSROOT}/lib"
)
# 根据浮点ABI调整库路径(有些发行版会区分)
if(FLOAT_ABI STREQUAL "hard")
list(APPEND CMAKE_LIBRARY_PATH "${CMAKE_SYSROOT}/usr/lib/arm-linux-gnueabihf")
endif()
你想想看,如果库路径配错了,链接器会去32位的目录里找64位的库,或者反过来。结果就是一堆“cannot find -lxxx”的错误。我刚开始做交叉编译时,经常花半天时间排查这种问题。
14.5 完整示例:一个支持多架构的工具链文件
下面是一个完整的工具链文件骨架。你可以直接拿去用,改改路径和CPU型号就行。
# arm-multiarch.cmake
# 用法: cmake -DCMAKE_TOOLCHAIN_FILE=arm-multiarch.cmake \
# -DARCH_BITS=64 -DFLOAT_ABI=hard -DARM_CPU=cortex-a53 ..
# 必须传入的变量
set(ARCH_BITS "32" CACHE STRING "32 or 64")
set(FLOAT_ABI "softfp" CACHE STRING "soft, softfp, hard")
set(ARM_CPU "cortex-a9" CACHE STRING "CPU model")
# 设置系统名称和处理器
set(CMAKE_SYSTEM_NAME Linux)
if(ARCH_BITS EQUAL 64)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
else()
set(CMAKE_SYSTEM_PROCESSOR arm)
endif()
# 编译器前缀
if(ARCH_BITS EQUAL 64)
set(TOOLCHAIN_PREFIX "aarch64-linux-gnu")
else()
set(TOOLCHAIN_PREFIX "arm-linux-gnueabihf")
endif()
# 编译器
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
# 标志
set(COMMON_FLAGS "-mcpu=${ARM_CPU} -mthumb")
if(ARCH_BITS EQUAL 64)
list(APPEND COMMON_FLAGS "-march=armv8-a")
else()
list(APPEND COMMON_FLAGS "-march=armv7-a")
endif()
if(FLOAT_ABI STREQUAL "hard")
list(APPEND COMMON_FLAGS "-mfloat-abi=hard -mfpu=neon-vfpv4")
elseif(FLOAT_ABI STREQUAL "softfp")
list(APPEND COMMON_FLAGS "-mfloat-abi=softfp -mfpu=neon-vfpv4")
else()
list(APPEND COMMON_FLAGS "-mfloat-abi=soft")
endif()
set(CMAKE_C_FLAGS "${COMMON_FLAGS}" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS "${COMMON_FLAGS}" CACHE STRING "" FORCE)
# 系统根目录
if(ARCH_BITS EQUAL 64)
set(CMAKE_SYSROOT "/usr/aarch64-linux-gnu/sysroot")
else()
set(CMAKE_SYSROOT "/usr/arm-linux-gnueabihf/sysroot")
endif()
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)
14.6 使用示例
编译32位硬浮点版本:
cmake -B build32 -DCMAKE_TOOLCHAIN_FILE=arm-multiarch.cmake \
-DARCH_BITS=32 -DFLOAT_ABI=hard -DARM_CPU=cortex-a9
编译64位软浮点版本:
cmake -B build64 -DCMAKE_TOOLCHAIN_FILE=arm-multiarch.cmake \
-DARCH_BITS=64 -DFLOAT_ABI=soft -DARM_CPU=cortex-a53
小技巧:你可以把这些命令写成shell脚本,或者用CMakePresets.json来管理。我个人更喜欢CMakePresets,因为它可以记录所有参数,团队里其他人直接cmake --preset=arm32-hard就能用,省心。
14.7 多架构支持的核心逻辑图
下面这张图展示了整个工具链文件的设计思路。你可以看到,用户传入的三个变量(ARCH_BITS、FLOAT_ABI、ARM_CPU)像三个旋钮,拧到不同位置,输出的编译器、标志、路径就跟着变。
14.8 避坑指南
最后,分享几个我实际踩过的坑,希望能帮你省点时间。
- 浮点ABI不匹配:我曾经把一个
softfp编译的静态库链接到hard编译的主程序里,链接器没报错,但运行时浮点计算全乱套。后来我养成了一个习惯:在工具链文件里显式设置-mfloat-abi,并且用CMAKE_C_FLAGS强制覆盖,不让用户乱改。 - 32位和64位的库混用:有一次我忘了切换sysroot,结果64位链接器去32位的目录里找libc.so,当然找不到。解决方案就是上面写的:根据
ARCH_BITS动态设置CMAKE_SYSROOT。 - CPU型号不匹配:如果你用
-mcpu=cortex-a53编译,但目标芯片是cortex-a9,虽然能编译通过,但生成的指令可能不兼容。我建议在工具链文件里加一个message()输出,提醒用户检查CPU型号。
总结一下:多架构支持的核心就是“变量驱动”。把架构相关的所有东西(编译器、标志、路径)都抽象成变量,然后在工具链文件里根据变量做分支。这样你只需要维护一个文件,就能覆盖32/64位、硬浮点/软浮点、不同CPU型号的各种组合。
好了,这一章就到这里。记住:工具链文件是你的“翻译官”,它负责把你的需求翻译成编译器能理解的语言。把翻译官训练好了,后面的事情就顺了。
公众号:蓝海资料掘金营,微信deep3321