链接选项控制:CMAKE_EXE_LINKER_FLAGS、CMAKE_SHARED_LINKER_FLAGS、静态与动态链接

链接阶段,说白了就是把你编译出来的那些 .o 文件,还有各种库,揉成一个可执行文件或者动态库。很多人觉得编译难,其实链接才是真正容易踩坑的地方。我刚开始用 CMake 做交叉编译时,就经常在链接这一步翻车——明明编译都过了,一链接就报 undefined reference,或者运行时提示找不到库。

今天咱们就聊聊链接选项怎么控制。重点看三个东西:CMAKE_EXE_LINKER_FLAGSCMAKE_SHARED_LINKER_FLAGS,以及静态链接和动态链接的选择。

CMAKE_EXE_LINKER_FLAGS:控制可执行文件的链接

这个变量,专门用来给可执行文件的链接器传参数。你想想看,当你用 add_executable 生成一个目标时,CMake 最终会调用链接器。如果你想给链接器加一些额外的参数,比如指定库搜索路径、链接特定的库、或者设置链接脚本,那就得靠它了。

举个例子。我在做 ARM 交叉编译时,经常需要指定链接脚本:

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${CMAKE_SOURCE_DIR}/linker.ld")

这里 -T 就是告诉链接器使用自定义的链接脚本。嗯,要注意一点——CMAKE_EXE_LINKER_FLAGS 是全局变量,会影响所有可执行文件。如果你只想给某个特定目标加链接参数,用 target_link_options 更合适。

我的习惯:全局链接选项放在 toolchain 文件里设置,比如库搜索路径 -L/path/to/libs。而目标专用的选项,比如链接某个特定库,我放在 CMakeLists.txt 中用 target_link_libraries 处理。这样分工清晰,不会乱。

CMAKE_SHARED_LINKER_FLAGS:控制动态库的链接

这个变量和上面那个类似,但它只影响动态库(.so 或 .dll)的链接过程。我遇到过不少新手,把 CMAKE_EXE_LINKER_FLAGSCMAKE_SHARED_LINKER_FLAGS 搞混,结果动态库链接时参数没传进去,编译出来的 .so 文件有问题。

常见的用法是什么呢?比如你想给动态库设置 soname:

set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-soname,libmylib.so.1")

-Wl 表示把后面的参数传给链接器。这里 -soname 设置了动态库的 soname,这样在运行时系统就能正确匹配库的版本。我曾经在一个项目中没设 soname,结果升级库版本后,所有依赖它的程序都崩溃了——因为系统找不到正确的符号版本。

注意:在交叉编译时,这两个变量一定要在 toolchain 文件中设置,而不是在 CMakeLists.txt 里。因为 CMakeLists.txt 可能会被多次执行,而 toolchain 文件只在配置阶段加载一次。我见过有人把链接参数写在 CMakeLists.txt 里,结果每次 cmake 重新配置时参数被重复追加,最后链接器报参数过长。

静态链接 vs 动态链接:怎么选?

这个问题,说白了就是「打包」和「共享」的取舍。静态链接把库的代码直接复制到你的可执行文件里,动态链接则是在运行时去加载 .so 文件。

我个人的经验是这样的:

  • 嵌入式系统:我倾向于静态链接。因为目标板上的文件系统可能很精简,不一定有动态库。而且静态链接可以减少运行时依赖,部署起来省心。但代价是可执行文件体积会变大。
  • 桌面应用或服务器:动态链接更合适。多个程序可以共享同一个 .so,节省内存。而且更新库时,只要保持接口兼容,不需要重新编译所有程序。

在 CMake 中控制静态/动态链接,主要有两种方式:

方式一:通过 target_link_libraries 指定

# 链接静态库
target_link_libraries(myapp libfoo.a)

# 链接动态库
target_link_libraries(myapp libfoo.so)

但这种方式不够灵活。你想想看,如果我想在 Debug 模式下用动态库,Release 模式下用静态库,那代码就不好维护了。

方式二:通过 CMAKE_FIND_LIBRARY_SUFFIXES 控制搜索优先级

# 优先搜索静态库
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".so")

# 优先搜索动态库
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so" ".a")

这个变量告诉 CMake 在查找库时,先找哪种后缀。我一般在交叉编译的 toolchain 文件中这样设置:

# 嵌入式平台,优先静态链接
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".so")
# 同时强制链接器使用静态库
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")

-static 这个参数告诉链接器,尽量使用静态库。但要注意,如果某些库没有静态版本,链接会失败。

一个完整的 toolchain 示例

说了这么多,咱们看一个实际的 toolchain 文件片段。这是我为一个 ARM Cortex-M4 项目写的:

# ARM GCC 交叉编译工具链
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_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${CMAKE_SOURCE_DIR}/STM32F407VGTx_FLASH.ld")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Map=${CMAKE_BINARY_DIR}/output.map")

# 强制静态链接
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")

# 库搜索路径
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SOURCE_DIR}/libs")

这里我做了几件事:

  • 指定了链接脚本 -T,这是嵌入式开发必须的,因为芯片的内存布局需要精确控制。
  • 用了 --gc-sections,可以去掉未使用的函数和数据段,减小固件体积。
  • 生成了 map 文件,方便分析内存占用。我在调试时经常看 map 文件,找哪些函数占空间太大。
  • 强制只搜索 .a 静态库,避免链接到动态库——因为裸机系统根本没有动态加载器。

避坑指南

最后分享几个我踩过的坑:

  • 链接顺序问题:链接器是从左到右处理库的。如果你写 target_link_libraries(myapp B A),而 B 依赖 A,那链接会失败。因为链接器先看到 B,发现它需要 A 中的符号,但此时 A 还没被处理。解决办法是把被依赖的库放在后面。我习惯把依赖关系画个图,然后从叶子节点开始链接。
  • 重复定义:静态链接时,如果两个库都定义了同一个函数,链接器会报错。我曾经在一个项目中,两个第三方库都实现了同一个日志函数,折腾了半天才找到原因。解决办法是用 --allow-multiple-definition 参数,但这是下策,最好还是从源码层面解决。
  • 运行时找不到 .so:动态链接的程序在目标板上运行时,如果 LD_LIBRARY_PATH 没设置对,就会报 cannot open shared object file。我一般会在编译时用 -Wl,-rpath,/usr/local/lib 把搜索路径硬编码到可执行文件里,这样部署时就不用操心环境变量了。

知识体系图

下面这张图总结了链接选项控制的核心逻辑:

链接选项控制知识体系 CMAKE_EXE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS 静态 vs 动态链接 链接脚本 -T 库搜索路径 -L --gc-sections 去冗余 soname 设置 -Wl,-soname rpath 硬编码搜索路径 版本控制 .so.1.0.0 CMAKE_FIND_LIBRARY_SUFFIXES -static 强制静态链接 target_link_libraries 指定 核心原则:全局选项放 toolchain,局部选项放 CMakeLists.txt

嗯,链接选项控制这块,说白了就是搞清楚「谁负责什么」。全局的、跟平台相关的选项,比如库搜索路径、链接脚本,放在 toolchain 文件里。而跟具体目标相关的,比如链接哪个库、用静态还是动态,放在 CMakeLists.txt 里用 target_link_libraries 处理。这样分工清晰,项目大了也不会乱。

一句话总结:CMAKE_EXE_LINKER_FLAGS 管可执行文件,CMAKE_SHARED_LINKER_FLAGS 管动态库,静态链接用 -static 或调整搜索后缀。交叉编译时,这些选项最好在 toolchain 文件中一次性设好,避免重复劳动。

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