English (UK)

Corso base assembler

Per i neofiti della programmazione a basso livello, il web è ricco di guide. Per iniziare proprio da zero, è comunque un buon approdo questo microcorso pubblicato sul sito programmazione.it:

Lezione 1
Lezione 2
Lezione 3
Lezione 4

Si tratta proprio dei primi passi, ma è spiegata bene e per rompere il ghiaccio è un buon inizio.

Per evitare che nel corso del tempo si perda il link sul blog da cui ho tratto i link, riporto anche il testo dei post per intero:

L’obiettivo di questo breve tutorial è quello di fornire alcuni semplici esempi pratici di utilizzo del celebre linguaggio Assembly. Si presupporrà quindi la conoscenza della teoria che c’è dietro questo linguaggio e, per alcune parti, del linguaggio C. In particolare ci soffermeremo sull’architettura Intel IA-32 e su un sistema GNU/Linux 2.6.

Per quanto riguarda la sintassi è necessario sottolineare che esistono due differenti dialetti Assembly molto noti:

* quello relativo agli assemblatori dei sistemi operativi Windows, che prevede una sintassi cosiddetta Intel;

* e quello relativo all’assemblatore GNU as, che prevede la sintassi chiamata AT&T. Questo assemblatore è quello che viene invocato dal compilatore gcc.

Anche se attualmente as è in grado di gestire entrambe le sintassi, nei nostri esempi utilizzeremo quella AT&T in quanto attraverso tale sintassi il compilatore gcc produce i file Assembly.

Partiamo quindi dal classico "Hello World!" e iniziamo a vedere come il compilatore gcc stesso costruisce il codice assembler, ottenuto con l'opzione –s:

.file "HelloWorld.c"
.section .rodata

.LC0:
.string "Hello World!n"
.text
.globl main
.type main,@function

main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
subl %eax, %esp
subl $12, %esp
pushl $.LC0
call printf
addl $16,%esp
movl $0, %eax
leave
ret


.size main -main
.ident "GCC: (GNU) 3.3.4"

Come possiamo vedere il programma inizia con una serie di comandi che definiscono il file, i simboli e la stringa "Hello World!". Successivamente troviamo il main con le prime due istruzioni pushl e movl, che rappresentano le classiche istruzioni di entry al programma Assembly. Il registro ebp è un puntatore ai dati dello stack, mentre il registro esp contiene l'indirizzo della cima dello stack utilizzato dal microprocessore.

Di seguito troviamo due istruzioni sub, che riservano rispettivamente 8 e 12 byte sullo stack, intervallate da una istruzione di mov, che azzera l'accumulatore eax, cioè il valore restituito dal main. Infine, prima delle istruzioni leave e ret che, lasciando nelle giuste condizioni lo stack, concludono la funzione, troviamo il fulcro del codice Assembly.

Con l'istruzione pushl $.LCO inseriamo quanto definito in testa al programma come argomento dell'istruzione successiva, la ben nota printf(). Una volta eseguita la printf(), infine, vengono rimossi gli argomenti con l'operazione di add. L'istruzione add, opposta alla precedente sub, pulisce lo stack perché le locazioni di memoria con indirizzo lineare minore contengono gli indirizzi aggiunti più di recente.

Dopo aver visto il classico programma "HelloWorld!", andiamo a cimentarci con un altro semplice esercizio: vediamo come sia possibile in Assembly calcolare il fattoriale di un numero intero senza segno a 32 bit dato in input ad una funzione. Anche il risultato sarà un numero intero senza segno.

Una funzione Assembly che porta a termine quanto detto è: .globl fattoriale fattoriale:

pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%ecx
movl $1,%eax jecxz
.Lexit
.Lloop:
mull %ecx jc
.Loverflow loop
.Lloop
.Lexit:
leave
ret
.Loverflow:
movl $0,%eax
jmp
.Lexit

Successivamente alla classica funzione di enter, la mov inserisce nell'operando ecx il contenuto del primo valore di input consegnato alla funzione, che nel nostro caso è il numero intero. Allo stesso modo, con l'istruzione di mov, viene assegnata una costante − preceduta dal simbolo "$" − al registro eax. L'istruzione di salto jecxz termina la funzione nel caso in cui il registro ecx sia nullo.

Arriviamo quindi al cuore della funzione, il ciclo .Lloop: la prima istruzione moltiplica il registro ecx per il contenuto del registro eax memorizzando in quest'ultimo il risultato dell'operazione. La successiva operazione è un controllo sulla presenza di overflow nella precedente operazione, ovvero se la moltiplicazione binaria ha prodotto un riporto viene impostato un apposito flag (carry flag) a 1. In caso positivo la funzione esce restituendo il valore di ritorno 0. Altrimenti il ciclo viene ripetuto fino a quando il fattoriale non viene completamente calcolato. Tale comportamento viene realizzato mediante l'istruzione loop, che decrementa il registro ecx ed esegue il salto se è ancora diverso da 0.

In questo esempio abbiamo visto come le istruzioni di salto possano essere utilizzate per eseguire cicli o strutture di scelta all'interno dei programmi Assembly. Per inserire una piccola nota di dettaglio diciamo che il numero di istruzioni di salto, condizionato e non, è molto elevato. I codici Assembly iniziano sempre con la lettera "j" e sono seguiti da una serie di lettere (solitamente una o due), che ricordano la condizione da verificare per realizzare il salto.

Nella parte precedente abbiamo visto un esempio di ciclo, mentre nel presente articolo andremo ad analizzare un'altra struttura essenziale per la soluzione di un problema e la stesura di un algoritmo: il costrutto if else.

Per semplificare le cose il nostro programma dovrà solamente verificare una condizione tra due numeri interi e di conseguenza chiamare una prima funzione (funzionea) o una seconda (funzioneb). In altre parole eseguire il frammento di codice C:

if (i>j) funzionea(i); else funzioneb(j);

Ad esempio possiamo scrivere:

movl i,%eax
movl j,%ebx
cmpl %ebx,%eax
jg 1f
pushl %ebx
call funzioneb
jmp 2f
1: pushl %eax
call funzionea
2: addl $4,%esp

Il codice si riferisce al solo segmento di istruzioni che implementano il costrutto if else. Per iniziare, viene assegnato il valore i al registro eax e j al registro ebx. Quindi, attraverso l'istruzione cmp, viene eseguito il confronto tra i valori dei due registri e vengono impostati gli opportuni flag, che verranno utilizzati per l'esecuzione delle successive istruzioni di salto. Per l'esattezza, l'istruzione cmp esegue la sottrazione tra eax e ebx — si noti che tale risultato non viene memorizzato in alcun registro — e imposta sia il Carry Flag che l'Overflow Flag che vedremo in seguito e che sono utilizzati rispettivamente in caso di numeri con o senza segno.

La successiva istruzione è quella di salto condizionato, che avviene se il valore del registro eax è maggiore di quello in ebx, ovvero quando nella precedente istruzione siano stati impostati i flag nel seguente modo:

ZF=0
SF=OF

dove ZF è lo Zero Flag che vale 1 se il risultato dell’ultima operazione è stato 0 (tutti i bit nulli); SF è il Sign Flag che corrisponde al segno, vale 1 se negativo, 0 se positivo; OF è l'Overflow Flag. Relativamente al codice C, questo corrisponde ad aver verificato la condizione(i>j) e a procedere quindi con la chiamata alla funzione funzionea. In Assembly, essa coincide con il saltare all'etichetta 1, dove viene inserito nello stack il valore del registro eax e viene chiamata quindi la funzione (istruzione call).

Qualora, invece, la condizione non fosse verificata si procede in modo del tutto analogo con il registro ebx e la chiamata alla funzione funzioneb. Infine, viene effettuata l'istruzione di add (e non pop) per far puntare esp allo stesso valore a cui puntava prima del segmento di codice analizzato. Questo semplice codice ci dà la possibilità di introdurre il concetto di simbolo temporaneo, ovvero simboli il cui nome è un numero compreso tra 0 e 9. La lettera "f" indica che il salto è in avanti verso la più vicina etichetta (forward), mentre l'utilizzo della lettera "b" indica che il salto è all’indietro (backward).

Come ultima doverosa nota possiamo evidenziare che è possibile ottimizzare al meglio questo frammento di codice (modificandolo) in base alla configurazione dei registri e alla probabilità con cui viene eseguito if o else.

Dopo aver analizzato un esempio di costrutto if-else cambiamo completamente registro e parliamo di un argomento leggermente più avanzato, i vettori. Come già detto, in questi quattro articoli abbiamo solamente voluto mostrare alcuni semplici esempi di programmazione Assembly, da utilizzare come spunto per lo studio o come prima verifica.

Il vettore è una delle strutture di dati ad alto livello più utilizzate. In Assembly i vettori in memoria seguono una regola molto semplice. Se gli elementi del vettore sono di dimensione "d" allora, dopo il primo elemento "e" con indice "0", l'elemento con indice "1" è memorizzato all'indirizzo “e+d”. Analogamente gli altri elementi i-esimi saranno memorizzati all'indirizzo "e+d * i".

Vediamo quindi come sommare tutti gli elementi di un vettore all'interno di un programma contenente codice Assembly. Un possibile codice è il seguente:

#include
int dati[10];
int ris;
int main(void)
{
int i;
for (i=0;i < 10;i++)
dati=i+1;
asm("pushl %eax"
"pushl %ebx"
"pushl %ecx"
"lea dati,%eax"
"movl $0,%ecx"
"movl $0,%ebx"
"1:"
"addl (%eax),%ebx"
"addl $4,%eax"
"incl %ecx"
"cmp $10,%ecx"
"jl 1b"
"movl %ebx, ris"
"popl %eax"
"popl %ebx"
"popl %ecx");
printf("risultato= %dn",ris);
return 0;
}

Come possiamo vedere, in questo esempio viene utilizzata la funzione asm che ci permette di scrivere codice Assembly direttamente dentro al nostro file C. Alla fine di ogni istruzione nella funzione asm andrebbero inseriti i caratteri di tabulazione e di nuova linea (omessi per motivi tecnici).

Dopo l'inizializzazione dell'array di dati, che per semplicità abbiamo considerato di lunghezza 10 e riempito con i primi dieci numeri naturali, avviene l'inserimento dei registri ebx ed ecx nello stack. L'istruzione lea, analogamente alla mov vista nei precedenti esempi, inserisce in eax l'indirizzo del nostro array di dati.

All'interno dell'etichetta "1" c'è il cuore del nostro semplice programma, che somma il valore puntato da eax a ebx e mette il risultato nel secondo, quindi ebx; sposta di una casella nell'array eax; incrementa ecx; fa il compare tra ecx e 10 per vedere se l'array è finito altrimenti ripete i passi precedenti.