Strace sous Linux : historique, conception et utilisation

Strace sous Linux : historique, conception 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 s'effectue via un petit ensemble de fonctions : les appels système. Cela signifie qu'à des fins de débogage, il peut être utile d'espionner les appels système exécutés par les processus.

Un utilitaire vous aide à surveiller la « vie intime » des programmes sous Linux strace, qui fait l’objet de cet article. Des exemples d'utilisation de matériel d'espionnage sont accompagnés d'un bref historique strace et une description de la conception de ces programmes.

Teneur

L'origine des espèces

L'interface principale entre les programmes et le noyau du système d'exploitation sous Unix est constituée des appels système. appels système, appels système), l'interaction des programmes avec le monde extérieur se fait exclusivement à travers eux.

Mais dans la première version publique d'Unix (Version6Unix, 1975), il n'existait aucun moyen pratique de suivre le comportement des processus utilisateur. Pour résoudre ce problème, les Bell Labs mettront à jour vers la version suivante (Version7Unix, 1979) a proposé un nouvel appel système - ptrace.

ptrace a été développé principalement pour les débogueurs interactifs, mais à la fin des années 80 (à l'ère du commerce Système V version 4) sur cette base, des débogueurs étroitement ciblés – des traceurs d’appels système – sont apparus et sont devenus largement utilisés.

première la même version de strace a été publiée par Paul Cronenburg sur la liste de diffusion comp.sources.sun en 1992 comme alternative à un utilitaire fermé trace du Soleil. Le clone et l'original étaient tous deux destinés à SunOS, mais en 1994 strace a été porté sur System V, Solaris et Linux, de plus en plus populaire.

Aujourd'hui, Strace ne prend en charge que Linux et s'appuie sur le même ptrace, envahi par de nombreuses extensions.

Mainteneur moderne (et très actif) strace - Dmitri Levine. Grâce à lui, l'utilitaire a acquis des fonctionnalités avancées telles que l'injection d'erreurs dans les appels système, la prise en charge d'un large éventail d'architectures et, surtout, mascotte. Des sources non officielles affirment que le choix s'est porté sur l'autruche en raison de la consonance entre le mot russe « autruche » et le mot anglais « strace ».

Il est également important que l'appel système ptrace et les traceurs n'aient jamais été inclus dans POSIX, malgré une longue histoire et une implémentation sous Linux, FreeBSD, OpenBSD et Unix traditionnel.

L'appareil Strace en quelques mots : Piglet Trace

"Vous n'êtes pas censé comprendre cela" (Dennis Ritchie, commentaire dans le code source de la version 6 d'Unix)

Depuis ma petite enfance, je ne supporte pas les boîtes noires : je ne jouais pas avec des jouets, mais j'essayais de comprendre leur structure (les adultes utilisaient le mot « cassé », mais ne croyaient pas aux mauvaises langues). C'est peut-être pour cela que la culture informelle du premier Unix et le mouvement open source moderne me sont si proches.

Pour les besoins de cet article, il n'est pas raisonnable de démonter le code source de strace, qui s'est développé au fil des décennies. Mais il ne devrait y avoir aucun secret pour les lecteurs. Par conséquent, pour montrer le principe de fonctionnement de tels programmes strace, je fournirai le code d'un traceur miniature - Trace de porcelet (ptr). Il ne sait rien faire de spécial, mais l'essentiel, ce sont les appels système du programme - il affiche :

$ 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 des centaines d'appels système Linux (voir. graphique) et ne fonctionne que sur l'architecture x86-64. Cela suffit à des fins éducatives.

Regardons le travail de notre clone. Dans le cas de Linux, les débogueurs et les traceurs utilisent, comme mentionné ci-dessus, l'appel système ptrace. Cela fonctionne en passant dans le premier argument les identifiants de commande, dont nous n'avons besoin que PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Le traceur démarre dans le style Unix habituel : fork(2) lance un processus enfant, qui à son tour utilise exec(3) lance le programme à l’étude. La seule subtilité ici est le défi ptrace(PTRACE_TRACEME) avant exec: Le processus enfant s'attend à ce que le processus parent le surveille :

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

Le processus parent devrait maintenant appeler wait(2) dans le processus enfant, c'est-à-dire assurez-vous que le passage en mode trace a eu lieu :

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

À ce stade, les préparatifs sont terminés et vous pouvez procéder directement au suivi des appels système dans une boucle sans fin.

Défi ptrace(PTRACE_SYSCALL) garantit que les wait Le parent se terminera soit avant l'exécution de l'appel système, soit immédiatement après sa fin. Entre deux appels vous pouvez effectuer n'importe quelle action : remplacer l'appel par un appel alternatif, modifier les arguments ou la valeur de retour.

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

En fait, le cycle :

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

C'est tout le traceur. Vous savez maintenant par où commencer le prochain portage DTrace sous Linux.

Notions de base : exécuter un programme exécutant strace

Comme premier cas d'utilisation strace, cela vaut peut-être la peine de citer le moyen le plus simple : lancer une application en cours d'exécution strace.

Afin de ne pas plonger dans la liste interminable d'appels d'un programme typique, écrivons programme minimum вокруг write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) is a simple wrapper around a syscall so it should be easy to
     * find in the syscall trace. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Construisons le programme et assurons-nous qu'il fonctionne :

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout

Et enfin, exécutons-le sous contrôle 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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Très « verbeux » et peu pédagogique. Il y a deux problèmes ici : la sortie du programme est mélangée à la sortie strace et une abondance d'appels système qui ne nous intéressent pas.

Vous pouvez séparer le flux de sortie standard du programme et la sortie d'erreur strace à l'aide du commutateur -o, qui redirige la liste des appels système vers un fichier d'arguments.

Reste à régler le problème des appels « supplémentaires ». Supposons que nous ne nous intéressons qu'aux appels write. Clé -e vous permet de spécifier des expressions par lesquelles les appels système seront filtrés. L’option de condition la plus populaire est, naturellement, trace=*, avec lequel vous pouvez laisser uniquement les appels qui nous intéressent.

Lorsqu'il est utilisé simultanément -o и -e Nous obtiendrons:

$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
)  = 20
+++ exited with 0 +++

Donc, vous voyez, c’est beaucoup plus facile à lire.

Vous pouvez également supprimer les appels système, par exemple ceux liés à l'allocation et à la libération de mémoire :

$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
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=124066, ...}) = 0
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, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20)  = 20
exit_group(0)                           = ?
+++ exited with 0 +++

Notez le point d'exclamation échappé dans la liste des appels exclus : ceci est requis par le shell de commande. coquille).

Dans ma version de la glibc, un appel système termine le processus exit_group, pas traditionnel _exit. C'est la difficulté de travailler avec les appels système : l'interface avec laquelle travaille le programmeur n'est pas directement liée aux appels système. De plus, il change régulièrement en fonction de l'implémentation et de la plateforme.

Les bases : rejoindre le processus à la volée

Initialement, l'appel système ptrace sur lequel il a été construit strace, ne peut être utilisé que lors de l'exécution du programme dans un mode spécial. Cette limitation a peut-être semblé raisonnable à l'époque de la version 6 d'Unix. De nos jours, cela ne suffit plus : il faut parfois enquêter sur les problèmes d'un programme qui fonctionne. Un exemple typique est un processus bloqué sur un handle ou en veille. Donc moderne strace peuvent rejoindre des processus à la volée.

Exemple de congélation programme:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Construisons le programme et assurons-nous qu'il est gelé :

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Essayons maintenant de le rejoindre :

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

Programme bloqué par appel pause. Voyons comment elle réagit aux signaux :

$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++

Nous avons lancé le programme gelé et l'avons rejoint en utilisant strace. Deux choses sont devenues claires : l'appel système pause ignore les signaux sans gestionnaires et, plus intéressant encore, strace surveille non seulement les appels système, mais également les signaux entrants.

Exemple : suivi des processus enfants

Travailler avec des processus via un appel fork - la base de tous les Unix. Voyons comment strace fonctionne avec un arbre de processus en utilisant l'exemple d'un simple « élevage » programme:

int main(int argc, char *argv[])
{
    pid_t parent_pid = getpid();
    pid_t child_pid = fork();
    if (child_pid == 0) {
        /* A child is born! */
        child_pid = getpid();

        /* In the end of the day printf is just a call to write(2). */
        printf("child (self=%d)n", child_pid);
        exit(EXIT_SUCCESS);
    }

    printf("parent (self=%d, child=%d)n", parent_pid, child_pid);

    wait(NULL);

    exit(EXIT_SUCCESS);
}

Ici, le processus d'origine crée un processus enfant, tous deux écrivant sur la sortie standard :

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

Par défaut, nous ne verrons que les appels système du processus parent :

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

L'indicateur vous aide à suivre l'ensemble de l'arborescence des processus -favec qui strace surveille les appels système dans les processus enfants. Cela ajoute à chaque ligne de sortie pid processus qui génère une sortie système :

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

Dans ce contexte, un filtrage par groupe d'appels système peut être utile :

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

Au fait, quel appel système est utilisé pour créer un nouveau processus ?

Exemple : chemins de fichiers au lieu de handles

Connaître les descripteurs de fichiers est certainement utile, mais les noms des fichiers spécifiques auxquels un programme accède peuvent également s'avérer utiles.

la prochaine programme écrit la ligne dans le fichier temporaire :

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    char tmp_filename_template[] = "/tmp/output_fileXXXXXX";

    int out_fd = mkstemp(tmp_filename_template);
    if (out_fd == -1) {
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Lors d'un appel normal strace affichera la valeur du numéro de descripteur transmis à l'appel système :

$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20)  = 20
+++ exited with 0 +++

