Как компилятор C++ реализует обработку исключений - C++ и исключения

ОГЛАВЛЕНИЕ

 

C++ и исключения

Вспомним, что в первом разделе было сказано о структуре EXCEPTION_REGISTRATION. Она используется для регистрации обратного вызова исключения в операционной системе, которая вызывает его, когда возникает исключение.

VC++ расширяет семантику этой функции путем добавления двух дополнительных полей в конце:


struct EXCEPTION_REGISTRATION
{
   EXCEPTION_REGISTRATION *prev;
   DWORD handler;
   int   id;
   DWORD ebp;
};

VC++, с несколькими исключениями, создает структуру EXCEPTION_REGISTRATION для каждой функции как ее локальную переменную. Последнее поле структуры перекрывает ячейку, на которую указывает указатель кадра EBP. Пролог функции создает эту структуру в ее кадре стека и регистрирует структуру в операционной системе. Эпилог возвращает EXCEPTION_REGISTRATION вызывающей функции. Значение поля идентификатора будет описано в следующих разделх.

Когда VC++ компилирует функцию, он создает два набора данных для функции:

a) Функция обратного вызова исключения.
b) Структура данных, содержащая важную информацию о функции, такую как блоки захвата исключений, их адреса, тип исключения, в захвате которого они заинтересованы, и т.д. Эта структура, названная funcinfo, будет подробно рассмотрена в следующем разделе.

Рисунок 4 показывает более полную картину того, как вещи выглядят во время выполнения при рассмотрении обработки исключений. Обратный вызов исключения виджета находится в голове цепочки исключений, на которую указывает FS:[0] (который был установлен прологом виджета). Обработчик исключения передает адрес структуры funcinfo виджета функции __CxxFrameHandler, которая просматривает эту структуру данных, чтобы увидеть, есть ли в функции какой-либо блок захвата исключения, заинтересованный в захвате текущего исключения. Если функция не найдет такого блока, она возвращает значение ExceptionContinueSearch обратно операционной системе. Операционная система извлекает следующий узел из списка обработки исключений и вызывает его обработчик исключения (который является обработчиком для вызывающей функции текущей функции).

Программирование, исходники, операционные системы - Как компилятор C++ реализует обработку исключений

Это продолжается до тех пор, пока обработчик исключения не найдет блок захвата исключения, заинтересованный в захвате исключения, при этом исключение не возвращается обратно в операционную систему. Но до того, как обработчик вызовет блок захвата (он знает адрес блока захвата из структуры funcinfo, смотрите рисунок 4), он должен выполнить раскрутку стека: очистку кадров стека функций ниже кадра этой функции. Очистка кадра стека включает небольшую хитрость: обработчик исключения должен найти все локальные объекты функции, находящиеся в кадре во время исключения, и вызвать их деструкторы. Это подробно описано в следующем разделе.

Обработчик исключения поручает задачу очистки кадра обработчику исключения, связанному с этим кадром. Он начинает с начала списка обработки исключений, на который указывает FS:[0], и вызывает обработчик исключения в каждом узле, сообщая ему, что стек раскручивается. В ответ на это обработчик вызывает деструктор для всех локальных объектов в кадре и возвращает значение. Это продолжается, пока он не достигнет узла, совпадающего с самим собой.

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

После копирования исключения и активации кадра обработчик исключения вызывает блок захвата. Блок захвата возвращает ему адрес, по которому должно быть передано управление в функцию после блока try-catch. В этот момент, даже если раскрутка стека произошла, и кадры были очищены, они не были удалены, и они все еще физически занимают место в стеке. Причина этого в том, что обработчик исключений все еще выполняется, и как любая другая нормальная функция, он также использует стек для своих локальных объектов, его кадр размещается ниже кадра последней функции, из которой возникло исключение. Когда блок захвата возвращает значение, он должен уничтожить исключение. После этого момента обработчик исключения удаляет все кадры, включая свой собственный, путем установки ESP на конец кадра функции (в которую он должен передать управление) и передает управление в конец блока try-catch. Как он узнает, где находится конец кадра функции? Он никак не может узнать. Вот почему компилятор сохраняет его (через пролог функции) в кадре стека функции, чтобы обработчик исключений нашел его. Смотрите рисунок 4. Он на 16 байтов ниже указателя кадра стека EBP.

Блок захвата сам может выбросить новое исключение или повторно выбросить такое же исключение. Обработчик исключений должен наблюдать за этой ситуацией и предпринять соответствующее действие. Если блок захвата выбрасывает новое исключение, обработчик исключения должен уничтожить старое исключение. Если блок захвата устанавливает повторный выброс, то обработчик исключения должен распространить старое исключение.

Так как каждый поток имеет свой собственный стек, это означает, что каждый поток имеет свой собственный список структур EXCEPTION_REGISTRATION, на который указывает FS:[0].