Strace no Linux: história, design e uso

Strace no Linux: história, design e uso

Em sistemas operacionais do tipo Unix, a comunicação de um programa com o mundo exterior e com o sistema operacional ocorre por meio de um pequeno conjunto de funções - chamadas de sistema. Isso significa que, para fins de depuração, pode ser útil espionar as chamadas do sistema executadas pelos processos.

Um utilitário ajuda a monitorar a “vida íntima” de programas no Linux strace, que é o assunto deste artigo. Exemplos de utilização de equipamentos de espionagem são acompanhados de um breve histórico strace e uma descrição do design de tais programas.

Conteúdo

Origem das especies

A principal interface entre os programas e o kernel do sistema operacional no Unix são as chamadas de sistema. chamadas de sistema, chamadas de sistema), a interação dos programas com o mundo exterior ocorre exclusivamente por meio deles.

Mas na primeira versão pública do Unix (Versão 6 Unix, 1975) não havia maneiras convenientes de rastrear o comportamento dos processos do usuário. Para resolver esse problema, o Bell Labs atualizará para a próxima versão (Versão 7 Unix, 1979) propôs uma nova chamada de sistema - ptrace.

ptrace foi desenvolvido principalmente para depuradores interativos, mas no final dos anos 80 (na era do comercial Versão 4 do System V) com base nisso, depuradores com foco restrito – rastreadores de chamadas de sistema – apareceram e se tornaram amplamente utilizados.

primeiro a mesma versão do strace foi publicada por Paul Cronenburg na lista de discussão comp.sources.sun em 1992 como uma alternativa a um utilitário fechado trace do Sol. Tanto o clone quanto o original foram destinados ao SunOS, mas em 1994 strace foi portado para System V, Solaris e o cada vez mais popular Linux.

Hoje o strace suporta apenas Linux e depende do mesmo ptrace, coberto de muitas extensões.

Mantenedor moderno (e muito ativo) strace - Dmitry Levin. Graças a ele o utilitário adquiriu recursos avançados como injeção de erros em chamadas de sistema suporte para uma ampla gama de arquiteturas e o mais importante mascote. Fontes não oficiais afirmam que a escolha recaiu sobre o avestruz devido à consonância entre a palavra russa “avestruz” e a palavra inglesa “strace”.

Também é importante que a chamada do sistema ptrace e os rastreadores nunca tenham sido incluídos no POSIX, apesar de uma longa história e implementação em Linux, FreeBSD, OpenBSD e Unix tradicional.

Resumindo o dispositivo Strace: Piglet Trace

"Não se espera que você entenda isso" (Dennis Ritchie, comentário no código-fonte Unix da versão 6)

Desde criança não suporto caixas pretas: não brincava com brinquedos, mas tentava entender sua estrutura (os adultos usavam a palavra “quebrado”, mas não acreditam nas línguas malignas). Talvez seja por isso que a cultura informal do primeiro Unix e o movimento moderno de código aberto estão tão próximos de mim.

Para os fins deste artigo, não é razoável desmontar o código-fonte do strace, que cresceu ao longo das décadas. Mas não deve haver segredos para os leitores. Portanto, para mostrar o princípio de funcionamento de tais programas strace, fornecerei o código para um rastreador em miniatura - Rastreamento de Leitão (ptr). Ele não sabe fazer nada de especial, mas o principal são as chamadas de sistema do programa - ele gera:

$ 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 reconhece centenas de chamadas de sistema Linux (consulte. a mesa) e funciona apenas na arquitetura x86-64. Isto é suficiente para fins educacionais.

Vejamos o trabalho do nosso clone. No caso do Linux, depuradores e rastreadores usam, conforme mencionado acima, a chamada de sistema ptrace. Funciona passando no primeiro argumento os identificadores do comando, dos quais só precisamos PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

O rastreador inicia no estilo Unix usual: fork(2) lança um processo filho, que por sua vez usa exec(3) lança o programa em estudo. A única sutileza aqui é o desafio ptrace(PTRACE_TRACEME) antes exec: o processo filho espera que o processo pai o monitore:

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

O processo pai agora deve chamar wait(2) no processo filho, ou seja, certifique-se de que ocorreu a mudança para o modo de rastreamento:

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

Neste ponto, os preparativos estão concluídos e você pode prosseguir diretamente para o rastreamento das chamadas do sistema em um loop infinito.

Desafio ptrace(PTRACE_SYSCALL) garante que subsequentes wait pai será concluído antes da chamada do sistema ser executada ou imediatamente após sua conclusão. Entre duas chamadas você pode realizar qualquer ação: substituir a chamada por uma alternativa, alterar os argumentos ou o valor de retorno.

Só precisamos chamar o comando duas vezes ptrace(PTRACE_GETREGS)para obter o estado do registro rax antes da chamada (número de chamada do sistema) e imediatamente depois (valor de retorno).

Na verdade, o 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);
}

Esse é o rastreador completo. Agora você sabe por onde começar a próxima portabilidade DTrace no Linux.

Noções básicas: executando um programa executando strace

Como primeiro caso de uso strace, talvez valha a pena citar o método mais simples - iniciar um aplicativo em execução strace.

Para não nos aprofundarmos na lista interminável de chamadas de um programa típico, escrevemos programa mínimo ao redor 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;
}

Vamos construir o programa e verificar se funciona:

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

E finalmente, vamos executá-lo sob controle 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)                           = ?

Muito “prolixo” e pouco educativo. Existem dois problemas aqui: a saída do programa é misturada com a saída strace e uma abundância de chamadas de sistema que não nos interessam.

Você pode separar o fluxo de saída padrão do programa e a saída de erro strace usando a opção -o, que redireciona a lista de chamadas do sistema para um arquivo de argumento.

Resta lidar com o problema das chamadas “extras”. Vamos supor que estamos interessados ​​apenas em chamadas write. Chave -e permite especificar expressões pelas quais as chamadas do sistema serão filtradas. A opção de condição mais popular é, naturalmente, trace=*, com o qual você pode deixar apenas as ligações que nos interessam.

Quando usado simultaneamente -o и -e nós conseguiremos:

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

Então, você vê, é muito mais fácil de ler.

Você também pode remover chamadas do sistema, por exemplo, aquelas relacionadas à alocação e liberação de memória:

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

Observe o ponto de exclamação com escape na lista de chamadas excluídas: isso é exigido pelo shell de comando. concha).

Na minha versão do glibc, uma chamada de sistema encerra o processo exit_group, não tradicional _exit. Essa é a dificuldade de trabalhar com chamadas de sistema: a interface com a qual o programador trabalha não está diretamente relacionada às chamadas de sistema. Além disso, muda regularmente dependendo da implementação e da plataforma.

Noções básicas: ingressar no processo rapidamente

Inicialmente, a chamada do sistema ptrace na qual foi construído strace, só poderia ser usado ao executar o programa em um modo especial. Essa limitação pode ter parecido razoável na época da Versão 6 do Unix. Hoje em dia isso não é mais suficiente: às vezes é preciso investigar os problemas de um programa em funcionamento. Um exemplo típico é um processo bloqueado em um identificador ou em suspensão. Portanto moderno strace pode ingressar em processos dinamicamente.

Exemplo de congelamento programa:

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

Vamos construir o programa e garantir que ele esteja congelado:

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

Agora vamos tentar participar:

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

Programa bloqueado por chamada pause. Vamos ver como ela reage aos sinais:

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

Lançamos o programa congelado e aderimos a ele usando strace. Duas coisas ficaram claras: a chamada do sistema pause ignora sinais sem manipuladores e, o que é mais interessante, o strace monitora não apenas as chamadas do sistema, mas também os sinais recebidos.

Exemplo: Rastreando Processos Filhos

Trabalhando com processos por meio de uma chamada fork - a base de todos os Unixes. Vamos ver como o strace funciona com uma árvore de processos usando o exemplo de um simples “reprodução” programa:

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

Aqui o processo original cria um processo filho, ambos gravando na saída padrão:

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

Por padrão, veremos apenas chamadas de sistema do processo pai:

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

O sinalizador ajuda a rastrear toda a árvore do processo -fcom o qual strace monitora chamadas do sistema em processos filhos. Isso adiciona a cada linha de saída pid processo que gera uma saída do 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 +++

Neste contexto, a filtragem por grupo de chamadas de sistema pode ser útil:

$ 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 propósito, qual chamada de sistema é usada para criar um novo processo?

Exemplo: caminhos de arquivo em vez de identificadores

Conhecer os descritores de arquivos é certamente útil, mas os nomes dos arquivos específicos que um programa acessa também podem ser úteis.

o próximo programa grava a linha no arquivo temporário:

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 uma chamada normal strace mostrará o valor do número do descritor passado para a chamada do 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 +++

Com uma bandeira -y O utilitário mostra o caminho para o arquivo ao qual o descritor corresponde:

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

Exemplo: rastreamento de acesso a arquivos

Outro recurso útil: exibe apenas chamadas de sistema associadas a um arquivo específico. Próximo programa anexa uma linha a um arquivo arbitrário passado como argumento:

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

Por padrão strace exibe muitas informações desnecessárias. Bandeira -P com um argumento faz com que o strace imprima apenas chamadas para o arquivo especificado:

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

Exemplo: programas multithread

Utilitário strace também pode ajudar ao trabalhar com multithread programa. O programa a seguir grava na saída padrão de dois fluxos:

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, ele deve ser compilado com uma saudação especial ao vinculador - o sinalizador -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
$

Bandeira -f, como no caso de processos regulares, adicionará o pid do processo ao início de cada linha.

Naturalmente, não estamos falando de um identificador de thread no sentido da implementação do padrão POSIX Threads, mas do número usado pelo agendador de tarefas no Linux. Do ponto de vista deste último, não existem processos ou threads – existem tarefas que precisam ser distribuídas entre os núcleos disponíveis da máquina.

Ao trabalhar em vários threads, as chamadas do sistema tornam-se muitas:

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

Faz sentido limitar-se apenas ao gerenciamento de processos e chamadas de 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 propósito, perguntas. Qual chamada de sistema é usada para criar um novo thread? Como essa chamada por threads difere da chamada por processos?

Master class: pilha de processos no momento de uma chamada do sistema

Um dos que apareceu recentemente strace capacidades - exibindo a pilha de chamadas de função no momento da chamada do sistema. Simples exemplo:

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, a saída do programa torna-se muito volumosa e, além da bandeira -k (exibição da pilha de chamadas), faz sentido filtrar as chamadas do sistema por 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: injeção de erro

E mais um recurso novo e muito útil: injeção de erros. Aqui programa, escrevendo duas linhas no fluxo de saída:

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

Vamos rastrear ambas as chamadas de gravação:

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

Agora usamos a expressão injectinserir um erro EBADF em todas as chamadas de gravação:

$ 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 quais erros são retornados todos desafios write, incluindo a chamada escondida atrás de perror. Só faz sentido retornar um erro para a primeira das chamadas:

$ 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 o segundo:

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

Não é necessário especificar o tipo de erro:

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

Em combinação com outros sinalizadores, você pode “quebrar” o acesso a um arquivo específico. Exemplo:

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

Além da injeção de erros, uma lata introduzir atrasos ao fazer chamadas ou receber sinais.

Posfácio

Utilitário strace - uma ferramenta simples e confiável. Mas, além das chamadas do sistema, outros aspectos da operação dos programas e do sistema operacional podem ser depurados. Por exemplo, ele pode rastrear chamadas para bibliotecas vinculadas dinamicamente. traço, eles podem examinar a operação do sistema operacional SystemTap и traçoe permite investigar profundamente o desempenho do programa perf. No entanto, é strace - a primeira linha de defesa em caso de problemas com os meus próprios programas e com os programas de outras pessoas, e utilizo-o pelo menos algumas vezes por semana.

Resumindo, se você ama Unix, leia man 1 strace e fique à vontade para dar uma olhada em seus programas!

Fonte: habr.com

Adicionar um comentário