Introduction

Ce didacticiel est destiné aux utilisateurs sachant écrire un peu de code de la Simple Virtual Machine.

Dans ce didacticiel, vous allez explorer le processeur de la machine virtuelle.

Le temps de lecture de ce didacticiel est estimé à 20 minutes.

Mise en place

Pour commencer, créez le canevas de l'application dans le fichier exécutable processeur.svm en utilisant ce code :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
	END
END

Puis lancez l'application en mode débugueur. La fenêtre du processeur montre :

Processor
Processor - K main - P application
State:
Next instruction: <main:1/1>
Current instruction: <main:1/0>
Code
Current memory: &0*0
Allocated memory:
Defined aliases:
Local interruptions:
Cascaded local interruptions:
Flags:
Cascaded flags:
Return stack:
Global interruptions:
Waiting interruptions:

Etat du processeur

L'état du processeur contient toutes les valeurs courantes nécessaires à l'exécution du code. Ces valeurs sont la cible privilégiée des opérations impliquant le processeur.

Registres

Les registres sont des petits emplacements mémoire contenant chacun une seule valeur d'un type fixé par l'architecture de la machine. Les opérations sur ces valeurs sont également fixées par l'architecture.

Prochaine instruction

Le premier registre contient un symbole. Ce symbole permet d'indiquer au processeur quelle sera l'instruction a exécuter lorsque l'instruction courante sera terminée.

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:memory INT/i
		0 -> &i
		:shift &i
		:goto l
		:shift &i
	:label l
		:shift &i
	END
END

Lancez l'application en mode débugueur, puis exécutez le programme instruction par instruction. Observez l'évolution du registre de prochaine instruction :

  1. il est initialisé sur la première instruction du code,
  2. à chaque instruction, la partie locale du symbole est incrémentée de manière totalement autonome par le processeur lui-même,
  3. une instruction de saut comme :goto modifie la valeur de ce registre pour réaliser un saut.

Pointeur de mémoire courante

Ce registre contient un pointeur qui désigne une zone particulière de la mémoire, dont l'utilisation principale est d'indiquer où se trouvent les paramètres d'une fonction.

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:memory INT/i
		0 -> &i
		:call f i
		:shift &i
		:shutdown
	:label f
		:shift &P
		:return
	END
END

Lancez l'application en mode débugueur, puis exécutez le programme instruction par instruction. Observez l'évolution du registre de mémoire courante :

  1. en dehors de la fonction, il vaut &0*0,
  2. à l'intérieur de la fonction, il vaut &0*1, qui est la valeur du pointeur placé en second argument de l'instruction :call.

Le processeur ne contient que deux registres :

Informations locales

En plus des registres directement utiles à l'exécution du code, l'état du processeur contient plusieurs informations dont la majorité a une portée limitée à l'exécution du code de la fonction courante.

Instruction courante

Le processeur conserve dans l'état le symbole pointant sur l'instruction actuellement en cours d'exécution. Cette information n'est pas utilisée pour l'exécution du code et ne sert qu'aux investigations par le developpeur ou pour accéder au code dans le débugueur.

Mémoire allouée et alias

Pour pouvoir libérer la mémoire à chaque retour de fonction, le processeur doit conserver une trace de la mémoire locale allouée au sein d'une fonction, ainsi que les alias définis.

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:memory INT/i
		:call f i
		:local b
		:shutdown
	:label f
		:memory STR/s
		:memory GLOBAL BLN/b
		:return
	END
END

Lancez l'application en mode débugueur, puis exécutez le programme instruction par instruction. Observez l'évolution des champs de mémoire allouée et d'alias définis dans la fenêtre du processeur :

  1. la première instruction :memory ajoute le pointeur &0*1 à la mémoire allouée et l'alias i aux alias définis,
  2. la seconde, à l'intérieur de la fonction, ajoute le pointeur &1*1 à la mémoire allouée et l'alias s aux alias définis,
  3. la troisième, toujours dans la fonction, n'ajoute rien, car la zone mémoire allouée est ici globale,
  4. l'instruction :return ne trouvant dans l'état du processeur que les éléments ajoutés par la seconde instruction :memory de libère que cette mémoire,
  5. l'instruction :local rajoute à l'état courant le pointeur &2*1 à la mémoire allouée et l'alias b aux alias définis.

Les pointeurs de mémoire allouée et les alias définis dans l'état du processeur correspondent à la mémoire locale d'une fonction. Les retours de fonction utilisent ces informations pour libérer la mémoire.

Interruptions locales et cascadées

L'état du processeur contient aussi les gestionnaires locaux d'interruption. Ces gestionnaires sont mis en place pour la fonction en cours, et ceux placés dans la partie cascadée sont mêmes transmis aux fonctions appellantes.

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PLUGIN "svmcom.so"
PLUGIN "svmrun.so"
PROCESS "application"
	CODE "main" INLINE
		:debug ADD FIRST
		:debug ADD SECOND
		:call f &0*0
		:shutdown
	:label f
		:interruption CASCADE FIRST i1
		:interruption SECOND i2
		:call g P
		:run.interrupt FIRST
		:run.interrupt SECOND
		:return
	:label g
		:interruption CASCADE SECOND i3
		:call h P
		:run.interrupt FIRST
		:run.interrupt SECOND
		:return
	:label h
		:interruption FIRST i4
		:run.interrupt FIRST
		:run.interrupt SECOND
		:return
	:label i1
		:com.message "1"
		:return
	:label i2
		:com.message "2"
		:return
	:label i3
		:com.message "3"
		:return
	:label i4
		:com.message "4"
		:return
	END
END

Puis exécutez l'application dans le débugueur. Pour chaque instruction :run.interrupt, déduisez grâce à l'état courant du processeur quel gestionnaire d'interruption est utilisé :

  1. la première interruption levée est FIRST, et deux gestionnaires d'interruption sont définis (un local et un cascadé). Ici, le local est prioritaire, c'est donc le gestionnaire "i4" qui est exécuté,
  2. la seconde interruption levée est SECOND, et seul un gestionnaire d'interruption cascadé est défini. Il est utilisé, et c'est donc le gestionnaire "i3" qui est exécuté,
  3. la troisième interruption levée est FIRST, et seul un gestionnaire d'interruption cascadé est défini depuis la fonction appellante. C'est donc le gestionnaire "i1" qui est exécuté,
  4. la quatrième interruption levée est SECOND, et seul un gestionnaire d'interruption cascadé est défini. Il est utilisé, et c'est donc le gestionnaire "i3" qui est exécuté,
  5. la cinquième interruption levée est FIRST, et seul un gestionnaire d'interruption cascadé est défini. Il est utilisé, et c'est donc le gestionnaire "i1" qui est exécuté,
  6. la dernière interruption levée est SECOND, et seul un gestionnaire d'interruption local est défini. Il est utilisé, et c'est donc le gestionnaire "i2" qui est exécuté.

Drapeaux locaux et cascadés

L'état courant contient aussi des drapeaux qui servent de condition non placée en mémoire.

Modifiez le code pour obtenir :

#!/usr/bin/env svm
DESCRIPTION
Processor exploration
END
LOG
DEBUG "Processor"
PLUGIN "svmcom.so"
PLUGIN "svmrun.so"
PROCESS "application"
	CODE "main" INLINE
		:debug BREAK
		:call f &0*0
		:shutdown
	:label f
		:flag CASCADE "a"
		:flag "b"
		:call g P
		:com.message "f:"
		:call i1 P :when "a" RAISED
		:call i2 P :when "b" RAISED
		:call i3 P :when "c" RAISED
		:return
	:label g
		:flag "c"
		:call h P
		:com.message "g:"
		:call i1 P :when "a" RAISED
		:call i2 P :when "b" RAISED
		:call i3 P :when "c" RAISED
		:return
	:label h
		:com.message "h:"
		:call i1 P :when "a" RAISED
		:call i2 P :when "b" RAISED
		:call i3 P :when "c" RAISED
		:return
	:label i1
		:com.message "1"
		:return
	:label i2
		:com.message "2"
		:return
	:label i3
		:com.message "3"
		:return
	END
END

Puis exécutez l'application :

./processeur.svm
h:
1
g:
1
3
f:
1
2

Le résultat montre que :

  1. dans la fonction "h", seul le drapeau cascadé "a" est détecté,
  2. dans la fonction "g", le drapeau cascadé "a" et le drapeau local "c" sont détectés alors que le drapeau "b" ne l'est pas,
  3. dans la fonction "f", le drapeau cascadé "a" et le drapeau local "b" sont détectés.

Pile de retour

Le processeur contient également une pile d'états sauvegardés.

Relancez l'application en mode débugueur, et observez ce qu'il se passe sur la pile de retour lorsque :

  1. Lorsqu'une fonction est appellée, l'état avant appel est sauvegardé sur la pile de retour.
  2. Lorsque une fonction se termine, le dernier état sauvegardé sur la pile est retiré et celui écrase complètement l'état courant.

Les drapeaux peuvent servir à taguer un ou plusieurs niveaux de la pile de retour, ce qui est souvent utile lors de manipulations de la pile de retour des fonctions.

Le processeur contient une pile d'états qui joue un rôle crucial dans l'exécution des fonctions. Cela permet au processeur de conserver l'état courant en le copiant sur la pile et restaurer plus tard cet état pour reprendre l'exécution du code au moment de la sauvegarde.

Le fait que cette pile soit de type LIFO (last in, first out en anglais, c'est-à-dire dernier entré, premier sorti) donne aux appels de fonctions cet aspect d'imbrication d'exécution.

Interruptions globales

Les gestionnaires d'interruption globaux sont indépendants de l'état courant du processeur, les rendant insensibles aux changements de fonction.

Lorsqu'une interruption est levée, le gestionnaire d'interruption global est invoqué :

Les gestionnaires d'interruption globaux sont invoqués en dernier recours sur les interruptions logicielles, ou sur les interruptions matérielles.

Interruptions en attente

Cette partie contient simplement la liste des interruptions détectées par le processeur, et non encore traitées.

Lorsque cette liste est traitée, pour chaque interruption :

Conclusion

Vous venez de voir comment fonctionne le processeur de la machine virtuelle.

Il est important de considérer les différents registres et informations contenues dans le processeur pour développer des applications cohérentes.

Altérer l'état du processeur de manière volontaire pour infléchir l'exécution du code permet de réaliser très simplement des opérations ayant une grande valeur ajoutée en terme d'orchestration d'exécution d'instructions.