【问题标题】:Calling C function which takes no parameters with parameters调用不带参数的C函数
【发布时间】:2015-07-09 18:12:22
【问题描述】:

关于 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    $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 位悖论

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

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

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

【问题讨论】:

  • 这不是讨论论坛。调用 UB 根本没有任何价值。
  • @yayg 编译器应该发出诊断消息,因为它看到了函数原型。
  • @Olaf 其实我也不知道这是不是UB
  • @VladfromMoscow 使用 -Wall -Wextra gcc 不会引发任何错误或警告
  • “我认为未定义的行为很有趣。” - 不是当你在调试别人的代码时,它看似无休止地调用它的赏金;相信我。

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


【解决方案1】:

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

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

这种无害的行为使 C 黑客曾一度开发出一种可变参数列表工具,该工具曾在古代 Unix C 库版本中位于 #include &lt;varargs.h&gt; 之下。

这演变成 ANSI C &lt;stdarg.h&gt;

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

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

来自&lt;stdarg.h&gt;va_arg 宏将正确考虑混合寄存器/堆栈参数传递约定。 (或者,更确切地说,当您对可变参数函数使用正确的声明时,它可能会抑制寄存器中尾随参数的传递,因此 va_arg 可以 只是在内存中前进。)

附:如果您添加了一些优化,您的机器代码可能更容易理解。比如序列

  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

由于看起来像一些浪费的数据移动,因此相当迟钝。

【讨论】:

    【解决方案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. 在函数定义中,空参数列表意味着该函数不接受任何参数。在函数声明中,空参数无法声明函数接受多少参数。

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

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

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

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

      (接上一句):如果函数定义的类型包含原型,并且原型以省略号 (, ...) 结尾,或者提升后的参数类型与参数类型,行为未定义。

    【讨论】:

    • 对于ARM,大部分是由所涉及的过程调用标准(PCS,最新的是AAPCS)定义的。 ABI 只是特定于语言的部分。 x86 的 IIRC 也有一些参数在寄存器中传递;至少结果是这样传递的。请注意,不同的平台也可能有不同的 PCS/ABI。例如,IIRC、Linux/GNU 使用与 Windows 不同的标准。不确定 OS-X。
    • @olaf:是的,返回值通常在寄存器中传递。 (除非它是一个结构,在这种情况下,还有一个附加参数,其中包含一个放置返回值的地址。)引用的 Wikipedia 页面有更多信息,我添加了几个有用的 ABI 链接。 Afaik,唯一在寄存器中传递参数的 IA-32 调用约定是 fastcall。
    • 好吧,我会接受这一点,因为我对 x86 不是很熟悉(总是尽量避免使用 x86 汇编程序,如果你使用过 68K 和现在的 ARM,甚至很多 8 位CPU)。
    猜你喜欢
    • 1970-01-01
    • 2020-07-29
    • 1970-01-01
    • 2020-11-12
    • 1970-01-01
    • 1970-01-01
    • 2012-07-24
    • 1970-01-01
    • 2014-04-02
    相关资源
    最近更新 更多