当前位置: 首页 > 面试题库 >

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

董品
2023-03-14
问题内容

x86-64 SysV
ABI除其他事项外,指定如何在寄存器中传递函数参数(在中的第一个参数rdi,然后依次rsi类推),以及如何将整数返回值传递回(对于真正的大值rax,则传递rdx)。

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

例如,对于以下功能:

void foo(unsigned x, unsigned y);


x将被传入rdiyrsi,但他们只是32位。不要的高32位rdirsi需求为零?直观上,我会假设是的,但是所有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(和其他编译器是相似的):

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文档,但是我找不到标准中是否记录了这种行为。

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

参数成本

处理参数值时,会给被调用方的实现带来成本。以及在处理参数时的功能。当然,由于该函数可以有效地忽略高位,所以该开销通常为零,或者由于可以使用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

ret仅需要将高位清零,就需要4条指令中的2条(忽略)。在实践中,使用消除运动可能很便宜,但是似乎仍然要付出很大的代价。

另一方面,如果ABI将高位指定为零,则对于调用者来说,我真的看不到类似的费用。因为rdirsi和其他传递参数的寄存器是 临时的
(即可以被调用者覆盖),所以您只有两种情况(我们看一下rdi,但是将其替换为您选择的参数reg):

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

  2. 传递给函数的值rdi 功能后需要。在这种情况下,由于rdi已保存了调用者,因此调用者mov无论如何都需要对保存的寄存器执行值的a。通常,您可以对其进行组织,以使该值 被调用方保存的寄存器(例如rbx)中开始,然后将其移至edilike mov edi, ebx,因此无需花费任何费用。

我看不到在很多情况下调零会给调用者带来很多费用。例如,如果最后指定的指令中需要64位数学运算,则可能会出现一些示例rdi。不过,这似乎很少见。

退货成本

这里的决定似乎更加中立。让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. 返回值的高位在返回之前是否需要清零?(在调用之前,是否需要将参数的高位清零?)
  2. 与该决定相关的成本/收益是什么?

第一个问题的答案是 否定的, 高位 可能 有垃圾,并且PeterCordes已经就该主题写了一个非常好的答案。

至于第二个问题,我怀疑未定义高位总体上对性能更好。一方面,使用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

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

当然,编译器实际上不会生成这样的代码,因为如上所述, 被调用方 有责任对其参数进行零扩展或符号扩展,而不是 调用方的
参数扩展。我认为,这是一个非常实际的原因,要让接收值的代码始终假定高位有垃圾并明确将其删除。



 类似资料:
  • 2. x86的寄存器 x86的通用寄存器有eax、ebx、ecx、edx、edi、esi。这些寄存器在大多数指令中是可以任意选用的,比如movl指令可以把一个立即数传送到eax中,也可传送到ebx中。但也有一些指令规定只能用其中某个寄存器做某种用途,例如除法指令idivl要求被除数在eax寄存器中,edx寄存器必须是0,而除数可以在任意寄存器中,计算结果的商数保存在eax寄存器中(覆盖原来的被除数

  • 我读到了关于和的相互矛盾的语句。 这个答案说: 使用must-revalidate时,如果服务器不响应重新验证请求,浏览器/代理应该返回504错误。在没有缓存的情况下,它只显示缓存的内容 然而,“只显示缓存的内容”而不进行重新验证与https://datatracker.ietf.org/doc/html/RFC7234#section-5.2.1.4中的内容相矛盾: “no-cache”requ

  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 8位寄存器,包括了Intel x86 Assembly& Microarchitecture 8位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 前四个16位寄存器可以将其高低字节和低半字节直接作为自己的寄存器进行访问: AH和AL是AX寄存器的上半部分和下半部分。 BH和BL是BX寄存器的上半部分和下

  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 32位寄存器,包括了Intel x86 Assembly& Microarchitecture 32位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 英特尔生产80386时,他们从16位处理器升级到了32位处理器。32位处理意味着两件事:要处理的数据都是32位,而要访问的内存地址是32位。为此,他们仍然

  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 16位寄存器,包括了Intel x86 Assembly& Microarchitecture 16位寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 当英特尔定义最初的8086时,它是一个具有20位地址总线的16位处理器(请参阅下文)。他们定义了8个通用16位寄存器-但为某些指令赋予了它们特定的作用:

  • 问题内容: 我可以定义setter方法以返回此方法而不是void吗? 喜欢: 然后我可以使用新的ClassA()。setItem1()。setItem2() 问题答案: 关于JavaBeans规范有很多误解。 它存在的主要原因是统一的Java“组件”模型。这是一种使用反射与Java对象进行编程交互的方式。该API本身名为JavaBeans Introspection 。请看一下示例用法,您将比普通