¿Qué significa alinear la stack?

He sido un codificador de alto nivel, y las architectures son bastante nuevas para mí, así que decidí leer el tutorial sobre la Asamblea aquí:

http://en.wikibooks.org/wiki/X86_Assembly/Print_Version

¡Lejos del tutorial, instrucciones sobre cómo convertir Hello World! progtwig

#include  int main(void) { printf("Hello, world!\n"); return 0; } 

en un código de ensamblaje equivalente y se generó lo siguiente:

  .text LC0: .ascii "Hello, world!\12\0" .globl _main _main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax call __alloca call ___main movl $LC0, (%esp) call _printf movl $0, %eax leave ret 

Para una de las líneas,

 andl $-16, %esp 

la explicación fue:

Este código “y” s ESP con 0xFFFFFFF0, alineando la stack con el siguiente límite más bajo de 16 bytes. Un examen del código fuente de Mingw revela que esto puede ser para las instrucciones SIMD que aparecen en la rutina “_main”, que operan solo en direcciones alineadas. Como nuestra rutina no contiene instrucciones SIMD, esta línea es innecesaria.

No entiendo este punto. ¿Puede alguien darme una explicación de lo que significa alinear la stack con el próximo límite de 16 bytes y por qué es necesario? ¿Y cómo está el andl logrando esto?

Supongamos que la stack se ve así en la entrada a _main (la dirección del puntero de la stack es solo un ejemplo):

 | existing | | stack content | +-----------------+ <--- 0xbfff1230 

Presione %ebp y resta 8 de %esp para reservar espacio para las variables locales:

 | existing | | stack content | +-----------------+ <--- 0xbfff1230 | %ebp | +-----------------+ <--- 0xbfff122c : reserved : : space : +-----------------+ <--- 0xbfff1224 

Ahora, la instrucción andl pone a cero los 4 bits bajos de %esp , lo que puede disminuirlo; en este ejemplo particular, tiene el efecto de reservar 4 bytes adicionales:

 | existing | | stack content | +-----------------+ <--- 0xbfff1230 | %ebp | +-----------------+ <--- 0xbfff122c : reserved : : space : + - - - - - - - - + <--- 0xbfff1224 : extra space : +-----------------+ <--- 0xbfff1220 

El objective de esto es que hay algunas instrucciones "SIMD" (instrucción única, datos múltiples) (también conocidas en x86-land como "SSE" para "Streaming SIMD Extensions") que pueden realizar operaciones paralelas en varias palabras en la memoria, pero requieren que esas palabras múltiples sean un bloque que comienza en una dirección que es un múltiplo de 16 bytes.

En general, el comstackdor no puede asumir que los desplazamientos particulares de %esp darán como resultado una dirección adecuada (porque el estado de %esp en la entrada a la función depende del código de llamada). Pero, al alinear deliberadamente el puntero de stack de esta manera, el comstackdor sabe que agregar cualquier múltiplo de 16 bytes al puntero de stack dará como resultado una dirección alineada de 16 bytes, que es segura para usar con estas instrucciones SIMD.

Esto no parece ser específico de la stack, sino alineación en general. Quizás piense en el término entero múltiple.

Si tiene elementos en la memoria que tienen un tamaño de byte, unidades de 1, entonces solo digamos que están todos alineados. Las cosas que tienen dos bytes de tamaño, luego los enteros multiplicados por 2 se alinearán, 0, 2, 4, 6, 8, etc. Y los múltiplos no enteros, 1, 3, 5, 7 no se alinearán. Los elementos que tienen 4 bytes de tamaño, los múltiplos enteros 0, 4, 8, 12, etc. están alineados, 1,2,3,5,6,7, etc. no lo están. Lo mismo vale para 8, 0,8,16,24 y 16 16,32,48,64, y así sucesivamente.

Lo que esto significa es que puede mirar la dirección base del artículo y determinar si está alineado.

 tamaño en bytes, dirección en forma de 
 1, xxxxxxx
 2, xxxxxx0
 4, xxxxx00
 8, xxxx000
 16, xxx0000
 32, xx00000
 64, x000000
 y así

En el caso de un comstackdor que mezcla datos con instrucciones en el segmento .text, es bastante sencillo alinear los datos según sea necesario (bueno, depende de la architecture). Pero la stack es una cosa de tiempo de ejecución, el comstackdor normalmente no puede determinar dónde estará la stack en tiempo de ejecución. Entonces, en el tiempo de ejecución, si tiene variables locales que necesitan alinearse, necesitará que el código ajuste la stack de forma programática.

Digamos, por ejemplo, que tiene dos elementos de 8 bytes en la stack, 16 bytes en total, y realmente quiere alinearlos (en límites de 8 bytes). Al ingresar, la función restaría 16 del puntero de la stack como de costumbre para dejar espacio para estos dos elementos. Pero para alinearlos, debería haber más código. Si queríamos estos dos elementos de 8 bytes alineados en los límites de 8 bytes y el puntero de la stack después de restar 16 era 0xFF82, los 3 bits inferiores no son 0, por lo que no están alineados. Los tres bits más bajos son 0b010. En un sentido genérico, queremos restar 2 del 0xFF82 para obtener 0xFF80. La forma en que determinamos que es un 2 sería haciendo anding con 0b111 (0x7) y restando esa cantidad. Eso significa operaciones alu y un restar. Pero podemos tomar un atajo si nosotros y con el valor de complemento de 0x7 (~ 0x7 = 0xFFFF … FFF8) obtenemos 0xFF80 usando una operación alu (siempre que el comstackdor y el procesador tengan una sola forma de código de operación para hacer eso, si no puede costarle más que el yy restar).

Esto parece ser lo que tu progtwig estaba haciendo. Anding con -16 es lo mismo que anding con 0xFFFF …. FFF0, lo que resulta en una dirección alineada en un límite de 16 bytes.

Entonces, para resumir esto, si tiene algo así como un puntero de stack típico que funciona desde la dirección más alta a la inferior, entonces desea

 
 sp = sp & (~ (n-1))

donde n es el número de bytes que se alinean (deben ser potencias, pero está bien que la alineación por lo general involucre potencias de dos). Si, por ejemplo, has hecho un malloc (las direcciones aumentan de menor a mayor) y quieres alinear la dirección de algo (recuerda malloc más de lo que necesitas por al menos el tamaño de alineación), entonces

 if (ptr & (~ (n-)) {ptr = (ptr + n) & (~ (n-1));}

O si lo desea, simplemente tome el si está allí y realice el agregado y la máscara cada vez.

muchas / la mayoría de las architectures que no son x86 tienen reglas y requisitos de alineación. x86 es demasiado flexible en lo que respecta al conjunto de instrucciones, pero en lo que respecta a la ejecución, puede / pagará una penalización por accesos no alineados en un x86, por lo que aunque pueda hacerlo, debe esforzarse por mantenerse alineado como lo haría con cualquier otra architecture. Tal vez eso es lo que estaba haciendo este código.

Esto tiene que ver con la alineación de bytes . Ciertas architectures requieren que las direcciones utilizadas para un conjunto específico de operaciones se alineen con límites de bits específicos.

Es decir, si quisiera una alineación de 64 bits para un puntero, por ejemplo, podría dividir conceptualmente toda la memoria direccionable en fragmentos de 64 bits empezando por cero. Una dirección se “alinearía” si encaja exactamente en uno de estos fragmentos, y no se alinearía si formara parte de un fragmento y parte de otro.

Una característica importante de la alineación de bytes (suponiendo que el número sea una potencia de 2) es que los bits X menos significativos de la dirección son siempre cero. Esto permite que el procesador represente más direcciones con menos bits simplemente sin usar los bits X inferiores.

Imagina este “dibujo”

 direcciones
  xxx0123456789abcdef01234567 ...
     [------] [------] [------] ...
 registros

Valores en direcciones múltiples de 8 “diapositivas” fácilmente en registros (de 64 bits)

 direcciones
          56789abc ...
     [------] [------] [------] ...
 registros

Por supuesto, registra “caminar” en pasos de 8 bytes

Ahora, si quiere poner el valor en la dirección xxx5 en un registro, es mucho más difícil 🙂


Editar andl -16

-16 es 11111111111111111111111111110000 en binario

cuando “y” cualquier cosa con -16 obtienes un valor con los últimos 4 bits configurados en 0 … o un múltiplo de 16.

Debería estar solo en direcciones pares, no en direcciones impares, porque hay un déficit de rendimiento al acceder a ellas.

Cuando el procesador carga datos de la memoria en un registro, necesita acceder por una dirección base y un tamaño. Por ejemplo, obtendrá 4 bytes de la dirección 10100100. Observe que hay dos ceros al final de ese ejemplo. Esto se debe a que los cuatro bytes están almacenados de manera que los 101001 bits principales son significativos. (El procesador realmente accede a ellos a través de un “no me importa” obteniendo 101001XX).

Entonces, alinear algo en la memoria significa reorganizar los datos (generalmente a través del relleno) para que la dirección del elemento deseado tenga suficientes bytes cero. Continuando con el ejemplo anterior, no podemos obtener 4 bytes de 10100101 ya que los últimos dos bits no son cero; eso causaría un error de bus. Por lo tanto, debemos ubicar la dirección hasta 10101000 (y perder tres ubicaciones de direcciones en el proceso).

El comstackdor hace esto automáticamente y se representa en el código de ensamblaje.

Tenga en cuenta que esto se manifiesta como una optimización en C / C ++:

 struct first { char letter1; int number; char letter2; }; struct second { int number; char letter1; char letter2; }; int main () { cout << "Size of first: " << sizeof(first) << endl; cout << "Size of second: " << sizeof(second) << endl; return 0; } 

El resultado es

 Size of first: 12 Size of second: 8 

Reordenar las dos char significa que la int se alineará correctamente, y por lo tanto el comstackdor no tiene que desplazar la dirección base a través del relleno. Es por eso que el tamaño del segundo es más pequeño.