Propósito de la instrucción NOP y declaración de alineación en el ensamblado x86

15

Ha pasado un año más o menos desde la última vez que tomé una clase de ensamblaje. En esa clase, estábamos usando MASM con las bibliotecas Irvine para facilitar la programación.

Después de haber revisado la mayoría de las instrucciones, dijo que la instrucción NOP esencialmente no hacía nada y no preocuparse por usarla. De todos modos, se trataba de medio término y él tiene un código de ejemplo que no se ejecuta correctamente, por lo que nos dijo que agreguemos una instrucción NOP y funcionó bien. Le pregunté por qué después de clase y qué hizo realmente, y él dijo que no lo sabía.

Alguien sabe?

alvonellos
fuente
NOP no hace nada, pero consume ciclos. No creo que su pregunta pueda ser respondida, sin el código que solo podemos adivinar. Bueno, creo que ha de ser una diapositiva de NOP ...
Yannis
11
NOP realmente hace algo. Incrementa el puntero de instrucción.
EricSchaefer

Respuestas:

37

Muchas veces NOPse usa para alinear direcciones de instrucciones. Esto generalmente se encuentra, por ejemplo, al escribir Shellcode para explotar el desbordamiento del búfer o formatear la vulnerabilidad de la cadena .

Supongamos que tiene un salto relativo a 100 bytes hacia adelante y realice algunas modificaciones al código. Lo más probable es que tus modificaciones alteren la dirección del objetivo del salto y, como tal, también deberías cambiar el salto relativo mencionado anteriormente. Aquí, puede agregar NOPs para avanzar la dirección de destino. Si tiene múltiples NOPs entre la dirección de destino y la instrucción de salto, puede eliminar las NOPs para tirar de la dirección de destino hacia atrás.

Esto no sería un problema si está trabajando con un ensamblador que admite etiquetas. Simplemente puede hacer JXX someLabel(donde JXX es un salto condicional) y el ensamblador reemplazará el someLabelcon la dirección de esa etiqueta. Sin embargo, si simplemente modifica el código de máquina ensamblado (los códigos de operación reales) a mano (como a veces sucede al escribir código de shell), también debe cambiar la instrucción de salto manualmente. O lo modifica, o luego mueve la dirección del código de destino usando NOPs.

Otro caso de uso para la NOPinstrucción sería algo llamado trineo NOP . En esencia, la idea es crear una gama lo suficientemente grande de instrucciones que no causen efectos secundarios (comoNOPo incrementando y luego decrementando un registro) pero aumenta el puntero de instrucción. Esto es útil, por ejemplo, cuando se quiere saltar a un determinado código cuya dirección no se conoce. El truco consiste en colocar dicho trineo NOP frente al código objetivo y luego saltar a algún lugar hacia dicho trineo. Lo que sucede es que, con suerte, la ejecución continúa desde la matriz que no tiene efectos secundarios y atraviesa instrucciones por instrucción hasta que alcanza el código deseado. Esta técnica se usa comúnmente en las vulnerabilidades de desbordamiento de búfer mencionadas anteriormente y especialmente para contrarrestar medidas de seguridad como ASLR .

Aún otro uso particular para la NOPinstrucción es cuando uno está modificando el código de algún programa. Por ejemplo, puede reemplazar partes de saltos condicionales con NOPsy, como tal, eludir la condición. Este es un método de uso frecuente al " descifrar " la protección de copia del software. Como mínimo, se trata de eliminar la construcción del código de ensamblaje para la if(genuineCopy) ...línea de código y reemplazar las instrucciones con NOPs y .. ¡Voilà! ¡No se realizan controles ni copias no originales!

Tenga en cuenta que, en esencia, ambos ejemplos de shellcode y cracking hacen lo mismo; modifique el código existente sin actualizar las direcciones relativas de las operaciones que dependen del direccionamiento relativo.

zxcdw
fuente
2
Esta fue una respuesta maravillosa, ¡gracias por tomarse el tiempo para explicar esto! Finalmente entiendo!
alvonellos
Ciertos sistemas en tiempo real (los PLC vienen a mi mente) le permiten "parchear" la nueva lógica en un programa existente mientras se está ejecutando. Estos sistemas dejan NOP antes de cada pequeña pieza de lógica para que pueda sobrescribir el NOP con un salto a la nueva lógica que está insertando. Al final de la nueva lógica, saltará al final de la lógica original que está reemplazando. La nueva lógica también tendrá un NOP en el frente para que pueda reemplazar la nueva lógica también.
Scott Whitlock
10

Se puede usar un nop en un intervalo de retraso cuando no se puede reordenar ninguna otra instrucción para colocarla allí.

lw   v0,4(v1)
jr   v0

En MIPS, esto sería un error porque en el momento en que jr estaba leyendo el registro v0, el registro v0 aún no se ha cargado con el valor de la instrucción anterior.

La forma de solucionar esto sería:

lw   v0,4(v1)
nop
jr   v0
nop

Esto llena los espacios comerciales después de la palabra de carga y las instrucciones de registro de salto con un nop para que la instrucción de palabra de carga se complete antes de que se ejecute el comando de registro de salto.

Lectura adicional: un poco sobre el llenado SPARC de las ranuras de retraso . De ese documento:

¿Qué se puede poner en la ranura de retraso?

  • Algunas instrucciones útiles que se deben ejecutar tanto si se ramifica como si no.
  • Algunas instrucciones que son útiles solo funcionan cuando se ramifica (o cuando no se ramifica), pero no hace ningún daño si se ejecuta en el otro caso.
  • Cuando todo lo demás falla, una instrucción NOP

¿Qué NO DEBE ponerse en la ranura de retraso?

  • Cualquier cosa que establezca el CC del que depende la decisión de la sucursal. La instrucción de ramificación toma la decisión de ramificarse o no de inmediato, pero en realidad no lo hace hasta después de la instrucción de retraso. (Solo se retrasa la sucursal, no la decisión).
  • Otra instrucción de rama. (¡Lo que sucede si haces esto ni siquiera está definido! ¡El resultado es impredecible!)
  • Una instrucción "establecida". Estas son realmente dos instrucciones, no una, y solo la mitad estará en la ranura de retraso. (El ensamblador le advertirá sobre esto).

Tenga en cuenta la tercera opción en qué poner en la ranura de retraso. El error que viste probablemente era alguien que llenaba una de las cosas que no se deben poner en la ranura de retraso. Poner un nop en esa ubicación solucionaría el error.

Nota: después de volver a leer la pregunta, esto fue para x86, que no tiene ranuras de retraso (la bifurcación en cambio solo detiene la canalización). Entonces esa no sería la causa / solución del error. En los sistemas RISC, esa podría haber sido la respuesta.


fuente
44
Tenga en cuenta que la pregunta está etiquetada como x86 y x86 no tiene ranuras de retraso. Nunca lo hará, ya que es un cambio radical.
MSalters
6

Al menos una razón para usar NOP es la alineación. Los procesadores x86 leen los datos de la memoria principal en bloques bastante grandes, y el inicio del bloque para leer siempre está alineado, por lo que si uno tiene un bloque de código, se leerá mucho, este bloque debe estar alineado. Esto dará como resultado una pequeña aceleración.

permeakra
fuente
No es exactamente que el bloque deba alinearse, es que no desea obtener los últimos dos bytes del bloque anterior. Entonces, está bien saltar 0x1002, porque todavía hay 14 bytes de instrucciones en el bloque alineado de 16B que contiene la dirección de destino, pero no está bien saltar 0x099D.
Peter Cordes
3

Un propósito para NOP (en ensamblaje general, no solo x86) es introducir demoras de tiempo. Por ejemplo, desea programar un microcontrolador que debe emitir a algunos LED con un retraso de 1 s. Este retraso puede implementarse con NOP (y sucursales). Por supuesto, podría usar ADD u otra cosa, pero eso haría que el código sea más ilegible; o tal vez necesites todos los registros.

m3th0dman
fuente
1
Por lo general, para marcos de tiempo largos, como 1 segundo, se utilizan temporizadores. Los NOPS se usan para épocas dentro de un orden de magnitud del reloj: nano y micro segundos.
mattnz
Esto solo tiene sentido en un microcontrolador, no en un x86 moderno. La mayoría del código x86 no satura el ancho de la tubería de las CPU fuera de orden superescalares modernas, por lo que agregar un NOP entre cada instrucción en la mayoría del código solo tendría un pequeño impacto (supongo que el número para el código "promedio" podría ser 5 a 20% por duplicar el número de instrucciones, con algunos códigos que no muestran desaceleración, pero algunos bucles ajustados que muestran casi una desaceleración de 2x.) De todos modos, el viejo código x86 crujiente usaba tradicionalmente la loopinstrucción para bucles de retardo , no NOP.
Peter Cordes
3

