Machine virtuelle du C

Le C s’exécute au sein d’une « machine virtuelle » ou « machine parfaite ». Ce runtime nécessite de l’ordinateur certaines contraintes pour que le programme écrit en C puisse s’exécuter correctement.

L’ordinateur met à disposition une partie de ses ressources pour qu’un programme C puisse exécuter son runtime et les programmes qui le nécessitent.

Un programme C respectant les règles de la machine virtuelle aura toujours le même comportement visible quel que soit la machine où il est compilé.

Selon la machine ce runtime peut n’exploiter qu’une fraction des ses capacités, et les fabricants de matériel se plient à ce runtime en s’assurant que le langage C puisse s’exécuter dessus, quite a brider les capacités de leur machine.

Ou au contraire la machine peut avoir des difficultés à se plier aux exigences du C (opérations en virgule flottante, recursion, …), dans ce cas le fabricant peut proposer une version allégée du langage.

Cette machine pilote le processeur, pour piloter d’autres composants tels que le disque dur, la carte graphique, la carte reseau ou tout composant, on passe par un driver dont la modalité n’est pas couverte par le langage C, ou un langage de shader.

L’accès au disque dur est couvert par la librairie standart, l’accès au réseau est normalisé par le concept de sockets mais son implémentation est disparate.

L’accès à la carte graphique se fait par un protocole étrange au langage C et également disparate.

L’accès à la mémoire vive est intégrée au langage. Toute mémoire est considérée comme une extension du processeur.

Travail de la machine

Globalement son rôle est de donner l’illusion à notre programme qu’il s’exécute dans un système d’exploitation UNIX.

Voici quelques services que cette machine nous rend :

Ouvrir les flux d’entree et sortie

Lorsqu’on démarre un programme C, trois flux de type “UNIX” sont ouverts auquel on donne les numéros 0, 1 et 2, par commodité on leur assigne par convention les noms standart stdin, stdout et stderr. Par defaut le clavier est associé au flux stdin et un écran d’affichage de type terminal est associé au flux stdout. Si les programme est appelé via un shell qui gère la redirection de flux de la sorte

echo "toto" > mon_programme.exe > fichier.txt

Alors le texte “toto” sera present sur le flux 0 avant même qu’on ait tapé quoi que ce soit au clavier, et toute écriture sur le flux 1 sera écrit dans un fichier nommé fichier.txt plutot que sur une console.

Assigner une pile

Une pile est assignée au démarrage du programme pour y stocker des variables de travail. La taille de cette pile est laissée à la discrétion de l’implémentation.

Déborder cette pile peut constituer un cas de panne .

Les variables déclarées successivement sont placées les unes à la suite des autres dans cette pile et non aléatoirement. C’est à dire que trois variables déclarées successivement sur la pile ont des adresses qui se suivent dans l’ordre où elles ont été déclarées et avec des valeurs d’adresse relativement proches.

Il peut y avoir du jeu entre des variables de pile, sauf si celles-ci sont déclarées dans un tableau. Lorsqu’une variable de pile sort de sa durée de vie, il n’est pas requis qu’aucune valeur y soit écrite (la valeur zero notamment).

Appels de fonction

Une autre pile est constituée pour garder en mémoire les lieux d’appels de fonctions pour y retourner une fois qu’un appel de fonction se termine. Comme pour la pile de variables, cette pile d’appels est limitée en taille, et appeller trop de fonctions récursivement peut causer une panne.

Généricité du C

Généralement le C demande certaines contraintes à l’environnement dans la mesure où ces contraintes ne soient néfastes à la performance optimale de celui ci.

Si une contrainte peut empêcher certaines optimisations dans une architecture donnée, le C ne définit pas de contrainte. Si un programme nécessite qu’un comportement non contraint du langage soit exécuté d’une manière précise, alors le comportement du programme devient non défini et n’obéït plus aux règles du langage.

Contraintes

Chaque instruction doit s’exécuter sequentiellement, l’instruction précédente doit être completement terminée avant que la suivante s’exécute. Le fil doit respecter les instructions de controle .

Le programme doit avoir a disposition une pile où stocker un nombre quelconque de variables.

On doit pouvoir récupérer l’adresse de n’importe quel objet, deux objets distinct doivent avoir des adresses différentes, cette adresse doit etre un nombre entier, convertible vers un nombre entier, et un nombre entier quelconque peut etre utilisé comme adresse à toute opération d’écriture à travers l’usage d’un pointeur .

Il est possible d’obtenir l’adresse de n’importe quel objet et declarer un pointeur vers celui-ci, sauf si cet objet est déclaré register ou fait partie d’un bit-field .

Dans un tableau les objets doivent être alignés de manière contigue sans aucun jeu entre eux, vers le sens croissant de l’espace d’adressage.

Dans une structure les objets doivent être placés dans l’ordre où ils sont déclarés, le premier objet se trouvant à l’adresse zero de la structure.

L’alignement des éléments d’une structure peut être contraint (depuis le C99 ).

L’appel recursif de fonctions doit être possible et optionellement les sauts non locaux doivent être possibles.

Les opérations artihmétiques d’adition, soustraction, multiplication et division euclidienne sont possibles entre nombres entiers.

On peut connaitre la partie entière et le reste d’une division euclidiène entre deux nombres entiers.

Les opéartions sur des nombres a virgules doivent être possible en simple et double précision.

Non contraintes

La taille de la pile de variables.

La taille ocupée en mémoire par les nombres entiers ou flottants, c’est à dire la taille d’un int, float

Le boutisme des nombres.

Le jeu de caractères du texte.

La représentation des nombres négatifs (et le comportement en cas de dépassement de capacité) (avant le C23 )

Le jalonement par défaut des éléments d’une structure (leur agencement lui est contraint).

L’ordre d’évaluation des opérandes d’une opération binaire ou ternaire.

L’ordre d’évaluation des paramètres d’une fonction.

Le comportement du programme en cas de boucle infinie .

Une division arityhmétique par zero.

La modification d’une chaine de caractères .