Strace no Linux: história, estrutura e utilização

Strace no Linux: história, estrutura e utilização

Em sistemas operacionais baseados em Unix, a comunicação do programa com o mundo externo e o sistema operacional ocorre por meio de um pequeno conjunto de funções — chamadas de sistema. Portanto, para fins de depuração, pode ser útil espiar os processos executados por chamadas de sistema.

Acompanhando a "vida íntima" dos programas no Linux, a ferramenta strace, que é o foco deste artigo. Juntamente com exemplos de uso de equipamentos de espionagem, apresenta-se uma breve história strace e descrição do funcionamento de programas semelhantes.

Conteúdo

A Origem das Espécies

A principal interface entre programas e o núcleo do SO em Unix são as chamadas de sistema (em inglês. system calls, além desses dois, utilizei apenas eles.), a interação dos programas com o mundo externo ocorre exclusivamente através delas.

Mas na primeira versão pública do Unix (Version 6 Unix, ano de 1975) não existiam formas convenientes de rastrear o comportamento de processos de usuários. Para resolver esse problema, os Bell Labs sugeriram na próxima versão (Version 7 Unix, ano de 1979) uma nova chamada de sistema — ptrace.

O ptrace foi desenvolvido principalmente para depuradores interativos, mas no final dos anos 80 (na época já comercial, System V Release 4) com base nele surgiram e se tornaram amplamente disseminados depuradores específicos — rastreadores de chamadas de sistema.

Primeiro A mesma versão do strace foi publicada por Paul Kronenburg na lista de discussão comp.sources.sun em 1992 como uma alternativa à ferramenta proprietária trace da Sun. Tanto o clone quanto o original eram destinados ao SunOS, mas até 1994 strace foi portado para System V, Solaris e o crescente popular Linux.

Hoje, o strace suporta apenas Linux e se baseia no mesmo ptrace, que foi enriquecido com uma variedade de extensões.

O mantenedor moderno (e bastante ativo) strace — Dmitry Levin. Graças a ele, a ferramenta ganhou recursos avançados como injeção de erros em chamadas de sistema, suporte a uma ampla gama de arquiteturas e, o mais importante, um mascote. Fontes não oficiais afirmam que a escolha do avestruz se deu pela semelhança entre a palavra russa "страус" e o inglês "strace".

É importante notar que a chamada de sistema ptrace e os depuradores nunca foram incluídos no POSIX, apesar da longa história e da existência de implementações no Linux, FreeBSD, OpenBSD e Unix tradicional.

O strace em poucas palavras: Piglet Trace

"You are not expected to understand this" (Dennis Ritchie, comentário no código-fonte do Version 6 Unix)

Desde a infância, eu não consigo suportar caixas pretas: não brinquei com brinquedos, mas tentei entender como eles funcionavam (os adultos usavam a palavra "quebrar", mas não acreditem em línguas malignas). Talvez por isso eu me identifique tanto com a cultura informal dos primeiros Unix e do moderno movimento open-source.

Dentro deste artigo, não faz sentido analisar o código-fonte do strace amadurecido ao longo das décadas. Mas também não deve haver segredos para os leitores. Portanto, para mostrar o princípio de funcionamento de programas semelhantes ao strace, apresentarei o código de um mini depurador — Piglet Trace (ptr). Ele não faz nada de especial, mas a principal função — imprimir as chamadas de sistema do programa — ele faz:

$ 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

O Piglet Trace reconhece cerca de cem chamadas de sistema do Linux (veja a tabela) e funciona apenas na arquitetura x86-64. Para fins educacionais, isso é suficiente.

Vamos analisar o funcionamento do nosso clone. No caso do Linux, para depuradores e rastreadores, é utilizado, como mencionado acima, a chamada de sistema ptrace. Ele funciona através da transmissão de identificadores de comandos no primeiro argumento, dos quais precisamos apenas de PTRACE_TRACEME, PTRACE_SYSCALL e PTRACE_GETREGS.

O funcionamento do depurador começa de uma maneira comum ao Unix: fork(2) lança um processo filho, e este, por sua vez, com o auxílio de exec(3) inicia o programa em estudo. A única sutileza aqui é a chamada ptrace(PTRACE_TRACEME) antes exec: o processo filho espera que o processo pai o rastreie:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Criança aqui */
    /* Um modo de rastreamento deve ser ativado. Um pai terá que esperar(2) para isso
     * acontecer. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Substitui-se por um programa a ser executado. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

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

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

Com isso, os preparativos estão concluídos e podemos começar a rastrear chamadas de sistema em um loop infinito.

Chamada ptrace(PTRACE_SYSCALL) garante que a próxima wait do pai termine antes da execução da chamada de sistema ou imediatamente após a sua conclusão. Entre as duas chamadas, podem ser realizadas algumas ações: substituir a chamada por uma alternativa, alterar argumentos ou o valor retornado.

Para nós, é suficiente chamar duas vezes o comando ptrace(PTRACE_GETREGS), para obter o estado do registrador rax antes da chamada (número da chamada de sistema) e imediatamente depois (valor retornado).

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

E esse é todo o rastreador. Agora você sabe por onde começar a próxima portabilidade DTrace para Linux.

Fundamentos: executar um programa sob o strace

Como primeiro exemplo de uso strace, talvez valha a pena apresentar a maneira mais simples - executar um aplicativo sob strace.

Para não vasculhar uma lista interminável de chamadas de uma programação típica, vamos escrever um programa mínimo em torno de exit:

int main(int argc, char *argv[])
{
    char str[] = "escreva-me para stdoutn";
    /* write(2) é um simples wrapper em torno de uma chamada de sistema, portanto, deve ser fácil de
     * encontrar no rastreamento da chamada de sistema. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Vamos compilar o programa e garantir que ele funciona:

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

E finalmente, vamos executá-lo sob o strace:

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113        3 >
Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster