In a previous concept, it was mentioned that both local labels and functions are just addresses in a section with executable code, such as section .text.
In fact, functions can be manipulated in the same way as any memory address, i.e., they can be loaded into registers, passed around and stored in memory.
It is also possible to use call or jmp to transfer execution to a function stored in a register or memory:
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
A function address that is passed around as a value is called a thunk. Thunks are a building block of higher-order programming in assembly: code that operates on other code.
Function addresses can also be stored in memory and retrieved later:
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 writes the function address it receives into cached_fn.
The value persists after save_op returns, so any later call to apply_op tail-jumps to whatever address was last stored.
This makes it possible to change which function apply_op invokes at runtime.
Storing function addresses in an array makes it possible to select different functions according to some index, possibly dependent on a runtime condition. This is called a dispatch table:
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
A thunk that reads or updates some persistent memory between calls may behave differently depending on what came before. Its result may depend on more than its arguments alone.
For example, a counter that takes a function and invokes it with the current count, advancing the count each time:
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 invokes the given function with the current count as its argument, then advances the count.
So a first call tick(square) invokes square(0), the next call tick(square) invokes square(1), the next square(2), and so on.
Another example would be a delayed computation:
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 takes a function and a value, stores them, and returns invoke.
When invoke is called, it runs the captured function with the saved argument.
Many of the patterns common in higher-level languages, such as callbacks, virtual methods, generators, currying, function composition, and many others, build on thunks paired with persistent state.
You are the bookkeeper at a small village bank. Each customer holds an account, and you keep its balance in your ledger. Throughout the year, transactions are applied to these balances: interest is credited, fees are deducted, bonuses are paid out, penalties are charged. Every transaction takes a balance and produces a new one.
You have four tasks.
You may assume that every thunk (transactions and guards) in this exercise is a function that:
The teller learns a new transaction at the start of the day and writes it down so it can be applied later when a customer arrives.
Define two functions:
remember_transaction takes a transaction and stores it in memory.apply_remembered takes a balance and applies the previously-stored transaction to it.Example, assuming add_interest is a transaction that credits five units of interest:
remember_transaction(add_interest);
apply_remembered(100);
// => 105
remember_transaction(service_fee);
apply_remembered(100);
// => 98 (assuming service_fee deducts 2)
For remember_transaction:
For apply_remembered:
The bank's manual has a list of frequent transactions stored in a dispatch table. Each branch maintains its own copy of the list, and may register different transactions depending on local policy.
Define two functions that operate on a dispatch table supplied by the caller:
register_transaction takes the memory address of a dispatch table, an index, and a transaction.
It stores this transaction at the given index in the table.select_transaction takes the memory address of a dispatch table, an index, and a balance.
It looks up the transaction at the given index and applies it to the balance, returning the new balance.select_transaction should reach the looked-up transaction with a single indirect tail call.
Example, assuming manual is the memory address of a dispatch table with four empty slots:
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
For register_transaction:
For select_transaction:
At the end of the month, a customer's account is reconciled. Every transaction that took place over the month is applied to the starting balance, one after the other, and the result is the new balance.
Define a function process_statement that takes a starting balance, the memory address of an array of transactions, and the number of transactions in the array.
For each transaction in sequence, it should apply the transaction to the running balance, then use the result as the balance for the next transaction.
The final balance is returned.
In pseudocode, process_statement(balance, transactions, n) computes:
for each transaction in transactions:
balance = transaction(balance)
return balance
Example, assuming transactions is the memory address of an array containing the transactions add_interest, service_fee, and add_interest in that order, where add_interest adds 5 and service_fee deducts 2:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
The first argument is a 64-bit non-negative integer. The second argument is the memory address of an array of transactions. The third argument is a 64-bit non-negative integer (the array length). The return value is a 64-bit non-negative integer.
The bank's policy requires that certain transactions be checked before being committed. A guard is a function that inspects a proposed balance and decides whether it is acceptable. This guard function returns a non-zero value to approve, or zero to reject.
Define process_with_guard that takes a starting balance, the memory address of an array of transactions, the number of transactions in the array, and a guard function.
For each transaction in sequence:
After all transactions have been processed, return the final balance alongside the number of approved transactions.
In pseudocode, process_with_guard(balance, transactions, n, guard) computes:
approved = 0
for each transaction in transactions:
tentative = transaction(balance)
if guard(tentative) is non-zero:
balance = tentative
approved = approved + 1
return balance, approved
For example, assume that:
add_interest is a transaction that adds 5 and service_fee is another transaction that deducts 2at_least_10 is a guard that returns a non-zero value when the balance is >= 10Then:
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
For process_with_guard:
rax, and the count of approved transactions in rdx.Sign up to Exercism to learn and master x86-64 Assembly with 22 concepts130 exercises, and real human mentoring, all for free.