Prima di cominciare a programmare direttamente in asm vi devo spiegare cos'e' lo stack e prima dello stack cos'e' la memoria :).

LA MEMORIA


Quando lanciamo un programma, esso viene allocato nella memoria del nostro pc. Un indirizzo di memoria, nei sistemi operativi che tratteremo (Windows e Linux), e' composto di due parti:

  SEGMENT:OFFSET

Il SEGMENT e' un numero (esadecimale) di 4 cifre (che va da 0 a FFFF) mentre l'OFFSET e' un numero di 8 cifre (che va da 0 a 4 Giga circa (2^32)). Un esempio di indirizzo di memoria e':

  0028:080237d9

Il segmento e' ad uso e consumo del sistema operativo, mentre quando debuggiamo un programma la parte che ci interessa e' l'OFFSET. Ad esempio guardiamo questa parte di programma:
 0x80483e7 :     jmp    0x80483f0 
 0x80483e9 :     jmp    0x80483f7 
 0x80483eb :    nop
 0x80483ec :    lea    0x0(%esi,1),%esi
 0x80483f0 :    call   0x80482ec 
 0x80483f5 :    jmp    0x80483e7 
 
Le cifre a sinistra indicano gli OFFSET in cui sono stati allocati gli OPCODE che compongono il nostro programma.
Ci tornano utili quando dobbiamo vedere dove arriva un j* o per fare altri giochetti moolto divertenti...

LO STACK

Per quanto riguarda lo stack, esso e' una parte particolare della memoria, che contiene dei valori in generale temporanei. Lo stack viene gestito come una struttura LIFO (Last In First Out), detta in gergo pila. Le istruzioni per la gestione dello stack sono:

  PUSH (val/reg) : inserisce un valore nello stack
  POP (reg) : prende un valore dallo stack

Un esempio di utilizzo dello stack puo' essere questo:
 push 8
 pop eax
 
Con questo codice eax contiene il valore 8. Un altro esempio puo' essere questo:

 push eax
 push ebx
 mov  eax,5
 mov  ebx,9
 add  eax,ebx
 mov  ecx,eax
 pop  ebx
 pop  eax
 
Questo breve snippet non fa altro che sommare i valori 5 e 9 e mettere il risultato in ecx. I valori di eax ed ebx sono stati salvati nello stack (le due istruzioni PUSH) prima di sostituirli con 5 e 9 rispettivamente. Alla fine della somma eax ed ebx tornano al loro stato primordiale (i due POP). E' facile qui notare la struttura LIFO dello stack.
STACK INTERNALS
Quando prima scrivevo che lo stack e' un'area di memoria `particolare' mi riferivo al fatto che il sistema operativo riserva allo stack un segmento puntato dal registro SS (Stack Segment). Al contrario di quanto intuitivamente si possa pensare, ogni volta che inseriamo un valore nello stack ( e quindi quando PUSHiamo un valore) esso viene decrementato, e quindi, quando ne POPpiamo uno, esso viene incrementato.
Gli altri due registri coivolti nelle operazioni dello stack sono ESP e EBP.
  ESP : e' lo Stack Pointer, che punta al TOS (Top Of Stack).
  EBP : e' il Base Pointer, che punta alla base dello stack-frame per la porzione di codice corrente.

Ma al di la' delle definizioni vediamo come funzionano un po' piu' da vicino. Il registro ESP varia per ogni PUSH e POP che eseguiamo in questo modo:
 
 (a)                              (b)
 
 .               .             |                 .               .
 |               |             |                 |               |
 +---------------+             |                 +---------------+
 |      15       |             |                 |      15       |
 +---------------+             |                 +---------------+
 |       1       |             |   PUSH 10       |       1       |
 +---------------+             |                 +---------------+
 |     800       |  <-- ESP    |                 |     800       |
 +---------------+             |                 +---------------+
 |               |             |                 |      10       | <- ESP
 +---------------+             |                 +---------------+
 |               |             |                 |               |
 +---------------+             |                 +---------------+
 |               |	       |		 | 		 |
 .		 . 	       |		 . 		 .
 
Possiamo leggere la figura sopra in questo modo: nella parte (a) abbiamo una raffigurazione dello stack con dei valori contenuti nelle diverse celle. ESP punta all'ultima cella allocata. Viene quindi eseguita l'istruzione PUSH 10, ESP `avanza' nello stack, decrementando il suo valore. Di quanto viene incrementato o decrementato ESP? Trattandosi di un registro a 32 bit, il registro varia di 4 byte la volta. Anche se nel codice asm scriviamo:
 push byte 1
 
il registro esp sara' decrementato di 4 byte.
Come utilizzare questi due registri in maniera efficace?
Se provate a disassemblare un qualsiasi programma noterete il ripetersi continuo di questo pattern (chiamato anche prologo):
 push ebp
 mov  ebp,esp
 
Se volessi dare una definizione 'scientifica' a questo breve snippet direi che esso serve a selezionare un nuovo frame nello stack per il codice che lo segue. Questa definizione e' tanto rigorosa quanto incomprensibile :). Analizziamo velocemente il codice:
 push ebp
 
con questa push salviamo il contenuto dello stack base pointer nello stack stesso (poi vedremo meglio perche'), l'istruzione successiva:
 mov  ebp,esp
 
sovrascrive l'ebp con l'esp.
Cerchiamo di capire la necessita' di tutto cio'. Una call ad una procedura ha in generale questa sintassi:
 push param2
 push param1
 call procedura
 add  esp,8
 
L'opcode CALL e' equivalente al codice:
 push EIP
 jmp procedura
 
quindi quando eseguiamo una call salviamo sullo stack anche l'EIP corrente. Subito dopo l'esecuzione della CALL lo stato dello stack e' il seguente:
 +--------+
 | param2 |
 +--------+
 | param1 |
 +--------+
 |   eip  | <-- ESP
 +--------+
 
dopo l'esecuzione del prologo lo stack diventa cosi':
 +--------+
 | param2 |
 +--------+
 | param1 |
 +--------+
 |  eip   |
 +--------+
 |  ebp   |  <-- ESP = EBP
 +--------+
 
In questo modo i parametri passati si trovano ad ebp-12 (param2) ed ebp-8 (param1), e posso cosi' facilmente referenziarli facilmente, facendo con lo stack quanto mi pare visto che l'unico registro a modificarsi e' l'ESP.
Quando chiudiamo una procedura dobbiamo riportare l'ESP a puntare la cella contenente l'EIP poiche' il ret e' interpretabile come:
 pop eax
 jmp eax
 
(in realta' l'aver usato eax non e' corretto, ma e' giusto codice per dare un'idea). Quindi se eseguiamo un ret quando il TOS (Top Of the Stack) non e' l'EIP con buona probabilita' il nostro programma andra' in segmentation fault... . La controparte del prologo e' questa:
 mov  esp,ebp
 pop  ebp
 
che dovremo scrivere prima del ret. In realta' non dovete troppo preoccuparvi di capire a fondo questo concetto, visto che quando analizzerete il codice di programmi compilati, potete esser ben sicuri che il compilatore ha fatto i calcoli giusti ;).



Programmare in ASM


L'assemblatore che useremo in questo corso e' il NASM. Sicuramente il NASM non e' l'unico strumento presente in linux in questo ambito, ma l'ho preferito per una serie di motivi, quali la sintassi INTEL per l'assembler, il suo carattere OPEN-SOURCE, il suo essere slegato da ogni tipo di compilatore di alto livello (come gas), e per la sua completezza nella documentazione.

La dichiarazione delle variabili
In un programma in asm le risorse (ad esempio le stringhe) sono relegate nella sezione .data in questa maniera:
 section .data
 
Bisogna far attenzione al fatto che in questa sezione ci sono i dati inizializzati, visto che i dati non inizializzati (le variabili) vanno nella sezione .bss .
E il codice? Il codice sara' racchiuso nella sezione .text .
Vediamo subito un esempio di programma per vedere come utilizzare quanto appreso e altro :).
 global main
 extern printf

 section .data
 msg     db      "Helloooooo, nurse!",0Dh,0Ah,0
 section .text
 main:
         push dword msg
         call printf
         pop eax
         ret
Analizziamo subito il programma. La keyword global serve a specificare i simboli globali esportabili (ad esempio le funzioni nelle librerie). Per quanto riguarda il main, serve al programmatore per capire dove effettivamente comincia il proprio programma. Con la keyword extern invece diciamo al sistema operativo di quali funzioni abbiamo bisogno per l'esecuzione del codice (senza quindi preoccuparci di importare le librerie come succede in C). In section .data possiamo notare la presenza di una variabile, approfondiamo la sintassi di dichiarazione:
msg     db      "Helloooooo, nurse!",0Dh,0Ah,0
dove msg indica il nome della variabile, db indica la dimensione della variabile (secondo la tabella che segue), ed infine abbiamo il contenuto della variabile che in questo caso e' una stringa.
Per la dichiarazione delle 'risorse' con il NASM ci si puo' attenere a questa tabella:
                 db 0x55                ; just the byte 0x55
                 db 0x55,0x56,0x57      ; three bytes in succession
                 db 'a',0x55            ; character constants are OK
                 db 'hello',13,10,'$'   ; so are string constants
                 dw 0x1234              ; 0x34 0x12
                 dw 'a'                 ; 0x41 0x00 (it's just a number)
                 dw 'ab'                ; 0x41 0x42 (character constant)
                 dw 'abc'               ; 0x41 0x42 0x43 0x00 (string)
                 dd 0x12345678          ; 0x78 0x56 0x34 0x12
                 dd 1.234567e20         ; floating-point constant
                 dq 1.234567e20         ; double-precision float
                 dt 1.234567e20         ; extended-precision float

Per i dati non inizializzati invece le keyword da utilizzare sono:
       buffer:   resb 64                ; reserve 64 bytes
       wordvar:  resw 1                 ; reserve a word
       realarray: resq 10               ; array of ten reals
In questo modo possiamo allocare spazio da utilizzare piu' avanti nel programma. In ultimo sulle variabili si puo' utilizzare questa struttura:
       message   db 'hello, world'
       msglen    equ $-message
dove si puo' notare l'utilizzo dell'operatore $ e di equ (msglen contiene la lunghezza di message), equ serve a specificare le costanti.
Approfondiamo il $
Il $ restituisce la posizione corrente (offset) nel programma. Ad esempio:
 JMP $
causera' un loop infinito. Con $$ invece viene indicato l'inizio della sezione, in modo che ad esempio per sapere a che distanza siamo dall'inizio di .text possiamo usare $-$$ (cioe' EIP corrente - Inizio della sezione).
Piu' che un corso di programmazione in asm definirei quanto scritto finora un crash-course nella programmazione. Chiunque voglia approfondire la programmazione puo' farlo leggendo la documentazione del NASM (a cui mi sono ispirato) o l'introduzione all'assembly language (soprattutto in ambito linux) su linuxassembly.org.
Uno script che ho trovato sull'Assembly Programming Journal puo' essere molto utile per la compilazione di file scritti in asm:
#!/bin/sh
# assemble.sh =========================================================
outfile=${1%%.*}
tempfile=asmtemp.o
nasm -o $tempfile -f elf $1
gcc $tempfile -o $outfile
rm $tempfile -f
#EOF ==================================================================


Calling Convetions
Il passaggio dei parametri alle funzioni nel corpo di un programma compilato puo' essere di vario tipo (visto che in assembler non ci sono regole ben predefinite), pero' nell'ambito della programmazione si e' pensato che fosse piu' facile per i programmatori avere una regola generale per il passaggio dei parametri (per rendere il codice portabile e soprattutto per condividere le librerie). Queste convenzioni vanno sotto il nome di Calling Conventions e se ne distinguono di quattro tipi:
La 'C calling convention' e' utilizzata nella quasi maggior parte dei casi, e funziona in questo modo:
        ;procedure(param1, param2)
        push dword param2
        push dword param1
        call procedure
        add  esp, 8
Quindi i parametri sono passati in ordine inverso rispetto alla dichiarazione della procedura. Da notare l'add finale che ha scopo di riallineare lo stack.
Nella 'Pascal calling convention' la differenza rispetto alla precedente si nota nel passaggio dei parametri:
        ;procedure(param1, param2)
        push dword param1
        push dword param2
        call procedure
Questo tipo di convenzione e' utilizzato nei programmi scritti in BASIC, Fortran e nelle API di windows a 16 bit. In questo caso lo stack viene riallineato da parte della procedura tipicamente con un'istruzione di tipo ret n dove n e' solitamente un multiplo di 4.
La 'StdCall' invece risulta una mixture delle due calling conventions precedenti: i parametri sono passati in ordine inverso a quello della dichiarazione (come quella in C), ma la procedura si occupa di riallineare lo stack (come quella in Pascal). Questo tipo di sintassi e' utilizzata nelle WinAPI a 32 bit. Un esempio puo' essere il seguente:
        ;procedure(param1, param2)
        push dword param2
        push dword param1
        call procedure
Ultima delle quattro convenzioni e' la 'Register calling convention'. Il codice seguente e' autoesemplificativo per il suo utilizzo:
        mov  eax, param1
        mov  edx, param2
        mov  ebx, param3
        call procedure
in cui l'ordine dei registri da usare e' obbligatorio e bisogna tenere in mente che i parametri dopo il terzo sono passati via stack. Lo stack e' nella maggior parte dei casi gestito come nella calling convention in Pascal, ma questa non e' una regola fissa.



Le system call in linux

Se provaste a visitare l'albero dei sorgenti di linux e per caso vi imbatteste nella directory /include/asm/, vi consiglio di dare uno sguardo al file unistd.h . In questo file e' contenuta una lista di funzioni affiancate da un valore numerico: si tratta delle funzioni di sistema piu' `intime' di linux e vediamo ora come invocarle. La regola per poter invocare queste funzioni e':
mov   eax,num   < - num e' il numero cha affianca la syscall in unistd.h
int 80h         < - questa istruzione invoca l'interrupt 0x80
Cos'e' un interrupt? Un interrupt e' un `dispositivo' attraverso cui e' possibile segnalare al processore lo svolgimento di una particolare attivita'. Nella storia del DOS gli interrupt erano utilizzatissimi ad esempio dai virus per potersi replicare nei file eseguiti o per fare scherzetti di altro genere. Nei sistemi operativi a 32bit non sarebbe in teoria possibile invocare direttamente gli interrupt, ma l'INT 80h fa eccezione. Le funzioni che potreste invocare attraverso l'INT 80h sono molteplici, e spaziano dal kill al settaggio della schedulazione dei processi, dalla mappatura della memoria all'esecuzione di una fork. Ad esempio per predere il pid del processo in esecuzione si potrebbe con poche istruzioni:
        mov eax, dword 20     ; getpid() syscall
        int 80h               ; syscall INT
ed eax conterra' il valore del pid (restituito dalla call).
Una syscall moooooo[ox1000]lto particolare e' quella rispondente al numero 54: ioctl. Ioctl e' uno strumento che ci permette di fare programmi che ad esempio possono eludere la possibilita' ad un utente di switchare tra le console e molto altro. Ma cio' non e' argomento di questo corso e rimando alla lettura dei sorgenti del kernel.