W A S M

Поки що я рідко згадував слова_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, що знаходиться в стеку, вам, можливо, захочеться дізнатися: що відомо щодо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 по гвинтиках, давайте поглянемо, як це робиться.

Розширений кадр обробки винятків

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

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

Повернемося до розширеної структуриEXCEPTION_REGISTRATION. У восьми байтах від її початкуVisual C++резервує значенняDWORDдля того значення покажчикастека (ESP), яке він має відразу після виконання коду прологу функції. ЦейDWORD- нормальне значення регіструESPпід час виконання функції (звичайно, крім тих випадків, коли перед викликом іншої функції параметри кладуться у стек).

Мабуть, я вивалив на вас тонну інформації. Перш ніж рухатися далі, погляньмо на стандартнийSEH-фрейм, який створюєVisual C++для функції, що використовуєSEH:

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

__except_handler3 та scopetable

Я не можу відправити вас до вихіднихRTL Visual C++, щоб ви самі подивилися функцію__except_handler3, тому що її там немає. Натомість вам доведеться мати справу з написаною мною версією цієї функції в псевдокоді (див. рис. 9).

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

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

Повернімося до коду функції__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].

Програма ShowSEHFrames

Якщо зараз ви відчуваєте, що трохи приголомшені речами на зразокEXCEPTION_REGISTRATIONs,scopetables,trylevels, виразами-фільтрами та розкруткою, то скажу вам, що на початку я теж пройшов через це. Тему: “SEH лише на рівні компілятора” , не можна вивчити поступово. Окремо більшість її не має сенсу, якщо ви не сприймаєте її всю як єдине ціле. Коли я стикаюся з великою кількістю теорії, я намагаюся писати код, який використовує концепції, які я вивчаю. Якщо код працює, значить (зазвичай), що я все зрозумів правильно.

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

функції
Малюнок 11. Результат роботи програми ShowSEHFrames.

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

З Microsoft Systems Journal. Січень 1997.