Jedną z ważniejszych umiejętności przy pisaniu programów jest sprawdzanie czy aby program działa tak jak powinien. Bywają w końcu błędy, które nie powodują wyłożenia się programu od razu tylko musi pochodzić dłuższy czas, a nawet i wtedy wcale nie musi być jednoznaczne, że zatkanie się systemu jest efektem działania naszego programu, a nie jakiegoś innego.
W końcu przy bardziej rozbudowanych programach odkryć wyciek pamięci wcale nie jest łatwo. Szczególnie jak mamy małe strukturki po kilka bajtów przez co nie zwracają na siebie uwagi. Zapomnieć o zwolnieniu takiego ustrojstwa jest dość łatwo. Czasem też się zdarza skorzystać ze zmiennej automatycznej bez jej inicjalizacji co może prowadzić do bardzo nieoczekiwanych rezultatów. Na szczęście mądrzy ludzie wymyślili program, który pomaga w radzeniu sobie w dużym stopniu z wieloma błędami. A nazwali go Valgrind
Poniżej przykład. Oczywiście w tym przykładzie większość błędów widać i bez pomocy Valgrinda, ale to załsuga jego dość niewielkich rozmiarów i celowego wpisania błędów.
#include <stdio.h> #include <stdlib.h> #include <string.h> struct lelem { int value; struct lelem *next; char text[5]; }; struct lelem * make_list(void){ struct lelem *lstart; struct lelem *ltmp; int i; lstart = NULL; for(i = 0; i text, "012345678910"); lstart->value = i; lstart->next = ltmp; } return lstart; } int main(int argc, char **argv){ struct lelem *list; int i; list = make_list(); while(list != NULL){ printf("%d, %s\n", list->value, list->text); list = list->next; } printf("%d\n", i); return 0; }
Kompilujemy (przełącznik g dodaje do pliku informacje przydatne do debugowania takiej jak nazwy funkcji, pliki i linie, w których się znajdują):
gcc -g -Wall -o errors.e file.c
Uruchamiamy
elmo@elmo ~/programy $ valgrind --leak-check=full --log-file=debug ./errors.e 19, 012345678910 18, 012345678910 17, 012345678910 16, 012345678910 15, 012345678910 14, 012345678910 13, 012345678910 12, 012345678910 11, 012345678910 10, 012345678910 9, 012345678910 8, 012345678910 7, 012345678910 6, 012345678910 5, 012345678910 4, 012345678910 3, 012345678910 2, 012345678910 1, 012345678910 0, 012345678910 67218560
Jak widać program wykonał się bez większych problemów - wszystko wypisał i zakończył się poprawnie. To teraz wyświetlmy sobie plik debug:
elmo@elmo ~/programy $ cat debug.9860 ==9860== Memcheck, a memory error detector. ==9860== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al. ==9860== Using LibVEX rev 1658, a library for dynamic binary translation. ==9860== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP. ==9860== Using valgrind-3.2.1, a dynamic binary instrumentation framework. ==9860== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al. ==9860== For more details, rerun with: -v ==9860== ==9860== My PID = 9860, parent PID = 9326. Prog and args are: ==9860== ./errors.e ==9860== ==9860== Invalid write of size 1 ==9860== at 0x40231B4: strcpy (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x8048418: make_list (erros.c:20) ==9860== by 0x8048456: main (erros.c:33) ==9860== Address 0x4166038 is 0 bytes after a block of size 16 alloc'd ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== Invalid write of size 1 ==9860== at 0x40231BE: strcpy (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x8048418: make_list (erros.c:20) ==9860== by 0x8048456: main (erros.c:33) ==9860== Address 0x416603C is 4 bytes after a block of size 16 alloc'd ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== Invalid read of size 1 ==9860== at 0x4023141: strlen (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x407C207: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804847E: main (erros.c:35) ==9860== Address 0x41664F8 is 0 bytes after a block of size 16 alloc'd ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== Invalid read of size 1 ==9860== at 0x409DF71: _IO_file_xsputn (in /lib/libc-2.5.so) ==9860== by 0x407C34C: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804847E: main (erros.c:35) ==9860== Address 0x41664FB is 3 bytes after a block of size 16 alloc'd ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== Invalid read of size 1 ==9860== at 0x409DFB3: _IO_file_xsputn (in /lib/libc-2.5.so) ==9860== by 0x407C34C: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804847E: main (erros.c:35) ==9860== Address 0x41664F8 is 0 bytes after a block of size 16 alloc'd ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== Use of uninitialised value of size 4 ==9860== at 0x407874F: (within /lib/libc-2.5.so) ==9860== by 0x407B3C8: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== Conditional jump or move depends on uninitialised value(s) ==9860== at 0x4078757: (within /lib/libc-2.5.so) ==9860== by 0x407B3C8: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== Use of uninitialised value of size 4 ==9860== at 0x4078770: (within /lib/libc-2.5.so) ==9860== by 0x407B3C8: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== Conditional jump or move depends on uninitialised value(s) ==9860== at 0x4078778: (within /lib/libc-2.5.so) ==9860== by 0x407B3C8: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== Conditional jump or move depends on uninitialised value(s) ==9860== at 0x407BA45: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== Conditional jump or move depends on uninitialised value(s) ==9860== at 0x407A6BE: vfprintf (in /lib/libc-2.5.so) ==9860== by 0x408190F: printf (in /lib/libc-2.5.so) ==9860== by 0x804849C: main (erros.c:38) ==9860== ==9860== ERROR SUMMARY: 378 errors from 11 contexts (suppressed: 7 from 1) ==9860== malloc/free: in use at exit: 320 bytes in 20 blocks. ==9860== malloc/free: 20 allocs, 0 frees, 320 bytes allocated. ==9860== For counts of detected errors, rerun with: -v ==9860== searching for pointers to 20 not-freed blocks. ==9860== checked 54,136 bytes. ==9860== ==9860== ==9860== 320 (16 direct, 304 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2 ==9860== at 0x40205D1: malloc (in /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so) ==9860== by 0x80483FF: make_list (erros.c:19) ==9860== by 0x8048456: main (erros.c:33) ==9860== ==9860== LEAK SUMMARY: ==9860== definitely lost: 16 bytes in 1 blocks. ==9860== indirectly lost: 304 bytes in 19 blocks. ==9860== possibly lost: 0 bytes in 0 blocks. ==9860== still reachable: 0 bytes in 0 blocks. ==9860== suppressed: 0 bytes in 0 blocks. ==9860== Reachable blocks (those to which a pointer was found) are not shown. ==9860== To see them, rerun with: --show-reachable=yes
Jak więc widać błędół jest całkiem sporo. Bardzo wygodną rzeczą jest kolejne wypisanie wywołań funkcji, które doprowadziły do złego użycia pamięci. Memchek (który jest uruchamiany jako domyślne narzędzie) bardzo ładnie wszystko nam wpisujue. Widać, że mamy wyciek pamięci, że piszemy poza obszarem, który zaalokowaliśmy oraz korzystamy ze zmiennej, której nie zainicjalizowaliśmy.
Oczywiście są błędy, których nie wykryje. Jeżeli struktura miałaby postać
struct lelem { char text[5]; int value; struct lelem *next; };
i wpisalibyśmy do pola text o bajt lub dwa za dużo to by nie zostało to wykryte (nadpisalibyśmy jedynie value i wskaźnik na następnik, ale w obszarze, który został dla nas zaalokowany, więc operacji w pełni prawidłowa z technicznego punktu widzenia).