Cela dépend vraiment du système, mais les systèmes d'exploitation modernes avec mémoire virtuelle ont tendance à charger leurs images de processus et à allouer de la mémoire quelque chose comme ceci:
+---------+
| stack | function-local variables, return addresses, return values, etc.
| | often grows downward, commonly accessed via "push" and "pop" (but can be
| | accessed randomly, as well; disassemble a program to see)
+---------+
| shared | mapped shared libraries (C libraries, math libs, etc.)
| libs |
+---------+
| hole | unused memory allocated between the heap and stack "chunks", spans the
| | difference between your max and min memory, minus the other totals
+---------+
| heap | dynamic, random-access storage, allocated with 'malloc' and the like.
+---------+
| bss | Uninitialized global variables; must be in read-write memory area
+---------+
| data | data segment, for globals and static variables that are initialized
| | (can further be split up into read-only and read-write areas, with
| | read-only areas being stored elsewhere in ROM on some systems)
+---------+
| text | program code, this is the actual executable code that is running.
+---------+
Il s'agit de l'espace d'adressage de processus général sur de nombreux systèmes de mémoire virtuelle courants. Le "trou" est la taille de votre mémoire totale, moins l'espace occupé par toutes les autres zones; cela donne une grande quantité d'espace pour que le tas se développe. Ceci est également «virtuel», ce qui signifie qu'il correspond à votre mémoire réelle via une table de traduction, et peut être stocké à n'importe quel endroit de la mémoire réelle. Ceci est fait de cette façon pour protéger un processus d'accéder à la mémoire d'un autre processus et pour faire croire à chaque processus qu'il s'exécute sur un système complet.
Notez que les positions, par exemple, de la pile et du tas peuvent être dans un ordre différent sur certains systèmes (voir la réponse de Billy O'Neal ci-dessous pour plus de détails sur Win32).
D'autres systèmes peuvent être très différents. DOS, par exemple, fonctionnait en mode réel , et son allocation de mémoire lors de l'exécution de programmes était très différente:
+-----------+ top of memory
| extended | above the high memory area, and up to your total memory; needed drivers to
| | be able to access it.
+-----------+ 0x110000
| high | just over 1MB->1MB+64KB, used by 286s and above.
+-----------+ 0x100000
| upper | upper memory area, from 640kb->1MB, had mapped memory for video devices, the
| | DOS "transient" area, etc. some was often free, and could be used for drivers
+-----------+ 0xA0000
| USER PROC | user process address space, from the end of DOS up to 640KB
+-----------+
|command.com| DOS command interpreter
+-----------+
| DOS | DOS permanent area, kept as small as possible, provided routines for display,
| kernel | *basic* hardware access, etc.
+-----------+ 0x600
| BIOS data | BIOS data area, contained simple hardware descriptions, etc.
+-----------+ 0x400
| interrupt | the interrupt vector table, starting from 0 and going to 1k, contained
| vector | the addresses of routines called when interrupts occurred. e.g.
| table | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that
| | location to service the interrupt.
+-----------+ 0x0
Vous pouvez voir que DOS permettait un accès direct à la mémoire du système d'exploitation, sans protection, ce qui signifiait que les programmes de l'espace utilisateur pouvaient généralement accéder directement ou écraser tout ce qu'ils voulaient.
Dans l'espace d'adressage de processus, cependant, les programmes avaient tendance à se ressembler, seulement ils étaient décrits comme un segment de code, un segment de données, un tas, un segment de pile, etc., et ils étaient mappés un peu différemment. Mais la plupart des zones générales étaient toujours là.
Lors du chargement du programme et des bibliothèques partagées nécessaires en mémoire, et de la distribution des parties du programme dans les bonnes zones, le système d'exploitation commence à exécuter votre processus où que se trouve sa méthode principale, et votre programme prend le relais à partir de là, faisant des appels système si nécessaire lorsque il en a besoin.
Différents systèmes (embarqués, peu importe) peuvent avoir des architectures très différentes, telles que des systèmes sans pile, des systèmes d'architecture de Harvard (avec le code et les données conservés dans une mémoire physique séparée), des systèmes qui conservent en fait le BSS en mémoire morte (initialement définie par le programmeur), etc. Mais c'est l'essentiel.
Tu as dit:
Je sais aussi qu'un programme informatique utilise deux types de mémoire: pile et tas, qui font également partie de la mémoire principale de l'ordinateur.
«Pile» et «tas» ne sont que des concepts abstraits, plutôt que des «types» de mémoire (nécessairement) physiquement distincts.
Une pile est simplement une structure de données dernier entré, premier sorti. Dans l'architecture x86, il peut en fait être adressé de manière aléatoire en utilisant un décalage par rapport à la fin, mais les fonctions les plus courantes sont PUSH et POP pour y ajouter et supprimer des éléments, respectivement. Il est couramment utilisé pour les variables locales de fonction (soi-disant "stockage automatique"), les arguments de fonction, les adresses de retour, etc. (plus ci-dessous)
Un "tas" est juste un surnom pour un morceau de mémoire qui peut être alloué à la demande, et est adressé de manière aléatoire (ce qui signifie que vous pouvez accéder directement à n'importe quel emplacement). Il est couramment utilisé pour les structures de données que vous allouez au moment de l'exécution (en C ++, en utilisant new
and delete
, et malloc
et des amis en C, etc.).
La pile et le tas, sur l'architecture x86, résident physiquement dans votre mémoire système (RAM) et sont mappés via l'allocation de mémoire virtuelle dans l'espace d'adressage de processus comme décrit ci-dessus.
Les registres (toujours sur x86), résident physiquement à l'intérieur du processeur (par opposition à la RAM), et sont chargés par le processeur, à partir de la zone TEXT (et peuvent également être chargés ailleurs dans la mémoire ou à d'autres endroits en fonction des instructions du processeur qui sont effectivement exécutés). Ce sont essentiellement des emplacements de mémoire sur puce très petits et très rapides qui sont utilisés à diverses fins.
La disposition des registres dépend fortement de l'architecture (en fait, les registres, le jeu d'instructions et la disposition / conception de la mémoire sont exactement ce que l'on entend par «architecture»), et je ne vais donc pas m'étendre là-dessus, mais je vous recommande de prendre un cours de langage d'assemblage pour mieux les comprendre.
Ta question:
À quel moment la pile est-elle utilisée pour l'exécution des instructions? Les instructions vont de la RAM, à la pile, aux registres?
La pile (dans les systèmes / langages qui les ont et les utilisent) est le plus souvent utilisée comme ceci:
int mul( int x, int y ) {
return x * y; // this stores the result of MULtiplying the two variables
// from the stack into the return value address previously
// allocated, then issues a RET, which resets the stack frame
// based on the arg list, and returns to the address set by
// the CALLer.
}
int main() {
int x = 2, y = 3; // these variables are stored on the stack
mul( x, y ); // this pushes y onto the stack, then x, then a return address,
// allocates space on the stack for a return value,
// then issues an assembly CALL instruction.
}
Écrivez un programme simple comme celui-ci, puis compilez-le en assembly ( gcc -S foo.c
si vous avez accès à GCC), et jetez un œil. L'assemblage est assez facile à suivre. Vous pouvez voir que la pile est utilisée pour les variables locales de fonction et pour appeler des fonctions, stocker leurs arguments et leurs valeurs de retour. C'est aussi pourquoi lorsque vous faites quelque chose comme:
f( g( h( i ) ) );
Tous sont appelés à tour de rôle. Il s'agit littéralement de construire une pile d'appels de fonction et de leurs arguments, de les exécuter, puis de les faire sauter au fur et à mesure qu'il redescend (ou monte;). Cependant, comme mentionné ci-dessus, la pile (sur x86) réside en fait dans l'espace mémoire de votre processus (dans la mémoire virtuelle), et peut donc être manipulée directement; ce n'est pas une étape distincte lors de l'exécution (ou du moins est orthogonale au processus).
Pour info, ce qui précède est la convention d'appel C , également utilisée par C ++. D'autres langages / systèmes peuvent pousser des arguments sur la pile dans un ordre différent, et certains langages / plates-formes n'utilisent même pas de piles et procèdent de différentes manières.
Notez également qu'il ne s'agit pas de lignes réelles d'exécution de code C. Le compilateur les a convertis en instructions en langage machine dans votre exécutable. Ils sont ensuite (généralement) copiés de la zone TEXT dans le pipeline CPU, puis dans les registres CPU, et exécutés à partir de là. [C'était incorrect. Voir la correction de Ben Voigt ci-dessous.]