Esiste un registro speciale chiamato rflags.
I suoi bit agiscono 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 |
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 | viene 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 livello superiore, questo si fa di solito con astrazioni come i condizionali if...else.
In x86-64, però, non esistono.
Al loro posto, x86-64 fornisce istruzioni che di fatto trasferiscono l'esecuzione in un altro punto del codice.
Questa operazione 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. Ed al ritorno, l'esecuzione viene trasferita di nuovo al chiamante.
Se non si trova alcun ret, l'esecuzione prosegue da una funzione alla successiva.
A volte questo può servire a 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 proseguirà.
Considera, per esempio, la seguente funzione:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Quando viene chiamata fn, l'esecuzione parte da mov rax, 5.
A quel punto, questo imposta il valore di rax a 5.
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 restituisce, il valore di rax è 5.
La famiglia di istruzioni jcc trasferisce l'esecuzione del programma in un altro punto solo se è soddisfatta una condizione specifica. Altrimenti, l'esecuzione prosegue 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 si riferiscono direttamente a un flag, così che l'istruzione salta a un'etichetta se quel 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 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 all'uguaglianza dei due operandi.
Altri suffissi, invece, verificano una combinazione di flag e non possono essere sostituiti direttamente da un suffisso relativo a un singolo 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 verrà fatto all'etichetta interna alla 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
In questo esercizio implementerai alcune regole del blackjack, come il modo in cui si gioca e si assegnano i punti.
In questo esercizio, le carte sono rappresentate da numeri; ogni carta numerica è rappresentata dal suo valore numerico, mentre i fanti, le regine, i re e gli assi sono identificati rispettivamente da 11, 12, 13 e 14 (i jolly non si usano nel gioco).
Per rendere più facile lavorare con questa rappresentazione, all'inizio del file sono definite alcune costanti: da C2 a C10 per le carte numeriche, CJ per un fante, CQ per una regina, CK per un re e CA per un asso.
Si presume l'uso di un mazzo standard di 52 carte con semi francesi, ma nella maggior parte delle versioni si mescolano insieme più mazzi per giocare.
Queste sono le istruzioni menzionate in questo concetto:
| Istruzione | Descrizione |
|---|---|
| cmp a, b | imposta i flag in base al risultato di a - b |
| jmp a | il codice smette di eseguire qui e continua dall'etichetta a |
| jcc a | il codice continua dall'etichetta a se la condizione in cc è soddisfatta |
Queste sono le condizioni controllate in un jcc dopo un cmp a, b:
| Istruzione | Salta quando |
|---|---|
| je | a == b |
| jl | a < b (signed) |
| jg | a > b (signed) |
| jb | a < b (unsigned) |
| ja | a > b (unsigned) |
| jle | a <= b (signed) |
| jge | a >= b (unsigned) |
| jbe | a <= b (unsigned) |
| jae | a >= b (unsigned) |
| jne | a != b |
| jnl | !(a < b) (signed) |
| jng | !(a > b) (signed) |
| jnb | !(a < b) (unsigned) |
| jna | !(a > b) (unsigned) |
| jnle | !(a <= b) (signed) |
| jnge | !(a >= b) (signed) |
| jnbe | !(a <= b) (unsigned) |
| jnae | !(a >= b) (unsigned) |
Nel blackjack, il valore di un CA è 1 oppure 11, a seconda della mano (ne parleremo più avanti).
Le figure (CJ, CQ e CK) valgono 10 punti e qualsiasi altra carta vale il suo valore numerico.
Definisci la funzione value_of_card con il parametro card, un numero che rappresenta una carta.
La funzione deve restituire il valore numerico della carta passata.
Dato che un CA può assumere più valori (1 o 11), per il momento fissiamo il valore di un CA a 1.
Più avanti implementerai una funzione per determinare il valore di un CA data una mano esistente.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Definisci la funzione higher_card con i parametri card_one e card_two, due numeri che rappresentano ciascuno una carta.
Ai fini del punteggio, i valori di un CJ, di una CQ e di un CK sono tutti 10.
La funzione deve restituire quale carta ha il valore più alto ai fini del punteggio.
Se entrambe le carte hanno lo stesso valore, restituiscile entrambe.
Un CA può assumere più valori, quindi per questo compito ne fisseremo il valore a 1.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Per restituire due numeri interi da una funzione, devi usare entrambi i registri rax e rdx:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Se viene restituita una sola carta, rdx deve essere impostato a 0.
Come accennato prima, un CA vale o 1 o 11 punti, a seconda della mano.
Le regole del blackjack richiedono che i valori dei CA siano scelti per massimizzare il punteggio della mano senza però superare 21 (cioè senza «sballare»).
Definisci la funzione value_of_ace con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte già presenti nella mano prima di ricevere l'ultimo CA.
La funzione deve restituire quale valore, 1 o 11, sarà assegnato al CA in arrivo.
Ricorda: il valore della nuova mano (con il CA) deve essere il più alto possibile senza superare 21.
Suggerimento: se abbiamo già un CA in mano, il valore del CA in arrivo sarà 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Se a un giocatore vengono servite un CA e una carta da dieci (C10, CJ, CQ o CK) come prime due carte, il giocatore ha un punteggio di 21.
Questa mano è nota come blackjack.
Definisci la funzione is_blackjack con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte.
La funzione deve restituire 1 se la mano di due carte è un blackjack, 0 altrimenti.
Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.
Nota: il calcolo del punteggio si può fare in molti modi.
Ma se possibile, vorremmo che controllassi se nella mano c'è un CA e una carta da dieci, invece di sommare i valori delle carte.
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
Se le prime due carte di un giocatore hanno lo stesso valore (ad esempio una mano con due C6, oppure una mano con un CQ e un CK), il giocatore può scegliere di trattarle come due mani separate.
Questa operazione è nota come «dividere le coppie».
Definisci la funzione can_split_pairs con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte.
La funzione deve restituire 1 se la mano di due carte può essere divisa in due mani, 0 altrimenti.
Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Quando le due carte iniziali servite totalizzano 9, 10 o 11 punti, un giocatore può piazzare una puntata aggiuntiva pari alla puntata originale. Questa operazione è nota come «raddoppiare».
Definisci la funzione can_double_down con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte.
La funzione deve restituire 1 se la mano di due carte permette al giocatore di «raddoppiare», 0 altrimenti.
Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.