如何让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
相关产品推荐
相关产品推荐

