Aller au contenu

Structure d'un module d'assemblage

La conception de programme en langage assembleur s’effectue généralement sous une forme modulaire à l’aide de plusieurs fichiers (les modules). La décomposition de l’application en plusieurs modules permet d’en simplifier le développement, la validation et avant tout sa réutilisation. Cette technique permet également la réalisation d’applications utilisant différents langages de programmation, ceci pour autant que l’interface au niveau du code objet binaire (ABI - Application Binary Interface) soit identique.

Module

Les modules assembleur implémentent leur code dans un fichier avec l’extension .S (majuscule). Comme pour un code développé entièrement en C/C++, la déclaration des éléments publics passe par l’intermédiaire d’un fichier d’en-tête, un fichier .h.

Un module assembleur se décompose généralement en sept sections distinctes :

  1. En-tête avec le copyright, l’auteur, la date, la version, …
  2. Inclusion de déclarations externes, si nécessaire
  3. Déclaration des symboles ou expressions textuelles
  4. Déclaration des constantes ou variables avec un accès en lecture uniquement
  5. Déclaration des variables avec une valeur initiale non nulle
  6. Déclaration des variables avec une valeur initiale nulle
  7. Implémentation des fonctions ou routines
/* 
 * 1. header with copyright
 */

// 2. Includes
  .include "file"

// 3. Symboles (textual expressions)
#define MY_SYMBOL (12)

// 4. Declaration of constants
  .section .rodata
  .align 2
MY_CONSTANT:  .long 10

// 5. Declaration of variables with non-zero initial value
  .section .data
  .align 2
my_init_var:   .long 5

// 6. Declaration of variables with zero initial value
  .section .bss
  .align 2
my_zero_var:   .long 0

// 7. Function implementation
  .section .text
  .thumb
  .syntax unified
  .align 2

  .global my_function
  .type my_function, %function
my_function:
  adds  r0, #10
  movs  r3, #213
  muls  r0, r3, r0
  bx      lr
  .size my_function, .-my_fonction

Le code assembleur s’écrit en une suite de lignes de code. Chaque ligne de code ne comprend qu’une seule instruction. L’indentation du code augmente grandement la lisibilité du code.

Commentaires

Les commentaires identifient des lignes de texte que l’assembleur doit ignorer. Il existe deux possibilités d’insérer des commentaires dans un module assembleur :

  • Commentaire de bloc
  • Commentaire de ligne

Le commentaire de bloc suit la même syntaxe que le code C/C++ :

  • Débute par un /* et se termine par un */
  • Peut être placé n’importe où dans le programme
  • Peut être placé sur plusieurs lignes
  • Ne peut pas être imbriqué

Le commentaire de ligne débute avec un marqueur spécial et se termine à la fin de la ligne. Il existe plusieurs marqueurs selon la distribution de l’assembleur :

  • @@ : caractère spécifique à l’assembleur GNU/AS
  • ; : caractère spécifique à l’assembleur ARM/AS (Keil)
  • // : commentaire de ligne identique à C/C++
  /* ce commentaire, écrit sur plusieurs lignes,
   * est tout à fait valide.
   */

  mov r0,#78 @@  commentaire de ligne à la GNU/AS
  mov r2,#97 ;  commentaire de ligne à la ARM/AS
  mov r1,#59 // commentaire de ligne à la C/C++ 

L’utilisation de la syntaxe C/C++ pour les commentaires de ligne est à privilégier, car la majorité des assembleurs la supporte.

Symboles

Tout comme en langage C/C++, les symboles permettent d’identifier les fonctions, les variables et les constantes. La visibilité d’un symbole est par défaut local au module dans lequel il est déclaré. Si sa visibilité doit être élargie à l’ensemble du programme, alors dans ce cas l’emploi de la directive .global symbol est nécessaire. Un symbole se déclare à l’aide d’un identifiant et se définit par une étiquette (label).

L’identifiant servant à la déclaration d’un symbole doit respecter certaines règles :

  • Comme 1er caractère, une lettre ou un de ces caractères spéciaux : ., _, $
  • Chiffres autorisés, mais pas comme premier caractère
  • Espaces non autorisés
  • Sensibilité à la casse (case sensitive)
  • Pas de limites de taille (tous les caractères significatifs)
  • Unique dans le module
  • Unique dans le programme, si global

Une étiquette est un identifiant valide placé au début d’une nouvelle ligne et terminé par deux points (:). Cette étiquette représente une adresse mémoire. Elle permet de définir le symbole d’une donnée (variable ou constante) ou d’une fonction.

L’exemple ci-dessous déclare et définit trois symboles, my_variable, my_constant ainsi que my_function. Les deux premiers symboles sont locaux au module, tandis que le troisième est global.

  .section .data
my_variable: .long 10

  .section .rodata
my_constant: .asciz "constant string"

  .section .text
  .global my_function
  .type my_function, %function
my_function:
  bx  lr
  .size my_function, .-my_fonction

Les symboles ne donnent aucune indication sur la nature de l’objet qu’ils représentent. Ils ne permettent pas de distinguer entre donnée et fonction, au demeurant information fort utile lors de sessions de débogage. La directive .type permet de pallier à ce manque. La directive .type str, %fonction indique que le symbole str est une fonction, tandis que la directive .type str, %object indique que le symbole str est une donnée (variable ou constante).

En assembleur, les symboles permettent également de représenter des expressions. Lors de l’assemblage du module, le symbole sera remplacé par son expression. L’exemple ci-dessous présente différentes manières de définir des symboles en assembleur.

SYMBOL_1 = 10

.equ   SYMBOL_2, 20
.set   SYMBOL_3, 30
.equiv SYMBOL_4, 40

#define SYMBOL_5 50

Avec l’utilisation du préprocesseur C/C++, il est également possible d’utiliser les symboles définis avec la pseudo-instruction #define (voir l’exemple ci-dessus).

Constantes

L’assembleur distingue deux types de constantes, les constantes numériques et les constantes caractères.

// numerical constants
an_integer:   .long 1234
a_float:      .float -1.05e4
// character constants
a_character:  .byte 'a'
a_string:     .asciz "hello world!\n"

Il existe principalement deux types de constantes numériques, les nombres entiers (int en C/C++) et les nombres à virgule flottante (float en C/C++).

  • Nombres entiers

    • Décimal: suite de chiffres (digits) entre 0 et 9, mais ne débutant pas par un 0
    • Binaire: 0b ou 0B suivit d’une suite de 0 et 1
    • Octal: 0 suivit par une suite de chiffres entre 0 et 7
    • Hexa: 0x ou 0X suivit par une suite de chiffres entre 0 et 9, a et f ou A et F
  • Nombres à virgule flottante

  • Normalement représentation décimale
  • Signe (optionnel), soit + ou -
  • Partie entière représentée par zéro ou plusieurs digits
  • Partie fractionnaire (optionnel) représentée par un . suivit de zéro ou plusieurs digits
  • Exposant en base décimale (optionnel) représenté par un E ou e, un signe et un ou plusieurs digits

Il existe deux types de constantes de caractères, les caractères et les chaînes de caractères (string). Un caractère est contenu dans un octet. Sa valeur numérique peut être utilisée dans des expressions arithmétiques et logiques. Les chaînes de caractères sont représentées généralement par plusieurs octets. Leurs valeurs ne peuvent pas être utilisées dans des expressions numériques.

  • Caractères

    • Caractère ASCII unique écrit soit avec un apostrophe (') suivie du caractère, soit avec un caractère entre deux apostrophes
    • Pour écrire le caractère backslash, on doit utiliser un 2e backslash (\\)
  • Chaînes de caractères

    • Une chaîne de caractères est écrite en plaçant une chaîne de caractères ASCII entre deux guillemets (")
    • Les caractères d’échappement peuvent également être utilisés:
    • \b: backspace (retour d’un caractère)
    • \f: formfeed (saut page)
    • \n: newline (nouvelle ligne)
    • \r: carriage-return (retour de chariot)
    • \t: horizontal tab (tabulateur horizontal)
    • \\: caractère \
    • \": caractère "
  • Les caractères représentés par une valeur octale \000 sont également autorisés
  • Les caractères représentés par une valeur hexadécimale \x1a sont également autorisés

Expressions

Le langage assembleur supporte l’utilisation d’expressions. Elles permettent de décrire plus explicitement une valeur et d’éviter ainsi de la calculer à la main et d’ajouter un commentaire d’explication. Les expressions peuvent s’utiliser aussi bien avec les symboles que comme opérande d’une instruction assembleur.

Les expressions couramment utilisées sont:

  • Addition (+)
  • Soustraction (-)
  • Multiplication (*)
  • Division entière (/)
  • Modulo / reste d’une division entière (%)
  • Décalages (>> et <<)
  • Opérations logiques sur les bits: ou/or (|), et/and (&), ou exclusif/xor (^), inverse/not (!)

Quelques exemples:

#define SIZE 100

list:  .space SIZE+2,0xff // list[102] = {0xff,..., 0xff}

  ldr     r0, =SIZE/4     // r0 = 25
  ldr     r1, =2+4*SIZE   // r1 = 402
  ldr     r2, =list+27    // r2 = &list[27]
  ldrb    r2, [r2]        // r2 = list[27]
  ldr     r3, ='a'-'A'    // r3 = 32
  ldr     r4, =1<<30      // r4 = 0x40000000

Données

L’assembleur supporte la déclaration de variables et de constantes, mais ces données ne sont pas typées au sens de C/C++. Les seules indications nécessaires en assembleur sont la taille et si le type de la donnée est un nombre entier (signé ou non signé), un nombre à virgule flottante ou une chaîne de caractères.

Lors de leur déclaration, la directive .section permet de regrouper les données dans un segment de mémoire, une section, propre à leur qualité. Le contenu de ces sections sera ensuite placé en mémoire lors du chargement de l’application sur la cible.

L’assembleur distingue par défaut trois sections correspondant aux trois qualités de données standard:

  • .section .data : pour les données avec une valeur initiale non nulle (!= 0)
  • .section .bss : pour les données avec une valeur initiale nulle (== 0)
  • .section .rodata : pour les constantes, données avec un accès en lecture uniquement

Il est cependant possible de définir d’autres sections si nécessaire. Dans ce cas, l’éditeur de liens devra en être informé afin de placer ces sections et leurs données à l’endroit souhaité dans la mémoire.

La déclaration d’une donnée nécessite trois champs, l’étiquette de la donnée (son symbole), le type de la donnée (sa taille) et finalement une expression ou plusieurs expressions séparées par des virgules (,) (sa valeur initiale), par exemple:

var:    .long 10        // en C/C++: static long var = 10;
array:  .byte 1,2,3,4   // en C/C++: static char array[4] = {1,2,3,4}
Pour les données de type entier, quatre tailles sont disponibles:

  • .byte : déclaration d’une donnée de 8 bits (1 octet)
  • .hword ou .short : déclaration d’une donnée de 16 bits (2 octets)
  • .word ou .long : déclaration d’une donnée de 32 bits (4 octets)
  • .quad : déclaration d’une donnée de 64 bits (8 octets)

Pour les données de type virgule flottante selon le standard IEEE 754, deux tailles sont disponibles:

  • .float : déclaration d’une donnée de 32 bits (4 octets)
  • .double : déclaration d’une donnée de 64 bits (8 octets)

Pour les données de type chaîne de caractères, deux variantes sont disponibles:

  • .ascii : chaîne de caractères non terminée
  • .asciz : chaîne de caractères terminée avec un 0 (caractère \000)

Il est possible de réserver un espace mémoire d’une taille exprimée en octets et éventuellement de la remplir avec une valeur initiale, pour cela deux possibilités:

  • .space number_of_bytes
  • .fill number_of_data, data_size, fill_value"

L’alignement des données en mémoire est un facteur important. Si les données sont bien alignées, le processeur pourra les accéder de façon optimale. La directive .align <expr> permet de définir l’alignement d’une donnée. L’alignement (<expr>) est indiqué en puissance de 2. Par exemple, la directive .align 3 déplace l’emplacement de la donnée dans la mémoire afin que son adresse soit un multiple de 8 (\(2^3\)).

Voici quelques exemples de déclaration de données:

// Constant declaration
  .section .rodata
  .align 2

// C/C++: static const uint32_t my_long = 0x01020304;
my_long:     .long 0x01020304          

// C/C++: static const char my_string[] = "hello world\n";
my_string:   .asciz "hello world\n"    
// Declaration of data with initial value
  .section .data
  .align 3

// C/C++: static uint64_t my_longlong = 0x0102030405060708;
my_longlong: .quad 0x0102030405060708

// C/C++: static int8_t my_byte = -25;
my_byte:     .byte -25
// Declaration of data without initial value
  .section .bss
  .align 2

// C/C++: static int32_t my_word;
my_word:     .word

// C/C++: static int16_t my_hword = 0;
my_hword:    .hword 0

Code

La directive .section .text sert à regrouper tout le code d’une application et ses fonctions dans une même section. Une fois la section de code déclarée, il faut indiquer à l’assembleur le jeu d’instructions qu’il doit utiliser pour générer le code objet. Trois jeux d’instructions sont disponibles, ARM, Thumb et Thumb-2:

// Instructions ARM     // Instructions Thumb   // Instructions Thumb-2
  .section .text         .section .text          .section .text
  .code 32                .code 16                .code 16
                                                  .syntax unified
  .align 2                .align 2                .align 2
ou
// Instructions ARM     // Instructions Thumb   // Instructions Thumb-2
  .sections .text         .section .text          .section .text
  .arm                    .thumb                  .thumb
                                                  .syntax unified
  .align 2                .align 2                .align 2

Comme pour la déclaration des données, il est important que le code soit correctement aligné. La directive .align 2 permet de garantir que le code soit placé à un multiple de 4. Là aussi, il est possible de placer le code dans des sections différentes. L’éditeur de liens devra également en être informé.

Fonctions

La déclaration d’une fonction s’effectue en plusieurs étapes. La fonction débute avec son étiquette (son symbole). Cette étiquette permet à d’autres fonctions de l’appeler. Si la fonction doit obtenir un accès public, externe au module, il est indispensable de le déclarer avec la directive .global. Il sera également utile de déclarer cette fonction dans un fichier d’en-tête afin des codes développés en C/C++ puissent connaître sa signature. Pour faciliter le débogage, il est fortement conseillé d’entourer l’implémentation de la fonction avec les directives .type <str>, %function et .size <str>, .-<str>.

L’exemple ci-dessus présente le squelette de la fonction publique my_fonction.

  .global my_function
  .type my_function, %function
my_function:
  bx  lr
  .size my_function, .-my_fonction

Macro

Lors de développement en assembleur, il est courant de devoir répéter certaines séquences de code. Ces séquences peuvent être strictement identiques ou similaires, ne demandant que quelques petites adaptations, tels un identifiant ou une valeur numérique. Dans de tels cas, l’utilisation d’une fonction n’est pas forcément adaptée ou possible. Les macros offrent une solution appropriée.

Deux mots clefs à connaître: .macro et .endm.

// macro definition
.macro macname macarg1, macarg2
  ldr r0, =\macarg1
  ldr r1, =\macarg2
.endm

// usage of the macro
macname 10, 20

La définition d’une macro débute avec la directive .macro suivie d’un identifiant, un nom, dans l’exemple ci-dessus macname. Si la macro nécessite des arguments, il est possible d’en passer en donnant leur nom après celui de la macro et en les séparant par une virgule (,), dans l’exemple macarg1 et macarg2. Pour utiliser et évaluer un argument à l’intérieur de la macro, il suffit de précéder son nom par une barre oblique inversée \ (backslash). La directive .endm termine la définition de la macro.

Pour obtenir plus de détails sur la définition et l’utilisation de macro, le manuel en ligne1 est un complément fort utile.

Assemblage conditionnel

Les directives pour un assemblage conditionnel peuvent s’avérer très utiles lors de développement de code en assembleur. Ces directives sont spécialement intéressantes lors de la réalisation de macros. Elles permettent de paramétriser l’expansion de la macro durant de la phase d’assemblage afin d’obtenir un code optimal.

Trois mots clefs à connaître: .if, .else et .endif.

// macro definition
.macro macname cond, macarg1, macarg2
  ldr r0, =\macarg1
  ldr r1, =\macarg2
.if \cond != 0
  sub r0, #1
.else 
  add r0, #1
.endif 
.endm

La directive .if <expr> marque le début d’un bloc de code conditionnel. Si l’évaluation de l’expression <expr> est non nulle, alors les instructions placées après la directive seront intégrées au code source, dans le cas contraire, elles seront tout simplement ignorées. La directive .endif marque la fin du bloc. La directive .else permet d’ajouter des instructions pour la condition inverse.

Le code ci-dessous donne deux exemples d’utilisation de la macro ainsi du résultat obtenu après son expansion lors de la phase d’assemblage.

// usage of the macro
_true:  macname 0, 10, 20
_false: macname 1, 10, 20
// generated code after assembly
_true:  
  ldr r0, =10
  ldr r1, =20
  add r0, #1

_false:  
  ldr r0, =10
  ldr r1, =20
  sub r0, #1

Il existe encore un grand nombre d’autres directives d’assemblage conditionnel2.