En general, en el 80x86, no se requieren instrucciones de NOP para la corrección del programa, aunque ocasionalmente en algunas máquinas un NOP colocado estratégicamente puede hacer que el código se ejecute más rápidamente. En el 8086, por ejemplo, el código se buscaría en fragmentos de dos bytes, y el procesador tenía un búfer interno de "captación previa" que podía contener tres de estos fragmentos. Algunas instrucciones se ejecutarían más rápido de lo que se podrían obtener, mientras que otras instrucciones tardarían un tiempo en ejecutarse. Durante las instrucciones lentas, el procesador intentaría llenar el búfer de captación previa, de modo que si las siguientes instrucciones fueran rápidas, podrían ejecutarse rápidamente. Si la instrucción que sigue a la instrucción lenta comienza en un límite de palabra par, se buscarán los siguientes seis bytes de instrucciones; si comienza en un límite de bytes impar, solo se capturarán previamente cinco bytes.

Tales problemas de alineación de memoria pueden afectar la velocidad del programa, pero generalmente no afectarán la corrección. Por otro lado, hay algunos problemas relacionados con la captación previa en aquellos procesadores más antiguos en los que un NOP podría afectar la corrección. Si una instrucción altera un byte de código que ya se ha obtenido previamente, el 8086 (y creo que el 80286 y el 80386) ejecutarán la instrucción obtenida previamente aunque ya no coincida con lo que hay en la memoria. Agregar un NOP o dos entre la instrucción que altera la memoria y el byte de código que se altera puede evitar que el byte de código se recupere hasta que se haya escrito. Tenga en cuenta, por cierto, que muchos esquemas de protección de copia explotan este tipo de comportamiento; tenga en cuenta también, sin embargo, que este comportamiento no está garantizado. Las diferentes variaciones del procesador pueden manejar la captación previa de manera diferente, algunos pueden invalidar bytes capturados previamente si se modifica la memoria de la cual fueron leídos, y las interrupciones generalmente invalidarán el búfer de captación previa; el código se volverá a buscar cuando vuelvan las interrupciones.

Super gato
fuente
3

Hay un caso específico x86 que aún no se describe en otras respuestas: manejo de interrupciones. Para algunos estilos, puede haber secciones de código cuando las interrupciones están deshabilitadas porque el código principal funciona con algunos datos compartidos con los manejadores de interrupciones, pero es razonable permitir interrupciones entre dichas secciones. Si uno ingenuamente escribe


    STI
    CLI

esto no procesará interrupciones pendientes porque, citando a Intel:

Después de establecer el indicador IF, el procesador comienza a responder a interrupciones externas y enmascarables después de que se ejecuta la siguiente instrucción.

así que esto se reescribirá al menos como:


    STI
    NOP
    CLI

En la segunda variante, todas las interrupciones pendientes se procesarán solo entre NOP y CLI. (Por supuesto, puede haber muchas variantes alternativas, como duplicar la instrucción STI. Pero el NOP explícito es más obvio, al menos para mí).

Netch
fuente
-2

NOP significa Sin Operación

Generalmente se usa para insertar o eliminar código de máquina o para retrasar la ejecución de un código en particular.

También utilizado por crackers y depuradores para establecer puntos de interrupción.

Entonces, probablemente haciendo algo como: XCHG BX, BX también dará como resultado lo mismo.

Me parece que hubo pocas operaciones que todavía estaban en proceso y, por lo tanto, causó un error.

Si está familiarizado con VB, puedo darle un ejemplo:

Si crea un sistema de inicio de sesión en vb y carga 3 páginas juntas: Facebook, YouTube y Twitter en 3 pestañas diferentes.

Y use 1 botón de inicio de sesión para todos. Podría dar un error si su conexión a Internet es lenta. Lo que significa que una de las páginas aún no se ha cargado. Así que ponemos Application.DoEvents para superar esto. De la misma manera en el ensamblaje se puede usar NOP.

Inmersión total en anime
fuente