Comptabilité

Comptabilité

Exercice d'apprentissage

Introduction

Thunks

Dans un concept précédent, on a mentionné que les étiquettes locales et les fonctions ne sont que des adresses dans une section de code exécutable, comme section .text.

En fait, les fonctions peuvent être manipulées de la même manière que n'importe quelle adresse mémoire : elles peuvent être chargées dans des registres, transmises çà et là et stockées en mémoire. Il est également possible d'utiliser call ou jmp pour transférer l'exécution vers une fonction stockée dans un registre ou en mémoire :

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

Une adresse de fonction transmise comme valeur est appelée thunk. Les thunks sont une brique de base de la programmation d'ordre supérieur en assembleur : du code qui agit sur d'autres codes.

Le code comme donnée

Les adresses de fonctions peuvent aussi être stockées en mémoire et récupérées plus tard :

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 écrit l'adresse de la fonction qu'elle reçoit dans cached_fn. La valeur persiste après le retour de save_op, donc tout appel ultérieur à apply_op effectue un saut de queue vers l'adresse stockée en dernier. Cela permet de changer la fonction que apply_op appelle à l'exécution.

Tables de dispatch

Stocker des adresses de fonctions dans un tableau permet de sélectionner différentes fonctions selon un indice, éventuellement dépendant d'une condition à l'exécution. C'est ce qu'on appelle une table de 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

Thunks à état

Un thunk qui lit ou met à jour une mémoire persistante entre les appels peut se comporter différemment selon ce qui s'est produit avant. Son résultat peut dépendre d'autre chose que de ses seuls arguments.

Par exemple, un compteur qui prend une fonction et l'appelle avec le compte actuel, en incrémentant le compte à chaque fois :

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 invoque la fonction donnée avec le compte actuel comme argument, puis incrémente le compte. Ainsi, un premier appel tick(square) invoque square(0), l'appel suivant tick(square) invoque square(1), le suivant square(2), et ainsi de suite.

Un autre exemple serait un calcul différé :

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 prend une fonction et une valeur, les stocke et renvoie invoke. Quand invoke est appelée, elle exécute la fonction capturée avec l'argument sauvegardé.

Beaucoup de motifs courants dans les langages de haut niveau, comme les fonctions de rappel, les méthodes virtuelles, les générateurs, la curryfication, la composition de fonctions, et bien d'autres, reposent sur des thunks associés à un état persistant.

Instructions

Tu es le comptable d'une petite banque de village. Chaque client possède un compte, et tu gardes son solde dans ton grand livre. Tout au long de l'année, des transactions sont appliquées à ces soldes : des intérêts sont crédités, des frais sont déduits, des bonus sont versés, des pénalités sont prélevées. Chaque transaction prend un solde et en produit un nouveau.

Tu as quatre tâches.

Note

Tu peux supposer que chaque thunk (transaction et garde) de cet exercice est une fonction qui :

  1. prend un entier non négatif de 64 bits en argument
  2. et renvoie aussi un entier non négatif de 64 bits.

1. Mémorise une transaction

Le guichetier apprend une nouvelle transaction au début de la journée et la note par écrit afin qu'elle puisse être appliquée plus tard, quand un client se présente.

Définis deux fonctions :

  • remember_transaction prend une transaction et la stocke en mémoire.
  • apply_remembered prend un solde et lui applique la transaction stockée précédemment.

Exemple, en supposant que add_interest soit une transaction qui crédite cinq unités d'intérêts :

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

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

Pour remember_transaction :

  • L'argument est une transaction à enregistrer pour un usage ultérieur.
  • Il n'y a pas de valeur de retour.

Pour apply_remembered :

  • L'argument est un entier non négatif de 64 bits.
  • La valeur de retour est un entier non négatif de 64 bits.

2. Le manuel de la banque

Le manuel de la banque contient une liste de transactions fréquentes, stockée dans une dispatch table. Chaque agence conserve sa propre copie de la liste, et peut enregistrer des transactions différentes selon la politique locale.

Définis deux fonctions qui opèrent sur une dispatch table fournie par l'appelant :

  • register_transaction prend l'adresse mémoire d'une dispatch table, un indice et une transaction. Elle stocke cette transaction à l'indice donné dans la table.
  • select_transaction prend l'adresse mémoire d'une dispatch table, un indice et un solde. Elle recherche la transaction à l'indice donné et l'applique au solde, puis renvoie le nouveau solde.

select_transaction doit atteindre la transaction trouvée au moyen d'un seul appel terminal indirect.

Exemple, en supposant que manual soit l'adresse mémoire d'une dispatch table comportant quatre emplacements vides :

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

Pour register_transaction :

  • Le premier argument est l'adresse mémoire d'une dispatch table.
  • Le deuxième argument est un entier non négatif de 64 bits (l'indice).
  • Le troisième argument est une transaction.
  • Il n'y a pas de valeur de retour.

Pour select_transaction :

  • Le premier argument est l'adresse mémoire d'une dispatch table.
  • Le deuxième argument est un entier non négatif de 64 bits (l'indice).
  • Le troisième argument est un entier non négatif de 64 bits (le solde).
  • La valeur de retour est un entier non négatif de 64 bits.

3. Traite un relevé mensuel

À la fin du mois, on procède au rapprochement du compte d'un client. Chaque transaction qui a eu lieu au cours du mois est appliquée au solde de départ, l'une après l'autre, et le résultat est le nouveau solde.

Définis une fonction process_statement qui prend un solde de départ, l'adresse mémoire d'un tableau de transactions et le nombre de transactions dans le tableau. Pour chaque transaction, dans l'ordre, elle doit appliquer la transaction au solde en cours, puis utiliser le résultat comme solde pour la transaction suivante. Le solde final est renvoyé.

En pseudo-code, process_statement(balance, transactions, n) calcule :

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

Exemple, en supposant que transactions soit l'adresse mémoire d'un tableau contenant les transactions add_interest, service_fee et add_interest, dans cet ordre, où add_interest ajoute 5 et service_fee déduit 2 :

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

Le premier argument est un entier non négatif de 64 bits. Le deuxième argument est l'adresse mémoire d'un tableau de transactions. Le troisième argument est un entier non négatif de 64 bits (la longueur du tableau). La valeur de retour est un entier non négatif de 64 bits.

4. Traite avec une garde

La politique de la banque exige que certaines transactions soient vérifiées avant d'être validées. Une garde est une fonction qui inspecte un solde proposé et décide s'il est acceptable. Cette fonction de garde renvoie une valeur non nulle pour approuver, ou zéro pour refuser.

Définis process_with_guard, qui prend un solde de départ, l'adresse mémoire d'un tableau de transactions, le nombre de transactions dans le tableau et une fonction de garde. Pour chaque transaction, dans l'ordre :

  1. Applique la transaction au solde en cours pour calculer un solde provisoire.
  2. Appelle la garde avec le solde provisoire.
  3. Si la garde renvoie une valeur non nulle, valide : le solde en cours devient le solde provisoire.
  4. Si la garde renvoie zéro, le solde en cours reste inchangé et la transaction est ignorée.

Une fois toutes les transactions traitées, renvoie le solde final ainsi que le nombre de transactions approuvées.

En pseudo-code, process_with_guard(balance, transactions, n, guard) calcule :

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

Par exemple, supposons que :

  1. add_interest est une transaction qui ajoute 5 et service_fee est une autre transaction qui déduit 2
  2. at_least_10 est une garde qui renvoie une valeur non nulle quand le solde est supérieur ou égal à 10

Alors :

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

Pour process_with_guard :

  • Le premier argument est un entier non négatif de 64 bits (le solde de départ).
  • Le deuxième argument est l'adresse mémoire d'un tableau de transactions.
  • Le troisième argument est un entier non négatif de 64 bits (la longueur du tableau).
  • Le quatrième argument est une fonction de garde qui prend un entier non négatif de 64 bits et renvoie un entier non négatif de 64 bits.
  • Les valeurs de retour sont deux entiers non négatifs de 64 bits : le solde final dans rax, et le nombre de transactions approuvées dans rdx.
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
x86-64 Assembly Exercism

Prêt à commencer Comptabilité ?

Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.