assembler - ¿Qué significa alinear la pila?
use assembler in c (6)
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 compilador 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 compilador 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.
He sido un codificador de alto nivel, y las arquitecturas 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! programa
#include <stdio.h>
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 pila 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 pila con el próximo límite de 16 bytes y por qué es necesario? ¿Y cómo está el andl
logrando esto?
Debería estar solo en direcciones pares, no en direcciones impares, porque hay un déficit de rendimiento al acceder a ellas.
Esto no parece ser específico de la pila, 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.
size in bytes, address in the form of 1, xxxxxxx 2, xxxxxx0 4, xxxxx00 8, xxxx000 16,xxx0000 32,xx00000 64,x000000 and so on
En el caso de un compilador que mezcla datos con instrucciones en el segmento .text, es bastante sencillo alinear los datos según sea necesario (bueno, depende de la arquitectura). Pero la pila es una cosa de tiempo de ejecución, el compilador normalmente no puede determinar dónde estará la pila 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 pila de forma programática.
Digamos, por ejemplo, que tiene dos elementos de 8 bytes en la pila, 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 pila 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 pila 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 compilador 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 programa 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 pila 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 arquitecturas 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 arquitectura. Tal vez eso es lo que estaba haciendo este código.
Esto tiene que ver con la en.wikipedia.org/wiki/Data_structure_alignment . Ciertas arquitecturas 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"
addresses xxx0123456789abcdef01234567 ... [------][------][------] ... registers
Valores en direcciones múltiples de 8 "diapositivas" fácilmente en registros (de 64 bits)
addresses 56789abc ... [------][------][------] ... registers
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.
Supongamos que la pila se ve así en la entrada a _main
(la dirección del puntero de la pila 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 objetivo 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 compilador 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 pila de esta manera, el compilador sabe que agregar cualquier múltiplo de 16 bytes al puntero de pila dará como resultado una dirección alineada de 16 bytes, que es segura para usar con estas instrucciones SIMD.