Новая векторная обработка исключений в Windows XP

Дата публикации 3 окт 2006

Новая векторная обработка исключений в Windows XP — Архив WASM.RU

Работая с Win32 более 8 лет назад, я создал список особенностей (уровня API), которые мне удалось заметить. Главным образом, они делали мою программистскую жизнь легче, а также помогали проще писать полезные инструменты. Когда я установил бета-версию Windows XP (известную под именем "Whistler"), я не ожидал увидеть так много новых API и был приятно этим удивлен. В этом месяце я собираюсь описать одно из нововведений, известное как векторная обработка исключений.

Я наткнулся на VEH, работая с утилитой PEDIFF. Вы указываете путь к двум разным копиям DLL, и PEDIFF выдает список отличающихся API, экспортируемых модулями. Так я обнаружил VEH, сравнивая директории экспорта kernel32.dll от Win2000 и WinXP. Имелось много новых API, но сразу бросилась в глаза функция AddVectoredExceptionHandler. Как дополнительный бонус, эта API была документирована в одном из последних выпусков MSDN Library, так что я не должен был охотиться за информацией.

Обзор SEH.

Так что такое VEH? Для начала, следует рассмотреть регулярную обработку исключений и заметить, чем отличается VEH. Статья рассчитана на подготовленных читателей, знающих основы SEH и языка программирования C++. SEH реализован в C++ с помощью выражений try/catch или расширением компилятора MS C++ __try/__except. Для детального ознакомления как работает SEH, смотрите мою статью "A Crash Course in Structured Exception Handling" за январь 1997 в MSJ. Короче говоря, SEH использует размещенные в стеке узлы. Когда вы используете блок try, информация об обработчике исключений сохраняется в стековом кадре текущей процедуры. В архитектуре x86, Microsoft использовала значение в fs:[0] для указания на текущий фрейм обработчика исключений. Фрейм содержит адрес кода, который будет вызван при возникновении исключения.

Если вы вызываете некоторую функцию внутри блока try, эта новая функция может установить свой собственный обработчик. Когда это происходит, в стеке создается новый фрейм обработчика и устанавливается указатель на предыдущий фрейм, как показано на рисунке 1. В сущности, SEH-фреймы образуют связанный список, начало которого адресуется через FS:[0]. Следует обратить внимание, что каждый последующий узел должен располагаться в стеке выше предыдущего. Операционная система предписывает это правило, следовательно, вы не можете создать произвольно как новый фрейм и вставить его в список.

Рисунок 1. Обработчики исключений в стеке.

Сей факт, что фреймы хранятся как узлы связанного списка, не только значительная деталь в великой схеме вещей, а ключ к пониманию механизма работы SEH. Когда происходит исключение, система начинает с головы списка и вызывает обработчики со словами: "Произошло исключение. Не желаете ли его обработать?". Обработчик может обработать исключение, решив проблему, и вернуть статус EXCEPTION_CONTINUE_EXECUTION.

Обработчик также может отклонить предложение и вернуть EXCEPTION_CONTINUE_SEARCH. Когда это происходит, система перемещается к следующему узлу в списке и задает тот же вопрос. Это продолжается до тех пор, пока кто-то не решится обработать исключение, или список не закончится. Я здесь не в давался в детали, но этого вполне достаточно для наших целей.

Каковы особенности архитектуры SEH? Важно, что данный обработчик может решить, что делать с исключением, несмотря на то, что другие обработчики (которые далее по списку) хотели бы обработать это исключение. Иногда это может быть проблемой. Следующий пример покажет почему.

Скажем, вы написали самый лучший в мире обработчик исключений. Когда происходит что-то плохое, ваш обработчик определяет причину, регистрирует необходимые детали, решает проблему мирового голода и за неделю отменяет назначенную встречу. Кроме того, вы помещаете ваш обработчик в WinMain, чтобы вся программа была охвачена. Теперь, в некотором месте вы вызываете внешний компонент, который никак не можете контролировать. Этот компонент также устанавливает обработчик. При первом же исключении, этот обработчик выходит из программы. Ваш обработчик никогда не получит шанса выполниться, потому что другой обработчик оказался первым в списке обработчиков. Короче говоря, прелести SEH ограничены тем фактом, что обработчики эффективны, пока кто-то глубже в цепи запросов не установил свой собственный обработчик.

Позвольте мне указать на еще одну особенность SEH перед тем, как перейти к VEH. Когда программа отлаживается, и происходит исключение, выполняется еще несколько шагов. Сперва отладчик получает первый шанс обработать исключение или позволить отлаживаемому процессу увидеть это самое исключение. Если этот дочерний процесс видит исключение, происходят ранее описанные действия. Если в процессе не нашлось обработчика, отладчик получает вторую возможность обработать исключение. (Обычно это происходит, когда отладчик выскакивает с диалогом необработанного исключения.)

ВВЕДЕНИН В VEH.

В общем, векторная обработка исключений подобна SEH с тремя ключевыми отличиями:

  • Обработчика не привязаны к определенной функции и стековому фрейму.
  • Компилятор не имеет ключевых слов (типа try/catch) для добавления новых обработчиков в список обработчиков.
  • Обработчики явно добавляются вашим кодом, а не являются плодом конструкции try/catch.

Новая API-функция AddVectoredExceptionHandler принимает указатель на функцию как аргумент и добавляет его в связанный список обработчиков. Так как система использует связанный список для хранения обработчиков, то программа может установить сколько угодно VEH-обработчиков.

Как VEH сосуществует с SEH? В WinXP, список VEH будет обработан перед списком SEH. Это сделано для совместимости с уже существующим кодом. Если бы VEH список обрабатывался после SEH, и SEH-обработчик мог обработать исключение, то VEH-обработчики не получили бы шанса заметить произошедшее исключение.

В отношении отладки, VEH работает подобно SEH. То есть, когда программа отлаживается, отладчик первым получает уведомление о произошедшем исключении. Если отладчик решает не обрабатывать исключение и передает его отлаживаемому процессу, то вызывается VEH-обработчик.

Прототип AddVectoredExceptionHandler можно найти в WINBASE.H:

Код (Text):
  1.  
  2. WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler(
  3.     ULONG FirstHandler,
  4.     PVECTORED_EXCEPTION_HANDLER VectoredHandler );
  5.  

Первый параметр сообщает системе, должен ли обработчик помещен в начало связанного списка обработчиков, либо в его конец. Список обработчиков не привязан к конкретному потоку и глобален для всего процесса. Таким образом, вы можете запросить поместить обработчик в начало списка, но вы не можете гарантировать, что ваш обработчик будет вызван первым. Вы не будете первым, если другой участок кода вызовет AddVectoredExceptionHandler после вас и поместил обработчик в начало списка. Каждый раз, когда вызывается AddVectoredExceptionHandler, новый обработчик помещается либо в начало, либо в конец списка.

Второй параметр - адрес функции обработчика исключений. Вот его прототип:

Код (Text):
  1.  
  2. LONG NTAPI VectoredExceptionHandler(PEXCEPTION_POINTERS);
  3.  

PEXCEPTION_POINTERS - указатель, который дает функции полную информацию об исключении, включая тип исключения, адрес и значения регистров. Функция должна возвратить одно из значений: EXCEPTION_CONTINUE_SEARCH или EXCEPTION_CONTINUE_EXECUTION.

Если обработчик вернул EXCEPTION_CONTINUE_EXECUTION, система пытается перезапустить выполнение процесса. VEH-обработчики, расположенные дальше по списку, не вызываются. Также не вызываются и SEH-обработчики. Если же получен код возврата EXCEPTION_CONTINUE_SEARCH, система вызывает следующий VEH-обработчик. После того, как все VEH-обработчики будут вызваны, система начинает обход списка SEH.

В дополнение AddVectoredExceptionHandler, также существует RemoveVectoredExceptionHandler, которая удаляет предварительно установленный обработчик из списка. Это неинтересно, и здесь упоминается лишь для законченности.

Возможности VEH.

Для людей, пишущих трассировочные и диагностические инструменты, точка останова - способ получить управление, когда выполняется желаемый участок кода. К сожалению, использование точек останова означает обработку исключений, в частности, точек останова и пошагового выполнения. В этом случае нереально использовать SEH для обработки этих исключений, так как вы не можете быть уверены в том, что ваш обработчик всегда увидит эти исключения.

Некоторые утилиты, например BugTrapper, обошли эту проблему, переписывая часть кода обработки исключений режима пользователя в NTDLL. Одним из мест, где можно это сделать, была функция KiUserExceptionDispatcher из NTDLL, описанная в моей статье про SEH в MSJ. Переписывание NTDLL - ненадежное решение, так как выходят новые версии NTDLL.

С VEH нет необходимости патчить NTDLL. VEH - простой способ видеть все исключения, подразумевая, что все обработчики ведут себя честно, как я писал выше. Для демонстрации VEH я создал небольшую программку, которая использует точки останова для мониторинга, когда программа вызывает LoadLibrary. В этой программе при вызове LoadLibrary мой код печатает имя подгружаемой DLL. Продвинутые читатели могут задаться вопросом по поводу правки таблицы адресов импорта (IAT), если это может дать тот же эффект, что мой метод на основе точек останова. Вы, конечно, можете использовать правку IAT, но это повлечет написание гораздо большего объема кода. В этом случае придется править IAT во всех DLL, включая те, которые загружены динамически посредством LoadLibrary. Поверьте мне, это тяжелее, чем кажется с первого взгляда. Использование точки останова - более простой способ добиться цели.

Вторая проблема, связанная с правкой IAT, в том, что это работает только для экспортируемых функций. Техника же, основанная на точках останова, будет работать для любого адреса в коде, не только для экспортируемых функций. Таким образом, это было бы полезно для вещей типа перехвата вызовов функции malloc при использовании статической run-time библиотеки (в противоположность MSVCRT.DLL).

Рисунок 2 содержит код DLL, которая использует VEH для мониторинга вызовов LoadLibrary. Каждый раз при вызове LoadLibrary, VectoredExcBP пишет имя DLL на стандартный вывод. DLL самодостаточна и не требует специальных вызовов для инициализации. Для экспериментов, достаточно вызвать единственную экспортируемую функцию.

Код (Text):
  1.  
  2. ===========================================================================
  3. // VectoredExcBP - Matt Pietrek 2001
  4. // MSDN Magazine, September 2001
  5. //
  6. // !^!^!^! WARNING WARNING WARNING !^!^!^!
  7. //  This code requires Windows XP or later.
  8. //  To compile this code correctly, you must:
  9. //      A) Have a WINBASE.H that defines AddVectoredExceptionHandler
  10. //      B) Make sure that the compiler finds *that* WINBASE.H before any
  11. //          other older versions of WINBASE.H.  See the compiler
  12. //          documentation if you're not sure how to do this.
  13. //      C) Define _WIN32_WINNT=0x0500   (or higher)
  14. //
  15. // This code compiles and works correctly with Beta 2 of Windows XP and
  16. // Visual C++ 6.0.  At the time of this writing, Windows XP is in beta,
  17. // so things could change, including API behavior, etc...  No guarantees
  18. // are made that this code will work in the future.
  19. //===========================================================================
  20. #include "stdafx.h"
  21.  
  22. #ifndef _M_IX86
  23. #error "This code only runs on an x86 architecture CPU"
  24. #endif
  25.  
  26. LONG NTAPI LoadLibraryBreakpointHandler(PEXCEPTION_POINTERS pExceptionInfo );
  27. void BreakpointCallback( PVOID pCodeAddr, PVOID pStackAddr );
  28. void SetupLoadLibraryExWCallback(void);
  29. BYTE SetBreakpoint( PVOID pAddr );
  30. void RemoveBreakpoint( PVOID pAddr, BYTE bOriginalOpcode );
  31.  
  32. // Global variables
  33. PVOID g_pfnLoadLibraryAddress = 0;
  34. BYTE g_originalCodeByte;
  35.  
  36. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  37.  
  38. BOOL APIENTRY DllMain( HANDLE hModule,
  39.                        DWORD  ul_reason_for_call,
  40.                        LPVOID lpReserved
  41.                      )
  42. {
  43.     // We don't need thread start/stop notifications, so disable them
  44.     DisableThreadLibraryCalls( (HINSTANCE)hModule );
  45.  
  46.     // At startup, set LoadLibrary breakpoint, at shutdown, remove it
  47.     if ( DLL_PROCESS_ATTACH == ul_reason_for_call )
  48.         SetupLoadLibraryExWCallback();
  49.     else if ( DLL_PROCESS_DETACH == ul_reason_for_call )
  50.         RemoveBreakpoint( g_pfnLoadLibraryAddress, g_originalCodeByte );
  51.  
  52.     return TRUE;
  53. }
  54.  
  55. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  56.  
  57. void SetupLoadLibraryExWCallback(void)
  58. {
  59.     // Obtain the address of LoadLibraryExW.  
  60.     // All LoadLibraryA/W/ExA calls
  61.     // go through LoadLibraryExW
  62.     g_pfnLoadLibraryAddress=(PVOID)GetProcAddress(GetModuleHandle("KERNEL32"),
  63.                                                     "LoadLibraryExW" );
  64.  
  65.     // Add a vectored exception handler for our breakpoint
  66.     AddVectoredExceptionHandler( 1, LoadLibraryBreakpointHandler );
  67.  
  68.     // Set the breakpoint on LoadLibraryExW.
  69.     g_originalCodeByte = SetBreakpoint( g_pfnLoadLibraryAddress );
  70. }
  71.  
  72. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  73. // Handler for our LoadLibraryExW breakpoint handler.  When the
  74. // breakpoint is hit, invoke the callback function (BreakpointCallback).
  75. // Then step the original instruction and let execution continue.  This
  76. // actually requires that two exception be handled, as described in the
  77. // function's code
  78. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  79.  
  80. LONG NTAPI LoadLibraryBreakpointHandler(PEXCEPTION_POINTERS
  81.                                         pExceptionInfo )
  82. {
  83.     // printf( "In LoadLibraryBreakpointHandler: EIP=%p\n",
  84.     //          pExceptionInfo->ExceptionRecord->ExceptionAddress );
  85.  
  86.     LONG exceptionCode = pExceptionInfo->ExceptionRecord->ExceptionCode;
  87.  
  88.     if ( exceptionCode == STATUS_BREAKPOINT)
  89.     {
  90.         // Make sure it's our breakpoint.  If not, pass on to other
  91.         // handlers
  92.         if ( pExceptionInfo->ExceptionRecord->ExceptionAddress
  93.             != g_pfnLoadLibraryAddress )
  94.         {
  95.             return EXCEPTION_CONTINUE_SEARCH;
  96.         }
  97.  
  98.         // We need to step the original instruction, so temporarily
  99.         // remove the breakpoint
  100.         RemoveBreakpoint( g_pfnLoadLibraryAddress, g_originalCodeByte );
  101.  
  102.         // Call our code to do whatever processing desired at this point
  103.         BreakpointCallback( pExceptionInfo->
  104.            ExceptionRecord->ExceptionAddress,
  105.                             (PVOID)pExceptionInfo->ContextRecord->Esp );
  106.  
  107.         // Set trace flag in the EFlags register so that only one
  108.         // instruction  will execute before we get a STATUS_SINGLE_STEP
  109.         // (see below)
  110.         pExceptionInfo->ContextRecord->EFlags |= 0x00000100;
  111.  
  112.         return EXCEPTION_CONTINUE_EXECUTION;    // Restart the instruction
  113.     }
  114.     else if ( exceptionCode == STATUS_SINGLE_STEP )
  115.     {
  116.         // Make sure the exception address is the single step we caused
  117.         // above.
  118.         // If not, pass on to other handlers
  119.         if ( pExceptionInfo->ExceptionRecord->ExceptionAddress
  120.             != (PVOID)((DWORD_PTR)g_pfnLoadLibraryAddress+1) )
  121.         {
  122.             return EXCEPTION_CONTINUE_SEARCH;
  123.         }
  124.  
  125.         // printf( "In STATUS_SINGLE_STEP handler\n" );
  126.  
  127.         // We've stepped the original instruction, so put the breakpoint
  128.         // back
  129.         SetBreakpoint( g_pfnLoadLibraryAddress );
  130.  
  131.         // Turn off trace flag that we set above
  132.         pExceptionInfo->ContextRecord->EFlags &= ~0x00000100;
  133.  
  134.         return EXCEPTION_CONTINUE_EXECUTION;    // Continue on!
  135.     }
  136.     else    // Not a breakpoint or single step.  Definitely not ours!
  137.     {
  138.         return EXCEPTION_CONTINUE_SEARCH;
  139.     }
  140. }
  141.  
  142. /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  143. // Invoked when LoadLibraryExW is called.  Passed the address of the
  144. // breakpoint, and the stack pointer.  The stack pointer can be used
  145. // to retrieve parameter values from the stack.  In this case, we want
  146. // to retrieve the unicode string that names to DLL to be loaded.
  147. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  148. void BreakpointCallback( PVOID pCodeAddr, PVOID pStackAddr )
  149. {
  150.     DWORD nBytes;
  151.  
  152.     LPWSTR pwszDllName;
  153.  
  154.     // pStackAddr+0 == return address
  155.     // pStackAddr+4 == first parameter
  156.     ReadProcessMemory(  GetCurrentProcess(),
  157.                         (PVOID)((DWORD_PTR)pStackAddr+4),
  158.                         &pwszDllName, sizeof(pwszDllName),
  159.                         &nBytes );
  160.    
  161.     printf( "LoadLibrary called on: %ls\n", pwszDllName );
  162. }
  163.  
  164. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  165. // Sets breakpoint at specific address, returns original opcode byte where
  166. // breakpoint was set.
  167. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  168. BYTE SetBreakpoint( PVOID pAddr )
  169. {
  170.     DWORD nBytes;
  171.     BYTE bOriginalOpcode;
  172.  
  173.     // Read the byte at the specified address
  174.     ReadProcessMemory( GetCurrentProcess(), pAddr,
  175.                         &bOriginalOpcode, sizeof(bOriginalOpcode),
  176.                         &nBytes);
  177.  
  178.     // Write breakpoint
  179.     BYTE bpOpcode = 0xCC;
  180.     WriteProcessMemory( GetCurrentProcess(), pAddr,
  181.                         &bpOpcode, sizeof(bpOpcode),
  182.                         &nBytes );
  183.  
  184.     return bOriginalOpcode;
  185. }
  186.  
  187. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  188. // Writes the original opcode byte back to the specified address where
  189. // a breakpoint was previously written.
  190. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  191. void RemoveBreakpoint( PVOID pAddr, BYTE bOriginalOpcode )
  192. {
  193.     DWORD nBytes;
  194.  
  195.     WriteProcessMemory( GetCurrentProcess(),
  196.                         pAddr,
  197.                         &bOriginalOpcode, sizeof(bOriginalOpcode),
  198.                         &nBytes );
  199. }
  200.  
  201. /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
  202. extern "C" void __declspec(dllexport) VectoredExcBP_ExportedAPI(void)
  203. {
  204.     // Do nothing function.  Just exported so that an EXE can link
  205.     // against this DLL.
  206. }
  207.  
Рисунок 2. VectoredExcBP

Также я написал демонстрационную программу TestVE (рисунок 3), которая загружает пару интересных DLL с помощью LoadLibrary. TestVE также вызывает фиктивную функцию из VectoredExcBP.DLL, что вынуждает загрузить DLL во время инициализации программы.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

// Prototype for function exported from VectoredExcBP DLL.
extern "C" void VectoredExcBP_ExportedAPI(void);

int main()
{
    // Load a couple of DLLs, which in turn also call LoadLibrary on
    // other DLLs.
    LoadLibrary( "MFC42" );

    LoadLibrary( "WININET" );

    // Call exported function in VectoredExcBP DLL.  This is simply
    // to force that DLL to be loaded.
    VectoredExcBP_ExportedAPI();

    return 0;
}
Рисунок 3. TestVE

Когда VectoredExcBP загружается, ее DllMain вызывает мою функцию SetupLoadLibraryExWCallback. Эта функция использует API AddVectoredExceptionHandler для регистрации нового обработчика. Дополнительно она же определяет адрес в памяти API-функции LoadLibraryExW из KERNEL32.DLL, и устанавливает точку останова на ее первую инструкцию.

Основное содержание VectoredExcBP - функция LoadLibraryBreakpointHandler. Это и есть обработчик, который устанавливается AddVectoredExceptionHandler. Когда происходит исключение, эта функция получает управление. Для всех неинтересных ей исключений, функция возвращает статус EXCEPTION_CONTINUE_SEARCH, чтобы дать возможность другим обработчикам увидеть исключение.

Не вдаваясь в теорию отладки, позвольте кратко описать последовательность событий, которые происходят при срабатывании точки останова и реакцию программы. Когда процессор выполняет инструкцию точки останова, первое, что происходит, это исключение типа STATUS_BREAKPOINT. При этом код, вызвавший исключение, уже не выполняется. Это подходящее время для исследования параметров и так далее.

Так как точка останова затерла оригинальную инструкцию, следующий шаг - это восстановление затертого байта, чтобы первоначальная инструкция могла выполниться. Как правило, это не трудно. Однако, есть проблема. Как только вы восстановите первоначальную инструкцию, и продолжите выполнение, вашей точки останова больше не будет, и обработчик не получит управление.

Решение (для процессоров x86) - выполнить только одну инструкцию и вернуть управление так, чтобы вы могли восстановить точку останова. Пошаговое выполнение на процессорах x86 - установка флага трассировки в регистре EFlags процессора. Когда флаг трассировки установлен, процессор выполняет только одну команду и генерирует исключение STATUS_SINGLE_STEP. После получения исключения STATUS_SINGLE_STEP, флаг трассировки может быть сброшен для продолжения нормального выполнения программы.

Рассмотрение функции LoadLibraryBreakpointHandler показывает, что она реализует в точности только что описанный алгоритм. Код экстра-параноидален, и проверяет, что адрес исключения тот, который именно он и ожидает. Код выполняет примерно то, что я описал и достаточно хорошо прокомментирован.

Ветка обработки исключения STATUS_BREAKPOINT функции LoadLibraryBreakpointHandler вызывает процедуру, названную мной BreakpointCallback. Она использует значения указателя стека в момент исключения, чтобы найти значения параметров. В случае LoadLibrary, это один параметр - указатель на имя подгружаемой DLL. Функция BreakpointCallback восстанавливает этот указатель от значения указателя стека. (Вы можете захотеть изменить вызов printf на что-то вроде OutputDebugString, если будете использовать эту DLL в неконсольном приложении.)

Вы можете быть удивлены, тем, что я использовал LoadLibraryExW для мониторинга. На это имеется серьезное основание. Поскольку LoadLibrary принимает строковый параметр, это может как ANSI, так и UNICODE строка. Наиболее часто используемая версия функции LoadLibrary - LoadLibraryA. Оказывается, LoadLibraryA всего лишь обертка вокруг LoadLibraryExA. В свою очередь, LoadLibraryExA - обертка вокруг LoadLibraryExW. Аналогично, LoadLibraryW сводится к вызову LoadLibraryExW. Все дороги ведут к LoadLibraryExW. С единственной точкой останова на эту API, я увижу вызовы любого варианта LoadLibrary.

Чтобы попробовать VectoredExcBP, убедитесь, что у нас запущена Windows XP Beta 2 или старше и запустите программу TestVE. TestVE только вызывает LoadLibrary для двух DLL (MFC42.DLL и WININET.DLL). Однако, эти DLL вызывают LoadLibrary внутри их DLLMain, так что вы должны видеть дополнительные вызовы LoadLibrary. Если все работает, вы должны увидеть примерно следующее:

Код (Text):
  1.  
  2. LoadLibrary called on: MFC42
  3. LoadLibrary called on: MSVCRT.DLL
  4. LoadLibrary called on: G:\WINDOWS\System32\MFC42LOC.DLL
  5. LoadLibrary called on: WININET
  6. LoadLibrary called on: kernel32.dll
  7. LoadLibrary called on: advapi32.dll
  8. LoadLibrary called on: kernel32.dll
  9.  

Выполнение VEH.

Выполнение VEH в Windows XP Beta 2 исключительно прямолинейно. AddVectoredExceptionHandler является форвадером RtlAddVectoredExceptionHandler из NTDLL. Рисунок 4 показывает псевдокод функции RtlAddVectoredExceptionHandler. Список VEH-обработчиков хранится как связанный список. Каждый зарегистрированный обработчик представлен 12-ти байтным узлом, расположенным в куче процесса. Критическая секция защищает код, который фактически вставляет новый обработчик в голову или конец списка. Если параметр FirstHandler не ноль, новый элемент вставляется в начало списка, иначе - в конец. Довольно просто! Нет кода, который проверяет, а не зарегистрирован ли уже этот новый обработчик, так что для некоторого обработчика возможно, что он будет зарегистрирован (и вызван) более одно раза.

struct _VECTORED_EXCEPTION_NODE
{
    DWORD   m_pNextNode;
    DWORD   m_pPreviousNode;
    PVOID   m_pfnVectoredHandler;
}

CRITICAL_SECTION RtlpCalloutEntryLock;
_VECTORED_EXCEPTION_NODE RtlpCalloutEntryList;

RtlAddVectoredExceptionHandler( ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectoredHandler )
{
    // Allocate space for the new node
    PVOID pExcptNode = HeapAlloc( GetProcessHeap(), 0, 0xC );
    if ( !pExcptNode )
        return 0;

    pExcptNode->m_pfnVectoredHandler = VectoredHandler;
    
    RtlEnterCriticalSection( &RtlpCalloutEntryLock );

    if ( FirstHandler )
    {
        pExcptNode->m_pNextNode = RtlpCalloutEntryList->m_pNextNode;
        pExcptNode->m_pPreviousNode = &RtlpCalloutEntryList;
        RtlpCalloutEntryList->m_pNextNode->m_pPreviousNode = pExcptNode;
        RtlpCalloutEntryList->m_pNextNode = pExcptNode;
    }
    else
    {
        pExcptNode->m_pNextNode = &RtlpCalloutEntryList;
        RtlpCalloutEntryList->m_pPreviousNode->m_pNextNode = pExcptNode;        
        pExcptNode->m_pPreviousNode = 
            RtlpCalloutEntryList->m_pPreviousNode;
        RtlpCalloutEntryList->m_pPreviousNode = pExcptNode;
    }

    RtlLeaveCriticalSection( &RtlpCalloutEntryLock );

    return pExcptNode;
}
Рисунок 4. RtlAddVectored

Другая примечательная особенность VEH - то, как вызываются обработчики. Как я написал в моей статье про SEH, KiUserExceptionDispatcher (в NTDLL) вызывает RtlDispatchException. Рисунок 5 показывает, как VEH был добавлен в код RtlDispatchException в NTDLL. Если вы сравните этот и оригинальный код из моей ранней статьи, вы увидите, что добавился только вызов функции RtlCallVectoredExceptionHandlers в самом начале RtlDispatchException. Это доказывает, что VEH-обработчика вызываются перед SEH-обработчиками.

RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
    DWORD    stackUserBase;
    DWORD    stackUserTop;    
    PEXCEPTION_REGISTRATION pRegistrationFrame;
    DWORD hLog;

    // The new bit of code
    RtlCallVectoredExceptionHandlers( pExcptRec, pContext );

    // Get stack boundaries from FS:[4] and FS:[8]
    RtlpGetStackLimits( &stackUserBase, &stackUserTop );

    pRegistrationFrame = RtlpGetRegistrationHead();
// ... rest of code
Рисунок 5. RtlDispatchException Pseudocode

Псевдокод RtlCallVectoredExceptionHandlers показан на рисунке 6. Снова, код очень простой. Критическая секция охраняет цикл while. Так цикл проходит через каждый зарегистрированный обработчик и вызывает функцию обработчика. Если обработчик вернул EXCEPTION_CONTINUE_EXECUTION, цикл прекращается без обхода остальных обработчиков. Также функция заботится о возвращаемом значении, говорящем RtlDispatchException, стоит ли затевать поиск SEH-обработчиков.

// Called from RtlDispatchException
RtlCallVectoredExceptionHandlers( PEXCEPTION_RECORD pExcptRec, 
    CONTEXT * pContext )
{
    bool bContinueExecution = false;

    // Guard the callbacks with a critical section
    RtlEnterCriticalSection( &RtlpCalloutEntryLock );

    // Get the head of the list     
    pCurrentNode = RtlpCalloutEntryList;

    // While nodes we haven't processed...
    while ( pCurrentNode != RtlpCalloutEntryList )
    {
        // Invoke the handler function
        EXCEPTION_POINTERS pExceptionPointers
        LONG disposition = pCurrentNode->m_pfnVectoredHandler
            ( &pExcptRec );

        // If the handler says to resume execution, break out of our loop
        if ( disposition == EXCEPTION_CONTINUE_EXECUTION )
        {
            bContinueExecution = true;
            break;
        }

        // go on to next node       
        pCurrentNode = pCurrentNode->m_pNextNode;
    }

    RtlLeaveCriticalSection( &RtlpCalloutEntryLock );

    return bContinueExecution;
}
Рисунок 6. RTLCallVectoredExceptionHandlers

Итак, VEH является существенным дополнением к Windows XP. Мне остается только сожалеть, что эта возможность не была добавлена раньше. Я продемонстрировал одно из многих преимуществ VEH и надеюсь, что в будущем эта технология будет широко использоваться в различных инновационных проектах.

Оригинал статьи

Из Microsoft Systems Journal. Сентябрь 2001.

© Мэт Питрек, пер. pawa

0 1.786
archive

archive
New Member

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