草庐IT

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

coder 2023-06-18 原文

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/

    有关linux - x86-64 SysV ABI 中参数和返回值寄存器的高位是否允许垃圾?的更多相关文章

    1. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    2. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

      我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

    3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

      我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

    4. ruby - 检查数组是否在增加 - 2

      这个问题在这里已经有了答案:Checktoseeifanarrayisalreadysorted?(8个答案)关闭9年前。我只是想知道是否有办法检查数组是否在增加?这是我的解决方案,但我正在寻找更漂亮的方法:n=-1@arr.flatten.each{|e|returnfalseife

    5. ruby - 检查字符串是否包含散列中的任何键并返回它包含的键的值 - 2

      我有一个包含多个键的散列和一个字符串,该字符串不包含散列中的任何键或包含一个键。h={"k1"=>"v1","k2"=>"v2","k3"=>"v3"}s="thisisanexamplestringthatmightoccurwithakeysomewhereinthestringk1(withspecialcharacterslike(^&*$#@!^&&*))"检查s是否包含h中的任何键的最佳方法是什么,如果包含,则返回它包含的键的值?例如,对于上面的h和s的例子,输出应该是v1。编辑:只有字符串是用户定义的。哈希将始终相同。 最佳答案

    6. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

      我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

    7. ruby - 检查日期是否在过去 7 天内 - 2

      我的日期格式如下:"%d-%m-%Y"(例如,今天的日期为07-09-2015),我想看看是不是在过去的七天内。谁能推荐一种方法? 最佳答案 你可以这样做:require"date"Date.today-7 关于ruby-检查日期是否在过去7天内,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/32438063/

    8. ruby - 如何验证 IO.copy_stream 是否成功 - 2

      这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

    9. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

      我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

    10. ruby - 在 Windows 机器上使用 Ruby 进行开发是否会适得其反? - 2

      这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby​​-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub

    随机推荐