You are on page 1of 230

Interprtation et compilation

Licence Informatique troisime anne

Jean Mhat
jm@univ-paris8.fr
Universit de Paris 8 Vincennes Saint Denis

5 avril 2013

Copyright (C) 2009 Jean Mhat


Permission is granted to copy, distribute and/or modify this document under
the terms of the GNU Free Documentation License, Version 1.3 or any later
version published by the Free Software Foundation ; with no Invariant Sections,
no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included
in the section entitled "GNU Free Documentation License".

Table des matires

1 Introduction

1.1 Pr-requis pour suivre le cours . . . . . . . . . . . .


1.2 L'intrt de l'tude de la compilation . . . . . . . . .
1.3 Ce qu'est un compilateur . . . . . . . . . . . . . . .
1.3.1 Compilateurs versus interprtes . . . . . . . .
1.3.2 Compilateur + interprtes . . . . . . . . . . .
1.4 La structure gnrale d'un compilateur . . . . . . . .
1.4.1 L'analyseur lexical . . . . . . . . . . . . . . .
1.4.2 L'analyseur syntaxique . . . . . . . . . . . . .
1.4.3 La gnration de code . . . . . . . . . . . . .
1.4.4 L'optimisation du code . . . . . . . . . . . . .
1.4.5 Variantes . . . . . . . . . . . . . . . . . . . .
1.5 Qui compile le compilateur ? . . . . . . . . . . . . . .
1.5.1 La compilation croise . . . . . . . . . . . . .
1.5.2 Le bootstrap d'un compilateur . . . . . . . .
1.6 Aprs la compilation, l'dition de liens (Supplment)
1.6.1 L'dition de liens . . . . . . . . . . . . . . . .
1.6.2 Le chargement en mmoire . . . . . . . . . .
1.6.3 Commandes Unix . . . . . . . . . . . . . . .
1.7 Plan du reste du cours . . . . . . . . . . . . . . . . .
1.8 Rfrences bibliographiques . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

2.1 Assembleur et langage machine . . . . . . . . . . . . . . . . . .


2.2 Brve introduction la programmation du processeur Intel 386
2.2.1 Les oprandes . . . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Les modes d'adressages . . . . . . . . . . . . . . . . . .

.
.
.
.

2 L'assembleur

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

8
8
9
9
10
12
12
13
13
13
14
15
15
15
16
16
20
20
23
24

25

25
25
26
27

2.2.3 Les instructions . . . . . . . . . . . . .


2.2.4 Les directives . . . . . . . . . . . . . .
2.3 Des exemples de programmes en assembleur .
2.3.1 Interactions entre l'assembleur et le C
2.3.2 Tests . . . . . . . . . . . . . . . . . . .
2.3.3 Boucles . . . . . . . . . . . . . . . . .
2.3.4 Pile . . . . . . . . . . . . . . . . . . .
2.4 Les autres assembleurs, les autres processeurs
2.4.1 Parenthse : les machines RISC . . . .

3 Comprendre un programme C compil

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

3.1 Du C vers l'assembleur . . . . . . . . . . . . . . . . . . . . . . . .


3.2 Prologue et pilogue des fonctions C . . . . . . . . . . . . . . . .
3.2.1 Le prologue . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.2 L'pilogue . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.3 L'intrt du frame pointer . . . . . . . . . . . . . . . . . .
3.3 Le code optimis . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4 L'utilisation des registres . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Variables et expressions intermdiaires dans les registres .
3.4.2 La problmatique de la sauvegarde . . . . . . . . . . . . .
3.5 Application : le fonctionnement et le contournement de stdarg .
3.5.1 Passage de paramtres de types double . . . . . . . . . .
3.5.2 Passages de paramtres de types varis . . . . . . . . . . .
3.5.3 La rception des arguments par la fonction appele . . . .
3.5.4 Fonctionnement de stdarg . . . . . . . . . . . . . . . . . .
3.5.5 Contenu du chier stdarg.h . . . . . . . . . . . . . . . .
3.5.6 A quoi sert stdarg ? . . . . . . . . . . . . . . . . . . . . .
3.6 Application : le fonctionnement de setjmp et longjmp en C . . .
3.6.1 Schma d'utilisation . . . . . . . . . . . . . . . . . . . . .
3.6.2 Fonctionnement intime de setjmplongjmp . . . . . . . .
3.6.3 Rapport avec les exceptions d'autres langages . . . . . . .
3.7 L'ordre des arguments dans la bibliothque d'entres-sorties standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8 Manipuler l'adresse de retour . . . . . . . . . . . . . . . . . . . .
3.9 Si vous avez un systme 64 bits . . . . . . . . . . . . . . . . . . .

4 L'analyse lexicale

28
32
33
33
38
38
40
42
42

44
44
48
48
50
50
53
55
55
55
58
59
59
61
63
63
64
65
65
66
67
68
68
69

71

4.1 Analyse lexicale, analyse syntaxique . . . . . . . . . . .


4.1.1 Analyse lexicale versus analyse syntaxique . . . .
4.1.2 Analyse lexicale et analyse syntaxique . . . . . .
4.2 En vrac . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Renvoyer un type et une valeur . . . . . . . . . .
4.2.2 Le caractre qui suit le mot, ungetc . . . . . . .
4.2.3 Les commentaires . . . . . . . . . . . . . . . . . .
4.2.4 Quelques dicults de l'analyse lexicale . . . . .
4.3 Un exemple lmentaire mais raliste d'analyseur lexical
4.3.1 Le langage . . . . . . . . . . . . . . . . . . . . .
4.3.2 L'analyseur syntaxique . . . . . . . . . . . . . . .
4.3.3 L'analyseur lexical . . . . . . . . . . . . . . . . .

5 L'analyse syntaxique : prsentation

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

5.1 Grammaires, langages, arbres syntaxiques, ambiguts . . . . . .


5.1.1 Les rgles de grammaires . . . . . . . . . . . . . . . . . .
5.1.2 Les symboles terminaux et non terminaux . . . . . . . . .
5.1.3 Les arbres syntaxiques . . . . . . . . . . . . . . . . . . . .
5.1.4 Les grammaires ambigus, l'associativit et la prcdence
5.1.5 BNF, EBNF . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 Les analyseurs prcdence d'oprateurs . . . . . . . . . . . . .
5.2.1 Un analyseur prcdence d'oprateurs lmentaire . . .
5.2.2 Un analyseur prcdence d'oprateurs moins lmentaire
5.2.3 Des problmes avec les analyseurs prcdence d'oprateurs

6 L'analyse syntaxique : utilisation de Yacc


6.1 Yacc et Bison, historique . . . . . . . . . .
6.2 Le fonctionnement de Yacc . . . . . . . .
6.3 Un exemple lmentaire . . . . . . . . . .
6.3.1 Structure d'un chier pour Yacc .
6.3.2 La partie dclaration . . . . . . . .
6.3.3 La partie code C . . . . . . . . . .
6.3.4 La partie grammaire et actions . .
6.3.5 Utilisation . . . . . . . . . . . . . .
6.4 Un exemple de calculateur simple . . . . .
6.5 Un calculateur avec des nombres ottants
6.5.1 La rcupration d'erreurs . . . . .
4

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

71
71
72
72
72
72
74
75
78
79
79
79

83

83
83
84
84
85
90
91
91
95
97

98

98
99
99
101
101
101
102
102
104
106
106

6.5.2 Typage des valeurs attaches aux nuds . . . . . . . . . . 106


6.6 Un calculateur avec un arbre vritable . . . . . . . . . . . . . . . 107

7 L'analyse syntaxique : le fonctionnement interne de Yacc


7.1 L'analyseur de Yacc est ascendant de gauche droite
7.2 Utilisation de la pile par l'analyseur . . . . . . . . .
7.3 Fermetures LR(n) . . . . . . . . . . . . . . . . . . .
7.3.1 Le chier output . . . . . . . . . . . . . . . .
7.3.2 Conits shift-reduce . . . . . . . . . . . . . .
7.3.3 Fermeture LR(1) et LALR(1) . . . . . . . . .
7.3.4 L'exemple . . . . . . . . . . . . . . . . . . . .
7.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . .
7.5 Les ambiguts rsiduelles . . . . . . . . . . . . . . .
7.6 Conits shift-reduce, le dangling else . . . . . . . . .
7.6.1 Conits reduce-reduce . . . . . . . . . . . . .
7.6.2 Laisser des conits dans sa grammaire . . . .
7.7 Des dtails supplmentaires sur Yacc . . . . . . . . .
7.7.1 Dnition des symboles terminaux . . . . . .
7.7.2 Les actions au milieu des rgles . . . . . . . .
7.7.3 Rfrences dans la pile . . . . . . . . . . . . .
7.7.4 Les nuds de type inconnu de Yacc . . . . .
7.7.5 %token . . . . . . . . . . . . . . . . . . . . .
7.7.6 %noassoc . . . . . . . . . . . . . . . . . . . .
7.8 Le reste . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

8.1 Les dclarations . . . . . . .


8.2 Les rgles de la grammaire .
8.2.1 Programme . . . . .
8.2.2 Les instructions . . .
8.2.3 Les expressions . . .
8.2.4 Les dclarations . .
8.3 Exercices . . . . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

8 L'analyseur syntaxique de Gcc


.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

9 La smantique, la gnration de code

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

109

109
112
112
120
120
121
121
123
125
125
125
126
126
126
127
127
127
127
127
128

129

129
130
130
130
131
131
131

133

9.1 Les grammaires attribues . . . . . . . . . . . . . . . . . . . . . . 133


9.2 Les conversions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
9.3 La gnration de code, ppcm . . . . . . . . . . . . . . . . . . . . . 134
5

9.3.1 Le langage source . . . . . . . . . .


9.3.2 La reprsentation des expressions .
9.3.3 L'analyseur lexical . . . . . . . . .
9.3.4 La gnration de code . . . . . . .
9.3.5 Prologues et pilogues de fonctions
9.3.6 Amliorations de ppcm . . . . . .
9.4 Exercices . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

10.1 Prliminaires . . . . . . . . . . . . . . . . .
10.1.1 Pourquoi optimise-t-on ? . . . . . . .
10.1.2 Quels critres d'optimisation ? . . . .
10.1.3 Sur quelle matire travaille-t-on ? . .
10.1.4 Comment optimise-t-on ? . . . . . .
10.2 Dnitions . . . . . . . . . . . . . . . . . . .
10.3 Optimisations indpendantes . . . . . . . .
10.3.1 Le pliage des constantes . . . . . . .
10.3.2 Les instructions de sauts . . . . . . .
10.3.3 ter le code qui ne sert pas . . . . .
10.3.4 Utilisation des registres . . . . . . .
10.3.5 Inliner des fonctions . . . . . . . . .
10.3.6 Les sous expressions communes . . .
10.3.7 La rduction de force . . . . . . . . .
10.3.8 Sortir des oprations des boucles . .
10.3.9 Rduction de force dans les boucles .
10.3.10 Drouler les boucles . . . . . . . . .
10.3.11 Modier l'ordre des calculs . . . . .
10.3.12 Divers . . . . . . . . . . . . . . . . .
10.3.13 Les problmes de prcision . . . . .
10.4 Tout ensemble : deux exemples . . . . . . .
10.4.1 Le problme des modications . . .
10.5 Optimisations de gcc . . . . . . . . . . . . .
10.6 Exercices . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

10 Optimisation

11 L'analyse lexicale : le retour

134
135
136
137
140
142
144

146

146
146
147
147
147
147
148
148
149
151
152
152
153
154
154
155
155
156
156
157
157
160
161
161

164

11.1 Les expressions rgulires . . . . . . . . . . . . . . . . . . . . . . 164


11.1.1 Les expressions rgulires dans l'univers Unix . . . . . . . 164
6

11.2
11.3

11.4

11.5
11.6

11.1.2 Les expressions rgulires de base . . . .


11.1.3 Une extension Unix importante . . . . .
Les automates nis dterministes . . . . . . . .
Les automates nis non dterministes . . . . .
11.3.1 Les transitions multiples . . . . . . . . .
11.3.2 Les transitions epsilon . . . . . . . . . .
11.3.3 Les automates nis dterministes ou pas
Des expressions rgulires aux automates . . .
11.4.1 tat d'acceptation unique . . . . . . . .
11.4.2 La brique de base . . . . . . . . . . . . .
11.4.3 Les oprateurs des expressions rgulires
11.4.4 C'est tout . . . . . . . . . . . . . . . . .
La limite des expressions rgulires . . . . . . .
Lex et Flex . . . . . . . . . . . . . . . . . . . .

12 Projet et valuation

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

166
168
168
172
172
174
175
177
177
178
178
179
181
181

182

12.1 Projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182


12.2 valuation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

13 Documments annexes

184

13.1 Les parseurs prcdence d'oprateur . . . . . . . . . . . . . . . 184


13.1.1 Le chier src/ea-oper0.c . . . . . . . . . . . . . . . . . 184
13.1.2 Le chier src/eb-oper1.c . . . . . . . . . . . . . . . . . 187
13.2 Les petits calculateurs avec Yacc . . . . . . . . . . . . . . . . . . 191
13.2.1 Le chier src/ed-1-calc-y . . . . . . . . . . . . . . . . . 191
13.2.2 Le chier src/ed-3-calc.y . . . . . . . . . . . . . . . . . 193
13.2.3 Le chier src/ee-2-calc.y . . . . . . . . . . . . . . . . . 199
13.3 La grammaire C de gcc . . . . . . . . . . . . . . . . . . . . . . . 201
13.4 Les sources de ppcm . . . . . . . . . . . . . . . . . . . . . . . . . 222
13.4.1 Le chier de dclarations ppcm.h . . . . . . . . . . . . . . 222
13.4.2 Le chier expr.c . . . . . . . . . . . . . . . . . . . . . . . 222
13.4.3 L'analyseur lexical dans le chier pccm.l . . . . . . . . . 223
13.4.4 Le grammaire et la gnration de code dans le chier ppcm.y 224

Chapitre 1
Introduction

Ce chapitre prsente brivement le sujet principal du cours (les compilateurs), avec leur structure ordinaire laquelle nous ferons rfrence dans la
suite du cours, puis expose l'organisation de ce support.

1.1 Pr-requis pour suivre le cours


Le cours suppose que l'tudiant est familier de la programmation et de l'environnement Unix. La plupart des exemples en langage de haut niveau sont pris
dans le langage C, dans lequel l'tudiant est suppos pouvoir programmer.
Il n'y a pas de rappel sur le hash-coding (voir le cours sur les structures de
donnes et les algorithmes.).

1.2 L'intrt de l'tude de la compilation


a permet de voir en dtail le fonctionnement d'un langage de programmation, y compris dans des aspects obscurs et peu tudis.
Pour crire du bon code, il faut avoir une ide de la faon dont il est traduit.
L'tude de la compilation le permet.
a nous amne tudier des algorithmes d'intrt gnral et des objets
abstraits comme les automates nis et les expressions rgulires qui sont utiles
dans d'autres contextes.
a nous conduit apprendre nous servir d'outils (comme Yacc et Bison,
des gnrateurs d'analyseurs syntaxiques) qui sont utiles dans de nombreux
contextes.
a nous donne un bon prtexte pour regarder un peu la programmation en
assembleur.

1.3 Ce qu'est un compilateur


Un compilateur est un programme qui est charg de traduire un programme
crit dans un langage dans un autre langage. Le langage du programme de dpart
est appel le langage source, le langage du programme rsultat le langage cible
(source language et target language en anglais).
Le plus souvent, le langage source est un langage dit de haut niveau, avec
des structures de contrle et de donnes complexes alors que le langage cible est
du langage machine, directement excutable par un processeur. Il y a cependant
des exceptions ; par exemple, certains compilateurs traduisent des programmes
d'un langage de haut niveau vers un autre.

1.3.1

Compilateurs versus interprtes

Un interprte est un programme qui est charg d'excuter un programme


crit dans un langage sur un processeur qui excute un autre langage. Il joue
presque le mme rle qu'un compilateur, mais prsente des caractres dirents.
Alors qu'un compilateur eectue un travail quivalent celui d'un traducteur
humain qui traduit un ouvrage d'une langue dans une autre, celui d'un interprte
voque plus d'un traduction simultane, qui produit la traduction mesure que
le discours est tenu.
En gnral, un interprte est beaucoup plus facile raliser qu'un compilateur.
Un compilateur traduit le programme une fois pour toutes : le rsultat est
un programme dans le langage cible, qu'on peut ensuite excuter un nombre
indni de fois. En revanche, un interprte doit traduire chaque lment du
langage source chaque fois qu'il faut l'excuter (mais seulement quand il faut
l'excuter : pas besoin de traduire ce qui ne sert pas). En terme de temps,
la compilation est une opration beaucoup plus lente que le chargement d'un
programme dans un interprte, mais l'excution d'un programme compil est
beaucoup plus rapide que celle d'un programme interprt.
Le compilateur ne peut dtecter des erreurs dans le programme qu' partir
d'une analyse statique : en examinant le programme sans le faire tourner. Un
interprte pourra aussi dtecter des erreurs de faon dynamique, lors de l'excution du programme.
Certains langages se prtent plus la compilation et d'autres plus l'interprtation. Par exemple les langages o les donnes ne sont pas dclares avec
un type prcis (comme Lisp) sont plus faciles interprter qu' compiler, alors
qu'un langage comme C a t pens ds sa conception pour tre facile compiler.

Exemple :

l'addition de deux nombres par un processeur est une opration


compltement dirente suivant qu'il s'agit de nombres entiers ou de nombres en
virgule ottante. On peut en Lisp crire une fonction (stupide) qui additionne
deux nombres sans spcier leur type :
9

(defun foo (a b) (+ a b))

Quand on appelle cette fonction avec deux arguments entiers (par exemple avec
(foo 1 1)) l'interprte eectue une addition entire parce qu'il dtecte que les
deux arguments sont du type entier ; si on l'appelle avec deux nombres en virgule
ottante (par exemple avec (foo 1.1 1.1)), alors il eectue une addition en
virgule ottante. Dans un langage o les donnes sont types, comme le C,
le compilateur a besoin de connatre le type des arguments au moment o il
compile la fonction foo, sans pouvoir se rfrer la faon dont la fonction est
appele. Il faudra par exemple avoir deux fonctions direntes pour traiter les
nombres entiers et les nombres en virgule ottante
int iadd(int a, int b){ return a+b; }
float fadd(float a, float b){ return a+b; }

1.3.2

Compilateur + interprtes

Pour mlanger les avantages des interprtes et des compilateurs, on rencontre souvent des situations o les concepteurs mlangent interprtation et
compilation.
Dans les langages fonctionnels, il est souvent possible d'aecter des types
aux donnes pour faciliter la compilation de parties du programme.
On a souvent un compilateur qui traduit le langage source dans un langage
cible universel, proche du langage machine, puis un interprte qui se charge
d'excuter ce programme en langage cible (plus rapidement qu'il ne le ferait
partir du langage source). Pour des raisons historiques, on appelle souvent
ce langage intermdiaire du byte-code. Le compilateur est crit une fois pour
toutes, et l'interprte du langage intermdiaire est plus petit, plus rapide et
beaucoup plus facile porter sur un nouveau processeur qu'un interprte pour
le langage source. On peut avoir des compilateurs pour des langages dirents
qui produisent tous du byte-code pour la mme machine virtuelle, et on pourra
alors excuter les programmes du langage source vers tous les processeurs pour
lesquels il existe des interprtes. Voir la gure 1.1.
Un exemple historique est le compilateur Pascal UCSD qui traduisait les
programmes en langage Pascal dans un langage machine universel, appel Pcode qui tait ensuite interprt.
Usuellement, les programmes Prolog sont interprts ; il est aussi souvent
possible de les compiler, en gnral vers le langage machine d'un processeur
adapt Prolog qu'on appelle WAM (comme Warren Abstract Machine), puis
les instructions de la WAM sont traduites par un interprte dans le langage
machine du processeur. (L'ouvrage de Hassan At-Kaci, Warren's Abstract Machine : A Tutorial Reconstruction, 1991, est disponible sur le web et contient
une description pdagogique et relativement abordable de la WAM.)
Un exemple rcent concerne le langage Java ; normalement un programme
Java est traduit par un compilateur dans le langage d'une machine universelle
10

Programme
en langage X

Compilateur de X
vers bytecode

Programme
en langage Y

Compilateur de Y
vers bytecode

Programme
en langage Z

Compilateur de Z
vers bytecode

Byte code

Interprete
de byte code
pour le processeur
A

Interprete
de byte code
pour le processeur
B

Interprete
de byte code
pour le processeur
C

Execution du
programme sur A

Execution du
programme sur B

Execution du
programme sur C

1.1  La combinaison d'un compilateur vers un byte-code unique et d'interprtes pour ce byte-code permet de combiner des avantages des compilateurs
et des interprtes.
Figure

11

Programme
en langage
source
= caracteres

analyseur
lexical

mots

analyseur
syntaxique

phrases

generation
de code

Programme final
= instructions du
langage cible

optimisaiton

1.2  La structure gnrale simplie d'un compilateur, qui prend un


programme en langage source et produit un programme quivalent en langage
cible.
Figure

appele JVM (comme Java Virtual Machine) ; ce programme pour la JVM peut
ensuite tre transmis entre machines puis interprt. Pour obtenir des performances dcentes, ce programme pour la JVM peut aussi tre compil juste avant
son excution sur la machine cible (on parle alors de compilation JIT  Just In
Time). La compilation du langage de la JVM vers le langage du processeur
est beaucoup plus simple et beaucoup plus rapide que la compilation du programme Java originel. L'excution du programme compil est plus rapide que
l'interprtation du byte-code d'origine.

1.4 La structure gnrale d'un compilateur


Un compilateur est en gnral un programme trop gros pour qu'il soit possible de le matriser d'un coup. Pour avoir des morceaux plus faciles digrer, on
le divise usuellement en parties distinctes qui jouent chacune un rle particulier.
La structure usuelle est rsume dans la gure 1.2
On va voir sommairement le rle de chacune ces parties dans cette section,
puis en dtail dans des chapitres ultrieurs.

1.4.1

L'analyseur lexical

Au dpart, le programme en langage source se prsente comme une suite de


caractres. Le premier travail est eectu par l'analyseur lexical, qui va dcouper
cette squence de caractres en mots constitutifs du langage. Le rsultat est une
squence de mots, avec leurs types et leurs valeurs autant qu'il lui est possible
de les reconnatre. Par exemple, le programme C
main(){printf("Hello world\n");}

sera dcoup en 10 mots comme dans la table suivante :

12

caractres

type
valeur
identicateur
main
parenthse ouvrante
parenthse fermante
accolade ouvrante
identicateur
printf
parenthse ouvrante
chane de caractres Hello world\n
parenthse fermante
point virgule
accolade fermante
Le travail n'est pas toujours aussi facile que dans cet exemple simple, et il
n'est pas tout fait vident de dcouper du C valide comme a+++b ou a--2.
Les nombres ottants notamment orent toutes sortes de piges divers qui compliquent les oprations.
main
(
)
{
printf
(
"Hello world\n"
)
;
}

1.4.2

L'analyseur syntaxique

L'analyseur syntaxique regroupe les mots produits par l'analyseur lexical


en phrases, en identiant le rle de chacun des mots. Les phrases en sortie
sont souvent reprsentes sous la forme d'un arbre syntaxique dont les feuilles
contiennent les mots, leurs valeurs et leurs rles et les noeuds internes la manire dont il sont regroups, ou bien sous la forme d'un programme en langage
intermdiaire.
Les mots du programme prcdent seront ainsi regroups, directement sous
la forme d'un arbre syntaxique (implicitement ou explicitement) comme dans la
gure 1.3

1.4.3

La gnration de code

La gnration de code consiste traduire les phrases produites par l'analyseur syntaxique dans le langage cible. Pour chaque construction qui peut apparatre dans une phrase, le gnrateur de code contient une manire de la
traduire.

1.4.4

L'optimisation du code

Il y a souvent des modications du code gnr qui permettent d'obtenir un


programme quivalent mais plus rapide. Pour cette raison, l'optimiseur examine
le code gnr pour le remplacer par du meilleur code.
Il y a trois raisons principales pour ne pas produire directement du code
optimis dans le gnrateur de code. D'une part cela permet au gnrateur
de code de rester plus simple. D'autre part l'optimisation peut tre trs coteuse en temps et en espace mmoire, et cela permet de sauter cette tape et
13

definition
de fonction

nom de la
fonction

arguments
de la fonction

corps de
la fonction

bloc dinstructions

instruction

appel de
fonction

liste
darguments

main

printf

"Hello world\n"

1.3  Un arbre syntaxique permet d'identier le rle de chaque mot et


de chaque groupe de mots d'un programme.
Figure

d'obtenir plus rapidement un programme tester qui ne tournera qu'une seule


fois ; le gain de temps obtenu en sautant l'optimisation compense largement le
(modeste) gain de temps qu'on aurait obtenu en faisant tourner une seule fois
le programme optimis. Finalement il est plus dicile de mettre au point un
programme optimis : certaines instructions du programme source ont pu tre
combines ou rordonnes, certaines variables ont pu disparatre.

1.4.5

Variantes

En C, avant l'analyseur lexical, il y a un traitement par le prprocesseur, qui


traite les lignes qui commencent par #, comme les # include et les # define ;
on peut voir le prprocesseur comme un compilateur, qui possde lui aussi un
analyseur lexical et un analyseur syntaxique et qui produit du C.
Le langage cible du premier compilateur C++, nomm cfront, a t pendant
longtemps le langage C ; le compilateur C se chargeait ensuite de traduire le
programme C en langage machine.
Frquemment, la gnration de code se fait en deux tapes ; l'analyseur syntaxique (ou un premier gnrateur de code) produit des instructions dans un
langage intermdiaire, proche de l'assembleur ; c'est l-dessus que travaille l'optimiseur. Ensuite ce langage intermdiaire est traduit en langage cible. Cela
14

permet d'avoir un optimiseur qui fonctionne quel que soit le langage cible.

1.5 Qui compile le compilateur ?


Le plus souvent, un compilateur produit du code pour le microprocesseur sur
lequel il s'excute. Il y a deux exceptions importantes : quand la machine qui doit
excuter le programme compil n'est pas accessible ou pas assez puissante pour
faire tourner le compilateur (c'est souvent le cas dans les systmes embarqus)
et quand on fabrique le premier compilateur d'une (famille de) machine, ce
qu'on appelle bootstrapper un compilateur, par analogie avec le dmarrage de la
machine.
On appelle ce type de compilateur un compilateur crois, par opposition au
cas courant qu'on appelle un compilateur natif.

1.5.1

La compilation croise

On utilise frquemment les compilateurs croiss pour produire du code sur


les machines embarques, qui ne disposent souvent pas de ressources susantes
pour faire tourner leur propre compilateur.
La mthode est voque sur la gure 1.4. Sur une machine A, on a utilise
le compilateur pour A pour compiler le compilateur pour B : on obtient un
programme qui tourne sur A et qui produit du code pour B. Ensuite, toujours
sur A, on utilise ce compilateur pour produire du code susceptible de s'excuter
sur B. Ensuite on doit transfrer ce code sur la machine B pour l'excuter.
L'installation et l'utilisation des compilateurs croiss sont souvent des oprations complexes. Il n'y a pas que le programme compilateur modier, mais
aussi les bibliothques et souvent une partie de leurs chiers de /usr/include.
Dans le systme Plan 9, ce problme a t rsolu d'une faon particulirement lgante : toutes les compilations sont des compilations croises, y compris
lorsqu'on compile pour la machine sur laquelle on compile. Le rsultat est d'une
simplicit d'utilisation droutante.

1.5.2

Le bootstrap d'un compilateur

Obtenir un compilateur sur une nouvelle architecture est un cas spcial de


compilation croise, comme indiqu sur la gure 1.5.
Le programme destin s'excuter sur la machine B est le compilateur luimme. Une fois qu'il est transfr sur B, il va servir recompiler le compilateur
pour B, de manire obtenir un compilateur natif compil par un compilateur
natif. En guise de test, on va sans doute procder encore une compilation an
de comparer les deux compilateurs.

15

sources du
compilateur
pour B

sources du
programme a
faire tourner
sur B

compilateur
pour A
compilateur
pour B

compilateur
pour B

programme
qui tourne
sur B

copie sur B

programme
qui tourne
sur B

1.4  Compilation croise d'un programme : les rectangles entourent des


chiers dont le contenu n'est pas excutable dans le contexte o ils se trouvent,
alors que les ellipses entourent des programmes qui peuvent s'excuter. Les deux
compilations ont lieu sur la machine A.
Figure

1.6 Aprs la compilation, l'dition de liens (Supplment)


Sous Unix le compilateur C (et beaucoup d'autres compilateurs) produisent
du langage machine mais avant l'excution du programme se placent encore
trois tapes : l'dition de liens, le chargement et la rsolution des bibliothques
dynamiques. On pourrait discuter pour savoir si ces tapes font parties de la
compilation ou pas.

1.6.1

L'dition de liens

Le compilateur compile indpendamment chacun des chiers et pour chacun


produit un chier qui contient la traduction du programme en langage machine
accompagn d'une table des symboles, dans un chier qui porte en gnral un
nom qui se termine par .o (comme objet). Si on appelle le compilateur C avec
l'option -c, il s'arrte cette tape.
Si on l'appelle sans l'option -c, le compilateur passe ensuite la liste des
chiers .o qu'il vient de traiter l'diteur de liens. Celui-ci regroupe tous les
chiers .o dans un seul chier et y ajoute le code ncessaire qu'il va prendre
dans les bibliothques.

16

sources du
compilateur
pour B

sources du
compilateur
pour B

compilateur
pour A
compilateur
pour B

compilateur
pour B
compilateur
pour B

copie sur B

copie sur B

sources du
compilateur
pour B

compilateur
pour B

compilateur
pour B
compilateur
pour B

compilateur
pour B
compilateur
pour B

1.5  La fabrication du premier compilateur natif pour la machine B,


l'aide de la machine A. Les deux compilations du haut se produisent sur A, les
deux du bas sur B, la machine cible.
Figure

17

Le regroupement des chiers .o


Quand le compilateur produit du langage machine, il doit spcier quel
endroit de la mmoire les instructions seront places. (Par exemple on verra au
prochain chapitre que les instructions de branchement contiennent l'adresse de
l'instruction laquelle sauter ; il faut bien savoir quelle adresse se trouve cette
instruction pour pouvoir y sauter.) Le compilateur spcie que toutes les instructions sont places partir de l'adresse 0 et il ajoute au langage machine une
table des symboles qui contient tous les endroits o une adresse est utilise, ainsi
que les endroits du programme qui font appel des adresses que le compilateur
ne connait pas (par exemple l'adresse de la fonction printf).
L'diteur de lien prend les morceaux de code prsents dans chaque chier
objet et les regroupe en leur assignant des adresses direntes. En utilisant les
tables des symboles, il met aussi jour chaque rfrence une adresse qui a t
modie.
Les choses sont un tout petit plus complexes que dcrit ici parce que le
code produit par le compilateur est en fait aect d'un type : les instructions,
les donnes qui ne seront pas modies, celles qui sont initialises 0 et celles
initialises avec autre chose que 0. L'diteur de lien regroupe dans le chier
rsultat le code par type. Ceci permet d'utiliser le gestionnaire de mmoire
virtuelle pendant l'excution du programme pour s'assurer que le programme
ne modie pas ses propres instructions et pour faire partager les instructions
et les donnes pas modies par les dirents processus qui excutent le mme
programme. (De plus, la mmoire qui est initialise 0 n'est stocke que sous
la forme d'un compteur.)
Le mot clef static aect une variable globale ou une fonction C permet
d'viter qu'elle apparaisse dans la table des symboles du chier objet et donc
qu'elle soit utilise dans un autre chier.
C'est cette tape qu'il est possible de dtecter les fonctions ou les variables
qui sont dnies plusieurs fois dans le programme (par exemple parce qu'elles
sont dnies dans un chier .h qui est inclus dans plusieurs chiers .c ; dans
les chiers .h, on prvient le compilateur de l'existence des variables globales,
mais avec extern on lui indique qu'il ne doit pas les dnir.

Les bibliothques
Le programme compil fait le plus souvent appel des fonctions qu'il n'a pas
dnies comme printf. L'diteur de liens va chercher dans ces bibliothques
le code qui correspond aux fonctions dont il n'a pas trouv le code dans le
programme.
(Une erreur commune chez les dbutants consiste imaginer que la fonction
printf se trouve dans le chier stdio.h ; en ralit, les chiers .h contiennent
des prototypes de fonction qui permettent de prvenir le compilateur de leur
existence, du type d'arguments qu'elles attendent et du type de valeurs qu'elles
18

renvoient, mais le code de printf est lui compil une fois pour toutes et plac
dans une bibliothque.)
Le plus souvent, ces bibliothques sont places dans les rpertoires /lib ou
/usr/lib. Par dfaut, le compilateur C demande l'diteur de lien d'utiliser la
seule bibliothque C standard. On peut lui demander d'utiliser d'autres bibliothques avec l'option -l ; par exemple l'option -lm indique qu'il faut utiliser la
bibliothque de fonctions mathmatiques et l'diteur de liens ira consulter aussi
le contenu des chiers libm.a ou libm.so.*.
Les bibliothques sont une forme compacte pour stocker de nombreux chiers
.o ; il peut s'agir d'une bibliothque statique (dans ce cas son nom se termine
par .a, l'diteur de lien extrait le code de la fonction et l'ajoute dans le chier
rsultat) ou bien d'une bibliothque dynamique (dans ce cas son nom se termine
par .so suivi d'un numro de version et l'diteur de lien ajoute au chier rsultat
l'information ncessaire pour que le code soit ajout au moment o le programme
sera lanc).
cette tape, il n'est pas possible de dtecter les fonctions qui existent
dans la bibliothque mais qui ne sont pas ajoutes au programme parce que le
programme contient dja une fonction de ce nom.

Le code de dmarrage
Une tche supplmentaire cone par le compilateur l'diteur de lien est de
mettre en place l'environnement ncessaire pour faire tourner un programme C.
Pour cela, le compilateur passe l'diteur de lien un chier .o supplmentaire
qui contient l'initialisation de l'environnement, l'appel la fonction main et la
sortie du processus (si la fonction main n'est pas sortie directement avec l'appel
systme exit).
Ce chier porte usuellement un nom qui contient crt comme C runtime.
Il en existe direntes versions suivant le compilateur et la manire dont le
programme est compil.
Le rsultat de l'dition de lien est plac dans un chier qui porte par dfaut
le nom a.out.

Les bibliothques dynamiques


L'diteur de lien peut aussi ajouter au programme des rfrences des bibliothques dynamiques ; ce n'est pas le code extrait de la bibliothque qui est
ajout au chier, mais une simple rfrence au code qui reste dans le chier
bibliothque.
Dans tous les systmes Linux que j'ai utilis, le compilateur choisit par dfaut
d'utiliser les bibliothques dynamiques. Personnellement, je pense le plus grand
mal de ces bibliothques. cause d'elles, on ne peut pas se contenter d'installer
un programme sous la forme d'un chier excutable (et ventuellement de son
chier de conguration) ; on a aussi besoin d'installer les librairies qui vont avec
19

si elles ne sont pas prsentes ; si elles sont prsentes, c'est souvent avec un numro
de version qui ne va pas ; alors on installe la nouvelle version de la librarie ; mais
les autres programmes qui utilisaient l'ancienne version ne fonctionnent plus.

1.6.2

Le chargement en mmoire

Finalement, lorsqu'un processus fait un appel systme exec d'un chier objet, le contenu du chier est charg en mmoire. Avec les MMU qu'on trouve
peu prs sur tous les processeurs, ce n'est pas un problme puisque chaque
processus dispose de son propre espace d'adressage : il sut de positionner la
MMU pour que l'espace d'adressage virtuel du processus corresponde de la
mmoire physique. En revanche les systmes embarqus n'ont frquemment pas
de MMU ; il faut alors que le systme translate le programme entier, pour que
l'image en mmoire corresponde aux adresses physiques o le programme est
eectivement charg.

1.6.3

Commandes Unix

Voici une srie de manips pour mettre en uvre ce qu'on vient de voir. Les
manipulations sont prsentes sous la forme d'exercices, mais il s'agit essentiellement de lancer des commandes et d'en constater les eets, qui sont indiqus
dans l'nonc.
On partira avec deux chiers ; l'un contient une fonction pgcd que nous
verrons dans un chapitre ultrieur et l'autre une fonction main qui sert appeler
cette fonction. Le chier pgcd.c contient
1
2
3
4
5

/ pgcd . c
/

int
pgcd ( int a , int b ){
int t ;

while ( a != 0){
if ( a < b ){
t = b;
b = a;
a = t;

7
8
9
10
11

12
13

14
15
16

a = b ;

return b ;

Le chier main.c contient


1

/ main . c
20

2
3
4
5
6
7
8
9

/
# include <stdio . h>

int
main ( int ac , char av [ ] ) {
if ( ac < 3){
fprintf ( stderr , " usage : %s N M \ n " , av [ 0 ] ) ;
return 1 ;

10
11
12
13

printf ("% d \ n " , pgcd ( atoi ( av [ 1 ] ) , atoi ( av [ 2 ] ) ) ) ;


return 0 ;

Produire un excutable et l'excuter avec les commandes


$ gcc main.c pgcd.c
$ a.out 91 28

On obtient un message d'erreur si on essaye de produire un excutable


partir d'un seul de ces chiers avec
$ gcc pgcd.c
$ gcc main.c

Ex. 1.1 

Quel sont les messages d'erreur ? quelle tape de la compilation


sont-ils produits ?
Compiler chaque chier indpendamment pour obtenir un chier .o
$ gcc -c main.c
$ gcc -c pgcd.c

Examiner avec la commande nm la table des symboles des chiers .o produits


par ces commandes. Dans la sortie de nm, chaque symbole occupe une ligne ; si le
symbole est marqu comme U, c'est qu'il reste dnir ; s'il est marqu comme
T c'est qu'il s'agit de code excuter et on voit sa valeur.
Quels sont les symboles indnis dans chacun des chiers ?
Combiner les deux chiers .o pour avoir un excutable et l'excuter avec

Ex. 1.2 

$ gcc main.o pgcd.o


$ a.out 85 68

Ex. 1.3 

Qu'imprime la commande a.out ?


La commande nm permet aussi d'examiner la table des symboles du chier
excutable.
Quels sont les symboles indnis dans le chier a.out obtenu
prcdemment ?

Ex. 1.4 
Ex. 1.5 

A quelles adresses se trouvent les fonctions main et pgcd ?


21

Sauver le chier a.out sous le nom dyn.out et compiler le programme sous


la forme d'un programme qui utilise les bibliothques statiques sous le nom
stat.out avec
$ mv a.out dyn.out
$ gcc -static pgcd.c main.c -o stat.out

Ex. 1.6 
Ex. 1.7 

Quelles sont les tailles respectives de dyn.out et de stat.out ?

Le T majuscule dans la sortie de nm permet peu prs d'identier


les fonctions. Combien y a-t-il de fonctions prsentes dans chacun des deux
excutables ?

Ex. 1.8 

La commande ldd permet de dterminer les librairies dynamiques


ncessaires un programme. Indiquer les librairies ncessaires pour les deux
chiers excutables.

Ex. 1.9 

Recompiler les programmes avec l'option verbose de gcc de manire voir toutes les tapes de la production de l'excutable.
On va maintenant refaire toutes les tapes en appelant chaque commande
directement au lieu de passer par gcc.
On peut appeler le prprocesseur soit avec la commande cpp (comme C
PreProcessor ; il y a parfois un problme avec certains systmes o le compilateur
C++ porte ce nom) ou avec l'option -E de gcc.
Faire passer le prprocesseur C sur chacun des chiers C, placer la sortie
dans des chiers nomms x et y.
Combien les chiers x et y comptent-ils de lignes ?
On peut trouver le compilateur gcc proprement dit, avec l'analyse lexicale,
l'analyse syntaxique et la gnration de code dans un chier qui porte le nom
cc1. On peut le trouver avec l'une des deux commandes

Ex. 1.10 

$ find / -name cc1 -print


$ locate */cc1

Ex. 1.11 

O se trouve le chier cc1 sur votre ordinateur ?


Le programme cc1 peut lire le programme sur son entre standard et place
dans ce cas la sortie dans un chier qui porte le nom gccdump. On peut donc
compiler la sortie de l'tape prcdente avec
$
$
$
$

cc1 < x
mv gccdump.s x2
cc1 < y
mv gccdump.s y2

Ex. 1.12 

Que contiennent les chiers x2 et y2 ?


Il faut ensuite faire traiter le contenu des chiers x2 et y2 par la commande
as, qui place sa sortie dans un chier nomm a.out. On peut donc faire
22

$
$
$
$

as
mv
as
mv

x2
a.out x3
y2
a.out y3

Ex. 1.13 

Que contiennent les chiers x3 et y3 ?


Il est aussi possible de pratiquer l'dition de lien directement sans passer
par la commande gcc, mais a devient franchement pnible. On revient donc au
programme principal pour la dernire tape.
$ mv x3 x.o
$ mv y3 y.o
$ gcc x.o y.o

Ex. 1.14 

Examiner le chier a.out produit avec la commande readelf


(pas de correction).

Ex. 1.15 

Trouver sur votre ordinateur tous les chiers qui portent un nom
du type crt*.o.

Ex. 1.16 

Trouver sur votre machine la librairie C standard statique (qui


s'appelle libc.a).

Ex. 1.17 
correction).

Examiner le contenu de libc.a avec la commande ar t (pas de

Ex. 1.18 

Extraire, de nouveau avec ar, le chier printf.o de libc.a.


Combien occupe-t-il d'octets ?

1.7 Plan du reste du cours


Les deux chapitres qui suivent parlent du langage cible le plus courant d'un
compilateur : l'assembleur. Le premier traite de l'assembleur en tant que tel, le
second examine l'assembleur produit lors de la compilation d'un programme C
et permet d'entrer dans le dtail de l'organisation de la pile d'un processus qui
excute un programme C.
Le quatrime chapitre prsente sommairement l'analyse lexicale, certaines
de ses dicults et la faon de les rsoudre avec les outils usuels, sans lex ni les
expressions rgulires qui sont abords dans un chapitre ultrieur.
Les trois chapitres qui suivent prsentent l'analyse syntaxique. Le premier
prsente la problmatique et un exemple lmentaire d'analyseur prcdence
d'oprateur pour les expressions arithmtiques ; le deuxime contient une introduction Yacc (ou Bison) fonde sur des exemples ; dans le troisime, on
trouvera ce qu'il faut de thorie pour mettre au point les grammaires pour des
analyseurs LALR(1).
23

Le huitime chapitre traite de la gnration de code (rapidement) et prsente


des techniques ordinaires d'optimisation.
Le neuvime chapitre contient une tude de cas : il analyse en dtail quelques
aspects du compilateur Gcc, notamment les rgles de la grammaire du langage
partir de ses sources, ainsi les tapes de la gnration de code telles que les
dcrit la documentation.
Le dixime chapitre prsente les sources d'un compilateur complet en une
passe pour un sous-ensemble du langage C.
Le onzime chapitre, optionnel, parle des expressions rgulires (ou expressions rationnelles) et des outils Lex et Flex qui permettent de les utiliser pour
fabriquer des analyseurs lexicaux.

1.8 Rfrences bibliographiques


Il y a de nombreux ouvrages sur la compilation, dont plusieurs sont la fois
excellents et traduits en franais.
Le livre de rfrence sur le sujet tait celui de Aho et Ullman, qui a connu
une deuxime version avec un auteur supplmentaire : A. Aho, R. Sethi et
J. Ullman, Compilers: Principles, Techniques and Tools, 1986. Ce livre est
traduit en franais chez InterEditions sous le nom Compilateurs : principes,
techniques, outils. On fait souvent rfrence ce livre sous le nom de Dragon
Book parce que la couverture est illustre avec un dragon.
Il y a eu une deuxime dition de la deuxime version du dragon book en
2006, avec un quatrime auteur, M. Lam ; cette deuxime dition a aussi t
traduite en franais, mais je ne l'ai pas lue.
Sur les deux outils d'aide l'criture des compilateurs Lex et Yacc, il y a un
livre chez O'Reilly de J. Levine, intitul Lex & Yacc. L'ouvrage est une bonne
introduction, mais mon avis il accorde trop de place lex et ne descend pas
susamment dans les dtails du fonctionnement de Yacc pour permettre son
utilisation pour des projets srieux. Il a t traduit en franais chez Masson,
mais cette traduction ne semble pas disponible l'heure actuelle.
Je recommande galement le livre Modern Compiler Design de D. Grune,
H. Bal, J. H. Jacobs et K. Langendoen paru chez John Wiley and Sons
en 2000, qui a t traduit chez Dunod sous le titre Compilateurs. Il contient
notamment un chapitre sur la compilation du langage fonctionnel Haskell et un
autre sur la compilation de Prolog.

24

Chapitre 2
L'assembleur

Ce chapitre contient une brve prsentation de l'assembleur du processeur


Intel 386 en mode 32 bits. Il vous donne les comptences ncessaires pour lire
et crire des programmes simples en assembleur.

2.1 Assembleur et langage machine


Le processeur lit des instructions dans la mmoire sous la forme de codes
numriques ; ce sont ces codes numriques qu'on appelle du langage machine.
Comme il est trs peu pratique pour nous de manipuler ces valeurs numriques
pour crire de gros programmes corrects 1 , on y accde en gnral sous la forme
de chanes de caractres qui sont traduites directement (et une par une) en
langage machine ; c'est ce qu'on appelle de l'assembleur.
En plus des instructions, les programmes en assembleur contiennent des directives qui ne sont pas traduites directement en langage machine mais indiquent
l'assembleur de quelle manire il doit eectuer son travail.

2.2 Brve introduction la programmation du


processeur Intel 386
J'utilise le processeur Intel 386 quoiqu'il n'ait pas une architecture trs plaisante, parce qu'il est trs rpandu ; on peut l'utiliser sur tous les processeurs Intel
32 bits. De toute faon, quand on sait programmer un processeur en assembleur,
il est plutt facile de s'adapter un nouveau processeur.
Si le systme install sur votre machine est un systme 64 bits, reportez-vous
aux instructions spciques de la n du chapitre suivant.

1. C'est facile d'crire des gros programmes faux directement en code machine et pas trs
dicile d'en crire de petits corrects.
25

Le processeur 386 possde un mode 16 bits dont nous ne parlerons peu


prs pas, qui existe principalement pour lui permettre de continuer excuter les
programmes crits pour le processeur prcdent d'Intel, le 286. Nous utiliserons
le mode 32 bits.
Un processeur se caractrise par 3 choses :
 le jeu d'instructions, qui dcrit les choses qu'il sait faire
 les types de donnes disponibles (la mmoire et les registres, qui sont des
mots mmoire pour lesquels l'accs est super-rapide)
 les modes d'adressage, c'est dire la faon de dsigner les oprandes
et la manire dont tout ceci se combine ; par exemple, certaines instructions ne
fonctionneront que sur certains oprandes adresss d'une certaine faon.

2.2.1

Les oprandes

Les oprandes sont les registres et la mmoire.


Sur le processeur 386, il y a 16 registres de 32 bits, dont 8 registres gnraux :
 Il y a quatre vrais registres gnraux %eax, %ebx, %ecx, %edx. Sur les
processeurs prcdents de la srie, ils contenaient 8 bits (un octet) et
portaient les noms a, b, c et d. Quand ils sont passs 16 bits, on leur a
ajout un x comme eXtended, et ils se sont appels %ax, %bx, %cx et %dx.
Quand ils ont passs 32 bits, on a ajout un e devant comme Extended
et ils portent maintenant ces noms ridicules o extended apparat deux
fois avec deux abrviations direntes
 %ebp et %esp sont deux registres, pas si gnraux que a, qu'on utilise
pour la base et la pile (Base Pointer et Stack Pointer. Nous reviendrons
longuement sur leurs rles dans le prochain chapitre.
 %esi et %edi ne sont pas tellement gnraux non plus ; ils sont spcialiss
dans les oprations sur les blocs d'octets dans la mmoire ; on peut cependant les utiliser comme espace de stockage et pour les calculs arithmtiques
courants.
 Il y a six registres de segment : %cs, %ds, %ss, %es, %fs, and %gs : on peut
les oublier ; puisqu'on n'utilise pas le mode d'adressage 16 bits du 386,
nous ne les utilisons pas.
 Il y a enn deux registres spcialiss : %eflags est le registre d'tat, qui
contiendra notamment le rsultat de la dernire comparaison et la retenue de la dernire opration ; %eip (comme Extended Instruction Pointer)
contient l'adresse de la prochaine instruction excuter.
Attention, les registres existent en un seul exemplaire, comme une variable
globale du C. Ils ne sont pas spciques d'un appel comme une variable locale.
La mmoire est organise sous la forme d'un tableau d'octets, indic par une
valeur qui varie entre 0 et 232 1 (code sur 32 bits d'adresse).
On peut adresser soit un octet, soit un mot de 16 bits (sur deux octets), soit
un long de 32 bits (sur quatre octets).
Les mots peuvent contenir n'importe quoi, mais le 386 ne saura calculer que
26

sur les valeurs entires sans signe ou codes en complment deux (et aussi,
pour mmoire, sur le Dcimal Cod Binaire). Les successeurs du 386 permettent
aussi de calculer sur les nombres ottants.

2.2.2

Les modes d'adressages

Pour chaque mode d'adressage, je donne un exemple d'utilisation avec son


quivalent en C en commentaire, puis quelques explications. Dans l'quivalent C,
i et j dsignent des variables automatiques entires et p un pointeur. Presque
tous les exemples utilisent l'instruction movl qui recopie un mot de 32 bits.
(Comme son nom ne l'indique pas, movl ne modie pas la valeur dsigne par
l'oprande de gauche.)

Adressage registre
Dsigne un registre.
movl %eax,%ebx

// j = i

copie dans le registre %ebx la valeur prsente dans le registre %eax C'est ce qu'on
utilise pour accder l'quivalent des variables ordinaires en C.

Adressage immdiat
Dsigne une valeur immdiate (prsente dans l'instruction).
movl $123,%eax

// i = 123

place la valeur 123 dans le registre %eax ; comme la valeur est code dans l'instruction, cela signie qu'on ne peut absolument pas la modier. C'est l'quivalent des constantes dans un programme C.
Dans les assembleurs d'Unix le caractre $ sert indiquer qu'il s'agit de la
valeur 123. Les autres assembleurs utilisent souvent le dise # pour cela mais
sous Unix ce caractre est trop largement utilis par le prprocesseur C.

Adressage absolu
Dsigne un mot mmoire par son adresse
movl $123,456

place la valeur 123 dans les quatre octets qui se trouvent aux adresses 456
459, considrs comme un seul mot de 32 bits. C'est l'quivalent de l'utilisation
d'une variable globale dans un programme C.

27

Adressage indirect
Dsigne un mot mmoire dont l'adresse est dans un registre.
movl (%ebp),%eax

// i = *p

place dans %eax la valeur dont l'adresse se trouve dans %ebp. C'est l'quivalent
d'une indirection travers un pointeur en C.

Adressage indirect+dplacement
movl 24(%esp),%edi

// z = tab[8]

place dans %edi la valeur qui se trouve 24 octets au-dessus du mot dont l'adresse
est dans %esp. C'est l'quivalent d'une indexation dans un tableau par une
constante en C, ou de l'accs un champs d'une structure.

Adressage indirect+index
movl $123,(%eax,%ebp)

// tab[x] = 123

additionne le contenu d'%eax et d'%ebp : a donne l'adresse du mot o il faut


placer la valeur 123. C'est l'quivalent de l'accs un tableau avec un index
variable.

Adressage indirect + dplacement + multiplication + index


On a aussi un mode dans lequel on peut combiner les deux derniers, plus
multiplier le contenu d'un des registres par 2, 4 ou 8. Par exemple :
movl 24(%ebp,%eax,4),%eax

multiplie le contenu de %eax par 4, lui ajoute le contenu de %ebp et 24 ; on a


ainsi l'adresse d'un mot mmoire dont le contenu est recopi dans %eax.

2.2.3

Les instructions

Je commence par prsenter les types de donnes manipules par le 386 ;


ensuite j'ai regroup les instructions par type d'opration eectue, avec d'abord
celles qui me semblent indispensables puis les autres, qu'on peut gnralement
ignorer.

Les types de donnes


La plupart des instructions peuvent manipuler des octets, des mots ou des
longs ; pour les distinguer, on les postxe avec b, w ou l. Ainsi l'instruction
28

movb dplacera un octet, l'instruction movw un mot de 16 bits sur deux octets
et l'instruction movl un mot de 32 bits sur quatre octets.
Les registres ne portent pas non plus le mme nom suivant la taille de l'oprande utiliser :
movb %al,%ah
recopie les bits 07 d'%eax dans les bits 815
movw %ax,%bx
recopie les bits 015 d'%eax dans ceux d'%ebx
movl %eax,%ebx
recopie les 32 bits d'%eax dans %ebx
movb %al,31415
movw %ax,31415
movl %eax,31415

recopie les bits 0-7 d'%eax l'adresse 31415


recopie les bits 0-15 d'%eax aux adresses 31415-31416
recopie les bits 0-32 d'%eax aux adresses 31415-31418

Dplacement de donnes
mov : recopie une donne
xchg : change deux donnes
push : empile une valeur
pop : dpile une valeur
lea : calcule une adresse et copie la (Load Eective Address)
pea : calcule une adresse et empile la (Push Eective Address)

conversions
movsx : convertit une valeur signe sur un octet (ou un mot) en une valeur sur

un mot (ou un long)


movzx : pareil avec une valeur non signe

arithmtique
addl %eax,%ebx : ajoute le contenu d'%eax celui d'%ebx
incl %eax : ajoute 1 au contenu d'%eax
subl %eax,%ebx : te le contenu d'%eax de celui d'%ebx
decl %eax : retire 1 au contenu d'%eax
cmpl %eax,%ebx : comme une soustraction mais sans stocker le rsultat
negl %eax : change le signe du contenu d'%eax
imull %ebx,%ecx : multiplication, rsultat dans %ecx
idivl %ecx : divise %edx-%eax par %ecx, rsultat dans %eax et reste dans %edx

logique, dcalage, rotation


and : et logique (bit bit) entre les deux oprandes

29

or : ou logique (bit bit) entre les deux oprandes


xor : ou exclusif (bit bit) entre les deux oprandes
not : nie chaque bit de son oprande
sal, shr : dcalage gauche, sign ou pas (c'est pareil)
sar, shr : dcalage droite, sign ou pas (ce n'est pas pareil ; dans un dcalage

de nombre non sign les places libres sont remplies avec des bits 0 alors que
pour un nombre sign elles sont remplies avec des copies du bit de signe).

transfert de contrle
jb : jump or branch, la prochaine instruction sera celle dont l'adresse est indique par l'oprande ; jump et branch sont deux instructions direntes pour le

processeur dans lesquelles l'adresse de la prochaine instruction n'est pas code


de la mme manire. L'assembleur choisit notre place la plus compacte des
deux. suivant la valeur de l'adresse destination.
jXX : jump conditionnal ; le saut ne sera eectu que si la condition est remplie.
XX indique la condition et peut tre e (equal), ne (not equal), g (greater), l
(less), ge, le etc. Le rsultat test est celui de la dernire opration.
call xxx : pour les appels de fonctions, cette instruction empile l'adresse qui
suit l'instruction et place xxx dans %eip.
ret : pour revenir d'une fonction, l'instruction dpile l'adresse qui se trouve
au sommet de la pile et la place dans %eip.
int : active une exception, pour eectuer un appel systme.

prologue et pilogue de fonctions


Nous reviendrons plus en dtail sur ces instructions dans le chapitre suivant
enter $24,%ebp : quivalent pushl %ebp ; movl %esp,%ebp ; subl $24,%esp
leave : quivalent movl %ebp,%esp; popl %ebp

mouvement (facultatif)
pusha/popa : push/pop all registers (les 8 registres dits gnraux)
lahf : recopie %ags dans %ah
sahf : recopie %ah dans %ags
pushf/popf : push/pop %ags

conversions (facultatif)
cdq (cwd) : duplique le bit de signe d'%eax (%ax) dans %edx (%dx).
cbw (cwde) : duplique le bit de signe d'%al (%ax) dans %ax (%eax)

30

arithmtique (facultatif)
: addition avec retenue
: soustraction avec retenue
%bl : multiplication non signe de %al, rsultat dans %ah-%al
%bx : idem pour %ax, rsultat dans %ax-%dx
%ebx : idem pour %eax rsultat dans %eax-%edx
: avec un seul oprande, c'est comme comme mul, mais pour des entiers
signs
imul : avec trois oprandes, constante oprande dans registre
div, idiv : division pour les octets et les mots courts
daa, das, aaa, aas, aam, aad : pour le Dcimal Cod Binaire ; le principe
est d'avoir deux chires dcimaux cods dans les deux groupes de quatre bits
d'un octet ; ce codage des nombres est utile pour les langages comme Cobol ou
PL/1 dans lesquels on peut spcier le nombre de chire dcimaux de prcision
des variables.
addc
subb
mulb
mulw
mull
imul

logique, dcalage, rotation (facultatif)


bt : bit test
bts : bit test and set
btr : bit test and reset
btc : bit test and complment
bsf : bit scan forward : trouve l'index du premier bit 1 (dans un octet, mot

ou long)
bsr : bit scan reverse : idem pour le dernier bit 1
shld, shrd : dcale deux mots pour en produire un seul (a permet de dcaler
des zones de mmoire facilement)
rol, ror : rotate left or right
rcl, rcr : idem sauf que a dcale aussi la retenue
test : comme and, mais ne stocke pas le rsultat

transfert de contrle (facultatif)


iret : retour d'exception n'est utilis que dans le noyau du systme d'exploita-

tion pour un retour aprs un appel systme.


loop : dcrmente %ecx et se dbrancher si %ecx vaut 0
loope, loopne : idem mais regarde aussi la valeur du ag Z

31

chanes (faculatatif)
movs, cmps, scas, lods,stos : move, compare, scan, load, store oprent sur
des suites d'octets (des chanes de caractres) sur lesquelles pointent %esi et
%edi (source et destination index) et dont la longueur est dans %ecx. Il y a dans
le mot d'tat un ag (DF : direction ag) qui indique s'il faut incrmenter ou
dcrmenter %esi et %edi et un prxe "rep" qui permet de rpter l'opration
jusqu' ce que %ecx contienne 0.
std, cld : mettre DF un (dcrmenter) ou zro (incrmenter)

autres (facultatif)
setXX : positionne un octet suivant l'tat des codes conditions
stc, clc, cmc : set/clear/complement le carry (= la retenue)
xlat : translate, %al = %ebx[%al]

Il y a aussi des instructions spcialises pour la lecture, criture et modication des registres de segments, mais on ne parle pas de mmoire segmente.
De mme, nous n'aurons pas l'occasion de faire des entres sorties directement
depuis l'assembleur.

2.2.4

Les directives

.long, .word, .byte, .string : rserve de la place dans la mmoire


.globl : fait apparatre un symbole dans la table des symboles du chier

fabriqu.

.text, .data : spcie dans quelle zone de la mmoire (une section) l'assembleur doit placer le code qui suit.
Indiquer ce qui est modi (avec sa nouvelle valeur) par chacune
des instructions suivantes, excutes en squence (en supposant que la mmoire
utilise est disponible) :

Ex. 2.1 

movl $24,%eax
movl $28,%ebx
movl %eax,24
addl %ebx,%eax
movl %eax,(%ebx)
movl %eax,(%eax)
movl 24(%ebx),%ecx
movl 24,%eax
subl (%eax),%ecx

32

movl %ecx,4(%eax)
movl $123,(%ecx,%eax)
movl $12,%eax
movl (%ecx,%eax,2),%ebx

2.3 Des exemples de programmes en assembleur


Le but de la section n'est pas de devenir un programmeur de bon niveau en
assembleur, mais d'en saisir les mcanismes essentiels pour tre en mesure de
comprendre l'assembleur produit par le compilateur C.

2.3.1

Interactions entre l'assembleur et le C

Comme c'est un peu compliqu de faire des appels systmes en assembleur,


nous allons utiliser des fonctions en C pour imprimer et pour terminer le programme.

La premire fonction en assembleur : accs aux variables globales


Je commence avec une fonction (stupide) add11 qui ajoute 11 la variable
globale a. En C, on la dnirait avec
extern a;
void
add11(void){
a += 11;
}

Le code assembleur est le suivant (les numros de lignes n'appartiennent pas au


chier ; ils ne sont l que pour faire rfrence au code plus facilement)
// bavarasm . s
. globl a
. globl add11

1
2
3
4
5
6
7
8

add11 :

. text
addl
ret

$11 , a

Le chier commence par deux directives .globl qui indiquent l'assembleur


que a et add11 doivent apparatre dans la table des symboles du chier rsultat
pour pouvoir tre utilises dans d'autres chiers du mme programme. A part
qu'on ne spcie pas le type de a et add11, c'est l'quivalent du C :
33

extern int a; extern void add11(void);

La ligne 5 contient aussi une directive, .text, pour indiquer que ce sont des
instructions qui suivent ; l'diteur de lien pourra donc les placer dans une zone
mmoire que le processus ne pourra pas modier (parce que les instructions ne
sont pas modies lors de l'excution d'un processus).
On a ensuite, ligne 6, la dnition d'une tiquette add11 pour notre fonction,
puis sur les deux lignes suivantes les instructions de la fonction : l'instruction
de la ligne 6 ajoute la constante 11 la variable a et celle de la ligne 7 eectue
un retour de la fonction.
Notre chier en assembleur n'est pas susant pour lancer le programme ; il
faut galement dnir et initialiser la variable a, avoir quelque chose qui commence et termine le programme et entre les deux, appeler la fonction et imprimer
la valeur de a. Je fais ceci avec un chier C :
1
2
3
4
5
6
7
8
9
10
11
12
13
14

/ bavar . c
Appel d ' une fonction en assembleur
/
# include <stdio . h>
int a ;
int
main ( ) {
a = 23;
add11 ( ) ;
printf (" a = %d \ n " , a ) ;
return 0 ;

On voit que l'appel de la fonction dnie en assembleur se fait comme pour une
fonction ordinaire.
Pour obtenir un programme excutable, nous passons les deux chiers au
programme gcc ; il va compiler le chier C et se contenter de traduire le chier
assembleur en langage machine. On peut excuter le chier a.out rsultat.
$ gcc -g ba-var.c ba-var-asm.s
$ a.out
a = 34
$

Renvoi de valeurs entires de l'assembleur vers le C


Au lieu de modier une variable globale, il est plus lgant de renvoyer une
valeur. Nous modions en consquence notre programme C :
34

1
2
3
4
5
6
7
8
9
10
11
12
13

/ bbret . c

Utiliser une valeur renvoyee de l ' assembleur


/
# include <stdio . h>
int a ;
int
main ( ) {
a = 23;
printf (" a = %d \ na + 11 = %d \ n " , a , add11bis ( ) ) ;
return 0 ;

Pour renvoyer une valeur entire, la convention adopte par le compilateur


est que la fonction qui appelle trouvera cette valeur dans le registre %eax ; la
fonction appele l'y aura place avant d'eectuer le retour avec ret 2 . Le chier
assembleur contient
1
2
3
4
5
6
7
8
9

// bbvarasm . c
. globl a
. globl add11bis
. text
add11bis :
movl
addl
ret

a ,% eax
$11 ,% eax

Ex. 2.2 

crire en assembleur une fonction delta() qui renvoie b2 4ac


calcule avec trois variables globales a, b et c.

Passage d'un argument du C vers l'assembleur


Plutt que de passer la fonction assembleur la valeur avec laquelle calculer
dans une variable globale, il serait prfrable de la lui passer en argument. Le
programme C deviendrait alors :
1
2
3
4

/ bcarg1 . c

Passer un argument a l ' assembleur


/
# include <stdio . h>

2. On peut noter que la fonction qui appelle n'a aucune manire de savoir si la fonction
appele a plac ou pas quelque chose dans %eax ; le registre contient
une valeur et il
n'y a pas de manire de savoir si elle a t place l dlibrment par la fonction appele ou
bien s'il s'agit du rsultat d'un calcul intermdiaire prcdent.
toujours

35

%esp

Pile
111111
000000
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111

Pile
111111
000000
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111

Pile
111111
000000
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111

Pile
1111111
0000000
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111

argument

argument

%esp

%esp

argument

Pile
111111
000000
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111

%esp
adr. de retour
%esp

(a)

(b)

(c)

(d)

(e)

2.1  Pour passer un argument, la fonction qui appelle l'empile (a),


puis excute l'instruction call qui empile par dessus l'adresse de retour (b). La
fonction appele trouve l'argument juste au-dessus de l'adresse de retour (c).
Pour nir, la fonction appele excute un ret qui dpile l'adresse de retour (d).
C'est la fonction appelante qui doit dpiler l'argument qu'elle avait empile pour
retrouver la situation initiale de la pile (e).
Figure

6
7
8

int
main ( ) {
int a = 2 3 ;

9
10
11
12

printf (" a = %d , a + 11 = %d \ n " , a , add11ter ( a ) ) ;


return 0 ;

Pour passer les arguments, la convention utilise par le compilateur gcc sur
le 386 est de les empiler, sur la pile principale pointe par %esp, en dessous
de l'adresse de retour 3 . Sur ce processeur, la pile commence dans les adresses
hautes et croit vers le bas. Le mcanisme est montr sur la gure 2.1. Le code
assembleur sera donc le suivant (attention, ce code ne fonctionne pas sur les
processeurs Intel 64 bits) :
1
2
3
4
5
6
7

// bcarg1asm . s
. globl add11ter
. text
add11ter :
movl
4(% esp ) ,% eax
addl
$11 ,% eax
ret

L'instruction intressante est celle de la ligne 5, qui rcupre l'argument dans


4(%esp) pour le placer dans %eax. La fonction add11ter trouve la pile dans

3. Le passage des arguments par la pile est couramment utilis par de nombreux compilateurs sur beaucoup de processeurs. Une autre convention possible qu'on rencontre aussi
frquemment consiste les faire passer par des registres, mais dans ce cas les choses se compliquent pour compiler les fonctions qui ont des arguments trop gros pour tenir dans un
registre (une structure C) ou qui ont plus d'arguments que le processeur n'a de registres.
36

l'tat dcrit dans le schma (c) de la gure 2.1. Comme la pile croit vers les
adresses basses et que l'adresse de retour occupe 4 octets (32 bits), l'argument
se trouve 4 octets au-dessus de l'adresse contenue dans le registre %esp qui
dsigne le sommet de la pile et on l'adresse donc avec 4(%esp).
crire en assembleur une fonction delta() qui renvoie b2 4ac
calcule avec trois arguments a, b et c.

Ex. 2.3 

Appel d'une fonction C depuis l'assembleur


Puisque nous connaissons les modalits de passage d'un argument dans le
langage C, nous pouvons aussi appeler des fonctions C depuis l'assembleur. Ainsi
le programme suivant :
// bdcallc . s
. globl main

1
2
3
4
5
6

. data
str :
. asciz " Bonjour tout le monde \ n "
strbis : . asciz " Impossible \ n "

7
8
9

main :

10
11
12
13
14
15
16
17
18
19
20

. text
pushl
call
popl

$str
printf
%eax

pushl
call
popl

$0
exit
%eax

pushl
call
. end

$strbis
printf

est peu prs quivalent au programme C


int
main(){

printf("Bonjour tout le monde\n");


exit(0);
printf("Impossible\n");

La fonction main est ici crite en assembleur. Elle empile la chane de caractres
imprimer, appelle la fonction printf puis dpile la chane 4 . Elle appelle en-

4. L'instruction popl %eax dpile l'adresse de la chane dans le registre %eax, ce qui est
sans intrt. En revanche elle se code sur deux octets alors que l'instruction normale pour ter
37

suite la fonction exit aprs avoir empil la constante 0 5 . Les trois instructions
suivantes ne sont normalement pas excutes, puisque exit tue le processus et
ne revient donc pas.

2.3.2

Tests

La principale dirence entre l'assembleur et les langages de programmation


usuels est la manire dont on eectue des tests en deux tapes. La plupart des
instructions qui modient les donnes positionnent dans le registre %eflags des
bits qui indiquent si le rsultat vaut 0, s'il est ngatif, s'il y a eu une retenue,
etc. Pour eectuer un test, on fait suivre ces instructions d'un saut conditionnel
en fonction de l'tat des bits de %eflags. Par exemple, au lieu d'crire en C :
if (a > 0){
b += 1;
a -= 1;
}

On pourra crire en assembleur

next:

andl
jle

a,a
next

incl
decl

b
a

// tester la valeur de a
// si a <= 0, sauter les deux
//
instructions suivantes
// incrmenter b
// dcrmenter a

On commence par positionner les bits du registre %eflags en fonction de la


valeur de a en calculant a & a ; la valeur de a ne sera pas modie et les bits de
%eflags seront positionns. Ensuite, si le rsultat de la dernire opration tait
infrieur ou gal 0 (c'est dire si a n'tait pas strictement positif), on saute
sur l'tiquette next, ce qui nous vite d'excuter les deux instructions suivantes
qui incrmentent b et dcrmentent a.
crire en assembleur une fonction mdian3 qui reoit trois arguments et renvoie celui qui n'est ni le plus grand, ni le plus petit. La fonction est
utile dans Quick Sort pour choisir le pivot entre le premier lment, le dernier
et celui du milieu.

Ex. 2.4 

2.3.3

Boucles

Souvent les processeurs n'ont pas d'instructions spciales pour les boucles.
(Le processeur 386 dispose d'instructions spciques pour les boucles, prsentes

quelque chose de la pile addl $4,%esp a besoin d'un octet pour coder l'instruction quatre
octets pour coder la constante 4 sur 32 bits.
5. Comme dans l'exemple prcdent, on pourrait utiliser les deux instructions xorl %eax
%eax ; pushl %eax pour n'utiliser que quatre octets au lieu des cinq qui sont ncessaires ici.
plus

38

sommairement plus haut, mais leur usage n'est souvent pas ais cause des
contraintes qu'elles imposent sur l'utilisation des registres.) On se contente de
combiner un test et un branchement conditionnel vers le dbut du corps de la
boucle. Ainsi, une boucle qui multiplie les dix premiers entiers non nuls entre
eux :
for(prod = i = 1; i <= 10; i++)
prod *= i;

sera traduite en assembleur par du code organis comme les instructions C :


redo:

out:

prod = i = 1;
if (i > 10)
goto out;
prod *= i;
i += 1;
goto redo;
;

Cela donne en assembleur 386, en plaant prod dans %eax et i dans %ebx :

redo:

out:

movl
movl

$1,%ebx
%ebx,%eax

cmpl
jg
imull
incl
jmp

$10,%ebx
out
%ebx,%eax
%ebx
redo

Notons qu'on peut retirer une instruction au corps de la boucle en la rorganisant ; il sut d'inverser le test et le placer la n ; on n'a plus de cette manire
qu'une seule instruction de saut dans la boucle.

redo:
in:

movl
movl
jmp

$1,%ebx
%ebx,%eax
in

imull
incl

%ebx,%eax
%ebx

cmpl
jle

$10,%ebx
redo

39

Ex. 2.5 

Traduire en assembleur la fonction indexa suivante, qui renvoie


l'adresse du premier a dans une chane (ou 0 s'il n'y est pas).
char *
indexa(char string[]){
int i;

for(i = 0; string[i] != 0; i++)


if (string[i] == 'a')
return & string[i];
return 0;

ou bien :
char * indexa(char * p){
for(; *p; p++)
if (*p == 'a')
return p
return 0;
}

Ex. 2.6 

crire une fonction rindexa qui renvoie l'adresse du dernier caractre 'a' dans la chane.

Ex. 2.7 

crire en assembleur une fonction quivalente la fonction strlen.


(Rappel : pour n'accder qu' un seul octet, il faut postxer l'instruction avec
b (comme byte) au lieu de l (comme long)).

Ex. 2.8 

Traduire la fonction suivante du C vers l'assembleur

int
fact(int n){
int r;

2.3.4

for(r = 1; n > 1; n--)


r *= n;
return r;

Pile

La pile est une zone de mmoire ordinaire sur laquelle pointe le registre %esp.
Les instructions call et ret l'utilisent pour empiler et dpiler les adresses de
retour lors des appels de fonctions. On a vu galement qu'elle servait passer
les arguments dans le code gnr par le compilateur C. La pile peut galement
servir stocker les variables locales quand elles sont trop nombreuses ou trop
grosses pour tre stockes dans des registres ou bien quand le code appelle une
40

fonction qui pourrait modier le contenu des registres.


Voici par exemple une fonction qui calcule rcursivement la factorielle de son
argument :
1
2
3

factR :

4
5
6
7
8

cont :

9
10
11
12
13
14
15

. globl
. text

factR

cmpl
jne
movl
ret

$0 ,4(% esp )
cont
$1 ,% eax

movl
decl
pushl
call
addl
imull
ret

4(% esp ) ,% eax


%eax
%eax

factR
$4 ,% esp
4(% esp ) ,% eax

Elle traduit le code C


int
factR ( int n ){
if ( n != 0)
return n ;
else
return n factR ( n 1 ) ;

1
2
3
4
5
6
7

Ex. 2.9 

Traduire la fonction suivante du C vers l'assembleur

int
fib(int n){
if (n < 2)
return n;
return fib(n - 1) + fib(n - 2);
}

Ex. 2.10 
heron:

Traduire l'assembleur suivant en C

.text
.globl

heron

pushl
movl
movl
movl
movl

%ebx
8(%esp),%eax
12(%esp),%ebx
16(%esp),%ecx
%eax,%edx

41

addl
addl
sarl
subl
subl
subl
imull
imull
imull
negl
popl
ret

%ebx,%eax
%ecx,%eax
$1,%eax
%eax,%ebx
%eax,%ecx
%eax,%edx
%ebx,%eax
%ecx,%eax
%edx,%eax
%eax
%ebx

2.4 Les autres assembleurs, les autres processeurs


On trouve souvent plusieurs assembleurs pour un mme processeur. La dirence la plus frappante est l'ordre dans lequel apparaissent les oprandes. Dans
l'assembleur que nous avons vu, pour l'instruction mov, on a la source en premier
et la destination en second. D'autres assembleurs utilisent la convention inverse
avec la destination en premier et la source en second, dans le mme ordre que
quand on crit une aectation en langage volu comme a = b en C.
Chaque processeur possde ses caractristiques propres, mais les principes
de base restent presque tous les mmes. Il n'est pas bien dicile, quand on a
pratiqu un peu l'assembleur, de s'adapter susamment un nouveau processeur pour tre en mesure de dchirer le code produit par le compilateur. En
revanche, coder ecacement en assembleur ncessite une connaissance approfondie de la manire dont il code les instructions et du temps ncessaire pour
eectuer chacune d'entre elles.

2.4.1

Parenthse : les machines RISC

Pendant longtemps, ce qui a limit la vitesse de calcul des processeurs, c'est


le temps d'accs la mmoire, qui croissait beaucoup moins vite que la vitesse
de calcul des processeurs. On appelait ce point (le bus qui relie le processus
la mmoire) le goulot d'tranglement de von Neumann (en anglais von Neumann bottleneck). On valuait mme grossirement la vitesse d'un processeur
simplement par la taille d'un programme excutable.
Les architectes de processeurs ont longtemps cherch viter le problme en
fabriquant des instructions lmentaires le plus compactes possible, an de diminuer le nombre d'changes entre la mmoire et le microprocesseur. Un exemple
typique est l'instruction rep cmps du 386 qui, alors qu'elle est code sur deux
octets seulement, incrmente ou dcrmente deux registres (%esi et %edi), en
dcrmente un troisime (%ecx), eectue une comparaison et recommence jusqu' trouver deux octets dirents.
42

Au milieu des annes 1980, deux quipes universitaires californiennes et une


quipe d'IBM ont ralis que ce genre d'instruction de longueur variable compliquait normment la partie contrle du processeur, qui de ce fait utilisait
une grande partie de la surface disponible dans le circuit intgr ; d'autre part
les conditions d'utilisation de ces instructions taient si complexes que les compilateurs avaient rarement l'occasion de les utiliser. Ils ont mis au point des
ordinateurs avec des instructions beaucoup plus simples qu'ils ont appel des
ordinateurs jeu d'instruction rduit (en anglais Reduced Instruction Set Computer, d'o vient le nom RISC). La caractristique principale en est que toutes
les instructions occupent le mme nombre d'octets, en gnral la taille d'un mot
mmoire. Les instructions de transfert entre les registres et la mmoire ne font
que du transfert ; les instructions de calcul n'oprent que sur des registres.
La simplication obtenue dans l'unit de contrle libre de la place pour disposer d'un plus grand nombre de registres et permet d'avoir des mmoires caches
sur le processeur, ce qui limite l'impact de la lenteur de l'accs la mmoire. On
peut avoir une excution en pipe-line (l'instruction suivante commence tre
excute avant que l'instruction courante ne soit termine) et de l'excution
spculative (on commence excuter l'instruction qui suit un branchement ; si
le branchement est pris, on ne stocke pas son rsultat ; sinon le calcul est dj
eectu et on peut utiliser le rsultat de suite).

43

Chapitre 3
Comprendre un programme C
compil

Dans ce chapitre, nous approfondissons notre approche de l'assembleur en


examinant la manire dont le compilateur traduit le C en langage machine. Le
point crucial ici rside dans le fonctionnement du prologue et de l'pilogue des
fonctions et le chapitre se termine par quelques applications que la comprhension du mcanisme nous permet.

3.1 Du C vers l'assembleur


Nous allons considrer ici une fonction pgcd crite en C. La fonction calcule le
plus grand commun diviseur de deux nombres positifs non nuls qu'elle reoit en
argument, seulement avec des soustractions, sans aucune division. Le principal
intrt de cette fonction ici est qu'elle est simple, mais se compose d'une boucle
qui contient elle-mme un test.
Le code C de la fonction est le suivant :
1
2
3
4
5
6
7
8
9

int
pgcd ( int a , int b ){
int t ;
while ( a != 0){
if ( a < b ){
t = b;
b = a;
a = t;

10
11
12

a = b ;

44

13
14

return b ;

On peut se reprsenter le travail de cette fonction sous forme graphique comme


dans la gure 3.1.
Au lieu de produire du langage machine, dicile dchirer, on peut demander au compilateur avec l'option -S de produire de l'assembleur que nous
pourrons lire plus facilement ; si le chier de dpart s'appelle pgcd.c, l'assembleur sera pos dans un chier pgcd.s. Sur une machine Intel 32 bits, j'obtiens
le chier suivant (avec les numros de lignes rajouts par mes soins pour rfrence) :
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)

pgcd:

.L4:

.L3:
.L2:

.file
"pgcd.c"
.text
.globl pgcd
.type
pgcd, @function
pushl
movl
subl
jmp

%ebp
%esp, %ebp
$16, %esp
.L2

movl
cmpl
jge
movl
movl
movl
movl
movl
movl

8(%ebp), %eax
12(%ebp), %eax
.L3
12(%ebp), %eax
%eax, -4(%ebp)
8(%ebp), %eax
%eax, 12(%ebp)
-4(%ebp), %eax
%eax, 8(%ebp)

movl
subl

12(%ebp), %eax
%eax, 8(%ebp)

cmpl
$0, 8(%ebp)
jne
.L4
movl
12(%ebp), %eax
leave
ret
.size
pgcd, .-pgcd
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section
.note.GNU-stack,"",@progbits

Dans ce paquet de ligne en assembleur, le plus facile reprer est sans doute
les lignes qui correspondent aux trois aectations des lignes 7 9 du chier
45

15
(a)
6

111111
000000
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
000000
111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
0000000
1111111
9

(b)
6

3
(c)
6

(d)
3

(e)

1111
0000
0000
1111
0000
1111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
1111111111111111
0000000000000000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
0 1
1
0 1
0 1
0
1

3.1  La recherche du pgcd de deux nombres a et b revient chercher


le plus grand carr qui permet de paver exactement le rectangle a b. En (a),
on a un rectangle 15 6 en guise d'exemple ; on voit que si on enlve le plus
grand carr possible de ce rectangle, comme dans (b), on obtient un carr 9 6
et que le plus grand carr qui permettra de paver la partie restante sera aussi
celui qui permettra de paver le rectangle de dpart. On recommence retirer le
plus grand carr possible pour obtenir un rectangle 3 6 en (c) puis 3 3 en
(d). Puisqu'on a maintenant un carr, on a obtenu la rponse. Le retrait du plus
grand carr possible correspond la soustraction de la ligne 11 de la fonction.
Figure

46

C. On les reconnat assez facilement dans l'assembleur entre les lignes 14 20.
On peut supposer que le compilateur n'a pas modi l'ordre des aectations et
puisque ces lignes dplacent, avec deux movl, ce qu'il y a dans 12(%ebp) dans
-4(%ebp), puis ce qu'il y a dans 8(%ebp) vers 12(%ebp) et nalement ce qui se
trouve dans -4(%ebp) vers 8(%ebp), on a identi l'endroit o sont stocks les
arguments a (dans 12(%ebp)) et b (dans 8(%ebp)) ainsi que la variable locale
t (dans -4(%ebp)).
On peut continuer en regardant les lignes prcdentes (11 13) de l'assembleur. Elles nous conrment dans notre supposition : il s'agit de comparer la
valeur de a et de b puis de sauter par dessus les trois aectations jusqu' l'tiquette .L3 si a est suprieur ou gal b, c'est dire si le test de la ligne 6 du
chier C est faux.
Enn les lignes 21 et 22 du chier en assembleur traduisent la soustraction
de la ligne 11 du chier C.
On a donc le corps de la boucle while du programme C dans le bloc des
lignes assembleur 11 22.
Les lignes 910 et 2425 de l'assembleur traduisent clairement la logique de
la boucle while : la ligne 9 branche directement sur le test de la ligne 21 ; la
ligne 21 compare la variable a avec 0 et si on a quelque chose de dirent, le
jne de la ligne 25 branche sur l'tiquette .L4 qui prcde le corps de la boucle.
Il ne reste donc analyser que les lignes 14 et 2931 qui contiennent des directives (dont deux que nous avons vues au chapitre prcdent .text et .globl ;
je vous laisse le soin de deviner le rle des autres), ainsi que le prologue de la
fonction aux lignes 69 et son pilogue aux lignes 2728.
Vois le code gnr avec des commentaires ajouts par mes soins :
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)

pgcd:

.L4:

.file
"pgcd.c"
.text
.globl pgcd
.type
pgcd, @function
pushl
movl
subl
jmp

%ebp
%esp, %ebp
$16, %esp
.L2

movl
cmpl
jge
movl
movl
movl
movl
movl

8(%ebp), %eax // si a >= b


12(%ebp), %eax
.L3
// pas d'affectations
12(%ebp), %eax // t = a;
%eax, -4(%ebp)
8(%ebp), %eax // a = b;
%eax, 12(%ebp)
-4(%ebp), %eax // b = t;

47

// prologue de la fonction
// va au test de la boucle

(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)

.L3:
.L2:

movl

%eax, 8(%ebp)

movl
subl

12(%ebp), %eax
%eax, 8(%ebp)

// a -= b;

// test de la boucle
cmpl
$0, 8(%ebp)
// si a != 0
jne
.L4
// recommencer
movl
12(%ebp), %eax // return b;
leave
// pilogue de la fonction
ret
.size
pgcd, .-pgcd
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section
.note.GNU-stack,"",@progbits

3.2 Prologue et pilogue des fonctions C


En plus du pointeur sur le sommet de la pile, les fonctions C utilisent un
registre qui pointe de faon permanente (pendant l'excution de la fonction) sur
un point xe dans la zone de la pile utilise par la fonction. On appelle couramment ce registre le frame pointeur (prononcer comme frme pohneteur ; je crois
qu'en franais on devrait appeler cela le pointeur sur l'enregistrement d'activation). Par dfaut, notre compilateur utilise le registre %ebp du 386 comme frame
pointer

3.2.1

Le prologue

Au dbut de la fonction, le sommet de pile pointe sur l'adresse de retour,


avant laquelle les arguments ont t empils, en commenant par le dernier ; le
frame pointer peut pointer n'importe o, mais le plus probable est qu'il contient
une adresse dans la pile. Ceci est voqu par la gure 3.2
Dans le prologue, la fonction va s'installer sur la pile. Elle commence par
sauver la valeur du registre %ebp, puis elle fait pointer %ebp sur la valeur sauvegarde (aux lignes 6 et 7), comme dans la gure 3.3.
Finalement, la fonction va rserver sur la pile l'espace ncessaire pour sauver
les registres qui ont besoin de l'tre, pour les variables locales et les expressions
temporaires. Ici, la ligne 8 de l'assembleur rserve 16 octets (je ne sais pas
pourquoi il rserve autant de place alors que seuls 4 octets sont utiliss pour
la variable t ; je souponne que c'est en esprant que le frame de la fonction
suivante commencera sur une adresse qui correspondra au dbut d'une ligne de
cache). L'tat de la pile est maintenant celui de la gure 3.4.
Il existe une instruction enter qui eectue ces trois tapes du prologue de
la fonction en une seule instruction. Je ne sais pas pourquoi le compilateur gcc
ne l'utilise pas. (Il me semble avoir lu quelque part il y longtemps qu' la suite
48

000000000
111111111
1
0
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
1111
0000
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
1111
0000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
Pile

%ebp

argument 2
argument 1

adr. de retour

%esp

Figure 3.2  A l'entre dans la fonction, la pile contient l'adresse de retour sur
le sommet de pile et dessous les arguments, empils avec le premier argument
le plus prs du sommet. Le frame pointer peut pointer n'importe o.

000000000
111111111
1
0
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
1111
0000
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
1111
0000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1

000000000
111111111
1
0
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
000000000
111111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
000
111
0
1
0
1
000
111
0
1
0
1
111
000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
Pile

Pile

%ebp

argument 2

argument 2

argument 1

argument 1

adr. de retour

adr. de retour

%ebp

ancien %ebp

%esp

ancien %ebp

%esp

(b)

(a)

3.3  Le prologue commence par empiler la valeur du frame pointer (a),


puis fait pointer le frame pointer sur la valeur qu'il vient de sauver.
Figure

49

00000000
11111111
1
0
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
0
1
11111
0000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
0
1
11111
0000
0
1
0
1
Pile

argument 2
argument 1

adr. de retour
ancien %ebp

%ebp

locales
et
temporaires

%esp

Figure 3.4  Le prologue de la fonction se termine par la rservation d'espace


sur le sommet de pile pour les variables locales et les expressions temporaires.

d'une erreur de conception, l'excution de cette instruction est plus lente que
celle des trois instructions quivalentes.)

3.2.2

L'pilogue

Lorsque la fonction a termin son travail, elle entre dans son pilogue pour
remettre la pile dans l'tat o elle l'a trouv en entrant. Dans le chier assembleur produit par le compilateur, l'pilogue correspond aux lignes 2728.
L'instruction leave de la ligne 27 est quivalente aux deux instructions
(27.1)
(27.2)

movl
popl

%ebp,%esp
%ebp

Elle dpile tout ce que la fonction a pu empiler au dessus de la sauvegarde


de l'ancien frame pointer puis dpile et rinstalle l'ancien frame pointer. Finalement, le ret de la ligne 28 dpile l'adresse de retour et rend la main la fonction
qui a appele. Ceci est montr sur la gure 3.5.

3.2.3

L'intrt du frame pointer

Le frame pointer permet d'avoir un repre xe sur la zone de la pile qu'utilise
un appel de la fonction. Il est install au prologue et reste en place jusqu'
l'pilogue. Le sommet de pile, lui, peut varier au cours de l'excution de la
fonction, mesure qu'on empile et qu'on dpile des arguments pour appeler
d'autres fonctions. Cela garantit qu'on peut trouver les arguments de la fonction
50

00000000
0111111111
10
00000000
1011111111
000000001010
1011111111
00000000
1011111111
10
00000000
11111111
101011111111
000000001010
00000000
11111111
1010
1010
10
10
1010
1010
1010
1010
10
10
1010
11111010
0000
1010
1010
1010
1010
1010
1010
111110
0000
10
Pile

Pile

argument 2

argument 2

argument 1

argument 1

adr. de retour
ancien %ebp

00000000
11111111
1
0
0
1
00000000
11111111
0
1
0
000000001
11111111
0
1
0
1
00000000
11111111
0
1
0
1
00000000
11111111
0
1
0
000000001
11111111
0
1
0
1
00000000
11111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
000
111
0
1
0
1
000
111
0
0
1
1111
000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
adr. de retour

%ebp

%ebp

ancien %ebp

%esp

locales
et
temporaires

%esp

(a)

(b)

00000000
11111111
1
0
0
1
00000000
11111111
0
1
0
000000001
11111111
0
1
0
1
00000000
11111111
0
1
0
1
11111111111
000
00000000
0
1
0
000000001
11111111
0
1
0
1
00000000
11111111
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
0
1
1111
000
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
Pile

%ebp

argument 2
argument 1

adr. de retour

%esp

(c)

3.5  L'pilogue de la fonction dmarre avec la pile comme en (a) ;


elle dpile les locales et les temporaires en recopiant le frame pointer dans le
pointeur de pile (b), dpile l'ancien frame pointer (c) puis l'adresse de retour
avec l'instruction ret (cette tape n'est pas reprsente sur la gure).
Figure

courante, ses variables locales et ses temporaires une distance qui ne varie pas
de l'endroit o pointe le frame pointer.
Surtout, le frame pointer permet de remonter dans la chane des appels de
fonction. C'est grce lui que le dbugger nous indique non seulement quelle
est la fonction courante (ce qu'il peut dcouvrir en regardant l'adresse contenue
dans %eip, le compteur ordinal), mais aussi quelle fonction a appel la fonction
courante, et quelle fonction a appel cette fonction, et ainsi de suite. La valeur
courante du frame pointer pointe juste au-dessus de l'adresse de retour, qui
permet de savoir quelle tait la fonction appelante, De plus il pointe sur la
sauvegarde du frame pointer de la fonction appelante, ce qui permet de connatre
sa propre adresse de retour (gure 3.6).
Finalement, dans les langages de programmation qui autorisent les dnitions de fonctions imbriques, le frame pointer peut tre le moyen le plus pratique de retrouver les variables locales d'une fonction enveloppante. Imaginons
le fragment de code suivant :
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)

void
foo(int n){
int a = 23;
void bar(int n){
if (n == 0)
a += 1
else
bar(n - 1);

51

Pile

frame
de
main

@ de retour dans main


frame
de

frame pointer de main

foo

@ de retour dans foo


frame
de
bar

frame pointer de foo

@ de retour dans bar


frame
de
joe

frame pointer de bar


%ebp

%esp

Figure 3.6  L'tat de la pile quand la fonction main a appel la fonction


foo qui a appel la fonction bar qui elle mme appel la fonction joe. La

sauvegarde du frame pointer par chacune des fonctions permet de remonter la


chane des appels.

52

(10)
(11)
(12)
(13)
(14)

}
...
bar(n);
...

La fonction bar modie a, une variable locale de foo qui existe donc dans la
pile dans le frame de la fonction foo. Au moment o il examine le code, le
compilateur ne peut en aucune manire savoir quelle distance dans la pile se
trouve cette variable, puisque cela dpend du nombre d'appels rcursifs de la
fonction bar, qui dpend lui-mme d'un argument inconnu au moment de la
compilation. Pour rsoudre le problme, le compilateur doit fabriquer du code
qui va remonter de frame en frame dans la pile jusqu' arriver celui de l'appel
de la fonction foo ; ce moment, la variable a se trouvera une distance xe
connue au moment de la compilation du frame pointer de la fonction foo.
On peut demander gcc de ne pas utiliser de frame pointer avec l'option
-fomit-frame-pointer. On a un registre disponible de plus pour nos variables,
mais on a aussi la garantie que le code produit sera parfaitement impossible
mettre au point.

3.3 Le code optimis


Si on compile la mme fonction pgcd avec l'option -O en plus de -S, on voit
l'assembleur qui correspond au code optimis :
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)

pgcd:

.L6:

.L3:

.file
"pgcd.c"
.text
.globl pgcd
.type
pgcd, @function
pushl
movl
movl
movl
testl
je

%ebp
%esp, %ebp
8(%ebp), %edx
12(%ebp), %ecx
%edx, %edx
.L2

cmpl
jge
movl
movl
movl

%ecx,
.L3
%ecx,
%edx,
%eax,

subl
jne

%ecx, %edx
.L6

%edx
%eax
%ecx
%edx

53

(21)
(22)
(23)
(24)
(25)
(26)
(27)

.L2:

movl
%ecx, %eax
popl
%ebp
ret
.size
pgcd, .-pgcd
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section
.note.GNU-stack,"",@progbits

On voit (aux lignes 1517) que les arguments a et b et la variable t sont


maintenant stocks dans les registres %edx, %ecx et %eax. (Gcc commet d'ailleurs
ici une maladresse : s'il plaait la variable b dans le registre %eax au lieu de %ecx,
l'instruction de la ligne 22 serait inutile.)
Puisque la soustraction a -= b de la fonction C est traduite par l'instruction
de la ligne 19, qui positionne les ags, le test de la boucle a disparu ; on se
contente de boucler la ligne 20 si le rsultat de la soustraction (la nouvelle
valeur de a) est dirent de 0. Il faut cependant prendre garde au cas o la
fonction ne fait aucun tour de boucle : c'est le rle des instructions des lignes
1011.
Dans l'pilogue, on voit que l'instruction leave a t remplace par un simple
popl %ebp, puisque le pointeur de pile pointe encore sur l'ancien frame pointer.
En compilant avec l'option -O3, on active d'autres optimisations.
Examiner le code assembleur produit et identier lesquelles.

Ex. 3.1 
Ex. 3.2 
fonction

Examiner le code produit par le compilateur quand il compile la

void
foo(){
}

if (0 == 1)
printf("Hello\n"); // jamais excut

L'appel a la fonction printf est-il encore dans le code ? Et la chane de caractre ? (Attention, le rsultat peut dpendre du niveau d'optimisation.)

Ex. 3.3 

Mme question pour

Ex. 3.4 

Mme question pour

void
foo(int x){
if (x == x + 1)
printf("Hello\n"); // jamais excut
}
void
foo(){

for(;;)

54

bar();
printf("Hello\n");

Ex. 3.5 

// jamais excut

Mme question pour

int
bar(void){
return 1;
}
void
foo(){
}

if (bar() != 1)
printf("Hello\n"); // jamais excut

3.4 L'utilisation des registres


On a dja vu, plus haut dans le chapitre, que les variables ne sont places
dans des registres que si on a compil avec l'optimisation. Ici on examine la
question de la sauvegarde des registres lors des appels de fonction.

3.4.1

Variables et expressions intermdiaires dans les registres

Une fonction sera plus rapide si elle utilise les registres pour stocker les
variables temporaires et les expressions intermdiaires, plutt que de les placer
dans la pile. En eet d'une part l'accs aux registres est plus rapide qu' la
mmoire de plusieurs ordres de grandeur et d'autre part les instructions qui
rfrencent les registres sont en gnral plus compactes (et donc plus rapides
charger depuis la mmoire ou le cache) que celles qui font rfrence la mmoire.
Sur les huit registres dits gnraux du 386, deux ont dj des rles aects
(le pointeur de pile %esp et le pointeur de frame %ebp). Il ne reste donc que six
registres disponibles.

3.4.2

La problmatique de la sauvegarde

Quand une fonction appelle une autre fonction, il est ncessaire de sauvegarder les registres qui sont utiliss par la fonction qui appelle et qui vont tre
utiliss par le fonction appele (ou par une fonction qu'elle appelle). Le problme
est que le compilateur C compile fonction par fonction (les fonctions peuvent
se trouver dans des chiers dirents et tre compiles par des appels dirents au compilateur) et ne dispose pas de toute l'information ncessaire. Il faut
55

pourtant, avec l'information partielle dont il dispose, choisir de faire sauver les
registres soit par la fonction qui appelle, soit par la fonction qui est appele.
(On appelle cela caller-save et callee-save en anglais).

Sauvegarde par la fonction appele


Si les registres sont sauvegards par la fonction appele, elle pourra le faire
dans son prologue et remettre les anciennes valeurs dans son pilogue. De plus,
on n'a besoin de sauver que les registres dont le compilateur sait qu'ils seront
utiliss par cette fonction. Le problme est qu'on risque de sauver (inutilement)
des registres que la fonction qui appelle n'utilise pas.

Sauvegarde par la fonction qui appelle


Si les registres sont sauvegards par la fonction qui appelle, on ne sauvera
que les registres eectivement utiliss avant chaque appel et on remettra les
valeurs aprs l'appel. Le problme est qu'on risque de sauver (inutilement) des
registres que la fonction appele n'utilise pas.

Des solutions
Le premier compilateur C aectait au plus trois registres (sur les huit disponibles sur le processeur PDP11, y compris le frame pointer et le pointeur de
pile), uniquement aux variables locales qui avaient t dclares avec le mot clef
register.
La solution adopte par Gcc consiste diviser les six registres disponibles
en deux groupes de trois, les uns sauver par l'appelante et les autres sauver
par l'appele.
Les registres %eax, %ecx et %edx sont utilisables par la fonction appele sans
prcaution particulire ; si elle y stocke une valeur qu'il faut conserver lors d'un
appel de fonction, le contenu du registre sera sauvegard avant l'appel et sera
rcupr aprs.
Les registres %ebx, %esi et %edi seront utiliss pour une fonction qui a
beaucoup de variables locales, mais une fonction qui les utilise sauvera leur
ancienne valeur dans son prologue et la remettra en place dans son pilogue.
On peut s'en rendre compte en compilant avec -O -S la fonction suivante :
foo(int a, int b, int c, int d, int e, int f, int g){
bar(a, b, c, d, e, f, g);
bar(a, b, c, d, e, f, g);
}

Le code produit est le suivant :


(1)

.file

"u.c"

56

(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)

.text
.globl foo
.type
foo:
pushl
movl
subl
movl
movl
movl

foo, @function
%ebp
%esp, %ebp
$40, %esp
%ebx, -12(%ebp)
%esi, -8(%ebp)
%edi, -4(%ebp)

(12)
(13)
(14)

movl
movl
movl

24(%ebp), %edi
28(%ebp), %esi
32(%ebp), %ebx

(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)

movl
movl
movl
movl
movl
movl
movl
movl
movl
movl
movl
call

%ebx, 24(%esp)
%esi, 20(%esp)
%edi, 16(%esp)
20(%ebp), %eax
%eax, 12(%esp)
16(%ebp), %eax
%eax, 8(%esp)
12(%ebp), %eax
%eax, 4(%esp)
8(%ebp), %eax
%eax, (%esp)
bar

(27)
(28)
(29)
(30)
(31)
(32)
(33)
(34)
(35)
(36)
(37)
(38)

movl
movl
movl
movl
movl
movl
movl
movl
movl
movl
movl
call

%ebx, 24(%esp)
%esi, 20(%esp)
%edi, 16(%esp)
20(%ebp), %eax
%eax, 12(%esp)
16(%ebp), %eax
%eax, 8(%esp)
12(%ebp), %eax
%eax, 4(%esp)
8(%ebp), %eax
%eax, (%esp)
bar

(39)
(40)
(41)
(42)
(43)

movl
movl
movl
movl
popl

-12(%ebp), %ebx
-8(%ebp), %esi
-4(%ebp), %edi
%ebp, %esp
%ebp

57

(44)
(45)
(46)
(47)

ret
.size
foo, .-foo
.ident "GCC: (Debian 4.3.2-1.1) 4.3.2"
.section
.note.GNU-stack,"",@progbits

Le prologue ici s'tend de la ligne 6 la ligne 11 : aprs avoir install le frame


pointer et rserv de l'espace dans la pile, le contenu des trois registres %ebx,
%esi et %edi est sauv. Ensuite les lignes 1214 rcuprent trois des arguments
et les placent dans ces registres pour acclrer leur empilage aux lignes 1517
(pour le premier appel bar) et 2729 (pour le second appel bar).
Les autres arguments sont rcuprs dans la pile ; ce n'est pas la peine d'en
stocker dans les registres %eax, %ecx et %edx puisque la fonction bar est susceptible de modier leur contenu.
Finalement, l'pilogue commence, aux lignes 3941, par rcuprer les anciennes valeurs de ces registres sauves lors du prologue.
Notons au passage que gcc rserve la pile ncessaire ds le prologue, ce qui
permet d'utiliser des dplacements simples au lieu des empilements avant l'appel
de la fonction bar et un dpilement aprs. A la ligne 8, il rserve bien les 12
octets ncessaires pour la sauvegarde des registres plus les 28 ncessaires pour
les sept arguments de bar.
La fonction foo n'a pas besoin d'avoir sept arguments pour mettre
la particularit de gcc en vidence. Quel est le nombre minimum d'arguments ?
S'il n'y a qu'un seul argument, ou est-il plac ?
On trouvera un tour d'horizon de direntes conventions d'appels de fonction
et d'appels de registres sur wikipedia la page http://en.wikipedia.org/
wiki/X86_calling_conventions (en anglais). Il semble notamment que les API
d'un systme d'exploitation assez rpandu sur PC utilisent une autre convention
qu'on peut demander gcc d'utiliser avec l'attribut __stdcall.
Dans les sections suivantes, je montre quelques exemples de ce que la connaissance intime du fonctionnement des appels de fonction sur notre processeur
permet au niveau de la programmation en C.

Ex. 3.6 

3.5 Application : le fonctionnement et le contournement de stdarg


Le chier d'inclusion <stdargs.h> permet d'crire des fonctions qui peuvent
tre utilises avec un nombre variables d'arguments la manire de printf.
Nous commenons par tudier le passage des arguments autres que des entiers,
de taille variable.

58

3.5.1

Passage de paramtres de types double

On peut forcer le compilateur passer des arguments qui ne tiennent pas


dans un int en utilisant des arguments du type double qui font 8 octets. Noter
que les floats sont convertis en doubles pour le passage en argument, comme
les char sont convertis en int. Le bout de code :
foobar(1.111111111111)

est traduit en

pushl $1072809756
pushl $1908873853
call foobar
addl $8,%esp

3.5.2

//
//
//
//

empiler
empiler
appeler
dpiler

la moiti de l'argument
l'autre moiti
la fonction
l'argument (8 octets)

Passages de paramtres de types varis

En passant des structures en arguments, on peut passer des objets dont on


peut contrler nement la taille. Ainsi, avec le code suivant :
struct {
char t[NCAR];
} x;
int toto;
foo(){
toto = sizeof x;
foobar(x);
foobar(x, x);
}

on peut faire varier la taille utile de la structure en faisant varier la constante


NCAR, connatre sa taille eective en regardant la valeur aecte la variable
toto, et observer le passage en argument avec un seul argument, et avec deux
arguments. (Je souligne encore une fois que le compilateur traduit un appel de
fonction, sans rien savoir sur le nombre et le type des arguments que la fonction
attend. Quand nous dclarons des prototypes, c'est pour demander au compilateur de nous fournir des messages d'erreurs quand nous appelons une fonction
avec des arguments qu'elle n'est pas prvue pour recevoir ; la fonction foobar
n'est pas dnie et nous n'avons pas dni son prototype, donc le compilateur
ne sait rien son sujet.
Si on place quatre octets dans la structure en compilant avec
gcc -S -DNCAR=4 file.c

on obtient l'assembleur (comment par mes soins) :

59

movl $4,toto
movl x,%eax
pushl %eax
call foobar
addl $4,%esp
movl x,%eax
pushl %eax
movl x,%eax
pushl %eax
call foobar
addl $8,%esp

//
//
//
//
//
//
//
//
//

toto = sizeof x
sizeof x = 4
foobar(x)
x %eax
%eax pile
appeler
dpiler
foobar(x, x)
2me argument

// 1er argument

On voit que si la structure utilise quatre octets, le passage d'argument se fait


comme pour un entier : les quatre octets sont empils froidement dans la pile.
Avec une structure un seul octet, on a :
movl $1,toto
addl $-2,%esp
movb x,%al
pushw %ax
call foobar
addl $4,%esp

//
//
//
//
//
//

toto = sizeof x
sizeof x = 1
foobar(x)
deux octets inutiliss dans la pile
x %al
%ax pile

// dpiler 4 octets
(Je rappelle que les registres %al et %ax sont des versions de %eax quand on n'ac-

cde qu' un ou deux octets). La chose importante est de voir que la structure sur
un seul octet a utilis quatre octets dans la pile, comme l'indique l'instruction
nale qui dpile les arguments. La mme chose se produit pour les structures
de deux et trois octets.
D'une manire gnrale, les structures passes en argument sont empiles
dans un espace dont la taille est arrondie au multiple de quatre suprieur ou
gal la taille de la structure ; par exemple, pour une structure de treize octets,
on a :

60

movl $13,toto
addl
movl
movl
movl
movl
movl
movl
movl
movb
movb
call
addl

$-16,%esp
$x,%eax
(%eax),%edx
%edx,(%esp)
4(%eax),%edx
%edx,4(%esp)
8(%eax),%edx
%edx,8(%esp)
12(%eax),%al
%al,12(%esp)
foobar
$16,%esp

//
//
//
//
//
//

toto = sizeof x
sizeof x = 13
foobar(x)
rserver 16 octets sur la pile
%eax = &x
octets 0 3 de x sur la pile

// idem pour les octets 4 7


// idem pour les octets 8 11
// idem pour l'octet 12
// librer les 16 octets

Quand la structure devient vraiment grande, le compilateur utilise les instructions spciales pour recopier ses octets vers la pile ; ainsi, avec une structure
de 367 octets.
movl $367,toto
addl $-368,%esp
movl %esp,%edi
movl $x,%esi
cld
movl $91,%ecx
rep movsl
movsw
movsb
call foobar
addl $368,%esp

//
//
//
//
//
//
//
//
//
//
//

toto = sizeof x
sizeof x = 367
foobar(x)
rserver 368 octets.
%edi = pile = adresse destination
%esi = x = adresse source
il va falloir incrmenter %esi et %edi
nombre de mots de 4 octets copier ?
copier les 91 4 octets
copier 2 octets
copier 1 octet

// librer les 368 octets

En rsum
Lors d'un appel de fonction, le compilateur empile les arguments dans une
zone mmoire de la pile dont la taille est arrondie au multiple de quatre suprieur
ou gal, avec le premier argument sur le sommet de la pile.

3.5.3

La rception des arguments par la fonction appele

Comme on a vu plus haut, la fonction appele ne connat de la pile que le


pointeur de pile, dont elle suppose qu'il pointe sur une adresse de retour et au
dessus de laquelle se trouvent les arguments avec le premier sur le dessus.
Les deux premires instructions de toute fonction fabrique par le compilateur sont :
61

pushl %ebp
// sauve le registre %ebp
movl %esp,%ebp // %ebp pointe sur l'ancien %ebp
On a appel %ebp le pointeur de frame : ce registre contient l'adresse qui se trouve

juste au dessus de l'adresse de retour et sa valeur ne bougera pas jusqu'au retour


de la fonction. Puisque la pile croit vers les adresses basses, l'adresse de retour
se trouve donc en 4(%ebp) ; la zone o les arguments ont t empils par la
fonction qui appelle commencent en 8(%ebp).

Utilisation de stdarg
Une fonction qui peut recevoir un nombre variable d'arguments, comme la
fonction printf, peut utiliser les macros stdarg pour les rcuprer. On trouve
dans la page de manuel stdarg(1) un exemple d'utilisation de ces macros :
void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *p, *s;
va_start(ap, fmt);
while (*fmt)
switch(*fmt++) {
case 's':
/* string */
s = va_arg(ap, char *);
printf("string %s\n", s);
break;
case 'd':
/* int */
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case 'c':
/* char */
/* Note: char is promoted to int. */
c = va_arg(ap, int);
printf("char %c\n", c);
break;
}
va_end(ap);

}
va_list dclare une variable du type appropri ; va_start initialise la variable pour qu'elle dsigne les arguments qui se trouvent aprs l'argument fmt ;
va_arg sert la fois rcuprer l'argument indiqu par ap, puis dplacer ap
sur l'argument suivant ; va_end indique qu'on a termin d'utiliser ap.

62

3.5.4

Fonctionnement de stdarg

Vu la manire dont sont passs les arguments, la variable initialise par

va_start doit faire pointer ap juste aprs l'argument fmt qu'on lui passe en
argument. La macro va_next doit renvoyer la valeur de ap, tout en faisant
avancer ap jusqu'au prochain multiple de quatre suprieur ou gal la taille du
type qu'on lui indique. Je ne vois pas ce que doit faire la macro va_end.

3.5.5

Contenu du chier stdarg.h

Le chier stdarg.h contient, une fois qu'on a retir le copyright et les

#ifdef :

(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)

typedef char *va_list;


#define __va_size(type) \
(sizeof(type) + sizeof(int) - 1) / sizeof(int)) \
* sizeof(int))
#define va_start(ap, last) \
((ap) = (va_list)&(last) + __va_size(last))
#define va_arg(ap, type) \
(*(type *)((ap) += __va_size(type), \
(ap) - __va_size(type)))
#define va_end(ap)

La macro va_end (dnie ligne 14) ne fait rien, comme prvu.


Le type va_list (dni ligne 1) est un pointeur sur des caractres ; ceci
signie que l'arithmtique sur une variable de type va_list fonctionnera correctement : si on ajoute 1 au contenu de la variable, elle pointera sur l'octet
suivant. Ceci suggre qu'on pourrait sans dommage dnir le type va_list
comme un int.
La macro va_size (dnie lignes 35) prend un type en argument. En
supposant que sizeof type vaille n, et que sizeof int vaille 4, elle calcule
(n+41)
4 ; comme il s'agit ici d'une division entire, ceci calcule, comme on
4
s'y attendait, le plus petit multiple de quatre suprieur ou gal au nombre d'octets utiliss par le type ; il s'agit du nombre d'octets utiliss par le compilateur
pour passer une variable d'un type donn en argument dans la pile. Noter que
l'argument type n'est utilis qu'avec sizeof : va_size fonctionne donc indiremment avec un type ou une variable comme argument, ce que n'indiquait pas
la documentation.
La macro va_start (lignes 78) place dans la variable indique par ap
l'adresse de l'argument indiqu par last plus la taille occupe par last dans la
pile, comme indiqu par va_size.
63

La macro va_arg (lignes 1012) modie la valeur de la variable indique


par ap pour remonter dans la pile par dessus l'espace utilis par une variable
du type type ; ensuite, pour renvoyer l'ancienne valeur de ap, elle soustrait du
contenu de ap cette mme valeur qu'elle vient d'ajouter.

3.5.6

A quoi sert stdarg ?

On a vu que les macros de stdarg sont une interface complique vers quelque
chose de simple. Ainsi, une fonction qui fait la somme d'une liste d'entiers termine par 0 qu'on lui passe en argument peut se coder, avec stdarg, comme :
1
2
3
4
5
6
7

/ corr1e . c
/

int
somme ( int premier , . . . ) {
va_list ap ;
int courant ;
int somme ;

8
9
10
11
12
13
14
15
16

somme = 0 ;
va_start ( ap , premier ) ;
for ( courant = premier ; courant != 0 ;
courant = va_arg ( ap , int ) )
somme += courant ;
va_end ( ap ) ;
return somme ;

On peut aussi la dnir, de faon plus simple, comme :


1
2
3
4
5
6

/ corr1f . c
/

int
somme ( int premier , . . . ) {
int p = &premier ;
int i , somme ;

7
8
9
10
11

for ( i = somme = 0 ; p [ i ] != 0 ; i++)


somme += p [ i ] ;
return somme ;

Le problme est ici celui de la portabilit : rien ne nous garantit que le


passage d'argument fonctionne de la mme manire sur tous les processeurs.
Cette fonction ne fonctionne plus quand la pile croit vers les adresses hautes, ni
quand les entiers sont passs dans la pile l'intrieur de huit octets, ni quand
les donnes sur huit octets doivent se trouver sur une adresse multiple de huit,
64

ni quand tout ou partie des arguments est pass via des registres. Par exemple,
cette fonction ne fonctionne pas sur les processeurs Intel 64 bits.
(pas de corrig) Le code prsent ici a t crit et test sous le
systme d'exploitation FreeBSD. Refaire le mme travail sous Linux.

Ex. 3.7 
Ex. 3.8 

(pas de corrig) Le code prsent ici ne fonctionne que sur un


processeur Intel 32 bits. Refaire le mme travail sur un processeur 64 bits.

3.6 Application : le fonctionnement de setjmp et


longjmp en C
Dans la librairie C, on trouve deux fonctions setjmp et longjmp qui permettent de faire des sauts entre les fonctions. Le principe est que la fonction
setjmp sauve le contexte dans lequel on l'a appele et plus tard, quand on
appelle longjmp depuis une autre fonction, on revient juste aprs l'appel de
setjmp.

3.6.1

Schma d'utilisation

Ces fonctions sont principalement utilises pour les traitements d'erreurs


dans les processus organiss autour d'une boucle. Le schma usuel d'utilisation
est le suivant :
1
2

# include <setjmp . h>


jmp_buf redemarrage ;
...

1
2
3
4
5
6

void
boucler ( void ){
if ( setjmp ( redemarrage ) == 0)
printf (" On demarre \ n " ) ;
else
printf (" On redemarre apres une erreur \ n " ) ;

7
8
9
10
11

for ( ; ; )
traiter ( ) ;
printf (" Bye \ n " ) ;

La ligne 7 dclare une variable du type jmp_buf : il s'agit d'un peu d'espace
dans lequel stocker le pointeur de pile et le frame pointer. Avant de dmarrer
la boucle de traitement, la ligne 29 appelle la fonction setjmp pour sauver
l'environnement au dmarrage de la boucle ; la fonction renvoie 0 lorsqu'on l'a
appele.
65

Ensuite, aux lignes 3435, on rentre dans la boucle ; dans notre exemple
elle appelle rptivement la fonction traiter. Quand la fonction traiter ou
une fonction appele par elle dtecte une erreur, elle peut appeler la fonction
longjmp avec redemarrage comme argument. Le retour de la fonction longjmp
ne se fait pas la suite du programme, mais comme si on revenait de l'appel de
la fonction setjmp la ligne 29.
Pour avoir un exemple qui fonctionne, je dnis une fonction traiter qui
lit une ligne et appelle la fonction erreur quand la ligne est vide. La fonction
erreur appelle longjmp pour redmarrer.
1
2
3
4
5
6
7
8
9

void
erreur ( char str ){
fprintf ( stderr , " erreur : %s \ n " , str ) ;
longjmp ( redemarrage , 1 ) ;

void
traiter ( void ){
char ligne [ 1 0 2 4 ] ;

10

while ( fgets ( ligne , sizeof ligne , stdin ) != NULL ){


if ( ligne [ 0 ] == 0 | | ligne [ 1 ] == 0) // il peut y avoir \ n dans ligne [ 0 ]
erreur (" ligne vide " ) ;
printf (" La ligne lue contient %s " , ligne ) ;

11
12
13
14
15
16

3.6.2

Fonctionnement intime de setjmplongjmp

Quand elle est appele, la fonction setjmp photographie l'tat de la pile


en recopiant dans le jmp_buf le frame pointer et l'adresse de retour avant de
renvoyer 0. Quand on appelle la fonction longjmp, elle remplace son frame
pointer et son adresse de retour par ce qu'elle trouve dans le jmp_buf (voir
gure 3.7.)
En fait, il reste une dicult, puisqu'il faut aussi s'assurer que la pile aprs
le retour est revenue dans l'tat o elle se trouvait au moment de l'appel la
fonction traiter. Cela implique une ligne de code en assembleur.
La maitrise du mcanisme permet de comprendre pourquoi, comme l'indique
la page de manuel de setjmp,  le contexte de pile sera invalide si la fonction qui
appelle setjmp retourne . Quand la fonction qui a appel setjmp eectue son
return, la pile qu'elle employait est libre et un longjmp sur l'environnement
sauv renverra sur une zone de pile inutilise ou bien utilise pour autre chose.

66

Pile

Pile

frame
de
boucler

frame pointer sauve

frame
de
boucler

frame
de
traiter

@ de retour de lappel
de setjmp

frame
de
setjmp

Pile

frame
de
boucler

frame pointer sauve

@ de retour de lappel
de traiter

frame
de
traiter

frame pointer sauve

frame pointer sauve

@ de retour de lappel
de traiter

frame pointer sauve

frame pointer sauve


%ebp

%esp

frame
de
erreur

frame
de
longjmp

@ de retour de lappel
de erreur

frame
de
erreur

frame pointer sauve

@ de retour de lappel
de longjmp

frame
de
longjmp

frame pointer sauve

(a)

@ de retour de lappel
de erreur

frame pointer sauve

@ de retour de lappel
de setjmp

frame pointer sauve

%ebp

%ebp

%esp

(b)

%esp

(c)

3.7  La fonction setjmp sauve son adresse de retour et son frame


pointer. Plus tard, quand la fonctionerreur appelle longjmp, celui-ci remplace
le frame-pointer sauv et son adresse de retour par ceux sauvs par l'appel de
setjmp.

Figure

3.6.3

Rapport avec les exceptions d'autres langages

Certains langages volus disposent de constructions d'chappement spciques pour traiter les erreurs et les exceptions ; la plus courante est celle qu'on
rencontre par exemple en Java et en C++, qui se prsente sous la forme d'un
try ... catch et d'un throw, organiss de la faon suivante
try {
ici du code qui peut provoquer une exception avec throw
}
catch (exception) {
traitement de l'exception
}

Ce code fonctionne avec des manipulations de piles similaires celles qu'on


rencontre dans setjmplongjmp. Il est quivalent au C
jmp_buf etat;
if (setjmp(etat) == 0){
le code qui peut provoquer une exception ; ici il
faut utiliser longjmp(etat) au lieu de throw
} else {
traitement de l'exception
}

67

3.7 L'ordre des arguments dans la bibliothque


d'entres-sorties standard
Si vous aussi vous avez du mal vous souvenir de l'ordre des arguments
dans les appels de fonctions, voici un truc mnmotechnique pour ceux de la
stdio : Toutes les fonctions de la stdio ont le descripteur de chier comme
dernier argument.
Comme dans les rgles de grammaires, cette rgle a une exception : elle
s'applique toutes les fonctions de la stdio sauf celles qui ont un nombre variable
d'arguments comme fprintf ou fscanf ; pour celles-ci le descripteur de chier est
le premier argument.
Quand on a compris le mcanisme de passage des arguments, la raison de
cette exception est assez limpide. Puisque ces fonctions ont un nombre d'arguments variable et que ceux-ci sont de types varis, elle doivent parcourir le
format pour savoir o les trouver dans la pile. Si le descripteur de chier tait
pass en dernier, comme dans les autres fonctions de la stdio, fprintf aurait
besoin de parcourir le format deux fois : une premire fois pour trouver le descripteur de chier o crire puis une seconde fois an d'crire eectivement les
caractres du format et les autres arguments formatts.
En passant le descripteur de chier dans le premier argument, les fonctions
comme fprintf peuvent le rcuprer immdiatement (il est sur le sommet de
la pile), rcuprer le format (il est juste en dessous), puis parcourir le format en
descendant dans la liste des arguments suivant leur type indiqu dans le format.
C'est bien sur la mme chose pour des fonctions comme snprintf (qui formatte dans la mmoire au lieu d'crire dans un chier) ou comme fscanf qui
lit au lieu d'crire. Pour toutes les autres, le descripteur de chier est le dernier
argument. 1

3.8 Manipuler l'adresse de retour


1
2
3
4
5
6
7
8
9
10

# include <stdio . h>


void
foo ( int x ){
printf (" On rentre dans la fonction foo , x vaut %d \ n " , x ) ;

void
bar ( int x ){
void p = &x 1 ; // adresse de retour

1. En revanche, je n'ai pas de moyen de me souvenir de quels sont les deuxime et troisime
arguments des fonctions fread et fwrite ; c'est une assez bonne raison pour les viter et appeler
directement read et write.
68

11
12
13
14
15
16
17
18
19
20
21

p = ( void ) foo ;
printf (" On rentre dans la fonction bar , x vaut %d \ n " , x ) ;

int
main ( ) {
printf (" main appelle bar ( 1 2 ) \ n " ) ;
bar ( 1 2 ) ;
printf (" fin normale du main \ n " ) ;
return 0 ;

3.9 Si vous avez un systme 64 bits


Avec un systme 64 bits, les exemples donns dans ce chapitre ne fonctionnent pas. Beaucoup d'ordinateurs rcents peuvent supporter un systme 32
ou 64 bits, la dirence tant principalement le nombre d'octets pour stocker
une adresse. Quand on installe une distribution Linux, on est souvent conduit
choisir entre une version 32 et 64 bits.
Pour tudier la matire du chapitre, deux solutions : trouver une machine
(ventuellement installer une machine virtuelle) 32 bits, ou bien traduire les
exemples du chapitre (et du prcdent).
Les dirences les plus importantes mon avis sont :
 Les adresses occupent bien sur huit octets. Les int et les long n'en occupent
que quatre.
 La version 64 bits des registres porte un nom qui commence par r . Par
exemple, le registre %eax s'appelle %rax quand on utilise ses 64 bits ; le
pointeur de pile s'appelle %rsp et le pointeur de frame %rbp.
 Il y huit registres gnraux supplmentaires, qui portent le nom %r8 %r15
(en version 64 bits) et %r8d %r15d en version 32 bits. (Ces registres sont
disponibles aussi sur les processeurs rcents qu'on utilise en mode 32 bits
mais il faudrait prvenir le compilateur qu'on ne compte pas faire tourner
le programme sur un vieux processeur 386 pour qu'il puisse les utiliser
sans danger.)
 Le passage des six premiers arguments se fait par les registres : dans
l'ordre %edi, %esi, %edx, %ecx, %r8d puis %r9d. Les arguments partir
du septime vont dans la pile, comme dans le compilateur pour le 386.
 L'appelant passe des informations supplmentaires la fonction appele
via le registre %eax ; le plus simple est de le mettre zro avant tous les
appels de fonction ; a fonctionne pour (presque) tous les exemples du
cours.
 La valeur renvoye est toujours place dans %eax (ou dans %rax si c'est
une adresse).
 Les prologues et les pilogues de fonctions sont pleins de directives qui
commencent par .cfi. Vous pouvez les ignorer pour les exemples du cha69

pitre.
 Quand la dernire chose que fait une fonction est d'en appeler une autre,
l'optimiseur remplace l'appel par un saut aprs avoir mis la pile dans
l'tat adquat : le retour de la fonction appele reviendra en ralit dans
la fonction qui a appel la fonction courante. (Formul autrement : gcc
reconnat et traite correctement la rcursion terminale.)

70

Chapitre 4
L'analyse lexicale

L'analyseur lexical est charg de dcouper en mots le ux de caractres que


contient le chier source. Ce petit chapitre introduit seulement le sujet. Pour
trouver une prsentation de Lex, fonde sur les expressions rgulires, voir le
dernier chapitre du support.

4.1 Analyse lexicale, analyse syntaxique


Ce paragraphe voque le problme que pose la dlimitation entre analyse
lexicale et analyse syntaxique, puis montre l'interaction normale des deux analyseurs dans un compilateur ordinaire.

4.1.1

Analyse lexicale versus analyse syntaxique

La limite entre analyse lexicale et analyse syntaxique est relativement oue.


Comme on le verra, les analyseurs syntaxiques sont plus puissants que les analyseurs lexicaux et on peut leur coner, en plus de leur travail principal, le travail
de l'analyseur lexical. C'est souvent le cas dans les dnitions formelles de langages qui utilisent une prsentation BNF (comme Backus Naur Form ; il s'agit
d'une manire de dcrire les langages dont nous reparlerons dans le prochain
chapitre). Cependant, distinguer le travail (assez simple) de l'analyseur lexical
de celui (plus complexe) de l'analyseur syntaxique permet de circonscrire la complexit inhrente aux analyseurs syntaxiques et nous conserverons la distinction
entre les deux.
En pratique, on considre que ce que l'analyseur lexical ne sait pas faire ressort de l'analyse syntaxique ; on verra vers la n du chapitre (sous la forme de
calembours C) des exemples avec un compilateur rel de formes de programmes
valides qui ne sont pas compils correctement cause de dfaillances de l'analyseur lexical.
71

4.1.2

Analyse lexicale et analyse syntaxique

Dans l'architecture usuelle des programmes, c'est l'analyseur syntaxique qui


dirige le travail. Il s'eorce de construire l'arbre syntaxique en utilisant les mots
que lui a dj renvoy l'analyseur lexical. Quand il est prt recevoir le mot
suivant, alors il appelle la fonction qui fait le travail de l'analyseur lexical. Cette
fonction lit le mot suivant et le renvoie l'analyseur syntaxique.

4.2 En vrac
Ici, j'voque quelques questions relatives aux analyseurs lexicaux et j'apporte
quelques rponses. Les indications de cette section sont mise en uvre dans
l'exemple qui conclue le chapitre.

4.2.1

Renvoyer un type et une valeur

Le rle de l'analyseur lexical est d'identier le mot suivant. Il doit donc


retourner le type du prochain mot.
Quand ce mot est par exemple un mot clef comme while ou else, le type
est susant pour dcrire compltement le mot. En revanche, quand il s'agit
de quelque chose comme un identicateur ou une constante, le type indiquera
de quelle sorte d'objet il s'agit, mais l'analyseur lexical devra aussi renvoyer la
valeur de l'objet : pour un identicateur, au moins la chane de caractres qui
contient son nom ; pour une constante, sa valeur.
L'usage veut qu'on nomme la fonction principale de l'analyseur lexical yylex,
que cette fonction renvoie le type du mot sous la forme d'un entier et qu'elle
place la valeur du mot dans une variable globale nomme yylval (lval comme
lexical value). Quand les mots reconnus par l'analyseur lexical peuvent avoir
des valeurs de types dirents (par exemple un nombre entier ou un nombre
ottant ou une chane de caractres), alors yylval sera dni comme une union
en C. L'analyseur syntaxique utilisera le type renvoy pour savoir quel champs
de l'union il doit utiliser.
Cet usage est en fait destin permettre l'intgration de l'analyseur lexical
avec les analyseurs syntaxiques fabriqus par Yacc et Bison, prsents dans les
chapitres suivants.

4.2.2

Le caractre qui suit le mot, ungetc

Il est ncessaire l'analyseur lexical de lire tous les caractres qui composent
un mot. Dans la plupart des cas, il lui faut aussi lire le premier caractre qui
suit le mot an d'tre en mesure d'en dtecter la n.
Par exemple, quand il voit un chire au dbut d'un mot, un analyseur lexical
pour le langage C doit traiter la lecture d'un nombre, qui peut se composer de
72

plusieurs chires ; il va donc rentrer dans une boucle qui lira les caractres de
ce nombre jusqu'au dernier. Pour dterminer quel est le dernier caractre du
nombre, l'analyseur lexical devra lire un caractre de plus : celui qui ne sera pas
un chire indiquera que le prcdent tait le dernier.
Aprs avoir lu le caractre qui suit le mot, l'analyseur lexical doit le remettre
en place pour que l'appel suivant trouve ce caractre. (Il ne s'agit pas toujours
d'un espace ; en C par exemple, les caractres 0+1 ne contiennent aucun espace
mais composent trois mots distincts.)

La mauvaise manire

On peut installer une (ne) couche logicielle entre


l'analyseur lexical et les fonctions qui lisent les caractres. Par exemple on pourra
crire deux fonctions lirecar qui lit un caractre et annulerlire qui annule
la lecture du dernier caractre :
static int dernierlu;
static int annule;
/* annuler -- annuler la dernire lecture de caractre */
void
annulerlire(void){
annule = 1;
}
/* lirecar -- lire le prochain caractre */
int
lirecar(void){
if (annul == 1){
annule = 0;
return dernierlu;
}
return dernierlu = getchar();
}

La fonction lirecar lit un caractre et le renvoie, sauf si la variable annule lui


indique qu'elle doit renvoyer de nouveau le dernier caractre lu, qui a t sauv
dans la variable dernierlu. (La variable dernierlu doit tre du type int et
non du type char parce que getchar peut aussi renvoyer EOF pour indiquer la
n du chier ; or EOF n'est pas le code d'un caractre.)

La bonne manire

La librairie d'entres sorties standard contient dj un


mcanisme pour eectuer ce travail, sous la forme de la fonction ungetc ; c'est
cette fonction qu'il convient d'utiliser (si on utilise la librairie d'entres sorties
standard) ; la seule dirence avec le code prcdent est qu'on ne peut pas
annuler la lecture du code EOF (puisque ce n'est pas un code de caractre). Dans
la fonction yylex, le code de lecture d'un entier dcimal pourra ressembler :
73

int
yylex(void){
int c;
redo:
c = getchar();

// lire un caractre :

if (c == EOF)
return EOF;

// - fin de fichier

if (isspace(c))
goto redo;

// - espaces : sauter

if (c >= '1' && c <= '9'){ // - constante dcimale


int res;

for(res = c - '0'; isdigit(c = getchar());


res = res * 10 + c - '0')
;
if (c == EOF){
erreur("EOF juste aprs l'entier %d (ignor)\n", res);
return EOF;
}
// dfaire la lecture du caractre qui suit
ungetc(c, stdin);
yylval = res;
// yylval = valeur du nombre
return INT;
// signaler qu'on a lu un entier

if (c == '0'){
...
}
...

4.2.3

// - constante octale ou hexadcimale

Les commentaires

L'analyseur lexical est en gnral charg aussi de retirer les commentaires


du programme source. Une question, laquelle rpond en gnral la description
du langage, est de savoir si les commentaires sur une ligne incluent ou pas la n
de ligne. A ma connaissance, le langage TEX est le seul pour lequel c'est le cas.
Cela permet que
foo% Ce commentaire inclue la fin de la ligne
bar

74

soit lu comme le mot foobar.


Notez qu'en C, les commentaires ne sont pas imbriqus (un /* l'intrieur
d'un commentaire est trait comme des caractres ordinaires). Cela complique
un peu les choses quand on souhaite commenter un bloc de code (mais on peut
utiliser # if 0 ou # ifdef NOTDEF), et permet une devinette : qu'imprime le
programme suivant ?
1
2
3
4

# include <stdio . h>


int
main ( ) {

5
6

a = 123;
p = &a ;
printf ("% d \ n " , a / p ) ;
printf (" La ligne precedente ne contient pas %d \ n " ,
1 / multiplication inutile ? / 3 2 1 ) ;
return 0 ;

7
8
9
10
11
12
13

C.)

int a , p ;

(Cette devinette devrait probablement aller dans la section Calembours en

4.2.4

Quelques dicults de l'analyse lexicale

L'analyse lexicale est un travail plutt facile mais elle contient quelques
piges, dont certains sont voqus dans les paragraphes suivants.

Les constantes en virgule ottante en C


Il y a de nombreuses manire d'crire les nombres en virgule ottante en C,
comme dans la plupart des langages de programmation.
 Une srie de chires qui contient un point, comme dans 000.000.
 Il peut ne pas y avoir de chires gauche ou droite du point dcimal
(comme dans 000. ou .000. (Peut-il n'y avoir de chires ni gauche ni
droite, d'aprs votre compilateur C favori ?)
 Les chires signicatifs peuvent tre suivis d'une indication d'exposant
( notation scientique ) ; l'indication d'exposant commence par un e
(majuscule ou minuscule) et est suivie par un nombre entier, positif ou
ngatif, comme dans 0.0e0, 0.0e-0 ou 0.0e+0.
 Quand il y a une indication d'exposant, alors le point dcimal dans la
mantisse n'est pas ncessaire, comme dans 0e0.
 Avant ceci, il peut y avoir ou pas une indication du signe du nombre avec
+ ou -.
75

 Aprs ceci, il peut y avoir un l ou un L pour indiquer que le nombre


ottant est du type double, ou bien f ou F pour le forcer au type float.
Cette liste de variantes de la notation des nombres en virgule ottante est incomplte (voir exercice).
Trouver, dans une norme du langage C, la description prcise de
toutes les formes que peut prendre une constante en virgule ottante. (L'exercice
demande bien de trouver cette description dans une norme, pas dans un ouvrage
sur le langage C.)

Ex. 4.1 

Identicateurs, mots clefs


Les identicateurs (en premire approximation les noms de variables et de
fonctions) ont la mme structure que la plupart des mots rservs comme while,
for ou return. Plutt que de traiter spcialement ces mots clefs, il est souvent
plus simple et plus ecace de les lire comme des identicateurs ; une fois que le
mot est lu, on vrie s'il est prsent dans la table des mots clefs et dans ce cas
on le traite spcialement.
Le fragment de code suivant donne la structure gnrale de ce traitement :
typedef struct Keyword Keyword;
struct Keyword {
char * string;
int retvalue;
};

// le mot clef
// la valeur renvoyer

/*
La table des mots clefs
(Dans la vraie vie, il vaudrait mieux construire une hash table)
*/
Keywork kwtable[] = {
{ "while", WHILE },
{ "for", FOR },
{ "return", RETURN },
...
{ 0 }
};
int
yylex(void){
int c;
redo:
c = getchar();
...

// lire un caractre :

76

if (isalpha(c) || c == '_'){ // identificateur, type ou mot clef


Keyword * p;
char name[MaxNameLen];
int i;
for(name[0] = c, i = 1; isalnum(c = getchar()) || c == '_'; i++)
if (i == MaxNameLen - 1)
erreur("identificateur trop long (%d caractres max)",
MaxNameLen);
else
name[i] = c;
name[i] = 0;
if (c == EOF)
erreur("EOF juste aprs %s\n", name);
p = lookupkw(string, kwtable);
if (p != 0)
return p->retvalue;
else {
yylval.ident = lookupident(string);
return IDENT;
}

}
...

Le fragment de code ci-dessus montre l'appel de la fonction lookupkw qui recherche l'identicateur lu dans la table des mots clefs, mais pas sa dnition.
La fonction lookupident regarde si le mot lu est prsent dans la table des
identicateurs et sinon l'y rajoute ; le fragment de code ne contient pas non plus
sa dnition.

Identicateurs et types dnis


On souhaite coner l'analyseur lexical la distinction entre les noms de types
(comme int, long ou double) et les identicateurs qui dsignent des noms de
variables ou de fonction.
Dans le langage C, la chose est peu prs impossible cause du mcanisme
du typedef qui permet de renommer un nom de type avec une chane choisie
par l'utilisateur. De plus, ce nom de type n'est pas un mot rserv et peut tre
utilis pour autre chose dans le mme programme. Par exemple, la dclaration
suivante de jill est valide
typedef int foo, * bar, (*joe)(foo, bar);

77

joe jill;

Pour cette raison, c'est l'analyseur syntaxique que la plupart des compilateurs C conent la distinction entre identicateurs et types dnis avec typedef.
On est ici la limite entre lexical et syntaxique voque au dbut du chapitre.

Calembours en C et leur traitement par gcc


Dans les programmes rels, les programmeurs prennent soin de placer des
espaces entre les mots de leurs programmes de manire pouvoir les relire.
Cependant un analyseur lexical ne peut pas compter sur ces espaces pour lui
faciliter le travail, parce que chacun place les espaces d'une faon dirente.
(D'ailleurs, chaque programmeur C, y compris moi, est fermement convaincu
que sa manire de disposer les espaces est la seule correcte).
Les expressions C qui suivent contiennent les constantes 1 et -1,
deux variables entires a et b, l'oprateur arithmtique binaire - (comme dans
a - b), l'oprateur unaire - (comme dans - a) et l'oprateur --, prx ou
postx.
Si a vaut 1 et que b vaut 2, quelle est la valeur de l'expression et la valeur de a
et b aprs son excution ?
Si on supprime les espaces de ces expressions, lesquelles sont encore correctes
d'aprs le compilateur ?
Pour les expressions correctes sans les espaces, quelles sera leur valeur et celle
de a et b aprs leur excution ?
Expliquer
valeur a aprs b aprs ok sans espace valeur a aprs
a - 1
o
a - -1
o
a - - 1
o
a - - - 1
o
a - - - - - b
o
a -- - -- b
o
(supplment)
a -- + -- b
o
a ++ + ++ b
o

Ex. 4.2 

4.3 Un exemple lmentaire mais raliste d'analyseur lexical


Dans cette section, je prsente un analyseur lexical simple. Pour qu'il soit
utilis, je prsente aussi un analyseur syntaxique lmentaire. Le tout donne un
petit programme qui peut servir additionner deux nombres entiers.
78

b aprs

4.3.1

Le langage

Le programme lit des lignes, qui devront contenir chacune une expression
arithmtique lmentaire eectuer. Un exemple simple de session pourra tre :
$ a.out
? 234 + 432
= 666
? 234+432
= 666
? 1+1+1
erreur de syntaxe
? 1
erreur de syntaxe
? ^D
Ciao
$

4.3.2

#
#
#
#
#
#

lancement du programme
une expression arithmtique simple
affichage du rsultat
la mme sans espace
c'est pareil
expression trop complexe

# expression trop simple


# fin de fichier

L'analyseur syntaxique

Le rle de l'analyseur syntaxique est jou par la fonction yyparse. Elle appelle la fonction yylex pour lire chaque mot : d'abord le premier nombre, puis
l'oprateur, puis le second nombre, puis la n de ligne. Elle eectue l'opration
et ache le rsultat.
En cas d'erreur, la fonction erreur met en uvre la reprise avec setjmp/longjmp
prsente la n du chapitre prcdent : elle ache le message d'erreur puis
revient la fonction yyparse dans le setjmp prsent au dbut.

4.3.3

L'analyseur lexical

L'analyseur lexical est ralis par la fonction yylex : la version prsente ici
lit l'expression ligne par ligne.
On trouvera dans db-elem.c une version qui lit l'expression caractre par
caractre, mais elle prsente des dciences dans la rcupration d'erreur. Quand
la ligne est trop courte, elle considre que la ligne suivante fait partie de l'erreur.
Quand une ligne ne se termine pas par un newline, le message d'erreur est
incohrent. (Une ligne peut ne pas se terminer par newline si elle est trop longue
pour rentrer dans le buer qu'on a pass fgets ou bien si on l'a envoye avec
Controle-D depuis le clavier.)

Lecture des caractres


La fonction lit une ligne complte dans le tableau de caractres ligne et
conserve ensuite un index iligne qui indique quelle est la position courante
79

dans la ligne. Ce qui se trouve entre ligne[0] et ligne[iligne-1] a dja t


trait par l'analyseur lexical ; le reste est analyser.
Au dbut de l'appel, on vrie qu'il reste des caractres traiter. Si ce n'est
pas le cas, la fonction lit une nouvelle ligne (avec la fonction fgets). Dans le cas
o il ne reste rien lire, elle renvoie 0 pour l'indiquer l'analyseur syntaxique.
Ensuite, la fonction saute tous les espaces avec la boucle de la ligne 50. Pour
faciliter la lecture de la fonction, le caractre courant est plac dans la variable
c (ligne 52).
Le caractre # sert introduire des commentaires. Dans ce cas, la boucle qui
suit (ligne 55 et seq.) conduit ignorer tous les caractres jusqu' la n de la
ligne.
Le test de la ligne 60 traite tous les caractres qui forment un mot eux
tous seuls : dans notre cas, seulement le +, la n de ligne et la n de chane.
Le test de la ligne 63 traite le cas o c'est un chire qui a t lu. Cela annonce
un nombre. L'analyseur lexical doit renvoyer la contante Nombre (dnie comme
la valeur 256) et placer la valeur du nombre dans la variable globale yylval. Le
problme est que la fonction doit lire tous les chires du nombre pour dterminer
sa valeur, ce qui peut se faire de diverses faons. mon avis elles prsentent
toutes des inconvnients. Le code prsente direntes mthodes, entre lesquelles
on peut choisir avec des directives de compilation conditionnelles. J'ai plac les
mthodes dans l'ordre dans lequel elles me semblent prfrables.

Conversion explicite par le programme

la ligne 65 (et suivantes), le


programme eectue lui-mme la conversion entre les codes des caractres qui
composent le chire et la valeur du chire en reprsentation interne de l'ordinateur. Pour obtenir la reprsentation interne de la valeur du chire, il retire le
code ascii de 0 de celui du chire.

Utilisation de la fonction strtol

Le code des lignes 70 et suivantes utilse


la fonction strtol pour eectuer la conversion que les lignes prcdentes effectuaient explicitement. On utilise le second argument pour rcuprer (dans
p) l'adresse du caractre qui suit le nombre et on l'utilise pour mettre jour
iligne, pour qu'il contienne l'index du premier caractre qui suit le nombre.

Utilisation de sscanf

C'est avec sscanf que le code de la ligne 75 et suivantes


eectue la conversion. On vrie avec l'assertion de la ligne 77 que la conversion
s'est bien eectue (il faut toujours vrier la valeur renvoye par scanf et ses
variantes). Comme la fonction n'indique pas le nombre de caractres occups
par le nombre, la boucle de la ligne 78 est ncessaire pour faire avancer l'index
du caractre courant aprs le nombre.

Utilisation de atoi

La fonction atoi fait le mme travail que strtol, mais


seulement en base 10 et sans donner d'indication du nombre caractres utiliss
80

par le nombre.
Modier le programme pour pouvoir aussi eectuer les autres oprations arithmtiques usuelles (soustraction, multiplication, division et modulo).

Ex. 4.3 
Ex. 4.4 

Ajouter la possibilit de dnir des variables simples, avec des


noms d'un seul caractre alphabtique, et d'utiliser la valeur de ces variables
dans les expressions, comme dans l'exemple suivant :
$ a.out
? 12 * 12
= 144
? a = 234
= 234
? a + 0
= 234
? 0 + a
= 234
? a + a
= 468
? a + 0
= 234
? b = a
= 234
? b + 0
= 234
? c + 0
= 0
? ^D
Ciao

# operation simple
# affectation de variable
# consultation de variable
# dans l'autre sens
# des deux cots
# la valeur n'a pas chang
# copie de valeur
# consultation de b
# variable indfinie
# valeur par dfaut

Indication : si on n'accepte que les variables dont le nom ne fait qu'un caractre,
on peut utiliser un tableau 26 entres. L'analyseur lexical doit maintenant
renvoyer une nouvelle constante (Variable par exemple) quand il rencontre
une variable et placer dans yylval quelque chose qui indique de quelle variable
il s'agit, par exemple une valeur entre 0 et 25 qui indique la place du nom de
la variable dans l'alphabet. On peut conserver la valeur des variables dans un
simple tableau de valeur 26 entres.

Ex. 4.5 

Modier le programme pour que les expressions soient reprsentes par des nombres en virgule ottante, au lieu d'entiers. La variable globale
yylval doit maintenant tre une union pour distinguer le cas o on a lu une
variable (la valeur du mot est un entier) et celle o il s'agit d'une nombre (la
valeur du mot est un nomre en virgule ottante).
(Indication : pour le lecture des nombres en virgule ottante, le plus facile
mon avis est d'utiliser strtod).
On pourra tester l'analyseur lexical par exemple avec :
81

$ a.out
? a = 0
# juste des chiffres
= 0
? a = 0.
# des chiffres et la virgule
= 0
? a = .0
# la virgule et des chiffres
= 0
? a = .
# scanf n'en veut pas
nombre flottant mal form
? a = 0e0
# exposant simple
= 0
? a = 0e-0 # exposant avec un signe = 0
? a = 0e+0 # le mme avec un signe +
= 0
? a = 0.0e0 # la virgule et l'exposant
= 0

Ex. 4.6 

Modier le programme prcdent pour accepter les noms de variables composs de plusieurs caractres.
(Indication : il faut maintenant que l'analyseur lexical lise tous les caractres
qui compopsent le nom de la variable ; l'union yylval doit maintenant pouvoir
recevoir une chane de caractres comme argument. La structure de donne qui
associe une variable avec une valeur ne peut plus tre un simple tableau de
valeur ; le corrig utilise un tableau de structures qui associent le nom d'une
variable et sa valeur ; un programme srieux utiliserait sans doute une table de
hash.)

Ex. 4.7 

(Trop dicile, ne pas faire) Modier le programme pour pouvoir


entrer des nombres ngatifs.

Ex. 4.8 

(Hors sujet, pas de correction) Modier le proramme pour pouvoir


placer dans une variable le rsultat d'une opration, comme dans a = b + c. (Il
s'agit d'un travail eectuer avec l'analyseur syntaxique, dont parle le prochain
chapitre.)

82

Chapitre 5
L'analyse syntaxique :
prsentation

L'analyse syntaxique prend les mots que produit l'analyse lexicale et les
regroupe jusqu' former des phrases du langage en appliquant des rgles de
grammaire. Dans le cas des compilateurs, ce regroupement se fait dans la plupart
des cas sous la forme d'un arbre syntaxique.
L'analyse syntaxique est le gros morceau de ce cours, et elle est prsente
dans trois chapitres : celui-ci montre deux analyseurs syntaxiques crits en C. Le
chapitre suivant montre des analyseurs syntaxiques qui utilisent les outils Yacc
et Bison. Le troisime dtaille le fonctionnement interne des parseurs construits
par Yacc et Bison.

5.1 Grammaires, langages, arbres syntaxiques, ambiguts


Les grammaires que nous tudions dans le cours de compilation ne ressemblent pas formidablement celle de notre langue maternelle que nous avons
tudie pendant nos premires annes d'cole. Elles sont constitues d'un ensemble de rgles qui dcrivent compltement la structure de toutes les phrases
d'un langage.

5.1.1

Les rgles de grammaires

Les rgles de nos grammaires sont formes avec une partie gauche et une
partie droite, pour dcrire une des constructions du langage. A gauche, un nud
interne d'un arbre ; droite, la liste des enfants de cet arbre. Par exemple :
expression : NOMBRE

83

indique qu'un nombre lui tout seul (comme reconnu par l'analyseur lexical)
forme une expression arithmtique correcte. Les rgles peuvent tres rcursives,
comme dans :
expression : expression '+' expression

avec laquelle on sait qu'une expression arithmtique suivie d'un + suivie d'une
autre expression arithmtique forme encore une expression arithmtique correcte. (La rgle est rcursive parce que le symbole de gauche apparat aussi
dans sa partie droite.)

5.1.2

Les symboles terminaux et non terminaux

On appelle symboles terminaux ceux qui apparaissent droite dans certaines


rgles de grammaire mais jamais gauche ; ce sont les types de mot que renvoie la
fonction d'analyse lexicale. Inversement, quand un symbole apparat gauche de
certaines rgles, cela signie qu'il est compos de mots et on l'appelle un symbole
non terminal. On abrge souvent ces noms en terminaux et non terminaux, sans
prciser qu'il s'agit d'un symbole. L'usage est de donner aux terminaux des
noms en majuscules et des noms en minuscules aux non terminaux.

5.1.3

Les arbres syntaxiques

On reprsente souvent le rsultat du travail de l'analyseur syntaxique sous


la forme d'un arbre syntaxique : par exemple, considrons la grammaire :
x : A y B ;
y : C x ;
y : D ;

Elle se compose de trois rgles, pour un langage dont les seuls mots sont A, B ,
C et D. A partir de ces quatre mots, elle permet de construire une innit de
phrases ADB , ACADBB , ACACADBBB , etc. On peut dcrire les phrases du
langage avec  Un D, prcd d'un seul A puis CA un nombre quelconque de
fois et suivi d'autant de B qu'il y a de A .
La premire rgle dcrit comment est construit un x, la deuxime et la troisime montrent deux manires direntes de construire un y .
L'analyseur syntaxique lit une phrase et applique les rgles sur les mots
jusqu' trouver une squence d'application des rgles qui rende compte de la
structure de la phrase, comme dans la gure 5.1 (ou bien jusqu' avoir dtect
une erreur de syntaxe).
La construction de l'arbre syntaxique par l'analyseur s'appelle une drivation. Quand on construit un nud de l'arbre syntaxique de type x partir des
mots y et z , on dit qu'on rduit y et z en x. Le symbole qui doit se trouver la
racine de l'arbre s'appelle le symbole de dpart.
84

(c)
(a)
x:AyB
y:Cx
y:D

y
(b)
A

5.1  A partir de la grammaire simple (a), deux arbres syntaxiques qui


rendent compte de la structure des deux phrases les plus simples du langage A
D B en (b) et A C A D B B en (c).

Figure

5.1.4

Les grammaires ambigus, l'associativit et la prcdence

Quand il est possible de driver plusieurs arbres syntaxiques dirents


partir d'une mme phrase, on dit que la grammaire est ambigu. Les ambiguts
dans les grammaires proviennent principalement de deux sources : soit on peut
driver un mot ou un groupe de mots en non terminaux dirents (en utilisant
donc des rgles direntes), soit on peut construire une arbre avec une forme
dirente en utilisant les mmes rgles.
Pour un exemple de la premire sorte, considrons la grammaire lmentaire
pour un langage qui ne comprend que la phrase A :
x
x
y
z

:
:
:
:

y
z
A
A

Sur A, la phrase unique de la grammaire, on peut driver deux arbres syntaxiques


dirents en appliquant des rgles direntes : celui o la racine x est constitue
d'un y qui lui mme est fait d'un A et celui o la racine x est constitue d'un
z lui aussi fait d'un A. Il n'y a pas de manire de dterminer en examinant la
grammaire laquelle des deux interprtations doit tre prfre.

L'associativit
Pour un exemple de la seconde sorte, regardons une grammaire pour un
langage dont les phrases sont constitues d'un nombre quelconque de A :
85

(b)

(c)

(a)
A

x:A
x:xx

(d1)

(d2)

Figure 5.2  A partir de la grammaire simple (a), on ne peut driver qu'un


seul arbre syntaxique pour la phrase A (en b) ou A A (en c) ; en revanche, il y
a deux arbres syntaxiques dirents possibles pour la phrase A A A (en d1 et
d2) : la grammaire est ambigu.

x : A
x : x x

Pour la phrase A, il n'y a un qu'un seul arbre : celui o le A est rduit en x par
la premire rgle. Pour la phrase A A, chaque A est rduit en x (par la premire
rgle) puis les deux x sont rduits en A par la seconde rgle. En revanche pour
la phrase A A A, il existe deux drivations possibles (gure 5.2). Chacune de ces
drivations emploie les mmes rgles mais les applique dans un ordre dirent.
On peut rencontrer ce genre de construction dans une grammaire des expressions arithmtiques ; si on se limite aux nombres et + :
expr : NBRE
expr : expr '+' expr

De la mme manire que la prcdente, cette grammaire permet de driver


deux arbres dirents pour une phrase qui contient trois nombres, comme dans
la gure 5.3. Le problme ici est celui de l'associativit de l'oprateur + : l'arbre
de gauche de la gure correspond un + associatif gauche, celui de droite
un + associatif droite.
86

expr

expr

expr

expr

expr

expr

expr

expr

expr

expr

NBRE

NBRE

NBRE

NBRE

NBRE

NBRE

Figure 5.3  Deux arbres syntaxiques dirents pour la mme expression arithmtiques 4 + 2 + 1, drivables partir de notre grammaire. Chacun des arbres
rend compte d'une interprtation dirente de la phrase, mais avec les deux
arbres l'expression rsultat vaut 7 grce aux proprits du +.

Si on remplace le + par un dans notre grammaire, alors les deux arbres


correspondront deux interprtations de l'expression arithmtique qui donnent
des rsultats dirents, comme dans la gure 5.4.

La prcdence
Avec l'interprtation usuelle des expressions arithmtiques, nous savons qu'une
expression comme 1 + 2 3 s'interprte de manire ce que sa valeur soit 7.
Une grammaire possible serait
expr : NBRE
expr : expr + expr
expr : expr * expr

mais partir de cette grammaire, il est possible de driver plusieurs arbres


syntaxiques dirents pour une expression arithmtique qui contient la fois
des + et des (gure 5.5). Comme dans la section prcdente, il s'agit des
mmes rgles de grammaires et la question porte sur la manire de les employer.

Rcritures de grammaires
Il est toujours possible de rcrire une grammaire pour en lever les ambiguts
dues aux questions de prcdence et d'associativit.
Quand une rgle prsente une ambigut due une question d'associativit
comme dans
87

expr

expr

expr

expr

expr

expr

expr

expr

expr

expr

NBRE

NBRE

NBRE

NBRE

NBRE

NBRE

5.4  Les mmes arbres syntaxiques que dans la gure 5.3 : du fait
des proprits du , l'expression arithmtique drive comme dans l'arbre de
gauche vaut 1 alors que drive comme dans l'arbre de droite elle vaut 3.

Figure

expr

expr

expr

expr

expr

expr

expr

expr

expr

expr

NBRE

NBRE

NBRE

NBRE

NBRE

NBRE

5.5  partir de la phrase 1 + 2 3, on peut driver deux arbres


syntaxiques dirents. Celui de gauche donne une une valeur de 9, celui de
droite une valeur (habituelle) de 7. La question est celle de la prcdence des
oprateurs + et .
Figure

88

x : A
x : x x

on peut la rcrire comme


x : A
x : x A

ou comme
x : A
x : A x

pour forcer l'associativit gauche ou droite.


Quand une rgle prsente une ambigut due la prcdence deux oprateurs
comme dans
x : A
x : x B x
x : x C x

on peut lever l'ambigut en introduisant un type de nud supplmentaire,


comme dans
x : x1
x : x B x1
x1 : A
x1 : x1 C x1

Toutes les occurrences de l'oprateur C seront rduites dans des applications de


la dernire rgle de grammaire avant de rduire les occurrences de l'oprateur
B par la deuxime rgle.
C'est pour xer la prcdence des oprateurs d'addition et de multiplication
qu'on voit souvent la grammaire des expressions arithmtiques crite comme :
expr : terme
terme : facteur
terme : terme + facteur
facteur : NBRE
facteur : facteur * NBRE
facteur : ( expr )

Ex. 5.1 
Ex. 5.2 

Que vaut l'expression 4 2 1 en C ?


Soit la grammaire :

p : A B
p : A B p

89

1. Driver l'arbre syntaxique de ABABAB.


2. Dcrire d'une phrase (en franais !) le langage dcrit par cette grammaire.
3. La grammaire est-elle ambigu ?

Ex. 5.3 

Mmes questions que pour l'exercice prcdent, avec la grammaire

Ex. 5.4 

Soit la grammaire :

p : /* rien */
p : A B p
p : A B
p : A p B

1. Gnrer partir de cette grammaire une phrase de 2 mots, de 4 mots,


de 6 mots, de 8 mots.
2. Dcrire d'une phrase le langage dcrit par cette grammaire.

Ex. 5.5 

Soit la grammaire :

p : /* rien */
p : A p B p

Gnrer partir de cette grammaire toutes les phrases de 2 mots, de 4 mots,


de 6 mots. On peut remplacer le mot A par une parenthse ouvrante et le mot
B par une parenthse fermante. Qu'obtient-on alors ?

Ex. 5.6 

Driver un arbre syntaxique pour chacune des phrases construites


l'exercice prcdent avec la grammaire :
p : /* rien */
p : p A p B

La grammaire est-elle ambige (peut-on driver plusieurs arbres syntaxiques


dirents partir de la mme phrase) ?

Ex. 5.7 

Mmes questions pour la grammaire :

p : /* rien */
p : A p B
p : p p

5.1.5

BNF, EBNF

Je n'ai pas envie de rdiger cette section. L'article EBNF sur wikipedia.org
est trs bien.

90

5.2 Les analyseurs prcdence d'oprateurs


Dans cette section, je montre la structure d'un petit analyseur qui utilise
la prcdence des oprateurs arithmtiques pour reconnatre la structure d'une
expression.
Nous commencerons par examiner une version lmentaire de l'analyseur qui
ne traite pas les parenthses, puis une version un peu plus avance dans laquelle
elles sont prises en compte.
On trouve dans le dragon book un squelette d'analyseur prcdence d'oprateur plus volu qui n'utilise qu'une seule pile.

5.2.1

Un analyseur prcdence d'oprateurs lmentaire

Cet analyseur ne traite que des expressions arithmtiques composes de


nombres entiers et des quatre oprations (addition, soustraction, division et
multiplication).

L'analyseur lexical
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# include <string . h>


enum {
Num = 1 ,
Executer = 1, Empiler = 1 ,
MaxStack = 1024 ,

// renvoye par l ' analyseur lexical


// comparaison de precedence
// taille maximum des piles

char operator = "+ /";


int yylval ;

// la liste des operateurs


// la valeur du lexeme

};

/ yylex

lit le prochain mot sur stdin ,


place sa valeur dans yylval ,
renvoie son type ,
proteste pour les caracteres invalides

/
int
yylex ( void ){
int c ;
int t ;

redo :
while ( isspace ( c = getchar ( ) ) )
if ( c == '\ n ' )

91

return 0 ;

25
26

if ( c == EOF )
return 0 ;

27
28
29

if ( isdigit ( c ) ) {
ungetc ( c , stdin ) ;
t = scanf ("% d " , &yylval ) ;
assert ( t == 1 ) ;
return Num ;

30
31
32
33
34

35
36

if ( strchr ( operator , c ) == 0){


fprintf ( stderr , " Caractere %c (\\0% o ) inattendu \ n " , c , c ) ;
goto redo ;

37
38
39

40
41
42

return c ;

L'analyseur lexical est une simple fonction qui lit des caractres sur l'entre
standard et renvoie le type du prochain mot.
Il traite la n de ligne comme le marqueur de la n de phrase et renvoie 0.
Il ignore tous les autres espaces (lignes 3237).
Un chire annonce un nombre ; il remet le chire dans les caractres lire,
lit la valeur du nombre avec scanf dans la variable globale yylval et renvoie le
type avec la constante Num.
Pour tous les autres caractres, il vrie avec strchr sa prsence dans la
liste des oprateurs et renvoie le code du caractre en guise de type (ici, les
oprateurs n'ont pas besoin d'avoir de valeur).

Le cur de l'analyseur
53
54
55

int operande [ MaxStack ] ;


int operateur [ MaxStack ] ;
int noperande , noperateur ;

...
110
111
112
113
114
115
116
117
118

int mot ;
noperateur = noperande = 0 ;
do {
mot = yylex ( ) ;
if ( mot == Num ){
assert ( noperande < MaxStack ) ;
operande [ noperande ++] = yylval ;

92

119

} else {
while ( noperateur > 0 && preccmp ( operateur [ noperateur 1 ] , mot ) < 0)
executer ( operateur [ noperateur ] ) ;
assert ( noperateur < MaxStack ) ;
operateur [ noperateur ++] = mot ;
}
} while ( mot != 0 ) ;

120
121
122
123
124
125
126
127
128
129
130
131
132

if ( noperateur != 1 | | noperande != 1 | | operateur [ 0 ] != 0)


fprintf ( stderr , " Erreur de syntaxe \ n " ) ;
else
printf ("% d \ n " , operande [ 0 ] ) ;

L'analyseur utilise deux piles : l'une pour les oprateurs et l'autre pour
les oprandes, dnies aux lignes 5355. (noperande et noperateur sont les
pointeurs de ces piles ; ce sont les index des premiers emplacements libres.)
Le principe gnral de l'analyseur est d'empiler les oprandes sur leur pile
(lignes 117119) ; pour les oprateurs, il compare leur prcdence (on dit aussi
leur priorit) avec celle de celui qui se trouve sur le sommet de la pile, avec
la fonction preccmp prsente plus loin ; tant que cette du sommet de pile est
suprieure, on le dpile et on l'excute ; nalement, on empile l'oprateur (lignes
122125).
La boucle s'arrte quand on a trait l'indicateur de n de phrase. Si l'expression arithmtique tait bien forme, alors il ne reste qu'un oprande (qui
reprsente la valeur de l'expression) sur la pile des oprandes et le marqueur de
n d'expression sur la pile des oprateurs.

La comparaison des prcdences


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

/ preccmp prec . de l ' operateur gauche prec . de l ' operateur droit /


int
preccmp ( int gop , int dop ){
assert ( gop != 0 ) ;
if ( dop == 0)
// EOF : executer ce qui reste .
return Executer ;
if ( gop == dop )
return Executer ;

// le meme : executer

if ( gop == '+ ' | | gop == ' '){ // + ou


if ( dop == '+ ' | | dop == ' ')
return Executer ; // puis + ou : executer
else
return Empiler ; // puis ou / : empiler

93

72
73
74
75

return 1; // dans tous les autres cas , executer

La comparaison des prcdences se fait avec une fonction preccmp, qui renvoie une valeur Executer ou Empiler. On l'utilise pour comparer la prcdence
de l'oprateur au sommet de la pile avec celle du nouvel oprateur. Quand la
prcdence du nouvel oprateur est infrieure celle qui se trouve au sommet
de la pile, il faudra excuter celui qui est au sommet de la pile avant d'empiler
le nouveau. l'inverse, quand elle est plus faible, il faudra empiler le nouvel
oprateur par dessus l'autre.
Pour cet exemple lmentaire, on aurait pu attribuer un nombre chaque
oprateur en guise de niveau de prcdence et comparer les nombres, mais cela
aurait compliqu les choses pour l'analyseur plus volu que nous verrons ensuite.

L'excution des oprateurs

L'excution des oprateurs est cone la fonction executer. En fait cette


excution des oprations ne ressort pas rellement de l'analyse syntaxique, mais
de la smantique des oprateurs.
Dans notre exemple simple, la fonction se contente de dpiler les oprandes,
d'eectuer l'opration et d'empiler le rsultat mais il est important de comprendre que ce faisant la fonction construit implicitement un arbre syntaxique.
Elle utilise la variable intermdiaire t car une expression comme
operande[noperande++] = operande[--noperande] + operande[--noperande]

n'a pas de rsultat bien dni en C : le compilateur peut faire eectuer les ++
et les -- dans un ordre imprvisible. L'utilisation de la variable intermdiaire t
nous permet de le forcer les placer dans l'ordre qui convient.
77
78
79
80
81
82
83
84
85
86
87
88
89
90

void
executer ( int op ){
int t ;
switch ( op ){
default :
fprintf ( stderr , " Operateur impossible , code %c (\\0% o )\ n " , op , op ) ;
return ;
case ' + ' :
t = operande [ noperande ] ;
t += operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' ':

94

t = operande [ noperande ] ;
t = operande [ noperande ] t ;
operande [ noperande ++] = t ;
return ;
case ' ' :
t = operande [ noperande ] ;
t = operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' / ' :
t = operande [ noperande ] ;
t = operande [ noperande ] / t ;
operande [ noperande ++] = t ;
return ;

91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

Ex. 5.8 
Ex. 5.9 

Qu'est qui change si on remplace la ligne 65 par return Empiler; ?

(trop facile pour mriter un corrig) Modier le programme pour


accepter l'oprateur modulo ('%'), avec la prcdence qu'il possde dans le
langage C.

Ex. 5.10 

(un peu plus dlicat) Modier le programme pour qu'il accepte


l'oprateur d'exponentiation ^ ; attention, cet oprateur a une prcdence plus
forte que les autres et est associatif droite.

Ex. 5.11 

Remplacer la fonction executer par quelque chose qui permet


d'imprimer l'expression lue sous forme polonaise postxe. Par exemple, quand
elle lit 2 * 3 + 4 * 5, elle crit 2 3 * 4 5 * +.

5.2.2

Un analyseur prcdence d'oprateurs moins lmentaire

J'tends ici l'analyseur pour lui faire traiter les expressions parenthses.

L'analyseur lexical
La modication de l'analyseur lexical est lmentaire : il sut d'ajouter les
parenthses dans la liste des oprateurs en remplaant la ligne 18 par
18

char * operator = "+-*/()";

95

// la liste des oprateurs

Le cur de l'analyseur
Il n'y a pas de modication faire au coeur de l'analyseur. C'est l le principal intrt de l'analyse prcdence d'oprateurs.

La comparaison des prcdences


Pour traiter les parenthses avec la prcdence, il faut :
 Quand le nouvel oprateur est une parenthse ouvrante, l'empiler systmatiquement.
 Pour tous les oprateurs usuels, on les empile par dessus une parenthse
ouvrante.
 Quand le nouvel oprateur est une parenthse fermante, excuter systmatiquement tous les oprateurs usuels et empiler la fermante par dessus
sa parenthse ouvrante.
 Pour tous les oprateurs, quand le sommet de la pile oprateur est une
parenthse fermante, il faut l'excuter ; l'excution consistera dpiler la
parenthse ouvrante associe.
On peut obtenir ce rsultat en modiant la fonction preccmp en ajoutant
les lignes suivantes :
(65.1)
(65.2)
(65.3)
(65.4)
(65.5)
(65.6)
(65.7)
(65.8)
(65.9)
(65.10)

if (gop == ')')
// toujours executer la parenthese fermante
return Executer;
if (dop == ')'){
// avec une nouvelle fermante :
if (gop == '(')
return Empiler; //
l'empiler sur son ouvrante
else
return Executer; //
et executer sur tous les autres.
}
if (dop == '(') // toujours empiler les nouvelles ouvrantes
return Empiler;

On peut noter que la relation de prcdence n'est pas une relation d'ordre
total : par exemple celle de '+' est plus forte que celle de ')' quand le '+' apparait
avant et plus faible quand il apparait aprs la fermante.

L'excution
(121.1)
(121.2)
(121.3)
(121.4)
(121.5)
(121.6)
(121.7)
(121.8)

case '(':
fprintf(stderr, "Ouvrante sans fermante\n");
return;
case ')':
if (noperateur == 0){
fprintf(stderr, "Fermante sans ouvrante\n);
return;
}

96

(121.9)
(121.10)
(121.11)
(121.12)
(121.13)
(121.14)

t = operateur[--noperateur];
if (t != '('){
fprintf(stderr, "Cas impossible avec la parenthese fermante\n");
return;
}
return;

Les lignes 121.1121.3 ne seront excutes que quand il restera une ouvrante
dans la pile la n de l'expression (puisque tous les autres oprateurs sont
empils par dessus, sauf le marqueur de n d'expression et la fermante, et que
l'excution de la fermante supprime l'ouvrante de la pile). La prsence d'une
ouvrante dans la pile indique donc que l'expression ne contient pas susamment
de fermantes.
Pour executer une fermante, on la la retire simplement de la pile avec
l'ouvrante correspondante ( la ligne 121.9).

5.2.3

Des problmes avec les analyseurs prcdence d'oprateurs

Avec les analyseurs prcdence d'oprateurs, c'est dicile de direncier le


mme oprateur sous sa forme unaire et sa forme binaire (comme * ou - en C)
au niveau de l'analyse syntaxique, ou les oprateurs unaires prxs ou postxs
comme ++ et --. Le moins compliqu est de coner la distinction entre les deux
versions de l'oprateur l'analyseur lexical (un oprateur qui apparat au dbut
d'une expression ou aprs un autre oprateur est un oprateur unaire prx),
mais cela reste malais.
La dtection d'erreur est complexe avec ces analyseurs ; par exemple mon
analyseur traite sans erreur une expression comme 1 2 3 + * + 4 et annonce
qu'elle vaut 11.

97

Chapitre 6
L'analyse syntaxique :
utilisation de Yacc

Ce chapitre traite d'un outil important d'Unix : Yacc. Le mot Yacc est un
acronyme pour Yet Another Compiler Compiler (en franais : encore un compilateur de compilateur). Il s'agit en fait d'un programme qui fabrique un analyseur
syntaxique partir d'une description de la grammaire qu'on lui fournit.
Aprs un bref historique et une description de comment l'utiliser, je montre
Yacc avec des exemples. Le chapitre suivant dcrit en dtail la manire dont il
analyse la grammaire pour produire l'analyseur.

6.1 Yacc et Bison, historique


Yacc est un outil qui date du milieu des annes 1970 ; il a t ralis par S.
pour le compilateur C qu'il crivait l'poque, qui s'appelait pcc,
comme Portable C Compiler.
Yacc est un outil important : il ne permet pas rellement de compiler un
compilateur (c'est dire de produire un compilateur partir d'une description
des langages source et cible) ; en revanche, il simplie considrablement la production d'un analyseur syntaxique correct. mon avis, c'est grce lui (et
l'existence de pcc qu'il a permis) que le langage C est rest singulirement libre
de variantes d'implmentations, sans comit de normalisation, pendant plus de
quinze ans (de sa conception jusqu' la n des annes 1980).
Le projet Gnu (qui comprend les commandes gcc, gdb et emacs) a rcrit
un programme compatible avec Yacc qu'il a nomm Bison ; je parle de Yacc
en particulier mais presque tous les points s'appliquent galement Bison ; j'ai
tent d'indiquer toutes les dirences signicatives entre les deux programmes ;
quand je ne mentionne pas de dirence, c'est que Yacc et Bison fonctionnent
de la mme manire.
C. Johnson

98

foo.y

y.tab.c
yacc

compilateur

executable

autres sources

6.1  Yacc prend une grammaire et ses actions dans le chier foo.y ; il
produit un chier C y.tab.c (ou foo.tab.c pour Bison) qui contient une fonction C yyparse qui implmente un analyseur syntaxique pour cette grammaire.

Figure

6.2 Le fonctionnement de Yacc


Le principe est le suivant : dans un chier (dont le nom se termine en gnral
par .y), on dcrit une grammaire et des actions eectuer lorsque l'analyseur
syntaxique a dtermin qu'il fallait appliquer une des rgles de la grammaire.
A partir de ce chier, Yacc produit un chier C qui contient essentiellement
une fonction yyparse, crite en C par Yacc, qui implmente un analyseur syntaxique pour cette grammaire. On intgre ce chier aux sources du programme
(voir gure 6.1).
Comme dans les exemples vus plus haut, la fonction yyparse appelle une
fonction yylex, qu'on doit crire, pour servir d'analyseur lexical. Il faut galement lui fournir une fonction yyerror qui sera appele en cas d'erreur (dans la
plupart des cas, ce sera cause d'une erreur de syntaxe et yyerror sera appele
avec la chane "Syntax error" en argument ; en se donnant beaucoup de mal,
on peut aussi russir provoquer un dbordement de pile).

6.3 Un exemple lmentaire


1
2
3
4
5
6
7
8
9

/ elem . y

Un exemple elementaire d ' analyseur syntaxique pour un langage stupide .


/
%term motA
%term motB

%{
# define YYDEBUG 1
int yydebug ;

10

99

11
12
13
14
15
16

int yylex ( void ) ;


int yyerror ( char ) ;

%}

%%

phrase : troisa

17

| troisb

18
19

20
21
22

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

troisb : motB motB motB

;
%%
# include <stdio . h>
# include <ctype . h>
# include <stdlib . h>

int
yyerror ( char str ){
fprintf ( stderr , "%s \ n " , str ) ;
return 0 ;

int
yylex ( void ){
int c ;

41

while ( isspace ( c = getchar ( ) ) )


if ( c == '\ n ' )
return 0 ;
if ( c == EOF )
return 0 ;
if ( c == ' A ' )
return motA ;
if ( c == ' B ' )
return motB ;

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

{ printf (" J ' ai trouve la phrase B B B \ n " ) ; }

troisa : motA motA motA

23
24

{ printf (" J ' ai trouve la phrase A A A \ n " ) ; }

fprintf ( stderr , " caractere imprevu %c (\\0% o )\ n " , c , c ) ;


exit ( 1 ) ;

int

100

57
58
59
60
61

main ( ) {
printf ("? " ) ;
yyparse ( ) ;
return 0 ;

Cet exemple lmentaire prsente un analyseur syntaxique pour un langage stupide qui se compose en tout et pour tour de deux phrases : A A A ou B B B. Il
est tout entier regroup dans le chier ec-elem.y.

6.3.1

Structure d'un chier pour Yacc

Le chier est dcoup en trois parties par des lignes qui ne contiennent que %%
(lignes 15 et 27). Au dbut (lignes 114 dans l'exemple) on place des dclarations
pour Yacc. La grammaire proprement dite avec les actions eectuer quand on
applique une rgle se trouve au milieu (lignes 1626). Aprs le second %% on peut
placer ce qu'on veut comme code C : Yacc ne le traitera pas et se contentera de
le recopier dans le chier rsultat.

6.3.2

La partie dclaration

La partie dclaration de notre exemple simple contient deux choses : d'une


part la dclaration des symboles terminaux que peut renvoyer la fonction yylex
(ici motA et motB aux lignes 45) avec le mot clef %term. D'autre part un peu
de code C que je souhaite voir apparatre avant l'analyseur syntaxique dans le
chier rsultat, encadr par deux lignes qui contiennent respectivement %{ et
%} (lignes 7 et 13).
Dans ce code C, il y a le prototype des deux fonctions utilises par l'analyseur syntaxique mais que je dois dnir : la fonction yylex qui joue le rle
de l'analyseur lexical et la fonction yyerror qui sera appele par l'analyseur
syntaxique en cas d'erreur.
J'ai galement, la ligne 8, dni la constante YYDEBUG pour que l'analyseur
contienne du code de mise au point. Ce code aidera comprendre ce qui se
passe en achant des messages lors de la drivation de l'arbre quand la variable
yydebug contiendra une valeur dirente de 0. (Dit autrement : si YYDEBUG n'est
pas dni, il n'y a pas de code de mise au point ; si YYDEBUG est dni mais que
yydebug contient 0, il n'y a rien d'imprim ; si YYDEBUG est dni et que yydebug
vaut 1, on aura des messages pour toutes les actions sur la grammaire) Sauf si
on a des contraintes de taille mmoire trs strictes, il faut toujours dnir la
constante YYDEBUG pour aider mettre la grammaire au point quand le besoin
s'en fait sentir.

6.3.3

La partie code C

On place ce qu'on veut dans la troisime partie (ici de la ligne 28 jusqu' la


n). J'y ai mis la fonction yyerror, la fonction yylex que l'analyseur appelle
101

pour lire l'un aprs l'autre les mots de la phrase traiter, ainsi qu'une fonction
main qui imprime un prompteur puis appelle l'analyseur via le nom de fonction
yyparse.

6.3.4

La partie grammaire et actions

Entre les lignes 16 et 26 se trouvent les rgles de grammaire ainsi que du


code excuter quand la rgle est applique. Le seul intrt de cette grammaire
est d'tre simple,.

6.3.5

Utilisation

Compiler faire tourner.


$ bison ec-elem.y
$ gcc -g -Wall ec-elem.tab.c
$ a.out
? A A A
J'ai trouv la phrase A A A
$ a.out
? B B B
J'ai trouv la phrase B B B
$ a.out
? A B A
syntax error

Pour faire tourner avec yydebug 1 (et voir les messages de mise au point
de la grammaire), lancer gdb pour examiner le programme :
$ gdb -q a.out
(gdb)

Mettre un point d'arrt au dbut du programme.


(gdb) b main
Breakpoint 1 at 0x401416: file ec-elem.y, line 58.

Lancer le programme qui s'arrte au dbut de main.


(gdb) run
Starting program: /home/jm/cours/compil/tex/src/a.out
Breakpoint 1, main () at ec-elem.y:58
58
printf("? ");

102

Mettre 1 dans yydebug.


(gdb) p yydebug=1
$1 = 1

Continuer.
(gdb) c
Continuing.
Starting parse
Entering state 0
Reading a token: ?

Le processus s'arrte pour lire la phrase. On tape A A A.


Reading a token: ? A A A
Next token is token motA ()
Shifting token motA ()
Entering state 1
Reading a token: Next token is token motA ()
Shifting token motA ()
Entering state 6
Reading a token: Next token is token motA ()
Shifting token motA ()
Entering state 9
Reducing stack by rule 3 (line 22):
$1 = token motA ()
$2 = token motA ()
$3 = token motA ()
-> $$ = nterm troisa ()
Stack now 0
Entering state 4
Reducing stack by rule 1 (line 16):
$1 = nterm troisa ()
J'ai trouv la phrase A A A
-> $$ = nterm phrase ()
Stack now 0
Entering state 3
Reading a token: Now at end of input.
Stack now 0 3
Cleanup: popping nterm phrase ()
Program exited normally.

Mme opration en tapant B B A :


103

(gdb) run
Starting program: /home/jm/cours/compil/tex/src/a.out
Breakpoint 1, main () at ec-elem.y:58
58
printf("? ");
(gdb) p yydebug = 1
$2 = 1
(gdb) c
Continuing.
Starting parse
Entering state 0
Reading a token: ? B B A
Next token is token motB ()
Shifting token motB ()
Entering state 2
Reading a token: Next token is token motB ()
Shifting token motB ()
Entering state 7
Reading a token: Next token is token motA ()
syntax error
Error: popping token motB ()
Stack now 0 2
Error: popping token motB ()
Stack now 0
Cleanup: discarding lookahead token motA ()
Stack now 0
Program exited normally.

6.4 Un exemple de calculateur simple


Le chier ed-1-calc.y contient un exemple simple d'utilisation de Yacc
pour construire un analyseur syntaxique pour les expressions arithmtiques.
On y retrouve la mme structure quand dans l'exemple prcdent : une partie
dclaration, une partie grammaire, une partie libre.
Les choses nouvelles dans les dclarations sont
 Quand un terminal reprsente un seul caractre constant, ce n'est pas
ncessaire de dnir un symbole. On peut utiliser directement le code du
caractre. Je l'utilise ici pour les oprateurs arithmtiques et le retour la
ligne. Juste pour montrer que ce n'est pas obligatoire, je ne l'ai pas utilis
pour l'oprateur d'exponentiation.
 La grammaire est ambigu, mais j'ai rajout des indications pour lever les
ambiguts aux lignes 1619 : chacune des lignes dnit une associativit
(gauche ou droite) et un niveau de prcdence, du plus faible au plus
104

fort et liste les oprateurs auxquels elle s'applique : d'abord l'addition


et la soustraction (associatifs gauche) ligne 16, puis la multiplication
et la division (eux aussi associatifs gauche) ligne 17, puis l'oprateur
d'exponentiation (associatif droite) ligne 18.
 Pour l'oprateur unaire, j'ai dni un niveau de prcdence suprieur
( la ligne 19) et j'ai annot la rgle de grammaire qui l'utilise (ligne 46)
avec le mot clef %prec pour indiquer le niveau de prcdence utiliser
pour cette rgle. C'est ncessaire puisqu'on a un seul symbole terminal (le
-) qui est utilis pour deux oprateurs dirents : le moins binaire qui a
la prcdence indique pour - et le moins unaire pour lequel la prcdence
sera ce qui est indiqu avec FORT.
 J'ai rajout, ligne 21, une indication du symbole qu'on veut trouver la
racine de l'arbre syntaxique. (Par dfaut, Yacc tente d'y placer le symbole
qui apparait gauche de la premire rgle).
La grammaire dnit le langage comme une suite d'expressions arithmtiques
spares par des sauts de ligne (lignes 2428).
Avec les rgles de grammaires, il y a des actions qui seront excutes quand
les rgles seront appliques. Les actions utilisent la possibilit d'attacher une
valeur (entire par dfaut) chacun des nuds de l'arbre syntaxique avec la
notation $ : $1 est la valeur attache au premier nud de la partie droite de la
rgle, $2 la valeur attache au second nud, etc. Le mot $$ dsigne la (nouvelle)
valeur attache au (nouveau) nud construit en application de la rgle.
tant donn le chier Yacc suivant qui analyse un langage dont
les phrases ne sont constitues que de a :

Ex. 6.1 

%term A
%%
phrase : l
l : A
| l A
;
%%
# include <stdio.h>
int main(){yyparse(); return 0;}
int yylex(void){ int c = getchar(); return c == 'a' ? A : 0; }
int yyerror(char*str){fprintf(stderr, "%s\n", str); }

(a) Ajouter les actions ncessaires cette grammaire pour que le programme
imprime le nombre de A prsents dans la phrase. Le faire uniquement avec les
actions et les valeurs attaches aux nuds, sans dnir aucune variable supplmentaire.
(b) Procder de mme pour imprimer le numro de chaque expression 1-calc.y,
pour avoir par exemple :
$ a.out
? 1 + 1
1: 2

105

? 1 + 1
2: 2
? 1 + 1
3: 2
? Bye
$

6.5 Un calculateur avec des nombres ottants


Le second calculateur, dans le chier 2-calc.y travaille sur les nombres
ottants et permet de continuer travailler aprs une erreur de syntaxe.

6.5.1

La rcupration d'erreurs

En ajoutant la rgle de la ligne 32


exprs : exprs error '\n'

on a indiqu Yacc un point de rcupration des erreurs avec le mot clef error.
Quand on fera une erreur de syntaxe dans une expression arithmtique, Yacc va
appeler yylex jusqu' trouver le retour la ligne, et ce moment re-synchroniser
l'analyseur syntaxique en rduisant le dernier nud exprs et tous les mots qui
le suivent en un nouveau nud exprs. Cela permet au compilateur de continuer
traiter le programme pour dtecter les erreurs suivantes.

6.5.2

Typage des valeurs attaches aux nuds

Avec la dclaration des lignes 1315 :


%union {
float f;
}

on a prvenu Yacc que certains nuds auraient une valeur du type float. Il
faut maintenant spcier le type de valeur attach chaque nud avec une
dclaration. Pour le symbole terminal NBRE, cela est eectu au moment de sa
dclaration ligne 17 avec
%term <f> NBRE

Pour les nuds de type expr, il faut ajouter nouvelle dclaration (ligne 18) :
%type <f> expr

106

6.6 Un calculateur avec un arbre vritable


Pour que les choses soient bien claires, j'ai ajout un troisime calculateur
qui construit explicitement l'arbre syntaxique en mmoire au lieu d'eectuer les
calculs mesure qu'il le construit (implicitement) en appliquant les rgles de
grammaire. On le trouvera dans le chier 3-calc.y.
Les dclarations de types sont maintenant faites avec
8
...
18
19
20
21
22
23
24

typedef struct Noeud Noeud;


%union {
int i;
Noeud * n;
};
%token <i> NBRE /* Les symbole renvoyes par yylex */
%type <n> expr /* Type de valeur attache au noeuds expr */

La valeur des feuilles est du type entier, les nuds internes sont des pointeurs
sur des structures Noeud.
Pour changer un peu par rapport aux programmes prcdents, l'addition
et la soustraction sont maintenant traites par la mme rgle ; c'est la valeur
attache au mot ADD qui permet de faire la dirence entre l'addition et la
soustraction.
43
44

expr : expr ADD expr


{ $$ = noeud($2, $1, $3); }

Il a fallu modier en consquence l'analyseur lexical


106
107
108

case '+': case '-':


yylval.i = c;
return ADD;

et prvenir Yacc du type de valeur attach ces nuds avec une dclaration
que j'ai faite en mme temps que la xation du niveau de prcdence :
26

%left <i> ADD

La multiplication et la division sont regroupes de la mme manire.


A l'aide de l'arbre ainsi construit, le programme imprime l'expression en
parenthsant compltement chaque sous-expression compose avant de calculer
et d'acher sa valeur. Les deux se font avec une descente rcursive simple dans
l'arbre construit, par la fonction parenthse (lignes 200 et seq.) et la fonction
eval (lignes 176 et seq.).
107

Ex. 6.2 

(moyen) Modier la fonction parenthese pour qu'elle n'imprime


que les parenthses ncessaires. (Indication : il faut tenir compte du contexte
dans lequel l'expression apparait, par exemple sous la forme d'un pointeur vers
le nud parent.)

108

Chapitre 7
L'analyse syntaxique : le
fonctionnement interne de
Yacc

Dans ce chapitre, je dtaille le fonctionnement interne de Yacc. Ceci est important deux titres : d'une part cela permet de rsoudre les problmes qui
ne manquent pas de se produire quand on crit une grammaire pour Yacc et
notamment de comprendre et rsoudre les conits shift-reduce qu'on rencontre
dans les grammaires ; d'autre part, cela permet de comprendre les particularits des automates piles (push-down automata en anglais) qui font partie des
concepts structurants important de notre discipline.
Je commence par examiner un peu en dtail l'ordre dans lequel les rgles
d'une grammaire sont activs ; ensuite je montre le fonctionnement de l'automate
avec sa pile ; nalement j'explique de quelle manire l'automate est construit
partir des rgles de grammaires.

7.1 L'analyseur de Yacc est ascendant de gauche


droite
L'analyseur syntaxique produit par Yacc est un analyseur ascendant (bottomup en anglais) : il lit les mots et les regroupe ds que c'est possible, en construisant l'arbre depuis les feuilles vers la racine.
Comme il lit les mots de la phrase dans l'ordre, sans jamais revenir en arrire,
on dit que c'est un analyseur de gauche droite (LR comme Left Right en
anglais).
Ceci est important pour comprendre l'ordre d'excution des actions associes
une rgle. Si on prend le chier Yacc suivant :
109

%term N
%%
l : l N

{ printf("on ajoute %d la liste\n", $2); }


| /* rien */
{ printf("liste vide\n"); }

%%
# include <stdio.h>
int yyerror(char * s){ fprintf(stderr, "%s\n", s); }
int
yylex(void){
static int it, t[] = { 1, 2, 3, 4, 0 };
if (t[it] == 0)
return 0;
yylval = t[it++];
return N;

}
int main(){ yyparse(); return 0; }

et qu'on le fait traiter par Yacc puis le compilateur C et qu'on le lance, on


obtient
$ yacc t.y
$ gcc y.tab.c
$ a.out
liste vide
on ajoute 1
on ajoute 2
on ajoute 3
on ajoute 4

la
la
la
la

liste
liste
liste
liste

parce que l'arbre syntaxique construit est celui de la gure 7.1.


Si on modie juste l'ordre dans la partie droite de la premire rgle, on a la
grammaire (et les actions) :
l : N l

{ printf("on ajoute %d la liste\n", $1); }


| /* rien */
{ printf("liste vide\n"); }

L'excution du programme rsultat donne l'ajout des lments en sens inverse :


liste vide
on ajoute 4 la liste
on ajoute 3 la liste

110

(a)

Arbre
syntaxique
construit
l
Phrase:

analyseur
lexical
Action:

liste vide

(b)

(c)
Arbre
syntaxique
construit

Arbre
syntaxique
construit

Phrase:

Phrase:

analyseur
lexical

analyseur
lexical
Action:

Action:

On ajoute 1

On ajoute 2

(d)

(e)
l

l
Arbre
syntaxique
construit

Phrase:

Arbre
syntaxique
construit

Phrase:

analyseur
lexical

analyseur
lexical
Action:

Action:

On ajoute 3

On ajoute 4

7.1  La construction de l'arbre syntaxique avec l'action associe : en


(a) on applique la deuxime rgle l : /* rien */ avant mme la lecture du
premier mot. Aprs la lecture de chaque mot, l'analyseur ajoute un nud
l'arbre syntaxique et active l'action de la premire rgle l : l N.
Figure

111

on ajoute 2 la liste
on ajoute 1 la liste

L'arbre a t construit dans l'autre sens, comme indiqu dans la gure 7.2

7.2 Utilisation de la pile par l'analyseur


L'automate construit par Yacc fonctionne en plaant les mots dans une pile.
Quand l'automate a dtermin qu'il devait appliquer une rgle, il dpile les
nuds qui correspondent la partie droite et empile la place le nud qui
apparait en partie gauche.
Le dtail du mcanisme est voqu par la gure 7.3, qui montre les tapes
de la construction de l'arbre syntaxique pour la phrase a + 1 3 + 4 avec la
grammaire :
%term N
%left '+'
%left '*'
%%
e : N
{
| e '+'
{
| e '*'
{
;

$$ = $1; }
e
$$ = $1 + $3; }
e
$$ = $1 * $3; }

7.3 Fermetures LR(n)


Pour dterminer les oprations eectuer sur la pile, Yacc construit un
automate dont chaque tat contient la liste des rgles de grammaires qui vont
peut-tre s'appliquer. On appelle cet automate une fermeture LR (ou closure en
anglais).
L'ide gnrale est la suivante : il est impossible de dterminer la rgle qu'on
est en train d'appliquer ds le dpart de la lecture des mots qui composent sa
partie droite ; la place, Yacc construit un automate dont chaque tat contient la
liste des rgles candidates l'application, avec l'endroit o il en est de la lecture
de la partie droite de cette rgle. Quand il a trouv toute la partie droite, alors il
peut la rduire en la remplaant dans la pile par le symbole de la partie gauche.
Pour dmarrer, il construit un tat 0 avec une rgle destine reconnatre
la phrase complte : elle aura en partie droite le symbole de dpart suivi du
marqueur de n (not $), et il indique l o il en est de la reconnaissance de
112

(a)

(a)

Arbre
syntaxique
construit

Arbre
syntaxique
construit

l
Phrase:

Phrase:

analyseur
lexical

Action:

(a)

analyseur
lexical

liste vide

Action:

(b)

Arbre
syntaxique
construit

liste vide

Arbre
syntaxique
construit

l
Phrase:

Phrase:

analyseur
lexical

Action:

analyseur
lexical

liste vide

ajoute 4

Action:

l
l
(c)

l
(d)

Arbre
syntaxique
construit

Arbre
syntaxique
construit

l
Phrase:

Phrase:

analyseur
lexical

Action:

analyseur
lexical

ajoute 3

Action:

ajoute 2

l
l
(d)

Arbre
syntaxique
construit

l
Phrase:

analyseur
lexical

Action:

ajoute 1

Figure 7.2  La construction de l'arbre aprs l'change des deux lments de la


partie droite de la premire rgle : tous les mots sont lus avant que l'analyseur
ne puisse commencer eectuer les rductions, d'abord par la seconde rgle (a)
puis par la premire en ajoutant les lments un par un la liste de la droite
vers la gauche. Les actions impriment donc les nombres dans l'ordre inverse de
celui o ils apparaissent dans la phrase de dpart.
113

Demarrage avec la pile vide

la phrase a lire

Pile
Valeur

Lecture et empilage du premier mot

Pile

Valeur

Reduction du sommet de pile par "e:N"


e
Pile

Valeur

N
1

Lecture et empilage du deuxieme et du troisieme mots


Reduction du sommet de pile par "e:N"
e
Pile

Valeur

e
2
N

Lecture des deux mots suivants


e

Reduction du somme de pile par "e:N"

Pile

Valeur

e
2

e
3

e
Reduction des trois noeuds du sommet par "e: e * e"
Pile

Valeur

e
e

e
Reduction des trois noeuds du sommet par "e : e + e"
Pile

Valeur

e
e

Lecture des deux derniers mots et reduction par "e: N"


Pile

Valeur

e
e

e
e

Reduction des trois noeuds du sommet par "e : e + e"

e
Pile

Valeur

11
e

Figure 7.3  La construction de l'arbre syntaxique de la phrase 1 + 2 3 + 4


correspond une squence de manipulations sur la pile de l'automate. Pour faire
tenir la gure sur une page je n'ai indiqu que la premire rduction de N en e.

114

cette rgle (je l'indiquerai dans les schmas avec le caractre _). L'tat de dpart
de l'automate contient donc :
etat 0
phrase : _ e $

Dans cette tat l'automate attend un e : il doit donc aussi tre prt appliquer, depuis le dbut, les trois rgles qui dcrivent la construction des e : cela
conduit ajouter les trois rgles e : N, e : e + N et e : e * e en insrant le
marqueur de lecture au tout dbut de la partie droite (on n'a encore rien lu de
cette partie droite).
etat 0
phrase : _ e $
e:_N
e:_e+e
e:_e*e

A partir de cet tat 0, l'automate doit tre prt recevoir n'importe lequel
des symboles (terminaux ou pas) qui apparaissent juste droite du marqueur
de lecture (ici e et N). Il ajoute donc deux transitions vers deux nouveaux tats,
qui seront prises respectivement quand il rencontrera un e ou un N :
etat 2

etat 0

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1

Pour dterminer les rgles qui peuvent tre en train de s'appliquer dans ces
tats, il sut d'extraire des rgles en cours d'application dans l'tat 0 celle o
un e apparat juste droite du point de lecture pour l'tat 2 et celle avec un N
pour l'tat 1. Cela donne :

115

etat 2
phrase : e _ $
e:e_+e
e:e_*e

etat 0

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1
e:N_

Dans l'tat 2, on ne sait encore laquelle des trois rgles il va falloir appliquer.
Dans l'tat 1, il n'y a qu'une seule rgle appliquer et on a compltement vu
sa partie droite : cela signie qu'on peut rduire (c'est dire appliquer) la rgle,
en retirant le N de la pile et en empilant un e la place. Je le note avec un R
entour d'un cercle pour reduce (rduire en franais).
etat 2
phrase : e _ $
e:e_+e
e:e_*e

etat 0

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1
e:N_

Dans l'tat 1, il n'y a pas de rgle avec un symbole droite du marqueur de


lecture : il n'y a donc pas de transition qui parte et on a donc termin le travail
pour cet tat.
Depuis l'tat 2 on des rgles avec $, + et * juste droite du marqueur de
lecture : il va donc en partir trois transitions tiquetes avec $, + et * vers trois
nouveaux tats :

116

etat 3
A

phrase : e $ _

etat 2

phrase : e _ $
e:e_+e
e:e_*e

etat 4
e:e+_e

etat 0

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1
e:N_

etat 5
e:e*_e

Dans l'tat 3, on a reconnu compltement la phrase et l'analyseur a termin


son travail. C'est une action spciale qu'on appelle accepter ; je la marque sur le
schma avec A entour d'un cercle. Dans les tats 4 et 5, la prochaine chose qu'on
devra lire est un e : il faut donc ajouter les rgles de la grammaire qui dcrivent
la structure d'un e avec le marqueur de lecture tout au dbut de la partie droite.
Cela implique qu' partir des tats 4 et 5, on va avoir des transitions tiquetes
par e et par N :
etat 3
A

phrase : e $ _

etat 2

phrase : e _ $
e:e_+e
e:e_*e

etat 4
e:e+_e

etat 0

e:_N
e:_e+e
e:_e*e

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1
e:N_

etat 5
R

e:e*_e
e
e:_N
e:_e+e
e:_e*e

On recommence le travail pour les quatre transitions qui partent des tats 4
et 5, pour obtenir le nouveau graphe :
117

etat 3
A

phrase : e $ _

etat 2

etat A

phrase : e _ $
e:e_+e
e:e_*e

e:N_

e:e+_e
etat 0

*
e:_N
e:_e+e
e:_e*e

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 1
e:N_

etat 4

etat 6

e : e + e_
e:e_+e
e:e_*e

etat 7

etat 5
e:e*_e

e : e + e_
e:e_+e
e:e_*e

e
e:_N
e:_e+e
e:_e*e

N
etat B
e:N_

On constate alors que les tats A et B sont exactement identiques l'tat 1 :


ils contiennent les mmes rgles avec le marqueur de lecture au mme endroit.
Au lieu de les crer, on peut donc faire pointer les transitions tiquetes avec N
depuis les tats 4 et 5 vers l'tat 1 :
etat 3
A

phrase : e $ _

etat 2

phrase : e _ $
e:e_+e
e:e_*e

etat 4

e:e+_e
etat 0

phrase : _ e $
e:_N
e:_e+e
e:_e*e

etat 6

etat 1
e:N_

e:_N
e:_e+e
e:_e*e

e:e*_e
e
N

e:_N
e:_e+e
e:_e*e

etat 7

etat 5
R

e : e + e_
e:e_+e
e:e_*e

e : e * e_
e:e_+e
e:e_*e

Pour terminer le travail, il ne reste plus qu' ajouter les transitions depuis les
tats 6 et 7 sur le + et le : on constate qu'elles emmnent vers des tats iden118

etat 3
A

phrase : e $ _

etat 2

phrase : e _ $
e:e_+e
e:e_*e

etat 4

e:e+_e
etat 0

*
N

phrase : _ e $
e:_N
e:_e+e
e:_e*e

e:_N
e:_e+e
e:_e*e

etat 1

etat 7

etat 5
e:e*_e

e : e * e_
e:e_+e
e:e_*e

e
N

e.

*
N

e:N_

Figure

etat 6

e : e + e_
e:e_+e
e:e_*e

e:_N
e:_e+e
e:_e*e

7.4  La fermeture LR(0) de la grammaire e : N | e '+' e | e '*'

tiques aux tats 4 et 5. On a construit la fermeture LR(0) de notre grammaire


(gure 7.4).
Nous venons de faire tourner la main un algorithme de construction de la
fermeture LR(0) ; on peut le dcrire d'une faon plus gnrale avec :
initialiser un tat initial avec Phrase : _ depart $.
placer l'tat initial dans la liste des tats traiter
pour chaque tat restant traiter
dpart = tat
pour chaque symbole qui apparait droite du marqueur dans dpart
courant = symbole
arrive = nouvel tat
ajouter une transition de dpart vers arrive
tiqueter la transition avec le symbole
pour chaque rgle de l'tat de dpart
si courant apparait juste droite du marqueur
ajouter la rgle l'tat d'arrive
y deplacer le marqueur d'une position vers la droite
pour chaque symbole qui apparait droite du marqueur dans arrive
ajouter toutes les rgles qui ont symbole comme partie gauche
y placer le marqueur au dbut de la partie droite
si l'etat d'arrive contient les mmes rgles qu'un tat dja construit
faire pointer la transition vers l'tat dja construit
supprimer l'tat d'arrivee

119

sinon
ajouter l'tat la liste des tats traiter

On a la garantie que l'algorithme se termine parce que le nombre de rgles


est ni et que chaque rgle contient un nombre ni de symboles (et donc de
positions pour le marqueur de lecture).
Une fois la fermeture construite, on la parcourt simplement comme un automate presque ordinaire, avec :
placer l'tat de dpart dans la pile
lire un mot et l'empiler
boucler sur :
empiler l'etat indiqu par l'tat et le symbole du sommet de pile
si l'tat indique qu'on accepte
c'est fini avec succs.
si l'tat indique une rduction par une rgle
dpiler la partie droite de la rgle (avec ses tats)
empiler la partie gauche
ou bien s'il y a une transition qui part de l'tat
lire un mot et l'empiler

A la dirence de la fermeture, l'automate est inni parce que son tat est
aussi dtermin par le contenu de la pile, qui a une profondeur innie (potentiellement, ou du moins dans les limites de la mmoire disponible).
Dans le cas o l'tat et le symbole du sommet de pile n'indiquent pas une
transition, alors c'est qu'on a une erreur de syntaxe.
Je prsente plus loin un exemple de fonctionnement de l'automate, une fois
rsolue la question des conits encore prsents dans la grammaire.

7.3.1

Le chier output

Quand on appelle Yacc avec l'option -v, il dcrit la fermeture de la grammaire dans un chier y.output. (Quand Bison traite un chier foo.y, il place
la description de la fermeture dans le chier foo.output.)

7.3.2

Conits shift-reduce

La fermeture LR(0) que nous avons construite prsente un problme dans les
tats 6 et 7 : l'automate peut choisir soit de rduire par la rgle compltement
vue, soit d'empiler le mot suivant si c'est un + ou une . On a l un conit
shift-reduce.
Ces conits sont la peste quand on crit une grammaire. Ils sont levs de
deux faons : d'une part, Yacc utilise un automate LALR(1), plus puissant que
le LR(0) prsent dans l'exemple plus haut ; d'autre part il applique des rgles
de dtermination aux ambiguts rsiduelles de la grammaire.
120

7.3.3

Fermeture LR(1) et LALR(1)

Sur le mme modle que la fermeture LR(0), on peut construire une fermeture LR(1) en ajoutant chaque rgle non seulement la position du marqueur
de lecture courant, mais aussi le lexme qui est susceptible de la suivre. De cette
manire, il est le plus souvent possible l'analyseur choisir entre shift et reduce
sans ambigit.
Au lieu de n'utiliser qu'un seul symbole qui apparait droite de la rgle, on
peut en utiliser n et on a alors un analyseur LR(n). L'inconvnient est que les
tables utilises pour dcrire la fermeture deviennent normes quand n grandit.
Yacc utilise une version simplie de l'analyseur LR(1) dans laquelle les
tats similaires sont regroups, et qu'on appelle LALR(1). Pour les langages de
programmation, ces analyseurs font en gnral l'aaire.
Le Dragon Book contient une description dtaille de la construction des
fermetures LR(1) et LALR(1) et des proprits de leurs analyseurs. Je ne pense
pas que ce soit essentiel de l'tudier en dtail pour utiliser Yacc avec prot.

7.3.4

L'exemple

Quand on fait tourner l'algorithme sur la phrase 1+23+4, avec l'automate


de la gure 5.10, l'analyseur passe dans les tats suivants. (On suppose pour le
moment que l'automate devine quand il doit eectuer une rduction et quand il
doit empiler le mot suivant ; la manire dont le choix est fait est expliqu juste
aprs).
Empilage de l'tat de dpart.
Pile : 0
Lecture du 1 par l'analyseur lexical qui renvoie N ; empilage du N .
Pile : 0 N
Transition avec N de l'tat 0 dans l'tat 1.
Pile : 0 N 1
Rduction du N en e.
Pile : 0 e
Transition avec e de l'tat 0 dans l'tat 2.
Pile : 0 e 2
Lecture du + par l'analyseur lexical qui renvoie + ; empilage du +.
Pile : 0 e 2 +
Transition avec + de l'tat 2 dans l'tat 4.
Pile : 0 e 2 + 4
Lecture du 2 par l'analyseur lexical qui renvoie N ; empilage du N .
Pile : 0 e 2 + 4 N
Transition avec N de l'tat 4 dans l'tat 1.
Pile : 0 e 2 + 4 N 1
121

Rduction du N en e.
Pile : 0 e 2 + 4 e
Transition avec e de l'tat 4 l'tat 6.
Pile : 0 e 2 + 4 e 6
Lecture du par l'analyseur lexical qui renvoie ; empilage de l'.
Pile : 0 e 2 + 4 e 6
Transition avec de l'tat 6 vers l'tat 5.
Pile : 0 e 2 + 4 e 6 5
Lecture du 3 par l'analyseur lexical qui renvoie N ; empilage du N .
Pile : 0 e 2 + 4 e 6 5 N
Transition avec N de l'tat 4 dans l'tat 1.
Pile : 0 e 2 + 4 e 6 5 N 1
Rduction du N en e.
Pile : 0 e 2 + 4 e 6 5 e
Transition avec e de l'tat 5 dans l'tat 7.
Pile : 0 e 2 + 4 e 6 5 e 7
Rduction du e e en e.
Pile : 0 e 2 + 4 e
Transition sur e de l'tat 4 dans l'tat 6.
Pile : 0 e 2 + 4 e 6
Rduction de e + e en e.
Pile : 0 e
Transition avec e de l'tat 0 dans l'tat 2.
Pile : 0 e 2
Lecture du + par l'analyseur lexical qui renvoie + ; empilage du +.
Pile : 0 e 2 +
Transition avec + de l'tat 2 dans l'tat 4.
Pile : 0 e 2 + 4
Lecture du 4 par l'analyseur lexical qui renvoie N ; empilage du N .
Pile : 0 e 2 + 4 N
Transition avec N de l'tat 4 dans l'tat 1.
Pile : 0 e 2 + 4 N
Rduction du N en e
Pile : 0 e 2 + 4 e
Transition avec e de l'tat 4 dans l'tat 6
Pile : 0 e 2 + 4 e 6
Rduction de e + e en e.
Pile : 0 e
Transition avec e de l'tat 0 dans l'tat 2
Pile : 0 e 2
Lecture de la n de chier par l'analyseur lexical qui renvoie $ ; empilage du
122

$.

Pile : 0 e 2 $
Transition avec $ de l'tat 2 dans l'tat 3
Pile : 0 e 2 $ 3
Acceptation : la phrase est bien forme.

7.4 Exercices
Les exercices qui suivent sont importants pour la matrise complte de la matire du cours. Ils mettent en vidence la faon dont les problmes de prcdence
et d'associativit se traduisent en conits pour Yacc.
Note : pour crire des drivations d'arbre, le plus intuitif est d'utiliser une
feuille, d'y crire des mots, puis de dessiner l'arbre ; cependant, si on souhaite
utiliser un clavier et placer la drivation dans un chier de texte sans image,
c'est plus facile de placer traduire l'arbre sous la forme d'une expression. Par
exemple, avec la grammaire de la page 112, la drivation de la phrase 1+23+4
de la gure 7.3 page 114 peut s'crire avec :

Ex.e=(e=(e=(N=(1))+e=(e=(N=(2))*e=(N=(3))))+e=(N=(4)))
7.1  (Associativit des oprateurs et conits shift-reduce) Soit la grammaire :

p : A
| p X p
;

1. Montrer que la grammaire est ambige en drivant deux arbres syntaxiques dirents pour la phrase AXAXA.
2. (hors sujet) Combien d'arbres syntaxiques dirents pouvez-vous driver pour une phrase constitue d'un A suivi de n fois XA ? (la question
prcdente rpond la question pour le cas o n vaut 2)
3. Comment Yacc vous prvient-il que cette grammaire est ambige ?
4. partir du chier .output fabriqu par Yacc, construire l'automate
LR(0) correspondant cette grammaire, et marquer l'tat o l'ambigit
se manifeste.
5. Refaire les mmes questions pour la grammaire
p : A
| A X p
;

L'oprateur X est-il ici associatif droite ou gauche ?


6. Refaire les mmes questions pour la grammaire
p : A

123

| p X A
;

L'oprateur X est-il ici associatif droite ou gauche ?


7. Reprendre la grammaire de la question 1, et spcier Yacc que l'oprateur X est associatif droite, puis gauche (avec l'indication %left
X ou %right X dans la partie dclaration du chier Yacc). Comparer les
automates obtenus avec ceux des rponses aux questions 1, 4 et 5. Quelle
conclusion en tirer ?

Ex. 7.2 

(LR(0), LALR(1) et LR(1)) Soit la grammaire :

p :
|
;
a :
;
b :
;

a C
b D
X
X

1. numrer les phrases que cette grammaire permet de construire. La


grammaire est-elle ambige ?
2. Construire la fermeture LR(0). La fermeture LR(0) indique-t-elle une
ambigut ? (Si c'est le cas dans quel tat ?)
3. Yacc pense-t-il que cette grammaire est ambige ? Pourquoi ?
4. Mmes questions pour la grammaire :
P :
|
;
a :
;
b :
;

Ex. 7.3 

a Y C
b Y D
X
X

(Variations sur les listes)

1. crire une grammaire pour Yacc qui dcrive une liste non vide de X.
2. crire une grammaire pour Yacc qui dcrive une liste de X qui peut tre
vide.
3. crire une grammaire pour Yacc qui dcrive une liste non vide de X
spars par des virgules
4. crire une grammaire pour Yacc qui dcrive une liste de X spars par
des virgules qui peut tre vide.

124

7.5 Les ambiguts rsiduelles


Quand il reste des ambiguts dans une grammaire, Yacc donne un message
d'erreur annonant le nombre de conits shift-reduce et de conits reduce-reduce
mais produit quand mme un analyseur en adoptant une rgle par dfaut pour
rsoudre les conits.

7.6 Conits shift-reduce, le dangling else


Quand il rencontre un conit shift-reduce, Yacc choisit le shift. (Cela signie
accessoirement que les oprateurs sont par dfaut associatifs droite, mais de
toute manire il est prfrable de dnir l'associativit des oprateurs avec %left
ou %right.)
Cette rgle permet de rsoudre de la faon souhaitable le problme du dangling else (le sinon pendant en franais) : dans un fragment de programme comme
if (x) if (y) e1(); else e2();

la question est celle du if avec lequel associer le else.


Le choix du shift plutt que du reduce permet d'avoir l'interprtation usuelle
des langages de programmation dans laquelle le else est associ avec le dernier
if.
if (x)
if (y)
e2();
else
e3();

Notez que depuis quelques annes quand gcc rencontre une forme comme
celle-l, il suggre de rajouter des accolades.

7.6.1

Conits reduce-reduce

Quand la grammaire prsente des conits reduce-reduce, Yacc choisit de


rduire par la premire rgle qui apparait dans la grammaire, comme on peut
s'en rendre compte avec la grammaire :
%term
%%
p : x
x : A
y : A
%%

A
| y;
{ printf("A rduit en X\n"); };
{ printf("A rduit en Y\n"); };

125

# include <stdio.h>
int main(){ return yyparse(); }
int yylex(){ static int t[] = {A, 0}, it; return t[it++]; }
int yyerror(char * s){ return fprintf(stderr, "%s\n", s); }

Notez que c'est la dnition de la rgle par laquelle rduire et non celle qui
utilise le terminal qui est utilise pour lever l'ambigut. On obtient le mme
rsultat si on remplace la premire rgle par
p : y | x;

7.6.2

Laisser des conits dans sa grammaire

En premire approximation, on ne doit pas laisser de conits reduce-reduce


dans une grammaire. C'est le plus souvent le signe qu'on ne la matrise pas et
cela risque de conduire des catastrophes.
On peut laisser des conits shift-reduce condition de bien comprendre ce
qui les produit. C'est notamment le cas pour le dangling else. Bison possde
une directive %expect qui indique combien la grammaire possde de conits et
permet d'viter un message d'erreur quand le nombre de conits correspond
ce qui est attendu.
Dans tous les cas, il faut documenter les conits, en expliquant ce qui les
produit, la faon dont ils sont rsolus par Yacc et la raison pour laquelle on les
conserve.

7.7 Des dtails supplmentaires sur Yacc


J'aborde ici des points sur les grammaires Yacc que j'ai laiss de cot jusqu'
maintenant. Ils me semblent moins important mais peuvent tre ncessaires pour
lire les grammaires crites par d'autres.

7.7.1

Dnition des symboles terminaux

Pour faciliter leur manipulation, Yacc aecte des valeurs aux symboles de
la grammaire, sous forme de valeurs numriques dnies avec des #define. Par
exemple la dclaration de
%term NBRE

va produire dans le chier y.tab.c une ligne du type


#define NBRE 318

Ces valeurs numriques doivent tre accessibles l'analyseur lexical, puisque


c'est ce qu'il doit renvoyer quand il rencontre un NBRE. Si l'analyseur lexical est
126

plac dans la troisime partie du chier .y, ces valeurs sont dnies ; en revanche
s'il est dans un autre chier, il faut appeler Yacc avec l'option -h ; il place alors
ces dnitions dans un chier nomm y.tab.h (ou foo.tab.h avec Bison quand
il traite un chier nomm foo.y). Ce chier doit ensuite tre inclus dans le chier
o est dni l'analyseur lexical.
A mon avis, dans les projets de taille moyenne, il est beaucoup plus sain
d'inclure l'analyseur dans le chier .y que de passer par y.tab.h.

7.7.2

Les actions au milieu des rgles

p : a b { action1 } c d { action2 }

quivaut
p : a b x c d { action2 } ;
x : /* rien */ { action1 } ;

y compris pour ce qui concerne les $n dans action2.

7.7.3

Rfrences dans la pile

On peut faire rfrence $0, $-1 etc. si on sait quel est le contexte de la pile
de Yacc dans laquelle une rgle est utilise.

7.7.4

Les nuds de type inconnu de Yacc

Avec les actions au milieu des rgles et les rfrences dans la pile, on ne peut
pas spcier le type de certains nuds. Pour ceux l (et ceux l seulement) on
peut dnir leur type avec $<type>n.

7.7.5

%token

On peut dclarer les symboles terminaux avec %token au lieu de %term.


(C'tait mme au dpart l'unique manire de les dclarer, %term a t ajout
ensuite comme un synonyme.) Un token (traduction littrale en franais : un
jeton) est quelque chose qu'on ne peut pas dcouper. C'est une faon informelle
de nommer les lexmes, les mots qu'identie l'analyseur lexical.

7.7.6

%noassoc

De mme qu'on peut spcier qu'un oprateur est associatif gauche ou


droite, on peut aussi indiquer qu'il n'est pas associatif avec %nonassoc.

127

7.8 Le reste
Il existe une autre catgorie importante d'analyseurs syntaxiques qui ne sont
pas traits dans le cours : les analyseurs descendants, dans lesquels on construit
l'arbre partir de la racine. C'est plus plus facile d'crire un analyseur descendant qu'un analyseur ascendant, mais comme de toute faon on n'crit pas les
analyseurs ascendants (c'est Yacc qui le fait), on peut probablement en ignorer
les dtails.

128

Chapitre 8
L'analyseur syntaxique de
Gcc

Ce court chapitre revient sur la question des grammaires. Nous y tudions


la grammaire du langage C pour Yacc telle qu'elle apparait dans la version 2.95
du compilateur Gcc.
J'ai extrait cette grammaire du chier source de gcc nomm c-parse.y ;
j'en ai retir toutes les actions, trs lgrement simpli les rgles et ajout des
numros de lignes. On trouvera le rsultat en annexe.
La suite de ce petit chapitre tudie (de faon incomplte) cette grammaire.
Le but est double : d'une part cela conduit regarder avec quelque dtails une
grammaire raliste d'un compilateur authentique ; d'autre part cela nous sert
aussi explorer des aspects peu apparents du langage C et les extensions au
langage que Gcc supportait dans sa version 2.95.

8.1 Les dclarations


Le type de valeur attach aux lexmes et aux noeuds de l'arbre syntaxique
est dni par le %union des lignes 10 et 11. La valeur de la plupart des noeuds
est du type ttype : un arbre qui reprsente l'arbre syntaxique. Cet arbre sera
ensuite presque immdiatement traduit en un langage intermdiaire appel RTL
comme Register Transfer Language.
Les lignes 1343 contiennent les noms qui ne sont pas des mots rservs :
les choses sont un peu compliques cause des typedefs et de la multiplications
des types possibles. L'analyseur lexical distingue les noms qui ne peuvent pas
dsigner un type (ce sont des IDENTIFIERs), ceux qui ont t dnis avec
typedef et peuvent donc dsigner un type (ou pas) (ce sont des TYPENAME,
ceux qui qualient un type (comme const ou volatile) et les storage class
specier comme static ou auto.
129

Toutes les constantes, entires ou ottantes, seront signales comme des


CONSTANT, sauf les chanes de caractres qui seront reconnues comme des
STRING. Les points de suspensions sont un ELLIPSIS.
Les mots clefs qui ne dnissent pas de type sont dnis aux lignes 4750.
ASM, TYPEOF et ALIGNOF sont des extensions de gcc qui n'appartiennent
pas au langage C, de mme que les terminaux des lignes 49 et 50.
Les oprateurs (avec leurs prcdences) sont dnis aux lignes 60 74. ASSIGN reprsente les oprateurs qui combinent une opration et une aectation
comme += ou =. Les noms des autres sont vidents.

8.2 Les rgles de la grammaire


Quatre grands morceaux : programme, instructions, expressions, dclarations. Seules les dclarations sont vraiment compliques.

8.2.1

Programme

Aux lignes 112126 : un programme est une liste (peut-tre vide) de dclaration de fonctions (fndef) et de dclarations de donnes (datadef), ou des
extensions dont on ne parle pas.

8.2.2

Les instructions

A partir de la ligne 684 jusqu' la ligne 834 on trouve les instructions. Les
choses sont un peu compliques cause d'une extension de gcc qui permet de
dnir des tiquettes (avec le mot clef label).
Les blocs d'instructions sont dnis aux lignes 729733 : ce sont soit rien que
des accolades (ligne 729), soit des dclarations et ventuellement des instructions
(ligne 730), soit pas de dclaration et des instructions (ligne 732). La rgle de
la ligne 731 est l pour aider la rcupration d'erreurs.
Les direntes varits d'instructions sont dnies comme des stmt (pour
statement) lignes 766793,
Noter que les deux formes de return, avec et sans valeur, sont dnis par
deux rgles direntes (780 et 781). L'extension asm utilise quatre rgles et on
dcouvre la ligne 791 que gcc autorise le calcul des tiquettes sur lesquelles
faire des goto (ce que ne permet pas C).
Les direntes sortes d'tiquettes sont aux lignes 799803. Il y a ici aussi une
extension de GCC la ligne 800 : on peut tiqueter des case dans un switch
avec des choses comme case 'a'...'z' pour reconnatre des intervalles de
valeurs.

130

8.2.3

Les expressions

Les expressions sont dnies avec des symboles intermdiaires pour forcer
les niveaux de prcdence.
Le premier niveau, avec la prcdence la plus forte, est celui des primary
(lignes 224 et suivantes) : identicateurs, constantes, expression entre parenthses, appels de fonction (ligne 231), utilisation de tableaux (ligne 232) ou
rfrences des champs de structures avec . (ligne 233) et -> (ligne 234), ++ et
nalement -- postxs.
Le deuxime niveau force la rduction des autres oprateurs unaires en
unary_expr (lignes 177193). Noter comment presque tous les oprateurs unaires
simples sont rduits par la rgle de la ligne 181, qui utilise le unop dni aux
lignes 155162 (mais pas l'*, qui peut s'appliquer une expression qui contient
un cast). Noter aussi comment les deux formes du sizeof (avec un type comme
dans sizeof(int) ou avec une expression comme dans sizeof 1) se traduit
par deux rgles direntes aux lignes 188 et 189.
Le troisime niveau contient les conversions forces (les cast en C), dans les
noeuds de type cast_expr (lignes 196200).
Le quatrime niveau contient presque tous les oprateurs binaires, sauf la
virgule : ce sont les expr_no_commas (lignes 202221). La virgule a un statut
spcial, puisqu'elle peut apparatre la fois dans une liste d'instruction (comme
dans l'expression i = 0, j = MAX) et dans une liste d'arguments (comme dans
foo(1, 2)).
Le niveau suivant est celui des exprlist (avec sa variante nonull_exprlist)
(lignes 167175) dans lequel on intgre les virgules, puis nalement l'expr de la
ligne 164.

8.2.4

Les dclarations

La plus grande partie du reste de la grammaire est consacr aux dclaration


de variables, avec ventuellement des valeurs initiales. Je ne dtaille pas cette
partie qui est vraiment complexe. La complexit des formes de dclarations du
langage C est l'un des problmes les plus fondamentaux du langage C.

8.3 Exercices

Ex. 8.1 

Comment dclarer en C un pointeur sur une fonction avec deux


arguments entiers qui renvoie un nombre en virgule ottante ?

Ex. 8.2 

Comment dclarer en C un pointeur sur une fonction dont l'unique


argument est une chane de caractre et qui renvoie un pointeur sur une fonction
avec un argument ottant qui renvoie une pointeur sur une chane de caractre ?

Ex. 8.3 

Quel arbre syntaxique gcc produira-t-il quand il rduira le frag131

ment de code return t[i++] * *p; en instruction ?

132

Chapitre 9
La smantique, la gnration
de code

Attention, la smantique des langages de programmation dsigne quand on


l'utilise seul tout un sous-domaine de l'informatique trs formel et passablement
strile. Dans le contexte de la compilation, il a un autre sens : c'est tout ce qu'il
est ncessaire d'ajouter l'information contenue dans l'arbre syntaxique pour
pouvoir gnrer du code correct.
En pratique, on met dans la smantique tout ce qu'on ne sait pas faire aisment avec l'analyseur syntaxique. Exemples : dclarations de variables, constances
des expressions dans les initialisations...

9.1 Les grammaires attribues


Je n'en parle (presque) pas. L'ide est d'avoir une manire synthtique de
dcrire les transmissions d'informations (notamment de type) dans l'arbre syntaxique. Il n'y a pas d'outil rpandu comme Yacc pour le faire. Il y a une bonne
description du travail dans le Dragon Book.

9.2 Les conversions


Le principal travail de la smantique du C, ce sont les conversions.
Comparer float x = 8/3; avec float x = 8/3.;. Idem avec int x = 8/3;
avec int x = 8/3.;.
Principe gnral du C : pour une opration entre deux types dirents, on
convertit le moins prcis dans le plus prcis pour faire l'opration.
Lors d'un appel de fonction, les oats sont passs comme des doubles, les
133

petits entiers (char et short) comme des int.


Les calculs sur les entiers de taille infrieure ou gale int se font dans des
int (ce qui signie que c'est un peu coteux d'utiliser des short : il va falloir
convertir les oprandes en int pour faire l'opration puis convertir le rsultat
en short).

9.3 La gnration de code, ppcm


Ce n'est pas bien compliqu de gnrer du code partir de l'arbre syntaxique.
Pour ancrer le propos dans le rel, je vais prsenter cette partie sous la forme
du commentaire d'un minuscule compilateur pour un sous-ensemble de C qui
gnre du code pour le processeur Intel 386. Le code est prsent (en partie)
en annexe avec des lignes numrotes et est il est prsent (en totalit) dans les
documents associs.
Attention, la simplicit extrme de ce compilateur n'a t obtenue qu'en
faisant l'impasse sur certaines amliorations lmentaires ; il ne faut pas l'utiliser
pour autre chose que pour une dmonstration.

9.3.1
C.

Le langage source

Le langage source trait par le compilateur est un sous-ensemble du langage

La principale simplication est que le seul type de donne est l'entier. La


mnagerie des oprateurs de C est aussi rduite au minimum.
Il permet de dnir une fonction vide avec :
main()
{
}

Il contient des boucles while comme dans :


fib_iter(n)
{
int a, b, c;
a = 0;
b = 1;
c = 0;
while(c
c = c
a = a
b = a
}

!= n){
+ 1;
+ b;
- b;

134

return a;

On peut appeler des fonctions et utiliser les valeurs qu'elles renvoient comme
dans :
main(){
int c;

while((c = getchar()) != -1)


putchar(c);

ou bien
fib_rec(n)
{
if (n != 0)
if (n != 1)
return fib_rec(n - 1)
+ fib_rec(n - 2);
return n;
}

Comme on peut le voir dans ces exemples, les programmes du langage


peuvent tous tre compils par un compilateur C ordinaire, sans erreur mais
avec des warnings (des messages d'alertes) parce qu'on ne prcise ni le type
des valeurs renvoyes ni celui des arguments des fonctions (ce sont toujours des
int).

9.3.2

La reprsentation des expressions

Une expression est reprsente par une structure expr, dnie dans le chier

ppcm.h (lignes 13-19) :


1
2
3
4

struct expr {
int position ;
char nom ;

/ en memoire , son index via %ebp /


/ le nom de la variable ( le cas echeant ) /

};

Elle contient d'une part le nom de l'expression (si elle correspond un argument ou une variable ; sinon elle correspond une expression intermdiaire et
le champs contient 0) et l'endroit o elle se trouve dans la pile, par rapport au
frame pointer de la fonction. Il n'y a pas moyen d'avoir de variable globales.
Chaque structure expr utilise est un lment du tableau du mme nom
dni dans le chier expr.c, ligne 7. Deux fonctions permettent d'y accder,
fairepr et exprvar.
135

1
2
3
4

/ fairexpr fabrique une expression ( parametre , argument ou temporaire ) /


struct expr
fairexpr ( char nom ){
register struct expr e ;

5
6
7
8
9
10
11

e = &expr [ nexpr ++];


e>position = posexpr ;
e>nom = nom ;
posexpr += incr ;
return e ;

La fonction fairexpr est utilise pour fabriquer une nouvelle expression


(chier expr.c, lignes 1020). On lui passe en argument le nom de l'expression
si c'est un argument ou une variable locale, ou NULL si c'est une expression
temporaire. Elle se contente d'initialiser les deux champs de prochain lment
libre de expr et de mettre jours la variable posexpr qui indique la position
qu'aura la prochaine expression.
1
2
3
4

/ exprvar renvoie l ' expression qui designe la variable /


struct expr
exprvar ( char s ){
register struct expr e , f ;

5
6
7
8
9
10
11

for ( e = & expr [ 0 ] , f = e + nexpr ; e < f ; e += 1)


if (/ e>nom != NULL && / e>nom == s )
return e ;
fprintf ( stderr , " Erreur , variable %s introuvable \ n " , s ) ;
return & expr [ 0 ] ;

La fonction exprvar (chier expr.c, lignes 2232) sert trouver dans le


tableau expr l'entre qui dcrit une variable dj dclare : elle se contente de
balayer le tableau et de renvoyer l'adresse de la structure qui la dcrit. La raison
pour laquelle on peut se contenter d'une comparaison entre pointeurs au lieu
d'une comparaison de chanes de caractres est donne plus loin.
Il y a galement une fonction reinitexpr (chier expr.c, lignes 3438) pour
rinitialiser le tableau expr aprs une fonction.

9.3.3

L'analyseur lexical

L'analyseur lexical est dni l'aide d'un outil que je n'ai pas encore prsent
dans le cours : lex (ou ex). Tout se trouve dans le chier ppcm.l, que lex va
transformer en une fonction yylex dnie dans un chier nomm lex.yy.c.
Le gros du travail de reconnaissance est dcrit par les lignes 7 20 du chier
ppcm.l : les mots clefs if, else, while, int et return ; les noms de variables
136

ou de fonctions, les constantes entires sous plusieurs formes, l'oprateur d'ingalit != et les caractres spciaux -, +, *, /, %, =, (, ), ;, {, } et ,.
La ligne 20 est utilise pour ignorer les commentaires ordinaires du langage
C ouverts avec /* et ferms avec */. Elle est l surtout pour montrer un exemple
non-trivial d'utilisation de lex et vous pouvez l'ignorer. (Les retours la ligne
dans les commentaires sont d'ailleurs ignors par l'analyseur lexical, si bien
que les numros de ligne indiqus par les messages d'erreurs qui suivent seront
errons.)
Le reste du chier ppcm.l contient une fonction chane utilise pour stocker
les noms de fonction et de variables de faon unique. Il y a simplement un
tableau qui contient toutes les chanes ; quand l'analyseur lexical reconnat un
nom de variable ou de fonction, il l'ajoute dans le tableau s'il n'y est pas dj
et dans tous les cas renvoie l'adresse indique par le tableau. C'est grce cela
qu'on peut comparer deux noms avec une simple comparaison de pointeurs
la ligne 22 du chier expr.c, au lieu d'avoir besoin d'utiliser strcmp : chaque
identieur n'apparat qu'une seule fois dans la mmoire. Un programme srieux
utiliserait une table de hash-coding cet endroit.

9.3.4

La gnration de code

La gnration de code proprement dite est faite dans le chier ppcm.y


mesure que l'analyseur syntaxique construit l'arbre syntaxique. Je dtaille sommairement la gnration de code pour les expressions, pour les instructions, puis
les prologues et pilogues de fonctions.

Les expressions
Le code pour les expressions va de la ligne 115 la ligne 172 du chier ppcm.y.
Comme mentionn plus haut, la principale caractristique est que chaque noeud
de type expr a pour valeur l'adresse d'une structure expr qui indique o se
trouvera sa valeur quand le code sera utilis.
Si l'expression est une variable (ligne 115) on utilise la fonction exprvar
(dans expr.c) pour trouver la structure expr qui la reprsente et on place son
adresse comme valeur du nud.
Si l'expression est une constante (ligne 119) on lui alloue un mot mmoire
et on gnre l'assembleur qui placera cette constante dans le mot mmoire. Ce
n'est clairement pas la meilleur manire de gnrer du bon code : il vaudrait
mieux stocker la valeur de la constante dans la structure expr et l'utiliser plus
tard, mais cela compliquerait le gnrateur de code.
Si l'expression est une aectation (ligne 123), on fabrique l'assembleur pour
recopier le contenu de l'adresse ou se trouvera la valeur de la partie droite
l'adresse o se trouve celle de la partie gauche de l'aectation. On peut noter
que cela permet d'crire des choses anormales comme 1 = a ou a + b = c. Pour
l'interdire, il surait ici de vrier que $1->nom ne vaut pas NULL et dsigne
137

donc bien une variable ou un argument.


Pour tous les autres oprateurs ( unaire, +, , , / et modulo), le schma
est le mme : on alloue un temporaire l'expression (avec fairexpr(NULL)) et
on fabrique l'assembleur qui calculera sa valeur.
Restent les appels de fonctions : on fabrique l'assembleur pour empiler leurs
valeurs mesure qu'on construit le nud listexpr (lignes 170172 (noter comment la liste est dnie de telle manire que les arguments sont empils en
commenant par la n). Une fois les arguments empils, on fabrique l'appel de
la fonction (ligne 158), le dpilage des arguments (lignes 159160) et on alloue un
temporaire pour y placer la valeur (peut-tre) renvoye (ligne 161) ; on termine
avec l'assembleur pour y recopier la valeur renvoye (ligne 162).

vidence

Il va sans dire (mais peut-tre mieux encore en le disant) que le code


est trait une fois, lors de sa compilation. Dans la fonction (stupide) suivante :
foo(){
int x;

x = 0;
while(x != 1000000)
x = x + 1;

il va y avoir un temporaire attribu pour contenir le rsultat du calcul de l'expression x+1. (Le temporaire sera ensuite recopi dans x). La boucle est eectue
un million de fois (dans cet exemple), mais il n'y a qu'un seul temporaire attribu par le compilateur, quand il a trait l'expression x+1.

Les instructions
Le code spcique pour les instructions se trouve aux lignes 82 113, et le
dbut est vraiment facile. Si c'est une instruction vide (ligne 82) il n'y a bien sur
rien faire ; si c'est un bloc d'instructions (ligne 83), rien faire (l'assembleur
aura t gnr pendant la construction des nuds qui forment le bloc) ; si
l'instruction se compose d'une expression (lignes 8485), rien faire non plus
(l'assembleur aura t gnr pendant la construction de l'expression).
Pour les if, le premier nud construit est ifdebut (lignes 109113) : la
construction du nud expr s'accompagne de la fabrication de l'assembleur pour
calculer la valeur de l'expression teste. On fabrique l'assembleur pour comparer
cette valeur avec 0 la ligne 109, et celui qui saute sur le else si elle est gale
la ligne 110.
Ensuite, si le test n'a pas de branche else (ligne 86), l'assembleur de la
branche si-vrai sera fabriqu lors de la construction du nud instr et il sut
d'ajouter l'tiquette else (ligne 87).
138

En revanche, si le test possde une branche else (lignes 88 et suivantes),


il faut produire l'assembleur pour sauter sur la n et placer l'tiquette else
juste aprs la construction du nud instr de la branche si-vrai (lignes 89
91) ; ensuite la construction du nud instr de la branche si-faux (ligne 92)
fabriquera l'assembleur pour le traduire, avant qu'on ajoute l'tiquette de n
(ligne 93).
Ainsi le corps de la fonction (stupide)
foo(){
int x;

if (x)
bar();

sera traduit par

else0:

cmpl $0,-4(%ebp)
// comparer x avec 0
je else0
// sauter s'il est gal
call bar
// appeler bar sinon
movl %eax,-8(%ebp) // valeur renvoye par bar
// fin du test.

En ce qui concerne les boucles, notre langage ne connat que les boucles

while, traites par la rgle des lignes 94 102. Attention, il ne s'agit que d'une

seule rgle avec des actions au milieu. Si on retirait les actions, la rgle serait :
intr : YWHILE '(' expr ')' instr

avec le mot clef while, le test entre parenthses puis l'instruction (peut-tre
compose) qui forme le corps de la boucle.
1
2
3
4
5
6
7
8
9

| YWHILE ' ( '


{ printf (" debut%d : \ n " , $<i>$ = label ( ) ) ; }
expr ' ) '
{ printf ("\ tcmpl $0 ,% d(%%ebp )\ n " , $4>position ) ;
printf ("\ tje fin%d \ n " , $<i >3); }
instr

{ printf ("\ tjmp debut%d \ n " , $<i >3);


printf (" fin%d : \ n " , $<i >3);
}

Avant l'assembleur qui calcule la valeur du test, on place une tiquette de dbut de boucle (ligne 95). La construction du nud expr (ligne 96) s'accompagne
de la production de l'assembleur pour calculer la valeur du test. On ajoute l'assembleur pour tester cette valeur et sauter sur la n si elle est gale 0 (lignes
9798). La construction du nud instr pour le corps de la boucle (ligne 99)
139

s'accompagne de la production de l'assembleur pour l'excuter ; la n, il ne


reste plus qu' ajouter l'assembleur pour sauter au dbut (ligne 100) et placer
l'tiquette de n (ligne 101).
Ainsi le code de la fonction (stupide)
foo(){
int x;

while(x)
bar();

sera traduit par


debut0:

fin0:

// debut
cmpl $0,-4(%ebp) //
je fin0
//
call bar
//
movl %eax,-8(%ebp) //
jmp debut0
//

de la boucle:
comparer x et 0
sauter en dehors si x == 0
appeler bar sinon
recuprer la valeur renvoye par bar
recommencer

Finalement, il reste l'instruction return, traite par la rgle de la ligne 103.


Il sut de fabriquer l'assembleur pour placer la valeur de l'expression retourne
dans le registre %eax (ligne 104) et de sauter sur l'pilogue de la fonction, qui
porte un nom convenu.

9.3.5

Prologues et pilogues de fonctions

La dnition des fonctions se fait avec une seule rgle entrelarde d'actions,
entre les lignes 44 et 66. Si on retire les actions, la rgle devient :
fonction : YNOM '(' .listarg ')' '{' listvar listinstr '}'

Une fonction dans notre langage se compose du nom de la fonction, la liste des
arguments (sans types ; ils sont tous entiers) entre parenthses, puis le corps de
la fonction avec une dclaration des variables puis une liste ventuelle d'instructions.
La liste des arguments et celle des variables sont tous les deux rduits en
.listnom (une liste facultative de nom) dnie lignes 7680 : pour chaque nom
rencontr, on appelle la fonction fairexpr qui initialise dans la mmoire du
compilateur une structure expr pour indiquer l o se trouvera sa valeur. Avec
les variables globales posexpr (comme position de l'expression) et incr (comme
incrment), fairexpr placera correctement les arguments dans la pile, partir
de 8 octets au dessus du pointeur de frame, en commenant par le premier grce
140

la ligne 46. Les variables automatiques quant elles seront places partir de
4 octets en dessous du pointeur de frame, en descendant.
Le rsultat est que quand le compilateur aura trait :
foo(a, b, c){
int x, y, z;

le tableau expr contiendra :


index nom position
0
a
8
1
b
12
c
16
2
3
x
-4
4
y
-8
5
z
-12
ce point, il n'y a pas encore une seule ligne d'assembleur gnre. Le compilateur fabrique maintenant les directives (ligne 50) qui prcdent la fonction.
On souhaiterait avoir le prologue de la fonction, mais on ne peut pas le fabriquer
parce qu'on ignore encore la quantit de mmoire qui sera ncessaire pour les
expressions temporaires. En consquence, le compilateur fabrique une tiquette
qui marque le dbut du corps de la fonction, sur laquelle on pourra sauter quand
on fabriquera le prologue (ligne 51).
Ensuite, lors de la construction du noeud .listinstr (ligne 54), le compilateur fabriquera l'assembleur qui traduit le corps de la fonction. Aprs l'accolade
fermante, il placera l'tiquette qui marque l'pilogue (ligne 56, pour qu'on y
saute lors d'un return ventuel), puis le code usuel de l'pilogue des fonctions
(ligne 57) : libration de la pile, dpilage de l'ancien frame pointer, retour de
d'appel.
Finalement, on peut maintenant construire le prologue puisqu'en traduisant
le corps de la fonction, le compilateur a mesur le nombre de temporaires ncessaires (ce nombre se traduit par la valeur de la variable globale posexpr). La
ligne 59 produit donc l'tiquette qui marque le dbut de la fonction, La ligne
60 les deux lignes usuelles d'assembleur qui commencent le prologue (empiler
l'ancien frame pointer, faire pointer le nouveau frame pointer sur la sauvegarde).
La ligne 61 produit son tour la ligne d'assembleur qui rserve l'espace sur la
pile (en utilisant la valeur de posexpr) ; le compilateur met nalement (ligne
62) l'assembleur pour sauter sur le dbut du corps de la fonction.
Ainsi, la compilation de la fonction (vide)
main(){
}

produit le code suivant :


`

.text

// directives

141

.align 16
.globl main
debmain:
finmain:
movl %ebp,%esp
popl %ebp
ret
main:
pushl %ebp
movl %esp,%ebp
subl $0,%esp
jmp debmain

9.3.6

// debut (et fin) du corps de la fonction


// EPILOGUE :
//
vider la pile
//
rcuprer le vieux frame pointer
// et revenir
// PROLOGUE
// sauver le vieux frame pointer
//
placer le nouveau frame pointer
//
rserver 0 octets sur la pile
// et attaquer le corps de la fonction

Amliorations de ppcm

On trouvera dans les documents associs au cours des amliorations ponctuelles apportes ppcm, que je prsente ici.
Je n'ai pas intgr ces amliorations dans le programme principal parce que je
pense qu'elles sont plus faciles tudier quand il n'y en a qu'une seule d'ajoute
la version simple du compilateur.
Pour les tudier, il est utile de savoir utiliser la commande diff (voir le
manuel).

Oprateur de comparaison ==
Dans le rpertoire cmp, il y a une modication (simple) pour rajouter l'oprateur d'galit (==). La modication est lmentaire.

Traitement des constantes


Le rpertoire cste contient une modication qui permet de ne pas placer
immdiatement les constantes dans un mot mmoire. Ici, la valeur est place
dans la structure expr en attendant qu'elle soit utilise ; quand c'est le cas, elle
est place directement dans l'assembleur (avec le mode d'adressage immdiat),
sans utiliser de mmoire dans la pile.
Il est facile (mais pas fait) d'y rajouter le calcul des oprations dont les
oprandes sont tous les deux des constantes.

Une autre forme de boucle


Pour montrer comment on peut raliser d'autres formes de boucles, le rpertoire dirtyfor contient l'ajout des boucles for du C, grand coup de sauts
inconditionnels dans le code gnr.

142

Les connecteurs logiques


Dans le rpertoire andor, on trouve la compilation des oprateurs && et ||
de C.
Ces oprateurs sont un peu spciaux parce que (comme dans la plupart des
langages de programmation), ces expressions ne sont values qu'autant que
ncessaire pour dterminer la valeur de l'expression. Dans l'expression e1 && e2 ,
l'expression e2 ne sera calcule que si e1 est vraie (si e1 est fausse, on sait dj
que l'expression est fausse et il n'y a pas besoin de calculer e2 ). De mme, dans
e1 || e2 , l'expression e2 ne sera calcule que si e1 est fausse (si e1 est vraie, on
sait dj que l'expression est vraie et il n'y a pas besoin de calculer e2 ).
Il s'agit d'une forme lmentaire de structure de contrle : e1 && e2 quivaut
if (e1 ) e2 ; et e1 || e2 quivaut if (! e1 ) e2 ;.
Cette subtilit est ncessaire pour pouvoir crire des choses comme :
if (npersonnes > 0 && ngateaux / npersonnes == 0)
printf("Il n'y en aura pas pour tout le monde\n");

Si npersonnes vaut 0, il ne faut en aucun cas eectuer la division ! On rencontre


aussi frquemment cette organisation de test avec les tableaux :
int tab[MAX], i;
...
if (i < MAX && tab[i] != 0)
...

Si i est suprieur ou gal MAX, il ne faut pas tester la valeur de tab[i], sinon
on risquerait d'accder un mot mmoire interdit et le processus s'arrterait
avec une erreur d'adressage.

Rutilisation des temporaires


Dans la version simple prsente ici, ppcm attribue un mot mmoire pour
chacune des expressions temporaires (et des constantes) qu'il manipule. Un rapide examen du code montre qu'en ralit chaque temporaire ou constante n'est
utilis qu'une seule fois. Il est donc tentant de rutiliser ces mots mmoire.
Le rpertoire free contient cette modication : la structure expr contient un
nouveau champs qui sert de marqueur pour indiquer s'il s'agit d'un temporaire
utilis ou pas.
Chaque fois qu'on utilise un temporaire, on bascule ce marqueur inutilis
(puisqu'on ne l'utilise qu'une fois).
Quand on a besoin d'un nouveau temporaire, la fonction fairexpr cherche
un temporaire inutilis et le rutilise plutt que d'en allouer un nouveau.

143

Tableaux et pointeurs
Le rpertoire point contient l'addition ppcm des oprateurs d'indirection
via des pointeurs et de l'oprateur [] pour accder aux lments d'un tableau.
Il a fallu que j'ajoute dans chaque noeud de l'arbre syntaxique qui pointe
sur une structure expr un indicateur du nombre d'indirections ncessaires pour
atteindre eectivement la valeur. En cas d'indirection on ne peut plus maintenant se contenter d'utiliser directement la valeur dans la pile. Il faut charger
son adresse (ce qui est fait dans le registre %ebx) pour rcuprer ou modier la
valeur de l'expression.

9.4 Exercices
Pour les exercices dont le corrig est fourni avec le cours (les extensions de
ppcm), il ne faut pas refaire le travail, mais simplement commenter les dirences
avec la version originale.
Quel arbre syntaxique le parseur de ppcm construira-t-il en compilant la fonction

Ex. 9.1 
main(){
int c;

while((c = getchar()) != -1)


putchar(c);

Ex. 9.2 
Ex. 9.3 

Ajouter les oprateurs de comparaison >, >=, <, <= ppcm.

Modier ppcm pour qu'il eectue les oprations dont tous les
oprandes sont constants au moment de la compilation.

Ex. 9.4 

Comparer le code produit par la compilation du code suivant avec


la version simple et la version free de ppcm. Pour chacun d'entre eux, indiquer
le rle (ou les rles) pour chaque temporaire alloue par ppcm.
fib_iter(n){
int a, b, c;
a = 0;
b = 1;
c = 0;
while(c
c = c
a = a
b = a
}

!= n){
+ 1;
+ b;
- b;

144

return a;

145

Chapitre 10
Optimisation

Ce chapitre prsente les optimisations qu'on peut attendre d'un compilateur.


C'est important de savoir de quoi le compilateur est capable, parce que cela peut
nous conduire crire le code d'une manire dirente pour tirer prot de ses
capacits ou pallier ses dciences.

10.1 Prliminaires
Attention, ce chapitre prsente le code optimis du C vers le C, mais il ne
faut jamais crire vos programmes directement sous la forme de code optimis :
c'est bien plus important d'avoir un programme juste qu'un programme rapide
et un programme optimis est trs dicilement dbuggable. (Si ce n'est pas
votre opinion, placez la ligne
# define while if

au dbut de vos chiers C : cela risque d'acclrer signicativement vos programmes ; vous pouvez aussi conomiser de la mmoire avec # define struct union.)

10.1.1

Pourquoi optimise-t-on ?

C'est souvent une bonne ide de gnrer du code amliorable puis de l'amliorer dans une seconde tape, plutt que de gnrer rapidement du code optimal.
Sparer permet de simplier.
On peut utiliser la compilation lente pour avoir du code rapide (pour la
production) ou bien une compilation rapide pour du code lent (pour la mise au
point).

146

10.1.2

Quels critres d'optimisation ?

Le terme optimisation est largement galvaud en informatique. Au sens


propre, optimiser quelque chose signie qu'on trouver la meilleure forme pour
cette chose. Dans notre discipline, on l'emploie frquemment avec le sens bien
plus restreint d'amlioration.
L'amlioration peut concerner de nombreux aspects : pour les compilateurs
actuels, il s'agit le plus souvent de minimiser le temps d'excution et parfois
la quantit de mmoire utilise (pour les systmes embarqus). Ces deux types
d'optimisation sont souvent contradictoires.
On distingue aussi l'optimisation pour le pire cas de l'optimisation pour
le cas moyen. La premire permet de limiter le temps maximum, alors que la
seconde s'intresse au cas le plus frquent.

10.1.3

Sur quelle matire travaille-t-on ?

Souvent du pseudo-code pour pouvoir rutiliser.


Code deux adresses (genre assembleur Intel)
Code trois adresses (genre assembleur RISC)
Pour mmoire, il y a du code 1 adresse (tous les calculs se font avec le mme
registre, souvent le sommet de pile)
Gcc travaille sur le RTL (Register Transfer Language). Le RTL est une sorte
d'assembleur pour une machine virtuelle avec un nombre inni de registres et
une syntaxe la Lisp. Gcc place tout dans des registres, sauf ce dont il doit
pouvoir calculer l'adresse (les tableaux). Ne pas essayer de manipuler l'arbre
syntaxique fabriqu par l'analyseur ; en revanche on peut manipuler le RTL.

10.1.4

Comment optimise-t-on ?

90 % du temps se passe (usuellement) dans 10 % du code (le cur des


boucles) ; c'est l qu'il faut travailler pour obtenir les rsultats les meilleurs.

10.2 Dnitions
Un optimiseur la lucarne ne considre que quelques instructions la fois ;
il n'examine que des instructions voisines et donc ne peut pas collecter des
informations globales sur le fonctionnement du code. En anglais, on appelle cela
un peep-hole optimizer (un optimiseur travers le trou de la serrure en franais).
Un optimiseur de ce type est relativement facile fabriquer.
On appelle bloc de base une suite d'instructions dont on a la garantie qu'elles
seront toutes excutes en squence (il ne contient pas d'instruction de saut, sauf
la n ; il ne ne contient pas d'tiquette sur laquelle sauter). Les optimiseurs
147

peuvent donc utiliser les informations glanes sur l'eet des premires instructions du bloc dans les instructions suivantes.
On considre que les expressions (ou les registres) ont une dure de vie : elle
commence quand l'expression est calcule (on dit alors qu'elle est dnie ; sa
mort intervient la suite de la dernire utilisation de l'expression.

10.3 Optimisations indpendantes


Cette section examine les techniques d'optimisation les plus courantes une
par une. Dans l'optimiseur, il y aura interactions entre ces techniques, considres dans la section suivante.

10.3.1

Le pliage des constantes

Quand on a des valeurs constantes dans le programme, le compilateur peut


eectuer les calculs directement sur ces valeurs au moment de la compilation
et remplacer les expressions qui les contiennent par le rsultat du calcul. Par
exemple, les trois instructions assembleurs :
movl $1,%eax
sall $10,%eax
movl %eax,-16(%ebp)

peuvent tre remplaces par


movl $1024,-16(%ebp)

A peu prs tous les compilateurs font cette optimisation ; a nous permet d'crire
du code plus lisible (si on peut faire conance au compilateur). Par exemple le
code prcdent est obtenu partir de
# define bit(n) (1 << (n))
...
x = bit(10);

De mme, pour dnir 5 mgas, je prfre 5 * 1024 * 1024 5242880.


Un autre exemple frquent d'apparition des constantes est de la forme :
if (foo < &tableau_global[MAX]) ...

Puisque le tableau est global, il est une adresse xe. MAX est constant, donc
l'adresse de &tableau_global[MAX] est une constante qui peut tre calcule
avant l'excution. Cette optimisation est moins frquente, parce qu'il faut que
le compilateur et l'diteur de lien cooprent : le compilateur ne sait pas o dans
la mmoire l'diteur de lien placera le tableau.
148

Tous les compilateurs ne savent pas exploiter les proprits des oprations
arithmtiques pour regrouper les constantes ; par exemple reconnatre que 1+x+1
est quivalent x+2.

Simplications algbriques
Certaines oprations avec des constantes donnent des rsultats connus : addition ou soustraction de 0, multiplication par 0 ou par 1, division par 1.
On ne peut pas appliquer systmatiquement les simplications algbriques ; il
faut s'assurer que la suppression d'une expression ne modie pas le programme.
Par exemple dans l'expression 0*printf("foo\n") on connat sa valeur (c'est
0) mais il ne faut pas supprimer l'appel de printf !

Propagation des constantes


Quand une instruction place une constante dans une variable, il y a moyen
de continuer utiliser sa valeur tant que la variable n'est pas rednie. Ceci
n'est pas tout fait pareil que les oprations sur les constantes (du point de vue
du compilateur). Par exemple x = 23; y = 2 * x; s'optimise facilement en x
= 23; y = 46;.
De mme, on peut imaginer une boucle formule comme :
for(p = q = &tableau_global[base]; p < q + MAX; p++)
*p = 0;

(crite, assez maladroitement, dans l'ide d'viter le calcul de &tableau_global[base]


chaque tour). La propagation des constantes permettra de remplacer q+MAX
par la valeur.

10.3.2

Les instructions de sauts

On peut gnrer des instruction de saut sans se poser de question, puis laisser
l'optimisation retirer les sauts inutiles.

Saut sur la prochaine instruction


Un instruction de saut sur l'instruction suivante est bien videmment inutile
et peut tre retire.
Un exemple o le gnrateur de code produit un saut sur l'instruction suivante : pour compiler l'instruction return x; il va produire des instructions
pour placer la valeur renvoye l'endroit convenu (dans le registre %eax puis
sauter sur l'pilogue de la fonction :
movl x,%eax
jump epilogue

149

Si le return est la dernire instruction du corps de la fonction, alors le saut sur


l'pilogue est en fait un saut sur la prochaine instruction.

les sauts par dessus des sauts


Le gnrateur de code peut produire des sauts conditionnels qui sautent
seulement par dessus une instruction de saut ; par exemple if (test()) ; else bar(); }
sera traduit par ppcm en
(1)
call test
(2)
movl %eax,-4(%ebp)
(3)
cmpl $0,-4(%ebp)
(4)
je else0
(5)
jmp fin0
(6) else0:
(7)
call bar
(8)
movl %eax,-8(%ebp)
(9) fin0:

Il est bien sur souhaitable de remplacer le lignes 46 par une seule instruction
de saut, avec le test invers :
(5)

jne fin0

Les sauts sur des sauts


On peut couramment rencontrer des sauts sur d'autres instructions de saut,
par exemple avec des boucles imbriques. Ppcm traduira le corps de la fonction :
foo(){
while(test1()){
corps1();
while(test2())
corps2();
}
}

par l'assembleur :
(1) debut0:
// premier while
(2)
call test1
// test1()
(3)
movl %eax,-4(%ebp)
(4)
cmpl $0,-4(%ebp)
// test1() == 0
(5)
je fin0
(6)
call corps1
// corps1()
(7)
movl %eax,-8(%ebp)

150

(8) debut1:
// deuxime while
(9)
call test2
// test2()
(10)
movl %eax,-12(%ebp)
(11)
cmpl $0,-12(%ebp) // test2() == 0
(12)
je fin1
(13)
call corps2
// corps2()
(14)
movl %eax,-16(%ebp)
(15)
jmp debut1
(16) fin1:
(17)
jmp debut0
(18) fin0:

Le saut de la ligne 12 peut bien sur tre remplac par jump debut0.
Il est aussi possible (mais plus complexe) de dtecter qu'un saut conditionnel
renvoie sur un autre saut conditionnel quivalent, comme dans dans l'exemple :
if (x >= 10)
if (x > 0)
...

Si l'exemple semble articiel, considrer le fragment de code suivant


# define abs(n) ((n) < 0 ? -(n) : (n))
...
if (x > Min && y > Min)
dist = abs(x) + abs(y); // distance Manhattan
else
dist = sqrt(x*x + y*y); // distance pythagoricienne

10.3.3

ter le code qui ne sert pas

On appelle le code inutilis du code mort. Le compilateur peut (dans certains


cas) le dtecter la compilation. Pour un exemple trivial :
foo(){
bar();
return 0;
joe();
}

L'appel de joe(); suit le return et ne sera donc jamais eectu.


La dtection du code du code mort est facile dans le cas ordinaire : c'est
un bloc de base qu'on ne peut pas atteindre. En revanche pour dtecter qu'une
boucle ne peut pas tre atteinte, il faut construire le graphe de tous les enchanements de blocs de base et en trouver les parties disjointes (ce qui est beaucoup
plus lourd).
151

10.3.4

Utilisation des registres

La qualit du code gnr par un compilateur dpend pour une grande partie
de l'allocation des registres. Il y a toutes sortes de mthodes heuristiques qui
permettent d'eectuer cette aectation d'une faon presque optimale si le code
correspond au comportement par dfaut (en gnral : qu'un test est plus souvent
faux que vrai, que les boucles sont excutes un certain nombre de fois). Sur ce
point, je renvoie le lecteur au Dragon Book sans dtailler.
Il est possible dans le langage C d'indiquer au compilateur les variables les
plus utilises avec le mot clef register, dans l'ide d'aider le compilateur
placer les bonnes variables dans des registres. Le compilateur gcc ignore purement et simplement ces indications, sans doute parce que sont ses auteurs sont
certains que leurs choix d'aectation des registres sont meilleurs que ceux de
l'auteur du programme. Cela me semble trs criticable comme point de vue ; j'ai
cependant cess d'utiliser register dans mes propres programmes.
Une optimisation bien plus accessible sur les registres est d'liminer les chargements et dchargements inutiles, qui sont nombreux dans le code produit par
un gnrateur de code naf. Par exemple, ppcm traduira le corps de :
foo(a, b, c){
a + b + c;
}

par
(1)
(2)
(3)

movl 8(%ebp),%eax
// a dans %eax
addl
12(%ebp),%eax // a+b dans %eax
movl %eax,-4(%ebp)
// sauver a+b

(4)
(5)
(6)

movl -4(%ebp),%eax
// a+b dans %eax
addl
16(%ebp),%eax // a+b+c dans %eax
movl %eax,-8(%ebp)
// sauver a+b+c

Un optimiseur la lucarne dtectera aisment que le chargement de la ligne 4


est inutile. C'est un peu plus dicile de constater que la valeur de -4%ebp n'est
utilis nulle part et que le dchargement de la ligne 3 est lui aussi inutile.
Placer les adresses mmoires dans des registres avant d'oprer dessus peut
aussi tre une source d'optimisation.

10.3.5

Inliner des fonctions

Les optimiseurs peuvent couramment choisir de remplacer un appel de fonction par le corps de la fonction lui-mme. Cela prsente deux avantages : d'une
part on se dispense du cot de l'appel fonction, d'autre part cela ouvre la voie
de nouvelles optimisations (par exemple des propagations de constantes).
152

En C, on peut indiquer lors de la dnition d'une fonction que ses appels


doivent tre remplacs par sa dnition avec le mot clef inline. Par exemple,
on peut faire peu prs indiremment :
# define ABS(n) (n < 0 ? -n : n)
static int
abs(int n){
if (n < 0)
return -n;
return n;
}

(En fait la dnition de ABS manque de parenthses et l'appel de ABS(-x) aura


des eets surprenants premire vue. A la dirence de la macro, la fonction
abs ne fonctionne pas avec des nombres ottants.)
Attention, l'abus des fonctions inline fait grossir le code, ce qui dgrade
l'ecacit du cache et peut conduire facilement qu'on ne le pense des pertes
de performances.

10.3.6

Les sous expressions communes

On a frquemment des sous-expressions qui apparaissent plusieurs fois. Le


compilateur peut dans ce cas rutiliser la valeur calcule lors de la premire
utilisation de l'expression. Pour un exemple lmentaire :
x = a + b + c;
y = a + b + d;

l'expression a+b apparat deux fois et l'optimiseur doit la reformuler de manire


a ne faire que trois additions, et non quatre comme dans la version originale :
t = a + b;
x = t + c;
y = t + d;

Quand on programme dans un style ordinaire, les sous-expressions communes


apparaissent tout particulirement dans les manipulations de tableaux. tant
donn un tableau dclar avec
char tab[X][Y][Z];

quand on fait rfrence un de ses lments tab[a][b][c], le compilateur va


calculer son adresse avec une srie de multiplications et d'addition tab + a *
Y * Z + b * Z + c. Si on fait rfrence aux cases de deux tableaux, comme
dans :

153

(1)
(2)
(3)

char from[X][Y][Z], to[X][Y][Z];


...
to[i][j][k] = from[i][j][k];

la squence d'oprations pour calculer les adresse des lments de from et de to


est la mme. Un bon optimiseur r-crira la ligne 3 dans l'quivalent de
(3.1)
(3.2)

tmp = i * Y * Z + j * Z + k;
*(to + tmp) = *(from + tmp);

ou comme :
(3.1)
(3.2)

10.3.7

tmp = (i * Y + j) * Z + k;
*(to + tmp) = *(from + tmp);

La rduction de force

Il s'agit de remplacer des oprations coteuses par d'autres quivalentes. En


anglais, on appelle cela de la strength reduction
On peut remplacer une multiplication par une puissance de 2 par un dcalage
ou la division d'un nombre ottant par une constante par une multiplication par
l'inverse de la constante. Il faut prendre garde dans ce dernier cas au problme
de prcision des calculs que ces manipulations peuvent aecter.
Une opration ordinaire que nous avons utilise dans la programmation en
assembleur consiste remplacer une comparaison avec 0 cmpl $0,%eax par
le test quivalent andl %eax,%eax et la mise 0 d'un registre comme movl
$0,%eax par xorl %eax,%eax.

10.3.8

Sortir des oprations des boucles

Quand l'optimiseur peut dterminer qu'une expression calcule dans une


boucle ne change pas de valeur entre deux tours, il peut sortir le calcul de la
boucle pour l'excuter une seule fois avant d'y entrer. Par exemple dans :
while(i < nelements - 1)
t[i++] = 0;

si le compilateur peut dterminer que nelements ne change pas de valeur dans


la boucle, alors il peut sortir le calcul de nelements - 1 de la boucle, pour donner
l'quivalent de
tmp1 = nelements - 1;
while( i < tmp1)
t[i++] = 0;"

154

10.3.9

Rduction de force dans les boucles

Souvent, il est possible de remplacer dans une boucle le calcul lent d'une
expression par une modication rapide de la valeur de l'expression au tour prcdent. Ainsi, il est intressant que le compilateur puisse rcrire :
int tab[Max];
for(i = 0; i < Max; i++)
tab[i] = i * 5;

comme
for(i = tmp = 0; i < Max; i++, tmp += 5)
tab[i] = tmp;

ce qui permet de remplacer une multiplication par une addition, moins coteuse.
On peut noter que la multiplication par 4 qu'implique et l'addition qu'implique
le calcul de l'adresse de tab[i] est susceptible de recevoir le mme traitement.

10.3.10

Drouler les boucles

Dans certains cas, il peut tre intressant que le compilateur droule les
boucles, en remplaant la boucle par la rptition de son corps. Par exemple il
peut remplacer avantageusement
for(i = 0; i < 4; i++)
to[i] = from[i];

par
i = 0;
to[i] =
i++;
to[i] =
i++;
to[i] =
i++;
to[i] =
i++;

from[i];
from[i];
from[i];
from[i];

Ceci vite les tests et les instructions de saut, mais surtout permet, aprs propagation des constantes, d'obtenir :
to[0] =
to[1] =
to[2] =
to[3] =
i = 4;

from[0];
from[1];
from[2];
from[3];

155

Il est aussi possible de drouler une boucle mme quand on ne peut pas
dterminer le nombre de fois qu'elle tournera. Cela revient par exemple remplacer
for(i = 0; i < n; i++)
to[i] = from[i];

par (en droulant deux fois) :


for(i =
to[i]
i++;
to[i]
}
if (i <
to[i]
i++;
}

0; i < n - 1; i++){
= from[i];
= from[i];
n){
= from[i];

Cela permet (au moins) d'viter une instruction de saut et un test pour chaque
droulement et ouvre la voie de nouvelles propagations de constantes et limination de sous-expressions communes.
Attention, l'abus du droulement des boucles fait grossir le code, dgrade
l'exploitation des caches et peut facilement induire des pertes de performances.

10.3.11

Modier l'ordre des calculs

Sur les processeurs actuels, il existe plusieurs units de calcul indpendantes,


susceptibles d'eectuer chacune un calcul en mme temps (on appelle cela un
processeur multithread. Le processeur charge plusieurs instructions en mme
temps (souvent quatre) et commence leur excution toutes les quatre. Si par
exemple un calcul sur l'UAL 2 dpend du rsultat du calcul sur l'UAL 1, le
travail est interrompu sur l'UAL 2 jusqu' ce qu'il soit disponible. On a des
problmes du mme genre avec les units de calcul en pipe-line.
Un bon optimiseur tiendra compte de la structure des units de calcul de
manire organiser les instructions indpendantes pour qu'elles soient excutes
en mme temps.
La rorganisation des instructions indpendantes permet aussi de diminuer
l'utilisation des registres. Une bonne heuristique est de commencer par calculer
la partie la plus simple de l'expression.

10.3.12

Divers

Gcc tente d'optimiser l'utilisation des caches pour la mmoire en alignant le


code et les donnes dans la pile sur ce qu'il suppose tre des dbuts des blocs
de mmoire stocks dans les caches (les lignes de cache).
156

Gcc permet de retarder le dpilement des arguments des fonctions empiles


avant un appel, et de ne pas utiliser de frame pointer.
Il ne garde qu'une seule fois les constantes qui apparaissent plusieurs endroits dirents dans le programme (notamment pour les chanes de caractres).
Il permet au programmeur d'indiquer la valeur probable d'un test, de manire amliorer la localit du code et insrer des instructions de pr-chargement
de la mmoire sur les processeurs qui le permettent. On utilise souvent ceci avec
les macros likely et unlikely :
#define likely(x)
#define unlikely(x)

__builtin_expect((x),1)
__builtin_expect((x),0)

La macro likely indique que l'expression x aura le plus souvent la valeur 1


(vrai) et unlikely qu'elle aura probablement la valeur 0 (faux).

10.3.13

Les problmes de prcision

Il est bien sur hors de question pour l'optimiseur de produire du code rapide
mais faux, ou que l'acclration des calculs conduise des pertes de prcision
dans les calculs. Certains programmes fonctionnent aussi moins bien quand les
calculs sont trop prcis. Dans ce cas il n'est pas possible de conserver dans un
registre de prcision double une expression dont la prcision n'est que float.

10.4 Tout ensemble : deux exemples


Considrons du code de mise au point conserv (mais inactiv) dans les
sources du programme, sous la forme d'une macro DEBUG :
# define FAUX (0 == 1)
# define DEBUG if(FAUX)printf
...
DEBUG("La variable i vaut %d\n", i);
...

Les tapes d'optimisation qui nous permettent de conserver ce code dans nos
programmes sans avoir aucun cot supplmentaire sont les suivantes.
 Le compilateur identie que l'expression 0 == 1 est constante et la remplace par sa valeur (0).
 Il reconnat que le test est toujours faux et remplace son saut conditionnel
par un saut sans condition.
 Il dtecte que l'appel printf est du code mort et le retire (le code n'est
mort qu'aprs la transformation de saut de l'tape prcdente).
 Finalement, puisque la chane de caractre n'est utilise nulle part, elle est
limine.
157

Considrons la fonction :
void
copier(int to[], int from[]){
int i;

for(i = 0; i < N; i++)


to[i] = from[i];

Le corps est traduit par un gnrateur de code naf en quelque chose comme :
movl
loop:
cmpl
jge
movl
sall
addl
movl
sall
addl
movl
incl
jump
fin:

$0,%eax

// i = 0

$N,%eax
fin
%eax,%ebx
$2,%ebx
from,%ebx
%eax,%ecx
$2,%ecx
to,%ecx
(%ebx),(%ecx)
%eax
loop

// i < N
// &from[i] dans %ebx
// &to[i] dans %ecx
// to[i[] = from[i]

On peut sortir une instruction de la boucle en rorganisant le code :


movl
jump
loop:
movl
sall
addl
movl
sall
addl
movl
incl
in:
cmpl
jlt
fin:

$0,%eax
in

// i = 0

%eax,%ebx
// &from[i] dans %ebx
$2,%ebx
from,%ebx
%eax,%ecx
// &to[i] dans %ecx
$2,%ecx
to,%ecx
(%ebx),(%ecx) // to[i[] = from[i]
%eax
$N,%eax
loop

// i < N

Aprs limination de la sous-expression commune :


158

xorl
jump
loop:
movl
sall
movl
addl
addl
movl
incl
in:
cmpl
jlt
fin:

%eax,%eax
in
%eax,%ebx
$2,%ebx
%ebx,%ecx
from,%ebx
to,%ecx
(%ebx),(%ecx)
%eax
$N,%eax
loop

// i = 0

//
//
//
//

&from[i] dans %ebx


&to[i] dans %ecx
to[i[] = from[i]
i++

// i < N

Calcul incrmental de 4i :
xorl
xorl
jump
loop:
movl
movl
addl
addl
movl
incl
addl
in:
cmpl
jlt
fin:

%eax,%eax
%edx,%edx
in
%edx,%ebx
%ebx,%ecx
from,%ebx
to,%ecx
(%ebx),(%ecx)
%eax
$4,%edx
$4*N,%edx
loop

// i = 0
// 4i = 0

//
//
//
//
//

&from[i] dans %ebx


&to[i] dans %ecx
to[i[] = from[i]
i++
4i += 4

// 4i < 4N

Suppression de la variable i qui ne sert plus rien :


xorl
jump
loop:
movl
movl
addl
addl
movl
addl
in:
cmpl
jlt

%edx,%edx
in
%edx,%ebx
%ebx,%ecx
from,%ebx
to,%ecx
(%ebx),(%ecx)
$4,%edx
$4*N,%edx
loop

// 4i = 0

//
//
//
//

&from[i] dans %ebx


&to[i] dans %ecx
to[i[] = from[i]
4i += 4

// 4i < 4N

159

fin:

Utilisation du mode d'adressage indirect index


xorl
movl
movl
jump
loop:
movl
addl
in:
cmpl
jlt
fin:

%edx,%edx
from,%ebx
to,%edx
in

// 4i = 0

(%edx,%ebx),(%edx,%ecx) // to[i[] = from[i]


$4,%edx
// 4i += 4
$4*N,%edx
loop

// 4i < 4N

Attribution de registres aux constantes


xorl
movl
movl
movl
movl
jump
loop:
movl
addl
in:
cmpl
jlt
fin:

%edx,%edx
from,%ebx
to,%edx
$4*N,%eax
$4,%esi
in

// 4i = 0
// %eax = 4*N
// %esi = 4

(%edx,%ebx),(%edx,%ecx) // to[i[] = from[i]


%esi,%edx
// 4i += 4
%eax,%edx
loop

// 4i < 4N

Cette version est plus ou moins l'quivalent du code C


for(p = &to, q = from, r = &to[N]; p < r)
*p++ = *to++;

Je rpte encore une fois qu'il ne faut pas programmer de cette faon ; mieux vaut
garder un programme lisible et laisser travailler l'optimiseur, ou se contenter
d'un programme un peu pluslent qu'on peut mettre relire.

10.4.1

Le problme des modications

Tous les appels de fonctions et les rfrences travers les pointeurs ont pu
modier toutes les variables qui ne sont pas locales.
Le mot clef const permet de limiter les dgts en signalant au compilateur
ce qui n'est pas modi. Par exemple :
160

int strcpy(char *, const char *);

pour indiquer que les caractres points par le premier argument sont modis,
mais pas ceux points par le second. Attention, const est d'un maniement trs
dlicat :
int foo(const char **);

indique que les caractres ne sont pas modis, mais le pointeur sur les caractres
peut l'tre, alors que
int foo(char const * *);

indique que le pointeur sur les caractres n'est pas modis mais que les caractres, eux, peuvent l'tre. Si ni l'un ni l'autre ne le sont, il faut crire :
int foo (const char const * *)

On peut mme dire :


int foo (const char const * const *)

ce qui n'a pas grand sens puisque l'argument est une copie ; on se moque de
savoir si elle est modie ou pas.

10.5 Optimisations de gcc


Les options -O0, -O o -O1, -O2, -O3, -Os, -funroll-loops, -funroll-all-loops
Renvoyer la page de documentation ou la reprendre.

10.6 Exercices

Ex. 10.1 

(facile) Dans l'assembleur suivant, produit par gcc en compilant


la fonction pgcd, quels sont les blocs de base ?
pgcd:

.L4:

pushl
movl
subl
jmp

%ebp
%esp, %ebp
$16, %esp
.L2

movl
cmpl
jge
movl
movl

8(%ebp), %eax
12(%ebp), %eax
.L3
12(%ebp), %eax
%eax, -4(%ebp)

161

.L3:
.L2:

movl
movl
movl
movl

8(%ebp), %eax
%eax, 12(%ebp)
-4(%ebp), %eax
%eax, 8(%ebp)

movl
subl

12(%ebp), %eax
%eax, 8(%ebp)

cmpl
jne
movl
leave
ret

$0, 8(%ebp)
.L4
12(%ebp), %eax

Ex. 10.2 

(assez facile) Dans la fonction copier (section 10.4), xer la


constante N 10000 et compiler avec les options -O2 -funroll-all-loops.
Comparer avec le code obtenu avec la seule option -O2 et commenter les diffrences. (un peu plus dicile) Quel gain en terme de nombre d'instructions
excutes l'optimisation permet elle d'obtenir ? (encore un peu plus dicile)
Comparer les vitesses d'excution des deux versions du code obtenu et expliquer.

Ex. 10.3 

Expliquer le code produit par gcc, avec et sans optimisation,


pour traduire la fonction foo.
enum {
N = 1000 * 1000,
};
void
foo(void){
char from[N], to[N];
int i;

init(from);
for(i = 0; i < N; i++)
to[i] = from[i];
manipule(from, to);

(pas de correction) Comparer la vitesse du code produit avec ce qui se passe


quand on remplace la boucle for par un appel la fonction memcpy.

Ex. 10.4 

En compilant le code suivant, partir de quel niveau d'optimisation gcc dtecte-t-il que la fonction joe ne sera jamais appele. (Dans ce cas,
il ne placera par d'appel de la fonction dans le code gnr.)
void

162

bar(int x){
if (x)
ceci();
else if (!x)
cela();
else
joe();
}

Ex. 10.5 

Mme question pour

void
bar(int x){
if (x)
ceci();
else if (!x)
cela();
else
for(;;)
joe();
}

(pas encore de corrig).

Ex. 10.6 

Compiler le code suivant en utilisant gcc avec et sans optimisations. Identier les optimisations apportes.
int fib_iter(int n){
int a = 0, b = 1, c = 0;
while(c != n){
c = c + 1;
a = a + b;
b = a - b;
}
return a;

163

Chapitre 11
L'analyse lexicale : le retour

Ce chapitre revient sur les analyseurs lexicaux traits dans un chapitre prcdent. Il montre l'quivalence qui existe entre les automates nis et les expressions rgulires, puis prsente brivement lex, un outil de prototypage rapide
d'analyseurs lexicaux.

11.1 Les expressions rgulires


Les expressions rgulires sont une faon de dcrire un ensemble de chanes
de caractres (le terme correct en franais est  expressions rationnelles , mais
il est trs peu utilis). Elles apparaissent de nombreux endroits dans le systme
Unix et la manire d'exploiter leurs capacits fait souvent la dirence entre un
utilisateur averti et un novice.

11.1.1

Les expressions rgulires dans l'univers Unix

Usage courant : extraire avec egrep les lignes importantes d'un (gros) chier.
Par exemple on peut extraire toutes les lignes d'un chier de logs qui contiennent
le mot important avec :
egrep 'important' fichier

o bien trouver la valeur de la variable HOME dans l'environnement avec


printenv | egrep HOME=

On peut raliser des recherches beaucoup plus sophistiques ; par exemple


extraire les lignes qui contiennent soit foo, soit bar avec 'foo|bar' ou bien
celles qui contiennent foobar ou foo et bar avec 'foo( et )?bar'.
La commande sed permet de manipuler des lignes sur la base des expressions
rgulires ; on l'utilise frquemment pour fabriquer des commandes. Par exemple
164

pour retirer le '.bak' la n des noms des chiers du rpertoire courant, on


peut prendre la liste des chiers donne par la commande ls, utiliser sed pour
fabriquer des lignes de commande et les passer l'interprte de commande :
ls *.bak | sed 's/\(.*\).bak/mv "&" "\1"/' | sh

Ou bien pour remplacer le (premier) blanc soulign des noms de chiers par un
espace :
ls *_* | sed 's/\(.*\)_\(.*\)/mv "&" "\1 \2"/' | sh

La commande ed permet de modier les chiers sur place. Par exemple, pour
remplacer tous les badname par GoodName dans tous les chiers C du rpertoire
courant :
for i in *.[ch] do ed - "$i" << EOF ; done
g/\([^a-zA-Z0-9_]\)badname\([^a-zA-Z0-9_]\)/s//\1GoodName\2/g
g/^badname\([^a-zA-Z0-9_]\)/s//GoodName\1/g
g/\([^a-zA-Z0-9_]\)badname$/s//\1GoodName/g
g/^badname$/s//GoodName/g
wq
EOF

La structure des quatre premires commandes pour ed sont


g/expr1/s//expr2/g

qui indique, dans toutes les lignes o une chane correspond l'expression rgulire expr1, de remplacer toutes les chaines qui y correspondent par l'expression
rgulire 2. La premire commande traite le cas gnral o badname apparat
au milieu de la ligne, la deuxime celle o elle apparat au dbut de la ligne, la
troisime celle o elle se trouve en n de ligne et la quatrime celle o elle se
trouve toute seule sur la ligne.
La commande awk permet de combiner ces traitements avec des calculs arithmtiques. Par exemple pour calculer une moyenne :
awk '/Result: / { sum += $2; nel += 1 }
END { print "moyenne", sum / nel }'

Pour calculer la moyenne et l'cart type :


awk '/Result: / { sum += res[NR] = $2; nel += 1 }
END { moy = sum / nel;
for (i in res)
sumecarre += (res[i] - moy) * (res[i] - moy);
print "moyenne", moy, "ecart type", sqrt(sumecarre / nel)
}'

165

Les langages perl et python permettent un accs ais aux expressions rgulires. Un bon programmeur perl n'a pas besoin de connatre awk et sed ou les
subtilits de la programmation shell : la matrise de perl permet de tout faire
dans le langage.
En C, les expressions rgulires permettent une analyse bien plus ne des
entres que le pauvre format de scanf avec ses mystres. Voir la documentation
des fonctions regcomp et regexec.

11.1.2

Les expressions rgulires de base

la base, il n'y a que cinq constructeurs pour fabriquer les expressions


rgulires : le caractre, la concatnation, l'alternative, l'optionnel et la rptition. Les expressions rgulires dans les commandes Unix comprennent de
nombreuses autres facilits d'expressions, mais reposent presque exclusivement
sur ces mcanismes de base.

Le caractre
La brique de base est une expression rgulire qui dsigne un caractre. Par
exemple egrep 'X' extraira toutes les lignes qui contiennent un X.

La concatnation
On peut lier les briques entre elles : une expression rgulire r1 suivie d'une
expression rgulire r2 donne une nouvelle expression rgulire r1 r2 qui reconnatra les chanes dont la premire partie est reconnue par r1 et la deuxime par
r2 .
On utilise cette construction pour spcier les expressions rgulires qui
contiennent plus d'un seul caractre. Par exemple, dans l'expression rgulire
'bar', on a concatn l'expression 'b', l'expression 'a' et l'expression 'r'.
On peut bien sur utiliser ce mcanisme pour concatner des expressions
rgulires plus complexes.

L'alternative
L'alternative se note usuellement avec la barre verticale |, comme dans
r1 |r2 : elle indique que l'expression rgulire acceptera n'importe quelle chane
accepte soit par r1 , soit par r2 . Par exemple 'a|b|c' reconnatra n'importe lequel des trois premiers caractres de l'alphabet, alors que 'pentium|((4|5)86)'
reconnatra 486, 586 ou pentium (noter les parenthses dans cet exemple, qui
permettent d'viter les ambigits sur l'ordre dans lequel interprter les concatnations et les alternatives).
Les expressions rgulires d'Unix possdent de nombreuses versions abrges de cet oprateur, qui permettent d'crire les expressions rgulires d'une
166

faon plus aise. La plus courante utilise les crochets quand les branches de
l'alternative ne contiennent qu'un caractre : '[aeiou]' permet de spcier
n'importe quelle voyelle et est beaucoup plus lisible que '(a|e|i|o|u)'. De
mme, '[0-9]' indique n'importe quel chire dcimal et '[A-Za-z]' n'importe
quel symbole alphabtique de l'ASCII.
Parce que l'alphabet est ni (c'est dire contient un nombre limit de caractres), on peut aussi utiliser une version tout sauf ... des crochets. Par exemple
'[ABC]' dsigne n'importe quel caractre sauf A, B et C. De mme le point '.'
dsigne n'importe quel caractre (sauf le retour la ligne dans certains outils).
Certaines implmentations des expressions rgulires ont galement des constructions spciques pour dsigner les direntes formes d'espace, les caractres qui
peuvent appartenir un mot, etc. Voir les pages de manuel des commandes pour
les dtails.
Il est important de comprendre que ces modes d'expression sont du glaage
syntaxique (en anglais syntactic sugar) : ils permettent d'exprimer de manire
plus concise la mme chose qu'avec les oprateurs de base, mais rien de plus.

L'option
L'oprateur d'option permet d'indiquer qu'on peut soit avoir une expression
rgulire, soit rien. Par exemple, 'foo( et )?bar' indique soit la chane 'foo
et bar' (si l'expression optionnelle ' et ' est prsente, soit la chane 'foobar'
(si elle est absente).
Certaines prsentations (plus formelles) des expressions rgulires utilisent
un autre mcanisme quivalent : une expression rgulire peut dsigner la chane
vide, note . On crirait alors l'expression rgulire du dernier exemple 'foo((
et )|)bar'. Un problme de cette prsentation est qu'on ne trouve pas le
caractre  sur un clavier ordinaire.

La rptition
Il y a nalement un oprateur qui permet d'indiquer la rptition de l'expression rgulire sur laquelle il s'applique, n'importe quel nombre de fois, y
compris zro. Il se note avec une toile '*' et s'appelle la fermeture de Kline.
Par exemple 'fo*bar' reconnatra fbar (zro fois o) et fobar (une fois o) et
foobar (deux fois o) et fooobar (trois fois o) et ainsi de suite pour n'importe
quel nombre de o entre le f et le b.
On combine frquemment le point '.' qui dsigne n'importe quel caractre
avec l'toile '*' pour indiquer n'importe quoi, y compris rien. Par exemple la commande egrep 'foo.*bar' imprime les lignes (de son entre standard ou des chiers arguments) qui contiennent la fois foo puis bar. Pour voir aussi les lignes
qui contiennent bar puis foo, on pourrait employer '(foo.*bar)|(bar.*foo)'.
Souvent, sous Unix, l'oprateur + indique que l'expression rgulire sur laquelle il s'applique doit apparatre au moins une fois. Ici aussi, il s'agit d'une
167

simple facilit d'expression, car il va de soi que ri + est quivalent ri (ri *) et


que ri * est une autre faon d'crire ri +?.

11.1.3

Une extension Unix importante

Une extension Unix qui enrichit les expressions rgulires (mais aussi qui les
complique) est la possibilit de faire rfrence une chane de caractre qui a
dj t rencontre dans la partie gauche de l'expression rgulire. Avec la contre
oblique suivie d'un nombre i, on dsigne la chane qui a rempli la ime expression
entre parenthses. Ainsi egrep '(.)\1' permet de slectionner toutes les lignes
dans lesquelles le mme caractre est redoubl.
Cette extension s'utilise largement pour les substitutions ; par exemple considrons la commande pour changer les mots d'une ligne :
sed -e 's/\(.*\) et \(.*\)/\2 et \1/'

L'argument spcie (avec s comme substitute) qu'il faut remplacer une expression rgulire (encadre par des obliques) par une chane (encadre elle aussi
par des obliques). L'expression rgulire se compose de n'importe quoi (entre
parenthses) suivi de et suivi d'un deuxime n'importe quoi (lui aussi entre parenthses). Ce qui le remplacera sera le deuxime n'importe quoi (\2) suivi de
et suivi du premier n'importe quoi (\1).

11.2 Les automates nis dterministes


Les automates sont une faon graphique de se reprsenter un certain nombre
de tches, dont notamment le travail d'un analyseur lexical.
Un automate ni peut se reprsenter avec des tats, qu'on peut voir comme
les noeuds d'un graphe, et des transitions qu'on peut voir comme les arcs qui
relient les noeuds du graphe entre eux, chacune tiquete par un ou plusieurs
symboles de l'alphabet d'entre.
L'automate dmarre dans l'tat de dpart, puis lit l'un aprs l'autre les
caractres de la chane traiter ; pour chaque caractre, il suit la transition vers
un autre tat qui devient le nouvel tat courant. (S'il n'y a pas de transition
tiquete par le caractre courant, alors la chane ne correspond pas celles
reconnues par l'automate.) Quand la chane est termine, si l'automate se trouve
dans un des tats marqus comme  acceptation , cela signie que la chane
est reconnue par l'automate. On peut voir un tel automate lmentaire sur la
gure 11.1.
Pour un exemple moins trivial, voyons un automate (erron) pour reconnatre les commentaires du langage C sur la gure 11.2.
L'automate dmarre dans l'tat de gauche, qui indique qu'on est en dehors
d'un commentaire. Pour tous les caractres sauf l'oblique (/), il suit la transition
168

Depart
0

11.1  Un automate lmentaire pour reconnatre la chane foo. Comme


indiqu, l'automate dmarre dans l'tat 0. Quand il lit un f, il passe dans l'tat
1. S'il lit ensuite un o, il passe dans l'tat 2, d'o un autre o l'amne dans l'tat
3 qui est un tat d'acceptation. Toute autre entre fait chouer l'automate : ce
n'est pas foo qui a t lu.

Figure

debut ?
pas /

pas *

pas /
hors
commentaire

dans un
commentaire
pas *

*
fin ?

11.2  Un automate qui tente de reconnatre les commentaires du langage C. Il ne traitera pas correctement la chane /***/.
Figure

qui le laisse dans le mme tat ; quand il rencontre une oblique, alors il passe
dans l'tat du haut qui indique qu'on est peut-tre au dbut d'un commentaire
(si on rencontre /*), mais peut-tre pas (comme dans a/2). La distinction entre
les deux cas se fait dans l'tat du haut : si le caractre suivant est une *, alors
on entre vraiment dans un commentaire et on passe dans l'tat de droite ; en
revanche, n'importe quoi d'autre fait revenir dans l'tat qui indique qu'on sort
d'un commentaire. La n du commentaire avec */ fait passer de l'tat de droite
l'tat de gauche avec un mcanisme identique, sauf que les transitions se font
d'abord sur * puis sur /.
Cet automate ne traitera pas correctement les commentaires qui contiennent
un nombre impair d'toiles avant la fermeture du commentaire, un cas qui se
prsente frquemment dans les chiers de ceux qui balisent leurs programmes
avec des lignes remplies d'toiles (personellement, je trouve cela de mauvais
got ; je prfre ponctuer mon chier avec de espaces vierges comme dans les
textes ordinaires ; j'estime que cela met mieux la structure du programme en
vidence et facilite sa lecture).
L'exemple plus simple d'un tel chec se produit avec /***/ : sur la premire
169

pas /

debut ?
B

pas *

ni /
ni *

hors
commentaire
A

dans un
commentaire
C

ni /
ni *

*
fin ?
D

11.3  L'automate de la gure 11.2 corrig : maintenant, les commentaires C sont correctement reconnus, mme quand ils se terminent par un nombre
impair d'toiles. J'ai galement ajout des noms aux tats pour les nommer plus
aisment.
Figure

oblique, l'automate passe de l'tat de gauche l'tat du haut, puis de l'tat du


haut l'tat de droite sur la premire toile. La deuxime toile fait passer de
l'tat de droite celui du bas (c'est peut-tre la n d'un commentaire), et la
troisime toile ramne dans l'tat de droite (en dnitive, ce n'tait pas la n
d'un commentaire) d'o l'automate ne sortira plus.
Un des avantages qu'il y a reprsenter les automates sous forme de graphe,
c'est qu'une fois que la cause de l'chec est identie, elle est facile corriger :
il sut de rajouter l'tat du bas une transition vers lui-mme sur le caractre
toile. Cet automate corrig est prsent sur la gure 11.3.
Modier l'automate de la gure 11.3 de manire lui permettre
de reconnatre aussi les commentaires la C++, qui commencent par deux
obliques et se prolongent jusqu' la n de la ligne.
Un autre avantage des automates de cette sorte est qu'ils sont faciles transformer en programme. Par exemple, l'automate de la gure 11.3 peut facilement
se traduire par le fragment de code :

Ex. 11.1 

enum { A, B, C, D }; // les tats


void
automate(){
int c;
int etat = A;
while((c = getchar()) != EOF){
if (etat == A){
if (c == '/')

170

etat = B;
/* else etat = A; */
} else if (etat == B){
if (c == '*')
etat = C;
else
etat = A;
} else if (etat == C){
if (c == '*')
etat = D;
/* else etat == C; */
} else if (etat == D){
if (c == '/')
etat = A;
else if (c != *)
etat = C;
/* else etat = D; */
}

L'tat courant est indiqu par une variable (nomme tat) initialise l'tat
de dpart. La fonction ne contient qu'une boucle qui lit un caractre et modie
l'tat de dpart en fonction de l'tat courant et du caractre lu.
J'ai rajout dans le code, en commentaire pour mmoire, les transitions qui
maintiennent dans un tat. Il serait facile d'y ajouter ce qu'il faut la fonction
pour n'imprimer que les commentaires, ou au contraire retirer les commentaires
d'un programme.
On peut faire la mme chose avec du code encore plus simple et des donnes
un peu plus complexes, en construisant une table des transitions ; si on se limite
aux codes de caractres sur 8 bits (ce qui n'est sans doute plus une trs bonne
ide en 2010, alors que le couple Unicode-UTF est en train de s'imposer), on
peut faire :
enum {
A = 0, B = 1, C = 2, D = 3,
Netat = 4,
Ncar = 256,
};

// les tats

int trans[Netat][Ncar];
void
inittrans(){
int i;

171

a
b

Depart

r
a

Figure 11.4  Un automate non dterministe pour reconnatre les mots baobab
ou babar.

for(i = 0; i
etat[A][i]
etat[C][i]
}
etat[A]['/']
etat[B]['*']
etat[D]['/']

< Ncar; i++){ // par dfaut


= etat[B][i] = A;
= etat[D][i] = C;
= B;
= etat[C]['*'] = C;
= A;

void
automate()
int c;
int etat;

while((c = getchar()) != EOF)


etat = trans[etat][c];

11.3 Les automates nis non dterministes


Les automates nis que nous avons vus dans la section prcdente sont dits
dterministes parce que dans un tat donn, le caractre lu ne permet de choisir qu'une seule transition : c'est ce qui permet de les traduire si facilement
en code compact. Nous voyons ici des automates qui ne prsentent pas cette
caractristique.

11.3.1

Les transitions multiples

Il est parfois plus facile de considrer des automates qui permettent d'atteindre plusieurs tats dirents avec le mme caractre, comme celui, lmentaire, de la gure 11.4. Dans cet automate, partir de l'tat de dpart, il y a
deux transitions tiquete avec b : ceci le rend non dterministe parce que dans
la fonction de parcours on ne peut pas dcider aprs n'avoir lu que le premier
caractre laquelle des deux transitions il convient de suivre.
Pour transformer cet automate en programme, le premier rexe d'un pro172

grammeur qui intgr la rcursivit consiste utiliser le backtracking : tenter de


prendre une des branches et si elle choue revenir en arrire et essayer l'autre.
Cela complique et ralentit considrablement le programme. (Au pire, au lieu
d'avoir un temps de traitement proportionnel au nombre de caractres lus, on
va en obtenir un proportionnel au nombre de transitions non dterministes par
noeud la puissance du nombre de caractres lus.)
Il existe une autre mthode bien plus facile mettre en oeuvre et bien plus
ecace, qui consiste maintenir, au lieu d'un tat courant, une liste de tous les
tats courants possibles. Pour ce faire, j'ai besoin de listes d'entiers (qui coderont
les numros d'tats) ; je peux les raliser de la faon simple suivante, parce qu'il
ne sera ncessaire que d'ajouter des lments dans la liste :
typedef struct List List;
enum {
MaxNel = 1024,
};

// nombre maximum d'lments

struct List {
int nel;
int el[MaxNel]; // il vaudrait mieux allouer dynamiquement
};
/* ajouter -- ajouter un lment dans la liste s'il n'y est pas encore */
static inline void
ajouter(int val, List * l){
int i;
for(i = 0; i < l->nel; i++)
if (l->el[i] == val)
return; // dja dans la liste

assert(l->nel < MaxNel);


l->el[l->nel++] = val;

Pour reprsenter l'automate, on utilise une structure transition avec les tats
de dpart et d'arrive et le caractre qui permet de suivre la transition. L'automate se reprsente avec sa liste de transition, le numro de l'tat de dpart et
la liste des tats d'arrive.
typedef struct Trans Trans;
struct Trans {
int src;
// tat de dpart
char car;
// caractre lu
int dst;
// tat d'arrive

173

};
Trans trans[Netat][MaxTrans];
int ntrans;
int depart;
List arrivee

//
//
//
//

les transitions
le nombre de transitions
numro de l'tat de dpart
les etats d'arrivee

Le code de la fonction de parcours de l'automate peut tre le suivant (sans


aucune amlioration) :
void
automate{}{
List courant, prochain;
int i, j;
int etat;

courant.nel = 0;
ajouter(depart, &courant);
while((c = getchar()) != EOF){
prochain.nel = 0;
for(i = 0; i < courant.nel; i++){
etat = courant.el[i];
for(j = 0; j < ntrans; j++)
if (trans[j].src == courant && trans[j].car == car)
ajouter(trans[j].dst, &prochain);
if (prochain.nel == 0)
return 0;
courant = prochain;
}
return member(courant, arrivee);

La boucle while lit chaque caractre. Pour ce caractre, la premire boucle


for balaye la liste des tats courants possibles. Pour chacun d'eux, la seconde
boucle for examine toutes les transitions qui en manent ; si elle est tiquete
par le caractre lu, la destination est ajoute dans la liste des nouveaux tats
possibles si elle n'y tait pas encore.
La mthode fonctionne parce que la seule chose qui nous intresse est l'tat
courant, sans se soucier du chemin employ pour l'atteindre. Elle n'est pas sans
rapport avec la programmation dynamique.

11.3.2

Les transitions epsilon

Il existe une seconde manire pour un automate ni d'tre non dterministe. Il est parfois pratique d'utiliser des transitions que l'automate est libre
de prendre ou pas, sans lire de caractre. On les appelle des transitions epsilon
174

(parce qu'elle sont ordinairement tiquetes avec le caractre ). L'automate est
alors non dterministe parce qu'il n'y a pas moyen de dterminer, simplement
en regardant le prochain caractre si une transition epsilon doit tre prise ou
pas.
Les transitions epsilon, sont trs utiles pour lier entre eux des automates,
comme montr dans la gure 11.5.
Le parcours d'un automate avec des transitions epsilon n'est pas beaucoup
plus complexe qu'avec un automate dot de transitions multiples. On commence
par dnir une fonction de fermeture qui rajoute une liste d'tats ceux qu'on
peut atteindre avec une transition epsilon :
void
epsilonfermer(List * l){
int i, j, etat;

for(i = 0; i < l->nel; i++){


etat = l->el[i];
for(j = 0; j < ntrans; j++)
if (trans[j].src == etat && trans[j].car == EOF)
ajouter(trans[j].dst, l);
}

La fonction se contente d'ajouter une liste d'tats future ceux qu'on peut
atteindre partir des tats de la liste prsente avec une ou plusieurs transitions
epsilon. J'ai utilis la valeur EOF pour coder epsilon, puisque je suis certain que
ce n'est pas le code d'un caractre valide.
Pour complter la liste avec les tats qu'on atteint aprs plusieurs transitions
epsilon, il est important que la fonction ajouter place le nouvel lment la n
de la liste. De cette manire, les transitions epsilon qui partent des tats ajouts
seront examines aussi.
Finalement, il sut de modier la fonction de parcours pour eectuer la
fermeture sur chaque nouvelle liste d'tats construits en remplaant la ligne :
courant = prochain;

par
courant = prochain;
epsilonfermer(&courant);

11.3.3

Les automates nis dterministes ou pas

Le parcours d'un automate non dterministe ressemble son traitement par


un interprte. Il est possible aussi d'eectuer sa compilation en numrant tous
175

(a)

(b)

(c)

Y
X

epsilon
(d)

Figure 11.5  L'automate (a) reconnat n'importe quel nombre de X, l'automate


(b) reconnat n'importe quel nombre de Y. Pour reconnatre n'importe quel
nombre de X suivi de n'importe quel nombre de Y, la tentative nave de fusion
de ces deux automates en (c) ne fonctionne pas, puisqu'elle reconnat n'importe
quel mlange de X et de Y. En revanche, l'utilisation d'une transition epsilon
en (d) permet d'enchaner facilement les deux automates.

176

les groupes d'tats dans lesquels il est possible de se trouver et en plaant des
transitions entre ces groupes d'tats. L'automate ainsi construit est un automate
dterministe. Un algorithme pour eectuer ce travail que j'appelle la dterminisation d'un automate se trouve dans le Dragon Book.
Cette construction dmontre un rsultat important et un peu tonnant au
premier abord : les automates non dterministes ne sont pas plus puissants que
les automates dterministes : tout ce qu'il est possible de dcrire avec l'un peut
aussi se dcrire avec l'autre.
(Pour le folklore) On peut renverser un automate : il sut de transformer
l'tat de dpart en tat d'acceptation, d'ajouter un nouvel tat de dpart et de
placer des transitions epsilon depuis cet tat de dpart vers les anciens tats
d'acceptation. Si on prend un automate, qu'on le renverse, qu'on dterminise
le rsultat, qu'on le renverse de nouveau et qu'on dterminise le rsultat, alors
on obtient un automate quivalent celui de dpart, mais qui est minimal en
nombre d'tat et de transitions. Je ne connais pas de source crite pour cette mthode qui m'a t indique par Harald Wertz. Elle est sans doute moins ecace
mais beaucoup plus lgante que la mthode de minimisation des automates
indique dans le Dragon Book.

11.4 Des expressions rgulires aux automates


Les automates sont une faon pratique de raisonner sur certains problmes
et ils permettent de structurer des programmes puissants et rapides, mais ils
prsentent un problme : s'il est ais de les dessiner, en revanche ils ne sont pas
faciles dcrire un ordinateur avec les outils usuels d'interaction.
Je montre dans cette section que les automates nis et les expressions rgulires sont quivalents : ce qu'on peut dcrire avec une expression rgulire peut
aussi tre dcrit avec un automate. Or les expressions rgulires sont (relativement) aises dcrire avec un clavier.

11.4.1

tat d'acceptation unique

Nous aurons besoin dans les tapes qui suivent d'avoir des automates qui
n'ont qu'un seul tat d'acceptation. Ceci est facile construire partir d'un
automate avec plusieurs tats d'acceptation. Il sut d'ajouter un nouvel tat
qui sera l'tat d'acceptation et de placer des transitions epsilon entre les anciens
tats d'acceptation et ce nouvel tat. Je reprsente un tel automate dans les
gures qui suivent par une forme indtermine dont n'mergent que l'tat de
dpart et l'tat d'acceptation, comme dans la gure 11.6.

177

automate

Figure 11.6  Une reprsentation d'un automate lorsque les seules choses importantes sont son tat de dpart et son (unique) tat d'acceptation. Le dtail
des tats et des transitions intermdiaires n'apparat pas sur la gure.

r1

r2
epsilon

11.7  Construction de l'automate quivalent r1 r2 partir des automates quivalents r1 et r2 .

Figure

11.4.2

La brique de base

Il va sans dire que les expressions rgulires qui se composent d'un seul
caractres sont quivalentes un automate avec un tat de dpart et un tat
d'acceptation joints par une unique transition tiquete par ce caractre.

11.4.3

Les oprateurs des expressions rgulires

tant donn deux automates qui sont quivalents deux expressions rgulires r1 et r2 , on construit un automate quivalent l'expression rgulire r1 r2
en ajoutant une transition epsilon entre l'tat de d'acceptation du premier vers
l'tat de dpart du second, comme sur la gure 11.7
tant donn deux automates qui sont quivalents deux expressions rgulires r1 et r2 , on construit un automate quivalent l'expression rgulire
r1 |r2 en ajoutant un nouvel tat de dpart et un nouvel tat d'arrive, deux
transitions epsilon de l'tat de dpart vers les anciens tats de dpart et deux
transitions epsilon depuis les anciens tats d'arrive vers le nouvel tat d'arrive,
comme dans la gure 11.8.
tant donn un automate quivalent l'expression rgulire r, on peut facilement construire l'automate quivalent l'expression rgulire r? en ajoutant
un nouvel tat de dpart et un nouvel tat d'acceptation reli l'ancien tat
de dpart et l'ancien tat d'acceptation par des transitions epsilon, avec entre
elles une autre transition epsilon qui permet d'viter de passer par l'automate,
178

r1
epsilon

epsilon

epsilon

epsilon
r2

11.8  Construction de l'automate quivalent r1 |r2 partir des automates quivalents r1 et r2 .

Figure

comme dans la gure 11.9


Finalement, pour la fermeture de Kline, il sut de d'ajouter la gure
prcdente une transition epsilon qui permet de revenir dans l'tat de dpart
une fois qu'on a atteint l'tat d'acceptation, comme dans la gure 11.10.

11.4.4

C'est tout

Parce que nous avons montr comment chaque mcanisme de construction des expressions rgulires peut se traduire en un mcanisme quivalent
de construction des automates, nous avons dmontr que tout ce que peuvent
les automates, les expressions rgulires le peuvent aussi.
En pratique, les mcanismes exposs ici sont souvent trop lourds pour les
expressions rgulires rellement manipules : il a t ncessaire d'tre prudent
pour que tous les cas soient traits correctement. Si, comme c'est souvent le
cas, l'tat d'acceptation n'a pas de transition sortante, il est possible d'utiliser
les tats d'acceptation des automates constituants et ainsi d'viter l'ajout d'un
nouvel tat d'acceptation et de transitions vide. De mme, si l'tat de dpart
n'a pas de transition entrante, l'ajout d'un nouvel tat de dpart est inutile : on
peut fusionner les tats de dpart des automates initiaux.
Nous n'avons pas montr comment construire des expressions rgulires
partir d'un automate, et donc il est possible d'imaginer que les expressions rgulires permettent d'exprimer des choses qui sont hors de porte des automates.
Ce n'est pas le cas, mais je vous demande de me croire sur parole sur ce point.
J'invite les incrdules consulter l'ouvrage de J. Aho et R. Sethi Les concepts
179

epsilon

epsilon

epsilon
Figure 11.9  Construction de l'automate quivalent r ? partir de l'automate
quivalent r. La transition du bas permet de passer directement de l'tat de
dpart l'tat d'arrive sans avoir reconnu l'expression rgulire r.

epsilon

epsilon

epsilon

epsilon
Figure 11.10  Construction de l'automate quivalent r * partir de l'automate quivalent r. La transition du haut permet de revenir dans l'tat de
dpart aprs avoir reconnu l'expression rgulire r.

180

a
a
b
a

b
c

c
11.11  Cet automate accepte n'importe quelle squence de a, b et c
condition que le mme caractre n'apparaisse pas deux fois de suite.

Figure

fondamentaux de l'informatique traduit chez InterEditions.


(assez facile) Traduire l'automate qui reconnat les commentaires
du langage C en expression rgulire.

Ex. 11.2 
Ex. 11.3 

(dicile) L'automate de la gure 11.11 accepte toutes les suites


(non vides) de a, b et c dans lesquelles il n'y a pas deux fois le mme caractre de
suite. Trouver une expression rgulire quivalente. (D'aprs Aho et sc Ullman,
Aspects fondamentaux de l'informatique

11.5 La limite des expressions rgulires

Ex. 11.4 

(instructif) Dessiner un automate ni, dterministe ou pas, qui


reconnat un nombre quelconque de A suivi du mme nombre de B.

11.6 Lex et Flex


On trouve sous Unix une commande lex qu'on peut utiliser pour construire
automatiquement un analyseur lexical partir d'expressions rgulires. La version du projet Gnu de cette commande s'appelle flex.
Le principal avantage de lex, c'est qu'il a t conu pour s'interfacer aisment
avec les analyseurs lexicaux fabriqus par yacc. Pour le reste, les analyseurs
lexicaux qu'il produit sont gros et pas particulirement rapides : on peut l'utiliser
comme un outil de prototypage rapide, mais il est souvent peu judicieux de s'en
servir en production dans les systmes critiques.

181

Chapitre 12
Projet et valuation

Ce court chapitre prsente le projet raliser ; il est aussi destin vous


permettre de donner votre valuation du cours.

12.1 Projet
Vous devez entrer en contact avec moi an de nous mettre d'accord avec un
projet que vous devrez raliser pour la validation du cours.
Le contenu du projet est relativement ouvert. Il peut parfaitement s'intgrer
dans quelque chose que vous ralisez par ailleurs, par exemple pour validation
d'un autre cours.
Le projet doit contenir l'criture d'une grammaire non triviale pour yacc,
dont le parseur produit des donnes qui sont ensuite utilises, ou bien une modication signicative d'une grammaire complexe.
Si vous n'avez pas d'ide, indiquez moi ce qui vous intresse ; j'en aurai
peut-tre une.
Quelques exemples de projets raliss les annes prcdentes :
 un assembleur pour l'ordinateur en papier
 une r-implmentation de la commande make (simplie).
 une modication de ppcm pour lui faire produire du code directement
excutable en mmoire la place de l'assembleur.
 diverses manipulations sur des chiers XML.

12.2 valuation
Merci de prendre aussi le temps de rpondre aux questions suivantes pour
m'aider valuer la faon dont le cours s'est pass de votre point de vue.
Notez que l'ide est d'essayer d'amliorer les choses, aussi les rponses exces182

sives sont peu prs inutiles. Par exemple avec  Le chapitre le plus intressant ?
Aucun. Le chapitre le moins intressant ? Tous. , on ne peut pas faire grand
chose.
 Le contenu du cours a-t-il correspondu ce que vous attendiez ? (si non,
de quelle manire)
 Qu'est-ce qui vous a le plus surpris (en bien) ?
 Qu'est-ce qui vous a le plus du ?
 Quels taient les chapitres les plus diciles ?
 Quels taient les chapitres les plus faciles ?
 Quels taient les chapitres les plus intressants ?
 Quels taient les chapitres les moins intressants ?
 Que me suggreriez-vous de modier dans l'ordre des chapitres ?
 Qu'est-ce qui est en trop dans le cours ?
 Qu'est-ce qui manque dans le cours ?
 Comment taient les exercices du point de vue quantit (pas assez, trop).
 Comment taient les exercices du point de vue dicult (trop durs, trop
faciles) ?
 Que donneriez-vous comme conseil un tudiant qui va suivre le cours ?
 Que me donneriez-vous comme conseil en ce qui concerne le cours ?
 Si vous deviez mettre une note globale au cours, entre 0 et 20, laquelle
mettriez-vous ?
 Quelles questions manque-t-il pour valuer correctement le cours (et bien
videmment, quelle rponse vous y apporteriez) ?

183

Chapitre 13
Documments annexes

13.1 Les parseurs prcdence d'oprateur


13.1.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Le chier src/ea-oper0.c

/ oper0 . c

Un parseur a precedence d ' operateurs avec deux piles en guise de premier analys
Version sans parentheses

Exercice
/
# include
# include
# include
# include

facile : ajouter l ' operateur %

<stdio . h>
<ctype . h>
<assert . h>
<string . h>

enum {
Num = 1 ,
Executer = 1, Empiler = 1 ,
MaxStack = 1024 ,

// renvoye par l ' analyseur lexical


// comparaison de precedence
// taille maximum des piles

char operator = "+ /";


int yylval ;

// la liste des operateurs


// la valeur du lexeme

};

/ yylex

lit le prochain mot sur stdin ,


place sa valeur dans yylval ,
renvoie son type ,
proteste pour les caracteres invalides

184

26
27
28
29

int
yylex ( void ){
int c ;
int t ;

30

redo :
while ( isspace ( c = getchar ( ) ) )
if ( c == '\ n ' )
return 0 ;

31
32
33
34
35

if ( c == EOF )
return 0 ;

36
37
38

if ( isdigit ( c ) ) {
ungetc ( c , stdin ) ;
t = scanf ("% d " , &yylval ) ;
assert ( t == 1 ) ;
return Num ;

39
40
41
42
43

44
45

if ( strchr ( operator , c ) == 0){


fprintf ( stderr , " Caractere %c (\\0% o ) inattendu \ n " , c , c ) ;
goto redo ;

46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

return c ;

int operande [ MaxStack ] ;


int operateur [ MaxStack ] ;
int noperande , noperateur ;

/ preccmp prec . de l ' operateur gauche prec . de l ' operateur droit /


int
preccmp ( int gop , int dop ){
assert ( gop != 0 ) ;
if ( dop == 0)
// EOF : executer ce qui reste .
return Executer ;
if ( gop == dop )
return Executer ;

// le meme : executer

if ( gop == '+ ' | | gop == ' '){ // + ou


if ( dop == '+ ' | | dop == ' ')
return Executer ; // puis + ou : executer
else

185

71

72
73
74
75
76
77
78
79

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

107
108
109
110
111
112
113
114

return 1; // dans tous les autres cas , executer

switch ( op ){
default :
fprintf ( stderr , " Operateur impossible , code %c (\\0% o )\ n " , op , op ) ;
return ;
case ' + ' :
t = operande [ noperande ] ;
t += operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' ':
t = operande [ noperande ] ;
t = operande [ noperande ] t ;
operande [ noperande ++] = t ;
return ;
case ' ' :
t = operande [ noperande ] ;
t = operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' / ' :
t = operande [ noperande ] ;
t = operande [ noperande ] / t ;
operande [ noperande ++] = t ;
return ;

81

106

puis ou / : empiler

void
executer ( int op ){
int t ;

80

105

return Empiler ; //

void
analyser ( void ){
int mot ;
noperateur = noperande = 0 ;
do {
mot = yylex ( ) ;

115

186

if ( mot == Num ){
assert ( noperande < MaxStack ) ;
operande [ noperande ++] = yylval ;

116
117
118
119

} else {
while ( noperateur > 0 && preccmp ( operateur [ noperateur 1 ] , mot ) < 0)
executer ( operateur [ noperateur ] ) ;
assert ( noperateur < MaxStack ) ;
operateur [ noperateur ++] = mot ;
}
} while ( mot != 0 ) ;

120
121
122
123
124
125
126
127

if ( noperateur != 1 | | noperande != 1 | | operateur [ 0 ] != 0)


fprintf ( stderr , " Erreur de syntaxe \ n " ) ;
else
printf ("% d \ n " , operande [ 0 ] ) ;

128
129
130
131
132

133
134
135
136
137
138
139

int
main ( ) {
analyser ( ) ;
return 0 ;

13.1.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Le chier src/eb-oper1.c

/ oper1 . c

Un parseur a precedence d ' operateurs avec deux piles en guise de premier analys
Version avec les parentheses

Exercice
/
# include
# include
# include
# include

facile : ajouter l ' operateur %

<stdio . h>
<ctype . h>
<assert . h>
<string . h>

enum {
Num = 1 ,
Executer = 1, Empiler = 1 ,
MaxStack = 1024 ,

// renvoye par l ' analyseur lexical


// comparaison de precedence
// taille maximum des piles

char operator = "+ /()";

// la liste des operateurs

};

187

19
20
21

int yylval ;

/ yylex

lit le prochain mot sur stdin ,


place sa valeur dans yylval ,
renvoie son type ,
proteste pour les caracteres invalides

22
23
24
25
26
27
28
29

/
int
yylex ( void ){
int c ;
int t ;

30

redo :
while ( isspace ( c = getchar ( ) ) )
if ( c == '\ n ' )
return 0 ;

31
32
33
34
35

if ( c == EOF )
return 0 ;

36
37
38

if ( isdigit ( c ) ) {
ungetc ( c , stdin ) ;
t = scanf ("% d " , &yylval ) ;
assert ( t == 1 ) ;
return Num ;

39
40
41
42
43

44
45

if ( strchr ( operator , c ) == 0){


fprintf ( stderr , " Caractere %c (\\0% o ) inattendu \ n " , c , c ) ;
goto redo ;

46
47
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62

// la valeur du lexeme

return c ;

int operande [ MaxStack ] ;


int operateur [ MaxStack ] ;
int noperande , noperateur ;

/ preccmp prec . de l ' operateur gauche prec . de l ' operateur droit /


int
preccmp ( int gop , int dop ){
assert ( gop != 0 ) ;
if ( dop == 0)
// EOF : executer ce qui reste .
return Executer ;

63

188

64
65
66
67
68
69
70
71
73
74
75
76
77
78
79
81
82
83
84
86

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

// avec une nouvelle fermante :


//

l ' empiler sur son ouvrante

//

et executer sur tous les autres .

if ( dop == ' ( ' )


return Empiler ;

// toujours empiler les nouvelles ouvrantes

if ( gop == dop )
return Executer ;

// le meme : executer

85

89

if ( dop == ' ) ' ) {


if ( gop == ' ( ' )
return Empiler ;
else
return Executer ;

if ( gop == '+ ' | | gop == ' '){ // + ou


if ( dop == '+ ' | | dop == ' ')
return Executer ;
// puis + ou : executer
else
return Empiler ;
// puis ou / : empiler

80

88

// toujours executer la parenthese fermante

72

87

if ( gop == ' ) ' )


return Executer ;

return Executer ; // dans tous les autres cas , executer

void
executer ( int op ){
int t ;
switch ( op ){
default :
fprintf ( stderr , " Operateur impossible , code %c (\\0% o )\ n " , op , op ) ;
return ;
case ' + ' :
t = operande [ noperande ] ;
t += operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' ':
t = operande [ noperande ] ;
t = operande [ noperande ] t ;
operande [ noperande ++] = t ;
return ;
case ' ' :

189

t = operande [ noperande ] ;
t = operande [ noperande ] ;
operande [ noperande ++] = t ;
return ;
case ' / ' :
t = operande [ noperande ] ;
t = operande [ noperande ] / t ;
operande [ noperande ++] = t ;
return ;
case ' ( ' :
fprintf ( stderr , " Ouvrante sans fermante \ n " ) ;
return ;
case ' ) ' :
if ( noperateur == 0){
fprintf ( stderr , " Fermante sans ouvrante \ n " ) ;
return ;

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

125

t = operateur [ noperateur ] ;
if ( t != ' ( ' ) {
fprintf ( stderr , " Cas impossible avec la parenthese fermante \ n " ) ;
return ;

126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

void
analyser ( void ){
int mot ;
noperateur = noperande = 0 ;
do {
mot = yylex ( ) ;
if ( mot == Num ){
assert ( noperande < MaxStack ) ;
operande [ noperande ++] = yylval ;

} else {
while ( noperateur > 0 && preccmp ( operateur [ noperateur 1 ] , mot ) < 0)
executer ( operateur [ noperateur ] ) ;
assert ( noperateur < MaxStack ) ;
operateur [ noperateur ++] = mot ;
}
} while ( mot != 0 ) ;
if ( noperateur != 1 | | noperande != 1 | | operateur [ 0 ] != 0)

190

fprintf ( stderr , " Erreur de syntaxe \ n " ) ;


else
printf ("% d \ n " , operande [ 0 ] ) ;

155
156
157
158

159
160
161
162
163
164
165

int
main ( ) {
analyser ( ) ;
return 0 ;

13.2 Les petits calculateurs avec Yacc


13.2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Le chier src/ed-1-calc-y

/ 1 calc . y

Une calculette elementaire avec Yacc


Les expressions arithmetiques avec les quatre operateurs et les parentheses
/

%{
# define YYDEBUG 1
int yydebug ;

/ Pour avoir du code de mise au point /

int yylex ( void ) ;


int yyerror ( char ) ;

%}

%term NBRE

/ Les symboles renvoyes par yylex /

%left '+ ' ' '


%left ' ' ' / '
%right EXP
%left FORT

/ Precedence et associativite /

%start exprs

/ le symbole de depart /

%%

exprs : / rien /

{ printf ("? " ) ; }


| exprs expr '\ n '
{ printf ("% d \ n ? " , $2 ) ; }
;

29

191

30

expr : NBRE

{ $$ = $1 ; }
| expr '+ ' expr
{ $$ = $1 + $3 ; }
| expr ' ' expr
{ $$ = $1 $3 ; }
| expr EXP expr
{ int i ;
$$ = 1 ;
for ( i = 0 ; i < $3 ; i++)
$$ = $1 ;
}
| expr ' ' expr
{ $$ = $1 $3 ; }
| expr ' / ' expr
{ $$ = $1 / $3 ; }
| ' ' expr
%prec FORT
{ $$ = $2 ; }
| ' ( ' expr ' ) '
{ $$ = $2 ; }
;

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

%%
# include <stdio . h>
# include <assert . h>

static int ligne = 1 ;

/ numero de ligne courante /

int
main ( ) {
yyparse ( ) ;
puts (" Bye " ) ;
return 0 ;

/ yyerror appele par yyparse en cas d ' erreur /


int
yyerror ( char s ){
fprintf ( stderr , "%d : %s \ n " , ligne , s ) ;
return 0 ;

/ yylex

appele par yyparse , lit un mot , pose sa valeur dans yylval


et renvoie son type /

int
yylex ( ) {

192

int c ;

75
76

re :
switch ( c = getchar ( ) ) {
default :
fprintf ( stderr , "'% c ' : caractere pas prevu \ n " , c ) ;
exit ( 1 ) ;

77
78
79
80
81
82

case ' ' : case '\ t ' :


goto re ;

83
84
85

case EOF :
return 0 ;

86
87
88

case ' 0 ' : case ' 1 ' : case ' 2 ' : case ' 3 ' : case ' 4 ' :
case ' 5 ' : case ' 6 ' : case ' 7 ' : case ' 8 ' : case ' 9 ' :
ungetc ( c , stdin ) ;
assert ( scanf ("% d " , &yylval ) == 1 ) ;
return NBRE ;

89
90
91
92
93
94

case '\ n ' :


ligne += 1 ;
/ puis /
case ' + ' : case ' ': case ' ' : case ' / ' : case ' ( ' : case ' ) ' :
return c ;

95
96
97
98
99
100

case ' ^ ' :


return EXP ;

101
102
103
104

13.2.2
1
2
3
4
5
6
7
8
9
10
11
12

Le chier src/ed-3-calc.y

/ 3 calc . y

Une calculette elementaire avec Yacc


Construction explicite de l ' arbre syntaxique en memoire
/

%{
# define YYDEBUG 1

/ Pour avoir du code de mise au point /

typedef struct Noeud Noeud ;


Noeud noeud ( int , Noeud , Noeud ) ;
Noeud feuille ( int , int ) ;
void print ( Noeud ) ;

13

193

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

int yylex ( void ) ;


int yyerror ( char ) ;

%}

%union {
int i ;
};

Noeud n ;

%token <i> NBRE


%type <n> expr

/ Les symbole renvoyes par yylex /


/ Type de valeur attache au noeuds expr /

%left
%left
%left
%left

/ Precedence et associativite /

<i> ADD
<i> MUL
'^ '
UMOINS

%start exprs

/ le symbole de depart /

%%

exprs : / rien /

{ printf ("? " ) ; }


| exprs expr '\ n '
{ print ( $2 ) ;
printf ("? " ) ; }
;

expr : NBRE

{ $$ =
| expr ADD expr
{ $$ =
| expr '^ ' expr
{ $$ =
| expr MUL expr
{ $$ =
| ' ' expr
{ $$ =
| ' ( ' expr ' ) '
{ $$ =
;

feuille ( NBRE , $1 ) ; }
noeud ( $2 , $1 , $3 ) ; }
noeud ( ' ^ ' , $1 , $3 ) ; }
noeud ( $2 , $1 , $3 ) ; }
%prec UMOINS
noeud ( UMOINS , $2 , 0 ) ; }
$2 ; }

%%
# include <stdio . h>
# include <assert . h>

static int ligne = 1 ;

/ numero de ligne courante /

59

194

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

int yydebug = 0 ;

/ different de 0 pour la mise au point /

int
main ( ) {
yyparse ( ) ;
puts (" Bye " ) ;
return 0 ;

/ yyerror appeler par yyparse en cas d ' erreur /


int
yyerror ( char s ){
fprintf ( stderr , "%d : %s \ n " , ligne , s ) ;
return 0 ;

/ yylex

appele par yyparse , lit un mot , pose sa valeur dans yylval


et renvoie son type /

int
yylex ( ) {
int c ;
int r ;

re :
switch ( c = getchar ( ) ) {
default :
fprintf ( stderr , "'% c ' : caractere pas prevu \ n " , c ) ;
goto re ;
case ' ' : case '\ t ' :
goto re ;
case EOF :
return 0 ;
case ' 0 ' : case ' 1 ' : case ' 2 ' : case ' 3 ' : case ' 4 ' :
case ' 5 ' : case ' 6 ' : case ' 7 ' : case ' 8 ' : case ' 9 ' :
ungetc ( c , stdin ) ;
assert ( scanf ("% d " , &r ) == 1 ) ;
yylval . i = r ;
return NBRE ;
case '\ n ' :
ligne += 1 ;
return c ;

105

195

case ' + ' : case ' ':


yylval . i = c ;
return ADD ;

106
107
108
109

case ' ' : case ' / ' :


yylval . i = c ;
return MUL ;

110
111
112
113

case ' ( ' : case ' ) ' : case ' ^ ' :


return c ;

114
115
116
117
118
119
120
121
122
123

}
/

Representation d ' un noeud de l ' arbre qui represente l ' expression


/
struct Noeud {
int type ;

124
125
126
127
128
129
130
131
132
133

};

136
137
138

139

142
143
144
145

return n ;

}
/ feuille

fabrique une feuille de l ' arbre /


Noeud
feuille ( int t , int v ){
Noeud n ;

146
147
148
149
150
151

/ valeur pour les feuilles /


/ enfants pour les autres /

n = malloc ( sizeof n [ 0 ] ) ;
if ( n == 0){
fprintf ( stderr , " Plus de memoire \ n " ) ;
exit ( 1 ) ;

135

141

int val ;
Noeud gauche , droit ;

/ nouvo alloue un nouveau noeud /


static Noeud
nouvo ( void ){
Noeud n ;
void malloc ( ) ;

134

140

n = nouvo ( ) ;
n>type = t ;
n>val = v ;
return n ;

196

152
153
154
155
156

/ noeud fabrique un noeud interne de l ' abre /


Noeud
noeud ( int t , Noeud g , Noeud d ){
Noeud n ;

157
158
159
160
161
162
163
164
165
166
167
168

/ puissance calcule ( mal ) n a la puissance p /


static int
puissance ( int n , int p ){
int r , i ;

169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197

n = nouvo ( ) ;
n>type = t ;
n>gauche = g ;
n>droit = d ;
return n ;

r = 1;
for ( i = 0 ; i < p ; i++)
r = n ;
return r ;

/ eval renvoie la valeur attachee a un noeud /


static int
eval ( Noeud n ){
switch ( n>type ){
default :
fprintf ( stderr , " eval : noeud de type %d inconnu \ n " , n>type ) ;
exit ( 1 ) ;
case NBRE :
return n>val ;
case UMOINS :
return eval ( n>gauche ) ;
case ' + ' :
return eval ( n>gauche ) + eval ( n>droit ) ;
case ' ':
return eval ( n>gauche ) eval ( n>droit ) ;
case ' ' :
return eval ( n>gauche ) eval ( n>droit ) ;
case ' / ' :
return eval ( n>gauche ) / eval ( n>droit ) ;
case ' ^ ' :
return puissance ( eval ( n>gauche ) , eval ( n>droit ) ) ;
}

197

198
199
200
201
202
203
204
205
206

}
/ parenthese ecrit une expression completement parenthesee /
static void
parenthese ( Noeud n ){
switch ( n>type ){
default :
fprintf ( stderr , " eval : noeud de type %d inconnu \ n " , n>type ) ;
exit ( 1 ) ;

207

case NBRE :
printf ("% d " , n>val ) ;
break ;

208
209
210
211

case UMOINS :
printf (" (");
parenthese ( n>gauche ) ;
putchar ( ' ) ' ) ;
break ;

212
213
214
215
216
217

case ' + ' : case ' ': case ' ' :


putchar ( ' ( ' ) ;
parenthese ( n>gauche ) ;
putchar ( n>type ) ;
parenthese ( n>droit ) ;
putchar ( ' ) ' ) ;

218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

case ' / ' :

case ' ^ ' :

/ freenoeuds libere la memoire attachee a un noeud et ses enfants /


static void
freenoeuds ( Noeud n ){
switch ( n>type ){
default :
fprintf ( stderr , " freenoeuds : noeud de type %d inconnu \ n " , n>type ) ;
exit ( 1 ) ;
case NBRE :
break ;
case UMOINS :
freenoeuds ( n>gauche ) ;
break ;
case ' + ' :

case ' ':

case ' ' :

198

case ' / ' :

case ' ^ ' :

243
244

245
246
247
248
249
250
251
252

free ( n ) ;

/ print imprime l ' expression parenthesee et sa valeur /


void
print ( Noeud n ){
parenthese ( n ) ;
/ expression parenthesee /

253

printf (" = %d \ n " , eval ( n ) ) ;

254
255
256
257

freenoeuds ( n>gauche ) ;
freenoeuds ( n>droit ) ;

freenoeuds ( n ) ;

13.2.3
1

Une calculette elementaire avec Yacc


Noeuds de types float ,
Recuperation des erreurs
/

3
4
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

Le chier src/ee-2-calc.y

/ 2 calc . y

/ valeur /

%{
# define YYDEBUG 1

/ Pour avoir du code de mise au point /

int yylex ( void ) ;


int yyerror ( char ) ;

%}

%union {
}

/ le type des valeurs attachees aux noeuds /

float f ;

%term <f> NBRE


%type <f> expr

/ Les symboles renvoyes par yylex /


/ Type de valeur attache aux noeuds expr /

%left
%left
%left
%left

/ Precedence et associativite /

'+ ' ' '


' ' '/ '

EXP
FORT

%start exprs

/ le symbole de depart /

%%

exprs : / rien /

199

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

{ printf ("? " ) ; }


| exprs expr '\ n '
{ printf ("% g \ n ? " , $2 ) ; }
| exprs error '\ n '
;
expr : NBRE

{ $$ = $1 ; }
| expr '+ ' expr
{ $$ = $1 + $3 ; }
| expr ' ' expr
{ $$ = $1 $3 ; }
| expr EXP expr
{ int i ;
$$ = 1 ;
for ( i = 0 ; i < $3 ; i++)
$$ = $1 ;
}
| expr ' ' expr
{ $$ = $1 $3 ; }
| expr ' / ' expr
{ $$ = $1 / $3 ; }
| ' ' expr
%prec FORT
{ $$ = $2 ; }
| ' ( ' expr ' ) '
{ $$ = $2 ; }
;

%%
# include <stdio . h>
# include <assert . h>

static int ligne = 1 ;

/ numero de ligne courante /

int yydebug = 0 ;

/ different de 0 pour la mise au point /

int
main ( ) {
yyparse ( ) ;
puts (" Bye " ) ;
return 0 ;

}
/ yyerror

appeler par yyparse en cas d ' erreur /


int
yyerror ( char s ){
fprintf ( stderr , "%d : %s \ n " , ligne , s ) ;
return 0 ;

200

75
76
77
78
79
80
81

}
/ yylex

appele par yyparse , lit un mot , pose sa valeur dans yylval


et renvoie son type /

int
yylex ( ) {
int c ;

82

re :
switch ( c = getchar ( ) ) {
default :
fprintf ( stderr , "'% c ' : caractere pas prevu \ n " , c ) ;
goto re ;

83
84
85
86
87
88

case ' ' : case '\ t ' :


goto re ;

89
90
91

case EOF :
return 0 ;

92
93
94

case ' 0 ' : case ' 1 ' : case ' 2 ' : case ' 3 ' : case ' 4 ' :
case ' 5 ' : case ' 6 ' : case ' 7 ' : case ' 8 ' : case ' 9 ' :
ungetc ( c , stdin ) ;
assert ( scanf ("% f " , &yylval . f ) == 1 ) ;
return NBRE ;

95
96
97
98
99
100

case '\ n ' :


ligne += 1 ;
/ puis /
case ' + ' : case ' ': case ' ' : case ' / ' : case ' ( ' : case ' ) ' :
return c ;

101
102
103
104
105
106

case ' ^ ' :


return EXP ;

107
108
109
110

13.3 La grammaire C de gcc


1
2
3
4
5
6

La grammaire du langage en C , extrait des sources de gcc ( v . 2 . 9 5 . 2 ) ,


fichier cparse . y , sans les actions , un peu simplifie .

jm , fevrier 2000
/

201

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

%start program
%union { long itype ; tree ttype ; enum tree_code code ;
char filename ; int lineno ; int ends_in_label ; }
/ All identifiers that are not reserved words

and are not declared typedefs in the current block /

%token IDENTIFIER

/ All identifiers that are declared typedefs in the current block .


In some contexts , they are treated just like IDENTIFIER ,
but they can also serve as typespecs in declarations .
/
%token TYPENAME
/ Reserved words that specify storage class .

yylval contains an IDENTIFIER_NODE which indicates which one .


/
%token SCSPEC

/ Reserved words that specify type .

yylval contains an IDENTIFIER_NODE which indicates which one .


/
%token TYPESPEC

/ Reserved words that qualify type : " const " , " volatile " , or " restrict " .
yylval contains an IDENTIFIER_NODE which indicates which one .
/
%token TYPE_QUAL
/ Character or numeric constants .

yylval is the node for the constant .

%token CONSTANT

/ String constants in raw form .


yylval is a STRING_CST node . /
%token STRING
/ " . . . " , used for functions with variable arglists .
%token ELLIPSIS
/ the
/ SCO
%token
%token

reserved words /
include files test " ASM " , so use something else . /
SIZEOF ENUM STRUCT UNION IF ELSE WHILE DO FOR SWITCH CASE DEFAULT
BREAK CONTINUE RETURN GOTO ASM_KEYWORD TYPEOF ALIGNOF

202

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

%token ATTRIBUTE EXTENSION LABEL


%token REALPART IMAGPART
/ Add precedence rules to solve dangling else s / r conflict /
%nonassoc IF
%nonassoc ELSE
/ Define the operator tokens and their precedences .
The value is an integer because , if used , it is the tree code
to use in the expression made from the operator . /
%right <code> ASSIGN '= '
%right <code> ' ? ' ' : '
%left <code> OROR
%left <code> ANDAND
%left <code> ' | '
%left <code> '^ '
%left <code> '& '
%left <code> EQCOMPARE
%left <code> ARITHCOMPARE
%left <code> LSHIFT RSHIFT
%left <code> '+ ' ' '
%left <code> ' ' ' / ' '% '
%right <code> UNARY PLUSPLUS MINUSMINUS
%left HYPERUNARY
%left <code> POINTSAT ' . ' ' ( ' ' [ '
%type <code> unop
%type
%type
%type
%type
%type
%type
%type
%type
%type
%type
%type
%type
%type

<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>

identifier IDENTIFIER TYPENAME CONSTANT expr nonnull_exprlist expr


expr_no_commas cast_expr unary_expr primary string STRING
typed_declspecs reserved_declspecs
typed_typespecs reserved_typespecquals
declmods typespec typespecqual_reserved
typed_declspecs_no_prefix_attr reserved_declspecs_no_prefix_attr
declmods_no_prefix_attr
SCSPEC TYPESPEC TYPE_QUAL nonempty_type_quals maybe_type_qual
initdecls notype_initdecls initdcl notype_initdcl
init maybeasm
asm_operands nonnull_asm_operands asm_operand asm_clobbers
maybe_attribute attributes attribute attribute_list attrib
any_word

%type <ttype> compstmt


%type <ttype> declarator
203

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

%type <ttype> notype_declarator after_type_declarator


%type <ttype> parm_declarator
%type
%type
%type
%type
%type
%type

<ttype>
<ttype>
<ttype>
<ttype>
<ttype>
<ttype>

%type <ttype> parmlist parmlist_1 parmlist_2


%type <ttype> parmlist_or_identifiers parmlist_or_identifiers_1
%type <ttype> identifiers_or_typenames
%type <ends_in_label > lineno_stmt_or_label lineno_stmt_or_labels stmt_or_label
%%

program : / empty /
| extdefs

114
115
116

extdefs :

117
118

121

extdef :

122
123
124
125

128

datadef :

129

|
|
|
|
|
|
|
;

130
131
132
133
134
135
136
137
138
139

fndef
| datadef
| ASM_KEYWORD ' ( ' expr ' ) '
| EXTENSION extdef

'; '

126
127

extdef
| extdefs extdef

119
120

structsp component_decl_list component_decl_list2


component_decl components component_declarator
enumlist enumerator
struct_head union_head enum_head
typename absdcl absdcl1 type_quals
xexpr parms parm identifiers

notype_initdecls ' ; '


declmods notype_initdecls ' ; '
typed_declspecs initdecls ' ; '
declmods ' ; '
typed_declspecs ' ; '
error ' ; '
error ' } '

'; '

fndef :

204

140
141

|
|

142
143
144

|
|
|
;

145
146
147
148
149
150
151
152

identifier :
IDENTIFIER
| TYPENAME

153
154
155

unop :

156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

typed_declspecs declarator
old_style_parm_decls compstmt_or_error
typed_declspecs declarator error
declmods notype_declarator
old_style_parm_decls compstmt_or_error
declmods notype_declarator error
notype_declarator old_style_parm_decls compstmt_or_error
notype_declarator error

expr :

|
|
|
|
|
|
;

'& '
''
'+ '

PLUSPLUS
MINUSMINUS

'~ '
'! '

nonnull_exprlist

exprlist :

/ empty /
| nonnull_exprlist
;

nonnull_exprlist :
expr_no_commas
| nonnull_exprlist ' , ' expr_no_commas

unary_expr :
primary
| ' ' cast_expr
%prec UNARY
| EXTENSION cast_expr
%prec UNARY
| unop cast_expr %prec UNARY
/ Refer to the address of a label as a pointer .
/
| ANDAND identifier

205

184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199

/ This seems to be impossible on some machines , so let ' s turn it off .


You can use __builtin_next_arg to find the anonymous stack args .
| '& ' ELLIPSIS
/
| SIZEOF unary_expr %prec UNARY
| SIZEOF ' ( ' typename ' ) ' %prec HYPERUNARY
| ALIGNOF unary_expr %prec UNARY
| ALIGNOF ' ( ' typename ' ) ' %prec HYPERUNARY
| REALPART cast_expr %prec UNARY
| IMAGPART cast_expr %prec UNARY
;
cast_expr :
unary_expr
| ' ( ' typename ' ) ' cast_expr %prec UNARY
| ' ( ' typename ' ) ' ' { ' initlist_maybe_comma ' } '

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221

expr_no_commas :
cast_expr
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas
| expr_no_commas

222
223
224
225
226
227
228

primary :

'+ '
''
' '
'/ '
'% '

expr_no_commas
expr_no_commas
expr_no_commas
expr_no_commas
expr_no_commas
LSHIFT expr_no_commas
RSHIFT expr_no_commas
ARITHCOMPARE expr_no_commas
EQCOMPARE expr_no_commas
'& ' expr_no_commas
' | ' expr_no_commas
'^ ' expr_no_commas
ANDAND expr_no_commas
OROR expr_no_commas
' ? ' expr ' : ' expr_no_commas
' ? ' ' : ' expr_no_commas
'= ' expr_no_commas
ASSIGN expr_no_commas

IDENTIFIER
| CONSTANT
| string
| ' ( ' expr ' ) '

206

%prec UNARY

229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

|
|
|
|
|
|
|
|
;

' ( ' error ' ) '


' ( ' compstmt ' ) '
primary ' ( ' exprlist ' ) '
%prec ' . '
primary ' [ ' expr ' ] '
%prec ' . '
primary ' . ' identifier
primary POINTSAT identifier
primary PLUSPLUS
primary MINUSMINUS

/ Produces a STRING_CST with perhaps more STRING_CSTs chained onto it .


/
string :
STRING

| string STRING
;

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271

old_style_parm_decls :
/ empty /
| datadecls
| datadecls ELLIPSIS
/ . . . is used here to indicate a varargs function .
/

/ The following are analogous to lineno_decl , decls and decl


except that they do not allow nested functions .
They are used for oldstyle parm decls . /
lineno_datadecl :
;

datadecl

datadecls :
lineno_datadecl
| errstmt
| datadecls lineno_datadecl
| lineno_datadecl errstmt

/ We don ' t allow prefix attributes here because they cause reduce / reduce
conflicts : we can ' t know whether we ' re parsing a function decl with
attribute suffix , or function defn with attribute prefix on first old
style parm . /
datadecl :
207

typed_declspecs_no_prefix_attr initdecls ' ; '


| declmods_no_prefix_attr notype_initdecls ' ; '
| typed_declspecs_no_prefix_attr ' ; '
| declmods_no_prefix_attr ' ; '

272
273
274
275

276
277
278
279
280
281
282

/ This combination which saves a lineno before a decl


is the normal thing to use , rather than decl itself .
This is to avoid shift / reduce conflicts in contexts
where statement labels are allowed . /
lineno_decl :

283

284
285
286

decls :

287
288
289
290

293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316

lineno_decl
| errstmt
| decls lineno_decl
| lineno_decl errstmt

291
292

decl

decl :

typed_declspecs initdecls ' ; '


| declmods notype_initdecls ' ; '
| typed_declspecs nested_function
| declmods notype_nested_function
| typed_declspecs ' ; '
| declmods ' ; '
| EXTENSION decl

/ Declspecs which contain at least one type specifier or typedef name .


( Just ` const ' or ` volatile ' is not enough . )
A typedef ' d name following these is taken as a name to be declared .
Declspecs have a nonNULL TREE_VALUE , attributes do not .
/
typed_declspecs :
typespec reserved_declspecs
| declmods typespec reserved_declspecs

reserved_declspecs : / empty /
| reserved_declspecs typespecqual_reserved
| reserved_declspecs SCSPEC
| reserved_declspecs attributes

208

317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347

;
typed_declspecs_no_prefix_attr :
typespec reserved_declspecs_no_prefix_attr
| declmods_no_prefix_attr typespec reserved_declspecs_no_prefix_attr

reserved_declspecs_no_prefix_attr :
/ empty /
| reserved_declspecs_no_prefix_attr typespecqual_reserved
| reserved_declspecs_no_prefix_attr SCSPEC

/ List of just storage classes , type modifiers , and prefix attributes .


A declaration can start with just this , but then it cannot be used
to redeclare a typedefname .
Declspecs have a nonNULL TREE_VALUE , attributes do not .
/
declmods :

declmods_no_prefix_attr

| attributes
| declmods declmods_no_prefix_attr
| declmods attributes
;
declmods_no_prefix_attr :
TYPE_QUAL
| SCSPEC
| declmods_no_prefix_attr TYPE_QUAL
| declmods_no_prefix_attr SCSPEC

348
349
350
351
352
353
354
355
356
357
358
359
360

/ Used instead of declspecs where storage classes are not allowed


( that is , for typenames and structure components ) .
Don ' t accept a typedefname if anything but a modifier precedes it .
/
typed_typespecs :
typespec reserved_typespecquals
| nonempty_type_quals typespec reserved_typespecquals

reserved_typespecquals : / empty /
| reserved_typespecquals typespecqual_reserved

209

361
362
363

/ A typespec ( but not a type qualifier ) .

364
365
366
367
368
369
370
371

Once we have seen one of these in a declaration ,


if a typedef name appears then it is being redeclared .

typespec : TYPESPEC
| structsp
| TYPENAME
| TYPEOF ' ( ' expr ' ) '
| TYPEOF ' ( ' typename ' ) '

372
373
374
375
376
377
378

/ A typespec that is a reserved word , or a type qualifier .


/
typespecqual_reserved : TYPESPEC
| TYPE_QUAL
| structsp

379
380
381
382
383

initdecls :
initdcl
| initdecls ' , ' initdcl

384
385
386
387
388

notype_initdecls :
notype_initdcl
| notype_initdecls ' , ' initdcl

389
390
391

maybeasm :

/ empty /
| ASM_KEYWORD ' ( ' string ' ) '
;

392
393
394
395
396
397
398
399
400
401
402
403
404

initdcl :

declarator maybeasm maybe_attribute '= ' init

| declarator maybeasm maybe_attribute


;

notype_initdcl :
notype_declarator maybeasm maybe_attribute '= ' init
| notype_declarator maybeasm maybe_attribute

210

405
406
407
408

maybe_attribute :
/ empty /
| attributes

409
410
411
412
413

attributes :
attribute
| attributes attribute

414
415
416
417

attribute :
ATTRIBUTE ' ( ' ' ( ' attribute_list ' ) ' ' ) '

418
419
420
421
422

attribute_list :
attrib
| attribute_list ' , ' attrib

423
424
425
426
427
428
429
430

attrib :
/ empty /
| any_word
| any_word ' ( ' IDENTIFIER ' ) '
| any_word ' ( ' IDENTIFIER ' , ' nonnull_exprlist ' ) '
| any_word ' ( ' exprlist ' ) '

431
432
433
434
435
436

/ This still leaves out most reserved keywords ,


shouldn ' t we include them ? /
any_word :

437
439
440
441
442
443
444
445
446
447
448
449

identifier

| SCSPEC
| TYPESPEC
| TYPE_QUAL
;

438

/ Initializers .
init :

` init ' is the entry point .

expr_no_commas
| ' { ' initlist_maybe_comma ' } '
| error

211

450
451
452
453
454

/ ` initlist_maybe_comma ' is the guts of an initializer in braces .


/
initlist_maybe_comma :
/ empty /
| initlist1 maybecomma
;

455
456
457

initlist1 :

458
460
461
462
463
464
465
466
467

/ ` initelt ' is a single element of an initializer .


It may use braces . /
initelt :
designator_list '= ' initval
| designator initval
| identifier ' : '
initval

468

| initval
;

469
470
471
472

initval :

473
474
475
476
477
478
479
480

483

designator :

' . ' identifier


/ These are for labeled elements .

484
485
486
487
488
489
490
491
492

' { ' initlist_maybe_comma ' } '


| expr_no_commas
| error
;

designator_list :
designator
| designator_list designator

481
482

initelt

| initlist1 ' , ' initelt


;

459

The syntax for an array element


initializer conflicts with the syntax for an Objective C message ,
so don ' t include these productions in the Objective C grammar .

| ' [ ' expr_no_commas ELLIPSIS expr_no_commas ' ] '


| ' [ ' expr_no_commas ' ] '
;

nested_function :

212

493
494
495

declarator old_style_parm_decls

/ This used to use compstmt_or_error .


That caused a bug with input ` f ( g ) int g { } ' ,
where
which
There
which

496
497
498
499
500

501
502
503
504

notype_nested_function :
notype_declarator old_style_parm_decls compstmt

505
506
507
508
509
510
511
512

/ Any kind of declarator ( thus , all declarators allowed


after an explicit typespec ) . /
declarator :
after_type_declarator
| notype_declarator

513
514
515
516
517
518
519
520
521
522
523
524

/ A declarator that is allowed only after an explicit typespec .


/
after_type_declarator :
' ( ' after_type_declarator ' ) '
| after_type_declarator ' ( ' parmlist_or_identifiers
%prec ' . '
| after_type_declarator ' [ ' expr ' ] ' %prec ' . '
| after_type_declarator ' [ ' ' ] ' %prec ' . '
| ' ' type_quals after_type_declarator %prec UNARY
| attributes after_type_declarator
| TYPENAME

525
526
527
528

/ Kinds of declarator that can appear in a parameter list


in addition to notype_declarator . This is like after_type_declarator

529
530
531
532
533
534
535

the use of YYERROR1 above caused an error


then was handled by compstmt_or_error .
followed a repeated execution of that same rule ,
called YYERROR1 again , and so on . /
compstmt

but does not allow a typedef name in parentheses as an identifier


( because it would conflict with a function with that typedef as arg ) .

parm_declarator :
parm_declarator ' ( ' parmlist_or_identifiers
| parm_declarator ' [ ' ' ' ' ] ' %prec ' . '
| parm_declarator ' [ ' expr ' ] ' %prec ' . '

213

%prec ' . '

536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580

|
|
|
|
;

parm_declarator ' [ ' ' ] ' %prec ' . '


' ' type_quals parm_declarator %prec UNARY
attributes parm_declarator
TYPENAME

/ A declarator allowed whether or not there has been


an explicit typespec . These cannot redeclare a typedefname .
/
notype_declarator :
notype_declarator ' ( ' parmlist_or_identifiers
| ' ( ' notype_declarator ' ) '
| ' ' type_quals notype_declarator %prec UNARY
| notype_declarator ' [ ' ' ' ' ] ' %prec ' . '
| notype_declarator ' [ ' expr ' ] ' %prec ' . '
| notype_declarator ' [ ' ' ] ' %prec ' . '
| attributes notype_declarator
| IDENTIFIER

%prec ' . '

struct_head :
STRUCT
| STRUCT attributes

union_head :
UNION
| UNION attributes

enum_head :

ENUM

| ENUM attributes
;
structsp :

|
|
|
|
|
|
|
|

struct_head identifier ' { ' component_decl_list ' } ' maybe_attribute


struct_head ' { ' component_decl_list ' } ' maybe_attribute
struct_head identifier
union_head identifier ' { ' component_decl_list ' } ' maybe_attribute
union_head ' { ' component_decl_list ' } ' maybe_attribute
union_head identifier
enum_head identifier ' { ' enumlist maybecomma_warn ' } ' maybe_attribute
enum_head ' { ' enumlist maybecomma_warn ' } ' maybe_attribute
enum_head identifier

214

581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626

;
maybecomma :

/ empty /
| ','
;

maybecomma_warn :
/ empty /

| ','
;

component_decl_list :
component_decl_list2
| component_decl_list2 component_decl

component_decl_list2 :
/ empty /
| component_decl_list2 component_decl ' ; '
| component_decl_list2 ' ; '

/ There is a shiftreduce conflict here , because ` components ' may


start with a ` typename ' . It happens that shifting ( the default resolution )
does the right thing , because it treats the ` typename ' as part of
a ` typed_typespecs ' .
It is possible that this same technique would allow the distinction
between ` notype_initdecls ' and ` initdecls ' to be eliminated .
But I am being cautious and not trying it . /
component_decl :
typed_typespecs components
| typed_typespecs
| nonempty_type_quals components
| nonempty_type_quals
| error
| EXTENSION component_decl

components :
component_declarator
| components ' , ' component_declarator

component_declarator :

215

declarator maybe_attribute

627

| declarator ' : ' expr_no_commas maybe_attribute


| ' : ' expr_no_commas maybe_attribute
;

628
629
630
631
632

/ We chain the enumerators in reverse order .

633
634
635
636
637

They are put in forward order where enumlist is used .


( The order used to be significant , but no longer is so .
However , we still maintain the order , just to be clean . )

enumlist :

638
640
641
642
643
644
645

enumerator :
identifier
| identifier '= ' expr_no_commas

646
647
648
649
650

typename :
typed_typespecs absdcl
| nonempty_type_quals absdcl

651
652
653

absdcl :

654
655
656
657
658
659
660

663

type_quals :

/ empty /
| type_quals TYPE_QUAL
;

664
665
666
667
668
669
670
671

/ an absolute declarator /
/ empty /
| absdcl1
;

nonempty_type_quals :
TYPE_QUAL
| nonempty_type_quals TYPE_QUAL

661
662

enumerator

| enumlist ' , ' enumerator


| error
;

639

absdcl1 :

/ a nonempty absolute declarator /


' ( ' absdcl1 ' ) '
/ ` ( typedef ) 1 ' is ` int ' .
| ' ' type_quals absdcl1 %prec UNARY
216

| ' ' type_quals %prec UNARY


| absdcl1 ' ( ' parmlist %prec ' . '
| absdcl1 ' [ ' expr ' ] ' %prec ' . '
| absdcl1 ' [ ' ' ] ' %prec ' . '
| ' ( ' parmlist %prec ' . '
| ' [ ' expr ' ] ' %prec ' . '
| ' [ ' ' ] ' %prec ' . '
/ ??? It appears we have to support attributes here , however
using prefix_attributes is wrong . /
| attributes absdcl1
;

672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688

/ at least one statement , the first of which parses without error .


/
/ stmts is used only after decls , so an invalid first statement
is actually regarded as an invalid decl and part of the decls .
/
stmts :

689
691
692
693
694
695

stmt_or_labels :
stmt_or_label
| stmt_or_labels stmt_or_label
| stmt_or_labels errstmt

696
697
698

xstmts :

699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714

stmt_or_labels

690

errstmt :

/ empty /
| stmts
;
;

error ' ; '

/ Read zero or more forwarddeclarations for labels


that nested functions can jump to . /
maybe_label_decls :
/ empty /
| label_decls
;
label_decls :
label_decl

217

715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756

| label_decls label_decl
;
label_decl :
LABEL identifiers_or_typenames ' ; '

/ This is the body of a function definition .

It causes syntax errors to ignore to the next openbrace .


/
compstmt_or_error :
compstmt
| error compstmt

compstmt : '{ ' } '


| '{ maybe_label_decls decls xstmts ' } '
| '{ maybe_label_decls error ' } '
| '{ maybe_label_decls stmts ' } '

/ Value is number of statements counted as of the closeparen .


/
simple_if :
if_prefix lineno_labeled_stmt

/ Make sure c_expand_end_cond is run once


for each call to c_expand_start_cond .
Otherwise a crash is likely . /
| if_prefix error
;
if_prefix :

IF ' ( ' expr ' ) '

/ This is a subroutine of stmt .


It is used twice , once for valid DO statements

and once for catching errors in parsing the end test .


/
do_stmt_start :
DO lineno_labeled_stmt WHILE

lineno_labeled_stmt :
stmt

218

757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797

| label lineno_labeled_stmt
;
stmt_or_label :
stmt
| label

/ Parse a single real statement , not including any labels .


/
stmt :
compstmt
| expr ' ; '
| simple_if ELSE lineno_labeled_stmt
| simple_if
%prec IF
| simple_if ELSE error
| WHILE ' ( ' expr ' ) ' lineno_labeled_stmt
| do_stmt_start ' ( ' expr ' ) ' ' ; '
/ This rule is needed to make sure we end every loop we start .
/
| do_stmt_start error
| FOR ' ( ' xexpr ' ; ' xexpr ' ; ' xexpr ' ) ' lineno_labeled_stmt
| SWITCH ' ( ' expr ' ) ' lineno_labeled_stmt
| BREAK ' ; '
| CONTINUE ' ; '
| RETURN ' ; '
| RETURN expr ' ; '
| ASM_KEYWORD maybe_type_qual ' ( ' expr ' ) ' ' ; '
/ This is the case with just output operands . /
| ASM_KEYWORD maybe_type_qual ' ( ' expr ' : ' asm_operands ' ) ' ' ; '
/ This is the case with input operands as well .
/
| ASM_KEYWORD maybe_type_qual ' ( ' expr ' : ' asm_operands ' : ' asm_operands
/ This is the case with clobbered registers as well .
/
| ASM_KEYWORD maybe_type_qual ' ( ' expr ' : ' asm_operands ' : '
asm_operands ' : ' asm_clobbers ' ) ' ' ; '
| GOTO identifier ' ; '
| GOTO ' ' expr ' ; '
| '; '
;

/ Any kind of label , including jump labels and case labels .


ANSI C accepts labels only before statements , but we allow them
also at the end of a compound statement . /
219

798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839

label :

CASE expr_no_commas ' : '


| CASE expr_no_commas ELLIPSIS expr_no_commas ' : '
| DEFAULT ' : '
| identifier ' : ' maybe_attribute
;
/ Either a typequalifier or nothing . First thing in an ` asm ' statement .
/
maybe_type_qual :
/ empty /
| TYPE_QUAL
;

xexpr :

/ empty /
| expr
;

/ These are the operands other than the first string and colon
in asm (" addextend %2,%1": "=dm " ( x ) , "0" ( y ) , " g " ( x ) )
/
asm_operands : / empty /
| nonnull_asm_operands
;
nonnull_asm_operands :
asm_operand
| nonnull_asm_operands ' , ' asm_operand

asm_operand :
STRING ' ( ' expr ' ) '

asm_clobbers :
string
| asm_clobbers ' , ' string

/ This is what appears inside the parens in a function declarator .


Its value is a list of . . . _TYPE nodes . /
parmlist :
;

parmlist_1

840

220

841
842
843
844
845

parmlist_1 :
parmlist_2 ' ) '
| parms ' ; '
parmlist_1
| error ' ) '

846
847
848
849
850
851
852
853

/ This is what appears inside the parens in a function declarator .

Is value is represented in the format that grokdeclarator expects .


/
parmlist_2 : / empty /
| ELLIPSIS
| parms
| parms ' , ' ELLIPSIS

854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884

parms :

parm
| parms ' , ' parm

/ A single parameter declaration or parameter type name ,


as found in a parmlist . /
parm :
|
|
|
|
;

typed_declspecs parm_declarator maybe_attribute


typed_declspecs notype_declarator maybe_attribute
typed_declspecs absdcl maybe_attribute
declmods notype_declarator maybe_attribute
declmods absdcl maybe_attribute

/ This is used in a function definition

where either a parmlist or an identifier list is ok .


Its value is a list of . . . _TYPE nodes or a list of identifiers .

/
parmlist_or_identifiers :
parmlist_or_identifiers_1

parmlist_or_identifiers_1 :
parmlist_1
| identifiers ' ) '

/ A nonempty list of identifiers .


identifiers :
221

IDENTIFIER
| identifiers ' , ' IDENTIFIER

885
886

887
888
889
890

/ A nonempty list of identifiers , including typenames .


identifiers_or_typenames :
identifier
| identifiers_or_typenames ' , ' identifier

891
892
893
894

%%

13.4 Les sources de ppcm


13.4.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/ ppcm . h

constantes arbitraires , variables globales , prototypes de fonctions


/
# define MAXNOM 3000
/ nbre max . de noms ( variables+fonctions ) /
# define MAXEXPR 1000
/ nbre max . d ' expresions par fonction /

frame : argn . . . arg1 @retour fp locale1 locale2 . . . localeN >adresses basses


/
extern int posexpr ;
/ index via %ebp de la prochaine expression /
extern int incr ;
/ la suivante ds la pile : +4 ou 4 /

dans expr . c
/
struct expr {
int position ;
char nom ;

2
3
4
5

/ en memoire , son index via %ebp /


/ le nom de la variable ( le cas echeant ) /

};

struct expr fairexpr ( char ) ;


struct expr exprvar ( char ) ;
void reinitexpr ( void ) ;

13.4.2
1

Le chier de dclarations ppcm.h

Le chier expr.c

/ expr . c

gestion des expressions , de la memoire et des registres de ppcm


/
# include <stdio . h>
# include " ppcm . h "

222

6
7
8
9
10
11
12
13

struct expr expr [ MAXEXPR ] ;


int nexpr ;

/ fairexpr fabrique une expression ( parametre , argument ou temporaire ) /


struct expr
fairexpr ( char nom ){
register struct expr e ;

14
15
16
17
18
19
20
21
22
23
24
25

27
28
29
30
31
33
34
35
36
37
38

e = &expr [ nexpr ++];


e>position = posexpr ;
e>nom = nom ;
posexpr += incr ;
return e ;

/ exprvar renvoie l ' expression qui designe la variable /


struct expr
exprvar ( char s ){
register struct expr e , f ;

26

32

for ( e = & expr [ 0 ] , f = e + nexpr ; e < f ; e += 1)


if (/ e>nom != NULL && / e>nom == s )
return e ;
fprintf ( stderr , " Erreur , variable %s introuvable \ n " , s ) ;
return & expr [ 0 ] ;

/ reinitexpr
void
reinitexpr ( ) {
nexpr = 0 ;

2
3
4
5
6
7
8
9
10

a la fin d ' une fonction , ni expression ni registre /

13.4.3
1

/ les expressions /
/ le nombre d ' expressions /

L'analyseur lexical dans le chier pccm.l

/ ppcm . l

analyseur lexical . representation unique des chaines de caracteres


/
char chaine ( ) ;
int lineno = 1 ;

%%
" if "
" while "
" else "
" int "

{
{
{
{

return
return
return
return

223

YIF ; }
YWHILE ; }
YELSE ; }
YINT ; }

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

" return "


{ return YRETURN ; }
[ azAZ_ ] [ azAZ0 9_ ] { yylval . c = chaine ( yytext ) ; return YNOM ; }
'. '
{ yylval . i = yytext [ 1 ] ; return YNUM ; }
[0 9]+
{ yylval . i = atoi ( yytext ) ; return YNUM ; }
[ + /%=();{} ,]
{ return yytext [ 0 ] ; }
!=
{ return YNEQ ; }
[ \t\f ]
;
\n
lineno += 1 ;
.
{ fprintf ( stderr , " yylex : (%c )\ n " , yytext [ 0 ] ) ; }
" / " ( [ ^ ] | ( " " + [ ^ / ] ) ) " " + " / " { ; / Commentaire / }
%%
static char chaines [ MAXNOM ] ;
static int nchaines = 0 ;
/ chaine renvoie une representation unique de la chaine argument /
char
chaine ( char s ){
register char p , f ;

29

for ( p = & chaines [ 0 ] , f = p + nchaines ; p < f ; p += 1)


if ( strcmp ( p , s ) == 0)
return p ;
if ( nchaines == MAXNOM ){
fprintf ( stderr , " Pas plus de %d noms \ n " , MAXNOM ) ;
exit ( 1 ) ;

30
31
32
33
34
35

36
37
38
39
40
41
42
43
44

if ( ( p = ( char ) malloc ( strlen ( s ) + 1 ) ) == NULL )


nomem ( ) ;
strcpy ( p , s ) ;
nchaines += 1 ;
return p ;

yywrap ( ) { return 1 ; }

13.4.4
1
2
3
4
5
6
7
8

Le grammaire et la gnration de code dans le chier ppcm.y

/ ppcm . y

parseur de ppcm : le code est genere au fur et a mesure du parsing


/

%{
# include <stdio . h>
char chainop ;
int posexpr , incr ;

/ assembleur pour l ' operateur courant /


/ deplacement ds la pile pour les variables /
224

9
10
11
12
13
14
15
16
17

char fonction ;

# include " ppcm . h "


# define YYDEBUG 1
%}
%union {
int i ;

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

};

char c ;
struct expr

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

/ constantes , etiquettes et nbre d ' arg . /


/ variables et fonctions /
/ expressions /

e;

%token <c> YNOM


%token <i> YNUM
%token YINT YIF YELSE YWHILE YRETURN
%type <i> ifdebut . listexpr
%type <e> expr
%right '= '
%nonassoc YNEQ
%left '+ ' ' '
%left ' ' ' / ' '% '
%left FORT
%%

programme : / rien /
| programme fonction

/ point d ' entree : rien que des fonctions /

. listinstr : / rien /
| . listinstr instr
;

/ liste d ' instructions /

38
39

/ le nom de la fonction courante /

fonction

: YNOM ' ( '


{ posexpr = 8 ; incr = 4 ; }
. listarg ' ) ' ' { '
{ posexpr = 4; incr = 4; }
. listvar
{ printf ( " . text \ n \ t . align 16\ n . globl %s \ n " , $1 ) ;
printf (" deb%s : \ n " , $1 ) ;
fonction = $1 ;
}
. listinstr ' } '
225

55
56
57
58
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73
74
75
76

84
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

/ la liste des noms : arguments ou variables /

{ fairexpr ( $1 ) ; }
| listnom ' , ' YNOM
{ fairexpr ( $3 ) ; }
;

80

85

/ liste des variables de la fonction /

listnom : YNOM

79

83

/ liste des arguments a fournir a l ' appel /

. listvar : / rien /
| YINT listnom ' ; '
;

78

82

reinitexpr ( ) ;

. listarg : / rien /
| listnom
;

77

81

/ epilogue /
printf (" fin%s : \ n " , $1 ) ;
printf ("\ tmovl %%ebp ,%%esp \ n \ tpopl %%ebp \ n \ tret \ n " ) ;
/ proloque /
printf ("% s : \ n " , $1 ) ;
printf ("\ tpushl %%ebp \ n \ tmovl %%esp ,%%ebp \ n " ) ;
printf ("\ tsubl $%d,%%esp \ n " , posexpr 4);
printf ("\ tjmp deb%s \ n " , $1 ) ;

instr

: '; '
/ Toutes les instructions /
| ' { ' . listinstr ' } '
| expr ' ; '
{ ; }
| ifdebut instr
{ printf (" else%d : \ n " , $1 ) ; }
| ifdebut instr YELSE
{ printf ("\ tjmp fin%d \ n " , $1 ) ;
printf (" else%d : \ n " , $1 ) ;
}
instr

{ printf (" fin%d : \ n " , $1 ) ; }


| YWHILE ' ( '
{ printf (" debut%d : \ n " , $<i>$ = label ( ) ) ; }
expr ' ) '
{ printf ("\ tcmpl $0 ,% d(%%ebp )\ n " , $4>position ) ;
printf ("\ tje fin%d \ n " , $<i >3); }
instr

{ printf ("\ tjmp debut%d \ n " , $<i >3);


226

101

102

| YRETURN expr ' ; '


{ printf ("\ tmovl %d(%%ebp ),%% eax \ n " , $2>position ) ;
printf ("\ tjmp fin%s \ n " , fonction ) ;
}
;

103
104
105
106
107
108
109
110
111

ifdebut : YIF ' ( ' expr ' ) '


{ printf ("\ tcmpl $0 ,% d(%%ebp )\ n " , $3>position ) ;
printf ("\ tje else%d \ n " , $$ = label ( ) ) ;

112

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

printf (" fin%d : \ n " , $<i >3);

expr

/ toutes les expressions /

: YNOM

{ $$ = exprvar ( $1 ) ; }
| ' ( ' expr ' ) '
{ $$ = $2 ; }
| YNUM
{ $$ = fairexpr ( NULL ) ;
printf ("\ tmovl $%d ,% d(%%ebp )\ n " , $1 , $$>position ) ;
}
| YNOM '= ' expr
{ printf ("\ tmovl %d(%%ebp ),%% eax \ n " , $3>position ) ;
printf ("\ tmovl %%eax ,% d(%%ebp )\ n " , exprvar ( $1)> position ) ;
$$ = $3 ;
}
| ' ' expr %prec FORT
{ printf ("\ tmovl %d(%%ebp ),%% eax \ n " , $2>position ) ;
printf ("\ tnegl %%eax \ n " ) ;
$$ = fairexpr ( NULL ) ;
printf ("\ tmovl %%eax ,% d(%%ebp )\ n " , $$>position ) ;
}
| expr ' ' expr
{ chainop = " subl " ;
bin :
printf ("\ tmovl %d(%%ebp ),%% eax \ n " , $1>position ) ;
printf ("\ t%s \ t%d(%%ebp ),%% eax \ n " , chainop , $3>position ) ;
$$ = fairexpr ( NULL ) ;
printf ("\ tmovl %%eax ,% d(%%ebp )\ n " , $$>position ) ;
}
| expr '+ ' expr
{ chainop = " addl " ; goto bin ; }
| expr ' ' expr
{ chainop = " imull " ; goto bin ; }
| expr YNEQ expr
227

147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192

{ chainop = " subl " ; goto bin ; }


| expr ' / ' expr
{ chainop = "%eax " ;
div : printf ("\ tmovl %d(%%ebp ),%% eax \ n \ tcltd \ n " , $1>position ) ;
printf ("\ tidivl \ t%d(%%ebp ),%% eax \ n " , $3>position ) ;
$$ = fairexpr ( NULL ) ;
printf ("\ tmovl %s ,% d(%%ebp )\ n " , chainop , $$>position ) ;
}
| expr '% ' expr
{ chainop = "%edx " ; goto div ; }
| YNOM ' ( ' . listexpr ' ) '
{ printf ("\ tcall %s \ n " , $1 ) ;
if ( $3 != 0)
printf ("\ taddl $%d,%%esp \ n " , $3 4 ) ;
$$ = fairexpr ( NULL ) ;
printf ("\ tmovl %%eax ,% d(%%ebp )\ n " , $$>position ) ;
}
;
/ liste d ' expressions ( appel de fonction ) /
. listexpr : / rien /
{ $$ = 0 ; }
| expr
{ $$ = 1 ; printf ("\ tpushl %d(%%ebp )\ n " , $1>position ) ; }
| expr ' , ' . listexpr
{ $$ = $3 + 1 ; printf ("\ tpush %d(%%ebp )\ n " , $1>position ) ; }
;
%%
int
main ( ) {
yyparse ( ) ;
return 0 ;

/ label renvoie un nouveau numero d ' etiquette a chaque appel /


label ( ) {
static int foo = 0 ;
return foo ++;

# include " lex . yy . c "

/ yylex et sa clique /

yyerror ( char message ){


extern int lineno ;
extern char yytext ;
fprintf ( stderr , "%d : %s at %s \ n " , lineno , message , yytext ) ;

228

193
194
195
196
197
198

}
nomem ( ) {
fprintf ( stderr , " Pas assez de memoire \ n " ) ;
exit ( 1 ) ;

229

You might also like