Was bedeutet "Zur Kompilierungszeit zugewiesener Speicher" wirklich?

159

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

Sagte Talha
quelle
Der Code und die Daten sind in den meisten modernen Architekturen vollständig getrennt. Während Quelldateien beide Codedaten an derselben Stelle enthalten, enthält der Bin nur Verweise auf Daten. Dies bedeutet, dass statische Daten in der Quelle nur als Referenzen aufgelöst werden.
Cholthi Paul Ttiopic

Antworten:

184

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:

int array[100];

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:

int array[] = { 1 , 2 , 3 , 4 };

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:

int a[4];
int b[] = { 1 , 2 , 3 , 4 };

int main()
{}

Ausgabebaugruppe:

a:
    .zero   16
b:
    .long   1
    .long   2
    .long   3
    .long   4
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, %eax
    popq    %rbp
    ret

Wie Sie sehen können, werden die Werte direkt in die Baugruppe eingefügt. Im Array ageneriert der Compiler eine Nullinitialisierung von 16 Byte, da der Standard vorschreibt, dass statisch gespeicherte Dinge standardmäßig auf Null initialisiert werden sollten:

8.5.9 (Initialisierer) [Hinweis]:
Jedes Objekt mit statischer Speicherdauer wird beim Programmstart auf Null initialisiert, bevor eine andere Initialisierung stattfindet. In einigen Fällen erfolgt die zusätzliche Initialisierung später.

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 .

Manu343726
quelle
2
Vielen Dank. Dies verdeutlicht viel. Der Compiler gibt also etwas aus, das "Speicher von 0xABC bis 0xXYZ für variables Array [] usw. reservieren" entspricht. und dann verwendet der Loader das, um es wirklich zuzuweisen, kurz bevor es das Programm ausführt?
Talha
1
@ TalhaSayed genau. Sehen Sie sich die Bearbeitung an, um das Beispiel
anzusehen
2
@Secko Ich habe die Dinge vereinfacht. Es ist nur eine Erwähnung, dass das Programm über den virtuellen Speicher funktioniert, aber da es sich nicht um den virtuellen Speicher handelt, habe ich das Thema nicht erweitert. Ich habe nur darauf hingewiesen, dass der Compiler dank des virtuellen Speichers zur Kompilierungszeit Annahmen über Speicheradressen treffen kann.
Manu343726
2
@Secko ja. mmm "generiert" ist ein besserer Begriff, denke ich.
Manu343726
2
"Es ist im statischen Mamory-Bereich des Prozessspeicherbereichs zugeordnet" Lesen, das einige statische Brustbereiche in meinem Prozessspeicherbereich zugewiesen hat.
Radiodef
27

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.

Ist die Speicherzuweisung nicht per Definition ein Laufzeitkonzept?

Der Speicher wird vor der Laufzeit nicht verwendet , aber unmittelbar vor dem Start der Ausführung wird seine Zuordnung vom System übernommen.

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?

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.

mah
quelle
1
Wenn ich schreibe 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.
Suraj Jain
Ihre ausführbare Datei verfügt über zwei Bereiche für statische Daten - einen für nicht initialisierte Statik und einen für initialisierte Statik. Der nicht initialisierte Bereich ist eigentlich nur eine Größenangabe; Wenn Ihr Programm ausgeführt wird, wird diese Größe verwendet, um den statischen Speicherbereich zu vergrößern, aber das Programm selbst musste nicht mehr als die Menge nicht initialisierter Daten enthalten. Für die initialisierte Statik muss Ihr Programm nicht nur die Größe (jeder) Statik enthalten, sondern auch die Größe, auf die es initialisiert wird. In Ihrem Beispiel enthält Ihr Programm also 2, 3, 5 und 5.
mah
Die Implementierung definiert, wo sie platziert wird / wie sie zugewiesen wird, aber ich bin nicht sicher, ob ich die Notwendigkeit verstehe, es zu wissen.
Mah
23

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.

char a[32];
char b;
char c;

Diese 3 Variablen werden "zur Kompilierungszeit zugewiesen". Dies bedeutet, dass der Compiler ihre Größe (die fest ist) zur Kompilierungszeit berechnet. Die Variable aist ein Offset im Speicher, beispielsweise zeigt sie auf Adresse 0, bzeigt auf Adresse 33 und cauf 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 um xBytes erhöht , sodass sich die Variable aim Beispiel an der Adresse x, b an der Adresse x+33und befindet demnächst.

fede1024
quelle
17

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

const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end

und wenn Sie es dann vim your_compiled_binwä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 ints, chars oder longs auf dem Stapel zuweisen und es in einer Schleife zuweisen, etwas in dieser Richtung

int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);

dann 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ärenint some_array[100]fügt dem Fach nur ein paar Bytes zusätzlicher Informationen hinzu, die dem System mitteilen, dass die Funktion X 100*sizeof(int)+ zusätzlichen Platz für die Buchhaltung benötigt.

Elias Van Ootegem
quelle
Vielen Dank. Noch eine Frage: Werden lokale Variablen für Funktionen während der Kompilierungszeit auf dieselbe Weise zugewiesen?
Talha sagte
@TalhaSayed: Ja, das habe ich gemeint, als ich sagte: "Informationen, die das System benötigt, um zu wissen, wie viel Speicher welche Funktion / welcher Block benötigt." Sobald Sie eine Funktion aufrufen, weist das System den erforderlichen Speicher für diese Funktion zu. Sobald die Funktion zurückkehrt, wird dieser Speicher wieder freigegeben.
Elias Van Ootegem
Zu den Kommentaren in Ihrem C-Code: Das passiert eigentlich nicht unbedingt. Beispielsweise wird die Zeichenfolge zum Zeitpunkt der Kompilierung höchstwahrscheinlich nur einmal zugewiesen. Daher wird es nie "freigegeben" (auch ich denke, dass Terminologie normalerweise nur verwendet wird, wenn Sie etwas dynamisch zuweisen), inicht "freigegeben" oder beides. Wenn ies 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ücksichtigen ioder cdie 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.
Phant0m
@ phant0m: Ich habe nie gesagt, dass die Zeichenfolge auf dem Stapel zugeordnet ist, nur der Zeiger wäre es auch, die Zeichenfolge selbst würde sich im Nur-Lese-Speicher befinden. Ich weiß, dass der mit den lokalen Variablen verknüpfte Speicher nicht im Sinne von 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 kann
Elias Van Ootegem
Ah ich sehe. In diesem Fall bedeutet mein Kommentar "Ich war durch Ihren Wortlaut verwirrt."
Phant0m
16

Auf 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 inthandelt sich um vier Bytes, hat ein Modul als einzige statische Zuordnung Folgendes:

int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;

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:

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

  2. 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).

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

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

Superkatze
quelle
13

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:

  • Ein System wird verwendet, um die virtuelle Adresse zu bestimmen, unter der das Element gespeichert wird
  • Die virtuelle Adresse wird einer physischen Adresse zugeordnet

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:

    int c;
  • Es erzeugt eine Ausgabe für den Assembler, die ihn anweist, Speicher für die Variable 'c' zu reservieren. Das könnte so aussehen:

    global _c
    section .bss
    _c: resb 4
  • 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, _cdie 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 _chinzugefü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 vonc(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 wird c.

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

Jules
quelle
9

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:

static char[1024];

Es sei denn, Sie geben natürlich einen Initialisierer an:

static char[1024] = { 1, 2, 3, 4, ... };

Daher enthält eine ausführbare Datei zusätzlich zu 'Maschinensprache' (dh CPU-Anweisungen) eine Beschreibung des erforderlichen Speicherlayouts.

Sinn-Angelegenheiten
quelle
5

Speicher kann auf viele Arten zugewiesen werden:

  • im Anwendungsheap (der gesamte Heap wird beim Start des Programms vom Betriebssystem für Ihre App zugewiesen)
  • im Betriebssystem-Heap (damit Sie immer mehr greifen können)
  • in Garbage Collector Controlled Heap (wie beide oben)
  • auf Stapel (so können Sie einen Stapelüberlauf bekommen)
  • reserviert im Code / Datensegment Ihrer Binärdatei (ausführbar)
  • an einem entfernten Ort (Datei, Netzwerk - und Sie erhalten ein Handle, keinen Zeiger auf diesen Speicher)

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.

exebook
quelle
1
Diese Antwort ist insofern verwirrend (oder verwirrt), als sie über "den Anwendungsheap", "den Betriebssystemheap" und "den GC-Heap" spricht, als ob dies alles sinnvolle Konzepte wären. Ich schließe daraus, dass Sie mit # 1 versucht haben zu sagen, dass einige Programmiersprachen (hypothetisch) ein "Heap Allocation" -Schema verwenden könnten, das Speicher aus einem Puffer fester Größe im Abschnitt .data zuweist, aber das scheint unrealistisch genug, um schädlich zu sein zum Verständnis des OP. Zu Nr. 2 und Nr. 3 ändert das Vorhandensein eines GC nichts wirklich. Und zu # 5 haben Sie die relativ VIEL wichtigere Unterscheidung zwischen .dataund weggelassen .bss.
Quuxplusone
4

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

Yves Daoust
quelle
2

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.

jmoreno
quelle
Ich mag Antworten, die die sokratische Methode verwenden, aber ich habe Sie trotzdem für die falsche Schlussfolgerung herabgestuft, dass "der Compiler Anweisungen generiert, um diesen Speicher zur Laufzeit irgendwie zuzuweisen". Sehen Sie sich die Antwort mit der höchsten Bewertung an, um zu sehen, wie ein Compiler "Speicher zuweisen" kann, ohne Laufzeitanweisungen zu generieren. (Beachten Sie, dass "Anweisungen" in einem Assembler-Kontext eine bestimmte Bedeutung haben, dh ausführbare Opcodes. Möglicherweise haben Sie das Wort umgangssprachlich verwendet, um so etwas wie "Rezept" zu bedeuten, aber in diesem Kontext wird das OP nur verwirrt. )
Quuxplusone
1
@Quuxplusone: Ich habe diese Antwort gelesen (und positiv bewertet). Und nein, meine Antwort befasst sich nicht speziell mit dem Problem der initialisierten Variablen. Selbstmodifizierender Code wird ebenfalls nicht behandelt. Diese Antwort ist zwar ausgezeichnet, hat aber nicht das angesprochen, was ich für ein wichtiges Thema halte - die Dinge in einen Kontext zu setzen. Daher meine Antwort, von der ich hoffe, dass sie dem OP (und anderen) hilft, anzuhalten und darüber nachzudenken, was los ist oder sein kann, wenn sie Probleme haben, die sie nicht verstehen.
jmoreno
@Quuxplusone: Tut mir leid, wenn ich hier falsche Anschuldigungen mache, aber ich nehme an, Sie waren einer der Leute, die auch meine Antwort gegeben haben. Wenn ja, würde es Ihnen etwas ausmachen, schrecklich darauf hinzuweisen, welcher Teil meiner Antwort der Hauptgrund dafür war, und möchten Sie auch meine Bearbeitung überprüfen? Ich weiß, dass ich ein paar Kleinigkeiten über die wahren Interna der Verwaltung des Stapelspeichers übersprungen habe, also habe ich jetzt ein bisschen hinzugefügt, dass ich meiner Antwort jetzt sowieso nicht 100% genau bin :)
Elias Van Ootegem
@jmoreno Der Punkt, den Sie zu "Kann es bedeuten, dass Speicher auf Chips, die noch nicht hergestellt wurden, für Computer, die noch nicht entworfen wurden, irgendwie reserviert ist? Nein." ist genau die falsche Bedeutung, die das Wort "Zuordnung" impliziert, was mich von Anfang an verwirrt hat. Ich mag diese Antwort, weil sie sich genau auf das Problem bezieht, auf das ich hinweisen wollte. Keine der Antworten hier hat diesen bestimmten Punkt wirklich berührt. Vielen Dank.
Talha sagte
2

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.

Ingenieur
quelle
2

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.

leeres Feld

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.

erste Instanz

Wenn die zweite Instanz dieses Programms ausgeführt wird, sieht der Speicher wie folgt aus.

zweite Instanz

Und der dritte ..

dritte Instanz

Und so weiter und so fort.

Ich hoffe, diese Visualisierung erklärt dieses Konzept gut.

user3258051
quelle
2
Wenn diese Diagramme den Unterschied zwischen statischem und dynamischem Speicher zeigen würden, wären sie meiner Meinung nach nützlicher.
Bartek Banachewicz
Dies war von mir bewusst vermieden worden, um die Dinge einfach zu halten. Mein Fokus liegt darauf, diese Funda klar und ohne große technische Unordnung zu erklären. Soweit dies für statische Variablen gedacht ist. Dieser Punkt wurde durch frühere Antworten gut festgelegt. Also habe ich dies übersprungen.
user3258051
1
Eh, dieses Konzept ist nicht besonders kompliziert, deshalb verstehe ich nicht, warum es einfacher ist, als es sein muss, aber da es nur als kostenlose Antwort gedacht ist, ok.
Bartek Banachewicz
1

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

user6882413
quelle