Debugowanie pod Windows

Autor: Paweł Kaczan

Spis treści


Wstęp


Niniejszy dokument dotyczy podstawowych kwestii związanych z odpluskwianiem programów w środowisku MS Windows. 

W pierwszej kolejności zostanie krótko przedstawione zestawienie podstawowych narzędzi. Następnie zostanie pokrótce omówione debugowanie programów przy użyciu Windbg.  Ponieważ możliwości debugowania programów pod Windbg są porównywalne do tych, dostępnych w gdb pod Linuksa, zatem opis będzie dotyczył bardziej elementów Windbg, niż samych poleceń i sposobów debugowania, które są analogiczne do tych, przedstawionych w dokumencie opisującym gdb.


Przegląd i porównanie debugerów


Do podstawowych odpluskwiaczy dostępnych w środowisku MS Windows należą:

Cecha KD NTSD WinDbg Visual Studio .NET
         
Kernel-mode debugging Y N Y N
User-mode debugging   Y Y Y
Unmanaged debugging Y Y Y Y
Managed debugging   Y Y Y
Remote debugging Y Y Y Y
Attach to process Y Y Y Y
Detach from process in Win2k and XP Y Y Y Y
SQL debugging N N N Y

WinDbg


Wstęp

The Microsoft Windows Debugger (WinDbg) udostępnia opcję debugowania programów zarówno w trybie użytkownika jak i w trybie jądra. Pozwala na:


Uruchamianie WinDbg

Po zainstalowaniu i uruchomieniu WinDbg pierwszą rzeczą, jaką musimy uczynić, jest podanie ścieżki do plików symboli. W tym celu w File | Symbol File Path dodajemy:

SRV*<c:\katalog_symboli>*http://msdl.microsoft.com/download/symbols


Korzystanie z opcji WinDbg

Jak już wspomniano, WinDbg udostępnia kilka opcji debugowania programu. Możliwe jest otwarcie pliku wykonywalnego programu, podłączenie do działającego procesu, otwarcie pliku zrzutu pamięci, a także podpięcie do zdalnej sesji debugowania. Wszystkie te opcje dostępne są z menu File. 


Tworzenie pliku zrzutu pamięci przez system operacyjny

Windows XP oferuje przygotowywanie trzech rodzajów zrzutów pamięci:
  1. Zrzut pamięci jądra
  2. Mały zrzut – 64 KB
  3. Pełny zrzut pamięci

Opcje te można znaleźć we Właściwości systemu w zakładce Zaawansowane pod pozycją Uruchamianie i odzyskiwanie.  


Debugowanie w WinDbg


Przykładowe scenariusze debugowania

Debugowanie zdalne

Debugowanie zdalne polega na odpluskwianiu systemu działającego na jednym komputerze, przy wykorzystaniu drugiego komputera. Na jednym komputerze - serwerze - instalujemy debuger, który pozwoli analizować daną maszynę. Drugi komputer - klient - kontroluje sesję debugowania uruchomiona na serwerze. 

W pierwszej kolejności należy zainstalować CDB, NTSD lub WinDbg na serwerze. Klient WinDbg może nawiązać połączenie z każdym z wymienionych debugerów (i vice versa) przy wykorzystaniu TCP lub łączy nazwanych (named pipes).

Serwer wyświetli informację na temat podłączonych klientów oraz poleceń, które wydają. Możliwe jest zakończenie serwera przez wpisanie qq. Zakończenie klienta następuje po wybraniu File | Exit. Aby debugować zdalnie trzeba być członkiem grupy Debugger Users, a serwer musi zapewniać możliwość zdalnego podłączenia.

Debugowanie "just-in-time"

Możliwe jest ustawienie WinDbg jako domyślnego debugera JIT. W tym celu należy uruchomić WinDbg z opcją '-I'.

WinDbg -I

Od tego momentu, WinDbg będzie uruchamiany automatycznie w przypadku, gdy aplikacja zgłasza wyjątek (poza sesją debugowania), którego nie potrafi obsłużyć.

Debugowanie wyjątków

Podczas debugowania, kiedy aplikacja zgłasza wyjątek, debuger otrzymuje dwie szanse obsłużenia tego wyjątku. Pierwsza nadarza się przed próbą obsłużenia wyjątku przez samą aplikację ("first chance exception"). Druga dotyczy sytuacji, kiedy aplikacja nie była w stanie obsłużyć samodzielnie wyjątku ("second-chance exception"). Jeśli debuger nie obsłuży wyjątku w drugim przypadku, aplikacja kończy działanie.

W takim przypadku przydatne stają się funkcje, które pozwalają na podgląd zapisu wyjątku i stosu wywołań aplikacji. Do tych funkcji należą:

 .lastevent oraz !analyze -v

 

Na szczególną uwagę zasługuje rozdział Debugging Techniques z dokumentacji Debugging Tools for Windows. Zawiera on opis podstawowych, jak i zaawansowanych sposobów debugowania programów.


Rzut oka na WinDbg

Po otwarciu pliku z programem do debugowania wygląd WinDbg będzie przypominał ten, przedstawiony na poniższym rysunku (rysunek przedstawia WinDbg w momencie debugowania programu w asemblerze):

Z menu View dostępny jest podgląd okien, które oferuje WinDbg.

Okno source

Zawiera kod źródłowy programu. Aktualnie wykonywana linia kodu jest podświetlana.

Okno command

Okno command udostępnia szczegółową informację na temat plików DLL oraz EXE, które zostały załadowane od chwili uruchomienia WinDbg. Daje możliwość wprowadzania poleceń dla WinDbg. 

Skrócony opis niektórych poleceń zamieszczono na poniższych tabelach, sporządzonych na podstawie źródeł materiałów do projektu. Po bardziej szczegółowy opis sposobu wykorzystania poleceń odsyłam do opisu debugowania w gdb (Linuks) oraz do dokumentacji WinDbg, gdzie wyczerpująco zostały opisane wszystkie polecenia wraz z przykładowym sposobem ich wykorzystania.

Dotyczy Polecenia Opis
przeglądanie stosu (stack trace) k, kb x Wyświetla stack trace dla stosu aktualnego wątku (x ramek). Kb uwzgędnia wypisanie trzech pierwszych argumentów przekazanych do każdej z funkcji.
ramka .frame x  
podgląd rejestrów r Wyświetla zawartość rejestrów; reax - wyświetla zawartość rejestru eax.
krok (step) t Step into (F11) - wejście wewnątrz wykonywanej funkcji.
  p Step over (F10) - przejście ponad wykonaniem funkcji
  Step out Shift + F11 - wyjście jeden poziom do góry z funkcji
(disassemble) u Deasemblowanie kilku następnych funkcji.
punkty kontrolne (breakpoints) Bl Wypisz punkty kontrolne.
  be, bd, bc Aktywuj, deaktywuj, wyczyść punkt kontrolny
  bp Ustaw punkt kontrolny.
  bu Ustawia punkt kontrolny z wyprzedzeniem. Używany w przypadku ustawiania punktu kontrolnego przy funkcji, znajdującej się w nie załadowanym(dotychczas) module.
kontynuowanie G<address/symbol> Kontynuuje wykonywanie do chwili osiągnięcia wskazanego miejsca.
(dumping data) dv Wyświetla zmienne.
  ds, da Wyświetla string.
  dt [dt module!typedef adr] Pozwala ona wyświetlić zawartość zmiennej sformatowaną zgodnie z rozpoznanym typem. Jest to przydatne gdy mamy do czynienia ze wskaźnikami na skomplikowane struktury, zatem ręczne przeglądanie zawartości wskaźnika byłoby męczące.
edycja/zmiana wartości zmiennej eb (byte), ed (dword), ea (ASCII), eu (Unicode) Edytuje wartość zmiennej.
moduły im Wyświetla moduły.
wątki ~ Wyświetla listę wątków.
wyszukiwanie symbolu w module X module!<pattern>  
(dump) .dump  
  ln address Wyświetli symbol leżący najbliżej lokacji.

Okno disassembly

Wyświetla listę kodu programu w asemblerze. Przykład:

