Como gerar automaticamente um rastreamento de pilha quando meu programa falha

590

Estou trabalhando no Linux com o compilador GCC. Quando meu programa C ++ falha, eu gostaria que ele gerasse automaticamente um rastreamento de pilha.

Meu programa está sendo executado por muitos usuários diferentes e também é executado no Linux, Windows e Macintosh (todas as versões são compiladas usando gcc).

Gostaria que meu programa pudesse gerar um rastreamento de pilha quando ele falhar e, na próxima vez em que o usuário o executar, ele perguntará se está OK para enviar o rastreamento de pilha para mim, para que eu possa rastrear o problema. Eu posso lidar com o envio das informações para mim, mas não sei como gerar a sequência de rastreamento. Alguma ideia?

KPexEA
fonte
4
backtrace e backtrace_symbols_fd não são seguros para sinal assíncrono. você não deve usar estas funções no manipulador de sinal
Parag Bafna
10
backtrace_symbols chama malloc e, portanto, não deve ser usado em um manipulador de sinais. As outras duas funções (backtrace e backtrace_symbols_fd) não têm esse problema e são comumente usadas em manipuladores de sinal.
Cmcabe #
3
@cmccabe que é backtrace_symbols_fd incorreta normalmente não chama malloc mas pode se algo der errado em seu bloco catch_error
Sam Saffron
6
"Pode" no sentido de que não há especificação POSIX para backtrace_symbols_fd (ou qualquer backtrace); no entanto, o backtrace_symbols_fd do GNU / Linux é especificado para nunca chamar malloc, conforme linux.die.net/man/3/backtrace_symbols_fd . Portanto, é seguro assumir que nunca chamará malloc no Linux.
Codetaku 17/07/2014
Como ele falha?
Ciro Santilli # 19/17

Respostas:

509

Para Linux e acredito que o Mac OS X, se você estiver usando gcc ou qualquer compilador que use glibc, poderá usar as funções backtrace () execinfo.hpara imprimir um stacktrace e sair normalmente quando houver uma falha de segmentação. A documentação pode ser encontrada no manual libc .

Aqui está um exemplo de programa que instala um SIGSEGVmanipulador e imprime um rastreamento de pilha para stderrquando ele é segmentado por falha. A baz()função aqui causa o segfault que aciona o manipulador:

#include <stdio.h>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>


void handler(int sig) {
  void *array[10];
  size_t size;

  // get void*'s for all entries on the stack
  size = backtrace(array, 10);

  // print out all the frames to stderr
  fprintf(stderr, "Error: signal %d:\n", sig);
  backtrace_symbols_fd(array, size, STDERR_FILENO);
  exit(1);
}

void baz() {
 int *foo = (int*)-1; // make a bad pointer
  printf("%d\n", *foo);       // causes segfault
}

void bar() { baz(); }
void foo() { bar(); }


int main(int argc, char **argv) {
  signal(SIGSEGV, handler);   // install our handler
  foo(); // this will call foo, bar, and baz.  baz segfaults.
}

Compilar com -g -rdynamicvocê obtém informações de símbolo em sua saída, que a glibc pode usar para criar um bom rastreamento de pilha:

$ gcc -g -rdynamic ./test.c -o test

Ao executar isso, você obtém esta saída:

$ ./test
Error: signal 11:
./test(handler+0x19)[0x400911]
/lib64/tls/libc.so.6[0x3a9b92e380]
./test(baz+0x14)[0x400962]
./test(bar+0xe)[0x400983]
./test(foo+0xe)[0x400993]
./test(main+0x28)[0x4009bd]
/lib64/tls/libc.so.6(__libc_start_main+0xdb)[0x3a9b91c4bb]
./test[0x40086a]

Isso mostra o módulo de carregamento, o deslocamento e a função de onde cada quadro da pilha veio. Aqui você pode ver o manipulador de sinal no topo da pilha, e as funções libc antes main, além de main, foo, bar, e baz.

Todd Gamblin
fonte
53
Há também /lib/libSegFault.so que você pode usar com LD_PRELOAD.
CesarB 23/10/08
6
Parece que as duas primeiras entradas na saída de backtrace contêm um endereço de retorno dentro do manipulador de sinais e provavelmente um dentro sigaction()da libc. Embora seu backtrace pareça estar correto, às vezes descobri que são necessárias etapas adicionais para garantir que a localização real da falha apareça no backtrace, pois ela pode ser substituída sigaction()pelo kernel.
jschmier
9
O que aconteceria se o acidente viesse de dentro do malloc? Você não seguraria uma trava e ficaria preso enquanto o "backtrace" tenta alocar memória?
Mattias Nilsson
7
catchsegvnão é o que o OP precisa, mas é incrível para detectar falhas de segmentação e obter todas as informações.
precisa saber é o seguinte
8
Para o ARM, eu também tive que compilar com -funwind-tables. Caso contrário, a profundidade da minha pilha era sempre 1 (vazia).
precisa saber é o seguinte
128

É ainda mais fácil do que "man backtrace", há uma biblioteca pouco documentada (específica para GNU) distribuída com glibc como libSegFault.so, que eu acredito que foi escrita por Ulrich Drepper para dar suporte ao programa catchsegv (consulte "man catchsegv").

Isso nos dá três possibilidades. Em vez de executar o "programa -o hai":

  1. Execute no catchsegv:

    $ catchsegv program -o hai
  2. Vincule-se ao libSegFault no tempo de execução:

    $ LD_PRELOAD=/lib/libSegFault.so program -o hai
  3. Link com libSegFault em tempo de compilação:

    $ gcc -g1 -lSegFault -o program program.cc
    $ program -o hai

Nos três casos, você obterá retornos mais claros com menos otimização (gcc -O0 ou -O1) e símbolos de depuração (gcc -g). Caso contrário, você pode acabar com uma pilha de endereços de memória.

Você também pode capturar mais sinais para rastreamentos de pilha com algo como:

$ export SEGFAULT_SIGNALS="all"       # "all" signals
$ export SEGFAULT_SIGNALS="bus abrt"  # SIGBUS and SIGABRT

A saída será mais ou menos assim (observe o backtrace na parte inferior):

*** Segmentation fault Register dump:

 EAX: 0000000c   EBX: 00000080   ECX:
00000000   EDX: 0000000c  ESI:
bfdbf080   EDI: 080497e0   EBP:
bfdbee38   ESP: bfdbee20

 EIP: 0805640f   EFLAGS: 00010282

 CS: 0073   DS: 007b   ES: 007b   FS:
0000   GS: 0033   SS: 007b

 Trap: 0000000e   Error: 00000004  
OldMask: 00000000  ESP/signal:
bfdbee20   CR2: 00000024

 FPUCW: ffff037f   FPUSW: ffff0000  
TAG: ffffffff  IPOFF: 00000000  
CSSEL: 0000   DATAOFF: 00000000  
DATASEL: 0000

 ST(0) 0000 0000000000000000   ST(1)
0000 0000000000000000  ST(2) 0000
0000000000000000   ST(3) 0000
0000000000000000  ST(4) 0000
0000000000000000   ST(5) 0000
0000000000000000  ST(6) 0000
0000000000000000   ST(7) 0000
0000000000000000

Backtrace:
/lib/libSegFault.so[0xb7f9e100]
??:0(??)[0xb7fa3400]
/usr/include/c++/4.3/bits/stl_queue.h:226(_ZNSt5queueISsSt5dequeISsSaISsEEE4pushERKSs)[0x805647a]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/player.cpp:73(_ZN6Player5inputESs)[0x805377c]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:159(_ZN6Socket4ReadEv)[0x8050698]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:413(_ZN12ServerSocket4ReadEv)[0x80507ad]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:300(_ZN12ServerSocket4pollEv)[0x8050b44]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/main.cpp:34(main)[0x8049a72]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7d1b775]
/build/buildd/glibc-2.9/csu/../sysdeps/i386/elf/start.S:122(_start)[0x8049801]

Se você deseja conhecer os detalhes sangrentos, infelizmente, a melhor fonte é a fonte: Consulte http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c e seu diretório pai http://sourceware.org/git/?p=glibc.git;a=tree;f=debug

jhclark
fonte
1
"Possibilidade 3. Link com libSegFault em tempo de compilação" não funciona.
HHK 23/01
5
@rafter: O que você quer dizer com "não funciona". O que você tentou, em qual idioma / compilador / cadeia de ferramentas / distribuição / hardware? Falha ao compilar? Para pegar um erro? Produzir saída? Para produzir uma saída difícil de usar? Obrigado por detalhes, isso ajudará a todos.
Stéphane Gourichon 31/03
1
'melhor fonte é infelizmente a fonte' ... Esperamos que, algum dia, a página de manual do catchsegv realmente mencione SEGFAULT_SIGNALS. Até então, há esta resposta para se referir.
Greggo
Eu não posso acreditar que estou programando C há 5 anos e nunca ouvi falar disso: /
DavidMFrey 16/16/16
6
@ StéphaneGourichon @HansKratz Para vincular ao libSegFault, você precisará adicionar -Wl,--no-as-neededaos sinalizadores do compilador. Caso contrário, de ldfato não será vinculado libSegFault, porque reconhece que o binário não usa nenhum de seus símbolos.
Phillip
122

Linux

Embora o uso de backtrace () funcione em execinfo.h para imprimir um stacktrace e sair normalmente quando você receber uma falha de segmentação, já foi sugerido , não vejo menção aos meandros necessários para garantir que o backtrace resultante aponte para o local real de a falha (pelo menos para algumas arquiteturas - x86 e ARM).

As duas primeiras entradas na cadeia de quadros da pilha quando você entra no manipulador de sinal contêm um endereço de retorno dentro do manipulador de sinal e um dentro de sigaction () na libc. O quadro de pilha da última função chamada antes que o sinal (que é o local da falha) seja perdido.

Código

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_GNU
#define __USE_GNU
#endif

#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ucontext.h>
#include <unistd.h>

/* This structure mirrors the one found in /usr/include/asm/ucontext.h */
typedef struct _sig_ucontext {
 unsigned long     uc_flags;
 struct ucontext   *uc_link;
 stack_t           uc_stack;
 struct sigcontext uc_mcontext;
 sigset_t          uc_sigmask;
} sig_ucontext_t;

void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
 void *             array[50];
 void *             caller_address;
 char **            messages;
 int                size, i;
 sig_ucontext_t *   uc;

 uc = (sig_ucontext_t *)ucontext;

 /* Get the address at the time the signal was raised */
#if defined(__i386__) // gcc specific
 caller_address = (void *) uc->uc_mcontext.eip; // EIP: x86 specific
#elif defined(__x86_64__) // gcc specific
 caller_address = (void *) uc->uc_mcontext.rip; // RIP: x86_64 specific
#else
#error Unsupported architecture. // TODO: Add support for other arch.
#endif

 fprintf(stderr, "signal %d (%s), address is %p from %p\n", 
  sig_num, strsignal(sig_num), info->si_addr, 
  (void *)caller_address);

 size = backtrace(array, 50);

 /* overwrite sigaction with caller's address */
 array[1] = caller_address;

 messages = backtrace_symbols(array, size);

 /* skip first stack frame (points here) */
 for (i = 1; i < size && messages != NULL; ++i)
 {
  fprintf(stderr, "[bt]: (%d) %s\n", i, messages[i]);
 }

 free(messages);

 exit(EXIT_FAILURE);
}

int crash()
{
 char * p = NULL;
 *p = 0;
 return 0;
}

int foo4()
{
 crash();
 return 0;
}

int foo3()
{
 foo4();
 return 0;
}

int foo2()
{
 foo3();
 return 0;
}

int foo1()
{
 foo2();
 return 0;
}

int main(int argc, char ** argv)
{
 struct sigaction sigact;

 sigact.sa_sigaction = crit_err_hdlr;
 sigact.sa_flags = SA_RESTART | SA_SIGINFO;

 if (sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL) != 0)
 {
  fprintf(stderr, "error setting signal handler for %d (%s)\n",
    SIGSEGV, strsignal(SIGSEGV));

  exit(EXIT_FAILURE);
 }

 foo1();

 exit(EXIT_SUCCESS);
}

Resultado

signal 11 (Segmentation fault), address is (nil) from 0x8c50
[bt]: (1) ./test(crash+0x24) [0x8c50]
[bt]: (2) ./test(foo4+0x10) [0x8c70]
[bt]: (3) ./test(foo3+0x10) [0x8c8c]
[bt]: (4) ./test(foo2+0x10) [0x8ca8]
[bt]: (5) ./test(foo1+0x10) [0x8cc4]
[bt]: (6) ./test(main+0x74) [0x8d44]
[bt]: (7) /lib/libc.so.6(__libc_start_main+0xa8) [0x40032e44]

Todos os riscos de chamar as funções backtrace () em um manipulador de sinal ainda existem e não devem ser ignorados, mas acho a funcionalidade que descrevi aqui bastante útil na depuração de falhas.

É importante observar que o exemplo que forneci foi desenvolvido / testado no Linux para x86. Eu também implementei isso com sucesso no ARM usandouc_mcontext.arm_pc vez deuc_mcontext.eip .

Aqui está um link para o artigo em que aprendi os detalhes desta implementação: http://www.linuxjournal.com/article/6391

jschmier
fonte
11
Em sistemas que usam o GNU ld, lembre-se de compilar com -rdynamicpara instruir o vinculador a adicionar todos os símbolos, não apenas os usados, à tabela de símbolos dinâmicos. Isso permite backtrace_symbols()converter endereços em nomes de funções
jschmier
1
Além disso, você precisa adicionar a opção "-mapcs-frame" a linha de comando do GCC 'para gerar quadros de pilha no braço plataforma
qehgt
3
Isso pode ser tarde demais, mas podemos usar o addr2linecomando de alguma forma para obter a linha exata onde ocorreu a falha?
enthusiasticgeek
4
Em compilações mais recentes de glibc uc_mcontextnão contém um campo chamado eip. Agora existe uma matriz que precisa ser indexada, uc_mcontext.gregs[REG_EIP]é o equivalente.
mmlb
6
Para o ARM, meus backtraces sempre tiveram profundidade 1 até adicionar a opção -funwind-tables ao compilador.
Jritz42
84

Embora tenha sido fornecida uma resposta correta que descreva como usar a backtrace()função GNU libc 1 e forneça minha própria resposta que descreve como garantir que um retorno de um manipulador de sinal aponte para o local real da falha 2 , não vejo qualquer menção à retirada dos símbolos C ++ do backtrace.

Ao obter backtraces de um programa C ++, a saída pode ser executada através de c++filt1 para desmembrar os símbolos ou usando 1 diretamente.abi::__cxa_demangle

  • 1 Linux e OS X Observe que c++filte __cxa_demanglesão específicos ao GCC
  • 2 Linux

O exemplo a seguir do C ++ Linux usa o mesmo manipulador de sinal da minha outra resposta e demonstra como c++filtpode ser usado para desmembrar os símbolos.

Código :

class foo
{
public:
    foo() { foo1(); }

private:
    void foo1() { foo2(); }
    void foo2() { foo3(); }
    void foo3() { foo4(); }
    void foo4() { crash(); }
    void crash() { char * p = NULL; *p = 0; }
};

int main(int argc, char ** argv)
{
    // Setup signal handler for SIGSEGV
    ...

    foo * f = new foo();
    return 0;
}

Saída ( ./test):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(crash__3foo+0x13) [0x8048e07]
[bt]: (2) ./test(foo4__3foo+0x12) [0x8048dee]
[bt]: (3) ./test(foo3__3foo+0x12) [0x8048dd6]
[bt]: (4) ./test(foo2__3foo+0x12) [0x8048dbe]
[bt]: (5) ./test(foo1__3foo+0x12) [0x8048da6]
[bt]: (6) ./test(__3foo+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

Saída desmontada ( ./test 2>&1 | c++filt):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(foo::crash(void)+0x13) [0x8048e07]
[bt]: (2) ./test(foo::foo4(void)+0x12) [0x8048dee]
[bt]: (3) ./test(foo::foo3(void)+0x12) [0x8048dd6]
[bt]: (4) ./test(foo::foo2(void)+0x12) [0x8048dbe]
[bt]: (5) ./test(foo::foo1(void)+0x12) [0x8048da6]
[bt]: (6) ./test(foo::foo(void)+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

O seguinte baseia-se no manipulador de sinal da minha resposta original e pode substituir o manipulador de sinal no exemplo acima para demonstrar como abi::__cxa_demanglepode ser usado para desmontar os símbolos. Este manipulador de sinal produz a mesma saída desmontada que o exemplo acima.

Código :

void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
    sig_ucontext_t * uc = (sig_ucontext_t *)ucontext;

    void * caller_address = (void *) uc->uc_mcontext.eip; // x86 specific

    std::cerr << "signal " << sig_num 
              << " (" << strsignal(sig_num) << "), address is " 
              << info->si_addr << " from " << caller_address 
              << std::endl << std::endl;

    void * array[50];
    int size = backtrace(array, 50);

    array[1] = caller_address;

    char ** messages = backtrace_symbols(array, size);    

    // skip first stack frame (points here)
    for (int i = 1; i < size && messages != NULL; ++i)
    {
        char *mangled_name = 0, *offset_begin = 0, *offset_end = 0;

        // find parantheses and +address offset surrounding mangled name
        for (char *p = messages[i]; *p; ++p)
        {
            if (*p == '(') 
            {
                mangled_name = p; 
            }
            else if (*p == '+') 
            {
                offset_begin = p;
            }
            else if (*p == ')')
            {
                offset_end = p;
                break;
            }
        }

        // if the line could be processed, attempt to demangle the symbol
        if (mangled_name && offset_begin && offset_end && 
            mangled_name < offset_begin)
        {
            *mangled_name++ = '\0';
            *offset_begin++ = '\0';
            *offset_end++ = '\0';

            int status;
            char * real_name = abi::__cxa_demangle(mangled_name, 0, 0, &status);

            // if demangling is successful, output the demangled function name
            if (status == 0)
            {    
                std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " 
                          << real_name << "+" << offset_begin << offset_end 
                          << std::endl;

            }
            // otherwise, output the mangled function name
            else
            {
                std::cerr << "[bt]: (" << i << ") " << messages[i] << " : " 
                          << mangled_name << "+" << offset_begin << offset_end 
                          << std::endl;
            }
            free(real_name);
        }
        // otherwise, print the whole line
        else
        {
            std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl;
        }
    }
    std::cerr << std::endl;

    free(messages);

    exit(EXIT_FAILURE);
}
jschmier
fonte
1
Obrigado por isso, jschmier. Eu criei um pequeno script bash para alimentar a saída disso no utilitário addr2line. Veja: stackoverflow.com/a/15801966/1797414
arr_sea
4
Não se esqueça de #include <cxxabi.h>
Bamaco
1
Uma boa documentação e um arquivo de cabeçalho simples foi postado aqui desde 2008 ... panthema.net/2008/0901-stacktrace-demangled muito semelhante à sua abordagem :)
kevinf
abi :: __ cxa_demangle parece não ser o sinal-assíncrono seguro, portanto, o manipulador de sinal pode causar um impasse em algum lugar do malloc.
orcy
O uso de std::cerr, free()e exit()todos violam as restrições contra chamando chamadas não assíncrona de sinal-seguro em sistemas POSIX. Este código irá impasse se o seu processo falhar em qualquer chamada, como free(), malloc() new, ou detete.
Andrew Henle
31

Pode valer a pena olhar para o Google Breakpad , um gerador de despejo de plataforma e ferramentas para processar os despejos.

Simon Steele
fonte
Ele relata coisas como falhas de segmentação, mas não informa informações sobre exceções não tratadas em C ++.
precisa saber é o seguinte
21

Você não especificou seu sistema operacional, portanto, isso é difícil de responder. Se você estiver usando um sistema baseado no gnu libc, poderá usar a função libc backtrace().

O GCC também possui dois recursos internos que podem ajudá-lo, mas que podem ou não ser totalmente implementados em sua arquitetura, e esses são __builtin_frame_addresse __builtin_return_address. Ambos querem um nível inteiro imediato (por imediato, quero dizer, não pode ser uma variável). Se __builtin_frame_addressum determinado nível for diferente de zero, deve ser seguro pegar o endereço de retorno do mesmo nível.

Brian Mitchell
fonte
13

Obrigado a entusiasticgeek por chamar minha atenção para o utilitário addr2line.

Eu escrevi um script rápido e sujo para processar a saída da resposta fornecida aqui : (muito obrigado a jschmier!) Usando o utilitário addr2line.

O script aceita um único argumento: O nome do arquivo que contém a saída do utilitário jschmier.

A saída deve imprimir algo como o seguinte para cada nível do rastreamento:

BACKTRACE:  testExe 0x8A5db6b
FILE:       pathToFile/testExe.C:110
FUNCTION:   testFunction(int) 
   107  
   108           
   109           int* i = 0x0;
  *110           *i = 5;
   111      
   112        }
   113        return i;

Código:

#!/bin/bash

LOGFILE=$1

NUM_SRC_CONTEXT_LINES=3

old_IFS=$IFS  # save the field separator           
IFS=$'\n'     # new field separator, the end of line           

for bt in `cat $LOGFILE | grep '\[bt\]'`; do
   IFS=$old_IFS     # restore default field separator 
   printf '\n'
   EXEC=`echo $bt | cut -d' ' -f3 | cut -d'(' -f1`  
   ADDR=`echo $bt | cut -d'[' -f3 | cut -d']' -f1`
   echo "BACKTRACE:  $EXEC $ADDR"
   A2L=`addr2line -a $ADDR -e $EXEC -pfC`
   #echo "A2L:        $A2L"

   FUNCTION=`echo $A2L | sed 's/\<at\>.*//' | cut -d' ' -f2-99`
   FILE_AND_LINE=`echo $A2L | sed 's/.* at //'`
   echo "FILE:       $FILE_AND_LINE"
   echo "FUNCTION:   $FUNCTION"

   # print offending source code
   SRCFILE=`echo $FILE_AND_LINE | cut -d':' -f1`
   LINENUM=`echo $FILE_AND_LINE | cut -d':' -f2`
   if ([ -f $SRCFILE ]); then
      cat -n $SRCFILE | grep -C $NUM_SRC_CONTEXT_LINES "^ *$LINENUM\>" | sed "s/ $LINENUM/*$LINENUM/"
   else
      echo "File not found: $SRCFILE"
   fi
   IFS=$'\n'     # new field separator, the end of line           
done

IFS=$old_IFS     # restore default field separator 
arr_sea
fonte
12

ulimit -c <value>define o limite de tamanho do arquivo principal no unix. Por padrão, o limite de tamanho do arquivo principal é 0. Você pode ver seus ulimitvalores com ulimit -a.

Além disso, se você executar o programa a partir do gdb, ele interromperá o programa por "violações de segmentação" ( SIGSEGVgeralmente quando você acessa um pedaço de memória que não havia alocado) ou pode definir pontos de interrupção.

ddd e nemiver são front-ends para o gdb, o que facilita muito o trabalho com o novato.

do utilizador
fonte
6
Os core dumps são infinitamente mais úteis que os rastreamentos de pilha, porque você pode carregar o core dump no depurador e ver o estado de todo o programa e seus dados no momento do travamento.
234 Adam Hawes
1
O recurso de backtrace sugerido por outros é provavelmente melhor do que nada, mas é muito básico - nem fornece números de linha. Por outro lado, usando dumps principais, vamos exibir retroativamente todo o estado do seu aplicativo no momento em que ele falhou (incluindo um rastreamento detalhado da pilha). Não pode haver problemas práticos com a tentativa de usar este para depuração campo, mas é definitivamente uma ferramenta mais poderosa para a análise de acidentes e afirma durante o desenvolvimento (pelo menos no Linux).
Nobar
10

É importante observar que, depois de gerar um arquivo principal, você precisará usar a ferramenta gdb para analisá-lo. Para que o gdb compreenda seu arquivo principal, você deve dizer ao gcc para instrumentar o binário com símbolos de depuração: para fazer isso, você compila com o sinalizador -g:

$ g++ -g prog.cpp -o prog

Então, você pode definir "ulimit -c unlimited" para deixá-lo despejar um núcleo ou apenas executar seu programa dentro do gdb. Eu gosto mais da segunda abordagem:

$ gdb ./prog
... gdb startup output ...
(gdb) run
... program runs and crashes ...
(gdb) where
... gdb outputs your stack trace ...

Eu espero que isso ajude.

Benson
fonte
4
Você também pode ligar gdbdiretamente do seu programa de falha. Manipulador de instalação para SIGSEGV, SEGILL, SIGBUS, SIGFPE que chamará gdb. Detalhes: stackoverflow.com/questions/3151779/… A vantagem é que você obtém um backtrace bonito e anotado, como bt fulltambém pode obter rastreamentos de pilha de todos os threads.
Vi.
Você também pode obter backtrace mais fácil do que na resposta: gdb -silent ./prog núcleo --eval-command = backtrace --batch -é iria mostrar backtrace e perto depurador
baziorek
10

Eu estive olhando para este problema por um tempo.

E enterrado no README das Ferramentas de desempenho do Google

http://code.google.com/p/google-perftools/source/browse/trunk/README

fala sobre libunwind

http://www.nongnu.org/libunwind/

Gostaria de ouvir opiniões desta biblioteca.

O problema com -rdynamic é que ele pode aumentar o tamanho do binário de forma relativamente significativa em alguns casos

Gregory
fonte
2
No x86 / 64, eu não vi muito aumento dinâmico do tamanho binário. A adição de -g gera um aumento muito maior.
Dan
1
Notei que a libunwind não tem funcionalidade para obter o número da linha e acho que (não testou) unw_get_proc_name retorna o símbolo da função (que é ofuscado por sobrecarga e outros) em vez do nome original.
Herbert
1
Está correto. Fica muito complicado fazer isso corretamente, mas tive um excelente sucesso com o gaddr2line, aqui há muitas informações práticas blog.bigpixel.ro/2010/09/stack-unwinding-stack-trace-with-gcc
Gregory
9

Você pode usar DeathHandler - classe C ++ pequena, que faz tudo por você, confiável.

markhor
fonte
1
infelizmente, ele usa execlp()para executar chamadas addr2line ... seria bom permanecer totalmente no próprio programa (o que é possível incluindo o código addr2line de alguma forma)
exemplo
9

Esqueça de mudar suas fontes e faça alguns hacks com a função backtrace () ou macroses - essas são apenas soluções ruins.

Como uma solução funcionando corretamente, aconselho:

  1. Compile seu programa com o sinalizador "-g" para incorporar símbolos de depuração em binários (não se preocupe, isso não afetará seu desempenho).
  2. No linux, execute o seguinte comando: "ulimit -c unlimited" - para permitir que o sistema faça grandes despejos de memória.
  3. Quando seu programa falhou, no diretório de trabalho, você verá o arquivo "core".
  4. Execute o próximo comando para imprimir o backtrace no stdout: gdb -batch -ex "backtrace" ./seu_programa_exe ./core

Isso imprimirá o rastreamento legível adequado do seu programa de maneira legível por humanos (com nomes de arquivos de origem e números de linha). Além disso, essa abordagem dará a você a liberdade de automatizar seu sistema: tenha um script curto que verifique se o processo criou um dump principal e envie backtraces por email aos desenvolvedores, ou faça o logon em algum sistema de log.

loopzilla
fonte
Dá os números de linha incorretos. Pode ser melhorado?
heyjude
7
ulimit -c unlimited

é uma variável do sistema, que permitirá criar um dump principal após a falha do aplicativo. Neste caso, uma quantidade ilimitada. Procure um arquivo chamado core no mesmo diretório. Certifique-se de compilar seu código com as informações de depuração ativadas!

Saudações

mana
fonte
5
O usuário não está solicitando um dump principal. Ele está pedindo um rastreamento de pilha. Veja delorie.com/gnu/docs/glibc/libc_665.html
Todd Gamblin
1
um core dump conterá a pilha de chamadas no momento do travamento, não é?
Mo.
3
Você está assumindo que ele está no Unix e está usando o Bash.
Paul Tomblin
2
Se você estiver usando tcsh, você tem que fazerlimit coredumpsize unlimited
sivabudh
6

Olhe para a:

homem 3 backtrace

E:

#include <exeinfo.h>
int backtrace(void **buffer, int size);

Estas são extensões GNU.

Stéphane
fonte
2
Pode haver exemplos adicionais para ajudar nesta página que eu criei há algum tempo: charette.no-ip.com:81/programming/2010-01-25_Backtrace
Stéphane
6

Consulte o recurso Rastreamento de pilha no ACE (ADAPTIVE Communication Environment). Já foi escrito para cobrir todas as principais plataformas (e mais). A biblioteca é licenciada no estilo BSD, para que você possa copiar / colar o código se não quiser usar o ACE.

Adam Mitz
fonte
O link parece estar morto.
#
5

Eu posso ajudar com a versão Linux: as funções backtrace, backtrace_symbols e backtrace_symbols_fd podem ser usadas. Veja as páginas de manual correspondentes.

terminus
fonte
5

Parece que em uma das últimas versões da biblioteca de c ++ apareceu a biblioteca para fornecer exatamente o que você deseja, provavelmente o código seria multiplataforma. É boost :: stacktrace , que você pode usar como na amostra boost :

#include <filesystem>
#include <sstream>
#include <fstream>
#include <signal.h>     // ::signal, ::raise
#include <boost/stacktrace.hpp>

const char* backtraceFileName = "./backtraceFile.dump";

void signalHandler(int)
{
    ::signal(SIGSEGV, SIG_DFL);
    ::signal(SIGABRT, SIG_DFL);
    boost::stacktrace::safe_dump_to(backtraceFileName);
    ::raise(SIGABRT);
}

void sendReport()
{
    if (std::filesystem::exists(backtraceFileName))
    {
        std::ifstream file(backtraceFileName);

        auto st = boost::stacktrace::stacktrace::from_dump(file);
        std::ostringstream backtraceStream;
        backtraceStream << st << std::endl;

        // sending the code from st

        file.close();
        std::filesystem::remove(backtraceFileName);
    }
}

int main()
{
    ::signal(SIGSEGV, signalHandler);
    ::signal(SIGABRT, signalHandler);

    sendReport();
    // ... rest of code
}

No Linux, você compila o código acima:

g++ --std=c++17 file.cpp -lstdc++fs -lboost_stacktrace_backtrace -ldl -lbacktrace

Exemplo de backtrace copiado da documentação do impulso :

0# bar(int) at /path/to/source/file.cpp:70
1# bar(int) at /path/to/source/file.cpp:70
2# bar(int) at /path/to/source/file.cpp:70
3# bar(int) at /path/to/source/file.cpp:70
4# main at /path/to/main.cpp:93
5# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
6# _start
baziorek
fonte
4

* nix: você pode interceptar o SIGSEGV (normalmente esse sinal é disparado antes de travar) e manter as informações em um arquivo. (além do arquivo principal que você pode usar para depurar usando o gdb, por exemplo).

win: Verifique isso no msdn.

Você também pode olhar o código do Google Chrome para ver como ele lida com falhas. Ele tem um bom mecanismo de manipulação de exceções.

INS
fonte
O SEH não ajuda na produção de um rastreamento de pilha. Embora possa fazer parte de uma solução, essa solução é mais difícil de implementar e fornece menos informações às custas da divulgação de mais informações sobre seu aplicativo do que a solução real : escreva um mini dump. E configure o Windows para fazer isso automaticamente para você.
IInspectable
4

Descobri que a solução @tgamblin não está completa. Ele não pode lidar com o stackoverflow. Eu acho que porque, por padrão, o manipulador de sinal é chamado com a mesma pilha e o SIGSEGV é acionado duas vezes. Para proteger você precisa registrar uma pilha independente para o manipulador de sinal.

Você pode verificar isso com o código abaixo. Por padrão, o manipulador falha. Com a macro definida STACK_OVERFLOW, está tudo certo.

#include <iostream>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <cassert>

using namespace std;

//#define STACK_OVERFLOW

#ifdef STACK_OVERFLOW
static char stack_body[64*1024];
static stack_t sigseg_stack;
#endif

static struct sigaction sigseg_handler;

void handler(int sig) {
  cerr << "sig seg fault handler" << endl;
  const int asize = 10;
  void *array[asize];
  size_t size;

  // get void*'s for all entries on the stack
  size = backtrace(array, asize);

  // print out all the frames to stderr
  cerr << "stack trace: " << endl;
  backtrace_symbols_fd(array, size, STDERR_FILENO);
  cerr << "resend SIGSEGV to get core dump" << endl;
  signal(sig, SIG_DFL);
  kill(getpid(), sig);
}

void foo() {
  foo();
}

int main(int argc, char **argv) {
#ifdef STACK_OVERFLOW
  sigseg_stack.ss_sp = stack_body;
  sigseg_stack.ss_flags = SS_ONSTACK;
  sigseg_stack.ss_size = sizeof(stack_body);
  assert(!sigaltstack(&sigseg_stack, nullptr));
  sigseg_handler.sa_flags = SA_ONSTACK;
#else
  sigseg_handler.sa_flags = SA_RESTART;  
#endif
  sigseg_handler.sa_handler = &handler;
  assert(!sigaction(SIGSEGV, &sigseg_handler, nullptr));
  cout << "sig action set" << endl;
  foo();
  return 0;
} 
Daneel S. Yaitskov
fonte
4

O novo rei da cidade chegou https://github.com/bombela/backward-cpp

1 cabeçalho para colocar no seu código e 1 biblioteca para instalar.

Pessoalmente, eu chamo isso usando essa função

#include "backward.hpp"
void stacker() {

using namespace backward;
StackTrace st;


st.load_here(99); //Limit the number of trace depth to 99
st.skip_n_firsts(3);//This will skip some backward internal function from the trace

Printer p;
p.snippet = true;
p.object = true;
p.color = true;
p.address = true;
p.print(st, stderr);
}
Roy
fonte
Uau! Finalmente é assim que deve ser feito! Acabei de lançar uma solução própria em favor desta.
tglas 01/12/19
3

Eu usaria o código que gera um rastreamento de pilha para vazamento de memória no Visual Leak Detector . Isso funciona apenas no Win32, no entanto.

Jim Buck
fonte
E requer que você envie símbolos de depuração com seu código. Em geral, não é desejável. Escreva um mini dump e configure o Windows para fazer isso automaticamente em exceções não tratadas.
11nspectable
3

Eu já vi muitas respostas aqui realizando um manipulador de sinal e saindo. Esse é o caminho a seguir, mas lembre-se de um fato muito importante: se você deseja obter o dump principal do erro gerado, não pode ligar exit(status). Ligue em abort()vez disso!

jard18
fonte
3

Como uma solução apenas para Windows, você pode obter o equivalente a um rastreamento de pilha (com muito, muito mais informações) usando o Relatório de Erros do Windows . Com apenas algumas entradas do Registro, ele pode ser configurado para coletar despejos no modo de usuário :

A partir do Windows Server 2008 e do Windows Vista com Service Pack 1 (SP1), o Relatório de Erros do Windows (WER) pode ser configurado para que os despejos completos do modo de usuário sejam coletados e armazenados localmente após a falha de um aplicativo no modo de usuário. [...]

Este recurso não está ativado por padrão. A ativação do recurso requer privilégios de administrador. Para habilitar e configurar o recurso, use os seguintes valores do registro na chave HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows \ Windows Error Reporting \ LocalDumps .

Você pode definir as entradas do registro no seu instalador, que possui os privilégios necessários.

A criação de um dump no modo de usuário tem as seguintes vantagens sobre a geração de um rastreamento de pilha no cliente:

  • Já está implementado no sistema. Você pode usar o WER conforme descrito acima ou ligar para MiniDumpWriteDump , se precisar de um controle mais refinado sobre a quantidade de informações a serem despejadas. (Lembre-se de chamá-lo de um processo diferente.)
  • Muito mais completo que um rastreamento de pilha. Entre outros, pode conter variáveis ​​locais, argumentos de função, pilhas para outros threads, módulos carregados etc. A quantidade de dados (e consequentemente o tamanho) é altamente personalizável.
  • Não há necessidade de enviar símbolos de depuração. Isso diminui drasticamente o tamanho da sua implantação e dificulta a engenharia reversa do seu aplicativo.
  • Em grande parte independente do compilador que você usa. O uso do WER nem requer nenhum código. De qualquer maneira, ter uma maneira de obter um banco de dados de símbolos (PDB) é muito útil para análises offline. Acredito que o GCC pode gerar PDBs ou existem ferramentas para converter o banco de dados de símbolos no formato PDB.

Observe que o WER só pode ser acionado por uma falha no aplicativo (ou seja, o sistema encerra um processo devido a uma exceção não tratada). MiniDumpWriteDumppode ser chamado a qualquer momento. Isso pode ser útil se você precisar despejar o estado atual para diagnosticar problemas que não sejam uma falha.

Leitura obrigatória, se você quiser avaliar a aplicabilidade de mini despejos:

IIspectável
fonte
2

Além das respostas acima, aqui como você faz o Debian Linux OS gerar dump principal

  1. Crie uma pasta "coredumps" na pasta inicial do usuário
  2. Vá para /etc/security/limits.conf. Abaixo da linha '', digite “núcleo flexível ilimitado” e “núcleo flexível raiz ilimitado” se ativar dumps de núcleo para raiz, para permitir espaço ilimitado para dumps de núcleo.
  3. NOTA: “* soft core ilimitado” não cobre raiz, e é por isso que a raiz deve ser especificada em sua própria linha.
  4. Para verificar esses valores, efetue logout, efetue login novamente e digite "ulimit -a". O "tamanho do arquivo principal" deve ser definido como ilimitado.
  5. Verifique os arquivos .bashrc (usuário e raiz, se aplicável) para garantir que o ulimit não esteja definido lá. Caso contrário, o valor acima será substituído na inicialização.
  6. Abra o /etc/sysctl.conf. Digite o seguinte na parte inferior: “kernel.core_pattern = /home//coredumps/%e_%t.dump”. (% e será o nome do processo e% t será a hora do sistema)
  7. Saia e digite “sysctl -p” para carregar a nova configuração Verifique / proc / sys / kernel / core_pattern e verifique se isso corresponde ao que você acabou de digitar.
  8. O core dump pode ser testado executando um processo na linha de comando ("&") e depois matando-o com "kill -11". Se o core dumping for bem-sucedido, você verá “(core dumped)” após a indicação de falha de segmentação.
entusiasta
fonte
2

Se você ainda quiser seguir sozinho, como eu, pode vincular bfde evitar o usoaddr2line como fiz aqui:

https://github.com/gnif/LookingGlass/blob/master/common/src/crash.linux.c

Isso produz a saída:

[E]        crash.linux.c:170  | crit_err_hdlr                  | ==== FATAL CRASH (a12-151-g28b12c85f4+1) ====
[E]        crash.linux.c:171  | crit_err_hdlr                  | signal 11 (Segmentation fault), address is (nil)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (0) /home/geoff/Projects/LookingGlass/client/src/main.c:936 (register_key_binds)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (1) /home/geoff/Projects/LookingGlass/client/src/main.c:1069 (run)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (2) /home/geoff/Projects/LookingGlass/client/src/main.c:1314 (main)
[E]        crash.linux.c:199  | crit_err_hdlr                  | [trace]: (3) /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f8aa65f809b]
[E]        crash.linux.c:199  | crit_err_hdlr                  | [trace]: (4) ./looking-glass-client(_start+0x2a) [0x55c70fc4aeca]
Geoffrey
fonte
1

No Linux / unix / MacOSX, use arquivos principais (você pode ativá-los com ulimit ou chamada de sistema compatível ). No Windows, use o relatório de erros da Microsoft (você pode se tornar um parceiro e obter acesso aos dados de falha do aplicativo).

Kasprzol
fonte
0

Eu esqueci a tecnologia GNOME de "apport", mas não sei muito sobre usá-la. É usado para gerar rastreamentos de pilha e outros diagnósticos para processamento e pode arquivar bugs automaticamente. Certamente vale a pena conferir.


fonte