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?
170
Odpowiedzi:
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.
źródło
Tak jak rozumiem:
W przypadku programowych punktów przerwania na platformie x86 debugger zastępuje pierwszy bajt instrukcji za pomocą
CC
(int3
). Odbywa się toWriteProcessMemory
w 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
CC
bajt, który był tam pierwotnie. Zestawy debugeraTF
pułapka Flag wEFLAGS
(modyfikującCONTEXT
) 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
CC
i 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.
źródło
W Linuksie debugowanie procesu rozpoczyna się wywołaniem systemowym ptrace (2) . W tym artykule znajduje się świetny samouczek dotyczący używania
ptrace
do implementowania prostych konstrukcji debugowania.źródło
(2)
mówi nam coś więcej (lub mniej) niż „ptrace jest wywołaniem systemowym”?(2)
jest numer sekcji instrukcji. Zobacz en.wikipedia.org/wiki/Man_page#Manual_sections, aby zapoznać się z opisem sekcji podręcznika.ptrace
jest to wywołanie systemowe.(2)
mówi nam, że możemy wpisaćman 2 ptrace
i uzyskać prawo manpage - nie ważne tutaj, bo nie ma innegoptrace
do dwuznaczności, ale dla porównaniaman printf
zman 3 printf
Linux.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.
źródło
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.
źródło
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
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.
źródło
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.
źródło