程序如何继承环境变量?

How does a program inherit environment variables?

当我使用标准 C 库中的函数 getenv() 时,我的程序从其父程序继承了环境变量。

示例:

$ export FOO=42
$ <<< 'int main() {printf("%s\n", getenv("FOO"));}' gcc -w -xc - && ./a.exe
42

在libc中,environ变量声明为environ.c。我希望它在执行时为空,但我得到 42.

更进一步getenv可以简化如下:

char * getenv (const char *name)
{
    size_t len = strlen (name);
    char **ep;
    uint16_t name_start;

    name_start = *(const uint16_t *) name;
    len -= 2;
    name += 2;

    for (ep = __environ; *ep != NULL; ++ep)
    {
        uint16_t ep_start = *(uint16_t *) *ep;

        if (name_start == ep_start && !strncmp (*ep + 2, name, len)
                && (*ep)[len + 2] == '=')
            return &(*ep)[len + 3];
    }
    return NULL;
}
libc_hidden_def (getenv)

这里我只获取__environ变量的内容。但是我从来没有初始化它。

所以我很困惑,因为 environ 应该是 NULL 除非我的主要功能不是我程序的真正入口点。也许 gcc 通过添加属于标准 C 库的 _init 函数来吸引我。

environ在哪里初始化的?

调用您的程序(您的shell)的父进程定义了 FOO。新创建的进程从父进程接收副本。

这里没有什么神秘的。

首先,shell 分叉。分叉进程显然具有相同的环境。然后在child中执行一个新的程序。有问题的系统调用是 execve,除其他外,它接受指向环境的指针。

那么,执行二进制文件后设置什么环境完全取决于执行执行的代码。

所有这些都可以通过 运行 strace 轻松查看。

编辑: 因为问题被编辑为询问 environ

当您执行动态链接的二进制文件时,执行任何操作的第一个用户空间代码都来自加载程序。除其他外,加载程序设置变量,如 argcargvenviron,然后才从二进制文件调用 main()

再一次,所有这些资源都是免费提供的。虽然 glibc 的源代码由于格式不当而难以阅读,但 BSD 的源代码很容易并且在概念上足够等同。

http://code.metager.de/source/xref/freebsd/libexec/rtld-elf/rtld.c#389

真正的问题是,shell 运行 是如何命令的?

答案是可能使用 fork()execl() 创建一个新进程,这将创建一个与当前进程具有相同环境的进程。

但是,您可以使用 execvpe()/execle().

创建具有自定义环境的新进程

但是在任何正常情况下都没有必要,特别是因为许多程序都希望定义一些环境变量,例如 PATH,通常子进程会从环境中继承环境变量它被调用。

环境变量作为第三个参数从父进程传递给main。发现这一点的最简单方法是阅读系统调用 execve 的文档,尤其是这一点:

int execve(const char *filename, char *const argv[], char *const envp[]);

Description

execve() executes the program pointed to by filename. [...] argv is an array of argument strings passed to the new program. By convention, the first of these strings should contain the filename associated with the file being executed. envp is an array of strings, conventionally of the form key=value, which are passed as environment to the new program. Both argv and envp must be terminated by a NULL pointer. The argument vector and environment can be accessed by the called program's main function, when it is defined as:

int main(int argc, char *argv[], char *envp[])

C 库在调用 main 之前将 envp 参数复制到其启动代码中某处的 environ 全局变量中:例如,GNU libc 在 _init and musl libc does it in __init_libc. (You may find musl libc's code easier to trace through than GNU libc's.) Conversely, if you start a program using one of the exec 包装函数 采用显式环境向量,C 库提供 environ 作为 execve 的第三个参数。因此,环境变量的继承是严格的 user-space 约定。就内核而言,每个程序接收两个参数向量,它并不关心它们中的内容。

(请注意,三参数 main 是 C 语言的扩展。C 标准仅指定 int main(void)int main(int argc, char **argv) 但它允许实现定义其他形式(C11 Annex J.5.1 Environment Arguments). The three-argument main has been how environment variables work since Unix V7 if not longer, and is documented by Microsoft too — see What should main() return in C and C++?.)

在 Linux 下,当程序启动时,它的参数和环境变量存储在堆栈中。对于 C 程序,在 main 之前执行的代码查看此代码,生成 argvenvp 指针数组,然后使用这些值调用 main(和 argc).

当程序调用 execvpe 变成一个新程序时(通常在调用 fork 之后),然后传入 envp 以及 argv。内核会将这些数据复制到新程序的堆栈中。

当调用任何其他 exec 函数时,glibc 会将当前程序的 environ 作为新程序的 envp 传递给 execvpe(或直接至 sys_exec).