Как автоматически генерировать трассировку стека при сбое моей программы - PullRequest
531 голосов
/ 17 сентября 2008

Я работаю в Linux с компилятором GCC. Когда моя программа на C ++ падает, я бы хотел, чтобы она автоматически генерировала трассировку стека.

Моя программа запускается многими разными пользователями, а также работает в Linux, Windows и Macintosh (все версии скомпилированы с использованием gcc).

Я бы хотел, чтобы моя программа могла генерировать трассировку стека в случае сбоя, и в следующий раз, когда пользователь запустит ее, он спросит, можно ли отправить мне трассировку стека, чтобы я мог отследить проблему , Я могу обработать отправку информации мне, но я не знаю, как генерировать строку трассировки. Есть идеи?

Ответы [ 29 ]

463 голосов
/ 17 сентября 2008

Для Linux, и я считаю, что Mac OS X, если вы используете gcc или любой другой компилятор, использующий glibc, вы можете использовать функции backtrace () в execinfo.h, чтобы распечатать трассировку стека и корректно завершить работу, когда вы получите сегментацию неисправность. Документацию можно найти в руководстве по libc .

Вот пример программы, которая устанавливает обработчик SIGSEGV и печатает трассировку стека в stderr, когда он вызывает ошибки. Функция baz() здесь вызывает segfault, который запускает обработчик:

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

Компиляция с -g -rdynamic возвращает информацию о символах в выводе, которую glibc может использовать для создания хорошей трассировки стека:

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

Выполнение этого дает вам этот вывод:

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

Здесь показаны модуль загрузки, смещение и функция, из которой получен каждый кадр в стеке. Здесь вы можете увидеть обработчик сигналов на вершине стека и функции libc до main в дополнение к main, foo, bar и baz.

118 голосов
/ 18 декабря 2009

Linux

Хотя использование функций backtrace () в execinfo.h для печати трассировки стека и корректного выхода при получении ошибки сегментации уже предложило , я не вижу упоминаний о тонкостях, необходимых для обеспечения результирующая обратная трассировка указывает на фактическое местоположение ошибки (по крайней мере, для некоторых архитектур - x86 и ARM).

Первые две записи в цепочке кадров стека, когда вы попадаете в обработчик сигнала, содержат адрес возврата внутри обработчика сигнала и один внутри sigaction () в libc. Кадр стека последней функции, вызванной до того, как сигнал (который является местоположением ошибки) будет потерян.

код

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

Выход

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]

Все опасности вызова функций backtrace () в обработчике сигналов все еще существуют и не должны игнорироваться, но я нахожу описанную здесь функциональность весьма полезной при отладке сбоев.

Важно отметить, что приведенный мной пример разработан / протестирован на Linux для x86. Я также успешно реализовал это на ARM, используя uc_mcontext.arm_pc вместо uc_mcontext.eip.

Вот ссылка на статью, где я узнал подробности этой реализации: http://www.linuxjournal.com/article/6391

117 голосов
/ 06 июля 2011

Это даже проще, чем "man backtrace", есть немного документированная библиотека (специфичная для GNU), распространяемая с glibc как libSegFault.so, которая, как я считаю, была написана Ульрихом Дреппером для поддержки программы catchsegv (см. "Man catchsegv" ).

Это дает нам 3 возможности. Вместо запуска "программа -o хай":

  1. Запуск в catchsegv:

    $ catchsegv program -o hai
    
  2. Ссылка на libSegFault во время выполнения:

    $ LD_PRELOAD=/lib/libSegFault.so program -o hai
    
  3. Ссылка с libSegFault во время компиляции:

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

Во всех трех случаях вы получите более четкие трассировки с меньшим количеством символов оптимизации (gcc -O0 или -O1) и символов отладки (gcc -g). В противном случае вы можете просто получить кучу адресов памяти.

Вы также можете поймать больше сигналов для трассировки стека с помощью чего-то вроде:

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

Вывод будет выглядеть примерно так (обратите внимание на обратный след внизу):

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

Если вы хотите узнать подробности, лучшим источником, к сожалению, является источник: см. http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c и его родительский каталог http://sourceware.org/git/?p=glibc.git;a=tree;f=debug

