Analisando aplicações Linux com strace e ltrace
- por Sergio Prado
Alguma vez você já tentou executar uma aplicação de linha de comando em Linux que simplesmente retornava sem exibir nenhuma mensagem de erro? Ou então um erro de segmentation fault que não fazia sentido? Já precisou entender porque uma aplicação estava demorando demais para executar? Ou travando sem nenhuma explicação?
Estas são situações bastante comuns em Linux, seja no universo desktop ou embarcado. Mas você não precisa se desesperar! Você esta em um ambiente ideal para debugar aplicações.
Olhe só este exemplo. O netcat (ou nc) é uma popular ferramenta de rede para trabalhar com o protocolo TCP/IP. O comando abaixo visa se conectar em um servidor na máquina local e na porta 1234.
$ nc localhost 1234 $ |
Veja que o comando simplesmente retornou sem exibir nenhuma mensagem de erro. O que aconteceu? Qual a melhor forma de analisar este tipo de situação?
É aí que entram as ferramentas strace e ltrace.
STRACE
O strace é uma ferramenta que monitora as chamadas de sistema (system calls) e os sinais recebidos pela aplicação. A maneira mais comum de executá-la é passando a aplicação a ser monitorada como parâmetro.
Voltando ao nosso exemplo, veja como ela funciona:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ strace nc localhost 1234 execve("/bin/nc", ["nc", "localhost", "2000"], [/* 37 vars */]) = 0 brk(0) = 0x9864000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7835000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=127096, ...}) = 0 mmap2(NULL, 127096, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7815000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) .......... socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 fcntl64(3, F_GETFL) = 0x2 (flags O_RDWR) fcntl64(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0 connect(3, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress) select(4, NULL, [3], NULL, NULL) = 1 (out [3]) getsockopt(3, SOL_SOCKET, SO_ERROR, [111], [4]) = 0 fcntl64(3, F_SETFL, O_RDWR) = 0 close(3) = 0 close(-1) = -1 EBADF (Bad file descriptor) exit_group(1) = ? |
Cada linha é uma chamada de sistema com os parâmetros e o código de retorno. Foram 262 chamadas ao sistema no total, e por questões de espaço, estou exibindo apenas as 10 primeiras e as 10 últimas linhas. Mas estas linhas são suficientes para entender o que esta acontecendo ao executar o comando nc.
Na linha 16, a chamada à função connect() esta retornando erro (-1) ao se conectar na minha máquina (127.0.0.1) na porta 1234 (não existe nenhum servidor na minha máquina escutando a porta 1234).
O exemplo foi bem simples, mas nos dá uma noção do poder desta ferramenta. A grande vantagem é que não precisamos do código-fonte da aplicação, nem de símbolos no arquivo binário. Tudo isso funciona através de uma funcionalidade fornecida pelo kernel, chamada de ptrace, que possibilita que um processo possa controlar outro processo, manipulando seus descritores de arquivo, memória, registradores, etc. É isso que faz o strace.
É claro que existem muitas outras aplicações para o strace. Basta usar a imaginação.
Você já instalou alguma aplicação mas não sabia onde ela buscava o arquivo de configuração? O comando abaixo pode te responder:
$ strace app_name 2>&1 | grep "open" | grep "\/etc" |
Com este comando, buscamos todas as chamadas open() em arquivos dentro de "/etc".
Perceba também que o strace não serve apenas para debug. É também a ferramenta perfeita para você entender o funcionamento de uma aplicação e até fazer engenharia reversa quando o que você tem é apenas o binário.
Além disso, com o strace podemos fazer análise de performance através do parâmetro "-c".
$ strace -c du /home/sprado % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 70.16 0.273238 12 23144 getdents64 28.63 0.111506 1 104334 fstatat64 0.43 0.001680 0 11559 write 0.29 0.001130 0 23734 close 0.24 0.000927 0 34716 fcntl64 0.23 0.000881 0 12148 openat 0.02 0.000096 0 12166 fstat64 0.00 0.000000 0 3 read 0.00 0.000000 0 30 13 open 0.00 0.000000 0 1 execve 0.00 0.000000 0 3 3 access 0.00 0.000000 0 14 brk 0.00 0.000000 0 2 munmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 21 mmap2 0.00 0.000000 0 1 set_thread_area ------ ----------- ----------- --------- --------- ---------------- 100.00 0.389458 221880 16 total |
Temos algumas informações valiosas na saída deste comando. Os contadores de cada chamada do sistema (calls) e tempo de processamento (seconds) são extremamente úteis quando queremos saber onde esta o gargalo na execução da nossa aplicação.
Existem ainda muitas outras funcionalidades. Você pode monitorar apenas as chamadas de sistema relacionadas à rede usando "trace=network" como parâmetro, ou então a comunicação entre processos usando "trace=ipc". Uma descrição completa das funcionalidades do strace podem ser encontradas no manual da ferramenta.
$ man strace |
LTRACE
O ltrace tem as mesmas características do strace, mas ao invés de monitorar as chamadas do sistema, ele monitora as chamadas às funções das bibliotecas carregadas dinamicamente.
Veja como ficaria o nosso exemplo do "nc" com o ltrace:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ ltrace nc localhost 1234 __libc_start_main(0x804a700, 3, 0xbf868d94, 0x804caa0, 0x804ca90 <unfinished ...=""> getopt(3, 0xbf868d94, "46Ddhi:jklnP:p:q:rSs:tT:Uuvw:X:x"...) = -1 getservbyname("1234", "tcp") = NULL strchr("1234", '-') = NULL strtoul(0xbf86a695, 0xbf866b4c, 10, 0xbf868d94, 0x804d0f8) = 1234 calloc(1, 6) = 0x091c0520 getaddrinfo("localhost", "1234", 0xbf866b78, 0xbf866b4c) = 0 socket(2, 1, 6) = 3 fcntl(3, 3, 0, 0xbf866b4c, 0x8e49ae) = 2 fcntl(3, 4, 2050, 0xbf866b4c, 0x8e49ae) = 0 connect(3, 0x91c0cf0, 16, 0xbf866b4c, 0x8e49ae) = -1 __errno_location() = 0xb77eb688 select(4, 0, 0xbf866a98, 0, 0) = 1 getsockopt(3, 1, 4, 0xbf866b44, 0xbf866b40) = 0 fcntl(3, 4, 2, 0xbf866b44, 0xbf866b40) = 0 close(3) = 0 freeaddrinfo(0x091c0cd0) = <void> close(-1) = -1 exit(1 <unfinished ...=""> +++ exited (status 1) +++ </unfinished></void></unfinished> |
Veja que, da mesma forma, conseguimos identificar o problema na chamada a connect() na linha 12.
O detalhe aqui é que esta ferramenta monitora apenas a chamada às funções de biblioteca linkadas dinamicamente com a aplicação, e por isso você não conseguirá usá-la se a aplicação for linkada estaticamente com as bibliotecas do sistema.
NO UNIVERSO EMBEDDED
A utilização destas ferramentas em Linux embarcado é idêntica. A única diferença é que você irá precisar cross-compilar o strace e o ltrace para executar na sua arquitetura-alvo.
O conhecimento e o uso deste tipo de ferramenta é essencial para o desenvolvedor Linux. Seja para debugar um problema em determinada aplicação, monitorar sua performance ou aprender sobre seu funcionamento, investir um tempo para conhecê-la mais profundamente é extremamente válido. Mas não existe conhecimento sem prática. Portanto, mãos à obra!
Um abraço,
Sergio Prado