Organisation de la mémoire
Le Mot Machine et l’Organisation de la Mémoire
Définition du Mot Machine
Un mot (mot machine ou word) est l’unité de base manipulée par un microprocesseur. La taille d’un mot s’exprime en bits et sert à classer les architectures : 32 bits ou 64 bits. Cette taille détermine la largeur des registres du processeur, la taille des adresses mémoire, et la quantité de données traitées en un cycle.
La taille du mot machine a des implications directes sur les capacités du système. Sur une architecture 32 bits, les adresses sont codées sur 32 bits, ce qui limite l’espace d’adressage à \(2^{32} = 4\) Go. Sur une architecture 64 bits, l’espace d’adressage théorique est de \(2^{64}\) octets, bien que les systèmes actuels n’utilisent que 48 bits d’adresse (256 To).
Vue de la Mémoire par un Programme
Un programme machine perçoit la mémoire comme un très grand tableau linéaire d’octets appelé mémoire virtuelle. Chaque octet possède une adresse unique, et l’ensemble des adresses possibles constitue l’espace d’adressage virtuel.
Adressage et Représentation des Données
Concepts Fondamentaux de l’Adressage
- Une adresse est un nombre qui identifie de manière unique un octet en mémoire
- Lorsqu’un objet occupe plusieurs octets, son adresse est celle de son premier octet (l’octet à l’adresse la plus basse)
- Pour accéder à un objet en mémoire, on a besoin de deux informations : son adresse et sa taille
En C, l’opérateur & permet d’obtenir l’adresse d’une variable. Cette adresse est toujours celle du premier octet occupé par la variable. Par exemple, si une variable int x est stockée aux adresses 0x1000 à 0x1003, alors &x vaut 0x1000.
Taille des Types selon l’Architecture
La taille des types primitifs en C dépend de l’architecture et du compilateur. Le standard C garantit seulement des tailles minimales et des relations d’ordre : sizeof(char) $\leq$ sizeof(short) $\leq$ sizeof(int) $\leq$ sizeof(long).
La différence de taille de long entre 32 et 64 bits est source de bugs. Pour un code portable, utilisez les types de <stdint.h> : int32_t, int64_t, uint8_t, etc.
La taille d’un pointeur correspond toujours à la taille du mot machine : 4 octets sur 32 bits, 8 octets sur 64 bits. C’est logique : un pointeur stocke une adresse, et une adresse occupe exactement un mot machine.
Endianness : Ordre des Octets en Mémoire
Le Problème de l’Ordre des Octets
Lorsqu’une donnée occupe plusieurs octets (comme un int de 4 octets), une question se pose : dans quel ordre sont stockés ces octets en mémoire ? Deux conventions existent, appelées endianness (du roman Les Voyages de Gulliver de Jonathan Swift).
- Little-Endian (« petit-boutiste ») : l’octet de poids faible (LSB - Least Significant Byte) est stocké à l’adresse la plus basse
- Big-Endian (« gros-boutiste ») : l’octet de poids fort (MSB - Most Significant Byte) est stocké à l’adresse la plus basse
Exemple Détaillé
Considérons la valeur hexadécimale 0x01234567 stockée à l’adresse 0x100. Cette valeur se décompose en 4 octets : 01 (MSB), 23, 45, 67 (LSB).
Quand l’Endianness Importe
L’endianness devient critique dans trois situations :
- Communication réseau : lorsqu’on envoie des données binaires entre machines de conventions différentes
- Lecture de fichiers binaires : un fichier créé sur une machine big-endian sera mal lu sur une machine little-endian
- Réinterprétation mémoire : lorsqu’on lit une zone mémoire avec un type différent de celui utilisé pour l’écrire
#include <stdio.h>
#include <stdint.h>
int is_little_endian(void) {
uint32_t x = 1; /* En mémoire : 01 00 00 00 (LE) ou 00 00 00 01 (BE) */
uint8_t *p = (uint8_t *)&x;
return p[0] == 1; /* Si le premier octet vaut 1, c'est little-endian */
}
int main(void) {
if (is_little_endian())
printf("Cette machine est Little-Endian\n");
else
printf("Cette machine est Big-Endian\n");
return 0;
}Les Pointeurs : Variables Contenant des Adresses
Définition et Déclaration
Un pointeur est une variable dont la valeur est une adresse mémoire. Le type du pointeur indique au compilateur comment interpréter les données à cette adresse et comment effectuer l’arithmétique des pointeurs.
La déclaration d’un pointeur utilise l’opérateur * :
int x = 42; /* Variable entière */
int *p; /* Déclaration : p est un pointeur vers un int */
p = &x; /* p contient maintenant l'adresse de x */
printf("x = %d\n", x); /* Affiche : x = 42 */
printf("&x = %p\n", &x); /* Affiche l'adresse de x */
printf("p = %p\n", p); /* Même adresse que &x */
printf("*p = %d\n", *p); /* Déréférencement : affiche 42 */Les Deux Aspects d’un Pointeur
Comme toute variable, un pointeur possède deux aspects :
Opérateurs sur les Pointeurs
&x |
Opérateur « adresse de » | Retourne l’adresse de la variable x |
|---|---|---|
*p |
Opérateur de déréférencement | Accède à la valeur à l’adresse p |
p + n |
Addition d’un entier | Avance de n \(\times\) sizeof(*p) octets |
p - n |
Soustraction d’un entier | Recule de n \(\times\) sizeof(*p) octets |
p - q |
Différence de pointeurs | Nombre d’éléments entre p et q |
Exemple Pas à Pas : Évolution de la Mémoire
Suivons l’exécution du code suivant sur une machine 32 bits little-endian :
int x = 0x3CD02700; /* &x = 0x04 */
int y = x + 3; /* &y = 0x18, y = 0x3CD02703 */
int *z; /* &z = 0x20 */
z = &y + 3; /* z = 0x18 + 3*4 = 0x24 */
*z = y; /* Écrit y à l'adresse 0x24 */
L’instruction z = &y + 3 illustre l’arithmétique des pointeurs : on ajoute 3 à un pointeur vers int, ce qui ajoute \(3 \times 4 = 12\) octets à l’adresse. Ainsi, 0x18 + 0x0C = 0x24.
Les Tableaux : Zones Mémoire Contiguës
Définition et Représentation en Mémoire
Un tableau est une séquence d’emplacements mémoire consécutifs stockant des éléments de même type. Les éléments sont indexés de 0 à \(n-1\) où \(n\) est la taille du tableau. Un tableau ne connaît pas sa propre taille.
int arr[5] = {10, 20, 30, 40, 50};
/* Si &arr[0] = 0x1000, alors :
arr[0] est à 0x1000, arr[1] à 0x1004, arr[2] à 0x1008, etc. */
Absence de Contrôle de Débordement
Le C ne vérifie jamais si un accès tableau est dans les limites. Un accès hors bornes compile sans erreur et s’exécute, mais corrompt la mémoire ou provoque un crash.
int notes[20];
for (int i = 0; i < 40; i++) /* Boucle jusqu'à 40 au lieu de 20 ! */
notes[i] = 0; /* Écrase 80 octets au-delà du tableau */
/* Ce code compile sans warning et peut corrompre la pile */Les conséquences d’un débordement dépendent de ce qui se trouve après le tableau en mémoire : autres variables, adresse de retour, etc. C’est une source majeure de vulnérabilités de sécurité (buffer overflow).
La figure suivante illustre l’évolution de la mémoire lors de manipulations de tableaux et de pointeurs, incluant des accès hors bornes dangereux.
- Étapes 0-1 : Déclaration de
int a[6]et affectations légales - Étape 2 :
a[6] = 0xBADécrit après le tableau (buffer overflow) - Étape 3 :
a[-1] = 0xBADécrit avant le tableau (aucune erreur !) - Étapes 4-5 : Manipulation via pointeur
pet arithmétique des pointeurs
Tableaux à Plusieurs Dimensions
Un tableau 2D est stocké en mémoire de manière linéaire, ligne par ligne (row-major order).
int mat[2][3] = {{1, 2, 3}, {4, 5, 6}};
/* En mémoire : 1, 2, 3, 4, 5, 6 (consécutifs) */
Relation Fondamentale Pointeurs-Tableaux
Conversion Tableau vers Pointeur
En C, le nom d’un tableau est automatiquement converti en pointeur vers son premier élément dans la plupart des contextes. Cette conversion est appelée array decay.
arr |
\(\equiv\) | &arr[0] |
|---|---|---|
arr + i |
\(\equiv\) | &arr[i] |
*(arr + i) |
\(\equiv\) | arr[i] |
p[i] |
\(\equiv\) | *(p + i) |
Ces équivalences expliquent pourquoi la syntaxe i[arr] est valide en C : elle est interprétée comme *(i + arr), ce qui est identique à *(arr + i) = arr[i].
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; /* Équivalent à : int *p = &arr[0]; */
/* Toutes ces expressions sont équivalentes et valent 30 : */
printf("%d\n", arr[2]); /* Notation tableau */
printf("%d\n", *(arr + 2)); /* Arithmétique pointeur */
printf("%d\n", p[2]); /* Pointeur avec notation tableau */
printf("%d\n", *(p + 2)); /* Pointeur avec déréférencement */
printf("%d\n", 2[arr]); /* Syntaxe exotique mais valide ! */Visualisation de Pointeurs sur un Tableau
Différences entre Tableau et Pointeur
Bien que très similaires dans leur utilisation, tableau et pointeur ne sont pas identiques :
Classes de Stockage et Durée de Vie des Tableaux
Les Trois Classes de Stockage
En C, un tableau peut être stocké dans trois zones mémoire différentes, chacune avec ses caractéristiques propres.
| Classe | Zone | Durée de vie | Taille |
|---|---|---|---|
| Automatique | Pile (stack) | Durée du bloc {} | Fixe à la compilation |
| Statique | Segment data | Toute l’exécution | Fixe à la compilation |
| Dynamique | Tas (heap) | Jusqu’à free() |
Variable à l’exécution |
int global[100]; /* Statique : segment data, existe toujours */
void exemple(int n) {
int local[10]; /* Automatique : sur la pile, détruit à la fin */
static int persistant[5]; /* Statique : segment data, conservé entre appels */
int *dynamique = malloc(n * sizeof(int)); /* Dynamique : sur le tas */
/* ... utilisation ... */
free(dynamique); /* Libération obligatoire */
} /* local est détruit ici, persistant survit */Le Problème du Retour de Tableau
Une erreur classique consiste à retourner un pointeur vers un tableau local (automatique).
/* ERREUR : retourne un pointeur vers une zone mémoire invalide */
int *creer_tableau_FAUX(void) {
int tab[5] = {1, 2, 3, 4, 5}; /* Alloué sur la pile */
return tab; /* Retourne l'adresse de tab */
} /* tab est détruit ici ! Le pointeur retourné est invalide */
int main(void) {
int *p = creer_tableau_FAUX();
printf("%d\n", p[0]); /* Comportement indéfini ! */
return 0;
}
Solutions pour Retourner un Tableau
Solution 1 : Tableau statique – La fonction retourne un pointeur vers un tableau qui persiste.
/* OK mais attention : un seul tableau partagé par tous les appels */
int *creer_tableau_static(void) {
static int tab[5] = {1, 2, 3, 4, 5}; /* Segment data */
return tab; /* Valide : tab existe toujours */
}
int main(void) {
int *p = creer_tableau_static();
printf("%d\n", p[0]); /* OK : affiche 1 */
/* ATTENTION : tous les appels partagent le même tableau */
int *q = creer_tableau_static();
q[0] = 99;
printf("%d\n", p[0]); /* Affiche 99 ! p et q pointent au même endroit */
return 0;
}Solution 2 : Allocation dynamique – Chaque appel crée un nouveau tableau.
/* OK : chaque appel alloue un nouveau tableau */
int *creer_tableau_dynamique(int n) {
int *tab = malloc(n * sizeof(int)); /* Sur le tas */
if (tab == NULL) return NULL; /* Vérifier l'allocation */
for (int i = 0; i < n; i++)
tab[i] = i + 1;
return tab; /* Valide : le tas n'est pas libéré au retour */
}
int main(void) {
int *p = creer_tableau_dynamique(5);
int *q = creer_tableau_dynamique(5); /* Deux tableaux distincts */
q[0] = 99;
printf("%d\n", p[0]); /* Affiche 1 : p et q sont indépendants */
free(p); /* OBLIGATOIRE : libérer la mémoire */
free(q);
return 0;
}Solution 3 : Passage par paramètre – L’appelant fournit le tableau.
/* L'appelant gère l'allocation */
void remplir_tableau(int *tab, int n) {
for (int i = 0; i < n; i++)
tab[i] = i + 1;
}
int main(void) {
int local[5]; /* Automatique */
remplir_tableau(local, 5); /* La fonction remplit le tableau fourni */
printf("%d\n", local[0]); /* OK : affiche 1 */
return 0;
}Allocation Dynamique : malloc, calloc, realloc, free
Principe de l’Allocation Dynamique
L’allocation dynamique permet de créer des zones mémoire dont la taille est déterminée à l’exécution. Ces zones sont allouées sur le tas (heap) et persistent jusqu’à leur libération explicite.
malloc(size) |
Alloue size octets (non initialisés) |
|---|---|
calloc(n, size) |
Alloue n × size octets (initialisés à 0) |
realloc(ptr, size) |
Redimensionne la zone pointée par ptr |
free(ptr) |
Libère la zone pointée par ptr |
#include <stdlib.h>
/* Allocation d'un tableau de n entiers */
int *tab = malloc(n * sizeof(int)); /* n * 4 octets (si int = 4) */
/* TOUJOURS vérifier le retour de malloc */
if (tab == NULL) {
fprintf(stderr, "Erreur allocation\n");
return 1;
}
/* Utilisation */
for (int i = 0; i < n; i++)
tab[i] = i * 10;
/* Libération OBLIGATOIRE */
free(tab);
tab = NULL; /* Bonne pratique : évite les accès après free */malloc vs calloc
/* malloc : mémoire NON initialisée (contient des valeurs aléatoires) */
int *p = malloc(5 * sizeof(int));
/* p[0], p[1], ... contiennent des valeurs indéterminées */
/* calloc : mémoire initialisée à ZÉRO */
int *q = calloc(5, sizeof(int));
/* q[0] = q[1] = ... = q[4] = 0 */
Redimensionnement avec realloc
int *tab = malloc(5 * sizeof(int));
/* ... remplissage de tab[0] à tab[4] ... */
/* Agrandir à 10 éléments */
int *nouveau = realloc(tab, 10 * sizeof(int));
if (nouveau == NULL) {
/* Échec : tab est toujours valide, mais pas agrandi */
free(tab);
return 1;
}
tab = nouveau; /* Succès : utiliser le nouveau pointeur */
/* tab[0]..tab[4] conservés, tab[5]..tab[9] non initialisés */
free(tab);Erreurs Classiques
/* 1. Oublier de vérifier malloc */
int *p = malloc(1000000000000); /* Peut échouer ! */
p[0] = 42; /* Crash si p == NULL */
/* 2. Oublier free (fuite mémoire) */
void fuite(void) {
int *p = malloc(100);
/* ... pas de free(p) ... */
} /* Mémoire perdue à chaque appel */
/* 3. Double free */
int *p = malloc(10);
free(p);
free(p); /* Comportement indéfini ! */
/* 4. Accès après free (use-after-free) */
int *p = malloc(10);
free(p);
p[0] = 42; /* Comportement indéfini ! */Tableaux à Longueur Variable (VLA)
Syntaxe et Utilisation
Depuis C99, il est possible de déclarer des tableaux dont la taille est une variable (et non une constante).
void traiter(int n) {
int tab[n]; /* VLA : taille déterminée à l'exécution */
for (int i = 0; i < n; i++)
tab[i] = i * i;
/* tab est automatique : détruit à la fin de la fonction */
}
int main(void) {
int taille;
printf("Taille : ");
scanf("%d", &taille);
int donnees[taille]; /* VLA : alloué sur la pile */
/* ... */
return 0;
}VLA vs Allocation Dynamique
- Les VLA sont alloués sur la pile, dont la taille est limitée (typiquement 8 Mo). Un VLA trop grand provoque un stack overflow.
- Les VLA ne peuvent pas être
staticni globaux. - Les VLA sont optionnels depuis C11 (macro
__STDC_NO_VLA__). - Pour de grands tableaux ou une durée de vie contrôlée, préférer
malloc.
/* Danger : VLA trop grand */
void danger(int n) {
int enorme[n]; /* Si n = 10000000, stack overflow probable */
}
/* Solution : utiliser malloc pour les grands tableaux */
void safe(int n) {
int *tab = malloc(n * sizeof(int));
if (tab == NULL) { /* gérer l'erreur */ }
/* ... */
free(tab);
}Arithmétique des Pointeurs en Détail
Règle Fondamentale
Toute opération arithmétique sur un pointeur est dimensionnée par la taille du type pointé. Ajouter \(n\) à un pointeur de type T* ajoute \(n \times \texttt{sizeof(T)}\) octets à l’adresse.
Cette règle permet de parcourir un tableau naturellement : p + 1 pointe toujours vers l’élément suivant, quelle que soit la taille des éléments.
Opérations Valides et Exemples
int arr[5] = {10, 20, 30, 40, 50}; /* &arr[0] = 0x1000 */
int *p = arr; /* p = 0x1000 */
int *q = &arr[3]; /* q = 0x100C */
/* Addition et soustraction d'entiers */
printf("p + 2 = %p\n", (void*)(p + 2)); /* 0x1008 (+8 octets) */
printf("q - 1 = %p\n", (void*)(q - 1)); /* 0x1008 (-4 octets) */
/* Différence de pointeurs : résultat en ÉLÉMENTS, pas en octets */
printf("q - p = %ld\n", q - p); /* 3 (éléments) */
/* Pour obtenir la différence en octets, cast vers char* */
printf("(char*)q - (char*)p = %ld\n", (char*)q - (char*)p); /* 12 octets */
/* Comparaisons */
if (p < q) printf("p est avant q en mémoire\n");
/* Incrémentation */
p++; /* p passe de 0x1000 à 0x1004 */Réinterprétation Mémoire par Cast
Un cast de pointeur permet de réinterpréter une zone mémoire avec un type différent. C’est utile pour examiner la représentation interne des données.
int x = 0x11223344;
char *cp = (char *)&x; /* Vue de x comme séquence d'octets */
printf("Octets de x en mémoire:\n");
for (int i = 0; i < 4; i++) {
printf(" octet %d: 0x%02X\n", i, (unsigned char)cp[i]);
}
/* Sur little-endian : 44 33 22 11 */
/* Sur big-endian : 11 22 33 44 */
Les Chaînes de Caractères
Définition et Représentation
Une chaîne de caractères est un tableau de char terminé par le caractère nul '\0' (code ASCII 0x00). Ce caractère de terminaison permet aux fonctions de déterminer où finit la chaîne.
char str1[] = "Hello"; /* Tableau de 6 chars : H e l l o \0 */
char str2[10] = "Hi"; /* Tableau de 10 chars : H i \0 ? ? ? ? ? ? ? */
char str3[] = {'A', 'B', 0}; /* Équivalent à "AB" */
Table ASCII Partielle
Les caractères sont stockés selon leur code ASCII (American Standard Code for Information Interchange).
Relations utiles : 'A' = 0x41, 'a' = 0x61, différence = 0x20. Les chiffres '0' à '9' sont aux codes 0x30 à 0x39.
Différence Capitale : Tableau vs Pointeur sur Chaîne
char arr[] = "Hello"; /* Tableau alloué sur la pile, MODIFIABLE */
char *ptr = "Hello"; /* Pointeur vers le segment de données, LECTURE SEULE */
arr[0] = 'J'; /* OK : modifie la copie locale -> "Jello" */
ptr[0] = 'J'; /* ERREUR : segfault ! Le segment données est protégé */
arr = ptr; /* ERREUR de compilation : arr n'est pas modifiable */
ptr = arr; /* OK : ptr peut pointer vers une autre adresse */
Fonctions Standard de Manipulation
Les fonctions de <string.h> opèrent sur des chaînes terminées par '\0'.
strlen(s) |
Longueur de s | Ne compte pas le '\0' |
|---|---|---|
strcpy(dst, src) |
Copie src dans dst | Danger si dst trop petit |
strncpy(dst, src, n) |
Copie au plus n chars | Plus sûr |
strcat(dst, src) |
Concatène src à dst | Danger si dst trop petit |
strcmp(s1, s2) |
Compare s1 et s2 | 0 si égales, <0 si s1<s2, >0 si s1>s2 |
strchr(s, c) |
Cherche c dans s | Retourne pointeur ou NULL |
#include <string.h>
char dest[20];
strcpy(dest, "Hello"); /* dest = "Hello" */
strcat(dest, " World"); /* dest = "Hello World" */
printf("Longueur: %zu\n", strlen(dest)); /* 11 */
printf("Taille: %zu\n", sizeof(dest)); /* 20 */
if (strcmp(dest, "Hello World") == 0)
printf("Chaînes identiques\n");Visualisation de la Mémoire en C
Le programme suivant permet d’examiner la représentation mémoire des données.
#include <stdio.h>
#include <stdint.h>
typedef unsigned char *byte_pointer;
/* Affiche len octets à partir de start, 2 caractères par octet */
void show_bytes(byte_pointer start, size_t len) {
printf(" ");
for (size_t i = 0; i < len; i++)
printf("%.2x ", start[i]);
printf("\n");
}
void show_int(int x) {
printf("int %d (0x%X):\n", x, x);
show_bytes((byte_pointer)&x, sizeof(int));
}
void show_float(float x) {
printf("float %f:\n", x);
show_bytes((byte_pointer)&x, sizeof(float));
}
void show_pointer(void *x) {
printf("pointer %p:\n", x);
show_bytes((byte_pointer)&x, sizeof(void *));
}
int main(void) {
int val = 0x12345678;
float f = 1.0f;
show_int(val);
show_float(f);
show_pointer(&val);
/* Vérification endianness */
printf("\nCette machine est ");
uint32_t test = 1;
if (*(uint8_t*)&test == 1)
printf("Little-Endian\n");
else
printf("Big-Endian\n");
return 0;
}int 305419896 (0x12345678):
78 56 34 12
float 1.000000:
00 00 80 3f
pointer 0x7ffd5c9fa9ac:
ac a9 9f 5c fd 7f 00 00
Cette machine est Little-Endian
Récapitulatif du Chapitre
- Le mot machine (32 ou 64 bits) détermine la taille des adresses et des pointeurs
- La mémoire est un tableau linéaire d’octets ; chaque octet a une adresse unique
- L’adresse d’un objet est l’adresse de son premier octet
- Endianness : Little-Endian (Intel) stocke le LSB à l’adresse basse ; Big-Endian fait l’inverse
- Un tableau occupe des cases mémoire consécutives ; le C ne vérifie pas les débordements
- Le nom d’un tableau est converti en pointeur vers son premier élément
- Équivalences :
arr[i]\(\equiv\)*(arr+i)et&arr[i]\(\equiv\)arr+i - L’arithmétique des pointeurs est dimensionnée par
sizeof(type pointé) - Une chaîne est un tableau de
charterminé par'\0' - Tableau vs pointeur sur chaîne : le tableau est modifiable, le littéral ne l’est pas
Adresse de arr[i] |
&arr[0] + i * sizeof(arr[0]) |
|---|---|
ptr + n |
Ajoute n * sizeof(*ptr) octets |
ptr1 - ptr2 |
Nombre d’éléments (pas d’octets) |
strlen(s) |
Nombre de caractères avant '\0' |
sizeof(arr) |
Taille totale du tableau en octets |
sizeof(ptr) |
Taille du pointeur (4 ou 8) |
Ressources du chapitre
- PDF du cours · Le chapitre en PDF (992 Ko)
- TD · TD — Organisation de la mémoire (108 Ko)
- TD · TD — Exercices d’application (123 Ko)
- TP · TP — Organisation de la mémoire (128 Ko)