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

2024-11-04 08:43:00
admin
原创
47
摘要:问题描述:除其他事项外,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。)

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用