50

我在 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
    }
}
4

3 回答 3

57

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

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

于 2013-12-11T18:16:42.170 回答
22

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

至于 的含义,英特尔指令集参考rep ret中没有提到这个指令序列,并且文档也不是很有帮助:rep

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

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

现在,来自AMD 指令集参考(1.2.6 重复前缀):

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

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

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

于 2013-12-11T17:59:49.833 回答
20

正如 Trillian 的回答所指出的,AMD K8 和 K10 在何时ret是分支目标或遵循条件分支(作为贯穿目标)时存在分支预测问题。那是因为ret只有 1 个字节长。

repz ret:为什么这么麻烦?有一些额外的细节,说明了为什么这给 K8 和巴塞罗那带来了困难的具体微架构原因。


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

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


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

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

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

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

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


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

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

rep ret没有经常使用到足以加起来很多东西。例如,firefox 及其映射的所有库只有大约 9k 个rep ret. 所以这大约是 4k 字节,跨越许多文件。(并且比这更少的内存,因为动态库中的许多函数从未被调用过。)

# 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

rep ret包括 Firefox 映射的所有库中的所有函数,而不仅仅是它曾经调用的函数。这有点相关,因为跨函数的较低代码密度意味着您的调用分布在更多内存页面上。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

于 2015-09-02T07:47:16.300 回答