正如 Trillian 的回答所指出的,AMD K8 and K10 have a problem with branch prediction 当 ret 是分支目标时,或者遵循条件分支(作为贯穿目标)。那是因为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 而施加的访问限制目录的权限。