Strace sous Linux : histoire, structure et utilisation

Strace sous Linux : histoire, structure et utilisation

Dans les systèmes d'exploitation de type Unix, la communication d'un programme avec le monde extérieur et le système d'exploitation se fait par un petit ensemble de fonctions, appelées appels système. Ainsi, pour des raisons de débogage, il est parfois utile de surveiller les processus exécutés par ces appels système.

Pour suivre la "vie intime" des programmes sous Linux, on utilise un outil appelé strace, qui est le sujet de cet article. Un aperçu de l'utilisation de ce "matériel d'espionnage" est accompagné d'une brève histoire strace et d'une description du fonctionnement de tels programmes.

Contenu

Origine des espèces

L'interface principale entre les programmes et le noyau de l'OS sous Unix est les appels système (en anglais system calls, syscalls), l'interaction des programmes avec le monde extérieur se fait exclusivement par leur intermédiaire.

Mais dans la première version publique d'Unix (Version 6 Unix, 1975), il n'y avait pas d'options pratiques pour suivre le comportement des processus utilisateurs. Pour résoudre ce problème, Bell Labs ont proposé un nouvel appel système dans la version suivante (Version 7 Unix, 1979) : ptrace.

Le ptrace a été principalement développé pour les débogueurs interactifs, mais à la fin des années 80 (à l'époque de la version commerciale déjà System V Release 4) sur cette base sont apparus et se sont largement répandus des débogueurs spécialisés — des traceurs d'appels système.

Premier La même version de strace a été publiée par Paul Kronenberg dans la liste de diffusion comp.sources.sun en 1992 comme une alternative à l'outil fermé trace de Sun. Tant le clone que l'original étaient destinés à SunOS, mais en 1994, strace il a été porté sur System V, Solaris et le Linux en pleine croissance.

Aujourd'hui, strace ne prend en charge que Linux et repose toujours sur ce même ptrace, enrichi de nombreuses extensions.

Le mainteneur moderne (et très actif) strace — Dmitry Levin. Grâce à lui, l'outil a acquis des fonctionnalités avancées telles que l'injection d'erreurs dans les appels système, le support d'un large éventail d'architectures et, surtout, une mascotte. Des sources non officielles affirment que le choix s'est porté sur l' autruche en raison de la similarité du mot russe « страус » avec l'anglais "strace".

Il est également important de noter que l'appel système ptrace et les traceurs n'ont toujours pas été inclus dans POSIX, malgré une longue histoire et leur implémentation dans Linux, FreeBSD, OpenBSD et les Unix traditionnels.

L'outil strace en deux mots : Piglet Trace

"You are not expected to understand this" (Dennis Ritchie, commentaire dans le code source de Version 6 Unix)

Depuis mon enfance, je ne peux pas supporter les boîtes noires : je n'y jouais pas, j'essayais de comprendre leur fonctionnement (les adultes utilisaient le mot « casser », mais ne croyez pas les mauvaises langues). C'est peut-être pourquoi la culture informelle des premiers Unix et du mouvement open-source moderne me touche tant.

Dans le cadre de cet article, il est peu judicieux d'examiner le code source de strace, qui a mûri au fil des décennies. Mais il ne doit pas rester de mystères pour les lecteurs. Ainsi, pour illustrer le principe de fonctionnement de programmes comme strace, je vais fournir le code d'un traceur miniature — Piglet Trace (ptr). Il ne fait rien d'extraordinaire, mais l'essentiel est qu'il affiche les appels systèmes du programme :

$ 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 reconnaît environ une centaine d'appels systèmes Linux (voir la tableau) et fonctionne uniquement sur l'architecture x86-64. Pour des fins pédagogiques, cela suffit.

Examinons le fonctionnement de notre clone. Dans le cas de Linux, pour les débogueurs et les traceurs, l'appel système ptrace est utilisé, comme mentionné précédemment. Il fonctionne en transmettant en tant que premier argument les identifiants de commande, dont nous ne retenons que PTRACE_TRACEME, PTRACE_SYSCALL et PTRACE_GETREGS.

Le fonctionnement du traceur commence dans un style Unix classique : fork(2) lance un processus fils, qui à son tour exécute le programme examiné à l'aide de exec(3) . La seule particularité ici est l'appel ptrace(PTRACE_TRACEME) avant exec: le processus fils attend que le processus parent l'observe :

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    
    /* L'enfant ici */
    /* Un mode de traçage doit être activé. Un parent devra attendre(2) que cela
     * se produise. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Remplacer lui-même par un programme à exécuter. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

Le processus parent doit maintenant appeler wait(2) dans le processus enfant, c'est-à-dire s'assurer que la commutation en mode de traçage s'est produite :

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

Les préparatifs sont désormais terminés et nous pouvons commencer à suivre les appels système dans une boucle infinie.

de system-nspawn ptrace(PTRACE_SYSCALL) garantit que le suivant wait du parent se terminera soit avant l'exécution de l'appel système, soit immédiatement après son achèvement. Entre les deux appels, nous pouvons effectuer des actions : remplacer l'appel par un alternatif, modifier les arguments ou la valeur de retour.

Il nous suffit d'appeler deux fois la commande ptrace(PTRACE_GETREGS), pour obtenir l'état du registre rax avant l'appel (numéro de l'appel système) et immédiatement après (valeur de retour).

En fait, la boucle :

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

Voilà, c'est tout pour le traceur. Maintenant, vous savez par où commencer pour un autre portage DTrace sur Linux.

Les bases : lancer un programme sous strace

Comme premier exemple d'utilisation strace, il vaut sans doute la peine de mentionner la manière la plus simple : exécuter une application sous le contrôle de strace.

Pour ne pas éplucher une liste interminable d'appels d'un programme typique, écrivons un programme minimal autour de write:

int main(int argc, char *argv[])
{
    char str[] = "écrivez-moi vers stdoutn";
    /* write(2) est un simple wrapper autour d'un appel système, il devrait donc être facile de
     * le trouver dans la trace des appels système. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Build the program and make sure it works:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
écrivez-moi vers stdout

Et enfin, exécutons-le sous strace :

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (Aucun fichier ou répertoire de ce type)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (Aucun fichier ou répertoire de ce type)
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 (Aucun fichier ou répertoire de ce type)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster