¿Cuántos bytes envía la instrucción push a la pila cuando no especifico el tamaño del operando?

Resuelto asked hace 7 años • 0 respuestas

Puedo insertar 4 bytes en la pila haciendo esto:

push DWORD 123

Pero descubrí que puedo usar pushsin especificar el tamaño del operando:

push 123

En este caso, ¿cuántos bytes pushempuja la instrucción a la pila? ¿La cantidad de bytes enviados depende del tamaño del operando (por lo que en mi ejemplo enviará 1 byte)?

 avatar Jul 16 '17 18:07
Aceptado

¿El número de bytes enviados depende del tamaño del operando?

No depende del valor del número. El término técnico x86 para determinar cuántos bytes pushse envían es "tamaño de operando", pero eso es algo independiente de si el número cabe en un imm8 o no.

Consulte también ¿Cada instrucción PUSH envía un múltiplo de 8 bytes en x64?

(Entonces, en mi ejemplo, enviará 1 byte).

No, el tamaño del inmediato no es el tamaño del operando. Siempre envía 4 bytes en código de 32 bits, o 64 en código de 64 bits, a menos que hagas algo extraño.

Recomendación: siempre escriba push 123o push 0x12345use el pushtamaño predeterminado para el modo en el que se encuentra y deje que el ensamblador elija la codificación. Eso es casi siempre lo que quieres. Si eso es todo lo que querías saber, puedes dejar de leer ahora.


En primer lugar, es útil saber qué tamaños pushson posibles en el código de máquina x86 :

  • En modo de 16 bits, puede presionar 16 o (con prefijo de tamaño de operando en 386 y posteriores) 32 bits.
  • En modo de 32 bits, puede presionar 32 o (con prefijo de tamaño de operando) 16 bits.
  • En modo de 64 bits, puede presionar 64 o (con prefijo de tamaño de operando) 16 bits.
    Un prefijo REX.W=0 no le permite codificar una inserción de 32 bits. 1

No hay otras opciones. El puntero de la pila siempre disminuye según el tamaño del operando del push 2 . (Por lo tanto, es posible "desalinear" la pila presionando 16 bits). poptiene las mismas opciones de tamaño: 16, 32 o 64, excepto que no hay pop de 32 bits en modo de 64 bits.

Esto se aplica ya sea que esté presionando un registro o un inmediato, e independientemente de si el inmediato encaja en un signo extendido imm8o necesita un imm32(o imm16para impulsos de 16 bits). (Un signo de 64 bits push imm32se extiende a 64 bits. No hay push imm64, solo mov reg, imm64)

En el código fuente de NASM, push 123se ensambla al tamaño de operando que coincide con el modo en el que se encuentra. En su caso, creo que está escribiendo código de 32 bits, al igual que push 123una inserción de 32 bits, aunque puede (y lo hace). ) utiliza la push imm8codificación.

Su ensamblador siempre sabe qué tipo de código está ensamblando, ya que tiene que saber cuándo usar o no prefijos de tamaño de operando cuando fuerza el tamaño de operando.

MASM es lo mismo; lo único que podría ser diferente es la sintaxis para forzar un tamaño de operando diferente.

Todo lo que escriba en ensamblador se ensamblará en una de las opciones válidas de código de máquina (porque las personas que escribieron el ensamblador saben qué es codificable y qué no), así que no, no puede enviar un solo byte con una instrucción push. Si quisieras eso, podrías emularlo con dec esp/mov byte [esp], 123


Ejemplos de NASM:

Salida desde nasm -l /dev/stdoutpara volcar un listado al terminal, junto con la línea fuente original.

Ligeramente editado para separar el código de operación y los bytes de prefijo de los operandos. (A diferencia de objdump -drwC -Mintel, el formato de desensamblado de NASM no deja espacios entre bytes en el volcado hexadecimal del código de máquina).

 68 80000000         push 128
 6A 80               push -128                 ;; signed imm8 is -128 to +127
 6A 7B               push byte 123
 6A 7B               push dword 123            ;; still optimized to the imm8 encoding
 68 7B000000         push strict dword 123
 6A 80               push strict byte 0x80     ;; will decode as push -128
 ******************       warning: signed byte value exceeds bounds [-w+number-overflow]

dwordNormalmente es una cuestión de tamaño de operando, mientras que strict dwordasí es como solicita que el ensamblador no lo optimice para una codificación más pequeña.

Todas las instrucciones anteriores son inserciones de 32 bits (o 64 bits en modo de 64 bits, con el mismo código de máquina). Todas las siguientes instrucciones son inserciones de 16 bits, independientemente del modo en que las ensamble. (Si se ensamblan en modo de 16 bits, no tendrán un 0x66prefijo de tamaño de operando)

 66 6A 7B            push word 123
 66 68 8000          push word 128
 66 68 7B00          push strict word 123

Aparentemente, NASM parece tratar las anulaciones bytey dwordcomo si se aplicaran al tamaño del inmediato, pero wordse aplica al tamaño del operando de la instrucción. En realidad, el uso o32 push 12en modo de 64 bits tampoco recibe ninguna advertencia. push eaxSin embargo, sí lo hace: "error: instrucción no admitida en modo de 64 bits".

Note que push imm8está codificado como 6A iben todos los modos. Sin prefijo de tamaño de operando, el tamaño del operando es el tamaño del modo. (por ejemplo, 6A FFdecodifica en modo largo como una inserción de tamaño de operando de 64 bits con un operando de -1, disminuyendo RSP en 8 y realizando un almacenamiento de 8 bytes).


El prefijo de tamaño de dirección sólo afecta el modo de direccionamiento explícito utilizado para la inserción con una fuente de memoria, por ejemplo, en modo de 64 bits: push qword [rsi](sin prefijos) frente a push qword [esi](prefijo de tamaño de dirección para modo de direccionamiento de 32 bits). push dword [rsi]no es codificable, porque nada puede hacer que el tamaño del operando sea de 32 bits en un código de 64 bits 1 . push qword [esi]no se trunca rspa 32 bits. Aparentemente, el "Ancho de la dirección de pila" es algo diferente, probablemente establecido en un descriptor de segmento. (Siempre es 64 en código de 64 bits en un sistema operativo normal, creo que incluso para la ABI x32 de Linux : ILP32 en modo largo).


¿Cuándo querrías impulsar los 16 bits? Si escribe en ASM por motivos de rendimiento, probablemente nunca . En mi código golf adler32 , un push estrecho -> pop ancho tomó menos bytes de código que shift/OR para combinar dos enteros de 16b en un valor de 32b.

O tal vez en un exploit para código de 64 bits, es posible que desee insertar algunos datos en la pila sin espacios. No puedes usar simplemente push imm32, porque ese signo o cero se extiende a 64 bits. Podrías hacerlo en fragmentos de 16 bits con múltiples instrucciones push de 16 bits. Pero probablemente aún sea más eficiente mov rax, imm64/ push rax(10B+1B = 11B para una carga útil de 8B imm). O push 0xDEADBEEF/ mov dword [rsp+4], 0xDEADC0DE(5B + 8B = 13B y no necesita registro). cuatro inyecciones de 16 bits requerirían 16B.


Notas a pie de página :

  1. De hecho, REX.W=0 se ignora y no modifica el tamaño del operando fuera de su valor predeterminado de 64 bits. NASM, YASM y GAS se ensamblan push r12en 41 54, no en 49 54. GNU objdjumppiensa 49 54que es inusual y lo decodifica como 49 54 rex.WB push r12. (Ambos ejecutan lo mismo). Microsoft también está de acuerdo y utiliza un 40hREX como rellenopush rbx en algunas DLL de Windows.

    Intel simplemente dice que las inserciones de 32 bits "no son codificables" (NE en la tabla) en modo largo. No entiendo por qué W=1 no es la codificación estándar para push/ popcuando se necesita un prefijo REX, pero aparentemente la elección es arbitraria.

    Dato curioso: solo las instrucciones de pila y algunas otras tienen por defecto un tamaño de operando de 64 bits en modo de 64 bits . En código máquina, add rax, rdxnecesita un prefijo REX (con el bit W configurado). De lo contrario, se decodificaría como add eax, edx. Pero no puede disminuir el tamaño del operando con a REX.W=0cuando el valor predeterminado es 64 bits, solo aumentarlo cuando el valor predeterminado es 32.

    http://wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix enumera las instrucciones predeterminadas en 64 bits en modo de 64 bits. Tenga en cuenta que jrcxzno pertenece estrictamente a esa lista, porque el registro que verifica (cx/ecx/rcx) está determinado por el tamaño de la dirección, no por el tamaño del operando, por lo que se puede anular a 32 bits (pero no a 16 bits). ) en modo de 64 bits. loopes el mismo.

    Es extraño que la entrada del manual de referencia de instrucciones de Intel parapush (extracto HTML: http://felixcloutier.com/x86/PUSH.html ) muestre lo que sucedería con una inserción de tamaño de operando de 32 bits en modo de 64 bits (el único caso en el que El ancho de la dirección de la pila puede ser 64, por lo que usa rsp). Quizás se pueda lograr de alguna manera con algunas configuraciones no estándar en el descriptor del segmento de código, por lo que no puede hacerlo en un código normal de 64 bits que se ejecuta en un sistema operativo normal. O más probablemente sea un descuido, y eso es lo que pasaría si fuera codificable, pero no lo es.

  2. Excepto que los registros de segmento son de 16 bits, pero lo normal push fsseguirá disminuyendo el puntero de la pila en el ancho de la pila (tamaño del operando). Intel documenta que las CPU Intel recientes solo almacenan 16b en ese caso, dejando el resto de los 32 o 64b sin modificar.

    x86 no tiene oficialmente un ancho de pila impuesto en el hardware. Es un término de convención de llamadas/software, por ejemplo, chary shortlos argumentos pasados ​​en la pila en cualquier convención de llamadas se completan a 4B u 8B, por lo que la pila permanece alineada. (Las convenciones de llamadas modernas de 32 y 64 bits, como el x86-32 System V psABI utilizado por Linux, mantienen la pila 16B alineada antes de las llamadas a funciones, aunque una "ranura" de argumento en la pila sigue siendo solo 4B). De todos modos, el "ancho de pila" es sólo una convención de programación en cualquier arquitectura.

    Lo más parecido en x86 ISA a un "ancho de pila" es el tamaño de operando predeterminado de push/ pop. Pero puedes manipular el puntero de la pila como quieras, por ejemplo sub esp,1. Puedes, pero no lo hagas por motivos de rendimiento :P

Peter Cordes avatar Jul 16 '2017 22:07 Peter Cordes

El "ancho de pila" en una computadora, que es la cantidad más pequeña de datos que se puede insertar en la pila, se define como el tamaño de registro del procesador. Esto significa que si se trata de un procesador con registros de 16 bits, el ancho de la pila será de 2 bytes. Si el procesador tiene registros de 32 bits, el ancho de la pila es de 4 bytes. Si el procesador tiene registros de 64 bits, el ancho de la pila es de 8 bytes.

No se confunda al utilizar sistemas x86/x86_64 modernos; Si el sistema se ejecuta en modo de 32 bits, el ancho de la pila y el tamaño del registro son 32 bits o 4 bytes. Si cambia al modo de 64 bits, entonces y sólo entonces cambiarán el registro y el tamaño de la pila.

David Hoelzer avatar Jul 16 '2017 12:07 David Hoelzer