Il minimo assoluto che funzionerà sulla piattaforma che sembra essere, è
.globl main
main:
pushl $.LC0
call puts
addl $4, %esp
xorl %eax, %eax
ret
.LC0:
.string "Hello world"
Ma questo infrange una serie di requisiti ABI. Il minimo per un programma conforme ad ABI è
.globl main
.type main, @function
main:
subl $24, %esp
pushl $.LC0
call puts
xorl %eax, %eax
addl $28, %esp
ret
.size main, .-main
.section .rodata
.LC0:
.string "Hello world"
Tutto il resto nel tuo file oggetto è o il compilatore non ottimizza il codice il più strettamente possibile o facoltativo annotazioni da scrivere nel file oggetto.
Il .cfi_*
le direttive, in particolare, sono annotazioni facoltative. Sono necessari se e solo se la funzione potrebbe trovarsi nello stack di chiamate quando viene lanciata un'eccezione C++, ma sono utili in qualsiasi programma da cui potresti voler estrarre una traccia dello stack. Se hai intenzione di scrivere codice non banale a mano in linguaggio assembly, probabilmente varrà la pena imparare a scriverlo. Sfortunatamente, sono documentati molto male; Al momento non trovo nulla a cui valga la pena collegarsi.
La linea
.section .note.GNU-stack,"",@progbits
è anche importante sapere se stai scrivendo il linguaggio assembly a mano; è un'altra annotazione facoltativa, ma preziosa, perché ciò che significa è "niente in questo file oggetto richiede che lo stack sia eseguibile". Se tutti i file oggetto in un programma hanno questa annotazione, il kernel non renderà eseguibile lo stack, il che migliora un po' la sicurezza.
(Per indicare che fai hai bisogno che lo stack sia eseguibile, metti "x"
invece di ""
. GCC può farlo se usi la sua estensione "nested function". (Non farlo.))
Vale probabilmente la pena ricordare che nella sintassi dell'assembly "AT&T" usata (per impostazione predefinita) da GCC e GNU binutils, ci sono tre tipi di righe:Una riga con un singolo token su di essa, che termina con i due punti, è un'etichetta. (Non ricordo le regole per quali caratteri possono apparire nelle etichette.) Una riga il cui first token inizia con un punto e no termina con i due punti, è una sorta di direttiva per l'assembler. Tutto il resto è un'istruzione di assemblaggio.
correlato:come rimuovere il "rumore" dall'output dell'assembly GCC/clang? Il .cfi
le direttive non ti sono direttamente utili e il programma funzionerebbe senza di esse. (Sono le informazioni di rimozione dello stack necessarie per la gestione delle eccezioni e i backtrace, quindi -fomit-frame-pointer
può essere abilitato per impostazione predefinita. E sì, gcc lo emette anche per C.)
Per quanto riguarda il numero di righe sorgente asm necessarie per produrre un buon programma Hello World, ovviamente vogliamo usare le funzioni libc per fare più lavoro per noi.
La risposta di @ Zwol ha l'implementazione più breve del tuo codice C originale.
Ecco cosa potresti fare manualmente , se non ti interessa lo stato di uscita del tuo programma, solo che stampa la tua stringa.
# Hand-optimized asm, not compiler output
.globl main # necessary for the linker to see this symbol
main:
# main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
movl $.LC0, 4(%esp) # replace our first arg with the string
jmp puts # tail-call puts.
# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
.asciz "Hello world" # asciz zero-terminates
L'equivalente C (hai appena chiesto il Hello World più breve, non uno che avesse una semantica identica):
int main(int argc, char **argv) {
return puts("Hello world");
}
Il suo stato di uscita è definito dall'implementazione ma stampa sicuramente. puts(3)
restituisce "un numero non negativo", che potrebbe essere al di fuori dell'intervallo 0..255, quindi non possiamo dire nulla sul fatto che lo stato di uscita del programma sia 0 / diverso da zero in Linux (dove lo stato di uscita del processo è il minimo 8 bit dell'intero passato al exit_group()
chiamata di sistema (in questo caso dal codice di avvio CRT che ha chiamato main()).
L'utilizzo di JMP per implementare la chiamata di coda è una pratica standard e viene comunemente utilizzato quando una funzione non deve eseguire alcuna operazione dopo il ritorno di un'altra funzione. puts()
tornerà infine alla funzione che ha chiamato main()
, proprio come se puts() fosse tornato a main() e poi main() fosse tornato. il chiamante di main() deve ancora occuparsi degli argomenti che ha messo nello stack per main(), perché sono ancora lì (ma modificati, e ci è permesso farlo).
gcc e clang non generano codice che modifica lo spazio di passaggio degli argomenti nello stack. Tuttavia, è perfettamente sicuro e conforme ad ABI:le funzioni "possiedono" i propri argomenti nello stack, anche se fossero const
. Se chiami una funzione, non puoi presumere che gli argomenti che hai messo in pila siano ancora lì. Per effettuare un'altra chiamata con argomenti uguali o simili, devi memorizzarli nuovamente tutti.
Nota anche che questo chiama puts()
con lo stesso allineamento dello stack che avevamo all'ingresso di main()
, quindi ancora una volta siamo conformi all'ABI nel preservare l'allineamento 16B richiesto dalla versione moderna dell'ABI x86-32 noto anche come i386 System V (utilizzato da Linux).
.string
stringhe con terminazione zero, come .asciz
, ma ho dovuto cercarlo per controllare. Consiglierei di usare solo .ascii
o .asciz
per assicurarti di essere chiaro se i tuoi dati hanno o meno un byte di terminazione. (Non ne hai bisogno se lo usi con funzioni di lunghezza esplicita come write()
)
In x86-64 System V ABI (e Windows), gli argomenti vengono passati nei registri. Questo rende l'ottimizzazione delle chiamate in coda molto più semplice, perché puoi riorganizzare gli argomenti o passare altro args (purché non si esauriscano i registri). Questo rende i compilatori disposti a farlo nella pratica. (Perché, come ho detto, al momento non gli piace generare codice che modifichi lo spazio degli argomenti in entrata nello stack, anche se l'ABI è chiaro che sono autorizzati a farlo, e le funzioni generate dal compilatore presumono che i chiamati ostruiscano i loro argomenti dello stack .)
clang o gcc -O3 eseguiranno questa ottimizzazione per x86-64, come puoi vedere nell'esploratore del compilatore Godbolt :
#include <stdio.h>
int main() { return puts("Hello World"); }
# clang -O3 output
main: # @main
movl $.L.str, %edi
jmp puts # TAILCALL
# Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
.asciz "Hello World"
Gli indirizzi di dati statici rientrano sempre nei 31 bit bassi dello spazio degli indirizzi e gli eseguibili non necessitano di codice indipendente dalla posizione, altrimenti il mov
sarebbe lea .LC0(%rip), %rdi
. (Lo otterrai da gcc se è stato configurato con --enable-default-pie
per creare eseguibili indipendenti dalla posizione.)
Come caricare l'indirizzo della funzione o dell'etichetta nel registro in GNU Assembler
Hello World con Linux x86 a 32 bit int 0x80
chiamate di sistema direttamente, niente libc
Vedi Hello, world in assembly language con chiamate di sistema Linux? La mia risposta è stata originariamente scritta per SO Docs, poi spostata qui come posto dove metterla quando SO Docs è stato chiuso. Non era proprio qui, quindi l'ho spostato in un'altra domanda.
correlato:Un tutorial vorticoso sulla creazione di eseguibili ELF davvero teenager per Linux. Il file binario più piccolo che puoi eseguire che fa solo una chiamata di sistema exit(). Si tratta di ridurre al minimo la dimensione del binario, non la dimensione della sorgente o anche solo il numero di istruzioni che vengono effettivamente eseguite.