Segfault (SIGSEGV) на бою почти всегда выглядит как «случайность»: сервис то живёт, то падает, в логах одни адреса, а systemd успевает перезапустить процесс быстрее, чем вы откроете терминал. Хорошая новость в том, что для первичной диагностики обычно достаточно правильно собрать core dump и один раз снять осмысленный backtrace.
Что такое segfault и почему в production он выглядит «рандомным»
Segfault — сигнал ядра, когда процесс обращается к недоступной области памяти: разыменовал битый указатель, вышел за границы массива, попытался выполнить код по неисполняемому адресу и т.д.
В production это часто воспринимается как «рандом» по нескольким причинам:
- сборка без символов: в стеке вызовов много
??, нет файлов и строк; - ASLR меняет адреса между запусками, из-за чего «адреса в логах» не похожи друг на друга;
- падение происходит в редком пути: гонки, use-after-free, переполнение, плагины;
- сервис попадает в crash loop: systemd перезапускает его снова и снова, и вы теряете контекст.
Цель статьи — быстро превратить «segfault в логе» в диагностируемый кейс: core dump → coredumpctl → gdb → thread apply all bt full → debuginfo → при необходимости addr2line.
Быстрый чек-лист на инцидент (когда сервис в crash loop)
Когда сервис падает и перезапускается, действуйте по короткой схеме, которая обычно даёт результат за 5–10 минут:
- Зафиксируйте время падения, имя юнита, PID/UID из журналов.
- Проверьте, что core dumps реально разрешены (лимиты shell и лимиты systemd-юнита).
- Найдите запись в
coredumpctlи сразу выгрузите core в файл (на случай ротации). - Откройте core в
gdb, снимите стек всех потоков, затем полный стек с локальными переменными. - Подтяните
debuginfoи повторите backtrace, чтобы получить функции/строки. - Если остались «голые» адреса, сопоставьте их через
addr2lineдля нужного ELF.

