Avatar ·

Как автоматически получить стек вызовов (stacktrace) при падении программы на C++?

Привет всем! Я пишу на C++ под Linux с компилятором GCC, и у меня постоянно возникают проблемы с отладкой трудноуловимых багов. Программа падает с segmentation fault, но я не могу понять, где именно это происходит, потому что падение случается редко и в случайных местах. Я уже использую assert'ы, но они не помогают, когда баг связан с повреждением памяти или переполнением буфера. Я хочу, чтобы при любом краше (SIGSEGV, SIGABRT, SIGFPE и т.д.) программа автоматически выводила полный стек вызовов с именами функций, файлами и номерами строк. Вот пример моего кода, который падает: ```cpp #include #include void dangerous_function(int* ptr) { *ptr = 42; // Здесь может быть segfault, если ptr == nullptr } int main() { std::vector vec(10); int* bad_ptr = nullptr; dangerous_function(bad_ptr); // Авария! return 0; } ``` Я слышал про backtrace() и backtrace_symbols(), но они выдают только адреса, а не читаемые имена. Как сделать, чтобы при падении в консоль выводилось что-то вроде: ``` Stack trace (most recent call first): [1] dangerous_function(int*) at main.cpp:5 [2] main at main.cpp:10 ``` Нужно решение, которое работает с GCC и не требует сторонних библиотек (только стандартные средства Linux). Буду благодарен за готовый код! <<>>

Решение: обработка сигналов + backtrace + addr2line

Да, проблема знакомая. Стандартные backtrace() и backtrace_symbols() действительно выдают только нечитаемые адреса. Но есть проверенный способ получить красивые имена функций и номера строк — использовать утилиту addr2line из binutils. Мы перехватим фатальные сигналы, соберём стек адресов и преобразуем их в читаемый формат.

Полный код решения

#include <iostream>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <execinfo.h>
#include <unistd.h>
#include <fstream>
#include <sstream>

// Максимальная глубина стека
const int MAX_STACK_FRAMES = 64;

// Функция для преобразования адреса в читаемую строку через addr2line
std::string addr2line(void* addr, const char* program_name) {
    char cmd[512];
    snprintf(cmd, sizeof(cmd), "addr2line -f -e %s %p 2>/dev/null", program_name, addr);
    
    FILE* fp = popen(cmd, "r");
    if (!fp) return "??";
    
    char result[256] = {0};
    if (fgets(result, sizeof(result), fp)) {
        // Убираем символ новой строки
        size_t len = strlen(result);
        if (len > 0 && result[len-1] == '\n') result[len-1] = '\0';
    }
    pclose(fp);
    return result;
}

// Обработчик сигналов
void signal_handler(int sig) {
    // Восстанавливаем стандартное поведение, чтобы избежать рекурсии
    signal(sig, SIG_DFL);
    
    std::cerr << "\n=== CRASH DETECTED! Signal: " << sig << " (" 
              << strsignal(sig) << ") ===" << std::endl;
    
    // Получаем стек адресов
    void* buffer[MAX_STACK_FRAMES];
    int frames = backtrace(buffer, MAX_STACK_FRAMES);
    
    // Получаем имя программы для addr2line
    char exe_path[1024] = {0};
    ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path)-1);
    if (len != -1) exe_path[len] = '\0';
    
    std::cerr << "Stack trace (most recent call first):" << std::endl;
    
    // Пропускаем первые 2 фрейма (сигнальный обработчик и raise)
    for (int i = 2; i < frames; ++i) {
        std::string info = addr2line(buffer[i], exe_path);
        std::cerr << "  [" << (i-1) << "] " << info << std::endl;
    }
    
    // Завершаем процесс
    _exit(EXIT_FAILURE);
}

// Установка обработчиков для всех критических сигналов
void setup_signal_handlers() {
    signal(SIGSEGV, signal_handler);  // Segmentation fault
    signal(SIGABRT, signal_handler);  // Abort (assert)
    signal(SIGFPE,  signal_handler);  // Floating point exception
    signal(SIGILL,  signal_handler);  // Illegal instruction
    signal(SIGBUS,  signal_handler);  // Bus error
}

// Пример использования
void dangerous_function(int* ptr) {
    *ptr = 42;  // Тут будет segfault
}

int main() {
    setup_signal_handlers();
    
    std::cout << "Program started. Will crash soon..." << std::endl;
    
    std::vector<int> vec(10);
    int* bad_ptr = nullptr;
    dangerous_function(bad_ptr);  // Авария!
    
    return 0;
}

Компиляция и запуск

Важно! Нужно компилировать с отладочными символами и без оптимизаций, иначе адреса будут неточными:

g++ -g -O0 -rdynamic crash_demo.cpp -o crash_demo
./crash_demo

Флаг -rdynamic обязателен — он добавляет все символы в динамическую таблицу, чтобы addr2line мог их найти.

Как это работает

  • Перехват сигналов: Мы устанавливаем обработчики для SIGSEGV, SIGABRT, SIGFPE и других фатальных сигналов.
  • Сбор стека: Функция backtrace() собирает массив адресов возврата (как void*).
  • Преобразование адресов: Для каждого адреса мы запускаем addr2line с флагами -f (имя функции) и -e (исполняемый файл).
  • Вывод: Результат выводится в stderr, чтобы не смешиваться с обычным выводом программы.

Важные замечания

  • В обработчике сигналов нельзя использовать malloc, printf и другие нереентерабельные функции. addr2line запускается через popen — это безопасно, так как создаётся новый процесс.
  • Для production-сборок уберите -g, но тогда номера строк будут недоступны. Можно оставить только имена функций.
  • Если addr2line не установлен, установите пакет binutils: sudo apt install binutils.

Пример вывода

=== CRASH DETECTED! Signal: 11 (Segmentation fault) ===
Stack trace (most recent call first):
  [1] dangerous_function(int*) at crash_demo.cpp:53
  [2] main at crash_demo.cpp:59
  [3] __libc_start_main at ../csu/libc-start.c:308
  [4] _start at ??:0
<<>>

Альтернатива: использование libunwind (более производительно)

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

Установка:

sudo apt install libunwind-dev

Пример кода с libunwind:

#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <cstdio>

void print_stacktrace() {
    unw_cursor_t cursor;
    unw_context_t context;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    
    char buf[256];
    unw_word_t offp;
    
    while (unw_step(&cursor) > 0) {
        unw_get_proc_name(&cursor, buf, sizeof(buf), &offp);
        printf("%s+0x%lx\n", buf, offp);
    }
}

Преимущества:

  • Не требует внешних процессов — работает быстрее и безопаснее в обработчиках сигналов.
  • Может работать даже если addr2line недоступен.
  • Поддерживает извлечение имён функций напрямую из ELF-секций.

Недостатки:

  • Зависимость от сторонней библиотеки (хотя она часто предустановлена в Linux).
  • Не показывает номера