Τήρηση βιβλίων

Τήρηση βιβλίων

Άσκηση εκμάθησης

Εισαγωγή

Thunks

Σε μια προηγούμενη ενότητα αναφέρθηκε ότι τόσο οι τοπικές ετικέτες όσο και οι συναρτήσεις δεν είναι παρά διευθύνσεις σε ένα τμήμα με εκτελέσιμο κώδικα, όπως το 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

Thunks με κατάσταση

Ένα 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 συνδυασμένα με μόνιμη κατάσταση.

Οδηγίες

Είσαι ο λογιστής σε μια μικρή τράπεζα του χωριού. Κάθε πελάτης έχει έναν λογαριασμό και κρατάς το υπόλοιπό του στο καθολικό σου. Καθ' όλη τη διάρκεια του χρόνου, εφαρμόζονται συναλλαγές σε αυτά τα υπόλοιπα: πιστώνονται τόκοι, αφαιρούνται χρεώσεις, καταβάλλονται μπόνους, επιβάλλονται πρόστιμα. Κάθε συναλλαγή παίρνει ένα υπόλοιπο και παράγει ένα νέο.

Έχεις τέσσερις εργασίες.

Note

Μπορείς να υποθέσεις ότι κάθε thunk (συναλλαγές και φρουροί) σε αυτή την άσκηση είναι μια συνάρτηση που:

  1. δέχεται ως όρισμα έναν μη αρνητικό ακέραιο 64 bit
  2. και επιστρέφει επίσης έναν μη αρνητικό ακέραιο 64 bit.

1. Θυμήσου μια συναλλαγή

Ο ταμίας μαθαίνει μια νέα συναλλαγή στην αρχή της ημέρας και τη σημειώνει, ώστε να μπορεί να εφαρμοστεί αργότερα, όταν έρθει ένας πελάτης.

Όρισε δύο συναρτήσεις:

  • Η 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:

  • Το όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit.
  • Η τιμή επιστροφής είναι ένας μη αρνητικός ακέραιος 64 bit.

2. Το εγχειρίδιο της τράπεζας

Το εγχειρίδιο της τράπεζας έχει μια λίστα με συχνές συναλλαγές αποθηκευμένες σε έναν πίνακα αποστολής. Κάθε υποκατάστημα διατηρεί το δικό του αντίγραφο της λίστας και μπορεί να καταχωρεί διαφορετικές συναλλαγές ανάλογα με την τοπική πολιτική.

Όρισε δύο συναρτήσεις που λειτουργούν σε έναν πίνακα αποστολής που παρέχει ο καλών:

  • Η 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:

  • Το πρώτο όρισμα είναι η διεύθυνση μνήμης ενός πίνακα αποστολής.
  • Το δεύτερο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (η θέση).
  • Το τρίτο όρισμα είναι μια συναλλαγή.
  • Δεν υπάρχει τιμή επιστροφής.

Για τη select_transaction:

  • Το πρώτο όρισμα είναι η διεύθυνση μνήμης ενός πίνακα αποστολής.
  • Το δεύτερο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (η θέση).
  • Το τρίτο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (το υπόλοιπο).
  • Η τιμή επιστροφής είναι ένας μη αρνητικός ακέραιος 64 bit.

3. Επεξεργάσου μια μηνιαία κατάσταση

Στο τέλος του μήνα, γίνεται εκκαθάριση του λογαριασμού ενός πελάτη. Κάθε συναλλαγή που πραγματοποιήθηκε μέσα στον μήνα εφαρμόζεται στο αρχικό υπόλοιπο, η μία μετά την άλλη, και το αποτέλεσμα είναι το νέο υπόλοιπο.

Όρισε μια συνάρτηση 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.

4. Επεξεργασία με φρουρό

Η πολιτική της τράπεζας απαιτεί να ελέγχονται ορισμένες συναλλαγές πριν οριστικοποιηθούν. Ένας φρουρός είναι μια συνάρτηση που εξετάζει ένα προτεινόμενο υπόλοιπο και αποφασίζει αν είναι αποδεκτό. Αυτή η συνάρτηση φρουρός επιστρέφει μια μη μηδενική τιμή για έγκριση ή μηδέν για απόρριψη.

Όρισε τη process_with_guard, που δέχεται ένα αρχικό υπόλοιπο, τη διεύθυνση μνήμης ενός πίνακα συναλλαγών, τον αριθμό των συναλλαγών στον πίνακα και μια συνάρτηση φρουρό. Για κάθε συναλλαγή με τη σειρά:

  1. Εφάρμοσε τη συναλλαγή στο τρέχον υπόλοιπο για να υπολογίσεις ένα προσωρινό νέο υπόλοιπο.
  2. Κάλεσε τον φρουρό με το προσωρινό υπόλοιπο.
  3. Αν ο φρουρός επιστρέψει μη μηδενική τιμή, οριστικοποιείται: το τρέχον υπόλοιπο γίνεται το προσωρινό υπόλοιπο.
  4. Αν ο φρουρός επιστρέψει μηδέν, το τρέχον υπόλοιπο μένει αμετάβλητο και η συναλλαγή παραλείπεται.

Αφού επεξεργαστούν όλες οι συναλλαγές, επέστρεψε το τελικό υπόλοιπο μαζί με τον αριθμό των εγκεκριμένων συναλλαγών.

Σε ψευδοκώδικα, η 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

Για παράδειγμα, υπόθεσε ότι:

  1. Η add_interest είναι μια συναλλαγή που προσθέτει 5 και η service_fee είναι μια άλλη συναλλαγή που αφαιρεί 2
  2. Ο at_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:

  • Το πρώτο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (το αρχικό υπόλοιπο).
  • Το δεύτερο όρισμα είναι η διεύθυνση μνήμης ενός πίνακα συναλλαγών.
  • Το τρίτο όρισμα είναι ένας μη αρνητικός ακέραιος 64 bit (το μήκος του πίνακα).
  • Το τέταρτο όρισμα είναι μια συνάρτηση φρουρός που δέχεται έναν μη αρνητικό ακέραιο 64 bit και επιστρέφει έναν μη αρνητικό ακέραιο 64 bit.
  • Οι τιμές επιστροφής είναι δύο μη αρνητικοί ακέραιοι 64 bit: το τελικό υπόλοιπο στο rax και ο αριθμός των εγκεκριμένων συναλλαγών στο rdx.
Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
x86-64 Assembly Exercism

Έτοιμος να ξεκινήσεις την άσκηση Τήρηση βιβλίων;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.