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 de cette façon :
+---------+
| 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 général des processus sur de nombreux systèmes de mémoire virtuelle courants. Le "trou" correspond à la taille de votre mémoire totale, moins l'espace occupé par toutes les autres zones ; cela donne une grande quantité d'espace pour la croissance du tas. Cet espace est également "virtuel", ce qui signifie qu'il correspond à votre réel la mémoire par l'intermédiaire d'une table de traduction, et peut être effectivement stockée à n'importe quel endroit de la mémoire réelle. Cette méthode permet de protéger un processus contre l'accès à la mémoire d'un autre processus, et de faire croire à chaque processus qu'il fonctionne sur un système complet.
Notez que les positions de, par exemple, la pile et le tas peuvent être dans un ordre différent sur certains systèmes (cf. 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érent. 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 constater que le DOS autorise l'accès direct à la mémoire du système d'exploitation, sans aucune protection, ce qui signifie que les programmes de l'espace utilisateur peuvent généralement accéder directement à tout ce qui leur plaît ou l'écraser.
Dans l'espace d'adressage du processus, cependant, les programmes avaient tendance à se ressembler, sauf qu'ils étaient décrits comme segment de code, segment de données, tas, segment de pile, etc. et qu'ils étaient mappés un peu différemment. Mais la plupart des zones générales étaient toujours là.
Après avoir chargé le programme et les librairies partagées nécessaires en mémoire, et distribué les parties du programme dans les zones appropriées, le système d'exploitation commence à exécuter votre processus à l'endroit où se trouve sa méthode principale, et votre programme prend le relais, en effectuant les appels système nécessaires quand il en a besoin.
Différents systèmes (embarqués, etc.) peuvent avoir des architectures très différentes, comme des systèmes sans pile, des systèmes à architecture Harvard (le code et les données étant conservés dans des mémoires physiques distinctes), des systèmes qui conservent réellement le BSS dans une mémoire en lecture seule (initialement définie par le programmeur), etc. Mais c'est là l'essentiel.
Tu as dit :
Je sais également qu'un programme informatique utilise deux types de mémoire : la pile et le tas, qui font également partie de la mémoire primaire de l'ordinateur.
"Pile" et "tas" ne sont que des concepts abstraits, plutôt que des "types" de mémoire physiquement distincts (nécessairement).
A pile est simplement une structure de données de type "dernier entré, premier sorti". Dans l'architecture x86, elle peut en fait être adressée de manière aléatoire en utilisant un décalage à partir de la fin, mais les fonctions les plus courantes sont PUSH et POP pour ajouter et retirer des éléments de cette structure, respectivement. Elle est couramment utilisée pour les variables locales des fonctions (appelées "stockage automatique"), les arguments des fonctions, les adresses de retour, etc.
A "tas" est juste un surnom pour un morceau de mémoire qui peut être alloué à la demande, et qui est adressé de manière aléatoire (ce qui signifie que vous pouvez accéder directement à n'importe quel emplacement dans la mémoire). Il est couramment utilisé pour les structures de données que vous allouez au moment de l'exécution (en C++, à l'aide de la fonction new
y delete
et malloc
et des amis en C, etc).
Sur l'architecture x86, la pile et le tas résident physiquement dans votre mémoire système (RAM) et sont mappés par l'allocation de mémoire virtuelle dans l'espace d'adressage du processus, comme décrit ci-dessus.
El 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 à partir d'autres endroits de la mémoire ou d'autres endroits en fonction des instructions du CPU qui sont effectivement exécutées). Il s'agit essentiellement d'emplacements de mémoire sur puce très petits et très rapides qui sont utilisés à des fins diverses.
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"). Je ne m'étendrai donc pas sur le sujet, mais je vous recommande de suivre un cours de langage d'assemblage pour mieux les comprendre.
Votre question :
A quel moment la pile est-elle utilisée pour l'exécution des instructions ? Les instructions passent de la RAM, à la pile, aux registres ?
La pile (dans les systèmes/langues qui en possèdent 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 assembleur ( gcc -S foo.c
si vous avez accès à GCC), et jetez-y un œil. L'assemblage est assez facile à suivre. Vous pouvez voir que la pile est utilisée pour les variables locales des fonctions, et pour appeler les fonctions, en stockant leurs arguments et leurs valeurs de retour. C'est aussi pourquoi lorsque vous faites quelque chose comme :
f( g( h( i ) ) );
Tous ces éléments sont appelés à tour de rôle. Il s'agit littéralement de construire une pile d'appels de fonctions et de leurs arguments, de les exécuter, puis de les retirer au fur et à mesure qu'elle redescend (ou remonte ;). 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 elle peut donc être manipulée directement ; ce n'est pas une étape séparée pendant l'exécution (ou du moins elle est orthogonale au processus).
Pour info, ce qui précède est le Convention d'appel C également utilisé par C++. D'autres langages/systèmes peuvent pousser les arguments sur la pile dans un ordre différent, et certains langages/plateformes n'utilisent même pas de piles, et s'y prennent de différentes manières.
Notez également qu'il ne s'agit pas de lignes de code C exécutées. Le compilateur les a converties en instructions en langage machine dans votre exécutable. Ils sont ensuite (généralement) copiés de la zone TEXT dans le pipeline du CPU, puis dans les registres du CPU, et exécutés à partir de là. [C'était incorrect. Voir La correction de Ben Voigt ci-dessous.]