Las otras respuestas sólo consideran un NOP que realmente se ejecuta en algún momento - que se utiliza con bastante frecuencia, pero no es el único uso de NOP.
El NOP que no se ejecuta también es bastante útil cuando se escribe código que puede ser parcheado - básicamente, se rellena la función con unos cuantos NOPs después de el RET
(o una instrucción similar). Cuando tengas que parchear el ejecutable, puedes añadir fácilmente más código a la función partiendo del original RET
y utilizando tantos NOPs como sea necesario (por ejemplo, para saltos largos o incluso código inline) y terminando con otro RET
.
En este caso de uso, nunca se espera que el NOP
se ejecute. El único punto es permitir Parcheando el ejecutable - en un teórico ejecutable no acolchado, tendrías que cambiar realmente el código de la función en sí (a veces puede encajar en los límites originales, pero muy a menudo necesitarás un salto de todos modos) - eso es mucho más complicado, especialmente considerando un ensamblaje escrito manualmente o un compilador optimizador; tienes que respetar los saltos y construcciones similares que podrían haber apuntado a alguna pieza importante de código. En definitiva, bastante complicado.
Por supuesto, esto se usaba mucho más en los viejos tiempos, cuando era útil hacer parches como estos pequeño y en línea . Hoy en día, simplemente se distribuye un binario recompilado y listo. Todavía hay quien utiliza Parcheando NOPs (ejecutando o no, y no siempre literal NOP
s - por ejemplo, Windows utiliza MOV EDI, EDI
para Parcheando en línea - es el tipo en el que se puede actualizar una biblioteca del sistema mientras el sistema está realmente funcionando, sin necesidad de reiniciar).
Así que la última pregunta es, ¿por qué tener una instrucción dedicada a algo que realmente no hace nada?
- Es una instrucción real - importante cuando se depura o se codifica manualmente el ensamblaje. Instrucciones como
MOV AX, AX
harán exactamente lo mismo, pero no señalan la intención con tanta claridad.
- Padding - "código" que está ahí sólo para mejorar el rendimiento general del código que depende de la alineación. Nunca está destinado a ejecutarse. Algunos depuradores simplemente ocultan los NOP de relleno en su desensamblaje.
- Esto da más espacio para la optimización de los compiladores - el patrón que todavía se utiliza es que tienes dos pasos de compilación, el primero es bastante simple y produce un montón de código ensamblador innecesario, mientras que el segundo limpia, reordena las referencias de dirección y elimina las instrucciones extrañas. Esto también se ve a menudo en los lenguajes compilados en JIT - tanto el IL de .NET como el código de bytes de JVM utilizan
NOP
s bastante; el código ensamblador realmente compilado ya no los tiene. Hay que tener en cuenta que esos no son x86- NOP
s, sin embargo.
- Facilita la depuración en línea tanto para la lectura (la memoria pre-cero será todo-
NOP
s, lo que hace que el desensamblaje sea mucho más fácil de leer) y para hot-Parcheando (aunque prefiero de lejos Editar y Continuar en Visual Studio :P).
Para la ejecución de los PON, hay por supuesto algunos puntos más:
- Rendimiento, por supuesto - esto no es por lo que estaba en el 8085, pero incluso el 80486 ya tenía una ejecución de instrucciones en tuberías, 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 se utilizan dos NOP
s, serían dos instrucciones a ejecutar.
EDITAR:
En realidad, la discusión con @DmitryGrigoryev me obligó a pensar en esto un poco más, y creo que es una adición valiosa a esta pregunta / respuesta, así que permítanme añadir algunos bits adicionales:
Primero, el punto, obviamente - por qué habría una instrucción que hace algo como mov ax, ax
? Por ejemplo, veamos el caso del código máquina 8086 (más antiguo incluso que el código máquina 386):
- Hay una instrucción NOP dedicada con opcode
0x90
. Todavía es la época en la que mucha gente escribía en ensamblador, eso sí. Así que incluso si no había un dedicado NOP
instrucción, el NOP
(alias/mnemónico) seguiría siendo útil y se asignaría a eso.
- Instrucciones como
MOV
en realidad se asignan a muchos opcodes 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
siendo el opcode, 0x2A
siendo el argumento "inmediato"). Por lo tanto, se necesitan dos bytes.
- No hay un opcode abreviado para
mov al, al
(ya que eso es una estupidez, básicamente), por lo que tendrá que utilizar el mov al, rmb
(rmb es "registro o memoria") sobrecarga. En realidad, eso lleva tres bytes. (aunque probablemente utilizaría el menos específico mov rb, rmb
en su lugar, que sólo debería tomar dos bytes para mov al, al
- el byte de argumento se utiliza para especificar tanto el registro de origen como el de destino; ahora ya sabe por qué el 8086 sólo tenía 8 registros :D). Compare con NOP
que es una instrucción de un solo byte. Esto ahorra memoria y tiempo, ya que la lectura de la memoria en el 8086 seguía siendo bastante cara - por no hablar de la carga de ese programa desde una cinta o disquete o algo así, por supuesto.
Entonces, ¿dónde está el xchg ax, ax
¿vienen? Sólo hay que mirar los opcodes de los otros xhcg
instrucciones. Verás 0x86
, 0x87
y finalmente, 0x91
- 0x97
. Así que nop
con su 0x90
parece una buena opción para xchg ax, ax
(que, de nuevo, no es una xchg
"sobrecarga" - tendrías que usar xchg rb, rmb
a dos bytes). Y, de hecho, estoy bastante seguro de que esto era un buen efecto secundario de la microarquitectura de la época: si no recuerdo mal, era fácil asignar toda la gama de 0x90-0x97
a "xchg, actuando sobre los registros ax
y ax
- di
" (siendo el operando simétrico, esto le dio el rango completo, incluyendo el nop xchg ax, ax
; tenga en cuenta que el orden es ax, cx, dx, bx, sp, bp, si, di
- bx
es después de dx
no ax
Recuerde que los nombres de los registros son mnemotécnicos, no nombres ordenados: acumulador, contador, datos, base, puntero de pila, puntero de base, índice de origen, índice de destino). El mismo enfoque se utilizó también para otros operandos, por ejemplo el mov someRegister, immediate
conjunto. En cierto modo, se podría pensar en esto como si el opcode no fuera realmente un byte completo: los últimos bits son "un argumento" para el operando "real".
Dicho esto, en x86, nop
puede considerarse una instrucción real, o no. La microarquitectura original sí la trataba como una variante de xchg
si no recuerdo mal, pero en realidad se llamaba nop
en el pliego de condiciones. Y como xchg ax, ax
no tiene realmente sentido como instrucción, se puede ver cómo los diseñadores del 8086 ahorraron en transistores y caminos en la decodificación de instrucciones explotando el hecho de que 0x90
se traslada de forma natural a algo que es totalmente "tonto".
Por otro lado, el i8051 tiene un opcode completamente diseñado para nop
- 0x00
. Algo práctico. El diseño de la instrucción es básicamente usar el nibble alto para la operación y el nibble bajo para seleccionar los operandos - por ejemplo, add a
es 0x2Y
y 0xX8
significa "registro 0 directo", por lo que 0x28
es add a, r0
. Ahorra mucho en silicio :)
Podría seguir, ya que el diseño de la CPU (por no hablar del 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 como está.
6 votos
NOP se utiliza con frecuencia en la depuración y actualización de su programa. Si en una fecha posterior, desea añadir algunas líneas a su programa, puede simplemente sobrescribir el NOP. De lo contrario, tendrá que insertar líneas, y la inserción significa desplazar todo el programa. También las instrucciones defectuosas (incorrectas) pueden ser reemplazadas (simplemente sobrescritas) por NOP siguiendo los mismos argumentos.
0 votos
Ah, sí. Pero usar nop también es aumentar el espacio. Mientras que nuestro objetivo principal es hacer que ocupe poco espacio.
0 votos
* Quiero decir que nuestro objetivo es hacer un problema más pequeño. Entonces, ¿no se convierte en un problema también?
2 votos
Por eso hay que usarlo con prudencia. De lo contrario, todo tu programa será un montón de NOPs.