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. |
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.
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 MEMWATCH
i
MW_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.
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
Pliki, na których testowałam program i kilka innych plików, które ściagamy razem z programem i które warto prześledzić:
Pakiet YAMD znajduje problemy z dynamiczną alokacją pamięci w C i C++.
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.
Ś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.
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
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()
.
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:
malloc
'iem, do której nie wpisaliśmy jeszcze żadnych wartości
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.
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.
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.
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:
malloc()
/free()
/new
/delete
są przechwytywane. Dzięki temu możemy wykryć następujące problemy:
malloc()
/new
/new []
i
free()
/delete
/delete []
src
i dst
w funkcji memcpy()
i jej podobnych
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:
ls -l
)
-v
(verbose)
-v
i dodatkowo
--leak-check=full
, co daje bardziej szczegółowe informacje
-v
(verbose)
-v
(verbose)
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
ElectricFence Aborting: free(41fc0e00): address not from malloc(). //tu było błędne free()
$ gdb test1
i śledząc program krok po kroku,
znaleźć źródło problemów.
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:
"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 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:
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
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
%Time
- Czas CPU w procentach potrzebny do wykonania danej procedury.
Seconds
- Czas CPU w sekundach potrzebny do wykonania danej procedury.
Cumsecs
- Sumaryczny czas CPU w sekundach.
#Calls
- Liczba wywołań danej procedury.
msec/call
- Średni czas wykonania danej procedury przy jednorazowym wykonaniu.
Name
- Nazwa procedury.
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] -----------------------------------------------
index
- Numer funkcji lub procedury.
%time
- Czas w procentach spędzony w procedurze i wszystkich jej 'dzieciach'.
self
- Czas w sekundach. Dla `rodzica' jest to czas spędzony w procedurze, pod warunkiem, ze została wywołana przez danego `rodzica'.
Jest to istotna informacja, jeśli `dziecko' może być wywołane przez różnych `rodziców'. Dla `dziecka' jest to czas wykonania `dziecka',
jeśli zostało wywołane przez rozpatrywaną procedurę.
descendents
- Czas w sekundach. Dla rozpatrywanej procedury jest to łączny czas spędzony we wszystkich jej `dzieciach'.
Dla 'rodzica' jest to czas spędzony w procedurze.
calls
- Dla rozpatrywanej procedury jest to liczba nierekurencyjnych wywołań danej procedury. Jeśli procedura wywołała
siebie sama, to pojawia się rownież liczba wywołań rekurencyjnych. Dla `rodzica' liczba m/n
oznacza, ze dla n
wywołań danej procedury m
razy została wywołana przez tego rodzica. Dla `dziecka' liczba m/n
oznacza,
że dla n
wywołań dziecka m
z tych wywołań to wywołania przez rozpatrywaną procedurę.
name/index
- Nazwy i numery procedury, jej `rodziców' i jej `dzieci'.
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 }
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 ->}
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.
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:
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.
Rysunek: Diagram pokazujący obciążenie systemu przez program OProfile (Źródło: http://oprofile.sourceforge.net/performance/)
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
Program warto stosować, gdy:
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 --shutdown
i opcontrol --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 ...
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
© Magdalena Dukielska |