
V unixových operačních systémech probíhá komunikace programu se světem a operačním systémem prostřednictvím malé sady funkcí - systémových volání. To znamená, že pro ladicí účely může být užitečné sledovat vykonávané procesy systémovými voláními.
Sledovat "intimní život" programů na Linuxu pomáhá utilita strace, které je tento článek věnován. K příkladům použití „špehovacího“ zařízení je přiložen stručný přehled historie strace a popis zařízení podobných programů.
Obsah
Původ druhů
Hlavní rozhraní mezi programy a jádrem OS v Unixu - systémová volání (angl. system calls, syscalls), interakce programů se světem probíhá výhradně prostřednictvím nich.
Ale v první veřejné verzi Unixu (, 1975) neexistovaly pohodlné způsoby sledování chování uživatelských procesů. K vyřešení tohoto problému Bell Labs navrhly v další verzi (, 1979) nový systémový apel - ptrace.
Ptrace byl vyvíjen především pro interaktivní ladicí programy, ale na konci 80. let (v éře již komerčního ) na jeho základě vznikly a získaly široké rozšíření specializované ladicí programy - trasovače systémových volání.
Stejná verze strace byla publikována Paulem Kronenburga v mailing listu comp.sources.sun v roce 1992 jako alternativa k uzavřené utilitě trace od Sun. Jak klon, tak originál byly určeny pro SunOS, ale do roku 1994 strace byly portovány na System V, Solaris a stále populárnější Linux.
Dnes strace podporuje pouze Linux a spoléhá na stále stejné ptrace, obehnané mnoha rozšířeními.
Moderní (a velmi aktivní) správce strace — . Díky němu získala utilita pokročilé možnosti, jako je injekce chyb do systémových volání, podpora širokého spektra architektur a především . Neoficiální zdroje tvrdí, že volba padla na pštrosa kvůli podobnosti ruského slova „страус“ a anglického "strace".
Není bez významu ani to, že systémové volání ptrace a debuggerové nástroje nebyly zahrnuty do POSIX, navzdory dlouhé historii a existenci implementací v Linuxu, FreeBSD, OpenBSD a tradičním Unixu.
Zařízení strace ve zkratce: Piglet Trace
"You are not expected to understand this" (Dennis Ritchie, komentář v zdrojovém kódu Version 6 Unix)
Od útlého dětství nesnáším černé skříňky: s hračkami jsem si nehrál, ale snažil jsem se pochopit jejich konstrukci (dospělí používali slovo „ničil“, ale nevěřte zlým jazykům). Možná proto mi tak blízká neformální kultura prvních Unixů a současného open-source hnutí.
V rámci tohoto článku je nerozumné analyzovat zdrojový kód strace, který se za desetiletí zdokonalil. Ale také by neměly zůstat žádné tajemství pro čtenáře. Proto, abych ukázal princip fungování podobných programů strace, přiložím kód mini-tracera — (ptr). Nic zvláštního nedělá, ale hlavní — systémové volání programu — vypisuje:
$ gcc examples/piglet-trace.c -o ptr
$ ptr echo test > /dev/null
BRK(12) -> 94744690540544
ACCESS(21) -> 18446744073709551614
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694657216512
CLOSE(3) -> 0
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
READ(0) -> 832
FSTAT(5) -> 0
MMAP(9) -> 140694657208320
MMAP(9) -> 140694650953728
MPROTECT(10) -> 0
MMAP(9) -> 140694655045632
MMAP(9) -> 140694655070208
CLOSE(3) -> 0
unknown(158) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MUNMAP(11) -> 0
BRK(12) -> 94744690540544
BRK(12) -> 94744690675712
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694646390784
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee terminatedPiglet Trace rozpoznává přibližně sto systémových volání Linuxu (viz ) a funguje pouze na architektuře x86-64. Pro vzdělávací účely je to dostatečné.
Pojďme prozkoumat funkci našeho klonu. V případě Linuxu pro debugery a trackery se používá, jak bylo zmíněno výše, systémové volání ptrace. Funguje prostřednictvím předání identifikátorů příkazů v prvním argumentu, z nichž potřebujeme pouze PTRACE_TRACEME, PTRACE_SYSCALL a PTRACE_GETREGS.
Funkce trackera začíná v běžném unixovém stylu: fork(2) spouští podřízený proces, a ten následně pomocí exec(3) spouští zkoumaný program. Jedinou jemností zde je volání ptrace(PTRACE_TRACEME) před exec: podřízený proces očekává, že proces rodič jej bude sledovat:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
/* Dítě zde */
/* Je nutné povolit režim sledování. Rodič bude muset počkat(2) na to, aby se to stalo. */
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Nahradí se programem, který se má spustit. */
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Rodičovský proces nyní musí zavolat wait(2) ve vytvářeném procesu, tedy zajistit, že došlo k přepnutí do režimu sledování:
/* Parent */
/* First we wait for the child to set the traced mode (see
* ptrace(PTRACE_TRACEME) above) */
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "traceme -> waitpid");Na tomto místě jsou přípravy u konce a můžeme se pustit přímo do sledování systémových volání v nekonečné smyčce.
Zavolání ptrace(PTRACE_SYSCALL) zaručuje, že následující wait rodiče skončí buď před provedením systémového volání, nebo hned po jeho dokončení. Mezi dvěma voláními lze provést nějaké akce: nahradit volání alternativním, změnit argumenty nebo návratovou hodnotu.
Nám stačí dvakrát zavolat příkaz ptrace(PTRACE_GETREGS), abychom získali stav registru rax před voláním (číslo systémového volání) a hned po něm (návratová hodnota).
Ostatně smyčka:
/* A system call tracing loop, one interation per call. */
for (;;) {
/* A non-portable structure defined for ptrace/GDB/strace usage mostly.
* It allows to conveniently dump and access register state using
* ptrace. */
struct user_regs_struct registers;
/* Enter syscall: continue execution until the next system call
* beginning. Stop right before syscall.
*
* It's possible to change the system call number, system call
* arguments, return value or even avoid executing the system call
* completely. */
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
err(EXIT_FAILURE, "enter_syscall");
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "enter_syscall -> waitpid");
/* According to the x86-64 system call convention on Linux (see man 2
* syscall) the number identifying a syscall should be put into the rax
* general purpose register, with the rest of the arguments residing in
* other general purpose registers (rdi,rsi, rdx, r10, r8, r9). */
if (ptrace(PTRACE_GETREGS, child_pid, NULL, ®isters) == -1)
err(EXIT_FAILURE, "enter_syscall -> getregs");
/* Note how orig_rax is used here. That's because on x86-64 rax is used
* both for executing a syscall, and returning a value from it. To
* differentiate between the cases both rax and orig_rax are updated on
* syscall entry/exit, and only rax is updated on exit. */
print_syscall_enter(registers.orig_rax);
/* Exit syscall: execute of the syscall, and stop on system
* call exit.
*
* More system call tinkering possible: change the return value, record
* time it took to finish the system call, etc. */
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
err(EXIT_FAILURE, "exit_syscall");
if (waitpid(child_pid, NULL, 0) == -1)
err(EXIT_FAILURE, "exit_syscall -> waitpid");
/* Retrieve register state again as we want to inspect system call
* return value. */
if (ptrace(PTRACE_GETREGS, child_pid, NULL, ®isters) == -1) {
/* ESRCH is returned when a child terminates using a syscall and no
* return value is possible, e.g. as a result of exit(2). */
if (errno == ESRCH) {
fprintf(stderr, "nTracee terminatedn");
break;
}
err(EXIT_FAILURE, "exit_syscall -> getregs");
}
/* Done with this system call, let the next iteration handle the next
* one */
print_syscall_exit(registers.rax);
}To je celý debugger. Nyní víte, jak začít s dalším portováním na Linuxu.
Základy: spuštění programu pod správou strace
Jako první příklad použití strace, možná stojí za to uvést nejjednodušší způsob — spuštění aplikace pod řízením strace.
Abychom se nemuseli prokopávat nekonečným seznamem volání typického programu, napíšeme kolem write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) je jednoduchý obal kolem systémového volání, takže by ho mělo být snadné
* najít ve sledování systémových volání. */
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Sestavíme program a ujistíme se, že funguje:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdoutA konečně jej spustíme pod řízením strace:
$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Žádný takový soubor nebo adresář)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Žádný takový soubor nebo adresář)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Žádný takový soubor nebo adresář)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >