Как автоматически получить стек вызовов (stacktrace) при падении программы на C++?
Решение: обработка сигналов + 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).
- Не показывает номера