
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 (, 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 (, 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, ) com base nele surgiram e se tornaram amplamente disseminados depuradores específicos — rastreadores de chamadas de sistema.
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 — . 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, . 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 — (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 terminatedO Piglet Trace reconhece cerca de cem chamadas de sistema do Linux (veja a ) 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, ®isters) == -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, ®isters) == -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 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 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 stdoutE 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 >