Assembleur Rappels d’architecture Un ordinateur se compose principalement d’un processeur, de mémoire. On y attache ensuite des périphériques, mais ils sont optionnels. données : disque dur, etc entrée utilisateur : clavier, souris sortie utilisateur : écran, imprimante processeur supplémentaire : GPU Mémoire Le processeur peut regarder trois zones de mémoire : la zone de code, la zone de données, la pile. En théorie, toutes les zones sont accessibles de la même façon : on peut donc enregistrer des variables au milieu du code. En pratique, non ! Les registres En plus de cette mémoire, le processeur dispose de registres. en nombre limité : pour MIPS, 32 registres en taille limitée : 32 ou 64 bits pour la plupart des processeurs L’accès est très rapide. De ce fait, le processeur ne réalise des opérations que sur les registres. Il faut donc continuellement faire des transferts entre la mémoire et les registres. Langage machine Une instruction de langage machine correspond à une instruction possible du processeur. Elle contient donc un code correspondant à opération à réaliser, les arguments de l’opération : valeurs directes, numéros de registres, adresses mémoire. code op opérandes Langage machine Si on ouvre un fichier exécutable avec un éditeur (hexadécimal), on obtient ... 01ebe814063727473747566662e6305f5f43544f525f4c 5f05f5f44544f525f4c4953545f5f05f5f4a43525f4c49 53545f5f05f5f646f5f676c6f62616c5f64746f72735f6 75780636f6d706c657465642e36353331064746f725f69 ... Langage machine lisible Si on ouvre un fichier exécutable avec un éditeur (hexadécimal), on obtient ... 01ebe814063727473747566662e6305f5f43544f525f4c 5f05f5f44544f525f4c4953545f5f05f5f4a43525f4c49 53545f5f05f5f646f5f676c6f62616c5f64746f72735f6 75780636f6d706c657465642e36353331064746f725f69 ... C’est une suite d’instructions comme 01ebe814, que l’on peut traduire directement de façon plus lisible : add $t7, $t3 , $sp C’est ce qu’on appelle l’assembleur. Assembleur L’assembleur est donc une représentation du langage machine. Il y a autant d’assembleurs que de type de processeurs différents. En projet, on utilisera l’architecture du processeur (et donc l’assembleur) MIPS. de type RISC (Reduced Instruction Set Computer), c’est-à-dire avec peu d’instructions disponibles sorti dans les années 1980, utilisé jusqu’au début 2000 (PlayStation 2) utilisé en informatique embarquée émulateurs : spim en ligne de commande, qtspim en version graphique (préférable). mars implémentation en JAVA. Registres Un processeur MIPS dispose de 32 registres de 4 octets chacun. On les distingue des adresses par le signe ’$’. On travaillera principalement avec les suivants : numéro 8-15, 24, 25 31 29 0 2, 3 4-7 nom $t0-$t9 $ra $sp $0 $v0,$v1 $a0-$a3 utilité registres courants adresse de retour (après un saut) pointeur de haut de pile la valeur zéro appel et résultat de routines arguments des routines Espace mémoire La zone de mémoire de données se déclare avec le mot-clef .data. C’est là que l’on stocke les “variables”. Le “type” des variables renseigne uniquement sur la place en mémoire de chaque variable : ce ne sont en réalité que des adresses. Le “type” .space indique simplement que l’on réserve le nombre d’octet indiqués (sans les mettre à zéro). c n1 n2 tab .data .byte .halfword .word .space ’a’ 26 353 40 ; octet ; 2 octets ; 4 octets Programme La zone mémoire du programme est signalée par le mot-clef .text. .text li li saut: add addi bgt $t1, $t2, $t1, $t2, $t2, 0 100 $t1, t2 $t2, -1 $0, saut On y lit : des instructions, des points d’accroche (saut), c’est-à-dire des adresses dans le code. Chargement, sauvegarde Les instructions commençant par l chargent (load) dans les registres, s sauvegardent (save) un registre en mémoire. Le caractère suivant donne la taille (word ou byte), ou bien immediate pour une valeur directe. lw lb sw li $t0, $t1, $t0, $t0, a b a 5 ; ; ; ; charge a dans t0 charge le premier octet de b dans t1 enregistre t0 dans a charge 5 dans t0 L’instruction move se contente de copier les registres. move $t0, $t1 ; t0 := t1 Opérations La forme générale des instructions binaires est OP $d $s $t qui veut dire “le registre $d prend la valeur $s OP $t”. OP peut prendre les valeurs suivantes : add, sub, and, or, xor, opérations boolénnes bit-à-bit. Certains ont également la variante OPi, où $t est remplacé par une valeur directe. add ori $t0, $t1, $t2 $t0, $t1, 1 ; t0 = t1 + t2 ; met le bit de poids faible de ; t1 a 1, puis le stocke dans t0 Opérations multiplicatives Une multiplication de deux entiers à 32 bits peut prendre 64 bits. L’opération mult sépare le résultat dans deux registres cachés Lo et Hi. On récupère la valeur avec les opérations mflo, mfhi (move from lo, hi). L’opération de division utilise ces mêmes registres pour enregistrer le quotient (dans Lo) et le reste (dans Hi). div mflo mfhi $t4, $t5 $t2 $t3 ; t2 = t4 / t5 ; t3 = t4 % t5 mult mflo $t4, $t5 $t0 ; t0 = t4 * t5 si ce n’est pas trop grand J-commandes : sauts Les sauts inconditionnels commencent par j comme jump : j jal adresse adresse jr $t0 ; ; ; ; va au label "adresse:" enregistre en plus la ligne du programme dans ra va a l’adresse de code t0 B-commandes : branchements Les saut conditionnels s’appellent branchements et commencent donc par la lettre b. beq bne bgt bge blt, ble li li saut: add addi bgt $t1, $t2, $t1, $t2, $t2, branche (saute) si égal branche si n’est pas égal branche si supérieur (greater than) branche si supérieur ou égal ... inférieur (less than) 0 100 $t1, t2 $t2, -1 $0, saut Que vaut $t1 à la fin de ce morceau de code ? Appels système MIPS permet de communiquer avec le système de façon simple par la commande syscall. La fonction utilisée est déterminée selon la valeur de $v0. $v0 1 4 5 8 commande print int print string read int read string 10 exit argument $a0 = entier à lire $a0 = adresse de chaı̂ne résultat $v0 = entier lu $a0 = adresse de chaı̂ne, $a1 = longueur max Chargement d’adresses Pour accéder à une adresse (un tableau, par exemple), on utilise l’instruction la (load adress). Pour accéder à la valeur adressée par $t0, on écrit 0($t0). On peut également accéder aux valeurs environnantes en variant le numéro, comme 4($t0). On ne peut pas écrire i($t0), ni $t1($t0), ni 0(tab). Pour accéder à l’indice i d’un tableau tab d’entiers de taille 10 : i: tab: .data .word 5 .space 40 .text la $t0, li $t1, add $t1, add $t1, add $t0, lw $t2, tab i $t1, $t1 $t1, $t1 $t1, $t0 0($t0) Chargement d’adresses La mauvaise nouvelle : on ne peut pas écrire a: b: .data .word .word 8 0 On se retrouve à factoriser toutes les variables. .data vars: .word .word 8 0 Chargement d’adresses La mauvaise nouvelle : on ne peut pas écrire a: b: .data .word .word 8 0 On se retrouve à factoriser toutes les variables. .data vars: .word .word 8 0 Ce n’est pas grave, on a justement une table des symboles ! Chargement d’adresses nom a b adresse 0 4 ... On veut calculer a+b, avec a initialisé à 8, b à 0. .data vars: .word .word .text la lw lw add 8 0 $t0, $t1, $t2, $t3, vars 0($t0) 4($t0) $t1, $t2 Et plus encore ... Il reste des zones de MIPS que l’on n’utilisera pas : coprocesseur pour flottants exceptions (retenue de l’addition, etc ...) Voir la table de référence sur la page web du cours. Par exemple, on pourra utiliser l’opération sll (shift left logical) pour multiplier les indices de tableau par 4 : add add $t1, $t1, $t1 $t1, $t1, $t1 sll $t1, $t1, 2 Compilation directe L’assembleur est une version “lisible” du langage machine, mais on pourrait en écrire directement. add $t7, $t3, $sp Le registre $t0 est en fait le numéro 8, et $sp le numéro 29. Le code de l’opération est ici coupé en deux : le premier (0) indique c’est une opération arithmétique, le deuxième de quelle opération arithmétique précisément. op1 000000 $t7 01111 $t3 01011 $sp 11101 (inutilisé) 00000 op2 010100 Compilation directe L’assembleur est une version “lisible” du langage machine, mais on pourrait en écrire directement. add $t7, $t3, $sp Le registre $t0 est en fait le numéro 8, et $sp le numéro 29. Le code de l’opération est ici coupé en deux : le premier (0) indique c’est une opération arithmétique, le deuxième de quelle opération arithmétique précisément. op1 000000 $t7 01111 $t3 01011 $sp 11101 (inutilisé) 00000 op2 010100 Soit, en hexadécimal, 01ebe814. On pourrait donc compiler directement un exécutable MIPS. Pseudo-instructions Certaines opérations ne sont pas des opérations réelles, et ne peuvent pas être traduites directement. Par exemple, l’instruction li (chargement d’une valeur directe) doit être encodée sous une autre forme. li $t0, 12 ori $t0, $0, 12 C’est toutefois une traduction mineure, que l’on ferait si on produisait du code exécutable, mais qui n’apporte pas grand-chose. Références Cours d’architecture de Peter Niebert : http://www.cmi.univ-mrs.fr/~niebert/archi2012.php Introduction au MIPS : http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm Table de référence du MIPS : http://pageperso.lif.univ-mrs.fr/~laurent.braud/compilation/mipsref.pdf Code à trois adresses Intuition Le code à trois adresses est une version simplifiée de l’assembleur où on ne se préoccupe pas ni de l’adressage des variables, ni des registres, ni du protocole des appels de fonction, ni d’autres subtilités de l’assembleur. . . Il s’agit simplement de “découper” un code simple en morceaux de taille (à peu près) trois : deux arguments, un résultat. Exemple a := b + f(2*c); devient t0 t1 a := := := 2∗c f( t0 ) b + t1 Instructions On se donne donc une quantité limitée d’instructions qui imitent l’assembleur. opérations arithmétiques et logiques ; lire, ecrire ; gestion mémoire limitée : load, store, ltab, stab pour les tableaux, loadimm pour les valeurs directes ; sauts : jump, jsifaux ; appels : param pour déclarer un paramètre, call pour appeler une fonction, entree, sortie pour délimiter une fonction. Exemple, suite a := b + f(2*c); t0 t1 a 2∗c f( t0 ) b + t1 := := := En pratique, on oublie les variables temporaires, il est plus simple de se référer au numéro de la ligne même. ligne 0 1 2 3 4 5 6 7 op load loadimm mult param appel load add store arg1 arg2 2 1 2 0 variable c f b 5 6 4 a Implémentation struct triplet{ c3a_op op; int arg1; int arg2; char *var; }; struct triplet code[TAILLECODE]; Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne appel f op 2 * op arg1 c arg2 variable Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 appel f op 2 * op load arg1 c arg2 variable b Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 appel f op 2 * op load loadimm arg1 2 c arg2 variable b Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 appel f op 2 * op load loadimm load arg1 c arg2 variable b 2 c Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 3 appel f op 2 * op load loadimm load mult arg1 c arg2 variable b 2 c 1 2 Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 3 3 appel f op 2 * op load loadimm load mult param arg1 c arg2 variable b 2 c 1 3 2 Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 3 3 5 appel f op 2 * op load loadimm load mult param appel arg1 c arg2 variable b 2 c 1 3 2 f Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 3 3 5 6 appel f op 2 * op load loadimm load mult param appel add arg1 c arg2 variable b 2 c 1 3 2 0 5 f Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect a := b + f(2*c); a op b + ligne 0 1 2 3 3 5 6 7 appel f op 2 * op load loadimm load mult param appel add store arg1 c arg2 variable b 2 c 1 3 2 0 6 5 f a Pile et appels fonctionnels . . . retour au véritable assembleur. La pile Zone de mémoire supposée très grande. Le registre traditionnellement utilisé pour le haut de pile s’appelle $sp. Par convention, il pointe vers le mot (4 octets) le plus en haut. Attention, on compte à l’envers en MIPS ! On augmente la pile en diminuant $sp. données de l’environnement données du programme ←$sp 0 (jamais atteint) La pile Attention, on compte à l’envers en MIPS ! Pour empiler un entier de 4 octets : addi sw $sp, $sp, -4 $t0, 0($sp) Pour dépiler, lw addi $t0, 0($sp) $sp, $sp, 4 On n’a pas besoin de s’occuper de la valeur initiale de $sp, elle est définie comme il faut. Appels de fonction Les fonctions (et procédures) en assembleur sont simplement des adresses dans le code. le passage des arguments se fait à la main, la récupération du résultat aussi, il n’y a pas de “variable locale”. On utilise la pile pour stocker le résultat, les arguments, les variables locales. Exemple : fonction simple function double(n : integer) : integer; begin double := n + n; end; begin a := double (b); end; carre: addi $sp, $sp, -4 main: lw $t0, b sw $ra, 0($sp) addi $sp, $sp, -8 lw $t0, 4($sp) sw $t0, 0($sp) add $t0, $t0, $t0 jal carre sw $t0, 8($sp) lw $t0, 0($sp) lw $ra, 0($sp) addi $sp, $sp, 4 addi $sp, $sp, 8 sw $t0, a jr $ra Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a ←$sp $t0 Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a ←$sp 3 $t0 b Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 3 (...) 3 $t0 ←$sp Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 3 (...) $t0 3 $ra ←$sp Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 3 (...) $t0 3 $ra ←$sp Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 6 (...) $t0 3 $ra ←$sp Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 6 6 $t0 3 $ra ←$sp Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 6 6 ←$sp $t0 Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a 6 6 ←$sp $t0 Exemple de fonction Si b = 3 : carre: addi sw lw add sw lw addi jr main: lw addi sw jal lw addi sw $sp, $ra, $t0, $t0, $t0, $ra, $sp, $ra $sp, -4 0($sp) 4($sp) $t0, $t0 8($sp) 0($sp) $sp, 8 $t0, b $sp, $sp, -8 $t0, 0($sp) carre $t0, 0($sp) $sp, $sp, 4 $t0, a ←$sp 6 $t0 a Appels — côté appelant Pour appeler une fonction, on doit 1 sauvegarder (= empiler) les registres encore utiles, 2 réserver de la place pour le résultat, 3 empiler les arguments, 4 sauter (jal) vers l’adresse de la fonction. A la réception de l’appel, 1 dépiler le résultat, 2 dépiler les registres sauvegardés. Appels — côté appelé Quand on entre dans une fonction, 1 réserver de la place dans la pile pour les variables locales, 2 empiler l’adresse de retour $ra. Quand on en sort, 2 dépiler l’adresse de retour, désallouer la mémoire des variables locales (et des arguments), 3 stocker le résultat, 4 sauter vers l’adresse de retour. 1 Exemple : fonction récursive function serie(n : integer) : integer; begin if n = 0 then serie := 0 else serie := n + serie (n-1); end; Au moment de l’appel récursif ... + n appel ... On charge n dans $t0, mais comme on ne s’en sert pas avant l’appel à la fonction suivante, il faut penser à le sauvegarder. Exemple : fonction récursive function serie(n : integer) : integer; begin if n = 0 then serie := 0 else serie := n + serie (n-1); end; ... addi sw addi li sub addi sw jal lw addi lw addi add ... $sp, $sp, -4 $t0, 0($sp) $sp, $sp, -4 $t1, 1 $t0, $t0, $t1 $sp, $sp, -8 $t0, 0($sp) serie $t1, 0($sp) $sp, $sp, 4 $t0, 0($sp) $sp, $sp, 4 $t0, $t0, $t1