Blog 5 Min. Lesezeit

Könnte Herb Sutters Aufruf zu mehr C++-Sicherheit bald umgesetzt werden?

Diesen Artikel teilen
Could Herb Sutter's call to action for C++ safety be achieved soon?

Kürzlich veröffentlichte Herb Sutter einen ausgezeichneten Artikel über C++-Sicherheit. Er erörterte zahlreiche Ideen; hier fasse ich seine Sicht darauf zusammen, was mittelfristig getan werden kann, um die Sicherheit von C++ zu verbessern.

In C++ standardmäßig durchsetzen …(A) Lösung für neuen/aktualisierten Code (kann Codeänderungen erfordern — keine Link-/Binäränderungen)(B) Lösung für bestehenden Code (erfordert nur Neukompilierung — keine manuellen Codeänderungen, keine Link-/Binäränderungen)
TypsicherheitAlle grundsätzlich unsicheren Casts und Konvertierungen verbietenBei unsicheren Casts und Konvertierungen mit sicherer Alternative automatisch die sichere Variante verwenden
GrenzensicherheitZeigerarithmetik verbieten; ungeprüfte Iteratorarithmetik verbietenBei jeder erlaubten Iteratorarithmetik Grenzen prüfen; bei allen Indexzugriffen Grenzen prüfen
InitialisierungssicherheitVerlangen, dass alle Variablen initialisiert werden (bei der Deklaration oder vor der ersten Verwendung)
LebensdauersicherheitViele häufige Lebensdauerfehler bei Zeigern/Iteratoren statisch diagnostizierenBei jeder Zeigerdereferenzierung auf Nicht-Null prüfen
Weniger undefiniertes VerhaltenBekannte Fälle von undefiniertem Verhalten (UB) bzw. Bugs statisch diagnostizieren, sodass echte Fehler in bestehendem Code allein durch Neukompilierung und ohne False Positives erkannt werden:
Mathematisch ungültige Vergleichsketten verbieten
(weitere Fälle aus der Überprüfung des UB-Anhangs ergänzen)
Bekannte UB-/Bug-Fälle automatisch korrigieren, sodass bestehende Fehler allein durch Neukompilierung und ohne False Positives tatsächlich behoben werden:
Mathematisch gültige Vergleichsketten definieren
Bei C-Zuweisungsoperatoren, die C& zurückgeben, standardmäßig return *this; verwenden
(weitere Fälle aus der Überprüfung des UB-Anhangs ergänzen)

Doch welche Möglichkeiten stehen derzeit zur Verfügung, um dieses Ziel zu erreichen?

Typsicherheit

Um Typsicherheit in C++ durchzusetzen, können Sie den folgenden Kompilierbefehl mit verschiedenen Flags verwenden, die strenge Typprüfungen und weitere Sicherheitsfunktionen aktivieren:

g++ -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Werror  -o output_file source_file.cpp

Hier eine Übersicht darüber, was diese Flags bewirken:

  • -Wall: Aktiviert alle gängigen Warnungen.
  • -Wextra: Aktiviert zusätzliche Warnungen.
  • -Wpedantic: Erzwingt die strikte Einhaltung des ISO-C++-Standards.
  • -Wconversion: Warnt vor impliziten Konvertierungen, die einen Wert verändern können.
  • -Wsign-conversion: Warnt vor impliziten Konvertierungen zwischen vorzeichenbehafteten und vorzeichenlosen Typen.
  • -Wshadow: Warnt, wenn eine Variablendeklaration eine Variable aus einem äußeren Gültigkeitsbereich verdeckt.
  • -Werror: Behandelt alle Warnungen als Fehler und stoppt die Kompilierung, sobald Warnungen auftreten.

Grenzensicherheit

Um Grenzensicherheit in C++ durchzusetzen, können Sie die Option -fsanitize=bounds mit gcc oder clang. Dieses Flag aktiviert Laufzeitprüfungen für Arrayzugriffe außerhalb der Grenzen. Hier ein Beispiel für einen Kompilierbefehl:

g++ -fsanitize=bounds -o my_program my_program.cpp

Dieser Befehl kompiliert my_program.cpp mit aktivierten Grenzprüfungen und erzeugt eine ausführbare Datei namens my_program. Wenn während der Ausführung ein Zugriff außerhalb der Grenzen erfolgt, meldet der Sanitizer diesen.

Initialisierungssicherheit

Für Initialisierungssicherheit in C++ können Sie mehrere Compiler-Flags verwenden, die nicht initialisierte Variablen und verwandte Probleme erkennen. Hier sind die Befehle für GCC und Clang:

GCC
g++ -Wall -Wextra -Wuninitialized -Wmaybe-uninitialized -o your_program your_program.cpp
Clang
clang++ -Wall -Wextra -Wuninitialized -o your_program your_program.cpp
Erläuterung
  • -Wall: Aktiviert alle häufig verwendeten Warnmeldungen.
  • -Wextra: Aktiviert zusätzliche Warn-Flags, die nicht durch -Wall.
  • -Wuninitialized: Warnt vor nicht initialisierten Variablen.
  • -Wmaybe-uninitialized: Warnt vor Variablen, die möglicherweise nicht initialisiert sind.

Lebensdauersicherheit

Um Lebensdauersicherheit in C++ durchzusetzen, benötigen Sie spezielle Compiler-Flags und gegebenenfalls Funktionen oder Werkzeuge für Lebensdaueranalyse und Sicherheitsprüfungen. Nach aktuellem Stand kann das -fsanitize=address Flag mit Clang oder GCC helfen, Lebensdauerprobleme zu erkennen. Für umfassendere Prüfungen können Sie zusätzlich den statischen Analyzer von Clang oder Microsofts Werkzeuge in Visual Studio verwenden.

Hier sind einige Befehle für GCC und Clang:

GCC
g++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp
Clang
clang++ -fsanitize=address -fno-omit-frame-pointer -g -o your_program your_program.cpp

Diese Befehle aktivieren AddressSanitizer, der verschiedene Speicherfehler erkennt, darunter Use-after-free, Use-after-return und Use-after-scope. Solche Prüfungen sind für die Lebensdauersicherheit besonders wichtig.

Undefiniertes Verhalten

Um C++ besser gegen undefiniertes Verhalten abzusichern, können Sie verschiedene Compiler-Flags und Werkzeuge einsetzen. Hier ein Kompilierbefehl mit g++ (Teil der GNU Compiler Collection), der gängige Flags zur Erkennung und Vermeidung undefinierten Verhaltens enthält:

g++ -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined -fstack-protector-all -O2 -g your_file.cpp -o your_program

Hier eine Übersicht der verwendeten Flags:

  • -Wall: Aktiviert alle häufig verwendeten Warnmeldungen.
  • -Wextra: Aktiviert zusätzliche Warnmeldungen, die nicht in -Wall.
  • -Werror: Behandelt alle Warnungen als Fehler, sodass sie behoben werden müssen.
  • -pedantic: Erzwingt die strikte Einhaltung des ISO-C++-Standards.
  • -fsanitize=address,undefined: Aktiviert AddressSanitizer und UndefinedBehaviorSanitizer, um Speicherfehler und undefiniertes Verhalten zur Laufzeit zu erkennen.
  • -fstack-protector-all: Fügt Stack-Schutz hinzu, um Stack-Pufferüberläufe zu erkennen.
  • -O2: Aktiviert Optimierungsstufe 2, einen guten Kompromiss zwischen Leistung und Debugging.
  • -g: Fügt Debug-Informationen für die Verwendung mit einem Debugger hinzu (z. B. gdb).

Probleme mit C++-Sanitizern

Wie wir sehen, können Sanitizer einige der von Herb Sutter genannten Probleme adressieren. Sie haben jedoch auch mehrere Nachteile:

1 – Performance-Overhead

  • Laufzeitleistung: Sanitizer verursachen einen erheblichen Laufzeit-Overhead. AddressSanitizer kann die Ausführung eines Programms beispielsweise um den Faktor zwei oder drei verlangsamen.
  • Speicherverbrauch: Sanitizer, insbesondere AddressSanitizer, erhöhen den Speicherverbrauch deutlich. In Umgebungen mit knappem Speicher kann dies problematisch sein.

2 – Kompatibilitätsprobleme

  • Plattformunterstützung: Nicht alle Sanitizer stehen auf allen Plattformen oder Compilern zur Verfügung. Das kann ihren Einsatz in plattformübergreifenden Projekten einschränken.
  • Drittanbieterbibliotheken: Die Verwendung von Sanitizern mit Drittanbieterbibliotheken, die nicht dafür kompiliert wurden, kann zu Kompatibilitätsproblemen oder irreführenden Fehlermeldungen führen.

3 – Komplexität von Build und Ausführung

  • Komplexität des Build-Prozesses: Die Integration von Sanitizern kann die Build-Konfiguration komplexer machen, insbesondere bei mehreren Build-Typen (z. B. Release und Debug).
  • Spezielle Ausführungsumgebung: Tests unter Sanitizern erfordern häufig eine spezielle Ausführungsumgebung und zusätzliche Einrichtung, was den Prozess aufwendiger macht.

4 – Begrenzter Umfang

  • Bestimmte Fehlertypen: Sanitizer sind darauf ausgelegt, bestimmte Fehlertypen zu erkennen (z. B. Speicherfehler und undefiniertes Verhalten). Logikfehler, algorithmische Ineffizienzen oder andere Fehlerarten erkennen sie möglicherweise nicht.

5 – Komplexität beim Debugging

  • Detaillierte Berichte: Detaillierte Sanitizer-Berichte sind hilfreich, können bei großen und komplexen Codebasen jedoch überwältigend oder schwer zu interpretieren sein.
  • Reproduzierbarkeit: Manche von Sanitizern gemeldeten Probleme können nicht deterministisch und schwer reproduzierbar sein, was das Debugging erschwert.

Es ist klar, dass zusätzliche Mechanismen nötig sind, um die Sicherheit zu verbessern, ohne die Leistung von C++ zu beeinträchtigen. Dieses Ziel hat für das C++-Komitee hohe Priorität, und wir hoffen, dass es bald eine definitive Lösung für dieses dringende Problem geben wird.

Diesen Artikel teilen