为什么编译器要在子程序之间插入 INT3 指令?

问题描述 投票:0回答:3

在调试某些软件时,我注意到很多情况下都会在子例程之间插入 INT3 指令。

我假设这些从技术上来说并不是插入在“函数之间”,而是插入在它们之后,以便在子例程由于某种原因在最后没有执行

retn
时暂停执行。

我的假设正确吗?如果不是的话,这些说明的目的是什么?

assembly compiler-construction x86
3个回答
10
投票

在 Linux 上,gcc 和 clang 使用 0x90 (NOP) 来对齐函数。 (当将

.o
与大小不均匀的部分链接时,即使链接器也会这样做)。

通常没有任何特别的优势,除非 CPU 对函数末尾的 RET 指令没有分支预测。在这种情况下,当发现正确的分支目标时,NOP 不会让 CPU 启动任何需要时间恢复的任务。


函数的最后一条指令可能不是 RET;它可能是间接 JMP(例如通过函数指针进行尾调用)。在这种情况下,分支预测更有可能失败。 (CALL/RET 对是由返回堆栈专门预测的。请注意,RET 是变相的间接 JMP;它基本上是一个

jmp [rsp]
和一个
add rsp, 8
)。

间接 JMP 或 CALL 的默认预测(当没有可用的分支目标缓冲区预测时)是跳转到下一条指令。 (显然,不进行预测并停止直到知道正确的目标不是一种选择,或者默认预测足以用于跳转表。)

如果默认预测导致推测性地执行 CPU 无法轻易中止的某些内容,例如 FP sqrt 或可能是微编码的内容,这会增加分支错误预测的惩罚。如果推测执行的指令导致 TLB 未命中、触发硬件分页或以其他方式污染缓存,情况会更糟。

INT 3 这样只生成异常的指令不会出现任何这些问题。 CPU 不会尝试提前执行 INT,因此不会发生任何不良情况。 IIRC,如果下一条指令默认预测没有用,建议在间接 JMP 之后放置类似的内容。


由于函数之间存在随机垃圾,即使预解码包含 RET 的 16B 机器代码块也可能会减慢速度。现代 CPU 以 4 条指令为一组并行解码,因此在后续指令已解码之前它们无法检测到 RET。 (这与推测执行不同)。避免在无条件分支(如 RET)之后的字节中缓慢解码长度更改前缀很有用,因为这会延迟分支的解码。

LCP 停顿仅影响 Intel CPU:AMD 在其 L1 缓存中标记指令边界,并在更大的组中进行解码。 (英特尔使用解码的 uop 缓存来获得高吞吐量,而无需每次循环中实际解码的功耗。)

请注意,在 Intel CPU 中,指令长度查找发生在实际解码的更早阶段。例如,Sandybridge 前端如下所示:

David Kanter's SnB writeup

(图表复制自 David Kanter 的 Haswell 文章。不过,我链接到了他的 Sandybridge 文章。它们都很棒。)

另请参阅 Agner Fog 的 microarch pdf,以及 标签 wiki 中的更多链接,了解我在本答案中描述的详细信息(以及更多内容)。


7
投票

错误的假设。

它们在函数之间填充,而不是在函数之后。随机决定跳过指令的 CPU 已损坏,应该扔掉。

INT 3
的原因有两个。它是单字节指令,这意味着即使只有一个字节的空间也可以使用它。绝大多数指令都是不合适的,因为它们太长了。此外,它是“调试中断”指令。这意味着调试器可以捕获在函数之间执行代码的尝试。这不是由于忽略
retn
造成的,而是出于更简单的原因,例如使用未初始化的函数指针。


0
投票

我可以补充一点,

int3
之后的
ret
指令用于缓解称为“直线推测”(SLS)的推测性缓存侧通道漏洞。 这里有一篇关于 Linux 内核中 SLS 缓解的文章:

最终阻止直线猜测。

© www.soinside.com 2019 - 2024. All rights reserved.