Dans les langages de programmation tels que C et C++, on parle souvent d'allocation de mémoire statique et dynamique. Je comprends le concept mais la phrase "Toute la mémoire a été allouée (réservée) pendant la compilation" m'embrouille toujours.
Si je comprends bien, la compilation convertit le code C/C++ de haut niveau en langage machine et génère un fichier exécutable. Comment la mémoire est-elle "allouée" dans un fichier compilé? La mémoire n’est-elle pas toujours allouée dans le RAM avec tout le matériel de gestion de la mémoire virtuelle?)?
L'allocation de mémoire, par définition, n'est-elle pas un concept d'exécution?
Si je crée une variable de 1 Ko allouée de manière statique dans mon code C/C++, cela augmentera-t-il la taille de l'exécutable du même montant?
C'est l'une des pages où la phrase est utilisée sous le titre "Répartition statique".
Back To Basics: Allocation de mémoire, un historique détaillé
La mémoire allouée au moment de la compilation signifie que le compilateur résout au moment de la compilation où certaines choses seront allouées à l'intérieur de la mappe de mémoire de processus.
Par exemple, considérons un tableau global:
int array[100];
Le compilateur connaît au moment de la compilation la taille du tableau et la taille d'un int
; il connaît donc la taille totale du tableau au moment de la compilation. De plus, une variable globale a une durée de stockage statique par défaut: elle est allouée dans la zone mémoire statique de l'espace mémoire du processus (section .data/.bss). Compte tenu de cette information , le compilateur décide lors de la compilation de quelle adresse de cette zone de mémoire statique se trouve le tableau .
Bien sûr que les adresses de mémoire sont des adresses virtuelles. Le programme suppose qu'il dispose de son propre espace mémoire (de 0x00000000 à 0xFFFFFFFF par exemple). C'est pourquoi le compilateur pourrait faire des hypothèses telles que "D'accord, le tableau sera à l'adresse 0x00A33211". Au moment de l'exécution, ces adresses sont traduites en adresses réelles/matérielles par MMU et le système d'exploitation.
Les éléments de stockage statique initialisés par valeur sont un peu différents. Par exemple:
int array[] = { 1 , 2 , 3 , 4 };
Dans notre premier exemple, le compilateur a uniquement décidé où le tableau serait alloué, en stockant ces informations dans l'exécutable.
Dans le cas d'éléments dont la valeur est initialisée, le compilateur injecte également la valeur initiale du tableau dans l'exécutable, et ajoute un code qui indique au programme de chargement du programme qu'après le processus d'allocation, le tableau doit être rempli. avec ces valeurs.
Voici deux exemples de l'assembly généré par le compilateur (GCC4.8.1 avec cible x86):
Code C++:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
Assemblée de sortie:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
Comme vous pouvez le constater, les valeurs sont directement injectées dans l’Assemblée. Dans le tableau a
, le compilateur génère une initialisation nulle de 16 octets, car la norme indique que les éléments stockés statiques doivent être initialisés à zéro par défaut:
8.5.9 (Initialiseurs) [Note]:
Chaque objet de durée de stockage statique est initialisé à zéro au démarrage du programme avant toute autre initialisation. Dans certains cas, une initialisation supplémentaire est effectuée ultérieurement.
Je suggère toujours aux personnes de désassembler leur code pour voir ce que le compilateur fait vraiment avec le code C++. Ceci s'applique des classes/durée de stockage (comme cette question) aux optimisations avancées du compilateur. Vous pouvez demander à votre compilateur de générer l'assembly, mais il existe d'excellents outils pour le faire de manière conviviale sur Internet. Mon préféré est GCC Explorer .
La mémoire allouée au moment de la compilation signifie simplement qu'il n'y aura plus d'allocation au moment de l'exécution - aucun appel à malloc, à une nouvelle méthode ou à d'autres méthodes d'allocation dynamique. La quantité de mémoire utilisée sera fixe, même si vous n’avez pas besoin de toute cette mémoire tout le temps.
L'allocation de mémoire, par définition, n'est-elle pas un concept d'exécution?
La mémoire n'est pas tilisée avant l'exécution, mais immédiatement avant le début de l'exécution, son allocation est gérée par le système.
Si je crée une variable de 1 Ko allouée de manière statique dans mon code C/C++, cela augmentera-t-il la taille de l'exécutable du même montant?
Déclarer simplement la valeur statique n'augmentera pas la taille de votre exécutable de plus de quelques octets. Le déclarer avec une valeur initiale non nulle sera (afin de conserver cette valeur initiale). Au lieu de cela, l'éditeur de liens ajoute simplement cette quantité de 1 Ko à l'exigence de mémoire créée par le chargeur du système juste avant son exécution.
La mémoire allouée au moment de la compilation signifie que lorsque vous chargez le programme, une partie de la mémoire sera immédiatement allouée et la taille et la position (relative) de cette allocation sont déterminées au moment de la compilation.
char a[32];
char b;
char c;
Ces 3 variables sont "allouées à la compilation", cela signifie que le compilateur calcule leur taille (qui est fixe) à la compilation. La variable a
sera un décalage en mémoire, en indiquant l'adresse 0, b
l'adresse 33 et c
en 34 (sans optimisation de l'alignement). Donc, allouer 1Ko de données statiques n'augmentera pas la taille de votre code, car cela changera simplement un décalage à l'intérieur de celui-ci. L'espace réel sera alloué au moment du chargement.
L'allocation réelle de la mémoire a toujours lieu au moment de l'exécution, car le noyau doit en assurer le suivi et mettre à jour ses structures de données internes (quantité de mémoire allouée pour chaque processus, pages, etc.). La différence est que le compilateur connaît déjà la taille de chaque donnée que vous allez utiliser et celle-ci est allouée dès que votre programme est exécuté.
Rappelez-vous aussi que nous parlons de adresses relatives. La véritable adresse où sera située la variable sera différente. Au moment du chargement, le noyau réservera de la mémoire pour le processus, disons à l'adresse x
, et toutes les adresses codées en dur contenues dans le fichier exécutable seront incrémentées de x
octets, de sorte que la variable a
dans l'exemple sera à l'adresse x
, b à l'adresse x+33
etc.
L'ajout de variables sur la pile prenant N octets n'augmente pas (nécessairement) la taille de la corbeille de N octets. En fait, il n’ajoutera que quelques octets la plupart du temps.
Commençons par un exemple montrant comment l’ajout de 1000 caractères à votre code volonté augmente la taille de la corbeille de manière linéaire.
Si le 1k est une chaîne de mille caractères, déclarée comme telle
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
et vous deviez alors vim your_compiled_bin
, vous seriez en mesure de voir cette chaîne quelque part dans la corbeille. Dans ce cas, oui: l'exécutable sera 1 k plus grand, car il contient la chaîne en entier.
Si, toutefois, vous allouez un tableau de int
s, char
s ou long
s sur la pile et l’affectez dans une boucle, quelque chose le long de ces lignes
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
alors, non: cela n'augmentera pas la corbeille ... de 1000*sizeof(int)
Allocation à la compilation signifie ce que vous comprenez maintenant (en fonction de vos commentaires): la corbeille compilée contient les informations nécessaires au système pour savoir combien de mémoire il faudra à la fonction/au bloc utilisé une fois exécuté. , ainsi que des informations sur la taille de la pile requise par votre application. C'est ce que le système allouera lorsqu'il exécutera votre bin, et votre programme deviendra un processus (eh bien, l'exécution de votre bin est le processus qui ... eh bien, vous obtenez ce que je dis).
Bien sûr, je ne brosse pas un tableau complet ici: la corbeille contient des informations sur la taille de la pile dont elle aura réellement besoin. Sur la base de ces informations (entre autres), le système réservera une partie de la mémoire, appelée pile, sur laquelle le programme jouira d’une sorte de règne libre. La mémoire de pile est toujours allouée par le système lors du lancement du processus (résultat de l'exécution de votre bin). Le processus gère ensuite la mémoire de pile pour vous. Lorsqu'une fonction ou une boucle (tout type de bloc) est appelée/est exécutée, les variables locales à ce bloc sont poussées dans la pile et elles sont supprimées (la mémoire de la pile est "libérée" parler) à utiliser par d’autres fonctions/blocs. Donc, déclarer int some_array[100]
N’ajoutera que quelques octets d’informations supplémentaires à la corbeille, ce qui indique au système que la fonction X nécessitera 100*sizeof(int)
+ de l’espace de comptabilité en plus.
Sur de nombreuses plates-formes, toutes les allocations globales ou statiques au sein de chaque module seront consolidées par le compilateur en trois allocations consolidées ou moins (une pour les données non initialisées (souvent appelée "bss"), une pour les données inscriptibles initialisées (souvent appelée "données" ), et une pour les données constantes ("const")), et l’ensemble des allocations globales ou statiques de chaque type dans un programme sera consolidé par l’éditeur de liens en un seul global pour chaque type. Par exemple, en supposant que int
représente quatre octets, les seules attributions statiques d'un module sont les suivantes:
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
il indiquerait à l'éditeur de liens qu'il lui fallait 208 octets pour bss, 16 octets pour "data" et 28 octets pour "const". En outre, toute référence à une variable serait remplacée par un sélecteur de zone et un décalage, de sorte que a, b, c, d et e seraient remplacés par bss + 0, const + 0, bss + 4, const + 24, données. +0 ou bss + 204, respectivement.
Lorsqu'un programme est lié, tous les domaines bss de tous les modules sont concaténés ensemble; de même les données et les zones const. Pour chaque module, l'adresse de toute variable relative à bss sera augmentée de la taille des zones bss de tous les modules précédents (encore une fois, de même avec data et const). Ainsi, lorsque l’éditeur de liens est terminé, tout programme aura une attribution bss, une attribution de données et une attribution const.
Lorsqu'un programme est chargé, l'une des quatre choses suivantes se produit généralement en fonction de la plate-forme:
L'exécutable indiquera le nombre d'octets nécessaires pour chaque type de données et - pour la zone de données initialisée, où le contenu initial peut être trouvé. Il inclura également une liste de toutes les instructions qui utilisent une adresse relative bss, data ou const. Le système d'exploitation ou le chargeur allouera la quantité d'espace appropriée pour chaque zone, puis ajoutera l'adresse de départ de cette zone à chaque instruction qui en a besoin.
Le système d'exploitation allouera une partie de la mémoire aux trois types de données et donnera à l'application un pointeur sur cette partie de la mémoire. Tout code utilisant des données statiques ou globales le déréférencera par rapport à ce pointeur (dans de nombreux cas, le pointeur sera stocké dans un registre pendant la durée de vie d'une application).
Le système d'exploitation n'allouera initialement aucune mémoire à l'application, à l'exception de ce qui contient son code binaire, mais la première chose que fera l'application sera de demander une allocation appropriée au système d'exploitation, qu'elle conservera pour toujours dans un registre.
Le système d'exploitation n'allouera initialement pas d'espace à l'application, mais l'application demandera une allocation appropriée au démarrage (comme ci-dessus). L'application inclura une liste d'instructions avec les adresses qui doivent être mises à jour pour indiquer où la mémoire a été allouée (comme pour le premier style), mais plutôt que l'application soit corrigée par le chargeur de système d'exploitation, l'application inclura suffisamment de code pour se corriger elle-même. .
Les quatre approches présentent des avantages et des inconvénients. Cependant, dans tous les cas, le compilateur consolidera un nombre arbitraire de variables statiques en un petit nombre fixe de demandes de mémoire et l'éditeur de liens consolidera toutes celles-ci en un petit nombre d'allocations consolidées. Même si une application doit recevoir une partie de la mémoire du système d’exploitation ou du chargeur, c’est le compilateur et l’éditeur de liens qui sont responsables de l’allocation de parties individuelles de cette grande partie à toutes les variables individuelles qui en ont besoin.
Le cœur de votre question est le suivant: "Comment la mémoire est-elle" allouée "dans un fichier compilé? La mémoire n'est-elle pas toujours allouée dans le RAM avec tout le matériel de gestion de la mémoire virtuelle? N'est-ce pas allocation par définition un concept d'exécution? "
Je pense que le problème est qu'il y a deux concepts différents impliqués dans l'allocation de mémoire. A la base, l'allocation de mémoire est le processus par lequel nous disons "cette donnée est stockée dans ce bloc de mémoire spécifique". Dans un système informatique moderne, cela implique un processus en deux étapes:
Ce dernier processus est purement du temps d'exécution, mais le premier peut être effectué au moment de la compilation, si les données ont une taille connue et qu'un nombre fixe d'entre elles est requis. Voici en gros comment ça marche:
Le compilateur voit un fichier source contenant une ligne qui ressemble à ceci:
int c;
Il produit une sortie pour l'assembleur qui lui demande de réserver de la mémoire pour la variable 'c'. Cela pourrait ressembler à ceci:
global _c
section .bss
_c: resb 4
Lorsque l'assembleur s'exécute, il conserve un compteur qui suit les décalages de chaque élément à partir du début d'un "segment" de mémoire (ou "section"). Cela ressemble aux parties d'une très grande 'structure' qui contient tout le contenu du fichier auquel aucune mémoire n'a été allouée pour le moment et qui peut être n'importe où. Il note dans un tableau que _c
a un décalage particulier (par exemple, 510 octets à partir du début du segment), puis incrémente son compteur de 4, de sorte que la variable suivante sera à (par exemple) 514 octets. Pour tout code nécessitant l'adresse de _c
, il suffit de mettre 510 dans le fichier de sortie et d'ajouter une note indiquant que la sortie a besoin de l'adresse du segment qui contient _c
en y ajoutant plus tard.
L'éditeur de liens prend tous les fichiers de sortie de l'assembleur et les examine. Il détermine une adresse pour chaque segment afin d'éviter tout chevauchement et ajoute les décalages nécessaires pour que les instructions fassent toujours référence aux éléments de données corrects. Dans le cas d'une mémoire non initialisée comme celle occupée par c
(l'assembleur a été informé que la mémoire ne serait pas initialisée par le fait que le compilateur la mettait dans le segment '.bss', qui est un nom réservé à non initialisé. mémoire), il inclut dans sa sortie un champ d’en-tête qui indique au système d’exploitation combien doit être réservé. Il peut être déplacé (et l'est généralement) mais il est généralement conçu pour être chargé plus efficacement à une adresse de mémoire particulière et le système d'exploitation essaie de le charger à cette adresse. À ce stade, nous avons une assez bonne idée de l’adresse virtuelle qui sera utilisée par c
.
L'adresse physique ne sera réellement déterminée que lorsque le programme sera en cours d'exécution. Cependant, du point de vue du programmeur, l’adresse physique n’est en réalité pas pertinente. Nous ne découvrirons jamais de quoi il s’agit, car le système d’exploitation ne se soucie généralement pas de le dire à quiconque, il peut changer fréquemment (même pendant l’exécution du programme). L’objectif principal du système d’exploitation est de faire abstraction de cette façon.
Un exécutable décrit l’espace à allouer pour les variables statiques. Cette allocation est effectuée par le système lorsque vous exécutez le fichier exécutable. Ainsi, votre variable statique de 1 Ko n'augmentera pas la taille de l'exécutable avec 1 Ko:
static char[1024];
À moins bien sûr de spécifier un initialiseur:
static char[1024] = { 1, 2, 3, 4, ... };
Ainsi, outre le "langage machine" (c'est-à-dire les instructions de la CPU), un exécutable contient une description de la structure de mémoire requise.
La mémoire peut être allouée de plusieurs manières:
Maintenant, votre question est: "Mémoire allouée au moment de la compilation". Il s’agit en réalité d’un énoncé incorrect, qui est supposé faire référence à une allocation de segment binaire ou à une allocation de pile, voire, dans certains cas, à une allocation de tas, mais dans ce cas, l’allocation est masquée par un appel invisible du constructeur. Ou probablement la personne qui a dit cela voulait juste dire que la mémoire n'est pas allouée sur le tas, mais ne connaissait pas l'allocation de pile ou de segment (ou ne voulait pas entrer dans ce genre de détail).
Mais dans la plupart des cas, la personne veut simplement dire que la quantité de mémoire allouée est connue au moment de la compilation.
La taille binaire ne changera que lorsque la mémoire est réservée dans le code ou le segment de données de votre application.
Tu as raison. La mémoire est effectivement allouée (paginée) au moment du chargement, c'est-à-dire lorsque le fichier exécutable est introduit dans la mémoire (virtuelle). La mémoire peut également être initialisée à ce moment. Le compilateur crée simplement une carte mémoire. [Au fait, les espaces de pile et de tas sont également alloués au moment du chargement!]
J'aimerais expliquer ces concepts à l'aide de quelques diagrammes.
C’est vrai que la mémoire ne peut pas être allouée au moment de la compilation. Mais alors que se passe-t-il en fait au moment de la compilation.
Voici l'explication. Par exemple, un programme a quatre variables x, y, z et k. Au moment de la compilation, il crée simplement une carte mémoire dans laquelle l'emplacement de ces variables les unes par rapport aux autres est déterminé. Ce diagramme l'illustrera mieux.
Maintenant, imaginez qu'aucun programme ne fonctionne en mémoire. Je montre cela par un grand rectangle vide.
Ensuite, la première instance de ce programme est exécutée. Vous pouvez le visualiser comme suit. C'est le moment où la mémoire est allouée.
Lorsque la deuxième instance de ce programme est en cours d'exécution, la mémoire ressemblerait à ce qui suit.
Et le troisième ..
Etc., etc.
J'espère que cette visualisation explique bien ce concept.
Si vous apprenez la programmation d'assemblage, vous verrez qu'il vous faut découper des segments pour les données, la pile, le code, etc. Le segment de données est l'endroit où résident vos chaînes et vos nombres. Le segment de code est l'endroit où réside votre code. Ces segments sont intégrés au programme exécutable. Bien sûr, la taille de la pile est également importante ... vous ne voudriez pas d'un débordement de pile!
Donc, si votre segment de données est de 500 octets, votre programme a une zone de 500 octets. Si vous modifiez le segment de données à 1500 octets, la taille du programme sera plus grande de 1000 octets. Les données sont assemblées dans le programme actuel.
C'est ce qui se passe lorsque vous compilez des langages de niveau supérieur. La zone de données réelle est allouée lorsqu’elle est compilée dans un programme exécutable, ce qui augmente la taille du programme. Le programme peut également demander de la mémoire à la volée. Il s’agit d’une mémoire dynamique. Vous pouvez demander de la mémoire à RAM et le processeur vous le donnera, vous pourrez vous en séparer et votre ramasse-miettes le remettra dans le processeur. Cela peut même être échangé sur un disque dur, si nécessaire, par un bon gestionnaire de mémoire, telles que les langages de haut niveau vous fournissent.
Je pense que vous devez prendre un peu de recul. Mémoire allouée à la compilation ... Qu'est-ce que cela peut signifier? Cela peut-il signifier que la mémoire sur des puces qui n'ont pas encore été fabriquées, pour des ordinateurs qui n'ont pas encore été conçus, est en quelque sorte réservée? Non, pas de compilateurs capables de manipuler l'univers.
Cela signifie donc que le compilateur génère des instructions pour allouer cette mémoire d’une manière ou d’une autre au moment de l’exécution. Mais si vous le regardez sous le bon angle, le compilateur génère toutes les instructions, alors quelle peut être la différence? La différence est que le compilateur décide, et au moment de l'exécution, votre code ne peut pas changer ou modifier ses décisions. S'il a décidé qu'il fallait 50 octets au moment de la compilation, au moment de l'exécution, vous ne pouvez pas le décider à en allouer 60 - cette décision a déjà été prise.