190 votes

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

Je connais la théorie générale mais je n'arrive 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 commence son exécution, il est entièrement copié dans la mémoire vive. 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 primaire 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 est lié à la fonction new opérateur en C++)

Ce que je n'arrive pas à comprendre, c'est le lien entre ces deux choses. À quel moment la pile est-elle utilisée pour l'exécution des instructions ? Les instructions vont de la RAM, à la pile, aux registres ?

165voto

Sdaz MacSkibbons Points 6982

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.]

62voto

Ben Voigt Points 151460

Sdaz a obtenu un nombre remarquable de votes positifs en très peu de temps, mais il 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 ?

dit Sdaz :

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. Elles sont ensuite (généralement) copiées de la zone TEXT dans le pipeline du CPU, puis dans les registres du CPU, et exécutées à partir de là.

Mais c'est faux. Sauf dans le cas particulier du code auto-modifiant, les instructions n'entrent jamais dans le chemin de données. Et elles ne sont pas, ne peuvent pas être, exécutées depuis le chemin de données.

El Registres du CPU x86 sont :

  • Registres généraux EAX EBX ECX EDX

  • Registres des segments CS DS ES FS GS SS

  • Index et pointeurs ESI EDI EBP EIP ESP

  • Indicateur EFLAGS

Il existe également quelques 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 de l'unité centrale dispose également de quelques registres qui lui sont propres, nous les traiterons à nouveau 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 en cours d'exécution, et non l'instruction elle-même.

Les instructions empruntent un chemin complètement différent de celui des données dans le processeur (architecture Harvard). Toutes les machines actuelles ont une architecture Harvard dans le CPU. La plupart des machines actuelles sont également d'architecture Harvard dans le cache. Les x86 (votre machine de bureau commune) sont d'architecture Von Neumann dans la mémoire principale, ce qui signifie que les données et le code sont mélangés dans la RAM. Cela n'a rien à voir, puisque nous parlons de ce qui se passe à l'intérieur de l'unité centrale.

La séquence classique enseignée dans le cadre de l'architecture des ordinateurs est "fetch-decode-execute". Le contrôleur mémoire recherche l'instruction stockée à l'adresse EIP . Les bits de l'instruction passent par une logique combinatoire afin de 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 envoyé par horloge à la destination. L'instruction suivante est alors extraite.

Sur un processeur moderne, les choses fonctionnent un peu différemment. Chaque instruction entrante est traduite en une série complète d'instructions de microcode. Cela permet le pipelining, car les ressources utilisées par la première microinstruction ne sont pas nécessaires par la suite, de sorte que l'on peut commencer à travailler sur la première microinstruction à partir de l'instruction suivante.

Pour couronner le tout, la terminologie est légèrement confuse car enregistrez est un terme d'ingénierie électrique pour une collection de D-flipflops. Et des instructions (ou surtout des 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 lambda utilise l'expression enregistrez . Ils désignent les registres du chemin de données tels qu'énumérés ci-dessus, et ceux-ci 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 CPU, comme ARM, MIPS, Alpha, PowerPC, mais toutes exécutent les instructions sans les passer par l'UAL.

17voto

Billy ONeal Points 50631

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érons 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.");
    }
}

Sur Windows NT (et ses enfants), ce programme va généralement produire :

Le tas est au-dessus de la pile

Sur les boîtes POSIX, il va dire :

La pile est au-dessus du tas

Le modèle de mémoire UNIX est très bien expliqué ici par @Sdaz MacSkibbons, je ne vais donc pas le répéter ici. Mais ce n'est pas le seul modèle de mémoire. La raison pour laquelle POSIX requiert ce modèle est le sbrk appel système. Fondamentalement, sur une boîte POSIX, pour obtenir plus de mémoire, un processus demande simplement au noyau de déplacer le séparateur entre le "trou" et le "tas" plus loin dans la région du "trou". Il n'y a aucun moyen de rendre de 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 s'en charger (via malloc).

