Jak działa debugger?

170

Ciągle się zastanawiam, jak działa debugger? W szczególności ten, który można „dołączyć” do już działającego pliku wykonywalnego. Rozumiem, że kompilator tłumaczy kod na język maszynowy, ale skąd debugger „wie”, do czego jest dołączany?

ya23
źródło
5
Artykuł Eli'ego został przeniesiony na eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1
Oktalist
@Oktalist Ten artykuł jest interesujący, ale mówi tylko o abstrakcji na poziomie API do debugowania w systemie Linux. Chyba OP chce wiedzieć więcej o tym, co kryje się pod maską.
smwikipedia

Odpowiedzi:

96

Szczegóły działania debugera będą zależeć od tego, co debugujesz i jaki jest system operacyjny. Szczegółowe informacje dotyczące debugowania w systemie Windows można znaleźć w witrynie MSDN: Win32 Debugging API .

Użytkownik informuje debugera, do którego procesu ma się dołączyć, według nazwy lub identyfikatora procesu. Jeśli jest to nazwa, debugger wyszuka identyfikator procesu i zainicjuje sesję debugowania poprzez wywołanie systemowe; w systemie Windows byłoby to DebugActiveProcess .

Po podłączeniu debugger wejdzie w pętlę zdarzeń, podobnie jak w przypadku każdego interfejsu użytkownika, ale zamiast zdarzeń pochodzących z systemu okienkowego system operacyjny będzie generował zdarzenia w oparciu o to, co dzieje się w debugowanym procesie - na przykład wystąpienie wyjątku. Zobacz WaitForDebugEvent .

Debugger jest w stanie odczytywać i zapisywać pamięć wirtualną procesu docelowego, a nawet dostosowywać wartości rejestrów za pomocą interfejsów API udostępnianych przez system operacyjny. Zobacz listę funkcji debugowania dla systemu Windows.

Debugger jest w stanie wykorzystać informacje z plików symboli do tłumaczenia adresów na nazwy zmiennych i lokalizacje w kodzie źródłowym. Informacje o pliku symboli są oddzielnym zestawem interfejsów API i nie są podstawową częścią systemu operacyjnego jako takiego. W systemie Windows odbywa się to za pomocą zestawu SDK dostępu do interfejsu debugowania .

W przypadku debugowania środowiska zarządzanego (.NET, Java itp.) Proces będzie zazwyczaj wyglądał podobnie, ale szczegóły są inne, ponieważ środowisko maszyny wirtualnej zapewnia interfejs API debugowania, a nie podstawowy system operacyjny.

Rob Walker
źródło
5
To pytanie może brzmieć głupio, ale w jaki sposób system operacyjny śledzi, jeśli osiągnięty zostanie określony adres w programie. Np. Punkt przerwania jest ustawiony na adres 0x7710cafe. Gdy wskaźnik instrukcji zmienia się, system operacyjny (a może procesor) będzie musiał porównać wskaźnik instrukcji ze wszystkimi adresami punktów przerwania, czy się mylę? Jak to działa ..?
nazwa wyświetlana
3
@StefanFalk Napisałem odpowiedź, która dotyczy niektórych szczegółów niższego poziomu (na x86).
Jonathon Reinhart,
Czy możesz wyjaśnić, jak dokładnie nazwy zmiennych są odwzorowywane na adresy? Czy aplikacja używa tych samych adresów pamięci dla zmiennych przy każdym uruchomieniu? Zawsze zakładałem, że po prostu znalazłem zmapowane z dostępnej pamięci, ale nigdy tak naprawdę nie zastanawiałem się, czy bajty będą mapowane bezpośrednio na to samo miejsce w przestrzeni pamięci aplikacji. Wygląda na to, że byłby to poważny problem dotyczący bezpieczeństwa.
James Joshua Street
@JamesJoshuaStreet Wyobrażam sobie, że jest to szczegół specyficzny dla debuggera.
moonman239
Ta odpowiedź coś ujawnia. Ale myślę, że op jest bardziej zainteresowany niektórymi szczegółami niskiego poziomu niż niektórymi abstrakcjami API.
smwikipedia
63

Tak jak rozumiem:

W przypadku programowych punktów przerwania na platformie x86 debugger zastępuje pierwszy bajt instrukcji za pomocą CC( int3). Odbywa się to WriteProcessMemoryw systemie Windows. Kiedy procesor dotrze do tej instrukcji i wykona plikint3 , powoduje to wygenerowanie przez procesor wyjątku debugowania. System operacyjny otrzymuje to przerwanie, zdaje sobie sprawę, że proces jest debugowany i powiadamia proces debugera o trafieniu w punkt przerwania.

Po trafieniu punktu przerwania i zatrzymaniu procesu debuger przegląda swoją listę punktów przerwania i zastępuje CCbajt, który był tam pierwotnie. Zestawy debugera TFpułapka Flag w EFLAGS(modyfikując CONTEXT) i kontynuuje proces. Flaga pułapki powoduje, że CPU automatycznie generuje jednoetapowy wyjątek ( INT 1) dla następnej instrukcji.

Gdy debugowany proces zatrzyma się następnym razem, debugger ponownie zamienia pierwszy bajt instrukcji punktu przerwania na CCi proces jest kontynuowany.

Nie jestem pewien, czy dokładnie tak jest zaimplementowany przez wszystkie debuggery, ale napisałem program Win32, który radzi sobie z debugowaniem za pomocą tego mechanizmu. Całkowicie bezużyteczne, ale edukacyjne.

Jonathon Reinhart
źródło
25

W Linuksie debugowanie procesu rozpoczyna się wywołaniem systemowym ptrace (2) . W tym artykule znajduje się świetny samouczek dotyczący używania ptracedo implementowania prostych konstrukcji debugowania.

Adam Rosenfield
źródło
1
Czy (2)mówi nam coś więcej (lub mniej) niż „ptrace jest wywołaniem systemowym”?
Lazer
5
@eSKay, nie, nie bardzo. To (2)jest numer sekcji instrukcji. Zobacz en.wikipedia.org/wiki/Man_page#Manual_sections, aby zapoznać się z opisem sekcji podręcznika.
Adam Rosenfield
2
@AdamRosenfield Z wyjątkiem faktu, że sekcja 2 jest konkretnie „Wywołania systemowe”. Więc pośrednio, tak, mówi nam, że ptracejest to wywołanie systemowe.
Jonathon Reinhart
1
Więcej Praktycznie, (2)mówi nam, że możemy wpisać man 2 ptracei uzyskać prawo manpage - nie ważne tutaj, bo nie ma innego ptracedo dwuznaczności, ale dla porównania man printfz man 3 printfLinux.
slim
9

Jeśli korzystasz z systemu operacyjnego Windows, doskonałym źródłem informacji na ten temat będzie „Debugowanie aplikacji dla Microsoft .NET i Microsoft Windows” autorstwa Johna Robbinsa:

(lub nawet starsze wydanie: „Aplikacje do debugowania” )

Książka zawiera rozdział o działaniu debuggera, który zawiera kod dla kilku prostych (ale działających) debuggerów.

Ponieważ nie jestem zaznajomiony ze szczegółami debugowania systemów Unix / Linux, te rzeczy mogą w ogóle nie mieć zastosowania do innych systemów operacyjnych. Sądzę jednak, że jako wprowadzenie do bardzo złożonego tematu koncepcje - jeśli nie szczegóły i interfejsy API - powinny „przenieść” do większości systemów operacyjnych.

Michael Burr
źródło
3

Innym cennym źródłem do zrozumienia debugowania jest podręcznik Intel CPU (Intel® 64 and IA-32 Architectures Software Developer's Manual). W tomie 3A, rozdziale 16, wprowadzono sprzętową obsługę debugowania, taką jak specjalne wyjątki i rejestry debugowania sprzętowego. Oto z tego rozdziału:

Flaga T (trap), TSS - generuje wyjątek debugowania (#DB), gdy podejmowana jest próba przełączenia się do zadania z flagą T ustawioną w jego TSS.

Nie jestem pewien, czy Windows lub Linux używają tej flagi, czy nie, ale przeczytanie tego rozdziału jest bardzo interesujące.

Mam nadzieję, że to komuś pomoże.

Jiang
źródło
2

Myślę, że należy odpowiedzieć na dwa główne pytania:

1. Skąd debugger wie, że wystąpił wyjątek?

Gdy wyjątek wystąpi w procesie, który jest debugowany, debuger jest powiadamiany przez system operacyjny, zanim jakiekolwiek procedury obsługi wyjątków użytkownika zdefiniowane w procesie docelowym będą miały szansę odpowiedzieć na wyjątek. Jeśli debuger zdecyduje się nie obsługiwać tego powiadomienia o wyjątku (pierwszej szansy), sekwencja wywoływania wyjątków jest kontynuowana, a wątek docelowy ma następnie szansę obsłużyć wyjątek, jeśli chce to zrobić. Jeśli wyjątek SEH nie jest obsługiwany przez proces docelowy, debuger jest następnie wysyłany inne zdarzenie debugowania, zwane powiadomieniem drugiej szansy, aby poinformować go, że w procesie docelowym wystąpił nieobsługiwany wyjątek. Źródło

wprowadź opis obrazu tutaj


2. Skąd debugger wie, jak zatrzymać się w punkcie przerwania?

Uproszczona odpowiedź brzmi: kiedy wstawiasz punkt przerwania do programu, debugger zastępuje twój kod w tym miejscu instrukcją int3, która jest przerwaniem programowym . W efekcie program zostaje zawieszony i wywoływany jest debugger.

InTheNameOfScience
źródło
1

Rozumiem, że kiedy kompilujesz aplikację lub plik DLL, cokolwiek kompiluje, zawiera symbole reprezentujące funkcje i zmienne.

Kiedy masz kompilację do debugowania, te symbole są znacznie bardziej szczegółowe niż w przypadku kompilacji wydania, dzięki czemu debuger może podać więcej informacji. Kiedy dołączasz debugger do procesu, sprawdza, które funkcje są aktualnie używane i rozwiązuje wszystkie dostępne symbole debugowania stąd (ponieważ wie, jak wyglądają elementy wewnętrzne skompilowanego pliku, może ustalić, co może być w pamięci , z zawartością int, floatów, stringów itp.). Jak powiedział pierwszy plakat, te informacje i sposób działania tych symboli w dużym stopniu zależy od środowiska i języka.

DavidG
źródło
2
Tu chodzi tylko o symbole. Debugowanie obejmuje znacznie więcej niż symbole.
Jonathon Reinhart,