Wie Speicherzeiger Pufferüberläufe in C verursachen

2

Um wirklich zu verstehen, warum sich Zeiger so verhalten, wie sie es tun, müssen Sie aufhören, in abstrakten Variablen zu denken, und mit der Visualisierung der Hardware beginnen. Es ist ein einfaches Modell, aber es ist die Grundlage für alles, von Segfaults bis hin zu Sicherheitslücken. Wenn Sie sich mit den Grundlagen von Bits, Bytes und Wörtern nicht auskennen, sollten Sie hier wahrscheinlich eine Pause einlegen und sich zuerst damit befassen. Der Rest dieser Erklärung geht davon aus, dass Sie den Unterschied zwischen einem Byte und einem Bit kennen.

Ihr Computer verfügt über Speicher oder RAM (Random Access Memory). Möglicherweise sind 16, 32 oder 64 Gigabyte davon installiert. Dabei handelt es sich nicht nur um Speicher; Es ist der Arbeitsbereich. Es enthält den Code, den Ihre CPU gerade ausführt, und die Daten, die diese Programme genau in dieser Sekunde bearbeiten. Stellen Sie sich RAM als eine riesige, lineare Byte-Anordnung vor.

Jedes einzelne Byte in diesem Array hat eine Adresse. Das erste Byte ist die Adresse 0. Das nächste ist 1. Dann 2, 3 und so weiter. Diese Adressen funktionieren genau wie Indizes in einem Standard-Programmierarray. Da die CPU sofort zu jeder Adresse springen kann, wird sie „Random Access Memory“ genannt. Es muss nicht sequentiell gelesen werden. Es kann sich alles schnappen, was es braucht, wann immer es es braucht. Wenn der Compiler einen größeren Datentyp speichern muss, greift er einfach auf einen Block zusammenhängender Bytes zurück. Eine Standard-Gleitkommazahl beispielsweise nimmt normalerweise 4 zusammenhängende Bytes ein.

Schauen wir uns eine globale Deklaration in C an:

„c
float f;

Für Sie ist dies eine Variable mit dem Namen „f“, die einen Gleitkommawert enthält. Für den Computer ist dies ein Befehl, 4 bestimmte Bytes im Speicherarray zu reservieren. Nehmen wir an, der Compiler weist diese Bytes dem Speicherort 248.440 zu.

Wenn Sie schreiben:

„c
f = 3,14;

Der Compiler denkt nicht an „Variable f aktualisieren“. Es denkt: „Lade den Wert 3,14 in die Speicheradresse 248.440.“ Die Abstraktion ist dünn. Unter der Haube dreht sich alles um Adressen und die Werte, die an diesen Adressen sitzen.

Diese mechanische Natur des Gedächtnisses führt zu einigen unangenehmen Nebenwirkungen. Betrachten Sie dieses C-Code-Snippet:

„c
int i, s[4], t[4], u=0;
für (i=0; i<=4; i++) { s[i] = i; t[i] = i; } printf("s:t\n"); für (i=0; i<=4; i++) printf("%d:%d\n", s[i], t[i]); printf("u = %d\n", u); „

Sie können erwarten, dass „s“ und „t“ „0:0“, „1:1“, „2:2“, „3:3“ enthalten. Und „u“ sollte 0 bleiben. Stattdessen sieht die Ausgabe so aus:


s:t
1:5
2:2
3:3
4:4
5:5
u = 5

Warte. Was ist mit „t[0]“ passiert? Und warum ist „u“ jetzt 5?

Das Problem ist ein klassischer Pufferüberlauf. Schauen Sie sich die Schleifenbedingung genau an: „i<=4“. Das Array „t“ wird als „t[4]“ deklariert, was bedeutet, dass es die Indizes 0, 1, 2 und 3 hat. Wenn „i“ 4 erreicht, schreibt der Code in „t[4]“. Dieser Index existiert nicht innerhalb des zugewiesenen Speicherplatzes für „t“. Es schreibt ein Element über das Ende des Arrays hinaus.

Da der Speicher zusammenhängend ist, platziert der Computer „s“, „t“ und „u“ direkt nebeneinander im Heap oder Stapel.

Wenn Sie über das Ende eines Arrays hinaus schreiben, kommt es nicht sofort zum Absturz. Sie überschreiben alles, was sich im Speichersteckplatz direkt daneben befindet.

In diesem speziellen Fall überschreibt das Schreiben in „t[4]“ den Speicherort, der „u“ enthält (der ursprünglich 0 war). Aber warum bist du?

Schreiben Sie an „s[4]“. Dieser Index existiert nicht. Aber dem Computer ist das egal. Es prüft nicht, ob Sie sich selbst auf die Füße treten. Es wird lediglich die Adresse berechnet. Und diese Adresse landet zufällig auf „t[0]“.

Du denkst, du schreibst an „s“. Du schreibst tatsächlich an „t“. Das System führt den Befehl aus, ohne zu blinken. Die Logik ist fehlerhaft. Das Programm beschädigt Daten. Es ist still, bis es nicht mehr da ist.

Versuchen Sie jetzt etwas Schlimmeres.

s[1000000] = 5;

Sie schreiben in den Speicher, der Ihrem Programm nicht gehört. Die Folgen hängen vollständig vom Betriebssystem ab, das Sie verwenden. Auf geschützten Systemen wie UNIX, Windows NT oder Windows 98 erkennt das Betriebssystem diesen Verstoß sofort. Es beendet das Programm. Harter Stopp. Es schützt den Rest des Systems vor Ihrem Fehler.

Ältere Systeme wie Windows 3.1 oder das klassische Mac OS verfügen nicht über diesen Luxus. Sie wissen nicht, was Sie tun. Am Ende überschreiben Sie Code oder Variablen in einer anderen Anwendung. Das Ergebnis? Ein Fehler. Ein Absturz. Oder schlimmer noch, ein systemweiter Fehler, der zufällig erscheint, tatsächlich aber das direkte Ergebnis eines ungeprüften Speicherzugriffs ist.

Im Speicher liegen Variablen wie „i“, „s“, „t“ und „u“ nebeneinander an bestimmten Adressen. Sie sind Nachbarn. Wenn Sie über die Grenze des einen hinausschreiben, gelangen Sie in den nächsten. Der Computer macht genau das, was Sie gesagt haben. Es ist einfach egal, dass du es gut gemeint hast.

Die Gefahr eines unkontrollierten Array-Zugriffs

C und C++ führen keine Bereichsprüfung durch. Sie gehen davon aus, dass Sie wissen, was Sie tun. Das bedeutet, dass Sie den Array-Bereichen große Aufmerksamkeit schenken müssen. Wenn Sie außerhalb der Grenzen lesen oder schreiben, ist das Verhalten fehlerhaft. Unvorhersehbar. Gefährlich.

Dieser Mangel an Sicherheit ist der Grund, warum nicht initialisierte Zeigerfehler in C so häufig vorkommen. Betrachten Sie diesen Code:

„c

int main() {
int i, j;
int p; / ein Zeiger auf eine Ganzzahl */
printf(“%d %d\n”, p, &i);
p = &i;
printf(“%d %d\n”, p, &i);
0 zurückgeben;
}

Der Compiler gibt den Wert in „p“ und die Adresse von „i“ aus. Anfangs enthält „p“ Müll oder Null. Die Adresse von „i“ ist normalerweise eine große Zahl. Beispielsweise könnten Sie Folgendes sehen:


0 2147478276
2147478276 2147478276

Nach „p = &i“ enthält „p“ die Adresse von „i“. Einfach.

Schauen Sie sich nun Folgendes an:

„c

void main() {
int p; / ein Zeiger auf eine Ganzzahl /
printf(“%d\n”,
p);
}

Dies gibt den Wert aus, auf den „p“ zeigt. Aber „p“ ist nicht initialisiert. Es zeigt auf die Adresse 0 oder ein zufälliges Bit im Speicher. Das Ergebnis ist fast immer ein Segmentierungsfehler. Ein Laufzeitfehler. Sie versuchen, auf Speicher zuzugreifen, der Ihnen nicht gehört.

Hinweise in neuem Licht

Sobald Sie diese Risiken verstanden haben, ändern sich die Hinweise. Sie sind nicht nur Variablen. Es handelt sich um direkte Adressen zu Speicherorten.

Nehmen Sie dieses Programm:

„c

int main() {
int i;
int p; / ein Zeiger auf eine Ganzzahl /
p = &i;
p = 5;
printf(“%d %d\n”, i, *p);
0 zurückgeben;
}

Folgendes passiert:

Wie Zeiger Speicheradressen speichern

Wenn Sie eine Ganzzahlvariable wie i deklarieren, belegt sie 4 Byte Speicherplatz im RAM. Ein Zeiger, nennen wir ihn p, belegt ebenfalls 4 Bytes. Diese Größe gilt für die überwiegende Mehrheit der derzeit im Umlauf befindlichen Systeme, bei denen die Speicheradressen 32 Bit lang sind. Die Branche stellt langsam auf die 64-Bit-Adressierung um, aber derzeit ist der 4-Byte-Footprint der Standard.

Stellen Sie sich ich als ein Haus vor. Es hat eine bestimmte Adresse – sagen wir 248.440. Der Zeiger p ist nicht das Haus selbst. Es ist ein Stück Papier, auf dem die Adresse steht. Wenn Sie p = &i zuweisen, schreiben Sie 248.440 auf das Papier.

Diese Unterscheidung ist entscheidend für das Verständnis, wie Zeiger in C funktionieren. Der Zeiger enthält nicht den Wert von i. Es enthält den Ort, an dem ich lebe. Wenn Sie p dereferenzieren (mit p *), gehen Sie zu dieser Adresse und lesen den Wert. In diesem Sinne sind p und i * funktional identisch. Sie verweisen auf dieselben Daten.

Anzeigen der Adresse im Code

Sie können dieses Verhalten mit einer einfachen print-Anweisung überprüfen.

printf(“%d”, p);

Dieser Befehl gibt den Wert von i nicht aus. Es druckt die auf das Papier geschriebene Nummer. Die Ausgabe ist die tatsächliche Speicheradresse von i.

Warum ist das wichtig? Weil Sie damit Standorte statt Datenkopien übergeben können. Sie können den Speicher direkt manipulieren. Sie können Strukturen miteinander verknüpfen. Sie können den Heap-Speicher effizient verwalten. Der Zeiger ist eine Referenz. Die Variable ist der Inhalt. Eine Verwechslung der beiden führt zu Fehlern. Das Verstehen des Unterschieds führt zur Kontrolle.

Einige Entwickler betrachten Zeiger als Magie. Das sind sie nicht. Es sind nur Zahlen. Speicheradressen sind ganze Zahlen. Behandle sie auch so.

Vorheriger ArtikelVerschwenden Sie kein Geld mehr für Massen-URL-Übermittlungsdienste für SEO
Nächster ArtikelTwitch wurde nicht gefunden? Canlı yayın dünyasına hızlı giriş