Strace in Linux: storia, funzionamento e utilizzo

Strace in Linux: storia, funzionamento e utilizzo

Nei sistemi operativi simili a Unix, la comunicazione dei programmi con il mondo esterno e con il sistema operativo avviene tramite un insieme ristretto di funzioni, chiamate di sistema. Pertanto, può essere utile monitorare le chiamate di sistema per scopi di debug.

Il monitoraggio della "vita intima" dei programmi su Linux è facilitato dallo strumento strace, di cui si parla in questo articolo. Viene fornita una breve storia dell'uso di attrezzature "spiatorie". strace e una descrizione del funzionamento di tali programmi.

Contenuto

L'origine delle specie

L'interfaccia principale tra i programmi e il 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.

Tuttavia, nella prima versione pubblica di Unix (Version 6 Unix, 1975) non c’erano metodologie pratiche per monitorare il comportamento dei processi utente. Per risolvere questo problema, Bell Labs nella versione successiva (Versione 7 di Unix, 1979) propose una nuova chiamata di sistema — ptrace.

Il ptrace è stato sviluppato principalmente per debugger interattivi, ma alla fine degli anni '80 (nel periodo commerciale già System V Release 4) sulla base di questo nacquero e si diffusero ampiamente debugger specializzati — tracer di chiamate di sistema.

Primo La stessa versione di strace fu pubblicata da Paul Kronenberg nella mailing list comp.sources.sun nel 1992 come alternativa a un’utilità proprietaria trace di Sun. Sia il clone che l'originale erano destinati a SunOS, ma entro il 1994 strace furono portati su System V, Solaris e il sempre più popolare Linux.

Oggi strace supporta solo Linux e si basa ancora su quella ptrace, che è cresciuta di molte estensioni.

Il moderno (e molto attivo) manutentore straceDmitry Levin. Grazie a lui, l’utilità ha acquisito funzionalità avanzate come l'iniezione di errori nelle chiamate di sistema, supporto per un'ampia gamma di architetture e, cosa più importante, un mascot. Fonti non ufficiali affermano che la scelta è ricaduta sull'estruzzo a causa della somiglianza tra la parola russa «страус» e l'inglese "strace".

È anche importante notare che la chiamata di sistema ptrace e i programmatori di traccia non sono mai stati inclusi in POSIX, nonostante la lunga storia e le implementazioni in Linux, FreeBSD, OpenBSD e tradizionali Unix.

In breve, strace: Piglet Trace

"Non ti aspettiamo di capire questo" (Dennis Ritchie, commento nel codice sorgente di Version 6 Unix)

Sin da piccolo, ho sempre detestato le scatole nere: con i giocattoli non giocavo, ma cercavo di capire come funzionavano (gli adulti usavano la parola «rompevo», ma non credete a quelle malelingue). Forse è per questo che sento una certa affinità con la cultura informale dei primi Unix e del moderno movimento open-source.

In questo articolo, esaminare il codice sorgente di strace, che è diventato più maturo nel corso degli anni, non è sensato. Ma non dovrebbero rimanere misteri per i lettori. Pertanto, per mostrare il principio di funzionamento di programmi simili a strace, presenterò il codice di un piccolo tracciatore — Piglet Trace (ptr). Non è in grado di fare nulla di speciale, ma la cosa principale — le chiamate di sistema del programma — le stampa:

$ 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 terminated

Piglet Trace riconosce circa un centinaio di chiamate di sistema Linux (vedi tabella) e funziona solo su architetture x86-64. Per scopi didattici, questo è sufficiente.

Analizziamo il funzionamento del nostro clone. Nel caso di Linux, i debugger e i tracer utilizzano, come menzionato sopra, la chiamata di sistema ptrace. Essa opera passando nel primo argomento gli identificatori di comando, dei quali abbiamo bisogno solo di PTRACE_TRACEME, PTRACE_SYSCALL e PTRACE_GETREGS.

Il funzionamento del tracer inizia in modo tradizionale Unix: fork(2) lancia un processo figlio, mentre quest’ultimo a sua volta utilizza exec(3) avvia il programma da esaminare. L'unico dettaglio 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:
    /* Qui il figlio */
    /* Deve essere abilitata una modalità tracciata. Un genitore dovrà attendere(2) che questo
     * accada. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Sostituisce se stesso con un programma da eseguire. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Il processo padre deve ora chiamare wait(2) nel processo figlio, cioè 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 finiti e possiamo iniziare a monitorare le chiamate di sistema in un ciclo infinito.

La chiamata ptrace(PTRACE_SYSCALL) garantisce che il successivo wait padre terminerà o prima dell'esecuzione della chiamata di sistema o subito dopo il suo completamento. Tra le due chiamate è possibile eseguire qualsiasi azione: sostituire la chiamata con un'alternativa, modificare gli argomenti o il valore di ritorno.

Ci basta chiamare due volte il comando ptrace(PTRACE_GETREGS), per ottenere lo stato del registro rax prima della chiamata (numero della chiamata di sistema) e subito dopo (valore di ritorno).

In effetti, 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, &registers) == -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, &registers) == -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);
}

Questo è l'intero tracciatore. Ora sapete da dove cominciare un'altra portabilità. DTrace su Linux.

Fondamenti: eseguire un programma sotto strace

Come primo esempio di utilizzo strace, vale la pena considerare il modo più semplice: eseguire un'applicazione sotto strace.

Per non dover scavare in un elenco infinito di chiamate di una tipica programma, scriviamo un programma minimo attorno a write:

int main(int argc, char *argv[])
{
    char str[] = "scrivimi su stdoutn";
    /* write(2) è un semplice wrapper attorno a una syscall, quindi dovrebbe essere facile trovare nel tracciato delle syscall. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Compiliamo il programma e verifichiamo che funzioni:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
scrivimi su stdout

E infine eseguiamolo sotto strace:

$ strace ./write-simple
execve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (File o directory non esistente)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (File o directory non esistente)
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 esistente)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster