Академический Документы
Профессиональный Документы
Культура Документы
com/memory-allocation/
hackndo
Gestion de la mémoire
11 Jan 2015 · 10 min Auteur : Pixis
Aujourd’hui, je vais tenter de rassembler tout ce que j’ai pu comprendre sur la gestion de la
mémoire lors de l’exécution d’un programme. Cet article est écrit en vu de comprendre
l’exploitation de certaines failles applicatives, telles que le buffer overflow, le heap overflow
ou encore la format string, failles que je décrirai dans les prochains articles.
Mémoire virtuelle
Les processus tournant sur une machine ont besoin de mémoire, et dans un ordinateur, la
quantité de mémoire est limitée. Il faut donc que les processus aillent chercher de la
mémoire disponible pour pouvoir travailler. Cependant, les processus tournent de nos jours
dans des systèmes d’exploitation multi-tâches. Plusieurs processus s’exécutent en même
temps. Que se passerait-il si deux processus voulaient accéder, au même instant, à la
même zone mémoire ? Et surtout, si jamais un processus écrivait dans une zone mémoire,
puis un autre processus écrasait cette même zone mémoire avec ses propres données,
alors le processus A, le pauvre, pensera retrouver ses données, mais il trouvera en fait les
données de B. Et là, c’est le drame ! Il faudrait alors que les processus communiquent en
permanence entre eux pour savoir qui fait quoi, où et quand. Ce serait une vraie perte de
temps et d’une complexité effroyable pour ce problème.
C’est là qu’intervient la mémoire virtuelle : Les processus ne vont plus piocher directement
dans la mémoire physique. On les met dans des bacs à sable (sand box), en leur allouant
une plage de mémoire virtuelle (de 4Go pour les machines 32 bits), en leur faisant croire
qu’ils sont les seuls à s’exécuter sur la machine. C’est alors que le kernel intervient, et
effectue le lien entre les différentes plages de mémoires virtuelles et la mémoire réelle.
Ceci est fait par le biais de tables de pages (page tables). Voici un schéma pour y voir plus
clair :
Chaque processus a sa propre table de pages. Cependant, si l’adressage virtuel est activé,
il s’applique à tous les programmes qui tournent sur la machine, kernel compris. Ainsi, il
faut réserver une portion de l’espace virtuel de chaque programme pour le noyau !
Segmentation de la mémoire
Ainsi, nous allons voir ici comment est segmentée la mémoire d’un programme compilé
lorsqu’il est chargé en mémoire afin de créer un processus (Son image, une sorte
d’instance, si ça vous parle).
1. Texte (.text)
2. Données (.data)
3. bss (.bss)
1. Tas (heap)
2. Pile (stack)
Rapidement, la première section texte (.text) est celle qui contient le code du programme,
et plus exactement les instructions en langage machine. C’est une section en lecture seule,
une fois qu’elle a été définie, elle est immuable. Elle sert seulement à stocker du code, pas
des variables. Des erreurs de programmation peuvent entraîner cette fameuse erreur :
“Segmentation Fault”, qui indique à l’utilisateur qu’une écriture non autorisée a tenté d’être
faite dans cette zone mémoire.
Du fait de son immuabilité, c’est une zone mémoire de taille fixe. Le programme démarrera
donc au début de ce segment, puis il va lire les instructions une par une. Cependant, cette
lecture n’est pas linéaire. En effet, avec le code haut niveau que nous produisons, il existe
beaucoup de structures de contrôles qui engendrent des appels à des bout de code qui ne
sont pas les uns à la suite des autres. On expliquera par la suite comment l’exécution du
programme fonctionne, notamment avec l’aide des registres.
La section de données (data) et la section bss stockent les variables globales et statiques
du programme. Si ces données sont initialisées, elles sont enregistrées dans la section
data, tandis que les autres sont dans la section bss. Ce sont également des zones
mémoires de taille fixe. Malgré la possibilité en écriture, les variables finales et statiques ne
changeront pas au cours de l’exécution du programme ou du contexte. C’est parce qu’elles
sont dans cette zone mémoire qu’elles peuvent persister.
#include <stdio.h>
int main(void) {
return 0;
}
Maintenant, ajoutons une variable globale non initialisée et étudions les tailles des
différentes sections à nouveau
#include <stdio.h>
int global;
int main(void) {
return 0;
}
On remarque que la section bss a augmenté de 4 octets pour stocker la variable statique
non-initialisée. Si de la même manière on ajoute une variable statique à l’intérieur de la
fonction main()
#include <stdio.h>
int global;
int main(void) {
static int var;
return 0;
}
Encore une fois, on remarque que bss a augmenté de 4 octets pour stocker cette variable.
Si maintenant on initialise la variable var
#include <stdio.h>
int global;
int main(void) {
static int var = 10;
return 0;
}
Cette fois-ci, la variable n’est plus stockée dans la section bss, mais dans la section data,
puisqu’on remarque qu’elle est passée de 560 à 564 alors que la section bss a diminué de
4 octets. Enfin, si on initialise également la variable globale global
#include <stdio.h>
int main(void) {
static int var = 10;
return 0;
Les deux variables sont stockées dans la section data, et non plus dans la section bss.
Le tas (heap) est, quant à lui, manipulable par le programmeur. C’est la zone dans laquelle
sont écrites les zones mémoires allouées dynamiquement (malloc() ou calloc()). Tout
comme la pile, cette zone mémoire n’a pas de taille fixe. Elle augmente et diminue en
fonction des demandes du programmeur, qui peut réserver ou supprimer des blocs via des
algorithmes d’allocation ou de libération pour une utilisation future. Plus la taille du tas
augmente, plus les adresses mémoires augmentent, et s’approchent des adresses
mémoires de la pile. La taille des variables dans le tas n’est pas limitée (sauf limite
physique de la mémoire), contrairement à la pile.
Par ailleurs, les variables stockées dans le tas sont accessibles partout dans le
programme, par l’intermédiaire des pointeurs. Cependant, l’accès aux variables stockées
dans le tas ne se faisant qu’avec des pointeurs, cela ralentit un peu ces accès,
contrairement aux accès dans la pile.
La pile (stack) possède également une taille variable, mais plus sa taille augmente, plus
les adresses mémoires diminuent, en s’approchant du haut du tas. C’est ici qu’on retrouve
les variables locales des fonctions ainsi que le cadre de pile (stack frame) de ces fonctions.
La stack frame d’une fonction est une zone mémoire, dans la pile, dans laquelle toutes les
informations, nécessaires à l’appel de cette fonction, sont stockées. S’y trouvent également
les variables locales de la fonction.
Vous avez donc j’espère une idée plus claire de la segmentation de la mémoire lors de
l’exécution d’un programme. Cependant il manque une notion importante qui est la gestion
des registres. En expliquant leur fonctionnement et leur utilité, nous seront à même de
mieux comprendre la notion de stack frame.
Registres
Les registres sont des emplacements mémoire qui sont à l’intérieur du processeur. Or dans
un ordinateur, les emplacements mémoire les plus proches du processeur sont ceux à qui
il est le plus rapide d’accéder, mais également les plus chers. Ainsi, plus on s’éloigne du
processeur, plus les accès sont longs, mais les coûts sont faibles. Les registres sont les
emplacements mémoire les plus proches (puisqu’ils sont internes au processeur), c’est
alors la mémoire la plus rapide de l’ordinateur. Cette pyramide de la mémoire est
représentée dans la figure suivante, qui oppose le coût de la mémoire à son temps d’accès
par le processeur :
Le processeur x86 32 bits possède (logiquement) 8 registres généraux (EAX, EBX, ECX,
EDX, ESP, EBP, ESI, EDI)
Pour les processeurs 64 bits, il y a 16 registres logiques. Mais dans la réalité, les derniers
processeurs en ont 168, pour pouvoir paralléliser les instructions.
Les 4 EAX, EBX, ECX et EDX appelés Accumulateur, Base, Compteur, Données ont
pour rôle de stocker des données temporaires pour le processeur lorsqu’il exécute un
programme.
Les 4 autres registres ESP, EBP, ESI et EDI appelés Pointeur de Pile (Stack),
Pointeur de Base, Index de Source et Index de Destination sont plutôt utilisés en tant
que pointeurs et index, comme leur nom l’indique. Par exemple, les deux premiers
stockent des adresses 32 bits (désignant des emplacements mémoire) pour délimiter
le stack frame courant.