Up Next

1  Retour à un contexte

Pour certaines applications, l’exécution du programme doit être reprise en un point particulier. Un point d’exécution est caractérisé par l’état courant de la pile d’appel et des registres du processeur ; on parle de contexte.

La bibliothèque Unix standard

La fonction setjmp() de la bibliothèque standard mémorise le contexte courant dans une variable de type jmp_buf et retourne 0.

La fonction longjump() permet de réactiver un contexte précédemment sauvegardé. À l’issue de cette réactivation, setjmp() « retourne » une valeur non nulle.

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

Exercice 1
 (Illustration du mécanisme de setjmp()/longjmp())   Il s’agit d’appréhender le comportement du programme suivant :
#include <setjmp.h>
#include <stdio.h>

static int i = 0;
static jmp_buf buf;

int
main()
{
    int j; 

    if (setjmp(buf)) 
        for (j=0; j<5; j++)
            i++;
    else { 
        for (j=0; j<5; j++)
            i--;
        longjmp(buf,~0);
    }
   printf("%d\n", i );
}
et de sa modification :
#include <setjmp.h>
#include <stdio.h>

static int i = 0;
static jmp_buf buf;

int
main()
{
    int j = 0; 

    if (setjmp(buf)) 
        for (; j<5; j++)
            i++;
    else { 
        for (; j<5; j++)
            i--;
        longjmp(buf,~0);
    }
   printf("%d\n", i );
}

Exercice 2
 (Lisez la documentation)   Expliquez en quoi le programme suivant est erroné :
#include <setjmp.h>
#include <stdio.h>

static jmp_buf buf;
static int i = 0;

static int
cpt()
{
    int j = 0; 
    
    if (setjmp(buf)) {
        for (j=0; j<5; j++)
            i++;    
    } else {
        for (j=0; j<5; j++)
            i--;
    }
}

int
main()
{
    int np = 0 ; 
    
    cpt();

    if (! np++)
        longjmp(buf,~0);

    printf("i = %d\n", i );
}
Vous pouvez ainsi apprécier l’extrait suivant de la page de manuel de setjump(3)
NOTES
    setjmp() and sigsetjmp make programs hard to understand and  maintain.
    If possible an alternative should be used.

Exercice 3
 (Utilisation d’un retour dans la pile d’exécution)   Modifiez le programme suivant qui calcule le produit d’un ensemble d’entiers lus sur l’entrée standard pour retourner dans le contexte de la première invocation de mul() si on rencontre une valeur nulle : le produit de termes dont l’un est nul est nul.
static int
mul(int depth)
{
    int i;

    switch (scanf("%d", &i)) {
        case EOF :
            return 1; /* neutral element */
        case 0 :
            return mul(depth+1); /* erroneous read */
        case 1 :
            if (i) 
                return i * mul(depth+1);
            else
                return 0;
    }
}

int
main()
{
    int product;

    printf("A list of int, please\n"); 
    product = mul(0);
    printf("product = %d\n", product); 
}

Assembleur en ligne dans du code C

Le compilateur GCC autorise l’inclusion de code assembleur au sein du code C via la construction asm(). De plus, les opérandes des instructions assembleur peuvent être exprimées en C.

Le code C suivant permet de copier le contenu de la variable x dans le registre %eax puis de le transférer dans la variable y ; on précise à GCC que la valeur du registre %eax est modifiée par le code assembleur :

int
main(void)
{
    int x = 10, y;

    asm ("movl %1, %%eax" "\n\t" "movl %%eax, %0"
           : "=r"(y) /* y is output operand */
           : "r"(x) /* x is input operand */
           : "%eax"); /* %eax is a clobbered register */
}

Attention, cette construction est hautement non portable et n’est pas standard ISO C ; on ne peut donc utiliser l’option -ansi de gcc.

On se référera au tutoriel d’utilisation d’assembleur x86 en ligne disponible à http://www-106.ibm.com/developerworks/linux/library/l-ia.html ou à la documentation de GCC disponible à http://gcc.gnu.org/onlinedocs/. Une copie locale est disponible ici.

Environnement 32 bits

Nous avons choisi de travailler sur les microprocesseurs Intel x86, c’est-à-dire compatibles avec le jeu d’instructions de l’Intel 8086. Les versions les plus récentes de cette famille (depuis 2003 quand même !) sont des microprocesseurs 64 bits (Athlon 64, Opteron, Pentium 4 Prescott, Intel Core 2, etc.).

Ces processeurs 64 bits peuvent fonctionner en mode compatibilité 32 bits avec le jeu d’instructions restreint de l’Intel 8086.

Sur ces machines, on indique au compilateur GCC de générer du code 32 bits en lui spécifiant l’option -m32.

Contexte d’exécution

Pour s’exécuter, les procédures d’un programme en langage C sont compilées en code machine. Ce code machine exploite lors de son exécution des registres et une pile d’exécution.

Dans le cas d’un programme compilé pour les microprocesseurs Intel x86, le sommet de la pile d’exécution est pointé par le registre 32 bits %esp (stack pointer). Par ailleurs le microprocesseur Intel définit un registre désignant la base de la pile, le registre %ebp (base pointer).

Ces deux registres définissent deux adresses, à l’intérieur de la zone réservée pour la pile d’exécution, l’espace qui les sépare est la fenêtre associée à l’exécution d’une fonction (frame) : %esp pointe le sommet de cette zone et %ebp la base.

Grossièrement, lorsque une procédure est appelée, les registres du microprocesseur (excepté %esp) sont sauvés au sommet de la pile, puis les arguments sont empilés et enfin le pointeur de programme, avant qu’il branche au code de la fonction appelée. Notez encore que la pile Intel est organisée selon un ordre d’adresse décroissant (empiler un mot de 32 bits en sommet de pile décrémente de 4 l’adresse pointée par %esp).

Sauvegarder les valeurs des deux registres %esp et %ebp suffit à mémoriser un contexte dans la pile d’exécution.

Restaurer les valeurs de ces registres permet de se retrouver dans le contexte sauvegardé. Une fois ces registres restaurés au sein d’une fonction, les accès aux variables automatiques (les variables locales allouées dans la pile d’exécution) ne sont plus possibles, ces accès étant réalisés par indirection à partir de la valeur des registres.

L’accès aux registres du processeur peut se faire par l’inclusion de code assembleur au sein du code C ; voir l’encart.

Première réalisation pratique

Dans un premier temps, fournissez un moyen d’afficher la valeur des registres %esp et %ebp de la fonction courante.

Implantez votre bibliothèque de retour dans la pile d’exécution et testez son comportement sur une variante du programme de l’exercice 3.

Observez l’exécution de ce programme sous le dévermineur; en particulier positionnez un point d’arrêt dans throw() et continuez l’exécution en pas à pas une fois ce point d’arrêt atteint.

Implantation d’une bibliothèque de retour dans la pile d’exécution

On définit un jeu de primitives pour retourner à un contexte préalablement mémorisé dans une valeur de type struct ctx_s.

typedef int (func_t)(int); /* a function that returns an int from an int */

int try(struct ctx_s *pctx, func_t *f, int arg);

Cette première primitive va exécuter la fonction f() avec le paramètre arg. Au besoin le programmeur pourra retourner au contexte d’appel de la fonction f mémorisé dans pctx. La valeur retournée par try() est la valeur retournée par la fonction f().

int throw(struct ctx_s *pctx, int r);

Cette primitive va retourner dans un contexte d’appel d’une fonction préalablement mémorisé dans le contexte pctx par try(). La valeur r sera alors celle « retournée » par l’invocation de la fonction au travers try().


Exercice 4
 (Implantation d’un retour dans la pile d’exécution)  
Question 1   Définissez la structure de données struct ctx_s.
Question 2   La fonction try() sauvegarde un contexte et appelle la fonction passée en paramètre. Donnez une implantation de try().
Question 3   Il s’agit de proposer une implantation de la fonction throw(). La fonction throw() restaure un contexte. On se retrouve alors dans un contexte qui était celui de l’exécution de la fonction try(). Cette fonction try() se devait de retourner une valeur. La valeur que nous allons retourner est celle passée en paramètre à throw().

Up Next