Как автоматически генерировать трассировку стека при сбое программы
Я разрабатываю программу на 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 файла?
Неопределенная ссылка на виртуальную таблицу (vtable)
Ошибка: версия `CXXABI_1.3.8` не найдена (требуется для ...)
Почему GCC вызывает sqrt() из libc, не используя его результат?