Windows Internals: LastErrorToBreakOn

W kodzie Windowsa można znaleźć czasem ciekawe fragmenty, które skrywają pomocne mechanizmy i sposoby ułatwiające debugowanie różnych elementów, nie tylko systemowych. Część, jeśli nie wszystkie, takie wstawki nie są nigdzie udokumentowane. Zatem nic dziwnego, że dziś znów trafiłem na kolejny taki trick w kodzie systemowym. Mowa tutaj o conditional breakpoint przy ustawianiu kodu błędu, co może być bardzo pomocne przy ciężkim debugowaniu nieszczęśliwych przypadków.

Większość systemowych funkcji, ale nie tylko takich, w razie błędu ustawia odpowiednią wartość nie-zerową, określającą zaistniałą sytuację. Coś jak errno w *niksowym świecie. Wartość ostatniego błędu można pobrać za pomocą dedykowanej funkcji WinAPI GetLastError, która odczytuje go prosto ze struktury TEB - przedstawiłem taki przypadek, gdy żonglowałem PEB/TEB-em. Nic nie stoi na przeszkodzie, aby operować na tych danych bezpośrednio, ale lepiej skorzystać z dedykowanego API. Zapis za pomocą adekwatnej funkcji - SetLastError (i RtlSetLastWin32Error z ntdll).

Jak można się domyślić funkcja ta jest wywoływana miliony razy. Gdy chcemy znaleźć callera, ustawiającego poszukiwana wartość kodu błędu, wykorzystanie zwykłego brekpointa na tej funkcji może nie być dobrym posunięciem. Na pewno będzie to meczące i spowalniajacę. Wtedy warto skorzystać z kilku dostępnych możliwości.

Warunkowy brakpoint

Standardowo, w takich przypadkach wystarczy ustawić odpowiedni warunek dla breakpointa na adresie funkcji SetLastError, który będzie determinował sytuację, kiedy faktycznie będzie potrzeba przerwania programu. Tym warunkiem będzie proste porównanie przypisywanej wartości z tą poszukiwaną.

Bez symboli nie powinno być problemów z dodaniem brekapointa na eksportowaną funkcję:

{,,kernel32}SetLastError

Z symbolami bez problemu powinna działać również pełna manglowana nazwa:

{,,kernel32}_SetLastError@4

A w warunku proste porówananie pierwszego parametru z wartoscią oczekiwaną:

*(unsigned*)(esp+4) == 666

W każdym szanującym się debugerze bez problemu można korzystać z różnych wyrażeń i prostych skryptów. Niestety obecnie prawie wcale nie korzystam z WinDbg, więc nie wiem czy przedstawiona wyżej składnia z VC będzie z nim kompatybilna. Sadząc po krótkim info z Conditional breakpoints in WinDbg and other Windows debuggers, raczej napewno nie :)

Mechanizm wykorzystujący warunkowy breakpoint jest prosty i skuteczny, ale trochę mało wydajny. Wszystkie przerwania zostaną odfiltrowane przez debuger, co napewno może spowolnić proces debugowania.

g_dwLastErrorToBreakOn

Innym ciekawym sposobem jest wykorzystanie mechanizmu jaki został zakodowany w samej funkcji SetLastError. Kod ten robi dokładnie to samo co warunkowy breakpoint, tyle że natywnie. Wystarczy zajrzeć do implementacji funkcji RtlSetLastWin32Error z ntdll.dll, do której trafiają finalnie wszystkie wywołania SetLastError, aby znależć fragment kodu odwołujący się do zmiennej globalnej g_dwLastErrorToBreakOn:

7DE9230B	mov  eax, _g_dwLastErrorToBreakOn
7DE92310	mov  edx, [ebp+arg_0]
7DE92313	test eax, eax
7DE92315	jnz  loc_7DEFFC7E
[...]
7DEFFC7E loc_7DEFFC7E:
7DEFFC7E	cmp  edx, eax
7DEFFC80	jnz  loc_7DE9231B
7DEFFC86	int  3

W pseudokodzie-C, całą implementację funckji można zapisać w bardziej ludzkiej postaci:

void RtlSetLastWin32Error(DWORD dwErrCode) {
 
	PTEB teb = NtCurrentTeb();
	if (g_dwLastErrorToBreakOn && dwErrCode == g_dwLastErrorToBreakOn)
		__debugbreak();
	if (teb->LastErrorValue != dwErrCode)
		teb->LastErrorValue = dwErrCode;
 
}

Jeśli ustawiana wartość kodu błędu dwErrCode będzie równa wartości ustawionej w zmiennej globalnej g_dwLastErrorToBreakOn, zostanie rzucony programowy wyjątek (przerwanie) numer 3, czyli pułapka aka breakpoint. I z debugerem, czy bez niego, program zostanie wstrzymany w tym miejscu.

Fajne rozwiązanie, na pewno jest to lepsze i wydajniejsze od typowej warunkowej pułapki. Nie ma żadnego dodatkowego narzutu w czasie wykonania, gdyż warunek z g_dwLastErrorToBreakOn zawsze jest sprawdzany...

Wartość zmiennej można ustawić za pomocą debugera, pod VC najprościej skorzystać z (Quick)Watch lub Immedaite Window. Odwołać się do zmiennej można rożnymi sposobami, obsługiwane są dwie składnie, ktoóre zwrócą adres zmiennej:

{,,ntdll}_g_dwLastErrorToBreakOn
ntdll.dll!_g_dwLastErrorToBreakOn

Któremu po zrzutowaniu i dereferencji można przypisać dowolną wymaganą wartość:

*(int*){,,ntdll}_g_dwLastErrorToBreakOn = 666

Pod debugerem WinDbg zapewne wystarczy tylko:

ed ntdll!g_dwLastErrorToBreakOn 666

Kod ten pojawił się pierwotnie w wersji XP systemu, początkowo tylko w implementacji SetLastError w module kernel32 i dotyczył tylko tego jawnego wywołania funkcji. Dopiero w Viście przeniesiono tę logikę do ntdll do funckji RtlSetLastWin32Error, przez co wszystkie inne możliwe funkcyjne sposoby ustawiania kodu błędu przechodzą przez ten kod. Pomijając oczywiście bezpośrednie grzebanie w TEB-ie.

Możliwość ta nie została do tej pory oficjalnie i publicznie udokumentowana, choć udało mi się znaleźć kilka odwołań do tego tematu. Na blogu Nynaeve - Debugger tricks: Break on a specific Win32 last error value in Windows Vista i dalej gdzieś na blogach MSDN-owych - What error?.

Istnieje wiele takich podobnych zmysłowych fragmentów kodu i trików zaszytych w samym kodzie Windowsa. Większość, jak pisałem, jest nieudokumentowana, ale grzebiąc w internalsach systemu czasem można znaleźć jakieś ciekawe perełki. Ostatnio znalazłem, ale tym razem w sieci, bardzo podobny mechanizm związany z bibliotekami dll, przydatny przy debugowaniu inicjalizacji konkretnych modułów. Może wkrótce uda mi się o nim napisać.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *