NULL neu definieren

118

Ich schreibe C-Code für ein System, in dem die Adresse 0x0000 gültig ist und Port-E / A enthält. Daher bleiben mögliche Fehler, die auf einen NULL-Zeiger zugreifen, unentdeckt und verursachen gleichzeitig gefährliches Verhalten.

Aus diesem Grund möchte ich NULL neu definieren, um eine andere Adresse zu sein, zum Beispiel eine Adresse, die nicht gültig ist. Wenn ich versehentlich auf eine solche Adresse zugreife, erhalte ich einen Hardware-Interrupt, mit dem ich den Fehler behandeln kann. Ich habe zufällig Zugriff auf stddef.h für diesen Compiler, sodass ich den Standardheader tatsächlich ändern und NULL neu definieren kann.

Meine Frage ist: Wird dies im Widerspruch zum C-Standard stehen? Soweit ich aus 7.17 im Standard ersehen kann, ist das Makro implementierungsdefiniert. Gibt es irgendwo anders im Standard etwas, das besagt, dass NULL 0 sein muss ?

Ein weiteres Problem besteht darin, dass viele Compiler eine statische Initialisierung durchführen, indem sie unabhängig vom Datentyp alles auf Null setzen. Obwohl der Standard besagt, dass der Compiler Ganzzahlen auf Null und Zeiger auf NULL setzen soll. Wenn ich NULL für meinen Compiler neu definieren würde, dann weiß ich, dass eine solche statische Initialisierung fehlschlagen wird. Könnte ich das als falsches Compilerverhalten betrachten, obwohl ich die Compiler-Header manuell manuell geändert habe? Weil ich mit Sicherheit weiß, dass dieser bestimmte Compiler bei der statischen Initialisierung nicht auf das NULL-Makro zugreift.

Lundin
quelle
3
Das ist eine wirklich gute Frage. Ich habe keine Antwort für Sie, aber ich muss fragen: Sind Sie sicher, dass es nicht möglich ist, Ihre gültigen Daten um 0x00 zu verschieben und NULL eine ungültige Adresse wie in "normalen" Systemen zu sein? Wenn Sie dies nicht können, können Sie nur sicher ungültige Adressen verwenden, bei denen Sie sicher sein können, dass Sie sie zuweisen und dann mprotectsichern können. Wenn die Plattform keine ASLR oder ähnliches hat, Adressen außerhalb des physischen Speichers der Plattform. Viel Glück.
Borealid
8
Wie funktioniert es, wenn Ihr Code verwendet wird if(ptr) { /* do something on ptr*/ }? Funktioniert es, wenn NULL anders als 0x0 definiert ist?
Xavier T.
3
C-Zeiger haben keine erzwungene Beziehung zu Speicheradressen. Solange die Regeln der Zeigerarithmetik eingehalten werden, kann ein Zeigerwert alles sein. Die meisten Implementierungen verwenden die Speicheradressen als Zeigerwerte, können jedoch alles verwenden, solange es sich um einen Isomorphismus handelt.
Datenwolf
2
@bdonlan Das würde auch gegen (beratende) Regeln in MISRA-C verstoßen.
Lundin
2
@Andreas Ja, das sind auch meine Gedanken. Hardware-Leute sollten keine Hardware entwerfen dürfen, auf der Software laufen sollte! :)
Lundin

Antworten:

84

Der C-Standard verlangt nicht, dass Nullzeiger an der Adresse Null der Maschine sind. Das Umwandeln einer 0Konstante in einen Zeigerwert muss jedoch zu einem NULLZeiger führen (§6.3.2.3 / 3), und die Auswertung des Nullzeigers als Boolescher Wert muss falsch sein. Dies kann etwas umständlich sein, wenn Sie wirklich eine Nulladresse möchten und NULLnicht die Nulladresse ist.

Trotzdem ist es bei (starken) Änderungen am Compiler und an der Standardbibliothek nicht unmöglich, NULLmit einem alternativen Bitmuster dargestellt zu werden, während die Standardbibliothek strikt eingehalten wird. Es reicht jedoch nicht aus, einfach die Definition von sich NULLselbst zu ändern , da dies dann als NULLwahr ausgewertet würde.

Insbesondere müssten Sie:

  • Sorgen Sie dafür, dass wörtliche Nullen in Zuweisungen zu Zeigern (oder Umwandlungen zu Zeigern) in einen anderen magischen Wert umgewandelt werden, z -1.
  • Vereinbaren Sie Gleichheitstests zwischen Zeigern und einer konstanten Ganzzahl 0, um stattdessen nach dem magischen Wert zu suchen (§6.5.9 / 6).
  • Ordnen Sie alle Kontexte an, in denen ein Zeigertyp als Boolescher Wert ausgewertet wird, um die Gleichheit mit dem magischen Wert zu überprüfen, anstatt nach Null zu suchen. Dies folgt aus der Semantik der Gleichheitstests, aber der Compiler kann sie intern anders implementieren. Siehe §6.5.13 / 3, §6.5.14 / 3, §6.5.15 / 4, §6.5.3.3 / 5, §6.8.4.1 / 2, §6.8.5 / 4
  • Aktualisieren Sie, wie im Café erwähnt, die Semantik für die Initialisierung statischer Objekte (§6.7.8 / 10) und partieller zusammengesetzter Initialisierer (§6.7.8 / 21), um die neue Nullzeigerdarstellung widerzuspiegeln.
  • Erstellen Sie eine alternative Möglichkeit, auf die wahre Adresse Null zuzugreifen.

Es gibt einige Dinge, die Sie nicht erledigen müssen. Beispielsweise:

int x = 0;
void *p = (void*)x;

Danach pist NICHT garantiert, dass es sich um einen Nullzeiger handelt. Es müssen nur konstante Zuweisungen verarbeitet werden (dies ist ein guter Ansatz für den Zugriff auf die wahre Adresse Null). Gleichfalls:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

Ebenfalls:

void *p = NULL;
int x = (int)p;

xist nicht garantiert zu sein 0.

Kurz gesagt, genau diese Bedingung wurde anscheinend vom C-Sprachkomitee berücksichtigt und Überlegungen für diejenigen angestellt, die eine alternative Vertretung für NULL wählen würden. Alles, was Sie jetzt tun müssen, ist, größere Änderungen an Ihrem Compiler vorzunehmen, und hey Presto, Sie sind fertig :)

Als Randnotiz kann es möglich sein, diese Änderungen mit einer Quellcode-Transformationsstufe vor dem eigentlichen Compiler zu implementieren. Das heißt, anstelle des normalen Ablaufs von Präprozessor -> Compiler -> Assembler -> Linker würden Sie einen Präprozessor -> NULL-Transformation -> Compiler -> Assembler -> Linker hinzufügen. Dann könnten Sie Transformationen durchführen wie:

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

Dies würde einen vollständigen C-Parser sowie einen Typparser und eine Analyse von Typedefs und Variablendeklarationen erfordern, um zu bestimmen, welche Bezeichner Zeigern entsprechen. Auf diese Weise können Sie jedoch vermeiden, dass Sie Änderungen an den Codegenerierungsteilen des Compilers vornehmen müssen. Clang kann nützlich sein, um dies zu implementieren - ich verstehe, dass es mit Blick auf solche Transformationen entwickelt wurde. Natürlich müssten Sie wahrscheinlich auch noch Änderungen an der Standardbibliothek vornehmen.

bdonlan
quelle
2
Ok, ich hatte den Text in §6.3.2.3 nicht gefunden, aber ich vermutete, dass es irgendwo eine solche Aussage geben würde :). Ich denke, dies beantwortet meine Frage, standardmäßig darf ich NULL nicht neu definieren, es sei denn, ich möchte einen neuen C-Compiler schreiben, um mich
Lundin
2
Ein guter Trick besteht darin, den Compiler so zu hacken, dass der Zeiger <-> Integer XOR einen bestimmten Wert konvertiert, der ein ungültiger Zeiger ist und dennoch so trivial ist, dass die Zielarchitektur dies kostengünstig tun kann (normalerweise wäre dies ein Wert mit einem einzelnen gesetzten Bit) wie 0x20000000).
Simon Richter
2
Eine andere Sache, die Sie im Compiler ändern müssten, ist die Initialisierung von Objekten mit zusammengesetztem Typ. Wenn ein Objekt teilweise initialisiert wird, müssen alle Zeiger, für die kein expliziter Initialisierer vorhanden ist, initialisiert werden NULL.
Café
20

Der Standard besagt, dass ein ganzzahliger Konstantenausdruck mit dem Wert 0 oder ein solcher in den void *Typ konvertierter Ausdruck eine Nullzeigerkonstante ist. Dies bedeutet , dass (void *)0immer ein Nullzeiger ist, aber wenn man bedenkt int i = 0;, (void *)imuss nicht sein.

Die C-Implementierung besteht aus dem Compiler zusammen mit seinen Headern. Wenn Sie die Header so ändern, dass sie neu definiert werden NULL, den Compiler jedoch nicht so ändern, dass statische Initialisierungen korrigiert werden, haben Sie eine nicht konforme Implementierung erstellt. Es ist die gesamte Implementierung zusammengenommen, die ein falsches Verhalten aufweist, und wenn Sie es gebrochen haben, haben Sie wirklich niemanden zu beschuldigen;)

Sie müssen mehr als nur statische Initialisierungen beheben, natürlich - einen Zeiger gegeben p, if (p)äquivalent zu if (p != NULL)aufgrund der obigen Regel.

caf
quelle
8

Wenn Sie die C std-Bibliothek verwenden, treten Probleme mit Funktionen auf, die NULL zurückgeben können. In der malloc-Dokumentation heißt es beispielsweise:

Wenn die Funktion den angeforderten Speicherblock nicht zuordnen konnte, wird ein Nullzeiger zurückgegeben.

Da malloc und verwandte Funktionen bereits in Binärdateien mit einem bestimmten NULL-Wert kompiliert sind, können Sie die C std-Bibliothek nur dann direkt verwenden, wenn Sie Ihre gesamte Toolkette einschließlich der C std-Bibliotheken neu erstellen können.

Auch aufgrund der Verwendung von NULL durch die Standardbibliothek können Sie eine in den Kopfzeilen aufgeführte NULL-Definition überschreiben, wenn Sie NULL neu definieren, bevor Sie Standard-Header einschließen. Alles, was eingefügt wird, ist inkonsistent mit den kompilierten Objekten.

Ich würde stattdessen Ihren eigenen NULL-Wert "MYPRODUCT_NULL" für Ihre eigenen Zwecke definieren und entweder vermeiden oder von / in die C-Standardbibliothek übersetzen.

Doug T.
quelle
6

Lassen Sie NULL in Ruhe und behandeln Sie IO für Port 0x0000 als Sonderfall. Verwenden Sie möglicherweise eine in Assembler geschriebene Routine, die nicht der Standard-C-Semantik unterliegt. IOW, definieren Sie NULL nicht neu, definieren Sie Port 0x00000 neu.

Beachten Sie, dass beim Schreiben oder Ändern eines C-Compilers die Arbeit, die erforderlich ist, um eine Dereferenzierung von NULL zu vermeiden (vorausgesetzt, in Ihrem Fall hilft die CPU nicht), unabhängig von der Definition von NULL gleich ist. Daher ist es einfacher, NULL definiert zu lassen als Null, und stellen Sie sicher, dass Null niemals von C dereferenziert werden kann.

Apalala
quelle
Das Problem tritt nur auf, wenn versehentlich auf NULL zugegriffen wird, nicht, wenn absichtlich auf den Port zugegriffen wird. Warum sollte ich dann die Port-E / A neu definieren? Es funktioniert bereits wie es sollte.
Lundin
2
@Lundin Versehentlich oder nicht kann, NULL nur in einem C - Programm mit dereferenziert werden *p, p[]oder p(), so dass nur der Compiler über diejenigen zu kümmern braucht 0x0000 IO - Port zu schützen.
Apalala
@Lundin Der zweite Teil Ihrer Frage: Sobald Sie den Zugriff innerhalb von C auf die Adresse Null beschränkt haben, benötigen Sie einen anderen Weg, um Port 0x0000 zu erreichen. Eine in Assembler geschriebene Funktion kann dies tun. Innerhalb von C könnte der Port 0xFFFF oder was auch immer zugeordnet werden, aber es ist am besten, eine Funktion zu verwenden und die Portnummer zu vergessen.
Apalala
3

In Anbetracht der extremen Schwierigkeit, NULL neu zu definieren, wie von anderen erwähnt, ist es möglicherweise einfacher, die Dereferenzierung für bekannte Hardwareadressen neu zu definieren . Fügen Sie beim Erstellen einer Adresse 1 zu jeder bekannten Adresse hinzu, sodass Ihr bekannter E / A-Port wie folgt lautet:

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

Wenn die Adressen, mit denen Sie sich befassen, zusammengefasst sind und Sie sich sicher fühlen können, dass das Hinzufügen von 1 zur Adresse nicht mit irgendetwas in Konflikt steht (was in den meisten Fällen nicht der Fall sein sollte), können Sie dies möglicherweise sicher tun. Und dann müssen Sie sich keine Gedanken mehr über die Neuerstellung Ihrer Toolkette / Standardbibliothek und Ausdrücke in der folgenden Form machen:

  if (pointer)
  {
     ...
  }

funktioniert noch immer

Verrückt, ich weiß, aber ich dachte nur, ich würde die Idee da rauswerfen :).

Doug T.
quelle
Das Problem tritt nur auf, wenn versehentlich auf NULL zugegriffen wird, nicht, wenn absichtlich auf den Port zugegriffen wird. Warum sollte ich dann die Port-E / A neu definieren? Es funktioniert bereits wie es sollte.
Lundin
@LundIn Ich denke, Sie müssen auswählen, was schmerzhafter ist, indem Sie die gesamte Toolchain neu erstellen oder diesen einen Teil Ihres Codes ändern.
Doug T.
2

Das Bitmuster für den Nullzeiger ist möglicherweise nicht dasselbe wie das Bitmuster für die Ganzzahl 0. Die Erweiterung des NULL-Makros muss jedoch eine Nullzeigerkonstante sein, dh eine konstante Ganzzahl mit dem Wert 0, die in (void) umgewandelt werden kann *).

Um das gewünschte Ergebnis zu erzielen und gleichzeitig konform zu bleiben, müssen Sie Ihre Werkzeugkette ändern (oder möglicherweise konfigurieren), dies ist jedoch erreichbar.

Ein Programmierer
quelle
1

Du fragst nach Ärger. Durch eine Neudefinition NULLauf einen Wert ungleich Null wird dieser Code beschädigt:

   if (myPointer)
   {
      // myPointer ist nicht null
      ...
   }}
Tony das Pony
quelle