Development z Zapomnianej Strony

..:: Paweł Hofman .NET Portal ::..

Mało kto zdaje sobie tak naprawdę sprawę, że oprócz wersjonowania kodu oraz oficjalnych/inżynierskich releasów wysyłanych do klientów wypada również wersjonować pliki PDB z nimi związane. Dlaczego? Odpowiedź jest bardzo prosta. Wyobraźmy sobie, iż nasza aplikacja się po prostu wysypuje. Zaraz ktoś mi powie – “hola hola, ale przecież mamy logi, w pięknym tekstowym formacie i wszystko w nich widać”. Może ktoś nawet słyszał o plikach MAP :) Super! No ale nie wszystkie nasze moduły muszą być przecież napisane w .NET. Te napisane w językach natywnych (C/C++) nie podadzą nam tak łatwo miejsca wystąpienia błędu (czytaj: pełny stos wywołań, call-stack). Jedyne na co możemy wtedy liczyć, bez wspomnianej informacji debugowej, to nazwa modułu i adres, pod którym spotkany został błąd. Ponadto nie zawsze mamy pod ręką Visual Studio. Jeśli chcielibyśmy przeprowadzić debugowanie na nieco niższym poziomie przy pomocy WinDBG, aby wykryć zakleszczenia (dead-locks) oraz wysokie zużycie pamięci, czy po prostu różne inne wycieki pamięci natywnej (nawet w kodzie .NET) to okazują się one wręcz niezbędne. W dodatku na różnych maszynach, ten sam błąd może wystąpić pod różnym adresem, co związane jest z niedeterministyczną realokacją modułów lub po prostu dysponujemy zrzutem pamięci (memory dump file), który pozwala nam w pełni cieszyć się chwilą awarii naszej aplikacji w trybie “pośmietnym” (określanym jako post-mortem debugging).

A tak niewiele trzeba, aby całe zadanie w pełni zautomatyzować. Potrzebujemy zrobić około trzech rzeczy:

  • założyć lokalne repozytorium plików PDB oraz EXE/DLL dla naszych produktów (dajmy na to w katalogu D:\SymbolStore\Projects)
  • dla jeszcze większej wygody założyć lokalne repozytorium na pliki PDB dla bibliotek systemu operacyjnego, które będziemy pobierać z publicznych serwerów Microsoftu (co pomoże rozszyfrować nazwy funkcji oraz przekazywane parametry do systemowych wywołań WinAPI – kernel32.dll, user32.dll…)
  • zdefiniować zmienną systemową _NT_SYMBOL_PATH, która będzie pokazywała na te repozytoria:

_NT_SYMBOL_PATH=srv*D:\SymbolStore\Microsoft\* http://msdl.microsoft.com/download/symbols;srv* D:\SymbolStore\Projects\

 

Od tej pory z każdą nową wersją naszej aplikacji wykonamy poniższe dwie linijki skryptu (rozszerzone samemu już też o pliki EXE/DLL lub inne cenne dla nas dane):

set MODPATH=D:\SymbolStore\Projects\
set ProjectPath=D:\Projects\MyProject
symstore add /r /f "%ProjectPath%\*.pdb" /s %MODPATH% /t "MyProject" /v "Build v2.0.1" /c "15/12/2009 Daily Build"

Narzędzie symstore służące do zarządzania lokalnym repozytorium plików PDB dostępne jest za darmo po zainstalowaniu pakietu Debugging Tools for Windows (czyli po prostu z WinDBG). Dodatkowo zmienną tę poprawnie rozpoznaje większość Microsoftowych debuggerów i automatycznie pobiera odpowiednie pliki PDB związane z modułami EXE/DLL, które aktualnie analizujemy.

Nie ma róży bez kolców:

  • w ciągu kilku pierwszych uruchomień, większość tych plików PDB dla naszego OSa musi zostać pobrana z Internetu, co może trochę potrwać (300MB się jednak trochę pobiera…)
  • zajmują one też coraz więcej miejsca na dysku ;)
  • no i w końcu sam czas włączania debugera, zwłaszcza Visual Studio, znacząco się wydłuża, bo są one ładowane do pamięci, aby rozwiązywać nazwy funkcji; WinDBG jest tu o tyle inteligentniejszy, że ładuje te pliki na żądanie lub kiedy podejrzewa, że będą potrzebne nie obciążając maszyny aż tak bardzo.

Miłego debugowania.



Z różnych powodów, czasem nie chcemy, aby ktoś przyglądał się naszej aplikacji w trakcie jej działania. Pomińmy jednak na chwilę te powody i skupmy się na samym wykrywaniu debuggera…

Najprostszą metodą sprawdzenia w systemie Windows, czy nasza aplikacja jest aktualnie uruchomiona w jego kontekście lub jest on podłączony można wykonać poprzez wywołanie funkcji IsDebuggerPresent() z WinAPI. Jednakże sprytny debugger może ją przesłonić i zawsze zwracać, że nie.

Informację tę zatem wyciągniemy samemu, “ręcznie” z bloku informacji o wątku (Thread Information Block – TIB). Pod indeksem 0x30 znajduje się wskaźnik na strukturę opisującą debugger. Drugi bajt tej struktury opisuje status tego, czego szukamy. Ponadto wartość ta jest dynamicznie uaktualniana, gdy tylko coś dzieje się z naszym procesem. Przykład implementacji funkcji w C, która pobierze dla nas tę wartość przestawiam poniżej.


int IsSuperDebuggerPresent ()
{
    int result;

    __asm
    {
        /* get the Thread Information Block (TIB) pointer */
        mov eax, fs:[18h]

        /* 0x30 byte points to the debugger structure: */
        mov eax, dword ptr [eax + 0x30]

        /* then second word indicates if the process is under debugging: */
        movzx eax, byte ptr [eax + 2]
        mov result, eax
    }

    return result;
}


Inną metodą pozwalającą sprawdzić, czy nic nie dzieje się wokół naszej aplikacji jest również sprawdzanie licznika instrukcji RTDSC (Time Stamp Counter). W swoim kodzie możemy oszacować ilość ticków procesora, która mija przy wykonywaniu pewnych, zabezpieczanych bloków instrukcji. Następnie porównywać je  ze statystykami prowadzonymi wśród całego kodu.  Tutaj jednak pojawiają się pytania – jak zachowywać się i w środowiskach wielowątkowych i wieloprocesorowych, gdzie wielkości te nie dają się łatwo przewidzieć i mogą zmieniać się w czasie…



Na 10.-tym spotkaniu grupy PLSSUG 23.lipca wydarzyła się rzecz niespotykana. Udało mi się przed grupą zapaleńców i administratorów baz danych (MS SQL 2005+) zaprezentować temat czysto .NET-owy. Odbiło się to nieco na końcowych ocenach, gdzie grupę administratorów zanudziłem na śmierć i uśpiłem na prawie 2h, a grupę programistów doprowadziłem do ekstazy i niemałych braw. Wystąpienie przedłużyło się prawie dwukrotnie, ale mimo to nikt nie zauważył nawałnicy, która tego dnia przetoczyła się przez Wrocław.

Jeśli jesteś miłośnikiem ułatwiania swojej pracy przy debugowaniu aplikacji, a w szczególności unikaniu go, zapraszam do lektury. Poruszyłem też trochę “ukryte” możliwości Visual Studio, wykorzystanie WinDBG na serwerach produkcyjnych oraz dokonałem pobieżnego przeglądu wewnętrznej architektury platformy .NET. O zaawansowanym debugowaniu wspominam przy serwerach symboli, debugowaniu po wyłączeniu aplikacji (post-mortem debugging) i idei crash-dumpów.

Materiały są oczywiście dostępne tutaj.

Następnym razem zapraszam na spotkanie.



 

Watch Window Debugowanie i podglądanie wartości zmiennych w oknie Watch, często byłoby dużo wygodniejsze, gdyby dane można rzutować na inne typy oraz poddawać je innym manipulacjom. Na szczęście prócz wymuszenia zmiany typów, okno to posiada wbudowany zestaw predefinowanych formatów, które mogą pobierać dane ze znanych typów wyliczeniowych (Windows Messages lub Code Errors) lub po prostu odczytywać napisy ANSI / UTF-16, bądź traktować dane jako tablice o zadanej wielkości, podając wartości wszystkich elementów. Wpisujemy je po przecinku na końcu wyrażenia, np.:

  • lpszFileName, su --> spowoduje wyświetlenie zawartości zmiennej jako tekstu UTF-16
  • dwMessage, wm  --> wyświetli zadaną liczbę jako kod Winows Message (WM_CREATE, …)
  • index, x  –-> wyświetli liczbę w trybie hexadecymalnym
  • (char *)lpszDeviceName, 10  --> wyświetli 10 pierwszych znaków napisu

Dla C++ ten zestaw formatów przedstawia się następująco:

Nazwa formatu Opis działania Przykładowe wyrażenie Wyświetlane
d, i signed decimal integer 0x1122FF, d  
u unsigned decimal integer 0x11, u  
o unsigned octal integer    
x, X hexadecimal integer    
l, h przedrostek long/short dla: d, i, u, o, x, X    
f signed floating point    
e signed scientific notation    
g signed floating point lub signed scientific notation w zależności od tego, który jest krótszy    
c single character    
s string    
su Unicode string 0x023571FA, su  
hr HRESULT 0x00000000L, hr S_OK
wc flagi Windows Class 0x00000040, wc WC_DEFAULTCHAR
wm numery komunikatów Windows Message 0x0001, wm WM_CREATE
! raw format, usuwa wszystkie informacje o typie    

 

Nazwa formatu Opis działania Przykładowe wyrażenie
ma 64 ASCII characters lpData, ma
m 16 bytes in hexadecimal, followed by 16 ASCII characters lpData, m
mb 16 bytes in hexadecimal, followed by 16 ASCII characters lpData, mb
mw 8 words lpData, mw
md 4 doublewords lpData, md
mq 2 quadwords lpData, mq
mu 2-byte characters (Unicode) lpData, mu

 

Tak samo i w programach w języku C# formaty przestawiają się:

Nazwa formatu Opis działania Przykładowe wyrażenie Wyświetlane
d decimal integer 0x64 100
h hexadecimal integer 11 0xB
nq string without quotes “Sample Text” Sample Text
private Wyświetla prywatne zmienne obiektu.    
raw Wyświetla dane własne obiektu. Poprawne tylko dla typów pośredniczących (proxy).    
Ac Wymusza wyliczanie wartości wyrażeń (przydatne gdy wyliczanie wartości i właściwości jest wyłączone).    

 

Jako ostatnie chciałem przestawić pseudo-zmienne, które pokazują charakterystyki uruchamianej aplikacji:

Pseudo-zmienna Opis działania
$handles Wyświetla liczbę zaalokowanych uchwytów (HANDLE) aplikacji.
$vframe Wyświetla adres aktualnej ramki stosu (stack frame).
$TID Wyświetla ID aktualnego wątku.
$ENV Wyświetla rozmiar środowiska przekazanego podczas uruchamiania aplikacji.

Próba edycji tej wartości wyświetli wszystkie zmienne środowiskowe w oknie Output, bez wykonania jakiejkolwiek zmiany.
$CMDLINE Wyświetla rozmiar linii poleceń, którą uruchomiono program.

Próba edycji wartości wyświetli wartość tekstową tej linii w oknie Output, bez jakiejkolwiek zmiany.
$registername
        or
@registername
Wyświetli zawartość rejestru o zadanej nazwie.
$clk Wyświetli aktualny czas w cyklach.
$user Wyświetli informacje o koncie użytkownika, z którego uruchomiono aplikację.
$exception (C#, VB.NET, F#) Wyświetli informacje o ostatnim wyjątku.

 

Więcej informacji można znaleźć w MSDN (o C++ tutaj, o C# tutaj).



Autor

Paweł Hofman [CodeTitans]

ASP.NET
C/C++, C#, Objective-C
SQL

License and Disclaimer

Moje Gry i Aplikacje

Zobacz mnie na GoldenLine

Zobacz mnie na LinkedIn
Supported by Polish SQL Server User Group (PLSSUG)

Supported by WrocNet.org

Zine.net.pl

Wpisy

Projekty

Moje projekty open-source:

Sign in