80 голосов
/ 26 марта 2010

Даже если был предоставлен правильный ответ , который описывает, как использовать функцию GNU libc backtrace() 1 , и я предоставил мой собственный ответ , который описывает Как гарантировать, что обратная трассировка от обработчика сигнала указывает на фактическое местоположение ошибки 2 , я не вижу упоминания о деманглировании символов C ++, выводимых из обратной трассировки.

При получении трассировок из программы на C ++ вывод можно выполнить через c++filt 1 для разборки символов или с помощью abi::__cxa_demangle 1 непосредственно.

  • 1 Linux & OS X Обратите внимание, что c++filt и __cxa_demangle являются специфическими для GCC
  • 2 Linux

В следующем примере C ++ Linux используется тот же обработчик сигнала, что и в моем другом ответе , и демонстрируется, как c++filt может использоваться для разборки символов.

Код

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

Выход (./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]

Выход с разделением на полосы (./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]

Следующее основано на обработчике сигнала из моего оригинального ответа и может заменить обработчик сигнала в приведенном выше примере, чтобы продемонстрировать, как abi::__cxa_demangle может использоваться для разборки символов. Этот обработчик сигнала выдает тот же выходной сигнал, что и вышеприведенный пример.

Код

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);
}
33 голосов
/ 17 сентября 2008

Может быть стоит взглянуть на Google Breakpad , кросс-платформенный генератор аварийных дампов и инструменты для обработки дампов.

21 голосов
/ 17 сентября 2008

Вы не указали свою операционную систему, поэтому трудно ответить. Если вы используете систему, основанную на gnu libc, вы можете использовать функцию libc backtrace().

GCC также имеет две встроенные функции, которые могут помочь вам, но которые могут быть или не быть полностью реализованы в вашей архитектуре, а это __builtin_frame_address и __builtin_return_address. Оба из них хотят немедленного целочисленного уровня (под непосредственным я имею в виду, что он не может быть переменной). Если __builtin_frame_address для данного уровня не равен нулю, то можно безопасно получить адрес возврата того же уровня.

12 голосов
/ 17 сентября 2008

ulimit -c <value> устанавливает ограничение размера основного файла в Unix. По умолчанию ограничение размера основного файла равно 0. Вы можете просмотреть значения ulimit с помощью ulimit -a.

также, если вы запустите свою программу из GDB, она остановит вашу программу из-за «нарушений сегментации» (SIGSEGV, как правило, когда вы получили доступ к фрагменту памяти, который не был выделен) или вы можете установить точки останова.

DDD и Nemiver являются интерфейсом для GDB, что значительно облегчает работу с ним для новичка.

11 голосов
/ 04 апреля 2013

Спасибо энтузиасту за то, что обратили мое внимание на утилиту addr2line.

Я написал быстрый и грязный скрипт для обработки вывода ответа, предоставленного здесь : (большое спасибо jschmier!) с помощью утилиты addr2line.

Сценарий принимает один аргумент: имя файла, содержащего выходные данные утилиты jschmier.

Вывод должен напечатать что-то вроде следующего для каждого уровня трассировки:

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;

Код:

#!/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 
10 голосов
/ 18 сентября 2008

Я уже давно смотрю на эту проблему.

И глубоко закопан в Google Performance Tools README

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

говорит о libunwind

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

Хотелось бы услышать мнение этой библиотеки.

Проблема с -rdynamic заключается в том, что в некоторых случаях она может значительно увеличить размер двоичного файла

10 голосов
/ 17 сентября 2008

Важно отметить, что после того, как вы сгенерируете файл ядра, вам нужно использовать инструмент gdb для его просмотра. Чтобы gdb имел смысл в вашем основном файле, вы должны указать gcc инструменту двоичный файл с отладочными символами: для этого вы компилируете с флагом -g:

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

Затем вы можете либо установить "ulimit -c unlimited", чтобы он выгружал ядро, либо просто запустить вашу программу внутри gdb. Мне больше нравится второй подход:

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

Надеюсь, это поможет.

...