"rep ret"是什么意思?

Dev*_*lus 42 x86 assembly

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

    rep ret /* REP to avoid AMD branch prediction penalty */
Run Code Online (Sandbox Code Playgroud)

当然我可以理解评论:)但是这个前缀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
    }
}
Run Code Online (Sandbox Code Playgroud)

Igo*_*sky 45

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

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

  • @Blindy:从gcc 8.1开始(2018年5月发布),默认输出`ret`. (4认同)
  • AFAICT,这个问题出现在AMD K8和K10(巴塞罗那)CPU中.它绝对不会出现在Bulldozer及其后期.最后的K10台式机CPU是Phenom II.gcc可能会在接下来的几年内停止默认为"rep ret". (2认同)
  • @PeterCordes,2018 年,它仍然存在。 (2认同)

Tri*_*ian 18

显然,当分支的目标或通过是ret指令时,某些AMD处理器的分支预测器表现很差,并且添加rep前缀可以避免这种情况.

关于其含义rep ret,英特尔指令集参考中没有提及此指令序列,并且文档rep不是很有帮助:

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

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

现在,从AMD指令集引用(1.2.6 Repeat Prefixes):

前缀只应与此类字符串指令一起使用.

通常,重复前缀只应在上面的表1-6,1-7和1-8中列出的字符串指令中使用[不包含ret].

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

  • 这不是未定义的行为.IIRC,英特尔的手册说,不适用于指令的前缀将被忽略.问题在于它可能无法面向未来:前缀字节可能在未来的指令集扩展中为该指令获得新的含义,或者整个前缀+操作码序列可能意味着其他内容."rep ret"不会发生这种情况,因为gcc默认使用它. (4认同)
  • 是的,根据建筑结构它是未定义的...如果你喜欢`rep ret`,你可能会喜欢`rep nop` :-) (2认同)

Pet*_*des 15

作为翠莲的回答指出,AMD K8和K10与分支预测的问题时,ret是转移目标,或遵循条件分支.

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


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

gcc rep ret默认仍然使用(没有-mtune=intel,-march=haswell或者什么).所以大多数Linux二进制文件都repz ret在其中.

rep ret一旦K10彻底过时,gcc可能会在几年内停止使用.再过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-用于生成asd输出到stdout以查看它何时执行此操作.)

因此,如果我们猜测16中的那个rep ret最终创建了额外的填充,其中a 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
Run Code Online (Sandbox Code Playgroud)

rep ret取决于firefox映射的所有库中的所有函数,而不仅仅是它调用的函数.这有点相关,因为跨函数的代码密度越低意味着您的调用分布在更多的内存页面上.ITLB和L2-TLB只有有限数量的条目.本地密度对L1I $(和Intel的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对该目录的权限而施加的访问限制.