Ich bereite einige Schulungsmaterialien in C vor und möchte, dass meine Beispiele zum typischen Stapelmodell passen.
In welche Richtung wächst ein C-Stack unter Linux, Windows, Mac OSX (PPC und x86), Solaris und den neuesten Unixen?
Ich bereite einige Schulungsmaterialien in C vor und möchte, dass meine Beispiele zum typischen Stapelmodell passen.
In welche Richtung wächst ein C-Stack unter Linux, Windows, Mac OSX (PPC und x86), Solaris und den neuesten Unixen?
Antworten:
Das Stapelwachstum hängt normalerweise nicht vom Betriebssystem selbst ab, sondern vom Prozessor, auf dem es ausgeführt wird. Solaris läuft beispielsweise unter x86 und SPARC. Mac OSX (wie bereits erwähnt) läuft unter PPC und x86. Linux läuft auf allem, von meinem großen Honkin 'System z bei der Arbeit bis zu einer mickrigen kleinen Armbanduhr .
Wenn die CPU eine Auswahlmöglichkeit bietet, gibt die vom Betriebssystem verwendete ABI / Aufrufkonvention an, welche Auswahl Sie treffen müssen, wenn Ihr Code den Code aller anderen aufrufen soll.
Die Prozessoren und ihre Richtung sind:
Der 1802 zeigte mein Alter in den letzten paar Jahren und war der Chip, mit dem die frühen Shuttles gesteuert wurden (ich vermute, dass die Türen geöffnet waren, basierend auf der Rechenleistung, die er hatte :-) und meinem zweiten Computer, dem COMX-35 ( nach meinem ZX80 ).
PDP11-Details von hier , 8051-Details von hier .
Die SPARC-Architektur verwendet ein Schiebefenster-Registermodell. Zu den architektonisch sichtbaren Details gehört auch ein kreisförmiger Puffer von Registerfenstern, die gültig sind und intern zwischengespeichert werden, mit Traps, wenn diese über- oder unterlaufen. Siehe hier für Details. Wie im SPARCv8-Handbuch erläutert , entsprechen die Anweisungen SAVE und RESTORE den Anweisungen ADD plus Register-Fenster-Rotation. Die Verwendung einer positiven Konstante anstelle des üblichen Negativs würde einen nach oben wachsenden Stapel ergeben.
Die oben erwähnte SCRT-Technik ist eine andere - die 1802 verwendete einige oder sechzehn 16-Bit-Register für SCRT (Standard-Call- und Return-Technik). Einer war der Programmzähler, man konnte jedes Register als PC mit der SEP Rn
Anweisung verwenden. Einer war der Stapelzeiger und zwei waren so eingestellt, dass sie immer auf die SCRT-Code-Adresse zeigten, einer für den Aufruf, einer für die Rückgabe. Kein Register wurde auf besondere Weise behandelt. Beachten Sie, dass diese Details aus dem Speicher stammen und möglicherweise nicht vollständig korrekt sind.
Wenn beispielsweise R3 der PC war, R4 die SCRT-Anrufadresse war, R5 die SCRT-Rücksprungadresse war und R2 der "Stapel" war (Anführungszeichen, wie sie in der Software implementiert sind), SEP R4
würde R4 als PC festgelegt und die Ausführung des SCRT gestartet Rufnummer.
Es würde dann R3 auf dem R2 "Stack" speichern (ich denke, R6 wurde für die temporäre Speicherung verwendet), es nach oben oder unten anpassen, die zwei Bytes nach R3 abrufen, sie in R3 laden , dann tun SEP R3
und an der neuen Adresse laufen.
Um zurückzukehren, würde SEP R5
es die alte Adresse vom R2-Stapel abziehen, zwei hinzufügen (um die Adressbytes des Aufrufs zu überspringen), sie in R3 laden und SEP R3
den vorherigen Code ausführen.
Nach all dem stapelbasierten Code 6502/6809 / z80 ist es anfangs sehr schwer, den Kopf herumzureißen, aber dennoch elegant, wenn man den Kopf gegen die Wand schlägt. Eines der meistverkauften Merkmale des Chips war auch eine vollständige Suite von 16 16-Bit-Registern, obwohl Sie sofort 7 davon verloren haben (5 für SCRT, zwei für DMA und Interrupts aus dem Speicher). Ahh, der Triumph des Marketings über die Realität :-)
System z ist tatsächlich ziemlich ähnlich und verwendet seine R14- und R15-Register für Anruf / Rückgabe.
In C ++ (anpassbar an C) stack.cc :
static int
find_stack_direction ()
{
static char *addr = 0;
auto char dummy;
if (addr == 0)
{
addr = &dummy;
return find_stack_direction ();
}
else
{
return ((&dummy > addr) ? 1 : -1);
}
}
static
dafür zu verwenden. Stattdessen können Sie die Adresse als Argument an einen rekursiven Aufruf übergeben.
static
, wenn Sie dies mehr als einmal aufrufen, können die nachfolgenden Anrufe scheitern ...
Der Vorteil des Herabwachsens besteht darin, dass sich der Stapel in älteren Systemen normalerweise oben im Speicher befand. Programme füllten normalerweise den Speicher von unten, so dass diese Art der Speicherverwaltung die Notwendigkeit minimierte, den unteren Rand des Stapels an einer sinnvollen Stelle zu messen und zu platzieren.
In MIPS und vielen modernen RISC - Architekturen (wie PowerPC RISC-V, SPARC ...) gibt es keine push
und pop
Anweisungen. Diese Operationen werden explizit ausgeführt, indem der Stapelzeiger manuell angepasst und dann der Wert relativ zum angepassten Zeiger geladen / gespeichert wird. Alle Register (mit Ausnahme des Nullregisters) sind universell einsetzbar, so dass theoretisch jedes Register ein Stapelzeiger sein kann und der Stapel in jede vom Programmierer gewünschte Richtung wachsen kann
Das heißt, der Stapel wächst normalerweise auf den meisten Architekturen nach unten, wahrscheinlich um den Fall zu vermeiden, dass die Stapel- und Programmdaten oder Heap-Daten wachsen und miteinander kollidieren. Es gibt auch die großen Adressierungsgründe, die in der Antwort von sh-s erwähnt wurden . Einige Beispiele: MIPS-ABIs wachsen nach unten und verwenden $29
(AKA $sp
) als Stapelzeiger, RISC-V ABI wächst ebenfalls nach unten und verwendet x2 als Stapelzeiger
In Intel 8051 wächst der Stapel, wahrscheinlich weil der Speicherplatz so klein ist (128 Byte in der Originalversion), dass kein Heap vorhanden ist und Sie den Stapel nicht darauf legen müssen, damit er vom wachsenden Heap getrennt wird vom Boden
Weitere Informationen zur Stapelverwendung in verschiedenen Architekturen finden Sie unter https://en.wikipedia.org/wiki/Calling_convention
Siehe auch
Nur eine kleine Ergänzung zu den anderen Antworten, die, soweit ich sehen kann, diesen Punkt nicht berührt haben:
Wenn der Stapel nach unten wächst, haben alle Adressen innerhalb des Stapels einen positiven Versatz relativ zum Stapelzeiger. Negative Offsets sind nicht erforderlich, da sie nur auf nicht genutzten Stapelspeicherplatz verweisen. Dies vereinfacht den Zugriff auf Stapelpositionen, wenn der Prozessor die Stapelzeiger-relative Adressierung unterstützt.
Viele Prozessoren verfügen über Anweisungen, die Zugriffe mit einem Nur-Positiv-Offset relativ zu einem Register ermöglichen. Dazu gehören viele moderne Architekturen sowie einige alte. Beispielsweise bietet der ARM Thumb ABI stapelzeigerbezogene Zugriffe mit einem positiven Offset, der in einem einzelnen 16-Bit-Befehlswort codiert ist.
Wenn der Stapel nach oben wachsen würde, wären alle nützlichen Offsets relativ zum Stapelzeiger negativ, was weniger intuitiv und weniger bequem ist. Es steht auch im Widerspruch zu anderen Anwendungen der registerbezogenen Adressierung, beispielsweise für den Zugriff auf Felder einer Struktur.
Auf den meisten Systemen wächst der Stapel ab, und mein Artikel unter https://gist.github.com/cpq/8598782 erklärt, warum er abfällt. Es ist ganz einfach: Wie können zwei wachsende Speicherblöcke (Heap und Stack) in einem festen Speicherblock angeordnet werden? Die beste Lösung ist, sie an die entgegengesetzten Enden zu legen und aufeinander zu wachsen zu lassen.
Es wächst nach unten, weil der dem Programm zugewiesene Speicher die "permanenten Daten" enthält, dh den Code für das Programm selbst unten und dann den Heap in der Mitte. Sie benötigen einen weiteren festen Punkt, von dem aus Sie auf den Stapel verweisen können, sodass Sie oben bleiben. Dies bedeutet, dass der Stapel nach unten wächst, bis er möglicherweise an Objekte auf dem Heap angrenzt.