深入理解CC++编译技术7:符号缺失行为与运行时动态加载

深入理解C/C++编译技术——动态库A4:链接时符号缺失行为与运行时动态加载

这一篇博客会更加重要一些,这里我们计划讨论的是各个平台上(Windows和GNU/Linux),当我们的可执行文件生成或者是其他库文件依赖的符号存在未定义时,不同平台的表现;以及比较重要的动态库动态加载编程。

链接时符号缺失行为的平台差异

这个很有趣,我们讨论的时在链接发生的时候,平台之间对存在未定义符号的容忍程度分析。在Windows上,动态库生成的时候,我们就已经要求不允许存在未定义符号,一旦发生未定义的符号,我们的工具链就会抱怨道找不到符号。

而在Linux上不会存在这样的事情。事实上,Linux的策略更加宽容,默认的情况下,我们允许符号未定义,直到上进程的时候,加载器会检查所有的依赖确保所有的重要符号都是被正确编址的。直到那个时候才会确认我们的程序是否真的存在重要的问题。

当然,如果您希望这种很严格的检查,有办法的:那就是在编译可重定位文件的时候传递-Wl,-no-undefined选项,来指导后续的链接器的报错行为即可。

运行时动态加载是什么?

官方的说,运行时动态链接(dynamic loading)指程序在运行时按需加载一个共享库(shared object / dynamic library / DLL),并查找需要的符号(函数、变量)后调用。笔者认为,**这是插件系统的一个重要的实现机制。**因为现在:

  • 我们可以动态的加载进入插件,在运行时根据配置加载不同功能模块(国际化、渲染后端、驱动等)。
  • 上述特性允许我们可以按照需求加载我们需要的依赖,节约一部分空间
  • 并且可以在运行时就支持热替换/扩展,至少,我们无需重编译主程序就可以扩展功能了。

好处多多,有麻烦嘛?

还真有,我们的错误处理要更加的小心了,毕竟,我们会有类似——符号对不上,加载失败了等一系列麻烦的问题,以及建议搞一个统一的管理类处理这些导出的符号,这是有原因的——插件好就好在随时可以安装和卸载,卸载之后,我们一定不能继续调用其函数或访问其静态资源。笔者认为可以搞一个类似QPointer那种带有Expire机制的函数包装对象访问之。

一些系统层次的API

这里枚举一部分系统层次的API

  • void *dlopen(const char *filename, int flag);
    • flag 常用:RTLD_LAZY(延迟解析符号)、RTLD_NOW(立即解析所有需要符号)、RTLD_LOCAL(符号本地)、RTLD_GLOBAL(符号可被随后加载的库解析)
  • void *dlsym(void *handle, const char *symbol); 返回指向函数/变量的指针
  • int dlclose(void *handle); 卸载
  • char *dlerror(void); 获取错误说明(非线程安全的实现可能返回静态字符串)

Windows 对应:

最小 C 动态库 + 程序(Linux) — C 风格函数导出

举个例子,笔者编写了一个简单的动态库

// mylib.c
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

const char *hello(void) {
    return "Hello from mylib";
}

在Linux下,我们这样构建动态库

# 生成共享库
gcc -fPIC -shared -o libmylib.so mylib.c
# 编译主程序(下面会用 dlopen)
gcc -o main main.c -ldl

随后编写一个使用的main.c来处理之:

// main.c
#include <stdio.h>
#include <dlfcn.h>

int main(void) {
    /* Pass here a valid path */
    /* So place the dynamic library same place */
    void *h = dlopen("./libmylib.so", RTLD_NOW); 
    if (!h) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return 1;
    }

    // 查找 symbol
    int (*add)(int,int) = (int(*)(int,int))dlsym(h, "add");
    const char *(*hello)(void) = (const char*(*)(void))dlsym(h, "hello");
    char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym error: %s\n", err);
        dlclose(h);
        return 1;
    }

    printf("add(2,3) = %d\n", add(2,3));
    printf("%s\n", hello());

    dlclose(h);
    return 0;
}

运行

# 确保当前目录可被加载(或设置 LD_LIBRARY_PATH)
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main

Windows 下的 DLL 和 LoadLibrary(MinGW / MSVC)

mylib.c(Windows DLL)

// mylib.c
#include <windows.h>

__declspec(dllexport) int add(int a, int b) {
    return a + b;
}

__declspec(dllexport) const char* hello(void) {
    return "Hello from mylib.dll";
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
    return TRUE;
}

构建(MSVC Developer Command Prompt)

cl /LD mylib.c /Fe:mylib.dll

构建(MinGW)

gcc -shared -o mylib.dll -Wl,--out-implib,libmylib.a -Wl,--export-all-symbols -fPIC mylib.c

main.c(使用 LoadLibrary)

// main_win.c
#include <windows.h>
#include <stdio.h>

typedef int (*add_t)(int,int);
typedef const char* (*hello_t)(void);

int main(void) {
    HMODULE h = LoadLibraryA("mylib.dll");
    if (!h) {
        DWORD e = GetLastError();
        printf("LoadLibrary failed: %lu\n", e);
        return 1;
    }

    add_t add = (add_t)GetProcAddress(h, "add");
    hello_t hello = (hello_t)GetProcAddress(h, "hello");
    if (!add || !hello) {
        printf("GetProcAddress failed\n");
        FreeLibrary(h);
        return 1;
    }
    printf("add(10,20) = %d\n", add(10,20));
    printf("%s\n", hello());

    FreeLibrary(h);
    return 0;
}

运行(在 DLL 同目录下或把 DLL 加到 PATH)

