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>    // 引入动态链接相关API

typedef 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();

    // 查找 initLogger
    InitLoggerFunc initLogger = (InitLoggerFunc)dlsym(handle, "initLogger");
    const char* error1 = dlerror();
    if (error1) {
        std::cerr << "Error loading symbol 'initLogger': " << error1 << std::endl;
        dlclose(handle);
        return 1;
    }

    // 查找 logInfo
    LogInfoFunc logInfo = (LogInfoFunc)dlsym(handle, "logInfo");
    const char* error2 = dlerror();
    if (error2) {
        std::cerr << "Error loading symbol 'logInfo': " << error2 << std::endl;
        dlclose(handle);
        return 1;
    }

    // 查找 closeLogger
    CloseLoggerFunc 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.so

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

main

#include <iostream>
#include <dlfcn.h>

typedef void (*CallHelloFunc)();

int main() {
    // 第一步:加载 libA.so,使用 RTLD_GLOBAL
    void* 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.so
    void* 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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值