Zoeken…


Invoering

Veel tijd van C ++ -ontwikkelaars wordt besteed aan foutopsporing. Dit onderwerp is bedoeld als hulpmiddel bij deze taak en als inspiratie voor technieken. Verwacht geen uitgebreide lijst met problemen en oplossingen die worden opgelost door de tools of een handleiding over de genoemde tools.

Opmerkingen

Dit onderwerp is nog niet compleet, voorbeelden over de volgende technieken / tools kunnen nuttig zijn:

  • Noem meer statische analysehulpmiddelen
  • Binaire instrumentatietools (zoals UBSan, TSan, MSan, ESan ...)
  • Hardening (CFI ...)
  • fuzzing

Mijn C ++ programma eindigt met segfault - valgrind

Laten we een elementair falend programma hebben:

#include <iostream>

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

int main() { 
    fail();
}

Bouw het (voeg -g toe om foutopsporingsinformatie op te nemen):

g++ -g -o main main.cpp

Rennen:

$ ./main
Segmentation fault (core dumped)
$

Laten we het debuggen met valgrind:

$ 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)
$

Eerst richten we ons op dit blok:

==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

De eerste regel vertelt ons dat segfault wordt veroorzaakt door het lezen van 4 bytes. De tweede en derde lijnen zijn call-stack. Het betekent dat het ongeldige lezen wordt uitgevoerd bij de functie fail() , regel 8 van main.cpp, die wordt aangeroepen door main, regel 13 van main.cpp.

Kijkend naar regel 8 van main.cpp zien we

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

Maar we controleren eerst de wijzer, dus wat is er mis? Laten we het andere blok controleren:

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

Het vertelt ons dat er een geunialiseerde variabele is op regel 7 en we lezen het:

if (p3) {

Dat wijst ons op de regel waar we p3 controleren in plaats van p2. Maar hoe is het mogelijk dat p3 niet geïnitialiseerd is? We initialiseren het door:

int *p3 = p1;

Valgrind raadt ons aan om te herhalen met --track-origins=yes , laten we het doen:

valgrind --track-origins=yes ./main

Het argument voor valgrind is net na valgrind. Als we het na ons programma plaatsen, zou het worden doorgegeven aan ons programma.

De output is bijna hetzelfde, er is maar één verschil:

==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)

Wat ons vertelt dat de niet-geïnitialiseerde waarde die we op regel 7 hebben gebruikt, op regel 3 is gemaakt:

int *p1;

die ons naar onze niet-geïnitialiseerde wijzer leidt.

Segfault-analyse met GDB

Laten we voor dit voorbeeld dezelfde code gebruiken als hierboven.

#include <iostream>

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

int main() { 
    fail();
}

Laten we het eerst compileren

g++ -g -o main main.cpp

Laten we het uitvoeren met GDB

gdb ./main

Nu zullen we in gdb-shell zijn. Typ run.

(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;

We zien dat de segmentatiefout plaatsvindt op regel 11. Dus de enige variabele die op deze regel wordt gebruikt, is pointer p2. Laten we de inhouds-typeprint bekijken.

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

Nu zien we dat p2 is geïnitialiseerd op 0x0 wat staat voor NULL. Op deze regel weten we dat we een NULL-aanwijzer proberen af te leiden. Dus we gaan het maken.

Schone code

Debuggen begint met het begrijpen van de code die u probeert te debuggen.

Slechte 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; }

Betere 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;
 }

Ongeacht de codeerstijlen die u verkiest en gebruikt, zal een consistente codeerstijl (en opmaak) u helpen de code te begrijpen.

Als we naar de bovenstaande code kijken, kunnen we een aantal verbeteringen identificeren om de leesbaarheid en foutopsporing te verbeteren:

Het gebruik van afzonderlijke functies voor afzonderlijke acties

Door het gebruik van afzonderlijke functies kunt u sommige functies in de debugger overslaan als u niet geïnteresseerd bent in de details. In dit specifieke geval bent u misschien niet geïnteresseerd in het maken of afdrukken van de gegevens en wilt u alleen maar beginnen met sorteren.

Een ander voordeel is dat u minder code hoeft te lezen (en te onthouden) terwijl u de code doorloopt. U hoeft nu slechts 3 coderegels in main() te lezen om het te begrijpen, in plaats van de hele functie.

Het derde voordeel is dat je gewoon minder code hebt om naar te kijken, wat een getraind oog helpt om deze bug binnen seconden te herkennen.

Gebruik consistente opmaak / constructies

Het gebruik van consistente opmaak en constructies verwijdert rommel uit de code, waardoor het gemakkelijker wordt om zich op de code te concentreren in plaats van op tekst. Veel discussies zijn gevoerd over de 'juiste' opmaakstijl. Ongeacht die stijl, zal het hebben van een enkele consistente stijl in de code de bekendheid verbeteren en wordt het gemakkelijker om u op de code te concentreren.

Aangezien het formatteren van code een tijdrovende taak is, wordt aanbevolen om hiervoor een speciaal hulpprogramma te gebruiken. De meeste IDE's hebben hier op zijn minst enige vorm van ondersteuning voor en kunnen formattering consistenter maken dan mensen.

Je merkt misschien dat de stijl niet beperkt is tot spaties en nieuwe regels omdat we niet langer de vrije stijl en de lidfuncties combineren om het begin / einde van de container te krijgen. ( v.begin() vs std::end(v) ).

Richt de aandacht op de belangrijke delen van uw code.

Ongeacht de stijl die u kiest, de bovenstaande code bevat een aantal markeringen die u een hint kunnen geven over wat belangrijk kan zijn:

  • Een commentaar dat optimized , duidt op een aantal mooie technieken
  • Sommige vroege resultaten in sortVector() geven aan dat we iets speciaals doen
  • De std::ref() geeft aan dat er iets aan de hand is met de sortVector()

Conclusie

Het hebben van schone code zal u helpen de code te begrijpen en zal de tijd verminderen die u nodig hebt om het te debuggen. In het tweede voorbeeld kan een code-revisor de bug zelfs op het eerste gezicht zien, terwijl de bug misschien verborgen zit in de details in de eerste. (PS: De bug is te vergelijken met 2 )

Statische analyse

Statische analyse is de techniek waarbij de code wordt gecontroleerd op patronen die zijn gekoppeld aan bekende bugs. Het gebruik van deze techniek is minder tijdrovend dan een codebeoordeling, maar de controles zijn alleen beperkt tot die welke in de tool zijn geprogrammeerd.

Controles kunnen de onjuiste puntkomma achter de if-instructie bevatten ( if (var); ) tot geavanceerde grafiekalgoritmen die bepalen of een variabele niet is geïnitialiseerd.

Compiler-waarschuwingen

Statische analyse inschakelen is eenvoudig, de meest simplistische versie is al ingebouwd in uw compiler:

Als u deze opties inschakelt, zult u merken dat elke compiler fouten vindt die de anderen niet vinden en dat u fouten zult krijgen in technieken die geldig of geldig kunnen zijn in een specifieke context. while (staticAtomicBool); is misschien acceptabel, zelfs als while (localBool); is het niet.

Dus in tegenstelling tot codereview vecht je tegen een tool die je code begrijpt, je veel nuttige bugs vertelt en het soms niet met je eens is. In dit laatste geval moet u de waarschuwing mogelijk lokaal onderdrukken.

Omdat de bovenstaande opties alle waarschuwingen inschakelen, kunnen ze waarschuwingen inschakelen die u niet wilt. (Waarom zou uw code compatibel zijn met C ++ 98?) Zo ja, dan kunt u die specifieke waarschuwing eenvoudig uitschakelen:

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

Waar compilerwaarschuwingen u tijdens de ontwikkeling helpen, vertragen ze de compilatie behoorlijk. Daarom wilt u ze misschien niet altijd standaard inschakelen. Ofwel voer je ze standaard uit, ofwel schakel je enige continue integratie in met de duurdere controles (of allemaal).

Externe hulpmiddelen

Als u besluit om enige continue integratie te hebben, is het gebruik van andere tools niet zo lang. Een tool zoals clang-tidy heeft een lijst met controles die een breed scala aan problemen behandelt, enkele voorbeelden:

  • Werkelijke bugs
    • Voorkomen van snijden
    • Beweert met bijwerkingen
  • Leesbaarheidcontroles
    • Misleidende inspringing
    • Controleer naamgeving van identificator
  • Moderniseringscontroles
    • Gebruik make_unique ()
    • Gebruik nullptr
  • Prestatiecontroles
    • Zoek onnodige kopieën
    • Vind inefficiënte algoritmeaanroepen

De lijst is misschien niet zo groot, omdat Clang al veel compilerwaarschuwingen heeft, maar het brengt je een stap dichter bij een codebasis van hoge kwaliteit.

Andere hulpmiddelen

Er bestaan andere tools met een vergelijkbaar doel, zoals:

Conclusie

Er bestaan veel statische analysehulpmiddelen voor C ++, zowel ingebouwd in de compiler als externe hulpmiddelen. Ze uitproberen kost niet veel tijd voor eenvoudige setups en ze zullen bugs vinden die je misschien mist in code review.

Safe-stack (stapelbeschadigingen)

Stapelbeschadigingen zijn irritante bugs om naar te kijken. Omdat de stapel beschadigd is, kan de debugger je vaak geen goed stapeloverzicht geven van waar je bent en hoe je daar bent gekomen.

Dit is waar safe-stack in het spel komt. In plaats van een enkele stapel voor uw threads, gebruikt deze er twee: een veilige stapel en een gevaarlijke stapel. De veilige stapel werkt precies zoals voorheen, behalve dat sommige delen naar de gevaarlijke stapel worden verplaatst.

Welke delen van de stapel worden verplaatst?

Elk onderdeel dat de stapel kan beschadigen, wordt uit de veilige stapel verwijderd. Zodra een variabele op de stapel door verwijzing wordt doorgegeven of als het adres van deze variabele wordt overgenomen, zal de compiler besluiten deze toe te wijzen aan de tweede stapel in plaats van de veilige.

Als gevolg hiervan kan elke bewerking die u met die verwijzingen uitvoert, elke wijziging die u in het geheugen aanbrengt (op basis van die verwijzingen / verwijzingen) alleen het geheugen in de tweede stapel beïnvloeden. Omdat je nooit een pointer krijgt die dicht bij de veilige stapel staat, kan de stapel de stapel niet beschadigen en kan de debugger nog steeds alle functies op de stapel lezen om een mooi spoor te geven.

Waar wordt het eigenlijk voor gebruikt?

De veilige stack is niet uitgevonden om je een betere debug-ervaring te geven, maar het is een leuk neveneffect voor vervelende bugs. Het oorspronkelijke doel is als onderdeel van het Code-Pointer Integrity (CPI) -project , waarin ze proberen te voorkomen dat de retouradressen worden genegeerd om code-injectie te voorkomen. Met andere woorden, ze proberen te voorkomen dat een hackerscode wordt uitgevoerd.

Om deze reden is de functie geactiveerd op chroom en is gerapporteerd dat deze een CPU-overhead van <1% heeft.

Hoe dit in te schakelen?

Op dit moment is de optie alleen beschikbaar in de clang-compiler , waar men -fsanitize=safe-stack aan de compiler kan doorgeven. Er is een voorstel gedaan om dezelfde functie in GCC te implementeren.

Conclusie

Stapelcorrupties kunnen gemakkelijker te debuggen worden als veilige stapel is ingeschakeld. Vanwege de lage overheadkosten kunt u zelfs standaard activeren in uw buildconfiguratie.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow