在前面的某个概念里我们提到过,局部标签和函数都只是可执行代码段中的地址,比如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。thunk 是汇编中高阶编程的基本构件:让代码去操作其他代码。
函数地址也可以存进内存,之后再取出来使用:
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的调用,都会尾跳转到最近一次存入的那个地址。这样一来,就能在运行时改变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时,它会用保存下来的实参运行那个被捕获的函数。
高级语言中常见的许多模式,比如回调、虚方法、生成器、柯里化、函数组合等等,都建立在 thunk 与持久状态配对的基础上。
你是一家小村庄银行的簿记员。 每位客户都持有一个账户,你把它的余额记在账簿里。 一年之中,各种交易会作用到这些余额上:利息被计入,手续费被扣除,奖金被发放,罚金被收取。 每一笔交易都接收一个余额,并产生一个新的余额。
你有 4 个任务。
你可以假设本练习中的每个 thunk(交易和守卫)都是这样的函数:
出纳员在一天开始时得知一笔新交易,把它记下来,以便之后客户到来时可以应用。
定义 2 个函数:
remember_transaction接收一笔交易,并将它存储在内存中。apply_remembered接收一个余额,并把之前存储的交易应用到它上面。示例,假设add_interest是一笔计入 5 个单位利息的交易:
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:
银行的手册里有一份常用交易列表,存储在一张_分派表_中。 每个分行都维护着自己的一份列表副本,并且可以根据当地政策登记不同的交易。
定义 2 个函数,它们操作由调用方提供的分派表:
register_transaction接收一张分派表的内存地址、一个下标和一笔交易。
它把这笔交易存储在表中的指定下标处。select_transaction接收一张分派表的内存地址、一个下标和一个余额。
它查找指定下标处的交易,把它应用到余额上,并返回新的余额。select_transaction应当通过一次间接尾调用抵达所查找到的交易。
示例,假设manual是一张有 4 个空槽的分派表的内存地址:
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 位非负整数。 第二个实参是一个交易数组的内存地址。 第三个实参是一个 64 位非负整数(数组长度)。 返回值是一个 64 位非负整数。
银行的政策要求某些交易在被提交之前先接受检查。 守卫是一个函数,它检查一个拟定中的余额,并判断它是否可以接受。 这个守卫函数返回非零值表示批准,返回零表示拒绝。
定义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是另一笔减 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:
rax中,已批准交易的数量在rdx中。