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.
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.
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
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.
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.
Tu peux supposer que chaque thunk (transaction et garde) de cet exercice est une fonction qui :
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 :
Pour apply_remembered :
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 :
Pour select_transaction :
À 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.
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 :
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 :
add_interest est une transaction qui ajoute 5 et service_fee est une autre transaction qui déduit 2at_least_10 est une garde qui renvoie une valeur non nulle quand le solde est supérieur ou égal à 10Alors :
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 :
rax, et le nombre de transactions approuvées dans rdx.Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.