Strona główna.
Następny rozdział: Techniki odpluskwiania jądra: ksymoops, kgdb, UML.
Poprzedni rozdział: Opcje odpluskwiania w gcc. Formaty plików obiektowych i przygotowanych do odpluskwiania.


Narzędzia do wykrywania wycieków pamięci w programie i do profilowania kodu.

Spis treści


Bibliografia

Powrót do góry strony


Wykrywanie wycieków pamięci.

Dynamiczna alokacja pamięci wydaje się prosta - kiedy potrzebujemy pamięci alokujemy ją, używając malloc()'a albo podobnej funkcji, a kiedy nie jest już dłużej potrzebna, zwalniamy ją za pomocą free(). Mimo to, problemy z zarządzaniem pamięcią są najczęstszymi błędami, ponieważ zazwyczaj ich nie widać, kiedy uruchamiamy program.

Programując w C mamy dużą kontrolę nad dynamiczną alokacją pamięci. Ta wolność może jednak prowadzić do poważnych problemów w zarządzaniu pamięcią i w efekcie doprowadzić do tego, że w programie wystąpi błąd krytyczny. Czasami zdarza się, że problem z pamięcią pojawi się dopiero po kilku tysiącach czy milionach wywołań jakiejś funkcji. Błąd może równie dobrze ujawnić się dopiero na maszynie innej niż ta, na której program został napisany, np. w wyniku trochę innego rozmieszczenia obiektów w pamięci.

Wycieki pamięci (memory leaks; sytuacje, w których pamięć zaalokowana za pomocą malloc() nie jest zwalniana odpowiednim wywołaniem free()) oraz nielegalne zapisy/odczyty (buffer overwrite, illegal read/write; np. pisanie poza obszarem pamięci zaalokowanym na tablicę) to typowe problemy, które mogą być bardzo trudne do wykrycia. Pierwszym symptomem wycieku pamięci jest to, że pamięć przydzielona programowi stale rośnie wraz z czasem jego działania. W rezultacie, kiedy zużycie pamięci zbliża się do limitu wyznaczonego przez system operacyjny, program się wywala, albo w najgorszym przypadku zawiesza się i powoduje awarię systemu operacyjnego. Nielegalny odczyt/zapis polega na tym, że program próbuje coś odczytać/zapisać do obszaru pamięci, który do niego nie należy. W niektórych systemach program zostanie natychmiast zakończony z błędem Segmentation fault, ale nie zawsze to się zdarza.

Poniżej opisuję kilka narzędzi, które znacznie upraszczają wykrywanie i lokalizację tego typu problemów.

Powrót do góry strony


MEMWATCH

MEMWATCH jest open-source'owym narzędziem do wykrywania błędów w zarządzaniu pamięcią w C. Dodając plik nagłówkowy "memwatch.h" do kodu i definiując makra MEMWATCHMW_STDIO w trakcie kompilacji za pomocą gcc, można śledzić wycieki pamięci w programie. MEMWATCH wspiera ANSI C, tworzy plik z logiem ze swojego działania i wykrywa podwójne czy błędne zwolnienia pamięci, niezwolnioną pamięć, nadpisanie buforów, itp.

Narzędzie działa w ten sposób, że zastępuje wszystkie wołania funkcji alokujących pamięć swoimi funkcjami, które pamiętają, jakie alokacje były wykonywane.

Zdefiniowane są też przydatne makra, takie jak np. ASSERT (analogiczne do assert z C++), które pozwalają nam sprawdzać poprawność różnych warunków w trakcie działania programu. Jeśli warunek nie jest spełniony, program kończy się z błędem (odpowiedni zapis pojawia się w logu).

Wiele problemów z pamięcią jest spowodowanych tym, że używamy niezainicjalizowanych wskaźników albo wskaźników, które są wprawdzie zainicjalizowane, ale wskazują na kawałek pamięci, który już nie powinien do nas należeć. Najlepszą metodą na unikanie tego typu błędów jest inicjalizowanie wskaźników na NULL i ustawianie ich na NULL po wywołaniu free. MEMWATCH stosuje podobną technikę, wpisując w obszar nowo zaalokowanej pamięci 0xFE a w obszar zwolniony 0xFD.

Warto spojrzeć na plik test.c, który dostajemy razem ze źródłami programu.

Przykład 1. (test1.c)

	
	1	#include <stdlib.h>
	2	#include <stdio.h>
	3	#include "memwatch.h"
	4
	5	int main(void)
	6	{
	7  		char *ptr1;
  	8		char *ptr2;
	9
	10		ptr1 = malloc(512);
	11		ptr2 = malloc(512);
	12	
	13		ptr2 = ptr1; //gubimy wskaźnik na drugi kawałek pamięci 
	14		free(ptr2);
	15		free(ptr1);
	16
	17	}

Ten kawałek kodu alokuje dwa bloki pamięci po 512B i ustawia wskaźnik z drugiego bloku na pierwszy. W rezultacie, adres drugiego bloku jest gubiony i mamy wyciek pamięci.

Kompilujemy np. za pomocą takiego polecenia:

		
	gcc -DMEMWATCH -DMW_STDIO test1.c memwatch.c -o test1

Kiedy odpalimy program test1, dostaniemy raport o wycieku pamięci. Plik logu (zapisywany w trakcie działania naszego programu do pliku memwatch.log) dla programu test1.c wygląda tak:

	
	
	MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh
	
	...
	double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14) //podwójne zwolnienie pamięci
	...
	unfreed: <2> test1.c(11), 512 bytes at 0x80519e4 //niezwolniona pamięć
	{FE FE FE FE FE FE FE FE FE FE FE FE ..............}
	
	Memory usage statistics (global):
	  N)umber of allocations made: 	2
	  L)argest memory usage : 	1024
	  T)otal of all alloc() calls: 	1024
	  U)nfreed bytes totals : 	512

MEMWATCH wskazuje nam linię, w której występuje problem. Jeśli zwolnimy już wcześniej zwolniony wskaźnik, to dostaniemy o tym informację. To samo dotyczy niezwalnianej pamięci. Ostatnia sekcja w logu to statystyki, które pokazują, ile pamięci straciliśmy, ile było użyte i ile w sumie zaalokowaliśmy.

Program można ściągnąć z http://www.linkdata.se/sourcecode.html

Powrót do góry strony


Testy

Pliki, na których testowałam program i kilka innych plików, które ściagamy razem z programem i które warto prześledzić:

Powrót do góry strony


YAMD (Yet Another Malloc Debugger)

Pakiet YAMD znajduje problemy z dynamiczną alokacją pamięciCC++. Korzysta on z mechanizmu stronnicowania, żeby narzucić granice na alokowane bloki pamięci. Malloc i tym podobne funkcje są emulowane na bardzo niskim poziomie, dzięki czemu sprawdzane są też wywołania takich funkcji bibliotecznych, jak strdup. Nie potrzebujemy też żadnych zmian w kodzie źródłowym programu.

Można go ściągnąć z  http://www.cs.hmc.edu/~nate/yamd/. Po ściągnięciu źródeł wykonujemy polecenie make, żeby skompilować program, a potem make install, żeby go zainstalować. Polecam przejrzenie katalogu tests, który zawiera pliki demonstrujące działanie programu dla typowych blędów.

Spróbujmy użyć ten program na poprzednim przykładzie (test1.c). Usuwamy wpis #include "memwatch.h" i kompilujemy następująco:


	gcc -g test1.c -o test1

Otrzymujemy coś takiego:

		
	YAMD version 0.32
	Executable: /usr/src/test/yamd-0.32/test1
	...
	INFO: Normal allocation of this block
	Address 0x40025e00, size 512
	...
	INFO: Normal allocation of this block
	Address 0x40028e00, size 512
	...
	INFO: Normal deallocation of this block
	Address 0x40025e00, size 512
	...
	ERROR: Multiple freeing At //podwójne zwolnienie pamięci
	free of pointer already freed
	Address 0x40025e00, size 512
	...
	WARNING: Memory leak //wyciek pamięci
	Address 0x40028e00, size 512
	WARNING: Total memory leaks:
	1 unfreed allocations totaling 512 bytes
	
	*** Finished at Tue ... 10:07:15 2006
	Allocated a grand total of 1024 bytes 2 allocations
	Average of 512 bytes per allocation
	Max bytes allocated at one time: 1024
	24 K alloced internally / 12 K mapped now / 8 K max
	Virtual program size is 1416 K
	End.

YAMD pokazuje, że już wcześniej zwolniliśmy pamięć i że jest wyciek pamięci.

Zobaczmy teraz kolejny przykład.

Przykład 2. (test2.c)

	
	
	1	#include <stdlib.h>
	2	#include <stdio.h>
	3
	4	int main(void)
	5	{
	6	  char *ptr1;
	7	  char *ptr2;
	8	  char *chptr;
	9	  int i = 1;
	10	  ptr1 = malloc(512);
	11	  ptr2 = malloc(512);
	12	  chptr = (char *)malloc(512);
	13	  for (i; i <= 512; i++) { 
	14	    chptr[i] = 'S'; //dla i=512 mamy nadpisanie bufora
	15	  }	
	16	  ptr2 = ptr1;
	17	  free(ptr2);
	18	  free(ptr1);
	19	  free(chptr);
	10	}

Używamy następującej komendy, żeby odpalić YAMD'a:

    ./run-yamd ./test2 

Wynik działania jest wypisany niżej. YAMD mówi, że w pętli wychodzimy poza zakres tablicy, czyli mamy klasyczny przykład nielegalnego zapisu.

	
	Running ./test2
	Temp output to /tmp/yamd-out.1243
	*********
	./run-yamd: line 101: 1248 Segmentation fault (core dumped)
	YAMD version 0.32
	Starting run: ./test2
	Executable: ./test2
	Virtual program size is 1380 K
	...
	INFO: Normal allocation of this block
	Address 0x40025e00, size 512
	...
	INFO: Normal allocation of this block
	Address 0x40028e00, size 512
	...
	INFO: Normal allocation of this block
	Address 0x4002be00, size 512
	ERROR: Crash //w tym miejscu powstaje Segmentation fault
	...
	Tried to write address 0x4002c000 //próba nielegalnego zapisu
	Seems to be part of this block:
	Address 0x4002be00, size 512
	...
	Address in question is at offset 512 (out of bounds)
	Will dump core after checking heap.
	Done.

Powrót do góry strony


Testy

Ściągnęłam go i przetestowałam. Polecam podobnie jak poprzedni program:)

Pliki, na których testowałam i wyniki testów:

MEMWATCH i YAMD to dwa proste i użyteczne narzędzia, które wymagają innego podejścia. W przypadku MEMWATCH musimy dodać do kodu plik memwatch.h i włączyć dwie flagi podczas kompilacji. YAMD wymaga jedynie opcji -g podczas linkowania. Kolejne narzędzie jest nieco bardziej zaawansowane.

Powrót do góry strony


Valgrind

Valgrind

