Pokud jste programátor a chcete do svého softwaru vložit určitou funkcionalitu, začněte přemýšlením o způsobech, jak ji implementovat – jako je psaní metody, definování třídy nebo vytváření nových datových typů. Poté implementaci napíšete v jazyce, kterému kompilátor nebo interpret rozumí. Ale co když kompilátor nebo interpret nerozumí instrukcím tak, jak jste je měli na mysli, i když jste si jisti, že jste udělali vše správně? Co když software většinu času funguje dobře, ale za určitých okolností způsobuje chyby? V těchto případech musíte vědět, jak správně používat debugger, abyste našli zdroj svých potíží.
GNU Project Debugger (GDB) je mocný nástroj pro hledání chyb v programech. Pomáhá vám odhalit důvod chyby nebo selhání sledováním toho, co se děje uvnitř programu během provádění.
Tento článek je praktickým návodem na základní použití GDB. Chcete-li pokračovat podle příkladů, otevřete příkazový řádek a naklonujte toto úložiště:
git clone https://github.com/hANSIc99/core_dump_example.git
Zkratky
Další zdroje pro Linux
- Cheat pro příkazy Linuxu
- Cheat sheet pro pokročilé příkazy systému Linux
- Bezplatný online kurz:Technický přehled RHEL
- Síťový cheat pro Linux
- Cheat sheet SELinux
- Cheat pro běžné příkazy pro Linux
- Co jsou kontejnery systému Linux?
- Naše nejnovější články o Linuxu
Každý příkaz v GDB lze zkrátit. Například info break
, který ukazuje nastavené body přerušení, lze zkrátit na i break
. Tyto zkratky možná uvidíte jinde, ale v tomto článku napíšu celý příkaz, aby bylo jasné, která funkce se používá.
Parametry příkazového řádku
GDB můžete připojit ke každému spustitelnému souboru. Přejděte do úložiště, které jste naklonovali, a zkompilujte jej spuštěním make
. Nyní byste měli mít spustitelný soubor s názvem coredump . (Viz můj článek o Vytváření a ladění souborů výpisu Linuxu pro více informací..
Chcete-li ke spustitelnému souboru připojit GDB, zadejte:gdb coredump
.
Váš výstup by měl vypadat takto:
Říká, že nebyly nalezeny žádné symboly ladění.
Informace o ladění jsou součástí objektového souboru (spustitelného souboru) a zahrnují datové typy, podpisy funkcí a vztah mezi zdrojovým kódem a operačním kódem. V tuto chvíli máte dvě možnosti:
- Pokračujte v ladění sestavy (viz „Ladění bez symbolů“ níže)
- Zkompilujte s informacemi o ladění pomocí informací v další části
Kompilace s informacemi o ladění
Chcete-li do binárního souboru zahrnout informace o ladění, musíte jej znovu zkompilovat. Otevřete Makefile a odstraňte hashtag (#
) z řádku 9:
CFLAGS =-Wall -Werror -std=c++11 -g
g
volba říká kompilátoru, aby zahrnul informace o ladění. Spusťte make clean
následuje make
a znovu vyvolejte GDB. Měli byste získat tento výstup a můžete začít s laděním kódu:
Další informace o ladění zvětší velikost spustitelného souboru. V tomto případě zvětší spustitelný soubor 2,5krát (z 26 088 bajtů na 65 480 bajtů).
Spusťte program pomocí -c1
přepněte zadáním run -c1
. Program se spustí a zhroutí se, když dosáhne State_4
:
Můžete získat další informace o programu. Příkaz info source
poskytuje informace o aktuálním souboru:
- 101 řádků
- Jazyk:C++
- Kompilátor (verze, ladění, architektura, příznak ladění, jazykový standard)
- Formát ladění:DWARF 2
- Nejsou k dispozici žádné informace o makrech preprocesoru (při kompilaci pomocí GCC jsou makra dostupná pouze v případě, že jsou kompilována pomocí
-g3
vlajka).
Příkaz info shared
vytiskne seznam dynamických knihoven s jejich adresami ve virtuálním adresním prostoru, který byl načten při spuštění, aby se program provedl:
Pokud se chcete dozvědět o práci s knihovnami v Linuxu, přečtěte si můj článek Jak zacházet s dynamickými a statickými knihovnami v Linuxu .
Ladění programu
Možná jste si všimli, že program můžete spustit v GDB pomocí run
příkaz. run
příkaz přijímá argumenty příkazového řádku, jaké byste použili ke spuštění programu z konzoly. -c1
přepínač způsobí pád programu na stupni 4. Chcete-li spustit program od začátku, nemusíte opouštět GDB; jednoduše použijte run
příkaz znovu. Bez -c1
přepínač, program provede nekonečnou smyčku. Museli byste to zastavit pomocí Ctrl+C .
Můžete také spustit program krok za krokem. V C/C++ je vstupním bodem main
funkce. Použijte příkaz list main
pro otevření části zdrojového kódu, která zobrazuje main
funkce:
main
funkce je na řádku 33, takže tam přidejte bod přerušení zadáním break 33
:
Spusťte program zadáním run
. Podle očekávání se program zastaví na main
funkce. Zadejte layout src
pro paralelní zobrazení zdrojového kódu:
Nyní jste v režimu textového uživatelského rozhraní (TUI) GDB. Pomocí kláves se šipkami nahoru a dolů procházejte zdrojový kód.
GDB zvýrazní řádek, který se má provést. Zadáním next
(n), můžete příkazy provádět řádek po řádku. GBD provede poslední příkaz, pokud nezadáte nový. Chcete-li kód projít, stačí stisknout Enter klíč.
Čas od času si všimnete, že se výstup TUI trochu poškodí:
Pokud k tomu dojde, stiskněte Ctrl+L pro resetování obrazovky.
Použijte Ctrl+X+A pro vstup a odchod do režimu TUI dle libosti. Další klávesové zkratky najdete v manuálu.
Chcete-li ukončit GDB, jednoduše napište quit
.
Sledovací body
Srdcem tohoto příkladu programu je stavový automat běžící v nekonečné smyčce. Proměnná n_state
je jednoduchý výčet, který určuje aktuální stav:
while(true){
switch(n_state){
case State_1:
std::cout << "State_1 reached" << std::flush;
n_state = State_2;
break;
case State_2:
std::cout << "State_2 reached" << std::flush;
n_state = State_3;
break;
(.....)
}
}
Chcete zastavit program, když n_state
je nastavena na hodnotu State_5
. Chcete-li tak učinit, zastavte program na main
a nastavte hlídací bod pro n_state
:
watch n_state == State_5
Nastavení sledovacích bodů s názvem proměnné funguje pouze v případě, že je požadovaná proměnná dostupná v aktuálním kontextu.
Když budete pokračovat v provádění programu zadáním continue
, měli byste dostat výstup jako:
Pokud budete v provádění pokračovat, GDB se zastaví, když se výraz watchpoint vyhodnotí jako false
:
Můžete určit sledovací body pro obecné změny hodnot, specifické hodnoty a přístup pro čtení nebo zápis.
Změna bodů přerušení a sledovaných bodů
Zadejte info watchpoints
pro tisk seznamu dříve nastavených sledovaných bodů:
Smazat body přerušení a sledované body
Jak vidíte, sledovací body jsou čísla. Chcete-li odstranit konkrétní sledovaný bod, zadejte delete
následované číslem hlídacího bodu. Například můj watchpoint má číslo 2; pro odstranění tohoto kontrolního bodu zadejte delete 2
.
Upozornění: Pokud použijete delete
bez zadání čísla, vše sledovací body a body přerušení budou smazány.
Totéž platí pro body přerušení. Na níže uvedeném snímku obrazovky jsem přidal několik bodů přerušení a vytiskl jsem jejich seznam zadáním info breakpoint
:
Chcete-li odstranit jeden bod přerušení, zadejte delete
následuje jeho číslo. Případně můžete bod přerušení odstranit zadáním čísla jeho řádku. Například příkaz clear 78
odstraní bod přerušení číslo 7, který je nastaven na řádku 78.
Zakázat nebo povolit body přerušení a sledovací body
Místo odstranění bodu přerušení nebo sledovacího bodu jej můžete deaktivovat zadáním disable
následuje jeho číslo. V následujícím jsou body přerušení 3 a 4 zakázány a jsou v okně kódu označeny znaménkem mínus:
Je také možné upravit řadu bodů přerušení nebo sledovacích bodů zadáním něčeho jako disable 2 - 4
. Pokud chcete body znovu aktivovat, napište enable
následuje jejich čísla.
Podmíněné zarážky
Nejprve odstraňte všechny body přerušení a sledovací body zadáním delete
. Stále chcete, aby se program zastavil na main
funkce, ale místo zadání čísla řádku přidejte bod přerušení přímým pojmenováním funkce. Zadejte break main
přidat zarážku na main
funkce.
Zadejte run
spustíte provádění od začátku a program se zastaví na main
funkce.
main
funkce obsahuje proměnnou n_state_3_count
, který se zvýší, když stavový stroj narazí na stav 3.
Chcete-li přidat podmíněný bod přerušení na základě hodnoty n_state_3_count
typ:
break 54 if n_state_3_count == 3
Pokračujte v provádění. Program spustí stavový automat třikrát, než se zastaví na řádku 54. Kontrola hodnoty n_state_3_count
, zadejte:
print n_state_3_count
Udělejte zarážky podmíněné
Je také možné podmínit existující bod přerušení. Odstraňte nedávno přidaný bod přerušení pomocí clear 54
a přidejte jednoduchý bod přerušení zadáním break 54
. Tento bod přerušení můžete podmínit zadáním:
condition 3 n_state_3_count == 9
3
odkazuje na číslo bodu přerušení.
Nastavte body přerušení v jiných zdrojových souborech
Pokud máte program, který se skládá z několika zdrojových souborů, můžete nastavit zarážky zadáním názvu souboru před číslem řádku, např. break main.cpp:54
.
Záchytné body
Kromě bodů přerušení a sledovacích bodů můžete nastavit i body záchytu. Záchytné body se vztahují na události programu, jako je provádění systémových volání, načítání sdílených knihoven nebo vyvolávání výjimek.
Chcete-li zachytit write
syscall, který se používá k zápisu do STDOUT, zadejte:
catch syscall write
Pokaždé, když program zapisuje na výstup konzoly, GDB přeruší provádění.
V příručce můžete najít celou kapitolu týkající se bodů přerušení, sledování a záchytných bodů.
Vyhodnocování a manipulace se symboly
Tisk hodnot proměnných se provádí pomocí print
příkaz. Obecná syntaxe je print <expression> <value>
. Hodnotu proměnné lze upravit zadáním:
set variable <variable-name> <new-value>.
Na níže uvedeném snímku obrazovky jsem uvedl proměnnou n_state_3_count
hodnotu 123 .
/x
výraz vypíše hodnotu v šestnáctkové soustavě; pomocí &
operátora, můžete vytisknout adresu ve virtuálním adresním prostoru.
Pokud si nejste jisti datovým typem určitého symbolu, můžete jej najít pomocí whatis
:
Pokud chcete vypsat všechny proměnné, které jsou dostupné v rozsahu main
zadejte info scope main
:
DW_OP_fbreg
hodnoty odkazují na posun zásobníku na základě aktuálního podprogramu.
Alternativně, pokud jste již uvnitř funkce a chcete vypsat všechny proměnné v aktuálním zásobníku, můžete použít info locals
:
Další informace o zkoumání symbolů naleznete v příručce.
Připojit k běžícímu procesu
Příkaz gdb attach <process-id>
umožňuje připojit se k již běžícímu procesu zadáním ID procesu (PID). Naštěstí coredump
program vytiskne svůj aktuální PID na obrazovku, takže jej nemusíte ručně hledat pomocí ps nebo top.
Spusťte instanci aplikace coredump:
./coredump
Operační systém udává PID 2849
. Otevřete samostatné okno konzoly, přesuňte se do zdrojového adresáře aplikace coredump a připojte GDB:
gdb attach 2849
GDB okamžitě zastaví provádění, když jej připojíte. Zadejte layout src
a backtrace
k prozkoumání zásobníku volání:
Výstup zobrazuje proces přerušený při provádění std::this_thread::sleep_for<...>(...)
funkce, která byla volána na řádku 92 main.cpp
.
Jakmile GDB ukončíte, proces bude pokračovat.
Další informace o připojení k běžícímu procesu naleznete v příručce GDB.
Posouvání v zásobníku
Vraťte se do programu pomocí up
dvakrát, abyste se posunuli v zásobníku nahoru na main.cpp
:
Obvykle kompilátor vytvoří podprogram pro každou funkci nebo metodu. Každý podprogram má svůj vlastní stack frame, takže pohyb nahoru v stackframe znamená pohyb nahoru v callstacku.
Další informace o vyhodnocení zásobníku naleznete v příručce.
Určete zdrojové soubory
Při připojování k již běžícímu procesu GDB vyhledá zdrojové soubory v aktuálním pracovním adresáři. Alternativně můžete zdrojové adresáře zadat ručně pomocí directory
příkaz.
Vyhodnocení souborů výpisu
Přečtěte si Vytváření a ladění souborů s výpisem stavu Linuxu pro informace o tomto tématu.
TL;DR:
- Předpokládám, že pracujete s nejnovější verzí Fedory
- Vyvolejte coredump pomocí přepínače c1:
coredump -c1
- Načtěte nejnovější soubor výpisu pomocí GDB:
coredumpctl debug
- Otevřete režim TUI a zadejte
layout src
Výstup backtrace
ukazuje, že k havárii došlo pět snímků zásobníku od main.cpp
. Enter pro skok přímo na chybný řádek kódu v main.cpp
:
Pohled na zdrojový kód ukazuje, že se program pokusil uvolnit ukazatel, který nebyl vrácen funkcí správy paměti. To má za následek nedefinované chování a způsobilo SIGABRT
.
Ladění bez symbolů
Pokud nejsou k dispozici žádné zdroje, věci jsou velmi obtížné. Svou první zkušenost jsem s tím měl, když jsem se snažil řešit výzvy reverzního inženýrství. Je také užitečné mít určitou znalost jazyka symbolických instrukcí.
Podívejte se, jak to funguje s tímto příkladem.
Přejděte do zdrojového adresáře, otevřete Makefile a upravte řádek 9 takto:
CFLAGS =-Wall -Werror -std=c++11 #-g
Chcete-li program znovu zkompilovat, spusťte make clean
následuje make
a spustit GDB. Program již nemá žádné ladicí symboly, které by vedly ke zdrojovému kódu.
Příkaz info file
odhaluje oblasti paměti a vstupní bod binárního souboru:
Vstupní bod odpovídá začátku .text
oblast, která obsahuje skutečný operační kód. Chcete-li přidat bod přerušení na vstupní bod, zadejte break *0x401110
poté spusťte spuštění zadáním run
:
Chcete-li nastavit bod přerušení na určité adrese, zadejte jej pomocí operátoru dereferencování *
.
Vyberte příchuť disassembler
Než se ponoříte hlouběji do sestavy, můžete si vybrat, kterou příchuť sestavy použijete. Výchozí nastavení GDB je AT&T, ale preferuji syntaxi Intel. Změňte jej pomocí:
set disassembly-flavor intel
Nyní otevřete sestavu a zaregistrujte okno zadáním layout asm
a layout reg
. Nyní byste měli vidět výstup takto:
Uložit konfigurační soubory
Přestože jste již zadali mnoho příkazů, ve skutečnosti jste nezačali s laděním. Pokud intenzivně ladíte aplikaci nebo se pokoušíte vyřešit problém reverzního inženýrství, může být užitečné uložit nastavení specifická pro GDB do souboru.
Konfigurační soubor gdbinit
v úložišti GitHub tohoto projektu obsahuje nedávno použité příkazy:
set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg
set write on
vám umožňuje modifikovat binární soubor během provádění.
Ukončete GDB a znovu ji otevřete pomocí konfiguračního souboru: gdb -x gdbinit coredump
.
Přečtěte si pokyny
Pomocí c2
přepnete, program se zhroutí. Program se zastaví na vstupní funkci, takže musíte napsat continue
pokračovat v provádění:
idiv
instrukce provede celočíselné dělení s dividendou v RAX
registru a dělitele zadaného jako argument. Kvocient se načte do RAX
registru a zbytek se nahraje do RDX
.
V přehledu registrů můžete vidět RAX
obsahuje 5 , takže musíte zjistit, která hodnota je uložena v zásobníku na pozici RBP-0x4
.
Čtení paměti
Chcete-li číst nezpracovaný obsah paměti, musíte zadat několik dalších parametrů než pro čtení symbolů. Když se ve výstupu sestavení trochu posunete nahoru, uvidíte rozdělení zásobníku:
Nejvíce vás zajímá hodnota rbp-0x4
protože toto je pozice, kde je argument pro idiv
Je uložen. Na snímku obrazovky můžete vidět, že další proměnná se nachází na rbp-0x8
, takže proměnná na rbp-0x4
je široký 4 bajty.
V GDB můžete použít x
příkaz prozkoumat jakýkoli obsah paměti:
x/
n f
u
>addr>
Volitelné parametry:
n
:Počet opakování (výchozí:1) se vztahuje k velikosti jednotkyf
:Specifikátor formátu, jako v printfu
:Velikost jednotkyb
:bajtůh
:poloviční slova (2 bajty)w
:slovo (4 bajty) (výchozí)g
:obří slovo (8 bajtů)
Chcete-li vytisknout hodnotu na rbp-0x4
, zadejte x/u $rbp-4
:
Pokud budete mít tento vzorec na paměti, je snadné prozkoumat paměť. Podívejte se do části o zkoumání paměti v příručce.
Manipulujte se sestavou
K aritmetické výjimce došlo v podprogramu zeroDivide()
. Když se posunete trochu nahoru pomocí tlačítka šipka nahoru, můžete najít tento vzor:
0x401211 <_Z10zeroDividev> push rbp
0x401212 <_Z10zeroDividev+1> mov rbp,rsp
Toto se nazývá prolog funkce:
- Základní ukazatel (
rbp
) volající funkce je uložena v zásobníku - Hodnota ukazatele zásobníku (
rsp
) se načte do základního ukazatele (rbp
)
Tento podprogram úplně přeskočte. Zásobník volání můžete zkontrolovat pomocí backtrace
. Jste pouze o jeden snímek zásobníku před vaším main
funkce, takže se můžete vrátit do main
jedním up
:
Ve vašem main
funkce, můžete najít tento vzor:
0x401431 <main+497> cmp BYTE PTR [rbp-0x12],0x0
0x401435 <main+501> je 0x40145f <main+543>
0x401437 <main+503> call 0x401211<_Z10zeroDividev>
Podprogram zeroDivide()
se zadává pouze tehdy, když jump equal (je)
vyhodnotí jako true
. Můžete to snadno nahradit jump-not-equal (jne)
instrukce, která má operační kód 0x75
(za předpokladu, že jste na architektuře x86/64; operační kódy se na jiných architekturách liší). Restartujte program zadáním run
. Když se program zastaví na vstupní funkci, manipulujte s operačním kódem zadáním:
set *(unsigned char*)0x401435 = 0x75
Nakonec zadejte continue
. Program přeskočí podprogram zeroDivide()
a už nebude padat.
Závěr
GDB můžete najít pracující na pozadí v mnoha integrovaných vývojových prostředích (IDE), včetně Qt Creator a rozšíření Native Debug pro VSCodium.
Je užitečné vědět, jak využít funkce GDB. Obvykle ne všechny funkce GDB lze použít z IDE, takže můžete využít zkušeností s používáním GDB z příkazového řádku.