AddSub2!main:
00401010 cc                         int 3
00401011 a100404000         mov eax,[AddSub2!val1 (00404000)]
00401016 030504404000     add eax,[AddSub2!val2 (00404004)]
0040101c 2b0508404000     sub eax,[AddSub2!val3 (00404008)]
00401022 a30c404000         mov [AddSub2!finalVal (0040400c)],eax
00401027 e855010000         call AddSub2!DumpRegs (00401181)
0040102c 6a00                     push 0x0
0040102e e859090000         call AddSub2!ExitProcess (0040198c)

Okno registers

Wyświetla aktualne wartości rejestrów procesora. Możliwe jest dostosowania okna do własnych potrzeb tak, aby wyświetlane były tylko wymagane przez nas rejestry.

.

Okno watch

Okno wyświetla aktualne wartości śledzonych zmiennych. 

Okno memory

Zawiera obraz pamięci.

Analiza zrzutu pamięci


W momencie awarii systemu operacyjnego, generowany jest plik zrzutu pamięci, w którym zapisywane są informacje na temat procesu w momencie jego nieprzewidzianego zakończenia. Możliwe jest także wygenerowanie pliku core dump przez użycie polecenia .dump.

Analiza pliku zrzutu pamięci może przebiegać w następujących krokach:

  1. Otwarcie pliku core dump: File | Open Crash Dump
  2. WinDbg zlokalizuje instrukcję, która była wykonywana w momencie, gdy nastąpiło utworzenie pliku core (zatem w czasie awarii)
  3. Wykorzystanie poleceń WinDbg w celu zlokalizowania błędu w programie. Należy także rozważyć użycie !analyze -v, które to rozszerzenie przeprowadza szereg analiz mających na celu ustalenie przyczyny awarii, a następnie wyświetla raport.

Poglądowy przykład zwracanego przez !analyze raportu:

Debugging Details:
------------------

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - Instrukcja spod "0x%08lx" odwołuje się do pamięci pod adresem "0x%08lx". Pamięć nie może być "%s".

FAULTING_IP:
HSF_CNXT+50f51
f4e5bf51 ff91c0000000 call dword ptr [ecx+0xc0]

TRAP_FRAME: f51dc7ec -- (.trap fffffffff51dc7ec)
ErrCode = 00000000
eax=ff645000 ebx=ff6670f8 ecx=00000000 edx=f4e89547 esi=f4e89500 edi=ff66765c
eip=f4e5bf51 esp=f51dc860 ebp=f51dc8b4 iopl=0 nv up ei ng nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00210286
HSF_CNXT+0x50f51:
f4e5bf51 ff91c0000000 call dword ptr [ecx+0xc0] ds:0023:000000c0=????????
Resetting default scope

CUSTOMER_CRASH_COUNT: 1

DEFAULT_BUCKET_ID: DRIVER_FAULT

BUGCHECK_STR: 0x8E

LAST_CONTROL_TRANSFER: from f4e3a970 to f4e5bf51

STACK_TEXT: 
WARNING: Stack unwind information not available. Following frames may be wrong.
f51dc8b4 f4e3a970 ff6670f8 00000000 00000000 HSF_CNXT+0x50f51
f51dc8cc f4e8aa3f ff6670f8 00000000 ff79e3d0 HSF_CNXT+0x2f970
f51dc8e4 804ec04f ff667040 80e74bf8 00000000 HSF_CNXT+0x7fa3f
f51dc8ec 80e74bf8 00000000 f95e965c ff668338 nt!IopfCallDriver+0x31
f51dc8f0 00000000 f95e965c ff668338 00000001 0x80e74bf8

FOLLOWUP_IP:
HSF_CNXT+50f51
f4e5bf51 ff91c0000000 call dword ptr [ecx+0xc0]

SYMBOL_STACK_INDEX: 0

FOLLOWUP_NAME: MachineOwner

SYMBOL_NAME: HSF_CNXT+50f51

MODULE_NAME: HSF_CNXT

IMAGE_NAME: HSF_CNXT.sys

DEBUG_FLR_IMAGE_TIMESTAMP: 3de3a497

STACK_COMMAND: .trap fffffffff51dc7ec ; kb

FAILURE_BUCKET_ID: 0x8E_HSF_CNXT+50f51

BUCKET_ID: 0x8E_HSF_CNXT+50f51
Followup: MachineOwner


Bibliografia



autor: Paweł Kaczan