当前位置: 首页 > news >正文

c++之使用 libdl.so 和 <dlfcn.h> 实现动态链接

静态链接与动态链接

静态链接

静态链接是指在程序编译阶段,将所有需要的库代码直接复制到最终生成的可执行文件中,形成一个整体,运行时不再依赖外部库文件。

静态链接就像是把所有用到的工具都打包进了你的应用程序里。不管用到的库有多大,最终生成的可执行文件(比如 a.out)都会把这些库内容直接塞进去。运行的时候,这个程序是“自给自足”的,不依赖外面的库文件,哪怕系统上删掉了原本的库,它也能跑。

动态链接

动态链接是指在程序运行时,按需加载外部共享库(Shared Libraries),程序本身只包含对库函数的引用,不包含库代码,运行时由操作系统动态地链接这些库。

动态链接就像是你写了个程序,但工具箱没直接塞到程序里,而是告诉程序到时候用外面的现成工具箱。程序里只是记了下:“要用 xxx 工具箱,到时候系统帮我找来”。运行时,操作系统发现你程序里要用 libm.so(数学库),就把 /usr/lib/ 里的 libm.so 给你挂上去。

动态链接的好处

  • 节省磁盘空间
    多个程序共享同一个库文件,不需要每个程序各拷一份。

  • 节省内存
    操作系统可以让多个程序共享加载到内存中的同一个库副本(比如共享只读段)。

  • 便于维护和升级
    修复一个库的 bug、安全漏洞,只要替换库文件,不需要修改和重新编译依赖它的所有程序。

  • 支持插件和模块化设计
    可以根据需要在运行时加载特定模块(比如浏览器动态加载视频插件)。

  • 提升程序灵活性
    程序可以根据实际情况决定加载哪个版本的库,甚至可以在运行时切换不同功能。

libdl.so 和 <dlfcn.h> 简介

libdl.so

libdl.so 是一个专门用于支持运行时动态链接(dynamic linking)的标准共享库,提供了在程序运行过程中加载、使用和卸载其他共享对象(.so文件)的接口。

libdl.so 就是一个专门管理“动态加载库”的小管家库。程序运行到一半的时候,如果需要额外功能(比如一个新的模块),就可以通过调用 libdl.so 里的功能,把外面的 .so 文件(共享库)搬进来用。简单理解,libdl.so 提供了“程序自己动手,运行时找库、开箱、拿函数用”的能力。

<dlfcn.h>

<dlfcn.h> 是一个标准头文件,定义了操作运行时共享对象(dynamic shared objects,DSO)所需的接口函数、数据类型和错误处理机制。

<dlfcn.h> 就是声明了那些能让你“打开.so文件、找符号、关闭库”的函数的地方。如果你想在程序里动态加载库,就必须先 #include <dlfcn.h>,否则编译器不知道你在说什么。

二者关系

libdl.so真正实现动态链接功能的共享库(里面是编译好的二进制代码)。

<dlfcn.h>提供 libdl.so 功能对应函数声明的头文件(让编译器知道你要调用哪些函数)。

一个是实打实干活的工具箱(libdl.so),
一个是告诉程序怎么用这个工具箱的说明书(dlfcn.h)。

<dlfcn.h> 提供的常用API

前置概念

共享库:是将某些功能或模块封装成一个独立的文件(.so 文件/.ddl文件)并供多个程序同时使用的技术。在 Linux 系统中,扩展名通常为 .so,在 Windows 系统中则为 .dll。比如在linux中自己实现了mymath.h、mymath.cpp,通过gcc/g++将其编译成了一个libmymath.so文件,这个文件就叫共享库。

共享库的符号:指共享库中的变量、函数、类等可提供外部程序使用的标识符,如mymath.h中实现的add(int x, int y)就称为共享库的符号

dlopen()

打开一个动态链接库文件并返回一个句柄,该句柄用于后续的操作,  如查找符号、关闭库等。

void* dlopen(const char* filename, int flag);

参数: 

filename:

  1. 1.指定动态库文件的路径,如果传入NULL表示打开的是调用进程的主程序(可以直接用来查找主程序中的符号:比如说我是在a.cpp文件中调用到dlopen并传入一个NULL,那得到的这个句柄中可以得到a.cpp中的符号)。
  2. 2.可以是相对路径或绝对路径。3.如果路径中不包含/,动态链接器会根据LD_LIBRARY_PATH环境变量搜索库文件。

flag:指定加载动态库的方式,可以通过|组合使用

返回值:

成功调用返回动态库的句柄,失败返回NULL,可通过dlerror获取错误信息

示例:

比如我们已经实现了mymath.h/.cpp并编译出一个Libmymath.so文件

void* handle = dlopen("./libmymath.so", RTLD_LAZY);
if (!handle) {std::cerr << "Failed to open library: " << dlerror() << std::endl;
}

dlsym()

在打开的动态库中查找指定的符号(函数、变量),并返回符号的地址。

void* dlsym(void* handle, const char* symbol);

参数:

handle:动态库的句柄,由dlopen返回。如果传入宏RTLD_DEFAULT,表示在全局符号表中查找符号。如果传入宏RTLD_NEXT,表示从当前共享库之后加载(如果我使用dlopen libb.so得到的句柄handleB去调用dlsym时,并且在dlopen libb.so之前,我dlopen了liba.so,在dlopen libb.so之后,我dlopen了libc.so,那么如果给handleB传一个RTLD_NEXT时,会在libc.so中去查找)。

symbol:要查找的符号名称(通常是函数名或变量名),大小写敏感

返回值:

成功调用返回符号的地址,调用失败返回NULL,可通过dlerror获取错误信息

示例:

假如mymath.h中有个方法叫add

typedef int (*AddFunc)(int, int);   // 定义函数指针类型// 查找符号 add
AddFunc add = (AddFunc)dlsym(handle, "add");
const char* dlsym_error = dlerror();
if (dlsym_error) {std::cerr << "Cannot load symbol 'add': " << dlsym_error << std::endl;dlclose(handle);
}

dlclose()

关闭先前打开的动态链接库。

int dlclose(void* handle);

参数:

handle:动态库的句柄,由dlopen返回

返回值

失败返回非0值。

如果关闭一个库时还有其他库正在使用该库的符号,库不会立即卸载,而是等待符号的引用计数降为0时卸载。在dlopen时使用RTLD_NODELETE宏时,调用dlclose后不会真正卸载。

dlerror()

返回最近一次动态链接库操作的错误信息。

const  char* dlerror();

返回值:

成功返回NULL表示无错误,失败返错误信息的字符串。

每次调用dlerror都会清楚之前的错误状态。

<dlfcn.h>使用示例

使用动态库加载一个日志模块

logger.h/logger.cpp的实现

// logger.h
#pragma once
#include <string>extern "C" {    //用了 extern "C",防止 C++ 函数名被编译器改名(名字修饰/mangling),这样 dlsym 能直接找字符串 "initLogger" 等。// 初始化日志系统
void initLogger(const char* filename);// 写一条 info 级别的日志
void logInfo(const char* message);// 关闭日志系统
void closeLogger();}// logger.cpp
#include "logger.h"
#include <fstream>static std::ofstream logFile;void initLogger(const char* filename) {logFile.open(filename, std::ios::app);if (logFile.is_open()) {logFile << "[Logger] Initialized.\n";}
}void logInfo(const char* message) {if (logFile.is_open()) {logFile << "[INFO]: " << message << "\n";}
}void closeLogger() {if (logFile.is_open()) {logFile << "[Logger] Closed.\n";logFile.close();}
}

 编译成共享库liblogger.so

g++ -fPIC -shared -o liblogger.so logger.cpp

main 

#include <iostream>
#include <dlfcn.h>    // 引入动态链接相关APItypedef void (*InitLoggerFunc)(const char*);
typedef void (*LogInfoFunc)(const char*);
typedef void (*CloseLoggerFunc)();int main() {// 打开动态库void* handle = dlopen("./liblogger.so", RTLD_LAZY);if (!handle) {std::cerr << "Error opening library: " << dlerror() << std::endl;return 1;}// 清除之前的错误dlerror();// 查找 initLoggerInitLoggerFunc initLogger = (InitLoggerFunc)dlsym(handle, "initLogger");const char* error1 = dlerror();if (error1) {std::cerr << "Error loading symbol 'initLogger': " << error1 << std::endl;dlclose(handle);return 1;}// 查找 logInfoLogInfoFunc logInfo = (LogInfoFunc)dlsym(handle, "logInfo");const char* error2 = dlerror();if (error2) {std::cerr << "Error loading symbol 'logInfo': " << error2 << std::endl;dlclose(handle);return 1;}// 查找 closeLoggerCloseLoggerFunc closeLogger = (CloseLoggerFunc)dlsym(handle, "closeLogger");const char* error3 = dlerror();if (error3) {std::cerr << "Error loading symbol 'closeLogger': " << error3 << std::endl;dlclose(handle);return 1;}// 使用动态库的功能initLogger("app.log");logInfo("Program started.");logInfo("Doing something important...");closeLogger();// 关闭动态库dlclose(handle);return 0;
}

dlfcn.h中的常用宏

RTLD_LAZY、RTLD_NOW:

是dlopen的可选参数,分别表示懒加载和立即加载动态链接库中的符号。

RTLD_GLOBAL、RTLD_LOCAL:

是dlopen的可选参数分别表示符号的全局可见性和局部可见性。

这些函数和宏可以用于在运行时加载和卸载动态链接库,动态链接库的使用使得程序可以在运行时动态的加载和调用函数,从而使得程序的可扩展性更强。

RTLD_LAZY:懒加载

当使用懒加载打开一个共享库(如.so文件)时,动态连接器会采用一种懒加载的方式,即只在你第一次实际使用某个函数或变量时(通过dlsym得到的符号被实际使用时)才去解析他(解析时只会解析该符号并与之相关的符号,其他符号不参与解析)。而不是在一开始就解析动态库中的所有符号(函数或变量)。就像一本字典,只有当你查某个单词时才去翻找某个单次,而不是一开始就把正本字典读一遍。

  1. 优点:启动更快、更节省资源
  2. 缺点:潜在问题暴露的更晚(只有等到用到这些符号是才会报错)
  3. void* handle = dlopen("./liblazytest.so", RTLD_LAZY);

RTLD_NOW:立即加载

当使用立即加载加载一个共享库时,动态连接器会立即解析共享库中的所有符号(函数和变量),并把它们帮绑定到程序中。他和懒加载不同,他是一种立即绑定的方式。就像一本字典,会立即查找所有单次的意思,并把它们写在笔记本上备用。在调用dlopen时,动态链接器会把共享库里所有的函数、变量都加载好,并检查他们是否有问题,如果某个符号有问题,程序会在启动时直接报错,而不是运行到那个地方才报错。

  1. 优点:更可靠,如果共享库有问题,可以马上发现问题
  2. 缺点:启动变慢,因为在启动时要解析所有符号
void* handle = dlopen("./liblazytest.so", RTLD_NOW);

RTLD_GLOBAL:全局加载

当在dlopen时使用了全局加载这个宏后,加载的共享库里的符号(函数或变量)会被放到一个全局符号表里,这样后续加载的其他共享库也可以直接解析使用这些符号,而不需要重新定义或加载他们。默认情况没有使用RTLD_GLOBAL时,如果使用dlopen加载一个共享库,这个库里的符号(比如函数名)只能被当前加载的这个库本身使用,其他共享库不能直接访问它。如果使用了RTLD_GLOBAL这个共享库会被公开,放到全局符号表里。这样后面加载的其他共享库可以直接使用这些符号,就像他们是公共资源一样。

示例:
如果libB.so中某个函数间接使用了libA.so中的符号,如果我们不使用RTLD_GLOBAL的话,直接通过dlopen打开libB.so动态链接库得到句柄handleB,然后通过dlsym,在handleB中查找某个函数(这个符号包括libA.so中的符号),虽然能正常查到这个函数,但调用这个函数时会报错,所以我们需要先通过dlopen打开libA.so并使用RTLD_GLOBAL宏,然后在使用handleB去查询该函数,此时不会报错。注意:在这种情况下,只有需要被共享的库libA.so需要在dlopen时使用RTLD_GLOBAL。

a.h  b.h

a.h
#pragma once
void hello_from_A();a.cpp
#include <iostream>
#include "a.h"
void hello_from_A() {std::cout << "Hello from libA.so!" << std::endl;
}b.h
#pragma once
void call_hello_from_A();
b.cpp
#include "a.h"
#include "b.h"
void call_hello_from_A() {hello_from_A(); // 调用 libA.so 中的函数
}

编译  libA.sp  libB.so

g++ -fPIC -shared a.cpp -o libA.sog++ -fPIC -shared b.cpp -o libB.so -L. -lA

main

#include <iostream>
#include <dlfcn.h>typedef void (*CallHelloFunc)();int main() {// 第一步:加载 libA.so,使用 RTLD_GLOBALvoid* handleA = dlopen("./libA.so", RTLD_LAZY | RTLD_GLOBAL);if (!handleA) {std::cerr << "Failed to open libA.so: " << dlerror() << std::endl;return -1;}std::cout << "libA.so loaded with RTLD_GLOBAL." << std::endl;// 第二步:加载 libB.sovoid* handleB = dlopen("./libB.so", RTLD_LAZY);if (!handleB) {std::cerr << "Failed to open libB.so: " << dlerror() << std::endl;return -1;}std::cout << "libB.so loaded." << std::endl;// 第三步:查找 call_hello_from_A 函数CallHelloFunc callHello = (CallHelloFunc)dlsym(handleB, "call_hello_from_A");if (!callHello) {std::cerr << "Failed to find call_hello_from_A: " << dlerror() << std::endl;return -1;}// 第四步:调用callHello();// 关闭dlclose(handleB);dlclose(handleA);return 0;
}

正常输出

libA.so loaded with RTLD_GLOBAL.
libB.so loaded.
Hello from libA.so!

 但是如果不在加载 libA.so 时加 RTLD_GLOBAL(比如只写 RTLD_LAZY)

void* handleA = dlopen("./libA.so", RTLD_LAZY); // 去掉 RTLD_GLOBAL

重新编译后运行,结果会是

  • dlsym 查询 call_hello_from_A 成功

  • 但在 callHello() 调用时,程序崩溃或者出现类似下面这种错误

undefined symbol: hello_from_A

原因就是libB.so 里的 call_hello_from_A() 需要用 hello_from_A(),但是因为 libA.so 的符号没有公开到全局符号表,libB.so 找不到

RTLD_LOCAL:本地加载/局部加载

在没有使用RTLD_GLOBAL宏时,它是默认使用的宏。它用来限制动态库的符号范围。使用RTLD_LOCAL加载的动态库,其内部的函数或变量(符号),不会被放入全局符号表,其他动态库无法访问这些符号。当多个动态库中有同名符号是可以避免不必要的冲突或覆盖。如果动态库只需要自身使用,不希望其他库依赖他的符号时,可以使用RTLD_LOCAL.

相关文章:

  • MySQL 的ANALYZE与 OPTIMIZE命令
  • 【基础篇】static_config采集配置详解
  • 《无刷空心杯电机减速机选型及行业发展趋势》
  • 邮件分类特征维度实验分析
  • QT事件Trick
  • 临床试验概述:从定义到实践的关键要素
  • Docker的常用命令
  • 为什么MySQL推荐使用自增主键?
  • 密码杂凑算法HaoLooog512设计原理详解
  • TRex 控制台命令解析
  • C++:BST、AVL、红黑树
  • 【Android】SettingsPreferenceService
  • 网络协议之为什么要分层
  • Mamba2模型的实现
  • 《系统架构 - Java 企业应用架构中的完整层级划分》
  • 大学之大:韩国科学技术研究院2025.4.28
  • 聊一聊接口自动化测试的稳定性如何保障
  • 探秘Transformer系列之(31)--- Medusa
  • 嵌入式RTOS实战:uC/OS-III最新版移植指南(附项目源码)
  • DAY9-USF4.0技术文档笔记
  • 俄宣布停火三天,外交部:希望各方继续通过对话谈判解决危机
  • 发出“美利坚名存实亡”呼号的卡尼,将带领加拿大走向何方?
  • 黄晓丹:用“诗心”找到生存的意义
  • 庆祝中华全国总工会成立100周年暨全国劳动模范和先进工作者表彰大会隆重举行,习近平发表重要讲话
  • 美媒:受关税政策影响,美国电商平台近千种商品平均涨价29%
  • 夜读丨庭院春韵