Strace en Linux: historia, estructura y uso

Strace en Linux: historia, estructura y uso

En sistemas operativos similares a Unix, la comunicación de un programa con el mundo exterior y el sistema operativo se realiza a través de un pequeño conjunto de funciones: llamadas al sistema. Esto significa que, para fines de depuración, a veces es útil observar los procesos ejecutados a través de las llamadas del sistema.

Para supervisar la "vida íntima" de los programas en Linux, se utiliza la herramienta strace, a la que está dedicada este artículo. A los ejemplos de uso de este "equipo espía" se adjunta una breve historia strace y una descripción del funcionamiento de programas similares.

Contenido

El origen de las especies

La principal interfaz entre los programas y el núcleo del sistema operativo en Unix son las llamadas al sistema (en inglés. system calls, syscalls), y la interacción de los programas con el mundo exterior se produce exclusivamente a través de ellas.

Pero en la primera versión pública de Unix (Version 6 Unix, 1975) no había formas cómodas de rastrear el comportamiento de los procesos de usuario. Para resolver este problema, Bell Labs en la próxima versión (Version 7 Unix, 1979) propusieron una nueva llamada al sistema — ptrace.

El ptrace fue desarrollado principalmente para depuradores interactivos, pero a finales de los 80 (en la época comercial ya de System V Release 4) surgieron y se popularizaron depuradores especializados basados en esto: rastreadores de llamadas al sistema.

Primero La misma versión de strace fue publicada por Paul Kronenburg en el mailing de comp.sources.sun en 1992 como alternativa a la herramienta cerrada trace de Sun. Tanto el clon como el original estaban destinados a SunOS, pero para 1994 strace fue portado a System V, Solaris y el cada vez más popular Linux.

Hoy en día, strace solo es compatible con Linux y se basa en el mismo ptrace, que ha crecido con numerosas extensiones.

El mantenedor moderno (y bastante activo) strace — Dmitry Levin. Gracias a él, la herramienta ha adquirido funcionalidades avanzadas como la inyección de errores en las llamadas al sistema, soporte para una amplia gama de arquitecturas y, lo más importante, una mascota. Fuentes no oficiales afirman que la elección recayó en el avestruz debido a la similitud entre la palabra rusa «страус» y el inglés "strace".

Es importante mencionar que la llamada al sistema ptrace y los rastreadores nunca fueron incluidos en POSIX, a pesar de su larga historia y de que hay implementación en Linux, FreeBSD, OpenBSD y Unix tradicionales.

A grandes rasgos, strace es: Piglet Trace

"No se espera que entiendas esto" (Dennis Ritchie, comentario en el código fuente de Version 6 Unix)

Desde la infancia, siempre he detestado las cajas negras: no jugaba con los juguetes, sino que intentaba entender cómo funcionaban (los adultos usaban la palabra «romper», pero no les crean). Quizás por eso me siento tan cercano a la cultura informal de los primeros Unix y al movimiento open-source moderno.

En el marco de este artículo, no es razonable analizar el código fuente del strace, que ha madurado durante décadas. Pero no debe haber secretos para los lectores. Por lo tanto, para mostrar el principio de funcionamiento de programas como strace, proporcionaré el código de un pequeño rastreador — Piglet Trace (ptr). No hace nada especial, pero lo principal — los llamados del sistema del programa — los muestra:

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

Piglet Trace reconoce alrededor de un centenar de llamadas al sistema de Linux (ver tabla) y funciona solo en la arquitectura x86-64. Para fines educativos, esto es suficiente.

Analicemos el funcionamiento de nuestro clon. En el caso de Linux, para depuradores y rastreadores se utiliza, como se mencionó anteriormente, la llamada al sistema ptrace. Funciona mediante la transmisión en el primer argumento de identificadores de comandos, de los cuales solo necesitamos PTRACE_TRACEME, PTRACE_SYSCALL y PTRACE_GETREGS.

El funcionamiento del rastreador comienza en un estilo típico de Unix: fork(2) inicia un proceso hijo, y este a su vez, mediante exec(3) lanza el programa en estudio. La única particularidad aquí es la llamada ptrace(PTRACE_TRACEME) antes de que exec: el proceso hijo espera que el proceso padre lo rastree:

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");
}

El proceso padre ahora debe llamar wait(2) en el proceso hijo, es decir, asegurarse de que el cambio al modo de trazado ha ocurrido:

/* 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");

Con esto, los preparativos han terminado y se puede proceder a rastrear las llamadas al sistema en un bucle infinito.

mountsnoop.py ptrace(PTRACE_SYSCALL) asegura que el siguiente esperar el padre se detendrá ya sea antes de la ejecución de la llamada al sistema, o inmediatamente después de su finalización. Entre dos llamadas se pueden realizar algunas acciones: reemplazar la llamada por una alternativa, modificar los argumentos o el valor de retorno.

Basta con llamar dos veces al comando ptrace(PTRACE_GETREGS), para obtener el estado del registro rax antes de la llamada (número de la llamada al sistema) y justo después (valor de retorno).

En efecto, el bucle:

/* 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);
}

Y ese es todo el trazador. Ahora sabes por dónde empezar el siguiente porting DTrace a Linux.

Fundamentos: ejecutar un programa bajo strace

Como primer ejemplo de uso strace, probablemente valga la pena mencionar la forma más sencilla: ejecutar una aplicación bajo strace.

Para no tener que hurgar en una lista interminable de llamadas de un programa típico, escribiremos un programa mínimo alrededor de write:

int main(int argc, char *argv[])
{
    char str[] = "escríbeme en stdoutn";
     write(2) es un simple envoltorio alrededor de una llamada al sistema, por lo que debería ser fácil de
     * encontrar en el rastro de la llamada al sistema. 
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Compilaremos el programa y aseguraremos que funciona:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
escríbeme en stdout

Y finalmente lo ejecutaremos bajo 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 >
Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster