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
13. Zjištění, odkud se funkce volá (backtrace)
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/systemtap/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/systemtap/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:
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
- SystemTap Reference: Context Functions
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/SystemTap_Tapset_Reference/context_stp.html - SystemTap Beginners Guide (RHEL 6)
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/SystemTap_Beginners_Guide/index.html - SystemTap Tapset Reference (RHEL 6)
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html-single/SystemTap_Tapset_Reference/index.html - SystemTap Beginners Guide (RHEL 7)
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/SystemTap_Beginners_Guide/index.html - SystemTap Tapset Reference (RHEL 6)
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/SystemTap_Tapset_Reference/index.html - Debuggery a jejich nadstavby v Linuxu
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu/ - Debuggery a jejich nadstavby v Linuxu (2. část)
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-2-cast/ - Debuggery a jejich nadstavby v Linuxu (3): Nemiver
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-3-nemiver/ - Debuggery a jejich nadstavby v Linuxu (4): KDbg
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-4-kdbg/ - 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/ - Tracing (software)
https://en.wikipedia.org/wiki/Tracing_%28software%29 - ltrace(1) – Linux man page
http://linux.die.net/man/1/ltrace - ltrace (Wikipedia)
https://en.wikipedia.org/wiki/Ltrace - strace(1) – Linux man page
http://linux.die.net/man/1/strace - strace (stránka projektu na SourceForge)
https://sourceforge.net/projects/strace/ - strace (Wikipedia)
https://en.wikipedia.org/wiki/Strace - SystemTap (stránka projektu)
https://sourceware.org/systemtap/ - SystemTap (Wiki projektu)
https://sourceware.org/systemtap/wiki - SystemTap (Wikipedia)
https://en.wikipedia.org/wiki/SystemTap - Dynamic Tracing with DTrace & SystemTap
http://myaut.github.io/dtrace-stap-book/ - DTrace (Wikipedia)
https://en.wikipedia.org/wiki/DTrace - GDB – Dokumentace
http://sourceware.org/gdb/current/onlinedocs/gdb/ - GDB – Supported Languages
http://sourceware.org/gdb/current/onlinedocs/gdb/Supported-Languages.html#Supported-Languages - GNU Debugger (Wikipedia)
https://en.wikipedia.org/wiki/GNU_Debugger - The LLDB Debugger
http://lldb.llvm.org/ - Debugger (Wikipedia)
https://en.wikipedia.org/wiki/Debugger - 13 Linux Debuggers for C++ Reviewed
http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817 - Getting started with ltrace: how does it do that?
https://www.ellexus.com/getting-started-with-ltrace-how-does-it-do-that/ - Reverse Engineering Tools in Linux – strings, nm, ltrace, strace, LD_PRELOAD
http://www.thegeekstuff.com/2012/03/reverse-engineering-tools/ - 7 Strace Examples to Debug the Execution of a Program in Linux
http://www.thegeekstuff.com/2011/11/strace-examples/ - Oracle® Solaris 11.3 DTrace (Dynamic Tracing) Guide
http://docs.oracle.com/cd/E53394_01/html/E53395/gkwpo.html#scrolltoc - An Introduction To Using GDB Under Emacs
http://tedlab.mit.edu/~dr/gdbintro.html - GNU Emacs
https://www.gnu.org/software/emacs/emacs.html - The Emacs Editor
https://www.gnu.org/software/emacs/manual/html_node/emacs/index.html - Emacs Lisp
https://www.gnu.org/software/emacs/manual/html_node/elisp/index.html - An Introduction to Programming in Emacs Lisp
https://www.gnu.org/software/emacs/manual/html_node/eintr/index.html - 27.6 Running Debuggers Under Emacs
https://www.gnu.org/software/emacs/manual/html_node/emacs/Debuggers.html - GdbMode
http://www.emacswiki.org/emacs/GdbMode - Emacs (Wikipedia)
https://en.wikipedia.org/wiki/Emacs - Emacs Lisp (Wikipedia)
https://en.wikipedia.org/wiki/Emacs_Lisp - Pyclewn installation notes
http://pyclewn.sourceforge.net/install.html - pip Installation
https://pip.pypa.io/en/latest/installing.html - Clewn
http://clewn.sourceforge.net/ - Clewn installation
http://clewn.sourceforge.net/install.html - Clewn – soubory
http://sourceforge.net/projects/clewn/files/OldFiles/ - KDbg: úvodní stránka
http://www.kdbg.org/ - Nemiver (stránky projektu)
https://wiki.gnome.org/Apps/Nemiver - Basic Assembler Debugging with GDB
http://dbp-consulting.com/tutorials/debugging/basicAsmDebuggingGDB.html - Nemiver FAQ
https://wiki.gnome.org/Apps/Nemiver/FAQ - Nemiver (Wikipedia)
https://en.wikipedia.org/wiki/Nemiver - Data Display Debugger
https://www.gnu.org/software/ddd/ - GDB – Dokumentace
http://sourceware.org/gdb/current/onlinedocs/gdb/ - BASH Debugger
http://bashdb.sourceforge.net/ - The Perl Debugger(s)
http://debugger.perl.org/ - Visual Debugging with DDD
http://www.drdobbs.com/tools/visual-debugging-with-ddd/184404519 - Pydb – Extended Python Debugger
http://bashdb.sourceforge.net/pydb/ - Insight
http://www.sourceware.org/insight/ - Supported Languages (GNU Debugger)
http://sourceware.org/gdb/current/onlinedocs/gdb/Supported-Languages.html#Supported-Languages - GNU Debugger (Wikipedia)
https://en.wikipedia.org/wiki/GNU_Debugger - The LLDB Debugger
http://lldb.llvm.org/ - Debugger (Wikipedia)
https://en.wikipedia.org/wiki/Debugger - 13 Linux Debuggers for C++ Reviewed
http://www.drdobbs.com/testing/13-linux-debuggers-for-c-reviewed/240156817 - Clewn
http://clewn.sourceforge.net/ - Clewn installation
http://clewn.sourceforge.net/install.html - Clewn – soubory ke stažení
http://sourceforge.net/projects/clewn/files/OldFiles/ - Pyclewn installation notes
http://pyclewn.sourceforge.net/install.html - Debugging
http://janus.uclan.ac.uk/pagray/labs/debug.htm