C 语言的一些功能最初只是碰巧起作用的 hack。
main 的多个签名以及可变长度参数列表是这些功能之一。
程序员注意到他们可以将额外的参数传递给函数,并且他们给定的编译器不会发生任何不好的事情。
如果调用约定如下:
- 调用函数清理参数。
- 最左边的参数更靠近堆栈顶部或堆栈框架的底部,因此虚假参数不会使寻址无效。
遵守这些规则的一组调用约定是基于堆栈的参数传递,调用者弹出参数,并将它们从右向左推送:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
在这种调用约定类型的编译器中,不需要做任何特别的事情来支持main 的两种类型,甚至是其他类型。 main 可以是没有参数的函数,在这种情况下,它不会注意到被压入堆栈的项目。如果它是两个参数的函数,那么它会找到 argc 和 argv 作为两个最顶层的堆栈项。如果它是带有环境指针(通用扩展)的特定于平台的三参数变体,那也可以:它会找到第三个参数作为堆栈顶部的第三个元素。
因此,固定调用适用于所有情况,允许将单个固定启动模块链接到程序。该模块可以用 C 编写,作为类似以下的函数:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
换句话说,这个启动模块总是调用一个三参数的 main。如果 main 不带参数,或者只带 int, char **,那么由于调用约定,它恰好可以正常工作,并且如果它不带参数。
如果你要在你的程序中做这种事情,它将是不可移植的并且被 ISO C 认为是未定义的行为:以一种方式声明和调用一个函数,并以另一种方式定义它。但是编译器的启动技巧不一定是可移植的。它不受可移植程序规则的指导。
但是假设调用约定不能以这种方式工作。在这种情况下,编译器必须特别对待main。当它注意到它正在编译 main 函数时,它可以生成与三参数调用兼容的代码。
也就是说,你这样写:
int main(void)
{
/* ... */
}
但是当编译器看到它时,它实际上执行了代码转换,因此它编译的函数看起来更像这样:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
除了名称__argc_ignore 并不存在。没有将此类名称引入您的范围,并且不会对未使用的参数发出任何警告。
代码转换导致编译器发出具有正确链接的代码,该链接知道它必须清理三个参数。
另一种实现策略是让编译器或链接器自定义生成__start 函数(或任何它被调用的函数),或者至少从几个预编译的替代方案中选择一个。信息可以存储在目标文件中,关于正在使用哪种受支持的main 形式。链接器可以查看此信息,并选择正确版本的启动模块,其中包含对与程序定义兼容的main 的调用。 C 实现通常只有少量支持的main 形式,因此这种方法是可行的。
C99 语言的编译器总是必须特别对待main,在某种程度上,以支持如果函数在没有return 语句的情况下终止,则行为就像return 0 被执行一样。同样,这可以通过代码转换来处理。编译器注意到正在编译一个名为main 的函数。然后它检查身体的末端是否可能到达。如果是这样,它会插入一个return 0;