Σε μια προηγούμενη ενότητα αναφέρθηκε ότι τόσο οι τοπικές ετικέτες όσο και οι συναρτήσεις δεν είναι παρά διευθύνσεις σε ένα τμήμα με εκτελέσιμο κώδικα, όπως το section .text.
Στην πραγματικότητα, οι συναρτήσεις μπορούν να χειριστούν με τον ίδιο τρόπο όπως οποιαδήποτε διεύθυνση μνήμης: μπορούν να φορτωθούν σε καταχωρητές, να περαστούν από εδώ και από εκεί και να αποθηκευτούν στη μνήμη.
Είναι επίσης δυνατό να χρησιμοποιήσεις τις call ή jmp για να μεταφέρεις την εκτέλεση σε μια συνάρτηση που είναι αποθηκευμένη σε έναν καταχωρητή ή στη μνήμη:
section .text
sum_op:
lea rax, [rdi + rsi] ; loads the sum rdi + rsi into rax
ret
apply_sum:
lea rax, [rel sum_op]
jmp rax ; tail call
Μια διεύθυνση συνάρτησης που περνιέται από εδώ και από εκεί ως τιμή ονομάζεται thunk. Τα thunks είναι ένα δομικό στοιχείο του προγραμματισμού ανώτερης τάξης στη γλώσσα assembly: κώδικας που λειτουργεί πάνω σε άλλον κώδικα.
Οι διευθύνσεις συναρτήσεων μπορούν επίσης να αποθηκευτούν στη μνήμη και να ανακτηθούν αργότερα:
section .bss
cached_fn resq 1
section .text
save_op:
mov qword [rel cached_fn], rdi
ret
apply_op:
; arguments are already set up according to the ABI
jmp qword [rel cached_fn] ; tail call
Η save_op γράφει τη διεύθυνση της συνάρτησης που δέχεται στη cached_fn.
Η τιμή παραμένει και μετά την επιστροφή της save_op, οπότε οποιαδήποτε μεταγενέστερη κλήση της apply_op κάνει tail-jump στη διεύθυνση που αποθηκεύτηκε τελευταία.
Έτσι γίνεται δυνατό να αλλάξεις ποια συνάρτηση καλεί η apply_op κατά την εκτέλεση.
Η αποθήκευση διευθύνσεων συναρτήσεων σε έναν πίνακα καθιστά δυνατή την επιλογή διαφορετικών συναρτήσεων με βάση κάποια θέση, η οποία μπορεί να εξαρτάται από μια συνθήκη που ισχύει κατά την εκτέλεση. Αυτό ονομάζεται πίνακας αποστολής:
section .data
dispatch_table dq add_op, sub_op, mul_op
section .text
dispatch:
; this function takes two arguments in rdi and rsi, and an index in rdx
; it then applies the function corresponding to the index in rdx to the arguments
lea rax, [rel dispatch_table]
jmp qword [rax + 8*rdx] ; tail-call the function address for the index
Ένα thunk που διαβάζει ή ενημερώνει κάποια μόνιμη μνήμη ανάμεσα στις κλήσεις μπορεί να συμπεριφέρεται διαφορετικά ανάλογα με το τι προηγήθηκε. Το αποτέλεσμα του μπορεί να εξαρτάται από περισσότερα πράγματα από τα ορίσματά του και μόνο.
Για παράδειγμα, ένας μετρητής που παίρνει μια συνάρτηση και την καλεί με την τρέχουσα τιμή του μετρητή, αυξάνοντας τον μετρητή κάθε φορά:
section .data
count dq 0
section .text
tick:
mov rax, rdi ; saves the function address
mov rdi, [rel count] ; loads the current count as the function's argument
inc qword [rel count] ; advances the count
jmp rax ; tail-calls the function
Η tick καλεί τη συνάρτηση που της δίνεται, με όρισμα την τρέχουσα τιμή του μετρητή, και μετά αυξάνει τον μετρητή.
Έτσι, η πρώτη κλήση tick(square) καλεί square(0), η επόμενη κλήση tick(square) καλεί square(1), μετά square(2), και ούτω καθεξής.
Ένα άλλο παράδειγμα θα ήταν ένας υπολογισμός με καθυστέρηση:
section .bss
captured_fn resq 1
argument resq 1
section .text
delay:
mov qword [rel captured_fn], rdi ; saves the function
mov qword [rel argument], rsi ; saves the argument
lea rax, [rel invoke] ; returns the `invoke` function
ret
invoke:
mov rdi, qword [rel argument] ; loads the saved argument into `rdi`
jmp qword [rel captured_fn] ; tail-calls the saved function
Η delay παίρνει μια συνάρτηση και μια τιμή, τις αποθηκεύει και επιστρέφει την invoke.
Όταν καλείται η invoke, εκτελεί τη συνάρτηση που έχει συλλάβει, περνώντας της το αποθηκευμένο όρισμα.
Πολλά από τα μοτίβα που συναντάς συχνά σε γλώσσες υψηλότερου επιπέδου, όπως τα callbacks, οι εικονικές μέθοδοι, οι γεννήτριες, το currying, η σύνθεση συναρτήσεων και πολλά άλλα, βασίζονται σε thunks συνδυασμένα με μόνιμη κατάσταση.
Είσαι ο λογιστής σε μια μικρή τράπεζα του χωριού. Κάθε πελάτης έχει έναν λογαριασμό και κρατάς το υπόλοιπό του στο καθολικό σου. Καθ' όλη τη διάρκεια του χρόνου, εφαρμόζονται συναλλαγές σε αυτά τα υπόλοιπα: πιστώνονται τόκοι, αφαιρούνται χρεώσεις, καταβάλλονται μπόνους, επιβάλλονται πρόστιμα. Κάθε συναλλαγή παίρνει ένα υπόλοιπο και παράγει ένα νέο.
Έχεις τέσσερις εργασίες.
Μπορείς να υποθέσεις ότι κάθε thunk (συναλλαγές και φρουροί) σε αυτή την άσκηση είναι μια συνάρτηση που:
Ο ταμίας μαθαίνει μια νέα συναλλαγή στην αρχή της ημέρας και τη σημειώνει, ώστε να μπορεί να εφαρμοστεί αργότερα, όταν έρθει ένας πελάτης.
Όρισε δύο συναρτήσεις:
remember_transaction δέχεται μια συναλλαγή και την αποθηκεύει στη μνήμη.apply_remembered δέχεται ένα υπόλοιπο και εφαρμόζει σε αυτό τη συναλλαγή που αποθηκεύτηκε προηγουμένως.Παράδειγμα, υποθέτοντας ότι η add_interest είναι μια συναλλαγή που πιστώνει πέντε μονάδες τόκου:
remember_transaction(add_interest);
apply_remembered(100);
// => 105
remember_transaction(service_fee);
apply_remembered(100);
// => 98 (assuming service_fee deducts 2)
Για τη remember_transaction:
Για τη apply_remembered:
Το εγχειρίδιο της τράπεζας έχει μια λίστα με συχνές συναλλαγές αποθηκευμένες σε έναν πίνακα αποστολής. Κάθε υποκατάστημα διατηρεί το δικό του αντίγραφο της λίστας και μπορεί να καταχωρεί διαφορετικές συναλλαγές ανάλογα με την τοπική πολιτική.
Όρισε δύο συναρτήσεις που λειτουργούν σε έναν πίνακα αποστολής που παρέχει ο καλών:
register_transaction δέχεται τη διεύθυνση μνήμης ενός πίνακα αποστολής, μια θέση και μια συναλλαγή.
Αποθηκεύει αυτή τη συναλλαγή στη θέση που δίνεται μέσα στον πίνακα.select_transaction δέχεται τη διεύθυνση μνήμης ενός πίνακα αποστολής, μια θέση και ένα υπόλοιπο.
Αναζητά τη συναλλαγή στη θέση που δίνεται και την εφαρμόζει στο υπόλοιπο, επιστρέφοντας το νέο υπόλοιπο.Η select_transaction πρέπει να φτάνει στη συναλλαγή που αναζήτησε με μία μόνο έμμεση ουραία κλήση.
Παράδειγμα, υποθέτοντας ότι το manual είναι η διεύθυνση μνήμης ενός πίνακα αποστολής με τέσσερις κενές θέσεις:
register_transaction(manual, 0, monthly_interest);
register_transaction(manual, 1, service_fee);
select_transaction(manual, 0, 100);
// applies monthly_interest to 100
select_transaction(manual, 1, 100);
// applies service_fee to 100
Για τη register_transaction:
Για τη select_transaction:
Στο τέλος του μήνα, γίνεται εκκαθάριση του λογαριασμού ενός πελάτη. Κάθε συναλλαγή που πραγματοποιήθηκε μέσα στον μήνα εφαρμόζεται στο αρχικό υπόλοιπο, η μία μετά την άλλη, και το αποτέλεσμα είναι το νέο υπόλοιπο.
Όρισε μια συνάρτηση process_statement που δέχεται ένα αρχικό υπόλοιπο, τη διεύθυνση μνήμης ενός πίνακα συναλλαγών και τον αριθμό των συναλλαγών στον πίνακα.
Για κάθε συναλλαγή με τη σειρά, πρέπει να εφαρμόζει τη συναλλαγή στο τρέχον υπόλοιπο και μετά να χρησιμοποιεί το αποτέλεσμα ως υπόλοιπο για την επόμενη συναλλαγή.
Το τελικό υπόλοιπο επιστρέφεται.
Σε ψευδοκώδικα, η process_statement(balance, transactions, n) υπολογίζει:
for each transaction in transactions:
balance = transaction(balance)
return balance
Παράδειγμα, υποθέτοντας ότι το transactions είναι η διεύθυνση μνήμης ενός πίνακα που περιέχει τις συναλλαγές add_interest, service_fee και add_interest με αυτή τη σειρά, όπου η add_interest προσθέτει 5 και η service_fee αφαιρεί 2:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
Το πρώτο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit. Το δεύτερο όρισμα είναι η διεύθυνση μνήμης ενός πίνακα συναλλαγών. Το τρίτο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (το μήκος του πίνακα). Η τιμή επιστροφής είναι ένας μη αρνητικός ακέραιος 64 bit.
Η πολιτική της τράπεζας απαιτεί να ελέγχονται ορισμένες συναλλαγές πριν οριστικοποιηθούν. Ένας φρουρός είναι μια συνάρτηση που εξετάζει ένα προτεινόμενο υπόλοιπο και αποφασίζει αν είναι αποδεκτό. Αυτή η συνάρτηση φρουρός επιστρέφει μια μη μηδενική τιμή για έγκριση ή μηδέν για απόρριψη.
Όρισε τη process_with_guard, που δέχεται ένα αρχικό υπόλοιπο, τη διεύθυνση μνήμης ενός πίνακα συναλλαγών, τον αριθμό των συναλλαγών στον πίνακα και μια συνάρτηση φρουρό.
Για κάθε συναλλαγή με τη σειρά:
Αφού επεξεργαστούν όλες οι συναλλαγές, επέστρεψε το τελικό υπόλοιπο μαζί με τον αριθμό των εγκεκριμένων συναλλαγών.
Σε ψευδοκώδικα, η process_with_guard(balance, transactions, n, guard) υπολογίζει:
approved = 0
for each transaction in transactions:
tentative = transaction(balance)
if guard(tentative) is non-zero:
balance = tentative
approved = approved + 1
return balance, approved
Για παράδειγμα, υπόθεσε ότι:
add_interest είναι μια συναλλαγή που προσθέτει 5 και η service_fee είναι μια άλλη συναλλαγή που αφαιρεί 2at_least_10 είναι ένας φρουρός που επιστρέφει μη μηδενική τιμή όταν το υπόλοιπο είναι >= 10Τότε:
process_with_guard(5, {add_interest, service_fee, add_interest}, 3, at_least_10);
// add_interest(5) = 10; at_least_10(10) != 0;
// => balance = 10, approved = 1
//
// service_fee(10) = 8; at_least_10(8) = 0;
// => balance = 10, approved = 1
//
// add_interest(10) = 15; at_least_10(15) != 0;
// => balance = 15, approved = 2
//
// final balance (15) is returned in rax
// number of approved transactions (2) is returned in rdx
Για τη process_with_guard:
rax και ο αριθμός των εγκεκριμένων συναλλαγών στο rdx.Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.