Contabilidade

Contabilidade

Exercício de aprendizagem

Introdução

Thunks

Num conceito anterior, foi referido que tanto os rótulos locais como as funções são apenas endereços numa secção com código executável, como a section .text.

Com efeito, as funções podem ser manipuladas da mesma forma que qualquer endereço de memória, ou seja, podem ser carregadas para registos, passadas adiante e guardadas na memória. Também é possível usar call ou jmp para transferir a execução para uma função guardada num registo ou na memória:

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

Um endereço de função que é passado adiante como um valor chama-se thunk. Os thunks são um bloco de construção da programação de ordem superior em assembly: código que opera sobre outro código.

Código como dados

Os endereços de funções também podem ser guardados na memória e recuperados mais tarde:

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

O save_op escreve o endereço de função que recebe em cached_fn. O valor persiste depois de o save_op devolver, por isso qualquer chamada posterior ao apply_op salta em cauda para o último endereço que ficou guardado. Isto torna possível mudar qual a função que o apply_op invoca em tempo de execução.

Tabelas de despacho

Guardar endereços de funções num array torna possível selecionar funções diferentes de acordo com um índice, possivelmente dependente de uma condição de tempo de execução. A isto chama-se uma tabela de despacho:

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

Thunks com estado

Um thunk que lê ou atualiza alguma memória persistente entre chamadas pode comportar-se de forma diferente consoante o que aconteceu antes. O seu resultado pode depender de mais do que apenas os seus argumentos.

Por exemplo, um contador que recebe uma função e a invoca com a contagem atual, avançando a contagem de cada vez:

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

O tick invoca a função dada com a contagem atual como argumento e depois avança a contagem. Assim, uma primeira chamada tick(square) invoca square(0), a chamada seguinte tick(square) invoca square(1), a seguinte square(2), e por aí adiante.

Outro exemplo seria uma computação diferida:

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

O delay recebe uma função e um valor, guarda-os e devolve o invoke. Quando o invoke é chamado, executa a função capturada com o argumento guardado.

Muitos dos padrões comuns em linguagens de nível superior, como callbacks, métodos virtuais, geradores, currying, composição de funções e muitos outros, assentam em thunks emparelhados com estado persistente.

Instruções

És o guarda-livros de um pequeno banco de aldeia. Cada cliente tem uma conta e tu guardas o respetivo saldo no teu livro-razão. Ao longo do ano, são aplicadas transações a esses saldos: creditam-se juros, deduzem-se comissões, pagam-se bónus, cobram-se penalizações. Cada transação recebe um saldo e produz um novo.

Tens quatro tarefas.

Note

Podes assumir que cada thunk (transações e guardas) deste exercício é uma função que:

  1. recebe como argumento um inteiro não negativo de 64 bits
  2. e devolve também um inteiro não negativo de 64 bits.

1. Memorizar uma transação

No início do dia, o caixa fica a conhecer uma nova transação e anota-a, para que possa ser aplicada mais tarde, quando chegar um cliente.

Define duas funções:

  • remember_transaction recebe uma transação e guarda-a em memória.
  • apply_remembered recebe um saldo e aplica-lhe a transação guardada anteriormente.

Exemplo, assumindo que add_interest é uma transação que credita cinco unidades de juros:

remember_transaction(add_interest);
apply_remembered(100);
// => 105

remember_transaction(service_fee);
apply_remembered(100);
// => 98   (assuming service_fee deducts 2)

Para remember_transaction:

  • O argumento é uma transação a guardar para uso posterior.
  • Não há valor devolvido.

Para apply_remembered:

  • O argumento é um inteiro não negativo de 64 bits.
  • O valor devolvido é um inteiro não negativo de 64 bits.

2. O manual do banco

O manual do banco tem uma lista de transações frequentes guardada numa tabela de despacho. Cada agência mantém a sua própria cópia da lista e pode registar transações diferentes, consoante a política local.

Define duas funções que operam sobre uma tabela de despacho fornecida por quem chama:

  • register_transaction recebe o endereço de memória de uma tabela de despacho, um índice e uma transação. Guarda essa transação no índice indicado da tabela.
  • select_transaction recebe o endereço de memória de uma tabela de despacho, um índice e um saldo. Procura a transação no índice indicado, aplica-a ao saldo e devolve o novo saldo.

select_transaction deve chegar à transação encontrada com uma única chamada de cauda indireta.

Exemplo, assumindo que manual é o endereço de memória de uma tabela de despacho com quatro fendas vazias:

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

Para register_transaction:

  • O primeiro argumento é o endereço de memória de uma tabela de despacho.
  • O segundo argumento é um inteiro não negativo de 64 bits (o índice).
  • O terceiro argumento é uma transação.
  • Não há valor devolvido.

Para select_transaction:

  • O primeiro argumento é o endereço de memória de uma tabela de despacho.
  • O segundo argumento é um inteiro não negativo de 64 bits (o índice).
  • O terceiro argumento é um inteiro não negativo de 64 bits (o saldo).
  • O valor devolvido é um inteiro não negativo de 64 bits.

3. Processar um extrato mensal

No final do mês, a conta de um cliente é reconciliada. Todas as transações que ocorreram durante o mês são aplicadas ao saldo inicial, uma a seguir à outra, e o resultado é o novo saldo.

Define uma função process_statement que recebe um saldo inicial, o endereço de memória de um array de transações e o número de transações do array. Para cada transação, por ordem, deve aplicar a transação ao saldo corrente e usar depois o resultado como saldo da transação seguinte. O saldo final é devolvido.

Em pseudocódigo, process_statement(balance, transactions, n) calcula:

for each transaction in transactions:
    balance = transaction(balance)
return balance

Exemplo, assumindo que transactions é o endereço de memória de um array que contém as transações add_interest, service_fee e add_interest, por essa ordem, em que add_interest acrescenta 5 e service_fee deduz 2:

process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105)  = 103
// add_interest(103) = 108
// => 108

O primeiro argumento é um inteiro não negativo de 64 bits. O segundo argumento é o endereço de memória de um array de transações. O terceiro argumento é um inteiro não negativo de 64 bits (o comprimento do array). O valor devolvido é um inteiro não negativo de 64 bits.

4. Processar com uma guarda

A política do banco exige que certas transações sejam verificadas antes de serem efetivadas. Uma guarda é uma função que inspeciona um saldo proposto e decide se é aceitável. Essa função de guarda devolve um valor diferente de zero para aprovar, ou zero para rejeitar.

Define process_with_guard, que recebe um saldo inicial, o endereço de memória de um array de transações, o número de transações do array e uma função de guarda. Para cada transação, por ordem:

  1. Aplica a transação ao saldo corrente para calcular um novo saldo provisório.
  2. Chama a guarda com o saldo provisório.
  3. Se a guarda devolver um valor diferente de zero, efetiva: o saldo corrente passa a ser o saldo provisório.
  4. Se a guarda devolver zero, o saldo corrente mantém-se e a transação é ignorada.

Depois de processares todas as transações, devolve o saldo final juntamente com o número de transações aprovadas.

Em pseudocódigo, process_with_guard(balance, transactions, n, guard) calcula:

approved = 0
for each transaction in transactions:
    tentative = transaction(balance)
    if guard(tentative) is non-zero:
        balance = tentative
        approved = approved + 1
return balance, approved

Por exemplo, assume que:

  1. add_interest é uma transação que acrescenta 5 e service_fee é outra transação que deduz 2
  2. at_least_10 é uma guarda que devolve um valor diferente de zero quando o saldo é >= 10

Então:

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

Para process_with_guard:

  • O primeiro argumento é um inteiro não negativo de 64 bits (o saldo inicial).
  • O segundo argumento é o endereço de memória de um array de transações.
  • O terceiro argumento é um inteiro não negativo de 64 bits (o comprimento do array).
  • O quarto argumento é uma função de guarda que recebe um inteiro não negativo de 64 bits e devolve um inteiro não negativo de 64 bits.
  • Os valores devolvidos são dois inteiros não negativos de 64 bits: o saldo final em rax e o número de transações aprovadas em rdx.
Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Contabilidade?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.