我在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
}
}
有一个完整的博客以这个指令命名。第一篇文章描述了它背后的原因:http://repzret.org/p/repzret/
基本上,AMD的分支预测器中存在一个问题,即在您引用的代码(以及其他一些情况)中,单个字节的ret
立即跟随条件跳转,解决方法是添加rep
前缀,CPU会忽略该前缀,但修复了预测器的损失。
显然,当分支的目标或故障是ret
指令时,一些AMD处理器的分支预测器表现不佳,添加rep
前缀可以避免这种情况。
至于rep ret
的含义,《英特尔指令集参考》中没有提到这个指令序列,rep
的文档也没有太大帮助:
与非字符串指令一起使用时,REP前缀的行为是未定义的。
这至少意味着rep
不必以重复的方式表现。
现在,从AMD指令集参考(1.2.6重复前缀):
前缀只能与此类字符串指令一起使用。
通常,重复前缀应仅用于上面表1-6、1-7和1-8中列出的字符串指令[其中不包含ret]。
因此,这看起来确实像是未定义的行为,但可以假设,在实践中,处理器只是忽略ret
指令上的rep
前缀。
正如Trillian的回答所指出的,当ret
是分支目标或遵循条件分支(作为贯穿目标)时,AMD K8和K10在分支预测方面存在问题。这是因为ret
只有1个字节长。
雷:为什么这么麻烦?提供了一些额外的细节,说明了为什么K8和巴塞罗那会遇到困难的具体微观结构原因。
避免将1字节ret
作为可能的分支目标:
AMD针对K10(巴塞罗那)的优化指南建议在这种情况下使用3字节ret 0
,它从堆栈中弹出零字节并返回。这个版本明显比英特尔上的rep ret
差。具有讽刺意味的是,在后来的AMD处理器(推土机及以后的处理器)上,它也比rep ret
更糟糕。因此,没有人在AMD的Family 10优化指南更新的基础上改用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 c3
识别为rep ret
。如果传统二进制文件的向后兼容性是不可能的(因为在功率和晶体管方面无法高效地实现),IDK你会看到什么样的时间框架。要比15年长得多,除非这是一款只占部分市场的CPU。
所以使用rep ret
是安全的,因为其他人都已经在做了。使用ret 0
是个坏主意。在新的代码中,再使用rep ret
几年可能仍然是个好主意。AMD PhenomII CPU可能已经不多了,但它们足够慢,没有额外的返回地址预测错误或w/e。问题是
成本很小。在大多数情况下,它最终不会占用任何额外的空间,因为它后面通常都是nop
填充。然而,在确实导致额外填充的情况下,最坏的情况是需要15B的填充才能到达下一个16B的边界。在这种情况下,gcc可以仅对准8B。(如果需要10个或更少的nop字节,则使用.p2align 4,,10;
与16B对齐,然后使用.p2align 3
始终与8B对齐。使用gcc -S -o-
向stdout生成asm输出,以查看何时执行此操作。)
因此,如果我们猜测每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缓存)很重要。无论如何,rep ret
的影响非常小。
我花了一分钟的时间思考了进程所有者无法访问/proc/<pid>/map_files/
的原因,但/proc/<pid>/maps
是。如果UID=root进程(例如来自suid根二进制文件)mmap(2)
是0700目录中的0666文件,那么setuid(nobody)
也是,任何运行该二进制文件的人都可以绕过目录上缺乏x for other
权限所施加的访问限制。