Dereferenzieren eines Zeigers auf 0 in C

Lesezeit: 9 Minuten

Benutzeravatar von gfv
gfv

Manchmal sind Daten an der Speicheradresse 0x0 sehr wertvoll – nehmen Sie x86-Realmodus-IVT als bekannteres Beispiel: Sie beginnt bei 0x0 und enthält Zeiger auf Interrupt-Handler: Ein Doppelwort bei 0x00 ist ein Zeiger auf einen Division-by-Zero-Error-Handler.

Der C11-Sprachstandard verbietet jedoch das Dereferenzieren von Nullzeigern [WG14 N1570 6.5.3.2]die als mit 0 initialisierte Zeiger oder mit einem Nullzeiger initialisierte Zeiger definiert sind [WG14 N1570 6.3.2.3]wodurch das allererste Byte effektiv verboten wird.

Wie verwenden die Leute eigentlich 0x0, wenn es nötig ist?

  • Meinst du C++11? Dieser Standard besagt ausdrücklich, dass nullptr überhaupt keine ganze Zahl ist. IE nullptr != 0x0.

    – GreenAsJade

    14. Januar 2014 um 1:47 Uhr

  • Können Sie bitte auf den relevanten Teil der C11-Spezifikation verweisen?

    – Mötz

    14. Januar 2014 um 1:47 Uhr

  • @GreenAsJade Nein, hier nicht C++, einfach C.

    – gfv

    14. Januar 2014 um 1:53 Uhr

  • Der Nullzeiger ist der Zeiger, den Sie von einem Ausdruck wie erhalten (void *)0aber es ist nicht Notwendig das gleiche wie ein Zeiger auf die Adresse Null.

    – Hobbs

    14. Januar 2014 um 2:01 Uhr

  • @alvits Im Real-Modus (16-Bit-Modus), nein. Im Real-Modus gibt es keine Trennung von Userspace und Kernelspace.

    – chbaker0

    14. Januar 2014 um 3:46 Uhr

Benutzeravatar von Keith Thompson
Keith Thompson

C nicht verbieten Durch die Dereferenzierung des Nullzeigers wird das Verhalten lediglich undefiniert.

Wenn Ihre Umgebung so ist, dass Sie einen Zeiger dereferenzieren können, der die Adresse enthält 0x0, dann solltest du das können. Der C-Sprachstandard sagt nichts darüber aus, was passiert, wenn Sie dies tun. (In den meisten Umgebungen führt dies zu einem Programmabsturz.)

Ein konkretes Beispiel (wenn ich mich richtig erinnere): Auf den 68k-basierten Sun 3-Rechnern führte das Dereferenzieren eines Nullzeigers nicht zu einem Trap; Stattdessen speicherte das Betriebssystem einen Nullwert an der Speicheradresse Null, und die Dereferenzierung eines Nullzeigers (der auf die Adresse Null zeigte) würde diesen Nullwert ergeben. Das bedeutete zum Beispiel, dass ein C-Programm einen Nullzeiger so behandeln konnte, als wäre er ein gültiger Zeiger auf einen leeren String. Einige Software war absichtlich oder nicht von diesem Verhalten abhängig. Dies erforderte viel Aufräumarbeiten beim Portieren von Software auf das SPARC-basierte Sun 4, das bei Nullzeiger-Dereferenzierungen abgefangen wurde. (Ich erinnere mich genau, darüber gelesen zu haben, aber ich konnte keine Referenz finden; ich werde dies aktualisieren, wenn ich es finden kann.)

Beachten Sie, dass der Nullzeiger ist nicht unbedingt Adresse Null. Genauer gesagt kann die Darstellung einer Null nur Bits-Null sein oder nicht. Es ist sehr häufig, aber es ist nicht garantiert. (Wenn dies nicht der Fall ist, wird die Umwandlung von Ganzzahlen in Zeiger von (void*)0 ist nicht trivial.)

§ 5 des comp.lang.c FAQ bespricht Nullzeiger.

  • Eigentlich frage ich mich, ob es jemals nicht UB ist, einem Zeiger eine beliebige Zahl zuzuweisen und zu dereferenzieren?

    – Mötz

    14. Januar 2014 um 2:11 Uhr

  • Antworten in der comp.lang.c-FAQ sehen etwas zu umständlich aus: ja, formal Sie weisen einem Zeiger keine 0 zu, aber ihr Geist füllt den Raum mit Nullen, und wie Sie bemerkt haben, ist dies nicht immer eine Nullzeigerdarstellung.

    – gfv

    14. Januar 2014 um 2:14 Uhr

  • Ich denke, in den meisten Fällen ruft es ein undefiniertes Verhalten auf, da der untere Teil des Speichers für die Adressen der Unterroutinen des Betriebssystems (Interrupt-Service-Routinen) reserviert ist.

    – Hacken

    14. Januar 2014 um 2:16 Uhr


  • Die @gfv-Implementierung ist de facto UB, außer ohne die Möglichkeit, Dämonen durch die Nase zu bekommen

    – Mötz

    14. Januar 2014 um 2:24 Uhr

  • @MikeWarren: Nicht unbedingt. Das Verhalten ist undefiniert, was konkret bedeutet, dass für den C-Standard alles passieren kann; eine “schlechte Zeit” ist nicht die einzige Möglichkeit. Einige Systeme hatten einen lesbaren Wert 0 an Adresse 0 (was zu einer Menge Spaß beim Portieren von Programmen führte, die für solche Systeme auf strengere Systeme geschrieben wurden, die beim Dereferenzieren von Nullzeigern abgefangen wurden).

    – Keith Thompson

    14. Januar 2014 um 14:47 Uhr

Wie verwenden die Leute eigentlich 0x0, wenn es nötig ist?

Durch entweder:

  • Schreiben des erforderlichen Codes in Assemblersprache, oder
  • Schreiben des Codes in C und Überprüfen, ob der Compiler die richtige Assemblersprache für die gewünschte Operation generiert

  • Wenn ein Zeiger auf null gesetzt wird oder eine 0x0-Adresse hat, zeigt er physisch auf die 0x0-Adresse? Das heißt, wenn wir Betriebssysteme mit virtuellem Speicherkonzept in Betracht ziehen?

    – Koushik Shetty

    14. Januar 2014 um 9:42 Uhr

  • @Koushik: Nein, virtueller Speicher bedeutet, dass die Adresse 0x0 in einem bestimmten Prozessadressraum nicht unbedingt auf die physische Adresse 0x0 zeigt.

    – Greg Hewgill

    14. Januar 2014 um 9:43 Uhr

  • Wenn Sie an eingebetteten Systemen arbeiten, die nur über physischen Speicher verfügen, zeigt dies ja auf die Adresse 0x0. In dem von Keith Thompson zitierten Beispiel würde die MC68xxx-CPU-Serie einen Busfehler auslösen (Ausnahme), wenn der Speicher physisch nicht vorhanden wäre.

    – Dan Haynes

    15. Januar 2014 um 0:23 Uhr

  • Hoppla – Zeitüberschreitung beim Bearbeiten von Kommentaren: Der Speicher bei 0x0 in einem MC68xxx-System musste existieren, da dort der Reset-Vektor lebt. Beim Einschalten würde die CPU den 32-Bit-Wert von 0x0000000..0x000003 abrufen und in den Stapelzeiger laden, dann 32 Bit von 0x0000004..0x000007 abrufen und diesen Wert als anfänglichen Befehlszeiger verwenden … und dann ab zu die Rennen würde es gehen.

    – Dan Haynes

    15. Januar 2014 um 0:31 Uhr

Die Aussage:

char * x = 0;

fügt nicht unbedingt 0x0 in x ein. Es setzt den definierten Nullzeigerwert für die aktuelle Architektur und den aktuellen Compiler in x.

In der Praxis enden alle allgemein verwendeten Compiler / Prozessoren damit, 32 (oder 64) 0-Bits hintereinander in ein Register oder einen Speicherplatz als Antwort auf diese Anweisung zu setzen. Wenn also die Speicheradresse 0 nützlich ist, dann stecken Sie, wie andere angedeutet haben, mit formal undefiniertem Verhalten fest. Es war jedoch einmal Hardware da draußen, für die ein „Nullzeiger“ ein Bitmuster war, das es war nicht alles Nullen, und wer weiß, vielleicht gibt es wieder.

  • Eine Implementierung von (Logitech, glaube ich) Modula-2, die ich vor Jahren verwendet habe, implementierte den NIL-Zeiger als FFFF:FFFF (segmentiertes 16-Bit-Land). Natürlich war es nicht C und die Regeln sind sowieso anders (dh Sie können nicht einfach tun if (p) ...).

    – Greg Hewgill

    14. Januar 2014 um 2:33 Uhr

  • @Greg Spaß mit undefiniertem Verhalten! FFFF:FFFF ist noch situativer als 0000:0000da es als lineare Adresse interpretiert werden könnte 10FFEF oder 00FFEF abhängig davon, ob das Adressbit 20 aktiviert ist, und das Stampfen auf das, was sich an einer dieser Stellen befindet, könnte zu zwei verschiedenen Arten von Problemen führen.

    – Jeffrey Hantin

    14. Januar 2014 um 5:00 Uhr

  • @JeffreyHantin: Nicht nur das, das Lesen (oder Schreiben) von mehr als einem Byte an dieser Adresse führt zu allen möglichen Verrücktheiten.

    – Greg Hewgill

    14. Januar 2014 um 5:04 Uhr

  • @GregHewgill Natürlich if (p) wird dann funktionieren, weil es nicht auf das 0-Muster testet, sondern auf das Vorhandensein (bzw. Nichtvorhandensein) des NULL Zeigermuster.

    – glglgl

    14. Januar 2014 um 12:30 Uhr


  • @glglgl: Ja natürlich, aber das meinte ich if (p) (mit einem implizit Vergleich gegen NULL oder nullptr) ist keine gültige Modula-2-Syntax, und das Äquivalent müsste es sein IF p # NIL wo der Vergleich ist explizit.

    – Greg Hewgill

    14. Januar 2014 um 18:06 Uhr


