Toutes les lignes d’un fichier assembleur ne deviennent pas des instructions. Beaucoup sont des directives — aussi appelées pseudo-instructions — des ordres adressés à l’assembleur lui-même : mets ceci dans la section de données, réserve 4 octets ici, rends ce nom visible des autres fichiers. D’autres sont des macros : des raccourcis que l’assembleur développe en d’autres lignes avant d’assembler quoi que ce soit.
Ni les unes ni les autres ne produisent d’instruction CPU par elles-mêmes. Mais ensemble, elles décident où va chaque octet du programme et ce qu’il a le droit de faire.
Deux syntaxes
Les directives appartiennent à l’assembleur, pas au CPU : elles diffèrent donc d’un assembleur à l’autre. Ce chapitre utilise les deux que vous croiserez le plus souvent sous Linux :
- GAS, l’assembleur GNU, celui que produisent
gccetclanget que litobjdump. Ses directives commencent par un point :.data,.long,.globl. - NASM, apprécié pour l’assembleur écrit à la main. Il utilise des mots-clés sans point :
section .data,dd,global.
MASM, chez Microsoft, a encore un autre jeu de directives (DB, DD, PUBLIC, SEGMENT). Le code machine est le même dans tous les cas.
Les sections
Un programme est découpé en sections, chacune avec son rôle et ses permissions :
| Section | Contient | À l’exécution |
|---|---|---|
.text | le code machine | lecture + exécution |
.data | les variables globales avec une valeur initiale | lecture + écriture |
.rodata | les constantes et les chaînes littérales | lecture seule |
.bss | les variables globales qui commencent à zéro | lecture + écriture |
On change de section avec une directive — .data, .text ou .section .rodata en GAS, section .data en NASM — et on peut passer de l’une à l’autre aussi souvent qu’on veut. L’assembleur tient un compteur d’emplacement par section et ajoute chaque ligne à la section courante.
Ces permissions expliquent certains plantages : écrire dans une chaîne littérale provoque une faute parce que .rodata est en lecture seule, et sauter dans .data en provoque une parce qu’elle n’est pas exécutable. L’éditeur de liens fusionne les sections de même nom de tous les fichiers objets, et le chargeur place chaque groupe en mémoire avec ses permissions, comme vu au chapitre précédent.
Les directives de données
Les directives de données placent des octets dans la section courante. Une étiquette placée devant nomme l’adresse de son premier octet :
- .data
- count: .long 5 ; 4 octets
- table: .byte 10, 20, 30 ; 3 octets
- .align 8 ; complète jusqu’à un multiple de 8
- big: .quad 0x1122334455667788
- .section .rodata
- msg: .asciz "hi" ; 'h', 'i', 0
- .bss
- buf: .zero 16
- .text
- _start:
- mov eax, DWORD PTR [rip+count]
- movzx ecx, BYTE PTR [rip+table+2]
- add eax, ecx
- mov DWORD PTR [rip+count], eax
- lea rdi, [rip+msg]
- lea rsi, [rip+buf]
Regardez le panneau des données :
countest en0x404000et contient 5.tableest en0x404004. Le panneau lit ses 4 octets comme0x001e140a: ce sont 10, 20, 30 (0a,14,1e) lus en petit-boutiste (little-endian), plus l’octet de remplissage00ajouté par.align 8.bigcommence en0x404008, le multiple de 8 suivant.msgest dans.rodata, etbuffait 16 octets à zéro dans.bss.
La démo s’est arrêtée après la deuxième instruction : eax = 5 et ecx = 30, le troisième octet de table. Continuez et count passe à 35. Les deux lea chargent ensuite les adresses de msg et de buf, pas leur contenu.
Le simulateur regroupe toutes ses données dans une seule région qui commence en 0x404000. Un vrai éditeur de liens garde .data, .rodata et .bss séparées, dans des pages distinctes aux permissions différentes.
Les mêmes directives dans les deux syntaxes :
| Rôle | GAS | NASM |
|---|---|---|
| valeurs de 1 octet | .byte 1, 2 | db 1, 2 |
| valeurs de 2 octets | .word / .short | dw |
| valeurs de 4 octets | .long / .int | dd |
| valeurs de 8 octets | .quad | dq |
| chaîne terminée par 0 | .asciz "hi" / .string "hi" | db "hi", 0 |
| chaîne sans 0 final | .ascii "hi" | db "hi" |
| n octets à zéro | .zero n / .skip n | times n db 0, ou resb n dans .bss |
| alignement | .balign 8 / .p2align 3 | align 8 |
Deux pièges. Sur x86, le .word de GAS fait 2 octets, pas la taille du mot du CPU (32 ou 64 bits). Et .align signifie « n octets » sur x86 mais « 2ⁿ octets » sur d’autres cibles : c’est pourquoi les compilateurs écrivent .p2align (une puissance de deux) ou .balign (des octets), qui ne sont pas ambigus.
Ce qui arrive dans le fichier objet
Assembler les mêmes données avec un vrai assembleur et afficher les sections donne :
Contents of section .data:
0000 05000000 0a141e00 88776655 44332211
Contents of section .rodata:
0000 686900 hi.
Chaque valeur est rangée en petit-boutiste : 05000000 vaut 5, et 8877665544332211 est 0x1122334455667788 à l’envers.
.bss n’apparaît pas dans l’affichage car elle n’a aucun contenu. L’en-tête de section ne note que sa taille. Déclarez un tampon de 4096 octets dans .bss et le fichier objet reste sous le kilo-octet ; c’est le chargeur qui fournit la mémoire mise à zéro à l’exécution. C’est toute la raison d’être de .bss.
En NASM, un simple align remplit avec des octets 0x90 — des instructions nop — même dans une section de données : les mêmes données donnent 0a141e90. Écrivez align 8, db 0 pour remplir avec des zéros. Si vous voyez un 90 isolé entre des données dans un dump hexadécimal, c’est probablement la raison.
Symboles : qui voit quel nom
Par défaut, une étiquette est locale à son fichier : deux fichiers peuvent chacun avoir une étiquette loop sans conflit. Certaines directives changent cela :
| Rôle | GAS | NASM |
|---|---|---|
| exporter un nom vers les autres fichiers | .globl main | global main |
| utiliser un nom défini ailleurs | (automatique) | extern puts |
| nommer une constante | .equ SIZE, 64 / .set | SIZE equ 64 |
GAS considère comme externe tout nom non défini dans le fichier : .extern y est donc facultatif. NASM exige extern.
Une constante définie par .equ ou equ n’occupe aucune mémoire. C’est un nom que l’assembleur remplace par un nombre pendant l’assemblage : elle ne laisse aucune trace dans le binaire, seulement le nombre.
Lire la sortie d’un compilateur
La plupart des directives que vous verrez viennent des compilateurs. Voici ce que produit clang -S -O1 -masm=intel -fno-pic pour deux lignes de C, int counter = 5; et const char *greet(void) { return "hi"; }, légèrement élagué. -fno-pic garde un code dépendant de la position. Sans cette option, on obtiendrait lea rax, [rip + .L.str] au lieu du mov :
.text
.globl greet
.p2align 4
.type greet,@function
greet:
mov eax, offset .L.str
ret
.Lfunc_end0:
.size greet, .Lfunc_end0-greet
.data
.globl counter
.p2align 2, 0x0
counter:
.long 5
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "hi"
Ligne par ligne :
.globlexportegreetetcounter— en C, tout ce qui n’est pas déclaréstatic..p2align 4fait commencer la fonction sur une frontière de 16 octets, et.p2align 2placecountersur une frontière de 4 octets..typeet.sizeinscrivent dans la table des symboles quegreetest une fonction, et sa longueur. Débogueurs et désassembleurs s’en servent..L.stret.Lfunc_end0commencent par.L, ce qui les rend locales à l’assembleur : elles servent pendant l’assemblage et n’atteignent jamais la table des symboles.nmsur le fichier objet listegreetetcounter, mais pas.L.str. C’est pourquoi un désassembleur doit inventer des noms commeloc_401020pour les cibles de sauts : la plupart des étiquettes ne sont jamais arrivées jusqu’au binaire..rodata.str1.1avec les drapeaux"aMS"marque une section de chaînes fusionnables : l’éditeur de liens peut ne garder qu’une copie des chaînes littérales identiques.
Les macros
Une macro donne un nom à un morceau de texte. Partout où ce nom apparaît ensuite, l’assembleur le remplace par ce texte — c’est l’expansion — avant d’assembler. Voici le même programme en GAS et en NASM :
; GAS
.equ SYS_EXIT, 60
.macro exit code
mov edi, \code
mov eax, SYS_EXIT
syscall
.endm
.macro clamp0 reg ; si reg < 0, le mettre à 0
test \reg, \reg
jns 1f
xor \reg, \reg
1:
.endm
_start:
clamp0 eax
clamp0 ebx
.rept 3
nop
.endr
exit 0
; NASM
SYS_EXIT equ 60
%macro exit 1
mov edi, %1
mov eax, SYS_EXIT
syscall
%endmacro
%macro clamp0 1 ; si reg < 0, le mettre à 0
test %1, %1
jns %%done
xor %1, %1
%%done:
%endmacro
_start:
clamp0 eax
clamp0 ebx
times 3 nop
exit 0
Une macro a un en-tête avec son nom et ses paramètres (\code en GAS, %1 en NASM), un corps, et un marqueur de fin. Chaque appel — clamp0 eax — est remplacé par le corps, paramètres remplis.
Les deux fichiers s’assemblent exactement en les mêmes 27 octets :
0: 85 c0 test eax, eax
2: 79 02 jns 6
4: 31 c0 xor eax, eax
6: 85 db test ebx, ebx
8: 79 02 jns c
a: 31 db xor ebx, ebx
c: 90 nop
d: 90 nop
e: 90 nop
f: bf 00 00 00 00 mov edi, 0
14: b8 3c 00 00 00 mov eax, 60
19: 0f 05 syscall
Il ne reste aucune trace des macros. clamp0 apparaît deux fois, entièrement écrite, et SYS_EXIT est devenu 3c. L’expansion se fait entièrement à l’assemblage : à partir du seul binaire, impossible de savoir si une macro a été utilisée. En rétro-ingénierie, les macros (comme les #define et les fonctions inline du C) n’apparaissent que sous forme d’un même motif d’instructions répété à plusieurs endroits.
Le problème des étiquettes
clamp0 contient une étiquette. Développez la macro deux fois avec une étiquette ordinaire et vous la définissez deux fois, ce qui est une erreur. Chaque assembleur a sa solution :
- GAS a les étiquettes locales numériques :
1:peut être défini autant de fois qu’on veut, et1f/1bsignifient « le prochain1en avant » et « le1précédent en arrière ». GAS propose aussi\@, un compteur différent à chaque expansion, pour construire des noms uniques. - NASM a les étiquettes locales de macro :
%%donedevient un nouveau nom à chaque expansion. NASM inscrit même ces noms dans la table des symboles — ici..@2.doneet..@3.done— et un désassemblage les affiche.
Macro ou fonction ?
Une macro et une fonction permettent toutes deux d’écrire un code une fois et de l’utiliser souvent, mais elles fonctionnent très différemment :
| Macro | Fonction (call) | |
|---|---|---|
| Traitée | à l’assemblage | à l’exécution |
| Copies du code dans le binaire | une par utilisation | une seule |
| Coût d’appel et de retour | aucun | un call, un ret, et souvent un prologue |
| Récursivité | seulement si elle s’arrête à l’assemblage | normale |
Les macros conviennent donc aux séquences courtes utilisées dans du code critique, où un call coûterait plus cher que le travail lui-même. Les fonctions conviennent à tout ce qui est long, puisque chaque utilisation d’une macro ajoute une copie complète du code.
Autres fonctions à l’assemblage
- Répétition :
.rept n….endren GAS,times nou%repen NASM. - Assemblage conditionnel :
.if/.else/.endifet.ifdefen GAS,%if/%ifdefen NASM. Un même source peut alors produire plusieurs variantes, par exemple 32 et 64 bits, ou debug et release. - Inclusion :
.include "defs.s"ou%include "defs.inc"recopie un autre fichier à cet endroit, en général un fichier de constantes et de macros.
À retenir
- Les directives sont des ordres à l’assembleur. Elles produisent des données, changent de section, alignent et gèrent les symboles, mais ne produisent aucune instruction.
- Les sections séparent code, données initialisées, données en lecture seule et données à zéro, chacune avec ses permissions.
.bssn’occupe aucune place dans le fichier. - Les directives de données rangent les valeurs en petit-boutiste. Le remplissage d’alignement peut apparaître sous forme de zéros — ou de
90avec lealignde NASM. - Les étiquettes sont locales sauf si on les exporte avec
.globl/global. Les étiquettes.Let les constantesequn’arrivent jamais dans le binaire. - Les macros sont une substitution de texte à l’assemblage. Elles ne laissent aucune trace dans le code machine, seulement des motifs répétés.