Dans les langages de programmation comme C et C ++, les gens se réfèrent souvent à l'allocation de mémoire statique et dynamique. Je comprends le concept mais l'expression "Toute la mémoire a été allouée (réservée) pendant la compilation" me confond toujours.
La compilation, si je comprends bien, 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 la RAM avec tous les éléments de gestion de la mémoire virtuelle?
L'allocation de mémoire n'est-elle pas par définition un concept d'exécution?
Si je crée une variable allouée statiquement de 1 Ko 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 la rubrique "allocation statique".
Retour aux sources: allocation de mémoire, une promenade dans l'histoire
la source
Réponses:
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 carte mémoire du processus.
Par exemple, considérons un tableau global:
Le compilateur connaît au moment de la compilation la taille du tableau et la taille de an
int
, donc il connaît la taille entière 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 ces informations, le compilateur décide lors de la compilation à quelle adresse de cette zone de mémoire statique le tableau sera .Bien sûr, les adresses mémoire sont des adresses virtuelles. Le programme suppose qu'il dispose de son propre espace mémoire entier (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 la MMU et le système d'exploitation.
Les éléments de stockage statique à valeur initialisée sont un peu différents. Par exemple:
Dans notre premier exemple, le compilateur a uniquement décidé où le tableau sera alloué, en stockant ces informations dans l'exécutable.
Dans le cas de choses initialisées par valeur, le compilateur injecte également la valeur initiale du tableau dans l'exécutable et ajoute du code qui indique au chargeur de programme qu'après l'allocation du tableau au démarrage du programme, le tableau doit être rempli avec ces valeurs.
Voici deux exemples d'assembly généré par le compilateur (GCC4.8.1 avec cible x86):
Code C ++:
Assemblage de sortie:
Comme vous pouvez le voir, les valeurs sont directement injectées dans l'assemblage. Dans le tableau
a
, le compilateur génère une initialisation à zéro de 16 octets, car la norme dit que les éléments stockés statiques doivent être initialisés à zéro par défaut:Je suggère toujours aux gens de désassembler leur code pour voir ce que le compilateur fait vraiment avec le code C ++. Cela 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 de merveilleux outils pour le faire sur Internet de manière conviviale. Mon préféré est GCC Explorer .
la source
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, nouvelle ou autre méthode d'allocation dynamique. Vous aurez une quantité fixe d'utilisation de la mémoire même si vous n'avez pas besoin de toute cette mémoire tout le temps.
La mémoire n'est pas utilisée avant l'exécution, mais juste avant l'exécution, son allocation est gérée par le système.
Le simple fait de déclarer la valeur statique n'augmentera pas la taille de votre exécutable de plus de quelques octets. Le déclarer avec une valeur initiale différente de zéro sera (pour conserver cette valeur initiale). Au contraire, l'éditeur de liens ajoute simplement cette quantité de 1 Ko à l'exigence de mémoire que le chargeur du système crée pour vous immédiatement avant l'exécution.
la source
static int i[4] = {2 , 3 , 5 ,5 }
, il augmentera de la taille de l'exécutable de 16 octets. Vous avez dit "Le simple fait de déclarer 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" Le déclarer avec une valeur initiale sera ce que cela signifie.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.
Ces 3 variables sont "allouées au moment de la compilation", cela signifie que le compilateur calcule leur taille (qui est fixe) au moment de la compilation. La variable
a
sera un offset en mémoire, disons, pointant vers l'adresse 0,b
pointera vers l'adresse 33 etc
vers 34 (en supposant aucune optimisation d'alignement). Ainsi, allouer 1 Ko de données statiques n'augmentera pas la taille de votre code , car cela changera simplement un décalage à l'intérieur. L'espace réel sera alloué au moment du chargement .L'allocation de mémoire réelle se produit toujours au moment de l'exécution, car le noyau doit en garder une trace et mettre à jour ses structures de données internes (la quantité de mémoire allouée à 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é.
Souvenez-vous également que nous parlons d' adresses relatives . L'adresse réelle où se trouvera 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 d'x
octets, de sorte que la variablea
dans l'exemple sera à l'adressex
, b à l'adressex+33
et bientôt.la source
L'ajout de variables sur la pile qui occupent N octets n'augmente pas (nécessairement) la taille du bac de N octets. En fait, il n'ajoutera que quelques octets la plupart du temps.
Commençons par un exemple de la façon dont l' ajout d' un 1000 caractères à votre code va augmenter la taille du bac de manière linéaire.
Si le 1k est une chaîne, de mille caractères, qui est déclarée comme ceci
et vous deviez alors
vim your_compiled_bin
, vous pourriez réellement voir cette chaîne dans la corbeille quelque part. Dans ce cas, oui: l'exécutable sera 1 k plus grand, car il contient la chaîne en entier.Si, cependant, vous allouez un tableau de
int
s,char
s oulong
s sur la pile et l'assignez dans une boucle, quelque chose du genrealors non: cela n'augmentera pas le bac ... par
1000*sizeof(int)
Allocation au moment de la compilation signifie ce que vous avez maintenant compris que cela signifie (basé sur vos commentaires): le bac compilé contient des informations dont le système a besoin pour savoir combien de mémoire de quelle fonction / bloc aura besoin lors de son exécution, ainsi que des informations sur la taille de pile dont votre application a besoin. C'est ce que le système allouera lorsqu'il exécutera votre bac, et votre programme deviendra un processus (enfin, l'exécution de votre bac est le processus qui ... eh bien, vous comprenez ce que je dis).
Bien sûr, je ne brosse pas l'image complète ici: le bac contient des informations sur la taille d'une pile dont le bac 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 obtiendra une sorte de règne libre. La mémoire de pile est toujours allouée par le système lorsque le processus (le résultat de l'exécution de votre bac) est lancé. Le processus gère ensuite la mémoire de la pile pour vous. Lorsqu'une fonction ou une boucle (tout type de bloc) est invoquée / est exécutée, les variables locales à ce bloc sont poussées vers la pile, et elles sont supprimées (la mémoire de la pile est "libérée" pour ainsi dire) pour être utilisées par d'autres fonctions / blocs. Alors déclarant
int some_array[100]
ajoutera seulement quelques octets d'informations supplémentaires à la corbeille, ce qui indique au système que la fonction X nécessitera100*sizeof(int)
+ un espace de comptabilité supplémentaire.la source
i
n'est pas "libéré" ou non plus. S'ili
devait résider dans la mémoire, il serait simplement poussé dans la pile, quelque chose qui n'est pas libéré dans ce sens du mot, sans tenir compte de celai
ouc
qui sera conservé dans les registres tout le temps. Bien sûr, tout dépend du compilateur, ce qui signifie que ce n'est pas si noir et blanc.free()
appels, mais la mémoire de pile qu'ils ont utilisée est libre pour une utilisation par d'autres fonctions une fois que la fonction que j'ai énumérée est retournée. J'ai supprimé le code, car il peut être déroutant pour certainsSur de nombreuses plates-formes, toutes les allocations globales ou statiques 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ées "bss"), une pour les données inscriptibles initialisées (souvent appelées "données") ), et une pour les données constantes ("const")), et toutes les allocations globales ou statiques de chaque type dans un programme seront consolidées par l'éditeur de liens en un global pour chaque type. Par exemple, en supposant qu'il
int
s'agit de quatre octets, un module a les éléments suivants comme seules allocations statiques:il indiquerait à l'éditeur de liens qu'il avait besoin de 208 octets pour bss, 16 octets pour "data" et 28 octets pour "const". De plus, toute référence à une variable serait remplacée par un sélecteur de zone et un décalage, donc a, b, c, d et e, seraient remplacés par bss + 0, const + 0, bss + 4, const + 24, data +0, ou bss + 204, respectivement.
Lorsqu'un programme est lié, toutes les zones bss de tous les modules sont concaténées ensemble; de même les zones de données et const. Pour chaque module, l'adresse de toutes les variables relatives au 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 allocation bss, une allocation de données et une allocation const.
Lorsqu'un programme est chargé, l'une des quatre choses se produira généralement en fonction de la plate-forme:
L'exécutable indiquera le nombre d'octets dont il a besoin pour chaque type de données et - pour la zone de données initialisée, où se trouve le contenu initial. Il comprendra é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 un morceau de mémoire pour contenir les trois types de données et donnera à l'application un pointeur vers ce bloc de mémoire. Tout code qui utilise 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 pour la durée de vie d'une application).
Le système d'exploitation n'allouera initialement aucune mémoire à l'application, sauf pour 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 pour l'application, mais l'application demandera une allocation appropriée au démarrage (comme ci-dessus). L'application inclura une liste d'instructions avec des adresses qui doivent être mises à jour pour refléter où la mémoire a été allouée (comme avec le premier style), mais plutôt que d'avoir l'application corrigée par le chargeur du système d'exploitation, l'application inclura suffisamment de code pour se patcher. .
Les quatre approches présentent des avantages et des inconvénients. Dans tous les cas, cependant, 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 devra recevoir un morceau de mémoire du système d'exploitation ou du chargeur, ce sont le compilateur et l'éditeur de liens qui sont responsables d'allouer des éléments individuels de ce gros morceau à toutes les variables individuelles qui en ont besoin.
la source
Le cœur de votre question est la suivante: "Comment la mémoire est-elle" allouée "dans un fichier compilé? La mémoire n'est-elle pas toujours allouée dans la RAM avec tout le matériel de gestion de la mémoire virtuelle? L'allocation de mémoire n'est-elle pas 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. À sa base, l'allocation de mémoire est le processus par lequel nous disons "cet élément de données est stocké dans ce bloc de mémoire spécifique". Dans un système informatique moderne, cela implique un processus en deux étapes:
Le dernier processus est purement 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 comment cela fonctionne:
Le compilateur voit un fichier source contenant une ligne qui ressemble un peu à ceci:
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:
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»). C'est comme les parties d'un très grand 'struct' qui contient tout dans le fichier entier, il n'a pas de mémoire réelle allouée à ce moment, et pourrait être n'importe où. Il note dans une table qui
_c
a un décalage particulier (disons 510 octets à partir du début du segment) puis incrémente son compteur de 4, donc la prochaine variable sera à (par exemple) 514 octets. Pour tout code nécessitant l'adresse de_c
, il met simplement 510 dans le fichier de sortie et ajoute une note indiquant que la sortie a besoin de l'adresse du segment qui contient l'_c
ajout ultérieur.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 qu'ils ne se chevauchent pas et ajoute les décalages nécessaires pour que les instructions se réfèrent toujours aux éléments de données corrects. Dans le cas d'une mémoire non initialisée comme celle occupée par
c
(on a dit à l'assembleur que la mémoire ne serait pas initialisée par le fait que le compilateur la place dans le segment '.bss', qui est un nom réservé à la mémoire non initialisée), il inclut un champ d'en-tête dans sa sortie 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 mémoire particulière, et le système d'exploitation essaiera de le charger à cette adresse. À ce stade, nous avons une assez bonne idée de l'adresse virtuelle qui sera utiliséec
.L'adresse physique ne sera pas réellement déterminée tant que le programme ne sera pas en cours d'exécution. Cependant, du point de vue du programmeur, l'adresse physique est en fait sans importance - nous ne saurons même jamais ce que c'est, car le système d'exploitation ne prend généralement pas la peine de le dire à personne, il peut changer fréquemment (même lorsque le programme est en cours d'exécution), et un L'objectif principal du système d'exploitation est de supprimer cela de toute façon.
la source
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 l'exécutable. Ainsi, votre variable statique de 1 Ko n'augmentera pas la taille de l'exécutable de 1 Ko:
Sauf bien sûr que vous spécifiez un initialiseur:
Ainsi, en plus du «langage machine» (c'est-à-dire des instructions CPU), un exécutable contient une description de la configuration de la mémoire requise.
la source
La mémoire peut être allouée de plusieurs manières:
Maintenant votre question est de savoir quelle est la "mémoire allouée au moment de la compilation". Il s'agit certainement d'un dicton mal formulé, censé faire référence à l'allocation de segment binaire ou à l'allocation de pile, ou dans certains cas même à une allocation de tas, mais dans ce cas, l'allocation est cachée aux yeux du programmeur par un appel de constructeur invisible. Ou probablement la personne qui a dit cela voulait juste dire que la mémoire n'est pas allouée sur le tas, mais qu'elle ne connaissait pas les allocations de pile ou de segment (ou ne voulait pas entrer dans ce genre de détails).
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.
la source
.data
et.bss
.Vous avez 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. [À propos, les espaces de pile et de tas sont également alloués au moment du chargement!]
la source
Je pense que vous devez prendre un peu de recul. Mémoire allouée au moment de la compilation ... Qu'est-ce que cela signifie? 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, voyage dans le temps, pas de compilateurs capables de manipuler l'univers.
Donc, cela doit signifier que le compilateur génère des instructions pour allouer cette mémoire d'une manière ou d'une autre à 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 avait besoin de 50 octets au moment de la compilation, au moment de l'exécution, vous ne pouvez pas le décider d'allouer 60 octets - cette décision a déjà été prise.
la source
Si vous apprenez la programmation d'assemblage, vous verrez que vous devez tailler des segments pour les données, la pile et le code, etc. Le segment de données est l'endroit où vivent vos chaînes et vos nombres. Le segment de code est l'endroit où vit 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 de 1000 octets plus grande. Les données sont assemblées dans le programme réel.
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, et il s'agit de mémoire dynamique. Vous pouvez demander de la mémoire à la RAM et le CPU vous la donnera à utiliser, vous pouvez la laisser aller, et votre garbage collector la restituera au CPU. Il peut même être échangé sur un disque dur, si nécessaire, par un bon gestionnaire de mémoire. Ces fonctionnalités sont ce que vous offrent les langages de haut niveau.
la source
Je voudrais expliquer ces concepts à l'aide de quelques schémas.
C'est vrai que la mémoire ne peut pas être allouée au moment de la compilation, bien sûr. Mais, alors ce qui se passe en fait au moment de la compilation.
Voici l'explication. Disons, par exemple, qu'un programme a quatre variables x, y, z et k. Maintenant, au moment de la compilation, il fait simplement une carte mémoire, où l'emplacement de ces variables les unes par rapport aux autres est déterminé. Ce diagramme l'illustrera mieux.
Imaginez maintenant qu'aucun programme ne fonctionne en mémoire. Je le montre 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 l'heure à laquelle la mémoire est allouée.
Lorsque la deuxième instance de ce programme est en cours d'exécution, la mémoire ressemblerait à ceci.
Et le troisième ...
Etc., etc.
J'espère que cette visualisation explique bien ce concept.
la source
Il y a une très belle explication donnée dans la réponse acceptée. Juste au cas où je publierai le lien que j'ai trouvé utile. https://www.tenouk.com/ModuleW.html
la source