在先前的概念中提過,區域標籤和函式其實都只是可執行程式碼區段中的位址,例如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 與持續狀態的搭配之上。
你是小村莊銀行裡的記帳員。 每位客戶都有一個帳戶,你在帳本裡記錄其餘額。 一年之中,各種交易會套用到這些餘額上:利息會入帳,手續費會被扣除,獎金會發放,罰金會收取。 每一筆交易都會取一個餘額,並產生新的餘額。
你有四項任務。
你可以假設這個練習中的每個 thunk(交易與守衛)都是這樣的函式:
行員在一天開始時得知一筆新的交易,並把它寫下來,之後客戶上門時就能套用它。
定義兩個函式:
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:
銀行的手冊裡有一份常用交易的清單,存放在一張_分派表_中。 每個分行都各自保存這份清單的副本,並可能依當地政策登記不同的交易。
定義兩個函式,用來操作由呼叫端提供的分派表:
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:
關於 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 中已核准交易的數量。