Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / ASM

Programmazione in ASM per Linux: compilazione e linking

Abstract
Questo articolo descrive i vari modi di creare file eseguibili a partire da sorgenti assembly sotto linux utilizzando nasm e as per la compilazione e gcc e ld per il linking. Per ogni esempio verranno utilizzate sia la sintassi AT&T che quella Intel.
Data di stesura: 26/12/2003
Data di pubblicazione: 26/04/2004
Ultima modifica: 04/04/2006
di Davide Coppola Discuti sul forum   Stampa

1. Premesse

Questo articolo utilizza dei codici di esempio in assembly che hanno come fine la stampa a video di "hello World!". Per quanto essi siano semplici e commentati è richiesta una conoscenza base dell'assembly, delle funzioni libc (printf) e delle system call linux per comprenderli. Inoltre è consigliato avere una minima esperienza nella compilazione mediante gcc.

2. Compilazione & linking

A partire da un sorgente è necessario eseguire diverse operazioni per ottenere un file eseguibile. Le due principali sono la compilazione e il linking.
Durante la fase di compilazione viene effettuata tutta l'analisi necessaria a verificare la correttezza del codice e, in caso di esito positivo, si passa alla traduzione del sorgente in linguaggio macchina.
Dopo la fase di compilazione, di solito, è necessaria una fase di linking in cui si vanno a collegare più file compilati ed eventuali librerie statiche e/o dinamiche per ottenere un eseguibile correttamente caricabile dal sistema operativo.

3. Sintassi Intel

Per compilare, sotto linux, sorgenti in asm scritti seguendo la sintassi intel è necessario utilizzare il nasm[1] conosciuto anche come Netwide Assembler.
Il NASM è in grado di generare file oggetto sia a.out che ELF (oltre tanti altri tipi, anche per windows, che non vedremo in questa guida).
Attualmente, per linux, è consigliato l'utilizzo di file eseguibili ELF, più moderni e performanti del vecchio tipo a.out, ed infatti tutti i file oggetto che genereremo saranno del tipo ELF.

Per quanto riguarda la fase di linking ci sono due possibilità, l'utilizzo di gcc oppure di ld. All'interno della guida alterneremo questi due strumenti andando ad utilizzare anche una funzione della libc (printf) per vedere come comportarci nel caso si necessitasse di funzioni C all'interno dei nostri programmi asm.

Di seguito sono riportate tutti i possibili casi con i rispettivi commenti, gli esempi sono ordinati in maniera tale da arrivare, alla fine, alla creazione del più piccolo eseguibile generabile mediante l'utilizzo del nasm e di gcc/ld.

3.1. Linking con gcc e utilizzo libc

Nel primo esempio utilizzeremo la funzione printf() della libc per stampare a video la nostra stringa "Hello World!", compileremo il sorgente col nasm e "linkeremo" con il gcc. Questo è il sorgente su cui opereremo:
  1. ;sezione dati  
  2.  
  3. section .data 
  4. mess:   db "hello world!", 0xa, 0  ;stringa, \n , NULL 
  5.  
  6. ;sezione sorgente 
  7.  
  8. section .text 
  9. extern printf      ;printf è un simbolo esterno al sorgente 
  10. global main        ;rende disponibile il main all'esterno 
  11.  
  12. main:            
  13.  
  14. ;usiamo printf 
  15.  
  16. push dword mess    ;pusha il valore dell'indirizzo della stringa 
  17. call printf        ;chiama printf della libc 
  18. add esp, 4         ;puliamo lo stack dagli ultimi 4 byte inseriti (mess) 
  19.  
  20. ;usiamo la syscall exit 
  21.  
  22. mov     ebx,0      ;unico argomento, il valore di ritorno 
  23. mov     eax,1      ;numero della syscall exit 
  24. int     0x80       ;si passa in kernel mode per eseguire la syscall 
Come potete notare utilizziamo main e non _start come etichetta (come solitamente si fa con i programmi asm) di inizio programma poiché l'etichetta _start è utilizzata già nel file crt1.o "linkato" dal gcc per utilizzare la libc (come vedremo meglio in seguito). Inoltre tale file va a cercare l'etichetta main nel nostro file oggetto, pertanto non ci è consentito utilizzare altre etichette all'infuori di main (come vuole lo standard ansi C d'altronde).

Per compilare e "linkare" eseguiamo da shell le seguenti azioni:

m3xican@napolihak.it:~/asm/$ nasm -felf hw_lc_gcc.asm

m3xican@napolihak.it:~/asm/$ gcc -s -o hw_lc_gcc hw_lc_gcc.o
Con la prima creeremo il file oggetto (hw_lc_gcc.o) che poi daremo in pasto al gcc per generare il file eseguibile (hw_lc_gcc).
Come possiamo notare quando compiliamo col nasm, utilizziamo l'opzione -f seguita da "elf", ciò serve a generare un file oggetto del formato ELF.
Durante la fase di linking del gcc si utilizza l'opzione -s, che serve ad effettuare lo strip dell'eseguibile, ovvero la rimozione di tutti i simboli e delle sezioni del file non utilizzate durante l'esecuzione, con ovvio risultato la generazione di un file eseguibile di dimensioni ridotte.
L'opzione -o invece serve a indicare il nome del file eseguibile, hw_lc_gcc nel nostro caso.

Adesso andiamo ad analizzare quello che abbiamo generato, dal punto di vista delle dimensioni.

m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2668 Dec 23 11:48 hw_lc_gcc*
-rw-r--r--    1 m3xican  users         720 Dec 23 11:47 hw_lc_gcc.o
-rw-r--r--    1 m3xican  users         658 Dec 26 16:44 hw_lc_gcc.asm
Considerando che lo stesso programma in C compilato e "linkato" con il gcc utilizzando l'opzione -s occupa la stessa esatta quantità di memoria (2668 byte) possiamo ritenere il nostro risultato non entusiasmante! Ma non scoraggiamoci e proseguiamo.

3.2. Linking con gcc e utilizzo di system call

In questo esempio, per stampare a video, non utilizzeremo più una funzione della libc, ma bensì una system call, write per la precisione. La compilazione sarà sempre affidata a nasm e il linking a gcc.
Ecco il sorgente:
  1. ;sezione dati 
  2.  
  3. section .data 
  4. ;stringa, \n 
  5. hw:     db      "Hello world!",0xa 
  6. ;la lunghezza della nostra stringa, calcolata come differenza  
  7. ;dell'indirizzo dell'istruzione meno l'indirizzo della stringa 
  8. lun:    equ     $ - hw 
  9.                                          
  10. ;sezione testo 
  11.  
  12. section .text 
  13.  
  14. global main         ;rende disponibile il main all'esterno 
  15.  
  16. main: 
  17.  
  18. ;usiamo la syscall write 
  19.  
  20. mov     edx,lun     ;terzo argomento, la lunghezza del messaggio 
  21. mov     ecx,hw      ;secondo argomento, l'indirizzo del messaggio 
  22. mov     ebx,1       ;primo argomento, il descrittore del file (stdout) 
  23. mov     eax,4       ;numero della syscall write 
  24. int     0x80        ;si passa in kernel mode per eseguire la syscall 
  25.  
  26. ;usiamo la syscall exit 
  27.  
  28. mov     ebx,0       ;unico argomento, il valore di ritorno 
  29. mov     eax,1       ;numero della syscall exit 
  30. int     0x80        ;si passa in kernel mode per eseguire la syscall 
Anche in questo caso vale il discorso per il simbolo main fatto nel paragrafo 3.1.

Per compilare e "linkare" eseguiamo da shell le seguenti azioni:

m3xican@napolihak.it:~/asm/$ nasm -felf hw_gcc.asm

m3xican@napolihak.it:~/asm/$ gcc -s -o hw_gcc hw_gcc.o
Avendo già commentato nel paragrafo 3.1 i comandi per la compilazione e il linking passiamo all'analisi delle dimensioni
m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2584 Dec 23 22:43 hw_gcc*
-rw-r--r--    1 m3xican  users         935 Dec 23 22:23 hw_gcc.asm
-rw-r--r--    1 m3xican  users         720 Dec 23 11:51 hw_gcc.o
Come potete vedere abbiamo risparmiato 84 byte, il guadagno non è così significativo, soprattutto considerando che ci siamo andati a complicare (relativamente) la vita utilizzando una system call e non una funzione libc.

3.3. Linking con ld e utilizzo libc

In questo esempio utilizzeremo la funzione printf() della libc. La compilazione avverrà sempre col nasm, ma utilizzeremo ld per "linkare".

Il sorgente da utilizzare in questo caso è lo stesso visto nel paragrafo 3.1, pertanto vi basterà copiarlo nel file hw_lc_ld.asm per poter effettuare le varie operazioni di compilazione e "linkaggio" che ora andremo a vedere.

m3xican@napolihak.it:~/asm/$ nasm -felf hw_lc_ld.asm

m3xican@napolihak.it:~/asm/$ ld -s -o hw_lc_ld hw_lc_ld.o \
	/usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o \
	-lc -dynamic-linker /lib/ld-linux.so.2
Per quanto riguarda la compilazione, niente di invariato, invece il linking sembra abbastanza complesso, ma andiamo ad analizzarlo in dettaglio.
Innanzitutto le opzioni -s e -o hanno la stessa funzione di quelle viste per il gcc. I vari file /usr/lib/crt* che inseriamo nel linking sono i file necessari per includere ed utilizzare correttamente le funzioni libc. L'opzione -l seguita da una c dice al linker di utilizzare la libreria c. L'opzione -dinamic-linker seguita da /lib/ld-linux.so.2 permette di aggiungere il linker/loader dinamico ld-linux, necessario per richiamare la libc (e in generale per richiamare qualsiasi libreria dinamica).

Ecco cosa siamo riusciti ad ottenere in termini di spazio.

m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2220 Dec 23 11:57 hw_lc_ld*
-rw-r--r--    1 m3xican  users         720 Dec 23 11:56 hw_lc_ld.o
Rispetto la versione mostrata nel paragrafo 3.1 abbiamo guadagnato 448 byte, il che ci fa capire che il gcc inserisce delle cose non necessarie alla corretta esecuzione, per sapere cosa possiamo "linkare" utilizzando l'opzione -v come segue:
m3xican@napolihak.it:~/asm/$ gcc -s -v -o hw_lc_gcc hw_lc_gcc.o
 ... specifiche varie... 
/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/collect2 \
	--eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 \
	-o hw_lc_gcc \
	-s /usr/lib/gcc-lib/i486-slackware-linux/3.2.3/../../../crt1.o \
	/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/../../../crti.o \
	/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/crtbegin.o \
	-L/usr/lib/gcc-lib/i486-slackware-linux/3.2.3 \
	-L/usr/i486-slackware-linux/lib \
	-L/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/../../.. \
	hw_lc_gcc.o -lgcc -lgcc_eh -lc -lgcc -lgcc_eh \
	/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/crtend.o \
	/usr/lib/gcc-lib/i486-slackware-linux/3.2.3/../../../crtn.o
In pratica vengono aggiunti oltre ai vari file crt* che abbiamo visto e all'ld-linux altri crt* utilizzati dal gcc (crtbegin.o e crtend.o) e anche un po' di librerie legate al gcc (-lgcc -lgcc_eh).

Il guadagno ottenuto non è male, ma possiamo fare sicuramente di meglio.

3.4. Linking con ld e utilizzo di system call

