Anfang 2018 wurde unser Blog durch eine Reihe von Artikeln zur sechsten Überprüfung des Quellcodes des Chromium-Projekts ergänzt. Die Reihe enthält 8 Artikel zu Fehlern und Empfehlungen zu deren Vorbeugung. Zwei Artikel lösten heftige Diskussionen aus, und gelegentlich erhalte ich immer noch Kommentare per E-Mail zu den darin behandelten Themen. Vielleicht sollte ich zusätzliche Erklärungen geben und, wie sie sagen, den Rekord klarstellen.
Ein Jahr ist vergangen, seit eine Reihe von Artikeln über eine regelmäßige Überprüfung des Quellcodes des Chromium-Projekts geschrieben wurde:
- Chrom: Der sechste Projektcheck und 250 Bugs
- Nettes Chrom und ungeschicktes Memset
- brechen und durchfallen
- Chrom: Speicherlecks
- Chrom: Tippfehler
- Chrom: Verwendung nicht vertrauenswürdiger Daten
- Warum ist es wichtig zu überprüfen, was die Malloc-Funktion zurückgegeben hat?
- Chrom: Andere Fehler
Artikel, die sich mit
Memset und
Malloc befassen, haben Debatten ausgelöst und führen auch weiterhin dazu, was mir seltsam vorkommt. Anscheinend gab es einige Verwirrung aufgrund der Tatsache, dass ich bei der Formulierung meiner Gedanken nicht genau genug gewesen war. Ich beschloss, zu diesen Artikeln zurückzukehren und einige Klarstellungen vorzunehmen.
Memset
Beginnen wir mit einem Artikel über
Memset , denn hier ist alles einfach. Es gab einige Argumente für den besten Weg, Strukturen zu initialisieren. Nicht viele Programmierer haben geschrieben, dass es besser wäre, die Empfehlung zu geben, nicht zu schreiben:
HDHITTESTINFO hhti = {};
aber auf folgende Weise zu schreiben:
HDHITTESTINFO hhti = { 0 };
Gründe:
- Die Konstruktion {0} ist beim Lesen von Code leichter zu erkennen als {}.
- Die Konstruktion {0} ist intuitiver verständlich als {}. Das heißt, 0 deutet darauf hin, dass die Struktur mit Nullen gefüllt ist.
Dementsprechend schlagen mir die Leser vor, dieses Initialisierungsbeispiel im Artikel zu ändern. Ich bin mit den Argumenten nicht einverstanden und plane keine Änderungen am Artikel. Jetzt werde ich meine Meinung erklären und einige Gründe nennen.
Ich denke, die Sichtbarkeit ist eine Frage des Geschmacks und der Gewohnheit. Ich denke nicht, dass das Vorhandensein von 0 in den Klammern die Situation grundlegend verändert.
Was das zweite Argument betrifft, stimme ich dem überhaupt nicht zu. Der Datensatz vom Typ {0} gibt einen Grund an, den Code falsch wahrzunehmen. Sie können beispielsweise annehmen, dass alle Felder durch Einsen initialisiert werden, wenn Sie 0 durch 1 ersetzen. Daher ist ein solcher Schreibstil eher schädlich als hilfreich.
Der PVS-Studio-Analysator verfügt sogar über eine zugehörige Diagnose
V1009 , deren Beschreibung unten angegeben ist.
V1009. Überprüfen Sie die Array-Initialisierung. Nur das erste Element wird explizit initialisiert.Der Analysator hat einen möglichen Fehler festgestellt, der darauf zurückzuführen ist, dass beim Deklarieren eines Arrays der Wert nur für ein Element angegeben wird. Somit werden die verbleibenden Elemente implizit durch Null oder durch einen Standardkonstruktor initialisiert.
Betrachten wir das Beispiel eines verdächtigen Codes:
int arr[3] = {1};
Vielleicht würde der erwartete Programmierer als
arr ganz aus solchen bestehen, aber das ist es nicht. Das Array besteht aus den Werten 1, 0, 0.
Richtiger Code:
int arr[3] = {1, 1, 1};
Eine solche Verwirrung kann aufgrund der Ähnlichkeit mit der Konstruktion
arr = {0} auftreten , die das gesamte Array mit Nullen initialisiert.
Wenn solche Konstruktionen in Ihrem Projekt aktiv verwendet werden, können Sie diese Diagnose deaktivieren.
Wir empfehlen außerdem, die Klarheit Ihres Codes nicht zu vernachlässigen.
Beispielsweise wird der Code zum Codieren von Werten einer Farbe wie folgt aufgezeichnet:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff };
Dank der impliziten Initialisierung werden alle Farben korrekt angegeben. Es ist jedoch besser, den Code klarer umzuschreiben:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 };
malloc
Bevor Sie weiterlesen, erinnern Sie sich bitte an den Inhalt des Artikels "
Warum es wichtig ist, zu überprüfen, was die Malloc-Funktion zurückgegeben hat ". Dieser Artikel hat zu vielen Debatten und Kritik geführt. Hier sind einige der Diskussionen:
reddit.com/r/cpp ,
reddit.com/r/C_Programming ,
habr.com (en). Gelegentlich schreiben mir die Leser immer noch eine E-Mail über diesen Artikel.
Der Artikel wird von den Lesern für folgende Punkte kritisiert:
1. Wenn malloc NULL zurückgegeben hat , ist es besser, das Programm sofort zu beenden, als eine Reihe von if -s zu schreiben und zu versuchen, den Speicher irgendwie zu handhaben, weshalb die Programmausführung ohnehin häufig unmöglich ist.Ich habe bis zum Ende nicht darauf gedrängt, mit den Folgen eines Speicherverlusts zu kämpfen, indem ich den Fehler immer höher weitergegeben habe. Wenn es Ihrer Anwendung gestattet ist, ihre Arbeit ohne Vorwarnung zu beenden, lassen Sie es so sein. Zu diesem Zweck reicht bereits eine einzige Überprüfung direkt nach
malloc oder mit
xmalloc aus (siehe nächster Punkt).
Ich protestierte und warnte vor dem Mangel an Überprüfungen, aufgrund derer das Programm weiter funktioniert, als wäre nichts passiert. Es ist ein ganz anderer Fall. Dies ist gefährlich, da es zu undefiniertem Verhalten, Datenkorruption usw. führt.
2. Es gibt keine Beschreibung einer Lösung, die darin besteht, Wrapper-Funktionen zum Zuweisen von Speicher mit einer darauf folgenden Prüfung oder zum Verwenden bereits vorhandener Funktionen wie xmalloc zu schreiben .Ich stimme zu, ich habe diesen Punkt verpasst. Beim Schreiben des Artikels habe ich einfach nicht darüber nachgedacht, wie ich die Situation beheben kann. Für mich war es wichtiger, dem Leser die Gefahr der Scheckabwesenheit zu vermitteln. Wie man einen Fehler behebt, ist eine Frage des Geschmacks und der Implementierungsdetails.
Die
xmalloc- Funktion ist nicht Teil der Standard-C-Bibliothek (siehe "
Was ist der Unterschied zwischen xmalloc und malloc? "). Diese Funktion kann jedoch in anderen Bibliotheken deklariert werden, z. B. in der GNU utils-Bibliothek (
GNU libiberty ).
Der Hauptpunkt der Funktion ist, dass das Programm abstürzt, wenn kein Speicher zugewiesen werden kann. Die Implementierung dieser Funktion könnte wie folgt aussehen:
void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; }
Dementsprechend können Sie durch Aufrufen einer
xmalloc- Funktion anstelle von
malloc jedes Mal sicher sein, dass aufgrund der Verwendung eines Nullzeigers kein undefiniertes Verhalten im Programm auftritt.
Leider ist
xmalloc auch kein Allheilmittel. Man sollte bedenken, dass die Verwendung von
xmalloc beim Schreiben von Bibliothekscode nicht
akzeptabel ist. Ich werde später darüber sprechen.
3. Die meisten Kommentare lauteten wie folgt: "In der Praxis gibt malloc niemals NULL zurück ."Zum Glück bin ich nicht der einzige, der versteht, dass dies der falsche Ansatz ist. Dieser
Kommentar in meiner Unterstützung hat mir sehr gut gefallen:
Nach meiner Erfahrung mit der Diskussion dieses Themas habe ich das Gefühl, dass es im Internet zwei Sekten gibt. Anhänger des ersten glauben fest daran, dass malloc unter Linux niemals NULL zurückgibt. Die Befürworter des zweiten behaupten von ganzem Herzen, dass, wenn in Ihrem Programm kein Speicher zugewiesen werden kann, nichts getan werden kann, Sie nur abstürzen können. Es gibt keine Möglichkeit, sie zu überreden. Besonders wenn sich diese beiden Sekten kreuzen. Sie können es nur als gegeben annehmen. Und es ist sogar nicht wichtig, auf welcher speziellen Ressource eine Diskussion stattfindet.Ich dachte eine Weile nach und beschloss, dem Rat zu folgen, also werde ich nicht versuchen, jemanden zu überzeugen :). Hoffentlich schreiben diese Entwicklergruppen nur nicht tödliche Programme. Wenn zum Beispiel einige Daten im Spiel beschädigt werden, ist nichts Entscheidendes daran.
Das einzige, was zählt, ist, dass Entwickler von Bibliotheken und Datenbanken dies nicht tun dürfen.
Sprechen Sie die Entwickler von stark abhängigem Code und Bibliotheken an
Wenn Sie eine Bibliothek oder einen anderen stark abhängigen Code entwickeln, überprüfen Sie immer den Wert des von der Funktion
malloc / realloc zurückgegebenen Zeigers und geben Sie einen Fehlercode nach außen zurück, wenn der Speicher nicht zugewiesen werden konnte.
In Bibliotheken können Sie die
Exit- Funktion nicht aufrufen, wenn die Speicherzuordnung fehlgeschlagen ist. Aus dem gleichen Grund können Sie
xmalloc nicht verwenden. Für viele Anwendungen ist es nicht akzeptabel, sie einfach abzubrechen. Aus diesem Grund kann beispielsweise eine Datenbank beschädigt werden. Man kann Daten verlieren, die für viele Stunden ausgewertet wurden. Aus diesem Grund kann das Programm zu "Denial-of-Service" -Schwachstellen führen, wenn eine Multithread-Anwendung anstelle einer korrekten Behandlung der wachsenden Arbeitslast einfach beendet wird.
Es kann nicht davon ausgegangen werden, auf welche Weise und in welchen Projekten die Bibliothek genutzt wird. Daher sollte davon ausgegangen werden, dass die Anwendung sehr kritische Aufgaben lösen kann. Deshalb ist es nicht gut, es nur zu töten, indem man den
Ausgang ruft. Höchstwahrscheinlich wird ein solches Programm unter Berücksichtigung der Möglichkeit eines Speichermangels geschrieben und kann in diesem Fall etwas bewirken. Beispielsweise kann ein CAD-System aufgrund der starken Fragmentierung des Speichers keinen geeigneten Speicherpuffer zuweisen, der für den regulären Betrieb ausreicht. In diesem Fall ist es nicht der Grund, im Notfallmodus mit Datenverlust zu quetschen. Das Programm bietet die Möglichkeit, das Projekt zu speichern und sich normal neu zu starten.
In keinem Fall ist es unmöglich, sich auf
malloc zu verlassen, dass es immer in der Lage sein wird, Speicher zuzuweisen. Es ist nicht bekannt, auf welcher Plattform und wie die Bibliothek verwendet wird. Wenn die Situation mit wenig Arbeitsspeicher auf einer Plattform exotisch ist, kann dies auf der anderen Plattform durchaus üblich sein.
Wir können nicht erwarten, dass das Programm abstürzt, wenn
malloc NULL zurückgibt. Alles kann passieren. Wie ich im
Artikel beschrieben habe , schreibt das Programm möglicherweise Daten nicht mit der Nulladresse. Infolgedessen können einige Daten beschädigt werden, was zu unvorhersehbaren Konsequenzen führt. Sogar
Memset ist gefährlich. Wenn das Auffüllen mit Daten in umgekehrter Reihenfolge erfolgt, werden zuerst einige Daten beschädigt, und dann stürzt das Programm ab. Der Absturz kann jedoch zu spät erfolgen. Wenn fehlerhafte Daten in parallelen Threads verwendet werden, während die
Memset- Funktion funktioniert, können die Folgen schwerwiegend sein. Sie können eine beschädigte Transaktion in einer Datenbank erhalten oder Befehle zum Entfernen "unnötiger" Dateien senden. Alles hat eine Chance zu passieren. Ich schlage einem Leser vor, sich auszudenken, was aufgrund der Verwendung von Müll im Speicher passieren könnte.
Somit hat die Bibliothek nur eine korrekte Arbeitsweise mit den
Malloc- Funktionen. Sie müssen SOFORT überprüfen, ob die Funktion zurückgegeben wurde. Wenn sie NULL ist, geben Sie einen Fehlerstatus zurück.
Zusätzliche Links
- OOM-Handhabung
- Spaß mit NULL-Zeigern: Teil 1 , Teil 2
- Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte: Teil 1 , Teil 2 , Teil 3