Warum stürzt ein Programm, das auf illegale Zeiger auf Zeiger zugreift, nicht ab?

Lesezeit: 8 Minuten

Benutzer-Avatar
pavan.mankala

Ein Programm, das auf illegale Zeiger auf Zeiger zugreift, stürzt bei SIGSEGV nicht ab. Das ist nicht gut, aber ich frage mich, wie das sein kann und wie der Prozess viele Tage in der Produktion überlebt hat. Es ist für mich verwirrend.

Ich habe dieses Programm in Windows, Linux, OpenVMS und Mac OS ausprobiert und sie haben sich nie beschwert.

#include <stdio.h>
#include <string.h>

void printx(void *rec) { // I know this should have been a **
    char str[1000];
    memcpy(str, rec, 1000);
    printf("%*.s\n", 1000, str);
    printf("Whoa..!! I have not crashed yet :-P");
}

int main(int argc, char **argv) {
    void *x = 0; // you could also say void *x = (void *)10;
    printx(&x);
}

  • Dies ist ein undefiniertes Verhalten, daher ist das Nicht-Abstürzen ein vollkommen gutes Ergebnis. Verwenden Sie ein geeignetes Tool zur Speicherüberprüfung, wenn Sie solche Dinge debuggen möchten.

    – David

    25. Juli 2013 um 7:59 Uhr

  • Es ist nur so, dass ich einen Zeiger auf einen Zeiger übergeben habe und wenn der Memcpy versucht, den Zeiger in der Funktion printx () zu dereferenzieren und versucht, einige 1000 Byte Müll zu kopieren, sollte er abgestürzt sein

    – pavan.mankala

    25. Juli 2013 um 8:00 Uhr

  • Memory Checker wie Valgrind versuchen, solche Dinge zu melden. Ansonsten besteht keine Gewährleistung.

    – hivert

    25. Juli 2013 um 8:11 Uhr

  • Siehe: Hotel.

    – TRIG

    25. Juli 2013 um 12:27 Uhr

Benutzer-Avatar
Wallyk

Das Fehlen eines Speicherfehlers überrascht mich nicht. Das Programm ist nicht Dereferenzieren eines nicht initialisierten Zeigers. Stattdessen kopiert und druckt es den Inhalt des Speichers, beginnend bei einer Zeigervariablen, und die 996 (oder 992) Bytes dahinter.

Da der Zeiger eine Stack-Variable ist, druckt er Speicher in der Nähe der Spitze des Stacks für einen Weg nach unten. Dieser Speicher enthält den Stapelrahmen von main(): möglicherweise einige gespeicherte Registerwerte, eine Anzahl von Programmargumenten, ein Zeiger auf die Programmargumente, ein Zeiger auf eine Liste von Umgebungsvariablen und ein gespeichertes Befehlsregister für main() zurückzugeben, normalerweise im Startcode der C-Laufzeitbibliothek. In allen Implementierungen, die ich untersucht habe, enthalten die Stack-Frames darunter Kopien der Umgebungsvariablen selbst, ein Array von Zeigern auf sie und ein Array von Zeigern auf die Programmargumente. In Unix-Umgebungen (die Sie andeuten, dass Sie sie verwenden) befinden sich die Programmargumentzeichenfolgen darunter.

Der gesamte Speicher kann “sicher” gedruckt werden, außer dass einige nicht druckbare Zeichen erscheinen, die ein Anzeigeterminal durcheinander bringen könnten.

Das potenzielle Hauptproblem besteht darin, ob genügend Stack-Speicher zugewiesen und abgebildet ist, um ein SIGSEGV während des Zugriffs zu verhindern. EIN Segmentfehler kann passieren, wenn zu wenig Umgebungsdaten vorhanden sind. Oder wenn die Implementierung diese Daten woanders ablegt, so dass hier nur ein paar Stapelworte vorhanden sind. Ich schlage vor, dies zu bestätigen, indem Sie die Umgebungsvariablen löschen und das Programm erneut ausführen.

Dieser Code wäre nicht so harmlos, wenn eine der C-Laufzeitkonventionen nicht zutrifft:

  • Die Architektur verwendet einen Stack
  • Eine lokale Variable (void *x) wird auf dem Stapel zugewiesen
  • Der Stack wächst in Richtung des niedriger nummerierten Speichers
  • Parameter werden auf dem Stack übergeben
  • Ob main() wird mit Argumenten aufgerufen. (Einige Light-Duty-Umgebungen, wie eingebettete Prozessoren, rufen auf main() ohne Parameter.)

In allen modernen Mainstream-Implementierungen sind alle diese im Allgemeinen wahr.

  • @pavan.mankala: Gerne. In der Tat habe ich versucht, mehrere Compiler zu schreiben und zu warten, und ziemlich viel Zeit damit verbracht, mich mit der Schnittstelle zum Aufrufen zu befassen main()hauptsächlich um eine begrenzte Speicherumgebung zu rationalisieren.

    – Wallyk

    25. Juli 2013 um 8:27 Uhr

  • +1: Das Dereferenzieren eines Nullzeigers führt in den meisten Betriebssystemen garantiert zu einem Segfault (. Es gibt Software, die davon abhängt.

    – Joni

    25. Juli 2013 um 8:29 Uhr


  • @DevSolar, was rein akademisch ist, besagt, dass alles, was jemals passiert, undefiniertes Verhalten ist, ohne dass das tatsächlich beobachtete Verhalten erklärt wird. Ich würde behaupten, dass es gute Technik und gute Informatik ist, erklären zu können, was eine Maschine in einer bestimmten Situation tut, selbst wenn oder insbesondere wenn die Sprachspezifikation die Verantwortung für eine Entscheidung den Implementierern des Compilers und des Betriebssystems überlässt. Unsere Programme laufen nicht isoliert von realen Problemen ab.

    – Joni

    25. Juli 2013 um 9:33 Uhr

  • @Joni: Sie denken also, es ist eine gute Erklärung, dass Zeiger 32- oder 64-Bit sind, Parameter auf dem Stapel übergeben werden, Stapel sich nach unten erstrecken und dass daneben ein Zeiger auf Umgebungsvariablen vorhanden ist argc / argvauf dem Stack gespeicherte Absenderadresse, yadda yadda, ohne diese Aussage auch nur als “…unter Linux und Windows” zu qualifizieren? Glückselig davon ausgegangen, dass dort ist so etwas wie SIGSEGV auf dem Zielrechner, und dass ein illegaler Zugriff nicht das gesamte Betriebssystem zum Absturz bringt? Für jede einzelne dieser Aussagen kenne ich ein System, bei dem diese Annahme nicht zutrifft.

    – DevSolar

    25. Juli 2013 um 9:44 Uhr


  • @DevSolar, Viele dieser Dinge lassen sich aus der Frage ableiten. Zum Beispiel erwähnt das OP bereits SIGSEGV, was auf eine Vertrautheit mit einem Unix-System irgendeiner Art hindeutet, und erwähnt später zwei Beispiele: Linux und OS X. Da Zeiger 32/64-Bit sind, scheint dies keine notwendige Annahme zu sein, sondern tendiert Dies ist bei Computern der Fall, auf denen Sie Windows und OS X ausführen können. Da es tatsächlich einen Stapel und Stapel gibt, die sich nach unten erstrecken, so arbeiten Compiler auf diesen Plattformen normalerweise Speicher organisieren, wieder eine ziemlich sichere Sache. Aber ja, die Antwort könnte besser sein, wenn sie die Annahmen relativiert.

    – Joni

    25. Juli 2013 um 10:09 Uhr

Benutzer-Avatar
DevSolar

Illegaler Speicherzugriff ist undefiniertes Verhalten. Dies bedeutet, dass Ihr Programm könnte abstürzen, ist aber nicht garantiert, weil genaues Verhalten ist nicht definiert.

(Ein Witz unter Entwicklern, besonders gegenüber Kollegen, die in solchen Dingen nachlässig sind, ist, dass “das Aufrufen von undefiniertem Verhalten Ihre Festplatte formatieren könnte, es ist nur nicht garantiert”. 😉 )

Aktualisieren: Hier wird gerade heiß diskutiert. Ja, Systementwickler sollten wissen, was eigentlich das passiert auf einem bestimmten System. Aber solches Wissen ist an die CPU, das Betriebssystem, den Compiler usw. gebunden und im Allgemeinen von begrenztem Nutzen, denn selbst wenn Sie den Code zum Laufen bringen würden, würde es funktionieren still von sehr schlechter Qualität sein. Deshalb habe ich meine Antwort auf den wichtigsten Punkt beschränkt und die eigentlich gestellte Frage (“warum stürzt das nicht ab”):

Der in der Frage gepostete Code hat kein genau definiertes Verhalten, aber das bedeutet nur, dass Sie sich nicht wirklich darauf verlassen können, was er tut, nicht dass es sollte Absturz.

  • Hier findet kein illegaler Speicherzugriff statt. Obwohl das Programm sorglos auf Stapelspeicher zugreift, ist daran nichts besonders auszusetzen, solange genügend Stapeldaten vorhanden sind. Ihre Antwort ist soweit richtig, aber sie spricht den obigen Code nicht an.

    – Wallyk

    25. Juli 2013 um 8:23 Uhr


  • @wallyk: Ich weiß nichts über C11, aber der C99-Standard erwähnt “Stack” nirgendwo. Je nachdem wie Ihr Implementierung handhabt Stack, das Lesen von 1000 Bytes wird Sie entweder dazu bringen, darüber hinauszugehen x, argv und argc ins Nichts (illegal) oder Siere trespassing from x` hinein strund verwenden memcpy() auf überlappenden Speicherbereichen liegt per Definition undefiniertes Verhalten. So oder so illegaler Code. Und Sie, mein Herr, nicht böse gemeint, sind nur der Typ-Codierer, der in der Zeile “Festplatten formatieren” gemeint ist: Der Typ, der Code wie diesen nicht sofort bemängeln kann, weil es so ist könnte Arbeit.

    – DevSolar

    25. Juli 2013 um 9:04 Uhr


  • Ich bin mir ziemlich sicher, dass die C11- und C99-Standards Sprachfunktionen behandeln, keine Implementierungsdetails. Stacks sind jedoch ziemlich erprobt und wahr. Mainframes, die ich vor langer Zeit verwendet habe, hatten sie nicht, aber Struktursprachen implementierten Stapel für ihre Bequemlichkeit. Es gibt keine Speicherüberlappung für memcpy(): Das 1000-Byte-Ziel ist zugewiesen und seine Quelle ist woanders. (Tatsächlich habe ich Code zur Festplattenformatierung geschrieben, aber es war immer vorhersehbar und beabsichtigt.)

    – Wallyk

    25. Juli 2013 um 9:09 Uhr

  • @wallyk: Auch dieser Code könnte auf Maschine A und Maschine B funktionieren, und A und B könnten zusammen 99 % aller Systeme weltweit ausmachen, aber der vom OP gepostete Code wird aufgerufen nicht definiert Verhalten, das ist definiert als “Verhalten, das nicht durch den Sprachstandard definiert ist”. Da es nicht definiert ist, sollte sich ein vorsichtiger Entwickler niemals darauf verlassen was eigentlich auf System X passiertweil es Wille eines Tages brechen. Ich respektiere Ihre praktische Erfahrung, aber ich bin der festen Überzeugung, dass zu vielen aufstrebenden Programmierern zu viel “unter der Haube” gezeigt wird, mit zu wenig “Nicht anfassen”-Warnungen.

    – DevSolar

    25. Juli 2013 um 9:20 Uhr

Wenn Sie einen ungültigen Zeiger dereferenzieren, rufen Sie undefiniertes Verhalten auf. Das heißt, das Programm kann abstürzen, es kann funktionieren, es könnte Kaffee kochen, was auch immer.

Wenn du. .. hast

int main(int argc, char **argv) {
    void *x = 0; // you could also say void *x = (void *)10;
    printx(&x);
}

Du erklärst x als Zeiger mit Wert 0, und dieser Zeiger befindet sich im Stapel, da es sich um eine lokale Variable handelt. Jetzt gehen Sie zu printx das die Anschrift von xwas bedeutet, dass mit

memcpy(str, rec, 1000);

Sie kopieren Daten von oberhalb des Stapels (oder tatsächlich vom Stapel selbst) auf den Stapel (weil die Adresse des Stapelzeigers bei jedem Push abnimmt). Die Quelldaten werden wahrscheinlich von demselben Seitentabelleneintrag abgedeckt, da Sie nur 1000 Bytes kopieren, sodass Sie keinen Segmentierungsfehler erhalten. Letztlich handelt es sich aber, wie bereits geschrieben, um undefiniertes Verhalten.

Es würde mit großer Wahrscheinlichkeit abgestürzt sein, wenn Sie schreiben zum unbetretenen Bereich. Aber du bist lesen, es kann ok sein. Aber das Verhalten wird noch undefiniert sein.

  • @pavan.mankala: Das passiert nicht. Siehe bitte meine Antwort.

    – Wallyk

    25. Juli 2013 um 8:17 Uhr

  • int main(void) { return *(int *)rand(); } – Segmentierungsfehler.

    – Ben Millwood

    25. Juli 2013 um 12:28 Uhr

  • @pavan.mankala: Das passiert nicht. Siehe bitte meine Antwort.

    – Wallyk

    25. Juli 2013 um 8:17 Uhr

  • int main(void) { return *(int *)rand(); } – Segmentierungsfehler.

    – Ben Millwood

    25. Juli 2013 um 12:28 Uhr

1225390cookie-checkWarum stürzt ein Programm, das auf illegale Zeiger auf Zeiger zugreift, nicht ab?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy