Linux环境下使用LD_PRELOAD+initfirst时读取环境变量的问题及相关机制疑问
Linux环境下使用LD_PRELOAD+initfirst时读取环境变量的问题及相关机制疑问
我之前也踩过这个坑——给LD_PRELOAD加载的共享库加了-Wl,-z,initfirst后,构造函数里调用getenv()居然返回NULL,当时也是一脸懵。咱们来拆解这个问题,一步步解决:
一、怎么在initfirst的情况下读取环境变量?
因为initfirst会让共享库的构造函数在glibc完成初始化之前就被动态链接器调用,而getenv()依赖glibc维护的__environ变量,这时候__environ还没被赋值,自然拿不到环境变量。针对Linux amd64+gcc+glibc的场景,有两个可靠的办法:
方法1:直接读取/proc/self/environ
Linux的proc文件系统会把进程的环境变量以\0分隔的形式存在/proc/self/environ里,我们可以直接读取这个文件来解析目标环境变量:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> void setup(void) __attribute__((constructor)); void setup(void) { int fd = open("/proc/self/environ", O_RDONLY); if (fd == -1) { fprintf(stderr, "Failed to open /proc/self/environ\n"); return; } char buf[1024]; ssize_t read_len = read(fd, buf, sizeof(buf) - 1); close(fd); if (read_len <= 0) { fprintf(stderr, "Failed to read environment data\n"); return; } buf[read_len] = '\0'; // 遍历环境变量(以\0分隔) char *env_entry = buf; while (*env_entry) { if (strncmp(env_entry, "FOO=", 4) == 0) { fprintf(stderr, "FOO=\"%s\"\n", env_entry + 4); break; } env_entry += strlen(env_entry) + 1; } }
编译和运行命令不变,这次就能正常输出FOO="123"了。唯一要注意的是,如果进程运行在没有挂载proc文件系统的环境(比如特殊chroot场景),这个方法会失效,但一般Ubuntu环境下没问题。
方法2:通过栈直接获取内核传递的envp
Linux内核启动进程时,会把argc、argv、envp依次放在栈上。我们可以通过内联汇编直接从栈上计算出envp的地址,完全不依赖glibc:
#include <stdio.h> #include <string.h> void setup(void) __attribute__((constructor)); // 通过内联汇编获取内核传递的envp char **get_kernel_envp(void) { char **envp; __asm__ volatile ( // amd64栈布局:栈顶是argc,接着是argv指针数组,最后是envp数组 "movq 8(%%rsp), %%rax\n" // 先拿到argv的地址(argc在0(rsp)) "movq (%%rax), %%rcx\n" // 取出argc的值 "leaq 8(%%rax, %%rcx, 8), %%rax\n" // envp = argv + (argc + 1)*8(跳过argc个argv元素+1个NULL) "movq %%rax, %0\n" : "=r"(envp) : : "%rax", "%rcx" ); return envp; } void setup(void) { char **envp = get_kernel_envp(); while (*envp) { if (strncmp(*envp, "FOO=", 4) == 0) { fprintf(stderr, "FOO=\"%s\"\n", *envp + 4); break; } envp++; } }
这个方法更底层,完全不依赖外部文件,在任何标准Linux amd64环境下都能工作。
二、__environ是被谁初始化的?
答案是:glibc的启动代码(crt0),具体流程是这样的:
- 内核启动进程时,把
argc、argv、envp放在进程的栈上,然后跳转到进程的_start入口(这个入口是glibc提供的crt0.o里的代码)。 _start函数会把内核传递的envp地址赋值给glibc全局变量__environ,之后才会调用__libc_start_main完成libc的初始化工作。- 正常情况下(没有
initfirst),共享库的构造函数会在__libc_start_main之后被动态链接器调用,这时候__environ已经初始化完成,getenv()就能正常工作。 - 但加了
initfirst后,动态链接器会跳过libc初始化步骤,优先调用这个共享库的构造函数,这时候__environ还没被赋值,所以getenv()返回NULL。
内容来源于stack exchange
相关产品推荐
相关产品推荐

