Aquí cómo va:
.file "test.c"
El nombre del archivo fuente original (utilizado por los depuradores).
.section .rodata
.LC0:
.string "Hello world!"
Se incluye una cadena terminada en cero en la sección ".rodata" ("ro" significa "solo lectura":la aplicación podrá leer los datos, pero cualquier intento de escribirlos 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 "principal" y visible globalmente (otros archivos objeto podrán invocarla).
leal 4(%esp), %ecx
Almacenamos en el registro %ecx
el valor 4+%esp
(%esp
es el puntero de la pila).
andl $-16, %esp
%esp
se modifica ligeramente para que se convierta en un múltiplo de 16. Para algunos tipos de datos (el formato de punto flotante correspondiente al double
de C y long double
), el rendimiento es mejor cuando los accesos a la memoria están en direcciones que son múltiplos de 16. Esto no es realmente necesario aquí, pero cuando se usa sin el indicador de optimización (-O2
...), el compilador tiende a producir una gran cantidad de código inútil genérico (es decir, código que podría ser útil en algunos casos pero no aquí).
pushl -4(%ecx)
Este es un poco extraño:en ese punto, la palabra en la dirección -4(%ecx)
es la palabra que estaba en la parte superior de la pila antes del andl
. El código recupera esa palabra (que, por cierto, debería ser la dirección de retorno) y la empuja de nuevo. Este tipo de emula lo que se obtendría con una llamada de una función que tuviera una pila alineada de 16 bytes. Supongo que este push
es un remanente de una secuencia de copia de argumentos. Dado que la función ha ajustado el puntero de la pila, debe copiar los argumentos de la función, a los que se podía acceder a través del valor anterior del puntero de la pila. Aquí, no hay ningún argumento, excepto la dirección de retorno de la función. Tenga en cuenta que esta palabra no se utilizará (una vez más, este es un código sin optimización).
pushl %ebp
movl %esp, %ebp
Este es el prólogo de la función estándar:guardamos %ebp
(ya que estamos a punto de modificarlo), luego configure %ebp
para apuntar al marco de la pila. A partir de entonces, %ebp
se usará para acceder a los argumentos de la función, haciendo %esp
libre de nuevo. (Sí, no hay argumento, así que esto es inútil para esa función).
pushl %ecx
Guardamos %ecx
(lo necesitaremos al salir de la función, para restaurar %esp
al valor que tenía antes del andl
).
subl $20, %esp
Reservamos 32 bytes en la pila (recuerda que la pila crece "hacia abajo"). Ese espacio se usará para almacenar los argumentos de printf()
(eso es exagerado, ya que hay un solo argumento, que usará 4 bytes [eso es un puntero]).
movl $.LC0, (%esp)
call printf
"empujamos" el argumento a printf()
(es decir, nos aseguramos de que %esp
apunta 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 printf()
.
addl $20, %esp
Cuando printf()
regresa, eliminamos el espacio asignado para los argumentos. Este addl
cancela lo que subl
arriba lo hizo.
popl %ecx
Recuperamos %ecx
(empujado arriba); printf()
puede haberlo modificado (las convenciones de llamada describen qué registro puede modificar una función sin restaurarlos al salir; %ecx
es uno de esos registros).
popl %ebp
Epílogo de la función:esto restaura %ebp
(correspondiente al 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 estableció en el código de operación de la primera función. Esto cancela cualquier alteración a %esp
, incluido el andl
.
ret
Salir de la función.
.size main, .-main
Esto establece el tamaño del main()
función:en cualquier momento durante el ensamblaje, ".
" es un alias para "la dirección en la que estamos agregando cosas en este momento". Si se agregara 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()
. El .size
La directiva le indica al ensamblador que escriba esa información en el archivo del objeto.
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
A GCC le encanta dejar huellas de su acción. Esta cadena termina como una especie 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. Se necesitan pilas ejecutables para algunos usos especiales (no C estándar). En los procesadores modernos, el núcleo puede crear una pila no ejecutable (una pila que activa una excepción si alguien intenta ejecutar como código algunos datos que están en la pila); Algunas personas ven esto como una "característica de seguridad" porque poner código en la pila es una forma común de explotar los desbordamientos de búfer. Con esta sección, el ejecutable se marcará como "compatible con una pila no ejecutable" que el kernel felizmente proporcionará como tal.
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
estas instrucciones no se comparan en su programa c, siempre se ejecutan al comienzo de cada función (pero depende del 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 "hola mundo") 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, son una especie de material de manipulación de pila. siempre ejecutado también
Aquí hay un complemento para @Thomas Pornin
la respuesta de.
.LC0
constante local, por ejemplo, cadena literal..LFB0
comienzo de la función local,.LFE0
terminación de función local,
El sufijo de esta etiqueta es un número y comienza desde 0.
Esta es la convención del ensamblador gcc.