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


19

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


5
NOP常用于调试和更新程序。如果以后希望在程序中添加一些行,则可以简单地覆盖NOP。否则,您将不得不插入行,并且插入意味着转移整个程序。同样,错误的指令(错误的)也可以由NOP替换(仅覆盖)相同的参数。
P走私者

哦 但是使用nop也会增加空间。而我们的主要目标是使其占据很少的位置。
Demietra95

*我的意思是我们的目标是使问题更小。难道这也不成问题吗?
Demietra95

2
这就是为什么应该明智地使用它。否则,您的整个程序将只是一堆NOP。
P走私者

Answers:


34

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

ŤdË一种ÿ=ñŤCØCķķ

其中K是处理NOP指令所需的周期数(通常为1),而是时钟周期。ŤCØCķ

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

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

另一个用途是将代码对准内存中的某些地址和其他“汇编技巧”,如Programmers.SE上的该线程和StackOverflow上的该另一个线程所解释的。

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

指向Google图书页面的链接特别是指8085 CPU。摘抄:

每个NOP指令都使用四个时钟来进行获取,解码和执行。

编辑 (以解决评论中表达的问题)

如果您担心速度,请记住(时间)效率只是要考虑的一个参数。这完全取决于应用程序:如果您想计算的十亿分之一的数字,那么也许您唯一关心的就是速度。另一方面,如果要记录通过ADC连接到MCU的温度传感器的数据,速度通常并不那么重要,但是等待适当的时间以使ADC正确完成每个读数至关重要。在这种情况下,如果MCU等待的时间不够长,则有可能获取完全不可靠的数据(我承认,虽然:o 会更快地获取数据)。π


3
许多事情(特别是uC外部的输出驱动IC)受制于时序约束,例如“ D稳定到时钟沿的最小时间是100 us”,或“ IR LED必须以1 MHz闪烁”。因此,经常需要(准确的)延迟。
Wouter van Ooijen 2015年

6
当对串行协议进行位碰撞时,NOP对于正确计时很有用。如果程序计数器损坏(例如,PSU故障,罕见的伽玛射线事件的影响等)并开始在程序计数器中执行代码,则在极少数情况下,它们还可用于填充未使用的代码空间,然后跳转至冷启动矢量。否则,清空部分代码空间。
Techydude

6
在Atari 2600视频计算机系统(运行存储在盒带上的程序的第二个视频游戏控制台)上,处理器将在每条扫描行上准确运行76个周期,并且在开始扫描后,需要执行一些确切的周期数的操作。扫描线。在该处理器上,记录的“ NOP”指令需要两个周期,但是代码通常会利用本来没有用的三周期指令来将延迟延迟到精确的周期数。更快地运行代码将产生完全乱码的显示。
2015年

1
在I / O设备在连续操作之间施加最小时间而不是最大时间的情况下,即使在非实时系统上,也可以使用NOP进行延迟。例如,在许多控制器上,将一个字节移出SPI端口将需要八个CPU周期。除了从内存中获取字节然后将其输出到SPI端口之外,什么也不做的代码可能会运行得有点快,但是添加逻辑以测试SPI端口是否已为每个字节准备就绪,将使其不必要地变慢。添加一两个NOP可能会使代码达到最大可用速度...
supercat 2015年

1
...在没有中断的情况下。如果遇到中断,则NOP会不必要地浪费时间,但是被一两个NOP浪费的时间将少于确定中断是否使它们不必要所需要的时间。
超级猫

8

其他答案仅考虑实际上在某个时刻执行的NOP-这是很普遍的用法,但这不是唯一的NOP用法。

在编写可以打补丁的代码时,非执行NOP也是非常有用的-基本上,您将在(或类似指令)之后用几个NOP填充该函数RET。当您必须修补可执行文件时,可以轻松地从原始函数开始添加更多代码,RET并根据需要使用任意数量的NOP(例如,用于跳远甚至是内联代码),然后再添加另一个RET

在这种用例中,noöne永远不会期望NOP执行。唯一的一点是允许修补可执行文件-在理论上未填充的可执行文件中,您实际上必须更改函数本身的代码(有时它可能符合原始边界,但是无论如何,您经常还是需要跳转)-这要复杂得多,尤其是考虑到手动编写的汇编程序或优化的编译器;您必须尊重跳跃和可能指向某些重要代码的类似构造。总而言之,非常棘手。

当然,在过去,当制作这样小的补丁和在线。今天,您只需要分发重新编译的二进制文件即可。仍然有一些人使用补丁NOP(是否执行,而不总是执行文字NOPs-例如,Windows MOV EDI, EDI用于在线补丁-在这种情况下,您可以在系统实际运行时更新系统库,而无需重新启动)。

所以最后一个问题是,为什么要专门针对实际上没有做任何事情的指令?

  • 这是一条实际的指令-在调试或手动编码汇编时很重要。像这样的说明MOV AX, AX将执行完全相同的操作,但不会如此清晰地表明意图。
  • 填充-仅用于提高依赖于对齐的代码的整体性能的“代码”。它决不是要执行的。一些调试器只是在其反汇编中隐藏了填充NOP。
  • 它为优化编译器提供了更多空间-仍然使用的模式是您有两个编译步骤,第一个步骤相当简单,并且产生了很多不必要的汇编代码,而第二个步骤进行了清理,重新连接了地址引用并删除了无关的指示。在JIT编译语言中也经常看到这种情况-.NET的IL和JVM的字节码都使用NOP很多。实际的汇编代码已经没有了。应该注意的是,这些不是x86- NOP
  • 它使在线调试在读取(预先置零的内存将变得NOP很容易,使分散的读取变得更容易)和热修补(尽管到目前为止我更喜欢在Visual Studio:P中选择Edit and Continue)方面都更加容易。

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

  • 性能,当然-这不是8085的原因,但即使80486也已经具有流水线指令执行,这使得“无所事事”变得有些棘手。
  • 就像看到的一样MOV EDI, EDI,还有其他有效的NOP而不是文字NOPMOV EDI, EDI在x86上具有2字节NOP的性能最佳。如果使用两个NOPs,则将执行两个指令。

编辑:

实际上,与@DmitryGrigoryev的讨论使我不得不多考虑这一点,并且我认为这是对该问题/答案的宝贵补充,因此让我添加一些额外的内容:

首先,很明显-为什么会有一条类似指令的指令mov ax, ax?例如,让我们看一下8086机器代码(比386机器代码还要早)的情况:

  • 有一个专用的NOP指令与opcode 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作为指令实际上并没有意义,因此您可以利用0x90自然映射到完全“噪声” 的事实,来了解8086的设计人员如何在指令解码中节省晶体管和路径。

在另一方面,i8051具有一个完全的设计,在操作码nop- 0x00。有点实用。该指令的设计基本上是使用用于操作的高半字节和低半字节用于选择的操作数-例如,add a0x2Y0xX8手段“寄存器0直接”,所以0x28add a, r0。在硅上节省很多:)

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


实际上,NOP通常是一个别名MOV ax, axADD ax, 0或类似的说明。您为什么要设计一条专门的指令,当有很多指令时什么也不做。
德米特里·格里戈里耶夫

@DmitryGrigoryev这实际上涉及到CPU语言(很好,微体系结构)本身的设计。大多数CPU(和编译器)将趋向于优化MOV ax, ax距离。NOP将始终具有固定数量的执行周期。但是我仍然看不出这与我在答案中写的内容有何关系。
a安2015年

CPU不能真正优化MOV ax, ax距离,因为当他们知道这是一条MOV指令时,它们已经在管道中。
德米特里·格里戈里耶夫

@DmitryGrigoryev这实际上取决于您正在谈论的CPU类型。现代台式机CPU做很多事情,而不仅仅是指令流水线。例如,CPU知道不必使高速缓存行等无效,它知道它不必实际做任何事情(对于HyperThreading甚至对于涉及的多个管道非常重要)。如果它也影响分支预测,我不会感到惊讶(尽管对于NOP和可能会相同MOV ax, ax)。现代的CPU比老式的C编译器要复杂得多:))
Luaan 2015年

1
+1可优化从无到无!我认为我们应该合作并回到这种拼写方式!Z80(以及8080)有7 LD r, r条指令,其中r任何一个寄存器都与您的MOV ax, ax指令相似。之所以是7而不是8是因为其中一条指令被重载变为HALT。因此8080和Z80至少还有7条其他指令与NOP!有趣的是,即使这些指令与位模式在逻辑上不相关,它们都需要4个T状态才能执行,因此没有理由使用这些LD指令!
CJ丹尼斯

5

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

使用NOOP的一个示例是在中断服务程序(ISR)中,该程序以8个字节的间隔隔开。该例程最终以9个字节长结束,并以(长)跳转到略微超出地址空间的地址。这意味着,按照小尾数字节顺序,高地址字节为00h,并放入下一个ISR的第一个字节,这意味着它(下一个ISR)以NOOP开头,所以“我们”可以容纳代码在有限的空间内!

因此,NOOP非常有用。另外,我怀疑intel以这种方式进行编码最容易-他们可能有一个他们想要实现的指令列表,并且像所有列表一样,它以“ 1”开头(这是FORTRAN的日子),所以零NOOP代码变成了失败。(我从未见过有文章认为NOOP是计算科学理论的重要组成部分(与Q:数学家是否有nul op区别于零组理论)是相同的问号。)


并非所有的CPU都将NOP编码为0x00(尽管8080、8080和我最熟悉的CPU Z80都可以)。但是,如果我正在设计处理器,那将是我的想法!方便的是,通常将内存初始化为全0x00,因此将其作为代码执行将不会执行任何操作,直到CPU到达未归零的内存为止。
CJ Dennis

@CJDennis我已经解释了为什么x86处理器不使用0x00nop我的回答。简而言之,它节省了指令解码的费用- xchg ax, ax从指令解码的工作方式自然而然地进行,并且执行了“ noppy”操作,那么为什么不使用它并调用它nop,对吗?:)过去在芯片上节省了很多时间用于指令解码...
2015年

5

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

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

但是,如果您在之后没有合适的说明JMP怎么办?在这种情况下,您必须使用NOP

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

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

另外,一些条件执行指令(If-True-False和类似的条件)对每个条件使用插槽,并且当特定条件没有与之关联的动作时,其插槽应由占用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

+1。当然,那些仅倾向于出现在不关心向后兼容性的体系结构上-如果x86在引入指令流水线时尝试了类似的操作,几乎每个人都会简单地将其称为错误(毕竟,他们只是升级了CPU,应用程序停止工作!)。因此,x86 必须确保应用程序不会注意到何时将类似这样的改进添加到CPU中-直到我们还是要使用多核CPU为止::D
Luaan 2015年

2

使用两字节 NOP的另一个示例:http : //blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

MOV EDI,EDI指令是一个两字节的NOP,它刚好足以插入跳转指令中的空间,因此可以动态更新该功能。目的是将MOV EDI,EDI指令替换为两字节的JMP $ -5指令,以将控制重定向到紧接函数启动之前的五个字节的补丁程序空间。完整跳转指令的五个字节就足够了,它可以将控制权发送到地址空间中其他位置安装的替换功能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.