Testování
Nikdo není neomylný a chyby při programování jsou běžná věc (až moc). Při pogramování v C/C++ mají chyby tu ošklivou vlastnosti, že se často projeví neoprávněným přistupem do paměti nebo něčím podobným, co program odstřelí. Zjistit, kde je chyba, bývá velmi obtížné. V této kapitole vám ukáži pár nástrojů, které vám v tom mohou pomoci.
GDB
GDB (gnu debugger) je klasický nástroj na debuggování, tj. vyhledávání a odstraňování chyb.
Česky by se asi řeklo deratizace, nebo odvšivení :-) Bug znamená anglicky brouk. V angličtině se výraz bug pro chybu používá dlouho. Dříve totiž nebylo neobvyklé, že za chybou nějakého elektronického zařízení stál skutečně nějaký brouk :-).
Moderní vývojové prostředí mají obvykle nějaký ten debugger integrovaný v sobě, ale není na škodu vědět, že existuje i starý dobrý GDB a jak ho požít.
Ukázku GDB předvedu na tomto kódu:
- /*------------------------------------------------*/
- /* 08testovani/write.c */
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <errno.h>
- int fd;
- FILE *file;
- fd = open("pokus.txt", O_WRONLY | O_APPEND);
- /* if(fd < 0) { fprintf(stderr,"Nepodarilo se otevrit %s\n", "pokus.txt"); exit(1); } */
- /* if (f == NULL) { fprintf(stderr,"Nepodarilo se vytvorit FILE *\n"); perror(NULL); exit(1); } */
- }
- zapisText(text);
- }
- /*------------------------------------------------*/
V programu jsou zakomentované kontroly chyb, jako že jsem na ně zapoměl.
Aby vám byl program gdb
co k čemu, musíte přeložit svůj testovaný
program s volbou -g
. Tato volba způsobí,
že překladač do výsledného spustitelného
souboru přidá nějaké ladící informace, které gdb
využije k tomu, aby vám mohl ukázat,
na jaké řádce ve zdrojovém kódu program havaroval, nebo abyste mohli program spouštět řádek
po řádku (tzv. krokovat) …
Teď spusťte gdb a jako argument mu předejte název programu, který chcete ladit.
For help, type "help".
Type "apropos word" to search for commands related to "word".
..
Reading symbols from /home/petr/resource/c/linux/zdrojaky-linux/08testovani/write...done.
(gdb)
Program gdb teď čeká na vaše přikazy.
Základním příkazem je help
, kterým si můžete zobrazit
nápovědu.
(gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous. (gdb) help running … run -- Start debugged program signal -- Continue program with the specified signal start -- Run the debugged program until the beginning of the main procedure step -- Step program until it reaches a different source line …
Výstup nápovědy jsem zkrátil, protože je moc dlouhý. Jak se můžete dočíst, příkaz
run
spustí debugovaný program (v našem příkladě program write
).
(gdb) run Starting program: /home/petr/resource/c/linux/zdrojaky-linux/08testovani/write Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7a74d2d in vfprintf () from /lib64/libc.so.6
Program se spustil, ale byl ukončen chybou Segmentation fault
. Teď
můžu použít gdb ke zjištění co se vlastně stalo.
Příkaz where
zobrazí zásobník volání funkcí k místu,
kde program přestal běžet.
(gdb) where #0 0x00007ffff7a74d2d in vfprintf () from /lib64/libc.so.6 #1 0x00007ffff7a7f237 in fprintf () from /lib64/libc.so.6 #2 0x0000000000400691 in zapisText (text=0x400783 "Ahoj Svete!\n") at write.c:18 #3 0x00000000004006db in main (argc=1, argv=0x7fffffffdd48) at write.c:24
Z výpisu se dočtete, že poslední volaná funkce byla vprintf()
z knihovny /lib64/libc.so.6.
Tu vyvolala funke fprintf()
z téže knihovny. A tu zase vyvolala funke zapisText()
z
našeho zdrojového kódu write.c na řádce 18.
Knihovna libc.so.6 nebyla přeložena s ladícími symboly, proto nevidíte zdrojový kód s řádkou, kde
jsou funke vprintf()
a fprintf()
volány. Nevidíte ani argumenty, se kterými
byli funkce volány.
U volání funkce zapisText()
vidíte, jaký jí byl předán arugment. Je to adresa 0x400783,
na které začíná řetězec Ahoj Svete!\n"
.
Příkazem list
můžete vypsat zdrojový kód z místa, kde se zrovna nacházíte.
Poslední zdrojový kód, který má gdb k dispozici je teď write.c
a nacházíte se na řádce 18.
Příkazem list
vypíšete kus zdrojového kódu kolem tohoto řádku.
(gdb) list 14 fd = open("pokus.txt", O_WRONLY | O_APPEND); 15 /* if(fd < 0) { fprintf(stderr,"Nepodarilo se otevrit %s\n", "pokus.txt"); exit(1); } */ 16 file = fdopen(fd, "a"); 17 /* if (f == NULL) { fprintf(stderr,"Nepodarilo se vytvorit FILE *\n"); perror(NULL); exit(1); } */ 18 fprintf(file, "%s", text); 19 fclose(file); 20 } 21 22 int main(int argc, char *argv[]) { 23 char * text = "Ahoj Svete!\n";
Z předchozího víte, že argument text ukazuje správně na "Ahoj světe!\n". Lze se tedy domnívat, že bude chyba v proměnné file. Můžete si zkusit vytisknout její hodnotu.
(gdb) print file No symbol "file" in current context.
Hmm, něco je špatně. Asi se nenacházíte ve správném contextu.
(gdb) info frame Stack level 0, frame at 0x7fffffffdb30: rip = 0x7ffff7a74d2d in vfprintf; saved rip 0x7ffff7a7f237 called by frame at 0x7fffffffdc10 Arglist at 0x7fffffffd560, args: Locals at 0x7fffffffd560, Previous frame's sp is 0x7fffffffdb30 Saved registers: rbx at 0x7fffffffdaf8, rbp at 0x7fffffffdb20, r12 at 0x7fffffffdb00, r13 at 0x7fffffffdb08, r14 at 0x7fffffffdb10, r15 at 0x7fffffffdb18, rip at 0x7fffffffdb28
Aha, jsme ve funkci vprintf()
(Stack #0).
Musíme se přesunout na místo, kde je volána fukce fprintf()
,
tj. do funkce zapisText()
, tj o 2 levely výše.
(gdb) up 2 #2 0x0000000000400691 in zapisText (text=0x400783 "Ahoj Svete!\n") at write.c:18 18 fprintf(file, "%s", text); (gdb) list 13 FILE *file; 14 fd = open("pokus.txt", O_WRONLY | O_APPEND); 15 /* if(fd < 0) { fprintf(stderr,"Nepodarilo se otevrit %s\n", "pokus.txt"); exit(1); } */ 16 file = fdopen(fd, "a"); 17 /* if (f == NULL) { fprintf(stderr,"Nepodarilo se vytvorit FILE *\n"); perror(NULL); exit(1); } */ 18 fprintf(file, "%s", text); 19 fclose(file); 20 } 21 22 int main(int argc, char *argv[]) { (gdb) print file $1 = (FILE *) 0x0
Aha, a už je to jasné. Proměnná file je ukazatel typu FILE *
,
který ukazuje na 0x0, tedy na NULL.
file vrací fdopen()
, tak schválně co on měl za argument (fd).
(gdb) print fd $2 = -1
Nojo, fd byl -1. Je jasné, že funkce open()
vrátila -1,
což znamená, že se jí nepodařilo otevřít soubor. A to proto,
že soubor neexistuje – ale na to už nepřijdete pomocí gdb, ale díky vašim
odborným znalostem
chování funkce open()
:-).
Příkaz up
má svého bratříčka down
,
kterým můžete jít zásobníkem volání zase dolů …
Breakpoints
Na stejném příkladu uvedu trochu jiný přístup hledání chyb. Řekněme že tušíte, že je chyba někde ve funkci
zobrazText()
. Můžete nastavit tzv. breakpoint, což je místo v kódu, kde
gdb vykonávání programu zastaví a umožní vám prohlížet si proměnné atp.
Breakpoint můžete nastavovat různými způsoby, např. můžete určit na jakém řádku v jakém zdrojovém souboru se má vykonávání programu zastavit, nebo, jako v následujícím příkladě, zadáte název funkce, při jejímž volání se program zastaví.
GNU gdb (GDB; openSUSE 13.1) 7.6.50.20130731-cvs
...
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)
Nejdříve načtu program (protože jsem ho nepředal jako argument příkazové řádky), pak nastavím breakpoint a program spustím.
(gdb) file ./write Reading symbols from /media/Documents/public_html/www/nette/install/resource/c/linux/zdrojaky-linux/08testovani/write...done. (gdb) break zapisText Breakpoint 1 at 0x400659: file write.c, line 14. (gdb) run Starting program: /home/petr/resource/c/linux/zdrojaky-linux/08testovani/write Breakpoint 1, zapisText (text=0x400783 "Ahoj Svete!\n") at write.c:14 14 fd = open("pokus.txt", O_WRONLY | O_APPEND); (gdb) list 9 10 11 void zapisText(char * text) { 12 int fd; 13 FILE *file; 14 fd = open("pokus.txt", O_WRONLY | O_APPEND); 15 /* if(fd < 0) { fprintf(stderr,"Nepodarilo se otevrit %s\n", "pokus.txt"); exit(1); } */ 16 file = fdopen(fd, "a"); 17 /* if (f == NULL) { fprintf(stderr,"Nepodarilo se vytvorit FILE *\n"); perror(NULL); exit(1); } */ 18 fprintf(file, "%s", text); (gbd)
Program se zastavil na řádce 14. Příkazem next
se tato řádka vykoná.
(Příkazem step
byste vstoupili do těla funkce open()
a vyvolali byste první řádek v ní – pokud byste měli k dispozici zdrojové kódy k open()
).
(gdb) next 16 file = fdopen(fd, "a"); (gdb)
Příkaz byl vykonán a program se zastavil na další řádce. Teď si můžete prohlédnout hodnotu proměnné fd a pokračovat v krokování programu.
(gdb) print fd $1 = -1 (gdb) next 18 fprintf(file, "%s", text); (gdb) print file $2 = (FILE *) 0x0 (gdb) print text $3 = 0x400760 "Ahoj Svete!\n" (gdb) next Program received signal SIGSEGV, Segmentation fault. 0x00007ffff7a99232 in fputs () from /lib64/libc.so.6
Program gdb umí snad všechno, co si usmyslíte. Breakpointy můžete libovolně
přidávat, zapínat, vypínat, mazat. Příkazem
continue
spustíte program, dokud nenarazí
gdb na další zapnutý breakpoint (nebo program neskončí), můžete nastavit
breakpoint s podmínkou (že se má aktivovat, jen když nějaká proměnná má
nějakou hodnotu …) atd.
Projdete-li si nápovědu (příkaz help
) uvidíte sami, co všechno umí.
Programy nm, ltrace a strace
Program nm
vypíše symboly z objektového souboru.
0000000000601058 B __bss_start
0000000000601058 b completed.6362
0000000000601048 W data_start
0000000000601048 D __data_start
0000000000400580 t deregister_tm_clones
00000000004005f0 t __do_global_dtors_aux
0000000000600e08 t __do_global_dtors_aux_fini_array_entry
0000000000601050 D __dso_handle
0000000000600e18 d _DYNAMIC
0000000000601058 D _edata
0000000000601060 B _end
U fclose@@GLIBC_2.2.5
U fdopen@@GLIBC_2.2.5
0000000000400744 T _fini
U fputs@@GLIBC_2.2.5
0000000000400610 t frame_dummy
0000000000600e00 t __frame_dummy_init_array_entry
00000000004008c0 r __FRAME_END__
0000000000601000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000004004b8 T _init
0000000000600e08 t __init_array_end
0000000000600e00 t __init_array_start
0000000000400750 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000600e10 d __JCR_END__
0000000000600e10 d __JCR_LIST__
w _Jv_RegisterClasses
0000000000400740 T __libc_csu_fini
00000000004006d0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
0000000000400697 T main
U open@@GLIBC_2.2.5
00000000004005b0 t register_tm_clones
0000000000400550 T _start
0000000000601058 D __TMC_END__
0000000000400640 T zapisText
Ve výpisu najdete funkci main()
i zapisText()
, ale třeba i
open()
(z GLIBC_2.2.5), takže hned víte, že se program možná pokusí
otevřít nějaký soubor. Že by to byl virus? :-).
Program ltrace
spustí program a sleduje,
jaké knihovní (library) funkce program používá.
Sleduje taky signály a může sledovat systémová volání.
Program strace
funguje podobně jako ltrace,
zaznamenává systémová volání spuštěného programu.
__libc_start_main(0x400697, 1, 0x7fff80e830e8, 0x4006d0 <unfinished ...>
open("pokus.txt", 1025, 020072030370) = -1
fdopen(0xffffffff, 0x40075e, 0x7fff80e830f8, -112) = 0
fputs("Ahoj Svete!\n", 0 <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
execve("./write", ["./write"], [/* 97 vars */]) = 0
brk(0) = 0x22bf000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3a4bb20000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=119213, ...}) = 0
mmap(NULL, 119213, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3a4baf8000
close(3) = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2008128, ...}) = 0
mmap(NULL, 3861056, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f3a4b548000
mprotect(0x7f3a4b6ed000, 2097152, PROT_NONE) = 0
mmap(0x7f3a4b8ed000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a5000) = 0x7f3a4b8ed000
mmap(0x7f3a4b8f3000, 14912, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3a4b8f3000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3a4bb1f000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3a4bb1e000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3a4bb1d000
arch_prctl(ARCH_SET_FS, 0x7f3a4bb1e700) = 0
mprotect(0x7f3a4b8ed000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f3a4bb17000, 4096, PROT_READ) = 0
munmap(0x7f3a4baf8000, 119213) = 0
open("pokus.txt", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory)
fcntl(-1, F_GETFL) = -1 EBADF (Bad file descriptor)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0} ---
+++ killed by SIGSEGV +++
Neoprávněný přístup do paměti (SIGSEGV)
To co vidíte za rovnítkem na konci volání funkce je její návratová hodnota. A někde k tomu máte navíc popis jejího významu.
Program strace skončil trochu předčasně, protože se pokusil zjistit
nějaké informace o deskriptoru -1
pomocí volání funkce fcntl()
…
Program strip
Výsledný program obsahuje kromě instrukcí také symboly, které se mohou
hodit např. pro ladění programu, informace o překladači, o zdrojovém kódu atp.
A to nejen když použijete volbu -g
.
Některé z těchto pro běh programu zbytečných informací lze odstranit pomocí
programu strip
, zmenšit tak program a tím jej zrychlit.
-rwxrwxrwx 1 root root 14614 17. srp 21.49 write
$ strip ./write
$ ls -l ./write
-rwxrwxrwx 1 root root 6360 17. srp 21.55 write
Program write
je od této chvíle pro ladění nepoužitelný.
Kdybyste byli jó zvědaví, podívejte se na Strip command examples.
Program mtrace
Program mtrace
vám pomůže odhalit úniky paměti (paměť, kterou jste
alokovali, ale neuvolnili).
Aby program mtrace
fungoval, musíte přidat do zdojového kódu
volání funkce mtrace()
deklarované
v hlavičkovém souboru <mcheck.h>. Tato funkce zapne logování volání
funkcí malloc()
, realloc()
a free()
.
Funkce muntrace()
to zase vypne
(takže můžete logovat jen určitou část programu, když chcete.)
- /*------------------------------------------------*/
- /* 08testovani/mtrace.c */
- #include <stdlib.h>
- #include <string.h> /* strncpy() */
- #include <mcheck.h>
- #define N 5
- #define M 10
- int i;
- mtrace();
- }
- }
- /* tady by se melo zase v cyklu uvolnit kazde pole[i] */
- muntrace();
- }
- /*------------------------------------------------*/
Překlad (s volbou -g
):
Aby sledovaný program vytvořil nějaký výstup pro program
mtrace
, musíte nastasvit
proměnnou prostředí MALLOC_TRACE
.
$ ./mtracetest # spoustim muj program
$ ls *.txt
mtrace-output.txt
$ mtrace ./mtracetest mtrace-output.txt
Memory not freed:
-----------------
Address Size Caller
0x0000000000e5c490 0xa at /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace.c:22
0x0000000000e5c4b0 0xa at /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace.c:22
0x0000000000e5c4d0 0xa at /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace.c:22
0x0000000000e5c4f0 0xa at /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace.c:22
0x0000000000e5c510 0xa at /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace.c:22
Ve výstupu vidíte adresu alokované a neuvolněné paměti, její velikost (0xa = 10 bajtů, což odpovídá 10xsizeof(char)). A taky vidíte číslo řádky zrojového souboru, kde byla paměť alokována.
Program gprof
Program grpof
je profiler,
který sleduje kolikrát byla jaká funkce spuštěna a jak dlouho její běh trval.
gprof analyzuje výstup, který vytvoří program přeložený s volbou -pg
.
V testovacím programu najdete funkci factorial()
, která v cyklu provádí
nějaký zybtečný výpočet, abych ji trochu pozdržel.
Profilovací data se ukládají do souboru gmon.out
.
Překladač Překlad, spuštění a profilování programu:
clang
bohužel (zatím?) -pg
neumí, takže použiji
gcc
.
$ ./gproftest
Zadejte číslo pro výpočet faktoriálu: 10
Výsledek je 3628800
$ gprof -b ./gproftest
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls s/call s/call name
101.03 43.92 43.92 1 43.92 43.92 factorial
0.00 43.92 0.00 1 0.00 0.00 nactiCislo
Call graph
granularity: each sample hit covers 2 byte(s) for 0.02% of 43.92 seconds
index % time self children called name
9 factorial [1]
43.92 0.00 1/1 main [2]
[1] 100.0 43.92 0.00 1+9 factorial [1]
9 factorial [1]
-----------------------------------------------
<spontaneous>
[2] 100.0 0.00 43.92 main [2]
43.92 0.00 1/1 factorial [1]
0.00 0.00 1/1 nactiCislo [3]
-----------------------------------------------
0.00 0.00 1/1 main [2]
[3] 0.0 0.00 0.00 1 nactiCislo [3]
-----------------------------------------------
Index by function name
[1] factorial [3] nactiCislo
Pokud nepoužijte volbu -b
, bude výstup prošpikován nápovědou (popisem toho, co co znamená).
Vyzkoušejte si to.
Z výstupu vidíte, že funkce factorial()
byla volána z funkce
factorial()
9x a
z funkce main()
1x. Což by pro fakoriál 10 dávalo smysl :). Taky vidíte,
že funkce factorial()
volala funkci faktorial()
9x.
V další části tabulky vidíte, že funkce main()
volala
funkce factorial()
a nactiCislo()
.
Protože funkce main()
volala funkci factorial()
, vidíte, že
její „děti“ (funkce, které volala, tedy hlavně factorial()
)
zabrali celkem čas 43.92
vteřiny.
Poslední část ukazuje, že funke natiCislo()
byla volána z funkce main()
.
Všechno proběhlo tak rychle, že jsou všude výsledné časy volání 0.0 sekund, až na funkci
factorial()
, ve které program strávil celkem 43.92
vteřiny.
Všiměte si, že se do délky volání funkce nactiCislo()
nezapočítal čas,
kdy funke čekala na uživatelský vstup.
Program valgrind
Valgrind je další nástroj pro analýzu. Valgrind umí profilovat program, zjišťovat úniky paměti, hledat chyby v multithreadových aplikacích a mnoho dalšího.
Další info viz jeho domovská stránka. Začít můžete tutoriálem.
...
==4128== 50 bytes in 5 blocks are definitely lost in loss record 1 of 2
==4128== at 0x4C277AB: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==4128== by 0x400691: main (in /home/petr/resource/c/linux/zdrojaky-linux/08testovani/mtrace)
==4128==
...
Valgrind, na rozdíl od mtrace, nevyžaduje úpravu zdrojového kódu (volání funkce
mtrace()
).