
Nei sistemi operativi Unix-like, la comunicazione di un programma con il mondo esterno e il sistema operativo avviene attraverso un piccolo insieme di funzioni - chiamate di sistema. Pertanto, per scopi di debug è utile dare un'occhiata alle chiamate di sistema eseguite dai processi.
Monitorare la "vita intima" dei programmi su Linux è facilitato dall'utilità strace, che è l'oggetto di questo articolo. Viene fornita una breve storia strace e una descrizione del funzionamento di programmi simili.
Contenuto
Origine delle specie
L'interfaccia principale tra programmi e kernel del sistema operativo in Unix sono le chiamate di sistema (in inglese. system calls, syscalls), l'interazione dei programmi con il mondo esterno avviene esclusivamente attraverso di esse.
Ma nella prima versione pubblica di Unix (, anno 1975) non c'erano modi convenienti per monitorare il comportamento dei processi utente. Per risolvere questo problema, Bell Labs nella versione successiva (, anno 1979) ha introdotto una nuova chiamata di sistema - ptrace.
Il ptrace è stato sviluppato principalmente per debugger interattivi, ma alla fine degli anni '80 (nella già commerciale ) sono emersi debugger specifici basati su di esso, noti come tracer di chiamate di sistema.
versione di strace è stata pubblicata da Paul Kronenbourg nella mailing list comp.sources.sun nel 1992 come alternativa all'utilità chiusa trace di Sun. Sia il clone che l'originale erano destinati a SunOS, ma entro il 1994 strace era stata portata su System V, Solaris e il crescente Linux.
Oggi strace supporta solo Linux e si basa sempre sul ptrace, arricchito da numerosi estensioni.
L'attuale (e molto attivo) maintainer strace — . Grazie a lui, l'utilità ha acquisito avanzate funzionalità come l'iniezione di errori nelle chiamate di sistema, supporto per un'ampia gamma di architetture e, cosa più importante, . Fonti non ufficiali sostengono che la scelta sia caduta sull'istrice per la sua somiglianza con la parola russa «страус» e l'inglese "strace".
È importante anche notare che la chiamata di sistema ptrace e i debugger non sono stati inclusi nel POSIX, nonostante la lunga storia e l'esistenza di implementazioni in Linux, FreeBSD, OpenBSD e nei Unix tradizionali.
La struttura di strace in poche parole: Piglet Trace
"You are not expected to understand this" (Dennis Ritchie, commento nel codice sorgente di Version 6 Unix)
Fin dalla tenera età, ho sempre odiato le scatole nere: non giocavo con i giocattoli, ma cercavo di capire come funzionavano (gli adulti usavano la parola «rompevo», ma non credete alle malelingue). È forse per questo che mi sento così vicino alla cultura informale dei primi Unix e del moderno movimento open-source.
In questo articolo non ha senso analizzare il codice sorgente di strace, che ha guadagnato popolarità nel corso degli anni. Tuttavia, non devono esserci segreti per i lettori. Pertanto, per mostrare il principio di funzionamento di programmi simili a strace, citerò il codice di un piccolo tracer — (ptr). Non fa nulla di speciale, ma la cosa principale è che visualizza le chiamate di sistema del programma:
$ 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 riconosce circa un centinaio di chiamate di sistema Linux (vedi ) e funziona solo su architettura x86-64. Per scopi didattici è sufficiente.
Analizziamo il funzionamento del nostro clone. Nel caso di Linux, per debugger e tracer viene utilizzata, come accennato sopra, la chiamata di sistema ptrace. Funziona passando nel primo argomento gli identificativi dei comandi, di cui abbiamo bisogno solo di PTRACE_TRACEME, PTRACE_SYSCALL e PTRACE_GETREGS.
Il funzionamento del tracer inizia in stile Unix classico: fork(2) avvia un processo figlio, il quale a sua volta utilizza exec(3) per avviare il programma in esame. L'unico punto delicato qui è la chiamata ptrace(PTRACE_TRACEME) prima exec: il processo figlio si aspetta che il processo padre lo segua:
pid_t child_pid = fork();
switch (child_pid) {
case -1:
err(EXIT_FAILURE, "fork");
case 0:
Child qui
Deve essere abilitata una modalità di tracciamento. Un genitore dovrà aspettare(2) affinché questo
* accada.
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
Sostituisci se stesso con un programma da eseguire.
execvp(argv[1], argv + 1);
err(EXIT_FAILURE, "exec");
}Il processo genitore ora deve chiamare wait(2) nel processo figlio, ovvero assicurarsi che il passaggio alla modalità di tracciamento sia avvenuto:
/* 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");A questo punto, i preparativi sono completati e possiamo iniziare a monitorare le chiamate di sistema in un ciclo infinito.
Chiamata ptrace(PTRACE_SYSCALL) garantisce che la successiva wait del genitore terminerà o prima dell'esecuzione della chiamata di sistema o immediatamente dopo il suo completamento. Tra le due chiamate possono essere eseguite alcune azioni: sostituire la chiamata con un'alternativa, modificare gli argomenti o il valore restituito.
A noi basta chiamare la comando ptrace(PTRACE_GETREGS), per ottenere lo stato del registro rax prima della chiamata (numero della chiamata di sistema) e subito dopo (valore restituito).
Di fatto, il ciclo:
/* 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);
}Ecco il tracciatore. Ora sapete da dove cominciare un'altra portazione su Linux.
Fondamenti: esecuzione di un programma sotto strace
Come primo esempio di utilizzo strace, è opportuno citare il modo più semplice: eseguire un'applicazione sotto la gestione di strace.
Per non perdersi in un elenco infinito di chiamate di una tipica programma, scriviamo attorno a write:
int main(int argc, char *argv[])
{
char str[] = "scrivimi su stdoutn";
write(2) è una semplice avvolgenza attorno a una chiamata di sistema quindi dovrebbe essere facile trovare nel tracciato delle chiamate di sistema.
if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
perror("write");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Compiliamo il programma e assicuriamoci che funzioni:
$ gcc examples/write-simple.c -o write-simple
$ .\/write-simple
scrivimi su stdoutE infine eseguiamolo sotto la gestione di strace:
$ strace .\/write-simple
pexecve(".\/write", [".\/write"], 0x7ffebd6145b0 71 vars ) = 0
brk(NULL) = 0x55ff5489e000
access("\/etc\/ld.so.nohwcap", F_OK) = -1 ENOENT (File o directory non trovati)
access("\/etc\/ld.so.preload", R_OK) = -1 ENOENT (File o directory non trovati)
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 (File o directory non trovati)
openat(AT_FDCWD, "\/lib\/x86_64-linux-gnu\/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 <