当main没有参数定义时,argc和argv是否仍然存在于堆栈中?

考虑一下非常简单:

int main(void) { return 0; } 

我编译它(使用mingw32-gcc)并将其作为main.exe foo bar执行。

现在,我曾预料到由明确声明为丧失生命参数的主函数引起的某种崩溃或错误。 缺乏错误导致了这个问题,这实际上是四个问题。

  • 为什么这样做? 答:因为标准是这样说的!

  • 输入参数是否被忽略,或者是使用argc&argv静默编写的堆栈? 答:在这种特殊情况下,堆栈已准备就绪。

  • 我该如何validation以上内容? 答:请参阅rascher的回答。

  • 这个平台依赖吗? 答:是的,不。

我不知道您的问题的跨平台答案。 但这让我很好奇。 那么我们该怎么办? 看堆栈!

对于第一次迭代:

test.c的

 int main(void) { return 0; } 

test2.c中

 int main(int argc, char *argv[]) { return 0; } 

现在看看汇编输出:

 $ gcc -S -o test.s test.c $ cat test.s .file "test.c" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp movl $0, %eax popl %ebp ret .size main, .-main .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3" .section .note.GNU-stack,"",@progbits 

没什么好激动的。 除了一件事: 两个C程序都具有相同的汇编输出!

这基本上是有道理的; 我们从来没有真正必须从main()的堆栈中推送/弹出任何东西,因为它是调用堆栈中的第一件事。

那么我写了这个程序:

 int main(int argc, char *argv[]) { return argc; } 

它的主题是:

 main: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax popl %ebp ret 

这告诉我们“argc”位于8(%ebp)

所以现在再增加两个C程序:

 int main(int argc, char *argv[]) { __asm__("movl 8(%ebp), %eax\n\t" "popl %ebp\n\t" "ret"); /*return argc;*/ } int main(void) { __asm__("movl 8(%ebp), %eax\n\t" "popl %ebp\n\t" "ret"); /*return argc;*/ } 

我们从上面偷了“return argc”代码并将其粘贴到这两个程序的asm中。 当我们编译并运行这些,然后调用echo $? (它回应了前一个过程的返回值)我们得到了“正确”的答案。 所以,当我运行“./test abcd”然后$? 两个程序给我“5” – 即使只有一个定义了argc / argv。 这告诉我,在我的平台上,argc肯定放在堆栈上。 我敢打赌,类似的测试会证实这是针对argv的。

在Windows上试试吧!

从C99标准:

5.1.2.2.1程序启动

程序启动时调用的函数名为main。 该实现声明此函数没有原型。 它应该使用返回类型int并且没有参数来定义:

 int main(void) { /* ... */ } 

或者使用两个参数(这里称为argc和argv,尽管可以使用任何名称,因为它们是声明它们的函数的本地名称):

int main(int argc,char * argv []){/ * … * /}

或同等学历; 或者以其他一些实现定义的方式。

在经典C中,你可以做类似的事情:

 void f() {} f(5, 6); 

没有什么可以阻止你调用具有不同数量参数的函数,因为它的定义假定。 (现代编译器,当然,认为这是一个令人震惊的错误,并强烈抵制实际编译代码。)

你的main()函数也会发生同样的事情。 C运行时库将调用

 main(argc, argv); 

但是你的函数不准备接收这两个参数这一事实对调用者来说并不重要。

在大多数编译器中,__ argc和__argv作为运行时库中的全局变量存在。 这些值是正确的。

在Windows上,如果入口点具有UTF-16签名,它们将不正确,这也是在该平台上获得正确命令参数的唯一方法。 在这种情况下它们将是空的,但这不是你的情况,并且有两个widechar替代变量。

  1. 工作原理:通常,函数参数在特定位置传递(寄存器或堆栈,通常)。 没有参数的函数永远不会检查它们,因此它们的内容是无关紧要的。 这取决于调用和命名约定,但请参阅#4。

  2. 通常会准备好堆栈。 在运行时库(例如DOS)解析argv的平台上,如果没有使用argv,编译器可能会选择不链接代码,但这是很少有人认为必要的复杂性。 在其他平台上,在程序加载之前,由exec()准备argv。

  3. 依赖于平台,但在Linux系统上,您实际上可以检查/ proc / PID / cmdline中的argv内容,无论它们是否被使用。 许多平台还提供单独的调用来查找参数。

  4. 按照Tim Schaeffer所引用的标准,主要不需要接受这些论点。 在大多数平台上,参数本身仍然存在,但是没有参数的main()永远不会知道它们。

有一些注意事项要做。

标准基本上说明了最主要的是:一个不带参数的函数,一个带两个参数的函数,或者其他什么!

例如,请参阅我对此问题的回答 。

但你的问题指向其他事实。

为什么这样做? 答:因为标准是这样说的!

这是不正确的。 它的工作原因有其他原因。 它的工作原理是调用约定。

这些约定可以是:在栈上推送参数,调用者负责清理堆栈。 因此,在实际的asm代码中,被调用者可以完全忽略堆栈中的内容。 电话看起来像

  push value1 push value2 call function add esp, 8 

(英特尔的例子,只是为了保持主流)。

在堆栈上推送的参数的function是完全无趣的,一切都会正常工作! 即使调用约定不同,这确实也是如此,例如

  li $a0, value li $a1, value jal function 

如果函数考虑了寄存器$ a0和$ a1,则不会改变任何东西。

因此被调用者可以忽略没有伤害参数,cn相信它们不存在,或者它可以知道它们存在,但更喜欢忽略它们(相反,如果被调用者从栈或寄存器获取值,而调用者则会出现问题)没有通过)。

这就是事情有效的原因。

从C的角度来看,如果我们在一个系统中,启动代码使用两个参数(int和char **)调用main并且期望一个int返回值,那么“正确”的原型将是

  int main(int argc, char **argv) { } 

但我们现在假设我们不使用这些论点。

int main(void)int main() (仍然在实现调用带有两个args的main并且期望一个int返回值的同一系统中,如前所述)?

事实上,标准并未说明我们必须做什么。 表示我们有两个参数的正确“原型”仍然是之前显示的那个。

但从逻辑的角度来看,正确的说法(我们知道它),但我们对它们不感兴趣,是

  int main() { /* ... */ } 

在这个答案中,我已经展示了如果我们将参数传递给声明为int func() ,以及如果我们将参数传递给声明为int func(void)的函数会发生什么。

在第二种情况下,我们有一个错误,因为(void)显式地说该函数没有参数。

使用main我们不能得到错误,因为我们没有一个真正的原型强制参数,但值得注意的是gcc -std=c99 -pedantic没有给出int main()int main(void)警告,并且这意味着1)即使使用std标志,gcc也不符合C99,或者2)两种方式都符合标准。 更可能是选项2。

一个是显式标准兼容( int main(void) ),另一个确实是int main(int argc, char **argv) ,但没有明确说出参数,因为我们对它们不感兴趣。

int main(void)即使在参数存在时也能工作,因为我之前写的是什么。 但它表明,主要没有争论。 虽然在很多情况下,如果我们可以编写int main(int argc, char **argv) ,那么它是false,而int main()必须是首选。

另一个有趣的事情是,如果我们说main在实现期望返回值的系统上没有返回值( void main() ),我们会得到一个警告。 这是因为调用者希望它对它做一些事情,所以如果我们不返回一个值,它就是“未定义的行为”(这并不意味着在main情况下放置显式return ,而是将main声明为返回一个int )。

在许多启动代码中,我看到主要是通过以下方式之一调用的:

  retval = main(_argc, _argv); retval = main(_argc, _argv, environ); retval = main(_argc, _argv, environ, apple); // apple specific stuff 

但是可能存在以不同方式调用main的启动代码,例如retval = main() ; 在这种情况下,为了显示这一点,我们可以使用int main(void) ,另一方面,使用int main(int argc, char **argv)会编译,但如果我们实际使用参数会使程序崩溃(因为检索到的值将是垃圾)。

这个平台依赖吗?

调用main的方式是依赖于平台(特定于实现),如标准所允许的那样。 “假定的”主要原型是一个结果,如前所述,如果我们知道有传入的参数但我们不会使用它们,我们应该使用int main() ,作为更长的int main(int argc, char **argv)的缩写formsint main(int argc, char **argv) ,而int main(void)意味着不同的东西:即main不带参数(在我们考虑的系统中是假的)