Cours de Systèmes d'exploitation avancé

TDTP1 -- Géstion de la mémoire

A.B.Dragut et V. Risch

Exo 01. Arithmétique des pointeurs

Dans cet exercice

vous étudiez les opérations d'accès à la mémoire avec de pointeurs et des déplacements.

Quoi de neuf :

int  tab[100];       // tableau de 100 entiers
int *p1 = &(tab[0]); // p1 pointe vers le premier element de tab
int *p2 = tab;       // p2 pointe "au debut" de tab -- donc pareil

De mêmeme, on peut soit écrire tab[k] soit *(p1+k), etc. C'est-à-dire, (p1+k) == &(tab[k]).
Comme ici tab donne donc l'adresse du début d'une zone mémoire qui contient les 100 entiers un après l'autre, le fait d'écrire p1+k nous fait nous déplacer de k entiers dedans -- donc de k*sizeof(int) octets. Ceci se passe (magiquement) ainsi, car p1 a bien été déclaré comme étant un pointeur vers des entiers. Autrement dit, le compilateur utilise toujours la taille de la représentation mémoire du type pointé pour les déplacements.
Il en va de même pour l'opérateur ++.
Travail à faire
Soit le petit programme C++ suivant
#include <iostream>
using namespace std;

int main() {
    int       tab[10]; 
    const int nElem(10);
    for(int k(0); k<nElem; k++) {
	cin >> tab[k];
    }
    int       somme(0);
    for(int k(0); k<nElem; k++) {
	somme += tab[k];
    }
    cout << somme << "\n";
    return 0;
}
  1. Récrivez ce programme en C, utilisant scanf() (et donc en passant l'adresse de chaque élément de tab utilisant l'opérateur &, donc &(tab[k])) et print(). Bien inclure la stdio.h.
  2. Récrivez le même programme, toujours en C, utilisant cette fois le pointeur p2 (comme montré plus haut) partout, donc pour scanf() et aussi pour l'accumulation dans somme (donc utilisant la construction *(pointeur+deplacement)) et pour printf().
  3. Récrivez le même programme, toujours en C, utilisant toujours le pointeur p2 (comme montré plus haut) pour scanf() et aussi pour l'accumulation dans somme, mais en le faisant avancer avec ++ au lieu de lui rajouter un déplacement. Pour la condition d'arrêt de la boucle initialisez un autre pointeur juste un entier "au delà" du dernier élément de tab et itérez donc tant que le pointeur avancé avec ++ n'arrive pas dessus.
  4. Remarquons au passage que cette dernière manière d'itérer dans une boucle a été généralisée au niveau de C++ avec ce qu'on appelle justement des itérateurs (de conteneurs par exemple, de la STL), où l'on obtient le début et la fin typiquement avec les méthodes begin() et end() et où l'on avance avec ++.

Exo 02. Pointeur void *

Dans cet exercice

vous étudiez la notion de pointeur vers "rien", les pointeurs de structure et les pointeurs de fonction.

Quoi de neuf :

Le prototype des fonctions malloc() et free() utilisent bien ce type de pointeur
void * malloc (size_t taille);
void   free   (void * pointeur);
Le type de pointeur void * sert de "type générique". De plus, il permet de "cacher" temporairement une structure plus riche. Utilisant le typecast, on peut ensuite retrouver la structure (à condition de "cast"er vers le type correct).
Lorsqu'on a un pointeur pStr vers une structure, pour accéder aux membres, il faut utiliser l'opérateur -> ainsi
   pStr -> NomDuMembre
