¿Por qué necesitamos las instrucciones de operación "nop" Ie No en el microprocesador 8085?

19

En la instrucción del microprocesador 8085, hay una operación de control de máquina "nop" (sin operación). Mi pregunta es ¿por qué necesitamos una operación no? Quiero decir que si tenemos que finalizar el programa usaremos HLT o RST 3. O si queremos pasar a la siguiente instrucción, daremos las siguientes instrucciones. ¿Pero por qué no hay operación? Cual es la necesidad?

Demietra95
fuente
55
NOP se utiliza con frecuencia en la depuración y actualización de su programa. Si en una fecha posterior, desea agregar algunas líneas a su programa, simplemente puede sobrescribir el NOP. De lo contrario, tendrá que insertar líneas, e insertar significa cambiar todo el programa. También las instrucciones defectuosas (incorrectas) pueden ser reemplazadas (simplemente sobrescritas) por NOP siguiendo los mismos argumentos.
Contrabandista de plutonio
Ohkay Pero usar nop también aumenta el espacio. Mientras que nuestro objetivo principal es hacer que tenga poco lugar.
Demietra95
* Quiero decir que nuestro objetivo es hacer un problema más pequeño. Entonces, ¿no se convierte en un problema también?
Demietra95
2
Es por eso que debe usarse sabiamente. De lo contrario, todo su programa será solo un montón de NOP.
Contrabandista de plutonio

Respuestas:

34

Un uso de la instrucción NOP (o NOOP, sin operación) en las CPU y MCU es insertar un pequeño retraso predecible en su código. Aunque los NOP no realizan ninguna operación, lleva algún tiempo procesarlos (la CPU tiene que buscar y decodificar el código de operación, por lo que necesita algo de tiempo para hacerlo). Tan poco como 1 ciclo de CPU se "desperdicia" para ejecutar una instrucción NOP (el número exacto se puede inferir de la hoja de datos de CPU / MCU, por lo general), por lo tanto, poner N NOP en secuencia es una manera fácil de insertar un retraso predecible:

tremiluny=norteTCloCkK

donde K es el número de ciclos (con mayor frecuencia 1) necesarios para el procesamiento de una instrucción NOP, y es el período de reloj.TCloCk

¿Por qué harías eso? Puede ser útil forzar a la CPU a esperar un poco para que los dispositivos externos (tal vez más lentos) completen su trabajo e informen los datos a la CPU, es decir, NOP es útil para fines de sincronización.

Vea también la página relacionada de Wikipedia sobre NOP .

Otro uso es alinear el código en ciertas direcciones en la memoria y otros "trucos de ensamblaje", como se explica también en este hilo en Programmers.SE y en este otro hilo en StackOverflow .

Otro interesante artículo sobre el tema .

Este enlace a una página del libro de Google se refiere especialmente a la CPU 8085. Extracto:

Cada instrucción NOP usa cuatro relojes para buscar, decodificar y ejecutar.

EDITAR (para abordar una preocupación expresada en un comentario)

Si le preocupa la velocidad, tenga en cuenta que la eficiencia (del tiempo) es solo un parámetro a tener en cuenta. Todo depende de la aplicación: si desea calcular la cifra 10 mil millones de , entonces quizás su única preocupación podría ser la velocidad. Por otro lado, si desea registrar datos de sensores de temperatura conectados a una MCU a través de un ADC, la velocidad no suele ser tan importante, pero es esencial esperar la cantidad de tiempo adecuada para permitir que el ADC complete correctamente cada lectura . En este caso, si la MCU no espera lo suficiente, corre el riesgo de obtener datos completamente poco confiables (aunque reconozco que obtendría esos datos más rápido : o).π

Lorenzo Donati apoya a Monica
fuente
3
Muchas cosas (especialmente las salidas que conducen circuitos integrados externos a la uC) están sujetas a restricciones de tiempo como 'el tiempo mínimo entre D estable y el borde del reloj es de 100 us' o 'el LED IR debe parpadear a 1 MHz'. Por lo tanto, a menudo se requieren demoras (precisas).
Wouter van Ooijen
66
Los NOP pueden ser útiles para obtener la sincronización correcta cuando se aplica un protocolo serie a bits. También pueden ser útiles para llenar el espacio de código no utilizado seguido de un salto al vector de arranque en frío para situaciones raras si el Contador del programa se corrompe (p. Ej., Falla de PSU, impacto por un raro evento de rayos gamma, etc.) y comienza a ejecutar código en un de lo contrario, parte vacía del espacio de código.
Techydude
66
En el Atari 2600 Video Computer System (la segunda consola de videojuegos que ejecuta programas almacenados en cartuchos), el procesador ejecutaría exactamente 76 ciclos en cada línea de exploración, y muchas operaciones tendrían que realizarse un número exacto de ciclos después del inicio de un línea de escaneo En ese procesador, la instrucción documentada "NOP" toma dos ciclos, pero también es común que el código haga uso de una instrucción de tres ciclos que de otro modo sería inútil para retrasar un número preciso de ciclos. Ejecutar un código más rápido habría producido una pantalla totalmente confusa.
supercat
1
El uso de NOP para retrasos puede tener sentido incluso en sistemas de tiempo no real en los casos en que un dispositivo de E / S impone un tiempo mínimo entre operaciones consecutivas, pero no un máximo. Por ejemplo, en muchos controladores, cambiar un byte a un puerto SPI tomará ocho ciclos de CPU. El código que no hace más que recuperar bytes de la memoria y enviarlos al puerto SPI podría ejecutarse demasiado rápido, pero agregar lógica para probar si el puerto SPI estaba listo para cada byte lo haría innecesariamente lento. Agregar un NOP o dos puede permitir que el código alcance la velocidad máxima disponible ...
supercat
1
... en el caso de que no haya interrupciones. Si se produce una interrupción, los NOP perderían innecesariamente el tiempo, pero el tiempo perdido por uno o dos NOP sería menor que el tiempo requerido para determinar si una interrupción los hizo innecesarios.
supercat
8

Las otras respuestas solo están considerando un NOP que realmente se ejecuta en algún momento, que se usa con bastante frecuencia, pero no es el único uso de NOP.

El NOP no de ejecución también es bastante útil cuando se escriben código que puede ser parcheado - básicamente, podrás rellenar la función con unos PON después de la RET(o instrucción similar). Cuando tiene que parchear el ejecutable, puede agregar fácilmente más código a la función comenzando desde el original RETy utilizando tantos NOP como necesite (por ejemplo, para saltos largos o incluso código en línea) y terminando con otro RET.

En este caso de uso, noöne espera NOPque se ejecute. El único punto es permitir parchear el ejecutable: en un ejecutable teórico no acolchado, tendría que cambiar el código de la función en sí (a veces podría ajustarse a los límites originales, pero a menudo necesitará un salto de todos modos ) - eso es mucho más complicado, especialmente considerando el ensamblaje escrito manualmente o un compilador optimizador; tienes que respetar los saltos y construcciones similares que podrían haber apuntado en alguna pieza importante de código. En general, bastante complicado.

Por supuesto, esto era mucho más utilizado en los viejos tiempos, cuando era útil hacer parches como estos pequeños y en línea . Hoy, simplemente distribuirá un binario recompilado y terminará con él. Todavía hay algunos que usan parches NOP (ejecutando o no, y no siempre literales NOP, por ejemplo, Windows usa MOV EDI, EDIpara parches en línea), ese es el tipo en el que puedes actualizar una biblioteca del sistema mientras el sistema se está ejecutando, sin necesidad de reinicios.

Entonces, la última pregunta es, ¿por qué tener una instrucción dedicada para algo que realmente no hace nada?

  • Es una instrucción real, importante al depurar o codificar manualmente. Las instrucciones como MOV AX, AXharán exactamente lo mismo, pero no indican la intención con tanta claridad.
  • Relleno: "código" que está ahí solo para mejorar el rendimiento general del código que depende de la alineación. Nunca se pretende ejecutar. Algunos depuradores simplemente ocultan los NOP de relleno en su desmontaje.
  • Da más espacio para optimizar los compiladores: el patrón que aún se usa es que tiene dos pasos de compilación, el primero es bastante simple y produce muchos códigos de ensamblaje innecesarios, mientras que el segundo limpia, vuelve a cablear las referencias de dirección y elimina instrucciones extrañas Esto también se ve a menudo en lenguajes compilados JIT: tanto el IL de .NET como el código de bytes de JVM usan NOPbastante; el código de ensamblado compilado real ya no tiene esos Sin NOPembargo, debe tenerse en cuenta que esos no son x86- s.
  • Hace que la depuración en línea sea más fácil tanto para la lectura (la memoria pre-puesta a cero será todo NOP, lo que hace que el desmontaje sea mucho más fácil de leer) como para parchear en caliente (aunque prefiero editar y continuar en Visual Studio: P).

Para ejecutar NOP, por supuesto, hay algunos puntos más:

  • Rendimiento, por supuesto, esta no es la razón por la que estaba en 8085, pero incluso el 80486 ya tenía una ejecución de instrucción canalizada, lo que hace que "no hacer nada" sea un poco más complicado.
  • Como se ve con MOV EDI, EDI, hay otros NOP efectivos además del literal NOP. MOV EDI, EDItiene el mejor rendimiento como NOP de 2 bytes en x86. Si usó dos NOPs, serían dos instrucciones para ejecutar.

EDITAR:

En realidad, la discusión con @DmitryGrigoryev me obligó a pensar un poco más sobre esto, y creo que es una valiosa adición a esta pregunta / respuesta, así que permítanme agregar algunos bits adicionales:

Primero, señale, obviamente, ¿por qué habría una instrucción que haga algo así mov ax, ax? Por ejemplo, veamos el caso del código de máquina 8086 (más antiguo que incluso el código de máquina 386):

  • Hay una instrucción NOP dedicada con opcode 0x90. Todavía es el momento en que muchas personas escribieron en asamblea, eso sí. Entonces, incluso si no hubiera una NOPinstrucción dedicada , la NOPpalabra clave (alias / mnemonic) seguiría siendo útil y se asignaría a eso.
  • Las instrucciones como en MOVrealidad se asignan a muchos códigos de operación diferentes, porque eso ahorra tiempo y espacio, por ejemplo, mov al, 42es "mover el byte inmediato al alregistro", que se traduce en 0xB02A( 0xB0ser el código de operación, 0x2Aser el argumento "inmediato"). Entonces eso toma dos bytes.
  • No hay un código de acceso directo para mov al, al(ya que es algo estúpido que hacer, básicamente), por lo que tendrá que usar la mov al, rmbsobrecarga (rmb es "registro o memoria"). Eso realmente toma tres bytes. (aunque probablemente usaría el menos específico en su mov rb, rmblugar, que solo debería tomar dos bytes mov al, al: el byte de argumento se usa para especificar tanto el registro de origen como el de destino; ahora sabe por qué 8086 solo tenía 8 registros: D). Compare con NOP, que es una instrucción de un solo byte. Esto ahorra memoria y tiempo, ya que leer la memoria en el 8086 todavía era bastante costoso, por no mencionar cargar ese programa desde una cinta o disquete o algo así, por supuesto.

Entonces, ¿de dónde xchg ax, axviene el? Solo tiene que mirar los códigos de operación de las otras xhcginstrucciones. Verás 0x86, 0x87y finalmente, 0x91- 0x97. Por noplo tanto, 0x90parece que es una buena opción xchg ax, ax(que, de nuevo, no es una xchg"sobrecarga", tendría que usar xchg rb, rmb, en dos bytes). Y, de hecho, estoy bastante seguro de que este fue un agradable efecto secundario de la microarquitectura de la época: si recuerdo correctamente, fue fácil mapear todo el rango de 0x90-0x97"xchg, actuando sobre registros axy ax- di" el operando es simétrico, esto le dio el rango completo, incluido el nop xchg ax, ax; tenga en cuenta que el orden es ax, cx, dx, bx, sp, bp, si, di- bxes después dx,ax; recuerde, los nombres de registro son mnemotécnicos, no nombres ordenados: acumulador, contador, datos, base, puntero de pila, puntero base, índice de origen, índice de destino). El mismo enfoque también se utilizó para otros operandos, por ejemplo, el mov someRegister, immediateconjunto. En cierto modo, se podría pensar en esto como si el código de operación en realidad no fuera un byte completo: los últimos bits son "un argumento" para el operando "real".

