You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让LD_PRELOAD加载的共享库中代码仅执行一次?

问题:LD_PRELOAD 共享库构造函数重复执行,如何确保代码仅运行一次?

以下代码展示了使用LD_PRELOAD时,同一进程中共享库可能被多次加载(构造函数被多次调用、静态数据被多次初始化):

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>

extern bool initialized = false;

void __attribute__ ((constructor)) setup() {
    printf("constructor %d %d %d\n", getpid(), gettid(), initialized);
    initialized = true;
}

实验步骤:

$ gcc -Wall -shared -fPIC -o libtest.so lib.c

$ env LD_PRELOAD=$PWD/libtest.so sh -c ls
constructor 98928 98928 0
constructor 98928 98928 0
... rest of output of ls

注意PID相同,问题并非由fork导致。initialized变量始终为0,推测是因为每次加载库时,加载器会将静态段放入新的内存区域。虽然将sh -c ls替换为ls可让库仅加载一次,但这种方法不具备通用性。

如何针对任意命令,让LD_PRELOAD加载的共享库中的代码仅执行一次?


解决方案:利用进程级全局标识实现单次执行

1. 文件锁方案(推荐,跨实例可靠)

借助文件系统的排他锁,同一进程内多次加载库时,只有第一个实例能获取锁并执行初始化逻辑:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdbool.h>

void __attribute__ ((constructor)) setup() {
    // 基于PID生成唯一锁文件,避免不同进程冲突
    char lock_path[64];
    snprintf(lock_path, sizeof(lock_path), "/tmp/libtest_%d.lock", getpid());

    int fd = open(lock_path, O_CREAT | O_WRONLY, 0600);
    if (fd == -1) {
        perror("open lock file failed");
        return;
    }

    // 非阻塞方式获取排他锁
    struct flock fl = {
        .l_type = F_WRLCK,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0
    };
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        // 锁已存在,说明初始化已执行过
        close(fd);
        return;
    }

    // 执行核心初始化逻辑
    printf("constructor %d %d\n", getpid(), gettid());

    // 进程退出时锁会自动释放,主动删除锁文件避免残留
    unlink(lock_path);
    close(fd);
}

2. 共享内存方案(轻量级进程全局标记)

通过shm_open创建进程唯一的共享内存区域,用原子操作标记是否已初始化:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdbool.h>

void __attribute__ ((constructor)) setup() {
    // 基于PID生成共享内存名称
    char shm_name[64];
    snprintf(shm_name, sizeof(shm_name), "/libtest_%d", getpid());

    int fd = shm_open(shm_name, O_CREAT | O_RDWR, 0600);
    if (fd == -1) {
        perror("shm_open failed");
        return;
    }

    // 设置共享内存大小为bool类型的长度
    ftruncate(fd, sizeof(bool));
    bool *initialized = mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    if (initialized == MAP_FAILED) {
        perror("mmap failed");
        shm_unlink(shm_name);
        return;
    }

    // 原子检查并设置标记,确保只有第一个实例执行初始化
    if (__atomic_test_and_set(initialized, __ATOMIC_SEQ_CST)) {
        munmap(initialized, sizeof(bool));
        shm_unlink(shm_name);
        return;
    }

    // 执行核心初始化逻辑
    printf("constructor %d %d\n", getpid(), gettid());

    // 主动清理共享内存,进程退出时也会自动释放
    shm_unlink(shm_name);
    munmap(initialized, sizeof(bool));
}

3. 加载场景识别方案(针对动态加载器行为)

部分场景下库重复加载是因为进程内部调用dlopen重新加载,此时可通过getauxval判断是否为进程启动时的初始加载:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/auxv.h>
#include <stdbool.h>

static bool is_initial_load() {
    // 初始加载时AT_EXECFN会指向进程可执行文件路径,后续dlopen加载时该值可能无效
    const char *exec_path = (const char *)getauxval(AT_EXECFN);
    return exec_path != NULL;
}

void __attribute__ ((constructor)) setup() {
    if (!is_initial_load()) {
        return;
    }

    // 执行核心初始化逻辑
    printf("constructor %d %d\n", getpid(), gettid());
}

原理说明

问题本质是:当进程通过dlopen(比如sh -c内部的动态加载逻辑)重复加载同一个共享库时,加载器会创建新的库实例,每个实例拥有独立的静态数据段,因此原代码中的initialized无法跨实例生效。

上述方案通过进程全局的外部状态(文件锁、共享内存)或加载场景识别,实现了跨库实例的执行控制,确保初始化逻辑仅运行一次。

内容的提问来源于stack exchange,提问作者charmoniumQ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 17:10:07