我有一些奇怪的问题,关于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但是因为我在汇编方面不是很好时,我真的不理解这些行。
否则我尝试了别的东西并开始打印我的程序堆栈。
使用给定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
为什么我会问这些愚蠢的问题?
谢谢花时间阅读到这里,帮助我理解某些内容或让我意识到我的问题毫无意义。
答案 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中提供的代码肯定是未定义的行为。
在函数 definition 中,空参数列表表示该函数不带任何参数。在函数声明中,空参数无法声明函数所用的参数数量。
§6.7.6.3/ p.14:函数声明符中的空列表是该函数定义的一部分,指定函数没有参数。函数声明符中的空列表不属于该函数的定义,指定不提供有关数字的信息或参数类型。
最终调用该函数时,必须使用正确数量的参数调用它:
§6.5.2.2/ p.6:如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将类型为float的参数提升为double。 .. 如果参数数量不等于参数数量,则行为未定义。
如果函数被定义为vararg函数(带有尾部省略号),则无论调用函数的哪个位置,vararg声明都必须可见。
(继续上一个引用):如果函数是使用包含原型的类型定义的,并且原型以省略号(,...)结尾,或者促销后的参数类型与参数的类型,行为未定义。