程序如何inheritance环境变量?
当我使用标准C库中的函数getenv()
,我的程序从其父级inheritance环境变量。
例:
$ 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
除非我的main函数不是我程序的真正入口点。 也许gcc
通过添加一个_init
函数来引导我,它是标准C库的一部分。
environ
初始化在哪里?
这里没有神秘感。
首先,shell分叉。 分叉过程显然具有相同的环境。 然后在孩子中执行新程序。 所讨论的系统调用是execve
,其中包括指向环境的指针。
那么,在执行二进制文件之后设置什么环境完全取决于执行exec的代码。
所有这一切都可以通过运行strace轻松看出。
编辑:自编辑问题以询问environ
:
当您执行动态链接的二进制文件时,执行任何操作的第一个用户空间代码都来自加载程序。 加载器和其他东西设置变量,如argc
, argv
或environ
,然后才从二进制文件中调用main()
。
再一次,所有这些的来源都是免费提供的。 虽然由于粗糙的格式化而使glibc的源代码难以读取,但BSD很容易在概念上等同。
http://code.metager.de/source/xref/freebsd/libexec/rtld-elf/rtld.c#389
环境变量从父进程向下传递,作为main
的第三个参数 。 发现这个的最简单方法是阅读系统调用execve
的文档,特别是这一点:
int execve(const char *filename, char *const argv[], char *const envp[]);
描述
execve()
执行filename
指向的程序。 […]argv
是传递给新程序的参数字符串数组。 按照惯例,这些字符串中的第一个应包含与正在执行的文件关联的文件名。envp
是一个字符串数组,通常格式为key=value
,它们作为环境传递给新程序。argv
和envp
必须由NULL指针终止。 参数向量和环境可以被被调用程序的main函数访问,当它被定义为:int main(int argc, char *argv[], char *envp[])
在它调用main
之前,C库将envp
参数复制到其启动代码中的environ
全局变量中:例如,GNU libc在_init
执行此操作,而musl libc在__init_libc
执行此__init_libc
。 (你可能会发现musl libc的代码比GNU libc更容易跟踪。)相反,如果你使用一个不采用显式环境向量的exec
包装函数启动一个程序,那么C库提供environ
作为第三个参数execve
。 因此,环境变量的inheritance严格地是用户空间约定。 就内核而言,每个程序都接收两个参数向量,而不关心它们中的内容。
(注意,三参数main
是C语言的扩展.C标准只指定int main(void)
和int main(int argc, char **argv)
但它允许实现定义其他forms( C11 Annex J. 5.1环境参数 )。三个参数main
是自Unix V7以来环境变量如何工作,如果不是更长,并且也由Microsoft记录 – 请参阅C和C ++中main()
应该返回什么?
在Linux下程序启动时,它的参数和环境变量存储在堆栈中。 对于C程序,在main
之前执行的代码查看此代码,生成指针的argv
和envp
数组,然后使用这些值(和argc
)调用argc
。
当一个程序调用execvpe
变成一个新程序时(通常在调用fork
),然后传入一个envp
和一个argv
。 内核会将这些数据复制到新程序的堆栈中。
当调用任何其他exec
函数时,glibc将作为新程序的envp
execvpe
(或直接传递给sys_exec)传入当前程序的environ
。
问题是,shell如何运行命令?
答案是通过使用fork()
和execl()
创建一个新进程,它创建一个与当前进程具有相同环境的进程。
但是,您可以使用execvpe()
/ execvpe()
创建具有自定义环境的新进程。
但是在任何不必要的正常情况下,特别是因为许多程序期望某些环境变量被定义为例如PATH
,所以通常子进程从调用它的环境inheritance环境变量。
调用程序(您的shell)的父进程定义了FOO。 新创建的进程从父进程接收副本。