FAQ du Projet Programmation 1

Je consigne ci-dessous quelques réponses à des questions importantes qu'on m'a posées, histoire que tout le monde soit au courant. N'hésitez pas à m'envoyer d'autres questions, déja posées ou pas, à ajouter ici.

ERRATUM : bug dans Exemple/sieve.c

Le malloc(4*n) qu'on trouve dans ce fichier devrait être malloc(8*n). Sans cette correction c'est "normal" que ce code source provoque une erreur de segmentation en 64 bits. C'est un bug historique datant de l'époque où ce projet était en 32 bits: désolé!

Est-ce que C-- permet ceci ou cela?

Rappel: en C une l-value est ce qu'on peut trouver sous un opérateur d'assignation ou de pre/post-incrément/décrément. Dans l'AST C-- du projet, l'assignation est explicitement restreinte aux l-values x et x[e] (où x est une variable et e une expression). Pour les incréments/décréments, l'AST n'est pas précis: vous pouvez faire la même hypothèse dans votre code.

On m'a fait remarquer à juste titre que la restriction des l-values est un peu artificielle. En fait, si l'on lit les règles de sémantique opérationnelle du poly en ignorant (au moins) une remarque informelle, elles expliquent comment traiter des l-values beaucoup plus générales, par exemple (x+3*t[i++])[j] ou simplement t[i][j]. Mais cela n'est pas demandé pour votre projet.

Que se passe-t-il quand une fonction C renvoie un booléen, entier 32 bits, etc. dans %rax?

C'est la convention d'appel qui doit répondre à cette question, mais je n'arrive pas à trouver de version convaincante qui garantisse quoi que ce soit sur les bits de %rax au delà de la taille de la valeur retournée. Par conséquent on ne peut faire aucune hypothèse! Si une fonction retourne un entier 32 bits, alors %eax est correct mais on ne sait rien sur la moitié haute des bits de %rax.

Cela n'est pas si choquant dans la mesure où un vrai compilo connait les types et va en général manipuler %eax directement s'il sait qu'un entier 32 bits a été retourné. Mais vous ne pouvez pas faire ça dans votre compilo, où on ne "voit" que du 64 bit.

La solution proposée est de considérer que toutes les fonctions déclarées dans votre fichier renvoient des entiers 64 bits, et que toutes les autres renvoie du 32 bits, sauf quelques exceptions comme malloc. En fonction de cette heuristique, vous pourrez alors étendre %eax sur 64 bits avec l'instruction movslq.

(Merci à Aliaume Lopez.)

La fonction main est-elle spéciale de quelque façon?

Dans votre compilo la fonction main est une fonction comme une autre, elle peut être déclarée n'importe où dans le fichier, et même pas déclarée du tout. C'est à la liaison que main prend son sens spécifique en tant que point d'entrée du programme.

Par conséquent, je m'attends à ce que votre compilateur produise une sortie correcte sur un fichier sans fonction main, si on l'utilise avec l'option -E.

(Merci à Noël Nadal, pour m'avoir reposé cette question qui est beaucoup ressortie en TP.)

Faut-il aligner %rsp sur 16 octets?

Oui car cela fait partie de la convention d'appel x86-64 (voir spec, section 3.2.2, page 16)... même si ce point a été oublié dans le poly et la première version de l'aide-mémoire.

Il y a plusieurs moyens d'assurer l'alignement. N'hésitez pas à glaner des astuces sur le web ou à échanger entre vous sur ce point, comme sur les autres aspects très "cambouis" du projet.

Dois-je respecter la convention d'appel pour mes propres fonctions?

Oui! La convention n'est pas seulement là pour pouvoir appeler les fonctions de la librairie standard. Votre objectif est de faire comme dans un vrai compilateur, qui doit produire des fonctions qu'on puisse appeler selon la convention.

A propos de stderr, stdout...

Si une variable globale apparait dans l'AST et qu'elle n'a pas été définie dans le code, ce n'est pas une erreur: il faut considérer que la variable est définie dans la librairie standard. Typiquement, vous avez besoin de cela pour les std*.

Pour adresser ces variables globales de la libc, deux solutions: stderr et stderr(%rip). La seconde est un mode d'adressage "relatif" qui permet d'obtenir une instruction assembleur prenant moins de place en mémoire. Vous pouvez ignorer ces considérations et adopter une solution ou l'autre.

Mon compilateur ne se comporte pas comme gcc, est-ce un problème?

Pas forcément, vous compilez du C-- et pas du C. La sémantique de C-- est définie en certains endroits où C laisse des comportements non définis. Voir par exemple ici.

(Merci à Laurent Prosperi.)

Quelle différence entre call/ret et callq/retq?

Aucune, ce sont des synonymes. La version avec un q est l'appelation Intel originale et l'équivalent sans q semble être introduit par GCC.

Quelle différence entre jmp et jmpq?

N'utilisez que jmp, saut inconditionnel vu en cours. Cette instruction prend en argument une adresse "absolue", en général donnée par le biais d'un label.

Contrairement aux apparences jmpq n'est pas un alias, mais une variante du précédent permettant de sauter à une adresse calculée dynamiquement.

Pourquoi ça marche pas?

Comme d'habitude: utilisez un débogueur, relisez-vous, simplifiez ou reformuler votre test pour mettre le doigt sur le problème, allez dormir, etc.

En dehors de ces raisons habituelles, un problème assez vicieux s'est manifesté plusieurs fois: si vos instructions assembleur ne sont pas dans une section text alors le débogueur ne "voit" plus rien, les accès aux variables globales de la libc échouent, etc. C'est vicieux car ce problème syntaxique évident n'est jamais signalé en tant que tel comme une erreur par le compilateur.

Comment débugger mon compilateur?

Une technique s'applique directement: le printf, il n'y a pas de honte. Sinon, on a deux autres outils à disposition, mais il faut compiler avec l'option de débuggage (-g, à la compilation et à la liaison). Pour cela:

Avantage immédiat, vous pouvez maintenant obtenir une backtrace quand votre compilateur échoue en remontant une exception. Pour cela, il faut le lancer avec une variable d'environnement bien fichue: OCAMLRUNPARAM=b ./mcc fichier.c. Notez que les backtraces ne sont pas toujours aussi détaillées que dans d'autres langages, car OCaml optimise les appels terminaux pour ne pas garder de stack frames inutiles.

Vous pouvez aussi utiliser le débuggeur bytecode ocaml, qui permet de revenir dans le temps. On lance simplement: ocamldebug ./mcc fichier.c. Mode d'emploi ici.