Skip to content

Niveau 1 · Chapitre 1.5

Directives, sections et macros

Les lignes d’un fichier assembleur qui ne sont pas des instructions : sections et permissions, directives de données, visibilité des symboles, alignement et macros — GAS et NASM côte à côte, et ce qui subsiste dans le binaire.

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 gcc et clang et que lit objdump. 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 :

SectionContientÀ l’exécution
.textle code machinelecture + exécution
.datales variables globales avec une valeur initialelecture + écriture
.rodatales constantes et les chaînes littéraleslecture seule
.bssles variables globales qui commencent à zérolecture + é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 :

Live · Globales, chaînes et alignement
program— ▸ is the next instruction
  1. .data
  2. count: .long 5 ; 4 octets
  3. table: .byte 10, 20, 30 ; 3 octets
  4. .align 8 ; complète jusqu’à un multiple de 8
  5. big: .quad 0x1122334455667788
  6. .section .rodata
  7. msg: .asciz "hi" ; 'h', 'i', 0
  8. .bss
  9. buf: .zero 16
  10. .text
  11. _start:
  12. mov eax, DWORD PTR [rip+count]
  13. movzx ecx, BYTE PTR [rip+table+2]
  14. add eax, ecx
  15. mov DWORD PTR [rip+count], eax
  16. lea rdi, [rip+msg]
  17. lea rsi, [rip+buf]
step 0
Loading emulator…

Regardez le panneau des données :

  • count est en 0x404000 et contient 5.
  • table est en 0x404004. Le panneau lit ses 4 octets comme 0x001e140a : ce sont 10, 20, 30 (0a, 14, 1e) lus en petit-boutiste (little-endian), plus l’octet de remplissage 00 ajouté par .align 8.
  • big commence en 0x404008, le multiple de 8 suivant.
  • msg est dans .rodata, et buf fait 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ôleGASNASM
valeurs de 1 octet.byte 1, 2db 1, 2
valeurs de 2 octets.word / .shortdw
valeurs de 4 octets.long / .intdd
valeurs de 8 octets.quaddq
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 ntimes n db 0, ou resb n dans .bss
alignement.balign 8 / .p2align 3align 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ôleGASNASM
exporter un nom vers les autres fichiers.globl mainglobal main
utiliser un nom défini ailleurs(automatique)extern puts
nommer une constante.equ SIZE, 64 / .setSIZE 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 :

  • .globl exporte greet et counter — en C, tout ce qui n’est pas déclaré static.
  • .p2align 4 fait commencer la fonction sur une frontière de 16 octets, et .p2align 2 place counter sur une frontière de 4 octets.
  • .type et .size inscrivent dans la table des symboles que greet est une fonction, et sa longueur. Débogueurs et désassembleurs s’en servent.
  • .L.str et .Lfunc_end0 commencent par .L, ce qui les rend locales à l’assembleur : elles servent pendant l’assemblage et n’atteignent jamais la table des symboles. nm sur le fichier objet liste greet et counter, mais pas .L.str. C’est pourquoi un désassembleur doit inventer des noms comme loc_401020 pour les cibles de sauts : la plupart des étiquettes ne sont jamais arrivées jusqu’au binaire.
  • .rodata.str1.1 avec 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, et 1f / 1b signifient « le prochain 1 en avant » et « le 1 pré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 : %%done devient un nouveau nom à chaque expansion. NASM inscrit même ces noms dans la table des symboles — ici ..@2.done et ..@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 :

MacroFonction (call)
Traitéeà l’assemblageà l’exécution
Copies du code dans le binaireune par utilisationune seule
Coût d’appel et de retouraucunun call, un ret, et souvent un prologue
Récursivitéseulement si elle s’arrête à l’assemblagenormale

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 … .endr en GAS, times n ou %rep en NASM.
  • Assemblage conditionnel : .if / .else / .endif et .ifdef en GAS, %if / %ifdef en 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. .bss n’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 90 avec le align de NASM.
  • Les étiquettes sont locales sauf si on les exporte avec .globl / global. Les étiquettes .L et les constantes equ n’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.

Dans ce niveau

  1. 1.1Les registres
  2. 1.2Drapeaux, cmp et sauts conditionnels
  3. 1.3Conventions d’appel (System V, cdecl)
  4. 1.4Assembleur, éditeur de liens et chargeur
  5. 1.5Directives, sections et macros