Avec un drapeau -y L'utilitaire affiche le chemin d'accès au fichier auquel correspond le descripteur :

$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Exemple : suivi de l'accès aux fichiers

Autre fonctionnalité utile : afficher uniquement les appels système associés à un fichier spécifique. Suivant programme ajoute une ligne à un fichier arbitraire passé en argument :

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    /*
     * Path will be provided by the first program argument.
     *  */
    const char *path = argv[1];

    /*
     * Open an existing file for writing in append mode.
     *  */
    int out_fd = open(path, O_APPEND | O_WRONLY);
    if (out_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Par défaut strace affiche beaucoup d’informations inutiles. Drapeau -P avec un argument, strace n'imprime que les appels au fichier spécifié :

$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Exemple : programmes multithread

Utilitaire strace peut également aider lorsque vous travaillez avec du multithread le programme. Le programme suivant écrit sur la sortie standard à partir de deux flux :

void *thread(void *arg)
{
    (void) arg;

    printf("Secondary thread: workingn");
    sleep(1);
    printf("Secondary thread: donen");

    return NULL;
}

int main(int argc, char *argv[])
{
    printf("Initial thread: launching a threadn");

    pthread_t thr;
    if (0 != pthread_create(&thr, NULL, thread, NULL)) {
        fprintf(stderr, "Initial thread: failed to create a thread");
        exit(EXIT_FAILURE);
    }

    printf("Initial thread: joining a threadn");
    if (0 != pthread_join(thr, NULL)) {
        fprintf(stderr, "Initial thread: failed to join a thread");
        exit(EXIT_FAILURE);
    };

    printf("Initial thread: done");

    exit(EXIT_SUCCESS);
}

Naturellement, il doit être compilé avec un message d'accueil spécial adressé à l'éditeur de liens - l'indicateur -pthread :

$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$

Drapeau -f, comme dans le cas des processus réguliers, ajoutera le pid du processus au début de chaque ligne.

Naturellement, nous ne parlons pas d'un identifiant de thread au sens de l'implémentation du standard POSIX Threads, mais du numéro utilisé par le planificateur de tâches sous Linux. Du point de vue de ce dernier, il n’y a pas de processus ni de threads : il y a des tâches qui doivent être réparties entre les cœurs disponibles de la machine.

Lorsque vous travaillez dans plusieurs threads, les appels système deviennent trop nombreux :

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Il est logique de se limiter uniquement à la gestion des processus et aux appels système. write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Au fait, des questions. Quel appel système est utilisé pour créer un nouveau thread ? En quoi cet appel à threads diffère-t-il de l’appel à processus ?

Master class : pile de processus lors d'un appel système

L'un des récemment parus strace capacités - affichage de la pile d'appels de fonction au moment de l'appel système. Simple exemple:

void do_write(void)
{
    char str[] = "write me to stdoutn";
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    do_write();
    return EXIT_SUCCESS;
}

Naturellement, le résultat du programme devient très volumineux et, en plus du drapeau -k (affichage de la pile d'appels), il est logique de filtrer les appels système par nom :

$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20)  = 20
 > /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
 > /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
 > /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
 > /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
 > /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++

Master class : injection d'erreurs

Et encore une fonctionnalité nouvelle et très utile : l’injection d’erreurs. Ici programme, en écrivant deux lignes dans le flux de sortie :

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Traçons les deux appels d'écriture :

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Maintenant, nous utilisons l'expression injectinsérer une erreur EBADF dans tous les appels d'écriture :

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Il est intéressant de savoir quelles erreurs sont renvoyées tous défis write, y compris l'appel caché derrière la perror. Il est logique de renvoyer une erreur uniquement pour le premier des appels :

$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Ou le deuxième :

$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

Il n'est pas nécessaire de préciser le type d'erreur :

$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++

En combinaison avec d'autres indicateurs, vous pouvez « interrompre » l'accès à un fichier spécifique. Exemple:

$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++

Outre l'injection d'erreurs, on peut introduire des retards lors des appels ou de la réception de signaux.

Postface

Utilitaire strace - un outil simple et fiable. Mais en plus des appels système, d'autres aspects du fonctionnement des programmes et du système d'exploitation peuvent être débogués. Par exemple, il peut suivre les appels vers des bibliothèques liées dynamiquement. trace, ils peuvent examiner le fonctionnement du système d'exploitation SystèmeTap и tracer, et vous permet d'étudier en profondeur les performances du programme Perf. Néanmoins, c'est strace - la première ligne de défense en cas de problèmes avec mes propres programmes et ceux des autres, et je l'utilise au moins deux fois par semaine.

Bref, si vous aimez Unix, lisez man 1 strace et n'hésitez pas à jeter un œil à vos programmes !

Source: habr.com

Ajouter un commentaire