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

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

我只是在看这个问题并编写了一个 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;
}

然而,当我跑步时

WIBBLE=hello ./test_program

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

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

答案1

当程序启动时,它会以指向某些字符串的指针数组的形式接收其环境,格式为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为上面第一个指针的地址,但对环境的任何修改(以及那些用于未来执行的修改)通常会导致创建一个新的指针数组并将其分配给environ.

如果environ是最初[a,b,c,d,NULL],哪里a是一个指向x=1bto y=2cto z=3dto的指针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);
}

在带有 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]

在 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)

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

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)

相关内容