Je connais la théorie générale mais je ne peux pas rentrer dans les détails.
Je sais qu'un programme réside dans la mémoire secondaire d'un ordinateur. Une fois que le programme a commencé son exécution, il est entièrement copié dans la RAM. Ensuite, le processeur récupère quelques instructions (cela dépend de la taille du bus) à la fois, les met dans des registres et les exécute.
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 principale de l'ordinateur. La pile est utilisée pour la mémoire non dynamique et le tas pour la mémoire dynamique (par exemple, tout ce qui concerne l'opérateur new
en C++)
Ce que je ne peux pas comprendre, c'est comment ces deux choses se connectent. À quel moment la pile est-elle utilisée pour l'exécution des instructions? Les instructions vont de la RAM, à la pile, aux registres?
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 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 est mappé à votre mémoire réelle via une table de traduction, et peut être réellement stocké à n'importe quel emplacement dans la mémoire réelle. Cela est fait de cette façon pour protéger un processus contre l'accès à 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 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, s'exécutait 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 du processus, cependant, les programmes avaient tendance à ressembler, seulement ils étaient décrits comme segment de code, segment de données, segment de mémoire, segment de pile, etc., et il était mappé un peu différemment. Mais la plupart des zones générales étaient toujours là.
Après avoir chargé le programme et les bibliothèques partagées nécessaires dans la mémoire et distribué les 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à, effectuant 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, tels que les systèmes sans pile, les systèmes d'architecture Harvard (avec le code et les données étant conservés dans une mémoire physique séparée), des systèmes qui gardent réellement le BSS en mémoire morte (initialement définie par le programmeur), etc. Mais c'est 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 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.
Un pile est simplement une structure de données de dernier entré, premier sorti. Dans l'architecture x86, il peut en fait être traité 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 supprimer des éléments, respectivement. Il est couramment utilisé pour les variables fonctionnelles locales (appelées "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 lors de l'exécution (en C++, en utilisant new
et delete
, et malloc
et des amis en C, etc.).
La pile et le tas, sur l'architecture x86, résident physiquement dans la mémoire système (RAM) et sont mappés via l'allocation de mémoire virtuelle dans l'espace d'adressage du 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 en mémoire ou autre en fonction des instructions CPU réellement exécutées). Ce sont essentiellement des emplacements de mémoire sur puce très petits et très rapides qui sont utilisés à différentes 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 donc je ne développerai pas dessus, mais je vous recommande de prendre un Cours de langue 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 dans Assembly (gcc -S foo.c
si vous avez accès à GCC), et jetez un œil. L'Assemblée est assez facile à suivre. Vous pouvez voir que la pile est utilisée pour les variables locales de fonction et pour appeler des 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 à tour. Il s'agit littéralement de constituer une pile d'appels de fonctions et de leurs arguments, de les exécuter, puis de les faire disparaître au fur et à mesure de la descente (ou de la montée;). Cependant, comme mentionné ci-dessus, la pile (sur x86) réside réellement dans votre espace de mémoire de processus (dans la mémoire virtuelle), et peut donc être manipulée directement; ce n'est pas une étape distincte pendant 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 certaines langues/plates-formes 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 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 correction de Ben Voigt ci-dessous.]
Sdaz a obtenu un nombre remarquable de votes positifs en très peu de temps, mais malheureusement, perpétue une idée fausse sur la façon dont les instructions se déplacent dans le processeur.
La question posée:
Les instructions vont de la RAM, à la pile, aux registres?
Sdaz a déclaré:
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à.
Mais c'est faux. À l'exception du cas spécial du code auto-modifiable, les instructions n'entrent jamais dans le chemin de données. Et ils ne sont pas, ne peuvent pas être exécutés à partir du chemin de données.
Les registres CPU x86 sont:
Registres généraux EAX EBX ECX EDX
Registres de segment CS DS ES FS GS SS
Index et pointeurs ESI EDI EBP EIP ESP
Indicateur EFLAGS
Il existe également des registres à virgule flottante et SIMD, mais pour les besoins de cette discussion, nous les classerons comme faisant partie du coprocesseur et non du CPU. L'unité de gestion de la mémoire à l'intérieur du CPU possède également certains registres, nous traiterons à nouveau cela comme une unité de traitement distincte.
Aucun de ces registres n'est utilisé pour le code exécutable. EIP
contient l'adresse de l'instruction d'exécution, pas l'instruction elle-même.
Les instructions passent par un chemin complètement différent dans le CPU à partir des données (architecture Harvard). Toutes les machines actuelles sont de l'architecture Harvard à l'intérieur du CPU. La plupart de ces jours sont également l'architecture Harvard dans le cache. x86 (votre machine de bureau commune) est une architecture Von Neumann dans la mémoire principale, ce qui signifie que les données et le code sont entremêlés dans la RAM. C'est à côté du point, car nous parlons de ce qui se passe à l'intérieur du CPU.
La séquence classique enseignée en architecture informatique est fetch-decode-execute. Le contrôleur de mémoire recherche l'instruction stockée à l'adresse EIP
. Les bits de l'instruction passent par une logique combinatoire pour créer tous les signaux de commande pour les différents multiplexeurs du processeur. Et après quelques cycles, l'unité arithmétique et logique arrive à un résultat qui est cadencé dans la destination. Ensuite, l'instruction suivante est récupérée.
Sur un processeur moderne, les choses fonctionnent un peu différemment. Chaque instruction entrante est traduite en toute une série d'instructions de microcode. Cela permet le pipelining, car les ressources utilisées par la première microinstruction ne sont pas nécessaires plus tard, afin qu'elles puissent commencer à travailler sur la première microinstruction à partir de l'instruction suivante.
Pour couronner le tout, la terminologie est légèrement confuse parce que registre est un terme d'ingénierie électrique pour une collection de D-flipflops. Et les instructions (ou en particulier les micro-instructions) peuvent très bien être stockées temporairement dans une telle collection de tongs en D. Mais ce n'est pas ce que l'on entend lorsqu'un informaticien, un ingénieur logiciel ou un développeur ordinaire utilise le terme registre . Ils signifient les registres de chemin de données répertoriés ci-dessus, et ils ne sont pas utilisés pour transporter du code.
Les noms et le nombre de registres de chemin de données varient pour d'autres architectures de processeur, telles que ARM, MIPS, Alpha, PowerPC, mais tous exécutent des instructions sans les faire passer par l'ALU.
La disposition exacte de la mémoire pendant l'exécution d'un processus dépend entièrement de la plate-forme que vous utilisez. Considérez le programme de test suivant:
#include <stdlib.h>
#include <stdio.h>
int main()
{
int stackValue = 0;
int *addressOnStack = &stackValue;
int *addressOnHeap = malloc(sizeof(int));
if (addressOnStack > addressOnHeap)
{
puts("The stack is above the heap.");
}
else
{
puts("The heap is above the stack.");
}
}
Sous Windows NT (et ses enfants), ce programme va généralement produire:
Le tas est au-dessus de la pile
Sur les box POSIX, ça va dire:
La pile est au-dessus du tas
Le modèle de mémoire UNIX est assez bien expliqué ici par @Sdaz MacSkibbons, donc je ne le répéterai pas ici. Mais ce n'est pas le seul modèle de mémoire. La raison pour laquelle POSIX requiert ce modèle est l'appel système sbrk . Fondamentalement, sur une boîte POSIX, pour obtenir plus de mémoire, un processus indique simplement au noyau de déplacer le séparateur entre le "trou" et le "tas" plus loin dans la région "trou". Il n'y a aucun moyen de retourner de la mémoire au système d'exploitation et le système d'exploitation lui-même ne gère pas votre segment de mémoire. Votre bibliothèque d'exécution C doit le fournir (via malloc).
Cela a également des implications sur le type de code réellement utilisé dans les binaires POSIX. Les boîtes POSIX (presque universellement) utilisent le format de fichier ELF. Dans ce format, le système d'exploitation est responsable des communications entre les bibliothèques dans différents fichiers ELF. Par conséquent, toutes les bibliothèques utilisent du code indépendant de la position (c'est-à-dire que le code lui-même peut être chargé dans différentes adresses mémoire et toujours fonctionner), et tous les appels entre les bibliothèques sont passés via une table de recherche pour savoir où le contrôle doit sauter pour la croix appels de fonction de bibliothèque. Cela ajoute une surcharge et peut être exploité si l'une des bibliothèques modifie la table de recherche.
Le modèle de mémoire de Windows est différent car le type de code qu'il utilise est différent. Windows utilise le format de fichier PE, qui laisse le code dans un format dépendant de la position. Autrement dit, le code dépend de l'endroit où exactement dans la mémoire virtuelle le code est chargé. Il y a un indicateur dans la spécification PE qui indique au système d'exploitation où exactement en mémoire la bibliothèque ou l'exécutable voudraient être mappés lorsque votre programme s'exécute. Si un programme ou une bibliothèque ne peut pas être chargé à son adresse préférée, le chargeur Windows doit rebaser la bibliothèque/l'exécutable - en gros, il déplace la position en fonction de la position code pour pointer vers les nouvelles positions - ce qui ne nécessite pas de tables de recherche et ne peut pas être exploité car il n'y a pas de table de recherche à remplacer. Malheureusement, cela nécessite une implémentation très compliquée dans le chargeur Windows et a un temps de démarrage considérable si une image doit être rebasée. Les gros progiciels commerciaux modifient souvent leurs bibliothèques pour démarrer à dessein à différentes adresses pour éviter le rebasage; Windows le fait lui-même avec ses propres bibliothèques (par exemple, ntdll.dll, kernel32.dll, psapi.dll, etc. - toutes ont des adresses de démarrage différentes par défaut)
Sous Windows, la mémoire virtuelle est obtenue du système via un appel à VirtualAlloc , et elle est renvoyée au système via VirtualFree (D'accord, techniquement VirtualAlloc se ferme vers NtAllocateVirtualMemory, mais c'est un détail d'implémentation) (Comparez cela à POSIX, où la mémoire ne peut pas être récupérée). Ce processus est lent (et l'IIRC, nécessite que vous allouiez en segments de taille de page physique; généralement 4 Ko ou plus). Windows fournit également ses propres fonctions de tas (HeapAlloc, HeapFree, etc.) dans le cadre d'une bibliothèque connue sous le nom de RtlHeap, qui est incluse en tant que partie de Windows elle-même, sur laquelle le runtime C (c'est-à-dire malloc
et amis) est généralement implémenté.
Windows possède également un certain nombre d'API d'allocation de mémoire héritées du temps où il devait gérer les anciens 80386, et ces fonctions sont désormais intégrées à RtlHeap. Pour plus d'informations sur les différentes API qui contrôlent la gestion de la mémoire dans Windows, consultez cet article MSDN: http://msdn.Microsoft.com/en-us/library/ms810627 .
Notez également que cela signifie que sous Windows, un seul processus et (et généralement) possède plusieurs segments de mémoire. (En règle générale, chaque bibliothèque partagée crée son propre segment de mémoire.)
(La plupart de ces informations proviennent du "Codage sécurisé en C et C++" de Robert Seacord)
La pile
Dans l'architecture X86, la CPU exécute des opérations avec des registres. La pile n'est utilisée que pour des raisons de commodité. Vous pouvez enregistrer le contenu de vos registres à empiler avant d'appeler un sous-programme ou une fonction système, puis les recharger pour continuer votre opération là où vous vous étiez arrêté. (Vous pouvez le faire manuellement sans la pile, mais c'est une fonction fréquemment utilisée, elle prend donc en charge le processeur). Mais vous pouvez faire à peu près n'importe quoi sans la pile dans un PC.
Par exemple une multiplication entière:
MUL BX
Multiplie le registre AX avec le registre BX. (Le résultat sera en DX et AX, DX contenant les bits supérieurs).
Les machines basées sur la pile (comme Java VM) utilisent la pile pour leurs opérations de base. La multiplication ci-dessus:
DMUL
Cela fait apparaître deux valeurs du haut de la pile et multiplie le temps, puis repousse le résultat dans la pile. La pile est essentielle pour ce type de machines.
Certains langages de programmation de niveau supérieur (comme C et Pascal) utilisent cette méthode ultérieure pour passer des paramètres aux fonctions: les paramètres sont poussés vers la pile dans l'ordre de gauche à droite et sautés par le corps de la fonction et les valeurs de retour sont repoussées. (C'est un choix que les fabricants de compilateurs font et abusent de la façon dont le X86 utilise la pile).
Le tas
Le tas est un autre concept qui n'existe que dans le domaine des compilateurs. Cela prend la peine de gérer la mémoire derrière vos variables, mais ce n'est pas une fonction du CPU ou du système d'exploitation, c'est juste un choix de gestion du bloc de mémoire qui est donné par le système d'exploitation. Vous pouvez le faire plusieurs fois si vous le souhaitez.
Accès aux ressources système
Le système d'exploitation dispose d'une interface publique permettant d'accéder à ses fonctions. En DOS, les paramètres sont passés dans les registres du CPU. Windows utilise la pile pour transmettre les paramètres des fonctions du système d'exploitation (l'API Windows).