Black Jack

Black Jack

Lernübung

Einführung

Bedingte Anweisungen

RFLAGs

Es gibt ein spezielles Register namens rflags. Seine Bits wirken wie Flags für verschiedene Bedingungen.

Einige davon sind unten aufgeführt:

Name Symbol Bit
carry CF 0
zero ZF 6
sign SF 7
overflow OF 11

Vergleichsinstruktionen

Die Flags in rflags werden nicht direkt verändert. Stattdessen werden sie von vielen verschiedenen Instruktionen gesetzt.

Zum Beispiel wird ZF von vielen arithmetischen oder bitweisen Operationen gesetzt, wenn das Ergebnis null ist.

Eine der häufigsten Instruktionen, um Bedingungen zu prüfen, ist cmp. Sie nimmt zwei Operanden und aktualisiert die Flags, verändert aber ihre Operanden nicht.

Die cmp-Instruktion

Die cmp-Instruktion subtrahiert den zweiten Operanden vom ersten und setzt die Flags entsprechend dem Ergebnis.

Wenn A der erste Operand ist und B der zweite:

Flag gesetzt, wenn
CF A < B (ohne Vorzeichen)
ZF A == B
SF A < B (mit Vorzeichen, kein Überlauf)
OF Überlauf bei Subtraktion mit Vorzeichen

Verzweigungen

Standardmäßig wird Code in x86-64 sequenziell von oben nach unten ausgeführt.

Aber es gibt viele Situationen, in denen es nötig ist, dieses Verhalten zu ändern. Zum Beispiel, um als Reaktion auf eine Bedingung eine andere Menge von Instruktionen auszuführen.

In höheren Programmiersprachen geschieht das normalerweise mit Abstraktionen wie if...else-Konstrukten. In x86-64 gibt es sie jedoch nicht.

Stattdessen bietet x86-64 Instruktionen, die die Ausführung effektiv an eine andere Stelle des Codes übergeben. Das nennt man branching.

Note

Wir haben bereits zwei solche Instruktionen gesehen: call und ret.

Wenn eine Funktion aufgerufen wird, wird die Ausführung vom Aufrufer an die aufgerufene Funktion übergeben. Und bei der Rückkehr wird die Ausführung zurück an den Aufrufer übergeben.

Wenn kein ret gefunden wird, geht die Ausführung von einer Funktion zur nächsten über. Das kann manchmal genutzt werden, um den Codefluss zu optimieren.

Unbedingter Sprung

Die Instruktion jmp übergibt die Ausführung des Programms bedingungslos an eine andere Stelle des Codes. Ihr einzelner Operand ist ein Label, das die Adresse der Stelle enthält, an der die Ausführung fortgesetzt wird.

Betrachte zum Beispiel die folgende Funktion:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

Wenn fn aufgerufen wird, beginnt die Ausführung bei mov rax, 5. Dadurch wird der Wert in rax an dieser Stelle auf 5 gesetzt.

Die nächste Instruktion ist jmp end, die die Ausführung an das Label end übergibt.

Nach end ist die nächste Instruktion ret, die die Ausführung zurück an die aufrufende Funktion übergibt.

Beachte, dass add rax, 10 nie ausgeführt wird, weil es nach jmp end und vor end steht. Der Wert von rax ist 5, wenn fn zurückkehrt.

Bedingter Sprung

Die Instruktionsfamilie jcc übergibt die Ausführung des Programms nur dann an eine andere Stelle, wenn eine bestimmte Bedingung erfüllt ist. Andernfalls läuft die Ausführung sequenziell weiter.

Jede Bedingung entspricht einem oder mehreren Flags in rflags. Einige jcc-Varianten prüfen, ob ein Flag gesetzt ist, andere prüfen, ob es gelöscht ist.

Das cc in jcc ist nicht wörtlich gemeint, sondern verweist auf das jeweilige Suffix, das zu dem getesteten Flag gehört.

Es gibt viele Suffixe, und viele davon prüfen dieselbe Bedingung wie ein anderes. Einige davon verweisen direkt auf ein Flag, sodass die Instruktion zu einem Label springt, wenn das jeweilige Flag gesetzt ist:

Suffix springt, wenn
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

Viele andere sind so gewählt, dass sie sich auf ihre Bedeutung in einer cmp-Instruktion beziehen. Zum Beispiel:

Instruktion Suffix springt, wenn
cmp A, B e A == B
cmp A, B l A < B (mit Vorzeichen)
cmp A, B b A < B (ohne Vorzeichen)
cmp A, B g A > B (mit Vorzeichen)
cmp A, B a A > B (ohne Vorzeichen)

Du kannst e nach l, b, g oder a hinzufügen, um die Gleichheit in die Bedingung einzubeziehen:

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

