Partie 3 - Mémoire¶
⚠️ une partie des codes que nous ferons ici sont volontairement bugués pour montrer le fonctionnement de la mémoire. Donc prenez du recul sur ce que vous faites !
p3e1 - Évolution du stack¶
On va chercher à visualiser la croissance du stack ou call stack (la pile d’appel ou pile d’exécution en français) en créant un code bugué.
créez une fonction récursive sans condition d’arrêt, qui déclare une variable demandant « beaucoup » de mémoire (par exemple,
char buf[1024]) puis affiche le niveau de récursion et l’adresse de la variable.Compilez le programme (avec les flags
-Wall -Wextra -pedanticvous aurez une erreurwarning: infinite recursion detectedmais le programme est quand même compilé).Exécutez le programme qui va vite crasher et regardez l’évolution des adresses.
En crashant, vous aurez une erreur du type :
Erreur de segmentation (core dumped). Lancez avec valgrind:gcc -std=c2x -Wall -Wextra -pedantic -g p3e1.c -o p3e1 && valgrind ./p3e1
Un message vous donnera plus d’informations sur le problème :
Stack overflow in thread #1: can't grow stack to 0x1ffe801000On peut voir une erreur de type stack overflow (surcharge de la pile d’appel ou dépassement de pile).
Qu’est-ce que ça veut dire ?
Le stack est la zone mémoire où on empile les variables propres à une fonction à chaque appel de celle-ci (qu’elle soit récursive ou non). Quand une fonction \(a\) est appelée :
la mémoire pour toutes les variables de \(a\) est automatiquement réservée sur le stack (d’où le terme d’allocation automatique)
quand \(a\) appelle une autre fonction \(b\)
la mémoire de \(b\) est empilée sur la call stack pour l’exécution de la fonction
une fois que \(b\) est terminée, la mémoire réservée sur le stack est automatiquement libérée lors du stack unwinding (déroulement de pile, dépilement de la pile, remontée de la pile d’appels)
maintenant que la mémoire de \(b\) est libérée, le haut du stack correspond aux variables locales à la fonction \(a\) qui seront libérées aussi à la fin de \(a\)
Le stack n’est pas illimité en espace mémoire car :
il ne doit stocker que les variables locales d’une fonction, les adresses de retour et les arguments
il doit rester contiguë en mémoire pour des raisons de performance, appeler une fonction et la terminer doit être le plus rapide possible donc on ne va pas allouer de la mémoire n’importe où en RAM et pour qu’il reste contiguë on ne peut pas se permettre de devoir la déplacer si elle devient trop grande.
Si on veut une zone mémoire « illimitée » (dont la seule limite est la RAM libre sur la machine utilisée), il faut utiliser la heap (le tas) avec l’allocation dynamique (
mallocen C,newen TypeScript).
p3e2 - Évolution de la heap¶
On va maintenant chercher à visualiser la croissance de la heap, toujours avec un code bugué.
Pour visualiser la heap, dans une longue boucle (
1000000000000itérations), demandez un bloc mémoire assez grand pour une variable avecmalloc(par exemple,malloc(1024 * 1024)) puis affichez son adresse avec le numéro de l’itération, ne libérez pas la mémoire et passez à l’itération de boucle suivante où vous demandez à nouveau un bloc mémoire pour votre variable.Ouvrez un nouveau terminal à côté et lancez la commande
htopqui va afficher l’utilisation des ressources (CPU et RAM)Lancez le programme et regardez la consommation mémoire (
Mem) dans le terminal avechtopRegardez l’évolution des adresses mémoire
Si vous demandez plus de mémoire, le programme tournera en boucle sans que l’OS ne vous la donne si vous n’avez pas géré le cas où malloc retourne NULL ou perror affichera un message du type : Cannot allocate memory.
p3e3-4 - Codes bugués¶
Résolution de codes bugués, téléchargez les fichiers suivants (depuis votre répertoire de travail) :
wget https://members.loria.fr/cyril.grelier/r3_05/files/p3e3.c
wget https://members.loria.fr/cyril.grelier/r3_05/files/p3e4.c
Ils contiennent des bugs mémoire, trouvez ces bugs et résolvez-les. Notez les erreurs dans le code et laissez des commentaires qui expliquent le problème.
Vous pouvez commencer par lancer la compilation sans les flags -Wall -Wextra -pedantic ni valgrind et regardez les erreurs à l’exécution avant de commencer à les activer.
p3e5 - Invalid pointer dereference¶
Créez un pointeur sur int initialisé à NULL (ou non initialisé) et modifiez la valeur pointé par le pointeur avec l’opérateur de déréférencement comme *p = 42;.
Observez le message d’erreur.
Lancez le programme avec valgrind : valgrind ./p3e5
Interprétez le rapport.
Changez le type en pointeur sur double et cherchez la différence.
p3e6 - Mémoire et adresses¶
Créez un programme C avec des tableaux de taille TAILLE, pour changer la taille facilement, ajoutez au début du fichier :
// permet de changer la valeur à la compilation avec -DTAILLE=1000
#ifndef TAILLE
#define TAILLE 1
#endif
Pour les tableaux, créez les variables suivantes :
une variable globale initialisée (définie avant le main avec une valeur par défaut, vous pouvez juste donner une valeur à la première case)
une variable statique non initialisée (définie avant le main, avec le mot clé
staticet sans valeur)une variable locale (allocation automatique sur le stack)
une allocation dynamique avec malloc
Affichez leurs adresses avec printf("type de variable : %p", (void *) var), affichez aussi l’adresse mémoire de la fonction main avec printf("code (text) main : %p\n", (void *)(uintptr_t)&main);.
Comparez les zones d’adresses et classez-les selon : segment text, data, bss, heap, stack.
Observez le /proc/PID/maps du programme, pour récupérer le PID (Processus ID) :
#include <unistd.h> // pour importer getpid()
printf("pid du programme : %d\n", getpid());
printf("lancer les commandes :\n");
printf("\tcat /proc/%d/maps\n", getpid());
printf("\tpmap %d\n", getpid());
printf("Entrée pour terminer (lancer les commandes avant)\n");
getchar();
Changez la TAILLE de 1 à 100000 et regardez l’impact sur la taille de l’exécutable après compilation avec la commande size p3e6 et pendant l’exécution avec la commande pmap PID.