我很难理解操作系统如何将数据从父进程的地址空间传递到子进程的地址空间。也就是说,在 C 程序中,argc 和 argv 在传递给 main 时存储在哪里?
我了解 argv 本质上是一个双指针。我不明白的是操作系统在将这些值加载到内核后对它们做了什么。在为子进程创建地址空间后,它是否会将这些值推送到新空间的堆栈中?我们显然不想传递指向另一个地址空间的指针。
作为记录,我正在使用 MIPS32 架构。
我很难理解操作系统如何将数据从父进程的地址空间传递到子进程的地址空间。也就是说,在 C 程序中,argc 和 argv 在传递给 main 时存储在哪里?
我了解 argv 本质上是一个双指针。我不明白的是操作系统在将这些值加载到内核后对它们做了什么。在为子进程创建地址空间后,它是否会将这些值推送到新空间的堆栈中?我们显然不想传递指向另一个地址空间的指针。
作为记录,我正在使用 MIPS32 架构。
在 Linux 上,至少在我使用过的架构上,该过程从%esp
指向以下内容开始:
argc | argv[0] | argv[1] | ... argv[argc - 1] | argv[argc] == NULL | envp[0] | envp[1] ... envp[?] == NULL
调用的第一个函数传统上命名为_start
,它的工作是计算(argc = %esp, argv = ((char *)%esp) + 1, envp = ((char *)%esp) + argc + 2)
,然后main
使用适当的调用约定进行调用。
在 x86 上,参数在堆栈上传递。
在 amd64 上,它们通过寄存器%rdi
、%rsi
和%rdx
.
在 mips 上,Google 告诉我使用了几种不同的调用约定——包括 O32、N32、N64——但它们都首先使用$a0
, 。$a1
$a2
对于不同的操作系统,该过程是不同的,并且确实根据创建新进程的方式而有所不同。由于我更熟悉现代 Microsoft 操作系统如何处理此问题,因此我将从那里开始,并在最后参考 nix 的。
当 [Microsoft] 操作系统创建一个进程时,它会分配一个进程环境块来保存特定于该进程的数据。其中包括调用程序的命令行参数。这个进程环境块被分配到目标进程的地址空间之外,并且指向它的指针被提供给进程的入口点。子进程的进程环境块通常通过将父进程的环境块复制到新进程的地址空间来初始化 - 不涉及直接共享内存。
对于基于 C 的程序,入口点不是main()
程序员提供的函数。相反,它是 C 运行时库提供的一个例程,负责在将控制权交给程序员的main()
.
有很多东西要初始化,但其中一个方面是设置argc和argv值。为此,运行时将查询进程环境块以查找调用它的程序名称和参数。然后它将(通常)从进程堆中为 argv 分配值(即,使用类似的东西malloc()
),并将 argc 分配给找到的参数数量(加一,作为程序名称)。
argc 和 argv 的实际值被压入堆栈,就像在 C 中传递的任何其他参数一样,因为main()
C 运行时的调用只是一个普通的函数调用。
因此,当您在子进程中编写的代码main()
访问 argv 时,它将从自己的进程堆中读取值。这些值的来源是进程环境块(由操作系统存储在本地地址空间中),最初是通过从父进程复制进程环境块来初始化的。
在 *nix 平台上,情况有很大不同。当前讨论的主要区别在于,nix 会将新进程的命令行参数直接存储到进程初始线程的堆栈空间中。(它还在此处存储环境变量。)因此,在 *nix 上,main
使用指向存储在堆栈本身中的值的 argv 参数调用。
您可以在execve的手册页中收集其中的一些内容,而Michael Kerrisk的Linux 编程接口在第 6.4 节中有很好的描述,您可能会在网上找到摘录。