C++
C ++ Felsöknings- och felsökningsverktyg och tekniker
Sök…
Introduktion
Mycket tid från C ++ -utvecklare spenderas vid felsökning. Detta ämne är avsett att hjälpa till med denna uppgift och ge inspiration till tekniker. Förvänta dig inte en omfattande lista över problem och lösningar fixade med verktygen eller en manual för de nämnda verktygen.
Anmärkningar
Det här ämnet är inte klart ännu, exempel på följande tekniker / verktyg skulle vara användbara:
- Nämna fler statiska analysverktyg
- Binära instrumentverktyg (som UBSan, TSan, MSan, ESan ...)
- Härdning (CFI ...)
- Fuzzing
Mitt C ++ -program slutar med segfault - valgrind
Låt oss ha ett grundläggande misslyckande program:
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p3 << std::endl;
}
}
int main() {
fail();
}
Bygg det (lägg till -g för att inkludera felsökningsinfo):
g++ -g -o main main.cpp
Springa:
$ ./main
Segmentation fault (core dumped)
$
Låt oss felsöka det med 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)
$
Först fokuserar vi på detta 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
Den första raden berättar att segfault orsakas av att läsa 4 byte. Den andra och den tredje raden är samtalstack. Det betyder att ogiltig avläsning utförs vid fail()
-funktionen, rad 8 i main.cpp, som kallas av main, rad 13 i main.cpp.
Ser vi på rad 8 i main.cpp ser vi
std::cout << *p3 << std::endl;
Men vi kontrollerar pekaren först, så vad är fel? Låt oss kontrollera det andra blocket:
==8515== Conditional jump or move depends on uninitialised value(s)
==8515== at 0x400813: fail() (main.cpp:7)
==8515== by 0x40083F: main (main.cpp:13)
Det säger att det finns en enhetlig variabel på rad 7 och vi läser den:
if (p3) {
Som pekar oss mot linjen där vi kontrollerar p3 istället för p2. Men hur är det möjligt att p3 är initialiserad? Vi initierar det genom:
int *p3 = p1;
Valgrind råder oss att köra igen med - --track-origins=yes
, låt oss göra det:
valgrind --track-origins=yes ./main
Argumentet för valgrind är strax efter valgrind. Om vi sätter det efter vårt program, skulle det skickas till vårt program.
Utgången är nästan densamma, det finns bara en skillnad:
==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)
Som säger att det oinitialiserade värdet vi använde på rad 7 skapades på rad 3:
int *p1;
vilket leder oss till vår oinitialiserade pekare.
Segfault-analys med GDB
Låt oss använda samma kod som ovan för det här exemplet.
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p2 << std::endl;
}
}
int main() {
fail();
}
Låt oss först kompilera det
g++ -g -o main main.cpp
Låter köra den med gdb
gdb ./main
Nu kommer vi att vara i gdb-skalet. Skriv körning.
(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;
Vi ser att segmenteringsfelet sker på rad 11. Så den enda variabeln som används på den här raden är pekaren p2. Låter undersöka dess innehållstyptryck.
(gdb) print p2
$1 = (int *) 0x0
Nu ser vi att p2 initialiserades till 0x0 vilket står för NULL. På den här linjen vet vi att vi försöker ta bort en NULL-pekare. Så vi går och fixar det.
Ren kod
Felsökning börjar med att förstå koden du försöker felsöka.
Dålig kod:
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; }
Bättre kod:
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;
}
Oavsett vilka kodningsstilar du föredrar och använder, kommer en konsekvent kodning (och formatering) att hjälpa dig att förstå koden.
När man tittar på koden ovan kan man identifiera ett par förbättringar för att förbättra läsbarheten och debugbarheten:
Användning av separata funktioner för separata åtgärder
Användningen av separata funktioner låter dig hoppa över vissa funktioner i felsökaren om du inte är intresserad av detaljerna. I detta specifika fall kanske du inte är intresserad av att skapa eller skriva ut data och bara vill gå in i sorteringen.
En annan fördel är att du måste läsa mindre kod (och memorera den) medan du går igenom koden. Nu behöver du bara läsa tre kodrader i main()
för att förstå den istället för hela funktionen.
Den tredje fördelen är att du helt enkelt har mindre kod att titta på, vilket hjälper ett tränat öga att upptäcka detta fel inom några sekunder.
Använd konsekvent formatering / konstruktioner
Användningen av konsekvent formatering och konstruktioner tar bort röran från koden vilket gör det lättare att fokusera på koden istället för text. Många diskussioner har matats om den "rätta" formateringsstilen. Oavsett vilken stil, med en enda konsekvent stil i koden kommer det att förbättra kännedomen och göra det lättare att fokusera på koden.
Eftersom formateringskoden är tidskrävande, rekommenderas det att använda ett särskilt verktyg för detta. De flesta IDE: er har åtminstone något slags stöd för detta och kan göra formatering mer konsekvent än människor.
Du kanske märker att stilen inte är begränsad till utrymmen och nylinjer eftersom vi inte längre blandar fri-stilen och medlemsfunktionerna för att komma i början / slutet av behållaren. ( v.begin()
vs std::end(v)
).
Var uppmärksam på de viktiga delarna av din kod.
Oavsett vilken stil du bestämmer dig för, innehåller koden ovan ett par markörer som kan ge dig ett tips om vad som kan vara viktigt:
- En kommentar som anger
optimized
, detta indikerar några snygga tekniker - Några tidiga returer i
sortVector()
indikerar att vi gör något speciellt -
std::ref()
indikerar att något händer medsortVector()
Slutsats
Att ha ren kod hjälper dig att förstå koden och minskar den tid du behöver felsöka den. I det andra exemplet kan en kodgranskare till och med upptäcka felet vid första anblicken, medan felet kan döljas i detaljerna i det första. (PS: Felet är i jämförelse med 2
)
Statisk analys
Statisk analys är den teknik där man kontrollerar koden för mönster kopplade till kända buggar. Att använda denna teknik är mindre tidskrävande än en kodgranskning, men dess kontroller är endast begränsade till de som är programmerade i verktyget.
Kontroller kan inkludera felaktig halvkolon bakom if-statement ( if (var);
) tills avancerade grafalgoritmer som avgör om en variabel inte initialiseras.
Compiler varningar
Det är enkelt att aktivera statisk analys, den mest förenklade versionen är redan inbyggd i din kompilator:
Om du aktiverar dessa alternativ kommer du att märka att varje kompilator hittar buggar som de andra inte gör och att du får fel på tekniker som kan vara giltiga eller giltiga i ett specifikt sammanhang. while (staticAtomicBool);
kan vara acceptabelt även om while (localBool);
är det inte.
Så till skillnad från kodgranskning, kämpar du med ett verktyg som förstår din kod, berättar för dig mycket användbara buggar och ibland inte håller med dig. I det sista fallet kanske du måste undertrycka varningen lokalt.
Eftersom alternativen ovan aktiverar alla varningar kan de aktivera varningar du inte vill ha. (Varför ska din kod vara C ++ 98-kompatibel?) I så fall kan du helt enkelt inaktivera den specifika varningen:
-
clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
g++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
cl.exe /W4 /WX /wd<no of warning>...
Där kompilatorvarningar hjälper dig under utvecklingen bromsar de sammanställningen ganska mycket. Det är därför du kanske inte alltid vill aktivera dem som standard. Antingen kör du dem som standard eller så aktiverar du en kontinuerlig integration med de dyrare kontrollerna (eller alla dem).
Externa verktyg
Om du bestämmer dig för att ha en viss kontinuerlig integration är användningen av andra verktyg inte en sådan sträckning. Ett verktyg som clang-tidy har en lista med kontroller som täcker ett brett spektrum av problem, några exempel:
- Faktiska buggar
- Förebyggande av skivning
- Påståenden med biverkningar
- Läsbarhetskontroller
- Vildledande intryck
- Kontrollera namnet på identifieraren
- Moderniseringskontroller
- Använd make_unique ()
- Använd nullptr
- Prestandakontroller
- Hitta onödiga kopior
- Hitta ineffektiva algoritmsamtal
Listan kanske inte är så stor, eftersom Clang redan har många kompilatorvarningar, men det kommer att leda dig ett steg närmare en högkodbas av hög kvalitet.
Andra verktyg
Andra verktyg med liknande syfte finns, som:
- statisk analysator för visuell studio som externt verktyg
- clazy , en Clang-kompilatorplugin för kontroll av Qt-kod
Slutsats
Det finns många statiska analysverktyg för C ++, båda inbyggda i kompilatorn som externa verktyg. Att testa dem tar inte så mycket tid för enkla inställningar och de hittar buggar du kanske missar vid kodgranskning.
Safe-stack (Stack korruptions)
Stapelkorruption är irriterande buggar att titta på. Eftersom stacken är skadad kan felsökaren ofta inte ge dig ett bra stackspår över var du är och hur du kom dit.
Det är här safe-stacken spelar in. Istället för att använda en enda stack för dina trådar, kommer den att använda två: En säker stack och en farlig stack. Den säkra stacken fungerar precis som tidigare, förutom att vissa delar flyttas till den farliga stacken.
Vilka delar av stacken flyttas?
Varje del som har potential att skada stapeln kommer att flyttas ur den säkra bunten. Så snart en variabel på bunten passeras genom referens eller en tar adressen till denna variabel kommer kompilatorn att besluta att tilldela detta på den andra bunten istället för den säkra.
Som ett resultat kan alla operationer du gör med dessa pekare, alla ändringar du gör i minnet (baserat på dessa pekare / referenser) endast påverka minnet i den andra stacken. Eftersom man aldrig får en pekare som ligger nära den säkra bunten, kan inte bunten förstöra stacken och felsökaren kan fortfarande läsa alla funktioner på bunten för att ge ett fint spår.
Vad används det faktiskt för?
Den säkra stacken uppfanns inte för att ge dig bättre felsökningsupplevelse, men det är en fin bieffekt för otäcka buggar. Det ursprungliga syftet är som en del av projektet Code-Pointer Integrity (CPI) , där de försöker förhindra att adresserna returneras för att förhindra kodinjicering. Med andra ord, de försöker förhindra att köra en hackarkod.
Av denna anledning har funktionen aktiverats på krom och har rapporterats ha en <1% CPU-overhead.
Hur aktiverar du det?
Just nu är alternativet endast tillgängligt i clang-kompilatorn , där man kan skicka -fsanitize=safe-stack
till kompilatorn. Ett förslag kom att implementera samma funktion i GCC.
Slutsats
Stack-korruption kan bli lättare att felsöka när säker stack är aktiverad. På grund av en låg prestanda kan du till och med aktivera som standard i din byggkonfiguration.