
În sistemele de operare similare Unix, comunicarea dintre un program și lumea exterioară, precum și cu sistemul de operare, se face printr-un mic set de funcții — apeluri de sistem. Asta înseamnă că, în scopuri de depanare, poate fi util să observăm apelurile de sistem executate de procese.
Urmărirea „vieții intime” a programelor pe Linux este ajutată de utilitarul strace, la care este dedicat acest articol. Exemplele de utilizare a echipamentului „spion” sunt însoțite de o scurtă istorie strace și o descriere a dispozitivului acestor programe.
Cuprins
Originea speciilor
Principalul interfață între programe și kernelul OS în Unix — apelurile de sistem (în engleză system calls, syscalls), interacțiunea programelor cu lumea exterioară se face exclusiv prin acestea.
Dar în prima versiune publică a Unix (, anul 1975) nu existau modalități convenabile de a urmări comportamentul proceselor utilizatorilor. Pentru a rezolva această problemă, Bell Labs a propus, pentru versiunea următoare (, anul 1979) un nou apel de sistem — ptrace.
Ptrace a fost dezvoltat în special pentru depanatori interactivi, dar până la sfârșitul anilor '80 (în epoca comercială a ) pe baza acestuia au apărut și au câștigat o popularitate imensă depanatori foarte specializați — trasatori de apeluri de sistem.
Aceeași versiune strace a fost publicată de Paul Kronenburg în lista de discuție comp.sources.sun în anul 1992 ca alternativă la utilitarul închis trace de la Sun. Atât clonul, cât și originalul erau destinate SunOS, dar până în anul 1994 strace au fost portate pe System V, Solaris și Linux-ul în creștere în popularitate.
Astăzi, strace sprijină doar Linux-ul și se bazează pe același ptrace, care a evoluat într-o multitudine de extensii.
Mentorul modern (și foarte activ) strace — . Datorită lui, utilitarul a dobândit funcționalități avansate, cum ar fi injectarea erorilor în apelurile de sistem, suportul pentru o gamă largă de arhitecturi și, cel mai important, . Sursele neoficiale afirmă că alegerea a căzut pe struț datorită asemănării între cuvântul rus „страус” și englezescul "strace".
Este important de menționat că apelul de sistem ptrace și instrumentele de urmărire nu au fost incluse în POSIX, în ciuda istoriei lungi și a implementărilor în Linux, FreeBSD, OpenBSD și Unix-uri tradiționale.
Dispozitivul strace într-o frază: Piglet Trace
"Nu se așteaptă să înțelegi asta" (Dennis Ritchie, comentariu în codul sursă Version 6 Unix)
Încă din copilărie, am urât cutiile negre: nu mă jucam cu jucăriile, ci încercam să înțeleg structura lor (adulții foloseau cuvântul „dezasamblat”, dar nu credeți zvonurile). Poate că de aceea cultura informală a primelor Unix și a mișcării open-source moderne îmi este atât de familiară.
În cadrul acestui articol, a analiza codul sursă al programului strace, care a evoluat de-a lungul decadelor, nu ar fi rațional. Dar nu ar trebui să rămână niciun mister pentru cititori. Așadar, pentru a demonstra principiul funcționării unor programe similare cu strace, voi prezenta codul unui mini-urmăritor — (ptr). Nu face nimic special, dar partea esențială — apelurile de sistem ale programului — le afișează:
$ 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 recunoaște aproximativ o sută de apeluri de sistem Linux (vezi ) și funcționează doar pe arhitectura x86-64. Pentru scopuri educaționale, acest lucru este mai mult decât suficient.
Hai să analizăm modul în care funcționează clonul nostru. În cazul Linux, pentru debugger-e și urmăriri se utilizează, așa cum am menționat anterior, apelul de sistem ptrace. Acesta funcționează prin transmiterea identificatorilor de comenzi în primul argument, din care avem nevoie doar de PTRACE_TRACEME, PTRACE_SYSCALL și PTRACE_GETREGS.
Funcționarea urmăritorului începe în stilul Unix obișnuit: fork(2) lancează un proces fiu, iar acesta, la rândul său, folosește exec(3) pentru a lansa programul examinat. Singura subtilitate aici este apelul ptrace(PTRACE_TRACEME) anterior: exec: procesul fiu așteaptă ca procesul părinte să îl urmărească:
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");
}Procesul părinte trebuie acum să apeleze wait(2) în procesul copil, adică să se asigure că comutarea în modul de urmărire a avut loc:
/* 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");Acum pregătirile s-au încheiat și se poate începe efectiv urmărirea apelurilor de sistem într-un ciclu infinit.
Apel ptrace(PTRACE_SYSCALL) garantează că apelul următor așteptați al părintelui se va încheia fie înainte de execuția apelului de sistem, fie imediat după aceasta. Între cele două apeluri, se pot face diverse acțiuni: înlocuirea apelului cu unul alternativ, modificarea argumentelor sau a valorii returnate.
Este suficient să apelăm de două ori comanda ptrace(PTRACE_GETREGS), pentru a obține starea registrului rax înainte de apel (numărul apelului de sistem) și imediat după (valoarea returnată).
Ciclul în sine:
/* 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);
}Acesta este totul despre urmăritor. Acum știți de unde să începeți următoarea portare pe Linux.
Bazele: lansarea unui program sub gestionarea strace
Ca prim exemplu de utilizare strace, poate merită să menționăm cea mai simplă metodă — rularea unei aplicații sub controlul strace.
Pentru a nu ne pierde în lista infinită de apeluri ale unei programe tipice, să scriem în jurul write:
int main(int argc, char *argv[])
{
char str[] = "write me to stdoutn";
\/\* write(2) este un wrapper simplu în jurul unui apel de sistem, așa că ar trebui să fie ușor de
* găsit în urmăririle apelurilor de sistem. \/\*
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Să compilăm programul și să ne asigurăm că funcționează:
$ gcc examples\/write-simple.c -o write-simple
$ .\/write-simple
write me to stdoutȘi în cele din urmă, să-l rulăm sub controlul strace:
$ strace .\/write-simple
pexecve(\