Linux x86-64 SysV ABI中参数和返回值寄存器的高位是否允许垃圾?
x86-64 SysV ABI除其他外,还指定了如何在寄存器中传递函数参数(第一个参数是Linux x86-64 SysV ABI中参数和返回值寄存器的高位是否允许垃圾?,linux,x86,x86-64,calling-convention,Linux,X86,X86 64,Calling Convention,x86-64 SysV ABI除其他外,还指定了如何在寄存器中传递函数参数(第一个参数是rdi,然后是rsi等等),以及如何传回整数返回值(对于真正大的值,在rax中,然后是rdx) 然而,我找不到的是,当传递小于64位的类型时,参数或返回值寄存器的高位应该是什么 例如,对于以下函数: void foo(unsigned x, unsigned y); x将在rdi中传递,在rsi中传递y,但它们仅为32位。rdi和rsi的高32位是否需要为零?直观地说,我会假设是的,但是所有gcc、clan
rdi
,然后是rsi
等等),以及如何传回整数返回值(对于真正大的值,在rax
中,然后是rdx
)
然而,我找不到的是,当传递小于64位的类型时,参数或返回值寄存器的高位应该是什么
例如,对于以下函数:
void foo(unsigned x, unsigned y);
x
将在rdi
中传递,在rsi
中传递y
,但它们仅为32位。rdi
和rsi
的高32位是否需要为零?直观地说,我会假设是的,但是所有gcc、clang和icc在开始时都有特定的mov
指令将高位归零,因此编译器似乎假设不是这样
类似地,编译器似乎假设返回值rax
的高位如果小于64位,则可能有垃圾位。例如,以下代码中的循环:
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;
}
。。。与clang
中的以下内容类似(与其他编译器类似):
请注意,返回32位的调用后的mov-eax,eax
,以及16位调用后的movzx-eax,ax
——这两种方法都具有分别将前32位或48位归零的效果。因此,这种行为有一定的代价——处理64位返回值的同一个循环忽略了这条指令
我已经非常仔细地阅读了这个标准,但是我找不到这个行为是否记录在标准中
这样的决定有什么好处?在我看来,有明显的成本:
参数成本
在处理参数值时,会对被调用方的实现施加成本。以及在处理参数时的函数中。当然,这种代价通常为零,因为函数可以有效地忽略高位,或者零化是免费的,因为可以使用32位操作数大小的指令隐式地将高位置零
但是,对于接受32位参数并进行一些可能从64位数学中受益的数学运算的函数,成本通常非常实际。例如:
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
以及其他参数传递寄存器都是临时寄存器(即可以被调用者覆盖),因此您只有几个场景(我们查看rdi
,但将其替换为您选择的参数寄存器):
rdi
中传递给函数的值无效(不需要)。在这种情况下,最后分配给rdi
的任何指令都必须分配给edi
。这不仅是免费的,而且如果避免REX前缀,它通常会小一个字节rdi
中传递给函数的值。在这种情况下,由于调用方保存了rdi
,因此调用方仍然需要对被调用方保存的寄存器执行值的mov
。您通常可以对其进行组织,使该值从被调用方保存的寄存器(例如,rbx
)开始,然后移动到edi
,如mov edi,ebx
,因此它不需要任何成本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:
1地址被占用的函数是否允许进行这种折叠非常重要。这里似乎有两个问题:
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;
}
sum:
lea rax, [rdi+rsi]
ret
unsigned char buf[256];
...
__fastcall void write_index(unsigned char index, unsigned char value) {
buf[index] = value;
}
write_index: ;; sil = index, dil = value
; movzx esi, sil ; skipped based on assumptions
mov [buf + rsi], dil
ret