webentwicklung-frage-antwort-db.com.de

Was passiert, wenn ein Computerprogramm ausgeführt wird?

Ich kenne die allgemeine Theorie, aber ich kann nicht in die Details passen.

Ich weiß, dass sich ein Programm im sekundären Speicher eines Computers befindet. Sobald das Programm mit der Ausführung beginnt, wird es vollständig in den RAM kopiert. Anschließend ruft der Prozessor jeweils einige Befehle ab (dies hängt von der Größe des Busses ab), legt sie in Registern ab und führt sie aus.

Ich weiß auch, dass ein Computerprogramm zwei Arten von Speicher verwendet: Stack und Heap, die auch Teil des Primärspeichers des Computers sind. Der Stack wird für nicht dynamischen Speicher und der Heap für dynamischen Speicher verwendet (z. B. alles, was mit dem Operator new in C++ zusammenhängt).

Was ich nicht verstehen kann, ist, wie diese beiden Dinge sich verbinden. Wann wird der Stack für die Ausführung der Anweisungen verwendet? Befehle gehen vom RAM zum Stack zu den Registern?

176
gaijinco

Es hängt wirklich vom System ab, aber moderne Betriebssysteme mit virtuellem Speicher neigen dazu, ihre Prozessabbilder zu laden und Speicher in etwa wie folgt zuzuweisen:

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

Dies ist der allgemeine Prozessadressraum auf vielen gängigen virtuellen Speichersystemen. Das "Loch" ist die Größe Ihres gesamten Speichers abzüglich des von allen anderen Bereichen belegten Speicherplatzes. Dies gibt viel Platz für den Haufen, in den er hineinwachsen kann. Dies ist auch "virtuell", was bedeutet, dass es über eine Übersetzungstabelle Ihrem actual - Speicher zugeordnet ist und tatsächlich an einem beliebigen Ort im tatsächlichen Speicher gespeichert werden kann. Dies geschieht auf diese Weise, um einen Prozess vor dem Zugriff auf den Speicher eines anderen Prozesses zu schützen und jedem Prozess den Eindruck zu vermitteln, dass er auf einem vollständigen System ausgeführt wird.

Beachten Sie, dass die Positionen von z. B. dem Stapel und dem Haufen auf einigen Systemen in einer anderen Reihenfolge sein können (siehe Billy O'Neals Antwort unten für weitere Details zu Win32).

Andere Systeme können sehr unterschiedlich sein. DOS zum Beispiel lief im Real-Modus und die Speicherzuordnung beim Ausführen von Programmen sah ganz anders aus:

+-----------+ 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

Sie können sehen, dass DOS den direkten Zugriff auf den Arbeitsspeicher des Betriebssystems ohne Schutz zuließ, was bedeutete, dass User-Space-Programme im Allgemeinen direkt auf alles zugreifen oder alles überschreiben konnten, was ihnen gefiel.

Im Adressraum des Prozesses sahen die Programme jedoch in der Regel ähnlich aus, nur wurden sie als Codesegment, Datensegment, Heap, Stapelsegment usw. beschrieben und etwas anders zugeordnet. Aber die meisten allgemeinen Bereiche waren noch da.

Wenn Sie das Programm und die erforderlichen gemeinsam genutzten Bibliotheken in den Speicher laden und die Programmteile auf die richtigen Bereiche verteilen, führt das Betriebssystem Ihren Prozess dort aus, wo sich seine Hauptmethode befindet, und Ihr Programm übernimmt von dort aus die erforderlichen Systemaufrufe es braucht sie.

Verschiedene Systeme (eingebettet, was auch immer) können sehr unterschiedliche Architekturen haben, wie z. B. Stackless-Systeme, Harvard-Architektursysteme (wobei Code und Daten in einem separaten physischen Speicher gespeichert werden), Systeme, die das BSS tatsächlich im Nur-Lese-Speicher halten (ursprünglich festgelegt von der Programmierer) usw. Aber das ist der allgemeine Kern.


Du sagtest:

Ich weiß auch, dass ein Computerprogramm zwei Arten von Speicher verwendet: Stack und Heap, die auch Teil des Primärspeichers des Computers sind.

"Stapel" und "Haufen" sind nur abstrakte Begriffe und keine (notwendigerweise) physisch unterschiedlichen "Arten" von Erinnerungen.

Ein Stapel ist lediglich eine Last-In-First-Out-Datenstruktur. In der x86-Architektur kann die Adresse tatsächlich zufällig mithilfe eines Versatzes vom Ende aus angesprochen werden. Die gebräuchlichsten Funktionen sind jedoch Push und POP, um Elemente hinzuzufügen bzw. daraus zu entfernen. Es wird üblicherweise für funktionslokale Variablen (sogenanntes "automatisches Speichern"), Funktionsargumente, Rücksprungadressen usw. verwendet (weiter unten).

Ein "Haufen" ist nur ein Spitzname für einen Speicherbereich, der bei Bedarf zugewiesen und zufällig adressiert werden kann (dh, Sie können direkt auf einen beliebigen Speicherbereich zugreifen). Es wird häufig für Datenstrukturen verwendet, die Sie zur Laufzeit zuweisen (in C++ mit new und delete und malloc und Freunden in C usw.).

Der Stack und der Heap in der x86-Architektur befinden sich beide physisch in Ihrem Systemspeicher (RAM) und werden wie oben beschrieben durch Zuweisung des virtuellen Speichers in den Prozessadressraum abgebildet.

Die Register (noch auf x86) befinden sich physisch im Prozessor (im Gegensatz zum RAM) und werden vom Prozessor aus dem TEXT-Bereich geladen (und können auch von einer anderen Stelle im Speicher oder einer anderen Stelle geladen werden) Stellen abhängig von den tatsächlich ausgeführten CPU-Anweisungen). Es handelt sich im Wesentlichen nur um sehr kleine, sehr schnelle On-Chip-Speicherplätze, die für verschiedene Zwecke verwendet werden.

Das Registerlayout ist stark von der Architektur abhängig (Register, Befehlssatz und Speicherlayout/-design sind genau das, was unter "Architektur" zu verstehen ist). Ich werde es daher nicht näher erläutern, empfehle jedoch die Verwendung von Assemblersprachkurs, um sie besser zu verstehen.


Ihre Frage:

Wann wird der Stack für die Ausführung der Anweisungen verwendet? Befehle gehen vom RAM zum Stack zu den Registern?

Der Stack (in Systemen/Sprachen, die sie haben und verwenden) wird am häufigsten wie folgt verwendet:

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

Schreiben Sie ein einfaches Programm wie dieses und kompilieren Sie es dann in Assembly (gcc -S foo.c, wenn Sie Zugriff auf GCC haben), und werfen Sie einen Blick darauf. Die Versammlung ist ziemlich einfach zu folgen. Sie sehen, dass der Stack für lokale Funktionsvariablen und zum Aufrufen von Funktionen verwendet wird, in denen deren Argumente und Rückgabewerte gespeichert sind. Dies ist auch der Grund, warum Sie Folgendes tun:

f( g( h( i ) ) ); 

All diese werden nacheinander aufgerufen. Es wird buchstäblich ein Stapel von Funktionsaufrufen und deren Argumenten aufgebaut, ausgeführt und dann beim Zurückspulen (oder Hochspulen;) entfernt. Wie oben erwähnt, befindet sich der Stapel (auf x86) jedoch tatsächlich in Ihrem Prozessspeicher (im virtuellen Speicher), sodass er direkt bearbeitet werden kann. Es ist kein separater Schritt während der Ausführung (oder zumindest orthogonal zum Prozess).

Zu Ihrer Information, das Obige ist die C-Aufruf-Konvention , die auch von C++ verwendet wird. Andere Sprachen/Systeme übertragen Argumente möglicherweise in einer anderen Reihenfolge auf den Stapel, und einige Sprachen/Plattformen verwenden nicht einmal Stapel und gehen anders vor.

Beachten Sie auch, dass dies keine tatsächlichen Zeilen von C-Code sind, die ausgeführt werden. Der Compiler hat sie in Anweisungen in Maschinensprache in Ihrer ausführbaren Datei konvertiert. Sie werden dann (allgemein) aus dem TEXT-Bereich in die CPU-Pipeline, dann in die CPU-Register kopiert und von dort ausgeführt.  [Das war falsch. Siehe Korrektur von Ben Voigt unten.]

158

Sdaz hat in kürzester Zeit eine bemerkenswerte Anzahl von Upvotes erhalten, setzt aber leider ein Missverständnis darüber fort, wie sich Anweisungen durch die CPU bewegen.

Die Frage stellte sich:

Befehle gehen vom RAM zum Stack zu den Registern?

Sdaz sagte:

Beachten Sie auch, dass dies keine tatsächlichen Zeilen von C-Code sind, die ausgeführt werden. Der Compiler hat sie in Anweisungen in Maschinensprache in Ihrer ausführbaren Datei konvertiert. Sie werden dann (allgemein) aus dem TEXT-Bereich in die CPU-Pipeline, dann in die CPU-Register kopiert und von dort ausgeführt.

Das ist aber falsch. Mit Ausnahme des Sonderfalls des sich selbst ändernden Codes geben Anweisungen niemals den Datenpfad ein. Und sie können nicht über den Datenpfad ausgeführt werden.

Die x86-CPU-Register lauten:

  • Allgemeine Register EAX EBX ECX EDX

  • Segmentregister CS DS ES FS GS SS

  • Index und Zeiger ESI EDI EBP EIP ESP

  • Indikator EFLAGS

Es gibt auch einige Gleitkomma- und SIMD-Register, aber für die Zwecke dieser Erörterung werden diese als Teil des Coprozessors und nicht der CPU klassifiziert. Die Speicherverwaltungseinheit in der CPU verfügt auch über eigene Register, die wir wiederum als separate Verarbeitungseinheit behandeln.

Keines dieser Register wird für ausführbaren Code verwendet. EIP enthält die Adresse des ausführenden Befehls, nicht den Befehl selbst.

Anweisungen gehen in der CPU einen völlig anderen Weg als Daten (Harvard-Architektur). Alle aktuellen Maschinen sind Harvard-Architekturen innerhalb der CPU. Die meisten dieser Tage sind auch Harvard-Architektur im Cache. x86 (Ihr gemeinsamer Desktop-Computer) ist eine Von Neumann-Architektur im Hauptspeicher, dh Daten und Code werden im RAM vermischt. Das ist nebensächlich, da wir darüber sprechen, was in der CPU passiert.

Die klassische Sequenz, die in der Computerarchitektur gelehrt wird, lautet Fetch-Decode-Execute. Der Speichercontroller schlägt den Befehl nach, der unter der Adresse EIP gespeichert ist. Die Bits des Befehls durchlaufen eine Kombinationslogik, um alle Steuersignale für die verschiedenen Multiplexer im Prozessor zu erzeugen. Und nach einigen Zyklen kommt die Recheneinheit zu einem Ergebnis, das in das Ziel getaktet wird. Dann wird die nächste Anweisung abgerufen.

Bei einem modernen Prozessor sieht das etwas anders aus. Jeder eingehende Befehl wird in eine ganze Reihe von Mikrocode-Befehlen übersetzt. Auf diese Weise wird das Pipelining aktiviert, da die vom ersten Mikrobefehl verwendeten Ressourcen später nicht benötigt werden, sodass sie ab dem nächsten Befehl mit der Bearbeitung des ersten Mikrobefehls beginnen können.

Um das Ganze abzurunden, die Terminologie ist leicht verwirrt, da register ein elektrotechnischer Begriff für eine Sammlung von D-Flipflops ist. Und Anweisungen (oder insbesondere Mikrobefehle) können sehr gut vorübergehend in einer solchen Sammlung von D-Flipflops gespeichert werden. Dies ist jedoch nicht gemeint, wenn ein Informatiker, Softwareentwickler oder gewöhnlicher Entwickler den Begriff register verwendet. Sie bezeichnen die oben aufgeführten Datenpfadregister, die nicht zum Transportieren von Code verwendet werden.

Die Namen und die Anzahl der Datenpfadregister variieren für andere CPU-Architekturen wie ARM, MIPS, Alpha, PowerPC, aber alle führen Anweisungen aus, ohne sie durch die ALU zu führen.

59
Ben Voigt

Das genaue Layout des Speichers während der Ausführung eines Prozesses hängt vollständig von der verwendeten Plattform ab. Betrachten Sie das folgende Testprogramm:

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

Unter Windows NT (und seinen Kindern) wird dieses Programm im Allgemeinen Folgendes produzieren:

Der Haufen liegt über dem Stapel

Auf POSIX-Boxen heißt es:

Der Stapel liegt über dem Haufen

Das UNIX-Speichermodell wird hier von @Sdaz MacSkibbons recht gut erklärt, daher werde ich das hier nicht wiederholen. Dies ist jedoch nicht das einzige Speichermodell. Der Grund, warum POSIX dieses Modell benötigt, ist der sbrk Systemaufruf. Um auf einer POSIX-Box mehr Speicher zu erhalten, weist ein Prozess den Kernel lediglich an, den Teiler zwischen dem "Loch" und dem "Haufen" weiter in den "Loch" -Bereich zu verschieben. Es gibt keine Möglichkeit, Speicher an das Betriebssystem zurückzugeben, und das Betriebssystem selbst verwaltet Ihren Heap nicht. Ihre C-Laufzeitbibliothek muss dies bereitstellen (über malloc).

Dies hat auch Auswirkungen auf die Art des in POSIX-Binärdateien tatsächlich verwendeten Codes. POSIX-Boxen (fast universell) verwenden das ELF-Dateiformat. In diesem Format ist das Betriebssystem für die Kommunikation zwischen Bibliotheken in verschiedenen ELF-Dateien verantwortlich. Daher verwenden alle Bibliotheken positionsunabhängigen Code (das heißt, der Code selbst kann in verschiedene Speicheradressen geladen werden und funktioniert weiterhin), und alle Aufrufe zwischen Bibliotheken werden über eine Nachschlagetabelle weitergeleitet, um herauszufinden, wo die Steuerung nach Cross springen muss Funktionsaufrufe der Bibliothek. Dies erhöht den Aufwand und kann ausgenutzt werden, wenn eine der Bibliotheken die Nachschlagetabelle ändert.

Das Speichermodell von Windows unterscheidet sich, da der verwendete Code unterschiedlich ist. Windows verwendet das PE-Dateiformat, das den Code in einem positionsabhängigen Format belässt. Das heißt, der Code hängt davon ab, wo genau im virtuellen Speicher der Code geladen wird. In der PE-Spezifikation befindet sich ein Flag, das dem Betriebssystem mitteilt, wo genau die Bibliothek oder die ausführbare Datei im Speicher abgelegt werden soll, wenn Ihr Programm ausgeführt wird. Wenn ein Programm oder eine Bibliothek nicht an ihrer bevorzugten Adresse geladen werden kann, muss der Windows-Loader rebase die Bibliothek/ausführbare Datei - im Grunde genommen verschiebt er den positionsabhängigen Code, um auf die neuen Positionen zu zeigen - Das erfordert keine Nachschlagetabellen und kann nicht ausgenutzt werden, da keine Nachschlagetabelle zum Überschreiben vorhanden ist. Leider erfordert dies eine sehr komplizierte Implementierung im Windows-Loader und verursacht einen erheblichen Zeitaufwand beim Start, wenn ein Image neu basiert werden muss. Große kommerzielle Softwarepakete ändern ihre Bibliotheken häufig so, dass sie absichtlich an verschiedenen Adressen gestartet werden, um ein erneutes Basieren zu vermeiden. Windows selbst erledigt dies mit seinen eigenen Bibliotheken (z. B. ntdll.dll, kernel32.dll, psapi.dll usw. - alle haben standardmäßig unterschiedliche Startadressen).

Unter Windows wird der virtuelle Speicher über einen Aufruf von VirtualAlloc vom System abgerufen und über VirtualFree an das System zurückgegeben das ist ein Implementierungsdetail) (Vergleichen Sie dies mit POSIX, wo Speicher nicht zurückgewonnen werden kann). Dieser Prozess ist langsam (und IIRC erfordert, dass Sie Blöcke mit physischer Seitengröße zuweisen (normalerweise 4 KB oder mehr). Windows bietet außerdem eigene Heap-Funktionen (HeapAlloc, HeapFree usw.) als Teil einer Bibliothek namens RtlHeap, die als Teil von Windows selbst enthalten ist und auf der die C-Laufzeit (d. H. malloc und friends) ist in der Regel implementiert.

Windows verfügt auch über eine Reihe älterer Speicherzuweisungs-APIs aus der Zeit, als es mit alten 80386s zu tun hatte, und diese Funktionen basieren jetzt auf RtlHeap. Weitere Informationen zu den verschiedenen APIs, die die Speicherverwaltung in Windows steuern, finden Sie in diesem MSDN-Artikel: http://msdn.Microsoft.com/en-us/library/ms810627 .

Beachten Sie auch, dass dies unter Windows bedeutet, dass ein einzelner Prozess (und in der Regel) mehr als einen Heap hat. (Normalerweise erstellt jede gemeinsam genutzte Bibliothek einen eigenen Heap.)

(Die meisten dieser Informationen stammen aus "Secure Coding in C und C++" von Robert Seacord.)

17
Billy ONeal

Der Stapel

In der X86-Architektur führt die CPU Operationen mit Registern aus. Der Stack wird nur aus praktischen Gründen verwendet. Sie können den Inhalt Ihrer Register zum Stapeln speichern, bevor Sie eine Unterroutine oder eine Systemfunktion aufrufen, und diese dann wieder laden, um die Operation dort fortzusetzen, wo Sie sie verlassen haben. (Sie können es manuell ohne den Stack ausführen, aber es ist eine häufig verwendete Funktion, sodass sie CPU-Unterstützung bietet.) Aber Sie können so ziemlich alles ohne den Stack in einem PC machen.

Zum Beispiel eine ganzzahlige Multiplikation:

MUL BX

Multipliziert das AX-Register mit dem BX-Register. (Das Ergebnis ist in DX und AX, wobei DX die höheren Bits enthält).

Stack-basierte Maschinen (wie Java VM)) verwenden den Stack für ihre Grundoperationen. Die obige Multiplikation:

DMUL

Dadurch werden zwei Werte vom oberen Rand des Stapels abgerufen, multipliziert und das Ergebnis zurück zum Stapel verschoben. Stack ist für diese Art von Maschinen unerlässlich.

Einige übergeordnete Programmiersprachen (wie C und Pascal) verwenden diese spätere Methode, um Parameter an Funktionen zu übergeben: Die Parameter werden in der Reihenfolge von links nach rechts in den Stapel verschoben und vom Funktionshauptteil abgerufen, und die Rückgabewerte werden zurückgeschoben. (Diese Entscheidung treffen die Compiler-Hersteller und missbrauchen die Art und Weise, wie der X86 den Stack verwendet.).

Der Haufen

Der Haufen ist ein anderes Konzept, das nur im Bereich der Compiler existiert. Es nimmt Ihnen die Mühe, den Speicher hinter Ihren Variablen zu verwalten, aber es ist keine Funktion der CPU oder des Betriebssystems, sondern lediglich eine Auswahl der Verwaltung des Speicherblocks, der vom Betriebssystem ausgegeben wird. Sie können dies oft tun, wenn Sie möchten.

Zugriff auf Systemressourcen

Das Betriebssystem verfügt über eine öffentliche Schnittstelle, über die Sie auf seine Funktionen zugreifen können. In DOS werden Parameter in Registern der CPU übergeben. Windows verwendet den Stack zum Übergeben von Parametern für Betriebssystemfunktionen (die Windows-API).

5
vbence