Suche…


Einführung

Das Debuggen wird viel Zeit von C ++ - Entwicklern aufgewendet. Dieses Thema soll bei dieser Aufgabe helfen und Techniken inspirieren. Erwarten Sie keine umfangreiche Liste von Problemen und Lösungen, die von den Tools oder einem Handbuch zu den genannten Tools behoben werden.

Bemerkungen

Dieses Thema ist noch nicht abgeschlossen. Beispiele zu folgenden Techniken / Tools wären nützlich:

  • Erwähnen Sie weitere statische Analysewerkzeuge
  • Werkzeuge für binäre Instrumente (wie UBSan, TSan, MSan, ESan ...)
  • Härten (CFI ...)
  • Fuzzing

Mein C ++ - Programm endet mit segfault-valgrind

Lassen Sie uns ein grundlegendes Fehlerprogramm haben:

#include <iostream>

void fail() {
    int *p1;
    int *p2(NULL);
    int *p3 = p1;
    if (p3) {
        std::cout << *p3 << std::endl;
    } 
}

int main() { 
    fail();
}

Erstellen Sie es (fügen Sie -g hinzu, um Debug-Informationen einzuschließen):

g++ -g -o main main.cpp

Lauf:

$ ./main
Segmentation fault (core dumped)
$

Lass uns mit valgrind debuggen:

$ valgrind ./main
==8515== Memcheck, a memory error detector
==8515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==8515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==8515== Command: ./main
==8515==
==8515== Conditional jump or move depends on uninitialised value(s)
==8515==    at 0x400813: fail() (main.cpp:7)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==
==8515== Invalid read of size 4
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==8515==
==8515==
==8515== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==8515==  Access not within mapped region at address 0x0
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  If you believe this happened as a result of a stack
==8515==  overflow in your program's main thread (unlikely but
==8515==  possible), you can try to increase the size of the
==8515==  main thread stack using the --main-stacksize= flag.
==8515==  The main thread stack size used in this run was 8388608.
==8515==
==8515== HEAP SUMMARY:
==8515==     in use at exit: 72,704 bytes in 1 blocks
==8515==   total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8515==
==8515== LEAK SUMMARY:
==8515==    definitely lost: 0 bytes in 0 blocks
==8515==    indirectly lost: 0 bytes in 0 blocks
==8515==      possibly lost: 0 bytes in 0 blocks
==8515==    still reachable: 72,704 bytes in 1 blocks
==8515==         suppressed: 0 bytes in 0 blocks
==8515== Rerun with --leak-check=full to see details of leaked memory
==8515==
==8515== For counts of detected and suppressed errors, rerun with: -v
==8515== Use --track-origins=yes to see where uninitialised values come from
==8515== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
$

Zuerst konzentrieren wir uns auf diesen Block:

==8515== Invalid read of size 4
==8515==    at 0x400819: fail() (main.cpp:8)
==8515==    by 0x40083F: main (main.cpp:13)
==8515==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

Die erste Zeile sagt uns, dass Segfault durch das Lesen von 4 Bytes verursacht wird. Die zweite und die dritte Leitung sind Anrufstapel. Das bedeutet, dass der ungültige Lesevorgang in der fail() Funktion in Zeile 8 von main.cpp ausgeführt wird, die von main, Zeile 13 von main.cpp aufgerufen wird.

Wenn wir Zeile 8 von main.cpp betrachten, sehen wir

std::cout << *p3 << std::endl;

Aber wir prüfen zuerst den Zeiger. Was ist los? Lass uns den anderen Block überprüfen:

==8515== Conditional jump or move depends on uninitialised value(s)
==8515==    at 0x400813: fail() (main.cpp:7)
==8515==    by 0x40083F: main (main.cpp:13)

Es sagt uns, dass es in Zeile 7 eine unitialisierte Variable gibt, und wir lesen sie:

if (p3) {

Was zeigt uns auf die Zeile, wo wir p3 statt p2 überprüfen. Aber wie ist es möglich, dass p3 nicht initialisiert wird? Wir initialisieren es durch:

int *p3 = p1;

Valgrind rät uns mit --track-origins=yes , lass es uns tun:

valgrind --track-origins=yes ./main

Das Argument für Valgrind ist kurz nach Valgrind. Wenn wir es nach unserem Programm stellen, wird es an unser Programm weitergegeben.

Die Ausgabe ist fast gleich, es gibt nur einen Unterschied:

==8517== Conditional jump or move depends on uninitialised value(s)
==8517==    at 0x400813: fail() (main.cpp:7)
==8517==    by 0x40083F: main (main.cpp:13)
==8517==  Uninitialised value was created by a stack allocation
==8517==    at 0x4007F6: fail() (main.cpp:3)

Was uns sagt, dass der nicht initialisierte Wert, den wir in Zeile 7 verwendeten, in Zeile 3 erstellt wurde:

int *p1;

das führt uns zu unserem nicht initialisierten Zeiger.

Segfault-Analyse mit GDB

Verwenden wir den gleichen Code wie oben für dieses Beispiel.

#include <iostream>

void fail() {
    int *p1;
    int *p2(NULL);
    int *p3 = p1;
    if (p3) {
        std::cout << *p2 << std::endl;
    } 
}

int main() { 
    fail();
}

Zuerst können wir es kompilieren

g++ -g -o main main.cpp

Lass es mit gdb laufen

gdb ./main

Jetzt werden wir in gdb shell sein. Geben Sie run ein.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/opencog/code-snippets/stackoverflow/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400850 in fail () at debugging_with_gdb.cc:11
11            std::cout << *p2 << std::endl;

Wir sehen, dass der Segmentierungsfehler in Zeile 11 auftritt. Daher wird in dieser Zeile nur der Zeiger p2 verwendet. Lässt den Inhalt des Druckvorgangs prüfen.

(gdb) print p2
$1 = (int *) 0x0 

Nun sehen wir, dass p2 auf 0x0 initialisiert wurde, was für NULL steht. In dieser Zeile wissen wir, dass wir versuchen, einen NULL-Zeiger zu dereferenzieren. Also gehen wir los und reparieren es.

Code reinigen

Das Debuggen beginnt mit dem Verstehen des Codes, den Sie debuggen möchten.

Falscher Code:

int main() {
    int value;
    std::vector<int> vectorToSort;
    vectorToSort.push_back(42); vectorToSort.push_back(13);
    for (int i = 52; i; i = i - 1)
    {
    vectorToSort.push_back(i *2);
    }
    /// Optimized for sorting small vectors
    if (vectorToSort.size() == 1);
    else
        {
        if (vectorToSort.size() <= 2)
            std::sort(vectorToSort.begin(), std::end(vectorToSort));
        }
    for (value : vectorToSort) std::cout << value << ' ';
return 0; }

Besserer Code:

std::vector<int> createSemiRandomData() {
    std::vector<int> data;
    data.push_back(42);
    data.push_back(13);
    for (int i = 52; i; --i)
        vectorToSort.push_back(i *2);
    return data;
}

/// Optimized for sorting small vectors
void sortVector(std::vector &v) {
    if (vectorToSort.size() == 1)
        return;
    if (vectorToSort.size() > 2)
        return;

    std::sort(vectorToSort.begin(), vectorToSort.end());
}

void printVector(const std::vector<int> &v) {
    for (auto i : v)
        std::cout << i << ' ';
}

int main() {
    auto vectorToSort = createSemiRandomData();
    sortVector(std::ref(vectorToSort));
    printVector(vectorToSort);

    return 0;
 }

Unabhängig von den von Ihnen bevorzugten und verwendeten Codierungsstilen hilft Ihnen ein einheitlicher Codierungsstil (und Formatierungsstil), den Code besser zu verstehen.

Wenn Sie den Code oben betrachten, können Sie einige Verbesserungen feststellen, um die Lesbarkeit und die Fehlersuche zu verbessern:

Die Verwendung separater Funktionen für separate Aktionen

Durch die Verwendung separater Funktionen können Sie einige Funktionen im Debugger überspringen, wenn Sie nicht an den Details interessiert sind. In diesem speziellen Fall sind Sie möglicherweise nicht an der Erstellung oder dem Ausdruck der Daten interessiert und möchten nur in die Sortierung einsteigen.

Ein weiterer Vorteil ist, dass Sie beim Durchlaufen des Codes weniger Code lesen und speichern müssen. Sie müssen jetzt nur 3 Zeilen Code in main() lesen, um es zu verstehen, anstatt die ganze Funktion zu verwenden.

Der dritte Vorteil ist, dass Sie einfach weniger Code zum Anschauen haben, was einem geschulten Auge hilft, diesen Fehler innerhalb von Sekunden zu erkennen.

Verwenden konsistenter Formatierungen / Konstruktionen

Durch die Verwendung konsistenter Formatierungen und Konstruktionen wird Unordnung aus dem Code entfernt, sodass Sie sich leichter auf den Code statt auf Text konzentrieren können. Es wurden viele Diskussionen über den "richtigen" Formatierungsstil geführt. Unabhängig von diesem Stil wird durch die Verwendung eines einzigen konsistenten Stils im Code die Vertrautheit verbessert und es wird einfacher, sich auf den Code zu konzentrieren.

Da Formatierungscode eine zeitaufwändige Aufgabe ist, wird empfohlen, hierfür ein spezielles Werkzeug zu verwenden. Die meisten IDEs haben zumindest eine gewisse Unterstützung dafür und können konsistentere Formatierungen als Menschen vornehmen.

Möglicherweise ist der Stil nicht auf Leerzeichen und Zeilenumbrüche beschränkt, da der freie Stil und die Member-Funktionen nicht mehr gemischt werden, um den Anfang und das Ende des Containers zu erhalten. ( v.begin() vs std::end(v) ).

Machen Sie auf die wichtigen Teile Ihres Codes aufmerksam.

Unabhängig davon, welchen Stil Sie wählen, enthält der obige Code einige Markierungen, die Ihnen einen Hinweis auf das geben könnten, was wichtig sein könnte:

  • Ein Kommentar mit optimized Angaben weist auf einige ausgefallene Techniken hin
  • Einige frühe Rückgaben in sortVector() zeigen an, dass wir etwas Besonderes tun
  • Die std::ref() zeigt an, dass mit sortVector() etwas sortVector()

Fazit

Mit sauberem Code können Sie den Code besser verstehen und die Zeit für das Debuggen reduzieren. Im zweiten Beispiel kann ein Code-Reviewer den Fehler sogar auf den ersten Blick erkennen, während der Fehler in den Details des ersten Fehlers versteckt ist. (PS: Der Fehler ist im Vergleich mit 2 )

Statische Analyse

Statische Analyse ist die Technik, bei der der Code auf Muster überprüft wird, die mit bekannten Fehlern verknüpft sind. Die Verwendung dieser Technik ist weniger zeitaufwändig als eine Codeüberprüfung. Die Überprüfung ist jedoch nur auf die im Tool programmierten beschränkt.

Bei Prüfungen kann das falsche Semikolon hinter der if-Anweisung ( if (var); ) bis zu fortgeschrittenen Graph-Algorithmen stehen, die bestimmen, ob eine Variable nicht initialisiert wird.

Compiler-Warnungen

Das Aktivieren der statischen Analyse ist einfach. Die einfachste Version ist bereits in Ihrem Compiler integriert:

Wenn Sie diese Optionen aktivieren, werden Sie feststellen, dass jeder Compiler Fehler findet, die andere nicht tun, und dass Sie Fehler in Bezug auf Techniken erhalten, die in einem bestimmten Kontext gültig oder gültig sind. while (staticAtomicBool); kann auch dann akzeptiert werden, wenn while (localBool); ist nicht

Anders als bei der Codeüberprüfung kämpfen Sie mit einem Tool, das Ihren Code versteht, viele nützliche Fehler anzeigt und manchmal mit Ihnen nicht einverstanden ist. In diesem letzten Fall müssen Sie die Warnung möglicherweise lokal unterdrücken.

Da die obigen Optionen alle Warnungen aktivieren, werden möglicherweise Warnungen aktiviert, die Sie nicht möchten. (Warum sollte Ihr Code C ++ 98-kompatibel sein?) Wenn dies der Fall ist, können Sie diese Warnung einfach deaktivieren:

  • clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...
  • g++ -Wall -Weverything -Werror -Wno-errortoaccept ...
  • cl.exe /W4 /WX /wd<no of warning>...

Wo Compiler-Warnungen Sie während der Entwicklung unterstützen, verlangsamen sie die Kompilierung erheblich. Aus diesem Grund möchten Sie sie möglicherweise nicht standardmäßig aktivieren. Entweder führen Sie sie standardmäßig aus, oder Sie ermöglichen eine fortlaufende Integration der teureren (oder aller) Prüfungen.

Externe Werkzeuge

Wenn Sie sich für eine fortlaufende Integration entscheiden, ist die Verwendung anderer Tools nicht so groß. Ein Tool wie clang-tidy enthält eine Liste von Überprüfungen, die ein breites Spektrum von Problemen abdeckt, einige Beispiele:

  • Aktuelle Fehler
    • Verhinderung des Schneidens
    • Assays mit Nebenwirkungen
  • Lesbarkeitsprüfungen
    • Irreführende Einrückung
    • Überprüfen Sie die Benennung der IDs
  • Modernisierungsprüfungen
    • Verwenden Sie make_unique ()
    • Verwenden Sie nullptr
  • Leistungsprüfungen
    • Finden Sie nicht benötigte Exemplare
    • Finden Sie ineffiziente Algorithmusaufrufe

Die Liste ist möglicherweise nicht so umfangreich, da Clang bereits viele Compiler-Warnungen enthält, Sie werden jedoch einer qualitativ hochwertigen Codebasis einen Schritt näher kommen.

Andere Werkzeuge

Andere Tools mit ähnlichem Zweck existieren, wie:

Fazit

Es gibt viele statische Analysewerkzeuge für C ++, die beide als externe Werkzeuge in den Compiler integriert sind. Das Ausprobieren kostet nicht viel Zeit für einfache Setups und sie werden Fehler finden, die Sie bei der Code-Überprüfung vermissen könnten.

Safe-Stack (Stack-Korruption)

Stapelverfälschungen sind ärgerliche Fehler. Da der Stapel beschädigt ist, kann der Debugger Ihnen oft keine gute Übersicht darüber geben, wo Sie sich befinden und wie Sie dorthin gekommen sind.

Hier kommt Safe-Stack ins Spiel. Anstatt einen einzelnen Stack für Ihre Threads zu verwenden, werden zwei verwendet: ein sicherer Stack und ein gefährlicher Stack. Der sichere Stapel funktioniert genauso wie zuvor, nur dass einige Teile in den gefährlichen Stapel verschoben werden.

Welche Teile des Stapels werden verschoben?

Jedes Teil, das möglicherweise den Stapel beschädigt, wird aus dem sicheren Stapel verschoben. Sobald eine Variable auf dem Stack als Referenz übergeben wird oder die Adresse dieser Variablen abgerufen wird, entscheidet der Compiler, diese auf dem zweiten Stack statt auf dem sicheren Stack zuzuordnen.

Daher können bei jeder Operation, die Sie mit diesen Zeigern ausführen, Änderungen am Speicher (basierend auf diesen Zeigern / Referenzen) nur den Speicher im zweiten Stapel beeinflussen. Da man nie einen Zeiger erhält, der sich in der Nähe des sicheren Stapels befindet, kann der Stapel den Stapel nicht beschädigen, und der Debugger kann trotzdem alle Funktionen auf dem Stapel lesen, um eine nette Spur zu erhalten.

Wofür wird es eigentlich verwendet?

Der sichere Stack wurde nicht entwickelt, um Ihnen ein besseres Debugging-Erlebnis zu bieten, ist jedoch ein schöner Nebeneffekt für böse Fehler. Ihr ursprünglicher Zweck ist Teil des CPI-Projekts (Code-Pointer Integrity) , in dem versucht wird, das Überschreiben der Rücksprungadressen zu verhindern, um die Code-Injektion zu verhindern. Mit anderen Worten, sie versuchen zu verhindern, dass ein Hackercode ausgeführt wird.

Aus diesem Grunde hat die Funktion wurde auf Chrom aktiviert und wurde berichtet , einen <1% CPU - Overhead haben.

Wie kann ich es aktivieren?

Im Moment ist die Option nur im Clang-Compiler verfügbar, wo -fsanitize=safe-stack an den Compiler übergeben werden kann. Ein Vorschlag gemacht wurde die gleiche Funktion in GCC zu implementieren.

Fazit

Stapelverfälschungen können einfacher zu debuggen werden, wenn der sichere Stapel aktiviert ist. Aufgrund eines geringen Leistungsaufwands können Sie sogar standardmäßig in Ihrer Build-Konfiguration aktiviert werden.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow