链接选项控制:CMAKE_EXE_LINKER_FLAGS、CMAKE_SHARED_LINKER_FLAGS、静态与动态链接
链接阶段,说白了就是把你编译出来的那些 .o 文件,还有各种库,揉成一个可执行文件或者动态库。很多人觉得编译难,其实链接才是真正容易踩坑的地方。我刚开始用 CMake 做交叉编译时,就经常在链接这一步翻车——明明编译都过了,一链接就报 undefined reference,或者运行时提示找不到库。
今天咱们就聊聊链接选项怎么控制。重点看三个东西:CMAKE_EXE_LINKER_FLAGS、CMAKE_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 更合适。
-L/path/to/libs。而目标专用的选项,比如链接某个特定库,我放在 CMakeLists.txt 中用 target_link_libraries 处理。这样分工清晰,不会乱。
CMAKE_SHARED_LINKER_FLAGS:控制动态库的链接
这个变量和上面那个类似,但它只影响动态库(.so 或 .dll)的链接过程。我遇到过不少新手,把 CMAKE_EXE_LINKER_FLAGS 和 CMAKE_SHARED_LINKER_FLAGS 搞混,结果动态库链接时参数没传进去,编译出来的 .so 文件有问题。
常见的用法是什么呢?比如你想给动态库设置 soname:
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-soname,libmylib.so.1")
-Wl 表示把后面的参数传给链接器。这里 -soname 设置了动态库的 soname,这样在运行时系统就能正确匹配库的版本。我曾经在一个项目中没设 soname,结果升级库版本后,所有依赖它的程序都崩溃了——因为系统找不到正确的符号版本。
静态链接 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把搜索路径硬编码到可执行文件里,这样部署时就不用操心环境变量了。
知识体系图
下面这张图总结了链接选项控制的核心逻辑:
嗯,链接选项控制这块,说白了就是搞清楚「谁负责什么」。全局的、跟平台相关的选项,比如库搜索路径、链接脚本,放在 toolchain 文件里。而跟具体目标相关的,比如链接哪个库、用静态还是动态,放在 CMakeLists.txt 里用 target_link_libraries 处理。这样分工清晰,项目大了也不会乱。
公众号:蓝海资料掘金营,微信deep3321