7

Как автоматически генерировать трассировку стека при сбое программы

27

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

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

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

5 ответ(ов)

1

Это даже проще, чем "man backtrace". Существует слабо документированная библиотека (специфичная для 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
    

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

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

$ export SEGFAULT_SIGNALS="all"       # "все" сигналы
$ export SEGFAULT_SIGNALS="bus abrt"   # SIGBUS и 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

Трассировка стека: /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.

0

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

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

0

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

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

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

$ gdb ./prog
... вывод при старте gdb ...
(gdb) run
... программа запускается и выдает сбой ...
(gdb) where
... gdb выводит ваш стек вызовов ...

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

0

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

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

ddd и nemiver - это интерфейсы для gdb, которые значительно упрощают работу с ним для новичков.

0

Вы можете использовать библиотеку backward-cpp для получения стека вызовов в своем коде, что делает ее отличным инструментом для отладки. Для начала вам нужно всего лишь подключить один заголовочный файл и установить одну библиотеку:

  1. Скачайте и подключите заголовочный файл backward.hpp.
  2. Установите саму библиотеку backward-cpp через пакетный менеджер или соберите её из исходников.

Вот пример, как вы можете вызвать функцию, выводящую стек вызовов:

#include "backward.hpp"

void stacker() {
    using namespace backward;
    StackTrace st;

    st.load_here(99); // Установите максимальную глубину стека в 99
    st.skip_n_firsts(3); // Пропустите первые три внутренние функции библиотеки

    Printer p;
    p.snippet = true; // Включите вывод фрагментов кода
    p.object = true;  // Включите вывод информации об объектах
    p.color = true;   // Включите цветной вывод
    p.address = true; // Включите вывод адресов

    p.print(st, stderr); // Выводим стек на стандартный поток ошибок
}

Этот код создает стек вызовов и пропускает ненужные внутренние функции, что позволяет сфокусироваться на важной информации. Библиотека также поддерживает различные опции форматирования вывода, как вы можете заметить из параметров Printer.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь