📚 Guida Rapida: da C a Assembly RISC-V (64-bit)

Tradurre da C ad Assembly significa "smontare" la logica di alto livello (come if, while, +) in istruzioni meccaniche che la CPU può eseguire.

La tua risorsa più importante è la Convenzione ABI (Application Binary Interface) di RISC-V, che definisce come vengono usati i registri.

I Registri Fondamentali (ABI)

Registri Alias Utilizzo Principale Chi lo salva?
x0 zero Valore costante 0 -
x1 ra Return Address (Indirizzo di ritorno) Chiamante
x2 sp Stack Pointer (Puntatore allo Stack) Chiamato
x8 s0/fp Saved Register 0 / Frame Pointer Chiamato
x9 s1 Saved Register 1 Chiamato
x10-x11 a0-a1 Argomenti 0-1 / Valore di ritorno Chiamante
x12-x17 a2-a7 Argomenti 2-7 Chiamante
"x5-x7, x28-x31" t0-t6 Temporary (Registri temporanei) Chiamante
x18-x27 s2-s11 Saved Registers 2-11 Chiamato

Registri in Virgola Mobile (ABI - Estensione 'D')

Per float (32-bit) e double (64-bit) si usano registri separati.

Registri Alias Utilizzo Principale Chi lo salva?
f0-f7 ft0-ft7 Temporary (Virgola Mobile) Chiamante
f8-f9 fs0-fs1 Saved (Virgola Mobile) Chiamato
f10-f11 fa0-fa1 Argomenti / Valori di ritorno (FP) Chiamante
f12-f17 fa2-fa7 Argomenti (FP) Chiamante
f18-f27 fs2-fs11 Saved (Virgola Mobile) Chiamato
f28-f31 ft8-ft11 Temporary (Virgola Mobile) Chiamante

🛠️ Comandi Principali (Cheat Sheet)

Ecco le istruzioni più comuni che userai.

1. Aritmetica e Logica

Nota importante: RV64 ha istruzioni standard (64-bit) e istruzioni "Word" (32-bit, con suffisso w). Poiché il C usa spesso int (32-bit), userai molto le versioni w.
Istruzione Esempio Significato
add rd, rs1, rs2 add s0, s1, s2 s0 = s1 + s2 (64-bit)
addw rd, rs1, rs2 addw s0, s1, s2 s0 = s1 + s2 (32-bit)
subw rd, rs1, rs2 subw s0, s1, s2 s0 = s1 - s2 (32-bit)
addi rd, rs1, imm addi s0, s1, 5 s0 = s1 + 5 (Immediato)
addiw rd, rs1, imm addiw s0, s1, 5 s0 = s1 + 5 (32-bit)
slli rd, rs1, shamt slli t0, s0, 2 t0 = s0 << 2 (Shift logico a sinistra)
andi rd, rs1, imm andi s0, s0, 0xFF s0 = s0 & 255 (AND bit a bit)
li rd, imm li s0, 10 s0 = 10 (Pseudo-istruzione: Load Immediate)
mv rd, rs mv a0, s0 a0 = s0 (Pseudo-istruzione: addi a0, s0, 0)

2. Memoria (Load/Store)

Si usa l'indirizzamento offset(base). 8(sp) significa "l'indirizzo 8 byte sopra quello puntato da sp".

Istruzione Esempio Significato
ld rd, offset(rs1) ld s0, 8(sp) Load Doubleword (Carica 64-bit da stack)
sd rs2, offset(rs1) sd s0, 8(sp) Store Doubleword (Salva 64-bit su stack)
lw rd, offset(rs1) lw s0, 0(a0) Load Word (Carica 32-bit da a0)
sw rs2, offset(rs1) sw s0, 4(a0) Store Word (Salva 32-bit in a0 + 4)
lb rd, offset(rs1) lb t0, 0(a0) Load Byte (Carica 8-bit)
sb rs2, offset(rs1) sb t0, 0(a0) Store Byte (Salva 8-bit)
la rd, label la a0, MY_ARRAY Load Address (Pseudo-istruzione)

3. Controllo di Flusso (Salti)

Istruzione Esempio Significato
j label j .L_LOOP Jump (Salta incondizionatamente)
beq rs1, rs2, label beq s0, s1, .L_END Branch if EQual (Salta se s0 == s1)
bne rs1, rs2, label bne s0, a1, .L_LOOP Branch if Not Equal (Salta se s0 != a1)
blt rs1, rs2, label blt s0, a1, .L_LOOP Branch if Less Than (con segno)
bge rs1, rs2, label bge s0, a1, .L_END Branch if Greater or Equal (con segno)
jal label jal sum_array Jump and Link (Chiama funzione, salva ra)
ret ret Return (Pseudo-istruzione: jalr zero, 0(ra))

4. Virgola Mobile (Estensione 'D')

Usano i registri f. Il suffisso .s è per float (Single-precision, 32-bit), .d per double (Double-precision, 64-bit).

Istruzione Esempio Significato
fld rd, offset(rs1) fld fa0, 0(a0) Load Double (Carica double da a0)
fsd rs2, offset(rs1) fsd fa0, 8(sp) Store Double (Salva double su stack)
flw rd, offset(rs1) flw fa0, 0(a0) Load Word (Carica float da a0)
fsw rs2, offset(rs1) fsw fa0, 4(sp) Store Word (Salva float su stack)
fadd.d rd, rs1, rs2 fadd.d fa0, fa0, fa1 fa0 = fa0 + fa1 (Somma di double)
fsub.s rd, rs1, rs2 fsub.s fa0, fa0, fa1 fa0 = fa0 - fa1 (Sottrazione di float)
fmul.d rd, rs1, rs2 fmul.d fa0, fa0, fa1 fa0 = fa0 * fa1 (Prodotto di double)
fdiv.s rd, rs1, rs2 fdiv.s fa0, fa0, fa1 fa0 = fa0 / fa1 (Divisione di float)
fcvt.w.d rd, rs1 fcvt.w.d s0, fa0 Converte double (fa0) in int (s0)
fcvt.d.w rd, rs1 fcvt.d.w fa0, s0 Converte int (s0) in double (fa0)

🔁 Esempi di Traduzione C -> Assembly

1. Aritmetica Semplice

// C
int a = 10;
int b = 20;
int c = a + b;

# Assembly (ipotizzando che a,b,c siano in s0,s1,s2)
    li   s0, 10       # a = 10
    li   s1, 20       # b = 20
    addw s2, s0, s1   # c = a + b (usiamo addw per 'int' a 32-bit)

2. Costrutto if

La logica è "al contrario": si salta se la condizione non è vera.

// C
if (i < 10) {
    // corpo 'then'
    a = a + 1;
}
// 'else' (o continuazione)
b = 0;

# Assembly
# i in s0, a in s1, b in s2
# 10 è in un temporaneo t0
    li t0, 10        # Controlla la condizione (i < 10)
    # Salta alla fine (.L_END) se la condizione è FALSA (i >= 10)
    bge s0, t0, .L_END
    
    # Corpo 'then' (eseguito solo se il salto non avviene)
    addiw s1, s1, 1   # a = a + 1
.L_END:
    # Continuazione
    li s2, 0          # b = 0

3. Ciclo for (come sum_array)

Questo è il modello più importante da imparare.

// C
int sum = 0;
for (int i = 0; i < size; i++) {
    sum += arr[i];
}

# Assembly
# sum in s0, i in s1
# size in a1 (argomento)
# &arr[0] in a0 (argomento)

    li s0, 0          # sum = 0
    li s1, 0          # i = 0
.L_LOOP_START:
    # 1. Condizione: if (i >= size) salta alla fine
    bge s1, a1, .L_LOOP_END
    
    # 2. Calcola indirizzo: &arr[i]
    #    Dato che 'int' è 4 byte, l'offset è i * 4 
    slli t0, s1, 2     # t0 = i * 4
    add  t1, a0, t0    # t1 = indirizzo_base_arr + offset = &arr[i]
    
    # 3. Carica valore: arr[i]
    lw t2, 0(t1)     # t2 = valore all'indirizzo t1 (lw per 'int')
    
    # 4. Esegui corpo: sum += arr[i]
    addw s0, s0, t2    # sum = sum + valore (addw per 'int')
    
    # 5. Incremento: i++  
    addi s1, s1, 1
    
    # 6. Ripeti
    j .L_LOOP_START
.L_LOOP_END:
    # Finito. 'sum' (in s0) è pronto.
    # (Metterlo in a0 prima di 'ret' per restituirlo)
    mv a0, s0
    ret

4. Ciclo while

Molto simile al for, ma l'incremento è all'interno del corpo.

// C
int i = 0;
int sum = 0;
while (i < size) {
    sum += arr[i];
    i++;
}

# Assembly
# sum in s0, i in s1
# size in a1 (argomento)
# &arr[0] in a0 (argomento)
    li s0, 0          # sum = 0
    li s1, 0          # i = 0
.L_WHILE_CHECK:
    # 1. Condizione: if (i >= size) salta alla fine
    bge s1, a1, .L_WHILE_END
    
    # --- Corpo del loop ---
    # 2. Calcola indirizzo: &arr[i]
    slli t0, s1, 2     # t0 = i * 4
    add  t1, a0, t0    # t1 = &arr[i]
    
    # 3. Carica valore: arr[i]
    lw t2, 0(t1)     # t2 = arr[i]
    
    # 4. Esegui corpo: sum += arr[i]
    addw s0, s0, t2    # sum = sum + t2
    
    # 5. Incremento: i++
    addi s1, s1, 1
    # --- Fine corpo ---
    
    # 6. Salta di nuovo al controllo
    j .L_WHILE_CHECK
.L_WHILE_END:
    # Finito
    mv a0, s0
    ret

5. Ciclo do-while

La caratteristica chiave è che il controllo è alla fine. Il corpo viene eseguito almeno una volta.

// C
int i = 0;
int x = 10;
do {
    x = x - 1;
    i++;
} while (i < 5);

# Assembly
# i in s0, x in s1
    li s0, 0  # i = 0
    li s1, 10 # x = 10
    li t0, 5  # t0 = 5 (per il confronto)
.L_DO_BODY:
    # --- Corpo del loop ---
    # 1. x = x - 1
    addiw s1, s1, -1
    
    # 2. i++
    addiw s0, s0, 1
    # --- Fine corpo ---
    
    # 3. Condizione: if (i < 5) ripeti
    # Salta all'indietro se (s0 < t0)
    blt s0, t0, .L_DO_BODY
# Finito

6. Puntatori

L'aritmetica dei puntatori viene tradotta in aritmetica intera, scalata per la dimensione del tipo.

// C
// (ptr è un 'int*' in a0, val è un 'int' in a1)
void set_value(int *ptr, int val) {
    *ptr = val;         // Scrittura
    ptr++;              // Aritmetica
    *ptr = val + 1;     // Scrittura
}

# Assembly
# ptr in a0, val in a1
    # 1. *ptr = val;
    #    Salva la Word (32-bit) in a1 all'indirizzo in a0
    sw a1, 0(a0)
    
    # 2. ptr++;
    #    Poiché ptr è 'int*', ++ significa "aggiungi 4 byte"
    addi a0, a0, 4
    
    # 3. *ptr = val + 1;
    #    Calcola prima il valore
    addi t0, a1, 1
    #    Poi salva la nuova Word
    sw t0, 0(a0)
    
    ret

7. Funzione Ricorsiva (Fattoriale)

Questo è l'esempio più complesso. Richiede una gestione attenta dello stack per salvare ra (indirizzo di ritorno) e n (l'argomento) prima della chiamata ricorsiva.

// C
int fact(int n) {
    if (n <= 1) {
        return 1; // Caso base
    } else {
        // Passo ricorsivo
        return n * fact(n - 1);
    }
}

# Assembly
# n è in a0 all'ingresso
fact:
    # --- Prologo ---
    # Dobbiamo salvare 'ra' (perché chiamiamo un'altra funzione)
    # e 'n' (perché ci serve DOPO la chiamata ricorsiva).
    # Salveremo n (a0) in s0.
    
    addi sp, sp, -16 # Alloca 16 byte (per 2 registri a 64-bit)
    sd   ra, 8(sp)   # Salva indirizzo di ritorno
    sd   s0, 0(sp)   # Salva s0 (dove metteremo n)
    
    mv s0, a0        # s0 = n (ora n è al sicuro)
    
    # --- Corpo ---
    # 1. Caso Base: if (n <= 1)
    li t0, 1
    ble s0, t0, .L_BASE_CASE # Salta al caso base se n <= 1
    
    # 2. Passo Ricorsivo (else)
    # 2a. Prepara argomento per fact(n - 1)
    addiw a0, s0, -1   # a0 = n - 1
    
    # 2b. Chiama fact(n - 1)
    jal fact           # Chiama ricorsivamente. Il risultato sarà in a0
    
    # 2c. Calcola: n * risultato
    #    n è in s0 (l'abbiamo salvato!)
    #    risultato (fact(n-1)) è in a0
    mulw a0, s0, a0    # a0 = s0 * a0 (n * risultato)
    
    # 2d. Salta alla fine (per evitare il caso base)
    j .L_END

.L_BASE_CASE:
    # Ritorna 1
    li a0, 1
.L_END:
    # --- Epilogo ---
    # Ripristina i registri nell'ordine inverso
    ld s0, 0(sp)
    ld ra, 8(sp)
    addi sp, sp, 16  # Dealloca stack
    
    ret # Ritorna al chiamante (con risultato in a0)

8. Funzione con Virgola Mobile (double)

Le variabili float e double vengono passate nei registri fa (es. fa0, fa1).

// C
double add_doubles(double a, double b) {
    return a + b;
}

# Assembly
# a è in fa0, b è in fa1
add_doubles:
    # Non c'è bisogno di prologo/epilogo perché
    # non usiamo registri 's' e non chiamiamo altre funzioni.
    
    # 1. Calcola: a + b
    #    Suffisso '.d' per double
    fadd.d fa0, fa0, fa1 # fa0 = fa0 + fa1
    
    # 2. Ritorna
    #    Il risultato è già in fa0 (il registro di ritorno)
    ret

📋 Guida Step-by-Step alla Traduzione

Quando traduci una funzione C, segui questi 5 passi.

Step 1: Il Piano (Allocazione Registri)

Prima di scrivere una sola riga di assembly, decidi:

Esempio per sum_array(int arr[], int size):

Step 2: Il Prologo (Setup della Funzione)

Ogni funzione (tranne le più banali) inizia con un "prologo" per preparare lo stack.

Esempio di prologo (usando s0, s1):

# Spazio: 8 (per ra) + 8 (per s0) + 8 (per s1) = 24. Lo allineiamo a 32.
SUM_ARRAY:
    addi sp, sp, -32  # 1. Alloca 32 byte
    sd   ra, 24(sp)   # 2. Salva ra
    sd   s0, 16(sp)   # 3. Salva s0
    sd   s1, 8(sp)    # 3. Salva s1

Step 3: Il Corpo (La Logica)

Ora traduci la logica C usando le istruzioni e i registri che hai scelto.

Step 4: L'Epilogo (Cleanup)

Prima di ret, devi rimettere tutto a posto. È l'esatto opposto del prologo.

Esempio di epilogo:

.L_LOOP_END:
    mv   a0, s0       # 1. Risultato (sum) in a0
    ld   s1, 8(sp)    # 2. Ripristina s1
    ld   s0, 16(sp)   # 2. Ripristina s0
    ld   ra, 24(sp)   # 3. Ripristina ra
    addi sp, sp, 32   # 4. Dealloca stack

Step 5: Il Ritorno

L'ultima istruzione della tua funzione.