Hlavní navigace

Trasování a ladění nativních aplikací v Linuxu

12. 5. 2016
Doba čtení: 19 minut

Sdílet

Seznámíme se s nástroji, které je možné použít pro trasování a ladění nativních aplikací v Linuxu. Probereme utility typu ltrace a strace i nástroje jako SystemTap, DTrace a GDB.

Obsah

1. Trasování a ladění nativních aplikací v Linuxu

2. Utilita ltrace – výpis informací o volaných knihovních funkcích

3. Ukázka použití utility ltrace

4. Zjištění doby trvání volané funkce a filtrace výstupu

5. Zjištění statistiky volaných funkcí

6. Utilita strace – výpis informací o volaných systémových funkcích

7. Ukázka použití utility strace

8. GNU Debugger

9. DTrace

10. SystemTap

11. Ukázky použití SystemTapu

12. Obsah následujícího článku

13. Demonstrační příklady použité v dnešním článku

14. Odkazy na Internetu

1. Trasování a ladění nativních aplikací v Linuxu

V operačním systému Linux můžeme pro ladění a trasování nativních aplikací použít hned několik nástrojů, které se od sebe liší jak způsobem použití, tak i svými možnostmi. Dnes se nejprve zmíníme o dvou základních nástrojích určených především pro sledování volání funkcí z knihoven popř. sledování volání systémových funkcí. Tyto nástroje se jmenují ltrace (kapitola číslo 2) a strace (kapitola číslo 6). Pokud možnosti těchto dvou nástrojů nestačí, je možné použít složitější prostředky typu GNU Debuggeru (kapitola číslo 8), prozatím neoficiální verze nástroje DTrace (kapitole 9) nebo sofistikované utility nazvané SystemTap (jedenáctá kapitola).

Typická aplikace běžící v uživatelském prostoru volá funkce různých (nativních) knihoven, zejména pak knihovny glibc. Funkce z glibc následně volají systémové funkce implementované (zjednodušeně řečeno) v kernelu nebo v jeho modulech:

+----------+
|          |
| aplikace |
|          |
+----------+
     |
     |
     v
+----------+
|          |
|  glibc   |
|          |
+----------+
     |
     |
     v
+----------+
|          |
|  jádro   |
|          |
+----------+

Pro sledování volání funkcí z nativních knihoven se používá ltrace, pro systémové volání strace, samotný GNU Debugger je možné použít pro ladění aplikace i knihoven a SystemTap je použitelný ve všech úrovních, a to včetně sledování stavů jádra operačního systému (KGDB si prozatím popisovat nebudeme):

+----------+
|          |..... gdb
| aplikace |
|          |..... SystemTap
+----------+
     |
     |...... ltrace
     v
+----------+
|          |..... gdb
|  glibc   |
|          |..... SystemTap
+----------+
     |
     |...... strace
     v
+----------+
|          |..... SystemTap
|  jádro   |
|          |..... KGDB
+----------+

2. Utilita ltrace – výpis informací o volaných knihovních funkcích

Prvním nástrojem, s nímž se v dnešním článku ve stručnosti seznámíme, je utilitka nazvaná jednoduše ltrace. Tento nástroj je možné použít ve chvíli, kdy potřebujeme zjistit, které knihovní funkce se volají popř. jak často se tyto funkce volají a kolik času v nich některá aplikace tráví. Takto zjištěné statistické informace lze použít různým způsobem, například pro vyhledání problémových či pomalých částí kódu, zjištění, ve kterých místech dokází k chybě, zjištění způsobů alokace a dealokace paměti (funkce malloc, free a příbuzné), popř. k jednoduchému trasování (což je asi nejběžnější). V tom nejjednodušším případě se ltrace používá následujícím způsobem:

ltrace jméno_binární_aplikace parametry_aplikace

popř. pokud je zapotřebí předat parametry i samotnému nástroji ltrace:

ltrace parametry_ltrace jméno_binární_aplikace parametry_aplikace

3. Ukázka použití utility ltrace

Podívejme se nyní na základní použití nástroje ltrace. Použijeme přitom demonstrační příklady, jejichž zdrojové kódy jsou zmíněny ve třinácté kapitole. Takže začněme:

ltrace ./hello
 
__libc_start_main(0x40053d, 1, 0x7fffd2e1f5f8, 0x400560 <unfinished ...>
puts("Hello world!"Hello world!
)                                                                    = 13
+++ exited (status 0) +++

V tomto jednoduchém příkladu se (přímo) mnoho knihovních funkcí nevolá.

ltrace ./random_bitmap
 
__libc_start_main(0x400bf4, 1, 0x7fff18e8d758, 0x400c70 <unfinished ...>
puts("processing:")                                                                     = 12
malloc(16)                                                                              = 0xa11010
malloc(921600)                                                                          = 0x7f45fdb46010
memset(0x7f45fdb46010, '\0', 921600)                                                    = 0x7f45fdb46010
memset(0x7f45fdb46010, '\0', 921600)                                                    = 0x7f45fdb46010
open("/dev/urandom", 0, 037560470000)                                                   = 3
read(3, "\270\325\332\340r8\304\354\306A\001S", 1920)                                   = 1920
...
...
...
vynecháno přibližně 480 řádků
...
...
...
close(3)                                                                                = 0
fopen("random.bmp", "wb")                                                               = 0xa11030
fwrite("BMF", 54, 1, 0xa11030)                                                          = 1
printf("%d pixels written\n", 307200)                                                   = 22
fwrite("\270\325\332\340r8\304\354\306A\001S", 921600, 1, 0xa11030)                     = 1
fclose(0xa11030)                                                                        = 0
puts("done!\n")                                                                         = 7
+++ exited (status 0) +++

Povšimněte si, že u funkcí se vypisují i jejich parametry. Můžeme zde vysledovat základní logiku aplikace, od alokace paměti pro hlavičku bitmapy i pro celou bitmapu:

bitmap *p=(bitmap *)malloc(sizeof(bitmap)); /* alokace struktury bitmapy */
if (!p) return NULL;
p->width=width;                             /* naplneni struktury */
p->height=height;
p->pixels=(unsigned char *)malloc(3*width*height);

přes čtení náhodných čísel:

int randomDataDevice = open("/dev/urandom", O_RDONLY);
size_t i;
unsigned char *pixels_ptr = p->pixels;
for (i=0; i < p->height; i++)
{
    /* nacteni celeho radku */
    ssize_t result = read(randomDataDevice, pixels_ptr, 3*p->width);
    if (result >= 0)
    {
        /* posun ukazatele na dalsi radek */
        pixels_ptr+=result;
    }
    else
    {
        perror("urandom read failed");
        close(randomDataDevice);
        return;
    }
}
close(randomDataDevice);

Až po závěrečnou sekvenci:

fout=fopen(fileName, "wb");
if (!fout) return;
fwrite(bmp_header, sizeof(bmp_header), 1, fout);
printf("%d pixels written\n", width * height);
fwrite(p->pixels, 3 * width * height, 1, fout);
fclose(fout);
puts("done!\n");

Poslední příklad:

ltrace ./fractal_renderer
 
__libc_start_main(0x400ec2, 1, 0x7fff0293d968, 0x400f60 <unfinished ...>
puts("processing:")                                                                     = 12
malloc(16)                                                                              = 0xf7f010
malloc(921600)                                                                          = 0x7fb63dc5a010
memset(0x7fb63dc5a010, '\0', 921600)                                                    = 0x7fb63dc5a010
memset(0x7fb63dc5a010, '\0', 921600)                                                    = 0x7fb63dc5a010
sin(0xf7f010, 1000, 0, 0xf7f010)                                                        = 0x3fa11111
...
...
...
vynechána další volání funkce sin - 614400x !!!
...
...
...
fopen("result.bmp", "wb")                                                               = 0xf7f030
fwrite("BMF", 54, 1, 0xf7f030)                                                          = 1
printf("%d pixels written\n", 307200)                                                   = 22
fwrite("z\036zz\036zz\036zz\036zz\036zz\036zz\036zz\036zz\036zz\036zz\036"..., 921600, 1, 0xf7f030) = 1
fclose(0xf7f030)                                                                        = 0
puts("done!\n")                                                                         = 7
+++ exited (status 0) +++

Zde můžeme vidět, že jasně dominuje volání funkce sin z knihovny libm.

4. Zjištění doby trvání volané funkce a filtrace výstupu

Pokud potřebujete zjistit čas volání nějaké funkce, je možné použít přepínač -t. Výsledek pak vypadá takto:

ltrace -t ./hello
 
20:17:06 __libc_start_main(0x40053d, 1, 0x7fff74550708, 0x400560 <unfinished ...>
20:17:06 puts("Hello world!"Hello world!
)                                                           = 13
20:17:06 +++ exited (status 0) +++

Pokud se namísto přepínače -t použije přepínač -tt, zvětší se přesnost na milisekundy:

ltrace -tt ./hello
 
20:49:54.521242 __libc_start_main(0x40053d, 1, 0x7fff78e06dd8, 0x400560 <unfinished ...>
20:49:54.521862 puts("Hello world!"Hello world!
)                                                  = 13
20:49:54.522886 +++ exited (status 0) +++

Samozřejmě je možné zjistit nejenom okamžik, kdy se do funkce vstupuje, ale i dobu trvání knihovních funkcí, a to díky přepínači -r:

ltrace -r ./hello
 
  0.000000 __libc_start_main(0x40053d, 1, 0x7fffdd8d7208, 0x400560 <unfinished ...>
  0.000479 puts("Hello world!"Hello world!
)                                                         = 13
  0.000985 +++ exited (status 0) +++

Mnohdy nepotřebujeme zjistit všechny volané knihovní funkce, ale pouze vybranou podmnožinu z nich. Zde přichází ke slovu přepínač -e, kterému můžeme předat seznam funkcí, které nás zajímají. Funkce se zapisují ve formě jednoduchého jazyka, což například znamená, že pro filtraci těch funkcí, jejichž volání nás zajímá, se vkládají znaky +:

ltrace -e malloc+free+open+close ./random_bitmap
 
processing:
random_bitmap->malloc(16)                                                               = 0x2137010
random_bitmap->malloc(921600)                                                           = 0x7f4fb333c010
random_bitmap->open("/dev/urandom", 0, 026320350000)                                    = 3
random_bitmap->close(3)                                                                 = 0
307200 pixels written
done!
 
+++ exited (status 0) +++

Naopak je možné při použití znaku – (minus) odfiltrovat ty knihovní funkce, které nás v daném okamžiku nezajímají. Řekněme, že nepotřebujeme sledovat volání funkce read:

ltrace -e -read ./random_bitmap
 
random_bitmap->__libc_start_main(0x400bf4, 1, 0x7fff7c9456e8, 0x400c70 <unfinished ...>
random_bitmap->puts("processing:"processing:
)                                                      = 12
random_bitmap->malloc(16 <unfinished ...>
libc.so.6->(0x7f7140a97bd0, 0x7fff7c945550, 0x7fff7c945540, 0)                          = 0x7f7140ffa4c0
<... malloc resumed> )                                                                  = 0x1ee3010
random_bitmap->malloc(921600)                                                           = 0x7f7140ef3010
random_bitmap->memset(0x7f7140ef3010, '\0', 921600)                                     = 0x7f7140ef3010
random_bitmap->memset(0x7f7140ef3010, '\0', 921600)                                     = 0x7f7140ef3010
random_bitmap->open("/dev/urandom", 0, 010077240000)                                    = 3
random_bitmap->close(3)                                                                 = 0
random_bitmap->fopen("random.bmp", "wb" <unfinished ...>
libc.so.6->memalign(568, 0x400cf4, 1, 0)                                                = 0x1ee3030
<... fopen resumed> )                                                                   = 0x1ee3030
random_bitmap->fwrite("BMF", 54, 1, 0x1ee3030)                                          = 1
random_bitmap->printf("%d pixels written\n", 307200307200 pixels written
)                                    = 22
random_bitmap->fwrite("Z\226\314H7\316\301\306\340\367iI\324O\332|\005\035![BL\325u\271\335L", 921600, 1, 0x1ee3030) = 1
random_bitmap->fclose(0x1ee3030 <unfinished ...>
libc.so.6->(0x1ee3030, 0, 0x1ee3110, 0xfbad000c)                                        = 1
<... fclose resumed> )                                                                  = 0
random_bitmap->puts("done!\n"done!
 
)                                                          = 7
libc.so.6->_dl_find_dso_for_object(0x7f7140dd1d90, 0x7f7140dd26c8, 1, -1)               = 0x7f7140fd5690
+++ exited (status 0) +++

5. Zjištění statistiky volaných funkcí

Velmi užitečná je volba -c, po jejímž zadání nástroj ltrace zobrazí statistiku volání jednotlivých funkcí s celkovým i průměrným časem, který program v dané funkci strávil. Opět si uveďme příklady:

ltrace -c ./hello
 
Hello world!
 
% time     seconds  usecs/call     calls      function
------ ----------- ----------- --------- --------------------
100.00    0.000455         455         1 puts
------ ----------- ----------- --------- --------------------
100.00    0.000455                     1 total

Zde asi nedošlo k žádnému většímu překvapení.

ltrace -c ./random_bitmap
 
% time     seconds  usecs/call     calls      function
------ ----------- ----------- --------- --------------------
 97.81    0.228395         475       480 read
  0.73    0.001701         850         2 fwrite
  0.44    0.001032         516         2 memset
  0.29    0.000670         335         2 puts
  0.24    0.000559         279         2 malloc
  0.13    0.000299         299         1 fclose
  0.10    0.000245         245         1 fopen
  0.10    0.000231         231         1 printf
  0.09    0.000214         214         1 open
  0.07    0.000174         174         1 close
------ ----------- ----------- --------- --------------------
100.00    0.233520                   493 total

Povšimněte si, že funkce read() se volala přesně 480×, což odpovídá zdrojovému kódu:

/* rozmery bitmapy */
#define BITMAP_WIDTH    640
#define BITMAP_HEIGHT   480
...
...
...
void fillBitmap(const bitmap *p)
{
    int randomDataDevice = open("/dev/urandom", O_RDONLY);
    size_t i;
    unsigned char *pixels_ptr = p->pixels;
    for (i=0; i < p->height; i++)
    {
        /* nacteni celeho radku */
        ssize_t result = read(randomDataDevice, pixels_ptr, 3*p->width);
        if (result >= 0)
        {
            /* posun ukazatele na dalsi radek */
            pixels_ptr+=result;
        }
        else
        {
            perror("urandom read failed");
            close(randomDataDevice);
            return;
        }
    }
    close(randomDataDevice);
}
ltrace -c ./fractal_renderer
 
processing:
307200 pixels written
done!
 
% time     seconds  usecs/call     calls      function
------ ----------- ----------- --------- --------------------
 99.99   72.237624         117    614400 sin
  0.00    0.001307         653         2 fwrite
  0.00    0.000695         347         2 puts
  0.00    0.000672         336         2 memset
  0.00    0.000487         487         1 fclose
  0.00    0.000409         409         1 fopen
  0.00    0.000338         169         2 malloc
  0.00    0.000160         160         1 printf
------ ----------- ----------- --------- --------------------
100.00   72.241692                614411 total

Podle očekávání (viz čtvrtou kapitolu) nás určitě nepřekvapí, že nejvíce času se celkově (kumulativně) strávilo ve funkci sin, i když tato funkce nepatří mezi nejpomalejší (ve skutečnosti je ze všech volaných funkcí nejrychlejší).

6. Utilita strace – výpis informací o volaných systémových funkcích

Druhým nástrojem, s nímž se dnes seznámíme, je utilita nazvaná příhodně strace. Zatímco výše popsaná ltrace sloužila k výpisu volaných knihovních funkcí (pocházejících například z knihovny glibc), je utilita strace určena ke zjištění systémových volání, a to nezávisle na tom, kde toto volání vzniklo (většinou se jedná opět o knihovnu glibc, ovšem nemusí tomu tak být vždycky. Podobně jako v případě ltrace je možné nástroj strace použít buď pro spuštění laděné/trasované aplikace, nebo je možné se přes přepínač -p{pid}) připojit k již běžící aplikaci (to je velmi užitečné například při sledování běhu „živých“ dlouhoběžících serverových aplikací apod.). Význam některých přepínačů je u straceltrace shodný, což je samozřejmě výhodné.

7. Ukázka použití utility strace

Podívejme se nyní na základní způsob použití strace. Pro jednoduchost ji vyzkoušíme pouze oproti programu Hello world, který ve svém kódu volá jedinou funkci puts a nic jiného. Ovšem z pohledu systémových volání je tento program mnohem složitější, o čemž se lze velmi snadno přesvědčit:

strace ./hello
 
execve("./hello", ["./hello"], [/* 53 vars */]) = 0
brk(0)                                  = 0xa52000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1af21d000
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=132679, ...}) = 0
mmap(NULL, 132679, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd1af1fc000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/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\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0
mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd1aec38000
mprotect(0x7fd1aedf3000, 2093056, PROT_NONE) = 0
mmap(0x7fd1aeff2000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7fd1aeff2000
mmap(0x7fd1aeff8000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd1aeff8000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1af1fb000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1af1f9000
arch_prctl(ARCH_SET_FS, 0x7fd1af1f9740) = 0
mprotect(0x7fd1aeff2000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7fd1af21f000, 4096, PROT_READ) = 0
munmap(0x7fd1af1fc000, 132679)          = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 7), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1af21c000
write(1, "Hello world!\n", 13Hello world!
)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

Povšimněte si způsobu výpisu – u každého systémového volání jsou uvedeny parametry, a to v některých případech inteligentně – namísto číselné konstanty se používá symbolická konstanta PROT_READ atd. Taktéž se vypisuje návratová hodnota volání, a to opět (pokud je to možné) formou symbolické konstanty (ENOENT) popř. zprávy („No such file or directory“.). Na konci můžeme vidět, že volání puts se provede formou write na standardní výstup (vrátí se počet zapsaných/vytištěných bajtů).

8. GNU Debugger

Od relativně jednoduchých a snadno použitelných nástrojů určených pro „pouhé“ trasování se na chvíli přesuňme k debuggerům, což jsou složitější, ovšem také mnohem mocnější nástroje. Pro operační systém Linux existuje větší množství debuggerů, včetně několika komerčních nástrojů (příkladem může být Affinic a TotalView), ovšem nejpoužívanějším debuggerem je pravděpodobně stále GNU Debugger neboli GDB, jehož první verze vznikla již v roce 1986, takže se po třiceti letech kontinuálního vývoje jedná o velmi vyzrálý produkt. GNU Debugger byl v průběhu svého vývoje portován jak na mnoho operačních systémů (většinou na různé komerční i nekomerční varianty Unixu, ovšem nalezneme ho například i v systému DOS), tak i na nepřeberné množství procesorových a mikroprocesorových architektur, z nichž jmenujme především řadu x86, x86_64, ARM (prakticky všechny 32bitové CPU i nová 64bitová jádra), Motorola 68HC11, MIPS či PowerPC.

Tento debugger podporuje všechny překladače z rodiny GNU, což mj. znamená, že dokáže zobrazit a pracovat se zdrojovými kódy v jazycích Ada, C, C++, Go, Objective-C, D, Fortran, Modula-2, Pascal a Java (ovšem jen v případě překladu Javy do nativního kódu). Na základě jazyka, v němž je laděný program napsán, se upravují i zprávy GNU Debuggeru, takže se například používá správný formát hexadecimálních čísel, struktur záznamů atd. Taktéž assemblery používané na Linuxu GNU Debugger přímo podporují (jedná se jak o as, tak i o NASM). Ladicí nástroj GNU Debugger primárně používá ke komunikaci s uživatelem příkazový řádek, alternativně lze použít i protokol pro nepřímé ovládání debuggeru (tuto technologii používají různé nadstavby nad debuggerem) a v případě potřeby je možné k laděné aplikaci přidat relativně krátký „stub“ sloužící pro přímé ladění takové aplikace (touto nepochybně zajímavou problematikou se však dnes nebudeme zabývat).

Většina často používaných příkazů má i svoji zkrácenou podobu (bt=backtrace, c=continue, f=frame) a navíc je možné používat klávesu [Tab] pro automatické doplnění celého jména příkazu. Pokud je správně nastavený terminál, bude fungovat i historie příkazového řádku, a to stejným způsobem, jaký známe ze shellu. Alternativně je možné využít gdbtui s celoobrazovkovým výstupem a přiblížit se tak možnostem debuggerů s plnohodnotným grafickým uživatelským rozhraním. Pokud se GNU Debugger používá pro trasování, lze do kódu vkládat takzvané tracepoints. Ty slouží pro zjištění stavu programu v nějakém specifikovaném bodu, ovšem bez (po)zastavení programu. Samotné pozastavení programu totiž může v některých případech způsobit jeho chybnou činnost či naopak zastínit některé chyby vyplývající ze špatně implementované synchronizace vláken či při přístupu ke sdíleným prostředkům. Podrobnosti o této užitečné technologii si řekneme v navazujícím článku.

9. DTrace

Dalším nástrojem, s nímž se jako vývojáři či administrátoři můžeme na některých systémech setkat, je DTrace. Tento nástroj původně vznikl pro operační systém Solaris (dokonce se mělo jednat o jeden z klíčových nástrojů, kvůli kterému si měli zákazníci koupit Solaris a nikoli přecházet na Linux) a i přes určitou snahu společnosti Oracle o konverzi DTrace pro Linux zatím tento nástroj v Linuxu oficiálně není podporovaný a existuje pouze v beta verzi (dodává přímo Oracle) nebo v neoficiální verzi dostupné na GitHubu. DTrace je založen na použití takzvaných „sond“ (probe), což jsou z hlediska uživatele (většinou) krátké skripty spuštěné ve chvíli, kdy dojde k nějaké předem definované události, a to buď přímo v jádře operačního systému nebo v laděné aplikaci. Samotná sonda (skript) má přístup k zásobníkovému rámci a tedy i k lokálním proměnným a parametrům předaným volané funkci.

Příklad naprogramované sondy:

#pragma D option flowindent
 
syscall::write:entry
/pid == $target/
{
        printf("Written %d bytes\n", arg2);
}

Pro zápis sond se sice používá programovací jazyk nazvaný D, ovšem jedná se o „jiné D“, než http://dlang.org/. Jazyk D použitý v DTrace je sice syntakticky taktéž podobný jazyku C, ovšem sémanticky se jedná o zcela odlišné jazyky s různými pravidly i překladači. Podrobnější popis tohoto jazyka lze nalézt na stránce http://docs.oracle.com/cd/E53394_01/html/E­53395/gkwpo.html

10. SystemTap

Nástroj SystemTap je v některých ohledech podobný výše zmíněné utilitě DTrace. I v SystemTapu se s využitím programovacího jazyka deklarují takzvané sondy (probe) a určuje se, za jakých okolností se mají tyto sondy provést. SystemTap lze použít jak pro zjišťování procesů probíhajících v kernelu, tak i v uživatelském prostoru (userspace). Sledovat lze například sycally (systémová volání), operace nad soubory, vstupy do funkcí na všech úrovních atd. Poněkud složitější bývala instalace SystemTapu (především kvůli nutnosti operovat s ladicími symboly jádra), to se však zlepšilo přidáním utility stap-prep. Předpokládejme, že balíček SystemTap je nainstalovaný. Potom příkaz stap-prep zjistí, jaké další balíčky je nutné doinstalovat a kde je získat (u Ubuntu se jedná o jiné kanály atd.):

stap-prep
 
Need to install the following packages:
kernel-devel-3.15.6-200.fc20.x86_64
kernel-debuginfo-3.15.6-200.fc20.x86_64

V některých případech stap-prep instalaci sám provede. Vezměme si příklad z Fedory:

stap-prep
 
Need to install the following packages:
kernel-devel-4.4.8-300.fc23.x86_64
kernel-debuginfo-4.4.8-300.fc23.x86_64
Yum command has been deprecated, redirecting to '/usr/bin/dnf install -y --enablerepo=* kernel-devel-4.4.8-300.fc23.x86_64 kernel-debuginfo-4.4.8-300.fc23.x86_64'.
See 'man dnf' and 'man yum2dnf' for more information.
To transfer transaction metadata from yum to DNF, run:
'dnf install python-dnf-plugins-extras-migrate && dnf-2 migrate'
Fedora 23 - x86_64 - Test Updates                                                     50 MB/s | 2.4 MB     00:00
Fedora 23 - x86_64 - Debug                                                            46 MB/s |  11 MB     00:00
Fedora 23 - Test Updates Source                                                      4.1 MB/s | 330 kB     00:00
Fedora 23 - Updates Source                                                            14 MB/s | 2.5 MB     00:00
Fedora 23 - x86_64 - Updates - Debug                                                  45 MB/s | 6.2 MB     00:00
Fedora 23 - x86_64                                                                    52 MB/s |  43 MB     00:00
Fedora 23 - x86_64 - Test Updates Debug                                               34 MB/s | 885 kB     00:00
Fedora 23 - Source                                                                   8.3 MB/s | 5.9 MB     00:00
Last metadata expiration check: 0:00:03 ago on Wed May 11 16:05:54 2016.
Dependencies resolved.
===================================================================================================================
 Package                                  Arch        Version             Repository                          Size
===================================================================================================================
Installing:
 kernel-debuginfo                         x86_64      4.4.8-300.fc23      updates-debuginfo                  379 M
 kernel-debuginfo-common-x86_64           x86_64      4.4.8-300.fc23      updates-debuginfo                   53 M
 kernel-devel                             x86_64      4.4.8-300.fc23      updates                             10 M
 
Transaction Summary
===================================================================================================================
Install  3 Packages
 
Total download size: 443 M
Installed size: 2.0 G
Downloading Packages:
(1/3): kernel-devel-4.4.8-300.fc23.x86_64.rpm                                       26 MB/s |  10 MB     00:00
(2/3): kernel-debuginfo-common-x86_64-4.4.8-300.fc23.x86_64.rpm                     26 MB/s |  53 MB     00:02
(3/3): kernel-debuginfo-4.4.8-300.fc23.x86_64.rpm                                   26 MB/s | 379 MB     00:14
--------------------------------------------------------------------------------------------------------------
Total                                                                               27 MB/s | 443 MB     00:16
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Installing  : kernel-debuginfo-common-x86_64-4.4.8-300.fc23.x86_64                                     1/3
  Installing  : kernel-debuginfo-4.4.8-300.fc23.x86_64                                                   2/3
  Installing  : kernel-devel-4.4.8-300.fc23.x86_64                                                       3/3
  Verifying   : kernel-devel-4.4.8-300.fc23.x86_64                                                       1/3
  Verifying   : kernel-debuginfo-4.4.8-300.fc23.x86_64                                                   2/3
  Verifying   : kernel-debuginfo-common-x86_64-4.4.8-300.fc23.x86_64                                     3/3
 
Installed:
  kernel-debuginfo.x86_64 4.4.8-300.fc23                            kernel-debuginfo-common-x86_64.x86_64 4.4.8-300.fc23                            kernel-devel.x86_64 4.4.8-300.fc23
 
Complete!
kernel-devel-4.4.8-300.fc23.x86_64
kernel-debuginfo-4.4.8-300.fc23.x86_64

Po instalaci se může provést kontrola, která má dle oficiální dokumentace vypadat takto:

stap -v -e 'probe vfs.read {printf("read performed\n"); exit()}'
 
Pass 1: parsed user script and 118 library scripts using 233764virt/36948res/7372shr/29452data kb, in 200usr/30sys/471real ms.
Pass 2: analyzed script: 1 probe, 1 function, 4 embeds, 0 globals using 372776virt/170852res/8824shr/168464data kb, in 3560usr/400sys/4282real ms.
Pass 3: translated to C into "/tmp/stapWy5VWo/stap_5ec9a356f694741098a16e111db111c9_1654_src.c" using 372776virt/171052res/9024shr/168464data kb, in 10usr/10sys/19real ms.
Pass 4: compiled C into "stap_5ec9a356f694741098a16e111db111c9_1654.ko" in 16650usr/3190sys/20283real ms.
Pass 5: starting run.
read performed
Pass 5: run completed in 20usr/60sys/440real ms.

11. Ukázky použití SystemTapu

Podrobnější informace o SystemTapu si uvedeme příště, takže si dnes jen pro ilustraci uveďme několik jednoduchých sond.

Sonda, která ihned po svém spuštění (begin) vypíše zprávu na standardní výstup a ukončí se. Povšimněte si, že se syntaxe zápisu podobá standardnímu céčku, což samozřejmě není náhoda:

probe begin
{
    printf ("hello world\n")
    exit ()
}

Spuštění této sondy:

stap hello.stp
 
hello world

Poněkud složitější sonda, která pozdraví uživatele jeho ID:

probe begin
{
  printf ("hello %d\n", uid())
  exit ()
}

Spuštění této sondy (spuštěno pod rootem, takže UID by mělo být 0):

stap hello2.stp
 
hello 0

Sonda reagující na časovač – po každé sekundě se vypíše zpráva:

probe timer.ms(1000)
{
  printf ("tiktak\n")
}

Spuštění této sondy:

stap clock.stp
 
tiktak
tiktak
tiktak

Výpis všech typů sond, které je možné použít:

stap  --dump-probe-types

Prozatím nejsložitější příklad – reakce na spuštění programu nazvaného hello, reakce na ukončení tohoto programu a taktéž reakce na vstup do funkce main v tomto programu:

probe process("hello").begin
{
  printf ("started\n")
}
 
 
 
probe process("hello").end
{
    printf ("finished\n")
}
 
 
 
probe process("hello").function("main")
{
    printf ("main function\n")
}

Sondu zaregistrujeme:

stab process.stp

a následně v jiném terminálu spustíme náš program typu Hello world!:

./hello

Ten se spustí, vypíše zprávu a ukončí se, zatímco sonda na prvním terminálu zareaguje následovně:

started
main function
finished

12. Obsah následujícího článku

V následující části tohoto článku si řekneme podrobnější informace o možnostech GNU Debuggeru (což je jedna z nejpropracovanějších GNU utilit vůbec) a taktéž o SystemTapu. V případě SystemTapu si ukážeme složitější sondy sloužící mj. i pro zjišťování různých statistických informací, pro profilování aplikace atd.

13. Demonstrační příklady použité v dnešním článku

Všechny tři demonstrační příklady, které jsme používali při popisu nástrojů ltrace, straceSystemTap, byly uloženy do GIT repositáře nazvaného https://github.com/tisnik/pre­sentations:

Způsob překladu těchto tří příkladu je ve skutečnosti velmi jednoduchý:

gcc -ansi -pedantic -Wall -o hello hello.c
gcc -ansi -pedantic -Wall -o random_bitmap random_bitmap.c
gcc -ansi -pedantic -Wall -o fractal_renderer fractal_renderer.c -lm

Poznámka: u posledního příkladu nezapomeňte přilinkovat knihovnu s matematickými funkcemi libm.

ict ve školství 24

Obrázek 1: Výsledek aplikace random_bitmap.

Obrázek 2: Výsledek aplikace fractal_renderer.

14. Odkazy na Internetu

  1. Tracing (software)
    https://en.wikipedia.org/wi­ki/Tracing_%28software%29
  2. ltrace(1) – Linux man page
    http://linux.die.net/man/1/ltrace
  3. ltrace (Wikipedia)
    https://en.wikipedia.org/wiki/Ltrace
  4. strace(1) – Linux man page
    http://linux.die.net/man/1/strace
  5. strace (stránka projektu na SourceForge)
    https://sourceforge.net/pro­jects/strace/
  6. strace (Wikipedia)
    https://en.wikipedia.org/wiki/Strace
  7. SystemTap (stránka projektu)
    https://sourceware.org/systemtap/
  8. SystemTap (Wiki projektu)
    https://sourceware.org/systemtap/wiki
  9. SystemTap (Wikipedia)
    https://en.wikipedia.org/wi­ki/SystemTap
  10. Dynamic Tracing with DTrace & SystemTap
    http://myaut.github.io/dtrace-stap-book/
  11. DTrace (Wikipedia)
    https://en.wikipedia.org/wiki/DTrace
  12. GDB – Dokumentace
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/
  13. GDB – Supported Languages
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/Suppor­ted-Languages.html#Supported-Languages
  14. GNU Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Debugger
  15. The LLDB Debugger
    http://lldb.llvm.org/
  16. Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/Debugger
  17. 13 Linux Debuggers for C++ Reviewed
    http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817
  18. Getting started with ltrace: how does it do that?
    https://www.ellexus.com/getting-started-with-ltrace-how-does-it-do-that/
  19. Reverse Engineering Tools in Linux – strings, nm, ltrace, strace, LD_PRELOAD
    http://www.thegeekstuff.com/2012/03/re­verse-engineering-tools/
  20. 7 Strace Examples to Debug the Execution of a Program in Linux
    http://www.thegeekstuff.com/2011/11/stra­ce-examples/
  21. Oracle® Solaris 11.3 DTrace (Dynamic Tracing) Guide
    http://docs.oracle.com/cd/E53394_01/html/E­53395/gkwpo.html#scrolltoc

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.