为什么 unsetenv() 不修改 /proc/pid/environ?

Flu*_*lup 7 environment-variables glibc

我只是在看这个问题并编写了一个 noddy 程序来演示unsetenv()修改/proc/pid/environ. 令我惊讶的是它没有效果!

这是我所做的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
  printf("pid=%d\n", getpid());
  printf("sleeping 10...\n");
  sleep(10);
  printf("unsetenv result: %d\n", unsetenv("WIBBLE"));
  printf("unset; sleeping 10 more...\n");
  sleep(10);

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

然而,当我跑

WIBBLE=hello ./test_program
Run Code Online (Sandbox Code Playgroud)

然后我WIBBLE在unsetenv()运行前后的环境中看到:

# before the unsetenv()
$ tr '\0' '\n' < /proc/498/environ | grep WIBBLE
WIBBLE=hello
# after the unsetenv()
$ tr '\0' '\n' < /proc/498/environ | grep WIBBLE
WIBBLE=hello
Run Code Online (Sandbox Code Playgroud)

为什么不unsetenv()修改/proc/pid/environ?

Sté*_*las 14

当程序启动时,它接收它的环境作为指向某些字符串的指针数组,格式为var=value. 在 Linux 上,它们位于堆栈的底部。在最底部,您将所有字符串一个接一个地塞入(这就是 中所示的内容/proc/pid/environ)。在上面你有一个指向这些字符串的指针数组(NULL 终止)(这就是char *envp[]你的int main(int argc, char* argv[], char* envp[]),libc 通常会初始化environ为)。

putenv()/ setenv()/ unsetenv(),不修改这些字符串,它们通常不甚至修改指针。在某些系统上,那些(字符串和指针)是只读的。

虽然 libc 通常会初始化char **environ为上面第一个指针的地址,但对环境的任何修改(以及那些用于将来的 exec),通常都会导致创建一个新的指针数组并将其分配给environ.

如果environ是最初[a,b,c,d,NULL],wherea是一个指向x=1、bto y=2、cto z=3、dto的指针q=5,如果你做了一个unsetenv("y"),environ就必须变成[a,c,d,NULL]。在系统中是只读的初始数组列表,一个新的列表将不得不进行分配,并分配到environ并[a,c,d,NULL]在那里保存。下一个时unsetenv(),可以就地修改列表。仅当您执行unsetenv("x")上述操作时,列表才不会被重新分配(environ只能增加到指向&envp[1]。我不知道某些 libc 实现是否实际执行了该优化)。

无论如何,没有理由以任何方式修改存储在堆栈底部的字符串本身。即使一个unsetenv()实现实际上是在原地修改最初在堆栈上接收到的数据,它也只会修改指针,它不会同时删除它们指向的字符串。(这似乎是 GNU libc 在 Linux 系统上所做的(至少带有 ELF 可执行文件),envp只要环境变量的数量不增加,它就会在适当的位置修改指针列表。

您可以使用如下程序观察行为:

#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern char **environ;
int main(int argc, char* argv[], char* envp[]) {
  char cmd[128];
  int i;

  printf("envp: %p environ: %p\n", envp, environ);
  for (i = 0; envp[i]; i++)
    printf("  envp[%d]: %p (%s)\n", i, envp[i], envp[i]);

#define DO(x) x; puts("\nAfter " #x "\n"); \
  printf("envp: %p environ: %p\n", envp, environ); \
  for (i = 0; environ[i]; i++) \
    printf("  environ[%d]: %p (%s)\n", i, environ[i], environ[i])

  DO(unsetenv("a"));
  DO(setenv("b", "xxx", 1));
  DO(setenv("c", "xxx", 1));

  puts("\nAddress of heap and stack:");
  sprintf(cmd, "grep -e stack -e heap /proc/%u/maps", getpid());

  fflush(stdout);
  system(cmd);
}
Run Code Online (Sandbox Code Playgroud)

在带有 GNU libc 的 Linux 上(与 klibc、musl libc 或dietlibc 相同,除了它们使用 mmapped 匿名内存而不是堆来分配内存),当作为 运行时env -i a=1 x=3 ./e,给出(内联注释):

envp: 0x7ffc2e7b3238 environ: 0x7ffc2e7b3238
  envp[0]: 0x7ffc2e7b4fec (a=1)
  envp[1]: 0x7ffc2e7b4ff0 (x=3)
   # envp[1] is almost at the bottom of the stack. I lied above in that
   # there are more things like the path of the executable
   # environ initially points to the same pointer list as envp

After unsetenv("a")

envp: 0x7ffc2e7b3238 environ: 0x7ffc2e7b3238
  environ[0]: 0x7ffc2e7b4ff0 (x=3)
   # here, unsetenv has reused the envp[] list and has not allocated a new
   # list. It has shifted the pointers though and not done the optimisation
   # I mention above

After setenv("b", "xxx", 1)

envp: 0x7ffc2e7b3238 environ: 0x1bb3420
  environ[0]: 0x7ffc2e7b4ff0 (x=3)
  environ[1]: 0x1bb3440 (b=xxx)
   # a new list has been allocated on the heap. (it could have reused the
   # slot freed by unsetenv() above but didn't, Solaris' version does).
   # the "b=xxx" string is also allocated on the heap.

After setenv("c", "xxx", 1)

envp: 0x7ffc2e7b3238 environ: 0x1bb3490
  environ[0]: 0x7ffc2e7b4ff0 (x=3)
  environ[1]: 0x1bb3440 (b=xxx)
  environ[2]: 0x1bb3420 (c=xxx)

Address of heap and stack:
01bb3000-01bd4000 rw-p 00000000 00:00                              [heap]
7ffc2e794000-7ffc2e7b5000 rw-p 00000000 00:00 0                    [stack]
Run Code Online (Sandbox Code Playgroud)

在 FreeBSD(此处为 11-rc1)上,已经在unsetenv(). 不仅如此,字符串本身也被复制到堆上,因此在第一次修改环境后与程序在启动时收到的environ完全断开envp[]:

envp: 0x7fffffffedd8 environ: 0x7fffffffedd8
  envp[0]: 0x7fffffffef74 (x=2)
  envp[1]: 0x7fffffffef78 (a=1)

After unsetenv("a")

envp: 0x7fffffffedd8 environ: 0x800e24000
  environ[0]: 0x800e15008 (x=2)

After setenv("b", "xxx", 1)

envp: 0x7fffffffedd8 environ: 0x800e24000
  environ[0]: 0x800e15018 (b=xxx)
  environ[1]: 0x800e15008 (x=2)

After setenv("c", "xxx", 1)

envp: 0x7fffffffedd8 environ: 0x800e24000
  environ[0]: 0x800e15020 (c=xxx)
  environ[1]: 0x800e15018 (b=xxx)
  environ[2]: 0x800e15008 (x=2)
Run Code Online (Sandbox Code Playgroud)

在 Solaris(此处为 11)上,我们看到了上面提到的优化(unsetenv("a")最终用 a 完成environ++),通过unsetenv()重用 for 来释放插槽b,但当然必须在插入新环境时分配新的指针列表变量 ( c):

envp: 0xfeffef6c environ: 0xfeffef6c
  envp[0]: 0xfeffefec (a=1)
  envp[1]: 0xfeffeff0 (x=2)

After unsetenv("a")

envp: 0xfeffef6c environ: 0xfeffef70
  environ[0]: 0xfeffeff0 (x=2)

After setenv("b", "xxx", 1)

envp: 0xfeffef6c environ: 0xfeffef6c
  environ[0]: 0x806145c (b=xxx)
  environ[1]: 0xfeffeff0 (x=2)

After setenv("c", "xxx", 1)

envp: 0xfeffef6c environ: 0x8061c48
  environ[0]: 0x8061474 (c=xxx)
  environ[1]: 0x806145c (b=xxx)
  environ[2]: 0xfeffeff0 (x=2)
Run Code Online (Sandbox Code Playgroud)

  • @PavelŠimerda,`execve()` 系统调用不保留任何内容,它接收一个执行路径和 2 个字符串列表(一个用于 arg 列表,一个用于 env 列表)。正是 libc 使用它的 `execl`/`execvp`/... _functions_(那些不是系统调用)来保护环境。libc 用它自己的内部汤来维护一个环境变量列表,然后当 `execl()` 等被调用时,它会传递给 `execve()` 系统调用。内核无法知道 libc 将其中间 env 列表保存在哪里。 (5认同)