Il existe un registre spécial appelé rflags.
Ses bits se comportent comme des drapeaux pour diverses conditions.
Certains d'entre eux sont listés ci-dessous :
| nom | symbole | bit |
|---|---|---|
| retenue | CF | 0 |
| zéro | ZF | 6 |
| signe | SF | 7 |
| débordement | OF | 11 |
Les drapeaux de rflags ne sont pas modifiés directement.
Ils sont au contraire positionnés par de nombreuses instructions différentes.
Par exemple, ZF est positionné par beaucoup d'opérations arithmétiques ou bit à bit lorsque le résultat vaut zéro.
L'une des instructions les plus courantes pour tester des conditions est cmp.
Elle prend deux opérandes et met à jour les drapeaux, mais ne modifie pas ses opérandes.
L'instruction cmp soustrait le second opérande au premier et positionne les drapeaux en fonction du résultat.
Si A est le premier opérande et B le second :
| drapeau | positionné quand |
|---|---|
| CF | A < B (non signé) |
| ZF | A == B |
| SF | A < B (signé, sans débordement) |
| OF | débordement lors d'une soustraction signée |
Par défaut, le code en x86-64 s'exécute séquentiellement, du haut vers le bas.
Mais il existe de nombreuses situations où il est nécessaire de modifier ce comportement. Par exemple, pour exécuter un ensemble d'instructions différent en réponse à une condition.
Dans les langages de plus haut niveau, on y parvient généralement avec des abstractions comme les conditionnelles if...else.
Or, celles-ci n'existent pas en x86-64.
À la place, x86-64 fournit des instructions qui transfèrent effectivement l'exécution vers un autre emplacement du code.
C'est ce qu'on appelle le branching.
Nous avons déjà vu deux instructions de ce type : call et ret.
Lorsqu'une fonction est appelée, l'exécution est transférée de l'appelant vers la fonction appelée. Et, au retour, l'exécution est retransférée vers l'appelant.
Si aucun ret n'est trouvé, l'exécution s'enchaîne d'une fonction à la suivante.
Cela peut parfois servir à optimiser le flux du code.
L'instruction jmp transfère inconditionnellement l'exécution du programme vers un autre point du code. Son unique opérande est une étiquette qui contient l'adresse du point où l'exécution se poursuivra.
Prenons par exemple la fonction suivante :
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Lorsque fn est appelée, l'exécution démarre à mov rax, 5.
Cela fixe la valeur de rax à 5 à cet instant.
L'instruction suivante est jmp end, qui transfère l'exécution vers l'étiquette end.
Après end, l'instruction suivante est ret, qui transfère l'exécution vers la fonction appelante.
Remarque que, comme add rax, 10 se trouve après jmp end et avant end, elle n'est jamais exécutée.
La valeur de rax au retour de fn est 5.
La famille d'instructions jcc transfère l'exécution du programme vers un autre point uniquement si une condition précise est remplie. Sinon, l'exécution se poursuit séquentiellement.
Chaque condition correspond à un ou plusieurs drapeaux de rflags.
Certaines variantes de jcc testent qu'un drapeau est positionné, d'autres qu'il est effacé.
Le cc de jcc n'est pas littéral, mais renvoie au suffixe précis associé au drapeau testé.
Il existe de nombreux suffixes, et beaucoup testent la même condition qu'un autre. Certains se réfèrent directement à un drapeau, de sorte que l'instruction saute vers une étiquette si ce drapeau est positionné :
| suffixe | saute si |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Beaucoup d'autres sont choisis pour faire référence à leur signification dans une instruction cmp.
Par exemple :
| instruction | suffixe | saute si |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (signé) |
| cmp A, B | b | A < B (non signé) |
| cmp A, B | g | A > B (signé) |
| cmp A, B | a | A > B (non signé) |
Il est possible d'ajouter e après l, b, g ou a pour inclure l'égalité dans la condition :
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
Pour tous les suffixes, il existe des variantes qui vérifient le comportement opposé.
Elles ont la même syntaxe, mais avec un n devant le suffixe.
Par exemple, jnz saute lorsque ZF n'est pas positionné.
De même, jnae saute lorsque A n'est pas >= B (A et B interprétés comme des entiers non signés).
Certains suffixes sont des alias d'une même condition.
Par exemple, jz (suffixe z, pour ZF) et je (suffixe e, pour l'égalité) sautent tous les deux lorsque ZF est positionné.
C'est parce qu'avec cmp, ZF est positionné lorsque la soustraction donne zéro, ce qui correspond à deux opérandes égaux.
D'autres suffixes, en revanche, testent une combinaison de drapeaux et ne peuvent pas être remplacés directement par un seul suffixe de drapeau.
Privilégie le suffixe qui décrit le mieux la sémantique de ta comparaison.
Les étiquettes sont visibles dans tout le fichier source ; elles ne sont pas locales à une fonction. Il est donc impossible de réutiliser le nom d'une étiquette.
Pour imiter le comportement d'une étiquette locale, NASM dispose d'une notation spéciale pour une étiquette déclarée avec un point (.) devant son nom.
Cette notation définit une étiquette qui inclut implicitement le nom de l'étiquette précédente sans point :
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
Il reste possible de sauter vers cette étiquette depuis n'importe où dans le code en utilisant le nom complet de l'étiquette, par exemple jmp fn1.example.
Cependant, un saut qui utilise la partie de l'étiquette à partir du point se fera vers l'étiquette à l'intérieur de la fonction englobante.
Par exemple, .example se comporte comme si elle était locale à la fonction :
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
Dans cet exercice, tu vas implémenter quelques règles du Blackjack, comme la façon dont le jeu se joue et dont les points se comptent.
Dans cet exercice, les cartes sont représentées par des nombres ; chaque carte numérotée est représentée par sa valeur numérique, tandis que les valets, les dames, les rois et les as sont identifiés respectivement par 11, 12, 13 et 14 (les jokers ne sont pas utilisés dans le jeu).
Pour faciliter le travail avec cette représentation, quelques constantes sont définies en haut du fichier ; C2 à C10 correspondent aux cartes numérotées, CJ à un valet, CQ à une dame, CK à un roi, et CA à un as.
On suppose un jeu standard de 52 cartes aux enseignes françaises, mais dans la plupart des versions, plusieurs jeux sont mélangés ensemble pour jouer.
Voici les instructions mentionnées dans ce concept :
| Instruction | Description |
|---|---|
| cmp a, b | positionne les drapeaux en fonction de a - b |
| jmp a | le code arrête son exécution ici et continue à l'étiquette a |
| jcc a | le code continue à l'étiquette a si la condition dans cc est remplie |
Voici les conditions vérifiées dans un jcc après un cmp a, b :
| Instruction | Saute quand |
|---|---|
| je | a == b |
| jl | a < b (signé) |
| jg | a > b (signé) |
| jb | a < b (non signé) |
| ja | a > b (non signé) |
| jle | a <= b (signé) |
| jge | a >= b (non signé) |
| jbe | a <= b (non signé) |
| jae | a >= b (non signé) |
| jne | a != b |
| jnl | !(a < b) (signé) |
| jng | !(a > b) (signé) |
| jnb | !(a < b) (non signé) |
| jna | !(a > b) (non signé) |
| jnle | !(a <= b) (signé) |
| jnge | !(a >= b) (signé) |
| jnbe | !(a <= b) (non signé) |
| jnae | !(a >= b) (non signé) |
Au Blackjack, la valeur d'un CA est soit 1, soit 11, selon la main (on y reviendra plus tard).
Les figures (CJ, CQ et CK) valent 10 points et toute autre carte vaut sa valeur numérique.
Définis la fonction value_of_card avec le paramètre card, un nombre représentant une carte.
La fonction doit renvoyer la valeur numérique de la carte passée.
Comme un CA peut prendre plusieurs valeurs (1 ou 11), on fixe pour l'instant la valeur d'un CA à 1.
Plus tard, tu implémenteras une fonction pour déterminer la valeur d'un CA à partir d'une main existante.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Définis la fonction higher_card avec les paramètres card_one et card_two, deux nombres représentant chacun une carte.
Pour le calcul du score, les valeurs d'un CJ, d'un CQ et d'un CK valent toutes 10.
La fonction doit renvoyer la carte qui a la valeur la plus élevée pour le score.
Si les deux cartes ont la même valeur, renvoie les deux.
Un CA peut prendre plusieurs valeurs, donc on fixe sa valeur à 1 pour cette tâche.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Pour renvoyer deux entiers depuis une fonction, tu dois utiliser les deux registres rax et rdx :
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Si une seule carte est renvoyée, rdx doit être mis à 0.
Comme mentionné plus tôt, un CA vaut soit 1 soit 11 points, selon la main.
Les règles du Blackjack exigent que la valeur des CA soit choisie pour maximiser le score de la main sans pour autant dépasser 21 (ce qu'on appelle « sauter »).
Définis la fonction value_of_ace avec les paramètres card_one et card_two, deux nombres représentant une paire de cartes déjà présentes dans la main avant de recevoir le dernier CA.
Ta fonction doit renvoyer la valeur, 1 ou 11, qui sera attribuée au prochain CA.
Rappel : la valeur de la nouvelle main (avec le CA) doit être la plus élevée possible sans dépasser 21.
Indice : si on a déjà un CA en main, la valeur du prochain CA sera 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Si un joueur reçoit un CA et une carte valant dix (C10, CJ, CQ ou CK) comme deux premières cartes, il obtient alors un score de 21.
Cette main est appelée un blackjack.
Définis la fonction is_blackjack avec les paramètres card_one et card_two, deux nombres représentant une paire de cartes.
La fonction doit renvoyer 1 si la main de deux cartes est un blackjack, 0 sinon.
Pour faciliter le travail avec ces valeurs, les constantes TRUE et FALSE, respectivement équivalentes à 1 et 0, sont définies en haut du fichier.
Note : Le calcul du score peut se faire de bien des façons.
Mais si possible, on aimerait que tu vérifies s'il y a un CA et une carte valant dix dans la main, plutôt que de sommer les valeurs des cartes.
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
Si les deux premières cartes d'un joueur ont la même valeur (par exemple une main avec deux C6, ou une main avec un CQ et un CK), le joueur peut choisir de les traiter comme deux mains séparées.
C'est ce qu'on appelle « séparer les paires ».
Définis la fonction can_split_pairs avec les paramètres card_one et card_two, deux nombres représentant une paire de cartes.
La fonction doit renvoyer 1 si la main de deux cartes peut être séparée en deux mains, 0 sinon.
Pour faciliter le travail avec ces valeurs, les constantes TRUE et FALSE, respectivement équivalentes à 1 et 0, sont définies en haut du fichier.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Lorsque les deux cartes distribuées au départ totalisent 9, 10 ou 11 points, un joueur peut placer une mise supplémentaire égale à sa mise initiale. C'est ce qu'on appelle « doubler la mise ».
Définis la fonction can_double_down avec les paramètres card_one et card_two, deux nombres représentant une paire de cartes.
La fonction doit renvoyer 1 si la main de deux cartes permet au joueur de « doubler la mise », 0 sinon.
Pour faciliter le travail avec ces valeurs, les constantes TRUE et FALSE, respectivement équivalentes à 1 et 0, sont définies en haut du fichier.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.