Як автоматично генерувати стек-трек при збої програми


590

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

Мою програму веде багато різних користувачів, а також вона працює на Linux, Windows та Macintosh (всі версії компілюються за допомогою gcc).

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


4
backtrace та backtrace_symbols_fd не є безпечними для асинхронізації. не слід використовувати ці функції в
обробці

10
backtrace_symbols викликає malloc, тому він не повинен використовуватися в обробці сигналів. Інші дві функції (backtrace та backtrace_symbols_fd) не мають цієї проблеми, і вони зазвичай використовуються в обробниках сигналів.
cmccabe

3
@cmccabe, що неправильно backtrace_symbols_fd, як правило, не називає malloc, але може, якщо щось піде не так у його блоці catch_error
Сем Сафрон

6
Це "може" в тому сенсі, що немає специфікації POSIX для backtrace_symbols_fd (або будь-якого зворотного сліду); однак Gtrack_symbols_fd GNU / Linux вказано, щоб ніколи не викликати malloc, відповідно до linux.die.net/man/3/backtrace_symbols_fd . Тому можна впевнено припустити, що він ніколи не викличе malloc в Linux.
кодетаку

Як воно виходить з ладу?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Відповіді:


509

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

Ось приклад програми, яка встановлює SIGSEGVобробник і друкує стек-трек, stderrколи він переходить у segfault . Тут baz()функція викликає сегментацію, яка запускає обробник:

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


53
Також є /lib/libSegFault.so, який ви можете використовувати з LD_PRELOAD.
CesarB

6
Виглядає так, що перші два записи у вашому зворотному виході містять зворотну адресу всередині обробника сигналу і, ймовірно, одну внутрішню sigaction()в libc. Незважаючи на те, що ваш зворотний шлях видається правильним, я іноді виявляв, що необхідні додаткові кроки для того, щоб фактичне розташування несправності з’явилося в backtrace, оскільки воно може бути замінено sigaction()ядром.
jschmier

9
Що станеться, якщо аварія прийде зсередини malloc? Ви б не тримали замок, а потім застрягли, коли "backtrace" намагається виділити пам'ять?
Маттіас Нільссон

7
catchsegvне те, що потрібно ОП, але є приголомшливим для виявлення недоліків сегментації та отримання всієї інформації.
Метт Кларксон

8
Для ARM мені довелося також компілювати з -funwind-table. Інакше глибина моєї стеки завжди була 1 (порожня).
jfritz42

128

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

Це дає нам 3 можливості. Замість запуску "програма -o hai":

  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

