Sechster Chromcheck, Nachwort

Published on February 04, 2019

Sechster Chromcheck, Nachwort

    strenges Einhorn

    Anfang 2018 erschien eine Reihe von Artikeln in unserem Blog, die der sechsten Überprüfung des Quellcodes des Chromium-Projekts gewidmet waren. Der Zyklus enthält 8 Artikel zu Fehlern und Empfehlungen zu deren Prävention. Zwei Artikel lösten eine heftige Diskussion aus, und bis jetzt habe ich gelegentlich Kommentare zu den darin behandelten Themen erhalten. Vielleicht sollten wir einige zusätzliche Erklärungen geben und, wie sie sagen, das i ankreuzen.

    Ein Jahr ist vergangen, seit die Artikelserie geschrieben wurde, die der nächsten Überprüfung des Quellcodes des Chromium-Projekts gewidmet ist:

    1. Chrom: die sechste Projektprüfung und 250 Fehler
    2. Wunderschönes Chrom und knorriges Memset
    3. brechen und durchfallen
    4. Chrom: Speicherlecks
    5. Chrom Tippfehler
    6. Chrom: Verwendung unzuverlässiger Daten
    7. Warum ist es wichtig zu überprüfen, ob die Malloc-Funktion zurückgegeben wurde?
    8. Chrom: andere Fehler

    Artikel auf Memset und malloc verursacht und weiterhin Debatte führen, die mir fremd zu sein scheinen. Anscheinend gab es ein Missverständnis, weil ich meine Gedanken nicht klar formuliert hatte. Ich beschloss, zu diesen Artikeln zurückzukehren und einige Erklärungen abzugeben.

    memset


    Beginnen wir mit dem Artikel über memset , da hier alles einfach ist. Es kam zu Streitigkeiten darüber, wie Strukturen am besten initialisiert werden können. Viele Programmierer haben geschrieben, dass es besser ist, eine Empfehlung zu geben, nicht zu schreiben:

    HDHITTESTINFO hhti = {};

    Und so:

    HDHITTESTINFO hhti = { 0 };

    Argumente:

    1. Das {0} -Konstrukt ist beim Lesen eines Codes leichter zu erkennen als {}.
    2. Das {0} -Konstrukt ist intuitiver als {}. Das heißt, 0 legt nahe, dass die Struktur mit Nullen gefüllt ist.

    Dementsprechend wird mir angeboten, dieses Initialisierungsbeispiel im Artikel zu ändern. Ich bin mit den Argumenten nicht einverstanden und plane keine Änderungen am Artikel. Jetzt begründe ich meine Position.

    Über Sichtbarkeit. Ich denke, das ist eine Frage des Geschmacks und der Gewohnheit. Ich glaube nicht, dass das Vorhandensein von 0 in den geschweiften Klammern die Situation grundlegend verändert.

    Aber mit dem zweiten Argument stimme ich nicht überein. Das Schreiben des Formulars {0} führt zu einer Fehlinterpretation des Codes. Sie können beispielsweise berechnen, dass, wenn Sie 0 durch 1 ersetzen, alle Felder mit Einsen initialisiert werden. Daher ist diese Schreibweise eher schädlich als nützlich.

    Im PVS-Studio-Analyzer gibt es zu diesem Thema sogar eine zugehörige Diagnose des V1009 , die ich jetzt beschreiben werde.

    V1009. Überprüfen Sie die Array-Initialisierung. Nur das erste Element wird explizit initialisiert.

    Der Analysator hat einen möglichen Fehler festgestellt, der damit zusammenhängt, dass beim Deklarieren eines Arrays nur für ein Element ein Wert angegeben wurde. Somit werden die verbleibenden Elemente implizit mit Null oder dem Standardkonstruktor initialisiert.

    Betrachten Sie ein Beispiel für einen verdächtigen Code:

    int arr[3] = {1};

    Vielleicht hat der Programmierer erwartet, dass arr aus einer Einheit besteht, aber das ist nicht der Fall. Das Array besteht aus den Werten 1, 0 und 0. Der

    richtige Code lautet:

    int arr[3] = {1, 1, 1};

    Eine solche Verwirrung kann aufgrund der Ähnlichkeit mit dem Konstrukt arr = {0} auftreten , das das gesamte Array auf Null initialisiert.

    Wenn solche Entwürfe in Ihrem Projekt aktiv verwendet werden, können Sie diese Diagnose deaktivieren.

    Es wird auch nicht empfohlen, die Sichtbarkeit des Codes zu vernachlässigen.

    Der Code zum Codieren von Farbwerten lautet beispielsweise wie folgt:

    int White[3] = { 0xff, 0xff, 0xff };
    int Black[3] = { 0x00 };
    int Green[3] = { 0x00, 0xff };

    Aufgrund der impliziten Initialisierung sind alle Farben korrekt eingestellt, 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 ich weiter lese, bitte ich Sie, den Inhalt des Artikels " Warum es wichtig ist, zu überprüfen, ob die Malloc-Funktion zurückgegeben wurde " zu aktualisieren . Dieser Artikel hat viele Diskussionen und Kritiker hervorgebracht. Hier sind einige der Diskussionen: reddit.com/r/cpp , reddit.com/r/C_Programming , habr.com . Gelegentlich schreiben sie mir über diesen Artikel in der Post und jetzt.

    Der Artikel kritisierte die Leser auf die folgenden Punkte:

    1. Wenn malloc zurück eine NULL , es besser ist , das Programm zu beenden als ein Bündel zu schreiben , wenn weibliche und versuchen , den Mangel an Speicher irgendwie handhaben , aufgrund derer das Programm ist oft noch nicht möglich.

    Ich habe bis vor kurzem überhaupt nicht versucht, die Folgen des Speichermangels zu bekämpfen, indem ich den Fehler immer weiter verschickte. Wenn die Anwendung die Arbeit ohne Vorwarnung abschließen darf, lassen Sie es sein. Alles, was benötigt wird, ist eine einzelne Überprüfung direkt nach malloc oder mit xmalloc (siehe nächster Absatz).

    Ich protestierte und warnte vor dem Fehlen von Überprüfungen, weshalb das Programm weiterhin funktioniert, "als ob nichts passiert wäre". Das ist ganz anders. Dies ist gefährlich, da dies zu undefiniertem Verhalten, Beschädigung von Daten usw. führt.

    2. Keine Angabe zu der Lösung, die darin besteht, eine Wrapper-Funktion für die Speicherzuweisung zu schreiben und anschließend vorhandene Funktionen wie xmalloc zu testen oder zu verwenden .

    Ich stimme zu, ich habe diesen Moment verpasst. Beim Schreiben eines Artikels habe ich einfach nicht darüber nachgedacht, wie ich die Situation korrigieren könnte. Es war mir wichtiger, dem Leser die Gefahr einer mangelnden Verifikation zu vermitteln. Wie man einen Fehler korrigiert, ist bereits 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, beispielsweise in der GNU-Utils-Bibliothek ( GNU libiberty ).

    Das Wesentliche der Funktion ist, dass das Programm beendet wird, wenn der Speicher nicht zugewiesen werden kann. Die Implementierung dieser Funktion könnte folgendermaßen 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;
    }

    Wenn Sie also anstelle von malloc überall xmalloc aufrufen , können Sie sicher sein, dass das Programm aufgrund der Verwendung des Nullzeigers kein undefiniertes Verhalten aufweist.

    Leider ist xmalloc auch kein Allheilmittel gegen alle Krankheiten. Es muss beachtet werden, dass die Verwendung von xmalloc beim Schreiben von Codebibliotheken nicht akzeptabel ist . Ich werde etwas später darüber sprechen.

    3. Die meisten Kommentare hatten die folgende Form: „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. Mir hat dieser Kommentar in meiner Unterstützung sehr gut gefallen :

    Aus der Erfahrung mit der Diskussion eines solchen Themas geht hervor, dass es im Internet zwei Sekten gibt. Die Anhänger des ersten sind fest davon überzeugt, dass malloc unter Linux niemals NULL zurückgibt. Die Anhänger des Zweiten sind fest davon überzeugt, dass man einfach fallen muss, wenn der Speicher im Programm nicht zugewiesen werden konnte, aber im Prinzip nichts getan werden kann. Sie können sie in keiner Weise überzeugen. Besonders wenn sich diese beiden Sekten kreuzen. Sie können es nur als selbstverständlich ansehen. Dabei spielt es keine Rolle, um welche Profilressource es sich bei der Diskussion handelt.

    Ich dachte und beschloss, dem Rat zu folgen und werde nicht versuchen, zu überzeugen :). Hoffentlich schreiben diese Entwicklungsteams nur unkritische Programme. Wenn zum Beispiel einige Daten im Spiel durcheinander geraten oder das Spiel abstürzt, ist das nicht beängstigend.

    Wichtig ist nur, dass die Entwickler von Bibliotheken, Datenbanken usw. dies nicht tun.

    Appell an die Entwickler von Bibliotheken und verantwortlichem Code


    Wenn Sie eine Bibliothek oder einen anderen verantwortlichen Code entwickeln, überprüfen Sie immer den Wert des von der Funktion malloc / realloc zurückgegebenen Zeigers und geben Sie den Fehlercode nach außen zurück, wenn der Speicher nicht zugeordnet werden konnte.

    In Bibliotheken können Sie die Exit- Funktion nicht aufrufen, wenn die Speicherzuweisung fehlgeschlagen ist. Aus dem gleichen Grund kann xmalloc nicht verwendet werden . Für viele Anwendungen ist es nicht akzeptabel, sie einfach zu nehmen und zum Absturz zu bringen. Aus diesem Grund ist beispielsweise die Datenbank möglicherweise beschädigt. Daten, die seit vielen Stunden berücksichtigt wurden, können verloren gehen. Aus diesem Grund kann es zu Denial-of-Service-Schwachstellen im Programm kommen, wenn die Arbeit einer Multithread-Anwendung einfach beendet wird, anstatt die steigende Last irgendwie richtig zu verarbeiten.

    Es ist unmöglich anzunehmen, wie und in welchen Projekten die Bibliothek verwendet wird. Daher ist davon auszugehen, dass die Anwendung sehr wichtige Aufgaben lösen kann. Und "töte" ihn einfach, indem du exit rufst , nicht gut. Höchstwahrscheinlich wird ein solches Programm unter Berücksichtigung der Möglichkeit eines Speichermangels geschrieben und kann in diesem Fall etwas tun. Beispielsweise kann ein bestimmtes CAD-System aufgrund einer starken Speicherfragmentierung nicht genügend Speicherpuffer für die nächste Operation zuweisen. Dies ist jedoch kein Grund, im Notfallmodus mit Datenverlust zu enden. Das Programm kann die Möglichkeit geben, das Projekt zu speichern und sich selbst im normalen Modus neu zu starten.

    In keinem Fall kann man sich darauf verlassen, dass mallockann immer Speicher zuweisen. Es ist nicht bekannt, auf welcher Plattform und wie die Bibliothek verwendet wird. Wenn auf einer Plattform der Speichermangel exotisch ist, kann dies auf der anderen eine sehr häufige Situation sein.

    Sie können nicht hoffen, dass das Programm abstürzt , wenn malloc NULL zurückgibt . Alles kann passieren. Wie ich in dem Artikel beschrieben habe , kann das Programm überhaupt keine Daten mit der Adresse Null schreiben. Infolgedessen können einige Daten beschädigt werden, was zu unvorhersehbaren Konsequenzen führt. Auch memset ist gefährlich. Wenn die Daten in umgekehrter Reihenfolge eingegeben werden, werden zunächst einige Daten beschädigt, und erst dann fällt das Programm aus. Aber der Herbst kann zu spät kommen. Wenn zum Zeitpunkt der Memset- FunktionIn parallelen Streams werden beschädigte Daten verwendet, die Folgen können schwerwiegend sein. Sie können eine beschädigte Transaktion in der Datenbank abrufen oder einen Befehl senden, um "unnötige" Dateien zu löschen. Alles kann passieren. Ich schlage dem Leser vor, sich selbst zu überlegen, wozu die Verwendung von Müll im Gedächtnis führen könnte.

    Somit hat die Bibliothek nur eine korrekte Arbeitsweise mit Malloc- Funktionen . Es muss sofort überprüft werden, ob die Funktion zurückgegeben wurde. Wenn sie NULL ist, muss der Fehlerstatus zurückgegeben werden.

    Zusätzliche Links


    1. OOM-Behandlung .
    2. Spaß mit Nullzeigern: Teil 1 , Teil 2 .
    3. Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte: Teil 1 , Teil 2 , Teil 3 .



    Wenn Sie diesen Artikel mit einem englischsprachigen Publikum teilen möchten, verwenden Sie bitte den Link zur Übersetzung: Andrey Karpov. Sechster Chromcheck, Nachwort .