Jak automatycznie wygenerować ślad stosu, gdy mój program się zawiesi


590

Pracuję na systemie Linux z kompilatorem GCC. Gdy mój program C ++ ulega awarii, chciałbym, aby automatycznie generował ślad stosu.

Mój program jest uruchamiany przez wielu różnych użytkowników, a także działa na systemach Linux, Windows i Macintosh (wszystkie wersje są kompilowane przy użyciu gcc).

Chciałbym, aby mój program był w stanie wygenerować ślad stosu, gdy ulega awarii, a następnym razem, gdy użytkownik go uruchomi, zapyta go, czy można wysłać ślad stosu do mnie, abym mógł wyśledzić problem. Mogę obsłużyć wysyłanie informacji do mnie, ale nie wiem, jak wygenerować ciąg śledzenia. Jakieś pomysły?


4
backtrace i backtrace_symbols_fd nie są bezpieczne dla sygnału asynchronicznego. nie należy używać tych funkcji w
module

10
backtrace_symbols wywołuje malloc, dlatego nie można go używać w procedurze obsługi sygnałów. Pozostałe dwie funkcje (backtrace i backtrace_symbols_fd) nie mają tego problemu i są powszechnie używane w programach obsługi sygnałów.
cmccabe,

3
@cmccabe, który jest niepoprawny backtrace_symbols_fd zwykle nie wywołuje malloc, ale może, jeśli coś pójdzie nie tak w bloku catch_error
Sam Saffron

6
„Może” w tym sensie, że nie ma specyfikacji POSIX dla backtrace_symbols_fd (lub jakiegokolwiek innego śledzenia); jednakże, backtrace_symbols_fd GNU / Linuksa jest określony, aby nigdy nie wywoływał malloc, zgodnie z linux.die.net/man/3/backtrace_symbols_fd . Dlatego można bezpiecznie założyć, że nigdy nie wywoła malloc w systemie Linux.
codetaku

Odpowiedzi:


509

W przypadku systemu Linux i uważam, że w systemie Mac OS X, jeśli używasz gcc lub dowolnego kompilatora korzystającego z glibc, możesz użyć funkcji backtrace () w execinfo.hcelu wydrukowania pliku stacktrace i wyjścia z wdziękiem, gdy pojawi się błąd segmentacji. Dokumentację można znaleźć w podręczniku libc .

Oto przykładowy program, który instaluje SIGSEGVmoduł obsługi i drukuje ślad stosu stderrpodczas segfaulta. Ta baz()funkcja powoduje segfault, który wyzwala moduł obsługi:

#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.
}

Kompilacja z -g -rdynamicpozwala uzyskać informacje o symbolu w danych wyjściowych, których glibc może użyć do stworzenia ładnego stacktrace:

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

Wykonanie tego daje ci następujące dane wyjściowe:

$ ./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]

Pokazuje moduł ładowania, przesunięcie i funkcję, z której pochodzi każda ramka na stosie. Tutaj można zobaczyć obsługi sygnału na wierzchu stosu i funkcji libc przed mainoprócz main, foo, bar, i baz.


53
Istnieje również /lib/libSegFault.so, którego można używać z LD_PRELOAD.
CesarB

6
Wygląda na to, że dwa pierwsze wpisy na wyjściu śledzenia wstecznego zawierają adres zwrotny wewnątrz procedury obsługi sygnału i prawdopodobnie jeden wewnątrz sigaction()libc. Podczas gdy twój ślad wydaje się być poprawny, czasami stwierdziłem, że konieczne są dodatkowe kroki, aby upewnić się, że faktyczna lokalizacja błędu pojawia się w śladzie wstecznym, ponieważ może zostać zastąpiony sigaction()przez jądro.
jschmier

9
Co by się stało, gdyby katastrofa pochodziła z centrum handlowego Malloc? Czy nie trzymałbyś wtedy blokady, a potem utknąłeś, gdy „ślad” próbuje przydzielić pamięć?
Mattias Nilsson,

7
catchsegvnie jest tym, czego potrzebuje OP, ale jest świetny do wychwytywania błędów segmentacji i uzyskiwania wszystkich informacji.
Matt Clarkson

