Propriété, tableau dynamique, listes, piles et files, erreurs du tas, intérieur de malloc
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Des structures de données qui grandissent à l’exécution : un tableau dynamique qui double sa capacité, une liste de nœuds alloués un à un, une pile et une file sur l’un ou l’autre. Chaque bloc a un propriétaire qui le libère ; les erreurs se lisent dans les rapports de valgrind et d’AddressSanitizer, et l’en-tête des blocs montre ce que fait malloc.
realloc.D’où l’on vient, où l’on va
Amont. Séance 3 : pile et tas, malloc, calloc, realloc, free, et les premières erreurs (fuite, double free, accès après free). Séance 4 : structures, ->, padding, structure auto-référentielle, nœud sur le tas.
Cette séance. Les structures de données dynamiques construites avec ces outils, la question de la propriété des blocs, et le fonctionnement interne de l’allocateur.
Aval. Séance 6 : pointeurs de fonctions et généricité : les mêmes tableaux, listes, piles et files, écrits une fois pour tout type d’élément (void *, fonction de comparaison).
Les structures de données de la séance reposent sur les quatre fonctions de <stdlib.h> vues en Séance 3.
| appel | rend | à retenir |
|---|---|---|
malloc(n) |
un bloc de \(n\) octets, ou NULL |
contenu indéterminé |
calloc(k, t) |
un bloc de \(k\times t\) octets à zéro, ou NULL |
|
realloc(p, n) |
le bloc redimensionné, ou NULL |
peut déplacer le bloc ; en cas d’échec, p reste valide |
free(p) |
rien | free(NULL) est sans effet ; p ne doit plus servir |
Les erreurs déjà rencontrées
La fuite de mémoire (un bloc dont plus aucune variable ne garde l’adresse), le double free (arrêt par la glibc, SIGABRT), l’accès après free (comportement indéfini : la lecture rendait 1431655769, le lien écrit par la glibc dans le bloc libéré).
Lecture. Un tableau dynamique ou une liste multiplie les blocs : il faut savoir, pour chacun, qui le libère et quand.
Quoi
Le propriétaire d’un bloc est le code chargé de le libérer, une fois et une seule. Une fonction qui rend un bloc alloué en transfère la propriété à l’appelant ; une fonction qui reçoit un pointeur sans le libérer l’emprunte : le bloc doit rester valide pendant l’appel. La durée de vie d’un bloc va du malloc au free.
Pourquoi
Le C ne libère rien de lui-même : la propriété, écrite dans le contrat de chaque fonction (« l’appelant libère »), dit où placer chaque free. Une structure qui possède ses blocs se libère par une fonction dédiée (vec_liberer, liberer).
Quand ça échoue
Aucun propriétaire : fuite. Deux propriétaires : double free. Un emprunteur qui garde l’adresse au-delà de la durée de vie : pointeur pendant, puis use after free.
Résultat \(\rightarrow\)
Kolda, Thies
Kolda, Kaolack
Kolda, Kolda
Sortie d’erreur sous valgrind \(\rightarrow\)
All heap blocks were freed -- no leaks are possible
Lecture. Les emprunt[i] désignent tous tampon ; dupliquer rend un bloc que main possède et libère.
Vecteurint vec_ajouter(Vecteur *v, int x)
{
if (v->taille == v->capacite) {
size_t cap = v->capacite ? 2 * v->capacite : 1;
int *t = realloc(v->donnees, cap * sizeof *t);
if (t == NULL)
return 0; /* ancien bloc intact */
v->donnees = t;
v->capacite = cap;
}
v->donnees[v->taille] = x;
v->taille++;
return 1;
}Lecture. Plein, le bloc double (une capacité 0 passe à 1) ; si realloc échoue, l’ancien bloc reste valide (et à libérer).
Adresses simplifiées ; sous gdb, v est en 0x7fffffffec50 (pile : dépend de l’environnement et du chemin du programme), le bloc en 0x5555555592a0 (tas).
Sortie \(\rightarrow\)
4 4
Résultat \(\rightarrow\)
double n = 1000 : 10 realloc, 1.02 copies par ajout au pire, 0 deplacements
double n = 1000000 : 20 realloc, 1.05 copies par ajout au pire, 7 deplacements
+1 n = 1000 : 999 realloc, 499.50 copies par ajout au pire, 1 deplacements
+1 n = 100000 : 99999 realloc, 49999.50 copies par ajout au pire, 1 deplacements
Lecture. En doublant, \(1+2+\dots+2^{k-1}<2^k<2n\) : moins de 2 copies par ajout (coût amorti constant) ; de 1 en 1, \(n/2\).
reallocRésultat \(\rightarrow\)
t[0] = 221
*premier = 1431655769
Sortie d’erreur avec AddressSanitizer \(\rightarrow\)
==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010
READ of size 4 at 0x502000000010 thread T0
#0 0x555555555391 in main s2_invalide.c:14
...
0x502000000010 is located 0 bytes inside of 8-byte region [0x502000000010,0x502000000018)
freed by thread T0 here:
#0 0x7ffff78fc778 in realloc asan_malloc_linux.cpp:85
#1 0x5555555552ea in main s2_invalide.c:9
...
SUMMARY: AddressSanitizer: heap-use-after-free s2_invalide.c:14 in main
Lecture. voisin suit le bloc : realloc le déplace, recopie et libère l’ancien. premier désigne l’ancien bloc : le lire est un use after free (gcc l’avertit ; ici 1431655769). Garder un indice, pas un pointeur.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
2020 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x08 |
0505 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x10 |
0606 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x18 |
2121 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x20 |
2424 |
0000 |
0000 |
0000 |
2929 |
0000 |
0000 |
0000 |
0x28 |
1b1b |
0000 |
0000 |
0000 |
1e1e |
0000 |
0000 |
0000 |
0x30 |
2323 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
Vecteur v recopié en 0x00, son bloc en 0x20 avec l’en-tête de la glibc (adresses abrégées) ; vec_init(&v, 3), puis 5 ajouts.
Énoncé
donnees, taille, capacite ;realloc, capacité, octets demandés.Correction
donnees \(=\) 0x20, taille \(=\) 5, capacite \(=\) 6.0x34 est libre.0x21 : 32 octets (bit 0 à part), 24 utiles : 6 int.realloc de 48 octets, capacité 12.Quoi
Une suite de nœuds alloués un à un sur le tas ; chaque nœud contient une valeur et l’adresse du suivant (Séance 4), NULL pour le dernier. La liste est désignée par un pointeur vers son premier nœud, la tête ; une liste vide est NULL.
Pourquoi
Insérer ou retirer un nœud ne déplace aucun autre élément : on réécrit un ou deux pointeurs. Un nœud ne change jamais d’adresse, si bien qu’un pointeur vers lui reste valide tant qu’il est dans la liste, contrairement à un élément de tableau dynamique.
Quand ça échoue
Atteindre le \(k\)e élément parcourt \(k\) nœuds. Chaque nœud coûte un malloc : 16 octets de nœud, un bloc de 32 dans la glibc. Pour modifier la tête, une fonction reçoit son adresse (Noeud **).
Adresses simplifiées ; sous gdb, tete est en 0x7fffffffec60 (pile : selon l’environnement), nœuds en 0x5555555592a0 et 0x5555555592c0.
Sortie \(\rightarrow\)
5 -> 12 -> NULL
Lecture. tete = NULL : la liste est vide.
Lecture. L’appel reçoit &tete : pt contient l’adresse de tete (0x40), et non sa valeur.
Lecture. malloc rend un bloc de 16 octets en 0x80, non initialisé.
Lecture. n->valeur reçoit 12.
Lecture. *pt lit tete à travers pt : NULL ; le nouveau nœud précède l’ancienne tête.
Lecture. *pt = n écrit dans tete, la variable de main : tete vaut 0x80.
Lecture. Second appel : un nouveau stack frame, au même endroit ; pt vaut encore &tete.
Lecture. Second bloc en 0xA0, 32 octets plus loin (16 de nœud, 16 d’en-tête et d’arrondi).
Lecture. n->valeur reçoit 5.
Lecture. n->suivant = *pt : l’ancienne tête, 0x80.
Lecture. tete vaut 0xA0 : 5 est en tête.
Lecture. Le parcours part de tete ; avec un paramètre Noeud *, seule une copie aurait changé.
Résultat \(\rightarrow\)
(nil)
Sortie d’erreur sous valgrind \(\rightarrow\)
16 bytes in 1 blocks are definitely lost in loss record 1 of 2
at 0x4846828: malloc (vgpreload_memcheck)
by 0x109185: inserer_tete (s3_mauvaise.c:9)
by 0x1091CE: main (s3_mauvaise.c:17)
...
16 bytes in 1 blocks are definitely lost in loss record 2 of 2
at 0x4846828: malloc (vgpreload_memcheck)
by 0x109185: inserer_tete (s3_mauvaise.c:9)
by 0x1091DF: main (s3_mauvaise.c:18)
...
definitely lost: 32 bytes in 2 blocks
indirectly lost: 0 bytes in 0 blocks
Lecture. tete = n modifie une copie : la tete de main reste NULL, les deux nœuds sont perdus (32 octets definitely lost).
Lecture. pp avance de champ suivant en champ suivant jusqu’au NULL final, où *pp = n range le nœud : la liste vide n’est pas un cas à part (pp reste &tete). chercher rend le nœud ou NULL.
Sortie \(\rightarrow\) Adresses simplifiées ; gdb : premier nœud 0x5555555592e0, tete 0x7fffffffec60 (pile, selon l’environnement).
3 -> 21 -> NULL
Lecture. Lignes 44 à 46 : nœuds alloués du dernier au premier ; la liste ne suit pas l’ordre des adresses.
Lecture. pp reçoit &tete : *pp est le pointeur à réécrire si le nœud cherché est en tête.
Lecture. *pp vaut 0x80, et (*pp)->valeur vaut 3 : ce n’est pas 8, la boucle continue.
Lecture. pp désigne maintenant le champ suivant du premier nœud, en 0x88.
Lecture. *pp vaut 0x60, dont la valeur est 8 : la boucle s’arrête.
Lecture. *pp n’est pas NULL ; n garde l’adresse du nœud à retirer.
Lecture. *pp = n->suivant écrit 0x40 dans ce champ : le premier nœud saute le deuxième.
Lecture. free(n) rend le bloc. Le même code traite la tête : pp vaut alors &tete.
Lecture. Au retour, pp et n disparaissent avec le stack frame ; la liste compte deux nœuds.
Lecture. Chaque nœud est un bloc : la liste se libère nœud par nœud, en lisant le suivant avant le free. *pt = NULL laisse à l’appelant une liste vide plutôt qu’une tête pendante.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x18 |
2121 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x20 |
0f0f |
0000 |
0000 |
0000 |
00 |
00 |
00 |
00 |
0x28 |
6060 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x30 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
0x38 |
2121 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x40 |
0606 |
0000 |
0000 |
0000 |
00 |
00 |
00 |
00 |
0x48 |
202020 |
000000 |
000000 |
000000 |
000000 |
000000 |
000000 |
000000 |
0x50 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
0x58 |
2121 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x60 |
0909 |
0000 |
0000 |
0000 |
00 |
00 |
00 |
00 |
0x68 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
Trois Noeud du tas avec leurs en-têtes (adresses abrégées) ; tete \(=\) 0x40 ; supprimer du pas à pas.
Énoncé
tete ;supprimer(&tete, 15) : pp, octets modifiés ;supprimer(&tete, 6).Correction
0x21 : 32 octets par nœud de 16.0x40), 15 (0x20), 9 (0x60), puis NULL.pp : &tete, puis 0x48 ; en 0x48 : 60 00 …\ 00 ; bloc 0x20 libéré.*pp est tete, qui devient 0x60 ; puis free du bloc 0x40.Lecture. Une pile rend d’abord le dernier élément rangé (LIFO). Sur le Vecteur, empiler est vec_ajouter, le sommet est la fin ; sur une liste, empiler est inserer_tete, le sommet est la tête. Mêmes sorties.
Lecture. Une file rend les éléments dans l’ordre d’arrivée (FIFO) : queue évite de parcourir la liste pour enfiler. Cas limites : enfiler dans une file vide met tete à jour ; défiler le dernier remet queue à NULL.
Lecture. debut avance modulo cap ; la case libre est (debut + n) % cap : 7 et 5 prennent les cases 3 et 0.
Lecture. Pleine (n == cap), la file passe à un bloc deux fois plus grand où les éléments sont recopiés dans l’ordre de la file : un realloc garderait l’ordre des cases (5 6 2 7).
| opération | tableau dynamique | liste chaînée |
|---|---|---|
| ajout en fin | \(O(1)\) amorti (doublement) | \(O(1)\) avec un pointeur queue, sinon \(O(n)\) |
| ajout en tête | \(O(n)\) (décalage) | \(O(1)\) |
| accès au \(k\)e élément | \(O(1)\) | \(O(k)\) |
| recherche d’une valeur | \(O(n)\) | \(O(n)\) |
| suppression d’un élément trouvé | \(O(n)\) (décalage) | \(O(1)\) si l’on tient pp |
| mémoire par élément | sa taille (plus les places libres) | un bloc : 32 octets pour un int (glibc) |
| pointeurs vers les éléments | invalidés par realloc |
stables jusqu’à la suppression |
Choix
Pile : tableau dynamique ou liste, toutes deux en \(O(1)\). File : tableau circulaire, ou liste avec tete et queue. Le tableau range les éléments côte à côte (un seul bloc, accès par indice) ; la liste évite toute recopie et garde chaque élément à son adresse.
FileC f, capacité 4, t mis à zéro par calloc, debut \(=\) n \(=\) 0. Opérations, dans l’ordre :
enfiler 5, 8, 3 ; defiler ; defiler ; enfiler 6, 1, 2 ; enfiler 4.
Énoncé
debut, n, t) après les trois premiers enfiler ;enfiler 6, 1, 2 ;enfiler 4, et l’état final.Correction
debut 0, n 3, t \(=\) 5 8 3 0.debut 2, n 1 ; t est inchangé (5 8 3 0) : défiler ne fait qu’avancer debut.debut 2, n 4, t \(=\) 1 2 3 6 : pleine.debut : 3 6 1 2, puis 4 en case 4 ; debut 0, n 5, t \(=\) 3 6 1 2 4 0 0 0.Résultat \(\rightarrow\)
3
Sortie d’erreur sous valgrind \(\rightarrow\)
32 (16 direct, 16 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
at 0x4846828: malloc (vgpreload_memcheck)
by 0x1091A5: nouveau (s5_fuite.c:9)
by 0x1091FB: main (s5_fuite.c:19)
...
definitely lost: 16 bytes in 1 blocks
indirectly lost: 16 bytes in 1 blocks
Lecture. Seule la tête est rendue. Le deuxième nœud n’est plus désigné que par un bloc libéré : definitely lost ; le troisième, par le deuxième : indirectly lost. LeakSanitizer donne la même répartition.
Compilateur \(\rightarrow\)
s5_liberer.c:21:32: warning: pointer 'p' used after 'free' [-Wuse-after-free]
Fin du processus : signal SIGSEGV (Segmentation fault)
Sortie d’erreur avec AddressSanitizer \(\rightarrow\)
==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000058
READ of size 8 at 0x502000000058 thread T0
#0 0x555555555383 in main s5_liberer.c:21
...
0x502000000058 is located 8 bytes inside of 16-byte region [0x502000000050,0x502000000060)
freed by thread T0 here:
#0 0x7ffff78fc4d8 in free asan_malloc_linux.cpp:52
#1 0x55555555535e in main s5_liberer.c:22
...
SUMMARY: AddressSanitizer: heap-use-after-free s5_liberer.c:21 in main
Lecture. free(p) a écrit à l’offset 8, là où était suivant, la clé de la glibc (aléatoire) : p reçoit cette clé, et le free de cette adresse arrête le processus (ici SIGSEGV). ASan s’arrête dès la lecture.
Résultat \(\rightarrow\)
1431655769
Sortie d’erreur avec AddressSanitizer \(\rightarrow\)
==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000030
READ of size 4 at 0x502000000030 thread T0
#0 0x555555555757 in main s5_pendant.c:49
...
0x502000000030 is located 0 bytes inside of 16-byte region [0x502000000030,0x502000000040)
freed by thread T0 here:
#0 0x7ffff78fc4d8 in free asan_malloc_linux.cpp:52
#1 0x555555555517 in supprimer s5_pendant.c:29
...
SUMMARY: AddressSanitizer: heap-use-after-free s5_pendant.c:49 in main
Lecture. x garde l’adresse du nœud 8 après sa suppression : un pointeur pendant ; sans outil, x->valeur lit 1431655769. Le rapport dit quoi (4 octets lus dans un bloc libéré), où (ligne 49) et qui a libéré (supprimer).
Résultat \(\rightarrow\)
8
Sortie d’erreur sous valgrind \(\rightarrow\)
Invalid read of size 4
at 0x109368: main (s5_pendant.c:49)
Address 0x4a7e090 is 0 bytes inside a block of size 16 free'd
at 0x484988F: free (vgpreload_memcheck)
by 0x109290: supprimer (s5_pendant.c:29)
by 0x109363: main (s5_pendant.c:48)
Block was alloc'd at
at 0x4846828: malloc (vgpreload_memcheck)
by 0x1091C5: nouveau (s5_pendant.c:9)
by 0x109324: main (s5_pendant.c:45)
| valgrind | ASan | |
|---|---|---|
| recompiler | non | oui |
| bloc libéré | mis de côté | interdit |
| après l’erreur | continue | s’arrête |
| fuites | bilan final | LeakSanitizer |
| valeur non initialisée | signalée à l’usage (test, appel, affichage) | non |
Lecture. valgrind exécute le programme tel quel et retarde la réutilisation des blocs libérés : la lecture rend encore 8, mais elle est signalée, avec le free et l’allocation. Les deux outils se complètent.
free par un pointeur pendantSortie d’erreur \(\rightarrow\)
free(): double free detected in tcache 2
Fin du processus : signal SIGABRT (Aborted)
Sortie d’erreur avec AddressSanitizer \(\rightarrow\)
==PID==ERROR: AddressSanitizer: attempting double-free on 0x502000000030 in thread T0:
#0 0x7ffff78fc4d8 in free asan_malloc_linux.cpp:52
#1 0x5555555556d4 in main s5_double.c:49
...
0x502000000030 is located 0 bytes inside of 16-byte region [0x502000000030,0x502000000040)
freed by thread T0 here:
#0 0x7ffff78fc4d8 in free asan_malloc_linux.cpp:52
#1 0x5555555554b7 in supprimer s5_double.c:29
...
SUMMARY: AddressSanitizer: double-free asan_malloc_linux.cpp:52 in free
Lecture. supprimer a déjà rendu le nœud : free(x) le libère une seconde fois. La glibc le détecte ici grâce à la clé du bloc libéré (section 6), sans garantie ; AddressSanitizer nomme les deux free.
int *t = malloc(3 * sizeof(int)); /* (a) */
for (int i = 0; i <= 3; i++)
t[i] = 10 * i;
Noeud *p = tete; /* (b) */
free(tete);
tete = NULL;
printf("%d\n", p->valeur);
for (int i = 0; i < 3; i++) { /* (c) */
Noeud *n = malloc(sizeof *n);
n->valeur = i;
n->suivant = tete;
tete = n;
}
tete = NULL;
Pile a = {malloc(4 * sizeof(int)), 0}; /* (d) */
Pile b = a;
free(a.t);
free(b.t);Noeud comme dans la séance ; Pile : struct { int *t; size_t n; }. Compilés avec -fsanitize=address : nommer l’erreur, la ligne du rapport qui la décrit, et la correction.
Correction
heap-buffer-overflow, WRITE of size 4, « 0 bytes after 12-byte region » : t[3] ; écrire i < 3.heap-use-after-free, READ of size 4 : p pend ; lire avant le free.tete.attempting double-free : b copie le pointeur, pas le bloc ; un seul free.mallocsize_t demande[6] = {0, 1, 24, 25, 40, 100};
unsigned char *p[6];
for (int i = 0; i < 6; i++)
p[i] = malloc(demande[i]); /* avant tout printf */
for (int i = 0; i < 6; i++) {
size_t entete = *(size_t *)(p[i] - 8); /* 8 octets avant le bloc */
size_t bloc = entete & ~(size_t)7; /* sans les 3 bits de poids faible */
printf("malloc(%3zu) : %p en-tete 0x%02zx bloc %3zu utile %3zu\n",
demande[i], (void *)p[i], entete, bloc, bloc - 8);
}Résultat \(\rightarrow\)
malloc( 0) : 0x5555555592a0 en-tete 0x21 bloc 32 utile 24
malloc( 1) : 0x5555555592c0 en-tete 0x21 bloc 32 utile 24
malloc( 24) : 0x5555555592e0 en-tete 0x21 bloc 32 utile 24
malloc( 25) : 0x555555559300 en-tete 0x31 bloc 48 utile 40
malloc( 40) : 0x555555559330 en-tete 0x31 bloc 48 utile 40
malloc(100) : 0x555555559360 en-tete 0x71 bloc 112 utile 104
Lecture. Ici glibc 2.39 : bloc \(=\) plus petit multiple de 16 \(\geq\) demande \(+\) 8, au moins 32 ; bit 0 : précédent occupé. Lire avant le bloc : comportement indéfini, ici pour observer seulement.
Adresses simplifiées ; a est en 0x5555555592a0, son en-tête en 0x555555559298. Ici glibc 2.39 ; la clé, tirée au hasard, n’est pas montrée.
Sortie \(\rightarrow\)
1
Lecture. malloc(20) : un bloc de 32 octets, dont 24 utiles ; l’en-tête 0x21 précède l’adresse rendue (a \(=\) 0x40).
Lecture. Le bloc suivant commence juste après : son en-tête en 0x58, b \(=\) 0x60.
Lecture. « Matam » et son terminateur : 6 octets.
Lecture. « Podor » dans b.
Lecture. ua garde l’adresse de a en entier, avant le free : après, le pointeur ne doit plus servir, pas même à une comparaison.
Lecture. free(a) : le bloc entre dans le tcache ; ses 8 premiers octets reçoivent le lien vers le suivant (NULL masqué par adresse >> 12), les 8 autres une clé contre le double free.
Lecture. malloc(20) reprend le dernier bloc libéré de cette taille : c \(=\) 0x40. La clé est effacée, le lien reste : un bloc rendu par malloc n’est pas initialisé.
Lecture. « Louga » recouvre le lien.
Lecture. c et l’ancienne valeur de a sont égales : un pointeur gardé après free désigne désormais les données de c.
void *a = malloc(40);
void *b = malloc(40);
uintptr_t ua = (uintptr_t)a; /* en entiers */
uintptr_t ub = (uintptr_t)b;
free(a);
free(b); /* dernier libere */
void *e = malloc(24); /* autre taille */
void *c = malloc(40);
void *d = malloc(40);
printf("a %#" PRIxPTR "\nb %#" PRIxPTR "\n", ua, ub);
printf("e %p\nc %p\nd %p\n", e, c, d);
free(c);
free(d);
free(e);Résultat \(\rightarrow\)
a 0x5555555592a0
b 0x5555555592d0
e 0x555555559300
c 0x5555555592d0
d 0x5555555592a0
Lecture. Les blocs libérés de 48 octets attendent dans une liste propre à leur taille : le dernier rendu est le premier repris (c reçoit b, d reçoit a). malloc(24), un bloc de 32, prend une place neuve.
Un allocateur pédagogique sur un tableau statique de 64 octets : en-tête de 8 octets, blocs multiples de 16, et une liste libre dont le lien s’écrit dans les données des blocs libres.
typedef struct Bloc {
size_t taille; /* en-tete */
struct Bloc *suivant; /* si libre */
} Bloc;
static alignas(16) unsigned char tas[TAILLE_TAS];
static Bloc *libre = NULL; /* liste libre */
static void mon_init(void)
{
libre = (Bloc *)(tas + 8);
libre->taille = TAILLE_TAS - 16;
libre->suivant = NULL;
}Lecture. Premier en-tête en tas + 8 : les données de chaque bloc tombent sur un multiple de 16. mon_free met le bloc en tête de la liste libre, sans fusionner les voisins. Lire le tableau à travers un Bloc * contourne la règle du type effectif (C17 6.5 §7) : un allocateur réel s’appuie sur son compilateur.
mon_malloc : premier bloc qui convientLecture. pp parcourt la liste libre comme dans supprimer. Le premier bloc assez grand est choisi (first fit) ; un reste d’au moins 16 octets devient un bloc libre, à sa place dans la liste.
Adresses : décalages dans tas, qui est en 0x555555558020 (.bss) ; les liens sont montrés de même.
Sortie \(\rightarrow\)
c == a : 1
Lecture. Un seul bloc libre de 48 octets en 0x08 : son en-tête, puis le lien NULL. Les données des blocs tomberont sur un multiple de 16.
Lecture. mon_malloc(6) : \(6+8\) arrondi à 16. Le bloc libre est découpé : 16 octets pour a (0x10), le reste, 32 octets, devient le bloc libre en 0x18.
Lecture. « Kolda » occupe 6 des 8 octets de a.
Lecture. mon_malloc(4) : le premier bloc libre assez grand est en 0x18 ; découpé à son tour, il laisse un bloc libre de 16 octets en 0x28.
Lecture. 221 \(=\) 0xdd, en little-endian.
Lecture. mon_free(a) : le bloc passe en tête de la liste libre ; son lien, écrit dans les données, recouvre « Kolda » (comme le tcache de la glibc).
Lecture. mon_malloc(8) : il faut 16 octets ; le premier bloc de la liste, en 0x08, convient exactement : il sort de la liste sans découpe, c \(=\) 0x10.
Lecture. « Fatick » et son terminateur : 7 octets.
Lecture. c réutilise le bloc de a : premier bloc libre qui convient (first fit), dernier libéré en tête de liste.
| Addr | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 |
|---|---|---|---|---|---|---|---|---|
0x00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
0x08 |
1010 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x10 |
4848 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x18 |
2020 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x20 |
0808 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x28 |
6f6f |
7272 |
0000 |
00 |
00 |
00 |
00 |
00 |
0x30 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
0x38 |
1010 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x40 |
eaea |
0707 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x48 |
1010 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x50 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0x58 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
00 |
TAILLE_TAS vaut 96 ; adresses : décalages dans tas, liens compris ; libre \(=\) 0x18.
Énoncé
0x08, et la liste libre ;mon_malloc(4) : bloc choisi, octets écrits ;mon_malloc(20).Correction
0x08 (16), 0x18 (32), 0x38 (16), 0x48 (16) ; libres : 0x18 \(\rightarrow\) 0x08 \(\rightarrow\) 0x48 ; 0x38 occupé.0x40 : ea 07, 2026 ; 0x28 : 6f 72 00, fin de « Ziguinchor », recouvert par le lien.0x18 (32) est découpé et rend 0x20 ; en-tête 16 ; bloc libre en 0x28 (16, lien 0x08) \(=\) libre.NULL, malgré 48 octets libres, en trois blocs de 16 : fragmentation.À retenir
realloc passe par un pointeur temporaire ; après un ajout qui peut réallouer, les pointeurs dans le bloc sont à recalculer.Noeud **, et un pointeur de pointeur qui avance de champ suivant en champ suivant traite insertion et suppression sans cas particulier.free.free et celle de l’allocation ; sans outil, ces erreurs passent souvent inaperçues.Séance 6 : pointeurs de fonctions et généricité
Les tableaux, listes, piles et files de cette séance ne rangent que des int. La séance suivante les rend génériques : des éléments désignés par void *, une taille d’élément passée en paramètre, et des pointeurs de fonctions pour comparer, afficher ou libérer les éléments (qsort, bsearch).
À préparer
Le TD et le TP de la séance 5 : dumps d’un tableau dynamique, d’une liste et du mini-allocateur, file circulaire, rapports de valgrind et d’AddressSanitizer à lire ; tableau dynamique, liste et allocateur à compléter sur machine.
realloc, erreurs, outils).Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, valgrind 3.22.0 ; adresses rendues déterministes par setarch -R.
Programmation C avancée — Séance 5 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Ligne 36 :
v, 24 octets dans la pile (pointeur, taille, capacité), non initialisés.Lecture.
malloc(8): la glibc réserve un bloc de 32 octets, dont 24 utiles, et en garde la taille dans l’en-tête, 8 octets avant l’adresse rendue (0x21: 32, bit 0 à 1).Lecture.
donnees[0] = 7, puistaillepasse à 1.Lecture.
taille\(=\)capacite\(=\) 2 : le bloc est plein.Lecture. Plein :
reallocdemande 16 octets. Le bloc en a 24 d’utiles : la glibc le garde en place, même adresse0x40, même en-tête ;capacitedouble.Lecture. Quatrième élément, sans réallocation.
Lecture. De nouveau plein : l’ajout suivant demandera 32 octets, plus que les 24 utiles du bloc.