Travail à faire
  1. Mettez le programme suivant dans un fichier toto.c, compilez-le, et exécutez-le. Qu'affiche-t-il ? Que fait la fonction fCreation(), étape par étape ?
  2. #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
        int a,b;
    } Toto;
    
    void *fCreation(int x, int y) {
        Toto *pToto = (Toto *)malloc(sizeof(Toto));
        pToto -> a  = x;
        pToto -> b  = y;
        return (void*)pToto; // typecast vers void*
    }
    
    void fAffichage(void *ptr) {
        Toto *pToto = (Toto *)ptr; // typecast vers Toto *
        printf("Toto: %d %d\n",pToto->a,pToto->b);
    }
    
    int main() {
        void *p = fCreation(1,2);
        /*printf("TotoMain: %d %d\n",p->a,p->b);*/
        fAffichage(p);
        free(p);
        return(0);
    }
    
  3. Maintenant, allez dans toto.c et enlevez le commentaire, pour avoir réelement le printf() dans le main(), donc ainsi
  4. int main() {
        void *p = fCreation(1,2);
        printf("TotoMain: %d %d\n",p->a,p->b);
        fAffichage(p);
        free(p);
        return(0);
    }
    
  5. Cela pourra-t-il compiler ? Pourquoi? Est-il permis d'utiliser l'opérateur * sur un pointeur de void ? Pourquoi ?
  6. Donc en conclusion, dans la fonction main() pouvons nous faire quoi que ce soit avec les membres de la structure Toto si on garde le pointeur vers void ?
  7. Dans la pratique, on peut mettre le code de définition de Toto et des fonctions fCreation() et fAffichage() dans d'autres fichiers source et ne donner que la bibliothèque compilée. Alors dans le fichier on ne mettra plus que le main() ainsi
    int main() {
        void *p = fCreation(1,2);
        fAffichage(p);
        free(p);
        return(0);
    }
    
    donc qui ne parle plus de Toto. Que faudrait-il quand même donner d'autre pour qu'on puisse compiler et linker un tel main() avec cette bibliothèque ? (Indication : c'est ce qu'on fait tout le temps avec stdio.h etc). Attention -- on n'a pas besoin de la définition de Toto.
  8. Comment avons-nous séparé les "responsabilités" ? Le code utilisateur (i.e. le main()) décide quand créer, et quand libérer, et garde le pointeur. Mais que n'a-t-il pas~?
  9. Quoi d'autre de neuf


    Un pointeur de fonction permet d'appeler une fonction parmi plusieurs préalablement définies -- en la choisissant pendant l'exécution. On défini d'abord le type, avec typedef, comme une fonction, mais en mettant une * devant le nom et des paranthès autour
      typedef TYPEDERETOUR (*NOM)(TYPEPARAM1 PARAM1, TYPEPARAM2 PARAM2, ...)
    

    Ici on reprend le calcul de la somme des éléments d'un tableau, mais on l'écrit de manière générique avec des void * et pointeurs de fonction.
  10. Mettez ce programme dans un fichier somme.c, compilez-le et exécutez-le.
  11. #include <stdio.h>
    
    typedef void (*PAppFunc)(void *dest, void **ppElem);
    
    void accumulerInt(void *acc, void **ppElem) {
        int *pAcc   = (int *)acc;
        int **pElem = (int **)ppElem;
        *pAcc += **pElem;
        (*pElem)++;
    }
    
    void balayerAppliquer(void *tableau, const int nElem, PAppFunc pAppFunc, void *base) {
        int k;
        for(k = 0; k<nElem; k++) {
    	pAppFunc(base,&tableau);
        }
    } 
    
    int main() {
        int  tab[10]; 
        const int nElem = 10;
        int k;
        int       somme = 0;
        for(k = 0; k<nElem; k++) {
    	scanf("%d",tab+k);
        }
        balayerAppliquer(tab,10,accumulerInt,&somme);
        printf("%d\n",somme);
        return 0;
    }
    
  12. Que fait balayerAppliquer() pas-à-pas ? Aux points précédents nous avons vu comment on cache l'information avec void*. Dans quelles lignes de ce programme fait-on la même chose ici~?
  13. Le profile de la fonction accumulerInt() ne mentionne pas le type int. Que fait-elle pas-à-pas ?
  14. Recopiez le fichier somme.c dans un autre fichier sommeD.c. Écrivez dans sommeD.c une autre fonction, accumulerDouble(), qui fait la même chose accumulerInt(), mais pour des double.
  15. Toujours dans sommeD.c modifiez le main() pour que tab soit un tableau de doubles et somme soit un double; modifiez le scanf() et au printf() en mettant '%f' à la place de '%d'. Compilez et testez.
  16. Pour que ceci fonctionne, avons-nous eu besoin de changer le code de balayerAppliquer() ? Pourquoi ? Donc ?

Exo 03. Manipulation des bits

Dans cet exercice

vous descendez encore plus bas vers les "entrailles" de la machine : malgré le fait que l'unité de transfert de l'information entre banc mémoire, CPU, composants, etc. soit l'octet et même le mot, au niveau logiciel, on peut néanmoins arriver à "mettre la main" sur de chiffres binaires individuelles -- les bits -- d'un octet. Ceci est fort utile et bien amusant.

Quoi de neuf

Écriture des nombres en base 10, 2, 8 et 16
 
0   0000   0    0
1   0001   1    1 
2   0010   2    2 
3   0011   3    3 
4   0100   4    4  
5   0101   5    5 
6   0110   6    6  
7   0111   7    7 
8   1000  10    8
9   1001  11    9
10  1010  12    A
11  1011  13    B 
12  1100  14    C
13  1101  15    D 
14  1110  16    E
15  1111  17    F
16 10000  20   10
algèbre booléenne de base
OU BINAIRE  |  0    1
------------+-----------     Operateur   |   en C/C++
    0       |  0    1
            | 
    1       |  1    1
            |


ET BINAIRE  |  0    1
------------+-----------     Operateur   &   en C/C++
    0       |  0    0
            | 
    1       |  0    1
            |

OU exclusif |  0    1
------------+-----------     Operateur   ^   en C/C++
    0       |  0    1
            | 
    1       |  1    0
            |

Parmi les opérations de base dans cette catégorie on a la mise à un ou respéctivement à zéro d'un bit de rang donné, ainsi que le test d'un bit de rang donné, et également le fait d'inverser un bit (donc s'il vallait un on le met à zéro, et s'il vallait zéro, on le met à un). La manière générique de procéder est l'utilisation de masques, puisque le OU binaire d'une valeur quelconque avec 0 la laisse inchangée, ainsi que le ET binaire avec 1. Autrement dit
   qqchose OU 0 == laMemeChose
   qqchose ET 1 == laMemeChose
et sym\'etriquement, le OU binaire d'une valeur quelconque avec la valeur 1 la transforme en 1, etc.
   qqchose OU 1 == 1
   qqchose ET 0 == 0
Enfin, on dispose d'un opérateur dit de "shift gauche", le <<, qui "déplace" les bits vers la gauche
  0001 << 2   donnera    0100
et aussi de l'opérateur de négation de bit, le ~. Avec tout ceci, on peut écrire une fonction qui prend deux arguments : un rang et une valeur, dont elle mettra le bit de rang donné à zéro (quelle que soit sa valeur initiale, et sans toucher aux autres bits):
  int mettreBitAZero(int valeur, int rang) {
     int masque = 1 << rang;   /* masque sera     00..010...0 */
     return valeur & ~masque;  /* ET binaire avec 11..101...1 */
  }

Travail à faire
  1. Écrivez de manière similaire les fonctions
  2.  int mettreBitAUn  (int valeur, int rang);
     int inverserBit   (int valeur, int rang);
     int testerBitAUn  (int valeur, int rang);
     int testerBitAZero(int valeur, int rang);
    
  3. Si on regarde les propriétés de l'OU EXCLUSIF (en anglais XOR) d'un bit quelconque r on voit que
  4.     r XOR 0  donne toujours  r
        r XOR 1  donne toujours  l'inverse de r
        r XOR r  donne toujours  0
    
    alors si on considère deux bits arbitraires r et s,
        (r XOR s) XOR r   donne toujours   s
    
    car si r et s sont identiques, on a
    (r XOR r) XOR r == 0 XOR r == r (donc == s par hypothese)
    
    et s'ils sont diff\'erents,
    (r XOR s) XOR r == 1 XOR r == l'inverse de r (donc == s par hypothese)
    
    Soit la fonction
     void rebelotte(int *pA, int *pB) {
         int c = *pA;
         *pA   = *pB;
         *pB   = c;
     }
    
    qui permute les deux valeurs. Écrivez une fonction similaire qui n'utilise pas une troisième variable, et qui se sert de XOR, en utilisant la relation prouvée ci-avant
        (r XOR s) XOR r   donne toujours   s
    
  5. Considérons que int est sur 32 bits. Utilisant
    • le fait que les entiers positifs ont le bit le plus significatif égal à zéro
    • le fait que les entiers négatifs ont le bit le plus significatif égal à un
    • en faisant shift à droite 31 d'un entier positif on obtient que des zéros partout, et pour un entier négatif, que des uns partout
    • les propriétés du ET binaire avec 1 et 0 (1 ET qqchose c'est la chose, 0 ET qqchose c'est zéro)
    • le fait que 0 XOR qqchose donne qqchose
    • le XOR "double", mais cette fois du style
          (a XOR b) XOR b
      
    écrivez trois lignes qui à partir de a et b entiers quelconques calculent le minimum d'entre eux sans utiliser de if. Indication : commencez par calculer la différence entre eux (qui évidemment sera positive ou négative).
    Attention, l'expression finale sera un peu plus compliqué que les deux XOR suggérés (mais pas beaucoup plus). Justifiez bien le bon fonctionnement.
  6. Que faut-il changer ci-avant pour calculer le maximum de la même manière?

Exo 04. Pointeur "perdu" et deux fois libre pour créer l'anarchie

Dans cet exercice

vous aller voir de près ce qui arrive quand on ne respecte pas les règles de bonne gestion de la mémoire
  1. Reprenons le petit exemple C++ du cours, avec le pointeur "perdu", pour essayer de voir les badaboums.
  2. class MaPetiteChaine {
    public:
      MaPetiteChaine(const char* data = "");
      virtual ~MaPetiteChaine();
      virtual const char* to_cstr() const;
    private:
      char* m_dataP;
    };
    MaPetiteChaine::MaPetiteChaine(const char* dataP) :
      m_dataP(new char[strlen(dataP)+1]) {
      strcpy(m_dataP,dataP);
    }
    MaPetiteChaine::~MaPetiteChaine() {
      delete [] m_dataP;
    }
    const char* MaPetiteChaine::to_cstr() const {
      return m_dataP;
    }
    int main() {
      MaPetiteChaine salut("Bonjour tout le monde");
      cout << salut.to_cstr() << endl;
    
           // on en fait une copie dynamiquement
      MaPetiteChaine* reSalutP = new MaPetiteChaine(salut);
    
           // on utilise la copie et l'original, tout baigne
      cout << reSalutP->to_cstr() << endl;
      cout << salut.to_cstr()     << endl;
      
      delete reSalutP; // on nettoie la copie
      reSalutP = 0;    // pour etre bien clean
    
           // et on devrait pouvoir encore utiliser l'original....
      cout << salut.to_cstr() << endl; // a l'air ok, mais BOUM !
    }      // et encore BADABOUM ! mais qu'est-ce qu'on a fait ?..
    
  3. Sauvegardez, compilez, essayez. Avec un peu de chance, il n'y a aucun badaboum "grave". Le "boum" par contre si -- il ne devrait pas y avoir de quatrième affichage.
  4. Pourquoi donc pas de second badaboum ? C'est-à-dire, pourquoi on nous laisse apparemment tenter de libérer la mémoire une seconde fois ? D'abord, en sommes-nous sûrs ? Rajoutez la ligne
      cout << "Effacement de m_dataP " << reinterpret_cast<long int>(m_dataP) << "\n";
    
    au début du destructeur, et recompilez et ressayez. Vous devrez constanter que les deux valeurs affichées sont bien les mêmes.
  5. Modifions le constructeur de recopie pour qu'il réserve
  6.    new char[strlen(dataP)+1000]
    
    donc 999 de caractès de plus. Aucune autre modification. Recompilez, relancez, et admirez le badaboum, assez explicite
    *** glibc detected *** ./a.out: double free or corruption (!prev)
    ======= Backtrace: =========
    ...
    
  7. Pourquoi n'y a-t-il pas eu de badaboum avant et maintenant il y en a?
  8. Réparez le programme (que faut-il rajouter à cette classe?), et retestez.

Exo 05. Comment boucher les fuites de mémoire (pas avec des pipes)

Dans cet exercice

vous aller voir comment se servir d'un outil de détection de fuites de mémoire.
  1. Mettez le programme suivant dans un fichier -- mettons fuiteMemoire.c, et compilez-le -- mettons avec le nom fuiteMemoire.run -- et exécutez-le.
  2. 
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int numberOfIter  = 5;
        int numberOfTabs  = 4;
        int eachTabLength = 4; 
        int kIter;
        for(kIter = 0; kIter < numberOfIter; kIter++) {
            // alloc 'em
            char **tab = (char**)malloc(numberOfTabs * sizeof(char*));
            int kTab;
            for(kTab = 0; kTab < numberOfTabs; kTab++) {
                tab[kTab] = (char *)malloc(eachTabLength * sizeof(char));
                int kElem;
                for(kElem = 0; kElem < eachTabLength-1; kElem++) {
                    tab[kTab][kElem] = 'A' + kTab + kElem;
                }
                tab[kTab][eachTabLength-1] = '\0';
            }
            // use 'em
            for(kTab = 0; kTab < numberOfTabs; kTab++) {
                printf("%s ",tab[kTab]);
            }
            // free 'em all
            free(tab); // or at least u think so
            printf("Free to go\n");
        }
        printf("Good bye.\n");
        return(0);
    }
    
  3. Tout a-t-il l'air normal ? Pourquoi ?
  4. Relancez le programme avec un outil spécial nommé valgrind, ainsi :
       valgrind fuiteMemoire.run
    
    et admirez le bilan détaillé. Que nous dit-il?
  5. Réparez ensuite le programme, et relancez-le avec valgrind. Que nous dit-il maintenant ?