main()函数如何接收命令行参数?

4

使用标准C++入口:

int main(int argc, char *argv[])
{
    // stuff
}
argv 是如何被填充的?编译器无法知道要为数组分配多大的空间,我认为操作系统负责将附加参数传递给程序,但是它们是如何传递到 main 的呢?指针数组是在哪里初始化的?是编译器创建并注入到程序启动序列中的函数吗?
这是我一直以来都理所当然的事情,在今天的一个问题上,我并不确定额外的参数最终是如何被 main 接收的,更不用说像在 CPython 中作为 sys.argv 传递给任何程序了。
奖励问题:操作系统如何处理命令行参数?显然 CLI(或 shell)知道如何解析字符串序列,但是额外的参数是如何“输送到”可执行文件中的?编译器是否添加了某些功能来从 stdin (缓冲区)读取并相应地解析参数,然后再传递给 main

1
相关:命令行参数是如何工作的? - Brian61354270
@JeremyFriesner 这可能回答了操作系统如何解析命令行并存储它们的问题,但它并没有回答操作系统如何将它们注入二进制可执行文件以传递给 main 的问题。我猜测操作系统运行可执行文件,将程序输出到内存中,然后可以访问 main 以传递数组。 - user8087992
@madeslurpy 操作系统不修改可执行文件,但它会设置可执行文件运行的内存空间。在该设置中分配和填充 argv 数组是其中的一部分,这是在调用 main() 之前完成的。 - Jeremy Friesner
@JeremyFriesner 很好,这就解释了为什么它被传递到主函数中并且 argc 在那里,但我仍然很好奇它是如何到达 main 的。 - user8087992
让我们在聊天中继续这个讨论 - Jeremy Friesner
显示剩余4条评论
3个回答

4
让我们以Linux x86-64为例。当进程调用execv("/my/prog", args)时,它会向内核发出系统调用。内核使用args指针在进程的内存中定位参数字符串,将它们复制到其他地方进行临时保存,然后撤销进程的虚拟内存。然后,它设置新程序的虚拟内存,并从其二进制文件/new/prog中加载其代码和数据(实际上只是映射,但这不重要)。
它还分配一块内存作为新程序的堆栈,并将命令行参数以及需要传递给新程序的环境变量和各种其他数据复制到其中。在这里,它还设置了指向程序堆栈内部字符串本身的argv指针数组,并将参数计数也压入堆栈。精确的布局在ABI中说明,参见图3.9。
现在要实际启动程序了。二进制文件的头指定一个地址作为入口点。连接器将已安排好的指向特殊的启动代码。该代码通常随您的标准C库一起提供,在名为crt0.o的对象文件中。它用汇编语言编写,其作业是处理命令行参数等内容,设置符合编译后的C或C ++代码要求的寄存器和内存,并调用标准库中的C / C ++函数进行进一步初始化,然后调用您的main。内核跳转到入口点地址并在路上切换到非特权模式,启动代码开始执行。
您可以在start.S中查看glibc的版本,但极简版本可能如下所示。
; main takes argc in rdi and argv in rsi

; bottom of stack contains argument count
mov rdi, [rsp]

; next is start of the argument pointer array
lea rsi, [rsp+8]

call main

; main returns, exit the program
mov rdi, rax
call exit
; exit() makes an exit system call and doesn't return

因此,当控制流程实际到达您的main函数时,寄存器中的值与通过另一个C++函数调用它时相同。 argv参数指向栈上的指针数组,其中每个指针指向堆栈内存中更高处的字符串,由内核设置。


1

对于Linux ELF进程:

它始于Linux内核:create_elf_tables

/* Now, let's put argc (and argv, envp if appropriate) on the stack */
if (put_user(argc, sp++))
    return -EFAULT;

/* Populate list of argv pointers back to argv strings. */
p = mm->arg_end = mm->arg_start;
while (argc-- > 0) {
    size_t len;
    if (put_user((elf_addr_t)p, sp++))
        return -EFAULT;
    len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
    if (!len || len > MAX_ARG_STRLEN)
        return -EINVAL;
    p += len;
}
if (put_user(0, sp++))
    return -EFAULT;
mm->arg_end = p;

/* Populate list of envp pointers back to envp strings. */
mm->env_end = mm->env_start = p;
while (envc-- > 0) {
    size_t len;
    if (put_user((elf_addr_t)p, sp++))
        return -EFAULT;
    len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
    if (!len || len > MAX_ARG_STRLEN)
        return -EINVAL;
    p += len;
}
if (put_user(0, sp++))
    return -EFAULT;
mm->env_end = p;

在glibc中使用,位于sysdeps/x86_64/start.S文件中

  45    %rsp         The stack contains the arguments and environment:
  46                 0(%rsp)                         argc
  47                 LP_SIZE(%rsp)                   argv[0]
  48                 ...
  49                 (LP_SIZE*argc)(%rsp)            NULL
  50                 (LP_SIZE*(argc+1))(%rsp)        envp[0]
  51                 ...
  52                                                 NULL

-1
argv 是如何填充的?
语言实现(我指的是除了它之外的一切,比如 shell、操作系统等)会处理它。
我会认为操作系统是负责将附加参数传递给程序的实体。
基本上是这样的。
指针数组在哪里初始化?
在语言实现选择初始化它们的某个地方。
额外问题:操作系统如何处理命令行参数?
没有一个操作系统。有很多,每个都有自己的做法。其中一些是开源的,所以你可以研究它们。

1
假设以下情况:语言实现为C++(无论是哪个标准版本),操作系统为Windows。如果以“它们都以自己的方式进行,不知何故”这样的形式回答,即使是有效的,也极其模糊,而任何组合中标准事件序列的工作示例则是启迪性的。否则就像说“电流只是因为介质的工作方式”。 - user8087992
@madeslurpy 我不知道Windows如何工作,也无法找到源代码来了解。"非常模糊"这个描述与问题的模糊程度相符。 - eerorika
很遗憾,你运气不好。MS Windows是一种商业操作系统,其大部分源代码被保密作为专有信息。有一点机会是它的文档可能埋在MSDN的某个地方,也许是需要真正购买访问权限的部分。或者,微软可能根本不会对其进行文档化。由于像这样的东西对于编写MS Windows的工作软件并不必要,因此微软没有太多理由公开发布任何公共文档。 - Sam Varshavchik
你可能不熟悉Windows,但肯定知道它们如何协同工作的某些组合。如果是这样,为什么不分享一下呢?为了更简洁明了,我们可以以Unix作为操作系统吗? - user8087992
1
如果是这种情况,那就这样吧。换句话说,我不是要求实际的源代码实现,而是其实现背后的一般理论。可执行文件是一个封闭的二进制文件,那么命令行参数如何“进入”可执行文件以在main中使用呢? - user8087992
1
@madeslurpy 大部分Nate的答案在Windows上也是相似的。其中主要区别是,当Windows创建一个新进程时,命令行被复制到进程的PEB头中,然后程序的运行时库的启动代码通过GetCommandLineA()检索命令行,并将其拆分成传递给main()函数的char[]数组。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接