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
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:
;sezione dati
section .data
mess: db "hello world!", 0xa, 0 ;stringa, \n , NULL
;sezione sorgente
section .text
extern printf ;printf è un simbolo esterno al sorgente
global main ;rende disponibile il main all'esterno
main:
;usiamo printf
push dword mess ;pusha il valore dell'indirizzo della stringa
call printf ;chiama printf della libc
add esp, 4 ;puliamo lo stack dagli ultimi 4 byte inseriti (mess)
;usiamo la syscall exit
mov ebx,0 ;unico argomento, il valore di ritorno
mov eax,1 ;numero della syscall exit
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:
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:
;sezione dati
section .data
;stringa, \n
hw: db "Hello world!",0xa
;la lunghezza della nostra stringa, calcolata come differenza
;dell'indirizzo dell'istruzione meno l'indirizzo della stringa
lun: equ $ - hw
;sezione testo
section .text
global main ;rende disponibile il main all'esterno
main:
;usiamo la syscall write
mov edx,lun ;terzo argomento, la lunghezza del messaggio
mov ecx,hw ;secondo argomento, l'indirizzo del messaggio
mov ebx,1 ;primo argomento, il descrittore del file (stdout)
mov eax,4 ;numero della syscall write
int 0x80 ;si passa in kernel mode per eseguire la syscall
;usiamo la syscall exit
mov ebx,0 ;unico argomento, il valore di ritorno
mov eax,1 ;numero della syscall exit
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:
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.
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:
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.
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:
#sezione dati
.data
#stringa\n
mess: .string "Hello World!\n"
#sezione sorgente
.text
.global main #rende disponibile il main all'esterno
main:
#usiamo printf
pushl $mess #pusha il valore dell'indirizzo della stringa
call printf #chiama printf della libc
addl $0x4, %esp #puliamo lo stack dagli ultimi 4 byte inseriti (mess)
#usiamo la syscall exit
movl $0x0, %ebx #unico argomento, il valore di ritorno
movl $0x1, %eax #numero della syscall exit
int $0x80 #si passa in kernel mode per eseguire la syscall
Per compilare e "linkare" effettuiamo le seguenti azioni.
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:
#sezione dati
.data
#stringa
msg: .string "Hello World!\n"
.text #sezione testo
.global main #rende disponibile il main all'esterno
main:
#usiamo write
movl $0xd,%edx #terzo argomento, la lunghezza del messaggio
movl $msg,%ecx #secondo argomento, l'indirizzo del messaggio
movl $0x1,%ebx #primo argomento, il descrittore del file (stdout)
movl $0x4,%eax #numero della syscall write
int $0x80 #si passa in kernel mode per eseguire la syscall
#usiamo exit
movl $0x0,%ebx #unico argomento, il valore di ritorno
movl $0x1,%eax #numero della syscall exit
int $0x80 #si passa in kernel mode per eseguire la syscall
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.
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.
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.