Ricerca…


introduzione

Un sacco di tempo dagli sviluppatori C ++ è dedicato al debugging. Questo argomento è pensato per aiutare con questo compito e dare ispirazione per le tecniche. Non aspettarti un ampio elenco di problemi e soluzioni risolti dagli strumenti o un manuale sugli strumenti menzionati.

Osservazioni

Questo argomento non è ancora completo, sarebbero utili esempi su tecniche / strumenti seguenti:

  • Menzione più strumenti di analisi statica
  • Strumenti di strumentazione binaria (come UBSan, TSan, MSan, ESan ...)
  • Indurimento (CFI ...)
  • fuzzing

Il mio programma C ++ termina con segfault - valgrind

Diamo un programma fallito di base:

#include <iostream>

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

int main() { 
    fail();
}

Costruiscila (aggiungi -g per includere le informazioni di debug):

g++ -g -o main main.cpp

Correre:

$ ./main
Segmentation fault (core dumped)
$

Facciamo il debug con 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)
$

Per prima cosa ci concentriamo su questo blocco:

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

La prima riga ci dice che il segfault è causato dalla lettura di 4 byte. La seconda e la terza riga sono chiamate stack. Significa che la lettura non valida viene eseguita nella funzione fail() , riga 8 di main.cpp, che è chiamata da main, riga 13 di main.cpp.

Guardando la riga 8 di main.cpp vediamo

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

Ma prima controlliamo il puntatore, quindi cosa c'è che non va? Consente di controllare l'altro blocco:

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

Ci dice che c'è una variabile unitaria alla linea 7 e la leggiamo:

if (p3) {

Che ci indica la linea in cui controlliamo p3 invece di p2. Ma com'è possibile che p3 non sia inizializzato? Inizializziamo per:

int *p3 = p1;

Valgrind ci consiglia di ripetere con --track-origins=yes , facciamolo:

valgrind --track-origins=yes ./main

L'argomento per Valgrind è appena dopo valgrind. Se lo inseriamo dopo il nostro programma, verrebbe passato al nostro programma.

L'output è quasi lo stesso, c'è solo una differenza:

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

Il che ci dice che il valore non inizializzato che usavamo alla linea 7 è stato creato alla riga 3:

int *p1;

che ci guida al nostro puntatore non inizializzato.

Analisi segfault con GDB

Consente di utilizzare lo stesso codice di cui sopra per questo esempio.

#include <iostream>

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

int main() { 
    fail();
}

Per prima cosa compilarlo

g++ -g -o main main.cpp

Lo eseguiamo con gdb

gdb ./main

Ora saremo nella shell gdb. Digitare 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;

Vediamo che l'errore di segmentazione sta accadendo alla riga 11. Quindi l'unica variabile utilizzata in questa linea è il puntatore p2. Esaminiamo il suo contenuto digitando stampa.

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

Ora vediamo che p2 è stato inizializzato a 0x0 che sta per NULL. A questa linea, sappiamo che stiamo cercando di dereferenziare un puntatore NULL. Quindi andiamo a sistemarlo.

Codice pulito

Il debug inizia con la comprensione del codice che stai cercando di eseguire il debug.

Codice errato:

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

Codice migliore:

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

Indipendentemente dagli stili di codifica che preferisci e utilizzi, avere uno stile coerente di codifica (e formattazione) ti aiuterà a capire il codice.

Guardando il codice sopra, è possibile identificare un paio di miglioramenti per migliorare la leggibilità e la debugabilità:

L'uso di funzioni separate per azioni separate

L'uso di funzioni separate consente di saltare alcune funzioni nel debugger se non si è interessati ai dettagli. In questo caso specifico, potresti non essere interessato alla creazione o alla stampa dei dati e vuoi solo entrare nell'ordinamento.

Un altro vantaggio è che è necessario leggere meno codice (e memorizzarlo) mentre si passa attraverso il codice. Ora devi solo leggere 3 righe di codice in main() per capirlo, invece dell'intera funzione.

Il terzo vantaggio è che hai semplicemente meno codice da guardare, il che aiuta un occhio esperto a individuare questo bug in pochi secondi.

Usando una formattazione / costruzioni coerenti

L'uso di formattazione e costruzioni consistenti rimuoverà il disordine dal codice, rendendo più facile concentrarsi sul codice anziché sul testo. Molte discussioni sono state alimentate dallo stile di formattazione "giusto". Indipendentemente da questo stile, avere uno stile unico e coerente nel codice migliorerà la familiarità e renderà più facile concentrarsi sul codice.

Poiché il codice di formattazione richiede molto tempo, si consiglia di utilizzare uno strumento dedicato per questo. La maggior parte degli IDE ha almeno un qualche tipo di supporto per questo e può fare una formattazione più coerente degli umani.

Potresti notare che lo stile non è limitato agli spazi e ai newline poiché non mescoliamo più lo stile libero e le funzioni membro per ottenere inizio / fine del contenitore. ( v.begin() vs std::end(v) ).

Fai attenzione alle parti importanti del tuo codice.

Indipendentemente dallo stile che decidi di scegliere, il codice sopra riportato contiene un paio di indicatori che potrebbero darti un suggerimento su ciò che potrebbe essere importante:

  • Un commento che afferma optimized , questo indica alcune tecniche di fantasia
  • Alcuni ritorni anticipati in sortVector() indicano che stiamo facendo qualcosa di speciale
  • std::ref() indica che qualcosa sta succedendo con sortVector()

Conclusione

Avere un codice pulito ti aiuterà a capire il codice e ridurrà il tempo necessario per il debug. Nel secondo esempio, un revisore del codice potrebbe persino individuare il bug a prima vista, mentre il bug potrebbe essere nascosto nei dettagli nel primo. (PS: il bug è in confronto con 2 ).

Analisi statica

L'analisi statica è la tecnica con cui controlla il codice per i pattern collegati ai bug noti. L'utilizzo di questa tecnica richiede meno tempo di una revisione del codice, tuttavia i suoi controlli sono limitati a quelli programmati nello strumento.

I controlli possono includere il punto e virgola errato dietro l'istruzione if (var); ( if (var); ) fino agli algoritmi di grafi avanzati che determinano se una variabile non è inizializzata.

Avvisi del compilatore

Abilitare l'analisi statica è facile, la versione più semplice è già integrata nel compilatore:

Se abiliti queste opzioni, noterai che ogni compilatore troverà bug che gli altri non hanno e che otterrai errori su tecniche che potrebbero essere valide o valide in un contesto specifico. while (staticAtomicBool); potrebbe essere accettabile anche se while (localBool); non lo è.

Quindi, a differenza della revisione del codice, stai combattendo uno strumento che capisce il tuo codice, ti dice molti bug utili ea volte non sei d'accordo con te. In quest'ultimo caso, potrebbe essere necessario sopprimere l'avviso localmente.

Poiché le opzioni precedenti abilitano tutti gli avvisi, potrebbero attivare gli avvisi che non si desidera. (Perché il tuo codice dovrebbe essere compatibile con C ++ 98?) Se è così, puoi semplicemente disabilitare quell'avvertimento specifico:

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

Dove gli avvertimenti del compilatore ti assistono durante lo sviluppo, rallentano la compilazione un po '. Questo è il motivo per cui potresti non voler sempre abilitarli di default. O li esegui di default o abiliti un'integrazione continua con i controlli più costosi (o tutti).

Strumenti esterni

Se si decide di avere un'integrazione continua, l'uso di altri strumenti non è così lungo. Uno strumento come clang-tidy ha una lista di controlli che copre una vasta gamma di problemi, alcuni esempi:

  • Bug effettivi
    • Prevenzione dell'affettatura
    • Asserisce con effetti collaterali
  • Controlli di leggibilità
    • Rientranza fuorviante
    • Controlla la denominazione dell'identificatore
  • Controlli di modernizzazione
    • Usa make_unique ()
    • Usa nullptr
  • Controlli sulle prestazioni
    • Trova copie non necessarie
    • Trova chiamate algoritmiche inefficienti

L'elenco potrebbe non essere così grande, poiché Clang ha già molti avvisi sul compilatore, tuttavia ti porterà un passo avanti verso una base di codice di alta qualità.

Altri strumenti

Esistono altri strumenti con scopi simili, come:

Conclusione

Esistono molti strumenti di analisi statica per C ++, entrambi incorporati nel compilatore come strumenti esterni. Provarle non richiede molto tempo per le configurazioni semplici e troveranno bug che potrebbero mancare nella revisione del codice.

Safe-stack (Stack corruzioni)

Le corruzioni dello stack sono fastidiosi bug da guardare. Dato che lo stack è corrotto, il debugger spesso non può darti una buona traccia di stack di dove sei e come ci sei arrivato.

È qui che entra in gioco lo stack sicuro. Invece di usare una singola pila per i tuoi thread, ne userà due: uno stack sicuro e uno stack pericoloso. Lo stack sicuro funziona esattamente come prima, tranne che alcune parti vengono spostate nella pila pericolosa.

Quali parti della pila vengono spostate?

Ogni parte che ha il potenziale di corrompere lo stack verrà spostata fuori dallo stack sicuro. Non appena una variabile nello stack viene passata per riferimento o uno prende l'indirizzo di questa variabile, il compilatore deciderà di allocarlo sul secondo stack invece che su quello sicuro.

Di conseguenza, qualsiasi operazione eseguita con quei puntatori, qualsiasi modifica apportata alla memoria (basata su quei puntatori / riferimenti) può solo influenzare la memoria nel secondo stack. Dato che non si ottiene mai un puntatore vicino allo stack sicuro, lo stack non può corrompere lo stack e il debugger può comunque leggere tutte le funzioni nello stack per dare una bella traccia.

A cosa serve effettivamente?

Lo stack sicuro non è stato inventato per darti una migliore esperienza di debug, tuttavia è un bell'effetto collaterale per i bug cattivi. Lo scopo originale è come parte del progetto Code-Pointer Integrity (CPI) , in cui si tenta di impedire l'override degli indirizzi di ritorno per prevenire l'iniezione di codice. In altre parole, cercano di impedire l'esecuzione di un codice hacker.

Per questo motivo, la funzione è stata attivata su cromo e è stato segnalato avere un sovraccarico della CPU <1%.

Come abilitarlo?

Al momento, l'opzione è disponibile solo nel compilatore clang , dove si può passare -fsanitize=safe-stack al compilatore. È stata presentata una proposta per implementare la stessa funzionalità in GCC.

Conclusione

Le corruzioni dello stack possono diventare più facili da eseguire il debug quando lo stack sicuro è abilitato. A causa di un sovraccarico di prestazioni ridotte, puoi anche essere attivato di default nella configurazione di build.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow