Подтвердить что ты не робот

Что я должен знать о Structured Exceptions (SEH) в С++?

Какие важные моменты о структурированных исключениях должны знать каждый разработчик С++?

4b9b3361

Ответ 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++. История становится запутанной.)