In Programmiersprachen wie C und C ++ wird häufig auf statische und dynamische Speicherzuweisung verwiesen. Ich verstehe das Konzept, aber der Satz "Der gesamte Speicher wurde während der Kompilierungszeit zugewiesen (reserviert)" verwirrt mich immer.
Nach meinem Verständnis konvertiert die Kompilierung C / C ++ - Code auf hoher Ebene in die Maschinensprache und gibt eine ausführbare Datei aus. Wie wird Speicher in einer kompilierten Datei "zugewiesen"? Ist nicht immer Speicher im RAM mit all dem virtuellen Speicherverwaltungsmaterial zugeordnet?
Ist die Speicherzuweisung nicht per Definition ein Laufzeitkonzept?
Wenn ich in meinem C / C ++ - Code eine statisch zugewiesene Variable mit 1 KB erstelle, erhöht dies die Größe der ausführbaren Datei um denselben Betrag?
Dies ist eine der Seiten, auf denen der Ausdruck unter der Überschrift "Statische Zuordnung" verwendet wird.
Zurück zu den Grundlagen: Speicherzuweisung, ein Spaziergang durch die Geschichte
quelle
Antworten:
Zur Kompilierungszeit zugewiesener Speicher bedeutet, dass der Compiler zur Kompilierungszeit aufgelöst wird, wenn bestimmte Dinge in der Prozessspeicherzuordnung zugewiesen werden.
Betrachten Sie beispielsweise ein globales Array:
Der Compiler kennt zur Kompilierungszeit die Größe des Arrays und die Größe eines
int
, sodass er zur Kompilierungszeit die gesamte Größe des Arrays kennt. Außerdem hat eine globale Variable standardmäßig eine statische Speicherdauer: Sie wird im statischen Speicherbereich des Prozessspeicherbereichs (Abschnitt .data / .bss) zugewiesen. Angesichts dieser Informationen entscheidet der Compiler während der Kompilierung, in welcher Adresse dieses statischen Speicherbereichs sich das Array befindet .Natürlich sind diese Speicheradressen virtuelle Adressen. Das Programm geht davon aus, dass es über einen eigenen Speicherplatz verfügt (z. B. von 0x00000000 bis 0xFFFFFFFF). Aus diesem Grund könnte der Compiler Annahmen wie "Okay, das Array befindet sich unter der Adresse 0x00A33211" treffen. Zur Laufzeit werden diese Adressen von der MMU und dem Betriebssystem in echte / Hardwareadressen übersetzt.
Wertinitialisierte statische Speicher Dinge sind ein bisschen anders. Beispielsweise:
In unserem ersten Beispiel hat der Compiler nur entschieden, wo das Array zugewiesen wird, und diese Informationen in der ausführbaren Datei gespeichert.
Bei wertinitialisierten Dingen fügt der Compiler auch den Anfangswert des Arrays in die ausführbare Datei ein und fügt Code hinzu, der dem Programmlader mitteilt, dass das Array nach der Arrayzuweisung beim Programmstart mit diesen Werten gefüllt werden soll.
Hier sind zwei Beispiele für die vom Compiler generierte Assembly (GCC4.8.1 mit x86-Ziel):
C ++ - Code:
Ausgabebaugruppe:
Wie Sie sehen können, werden die Werte direkt in die Baugruppe eingefügt. Im Array
a
generiert der Compiler eine Nullinitialisierung von 16 Byte, da der Standard vorschreibt, dass statisch gespeicherte Dinge standardmäßig auf Null initialisiert werden sollten:Ich empfehle immer, den Code zu zerlegen, um zu sehen, was der Compiler wirklich mit dem C ++ - Code macht. Dies gilt von Speicherklassen / Dauer (wie diese Frage) bis zu erweiterten Compileroptimierungen. Sie könnten Ihren Compiler anweisen, die Assembly zu generieren, aber es gibt wunderbare Tools, um dies im Internet auf freundliche Weise zu tun. Mein Favorit ist GCC Explorer .
quelle
Der zur Kompilierungszeit zugewiesene Speicher bedeutet lediglich, dass zur Laufzeit keine weitere Zuweisung erfolgt - keine Aufrufe von malloc, new oder anderen dynamischen Zuweisungsmethoden. Sie haben eine feste Speicherauslastung, auch wenn Sie nicht immer den gesamten Speicher benötigen.
Der Speicher wird vor der Laufzeit nicht verwendet , aber unmittelbar vor dem Start der Ausführung wird seine Zuordnung vom System übernommen.
Durch einfaches Deklarieren der statischen Aufladung wird die Größe Ihrer ausführbaren Datei nicht um mehr als einige Bytes erhöht. Wenn Sie es mit einem Anfangswert ungleich Null deklarieren, wird dies (um diesen Anfangswert zu halten). Vielmehr addiert der Linker diesen Betrag von 1 KB einfach zu dem Speicherbedarf, den der Systemlader unmittelbar vor der Ausführung für Sie erstellt.
quelle
static int i[4] = {2 , 3 , 5 ,5 }
, erhöht sich die Größe der ausführbaren Datei um 16 Byte. Sie sagten: "Durch einfaches Deklarieren der statischen Aufladung wird die Größe Ihrer ausführbaren Datei nicht um mehr als einige Bytes erhöht. Wenn Sie sie mit einem Anfangswert ungleich Null deklarieren, bedeutet dies:" Wenn Sie sie mit einem Anfangswert deklarieren, bedeutet dies, was dies bedeutet.In der Kompilierungszeit zugewiesener Speicher bedeutet, dass beim Laden des Programms ein Teil des Speichers sofort zugewiesen wird und die Größe und (relative) Position dieser Zuordnung zur Kompilierungszeit bestimmt wird.
Diese 3 Variablen werden "zur Kompilierungszeit zugewiesen". Dies bedeutet, dass der Compiler ihre Größe (die fest ist) zur Kompilierungszeit berechnet. Die Variable
a
ist ein Offset im Speicher, beispielsweise zeigt sie auf Adresse 0,b
zeigt auf Adresse 33 undc
auf 34 (vorausgesetzt, keine Ausrichtungsoptimierung). Wenn Sie also 1 KB statische Daten zuweisen, wird Ihr Code nicht vergrößert , da nur ein Versatz darin geändert wird. Der tatsächliche Speicherplatz wird beim Laden zugewiesen .Die reale Speicherzuweisung erfolgt immer zur Laufzeit, da der Kernel den Überblick behalten und seine internen Datenstrukturen aktualisieren muss (wie viel Speicher für jeden Prozess, jede Seite usw. zugewiesen ist). Der Unterschied besteht darin, dass der Compiler bereits die Größe der Daten kennt, die Sie verwenden möchten, und diese werden zugewiesen, sobald Ihr Programm ausgeführt wird.
Denken Sie auch daran, dass es sich um relative Adressen handelt . Die reale Adresse, an der sich die Variable befindet, ist unterschiedlich. Beim Laden reserviert der Kernel etwas Speicher für den Prozess, beispielsweise an der Adresse
x
, und alle in der ausführbaren Datei enthaltenen fest codierten Adressen werden umx
Bytes erhöht , sodass sich die Variablea
im Beispiel an der Adressex
, b an der Adressex+33
und befindet demnächst.quelle
Das Hinzufügen von Variablen auf dem Stapel, die N Bytes belegen, erhöht die Bin-Größe (notwendigerweise) nicht um N Bytes. Tatsächlich werden die meiste Zeit nur einige Bytes hinzugefügt.
Lasst uns mit einem Beispiel dafür , wie das Hinzufügen ein 1000 Zeichen , um Ihren Code beginnen wird der Behälter der Größe in linearer Weise zu erhöhen.
Wenn 1k eine Zeichenfolge mit tausend Zeichen ist, wird dies wie folgt deklariert
und wenn Sie es dann
vim your_compiled_bin
wären, könnten Sie diese Zeichenfolge tatsächlich irgendwo im Papierkorb sehen. In diesem Fall ja: Die ausführbare Datei ist 1 KB größer, da sie die Zeichenfolge vollständig enthält.Wenn Sie jedoch ein Array von
int
s,char
s oderlong
s auf dem Stapel zuweisen und es in einer Schleife zuweisen, etwas in dieser Richtungdann nein: es wird den Behälter nicht vergrößern ... durch
1000*sizeof(int)
Zuweisung zur Kompilierungszeit bedeutet, was Sie jetzt verstanden haben, bedeutet dies (basierend auf Ihren Kommentaren): Der kompilierte Behälter enthält Informationen, die das System benötigt, um zu wissen, wie viel Speicher vorhanden ist Welche Funktion / welcher Block benötigt, wenn er ausgeführt wird, sowie Informationen zur Stapelgröße, die Ihre Anwendung benötigt. Das ist es, was das System zuweist, wenn es Ihren Bin ausführt und Ihr Programm zu einem Prozess wird (nun, das Ausführen Ihres Bin ist der Prozess, der ... nun, Sie verstehen, was ich sage).
Natürlich male ich hier nicht das ganze Bild: Der Behälter enthält Informationen darüber, wie groß ein Stapel ist, den der Behälter tatsächlich benötigt. Basierend auf diesen Informationen (unter anderem) reserviert das System einen Speicherblock, den so genannten Stack, über den das Programm frei regieren kann. Der Stapelspeicher wird vom System weiterhin zugewiesen, wenn der Prozess (das Ergebnis der Ausführung Ihres Bin) gestartet wird. Der Prozess verwaltet dann den Stapelspeicher für Sie. Wenn eine Funktion oder Schleife (ein beliebiger Blocktyp) aufgerufen / ausgeführt wird, werden die für diesen Block lokalen Variablen in den Stapel verschoben und entfernt (der Stapelspeicher wird sozusagen "freigegeben" ), um von anderen verwendet zu werden Funktionen / Blöcke. Also erklären
int some_array[100]
fügt dem Fach nur ein paar Bytes zusätzlicher Informationen hinzu, die dem System mitteilen, dass die Funktion X100*sizeof(int)
+ zusätzlichen Platz für die Buchhaltung benötigt.quelle
i
nicht "freigegeben" oder beides. Wenni
es sich im Speicher befinden würde, würde es einfach auf den Stapel verschoben, etwas, das nicht im eigentlichen Sinne des Wortes freigegeben wird, ohne dies zu berücksichtigeni
oderc
die ganze Zeit in Registern gespeichert zu werden. Natürlich hängt das alles vom Compiler ab, was bedeutet, dass es nicht so schwarz und weiß ist.free()
Aufrufen freigegeben wird , aber der von ihnen verwendete Stapelspeicher kann von anderen Funktionen verwendet werden, sobald die von mir aufgeführte Funktion zurückgegeben wird. Ich habe den Code entfernt, da er für einige verwirrend sein kannAuf vielen Plattformen werden alle globalen oder statischen Zuordnungen in jedem Modul vom Compiler in drei oder weniger konsolidierte Zuordnungen konsolidiert (eine für nicht initialisierte Daten (häufig als "bss" bezeichnet), eine für initialisierte beschreibbare Daten (häufig als "Daten" bezeichnet). ) und eine für konstante Daten ("const")) und alle globalen oder statischen Zuordnungen jedes Typs innerhalb eines Programms werden vom Linker zu einer globalen Zuordnung für jeden Typ konsolidiert. Angenommen, es
int
handelt sich um vier Bytes, hat ein Modul als einzige statische Zuordnung Folgendes:es würde dem Linker mitteilen, dass er 208 Bytes für bss, 16 Bytes für "data" und 28 Bytes für "const" benötigt. Ferner würde jeder Verweis auf eine Variable durch einen Flächenwähler und einen Versatz ersetzt, so dass a, b, c, d und e durch bss + 0, const + 0, bss + 4, const + 24, Daten ersetzt würden +0 bzw. bss + 204.
Wenn ein Programm verknüpft ist, werden alle BSS-Bereiche aller Module miteinander verknüpft. ebenso die Daten- und Konstantenbereiche. Für jedes Modul wird die Adresse aller bss-relativen Variablen um die Größe der bss-Bereiche aller vorhergehenden Module erhöht (ebenfalls mit Daten und const). Wenn der Linker fertig ist, hat jedes Programm eine BSS-Zuordnung, eine Datenzuweisung und eine Konstantenzuweisung.
Wenn ein Programm geladen wird, geschieht im Allgemeinen je nach Plattform eines von vier Dingen:
Die ausführbare Datei gibt an, wie viele Bytes für jede Art von Daten benötigt werden und - für den initialisierten Datenbereich, in dem sich der ursprüngliche Inhalt befindet. Es enthält auch eine Liste aller Anweisungen, die eine bss-, daten- oder const-relative Adresse verwenden. Das Betriebssystem oder der Lader weist jedem Bereich die entsprechende Menge an Speicherplatz zu und fügt dann jedem Befehl, der ihn benötigt, die Startadresse dieses Bereichs hinzu.
Das Betriebssystem weist einen Speicherblock für alle drei Arten von Daten zu und gibt der Anwendung einen Zeiger auf diesen Speicherblock. Jeder Code, der statische oder globale Daten verwendet, dereferenziert ihn relativ zu diesem Zeiger (in vielen Fällen wird der Zeiger für die Lebensdauer einer Anwendung in einem Register gespeichert).
Das Betriebssystem weist der Anwendung zunächst keinen Speicher zu, außer dem, was seinen Binärcode enthält. Als Erstes fordert die Anwendung jedoch vom Betriebssystem eine geeignete Zuordnung an, die es für immer in einem Register speichert.
Das Betriebssystem weist der Anwendung zunächst keinen Speicherplatz zu, die Anwendung fordert jedoch beim Start eine geeignete Zuordnung an (wie oben). Die Anwendung enthält eine Liste von Anweisungen mit Adressen, die aktualisiert werden müssen, um anzuzeigen, wo Speicher zugewiesen wurde (wie beim ersten Stil). Anstatt die Anwendung vom OS Loader patchen zu lassen, enthält die Anwendung genügend Code, um sich selbst zu patchen .
Alle vier Ansätze haben Vor- und Nachteile. In jedem Fall konsolidiert der Compiler jedoch eine beliebige Anzahl statischer Variablen in einer festen kleinen Anzahl von Speicheranforderungen, und der Linker konsolidiert alle diese in einer kleinen Anzahl konsolidierter Zuordnungen. Obwohl eine Anwendung einen Teil des Arbeitsspeichers vom Betriebssystem oder Loader empfangen muss, sind der Compiler und der Linker dafür verantwortlich, einzelne Teile dieses großen Teils allen einzelnen Variablen zuzuweisen, die ihn benötigen.
quelle
Der Kern Ihrer Frage lautet: "Wie wird Speicher in einer kompilierten Datei" zugewiesen "? Wird Speicher nicht immer im RAM mit allen Verwaltungsaufgaben für den virtuellen Speicher zugewiesen? Ist die Speicherzuweisung per Definition kein Laufzeitkonzept?"
Ich denke, das Problem ist, dass es zwei verschiedene Konzepte bei der Speicherzuweisung gibt. Grundsätzlich ist die Speicherzuweisung der Prozess, bei dem wir sagen, dass "dieses Datenelement in diesem bestimmten Speicherblock gespeichert ist". In einem modernen Computersystem umfasst dies einen zweistufigen Prozess:
Der letztere Prozess ist reine Laufzeit, der erstere kann jedoch zur Kompilierungszeit ausgeführt werden, wenn die Daten eine bekannte Größe haben und eine feste Anzahl von ihnen erforderlich ist. Hier ist im Grunde, wie es funktioniert:
Der Compiler sieht eine Quelldatei mit einer Zeile, die ungefähr so aussieht:
Es erzeugt eine Ausgabe für den Assembler, die ihn anweist, Speicher für die Variable 'c' zu reservieren. Das könnte so aussehen:
Wenn der Assembler ausgeführt wird, behält er einen Zähler bei, der die Offsets jedes Elements vom Beginn eines Speichersegments (oder -abschnitts) an verfolgt. Dies ist wie die Teile einer sehr großen 'Struktur', die alles in der gesamten Datei enthält, der zu diesem Zeitpunkt kein tatsächlicher Speicher zugewiesen ist und der sich überall befinden könnte. Es notiert in einer Tabelle,
_c
die einen bestimmten Versatz hat (z. B. 510 Bytes ab dem Beginn des Segments), und erhöht dann seinen Zähler um 4, sodass die nächste solche Variable bei (z. B.) 514 Bytes liegt. Für jeden Code, der die Adresse von benötigt_c
, wird nur 510 in die Ausgabedatei eingefügt und ein Hinweis_c
hinzugefügt, dass die Ausgabe die Adresse des Segments benötigt, das später hinzugefügt werden soll.Der Linker nimmt alle Ausgabedateien des Assemblers und untersucht sie. Es bestimmt eine Adresse für jedes Segment, damit sie sich nicht überlappen, und fügt die erforderlichen Offsets hinzu, damit sich die Anweisungen weiterhin auf die richtigen Datenelemente beziehen. Im Falle eines nicht initialisierten Gedächtnisses wie dem von
c
(Dem Assembler wurde mitgeteilt, dass der Speicher nicht initialisiert werden würde, da der Compiler ihn in das Segment '.bss' gestellt hat, das für den nicht initialisierten Speicher reserviert ist.) In seiner Ausgabe befindet sich ein Header-Feld, das dem Betriebssystem mitteilt wie viel muss reserviert werden. Es kann verschoben werden (und ist es normalerweise), ist jedoch normalerweise so ausgelegt, dass es effizienter an einer bestimmten Speicheradresse geladen werden kann, und das Betriebssystem versucht, es an dieser Adresse zu laden. Zu diesem Zeitpunkt haben wir eine ziemlich gute Vorstellung davon, welche virtuelle Adresse von verwendet wirdc
.Die physikalische Adresse wird erst ermittelt, wenn das Programm ausgeführt wird. Aus Sicht des Programmierers ist die physische Adresse jedoch eigentlich irrelevant - wir werden nie herausfinden, was es ist, da das Betriebssystem normalerweise niemandem davon erzählt, dass es sich häufig ändern kann (auch während das Programm ausgeführt wird), und a Hauptzweck des Betriebssystems ist es, dies sowieso weg zu abstrahieren.
quelle
Eine ausführbare Datei beschreibt, welcher Speicherplatz für statische Variablen reserviert werden soll. Diese Zuordnung erfolgt durch das System, wenn Sie die ausführbare Datei ausführen. Ihre statische Variable von 1 KB erhöht also nicht die Größe der ausführbaren Datei mit 1 KB:
Es sei denn, Sie geben natürlich einen Initialisierer an:
Daher enthält eine ausführbare Datei zusätzlich zu 'Maschinensprache' (dh CPU-Anweisungen) eine Beschreibung des erforderlichen Speicherlayouts.
quelle
Speicher kann auf viele Arten zugewiesen werden:
Ihre Frage ist nun, was "Speicher zur Kompilierungszeit zugewiesen" ist. Auf jeden Fall handelt es sich nur um ein falsch formuliertes Sprichwort, das sich entweder auf die binäre Segmentzuweisung oder die Stapelzuweisung oder in einigen Fällen sogar auf eine Heapzuweisung beziehen soll. In diesem Fall wird die Zuweisung jedoch durch unsichtbaren Konstruktoraufruf vor den Augen des Programmierers verborgen. Oder wahrscheinlich die Person, die das gesagt hat, wollte nur sagen, dass der Speicher nicht auf dem Heap zugeordnet ist, wusste aber nichts über Stapel- oder Segmentzuordnungen (oder wollte nicht auf diese Art von Details eingehen).
In den meisten Fällen möchte die Person jedoch nur sagen, dass die zugewiesene Speichermenge zur Kompilierungszeit bekannt ist .
Die Binärgröße ändert sich nur, wenn der Speicher im Code- oder Datensegment Ihrer App reserviert ist.
quelle
.data
und weggelassen.bss
.Du hast recht. Der Speicher wird beim Laden tatsächlich zugewiesen (ausgelagert), dh wenn die ausführbare Datei in den (virtuellen) Speicher gebracht wird. In diesem Moment kann auch der Speicher initialisiert werden. Der Compiler erstellt lediglich eine Speicherzuordnung. [Stack- und Heap-Speicherplätze werden übrigens auch beim Laden zugewiesen!]
quelle
Ich denke, Sie müssen ein bisschen zurücktreten. Bei der Kompilierung zugewiesener Speicher .... Was kann das bedeuten? Kann dies bedeuten, dass Speicher auf Chips, die noch nicht hergestellt wurden, für Computer, die noch nicht entwickelt wurden, irgendwie reserviert wird? Nein, Zeitreisen, keine Compiler, die das Universum manipulieren können.
Es muss also bedeuten, dass der Compiler Anweisungen generiert, um diesen Speicher zur Laufzeit irgendwie zuzuweisen. Wenn Sie es jedoch aus dem richtigen Winkel betrachten, generiert der Compiler alle Anweisungen. Was kann also der Unterschied sein? Der Unterschied besteht darin, dass der Compiler entscheidet und Ihr Code zur Laufzeit seine Entscheidungen nicht ändern oder modifizieren kann. Wenn entschieden wurde, dass zur Kompilierungszeit und zur Laufzeit 50 Byte benötigt werden, können Sie nicht entscheiden, 60 zuzuweisen - diese Entscheidung wurde bereits getroffen.
quelle
Wenn Sie die Assembly-Programmierung lernen, werden Sie feststellen, dass Sie Segmente für die Daten, den Stapel und den Code usw. herausarbeiten müssen. Im Datensegment befinden sich Ihre Zeichenfolgen und Zahlen. Im Codesegment lebt Ihr Code. Diese Segmente sind in das ausführbare Programm integriert. Natürlich ist auch die Stapelgröße wichtig ... Sie möchten keinen Stapelüberlauf !
Wenn Ihr Datensegment also 500 Byte umfasst, hat Ihr Programm einen Bereich von 500 Byte. Wenn Sie das Datensegment auf 1500 Byte ändern, ist das Programm 1000 Byte größer. Die Daten werden zum eigentlichen Programm zusammengestellt.
Dies ist der Fall, wenn Sie übergeordnete Sprachen kompilieren. Der eigentliche Datenbereich wird zugewiesen, wenn er zu einem ausführbaren Programm kompiliert wird, wodurch das Programm vergrößert wird. Das Programm kann auch im laufenden Betrieb Speicher anfordern, und dies ist ein dynamischer Speicher. Sie können Speicher aus dem RAM anfordern und die CPU gibt ihn Ihnen zur Verwendung, Sie können ihn loslassen und Ihr Garbage Collector gibt ihn an die CPU zurück. Bei Bedarf kann es sogar von einem guten Speichermanager auf eine Festplatte übertragen werden. Diese Funktionen bieten Ihnen Hochsprachen.
quelle
Ich möchte diese Konzepte anhand weniger Diagramme erläutern.
Dies ist wahr, dass Speicher zur Kompilierungszeit sicher nicht zugewiesen werden kann. Aber was passiert dann tatsächlich zur Kompilierungszeit?
Hier kommt die Erklärung. Angenommen, ein Programm hat vier Variablen x, y, z und k. Jetzt wird zur Kompilierungszeit einfach eine Speicherzuordnung erstellt, in der die Position dieser Variablen in Bezug zueinander ermittelt wird. Dieses Diagramm wird es besser veranschaulichen.
Stellen Sie sich nun vor, kein Programm läuft im Speicher. Dies zeige ich durch ein großes leeres Rechteck.
Als nächstes wird die erste Instanz dieses Programms ausgeführt. Sie können es wie folgt visualisieren. Dies ist die Zeit, zu der tatsächlich Speicher zugewiesen wird.
Wenn die zweite Instanz dieses Programms ausgeführt wird, sieht der Speicher wie folgt aus.
Und der dritte ..
Und so weiter und so fort.
Ich hoffe, diese Visualisierung erklärt dieses Konzept gut.
quelle
Die akzeptierte Antwort enthält eine sehr schöne Erklärung. Nur für den Fall, dass ich den Link posten werde, den ich nützlich gefunden habe. https://www.tenouk.com/ModuleW.html
quelle