Fehlersuche, undefiniertes Verhalten und sicheres C
AnfängerEinheit 8 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
Was du lernst
- 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 lesenKompiliere 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ählenVerwende 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.
- Warnungen aktivieren und Compilermeldungen lesen
- 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üfenBeschreibe 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 durchlaufenStarte 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 untersuchenNutze nach einem Absturz den Aufrufstapel, wechsle zwischen Stackframes und finde mit Datenhaltepunkten, bedingten Haltepunkten und Speicherabbildern heraus, woher ein fehlerhafter Wert kommt.
- Methodisch nach Fehlern suchen: reproduzieren, eingrenzen und eine Hypothese prüfen
- 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 bedeutetUnterscheide 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 erkennenErkenne Ü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 erkennenErkenne 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.
- Verstehen, was undefiniertes Verhalten bedeutet
- 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ückenVerwende 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 findenErstelle 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 etablierenVerwende UndefinedBehaviorSanitizer und lerne die anderen Sanitizer kennen. Kombiniere anschließend Warnungen, Sanitizer und Tests zu einem Ablauf, der Fehler früh erkennt.
- Annahmen durch Zusicherungen ausdrücken
- 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 anwendenErklä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 umgehenKenne 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 behandelnBehandle 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ägenErklä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.
- Einen Standard für sichere Programmierung anwenden
Fehlersuche in C einfach erklärt
1Den 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:
| Option | Was sie aufdecken hilft |
|---|---|
-Wconversion | Implizite Typumwandlungen, die einen Wert verändern können |
-Wshadow | Eine lokale Deklaration, die eine andere Variable verdeckt |
-Wformat=2 | Zusätzliche Prüfungen von Formatstrings |
-Werror | Aktivierte 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.
-Wallaktiviert 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 Zweck | Was du sehen kannst |
|---|---|---|
-O0 | Einfache Fehlersuche | Die Reihenfolge im Quelltext lässt sich gewöhnlich am leichtesten verfolgen |
-Og | Fehlersuche mit leichter Optimierung | Einige Umformungen bleiben erhalten |
-O2 | Effizienter Release-Build | Umgeordnete 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
-gfür Debuginformationen zum Quelltext. - Suche mit
-O0oder-Ognach Fehlern und teste auch den Release-Build. - Halte Eingabeprüfungen aktiv, wenn
NDEBUGZusicherungen 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.
2Programmfehler 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
| Befehl | Zweck in GDB und LLDB |
|---|---|
next oder n | Die aktuelle Zeile ausführen und dabei Aufrufe normalerweise überspringen |
step oder s | In eine aufgerufene Funktion hineingehen, wenn Quelltextinformationen das erlauben |
finish | Bis zur Rückkehr der ausgewählten Funktion ausführen |
continue oder c | Bis 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,finishundcontinue, 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
btzurü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.
3Undefiniertes 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
| Art | Was die Sprache erlaubt | Beispiel in C17 |
|---|---|---|
| Undefiniert | Kein vorgeschriebenes Verhalten | Überlauf bei vorzeichenbehafteten Ganzzahlen |
| Implementierungsdefiniert | Eine dokumentierte Entscheidung der Implementierung | Ob einfaches char vorzeichenbehaftet ist |
| Nicht spezifiziert | Eine erlaubte Entscheidung, die nicht dokumentiert werden muss | Reihenfolge 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.
4Fehler 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:
| Art | Was sie festlegt | Beispiel |
|---|---|---|
| Vorbedingung | Beim Eintritt erforderlich | Ein nicht leeres Eingabearray |
| Nachbedingung | Nach der Arbeit garantiert | Ein Array ist sortiert |
| Invariante | Über mehrere Schritte hinweg erhalten | Eine 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.
NDEBUGdeaktiviert 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:
- Bestimme die Fehlerkategorie und ob der Zugriff ein Lese- oder Schreibzugriff war.
- Suche in der Rückverfolgung des Zugriffs den ersten Stackframe in deinem eigenen Code.
- Vergleiche den Zugriff mit dem reservierten Bereich und seiner Elementgröße.
- 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=addressund 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
| Werkzeug | Hauptzweck | Wichtige Grenze |
|---|---|---|
| UBSan | Viele undefinierte arithmetische Operationen sowie Bitverschiebungen, Nullzeiger-, Ausrichtungs- und Arrayoperationen | Erkennt nicht jede Art von UB |
| ASan | Viele Grenzen- und Lebensdauerfehler | Verfolgt keine gewöhnlichen nicht initialisierten Werte |
| MemorySanitizer | Verwendungen nicht initialisierten Speichers | Benötigt instrumentierte Abhängigkeiten für zuverlässige Ergebnisse |
| ThreadSanitizer | Datenwettläufe | Benö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.
5Sichereren 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
snprintfauf Fehler und Abschneiden. - Verwende
memmovefü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
errnovorstrtolzurück und untersuche den Endzeiger. - Prüfe Bibliotheksfehler und erhalte Ressourcen auf jedem Pfad.
- Verwende für
reallocmit 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.
| Ansatz | Speicherschutz | Kosten oder Verantwortung |
|---|---|---|
| C | Vom Programmierer durchgesetzte Grenzen und Lebensdauern | Manuelle Analyse, Prüfungen und Tests |
| Sicheres Rust | Besitz, Ausleihen und Grenzenprüfungen | Erlernen der Besitzregeln |
| Go, Java, C#, Python | Verwaltete Lebensdauern und Grenzenprüfungen | Unterschiedlicher Laufzeit- und Speicheraufwand |
| Swift | Referenzzählung und geprüfter Zugriff | Unterschiedliche 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
1Ein untersuchbares Programm erstellen
9 Schritte
- 1Einfü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.
- 2Erklä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.-Wallbedeutet nicht jede Warnung. Ergänze-Wconversionfür implizite Typumwandlungen, die Werte ändern, und-Wformat=2für strengere Formatprüfungen.-Werrormacht aktivierte Warnungen zu Erstellungsfehlern. - 3BeispielDie 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. - 4HerausforderungDen Ausgabetyp korrigieren
double ratio = 2.5;wird mitprintf("%d\n", ratio);ausgegeben. Was sollte geändert werden und warum?---reveal---
Verwende
printf("%f\n", ratio);.%derwartet einenint, während%finprintfeinendoubleerwartet. Ein unpassendes Format führt zu undefiniertem Verhalten. Eine explizite Umwandlung inintwürde die ausgegebene Zahl verändern, indem sie ihren Nachkommateil abschneidet. Sie würde daher das Ziel, das Verhältnis auszugeben, nicht erfüllen. - 5TippMeldungen 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.
- 6Erklä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.-gdeaktiviert die Optimierung nicht. Mit-O2kann Code weiterhin im Debugger untersucht werden, aber Zeilen können zu springen scheinen und Variablen wegoptimiert werden.-Ogist eine weitere Möglichkeit für die Entwicklung, die leichte Optimierung beibehält. - 7VerbindungPrü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.
- 8HerausforderungPrü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.
NDEBUGentfernt 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. - 9ZusammenfassungDie 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.
2Eine falsche Summe und einen Absturz zurückverfolgen
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungAus einem Symptom eine Frage machen
- 2ErklärungDen fehlerauslösenden Fall bewahren
- 3HerausforderungEinen kontrollierten Versuch wählen
- 4BeispielAm Schleifenrumpf anhalten
- 5BeispielDen gültigen Durchläufen folgen
- 6HerausforderungDen zusätzlichen Durchlauf korrigieren und prüfen
- 7TippNur in hilfreichen Code hineingehen
- 8BeispielZum Aufrufer wechseln
- 9ErklärungEine Änderung bei ihrem Auftreten erfassen
- 10HerausforderungEinen Absturz in der Speicherverwaltung untersuchen
- 11VerbindungEinen gespeicherten Absturzzustand nutzen
- 12ZusammenfassungDie Ansicht wählen, die die Frage beantwortet
3Die Operation prüfen, bevor sie ungültig wird
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungFragen, was C garantiert
- 2ErklärungAbweichungen von einem Defekt unterscheiden
- 3BeispielEin Überlauftest, der zu spät kommt
- 4HerausforderungDie Zeigerprüfung platzieren
- 5BeispielEine vorzeichenbehaftete Summe absichern
- 6KernaussageAuch definierter Wraparound kann das Programm stören
- 7HerausforderungVor der Multiplikation erweitern
- 8TippMehrere Grenzen bei Ausdrücken prüfen
- 9BeispielEine Endmarkierung von einem Element unterscheiden
- 10BeispielJeden Alias bei der Freigabe verfolgen
- 11HerausforderungEine Summe und ihre Lebensdauer korrigieren
- 12ZusammenfassungTyp, Wertebereich, Grenzen und Lebensdauer prüfen
4Prüfungen als Hinweise auf einen bestimmten Defekt lesen
11 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMehr als die endgültige Ausgabe prüfen
- 2ErklärungEine interne Zusage zusichern
- 3HerausforderungDie Operation in jedem Build erhalten
- 4ErklärungEine instrumentierte ausführbare Datei erstellen
- 5BeispielDen gemeldeten Bereich lesen
- 6HerausforderungDie zugehörige Schleife korrigieren
- 7KernaussageEine fehlende Meldung hat mehrere Erklärungen
- 8BeispielDen Test bei einem arithmetischen Fehler stoppen
- 9ErklärungWeitere Prüfwerkzeuge bewusst auswählen
- 10HerausforderungEinen unvollständigen Prüfablauf verbessern
- 11ZusammenfassungBerichte in wiederholbare Prüfungen umwandeln
5Eingaben, Größen und Besitz ausdrücklich festhalten
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDaten durch das Programm verfolgen
- 2ErklärungDie Regel als Frage verwenden
- 3HerausforderungEingaben als Daten behandeln
- 4BeispielDie Kapazität vom Aufrufer übergeben
- 5BeispielEinen sicheren Anfangsteil von Erfolg unterscheiden
- 6HerausforderungEine unvollständige Zeile behandeln
- 7BeispielDas Ende der Konvertierung untersuchen
- 8BeispielDen Wertebereich des Programms anwenden
- 9BeispielDen besitzenden Zeiger bei einer Größenänderung erhalten
- 10HerausforderungDen Fehlerpfad planen
- 11ErklärungDie Sicherheitsgrenze erkennen
- 12VerbindungSchutzebenen mit unterschiedlichen Aufgaben wählen
- 13HerausforderungEine Sprachentscheidung treffen
- 14HerausforderungDen vollständigen Eingabepfad prüfen
- 15ZusammenfassungJede Grenze sichtbar halten
Fehlersuche in C: Übungsaufgaben
Set 1: Compilerhinweise und Entscheidungen im Debugger10 Übungsfragen
Frage 1 von 10
-Wall ohne Meldungen wandelt eine Anzahl vom Typ double stillschweigend in int um. Welche zusätzliche Warnoption zielt auf diese implizite Wertänderung?Antwort zeigenAntwort ausblenden
-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
double reading = 7.25; und ruft printf("%d\n", reading); auf. Welche Korrektur erhält dieses Ziel?Antwort zeigenAntwort ausblenden
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
Antwort zeigenAntwort ausblenden
-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
Antwort zeigenAntwort ausblenden
Frage 5 von 10
Antwort zeigenAntwort ausblenden
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
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?Antwort zeigenAntwort ausblenden
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
strlen und Stackframe zwei in deiner Funktion render. Du möchtest den Zeiger caption von render in GDB untersuchen. Was solltest du tun?Antwort zeigenAntwort ausblenden
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
Antwort zeigenAntwort ausblenden
Frage 9 von 10
-O0 und -O2 unterschiedliche Summen. Bei -O2 ist außerdem eine Zwischenvariable im Debugger nicht verfügbar. Welche Untersuchung berücksichtigt beide Beobachtungen?Antwort zeigenAntwort ausblenden
Frage 10 von 10
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?Antwort zeigenAntwort ausblenden
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
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
unsigned int mit 32 Bit aus. Welchen Wert speichert unsigned int slots = 0u - 1u;?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
unsigned int mit 32 Bit aus. Eine Eingabe liefert shift = 32. Welchen Status hat 1u << shift?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
local zurück, nachdem sie char local[24] = "ready"; deklariert hat. Warum ist es ungültig, das Ergebnis beim Aufrufer zu lesen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
int a und einen positiven int b einen Überlauf von a + b, ohne diese Summe auszuwerten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
-fsanitize=undefined. Eine size_t-Multiplikation führt zu Wraparound, erzeugt aber keine Meldung. Warum kann das geschehen?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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
assert(user_count > 0); als einzige Zurückweisung ungültiger Benutzeranzahlen. Release-Builds definieren NDEBUG. Was sollte diesen Schutz ersetzen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
-fsanitize=undefined melden nichts, aber der benachbarte Wert und die Allokationsgröße sind falsch. Welche Untersuchung behandelt beide Defekte?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
char line[48] an read_line(char line[]) auf einem System mit Zeigern von acht Byte. Was misst sizeof line innerhalb der Funktion?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
errno zurück und konvertiert „64kg“ mit strtol. Die Konvertierung gibt 64 zurück und errno bleibt null. Was muss zur Ablehnung führen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
abschließendes Nullzeichen
Abschneiden
snprintf, der mindestens so groß wie die Kapazität ist, zeigt an, dass der vollständige Text nicht hineinpasste.AddressSanitizer
ASLR
Aufräumpfad
Aufrufrückverfolgung
bt, den ersten Stackframe zu finden, der zu deinem eigenen Quelltext gehört.Aufrufstapel
main erhalten haben.Ausdrucksauswertung
count beobachtet einen skalaren Wert; die Auswertung von *p versucht einen Zugriff über diesen Zeiger.B
bedingter Haltepunkt
i == 650 erspart es, frühere Schleifendurchläufe manuell schrittweise auszuführen.Besitz
realloc mit positiver Größe fehlschlägt.C
Compilermeldung
%d zeigen und das nicht dazu passende double-Argument nennen.Compilerwarnung
D
Datenhaltepunkt
Datenwettlauf
Debug-Build
-g -O0 erleichtert es, einer fehlerhaften Schleife Quelltextanweisung für Quelltextanweisung zu folgen.Debugger
Debuginformationen
-g hilft einem Debugger, an einem gewöhnlichen Haltepunkt die Quelltextzeile und lokale Variablen zu erkennen.doppelte Speicherfreigabe
E
Endzeiger
strtol zurückgegebener Endzeiger bezeichnet das erste nicht konvertierte Zeichen oder den Eingabeanfang, wenn keine Ziffern eingelesen wurden.F
Fehlverhalten
free kann das sichtbare Fehlverhalten sein, das ein viel früherer Schreibzugriff verursacht hat.Formatstring
printf-Format verwendet %zu für size_t; Benutzertext gehört in ein separates Datenargument.Fuzzing
G
Ganzzahlerweiterung
int oder unsigned int um.gestaffelte Verteidigung
H
Haltepunkt
sum += a[i] lässt dich i untersuchen, ohne den Arrayzugriff auszuwerten.hängender Zeiger
Härtung
Hypothese
I
implementierungsdefiniertes Verhalten
char vorzeichenbehaftet oder vorzeichenlos ist.implizite Typumwandlung
double an einen int schneidet den Nachkommateil ab, wenn der abgeschnittene Wert darstellbar ist.Instrumentierung
Invariante
M
MemorySanitizer
minimales reproduzierbares Beispiel
N
Nachbedingung
nicht regelkonformer Code
printf(input) lässt externe Daten Formatkonvertierungen liefern und verstößt gegen FIO30-C.nicht sequenzierte Seiteneffekte
i als auch ++i an einen Aufruf mischt einen Lesezugriff und eine Änderung ohne sichere Reihenfolge.nicht spezifiziertes Verhalten
nicht vertrauenswürdige Eingabe
O
Objektlebensdauer
Optimierung
-O2 zeigt ein Debugger für eine lokale Variable, die der Compiler entfernt hat, möglicherweise keinen separaten Wert.P
Portabilität
INT_MAX vermeidet die Annahme, dass jede Implementierung für int denselben Höchstwert festlegt.Programmfehler
Pufferkapazität
Q
Quarantäne
R
räumliche Speichersicherheit
Redzone
regelkonforme Lösung
%s hält übergebenen Text im Datenargument eines printf-Aufrufs.Regressionstest
Release-Build
NDEBUG seine Zusicherungen entfernt.Risikopriorität
S
Schattenspeicher
schrittweise Ausführung
next, um einen Aufruf zu überspringen, und step, wenn du in den Code der Funktion hineingehen musst.SEI CERT C Coding Standard
Shell-Injection
system() aus einem ungeprüften Argument aufgebaut, kann eine Shell die gelieferte Syntax interpretieren.Speicherabbild
speichersichere Programmiersprache
Stackframe
name, macht die Auswahl des Stackframes des Aufrufers nach einem Absturz diesen Zeiger für die Untersuchung verfügbar.Standard für sichere Programmierung
statische Analyse
statische Zusicherung
static_assert in C17 kann eine Plattform zurückweisen, auf der eine erforderliche Annahme über die Größe von int falsch ist.T
ThreadSanitizer
Ü
überlappende Speicherbereiche
memmove erfordern, weil memcpy Überlappung verbietet.Überlauf bei vorzeichenbehafteten Ganzzahlen
int mit 32 Bit kann die Addition von eins zu INT_MAX keinen portablen, definierten Folgewert erzeugen.überlaufgeprüfte Arithmetik
ckd_add aus C23 meldet, ob eine Summe darstellbar ist, sodass das Programm den Fehler ausdrücklich behandeln kann.Übersetzungsfehler
U
unbestimmter Wert
int sum; deklariert und sofort sum += x; verwendet, liest einen Wert, der nie initialisiert wurde.UndefinedBehaviorSanitizer
undefiniertes Verhalten
V
Verdeckung von Variablen
count einer inneren Schleife kann aktualisiert werden, während das äußere count unverändert bleibt.Vorbedingung
W
Wertebereichsfehler
errno setzt ein Wertebereichsfehler von strtol den Fehlerwert auf ERANGE, obwohl ein Grenzwert zurückgegeben wird.Wraparound bei vorzeichenlosen Ganzzahlen
Z
Zeichenkettenlänge
strlen("safe") ist vier, während das Speichern der vollständigen Zeichenkette fünf Byte benötigt.zeitliche Speichersicherheit
free verletzt die Lebensdauersicherheit, selbst wenn seine Adresse noch vertraut aussieht.Zugriff außerhalb der Speichergrenzen
Zugriff nach der Speicherfreigabe
Zusicherung
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.