C语言的某些功能起初只是一些hack,而这些hack恰好起作用了。
主功能列表和变长参数列表的多个签名是这些功能之一。
程序员注意到,他们可以将额外的参数传递给函数,并且给定的编译器也不会发生任何不良情况。
如果调用约定如下:
- 调用函数清除参数。
- 最左边的参数更靠近堆栈的顶部或堆栈框架的底部,因此伪参数不会使寻址无效。
遵循这些规则的一组调用约定是基于堆栈的参数传递,由此调用者弹出参数,并将它们从右向左推送:
;; 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
某种程度上始终必须特别对待,以支持这样的hack:如果函数在没有return
声明的情况下终止,则行为就好像return 0
已执行一样。同样,这可以通过代码转换来解决。编译器注意到main
正在调用一个函数。然后,它检查身体的末端是否可能触及。如果是这样,它将插入一个return 0;
main
在一个程序中C
,您只能拥有一种方法(或者,实际上,几乎任何具有这种构造的语言都可以)。