Ecco come va:
.file "test.c"
Il nome del file di origine originale (utilizzato dai debugger).
.section .rodata
.LC0:
.string "Hello world!"
Una stringa con terminazione zero è inclusa nella sezione ".rodata" ("ro" significa "sola lettura":l'applicazione sarà in grado di leggere i dati, ma qualsiasi tentativo di scrittura su di essi attiverà un'eccezione).
.text
Ora scriviamo le cose nella sezione ".text", che è dove va il codice.
.globl main
.type main, @function
main:
Definiamo una funzione chiamata "main" e globalmente visibile (altri file oggetto potranno invocarla).
leal 4(%esp), %ecx
Archiviamo nel registro %ecx
il valore 4+%esp
(%esp
è il puntatore dello stack).
andl $-16, %esp
%esp
è leggermente modificato in modo che diventi un multiplo di 16. Per alcuni tipi di dati (il formato a virgola mobile corrispondente al double
di C e long double
), le prestazioni sono migliori quando gli accessi alla memoria sono a indirizzi multipli di 16. Questo non è veramente necessario qui, ma se usato senza il flag di ottimizzazione (-O2
...), il compilatore tende a produrre un bel po' di codice generico inutile (cioè codice che potrebbe essere utile in alcuni casi ma non qui).
pushl -4(%ecx)
Questo è un po' strano:a quel punto, la parola all'indirizzo -4(%ecx)
è la parola che era in cima alla pila prima di andl
. Il codice recupera quella parola (che dovrebbe essere l'indirizzo di ritorno, tra l'altro) e la spinge di nuovo. Questo tipo di emula ciò che si otterrebbe con una chiamata da una funzione che aveva uno stack allineato a 16 byte. La mia ipotesi è che questo push
è un residuo di una sequenza di copia di argomenti. Poiché la funzione ha modificato il puntatore dello stack, deve copiare gli argomenti della funzione, che erano accessibili tramite il vecchio valore del puntatore dello stack. Qui non ci sono argomenti, eccetto l'indirizzo di ritorno della funzione. Nota che questa parola non verrà utilizzata (ancora una volta, questo è codice senza ottimizzazione).
pushl %ebp
movl %esp, %ebp
Questo è il prologo della funzione standard:salviamo %ebp
(poiché stiamo per modificarlo), quindi imposta %ebp
puntare allo stack frame. Successivamente, %ebp
verrà utilizzato per accedere agli argomenti della funzione, creando %esp
di nuovo libero. (Sì, non ci sono argomenti, quindi è inutile per quella funzione.)
pushl %ecx
Salviamo %ecx
(ne avremo bisogno all'uscita dalla funzione, per ripristinare %esp
al valore che aveva prima di andl
).
subl $20, %esp
Riserviamo 32 byte sullo stack (ricorda che lo stack cresce "verso il basso"). Quello spazio verrà utilizzato per memorizzare gli argomenti di printf()
(questo è eccessivo, dato che c'è un singolo argomento, che utilizzerà 4 byte [questo è un puntatore]).
movl $.LC0, (%esp)
call printf
"Spingiamo" l'argomento a printf()
(cioè ci assicuriamo che %esp
punta a una parola che contiene l'argomento, qui $.LC0
, che è l'indirizzo della stringa costante nella sezione rodata). Quindi chiamiamo printf()
.
addl $20, %esp
Quando printf()
restituisce, rimuoviamo lo spazio allocato per gli argomenti. Questo addl
annulla ciò che il subl
sopra.
popl %ecx
Recuperiamo %ecx
(spinto sopra); printf()
potrebbe averlo modificato (le convenzioni di chiamata descrivono quali registri possono essere modificati da una funzione senza ripristinarli all'uscita; %ecx
è uno di questi registri).
popl %ebp
Epilogo della funzione:ripristina %ebp
(corrispondente al pushl %ebp
sopra).
leal -4(%ecx), %esp
Ripristiniamo %esp
al suo valore iniziale. L'effetto di questo codice operativo è di memorizzare in %esp
il valore %ecx-4
. %ecx
è stato impostato nel primo codice operativo della funzione. Questo annulla qualsiasi modifica a %esp
, incluso il andl
.
ret
Uscita dalla funzione.
.size main, .-main
Questo imposta la dimensione del main()
function:in qualsiasi momento durante l'assembly, ".
" è un alias per "l'indirizzo a cui stiamo aggiungendo le cose in questo momento". Se un'altra istruzione è stata aggiunta qui, andrebbe all'indirizzo specificato da ".
". Pertanto, ".-main
", qui, è la dimensione esatta del codice della funzione main()
. Il .size
indica all'assembler di scrivere tali informazioni nel file oggetto.
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
GCC ama solo lasciare tracce della sua azione. Questa stringa finisce come una sorta di commento nel file oggetto. Il linker lo rimuoverà.
.section .note.GNU-stack,"",@progbits
Una sezione speciale in cui GCC scrive che il codice può contenere uno stack non eseguibile. Questo è il caso normale. Gli stack eseguibili sono necessari per alcuni usi speciali (non C standard). Sui processori moderni, il kernel può creare uno stack non eseguibile (uno stack che attiva un'eccezione se qualcuno tenta di eseguire come codice alcuni dati che si trovano nello stack); questo è visto da alcune persone come una "caratteristica di sicurezza" perché mettere il codice nello stack è un modo comune per sfruttare gli overflow del buffer. Con questa sezione, l'eseguibile sarà contrassegnato come "compatibile con uno stack non eseguibile" che il kernel fornirà felicemente come tale.
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
queste istruzioni non si confrontano nel tuo programma c, sono sempre eseguite all'inizio di ogni funzione (ma dipende dal compilatore/piattaforma)
movl $.LC0, (%esp)
call printf
questo blocco corrisponde alla tua chiamata printf(). la prima istruzione mette nello stack il suo argomento (un puntatore a "ciao mondo") quindi chiama la funzione.
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
queste istruzioni sono opposte al primo blocco, sono una sorta di materiale per la manipolazione dello stack. sempre eseguito anch'esso
Ecco qualche supplemento a @Thomas Pornin
la risposta di.
.LC0
costante locale, ad esempio stringa letterale..LFB0
inizio della funzione locale,.LFE0
fine della funzione locale,
Il suffisso di queste etichette è un numero e parte da 0.
Questa è la convenzione dell'assembler gcc.