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.
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 :
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.
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.
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 :
:goto
modifie la valeur de ce registre pour réaliser un saut.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 :
&0*0
,&0*1
, qui est la valeur du pointeur placé en second argument de l'instruction :call
.Le processeur ne contient que deux registres :
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.
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.
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 :
:memory
ajoute le pointeur &0*1
à la mémoire allouée et l'alias i
aux alias définis,&1*1
à la mémoire allouée et l'alias s
aux alias définis,: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,: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.
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é :
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é,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é,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é,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é,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é,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é.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 :
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 :
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.
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.
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 :
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.