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.
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.
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
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.
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.
Du kannst davon ausgehen, dass jeder Thunk (Transaktionen und Guards) in dieser Übung eine Funktion ist, die:
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:
Für apply_remembered:
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:
Für select_transaction:
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.
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:
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:
add_interest ist eine Transaktion, die 5 addiert, und service_fee ist eine weitere Transaktion, die 2 abziehtat_least_10 ist ein Guard, der einen von null verschiedenen Wert zurückgibt, wenn der Kontostand >= 10 istDann:
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:
rax und die Anzahl der genehmigten Transaktionen in rdx.Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.