Cela a également des implications pour le type de code réellement utilisé dans les binaires POSIX. Les binaires POSIX utilisent (presque universellement) 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é à différentes adresses mémoire et continuer à fonctionner), et tous les appels entre les bibliothèques passent par une table de consultation pour savoir où le contrôle doit sauter pour les appels de fonctions entre bibliothèques. Cela ajoute une certaine surcharge et peut être exploité si l'une des bibliothèques modifie la table de consultation.

Le modèle de mémoire de Windows est différent parce que 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 exact de la mémoire virtuelle où il est chargé. La spécification PE contient un indicateur qui indique au système d'exploitation l'endroit exact de la mémoire où la bibliothèque ou l'exécutable souhaite ê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 de Windows doit rebasement la bibliothèque/exécutable -- en gros, elle déplace le code dépendant de la position pour pointer vers les nouvelles positions -- ce qui ne nécessite pas de tables de consultation et ne peut pas être exploité car il n'y a pas de table de consultation à écraser. Malheureusement, cela nécessite une mise en œuvre très compliquée dans le chargeur de Windows, et entraîne un surcoût considérable au démarrage si une image doit être rebasée. Les grands logiciels commerciaux modifient souvent leurs bibliothèques pour qu'elles démarrent délibérément à des adresses différentes afin d'éviter le rebasage ; Windows lui-même le fait 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 il est renvoyé au système par VirtualFree (D'accord, techniquement, VirtualAlloc se transforme en 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 IIRC, exige que vous allouiez en morceaux de la taille d'une page physique ; typiquement 4kb 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 dans Windows lui-même, sur laquelle le runtime C (c'est-à-dire, malloc et amis) est généralement mis en œuvre.

Windows possède également un certain nombre d'API d'allocation de mémoire héritées de l'époque où il devait gérer les vieux 80386, et ces fonctions sont maintenant 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 peut (et c'est généralement le cas) avoir plus d'un tas. (Typiquement, 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).

5voto

vbence Points 10528

La pile

Dans l'architecture X86, l'unité centrale exécute les opérations à l'aide de registres. La pile n'est utilisée que pour des raisons de commodité. Vous pouvez sauvegarder le contenu de vos registres sur la pile avant d'appeler une sous-routine ou une fonction système, puis les recharger pour continuer votre opération là où vous l'avez laissée. (Vous pourriez le faire manuellement sans la pile, mais il s'agit d'une fonction fréquemment utilisée et elle est donc supportée par le CPU). Mais vous pouvez faire à peu près tout sans la pile dans un PC.

Par exemple, une multiplication de nombres entiers :

MUL BX

Multiplie le registre AX par le registre BX. (Le résultat sera dans DX et AX, DX contenant les bits les plus élevés).

Les machines basées sur la pile (comme JAVA VM) utilisent la pile pour leurs opérations de base. La multiplication ci-dessus :

DMUL

Cette opération extrait deux valeurs du haut de la pile et les multiplie, puis repousse le résultat sur la pile. La pile est essentielle pour ce type de machines.

Certains langages de programmation de plus haut niveau (comme le C et le Pascal) utilisent cette dernière méthode pour passer les paramètres aux fonctions : les paramètres sont poussés sur la pile dans l'ordre de gauche à droite, puis extraits par le corps de la fonction et les valeurs de retour sont repoussées. (Il s'agit d'un choix fait par les fabricants de compilateurs, qui abuse de la façon dont le X86 utilise la pile).

Le tas

Le tas est un autre concept qui n'existe que dans le royaume des compilateurs. Il élimine la difficulté 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 de manière multidimensionnelle si vous le souhaitez.

Accès aux ressources du système

Le système d'exploitation possède une interface publique qui vous permet d'accéder à ses fonctions. Sous DOS, les paramètres sont passés dans les registres de l'unité centrale. Windows utilise la pile pour passer les paramètres des fonctions du système d'exploitation (l'API Windows).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X