Какие важные моменты о структурированных исключениях должны знать каждый разработчик С++?
Что я должен знать о Structured Exceptions (SEH) в С++?
Ответ 1
Они эквивалент Win32 для сигналов Unix, и позволяют вам улавливать исключения процессора, такие как нарушение доступа, незаконное указание, делить на ноль.
С правильными параметрами компилятора (/EHa для Visual С++) исключения С++ используют тот же механизм, что и разматывание стека правильно работает как для исключений С++ (пользователя), так и для исключений SEH (OS).
В отличие от исключений С++, SEH не печатаются, но все они имеют одну и ту же структуру данных, в которой есть код исключения (причина) и дополнительная информация о том, какой код был поврежден, и о том, что регистры процессора сохраняются во время сбоя. Подробнее см. GetExceptionCode
и GetExceptionInformation
. это.
Кроме того, SEH имеет обработку "первого шанса", которая позволяет вам регистрировать или иным образом обрабатывать исключение, прежде чем разматывать уничтожает все локальные переменные.
Ответ 2
У меня недавно возникла проблема, которая была вызвана косвенно SEH, особенно из-за одной особенности SEH, о которой я думаю, что каждый разработчик должен знать:
При использовании SEH деструкторы не вызываются, поэтому, если у вас есть код очистки в деструкторе, он не будет очищен.
Наша проблема была вызвана критической секцией, которая была обернута объектом с блокировкой в конструкторе и разблокирована в деструкторе.
У нас была ситуация в тупике, и мы не могли понять, почему, и примерно через неделю после копания кода, дампов и отладки мы, наконец, поняли, что это произошло потому, что было исключение, которое было обработано COM и вызвало раздел Critical оставаться в замке. Мы изменили флаг компиляции в VS в свойствах проекта, которые говорят ему запускать деструкторы даже для SEH, и это решило проблему.
Поэтому, даже если вы не можете использовать SEH в своем коде, вы можете использовать библиотеку, которая делает (например, COM), и это может вызвать неожиданное поведение.
Ответ 3
Они должны знать, что они не являются частью Standard С++ - они являются изобретением Microsoft и могут использоваться на языках, отличных от С++.
Ответ 4
Ускоренный курс по глубине структурной обработки исключений Win32 ™
Эта статья является ссылкой на получение до скорости с SEH. 13 лет спустя все еще остается лучшим.
Существует специальная тема по MSDN для SEH и C++ Различия в обработке исключений.
Некоторые вещи C++ разработчик должен знать, обсуждается ли SEH:
Написание C/C++ SEH обработчиков исключений:
__try
{
// guarded code
}
__except ( expression )
{
// exception handler code
}
Это не обработка C++ исключений, это специфичные для MS расширения для перехвата прямого доступа в SEH. Он работает совсем не так, как ваши обычные исключения C++. Вам нужно хорошее понимание SEH, чтобы использовать их.
Написание обработчиков завершения C/C++ SEH:
__try {
// guarded code
}
__finally ( expression ) {
// termination code
}
Как и в случае с обработчиком SEH, не путайте это с семантикой исключений C++. Вам нужно хорошее понимание SEH.
_set_se_trasnlator
: эта функция преобразует исключения SEH в исключения типа C++ при использовании асинхронных исключений /EHa.
И, наконец, личное мнение: должен ли разработчик C++ знать SEH? После вашего первого новичка .ecxr вы поймете, что когда толчок приходит к пушу C++, исключения - это всего лишь иллюзия, предоставленная для вашего удобства. Единственное, что происходит, это SEH.
Ответ 5
Windows x64 SEH
Компилятор помещает каталог исключений в раздел .pdata образа .exe. Компилятор заполняет каталог исключений с помощью _RUNTIME_FUNCTION
s.
typedef struct _RUNTIME_FUNCTION {
ULONG BeginAddress;
ULONG EndAddress;
ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
Каждый _RUNTIME_FUNCTION
описывает функцию в изображении, которая использует SEH. Каждая функция в программе, в которой есть блок try/исключением или try/finally, имеет такую функцию. BeginAddress указывает на начало функции, а EndAddress указывает на конец функции.
UnwindData указывает на _UNWIND_INFO
таблицы _UNWIND_INFO
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
union {
//
// If (Flags & UNW_FLAG_EHANDLER)
//
OPTIONAL ULONG ExceptionHandler;
//
// Else if (Flags & UNW_FLAG_CHAININFO)
//
OPTIONAL ULONG FunctionEntry;
};
//
// If (Flags & UNW_FLAG_EHANDLER)
//
OPTIONAL ULONG ExceptionData[];
} UNWIND_INFO, *PUNWIND_INFO;
Если UNW_FLAG_EHANDLER
установлен, то ExceptionHandler
указывает на универсальный обработчик с именем _C_specific_handler
, целью которого является анализ ExceptionData
который указывает на структуру SCOPE_TABLE
. Если UNW_FLAG_UHANDLER
установлен, то это блок try/finally, и он будет указывать на обработчик завершения также псевдонимом _C_specific_handler
.
typedef struct _SCOPE_TABLE {
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;
Структура SCOPE_TABLE
- это структура переменной длины с ScopeRecord
для каждого блока try, который содержит начальный и конечный адрес (возможно, RVA) блока try. HandlerAddress
- это смещение функции фильтра исключений в круглых скобках __except()
(EXCEPTION_EXECUTE_HANDLER
означает всегда выполнять исключение, поэтому он аналогичен исключением Exception), а JumpTarget
- это смещение первой инструкции в блоке __except
связанном с блоком __try
.
Как только процессор выдает исключение, стандартный механизм обработки исключений в Windows найдет RUNTIME_FUNCTION для указателя неверной инструкции и вызовет ExceptionHandler. Это всегда приведет к вызову _C_specific_handler для кода режима ядра, работающего в текущих версиях Windows. _C_specific_handler затем начнет обход всех записей SCOPE_TABLE в поисках совпадения в ошибочной инструкции и, будем надеяться, найдет оператор __except, который охватывает код, вызывающий сбой. (Источник)
Чтобы добавить к этому, для вложенных исключений я представляю, что он всегда найдет наименьший диапазон, который покрывает текущую ошибочную инструкцию, и раскрутится через большие диапазоны, исключение не обрабатывается.
Также не ясно, как обработчик исключений ОС знает, в какой каталог исключений dll нужно обратиться. Я полагаю, он мог бы использовать RIP и обратиться к процессу VAD, а затем получить первый адрес конкретного распределения и вызвать для него RtlLookupFunctionEntry
.
Фильтры исключений
Пример функции, которая использует SEH:
BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult)
{
__try
{
*pResult = dividend / divisor;
}
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
return FALSE;
}
return TRUE;
}
Допустим, что catch (ArithmeticException a){//do something}
были использованы в Java. Это в точности эквивалентно:
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {//do something}
Строка фильтра в скобках указана значением ExceptionHandler
из предыдущего. Фильтр всегда равен EXCEPTION_CONTINUE_SEARCH
, EXCEPTION_EXECUTE_HANDLER
или EXCEPTION_CONTINUE_EXECUTION
. GetExceptionCode
получает ExceptionCode
(постоянную ошибки Windows) из _EXCEPTION_RECORD
который, вероятно, был создан специальным обработчиком исключений в IDT с использованием кода ошибки и исключения №. (_EXCEPTION_RECORD
хранится где-то так, чтобы он был доступен через вызов). Это сравнивается с конкретной ошибкой, EXCEPTION_INT_DIVIDE_BY_ZERO
является тем, что будет использоваться ArithmeticException
. Если выражение фильтра оценивается как EXCEPTION_EXECUTE_HANDLER
то оно перейдет к JumpTarget
противном случае я бы предположил, что он ищет ScopeRecord
с более широкой областью действия. Если у него заканчивается ScopeRecord
который покрывает RIP ошибочной инструкции, ему нужно вызвать исключение блока try, определенного для самого процесса создания потока, как показано ниже:
VOID
WINAPI
BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress)
{
DPRINT("BaseProcessStartup(..) - setting up exception frame.\n");
_SEH2_TRY
{
/* Set our Start Address */
NtSetInformationThread(NtCurrentThread(),
ThreadQuerySetWin32StartAddress,
&lpStartAddress,
sizeof(PPROCESS_START_ROUTINE));
/* Call the Start Routine */
ExitThread(lpStartAddress());
}
_SEH2_EXCEPT(UnhandledExceptionFilter(_SEH2_GetExceptionInformation()))
{
/* Get the Exit code from the SEH Handler */
if (!BaseRunningInServerProcess)
{
/* Kill the whole process, usually */
ExitProcess(_SEH2_GetExceptionCode());
}
else
{
/* If running inside CSRSS, kill just this thread */
ExitThread(_SEH2_GetExceptionCode());
}
}
_SEH2_END;
}
Если приложение в настоящее время не отлаживается, то необработанный фильтр, который вызывается, возвращает EXCEPTION_EXECUTE_HANDLER
который вызывает исключение и завершает поток/процесс. Было бы разумно, если бы для каждого потока был ScopeRecord
который должен использовать этот код диспетчеризации исключений ОС, что указывает на приведенный выше блок try/исключением. Было бы разумно, если бы он был сохранен в структуре ETHREAD
или что-то еще, или, возможно, если _RUNTIME_FUNCTION были записаны в изображение, описывающее функцию, которая инициализирует и вызывает поток (BaseProcessStartup
), но помните, что RIP будет внутри BaseProcessStartup
который будет иметь ядро RIP, поэтому поиск модуля в пространстве VAD не будет работать, поэтому в обработчике исключений ОС также может быть функция, которая проверяет, является ли RIP адресом режима ядра, и затем она знает смещение фильтра, потому что Длина и точная функция заранее известны, так как это функция ядра.
UnhandledExceptionFilter
вызовет фильтр, указанный в SetUnhandledExceptionFilter
который хранится в адресном пространстве процесса под псевдонимом GlobalTopLevelExceptionFilter
который инициализируется при динамическом связывании kernel32.dll, я думаю.
Пролог и Эпилог исключения
Внутри функции, описываемой структурой _RUNTIME_FUNCTION
, исключение может возникнуть в прологе или эпилоге функции, а также в теле функции. Пролог является частью вызова функции, которая согласовывает передачу параметров, соглашение о вызовах и передачу параметров, CS: RIP, RBP в стек. Эпилог - это обращение этого процесса, то есть возвращение из функции.
После нахождения _RUNTIME_FUNCTION
для которого RIP находится в диапазоне, перед _C_specific_handler
код обработки исключений ОС проверяет, находится ли RIP между BeginAddress
и BeginAddress + SizeOfProlog
определенных в структурах _RUNTIME_FUNCTION
и _UNWIND_INFO
соответственно. Если это так, он просматривает массив UnwindCodes
для первой записи со смещением, меньшим или равным смещению RIP от начала функции. Затем он отменяет все действия, описанные в массиве по порядку. Одним из этих действий может быть UWOP_PUSH_MACHFRAME
которое означает, что кадр прерывания был выдвинут. Восстановление этого кадра прерывания приведет к тому, что RIP будет таким же, как и до вызова функции. Процесс перезапускается с использованием протокола RIP перед вызовом функции после отмены действий; обработка исключений ОС теперь будет использовать этот RIP, чтобы найти _RUNTIME_FUNCTION
которая будет функцией вызывающей функции. Теперь это будет в теле вызывающей функции, так что теперь можно вызвать _C_specific_handler
родительского _UNWIND_INFO
для сканирования ScopeRecord
.
Если RIP не находится в диапазоне BeginAddress
- BeginAddress + SizeOfProlog
то он проверяет поток кода после RIP и, если он соответствует завершающей части легитимного эпилога, тогда он в эпилоге (странно, что он не только определяет SizeOfEpilog
и EndAddress
из EndAddress
но мы идем), а оставшаяся часть эпилога моделируется с _CONTEXT_RECORD
структуры _CONTEXT_RECORD
он строит, обновляясь при обработке каждой инструкции. RIP теперь будет сразу после вызова функции в вызывающей функции, поэтому родительский _RUNTIME_FUNCTION
будет снова выбран, как в случае с прологом, и будет использоваться для обработки исключения.
Если он не находится ни в прологе, ни в эпилоге, то он вызывает _C_specific_handler
_RUNTIME_FUNCTION
как и собирался.
Другой сценарий, о котором стоит упомянуть, - если функция является _RUNTIME_FUNCTION
функцией, у нее не будет записи _RUNTIME_FUNCTION
поскольку _RUNTIME_FUNCTION
функция не вызывает никаких других функций и не выделяет какие-либо локальные переменные в стеке. Следовательно, RSP напрямую обращается к указателю возврата. Указатель возврата на [RSP] сохраняется в обновленном контексте, имитируемый RSP увеличивается на 8, а затем ищет другой _RUNTIME_FUNCTION
.
закрытие
Когда фильтр возвращает EXCEPTION_CONTINUE_SEARCH
а не EXCEPTION_EXECUTE_HANDLER
, он должен вернуться из функции, которая называется unwinding. Для этого он просто проходит через массив UnwindCode
как и ранее, и отменяет все действия по восстановлению состояния ЦП до вызова функции - ему не нужно беспокоиться о локальных элементах, потому что они будут потеряны для Эфир, когда он движется вниз стека кадра. Затем он ищет _RUNTIME_FUNCTION
родительской функции и вызывает __C_specific_handler
. Если исключение обрабатывается, оно передает управление блоку исключений в JumpTarget
и выполнение продолжается как обычно. Если он не обрабатывается (то есть выражение фильтра не оценивается как EXCEPTION_EXECUTE_HANDLER
то он продолжает разматывать стек до тех пор, пока не достигнет BaseProcessStartup
и RIP не окажется в границах этой функции, что означает, что исключение не обработано. Как я уже говорил ранее, оно могло признать, что это адрес ядра и индекс для выражения фильтра исключений, которое оказывается UnhandledExceptionFilter(_SEH2_GetExceptionInformation())
в котором он будет передан отладчику, если процесс отлаживается, или вызовет пользовательский набор фильтров с SetUnhandledExceptionFilter
который будет выполнять некоторые действия, но должен возвращать EXCEPTION_EXECUTE_HANDLER
, в противном случае он просто возвратит EXCEPTION_EXECUTE_HANDLER
.
Ответ 6
Важным моментом является знание того, когда использовать SEH, а когда использовать стандартные исключения C++. Во-первых, выберите только одну систему - системы микширования имеют тенденцию создавать проблемы, требующие глубокого понимания того и другого для правильной реализации. Во-вторых, на высоком уровне SEH не ограничивается C++, в то время как стандартные исключения C++ не ограничиваются Windows. Если это не диктует ваше решение, выбирайте стандартные исключения, если они не соответствуют требованиям (см. Другие ответы для получения дополнительной информации о том, что может делать SEH).
Цитата из документации Microsoft (от 13.08.2008) подтверждает этот вывод.
Структурная обработка исключений (SEH) - это расширение Microsoft до C, которое корректно обрабатывает определенные исключительные ситуации кода, такие как сбои оборудования. Хотя Windows и Microsoft C++ поддерживают SEH, мы рекомендуем вам использовать обработку исключений в стандарте ISO C++, потому что это делает ваш код более переносимым и гибким. Тем не менее, чтобы поддерживать существующий код или для определенных видов программ, вам все равно придется использовать SEH.
Почему автор расширения рекомендует не использовать его в большинстве случаев? Предположительно, потому что расширение было написано для C, а текущий контекст - C++. Языки схожи, поэтому перенос SEH на C++ в качестве побочной выгоды, вероятно, был достаточно легким, даже если только "особые виды программ" действительно выиграли бы. (Или, возможно, по какой-то другой причине; возможно, портирование было начато до стандартизации C++. История становится запутанной.)