Samouczek debugowania GDB dla początkujących

Samouczek debugowania GDB dla początkujących

Możesz już być zaznajomiony z debugowaniem skryptów bash (zobacz, jak debugować skrypty bash, jeśli nie znasz jeszcze debugowania bash), ale jak debugować c lub c++? Odkryjmy.

GDB to od dawna i kompleksowe narzędzie do debugowania Linux, którego nauczenie się zajęłoby wiele lat, gdybyś chciał dobrze poznać narzędzie. Jednak nawet dla początkujących narzędzie może być bardzo potężne i przydatne, jeśli chodzi o debugowanie C lub C++.

Na przykład, jeśli jesteś inżynierem QA i chciałbyś debugować program C i binarny, nad którym pracuje Twój zespół i zawiesza się, możesz użyć GDB, aby uzyskać wycofanie się (lista stosów o nazwie - jak drzewo - które ostatecznie doprowadził do awarii). Lub, jeśli jesteś programistą C lub C ++ i właśnie wprowadziłeś błąd do swojego kodu, możesz użyć GDB do debugowania zmiennych, kodu i innych! Zanurzmy się!

W tym samouczku się nauczysz:

  • Jak zainstalować i używać narzędzia GDB z wiersza poleceń w BASH
  • Jak wykonać podstawowe debugowanie GDB za pomocą konsoli GDB i montażu
  • Dowiedz się więcej o szczegółowych wyjściowych wytwarzanych przez GDB
Samouczek debugowania GDB dla początkujących

Zastosowane wymagania i konwencje oprogramowania

Wymagania oprogramowania i konwencje linii poleceń Linux
Kategoria Wymagania, konwencje lub wersja oprogramowania
System Niezależny od rozkładu Linuksa
Oprogramowanie Linie poleceń BASH i GDB, system oparty na systemie Linux
Inny Narzędzie GDB można zainstalować za pomocą poleceń podanych poniżej
Konwencje # - Wymaga wykonywania Linux -Commands z uprawnieniami root bezpośrednio jako użytkownik root lub za pomocą sudo Komenda
$-Wymaga wykonania Linux-commands jako zwykłego niepewnego użytkownika

Konfigurowanie GDB i programu testowego

W tym artykule przyjrzymy się małym test.C Program w języku rozwoju C, który wprowadza błąd podziału po zeru w kodzie. Kod jest nieco dłuższy niż to, co jest potrzebne w prawdziwym życiu (zrobiłoby to kilka wierszy i nie będzie wymagane żadne użycie funkcji), ale zostało to wykonane, aby podkreślić, w jaki sposób nazwy funkcji można zobaczyć wyraźnie w GDB podczas debugowania.

Najpierw zainstalujmy narzędzia, których potrzebujemy użyć sudo apt instal (Lub Sudo Yum Instal Jeśli używasz dystrybucji opartej na czerwonym kapeluszu):

sudo apt instal instalacja gdb-cunel gcc 

Zbudowanie I GCC pomogą ci skompilować test.C Program C w twoim systemie.

Następnie zdefiniujmy test.C skrypt w następujący sposób (możesz skopiować i wkleić następujące do swojego ulubionego edytora i zapisać plik jako test.C):

int rzeczywiste_calc (int a, int b) int c; C = A/B; powrót 0;  int calc () int a; int b; a = 13; B = 0; rzeczywiste_calc (a, b); powrót 0;  int main () calc (); powrót 0;  


Kilka notatek o tym skrypcie: możesz to zobaczyć, kiedy główny Funkcja zostanie uruchomiona ( główny Funkcja jest zawsze główną i pierwszą funkcją wywoływaną po uruchomieniu skompilowanego binarnego, jest to część standardu C), natychmiast wywołuje funkcję calc, Co z kolei dzwoni ATUAL_CALC Po ustawieniu kilku zmiennych A I B Do 13 I 0 odpowiednio.

Wykonanie naszego skryptu i konfigurowanie zrzutów podstawowych

Pozwól nam teraz skompilować ten skrypt za pomocą GCC i wykonaj to samo:

$ gcc -ggdb test.Test C -O.$ ./test.Wyjątek z pływającego punktu (zrzucony rdzeń) 

-GGDB opcja GCC Zapewni, że nasza sesja debugowania za pomocą GDB będzie przyjazna; dodaje do informacji o debugowaniu specyficznych dla GDB test.na zewnątrz dwójkowy. Nazwamy ten wyjściowy plik binarny za pomocą -o opcja GCC, i jako dane wejściowe mamy nasz skrypt test.C.

Kiedy wykonujemy skrypt, natychmiast otrzymujemy tajemnicze przesłanie Wyjątek zmiennoprzecinkowy (zrzucony rdzeń). Część, którą jesteśmy zainteresowani na chwilę Rdzeń porzucony wiadomość. Jeśli nie widzisz tej wiadomości (lub jeśli widzisz wiadomość, ale nie możesz zlokalizować pliku podstawowego), możesz skonfigurować lepsze zrzucenie rdzenia w następujący sposób:

Jeśli ! grep -qi 'jądro.core_pattern ' /etc /sysctl.conf; Następnie jądro sudo sh -c 'echo ".Core_Pattern = Core.%P.%u.%S.%mi.%t ">> /etc /sysctl.conf 'sudo sysctl -p fililimit -c nieograniczony 

Tutaj najpierw upewniamy się, że nie ma wzoru rdzenia jądra Linux (jądro.Core_pattern) Ustawienie wykonane jeszcze w /etc/sysctl.conf (Plik konfiguracyjny do ustawiania zmiennych systemowych w Ubuntu i innych systemach operacyjnych) oraz - pod warunkiem, że nie znaleziono istniejącego wzorca podstawowego - dodaj poręczny wzór nazwy plików (rdzeń.%P.%u.%S.%mi.%T) do tego samego pliku.

sysctl -p polecenie (do wykonania jako root, stąd sudo) Następnie zapewnia natychmiastowe przeładowanie pliku bez konieczności ponownego uruchomienia. Aby uzyskać więcej informacji na temat podstawowego wzoru, możesz zobaczyć Nazywanie podstawowych plików zrzutowych sekcja, do której można uzyskać dostęp za pomocą Man Core Komenda.

Wreszcie Ulimit -c Unlimited polecenie po prostu ustawia maksimum rozmiaru pliku podstawowego Nieograniczony na tę sesję. To ustawienie jest nie wytrwałe w uruchomieniu. Aby było to na stałe, możesz zrobić:

sudo bash -c "cat < /etc/security/limits.conf * soft core unlimited * hard core unlimited EOF 

Co doda * miękki rdzeń nieograniczony I * Hard Core Unlimited Do /itp./bezpieczeństwo/limity.conf, Zapewnienie nie ma ograniczeń dla zrzutów podstawowych.

Kiedy teraz ponownie wykorzystasz test.na zewnątrz plik, który powinieneś zobaczyć Rdzeń porzucony Wiadomość i powinieneś być w stanie zobaczyć plik podstawowy (z określonym wzorem podstawowym), w następujący sposób:

$ ls Core.1341870.1000.8.test.na zewnątrz.1598867712 Test.Test C.na zewnątrz 

Następnie zbadajmy metadane pliku podstawowego:

$ plik rdzeń.1341870.1000.8.test.na zewnątrz.1598867712 Core.1341870.1000.8.test.na zewnątrz.1598867712: ELF 64-bitowy plik rdzenia LSB, x86-64, wersja 1 (SYSV), w stylu SVR4, od './test.OUT ', prawdziwy UID: 1000, efektywny UID: 1000, prawdziwy GID: 1000, efektywny GID: 1000, execfn:'./test.out ', platforma: „x86_64” 

Widzimy, że jest to 64-bitowy plik podstawowy, którego identyfikator użytkownika był używany, jaka była platforma, i wreszcie, jak użyto wykonywalnego. Możemy również zobaczyć nazwę pliku (.8.) że był to sygnał 8, który zakończył program. Sygnał 8 to sigfpe, wyjątek zmiennoprzecinkowy. GDB pokaże nam później, że jest to wyjątek arytmetyczny.

Korzystanie z GDB do analizy zrzutu podstawowego

Otwórzmy podstawowy plik z GDB i zakładajmy, że na sekundę nie wiemy, co się stało (jeśli jesteś doświadczonym programistą, być może już widziałeś rzeczywisty błąd w źródle!):

$ gdb ./test.na zewnątrz ./rdzeń.1341870.1000.8.test.na zewnątrz.1598867712 GNU GDB (Ubuntu 9.1-0ubuntu1) 9.1 Copyright (c) 2020 Free Software Foundation, Inc. Licencja GPLV3+: GNU GPL Wersja 3 lub nowsza To jest bezpłatne oprogramowanie: możesz je zmienić i redystrybuować. Nie ma gwarancji, w jakim stopniu dozwolonym przez prawo. Wpisz „Pokaż kopiowanie” i „Pokaż gwarancję”, aby uzyskać szczegółowe informacje. Ten GDB został skonfigurowany jako „x86_64-Linux-gnu”. Wpisz „Pokaż konfigurację” dla szczegółów konfiguracji. Instrukcje zgłaszania błędów można znaleźć: . Znajdź instrukcję obsługi GDB i inne zasoby dokumentacji online pod adresem: . Aby uzyskać pomoc, wpisz „Pomoc”. Wpisz „słowo apropos”, aby wyszukać polecenia związane z „słowem”… czytanie symboli z ./test.OUT… [Nowy rdzeń LWP 1341870] został wygenerowany przez ''./test.na zewnątrz'. Program zakończony sygnałem SIGFPE, wyjątek arytmetyczny. #0 0x000056468844813b w rzeczywistym_calc (a = 13, b = 0) podczas testu.C: 3 3 C = A/B; (GDB) 


Jak widać, w pierwszej linii, którą zadzwoniliśmy GDB z pierwszą opcją naszą binarną i drugą opcją plik podstawowy. Po prostu pamiętaj binarny i rdzeń. Następnie widzimy inicjowanie GDB i otrzymujemy pewne informacje.

Jeśli zobaczysz Ostrzeżenie: nieoczekiwany rozmiar sekcji.Reg-Xstate/1341870 'w pliku podstawowym.„Lub podobna wiadomość, możesz to zignorować na razie.

Widzimy, że zrzut rdzenia został wygenerowany przez test.na zewnątrz i powiedziano im, że sygnał był sigfpe, arytmetyczny wyjątek. Świetnie; Wiemy już, że coś jest nie tak z naszą matematyką, a może nie z naszym kodem!

Następnie widzimy ramkę (pomyśl o rama jak procedura w kodzie na razie), w którym program zakończył się: ramka #0. GDB dodaje do tego wszelkiego rodzaju poręczne informacje: adres pamięci, nazwa procedury faktyczne_calc, Jakie były nasze wartości zmienne, a nawet na jednej linii (3) z którego pliku (test.C) Problem się wydarzył.

Następnie widzimy wiersz kodu (wiersz 3) Ponownie, tym razem z rzeczywistym kodem (C = A/B;) z tej linii. Wreszcie otrzymujemy monit GDB.

Problem jest już teraz bardzo jasny; zrobiliśmy C = A/B, lub z wypełnionymi zmiennymi c = 13/0. Ale człowiek nie może podzielić przez zero, a zatem komputer też nie może. Jak nikt nie powiedział komputera, jak podzielić przez zero, wystąpił wyjątek, wyjątek arytmetyczny, wyjątek / błąd zmiennoprzecinkowy.

Powrót

Zobaczmy więc, co jeszcze możemy odkryć o GDB. Spójrzmy na kilka podstawowych poleceń. Pięść to ta, której najprawdopodobniej użyjesz najczęściej: Bt:

(GDB) BT #0 0x000056468844813b w rzeczywistym_calc (a = 13, b = 0) podczas testu.C: 3 #1 0x0000564688448171 w calc () w teście.C: 12 #2 0x000056468844818a w Main () w teście.C: 17 

To polecenie jest skrótem dla Cofnij się i zasadniczo daje nam ślad obecnego stanu (Procedura po wywołanej procedurze). Pomyśl o tym jak o odwrotnej kolejności rzeczy, które się wydarzyły; rama #0 (Pierwsza ramka) to ostatnia funkcja, która była wykonywana przez program podczas awarii, i ramki #2 była pierwsza ramka o nazwie, kiedy program został uruchomiony.

Możemy w ten sposób przeanalizować, co się stało: program się zaczął i główny() został automatycznie nazywany. Następny, główny() zwany calc () (i możemy to potwierdzić w powyższym kodzie źródłowym), a na koniec calc () zwany faktyczne_calc I tam coś poszło nie tak.

Ładnie widzimy każdą linię, w której coś się wydarzyło. Na przykład rzeczywiste_calc () Funkcja została wywołana z wiersza 12 cali test.C. Zauważ, że tak nie jest calc () który został powołany z linii 12, ale raczej rzeczywiste_calc () co ma sens; test.C skończyło się na linii 12, aż do calc () Funkcja jest dotyczyła, ponieważ tutaj calc () Funkcja wywołana rzeczywiste_calc ().

Wskazówka dotycząca użytkownika zasilacza: Jeśli używasz wielu wątków, możesz użyć polecenia Wątek Zastosuj wszystkie BT Aby uzyskać wycofanie się dla wszystkich wątków, które działały w miarę rozbicia programu!

Kontrola ramy

Jeśli chcemy, możemy sprawdzić każdą ramkę, pasujący kod źródłowy (jeśli jest dostępny) i każda zmienna krok po kroku:

(GDB) F 2 #2 0x000055FA2323318A w Main () w teście.C: 17 17 calc (); (GDB) Lista 12 rzeczywistą_calc (A, B); 13 Zwrot 0; 14 15 16 int main () 17 calc (); 18 Zwrot 0; 19 (GDB) P bez symbolu „A” w bieżącym kontekście. 

Tutaj „wskoczamy do” ramki 2 za pomocą f 2 Komenda. F jest krótką ręką dla rama Komenda. Następnie wymieniamy kod źródłowy za pomocą lista polecenie i wreszcie próbuj wydrukować (używając P Polecenie skrót) wartość A zmienna, która zawodzi, jak w tym momencie A nie został jeszcze zdefiniowany w tym momencie kodu; UWAGA Pracujemy w wierszu 17 w funkcji główny(), oraz rzeczywisty kontekst, który istniał w granicach tej funkcji/ramki.

Należy zauważyć, że funkcja wyświetlania kodu źródłowego, w tym część kodu źródłowego wyświetlonego w poprzednich wynikach powyżej, jest dostępna tylko wtedy, gdy dostępny jest rzeczywisty kod źródłowy.

Tutaj od razu widzimy również Gotcha; Jeśli kod źródłowy jest inny niż kod, z którego skompilowano binarny, można łatwo wprowadzić w błąd; Dane wyjściowe może pokazać nie do zastosowania źródło / zmienione źródło. GDB robi nie Sprawdź, czy istnieje dopasowanie wersji kodu źródłowego! Jest zatem niezwykle ważne, aby używać dokładnie tej samej rewizji kodu źródłowego, jak ta, z której skompilowano twój binarny.

Alternatywą jest w ogóle nie korzystanie z kodu źródłowego i po prostu debugowanie konkretnej sytuacji w określonej funkcji, przy użyciu nowszej wersji kodu źródłowego. Dzieje się tak często w przypadku zaawansowanych programistów i debuggerów, którzy prawdopodobnie nie potrzebują zbyt wielu wskazówek na temat tego, gdzie problem może być w danej funkcji.

Następnie zbadajmy ramkę 1:

(GDB) f 1 #1 0x000055FA23233171 w calc () w teście.C: 12 12 rzeczywistą_calc (a, b); (gdb) lista 7 int calc () 8 int a; 9 int b; 10 A = 13; 11 B = 0; 12 rzeczywistą_calc (a, b); 13 Zwrot 0; 14 15 16 int main ()  

Tutaj znowu możemy zobaczyć wiele informacji przez GDB, które pomoże deweloperowi w debugowaniu danego problemu. Ponieważ jesteśmy teraz calc (on wiersz 12) i już zainicjowaliśmy, a następnie ustawiliśmy zmienne A I B Do 13 I 0 Odpowiednio możemy teraz wydrukować ich wartości:

(GDB) P A 1 USD = 13 (GDB) P B $ 2 = 0 (GDB) P C brak symbolu „C” w bieżącym kontekście. (GDB) P. A/B Division by Zero 


Zauważ, że kiedy próbujemy wydrukować wartość C, Nadal znowu się nie udaje C do tego momentu nie jest jeszcze zdefiniowane (programiści mogą mówić o „w tym kontekście”).

Wreszcie, patrzymy w ramkę #0, Nasza rama awaryjna:

(GDB) F 0 #0 0x000055FA2323313B w rzeczywistym_calc (a = 13, b = 0) podczas testu.C: 3 3 C = A/B; (GDB) P A 3 USD = 13 (GDB) P B $ 4 = 0 (GDB) P C $ 5 = 22010 

Wszystkie oczywiste, z wyjątkiem wartości zgłoszonej dla C. Zauważ, że zdefiniowaliśmy zmienną C, ale nie dał jeszcze wartości początkowej. Takie jak C jest naprawdę niezdefiniowany (i nie było wypełnione równaniem C = A/B jednak, gdy ten się nie powiodła) i wynikowa wartość prawdopodobnie odczytano z pewnej przestrzeni adresowej, do której zmienna C został przypisany (a przestrzeń pamięci nie została jeszcze zainicjowana/wyczyszczona).

Wniosek

Świetnie. Byliśmy w stanie debugować podstawowy zrzut programu C i w międzyczasie oparliśmy podstawy debugowania GDB. Jeśli jesteś inżynierem QA lub młodszym deweloperem, i dobrze zrozumiałeś i nauczyłeś się wszystkiego w tym samouczku, już niewiele wyprzedzasz większość inżynierów QA i potencjalnie innych programistów wokół ciebie.

A następnym razem, gdy obejrzysz Star Trek i kapitan Janeway lub kapitan Picard, chcą „rzucić rdzeń”, na pewno uśmiechniesz się szerszy uśmiech. Ciesz się debugowaniem następnego porzuconego rdzenia i zostaw nam komentarz poniżej z debugującymi przygodami.

Powiązane samouczki Linux:

  • Wprowadzenie do automatyzacji, narzędzi i technik Linuksa
  • Rzeczy do zainstalowania na Ubuntu 20.04
  • Mastering Bash Script Loops
  • Rzeczy do zrobienia po zainstalowaniu Ubuntu 20.04 Focal Fossa Linux
  • Mint 20: Lepsze niż Ubuntu i Microsoft Windows?
  • Ubuntu 20.04 Przewodnik
  • Rzeczy do zainstalowania na Ubuntu 22.04
  • Zagnieżdżone pętle w skryptach Bash
  • Jak podwójnie rozruch Kali Linux i Windows 10
  • Hung Linux System? Jak uciec do wiersza poleceń i…