
Unix-taoliste operatsioonisüsteemide puhul suhtleb programm välist maailma ja operatsioonisüsteemi kaudu väikse arvu funktsioonide - süsteemikõnede - kaudu. Seega on tõrkeotsingu eesmärgil kasulik jälgida, milliseid süsteemikõnesid täidetakse.
Linuxis programmide "intimset elu" jälgida aitab utiliit strace, millele on pühendatud see artikkel. Jälgimise "spioneerimise" seadmete kasutamise näidetele lisandub lühike ajalugu strace ja sarnaste programmide seadme kirjeldus.
Sisukord
Liikide päritolu
Peamine liides programmide ja Unix OC tuuma vahel on süsteemikõned (ingl. system calls, syscalls), programmide ja välist maailma suhtlemine toimub üksnes nende kaudu.
Kuid Unix'i esimeses avalikus versioonis (, 1975. aasta) ei olnud mugavaid viise kasutaja protsesside käitumise jälgimiseks. Selle probleemi lahendamiseks pakkusid Bell Labs järgmises versioonis (, 1979. aasta) uut süsteemikõnet - ptrace.
ptrace'i arendati peamiselt interaktiivsete tõrkeotsijate jaoks, kuid 80ndate lõpuks (kaubanduslikul ) selle alusel ilmusid ja said laialdaselt levinud kitsasfookusega tõrkeotsijad - süsteemikõnede jälgijad.
strace'i versioon avaldati Paul Kroeningenburgi poolt 1992. aastal comp.sources.sun'i postituses alternatiivina suletud utiliidile trace Sunilt. Nii kloon kui ka originaal olid mõeldud SunOS'ile, kuid 1994. aastaks strace portiti see System V-le, Solaris'ele ja kiiresti populaarsust koguvusele Linuxile.
Praegu toetab strace ainult Linuxit ja tugineb endiselt ptrace, olles kasvanud paljude laiendustega.
Kaasaegne (ja väga aktiivne) hooldaja strace — . Tänu temale on utiliit saanud täiustatud funktsioone, näiteks vigade süstimine süsteemikõnedes, laia arhitektuuri toetamine ja mis kõige tähtsam, Mitteametid väidavad, et valik langes struusi peale, kuna venekeelne sõna «страус» kõlab sarnaselt ingliskeelsele "strace".
Oluline on ka see, et süsteemi kutsed ptrace ja jälgijad ei ole kuigi sisaldunud POSIX-s, vaatamata pikaajalisele ajaloole ja olemasolevatele rakendustele Linuxis, FreeBSD-s, OpenBSD-s ja traditsioonilistes Unixides.
strace'i seadmed kahe sõnaga: Piglet Trace
"Te ei pea seda mõistma" (Dennis Ritchie, kommentaar Version 6 Unix исходный кодis)
Lasteeast saati olen ma vihkanud musti kaste: ma ei mänginud nende mänguasjadega, vaid püüdsin neist aru saada (täiskasvanud kasutasid sõna «purustama», kuid ärge uskuge halbu keeli). Võib-olla on seetõttu mulle nii lähedane esimese Unix-i ja tänapäeva avatud lähtekoodiga liikumise mitteametlik kultuur.
Käesolevas artiklis ei ole mõistlik analüüsida sajandite jooksul arenenud strace allikat. Kuid ka lugejatele ei tohiks jääda saladusi. Seetõttu, et näidata sarnaste strace programmide tööpõhimõtet, toon välja miniatuurse jälgija koodi — (ptr). Ta ei oska midagi erilist teha, kuid mis kõige tähtsam — ta väljastab programmi süsteemi kutsed:
$ 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 lõppenudPiglet Trace tunnustab umbes sada Linuxi süsteemi kutset (vt. ), ja töötab ainult x86-64 arhitektuuril. Õppimise eesmärgil on sellest piisavalt.
Uurime, kuidas meie kloon töötab. Linuxis kasutavad järelvaldijad ja jälgijad, nagu eelnevalt mainitud, süsteemi kutset ptrace. See töötab, edastades esimese argumendi kaudu käskude identifikaatoreid, millest vajame ainult PTRACE_TRACEME, PTRACE_SYSCALL ja PTRACE_GETREGS.
Jälgija töö algab tavapärases Unix-stiilis: fork(2) käivitab alampassile, kes omakorda käivitab uuritava programmi sayesinde exec(3) . Ainuke nüanss on siin kutse ptrace(PTRACE_TRACEME) enne exec: alampass ootab, et vanemprotsess jälgiks teda:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
/* Lapsene siin */
/* Jälgimisrežiim peab olema lubatud. Vanem peab ootama(2), et see
* juhtuks. */
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Asendage end programmina, mida käivitada. */
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Vanemprotsess peab nüüd kutsuma wait(2) lastes veenduda, et jälgimisrežiimi vahetamine on toimunud:
/* 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");Nüüd on ettevalmistused lõpetatud ja saab otse hakata jälgima süsteemikõnesid lõpmatus tsüklis.
Kutsumine ptrace(PTRACE_SYSCALL) tagab, et järgnev ootama vanem lõpetab kas enne süsteemikõne sooritamist või kohe pärast selle täitmist. Kahe kõne vahel saab teha mingeid toiminguid: asendada kõne alternatiivse, muuta argumendid või tagastatud väärtuse.
Meil piisab, kui kutsume kaks korda käsu ptrace(PTRACE_GETREGS), et saada registri olek rax kuni kõne sooritamiseni (süsteemikõne number) ja kohe pärast seda (tagastatud väärtus).
Tegelikult tsükkel:
/* 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);
}See ongi kogu jälgija. Nüüd teate, kust alustada järgmise porteerimisega Linuxisse.
Alused: programmi käivitamine strace'i all
Esimese kasutuse näitena strace, arvatavasti on mõistlik tuua välja kõige lihtsam viis - käitada rakendust strace.
Kuna ei taha kaevata tüüpilise programmi lõpututes kõnekutsungite loetelu, kirjutame ümber kirjutama:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
/* write(2) on lihtne wrapper süsteemi kõne ümber, seega peaks see olema lihtne
* leida süsteemi kõnede jälgimises. */
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Kogume programmi ja veendume, et see töötab:
$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdoutJa lõpuks, käitame seda strace alla:
$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 muutuja */) = 0
brk(NULL) = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (Faili ega katalooge ei leitud)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (Faili ega katalooge ei leitud)
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 (Faili ega katalooge ei leitud)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 >