GNU/Linux >> Znalost Linux >  >> Linux

Praktický návod pro použití GNU Project Debugger

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:

  1. Předpokládám, že pracujete s nejnovější verzí Fedory
  2. Vyvolejte coredump pomocí přepínače c1:coredump -c1

  3. Načtěte nejnovější soubor výpisu pomocí GDB:coredumpctl debug
  4. 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 jednotky
  • f :Specifikátor formátu, jako v printf
  • u :Velikost jednotky
    • b :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:

  1. Základní ukazatel (rbp ) volající funkce je uložena v zásobníku
  2. 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.


Linux
  1. 7 praktických triků pro použití příkazu wget pro Linux

  2. Odstraňte problémy s použitím souborového systému proc na Linuxu

  3. 5 tipů pro GNU Debugger

  1. Linuxové tipy pro používání GNU Screen

  2. 8 tipů pro příkazový řádek Linuxu

  3. Kali na podsystému Windows pro Linux

  1. Tipy pro použití příkazu top v Linuxu

  2. Pomocí příkazu Linux zdarma

  3. Linux ln Command Tutorial pro začátečníky (5 příkladů)