У всіх 3 випадках ви отримаєте чіткіші зворотні звороти з меншою оптимізацією (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


1
"Можливість 3. Посилання з libSegFault під час компіляції" не працює.
HHK

5
@crafter: Що означає "не працює". Що ви спробували, на якій мові / компілятор / ланцюжок інструментів / дистрибутив / обладнання? Не вдалося її скласти? Щоб виявити помилку? Для виробництва взагалі? Щоб отримати важкий у використанні вихід? Дякую за деталі, що допоможе всім.
Стефан Гурішон

1
"Найкраще джерело, на жаль, джерело" ... Сподіваємось, колись сторінка "mansegv" фактично згадає "SEGFAULT_SIGNALS". До цього часу є така відповідь, на яку слід посилатися.
greggo

Я не можу повірити, що я програмував C протягом 5 років і ніколи про це не чув: /
DavidMFrey

6
@ StéphaneGourichon @HansKratz Щоб зв’язатися з libSegFault, вам доведеться додати -Wl,--no-as-neededпрапори компілятора. В іншому випадку ldдійсно не буде посилання на libSegFault, тому що він визнає, що двійковий файл не використовує жодного зі своїх символів.
Філіпп

122

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


11
У системах, що використовують GNU ld, не забудьте компілювати, -rdynamicщоб доручити лінкеру додавати всі символи, не тільки використані, до таблиці динамічних символів. Це дозволяє backtrace_symbols()конвертувати адреси в імена функцій
jschmier

1
Крім того, вам потрібно додати параметр "-mapcs-frame" до командного рядка GCC ', щоб генерувати кадри стека на платформі ARM
qehgt

3
Це може бути пізно, але чи можемо ми використовувати addr2lineкоманду якось, щоб дістати точну лінію, де стався збій?
Энтузиазм

4
У останніх збірниках glibc uc_mcontextне міститься поля з назвою eip. Зараз є масив, який потрібно індексувати, uc_mcontext.gregs[REG_EIP]це еквівалент.
mmlb

6
Для ARM у моїх відстань завжди була глибина 1, поки я не додав до компілятора опцію -funwind-table.
jfritz42

84

Навіть при тому , що правильна відповідь було передбачено , що описує , як використовувати GNU Libc backtrace()функції 1 і я надав свою відповідь , який описує , як забезпечити трасування з обробника сигналу вказує на фактичне місцезнаходження несправності 2 , я не бачу будь-яка згадка про демонтажу символів C ++, що виводяться із задніх слідів .

При отриманні зворотних зв'язків з програми C ++, вихід може бути пропущений через c++filt1 для демонтажу символів або використання 1 безпосередньо.abi::__cxa_demangle

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

1
Дякую за це, jschmier. Я створив невеликий сценарій bash, щоб подати висновок цього в утиліту addr2line. Дивіться:
stackoverflow.com/a/15801966/1797414

4
Не забувайте #include <cxxabi.h>
Бамако

1
Хороша документація та прямий файл заголовка розміщений тут з 2008 року ... panthema.net/2008/0901-stacktrace-зазначений дуже схожим на ваш підхід :)
kevinf

abi :: __ cxa_demangle, здається, не є безпечним для асинхронного сигналу, тому обробник сигналу може зайти в тупик десь у malloc.
orcy

Використання std::cerr, free()і exit()все порушують обмеження в відношенні Телефон не Асінхри-сигнал безпечних викликів на системах POSIX. Цей код буде тупиковим , якщо ваш процес зазнає невдачі в будь-якому виклику , такі як free(), malloc() newабо detete.
Ендрю Генле

31

Можливо, варто поглянути на Google Breakpad , генератор звалищних платформ для краху платформи та інструменти для обробки сміттєзвалищ.


Він повідомляє про такі речі, як помилки сегментації, але не повідомляє жодної інформації про необроблені винятки C ++.
DBedrenko

21

Ви не вказали свою операційну систему, тому на це важко відповісти. Якщо ви використовуєте систему, засновану на gnu libc, можливо, ви зможете використовувати функцію libc backtrace().

У GCC також є два вбудовані елементи, які можуть допомогти вам, але які можуть бути, а можуть і не бути повністю застосовані у вашій архітектурі, і це є __builtin_frame_addressі __builtin_return_address. Обидва хочуть негайного цілого рівня (під негайним, я маю на увазі, що це не може бути змінною). Якщо __builtin_frame_addressдля даного рівня не дорівнює нулю, потрібно захопити зворотну адресу того ж рівня.


13

Дякую ентузіазму за те, що звернули мою увагу на утиліту 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 

12

ulimit -c <value>встановлює обмеження розміру основного файлу на unix. За замовчуванням обмеження розміру основного файлу становить 0. Ви можете бачити свої ulimitзначення ulimit -a.

Крім того, якщо ви запускаєте свою програму в межах gdb, вона зупинить вашу програму на "порушення сегментації" ( SIGSEGVяк правило, коли ви отримали доступ до фрагменту пам'яті, який ви не виділили), або ви зможете встановити точки проходу.

ddd і nemiver - це передні частини для gdb, які значно полегшують роботу з початківцем.


6
Основні скиди нескінченно корисніші, ніж сліди стека, тому що ви можете завантажити основний дамп у відладчику і побачити стан всієї програми та її даних у місці аварії.
Адам Хоуз

1
Обмеження, яке запропонували інші, є, мабуть, кращим, ніж нічого, але воно є дуже простим - воно навіть не дає номерів рядків. З іншого боку, використовуючи основні демпфери, ми дозволимо вам заднім числом переглянути весь стан вашої програми під час її виходу з ладу (включаючи докладний слід стека). Там може бути практичні проблеми зі спробою використовувати це для налагодження поля, але це, безумовно , більш потужний інструмент для аналізу аварій і стверджує в процесі розробки (принаймні на Linux).
nobar

10

Важливо зазначити, що коли ви генеруєте основний файл, вам потрібно буде використовувати інструмент 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 ...

Я сподіваюся, що це допомагає.


4
Ви також можете зателефонувати gdbпрямо з вашої програми збоїв. Інструмент налаштування для SIGSEGV, SEGILL, SIGBUS, SIGFPE, який буде викликати gdb. Детальніше: stackoverflow.com/questions/3151779/… Перевага полягає в тому, що ви отримуєте красиві, анотовані зворотні кадри, як у bt full, а також ви можете отримати сліди стеки всіх потоків.
Ві.

Ви також можете отримати зворотний трек простіше, ніж у відповіді: gdb -silent ./prog core --eval-command = backtrace --batch - це покаже
зворотний шлях

10

Я деякий час дивився на цю проблему.

І похований глибоко в інструментах Google Performance Performance README

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

розповідає про вільний вітер

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

Мені хотілося б почути думки цієї бібліотеки.

Проблема з -рдинамікою полягає в тому, що він може порівняно значно збільшити розмір двійкового файлу в деяких випадках


2
У x86 / 64 я не бачив -рдинамічного збільшення бінарного розміру сильно. Додавання -g значно збільшує.
День

1
Я помітив, що libunwind не має функціональності для отримання номера рядка, і я здогадуюсь (не перевіряв) un_get_proc_name повертає символ функції (який затуманений для перевантаження та таке) замість оригінальної назви.
Герберт

1
Це правильно. Це дуже складно зробити це правильно, але я мав відмінний успіх з gaddr2line, тут є багато практичної інформації blog.bigpixel.ro/2010/09/stack-unwinding-stack-trace-with-gcc
Григорій


9

Ви можете використовувати DeathHandler - невеликий клас C ++, який робить все за вас, надійно.


1
на жаль, він використовує execlp()для виконання addr2line дзвінків ... було б непогано повністю залишитися у власній програмі (що можливо, включивши код addr2line в якусь форму)
наприклад,

9

Забудьте про зміну джерел і зробіть деякі хаки з функцією backtrace () або макрозами - це лише погані рішення.

Як правильно працююче рішення, я б порадив:

  1. Складіть програму зі знаком "-g" для вбудовування символів налагодження у бінарний (не хвилюйтесь, це не вплине на вашу ефективність).
  2. На Linux запустіть наступну команду: "ulimit -c unlimited" - щоб система могла робити великі скиди на аварію.
  3. Коли ваша програма вийшла з ладу, у робочому каталозі ви побачите файл "core".
  4. Виконайте наступну команду для друку backtrace до stdout: gdb -batch -ex "backtrace" ./your_program_exe ./core

Це дозволить надрукувати належну читабельну зворотній зв'язок вашої програми з легким для читання способом (із назвами вихідних файлів та номерами рядків). Більше того, такий підхід дасть вам свободу для автоматизації вашої системи: мати короткий сценарій, який перевіряє, чи процес створив основний дамп, а потім надсилає зворотні повідомлення електронною поштою розробникам, або вписуйте це в якусь систему реєстрації.


Він дає неправильні номери рядків. Чи можна її покращити?
HeyJude

7
ulimit -c unlimited

є системною змінною, що дозволить створити основний дамп після збоїв у вашій програмі. У цьому випадку необмежена кількість. Шукайте файл під назвою core в тій самій каталозі. Переконайтеся, що ви склали свій код із ввімкненою інформацією про налагодження!

з повагою


5
Користувач не запитує основний дамп. Він просить простежити стек. Дивіться delorie.com/gnu/docs/glibc/libc_665.html
Todd Gamblin

1
основний дамп буде містити стек виклику в момент аварії, чи не так?
Пн.

3
Ви припускаєте, що він на Unix і використовує Bash.
Пол Томблін

2
Якщо ви використовуєте tcsh, ви повинні зробитиlimit coredumpsize unlimited
sivabudh


6

Дивіться інструмент «Слідування слідів» у ACE (ADAPTIVE Environment Environment). Це вже написано, щоб охопити всі основні платформи (і більше). Бібліотека має BSD-ліцензію, тому ви навіть можете скопіювати / вставити код, якщо не хочете використовувати ACE.


Здається, посилання мертва.
tglas

5

Я можу допомогти з версією Linux: можна використовувати функції backtrace, backtrace_symbols та backtrace_symbols_fd. Дивіться відповідні сторінки керівництва.


5

Схоже, в одній з останніх збільшилася версія c ++ з'явилася бібліотека, щоб забезпечити саме те, що ви хочете, ймовірно, код буде багатоплатформенним. Це boost :: stacktrace , який ви можете використовувати як у прикладі 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
}

У Linux Ви складаєте код вище:

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

Приклад трасування копіюється з бустерной документації :

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: ви можете перехопити SIGSEGV (зазвичай цей сигнал піднімається перед збоєм) і зберігати інформацію у файлі. (крім основного файлу, який ви можете використовувати для налагодження, наприклад, за допомогою gdb).

win: Перевірте це у msdn.

Ви також можете подивитися на хромований код google, щоб побачити, як він обробляє збої. Він має приємний механізм обробки винятків.


SEH не допомагає виробляти стек стеження. Хоча це може бути частиною рішення, це рішення важче реалізувати та надає менше інформації за рахунок розкриття більше інформації про вашу програму, ніж реального рішення: Напишіть міні-дамп. І налаштуйте Windows, щоб це зробити автоматично для вас.
IIнеочікуваний

4

Я виявив, що рішення @tgamblin не є повним. Він не може впоратися з stackoverflow. Я думаю, тому що за замовчуванням обробник сигналу викликається з однаковим стеком і SIGSEGV кидається двічі. Для захисту потрібно зареєструвати незалежну стек для обробника сигналу.

Ви можете перевірити це за допомогою коду нижче. За замовчуванням обробник не працює. З визначеним макросом STACK_OVERFLOW все в порядку.

#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

Новий король у місто прибув https://github.com/bombela/backward-cpp

1 заголовок для розміщення у вашому коді та 1 бібліотека для встановлення.

Особисто я називаю це за допомогою цієї функції

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

Оце Так! Ось нарешті, як це треба зробити! Я щойно відмовився від власного рішення на користь цього.
tglas

3

Я використовував би код, який генерує стек стеження для просоченої пам’яті у візуальному детекторі витоку . Це працює лише на Win32.


І вимагає, щоб ви відправляли символи налагодження разом із кодом. Взагалі не бажано. Напишіть міні-дамп і налаштуйте Windows, щоб він це робив автоматично для вас за непереробленими винятками.
Неочікуваний

3

Я бачив тут багато відповідей, коли виконував обробку сигналу і потім виходив. Це шлях, але пам’ятайте дуже важливий факт: якщо ви хочете отримати основний дамп для генерованої помилки, ви не можете зателефонувати exit(status). Дзвінокabort() замість цього!


3

Як єдине рішення для Windows, ви можете отримати еквівалент сліду стека (з набагато більшою кількістю інформації) за допомогою Windows Report Error Reporting . За допомогою лише кількох записів реєстру, його можна налаштувати для збору дампів у режимі користувача :

Починаючи з Windows Server 2008 та Windows Vista з пакетом оновлень 1 (SP1), звіт про помилки Windows (WER) можна налаштувати так, щоб повноцінні звалища в режимі користувача збиралися та зберігалися локально після збоїв програми в режимі користувача. [...]

Ця функція за замовчуванням не включена. Увімкнення функції вимагає права адміністратора. Щоб увімкнути та налаштувати цю функцію, використовуйте такі значення реєстру під клавішею HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows \ Windows Error Reporting \ LocalDumps .

Ви можете встановити записи реєстру від свого інсталятора, який має необхідні привілеї.

Створення дампа в режимі користувача має такі переваги перед генеруванням сліду стека на клієнті:

  • Це вже реалізовано в системі. Ви можете використовувати WER, як зазначено вище, або зателефонувати на MiniDumpWriteDump самостійно, якщо вам потрібен більш тонкий контроль над кількістю інформації для скидання. (Не забудьте зателефонувати йому з іншого процесу.)
  • Шлях більш повний, ніж слід стека. Серед інших він може містити локальні змінні, аргументи функцій, стеки для інших потоків, завантажені модулі тощо. Обсяг даних (а отже, і розмір) дуже настроюється.
  • Не потрібно надсилати налагоджувальні символи. Це різко зменшує розмір вашого розгортання, а також ускладнює реверсивну розробку вашої програми.
  • В значній мірі незалежний від компілятора, який ви використовуєте. Використання WER навіть не вимагає коду. У будь-якому випадку, спосіб отримання бази даних символів (PDB) дуже корисний для офлайн-аналізу. Я вважаю, що GCC може або генерувати PDB, або є інструменти для перетворення бази даних символів у формат PDB.

Візьміть до уваги, що WER може бути викликаний лише збоєм програми (тобто системою, що завершує процес через необроблений виняток). MiniDumpWriteDumpможна викликати в будь-який час. Це може бути корисно, якщо вам потрібно скинути поточний стан, щоб діагностувати інші проблеми, а не збій.

Обов'язкове читання, якщо ви хочете оцінити застосованість міні-смітників:


2

На додаток до вищенаведених відповідей, ось як змусити Debian Linux ОС генерувати основний дамп

  1. Створіть папку «coredumps» у домашній папці користувача
  2. Перейдіть на сторінку /etc/security/limits.conf. Внизу рядка '' введіть «м'яке ядро ​​необмежене» та «root soft core unlimited», якщо активізувати основні скидки для root, щоб забезпечити необмежений простір для скидів ядра.
  3. ПРИМІТКА: "* soft core unlimited" не охоплює корінь, через що корінь повинен бути вказаний у власному рядку.
  4. Щоб перевірити ці значення, вийдіть із системи, увійдіть назад та введіть “ulimit -a”. "Розмір основного файлу" повинен бути встановлений необмеженим.
  5. Перевірте .bashrc файли (користувач та root, якщо це можливо), щоб переконатися, що ulimit там не встановлено. В іншому випадку значення вище буде перезаписано при запуску.
  6. Відкрийте /etc/sysctl.conf. Внизу введіть наступне: “kernel.core_pattern = /home//coredumps/%e_%t.dump”. (% e буде ім'ям процесу, а% t - системним часом)
  7. Вийдіть і введіть “sysctl -p”, щоб завантажити нову конфігурацію Check / proc / sys / kernel / core_pattern і переконайтеся, що вона відповідає тому, що ви тільки що ввели.
  8. Демпінг ядра можна перевірити, запустивши процес у командному рядку ("&"), а потім знищивши його за допомогою "kill -11". Якщо демпінг ядра успішний, після індикації помилок сегментації ви побачите "(core dumped)".

2

Якщо ви все ще хочете піти на самоті, як я, ви можете зв’язатись bfdі уникати використанняaddr2line як я це зробив тут:

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

Це дає результат:

[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

У Linux / unix / MacOSX використовуйте основні файли (їх можна ввімкнути за допомогою ulimit або сумісного системного виклику ). У Windows використовуйте звіт про помилки Microsoft (ви можете стати партнером та отримати доступ до даних про аварійне завершення роботи програми).


0

Я забув про технологію GNOME "apport", але не знаю багато про те, як її використовувати. Він використовується для генерації стек-траксів та іншої діагностики для обробки та може автоматично подавати помилки. Це, безумовно, варто зареєструватися.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.