Que se passe-t-il lorsqu'un programme informatique s'exécute?

180

Je connais la théorie générale mais je ne peux pas entrer dans les détails.

Je sais qu'un programme réside dans la mémoire secondaire d'un ordinateur. Une fois que le programme commence l'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 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. 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' newopérateur en C ++)

Ce que je ne comprends pas, c'est comment ces deux choses sont liées. À quel moment la pile est-elle utilisée pour l'exécution des instructions? Les instructions vont de la RAM, à la pile, aux registres?

gaijinco
la source
43
+1 pour poser une question fondamentale!
mkelley33
21
hmm ... vous savez, ils écrivent des livres à ce sujet. Voulez-vous vraiment étudier cette partie de l'architecture du système d'exploitation avec l'aide de SO?
Andrey
1
J'ai ajouté quelques balises basées sur la nature de la question liée à la mémoire et la référence à C ++, même si je pense qu'une bonne réponse pourrait également venir de quelqu'un qui connaît Java ou C #!)
mkelley33
14
Vote positif et favori. J'ai toujours eu trop peur de demander ...
Maxpm
2
Le terme «les met dans des registres» n'est pas tout à fait exact. Sur la plupart des processeurs, les registres sont utilisés pour contenir des valeurs intermédiaires et non du code exécutable.

Réponses:

161

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 newand delete, et mallocet 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.csi 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.]

Sdaz MacSkibbons
la source
4
désolé, mais une bonne recommandation de livre serait une meilleure réponse, IMO
Andrey
13
Ouais, "RTFM" est toujours meilleur.
Sdaz MacSkibbons
56
@Andrey: peut-être devriez-vous changer ce commentaire en "aussi, vous voudrez peut-être lire votre-bonne-recommandation-de-livre ", je comprends que ce genre de question mérite une enquête plus approfondie, mais chaque fois que vous devez commencer un commentaire par "désolé mais. .. "peut-être devriez-vous vraiment envisager de signaler le message à l'attention du modérateur ou au moins d'expliquer pourquoi votre opinion devrait avoir de l'importance pour quiconque de toute façon.
mkelley33
2
Excellente réponse. Cela a certainement éclairci certaines choses pour moi!
Maxpm
2
@Mikael: Selon l'implémentation, vous pouvez avoir une mise en cache obligatoire, auquel cas chaque fois que des données sont lues à partir de la mémoire, une ligne de cache entière est lue et le cache est rempli. Ou il peut être possible de donner au gestionnaire de cache un indice que les données ne seront nécessaires qu'une seule fois, donc les copier dans le cache n'est pas utile. C'est pour lire. Pour l'écriture, il existe des caches de réécriture et d'écriture directe, qui affectent le moment où les contrôleurs DMA peuvent lire les données, puis il existe toute une série de protocoles de cohérence de cache pour traiter plusieurs processeurs ayant chacun leur propre cache. Cela mérite vraiment son propre Q.
Ben Voigt
61

Sdaz a obtenu un nombre remarquable de votes positifs en très peu de temps, mais perpétue malheureusement 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 particulier 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 du processeur x86 sont:

  • Registres généraux EAX EBX ECX EDX

  • Registres de segments 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 a également ses propres registres, nous allons à nouveau traiter cela comme une unité de traitement séparée.

Aucun de ces registres n'est utilisé pour le code exécutable. EIPcontient 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 des données (architecture Harvard). Toutes les machines actuelles ont une architecture Harvard à l'intérieur du CPU. La plupart de ces jours sont également l'architecture de Harvard dans le cache. x86 (votre ordinateur de bureau commun) sont l'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 hors de propos, puisque nous parlons de ce qui se passe à l'intérieur du processeur.

La séquence classique enseignée dans l'architecture informatique est l'extraction-décodage-exécution. 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 une série entière d'instructions de microcode. Cela permet le pipelining, car les ressources utilisées par la première micro-instruction ne sont pas nécessaires plus tard, afin qu'ils puissent commencer à travailler sur la première micro-instruction à partir de l'instruction suivante.

