Contabilità

Contabilità

Esercizio di apprendimento

Introduzione

Thunk

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.

Il codice come dato

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.

Tabelle di dispatch

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

Thunk con stato

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.

Istruzioni

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.

Note

Puoi dare per scontato che ogni thunk (transazioni e guardie) in questo esercizio sia una funzione che:

  1. prende come argomento un intero non negativo a 64 bit
  2. e restituisce anch'essa un intero non negativo a 64 bit.

1. Ricordare una transazione

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:

  • L'argomento è una transazione da salvare per usarla più tardi.
  • Non c'è alcun valore restituito.

Per apply_remembered:

  • L'argomento è un intero non negativo a 64 bit.
  • Il valore restituito è un intero non negativo a 64 bit.

2. Il manuale della banca

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:

  • Il primo argomento è l'indirizzo di memoria di una tabella di dispatch.
  • Il secondo argomento è un intero non negativo a 64 bit (l'indice).
  • Il terzo argomento è una transazione.
  • Non c'è alcun valore restituito.

Per select_transaction:

  • Il primo argomento è l'indirizzo di memoria di una tabella di dispatch.
  • Il secondo argomento è un intero non negativo a 64 bit (l'indice).
  • Il terzo argomento è un intero non negativo a 64 bit (il saldo).
  • Il valore restituito è un intero non negativo a 64 bit.

3. Elaborare un estratto conto mensile

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.

4. Elaborare con una guardia

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:

  1. Applica la transazione al saldo corrente per calcolare un nuovo saldo provvisorio.
  2. Chiama la guardia con il saldo provvisorio.
  3. Se la guardia restituisce un valore diverso da zero, conferma: il saldo corrente diventa il saldo provvisorio.
  4. Se la guardia restituisce zero, il saldo corrente resta invariato e la transazione viene saltata.

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:

  1. add_interest sia una transazione che aggiunge 5 e service_fee sia un'altra transazione che detrae 2
  2. at_least_10 sia una guardia che restituisce un valore diverso da zero quando il saldo è >= 10

Allora:

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:

  • Il primo argomento è un intero non negativo a 64 bit (il saldo iniziale).
  • 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 quarto argomento è una funzione di guardia che prende un intero non negativo a 64 bit e restituisce un intero non negativo a 64 bit.
  • I valori restituiti sono due interi non negativi a 64 bit: il saldo finale in rax e il numero di transazioni approvate in rdx.
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
x86-64 Assembly Exercism

Vuoi iniziare Contabilità?

Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.