
V Unix-podobnih operativnih sistemih se komunikacija programov z zunanjim svetom in operacijskim sistemom odvija prek majhnega nabora funkcij - sistemskih klicev. To pomeni, da je v namene odpravljanja napak včasih koristno opazovati izvrševanje procesov prek sistemskih klicev.
Ugotavljanje 'intimnega življenja' programov na Linuxu olajša pripomoček strace, kateremu je ta članek posvečen. K primerom uporabe 'vohunskega' orodja sta priložena kratka zgodovina strace in opis naprav takšnih programov.
Vsebina
Izvor vrst
Glavni vmesnik med programi in jedrom OS v Unixu so sistemski klici (ang. system calls, syscalls), interakcija programov z zunanjim svetom poteka izključno prek njih.
Vendar v prvi javni različici Unixa (, leto 1975) ni bilo priročnih načinov za sledenje vedenju uporabniških procesov. Za rešitev tega problema so pri Bell Labs pri naslednji različici (, leto 1979) predlagali nov sistemski klic - ptrace.
Ptrace je bil prvotno zasnovan za interaktivne razhroščevalnike, vendar je do konca 80-ih (v času že komercialnega ) na tej osnovi nastalo in pridobilo široko razširjenost ozko usmerjenih razhroščevalnikov - sledi sistemskih klicev.
Ta različica strace je bila leta 1992 objavljena s strani Paula Kronenburga v razdelitvi comp.sources.sun kot alternativa zaprtemu orodju trace od Sun. Tako klon kot izvirnik sta bila namenjena SunOS, vendar je bila do leta 1994 strace portirana na System V, Solaris in hitro rastoči Linux.
Danes strace podpira le Linux in se opira na vse tiste ptrace, ki je obogateno z mnogimi razširitvami.
Sodobni (in precej aktivni) vzdrževalec strace — Zaradi njega je orodje pridobilo napredne funkcionalnosti, kot so injekcija napak v sistemske klice, podpora širokemu spektru arhitektur in, kar je najpomembnejše, Neuradni viri trdijo, da je bila izbira padla na noja zaradi zvočne podobnosti med rusko besedo 'страус' in angleško 'strace'.
Pomembno je tudi to, da sistemski klic ptrace in razsodniki še vedno nista bila vključena v POSIX, kljub dolgi zgodovini in obstoju implementacij v Linuxu, FreeBSD, OpenBSD in tradicionalnih Unixo.
Orodje strace na kratko: Piglet Trace
"Ne pričakujete, da boste to razumeli" (Dennis Ritchie, komentar v izvorni kodi Version 6 Unix)
Od malih nog se ne morem navaditi na črne škatle: z igračami se nisem igral, ampak sem poskušal razumeti njihovo delovanje (odrasli so uporabljali besedo 'lomiti', a ne verjemite zlobnim jezikom). Morda zato tako pripadam neformalni kulturi prvih Unixa in sodobnemu gibanju odprte kode.
V okviru tega članka analizirati izvorno kodo strace, ki je postala prijazna skozi desetletja, ni smiselno. A tudi skrivnosti za bralce ne bi smele ostati. Zato, da pokažem načelo delovanja podobnih strace programov, bom predstavil kodo miniaturnega razsodnika — (ptr). Nič posebnega ne zna početi, a glavno — sistemske klice programa — izpisuje:
$ 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 prepozna okoli sto sistemskih klicev Linux (glej ) in deluje le na arhitekturi x86-64. Za učne namene je to dovolj.
Poglejmo delovanje našega klona. V primeru Linuxa se za razsodnike in sledi uporablja, kot je bilo omenjeno, sistemski klic ptrace. Deluje tako, da v prvem argumentu prenaša identifikatorje ukazov, od katerih potrebujemo le PTRACE_TRACEME, PTRACE_SYSCALL in PTRACE_GETREGS.
Delo razsodnika se začne na običajen Unix način: fork(2) zažene podproces, ta pa nato z exec(3) zažene raziskovano program. Edina posebnost je tukaj — klic ptrace(PTRACE_TRACEME) pred exec: proces-podrejeni pričakuje, da ga bo proces-roditelj spremljal:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
/* Child here */
/* A traced mode has to be enabled. A parent will have to wait(2) for it
* to happen. */
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Replace itself with a program to be run. */
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Starševski proces mora pozvati wait(2) v otroškem procesu, kar pomeni, da se mora prepričati, da je preklop v način sledenja potekal:
/* 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");S tem so priprave zaključene in lahko se neposredno lotimo sledenju sistemskim klicem v neskončni zanki.
Poziv ptrace(PTRACE_SYSCALL) zagotavlja, da bo naslednji čakati starševski proces končan bodisi pred izvršitvijo sistemskega klica bodisi takoj po njegovem zaključku. Med obema pozivoma lahko izvedemo kakršne koli akcije: zamenjamo poziv z alternativnim, spremenimo argumente ali vračano vrednost.
Zadostuje, da dvakrat pokličemo ukaz ptrace(PTRACE_GETREGS), da pridobimo stanje registra rax pred klicem (številka sistemskega klica) in takoj po njem (vrnjena vrednost).
V bistvu, cikel:
/* 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);
}In to je celoten sledilnik. Zdaj veste, od kod začeti novo prenos na Linux.
Osnove: zagon programa pod upravljanjem strace
Kot prvi primer uporabe strace, morda bi bilo smiselno navesti najpreprostejši način — zagon aplikacije pod vodstvom strace.
Da se ne bi zatikali v neskončnem seznamu klicev običajnega programa, napišimo okoli write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) is a simple wrapper around a syscall so it should be easy to
* find in the syscall trace. */
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Sestavimo programo in se prepričamo, da deluje:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdoutIn končno jo zagnati pod vodstvom strace:
$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
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 (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >