Как автоматически генерировать трассировку стека при сбое программы
Я разрабатываю программу на C++ под Linux с использованием компилятора GCC. В случае сбоя моей программы мне хотелось бы, чтобы она автоматически генерировала стек вызовов.
Моя программа запускается различными пользователями и работает на Linux, Windows и Macintosh (все версии компилируются с помощью gcc
).
Я хочу, чтобы при сбое программа могла сгенерировать стек вызовов, а при следующем запуске спросила у пользователя, согласен ли он отправить стек вызовов мне для отслеживания проблемы. Я могу обрабатывать отправку информации, но не знаю, как сгенерировать строку с трассировкой. Есть идеи?
5 ответ(ов)
Это даже проще, чем "man backtrace". Существует слабо документированная библиотека (специфичная для GNU), распределяемая с glibc под именем libSegFault.so, которая, как я полагаю, была написана Ульрихом Дреппером для поддержки программы catchsegv (см. "man catchsegv").
Это дает нам 3 возможности. Вместо того чтобы запускать программу с параметром "-o hai":
Запустить с помощью catchsegv:
$ catchsegv program -o hai
Использовать связывание с libSegFault во время выполнения:
$ LD_PRELOAD=/lib/libSegFault.so program -o hai
Использовать связывание с 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.
Вы не указали операционную систему, поэтому ответить на ваш вопрос довольно сложно. Если вы используете систему, основанную на GNU libc, то можете попробовать использовать функцию backtrace()
из libc.
Также в GCC есть две встроенные функции, которые могут помочь вам, но их реализация может быть неполной для вашей архитектуры. Это __builtin_frame_address
и __builtin_return_address
. Оба требуют немедленное целое число в качестве уровня (под немедленным я имею в виду, что это не может быть переменная). Если __builtin_frame_address
для заданного уровня ненулевое, то можно безопасно получить адрес возврата на этом же уровне.
Важно отметить, что после генерации core-файла вам потребуется использовать инструмент gdb для его анализа. Чтобы gdb смог корректно интерпретировать ваш core-файл, необходимо скомпилировать бинарный файл с отладочными символами. Для этого используйте флаг -g при компиляции:
$ g++ -g prog.cpp -o prog
Затем вы можете либо установить "ulimit -c unlimited", чтобы разрешить создание core-файлов, либо просто запустить вашу программу внутри gdb. Лично мне больше нравится второй подход:
$ gdb ./prog
... вывод при старте gdb ...
(gdb) run
... программа запускается и выдает сбой ...
(gdb) where
... gdb выводит ваш стек вызовов ...
Надеюсь, это поможет!
ulimit -c <value>
устанавливает лимит на размер файла ядра в Unix. По умолчанию лимит на размер файла ядра равен 0. Вы можете увидеть свои значения ulimit
, выполнив команду ulimit -a
.
Если вы запускаете свою программу в gdb, он остановит выполнение вашей программы при "нарушениях сегментации" (SIGSEGV
, что обычно происходит, когда вы обращаетесь к участку памяти, который не был выделен) или вы можете установить точки прерывания.
ddd и nemiver - это интерфейсы для gdb, которые значительно упрощают работу с ним для новичков.
Вы можете использовать библиотеку backward-cpp
для получения стека вызовов в своем коде, что делает ее отличным инструментом для отладки. Для начала вам нужно всего лишь подключить один заголовочный файл и установить одну библиотеку:
- Скачайте и подключите заголовочный файл
backward.hpp
. - Установите саму библиотеку
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
.
В чём разница между g++ и gcc?
Как вывести список символов из .so файла?
Отладка против Релиза в CMake
Неопределенная ссылка на виртуальную таблицу (vtable)
Программа, скомпилированная с флагом -g в gcc, работает медленнее, чем та же программа, скомпилированная без -g?