Win32™ SEH изнутри (ч.2)

Дата публикации 11 авг 2005

Win32™ SEH изнутри (ч.2) — Архив WASM.RU

SEH на уровне компилятора

(листинг)

Пока я редко упоминал слова _try и _except, так как всё, что я описывал до сих пор, реализуется на уровне ОС. Однако, при взгляде на кривость двух моих маленьких программ, которые использовали, базовый SEH, обертка компилятора вокруг этого механизма - смотрится гораздо лучше. Давайте теперь посмотрим, как Visual C++ формирует свою SEH-надстройку над базовым SEH.

Для начала, важно запомнить, что другой компилятор, используя средства базового SEH уровня системы, может сделать что-то совершенно другое. Ничто не заставляет компилятор реализовывать модель _try/_except, которую описывает документация Win32 SDK. Например, развивающийся Visual Basic 5.0 использует SEH во время выполнения кода, но структуры данных и алгоритмы полностью отличаются от описанных здесь.

Если вы читали раздел документации Win32 SDK о SEH, вам будет понятен следующий синтаксис так называемого “основанного на кадре” ("frame-based”) обработчика исключений:

 try {
     // тело защищаемого кода
 }
 except (выражение-фильтр) {
     // блок обработчика исключений
 }

Проще говоря, весь код внутри _try-блока защищён структурой EXCEPTION_REGISTRATION, которая создана в кадре стека функции. При входе в функцию новая структура EXCEPTION_REGISTRATION помещается в начало связанного списка обработчиков исключений. После завершения блока try, его структура EXCEPTION_REGISTRATION удаляется из списка. Как я уже говорил, адрес начала списка хранится по адресу FS:[0]. Таким образом, если вы при отладке программы встречаете строчки типа:

 MOV DWORD PTR FS:[00000000],ESP

или

 MOV DWORD PTR FS:[00000000],ECX

вы можете быть уверены, что код устанавливает или удаляет _try/_except-блок.

Теперь, когда вы знаете, что блок _try соответствует структуре EXCEPTION_REGISTRATION находящейся в стеке, вам, возможно, захочется узнать: что известно относительно callback-функции закрепленной за EXCEPTION_REGISTRATION? Используя терминологию Win32, callback-функция обработчика исключений соответствует коду выражения-фильтра. Чтобы освежить вашу память скажу, выражение-фильтр - это код находящийся в скобках, которые идут сразу после ключевого слова _except. Именно этот код решает, выполнится ли код в последующем {}-блоке.

После того как вы напишите код выражения-фильтра, вы получаете возможность решать, будет ли возникшее исключение обработано кодом данного _except-блока. Код выражения-фильтра может быть очень простым, и просто возвращать значение "EXCEPTION_EXECUTE_ HANDLER". С другой стороны, выражение-фильтр могло бы вызвать функцию, которая производит сложные вычисления, и на основе полученного результата возвращает код, который указывает системе, что делать дальше. В общем, что это будет за код, решаете вы. Ключевой момент: код выражения-фильтра вызывается системой (callback) при возникновении исключения.

То описание, которое я только что дал, является на самом деле упрощенным и идеализированным отражением реальности, предназначенным для того, чтобы вам было проще понять ее. На самом деле все несколько сложнее. Для начала, код выражения-фильтра не вызывается непосредственно операционной системой. На самом деле, поле exception handler в любом EXCEPTION_REGISTRATION указывает на одну и ту же функцию. Эта функция находится в RTL Visual C++, и называется __except_handler3. Код выражения-фильтра вызывается уже из неё. Я вернусь к этому чуть позже.

Другое искажение, сделанное мной для облегчения восприятия, заключается в том, что структура EXCEPTION_REGISTRATION не создаётся и не разрушается каждый раз при входе и выходе из любого _try-блока. Вместо этого, в каждой функции использующей SEH создается только один EXCEPTION_REGISTRATION. Другими словами, вы можете иметь множество _try/_except конструкций в одной функции, но в стеке будет создан только один EXCEPTION_REGISTRATION. Аналогично, вы могли бы сделать в функции вложенные блоки _try. Однако, при этом, Visual C++ создаст только один EXCEPTION_REGISTRATION.

Если единственный обработчик исключений (это упоминавшийся ранее __except_handler3) покрывает весь EXE или DLL, и если единственный EXCEPTION_REGISTRATION разделяет множество _try-блоков, то там очевидно делается более сложная работа, чем кажется на первый взгляд. Этот фокус реализован с помощью таблицы данных, которую вы обычно не видите. Однако, так как общая цель этой статьи состоит в том, чтобы разобрать SEH по винтикам, давайте взглянем, как это делается.

Расширенный фрейм обработки исключений

SEH, реализованный в Visual C++, не использует базовую структуру EXCEPTION_REGISTRATION. Вместо этого, он добавляет в конец этой структуры дополнительные поля данных. Именно эти дополнительные данные позволяют одной функции (__except_handler3) обработать все исключения программы, и управлять вызовом соответствующих выражений-фильтров и _except-блоков. Подсказка к формату используемой в Visual C++ расширенной структуры EXCEPTION_REGISTRATION была найдена в файле EXSUP.INC, который находится в исходниках RTL Visual C++. В этом файле, вы найдете следующее (закомментированное) определение:

 ;struct _EXCEPTION_REGISTRATION{
 ;     struct _EXCEPTION_REGISTRATION *prev;
 ;     void (*handler)(PEXCEPTION_RECORD,
 ;                     PEXCEPTION_REGISTRATION,
 ;                     PCONTEXT,
 ;                     PEXCEPTION_RECORD);
 ;     struct scopetable_entry *scopetable;
 ;     int trylevel;
 ;     int _ebp;
 ;     PEXCEPTION_POINTERS xpointers;
 ;};

С первыми двумя полями (prev и handler) вы уже знакомы. Они составляют основу структуры EXCEPTION_REGISTRATION. Новыми здесь являются последние три поля: scopetable, trylevel и _ebp. Поле scopetable содержит адрес массива структур типа scopetable_entries, в то время как поле trylevel по существу содержит индекс в этом массиве. Последнее поле, _ebp - значение указателя кадра стека (EBP) до создания структуры EXCEPTION_REGISTRATION.

Это не случайно, что поле _ebp стало частью расширенной структуры EXCEPTION_REGISTRATION. Оно включается в структуру операцией PUSH EBP, с которой начинается большинство функций. Это даёт возможность обращаться к другим полям структуры EXCEPTION_REGISTRATION с помощью отрицательного смещения от указателя кадра. Например, поле trylevel находится по адресу [EBP-04], указатель scopetable находится по адресу [EBP-08] и т.д.

Сразу вслед за расширенной структурой EXCEPTION_REGISTRATION, Visual C++ кладёт в стек два дополнительных значения. В значении DWORD, которое он кладёт в первую очередь, хранится указатель на структуру EXCEPTION_POINTERS (стандартную структуру Win32). Это указатель, который возвращает API-функция GetExceptionInformation. Хотя документация SDK подразумевает, что GetExceptionInformation является стандартной API-функцией Win32, на самом же деле это встроенная функция компилятора. Когда вы вызываете эту функцию, Visual C++ генерирует следующее:

 MOV EAX,DWORD PTR [EBP-14]

Так же, как и GetExceptionInformation, встроенной функцией компилятора, является связанная с ней функция GetExceptionCode. GetExceptionCode просто находит, и возвращает значение поля, принадлежащего одной из структур данных, возвращенных GetExceptionInformation. Я надеюсь что читатель, в качестве упражнения, самостоятельно выяснит, что делают, следующие команды, которые Visual C++ генерирует для GetExceptionCode:

 MOV EAX,DWORD PTR [EBP-14]
 MOV EAX,DWORD PTR [EAX]
 MOV EAX,DWORD PTR [EAX]

Вернемся к расширенной структуре EXCEPTION_REGISTRATION. В восьми байтах от её начала Visual C++ резервирует значение DWORD для того значения указателя стека (ESP), которое он имеет сразу после исполнения кода пролога функции. Этот DWORD - нормальное значение регистра ESP во время выполнения функции (конечно, кроме тех случаев, когда перед вызовом другой функции параметры кладутся в стек).

Похоже, я вывалил на вас тонну информации. Прежде чем двигаться дальше, давайте взглянем на стандартный SEH-фрейм, который создаёт Visual C++ для функции, использующей SEH:

 EBP-00 _ebp
 EBP-04 trylevel
 EBP-08 указатель на таблицу scopetable
 EBP-0C адрес функции обработчика
 EBP-10 Указатель на предыдущую EXCEPTION_REGISTRATION
 EBP-14 GetExceptionPointers
 EBP-18 Обычное значение регистра ESP в фрейме

С точки зрения ОС, существуют только два поля, из которых состоит базовая структура EXCEPTION_REGISTRATION: указатель prev в [EBP-10] и указатель функцию обработчика в [EBP-0Ch]. Остальное содержимое фрейма является специфическим расширением Visual C++. Теперь, зная всё это, давайте посмотрим на процедуру из RTL Visual C++, которая воплощает SEH на уровне компилятора: __except_handler3.

__except_handler3 и scopetable

Я не могу отправить вас к исходникам RTL Visual C++, чтобы вы сами посмотрели функцию __except_handler3, потому, что её там нет. Вместо этого вам придётся иметь дело с написанной мной версией этой функции в псевдокоде (см. рис. 9).

int __except_handler3(
     struct _EXCEPTION_RECORD * pExceptionRecord,
     struct EXCEPTION_REGISTRATION * pRegistrationFrame,
     struct _CONTEXT *pContextRecord,
     void * pDispatcherContext )
 {
     LONG filterFuncRet
     LONG trylevel
     EXCEPTION_POINTERS exceptPtrs
     PSCOPETABLE pScopeTable
 
     CLD     // Сбрасываем флаг направления (не делайте никаких допущений!)
 
     // Если ни один из битов: EXCEPTION_UNWINDING, или EXCEPTION_EXIT_UNWIND,
     // не установлен, то обработчик работает
     // в своем основном режиме (т.е. он работает не в режиме раскрутки).
 
     if ( ! (pExceptionRecord->ExceptionFlags
             & (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
     {
         // Создаем в стеке структуру EXCEPTION_POINTERS.
         exceptPtrs.ExceptionRecord = pExceptionRecord;
         exceptPtrs.ContextRecord = pContextRecord;
 
         // Помещаем указатель на EXCEPTION_POINTERS в стек на 4 байта ниже
         // SEH-фрейма.
         // Смотри ассемблерный код функции GetExceptionInformation.
         *(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;
 
         // Получаем начальное значение поля "trylevel".
         trylevel = pRegistrationFrame->trylevel 
 
         // Получаем указатель на массив scopetable.
         scopeTable = pRegistrationFrame->scopetable;
 
 search_for_handler: 
 
         if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
         {
             if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
             {
                 PUSH EBP // Сохраняем значение регистра EBP для этого фрейма.
 
                 // !!!Очень важно!!!  Переключение на оригинальное значение регистра
                 // EBP.  Это позволяет иметь, в пределах фрейма, одно и 
                 // тоже значение в регистре EBP, как до возникновения исключения,
                 // так и после него.
                 EBP = &pRegistrationFrame->_ebp 
 
                 // Вызываем функцию фильтра.
                 filterFuncRet = scopetable[trylevel].lpfnFilter();
 
                 POP EBP // Восстанавливаем значение регистра EBP нужное для этого
                         // фрейма обработчика.
 
                 if ( filterFuncRet != EXCEPTION_CONTINUE_SEARCH )
                 {
                     if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
                         return ExceptionContinueExecution;
 
                     // Если управление дошло до этого места, значит фильтр вернул
                     // значение EXCEPTION_EXECUTE_HANDLER.
                     scopetable == pRegistrationFrame->scopetable
 
                     // С помощью следующей функции ОС фактически производит очистку
                     // зарегистрированных фреймов.
                     // Эта функция рекурсивная.
                     __global_unwind2( pRegistrationFrame );
 
                     // Когда выполнение доходит до этого места, все фреймы,
                     // зарегистрированные после фрейма обработчика обрабатывающего
                     // исключение, уже очищены.
                     EBP = &pRegistrationFrame->_ebp
                     
                     __local_unwind2( pRegistrationFrame, trylevel );
 
                     // NLG == "non-local-goto" (setjmp/longjmp stuff)
                     __NLG_Notify( 1 );  // EAX == scopetable->lpfnHandler
 
                     // Устанавливаем текущий trylevel относительно того элемента
                     // SCOPETABLE,
                     // который использовался, когда был найден обработчик.
                     pRegistrationFrame->trylevel = scopetable->previousTryLevel;
 
                     // Вызываем код _except-блока.  Он не возвращает сюда
                     // управление.
                     pRegistrationFrame->scopetable[trylevel].lpfnHandler();
                 }
             }
 
             scopeTable = pRegistrationFrame->scopetable;
             trylevel = scopeTable->previousTryLevel
 
             goto search_for_handler;
         }
         else    // trylevel == TRYLEVEL_NONE
         {
             retvalue == DISPOSITION_CONTINUE_SEARCH;
         }
     }
     else    // Установлен какой-то из флажков: EXCEPTION_UNWINDING или
             // EXCEPTION_EXIT_UNWIND.
     {
         PUSH EBP    // Сохраняем значение регистра EBP.
         EBP = pRegistrationFrame->_ebp  // Устанавливаем значение регистра EBP для 
                                         // __local_unwind2
 
         __local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
 
         POP EBP     // Восстанавливаем значение регистра EBP/
 
         retvalue == DISPOSITION_CONTINUE_SEARCH;
     }
 }
Рисунок 9. Псевдокод __except_handler3.

Хотя вы увидите в __except_handler3 большое количество кода, помните, что это всего лишь callback-функция, вызываемая при возникновении исключения, подобная той, которую я описал в начале этой статьи. Она имеет четыре параметра, аналогичные тем, которые имеют изготовленные мной функции MYSEH.EXE и MYSEH2.EXE. На самом верхнем уровне, функция __except_handler3, с помощью оператора if, разбита на две части. Это сделано потому, что функция может быть вызвана в двух режимах: в обычном режиме (в этом режиме функция вызывается при возникновении исключения) и в режиме раскрутки (в этом режиме функция вызывается в процессе раскрутки). Большая часть функции предназначена работы в обычном режиме.

Сначала этот код создает в стеке структуру EXCEPTION_POINTERS, инициализируя ее двумя параметрами, переданными в __except_handler3. Адрес этой структуры, которую в псевдокоде я назвал: exceptPtrs, помещается по адресу [EBP-14]. Этим производится инициализация указателя, который используют функции GetExceptionInformation и GetExceptionCode.

Затем, __except_handler3 получает текущее значение trylevel из фрейма EXCEPTION_REGISTRATION[EBP-04]). Значение поля trylevel используется в качестве индекса в массиве scopetable, благодаря которому множество _try-блоков (в том числе, и вложенных) находящиеся в одной функции, используют одну и туже структуру EXCEPTION_REGISTRATION. Каждый элемент scopetable выглядит следующим образом:

typedef struct _SCOPETABLE
 {
     DWORD       previousTryLevel;
     DWORD       lpfnFilter
     DWORD       lpfnHandler
} SCOPETABLE, *PSCOPETABLE;

Назначение второго и третьего поля в SCOPETABLE понять не сложно. Они содержат адреса кодов выражения-фильтра и соответствующего _except-блока. Понять назначение поля previousTryLevel немного сложнее. В общем, оно нужно для вложенных try-блоков. Важно, что для каждого _try-блока в функции существует один элемент в SCOPETABLE.

Как я уже говорил, текущий trylevel определяет используемый элемент в массиве scopetable. Этот элемент, в свою очередь, определяет адреса выражения-фильтра и _except-блока. Теперь рассмотрим сценарий, когда один _try-блок вложен в другой. Если выражение-фильтр внутреннего _try-блока решает что связанный с ним _except-блок не будет обрабатывать возникшее исключение, о нём должно быть проинформировано выражение-фильтр внешнего _try-блока. Каким образом __except_handler3 узнает, какой элемент SCOPETABLE соответствует внешнему _try-блоку? Индекс этого элемента содержится в поле previousTryLevel, принадлежащем элементу SCOPETABLE. Используя эту схему, вы сможете создать произвольное количество вложенных _try-блоков. Поле previousTryLevel используется как узел в связанном списке возможных обработчиков исключений в пределах функции. В последнем элементе этого списка, поле trylevel содержит значение 0xFFFFFFFF.

Вернемся к коду функции __except_handler3. После того, как текущий trylevel получен, она берет соответствующий элемент SCOPETABLE, и вызывает указанный в нем код выражения-фильтра. Если выражение-фильтр возвращает значение EXCEPTION_CONTINUE_SEARCH, функция __except_handler3 переходит к следующему элементу SCOPETABLE, индекс которого указан в поле previousTryLevel. Если в процессе перебора списка подходящий обработчик не найден, функция __except_handler3 возвращает значение DISPOSITION_CONTINUE_SEARCH, которое заставляет систему перейти к следующему фрейму EXCEPTION_REGISTRATION.

Если выражение-фильтр возвращает значение EXCEPTION_EXECUTE_HANDLER, значит, исключение должно быть обработано кодом соответствующего _except-блока. Значит, все предыдущие фреймы EXCEPTION_REGISTRATION должны быть удалены из списка, и должен быть выполнен код _except-блока. Первая из этих технических задач решается вызовом функции __global_unwind2, которую я опишу позже. После кода, совершающего очистку, который я пока проигнорирую, исполнение продолжается в _except-блоке. Странно, что управление, никогда не возвращается из _except-блока, несмотря на то, что __except_handler3 вызывает его с помощью инструкции CALL.

Как устанавливается текущее значение trylevel? Это делается компилятором, который “на лету” изменяет поле trylevel в расширенной структуре EXCEPTION_REGISTRATION. Если вы исследуете ассемблерный код, который был сгенерирован для функции, использующей SEH, то в различных местах функции вы увидите код, который изменяет текущее значение trylevel в [EBP-04].

Почему, несмотря на то, что функция __except_handler3 передает управление _except-блоку с помощью инструкции CALL, управление к ней никогда не возвращается? Так как операция CALL помещает адрес возврата в стек, вы наверно подумали, что код _except-блока портит стек. Если вы исследуете код сгенерированный для _except-блока, то увидите, что первым делом он загружает в регистр ESP значение (DWORD), которое находится в стеке на 8 байтов ниже структуры EXCEPTION_REGISTRATION. Как часть своего кода пролога, функция сохраняет значение ESP, чтобы блок _except мог получить его позже.

Программа ShowSEHFrames

Если сейчас вы чувствуете, что немного ошеломлены вещами наподобие EXCEPTION_REGISTRATIONs, scopetables, trylevels, выражениями-фильтрами и раскруткой, то скажу вам, что в начале я тоже прошел через это. Тему: “SEH на уровне компилятора”, нельзя изучить постепенно. По отдельности большая ее часть не имеет смысла, если вы не воспринимаете её всю как единое целое. Когда я сталкиваюсь с большим количеством теории я стараюсь писать код, использующий концепции, которые я изучаю. Если код работает, значит (обычно) что я все понял правильно.

На рис. 10 показан исходный код программы ShowSEHFrames.EXE. Она использует _try/_except-блоки, чтобы создать список из нескольких Visual C++ SEH-фреймов. Впоследствии, она отображает информацию о каждом фрейме, а также структуры scopetable, которые Visual C++ создаёт для каждого SEH-фрейма. Программа не вызывает и не ожидает никаких исключений. Я включил все _try-блоки для того, чтобы Visual C++ создал множество кадров EXCEPTION_REGISTRATION, с множеством элементов scopetable в них.

В ShowSEHFrames важны следующие функции: WalkSEHFrames и ShowSEHFrame. WalkSEHFrames сначала выводит на экран адрес функции __except_handler3 (причина, по которой это делается, станет ясна вам чуть позже). Затем, функция получает указатель на начало списка обработчиков исключений из FS:[0], и обходит каждый его узел. Все узлы имеют тип VC_EXCEPTION_REGISTRATION, который является структурой, которую я определил, чтобы описать Visual C++ SEH-фрейм. WalkSEHFrames передает в функцию ShowSEHFrame указатель на каждый узел в списке.

//==================================================
 // ShowSEHFrames - Мэт Питрек 1997
 // Microsoft Systems Journal, Февраль 1997
 // FILE: ShowSEHFrames.CPP
 // To compile: CL ShowSehFrames.CPP
 //==================================================
 #define WIN32_LEAN_AND_MEAN
 #include 
 #include 
 #pragma hdrstop
 
 //---------------------------------------------------------------------------
 // !!! ВНИМАНИЕ !!!  Эта программа будет компилироваться только в Visual C++,
 // т.к. использует специфичные для Visual C++ структуры данных.
 //---------------------------------------------------------------------------
 
 #ifndef _MSC_VER
 #error Visual C++ Required (Visual C++ specific information is displayed)
 #endif
 
 //----------------------------------------------------------------------------
 // Structure Definitions
 //----------------------------------------------------------------------------
 
 // Структура данных, используемая в качестве основного SEH-фрейма, определенного
 // на уровне ОС.
 
 struct EXCEPTION_REGISTRATION
 {
     EXCEPTION_REGISTRATION* prev;
     FARPROC                 handler;
 };
 
 
 // Структура (-ы) данных на которую указывает расширенный SEH-фрейм, используемый 
 // на уровне Visual C++.
 
 struct scopetable_entry
 {
     DWORD       previousTryLevel;
     FARPROC     lpfnFilter;
     FARPROC     lpfnHandler;
 };
 
 // Расширенная структура данных, используемая в качестве SEH-фрейма, определенного 
 // на уровне Visual C++.
 
 struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
 {
     scopetable_entry *  scopetable;
     int                 trylevel;
     int                 _ebp;
 };
 
 //----------------------------------------------------------------------------
 // Prototypes
 //----------------------------------------------------------------------------
 
 // __ except_handler3 - функция RTL Visual C++. Мы хотим обратиться к ней,
 // чтобы вывести на экран её адрес.  Однако, т.к. она не представлена в заголовочных
 // файлах нам нужен ее псевдокод.
 
 extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *,
                                 PCONTEXT, PEXCEPTION_RECORD);
 
 
 //----------------------------------------------------------------------------
 // Code
 //----------------------------------------------------------------------------
 
 //
 // Отобразить на экране информацию об одном SEH-фрейме, наряду с его scopetable.
 // 
 void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec )
 {
     printf( "Frame: %08X  Handler: %08X  Prev: %08X  Scopetable: %08X\n",
             pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
             pVCExcRec->scopetable );
 
     scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;
 
     for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ )
     {
         printf( "    scopetable[%u] PrevTryLevel: %08X  "
                 "filter: %08X  __except: %08X\n", i,
                 pScopeTableEntry->previousTryLevel,
                 pScopeTableEntry->lpfnFilter,
                 pScopeTableEntry->lpfnHandler );
 
         pScopeTableEntry++;
     }
 
     printf( "\n" );
 }   
 
 //
 // Обходим связанный список обработчиков, и по очереди отображаем информацию 
 // о каждом из них.
 
 void WalkSEHFrames( void )
 {
     VC_EXCEPTION_REGISTRATION * pVCExcRec;
 
     // Вывести на экран местоположение функции __ except_handler3.
     printf( "_except_handler3 расположен по адресу: %08X\n", _except_handler3 );
     printf( "\n" );
 
     // Получить указатель на начало списка из FS:[0]
     __asm   mov eax, FS:[0]
     __asm   mov [pVCExcRec], EAX
 
     // Делаем перебор связанного списка фреймов.  Значение 0xFFFFFFFF 
     // сигнализирует об окончании списка.
     while (  0xFFFFFFFF != (unsigned)pVCExcRec )
     {
         ShowSEHFrame( pVCExcRec );
         pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
     }       
 }
 
 void Function1( void )
 {
     // Установить три вложенные _try-блоки (вследствие этого, появляется три 
     // элемента scopetable).
     _try
     {
         _try
         {
             _try
             {
                 WalkSEHFrames();    // Показать все SEH-фреймы.
             }
             _except( EXCEPTION_CONTINUE_SEARCH )
             {
             }
         }
         _except( EXCEPTION_CONTINUE_SEARCH )
         {
         }
     }
     _except( EXCEPTION_CONTINUE_SEARCH )
     {
     }
 }
 
 int main()
 {
     int i;
 
     // Используем два _try-блока (не вложенные).  Это вызывает генерацию двух 
     // элементов scopetable для этой функции.
     _try
     {
         i = 0x1234;     // Не делаем ничего особенного.
     }
     _except( EXCEPTION_CONTINUE_SEARCH )
     {
         i = 0x4321;     // Ничего особенного (в обратном порядке)
     }
 
     _try
     {
         Function1();    // Вызываем функцию, которая установит побольше SEH-фреймов.
     }
     _except( EXCEPTION_EXECUTE_HANDLER )
     {
         // Сюда управление передано не будет, т.к. мы не ожидаем исключения.
         printf( "Caught Exception in main\n" );
     }
 
     return 0;
 }
Рисунок 10. ShowSEHFrames.cpp

В процессе своей работы функция ShowSEHFrame выводит на экран адрес SEH-фрейма, адрес callback-функции обработчика исключений, адрес предыдущего SEH-фрейма и указатель на scopetable. Затем, для каждого элемента scopetable, она выводит на экран предыдущий trylevel, адрес выражения-фильтра и адрес _except-блока. Каким образом я узнаю, сколько элементов содержится в scopetable? На самом деле, я этого не знаю. Скорее, я предполагаю, что текущий trylevel в структуре VC_EXCEPTION_REGISTRATION на единицу меньше чем общее количество элементов в scopetable.

На рис. 11 показаны результат выполнения программы ShowSEHFrames. Сначала, исследуйте каждую строчку, которая начинается с "Frame:". Обратите внимание, что каждый следующий показанный экземпляр SEH-фрейма находится в стеке выше предыдущего. Затем, обратите внимание, что первые три строки, начинающиеся с "Frame: ", имеют одно и то же значение в поле Handler (004012A8). Посмотрев в начало информации, выданной программой, вы увидите, что это значение (004012A8) соответствует адресу функции __except_handler3 находящейся в RTL Visual C++. Это доказывает сделанное мной ранее утверждение, что существует только одна точка входа для всех обработчиков исключений.

Рисунок 11. Результат работы программы ShowSEHFrames.

У вас может возникнуть вопрос: почему программа показала три SEH-фрейма, использующих в качестве callback-обработчика функцию __except_handler3, в то время как в функции ShowSEHFrames явно указанны только две функции, использующие SEH? Третий фрейм устанавливается в RTL Visual C++. В коде находящемся в файле CRT0.C, который можно найти в исходниках RTL Visual C++, видно, что вызов функции main или WinMain обернут _try/_except-блоком. Код выражения-фильтра для этого _try-блока можно найти в файле WINXFLTR.C.

Вернемся к программе ShowSEHFrames, значение поля Handler в последней строчке начинающейся с "Frame:" содержит уже другой адрес - 77F3AB6C. Немного пошуровав вокруг, вы поймете, что этот адрес находится в KERNEL32.DLL. Этот специфический фрейм установлен, описанной мной ранее, функцией BaseProcessStart, которая находится в KERNEL32.DLL.

Из Microsoft Systems Journal. Январь 1997..

(продолжение статьи)

© Matt Pietrek / пер. Oleg_SK, SI

0 2.303
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532