Contabilidad

Contabilidad

Ejercicio de aprendizaje

Introducción

Thunks

En un concepto anterior, se mencionó que tanto las etiquetas locales como las funciones son solo direcciones en una sección con código ejecutable, como section .text.

De hecho, las funciones se pueden manipular igual que cualquier dirección de memoria, es decir, se pueden cargar en registros, pasar de un lado a otro y almacenar en memoria. También es posible usar call o jmp para transferir la ejecución a una función almacenada en un registro o en 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

Una dirección de función que se pasa como un valor se llama thunk. Los thunks son un componente fundamental de la programación de orden superior en ensamblador: código que opera sobre otro código.

El código como datos

Las direcciones de funciones también se pueden almacenar en memoria y recuperar más 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

save_op escribe la dirección de la función que recibe en cached_fn. El valor persiste después de que save_op devuelve, así que cualquier llamada posterior a apply_op hace un salto de cola a la dirección que se haya almacenado más recientemente. Esto hace posible cambiar qué función invoca apply_op en tiempo de ejecución.

Tablas de despacho

Almacenar direcciones de funciones en un array hace posible seleccionar distintas funciones según algún índice, que puede depender de una condición en tiempo de ejecución. Esto se llama una tabla 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 con estado

Un thunk que lee o actualiza alguna memoria persistente entre llamadas puede comportarse de forma distinta según lo que ocurrió antes. Su resultado puede depender de algo más que sus propios argumentos.

Por ejemplo, un contador que recibe una función y la invoca con el conteo actual, incrementando el conteo 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 la función dada con el conteo actual como su argumento y luego incrementa el conteo. Así, una primera llamada tick(square) invoca a square(0), la siguiente llamada tick(square) invoca a square(1), la siguiente square(2), y así sucesivamente.

Otro ejemplo sería una computación 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

delay recibe una función y un valor, los almacena y devuelve invoke. Cuando se llama a invoke, ejecuta la función capturada con el argumento guardado.

Muchos de los patrones comunes en lenguajes de nivel superior, como callbacks, métodos virtuales, generadores, currying, composición de funciones y muchos otros, se construyen sobre thunks combinados con estado persistente.

Instrucciones

Eres quien lleva la contabilidad en un pequeño banco de aldea. Cada cliente tiene una cuenta, y llevas su saldo en tu libro mayor. A lo largo del año, se aplican transacciones a estos saldos: se abonan intereses, se descuentan comisiones, se pagan bonos, se cobran penalizaciones. Cada transacción toma un saldo y produce uno nuevo.

Tienes cuatro tareas.

Note

Puedes asumir que cada thunk (transacciones y guardas) en este ejercicio es una función que:

  1. toma como argumento un entero no negativo de 64 bits
  2. y además devuelve un entero no negativo de 64 bits.

1. Recordar una transacción

El cajero conoce una nueva transacción al comienzo del día y la anota para poder aplicarla más tarde, cuando llegue un cliente.

Define dos funciones:

  • remember_transaction toma una transacción y la guarda en memoria.
  • apply_remembered toma un saldo y le aplica la transacción guardada previamente.

Por ejemplo, suponiendo que add_interest es una transacción que abona cinco unidades de interés:

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

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

Para remember_transaction:

  • El argumento es una transacción que se guardará para usarla más tarde.
  • No hay valor de retorno.

Para apply_remembered:

  • El argumento es un entero no negativo de 64 bits.
  • El valor de retorno es un entero no negativo de 64 bits.

2. El manual del banco

El manual del banco tiene una lista de transacciones frecuentes guardada en una tabla de despacho. Cada sucursal mantiene su propia copia de la lista y puede registrar transacciones distintas según su política local.

Define dos funciones que operan sobre una tabla de despacho que reciben de quien llama:

  • register_transaction toma la dirección de memoria de una tabla de despacho, un índice y una transacción. Guarda esta transacción en el índice indicado de la tabla.
  • select_transaction toma la dirección de memoria de una tabla de despacho, un índice y un saldo. Busca la transacción en el índice indicado y la aplica al saldo, devolviendo el nuevo saldo.

select_transaction debe llegar a la transacción buscada con una sola llamada de cola indirecta.

Por ejemplo, suponiendo que manual es la dirección de memoria de una tabla de despacho con cuatro espacios vacíos:

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:

  • El primer argumento es la dirección de memoria de una tabla de despacho.
  • El segundo argumento es un entero no negativo de 64 bits (el índice).
  • El tercer argumento es una transacción.
  • No hay valor de retorno.

Para select_transaction:

  • El primer argumento es la dirección de memoria de una tabla de despacho.
  • El segundo argumento es un entero no negativo de 64 bits (el índice).
  • El tercer argumento es un entero no negativo de 64 bits (el saldo).
  • El valor de retorno es un entero no negativo de 64 bits.

3. Procesar un estado de cuenta mensual

Al final del mes, se concilia la cuenta de un cliente. Cada transacción que ocurrió durante el mes se aplica al saldo inicial, una tras otra, y el resultado es el nuevo saldo.

Define una función process_statement que toma un saldo inicial, la dirección de memoria de un array de transacciones y el número de transacciones del array. Para cada transacción, en orden, debe aplicarla al saldo acumulado y luego usar el resultado como saldo para la siguiente transacción. Se devuelve el saldo final.

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

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

Por ejemplo, suponiendo que transactions es la dirección de memoria de un array que contiene las transacciones add_interest, service_fee y add_interest en ese orden, donde add_interest suma 5 y service_fee descuenta 2:

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

El primer argumento es un entero no negativo de 64 bits. El segundo argumento es la dirección de memoria de un array de transacciones. El tercer argumento es un entero no negativo de 64 bits (la longitud del array). El valor de retorno es un entero no negativo de 64 bits.

4. Procesar con una guarda

La política del banco exige que ciertas transacciones se verifiquen antes de confirmarse. Una guarda es una función que inspecciona un saldo propuesto y decide si es aceptable. Esta función de guarda devuelve un valor distinto de cero para aprobar, o cero para rechazar.

Define process_with_guard, que toma un saldo inicial, la dirección de memoria de un array de transacciones, el número de transacciones del array y una función de guarda. Para cada transacción, en orden:

  1. Aplica la transacción al saldo acumulado para calcular un nuevo saldo tentativo.
  2. Llama a la guarda con el saldo tentativo.
  3. Si la guarda devuelve un valor distinto de cero, confirma: el saldo acumulado pasa a ser el saldo tentativo.
  4. Si la guarda devuelve cero, el saldo acumulado no cambia y se omite la transacción.

Cuando se hayan procesado todas las transacciones, devuelve el saldo final junto con el número de transacciones aprobadas.

En 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 ejemplo, supón que:

  1. add_interest es una transacción que suma 5 y service_fee es otra transacción que descuenta 2
  2. at_least_10 es una guarda que devuelve un valor distinto de cero cuando el saldo es >= 10

Entonces:

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:

  • El primer argumento es un entero no negativo de 64 bits (el saldo inicial).
  • El segundo argumento es la dirección de memoria de un array de transacciones.
  • El tercer argumento es un entero no negativo de 64 bits (la longitud del array).
  • El cuarto argumento es una función de guarda que toma un entero no negativo de 64 bits y devuelve un entero no negativo de 64 bits.
  • Los valores de retorno son dos enteros no negativos de 64 bits: el saldo final en rax, y el número de transacciones aprobadas en rdx.
Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Contabilidad?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.