mercredi 23 mars 2011

Segmentation de la mémoire d'un programme

Quand on s'essaye à l'exploitation de binaire, il est impératif de connaître parfaitement comment fonctionne un programme et comment sa mémoire est segmentée.
La mémoire d'un programme est divisée en plusieurs segments :
  • text
  • data
  • bss
  • heap (ou tas en français)
  • stack (ou pile en français)
Chaque segment contient des données bien précises.

Le segment text contient les instructions du programme avec toute sa logique. Ce segment est en lecture seule car le code n'est pas modifié lors de l'exécution. Cela permet d'avoir plusieurs processus qui partagent cette même zone. Quand plusieurs utilisateurs exécutent la commande ls en même temps par exemple, le code de la commande contenu dans le segment text n'est pas dupliqué, il est partagé par tous les processus. Cela ne pose aucun problème car cette zone ne peut être modifiée.

Les segments bss et data permettent de stocker les variables globales et statiques du programme. Les variables initialisées sont stockées dans le segment data alors que les variables non initialisées sont situées dans le segment bss. Contrairement au segment text, les segments bss et data ne sont pas en lecture seule (le programme peut modifier ses variables globales et statiques au cours de son exécution), par contre leur taille est fixe puisque connue dès la compilation du programme.

Le segment heap est une zone qui va permettre l'allocation de mémoire de façon dynamique, via les fonctions bien connues de type malloc. Ce segment a donc une taille variable car les zones mémoires vont pouvoir être allouées/désallouées dynamiquement en fonction des besoins du programme.

Le segment stack ou pile a pour objectif de stocker les variables locales des fonctions ainsi que le contexte de ces dernières (arguments...). A chaque fois qu'une fonction est appelée par un programme, une zone mémoire lui est allouée dans la pile on appelle ça un stack frame. Chaque appel de fonction produit un nouveau stack frame propre à cet appel, ce qui permet par exemple d'avoir des contextes complètement différents pour la même fonction et donc des comportements différents. Quand la fonction se termine, le stack frame est détruit. La pile est aussi de taille variable, car il n'est pas possible de savoir quelles fonctions et combien de fois elles seront appelées. Au fur est à mesure des appels de fonction la pile va grandir et diminuer. Contrairement au segment heap, la pile grandit vers les adresses basses : Plus les stack frames se rajoutent et plus leurs adresses sont basses.

Le programme suivant va permettre de mettre en évidence les différentes explications que je viens de donner (il est largement commenté) :
#include <stdio.h>
#include <stdlib.h>
 
// Variables globales non initialisées qui vont dans le segment BSS
int global_a;
static static_a;
 
// Variables globales initialisées qui vont dans le segment DATA
int global_b = 1;
static int static_b = 2;
 
void g(void);
 
void f(void) {
 // Variable locale de f qui va dans le segment STACK
 int local_f_a= 1;
 
 // Variable statique initialisée, donc va dans le segment DATA
 // La valeur est commune lors de tous les appels de fonction
 static int static_f_c = 3;
 
 printf("Addresse Variable local_f_a : %08x\n",&local_f_a);
 printf("Addresse Variable static_f_c : %08x\n",&static_f_c);
 
 g();
 
 return;
}
 
void g(void) {
 // Variable locale de g qui va dans le segment STACK
 int *local_g_a = NULL;
 
 local_g_a = (int*)malloc(sizeof(int));
 
 printf("Addresse Variable local_g_a : %08x\n",&local_g_a);
 printf("Addresse pointee par local_g_a : %08x\n",local_g_a);
 
 free(local_g_a);
 
 return;
}
 
int main(int argc, char **argv) {
 // Variable locale de main qui va dans le segment STACK
 // main est une fonction comme une autre
 int local_main_a = 10;
 
 printf("Addresse Variable local_main_a : %08x\n",&local_main_a);
 printf("Addresse Variable global_a : %08x\n",&global_a);
 printf("Addresse Variable static_a : %08x\n",&static_a);
 printf("Addresse Variable global_b : %08x\n",&global_b);
 printf("Addresse Variable static_b : %08x\n",&static_b);
 
 f();
 g();
 
 return EXIT_SUCCESS;
}

L'exécution du programme donne :

# ./memory_segment
Addresse Variable local_main_a : bff082dc
Addresse Variable global_a : 0804a034
Addresse Variable static_a : 0804a030
Addresse Variable global_b : 0804a01c
Addresse Variable static_b : 0804a020
Addresse Variable local_f_a : bff082ac
Addresse Variable static_f_c : 0804a024
Addresse Variable local_g_a : bff0827c
Addresse pointee par local_g_a : 09e15008
Addresse Variable local_g_a : bff082ac
Addresse pointee par local_g_a : 09e15008
Les variables dont l'adresse est la plus basse sont les variables global_b, static_b et static_f_c. Normal, comme le montre le schéma plus haut, ce sont des variables globales et statiques initialisées, donc leur place est dans le segment data, la zone mémoire la plus basse après le code du programme. La variable static_f_c même si elle se trouve dans une fonction est d'abord une variable statique. Toutes les fonctions f() appelées utilisent la même zone mémoire pour cette variable. Une modification dans un appel, se verra donc dans l'appel suivant.

Ensuite viennent les variables global_a et static_a des variables globales non initialisées, donc dans le segment bss.

La variables locales ont une adresse beaucoup plus grande, commençant par 0xbff. Si on regarde l'ordre d'appel des fonctions, main() est créée avant f(). Du coup comme dit précédemment les variables de main() ont une adresse plus haute car la pile grandit vers les adresses basses. f() appelant g(), les variables de f() ont une adresse plus haute que celles de la fonction g() appelée dans f(). Par contre, lors de l'appel de g() dans main(), on voit que local_g_a a exactement la même adresse qu'avait local_f_a. Cela ne pose aucun problème, la fonction f() est totalement terminée, du coup son stack frame a été détruit, et le stack frame de g() s'est créée au même endroit.

La variable local_g_a est une variable locale à g() donc située dans la pile. Par contre malloc alloue une zone mémoire qui elle pointe dans tas (ou heap). La zone mémoire qui stocke local_g_a est dans la pile, et l'adresse contenue dans cette zone mémoire pointe vers le tas.

Aucun commentaire:

Enregistrer un commentaire