Вы пишете приложение на C/C++ под Windows. Оно падает. Вы смотрите в отладчике — и видите кучу странных стеков: ntdll!RtlRaiseException, kernel32!UnhandledExceptionFilter, __except_handler4. Вы понимаете: это SEH. Но что это на самом деле? Почему ваш __try/__except срабатывает в одном случае и игнорируется в другом? Почему в 64-битной версии всё ведёт себя иначе? И зачем вообще это нужно, если есть try/catch в C++?
Это не теория. Это то, что нужно знать, когда вы пытаетесь понять, почему ваш драйвер падает на клиентском ПК, или почему в вашем кроссплатформенном приложении на Windows внезапно исчезает обработчик исключений. Я не буду рассказывать про «механизм обработки исключений» как в учебнике. Я покажу, как это работает на практике — и как это влияет на ваш код.
- SEH — это не C++ исключения. Это системный механизм
- Как это выглядит на уровне кода
- Чем SEH отличается от C++ исключений
- 64-битные системы — всё по-другому
- Частые ошибки — и почему они ломают всё
- Когда SEH действительно нужен — и когда лучше обойтись без него
- Как правильно использовать SEH — практические рекомендации
- Что делать, если вы видите краш с SEH
- Итог — что делать прямо сейчас
SEH — это не C++ исключения. Это системный механизм
Самое важное, что нужно понять: SEH — это часть ОС, а не язык программирования. Он работает на уровне процессора и ядра Windows. Даже если вы пишете на чистом C, без C++, SEH будет работать. Даже если вы используете компилятор, который не поддерживает try/catch — SEH всё равно есть. Он был в Windows ещё в 16-битной версии.
Когда ваша программа пытается обратиться к нулевому указателю, делит на ноль или вызывает недопустимую инструкцию — процессор генерирует исключение. Это не «ошибка», а событие, которое прерывает нормальное выполнение. Windows не просто «вылетает» — она ищет: «Кто может это обработать?»
SEH — это система, которая позволяет вашему коду перехватить это событие и попытаться восстановиться. Без SEH ваша программа просто убивалась бы с сообщением «Приложение перестало работать». SEH даёт шанс: «А может, я могу это исправить?»
Как это выглядит на уровне кода
В Visual C++ вы используете:
__try {
int* p = nullptr;
*p = 42; // Вызовет ACCESS_VIOLATION
}
__except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
printf("Поймал доступ к нулю!\n");
}
Но что происходит под капотом?
- Компилятор при компиляции
__try/__exceptвставляет в вашу функцию специальные структуры — exception registration records. Они записываются в стек и указывают на обработчик. - Каждый поток в Windows имеет список этих записей — он называется Exception Handling Chain. Это связный список, где каждая запись — это структура, указывающая на обработчик и адрес, где он должен быть вызван.
- Когда происходит исключение, ядро Windows (через
ntdll) проходит по этому списку — от последней зарегистрированной записи к первой (LIFO — как стек). - Для каждой записи вызывается её обработчик. Обработчик возвращает одно из трёх значений:
EXCEPTION_CONTINUE_SEARCH— не моё, иду дальше.EXCEPTION_EXECUTE_HANDLER— я обработал, прыгаем в блок__except.EXCEPTION_CONTINUE_EXECUTION— исключение устранено, продолжаем с того же места (редко, только если вы знаете, что делаете).
- Если ни один обработчик не взял исключение — вызывается
UnhandledExceptionFilter, и приложение падает.
Это не C++. Это системный вызов. Даже если вы используете try/catch в C++, компилятор может превратить его в SEH под капотом — особенно если вы используете исключения с throw в коде, который вызывает WinAPI.
Чем SEH отличается от C++ исключений
Многие думают, что try/catch в C++ — это то же самое, что SEH. Это не так.
| Характеристика | SEH (Structured Exception Handling) | C++ Exceptions |
|---|---|---|
| Уровень | Операционная система | Язык программирования |
| Что ловит | Все исключения: доступ к памяти, деление на ноль, системные ошибки | Только те, что брошены через throw |
| Производительность | Высокая при возникновении исключения, но низкая при обычном выполнении | Высокая при обычном выполнении, но тяжелее при выбросе |
| Поддержка в C | Да | Нет |
| Совместимость с WinAPI | Полная | Нет — WinAPI не бросает C++ исключения |
| Приоритет обработки | Сначала SEH, потом C++ | Только если не перехвачено SEH |
Пример: вы вызываете VirtualAlloc — она может вернуть NULL, но не бросает исключение. Если вы передадите неправильный указатель в WriteFile — это вызовет исключение EXCEPTION_ACCESS_VIOLATION. Это только SEH. C++ try/catch его не поймает.
А если вы используете throw std::exception() внутри __try — то SEH перехватывает это исключение, а потом передаёт его C++-обработчику. Это сложный, но важный нюанс.
64-битные системы — всё по-другому
В 64-битной Windows SEH работает иначе. Это не просто «усовершенствование» — это полная переработка.
В 32-битной версии обработчики хранятся в стеке — в виде структур, которые компилятор вставляет. В 64-битной — их нет. Вместо этого используется unwind metadata — таблицы, которые компилятор генерирует в отдельной секции PE-файла (обычно .pdata и .xdata).
Что это значит на практике?
- Вы не можете динамически добавлять обработчики в 64-битном коде — всё фиксируется на этапе компиляции.
- Ваш
__try/__exceptработает, но только если компилятор знает о нём заранее. Динамическая генерация кода (например, через JIT) не будет работать с SEH в 64-битной системе без ручной регистрации метаданных. - Отладка сложнее: вы не увидите обработчики в стеке, как в 32-битной версии. В отладчике вы видите только
__CxxFrameHandler3— и это всё.
Если вы пишете драйвер или библиотеку, которая работает и в 32, и в 64 битах — вы должны понимать: ваш код с __try/__except может работать по-разному. В 64-битной версии компилятор может оптимизировать обработчики, и если вы полагаетесь на побочные эффекты (например, изменение переменных в __except), это может сломаться.
Частые ошибки — и почему они ломают всё
Вот что чаще всего ломает SEH в реальных приложениях:
- Использование
__exceptдля логики. Многие пишут:__except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { // Пытаемся восстановить состояние return some_value; }Это опасно. Вы не знаете, в каком состоянии находится стек. Переменные могут быть повреждены. Вы не можете надёжно восстановить состояние. Это как пытаться починить самолёт в полёте, не зная, какие детали оторвались.
- Смешивание
__try/__exceptи C++try/catchбез понимания порядка. Если вы обернули__tryвtry, а внутри__tryбросаете исключение — порядок обработки неочевиден. SEH срабатывает первым. Если он не перехватывает — тогда C++. - Использование
__exceptв динамически загружаемых библиотеках. Если вы загружаете DLL с помощьюLoadLibrary, а в ней есть__try/__except— и она крашится — обработчик может не сработать, потому что стек не был правильно инициализирован. Особенно актуально в плагинах. - Неправильная работа с
SetUnhandledExceptionFilter. Многие думают, что если они установили свой фильтр, то они могут «поймать всё». Но если в приложении есть несколько потоков, и один из них крашится в системной библиотеке — ваш фильтр может не сработать. Он работает только для необработанных исключений в основном потоке. - Предположение, что SEH — это «безопасный» способ обработки ошибок. Это не так. SEH — это механизм для аварийного восстановления. Он не заменяет проверки ввода, валидацию, проверки указателей. Он — последняя линия обороны.
Когда SEH действительно нужен — и когда лучше обойтись без него
Вот сценарии, где SEH — ваш лучший друг:
- Отладка крашей на клиенте. Вы пишете ПО, которое раздаётся тысячам пользователей. Один из них получает доступ к нулевому указателю. Вы хотите, чтобы приложение не просто вылетало, а сохраняло дамп памяти. SEH — единственный способ поймать это на уровне ОС и вызвать
MiniDumpWriteDump. - Интеграция с WinAPI, которые не используют C++ исключения. Например, вы вызываете
RegQueryValueEx— она не бросает исключения. Но если передать ей неверный указатель — вы получитеEXCEPTION_ACCESS_VIOLATION. Только SEH может это поймать. - Работа с низкоуровневым кодом: драйверы, хуки, JIT-компиляторы, эмуляторы. Там вы не можете позволить себе «вылет» — нужно уметь перехватить сбой и попытаться восстановить.
А вот когда SEH — плохая идея:
- Если вы пишете обычное приложение на C++ и используете только стандартные библиотеки — используйте
try/catch. Это чище, понятнее, безопаснее. - Если вы делаете библиотеку, которую будут использовать другие — не используйте SEH в интерфейсе. Он не переносим. Даже если вы используете только Windows, другие разработчики могут не знать, как с ним работать.
- Если вы пытаетесь «запихнуть» SEH как основной механизм обработки ошибок — это приведёт к нечитаемому коду. Каждый
__try— это как «бомба замедленного действия»: вы не знаете, где она может взорваться.
Как правильно использовать SEH — практические рекомендации
Вот что я делаю на практике:
- Использую SEH только для логирования и дампов. Всё, что происходит внутри
__except— это запись в лог, вызовMiniDumpWriteDump, и немедленныйexit(). Никаких попыток «восстановить» состояние. Это не работает. - Оборачиваю только внешние вызовы. Например:
__try { result = SomeWinAPICall(param); } __except (EXCEPTION_EXECUTE_HANDLER) { LogCrash("SomeWinAPICall failed with code %d", GetExceptionCode()); return false; }Это безопасно. Вы знаете, что исключение произошло именно в этой функции, и вы не нарушаете состояние внутренних переменных.
- В 64-битной версии проверяю, что компилятор не оптимизировал обработчики. В Visual Studio — включён флаг
/EHa(asynchronous exception handling). Без него__try/__exceptможет не работать в некоторых случаях (например, при вызовеlongjmp). - Никогда не использую
__finallyдля «очистки» в случае исключения.__finallyвыполняется всегда — даже если исключение не было обработано. Но если вы в нём меняете глобальное состояние — это может сломать всё. Используйте RAII (конструкторы/деструкторы) для очистки ресурсов — это надёжнее. - Пишу свой обработчик только один раз — в главном потоке. И только для дампов. В других потоках — ничего не делаю. SEH в потоках — это ловушка. Если вы не знаете, что делаете — лучше оставить всё как есть.
Что делать, если вы видите краш с SEH
Если ваше приложение падает, и в дампе вы видите ntdll!RtlRaiseException — это значит, что исключение не было обработано. Что делать?
- Проверьте, есть ли в стеке
__exceptобработчики. Если их нет — значит, вы не обернули вызов, который может крашиться. - Проверьте, не используете ли вы
__try/__exceptв динамически загружаемой DLL. В 64-битной системе это часто не работает. - Проверьте, не вызываете ли вы WinAPI с неправильными параметрами. Например, передаёте
NULLвместо указателя на буфер. - Включите отладочные символы и смотрите на адрес исключения. Если это
0x00000000— значит, обращение к нулевому указателю. Если0x00000004— возможно, вы обращаетесь к полю структуры через нулевой указатель. - Используйте
Application Verifierот Microsoft — он может поймать ошибки до того, как они приведут к исключению.
Итог — что делать прямо сейчас
Если вы пишете приложение под Windows и сталкиваетесь с крашами:
- Если вы используете только C++ и стандартные библиотеки — забудьте про SEH. Используйте
try/catchи RAII. Это проще, чище и безопаснее. - Если вы работаете с WinAPI, драйверами, плагинами или пишете ПО для клиентов — используйте SEH, но только для одного: записи дампов и логов. Никаких попыток «исправить» состояние. После этого — немедленно завершайте процесс.
- В 64-битной версии — убедитесь, что компилятор включил
/EHaи что ваш код не использует динамические обработчики. - Никогда не используйте SEH как основной механизм обработки ошибок. Это как использовать пожарный шланг, чтобы убрать каплю воды. Возможно, сработает. Но это неправильно.
SEH — это не инструмент для повседневного программирования. Это аварийная кнопка. И если вы её нажимаете часто — значит, вы делаете что-то не так в другом месте. Найдите причину, а не просто ловите последствия.
Информация в этой статье носит ознакомительный характер. Работа с низкоуровневыми механизмами Windows требует глубокого понимания системы и тестирования на целевых платформах. При разработке критически важных приложений всегда консультируйтесь с опытным разработчиком Windows-систем.
