Санитайзеры в C++: рабочие приёмы отладки памяти и UB
AddressSanitizer, UBSanitizer и ThreadSanitizer ловят use-after-free, гонки и переполнение буфера в рантайме. Разбираем, как включить их в проекте и не сломать себе CI ложными и настоящими срабатываниями.

У вас падает тест раз в двадцать запусков, и стектрейс каждый раз разный. Обычный отладчик показывает мусор в переменной, которая двумя строками выше была валидной. Это почти всегда порча памяти или неопределённое поведение, и искать его глазами по коду можно неделями. Санитайзеры — набор инструментов в GCC и Clang, которые инструментируют программу при компиляции и ловят такие ошибки в момент, когда они происходят, с точным адресом и стеком.
Речь пойдёт не о том, что такое AddressSanitizer в теории, а о рабочих приёмах: какие флаги ставить, как гонять санитайзеры в CI, как подавлять чужие срабатывания из сторонних библиотек и почему нельзя включить все санитайзеры одновременно. Проверено на Clang 17–18 и GCC 13–14; синтаксис флагов у них совпадает, различия отмечу отдельно.
Четыре санитайзера и что они ловят
Санитайзеры — не один инструмент, а семейство. У каждого своя цель и своя цена по скорости и памяти. Смешивать их наугад нельзя: часть комбинаций несовместима.
| Флаг | Что ловит | Замедление | Память |
|---|---|---|---|
-fsanitize=address | use-after-free, выход за границы буфера, двойное освобождение, утечки (через LeakSanitizer) | ~2x | ~3x |
-fsanitize=undefined | переполнение знаковых целых, разыменование null, сдвиг за пределы разрядности, невыровненный доступ | ~1.2x | небольшая |
-fsanitize=thread | гонки данных между потоками | 5–15x | 5–10x |
-fsanitize=memory | чтение неинициализированной памяти (только Clang) | ~3x | ~2x |
Главное правило совместимости: ASan и TSan вместе не собираются — они по-разному управляют памятью. MSan (MemorySanitizer) требует, чтобы вся программа, включая стандартную библиотеку, была собрана с ним, иначе он тонет в ложных срабатываниях. ASan и UBSan, наоборот, дружат и обычно ставятся вместе.
Термин: инструментирование
Инструментирование — вставка компилятором дополнительного кода вокруг обращений к памяти и арифметических операций. Именно поэтому санитайзеры работают в рантайме и требуют пересборки: они не анализируют готовый бинарник, а встраиваются в него на этапе компиляции.
Базовый набор флагов, который стоит поставить сразу
Минимум для отладочной сборки — ASan плюс UBSan. Флаги нужны и на компиляцию, и на линковку, иначе получите ошибки о неопределённых символах __asan_*.
# CMake: отдельная конфигурация под санитайзеры
cmake -B build-asan \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build build-asanПро каждый флаг коротко:
-fno-omit-frame-pointer— без него стектрейсы в отчёте будут неполными или неверными;-g— отладочная информация нужна, чтобы в отчёте были имена файлов и номера строк, а не голые адреса;- оптимизацию
-O1обычно оставляют: с ней ASan работает заметно быстрее, чем на чистом-O0, а точность не страдает.
Запускать бинарник нужно так же, как обычно, — санитайзер уже внутри. При ошибке программа печатает отчёт и по умолчанию завершается ненулевым кодом.
Как читать отчёт AddressSanitizer
Отчёт длинный, но структура у него постоянная. Первая строка — тип ошибки: heap-use-after-free, heap-buffer-overflow, stack-buffer-overflow. Дальше три блока стеков: где произошло обращение, где память была освобождена, где она была выделена. Именно связка «освобождена здесь — обращение здесь» обычно и указывает на баг напрямую, без гадания.
Санитайзер не находит все ошибки в коде — он находит те, что реально сработали на вашем тесте. Ошибка на непройденной ветке останется невидимой. Поэтому санитайзеры без покрытия тестами дают ложное чувство безопасности.
Настройка поведения через переменные окружения
Поведение ASan управляется переменной ASAN_OPTIONS, UBSan — UBSAN_OPTIONS. Значения разделяются двоеточием. Самые полезные:
# Останавливаться на первой же ошибке UB, а не только логировать
export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1
# Проверять утечки при выходе и печатать полный лог
export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1
# Отключить проверку утечек, если она мешает (например, в короткоживущих тулзах)
export ASAN_OPTIONS=detect_leaks=0По умолчанию UBSan только печатает предупреждение и продолжает выполнение — из-за этого его срабатывания легко проглядеть в шумном логе. В CI обязательно ставьте halt_on_error=1, иначе сборка останется зелёной при наличии UB.
Подавление чужих срабатываний
Реальная боль — не ваш код, а сторонние библиотеки, которые срабатывают на UBSan или текут памятью по своей внутренней логике. Гасить их точечно, а не отключать санитайзер целиком.
Для утечек из чужого кода заведите файл подавлений и укажите его через переменную:
# файл lsan.supp
leak:libSomeThirdParty
leak:known_leaking_function
# запуск
export LSAN_OPTIONS=suppressions=lsan.suppДля UBSan есть атрибут прямо в коде — им помечают функцию, где UB осознанное и неизбежное (например, преднамеренное переполнение в хеш-функции):
#include <cstdint>
// Отключаем проверку конкретного типа UB для одной функции
__attribute__((no_sanitize("unsigned-integer-overflow")))
uint64_t hash_mix(uint64_t value) {
value *= 0x9E3779B97F4A7C15ULL; // переполнение здесь ожидаемо
return value ^ (value >> 32);
}Точечное подавление лучше глобального отключения проверки: вы сохраняете контроль над остальным кодом и оставляете в исходнике явную пометку, что здесь так задумано.
Санитайзеры в CI
Гонять их локально удобно, но настоящую пользу они дают в непрерывной интеграции, где ловят регрессии до релиза. Практика, которая работает:
- Заведите отдельную джобу CI с отдельной сборкой под
-fsanitize=address,undefined. Не пытайтесь встроить санитайзеры в основную релизную сборку — они там не нужны и замедляют её. - В этой джобе поставьте
ASAN_OPTIONS=abort_on_error=1иUBSAN_OPTIONS=halt_on_error=1, чтобы любая ошибка роняла сборку. - TSan запускайте отдельной джобой и только на тестах, где реально есть многопоточность. Гонять весь набор под TSan дорого по времени.
- Файлы подавлений держите в репозитории рядом с CI-конфигом, а не в переменных окружения раннера — так их видно на ревью.
Где приёмы ломаются
Санитайзеры мощные, но у них есть граничные случаи, о которые спотыкаются регулярно.
- ASan не ловит переполнение стека в глубокой рекурсии как
heap-buffer-overflow— это отдельный класс, и часто вы увидитеSEGVбез подробного отчёта. Для контроля стека есть отдельная проверка-fsanitize=stackу части реализаций, но она не универсальна. - MSan требует инструментированной стандартной библиотеки. Без пересобранного с MSan libc++ он выдаёт лавину ложных срабатываний на инициализации из стандартных контейнеров. На практике MSan применяют редко именно из-за этого требования.
- TSan видит только те гонки, что проявились в конкретном запуске. Гонка на редкой раскладке потоков может не сработать ни разу за прогон. Помогает многократный запуск тестов и нагрузка.
- Санитайзерная сборка несовместима с продакшн-профилированием по скорости. Не делайте выводов о производительности по бинарнику с ASan — замедление в 2–3 раза искажает всё.
Что делать дальше
Начните с малого: соберите проект с -fsanitize=address,undefined и прогоните существующие тесты. Почти наверняка что-то всплывёт сразу. Потом заведите отдельную CI-джобу, чтобы регрессии не возвращались. Когда ASan+UBSan станут привычными, добавьте отдельную джобу под TSan для многопоточных участков. Полный список проверок и опций смотрите в документации Clang — там же указано, какие проверки появились в какой версии, если вы работаете на старом компиляторе.
Источники: Clang Documentation: AddressSanitizer, Clang Documentation: UndefinedBehaviorSanitizer
Частые вопросы
Можно ли включить ASan и TSan одновременно?
У меня GCC вместо Clang — приёмы подойдут?
Санитайзер молчит, но программа явно портит память — почему?
Сильно ли санитайзеры замедляют программу и стоит ли их держать в проде?
Что делать, если ложные срабатывания идут из сторонней библиотеки?
Почему UBSan показывает предупреждение, но сборка остаётся зелёной?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.