Rysunek: Logo programu Valgrind (Źródło: http://valgrind.org/)

Valgrind jest debugerem powszechnie używanym przez ludzi, którzy rozwijają aplikacje dla Linuxa. Jeśli chodzi o wykrywanie problemów z zarządzaniem pamięcią, jest to bardzo silne narzędzie.

Żeby go zainstalować, ściągamy źródła ze strony http://valgrind.org/, przechodzimy do odpowiedniego katalogu i wykonujemy:


	# make
	# make check
	# make install

(albo poldek -i valgrind)

Przykładowy wynik działania tego programu może wyglądać tak:


	# valgrind du -x -s
	.
	==29404==  Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd
	==29404==    at 0xFFB9964: malloc (vg_replace_malloc.c:130)
	==29404==    by 0xFEE1AD0: strdup (in /lib/tls/libc.so.6)
	==29404==    by 0xFE94D30: setlocale (in /lib/tls/libc.so.6)
	==29404==    by 0x10001414: main (in /usr/bin/du)

==29404== to PID procesu. Informacja 'Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd' mówi nam, że nie mamy pamięci za końcem tablicy o rozmiarze 12B. Kolejne linie mówią, że alokujemy pamięć w linii 130 w  pliku vg_replace_malloc.c za pomocą funkcji strdup(), która była wywołana w funkcji setlocale() z biblioteki libc.so.6; z kolei setlocale() było wywołane przez funkcję main().

Powrót do góry strony


Niezainicjalizowana pamięć.

Jeden z najczestszych błędów występuje, gdy program chce skorzystać z pamięci, która nie była zainicjalizowana. Może do tego dojść w wyniku użycia:

Poniższy przykład (test3.c) używa niezainicjalizowanej tablicy:


      2 int main() {
      3         int i[5];
      4 
      5         if (i[0] == 0)
      6                 i[1]=1;
      7         return 0;
      8 }

W tym przykładzie tablica int'ów jest niezainicjalizowana, więc i[0] zawiera losową liczbę. Użycie tej wartości jako warunku w if'ie daje nieprzewidywalny wynik. Taka sytuacja może być łatwo wyłapana przez Valgrind. Odpalając go na tym kodzie, dostajemy:


	# gcc -g -o test3 test3.c
	# valgrind ./test3
	.
	.
	==31363== 
	==31363== Conditional jump or move depends on uninitialised value(s) //korzystamy z losowej wartości
	==31363==    at 0x1000041C: main (test3.c:5)
	==31363== 
	==31363== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
	==31363== malloc/free: in use at exit: 0 bytes in 0 blocks.
	==31363== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
	==31363== For counts of detected errors, rerun with: -v
	==31363== No malloc'd blocks -- no leaks are possible.

Powrót do góry strony


Wyciek pamięci

Wypróbujmy to narzędzie na naszym wcześniejszym przykładzie (test1.c):


      1 int main(void)
      2 {
      3         char *ptr1;
      4         char *ptr2;
      5 
      6         ptr1 = (char *) malloc(512);
      7         ptr2 = (char *) malloc(512);
      8 
      9         ptr1=ptr2;
      10 
      11        free(ptr1);
      12        free(ptr2);
      13 }

Wynik działania:


	# gcc -g -o test1 test1.c
	# valgrind ./test1
	.
	.
	==31468== Invalid free() / delete / delete[]
	==31468==    at 0xFFB9FF0: free (vg_replace_malloc.c:152)
	==31468==    by 0x100004B0: main (test1.c:12) //błąd w linii 12
	==31468== Address 0x11899258 is 0 bytes inside a block of size 512 free'd
	==31468==    at 0xFFB9FF0: free (vg_replace_malloc.c:152)
	==31468==    by 0x100004A4: main (test1.c:11)
	==31468== 
	==31468== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
	==31468== malloc/free: in use at exit: 512 bytes in 1 blocks.
	==31468== malloc/free: 2 allocs, 2 frees, 1024 bytes allocated.
	==31468== For counts of detected errors, rerun with: -v
	==31468== searching for pointers to 1 not-freed blocks.
	==31468== checked 167936 bytes.
	==31468== 
	==31468== LEAK SUMMARY:
	==31468==    definitely lost: 512 bytes in 1 blocks. //straciliśmy w sumie 512B
	==31468==      possibly lost: 0 bytes in 0 blocks.
	==31468==    still reachable: 0 bytes in 0 blocks.
	==31468==         suppressed: 0 bytes in 0 blocks.
	==31468== Use --leak-check=full to see details of leaked memory.

Powrót do góry strony


Nielegalny zapis/odczyt
Przykład 4. (test4.c)


      1 int main() {
      2         int i, *iw, *ir;
      3 
      4         iw = (int *)malloc(10*sizeof(int));
      5         ir = (int *)malloc(11*sizeof(int));
      6 
      7 
      8         for (i=0; i<11; i++)
      9                 iw[i] = i;
      10 
      11        for (i=0; i<11; i++)
      12                 ir[i] = iw[i];
      13 
      14        free(iw);
      15        free(ir);
      16 } 

W tym przykładzie dostęp do iw[10] jest nielegalny, bo tablica ma elementy od 0 do 9. Valdrind wyprodukuje dla tego kodu następujące informacje:


	# gcc -g -o test4 test4.c
	# valgrind ./test4
	.
	.
	==31522== Invalid write of size 4 //zapisujemy int'a poza zakresem tablicy
	==31522==    at 0x100004C0: main (test4.c:9)
	==31522==  Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
	==31522==    at 0xFFB9964: malloc (vg_replace_malloc.c:130)
	==31522==    by 0x10000474: main (test4.c:4)
	==31522== 
	==31522== Invalid read of size 4 //czytamy int'a poza zakresem tablicy
	==31522==    at 0x1000050C: main (test4.c:12)
	==31522==  Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
	==31522==    at 0xFFB9964: malloc (vg_replace_malloc.c:130)
	==31522==    by 0x10000474: main (test4.c:4)
	==31522== 
	==31522== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 7 from 1)
	==31522== malloc/free: in use at exit: 0 bytes in 0 blocks.
	==31522== malloc/free: 2 allocs, 2 frees, 84 bytes allocated.
	==31522== For counts of detected errors, rerun with: -v
	==31522== No malloc'd blocks -- no leaks are possible.

Czyli wykryty został nielegalny zapis 4B w linii 9 i nielegalny odczyt 4B w linii 12.

Powrót do góry strony


Ogólnie

W powyższych przykładach Valgrind był użyty z domyślnymi opcjami, przez co skorzystaliśmy tylko z jednego z dostępnych narzędzi (Memcheck). Jego możliwości są jednak znacznie większe, ponieważ jest to cały zestaw narzędzi do debugowania i do profilowania programów dla Linuxa.

Dostępne narzędzia to:

Powrót do góry strony


Testy

Instalacja na PLD jest bezproblemowa i to zarówno poldkiem, jak i ze źródeł. Na Virtual PC nie udało się go jednak odpalić, bo narzekał na to, że ma za mało pamięci. Za to na Fedorze (FC4) jest domyślnie zainstalowany i działa nawet pod VMware.

Pliki, na których testowałam i wyniki testów:

Powrót do góry strony


Electric Fence

Pakiet z programem można ściągnąć z http://perens.com/FreeSoftware/ElectricFence/, choć zawiera go wiele dystrybucji Linuxa (np. Debian, na standardowym pld też jest - patrz man efence).

Jest to biblioteka do debugowania malloc()'a. Alokuje ona chronioną stronę pamięci zaraz po zaalokowanym fragmencie pamięci. Jeśli w którymś miejscu np. wykroczymy poza zakres tablicy, program natychmiast zakończy się z błędem ochrony pamięci, czyli nastąpi Segmentation fault. Łącząc Electric Fence z gdb albo dowolnym innym debuggerem, można łatwo zidentyfikować, w którym miejscu kodu doszło do dostępu do chronionej pamięci. W podobny sposób pamięć zwalniana przez free() staje się niedostępna tak, że każda instrukcja, która się do niej odwoła, zakończy program z błędem Segmentation fault. Inną cechą pakietu jest wykrywanie wycieków pamięci.

Aby skorzystać z tej biblioteki wystarczy zlinkować nasz program z libefence.a (przy standardowej konfiguracji wystarczy dodać -lefence jako opcję dla linkera). Pozwoli to wykryć większość błędów nadpisania bufora, choć, aby mieć pewność, że znaleźliśmy wszystkie, trzeba zastosować bardziej skomplikowane techniki. Najlepiej od razu uruchomić program w debugerze, a nie czekać, aż wystąpi błąd i debugować dopiero zrzut pamięci, bo Electric Fence może wygenerować bardzo duży plik core tak, że samo jego zrzucenie może trwać kilka minut.

Można łatwo przetestować możliwości Electric Fence na poprzednich przykładach, np. na test1.c

Kompilujemy i odpalamy:


	$ gcc -g -o test1 test1.c -lefence
	$ ./test1

wynik jest taki:


	ElectricFence Aborting: free(41fc0e00): address not from malloc(). //tu było błędne free()

Być może to niewiele nam mówi, ale możemy odpalić gdb, np. $ gdb test1 i śledząc program krok po kroku, znaleźć źródło problemów.

Powrót do góry strony


Testy

Jest domyślnie zainstalowany na labowym PLD i działa bez problemu pod Virtual PC. Trochę mniej wygodny niż np. yamd, bo, żeby go skutecznie użyć, musimy odpalić korzystający z niego program w gdb (zrobiłam to i można skutecznie odnaleźć linię kodu, która stwarza problemy).

Pliki, na których testowałam i wyniki testu:

Powrót do góry strony


"Bottlenecks occur in surprising places, so don't try to
second guess and put in a speed hack until you've
proven that's where the bottleneck is.
- Rob Pike

Profilowanie kodu.

Profilowanie kodu to określanie, jak często poszczególne kawałki kodu są wykonywane. Wiedząc, jak długo i ile razy fragment kodu jest wykorzystywany, możemy skuteczniej stwierdzać, czy warto go optymalizować. Zatem profilowanie kodu polega na mierzeniu różnych parametrów wykonania fragmentów programu po to, aby zrozumieć, które funkcje zajmują najwięcej czasu i są najczęściej wykonywane. Właściwe użycie narzędzi do profilowania pozwala m.in. odpowiedzieć na następujące pytania:

Często okazuje się też, że niektóre fragmenty programu wykonują się dużo częściej lub rzadziej (ew. wcale) niż się spodziewano, co może wskazywać na błędy.

Powrót do góry strony


gprof

gprof, dostępny w praktycznie każdej dystrybucji Linuxa, jest jednym z najpopularniejszych programów służących do profilowania.

Jeśli chcemy przyspieszyć wolny program, możemy go skompilować zwykłym gcc z opcją -pg, czyli z opcją profilowania. Kiedy program będzie działał, zapisany zostanie plik gmon.out, który opisuje, ile czasu zostało spędzone w każdej funkcji i na każdej linii kodu.

Program gprof służy do analizowania tego pliku. Dzięki niemu dowiemy się, na czym program spędził najwięcej czasu i będziemy mogli skoncentrować się na optymalizowaniu tych fragmentów.

Przykład użycia:

	
	$ cc program.c -pg -o program -lm
	$ program
	$ gprof program > program_gprof.txt 

Powrót do góry strony


Jak to działa?

Profiler używa informacji zgromadzonych w trakcie wykonania programu i może być użyty dla programów zbyt skomplikowanych do ręcznej analizy. Oczywiście sposób uruchomienia programu ma wpływ na dane zgromadzone w jego profilu. Jeśli nie wykorzystamy jakiejś funkcji w trakcie zbierania danych, to nie dostaniemy o niej żadnych informacji.

Profilowanie składa się z kilku kroków:

Wynikiem analizy jest plik zawierający dwie tabele: tzw. płaski profil (flat profile) i graf wywołań (call graph). Płaski profil mówi, ile czasu program spędził w każdej funkcji i ile razy ta funkcja była wołana. Graf wywołań pokazuje dla każdej funkcji, które funkcje ją wołały, a które ona wywołała i ile razy. Znajduje się tam również oszacowanie, ile czasu zajęły poszczególne instrukcje funkcji.

Przykładowe fragmenty pliku (płaski profil):


	%Time 	Seconds	Cumsecs #Calls 	msec/call 	Name
	99.0	62.51	62.51	1	62510.		cholesky
	0.8	0.51	63.02	1	510.		back
	0.2	0.11	63.13	1	110.		init_matrix
	0.0	0.00	63.13	1	0.		main
	0.0	0.00	63.13	1	0.		init_rhs

gdzie:
i graf wywołań:

	                                  called/total       parents 
	index  %time    self descendents  called+self    name    	index
	                                  called/total       children
	
	                0.00       61.98       1/1           _start [2]
	[1]    100.0    0.00       61.98       1         main [1]
               		61.41        0.00       1/1           cholesky [3]
	                0.45        0.00       1/1           back [4]
	                0.12        0.00       1/1           init_matrix [5]
                	0.00        0.00       1/1           init_rhs [6]
	-----------------------------------------------                                                     
	[2]    100.0    0.00       61.98                 _start [2]
    	            0.00       61.98       1/1           main [1]
	-----------------------------------------------
    	           61.41        0.00       1/1           main [1]
	[3]     99.1   61.41        0.00       1         cholesky [3]
	-----------------------------------------------
	                0.45        0.00       1/1           main [1]
	[4]      0.7    0.45        0.00       1         back [4]
	-----------------------------------------------
	                0.12        0.00       1/1           main [1]
	[5]      0.2    0.12        0.00       1         init_matrix [5]
	-----------------------------------------------
	                0.00        0.00       1/1           main [1]
	[6]      0.0    0.00        0.00       1         init_rhs [6]
	-----------------------------------------------

przy czym:

Bardzo ciekawą opcją gprof jest 'skomentowany wydruk kodu'. Jest to najbardziej szczegółowa informacja, jaką potrafi on udzielić. Dzięki niej nasz program przestanie przed nami kryć jakiekolwiek tajemnice. Będziemy mogli na przykład zobaczyć, że w pojedynczej funkcji pewne linijki kodu nie wykonują się wcale (np. warunek if nigdy nie jest spełniony, choć myślimy, że powinien) podczas, gdy inne wykonują się zdecydowanie zbyt często.

Powiedzmy, że mamy następujący kod:


	 1 ulg updcrc(s, n)
	 2     uch *s;
	 3     unsigned n;
	 4 {
	 5     register ulg c;
	 6
	 7     static ulg crc = (ulg)0xffffffffL;
	 8
	 9     if (s == NULL) {
	10         c = 0xffffffffL;
	11     } else {
	12         c = crc;
	13         if (n) do {
	14             c = crc_32_tab[...];
	15         } while (--n);
	16     }
	17     crc = c;
	18     return c ^ 0xffffffffL;
	19 }

po zastosowaniu odpowiedniej opcji gprof potrafi wyprodukować coś takiego:


                ulg updcrc(s, n)
                    uch *s;
                    unsigned n;
            2 ->{
                    register ulg c;
                
                    static ulg crc = (ulg)0xffffffffL;
                
            2 ->    if (s == NULL) {
            1 ->	c = 0xffffffffL;
            1 ->    } else {
            1 ->	c = crc;
            1 ->        if (n) do {
        26312 ->            c = crc_32_tab[...];
26312,1,26311 ->        } while (--n);
                    }
            2 ->    crc = c;
            2 ->    return c ^ 0xffffffffL;
            2 ->}

Tu już widać jak na dłoni, że funkcja updcrc() wykonała się dwa razy, że s zostało dwukrotnie sprawdzone, czy jest NULL'em. Raz okazało się, że tak i wykonała się linijka c = 0xffffffffL;, a raz okazało się, że nie i wtedy mieliśmy 26312 podstawień, bo tyle było trzeba by n sprowadzić do zera.

Więcej na temat programu gprof oraz wyjaśnienie jak dokładnie interpretuje się płaski profil i graf wywołań znajdziemy na stronie: http://www.cs.utah.edu/dept/old/texinfo/as/gprof.html oraz (po polsku!!) na stronie: http://mops.uci.agh.edu.pl/~mylka/gprof/index.html.

Powrót do góry strony


Testy

Jest domyślnie zainstalowany na labowym PLD i działa bez problemu pod VPC. Jedyne czego nie udało mi się osiągnąć, to pokazany wyżej wydruk kodu programu ze statystykami dla każdej linii. Problem był jednak z gcc a nie z gprof. Aby z tego skorzystać trzeba skompilować program z dodatkową opcją -a, a gcc z labowego PLD twierdzi, że takiej opcji nie zna. A szkoda, bo jeśliby się udało, to wystarczą opcje -l -A w gprof i otrzymalibyśmy taki ładny wydruk ;).

Pliki, na których testowałam i wyniki testu:

Powrót do góry strony


OProfile

OProfile jest narzędziem do profilowania Linuxa (jadra 2.2, 2.4, 2.6), które może pracować na wielu architekturach. Można za jego pomocą profilować wszystkie części systemu operacyjnego od jądra (włącznie z modułami i obsługą przerwań) do dzielonych bibliotek i zwykłych aplikacji. Chodzi on w tle, zbierając dane i nie obciążając zbytnio systemu.

OProfile

Rysunek: Diagram pokazujący obciążenie systemu przez program OProfile (Źródło: http://oprofile.sourceforge.net/performance/)

Dzięki tym cechom program bardzo dobrze nadaje się do profilowania całych systemów, zwłaszcza kiedy chcemy znaleźć przyczynę problemów z wydajnością.

Wiele procesorów oferuje tzw. `performance counters', które są specjalnymi rejestrami przenaczonymi do liczenia różnych zdarzeń w systemie takich jak, np. ilość cykli procesora czy nietrafień w pamięci cache. Profilując kod, OProfile korzysta właśnie z tych liczników: cyklicznie, co pewną liczbę zdarzeń (którą można konfigurować) zapisywana jest wartość licznika rozkazów procesora. Następnie te dane są zbierane i tworzą profil analizowanego programu. Niestety ta funkcjonalność nie jest dostępna w maszynach wirtualnych takich jak Virtual PC czy VMware, ponieważ nie mamy dostępu do tych specjalnych rejestrów procesora. Wkładając w to jednak więcej wysiłku, da się ten problem obejść. Szczegóły można znaleźć na stronie programu: http://oprofile.sourceforge.net

Powrót do góry strony


Skrót zastosowań OProfile

Program warto stosować, gdy:

Powrót do góry strony


Podstawy

Minimalnym wymaganiem, żeby zacząć pracę z programem jest powiedzienie mu, gdzie znajduje się plik vmlinux odpowiadający jądru, które używamy (Uwaga! potrzebny jest nieskompresowany obraz jądra). Przykładowo:


	opcontrol --vmlinux=/boot/vmlinux-`uname -r`

Jeśli nie chemy profilować jądra, możemy też powiedzieć programowi, że nie mamy pliku vmlinux


	opcontrol --no-vmlinux

Po tych wstępnych działaniach możemy uruchomić demona (oprofiled), który zbiera interesujące nas dane:


	opcontrol --start

Jeśli będziemy chcieli skończyć profilowanie, możemy to zrobić tak:


	opcontrol --shutdown

Zauważmy znaczącą różnicę z gprof - nie są potrzebne żadne opcje jak -pg czy -a dla gcc. Co jakiś czas (też w wyniku komend opcontrol --shutdownopcontrol --dump) zebrane dane są zapisywane do pliku w katalogu /var/lib/oprofile/samples. Te pliki zawierają informacje zebrane na temat bibliotek dzielonych, aplikacji użytkownika, jądra i modułów jądra. W każdej chwili możemy wyczyścić te dane komendą opcontrol --reset.

Zachęcam do przeczytania całego manuala, który znajduje się na stronie: http://oprofile.sourceforge.net/doc/.

Warto też przejrzeć przykładowe raporty dostępne pod adresem: http://oprofile.sourceforge.net/examples/.

Jeden z nich wygląda tak (anotacja połączenia kodu źródłowego z kodem w asemblerze wyprodukowanym przez kompilator):


	$ opannotate --source --assembly `which oprofiled`
	... 
				: index = hash->hash_base[odb_do_hash(hash, key)];
				: while (index) {
 	1455 14.2689	: 804c5c2:	test %eax,%eax
   	13 0.1275	: 804c5c4:	je 804c5ef  
   				: 	if (index <= 0 || index >= hash->descr->current_size) { 
	12 0.1177 	: 804c5c6: 	mov 0x8(%esi),%ecx 
	2 0.0196 	: 804c5c9: 	lea 0x0(%esi,1),%esi 
	19 0.1863	: 804c5d0: 	cmp 0x4(%ecx),%eax 
	78 0.7649	: 804c5d3: 	jae 804c638   
				: 		char * err_msg; 
				: 		asprintf(&err_msg, "odb_insert() invalid index %u\n", : index); 
				: 		odb_set_error(hash, err_msg);
				:		return EXIT_FAILURE; 
				: 	} 
				: 	node = &hash->node_base[index]; 
	12 0.1177 	: 804c5d5: 	lea (%eax,%eax,2),%edx 
	41 0.4021	: 804c5d8: 	mov (%esi),%eax 
			: 804c5da: 	lea (%eax,%edx,4),%ebx 
	...

Powrót do góry strony


Fenris

Fenris

Rysunek: Logo programu Fenris (Źródło: http://lcamtuf.coredump.cx/fenris/)

Fenris jest zaawansowanym zestawem narzędzi do analizy i profilowania kodu, debugowania, inżynierii wstecz, diagnostyki, analizy bezpieczeństwa i wielu innych celów. Jego główne komponenty to:

Szczegółowe informacje można znaleźć na stronie projektu: http://lcamtuf.coredump.cx/fenris/

W tym szczegółowy opis możliwości programu wraz z przykładami: http://lcamtuf.coredump.cx/fenris/README

Powrót do góry strony


© Magdalena Dukielska