【发布时间】:2017-03-21 10:18:28
【问题描述】:
x86-64 SysV ABI 指定函数参数如何在寄存器中传递(rdi 中的第一个参数,然后是rsi 等等),以及整数返回值如何传回(@ 987654331@ 然后rdx 用于非常大的值)。
但是,当传递小于 64 位的类型时,我找不到参数或返回值寄存器的高位应该是什么。
例如,对于以下函数:
void foo(unsigned x, unsigned y);
...x 将在 rdi 和 y 中传递给 rsi,但它们只有 32 位。 rdi 和 rsi 的高 32 位是否需要为零?直觉上,我会假设是的,但是所有 gcc、clang 和 icc 的 code generated 在开始时都有特定的 mov 指令将高位清零,所以编译器似乎假设不是这样。
同样,如果返回值小于 64 位,编译器似乎假设返回值rax 的高位可能有垃圾位。例如下面代码中的循环:
unsigned gives32();
unsigned short gives16();
long sum32_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives32();
}
return total;
}
long sum16_64() {
long total = 0;
for (int i=1000; i--; ) {
total += gives16();
}
return total;
}
...compile 到 clang 中的以下内容(其他编译器类似):
sum32_64():
...
.LBB0_1:
call gives32()
mov eax, eax
add rbx, rax
inc ebp
jne .LBB0_1
sum16_64():
...
.LBB1_1:
call gives16()
movzx eax, ax
add rbx, rax
inc ebp
jne .LBB1_1
注意调用返回 32 位后的 mov eax, eax 和 16 位调用后的 movzx eax, ax - 两者都分别具有将前 32 位或 48 位清零的效果。所以这种行为是有代价的——处理 64 位返回值的相同循环省略了这条指令。
我已经非常仔细地阅读了x86-64 System V ABI document,但我找不到标准中是否记录了这种行为。
这样的决定有什么好处?在我看来有明确的成本:
参数成本
在处理参数值时,被调用者的实现会产生成本。并在处理参数时的函数中。当然,这个成本通常为零,因为该函数可以有效地忽略高位,或者归零是免费的,因为可以使用 32 位操作数大小的指令来隐式地将高位归零。
但是,在函数接受 32 位参数并进行一些可以从 64 位数学中受益的数学运算的情况下,成本通常是非常实际的。以this function 为例:
uint32_t average(uint32_t a, uint32_t b) {
return ((uint64_t)a + b) >> 2;
}
直接使用 64 位数学计算一个函数,否则必须仔细处理溢出问题(以这种方式转换许多 32 位函数的能力是 64 位架构的一个经常被忽视的好处)。这编译为:
average(unsigned int, unsigned int):
mov edi, edi
mov eax, esi
add rax, rdi
shr rax, 2
ret
仅需要 4 条指令中的 2 条(忽略 ret)将高位清零。这在实践中使用 mov 消除可能很便宜,但仍然需要付出很大的代价。
另一方面,如果 ABI 指定高位为零,我真的看不到调用者的类似相应成本。因为rdi 和rsi 和其他参数传递寄存器是scratch(即可以被调用者覆盖),你只有几个场景(我们看rdi,但是替换它与您选择的参数 reg 一起使用):
rdi中传递给函数的值在调用后代码中已失效(不需要)。在这种情况下,最后分配给rdi的任何指令只需分配给edi。这不仅是免费的,如果您避免使用 REX 前缀,它通常会小一个字节。-
rdi中传递给函数的值 需要在函数之后。在这种情况下,由于rdi是调用者保存的,调用者无论如何都需要对被调用者保存的寄存器执行mov的值。您通常可以组织它,以便值 starts 在被调用方保存的寄存器中(比如rbx),然后像mov edi, ebx一样移动到edi,所以它没有任何成本。
我看不到很多归零会花费调用者很多钱的情况。一些例子是如果在分配rdi 的最后一条指令中需要 64 位数学。不过,这似乎很少见。
回报价值成本
这里的决定似乎更加中立。让被调用者清除垃圾有一个明确的代码(您有时会看到执行此操作的mov eax, eax 指令),但如果允许垃圾,则成本会转移到被调用者身上。总体而言,调用者似乎更有可能免费清除垃圾,因此允许垃圾似乎总体上不会损害性能。
我想这种行为的一个有趣用例是具有不同大小的函数可以共享相同的实现。例如,以下所有函数:
short sums(short x, short y) {
return x + y;
}
int sumi(int x, int y) {
return x + y;
}
long suml(long x, long y) {
return x + y;
}
实际上可以共享相同的实现1:
sum:
lea rax, [rdi+rsi]
ret
1对于被占用地址的函数,这种折叠是否真的允许非常多open to debate。
【问题讨论】:
-
在 i386 和 SYSV ABI(即使是最新版本)中似乎都没有为 INTEGER 类类型指定。在 GCC 邮件列表上也对此进行了讨论 gcc.gnu.org/bugzilla/show_bug.cgi?id=46942
-
另一件值得注意的可能是原始的 i386 ABI 这是指定的 函数将所有整数值参数作为单词传递,扩展或填充有符号或无符号字节和半字为需要。 .在该 ABI 中使用了这些定义在本规范中,术语半字指的是 16 位对象,术语字指的是 32 位对象,术语双字指的是 64 位对象。我> 。有问题的 ABI(大约 1997 年)可以在这里找到:sco.com/developers/devspecs/abi386-4.pdf
-
gcc 问题的绝佳链接。如果您愿意,请随时将其总结为答案,或者我会在某个时候。 i386 文档的另一个链接很有趣,因为 gcc 链接的当前“未写入 ABI”行为似乎遵循最多 32 位的约定(即 8 位或 16 位值 是 零/由 调用者 扩展的符号),但是 64 位约定中高 32 位的约定是不同的(8、16 或 32 位值是 不是调用者扩展的零/符号)。
-
stackoverflow.com/questions/36706721/… 的可能重复项。 TL:DR: 是的,正如 Michael Matz(ABI 维护者之一)今年所证实的那样,args 可能包含大量垃圾,包括持有标量浮点数的 XMM 寄存器。 (所以不要在标量函数的 args 上粗心地使用打包操作来引发虚假的 FP 异常。)对于整数,窄函数 args 被调用者符号或零扩展为 32 位。 (这是 clang 所依赖的未记录行为。)只有 args,而不是返回值。
-
我不是当前设计的忠实拥护者,尤其是对于小功能,但它很健壮。 (当然,小函数应该内联...)有趣的事实:IIRC,
mov same, same在 Intel CPU 上总是需要一个执行单元,但是可以消除两个不同架构寄存器之间的 MOV(IvB 和更高版本)。
标签: linux x86 x86-64 calling-convention