Linux 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

x86-64 SysV ABI除其他外,还指定了如何在寄存器中传递函数参数(第一个参数是
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地址被占用的函数是否允许进行这种折叠非常重要。

    这里似乎有两个问题:

  • 返回值的高位是否需要在返回前归零?(调用前是否需要将参数的高位置零?)
  • 与此决策相关的成本/收益是什么
  • 第一个问题的答案是:不,高位可能有垃圾,彼得·科尔德斯已经就此写了一篇文章

    至于第二个问题,我怀疑保留高位未定义总体上对性能更好。一方面,当使用32位操作时,预先零扩展值不会带来额外的成本。但另一方面,事先将高位归零并不总是必要的。如果允许在高位使用垃圾,那么可以让接收值的代码在实际需要时只执行零扩展(或符号扩展)

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

    信息泄露 当结果的高位未清除时,它们可能保留fra
    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