Pliki PDB – Co każdy programista powienien wiedzieć?

Większość programistów zdaje sobie sprawę z tego, że pliki PDB pomagają w debugowaniu kodu – na tym jednak kończy się ich wiedza. Nie czuj się źle jeżeli nie wiesz co dzieje się z plikami PDB. W Internecie dostępna jest dokumentacja, jest jednak rozproszona i skierowana do osób odpowiedzialnych za pisanie kompilatorów oraz narzędzi do debuggowania. Chodź jest to (zapewne) bardzo fajne i interesujące zajęcie, nie jest to (prawdopodobnie) częścią Twoich obowiązków.

Celem tego artykułu jest umieszczenie wszystkich informacji w jednym miejscu, które są przydatne osobą pracującym na platformie .NET. Zacznę od omówienia przechowywania plików PDB oraz ich zawartości. Ponieważ debugger używa tych plików, omówie dokładnie w jaki sposób odnajduje konkretny PDB dla Twojej binarki. Na samym końcu porozmawiamy o tym jak debugger szuka tych plików w trakcie przeprowadzania debuggowania oraz pokaże Wam jak debugger odnajduje kod źródłowy.

Zanim jednak przejdziemy dalej należy wyjaśnić dwa ważne terminy. Build, który wykonujesz na swoim komputerze to tzw. "private build". Ten przygotowany na dedykowanej maszynie to "public build”. Jest to bardzo ważna różnica ponieważ debudowanie binarek na Twojej maszynie lokalnej jest proste. Problemy zawsze pojawiają się podczas debugowania buildów publicznych.

Najważniejszą rzeczą jaką powinnien wiedzieć każdy programista to fakt, że pliki PDB są tak samo ważne jak kod źródłowy. Tak... specjalnie to zdanie zostało pogrubione!
W jednym z artykułów natknąłem się na takie informacje: "Byłem w niezliczonych firmach, aby pomóc im przeprowadzić operację debuggowania na błędach kosztujących setki tysiecy dolarów ale nikt nie był wstanie wskazać lokalizacji plików PDB na serwerze produkcyjnym. Bez tych plików debuggowanie jest niemal niemożliwe. Dzięki wysiłkowi jednej z firm udało mi się odnaleźć problemy bez wspomnianych wyżej plików – można jednak było zaoszczędzić dużo pieniędzy jeżeli udałoby się odnaleźć te pliki w pierwszej kolejności".

John Cunningham, kierownik ds. Rozwoju wszystkich elementów diagnostycznych Visual Studio, na temat plików PDB powiedział: "Kochaj, trzymaj i chroń swoje pliki PDB”. Każdy z zespołów powinien przynajmiej utworzyć serwer symboli (Symbol Server). Możesz o nich przeczytać w dokumentacji dotyczącej narzędzi debuggowania. Postaram się opisać w największym skórcie o czym jest mowa w tej dokumentacji. Symbol Server przechowuje pliki PDB oraz binarki dla wszystkich Twoich publicznych buildów. Niezależnie od tego, który build powoduje awarie lub problem masz dokładnie dopasowany plik PDB, który jest dostępny przez degugger builda publicznego. Visual Studio oraz WinDBG wie jak uzyskać dostęp do serwera symboli a jeżeli binarka jest publicznego builda, debugger automatycznie otrzyma odpowiedni plik PDB.

Większość z Was będzie musiała wykonać jeden dodatkowy krok przed umieszczeniem plików PDB na serwerze symboli. Krok ten polega na uruchomieniu narzędzia Source Server na wszystkich swoich plikach PDB. Krok ten nazywany jest indeksowaniem źródeł. Indeksowanie zawiera polecenia sterujące wersją w celu pobrania dokładnego pliku źródłowego wykorzystywanego w danym buildzie publicznym. Dzięki temu, podczas debugowania publicznego nie musisz się martwić o znalezienie pliku źródłowego dla tej kompilacji. Jeżeli jesteś zespołem składającym się z dwóch osób możesz poradzić sobie bez tego serwera. Każdy większy zespół powinien zapoznać się z poniższym artykułem: Debugowanie za pomocą Symboli.

Dalsza cześć artykułu zakłada, że skonfigurowałeś indeksowanie serwera symboli (Symbol Server) oraz serwera żródłowego (Source Server). Dobrą informacją dla osób używających Team Foundation Server jest fakt, że build serwer będzie posiadał wbudowane zadanie dla indeksowania w ramach wykonywania build’a.

Jedyny problem wspomniany przez większe zespoły przy konfigurowaniu serwera symboli to informacja o zbyt dużym i za bardzo skomplikowanym projekcie. Raczej ciężko się spodziewać, żeby Twoje oprogramowanie było większe niż produkty Microsoftu... . Firma ta przechowuje na serwerze symboli informację o każdym buildzie. Oznacza to, że produkty takie jak Windows, Office, SQL czy gry znajdują się w centralnej lokalizacji.

Skupmy się teraz na omówieniu zawartości plików PDB oraz w jaki sposób debugger odnajduje te informacje. Rzeczywisty format pliku PDB jest ściśle strzeżoną tajemnicą ale firma Microsoft dostarcza API służące do zwracania danych dla debuggera. Oryginalny plik PDB języka C++ zawiera szereg informacji:

  • publiczne, prywatne i statyczne adresy funkcji;
  • globalne nazwy zmiennych i ich adresy;
  • parametry i lokalne nazwy zmiennych oraz informacje o ich lokalizacji na stosie;
  • typy danych składające się z klas, struktur oraz definicji;
  • FPO (Frame Pointer Omission), które są kluczem do natywnego stosu;
  • nazwy plików żródłowych i ich zawartość.

Pliki PDB dla .NET zawierają dwa rodzaje informacji: nazwę plików źródlowych i ich zawartość oraz nazwy zmiennych lokalnych. Wszystkie pozostałe informacje są zawarte w metadanych, nie ma więc potrzeby ich dublowania w plikach PDB.

Podczas ładowania modułu do przestrzeni adresowej debugger korzysta z dwóch informacji aby zlokalizować odpowiedni plik PDB. Pierwszym jest oczywiście nazwa pliku. Jeżeli zatem załadujesz test.dll, debugger szuka test.pdb. Bardzo istotną częścią tego procesu jest dokładne dopasowanie pliku PDB do naszej binarki. Odbywa się to za pomocą identyfikatora - GUID, który jest wbudowany do obydwu plików. Jeżeli ten identyfikator nie będzie pasował, nie będziesz w stanie wykonać debuggowania na poziomie kodu żródłowego.

Kompilator .NET oraz natywny łącznik umieszcza identifikator w każdym z plików. Ponieważ kompilacja tworzy identyfikator – musimy się na chwile zatrzymać i zadać sobie jedno pytanie. Jeżeli masz wczorajszy build i nie zapisałeś plików PDB - czy będziesz w stanie dokonać procesu debuggowania ponownie? Odpowiedź brzmi: NIE! Dlatego tak ważnym jest, aby zapisywać te pliki dla każdego builda. Uprzedzając pytanie, które zapewne narodziło się w Twojej głowie: Nie, nie ma sposobu na zmianę identyfiaktora.

Możesz jednak sprawdzić wartość identyfiaktora w swoim pliku binarnym. Za pomocą wiersza poleceń dołączonego do Visual Studio oraz polecenia: "DUMPBIN” możesz wyświetlić listę wszystkich przenośnych plików wykonywalnych (PE).

Istnieje wiele opcji wiersza polecenia dla DUMPBIN a ten, który pozwala nam na wyświetlenie identyfikatora to /HEADERS. W tym punkcie skupimy się na Debug Directories:

Debug Directories
Time Type Size RVA Pointer
——– —— ——– ——– ——–
4A03CA66 cv 4A 000025C4 7C4 Format: RSDS,
{4B46C704-B6DE-44B2-B8F5-A200A7E541B0}, 1,
C:\junk\stuff\HelloWorld\obj\Debug\HelloWorld.pdb

Teraz, kiedy wiemy jak debugger określa pasujące pliki PDB należy skupić się na sposobie odnajdywania tych plików. Ścieżkę tą możesz sprawdzić sam korzystając z okna Visual Studio Modules, a konkretnie kolumny Symbol File w trakcie przeprowadzania operacji debuggowania. Pierwszym miejscem jest lokalizacja w której znajdują się binarki. Jeżeli pliku PDB nie ma w tej lokalizacji, debugger skorzysta ze ścieżki zapisanej na sztywno w pliku PE – Portable Executable. Jeżeli spojrzysz na powyższe wyjście możesz zobaczyć pełną ścieżkę. Jeżeli jednak pliku PDB nie będzie w powyższych lokalizacjach a serwer symboli jest skonfigurowany, to właśnie w tym miejscu debugger będzie szukał plików – dokładnie w katalogu pamięci podręcznej. Ostatecznie, jeżeli również w tej lokalizacji plik nie zostanie odnaleziony, debugger przeszuka sam serwer. Taki porządek wyszukiwania powoduje, że Twoje lokalne oraz publiczne buildy nigdy nie są w konflikcie.

