TP — Séance 3 — Organisation de la mémoire

Observer les octets, les adresses, la pile et le tas

TP de la séance 3 de Programmation C avancée : adresses, pointeurs, tableaux, pile et tas.
Auteur

Dr. El Hadji Bassirou TOURÉ · DMI/FST/UCAD

NoteObjectif et organisation

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.

ImportantCommandes

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;
}
  1. Observer. Avant toute exécution, écrire sur papier les octets attendus pour nb = 250000, alt = -45 et la chaîne "Sedhiou". Compiler et exécuter, puis comparer avec la ligne affichée pour nb.
  2. Modifier. Écrire show_short, show_double et show_pointer (TODO 1), les appeler pour alt, note et ptr (TODO 2), puis afficher les sizeof(region) octets de region (TODO 3). Retirer les lignes (void).
  3. Expliquer. Justifier les octets de alt par le complément à deux, et ceux de note = 13.5 par le format IEEE 754 (signe, exposant, mantisse).
  4. Expliquer. Comparer les octets affichés par show_pointer(ptr) avec la valeur affichée par %p. Combien d’octets valent 00, et pourquoi ?
  5. Modifier. Ajouter float f = 13.5f; et afficher ses octets. Comparer avec ceux de note.

É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;
}
  1. Observer. Exécuter avec setarch -R. Ranger les neuf objets par adresse croissante.
  2. Expliquer. Associer chaque objet à sa classe de stockage (automatique, statique, dynamique) et à son segment (texte, .rodata, .data, .bss, tas, zone mmap, pile). Que signifient les droits r-xp, r--p et rw-p ?
  3. Expliquer. Pourquoi *grand n’est-il pas dans [heap], contrairement à *petit ?
  4. Modifier. Remplacer malloc(500000) par malloc(100000), puis par malloc(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 &notes[5] - &notes[1] et (char *)&notes[5] - (char *)&notes[1] */
    (void)b;
    taille(notes);
    return 0;
}
  1. Observer. Avant d’exécuter les TODO, calculer à la main l’écart en octets de notes + 4, de releves + 2, et de &grille[i][j] pour \((i, j) = (0, 4), (1, 0), (2, 3)\).
  2. Modifier. Compléter les TODO 1 à 3 et vérifier les prédictions.
  3. Expliquer. Pourquoi &notes[5] - &notes[1] et (char *)&notes[5] - (char *)&notes[1] diffèrent-ils ?
  4. Modifier. Compléter le TODO 4. Comparer sizeof t dans taille et sizeof notes dans main, 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;
}
  1. Observer. Compiler sans -fsanitize, exécuter. compteur a-t-il changé ? Situer &compteur par rapport à mesures et à &mesures[5], et expliquer le résultat.
  2. 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.
  3. 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;
}
  1. 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 ?
  2. Modifier. Corriger de deux façons : en dimensionnant nom, puis en gardant un tableau de 6 octets et une copie bornée par snprintf. 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;
}
  1. Observer. Compiler et exécuter. Qu’affiche la dernière ligne, et pourquoi n’est-ce pas 17 ?
  2. Observer. Exécuter sous valgrind --leak-check=full. Relever les trois erreurs signalées (nature, ligne, taille du bloc) et le bilan des fuites.
  3. 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 ?
  4. Modifier. Corriger les trois erreurs, puis vérifier que valgrind affiche All heap blocks were freed et 0 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;
}
  1. Modifier. Écrire afficher_zone selon 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.
  2. Observer. Exécuter. Retrouver dans la sortie le terminateur de texte et les quatre entiers de valeurs.
  3. Expliquer. Justifier les octets de 65536 et de -2.
  4. Observer. Écrire "Saint-Louis 2026" dans un fichier ville.txt (printf "Saint-Louis 2026" > ville.txt) et comparer avec la sortie de od -A x -t x1z ville.txt.

Pour cette séance

Le cours

Les slides

À lire en ligne

À télécharger

Retour au sommet