理解链接动态库:二进制的调库

理解链接动态库:二进制的调库

前情提要

之前做的 rev 题一般动态库都是直接在编译时 link 好了动态库,从反编译代码里看不出来调用的过程

机缘巧合仔细了解一下才发现二进制的调库原来跟 python, js 这种语言的依赖引用是很像的,把库以二进制 dll/so 的形式放在 path 下面,让系统能找到,然后用路径和名称调用符号就行了。

基本方式1 - 显式连接 dlopen

runtime linking / manually linking

通过 dlopen 可以打开任意动态库,然后通过函数指针使用里面的函数。

int main() {
    // 加载共享库,RTLD_LAZY 表示延迟解析符号(只有当实际调用时才解析)
    void *handle = dlopen("libm.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen error: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    // 清除之前的错误
    dlerror();

    // 从库中查找符号,例如查找数学库中的 cos 函数
    double (*cos_func)(double) = (double (*)(double)) dlsym(handle, "cos");
    char *error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym error: %s\n", error);
        dlclose(handle);
        exit(EXIT_FAILURE);
    }

    // 调用动态加载的函数
    double result = cos_func(2.0);
    printf("cos(2.0) = %f\n", result);

    // 使用完毕后关闭共享库
    dlclose(handle);
    return 0;
}

基本方式 2 - 隐式链接 compile + link + ld

load time linking

在编译和链接阶段已经将动态库的依赖信息写入到可执行文件中,运行时,操作系统的动态链接器会自动加载这些库并解析其中的符号。

程序启动时,系统加载器(例如 /lib/ld-linux.so.*)自动读取 ELF 文件中的依赖信息,并加载所有必需的动态库。符号解析和重定位工作由动态链接器完成,开发者在代码中直接调用库函数即可

加载器如何识别依赖的动态库?链接器(通常是 ld)会在编译器把依赖的共享库的信息写入到最终生成的 ELF 文件中。这个信息主要记录在动态段 .dynamic 里,其中有一个重要的字段 DT_NEEDED,用于列出程序运行时所需要加载

衍生方式 3 - 弱链接 weak linking

弱链接允许在编译阶段声明某个符号为“可选”的(或说“弱”),这样在运行时即使目标动态库中不存在该符号,程序也不会链接失败。开发者可以在运行时检测该符号是否为 NULL,然后决定是否调用或使用备用实现。当应用需要兼容不同版本的库,某些新版本的 API 在旧版本中可能不存在时很有用。也可以用于实现缺失时回退到默认行为或其他实现。

在 GCC 中,可以这样声明一个函数为弱符号:

extern void some_optional_function() __attribute__((weak));

int main() {
    if (some_optional_function) {
        some_optional_function();
    } else {
        // 使用备用方案
    }
    return 0;
}

这样,如果链接的库中没有 some_optional_function,程序依然可以运行,并根据检测结果选择调用。

衍生方式 4 - dlmopen 加载到独立命名空间

防止名称冲突的衍生系统调用

 // LM_ID_NEWLM 表示在一个新的命名空间中加载
    void *handle = dlmopen(LM_ID_NEWLM, "libexample.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlmopen error: %s\n", dlerror());
        return -1;
    }
    double (*cos_func)(double) = (double (*)(double)) dlsym(handle, "cos");
    ...
}

调试所有符号

#include <stdio.h>
#include <dlfcn.h>

int main() {
    // 使用 RTLD_DEFAULT 搜索全局符号
    void (*printf_ptr)(const char*, ...) = dlsym(RTLD_DEFAULT, "printf");
    if (printf_ptr) {
        printf_ptr("Found printf via RTLD_DEFAULT\n");
    } else {
        fprintf(stderr, "Symbol not found: %s\n", dlerror());
    }
    return 0;
}

补充知识

1. 动态库的创建与编译

动态库的生成步骤

# 编译为位置无关代码(-fPIC)
gcc -c -fPIC mylib.c -o mylib.o

# 生成共享库(-shared)
gcc -shared -o libmylib.so mylib.o

# 隐式链接时,编译主程序需指定链接库路径和名称
gcc main.c -L. -lmylib -o main
  • -fPIC:生成位置无关代码(Position-Independent Code),确保动态库可被加载到任意内存地址。
  • -shared:指示生成共享库(.so 文件)。
  • -L.:添加库搜索路径(此处为当前目录)。
  • -lmylib:链接名为 libmylib.so 的库。

2. 动态库的搜索路径

系统按以下顺序查找动态库:

  1. 编译时指定的 -rpath-rpath-link 路径。
  2. 环境变量 LD_LIBRARY_PATH 中的路径。
  3. /etc/ld.so.cache 中缓存的路径(通过 ldconfig 更新)。
  4. 默认路径:/lib, /usr/lib, /lib64, /usr/lib64 等。

示例:临时添加库路径:

export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH
./main

3. 符号可见性控制

默认情况下,动态库会导出所有全局符号。可通过编译选项或源码属性限制符号导出,避免污染全局命名空间。

方法1:编译时隐藏符号
使用 -fvisibility=hidden,然后显式导出需要的符号:

// 在源码中声明导出符号
__attribute__((visibility("default"))) void public_func() { ... }

方法2:版本脚本
使用链接器脚本 libmylib.version 控制符号可见性:

gcc -shared -o libmylib.so mylib.o -Wl,--version-script=libmylib.version

脚本内容:

VERS_1.0 { 
  global: public_func; 
  local: *; 
};

4. 动态库的版本控制

通过 soname(Shared Object Name)管理兼容性:

# 编译时指定 soname
gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0 mylib.o

# 创建软链接供程序查找
ln -s libmylib.so.1.0 libmylib.so.1
ln -s libmylib.so.1 libmylib.so
  • 主版本号(Major)libmylib.so.1,表示二进制兼容性。
  • 次版本号(Minor)libmylib.so.1.0,表示新增功能但兼容。
  • 发布版本(Release):可选,如 libmylib.so.1.0.0

5. 动态库的初始化与清理函数

可在库中定义构造函数(加载时执行)和析构函数(卸载时执行):

__attribute__((constructor)) void init() {
    printf("Library loaded!\n");
}

__attribute__((destructor)) void cleanup() {
    printf("Library unloaded!\n");
}

6. 调试工具

  • ldd:查看程序的动态库依赖关系。
    ldd ./main
    
  • nm:列出库中的符号。
    nm -D libmylib.so
    
  • readelf:查看 ELF 文件详细信息。
    readelf -d libmylib.so  # 查看动态段(DT_NEEDED 等)
    
  • LD_DEBUG:启用动态链接器的调试输出。
    LD_DEBUG=files,libs ./main
    

7. 安全性注意事项

  • LD_PRELOAD:强制优先加载指定库,可用于劫持函数(慎用)。
    LD_PRELOAD=/path/to/malicious.so ./main
    
  • 符号冲突:若两个动态库导出同名符号,先加载的符号会被使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值