Voici comment ça se passe :
.file "test.c"
Le nom du fichier source d'origine (utilisé par les débogueurs).
.section .rodata
.LC0:
.string "Hello world!"
Une chaîne terminée par zéro est incluse dans la section ".rodata" ("ro" signifie "lecture seule" :l'application pourra lire les données, mais toute tentative d'écriture dedans déclenchera une exception).
.text
Maintenant, nous écrivons les choses dans la section ".text", où va le code.
.globl main
.type main, @function
main:
On définit une fonction appelée "main" et visible globalement (d'autres fichiers objets pourront l'invoquer).
leal 4(%esp), %ecx
Nous stockons dans le registre %ecx
la valeur 4+%esp
(%esp
est le pointeur de pile).
andl $-16, %esp
%esp
est légèrement modifié pour devenir un multiple de 16. Pour certains types de données (le format à virgule flottante correspondant au double
de C et long double
), les performances sont meilleures lorsque les accès mémoire sont à des adresses multiples de 16. Ce n'est pas vraiment nécessaire ici, mais lorsqu'il est utilisé sans le drapeau d'optimisation (-O2
...), le compilateur a tendance à produire pas mal de code générique inutile (c'est-à-dire du code qui pourrait être utile dans certains cas mais pas ici).
pushl -4(%ecx)
Celui-ci est un peu bizarre :à ce moment-là, le mot à l'adresse -4(%ecx)
est le mot qui était en haut de la pile avant le andl
. Le code récupère ce mot (qui devrait être l'adresse de retour, soit dit en passant) et le pousse à nouveau. Ce type d'émule ce qui serait obtenu avec un appel d'une fonction qui avait une pile alignée de 16 octets. Je suppose que ce push
est un reste d'une séquence de copie d'arguments. Puisque la fonction a ajusté le pointeur de pile, elle doit copier les arguments de la fonction, qui étaient accessibles via l'ancienne valeur du pointeur de pile. Ici, il n'y a pas d'argument, sauf l'adresse de retour de la fonction. Notez que ce mot ne sera pas utilisé (encore une fois, c'est du code sans optimisation).
pushl %ebp
movl %esp, %ebp
C'est le prologue de la fonction standard :nous économisons %ebp
(puisque nous sommes sur le point de le modifier), puis définissez %ebp
pour pointer vers le cadre de la pile. Par la suite, %ebp
sera utilisé pour accéder aux arguments de la fonction, faisant %esp
libre à nouveau. (Oui, il n'y a pas d'argument, donc c'est inutile pour cette fonction.)
pushl %ecx
Nous économisons %ecx
(nous en aurons besoin à la sortie de la fonction, pour restaurer %esp
à la valeur qu'il avait avant le andl
).
subl $20, %esp
Nous réservons 32 octets sur la pile (rappelez-vous que la pile grandit "vers le bas"). Cet espace sera utilisé pour stocker les arguments de printf()
(c'est exagéré, puisqu'il y a un seul argument, qui utilisera 4 octets [c'est un pointeur]).
movl $.LC0, (%esp)
call printf
Nous "poussons" l'argument à printf()
(c'est-à-dire que nous nous assurons que %esp
pointe sur un mot qui contient l'argument, ici $.LC0
, qui est l'adresse de la chaîne constante dans la section rodata). Ensuite, nous appelons printf()
.
addl $20, %esp
Quand printf()
renvoie, nous supprimons l'espace alloué aux arguments. Ce addl
annule ce que le subl
ci-dessus a fait.
popl %ecx
Nous récupérons %ecx
(poussé dessus); printf()
peut l'avoir modifié (les conventions d'appel décrivent quel registre une fonction peut modifier sans les restaurer à la sortie ; %ecx
est l'un de ces registres).
popl %ebp
Épilogue de la fonction :cela restaure %ebp
(correspondant au pushl %ebp
ci-dessus).
leal -4(%ecx), %esp
Nous restaurons %esp
à sa valeur initiale. L'effet de cet opcode est de stocker en %esp
la valeur %ecx-4
. %ecx
a été défini dans le premier opcode de fonction. Cela annule toute modification de %esp
, y compris le andl
.
ret
Sortie de fonction.
.size main, .-main
Ceci définit la taille du main()
fonction :à tout moment de l'assemblage, ".
" est un alias pour "l'adresse à laquelle nous ajoutons des choses en ce moment". Si une autre instruction était ajoutée ici, elle irait à l'adresse spécifiée par ".
". Ainsi, ".-main
", ici, c'est la taille exacte du code de la fonction main()
. Le .size
indique à l'assembleur d'écrire ces informations dans le fichier objet.
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
GCC adore laisser des traces de son action. Cette chaîne se termine comme une sorte de commentaire dans le fichier objet. L'éditeur de liens le supprimera.
.section .note.GNU-stack,"",@progbits
Une section spéciale où GCC écrit que le code peut contenir une pile non exécutable. C'est le cas normal. Des piles exécutables sont nécessaires pour certaines utilisations spéciales (pas le C standard). Sur les processeurs modernes, le noyau peut créer une pile non exécutable (une pile qui déclenche une exception si quelqu'un essaie d'exécuter en tant que code certaines données qui se trouvent sur la pile) ; ceci est considéré par certaines personnes comme une "fonction de sécurité" car mettre du code sur la pile est un moyen courant d'exploiter les débordements de tampon. Avec cette section, l'exécutable sera marqué comme "compatible avec une pile non exécutable" que le noyau fournira volontiers comme tel.
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
ces instructions ne se comparent pas dans votre programme c, elles sont toujours exécutées au début de chaque fonction (mais cela dépend du compilateur/de la plate-forme)
movl $.LC0, (%esp)
call printf
ce bloc correspond à votre appel printf(). la première instruction place sur la pile son argument (un pointeur vers "hello world") puis appelle la fonction.
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
ces instructions sont opposées au premier bloc, ce sont une sorte de trucs de manipulation de pile. toujours exécuté aussi
Voici un supplément à @Thomas Pornin
la réponse.
.LC0
constante locale, par exemple un littéral de chaîne..LFB0
début de la fonction locale,.LFE0
fin de fonction locale,
Le suffixe de ces étiquettes est un nombre et commence à 0.
C'est la convention de l'assembleur gcc.