我正在浏览 MSIL 并注意到 MSIL 中有很多nop指令。
MSDN 文章说,如果操作码被修补,它们不会执行任何操作并用于填充空间。它们在调试版本中的使用比在发布版本中更多。
我知道这些语句在汇编语言中用于对齐后面的指令,但是为什么 MSIL 中需要 MSIL nop 呢?
(编者注:接受的答案是关于机器代码 NOP,而不是问题最初询问的 MSIL/CIL NOP。)
NOP 有多种用途:
以下是调试使用MSIL / CIL nops(不是 x86 机器代码nop
)的方式:
语言编译器(C#、VB 等)使用 Nop 来定义隐式序列点。这些告诉 JIT 编译器在哪里确保机器指令可以映射回 IL 指令。
Rick Byer 关于DebuggingModes.IgnoreSymbolStoreSequencePoints的博客文章解释了一些细节。
C# 还在调用指令之后放置 Nops,以便源中的返回站点位置是调用而不是调用之后的行。
它为代码中的基于行的标记(例如断点)提供了机会,其中发布版本不会发出任何内容。
老兄!无操作太棒了!这是一个除了消耗时间什么都不做的指令。在昏暗的黑暗时代,您可以使用它来对关键循环中的时序进行微调,或者更重要的是作为自修改代码的填充物。
在针对特定处理器或架构进行优化时,它还可以使代码运行得更快:
长期以来,处理器采用多条大致并行工作的流水线,因此可以同时执行两条独立的指令。在具有两个管道的简单处理器上,第一个可能支持所有指令,而第二个仅支持一个子集。此外,当必须等待尚未完成的前一条指令的结果时,流水线之间会出现一些停顿。
在这种情况下,一个专用的nop可能会强制下一条指令进入特定的管道(第一条,或者不是第一条),并改进后续指令的配对,从而使nop的成本 超过摊销。
在我最近(四年)工作的一个处理器中,NOP 用于确保在下一个操作开始之前完成上一个操作。例如:
将值加载到寄存器(需要 8 个周期) nop 8 add 1 to register
这确保寄存器在添加操作之前具有正确的值。
另一个用途是填充执行单元,例如必须具有一定大小(32 字节)的中断向量,因为向量 0 的地址是 0,向量 1 的地址是 0x20 等等,所以编译器会在其中放入 NOP需要。
他们可以在调试时使用它们来支持编辑并继续。它为调试器提供了工作空间,可以在不更改偏移量等的情况下用新代码替换旧代码。
50 年太晚了,但是嘿。
如果您手动输入汇编代码,则 Nop 很有用。如果你必须删除代码,你可以 nop 旧的操作码。
类似地,您可以通过覆盖一些操作码并跳转到其他地方来插入新代码。在那里放置被覆盖的操作码,并插入新代码。准备好后你跳回去。
有时您必须使用可用的工具。在某些情况下,这只是一个非常基本的机器代码编辑器。
如今,对于编译器,这些技术不再有意义。
它们的一个典型用途是使您的调试器始终可以将源代码行与 IL 指令相关联。
在软件破解场景中,解锁应用程序的经典方法是使用 NOP 修补检查密钥或注册或时间段或诸如此类的行,这样它就什么也不做,只是继续启动应用程序,就好像它已注册一样.
我还看到代码中的 NOP 会修改自身以混淆它作为占位符的作用(非常古老的复制保护)。
正如 ddaa 所说,nops 让您考虑堆栈中的差异,因此当您覆盖返回地址时,它会跳转到 nop 雪橇(连续很多 nop),然后正确命中可执行代码,而不是跳转到一些不是开始的指令中的字节。
NOP-Slides是一个有点非正统的用法,用于缓冲区溢出漏洞利用。
它们允许链接器将较长的指令(通常是长跳转)替换为较短的指令(短跳转)。NOP 占用了额外的空间 - 代码无法移动,因为它会阻止其他跳转工作。这发生在链接时,因此编译器无法知道长跳转或短跳转是否合适。
至少,这是他们的传统用途之一。
这不是您特定问题的答案,但在过去,如果您无法用其他有用的指令填充它,您可以使用 NOP 来填充分支延迟槽。
.NET 编译器是否对齐 MSIL 输出?我想它可能对加快对 IL 的访问很有用......另外,我的理解是它被设计为可移植的,并且在其他一些硬件平台上需要对齐访问。
我学到的第一个程序集是 SPARC,所以我熟悉分支延迟槽,如果你不能用另一条指令填充它,通常是你要放在分支指令之上的指令或在循环中增加一个计数器,你使用一个NOP。
我不熟悉破解,但我认为使用 NOP 覆盖堆栈是很常见的,因此您不必准确计算恶意函数的开始位置。
我使用 NOP 自动调整进入 ISR 后累积的延迟。非常方便地确定时间。