Sposób wyszukiwania plików będzie działał praktycznie dla każdego typu aplikacji jaką będziesz rozwijał. Nieco bardziej intersujące jest gdzie ładują się pliki PDB w aplikacjach .NET, które wymagają umieszczenia bibliotek w Global Assembly Cache (GAC). W tym punkcie chciałbym wymienić SharePoint oraz problemy, które możesz napotkać w częściach webowych pisanych aplikacji. Życie jest proste w przypadku prywatnych bulidów na maszynie lokalnej, ponieważ debugger będzie szukał plików w ścieżkach opisanych powyżej. Problemy zaczynają się pojawiać, kiedy potrzebujesz przeprowadzić test lub operację debuggowania prywatnego build’a na innej maszynie.

Często można spotkać się z poniższym tokiem postępowania. Po użyciu GACUTIL w celu umieszczenia danego assembly wewnątrz GAC jest otwarcie okna poleceń i przeszukania lokalizacji: C:\WINDOWS\ASSEMBLY\ w celu odnalezienia fizycznej lokalizacji na dysku. Obecnie assembly skompliowane dla każdego CPU znajduje się w poniższej lokalizacji: C:\Windows\assembly\GAC_MSIL\Example\1.0.0.0__682bc775ff82796a . Warto jednak mieć na uwadze, że w przyszłości ta lokalizacja może się zmienić.

W powyższym przykładzie możecie zobaczyć wersję danego assembly: 1.0.0.0 oraz wartość klucza publicznego: 682bc775ff82796a.

Jeżeli masz ochotę na takie przechodzenie po lokalizacji GAC warto wiedzieć, że jest lepszy sposób na przeprowdzenie takiej operacji. Tym sposobem jest DEVPATH - nie jest powszechnie znany. Idea polega na zmianie kilku ustawień w środowisku .NET, które następnie doda katalog jaki zdefiniujesz dla GAC. Dzięki temu debugggowanie będzie dużo łatwiejsze. Należy jedynie zdefiniować DEVPATH na maszynach deweloperskich ponieważ pliki przechowywane w określonym katalogu nie są sprawdzane pod względem wersji – tak jak ma to miejsce w prawdziwym GAC.

Przy okazji, jeżeli wyszukujesz informacji na temat DEVPATH w jakiejkolwiek przeglądarce internetowej, jednym z pierwszych artykułów na liście jest przestarzały wpisny na blogu Suzanne Cook mówiący, że Microsoft pozbył się DEVPATH. Nie jest to prawda. Wpis jest z roku 2003.

Aby używać DEVPATH w pierwszej kolejności należy utworzyć katalog z uprawnieniami odczytu dla każdego konta oraz ustawić uprawnienia odczytu przynajmniej dla Twojego konta deweloperskiego. Katalog ten może znajdować się w dowolnej lokalizacji. Drugim krokiem jest utworzenie zmiennej środowiskowej, DEVPATH, której wartością jest katalog utworzony przez Ciebie. Dokumentacja związana z DEVPATH nie jest przejrzysta, jednakże należy ustawić jej wartość przed przejściem do kolejnego kroku.

Należy jeszcze poinformować .NET, że korzystasz z DEVPATH. Trzeba dodać odpowiedni wpis w jednym z następujących plików: app.config, web.config lub machine.config:

<configuration> 
	<runtime> 
		<developmentMode developerInstallation="true" />
	</runtime> 
</configuration> 

Gdy włączysz tryb programowania, zobaczysz, że wystąpił problem albo z brakującą zmienną środowiskową DEVPATH dla procesu lub ścieżka, którą ustawiłeś nie istnieje, jeżeli Twoja aplikacja wyrzuca przy starcie błąd związany z COMException, której komunikat błędu jest niejasny: "Nieprawidłowa wartość rejestru”. Należy również zachować czujność, jeżeli chcesz używać DEVPATH w machine.config ponieważ wpływa to na każdy proces na komputerze.

Ostatni punkt ważny dla każdego programisty dotyczy sposobu w jaki informacje o pliku źródłowym są przechowywane w pliku PDB. W przypadku buildów publicznych na których są uruchamiane narzędzia do indeksowania zasobów, pamięć odpowiada za kontrolę wersji, która umożliwa pobranie pliku źródłowego do podanej pamięci podręcznej. Z kolei dla prywatnych buildów przechowywana jest pełna ścieżka do plików źródłowych, której kompilator użył do utworzenia pliku binarnego. Innymi słowy, jeżeli używasz pliku źródłowego mytest.cpp w katalogu C:/temporary w pliku PDB znajdziesz C:/temporary/mytest.cpp.

Zapewne nie wszystkie informacje zawarte w tym artykule są dla Ciebie nowe, starałem się jednak zebrać wszystko co istotne o plikach PDB w jednym miejscu. Mam jednak nadzieje, że dzięki temu artykułowi zrozumiesz lepiej co dzieję się w trakcie debuggowania oraz jak wygląda ten proces w rzeczywistości.