On envisage maintenant la duplication d’un contexte existant. Cela nécessite quelques manipulations de la pile d’exécution qui sont préalablement introduites.
Convention d’appel et organisation de la pile (partie 1)
Une convention d’appel est une méthode qui assure, lors d’un appel de fonction, la cohérence entre ce qui est réalisé par la fonction appelante et la fonction appelée. En particulier, une convention d’appel précise comment les paramètres et la valeur de retour sont passées, comment la pile est utilisée par la fonction.
- Conventions d’appel
- Plusieurs conventions sont utilisées :
- __cdecl est la convention qui supporte la sémantique du langage C et en particulier l’existence de fonctions variadiques (fonctions à nombre variable de paramètres, par exemple printf()). Cette convention est le standard de fait, en particulier sur architecture x86 ;
- __stdcall est une convention qui suppose que toutes les fonctions ont un nombre fixe de paramètres. Cette convention simplifie le nettoyage la pile après un appel de fonction ;
- __fastcall est une convention qui utilise certains registres pour passer les premiers paramètres de la fonction.
- Registres utilisés
- Sur Intel x86, ces conventions utilisent toutes les registres suivants :
- le registre %esp est manipulé implicitement par certaines instructions telles push, pop, call, et ret. Ce registre contient toujours l’adresse sur la pile du dernier emplacement utilisé (et non du premier emplacement libre). On oubliera pas que la pile est gérée suivant les adresses décroissantes. Le sommet de la pile est donc toujours à une adresse la plus basse.
- le registre %ebp est utilisé comme base pour référencer les paramètres, les variables locales, etc. dans la fenêtre courante. Ce registre n’est manipulé qu’explicitement. L’implantation d’une convention d’appel repose principalement sur ce registre.
- le registre %eip contient l’adresse de la prochaine instruction à exécuter. Le couple d’instructions call/ret sauvegarde et restaure ce registre sur la pile. Bien entendu, chacune des instructions de saut modifie ce registre.
Convention d’appel et organisation de la pile (partie 2)
- Appel d’une fonction __cdecl
- Les étapes suivantes sont réalisées lors de l’appel d’une fonction selon la convention __cdecl :
- empilement des valeurs des paramètres (de droite à gauche). L’appelant mémorise combien paramètres ont été empilés (en fait la taille en octets de l’ensemble des paramètres) ;
- appel de la fonction via un call. Le registre %eip est sauvegardé sur la pile avec la valeur de l’instruction qui suit le call ;
- mise à jour du pointeur de pile. On est maintenant dans le code de la fonction appelée. La pile de cette fonction débute au dessus de la pile de la fonction appelante ; on empile l’ancienne valeur de %ebp et on lui affecte la valeur actuelle de l’adresse de sommet de pile :
À partir de cette nouvelle valeur de %ebp, la fonction appelée peut accéder à ses arguments par un déplacement positif (car la pile est rangée suivant les adresses décroissantes) : 8(%ebp) référence le premier paramètre, 12(%ebp) le second, etc.push %ebp
mov~ %esp, %ebpÀ partir de cette nouvelle valeur de %ebp, on peut aussi retrouver l’ancien pointeur d’instruction à 4(%ebp) et l’ancienne valeur de %ebp à 0(%ebp) ;
[width=0.35@percent]ctx-stack
- allocation des variables locales. La fonction peut allouer ses variables locales dans la pile en décrémentant simplement le registre %esp. Les accès à ces variables se feront par un déplacement négatif par rapport à %ebp : -4(%ebp) pour la première variable, etc. ;
- sauvegarde de registres. Si la fonction utilise des registres pour l’évaluation d’expressions, elle les sauvegarde à ce niveau sur la pile et devra les restaurer avant le retour à la fonction appelante ;
- exécution du code de la fonction. L’état de la pile est ici cohérent et le registre %ebp est utilisé pour accéder aux paramètres et à variables locales. Le code de la fonction peut accéder aux registres qui ont été sauvegardés, mais ne peut pas modifier la valeur du registre %ebp. Les allocations supplémentaires dans la pile se font par des manipulation du registre %esp ; cependant ces allocations doivent être compensées par autant de libérations ;
- restauration des registres. Les registres sauvegardés à l’entrée de la fonction doivent maintenant être restaurés ;
- restauration de l’ancien pointeur de pile. La restauration de l’ancienne valeur de %ebp a pour effet de supprimer toutes les allocations réalisées par la fonction et de remettre la pile dans l’état correct pour la fonction appelante ;
- retour à la fonction appelante. L’instruction ret récupère l’ancienne valeur de %eip sur la pile et saute à cette adresse. Le flux d’exécution retourne ainsi dans la fonction appelante ;
- nettoyage de la pile. La fonction appelante se doit de nettoyer la pile des valeurs des paramètres qu’elle y avait empilées.
Manipulation de la pile d’exécution
La taille de la pile associée à un contexte est choisie à la création du contexte. Déterminer une taille optimale est une chose délicate. On se propose d’automatiser la variation de la taille de cette pile.
Exercice 16 Proposez une fonction ou macroqui vérifie que la taille de pile disponible pour un contexte donné est supérieure à une valeur en octets donnée.int check_stack(struct ctx_s *pctx, unsigned size);Nous nous proposons de réallouer cette pile quand la taille libre restante devient petite.
Exercice 17 Discutez des moments opportuns auxquels réaliser cette réalllocation.Une fois la décision de réallouer la pile prise et une nouvelle zone mémoire obtenue, il s’agit de translater la pile dans cette nouvelle zone mémoire : les références comportant des adresses dans l’ancienne pile doivent être modifiées. En particulier, le chaînage des anciennes valeurs de %ebp doit être mis à jour.
Exercice 18 Listez l’ensemble des références vers des adresses dans la pile. En particulier explicitez comment identifier toutes les valeurs de chaînage des anciennes valeurs de %ebp.Proposez une fonction
void translate_stack(struct ctx_s *ctx, unsigned char *nstack, unsigned nstack_size);translation de la pile d’un contexte. Cette fonction suppose que la mémoire de la nouvelle pile a été allouée et que le contenu de l’ancienne pile y a déjà été copié.
Duplication de contextes
On désire maintenant fournir une primitive
int dup_ctx();du type de fork() qui duplique le contexte courant. Les deux copies du contexte n’étant distinguées que par la valeur retournée par cette fonction de duplication, 0 dans le « fils » et une valeur différente de zéro dans le père.
Dupliquer un contexte consiste à dupliquer la structure struct ctx_s associée, mais aussi sa pile d’exécution. Ainsi le fils poursuivra son exécution à la sortie de la fonction dup_ctx(), mais remontera aussi tous les appels de fonctions qui avaient été faits par le père avant le dup_ctx().
Dans un second temps, il sera nécessaire de prendre en compte le point technique suivant : l’exécution du contexte créé par duplication, comme celle de tous les contextes, va reprendre lors d’un appel à switch_to(). Il est donc important que la pile d’exécution créée par duplication soit « compatible » avec une pile laissée par un appel à switch_to(). Une telle pile peut être créée par un appel à une fonction de prototype identique à switch_to() qui va sauvegarder le contexte dans la structure adéquate.
Enfin, on pourra se soucier de la mise en place d’un mécanisme pour retourner une valeur différente dans le contexte père et dans le contexte fils.
Exercice 19 Donnez le code de la fonction dup_ctx() qui consiste donc à allouer une structure pour le nouveau contexte, à y copier le contexte du père, à faire un appel à une fonction « compatible » avec switch_to() qui va sauvegarder les registres dans les champs idoines pour pouvoir revenir à ce contexte ensuite, à allouer la pile de ce nouveau contexte, à y copier celle du père, à translater cette nouvelle pile, enfin à insérer ce nouveau contexte dans l’anneau des contextes.[fin (toujours provisoire...) du sujet]