“rep ret”是什么意思?

时间:2013-12-11 17:48:55

标签: assembly x86

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

3 个答案:

答案 0 :(得分:44)

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

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

答案 1 :(得分:18)

显然,当某个分支的目标或突破是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]。

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

答案 2 :(得分:15)

正如Trillian的答案所指出的,AMD K8 and K10 have a problem with branch predictionret是分支目标时,或者遵循条件分支。

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

处理器手册警告未来的处理器可以不同地解释它不会修改的前缀和指令的组合。这在理论上是正确的,但是没有人会制造出能够运行大量现有二进制文件的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 c3识别为rep ret }。如果遗留二进制文件的向后兼容性是不可能的(因为它无法在功率和晶体管方面高效完成),IDK您正在考虑什么样的时间框架。超过15年,除非这只是部分市场的CPU。

因此使用rep ret是安全的,因为其他人都已经这样做了。使用ret 0是一个坏主意。在新代码中,将rep ret用于其他几年仍然是一个好主意。可能还没有太多的AMD PhenomII CPU,但它们的速度很慢,没有额外的返回地址误预测,或者没有问题。

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

因此,如果我们猜测16 ret中的那个最终创建额外的填充,其中rep刚刚达到所需的对齐,并且额外的填充到达8B边界,这意味着每个rep ret的平均成本为8 * 1/16 =半个字节。

rep ret经常使用不足以累加任何内容。例如,firefox与它映射的所有库只有〜{9k # 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 个实例。因此,在许多文件中大约有4k字节。 (并且RAM少于此,因为动态库中的许多函数永远不会被调用。)

rep ret

在firefox映射的所有库中的所有函数中计算rep ret,而不仅仅是它调用的函数。这有点相关,因为跨函数的代码密度越低意味着您的调用分布在更多的内存页面上。 ITLB和L2-TLB只有有限数量的条目。本地密度对L1I $(以及Intel的uop-cache)很重要。无论如何,/proc/<pid>/map_files/的影响很小。

我花了一分钟时间考虑了/proc/<pid>/maps该进程所有者无法访问的原因,但mmap(2)是。setuid(nobody)。如果UID = root进程(例如来自suid-root二进制文件)x for other sa 0666文件位于0700目录中,那么AAA : aaaaaaaaaaaaaaaaaa B : bbbbb CCCCC : cccccccc ,运行该二进制文件的任何人都可以绕过访问限制由于缺少对目录的AAA : aaaaaaaaaaaaaaaaaa B : bbbbb CCCCC : cccccccc 权限而强加的。