set PATH=%CD%;%PATH%
main_win.exe

C++ 插件接口与 extern “C” 工厂(推荐做法)

当需要导出 C++ 对象或类时,常见策略是导出一个工厂函数(extern "C")返回不透明指针,或导出一张 struct 的函数表(接口表),避免 C++ 名字修饰影响。

// plugin.h
#ifdef __cplusplus
extern "C" {
#endif

typedef struct PluginAPI {
    int (*init)(void);
    void (*shutdown)(void);
    int (*do_work)(int arg);
} PluginAPI;

// 导出工厂:返回函数表指针
PluginAPI* create_plugin_api(void);

#ifdef __cplusplus
}
#endif

plugin_impl.c(插件实现)

// plugin_impl.c
#include "plugin.h"
#include <stdio.h>

static int my_init(void) { printf("plugin init\n"); return 0; }
static void my_shutdown(void) { printf("plugin shutdown\n"); }
static int my_do_work(int arg) { printf("plugin do work %d\n", arg); return arg*2; }

static PluginAPI api = {
    .init = my_init,
    .shutdown = my_shutdown,
    .do_work = my_do_work
};

PluginAPI* create_plugin_api(void) {
    return &api;
}

主程序只需通过 dlsym(h, "create_plugin_api") 拿到 PluginAPI*,就能无缝调用插件函数,无需关心 C++ 名字修饰。

笔者遇到的一些问题,和笔者使用的排查手段积累

为什么 dlsym 拿不到我在 C++ 中的函数?

笔者当时手搓PDF浏览器,然后准备做插件系统的时候,被干过,我在之前的博客中谈到C++ 编译器会对符号名进行修饰(name mangling)。自然解决方案就是用 extern "C" 导出 C 风格接口,或者是笔者说的上面的方案。

Windows 的 GetProcAddress 失败怎么排查?

检查导出名称(使用 dumpbin /EXPORTSnm),检查调用约定是否匹配(__stdcall 会改变导出名),或是否使用了 C++ 名称修饰。建议 __declspec(dllexport) + extern "C"

### 问题分析 在交叉编译 `libevent` 的过程中,如果遇到无法找到 `.libs/libevent.so` 的情况,通常是因为以下几个原因: 1. **未完成构建过程**:可能由于某些配置选项不正确或者依赖缺失,导致共享库未能成功生成。 2. **目标架构差异**:交叉编译的目标平台主机环境不同步可能导致工具链或路径设置错误。 3. **权限不足**:文件写入失败可能是由于目录权限不够。 以下是针对该问题的具体解决方案以及相关说明。 --- ### 解决方案 #### 方法一:重新运行 `make` 并确认输出 确保已经完成了完整的构建流程。可以尝试清理之前的构建缓存并重新执行以下命令: ```bash ./configure --host=<target_arch> CC=<cross_compiler> make clean make ``` 其中 `<target_arch>` 是目标体系结构(如 arm、mips),`<cross_compiler>` 是指定的交叉编译器路径[^1]。 通过观察 `make` 输出日志来验证是否存在任何警告或错误消息提示关于 `.libs/libevent.so` 创建失败的信息。 #### 方法二:检查安装前后的文件存在状态 有即使生成了临版本的 `.so` 文件,在正式安装之前它们可能会被移动到其他地方。因此建议先定位这些中间产物的位置后再决定如何处理: ```bash find ./ -name "*.so" ``` 假如发现实际生成的是另一个名字比如 `libevent_core.so` 或者类似的变体形式,则需调整后续链接阶段使用的名称匹配逻辑[^2]。 #### 方法三:手动创建软连接 当确实缺少某个特定命名模式下的动态库,可以通过建立符号链接的方式解决问题。例如假设最终产品位于 `/path/to/output/libevent-2.x.y.so.z` 下面的话,那么就可以这样做: ```bash ln -sf /path/to/output/libevent-2.x.y.so.z ./.libs/libevent.so ``` 注意替换掉上面例子中的具体数值部分以适应实际情况需求[^3]。 #### 方法四:修改 Makefile 中的相关定义 如果以上方法均不可行,还可以考虑直接编辑项目的Makefile文件,强制设定 LIBS 变量指向正确的预编译成果地址。像这样操作: ```makefile LIBS += -L/path/to/prebuilt/libs -levent ``` 之后再次调用 make 工具继续加工剩余环节即可[^4]。 #### 方法五:更新 ldconfig 缓存 对于某些特殊场景而言,仅仅依靠本地项目内部结构调整还不够充分,还需要同步通知操作系统级别的加载机制认识新增加的内容项。所以最后一步不妨试试刷新全局范围内的共享库索引表单记录: ```bash sudo ldconfig ``` 这有助于消除潜在遗留影响因素干扰正常运作效果评估进程[^5]。 --- ### 注意事项 在整个排查修复期间务必保持耐心细致的态度对待每一个细节变化迹象捕捉工作,并且随准备回滚至初始状态以便于对比测试结果有效性判断依据更加清晰明了。 --- ### 示例代码片段 下面给出一段简单的脚本用于辅助自动化检测整个环境中涉及到的关键要素是否齐全完好无损状况良好程度评价标准参考指南如下所示: ```python import os def check_libevent(): paths = [ "./.libs", "/usr/local/lib", "/opt/cross-toolchain/<arch>/sysroot/usr/lib" ] for p in paths: so_file = os.path.join(p, 'libevent.so') if os.path.exists(so_file): print(f"Found {so_file}") return True print("No libevent.so found!") return False if __name__ == "__main__": result = check_libevent() exit(0 if result else 1) ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值