CIL nop操作码的目的是什么?

82

我正在查看MSIL并注意到有很多nop指令。

MSDN文章说它们不执行任何操作,并且如果需要修补,它们用于填充空间。在调试版本中使用得比发布版要多。

我知道这些语句在汇编语言中用于对齐后面的指令,但为什么MSIL需要使用nop指令呢?

(编辑说明:被接受的答案是关于机器码NOP而不是最初问题所问的MSIL/CIL NOPs。)


19
这些答案中存在对编程语言编译器发出的MSIL nop指令和JIT编译器运行程序时发出的x86 nop指令(在该平台上)之间的混淆。[实际上,被接受的答案是关于x86 nops的,与MSIL无关。]理想情况下,这个问题应该分成两个不同的问题:MSIL :: nop的目的?本地平台nop的目的? - Steve Steiner
18个回答

110

NOP指令有几个用途:

  • 它们允许调试器在即使在生成的代码中它与其他行合并时,仍可以在该行上设置断点。
  • 它允许加载程序使用不同大小的目标偏移量修补跳转指令。
  • 它允许将一段代码块对齐到特定边界,这对于缓存很有好处。
  • 它允许增量链接覆盖代码块并调用新部分,而不必担心整个函数体的大小改变。

NOP最常用于计时目的,强制内存对齐,防止危险,占据分支延迟槽,使现有指令无效(例如跳转),或作为占位符,在程序开发中稍后替换为活动指令(或在重构会很困难或耗时的情况下替换已删除的指令)。在某些情况下,NOP可能会产生轻微的副作用;例如,在Motorola 68000系列处理器上,NOP操作码会导致流水线同步。 - mbomb007

12
这是MSIL/CIL nops(不是x86机器码的nop)在调试中的使用方式:
Nops被语言编译器(C#、VB等)用于定义隐式序列点。这些序列点告诉JIT编译器在哪里确保机器指令可以映射回IL指令。
Rick Byer在DebuggingModes.IgnoreSymbolStoreSequencePoints的博客文章中解释了一些细节。
C#还在调用指令后放置Nops,以便源中的返回站点位置是调用站点而不是调用后的行。

1
C#还在调用指令后面放置Nops,以便源中的返回站点位置是调用而不是调用后的行。我不确定是否理解这一点。您有任何可用的参考资料吗? - user492238

9

它为基于行的标记(例如断点)提供了一个机会,在这些代码中,发布构建不会发出任何标记。


断点需要放在nop上吗?为什么不直接在常规操作码上设置断点? - Dan Goldstein
4
在发布版本中,许多常规操作码都被优化掉了。这会破坏你的断点,除非在那里有一个占位符nop,否则断点仍然会指向该位置。 - Jimmy
1
在调试版本中,它们也用于提供一个指令,在代码没有指令的地方中断。例如,开放括号。 - Greg D
1
你可能想要添加一个引用到http://blogs.msdn.com/oldnewthing/archive/2007/08/17/4422794.aspx作为来源。 - Greg D
1
感谢Greg和Jimmy将此转化为实际答案。 - harpo

6

伙计!无操作指令太棒了!它是一条什么都不做,但消耗时间的指令。在遥远的旧时代,您会使用它在关键循环中进行微调,或更重要的是作为自修改代码的填充。


我理解当它直接在硬件上运行时的用法,但 MSIL 是 JITed 的。 - Dan Goldstein
MSIL 只有在具有 JIT 的系统上才会进行 JIT 编译 - MSIL 不要求 JIT。 - plinth

6
它还可以使代码在针对特定处理器或架构进行优化时运行更快:
长期以来,处理器采用多个大致并行工作的流水线,因此两个独立指令可以同时执行。在具有两个流水线的简单处理器上,第一个可能支持所有指令,而第二个只支持子集。此外,在流水线之间存在一些停顿,当一个必须等待尚未完成的先前指令的结果时,就会发生这种情况。
在这些情况下,专用的nop指令可以强制将下一个指令放入特定的流水线(第一个或不是第一个),并改善后续指令的配对,以使nop指令的成本得到摊销。

5

我曾在一家处理器公司工作了四年,他们使用NOP指令来确保前一个操作完成后再开始下一个操作。例如:

将值加载到寄存器(需要8个周期) nop 8 将1加到寄存器

这确保了在执行添加操作之前,寄存器具有正确的值。

NOP指令还可以用于填充执行单元,例如中断向量必须具有特定大小(32个字节),因为向量0的地址是0,向量1的地址是0x20等等,所以编译器在必要时会插入NOP指令。


4

虽然有些晚了50年,但还是来得及。

Nop指令在手动编写汇编代码时非常有用。如果您需要删除代码,则可以将旧的操作码替换为nop指令。

同样地,您可以通过覆盖某些操作码并跳转到其他位置来插入新代码。在那里,您放置被覆盖的操作码,并插入新代码。准备好后再跳回原来的位置。

有时候必须使用可用的工具。在某些情况下,这只是一个非常基本的机器码编辑器。

现在,随着编译器的出现,这些技术已经没有任何意义了。


4

他们可能会在调试时使用它们来支持编辑并继续。这为调试器提供了空间,以便替换旧代码而不更改偏移量等内容。


4
我在VS中为"编辑和继续"实现了调试器支持(我没有实现CLR或编译器部分)。Nop是故事的一部分,以确保从旧版本到新版本方法的正确映射(特别是在跳出异常处理代码的情况下)。然而,替换MSIL是按整个函数来完成的。对于CLR托管代码,不需要在msil中留出空间来完成该部分。你在这里说的是关于本地Edit and continue的正确性。 - Steve Steiner

3

一种有些不正统的用法是 NOP-Slides,它们被用于缓冲区溢出漏洞。


找到了这个,太棒了 :) - Suraj Jain
请更正您的链接,它无法工作。请确保您提交的是网络存档链接,因为它们不会出现404未找到错误。 - Suraj Jain
目前正在研究shellcode并希望了解更多关于NOP的上下文。不得不回溯一下,但这是一个存档链接:https://web.archive.org/web/20110124015428/http://www.phreedom.org:80/solar/honeynet/scan20/scan20.html - saniboy

3

其中一个经典用途是使您的调试器始终将源代码行与IL指令相关联。


由于MSIL是在运行时JIT编译的,因此可能会丢失映射(例如,本机指令没有唯一的MSIL指令)。 NOPs被用作语言编译器到JIT编译器的通信机制,以保持这种映射。 - Steve Steiner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接