Contabilidade

Contabilidade

Exercício de aprendizagem

Introdução

Thunks

Em um conceito anterior, foi mencionado que tanto rótulos locais quanto funções são apenas endereços em uma seção com código executável, como section .text.

Na verdade, funções podem ser manipuladas da mesma forma que qualquer endereço de memória, ou seja, podem ser carregadas em registradores, passadas adiante e armazenadas na memória. Também é possível usar call ou jmp para transferir a execução para uma função armazenada em um registrador 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 é chamado de thunk. Thunks são um bloco de construção da programação de ordem superior em assembly: código que opera sobre outros códigos.

Código como dados

Endereços de função também podem ser armazenados na memória e recuperados depois:

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 escreve o endereço de função que recebe em cached_fn. O valor persiste depois que save_op retorna, então qualquer chamada posterior a apply_op faz um salto de cauda para o endereço armazenado por último. Isso torna possível mudar qual função apply_op invoca em tempo de execução.

Tabelas de despacho

Armazenar endereços de função em um array torna possível selecionar funções diferentes de acordo com algum índice, possivelmente dependente de uma condição de tempo de execução. Isso é chamado de 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 se comportar de forma diferente dependendo do que veio antes. Seu resultado pode depender de mais do que apenas seus argumentos.

Por exemplo, um contador que recebe uma função e a invoca com a contagem atual, avançando a contagem a 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

tick invoca a função dada com a contagem atual como argumento, e então avança a contagem. Assim, uma primeira chamada tick(square) invoca square(0), a próxima chamada tick(square) invoca square(1), a seguinte square(2), e assim por diante.

Outro exemplo seria uma computação adiada:

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 recebe uma função e um valor, armazena ambos e retorna invoke. Quando invoke é chamado, ele executa a função capturada com o argumento salvo.

Muitos dos padrões comuns em linguagens de nível mais alto, como callbacks, métodos virtuais, geradores, currying, composição de funções e muitos outros, baseiam-se em thunks combinados com estado persistente.

Instruções

Você é o guarda-livros de um pequeno banco de vila. Cada cliente tem uma conta, e você mantém o saldo dela no seu livro-razão. Ao longo do ano, transações são aplicadas a esses saldos: juros são creditados, taxas são descontadas, bônus são pagos, penalidades são cobradas. Toda transação recebe um saldo e produz um novo.

Você tem quatro tarefas.

Note

Você pode assumir que todo thunk (transações e guardas) neste exercício é uma função que:

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

1. Memorize uma transação

O caixa fica sabendo de uma nova transação no início do dia e a anota, para que ela possa ser aplicada mais tarde, quando um cliente chegar.

Defina duas funções:

  • remember_transaction recebe uma transação e a armazena na memória.
  • apply_remembered recebe um saldo e aplica a ele a transação armazenada anteriormente.

Exemplo, supondo 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 ser guardada para uso posterior.
  • Não há valor de retorno.

Para apply_remembered:

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

2. O manual do banco

O manual do banco tem uma lista de transações frequentes armazenada em uma tabela de despacho. Cada agência mantém a própria cópia da lista e pode registrar transações diferentes conforme a política local.

Defina 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. Ela armazena 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. Ela busca a transação no índice indicado e a aplica ao saldo, retornando o novo saldo.

select_transaction deve alcançar a transação buscada com uma única chamada de cauda indireta.

Exemplo, supondo que manual é o endereço de memória de uma tabela de despacho com quatro posições 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 de retorno.

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 de retorno é um inteiro não negativo de 64 bits.

3. Processe um extrato mensal

No fim do mês, a conta de um cliente é conciliada. Todas as transações que ocorreram ao longo do mês são aplicadas ao saldo inicial, uma após a outra, e o resultado é o novo saldo.

Defina 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 no array. Para cada transação, em sequência, ela deve aplicar a transação ao saldo corrente e então usar o resultado como o saldo da próxima transação. O saldo final é retornado.

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

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

Exemplo, supondo que transactions é o endereço de memória de um array que contém as transações add_interest, service_fee e add_interest, nessa ordem, em que add_interest soma 5 e service_fee desconta 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 de retorno é um inteiro não negativo de 64 bits.

4. Processe com um guarda

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

Defina 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 no array e uma função de guarda. Para cada transação, em sequência:

  1. Aplique a transação ao saldo corrente para calcular um novo saldo provisório.
  2. Chame o guarda com o saldo provisório.
  3. Se o guarda retornar um valor diferente de zero, efetive: o saldo corrente passa a ser o saldo provisório.
  4. Se o guarda retornar zero, o saldo corrente permanece igual e a transação é ignorada.

Depois de processar todas as transações, retorne o saldo final junto 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, suponha que:

  1. add_interest é uma transação que soma 5 e service_fee é outra transação que desconta 2
  2. at_least_10 é um guarda que retorna 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 retorna um inteiro não negativo de 64 bits.
  • Os valores de retorno são dois inteiros não negativos de 64 bits: o saldo final em rax e a contagem de transações aprovadas em rdx.
Editar via GitHub O link abre em uma nova janela ou aba
x86-64 Assembly Exercism

Tudo pronto para começar Contabilidade?

Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.