In un concetto precedente, abbiamo visto che sia le etichette locali sia le funzioni non sono altro che indirizzi in una sezione di codice eseguibile, come section .text.
In effetti, le funzioni si possono manipolare come qualsiasi indirizzo di memoria: si possono caricare nei registri, passare in giro e salvare in memoria.
È anche possibile usare call o jmp per trasferire l'esecuzione a una funzione memorizzata in un registro o in memoria:
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
Un indirizzo di funzione che viene passato in giro come un valore si chiama thunk. I thunk sono uno degli elementi costitutivi della programmazione di ordine superiore in assembly: codice che opera su altro codice.
Gli indirizzi di funzione si possono anche memorizzare e recuperare più tardi:
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 scrive l'indirizzo di funzione che riceve in cached_fn.
Il valore resta lì anche dopo che save_op termina, quindi ogni chiamata successiva ad apply_op salta in coda all'indirizzo memorizzato per ultimo.
Questo permette di cambiare quale funzione apply_op chiama a runtime.
Memorizzare gli indirizzi di funzione in un array permette di selezionare funzioni diverse in base a un indice, che può dipendere da una condizione determinata a runtime. Questa si chiama tabella di dispatch:
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
Un thunk che legge o aggiorna una memoria persistente tra una chiamata e l'altra può comportarsi in modo diverso a seconda di ciò che è avvenuto prima. Il suo risultato può dipendere da qualcosa di più dei soli argomenti.
Per esempio, un contatore che prende una funzione e la chiama con il conteggio attuale, incrementando il conteggio ogni volta:
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 chiama la funzione ricevuta con il conteggio attuale come argomento, poi incrementa il conteggio.
Quindi una prima chiamata tick(square) chiama square(0), la chiamata successiva tick(square) chiama square(1), quella dopo square(2), e così via.
Un altro esempio è una computazione ritardata:
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 prende una funzione e un valore, li memorizza e restituisce invoke.
Quando invoke viene chiamata, esegue la funzione catturata con l'argomento salvato.
Molti dei pattern comuni nei linguaggi di livello superiore, come le callback, i metodi virtuali, i generatori, il currying, la composizione di funzioni e molti altri, si basano su thunk abbinati a uno stato persistente.
Sei il contabile di una piccola banca di paese. Ogni cliente ha un conto, e tu ne tieni il saldo nel registro. Nel corso dell'anno, a questi saldi vengono applicate delle transazioni: si accredita l'interesse, si detraggono le commissioni, si pagano i bonus, si addebitano le penali. Ogni transazione prende un saldo e ne produce uno nuovo.
Hai quattro compiti.
Puoi dare per scontato che ogni thunk (transazioni e guardie) in questo esercizio sia una funzione che:
Il cassiere apprende una nuova transazione all'inizio della giornata e la annota, in modo che possa applicarla in seguito quando arriva un cliente.
Definisci due funzioni:
remember_transaction prende una transazione e la salva in memoria.apply_remembered prende un saldo e vi applica la transazione memorizzata in precedenza.Esempio, supponendo che add_interest sia una transazione che accredita cinque unità di interesse:
remember_transaction(add_interest);
apply_remembered(100);
// => 105
remember_transaction(service_fee);
apply_remembered(100);
// => 98 (assuming service_fee deducts 2)
Per remember_transaction:
Per apply_remembered:
Il manuale della banca contiene un elenco di transazioni frequenti, memorizzato in una tabella di dispatch. Ogni filiale mantiene la propria copia dell'elenco e può registrare transazioni diverse a seconda delle politiche locali.
Definisci due funzioni che operano su una tabella di dispatch fornita dal chiamante:
register_transaction prende l'indirizzo di memoria di una tabella di dispatch, un indice e una transazione.
Memorizza questa transazione all'indice indicato nella tabella.select_transaction prende l'indirizzo di memoria di una tabella di dispatch, un indice e un saldo.
Cerca la transazione all'indice indicato e la applica al saldo, restituendo il nuovo saldo.select_transaction dovrebbe raggiungere la transazione cercata con un'unica chiamata in coda indiretta.
Esempio, supponendo che manual sia l'indirizzo di memoria di una tabella di dispatch con quattro slot vuoti:
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
Per register_transaction:
Per select_transaction:
Alla fine del mese, il conto di un cliente viene riconciliato. Ogni transazione avvenuta nel corso del mese viene applicata al saldo iniziale, una dopo l'altra, e il risultato è il nuovo saldo.
Definisci una funzione process_statement che prende un saldo iniziale, l'indirizzo di memoria di un array di transazioni e il numero di transazioni nell'array.
Per ogni transazione, in ordine, deve applicare la transazione al saldo corrente, poi usare il risultato come saldo per la transazione successiva.
Il saldo finale viene restituito.
In pseudocodice, process_statement(balance, transactions, n) calcola:
for each transaction in transactions:
balance = transaction(balance)
return balance
Esempio, supponendo che transactions sia l'indirizzo di memoria di un array contenente le transazioni add_interest, service_fee e add_interest, in quest'ordine, dove add_interest aggiunge 5 e service_fee detrae 2:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
Il primo argomento è un intero non negativo a 64 bit. Il secondo argomento è l'indirizzo di memoria di un array di transazioni. Il terzo argomento è un intero non negativo a 64 bit (la lunghezza dell'array). Il valore restituito è un intero non negativo a 64 bit.
Le politiche della banca richiedono che alcune transazioni vengano controllate prima di essere confermate. Una guardia è una funzione che esamina un saldo proposto e decide se è accettabile. Questa funzione di guardia restituisce un valore diverso da zero per approvare, oppure zero per rifiutare.
Definisci process_with_guard, che prende un saldo iniziale, l'indirizzo di memoria di un array di transazioni, il numero di transazioni nell'array e una funzione di guardia.
Per ogni transazione, in ordine:
Dopo aver elaborato tutte le transazioni, restituisci il saldo finale insieme al numero di transazioni approvate.
In pseudocodice, process_with_guard(balance, transactions, n, guard) calcola:
approved = 0
for each transaction in transactions:
tentative = transaction(balance)
if guard(tentative) is non-zero:
balance = tentative
approved = approved + 1
return balance, approved
Per esempio, supponi che:
add_interest sia una transazione che aggiunge 5 e service_fee sia un'altra transazione che detrae 2at_least_10 sia una guardia che restituisce un valore diverso da zero quando il saldo è >= 10Allora:
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
Per process_with_guard:
rax e il numero di transazioni approvate in rdx.Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.