在这里插入图片描述

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!

🎬 博主简介:

在这里插入图片描述



前言:

在 C/C++ 开发中,库的管理是工程化的核心环节。静态库虽然简单,但会导致可执行文件体积膨胀、内存浪费;而 动态库(共享库) 凭借共享内存、按需加载、易于更新等优势,成为大型项目的首选。然而,动态库的运行时加载、版本管理、跨平台分发一直是新手的噩梦。CMake 作为 C/C++ 事实上的构建标准,提供了一套完整的动态库管理机制。本文将从底层原理出发,结合完整实战案例,带你掌握 CMake 动态库的生成、引用、安装、发布,以及find_package的两种核心查找模式,彻底解决 “cannot open shared object file” 等经典问题。

本篇飞书笔记


一. 动态库核心原理与 CMake 基础

1.1 静态库 vs 动态库:核心区别

在开始实战前,我们先明确两种库的本质差异,这是理解后续所有操作的基础:

对比维度 静态库 (.a/.lib) 动态库 (.so/.dll/.dylib)
链接时机 编译期直接嵌入可执行文件 运行时由动态链接器加载
内存占用 每个进程一份,无法共享 多进程共享同一份内存
可执行文件大小 大(包含完整库代码) 小(仅含重定位表)
更新方式 需重新编译整个可执行文件 只需替换库文件
插件机制 无法实现 支持dlopen()/LoadLibrary()动态加载
许可证兼容性 与 GPL 兼容性高 LGPL 允许动态链接闭源程序
CRT 依赖 (Windows) 静态 CRT (MT),单文件部署 动态 CRT (MD),需分发运行时库

1.2 位置无关代码 (PIC) 与 PIE

动态库必须编译为位置无关代码 (PIC),这是它能被任意进程映射到任意虚拟地址的关键:

  • -fPIC:生成位置无关目标文件,专门用于共享库。代码中不使用绝对地址,所有地址都通过 GOT/PLT 表间接访问。
  • -fPIE:生成位置无关可执行文件,与链接器参数-pie配合使用,可配合 ASLR(地址空间布局随机化)提高程序安全性。

一句话总结:PIC 用于库,PIE 用于最终可执行文件。现代 64 位 CPU 的性能损耗几乎可以忽略,Linux 发行版默认启用 PIE。

GOT/PLT:动态链接的核心

ELF 格式的动态链接依赖两张关键的跳转表:

  • GOT (全局偏移表):存放变量和函数的真实内存地址
  • PLT (过程链接表):存放跳转指令的桩代码,负责将第一次函数调用转发给动态解析器,后续调用直接跳转到 GOT 中缓存的真实地址

1.3 Linux 动态库加载机制

当你运行一个依赖动态库的程序时,ld-linux.so动态链接器会按以下优先级顺序查找库文件:

  1. LD_LIBRARY_PATH环境变量指定的目录
  2. ELF 文件的DT_RUNPATH段记录的路径(现代标准)
  3. 系统缓存/etc/ld.so.cache(由ldconfig更新)
  4. 默认系统目录/lib/usr/lib

关键坑点:即使可执行文件和动态库在同一个目录,如果没有配置RPATH,运行时仍然会报错找不到库!

1.4 CMake 的 RPATH 机制:解决运行时加载问题

CMake 为了解决 “编译完跑不起来” 的问题,实现了一套智能的 RPATH 机制,核心思想是:在构建树写入开发用的路径,在安装树重写或删除路径

DT_RPATH vs DT_RUNPATH

  • DT_RPATH:旧标记,优先级高于LD_LIBRARY_PATH,已被弃用
  • DT_RUNPATH:新标记,优先级低于LD_LIBRARY_PATH,运行时可被覆盖(GNU ld 默认生成)

RPATH 的两个生命周期

  1. 构建阶段 (Build Tree):CMake 自动将依赖库的输出目录写入 RPATH,让刚编译的程序能直接运行
  2. 安装阶段 (Install Tree):默认会删除 RPATH,需要手动配置安装后的 RPATH

关键控制变量

变量名 作用
CMAKE_SKIP_RPATH 完全禁用 RPATH
CMAKE_BUILD_RPATH 显式指定构建阶段 RPATH
CMAKE_INSTALL_RPATH 显式指定安装阶段 RPATH
CMAKE_INSTALL_RPATH_USE_LINK_PATH 使用链接路径填充安装 RPATH
CMAKE_BUILD_RPATH_USE_ORIGIN 使用$ORIGIN相对路径

二. CMake 动态库实战:从生成到内部引用

我们以一个简单的数学库MyMath为例,完整演示动态库的生成和项目内部引用流程。

2.1 项目结构

my_math_shared/
├── CMakeLists.txt    # 顶层CMake
├── my_lib/           # 动态库模块
│   ├── CMakeLists.txt
│   ├── include/
│   │   └── math.h
│   └── src/
│       ├── add.cpp
│       └── sub.cpp
└── app/              # 测试程序
    ├── CMakeLists.txt
    └── main.cpp

2.2 生成动态库:my_lib/CMakeLists.txt

# 1. 收集库的源代码
file(GLOB SRC_LISTS "src/*.cpp")

# 2. 添加动态库目标(SHARED表示生成动态库)
add_library(MyMath SHARED ${SRC_LISTS})

# 3. 设置库的使用要求
# PUBLIC:头文件路径会传递给所有依赖MyMath的目标
target_include_directories(MyMath
    PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include
)

# 4. 设置库属性
set_target_properties(MyMath PROPERTIES
    # 静态库输出路径
    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
    # 动态库输出路径
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
    # 库文件名(默认会自动加lib前缀和.so后缀)
    OUTPUT_NAME MyMath
    # 完整版本号
    VERSION 1.2.3
    # API版本号(用于兼容性控制)
    SOVERSION 20
    # 强制生成位置无关代码
    COMPILE_OPTIONS "-fPIC"
)

代码解读

  • add_library(MyMath SHARED ...):指定生成动态库,CMake 会根据平台自动生成libMyMath.so(Linux) 或MyMath.dll(Windows)
  • SOVERSION:API 版本号,用于兼容性管理。当 API 发生不兼容变更时,需要递增此值
  • COMPILE_OPTIONS "-fPIC":虽然 CMake 在生成 SHARED 目标时会自动添加-fPIC,但显式指定更清晰

2.3 内部引用动态库:app/CMakeLists.txt

# 1. 收集源文件
file(GLOB SRC_LISTS "*.cpp")

# 2. 添加可执行文件
add_executable(main ${SRC_LISTS})

# 3. 链接动态库
# PRIVATE:依赖仅用于main自身,不会传递给其他目标
target_link_libraries(main PRIVATE MyMath)

# 4. 设置可执行文件输出路径
set_target_properties(main PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)

# 5. 打印动态库路径(调试用)
add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo 
        "动态库路径: $<TARGET_FILE:MyMath>"
    COMMENT "获取动态库输出路径"
)

2.4 编译运行验证

# 创建构建目录并进入
mkdir build && cd build

# 生成构建文件
cmake ..

# 编译
cmake --build .

编译输出会显示:

[20%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/add.cpp.o
[40%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/sub.cpp.o
[60%] Linking CXX shared library ../lib/libMyMath.so
获取动态库输出路径: /home/bit/workspace/my_math_shared/build/lib/libMyMath.so
[80%] Building CXX object app/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable ../bin/main
[100%] Built target main

验证运行时加载

# 直接运行,不会报错!
./bin/main
# 输出:
# 3 + 4 = 7
# 3 - 4 = -1

查看 ELF 中的 RUNPATH

readelf -d bin/main | grep RUNPATH
# 输出:
# 0x000000000000001d (RUNPATH) Library runpath: [/home/bit/workspace/my_math_shared/build/lib]

这就是 CMake 的魔法:它自动将动态库的构建目录写入了RUNPATH,所以程序能直接找到库。


三. 动态库的安装与发布

内部引用只是开发阶段的需求,当我们想把库分享给其他项目使用时,就需要将库安装到系统标准路径,并生成 CMake 配置文件,让其他项目能通过find_package轻松引用。

3.1 完整安装配置:my_lib/CMakeLists.txt

在之前的基础上添加以下安装代码:

# 引入GNU安装目录标准
include(GNUInstallDirs)

# 5. 安装库文件
install(TARGETS MyMath
    # 导出目标集合,用于生成配置文件
    EXPORT MyMathTargets
    # 动态库安装到${CMAKE_INSTALL_LIBDIR}(默认/usr/local/lib)
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    # 静态库安装到同一目录
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

# 6. 安装头文件
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/math
    FILES_MATCHING PATTERN "*.h"
)

# 7. 导出目标到构建树(供本地开发使用)
export(EXPORT MyMathTargets
    FILE ${CMAKE_CURRENT_BINARY_DIR}/MyMathTargets.cmake
    NAMESPACE MyMath::
)

# 8. 安装导出目标到系统路径
install(EXPORT MyMathTargets
    FILE MyMathTargets.cmake
    NAMESPACE MyMath::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath
)

# 9. 生成Config.cmake文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    INSTALL_DESTINATION "lib/cmake/MyMath"
)

# 10. 安装Config.cmake文件
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    DESTINATION "lib/cmake/MyMath"
)