Anche questa volta, per stampare a video, non utilizzeremo più una funzione della libc, ma la system call write. La compilazione sarà sempre affidata a nasm e il linking a ld.

Il sorgente da utilizzare in questo caso è lo stesso visto nel paragrafo 3.2, pertanto vi basterà copiarlo nel file hw_ld.asm per poter effettuare le varie operazioni di compilazione e "linkaggio" che ora andremo a vedere.

m3xican@napolihak.it:~/asm/$ nasm -felf hw_ld.asm

m3xican@napolihak.it:~/asm/$ ld -e main -s -o hw_ld hw_ld.o
La compilazione non necessita commenti, mentre il "linkaggio" presenta un'opzione nuova, -e. Questa opzione ci permette di utilizzare un nome scelto da noi come simbolo iniziale del programma. Nel caso non l'avessimo utilizzata avremmo dovuto utilizzare _start al posto di main all'interno del sorgente.

Passiamo all'analisi delle dimensioni.

m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users         488 Dec 24 13:42 hw_ld*
-rw-r--r--    1 m3xican  users         720 Dec 24 13:42 hw_ld.o
come potete vedere questa volta il guadagno è davvero notevole, ben 2180 byte di differenza dalla versione realizzata nel pragrafo 3.1 utilizzando libc e gcc come linker.

4. Sintassi AT&T

Per compilare, sotto linux, sorgenti in asm scritti seguendo la sintassi AT&T è necessario utilizzare as, compilatore presente nel pacchetto binutils[2] fornito con ogni "distro". L'as produce file oggetto in formato ELF, pertanto va più che bene per le nostre esigenze

Per quanto riguarda la fase di linking, anche in questo caso ci sono le due possibilità già viste, ovvero l'utilizzo di gcc oppure di ld.

Oltre queste quattro combinazioni è possibile compilare e "linkare" direttamente con il gcc, utilizzando la classica sintassi che si usa quando si vuole compilare un file .c, ma non analizzeremo in dettaglio questa possibilità in quanto essa produce il risultato peggiore in termini di spazio e poiché vogliamo compilare con as.

Di seguito sono riportate tutti i possibili casi con i rispettivi commenti, gli esempi sono ordinati in maniera tale da arrivare, alla fine, alla creazione del più piccolo eseguibile generabile mediante l'utilizzo di as e di gcc/ld.

4.1 Linking con gcc e utilizzo libc

Nel primo esempio utilizzeremo la funzione printf() della libc per stampare a video la nostra stringa "Hello World!", compileremo il sorgente col nasm e linkeremo con il gcc. Questo è il sorgente su cui opereremo:
  1. #sezione dati  
  2.  
  3. .data 
  4. #stringa\n  
  5. mess:   .string  "Hello World!\n" 
  6.  
  7. #sezione sorgente 
  8.  
  9. .text 
  10. .global main        #rende disponibile il main all'esterno 
  11.  
  12. main:            
  13.  
  14. #usiamo printf 
  15.  
  16. pushl   $mess       #pusha il valore dell'indirizzo della stringa 
  17. call    printf      #chiama printf della libc 
  18. addl    $0x4, %esp  #puliamo lo stack dagli ultimi 4 byte inseriti (mess) 
  19.  
  20. #usiamo la syscall exit 
  21.  
  22. movl   $0x0, %ebx   #unico argomento, il valore di ritorno 
  23. movl   $0x1, %eax   #numero della syscall exit 
  24. int    $0x80        #si passa in kernel mode per eseguire la syscall 
Per compilare e "linkare" effettuiamo le seguenti azioni.
m3xican@napolihak.it:~/asm/$ as -o hw_lc_gcc.o hw_lc_gcc.s

m3xican@napolihak.it:~/asm/$ gcc -s -o hw_lc_gcc hw_lc_gcc.o
A differenza del nasm, as, di default, genera come file oggetto a.out, pertanto per generare hw_lc_gcc.o dobbiamo utilizzare la classica opzione -o.
Il linking non presenta nessuna differenza da quelli già visti in precedenze, pertanto analizziamo il risultato dal punto di vista dei byte occupati.
m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2620 Dec 23 22:09 hw_lc_gcc*
-rw-r--r--    1 m3xican  users         604 Dec 23 22:06 hw_lc_gcc.o
-rw-r--r--    1 m3xican  users         590 Dec 24 16:06 hw_lc_gcc.s
Anche in questo caso "linkare" con gcc non porta a risultati entusiasmanti (anche se leggermente superiori a quelli ottenuti col nasm), ma la cosa non era inaspettata, soprattutto dopo aver visto quante cose gcc aggiunge all'eseguibile.

4.2. Linking con gcc e utilizzo di system call

In questo esempio, per stampare a video, non utilizzeremo più una printf, ma bensì la system call write. La compilazione sarà sempre affidata ad as ed il linking a gcc. Ecco il sorgente:
  1. #sezione dati 
  2.  
  3. .data 
  4. #stringa 
  5. msg:    .string "Hello World!\n" 
  6.  
  7. .text               #sezione testo 
  8. .global main        #rende disponibile il main all'esterno 
  9.  
  10. main: 
  11.  
  12. #usiamo write 
  13.  
  14. movl    $0xd,%edx   #terzo argomento, la lunghezza del messaggio 
  15. movl    $msg,%ecx   #secondo argomento, l'indirizzo del messaggio 
  16. movl    $0x1,%ebx   #primo argomento, il descrittore del file (stdout) 
  17. movl    $0x4,%eax   #numero della syscall write 
  18. int     $0x80       #si passa in kernel mode per eseguire la syscall 
  19.  
  20. #usiamo exit 
  21.  
  22. movl    $0x0,%ebx   #unico argomento, il valore di ritorno 
  23. movl    $0x1,%eax   #numero della syscall exit 
  24. int     $0x80       #si passa in kernel mode per eseguire la syscall 
Per compilare e "linkare" eseguiamo:
m3xican@napolihak.it:~/asm/$ as -o hw_gcc.o hw_gcc.s

m3xican@napolihak.it:~/asm/$ gcc -s -o hw_gcc hw_gcc.o
Non essendoci novità nelle azioni compiute possiamo passare subito ad analizzare il risultato
m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2584 Dec 23 22:43 hw_gcc*
-rw-r--r--    1 m3xican  users         653 Dec 24 15:56 hw_gcc.s
-rw-r--r--    1 m3xican  users         588 Dec 23 22:24 hw_gcc.o
Come possiamo notare abbiamo ottenuto la stessa occupazione di byte ottenuta nel paragrafo 3.2, ovvero, utilizzando la system call write, è stato il gcc a "non" fare la differenza eguagliando i risultati.

4.3. Linking con ld e utilizzo libc

In questo esempio utilizzeremo la funzione printf() della libc. La compilazione avverrà sempre con as, ma utilizzeremo ld per "linkare".

Il sorgente da utilizzare in questo caso è lo stesso visto nel paragrafo 4.1, pertanto vi basterà copiarlo nel file hw_lc_ld.s per poter effettuare le varie operazioni di compilazione e "linkaggio" che ora andremo a vedere.

m3xican@napolihak.it:~/asm/$ as -o hw_lc_ld.o  hw_lc_ld.s

m3xican@napolihak.it:~/asm/$ ld -s -o hw_lc_ld hw_lc_ld.o /usr/lib/crt1.o \
	/usr/lib/crti.o /usr/lib/crtn.o -lc -dynamic-linker /lib/ld-linux.so.2
Anche in questo caso tocca "linkare" tutte le componenti richieste per far funzionare correttamente le funzioni della libc. Vediamone adesso i risultati in termini di spazio occupato.
m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users        2192 Dec 23 22:40 hw_lc_ld*
-rw-r--r--    1 m3xican  users         616 Dec 23 22:40 hw_lc_ld.o
Anche in questo caso abbiamo ottenuto un eseguibile di dimensioni minori, e soprattutto di dimensioni minori rispetto a quello realizzato con sintassi intel nel paragrafo 3.3.

4.4. Linking con ld e utilizzo di system call

Anche questa volta, per stampare a video, non utilizzeremo più una funzione della libc, ma la system call write. La compilazione sarà sempre affidata ad as ed il linking a ld.

Il sorgente da utilizzare in questo caso è lo stesso visto nel paragrafo 4.2, pertanto vi basterà copiarlo nel file hw_ld.smper poter effettuare le varie operazioni di compilazione e "linkaggio" che ora andremo a vedere.

m3xican@napolihak.it:~/asm/$ as -o hw_ld.o hw_ld.s

m3xican@napolihak.it:~/asm/$ ld -e main -s -o hw_ld hw_ld.o
Avendo già abbondantemente descritto le operazioni di compilazione e "linkaggio", possiamo passare all'analisi dei risultati in termini di spazio occupato.
m3xican@napolihak.it:~/asm/$ ls -lS

-rwxr-xr-x    1 m3xican  users         396 Dec 23 22:30 hw_ld*
-rw-r--r--    1 m3xican  users         588 Dec 23 22:30 hw_ld.o
Questa volta abbiamo ottenuto un guadagno notevole, ben 2224 byte rispetto la versione iniziale vista nel paragrafo 4.1, ma soprattutto ben 2272 byte rispetto alla versione iniziale realizzata con sintassi intel nel paragrafo 3.1.
Volendola paragonare alla corrispettiva vista nel paragrafo 3.4 il guadagno è di 92 byte, non male davvero.

5. Andare oltre

Abbiamo visto diverse tecniche per compilare e "linkare" un semplice programma realizzato in asm per linux, riuscendo a realizzare un "Hello World!" di ben 396 byte, credo che chiedersi se si possa andare oltre e ridurre ancora le dimensioni sia una curiosità naturale.
Logicamente la risposta è affermativa, infatti una prima riduzione dello spazio può essere effettuata andando a modificare delle istruzioni asm, utilizzandone corrispettivi che utilizzano meno byte. Però questa riduzione non và oltre le poche decine di byte, per avere una riduzione davvero significativa si devono inserire manualmente (per mezzo dell'asm) tutte e sole le informazioni necessarie e sufficienti alla corretta esecuzione del file di formato ELF, andando ad eseguire, in pratica, uno strip manuale. Applicando tale tecnica si può scendere facilmente sotto la soglia dei 150 byte, ma di questo, vista la complessità e le conoscenze richieste, ne parleremo in dettaglio in una prossima guida.

6. Tabella riassuntiva

Compilazione nasm -felf prog.asm
Linking con gcc e libc gcc -s -o prog prog.o
Linking con gcc e system call gcc -s -o prog prog.o
Linking con ld e libc ld -s -o prog prog.o /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o -lc -dynamic-linker /lib/ld-linux.so.2
Linking con ld e system call ld -e main -s -o prog prog.o
Compilazione as -o prog.o prog.s
Linking con gcc e libc gcc -s -o prog prog.o
Linking con gcc e system call gcc -s -o prog prog.o
Linking con ld e libc ld -s -o prog prog.o /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o -lc -dynamic-linker /lib/ld-linux.so.2
Linking con ld e system call ld -e main -s -o prog prog.o

Informazioni sull'autore

Davide Coppola, studente di informatica alla Federico II di Napoli, appassionato di programmazione, sicurezza e linux. Fondatore e sviluppatore di dev-labs e di Mars Land of No Mercy. Per maggiori informazioni e contatti potete visitare la sua home page.

È possibile consultare l'elenco degli articoli scritti da Davide Coppola.

Altri articoli sul tema Linguaggi / ASM.

Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.