Esiste un registro speciale chiamato rflags.
I suoi bit funzionano come flag per varie condizioni.
Alcuni di essi sono elencati di seguito:
| nome | simbolo | bit |
|---|---|---|
| riporto | CF | 0 |
| zero | ZF | 6 |
| segno | SF | 7 |
| overflow | OF | 11 |
Per un elenco completo, consulta il manuale di Intel.
I flag in rflags non vengono modificati direttamente.
Vengono invece impostati da molte istruzioni diverse.
Per esempio, ZF viene impostato da molte operazioni aritmetiche o bit a bit quando il risultato è zero.
Una delle istruzioni più comuni usate per verificare le condizioni è cmp.
Prende due operandi e aggiorna i flag, ma non modifica i suoi operandi.
L'istruzione cmp sottrae il secondo operando dal primo e imposta i flag in base al risultato.
Se A è il primo operando e B il secondo:
| flag | impostato quando |
|---|---|
| CF | A < B (senza segno) |
| ZF | A == B |
| SF | A < B (con segno, senza overflow) |
| OF | overflow nella sottrazione con segno |
Per impostazione predefinita, il codice in x86-64 viene eseguito in sequenza dall'alto verso il basso.
Ma ci sono molte situazioni in cui è necessario modificare questo comportamento. Per esempio, per eseguire un insieme diverso di istruzioni in risposta a una condizione.
Nei linguaggi di alto livello, questo si fa di solito con astrazioni come i condizionali if...else.
Tuttavia, in x86-64 non esistono.
Al loro posto, x86-64 fornisce istruzioni che trasferiscono di fatto l'esecuzione in un'altra posizione del codice.
Questo si chiama branching.
Abbiamo già visto due istruzioni di questo tipo: call e ret.
Quando viene chiamata una funzione, l'esecuzione viene trasferita dal chiamante alla funzione chiamata. E, al ritorno, l'esecuzione viene trasferita di nuovo al chiamante.
Se non si trova alcun ret, l'esecuzione passa direttamente da una funzione a quella successiva.
Questo a volte può essere usato per ottimizzare il flusso del codice.
L'istruzione jmp trasferisce incondizionatamente l'esecuzione del programma in un altro punto del codice. Il suo unico operando è un'etichetta che contiene l'indirizzo del punto in cui l'esecuzione continuerà.
Considera, per esempio, la seguente funzione:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Quando viene chiamata fn, l'esecuzione inizia da mov rax, 5.
Questo imposta il valore di rax a 5 in quel punto.
L'istruzione successiva è jmp end, che trasferisce l'esecuzione all'etichetta end.
Dopo end, l'istruzione successiva è ret, che trasferisce l'esecuzione di nuovo alla funzione chiamante.
Nota che, dato che add rax, 10 si trova dopo jmp end e prima di end, non viene mai eseguita.
Quando fn termina, il valore di rax è 5.
La famiglia di istruzioni jcc trasferisce l'esecuzione del programma in un altro punto solo se una condizione specifica è soddisfatta. Altrimenti, l'esecuzione continua in sequenza.
Ogni condizione corrisponde a uno o più flag in rflags.
Alcune varianti di jcc verificano che un flag sia impostato, altre che sia azzerato.
Il cc in jcc non è letterale, ma si riferisce al suffisso specifico associato al flag verificato.
Ci sono molti suffissi e molti di essi verificano la stessa condizione di un altro. Alcuni di essi si riferiscono direttamente a un flag, così che l'istruzione salta a un'etichetta se il flag specifico è impostato:
| suffisso | salta se |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Molti altri sono scelti per riferirsi al loro significato in un'istruzione cmp.
Per esempio:
| istruzione | suffisso | salta se |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (con segno) |
| cmp A, B | b | A < B (senza segno) |
| cmp A, B | g | A > B (con segno) |
| cmp A, B | a | A > B (senza segno) |
È possibile aggiungere e dopo l, b, g o a per includere l'uguaglianza nella condizione:
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
Per tutti i suffissi, esistono varianti che verificano il comportamento opposto.
Hanno la stessa sintassi, ma con una n prima del suffisso.
Per esempio, jnz salta quando ZF non è impostato.
Analogamente, jnae salta quando A non è >= B (A e B interpretati come numeri interi senza segno).
Alcuni suffissi sono alias delle stesse condizioni.
Per esempio, jz (suffisso z, per ZF) e je (suffisso e, per «uguale») saltano entrambi quando ZF è impostato.
Questo perché, con cmp, ZF viene impostato quando la sottrazione dà zero, il che corrisponde al fatto che i due operandi sono uguali.
Altri suffissi, invece, verificano una combinazione di flag e non possono essere sostituiti direttamente da un singolo suffisso di flag.
Preferisci il suffisso che descrive meglio la semantica del tuo confronto.
Le etichette sono visibili nell'intero file sorgente, non sono locali a una funzione. Quindi è impossibile riutilizzare il nome di un'etichetta.
Per imitare il comportamento di un'etichetta locale, NASM ha una notazione speciale per un'etichetta dichiarata con un punto (.) davanti.
Questa notazione definisce un'etichetta che include implicitamente il nome dell'etichetta precedente non puntata:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
È comunque possibile saltare a questa etichetta da qualsiasi punto del codice usando il nome completo dell'etichetta, per esempio jmp fn1.example.
Tuttavia, un salto che usa la parte dell'etichetta a partire dal punto viene effettuato verso l'etichetta all'interno della funzione superiore.
Per esempio, .example si comporta come se fosse locale alla funzione:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
Nota che un'etichetta non puntata all'interno di una funzione, in pratica, definisce un'altra funzione:
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret