调用不带参数的带参数的C函数

Calling C function which takes no parameters with parameters

我有一些关于 C 调用约定和 64/32 位编译之间可能未定义行为的奇怪问题。 首先是我的代码:

int f() { return 0; }

int main()
{
    int x = 42;
    return f(x);
}

如您所见,我在调用 f 时带有一个参数,而 f 不带任何参数。 我的第一个问题是这个参数是否真的在调用 f 时提供给它。

神秘的台词

经过一点 objdump 我得到了奇怪的结果。 将 x 作为 f 的参数传递时:

00000000004004b6 <f>:
  4004b6:   55                      push   %rbp
  4004b7:   48 89 e5                mov    %rsp,%rbp
  4004ba:   b8 00 00 00 00          mov    [=11=]x0,%eax
  4004bf:   5d                      pop    %rbp
  4004c0:   c3                      retq   

00000000004004c1 <main>:
  4004c1:   55                      push   %rbp
  4004c2:   48 89 e5                mov    %rsp,%rbp
  4004c5:   48 83 ec 10             sub    [=11=]x10,%rsp
  4004c9:   c7 45 fc 2a 00 00 00    movl   [=11=]x2a,-0x4(%rbp)
  4004d0:   8b 45 fc                mov    -0x4(%rbp),%eax
  4004d3:   89 c7                   mov    %eax,%edi
  4004d5:   b8 00 00 00 00          mov    [=11=]x0,%eax
  4004da:   e8 d7 ff ff ff          callq  4004b6 <f>
  4004df:   c9                      leaveq 
  4004e0:   c3                      retq   
  4004e1:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4004e8:   00 00 00 
  4004eb:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

不传递 x 作为参数:

00000000004004b6 <f>:
  4004b6:   55                      push   %rbp
  4004b7:   48 89 e5                mov    %rsp,%rbp
  4004ba:   b8 00 00 00 00          mov    [=12=]x0,%eax
  4004bf:   5d                      pop    %rbp
  4004c0:   c3                      retq   

00000000004004c1 <main>:
  4004c1:   55                      push   %rbp
  4004c2:   48 89 e5                mov    %rsp,%rbp
  4004c5:   48 83 ec 10             sub    [=12=]x10,%rsp
  4004c9:   c7 45 fc 2a 00 00 00    movl   [=12=]x2a,-0x4(%rbp)
  4004d0:   b8 00 00 00 00          mov    [=12=]x0,%eax
  4004d5:   e8 dc ff ff ff          callq  4004b6 <f>
  4004da:   c9                      leaveq 
  4004db:   c3                      retq   
  4004dc:   0f 1f 40 00             nopl   0x0(%rax)

所以我们可以看到:

  4004d0:   8b 45 fc                mov    -0x4(%rbp),%eax
  4004d3:   89 c7                   mov    %eax,%edi

当我用 x 调用 f 时会发生这种情况,但因为我不太擅长汇编,所以我不太理解这些行。

64/32 位悖论

否则我尝试了其他方法并开始打印我的程序堆栈。

将 x 赋给 f 的堆栈(以 64 位编译):

Address of x: ffcf115c
  ffcf1128:          0          0
  ffcf1130:   -3206820          0
  ffcf1138:   -3206808  134513826
  ffcf1140:         42   -3206820
  ffcf1148: -145495616  134513915
  ffcf1150:          1   -3206636
  ffcf1158:   -3206628         42
  ffcf1160: -143903780   -3206784

没有给 f 的 x 的堆栈(以 64 位编译):

Address of x: 3c19183c
  3c191818:          0          0
  3c191820: 1008277568      32766
  3c191828:    4195766          0
  3c191830: 1008277792      32766
  3c191838:          0         42
  3c191840:    4195776          0

并且出于某种原因,在 32 位中 x 似乎被压入堆栈。

将 x 赋给 f 的堆栈(以 32 位编译):

Address of x: ffdc8eac
  ffdc8e78:          0          0
  ffdc8e80:   -2322772          0
  ffdc8e88:   -2322760  134513826
  ffdc8e90:         42   -2322772
  ffdc8e98: -145086016  134513915
  ffdc8ea0:          1   -2322588
  ffdc8ea8:   -2322580         42
  ffdc8eb0: -143494180   -2322736

为什么 x 出现在 32 而不是 64 ???

打印代码:http://paste.awesom.eu/yayg/QYw6&ln

为什么我要问这么愚蠢的问题?

感谢您 花时间阅读到这里,帮助我理解某些东西或让我意识到我的问题毫无意义。

答案是,正如您所怀疑的那样,您所做的是未定义的行为(在传递多余参数的情况下)。

但是,许多实现中的实际行为 是无害的。在堆栈上准备了一个参数,并被调用的函数忽略。被调用的函数不负责从堆栈中删除参数,因此没有危害(例如不平衡的堆栈指针)。

正是这种无害的行为使 C 黑客能够开发出一种可变参数列表工具,该工具曾经在 #include <varargs.h> 的古代版本的 Unix C 库中。

这演变成 ANSI C <stdarg.h>

想法是:将额外的参数传递给函数,然后动态地遍历堆栈以检索它们。

今天不行。例如,如您所见,参数实际上并没有入栈,而是被加载到RDI寄存器中。这是 GCC 在 x86-64 上使用的约定。如果遍历堆栈,您将找不到前几个参数。相比之下,在 IA-32 上,GCC 使用堆栈传递参数:尽管您可以使用 "fastcall" 约定获得基于寄存器的行为。

<stdarg.h> 中的 va_arg 宏将正确考虑混合的 register/stack 参数传递约定。 (或者,更确切地说,当您对可变参数函数使用正确的声明时,它可能会抑制寄存器中尾随参数的传递,因此 va_arg can 可以通过内存。)

P.S。如果您添加了一些优化,您的机器代码可能更容易理解。例如,序列

  4004c9:   c7 45 fc 2a 00 00 00    movl   [=10=]x2a,-0x4(%rbp)
  4004d0:   8b 45 fc                mov    -0x4(%rbp),%eax
  4004d3:   89 c7                   mov    %eax,%edi
  4004d5:   b8 00 00 00 00          mov    [=10=]x0,%eax

由于看起来有些浪费的数据移动,所以相当迟钝。

如何将参数传递给函数取决于平台 ABI(应用程序二进制接口)。 ABI 使得使用编译器 X 编译库并将它们与使用编译器 Y 编译的代码一起使用成为可能。None 由标准定义。

标准甚至没有要求“堆栈”存在,更不用说它用于函数调用了。

x86 芯片的寄存器数量有限,ABI 反映了这一事实;正常的 32 位 x86 调用约定对所有参数使用堆栈。

64 位架构不是这种情况,它有更多的寄存器并将其中一些用于前几个参数。这显着加快了函数调用的速度。

类似地,Windows 32 位“fastcall”调用约定在寄存器中传递一些参数。 (为了使用非标准的调用约定,您需要适当地注释函数声明,并在定义的地方一致地这样做。)

您可以在此 Wikipedia article. The AMD64 ABI can be found on x86-64.org (PDF document). The original System V IA-32 ABI (the basis of the ABI used on Linux, xBSD and OS X) can still be accessed from www.sco.com (PDF document) 中找到有关各种调用约定的更多信息。


未定义的行为?

OP 中提供的代码绝对是未定义的行为。

  1. 在函数定义中,空参数列表意味着该函数不接受任何参数。在函数 声明 中,一个空参数无法声明该函数需要多少个参数。

    §6.7.6.3/p.14: An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.

  2. 当最终调用该函数时,必须使用正确数量的参数调用它:

    §6.5.2.2/p.6: If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double... If the number of arguments does not equal the number of parameters, the behavior is undefined.

  3. 如果函数定义为可变参数函数(带有尾部省略号),则可变参数声明必须在调用函数的任何地方可见。

    (Continuing from previous quote): If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined.