Résumé Analyse INFO0004 Structure des langages de programmation Génération de code ; liaison et exécution Justus H. Piater Programmation Analyse Compilation Liaison fichier texte Analyse lexicale flux de jetons Analyse syntaxique arbre de syntaxe abstraite Interprétation Exécution Analyse sémantique arbre annoté 2 / 51 Résumé Génération de code Le code intermédiaire arbre annoté Programmation Création de code intermédiaire Analyse pseudo- instructions en blocs élémentaires Compilation Amélioration pseudo- instructions en blocs élémentaires Linkage Génération de code Interprétation Exécution code assembleur Amélioration code assembleur Assemblage code binaire 3 / 51
Code intermédiaire Fibonacci ressemble au code assembleur : code linéaire registres (nombre infini) données brutes indépendant d une machine quelconque pour effectuer des optimisations pour réduire au minimum le code spécifique à la machine cible // compute result the nth Fibonacci number void main() { int n, fib0, fib1, temp, result; n = 8; fib0 = 0; fib1 = 1; while (n > 0) { temp = fib0; fib0 = fib1; fib1 = fib0 + temp; n = n 1; result = fib0; Le code intermédiaire 5 / 51 Le code intermédiaire 6 / 51 Fibonacci : code intermédiaire Cf. UASM Block 0: LoadC v0, 8 Store n, v0 LoadC v1, 0 Store fib0, v1 LoadC v2, 1 Store fib1, v2 Block 1: Load v12, n LoadC v13, 0 v11 := v12 > v13 If v11 Goto Block 2 Goto Block 3 Block 2: Load v3, fib0 Store temp, v3 Load v4, fib1 Store fib0, v4 Load v6, fib0 Load v7, temp v5 := v6 + v7 Store fib1, v5 Load v9, n LoadC v10, 1 v8 := v9 v10 Store n, v8 Goto Block 1 Block 3: Load v14, fib0 Store result, v14 Fibonacci : code intermédiaire (suite) Le code intermédiaire 7 / 51 Le code intermédiaire 8 / 51
Représenter le code intermédiaire Créer du code intermédiaire Chaque instruction du code intermédiaire est une structure de données qui encode : le type de l instruction (Load, :=, ) le type de chaque opérande (registre virtuel, variable globale, variable locale, ou constante) l opérateur (le cas échéant) des informations sémantiques supplémentaires tout ce qui est utile pour l amélioration et génération du code Formellement : spécification par une grammaire attribuée (attribute grammar) Pour nous : Très similaire à l interprétation du code (voir les méthodes sémantiques) : On parcourt l arbre de syntaxe abstraite; à chaque nœud, on crée des instructions. En vue d une amélioration du code par la suite, on ne réutilise jamais des registres ; un nouveau registre virtuel est prévu chaque fois. Le code intermédiaire 9 / 51 Le code intermédiaire 10 / 51 Les blocs élémentaires Assignment Pour gérer des sauts, il faut insérer des étiquettes. Le morceau de code entre une étiquette et le prochain saut constitue un bloc élémentaire (basic block). Nous allons couper le code en blocs élémentaires (pas essentiel ici, mais pour l amélioration du code plus tard). Ainsi, on crée un graphe de flot de contrôle (control flow graph). BasicBlock* generate(const Assignment* s, BasicBlock* bb) { bb >vec.push_back(new MStore(s >target, generate(s >source, bb))); return bb; Il faut passer le bloc pour savoir où ajouter le code. La source est un registre virtuel. Il faut renvoyer le bloc parce qu une instruction peut rompre le bloc élémentaire actuel. Note Cette implémentation fonctionne parce qu ici, une expression ne rompt jamais le bloc élémentaire actuel. Le code intermédiaire 11 / 51 Le code intermédiaire 12 / 51
Binary Loop VirtualRegister* generate(const Binary* e, BasicBlock* bb) { VirtualRegister* target = newregister(); bb >vec.push_back(new MBinary(e >op, target, generate(e >term1, bb), generate(e >term2, bb) )); return target; BasicBlock* generate(const Loop* s, BasicBlock* bb) { BasicBlock* bbtest = basicblocks >getnewbasicblock(); bb >vec.push_back(new MGoto(bbTest)); BasicBlock* bbbodybeg = basicblocks >getnewbasicblock(); BasicBlock* bbbodyend = generate(s >body, bbbodybeg); bbbodyend >vec.push_back(new MGoto(bbTest)); VirtualRegister* test = generate(s >test, bbtest); BasicBlock* bbnext = basicblocks >getnewbasicblock(); bbtest >vec.push_back(new MCondGoto(test, bbbodybeg)); bbtest >vec.push_back(new MGoto(bbNext)); return bbnext; Le code intermédiaire 13 / 51 Le code intermédiaire 14 / 51 La génération du code assembleur Allocations familières arbre annoté Création de code intermédiaire pseudo- instructions en blocs élémentaires Génération de code code assembleur Allocation des registres Allocation des variables locales Allocation des variables globales Allocation des constantes La génération du code assembleur 16 / 51
Allocation des registres Allocation de ressources Équivalent à la coloration de graphe (NP-hard) L idée : Nœuds sont registres virtuels, arêtes connectent des nœuds simultanément actifs. Couleurs sont registres réels. Faisons-le pour Fibonacci en regardant les plages actives (live ranges) des registres virtuels Simplification bloc-par-bloc : Sans plages actives inter-blocs élémentaires, un algorithme d allocation de ressources convient. Problème : Nous avons un ensemble de tâches. Chaque tâche a une heure de début et une heure de fin avec. Chaque tâche doit être exécutée sur une machine ; une machine ne peut exécuter qu une tâche à la fois. Deux tâches et ne sont pas en conflit si ou. Il s agit d exécuter toutes les tâches sur un nombre minimal de machines. La génération du code assembleur 17 / 51 Algorithme La génération du code assembleur 18 / 51 Correction Temps d exécution? Algorithm resourceallocation( ): Input: A set of tasks with start and end times and. Output: A minimal, nonconflicting allocation of machine for all tasks in. // minimal number of machines while do remove from the task with smallest if there is a machine with no task conflicting with then schedule task on machine else // add a new machine schedule task on machine Proposition : L algorithme resourceallocation() est correct. Preuve (par contradiction) : Supposons que l algorithme trouve une solution avec machines, alors qu il existe une solution avec machines. Soit la première tâche allouée sur la machine. Quand l algorithme traita la tâche, toutes les machines contenaient des tâches en conflict avec et aussi (par le même argument) entre elles, ce qui implique la présence dans de tâches en conflit entre elles. La génération du code assembleur 19 / 51 La génération du code assembleur 20 / 51
Nombre insuffisant de registres? Déborder des registres vers la pile. Avant d allouer les registres, diviser la durée de vie d un registre virtuel en plusieurs intervalles. Défi : Minimiser le coût des Store/Load. Trois registres réels suffisent toujours. Trivial : L allocation des variables et constantes Parcourir toutes les instructions intermédiaires pour créer le tableau de symboles (normalement c est déjà fait lors de l analyse syntaxique). À chaque variable locale, affecter une adresse relative au frame pointer dans le cadre de pile. À chaque variable et constante statique, affecter une adresse relative au début de la section des données statiques. On parlera plus tard des variables globales. La génération du code assembleur 21 / 51 La génération du code assembleur 22 / 51 Créer du code assembleur Créer du code assembleur (suite) Chaque instruction intermédiaire est traduite en une ou plusieurs instructions assembleur, en utilisant les registres réels / adresses des variables allouées. (On peut laisser tomber les petites constantes si elles sont toutes utilisées en tant qu argument immédiat.) Cas spéciaux : des constantes suffisamment petites pour les intégrer dans une instruction immédiate des opérateurs du code intermédiaire sans équivalent en assembleur (toutes les variantes de <, >, immédiat ou non, existent-elles?) On crée un bloc élémentaire à la fois, chacun précédé par son étiquette. Finalement, on crée les variables statiques et les constantes, chacune précédée par son étiquette. La génération du code assembleur 23 / 51 La génération du code assembleur 24 / 51
La machine Beta Fibonacci : UASM architecture RISC, 32 bits Toute opération sur des valeurs se fait entre registres. 32 registres, dont : R0 valeur de retour R27 BP base (frame) pointer R28 LP linkage pointer R29 SP stack pointer R30 XP exception pointer R31 toujours zéro Pas de caches, délais, Cf. IMF.include beta.uasm.options annotate.protect prologue: CMOVE(stack,SP) MOVE(SP,BP) ALLOCATE(5) block0: CMOVE(8, r7) 8 ST(r7, 12, BP) n CMOVE(0, r6) 0 ST(r6, 8, BP) fib0 CMOVE(1, r5) 1 ST(r5, 4, BP) fib1 BR(block1) block1: La génération du code assembleur 25 / 51 Fibonacci : UASM (suite) La génération du code assembleur 26 / 51 Fibonacci : UASM (suite) LD(BP, 12, r10) n CMOVE(0, r9) 0 CMPLE(r10, r9, r11) XORC(r11, 1, r11) BT(r11, block2) BR(block3) block2: LD(BP, 8, r4) fib0 ST(r4, 16, BP) temp LD(BP, 4, r3) fib1 ST(r3, 8, BP) fib0 LD(BP, 8, r1) fib0 LD(BP, 16, r15) temp ADD(r1, r15, r2) ST(r2, 4, BP) fib1 LD(BP, 12, r13) n CMOVE(1, r12) 1 SUB(r13, r12, r14) ST(r14, 12, BP) n BR(block1) block3: LD(BP, 8, r8) ST(r8, 0, BP) fib0 result epilogue: don't clean up stack for debugging.breakpoint HALT().unprotect stack: La génération du code assembleur 27 / 51 La génération du code assembleur 28 / 51
Goto BR, Load LD L assemblage void generatecode(const MGoto* ii, ofstream& outfile) const { outfile << indent << "BR(block" << ii >bb >getid() << ")\n"; void generatecode(const MLoad* ii, ofstream& outfile) const { outfile << indent << "LD(BP, " << VAR_SIZE * dict >getoffset(*(ii >source)) << ", r" << dict >getoffset(*(ii >target)) << ")\t " << ii >source >id << "\n"; Expansion de macros Remplacer les mnémoniques et les opérandes par le code binaire de la machine Remplacer des étiquettes symboliques par des adresses Parfois, amélioration du code. La génération du code assembleur 29 / 51 La génération du code assembleur 30 / 51 La liaison Résumé Liaison et exécution fichiers objet, librairies Programmation Réadressage Analyse Résolution de noms Compilation fichier exécutable Liaison Chargement Interprétation Exécution processus Liaison dynamique La liaison 32 / 51
La liaison 500 300 800 Une illustration 1600 1500 1000 400 3000 2300 900 1800 500 800 300 1. Placement des fichiers objet dans un ordre quelconque 2. Réadressage (relocation) insertion des bonnes adresses des destinations internes 3. Résolution de noms insertion des bonnes adresses des destinations externes a. Recherche d un nom non résolu dans les librairies b. Procédure récursive pour des objets des librairies Imports M M Exports X Réadressage Code r1 := &M call M Données X: Fichiers objets réadressables Imports X Exports M Réadressage Code L: M: Données Y: r1 := &L(1000) r2 := Y(400) r3 := X Fichier objet exécutable Code r1 := &M(2300) call M(2300) r1 := &L(1800) r2 := Y(3900) r3 := X(3300) L: M: Données X: Y: [Adapté de Scott 2000 Fig. 9.10] La liaison 33 / 51 Le contrôle de types La liaison 34 / 51 Un exemple: fibonacci.cc Par inclusion d un symbole fictif qui représente une définition d interface (p.ex., checksum) Par codage de la signature d une méthode dans son nom (name mangling) // compute result the nth Fibonacci number #include <cstdio> static int lastfib; int fibonacci(int n) { int fib0 = 0, fib1 = 1, temp; for (; n > 0; n ) { printf("%d iterations to go\n", n); temp = fib0; fib0 = fib1; fib1 = fib0 + temp; return lastfib = fib0; La liaison 35 / 51 La liaison 36 / 51
fibonacci.cc compilé, désassemblé 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 18 sub $0x18,%esp 6: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp) d: c7 45 f8 01 00 00 00 movl $0x1,0xfffffff8(%ebp) 14: 83 7d 08 00 cmpl $0x0,0x8(%ebp) 18: 7f 02 jg 1c <_Z9fibonaccii+0x1c> 1a: eb 2d jmp 49 <_Z9fibonaccii+0x49> 1c: 83 ec 08 sub $0x8,%esp 1f: ff 75 08 pushl 0x8(%ebp) 22: 68 00 00 00 00 push $0x0 27: e8 fc ff ff ff call 28 <_Z9fibonaccii+0x28> 2c: 83 c4 10 add $0x10,%esp 2f: 8b 45 fc mov 0xfffffffc(%ebp),%eax 32: 89 45 f4 mov %eax,0xfffffff4(%ebp) 35: 8b 45 f8 mov 0xfffffff8(%ebp),%eax 38: 89 45 fc mov %eax,0xfffffffc(%ebp) 3b: 8b 45 f4 mov 0xfffffff4(%ebp),%eax fibonacci.cc compilé, désassemblé (suite) 3e: 03 45 fc add 0xfffffffc(%ebp),%eax 41: 89 45 f8 mov %eax,0xfffffff8(%ebp) 44: ff 4d 08 decl 0x8(%ebp) 47: eb cb jmp 14 <_Z9fibonaccii+0x14> 49: 8b 45 fc mov 0xfffffffc(%ebp),%eax 4c: a3 00 00 00 00 mov %eax,0x0 51: c9 leave 52: c3 ret La liaison 37 / 51 fibonacci.cc compilé, optimisé, désassemblé 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 57 push %edi 4: 56 push %esi 5: 53 push %ebx 6: 83 ec 0c sub $0xc,%esp 9: 8b 5d 08 mov 0x8(%ebp),%ebx c: be 00 00 00 00 mov $0x0,%esi 11: bf 01 00 00 00 mov $0x1,%edi 16: 85 db test %ebx,%ebx 18: 7e 1d jle 37 <_Z9fibonaccii+0x37> 1a: 83 ec 08 sub $0x8,%esp 1d: 53 push %ebx 1e: 68 00 00 00 00 push $0x0 23: e8 fc ff ff ff call 24 <_Z9fibonaccii+0x24> 28: 89 f0 mov %esi,%eax 2a: 89 fe mov %edi,%esi 2c: 8d 3c 38 lea (%eax,%edi,1),%edi La liaison 38 / 51 fibonacci.cc compilé, optimisé, désassemblé (suite) 2f: 83 c4 10 add $0x10,%esp 32: 4b dec %ebx 33: 85 db test %ebx,%ebx 35: 7f e3 jg 1a <_Z9fibonaccii+0x1a> 37: 89 35 00 00 00 00 mov %esi,0x0 3d: 89 f0 mov %esi,%eax 3f: 8d 65 f4 lea 0xfffffff4(%ebp),%esp 42: 5b pop %ebx 43: 5e pop %esi 44: 5f pop %edi 45: 5d pop %ebp 46: c3 ret La liaison 39 / 51 La liaison 40 / 51
Tableaux de réadressage et de symboles objdump -r fibonacci.o RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000001f R_386_32.rodata.str1.1 00000024 R_386_PC32 printf 00000039 R_386_32.bss nm fibonacci.o 00000000 b lastfib U printf 00000000 T _Z9fibonaccii Les différentes sections du fichier objet objdump -h fibonacci.o Idx Name Size VMA LMA File off Algn 0.text 00000047 00000000 00000000 00000034 2**2 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1.data 00000000 00000000 00000000 0000007c 2**2 CONTENTS, ALLOC, LOAD, DATA 2.bss 00000004 00000000 00000000 0000007c 2**2 ALLOC 3.rodata.str1.1 00000015 00000000 00000000 0000007c 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4.note.GNU-stack 00000000 00000000 00000000 00000091 2**0 CONTENTS, READONLY 5.comment 00000032 00000000 00000000 00000091 2**0 CONTENTS, READONLY La liaison 41 / 51 La liaison 42 / 51 Le chargement Lancer un processus avec mémoire virtuelle 1. S il n y est pas encore, charger le programme dans la mémoire 2. Prévoir une petite pile 3. Prévoir un petit tas 4. Démarrer le programme à l adresse zéro Le chargement 44 / 51
Lancer un processus sans mémoire virtuelle 1. S il n y est pas encore, charger le programme dans la mémoire, à une adresse donnée 2. Réadressage 3. Prévoir la pile 4. Prévoir le tas 5. Démarrer le programme à l adresse donnée La liaison dynamique Observation : Beaucoup de programmes et de processus partagent de grosses librairies. Idée : Créer une seule copie de ce code les librairies partagées. Problème : Le même code peut se trouver à des différentes adresses pour différents processus! Solution : PIC (Position-Independent Code) pour éviter le réadressage : toutes les adresses relatives au PC accès aux données non-partagées par un niveau supplémentaire d indirection le tableau de liaison Le chargement 45 / 51 Le chargement 46 / 51 Une illustration La liaison paresseuse gp(main) C B A main: *(sp+ N) := gp - - call foo: t9 := *(gp+ A) jalr t9 gp := *(sp+ N) - - load X: t0 := *(gp+ C) t0 := *t0 - - load Y: t0 := *(gp+ B) t0 := *t0 gp(foo) D E F G foo: gp := t9+ (E- D) - - load X: t0 := *(gp+ F) t0 := *t0 - - load Y: t0 := *(gp+ G) t0 := *t0 Librairie partagée liée dynamiquement Code partagé (PIC) Tableau de liaison (un par processus) Observation : Idée : Principe : Beaucoup de liaisons ne sont jamais utilisées lors de la durée de vie d un processus. Lier en cas de besoin. Lier des morceaux de code qui appellent le lieur dynamique, qui les remplacera par les vrais liens. X: Données privées Y: (par processus) [Adapté de Scott 2000 Fig. 9.11] Le chargement 47 / 51 Le chargement 48 / 51
Résumé Résumé Génération de code : code intermédiaire code assembleur Liaison et chargement : réadressage résolution de noms librairies partagées, PIC, liaison dynamique Résumé 50 / 51 References M. Scott, Programming Language Pragmatics, Morgan Kaufmann 2000. Résumé 51 / 51