III - La machine virtuelle

 

La capacité à exécuter une application Java sur une plate-forme donnée est obligatoirement conditionnée par l'existence d'une implémentation de la Machine Virtuelle Java (ou JVM, Java Virtual Machine en anglais) sur cette dite plate-forme. Ce chapitre présente le rôle et le fonctionnement interne de la JVM.

 

1. Présentation

Nous avons vu, dans le chapitre précédent, qu'un compilateur Java générait un fichier .class contenant des bytecodes ou P-codes (P pour Program). Ces bytecodes ne peuvent pas être exécutés tel quels par le processeur de la machine sur laquelle on désire lancer un programme Java compilé. Il est nécessaire d'introduire une couche logicielle ayant pour principale fonction de traduire les bytecodes en instructions exécutables par le processeur de la machine hôte. C'est cette couche que l'on appelle la Machine Virtuelle Java (JVM). On peut donc dire que, porter Java sur une plate-forme, c'est simplement porter la machine virtuelle Java. Sun propose d'ailleurs sur son serveur web une spécification très précise destinée aux développeurs voulant effectuer un tel portage.

La JVM est en fait l'implémentation d'un processeur virtuel, disposant d'un jeu d'instructions propres. Chaque instruction est codée sur un octet et il en existe environ 200 actuellement. Ce nombre peut paraître important, à première vue, mais en fait les concepteurs de Java ont défini plusieurs instructions faisant les mêmes opérations mais pour des types de données différents (entier, réel, etc.).

Le jeu d'instructions de la JVM comporte un certain nombre d'instructions classiques, comme les opérations arithmétiques, logiques et les sauts conditionnels. Cependant, le processeur virtuel Java se distingue des processeurs "classiques" par le fait qu'il ne comprend aucun registre utilisateur c'est à dire des registres que le programmeur peut utiliser directement (le processeur Java dispose bien évidemment de registres spécialisés pour stocker le PC, Program Counter ou le SP, Stack Pointer). A la place, on utilise des variables locales numérotées à partir de 0. Cette caractéristique peut paraître étonnante au premier abord mais en réalité, elle est tout à fait logique : le bytecode Java devant être exécutable sur plusieurs plates-formes différentes, il n'était pas possible de définir un nombre de registres précis, sans risquer de rencontrer un processeur n'en disposant pas d'autant.

L'absence de registres implique l'utilisation intensive de la pile. En fait, à chaque fois que la JVM charge une classe afin de l'exécuter (nous en reparlerons juste après), elle crée un thread dédié. Chaque thread dispose de sa propre pile, celle-ci n'étant donc pas partagée entre plusieurs instances de classes en cours de traitement. Le processeur fait usage de la pile pour passer des arguments à des fonctions lors de leur appel, comme pour l'exécution d'opérations mathématiques. Le jeu d'instructions comporte donc un certain nombre de commandes permettant de gérer la pile.

La JVM dispose enfin d'instructions natives dédiées à des fonctionnalités spécifiques du langages Java, comme les threads, la création et la manipulation de tableaux et d'objets.

Avant d'aller plus loin, précisons un point. Bien que nous ayons surtout cité l'interpréteur Java livré dans un kit de développement, comme étant la première implémentation d'une machine virtuelle, il en existe en fait une autre, tout aussi importante : le navigateur web. En effet, quand on dit qu'un navigateur est compatible Java, cela veut tout simplement dire qu'il implémente une machine virtuelle Java.

 

2. Architecture de la machine virtuelle

Nous venons en fait de décrire qu'une partie de la JVM, à savoir, le moteur d'exécution (ou Execution Engine), chargé de traduire les bytecodes en instructions exécutables par le processeur de la machine hôte. Bien que ce moteur soit le cœur de la machine virtuelle, celle-ci comporte d'autres éléments comme le chargeur de classes (class loader) et le vérificateur de classes (class verifier), élément clé de la sécurité de la JVM, faisant l'objet d'un paragraphe dédié à la fin de ce chapitre.

La machine virtuelle Java constitue en réalité ce qu'on appelle un runtime Java. Le schéma ci-dessous présente l'architecture d'un runtime Java.

 

Détaillons chacun des composants présentés dans la figure précédente :

Les composants les plus sensibles de la Machine Virtuelle Java, dont nous venons de découvrir l'architecture interne, sont ceux qui assurent la sécurité de celle-ci (ou plutôt du système d'exploitation hôte). Examinons de plus près le fonctionnement de ces composants.

 

3. La sécurité

On peut distinguer plusieurs niveaux de sécurité :

La sécurité vis à vis d'un programme Java est assurée par plusieurs éléments qui interviennent à différents endroits du "chemin d'évolution" d'un programme, en commençant par son écriture et en terminant par son exécution.

Il est possible de définir trois niveaux de fonctionnalités permettant d'assurer la sécurité.

Le compilateur Java

Première barrière, le compilateur a pour rôle de vérifier qu'un programme Java n'effectue pas des instructions illégales comme tenter d'accéder à une donnée privée d'une autre classe ou essayer de modifier le contenu d'une adresse mémoire (car ce n'est pas autorisé en Java).

Le vérificateur de classes et de bytecodes

Le compilateur permet de s'assurer qu'aucune opération invalide n'est effectuée. Cependant, il faut savoir qu'il est tout à fait possible d'écrire un compilateur Javaà bytecode n'effectuant aucun contrôle, permettant ainsi de faire planter la JVM ou d'accéder à des données dont l'accès est interdit. Encore mieux, on peut également créer un assembleur permettant de générer du bytecode, sans qu'il soit nullement question du langage Java.


C'est pour éviter que des classes générées de cette façon ne provoquent des opérations invalides, que Sun a intégré un vérificateur de classes, exécuté sur toutes les classes chargées non localement par le chargeur de classes, lors de l'exécution d'un programme. Le vérificateur de classes vérifie la structure de celles-ci, puis exécute un vérificateur de bytecode.

Ce vérificateur va plus loin en examinant les bytecodes de façon à vérifier qu'il n'y a pas, par exemple, de sauts à des adresses invalides ou qu'on ne tente pas d'exécuter des opérations avec des types de données incompatibles. L'une des fonctions de ce vérificateur est également de s'assurer qu'un programme ne viendra pas saturer les ressources de la JVM, comme la pile, en exécutant un démonstrateur de théorèmes (theorem prover), visant à détecter d'éventuelles incohérences dans la sturcture des zones de données durant l'exécution répétée (i.e. dans une boucle) d'une même séquence d'instructions.

Le gestionnaire de sécurité

Dernier rempart, le gestionnaire de sécurité ou Security Manager (SM) a pour but de s'assurer qu'un programme n'accède pas, par exemple, à un fichier local alors qu'il n'en a pas le droit. Le Security Manager peut être redéfini par le programmeur d'une application indépendante d'un navigateur, mais il est impossible de faire une telle redéfinition dans une applet qui, par définition, vient d'une source distante dont l'honnêteté ne peut être assurée. C'est pourquoi une applet ne peut pas accéder au système de fichier local, ou créer un serveur sur la machine hôte.

Cependant, avec l'arrivée des applet signées, il devient possible d'envisager d'élargir les possibilités des applets, grâce à l'utilisation de clés chiffrées identifiant de manière absolue (ou presque) l'origine d'une classe. C'est ce système qui a été adopté par Microsoft pour assurer la sécurité des "applications" ActiveX, comme nous le verrons dans la deuxième partie de cette étude.