gpt4 book ai didi

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

转载 作者:IT王子 更新时间:2023-10-29 00:33:35 26 4
gpt4 key购买 nike

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

然而,我找不到的是,当传递小于 64 位的类型时,参数或返回值寄存器的高位应该是什么。

例如,对于以下函数:

void foo(unsigned x, unsigned y);

... x将在 rdi 中通过和 yrsi ,但它们只有 32 位。做 rdi的高32位和 rsi必须为零?直觉上,我会假设是的,但是 code generated所有 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;
}

... 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

请注意 mov eax, eax调用返回 32 位后, movzx eax, ax在 16 位调用之后 - 两者都分别具有将前 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 )来将高位清零。这在实践中使用移动消除可能很便宜,但似乎仍然需要付出很大的代价。

另一方面,如果 ABI 指定高位为零,我真的看不到调用者的类似相应成本。因为 rdirsi并且其他参数传递寄存器是临时的(即可以被调用者覆盖),您只有几种情况(我们查看 rdi ,但将其替换为您选择的参数 reg):
  • 传递给 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:
    sum:
    lea rax, [rdi+rsi]
    ret

    1 对于取地址的函数是否真的允许这种折叠非常 open to debate .

    最佳答案

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

  • 返回值的高位是否需要在返回前清零? (在调用之前是否需要将参数的高位清零?)
  • 与此决定相关的成本/ yield 是什么?

  • 第一个问题的答案是 不,高位可能有垃圾 ,而 Peter Cordes 已经写了一个 very nice answer就此主题而言。
    至于第二个问题,我怀疑不定义高位总体上对性能更好。一方面,当使用 32 位操作时,预先零扩展值不会带来额外的成本。但另一方面,事先将高位清零并不总是必要的。如果您允许高位中的垃圾,那么您可以让接收值的代码仅在实际需要时执行零扩展(或符号扩展)。
    但我想强调另一个考虑: 安全
    信息泄露
    当结果的高位未被清除时,它们可能会保留其他信息片段的片段,例如函数指针或堆栈/堆中的地址。如果存在一种机制来执行更高特权的函数并检索 rax 的全部值(或 eax )之后,这可能会导致信息泄漏。例如,一个系统调用可能会泄漏一个从内核到用户空间的指针,导致内核 ASLR失效。 .或 IPC机制可能会泄漏有关另一个进程的地址空间的信息,这有助于开发 sandbox爆发。
    当然,有人可能会争辩说,防止信息泄露不是 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
    ; movzx esi, sil ; skipped based on assumptions
    mov [buf + rsi], dil
    ret
    但是如果我们可以从我们自己的代码中调用这个函数,我们可以提供一个值 rsi出了 [0,255]范围并写入超出缓冲区范围的内存。
    当然,编译器实际上不会生成这样的代码,因为如上所述,被调用者有责任对其参数进行零扩展或符号扩展,而不是调用者的责任。我认为,这是让接收值的代码始终假设高位中有垃圾并明确删除它的一个非常实际的原因。
    (对于英特尔 IvyBridge 及更高版本(移动消除),编译器希望将零扩展到不同的寄存器,以至少避免延迟,如果不是前端吞吐量成本, movzx 指令。)

    关于linux - x86-64 SysV ABI 中参数和返回值寄存器的高位是否允许垃圾?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40475902/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com