2011-03-16 6 views
21

me corrieron -S gcc sobre esto:¿Cuál es el significado de cada línea de la salida de ensamblaje de un C hello world?

int main() 
{ 
printf ("Hello world!"); 
} 

y me dieron el código de montaje:

 .file "test.c" 
     .section  .rodata 
.LC0: 
     .string "Hello world!" 
     .text 
.globl main 
     .type main, @function 
main: 
     leal 4(%esp), %ecx 
     andl $-16, %esp 
     pushl -4(%ecx) 
     pushl %ebp 
     movl %esp, %ebp 
     pushl %ecx 
     subl $20, %esp 
     movl $.LC0, (%esp) 
     call printf 
     addl $20, %esp 
     popl %ecx 
     popl %ebp 
     leal -4(%ecx), %esp 
     ret 
     .size main, .-main 
     .ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)" 
     .section  .note.GNU-stack,"",@progbits 

tengo curiosidad para entender esta salida. ¿Alguien puede compartir algunos consejos para entender este resultado, o si alguien puede marcar comentarios en contra de cada una de estas líneas/grupo de líneas explicando qué hace, sería genial?

+0

Es cierto, ¿alguna sugerencia? .. específico de Linux? o tal vez buenos blogs, sitios web? (Sí, puedo buscar esto en Google, pero a veces la gente está mejor informada de lo que Google le arroja ...) – Mohammed

+1

No, yo no sería una de esas personas. Personalmente, sé * extremadamente * poco sobre el ensamblador. Quizás [esta pregunta] (http://stackoverflow.com/questions/199679/good-beginners-books-for-assembly-languages) sería útil? –

+0

Votación para cerrar como demasiado amplia. –

Respuesta

46

Aquí cómo va:

 .file "test.c" 

El nombre del archivo fuente original (usado por los depuradores).

 .section  .rodata 
.LC0: 
     .string "Hello world!" 

Una cadena terminada en cero se incluye en la sección ".rodata" ('ro' significa 'sólo lectura': la aplicación será capaz de leer los datos, pero cualquier intento de escribir en él lo harán desencadenar una excepción).

 .text 

Ahora escribimos cosas en la sección ".text", que es donde va el código.

.globl main 
     .type main, @function 
main: 

Definimos una función llamada "main" y globalmente visible (otros archivos de objetos podrán invocarla).

 leal 4(%esp), %ecx 

que almacenamos en el registro %ecx el valor 4+%esp (%esp es el puntero de pila).

 andl $-16, %esp 

%esp se modifica ligeramente para que sea un múltiplo de 16. Para algunos tipos de datos (el formato de coma flotante correspondiente a C de double y long double), el rendimiento es mejor cuando los accesos de memoria están en direcciones que son múltiplo de 16. Esto no es realmente necesario aquí, pero cuando se utiliza sin el indicador de optimización (-O2 ...), el compilador tiende a producir bastante código genérico inútil (es decir, código que podría ser útil en algunos casos pero no aquí))

 pushl -4(%ecx) 

Ésta es un poco raro: en ese momento, la palabra en la dirección -4(%ecx) es la palabra que estaba en la parte superior de la pila antes de la andl. El código recupera esa palabra (que debería ser la dirección de retorno, por cierto) y la empuja nuevamente.Este tipo de emula lo que se obtendría con una llamada de una función que tenía una pila alineada de 16 bytes. Supongo que este push es un remanente de una secuencia de copia de argumentos. Como la función ha ajustado el puntero de la pila, debe copiar los argumentos de la función, a los que se puede acceder a través del valor anterior del puntero de la pila. Aquí, no hay argumento, excepto la dirección de retorno de la función. Tenga en cuenta que esta palabra no se usará (una vez más, este es código sin optimización).

 pushl %ebp 
     movl %esp, %ebp 

Este es el prólogo de función estándar: que guardar %ebp (ya que estamos a punto de modificarlo), a continuación, establecer %ebp para que apunte al marco de pila. A partir de entonces, se usará %ebp para acceder a los argumentos de la función, haciendo que %esp vuelva a estar libre. (Sí, no hay ningún argumento, por lo que este es inútil para esa función.)

 pushl %ecx 

Ahorramos %ecx (que lo necesitará en la salida de función, para restaurar %esp en el valor que tenía antes de la andl).

 subl $20, %esp 

Nos reservamos 32 bytes en la pila (recordemos que la pila crece "hacia abajo"). Ese espacio se usará para guardar los argumentos en printf() (eso es exagerado, ya que hay un solo argumento, que usará 4 bytes [eso es un puntero]).

 movl $.LC0, (%esp) 
     call printf 

Nosotros "empujar" el argumento para printf() (es decir, nos aseguramos de que %esp puntos a una palabra que contiene el argumento, aquí $.LC0, que es la dirección de la cadena constante en la sección Rodata). Luego llamamos al printf().

 addl $20, %esp 

Cuando printf() rendimientos, se elimina el espacio asignado para los argumentos. Este addl cancela lo que hizo el subl anterior.

 popl %ecx 

Recuperamos %ecx (presionada arriba); printf() puede haberlo modificado (las convenciones de llamadas describen qué registro puede modificar una función sin restaurarlas al salir; %ecx es uno de esos registros).

 popl %ebp 

Función epílogo: esto restaura %ebp (correspondiente a la pushl %ebp arriba).

 leal -4(%ecx), %esp 

Restauramos %esp a su valor inicial. El efecto de este código de operación es almacenar en %esp el valor %ecx-4. %ecx se configuró en el primer código de operación de función. Esto cancela cualquier alteración al %esp, incluido el andl.

 ret 

Función de salida.

 .size main, .-main 

Ajusta el tamaño de la función main(): en cualquier punto durante el montaje, "." es un alias de "la dirección en la que estamos añadiendo cosas en este momento".Si se agregó otra instrucción aquí, iría a la dirección especificada por ".". Por lo tanto, ".-main", aquí está el tamaño exacto del código de la función main(). La directiva .size indica al ensamblador que escriba esa información en el archivo de objeto.

 .ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)" 

GCC just loves to leave traces of its action. Esta cadena termina como un tipo de comentario en el archivo de objeto. El enlazador lo eliminará.

 .section  .note.GNU-stack,"",@progbits 

Una sección especial donde GCC escribe que el código puede acomodar una pila no ejecutable. Este es el caso normal. Las pilas ejecutables son necesarias para algunos usos especiales (no estándar C). En los procesadores modernos, el kernel puede hacer una pila no ejecutable (una pila que desencadena una excepción si alguien intenta ejecutar como código algunos datos que están en la pila); Algunas personas lo consideran una "característica de seguridad" porque poner código en la pila es una forma común de explotar los desbordamientos del búfer. Con esta sección, el ejecutable se marcará como "compatible con una pila no ejecutable" que el kernel proporcionará gustoso como tal.

+2

Buena descripción de hecho. :-) – sjsam

2
leal 4(%esp), %ecx 
    andl $-16, %esp 
    pushl -4(%ecx) 
    pushl %ebp 
    movl %esp, %ebp 
    pushl %ecx 
    subl $20, %esp 

estas instrucciones no se pueden comparar en su programa de C, siempre están ejecutados al comienzo de cada función (pero depende de compilador/plataforma)

movl $.LC0, (%esp) 
    call printf 

este bloque corresponde a su llamada printf(). la primera instrucción coloca en la pila su argumento (un puntero a "hello world") y luego llama a la función.

addl $20, %esp 
    popl %ecx 
    popl %ebp 
    leal -4(%ecx), %esp 
    ret 

estas instrucciones son opuestas al primer bloque, que son una especie de animal de manipulación de pila. Siempre ejecutado demasiado

5

Aquí hay un suplemento a la respuesta de @Thomas Pornin.

  • .LC0 local constant, e.g string literal.
  • .LFB0 principio de función local,
  • .LFE0 final función local,

El sufijo de estas etiquetas es un número, y empezar desde 0.

Esta es la convención ensamblador gcc.