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 RET
y 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 NOP
que 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, EDI
para 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, AX
hará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
NOP
bastante; el código de ensamblado compilado real ya no tiene esos Sin NOP
embargo, 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, EDI
tiene el mejor rendimiento como NOP de 2 bytes en x86. Si usó dos NOP
s, 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 NOP
instrucción dedicada , la NOP
palabra clave (alias / mnemonic) seguiría siendo útil y se asignaría a eso.
- Las instrucciones como en
MOV
realidad se asignan a muchos códigos de operación diferentes, porque eso ahorra tiempo y espacio, por ejemplo, mov al, 42
es "mover el byte inmediato al al
registro", que se traduce en 0xB02A
( 0xB0
ser el código de operación, 0x2A
ser 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, rmb
sobrecarga (rmb es "registro o memoria"). Eso realmente toma tres bytes. (aunque probablemente usaría el menos específico en su mov rb, rmb
lugar, 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, ax
viene el? Solo tiene que mirar los códigos de operación de las otras xhcg
instrucciones. Verás 0x86
, 0x87
y finalmente, 0x91
- 0x97
. Por nop
lo tanto, 0x90
parece 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 ax
y 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
- bx
es 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, immediate
conjunto. 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, nop
podría considerarse una instrucción real, o no. La microarquitectura original lo trató como una variante de xchg
si recuerdo correctamente, pero en realidad fue nombrado nop
en la especificación. Y dado xchg ax, ax
que 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 0x90
asigna 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 a
es 0x2Y
y 0xX8
significa "registro 0 directo", así 0x28
es 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.