Previous Up Next

8  Quelques conseils utiles

8.1  Quand commencer

Tout de suite!

D’abord, il vaut mieux se mettre à faire le projet de programmation le plus tôt possible! J’estime que vous pouvez faire un compilateur C--, sans les exceptions, en deux à trois semaines, à condition de ne pas tarder à s’y mettre. La gestion des exceptions (throw, trycatchfinally) est quelque chose d’un peu compliqué, qu’il vaut mieux aborder avec méthode.

8.2  La débrouille

Un peu de débrouille ne fait pas de mal… Si vous ne voyez pas bien comment compiler quelque chose que vous sauriez écrire en C, écrivez-le en C, appelez gcc -S pour produire le code assembleur correspondant, et déduisez-en quel code assembleur gcc fabrique. Par exemple, si vous ne savez pas comment calculer un quotient de deux entiers, créez un ficher /tmp/a.c, contenant:

et tapez gcc -S a.c dans le répertoire /tmp. Vous obtiendrez un fichier /tmp/a.s de la forme:

dont vous pourrez déduire que le quotient se calcule par une combinaison des instructions cltd et idivl: voir la section 9. (C’est l’instruction la plus compliquée à compiler, hormis les exceptions.)

8.3  Le style de programmation

On peut programmer en Caml en différents styles. Par exemple, une factorielle en style fonctionnel s’écrira:

La même, en style impératif, pourra s’écrire:

On peut encore l’écrire en style objet, soit grâce aux caractéristiques objet d’OCaml, soit en se fabriquant des classes à la main.

Tous les styles sont bons, et l’on peut coder ce que l’on veut dans chaque style. Chacun a ses préférences. Cependant, il est bon de savoir que le compilateur que vous avez à écrire s’écrit probablement le plus naturellement du monde dans un style fonctionnel.

Par exemple, j’ai défini dans ma version du compilateur une fonction compile_expr pour compiler les expressions, de type loc_expr, et une fonction compile_code pour compiler les instructions et les programmes, de type loc_code, les deux fonctions s’appelant récursivement, et la seconde fonction appelant naturellement la première.

Au passage, le type de ma fonction compile_expr est:

out_channel -> string -> (string * int) list -> int -> loc_expr -> unit

L’idée est que l’appel compile_expr out f env stack e, compile le code assembleur qui évalue l’expression e et retourne sa valeur dans %eax, où:

Il est quand même pratique d’utiliser quelques références (ref, !, :=), sans en abuser. Dans mon compilateur, je n’ai qu’une seule référence, qui est de plus une variable globale:

let strings = ref ([] : (string * string) list);;

et qui sert à associer à chaque constante chaîne (par exemple, "r" dans le fichier cat.c, mais aussi les noms d’exception comme Trouve dans le fichier exc1.c) le nom d’un label où cette chaîne sera stockée. Regardez bien, par exemple, où la chaîne "r" est décrite dans le fichier assembleur cat.s. Si vous ne comprenez pas tout de suite à quoi peut servir la variable globale strings ci-dessus, vous le comprendrez en écrivant le compilateur…

8.4  Quelques trucs sur les exceptions

En ce qui concerne les exceptions, leur sémantique, décrite dans un cadre plus simple que celui de C--, est donnée en section 7. C’est un peu compliqué à compiler, et la clause finally est particulièrement retorse. (Vous comprendrez pourquoi en la compilant…) Néanmoins, et même s’il existe de nombreuses façons de le faire, vous pouvez vous inspirer de ce que mon compilateur fait. Ma version du compilateur mcc a le bon goût d’ajouter plein de commentaires dans l’assembleur produit aux alentours des throw et des trycatchfinally pour expliquer ce que le code qu’il produit est censé faire, et sur quelles conventions (usage des registres %ebx, %ecx, ce qu’il y a sur la pile, comment la pile __exception_handler des handlers d’exception courants est gérée et ce qu’elle contient, etc.) Par exemple, le fichier exc1.s, qui est un exemple très simple d’utilisation des exceptions, sans clause finally, que voici:

se compile dans l’assembleur suivant, abondamment commenté quant à la gestion des exceptions:

.globl __exception_handler .bss .align 4 .type __exception_handler, @object .size __exception_handler, 4 __exception_handler: .zero 4 .text .globl f .type f,@function f: pushl %ebp movl %esp, %ebp movl $0, %eax pushl %eax movl 12(%ebp), %eax popl %ebx cmpl %ebx, %eax je .f_3 movl $0, %eax jmp .f_4 .f_3: movl $1, %eax .f_4: cmpl $0, %eax je .f_1 movl 8(%ebp), %eax movl %eax, %ecx movl $.f_5, %ebx // On lance l'exception %ebx, avec valeur %ecx. // Depilons __exception_handler. movl __exception_handler, %eax // On doit d'abord verifier que __exception_handler!=NULL. cmpl $0, %eax jne .f_6 // Si __exception_handler==NULL, on est arrive au bout de la pile d'exceptions, // et l'on choisit d'afficher un message informatif et de s'arreter en urgence, // plutot que de laisser le programme se planter salement tout seul. pushl %ebx pushl $.f_7 pushl stderr call fprintf call fflush jmp abort .f_6: // Sinon, on recupere la suite de la pile d'exceptions dans %esi, // et on la met dans __exception_handler. // Il serait plus elegant d'appeler free() sur %esi, aussi (laisse en exercice). movl (%eax), %esi movl %esi, __exception_handler // On recupere l'adresse ou il faudra continuer l'execution dans %esi, movl 4(%eax), %esi // puis le %ebp sauvegarde, movl 12(%eax), %ebp // puis le %esp sauvegarde (ce qui revient a depiler %esp assez brutalement). movl 8(%eax), %esp // Et hop, on saute vers le code qui va traiter l'exception. jmp *%esi jmp .f_2 .f_1: .f_2: movl $1, %eax pushl %eax movl 12(%ebp), %eax popl %ebx cmpl %ebx, %eax je .f_10 movl $0, %eax jmp .f_11 .f_10: movl $1, %eax .f_11: cmpl $0, %eax je .f_8 movl 8(%ebp), %eax movl %eax, %ecx movl $.f_12, %ebx // On lance l'exception %ebx, avec valeur %ecx. // Depilons __exception_handler. movl __exception_handler, %eax // On doit d'abord verifier que __exception_handler!=NULL. cmpl $0, %eax jne .f_13 // Si __exception_handler==NULL, on est arrive au bout de la pile d'exceptions, // et l'on choisit d'afficher un message informatif et de s'arreter en urgence, // plutot que de laisser le programme se planter salement tout seul. pushl %ebx pushl $.f_7 pushl stderr call fprintf call fflush jmp abort .f_13: // Sinon, on recupere la suite de la pile d'exceptions dans %esi, // et on la met dans __exception_handler. // Il serait plus elegant d'appeler free() sur %esi, aussi (laisse en exercice). movl (%eax), %esi movl %esi, __exception_handler // On recupere l'adresse ou il faudra continuer l'execution dans %esi, movl 4(%eax), %esi // puis le %ebp sauvegarde, movl 12(%eax), %ebp // puis le %esp sauvegarde (ce qui revient a depiler %esp assez brutalement). movl 8(%eax), %esp // Et hop, on saute vers le code qui va traiter l'exception. jmp *%esi jmp .f_9 .f_8: .f_9: movl $2, %eax pushl %eax movl 12(%ebp), %eax popl %ebx cltd idivl %ebx movl %edx, %eax cmpl $0, %eax je .f_14 movl $1, %eax pushl %eax movl 12(%ebp), %eax pushl %eax movl $3, %eax popl %ebx imull %ebx, %eax popl %ebx addl %ebx, %eax pushl %eax movl $1, %eax pushl %eax movl 8(%ebp), %eax popl %ebx addl %ebx, %eax pushl %eax call f addl $8, %esp movl %ebp, %esp popl %ebp ret jmp .f_15 .f_14: movl $2, %eax pushl %eax movl 12(%ebp), %eax popl %ebx cltd idivl %ebx pushl %eax movl $1, %eax pushl %eax movl 8(%ebp), %eax popl %ebx addl %ebx, %eax pushl %eax call f addl $8, %esp movl %ebp, %esp popl %ebp ret .f_15: movl %ebp, %esp popl %ebp ret .globl main .type main,@function main: pushl %ebp movl %esp, %ebp subl $4, %esp movl $2, %eax pushl %eax movl 8(%ebp), %eax popl %ebx cmpl %ebx, %eax je .main_20 movl $0, %eax jmp .main_21 .main_20: movl $1, %eax .main_21: cmpl $0, %eax je .main_18 movl $0, %eax jmp .main_19 .main_18: movl $1, %eax .main_19: cmpl $0, %eax je .main_16 movl $.main_22, %eax pushl %eax movl stderr, %eax pushl %eax call fprintf addl $8, %esp movl stderr, %eax pushl %eax call fflush addl $4, %esp movl $10, %eax pushl %eax call exit addl $4, %esp jmp .main_17 .main_16: .main_17: movl $1, %eax pushl %eax movl 12(%ebp), %eax popl %ebx movl (%eax, %ebx, 4), %eax pushl %eax call atoi addl $4, %esp movl %eax, -4(%ebp) // Debut d'une instruction try { ... }. // On va empiler dans la liste __exception_handler suffisamment d'infos // pour pouvoir retrouver nos billes lorsqu'une exception sera lancee par throw. // Pour ceci, on alloue une structure que l'on pourrait ecrire en C: // struct xhandler { struct xhandler *next; char *next_eip, *save_esp, *save_ebp; } // Le champ next sert a retrouver la suite de la liste, // le champ next_eip sera l'adresse du code qui traitera les catch { ... } // et les finally du try { ... } courant, // le champ save_esp est une sauvegarde du pointeur de pile courant, // et save_ebp sauvegarde %ebp. // Cette structure fait 16 octets: on commence par l'allouer. push $16 call malloc addl $4, %esp // %eax->next = __exception_handler; movl __exception_handler, %ebx movl %ebx, (%eax) // exception_handler = %eax; movl %eax, __exception_handler // %eax->next_eip = &.main_23; // c'est la qu'on ira si une exception est lancee! movl $.main_23, %ebx movl %ebx, 4(%eax) // %eax->save_esp = %esp; movl %esp, 8(%eax) // %eax->save_ebp = %ebp; movl %ebp, 12(%eax) // Maintenant que le handler a ete empile, on passe au corps du try { ... }. movl -4(%ebp), %eax pushl %eax movl $0, %eax pushl %eax call f addl $8, %esp movl $.main_27, %eax pushl %eax movl stderr, %eax pushl %eax call fprintf addl $8, %esp // Fin du corps du try { ... }. On doit depiler le handler d'exception, // et traiter le finally { ... } s'il y en a un. Ceci sera fait en .main_24. jmp .main_24 // Ici, on va traiter des catch (...) successifs. // Le nom de l'exception sera dans le registre %ebx, // la valeur de l'exception sera dans %ecx, // et le handler empile par le try { ... } a deja ete depile par le throw responsable du lancement de l'exception. .main_23: // On commence par empiler la valeur %ecx de l'exception. pushl %ecx // Entree de catch(Trouve ...): on compare le nom de l'exception a 'Trouve'. pushl %ebx pushl $.f_12 call strcmp addl $8, %esp // Note: %ebx n'est pas modifie par strcmp(), on dispose donc toujours du nom de l'exception. cmpl $0, %eax jnz .main_28 // Corps du catch (Trouve ...). movl -4(%ebp), %eax pushl %eax movl -8(%ebp), %eax pushl %eax movl $.main_29, %eax pushl %eax movl stderr, %eax pushl %eax call fprintf addl $16, %esp // On depile la valeur de l'exception, et on va traiter le finally { ... } s'il y en a un. addl $4, %esp jmp .main_25 .main_28: // Exception non rattrapee: on la relance. popl %ecx // On lance l'exception %ebx, avec valeur %ecx. // Depilons __exception_handler. movl __exception_handler, %eax // On doit d'abord verifier que __exception_handler!=NULL. cmpl $0, %eax jne .main_30 // Si __exception_handler==NULL, on est arrive au bout de la pile d'exceptions, // et l'on choisit d'afficher un message informatif et de s'arreter en urgence, // plutot que de laisser le programme se planter salement tout seul. pushl %ebx pushl $.f_7 pushl stderr call fprintf call fflush jmp abort .main_30: // Sinon, on recupere la suite de la pile d'exceptions dans %esi, // et on la met dans __exception_handler. // Il serait plus elegant d'appeler free() sur %esi, aussi (laisse en exercice). movl (%eax), %esi movl %esi, __exception_handler // On recupere l'adresse ou il faudra continuer l'execution dans %esi, movl 4(%eax), %esi // puis le %ebp sauvegarde, movl 12(%eax), %ebp // puis le %esp sauvegarde (ce qui revient a depiler %esp assez brutalement). movl 8(%eax), %esp // Et hop, on saute vers le code qui va traiter l'exception. jmp *%esi .main_24: // Aucune exception n'a ete lancee, il faut d'abord // depiler __exception_handler avant de continuer. // Depilons __exception_handler. movl __exception_handler, %eax // On doit d'abord verifier que __exception_handler!=NULL. cmpl $0, %eax jne .main_31 // Si __exception_handler==NULL, on est arrive au bout de la pile d'exceptions, // et l'on choisit d'afficher un message informatif et de s'arreter en urgence, // plutot que de laisser le programme se planter salement tout seul. pushl %ebx pushl $.f_7 pushl stderr call fprintf call fflush jmp abort .main_31: // Sinon, on recupere la suite de la pile d'exceptions dans %esi, // et on la met dans __exception_handler. // Il serait plus elegant d'appeler free() sur %esi, aussi (laisse en exercice). movl (%eax), %esi movl %esi, __exception_handler // Ici, l'exception a ete rattrapee par un catch, et __exception_handler // a ete depile par le throw. On fait le finally et on continue. .main_25: movl stderr, %eax pushl %eax call fflush addl $4, %esp movl $0, %eax movl %ebp, %esp popl %ebp ret addl $4, %esp movl %ebp, %esp popl %ebp ret .main_29: .string "La suite termine apres %d iterations en partant de %d.\n" .align 4 .main_27: .string "Pas trouve...\n" .align 4 .main_22: .string "Usage: ./exc1 <n>\ncalcule a quelle iteration une suite mysterieuse termine, en partant de <n>.\n" .align 4 .f_12: .string "Trouve" .align 4 .f_7: .string "Uncaught exception %s: abort.\n" .align 4 .f_5: .string "Zero" .align 4

8.5  Les fichiers ML fournis

Dans la distribution mcc qui vous est fournie, il ne manque que le fichier compile.ml, que vous devrez écrire. Certains pensent qu’il faut qu’ils lisent les fichiers ML fournis pour comprendre comment ça marche. En principe, je favorise la curiosité: si vous pouvez apprendre quelque chose en le faisant, faites-le!

Mais, en l’occurrence, ceci ne vous servira pas à grand-chose. Le code ML est probablement trop complexe, et utilise probablement des choses que vous ne connaissez pas (ocamllex, ocamlyacc), pour que vous puissiez en tirer vraiment profit.

Lire les fichiers ML fournis est donc, en fait, essentiellement une perte de temps dans le cadre du projet.

En revanche, il peut être utile de comprendre quel genre de syntaxe abstraite est produite par mcc à partir d’un fichier source C-- donné. Je rappelle que cette syntaxe abstraite est de type Cparse.var_declaration list, le type du deuxième argument de la fonction compile que vous devez écrire.

Une façon de faire est de réserver une variable globale Caml, disons ast (pour “abstract syntax tree”), de stocker l’arbre de syntaxe fourni à compile dedans, et de demander à afficher le contenu de ast après exécution, ce qui se fait bien sous le toplevel.

Pour utiliser les fonctions du compilateur mcc sous le toplevel Caml, lancer ocaml, et taper:

#use "top.ml";;

Le fichier top.ml est en gros le compilateur mcc en réduction. Si l’on écrit un fichier compile.ml bidon:

puis que l’on tape make pour recompiler le tout, enfin que l’on tape #use "top.ml";; sous le toplevel Caml (ce qui charge tous les fichiers compilés, y compris compile.ml), on obtiendra une fonction teste de type string -> unit.

Donnons-lui à traiter le fichier cat.c, par exemple. Contrairement au compilateur final mcc, le programme que nous venons de charger sous le toplevel Caml ne sait pas appeler le préprocesseur C tout seul. Appelons-le donc pour fabriquer le fichier préprocessé cat1.c, en tapant la ligne suivante sous le shell (note: le faire dans une autre fenêtre, par exemple):

cpp -DMCC cat.c >cat1.c

Sous le toplevel Caml avec top.ml chargé, on écrit:

teste "cat1.c";;

puis l’on demande la valeur de la variable ast, en tapant:

ast;;

ce qui donne en l’occurrence:

- : Cparse.var_declaration list ref = {contents = [CFUN (("cat.c", 12, 4, 12, 8), "main", [CDECL (("cat.c", 12, 14, 12, 18), "argc"); CDECL (("cat.c", 12, 27, 12, 31), "argv")], (("cat.c", 13, 0, 27, 1), CBLOCK ([CDECL (("cat.c", 14, 6, 14, 7), "i"); CDECL (("cat.c", 14, 9, 14, 10), "c")], [(("cat.c", 16, 2, 24, 5), CBLOCK ([], [(("cat.c", 16, 7, 16, 10), CEXPR (("cat.c", 16, 7, 16, 10), SET_VAR ("i", (("cat.c", 16, 9, 16, 10), CST 1)))); (("cat.c", 16, 2, 24, 5), CWHILE ((("cat.c", 16, 12, 16, 18), CMP (C_LT, (("cat.c", 16, 12, 16, 13), VAR "i"), (("cat.c", 16, 14, 16, 18), VAR "argc"))), (("cat.c", 16, 20, 24, 5), CBLOCK ([], [(("cat.c", 17, 4, 24, 5), CBLOCK ([CDECL (("cat.c", 18, 12, 18, 13), "f")], [(("cat.c", 20, 6, 20, 28), CEXPR (("cat.c", 20, 6, 20, 28), SET_VAR ("f", (("cat.c", 20, 10, 20, 28), CALL ("fopen", [(("cat.c", 20, 17, 20, 24), OP2 (S_INDEX, (("cat.c", 20, 17, 20, 21), VAR "argv"), (("cat.c", 20, 22, 20, 23), VAR "i"))); (("cat.c", 20, 27, 20, 28), STRING "r")]))))); (("cat.c", 21, 6, 22, 18), CWHILE ((("cat.c", 21, 14, 21, 33), EIF ((("cat.c", 21, 14, 21, 33), CMP (C_EQ, (("cat.c", 21, 14, 21, 27), SET_VAR ("c", (("cat.c", 21, 18, 21, 27), CALL ("fgetc", [(("cat.c", 21, 25, 21, 26), VAR "f")])))), (("cat.c", 21, 31, 21, 33), OP1 (M_MINUS, (("cat.c", 21, 32, 21, 33), CST 1))))), (("cat.c", 21, 14, 21, 33), CST 0), (("cat.c", 21, 14, 21, 33), CST 1))), (("cat.c", 22, 1, 22, 18), CEXPR (("cat.c", 22, 1, 22, 18), CALL ("fputc", [(("cat.c", 22, 8, 22, 9), VAR "c"); (("cat.c", 22, 11, 22, 17), VAR "stdout")]))))); (("cat.c", 23, 6, 23, 16), CEXPR (("cat.c", 23, 6, 23, 16), CALL ("fclose", [(("cat.c", 23, 14, 23, 15), VAR "f")])))])); (("cat.c", 16, 20, 16, 23), CEXPR (("cat.c", 16, 20, 16, 23), OP1 (M_POST_INC, (("cat.c", 16, 20, 16, 21), VAR "i"))))]))))])); (("cat.c", 25, 2, 25, 17), CEXPR (("cat.c", 25, 2, 25, 17), CALL ("fflush", [(("cat.c", 25, 10, 25, 16), VAR "stdout")]))); (("cat.c", 26, 2, 26, 10), CEXPR (("cat.c", 26, 2, 26, 10), CALL ("exit", [(("cat.c", 26, 8, 26, 9), CST 0)])))])))]}

En général, le fichier top.ml permet de tester son compilateur sous le toplevel Caml. Mais l’insertion de Printf.printf judicieux dans le code du compilateur peut s’avérer être une technique plus efficace de debugging…


Previous Up Next