Централізована обробка винятків h2>
Бєляєв Олексій p>
Коли додаток перестає працювати в офісі у
розробника, знайти помилку і виправити її не складає труднощів. Коли ж
додаток відмовляє у клієнта, то важко знайти спільну мову з засмученим
користувачем, і зрозуміти що ти зробив не так ... p>
Введення
h2>
Що таке помилка? Відповідаючи на це питання коротко, можна
сказати, що помилка - це відхилення від описаного поведінки. Для розробника
це означає, що необхідно шукати і виправляти причину цього відхилення. Для
програміста контролю якості ПЗ це означає, що необхідно доопрацювати тести
і включити їх в базовий цикл тестування програми. Для керівництва це
означає збільшення часу і витрат на розробку продукту. p>
Помилки бувають різні, одні відтворюються легко,
інші важко. На пошук одних витрачається небагато часу, на пошук інших йдуть
дні. Основна проблема пошуку помилки часто пов'язана з браком інформації
по її відтворенню або стані додатки в момент виникнення помилки.
Якби розробник мав інформацію про те, яка рядок програми містить
помилку, йому не склало б труднощів виправити її. p>
Єдиний спосіб уникнути помилок в програмах - це
писати код без помилок. Але людина не може не робити помилок, тому в будь-який
програмі вони є. Єдине, що розробник може постаратися зробити --
це мінімізувати кількість помилок, а також полегшити їх пошук та виправлення.
У цій статті розглядається спосіб, що допомагає прискорити пошук і виправлення
помилок. p>
Windows та необроблені виключення
h2>
Коли в додатку, що працює під управлінням ОС
Windows (від 9х до ХР), виникає необроблене виняток, операційна
система обробляє його, створює dump-файл і записує в нього інформацію,
аналізуючи яку можна відновити стан програми, але швидко знайти
помилку. До інформації, яку зберігає операційна система, належить: p>
інформація про потоки; p>
інформація про завантажені модулях; p>
інформація про виключення; p>
інформація про систему; p>
інформація про пам'ять. p>
Всю зібрану інформацію операційна система
пропонує послати в Microsoft для подальшого аналізу. З огляду на корисність
цієї інформації і вигоду, яку надає володіння їй, необхідно на етапі
проектування програми закласти можливість для збору інформації про
додатку під час виникнення виключення для подальшого її аналізу. Тоді
розробники будуть мати більше інформації для пошуку та виправлення помилок. p>
Яким чином Windows XP визначає, що в додатку
відбулося необроблене виняток? Відповісти на це питання можна, якщо
розібратися з механізмом структурованої обробки виключень (SEH). Всі
версії Windows, починаючи з версії Windows 95 і Windows NT, підтримують цей механізм
обробки виключень, що дозволяє операційній системі та додатку тісно
взаємодіяти у разі виникнення виняткову ситуацію. І якщо в
будь-якому додатку виникає необроблене виняток, операційна система
обробляє його і завершує додаток. p>
Структурована обробка помилок
h2>
Структурована обробка виключень (SEH) - це
що надається системою сервіс, навколо якого бібліотеки сучасних мов
програмування реалізують свої власні функції для роботи з винятками. p>
C + +-програмісти напевно знайомі з SEH в основному по
використання конструкцій __try ... __except. Зустрівши в тілі функції
конструкцію __try ... __except, компілятор, що підтримує SEH, генерує код
для реєстрації обробника виключення. Потім, після виникнення виключення,
операційна система шукає відповідний обробник. Якщо відповідний обробник НЕ
знайдений, операційна система створює dump-файл і завершує роботу програми. p>
Таким чином, перед нами стоїть завдання - зробити так,
щоб після виникнення в додатку необробленого виключення викликався наш
обробник. p>
Для вирішення цього завдання необхідно з'ясувати, як
операційна система шукає обробник виключення. У пошуках відповіді на це
питання я заглиблювався в документацію по механізму структурованих винятків,
аналізував системний асемблерні код, дивився, що генерує компілятор,
коли зустрічає конструкцію __try ... __except, але відповідного рішення не
знаходилося. Ні в SDK, ні в DDK я не знайшов нічого, що могло б відповісти на це
питання. Аналізуючи код, що генерується компілятором для конструкції __try ...
__except, я побачив, що для кожного нового обробника виключень у стеку
створюється запис, який поміщається в зв'язаний список. Ось приклад простий
функції, що допоможе зрозуміліше пояснити це: p>
void foo () p>
( p>
__try p>
( p>
) p>
__except (1) p>
( p>
) p>
) p>
Код, який був згенерований компілятором VC 7.0: p>
void foo () p>
( p>
00411DE0 push ebp p>
00411DE1 mov ebp, esp p>
00411DE3 push 0FFFFFFFFh p>
00411DE5 push 424140h p>
00411DEA push offset @ ILT +390 (__except_handler3) (41118Bh) p>
00411DEF mov eax, dword ptr fs: [00000000h] p>
00411DF5 push eax p>
00411DF6 mov dword ptr fs: [0], esp p>
00411DFD add esp, 0FFFFFF38h p>
00411E03 push ebx p>
00411E04 push esi p>
00411E05 push edi p>
00411E06 lea edi, [ebp-0D8h] p>
00411E0C mov ecx, 30h p>
00411E11 mov eax, 0CCCCCCCCh p>
00411E16 rep stos dword ptr [edi] p>
00411E18 mov dword ptr [ebp-18h], esp p>
__try p>
00411E1B mov dword ptr [ebp-4], 0 p>
00411E22 mov dword ptr [ebp-4], 0FFFFFFFFh p>
00411E29 jmp $ L19329 0 Ah (411E3Bh) p>
( p>
) p>
__except (1) p>
00411E2B mov eax, 1 p>
$ L19330: p>
00411E30 ret p>
$ L19329: p>
00411E31 mov esp, dword ptr [ebp-18h] p>
00411E34 mov dword ptr [ebp-4], 0FFFFFFFFh p>
( p>
) p>
) p>
00411E3B mov ecx, dword ptr [ebp-10h] p>
00411E3E mov dword ptr fs: [0], ecx p>
00411E45 pop edi p>
00411E46 pop esi p>
00411E47 pop ebx p>
00411E48 mov esp, ebp p>
00411E4A pop ebp p>
00411E4B ret p>
Тепер давайте подивимося, що робить цей асемблерні
код. На початку функції створюється фрейм стека. У цьому немає нічого незвичайного --
такий код є в більшості функцій, але ось декілька наступних інструкцій
здалися мені дуже цікавими: p>
00411DE3 push 0FFFFFFFFh p>
00411DE5 push 424140h p>
00411DEA push offset @ ILT +390 (__except_handler3) (41118Bh) p>
00411DEF mov eax, dword ptr fs: [00000000h] p>
00411DF5 push eax p>
00411DF6 mov dword ptr fs: [0], esp p>
Спочатку в стек кладеться -1 (як виявилося
згодом, просто резервується місце в стеку), а потім в стек записується
адреса статичної змінної та адреса обробника виключення. Якщо придивитися
до останніх трьох інструкцій, то можна побачити, що з пам'яті за адресою fs: [0]
прочитується якесь число, і кладеться в стек, а на його місце заноситься поточний
покажчик стека. У принципі, нічого підозрілого тут немає, але якщо розташувати
ці три інструкції послідовно кілька разів, то стане помітно, що вони
формують пов'язаний список, причому перший елемент цього списку завжди вказує
на попередній елемент. На виході з функції знаходиться код, який
відновлює попереднє значення змінної за адресою fs: [0]: p>
00411E3B mov ecx, dword ptr [ebp-10h] p>
00411E3E mov dword ptr fs: [0], ecx p>
Таким чином, якщо функція має в собі конструкцію
__try ... __except, то компілятор створює в стеку запис про новий обробнику
виключень і поміщає інформацію про неї в список обробників. Прийшовши до такого
висновку, я почав шукати хоч якусь інформацію про обробника виключень і
знайшов публікацію, написану Matt Pietrek-му 7 років тому (A Crash Course on the Depths
of Win32 Structured Exception Handling). У
цій статті описана структура SEH, і підтверджуються висновки, зроблені шляхом
аналізу коду наведеної вище функції. Вивчивши цю статтю і перевіривши написане
в ній, я виявив, що з тих пір в області обробки виключень практично
нічого не змінилося. p>
Зі статті випливає, що за адресою fs: [0], знаходиться
початок пов'язаного списку зареєстрованих обробників виключення, елементами
якого є структури типу _EXCEPTION_REGISTRATION, розташовані в
стеку. p>
struct _EXCEPTION_REGISTRATION p>
( p>
// покажчик на наступний запис p>
_EXCEPTION_REGISTRATION * prev; p>
// обробник виключення, створений Runtime бібліотекою p>
SEHHandler handler; p>
// покажчик на структуру, що описує блок
__try ... __except p>
PSCOPETABLE scopetable; p>
// рівень вкладеності поточного блоку try p>
int trylevel; p>
// покажчик на наступний запис p>
int _ebp; p>
); p>
У цій структурі handler є процедурою обробки
винятку. Прототип цієї функції наведено нижче: p>
typedef
int (* SEHHandler) (PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT,
void *); p>
Як бачите, функція обробника виключення приймає 4
параметра. Перший параметр має тип PEXCEPTION_RECORD - це покажчик на
структуру, яка містить інформацію про його виключення. Ви можете знайти оголошення цієї
структури в заголовки winnt.h: p>
typedef struct _EXCEPTION_RECORD ( p>
DWORD ExceptionCode; p>
DWORD ExceptionFlags; p>
_EXCEPTION_RECORD
* ExceptionRecord; p>
PVOID ExceptionAddress; p>
DWORD NumberParameters; p>
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS]; p>
) EXCEPTION_RECORD; p>
Опис найбільш значущих полів цієї структури
наведено нижче: p>
ExceptionCode - тип виключення. p>
ExceptionFlags - прапор виключення. p>
ExceptionAddress - адреса ділянки коду, де виникло
виняток. p>
Другий параметр функції містить в собі вказівник на
структуру PEXCEPTION_REGISTRATION. Її опис і призначення було приведено
вище. p>
Третій параметр вказує на змінну типу PCONTEXT
і несе інформацію про стан регістрів під час виключення. p>
Таким чином, механізм обробки виключень
стає більш-менш ясним, тобто коли в додатку виникає виняток,
операційна система бере поточний покажчик за адресою fs: [0] і переглядає
список обробників в пошуках потрібного обробника виключення. Якщо обробник
знайдено, вона передає йому керування. В іншому випадку операційна система
виконує свій обробник, який викликає функцію UnhandledExceptionFilter.
Отже, для отримання управління у разі виникнення необробленого
винятку, потрібно зареєструвати свій оброблювач і розташувати його в вершині
цього списку. Але світ програмування не був би таким цікавим, якби все
було так просто! Давайте пройдемо далі і подивимося, що відбувається під час
старту програми, і яку роль в обробці виключень грає
runtime-бібліотека. p>
Старт програми та ініціалізація runtime
h2>
Коли операційна система завантажує програму, вона
зчитує вміст файлу з диска в пам'ять, завантажує всі необхідні для
роботи програми зовнішні бібліотеки та ініціалізує таблицю імпорту адресами
реальних функцій. Після цього завантажувач передає управління на точку входу
додатки. У разі З + +-додатків, написаних з використанням Microsoft
Visual Studio 6.0 (7.x, 8.0), управління передається функції WinMainCRTStartup
або wWinMainCRTStartup, залежно від версії runtime-бібліотеки - UNICODE
або Multi-Byte Character Set (MBCS). Ця функція готує додаток до
роботі, ініціалізує runtime, виконує конструктори всіх статичних
змінних і передає управління на точку входу, певну розробником.
Якщо уважно розглянути цю функцію, можна побачити, що ініціалізація
призначених для користувача статичних змінних і передача керування на
налаштовувану точку входу здійснюється всередині блоку __try ... __except.
Заглибившись в дослідження фільтра винятків runtime-бібліотеки, я виявив,
що у разі виникнення необробленого виключення він викликає функцію
UnhandledExceptionFilter. P>
Функція UnhandledExceptionFilter знаходиться в
бібліотеці kernel32.dll і присутній в Windows, починаючи з версії Windows
95/NT. Призначення цієї функції, як видно з назви - обробка
необроблених виключень і завершення програми. Залежно від того, як
запущений додаток, UnhandledExceptionFilter веде себе по-різному. Так, якщо
додаток знаходиться під налагодженням, вона передає управління відладчику, в
Інакше вона виводить діалогове вікно «Application Error». Отже, для
обробки необроблених винятків, слід встановити свій оброблювач не на
вершину списку обробників, як здавалося раніше, а перед обробником Runtime
бібліотеки. p>
Установка обробника верхнього рівня
h2>
Давайте трохи відпочинемо і підсумовуємо все сказане
вище. SEH - це системний сервіс, в якому уніфікований механізм обробки
винятків, все обробники поточного потоку реєструються у списку реєстрації
обробників винятків. Якщо у функції зустрічається конструкція __try ...
__except, то створюється код, який реєструє новий обробник виключення і
поміщає інформацію про нього в стек. Під час завершення функції (а точніше, після
того, як управління вийшло з секції __try), функція разрегістрірует
обробник. Значить, якщо до поточного моменту в стеку знаходиться три функції,
кожна з яких встановила свій обробник виключення, то в списку
обробників виключення повинно знаходитися принаймні три обробника, а в
стеку повинні перебувати три записи про обробника виключень. Інформація про
поточному обробнику доступна за адресою fs: [0]. Runtime-бібліотека реєструє
свій оброблювач виключень, який (якщо виняток не обробляється програмою)
викликає функцію UnhandledExceptionFilter, після чого завершенні програм з
висновком діалогового вікна «Application Error». p>
Тепер настав час написати код, який би
використовував сказане вище і підтвердив правильність наших суджень. Давайте
напишемо просту функцію, яка б пробігала по всім зареєстрованим
обробника і виводила інформацію про них на екран. Код функції наведено нижче: p>
void zWalkThroughSEH () p>
( p>
_EXCEPTION_REGISTRATION *
pVCExcRec; p>
__asm mov eax, FS: [0] p>
__asm mov [pVCExcRec], EAX p>
// перебираємо блоки у зв'язаному списку. 0xFFFFFFFF означає кінець списку. P>
printf ( "Exception
Registration chain: n "); p>
while (0xFFFFFFFF! =
(unsigned) (UINT_PTR) pVCExcRec) p>
( p>
printf ( "tCurrent SEH
record: 0x% XntPrev SEH Record: 0x% XntHandler: 0x% Xnn ", p>
pVCExcRec, p>
pVCExcRec-> prev, p>
pVCExcRec-> hander); p>
pVCExcRec =
(_EXCEPTION_REGISTRATION *) (PVCExcRec-> prev); p>
) p>
) p>
Виклик цієї функції після початку виконання функції main
покаже, що до моменту виконання функції main в списку обробників вже
зареєстровані два обробника. Поточний обробник, як було показано раніше,
встановлений бібліотекою Runtime. А ось останній встановлений системою. P>
Exception Registration chain: p>
Current SEH record: 0x12FFB0 p>
Prev SEH Record: 0x12FFE0 p>
Handler: 0x41123A p>
Current SEH record: 0x12FFE0 p>
Prev SEH Record: 0xFFFFFFFF p>
Handler: 0x77E94809 p>
Як ви, напевно, вже здогадалися, створювати свій
обробник і мати у своєму розпорядженні його за системним немає ніякого сенсу, оскільки
виключення буде оброблено в runtime-бібліотеці, і додаток буде завершено.
Тоді я спробував створити свій оброблювач і розташував інформацію про нього перед
обробником runtime-бібліотеки, виділивши для нього місце у динамічній пам'яті,
але на жаль, моя програма просто було вивантажено з пам'яті після виникнення
винятку, а вставлений обробник не був виконаний. Як виявилося, так робити
не можна тому, що всі записи списку обробників винятків повинні лежати в
стеку, причому кожна наступна запис має бути розташована вище попередньої. p>
Отже, не можна розташувати інформацію про обробнику
перед інформацією про runtime-обробнику. Але ніхто не заважає переписати значення
поля hander обробника runtime-бібліотеки, встановивши його так, щоб він
вказував на нашу функцію. Код, який реалізує це, наведено нижче. P>
void zHookUpSEHChain (SEHHandler handler) p>
( p>
_EXCEPTION_REGISTRATION *
pVCExcRec; p>
__asm mov eax, FS: [0] p>
__asm mov [pVCExcRec], EAX p>
// перебираємо блоки у зв'язаному списку. 0xFFFFFFFF означає кінець
списку. p>
while (0xFFFFFFFF! = (unsigned) (UINT_PTR) pVCExcRec) p>
( p>
if (
(unsigned) (UINT_PTR) pVCExcRec-> prev-> prev == 0xFFFFFFFF) p>
( p>
defHandler =
pVCExcRec-> hander; p>
pVCExcRec-> hander = handler; p>
break; p>
) p>
pVCExcRec =
(_EXCEPTION_REGISTRATION *) (PVCExcRec-> prev); p>
) p>
) p>
де p>
defHandler - статична змінна, в якій
зберігається адресу попереднього обробника. p>
handler - наш обробник виключення. p>
Зрозуміло, уважний читач уже помітив
деяку нелогічність у цих судженнях. Навіщо намагатися зареєструвати свій
обробник таким витонченим методом, якщо досить помістити свій блок __try
__except у функції main? Справа в тому, що при використанні MFC, ATL або
якоїсь іншої бібліотеки немає доступу до призначеної для користувача точки входу, і,
отже, не можна встановити свій оброблювач. p>
Зараз настав час зібрати воєдино все сказане вище
і написати невелику програму, що ілюструє метод встановлення обробника. До
статті додається файл ehSimple.cpp, в якому ви знайдете код установки
обробника. Перший обробник реалізований у вигляді класу CatUnhandledExceptionFilter,
оголошеного в такий спосіб: p>
class
CatUnhandledExceptionFilter p>
( p>
private: p>
// SEHHandler oldHandler - змінна, в
яку буде записана адреса p>
// попереднього обробника виключення.
Оголошення типу SEHHandler p>
// було наведено вище. p>
staticSEHHandler oldHandler; p>
static void
zHookUpSEHChain (SEHHandler handler); p>
static int
myHandler (PEXCEPTION_RECORD pEhRecors, PEXCEPTION_REGISTRATION pEhRegRecord,
PCONTEXT pContext, void * pp); p>
public: p>
CatUnhandledExceptionFilter (); p>
~ CatUnhandledExceptionFilter (); p>
); p>
static void zHookUpSEHChain (SEHHandler handler); - це
функція для підміни обробника виключень runtime-бібліотеки. Код її майже не
відрізняється від запропонованого раніше. Єдиною зміною є змінна,
в якій зберігається адреса попереднього обробника. p>
static int myHandler (PEXCEPTION_RECORD pEhRecors,
PEXCEPTION_REGISTRATION pEhRegRecord, PCONTEXT pContext, PEXCEPTION_RECORD pp);
- Це наш обробник, який буде викликаний в разі виникнення
необробленого виключення. p>
int CatUnhandledExceptionFilter:: myHandler (PEXCEPTION_RECORD
pEhRecors, p>
PEXCEPTION_REGISTRATION
pEhRegRecord, PCONTEXT pContext, void * pp) p>
( p>
printf ("*** In My Handler
*** n "); p>
printf ( "Exception address:
0x% Xn ", pEhRecors-> ExceptionAddress); p>
printf ( "Exception code:
0x% Xn ", pEhRecors-> ExceptionCode); p>
return
CatUnhandledExceptionFilter:: oldHandler (pEhRecors, pEhRegRecord, pContext,
pp); p>
) p>
У програмі створюється статичний об'єкт типу
CatUnhandledExceptionFilter. Під час створення цього об'єкта в конструкторі
викликається функція підміни самого верхнього обробника виключень. Після того,
як статичні об'єкти програми створені, runtime передає управління функції
main, в якій генерується виключення з доступу до пам'яті, в результаті чого управління
переходить нашому обробникові винятку, який зараз не робить нічого, а
просто виводить інформацію про виключення на екран і передає управління
підміненого обробнику. p>
Після виникнення необробленого виключення
операційна система викликає наш обробник для обробки винятку і він,
виконавши те, що йому потрібно, передет керування далі. p>
Недоліком наведеного вище коду, тобто те, що він
працездатний тільки в однопоточний додатках. Все це відбувається тому,
що вказівник на вершину ланцюжка EXCEPTION_REGISTRATION знаходиться в структурі
TIB, яка зберігає в собі інформацію про поточний потоці, а значить, при вставці
обробника виключення з використанням значення FS: [0] ми встановимо обробник
тільки для одного потоку. p>
Але що ж робити у випадку багатопотокового програми?
Для цього, мабуть, потрібно виконати описані дії для кожного потоку. До
щастя, цього робити не доведеться. У наступному розділі ви побачите, як
обробити необроблені винятку у всіх потоках. p>
Фільтр необроблених виключень програми
h2>
Як уже згадувалося раніше, якщо ОС не знайшла
підходящого обробника виключень, вона викликає функцію
UnhandledExceptionFilter. Ця функція знаходиться в kernel32.dll і є у
всіх версіях Windows. У заголовків файлах SDK вона оголошена наступним
так: p>
LONG
UnhandledExceptionFilter (_EXCEPTION_POINTERS * ExceptionInfo); p>
де ExceptionInfo - це покажчик на структуру,
яка описує виключення і вміст регістрів під час виникнення
винятку. p>
Призначення цієї функції - показати діалогове вікно,
що повідомляє, що відбулося необроблене виключення, і, залежно від того,
знаходиться додаток під налагодженням чи ні, передати управління відладчику або
завершити потік. Ще однією функцією цього фільтра є виклик
користувальницького обробника необроблених винятків, що може бути
встановлений функцією SetUnhandledExceptionFilter. p>
Я свідомо не згадував про функції
SetUnhandledExceptionFilter, тому що її використання має ряд важливих
недоліків, що перекривають переваги, що надаються її використанням.
Щоб внести ясність, давайте розглянемо її код: p>
SetUnhandledExceptionFilter: p>
77E7E5A1 mov ecx, dword ptr [esp +4] p>
77E7E5A5 mov eax, dword ptr ds: [77ED73B4h] p>
77E7E5AA mov dword ptr ds: [77ED73B4h], ecx p>
77E7E5B0 ret 4 p>
Як видно з коду, функція бере адреса, переданий їй
як параметр, і поміщає його в системну область пам'яті, повертаючи
попереднє значення. Тобто вона замінює встановлений раніше обробник новим.
Така поведінка говорить, що використання цієї функції може призвести до того,
що обробник може бути переустановлений ким завгодно. p>
Другим важливим недоліком використання цієї функції
є неможливість налагодження обробника, оскільки функція UnhandledExceptionFilter
передає управління відладчику до виклику користувача обробника, заважаючи
нормальної налагодженні. p>
Якщо ви згодні з переліченими вадами
використання функції SetUnhandledExceptionFilter, то давайте подумаємо, як
отримати управління під час виникнення необробленого виключення. Якщо ви
не згодні, то можете пропустити наступний розділ і перейти безпосередньо до
розділу «Збір та зберігання інформації про стан процесу». p>
Отримання управління
h2>
До цих пір мова йшла про механізм обробки винятків,
використовується в ОС Windows, і про те, як він використовується компіляторами.
Реалізований вище простий алгоритм підміняє встановлений компілятором
обробник, що дозволяє отримати управління при виникненні в додатку
необробленого виключення. Але алгоритм був розрахований на Однопотокові
додаток, а це значно звужує можливості його використання. Крім усього
іншого, якщо в додатку виникає необроблене виняток, то
встановлений Runtime-бібліотекою обробник викликає функцію
UnhandledExceptionFilter, яка також може викликати операційною системою.
Таким чином, функція UnhandledExceptionFilter є ключовою системної
функцією, яка обробляє всі необроблені виключення. Підміна цієї
функції дозволила б виконати поставлене завдання. p>
Оскільки функція UnhandledExceptionFilter може бути
викликана як з обробника Runtime, так і системою, то зміна її адреса в
таблиці імпортуються функцій може не вирішити всієї проблеми, тому для
більшої надійності потрібно змінити код функції UnhandledExceptionFilter так, що
б відразу після її виклику управління потрапляло до нас. До статті додається проект
ehfilter, в якому реалізований клас CatUnhandledExceptionFilter,
встановлює обробник необроблених винятків. Оголошення класу
CatUnhandledExceptionFilter наведено нижче: p>
class CatUnhandledExceptionFilter p>
( p>
private: p>
friend LONG
myUnhandledExceptionFilter (_EXCEPTION_POINTERS * ExceptionInfo); p>
UINT_PTR
m_oldSystemUnhandledFilter; p>
LONG
UnhandledExceptionFilter (_EXCEPTION_POINTERS * ExceptionInfo); p>
public: p>
CatUnhandledExceptionFilter (); p>
~ CatUnhandledExceptionFilter (); p>
ool HookUpUnhandledFilter (); p>
); p>
де p>
m_oldSystemUnhandledFilter - адреса оригінальній
функції UnhandledExceptionFilter. p>
myUnhandledExceptionFilter - дружня класу
функція-перехідник, її завдання і код будуть розглянуті нижче. p>
UnhandledExceptionFilter - наш фільтр необроблених
винятків p>
HookUpUnhandledFilter - функція установки нашого
фільтра винятків. p>
Проект ehfilter є звичайною DLL, яка повинна
бути завантажена в адресний простір програми. Під час завантаження бібліотеки
у файлі main.cpp створюється глобальна змінна gFeedBackFilter типу
CatUnhandledExceptionFilter. Під час створення цієї змінної в конструкторі
визначається адреса функції UnhandledExceptionFilter і запам'ятовується у змінній
m_oldSystemUnhandledFilter. Коли до бібліотеки приходить повідомлення
DLL_PROCESS_ATTACH, викликається функція HookUpUnhandledFilter, яка
встановлює наш фільтр необроблених винятків. p>
Код функції HookUpUnhandledFilter наведено нижче: p>
bool CatUnhandledExceptionFilter:: HookUpUnhandledFilter () p>
( p>
if (m_oldSystemUnhandledFilter
== 0) p>
return false; p>
DWORD addr =
m_oldSystemUnhandledFilter; p>
DWORD old = 0; p>
if (TRUE ==
VirtualProtect ((LPVOID) addr, 5, PAGE_READWRITE, & old)) p>
( p>
unsigned char * p = (unsigned
char *) addr; p>
* p = 0xE9; p>
UINT_PTR ehFilter =
(UINT_PTR) myUnhandledExceptionFilter; p>
addr + = 5; p>
ehFilter = ehFilter - addr; p>
p ++; p>
DWORD * pp = (DWORD *) p; p>
* pp = ehFilter; p>
m_oldSystemUnhandledFilter + =
5; p>
VirtualProtect ((LPVOID) addr, 5,
old, & old); p>
return true; p>
) p>
return false; p>
) p>
ПРИМІТКА p>
Цей код буде працювати
тільки в сімействі ОС Windows NT. Справа в тому, що 2 верхніх гігабайти, де
розміщені системні бібліотеки, в Windows 9х недоступні на запис з
користувача (user) режиму. - Прим.ред. P>
Спочатку функція перевіряє, чи був знайдений системний
обробник UnhandledExceptionFilter. Якщо він не знайдено, функція повертає
false і завершує свою роботу. Потім, оскільки необхідно писати в системну
область пам'яті, що змінюються атрибути доступу до неї, що робить її доступною для
читань/запису, і записується інструкція безумовного переходу на
функцію-перехідник myUnhandledExceptionFilter. Функція-перехідник має два
мети - це викликати фільтр і повернути управління системної функції. p>
Наведені в прикладі реалізації функцій
HookUpUnhandledFilter і myUnhandledExceptionFilter є сильно спрощеними
і, звичайно, не застосовуються у реальному житті. Мало того, спроба їх застосування
може призвести до сумних наслідків. Однак вони достатні для ілюстрації
механізму підміни виклику системних функцій. У реальності ж необхідно
дизасемблювати код функції UnhandledExceptionFilter, запам'ятовувати його і потім
використовувати при поверненні управління. Але це значною мірою ускладнило б
код і могло приховати основний його зміст, тому я вирішив залишити цю реалізацію
для демонстрації самого факту можливості подібних дій. p>
Збір та зберігання інформації про стан процесу
h2>
Як було згадано вище, функція
UnhandledExceptionFilter приймає один аргумент, який є дороговказом на
_EXCEPTION_POINTERS. У цій структурі збережена інформація про стан
програми після виникнення виключення, стан регістрів, інформація про
виключення. Але цього може бути замало для відновлення реальної картини
події. Тому у своєму обробнику я спробую зібрати ту інформацію,
яка допоможе зрозуміти причину збою. Обсяг цієї інформації може залежати від
типу програми, але не можна привести весь список необхідних параметрів, здатних
допомогти при аналізі причини падіння програми. Програміст повинен сам вирішити,
який обсяг і тип інформації йому потрібна. Тут я хочу лише сказати, що
інформації, яку збирає про програму бібліотека dbghelp.dll, що поставляється
з Windows, достатньо для більшості додатків. dbghelp.dll зберігає
наступне: p>
- інформація про потоки; p>
- інформація про завантажені модулях; p>
- інформація про виключення; p>
- інформація про систему; p>
- інформація про пам'ять. p>
Після збору інформації її можна зберегти у файлі на
диску для подальшого аналізу та розбору. p>
Висновок
h2>
Операційна система Windows надає кожному
розробнику унікальну можливість обробляти виключення, що виникають у його
програмах, за допомогою механізму структурованої Обробки Винятків (SEH).
Але програми не повністю використовують можливості, надані цим
системним сервісом, і залишають частину помилок необробленими. У статті я навів
приклад того, як отримати управління в разі фатальної помилки програми, і
дав додатком отримати управління замість того, щоб бути просто вивантажені.
Цей метод може дати додатком останній шанс: p>
зберегти дані; p>
зібрати інформацію про стан на момент помилки; p>
хоча б вибачитися .... p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>