Buchhaltung

Buchhaltung

Lernübung

Einführung

Thunks

In einem früheren Konzept wurde erwähnt, dass sowohl lokale Labels als auch Funktionen einfach Adressen in einem Abschnitt mit ausführbarem Code sind, wie zum Beispiel section .text.

Tatsächlich lassen sich Funktionen genauso behandeln wie jede andere Speicheradresse: Sie können in Register geladen, weitergereicht und im Speicher abgelegt werden. Es ist auch möglich, mit call oder jmp die Ausführung an eine Funktion zu übergeben, die in einem Register oder im Speicher abgelegt ist:

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

Eine Funktionsadresse, die als Wert weitergereicht wird, nennt man einen Thunk. Thunks sind ein Baustein der Programmierung höherer Ordnung in Assembler: Code, der mit anderem Code arbeitet.

Code als Daten

Funktionsadressen können auch im Speicher abgelegt und später wieder abgerufen werden:

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 schreibt die Funktionsadresse, die es erhält, nach cached_fn. Der Wert bleibt erhalten, nachdem save_op zurückgekehrt ist. Jeder spätere Aufruf von apply_op springt also per Tail-Call zu der Adresse, die zuletzt dort abgelegt wurde. So lässt sich zur Laufzeit ändern, welche Funktion apply_op aufruft.

Dispatch-Tabellen

Wenn du Funktionsadressen in einem Array ablegst, kannst du anhand eines Index verschiedene Funktionen auswählen, wobei der Index von einer Bedingung zur Laufzeit abhängen kann. Das nennt man eine Dispatch-Tabelle:

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

Zustandsbehaftete Thunks

Ein Thunk, der zwischen Aufrufen einen dauerhaften Speicherbereich liest oder verändert, kann sich je nachdem, was vorher passiert ist, unterschiedlich verhalten. Sein Ergebnis kann von mehr abhängen als nur von seinen Argumenten.

Ein Beispiel ist ein Zähler, der eine Funktion entgegennimmt und sie mit dem aktuellen Zählerstand aufruft, wobei er den Zählerstand jedes Mal weiterzählt:

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 ruft die übergebene Funktion mit dem aktuellen Zählerstand als Argument auf und erhöht anschließend den Zählerstand. Ein erster Aufruf tick(square) ruft also square(0) auf, der nächste Aufruf tick(square) ruft square(1) auf, der darauffolgende square(2) und so weiter.

Ein weiteres Beispiel wäre eine verzögerte Berechnung:

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 nimmt eine Funktion und einen Wert entgegen, speichert beide und gibt invoke zurück. Wenn invoke aufgerufen wird, führt es die festgehaltene Funktion mit dem gespeicherten Argument aus.

Viele Muster, die in höheren Programmiersprachen üblich sind, wie Callbacks, virtuelle Methoden, Generatoren, Currying, Funktionskomposition und viele weitere, bauen auf Thunks in Kombination mit dauerhaftem Zustand auf.

Anleitung

Du bist der Buchhalter bei einer kleinen Dorfbank. Jeder Kunde hat ein Konto, und du führst den Kontostand in deinem Hauptbuch. Über das Jahr hinweg werden Transaktionen auf diese Kontostände angewendet: Zinsen werden gutgeschrieben, Gebühren abgezogen, Boni ausgezahlt und Strafgebühren erhoben. Jede Transaktion nimmt einen Kontostand entgegen und erzeugt einen neuen.

Du hast vier Aufgaben.

Note

Du kannst davon ausgehen, dass jeder Thunk (Transaktionen und Guards) in dieser Übung eine Funktion ist, die:

  1. eine nicht-negative 64-Bit-Ganzzahl als Argument entgegennimmt
  2. und ebenfalls eine nicht-negative 64-Bit-Ganzzahl zurückgibt.

1. Eine Transaktion merken

Der Schalterbeamte lernt zu Beginn des Tages eine neue Transaktion und schreibt sie auf, damit sie später angewendet werden kann, wenn ein Kunde kommt.

Definiere zwei Funktionen:

  • remember_transaction nimmt eine Transaktion entgegen und speichert sie im Speicher.
  • apply_remembered nimmt einen Kontostand entgegen und wendet die zuvor gespeicherte Transaktion darauf an.

Beispiel: Angenommen, add_interest ist eine Transaktion, die fünf Einheiten Zinsen gutschreibt:

remember_transaction(add_interest);
apply_remembered(100);
// => 105

remember_transaction(service_fee);
apply_remembered(100);
// => 98   (assuming service_fee deducts 2)

Für remember_transaction:

  • Das Argument ist eine Transaktion, die für eine spätere Verwendung gespeichert wird.
  • Es gibt keinen Rückgabewert.

Für apply_remembered:

  • Das Argument ist eine nicht-negative 64-Bit-Ganzzahl.
  • Der Rückgabewert ist eine nicht-negative 64-Bit-Ganzzahl.

2. Das Handbuch der Bank

Das Handbuch der Bank enthält eine Liste häufiger Transaktionen, die in einer Sprungtabelle gespeichert ist. Jede Filiale führt ihre eigene Kopie der Liste und kann je nach lokaler Richtlinie unterschiedliche Transaktionen registrieren.

