在x86_64 / Linux上,使用gcc / clang -O3编译:
.text
反汇编为:
printf
在第二种情况(void void_unspec0(),void_unspec1(),void_unspec2(),void_unspec3(),void_void(void);
void call_void_void()
{
void_void();
void_void();
void_void();
void_void();
void_void();
}
void call_void_unspec()
{
void_unspec0();
void_unspec0();
void_unspec0();
void_unspec0();
void_unspec1(.0,.0,.0);
void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}
)中,编译器正在对寄存器中传递的浮点参数进行计数,大概是因为认为应该这样做。
调用带有可变参数的函数时,必须将
0000000000000000 <call_void_void>: 0: 48 83 ec 08 sub $0x8,%rsp 4: e8 00 00 00 00 callq 9 <call_void_void+0x9> 9: e8 00 00 00 00 callq e <call_void_void+0xe> e: e8 00 00 00 00 callq 13 <call_void_void+0x13> 13: e8 00 00 00 00 callq 18 <call_void_void+0x18> 18: 48 83 c4 08 add $0x8,%rsp 1c: e9 00 00 00 00 jmpq 21 <call_void_void+0x21> 21: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1) 28: 00 00 00 00 2c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000000030 <call_void_unspec>: 30: 48 83 ec 08 sub $0x8,%rsp 34: 31 c0 xor %eax,%eax 36: e8 00 00 00 00 callq 3b <call_void_unspec+0xb> 3b: 31 c0 xor %eax,%eax 3d: e8 00 00 00 00 callq 42 <call_void_unspec+0x12> 42: 31 c0 xor %eax,%eax 44: e8 00 00 00 00 callq 49 <call_void_unspec+0x19> 49: 31 c0 xor %eax,%eax 4b: e8 00 00 00 00 callq 50 <call_void_unspec+0x20> 50: 66 0f ef d2 pxor %xmm2,%xmm2 54: b8 03 00 00 00 mov $0x3,%eax 59: 66 0f ef c9 pxor %xmm1,%xmm1 5d: 66 0f ef c0 pxor %xmm0,%xmm0 61: e8 00 00 00 00 callq 66 <call_void_unspec+0x36> 66: 66 0f ef ff pxor %xmm7,%xmm7 6a: b8 08 00 00 00 mov $0x8,%eax 6f: 66 0f ef f6 pxor %xmm6,%xmm6 73: 66 0f ef ed pxor %xmm5,%xmm5 77: 66 0f ef e4 pxor %xmm4,%xmm4 7b: 66 0f ef db pxor %xmm3,%xmm3 7f: 66 0f ef d2 pxor %xmm2,%xmm2 83: 66 0f ef c9 pxor %xmm1,%xmm1 87: 66 0f ef c0 pxor %xmm0,%xmm0 8b: e8 00 00 00 00 callq 90 <call_void_unspec+0x60> 90: 66 0f ef c0 pxor %xmm0,%xmm0 94: 6a 00 pushq $0x0 96: 66 0f ef ff pxor %xmm7,%xmm7 9a: 6a 00 pushq $0x0 9c: 66 0f ef f6 pxor %xmm6,%xmm6 a0: b8 08 00 00 00 mov $0x8,%eax a5: 66 0f ef ed pxor %xmm5,%xmm5 a9: 66 0f ef e4 pxor %xmm4,%xmm4 ad: 66 0f ef db pxor %xmm3,%xmm3 b1: 66 0f ef d2 pxor %xmm2,%xmm2 b5: 66 0f ef c9 pxor %xmm1,%xmm1 b9: e8 00 00 00 00 callq be <call_void_unspec+0x8e> be: 48 83 c4 18 add $0x18,%rsp c2: c3 retq
设置为 传递给函数的浮点参数总数 上证所注册
ABI规范中该规则的原因是什么?假设在调用之前用call_void_unspec()
(省略号)定义的函数是SysVABI/AMD64 spec,那么未原型函数调用必须遵守吗?没有%rax
的函数也可以可变吗?
答案 0 :(得分:5)
请注意,可变参数函数只能在存在原型时才能调用。如果尝试在没有原型的情况下调用printf()
,则会得到UB(不确定的行为)。
C11 §6.5.2.2 Function calls ¶6说:
¶6如果表示被调用函数的表达式的类型不包含原型,则对每个参数执行整数提升,并将类型为
float
的参数提升为double
。这些称为默认参数提升。。如果参数数量与参数数量不相等,则行为未定义。如果函数是用包含原型的类型定义的,并且原型以省略号(, ...
结尾),或者升级后的参数类型与参数类型不兼容,则行为是不确定的。如果使用不包含原型的类型定义函数,并且升级后的参数类型与升级后的参数类型不兼容,则该行为是不确定的,除了以下情况:
- 一种提升类型是有符号整数类型,另一种提升类型是相应的无符号整数类型,并且值在两种类型中都可以表示;
- 这两种类型都是指向字符类型或
void
的合格或不合格版本的指针。
问题中的原始代码与此类似-连续的相同函数调用已减少为单个调用。
void void_unspec(), void_void(void);
void call_void_void()
{
void_void();
}
void call_void_unspec()
{
void_unspec();
void_unspec(.0,.0,.0);
void_unspec(.0,.0,.0,.0,.0,.0,.0,.0);
void_unspec(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}
此代码调用UB,因为调用void_unspec()
的函数的参数数量并不全部与定义要采用的参数数量匹配(无论定义是什么;它不能同时采用0、3, 8和10个参数)。这不是违反约束,因此不需要诊断。编译器通常会尽其所能来实现向后兼容性,并且通常不会导致彻底的崩溃,但是会引起程序员违反标准规则而产生的任何问题。
并且因为该标准指出行为是未定义的,所以没有特定的原因要求编译器必须设置%rax
(当然,C标准对%rax
一无所知),但是具有简单的一致性建议这样做。
问题中的代码是这样修改的(重复的连续调用被省略了):
void void_unspec0(), void_unspec1(), void_unspec2(), void_unspec3(), void_void(void);
void call_void_void()
{
void_void();
}
void call_void_unspec()
{
void_unspec0();
void_unspec1(.0,.0,.0);
void_unspec2(.0,.0,.0,.0,.0,.0,.0,.0);
void_unspec3(.0,.0,.0,.0,.0,.0,.0,.0,.0,.0);
}
该代码不再不可避免地调用未定义的行为。但是,在定义了void_unspec0()
等函数的地方,它们看起来应该类似于:
void void_unspec0(void) { … }
void void_unspec1(double a, double b, double c) { … }
void void_unspec2(double a, double b, double c, double d, double e, double f, double g, double h) { … }
void void_unspec3(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { … }
一种等效的表示法是:
void void_unspec2(a, b, c, d, e, f, g, h)
double a, b, c, d, e, f, g, h;
{
…
}
这使用的是K&R标准非原型定义。
如果函数定义与之不匹配,则6.5.2.2¶6节说调用结果是未定义的行为。这省去了必须立法在各种可疑情况下发生的情况的标准。和以前一样,编译器可以自由地在%rax
中传递浮点值的数量。那讲得通。但是,在争论将要发生的事情上几乎没有什么可以做的-调用符合定义并且一切都很好,或者调用不符合要求,并且存在未指定(和未指定)的潜在问题。
请注意,call_void_void()
和call_void_unspec()
都没有定义原型。它们都是带有零参数的函数,但是没有可见的原型可以强制执行此操作,因此同一文件中的代码可以调用call_void_void(1, "abc")
,而不会引起编译器的抱怨。 (在这方面,与许多其他方面一样,C ++是另一种语言,具有不同的规则。)
答案 1 :(得分:3)
该标准的没有...的功能也可以可变吗?
Paragraph 6.5.2.2/6可能是最相关的:
如果表示被调用函数的表达式的类型为 不包含原型,整数促销将在 每个参数以及类型为float的参数都被提升为 双。这些称为默认参数提升。 如果 参数数量不等于参数数量, 行为是不确定的。
(添加了强调。)在函数的声明类型不包含参数列表的情况下(不同于仅由void
组成的参数列表)。调用方仍然负责传递正确数量的参数。
如果函数的类型定义为 包括一个原型,或者该原型以省略号结尾 (,...)或提升后的参数类型不是 与参数类型兼容,其行为是 未定义。
这是将功能定义的属性与表示该功能的功能 call 的子表达式的类型区分开。请注意,它明确表示通过类型不包含原型的函数表达式调用可变参数函数的行为是不确定的。还需要在提升的参数和参数之间进行类型匹配。
如果函数定义的类型不是 包括原型,以及升级后的参数类型 与升级后的参数不兼容, 除以下情况外,行为是不确定的:
- 一种提升的类型是有符号整数类型,另一种提升的类型是相应的无符号整数类型,其值是 两种类型都可以表示;
- 这两种类型都是指向字符类型或void的合格或不合格版本的指针。
这是K&R样式的函数定义的情况。它也需要参数和参数之间的数字和类型匹配,因此此类函数是非可变的。
因此
ABI规范中该规则的原因是什么?必须无原型 给定使用...定义的函数,函数调用应遵守 (省略号)需要原型吗?
我认为该规则的原因是为了传达功能实现需要保存或保留的FP寄存器。由于通过类型不包含原型的函数表达式对可变参数函数的调用具有UB,因此C实现无需特别遵循ABI条款。