Fehlersuche, undefiniertes Verhalten und sicheres C

Lerne, Programmfehler zu finden und zu verhindern, durch die C-Programme abstürzen, falsche Ergebnisse liefern oder unsicher werden. Aktiviere Compilerwarnungen und lies, was sie bedeuten, verfolge Abstürze und falsche Werte in gdb oder lldb Schritt für Schritt zurück und erkenne undefiniertes Verhalten bei Arithmetik, Zeigern und Speicher. Prüfe deine Annahmen mit Zusicherungen, erkenne Speicherfehler und undefiniertes Verhalten mit AddressSanitizer und UndefinedBehaviorSanitizer und schreibe sichereren Code, indem du Regeln für den sicheren Umgang mit Zeichenketten, Eingaben und Fehlern befolgst. Wäge schließlich ab, was die fehlende Speichersicherheit von C kostet und welche Kontrolle C dir dafür gibt.

AnfängerEinheit 8 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C

Was du lernst

  1. Aktiviere die Warnungen des Compilers, lies seine Meldungen und wähle Erstellungseinstellungen, die Programmfehler früh sichtbar machen, bevor das Programm überhaupt läuft.
    • Warnungen aktivieren und Compilermeldungen lesen
      Kompiliere mit einer strengen Auswahl an Warnoptionen, lies jede Compilermeldung Zeile für Zeile und behebe die Ursache, statt die Meldung zu unterdrücken.
    • Erstellungseinstellungen für Fehlersuche, Tests und Veröffentlichung auswählen
      Verwende getrennte Debug- und Release-Builds, lerne die Wirkung von -g und den Optimierungsstufen kennen und ergänze statische Analyse und Optionen zur Härtung an geeigneter Stelle.
  2. Verfolge Programmfehler methodisch zurück: Reproduziere und isoliere das Fehlverhalten und halte das Programm dann mit gdb oder lldb an, durchlaufe es schrittweise, prüfe Werte und finde heraus, woher ein Absturz oder falscher Wert tatsächlich kommt.
    • Methodisch nach Fehlern suchen: reproduzieren, eingrenzen und eine Hypothese prüfen
      Beschreibe ein zunächst unklar beobachtetes Fehlverhalten genau, lies die Absturzmeldung und grenze die Ursache mit kleinen, kontrollierten Versuchen ein.
    • Ein Programm in gdb oder lldb ausführen, anhalten und schrittweise durchlaufen
      Starte ein Programm unter gdb oder lldb, setze Haltepunkte, durchlaufe es Zeile für Zeile und prüfe Variablen und Arrays, während das Programm angehalten ist.
    • Abstürze und unerwartete Werte untersuchen
      Nutze nach einem Absturz den Aufrufstapel, wechsle zwischen Stackframes und finde mit Datenhaltepunkten, bedingten Haltepunkten und Speicherabbildern heraus, woher ein fehlerhafter Wert kommt.
  3. Erkläre, was undefiniertes Verhalten ist und warum es C-Programme unberechenbar macht, und erkenne seine häufigsten Ursachen bei Arithmetik, Ausdrücken, Zeigern und Speicher.
    • Verstehen, was undefiniertes Verhalten bedeutet
      Unterscheide undefiniertes, implementierungsdefiniertes und nicht spezifiziertes Verhalten und erkläre, warum Code mit undefiniertem Verhalten scheinbar funktionieren und dann fehlschlagen kann.
    • Undefiniertes Verhalten in arithmetischen Operationen und Ausdrücken erkennen
      Erkenne Überläufe bei vorzeichenbehafteten Ganzzahlen, fehlerhafte Divisionen und Bitverschiebungen, Konvertierungen außerhalb des Wertebereichs, nicht sequenzierte Seiteneffekte und Lesezugriffe auf nicht initialisierte Werte und schreibe Prüfungen, die sie verhindern.
    • Undefiniertes Verhalten beim Umgang mit Zeigern, Arrays und Speicher erkennen
      Erkenne Zugriffe außerhalb der Speichergrenzen, fehlerhafte Zeichenketten, Nullzeiger und hängende Zeiger, Zugriffe nach der Speicherfreigabe und falsche Allokationsgrößen und kenne jeweils das sichere Vorgehen.
  4. Lass ein Programm während der Ausführung seine eigenen Annahmen prüfen: Formuliere sie als Zusicherungen und erstelle Testversionen mit Sanitizern, die bei Speicherfehlern und undefiniertem Verhalten anhalten und genau melden, wo diese aufgetreten sind.
    • Annahmen durch Zusicherungen ausdrücken
      Verwende assert für Bedingungen, die nur durch einen Programmfehler verletzt werden können, verstehe die Wirkung von NDEBUG und trenne Zusicherungen von der Behandlung erwarteter Fehler.
    • Speicherfehler mit AddressSanitizer finden
      Erstelle eine Testversion mit AddressSanitizer, lies seine Fehlerberichte und lerne, was er findet, was er übersieht und welchen Aufwand er verursacht.
    • Undefiniertes Verhalten mit UBSan erkennen und einen regelmäßigen Prüfablauf etablieren
      Verwende UndefinedBehaviorSanitizer und lerne die anderen Sanitizer kennen. Kombiniere anschließend Warnungen, Sanitizer und Tests zu einem Ablauf, der Fehler früh erkennt.
  5. Wende Regeln für sichere Programmierung wie die SEI-CERT-C-Regeln auf Zeichenketten, Puffer, Eingaben und Fehler an und wäge ab, was die fehlende Speichersicherheit von C kostet und was C dafür bietet.
    • Einen Standard für sichere Programmierung anwenden
      Erkläre den Aufbau des SEI CERT C Coding Standard, lies eine Regel mit ihren Beispielen und lerne die zentralen Regeln kennen, die die häufigsten C-Programmfehler verhindern.
    • Sicher mit Zeichenketten und Puffern umgehen
      Kenne stets die Größe eines Puffers, lies und kopiere Zeichenketten mit Funktionen, die diese Größe beachten, und erkenne abgeschnittene Ergebnisse.
    • Eingaben auf Zulässigkeit prüfen und Fehler behandeln
      Behandle alle externen Daten als nicht vertrauenswürdig, konvertiere Text mit vollständiger Fehlerprüfung in Zahlen und prüfe das Ergebnis jedes Bibliotheksaufrufs, der fehlschlagen kann.
    • Zielkonflikte bei der Speichersicherheit abwägen
      Erkläre, was Speichersicherheit ist, warum C sie nicht bietet, was das kostet und was C dafür bietet und wie mehrere Schutzebenen und speichersichere Programmiersprachen das Risiko verringern.

Fehlersuche in C einfach erklärt

5 Kapitel

1

Den Compiler zur Fehlersuche nutzen

Der Compiler kann dich auf viele Probleme hinweisen, bevor du ein Programm ausführst. Lerne, seine Meldungen als Hinweise auf deinen Code zu lesen, und wähle dann Optionen, die mehr von dem sichtbar machen, was wichtig ist. Debuginformationen, Optimierung, statische Analyse und Härtung für die Veröffentlichung erfüllen unterschiedliche Aufgaben. Wenn du diese Aufgaben auseinanderhältst, kannst du ein Programm erstellen, das du während der Entwicklung untersuchen und in der Form erneut testen kannst, in der es deine Nutzer ausführen.

1.1 Warnungen aktivieren und Compilermeldungen lesen

Warnungen geben dir die Gelegenheit, einen Defekt zu beheben, bevor eine Eingabe ihn auslöst. Ein Fehler verhindert eine erfolgreiche Erstellung. Eine Warnung lässt die Erstellung weiterlaufen, meldet aber etwas, das du verstehen musst: verdächtigen gültigen Code, mögliches undefiniertes Verhalten oder einen Verstoß gegen eine Sprachregel, den der Compiler akzeptiert.

Nützliche Prüfungen auswählen

Starte GCC oder Clang mit einer ausdrücklich gewählten Sprachversion und einer breiten Auswahl an Warnungen:

clang -std=c17 -Wall -Wextra -Wpedantic -g program.c -o program

-Wall aktiviert viele gängige Prüfungen, nicht alle Warnungen. -Wextra ergänzt weitere, darunter Vergleiche zwischen vorzeichenbehafteten und vorzeichenlosen Werten in C. -Wpedantic meldet viele Abweichungen vom gewählten ISO-Standard; es ist kein vollständiger Test der Standardkonformität. /W4 von MSVC erfüllt unter Windows einen ähnlichen Zweck.

Ergänze Prüfungen, die zu den Risiken in deinem Code passen:

OptionWas sie aufdecken hilft
-WconversionImplizite Typumwandlungen, die einen Wert verändern können
-WshadowEine lokale Deklaration, die eine andere Variable verdeckt
-Wformat=2Zusätzliche Prüfungen von Formatstrings
-WerrorAktivierte Warnungen lassen die Erstellung fehlschlagen

Zum Beispiel verwirft double price = 3.7; int whole = price; den Nachkommateil. -Wconversion macht diese Umwandlung sichtbar. Wenn das Abschneiden beabsichtigt und der Wert darstellbar ist, drückt (int)price diese Entscheidung aus. Wenn der Nachkommateil wichtig ist, behalte einen Gleitkommatyp. Für das Runden auf die nächste Ganzzahl erfüllt lround aus <math.h> eine andere Aufgabe als eine explizite Typumwandlung.

Die erste hilfreiche Meldung lesen

Eine Compilermeldung nennt normalerweise eine Stelle im Quelltext, eine Meldung und die Warnoption. Ein Zirkumflex markiert den betreffenden Ausdruck; nachfolgende Hinweise können eine Deklaration nennen oder eine Korrektur vorschlagen. Lies die gesamte Gruppe. Wenn ein Syntaxfehler viele spätere Meldungen verursacht, behebe den ersten Fehler und kompiliere erneut, bevor du den übrigen nachgehst.

Angenommen, ein Programm enthält diese drei Zeilen, wobei average als double deklariert ist:

if (count = 0) { /* report no data */ }
printf("%d\n", average);
int unused;

Die Zuweisung in der Bedingung setzt count auf null und macht die Bedingung damit falsch. War ein Vergleich beabsichtigt, verwende count == 0. Zusätzliche Klammern um die Zuweisung können eine Warnung unterdrücken, während der Programmfehler bestehen bleibt.

Das Format %d erwartet einen int, aber das Argument ist ein double. Verwende %f; ein unpassendes Format ist undefiniertes Verhalten und keine Aufforderung, den Wert umzuwandeln. Lösche die ungenutzte Deklaration, wenn sie keinen Zweck erfüllt. Das sind drei unterschiedliche Ursachen, die jeweils eine eigene Korrektur brauchen.

Den Hinweisen folgen

Achte besonders auf nicht initialisierte Werte, fehlende Rückgaben, zurückgegebene Adressen lokaler Variablen und sizeof bei Arrayparametern. Durch eine fehlende Headerdatei kann eine Funktion undeklariert bleiben; aktuelle Versionen von GCC und Clang weisen dies standardmäßig zurück. Füge nicht einfach eine explizite Typumwandlung hinzu, damit eine Meldung verschwindet. Erkläre zuerst, welche Typen und Werte beabsichtigt sind, und bringe diese Absicht dann im Code zum Ausdruck.

Ein Programm ohne Warnungen kann trotzdem eine fehlerhafte Schleifenbedingung oder einen fehlerhaften Index aus einer Eingabe enthalten. Manche Warnungen hängen außerdem von Optimierung und Datenflussanalyse ab, und verschiedene Compiler erkennen unterschiedliche Fälle. Lass die Warnungen bei gewöhnlichen Erstellungsvorgängen aktiviert und untersuche anschließend das Verhalten zur Laufzeit mit dem Debugger und Testwerkzeugen.

Wichtige Erkenntnisse

  • Wähle die C-Version ausdrücklich.
  • -Wall aktiviert nicht jede Warnung.
  • Behebe die Ursache einer Compilermeldung, bevor du sie unterdrückst.
  • Eine Formatkonvertierung muss zum Argumenttyp passen.
  • Auch nach einer Erstellung ohne Meldungen sind Laufzeittests nötig.

1.2 Erstellungseinstellungen für Fehlersuche, Tests und Veröffentlichung auswählen

Derselbe Quelltext kann für eine einfache Untersuchung, intensive Tests oder eine effiziente Veröffentlichung erstellt werden. Die Erstellungseinstellungen bestimmen, welche Informationen du sehen kannst und welche zusätzlichen Prüfungen ausgeführt werden. Gib jedem Build einen klaren Zweck.

Quelltextinformationen erhalten

Die Option -g ergänzt Namen, Typen und Zuordnungen zwischen Maschinenbefehlen und Quelltextzeilen. Damit kann ein Debugger deinen Code anzeigen, und Sanitizerberichte können Stellen im Quelltext nennen. Die Option aktiviert weder Laufzeitprüfungen noch deaktiviert sie die Optimierung. Ohne Debuginformationen können manche Funktionsnamen verfügbar bleiben, aber Quelltextzeilen und lokale Variablen lassen sich viel schwerer ermitteln.

Die Optimierung verändert, wie eng die Ausführung der Reihenfolge im Quelltext folgt:

EinstellungÜblicher ZweckWas du sehen kannst
-O0Einfache FehlersucheDie Reihenfolge im Quelltext lässt sich gewöhnlich am leichtesten verfolgen
-OgFehlersuche mit leichter OptimierungEinige Umformungen bleiben erhalten
-O2Effizienter Release-BuildUmgeordnete Arbeitsschritte und entfernte Variablen

Bei einer Debuggersitzung mit optimiertem Code können Zeilen übersprungen werden oder eine Variable als „“ angezeigt werden. Der Compiler hat das Verhalten von definiertem Code erhalten, muss aber nicht mehr jeden Zwischenwert aus dem Quelltext beibehalten. -O0 übersetzt nicht buchstäblich jede C-Zeile in genau einen Maschinenbefehl.

Für den jeweiligen Zweck erstellen

Ein einfacher Debug-Befehl lautet:

gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 program.c -o program-debug

Ein Test-Build ergänzt Sanitizer. Ein Release-Build verwendet gewöhnlich -O2 und kann NDEBUG definieren, um Zusicherungen zu entfernen. Gewöhnliche Eingabeprüfungen und die Fehlerbehandlung müssen aktiv bleiben, wenn Zusicherungen entfallen. Teste auch die Release-Konfiguration: Ein Problem, das nur bei Optimierung sichtbar wird, ist ein starkes Anzeichen dafür, nach undefiniertem Verhalten zu suchen, einschließlich Lesezugriffen auf nicht initialisierte Werte und Überläufen bei vorzeichenbehafteten Ganzzahlen.

Halte gemeinsam verwendete Optionen in einem Makefile oder einer anderen Erstellungskonfiguration fest. Getrennte Ausgabenamen verhindern, dass du versehentlich eine alte ausführbare Release-Datei startest, während du gerade bearbeiteten Quelltext untersuchst. Erstelle nach jeder Quelltextänderung die ausführbare Datei erneut, die du ausführen möchtest.

Analyse und Härtung ergänzen

Die statische Analyse verfolgt mögliche Pfade durch den Quelltext, ohne das Programm auszuführen. Probiere gcc -fanalyzer oder clang --analyze; scan-build, clang-tidy und cppcheck bieten weitere Analyseverfahren. Ein Analysewerkzeug kann bemerken, dass ein Fehlerzweig einen Zeiger zweimal freigibt, selbst wenn deine Tests diesen Zweig nie durchlaufen. Prüfe seine Begründung: Die Analyse möglicher Pfade kann Fehlalarme erzeugen.

Die dynamische Analyse prüft die tatsächliche Ausführung. Sanitizer und Valgrind sehen nur die Pfade, die du ausführst, sodass ihre Abdeckung von deinen Eingaben abhängt. Kombiniere beide Arten der Analyse.

Die Härtung für die Veröffentlichung begrenzt den Schaden durch verbleibende Defekte. -fstack-protector-strong fügt in ausgewählten Stackframes Prüfungen ein, die viele Überschreibungen erkennen. Auf unterstützten Systemen ergänzt _FORTIFY_SOURCE Prüfungen bei Bibliotheksaufrufen, wenn sich Objektgrößen bestimmen lassen. Es benötigt Optimierung; Stufe 2 ist üblich, während Stufe 3 eine geeignete Unterstützung durch Compiler und C-Bibliothek voraussetzt. Dies sind Plattformfunktionen, keine Garantien von ISO C.

Ein gehärtetes Programm kann anhalten, wenn es eine Beschädigung erkennt. Die ungültige Operation muss trotzdem im Quelltext korrigiert werden; eine Änderung der Release-Optionen kann keine Schleife korrigieren, die außerhalb der Speichergrenzen zugreift.

Wichtige Erkenntnisse

  • Verwende -g für Debuginformationen zum Quelltext.
  • Suche mit -O0 oder -Og nach Fehlern und teste auch den Release-Build.
  • Halte Eingabeprüfungen aktiv, wenn NDEBUG Zusicherungen entfernt.
  • Statische Analyse und Laufzeitanalyse finden unterschiedliche Pfade und Defekte.
  • Halte Erstellungsoptionen in einer reproduzierbaren Konfiguration fest.
  • Härtung verringert den Schaden durch verbleibende Programmfehler.
2

Programmfehler mit einem Debugger finden und beheben

Ein Absturz zeigt dir, wo die Ausführung angehalten hat; der Fehler kann viel früher aufgetreten sein. Beginne mit einem reproduzierbaren Fehlverhalten und einer kleinen Eingabe und untersuche das Programm dann mit einem Debugger zu ausgewählten Zeitpunkten. Haltepunkte und schrittweise Ausführung zeigen, wie sich Werte entwickeln. Stackframes, Datenhaltepunkte und bedingte Haltepunkte helfen, einen fehlerhaften Wert bis zu dem Code zurückzuverfolgen, der ihn erzeugt hat, sodass jeder Versuch eine konkrete Frage beantwortet.

2.1 Methodisch nach Fehlern suchen: reproduzieren, eingrenzen und eine Hypothese prüfen

Ein Fehlverhalten ist das Verhalten, das du beobachtest: ein Absturz, eine falsche Summe oder eine Ausgabe, die nie erscheint. Ein Programmfehler ist der Fehler, der es verursacht hat. Wenn du beides auseinanderhältst, änderst du nicht vorschnell die Absturzzeile, bevor du herausgefunden hast, wie das Programm dorthin gelangt ist.

Das Fehlverhalten reproduzierbar machen

Halte den Befehl, die Eingabe, die Erstellungsoptionen, das erwartete und das tatsächliche Ergebnis fest. Lass die fehlerauslösende Eingabe unverändert, während du experimentierst. Schlägt ein Programm nur bei einer Datei fehl, speichere diese Datei, bevor du sie bearbeitest. Durch die Reproduktion kannst du beurteilen, ob eine vorgeschlagene Korrektur funktioniert.

Verkleinere als Nächstes das Beispiel, ohne das Fehlverhalten zu beseitigen. Entferne nicht benötigte Datensätze oder Code und führe das Programm erneut aus. Angenommen, ein Datensatz unter 1.000 löst unabhängig von den anderen einen Absturz aus. Durch das Prüfen von Hälften lässt er sich in etwa 10 Runden finden. Schlägt keine der beiden Hälften allein fehl, benötigt das Fehlverhalten möglicherweise eine Folge von Datensätzen oder einen zuvor aufgebauten Zustand. Behalte diese Folge bei und entferne kleinere Teile darum herum. Halbieren hilft nur, wenn die verbleibende Eingabe die Ursache weiterhin auslöst.

Eine prüfbare Frage stellen

Ein leeres Feld in einer minimierten Datei legt eine Hypothese nahe: „Der Parser verwendet das Feld, bevor er prüft, ob es existiert.“ Prüfe das mit einer kleinen Eingabe, die ein leeres Feld enthält. Untersuche den zurückgegebenen Zeiger oder ergänze eine gezielte Prüfung. Ändere jeweils nur eine Sache; wenn du Parser, Puffergröße und Eingabe gleichzeitig änderst, zeigt ein Erfolg nicht, welche Änderung entscheidend war.

Behebe den Regelverstoß, führe das minimierte Beispiel erneut aus und danach die ursprüngliche Eingabe und die vorhandenen Tests. Speichere den Auslöser als Regressionstest, damit eine spätere Änderung nicht unbemerkt dasselbe Fehlverhalten zurückbringen kann.

Den Stopp deuten

„Segmentation fault“ meldet gewöhnlich SIGSEGV: Das System hat einen ungültigen Speicherzugriff gestoppt. Ein Nullzeiger, ein Zeiger auf freigegebenen Speicher oder eine Adresse außerhalb des zulässigen Bereichs ist eine häufige Ursache. Ein ungültiger Zugriff kann auch ohne unmittelbaren Absturz bleiben, sodass das Ausbleiben einer Meldung allein wenig aussagt.

„Aborted“ meldet häufig SIGABRT, das durch abort() ausgelöst wird. Eine fehlgeschlagene Zusicherung oder eine Bibliotheksprüfung, die eine Beschädigung des Heaps erkennt, kann dies verursachen. In bash sind übliche Signal-Rückgabestatus 139 für SIGSEGV und 134 für SIGABRT; diese Zahlen sind Shell-Konventionen und keine C-Rückgabewerte, die dein Programm verwenden muss.

Beobachten, ohne Ausgaben zu verlieren

Eine kurze Diagnoseausgabe kann den ersten falschen Wert aufdecken:

fprintf(stderr, "index=%zu total=%d\n", index, total);

Beschriftungen machen die Ausgabe lesbar, wenn dieselbe Zeile viele Male ausgeführt wird. stderr ist für Diagnosemeldungen nützlich, weil es nicht vollständig gepuffert ist; stdout ist bei Umleitung in eine Datei oder Pipe normalerweise vollständig gepuffert, sodass bei einem Absturz ausstehende Ausgaben verloren gehen können. Entferne vorübergehende Ausgaben nach der Korrektur.

Erkläre eine verdächtige Funktion Zeile für Zeile laut. Diese Gummientenmethode zwingt dich, jede Operation mit dem beabsichtigten Verhalten abzugleichen. Wenn sich ein Programmfehler verändert, sobald du Ausgaben hinzufügst oder die Optimierung änderst, untersuche mit Sanitizern, ob undefiniertes Verhalten vorliegt. Ein Array so lange zu vergrößern, bis ein Absturz verschwindet, kann einen ungültigen Index verbergen, ohne ihn zu korrigieren.

Wichtige Erkenntnisse

  • Sichere eine genaue Reproduktion, bevor du Code änderst.
  • Minimiere die Eingabe, ohne das Fehlverhalten zu beseitigen.
  • Prüfe eine Hypothese mit einer kontrollierten Änderung oder Beobachtung.
  • Verfolge einen Absturz bis zu seiner Ursache zurück, die früher liegen kann.
  • Führe den ursprünglichen Fall erneut aus und behalte einen Regressionstest.

2.2 Ein Programm in gdb oder lldb ausführen, anhalten und schrittweise durchlaufen

Mit einem Debugger kannst du ein laufendes Programm anhalten und die Werte untersuchen, die sein Verhalten erklären. Verwende GDB auf vielen Linux-Systemen oder LLDB unter macOS. Die Debugger-Schaltflächen in Entwicklungsumgebungen beruhen auf denselben Ideen: anhalten, ausführen, untersuchen und schrittweise weitergehen.

Mit der richtigen ausführbaren Datei beginnen

Kompiliere mit -g -O0 und öffne das Ergebnis dann mit gdb ./program oder lldb ./program. An der Eingabeaufforderung startet run records.txt 3 das Programm mit zwei Argumenten. In GDB leitet run < input.txt die Standardeingabe um; in LLDB verwendest du process launch -i input.txt. Mit quit beendest du den Debugger.

Setze vor der Ausführung einen Haltepunkt. break main in GDB und b main in LLDB halten am Anfang von main an. Beide akzeptieren eine Datei- und Zeilenangabe wie b program.c:12. Vollständige LLDB-Befehle sind unter anderem breakpoint set --name main und breakpoint set --file program.c --line 12.

Ein Haltepunkt hält normalerweise an, bevor die angegebene Quelltextanweisung ausgeführt wird. Weist die ausgewählte Zeile einen Wert zu, gib ihn nach der Ausführung der Zeile aus, um den neuen Wert zu untersuchen. Erstelle das Programm nach Änderungen am Quelltext erneut: Der Debugger führt Maschinencode aus, nicht den Text in deinem Editor.

Die Art des Weitergehens wählen
BefehlZweck in GDB und LLDB
next oder nDie aktuelle Zeile ausführen und dabei Aufrufe normalerweise überspringen
step oder sIn eine aufgerufene Funktion hineingehen, wenn Quelltextinformationen das erlauben
finishBis zur Rückkehr der ausgewählten Funktion ausführen
continue oder cBis zum nächsten Halt oder Programmende fortfahren

Andere Haltepunkte können next oder finish unterbrechen. Verwende next für einen Bibliotheksaufruf, den du nicht untersuchen musst. Gehst du in unerwünschten Code hinein, kehrst du mit finish zum Aufrufer zurück.

Eine Schleife Durchlauf für Durchlauf untersuchen

Betrachte eine mit null initialisierte Summe und dieses Array:

int a[5] = {4, 8, 15, 16, 23};
for (int i = 0; i <= 5; ++i) {
    sum += a[i];
}

Halte im Schleifenrumpf an und gib i und sum vor der Addition aus. Nach den ersten fünf Additionen lauten die Zwischensummen 4, 12, 27, 43 und 66. Beim nächsten Halt gilt i == 5, aber die gültigen Indizes enden bei 4. Du hast den zusätzlichen Durchlauf gefunden, ohne das ungültige Element lesen zu müssen. Ändere die Bedingung in i < 5, erstelle das Programm erneut und führe es wieder aus.

print expr oder p expr wertet in beiden Debuggern einen Ausdruck aus. Beginne mit sicheren Abfragen wie p i und p sum; ein Debuggerausdruck, der einen ungültigen Zeiger dereferenziert, kann selbst einen ungültigen Lesezugriff versuchen. display i in GDB wiederholt die Ausgabe bei jedem Halt. info locals in GDB und frame variable in LLDB zeigen lokale Variablen. Für fünf gültige Elemente über ptr verwendest du p *ptr@5 in GDB oder parray 5 ptr in LLDB.

Du kannst eine Hypothese prüfen, indem du einen Wert bei angehaltenem Programm änderst: GDB akzeptiert set var i = 4, LLDB akzeptiert expr i = 4. Das ändert einen Programmlauf, nicht den Quelltext. Zur Verwaltung der Haltepunkte verwendest du info breakpoints und delete 1 in GDB oder breakpoint list und breakpoint delete 1 in LLDB.

Wichtige Erkenntnisse

  • Erstelle die ausführbare Datei mit Debuginformationen und erstelle sie bei Bedarf erneut.
  • Eine Quelltextanweisung, an der das Programm anhält, wurde normalerweise noch nicht ausgeführt.
  • Verwende next, step, finish und continue, um unterschiedlich weit fortzufahren.
  • Prüfe Indizes und Grenzen, bevor du Speicherausdrücke auswertest.

2.3 Abstürze und unerwartete Werte untersuchen

Ein Absturz innerhalb einer Bibliothek beginnt oft mit einem ungültigen Argument aus deinem eigenen Code. Der Aufrufstapel zeigt die aktiven Funktionsaufrufe. Nutze ihn, um von der Stelle des Halts zum Aufrufer zu gelangen, dessen Werte du erklären kannst.

Stackframes vom aktuellen Aufruf aus rückwärts lesen

Gib in einem der beiden Debugger bt ein. Stackframe #0 ist der innerste aktive Aufruf; höhere Stackframe-Nummern bezeichnen dessen Aufrufer. Ein Stackframe zeigt den Ausführungskontext der Funktion, einschließlich verfügbarer Argumente und lokaler Variablen. Durch Optimierung können einige dieser Informationen entfallen.

Angenommen, find_name(2) findet keinen passenden Eintrag und der Aufrufer führt Folgendes aus:

const char *name = find_name(2);
printf("%zu\n", strlen(name));

Der Halt kann innerhalb von strlen liegen. Suche in bt den ersten Stackframe, der zu deinem Code gehört. Ist es Stackframe 1, wähle ihn mit frame 1 in GDB oder frame select 1 in LLDB aus. Gib name aus. Ein Nullwert erklärt, warum die Zeichenkettenoperation fehlgeschlagen ist. Der Aufrufer muss den fehlenden Namen behandeln, bevor er strlen aufruft; eine Änderung der Bibliothek kann das Argument nicht korrigieren.

Mit up und down bewegst du dich zwischen Aufrufern und aufgerufenen Funktionen. Die Auswahl eines Stackframes ändert, was der Debugger anzeigt und auf welche lokalen Variablen sich ein Ausdruck bezieht. Sie setzt weder die Ausführung fort noch macht sie einen Aufruf rückgängig.

Anhalten, wenn sich ein Wert ändert

Ein Haltepunkt fragt: „Was geschieht an dieser Stelle?“ Ein Datenhaltepunkt fragt: „Welche Operation ändert diesen Wert?“ Wird eine gespeicherte Anzahl lange vor einem Absturz falsch, halte an, während die Anzahl im Gültigkeitsbereich ist, und verwende:

GDB:  watch count
LLDB: watchpoint set variable count

Fahre fort, bis der Debugger bei einer Änderung anhält. Untersuche den alten und den neuen Wert sowie die Quelltextstelle der schreibenden Anweisung. Eine Änderung kann zulässig sein, vergleiche sie also mit der Regel der Datenstruktur. Die Hardware bietet nur wenige Datenhaltepunkte; auf x86-64 sind vier üblich. Der Speicher einer lokalen Variablen ist nur sinnvoll nutzbar, solange ihre Funktion aktiv ist.

Für einen bekannten späten Durchlauf verwende einen bedingten Haltepunkt. break 20 if i == 999 in GDB hält in Zeile 20 nur an, wenn die Bedingung erfüllt ist. Das entsprechende Kommando in LLDB lautet breakpoint set --line 20 --condition 'i == 999'. Das erspart Hunderte manuelle Schritte.

Einen gespeicherten Absturz untersuchen

Ein Speicherabbild hält den Zustand eines abgestürzten Prozesses fest. Wo Speicherabbilder aktiviert sind, erlaubt ulimit -c unlimited sie für Programme, die aus dieser Shell gestartet werden. Öffne ein gespeichertes Abbild mit gdb ./program core oder lldb ./program -c core und verwende dabei die ausführbare Datei, die es erzeugt hat. Systemrichtlinien bestimmen, ob und wo ein Abbild gespeichert wird; Linux-Systeme mit systemd können es über coredumpctl verwalten.

Führe bt aus, wähle deinen Stackframe und untersuche Werte wie bei einem aktuellen Absturz. Mit dem Abbild lässt sich das Programm nicht fortsetzen. Wenn malloc oder free eine Beschädigung erkennt, kann der ursprüngliche fehlerhafte Schreibzugriff in diesem Aufrufstapel fehlen. Reproduziere den Fehler mit AddressSanitizer, um näher an diesem Schreibzugriff anzuhalten. Sieht ein lokaler Wert beliebig aus, prüfe, ob die Ausführung seinen Initialisierer bereits erreicht hat, bevor du den Debugger verantwortlich machst.

Wichtige Erkenntnisse

  • Verfolge die aktiven Aufrufe mit bt zurück.
  • Wähle einen Stackframe in deinem Code und untersuche die an die Bibliothek übergebenen Argumente.
  • Datenhaltepunkte halten bei Wertänderungen an; bedingte Haltepunkte bei ausgewählten Zuständen.
  • Ein Speicherabbild ermöglicht die Untersuchung nach einem Absturz.
  • Die Beschädigung kann vor dem beim abschließenden Absturz sichtbaren Aufrufstapel entstanden sein.
3

Undefiniertes Verhalten erkennen und vermeiden

Für manche C-Operationen garantiert die Sprache kein Ergebnis. Sie können in einem Build scheinbar funktionieren und in einem anderen fehlschlagen, sodass gewöhnliches Ausprobieren irreführend ist. Lerne, undefiniertes Verhalten von dokumentierten Plattformentscheidungen und erlaubten Abweichungen zu unterscheiden. Untersuche anschließend Arithmetik, Auswertungsreihenfolge von Ausdrücken, Arraygrenzen und Speicherlebensdauern und setze Prüfungen vor die Operation, die eine dieser Grenzen überschreitet.

3.1 Verstehen, was undefiniertes Verhalten bedeutet

Undefiniertes Verhalten, kurz UB, bedeutet, dass der C-Standard keine Anforderungen daran stellt, was nach einer ungültigen Operation geschieht. Er verspricht weder eine Ausnahme noch eine hilfreiche Fehlermeldung oder einen vorhersagbaren Wert durch Wraparound. Ein Programm kann abstürzen, Daten beschädigen, eine plausibel wirkende Antwort ausgeben oder scheinbar funktionieren.

Drei Arten von Verhalten unterscheiden
ArtWas die Sprache erlaubtBeispiel in C17
UndefiniertKein vorgeschriebenes VerhaltenÜberlauf bei vorzeichenbehafteten Ganzzahlen
ImplementierungsdefiniertEine dokumentierte Entscheidung der ImplementierungOb einfaches char vorzeichenbehaftet ist
Nicht spezifiziertEine erlaubte Entscheidung, die nicht dokumentiert werden mussReihenfolge der Auswertung unabhängiger Funktionsargumente

Implementierungsdefiniertes Verhalten kann gezielt genutzt werden, wenn die dokumentierte Plattform Teil des Programmentwurfs ist. Für portablen Code solltest du nicht annehmen, dass alle Implementierungen dieselbe Wahl treffen. Auch nicht spezifiziertes Verhalten ist gültig: f(a(), b()) kann jeden der beiden unabhängigen Aufrufe zuerst auswerten. Schreibe Code, der bei dieser erlaubten Abweichung korrekt bleibt. Die Bitbreite von int und in C17 die Rechtsverschiebung einer negativen vorzeichenbehafteten Ganzzahl sind weitere plattformabhängige Entscheidungen.

Warum Optimierung Programmfehler sichtbar macht

C überlässt die Prüfung von Arraygrenzen und Additionen vorzeichenbehafteter Zahlen dem Programmierer. Der Compiler darf unter der Annahme optimieren, dass das Programm die Sprachregeln befolgt. Ein versuchter Überlaufdetektor kann genau gegen die Regel verstoßen, die er prüfen soll:

int will_overflow(int x) {
    return x + 1 < x;
}

Für jedes x, bei dem die vorzeichenbehaftete Addition definiert ist, ist x + 1 größer als x. Der Compiler kann das Ergebnis daher durch null ersetzen. Bei INT_MAX ist die Addition UB, sodass der Compiler dem Aufrufer kein Ergebnis durch Wraparound schuldet. Eine andere Antwort in einem nicht optimierten Build macht dies nicht zu einem gültigen Test.

Der sichere Test erfolgt vor der Operation:

if (x == INT_MAX) {
    /* handle overflow */
} else {
    ++x;
}

Verwende <limits.h> für die Schranken der Implementierung. Ein fest im Code angegebener Höchstwert setzt eine bestimmte Bitbreite des Typs voraus; das Makro benennt die tatsächliche Schranke.

Vor der ersten Verwendung prüfen

Diese Reihenfolge ist auch für Zeiger entscheidend:

if (p == NULL) {
    return -1;
}
int v = *p;

Werden diese Anweisungen vertauscht, kann ein Nullzeiger vor der Schutzprüfung dereferenziert werden. Sobald der Code p dereferenziert, kann ein Optimierer folgern, dass für einen definierten Pfad bereits gelten muss, dass p kein Nullzeiger ist, und eine spätere Nullprüfung entfernen.

Das Ändern von Optionen, das Hinzufügen einer Ausgabe oder das Verschieben einer Variablen kann das sichtbare Symptom von UB verändern. Ein erfolgreicher Lauf zeigt nur, was dieser Build bei dieser Eingabe getan hat. Um den Code zu korrigieren, finde heraus, welche Operation keine definierte Bedeutung hat und welche Bedingung vorher erfüllt sein muss. Warnungen können einige Fälle erkennen; statische Analyse kann mögliche Pfade untersuchen; ASan und UBSan prüfen viele ausgeführte Operationen. Programmierregeln liefern die entsprechenden Muster für den Quelltext, etwa einen Index oder eine arithmetische Schranke vor der Verwendung zu prüfen.

Wichtige Erkenntnisse

  • UB garantiert weder einen Absturz noch ein stabiles Ergebnis.
  • Implementierungsdefinierte Entscheidungen müssen dokumentiert werden.
  • Nicht spezifizierte Entscheidungen bleiben innerhalb der Sprachregeln.
  • Optimierer dürfen sich darauf verlassen, dass in definiertem Code kein Überlauf bei vorzeichenbehafteten Ganzzahlen auftritt.
  • Prüfe einen Zeiger oder eine arithmetische Schranke vor der riskanten Operation.

3.2 Undefiniertes Verhalten in arithmetischen Operationen und Ausdrücken erkennen

Eine arithmetische Operation kann fehlschlagen, bevor du ihr Ergebnis untersuchen kannst. Prüfe die Operanden, solange ihre Werte noch gültig sind, und kenne den Typ, in dem jede Operation ausgeführt wird.

Schranken vorzeichenbehafteter und vorzeichenloser Typen

Ein Überlauf bei vorzeichenbehafteten Ganzzahlen ist undefiniert. Bei einem int mit 32 Bit ist INT_MAX gleich 2.147.483.647; die Addition von eins ist kein portabler Weg, eine negative Zahl zu erhalten. Auch INT_MIN / -1, -INT_MIN und eine zu große Multiplikation schlagen fehl, wenn das mathematische Ergebnis nicht darstellbar ist.

Vorzeichenlose Arithmetik führt innerhalb des Wertebereichs ihres Typs zu Wraparound. Für einen unsigned int mit 32 Bit ist UINT_MAX + 1u gleich null und 0u - 1u gleich 4.294.967.295. Das ist definiert, kann aber dennoch eine Größenberechnung verfälschen. Eine rückwärts laufende Schleife mit einer vorzeichenlosen Variablen und i >= 0 erreicht nie einen negativen Wert zum Anhalten.

Prüfe vor einer allgemeinen Addition vorzeichenbehafteter Zahlen:

if ((b > 0 && a > INT_MAX - b) ||
    (b < 0 && a < INT_MIN - b)) {
    /* report overflow */
} else {
    result = a + b;
}

Die in diesen Prüfungen verwendeten Subtraktionen bleiben im Wertebereich, weil jede durch das Vorzeichen von b abgesichert ist. Erst die Summe zu berechnen und sie dann zu vergleichen, ist zu spät. Ein breiterer Typ hilft nur, wenn er jedes Zwischenergebnis aufnehmen kann: Konvertiere einen Operanden vor der Operation. C23 bietet ckd_add in <stdckdint.h>; GCC und Clang bieten __builtin_add_overflow.

Division, Bitverschiebungen und Typumwandlungen

Lehne null vor einer Ganzzahldivision oder Restbildung ab. Lehne auch die Division des vorzeichenbehafteten Minimums durch minus eins ab, wenn das Ergebnis nicht darstellbar ist. Gleitkommasysteme nach IEEE 754 erzeugen bei Division durch null häufig unendlich oder NaN, sodass sich ungültige numerische Ergebnisse ohne eine Abbruchreaktion wie bei Ganzzahlen ausbreiten können.

Verwende für Bitoperationen vorzeichenlose Operanden. Die Verschiebungsweite muss nicht negativ und kleiner als die Bitbreite des erweiterten linken Operanden sein. Bei Operanden mit 32 Bit ist eine Verschiebung um 32 undefiniert. In C17 ist die Linksverschiebung eines negativen vorzeichenbehafteten Werts undefiniert; auch 1 << 31 ist für einen int mit 32 Bit undefiniert, während 1u << 31 für einen unsigned int mit 32 Bit gültig ist.

Bei der Umwandlung einer Gleitkommazahl in eine Ganzzahl muss der abgeschnittene Wert darstellbar sein, auch wenn der Zieltyp vorzeichenlos ist. (unsigned)-1.0 ist undefiniert. Die Umwandlung einer Ganzzahl in einen vorzeichenlosen Typ reduziert den Wert dagegen auf dessen Wertebereich: (unsigned)-1 wird zu UINT_MAX. Die Umwandlung einer Ganzzahl außerhalb des Wertebereichs in einen vorzeichenbehafteten Typ ist in C17 implementierungsdefiniert. Prüfe Wertebereiche vor einer Typverengung; -Wconversion hilft, implizite Änderungen sichtbar zu machen.

Die Auswertungsreihenfolge unschädlich machen

printf("%d %d\n", i, ++i); liest und verändert i ohne die erforderliche Reihenfolge. Auch i = i++ + 1; und a[i] = i++; enthalten nicht sequenzierte Operationen. Teile die Arbeit in separate Anweisungen mit einer ausdrücklich festgelegten Reihenfolge auf.

Initialisiere automatische Variablen, bevor du sie liest: int sum = 0; macht sum += x; sinnvoll. Globale und statische Variablen beginnen bei null. Die in C23 vorgeschriebene Darstellung im Zweierkomplement definiert keinen Überlauf bei vorzeichenbehafteten Ganzzahlen; portabler Code benötigt weiterhin die Prüfungen.

Wichtige Erkenntnisse

  • Prüfe vorzeichenbehaftete arithmetische Operationen, bevor du ihr Ergebnis berechnest.
  • Wraparound bei vorzeichenlosen Ganzzahlen ist definiert, kann aber Größen und Schleifen verfälschen.
  • Prüfe Divisionsoperanden und Verschiebungsweiten.
  • Die Umwandlung einer Gleitkommazahl in einen vorzeichenlosen Typ unterscheidet sich von der einer Ganzzahl.
  • Ordne Änderungen ausdrücklich und initialisiere Werte vor der Verwendung.
  • Auch C23 lässt Überläufe bei vorzeichenbehafteten Ganzzahlen undefiniert.

3.3 Undefiniertes Verhalten beim Umgang mit Zeigern, Arrays und Speicher erkennen

Ein Zeiger braucht mehr als eine plausibel wirkende Adresse. Das Objekt muss existieren, der Zugriff muss innerhalb seiner Grenzen bleiben und die Operation muss für dieses Objekt erlaubt sein. Diese Prüfungen erklären sowohl unmittelbare Abstürze als auch Beschädigungen, die erst viel später entdeckt werden.

Innerhalb des Objekts bleiben

Ein Array mit n Elementen hat Indizes von null bis n - 1. Ein Zeiger unmittelbar hinter das letzte Element kann ein Schleifenende markieren, aber du darfst ihn nicht dereferenzieren. i <= n besucht ein Element zu viel. CERT ARR30-C behandelt diese Grenzfehler.

Ein Schreibzugriff außerhalb der Speichergrenzen kann in zugänglichem Speicher landen, der zu einem anderen Objekt gehört. Das Programm kann weiterlaufen, bis eine beschädigte Anzahl, ein beschädigter Zeiger oder ein beschädigter Eintrag der Speicherverwaltung verwendet wird. Ein späterer Absturz in free zeigt nicht den ursprünglichen Schreibzugriff. ASan kann beim Schreibzugriff anhalten, wenn seine Prüfungen die Grenzverletzung erkennen.

Zeichen zu einer gültigen Zeichenkette machen

Eine C-Zeichenkette endet mit '\0'. Ihr Speicher benötigt ein Byte mehr als ihre Anzahl an Zeichen. C akzeptiert char s[5] = "hello";, aber alle fünf Bytes enthalten Buchstaben, sodass kein Platz für ein abschließendes Nullzeichen bleibt. Wird das Array an strlen oder %s übergeben, kann über das Array hinaus gelesen werden. Verwende genügend Platz und stelle sicher, dass ein abschließendes Nullzeichen tatsächlich vorhanden ist. STR31-C behandelt die Kapazität; STR32-C behandelt die Übergabe einer abgeschlossenen Zeichenkette.

Ein Zeichenkettenliteral ist nicht beschreibbar. Nach char *p = "hi"; ist p[0] = 'H'; undefiniert. Verwende char p[] = "hi"; für eine beschreibbare Kopie oder const char *p, um Änderungsversuche am Literal für den Compiler sichtbar zu machen. Dies ist STR30-C.

Lebensdauer und Besitz beachten

Prüfe das Ergebnis einer Speicherallokation, einer Dateiöffnung oder einer Suche vor dem Dereferenzieren auf NULL. Wird ein Block freigegeben, verliert jeder Zeiger hinein den Zugriff auf ein noch existierendes Objekt. Lies und beschreibe den Block nicht und gib ihn nicht erneut frei. Gib nur den passenden Zeiger frei, der von einer dynamischen Speicherallokation zurückgegeben wurde, niemals ein lokales Array oder einen Zeiger ins Objektinnere. free(NULL) ist harmlos, aber wenn du einen Zeiger nach der Freigabe auf null setzt, werden andere Aliase dadurch nicht korrigiert.

Eine Funktion, die die Adresse eines lokalen Arrays zurückgibt, gibt einen hängenden Zeiger zurück:

char *greeting(void) {
    char text[16] = "Welcome";
    return text;
}

Die Lebensdauer des Arrays endet bei der Rückkehr aus der Funktion. Übergib der Funktion einen Puffer und seine Kapazität, den sie füllen kann, oder reserviere ein Ergebnis, dessen Besitz an den Aufrufer übergeht. Eine erfolgreiche Ausgabe unmittelbar nach der Rückkehr kann die Lebensdauer des alten Arrays nicht verlängern.

Byte für jedes Element reservieren

malloc(n) fordert Byte an, nicht n Objekte des Typs, auf den gezeigt wird. Der übliche elementbezogene Ausdruck ist n * sizeof *a. Verhindere zuerst, dass dieses Produkt durch Wraparound überläuft:

if (n > SIZE_MAX / sizeof *a) {
    /* reject the requested size */
}

Binde <stdint.h> für SIZE_MAX und <stdlib.h> für Allokationsfunktionen ein. Sobald die Größe gültig ist, reserviere den Speicher, prüfe den zurückgegebenen Zeiger und schreibe jeden Wert, bevor du ihn liest. malloc lässt den Speicher nicht initialisiert. calloc(n, sizeof *a) füllt seinen Speicher mit Nullen, und gängige Bibliotheken weisen überlaufende Produkte zurück. Eine Größenprüfung, eine Prüfung des Allokationsergebnisses und ein gültiger Index lösen unterschiedliche Probleme; alle drei sind wichtig.

Wichtige Erkenntnisse

  • Dereferenziere Zeiger nur innerhalb der Grenzen eines noch existierenden Objekts.
  • Reserviere Platz für das abschließende Nullzeichen und speichere es.
  • Verändere keine Zeichenkettenliterale.
  • Die Freigabe eines Speicherblocks macht Zugriffe über alle seine Aliase ungültig.
  • Gib Speicher zurück, dessen Lebensdauer der Aufrufer nutzen kann.
  • Prüfe die Multiplikation mit der Elementanzahl und das Allokationsergebnis.
4

Fehler zur Laufzeit mit Zusicherungen und Sanitizern erkennen

Ein Test-Build kann mehr als seine endgültige Ausgabe prüfen. Zusicherungen prüfen die Annahmen, deren Einhaltung dein eigener Code verspricht, während Sanitizer Operationen wie Speicherzugriffe und Arithmetik während ihrer Ausführung prüfen. Lerne, jeden Bericht zu lesen, ein Werkzeug für den vermuteten Fehler auszuwählen und Eingaben auszuführen, die die riskanten Pfade erreichen. Ein nützlicher Prüfablauf verbindet einen erkannten Fehler mit einer Korrektur und einem dauerhaften Test.

4.1 Annahmen durch Zusicherungen ausdrücken

Eine Zusicherung (Assertion) drückt etwas aus, das wahr sein sollte, weil dein eigener Code korrekt ist. Sie macht eine verborgene verletzte Annahme durch einen Halt nahe der Stelle sichtbar, an der diese Annahme wichtig ist.

Einen Programmiervertrag prüfen

Binde <assert.h> ein und schreibe assert(condition);. Eine wahre Bedingung lässt die Ausführung weiterlaufen. Eine falsche Bedingung gibt Diagnoseinformationen auf stderr aus und ruft abort() auf. Die Meldung nennt den Ausdruck, die Quelltextstelle und die Funktion; ihr genauer Wortlaut hängt von der Plattform ab.

Für eine interne Hilfsfunktion zur Mittelwertberechnung, deren Aufrufer gültige Argumente zusichert:

double average(const int *a, size_t n) {
    assert(a != NULL);
    assert(n > 0);
    /* compute the average */
}

Ein versehentlicher Aufruf mit null Elementen schlägt an der verletzten Zusage fehl. Unter einem Debugger zeigt bt die Zusicherung und den Abbruchmechanismus, gefolgt von den Aufrufen in deinem Code. Wähle den Stackframe des Aufrufers, um zu untersuchen, wie er null übergeben hat.

Verträge treten an mehreren Stellen auf:

ArtWas sie festlegtBeispiel
VorbedingungBeim Eintritt erforderlichEin nicht leeres Eingabearray
NachbedingungNach der Arbeit garantiertEin Array ist sortiert
InvarianteÜber mehrere Schritte hinweg erhaltenEine gespeicherte Anzahl passt zur Struktur

Eine Zusicherung ist ausführbare Dokumentation: Sie benennt die Eigenschaft und prüft sie während eines Testlaufs. Wähle Ausdrücke, die den Zustand beobachten, ohne ihn zu verändern.

Benötigte Arbeit außerhalb von Zusicherungen halten

Wird NDEBUG vor dem Einbinden von <assert.h> definiert, üblicherweise mit -DNDEBUG, deaktiviert dies assert. Sein Ausdruck wird nicht ausgewertet. Das erzeugt einen schwerwiegenden Programmfehler, wenn der Ausdruck benötigte Arbeit ausführt:

assert(++count <= MAX);

In einem Build mit NDEBUG entfällt das Inkrement. Schreibe die Aktion gesondert:

++count;
assert(count <= MAX);

Dieselbe Regel schließt das Öffnen von Dateien, das Reservieren von Speicher und das Schließen von Dateien innerhalb einer Zusicherung aus. Diese Operationen müssen in allen Konfigurationen stattfinden. Ihre Fehler benötigen eine gewöhnliche Fehlerbehandlung.

Erwartete Fehler regulär behandeln

Eine fehlende Eingabedatei, ungültiger Benutzertext und ausgeschöpfter Speicher können bei korrekter Verwendung des Programms auftreten. Prüfe sie mit if, melde den Fehler und kehre zurück oder gib Ressourcen frei. Eine private Hilfsfunktion kann einen geprüften Wertebereich zusichern, aber an der öffentlichen Eingabeschnittstelle muss dieser Bereich mit Prüfungen durchgesetzt werden, die im Release-Build erhalten bleiben.

Prüfe zum Beispiel beim Schließen einer Ausgabedatei immer fclose(f) == EOF im gewöhnlichen Kontrollfluss. Gepufferte Ausgaben können beim Schließen fehlschlagen; wird der Aufruf oder seine Prüfung entfernt, kann das verlorene Daten verbergen.

Feste Sachverhalte zur Übersetzungszeit prüfen

Eine statische Zusicherung prüft beim Kompilieren einen konstanten Ausdruck. In C17 stellt <assert.h> static_assert als Makro für _Static_assert aus C11 bereit:

static_assert(sizeof(int) == 4, "requires a 4-byte int");

Eine falsche Bedingung lässt die Erstellung fehlschlagen. Eine vom Nutzer eingegebene Anzahl, die erst zur Laufzeit bekannt ist, kann sie nicht prüfen. C23 macht static_assert zu einem Schlüsselwort und erlaubt es, die Meldung wegzulassen. Nutze diesen Unterschied, um jede Prüfung dort zu platzieren, wo die benötigten Informationen verfügbar sind.

Wichtige Erkenntnisse

  • Zusicherungen prüfen interne Programmierzusagen.
  • Eine fehlgeschlagene Laufzeitzusicherung meldet ihre Position und bricht ab.
  • Halte Seiteneffekte aus Ausdrücken von Zusicherungen heraus.
  • Verwende gewöhnliche Fehlerbehandlung für ungültige Eingaben und Ressourcenfehler.
  • NDEBUG deaktiviert Laufzeitzusicherungen.
  • Statische Zusicherungen prüfen konstante Ausdrücke während der Kompilierung.

4.2 Speicherfehler mit AddressSanitizer finden

AddressSanitizer, gewöhnlich ASan genannt, ergänzt Prüfungen bei kompilierten Speicheroperationen. Er kann einen fehlerhaften Zugriff in dem Moment melden, in dem er geschieht, bevor beschädigte Daten einen späteren Absturz an anderer Stelle verursachen.

Mit eingebundenen Prüfungen erstellen

Verwende GCC oder Clang, um mit ASan zu kompilieren und zu linken:

clang -std=c17 -g -fsanitize=address -fno-omit-frame-pointer program.c -o program-asan

-g liefert Quelltextstellen. Das Beibehalten von Frame-Pointern hilft bei Aufrufrückverfolgungen. Instrumentiere bei einem Projekt mit mehreren Dateien die relevanten Quelldateien und verwende die Sanitizeroption auch beim abschließenden Linken. MSVC bietet /fsanitize=address für unterstützte Windows-Builds.

ASan erkennt viele Überläufe von Heap-Blöcken, Stack-Arrays und globalen Arrays. Außerdem erkennt er Zugriffe nach der Speicherfreigabe, doppelte und ungültige Speicherfreigaben sowie unterstützte Fälle der Verwendung nach Ende des Gültigkeitsbereichs. Unter Linux enthält die übliche ASan-Laufzeitumgebung eine Leckprüfung beim Programmende. Das mit macOS gelieferte Clang von Apple bietet diese Leckerkennung nicht; LLVM-Werkzeugketten und Plattformunterstützung unterscheiden sich.

Einem Bericht zur Ursache folgen

Betrachte ein erfolgreich reserviertes Array mit einem int von 4 Byte:

int *a = malloc(10 * sizeof *a);
for (int i = 0; i <= 10; ++i) {
    a[i] = i;
}

Der reservierte Block enthält 40 Byte. Die Indizes null bis neun nutzen diese Bytes. Index zehn beginnt unmittelbar dahinter. ASan kann „heap-buffer-overflow“, „WRITE of size 4“ und eine Stelle null Byte hinter einem Bereich von 40 Byte melden. Die Rückverfolgung des Schreibzugriffs bezeichnet die Zuweisung; die Rückverfolgung der Speicherallokation bezeichnet das malloc, das die Blockgrenzen festgelegt hat.

Lies Berichte in dieser Reihenfolge:

  1. Bestimme die Fehlerkategorie und ob der Zugriff ein Lese- oder Schreibzugriff war.
  2. Suche in der Rückverfolgung des Zugriffs den ersten Stackframe in deinem eigenen Code.
  3. Vergleiche den Zugriff mit dem reservierten Bereich und seiner Elementgröße.
  4. Suche bei einem Lebensdauerfehler auch die Rückverfolgungen der Allokation und der Freigabe.

Hier lautet die Korrektur i < 10. Ein weiteres Element zu reservieren und die falsche Abbruchregel beizubehalten, verbirgt die Abweichung zwischen der beabsichtigten Anzahl und der Schleife. ASan hält standardmäßig beim ersten erkannten Fehler an. Behebe ihn, erstelle das Programm erneut und führe es wieder aus.

Die Grenzen des Prüfwerkzeugs verstehen

ASan verfolgt zulässigen Speicher mithilfe von Schattenspeicher. Verbotene Redzones umgeben unterstützte Speicherblöcke und Arrays. Freigegebene Blöcke verbringen vor ihrer Wiederverwendung Zeit in einer Quarantäne, was veraltete Zugriffe sichtbar machen hilft. Ein Schreibzugriff über eine Arraykomponente hinaus in eine andere Komponente derselben Struktur kann innerhalb eines größeren gültigen Bereichs bleiben und unerkannt bleiben.

ASan prüft keine gewöhnlichen Lesezugriffe auf nicht initialisierte Werte. Memcheck von Valgrind kann diese Verwendungen und viele Heap-Defekte ohne erneute Kompilierung finden, läuft aber üblicherweise etwa 20- bis 30-mal langsamer. Die typische Verlangsamung durch ASan beträgt etwa das Zweifache, bei zusätzlichem Speicherverbrauch. Kombiniere einen ASan-Build nicht mit Valgrind, MemorySanitizer oder ThreadSanitizer. Führe gezielt Grenzfalleingaben aus: Ein Fehler auf einem nicht ausgeführten Pfad kann nicht im Bericht erscheinen.

Wichtige Erkenntnisse

  • Kompiliere und linke mit -fsanitize=address und Debuginformationen.
  • Lies die Rückverfolgung des Zugriffs zusammen mit denen der Allokation oder Freigabe.
  • Behebe die erste gemeldete Ursache, erstelle das Programm erneut und führe es wieder aus.
  • ASan erkennt keine gewöhnlichen Lesezugriffe auf nicht initialisierte Werte.
  • Teste Grenzfalleingaben, um riskante Speicherzugriffe zu erreichen.

4.3 Undefiniertes Verhalten mit UBSan erkennen und einen regelmäßigen Prüfablauf etablieren

Ein Speicherprüfwerkzeug deckt nicht jede ungültige C-Operation ab. UndefinedBehaviorSanitizer, kurz UBSan, instrumentiert viele arithmetische Operationen sowie Zeiger- und Arrayoperationen. Kombiniere ihn mit ASan, damit ein Testlauf mehr als eine Klasse von Defekten erkennen kann.

Arithmetische Fehler sichtbar machen

Bei einem int mit 32 Bit ergibt die Addition zweier Werte von 2.000.000.000 eine Summe von 4.000.000.000. Dieses Ergebnis überschreitet INT_MAX. Ein UBSan-Bericht erkennt den Überlauf bei vorzeichenbehafteten Ganzzahlen an der Addition und nennt die Operanden. Die Ausgabe eines negativ aussehenden Ergebnisses durch Wraparound ohne das Prüfwerkzeug würde die Operation nicht gültig machen.

Verwende für die Summe einen Typ, der den akzeptierten Bereich darstellen kann, oder lehne eine Addition ab, bevor sie überläuft. Das bereits übergelaufene Ergebnis in long long umzuwandeln, ist zu spät; die Operation selbst muss einen geeigneten Typ verwenden.

Ein üblicher Testbefehl lautet:

clang -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined program.c -o program-check

UBSan erlaubt normalerweise nach vielen Meldungen die Fortsetzung, sodass späterer Code weiterlaufen kann. -fno-sanitize-recover=undefined sorgt dafür, dass diese Fehler den Test mit einem Fehlerstatus beenden. Das hilft automatisierten Prüfungen, einen Programmfehler selbst dann zu erkennen, wenn die endgültige Ausgabe richtig aussieht.

Das Werkzeug auf den Defekt abstimmen
WerkzeugHauptzweckWichtige Grenze
UBSanViele undefinierte arithmetische Operationen sowie Bitverschiebungen, Nullzeiger-, Ausrichtungs- und ArrayoperationenErkennt nicht jede Art von UB
ASanViele Grenzen- und LebensdauerfehlerVerfolgt keine gewöhnlichen nicht initialisierten Werte
MemorySanitizerVerwendungen nicht initialisierten SpeichersBenötigt instrumentierte Abhängigkeiten für zuverlässige Ergebnisse
ThreadSanitizerDatenwettläufeBenötigt einen von ASan getrennten Build

Wraparound bei vorzeichenlosen Ganzzahlen ist definiert, daher meldet die Gruppe undefined ihn nicht. Clang hat eine separate Prüfung mit -fsanitize=unsigned-integer-overflow für Code, bei dem Wraparound einen Programmfehler anzeigt. Schließe nicht daraus, dass jedes unerwünschte Ganzzahlergebnis UB ist.

MemorySanitizer ist ein Clang-Werkzeug, das hauptsächlich unter Linux eingesetzt wird; nicht instrumentierte Bibliotheken können seine Nachverfolgung stören. ThreadSanitizer erkennt widersprüchliche Zugriffe von Threads ohne Synchronisierung, von denen mindestens einer schreibt. Die Zugriffe müssen nicht genau zur selben Zeit stattfinden. Keines der beiden Werkzeuge lässt sich in einem Build mit ASan kombinieren.

Das Prüfen zur Routine machen

Lass Warnungen aktiviert und behebe sie. Erstelle das Programm mit ASan und UBSan und führe dann Tests mit leeren Eingaben, Eingaben mit einem Element, vollen Puffern, ungültigem Text und Werten nahe den zulässigen Schranken aus. Führe eine statische Analyse aus, um Pfade zu untersuchen, die die Tests möglicherweise übersehen, und wiederhole die Prüfungen automatisch, wenn sich der Code ändert. Führe auch die Release-Konfiguration aus, in der sich die Einstellungen für Optimierung und Zusicherungen unterscheiden.

Fuzzing liefert viele erzeugte oder veränderte Eingaben. libFuzzer und AFL++ können ungewöhnliche Parserpfade aufdecken; Sanitizer machen aus einer ausgelösten ungültigen Operation einen nützlichen Bericht. Speichere eine Eingabe, die einen Defekt findet, minimiere sie, ohne das Fehlverhalten zu verlieren, behebe die Ursache und behalte sie als Regressionstest. Ein fehlerfreier Lauf ist ein Beleg für die ausgeführten Eingaben, keine Garantie für alle möglichen Läufe.

Wichtige Erkenntnisse

  • Verwende UBSan für viele Fehler in Arithmetik und Ausdrücken.
  • Lass UBSan-Fehler automatisierte Tests stoppen, wenn eine Fortsetzung sie verbergen würde.
  • Unerwünschter Wraparound bei vorzeichenlosen Ganzzahlen benötigt eine separate Prüfung.
  • Verwende separate Builds für MemorySanitizer oder ThreadSanitizer.
  • Kombiniere Grenzfalltests, Fuzzing, statische Analyse und Regressionseingaben.
  • Die Abdeckung durch Sanitizer hängt von der Ausführung ab.
5

Sichereren C-Code schreiben

Sichereres C beginnt dort, wo Daten in ein Programm gelangen, und setzt sich bei jeder Größenberechnung, Kopie, Speicherallokation und jedem Fehlerpfad fort. Ein Programmierstandard macht aus wiederkehrenden Defekten konkrete Prüfungen, die du beim Schreiben von Code anwenden kannst. Lerne, Puffergrößen zu erhalten, vollständige numerische Eingaben zu prüfen und den Besitz von Ressourcen eindeutig zu regeln. Wäge anschließend diese Verantwortung gegen die Kontrolle von C über den Speicher und den Schutz durch speichersichere Programmiersprachen ab.

5.1 Einen Standard für sichere Programmierung anwenden

Ein Standard für sichere Programmierung sammelt konkrete Regeln, die wiederkehrende Defekte verhindern. Er gibt einem Prüfer hilfreichere Fragen an die Hand als „Sieht das sicher aus?“. Jede Regel verbindet eine riskante Operation mit der Bedingung oder dem Entwurf, die oder der sie sicher macht.

Eine Regel verstehen

Der SEI CERT C Coding Standard stammt vom Software Engineering Institute der Carnegie Mellon University. Neben der Buchausgabe von 2016 gibt es eine sich weiterentwickelnde Onlineversion. Der Standard unterscheidet Regeln, deren Verletzung wahrscheinlich Defekte verursacht, von Empfehlungen, die Zuverlässigkeit und Sicherheit verbessern.

Kennungen verbinden eine Kategorie, eine Nummer und -C. Regeln verwenden die Nummern 30 bis 99; Empfehlungen verwenden 00 bis 29. Zum Beispiel betrifft INT32-C Überläufe bei vorzeichenbehafteten Ganzzahlen, während DCL00-C eine Empfehlung für Deklarationen ist. Zu den Kategorien gehören Deklarationen (DCL), Ausdrücke (EXP), Ganzzahlen (INT), Gleitkommazahlen (FLP), Arrays (ARR), Zeichenketten (STR), Speicher (MEM), Ein- und Ausgabe (FIO), Umgebung (ENV), Fehler (ERR) und Verschiedenes (MSC).

Eine Regelseite erklärt normalerweise den Defekt, zeigt nicht regelkonformen Code, stellt regelkonforme Lösungen vor und bewertet das Risiko. Lies die Annahmen jeder Lösung. Eine Ersatzfunktion ist nur nützlich, wenn ihr Verhalten die Anforderung tatsächlich erfüllt.

Eine Regel auf einen Datenpfad anwenden

FIO30-C betrifft Formatstrings. Dieser Code behandelt externe Daten wie ein Formatierungsprogramm:

printf(user_name);

Ein Benutzername mit Formatkonvertierungen veranlasst printf, nach nicht übergebenen Argumenten zu suchen; %n kann einen Schreibzugriff auf den Speicher verursachen. Halte das Format unter Kontrolle des Programms:

printf("%s", user_name);

Alternativ gibt fputs(user_name, stdout) den Text ohne Formatverarbeitung aus. -Wformat=2 enthält Prüfungen, die unsichere Formate finden helfen. Die Korrektur ändert, wie Daten interpretiert werden, nicht lediglich, wie lang die Zeichenkette sein darf.

Eine nützliche Prüfliste erstellen

Frage beim Speicher, ob Indizes innerhalb von Objekten bleiben (ARR30-C), der Speicher einer Zeichenkette das abschließende Nullzeichen enthält (STR31-C), auf freigegebenen Speicher nie zugegriffen wird (MEM30-C), Speicherblöcke in eigenem Besitz freigegeben werden (MEM31-C), nur gültige Speicherblöcke freigegeben werden (MEM34-C) und genügend Byte reserviert werden (MEM35-C).

Prüfe bei Ausdrücken unbeabsichtigten Wraparound bei vorzeichenlosen Ganzzahlen (INT30-C), Überlauf bei vorzeichenbehafteten Ganzzahlen (INT32-C), ungültige Division (INT33-C), Lesezugriffe auf nicht initialisierte Werte (EXP33-C) und das Dereferenzieren von Nullzeigern (EXP34-C). Behandle an Bibliotheksschnittstellen gemeldete Fehler (ERR33-C) und Konvertierungsfehler (ERR34-C). Vermeide system() gemäß ENV33-C, weil eine Shell dessen Eingabe interpretiert.

Risikotabellen bewerten Schweregrad, Wahrscheinlichkeit und Aufwand für die Behebung von 1 bis 3; eine höhere Bewertung des Aufwands bedeutet eine günstigere Behebung. Ihr Produkt ist die Priorität von 1 bis 27. Die Stufen gruppieren Prioritäten in L1 (12 bis 27), L2 (6 bis 9) und L3 (1 bis 4). Das hilft, Korrekturen zu ordnen, ohne Defekte mit niedrigerer Priorität für harmlos zu erklären.

Verwende Regeln bei der Codeüberprüfung und nutze Ergebnisse von Analysewerkzeugen, die ihren Kennungen zugeordnet sind. MISRA C ist ein weiterer Standard, der bei sicherheitskritischer eingebetteter Software verwendet wird. Regeln helfen, Defekte zu verhindern, während Warnungen, Tests und Sanitizer prüfen, ob die Implementierung sie tatsächlich befolgt. Das bloße Auswendiglernen von Kennungen kann weder ein fehlendes abschließendes Nullzeichen noch einen übersprungenen Fehlerzweig erkennen.

Wichtige Erkenntnisse

  • Lies den Fehlermechanismus und die regelkonforme Lösung zusammen.
  • CERT-Kennungen unterscheiden Kategorien, Regeln und Empfehlungen.
  • Halte externe Daten aus Formatstrings heraus.
  • Wende konkrete Prüfungen auf Grenzen, Lebensdauer, Arithmetik und Fehler an.
  • Nutze Risikobewertungen, um die Arbeit zu ordnen, und prüfe den Code anschließend mit Werkzeugen.

5.2 Sicher mit Zeichenketten und Puffern umgehen

Sicherer Umgang mit Zeichenketten benötigt eine gültige Quelle, eine bekannte Zielkapazität und eine Entscheidung über Text, der nicht hineinpasst. Eine Funktion mit Größenbegrenzung verhindert manche Überschreibungen, aber du musst weiterhin prüfen, ob die gesamte Operation erfolgreich war.

Die Kapazität mit dem Zeiger übergeben

sizeof misst den Typ seines Operanden. Für ein lokales char buf[20] ergibt es 20 Byte. In void fill(char buf[]) wird der Parameter zu einem Zeiger angepasst; sizeof buf misst diesen Zeiger, nicht das Array des Aufrufers. Übergib einen Kapazitätsparameter, etwa mit fill(buf, sizeof buf), solange beim Aufrufer noch der Arraytyp vorliegt.

Eine Zeichenkette mit fünf Zeichen benötigt einschließlich '\0' sechs Byte. Die Kapazität zählt jedes beschreibbare Byte; die Zeichenkettenlänge schließt das abschließende Nullzeichen aus. Deshalb reserviert malloc(strlen(src)) für eine Kopie ein Byte zu wenig.

Eine begrenzte Zeile lesen

Verwende fgets(buf, sizeof buf, stdin) und prüfe den Rückgabewert, bevor du den Inhalt verwendest. Die Funktion speichert höchstens die Kapazität minus eins Zeichen und dann ein abschließendes Nullzeichen. Sie behält ein Zeilenumbruchzeichen, wenn dieses hineinpasst. Nach einem erfolgreichen Lesevorgang entfernt buf[strcspn(buf, "\n")] = '\0'; ein gespeichertes Zeilenumbruchzeichen.

Füllt eine lange Zeile den Puffer, bleiben ungelesene Zeichen für den nächsten Aufruf übrig. Entscheide, ob du den Rest ablehnst und aus der Eingabe entfernst, mehr liest oder das Abschneiden akzeptierst. Das Entfernen eines Zeilenumbruchzeichens beweist nicht, dass du die vollständige Zeile gelesen hast. Bei gets lässt sich keine Kapazität angeben; die Funktion wurde in C11 entfernt. Verwende sie niemals.

Für ein einzelnes Wort in char name[20] begrenzt %19s in scanf die gespeicherten Zeichen und lässt Platz für das abschließende Nullzeichen. Prüfe, ob die erwartete Anzahl an Konvertierungen erfolgreich war. Lies nicht vertrauenswürdigen numerischen Text vorzugsweise als vollständige Zeile und wandle ihn anschließend mit geprüftem strtol um; %d zeigt undefiniertes Verhalten, wenn der konvertierte Wert nicht darstellbar ist, selbst wenn du die Rückgabeanzahl von scanf prüfst.

Eine verkürzte Kopie erkennen

strcpy und strcat kennen die Kapazität des Ziels nicht. strncpy kann das Ergebnis ohne abschließendes Nullzeichen lassen. Bei positiver Kapazität schließt snprintf den gespeicherten Text mit einem Nullzeichen ab und meldet, wie viele Zeichen die vollständige Ausgabe benötigt hätte:

char dst[8];
int r = snprintf(dst, sizeof dst, "%s", "debugging");
if (r < 0) {
    /* report encoding failure */
} else if ((size_t)r >= sizeof dst) {
    /* report truncation */
}

Die gespeicherte Zeichenkette ist „debuggi“ und r ist 9. Die vollständige Ausgabe benötigt einschließlich ihres abschließenden Nullzeichens zehn Byte. Prüfe, ob das Ergebnis negativ ist, bevor du es in size_t umwandelst. Damit das Ergebnis hineinpasst, muss r kleiner als die Kapazität sein.

Die Operation auf den Speicher abstimmen

Verwende memmove, wenn sich Quelle und Ziel überlappen; memcpy bei Überlappung ist undefiniert. strlcpy ist auf BSD-Systemen, macOS und ab glibc 2.38 verfügbar, gehört aber nicht zu ISO C. Funktionen aus Anhang K sind optional und in vielen Bibliotheken nicht verfügbar.

Halte Benutzerdaten aus dem Format selbst heraus. Stimme jede Konvertierung auf ihr Argument ab: %d für int, %ld für long, %zu für size_t, %f für double und %s für eine abgeschlossene Zeichenkette. %p benötigt einen void *, übergib also einen passend umgewandelten Zeiger. Eine Breitenbegrenzung kann einen unpassenden Argumenttyp nicht korrigieren.

Wichtige Erkenntnisse

  • Übergib die Zielkapazität zusammen mit einem Zeiger.
  • Reserviere ein Byte für das abschließende Nullzeichen der Zeichenkette.
  • Behandle unvollständige Eingabezeilen ausdrücklich.
  • Prüfe snprintf auf Fehler und Abschneiden.
  • Verwende memmove für überlappende Speicherbereiche.
  • Stimme jede Formatkonvertierung auf ihren Argumenttyp ab.

5.3 Eingaben auf Zulässigkeit prüfen und Fehler behandeln

Externe Daten können syntaktisch gültig und dennoch für die beabsichtigte Operation unsicher sein. Prüfe ihre Länge, ihr vollständiges Format und ihren sinnvoll nutzbaren Wertebereich, bevor sie zu einem Index, einer Allokationsgröße oder einer Anzahl von Schleifendurchläufen werden.

Die vollständige Zahl einlesen

atoi bietet keine zuverlässige Möglichkeit, eine gültige null von ungültigem Text zu unterscheiden, und bei einem nicht darstellbaren Ergebnis ist das Verhalten undefiniert. strtol liefert einen Endzeiger und meldet einen Wertebereichsfehler. Nachdem eine vollständige Zeile gelesen und ihr Zeilenumbruchzeichen entfernt wurde, kann ein strenger Parser für Dezimalzahlen Folgendes verwenden:

errno = 0;
char *end;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
    /* reject the numeric text */
} else if (v < 0 || v > 150) {
    /* reject this application's range */
} else {
    int age = (int)v;
    /* use age */
}

Binde <errno.h> und <stdlib.h> ein. Der erste Test unterscheidet drei Fehler: keine Ziffern, nachfolgenden Text und einen Wert außerhalb des Wertebereichs von long. Die Prüfung des anwendungsspezifischen Wertebereichs erfolgt vor der Verengung auf int. strtol akzeptiert führende Leerzeichen und ein optionales Vorzeichen; entscheide, ob die Grammatik der Anwendung sie erlaubt.

Für „42abc“ kann die Konvertierung 42 zurückgeben, aber end zeigt auf die verbleibenden Buchstaben. Für „abc“ werden keine Ziffern eingelesen. Bei einer zu großen dezimalen Ganzzahl wird errno zu ERANGE. Eine zurückgegebene Zahl ungleich null allein belegt weder eine vollständige Konvertierung noch einen sicheren Wertebereich.

Den Besitz im Fehlerfall erhalten

Jede Operation, die fehlschlagen kann, benötigt eine Reaktion. Prüfe Allokationsergebnisse, fopen, fgets, die Anzahl der Konvertierungen und den Abschluss von Ausgaben. Gibt fgets den Wert NULL zurück, kann das Eingabeende oder einen Lesefehler bedeuten. fclose kann einen Fehler beim gepufferten Schreiben melden. Verwende die zur Funktion passenden Fehlerinformationen; viele Bibliotheksaufrufe setzen errno, das perror oder strerror in eine lesbare Fehlermeldung umwandeln kann.

Schütze bei einer Größenänderung auf einen positiven Wert die Größenberechnung und den ursprünglichen Speicherblock:

if (new_n == 0) {
    free(a);
    a = NULL;
} else if (new_n > SIZE_MAX / sizeof *a) {
    /* reject the size */
} else {
    int *tmp = realloc(a, new_n * sizeof *a);
    if (tmp == NULL) {
        /* handle failure; a still owns its old block */
    } else {
        a = tmp;
    }
}

Wird das Ergebnis von realloc direkt dem einzigen besitzenden Zeiger zugewiesen, geht bei einem Fehler die Adresse des alten Blocks verloren. Behandle null gesondert: C17 macht realloc mit Größe null implementierungsdefiniert, C23 dagegen undefiniert. Verwende nach erfolgreicher Größenänderung den zurückgegebenen Zeiger und verwirf veraltete Aliase.

Einen klaren Fehlerpfad vorsehen

Ein einzelner Aufräumabschnitt kann angeforderte Ressourcen in umgekehrter Reihenfolge freigeben. Initialisiere besitzende Zeiger mit NULL, schließe nur geöffnete Dateien und gib nach der Meldung der Ursache einen Fehlerstatus zurück. Ein goto cleanup kann diese Pfade leichter überprüfbar machen.

Vermeide system() für Aufgaben mit externem Text: Die Shell kann diesen Text als Befehle interpretieren. Verwende vorzugsweise eine Bibliotheksoperation oder eine geeignete Schnittstelle zum Starten von Programmen, an die Argumente getrennt übergeben werden. Behandle Kommandozeilenargumente, Umgebungswerte, Dateiinhalte und Netzwerkdaten als Eingaben, die dieselbe Sorgfalt benötigen wie eingetippter Text.

Wichtige Erkenntnisse

  • Prüfe die vollständige Konvertierung und den anwendungsspezifischen Wertebereich, bevor du eine Zahl verwendest.
  • Setze errno vor strtol zurück und untersuche den Endzeiger.
  • Prüfe Bibliotheksfehler und erhalte Ressourcen auf jedem Pfad.
  • Verwende für realloc mit positiver Größe einen temporären Zeiger.
  • Behandle die Größe null und einen Multiplikationsüberlauf vor der Größenänderung.
  • Gib nur tatsächlich angeforderte Ressourcen frei.
  • Halte externen Text aus Shell-Befehlszeichenketten heraus.

5.4 Zielkonflikte bei der Speichersicherheit abwägen

Speichersicherheit bedeutet, dass Zugriffe innerhalb des Speichers bleiben, den ein Programm verwenden darf. C bietet direkte Kontrolle über den Speicher und überlässt Grenzen- und Lebensdauerprüfungen dem Programmierer.

Grenzen und Lebensdauer unterscheiden

Räumliche Sicherheit hält einen Zugriff innerhalb der Grenzen seines Objekts. Das Schreiben von Element zehn eines Arrays mit zehn Elementen verletzt sie. Zeitliche Sicherheit hält Zugriffe innerhalb der Lebensdauer des Objekts. Eine Speicherallokation nach free zu lesen, sie zweimal freizugeben oder eine zurückgegebene lokale Adresse zu verwenden, verletzt Lebensdauerregeln.

Die Folgen können über einen Absturz hinausgehen. Ein Lesezugriff kann vertrauliche Daten preisgeben. Ein Schreibzugriff kann eine Datei beschädigen, Kontrolldaten verändern oder eine Sicherheitslücke schaffen, wenn externe Eingaben die Operation steuern. Ein Programm, das nach einer Beschädigung weiterläuft, kann überzeugende, aber falsche Ergebnisse liefern.

Diese Belastung betrifft auch erfahrene Teams. Microsoft berichtete 2019, dass über mehr als ein Jahrzehnt hinweg Jahr für Jahr etwa 70 % der Sicherheitslücken, denen das Unternehmen CVE-Kennungen zugewiesen hatte, die Speichersicherheit betrafen. Im Dezember 2023 forderten CISA und Partnerbehörden Softwarehersteller auf, Übergänge zu speichersicheren Programmiersprachen zu planen. Dies sind konkrete historische Beobachtungen, kein gemessener Prozentsatz für jedes C-Programm.

Gründe für die Wahl von C verstehen

C bietet Kontrolle über Speicherlayout, Speicherallokation und Hardwarezugriff, ohne automatische Speicherbereinigung zu benötigen. Seine kleine Laufzeitumgebung eignet sich für Firmware mit begrenzten Ressourcen. Es gibt breite Compilerunterstützung, große vorhandene Codebestände und Schnittstellen, die viele andere Sprachen aufrufen können. Diese Vorteile erklären seinen Einsatz in Betriebssystemkernen, eingebetteter Software, Laufzeitumgebungen und Bibliotheken.

Sie machen nicht jedes C-Programm vorhersagbar schnell: Algorithmen, Speicherallokation, Caches und Systemaufrufe sind weiterhin wichtig. Der entscheidende Vorteil ist die Kontrolle über Kosten und Laufzeitanforderungen.

AnsatzSpeicherschutzKosten oder Verantwortung
CVom Programmierer durchgesetzte Grenzen und LebensdauernManuelle Analyse, Prüfungen und Tests
Sicheres RustBesitz, Ausleihen und GrenzenprüfungenErlernen der Besitzregeln
Go, Java, C#, PythonVerwaltete Lebensdauern und GrenzenprüfungenUnterschiedlicher Laufzeit- und Speicheraufwand
SwiftReferenzzählung und geprüfter ZugriffUnterschiedliche Besitz- und Laufzeitkosten

Unsichere Operationen und Schnittstellen zu C benötigen in ansonsten speichersicheren Systemen besondere Sorgfalt. Speichersicherheit lässt außerdem logische Fehler und durch Eingaben ausgelöste Injection als zu lösende Probleme bestehen.

Schutzebenen für das verbleibende Risiko wählen

Verhindere Defekte mit Programmierregeln, Warnungen und Codeüberprüfung. Erkenne Defekte mit statischer Analyse, Grenzfalltests, Sanitizern und Fuzzing. Begrenze verbleibende Schäden mit Stack-Schutz, unterstützten _FORTIFY_SOURCE-Prüfungen, zufälliger Anordnung des Adressraums (ASLR) und nicht ausführbarem Speicher. Eine Schutzmaßnahme kann einen Angriff stoppen oder eine Beschädigung in einen Absturz umwandeln, während der Defekt im Quelltext bestehen bleibt.

Die Kosten bestimmen den Einsatz. ASan verdoppelt typischerweise die Ausführungszeit und erhöht den Speicherbedarf; Valgrind verlangsamt die Ausführung üblicherweise um das 20- bis 30-Fache. Sie eignen sich besser für Tests als für den gewöhnlichen Einsatz in veröffentlichten Programmen. Härtungsmaßnahmen mit geringeren Kosten bleiben häufig in Release-Builds erhalten.

Für einen neuen Parser für Dateien aus dem Internet, bei dem keine Einschränkung C verlangt, verringert eine speichersichere Sprache ein großes Risiko. Bei vorhandener C-Firmware auf einem Mikrocontroller mit 32 KB Speicher können Sprachunterstützung und Ressourcenbeschränkungen für C mit strengen Prüfungen sprechen. Triff die Wahl anhand von Einschränkungen, Kontakt mit Eingaben und Wartbarkeit; auch der schrittweise Ersatz eines riskanten Parsers kann praktikabel sein.

Wichtige Erkenntnisse

  • Räumliche Sicherheit betrifft Grenzen; zeitliche Sicherheit betrifft Lebensdauern.
  • Speicherfehler können Daten preisgeben und Ergebnisse verfälschen.
  • C bietet direkte Kontrolle und breite Kompatibilität, überträgt die Verantwortung für Sicherheit aber dem Programmierer.
  • Speichersichere Sprachen beseitigen viele Speicherfehler, lassen aber andere Fehlerklassen bestehen.
  • Kombiniere Vermeidung, Erkennung und Schutzmaßnahmen für die Veröffentlichung.
  • Wähle eine Sprache anhand tatsächlicher Ressourcen- und Integrationseinschränkungen.

Fehlersuche in C: Beispiele

5 Interaktive Lektionen

1

Ein untersuchbares Programm erstellen

9 Schritte
Lies Compilermeldungen, korrigiere den Code, auf den sie hinweisen, und wähle getrennte Erstellungseinstellungen für Fehlersuche und Veröffentlichung.
  1. 1
    EinführungVor dem ersten Lauf beginnen

    Ein Programm kann kompilieren und dennoch einen Defekt enthalten, auf den der Compiler hinweisen kann. Deine erste Aufgabe ist, diese Rückmeldung in Form von Warnungen anzufordern und zu deuten.

    Du wirst eine versehentliche Zuweisung und ein unpassendes Format untersuchen und dann Einstellungen für eine Debuggersitzung (einen Debug-Build) und einen Release-Build auswählen. Diese Entscheidungen bereiten spätere Werkzeuge darauf vor, nützliche Hinweise statt unerklärter Adressen oder fehlender lokaler Werte zu zeigen.

  2. 2
    ErklärungDie Warnungen auswählen

    Beginne mit -std=c17 -Wall -Wextra -Wpedantic. Die erste Option legt die Sprachversion fest; die anderen aktivieren gängige Warnungen, zusätzliche Warnungen und Meldungen für viele Abweichungen von diesem Standard.

    -Wall bedeutet nicht jede Warnung. Ergänze -Wconversion für implizite Typumwandlungen, die Werte ändern, und -Wformat=2 für strengere Formatprüfungen. -Werror macht aktivierte Warnungen zu Erstellungsfehlern.

  3. 3
    BeispielDie Bedingung lesen

    Der Programmierer möchte eine leere Sammlung melden:

    if (items = 0) {
        puts("No items");
    }
    

    Eine Compilerwarnung weist auf eine Zuweisung in einer Bedingung hin. Frage zuerst, was der Ausdruck tut: Er speichert null in items, dann prüft die Bedingung diese null. Der Rumpf wird nie ausgeführt. Ersetze = durch ==, um die vorhandene Anzahl zu prüfen; zusätzliche Klammern um die Zuweisung würden die falsche Operation beibehalten.

  4. 4
    HerausforderungDen Ausgabetyp korrigieren

    double ratio = 2.5; wird mit printf("%d\n", ratio); ausgegeben. Was sollte geändert werden und warum?

    ---reveal---

    Verwende printf("%f\n", ratio);. %d erwartet einen int, während %f in printf einen double erwartet. Ein unpassendes Format führt zu undefiniertem Verhalten. Eine explizite Umwandlung in int würde die ausgegebene Zahl verändern, indem sie ihren Nachkommateil abschneidet. Sie würde daher das Ziel, das Verhältnis auszugeben, nicht erfüllen.

  5. 5
    TippMeldungen der Reihe nach lesen

    Eine Compilermeldung nennt gewöhnlich die Quelltextstelle, das Problem und die verantwortliche Option. Lies auch zugehörige Hinweise; sie können die widersprüchliche Deklaration nennen.

    Behebe den ersten Übersetzungsfehler und erstelle das Programm dann erneut. Ein fehlendes Semikolon kann das weitere Parsen stören und eine Kaskade von Meldungen erzeugen. Sind die Fehler beseitigt, untersuche die Ursache jeder Warnung. Auch ein Build ohne Warnungen benötigt Tests auf Logikfehler und eingabeabhängige Fehler.

  6. 6
    ErklärungNützliche Zustände erhalten

    Ergänze für einen einfachen Debug-Build -g -O0. Debuginformationen ordnen die Ausführung Namen und Zeilen im Quelltext zu; geringe Optimierung hält mehr Zwischenzustände für die Untersuchung verfügbar.

    -g deaktiviert die Optimierung nicht. Mit -O2 kann Code weiterhin im Debugger untersucht werden, aber Zeilen können zu springen scheinen und Variablen wegoptimiert werden. -Og ist eine weitere Möglichkeit für die Entwicklung, die leichte Optimierung beibehält.

  7. 7
    VerbindungPrüfungen für verschiedene Aufgaben wählen

    Statische Analyse verfolgt mögliche Quelltextpfade, ohne das Programm auszuführen. Ein Sanitizer prüft Operationen auf Pfaden, die die Tests tatsächlich ausführen. Verwende beides, um die Abdeckung zu erweitern.

    Härtung für die Veröffentlichung dient einem weiteren Zweck: Stack-Schutz und unterstützte Absicherungsprüfungen können manche verbleibenden Beschädigungen erkennen oder begrenzen. Diese Absicherung benötigt Optimierung und geeignete Bibliotheksunterstützung. Eine erkannte Überschreibung muss weiterhin im Quelltext korrigiert werden.

  8. 8
    HerausforderungPrüfungen im Release-Build erhalten

    Ein Debug-Build verwendet strenge Warnungen und Zusicherungen. Ein Release-Build ergänzt -O2 -DNDEBUG. Der Programmierer schlägt vor, assert(valid_input) als einzige Eingabeprüfung zu verwenden. Was muss vor der Veröffentlichung geändert werden?

    ---reveal---

    Prüfe die Eingabe im gewöhnlichen Kontrollfluss und behandle Fehler. NDEBUG entfernt die Auswertung von Zusicherungen, sodass die vorgeschlagene Prüfung entfällt. Lass Warnungen aktiviert und teste sowohl die Release-Konfiguration als auch den Debug-Build; unterschiedliche Einstellungen können verborgene Defekte aufdecken.

  9. 9
    ZusammenfassungDie Erstellungseinstellungen verbinden

    Nutze Compilermeldungen, um korrekturbedürftige Operationen zu erkennen. Stimme Formate auf Typen ab, untersuche Typumwandlungen und behebe frühe Fehler, bevor du ihren Folgemeldungen nachgehst.

    Erstelle für die nächste Debuggersitzung die richtige ausführbare Datei mit Quelltextinformationen und einer geeigneten Optimierungsstufe. Halte Konfigurationen reproduzierbar, ergänze Analyse und Tests mit Sanitizern und behalte Eingabeprüfungen bei, wenn Release-Einstellungen Zusicherungen entfernen.

2

Eine falsche Summe und einen Absturz zurückverfolgen

12 Schritte
Gehe von der Reproduktion zur schrittweisen Ausführung, zu Aufrufrückverfolgungen und gezielten Halten über, die den Ursprung eines fehlerhaften Werts aufzeigen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungAus einem Symptom eine Frage machen
  2. 2
    ErklärungDen fehlerauslösenden Fall bewahren
  3. 3
    HerausforderungEinen kontrollierten Versuch wählen
  4. 4
    BeispielAm Schleifenrumpf anhalten
  5. 5
    BeispielDen gültigen Durchläufen folgen
  6. 6
    HerausforderungDen zusätzlichen Durchlauf korrigieren und prüfen
  7. 7
    TippNur in hilfreichen Code hineingehen
  8. 8
    BeispielZum Aufrufer wechseln
  9. 9
    ErklärungEine Änderung bei ihrem Auftreten erfassen
  10. 10
    HerausforderungEinen Absturz in der Speicherverwaltung untersuchen
  11. 11
    VerbindungEinen gespeicherten Absturzzustand nutzen
  12. 12
    ZusammenfassungDie Ansicht wählen, die die Frage beantwortet
3

Die Operation prüfen, bevor sie ungültig wird

12 Schritte
Untersuche Optimierung, arithmetische Schranken und Objektlebensdauern, ohne dich auf zufällige Ausgaben zu verlassen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungFragen, was C garantiert
  2. 2
    ErklärungAbweichungen von einem Defekt unterscheiden
  3. 3
    BeispielEin Überlauftest, der zu spät kommt
  4. 4
    HerausforderungDie Zeigerprüfung platzieren
  5. 5
    BeispielEine vorzeichenbehaftete Summe absichern
  6. 6
    KernaussageAuch definierter Wraparound kann das Programm stören
  7. 7
    HerausforderungVor der Multiplikation erweitern
  8. 8
    TippMehrere Grenzen bei Ausdrücken prüfen
  9. 9
    BeispielEine Endmarkierung von einem Element unterscheiden
  10. 10
    BeispielJeden Alias bei der Freigabe verfolgen
  11. 11
    HerausforderungEine Summe und ihre Lebensdauer korrigieren
  12. 12
    ZusammenfassungTyp, Wertebereich, Grenzen und Lebensdauer prüfen
4

Prüfungen als Hinweise auf einen bestimmten Defekt lesen

11 Schritte
Wähle Prüfungen mit Zusicherungen, ASan und UBSan und verbinde anschließend jeden Bericht mit einer dauerhaften Korrektur.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMehr als die endgültige Ausgabe prüfen
  2. 2
    ErklärungEine interne Zusage zusichern
  3. 3
    HerausforderungDie Operation in jedem Build erhalten
  4. 4
    ErklärungEine instrumentierte ausführbare Datei erstellen
  5. 5
    BeispielDen gemeldeten Bereich lesen
  6. 6
    HerausforderungDie zugehörige Schleife korrigieren
  7. 7
    KernaussageEine fehlende Meldung hat mehrere Erklärungen
  8. 8
    BeispielDen Test bei einem arithmetischen Fehler stoppen
  9. 9
    ErklärungWeitere Prüfwerkzeuge bewusst auswählen
  10. 10
    HerausforderungEinen unvollständigen Prüfablauf verbessern
  11. 11
    ZusammenfassungBerichte in wiederholbare Prüfungen umwandeln
5

Eingaben, Größen und Besitz ausdrücklich festhalten

15 Schritte
Wende Programmierregeln auf einen kleinen Eingabeverarbeitungspfad an und wäge anschließend den Sicherheitsaufwand bei der Wahl von C ab.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDaten durch das Programm verfolgen
  2. 2
    ErklärungDie Regel als Frage verwenden
  3. 3
    HerausforderungEingaben als Daten behandeln
  4. 4
    BeispielDie Kapazität vom Aufrufer übergeben
  5. 5
    BeispielEinen sicheren Anfangsteil von Erfolg unterscheiden
  6. 6
    HerausforderungEine unvollständige Zeile behandeln
  7. 7
    BeispielDas Ende der Konvertierung untersuchen
  8. 8
    BeispielDen Wertebereich des Programms anwenden
  9. 9
    BeispielDen besitzenden Zeiger bei einer Größenänderung erhalten
  10. 10
    HerausforderungDen Fehlerpfad planen
  11. 11
    ErklärungDie Sicherheitsgrenze erkennen
  12. 12
    VerbindungSchutzebenen mit unterschiedlichen Aufgaben wählen
  13. 13
    HerausforderungEine Sprachentscheidung treffen
  14. 14
    HerausforderungDen vollständigen Eingabepfad prüfen
  15. 15
    ZusammenfassungJede Grenze sichtbar halten

Fehlersuche in C: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Compilerhinweise und Entscheidungen im Debugger10 Übungsfragen

Frage 1 von 10

Ein Build mit -Wall ohne Meldungen wandelt eine Anzahl vom Typ double stillschweigend in int um. Welche zusätzliche Warnoption zielt auf diese implizite Wertänderung?
  1. -Wshadow
  2. -Wformat=2
  3. -Wconversion
  4. -Wpedantic
Antwort zeigenAntwort ausblenden
Antwort: C
-Wconversion zielt auf implizite Typumwandlungen, die einen Wert verändern können, einschließlich des Abschneidens eines Gleitkomma-Nachkommateils. -Wshadow betrifft eine verdeckte Deklaration mit demselben Namen, keine Typänderung. Eine Warnung sollte dazu führen, den beabsichtigten Wert zu prüfen, bevor eine explizite Typumwandlung ergänzt wird.

Frage 2 von 10

Ein Programm muss einen Messwert mit Nachkommateil ausgeben. Es deklariert double reading = 7.25; und ruft printf("%d\n", reading); auf. Welche Korrektur erhält dieses Ziel?
  1. Verwende %f mit reading.
  2. Wandle reading in int um.
  3. Verwende %ld mit reading.
  4. Wandle reading in long um.
Antwort zeigenAntwort ausblenden
Antwort: A
printf erwartet für %f einen double. Die Änderung des Formats erhält daher den Nachkommateil und passt zum Argument. Eine Umwandlung in int könnte zu %d passen, würde aber den Nachkommateil verwerfen, den das Programm ausgeben muss. Die ursprüngliche Abweichung führt zu undefiniertem Verhalten; das Format ist keine automatische Zahlenkonvertierung.

Frage 3 von 10

Ein Debugger zeigt Funktionsadressen, kann aber keine Quelltextzeilen oder Namen lokaler Variablen anzeigen. Welche Compileroption liefert die fehlenden Quelltextinformationen?
  1. -Wextra
  2. -DNDEBUG
  3. -Werror
  4. -g
Antwort zeigenAntwort ausblenden
Antwort: D
-g ergänzt die Debuginformationen, die die Ausführung Quelltextstellen, Typen und Namen zuordnen. -Wextra aktiviert zusätzliche Meldungen während der Kompilierung, liefert aber diese Zuordnungen nicht. Die Optimierung bleibt eine separate Entscheidung und kann auch bei vorhandenen Debuginformationen Werte entfernen.

Frage 4 von 10

Eine Eingabe mit 240 Datensätzen führt zum Absturz, aber jede Hälfte gelingt bei separater Ausführung. Welche Strategie zum weiteren Verkleinern ist am hilfreichsten?
  1. Nur den letzten Datensatz behalten und als unabhängigen Auslöser testen.
  2. Kleinere Teile entfernen und die fehlerauslösende Folge erhalten.
  3. Jeden Puffer verdoppeln und die ursprüngliche Eingabe verwerfen.
  4. Eine der erfolgreichen Hälften ausführen, bis der Absturz zurückkehrt.
Antwort zeigenAntwort ausblenden
Antwort: B
Das Fehlverhalten kann von mehreren Datensätzen zusammen oder von einem aufgebauten Zustand abhängen. Behalte daher eine fehlerauslösende Folge bei, während du kleinere Teile entfernst. Einen einzelnen Datensatz nur deshalb auszuwählen, weil er der letzte ist, setzt eine Unabhängigkeit voraus, die die beiden erfolgreichen Hälften nicht belegen. Jede Verkleinerung muss anhand des tatsächlichen Fehlverhaltens geprüft werden.

Frage 5 von 10

Die Ausführung ist an einem Aufruf angehalten, den du als einen Quelltextschritt behandeln möchtest. Welcher Debuggerbefehl überspringt diesen Aufruf normalerweise?
  1. step
  2. next
  3. print
  4. backtrace
Antwort zeigenAntwort ausblenden
Antwort: B
next führt normalerweise die aktuelle Quelltextzeile aus und überspringt dabei ihre Funktionsaufrufe. step dient dazu, in eine aufgerufene Funktion hineinzugehen, wenn hilfreiche Quelltextinformationen vorhanden sind. Ein Haltepunkt innerhalb des Aufrufs kann next dennoch unterbrechen. Der Befehl beschreibt also das schrittweise Vorgehen und deaktiviert nicht alle Halte.

Frage 6 von 10

Auf result = compute(); liegt ein Haltepunkt. Die Ausgabe von result zeigt den früheren Wert, und bei diesem Halt wurde noch kein Aufruf ausgeführt. Wie solltest du das berechnete Ergebnis untersuchen?
  1. Erneut print result eingeben, ohne fortzufahren.
  2. Den Haltepunkt löschen und den Initialisierer ändern.
  3. Zum Aufrufer wechseln, ohne den Aufruf auszuführen.
  4. next verwenden und dann das neue result untersuchen.
Antwort zeigenAntwort ausblenden
Antwort: D
Der Halt liegt vor der Zuweisung, sodass result noch seinen früheren Wert enthält. Wird die Zeile mit next ausgeführt, können Aufruf und Zuweisung vor der Untersuchung abgeschlossen werden. Eine wiederholte Ausgabe ohne Fortsetzung zeigt denselben Zustand und liefert keinen Beleg dafür, dass die Zuweisung fehlgeschlagen ist.

Frage 7 von 10

Eine Aufrufrückverfolgung zeigt Stackframe null in strlen und Stackframe zwei in deiner Funktion render. Du möchtest den Zeiger caption von render in GDB untersuchen. Was solltest du tun?
  1. frame 2 und dann p caption ausführen.
  2. frame 0 und dann p caption ausführen.
  3. next und dann p caption ausführen.
  4. continue und dann p caption ausführen.
Antwort zeigenAntwort ausblenden
Antwort: A
Die Auswahl eines Stackframes macht den lokalen Kontext der gewählten Funktion verfügbar, ohne weiteren Code auszuführen. frame 2 wählt render aus, wo caption untersucht werden kann. In Stackframe null zu bleiben untersucht den Kontext der Bibliothek; die Ausführung fortzusetzen ist unnötig, wenn der benötigte Hinweis bereits im Stackframe eines Aufrufers liegt.

Frage 8 von 10

Eine Anzahl wird zwischen zwei bekannten Beobachtungen, einer korrekten und einer falschen, irgendwo falsch. Die schreibende Stelle ist unbekannt. Welche Debuggerfunktion findet diesen Schreibzugriff am besten?
  1. Ein Haltepunkt beim letzten printf-Aufruf.
  2. Eine Aufrufrückverfolgung nach normaler Beendigung.
  3. Ein Datenhaltepunkt auf der noch gültigen Anzahlvariablen.
  4. Eine Quelltextliste um die Deklaration der Variablen.
Antwort zeigenAntwort ausblenden
Antwort: C
Ein Datenhaltepunkt hält an, wenn sich der überwachte Speicher ändert, und zeigt die verantwortliche Operation sowie den alten und den neuen Wert. Ein Haltepunkt bei der letzten Ausgabe beobachtet nur den bereits falschen Zustand. Prüfe jeden Schreibzugriff, bei dem das Programm anhält, anhand der beabsichtigten Regel für die Anzahl, denn manche Änderungen der Variablen können zulässig sein.

Frage 9 von 10

Ein Programm ohne Warnungen erzeugt bei -O0 und -O2 unterschiedliche Summen. Bei -O2 ist außerdem eine Zwischenvariable im Debugger nicht verfügbar. Welche Untersuchung berücksichtigt beide Beobachtungen?
  1. Die Optimierung dauerhaft entfernen und die ursprüngliche Debug-Summe akzeptieren.
  2. -O2 beibehalten und die fehlende Variable als Beschädigung behandeln.
  3. Debugsymbole deaktivieren und die ausgegebenen Adressen vergleichen.
  4. UB mit Sanitizern untersuchen und für die schrittweise Ausführung -g -O0 verwenden.
Antwort zeigenAntwort ausblenden
Antwort: D
Optimierung kann Zwischenwerte zulässigerweise entfernen, während veränderte Summen ein Grund sind, undefinierte Operationen im Code zu untersuchen. Ein Sanitizer-Build hilft, diese Operationen zu finden, und ein Debug-Build mit geringer Optimierung erleichtert die Untersuchung. Wird allein das Debug-Ergebnis akzeptiert, bleibt die Ursache des Unterschieds im Release-Build ungeklärt.

Frage 10 von 10

Ein Parser stürzt nach dem Lesen vieler Datensätze in free ab. Das Vergrößern eines Namenspuffers verhindert den sichtbaren Absturz, aber bei der gespeicherten Eingabe meldet ASan weiterhin einen Schreibzugriff hinter diesem Puffer. Welche Maßnahme behebt die Ursache?
  1. Den größeren Puffer behalten und ASan für diesen Test deaktivieren.
  2. Die Grenze des Schreibzugriffs zurückverfolgen und korrigieren, dann die Eingabe behalten.
  3. Den Aufruf der Speicherverwaltung durch eine Zusicherung ersetzen und den größeren Puffer behalten.
  4. Den Absturz der Speicherverwaltung abfangen und mit den Datensätzen fortfahren.
Antwort zeigenAntwort ausblenden
Antwort: B
Der ASan-Bericht zeigt einen ungültigen Schreibzugriff, der trotz des veränderten Symptoms bestehen bleibt. Korrigiere den Index oder die Kopiergrenze, führe den Test mit der ursprünglichen Eingabe erneut aus und behalte sie als Regressionstest. Wird benachbarter Speicher vergrößert, kann das eine Beschädigung vor einer späteren Prüfung der Speicherverwaltung verbergen, ohne den ursprünglichen Zugriff gültig zu machen.
Set 2: Typen, Wertebereiche und existierende Objekte10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

In merge(left(), right()) werden zwei unabhängige Aufrufe als Argumente übergeben. Beide Auswertungsreihenfolgen sind erlaubt und kein gemeinsam genutzter Zustand verursacht einen Konflikt. Wie wird diese Reihenfolge in C eingeordnet?
  1. Undefiniertes Verhalten.
  2. Nicht spezifiziertes Verhalten.
  3. Implementierungsdefiniertes Verhalten.
  4. Eine vorgeschriebene Reihenfolge von links nach rechts.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Gehe von einem unsigned int mit 32 Bit aus. Welchen Wert speichert unsigned int slots = 0u - 1u;?
  1. Null, weil vorzeichenlose Werte nicht negativ sein können.
  2. Minus eins, wobei das mathematische Ergebnis erhalten bleibt.
  3. Keinen definierten Wert, weil die Subtraktion unterläuft.
  4. 4.294.967.295 durch definierten vorzeichenlosen Wraparound.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Gehe von einem unsigned int mit 32 Bit aus. Eine Eingabe liefert shift = 32. Welchen Status hat 1u << shift?
  1. Undefiniert: Die Verschiebungsweite erreicht die Bitbreite des Typs.
  2. Definiert als null: Alle gesetzten Bits werden jenseits der Ergebnisbreite verworfen.
  3. Definiert als eins: Die Verschiebungsweite wird modulo der Bitbreite reduziert.
  4. Implementierungsdefiniert: Vorzeichenlose Verschiebungen unterscheiden sich.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Array hat sieben Elemente. Was ist der größte gültige Elementindex?
  1. 7
  2. 8
  3. 6
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine Funktion gibt local zurück, nachdem sie char local[24] = "ready"; deklariert hat. Warum ist es ungültig, das Ergebnis beim Aufrufer zu lesen?
  1. Das Array hat zu viel ungenutzten Platz für eine Zeichenkette.
  2. Die Rückgabe wandelt das Array in ein Zeichenkettenliteral um.
  3. Die Lebensdauer des lokalen Arrays endete bei der Rückkehr.
  4. Der Aufrufer muss vor jedem Zeichenkettenlesen sizeof anwenden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welche Bedingung erkennt für einen vorzeichenbehafteten int a und einen positiven int b einen Überlauf von a + b, ohne diese Summe auszuwerten?
  1. a > INT_MAX - b
  2. a + b > INT_MAX
  3. a > INT_MAX + b
  4. a - b > INT_MAX
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine Hilfsfunktion führt int old = *p; vor if (p == NULL) return -1; aus. Ein Test mit einem Nullzeiger scheint bei geringer Optimierung den Fehlerwert zurückzugeben, stürzt aber im Release-Build ab. Welche Korrektur schafft einen definierten Pfad für den Nullzeiger?
  1. Die ursprüngliche Reihenfolge beibehalten und die gelesene lokale Variable als volatile deklarieren.
  2. Die Reihenfolge beibehalten und ohne -Werror kompilieren.
  3. p zwischen dem Lesezugriff und der Prüfung ausgeben.
  4. Den Nulltest vor das erste Dereferenzieren verschieben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Auf einer Plattform mit einem int von 32 Bit und einem long long von 64 Bit sind beide Faktoren vom Typ int: units = 70000, rate = 40000. Welcher Ausdruck berechnet ihr Produkt korrekt in long long?
  1. (long long)(units * rate)
  2. (long long)units * rate
  3. (int)((long long)units * rate)
  4. (long long)(int)(units * rate)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine C-Funktion besitzt über p einen Heap-Puffer; alias zeigt in ihn hinein. Ein realloc mit positiver Größe gelingt und der Block wird verschoben. Die Funktion weist das Ergebnis p zu, schreibt über alias und gibt dann sowohl p als auch alias frei. Welche Korrektur der Besitzverwaltung ist erforderlich?
  1. Den veralteten Alias verwerfen, den zurückgegebenen Block verwenden und einmal freigeben.
  2. Den veralteten Alias behalten, den neuen Block vergrößern und beide Zeiger freigeben.
  3. Den Alias auf null prüfen, darüber schreiben und nur diesen Zeiger freigeben.
  4. Den zurückgegebenen Block verwenden und dann den Alias vor dem besitzenden Zeiger freigeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Prüf-Build verwendet nur -fsanitize=undefined. Eine size_t-Multiplikation führt zu Wraparound, erzeugt aber keine Meldung. Warum kann das geschehen?
  1. Die Prüfung läuft nur, wenn NDEBUG definiert ist.
  2. Der Wraparound bleibt verborgen, weil size_t ein Zeiger ist.
  3. Vorzeichenloser Wraparound ist definiert und liegt außerhalb dieser Gruppe.
  4. UBSan prüft nur Schreibzugriffe auf Stack- und Heap-Arrays.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Zusicherungen und Sanitizerberichte10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Eine private Listenhilfsfunktion sichert zu, dass ihre gespeicherte Anzahl nach jeder Aktualisierung immer der Knotenzahl entspricht. Um welche Art von Programmierbedingung handelt es sich?
  1. Eine Invariante.
  2. Ein Eingabeformat.
  3. Ein Allokationsergebnis.
  4. Eine Compileroption.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Eine Anwendung verwendet assert(user_count > 0); als einzige Zurückweisung ungültiger Benutzeranzahlen. Release-Builds definieren NDEBUG. Was sollte diesen Schutz ersetzen?
  1. Eine statische Zusicherung für dieselbe Laufzeitvariable.
  2. Eine zusätzliche Zusicherung nach erfolgreicher Speicherallokation.
  3. Eine Umwandlung von user_count in einen vorzeichenlosen Typ.
  4. Eine gewöhnliche Wertebereichsprüfung mit einem Fehlerpfad.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

ASan meldet einen Schreibzugriff von vier Byte „0 bytes after 28-byte region“. Gehe davon aus, dass jedes Arrayelement vier Byte groß ist. Welcher versuchte Index beginnt an dieser Adresse?
  1. Index 6.
  2. Index 7.
  3. Index 8.
  4. Index 28.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Test muss gewöhnliche Verwendungen nicht initialisierter Werte erkennen. Warum reicht ein Build nur mit ASan nicht aus?
  1. ASan verfolgt diese Verwendungen nur auf dem Heap.
  2. ASan verfolgt diese Verwendungen nur bei globalen Variablen.
  3. ASan verfolgt diese Verwendungen von Werten nicht.
  4. ASan benötigt den Fortsetzungsmodus, um sie zu verfolgen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein automatisierter UBSan-Test protokolliert einen Überlauf bei vorzeichenbehafteten Ganzzahlen, läuft aber weiter und meldet Erfolg. Welche Option sorgt dafür, dass ein erkannter UB-Fehler den Lauf stoppt?
  1. -fno-omit-frame-pointer: Stackframes erhalten.
  2. -Wconversion: Implizite Wertänderungen melden.
  3. -fno-sanitize-recover=undefined
  4. -D_FORTIFY_SOURCE=2: Unterstützte Kopien prüfen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Test schreibt über die Arraykomponente einer Struktur hinaus in ihre nächste Komponente und ein vorzeichenloses Produkt zur Berechnung einer Byteanzahl läuft durch Wraparound über. ASan und -fsanitize=undefined melden nichts, aber der benachbarte Wert und die Allokationsgröße sind falsch. Welche Untersuchung behandelt beide Defekte?
  1. Die Komponentengrenzen untersuchen und das vorzeichenlose Größenprodukt vor der Verwendung absichern.
  2. Die Komponentengrenzen akzeptieren und nur vorzeichenbehaftete Arithmetik in der Summe untersuchen.
  3. Die Struktur vergrößern und die durch Wraparound veränderte Allokationsgröße als geprüft akzeptieren.
  4. Den Komponentenindex beibehalten und jede gespeicherte Anzahl in eine Zusicherung umwandeln.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein Team möchte sowohl Heap-Überläufe als auch Verwendungen nicht initialisierter Werte untersuchen. Sein aktueller ASan-Build linkt normal kompilierte Bibliotheken von Drittanbietern. Welche Prüfanordnung berücksichtigt die Anforderungen beider Werkzeuge?
  1. ASan- und MSan-Instrumentierung kombinieren und alle Drittanbieterbibliotheken unverändert lassen.
  2. Statt beider Werkzeuge UBSan verwenden und die Bibliotheken unverändert lassen.
  3. Die ausführbare ASan-Datei für jeden Test unter Valgrind ausführen.
  4. ASan beibehalten und MSan separat mit kompatiblen Abhängigkeiten erstellen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Du hast ein Speicherabbild von einem Absturz, der sich nicht mehr reproduzieren lässt. Was kannst du mit der passenden ausführbaren Datei tun?
  1. Ab dem Absturz fortfahren und die Eingabe ändern.
  2. Stackframes und verfügbare gespeicherte Variablen untersuchen.
  3. Frühere Schreibzugriffe aus der gespeicherten Aufrufrückverfolgung erneut ausführen.
  4. Das Abbild in eine korrigierte ausführbare Datei umkompilieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Hilfsfunktion führt assert(++used < capacity); aus, bevor sie bei Index used speichert. Tests bestehen, aber mit NDEBUG überschreibt sie wiederholt denselben Platz. Welche Quelltextänderung erhält die beabsichtigte Zustandsaktualisierung in beiden Builds?
  1. Gesondert inkrementieren und dann die interne Schranke zusichern.
  2. Das Inkrement in assert lassen und -O0 verwenden.
  3. Die Zusicherung unverändert durch static_assert ersetzen.
  4. used vorzeichenlos machen und die Zusicherung beibehalten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Fuzzing findet einen Absturz, wenn ein Parser „-3“ konvertiert, in size_t umwandelt, einen von der Anzahl abhängigen Puffer reserviert und ihn füllt. Er prüft weder die vollständige Konvertierung noch den anwendungsspezifischen Wertebereich. Welche Reihenfolge der Korrekturen behandelt den Pfad von der Eingabe bis zum Speicher?
  1. Die Allokation vergrößern, zuerst umwandeln und jede Konvertierung ungleich null akzeptieren.
  2. Das Füllen nach der Ausführung prüfen und dann über den Konvertierungserfolg entscheiden.
  3. Konvertierung und Anzahl prüfen, das Größenprodukt absichern und dann die Allokation prüfen.
  4. Die Füllanzahl vorzeichenlos machen und Konvertierung und Allokation beibehalten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Zeichenketten, numerische Eingaben und Besitz von Ressourcen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein Programm muss eine in einer Datei gelieferte abgeschlossene Zeichenkette ausgeben, ohne eingebettete Formatkonvertierungen zu interpretieren. Welcher Aufruf tut das?
  1. printf(label);
  2. printf("%s", label);
  3. printf("%d", label);
  4. printf("%p", label);
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Aufrufer übergibt char line[48] an read_line(char line[]) auf einem System mit Zeigern von acht Byte. Was misst sizeof line innerhalb der Funktion?
  1. Das Array des Aufrufers mit 48 Byte.
  2. Die aktuelle Länge der Zeichenkette.
  3. Die Arraygröße plus ein abschließendes Nullzeichen.
  4. Den Zeigerparameter mit acht Byte.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

snprintf wird mit einer Zielkapazität von 11 aufgerufen und gibt 11 zurück. Was steht damit fest?
  1. Die vollständige Ausgabe passte nicht hinein.
  2. Genau elf Zeichen wurden gespeichert.
  3. Die Ausgabe passte mit ihrem abschließenden Nullzeichen hinein.
  4. Ein Kodierungsfehler verhinderte die Ausgabe.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein strenger Parser für Dezimalzahlen setzt errno zurück und konvertiert „64kg“ mit strtol. Die Konvertierung gibt 64 zurück und errno bleibt null. Was muss zur Ablehnung führen?
  1. Der zurückgegebene Zahlenwert ist ungleich null.
  2. Das Basisargument ist dezimal.
  3. Der Endzeiger zeigt auf nachfolgenden Text.
  4. Das Ergebnis passt in den Wertebereich von long.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Programm akzeptiert dezimale Anzahlen von 1 bis 900. Es liest „ -4rest“ mit strtol ein, prüft nur errno, wandelt in size_t um und multipliziert für die Speicherallokation mit der Elementgröße. Welche Prüfungen müssen der Allokation vorausgehen?
  1. Den Anfangsteil umwandeln, multiplizieren und dann ein Ergebnis von null und eine fehlgeschlagene Allokation ablehnen.
  2. Vollständige Konvertierung und zulässigen Wertebereich verlangen und dann das vorzeichenlose Produkt absichern.
  3. Den Anfangsteil bei errno gleich null akzeptieren und dann nur die Pufferkapazität durchsetzen.
  4. Das Minuszeichen entfernen, den Anfangsteil akzeptieren und dann eine Zusicherung für die Größe verwenden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Array befindet sich ausschließlich über a in Besitz. Nachdem eine positive eingegebene Anzahl in new_n gelesen wurde, führt der Code a = realloc(a, new_n * sizeof *a); a[new_n - 1] = 0; aus. Jede gültige Anzahl muss behandelt werden, und eine fehlgeschlagene Größenänderung muss das alte Array erhalten. Welche Reihenfolge ist erforderlich?
  1. Das Produkt absichern, die Größe über einen temporären Zeiger ändern und erst nach Erfolg schreiben.
  2. Das Produkt zuerst berechnen, die Größe direkt über a ändern und die alte Größe prüfen.
  3. Die Größe über einen temporären Zeiger ändern, null ignorieren und über das ursprüngliche a schreiben.
  4. Nur auf null prüfen, a ersetzen und das Produkt nach dem Schreiben prüfen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein Zeilenpuffer mit 12 Byte erhält von fgets 11 Zeichen, ohne dass ein Zeilenumbruchzeichen gespeichert wird. Die Anwendung verlangt einen vollständigen Datensatz pro Zeile. Welche Behandlung verhindert, dass ein möglicher Rest als neuer Datensatz gilt?
  1. Das letzte gespeicherte Zeichen entfernen und den Anfangsteil als vollständige Zeile akzeptieren.
  2. Den Anfangsteil parsen und den nächsten Aufruf einen Datensatz erzeugen lassen.
  3. Ein abschließendes Nullzeichen anhängen und die Zeile für vollständig erklären.
  4. Die verbleibende Eingabe ermitteln und die Zeilenrichtlinie durchsetzen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Funktion erhält einen Zielzeiger und die Kapazität 7. Sie muss eine vollständige Eingabe mit neun Zeichen erhalten oder einen Fehler melden. Sie verwendet strncpy(dst, src, sizeof dst) und gibt später dst mit %s aus. Gehe von Zeigern mit acht Byte aus. Welcher Neuentwurf erfüllt die Anforderung?
  1. sizeof dst beibehalten, nach acht Byte ein Nullzeichen anhängen und den Anfangsteil ausgeben.
  2. Sechs Zeichen kopieren, ein Nullzeichen anhängen und Erfolg für den Anfangsteil melden.
  3. Die übergebene Kapazität verwenden, eine begrenzte Kopie prüfen und Abschneiden ablehnen.
  4. Die Eingabe als printf-Format verwenden, sodass keine Prüfung der Zielgröße nötig ist.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Programm wird nun als C23 erstellt. Es liest einen size_t count aus einer Datei und führt sofort a = realloc(a, count * sizeof *a); aus. Leere Arrays sind erlaubt, und eine fehlgeschlagene Anforderung muss das alte Array erhalten. Welcher Neuentwurf erfüllt diese Anforderungen für jede Anzahl?
  1. realloc für jede Größe aufrufen, dem besitzenden Zeiger zuweisen und dann das Produkt auf Überlauf prüfen.
  2. Null gesondert behandeln, das Produkt absichern und dann einen temporären Zeiger verwenden.
  3. Das Produkt in einen vorzeichenbehafteten int umwandeln und dann realloc aufrufen.
  4. assert(new_n > 0) verwenden und dann den besitzenden Zeiger direkt ersetzen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein neuer Dateiparser verarbeitet Daten aus dem Internet und unterliegt keiner Integrations-, Hardware- oder Ressourcenbeschränkung, die C verlangt. Welche Entwurfsentscheidung verringert das Risiko bei der manuellen Prüfung von Grenzen und Lebensdauern am unmittelbarsten?
  1. Für den Parser eine speichersichere Sprache verwenden.
  2. C nur mit einem größeren Standardpuffer verwenden.
  3. C verwenden und alle dynamischen Speicherallokationen deaktivieren.
  4. C nur mit zufälliger Adressanordnung verwenden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 5: Zusammenwirkende Defekte beheben10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein Zeiger wird verwendet, nachdem sein Objekt freigegeben wurde. Welcher Teil der Speichersicherheit wird verletzt?
  1. Formatstringsicherheit.
  2. Räumliche Sicherheit.
  3. Numerische Sicherheit.
  4. Zeitliche Sicherheit.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Für int i = 4; in C17 schreibt ein Programmierer printf("%d %d\n", i, ++i);. Welche Korrektur macht die beabsichtigte Ausgabe „alter Wert, dann neuer Wert“ wohldefiniert?
  1. i in old speichern, i inkrementieren und dann old und i ausgeben.
  2. Die Reihenfolge der beiden Argumente tauschen und das Inkrement im Aufruf behalten.
  3. Klammern um ++i setzen und beide Argumente beibehalten.
  4. Beide Argumente innerhalb desselben Aufrufs in unsigned umwandeln.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine CERT-Risikobewertung ergibt einen Schweregrad von 3, eine Wahrscheinlichkeit von 2 und eine Bewertung des Behebungsaufwands von 2. Welche Priorität und Stufe ergeben sich?
  1. Priorität 7, Stufe L2.
  2. Priorität 9, Stufe L2.
  3. Priorität 12, Stufe L1.
  4. Priorität 18, Stufe L1.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Text wird innerhalb desselben Arrays nach links verschoben, und die Bytebereiche von Quelle und Ziel überlappen sich. Welche Funktion bietet für diese Situation ein definiertes Kopieren?
  1. memcpy
  2. memmove
  3. strncpy
  4. snprintf
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine Funktion besitzt word, berechnet int bytes = strlen(word);, reserviert diese Anzahl an Byte, kopiert die vollständige Zeichenkette, gibt den neuen Block frei und gibt dessen Adresse zurück. Eingaben können für int zu lang sein. Welcher Entwurf behandelt Größe und Lebensdauer?
  1. Die int-Anzahl behalten, ein Byte ergänzen und die Freigabe vor der Rückgabe beibehalten.
  2. size_t verwenden, aber die Länge in Byte reservieren und den resultierenden Block übertragen.
  3. Eine geprüfte size_t-Länge samt abschließendem Nullzeichen verwenden und einen gültigen, geprüften Speicherblock übertragen.
  4. Die int-Anzahl und Allokation behalten, aber nur die frühe Freigabe entfernen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Parser entfernt das Zeilenumbruchzeichen aus einer vollständigen Zeile, ruft strtol auf und prüft nur errno != ERANGE. Danach wandelt er das Ergebnis für einen Arrayindex in int um. Welche fehlenden Prüfungen können weiterhin ungültige Eingaben zulassen?
  1. Ob die Eingabe einen Dezimalpunkt hat und ob eine Ziffer davorsteht.
  2. Nur, ob das Ergebnis gleich LONG_MAX ist.
  3. Nur, ob die Konvertierung die Dezimalbasis verwendete.
  4. Fehlende Ziffern, nachfolgenden Text und den gültigen Indexbereich.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine größenabhängige Schleife wird unter ASan und UBSan nur mit einem Element getestet. Sie besteht. Im Produktivbetrieb schlägt sie bei vollem Puffer und deaktivierten Zusicherungen fehl. Welcher Prüfplan berücksichtigt diese Unterschiede?
  1. Grenzfalleingaben in Prüf- und Release-Builds ausführen.
  2. Den vorhandenen Prüf-Build-Test mit einem Element und zusätzlichen Diagnoseausgaben wiederholen.
  3. Zusicherungen aktiviert lassen und den Fall mit vollem Puffer überspringen.
  4. Statische Analyse ausführen und alle dynamischen Tests weglassen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein bestehender C-Parser aktiviert Stack-Schutz und ASLR. Eine durch Fuzzing erzeugte Eingabe löst dennoch einen von ASan gemeldeten Schreibzugriff hinter einem Heap-Block aus. Welche Reaktion kombiniert Vermeidung und Schutzmaßnahmen korrekt?
  1. Den Sanitizer entfernen, weil Härtung den Schreibzugriff definiert.
  2. Die Heap-Grenze korrigieren und die Release-Schutzmaßnahmen beibehalten.
  3. ASLR deaktivieren, um den ungültigen Schreibzugriff gültig zu machen.
  4. Den Heap-Block vergrößern, bis der Absturz verzögert wird.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Nach einem Absturz in einer Zeichenkettenfunktion enthält Stackframe null keine hilfreichen lokalen Variablen. Stackframe drei in deinem Code übergibt einen Zeiger, der bei einer Suche zurückgegeben wurde. Dieser Zeiger ist ein Nullzeiger, und fehlende Einträge sind gültige Eingaben. Welche Korrektur und Prüfung folgen?
  1. An der Bibliotheksschnittstelle eine Laufzeitzusicherung ergänzen und den vorhandenen Aufrufer beibehalten.
  2. Die Suche durch ein größeres reserviertes Array ersetzen.
  3. continue verwenden, bis der Zeiger kein Nullzeiger mehr ist.
  4. Das fehlende Suchergebnis vor dem Aufruf behandeln und diese Eingabe dann testen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Funktion öffnet eine Ausgabedatei und reserviert zwei Puffer. Die zweite Allokation schlägt fehl. Der Aufräumcode schließt die Datei und kehrt zurück, gibt aber den ersten Puffer nie frei. Welche Korrektur wahrt sowohl den Besitz als auch die Fehlermeldung?
  1. Den Fehler melden und die Datei schließen, aber den ersten Puffer ohne Aufrufer behalten.
  2. Den ersten Puffer freigeben und die Datei schließen, aber Erfolg für die unvollständige Arbeit zurückgeben.
  3. Den reservierten Puffer freigeben, das Schließen prüfen und die Operation als fehlgeschlagen melden.
  4. Das zweite Ergebnis als Nullzeiger freigeben und schließen, während die erste Allokation bestehen bleibt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

78 Schlüsselbegriffe, jeweils mit Definition

A

abschließendes Nullzeichen
Ein abschließendes Nullzeichen ist das Nullzeichen, das das Ende einer C-Zeichenkette markiert und Speicherplatz zusätzlich zu ihren sichtbaren Zeichen benötigt.
Die Zeichenkette „ready“ benötigt sechs Byte, damit Zeichenkettenfunktionen nach ihren fünf Buchstaben ein abschließendes Nullzeichen finden.
Abschneiden
Abschneiden verwirft den Teil eines Textes, der nicht in ein Ziel passt, selbst wenn der gespeicherte Anfangsteil korrekt abgeschlossen ist.
Ein nicht negativer Rückgabewert von snprintf, der mindestens so groß wie die Kapazität ist, zeigt an, dass der vollständige Text nicht hineinpasste.
AddressSanitizer
AddressSanitizer ist eine compilerbasierte Instrumentierung, die viele ungültige Operationen an Grenzen, Lebensdauern und bei der Speicherfreigabe zur Laufzeit erkennt.
Ein Bericht kann den fehlerhaften Schreibzugriff und den Speicherblock nennen, dessen Grenzen dieser Schreibzugriff überschritten hat.
ASLR
Die zufällige Anordnung des Adressraums (ASLR) verändert Speicherpositionen zwischen Programmläufen, um manche Angriffe auf verbleibende Speicherdefekte zu erschweren.
ASLR kann die Ausnutzung eines fehlerhaften Zugriffs erschweren, während der ursprüngliche Schreibzugriff außerhalb der Speichergrenzen unkorrigiert bleibt.
Aufräumpfad
Ein Aufräumpfad gibt tatsächlich angeforderte Ressourcen frei, bevor eine Funktion zurückkehrt, auch nach einer teilweise abgeschlossenen Operation.
Ein gemeinsamer Aufräumabschnitt kann einen Puffer freigeben und eine Datei schließen, nachdem eine spätere Allokation fehlschlägt.
Aufrufrückverfolgung
Eine Aufrufrückverfolgung ist ein Bericht des Debuggers über aktive Stackframes, die üblicherweise beginnend mit null für den innersten Stackframe nummeriert sind.
Nach einem Absturz hilft dir bt, den ersten Stackframe zu finden, der zu deinem eigenen Quelltext gehört.
Aufrufstapel
Der Aufrufstapel ist die Kette aktiver Funktionsaufrufe, von der aktuellen Funktion zurück durch die Funktionen, die sie aufgerufen haben.
Ein Aufruf der Zeichenkettenbibliothek in Stackframe null kann seinen ungültigen Zeiger von main erhalten haben.
Ausdrucksauswertung
Die Ausdrucksauswertung im Debugger berechnet bei angehaltener Ausführung einen Ausdruck anhand des aktuellen Zustands des ausgewählten Stackframes.
Die Ausgabe von count beobachtet einen skalaren Wert; die Auswertung von *p versucht einen Zugriff über diesen Zeiger.

B

bedingter Haltepunkt
Ein bedingter Haltepunkt hält an einer ausgewählten Stelle nur an, wenn seine Bedingung im aktuellen Programmzustand wahr ist.
Eine Bedingung wie i == 650 erspart es, frühere Schleifendurchläufe manuell schrittweise auszuführen.
Besitz
Besitz ist die Verantwortung eines Programms, die Lebensdauer einer Ressource zu verwalten, sie freizugeben und ihren gültigen Zugriffswert zu behalten.
Ein temporäres Ergebnis erhält den ursprünglichen besitzenden Zeiger, wenn realloc mit positiver Größe fehlschlägt.

C

Compilermeldung
Eine Compilermeldung nennt eine Quelltextstelle und ein vermutetes oder festgestelltes Problem, das bei der Kompilierung gefunden wurde.
Eine Formatmeldung kann auf %d zeigen und das nicht dazu passende double-Argument nennen.
Compilerwarnung
Eine Compilerwarnung meldet verdächtigen Code oder ein erkanntes Problem und lässt die Kompilierung normalerweise weiterlaufen.
Eine als Bedingung verwendete Zuweisung kann erfolgreich kompiliert werden und dennoch eine Warnung erhalten.

D

Datenhaltepunkt
Ein Datenhaltepunkt hält die Ausführung an, wenn sich eine überwachte Speicherstelle ändert, und hilft so, die Anweisung zu finden, die ihren Wert geschrieben hat.
Die Überwachung der Anzahl einer Struktur kann lange vor einem späteren Absturz eine unerwartete Zuweisung aufdecken.
Datenwettlauf
Ein Datenwettlauf tritt auf, wenn Threads ohne erforderliche Synchronisierung widersprüchliche Zugriffe ausführen und mindestens einer der Zugriffe schreibt.
Zwei nicht synchronisierte Aktualisierungen eines gemeinsam genutzten Zustands müssen nicht genau gleichzeitig stattfinden, um einen Datenwettlauf zu bilden.
Debug-Build
Ein Debug-Build ist eine für die Untersuchung konfigurierte ausführbare Datei, üblicherweise mit Quelltextinformationen und geringer Optimierung.
Ein Build mit -g -O0 erleichtert es, einer fehlerhaften Schleife Quelltextanweisung für Quelltextanweisung zu folgen.
Debugger
Ein Debugger steuert die Ausführung eines Programms, sodass du es anhalten, seinen Zustand untersuchen und ausgewählte Operationen weiter ausführen kannst.
GDB und LLDB können vor einem Schleifenrumpf anhalten, damit du den aktuellen Index mit der Grenze vergleichen kannst.
Debuginformationen
Debuginformationen beschreiben Namen, Typen und Positionen aus dem Quelltext in einer ausführbaren Datei, damit Untersuchungswerkzeuge Anweisungen dem Code zuordnen können.
Die Option -g hilft einem Debugger, an einem gewöhnlichen Haltepunkt die Quelltextzeile und lokale Variablen zu erkennen.
doppelte Speicherfreigabe
Eine doppelte Speicherfreigabe versucht, denselben Speicherblock mehr als einmal freizugeben, ohne dass eine neue gültige Allokation den Besitz begründet.
Ein Aufräumzweig, der einen bereits freigegebenen Block freigibt, kann eine Meldung der Speicherverwaltung oder von ASan auslösen.

E

Endzeiger
Ein von strtol zurückgegebener Endzeiger bezeichnet das erste nicht konvertierte Zeichen oder den Eingabeanfang, wenn keine Ziffern eingelesen wurden.
Bei „73x“ bezeichnet der Endzeiger das nachfolgende „x“, das ein strenger numerischer Parser ablehnt.

F

Fehlverhalten
Ein Fehlverhalten ist eine beobachtete Abweichung vom beabsichtigten Verhalten, etwa ein Absturz, ein falsches Ergebnis oder eine fehlende Ausgabe.
Ein Absturz in free kann das sichtbare Fehlverhalten sein, das ein viel früherer Schreibzugriff verursacht hat.
Formatstring
Ein Formatstring gibt einer Funktion für formatierte Ein- und Ausgabe vor, wie sie Argumente interpretieren soll; seine Konvertierungen müssen daher zu deren tatsächlichen Typen passen.
Ein printf-Format verwendet %zu für size_t; Benutzertext gehört in ein separates Datenargument.
Fuzzing
Fuzzing testet ein Programm mit vielen automatisch erzeugten oder veränderten Eingaben, um fehlschlagende Fälle zu entdecken, die geplante Tests übersehen.
Ein veränderter Datensatz kann einen Parserzweig erreichen, in dem ein Sanitizer einen ungültigen Zugriff erkennt.

G

Ganzzahlerweiterung
Eine Ganzzahlerweiterung wandelt einen kleineren Ganzzahltyp vor vielen arithmetischen Operationen und Bitverschiebungen in int oder unsigned int um.
Die Gültigkeit einer Verschiebungsweite hängt von der Bitbreite des erweiterten linken Operanden ab, nicht nur von seiner ursprünglichen Schreibweise.
gestaffelte Verteidigung
Gestaffelte Verteidigung kombiniert Vermeidung, Erkennung und Schadensbegrenzung, sodass mehrere Schutzmaßnahmen verbleibende Defekte berücksichtigen.
Warnungen und Codeüberprüfung verhindern Programmfehler, Sanitizer erkennen sie in Tests und Härtung begrenzt manche Schäden.

H

Haltepunkt
Ein Haltepunkt ist eine ausgewählte Ausführungsstelle, an der ein Debugger das Programm anhält, normalerweise bevor die angegebene Anweisung ausgeführt wird.
Ein Haltepunkt vor sum += a[i] lässt dich i untersuchen, ohne den Arrayzugriff auszuwerten.
hängender Zeiger
Ein hängender Zeiger verweist auf Speicher, dessen Objektlebensdauer beendet ist, sodass er keinen gültigen Zugriff auf das alte Objekt bieten kann.
Die Rückgabe der Adresse eines lokalen Arrays hinterlässt beim Aufrufer einen Zeiger auf Speicher, dessen Lebensdauer abgelaufen ist.
Härtung
Härtung ergänzt Schutzmaßnahmen, die manche Beschädigungen erkennen oder den Schaden durch Defekte begrenzen sollen, die in einem veröffentlichten Programm verbleiben.
Eine Stack-Schutzprüfung kann eine Funktion anhalten, nachdem sie eine Überschreibung ihres Prüfwerts erkannt hat.
Hypothese
Eine Hypothese bei der Fehlersuche ist ein Erklärungsversuch für ein Fehlverhalten, aus dem eine Vorhersage folgt, die du durch eine Beobachtung oder Änderung prüfen kannst.
Die Vermutung eines fehlenden Feldes legt nahe, dessen Zeiger unmittelbar vor der Zeichenkettenverarbeitung zu untersuchen.

I

implementierungsdefiniertes Verhalten
Implementierungsdefiniertes Verhalten erlaubt eine Entscheidung, die jede C-Implementierung dokumentieren muss, damit Programme sie berücksichtigen können.
Eine C17-Implementierung dokumentiert, ob ihr einfaches char vorzeichenbehaftet oder vorzeichenlos ist.
implizite Typumwandlung
Eine implizite Typumwandlung ändert den Typ eines Werts ohne ausdrücklich geschriebene Umwandlung und kann Informationen verlieren oder den Wert verändern.
Die Zuweisung eines double an einen int schneidet den Nachkommateil ab, wenn der abgeschnittene Wert darstellbar ist.
Instrumentierung
Instrumentierung fügt einem Programm Prüf- oder Beobachtungscode hinzu, damit Werkzeuge Operationen während der Ausführung untersuchen können.
Ein ASan-Build benötigt instrumentierte Objektdateien und die Sanitizer-Laufzeitumgebung beim abschließenden Linken.
Invariante
Eine Invariante ist eine Eigenschaft, die über wiederholte Schritte oder gültige Zustände einer Schleife oder Datenstruktur hinweg erhalten bleibt.
Die gespeicherte Anzahl einer Liste sollte nach jedem abgeschlossenen Einfügen ihrer Knotenzahl entsprechen.

M

MemorySanitizer
MemorySanitizer ist Clangs Laufzeitwerkzeug zur Verfolgung von Verwendungen nicht initialisierter Werte und benötigt eine kompatible Instrumentierung der Abhängigkeiten.
Er untersucht eine Klasse von Fehlern mit nicht initialisierten Werten, die gewöhnliche ASan-Prüfungen nicht verfolgen.
minimales reproduzierbares Beispiel
Ein minimales reproduzierbares Beispiel ist ein kleiner Fall aus Eingabe und Programm, der das untersuchte Fehlverhalten weiterhin auslöst.
Werden nicht benötigte Datensätze aus der Eingabe eines abstürzenden Parsers entfernt, bleibt ein leeres Feld, das den Absturz weiterhin zuverlässig auslöst.

N

Nachbedingung
Eine Nachbedingung ist eine Eigenschaft, deren Herstellung eine Operation bei erfolgreichem Abschluss zusichert.
Nach dem Sortieren eines Arrays kann eine Nachbedingung festlegen, dass jedes Element höchstens so groß wie das nächste ist.
nicht regelkonformer Code
Nicht regelkonformer Code ist ein Beispiel, das unter den von einer bestimmten Programmierregel beschriebenen Bedingungen gegen diese Regel verstößt.
Die Verwendung von printf(input) lässt externe Daten Formatkonvertierungen liefern und verstößt gegen FIO30-C.
nicht sequenzierte Seiteneffekte
Nicht sequenzierte Seiteneffekte sind Änderungen und widersprüchliche Verwendungen desselben skalaren Objekts ohne die Reihenfolge, die C für eine definierte Auswertung verlangt.
Die Übergabe von sowohl i als auch ++i an einen Aufruf mischt einen Lesezugriff und eine Änderung ohne sichere Reihenfolge.
nicht spezifiziertes Verhalten
Nicht spezifiziertes Verhalten erlaubt eine Wahl zwischen gültigen Ergebnissen, ohne von der Implementierung zu verlangen, ihre Entscheidung zu dokumentieren.
Unabhängige Aufrufe, die als Argumente verwendet werden, dürfen in beliebiger Reihenfolge ausgewertet werden, ohne den Aufruf ungültig zu machen.
nicht vertrauenswürdige Eingabe
Nicht vertrauenswürdige Eingabe bezeichnet Daten außerhalb der Kontrolle des Programms, deren Format, Länge und Wertebereich vor einer sicherheitsrelevanten Verwendung geprüft werden müssen.
Eine Anzahl aus der Kommandozeile muss geprüft werden, bevor sie eine Arrayallokation oder Schleifengrenze steuert.

O

Objektlebensdauer
Die Objektlebensdauer ist der Zeitraum während der Ausführung, in dem ein Objekt existiert und auf seinen Speicher nach den Regeln von C zugegriffen werden darf.
Ein lokales automatisches Array existiert während seines Funktionsaufrufs, während ein reservierter Speicherblock bis zur Freigabe besteht.
Optimierung
Optimierung formt ein Programm um, um Ausführung oder Speicherbedarf zu verbessern, und erhält dabei das vorgeschriebene Verhalten von definiertem Code.
Bei -O2 zeigt ein Debugger für eine lokale Variable, die der Compiler entfernt hat, möglicherweise keinen separaten Wert.

P

Portabilität
Portabilität ist die Fähigkeit von Code, sich über verschiedene Implementierungen hinweg wie beabsichtigt zu verhalten, ohne sich auf nicht unterstützte Plattformannahmen zu verlassen.
Die Verwendung von INT_MAX vermeidet die Annahme, dass jede Implementierung für int denselben Höchstwert festlegt.
Programmfehler
Ein Programmfehler ist ein Defekt im Code, der falsches Verhalten verursachen kann, wenn ein bestimmter Pfad oder eine bestimmte Eingabe ihn erreicht.
Eine Schleifenbedingung kann selbst bei Läufen falsch sein, in denen der dadurch verursachte ungültige Zugriff keinen sichtbaren Absturz erzeugt.
Pufferkapazität
Die Pufferkapazität ist der insgesamt verfügbare beschreibbare Speicher, einschließlich des Platzes, der für das abschließende Nullzeichen einer Zeichenkette benötigt wird.
Ein Array mit zehn Byte kann höchstens neun Einzelbytezeichen als abgeschlossene C-Zeichenkette speichern.

Q

Quarantäne
Eine Quarantäne von ASan verzögert die Wiederverwendung freigegebener Blöcke und erhöht so die Chance, dass veraltete Zugriffe auf noch als ungültig markierten Speicher treffen.
Ein hängender Zeiger kann erkannt werden, bevor der freigegebene Block für eine andere Allokation wiederverwendet wird.

R

räumliche Speichersicherheit
Räumliche Speichersicherheit verlangt, dass jeder Speicherzugriff innerhalb der Grenzen des Objekts bleibt, auf das er zugreifen darf.
Eine Grenzenprüfung vor einem Schreibzugriff auf ein Array verhindert, dass ein Index ein Element außerhalb dieses Arrays auswählt.
Redzone
Eine Redzone ist ein verbotener Speicherbereich um ein unterstütztes Objekt, damit ein Prüfwerkzeug Zugriffe über dessen Grenze hinweg erkennen kann.
Ein zusätzlicher Schreibzugriff unmittelbar hinter einem Heap-Block kann in dessen Redzone gelangen und ASan auslösen.
regelkonforme Lösung
Eine regelkonforme Lösung ist ein Entwurf oder Codebeispiel, das unter den angegebenen Bedingungen der Lösung die Anforderung einer Regel erfüllt.
Ein festes Format %s hält übergebenen Text im Datenargument eines printf-Aufrufs.
Regressionstest
Ein Regressionstest prüft ein Verhalten, das zuvor fehlgeschlagen ist, damit spätere Änderungen auf eine Rückkehr dieses Defekts geprüft werden können.
Eine gespeicherte Eingabe mit leerem Datensatz, die einen Parser einst zum Absturz brachte, wird nach jeder Parseränderung erneut ausgeführt, um eine Rückkehr des Absturzes zu erkennen.
Release-Build
Ein Release-Build ist eine für den gewöhnlichen Gebrauch konfigurierte ausführbare Datei, üblicherweise mit aktivierter Optimierung und unterstützter Härtung.
Ein Release-Build muss Eingabeprüfungen beibehalten, selbst wenn NDEBUG seine Zusicherungen entfernt.
Risikopriorität
Eine CERT-Risikopriorität ist das Produkt der Bewertungen von Schweregrad, Wahrscheinlichkeit und Behebungsaufwand und hilft, die Durchsetzung von Regeln oder Korrekturarbeiten zu ordnen.
Eine höhere Bewertung des Behebungsaufwands erhöht die Priorität, weil die entsprechende Korrektur günstiger durchzuführen ist.

S

Schattenspeicher
Schattenspeicher ist zusätzlicher Speicher, den ein Prüfwerkzeug verwendet, um Informationen über die Speicherbytes der Anwendung festzuhalten.
ASan zieht Schatteninformationen heran, um zu prüfen, ob ein instrumentierter Zugriff erlaubten Speicher berührt.
schrittweise Ausführung
Die schrittweise Ausführung führt das Programm um einen ausgewählten Abschnitt auf Quelltextebene weiter und erlaubt Untersuchungen zwischen Anweisungen oder Funktionsaufrufen.
Verwende next, um einen Aufruf zu überspringen, und step, wenn du in den Code der Funktion hineingehen musst.
SEI CERT C Coding Standard
Der SEI CERT C Coding Standard ist die Sammlung von C-Regeln, Empfehlungen, Beispielen und Risikobewertungen des SEI der Carnegie Mellon University.
Seine Regel MEM35-C hilft Prüfenden zu fragen, ob ein Speicherblock genügend Byte für seine Objekte enthält.
Shell-Injection
Shell-Injection tritt auf, wenn externer Text die Struktur eines Shell-Befehls verändert und unbeabsichtigte Befehle oder Operationen verursacht.
Wird eine Zeichenkette für system() aus einem ungeprüften Argument aufgebaut, kann eine Shell die gelieferte Syntax interpretieren.
Speicherabbild
Ein Speicherabbild ist eine gespeicherte Momentaufnahme eines Prozesses bei einem Absturz, die ein Debugger mit der passenden ausführbaren Datei untersuchen kann.
Ein gespeichertes Abbild ermöglicht eine Aufrufrückverfolgung und die Untersuchung lokaler Werte, nachdem der fehlgeschlagene Prozess beendet wurde.
speichersichere Programmiersprache
Eine speichersichere Programmiersprache setzt für ihre sicheren Operationen Regeln für Grenzen und Lebensdauern durch und verringert so den Bedarf an manuellen Speichersicherheitsprüfungen.
Sicheres Rust kombiniert Besitzregeln mit Grenzenprüfungen; der Aufruf unsicheren C-Codes benötigt weiterhin Sorgfalt.
Stackframe
Ein Stackframe ist der Ausführungskontext eines aktiven Funktionsaufrufs mit seinen verfügbaren Argumenten, lokalen Variablen und Rücksprunginformationen.
Übergibt ein Aufrufer einen Zeiger name, macht die Auswahl des Stackframes des Aufrufers nach einem Absturz diesen Zeiger für die Untersuchung verfügbar.
Standard für sichere Programmierung
Ein Standard für sichere Programmierung ist eine Sammlung konkret anwendbarer Anforderungen und Empfehlungen zur Vermeidung bekannter Defekte und Sicherheitslücken.
Eine Prüfliste für die Codeüberprüfung kann jede Prüfung einer Allokationsgröße mit dem Defekt verbinden, den sie verhindern soll.
statische Analyse
Statische Analyse untersucht Quellcode und mögliche Ausführungspfade, ohne das Programm mit einer Testeingabe auszuführen.
Ein Analysewerkzeug kann einen Pfad mit doppelter Speicherfreigabe finden, den kein aktueller Regressionstest erreicht.
statische Zusicherung
Eine statische Zusicherung prüft während der Kompilierung einen konstanten Ausdruck und lässt die Erstellung fehlschlagen, wenn die Bedingung falsch ist.
Ein static_assert in C17 kann eine Plattform zurückweisen, auf der eine erforderliche Annahme über die Größe von int falsch ist.

T

ThreadSanitizer
ThreadSanitizer ist ein Laufzeitprüfwerkzeug, das widersprüchliche, nicht synchronisierte Zugriffe von Threads auf gemeinsam genutzten Speicher erkennt.
Bei der Untersuchung möglicher Datenwettläufe benötigt er einen von ASan getrennten Build.

Ü

überlappende Speicherbereiche
Überlappende Speicherbereiche teilen mindestens ein Byte Speicher, sodass das Kopieren zwischen ihnen ein Verhalten benötigt, das die ursprünglichen Quelldaten erhält.
Das Verschieben von Text innerhalb desselben Arrays kann memmove erfordern, weil memcpy Überlappung verbietet.
Überlauf bei vorzeichenbehafteten Ganzzahlen
Ein Überlauf bei vorzeichenbehafteten Ganzzahlen tritt auf, wenn ein arithmetisches Ergebnis in seinem vorzeichenbehafteten Zieltyp nicht darstellbar ist, und führt zu undefiniertem Verhalten.
Bei einem int mit 32 Bit kann die Addition von eins zu INT_MAX keinen portablen, definierten Folgewert erzeugen.
überlaufgeprüfte Arithmetik
Überlaufgeprüfte Arithmetik erkennt ein nicht darstellbares mathematisches Ergebnis, ohne dass das Programm dafür einen undefinierten Überlauf ausführen muss.
ckd_add aus C23 meldet, ob eine Summe darstellbar ist, sodass das Programm den Fehler ausdrücklich behandeln kann.
Übersetzungsfehler
Ein Übersetzungsfehler ist eine Compilermeldung, die eine erfolgreiche Erstellung verhindert, bis das gemeldete Problem behoben ist.
Die Korrektur eines fehlenden Semikolons kann mehrere spätere Meldungen beseitigen, die durch denselben Syntaxfehler verursacht wurden.

U

unbestimmter Wert
Ein unbestimmter Wert ist der Zustand eines Objekts, dessen verwendbarer Wert noch nicht festgelegt wurde, wie bei vielen nicht initialisierten automatischen Variablen.
Wer int sum; deklariert und sofort sum += x; verwendet, liest einen Wert, der nie initialisiert wurde.
UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer fügt Prüfungen für viele undefinierte Operationen während der Ausführung ein, darunter Überläufe bei vorzeichenbehafteten Ganzzahlen und ungültige Bitverschiebungen.
Ein UBSan-Bericht kann eine überlaufende Addition erkennen, selbst wenn das Programm eine plausible Summe ausgibt.
undefiniertes Verhalten
Undefiniertes Verhalten ist Verhalten, an das der C-Standard nach einer ungültigen Operation keine Anforderungen stellt.
Ein Überlauf bei vorzeichenbehafteten Ganzzahlen kann in einem Build nach Wraparound aussehen und sich nach der Optimierung anders verhalten.

V

Verdeckung von Variablen
Verdeckung von Variablen tritt auf, wenn eine Deklaration in einem inneren Gültigkeitsbereich eine andere Deklaration mit demselben Namen verdeckt.
Das count einer inneren Schleife kann aktualisiert werden, während das äußere count unverändert bleibt.
Vorbedingung
Eine Vorbedingung ist eine Anforderung, die beim Beginn einer Funktion oder Operation gelten muss und vom Aufrufer oder durch eine frühere Prüfung sichergestellt wird.
Eine private Hilfsfunktion kann einen Zeiger verlangen, der kein Nullzeiger ist, bevor sie das erste Arrayelement untersucht.

W

Wertebereichsfehler
Ein Wertebereichsfehler tritt auf, wenn der mathematische Wert einer Konvertierung außerhalb des darstellbaren Wertebereichs des Ergebnistyps liegt.
Nach dem Zurücksetzen von errno setzt ein Wertebereichsfehler von strtol den Fehlerwert auf ERANGE, obwohl ein Grenzwert zurückgegeben wird.
Wraparound bei vorzeichenlosen Ganzzahlen
Wraparound bei vorzeichenlosen Ganzzahlen reduziert ein arithmetisches Ergebnis auf den Wertebereich des vorzeichenlosen Typs; er ist definiert, kann aber Größenannahmen eines Programms verletzen.
Ein überlaufendes Produkt zur Berechnung der Byteanzahl kann einen viel kleineren Speicherblock anfordern, als das beabsichtigte Array benötigt.

Z

Zeichenkettenlänge
Die Zeichenkettenlänge ist die Anzahl der Zeichen vor dem ersten Nullzeichen, ohne dieses abschließende Zeichen selbst.
strlen("safe") ist vier, während das Speichern der vollständigen Zeichenkette fünf Byte benötigt.
zeitliche Speichersicherheit
Zeitliche Speichersicherheit verlangt, dass nur auf ein Objekt zugegriffen wird, solange es existiert und für diesen Zugriff gültig ist.
Ein veralteter Alias nach free verletzt die Lebensdauersicherheit, selbst wenn seine Adresse noch vertraut aussieht.
Zugriff außerhalb der Speichergrenzen
Ein Zugriff außerhalb der Speichergrenzen liest oder schreibt außerhalb der gültigen Elemente eines Objekts, selbst wenn die Adresse zufällig einem Speicherbereich zugeordnet ist.
Index sechs liegt außerhalb eines Arrays mit sechs Elementen, weil sein letztes gültiges Element den Index fünf hat.
Zugriff nach der Speicherfreigabe
Ein Zugriff nach der Speicherfreigabe ist die Verwendung eines Speicherblocks, nachdem er freigegeben wurde, und verletzt damit die Lebensdauer des ursprünglichen Objekts.
Die Freigabe eines besitzenden Zeigers beendet auch den gültigen Zugriff über einen Alias, der noch seine alte Adresse enthält.
Zusicherung
Eine Zusicherung ist eine ausführbare Prüfung einer Programmierannahme, die bei Aktivierung und einer falschen Bedingung einen Fehler meldet und abbricht.
Eine interne Hilfsfunktion zur Mittelwertberechnung kann zusichern, dass ihr Aufrufer nach entsprechender Prüfung eine Anzahl ungleich null liefert.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten