Как компилятор C++ реализует обработку исключений - Очистка кадра стека
ОГЛАВЛЕНИЕ
Очистка кадра стека
Стандарт C++ говорит, что когда стек раскручивается, должен быть вызван деструктор для всех локальных объектов, существующих в момент исключения. Рассмотрите:
int g_i = 0;
void foo()
{
T o1, o2;
{
T o3;
}
10/g_i; //исключение возникает здесь
T o4;
//...
}
Когда возникает исключение, локальные объекты o1 и o2 существуют в кадре foo, в то время как время существования o3 закончилось. O4 вообще не был создан. Обработчик исключений должен знать об этом факте и должен вызвать деструктор для o1 и o2.
Как сказано ранее, компилятор добавляет код в функцию в нескольких специальных точках, который регистрирует текущее состояние времени выполнения функции, по мере того как выполнение продолжается. Он назначает идентификаторы этим специальным областям в функции. Например, точка входа в блок try – специальная область. Как сказано ранее, когда будет выполнен вход в блок try, компилятор добавит оператор в функцию в точке, который запишет идентификатор начала блока try в кадр функции.
Другая специальная область в функции находится там, где создается или уничтожается локальный объект. Иначе говоря, компилятор присваивает каждому локальному объекту уникальный идентификатор. Когда компилятор встречает такое определение объекта:
void foo()
{
T t1;
//.
}
Он добавляет оператор после определения (после точки, когда объект будет создан) для записи значения его идентификатора в кадр:
void foo()
{
T t1;
_id = t1_id; //оператор, добавленный компилятором
//.
}
Компилятор создает скрытую локальную переменную (обозначенную в коде выше как _id), которая перекрывается с полем id структуры EXCEPTION_REGISTRATION. Аналогично, он добавляет оператор перед вызовом деструктора для объекта, чтобы записать идентификатор предыдущей области.
Когда обработчик исключений должен очистить кадр, он читает значение идентификатора из кадра (поле id структуры EXCEPTION_REGISTRATION или 4 байта ниже указателя кадра, EBP). Этот идентификатор указывает, что код в функции вплоть до точки, которой соответствует текущий идентификатор, выполнился без каких-либо исключений. Все объекты выше этой точки были созданы. Деструкторы для всех или некоторых из объектов выше этой точки должны быть вызваны. Некоторые из этих объектов могли быть уничтожены, если они являются частью подблока. Деструкторы для них не должны вызываться.
Компилятор создает еще одну структуру данных для функции, unwindtable(мое имя), которая является массивом структур раскручивания. Эта таблица доступна через структуру funcinfo. Смотрите рисунок 5. Для каждой специальной области в функции есть одна структура раскручивания. Элементы структуры появляются в unwindtable в том же порядке, в каком их соответствующие области появляются в функции. Структура раскручивания, соответствующая объектам, представляет интерес (помните, каждое определение объекта указывает на специальную область и имеет связанный с ним идентификатор). Он содержит информацию для уничтожения объекта. Когда компилятор встречает определение объекта, он создает короткую подпрограмму, которой известен адрес объекта в кадре (или его смещение от указателя кадра), и уничтожает этот объект. Одно из полей структуры раскручивания содержит адрес этой подпрограммы:
typedef void (*CLEANUP_FUNC)();
struct unwind
{
int prev;
CLEANUP_FUNC cf;
};
структура раскручивания для блока try имеет нулевое значение для второго поля. Поле prev означает, что unwintable – связанный список структур раскручивания. Когда обработчик исключения должен очистить кадр, он читает текущий идентификатор из кадра и использует его как индекс в таблице раскручивания. Он читает структуру раскручивания по этому индексу и вызывает функцию очистки, как задается вторым полем структуры. Это уничтожает объект, соответствующий этому идентификатору. Затем обработчик читает предыдущую структуру раскручивания из таблицы раскручивания по индексу, указанному в поле prev. Это продолжается, пока не будет достигнут конец списка (prev равен -1). Рисунок 7 показывает, как таблица раскручивания может выглядеть для функции на рисунке.
Рассмотрим случай нового оператора:
T* p = new T();
Сначала система выделяет память для T и затем вызывает конструктор. Если конструктор выбрасывает исключение, то система должна освободить память, выделенную для этого объекта. Для достижения этого VC++ присваивает идентификатор каждому новому оператору для типа, имеющего нетривиальный конструктор. Есть соответствующая запись в таблице раскручивания, процедура очистки освобождает выделенный участок памяти. Перед вызовом конструктора она сохраняет идентификатор для выделения в структуре EXCEPTION_REGISTRATION. После того, как конструктор успешно выполнил возврат, она восстанавливает идентификатор предыдущей специальной области.
Более того, объект может быть частично создан, когда конструктор выбрасывает исключение. Если он имеет подобъекты-члены или подобъекты базового класса, и некоторые из них были созданы в момент исключения, для этих объектов должен быть вызван деструктор. Компилятор создает такой же набор данных для конструктора, как и для любой нормальной функции, чтобы выполнить эти задачи.
Обработчик исключений вызывает пользовательские деструкторы во время раскручивания стека. Деструктор может выбросить исключение. Стандарт C++ говорит, что во время раскручивания стека деструктор не вправе выбрасывать исключение. Если он это делает, система вызывает std::terminate.