8
W przypadku ARM musiałem również skompilować z tabelami -funwind-tab. W przeciwnym razie moja głębokość stosu zawsze wynosiła 1 (pusta).
jfritz42

128

Jest to nawet łatwiejsze niż „man backtrace”, istnieje mała udokumentowana biblioteka (specyficzna dla GNU) rozpowszechniana z glibc jako libSegFault.so, która, jak sądzę, została napisana przez Ulricha Dreppera do obsługi programu catchsegv (patrz „man catchsegv”).

To daje nam 3 możliwości. Zamiast uruchamiać „program -o hai”:

  1. Uruchom w catchsegv:

    $ catchsegv program -o hai
  2. Łącze z libSegFault w czasie wykonywania:

    $ LD_PRELOAD=/lib/libSegFault.so program -o hai
  3. Łącze z libSegFault w czasie kompilacji:

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

We wszystkich 3 przypadkach otrzymasz wyraźniejsze ślady wstecz z mniejszą optymalizacją (gcc -O0 lub -O1) i symbolami debugowania (gcc -g). W przeciwnym razie możesz po prostu skończyć ze stosem adresów pamięci.

Możesz także złapać więcej sygnałów dla śladów stosu za pomocą czegoś takiego:

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

Dane wyjściowe będą wyglądać mniej więcej tak (zwróć uwagę na ślad u dołu):

*** 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]

Jeśli chcesz poznać szczegóły krwawe, najlepszym źródłem jest niestety źródło: patrz http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c i jego katalog nadrzędny http://sourceware.org/git/?p=glibc.git;a=tree;f=debug


1
„Możliwość 3. Łącze z libSegFault w czasie kompilacji” nie działa.
HHK

5
@crafter: Co masz na myśli mówiąc „nie działa”. Czego próbowałeś, na jakim języku / kompilatorze / toolchain / dystrybucji / sprzęcie? Nie udało się skompilować? Aby złapać błąd? Czy w ogóle produkować? Aby wyprodukować trudne w użyciu wyjście? Dziękuję za szczegóły, pomoże wszystkim.
Stéphane Gourichon

1
„najlepsze źródło jest niestety źródłem” ... Mam nadzieję, że któregoś dnia strona podręcznika dla catchsegv faktycznie wspomni SEGFAULT_SIGNALS. Do tego czasu istnieje odpowiedź, na którą można się powołać.
greggo

Nie mogę uwierzyć, że programuję C od 5 lat i nigdy o tym nie słyszałem: /
DavidMFrey

6
@ StéphaneGourichon @HansKratz Aby połączyć się z libSegFault, musisz dodać -Wl,--no-as-neededdo flag kompilatora. W przeciwnym razie ldrzeczywiście nie będzie łączył się przeciwko libSegFault, ponieważ rozpoznaje, że plik binarny nie używa żadnego z jego symboli.
Phillip

122

Linux

Chociaż użycie funkcji backtrace () w pliku execinfo.h do drukowania stacktrace i wyjścia z gracją po otrzymaniu błędu segmentacji zostało już zasugerowane , nie widzę wzmianki o zawiłościach koniecznych do zapewnienia, że ​​wynikowy ślad wskazuje rzeczywistą lokalizację błąd (przynajmniej dla niektórych architektur - x86 i ARM).

Pierwsze dwa wpisy w łańcuchu ramek stosu po wejściu do modułu obsługi sygnału zawierają adres zwrotny wewnątrz modułu obsługi sygnału i jeden wewnątrz sigaction () w libc. Ramka stosu ostatniej funkcji wywołanej przed sygnałem (będącym lokalizacją błędu) zostaje utracona.

Kod

#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);
}

Wynik

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]

Wszystkie niebezpieczeństwa wywołania funkcji backtrace () w procedurze obsługi sygnałów nadal istnieją i nie należy ich lekceważyć, ale funkcjonalność, którą tu opisałem, jest bardzo pomocna w debugowaniu awarii.

Należy zauważyć, że podany przeze mnie przykład został opracowany / przetestowany w systemie Linux dla x86. Z powodzeniem zaimplementowałem to również w ARM za pomocą uc_mcontext.arm_pczamiast uc_mcontext.eip.

Oto link do artykułu, w którym poznałem szczegóły tej implementacji: http://www.linuxjournal.com/article/6391


11
W systemach korzystających z GNU ld pamiętaj o kompilacji z -rdynamicpoleceniem linkerowi dodania wszystkich symboli, nie tylko używanych, do dynamicznej tablicy symboli. Pozwala to backtrace_symbols()na konwersję adresów na nazwy funkcji
jschmier

1
Musisz także dodać opcję „-mapcs-frame” do wiersza poleceń GCC, aby wygenerować ramki stosu na platformie ARM
qehgt

3
To może być za późno, ale czy możemy w addr2linejakiś sposób użyć polecenia, aby uzyskać dokładną linię, w której nastąpiła awaria?
entuzjastyczny

4
W nowszych wersjach glibc uc_mcontextnie zawiera pola o nazwie eip. Jest teraz tablica, która musi zostać zindeksowana, uc_mcontext.gregs[REG_EIP]jest równoważna.
mmlb

6
W przypadku ARM moje ślady zawsze miały głębokość 1, dopóki nie dodałem opcji -funwind-tables do kompilatora.
jfritz42

84

Mimo że podano prawidłową odpowiedź , która opisuje sposób korzystania z backtrace()funkcji GNU libc 1, a ja podałem własną odpowiedź, która opisuje, w jaki sposób zapewnić powrót śledzenia z procedury obsługi sygnału do faktycznej lokalizacji błędu 2 , nie widzę wszelkie wzmianki o demontażu symboli C ++ wyprowadzanych z śledzenia wstecznego.

Podczas uzyskiwania śladów zwrotnych z programu C ++ dane wyjściowe można przepuszczać przez c++filt1, aby rozplątać symbole lub bezpośrednio 1 .abi::__cxa_demangle

  • 1 Linux i OS X Zauważ, że c++filti __cxa_demanglesą specyficzne GCC
  • 2 Linux

Poniższy przykład C ++ Linux używa tej samej procedury obsługi sygnału, co moja inna odpowiedź i pokazuje, jak c++filtmożna użyć do rozplątania symboli.

Kod :

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;
}

Wyjście ( ./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]

Wyjście demontowane ( ./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]

Poniższy fragment opiera się na procedurze obsługi sygnału z mojej oryginalnej odpowiedzi i może zastąpić procedurę obsługi sygnału w powyższym przykładzie, aby zademonstrować, jak abi::__cxa_demanglemożna użyć do rozplątania symboli. Ta procedura obsługi sygnału wytwarza takie same odkodowane dane wyjściowe jak w powyższym przykładzie.

Kod :

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);
}

1
Dziękuję ci za to, jschmier. Stworzyłem mały skrypt bash, aby przekazać dane wyjściowe do narzędzia addr2line. Zobacz: stackoverflow.com/a/15801966/1797414
arr_sea

4
Nie zapomnij o #include <cxxabi.h>
Bamaco

1
Dobra dokumentacja i prosty plik nagłówkowy został opublikowany tutaj od 2008 roku ... panthema.net/2008/0901-stacktrace-demangled bardzo podobny do twojego podejścia :)
kevinf

abi :: __ cxa_demangle wydaje się nie być bezpiecznym sygnałem asynchronicznym, więc procedura obsługi sygnału może zakleszczyć się gdzieś w malloc.
orcy

Zastosowanie std::cerr, free()a exit()wszystko naruszać ograniczeń wobec dzwoni non-asynchroniczny sygnał bezpieczne połączenia w systemach POSIX. Kod ten impas, jeśli proces nie powiedzie się w każdym wywołaniu takich jak free(), malloc() newlub detete.
Andrew Henle

31

Warto przyjrzeć się Google Breakpad , wieloplatformowemu generatorowi zrzutów awaryjnych i narzędziom do przetwarzania zrzutów.


Raportuje takie rzeczy jak błędy segmentacji, ale nie zgłasza żadnych informacji na temat nieobsługiwanych wyjątków C ++.
DBedrenko,

21

Nie określiłeś systemu operacyjnego, więc trudno jest na nie odpowiedzieć. Jeśli używasz systemu opartego na gnu libc, być może będziesz mógł skorzystać z funkcji libc backtrace().

