TP — Séance 3 — Organisation de la mémoire
Observer les octets, les adresses, la pile et le tas
Vérifier sur machine ce que le cours décrit : les octets d’un objet, les zones où vivent les objets, l’arithmétique des pointeurs, et les erreurs que détectent AddressSanitizer et valgrind. Le TP compte six étapes. Chaque étape fournit un programme du dossier TP_S03_code_depart/ (avec un Makefile), à compiler et exécuter ; les questions demandent d’observer une sortie, de l’expliquer avec le cours, puis de modifier le programme. Les réponses se notent dans un fichier reponses.txt, avec les sorties copiées.
Décompresser TP_S03_code_depart.zip, se placer dans le dossier TP_S03_code_depart/ qu’il crée, puis lancer le conteneur depuis ce dossier :
docker run -it --rm --security-opt seccomp=unconfined \
-v "$(pwd)":/workspace elbachirtoure/c-advanced-course:v2.0 # lancer le conteneur
Windows, PowerShell : -v "${PWD}:/workspace" à la place de -v "$(pwd)":/workspace ; Git Bash et autres cas : TP-1, étape 2. Dans le conteneur, ce dossier est /workspace :
make # compile tout (gcc -std=c17 -Wall -Wextra -O0 -g)
make etape2_zones # un seul programme (apres une modification)
make -B etape3_tableaux # recompiler meme a jour (relire les avertissements de gcc)
make asan # etape4_debord_asan, etape4_chaine_asan (-fsanitize=address)
make clean # retirer les executables
setarch -R ./prog # execution sans alea des adresses
valgrind --leak-check=full ./prog # fuites et acces invalides au tas
Avec setarch -R, les adresses affichées se répètent d’une exécution à l’autre ; elles diffèrent toutefois d’une machine à l’autre. Les questions portent sur les écarts et les octets, qui ne dépendent pas de la machine.
Étape 1 : les octets d’un objet
Le programme etape1_octets.c reprend la fonction show_bytes du cours : elle affiche les octets d’un objet à partir de son adresse et de sa taille.
/* etape1_octets.c -- TP Seance 3, etape 1 : les octets d'un objet.
Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape1_octets.c -o etape1_octets */
#include <stdio.h>
typedef unsigned char *byte_pointer;
/* affiche len octets a partir de start, deux chiffres hexadecimaux par octet */
void show_bytes(byte_pointer start, size_t len)
{
for (size_t i = 0; i < len; i++)
printf(" %.2x", start[i]);
printf("\n");
}
void show_int(int x)
{
printf("int %d :", x);
show_bytes((byte_pointer)&x, sizeof(int));
}
/* TODO 1 : ecrire show_short, show_double et show_pointer sur le modele de show_int */
int main(void)
{
int nb = 250000;
short alt = -45;
double note = 13.5;
char region[] = "Sedhiou";
int *ptr = &nb;
show_int(nb);
/* TODO 2 : afficher les octets de alt, note et ptr avec les fonctions du TODO 1 */
/* TODO 3 : afficher les sizeof(region) octets de region avec show_bytes */
printf("ptr = %p\n", (void *)ptr);
(void)alt;
(void)note;
(void)region;
return 0;
}- Observer. Avant toute exécution, écrire sur papier les octets attendus pour
nb = 250000,alt = -45et la chaîne"Sedhiou". Compiler et exécuter, puis comparer avec la ligne affichée pournb. - Modifier. Écrire
show_short,show_doubleetshow_pointer(TODO 1), les appeler pouralt,noteetptr(TODO 2), puis afficher lessizeof(region)octets deregion(TODO 3). Retirer les lignes(void). - Expliquer. Justifier les octets de
altpar le complément à deux, et ceux denote = 13.5par le format IEEE 754 (signe, exposant, mantisse). - Expliquer. Comparer les octets affichés par
show_pointer(ptr)avec la valeur affichée par%p. Combien d’octets valent00, et pourquoi ? - Modifier. Ajouter
float f = 13.5f;et afficher ses octets. Comparer avec ceux denote.
Étape 2 : les zones d’un processus
Le programme etape2_zones.c affiche l’adresse de neuf objets et, pour chacun, la projection de /proc/self/maps qui le contient (droits d’accès et nom).
/* etape2_zones.c -- TP Seance 3, etape 2 : ou vivent les objets.
zone() cherche dans /proc/self/maps la projection qui contient une adresse. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int population = 1100000; /* globale initialisee */
int compteur_global; /* globale a zero */
const int code_region = 21; /* constante globale */
void zone(const char *nom, const void *adr)
{
FILE *f = fopen("/proc/self/maps", "r");
char ligne[512];
unsigned long a = (unsigned long)adr;
while (f != NULL && fgets(ligne, sizeof ligne, f) != NULL) {
unsigned long debut, fin;
char droits[8];
char chemin[256] = "";
sscanf(ligne, "%lx-%lx %7s %*s %*s %*s %255s", &debut, &fin, droits, chemin);
if (a >= debut && a < fin) {
const char *base = strrchr(chemin, '/');
const char *z = base != NULL ? base + 1 : chemin;
if (z[0] == '\0')
z = "(anonyme)";
printf("%-14s %14p %s %s\n", nom, adr, droits, z);
break;
}
}
if (f != NULL)
fclose(f);
}
int main(void)
{
static int appels = 0; /* statique locale */
int locale = 7; /* automatique */
int *petit = malloc(16); /* tas */
char *grand = malloc(500000); /* 500 000 octets */
const char *ville = "Ziguinchor";
zone("main", (void *)main);
zone("\"Ziguinchor\"", ville);
zone("code_region", &code_region);
zone("population", &population);
zone("compteur", &compteur_global);
zone("appels", &appels);
zone("*petit", petit);
zone("*grand", grand);
zone("locale", &locale);
free(petit);
free(grand);
return 0;
}- Observer. Exécuter avec
setarch -R. Ranger les neuf objets par adresse croissante. - Expliquer. Associer chaque objet à sa classe de stockage (automatique, statique, dynamique) et à son segment (texte,
.rodata,.data,.bss, tas, zonemmap, pile). Que signifient les droitsr-xp,r--petrw-p? - Expliquer. Pourquoi
*grandn’est-il pas dans[heap], contrairement à*petit? - Modifier. Remplacer
malloc(500000)parmalloc(100000), puis parmalloc(200000). Noter la zone obtenue dans chaque cas et en déduire un encadrement du seuil de la glibc.
Étape 3 : adresses des éléments d’un tableau
/* etape3_tableaux.c -- TP Seance 3, etape 3 : adresses des elements d'un tableau. */
#include <stdio.h>
/* TODO 4 : afficher sizeof t dans cette fonction et comparer avec sizeof notes dans main */
void taille(short t[6])
{
(void)t;
}
int main(void)
{
short notes[6] = {12, 15, 9, 18, 11, 14};
double releves[4] = {28.5, 31.0, 33.5, 30.0};
int grille[3][5] = {{0}};
char *b = (char *)grille;
printf("notes = %p\n", (void *)notes);
printf("releves = %p\n", (void *)releves);
printf("grille = %p\n", (void *)grille);
/* TODO 1 : afficher notes + 4 et releves + 2, et leur ecart en octets avec le debut */
/* TODO 2 : afficher (char *)&grille[i][j] - b pour (i, j) = (0, 4), (1, 0), (2, 3) */
/* TODO 3 : afficher ¬es[5] - ¬es[1] et (char *)¬es[5] - (char *)¬es[1] */
(void)b;
taille(notes);
return 0;
}- Observer. Avant d’exécuter les TODO, calculer à la main l’écart en octets de
notes + 4, dereleves + 2, et de&grille[i][j]pour \((i, j) = (0, 4), (1, 0), (2, 3)\). - Modifier. Compléter les TODO 1 à 3 et vérifier les prédictions.
- Expliquer. Pourquoi
¬es[5] - ¬es[1]et(char *)¬es[5] - (char *)¬es[1]diffèrent-ils ? - Modifier. Compléter le TODO 4. Comparer
sizeof tdanstailleetsizeof notesdansmain, et lire l’avertissement de gcc.
Étape 4 : débordements et AddressSanitizer
Les deux programmes de cette étape contiennent chacun une erreur volontaire.
/* etape4_debord.c -- TP Seance 3, etape 4 : un tour de boucle de trop (erreur volontaire). */
#include <stdio.h>
int main(void)
{
int compteur = 100;
int mesures[5];
printf("&compteur = %p, mesures = %p, &mesures[5] = %p\n",
(void *)&compteur, (void *)mesures, (void *)&mesures[5]);
for (int i = 0; i <= 5; i++)
mesures[i] = i * i;
printf("compteur = %d, mesures[4] = %d\n", compteur, mesures[4]);
return 0;
}- Observer. Compiler sans
-fsanitize, exécuter.compteura-t-il changé ? Situer&compteurpar rapport àmesureset à&mesures[5], et expliquer le résultat. - Observer. Compiler avec
-fsanitize=address(make asan) et exécuter./etape4_debord_asan. Relever dans le rapport la nature de l’erreur, la ligne fautive, la variable débordée et l’intervalle qu’elle occupe. - Modifier. Corriger la boucle, puis vérifier que le programme compilé avec AddressSanitizer ne signale plus rien.
/* etape4_chaine.c -- TP Seance 3, etape 4 : une chaine trop longue (erreur volontaire). */
#include <stdio.h>
#include <string.h>
int main(void)
{
char nom[8];
strcpy(nom, "Kedougou");
printf("nom = %s, strlen = %zu, sizeof = %zu\n", nom, strlen(nom), sizeof nom);
return 0;
}- Observer. Compiler (lire l’avertissement de gcc) et exécuter sans puis avec AddressSanitizer. Combien d’octets
strcpyécrit-il, et combien le tableau en contient-il ? - Modifier. Corriger de deux façons : en dimensionnant
nom, puis en gardant un tableau de 6 octets et une copie bornée parsnprintf. Qu’affiche la seconde version ?
Étape 5 : le tas sous valgrind
/* etape5_tas.c -- TP Seance 3, etape 5 : trois erreurs volontaires dans l'usage du tas. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *dupliquer(const char *s)
{
char *c = malloc(strlen(s));
if (c != NULL)
strcpy(c, s);
return c;
}
int main(void)
{
const char *villes[3] = {"Tambacounda", "Louga", "Fatick"};
for (int k = 0; k < 3; k++) {
char *copie = dupliquer(villes[k]);
printf("%d : %s\n", k, copie);
}
int *scores = malloc(4 * sizeof *scores);
if (scores == NULL)
return 1;
scores[0] = 17;
free(scores);
printf("scores[0] = %d\n", scores[0]);
return 0;
}- Observer. Compiler et exécuter. Qu’affiche la dernière ligne, et pourquoi n’est-ce pas 17 ?
- Observer. Exécuter sous
valgrind --leak-check=full. Relever les trois erreurs signalées (nature, ligne, taille du bloc) et le bilan des fuites. - Expliquer. Pourquoi le bloc alloué pour
"Tambacounda"est-il trop petit d’un octet ? Combien d’octets sont perdus au total, et d’où vient ce nombre ? - Modifier. Corriger les trois erreurs, puis vérifier que valgrind affiche
All heap blocks were freedet0 errors.
Étape 6 : un afficheur de zone
/* etape6_hexdump.c -- TP Seance 3, etape 6 : un afficheur de zone. */
#include <stdio.h>
#include <stddef.h>
/* TODO : afficher n octets a partir de p, 16 par ligne :
- le decalage sur 4 chiffres hexadecimaux ;
- les 16 octets en hexadecimal ;
- les caracteres imprimables (0x20 a 0x7e), un point pour les autres. */
void afficher_zone(const void *p, size_t n)
{
(void)p;
(void)n;
}
int main(void)
{
char texte[] = "Saint-Louis 2026";
int valeurs[4] = {1, 255, 65536, -2};
afficher_zone(texte, sizeof texte);
afficher_zone(valeurs, sizeof valeurs);
return 0;
}- Modifier. Écrire
afficher_zoneselon le commentaire : chaque ligne donne le décalage sur 4 chiffres hexadécimaux, 16 octets, puis leurs caractères. La dernière ligne, incomplète, garde la colonne des caractères alignée. - Observer. Exécuter. Retrouver dans la sortie le terminateur de
texteet les quatre entiers devaleurs. - Expliquer. Justifier les octets de
65536et de-2. - Observer. Écrire
"Saint-Louis 2026"dans un fichierville.txt(printf "Saint-Louis 2026" > ville.txt) et comparer avec la sortie deod -A x -t x1z ville.txt.
Pour cette séance
Le cours
Les slides
À lire en ligne
À télécharger
- Code de départ du TP (ZIP, 4 Ko)