Empfohlenes Webinar: Vorstellung von Parasoft C/C++test CT für kontinuierliche Tests und Compliance-Exzellenz | Zum Video

So verwalten Sie Zeiger in C effektiv, um Missbrauch zu verhindern

Kopfbild von Ricardo Camacho, Director of Safety & Security Compliance
1. Dezember 2023
4 min lesen

Obwohl Sie mit Zeigern Ihrer Kreativität freien Lauf lassen können, um ein bestimmtes Problem zu lösen, weisen sie einige Einschränkungen auf, die für Programmierer am schwierigsten zu bewältigen sind. Lesen Sie weiter, um zu erfahren, wie Sie mit Insure++ Zeigerprobleme automatisch identifizieren können.

Pointer sind sowohl die Stärke als auch die Achillesferse von Programmierung in C und C++. Mithilfe von Hinweisen können Sie hingegen sehr kreativ und flexibel an die Lösung eines bestimmten Problems herangehen. Es ist jedoch leicht, unabsichtlich Fehler in Ihren Code einzuführen. Probleme mit Zeigern gehören zu den schwierigsten Problemen für C-Programmierer. Vor einer Generation waren Brute-Force-Techniken wie das Einfügen von Print-Anweisungen in den Code oft die beste Strategie, um diese Probleme zu finden.

Tools zur Erkennung von Zeigermissbrauch in C

Insure++: Ein Tool für dynamische Speicherzuordnungsprüfungen

Heutzutage gibt es Tools zur Speicherfehlererkennung wie ++ versichern kann zeigerbezogene Probleme automatisch erkennen, während der Code ausgeführt wird, was viel Zeit und Kopfschmerzen spart. Insure++ findet Probleme in den folgenden Kategorien:

  • Operationen auf NULL-Zeigern
  • Operationen an nicht initialisierten Zeigern
  • Operationen an Zeigern, die nicht auf gültige Daten verweisen
  • Operationen, die versuchen, Zeiger zu vergleichen oder anderweitig in Beziehung zu setzen, die nicht auf dasselbe Datenobjekt zeigen
  • Funktionsaufrufe über Funktionszeiger, die nicht auf Funktionen verweisen
  • Viele andere Ursachen für mögliches undefiniertes Verhalten oder durch die Implementierung definiertes Verhalten

Insure++ verwendet einen hochmodernen Code-Parser sowie Hunderte von Heuristiken, um den Anwendungscode zu analysieren und dabei mehrere mögliche statische Verstöße zu melden. Während der Codeanalyse wird eine neue Quellcodedatei mit entsprechender Instrumentierung geschrieben, die an Problemstellen eingefügt wird, z. B. Zeigerdereferenzierung, Bereichsexit usw. Die resultierende Quelldatei wird automatisch kompiliert und alle resultierenden Objektcodedateien werden in ein neues ausführbares Programm verknüpft.

Identifizieren der Adresse einer Variablen: Best Practices

Wenn Sie versuchen, Speicherverwaltungsprobleme zu lösen und herauszufinden, ob eine Variable beschädigt ist, müssen Sie in vielen Fällen die Adresse dieser Variablen kennen und wahrscheinlich herausfinden, dass es sich nicht nur um diese Variable, sondern auch um andere handelt. Die Verwendung eines Debuggers wird häufig als bewährte Methode angesehen, wenn es darum geht, Probleme in Ihrem Code zu identifizieren, einschließlich des Verständnisses von Variablenwerten und Speicheradressen.

Debugger bieten leistungsstarke Tools zum Überprüfen und Bearbeiten der Ausführung Ihres Programms. Es ist jedoch wichtig zu beachten, dass sich die Verwendung eines Debuggers nicht gegenseitig mit anderen Debugging-Techniken ausschließt. Druckanweisungen und manuelle Überprüfung können immer noch wertvolle Werkzeuge sein, insbesondere in Situationen, in denen die Verwendung eines Debuggers möglicherweise unpraktisch ist.

Ein einfacher Ansatz, den ich seit vielen Jahren verwende, besteht darin, einfach den Adressoperator „&“ in einer printf-Anweisung zu verwenden. Nehmen Sie das folgende Codebeispiel.

int myVariable = 77;
printf("Address of myVariable is: %p\n", (void *)&myVariable);

Der &myVariable-Ausdruck gibt die Adresse von myVariable zurück. Der Formatbezeichner %p wird mit printf verwendet, um die Adresse in einem Zeigerformat auszugeben. Die (void*)-Umwandlung wird verwendet, um den %p-Formatbezeichner abzugleichen. Vergessen Sie nicht, dass die tatsächliche Speicheradresse bei jeder Programmausführung variieren kann.

Praxisbeispiel: Zeiger in einem „Hello World“-Programm verwalten

Unten finden Sie den Code für ein „Hello, World“-Programm, das dynamische Speicherzuweisung verwendet.

    
/*
 * File: hello.c
 */
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
    char *string, *string_so_far;
    int i, length;     length = 0;
    for(i=0; i<argc; i++) {
        length += strlen(argv[i])+1;
        string = malloc(length+1);
 
        /*  * Copy the string built so far. */
        if(string_so_far != (char *)0)
            strcpy(string, string_so_far);
        else *string = '\0';
        strcat(string, argv[i]);
        if(i < argc-1) strcat(string, " ");
        string_so_far = string;
    }
    printf("You entered: %s\n", string_so_far);
    return (0);
}
    

Die Grundidee dieses Programms besteht darin, dass wir die aktuelle Stringgröße in der Variablenlänge verfolgen. Während jedes neue Argument verarbeitet wird, addieren wir seine Länge zur Längenvariablen und weisen einen Speicherblock der neuen Größe zu. Beachten Sie, dass der Code bei der Berechnung der Zeichenfolgenlänge (Zeile 14) und des Abstands zwischen Zeichenfolgen sorgfältig darauf achtet, das letzte NULL-Zeichen einzubeziehen. Beides sind leicht zu begehende Fehler. Es ist eine interessante Übung zu sehen, wie schnell Sie einen solchen Fehler mit einem Tool zur Speicherfehlererkennung wie Parasoft Insure++ finden können.

Der Code kopiert das Argument entweder in den Puffer oder hängt es an, je nachdem, ob dies der erste Durchgang um die Schleife ist oder nicht. Schließlich zeigt der Zeiger string_so_far auf die neue längere Zeichenfolge.

Wenn Sie dieses Programm unter Insure ++ kompilieren und ausführen, werden "nicht initialisierte Zeiger" -Fehler angezeigt, die für den Code "strcpy (string, string_so_far)" gemeldet wurden. Dies liegt daran, dass die Variable string_so_far vor dem ersten Durchlaufen der Argumentschleife auf nichts gesetzt wurde. In einem kleinen Codebeispiel wie diesem ist ein solches Problem offensichtlich, aber selbst wenn der Fehler in einem Haufen von Hunderttausenden von Codezeilen vergraben ist und viel subtiler als der obige Fehler ist, wird Insure ++ ihn jedes Mal finden.

Berichte und Erkenntnisse von Insure++

Insure++ meldet alle gefundenen Probleme. Insure++-Berichte enthalten detaillierte Informationen wie die Art des Fehlers, die Nummer der Quelldatei und -zeile, den tatsächlichen Zeileninhalt des Quellcodes und Ausdrücke, die das Problem verursacht haben. Zu den Berichten gehören:

  • Der Typ des Fehlers, zum Beispiel EXPR_UNRELATED_PTRCMP
  • Die Quelldatei und Zeilennummer, zum Beispiel foo.cc:42
  • Der eigentliche Zeileninhalt des Quellcodes, zum Beispiel „while (p < g) {“
  • Der Ausdruck, der das Problem verursacht hat, zum Beispiel „p < g“
  • Informationen zu allen am Fehler beteiligten Zeigern und Speicherblöcken:
    • Zeigerwerte
    • Speicherblöcke, auf die verwiesen wird (falls vorhanden) und ein etwaiger Offset
    • Informationen zur Blockzuordnung:
      • Stack-Trace, wenn dynamisch zugewiesen
      • Speicherort der Blockdeklaration (Quelldatei und Zeilennummer), sofern auf dem Stapel oder global zugewiesen
      • Gegebenenfalls Stack-Trace der Freigabe des Blocks
    • Stack-Trace, der zeigt, wie das Programm zum Fehlerort gelangt ist

Nächste Schritte: So bewahren Sie Ihre Zeiger in C sicher auf

Die Testabdeckung ist entscheidend für die Sicherheit von Zeigern in C und C++. Statische Analyse und dynamische Analyse spielen eine wichtige Rolle.

Die Bedeutung der Testabdeckung in Zeigern

Wenn es um die Arbeit mit Zeigern in C oder C++ geht, ist die Testabdeckung wichtig, da sie Risiken mit sich bringt. Das Risiko kann auf subtile Weise auftreten und manchmal sehr schwer aufzuspüren sein, was hohe Arbeitskosten nach sich zieht.

Der Einsatz umfassender Testmethoden zur Abdeckung einer Liste von Zeigerproblemen und -szenarien ist von entscheidender Bedeutung. Zum Beispiel möchten Sie Verwenden Sie ein Tool wie Insure++ um Entwicklern dabei zu helfen, unregelmäßige Programmier- und Speicherzugriffsfehler wie Heap-Beschädigung, unerwünschte Threads, Speicherlecks, Array außerhalb der Grenzen und ungültige Zeiger zu finden.

Statische Analyse

Ein wirksamer Einstieg ist durch mittels statischer Analyse oder Fehlererkennung bei der Kompilierung, die potenzielle Probleme im Zusammenhang mit Zeigern während der Kompilierungsphase erkennt. Probleme wie der Verlust der Präzision bei der Umwandlung eines Zeigers, eine Nichtübereinstimmung der Formatspezifikation oder des Argumenttyps, nicht verwendete Variablen, toter Code, Division durch Null und mehr.

Dynamische Analyse

Dynamische Analysetests ist ein weiterer wirksamer und ergänzender Ansatz zur statischen Analyse. Die Erkennung von Laufzeitfehlern hilft bei der Identifizierung beschädigter Heap- und Stack-Speicher sowie aller Arten von Speicherlecks, Speicherzuweisungen, freien Fehlern oder Nichtübereinstimmungen und Array-Out-of-Bound-Fehlern.

Um sicherzustellen, dass Sie den gesamten Code getestet haben, ermöglicht die Codeabdeckungsanalyse die visuelle Identifizierung, welche Codeabschnitte ausgeführt wurden und welche nicht. Das Belassen von ungetestetem Code in Ihrer Anwendung kann Ihnen schlaflose Nächte bereiten oder Ihnen später schaden.

Fügen Sie Ihrer Sicherheitstest-Toolbox eine statische Analyse hinzu