GCC ma również dwa wbudowane narzędzia, które mogą ci pomóc, ale które mogą, ale nie muszą być w pełni zaimplementowane w twojej architekturze, i są __builtin_frame_addressi __builtin_return_address. Oba chcą bezpośredniego poziomu liczb całkowitych (przez natychmiastowe, to znaczy, że nie może to być zmienna). Jeśli __builtin_frame_addressdla danego poziomu nie jest zero, należy bezpiecznie pobrać adres zwrotny tego samego poziomu.


13

Dziękuję entuzjastycznie za zwrócenie mojej uwagi na narzędzie addr2line.

Napisałem szybki i brudny skrypt do przetworzenia wyniku odpowiedzi podanej tutaj : (wielkie dzięki dla jschmier!) Za pomocą narzędzia addr2line.

Skrypt akceptuje jeden argument: nazwę pliku zawierającego dane wyjściowe z narzędzia jschmier.

Dane wyjściowe powinny wypisać coś takiego dla każdego poziomu śledzenia:

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;

Kod:

#!/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 

12

ulimit -c <value>ustawia limit rozmiaru rdzenia pliku na Uniksie. Domyślnie limit rozmiaru podstawowego pliku wynosi 0. Możesz zobaczyć swoje ulimitwartości za pomocą ulimit -a.

Ponadto, jeśli uruchomisz program z poziomu gdb, zatrzyma on Twój program w przypadku „naruszeń segmentacji” ( SIGSEGVzazwyczaj, gdy uzyskasz dostęp do fragmentu pamięci, który nie został przydzielony) lub możesz ustawić punkty przerwania.

ddd i nemiver to nakładki na gdb, które znacznie ułatwiają pracę z nim nowicjuszowi.


6
Zrzuty rdzenia są nieskończenie bardziej przydatne niż ślady stosu, ponieważ można załadować zrzut rdzenia do debuggera i zobaczyć stan całego programu i jego danych w momencie awarii.
Adam Hawes

1
Funkcja śledzenia wstecznego, którą sugerowali inni, jest prawdopodobnie lepsza niż nic, ale jest bardzo prosta - nawet nie podaje numerów linii. Z drugiej strony, korzystając ze zrzutów rdzenia, możesz z mocą wsteczną wyświetlić cały stan aplikacji w momencie jej awarii (w tym szczegółowy ślad stosu). Nie może być praktyczne problemy z starając się korzystać z tego pola do debugowania, ale jest zdecydowanie bardziej potężnym narzędziem do analizowania wypadków i utrzymuje w trakcie rozwoju (przynajmniej na Linuksie).
nobar

10

Ważne jest, aby pamiętać, że po wygenerowaniu pliku podstawowego należy użyć narzędzia gdb, aby na niego spojrzeć. Aby gdb zrozumiał twój plik podstawowy, musisz powiedzieć gcc, aby instrumentował plik binarny za pomocą symboli debugowania: aby to zrobić, skompiluj flagę -g:

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

Następnie możesz ustawić „ulimit -c unlimited”, aby zrzucił rdzeń, lub po prostu uruchomić program w gdb. Drugie podejście bardziej mi się podoba:

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

Mam nadzieję, że to pomoże.


4
Możesz także zadzwonić gdbbezpośrednio z programu do zawieszania się. Procedura obsługi SIGSEGV, SEGILL, SIGBUS, SIGFPE, która wywoła gdb. Szczegóły: stackoverflow.com/questions/3151779/… Zaletą jest to, że otrzymujesz piękne, opatrzone adnotacjami bt fullślady wstecz, jak w , a także możesz uzyskać ślady stosu wszystkich wątków.
Vi.

Możesz także uzyskać śledzenie wstecz łatwiejsze niż w odpowiedzi: gdb -silent ./prog core --eval-command = backtrace --batch -it wyświetli ślad i zamknie debugger
baziorek

10

Przez jakiś czas przyglądałem się temu problemowi.

I pochowany głęboko w README Narzędzi Google

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

mówi o libunwind

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

Bardzo chciałbym usłyszeć opinie o tej bibliotece.

Problem z -rdynamic polega na tym, że w niektórych przypadkach może on znacznie zwiększyć rozmiar pliku binarnego


2
Na x86 / 64 nie widziałem -rdynamic znacznie zwiększającego rozmiar binarny. Dodanie -g powoduje znacznie większy wzrost.
Dan

1
Zauważyłem, że libunwind nie ma funkcji, aby uzyskać numer linii, i chyba (nie testowałem) un_get_proc_name zwraca symbol funkcji (która jest zaciemniona z powodu przeciążenia itp.) Zamiast oryginalnej nazwy.
Herbert

1
To jest poprawne. Bardzo trudno jest to zrobić poprawnie, ale miałem doskonały sukces z gaddr2line, jest tu wiele praktycznych informacji blog.bigpixel.ro/2010/09/stack-unwinding-stack-trace-with-gcc
Gregory


9

Możesz użyć DeathHandler - małej klasy C ++, która robi wszystko za Ciebie, niezawodnie.


1
niestety używa execlp()do wykonywania wywołań addr2line ... fajnie byłoby w pełni pozostać we własnym programie (co jest możliwe dzięki dołączeniu kodu addr2line w jakiejś formie)
przykład

9

Zapomnij o zmianie źródeł i zrób kilka hacków z funkcją backtrace () lub makrami - to tylko kiepskie rozwiązania.

Jako właściwie działające rozwiązanie doradziłbym:

  1. Skompiluj swój program z flagą „-g”, aby osadzić binarne symbole debugowania (nie martw się, nie wpłynie to na wydajność).
  2. W systemie Linux uruchom następną komendę: „ulimit -c unlimited” - aby umożliwić systemowi zrzuty dużych awarii.
  3. Gdy twój program się zawiesi, w katalogu roboczym zobaczysz plik „core”.
  4. Uruchom następną komendę, aby wydrukować ślad na standardowe wyjście: gdb -batch -ex "backtrace" ./your_program_exe ./core

Spowoduje to wydrukowanie odpowiedniego, czytelnego śladu wstecznego programu w sposób czytelny dla człowieka (z nazwami plików źródłowych i numerami wierszy). Co więcej, takie podejście zapewni Ci swobodę automatyzacji systemu: przygotuj krótki skrypt, który sprawdza, czy proces utworzył zrzut pamięci, a następnie wysyłaj ślady za pośrednictwem poczty e-mail do programistów lub zaloguj się do jakiegoś systemu logowania.


Podaje nieprawidłowe numery linii. Czy można to poprawić?
HeyJude,

7
ulimit -c unlimited

jest zmienną systemową, która pozwoli na utworzenie zrzutu podstawowego po awarii aplikacji. W tym przypadku nieograniczona ilość. Poszukaj pliku o nazwie core w tym samym katalogu. Upewnij się, że skompilowałeś swój kod z włączonymi informacjami debugowania!

pozdrowienia


5
Użytkownik nie prosi o zrzut podstawowy. On prosi o ślad stosu. Zobacz delorie.com/gnu/docs/glibc/libc_665.html
Todd Gamblin

1
zrzut podstawowy będzie zawierał stos wywołań w momencie awarii, prawda?
Pon

3
Zakładasz, że jest na Uniksie i używasz Basha.
Paul Tomblin,

2
Jeśli używasz tcsh, musisz zrobićlimit coredumpsize unlimited
sivabudh

6

Patrzeć na:

man 3 backtrace

I:

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

Są to rozszerzenia GNU.


2
Na tej stronie mogą znajdować się dodatkowe przykłady, które pomogłem
Stéphane

6

Zobacz funkcję śledzenia stosu w ACE (ADAPTIVE Communication Environment). Jest już napisany, aby objąć wszystkie główne platformy (i więcej). Biblioteka jest licencjonowana w stylu BSD, więc możesz nawet kopiować / wklejać kod, jeśli nie chcesz używać ACE.


Łącze wydaje się martwe.
tglas,

5

Mogę pomóc w wersji dla systemu Linux: można użyć funkcji backtrace, backtrace_symbols i backtrace_symbols_fd. Zobacz odpowiednie strony podręcznika.


5

Wygląda na to, że w jednej z ostatnich wersji doładowania c ++ pojawiła się biblioteka zapewniająca dokładnie to, czego chcesz, prawdopodobnie kod byłby wieloplatformowy. Jest to boost :: stacktrace , którego możesz używać jak w próbce 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
}

W systemie Linux kompilujesz powyższy kod:

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

Przykład śledzenia wstecznego skopiowanego z dokumentacji doładowania :

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

4

* nix: możesz przechwycić SIGSEGV (zwykle ten sygnał jest podnoszony przed awarią) i przechowywać informacje w pliku. (oprócz podstawowego pliku, którego można użyć do debugowania na przykład przy użyciu gdb).

win: sprawdź to z msdn.

Możesz także spojrzeć na kod chrome Google, aby zobaczyć, jak radzi sobie z awariami. Ma ładny mechanizm obsługi wyjątków.


SEH nie pomaga w tworzeniu śladu stosu. Chociaż może to być część rozwiązania, jest ono trudniejsze do wdrożenia i zapewnia mniej informacji kosztem ujawnienia większej ilości informacji o Twojej aplikacji niż prawdziwe rozwiązanie: Napisz mini zrzut. I skonfiguruj system Windows, aby zrobił to automatycznie.
Widoczny

4

Odkryłem, że rozwiązanie @tgamblin nie jest kompletne. Nie można go obsłużyć przy przepełnieniu stosu. Myślę, że ponieważ domyślnie procedura obsługi sygnału jest wywoływana z tym samym stosem, a SIGSEGV jest wyrzucany dwukrotnie. Aby go chronić, musisz zarejestrować niezależny stos dla procedury obsługi sygnału.

Możesz to sprawdzić za pomocą kodu poniżej. Domyślnie procedura obsługi kończy się niepowodzeniem. Przy zdefiniowanym makrze STACK_OVERFLOW wszystko jest w porządku.

#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;
} 

4

Nowy król w mieście przybył https://github.com/bombela/backward-cpp

1 nagłówek do umieszczenia w kodzie i 1 biblioteka do zainstalowania.

Osobiście nazywam to za pomocą tej funkcji

#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);
}

Łał! Tak wreszcie należy to zrobić! Właśnie rzuciłem własne rozwiązanie na korzyść tego.
tglas,

3

Użyłbym kodu, który generuje ślad stosu dla wycieku pamięci w Visual Leak Detector . Działa to jednak tylko w systemie Win32.


I wymaga wysyłania symboli debugowania wraz z kodem. Zasadniczo niepożądane. Napisz mini zrzut i skonfiguruj system Windows, aby robił to automatycznie dla nieobsługiwanych wyjątków.
Widoczny

3

Widziałem tutaj wiele odpowiedzi wykonujących procedurę obsługi sygnału, a następnie wychodzących. Tak należy postępować, ale pamiętaj o bardzo ważnym fakcie: jeśli chcesz uzyskać zrzut pamięci dla wygenerowanego błędu, nie możesz zadzwonić exit(status). Zadzwoń abort()zamiast!


3

Jako rozwiązanie tylko dla systemu Windows można uzyskać ekwiwalent śledzenia stosu (z dużo, znacznie więcej informacji) za pomocą Raportowania błędów systemu Windows . Za pomocą zaledwie kilku wpisów rejestru można skonfigurować zbieranie zrzutów w trybie użytkownika :

Począwszy od systemu Windows Server 2008 i Windows Vista z dodatkiem Service Pack 1 (SP1), raportowanie błędów systemu Windows (WER) można skonfigurować tak, aby pełne zrzuty w trybie użytkownika były gromadzone i przechowywane lokalnie po awarii aplikacji w trybie użytkownika. [...]

Ta funkcja nie jest domyślnie włączona. Włączenie tej funkcji wymaga uprawnień administratora. Aby włączyć i skonfigurować tę funkcję, użyj następujących wartości rejestru w kluczu HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows \ Windows Error Reporting \ LocalDumps .

Wpisy rejestru można ustawić za pomocą instalatora, który ma wymagane uprawnienia.

Utworzenie zrzutu trybu użytkownika ma następujące zalety w porównaniu do generowania śledzenia stosu na kliencie:

  • Jest już zaimplementowany w systemie. Możesz albo użyć WER, jak opisano powyżej, lub samodzielnie wywołać MiniDumpWriteDump , jeśli potrzebujesz bardziej szczegółowej kontroli nad ilością informacji do zrzucenia. (Pamiętaj, aby wywołać go z innego procesu).
  • Znacznie bardziej kompletny niż ślad stosu. Między innymi może zawierać zmienne lokalne, argumenty funkcji, stosy dla innych wątków, załadowane moduły i tak dalej. Ilość danych (a co za tym idzie rozmiar) jest wysoce konfigurowalna.
  • Nie ma potrzeby wysyłania symboli debugowania. To zarówno drastycznie zmniejsza rozmiar wdrożenia, jak i utrudnia przebudowę aplikacji.
  • W dużej mierze niezależny od kompilatora, którego używasz. Korzystanie z WER nie wymaga nawet żadnego kodu. Tak czy inaczej, posiadanie sposobu na uzyskanie bazy danych symboli (PDB) jest bardzo przydatne do analizy offline. Wierzę, że GCC może albo generować PDB, albo istnieją narzędzia do konwersji bazy danych symboli do formatu PDB.

Zauważ, że WER może być wyzwalany tylko przez awarię aplikacji (tj. System kończący proces z powodu nieobsługiwanego wyjątku). MiniDumpWriteDumpmożna wywołać w dowolnym momencie. Może to być pomocne, jeśli musisz zrzucić bieżący stan, aby zdiagnozować problemy inne niż awaria.

Obowiązkowa lektura, jeśli chcesz ocenić możliwość zastosowania mini-zrzutu:


2

Oprócz powyższych odpowiedzi, tutaj możesz dowiedzieć się, jak sprawić, by Debian Linux OS generował zrzut pamięci

  1. Utwórz folder „coredumps” w folderze domowym użytkownika
  2. Przejdź do /etc/security/limits.conf. Pod linią „” wpisz „soft core unlimited” i „root soft core unlimited”, jeśli włączasz zrzuty rdzeni dla roota, aby zapewnić nieograniczoną przestrzeń dla zrzutów rdzeni.
  3. UWAGA: „* soft core unlimited” nie obejmuje roota, dlatego root musi być określony we własnej linii.
  4. Aby sprawdzić te wartości, wyloguj się, zaloguj ponownie i wpisz „ulimit -a”. „Rozmiar pliku podstawowego” należy ustawić na nieograniczony.
  5. Sprawdź pliki .bashrc (użytkownik i root, jeśli dotyczy), aby upewnić się, że nie ustawiono tam ulimit. W przeciwnym razie powyższa wartość zostanie nadpisana podczas uruchamiania.
  6. Otwórz /etc/sysctl.conf. Wpisz u dołu: „kernel.core_pattern = /home//coredumps/%e_%t.dump”. (% e będzie nazwą procesu, a% t będzie czasem systemowym)
  7. Wyjdź i wpisz „sysctl -p”, aby załadować nową konfigurację. Sprawdź / proc / sys / kernel / core_pattern i sprawdź, czy odpowiada temu, co właśnie wpisałeś.
  8. Zrzut rdzenia można przetestować, uruchamiając proces w wierszu poleceń („&”), a następnie zabijając go za pomocą „kill -11”. Jeśli zrzut rdzenia zakończy się powodzeniem, po wskazaniu błędu segmentacji pojawi się „(zrzut rdzenia)”.

2

Jeśli nadal chcesz iść sam, tak jak ja, możesz połączyć się z linkiem bfdi unikać używania addr2linetego, co zrobiłem tutaj:

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

To daje wynik:

[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]

1

W systemach Linux / unix / MacOSX używaj plików podstawowych (możesz włączyć je za pomocą ulimit lub kompatybilnego wywołania systemowego ). W systemie Windows używaj raportowania błędów Microsoft (możesz zostać partnerem i uzyskać dostęp do danych awarii aplikacji).


0

Zapomniałem o technologii „apport” w GNOME, ale niewiele wiem o jej używaniu. Służy do generowania śladów stosu i innych danych diagnostycznych do przetwarzania i może automatycznie zgłaszać błędy. Na pewno warto się zameldować.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.