Pour couronner le tout, la terminologie est légèrement confuse car registre est un terme de génie électrique pour une collection de bascules D. Et les instructions (ou en particulier les micro-instructions) peuvent très bien être stockées temporairement dans une telle collection de D-flipflops. Mais ce n'est pas ce que l'on entend lorsqu'un informaticien, un ingénieur en logiciel ou un développeur ordinaire utilise le terme registre . Ils désignent les registres de chemin de données énumérés ci-dessus, et ceux-ci ne sont pas utilisés pour le transport de code.

Les noms et le nombre de registres de chemin de données varient pour d'autres architectures de CPU, telles que ARM, MIPS, Alpha, PowerPC, mais tous exécutent des instructions sans les passer par l'ALU.

Ben Voigt
la source
Merci pour la clarification. J'ai hésité à ajouter cela car je ne le connais pas intimement, mais je l'ai fait à la demande de quelqu'un d'autre.
Sdaz MacSkibbons
s / ARM / RAM / in "signifiant que les données et le code sont entremêlés dans ARM". Droite?
Bjarke Freund-Hansen
@bjarkef: La première fois oui, mais pas la seconde. Je le réparerai.
Ben Voigt
17

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 boîtiers 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 dit simplement au noyau de déplacer le séparateur entre le "trou" et le "tas" plus loin dans la région "trou". Il n'existe aucun moyen de restituer la mémoire au système d'exploitation et le système d'exploitation lui-même ne gère pas votre tas. Votre bibliothèque d'exécution C doit fournir cela (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 un 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 bibliothèques sont passés par une table de recherche pour savoir où le contrôle doit sauter pour traverser 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 au format dépendant de la position. Autrement dit, le code dépend de l'endroit exact où le code est chargé dans la mémoire virtuelle. 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 aimerait être mappé 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 rebasela bibliothèque / l'exécutable - en gros, il déplace le code dépendant de la position 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 à écraser. Malheureusement, cela nécessite une implémentation très compliquée dans le chargeur Windows et un temps de démarrage considérable si une image doit être rebasée. Les grands progiciels commerciaux modifient souvent leurs bibliothèques pour démarrer délibérément à des adresses différentes pour éviter le rebasage; Windows lui-même le fait avec ses propres bibliothèques (par exemple ntdll.dll, kernel32.dll, psapi.dll, etc. - ont toutes des adresses de démarrage différentes par défaut)

Sous Windows, la mémoire virtuelle est obtenue à partir 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 de mise en œuvre) (Comparez ceci à POSIX, où la mémoire ne peut pas être récupéré). Ce processus est lent (et IIRC, nécessite que vous allouiez des blocs de la taille d'une 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 fait partie de Windows lui-même, sur laquelle le runtime C (c'est-à-dire mallocet les amis) est généralement implémenté.

Windows dispose également de quelques API d'allocation de mémoire héritées de l'époque où il devait gérer les anciens 80386, et ces fonctions sont désormais construites au-dessus de 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 a (et a généralement) plus d'un tas. (En règle générale, chaque bibliothèque partagée crée son propre tas.)

(La plupart de ces informations proviennent de «Secure Coding in C and C ++» de Robert Seacord)

Billy ONeal
la source
Grande info, merci! J'espère que "user487117" reviendra finalement. :-)
Sdaz MacSkibbons
5

La pile

Dans l'architercture 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 sauvegarder 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 en étiez. (Vous pouvez le faire manuellement sans la pile, mais c'est une fonction fréquemment utilisée, donc elle prend en charge le processeur). Mais vous pouvez faire à peu près tout 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 tem, 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 dans 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 font les fabricants du compilateur et qui abuse en quelque sorte 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 enlève 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 interne 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éder aux ressources système

Le système d'exploitation dispose d'une interface publique permettant d'accéder à ses fonctions. Sous 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).

vbence
la source