Шаг 1. Убедиться, что core dumps включены: ulimit и лимиты systemd
Самая частая причина «нет core» — их запрещает лимит. Для текущей сессии проверьте:
ulimit -c
Если видите 0, core dumps выключены. Для интерактивного запуска можно временно включить:
ulimit -c unlimited
Но на бою сервисы обычно стартуют через systemd, и ваш ulimit в SSH-сессии на них не влияет. Посмотрите эффективное значение для юнита:
systemctl show -p LimitCORE your-service.service
Если нужно включить core dumps для конкретного сервиса, сделайте drop-in override:
systemctl edit your-service.service
Добавьте:
[Service]
LimitCORE=infinity
Примените и перезапустите:
systemctl daemon-reload
systemctl restart your-service.service
Если сервис падает слишком быстро, сначала имеет смысл увеличить паузу между рестартами (см. ниже про crash loop), чтобы успевать снять артефакты и не забить диски core dump’ами.
Шаг 2. Куда попадает core: core_pattern и systemd-coredump
Маршрут core dump определяет /proc/sys/kernel/core_pattern. Посмотрите текущее значение:
cat /proc/sys/kernel/core_pattern
Есть два типовых сценария:
- ядро пишет core напрямую в файл (например,
coreили путь вида/var/coredumps/core.%e.%p); - core отправляется через pipe обработчику (значение начинается с
|) — чаще всего этоsystemd-coredump.
На современных дистрибутивах обычно включён systemd-coredump: core хранится в управляемом хранилище и извлекается через coredumpctl.
Если у вас pipe в
systemd-coredump, не ищите файлcoreв рабочем каталоге сервиса: его может не быть. Используйтеcoredumpctl.
Проверка, что systemd-coredump работает и не режет core
Проверьте состояние обработчика (название может отличаться в зависимости от дистрибутива):
systemctl status systemd-coredump
Если core большие или их много, проверьте настройки в /etc/systemd/coredump.conf и drop-in каталоге /etc/systemd/coredump.conf.d/. На практике чаще всего влияют Storage, ProcessSizeMax, ExternalSizeMax и MaxUse.
Шаг 3. Найти падение через coredumpctl и выгрузить core
Посмотреть последние падения:
coredumpctl list
Отфильтровать по юниту:
coredumpctl list --unit your-service.service
Отфильтровать по имени бинаря:
coredumpctl list your-binary-name
Когда нашли нужную запись (ориентируйтесь по времени, PID, UID), получите подробности:
coredumpctl info PID
Чтобы не зависеть от ротации и лимитов хранения, выгрузите core в файл:
coredumpctl dump PID --output=/root/core.your-service.PID
И отдельно сохраните метаданные (потом это сильно экономит время):
coredumpctl info PID > /root/core.your-service.PID.info.txt
Шаг 4. Открыть core в gdb и снять thread apply all bt full
Самый удобный путь — запуск gdb через coredumpctl: он сам подхватит правильный executable и параметры:
coredumpctl gdb PID
Внутри gdb начните с базового набора:
set pagination off
thread apply all bt
thread apply all bt full
thread apply all btпоказывает стек по всем потокам (важно для гонок, пулов, фоновых воркеров).thread apply all bt fullдополнительно печатает аргументы и локальные переменные, где часто видно «битые» значения.
Если backtrace бедный (много ??, нет файлов/строк), это почти всегда отсутствие символов. Дальше нужен debuginfo.
Шаг 5. Debuginfo: как получить имена функций и номера строк
debuginfo (debug symbols) — отдельные пакеты/артефакты с DWARF-символами для бинарей и библиотек. На бою их часто нет (место, политика, скорость обновлений), поэтому есть два рабочих подхода:
- временно поставить debuginfo на production-сервер (если политика позволяет);
- перенести core + бинарь + нужные библиотеки на отдельную debug-машину с теми же версиями пакетов.
Debian/Ubuntu: пакеты *-dbgsym
На Debian/Ubuntu символы обычно в пакетах вида *-dbgsym (иногда *-dbg). Критично, чтобы версии debuginfo точно совпадали с версиями бинаря и библиотек, загруженных в core.
Минимальный набор, который часто нужен для осмысленного стека:
- ваше приложение (если пакетировано);
- glibc;
- libstdc++ (для C++);
- любые библиотеки, которые фигурируют в backtrace (crypto, ssl, драйверы, плагины).
После установки повторите coredumpctl gdb и thread apply all bt full: разница обычно радикальная.
RHEL/Alma/Rocky/CentOS: debuginfo
В RPM-мире символы обычно в пакетах debuginfo. Старайтесь ставить их точечно: для пакетов, которые реально участвуют в стеке. Так вы быстрее получите результат и не раздуете систему лишними пакетами.
Если приложение собрано вами: храните symbols по build-id
Для собственных C/C++/Rust/Go-бинарей лучше заранее выстроить процесс:
- в CI сохранять symbol files (DWARF) отдельно от production-артефакта;
- в production выкатывать «strip»-версию;
- хранить символы столько же, сколько вы храните релизы и core dumps.
Это превращает «падение в проде» из лотереи в стандартную процедуру.

Шаг 6. Addr2line: когда у вас есть только адреса
Иногда даже с gdb остаются адреса (например, часть стека в stripped-библиотеке без символов или у вас есть только адрес из мониторинга). Тогда помогает addr2line.
Рабочий алгоритм:
- Понять, к какому модулю относится адрес (основной ELF или конкретная
.so). Вgdbпомогаютinfo filesиinfo proc mappings. - Взять адрес instruction pointer из фрейма, который выглядит как точка падения (на x86_64 обычно RIP).
- Прогнать адрес через
addr2lineс указанием правильного ELF и наличием символов.
Пример:
addr2line -e /usr/bin/your-binary -f -C 0x0000000000401234
-fпечатает имя функции;-Cдеманглит C++ символы;-eуказывает бинарь/библиотеку.
Шаг 7. Узнать класс причины по паттернам в backtrace
Один хороший backtrace ещё не всегда даёт корень проблемы, но обычно указывает класс ошибки и направление, куда копать.
Частые сценарии
- NULL dereference: обращение к адресу около
0x0, а вbt fullуказатели/аргументы равны нулю. - Use-after-free: падение «глубоко» в аллокаторе или при доступе к полям структуры; значения выглядят мусорными, стек может «плавать» между запусками.
- Stack overflow: очень глубокая рекурсия и повторяющиеся фреймы.
- ABI mismatch: символы есть, но параметры «невозможные»; часто появляется после обновления библиотек без пересборки модулей.
- Крэш в сторонней библиотеке: обязательно подтяните debuginfo именно для неё, иначе вы будете слепы на половине стека.
Шаг 8. Crash loop: как остановить лавину и не потерять данные
Crash loop опасен тем, что вы:
- быстро забиваете лимиты хранения core dumps;
- заспамливаете journald;
- создаёте лишнюю нагрузку на CPU/IO;
- можете усугубить состояние данных, если падение происходит в миграциях/инициализации.
Ограничить рестарты systemd на время расследования
Посмотрите текущее состояние и причины рестартов:
systemctl status your-service.service
Полезные параметры юнита: Restart, RestartSec, StartLimitIntervalSec, StartLimitBurst. На время диагностики иногда разумно увеличить RestartSec, чтобы успевать собирать контекст. Делайте это через drop-in, чтобы легко откатить.
Если у вас воркеры/демоны, которыми управляет systemd, может пригодиться материал про системный подход к управлению процессами: очереди и воркеры под systemd вместо supervisor.
Дисковая гигиена core dumps
Следите за лимитами в systemd-coredump и свободным местом. Отдельно проверьте, не режется ли размер core параметрами ProcessSizeMax и ExternalSizeMax. Обрезанный core может давать «странные» ошибки чтения памяти в gdb и ломать backtrace.
Шаг 9. Минимальный пакет артефактов, который стоит сохранять
Чтобы разбор segfault не превращался в археологию, стандартизируйте набор артефактов на инцидент:
- core dump (файл или стабильный идентификатор в
coredumpctl); - точная версия бинаря (build-id, хэш, версия пакета);
- версии ключевых библиотек, которые участвуют в стеке;
- вывод
thread apply all bt full; - конфиг лимитов и маршрутизации:
LimitCORE, значениеcore_pattern, настройкиsystemd-coredump.
Если вы разворачиваете сервисы на VDS, удобно держать отдельную «debug-ноду» с теми же версиями ОС/пакетов: туда можно безопасно переносить core и разбирать падения, не трогая production-пакеты и политики.
Частые проблемы и быстрые ответы
coredumpctl list пустой, но segfault точно был
Проверьте последовательно:
- лимит core для systemd-юнита:
systemctl show -p LimitCORE your-service.service; /proc/sys/kernel/core_pattern(куда вообще пишется core);- что хранение не выключено:
Storage=noneвcoredump.conf; - лимиты размера:
ProcessSizeMax,ExternalSizeMax; - не потеряли ли вы падение из-за ротации (поэтому dump в файл лучше делать сразу).
В gdb везде ?? и нет строк
Почти всегда это отсутствие debuginfo или несоответствие версий. Символы должны быть именно для тех версий бинаря и библиотек, которые загружены в core.
Backtrace есть, но выглядит бессмысленно
Чаще всего помогает:
- снять стек всех потоков:
thread apply all bt full; - посмотреть регистры и текущий фрейм:
info registers,frame 0; - проверить признаки повреждения стека (stack smashing);
- воспроизвести в staging под ASan/UBSan (для C/C++) для точного места ошибки.
Итог
Segfault в production лечится не «магией», а дисциплиной: core dumps включены и не обрезаются, маршрут хранения понятен (core_pattern и systemd-coredump), core быстро находится через coredumpctl, а backtrace становится читаемым после установки debuginfo. Если добавить к этому хранение символов в CI по build-id и шаблон сбора артефактов на инцидент, даже crash loop превращается в задачу с конкретными функциями, строками и проверяемыми гипотезами.


