Strona główna.
Następny rozdział: Narzędzia do wykrywania wycieków pamięci w programie i do profilowania kodu.
Poprzedni rozdział: Jak działa odpluskwiacz? "Zwykłe" odpluskwiacze systemów uniksowych. Podstawy debugowania na przykładzie gdb.


Dlaczego programy, które chcemy porządnie odpluskwiać, powinniśmy skompilować z opcjami odpluskwiania? Opcje odpluskwiania w gcc. Formaty plików obiektowych i przygotowanych do odpluskwiania (stabs, DWARF).

Spis treści


Bibliografia

Powrót do góry strony


Formaty plików obiektowych i przygotowanych do odpluskwiania (stabs, DWARF).

O plikach obiektowych ogólnie

Prawie wszytkie systemy operacyjne korzystają z plików obiektowych do reprezentowania wykonywalnego kodu maszynowego. Co więcej zazwyczaj system operacyjny daje nam możliwość wykonywania kodu zapisanego w takich plikach. Pliki obiektowe są tworzone za pomocą kompilatorówlinkerów, a mogą z nich korzystać chociażby debugery. Żeby jedne narzędzia umiały wygenerować, a inne odpowiednio zinterpretować taki plik, potrzebne są standardy opisujące, jak powinien on być zbudowany.

Najprostszy przykład formatu plików obiektowych to .COM z MSDOS. Pliki w takim formacie miały zazwyczaj rozszerzenia .bin czy .com i zawierały praktycznie tylko kod maszynowy programu. Ładując taki plik do pamięci, wystarczy go przeczytać i przekazać sterowanie do pierwszego adresu z tego pliku. Bardziej skomplikowane formaty to, np. a.out używany w systemie BSD UNIX, ELF z Systemu V, rozszerzony format COFF (ECOFF) do linkowania i wykonywalny PE stosowane przez 32-bitowe Windows'y czy OMF używany przez Windows'a zanim powstał COFF.

Plik obiektowy powinien zawierać przynajmniej część z następujących informacji:

Dokładną zawartość danego pliku obiektowego możemy obejrzeć korzystając z programu objdump (patrz man objdump).

Powrót do góry strony


Zarządzanie symbolami z pliku obiektowego

Linkery obsługują różne rodzaje symboli, które określają np. powiązania między modułami (każdy moduł zawiera własną tablicę symboli). Rozróżniamy następujące rodzaje symboli:

Powrót do góry strony


Przechowywanie informacji do debugowania

Dzięki informacjom zgromadzonym w fazie kompilacji i linkowania programu, programista może odpluskwiać plik obiektowy, odwołując się do kodu źródłowego funkcji i nazw zmiennych, ustawiać punkty kontrolne (breakpoints) i wykonywać program instrukcja po instrukcji. Kompilatory wspomagają ten proces, mapując numery linii z kodu źródłowego w odpowiednie adresy w kodzie maszynowym (podobnie dla opisów funkcji, zmiennych, typów i struktur użytych w programie).

Kompilatory UNIXowe posługują się dwoma różnymi formatami wspomagającymi debugowanie: stab (skrót od `symbol table') używany w a.out, COFFELF (poza Systemem V) i DWARF zdefiniowany dla plików ELF Systemu V. Microsoft również stworzył swoje własne formaty dla debugera Codeview, najnowszy z nich to CV4.

Powrót do góry strony


Informacje o numerach linii

Zamapowanie numerów linii z kodu źródłowego do pliku obiektowego pozwala odpluskwiaczowi umieścić punkt kontrolny (breakpoint) w odpowiednim miejscu w kodzie oraz umożliwia śledzenie stosu wywołańraportowanie błędów z odniesieniami do tego kodu. Oczywiście trzeba brać pod uwagę to, czy kod wykonywalny nie jest przypadkiem zoptymalizowany przez kompilator, bo wówczas odpowiedniość między kodem i plikiem obiektowym nie zawsze jest zachowana i można się natknąć na przykrą niespodziankę (częste są np. problemy ze zmiennymi lokalnymi i ich wartościami).

Dla każdej linii w kodzie źródłowym, dla której powstał jakiś kod maszynowy, kompilator produkuje dane składające się z numeru linii i  adresu początku kodu maszynowego, który jej odpowiada (tzw. line number entry). Jeśli adres znajduje się między dwoma takimi wpisami, to odpluskwiacz przypisuje ten adres niższemu numerowi linii. Numery linii obowiązują tylko w zakresie pliku źródłowego o danej nazwie, ewentualnie dodatkowo nazwie pliku dołączonego np. dyrektywą #include. Niektóre formaty radzą sobie z tym w ten sposób, że tworzą listę plików i dodają do każdej pojedynczej danej o numerze linii dodatkowo indeks pliku. Inne dodają znaczniki "begin include""end include" do listy numerów linii i w ten sposób jawnie utrzymują stos numerów linii.

Kiedy optymalizacja wykonana przez kompilator powoduje, że kod wygenerowany z jednej linii staje się nieciągły w pliku obiektowym, niektóre formaty (np. DWARF) pozwalają zamapować każdy bajt wygenerowanego kodu maszynowego z powrotem w jedną linię źródłową (w ten sposób rozmiar pliku znacznie rośnie). Inne formaty (np. stabs) podają w takich przypadkach tylko przybliżone położenie.

Powrót do góry strony


Informacje o symbolach i zmiennych

Kompilator musi zachować nazwy, typy i położenie każdej zmiennej w programie. Informacja do debugowania w tym przypadku jest dość skomplikowana, bo musimy zakodować nie tylko typ, ale także często strukturę tego typu, żeby odpluskwiacz mógł poprawnie formatować poszczególne pola w tej strukturze.

Informacja o takich symbolach jest przechowywana w formie drzewa. Na górnym poziomie dla każdego pliku mamy listę typów, zmiennych i funkcji zdefiniowanych w nim globalnie. W poddrzewach znajdują się pola struktur, zmienne lokalne dla funkcji, itd. Dla funkcji mamy też dodatkowo znaczniki "begin block""end block" odnoszące się do numerów linii, żeby odpluskwiacz mógł określić, które zmienne są widoczne w danym miejscu w programie.

Najtrudniejszą cześcią jest przechowywanie danych o położeniu. Położenie zmiennej statycznej się nie zmienia, ale zmienna lokalna może znajdować się na stosie, w rejestrze, w zoptymalizowanym kodzie i jest przemieszczana z jednego miejsca funkcji w inne. Dodatkowo w większości architektur standardowa metoda wywoływania funkcji utrzymuje ciąg zachowanych wskaźników do wierzchołka stosu i wskaźników do rekordów aktywacji (tzw. wskaźnik ramki albo frame pointer) odpowiadających kolejnym zagnieżdżonym wywołaniom funkcji. Funkcje, które są na końcu takiego łańcucha wywołań i nie alokują żadnych lokalnych zmiennych na stosie, często są optymalizowane w ten sposób, że nie tworzy się dla nich wskaźnika ramki. Odpluskwiacz musi o tym wiedzieć, żeby poprawnie zinterpretować stos wywołań i znaleźć wszystkie zmienne lokalne.

Powrót do góry strony


Kilka praktycznych uwag

Linkery często wykrywają i usuwają nadmiarowe kopie tej samej informacji do debugowania. W C i C++ programy zazwyczaj używają zestawu plików nagłówkowych, które definiują typy i deklarują funkcje i jeśli kilka plików źródłowych korzysta z tych samych nagłówków, w wynikowym pliku obiektowym powstałym po zlinkowaniu ta informacja pojawia się w wielu kopiach.

Kompilatory zachowują informacje do debugowania dla każdego pliku nagłówkowego dołączonego do każdego pliku źródłowego. Oznacza to, że jeśli pewien plik nagłówkowy jest dołączony do 20 plików źródłowych, które są w jednym projekcie, linker dostanie 20 identycznych kopii informacji do debugowania dla tego pliku. Odpluskwiacze dobrze radzą sobie z tymi nadmiarowymi danymi, ale rozmiar takiego pliku może być bardzo duży. Linker może usunąć nadmiar danych, żeby przyspieszyć działanie debugera i oszczędzać miejsce. Czasami kompilator umieszcza informacje dla odpluskwiacza w oddzielnych plikach przeznaczonych specjalnie dla niego tak, że linker w ogóle nie musi się nimi zajmować.

Kiedy dane do debugowania są umieszczane w pliku obiektowym, czasami są pomieszane z innymi symbolami w jednej dużej tablicy symboli, a czasem są trzymane oddzielnie. Systemy UNIXowe dodawały stopniowo dane do debugowania do plików, więc wszystko znajduje się w jednej dużej tablicy symboli (tak jest w formacie stabs). Inne formaty, jak np. ECOFF Microsoft oddzielają jedne symbole od drugich.

Jak już pisałam wcześniej, czasami informacje do debugowania trafiają do oddzielnego pliku, a czasami są trzymane razem z resztą pliku obiektowego. Zaletą drugiego podejścia jest uproszczenie procesu budowania programu, bo mamy wszystko dostępne od razu w jednym miejscu. Wadą jest to, że plik wykonywalny może być bardzo duży. Poza tym, jeśli oddzielimy informacje do debugowania, łatwo będzie opublikować końcową wersję programu bez tych informacji. Dzięki temu plik wykonywalny jest rozsądnych rozmiarów, zniechęcamy inne firmy do podglądania naszych rozwiązań ;), a my mamy dalej pliki potrzebne do znajdowania błędów w opublikowanym kodzie. W UNIXie mamy polecenie "strip", które usuwa dane do debugowania z pliku obiektowego, nie zmieniając reszty kodu. Dzięki temu możemy opublikować okrojoną (stripped), a zachować pełną wersję tego samego pliku. Pomimo że te dwa pliki będą się od siebie różniły, będą korzystały z tych samych symboli i zrzut pamięci wykonany okrojoną wersją programu będzie mógł być analizowany przez odpluskwiacz podłączony do pełnej wersji.

Powrót do góry strony


stabs (Symbol Table)

Informacja do debugowania w tym formacie znajduje się w dyrektywach asemblera, które nazywamy dyrektywani stab i które są przemieszane z generowanym kodem. Stabs jest natywnym formatem do debugowania dla plików a.out i XCOFF, ale można z niego korzystać też w plikach ELF, w których niezbędne informacje są przechowywane w sekcjach .stab.stabstr.

Są 3 podstawowe rodzaje dyrektyw stab: .stabs (string), .stabn (number), .stabd (dot).

Ogólny format tych dyrektyw to:


		.stabs "string",type,other,desc,value
		.stabn type,other,desc,value
		.stabd type,other,desc

Rozważmy bardzo prosty przykład (test0.c)

		void main()
		{
			printf("Hello world");
		}

Kompilując go z opcją -g i oglądając powstały plik .s (kod maszynowy wygenerowany przez kompilator) dostaniemy coś takiego:

    		
		1  gcc2_compiled.:
		2  .stabs "/home/heathcliff/stabs/",100,0,0,Ltext0
		3  .stabs "test0.c",100,0,0,Ltext0
		4  .text
		5  Ltext0:
		6  .stabs "int:t1=r1;-2147483648;2147483647;",128,0,0,0 //przykładowa dyrektywa .stabs (opisje się nią np. zmienne)
		7  .stabs "char:t2=r2;0;127;",128,0,0,0
		8  .stabs "long int:t3=r1;-2147483648;2147483647;",128,0,0,0
		9  .stabs "unsigned int:t4=r1;0;-1;",128,0,0,0
		10 .stabs "long unsigned int:t5=r1;0;-1;",128,0,0,0
		11 .stabs "short int:t6=r1;-32768;32767;",128,0,0,0
		12 .stabs "long long int:t7=r1;0;-1;",128,0,0,0
		13 .stabs "short unsigned int:t8=r1;0;65535;",128,0,0,0
		14 .stabs "long long unsigned int:t9=r1;0;-1;",128,0,0,0
		15 .stabs "signed char:t10=r1;-128;127;",128,0,0,0
		16 .stabs "unsigned char:t11=r1;0;255;",128,0,0,0
		17 .stabs "float:t12=r1;4;0;",128,0,0,0
		18 .stabs "double:t13=r1;8;0;",128,0,0,0
		19 .stabs "long double:t14=r1;8;0;",128,0,0,0
		20 .stabs "void:t15=15",128,0,0,0
		21      .align 4
		22 LC0:
		23      .ascii "Hello, world!\12\0"
		24      .align 4
		25      .global _main
		26      .proc 1
		27 _main:
		28 .stabn 68,0,4,LM1
		29 LM1:
		30      !#PROLOGUE# 0
		31      save %sp,-136,%sp
		32      !#PROLOGUE# 1
		33      call ___main,0
		34      nop
		35 .stabn 68,0,5,LM2 //przykładowa dyrektywa .stabn (opisuje się nią m.in. etykiety)
		36 LM2:
		37 LBB2:
		38      sethi %hi(LC0),%o1
		39      or %o1,%lo(LC0),%o0
		40      call _printf,0
		41      nop
		42 .stabn 68,0,6,LM3
		43 LM3:
		44 LBE2:
		45 .stabn 68,0,6,LM4
		46 LM4:
		47 L1:
		48      ret
		49      restore
		50 .stabs "main:F1",36,0,0,_main
		51 .stabn 192,0,0,LBB2
		52 .stabn 224,0,0,LBE

Dodatkowe infornacje można znaleźćć na stronie: http://www.informatik.uni-frankfurt.de/doc/texi/stabs_toc.html

Powrót do góry strony


DWARF (Debugging With Attributed Record Formats)

DWARF jest nowszy niż stabs i związany przede wszystkim z formatem ELF, choć był używany też z innymi formatami plików obiektowych. Informacje potrzebne do debugowania w tym formacie są umieszczane w sekcji .debug pliku obiektowego (przynajmniej w wersji 1.1) i składają się ze specjalnych rekordów, które reprezentują kod źródłowy programu. Każda taka dana jest opisywana przez jednoznaczny identyfikator (tag) i zawiera zbiór atrybutów. Identyfikator mówi, do jakiej klasy konkretne dane należą, a atrybuty zapisują pozostałe informacje.

Autorzy formatu twierdzą, że DWARF dzięki blokowej strukturze jest łatwo rozszerzalny, bo można dodawać opisy nowych obiektów lub rozszerzać istniejące. Stabs jest pod tym względem bardziej restrykcyjny, ponieważ opiera się na predefiniowanych dyrektywach. DWARF może też opisywać bardziej skomplikowane struktury niż stabs, np. nieciągłe zakresy widoczności zmiennych czy strukturę stosu.

Dodatkowe informacje można znaleźć na stronach:

Powrót do góry strony


XCOFF (eXtended Common Object File Format)

Szczegółowe informacje o tym formacie znajdują się na stronie: http://www16.boulder.ibm.com/pseries/en_US/files/aixfiles/XCOFF.htm

Powrót do góry strony


Opcje odpluskwiania w gcc.

gcc oferuje ogromną kolekcję przydatnych opcji. Tutaj wspomnę tylko o najważniejszych z nich, które rzeczywiście często się przydają i znacznie ułatwiają debugowanie. Spis wszystkich opcji można znaleźć na stronach: http://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html i http://developer.apple.com/documentation/DeveloperTools/gcc-4.0.1/gcc/Debugging-Options.html

Powrót do góry strony


© Magdalena Dukielska