Como se pode obter um rastreamento de pilha em C?

83

Eu sei que não existe uma função C padrão para fazer isso. Fiquei me perguntando quais são as técnicas para fazer isso no Windows e * nix? (Windows XP é meu sistema operacional mais importante para fazer isso agora.)

Kevin
fonte
Eu testei vários métodos detalhadamente em: stackoverflow.com/questions/3899870/print-call-stack-in-c-or-c/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respostas:

81

glibc fornece a função backtrace ().

http://www.gnu.org/software/libc/manual/html_node/Backtraces.html

Sanxiyn
fonte
6
glibc FTW ... de novo. (Esta é mais uma razão pela qual considero glibc o padrão ouro absoluto quando se trata de programação C (isso e o compilador que a acompanha).)
Trevor Boyd Smith
4
Mas espere, tem mais! A função backtrace () fornece apenas uma matriz de ponteiros void * que representam as funções da pilha de chamadas. "Isso não é muito útil. Arg." Não tema! glibc fornece uma função que converte todos os endereços void * (os endereços de função de pilha de chamadas) em símbolos de string legíveis por humanos. char ** backtrace_symbols (void *const *buffer, int size)
Trevor Boyd Smith,
1
Advertência: só funciona para funções C, eu acho. @Trevor: Ele está apenas procurando símbolos por endereço na tabela ELF.
Conrad Meyer,
2
Existe também o void backtrace_symbols_fd(void *const *buffer, int size, int fd)que pode enviar a saída diretamente para stdout / err, por exemplo.
semana
2
backtrace_symbols()é uma merda. Requer a exportação de todos os símbolos e não oferece suporte a símbolos DWARF (depuração). O libbacktrace é uma opção muito melhor na maioria dos casos.
Erwan Legrand
29

Existem backtrace () e backtrace_symbols ():

Na página de manual:

#include <execinfo.h>
#include <stdio.h>
...
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i) {
    printf("%s\n", strs[i]);
}
free(strs);
...

Uma maneira de usar isso de uma maneira / OOP mais conveniente é salvar o resultado de backtrace_symbols () em um construtor de classe de exceção. Portanto, sempre que você lançar esse tipo de exceção, terá o rastreamento de pilha. Em seguida, basta fornecer uma função para imprimi-lo. Por exemplo:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

try {
   throw MyException("Oops!");
} catch ( MyException e ) {
    e.printStackTrace();
}

Ta da!

Observação: habilitar sinalizadores de otimização pode tornar o rastreamento de pilha impreciso. O ideal seria usar esse recurso com sinalizadores de depuração ativados e sinalizadores de otimização desativados.

Tom
fonte
@shuckc apenas para converter o endereço em uma string de símbolo, o que pode ser feito externamente usando outras ferramentas, se necessário.
Woodrow Barlow
22

Para Windows, verifique a API StackWalk64 () (também no Windows de 32 bits). Para UNIX, você deve usar a maneira nativa do sistema operacional de fazer isso, ou recorrer ao backtrace () da glibc, se disponível.

Observe, entretanto, que realizar um Stacktrace em código nativo raramente é uma boa ideia - não porque não seja possível, mas porque você normalmente está tentando obter o resultado errado.

Na maioria das vezes as pessoas tentam obter um rastreamento de pilha em, digamos, uma circunstância excepcional, como quando uma exceção é detectada, uma afirmação falha ou - o pior e mais errado de todos - quando você obtém uma "exceção" fatal ou sinal como um violação de segmentação.

Considerando o último problema, a maioria das APIs exigirá que você aloque explicitamente a memória ou pode fazê-lo internamente. Fazer isso no estado frágil em que seu programa pode estar atualmente pode piorar ainda mais as coisas. Por exemplo, o relatório de travamento (ou coredump) não refletirá a causa real do problema, mas sua tentativa fracassada de lidar com ele).

Suponho que você esteja tentando obter aquela coisa de tratamento de erros fatais, como muitas pessoas parecem tentar quando se trata de obter um rastreamento de pilha. Nesse caso, eu confiaria no depurador (durante o desenvolvimento) e deixaria o processo coredump na produção (ou mini-dump no windows). Junto com o gerenciamento de símbolos adequado, você não deve ter problemas para descobrir a instrução causadora post-mortem.

Christian.K
fonte
2
Você está certo sobre ser frágil tentar alocar memória em um manipulador de sinal ou exceção. Uma possível saída é alocar uma quantidade fixa de espaço de "emergência" no início do programa ou usar um buffer estático.
j_random_hacker
Outra saída é a criação de um serviço coredump, que funciona de forma independente
Kobor42
5

Você deve usar a biblioteca de desdobramento .

unw_cursor_t cursor; unw_context_t uc;
unw_word_t ip, sp;
unw_getcontext(&uc);
unw_init_local(&cursor, &uc);
unsigned long a[100];
int ctr = 0;

while (unw_step(&cursor) > 0) {
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_reg(&cursor, UNW_REG_SP, &sp);
  if (ctr >= 10) break;
  a[ctr++] = ip;
}

Sua abordagem também funcionaria bem, a menos que você faça uma chamada de uma biblioteca compartilhada.

Você pode usar o addr2linecomando no Linux para obter a função de origem / número da linha do PC correspondente.

user262802
fonte
"função fonte / número da linha"? E se a vinculação for otimizada para tamanho de código reduzido? Direi, porém, que este parece um projeto útil. Pena que não tem como conseguir os registros. Definitivamente vou investigar isso. Você sabia que é absolutamente independente do processador? Apenas funciona em qualquer coisa que tenha um compilador C?
Mawg diz para restabelecer Monica em
1
Ok, este comentário valeu a pena, apenas por causa da menção do útil comando addr2line!
Ogre Salmo33
addr2line falha para código relocável em sistemas com ASLR (ou seja, a maior parte do que as pessoas têm usado durante a última década).
Erwan Legrand
4

Não existe uma maneira independente de plataforma para fazer isso.

A coisa mais próxima que você pode fazer é executar o código sem otimizações. Dessa forma, você pode anexar ao processo (usando o depurador visual c ++ ou GDB) e obter um rastreamento de pilha utilizável.

Nils Pipenbrinck
fonte
Isso não me ajuda quando ocorre um travamento em um computador embarcado no campo. :(
Kevin,
@Kevin: Mesmo em máquinas embarcadas, geralmente há uma maneira de obter um stub do depurador remoto ou pelo menos um dump de núcleo. Talvez não depois de implantado em campo, no entanto ...
efêmero
se você executar usando gcc-glibc em sua plataforma de escolha windows / linux / mac ... então backtrace () e backtrace_symbols () funcionarão em todas as três plataformas. Dada essa afirmação, eu usaria as palavras "não há maneira [portátil] de fazer isso".
Trevor Boyd Smith,
4

Para Windows, CaptureStackBackTrace()também é uma opção, que requer menos código de preparação do que o usuário StackWalk64(). (Além disso, para um cenário semelhante que tive, CaptureStackBackTrace()acabou funcionando melhor (mais confiável) do que StackWalk64().)

Quasidart
fonte
2

Solaris tem o comando pstack , que também foi copiado para o Linux.

wnoise
fonte
1
Útil, mas não exatamente C (é um utilitário externo).
efemiente
1
também, a partir da descrição (seção: restrições) ": o pstack atualmente funciona apenas no Linux, apenas em uma máquina x86 executando binários ELF de 32 bits (64 bits não suportados)"
Ciro Costa
0

Você pode fazer isso movendo a pilha para trás. Na realidade, porém, é frequentemente mais fácil adicionar um identificador a uma pilha de chamadas no início de cada função e colocá-lo no final, depois apenas percorrer a impressão do conteúdo. É um pouco como um PITA, mas funciona bem e vai poupar tempo no final.

Serafina Brocious
fonte
1
Você poderia explicar o "andar a pilha para trás" mais detalhadamente?
Spidey
@Spidey Em sistemas embarcados, às vezes isso é tudo que você tem - acho que foi rejeitado porque a plataforma é WinXP. Mas sem uma libc que suporte stack walking, basicamente você tem que "caminhar" pela stack. Você começa com o ponteiro de base atual (em x86, este é o conteúdo do reg RBP). Isso leva você a apontar na pilha com 1. o RBP anterior salvo (é assim que mantém a movimentação da pilha) e 2. o endereço de retorno de chamada / ramificação (chamando o reg RIP salvo da função), que informa qual era a função. Então, se você tiver acesso à tabela de símbolos, poderá pesquisar o endereço da função.
Ted Middleton
0

Nos últimos anos, tenho usado o libbacktrace de Ian Lance Taylor. É muito mais limpo do que as funções da biblioteca GNU C, que exigem a exportação de todos os símbolos. Ele fornece mais utilidade para a geração de backtraces do que o libunwind. E por último, mas não menos importante, não é derrotado pelo ASLR, pois são abordagens que requerem ferramentas externas, comoaddr2line .

Libbacktrace era inicialmente parte da distribuição GCC, mas agora está disponível pelo autor como uma biblioteca independente sob uma licença BSD:

https://github.com/ianlancetaylor/libbacktrace

No momento em que este artigo foi escrito, eu não usaria mais nada, a menos que precise gerar backtraces em uma plataforma que não seja compatível com libbacktrace.

Erwan Legrand
fonte