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

9. 6. 2016
Doba čtení: 24 minut

Sdílet

Ve čtvrté části seriálu o trasování a ladění aplikací v Linuxu si ukážeme použití nástroje SystemTap, s nímž jsme se ve stručnosti seznámili již v úvodním článku.

Obsah

1. Trasování a ladění nativních aplikací v Linuxu: použití nástroje SystemTap

2. Instalace nástroje SystemTap

3. Vytvoření jednoduché sondy aneb pět možností zápisu „Hello world!“

4. Implicitní a explicitní řízení pořadí spuštění sond

5. Sondy zavolané při spuštění nebo ukončení programu

6. Detekce systémových volání (syscallů)

7. Vylepšení předchozího programu – detekce správného čísla procesu

8. Výpis základních informací o syscallu (volané služby jádra)

9. Přístup k bufferu použitém v syscallu write

10. Opravení předchozího příkladu: výpočet skutečné délky řetězce před jeho výpisem

11. Specifikace jména sledovaného programu z příkazové řádky

12. Výpis volaných funkcí

13. Zjištění, odkud se funkce volá (backtrace)

14. Odkazy na Internetu

1. Trasování a ladění nativních aplikací v Linuxu: použití nástroje SystemTap

Ve čtvrté části (mini)seriálu o trasování a ladění nativních aplikací v Linuxu se budeme zabývat popisem způsobu použití všestranného nástroje nazvaného SystemTap. Tento nástroj lze použít v mnoha oblastech a může být užitečný jak pro vývojáře (detekce problémů ve vyvíjené aplikaci či knihovně), tak i pro administrátory (zjištění statistik o síťovém provozu, vytížení souborového systému, případných „divných“ volání funkcí jádra, volání funkcí ze sledovaných knihoven atd.). SystemTap se dokonce používá v součinnosti s JVM pro trasování volaných Javovských metod apod., ovšem touto bezpochyby zajímavou funkcionalitou se budeme zabývat v samostatném článku, protože v tomto případě se nejedná o nativní aplikace, ale o aplikace určené pro virtuální stroj Javy. I díky těmto vlastnostem dokáže SystemTap v mnoha případech nahradit podobně koncipovaný nástroj nazvaný DTrace, který možná znají uživatelé Solarisu.

V nástroji SystemTap se pracuje s takzvanými sondami („probe“), které mohou sledovat různé události, ke kterým v systému dochází. Sondy lze nastavit jak na úrovni sledované aplikace (vstupy do funkcí…), tak i knihoven (volání knihovních funkcí…) a jádra (syscally, souborový systém, síťový subsystém):

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

2. Instalace nástroje SystemTap

Instalace nástroje SystemTap se skládá z několika kroků. V prvním kroku je nutné s využitím správce balíčků (apt-get, yum atd.) nainstalovat balíček nazvaný systemtap popř. u multilib instalace balíček systemtap.x86_64. Na mém testovacím systému s Fedorou 22 se jedná o tento balíček:

Name        : systemtap
Arch        : x86_64
Epoch       : 0
Version     : 3.0
Release     : 2.fc22
Size        : 206 k
Repo        : @System
From repo   : fedora-updates
Summary     : Programmable system-wide instrumentation system
URL         : http://sourceware.org/systemtap/
License     : GPLv2+
Description : SystemTap is an instrumentation system for systems running Linux.
            : Developers can write instrumentation scripts to collect data on
            : the operation of the system.  The base systemtap package contains/requires
            : the components needed to locally develop and execute systemtap scripts.

Pokud je již balíček systemtap nainstalovaný, použijeme utilitu nazvanou stap-prep, která zjistí, jaké další součásti systému jsou ještě potřeba. Tato utilita je uzpůsobena pro konkrétní distribuci, takže se například chová odlišně na Fedoře (kde jsou všechny potřebné balíčky připraveny již v základním kanálu) a na Ubuntu (rozdílné kanály atd.). Tato utilita se spustí bez parametrů:

stap-prep

Jak jsme si již řekli, tento skript po svém spuštění nejdříve ověří, které další balíčky je nutné do operačního systému doinstalovat a následně zavolá příslušného správce balíčků (varování o použití zastaralého příkazu yum namísto novějšího dnf můžeme v tuto chvíli ignorovat):

Need to install the following packages:
kernel-devel-4.4.11-200.fc22.x86_64
kernel-debuginfo-4.4.11-200.fc22.x86_64
Yum command has been deprecated, redirecting to '/usr/bin/dnf install -y --enablerepo=* kernel-devel-4.4.11-200.fc22.x86_64 kernel-debuginfo-4.4.11-200.fc22.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'
 
Last metadata expiration check performed 0:00:11 ago on Tue Jun  7 15:20:06 2016.
Dependencies resolved.
================================================================================
 Package                        Arch   Version          Repository         Size
================================================================================
Installing:
 kernel-debuginfo               x86_64 4.4.11-200.fc22  updates-debuginfo 379 M
 kernel-debuginfo-common-x86_64 x86_64 4.4.11-200.fc22  updates-debuginfo  54 M
 kernel-devel                   x86_64 4.4.11-200.fc22  updates            10 M
 
Transaction Summary
================================================================================
Install  3 Packages
 
Total download size: 442 M
Installed size: 2.0 G

Z předchozího výpisu můžeme vidět, že se budou stahovat a instalovat velmi objemné balíčky s ladicími informacemi jádra. Celková velikost má po rozbalení přesáhnout dva gigabajty! (jádro se svými jeho moduly se stává pěkně vypaseným bumbrlíčkem :-).

Následuje vlastní instalace a ověřování balíčků:

Total                                           7.0 MB/s | 442 MB     01:02
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Installing  : kernel-debuginfo-common-x86_64-4.4.11-200.fc22.x86_64       1/3
  Installing  : kernel-debuginfo-4.4.11-200.fc22.x86_64                     2/3
  Installing  : kernel-devel-4.4.11-200.fc22.x86_64                         3/3
  Verifying   : kernel-devel-4.4.11-200.fc22.x86_64                         1/3
  Verifying   : kernel-debuginfo-4.4.11-200.fc22.x86_64                     2/3
  Verifying   : kernel-debuginfo-common-x86_64-4.4.11-200.fc22.x86_64       3/3
 
Installed:
  kernel-debuginfo.x86_64 4.4.11-200.fc22
  kernel-debuginfo-common-x86_64.x86_64 4.4.11-200.fc22
  kernel-devel.x86_64 4.4.11-200.fc22
 
Complete!
kernel-devel-4.4.11-200.fc22.x86_64
kernel-debuginfo-4.4.11-200.fc22.x86_64

Nyní by již vše mělo být připraveno na použití SystemTapu. Pro jednoduchost budeme „sondy“ spouštět po rootem resp. pod uživatelem s efektivními právy roota. Ověřme si, zda SystemTap skutečně pracuje korektně, a to skriptem převzatým z oficiální dokumentace:

stap -v -e 'probe vfs.read {printf("read performed\n"); exit()}'
 
Pass 1: parsed user script and 118 library scripts using 220380virt/37700res/6832shr/31052data kb, in 170usr/0sys/176real ms.
Pass 2: analyzed script: 1 probe, 1 function, 4 embeds, 0 globals using 377596virt/190588res/8776shr/188268data kb, in 1670usr/130sys/1795real ms.
Pass 3: translated to C into "/tmp/stapZoFUBy/stap_15ce19c7f74e555a3f30d77e16514cf3_1661_src.c" using 377596virt/190840res/9028shr/188268data kb, in 10usr/0sys/5real ms.
Pass 4: compiled C into "stap_15ce19c7f74e555a3f30d77e16514cf3_1661.ko" in 1900usr/450sys/2228real ms.
Pass 5: starting run.
read performed
Pass 5: run completed in 10usr/30sys/336real ms.

Přepínač -v zapíná „ukecaný“ režim (ony řádky začínající na „Pass“) a pomocí přepínače -e lze sondu zapsat přímo na příkazovém řádku.

3. Vytvoření jednoduché sondy aneb pět možností zápisu „Hello world!“

Podívejme se nyní, jakým způsobem lze vytvořit velmi jednoduchou sondu, která vlastně nebude provádět žádné skutečné trasování ani sledování systémových volání, ale pouze po svém spuštění vypíše na standardní výstup řetězec „Hello world!“ a následně se korektně ukončí. Jedna z variant zápisu této sondy vypadá takto:

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

Sonda typu begin se spustí po inicializaci SystemTapu, takže právě zde je možné naplnit případné datové struktury, zjistit informace o trasovaném systému atd. Opakem je podle očekávání sonda typu end.

Zdrojový kód sondy uložíme do souboru nazvaného „hello.stp“ a následně sondu spustíme:

stap hello.stp
 
hello world

Pravděpodobně jste si povšimli, že se zdrojový kód sondy nápadně podobá deklaraci céčkové funkce, což není náhoda, ale záměr autorů SystemTapu. Céčku se můžeme ještě více přiblížit použitím středníků, které sice nejsou nutné, ale například textový editor Vim je při editaci souborů SystemTapu nastaven do režimu editace céčka, takže bez použití středníků špatně volí odsazení následujících řádků (středníky je taktéž nutné použít při zápisu většího množství příkazů na jednom řádku):

probe begin
{
    printf("Hello world!\n");
    exit();
}

Další úprava může spočívat v náhradě funkce printf (opět známé z céčka) za jednodušší funkci println, která pouze vytiskne svůj parametr a provede odřádkování. Pro program typu „Hello world!“ je použití funkce println samozřejmě jednodušší:

probe begin
{
    println("Hello world!");
    exit();
}

Spuštění této sondy stále povede ke stejnému výsledku:

stap hello.stp
 
hello world

Již na konci předchozí kapitoly jsme si ukázali způsob zápisu celého popisu sondy přímo na příkazovém řádku. Pro jednodušší sondy může být tento způsob výhodnější, ovšem nesmíme zde zapomenout na středníky:

stap -e 'probe begin {printf("Hello world!\n");exit();}'
 
Hello world!

Poslední způsob zápisu používá speciální sondu typu „oneshot“, která se provede pouze jedenkrát a tudíž zde nemusí být uveden příkaz exit():

probe oneshot
{
    println("Hello world!");
}

Mezi podobné speciální typy sond patří: never, end, init, ioblock či scsi.

Pro výpis všech typů sond použijte příkaz:

stap --dump-probe-types

(výstup tohoto příkazu neuvádím, protože se liší podle verze SystemTapu)

4. Implicitní a explicitní řízení pořadí spuštění sond

Náš zdrojový kód s jedinou sondou, která vypíše řetězec „Hello world!“ a následně se ukončí, můžeme rozšířit a ukázat si další vlastnost SystemTapu. Sond typu begin je totiž možné deklarovat libovolné množství, takže si celý výpis a ukončení uměle rozdělíme na čtyři části (povšimněte si, že namísto příkazu println používáme v prvních dvou sondách příkaz print, který neprovádí odřádkování):

probe begin
{
    print("Hello");
}
 
probe begin
{
    print(" world");
}
 
probe begin
{
    println(" !");
}
 
probe begin
{
    exit();
}

Díky tomuto rozdělení je například možné u složitějších skriptů inicializaci provádět podle funkčních celků. Dokonce ani není nutné dodržet přesné pořadí sond ve zdrojovém kódu, protože u každé sondy lze zapsat celé číslo (i záporné), kterým se pořadí definuje explicitně. Pokusme se nyní sondy ve zdrojovém textu prohodit a uvidíme, že po spuštění se stejně budou spouštět takovým způsobem, aby jejich indexy tvořily rostoucí řadu (resp. přesněji řečeno neklesající řadu, protože je možné pro více sond použít stejná čísla – potom záleží na pořadí jejich zápisu ve zdrojovém textu):

probe begin(3)
{
    println(" !");
}
 
probe begin(100)
{
    exit();
}
 
probe begin(2)
{
    print(" world");
}
 
probe begin(1)
{
    print("Hello");
}

5. Sondy zavolané při spuštění nebo ukončení programu

Sondu typu begin a nepřímo taktéž sondu typu end jsme si již ukázali v předchozích dvou kapitolách. V praxi se velmi často používá poněkud odlišný typ sondy nazvaný process(jméno_procesu).begin a process(jméno_procesu).end. Tyto sondy se spustí ve chvíli, kdy je detekováno spuštění či naopak ukončení procesu se zadaným jménem (jméno může být zadáno bez cesty či explicitně s cestou). Podívejme se na jednoduchý příklad detekce spuštění a zastavení příkazu ls:

probe process("ls").begin
{
    println("ls started");
}
 
probe process("ls").end
{
    println("ls finished");
}

Pokud tento skript spustíme (opět příkazem stap) a v jiném terminálu napíšeme ls, vypíše SystemTap (na prvním terminálu) následující zprávy:

ls started
ls finished

Předchozí skript si ještě rozšíříme o možnost výpisu PID (process ID) právě spuštěného procesu ls (přesněji řečeno procesu vytvořenému z programu ls). Pro tento účel použijeme funkci pid(), která číslo procesu vrací. Povšimněte si, že funkci printf používáme skutečně podobným způsobem, jako v céčku (použití %d pro dekadické celé číslo atd.):

probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}

Po spuštění sondy a příkazu ls v jiném terminálu dostaneme tento výsledek (číslo procesu se bude pochopitelně lišit):

STAP prepared
ls with PID=20330 started
ls with PID=20330 finished

Poznámka: v případě potřeby je možné namísto sond typu process(jméno_procesu).begin a process(jméno_procesu).end použít sondy process(PID_procesu).begin a process(PID_procesu).end.

6. Detekce systémových volání (syscallů)

Poměrně zajímavé je použití SystemTapu pro sledování volání služeb jádra (syscallů). Seznam všech syscallů lze najít na adrese https://sourceware.org/sys­temtap/tapsets/syscalls.html. Alternativně je možné použít i příkaz, který vypíše aliasy (zkrácené názvy) syscallů podporovaných aktuálně nainstalovanou verzí SystemTapu:

stap --dump-probe-aliases |grep ^syscall

Podívejme se nyní na způsob použití těchto typů sond. Vytvoříme čtyři sondy zavolané po inicializaci SystemTapu, po spuštění procesu ls, po ukončení tohoto procesu a taktéž při zavolání systémové funkce open (známé spíše pod jménem sys_open). U poslední sondy si povšimněte, že máme přístup ke speciální proměnné $filename, která je (pouze v kontextu této sondy!) naplněna jménem otevíraného souboru. U jiných typů sond budou přístupné odlišné speciální proměnné – opět viz https://sourceware.org/sys­temtap/tapsets/syscalls.html):

probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    printf("ls opened file %s\n", filename);
}

Po spuštění této sondy můžeme vidět zajímavý výsledek, z něhož je patrné, že v systému běží nějaký další proces nazvaný ls:

STAP prepared
ls opened file /proc/1286/cgroup
ls opened file /proc/loadavg
ls opened file /sys/fs/cgroup/systemd/system.slice/systemd-udevd.service/cgroup.procs
ls with PID=22086 started
ls opened file /etc/ld.so.cache
ls opened file /lib64/libselinux.so.1
ls opened file /lib64/libcap.so.2
ls opened file /lib64/libacl.so.1
ls opened file /lib64/libc.so.6
ls opened file /lib64/libpcre.so.1
ls opened file /lib64/libdl.so.2
ls opened file /lib64/libattr.so.1
ls opened file /lib64/libpthread.so.0
ls opened file /usr/lib/locale/locale-archive
ls with PID=22086 finished
ls opened file /proc/loadavg
ls opened file /proc/loadavg
ls opened file /proc/1089/cgroup
ls opened file /proc/loadavg

7. Vylepšení předchozího programu – detekce správného čísla procesu

Aby byl náš skript určený pro sledování chování příkazu ls užitečnější, můžeme do něj přidat jednoduché (ve skutečnosti velmi primitivní) filtrování. To bude fungovat následujícím způsobem – v sondě typu process(jméno_procesu).begin si zapamatujeme PID právě spuštěného příkazu ls a toto PID použijeme jako filtr v sondě typu syscall.open. V tomto skriptu můžeme vidět hned dvě nové vlastnosti skriptů psaných pro SystemTap. První vlastností jsou globální proměnné vytvářené deklarací global jméno_proměnné a druhou vlastností rozhodovací konstrukce if-then-(else), jejíž syntaxe a sémantika je prakticky kompletně převzatá z programovacího jazyka C:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    if (pid==pid()) {
        printf("ls opened file %s\n", filename);
    }
}

Zprávy vypisované touto sondou již budou obsahovat jen relevantní informace, nikoli informace z již běžícího procesu ls:

ls with PID=22060 started
ls opened file /etc/ld.so.cache
ls opened file /lib64/libselinux.so.1
ls opened file /lib64/libcap.so.2
ls opened file /lib64/libacl.so.1
ls opened file /lib64/libc.so.6
ls opened file /lib64/libpcre.so.1
ls opened file /lib64/libdl.so.2
ls opened file /lib64/libattr.so.1
ls opened file /lib64/libpthread.so.0
ls opened file /usr/lib/locale/locale-archive
ls with PID=22060 finished

Chování sondy si můžeme zkontrolovat přes již popsanou utilitu strace:

strace -e trace=open ls
 
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3

Sondu tedy evidentně nemáme napsanou úplně špatně…

8. Výpis základních informací o syscallu (volané služby jádra)

Podívejme se na další příklad, který sleduje volání dvou služeb jádra. Kromě nám již známé služby open (sys_open) se nyní sleduje i služba write (sys_write). U služby write se naplňují následující speciální proměnné: buf, count, fd. První proměnná obsahuje adresu zapisovaného bufferu (viz též navazující kapitolu), druhá proměnná počet zapisovaných bajtů a třetí proměnná file deskriptor. Právě počet zapisovaných bajtů a file deskriptor budeme sledovat:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    if (pid==pid()) {
        printf("ls opened file %s\n", filename);
    }
}
 
probe syscall.write
{
    bytes=$count
    into=$fd
    if (pid==pid()) {
        printf("write %d bytes to file descriptor %d\n", bytes, into);
    }
}

Podívejme se, co se stane při spuštění sondy a příkazu ls v jiném terminálu:

STAP prepared
ls with PID=23120 started
ls opened file /etc/ld.so.cache
ls opened file /lib64/libselinux.so.1
ls opened file /lib64/libcap.so.2
ls opened file /lib64/libacl.so.1
ls opened file /lib64/libc.so.6
ls opened file /lib64/libpcre.so.1
ls opened file /lib64/libdl.so.2
ls opened file /lib64/libattr.so.1
ls opened file /lib64/libpthread.so.0
ls opened file /usr/lib/locale/locale-archive
write 67 bytes to file descriptor 1
write 55 bytes to file descriptor 1
ls with PID=23120 finished

Výsledek je zajímavý a vlastně i predikovatelný – zapisují se data do file deskriptoru číslo 1, což je standardní výstup. Pokud uděláme při spuštění příkazu ls chybu (ls –5432 atd.), vypíše se hlášení namísto toho do chybového výstupu:

ls with PID=7339 started
write 26 bytes to file descriptor 2
write 38 bytes to file descriptor 2
ls with PID=7339 finished

Poznámka: jedná se o následující dvě zprávy:

ls: invalid option -- '5'
Try 'ls --help' for more information.

(povšimněte si přesné délky 25 resp. 37 znaků + znaku pro odřádkování)

9. Přístup k bufferu použitém v syscallu write

Pokusme se nyní skript se sondou upravit takovým způsobem, aby sonda získala a následně vypsala řetězec, který (paralelně) vypisuje příkaz ls. První verze bude implementována poněkud naivně – použijeme funkci user_string pro přečtení řetězce z adresového prostoru sledovaného procesu a následně tento řetězec bez dalších úprav vytiskneme funkcí println:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    if (pid==pid()) {
        printf("ls opened file %s\n", filename);
    }
}
 
probe syscall.write
{
    bytes = $count;
    into  = $fd;
    msg   = user_string($buf);
    if (pid==pid()) {
        printf("write %d bytes to file descriptor %d\n", bytes, into);
        println(msg);
    }
}

Zdrojový text sondy sice zdánlivě vypadá v pořádku, ale podívejme se, co se stane při sledování příkazu ls -l:

write 9 bytes to file descriptor 1
total 40
 
ls opened file /etc/localtime
write 62 bytes to file descriptor 1
-rw-------. 1 root root 14791  7. čen 15.02 anaconda-ks.cfg
 
write 63 bytes to file descriptor 1
-rw-r--r--. 1 root root    48  7. čen 15.38 hello_world2.stp
 
write 63 bytes to file descriptor 1
-rw-r--r--. 1 root root   140  7. čen 15.47 hello_world3.stp
 
write 62 bytes to file descriptor 1
-rw-r--r--. 1 root root    58  7. čen 15.38 hello_world.stp
 
 (prázdný řádek)
write 53 bytes to file descriptor 1
-rw-r--r--. 1 root root   585  7. čen 16.54 ls.stp
orld.stp
 
 
write 65 bytes to file descriptor 1
-rw-r--r--. 1 root root     4  7. čen 15.00 NETBOOT_METHOD.TXT
 
write 57 bytes to file descriptor 1
-rw-r--r--. 1 root root     8  7. čen 15.00 RECIPE.TXT
HOD.TXT
 
ls with PID=47910 finished

Ve výpisu by se teoreticky měly střídat řádky s informacemi o počtu zapisovaných bajtů a file deskriptoru s řádkem obsahujícím zprávu, kterou produkuje příkaz ls -l. Ve skutečnosti však vidíme, že tomu tak není, protože získáme i nějaké další fragmenty textu, které mohou být dosti dlouhé (a obecně náhodné). Proč tomu tak je?

10. Opravení předchozího příkladu: výpočet skutečné délky řetězce před jeho výpisem

Problém spočívá v tom, že sice získáme ukazatel na buffer, který následně převedeme na řetězec, ovšem nikde není řečeno, že tento buffer za všech okolností končí za posledním znakem nulou, která v céčku ukončuje běžné řetězce. Takto totiž systémová funkce write není navržena, a to z toho prostého důvodu, že musí pracovat i s binárními soubory, v nichž se znak s kódem nula běžně vyskytuje. Při volání této funkce se předává adresa bufferu a jeho délka, přičemž obsah není žádným způsobem interpretován. To vlastně znamená, že předchozí sonda mohla vypisovat i velmi dlouhé řetězce, a to tehdy, pokud by se v datech ZA bufferem dlouho nenacházela žádná nula. Jak tento problém vyřešit? Nemůžeme použít funkci user_string a následně řetězec na správném místě odříznout, protože teoreticky může dojít k chybě při přístupu na adresu nepatřící procesu. Namísto toho je nutné použít alternativní funkci nazvanou user_stringn:

function user_string_n:string(addr:long,n:long)

Upravený zdrojový text sondy vypadá takto:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process("ls").begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process("ls").end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    if (pid==pid()) {
        printf("ls opened file %s\n", filename);
    }
}
 
probe syscall.write
{
    bytes = $count;
    into  = $fd;
    msg   = user_string_n($buf, bytes);
    if (pid==pid()) {
        printf("write %d bytes to file descriptor %d\n", bytes, into);
        println(msg);
    }
}

Nyní již vypisované zprávy pro ls -l budou korektní:

write 9 bytes to file descriptor 1
total 40
 
ls opened file /etc/localtime
write 62 bytes to file descriptor 1
-rw-------. 1 root root 14791  7. čen 15.02 anaconda-ks.cfg
 
write 63 bytes to file descriptor 1
-rw-r--r--. 1 root root    48  7. čen 15.38 hello_world2.stp
 
write 63 bytes to file descriptor 1
-rw-r--r--. 1 root root   140  7. čen 15.47 hello_world3.stp
 
write 62 bytes to file descriptor 1
-rw-r--r--. 1 root root    58  7. čen 15.38 hello_world.stp
 
write 53 bytes to file descriptor 1
-rw-r--r--. 1 root root   594  7. čen 16.58 ls.stp
 
write 65 bytes to file descriptor 1
-rw-r--r--. 1 root root     4  7. čen 15.00 NETBOOT_METHOD.TXT
 
write 57 bytes to file descriptor 1
-rw-r--r--. 1 root root     8  7. čen 15.00 RECIPE.TXT
 
ls with PID=47931 finished

11. Specifikace jména sledovaného programu z příkazové řádky

Náš skript se sondami má jednu vadu – je určen pouze pro sledování procesu pojmenovaného ls, což není příliš univerzální. SystemTap nám ovšem i v této chvíli nabízí řešení, a to formou speciálních proměnných @číslo. Tyto proměnné jsou naplněny parametry předanými na příkazovém řádku, přičemž první parametr bude mít jméno @1, druhý parametr jméno @2 atd. Zavináče jsou použity pro řetězce, pokud je však nějaký parametr celočíselný, používají se jména $číslo ($1, $2, …). To může být trošku matoucí, zejména pro programátory používající shell, takže si na rozdíl mezi $1 a @1 budeme muset dávat pozor. Univerzální skript se sondami, který je použitelný pro jakýkoli proces, vypadá následovně:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process(@1).begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process(@1).end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe syscall.open
{
    filename = user_string($filename);
    if (pid==pid()) {
        printf("ls opened file %s\n", filename);
    }
}
 
probe syscall.write
{
    bytes = $count;
    into  = $fd;
    msg   = user_string_n($buf, bytes);
    if (pid==pid()) {
        printf("write %d bytes to file descriptor %d\n", bytes, into);
        println(msg);
    }
}

Takto vytvořený skript použijeme pro sledování příkazu whoami:

stap check.stp whoami
STAP prepared
ls with PID=49026 started
ls opened file /etc/ld.so.cache
ls opened file /lib64/libc.so.6
ls opened file /usr/lib/locale/locale-archive
ls opened file /etc/nsswitch.conf
ls opened file /etc/ld.so.cache
ls opened file /lib64/libnss_files.so.2
ls opened file /etc/passwd
write 5 bytes to file descriptor 1
root
 
ls with PID=49026 finished

Poznámka: pozor musíte dát pouze na příkazy zabudované přímo do shellu. Ty pochopitelně nebudou rozeznány, neboť se pro ně nespouští další proces. To znamená, že všechny příkazy, které najdete v man builtins nelze takto jednoduše sledovat (ovšem i zde řešení existuje).

12. Výpis volaných funkcí

Nyní se podívejme na poněkud odlišnou sondu. Bude se jednat o sondu volanou ve chvíli, kdy sledovaný proces (zde opět ls) volá libovolnou funkci. Znak * zde představuje „žolíkový znak“, podobně jako v shellu. Jméno volané funkce se zjistí pomocí ppfunc:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process(@1).begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process(@1).end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe process("ls").function("*").call
{
    println(ppfunc());
}

Výsledek běhu SystemTapu jsem musel zkrátit, protože i pro jednoduché ls se volá několik set funkcí (zaujala mě především funkce nazvaná get_funky_string, asi nastal správný okamžik projít zdrojovými kódy základních příkazů shellu):

ls with PID=4269 started
_start
__libc_csu_init
_init
frame_dummy
register_tm_clones
main
set_program_name
atexit
__xargmatch_internal
argmatch
human_options
clone_quoting_options
xmemdup
xmalloc
get_quoting_style
clone_quoting_options
xmemdup
xmalloc
set_char_quoting
xstrdup
xmemdup
xmalloc
get_funky_string
...
...
...
close_stdout
close_stream
rpl_fclose
rpl_fflush
close_stream
rpl_fclose
rpl_fflush
__do_global_dtors_aux
deregister_tm_clones
_fini
ls with PID=4269 finished

13. Zjištění, odkud se funkce volá (backtrace)

Předchozí sondu lze rozšířit. Předpokládejme, že program ls má problém při alokaci paměti funkcí xalloc. Budeme tedy sledovat pouze volání této funkce a současně u každého volání vypíšeme backtrace (ten již známe z popisu GNU Debuggeru). Pro tento výpis použijeme funkci nazvanou print_ubacktrace:

global pid=-1
 
probe begin
{
    println("STAP prepared");
}
 
probe process(@1).begin
{
    pid=pid()
    printf("ls with PID=%d started\n", pid());
}
 
probe process(@1).end
{
    printf("ls with PID=%d finished\n", pid());
}
 
probe process("ls").function("xmalloc").call
{
    println(ppfunc());
    print_ubacktrace();
}

Výsledek bude vypadat takto:

ict ve školství 24

xmalloc
 0x4112f0 : xmalloc+0x0/0x20 [/usr/bin/ls]
 0x4051c8 : sort_files+0x148/0x190 [/usr/bin/ls]
 0x403c72 : main+0x12e2/0x2160 [/usr/bin/ls]
 0x7f51b4e71700 [/usr/lib64/libc-2.21.so+0x20700/0x3c1000]
xmalloc
 0x4112f0 : xmalloc+0x0/0x20 [/usr/bin/ls]
 0x4060fc : calculate_columns+0xcc/0x2d0 [/usr/bin/ls]
 0x407abe : print_current_files+0x33e/0x4e0 [/usr/bin/ls]
 0x403d73 : main+0x13e3/0x2160 [/usr/bin/ls]
 0x7f51b4e71700 [/usr/lib64/libc-2.21.so+0x20700/0x3c1000]
ls with PID=5278 finished

V případě potřeby je možné před zavoláním funkce xalloc získat předávané parametry i návratovou hodnotu.

Příště si ukážeme další typy sond, zejména sondy určené pro sledování práce se souborovým systémem a síťovým subsystémem.

14. Odkazy na Internetu

  1. SystemTap Reference: Context Functions
    https://access.redhat.com/do­cumentation/en-US/Red_Hat_Enterprise_Linux/6/html/Sys­temTap_Tapset_Reference/con­text_stp.html
  2. SystemTap Beginners Guide (RHEL 6)
    https://access.redhat.com/do­cumentation/en-US/Red_Hat_Enterprise_Linux/6/html/Sys­temTap_Beginners_Guide/in­dex.html
  3. SystemTap Tapset Reference (RHEL 6)
    https://access.redhat.com/do­cumentation/en-US/Red_Hat_Enterprise_Linux/6/html-single/SystemTap_Tapset_Re­ference/index.html
  4. SystemTap Beginners Guide (RHEL 7)
    https://access.redhat.com/do­cumentation/en-US/Red_Hat_Enterprise_Linux/7/html/Sys­temTap_Beginners_Guide/in­dex.html
  5. SystemTap Tapset Reference (RHEL 6)
    https://access.redhat.com/do­cumentation/en-US/Red_Hat_Enterprise_Linux/7/html/Sys­temTap_Tapset_Reference/in­dex.html
  6. Debuggery a jejich nadstavby v Linuxu
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu/
  7. Debuggery a jejich nadstavby v Linuxu (2. část)
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-2-cast/
  8. Debuggery a jejich nadstavby v Linuxu (3): Nemiver
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-3-nemiver/
  9. Debuggery a jejich nadstavby v Linuxu (4): KDbg
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-4-kdbg/
  10. Debuggery a jejich nadstavby v Linuxu (5): ladění aplikací v editorech Emacs a Vim
    http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-5-ladeni-aplikaci-v-editorech-emacs-a-vim/
  11. Tracing (software)
    https://en.wikipedia.org/wi­ki/Tracing_%28software%29
  12. ltrace(1) – Linux man page
    http://linux.die.net/man/1/ltrace
  13. ltrace (Wikipedia)
    https://en.wikipedia.org/wiki/Ltrace
  14. strace(1) – Linux man page
    http://linux.die.net/man/1/strace
  15. strace (stránka projektu na SourceForge)
    https://sourceforge.net/pro­jects/strace/
  16. strace (Wikipedia)
    https://en.wikipedia.org/wiki/Strace
  17. SystemTap (stránka projektu)
    https://sourceware.org/systemtap/
  18. SystemTap (Wiki projektu)
    https://sourceware.org/systemtap/wiki
  19. SystemTap (Wikipedia)
    https://en.wikipedia.org/wi­ki/SystemTap
  20. Dynamic Tracing with DTrace & SystemTap
    http://myaut.github.io/dtrace-stap-book/
  21. DTrace (Wikipedia)
    https://en.wikipedia.org/wiki/DTrace
  22. GDB – Dokumentace
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/
  23. GDB – Supported Languages
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/Suppor­ted-Languages.html#Supported-Languages
  24. GNU Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Debugger
  25. The LLDB Debugger
    http://lldb.llvm.org/
  26. Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/Debugger
  27. 13 Linux Debuggers for C++ Reviewed
    http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817
  28. Getting started with ltrace: how does it do that?
    https://www.ellexus.com/getting-started-with-ltrace-how-does-it-do-that/
  29. Reverse Engineering Tools in Linux – strings, nm, ltrace, strace, LD_PRELOAD
    http://www.thegeekstuff.com/2012/03/re­verse-engineering-tools/
  30. 7 Strace Examples to Debug the Execution of a Program in Linux
    http://www.thegeekstuff.com/2011/11/stra­ce-examples/
  31. Oracle® Solaris 11.3 DTrace (Dynamic Tracing) Guide
    http://docs.oracle.com/cd/E53394_01/html/E­53395/gkwpo.html#scrolltoc
  32. An Introduction To Using GDB Under Emacs
    http://tedlab.mit.edu/~dr/gdbin­tro.html
  33. GNU Emacs
    https://www.gnu.org/softwa­re/emacs/emacs.html
  34. The Emacs Editor
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­macs/index.html
  35. Emacs Lisp
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­lisp/index.html
  36. An Introduction to Programming in Emacs Lisp
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­intr/index.html
  37. 27.6 Running Debuggers Under Emacs
    https://www.gnu.org/softwa­re/emacs/manual/html_node/e­macs/Debuggers.html
  38. GdbMode
    http://www.emacswiki.org/e­macs/GdbMode
  39. Emacs (Wikipedia)
    https://en.wikipedia.org/wiki/Emacs
  40. Emacs Lisp (Wikipedia)
    https://en.wikipedia.org/wi­ki/Emacs_Lisp
  41. Pyclewn installation notes
    http://pyclewn.sourceforge­.net/install.html
  42. pip Installation
    https://pip.pypa.io/en/la­test/installing.html
  43. Clewn
    http://clewn.sourceforge.net/
  44. Clewn installation
    http://clewn.sourceforge.net/in­stall.html
  45. Clewn – soubory
    http://sourceforge.net/pro­jects/clewn/files/OldFiles/
  46. KDbg: úvodní stránka
    http://www.kdbg.org/
  47. Nemiver (stránky projektu)
    https://wiki.gnome.org/Apps/Nemiver
  48. Basic Assembler Debugging with GDB
    http://dbp-consulting.com/tutorials/de­bugging/basicAsmDebuggingGDB­.html
  49. Nemiver FAQ
    https://wiki.gnome.org/Ap­ps/Nemiver/FAQ
  50. Nemiver (Wikipedia)
    https://en.wikipedia.org/wiki/Nemiver
  51. Data Display Debugger
    https://www.gnu.org/software/ddd/
  52. GDB – Dokumentace
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/
  53. BASH Debugger
    http://bashdb.sourceforge.net/
  54. The Perl Debugger(s)
    http://debugger.perl.org/
  55. Visual Debugging with DDD
    http://www.drdobbs.com/tools/visual-debugging-with-ddd/184404519
  56. Pydb – Extended Python Debugger
    http://bashdb.sourceforge.net/pydb/
  57. Insight
    http://www.sourceware.org/insight/
  58. Supported Languages (GNU Debugger)
    http://sourceware.org/gdb/cu­rrent/onlinedocs/gdb/Suppor­ted-Languages.html#Supported-Languages
  59. GNU Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/GNU_Debugger
  60. The LLDB Debugger
    http://lldb.llvm.org/
  61. Debugger (Wikipedia)
    https://en.wikipedia.org/wi­ki/Debugger
  62. 13 Linux Debuggers for C++ Reviewed
    http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817
  63. Clewn
    http://clewn.sourceforge.net/
  64. Clewn installation
    http://clewn.sourceforge.net/in­stall.html
  65. Clewn – soubory ke stažení
    http://sourceforge.net/pro­jects/clewn/files/OldFiles/
  66. Pyclewn installation notes
    http://pyclewn.sourceforge­.net/install.html
  67. Debugging
    http://janus.uclan.ac.uk/pa­gray/labs/debug.htm

Autor článku

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