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.
| 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 |
call. È responsabile di salvare i registri t e a prima della chiamata, se le servono dopo.ra e i registri s che usa, per poi ripristinarli prima di ritornare.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 |
Ecco le istruzioni più comuni che userai.
Nota importante: RV64 ha istruzioni standard (64-bit) e istruzioni "Word" (32-bit, con suffissow). Poiché il C usa spessoint(32-bit), userai molto le versioniw.
| 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) |
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) |
| 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)) |
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) |
// 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)
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
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
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
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
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
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)
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
Quando traduci una funzione C, segui questi 5 passi.
Prima di scrivere una sola riga di assembly, decidi:
s (es. s0, s1, s2).t (es. t0, t1).a0, a1, ecc. (o fa0, fa1 per float/double). Probabilmente vorrai salvarli subito in registri s (o fs) se devi usarli dopo una jal (chiamata a funzione).Esempio per sum_array(int arr[], int size):
arr (in a0) -> Lo lascio in a0 (o lo copio in s2).size (in a1) -> Lo lascio in a1 (o lo copio in s3).sum -> Lo metto in s0.i -> Lo metto in s1.s usati: s0, s1.Ogni funzione (tranne le più banali) inizia con un "prologo" per preparare lo stack.
addi sp, sp, -N
N deve essere un multiplo di 16 (per allineamento).ra + 8 byte per ogni registro s (o fs) che hai deciso di usare.ra sullo stack. sd ra, N-8(sp)s: Salva tutti i registri s (e fs) che hai pianificato di usare. sd s0, N-16(sp), fsd fs0, N-24(sp), ecc.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
Ora traduci la logica C usando le istruzioni e i registri che hai scelto.
addiw, subw, slli, ecc.fadd.d, fmul.s, ecc.lw / sw (per int) o fld / fsd (per double) per accedere allo stack o agli array.if e while, usa le istruzioni b (branch) per saltare alle etichette (es. .L_LOOP_END).Prima di ret, devi rimettere tutto a posto. È l'esatto opposto del prologo.
a0 (o fa0). mv a0, s0 (se il risultato era in s0).s: Ricarica i registri s (e fs) dallo stack. ld s1, 8(sp), ld s0, 16(sp).ra. ld ra, 24(sp).addi sp, sp, 32.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
L'ultima istruzione della tua funzione.
ret