Mémoire - utilisation    Enregistrer au format PDF

Étude du fonctionnement de la pile pendant l’exécution d’un programme.


par S3cur3D

Nous allons prendre en exemple le court programme en C suivant, fichier exemple.c (exemple de déclarations et de mise en mémoire) :

  1.     int variable_globale = 1;
  2.     char variable_globale2;
  3.  
  4.     void fonction(int entier1, int entier2, char caractere) {
  5.  
  6.         char variable_interne;
  7.  
  8.         char buffer[10];
  9.  
  10.         //Corps de la fonction
  11.     }
  12.  
  13.     int main() {
  14.  
  15.         int entier;
  16.         entier = 24;
  17.  
  18.         fonction(entier,variable_globale,variable_globale2);
  19.  
  20.         //Corps du programme
  21.  
  22.         return 0;
  23.     }

Télécharger

Dans un programme en C, la fonction principale qui est éxécutée en premier est la fonction main(). Les variables déclarées en dehors du corps de toute fonction sont les variables globales, accessibles par n’importe quelle fonction et n’importe où dans le programme. Par conséquent, après la création des variables globales, la fonction main() s’éxécute. Ensuite, on fait appel à la fonction "fonction". Cette fonction prend en arguments deux entiers et un caractère. Elle possède elle-même deux variables locales créées pendant l’appel de la fonction. Le type void de la fonction signifie qu’elle ne renvoie aucune variable.
Nous allons maintenant traduire ce programme en termes d’occupation de la mémoire :

  1. Le code ici présent est traduit en code machine et implémenté dans le text
  2. On réserve l’espace pour les variables globales. Ici, on a la variable "variable_globale" de type entier qui est initialisée dès le début du programme. Cette variable sera donc ajoutée au data. La variable "variable_globale2" de type caractère n’est pas initialisée, elle ira donc dans le bss.
  3. Les choses sérieuses commencent : la fonction "fonction" est appellée... On commence donc à remplir la pile. Tout d’abord, il faut sauvegarder les variables qui sont passées en argument, à savoir le caractère puis l’entier2 puis l’entier1. On sauvegarde la prochaine adresse de l’EIP et on la push sur la pile en tant que return address. On sauvegarde l’adresse de l’EBP et on l’ajoute à la pile en tant que stack frame pointer. La position des variables du second bloc étant sauvegardée, on peut affecter à l’EBP sa nouvelle valeur. Enfin, les variables locales de la fonction sont ajoutées à la pile dans l’ordre, donc "variable_interne" puis "buffer". Ce moment est schématisé ci-dessous.
  4. Pour finir, on dépile le bloc de la fonction qui a été appellée, EBP peut reprendre sa valeur pré-appel et le pointeur return address est affecté à l’EIP

Pour compléter cette explication, voici deux schémas, l’un représentant la mémoire du programme dans sa totalité et l’autre l’état de la pile après l’étape 3 :

Comme explicité plus tôt, on comprend maintenant pourquoi le heap et le stack grandissent dans des directions opposées : étant tous les deux dynamiques, ils grandissent l’un vers l’autre et minimisent ainsi l’espace perdu et la possibilité de chevauchement des segments (qui aurait été beaucoup plus probable si les deux segments grandissaient dans le même sens). Voilà, vous savez désormais le fonctionnement théorique de la mémoire et comment sont placées les variables dans les différents segments.

Documentations publiées dans cette rubrique