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

Inv*_*ion 5 compiler-construction x86 assembly

在调试某些软件时,我注意到在很多情况下,子程序之间插入了INT3指令.

这是一个例子.

我假设这些技术上并没有在'之间'插入函数,而是在它们之后,为了暂停执行,如果子程序retn在最后没有执行,无论出于何种原因.

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

MSa*_*ers 7

不正确的假设.

他们在功能之间填充,而不是之后.并且随机决定跳过指令的CPU被破坏并且应该被丢弃.

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

  • 由于它永远都不应执行,因此从理论上讲“ 0x00”就可以了。但是实际上,CPU只是将字节解码为x86指令,而不知道函数边界在哪里,因此使函数之间的填充为有效指令,这样在解码(或推测执行)时不会减慢CPU的速度也是一个优点。但要指出的是,在极少数情况下,INT3会导致早期/嘈杂的故障,这种情况是间接跳转或损坏的返回地址将您带入填充区的;通常,这可能比通过NOP填充默默地进入下一个功能要好(例如Linux上的典型功能)。 (2认同)
  • 次要注释:int3是一个字节,int 3是两个字节。两种指令的行为略有不同。 (2认同)

Pet*_*des 6

在Linux上,gcc和clang pad用0x90(NOP)来对齐函数.(当链接.o到不均匀尺寸的部分时,甚至连接器也会这样做).

通常没有任何特别的优点,除非CPU在函数结束时没有对RET指令进行分支预测.在这种情况下,NOP不会从发现正确分支目标时需要时间恢复的任何事情上启动CPU.


函数的最后一条指令可能不是RET; 它可能是间接JMP(例如通过函数指针进行尾调用).在这种情况下,分支预测更有可能失败.(CALL/RET对由返回栈特别预测.注意RET是伪装的间接JMP;它基本上是a jmp [rsp]和a 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的SnB写作

(图片复制自David Kanter的Haswell撰写.虽然我与他的Sandybridge写作有关.他们都很优秀.)

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

  • UD2和INT3在停止推测方面可能相似。关于雾(我可以找到),它没有太多话要说,但是可以从《英特尔优化参考手册》中找到:_Assembly / Compiler Coding Rule14。(M影响,L通用性)当存在间接分支时,请尝试放置最可能的分支。间接分支之后紧接的间接分支的目标。或者,如果间接分支是通用的,但分支预测硬件无法预测它们,则在UD2指令之后跟随间接分支,这将阻止处理器解码掉线路径。 (2认同)