Gerando backtrace em C/C++ no Linux

· 4 minutos de leitura
Gerando backtrace em C/C++ no Linux

Olá pessoal, creio que muitos aqui já se depararam com algum crash em seus programas em C/C++, e gostariam de poder ver a pilha de chamadas, ou stacktrace/backtrace, ao menos no ambiente de testes. Pois bem, a boa notícia é que no Linux existe a possibilidade de mostrar qual o ultimo método chamado antes de receber um crash.

Chega de falar, vamos ao exemplo abaixo:

Para começar, vamos falar da função main. Logo no início nós "sinalizamos" ao programa que, no caso de um SIGINT ou de um SIGSEGV, ele deve chamar a função handler. Em termos simples, um SIGINT acontece quando um programa recebe um kill -2, ou aquele famoso Ctrl-C que você pressiona quando algum programa está executando no terminal e você deseja pará-lo de alguma forma. Este geralmente acontece por ação do usuário. Já o SIGSEGV acontece quando um ponteiro inválido é acessado. Para programadores Java, o SIGSEGV seria a leitura de um objeto não inicializado. Nestes casos, o Java por exemplo, mostra o backtrace e diz que ocorreu um "Null Pointer Reference", ou seja, uma referencia para um ponteiro nulo. Um caso simples de explicar um SIGSEGV no C pode ser exemplificado com o código abaixo, onde o programa tenta ler o conteúdo de um ponteiro que indica a posição -1, ou seja, inválido:

Voltando ao programa inicial, após as chamadas de signal, a função func1 é chamada. Esta aguarda por 20 segundos, então é chamada a função func2. Esse tempo foi introduzido para poder exemplificar o SIGINT, pois o usuário pode executar um kill -2 e ver o backtrace gerado pelo SIGINT. Caso o usuário espere os 20 segundos, a func2 será chamada, executando um SIGSEGV em si mesmo, para fim de exemplo. Ao ser executado o SIGSEGV, você verá o backtrace mostrando que a func1 e a func2 foram chamadas e que então o programa terminou na função handler.

A função handler por sua vez faz a chamada da função backtrace, que retorna um array de endereços de cada função chamada. O tamanho do array depende o segundo parâmetro. Se o backtrace for maior do que o parâmetro passado, então somente as N mais recentes funções serão retornadas. Como esta chamada retorna endereços, fica ruim distinguir qual foram as funções chamada somente olhando os endereços em hexadecimal, então utilizamos a função backtrace_symbols_fd, que pega os endereços em hexa e traduz em strings com os nomes da respetivas funções. Além disso, a chamada backtrace_symbols_fd também escreve, uma função por linha, no file descriptor especificado pelo terceiro parâmetro. Como exemplo, nós escrevemos direto na saída de erro do programa. No final da função handler é executado um exit(1), que termina o programa com erro.

Após toda essa explicação, vamos agora verificar a saída desse programa. Para compilar o código acima, basta chamar o make com Makefile abaixo:

Ao executar o programa e logo efetuar um kill -2 nele:


[marcos@xfiles backtrace]$ ./backtrace &
[1] 17292
[marcos@xfiles backtrace]$ kill -2 17292
Signal: 2
backtrace returned 8 entries
./backtrace(handler+0x25)[0x400a7b]
/lib64/libc.so.6(+0x34a50)[0x7f92edbdfa50]
/lib64/libc.so.6(nanosleep+0x10)[0x7f92edc73a40]
/lib64/libc.so.6(sleep+0xd4)[0x7f92edc738f4]
./backtrace(func1+0xe)[0x400b02]
./backtrace(main+0x2c)[0x400b3b]
/lib64/libc.so.6(__libc_start_main+0xf0)[0x7f92edbcb700]
./backtrace(_start+0x29)[0x400989]
[1]+  Fim da execução com status 1      ./backtrace

Na execução acima, podemos verificar que a função main e a func1 foram chamadas, e depois verificamos a função sleep ser chamada, e logo mais vemos a função handler como sendo a ultima do programa.

E ao esperar os 20 segundos:


[marcos@xfiles backtrace]$ ./backtrace &
[1] 17305
[marcos@xfiles backtrace]$ Signal: 11
backtrace returned 8 entries
./backtrace(handler+0x25)[0x400a7b]
/lib64/libc.so.6(+0x34a50)[0x7fd6c8137a50]
/lib64/libc.so.6(kill+0x7)[0x7fd6c8137d07]
./backtrace(func2+0x15)[0x400af1]
./backtrace(func1+0x18)[0x400b0c]
./backtrace(main+0x2c)[0x400b3b]
/lib64/libc.so.6(__libc_start_main+0xf0)[0x7fd6c8123700]
./backtrace(_start+0x29)[0x400989]
[1]+ Fim da execução com status 1 ./backtrace

Agora nesta execução, podemos verificar que além das funções main e func1, a função func2 foi chamada, e então vemos kill ser executada e logo mais vemos a função handler sendo a última do programa.

Observações: No Makefile podemos verificar a opção -rdynamic. Ela é necessária para que o GCC mostre os nomes das funções. Podemos ver abaixo um exemplo de saída do programa sem essa opção especificada:


[marcos@xfiles backtrace]$ ./backtrace &
[1] 18323
[marcos@xfiles backtrace]$ kill -2 18323
Signal: 2
backtrace returned 8 entries
./backtrace[0x4007ab]
/lib64/libc.so.6(+0x34a50)[0x7f5c2b398a50]
/lib64/libc.so.6(nanosleep+0x10)[0x7f5c2b42ca40]
/lib64/libc.so.6(sleep+0xd4)[0x7f5c2b42c8f4]
./backtrace[0x400832]
./backtrace[0x40086b]
/lib64/libc.so.6(__libc_start_main+0xf0)[0x7f5c2b384700]
./backtrace[0x4006b9]
[1]+  Fim da execução com status 1      ./backtrace

Como podemos ver, todas as referências ao binário backtrace agora não mostram mais o nome das funções, somente seu endereço em memória. Se uma função for static inline, está também não irá aparecer na pilha de chamadas.

Espero que tenham gostado! Não se esqueçam de se inscrever nas nossas redes sociais! Até a próxima!

Referências: man backtrace