Assembly assembly
Controllo di condizione
IF (x == 1) {
// CODE Block A
} ELSE IF (x == 2) {
// CODE Block B
} ELSE {
// CODE Block C
}
CMPB $1, %AL
JNZ .ELSEIF
# CODE Block A
JMP .ENDIF
.ELSEIF :
CMPB $2, %AL
JNZ .ELSE
# CODE Block B
JMP .ENDIF
.ELSE
# CODE Block C
.ENDIF ;
Aggiornamento carry flag
a - b richiede prestito quando b > a
- Se addizione non c'è riporto, c'è un prestito
- Se addizione c'è riporto, non c'è prestito
CF = 1 se c'è riporto, ZF = 1 se risultato zero
| Condizione | Primo controllo | Secondo controllo |
|---|---|---|
| dest < source | CF = 1 | |
| dest ≥ source | CF = 0 | |
| dest > source | CF = 0 | ZF = 0 |
| dest = source | ZF = 1 | |
| dest ≠ source | ZF = 0 |
In aritmetica segnata
Chiediamo DEST-SRC < 0.
Risultato dato dal segno SF.
Se c'è stato overflow, il segno è cambiato.
OF = 1 se riporti diversi, OF = 0 riporti uguali.
Confronti in aritmetica segnata
.org 0x800
.data
x: .word 3
y: .word -2
.text
# Imposta a 1 l’indirizzo 0x1280 solo se x > y
# Assumo che x ed y possano assumere valori negativi
movw x, %ax
movw y, %bx
cmpw %bx, %ax
jz .nonImpostare
js .SFSet
jo .nonImpostare # eseguita se SF = 0. Se OF = 1 allora SF != OF
jmp .set
.SFSet:
jno .nonImpostare # eseguita se SF = 1. Se OF = 0 allora Sf != OF
.set:
movb $1, 0x1280
.nonImpostare:
hlt
Cicli
while(TEST) {
CODE;
}
do {
CODE;
} while(TEST);
for(INIT; TEST; POST) {
CODE;
}
Tipi di variabili in C
In C ogni variabile ha un tipo.
- Tipi primitivi (interi, virgola mobile, ecc.)
- Tipi aggregati (struct, union)
- Puntatori
- Variabili globali: occupano memoria nelle sezioni .DATA e .bss visibili da tutte le funzioni
- Variabili locali: occupano memoria all'interno dello stack, visibili alla funzione stessa
La finestra di stack (o record di attivazione)
void function() {
int x = 128;
...
return;
}
function:
pushq %rbp
movq %rsp, %rbp
subq $4, %rsp
movl $128, -4(%rbp)
...
leave
ret
- L'istruzione x86 leave invalida la finestra di stack corrente
- Corrisponde a: movl %rbp, %rsp; popl %rbp
- Esiste anche l'istruzione enter, ma non è efficiente
- I debugger usano le finestre di stack per ricostruire le chiamate effettuate
Calling convention z64/x86
Primi 6 parametri di una subroutine sono passati tramite registri:
RDI, RSI, RDX, RCX, R8, R9
Si utilizza lo stack per quelli aggiuntivi.
Due registri per il valore di ritorno: RAX e RDX.
Registri:
Callee - save: RBP, RBX, R12 - R15
Caller - save: tutti gli altri
Anatomia dello stack
void f(int first, int second, int third, int fourth, int fifth, int sixth, int seventh, int eighth,) {
int x, y;
...
}
Tipi primitivi
In C sono 4: CHAR, INT, FLOAT, DOUBLE.
Alterati usando: SIGNED, UNSIGNED.
I modificatori signed, unsigned indicano al compilatore se devono essere utilizzate istruzioni aritmetiche per lavorare o meno in complemento a 2 e quali sono i bit di FLAGS da controllare per effettuare salti condizionali.
| Tipo C | Dimensione dato |
|---|---|
| CHAR | BYTE |
| INT | WORD |
| FLOAT | LONG WORD |
| DOUBLE | QUADWORD |
Cast: conversione tra tipi.
L'operazione di cast converte un dato da un tipo all’altro: (tipo di destinazione)variabile;
Ad esempio:
int integer = 10;
double real = (double)integer;
La conversione tra tipi interi viene realizzata mediante le istruzioni movsx e movzx oppure troncando la parte più significativa.
Il tipo booleano
Dal punto di vista architetturale, il tipo “booleano” non esiste.
Non esiste alcuna istruzione in nessuna ISA che gestisca i booleani.
Per convenzione: 0 è false, un valore diverso da 0 è true.
char booleano = 1;
if(booleano)
printf("true\n");
Il controllo sui booleani può essere realizzato con le istruzioni jz/jnz.
Array
Collezione di elementi, ciascuno identificato con un indice.
Si usa base o spiazzamento per inizio array.
Si usa indice e scala per indirizzo elemento preciso.
movq $0, %rcx # %rcx viene usato come indice
movq $array, %rax # %rax viene usato come base
.loop:
movq (%rax, %rcx, 8), %rdx # carica il dato nella CPU
# addq $1, %rcx
cmpq $size, %rcx
jnz .loop
movq $0, %rcx # %rcx viene usato come indice
.loop:
movq array(, %rcx, 8), %rdx # carica il dato nella CPU
# addq $1, %rcx
cmpq $size, %rcx
jnz .loop
Se vettore non è composto da tipi primitivi, si usa registro puntatore, che deve essere incrementato.
movq $array, %rax # carica indirizzo del primo elemento
movq $size, %rcx
.loop:
movq (%rax), %rdx # carica il dato nella CPU
# addq $element_size, %rax incremento
subq $1, %rcx
jnz .loop
Stringhe
Vettori di char, il cui ultimo elemento è il Terminatore di stringa.
Matrici
Strutture dati a più dimensioni, "linearizzate" in memoria.
ADDRESS ⟺ i * N * size + j * size
Puntatori
Tipo di dato che contiene indirizzo di memoria di una variabile.
INT * INTPTR
Esiste un generico puntatore a memoria: VOID *
Puntatori: equivalente in assembly
L’utilizzo dei puntatori in assembly è immediato.
L’indirizzo dell’area da puntare viene scritto in un registro.
Si utilizza quel registro come registro base.
Il contenuto del registro può essere manipolato a piacimento.
int var;
int *ptr = &var;
*ptr = 42;
ptr++;
movq $var, ptr
movq ptr, %rax
movl $42, (%rax)
addq $4, %rax
Strutture
Modo per aggregare tipi primitivi o vettori.
struct book {
char title[10];
char author[10];
int year;
int price;
};
Per accedere ai membri di una struttura in assembly, si può utilizzare la modalità di indirizzamento generale.
Si calcola uno spiazzamento a partire dalla base della struct.
Si carica in un registro l’indirizzo iniziale della struct.
Si utilizza un operando in memoria composto da spiazzamento e base.
Attenzione al padding se si interagisce con codice generato da un compilatore.
struct book {
char title[10];
char author[10];
int publication_year;
float price;
};
struct book b;
b.publication_year = 1987;
In C il passaggio di parametri è sempre fatto per valore.
Funzione riceve copia del parametro.
Effettuata copia di tutta la struttura.
Usare puntatori per passaggio per riferimento (indirizzo).
Passaggio di parametri
struct huge {
char member[4096];
};
extern void f1(struct huge s);
extern void f2(struct huge *s);
struct huge glbl;
void f(void) {
f2(&glbl);
}
pushq %rbp
movq %rsp, %rbp
movq $glbl, %rdi
call f2
popq %rbp
ret
Caso in cui uso puntatore.
Passaggio di parametri
struct huge {
char member[4096];
};
extern void f1(struct huge s);
extern void f2(struct huge *s);
struct huge glbl;
void f(void) {
f1(glbl);
}
pushq %rbp
movq %rsp, %rbp
subq $4096, %rsp
movq %rsp, %rdi
movq $glbl, %rsi
movq $512, %rcx
cld
rep movsq
call f1
addq $4096, %rsp
leave
ret
Caso in cui copio tutta la struttura.
Movs e stos
- Si tratta di istruzioni per operare su stringhe (buffer di dati)
- Utilizzano dei registri impliciti:
- RCX: contatore del numero di operazioni elementari da eseguire
- RSI: indirizzo sorgente (per il movimento)
- RDI: indirizzo destinazione
- RAX: valore cui impostare la memoria (per l’impostazione)
- Il direction flag (DF) identifica la direzione dell’operazione:
- DF = 0: l’operazione di copia si svolge in avanti
- DF = 1: l’operazione di copia si svolge all’indietro
movs: move data from string to string
stos: store string
movq $source, %rsi
movq $destination, %rdi
movq $size/8, %rcx
cld
movsq
movq $0x0, %rax
movq $destination, %rdi
movq $size/8, %rcx
cld
stosq
Se sorgente e destinazione sono sovrapposte, una copia in avanti può portare ad un risultato errato.
movb $src, %rsi
movb $dst, %rdi
movb $size, %rcx
cld
movsb
Soluzione: effettuare la copia all’indietro.
Se la sorgente e la destinazione sono sovrapposte, una copia in avanti può portare a un risultato errato.
Soluzione: effettuare la copia all'indietro.
RSI e RDI puntano alla fine dei buffer coinvolti.
std configura il processore per fare la copia all'indietro.
Equivalenti in C
#include <string.h>
void *memcpy(void *restrict dest, const void *restrict src, size_t n);
Effettua una copia memoria/memoria in avanti.
void *memmove(void *dest, const void *src, size_t n);
Effettua una copia memoria/memoria all'indietro (i buffer possono sovrapporsi).
void *memset(void *s, int c, size_t n);
Imposta un’area di memoria al valore c.
Unione
Blocco di memoria per conservare variabili differenti.
union number {
short little_number;
int large_number;
double fractional_number;
} numbers
Maschere di bit: forzatura
Per forzare dei bit ad un valore specifico, si usano maschere di bit.
Per forzare un bit a 1, si utilizza l'istruzione or:
Forzatura a 1 del bit più significativo: orl $0x80000000, %eax
Per forzare un bit a 0, si utilizza l'istruzione and:
Forzatura a 0 del bit più significativo: andl $0x7FFFFFFF, %eax
Per invertire un bit, si utilizza l'istruzione xor:
Inversione dell'ultimo bit: xorl $0x80000000, %eax
Per azzerare un registro, si possono usare due istruzioni equivalenti:
movq $0, %rax
xorq %rax, %rax
La seconda è preferibile perché più efficiente.
Si possono comporre maschere di bit per forzare più bit contemporaneamente.
Maschere di bit: estrazione
Supponiamo di avere un numero a 32 bit e di voler estrarre il valore dei 3 bit meno significativi.
Si costruisce una maschera di bit del tipo 000....00111, equivalente a 7.
Estrazione dei bit: andl $7, %eax
Per verificare se i bit sono a zero: testl $7, %eax
Che cosa fa l'istruzione testq $2, %rax?
Che cosa fa l'istruzione testq %rax, %rax?
-
Istruzioni assembly
-
Assembly x86 AMD64
-
Sistema operativo + linguaggio assembly
-
Raccolta Esercizi Assembly