Υπάρχει ένας ειδικός καταχωρητής που ονομάζεται rflags.
Τα bit του λειτουργούν σαν σημαίες για διάφορες συνθήκες.
Μερικές από αυτές αναφέρονται παρακάτω:
| όνομα | σύμβολο | bit |
|---|---|---|
| μεταφορά | CF | 0 |
| μηδέν | ZF | 6 |
| πρόσημο | SF | 7 |
| υπερχείλιση | OF | 11 |
Οι σημαίες στο rflags δεν τροποποιούνται άμεσα.
Αντίθετα, ορίζονται από πολλές διαφορετικές εντολές.
Για παράδειγμα, το ZF ορίζεται από πολλές αριθμητικές πράξεις ή πράξεις bitwise, όταν το αποτέλεσμα είναι μηδέν.
Μία από τις πιο συνηθισμένες εντολές που χρησιμοποιούνται για τον έλεγχο συνθηκών είναι η cmp.
Δέχεται δύο τελεστέους και ενημερώνει τις σημαίες, αλλά δεν τροποποιεί τους τελεστέους της.
Η εντολή cmp αφαιρεί τον δεύτερο τελεστέο από τον πρώτο και ορίζει τις σημαίες ανάλογα με το αποτέλεσμα.
Αν το A είναι ο πρώτος τελεστέος και το B ο δεύτερος:
| σημαία | ορίζεται όταν |
|---|---|
| CF | A < B (χωρίς πρόσημο) |
| ZF | A == B |
| SF | A < B (με πρόσημο, χωρίς υπερχείλιση) |
| OF | υπερχείλιση σε αφαίρεση με πρόσημο |
Από προεπιλογή, ο κώδικας σε x86-64 εκτελείται διαδοχικά από πάνω προς τα κάτω.
Υπάρχουν όμως πολλές περιπτώσεις όπου είναι απαραίτητο να τροποποιήσουμε αυτή τη συμπεριφορά. Για παράδειγμα, για να εκτελέσουμε ένα διαφορετικό σύνολο εντολών ως απάντηση σε μια συνθήκη.
Σε γλώσσες υψηλότερου επιπέδου, αυτό γίνεται συνήθως με αφαιρέσεις όπως οι συνθήκες if...else.
Ωστόσο, αυτές δεν υπάρχουν σε x86-64.
Αντ' αυτού, το x86-64 παρέχει εντολές που μεταφέρουν ουσιαστικά την εκτέλεση σε άλλο σημείο του κώδικα.
Αυτό ονομάζεται branching.
Έχουμε ήδη δει δύο τέτοιες εντολές: call και ret.
Όταν καλείται μια συνάρτηση, η εκτέλεση μεταφέρεται από τον καλώντα στη συνάρτηση που καλείται. Και, κατά την επιστροφή, η εκτέλεση μεταφέρεται πίσω στον καλώντα.
Αν δεν βρεθεί ret, η εκτέλεση συνεχίζει απευθείας από τη μια συνάρτηση στην επόμενη.
Αυτό μπορεί μερικές φορές να χρησιμοποιηθεί για τη βελτιστοποίηση της ροής του κώδικα.
Η εντολή jmp μεταφέρει χωρίς συνθήκη την εκτέλεση του προγράμματος σε άλλο σημείο του κώδικα. Ο μοναδικός τελεστέος της είναι μια ετικέτα που περιέχει τη διεύθυνση του σημείου όπου θα συνεχιστεί η εκτέλεση.
Ας δούμε, για παράδειγμα, την παρακάτω συνάρτηση:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
Όταν καλείται η fn, η εκτέλεση ξεκινά από το mov rax, 5.
Έτσι, η τιμή στο rax γίνεται 5 σε εκείνο το σημείο.
Η επόμενη εντολή είναι η jmp end, η οποία μεταφέρει την εκτέλεση στην ετικέτα end.
Μετά το end, η επόμενη εντολή είναι η ret, η οποία μεταφέρει την εκτέλεση πίσω στη συνάρτηση που κάλεσε.
Παρατήρησε ότι, επειδή το add rax, 10 βρίσκεται μετά το jmp end και πριν το end, δεν εκτελείται ποτέ.
Η τιμή του rax όταν επιστρέφει η fn είναι 5.
Η οικογένεια εντολών jcc μεταφέρει την εκτέλεση του προγράμματος σε άλλο σημείο μόνο αν ικανοποιείται μια συγκεκριμένη συνθήκη. Διαφορετικά, η εκτέλεση συνεχίζεται διαδοχικά.
Κάθε συνθήκη αντιστοιχεί σε μία ή περισσότερες σημαίες στο rflags.
Κάποιες παραλλαγές της jcc ελέγχουν αν μια σημαία είναι ορισμένη, άλλες ελέγχουν αν είναι καθαρισμένη.
Το cc στο jcc δεν είναι κυριολεκτικό, αλλά παραπέμπει στο συγκεκριμένο επίθημα που σχετίζεται με τη σημαία που ελέγχεται.
Υπάρχουν πολλά επιθήματα και πολλά από αυτά ελέγχουν την ίδια συνθήκη με κάποιο άλλο. Κάποια από αυτά παραπέμπουν απευθείας σε μια σημαία, ώστε η εντολή να κάνει άλμα σε μια ετικέτα αν η συγκεκριμένη σημαία είναι ορισμένη:
| επίθημα | κάνει άλμα αν |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
Πολλά άλλα επιλέγονται ώστε να παραπέμπουν στο νόημά τους μέσα σε μια εντολή cmp.
Για παράδειγμα:
| εντολή | επίθημα | κάνει άλμα αν |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B (με πρόσημο) |
| cmp A, B | b | A < B (χωρίς πρόσημο) |
| cmp A, B | g | A > B (με πρόσημο) |
| cmp A, B | a | A > B (χωρίς πρόσημο) |
Είναι δυνατό να προσθέσεις e μετά από l, b, g ή a, για να συμπεριλάβεις την ισότητα στη συνθήκη:
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
Για όλα τα επιθήματα, υπάρχουν παραλλαγές που ελέγχουν την αντίθετη συμπεριφορά.
Έχουν την ίδια σύνταξη, αλλά με ένα n πριν από το επίθημα.
Για παράδειγμα, η jnz κάνει άλμα όταν το ZF δεν είναι ορισμένο.
Παρόμοια, η jnae κάνει άλμα όταν το A δεν είναι >= B (τα A και B ερμηνεύονται ως ακέραιοι χωρίς πρόσημο).
Κάποια επιθήματα είναι ψευδώνυμα για τις ίδιες συνθήκες.
Για παράδειγμα, τα jz (επίθημα z, για το ZF) και je (επίθημα e, για το equal) κάνουν και τα δύο άλμα όταν το ZF είναι ορισμένο.
Αυτό συμβαίνει επειδή, με την cmp, το ZF ορίζεται όταν η αφαίρεση δίνει μηδέν, το οποίο αντιστοιχεί στο να είναι οι δύο τελεστέοι ίσοι.
Ωστόσο, άλλα επιθήματα ελέγχουν έναν συνδυασμό σημαιών και δεν μπορούν να αντικατασταθούν άμεσα από ένα επίθημα μιας μόνο σημαίας.
Προτίμησε το επίθημα που περιγράφει καλύτερα τη σημασιολογία της σύγκρισής σου.
Οι ετικέτες είναι ορατές σε ολόκληρο το αρχείο πηγαίου κώδικα, δεν είναι τοπικές σε μια συνάρτηση. Έτσι, είναι αδύνατο να χρησιμοποιήσεις ξανά το ίδιο όνομα ετικέτας.
Για να μιμηθεί τη συμπεριφορά μιας τοπικής ετικέτας, το NASM έχει έναν ειδικό συμβολισμό για μια ετικέτα που δηλώνεται με μια τελεία (.) μπροστά της.
Αυτός ο συμβολισμός ορίζει μια ετικέτα που περιλαμβάνει σιωπηρά το όνομα της προηγούμενης ετικέτας χωρίς τελεία:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
Είναι ακόμα δυνατό να κάνεις άλμα σε αυτή την ετικέτα από οπουδήποτε στον κώδικα, χρησιμοποιώντας το πλήρες όνομα της ετικέτας, για παράδειγμα jmp fn1.example.
Ωστόσο, ένα άλμα που χρησιμοποιεί το μέρος της ετικέτας που αρχίζει από την τελεία θα γίνει προς την ετικέτα μέσα στην εξωτερική συνάρτηση.
Για παράδειγμα, το .example συμπεριφέρεται σαν να ήταν τοπικό στη συνάρτηση:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
Σε αυτή την άσκηση θα υλοποιήσεις μερικούς κανόνες του Blackjack, όπως τον τρόπο με τον οποίο παίζεται και βαθμολογείται το παιχνίδι.
Σε αυτή την άσκηση, τα φύλλα αντιπροσωπεύονται από αριθμούς· κάθε φύλλο με αριθμό αντιπροσωπεύεται από την αριθμητική του τιμή, ενώ οι βαλέδες, οι ντάμες, οι ρήγες και οι άσσοι ξεχωρίζουν από τους αριθμούς 11, 12, 13 και 14 αντίστοιχα (οι τζόκερ δεν χρησιμοποιούνται στο παιχνίδι).
Για να είναι πιο εύκολη η εργασία με αυτή την αναπαράσταση, μερικές σταθερές ορίζονται στην αρχή του αρχείου: τα C2 έως C10 είναι για τα φύλλα με αριθμό, το CJ για τον βαλέ, το CQ για τη ντάμα, το CK για τον ρήγα και το CA για τον άσσο.
Θεωρείται πως χρησιμοποιείται μια τυποποιημένη τράπουλα 52 φύλλων με γαλλικά χρώματα, αλλά στις περισσότερες εκδοχές ανακατεύονται μαζί αρκετές τράπουλες για το παιχνίδι.
Αυτές είναι οι εντολές που αναφέρονται σε αυτή την έννοια:
| Εντολή | Περιγραφή |
|---|---|
| cmp a, b | ορίζει τις σημαίες ανάλογα με το a - b |
| jmp a | ο κώδικας σταματά να εκτελείται εδώ και συνεχίζει στην ετικέτα a |
| jcc a | ο κώδικας συνεχίζει στην ετικέτα a αν ικανοποιείται η συνθήκη στο cc
|
Αυτές είναι οι συνθήκες που ελέγχονται σε μια jcc μετά από μια cmp a, b:
| Εντολή | Μετάβαση όταν |
|---|---|
| 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) |
Στο Blackjack, η τιμή ενός CA είναι είτε 1 είτε 11, ανάλογα με το χέρι (περισσότερα γι' αυτό παρακάτω).
Τα φύλλα με φιγούρα (CJ, CQ και CK) βαθμολογούνται με 10 πόντους και κάθε άλλο φύλλο αξίζει την αριθμητική του τιμή.
Όρισε τη συνάρτηση value_of_card με παράμετρο card, έναν αριθμό που αντιπροσωπεύει ένα φύλλο.
Η συνάρτηση πρέπει να επιστρέφει την αριθμητική τιμή του φύλλου που της περνάς.
Επειδή ένα CA μπορεί να πάρει πολλές τιμές (1 ή 11), για την ώρα καθορίζουμε την τιμή του CA σε 1.
Αργότερα θα υλοποιήσεις μια συνάρτηση που θα προσδιορίζει την τιμή ενός CA με δεδομένο ένα υπάρχον χέρι.
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
Όρισε τη συνάρτηση higher_card με παραμέτρους card_one και card_two, δύο αριθμούς που αντιπροσωπεύουν ο καθένας από ένα φύλλο.
Για τη βαθμολόγηση, οι τιμές του CJ, του CQ και του CK είναι όλες 10.
Η συνάρτηση πρέπει να επιστρέφει ποιο φύλλο έχει τη μεγαλύτερη τιμή για βαθμολόγηση.
Αν και τα δύο φύλλα έχουν ίση τιμή, επέστρεψε και τα δύο.
Ένα CA μπορεί να πάρει πολλές τιμές, οπότε για αυτή την εργασία θα καθορίσουμε την τιμή του σε 1.
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
Για να επιστρέψεις δύο ακέραιους από μια συνάρτηση, θα πρέπει να χρησιμοποιήσεις και τους δύο καταχωρητές rax και rdx:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
Αν επιστραφεί μόνο ένα φύλλο, το rdx πρέπει να τεθεί σε 0.
Όπως αναφέρθηκε προηγουμένως, ένα CA αξίζει είτε 1 είτε 11 πόντους, ανάλογα με το χέρι.
Οι κανόνες του Blackjack απαιτούν οι τιμές των CA να επιλέγονται έτσι ώστε να μεγιστοποιούν το σκορ του χεριού χωρίς όμως να ξεπερνούν το 21 (να "μπουστάρει" το χέρι).
Όρισε τη συνάρτηση value_of_ace με παραμέτρους card_one και card_two, που είναι δύο αριθμοί που αντιπροσωπεύουν ένα ζευγάρι φύλλων που βρίσκονται ήδη στο χέρι πριν πάρεις το τελευταίο CA.
Η συνάρτηση πρέπει να επιστρέφει ποια τιμή, 1 ή 11, θα αποδοθεί στο CA που έρχεται.
Θυμήσου: η τιμή του νέου χεριού (με το CA) πρέπει να είναι όσο το δυνατόν μεγαλύτερη χωρίς να ξεπερνά το 21.
Υπόδειξη: αν έχουμε ήδη ένα CA στο χέρι, τότε η τιμή για το CA που έρχεται θα είναι 1.
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
Αν ένας παίκτης πάρει ένα CA και ένα δεκάρι (C10, CJ, CQ ή CK) ως τα δύο πρώτα του φύλλα, τότε έχει σκορ 21.
Αυτό το χέρι είναι γνωστό ως blackjack.
Όρισε τη συνάρτηση is_blackjack με παραμέτρους card_one και card_two, που είναι δύο αριθμοί που αντιπροσωπεύουν ένα ζευγάρι φύλλων.
Η συνάρτηση πρέπει να επιστρέφει 1 αν το χέρι των δύο φύλλων είναι blackjack, αλλιώς 0.
Για να είναι πιο εύκολη η εργασία με τις τιμές, οι σταθερές TRUE και FALSE, που ισοδυναμούν με 1 και 0 αντίστοιχα, ορίζονται στην αρχή του αρχείου.
Σημείωση: Ο υπολογισμός του σκορ μπορεί να γίνει με πολλούς τρόπους.
Αν είναι δυνατόν όμως, θα θέλαμε να ελέγξεις αν υπάρχει ένα CA και ένα δεκάρι μέσα στο χέρι, αντί να αθροίσεις τις τιμές των φύλλων.
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
Αν τα δύο πρώτα φύλλα ενός παίκτη έχουν την ίδια τιμή (π.χ. ένα χέρι με δύο C6 ή ένα χέρι με ένα CQ και ένα CK), ο παίκτης μπορεί να επιλέξει να τα αντιμετωπίσει ως δύο ξεχωριστά χέρια.
Αυτό είναι γνωστό ως "χωρισμός ζευγαριών".
Όρισε τη συνάρτηση can_split_pairs με παραμέτρους card_one και card_two, που είναι δύο αριθμοί που αντιπροσωπεύουν ένα ζευγάρι φύλλων.
Η συνάρτηση πρέπει να επιστρέφει 1 αν το χέρι των δύο φύλλων μπορεί να χωριστεί σε δύο χέρια, αλλιώς 0.
Για να είναι πιο εύκολη η εργασία με τις τιμές, οι σταθερές TRUE και FALSE, που ισοδυναμούν με 1 και 0 αντίστοιχα, ορίζονται στην αρχή του αρχείου.
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
Όταν τα δύο αρχικά φύλλα που μοιράστηκαν αθροίζουν 9, 10 ή 11 πόντους, ο παίκτης μπορεί να ποντάρει ένα επιπλέον ποσό ίσο με το αρχικό του ποντάρισμα. Αυτό είναι γνωστό ως "διπλασιασμός του πονταρίσματος".
Όρισε τη συνάρτηση can_double_down με παραμέτρους card_one και card_two, που είναι δύο αριθμοί που αντιπροσωπεύουν ένα ζευγάρι φύλλων.
Η συνάρτηση πρέπει να επιστρέφει 1 αν το χέρι των δύο φύλλων επιτρέπει στον παίκτη να "διπλασιάσει το ποντάρισμα", αλλιώς 0.
Για να είναι πιο εύκολη η εργασία με τις τιμές, οι σταθερές TRUE και FALSE, που ισοδυναμούν με 1 και 0 αντίστοιχα, ορίζονται στην αρχή του αρχείου.
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0
Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.