1. Indice

2. Compilare File

Siamo 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

3. Prologo ed Epilogo

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

4. Assembler intelx86 - 64bit

Nell’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

5. Mangling e Differenze C - C++

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:

Nel 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)

5.1. Struttura

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

6. QEMU

È 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.

6.1. Utilizzo HD macchina

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

6.2. Eccezioni

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: