Projet de programmation 1: compilateur de C--

Jean Goubault-Larrecq
LSV/UMR 8643, CNRS, ENS Cachan & INRIA Futurs projet SECSI
61 avenue du président-Wilson, F-94235 Cachan Cedex
goubault@lsv.ens-cachan.fr
Phone: +33-1 47 40 75 68   Fax: +33-1 47 40 75 21


1  Introduction

Le cours de programmation s'étend tout au long de la 1ère année à Cachan. Vous aurez à y faire plusieurs projets. Le tout premier d'entre eux commence le mardi 27 septembre 2005, et est à rendre le vendredi 14 octobre 2005.

Il s'agit d'écrire un compilateur pour un sous-ensemble strict du langage C, que nous appellerons C--. Le langage cible sera l'assembleur x86. Votre compilateur sera écrit en OCaml.

Voici un exemple de programme que votre compilateur devrait compiler:



Ce programme, qui sera un de nos exemples le premier jour de cours (vendredi 30 septembre 2005) code la commande Unix cat.

Au passage, j'ai évidemment fait le projet avant vous. Voici ce que mon compilateur produit:

.globl main .type main,@function main: pushl %ebp movl %esp, %ebp subl $4, %esp subl $4, %esp movl $0x1, %eax pushl %eax movl (%esp), %eax movl %eax, 0x-4(%ebp) jmp .label_1 .label_1: movl 0x-4(%ebp), %eax pushl %eax movl 0x8(%ebp), %eax pushl %eax popl %ebx popl %eax cmp %ebx, %eax jl .label_2 movl $0x0, %eax jmp .label_3 .label_2: movl $0x1, %eax .label_3: pushl %eax popl %eax cmp $0x0, %eax jne .label_4 jmp .label_5 .label_4: subl $4, %esp movl $.label_6, %eax pushl %eax movl 0xc(%ebp), %eax pushl %eax movl 0x-4(%ebp), %eax pushl %eax popl %ebx popl %eax movl (%eax, %ebx, 4), %ecx pushl %ecx call fopen addl $0x8, %esp movl (%esp), %eax movl %eax, 0x-1-4(%ebp) jmp .label_7 .label_7: movl 0x-1-4(%ebp), %eax pushl %eax call fgetc addl $0x4, %esp movl (%esp), %eax movl %eax, 0x-8(%ebp) movl $0x1, %eax pushl %eax popl %eax negl %eax pushl %eax popl %ebx popl %eax cmp %ebx, %eax je .label_8 movl $0x0, %eax jmp .label_9 .label_8: movl $0x1, %eax .label_9: pushl %eax popl %eax cmp $0x0, %eax jne .label_10 movl $0x1, %eax pushl %eax jmp .label_11 .label_10: movl $0x0, %eax pushl %eax .label_11: popl %eax cmp $0x0, %eax jne .label_12 jmp .label_13 .label_12: push stdout movl 0x-8(%ebp), %eax pushl %eax call fputc addl $0x8, %esp jmp .label_7 .label_13: movl 0x-1-4(%ebp), %eax pushl %eax call fclose addl $0x4, %esp movl 0x-4(%ebp), %eax pushl %eax movl (%esp), %eax addl $0x1, %eax movl %eax, 0x-4(%ebp) jmp .label_1 .label_5: push stdout call fflush addl $0x4, %esp movl $0x0, %eax pushl %eax call exit addl $0x4, %esp .label_6: .string "r" .align 4


Ceci peut vous sembler bien mystérieux. C'est normal! Ça le sera moins après le premier cours.

2  Comment ça se passe

D'abord, je vous recommande de travailler sous Linux. N'importe quelle version devrait faire l'affaire. Toutes les versions de Caml ou OCaml ≥ 2.04 devraient aussi faire l'affaire. Si vous voulez travailler sur un autre système d'exploitation (Windows par exemple), c'est votre choix. Sachez cependant que, même avec Cygwin sous Windows, vous vous exposez à des complications. Allez-y si vous avez une âme de hacker.

Ensuite, vous devez récupérer l'archive du projet. Décompressez-là avec gunzip et désarchivez le tout avec tar. Vous pouvez faire le tout en une seule étape en tapant tar -xvzf projet.tgz.

Vous devriez obtenir un répertoire MiniC et les fichiers suivants:

drwxr-xr-x goubault/lsv 0 2005-09-26 17:57:13 ProjetMiniC/ -rw-r--r-- goubault/lsv 1041 2005-10-07 11:15:35 ProjetMiniC/genlab.ml -rw-r--r-- goubault/lsv 396 2005-09-26 17:56:08 ProjetMiniC/cat.c -rw-r--r-- goubault/lsv 12949 2005-10-07 11:15:35 ProjetMiniC/clex.mll -rw-r--r-- goubault/lsv 2198 2005-10-07 11:15:35 ProjetMiniC/main.ml -rw-r--r-- goubault/lsv 226254 2005-10-07 11:15:35 ProjetMiniC/clex.ml -rw-r--r-- goubault/lsv 936 2005-10-07 11:15:35 ProjetMiniC/verbose.ml -rw-r--r-- goubault/lsv 2752 2005-10-07 11:15:35 ProjetMiniC/error.ml -rw-r--r-- goubault/lsv 12910 2005-10-07 11:15:35 ProjetMiniC/ctab.mly drwxr-xr-x goubault/lsv 0 2005-10-07 11:15:36 ProjetMiniC/Exemples/ -rw-r--r-- goubault/lsv 459 2005-10-07 11:15:36 ProjetMiniC/Exemples/cat.c -rw-r--r-- goubault/lsv 1308 2005-10-07 11:15:36 ProjetMiniC/Exemples/cat.s -rw-r--r-- goubault/lsv 739 2005-10-07 11:15:36 ProjetMiniC/Exemples/fact.c -rw-r--r-- goubault/lsv 7256 2005-10-07 11:15:36 ProjetMiniC/Exemples/sieve.s -rw-r--r-- goubault/lsv 2064 2005-10-07 11:15:36 ProjetMiniC/Exemples/sieve.c -rw-r--r-- goubault/lsv 2425 2005-10-07 11:15:36 ProjetMiniC/Exemples/fact.s -rw-r--r-- goubault/lsv 1214 2005-10-07 11:15:35 ProjetMiniC/ctab.mli -rw-r--r-- goubault/lsv 9081 2005-10-07 11:15:35 ProjetMiniC/cparse.ml -rw-r--r-- goubault/lsv 521 2005-10-07 11:15:35 ProjetMiniC/depend -rw-r--r-- goubault/lsv 981 2005-10-07 11:15:35 ProjetMiniC/compile.mli -rw-r--r-- goubault/lsv 52296 2005-10-07 11:15:35 ProjetMiniC/ctab.ml -rw-r--r-- goubault/lsv 1849 2005-10-07 11:15:35 ProjetMiniC/Makefile


Votre travail consiste à écrire le fichier manquant compile.ml. Vous pouvez y écrire ce que vous voulez, l'important est de définir la fonction dont le type est spécifié dans le fichier d'interface compile.mli:

val compile : out_channel -> Cparse.var_declaration list -> unit;;

Cette fonction prend un canal de sortie en premier argument, appelons-le out, et un programme C--, appelons-le π, et imprime sur out un texte en assembleur x86 qui code le programme π. Ce texte en assembleur ressemblera donc au texte de l'introduction (voir le fichier cat.s).

Lorsque vous aurez écrit le fichier compile.mli, compilez le tout en invoquant make. Comme vous l'aura sûrement expliqué Sébastien, ceci va appliquer un certain nombre de commandes dans un ordre défini par les dépendances exprimées dans le fichier Makefile.

Le résultat sera un programme, qui s'appellera mcc et sera dans le répertoire ProjetMiniC. Vous pourrez tester ce programme en tapant ./mcc suivi du nom d'un fichier C-- à compiler.

Par exemple, si vous tapez (en partant du répertoire ProjetMiniC):
cd Exemples ../mcc cat.c
Ceci devrait produire un fichier assembleur cat.s, que vous pourrez consulter, et un fichier exécutable cat, que vous pourrez tester en tapant (sous le répertoire ProjetMiniC/Exemples):
./cat cat.s


Il est évidemment plus que probable que le code assembleur que vous produisiez, au moins au début, ne soit pas de la meilleure qualité. Notamment, mcc appelle le compilateur gcc pour compiler le code assembleur que vous aurez produit vers un vrai exécutable. Si votre code assembleur n'est pas syntaxiquement correct, ceci échouera. Il se peut aussi que vous obteniez un exécutable, mais qu'il plante (message Segmentation fault, typiquement).

Dans tous les cas, vous pouvez:

3  Quelques règles du jeu

Vous pouvez allez regarder les fichiers que vous voulez. Le seul fichier que vous n'aurez pas est celui que vous avez à écrire, compile.ml.

Vous n'avez le droit de modifier aucun des fichiers de l'archive du projet. En fait, vous devrez vous limiter à écrire vos fonctions dans le fichier compile.ml. L'idée, c'est que si je prends votre fichier compile.ml et je l'ajoute à l'archive, que je tape make, j'obtiens un compilateur fonctionnel pour C--.

Une autre façon de le dire: vous pouvez aller bidouiller les autres fichiers du projet si ça vous chante, par exemple, pour ajouter des appels à Printf.printf, et voir comment ça marche, mais vous ne devez pas faire des modifications dans ces fichiers qui soit essentielles à la bonne marche de votre compilateur.

4  L'architecture de mcc

D'abord, quand on exécute mcc, il va commencer par exécuter le code qui se trouve dans le fichier main.ml.

Celui-ci lit les options de commande (-E par exemple), puis appelle l'analyseur syntaxique Ctab.translation_unit, auquel on passe l'analyseur lexical Clex.ctoken.

Le source de l'analyseur syntaxique, qui servira aussi de documentation pour la grammaire du langage C--, est dans le fichier ctab.mly. La description des tokens lexicaux est dans clex.mll.

L'analyseur syntaxique fournit un programme sous forme d'une liste de Cparse.var_declaration. Les d'une liste de var_declaration sont déclarées au début du fichier cparse.ml, comme suit:
type mon_op = M_MINUS | M_NOT | M_POST_INC | M_POST_DEC | M_PRE_INC | M_PRE_DEC (* Les operations unaires: M_MINUS: calcule l'oppose -e de e; M_NOT: calcule la negation logique ~e de e; M_POST_INC: post-incrementation e++; M_POST_DEC: post-decrementation e--; M_PRE_INC: pre-incrementation ++e; M_PRE_DEC: pre-decrementation --e. *) type bin_op = S_MUL | S_DIV | S_MOD | S_ADD | S_SUB | S_INDEX (* Les operations binaires: S_MUL: multiplication entiere; S_DIV: division entiere (quotient); S_MOD: division entiere (reste); S_ADD: addition entiere; S_SUB: soustraction entiere; S_INDEX: acces \`a un element de tableau a[i]. *) type cmp_op = C_LT | C_LE | C_EQ (* Les operations de comparaison: C_LT (less than): <; C_LE (less than or equal to): <=; C_EQ (equal): ==. *) type loc_expr = locator * expr and expr = VAR of string (* une variable --- toujours de type int. *) | CST of int (* une constante entiere. *) | STRING of string (* une constante chaine. *) | SET_VAR of string * loc_expr (* affectation x=e. *) | SET_ARRAY of string * loc_expr * loc_expr (* affectation x[e]=e'. *) | CALL of string * loc_expr list (* appel de fonction f(e1,...,en) *) (* operations arithmetiques: *) | OP1 of mon_op * loc_expr (* OP1(mop, e) denote -e, ~e, e++, e--, ++e, ou --e. *) | OP2 of bin_op * loc_expr * loc_expr (* OP2(bop,e,e') denote e*e', e/e', e%e', e+e', e-e', ou e[e']. *) | CMP of cmp_op * loc_expr * loc_expr (* CMP(cop,e,e') vaut e<e', e<=e', ou e==e' *) | EIF of loc_expr * loc_expr * loc_expr (* EIF(e1,e2,e3) est e1?e2:e3 *) | ESEQ of loc_expr list (* e1, ..., en [sequence, analogue a e1;e2 au niveau code]; si n=0, represente skip. *) type var_declaration = CDECL of locator * string (* declaration de variable de type int. *) | CFUN of locator * string * var_declaration list * loc_code (* fonction avec ses arguments, et son code. *) and loc_code = locator * code and code = CBLOCK of var_declaration list * loc_code list (* { declarations; code; } *) | CEXPR of loc_expr (* une expression e; vue comme instruction. *) | CIF of loc_expr * loc_code * loc_code (* if (e) c1; else c2; *) | CWHILE of loc_expr * loc_code (* while (e) c1; *) | CRETURN of loc_expr option (* return; ou return (e); *)
Le type locator est juste un pointeur sur des informations permettant aux fonctions Error.error et Error.warning d'afficher des message d'erreur sensés.

Le fichier cparse.ml contient aussi des fonctions utiles, comme string_of_loc_expr, qui vous permettent d'obtenir une version imprimable d'une expression C--, ce que vous pouvez utiliser en phase de débogage.

Le fichier genlab.ml contient une fonction genlab qui vous permettra de fabriquer des labels uniques, ce qui devrait être bien pratique lorsque vous émettrez des instructions de saut dans votre code assembleur.

Le fichier verbose.ml déclare une variable verbose que vous pourrez tester pour régler le niveau d'informations de débogage que vous voudrez afficher (elle vaut 1 si vous appelez mcc avec l'option -v1, 2 si avec l'option -v2, et 0 par défaut).

5  Le langage C--

C-- est un sous-ensemble extrêmement réduit du langage C. Ont été supprimés de C: tous les types struct, union, enum, ainsi que les tableaux, et la plupart des types scalaires. En fait, il ne reste que les types int, char *, et les pointeurs sur les types C-- (récursivement).

L'avantage est que tous ces types tiennent sur 32 bits. Vous n'aurez donc pas à vous casser la tête pour savoir combien de place allouer à chaque variable: ce sera toujours 4 octets.

Ensuite, de nombreuses constructions de C ont été supprimées, mais pas suffisamment pour ne pas pouvoir compiler des programmes intéressants.

En principe, C-- est un sous-ensemble de C, ce qui signifie que vous pourrez tester mcc en comparant les programmes qu'il produit avec ceux produits par gcc.

Il n'y a qu'une exception à ce principe: l'arithmétique sur les pointeurs a une sémantique différente en C-- et en C. Si vous déclarez int *p et écrivez p++ en C, ceci ajoute sizeof(int), soit 4, à l'adresse p. En C–, ça n'est censé qu'ajouter un. Je doute que vous ayez à considérer ce genre de subtilités, mais qui sait.

6  La sémantique opérationnelle à grands pas de C--

Soit Z32 l'ensemble de tous les entiers signés 32 bits: Z32 = [−231, 231−1]. Soit Addr le sous-ensemble de toutes les adresses.

Nous allons présenter la sémantique de C-- en deux étapes. En section 6.1, nous présenterons une sémantique pour un sous-ensemble extrêmement réduit de C--. L'avantage est que cette sémantique sera très simple. Certaines particularités de C-- nous forceront à décrire une sémantique plus compliquée en section 6.2.

6.1  Une sémantique simple pour un sous-ensemble de C--

Considérons un langage où les expressions sont juste celles de la forme:
e ::= x variable
  | e  plus  e addition
  | call  f (e, …, e) appel de fonction
et où les instructions sont de la forme:
c ::= x=e affectation
  | skip ne rien faire
  | c;c séquence
  | if  (ethen  c  else  c test
  | while  (ec boucle
Pour pouvoir donner une sémantique aux appels de fonction (call), il nous faut nous donner la description d'un programme π. Pour nous, un programme π sera une fonction mathématique, de domaine fini, qui à des noms de fonction f associe un triplet (ℓ, c, e), où ℓ est une liste de variables distinctes deux à deux (les paramètres formels de la fonction f), c est une instruction (le corps de la fonction), et e est une expression (dénotant la valeur retournée par la fonction).

Les expressions et les instructions seront évaluées dans un environnement ρ, qui est une fonction de domaine fini associant une valeur dans Z32 à des variables.

Définissons des règles permettant de déduire des jugements de la forme ρ ⊢πeV, signifiant “dans l'environnement ρ, l'expression e a pour valeur V”.

On a d'abord une règle pour la variable:
      (1)
Cette règle s'applique dès que x est dans le domaine de ρ. Elle n'a aucune (autre) prémisse.

On a ensuite une règle pour l'addition:
      (2)
(La raison pour laquelle nous écrivons l'addition e1 plus  e2 plutôt que e1+e2 par exemple est pour bien distinguer l'opération syntaxique plus de l'opération mathématique d'addition, notée +.)

Nous traiterons de l'appel de fonctions plus loin.

On peut maintenant définir des jugements de la forme ρ ⊢πc ⇒ ρ' pour chaque instruction c, signifiant “en partant de l'environnement ρ, l'instruction c termine sur un nouvel environnement ρ', le programme courant étant π”. De nouveau, les jugements dérivables sont définis par des règles de déduction.

La première est pour l'affectation:
      (3)
La notation ρ [xV] désigne la fonction de domaine dom  ρ ∪ {x}, qui à x associe V et à tout y in dom  ρ \ {x} associe ρ (y).

L'instruction skip ne fait strictement rien de remarquable:
      (4)


La séquence évalue ses arguments... en séquence:
      (5)
Elle introduit donc un environnement intermédiaire ρ' représentant l'environnement après l'exécution de e1 et avant l'exécution de e2.

Jusqu'ici, nous avions une règle de déduction par construction syntaxique. Ceci change pour le test. Nous avons une règle lorsque la condition testée est fausse, et une lorsqu'elle est vraie:
      (6)
Comme en C, 0 est faux, et tout autre nombre est considéré comme vrai.
      (7)


La boucle while  (e)  c est équivalente à if  (ethen  c;while  (e)  c  else skip. Ceci donne lieu, ici aussi, à deux règles pour l'évaluation des boucles.
      (8)
      (9)
La seconde règle a la particularité que sa seconde prémisse ne porte pas sur une instruction plus petite que celle en conclusion, contrairement aux règles présentées jusqu'ici. Ceci mène à des comportements non terminants. Disons que c termine dans π, à partir de ρ, si et seulement s'il existe ρ' tel que ρ ⊢πc ⇒ ρ' soit déductible.

Exercice 1   Si ρ (x) n'est pas défini, while (xskip termine-t-il?


Exercice 2   Montrer que while (xskip termine à partir de ρ si et seulement si x in dom ρ et ρ (x) = 0.


L'appel de fonction fait aussi ce qu'on attend de lui:
      (10)
où [x1V1, …, xnVn] est l'environnement qui à x1 associe V1, ..., à xn associe Vn.

Exercice 3   Que se passe-t-il si on appelle une fonction f sur le mauvais nombre d'arguments, i.e., si on cherche à évaluer call  f (e1, …, en) alors que π (f) = ((x1, …, xm), c) et mn? Que se passe-t-il si fdom  π?


Exercice 4   Montrer que cette sémantique est déterministe: si ρ ⊢πc ⇒ ρ' et ρ ⊢πc ⇒ ρ'' sont déductibles, alors ρ' = ρ''. Dites précisément sur quoi vous effectuez une récurrence, et décrivez quelques-uns des cas que vous traitez, notamment ceux utilisés lorsque c est une boucle while.


Exercice 5   En Caml, on n'a pas besoin de la construction while. Au lieu d'écrire:

while e do c;;
on peut en effet écrire

let rec f () = 
        if e
           then begin
                  c;
                  f ()
                end
        else ()
in f ();;
f est un nom frais (non utilisé dans e ou c). Peut-on recoder les boucles while par un codage similaire dans le langage de cette section? Et en C? Et en C--?


Il manque à cette sémantique, notamment, l'allocation mémoire et les pointeurs, les variables globales, la possibilité d'effectuer des effets de bord à l'intérieur des expressions. Ceci sera réparé dans la sémantique de C--, que nous décrivons tout de suite.

6.2  La sémantique de C--

Un programme π est pour nous un couple (ρπ, funπ): On supposera pour simplifier que funπ contient aussi des entrées pour toutes les fonctions de la libc, comme printf, strcpy, malloc, free, etc. Et que, de même, ρπ contient aussi des entrées pour toutes les variables globales de la libc, comme errno par exemple.

Exercice 6   La définition de π permet-elle d'allouer deux variables à la même adresse? Ceci est-il souhaitable? Si oui, pourquoi? Si non, comment réparer la définition?


Exercice 7   La définition de funπ n'autorise pas à avoir deux fonctions de même nom. Mais il peut y avoir deux CFUN (loc, f, params, code) avec le même f fourni à la fonction compile. Comment traduisez-vous ces dernières var_declaration en entrées pour funπ, pour tenir compte de cette différence conceptuelle?


On définit une série de jugements de sémantique opérationnelle de la forme μ, ρ ⊢πev, μ', qui dit qu'en partant d'une mémoire μ : AddrZ32, et d'un environnement ρ : stringAddr qui dit à quelles adresses sont stockées les variables, l'expression C-- e peut s'évaluer en la valeur v in Z32, et modifier la mémoire en μ'.

Il y a deux sortes de variables. Les variables locales (ou automatiques dans le jargon du C) sont données dans l'environnement ρ:
      (11)
Les variables globales sont décrites par l'environnement global ρπ.
      (12)


Exercice 8   Quelle règle s'applique-t-elle dans le cas d'une variable locale (x in dom  ρ) qui a le même nom qu'une variable globale (x in dom  ρπ)?


Exercice 9   Que se passe-t-il si x est une variable globale pour laquelle aucune mémoire n'est allouée (ρπ(x) ∉dom  μ?)


      (13)


L'évaluation des chaînes est un peu plus étrange. Elle retourne une adresse mémoire quelconque où se trouve déjà la représentation de s. Disons que s est stockée à l'adresse a si et seulement si s ne contient pas l'octet 0, il existe une adresse a+k dans la mémoire μ contenant un octet nul, avec k minimal, et la suite de tous les octets aux adresses a, a+1, a+2, ..., a+k−1 vaut s.
      (14)
En pratique, le compilateur devra allouer de la place pour la chaîne s à la compilation. Ceci est fait pratiquement automatiquement par l'assembleur. Par exemple,
.main_3: .string "r" .align 4
réserve de la place pour les octets `r' et 0, les place à une adresse nommée .main_3. L'instruction mystérieuse .align 4 sert à assurer que ce qui suit commencera à une adresse multiple de 4, ce qui facilite la tâche du processeur pour certaines opérations.

Exercice 10   La sémantique ci-dessus n'oblige pas à stocker toute chaîne présente dans le programme. Que se passe-t-il si s n'est stockée nulle part?


Pour toute mémoire μ, pour toute adresse a in Addr, pour toute valeur v in Z32, notons μ [av] la mémoire qui à a associe v, et à toute autre adresse a' ≠ a associe μ (a').

On a encore deux règles pour SET_VAR, selon que la variable est locale ou globale.
      (15)
      (16)


Exercice 11   Que se passe-t-il si on écrit x=1 en C-- mais que x n'est pas dans dom  ρ? ... ni dans dom  ρ ni dans dom  ρπ? Ceci arrive lorsque x n'aura pas été déclarée, typiquement par int x. Comment réagit mcc dans ce cas? Comment réagit gcc dans ce cas?


Exercice 12   Pourquoi demande-t-on ρ (x) in dom  μ', resp. ρπ(x) in dom  μ' dans ces règles? Autrement dit, quel genre de comportement étrange l'omission de ces conditions autoriserait-il?


Passons aux instructions de la forme x[i]=e. On comprend l'addition a+v comme étant effectuée modulo 232.
      (17)
      (18)


Exercice 13   Dans quel ordre cette règle évalue-t-elle e et e'? Que spécifie la norme ANSI C?


Exercice 14   Pourquoi demandons-nous que v in Z32 et a+v in Addr dans ces règles?


Disons qu'une mémoire μ' étend μ, ce que nous noterons μ' ⊑ μ, si et seulement si dom  μ' ⊇ dom  μ et μ'|dom  μ = μ. On note [x1a1, …, xnan] la fonction qui à chaque xi associe ai, 1≤ in. Ceci est bien défini lorsque les xi sont distinctes deux à deux.

      (19)


Exercice 15   Dans quel ordre sont évalués les arguments d'un appel de fonction CALL comme ci-dessus? Que dit la norme ANSI C à ce sujet?


Exercice 16   Pourquoi demande-t-on μ'n ⊑ μn, ..., μ'2 ⊑ μ2, μ'1 ⊑ μ1 dans (19)? Pour répondre à cette question, imaginez que μ'nn, ..., μ'22 dans (19), et demandez-vous si votre compilateur produit effectivement du code correspondant à la spécification.


Exercice 17   Pourquoi, sur le même principe, ne demanderions-nous pas des mémoires auxiliaires qui étendent μ', plutôt que de réutiliser directement μ' dans l'évaluation de e', dans les règles (17) et (18)?


Exercice 18   Que se passe-t-il si la fonction f n'est pas définie? Si f est définie avec un nombre d'arguments différent de n? Si la liste des paramètres formels de f n'est pas de la forme CDECL (l1, x1), …, CDECL (ln, xn) mais contient un élément de la forme CFUN (…)?


Exercice 19   Pourquoi demande-t-on a1dom  μ1, ..., andom  μn dans les prémisses de (19)?


Exercice 20   Rien n'empêche les variables x1, ..., xn d'être non distinctes. Si c'est le cas, la notation [x1a1, …, xnan] n'a aucun sens. Réparez la règle (19) pour tenir compte de ce fait.


Passons aux opérations unaires. La négation effectue son calcul modulo 232.
      (20)


Exercice 21   Dans quel cas le modulo 232 est-il nécessaire?


      (21)


Exercice 22   On aurait pu s'attendre que la négation bit à bit soit décrite en termes des bits de v. Montrer que la définition ci-dessus est équivalente, autrement dit que −v−1 est bit la négation bit à bit de v.


      (22)


      (23)


Exercice 23   Que se passe-t-il si on applique M_POST_INC à autre chose qu'une variable?


Exercice 24   Écrire les règles sémantiques pour M_PRE_INC, M_POST_DEC, M_PRE_DEC.


      (24)


Exercice 25   Pourquoi μ'2 ⊑ μ2?


Exercice 26   Dans quel ordre sont évalués les arguments?


Exercice 27   Écrire les règles sémantiques pour S_DIV, S_MOD, S_ADD, S_SUB.


      (25)


Exercice 28   Écrire le reste de la sémantique!


7  Guide de référence rapide de l'assembleur x86

Les registres: %eax %ebx %ecx %edx %esi %edi %ebp %esp.
Ils contiennent tous un entier de 32 bits (4 octets), qui peut aussi être vu comme une adresse. Le registre %esp est spécial, et pointe sur le sommet de pile; il est modifié par les instructions pushl, popl, call, ret notamment.

Il y a aussi d'autres registres que l'on ne peut pas manipuler directement. (L'instruction info registers sous gdb ou ddd vous les montrera.) Le plus important est eip, le compteur de programme: il contient en permanence l'adresse de la prochaine instruction à exécuter. 1em

8  Un dernier mot

Bon courage, et travaillez bien! N'hésitez pas à nous contacter, goubault@lsv.ens-cachan.fr ou bardin@lsv.ens-cachan.fr


This document was translated from LATEX by HEVEA.