Assembleur



- Le langage machine, le langage d'assemblage
- La tâche de l'assembleur
- L'organisation d'un assembleur
- Programmation en langage d'assemblage
    - Calcul et affectation de variables
    - Exécution conditionnelle, branchement
    - Sousprogrammes
        - Transfert de contrôle
        - Passage de paramètres
        - Utilisation d'une pile
    - Traitement d'exceptions


Le langage machine :

Un micro-processeur, qui est ici la machine dont on parle, a comme langage, le binaire.  Il lit des instructions, où mots, en binaire, de la mémoire, et les exécutent.  Ici, je parle de mot puisqu'on parle de langage, mais aussi, sur les machines avec instructions de taille fixe, ce qu'on appelle un mot est une série de bits qui a la même taille que l'instruction.  Sur la SRC un mot ("word" en anglais) fait 32 bits, comme les instructions.

Soit le code suivant, sur la SRC :  00001101 10001000 00000000 00011000
(ou 0D 88 00 18 en hexadécimal)
Ceci forme un mot, puisqu'il y a 32 bits, qui est facile à lire par un ordinateur, mais pas par un humain.  Aussi, ce mot peut avoir plusieurs significations: l'entier 227016728  (en base 10), ou l'instruction "ld r22,24(r4)", selon qu'on utilise le mot comme donnée ou comme instruction.
L'assembleur a donc été inventé pour simplifier la lecture et l'écriture du code par des humains sans enlever la flexibilité du langage machine.  Il y a souvent une correspondance 1 à 1 entre une ligne assembleur et un mot machine.

L'assembleur (ou le langage d'assemblage):  (app. C)

Forme symbolique d'un programme en langage machine.
- Lisible et écrivable par des humains (pas toujours facile, mais bon...)
- Constantes, adresses et noms d'instructions symboliques
- Directives à l'assemblage (pseudo-instructions ou "pseudo-operations" dans le livre)
        - définiton de constantes
        - initialisation de la mémoire
        - allocation de mémoire
        - assemblage conditionnel
(Ce qui suit n'est pas fait dans l'assembleur utilisé pour la SRC (auteur F. Boyer))
- Arithmétique lors de l'assemblage pour le calcul des constantes (on peut écrire 38*164 et il va remplacer par le résultat)
- Instructions synthétiques: instructions qui n'existes pas vraiment sur le processeur; l'assembleur génère une courte séquence d'instructions pour faire l'opération.
- Expansion des macroinstructions

Le processus d'assemblage :
Avant l'exécution d'un programme en assembleur, il faut passer par plusieurs étapes: Figure C.1, page 556.
L'assemblage lui-même se fait souvent en deux passes (c'est ce que fait mon assembleur pour la SRC).  Ces passes sont décrites à la page 562.  En gros, la première passe numérote chaque ligne avec son adresse et fait une table qui associe à chaque symbole l'adresse de la ligne où sa définition se trouve (sauf les ".equ", qui associent une valeur spécifiée dans la pseudo-instruction).  La deuxième passe génère le code machine en remplaçant les symboles par la valeur associée, selon la table créée dans la première passe.

L'assembleur de la SRC :
Chaque ligne a la structure suivante (où chaque partie est facultative) :
    étiquette:   instruction        ; commentaire
L'instruction peut être n'importe quelle opération (couverture arrière du livre), ou pseudo-opération, suivie des opérandes.
Un programme commence d'habitude par des définitions de constantes et de l'origine du programme (".org").
Attention, la définition de ".org", sur la couverture du livre, porte à confusion:  ça charge les lignes qui suivent le ".org" à partir de l'adresse spécifiée.  Il peut dont y avoir plusieurs ".org" pour charger différentes parties du programme à différents endroits en mémoire.

La SRC est une machine qui effectue les opérations arithmétiques entre les registres et qui a des opérations load et store ("ld" et "st") pour accéder à la mémoire.  On dit généralement de ces machines qu'elles sont de type load/store, et ceci est une caractéristique des machines RISC.


Procédures et fonctions :  (pas dans le livre)

Ce sont des sous programmes (souvent petits) qui font un travail qui est utile à plusieurs endroits dans le programme (comme des méthodes en Java).  Ces sous programmes sont souvent appelés sous-routines ("subroutines" en anglais), ou, en plus court, routines.  Normalement on appel "fonction" une sous-routine qui retourne un résultat (habituellement dans un registre), et "procédure" une sous-routine qui modifie le contenu de la mémoire ou fait des entrées/sorties, ou retourne des résultats sur la pile.  Dans ce cours, on ne fera pas ces distinctions, qui sont souvent vagues pour des programmes en assembleur, et on utilisera surtout les termes génériques sous-routine ou routine.

Avantages:
- Réutilisation d'un algorithme à plusieurs endroits dans le programme
- Facilite la maintenance (une seule copie à mettre à jour)
- Le programme complet peut prendre moins de d'espace en mémoire
Inconvénient principal:
- Vitesse d'exécution moindre (temps pour passer les paramètres, brancher à la sous-routine, et retourner les résultats)

Après son exécution, une sous-routine doit retourner l'exécution à l'endroit qui l'a appelée.  Il faut donc garder l'adresse d'où le programme a fait l'appel (l'adresse de retour).  Il y a plusieurs place où un peut garder cette adresse:
- Dans une variable globale (une case mémoire fixe): ceci ne permet pas l'appel de sous-routines à l'intérieur d'une sous-routine.
- Dans une variable locale à la sous-routine: ceci ne permet pas l'appel récursif (une sous-routine qui s'appel elle-même).
- Dans une pile

Dans le cas de la SRC, l'instruction qui sert à garder la valeur du PC (l'adresse de l'instruction suivante) est BRL (pour BRanch-and-Link).  Cette instruction fait un branchement à l'adresse spécifiée dans l'instruction (l'adresse de la sous-routine) et met l'adresse de retour dans un registre.  La sous-routine doit s'occuper de garder la valeur de ce registre pour qu'à la fin de la routine on puisse faire un branchement à cette adresse.  On peut, par exemple, empiler la valeur du registre au début de la sous-routine et la dépiler à la fin.

Pile:
Habituellement elle est réalisée dans la mémoire (un tableau), et on utilise un registre pour nous dire où se trouve le dessus de la pile.  Souvent on met la pile avec son fond dans les grosses adresses, et son dessus dans les adresses plus petite.  Donc, quand on empile, on dimminue l'adresse du dessus de pile.  Ceci permet une utilisation plus complète de la mémoire; l'allocation des objets se faisant à partir des petites adresses, il y aura un problème d'allocation seulement lorsqu'il n'y aura plus de mémoire.

Supposons que r1 est le pointeur de dessus de pile.
Empiler le mot qui est dans le registre r2:
    addi  r1,r1,-4
    st    r2,(r1)
Dépiler un mot dans le registre r2:
    ld    r2,(r1)
    addi  r1,r1,4
Mettre le k-ième mot à partir du dessus de la pile, dans r2:
    ld    r2,k*4(r1)
(ici il faut calculer à la main k*4 puisque l'assembleur utilisé ne fait pas l'arithmétique à l'assemblage)
 

Conventions d'appel:

Ce sont des conventions pour le passage des paramètres, le retour des valeurs et la sauvegarde des registres lors d'appels de sous-routines.  Pour simplifier la lecture d'un programme, on utilise généralement la même convention pour toutes les sous-routines (ou on spécifi clairement, pour chaque sous-routine, quel convention est utilisée).

Passage de paramètres (même chose pour le retour de valeurs):
Les paramètres, ou arguments, d'une fonction (sous-routine) peuvent être passés de différentes façons:
- Les valeurs sont mises dans les registres: rapide mais ne permet pas un grand nombre de valeurs.
- L'adresse d'un tableau est mise dans un registre: permet un grand nombre de valeurs, il faut allouer le tableau.
- Les valeurs et/ou les adresses des valeurs (scalaires ou tableaux) en mémoire sont mises sur la pile.

Exemple de passage par la pile:
; poiteur de pile dans r1
appel: addi  r1,r1,-8  ; Empile deux valeurs (avec les deux additions ensembles)
       st    r2,(r1)   ; Premier argument
       st    r3,4(r1)  ; Deuxieme argument
       la    r0,max    ; L'adresse de la sous-routine
       brl   r2,r0     ; L'appel (met l'adr. de retour dans r2)
                       ; Le resultat est dans r3
; ...
; Prend deux arguments sur la pile et retourne le plus gros des deux dans r3
max:   addi  r1,r1,-4  ; Empile
       st    r2,(r1)   ;   l'adr. de retour
       ld    r2,4(r1)  ; Le premier argument -> r2
       ld    r3,8(r1)  ; Le deuxieme argument -> r3
       sub   r2,r2,r3  ; Compare les deux
       lar   r0,maxY
       brmi  r0,r2     ; Branche si premier<deuxieme
       ld    r3,4(r1)  ; Le premier argument -> r3
maxY:  ld    r2,(r1)   ; Prend l'adresse de retour sur la pile
       addi  r1,r1,12  ; Enleve l'adresse de retour et les arguments de la pile
       br    r2        ; Retourne

Sauvegarde des registres:
Il y a deux conventions principales:
- caller-saved (sauvés par la routine qui fait l'appel): on sauve tous les registres (habituellement sur la pile), qui contiennent des valeurs importantes, avant de faire l'appel d'une sous-routine, et on recharge ces valeurs avant de les utiliser.  Dans une sous-routine on peut écrire dans n'importe quel registre sans avoir à conserver les anciennes valeurs de registres et les restaurer à la fine de la routine.
- callee-saved (sauvés par la routine qui est appelée): au début d'une sous-routine, on sauve tous les registres qu'on utilise, et à la fin on les restaure.  Quand on fait un appel on peut supposer que les registres out la même valeur lors du retour que lors de l'appel de la sous-routine (sauf le registre qui sert à retourner le résultat, si on utilise une convention de retour dans un registre, et celui utilisé pour conserver l'adresse de retour, s'il y a lieu comme dans la SRC).


Exceptions :  (sections 8.3 et.B.2)

Une exception est un événement hors de la séquence normale d'exécution, qui force un changement de cette séquence.
Par exemple on pourrait vouloir afficher une horloge.  Sur un processeur qui génère une exception à chaque seconde, il suffit de faire une routine qui change l'affichage et de dire au processeur que c'est cette routine qu'il faut exécuter lors de cette exception.
S'il n'y avait pas d'exception, il faudrait faire une boucle qui perd son temps (busy waiting) à vérifier l'heure tant que la seconde n'est pas écoulée.
Exception externe - appelée une interruption :  causée par un événement à l'extérieur du processur; habituellement asynchrone (peut arriver entre deux cycles d'horloge).  Par exemple: horloge, souris, clavier, et en générale les entrées/sorties.
Exception interne - appelée un(e) trap :  causée par le processeur.  Par exemple: division par 0, débordement, adresse invalide, instruction invalide, violation de protection d'accès mémoire, défaut de page (pour la mémoire virtuelle).
Les deux types d'exception sont traitées de la même façon par le processeur: exécute une routine pour traiter l'exception.

Les exceptions:
- ont des priorités; s'il y en a plusieurs en même temps le processeur commence par exécuter la routine pour l'exception la plus prioritaire.
- peuvent être masquables ou non-masquables (une exception masquée ne change pas la séquence habituelle d'exécution immédiatement, sa routine de traitement est exécutée lorsqu'on enlève le masque sur l'exception) individuellement ou globalement.
- sont traitées entre les instructions (attendent que l'instruction termine) sauf pour certains traps qui doivent être traités immédiatement (on ne peut pas terminer l'exécution d'une instruction s'il y a un défaut de page, par exemple).
- causent l'exécution d'une routine.  Dans les machines modernes, la routine exécutée dépend du type d'exception et il y a un tableau des mots en mémoire qui dit ce qu'il faut faire pour chaque type d'exception.  Ce tableau est souvent appelé vecteur d'interruption (même s'il contient aussi les informations pour les traps).  Une autre possibilitée est d'avoir une seule routine et un identificateur du type d'interruption (dans un registre) pour permettre une exécution conditionnelle selon le type.

Lors du traitement d'une exception, il faut préserver l'état du processeur pour revenir à cet état lorsque le traitement est fini.  Cette sauvegarde est souvent faite sur la pile (comme dans le cas des routines).  Il faut aussi sauver l'état du processeur tel que vu par le programme interrompu, car l'interruption peut être traitée entre toute paire d'instructions consécutives. Pour les exception qui doivent être traitées en plein milieu d'un intruction (défaut de page dans le cas de mémoire virtuelle) il faut soit préserver le micro-état de la machine (pas nécessairement facile) ou sauver l'état avant l'exécution de l'instruction et recommencer son exécution en entier lors du retour (ce qui est fait sur la plupart des processeurs).

Sur la SRC:
Matériel additionnel pour le traitement des interruption (et exception):
- signaux ireq et iack
- IE (Interrupt Enable): drapeau d'autorisation des interruption (de manière globale)
- IPC<31..0> :  registre d'adresse de retour de l'exception
- II<31..0> :  registre d'identification de la source de l'exception
- Isrc_vect<7..0> := II<23..16> :  type de l'exception (fourni par la source)
- Isrc_info<15..0> := II<15..0> :  information supplémentaire fournie par la source de l'exception
- Instructions: svi, ri, een, edi, rfi
Lors d'une exception le processeur met le PC dans le IPC et passe le contrôle à l'adresse Ivect<31..0> := 20@0#Isrc_vect<7..0>#4@0.
Les modifications à la description du processeur se trouvent dans la section B.2 (page 547).
Remarques:  IE est mis à 0 lors d'une exception, il faut s'assurer de sauver II et IPC avant de mettre IE à 1 (pour permettre les interruptions) dans une routine d'exception.  Aussi, il faut s'assurer qu'il n'y aura pas de trap généré par l'exécution des instructions avant d'avoir sauvé ces registres.

Organisation générale d'un programme qui traite des exceptions:
        la   r0,Start
        br   r0          ; brancher au début du programme
; ...
        .org 16*IntNumber
        str  r0,saveR0
        la   r0,Serve
        br   r0          ; brancher à la routine de traitement de l'exception
saveR0: .dw  1
; ...
        .org 1000h
Serve:  ;...             ; empiler tous les registres...
        svi  r2,r3       ; sauver l'état II, IPC dans r2, r3
        ;...             ; empiler r2,r3
        een              ; autoriser d'autres interruptions
        ;...             ; traitement... (corps de la routine)
        edi              ; inhiber les interruptions
        ;...             ; dépiler r2,r3
        ri   r2,r3       ; restaurer II et IPC
        ;...             ; dépiler tous les registres...
        ld   r0,saveR0   ; restaurer r0
        rfi              ; retour au programme
; ...
Start:  ;...             ; initialiser la pile
        ;...             ; initialisation des exceptions, si nécessaire
        een              ; autoriser les interruptions
;...


Projections en classe (.PDF)