Le code assembleur écrit à la main regorge de répétitions. La même petite suite d'instructions revient sans cesse, avec peu de variations, et les abstractions qui permettraient de réduire cette répétition sont rares. Et ce n'est pas tout : écrire en assembleur oblige à manipuler des valeurs sans sémantique évidente. Il n'y a pas de paramètres nommés, et la plupart des détails de la machine sous-jacente doivent être gérés à la main par qui programme.
NASM, l'assembleur utilisé par le parcours, propose quelques outils qui aident sur ces deux points. Ces outils s'exécutent au moment de l'assemblage, avant qu'aucune instruction n'atteigne le processeur. Ils indiquent à l'assembleur comment réécrire le texte source avant même qu'il ne soit traduit en code machine.
Le plus important de ces outils est la macro, que l'on peut voir comme une recette destinée à l'assembleur. Une macro est développée là où on l'utilise, comme si le code y avait été écrit à la main.
Une macro peut prendre un certain nombre de jetons littéraux en paramètres, ce qui la rend réutilisable dans différents contextes.
En revanche, ce ne sont pas des fonctions : il n'y a pas de call, pas de ret, aucun transfert de contrôle vers un autre point du code.
La définition la plus simple donne un nom à une valeur ou à une courte expression avec %define :
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Chaque occurrence ultérieure de WIDTH est remplacée par 8 avant l'assemblage.
Le remplacement est textuel : le nom est substitué partout où il apparaît, que ce soit dans un opérande ou à l'intérieur d'une autre définition.
Pour les constantes numériques, préfère equ plutôt que %define.
Une macro sur une ligne déclarée avec %define ne se développe qu'au moment où on l'utilise, et peut être réaffectée ou indéfinie.
Une constante equ, à l'inverse, est évaluée à l'endroit de sa définition et n'est jamais modifiée.
Un %define peut aussi prendre des paramètres nommés, et se comporte alors comme une petite fonction en ligne :
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
Mets des parenthèses autour du corps entier et de chaque paramètre.
Avec %define square(x) x*x, l'appel square(1+1) se développe en 1+1*1+1, ce qui donne 3.
Écrit sous la forme %define square(x) ((x)*(x)), le même appel se développe en ((1+1)*(1+1)), ce qui donne 4.
Les noms définis avec %define sont sensibles à la casse.
La variante %idefine rend le nom insensible à la casse, si on le souhaite.
Les macros sur une ligne sont utiles pour donner du sens à différentes parties du code. Utilisées avec discernement, elles améliorent la lisibilité et réduisent le risque de fautes de frappe. Par exemple, un registre qui joue un rôle précis dans une fonction peut être défini par ce rôle :
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Toute une séquence de code, quel que soit le nombre de lignes, se capture avec %macro et se termine par %endmacro.
Un nombre après le nom indique combien d'arguments la macro prend, ce qui peut être zéro.
Lorsqu'on passe des arguments, ils sont séparés par une virgule (,) :
%macro example_macro 0
...
%endmacro
Les arguments des macros sur plusieurs lignes ne sont pas nommés.
On y fait référence par %1, %2, etc., dans l'ordre où ils sont passés :
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Note que les arguments doivent être une syntaxe valide pour les opérations effectuées dans le corps de la macro.
Ici, mult_two eax, ecx et mult_two r8, r9 s'assemblent tous les deux, mais mult_two eax, cl non, car imul eax, cl n'est pas défini.
Une macro qui définit une étiquette ordinaire casse dès qu'on l'utilise deux fois, car l'étiquette serait alors définie deux fois elle aussi.
Pour utiliser des étiquettes dans une macro susceptible d'être appelée plusieurs fois, il faut faire précéder ces étiquettes de %%.
Cela demande à l'assembleur de donner à cette étiquette un nom différent et unique à chaque développement :
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
On peut répéter un bloc de code d'une seule ligne avec times.
C'est particulièrement utile pour déclarer des valeurs répétées en mémoire :
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
Pour répéter du code sur plusieurs lignes, on peut utiliser %rep à la place :
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Note que le nombre de répétitions se précise juste après %rep, et qu'un %endrep vient fermer le bloc répété.
Il est courant d'utiliser un compteur au moment de l'assemblage à l'intérieur d'un bloc %rep, un peu comme une boucle à l'exécution utilise un compteur.
Si une constante entière connue au moment de l'assemblage peut être déclarée avec equ, pour les variables entières du moment de l'assemblage on utilise plutôt %assign.
Contrairement à %define, %assign est évalué immédiatement en un nombre et peut être réaffecté :
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Une fois le préprocesseur passé, la boucle a disparu et il reste quatre instructions add, avec les valeurs immédiates 0, 1, 2 et 3 intégrées.
C'est du déroulage de boucle effectué par l'assembleur : pas de registre compteur, pas de branchement, juste du code linéaire.
Le préprocesseur dispose de plusieurs conditionnelles. Elles choisissent entre plusieurs chemins possibles et développent leur corps sur place, mais uniquement lorsque leur condition est vraie.
%if ... %elif ... %else ... %endif sélectionnent une branche à partir d'un test numérique :
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
Les macros sont souvent regroupées dans un fichier à part, que chaque source qui en a besoin importe avec %include :
%include "patterns.mac"
Le fichier nommé est inséré mot pour mot à cet endroit, avant l'assemblage, exactement comme si son contenu avait été tapé sur place. Ainsi les définitions restent au même endroit et plusieurs fichiers source peuvent les partager.
Si le fichier de macros peut être assemblé seul, il doit avoir l'extension .asm.
Note que les macros n'ont aucune liaison.
Même si le fichier de macros est assemblé, ses macros ne sont visibles que pour un autre fichier qui l'inclut avec %include.
Sinon, un fichier destiné uniquement à être inclus avec %include peut avoir n'importe quelle extension.
Utiliser .asm ou .mac est une convention courante.
Tu maintiens la bibliothèque de macros sur laquelle repose le moteur de composition d'un magazine. Le moteur lui-même, les fonctions qui composent chaque page, est figé et tu ne l'écris pas. Ce que tu écris, c'est la machinerie d'assemblage qui le constitue.
Cet exercice ne te demande d'écrire aucune fonction, seulement des macros. Ces macros sont ensuite incluses et appelées dans des fonctions déjà prédéfinies dans un autre fichier.
Tu as quatre tâches.
Chaque macro est appelée avec exactement le nom et l'ordre des arguments que sa tâche indique, alors écris-les exactement. Sinon, le fichier ne s'assemblera pas.
Le format du magazine tient en une poignée de nombres fixes, et le reste de la mise en page n'y fait référence que par leur nom. Tu dois donner à chacun un nom lisible à l'assemblage :
COLUMN_COUNT vaut 4
COLUMN_STRIDE vaut 18
SPACE vaut 0x20, l'octet de l'espaceRULE vaut 0x3D, l'octet =
RULE_WIDTH vaut 4
PLAIN vaut 0x2E, l'octet .
RULED vaut 0x2D, l'octet -
BOXED vaut 0x23, l'octet #
Le moteur les renvoie dans l'ordre ci-dessus.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
Le moteur remplit de blanc une galée et pose les filets de l'ours en estampillant une suite d'un seul octet. Fais de cette étape une macro multiligne que le moteur peut appeler partout où une suite doit être remplie.
D'abord, définis un nom lisible COUNTER pour le registre rcx.
Ensuite, définis la macro fill_run, qui doit remplir un tableau avec N copies d'un octet.
Elle prend trois arguments dans cet ordre :
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
La macro fill_run peut écraser COUNTER librement, mais aucun autre registre.
Note que le nombre d'octets peut être passé dans un registre général 64 bits, comme rax ou rsi, ou en valeur immédiate, comme 10 ou 4.
Tu peux considérer que ce nombre n'est jamais négatif.
Cette macro est appelée trois fois, dans deux fonctions différentes.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
La grille est figée pour ce format, donc les débuts de colonne peuvent être calculés à l'assemblage.
Définis la macro lay_columns, qui écrit COLUMN_COUNT décalages, chacun étant un entier 64 bits.
Elle prend deux arguments dans cet ordre :
lay_columns rsi, rax
Le nombre de décalages à écrire à l'adresse de destination est fixe et égal à COLUMN_COUNT.
Chaque décalage est une valeur 64 bits calculée avec offset[i] = margin + i * COLUMN_STRIDE, où i est l'indice du décalage en partant de 0, et margin est le second argument.
Cette macro peut écraser ses deux arguments registres, mais aucun autre. Elle est appelée une fois.
margin = 3
out = {3, 21, 39, 57}
set_header
Chacune des trois sections permanentes a un style d'en-tête fixé par le format, donc l'octet est connu à l'assemblage.
Définis set_header, qui prend deux arguments dans cet ordre :
0 est simple, 1 est fileté et 2 est encadré.L'adresse de destination est l'expression d'une adresse mémoire effective.
Elle prend un registre de base qui peut être additionné à un registre d'index mis à l'échelle par 1, 2, 4 ou 8, ainsi qu'une valeur immédiate :
set_header rdx+8*rcx+2, 1
Sélectionne l'octet d'en-tête (PLAIN, RULED ou BOXED) par assemblage conditionnel (%if / %elif / %else) plutôt que par un branchement à l'exécution.
Cette macro ne doit écraser aucun registre. Elle est appelée trois fois dans la même fonction.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.