Strace in Linux: storia, progettazione e utilizzo

Strace in Linux: storia, progettazione e utilizzo

Nei sistemi operativi di tipo Unix, la comunicazione di un programma con il mondo esterno e il sistema operativo avviene attraverso un piccolo insieme di funzioni: chiamate di sistema. Ciò significa che per scopi di debug può essere utile spiare le chiamate di sistema eseguite dai processi.

Un'utilità ti aiuta a monitorare la "vita intima" dei programmi su Linux strace, che è l'oggetto di questo articolo. Esempi dell'uso di apparecchiature di spionaggio sono accompagnati da una breve storia strace e una descrizione della progettazione di tali programmi.

contenuto

Origine delle specie

L'interfaccia principale tra i programmi e il kernel del sistema operativo in Unix sono le chiamate di sistema. chiamate di sistema, chiamate di sistema), l'interazione dei programmi con il mondo esterno avviene esclusivamente attraverso di essi.

Ma nella prima versione pubblica di Unix (Versione 6 Unix, 1975) non esistevano metodi convenienti per tracciare il comportamento dei processi utente. Per risolvere questo problema, i Bell Labs aggiorneranno alla versione successiva (Versione 7 Unix, 1979) propose una nuova chiamata di sistema - ptrace.

ptrace è stato sviluppato principalmente per debugger interattivi, ma alla fine degli anni '80 (nell'era del commercio Sistema V versione 4) su questa base, apparvero e divennero ampiamente utilizzati debugger strettamente mirati, i traccianti delle chiamate di sistema.

prima la stessa versione di strace è stata pubblicata da Paul Cronenburg sulla mailing list comp.sources.sun nel 1992 come alternativa a un'utilità chiusa trace da sole. Sia il clone che l'originale erano destinati a SunOS, ma nel 1994 strace è stato portato su System V, Solaris e sul sempre più popolare Linux.

Oggi strace supporta solo Linux e si basa sullo stesso ptrace, ricoperta da numerose estensioni.

Manutentore moderno (e molto attivo). strace - Dmitrij Levin. Grazie a lui, l'utility ha acquisito funzionalità avanzate come l'inserimento di errori nelle chiamate di sistema, il supporto per un'ampia gamma di architetture e, soprattutto, mascotte. Fonti non ufficiali sostengono che la scelta sia caduta sullo struzzo a causa della consonanza tra la parola russa “struzzo” e la parola inglese “strace”.

È anche importante che la chiamata di sistema ptrace e i traccianti non siano mai stati inclusi in POSIX, nonostante una lunga storia e implementazione in Linux, FreeBSD, OpenBSD e Unix tradizionale.

Il dispositivo Strace in poche parole: Piglet Trace

"Non ci si aspetta che tu lo capisca" (Dennis Ritchie, commento nel codice sorgente Unix della versione 6)

Fin dalla prima infanzia, non sopporto le scatole nere: non giocavo con i giocattoli, ma cercavo di capirne la struttura (gli adulti usano la parola "rotto", ma non credono alle lingue malvagie). Forse è per questo che la cultura informale del primo Unix e il moderno movimento open source mi sono così vicini.

Ai fini di questo articolo, non è ragionevole smontare il codice sorgente di strace, che è cresciuto nel corso di decenni. Ma non dovrebbero esserci segreti per i lettori. Pertanto, per mostrare il principio di funzionamento di tali programmi strace, fornirò il codice per un tracciante in miniatura: Traccia di maialino (ptr). Non sa come fare nulla di speciale, ma la cosa principale sono le chiamate di sistema del programma: restituisce:

$ 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 centinaia di chiamate di sistema Linux (vedi. il tavolo) e funziona solo su architettura x86-64. Questo è sufficiente per scopi didattici.

Diamo un'occhiata al lavoro del nostro clone. Nel caso di Linux, debugger e traccianti utilizzano, come menzionato sopra, la chiamata di sistema ptrace. Funziona passando nel primo argomento gli identificatori del comando, di cui abbiamo solo bisogno PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Il tracciante inizia nel solito stile Unix: fork(2) avvia un processo figlio, che a sua volta utilizza exec(3) avvia il programma in studio. L’unica sottigliezza qui è la sfida ptrace(PTRACE_TRACEME) prima di exec: Il processo figlio si aspetta che il processo genitore lo monitori:

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

Il processo genitore ora dovrebbe chiamare wait(2) nel processo figlio, ovvero assicurarsi che sia avvenuto il passaggio alla modalità traccia:

/* 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 completi e puoi procedere direttamente al monitoraggio delle chiamate di sistema in un ciclo infinito.

chiamata ptrace(PTRACE_SYSCALL) garantisce che il successivo wait parent completerà prima che la chiamata di sistema venga eseguita o immediatamente dopo il suo completamento. Tra due chiamate è possibile eseguire qualsiasi azione: sostituire la chiamata con una alternativa, modificare gli argomenti o il valore restituito.

Dobbiamo solo chiamare il comando due volte ptrace(PTRACE_GETREGS)per ottenere lo stato del registro rax prima della chiamata (numero di chiamata di sistema) e immediatamente dopo (valore restituito).

In realtà, 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 tracciante. Ora sai da dove iniziare il prossimo porting DTrace su Linux.

Nozioni di base: eseguire un programma che esegue strace

Come primo caso d'uso strace, forse vale la pena citare il metodo più semplice: avviare un'applicazione in esecuzione strace.

Per non addentrarci nell'elenco infinito di chiamate di un tipico programma, scriviamo programma minimo вокруг 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;
}

Costruiamo il programma e assicuriamoci che funzioni:

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

E infine, eseguiamolo sotto controllo 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)                           = ?

Molto “prolisso” e poco istruttivo. Ci sono due problemi qui: l'output del programma è mescolato con l'output strace e un'abbondanza di chiamate di sistema che non ci interessano.

È possibile separare il flusso di output standard del programma e l'output degli errori stracciati utilizzando l'opzione -o, che reindirizza l'elenco delle chiamate di sistema a un file di argomenti.

Resta da affrontare il problema delle chiamate “extra”. Supponiamo che a noi interessino solo le chiamate write. Chiave -e consente di specificare le espressioni in base alle quali verranno filtrate le chiamate di sistema. L'opzione di condizione più popolare è, naturalmente, trace=*, con il quale puoi lasciare solo le chiamate che ci interessano.

Se utilizzati contemporaneamente -o и -e otterremo:

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

Quindi, vedi, è molto più facile da leggere.

Puoi anche rimuovere le chiamate di sistema, ad esempio quelle relative all'allocazione e alla liberazione della memoria:

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

Da notare il punto esclamativo escape nell'elenco delle chiamate escluse: questo è richiesto dalla shell dei comandi. conchiglia).

Nella mia versione di glibc, una chiamata di sistema termina il processo exit_group, non tradizionale _exit. Questa è la difficoltà di lavorare con le chiamate di sistema: l'interfaccia con cui lavora il programmatore non è direttamente correlata alle chiamate di sistema. Inoltre, cambia regolarmente a seconda dell'implementazione e della piattaforma.

Nozioni di base: partecipare al processo al volo

Inizialmente, la chiamata di sistema ptrace su cui è stato costruito strace, può essere utilizzato solo quando si esegue il programma in una modalità speciale. Questa limitazione poteva sembrare ragionevole ai tempi della versione 6 di Unix. Al giorno d'oggi questo non basta più: a volte è necessario approfondire le problematiche di un programma funzionante. Un tipico esempio è un processo bloccato su una maniglia o in stato di stop. Quindi moderno strace può unirsi ai processi al volo.

Esempio di congelamento programma:

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

Costruiamo il programma e assicuriamoci che sia congelato:

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

Ora proviamo ad unirci ad esso:

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

Programma bloccato dalla chiamata pause. Vediamo come reagisce ai segnali:

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

Abbiamo lanciato il programma Frozen e ci siamo uniti utilizzando strace. Due cose sono diventate chiare: la chiamata di sistema pause ignora i segnali senza gestori e, cosa più interessante, strace monitora non solo le chiamate di sistema, ma anche i segnali in entrata.

Esempio: monitoraggio dei processi secondari

Lavorare con i processi attraverso una chiamata fork - la base di tutti gli Unix. Vediamo come funziona strace con un albero di processi utilizzando l'esempio di un semplice “breeding” programma:

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

Qui il processo originale crea un processo figlio, entrambi scritti sullo standard output:

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

Per impostazione predefinita, vedremo solo le chiamate di sistema dal processo principale:

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

Il flag ti aiuta a tenere traccia dell'intero albero del processo -fcon cui strace monitora le chiamate di sistema nei processi figli. Questo si aggiunge a ogni riga di output pid processo che produce un output di sistema:

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

In questo contesto può essere utile filtrare per gruppo di chiamate di sistema:

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

A proposito, quale chiamata di sistema viene utilizzata per creare un nuovo processo?

Esempio: percorsi di file invece di handle

Conoscere i descrittori di file è sicuramente utile, ma possono tornare utili anche i nomi dei file specifici a cui accede un programma.

il prossimo programma scrive la riga in un file temporaneo:

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

Durante una chiamata normale strace mostrerà il valore del numero del descrittore passato alla chiamata di sistema:

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

Con una bandiera -y L'utilità mostra il percorso del file a cui corrisponde il descrittore:

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

Esempio: monitoraggio dell'accesso ai file

Un'altra caratteristica utile: mostra solo le chiamate di sistema associate a un file specifico. Prossimo programma aggiunge una riga a un file arbitrario passato come argomento:

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

Per impostazione predefinita strace visualizza molte informazioni non necessarie. Bandiera -P con un argomento fa sì che strace stampi solo le chiamate al file specificato:

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

Esempio: programmi multithread

Utilità strace può anche aiutare quando si lavora con il multi-thread il programma. Il seguente programma scrive sull'output standard da due flussi:

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

Naturalmente, deve essere compilato con un saluto speciale al linker: il flag -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
$

bandiera -f, come nel caso dei processi regolari, aggiungerà il pid del processo all'inizio di ogni riga.

Naturalmente non stiamo parlando di un identificatore di thread nel senso di implementazione dello standard POSIX Threads, ma del numero utilizzato dall'utilità di pianificazione in Linux. Dal punto di vista di quest’ultimo, non ci sono processi o thread: ci sono compiti che devono essere distribuiti tra i core disponibili della macchina.

Quando si lavora su più thread, le chiamate di sistema diventano troppe:

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

Ha senso limitarsi solo alla gestione dei processi e alle chiamate di sistema 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 +++

A proposito, domande. Quale chiamata di sistema viene utilizzata per creare un nuovo thread? In cosa differisce questa chiamata per i thread dalla chiamata per i processi?

Master class: stack di processi al momento di una chiamata di sistema

Uno di quelli apparsi di recente strace funzionalità: visualizzazione dello stack di chiamate di funzione al momento della chiamata di sistema. Semplice esempio:

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

Naturalmente l'output del programma diventa molto voluminoso e, oltre alla bandiera -k (visualizzazione dello stack di chiamate), è opportuno filtrare le chiamate di sistema per nome:

$ 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: iniezione di errori

E un'altra funzionalità nuova e molto utile: l'inserimento degli errori. Qui programma, scrivendo due righe nel flusso di output:

#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;
}

Tracciamo entrambe le chiamate di scrittura:

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

Ora usiamo l'espressione injectper inserire un errore EBADF in tutte le chiamate di scrittura:

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

È interessante quali errori vengono restituiti tutti sfide write, inclusa la chiamata nascosta dietro perror. Ha senso restituire un errore solo per la prima delle chiamate:

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

Oppure il secondo:

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

Non è necessario specificare il tipo di errore:

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

In combinazione con altri flag, puoi "interrompere" l'accesso a un file specifico. Esempio:

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

Oltre all'inserimento degli errori, si può introdurre ritardi nell'effettuare chiamate o ricevere segnali.

postfazione

Utilità strace - uno strumento semplice e affidabile. Ma oltre alle chiamate di sistema, è possibile eseguire il debug di altri aspetti del funzionamento dei programmi e del sistema operativo. Ad esempio, può tenere traccia delle chiamate alle librerie collegate dinamicamente. traccia, possono esaminare il funzionamento del sistema operativo Sistema Tocca и tracciae consente di analizzare in modo approfondito le prestazioni del programma perf. Tuttavia, lo è strace - la prima linea di difesa in caso di problemi con i programmi miei e degli altri, e lo utilizzo almeno un paio di volte a settimana.

In breve, se ami Unix, leggi man 1 strace e sentiti libero di dare un'occhiata ai tuoi programmi!

Fonte: habr.com

Aggiungi un commento