64bitSiamo abituati a compilare tramite il comando:
gcc nomeEseguibile -o fileDaCompilare.cpp
Questo comando esegue compilazione e linking. Per poter eseguire compilazione e linking in due momenti diversi si possono utilizzare i seguenti comandi:
# Compilazione (valida anche per file .s)
gcc -c main.c -o main.o
# Linking
gcc file.o -o eseguibile
Per poter visualizzare il contenuto del file .o generato dalla compilazione si può utilizzare il seguente comando:
objdump -d fileOggetto.o
Quando si compila un file .c o .cpp prima di tradurre il codice dal main e dalle varie funzioni vanno inseriti due pezzi di codice chiamati prologo ed epilogo.
Entrambe le sezioni sono indispensabili affinché il processore possa eseguire il programma in maniera corretta.
Concretamente si occupano infatti di inizializzare/gestire correttamente lo spazio dedicato ad una funzione nella pila tramite i registri %rbp e %rsp.
Quando un programma viene eseguito il registro %rsp punta alla prima locazione di memoria dedicata, mentre %rbp punta al primo indirizzo fuori dalla locazione di memoria.
Il prologo si occupa di allocare la memoria necessaria per salvare tutte le variabili di una determinata funzione.
Per fare ciò la struttura è la seguente:
; Prologo
PUSHq %rbp ; Salvo la vecchia ultima locazione, mi servirà per tornarci nell'epilogo
MOVq %rsp, %rbp ; La vecchia prima locazione è la nuova ultima locazione
SUBq $spazioNecessario, %rsp ; La nuova prima locazione si trova X byte indietro rispetto a quella attuale
Dove per $spazioNecessario si intende uno spazio multiplo di 8Byte.
L’epilogo invece fa la stessa cosa ma in ordine inverso. I passaggi sono i seguenti:
; Epilogo
MOVq %rbp, %rsi
POPq %rbp
; --- Sono condensati nel seguente comando --- ;
LEAVE
64bitNell’assembelr a 64bit esistono delle regole sui registri “sporcabili” dai programmmi che il compilatore segue.
| Registro | Descrizione | Stato di utilizzo |
|---|---|---|
%rax |
Usato per i valori di ritorno | Utilizzabile |
%rbx |
Generalmente salvato dal chiamato | Non tipicamente utilizzabile |
%rcx |
Comunemente usato per i contatori | Utilizzabile |
%rdx |
Spesso usato per operazioni I/O | Utilizzabile |
%rsi |
Tipicamente salvato dal chiamato | Non tipicamente utilizzabile |
%rdi |
Spesso usato per operazioni su stringhe | Non tipicamente utilizzabile |
%rbp |
Puntatore base per il frame dello stack | Non utilizzabile |
%rsp |
Puntatore dello stack | Non utilizzabile |
%r8 |
Registro utilizzabile | Utilizzabile |
%r9 |
Registro utilizzabile | Utilizzabile |
%r10 |
Registro utilizzabile | Utilizzabile |
%r11 |
Registro utilizzabile | Utilizzabile |
%r12 |
Tipicamente salvato dal chiamato | Non tipicamente utilizzabile |
%r13 |
Tipicamente salvato dal chiamato | Non tipicamente utilizzabile |
%r14 |
Tipicamente salvato dal chiamato | Non tipicamente utilizzabile |
%r15 |
Tipicamente salvato dal chiamato | Non tipicamente utilizzabile |
In C, se volessi utilizzare una funzione scritta in assembly nel mio file .c, è sufficente che i file rispettino questo standard:
extern int funzione();
.global funzione
funzione:
NOP
...
RET
Infatti in C le funzioni sono definite univocamente dal loro nome.
In C++ la cosa è diversa, infatti esiste l’overloading delle funzioni, che non sono più univocamente definite dal nome, ma anche dal tipo/ordine/numero di parametri
In C++ si utilizza quindi questa sintassi
extern int funzione();
.global _ZXnome
_ZXnome:
NOP
...
RET
Infatti si utilizza un comando del seguente tipo:
_Zn(nome)(tipi input)
n indica il numero di caratteri dal quale è formato il nome
Per tipi di input si intendono:
i $\to$ intc $\to$ charP(elemento) $\to$ puntatore a elementoR(elemento) $\to$ riferimentoE puntatore thisC1 indica il costruttoreNel caso di metodi annidati, invece di _Zn(nomeFunzione) si utilizza _ZN(nomeClasse)n(nomeFunzione):
class cl{
cl(int);
// ...
void elab1();
}
; cl::elab1
.global _ZN2cl5elab1E
; cl::cl
.global _ZN2clC1i
Per verificare la sintassi di un nome è possibile utilizzare il comando c++filt _Z....
Nel caso in cui si volesse passare come parametro due volte lo stesso tipo definito dall’utente, si utilizza S(n)_.
Questo comando ripete il tipo defiito dall’utente.
n indica quale dei vari tipi già dichiarati va ripetuto (si omette per il primo, 0 per il secondo, 1 peer il terzo, …):
c++filt _Z5somma4caso5puntoS_
c++filt _Z5somma4caso5puntoS0_
c++filt _Z5somma4casoP5puntoS0_
somma(caso, punto caso)
somma(caso, punto, punto)
Passaggio Struttura
struct st{
int x;
int y;
}
extern int somma_struttura(st struttura);
Il file assembly diventa così:
.global _Z15somma_struttura2st
;
; args:
; - st struttura: %rdi
; return:
; %rax
; |000000000000000|
; +---------------+ <- %rsp
; | a | b |
; +---------------+ <- %rbp (base pointer)
; |fffffffffffffff|
;
.set struttura, -8
.set freccia_a, 0
.set freccia_b, 4
_Z15somma_struttura2st:
; Prologo
PUSH %RPB
MOVq %RSP, %RBP
SUB $8, %RSP
; Corpo
MOVq %rdi, struttura(%rbp)
MOVl struttura+freccia_a(%rbp), %eax
addl struttura+freccia_b(%rbp), %eax
; Epilogo
LEAVE
RET
Passaggio Struttura per puntatore
struct st{
int x;
int y;
}
extern int somma_struttura(st* struttura);
Il file assembly diventa così:
.global _Z18somma_struttura_ptP2st
;
; args:
; - st* struttura: %rdi
; return:
; struttura->a + struttura->b
; %rax
; |000000000000000|
; +---------------+ <- %rsp +---------------+
; | st* struttura | ----------------------------------> | a | b |
; +---------------+ <- %rbp (base pointer) +---------------+
; |fffffffffffffff|
;
.set struttura_pt, -8
.set freccia_a, 0
.set freccia_b, 4
_Z15somma_struttura2st:
; Prologo
PUSH %RPB
MOVq %RSP, %RBP
SUB $8, %RSP
; Corpo
MOVq %rdi, struttura_pt(%rbp)
MOVq struttura_pt(%rbp), %rsi
MOVl freccia_a(%rsi), %eax
ADDl freccia_b(%rsi), %eax
; Epilogo
LEAVE
RET
È una macchina virtuale che emula un sistema basato su Intelx86 a 64bit senza bootloader.
L’unico programma che è conservato in memoria della macchina dobbiamo caricarlo noi.
Per fare ciò ci posizioniamo nella cartella contenente i file che si desidera caricare.
Possono essere file .cpp, .c e .s.
Viene poi eseguito il comando compile.
Dopo aver eseguito l’esecuzione compile in maniera corretta, è possibile avviare QEMU tramite il comando boot.
Nel caso in cui volessimo effettuare del debug del codice dobbiamo bootare la macchina con il comando boot -g, e successivamente, da un nuovo terminale, eseguire il comando debug.
La macchina QEMU utilizza un HD virtuale salvato nella cartella CE/share/hd.img.
Nel caso volessimo svuotare l’HD il comando per farlo è il seguente:
truncate -s 20971520 ~/CE/share/hd.img
Per visualizzarne invece il contenuto il comando è il seguente:
hexdump -C ~/CE/share/hd.img | less
Come abbiamo già visto nella parte teorica non possono essere chiamate dall’utente, ma vengono generate quando si verificano certe cose.
Si dividono in tre tipi:
Fault: l’IP punta all’istruzione stessa che genera l’eccezione, e prova a rieseguirla dopo la routineTrap: l’IP punta all’istruzione successiva a quella che genera l’eccezioneAbort