3.2 创建 Config.cmake.in 模板

my_lib目录下创建Config.cmake.in文件:

@PACKAGE_INIT@

# 包含导出的目标文件
include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)

3.3 执行安装

cd build
# 安装到系统默认路径/usr/local
sudo cmake --install .

安装完成后,系统会生成以下文件:

  • 库文件:/usr/local/lib/libMyMath.so.1.2.3/usr/local/lib/libMyMath.so.20/usr/local/lib/libMyMath.so
  • 头文件:/usr/local/include/math/math.h
  • CMake 配置文件:/usr/local/lib/cmake/MyMath/MyMathConfig.cmake/usr/local/lib/cmake/MyMath/MyMathTargets.cmake

四. 动态库的查找与使用

现在我们创建一个新的项目test_MyMath,演示如何使用find_package查找并引用我们刚刚安装的MyMath库。

4.1 项目结构

test_MyMath/
├── CMakeLists.txt
└── main.cpp

4.2 CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(MyMathApp)

# 查找MyMath库,CONFIG模式强制使用配置文件
find_package(MyMath REQUIRED CONFIG)

# 添加可执行文件
add_executable(main main.cpp)

# 链接导入目标MyMath::MyMath
target_link_libraries(main PRIVATE MyMath::MyMath)

4.3 main.cpp

#include <iostream>
// 注意头文件路径是math/math.h,因为我们安装时指定了DESTINATION math
#include "math/math.h"

int main() {
    std::cout << "add(3,4) = " << add(3, 4) << std::endl;
    std::cout << "sub(3,4) = " << sub(3, 4) << std::endl;
    return 0;
}

4.4 编译运行

mkdir build && cd build
cmake ..
cmake --build .
./main

输出:

add(3,4) = 7
sub(3,4) = -1

查看安装后的 RUNPATH

readelf -d main | grep RUNPATH
# 输出:
# 0x000000000000001d (RUNPATH) Library runpath: [/usr/local/lib]

五. 第三方库的两种查找模式详解

find_package是 CMake 中查找第三方库的核心命令,它有两种完全不同的工作模式:Module 模式Config 模式。理解这两种模式的区别,是解决 “找不到库” 问题的关键。

5.1 两种模式的工作原理

CMake 2.3 + 默认的查找顺序是:先尝试 Config 模式,失败后回落到 Module 模式。你可以通过关键字强制使用某一种模式:

# 强制使用Config模式
find_package(jsoncpp CONFIG REQUIRED)
# 强制使用Module模式
find_package(JsonCpp MODULE REQUIRED)

5.2 Module 模式(Find.cmake)

Module 模式依赖于Find<PackageName>.cmake脚本文件,这些脚本由 CMake 官方或社区提供,负责手动扫描磁盘查找库文件和头文件。

工作流程

  1. 查找Find<PackageName>.cmake文件
    1. 先在CMAKE_MODULE_PATH指定的目录查找
    2. 再在 CMake 内置模块目录/usr/share/cmake/Modules/查找
  2. 执行脚本,内部通过find_library()find_path()扫描磁盘
  3. 设置<PackageName>_FOUND<PackageName>_INCLUDE_DIRS<PackageName>_LIBRARIES等变量

适用场景

  • 目标库不是用 CMake 构建的
  • 目标库没有提供 Config.cmake 文件
  • 系统发行版提供了成熟的 Find 模块

实战:Module 模式查找 JsonCpp

  1. 首先安装 JsonCpp 开发包:
    1. sudo apt install libjsoncpp-dev
      
  2. 创建cmake/FindJsonCpp.cmake模块文件:
    1. # 查找头文件
      find_path(JsonCpp_INCLUDE_DIR
          NAMES json/json.h
          PATHS /usr/include /usr/local/include
          PATH_SUFFIXES jsoncpp
      )
      
      # 查找库文件
      find_library(JsonCpp_LIBRARY
          NAMES jsoncpp libjsoncpp
          PATHS /usr/lib /usr/local/lib
      )
      
      # 设置结果变量
      if(JsonCpp_INCLUDE_DIR AND JsonCpp_LIBRARY)
          set(JsonCpp_FOUND TRUE)
          set(JsonCpp_INCLUDE_DIRS ${JsonCpp_INCLUDE_DIR})
          set(JsonCpp_LIBRARIES ${JsonCpp_LIBRARY})
          
          # 创建导入目标(现代CMake推荐)
          if(NOT TARGET JsonCpp::JsonCpp)
              add_library(JsonCpp::JsonCpp UNKNOWN IMPORTED)
              set_target_properties(JsonCpp::JsonCpp PROPERTIES
                  IMPORTED_LOCATION "${JsonCpp_LIBRARY}"
                  INTERFACE_INCLUDE_DIRECTORIES "${JsonCpp_INCLUDE_DIRS}"
              )
          endif()
      else()
          set(JsonCpp_FOUND FALSE)
      endif()
      
      mark_as_advanced(JsonCpp_INCLUDE_DIR JsonCpp_LIBRARY)
      
  3. 项目 CMakeLists.txt:
    1. cmake_minimum_required(VERSION 3.18)
      project(JsonCppModuleDemo)
      set(CMAKE_CXX_STANDARD 11)
      
      # 添加自定义模块路径
      list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
      
      # 使用Module模式查找JsonCpp
      find_package(JsonCpp REQUIRED MODULE)
      
      add_executable(main main.cpp)
      target_link_libraries(main PRIVATE JsonCpp::JsonCpp)
      

5.3 Config 模式(Config.cmake)

Config 模式是现代 CMake 推荐的方式,它依赖于库的发布者提供的<PackageName>Config.cmake文件,该文件包含了库的完整信息和导入目标。

工作流程

  1. 查找<PackageName>Config.cmake<package-name>-config.cmake文件
    1. 先在CMAKE_PREFIX_PATH指定的目录查找
    2. 再在系统标准路径/usr/local/lib/cmake/等查找
  2. 执行配置文件,直接定义导入目标(如JsonCpp::JsonCpp
  3. 导入目标已经包含了头文件路径、库文件路径、编译选项等所有信息

优势

  • 不需要手写查找脚本
  • 自动处理版本依赖和组件
  • 跨平台行为一致
  • 支持目标属性的自动传递

实战:Config 模式查找 JsonCpp

  1. 从源码编译安装 JsonCpp(会自动生成 Config.cmake):
    1. git clone https://gitee.com/lizhengping189/jsoncpp.git
      cd jsoncpp
      mkdir build && cd build
      cmake .. -DBUILD_SHARED_LIBS=ON -DJSONCPP_WITH_TESTS=OFF
      cmake --build .
      sudo cmake --install .
      
  2. 项目 CMakeLists.txt:
    1. cmake_minimum_required(VERSION 3.18)
      project(JsonCppConfigDemo)
      set(CMAKE_CXX_STANDARD 11)
      
      # 使用Config模式查找JsonCpp
      find_package(jsoncpp CONFIG REQUIRED)
      
      add_executable(main main.cpp)
      # 链接导入目标,自动包含头文件路径
      target_link_libraries(main PRIVATE JsonCpp::JsonCpp)
      

5.4 两种模式对比

对比维度 Module 模式 Config 模式
依赖文件 Find<Package>.cmake <Package>Config.cmake
文件来源 CMake 官方 / 社区 / 自己编写 库的发布者提供
工作方式 手动扫描磁盘查找 直接使用发布者提供的信息
优点 兼容非 CMake 项目 无需手写脚本,自动处理依赖
缺点 需要维护脚本,版本兼容性差 仅适用于 CMake 构建的库
推荐程度 兼容旧库时使用 现代 CMake 首选

5.5 常见坑点

  1. 包名大小写问题:包名区分大小写!例如find_package(Boost)find_package(boost)是不同的。官方推荐使用大写首字母。
  2. C++ 标准不匹配:这是最隐蔽的坑!以 JsonCpp 为例:
    1. JsonCpp 1.9.3 + 在 C++17 下新增了operator[](std::string_view)重载
    2. 如果库是用 C++11 编译的,而你的代码用 C++17 编译,头文件会看到这个函数声明,但库文件中没有实现,导致链接失败
    3. 解决方法:确保你的项目和库使用相同的 C++ 标准编译
  3. 静态库优先问题:很多库(如 JsonCpp)会同时生成静态库和动态库,Config 模式下默认优先链接静态库。如果想强制使用动态库,可以直接链接jsoncpp_lib目标。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:本文完整覆盖了 CMake 动态库管理的全流程,从底层原理到实战落地,掌握这些知识,你就能彻底解决 C/C++ 项目中动态库管理的各种问题,写出规范、可移植的 CMake 构建脚本。下一步学习建议:尝试使用 CPack 将动态库和可执行文件打包成可分发的安装包,学习如何使用$ORIGIN相对路径实现 “解压即运行” 的绿色软件。

✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど

在这里插入图片描述

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