¿Cuántos bytes empuja la instrucción push en la stack cuando no especifico el tamaño del operando?

Puedo empujar 4 bytes en la stack haciendo esto:

push DWORD 123 

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

 push 123 

En este caso, ¿cuántos bytes push instrucción push en la stack? ¿El número de bytes empujados depende del tamaño del operando (por lo que en mi ejemplo empujará 1 byte)?

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

No depende del valor del número. El término técnico x86 para la cantidad de empujes de push bytes es “tamaño de operando”, pero eso es algo separado de si el número cabe en un archivo imm8 o no.

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

(Entonces, en mi ejemplo, empujará 1 byte)?

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

Recomendación: siempre escriba push 123 o push 0x12345 para usar el tamaño de push predeterminado para el modo en el que se encuentra y deje que el ensamblador elija la encoding. Eso es casi siempre lo que quieres. Si eso es todo lo que quería saber, puede dejar de leer ahora.


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

  • En modo de 16 bits, puede presionar 16 o (con prefijo de tamaño de operando en 386 y posterior) 32 bits.
  • En el modo de 32 bits, puede presionar 32 o (con el prefijo del tamaño del operando) 16 bits.
  • En el modo de 64 bits, puede presionar 64 o (con el prefijo del tamaño del 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 stack siempre se decrementa por el tamaño de operando del empuje 2 . (De modo que es posible “desalinear” la stack presionando 16 bits). pop tiene 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 registro inmediato, y sin importar si el inmediato se ajusta en una imm8 signo extendido o si necesita un imm32 (o imm16 para los imm16 de 16 bits). (Un signo de push imm32 64 bits push imm32 : se extiende a 64 bits. No hay push imm64 , solo mov reg, imm64 )

En el código fuente de NASM, push 123 ensambla en el tamaño del operando que coincida con el modo en el que se encuentra. En su caso, creo que está escribiendo código de 32 bits, por lo que push 123 es un impulso de 32 bits, aunque puede (y lo hace) usar la encoding push imm8 .

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

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

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


Ejemplos de NASM:

Salida de nasm -l /dev/stdout para volcar una lista en la terminal, junto con la línea fuente original.

Ligeramente editado para separar el código de operación y prefijar los bytes de los operandos. (A diferencia de objdump -drwC -Mintel , el formato de desassembly de NASM no deja espacios entre bytes en el hexdump de 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] 

dword es normalmente una cosa de tamaño de operando, mientras que strict dword es cómo se solicita que el ensamblador no lo optimice a una encoding más pequeña.

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

  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 de byte y dword como aplicables al tamaño de la inmediata, pero la word aplica al tamaño de operando de la instrucción. En realidad, usar o32 push 12 en modo de 64 bits tampoco recibe una advertencia. push eax does, though: “error: instrucción no admitida en modo de 64 bits”.

Observe que push imm8 está codificado como 6A ib en todos los modos. Sin el prefijo de tamaño de operando, el tamaño del operando es el tamaño del modo. (por ejemplo, 6A FF decodifica en modo largo como un impulso de tamaño de operando de 64 bits con un operando de -1 , decrementando el RSP por 8 y haciendo un almacenamiento de 8 bytes).


El prefijo de tamaño de dirección solo afecta el modo de direccionamiento explícito utilizado para push con una fuente de memoria, por ejemplo, en modo de 64 bits: push qword [rsi] (sin prefijos) vs. push qword [esi] (prefijo de tamaño de dirección para 32 modo de direccionamiento de bit). push dword [rsi] no es codificable, porque nada puede hacer que el tamaño del operando sea de 32 bits en el código 1 de 64 bits. push qword [esi] no trunca rsp a 32 bits. Aparentemente, “Ancho de dirección de stack” 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 x32 ABI de Linux : ILP32 en modo largo).


¿Cuándo querrías empujar 16 bits? Si escribe en asm por motivos de rendimiento, probablemente nunca . En mi adler32 de código de golf , un pop estrecho push -> wide pop 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 el código de 64 bits, es posible que desee insertar algunos datos en la stack sin espacios vacíos. No puedes usar push imm32 porque ese signo o cero se extiende a 64 bits. Podrías hacerlo en fragmentos de 16 bits con varias instrucciones de inserción de 16 bits. Pero aún probablemente sea más eficiente para 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 un registro). cuatro impulsos de 16 bits tomarían 16B.


Notas al pie :

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

    Intel solo dice que los empujes de 32 bits “no son codificables” (NE en la tabla) en modo largo. No entiendo por qué W = 1 no es la encoding estándar para push / pop cuando se necesita un prefijo REX, pero aparentemente la elección es arbitraria.

    Fun-fact: solo las instrucciones de astackmiento y algunas otras opciones predeterminadas para el tamaño de operando de 64 bits en el modo de 64 bits . En el código máquina, add rax, rdx necesita 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 un REX.W=0 cuando por defecto es de 64 bits, solo incremente cuando el valor predeterminado sea 32.

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

    Es extraño que la entrada del manual de referencia de instrucciones de Intel para push (Extracto de HTML: http://felixcloutier.com/x86/PUSH.html ) muestra lo que sucedería con un impulso de tamaño de operando de 32 bits en el modo de 64 bits (el único caso) donde el ancho de la dirección de la stack puede ser 64, entonces usa rsp ). Tal vez se puede lograr de alguna manera con algunas configuraciones no estándar en el descriptor de 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 bien es un descuido, y eso es lo que sucedería si fuera codificable, pero no lo es.

  2. Excepto que los registros de segmento son de 16 bits, pero un push fs normal push fs disminuirá aún el puntero de la stack por el ancho de la stack (tamaño del operando). Intel documenta que las CPU Intel recientes solo hacen una tienda 16b en ese caso, dejando el rest de la 32 o 64b sin modificar.

    x86 oficialmente no tiene un ancho de stack que se aplica en el hardware. Es un término de convención de software / llamada, por ejemplo, char y args short pasados ​​en la stack en cualquier convención de llamadas que se rellenan en 4B u 8B, por lo que la stack permanece alineada. (Las convenciones de llamadas modernas de 32 y 64 bits, como el sistema x86-32 V psABI utilizado por Linux mantienen la stack 16B alineada antes de las llamadas de función, incluso aunque una “ranura” arg en la stack siga siendo solo 4B). De todos modos, el “ancho de la stack” es solo una convención de progtwigción en cualquier architecture.

    Lo más parecido en el ISA x86 a un “ancho de stack” es el tamaño de operando predeterminado de push / pop . Pero puede manipular el puntero de la stack como lo desee, por ejemplo, sub esp,1 . Puede hacerlo, pero no por motivos de rendimiento: P

El “ancho de stack” en una computadora, que es la cantidad más pequeña de datos que se puede insertar en la stack, 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 stack será de 2 bytes. Si el procesador tiene registros de 32 bits, el ancho de la stack es de 4 bytes. Si el procesador tiene registros de 64 bits, el ancho de la stack es de 8 bytes.

No se confunda al usar sistemas modernos x86 / x86_64; si el sistema se está ejecutando en un modo de 32 bits, el ancho de la stack y el tamaño del registro son 32 bits o 4 bytes. Si cambia al modo de 64 bits, entonces y solo entonces cambiarán el tamaño del registro y la stack.