Anhang J Es ist ein undefiniertes Verhalten, wenn…

Der Operand des unären *-Operators hat einen ungültigen Wert (6.5.3.2).

In derselben Fußnote, die Sie erwähnt haben, heißt es, dass ein Nullzeiger ein ungültiger Wert ist. Daher ist es kein verbotenes, sondern undefiniertes Verhalten. Was die Unterscheidung zwischen Adresse 0x0 und einen Nullzeiger, siehe Ist die Speicheradresse 0x0 verwendbar?.

Der Nullzeiger ist nicht unbedingt die Adresse 0x0, daher könnte eine Architektur möglicherweise eine andere Adresse auswählen, um den Nullzeiger darzustellen, und Sie könnten 0x0 von neu als gültige Adresse erhalten.

Ob der Null-Zeiger vom Betriebssystem oder der C++-Implementierung reserviert ist, ist nicht spezifiziert, aber Plain New wird niemals einen Null-Zeiger zurückgeben, was auch immer seine Adresse ist (nothrow new ist eine andere Bestie). Also, um deine Frage zu beantworten:

Ist die Speicheradresse 0x0 nutzbar?

Vielleicht hängt es von der jeweiligen Implementierung/Architektur ab.

Mit anderen Worten, Sie können es gerne verwenden 0x0 wenn Sie sicher sind, dass Ihr System keinen Absturz verursacht.

Das Betriebssystem verwendet eine Tabelle von Zeigern, um Routinen zu unterbrechen, um geeignete Unterbrechung(en) aufzurufen. Im Allgemeinen (in den meisten Betriebssystemen) wird die Zeigertabelle im niedrigen Speicher (der ersten paar hundert oder so Orte), Diese Orte enthalten die Adressen der Interrupt-Service-Routinen für die verschiedenen Geräte.

Also, wenn du es tust

char *ptr = 0x0; 

dann initialisieren Sie höchstwahrscheinlich Ihren Zeiger mit der Adresse einer Interrupt-Service-Routine. Das Dereferenzieren (oder Ändern) eines Speicherorts, der zum Betriebssystem gehört, führt höchstwahrscheinlich zum Absturz des Programms.
Also besser keinen Zeiger auf initialisieren 0x0 und dereferenzieren Sie es, bis Sie die Bestätigung haben, dass es nicht zum Betriebssystem gehört.

  • Was ist, wenn Sie tatsächlich das Betriebssystem schreiben? Sie brauchen immer noch einen Weg, um so etwas zu tun.

    – Greg Hewgill

    14. Januar 2014 um 2:56 Uhr


  • @GregHewgill; WAHR. Aber im Allgemeinen können Sie die Adresse nicht dereferenzieren, die zum Betriebssystem gehört.

    – Hacken

    14. Januar 2014 um 2:58 Uhr

  • Gibt es keine Trennung zwischen Kernelspace und Userspace?

    – alvits

    14. Januar 2014 um 3:02 Uhr

  • @hackks – bitte kläre mich auf. Wenn eine App im Benutzerbereich ausgeführt wird, ist die Adresse 0x0 nicht relativ zur Basisadresse des Benutzerbereichs?

    – alvits

    14. Januar 2014 um 3:03 Uhr

  • @alvits; Ich hoffe, Sie sind sich dessen bewusst Dual-Mode-Betrieb, dh Kernelmodus und Benutzermodus. Wenn Sie Ihr Anwendungsprogramm ausführen, befindet sich Ihr System im Benutzermodus. Wenn es einen Systemaufruf anfordert, erfolgt der Übergang vom Benutzermodus in den Kernelmodus, um die Anforderung zu erfüllen.

    – Hacken

    14. Januar 2014 um 3:13 Uhr


  • Was ist, wenn Sie tatsächlich das Betriebssystem schreiben? Sie brauchen immer noch einen Weg, um so etwas zu tun.

    – Greg Hewgill

    14. Januar 2014 um 2:56 Uhr


  • @GregHewgill; WAHR. Aber im Allgemeinen können Sie die Adresse nicht dereferenzieren, die zum Betriebssystem gehört.

    – Hacken

    14. Januar 2014 um 2:58 Uhr

  • Gibt es keine Trennung zwischen Kernelspace und Userspace?

    – alvits

    14. Januar 2014 um 3:02 Uhr

  • @hackks – bitte kläre mich auf. Wenn eine App im Benutzerbereich ausgeführt wird, ist die Adresse 0x0 nicht relativ zur Basisadresse des Benutzerbereichs?

    – alvits

    14. Januar 2014 um 3:03 Uhr

  • @alvits; Ich hoffe, Sie sind sich dessen bewusst Dual-Mode-Betrieb, dh Kernelmodus und Benutzermodus. Wenn Sie Ihr Anwendungsprogramm ausführen, befindet sich Ihr System im Benutzermodus. Wenn es einen Systemaufruf anfordert, erfolgt der Übergang vom Benutzermodus in den Kernelmodus, um die Anforderung zu erfüllen.

    – Hacken

    14. Januar 2014 um 3:13 Uhr


1411460cookie-checkDereferenzieren eines Zeigers auf 0 in C

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

Privacy policy