Todo esto dicho, en x86, noppodría considerarse una instrucción real, o no. La microarquitectura original lo trató como una variante de xchgsi recuerdo correctamente, pero en realidad fue nombrado nopen la especificación. Y dado xchg ax, axque realmente no tiene sentido como una instrucción, puede ver cómo los diseñadores del 8086 ahorraron en transistores y vías en la decodificación de instrucciones explotando el hecho de que se 0x90asigna de forma natural a algo que es completamente "noppy".

Por otro lado, el i8051 tiene un código de operación completamente diseñado para nop- 0x00. Un poco práctico El diseño de la instrucción básicamente utiliza el mordisco alto para la operación y el mordisco bajo para seleccionar los operandos, por ejemplo, add aes 0x2Yy 0xX8significa "registro 0 directo", así 0x28es add a, r0. Ahorra mucho en silicio :)

Todavía podría continuar, ya que el diseño de la CPU (sin mencionar el diseño del compilador y el diseño del lenguaje) es un tema bastante amplio, pero creo que he mostrado muchos puntos de vista diferentes que entraron en el diseño bastante bien tal como están.

Luaan
fuente
En realidad, NOPes por lo general un alias MOV ax, ax, ADD ax, 0o instrucciones similares. ¿Por qué diseñaría una instrucción dedicada que no hace nada cuando hay muchas por ahí?
Dmitry Grigoryev
@DmitryGrigoryev Eso realmente entra en el diseño del lenguaje de la CPU (bueno, microarquitectura) en sí. La mayoría de las CPU (y compiladores) tenderán a optimizar la MOV ax, axdistancia; NOPsiempre tendrá una cantidad fija de ciclos para la ejecución. Pero no veo cómo eso es relevante para lo que he escrito en mi respuesta de todos modos.
Luaan
Las CPU realmente no pueden optimizar un MOV ax, axausente, porque para el momento en que lo saben, MOVla instrucción ya está en proceso.
Dmitry Grigoryev
@DmitryGrigoryev Eso realmente depende del tipo de CPU del que estés hablando. Las CPU de escritorio modernas hacen muchas cosas, no solo la canalización de instrucciones. Por ejemplo, la CPU sabe que no tiene que invalidar las líneas de caché, etc., sabe que en realidad no tiene que hacer nada (muy importante para HyperThreading e incluso para las múltiples tuberías involucradas en general). No me sorprendería si también influyera en la predicción de rama (aunque eso probablemente sería lo mismo para NOPy MOV ax, ax). Las CPU modernas son mucho más complejas que los compiladores de C de la vieja escuela :))
Luaan
1
¡+1 para optimizar a nadie para noöne! ¡Creo que todos deberíamos cooperar y volver a este estilo de ortografía! El Z80 (y, a su vez, el 8080) tiene 7 LD r, rinstrucciones donde rhay un único registro, similar a su MOV ax, axinstrucción. La razón es 7 y no 8 es porque una de las instrucciones está sobrecargada para convertirse HALT. ¡Entonces el 8080 y el Z80 tienen al menos otras 7 instrucciones que hacen lo mismo que NOP! Curiosamente, a pesar de que estas instrucciones no están lógicamente relacionadas por el patrón de bits, todas requieren 4 estados T para ejecutarse, por lo que no hay razón para usarlas LD.
CJ Dennis
5

A finales de los años 70, nosotros (yo era un joven estudiante de investigación entonces) teníamos un pequeño sistema de desarrollo (8080 si la memoria funciona) que se ejecutaba en 1024 bytes de código (es decir, un solo UVEPROM): solo tenía cuatro comandos para cargar (L ), guardar (S), imprimir (P) y algo más que no recuerdo. Fue conducido con un teletipo real y cinta adhesiva. ¡Estaba fuertemente codificado!

Un ejemplo del uso de NOOP fue en una rutina de servicio de interrupción (ISR), que se espaciaron en intervalos de 8 bytes. Esta rutina terminó teniendo 9 bytes de longitud y terminando con un salto (largo) a una dirección un poco más arriba en el espacio de direcciones. Esto significaba, dado el pequeño orden de bytes endian, que el byte de alta dirección era 00h, y se ubicaba en el primer byte del siguiente ISR, lo que significaba que (el próximo ISR) comenzaba con NOOP, solo para que 'pudiéramos' encajar El código en el espacio limitado!

Entonces el NOOP es útil. Además, sospecho que fue más fácil para Intel codificarlo de esa manera: probablemente tenían una lista de instrucciones que querían implementar y comenzó en '1', como todas las listas (este fue el día de FORTRAN), por lo que el cero El código NOOP se convirtió en una falla. (Nunca he visto un artículo que argumenta que un NOOP es una parte esencial de la teoría de la Ciencia de la Computación (la misma P que: ¿los matemáticos tienen una operación nula, a diferencia del cero de la teoría de grupos?)

Philip Oakley
fuente
No todas las CPU tienen NOP codificado a 0x00 (aunque el 8085, el 8080 y la CPU con la que estoy más familiarizado, el Z80, todos lo hacen). Sin embargo, si estuviera diseñando un procesador, ¡ahí es donde lo pondría! Otra cosa útil es que la memoria generalmente se inicializa a todos 0x00, por lo que ejecutar esto como código no hará nada hasta que la CPU llegue a la memoria no puesta a cero.
CJ Dennis
@CJDennis he explicado por qué los CPUs x86 no utilizan 0x00para nopen mi respuesta. En resumen, ahorra en la decodificación de instrucciones: xchg ax, axfluye naturalmente de la forma en que funciona la decodificación de instrucciones y hace algo "noppy", así que ¿por qué no usar eso y llamarlo nop, verdad? :) Solía ​​ahorrar bastante en el silicio para la decodificación de instrucciones ...
Luaan
5

En algunas arquitecturas, NOPse utiliza para ocupar ranuras de retardo no utilizadas . Por ejemplo, si la instrucción de bifurcación no borra la tubería, varias instrucciones después de que se ejecute de todos modos:

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

Pero, ¿qué pasa si no tiene instrucciones útiles para encajar después del JMP? En ese caso tendrás que usar NOPs.

Los espacios de demora no se limitan a los saltos. En algunas arquitecturas, los riesgos de datos en la canalización de la CPU no se resuelven automáticamente. Esto significa que después de cada instrucción que modifica un registro hay una ranura donde el nuevo valor del registro aún no es accesible. Si la siguiente instrucción necesita ese valor, el espacio debe estar ocupado por un NOP:

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

Además, algunas instrucciones de ejecución condicional ( If-True-False y similares) usan espacios para cada condición, y cuando una condición en particular no tiene acciones asociadas, su espacio debe estar ocupado por un 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
Dmitry Grigoryev
fuente
+1. Por supuesto, esos solo tienden a aparecer en arquitecturas que no se preocupan por la compatibilidad con versiones anteriores: si x86 intentara algo así al introducir la canalización de instrucciones, casi todos simplemente lo llamarían mal (después de todo, simplemente actualizaron su CPU y sus las aplicaciones dejaron de funcionar!). Así x86 tiene para asegurarse de que la aplicación no se da cuenta cuando se añaden mejoras como esta para la CPU - hasta que llegamos a las CPU de varios núcleos de todos modos ...: D
Luaan
2

Otro ejemplo de uso para un NOP de dos bytes : http://blogs.msdn.com/b/oldnewthing/archive/2011/09/21/10214405.aspx

La instrucción EDI MOV, EDI es un NOP de dos bytes, que es suficiente espacio para parchear en una instrucción de salto para que la función se pueda actualizar sobre la marcha. La intención es que la instrucción MOV EDI, EDI sea reemplazada por una instrucción JMP $ -5 de dos bytes para redirigir el control a cinco bytes de espacio de parche que viene inmediatamente antes del inicio de la función. Cinco bytes son suficientes para una instrucción de salto completa, que puede enviar el control a la función de reemplazo instalada en otro lugar del espacio de direcciones.

pjc50
fuente