在调试某些软件时,我注意到在很多情况下,子程序之间插入了INT3指令。
我认为这些不是技术上插入'在'之间的函数,而是在它们之后,为了在子程序最后没有执行retn
的情况下暂停执行,无论出于何种原因。
我的假设是否正确?如果不是,这些说明的目的是什么?
答案 0 :(得分:7)
不正确的假设。
他们在功能之间填充,而不是之后。并且随机决定跳过指令的CPU会被破坏,应该被丢弃。
INT 3
的原因是双重的。它是一个单字节指令,这意味着即使只有一个字节的空间也可以使用它。绝大多数说明都不合适,因为它们太长了。此外,它是“调试中断”指令。这意味着调试器可以捕获在函数之间执行代码的尝试。这不是由于忽略retn
引起的,而是出于更简单的原因,例如使用未初始化的函数指针。
答案 1 :(得分:6)
在Linux上,gcc和clang pad用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缓存中标记指令边界,并在较大的组中进行解码。 (英特尔使用解码后的高速缓存来获得高吞吐量,而无需每次在循环中实际解码的电源成本。)
请注意,在Intel CPU中,指令长度查找发生在比实际解码更早的阶段。例如,Sandybridge前端看起来像这样:
(图片来自David Kanter的Haswell写作。虽然我与他的Sandybridge写作有关。他们都非常出色。)
另请参阅Agner Fog's microarch pdf以及x86标记wiki中的更多链接,了解我在此答案中所描述的内容(以及更多内容)。