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

2024-11-04 08:43:00
admin
原创
182
摘要:问题描述:除其他事项外,x86-64 SysV ABI 指定了如何在寄存器中传递函数参数(第一个参数在rdi,然后rsi等等),以及如何传回整数返回值(对于非常大的值,在rax然后rdx)。然而,我找不到在传递小于 64 位的类型时参数或返回值寄存器的高位应该是什么。例如,对于以下函数:void foo(un...

问题描述:

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

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

例如,对于以下函数:

void foo(unsigned x, unsigned y);

...x将传入rdi和,但它们只有 32 位。和yrsi高 32 位是否需要为零?直观地讲,我认为是的,但是gcc、clang 和 icc生成的代码在开始时都有特定指令来将高位清零,因此似乎编译器的假设并非如此。rdi`rsi`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  

仅需4 条指令中的 2 条(忽略ret)即可将高位清零。使用 mov 消除法,这在实践中可能很便宜,但似乎成本仍然很高。

另一方面,如果 ABI 指定高位为零,我实际上看不到调用者有类似的相应成本。由于rdirsi和其他参数传递寄存器是临时的(即,可以被调用者覆盖),因此您只有几种情况(我们查看rdi,但将其替换为您选择的参数寄存器):

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

  2. 传递给函数的值在函数执行后需要。在这种情况下,由于rdi rdi调用者保存的,因此调用者mov无论如何都需要将值的 移至被调用者保存的寄存器。通常可以组织它,以便值从被调用者保存的寄存器(例如)开始rbx,然后移动到edimov 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对于地址已被占用的函数,是否实际上允许进行这样的折叠,这仍是一个有待商榷的问题。


解决方案 1:

看起来您这里有两个问题:

  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
      ; movzx esi, sil       ; skipped based on assumptions
    mov [buf + rsi], dil
    ret

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

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

(对于 Intel IvyBridge 及更高版本(mov 消除),编译器希望将零扩展到不同的寄存器,以至少避免指令的延迟,如果不是前端吞吐量成本的话movzx。)

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1120  
  IPD(Integrated Product Development,集成产品开发)流程是一种广泛应用于高科技和制造业的产品开发方法论。它通过跨职能团队的紧密协作,将产品开发周期缩短,同时提高产品质量和市场成功率。在IPD流程中,CDCP(Concept Decision Checkpoint,概念决策检查点)是一个关...
IPD培训课程   75  
  研发IPD(集成产品开发)流程作为一种系统化的产品开发方法,已经在许多行业中得到广泛应用。它不仅能够提升产品开发的效率和质量,还能够通过优化流程和资源分配,显著提高客户满意度。客户满意度是企业长期成功的关键因素之一,而IPD流程通过其独特的结构和机制,能够确保产品从概念到市场交付的每个环节都围绕客户需求展开。本文将深入...
IPD流程   66  
  IPD(Integrated Product Development,集成产品开发)流程是一种以跨职能团队协作为核心的产品开发方法,旨在通过优化资源分配、提高沟通效率以及减少返工,从而缩短项目周期并提升产品质量。随着企业对产品上市速度的要求越来越高,IPD流程的应用价值愈发凸显。通过整合产品开发过程中的各个环节,IPD...
IPD项目管理咨询   76  
  跨部门沟通是企业运营中不可或缺的一环,尤其在复杂的产品开发过程中,不同部门之间的协作效率直接影响项目的成败。集成产品开发(IPD)作为一种系统化的项目管理方法,旨在通过优化流程和增强团队协作来提升产品开发的效率和质量。然而,跨部门沟通的复杂性往往成为IPD实施中的一大挑战。部门之间的目标差异、信息不对称以及沟通渠道不畅...
IPD是什么意思   70  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用