Für alle Suffixe gibt es Varianten, die das entgegengesetzte Verhalten prüfen. Sie haben dieselbe Syntax, aber mit einem n vor dem Suffix.

Zum Beispiel springt jnz, wenn ZF nicht gesetzt ist. Ebenso springt jnae, wenn A nicht >= B ist (A und B als ganze Zahlen ohne Vorzeichen interpretiert).

Note

Einige Suffixe sind Aliase für dieselben Bedingungen. Zum Beispiel springen jz (Suffix z, für ZF) und je (Suffix e, für „equal“) beide, wenn ZF gesetzt ist. Das liegt daran, dass bei cmp ZF gesetzt wird, wenn die Subtraktion null ergibt, was bedeutet, dass die beiden Operanden gleich sind.

Andere Suffixe prüfen jedoch eine Kombination von Flags und lassen sich nicht direkt durch ein einzelnes Flag-Suffix ersetzen.

Bevorzuge das Suffix, das die Semantik deines Vergleichs besser beschreibt.

Lokale Labels

Labels sind in der gesamten Quelldatei sichtbar, sie sind nicht auf eine Funktion beschränkt. Deshalb ist es unmöglich, einen Labelnamen wiederzuverwenden.

Um das Verhalten eines lokalen Labels nachzuahmen, hat NASM eine spezielle Notation für ein Label, dem ein Punkt (.) vorangestellt ist. Diese Notation definiert ein Label, das implizit den Namen des vorherigen Labels ohne Punkt enthält:

section .text
fn1:
    ...
.example: ; this is fn1.example
    ...
    ret

fn2:
    ...
.example: ; this is fn2.example
    ...
    ret

Es ist weiterhin möglich, von überall im Code zu diesem Label zu springen, indem du den vollständigen Labelnamen verwendest, zum Beispiel jmp fn1.example.

Ein Sprung, der nur den ab dem Punkt beginnenden Teil des Labels verwendet, erfolgt jedoch zu dem Label innerhalb der umgebenden Funktion. Zum Beispiel verhält sich .example, als wäre es lokal für die Funktion:

section .text
fn1:
    ...
.example:
    ...
    jmp .example ; this jumps to fn1.example

fn2:
    ...
.example:
    ...
    jmp .example ; this jumps to fn2.example

Anleitung

In dieser Übung implementierst du einige Regeln von Blackjack, etwa wie das Spiel gespielt und gewertet wird.

Note

In dieser Übung werden die Karten durch Zahlen dargestellt. Jede Zahlenkarte wird durch ihren numerischen Wert dargestellt, während Buben, Damen, Könige und Asse durch 11, 12, 13 bzw. 14 gekennzeichnet sind (Joker werden im Spiel nicht verwendet).

Damit du mit dieser Darstellung leichter arbeiten kannst, sind am Anfang der Datei einige Konstanten definiert; C2 bis C10 stehen für Zahlenkarten, CJ für einen Buben, CQ für eine Dame, CK für einen König und CA für ein Ass.

Es wird ein Standard-Deck mit 52 französischen Karten angenommen, aber in den meisten Varianten werden mehrere Decks gemischt und zusammen gespielt.

Note

Das sind die Befehle, die in diesem Konzept erwähnt werden:

Befehl Beschreibung
cmp a, b setzt die Flags entsprechend a - b
jmp a der Code wird hier nicht mehr ausgeführt und bei Label a fortgesetzt
jcc a der Code wird bei Label a fortgesetzt, wenn die Bedingung in cc erfüllt ist

Das sind die Bedingungen, die in einem jcc nach einem cmp a, b geprüft werden:

Befehl Springt, wenn
je a == b
jl a < b (mit Vorzeichen)
jg a > b (mit Vorzeichen)
jb a < b (ohne Vorzeichen)
ja a > b (ohne Vorzeichen)
jle a <= b (mit Vorzeichen)
jge a >= b (ohne Vorzeichen)
jbe a <= b (ohne Vorzeichen)
jae a >= b (ohne Vorzeichen)
jne a != b
jnl !(a < b) (mit Vorzeichen)
jng !(a > b) (mit Vorzeichen)
jnb !(a < b) (ohne Vorzeichen)
jna !(a > b) (ohne Vorzeichen)
jnle !(a <= b) (mit Vorzeichen)
jnge !(a >= b) (mit Vorzeichen)
jnbe !(a <= b) (ohne Vorzeichen)
jnae !(a >= b) (ohne Vorzeichen)

1. Berechne den Wert einer Karte

In Blackjack ist der Wert einer CA entweder 1 oder 11, je nach Hand (mehr dazu später). Bildkarten (CJ, CQ und CK) zählen 10 Punkte, und jede andere Karte ist ihren numerischen Wert wert.

Definiere die Funktion value_of_card mit dem Parameter card, einer Zahl, die eine Karte repräsentiert. Die Funktion muss den numerischen Wert der übergebenen Karte zurückgeben. Da eine CA mehrere Werte annehmen kann (1 oder 11), legen wir den Wert einer CA vorerst auf 1 fest. Später wirst du eine Funktion implementieren, die den Wert einer CA für eine bestehende Hand ermittelt.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Bestimme, welche Karte den höheren Wert hat

Definiere die Funktion higher_card mit den Parametern card_one und card_two, zwei Zahlen, die jeweils eine Karte repräsentieren. Für die Punktewertung haben CJ, CQ und CK alle den Wert 10. Die Funktion muss zurückgeben, welche Karte für die Punktewertung den höheren Wert hat. Haben beide Karten den gleichen Wert, gib beide zurück.

Eine CA kann mehrere Werte annehmen, daher legen wir ihren Wert für diese Aufgabe auf 1 fest.

higher_card(13, 11)
// => {13, 11}

higher_card(4, 6)
// => 6

>>> higher_card(13, 14)
// => 13
Note

Um zwei Ganzzahlen aus einer Funktion zurückzugeben, solltest du die Register rax und rdx verwenden:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Wird nur eine Karte zurückgegeben, muss rdx auf 0 gesetzt werden.

3. Berechne den Wert eines Asses

Wie bereits erwähnt, ist eine CA entweder 1 oder 11 Punkte wert, je nach Hand. Die Regeln von Blackjack verlangen, dass die Werte der CA so gewählt werden, dass sie den Punktestand der Hand maximieren, ohne dabei über 21 zu gehen (was als „bust“ bezeichnet wird).

Definiere die Funktion value_of_ace mit den Parametern card_one und card_two, zwei Zahlen, die ein Kartenpaar repräsentieren, das sich vor Erhalt der letzten CA bereits auf der Hand befindet. Deine Funktion muss zurückgeben, welcher Wert, 1 oder 11, der kommenden CA zugewiesen wird. Denk daran: Der Wert der neuen Hand (mit der CA) muss so hoch wie möglich sein, ohne über 21 zu gehen.

Hinweis: Wenn wir bereits eine CA auf der Hand haben, wäre der Wert für die kommende CA 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Bestimme, ob die Hand ein Blackjack ist

Bekommt ein Spieler als erste zwei Karten eine CA und eine Zehnerkarte (C10, CJ, CQ oder CK), hat er einen Punktestand von 21. Diese Hand nennt man Blackjack.

Definiere die Funktion is_blackjack mit den Parametern card_one und card_two, zwei Zahlen, die ein Kartenpaar repräsentieren. Die Funktion muss 1 zurückgeben, wenn die Zwei-Karten-Hand ein Blackjack ist, andernfalls 0. Damit du leichter mit den Werten arbeiten kannst, sind die Konstanten TRUE und FALSE, die 1 bzw. 0 entsprechen, am Anfang der Datei definiert.

Hinweis : Die Berechnung des Punktestands kann auf viele Arten erfolgen. Wenn möglich, möchten wir aber, dass du prüfst, ob sich eine CA und eine Zehnerkarte in der Hand befinden, anstatt die Kartenwerte zu summieren.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. Paare aufteilen

Wenn die ersten beiden Karten eines Spielers denselben Wert haben (z. B. die Hand aus zwei C6 oder die Hand aus einer CQ und einem CK), kann der Spieler sie als zwei getrennte Hände behandeln. Das nennt man „Paare aufteilen“.

Definiere die Funktion can_split_pairs mit den Parametern card_one und card_two, zwei Zahlen, die ein Kartenpaar repräsentieren. Die Funktion muss 1 zurückgeben, wenn die Zwei-Karten-Hand in zwei Hände aufgeteilt werden kann, andernfalls 0. Damit du leichter mit den Werten arbeiten kannst, sind die Konstanten TRUE und FALSE, die 1 bzw. 0 entsprechen, am Anfang der Datei definiert.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Verdoppeln

Wenn die beiden ursprünglich ausgeteilten Karten zusammen 9, 10 oder 11 Punkte ergeben, kann der Spieler einen zusätzlichen Einsatz in Höhe seines ursprünglichen Einsatzes setzen. Das nennt man „Verdoppeln“.

Definiere die Funktion can_double_down mit den Parametern card_one und card_two, zwei Zahlen, die ein Kartenpaar repräsentieren. Die Funktion muss 1 zurückgeben, wenn die Zwei-Karten-Hand dem Spieler das „Verdoppeln“ erlaubt, andernfalls 0. Damit du leichter mit den Werten arbeiten kannst, sind die Konstanten TRUE und FALSE, die 1 bzw. 0 entsprechen, am Anfang der Datei definiert.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
x86-64 Assembly Exercism

Bereit, mit Black Jack zu starten?

Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.