【问题标题】:What does `rep ret` mean?'rep ret' 是什么意思?
【发布时间】:2013-12-29 20:27:29
【问题描述】:

我在 Visual Studio 2008 上测试了一些代码并注意到 security_cookie。我可以理解它的重点,但我不明白这个指令的目的是什么。

    rep ret /* REP to avoid AMD branch prediction penalty */

当然我可以理解评论:) 但是这个前缀exaclty 在ret 的上下文中做了什么,如果ecx 是!= 0 会发生什么?显然,当我调试它时,ecx 的循环计数被忽略了,这是意料之中的。

我在这里找到的代码(由编译器注入以确保安全):

void __declspec(naked) __fastcall __security_check_cookie(UINT_PTR cookie)
{
    /* x86 version written in asm to preserve all regs */
    __asm {
        cmp ecx, __security_cookie
        jne failure
        rep ret /* REP to avoid AMD branch prediction penalty */
failure:
        jmp __report_gsfailure
    }
}

【问题讨论】:

  • 只要 google 一下“amd rep ret”,您就会发现很多优秀的内容,包括 AMD 优化指南的链接。我们尽量不在这里复制它们。
  • 谷歌“amd rep ret”现在提出了这篇stackoverflow文章:-(

标签: assembly x86 micro-optimization branch-prediction


【解决方案1】:

显然,当分支的目标或失败是 ret 指令时,某些 AMD 处理器的分支预测器表现不佳,添加 rep 前缀可以避免这种情况。

至于rep ret的含义,Intel Instruction Set Reference中没有提到这个指令序列,rep的文档也不是很有帮助:

与非字符串指令一起使用时,REP 前缀的行为未定义。

这至少意味着rep 不必以重复的方式运行。

现在,来自AMD instruction set reference(1.2.6 重复前缀):

前缀只能与此类字符串指令一起使用。

一般来说,重复前缀只能用在上面表1-6、1-7、1-8列出的字符串指令中[不包含ret]。

所以这看起来确实是未定义的行为,但可以假设,在实践中,处理器只是忽略 ret 指令上的 rep 前缀。

【讨论】:

  • 是的,我在询问之前也查看了英特尔手册,但我从评论中感到愤怒,我不会在那里找到有用的东西(实际上我没有),正如评论已经说过的那样无论如何都是关于 AMD 的。
  • @Devolus Right,AMD 的文档也说了同样的话。我猜如果微软在 CRT 中使用它,他们一定有理由认为它是 nop 并且会保持这种状态。
  • 由于是 Visual Studio 2008,可能已经在较新的版本中进行了更改。
  • 是的,根据架构它是未定义的......如果你喜欢rep ret,你可能会喜欢rep nop :-)
  • 这不是未定义的行为。 IIRC,英特尔的手册说不适用于指令的前缀将被忽略。问题在于它可能不会面向未来:前缀字节可能会在未来的指令集扩展中为该指令获得新的含义,或者整个前缀+操作码序列可能意味着其他东西。 rep ret 不会发生这种情况,因为 gcc 默认使用它。
【解决方案2】:

有一个完整的博客以这条指令命名。第一篇文章描述了其背后的原因:http://repzret.org/p/repzret/

基本上,当单字节 ret 紧跟在您引用的代码中的条件跳转之后(以及其他一些情况),AMD 的分支预测器中存在问题,解决方法是添加 @987654323 @ 前缀,被 CPU 忽略,但修复了预测器惩罚。

【讨论】:

  • AFAICT,该问题存在于 AMD K8 和 K10(巴塞罗那)CPU 中。它绝对不会出现在 Bulldozer 及更高版本中。最后的 K10 桌面 CPU 是 Phenom II。 gcc 可能会在未来几年的某个时候停止默认为 rep ret
  • @PeterCordes,2018 年,它仍然存在。
  • @Blindy:从 gcc 8.1(2018 年 5 月发布)开始,默认输出 ret
【解决方案3】:

正如 Trillian 的回答所指出的,AMD K8 and K10 have a problem with branch predictionret 是分支目标时,或者遵循条件分支(作为贯穿目标)。那是因为ret 只有 1 个字节长。

repz ret: why all the hassle? 提供了一些额外的细节,说明了导致 K8 和巴塞罗那陷入困境的具体微架构原因。


避免将 1 字节 ret 作为可能的分支目标:

AMD 的 K10(巴塞罗那)优化指南建议在这些情况下使用 3 字节 ret 0,它会从堆栈中弹出零字节并返回。该版本比英特尔上的rep ret 差很多。具有讽刺意味的是,在后来的 AMD 处理器(Bulldozer 及更高版本)上,它也比 rep ret 更糟糕。所以根据 AMD 的 Family 10 优化指南更新,没人改用 ret 0 是件好事。


处理器手册警告说,未来的处理器可能会以不同的方式解释前缀和它不修改的指令的组合。这在理论上是正确的,但没有人会制造出无法运行大量现有二进制文件的 CPU。

gcc 默认仍然使用rep ret(没有-mtune=intel,或-march=haswell 或其他东西)。所以大多数 Linux 二进制文件的某个地方都有一个repz ret

gcc 可能会在几年内停止使用rep ret,一旦 K10 彻底过时。再过 5 或 10 年,几乎所有的二进制文件都将使用更新的 gcc 构建。再过 15 年,CPU 制造商可能会考虑将 f3 c3 字节序列重新用作不同指令的(一部分)。

仍然会有使用 rep ret 的旧式闭源二进制文件,它们没有可用的最新版本,但需要有人继续运行。因此,无论f3 c3 != rep ret 属于哪个新功能,都需要禁用(例如,使用 BIOS 设置),并让该设置实际更改指令解码器行为以将f3 c3 识别为rep ret。如果传统二进制文件的向后兼容性是不可能的(因为它不能在功率和晶体管方面有效地完成),IDK 你会看什么样的时间框架。比 15 年长得多,除非这只是部分市场的 CPU。

所以使用rep ret 是安全的,因为其他人都已经在这样做了。使用ret 0 是个坏主意。在新代码中,再使用几年rep ret 可能仍然是个好主意。周围可能没有太多 AMD PhenomII CPU,但它们足够慢,没有额外的返回地址错误预测或问题出在。


成本很小。在大多数情况下,它最终不会占用任何额外的空间,因为它通常后面跟着nop 填充。但是,在确实导致额外填充的情况下,最坏的情况是需要 15B 的填充才能到达下一个 16B 边界。在这种情况下,gcc 只能对齐 8B。 (如果需要 10 个或更少的 nop 字节,则使用 .p2align 4,,10; 对齐到 16B,然后使用 .p2align 3 始终对齐到 8B。使用 gcc -S -o- 将 asm 输出生成到 stdout 以查看它何时执行此操作。)

因此,如果我们估计 16 分之一的 rep ret 最终会创建额外的填充,而 ret 将刚好达到所需的对齐,并且额外的填充到达 8B 边界,这意味着每个 rep 都有平均成本为 8 * 1/16 = 半个字节。

rep ret 的使用频率不足以将任何东西加起来。例如,firefox 及其映射的所有库只有大约 9k 个 rep ret 实例。所以这大约是 4k 字节,跨越许多文件。 (而且比这更少的 RAM,因为动态库中的许多函数从未被调用。)

# disassemble every shared object mapped by a process.
ffproc=/proc/$(pgrep firefox)/
objdump -d "$ffproc/exe" $(sudo ls -l "$ffproc"/map_files/ |
       awk  '/\.so/ {print $NF}' | sort -u) |
       grep 'repz ret' -c
objdump: '(deleted)': No such file  # I forgot to restart firefox after the libexpat security update
9649

这在 Firefox 映射的所有库中的所有函数中都包括 rep ret,而不仅仅是它曾经调用的函数。这有点相关,因为跨函数的较低代码密度意味着您的调用分布在更多内存页面上。 ITLB 和 L2-TLB 只有有限数量的条目。本地密度对 L1I$(和英特尔的 uop-cache)很重要。无论如何,rep ret 的影响非常小。

我花了一分钟才想到一个原因,/proc/<pid>/map_files/ 是进程所有者无法访问的,但 /proc/<pid>/maps 是。如果 UID=root 进程(例如,来自 suid-root 二进制文件)mmap(2)sa 0666 文件位于 0700 目录中,那么 setuid(nobody) 会执行此操作,任何运行该二进制文件的人都可以绕过由于缺少 x for other 而施加的访问限制目录的权限。

【讨论】:

  • 从 gcc 8.1(2018 年 5 月发布)开始,默认输出ret
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-30
  • 1970-01-01
  • 2011-10-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-12
相关资源
最近更新 更多