为什么我们需要“nop”即微处理器8085中的无操作指令?

电器工程 微处理器
2022-01-11 10:44:03

在微处理器8085指令中,有一个机器控制操作“nop”(无操作)。我的问题是为什么我们需要无操作?我的意思是,如果我们必须结束程序,我们将使用 HLT 或 RST 3。或者如果我们想要移动到下一条指令,我们将给出下一条指令。但是为什么不做手术呢?有什么需要?

4个回答

在 CPU 和 MCU 中使用 NOP(或 NOOP,无操作)指令的一种用途是在代码中插入一点可预测的延迟。虽然 NOP 不执行任何操作,但处理它们需要一些时间(CPU 必须获取和解码操作码,所以它需要一些时间来完成)。执行一条 NOP 指令只需“浪费” 1 个 CPU 周期(通常可以从 CPU/MCU 数据表中推断出确切的数量),因此将 N 个 NOP 按顺序排列是插入可预测延迟的一种简单方法:

\$ t_{延迟} = N \cdot T_{时钟} \cdot K\$

其中 K 是处理 NOP 指令所需的周期数(通常为 1),\$T_{clock}\$ 是时钟周期。

为什么要这么做?强制 CPU 稍等片刻以等待外部(可能较慢的)设备完成其工作并向 CPU 报告数据可能很有用,即 NOP 对于同步目的很有用。

另请参阅NOP 上的相关 Wikipedia 页面

另一个用途是在内存中的某些地址和其他“汇编技巧”处对齐代码,正如Programmers.SE上的这个线程和StackOverflow 上的这个另一个线程中所解释的那样。

关于这个主题的另一篇有趣的文章

这个指向谷歌书页的链接特别提到了 8085 CPU。摘抄:

每条 NOP 指令使用四个时钟来获取、解码和执行。

编辑 (解决评论中表达的担忧)

如果您担心速度,请记住(时间)效率只是要考虑的一个参数。这完全取决于应用程序:如果您想计算 \$\pi\$ 的第 100 亿个数字,那么您唯一关心的可能就是速度。另一方面,如果您想记录来自通过 ADC 连接到 MCU 的温度传感器的数据,速度通常不是那么重要,但等待适当的时间以使 ADC 正确完成每次读取是必不可少的在这种情况下,如果 MCU 没有等待足够长的时间,则可能会获得完全不可靠的数据(我承认它会更快地获得该数据,尽管 :o)。

其他答案只考虑在某个时候实际执行的 NOP - 这很常用,但它不是 NOP 的唯一用途。

在编写可以修补的代码时,非执行 NOP 也非常有用 - 基本上,您将(或类似指令)之后用一些 NOP 填充函数。RET当您必须修补可执行文件时,您可以轻松地将更多代码添加到函数中,从原始代码开始,RET并根据需要使用尽可能多的 NOP(例如,用于长跳转甚至内联代码)并以另一个RET.

在这个用例中,没有人期望NOP执行。唯一的一点是允许修补可执行文件 - 在理论上的非填充可执行文件中,您必须实际更改函数本身的代码(有时它可能符合原始边界,但通常您仍然需要跳转) - 这要复杂得多,尤其是考虑到手动编写的程序集或优化编译器;您必须尊重可能指向某些重要代码的跳转和类似结构。总而言之,相当棘手。

当然,这在过去被大量使用,当时制作像这样的小型在线补丁很有用。今天,您将分发一个重新编译的二进制文件并完成它。仍然有一些人使用修补 NOP(执行与否,并不总是字面上NOP的 s - 例如,WindowsMOV EDI, EDI用于在线修补 - 这是您可以在系统实际运行时更新系统库而无需重新启动的那种)。

所以最后一个问题是,为什么要对一些实际上什么都不做的事情有专门的说明?

  • 这是一条实际指令——在调试或手动编码汇编时很重要。类似的指令MOV AX, AX会做同样的事情,但不会如此清楚地表明意图。
  • 填充 - “代码”只是为了提高依赖于对齐的代码的整体性能。它从来没有打算执行。一些调试器只是在反汇编中隐藏填充 NOP。
  • 它为优化编译器提供了更多空间 - 仍然使用的模式是你有两个编译步骤,第一个相当简单并产生许多不必要的汇编代码,而第二个清理,重新连接地址引用并删除无关的指令。这在 JIT 编译的语言中也很常见——.NET 的 IL 和 JVM 的字节码都NOP大量使用 s;实际编译的汇编代码不再有这些。应该注意的是,这些不是 x86- NOPs。
  • 它使在线调试更容易阅读(预归零的内存将是全部NOP,使反汇编更容易阅读)和热补丁(尽管我到目前为止更喜欢在 Visual Studio 中编辑并继续:P)。

对于执行 NOP,当然还有几点:

  • 性能,当然——这不是它在 8085 中的原因,但即使是 80486 也已经具有流水线指令执行,这使得“什么都不做”有点棘手。
  • 如 所见MOV EDI, EDI,除了文字之外,还有其他有效的 NOP NOPMOV EDI, EDI在 x86 上作为 2 字节 NOP 具有最佳性能。如果您使用两个NOPs,那将是两个要执行的指令。

编辑:

实际上,与@DmitryGrigoryev 的讨论迫使我更多地考虑这个问题,我认为这是对这个问题/答案的一个有价值的补充,所以让我添加一些额外的内容:

首先,很明显,为什么会有这样的指令mov ax, ax呢?例如,让我们看一下 8086 机器码(甚至比 386 机器码更早)的情况:

  • 有一个带有 opcode 的专用 NOP 指令0x90请注意,这仍然是许多人在汇编中写作的时候。因此,即使没有专门的NOP指令,NOP关键字(别名/助记符)仍然有用并且会映射到该指令。
  • 像这样的指令MOV实际上映射到许多不同的操作码,因为这样可以节省时间和空间 - 例如,mov al, 42“将立即字节移动到al寄存器”,它转换为0xB02A0xB0作为操作码,0x2A作为“立即”参数)。所以这需要两个字节。
  • 没有快捷操作码mov al, al(因为这基本上是一件愚蠢的事情),所以你必须使用mov al, rmb(rmb 是“寄存器或内存”)重载。这实际上需要三个字节。(虽然它可能会使用不太具体的mov rb, rmb,它应该只占用两个字节mov al, al- 参数字节用于指定源寄存器和目标寄存器;现在您知道为什么 8086 只有 8 个寄存器:D)。比较NOP,这是一个单字节指令!这节省了内存和时间,因为读取 8086 中的内存仍然非常昂贵——当然,更不用说从磁带或软盘或其他东西加载该程序了。

那么xchg ax, ax从哪里来呢?您只需要查看其他xhcg指令的操作码。你会看到0x860x87最后,0x91- 0x97因此nop,它0x90似乎非常适合xchg ax, ax(这又不是xchg“重载”-您必须xchg rb, rmb在两个字节处使用 , )。事实上,我很确定这是当时微架构的一个很好的副作用——如果我没记错的话,很容易将整个范围映射0x90-0x97到“xchg,作用于寄存器axax—— di”(操作数是对称的,这给了你完整的范围,包括nop xchg ax, ax;请注意,顺序是ax, cx, dx, bx, sp, bp, si, di-bx在之后dxax; 请记住,寄存器名称是助记符,而不是有序名称 - 累加器、计数器、数据、基址、堆栈指针、基址指针、源索引、目标索引)。同样的方法也用于其他操作数,例如mov someRegister, immediate集合。在某种程度上,你可以把它想象成操作码实际上不是一个完整的字节——最后几位是“真实”操作数的“参数”。

所有这一切,在 x86 上,nop可能被认为是一个真正的指令,或者不是。如果我没记错的话,最初的微架构确实将它视为一个变体xchg,但它实际上是nop在规范中命名的。由于xchg ax, ax作为指令并没有真正意义,您可以看到 8086 的设计者如何通过利用0x90自然映射到完全“noppy”的东西的事实来节省指令解码中的晶体管和路径。

另一方面,i8051 有一个完全为nop-设计的操作码0x00有点实用。指令设计基本上是使用高半字节进行操作,使用低半字节选择操作数——例如add ais 0x2Y0xX8意思是“寄存器 0 直接”,所以0x28is add a, r0在硅上节省了很多:)

我仍然可以继续,因为 CPU 设计(更不用说编译器设计和语言设计)是一个相当广泛的主题,但我认为我已经展示了许多不同的观点,这些观点很好地融入了设计。

早在 70 年代后期,我们(那时我还是一名年轻的研究生)有一个小型开发系统(8080,如果有记忆的话),它以 1024 字节的代码运行(即单个 UVEPROM)——它只有四个要加载的命令(L )、保存 (S)、打印 (P) 以及其他我不记得的东西。它是用真正的电传打字机和打孔带驱动的。它被严格编码!

NOOP 使用的一个示例是在中断服务程序 (ISR) 中,它以 8 字节为间隔。这个例程最终有 9 个字节长,并以(长)跳转到地址空间稍远的地址结束。这意味着,给定小端字节顺序,高地址字节是 00h,并插入下一个 ISR 的第一个字节,这意味着它(下一个 ISR)以 NOOP 开头,所以“我们”可以适合代码在有限的空间!

所以 NOOP 很有用。另外,我怀疑英特尔以这种方式编码是最简单的——他们可能有一个他们想要实现的指令列表,它从“1”开始,就像所有列表一样(这是 FORTRAN 的日子),所以零NOOP 代码变成了失败。(我从未见过一篇文章认为 NOOP 是计算科学理论的重要组成部分(与 Q 相同的问题:数学家是否有 nul op,与群论的零点不同?)

在某些架构NOP上,用于占用未使用的延迟槽例如,如果分支指令没有清除管道,那么它之后的几条指令无论如何都会被执行:

 JMP     .label
 MOV     R2, 1    ; these instructions start execution before the jump
 MOV     R2, 2    ; takes place so they still get executed

但是,如果您在 之后没有任何有用的说明,该JMP怎么办?在这种情况下,您将不得不使用NOPs.

延迟槽不限于跳转。在某些架构上,CPU 管道中的数据危害不会自动解决。这意味着在修改寄存器的每条指令之后都有一个插槽,其中寄存器的新值尚无法访问。如果下一条指令需要该值,则该插槽应由 a 占用NOP

 ADD     R1, R1
 NOP               ; The new value of R1 is not ready yet
 ADD     R1, R3

此外,一些条件执行指令(If-True-False和类似的)为每个条件使用槽,当特定条件没有与之关联的操作时,它的槽应该被 a 占用NOP

CMP     R0, R1       ; Compare R0 and R1, setting flags
ITF     GT           ; If-True-False on GT flag 
MOV     R3, R2       ; If 'greater than', move R2 to R3
NOP                  ; Else, do nothing