Empfohlenes Webinar: KI-gestütztes API-Testing: Ein No-Code-Ansatz zum Testen | Zum Video

Erkennen von Speicherbeschädigungen in C und C ++

Kopfbild von Ricardo Camacho, Director of Safety & Security Compliance
22. November 2023
7 min lesen

Sehen Sie sich diese kurze Erklärung an, warum Speicherbeschädigungen in C und C++ durch Codeanalyse so schwer zu erkennen sind, und Anweisungen zur Verwendung eines Speicherfehlererkennungstools, das Ihnen stundenlange Debugging-Sitzungen erspart.

Programmierer verwenden weiterhin die Programmiersprachen C und C++, weil sie problemlos mit dem Speicher interagieren können, eng mit der Hardware zusammenarbeiten und die Leistung, Leistung und Effizienz bieten, die für die Embedded-Entwicklung erforderlich sind. Allerdings sind diese Sprachen anfällig für subtile Speicherprobleme wie Speicherlecks, Pufferüberlauf, numerischer Überlauf und mehr.

Leider kommt es nur allzu häufig vor, dass solche Fehler bei normalen Tests verborgen bleiben. Software mit subtilen Problemen wie Speicherbeschädigung kann auf einem Computer einwandfrei laufen, auf einem anderen jedoch abstürzen, oder sie läuft eine bestimmte Zeit lang einwandfrei und stürzt dann unerwartet ab, wenn das System mehrere Tage lang in Betrieb war.

Diese Art der Speicherbeschädigung sowie andere häufige Fehler wie Probleme bei der Zeichenfolgenmanipulation, unsachgemäße Initialisierung und Zeigerfehler führen zu Abstürzen in der Produktion. Mit der zunehmenden Verbreitung eingebetteter Software in Flugzeugen, Autos, medizinischen Geräten und dem wachsenden IoT-Markt sind die Folgen fehlerhafter Software mehr als nur unzufriedene Kunden. Sie können lebensbedrohlich sein.

Was ist Speicherbeschädigung?

Speicherbeschädigungsfehler sind unangenehm, insbesondere wenn sie gut getarnt werden. Wenn sie sich manifestieren, kann es trügerisch schwierig sein, sie zu reproduzieren und aufzuspüren. Betrachten Sie als Beispiel dafür, was passieren kann, das unten gezeigte Programm.

Dieses Programm verkettet die in der Befehlszeile angegebenen Argumente und gibt die resultierende Zeichenfolge aus:

/*
 * File: hello.c
 */
#include <string.h>
#include <string.h>
int main(argc, argv)
    int argc;
    char *argv[];
{
    int i;
    char str[16];
    str[0] = '\0';
    for(i=0; i<; argc; i++) {
        strcat(str, argv[i]);
        if(i < (argc-1)) strcat(str, “ “);
    }
    printf("You entered: %s\n", str);
    return (0);
}

Wenn Sie dieses Programm mit Ihrem normalen Compiler kompilieren und ausführen, werden Sie wahrscheinlich nichts Interessantes sehen. Zum Beispiel:

c:\source> cc -o hello hello.c 
    c:\source> cc -o hello  
    You entered: hello
    c:\source> hello world
    You entered: hello world
    c:\source> hello cruel world
    You entered: hello cruel world

Wenn dies der Umfang Ihrer Testverfahren wäre, würden Sie wahrscheinlich zu dem Schluss kommen, dass dieses Programm korrekt funktioniert, obwohl es einen sehr schwerwiegenden Speicherbeschädigungsfehler aufweist, der sich nur nicht durch die Erzeugung einer falschen Ausgabe manifestiert hat. Dies tritt häufig bei Speicherproblemen auf. Sie können häufig unentdeckt bleiben, da sie die Ausgabe möglicherweise nicht direkt beeinflussen und daher von normalen Komponententests oder Funktionstests nicht erkannt werden.

Diese Art von Fehler sieht einfach genug aus, wenn er sich in einem kleinen Beispielprogramm befindet, in dem er nicht übersehen wird. Wenn er jedoch in kompliziertem Code mit Hunderttausenden von Zeilen und vielen dynamischen Zuordnungen vergraben ist, kann die Erkennung bis nach der Veröffentlichung leicht vermieden werden.

Arten von Speicherbeschädigung in C und C++

Speicherbeschädigung ist ein kritisches Problem bei der Programmierung, insbesondere in C und C++, da diese Sprachen direkten Zugriff auf die Speicherverwaltung ermöglichen und so das Fehlerpotenzial erhöhen. Wie oben erwähnt, kommt es zu einer Speicherbeschädigung, wenn ein Programm unbeabsichtigt auf den Speicher zugreift oder ihn verändert, was zu Datenbeschädigung, Abstürzen oder Sicherheitslücken führt.

Das Verständnis der verschiedenen Arten von Speicherbeschädigungen ist von entscheidender Bedeutung Speicherprobleme erkennen und ebnet auch Möglichkeiten, sie zu mildern. Die folgenden Abschnitte bieten Einblicke in vier häufige Arten von Speicherbeschädigungen.

1. Pufferüberläufe

Pufferüberläufe treten auf, wenn ein Programm versucht, mehr Daten in einen Puffer zu schreiben, als er eigentlich aufnehmen soll. Dies kann dazu führen, dass Daten in benachbarten Speicherorten überschrieben werden, wodurch möglicherweise andere Daten beschädigt werden oder das Programm abstürzt. Pufferüberläufe sind eine häufige Quelle von Sicherheitslücken, da sie zur Ausführung beliebigen Codes ausgenutzt werden können.
Es gibt viele Möglichkeiten, wie Pufferüberläufe auftreten können. Wenn ein Programm beispielsweise eine Zeichenfolge in einen Puffer kopiert, ohne die Länge der Zeichenfolge zu überprüfen, überschreibt es möglicherweise den Speicher über das Ende des Puffers hinaus. Wenn ein Programm außerdem einen Array-Index verwendet, der außerhalb der Grenzen liegt, greift es möglicherweise auf Speicher zu, der ihm nicht gehört, und beschädigt die Daten.

Um Pufferüberläufe zu verhindern, sollten Programmierer immer die Größe des Zielpuffers überprüfen, bevor sie Daten dorthin kopieren. Sie sollten auch sichere Programmierpraktiken anwenden, wie z. B. die Funktionen strcpy_s und strncpy_s in C oder die Klasse std::string in C++, die eine Grenzüberprüfung ermöglicht. Codierungsstandards wie MISRA C/C++ identifizieren die Verwendung unsicherer Systemfunktionen und bieten Alternativen zur Behebung dieser identifizierten Schwachstellen.

2. Nachträgliche Nutzung

Use-after-free-Fehler treten auf, wenn ein Programm versucht, auf bereits freigegebenen Speicher zuzugreifen oder diesen zu ändern. Dies kann passieren, wenn ein Zeiger auf den freigegebenen Speicher nicht ordnungsgemäß ungültig gemacht wird oder wenn der Zeiger an einen anderen Teil des Programms übergeben wird, der nicht weiß, dass der Speicher nicht mehr gültig ist. Diese Art von Speicherbeschädigungsfehler kann zu unvorhersehbarem Verhalten führen, da der freigegebene Speicher möglicherweise einem anderen Zweck zugewiesen wird. Zu den häufigsten Ursachen zählen das Versäumnis, Zeiger auf NULL zu setzen, nachdem der zugehörige Speicher freigegeben wurde, und die falsche Verwendung eines Zeigers, nachdem er an eine Funktion übergeben wurde, die den zugehörigen Speicher freigibt.

Use-After-Free-Fehler können schwer zu erkennen und zu beheben sein, da das Programm möglicherweise ordnungsgemäß funktioniert, bis der freigegebene Speicher wiederverwendet wird. Dies kann zu unvorhersehbarem Verhalten wie Abstürzen oder Datenbeschädigung führen.

Entwickler können diese Art von Speicherbeschädigung abmildern, indem sie Speicher freigeben, wenn er nicht mehr benötigt wird. Sie sollten auch geeignete Zeigerverwaltungstechniken verwenden, wie z. B. das Setzen von Zeigern auf NULL, nachdem der Speicher, auf den sie zeigen, freigegeben wurde.

3. Doppelt frei

Double-Free-Fehler, auch Double-Deletion-Fehler genannt, treten auf, wenn ein Programm zweimal versucht, denselben Speicherblock freizugeben. Dies kann passieren, wenn das Programm mehrere Zeiger auf denselben Speicherblock verwaltet und ihn mehrmals freigibt oder wenn es denselben Zeiger mehrmals an die freie Funktion übergibt.

Double-Free-Fehler sind schwerwiegende Speicherbeschädigungsprobleme, die zu unvorhersehbarem Programmverhalten, Abstürzen und Sicherheitslücken führen können, wenn sie nicht behoben werden. Um Double-Free-Fehler zu vermeiden, sollten Programmierer ordnungsgemäße Aufzeichnungen der zugewiesenen Speicherblöcke führen und sicherstellen, dass die Zuordnung nur einmal aufgehoben wird. Bevor sie einen Speicherblock freigeben, sollten sie den Zeiger validieren, um zu prüfen, ob er bereits freigegeben wurde, um Versuche zu verhindern, denselben Speicher zweimal freizugeben.

4. Speicherlecks

Speicherlecks treten auf, wenn ein Programm Speicher zuweist, ihn jedoch nicht freigibt oder freigibt, wenn er nicht mehr benötigt wird. Diese Art der Speicherbeschädigung kann zu einer allmählichen Erschöpfung des verfügbaren Speichers führen, was zu Leistungsproblemen führt. Das Vergessen, dynamisch zugewiesenen Speicher freizugeben, und der Verlust aller Verweise auf zugewiesenen Speicher, ohne ihn freizugeben, sind häufige Ursachen für Speicherlecks.

Wie bei den anderen Arten von Speicherbeschädigungsproblemen kann es auch schwierig sein, sie zu erkennen und zu beheben, da das Programm möglicherweise einige Zeit lang ordnungsgemäß zu funktionieren scheint, bevor ihm der Speicher ausgeht.
Um Speicherlecks zu verhindern, sollten Programmierer Speicher immer dann explizit freigeben, wenn er nicht mehr benötigt wird. Sie sollten auch einen automatisierten Speicher-Debugger für C und C++ verwenden, wie Parasoft Insure++.

Häufige Ursachen und Folgen von Speicherbeschädigungen in C++

Speicherbeschädigung in C++ kann verschiedene Ursachen haben, von Programmierfehlern bis hin zu unsicheren Praktiken, und ihre Folgen können sich negativ auf das Programmverhalten, die Sicherheit und die Datenintegrität auswirken. Erwägen Sie die Zuweisung von Speicher zu Beginn der Anwendung und die Verwaltung dieses Speicherblocks mit überlasteten neuen und freien Funktionen, um Speicherbeschädigungsprobleme zu kontrollieren und zu beheben

Die folgenden Abschnitte bieten einen Überblick über einige häufige Ursachen und Folgen von Speicherbeschädigungen in C++.

1. Undefiniertes Verhalten

Undefiniertes Verhalten ist eine der häufigsten Ursachen für Speicherbeschädigung in C und C++. Es tritt auf, wenn das Programm Code ausführt, der nicht den Sprachspezifikationen entspricht. Im Kontext des Speichers können der Zugriff auf nicht initialisierten Speicher, das Lesen/Schreiben über Array-Grenzen hinaus und die Dereferenzierung von Null- oder Dangling-Zeigern zu undefiniertem Verhalten führen. Die Folgen undefinierten Verhaltens sind unvorhersehbar, was es zu einem kritischen Problem für Entwickler macht.

2. Sicherheitslücken

Speicherbeschädigung stellt eine ernsthafte Bedrohung für die Sicherheit von C- und C++-Programmen dar. Das Ausnutzen von Speicherschwachstellen ist eine gängige Technik für Angreifer, um Systeme zu kompromittieren. Pufferüberläufe, „Use-after-free“ und andere speicherbezogene Probleme können ausgenutzt werden, um beliebigen Code auszuführen, bösartige Payloads einzuschleusen oder das Programmverhalten zu manipulieren. Das Verstehen und Beseitigen dieser Schwachstellen ist für die Entwicklung sicherer Software von entscheidender Bedeutung.

3. Programmabstürze und Instabilität

Speicherbeschädigungen äußern sich häufig in Programmabstürzen und Instabilität. Wenn auf beschädigten Speicher zugegriffen oder dieser manipuliert wird, kann es zu unerwartetem Verhalten und zum Absturz des Programms kommen. Die Ermittlung der Grundursache solcher Abstürze kann eine Herausforderung sein und erfordert eine gründliche Fehlerbehebung und Analyse. Richtige Speicherverwaltungspraktiken und -tools können dazu beitragen, diese Probleme zu verhindern und die Programmstabilität zu verbessern.

4. Datenkorruption

Eine Speicherbeschädigung kann zur Beschädigung kritischer Datenstrukturen und Variablen innerhalb eines Programms führen. Dies kann zu falschen Berechnungen, Datenverlust oder unbeabsichtigtem Verhalten führen. Beispielsweise können Pufferüberläufe wichtige Kontrollstrukturen überschreiben und zu Datenbeschädigungen führen. Um eine Datenbeschädigung zu verhindern, sind eine sorgfältige Speicherverwaltung, eine ordnungsgemäße Überprüfung der Grenzen und die Einhaltung sicherer Codierungspraktiken erforderlich.

Wie erkennt man Speicherfehler?

Der beste Weg, um komplexe Speicherfehler zu finden, ist die Verwendung von a Tool zur Speicherfehlererkennung oder „Laufzeit-Debugger“. Es ist einfach zu bedienen. Ersetzen Sie einfach Ihren Compilernamen (cc) wie unten durch „insure“.

cc -o hello hello.c

wird

insure -o hello hello.c

Führen Sie als Nächstes das Programm aus. Wenn Sie über ein gut formatiertes Makefile verfügen, können Sie Parasoft Insure++ verwenden, indem Sie Ihren Compiler-Befehl auf „insure“ einstellen:

make CC=insure hello

Nachdem Sie mit dem Laufzeit-Debugger kompiliert haben, können Sie den folgenden Befehl ausführen:

hello cruel world

Es werden die unten gezeigten Fehler generiert, da die Zeichenfolge, die verkettet wird, länger als die 16 Zeichen wird, die in der Deklaration in Zeile 11 zugewiesen sind:

[hello.c:14] **WRITE_OVERFLOW**
&gt;&gt;         strcat(str, argv[i]);
  Writing overflows memory: &lt;argument 1&gt;
          bbbbbbbbbbbbbbbbbbbbbbbbbb
          |           16           | 2 |
          wwwwwwwwwwwwwwwwwwwwwwwwwwwwww
   Writing  (w) : 0xbfffeed0 thru 0xbfffeee1 (18 bytes)
   To block (b) : 0xbfffeed0 thru 0xbfffeedf (16 bytes)
                 str, declared at hello.c, 11
  Stack trace where the error occurred:
                          strcat()  (interface)
                            main()  hello.c, 14
**Memory corrupted.  Program may crash!!**
[hello.c:17] **READ_OVERFLOW**
&gt;&gt;     printf("You entered: %s\n", str);
  String is not null terminated within range: str
  Reading   : 0xbfffeed0
  From block: 0xbfffeed0 thru 0xbfffeedf (16 bytes)
             str, declared at hello.c, 11
  Stack trace where the error occurred:
                            main()  hello.c, 17
You entered: hello cruel world    

Ihnen ist wahrscheinlich etwas Interessantes in der Ausgabe aufgefallen, nämlich dass es zwei Fehler gibt, die auf dieses Problem zurückzuführen sind:

  1. Der Schreibüberlauf, wenn Sie versuchen, zu viele Bytes in den String-Puffer zu schreiben.
  2. Beim Lesen aus dem String-Puffer kommt es zu einem Leseüberlauf.

Wie Sie sehen, kann sich der Fehler an verschiedenen Stellen auf unterschiedliche Weise manifestieren. Stellen Sie sich also vor, was in einem echten Programm passieren kann. Es ist fast offensichtlich, dass alle funktionierenden C- und C++-Programme Speicherlecks und andere Speicherfehler aufweisen.

Wenn Sie diese Fehler finden möchten, ohne wochenlang obskure Probleme zu verfolgen, werfen Sie einen Blick darauf Parasoft Insure ++. Es kann alle Probleme finden, die mit dem Überschreiben des Speichers oder dem Lesen über die zulässigen Grenzen eines Objekts hinausgehen, unabhängig davon, ob es statisch, also als globale Variable, lokal auf dem Stapel, dynamisch mit malloc oder new oder sogar als gemeinsam genutzter Speicher zugewiesen ist Block. Es kann sogar Situationen erkennen, in denen ein Zeiger von einem Speicherblock in einen anderen wechselt und dort beginnt, den Speicher zu überschreiben, selbst wenn die Speicherblöcke benachbart sind. Die Laufzeitfehlererkennung mit Insure++ stärkt Ihre Anwendung und erspart Ihnen die nächtlichen Debugging-Sitzungen.

Schauen Sie sich den ultimativen Speicher-Debugger für C und C++ an.

„MISRA“, „MISRA C“ und das Dreieckslogo sind eingetragene Marken von The MISRA Consortium Limited. ©The MISRA Consortium Limited, 2021. Alle Rechte vorbehalten.