Rappels d’architecture Un ordinateur se compose principalement Assembleur 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 : Les registres En plus de cette mémoire, le processeur dispose de registres. la zone de code, en nombre limité : pour MIPS, 32 registres la zone de données, en taille limitée : 32 ou 64 bits pour la plupart des processeurs 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 ! 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 Langage machinemachine lisible 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 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 Registres 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. 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 Programme 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). .data .byte .halfword .word .space c n1 n2 tab ’a’ 26 353 40 ; octet ; 2 octets ; 4 octets Chargement, sauvegarde Les instructions commençant par l chargent (load) dans les registres, s sauvegardent (save) un registre en mémoire. La zone mémoire du programme est signalée par le mot-clef .text. .text li li saut: add addi bgt $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 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. 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 : Le caractère suivant donne la taille (word ou byte), ou bien immediate pour une valeur directe. lw lb sw li $t1, $t2, $t1, $t2, $t2, 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 J-commandes : sauts 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). Les sauts inconditionnels commencent par j comme jump : L’opération de division utilise ces mêmes registres pour enregistrer le quotient (dans Lo) et le reste (dans Hi). j jal adresse adresse jr $t0 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 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 ? ; ; ; ; va au label "adresse:" enregistre en plus la ligne du programme dans ra va a l’adresse de code t0 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 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). La mauvaise nouvelle : on ne peut pas écrire On ne peut pas écrire i($t0), ni $t1($t0), ni 0(tab). a: b: Pour accéder à l’indice i d’un tableau tab d’entiers de taille 10 : i: tab: 8 0 On se retrouve à factoriser toutes les variables. .data .word 5 .space 40 .text la $t0, li $t1, add $t1, add $t1, add $t0, lw $t2, .data .word .word .data vars: .word .word Ce n’est pas grave, on a justement une table des symboles ! tab i $t1, $t1 $t1, $t1 $t1, $t0 0($t0) Chargement d’adresses nom a b 8 0 Et plus encore ... adresse 0 4 ... Il reste des zones de MIPS que l’on n’utilisera pas : coprocesseur pour flottants exceptions (retenue de l’addition, etc ...) 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 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 Pseudo-instructions 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 01111 $t7 01111 01011 $t3 01011 11101 $sp 11101 00000 (inutilisé) 00000 010100 op2 010100000000 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. Soit, en hexadécimal, 01ebe814. On pourrait donc compiler directement un exécutable MIPS. 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 Exemple 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, a := b + f(2*c); devient ni du protocole des appels de fonction, ni d’autres subtilités de l’assembleur. . . t0 t1 a := := := 2∗c f( t0 ) b + t1 t0 t1 a := := := 2∗c f( t0 ) b + t1 Il s’agit simplement de “découper” un code simple en morceaux de taille (à peu près) trois : deux arguments, un résultat. Instructions Exemple, suite a := b + f(2*c); 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. 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 Production Pour construire le code, on parcourt l’arbre abstrait dans le bon ordre. affect struct triplet{ c3a_op op; int arg1; int arg2; char *var; }; a op b + ligne 0 1 2 3 3 5 6 7 struct triplet code[TAILLECODE]; a := b + f(2*c); 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 La pile Pile et appels fonctionnels . . . retour au véritable assembleur. 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 Appels de fonction Attention, on compte à l’envers en MIPS ! Pour empiler un entier de 4 octets : addi sw $sp, $sp, -4 $t0, 0($sp) le passage des arguments se fait à la main, la récupération du résultat aussi, il n’y a pas de “variable locale”. Pour dépiler, lw addi Les fonctions (et procédures) en assembleur sont simplement des adresses dans le code. $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. 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 On utilise la pile pour stocker le résultat, les arguments, les variables locales. 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 63 (...) 6 ←$sp $t0 3 ←$sp $ra ←$sp a b Appels — côté appelant Appels — côté appelé Pour appeler une fonction, on doit Quand on entre dans une fonction, 1 sauvegarder (= empiler) les registres encore utiles, 1 réserver de la place dans la pile pour les variables locales, 2 réserver de la place pour le résultat, 2 empiler l’adresse de retour $ra. 3 empiler les arguments, 4 sauter (jal) vers l’adresse de la fonction. Quand on en sort, A la réception de l’appel, 2 dépiler l’adresse de retour, désallouer la mémoire des variables locales (et des arguments), 1 1 dépiler le résultat, 3 stocker le résultat, 2 dépiler les registres sauvegardés. 4 sauter vers l’adresse de retour. Exemple : fonction récursive 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. 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