x86-64 SysV ABI中的参数和返回值寄存器的高位允许垃圾吗?

x86-64 SysV ABI指定了函数参数如何在寄存器中传递( rdi第一个参数,然后是rsi等),以及如何传回整数返回值(在rax ,然后在rdx中传递非常大的值)。

但是,我不能find的是当传递小于64位的types时,参数或返回值寄存器的高位应该是什么。

例如,对于以下function:

 void foo(unsigned x, unsigned y); 

x将在rsirdiy传递,但它们只有32位。 rdirsi的高32位是否需要为零? 直觉上,我会承认是的,但所有的海湾合作委员会,铛和ICC 生成的代码具有特定的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; } 

…在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 

请注意mov eax, eax在调用返回32位之后的mov eax, eax以及16位调用之后的movzx eax, ax都具有分别清零前32位或48位的效果。 所以这个行为有一些代价 – 处理64位返回值的同一个循环省略了这个指令。

我已经仔细阅读了x86-64 System V ABI文档 ,但是我无法find标准中是否logging了这种行为。

这样的决定有什么好处? 在我看来,有明确的成本:

参数成本

在处理参数值时,强制执行被调用者的代价是成本。 并在处理参数的function。 当然,这个代价通常是零,因为这个函数可以有效地忽略高位,或者零位是免费的,因为可以使用32位操作数大小的指令,这些指令隐含地将高位归零。

然而,在接受32位参数的函数以及做一些可能从64位math中受益的math中,成本通常是非常真实的。 以此function为例:

 uint32_t average(uint32_t a, uint32_t b) { return ((uint64_t)a + b) >> 2; } 

直接使用64位math来计算一个本来不得不小心处理溢出的函数(以这种方式转换许多32位函数的能力是64位体系结构常常没有注意到的)。 这编译为:

 average(unsigned int, unsigned int): mov edi, edi mov eax, esi add rax, rdi shr rax, 2 ret 

完全4条指令中的2条(忽略ret )只是为了清零高位。 这在实践中可能是便宜的,但是这似乎是一个很大的代价。

另一方面,如果ABI指定高位为零,那么对于呼叫者来说,我看不到相似的相应成本。 因为rdirsi以及其他parameter passing寄存器都是从头开始的 (即可以被调用者覆盖),所以您只有几个场景(我们看rdi ,但用您select的参数regreplace它):

  1. 传递给rdi函数的值在调用后的代码中是死的(不需要)。 在这种情况下,最后分配给rdi任何指令只需要分配给edi 。 这不仅是免费的,如果避免使用REX前缀,它通常会减less一个字节。

  2. 在函数之后需要传递给rdi函数的值。 在这种情况下,由于rdi是主叫方保存的,因此主叫方需要将该值写入被叫方保存的寄存器。 你通常可以组织它,使得值在被调用者保存的寄存器(比如rbx )中开始,然后像mov edi, ebx一样移动到edi ,所以它rbx任何费用。

我看不到很多情况下,调零成本很高。 如果在分配了rdi的最后一条指令中需要64位math运算,那么就是一些例子。 这似乎相当罕见,但。

返回值成本

这个决定似乎更加中立。 被叫方清除垃圾邮件有一个明确的代码(你有时会看到mov eax, eax指令来做这个),但是如果垃圾被允许,成本会转移到被调用方。 总的来说,调用者似乎更有可能免费清理垃圾,因此允许垃圾对整个性能来说似乎没有任何不利影响。

我想这个行为的一个有趣的用例是具有不同大小的函数可以共享相同的实现。 例如,所有以下function:

 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这种折叠是否实际上被允许用于具有他们的地址的function是非常容易争论的 。

Solutions Collecting From Web of "x86-64 SysV ABI中的参数和返回值寄存器的高位允许垃圾吗?"

看起来你在这里有两个问题:

  1. 在返回之前,返回值的高位是否需要清零? (在调用之前需要将参数的高位清零?)
  2. 与此决定相关的成本/收益是多少?

第一个问题的答案是否定的,高位可能会有垃圾 ,Peter Cordes已经就这个问题写了一个非常好的答案 。

至于第二个问题,我怀疑留下未定义的高位整体上是更好的表现。 一方面,当使用32位操作时,预先零扩展值不需要额外的成本。 但是另一方面,预先调高高位并不总是必要的。 如果你允许高位垃圾,那么你可以把它留给接收值的代码,只在实际需要时才执行零扩展(或符号扩展)。

但我想强调另一个考虑: 安全

信息泄露

当结果的高位不被清除时,它们可以保留其他信息片段,例如堆栈/堆中的函数指针或地址。 如果存在一种机制来执行更高权限的函数,并在之后检索rax (或eax )的全部值,那么这可能会引入信息泄漏 。 例如,系统调用可能会将指针从内核泄漏到用户空间,从而导致内核ASLR失败。 或者IPC机制可能会泄露有关另一个进程地址空间的信息,这可能有助于开发沙盒突破。

当然,有人可能会认为ABI不负责防止信息泄露, 程序员可以正确地实现他们的代码。 虽然我同意,但要求编译器将高位清零仍然会消除这种特定形式的信息泄漏。

你不应该相信你的意见

在另一方面,更重要的是,编译器不应该盲目地相信任何接收到的值都将其高位清零,否则该函数可能不会像预期的那样工作,这也可能导致可利用的条件。 例如,请考虑以下几点:

 unsigned char buf[256]; ... __fastcall void write_index(unsigned char index, unsigned char value) { buf[index] = value; } 

如果允许我们假定index高位被清零,那么我们可以将上面的内容编译成:

 write_index: ;; sil = index, dil = value mov rax, offset buf mov [rax+rsi], dil ret 

但是如果我们可以从我们自己的代码中调用这个函数,我们可以提供超出[0,255]范围的rsi值,并写入超出缓冲区边界的内存。

当然,编译器实际上不会像这样生成代码,因为如上所述, 被调用者有责任将其参数置零或签名扩展,而不是调用者的参数 。 我认为这是一个非常实际的理由,让接收到值的代码总是假定高位中有垃圾,并明确地将其删除。