Definiere zwei Funktionen, die auf einer vom Aufrufer übergebenen Sprungtabelle arbeiten:

  • register_transaction nimmt die Speicheradresse einer Sprungtabelle, einen Index und eine Transaktion entgegen. Sie speichert diese Transaktion an dem angegebenen Index in der Tabelle.
  • select_transaction nimmt die Speicheradresse einer Sprungtabelle, einen Index und einen Kontostand entgegen. Sie schlägt die Transaktion am angegebenen Index nach und wendet sie auf den Kontostand an, wobei sie den neuen Kontostand zurückgibt.

select_transaction sollte die nachgeschlagene Transaktion mit einem einzigen indirekten Tail-Call erreichen.

Beispiel: Angenommen, manual ist die Speicheradresse einer Sprungtabelle mit vier leeren Plätzen:

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

Für register_transaction:

  • Das erste Argument ist die Speicheradresse einer Sprungtabelle.
  • Das zweite Argument ist eine nicht-negative 64-Bit-Ganzzahl (der Index).
  • Das dritte Argument ist eine Transaktion.
  • Es gibt keinen Rückgabewert.

Für select_transaction:

  • Das erste Argument ist die Speicheradresse einer Sprungtabelle.
  • Das zweite Argument ist eine nicht-negative 64-Bit-Ganzzahl (der Index).
  • Das dritte Argument ist eine nicht-negative 64-Bit-Ganzzahl (der Kontostand).
  • Der Rückgabewert ist eine nicht-negative 64-Bit-Ganzzahl.

3. Einen Monatsauszug verarbeiten

Am Ende des Monats wird das Konto eines Kunden abgeglichen. Jede Transaktion, die im Laufe des Monats stattgefunden hat, wird nacheinander auf den Anfangskontostand angewendet, und das Ergebnis ist der neue Kontostand.

Definiere eine Funktion process_statement, die einen Anfangskontostand, die Speicheradresse eines Arrays von Transaktionen und die Anzahl der Transaktionen im Array entgegennimmt. Für jede Transaktion der Reihe nach sollte sie die Transaktion auf den laufenden Kontostand anwenden und das Ergebnis dann als Kontostand für die nächste Transaktion verwenden. Der endgültige Kontostand wird zurückgegeben.

In Pseudocode berechnet process_statement(balance, transactions, n):

for each transaction in transactions:
    balance = transaction(balance)
return balance

Beispiel: Angenommen, transactions ist die Speicheradresse eines Arrays, das die Transaktionen add_interest, service_fee und add_interest in dieser Reihenfolge enthält, wobei add_interest 5 addiert und service_fee 2 abzieht:

process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105)  = 103
// add_interest(103) = 108
// => 108

Das erste Argument ist eine nicht-negative 64-Bit-Ganzzahl. Das zweite Argument ist die Speicheradresse eines Arrays von Transaktionen. Das dritte Argument ist eine nicht-negative 64-Bit-Ganzzahl (die Array-Länge). Der Rückgabewert ist eine nicht-negative 64-Bit-Ganzzahl.

4. Mit einem Guard verarbeiten

Die Richtlinie der Bank verlangt, dass bestimmte Transaktionen überprüft werden, bevor sie übernommen werden. Ein Guard ist eine Funktion, die einen vorgeschlagenen Kontostand prüft und entscheidet, ob er akzeptabel ist. Diese Guard-Funktion gibt einen von null verschiedenen Wert zurück, um zu genehmigen, oder null, um abzulehnen.

Definiere process_with_guard, die einen Anfangskontostand, die Speicheradresse eines Arrays von Transaktionen, die Anzahl der Transaktionen im Array und eine Guard-Funktion entgegennimmt. Für jede Transaktion der Reihe nach:

  1. Wende die Transaktion auf den laufenden Kontostand an, um einen vorläufigen neuen Kontostand zu berechnen.
  2. Rufe den Guard mit dem vorläufigen Kontostand auf.
  3. Wenn der Guard einen von null verschiedenen Wert zurückgibt, übernimm: Der laufende Kontostand wird zum vorläufigen Kontostand.
  4. Wenn der Guard null zurückgibt, bleibt der laufende Kontostand unverändert und die Transaktion wird übersprungen.

Nachdem alle Transaktionen verarbeitet wurden, gib den endgültigen Kontostand zusammen mit der Anzahl der genehmigten Transaktionen zurück.

In Pseudocode berechnet 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

Angenommen zum Beispiel:

  1. add_interest ist eine Transaktion, die 5 addiert, und service_fee ist eine weitere Transaktion, die 2 abzieht
  2. at_least_10 ist ein Guard, der einen von null verschiedenen Wert zurückgibt, wenn der Kontostand >= 10 ist

Dann:

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

Für process_with_guard:

  • Das erste Argument ist eine nicht-negative 64-Bit-Ganzzahl (der Anfangskontostand).
  • Das zweite Argument ist die Speicheradresse eines Arrays von Transaktionen.
  • Das dritte Argument ist eine nicht-negative 64-Bit-Ganzzahl (die Array-Länge).
  • Das vierte Argument ist eine Guard-Funktion, die eine nicht-negative 64-Bit-Ganzzahl entgegennimmt und eine nicht-negative 64-Bit-Ganzzahl zurückgibt.
  • Die Rückgabewerte sind zwei nicht-negative 64-Bit-Ganzzahlen: der endgültige Kontostand in rax und die Anzahl der genehmigten Transaktionen in rdx.
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
x86-64 Assembly Exercism

Bereit, mit Buchhaltung 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.