调用C函数,该函数不带参数参数

时间:2015-07-09 18:12:23

标签: c 32bit-64bit stack-trace undefined-behavior calling-convention

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

int f() { return 0; }

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

正如你所看到的,我用一个参数调用f而f不带参数。 我的第一个问题是这个论点在调用它时是否真的给了f。

神秘的线条

经过一番观察后,我获得了好奇的结果。 传递x作为f:

的参数
00000000004004b6 <f>:
  4004b6:   55                      push   %rbp
  4004b7:   48 89 e5                mov    %rsp,%rbp
  4004ba:   b8 00 00 00 00          mov    $0x0,%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    $0x10,%rsp
  4004c9:   c7 45 fc 2a 00 00 00    movl   $0x2a,-0x4(%rbp)
  4004d0:   8b 45 fc                mov    -0x4(%rbp),%eax
  4004d3:   89 c7                   mov    %eax,%edi
  4004d5:   b8 00 00 00 00          mov    $0x0,%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    $0x0,%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    $0x10,%rsp
  4004c9:   c7 45 fc 2a 00 00 00    movl   $0x2a,-0x4(%rbp)
  4004d0:   b8 00 00 00 00          mov    $0x0,%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位矛盾

否则我尝试了别的东西并开始打印我的程序堆栈。

使用给定f的x进行堆栈(以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

堆栈x没有给f(用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似乎是在堆栈上推。

使用给定f的x进行堆栈(以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中调用可变参数函数而不给出参数计数。
  • 最后但同样重要的是,我认为未定义的行为很有趣。

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

2 个答案:

答案 0 :(得分:3)

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

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

这种无害的行为使得C黑客一度开发了一个变量参数列表工具,该工具曾经在Unix C库的古老版本中位于#include <varargs.h>之下。

这演变为ANSI C <stdarg.h>

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

今天不行。例如,正如您所看到的,该参数实际上并未放入堆栈,而是加载到RDI寄存器中。这是GCC在x86-64上使用的约定。如果你在堆栈中游走,你将找不到前几个参数。相比之下,在IA-32上,GCC使用堆栈传递参数:尽管您可以使用“fastcall”约定获得基于寄存器的行为。

来自va_arg的{​​{1}}宏将正确地考虑混合寄存器/堆栈参数传递约定。 (或者,相反,当您对可变参数函数使用正确的声明时,它可能会抑制寄存器中尾随参数的传递,以便<stdarg.h> 可以只是通过内存进行。)

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

va_arg
由于看起来像是一些浪费的数据,所以

相当迟钝。

答案 1 :(得分:2)

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

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

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

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

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

您可以在此Wikipedia article中找到有关各种呼叫约定的更多信息。 AMD64 ABI可以在x86-64.org (PDF document)找到。最初的System V IA-32 ABI(Linux,xBSD和OS X上使用的ABI的基础)仍然可以从www.sco.com (PDF document)访问。

未定义的行为?

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

  1. 在函数 definition 中,空参数列表表示该函数不带任何参数。在函数声明中,空参数无法声明函数所用的参数数量。

      

    §6.7.6.3/ p.14:函数声明符中的空列表是该函数定义的一部分,指定函数没有参数。函数声明符中的空列表不属于该函数的定义,指定不提供有关数字的信息或参数类型。

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

      

    §6.5.2.2/ p.6:如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将类型为float的参数提升为double。 .. 如果参数数量不等于参数数量,则行为未定义。

  3. 如果函数被定义为vararg函数(带有尾部省略号),则无论调用函数的哪个位置,vararg声明都必须可见。

      

    (继续上一个引用):如果函数是使用包含原型的类型定义的,并且原型以省略号(,...)结尾,或者促销后的参数类型与参数的类型,行为未定义。