Продолжаем перевод серии статей об обработки исключений в С++
1 часть
3 часть
Наша поездка в удивительном путешествии изучения работы исключений еще далека от конца, нам еще предстоит изучить что-то называемое "call frame information", помогающая библиотеке Unwind делать разворачивание стэка, а так же что компилятор пишет в чем-то, называемом LSDA, в которой определяется, какие ошибки метод может обрабатывать. А так же мы уже узнали, что большинство магии происходит в персональной функции, которую мы пока еще не видели в действии. Давайте резюмируем, что мы уже знаем о пробросе и отлове ошибок (или, точнее, что мы уже знаем о том, как брошенное будет перехвачено):
Покуда мы уже узнали все это, наступило подходящее время для написания собственной персональной функции! Наш ABI используется для того, чтобы, когда исключение было выброшено, печатать следующее:
Вернемся назад в нашу
Напомню, вы можете посмотреть актуальный код в моем гитхаб репозитории.
И, конечно же, когда мы запустим приложение, наша персональная функция будет вызвана. Мы видим, что на верном пути, а так же знаем, что от нее хотим. Почему бы тогда не использовать правильное определение функции:
Если мы положим это в наш
Скомпилируем и слинкуем все, после этого запустим и начнем анализировать каждый параметр этой функции с небольшой помощью gdb:
Что ж, мы имеем работающую (нууу, по крайней мере, линкуемую) персональну функцию. Делает она не очень много, так что далее мы будем наполнять её реальным поведением и постараемся заставить её обрабатывать исключения.
Мы закончили предыдущую главу тем, что добавили персональную функцию, которую Unwind может вызывать. Она, в общем то, ничего не делает, пока что. Наш ABI реализует уже половину работы по пробросу исключений и их отлову, но персональная функция еще должна научиться верно выбирать блок (landing pad) обработки ошибок. Начнем эту главу с попытки понять параметры, которые принимает функция __gxx_personality_v0 и добавим ей реальное поведение: будем выводить "да, текущий блок стэка может обрабатывать исключение".
Мы уже говорили, что плевать хотели на версию или класс исключения в нашем mini ABI. Что ж, забьем пока что и на контекст тоже: мы просто обрабатываем каждое исключение в первом же фрейме стэка. Не забудте подставить try/catch блок в функции прям над вызовом метода, выкидывающего исключения, иначе все поломается. Стоит так же помнить, что catch-блок будет игнорировать тип исключения. Как мы дадим функции
_Unwind_Reason_Code — возвращаемое значение персональной функцией, которая говорит функции Unwind: нашли ли мы landing pad для обработки ошибки или нет. Вернем же из нашей персональной функции *_URC_HANDLER_FOUND и посмотрим, что произойдет:
Видите это? Мы сказали раскрутчику, что нашли обработчик, и он вызвал персональную функцию еще раз! Что за черт тут происходит?
Помните параметр action? Вот как Unwind говорит нам, что конкретно он хочет, потому что обработка исключений происходит в два этапа: поиск и очистка (или _UA_SEARCH_PHASE и _UA_CLEANUP_PHASE). Вернемся обратно к нашему рецепту обработки ошибок:
Следует обратить внимание на эти две важные вещи:
Теперь мы понимаем как работает фаза поиска обработчика и можем продолжить реализацию своей персональной функции.
Мы закончили предыдущую главу, научив персональную функцию отвечать раскрутчику. Настало время добавить реальное поведение __gxx_personality_v9: научим её обрабатывать проход в две фазы.
Наша персональная функция примет вид:
Напомню: исходный код можно обнаружить на моем github репозитории.
Запустим и посмотрим, что произойдет:
Это работает, но что-то идет не так: обработчик внутри catch/try блока никогда не запускается! Это происходит из-за того, что персональная функция говорит разкрутчику "установить контекст" (т.е. продолжить выполнение), но никогда не говорит какой именно контекст. В этом случае он, вероятно, продолжает выполнение после блока landing pad, однако это, я полагаю, неопределенное поведение. Далее мы посмотрим, как указать точку, одкуда продолжать выполнение кода (landing pad), используя информацию, доступную в .gcc_except_table (наш старый друг, LSDA).
Мы оставили наш мини-ABI, способным пробрасывать исключения, и теперь мы работаем над отловом их. Мы реализовали персональную функцию, способную определять и слушать исключение, но она еще не завершена: даже если она может уведомлять раскрутчик когда ему стоит остановиться, она все еще не может запускать код внутри блока обработчика ошибки. Это уже лучше, чем то, с чего мы начинали, но нам предстоит все еще длинный путь до создания пригодной системы обработки ошибок ABI. Можем ли мы улучшить наш код?
Как мы можем сказать раскрутчику где наш landing pad, чтобы мы могли продолжить исполнение кода внутри нашего catch блока? Если мы вернемся к спецификации ABI, найдем там несколько функций управления контекстом, которые могут быть полезны:
Давайте посмотрим на эти функции с gdb. На моей машине:
Если мы исследуем эти переменные, мы увидим, что _Unwind_GetRegionStart указывает на текущий фрейм стэка (try_but_dont_catch) и что _Unwind_GetIp — IP на на позицию, где был сделан вызов следующего фрейма. _Unwind_GetRegionStart указывает нам на место, где исключение было впервые проброшего, это немного сложно для объяснения, оставим это на потом. Еще мы не видим указателя LSDA тут, но мы можем его предположить, что он сразу за кодом функции, покуда _Unwind_GetLanguageSpecificData ссылается прямиком на строчку после конца функции.
С помощью Unwind мы теперь можем получить достаточно информации о текущем фрейме стэка чтобы определить, можем ли мы обработать исключение или нет, а так же как мы должны его обработать. Последний шаг, который нам нужен перед тем, как мы определим — можем ли мы определить landing pad или нет — мы должны интерпретировать информацию CFI в конце функции. Это часть спецификации DWARF и её реализация несколько непростая. Как и в нашем ABI, мы будем использовать необходимый минимум.
Для правильной обработки исключений наша персональная функция, которую мы реализуем в нашем ABI, должна читать LSDA, чтобы узнать какой фрейм (т.е. какая функция) может обрабатывать исключение, и какое исключение, а так же чтобы узнать где landing pad (catch-блок) может быть найден. Таблица LSDA задана в CFI формате, и в этой главе мы научимся читать её.
Данные CFI могут быть считаны довольно просто, но есть несколько подводных камней, которые нужно учесть. Два, на самом деле:
Насколько я знаю, большинство данных DWARF кодируются в LEB, что является отличной идеей, чтобы запутать программистов, ну и чтобы сократить место под код для кодирования int-ов произвольной длины. К счастью, мы можем немножко считерить тут: в основном, закодированные в LEB числа читаются простым uint8_t, потому что мы не будем иметь дело с большими таблицами исключений или чего-то типа того.
Как всегда, актуальная версия кода для этой главы в репозитории.
Давайте начнем анализировать CFI напрямую с дизассемблирования и посмотрим, сможем ли мы построить что-то для чтения этих данных в нашей персональной фунции. Я переназвал метки, чтобы сделать их более человеко-удобными. LSDA имеют три секции, попробуйти определить их ниже:
Все очень просто: лишь заголовок, говорящий что мы собираемся использовать __gxx_personality_v0 как глобальную, а так же дает линкеру знать, что мы собираемся определить секцию .gcc_except_table.
Двигаемся дальше:
Тут уже гораздо больше информации. Эти метки весьма расплывчивы, но они следуют шаблону. LSDA означает специфичную для языка зону данных, L в начале означает "локальную", так что это локальные (для транслируемого модуля, .o файл). данные зоны номер один. Прочие метки следуют этому же шаблону, но я не взялся за их описание. Да и они, в общем то, нам и не нужны.
Еще один скучный заголовок, идем далее:
Это гораздо более интересно, тут мы видим таблицу вызовов воочию. Каким-то образом, во всех этих записях мы должны найти свой landing pad. В соответствии с какой-то случайной страницей в интернете, формат каждой заиси должен соответствовать структуре:
Что ж, похоже мы на правильном пути, тем не менее, мы до сих пор не знаем, почему тут 3 точки входа, когда мы определили только один langing pad. В любом случае, мы можем немного считерить: рассматривая дизассемблированный код, мы можем определить все значения CFI будут менее, чем 128, что означает что LEB-кодировка может быть считана как uchars. Это делает наш код чтения CFI гораздо проще, и теперь мы можем посмотреть, как же использовать его в нашей персональной функции далее.
Вспомним, что мы уже сделали: научились пробрасывать ошибки, написали персональную функцию __gxx_personality_v0, умеющую определять и обрабатывать ошибки, так же сообщающую разворотчику стека когда ему стоит остановиться, но она по-прежнему не умеет определять необходимый catch-блок. Так же мы научились читать LSDA, теперь осталось это все совместить!
Давайте сделаем что-то типа этого и посмотрим, на верном ли мы пути (обратите внимание, что этот код может работать только с uint8, а так же вероятно не портируемый):
Актуальный код
Как можете видеть (если запустите этот код) все точки в таблице вызовов относителные. Относительные чего? Старта функции, конечно же. Это означает, что если мы захотим получить EIP (указатель инструкции) для конкретного landing pad, все что нам нужно — это сложить: _Unwind_GetRegionStart + LSDA_Call_Site.cs_Ip!
Наконец, теперь мы способны решить нашу проблему: давайте изменим нашу персональную функцию, чтобы она выполняла корректный landing pad. Теперь нам нужно использовать другую Unwind функцию, чтобы указать, откуда мы хотим продолжить выполнение: _Unwind_SetIP. Изменим нашу персональную функцию снова, чтобы запускать первый landing pad.
Попробуйте запустить этот код и наблюдайте прекрасный вечный цикл. Можете угадать что пошло не так? Ответ в следующей главе!
В последней главе мы окончательно написали почти работающую персональную функцию. Мы можем определять каждый фрейм стэка с доступными landing pads, и, затем, говорим Unwind что конкретно мы хотим запустить. Однако мы получили небольшую проблему: для установки контекста Unwind для продолжения выполнения на корректном landing pad мы должны устанавливать текущее исключение в регистр. Это, в общем то, означает, что landing pad не хочет знать какое исключение должно быть обработано, так что он скажет лишь "Я не могу обработать это". Unwind тогда скажет "пожалуйста, попробуй следующий landing pad", однако наш ABI настолько прост, что у него даже нет идей как он должен найти другой landing pad, и просто пробует подсунуть тот же. Снова и снова. Мы, похоже, придумали самый надуманный пример для while(true)!
Исправим контекст для landing pad и чуть улучшим наш ABI:
Актуальная версия кода и гораздо более деталезированное описание LSDA.
Наконец, это работает! Мы должны получить что-то типа этого:
Конечно же, мы немного обманули Unwind: мы сказали ему, что он должен обрабатывать все подряд исключения на первом же catch блоке. Это превращает catch(Exception&) в catch(...) и весь ад сокрушится на нас, если первая функция в фрейме не имеет catch-блока. Тем не менее, мы прошли первый этап в создании очень простого ABI!
Можем ли мы улучшить наш код и заставить его корректно обрабатывать исключения в корректном фрейме? Конечно же!
C++ exceptions под капотом: несколько landing pads & учения гуру
После тяжелого пути, мы наконец сделали работающую персональную функцию, которая позволяет нам обрабатывать ошибки без помощи lbstdc++. Она беспорядочно обрабатывает все ошибки, но работает! Корректная обработка исключений — большой вопрос, на который мы еще не ответили, но если мы вернемся к LSDA, мы увидим что-то типа этого:
Тут три landing pads, даже если мы напишем единственный try/catch блок. Что здесь происходит?
Если вы внимательно посмотрите предыдущую главу, то заметите, что я добавил некоторые комментарии в определении структуры LSDA_CS:
Много интересного тут, но сначала взглянем на структуру поле за полем для этого примера:
Интересующие нас поля это start и len: в функциях с множеством try/catch блоков мы можем определить — стоит ли нам обрабатывать исключения, проверяя стоит ли указатель инструкции (IP) для текущего фрейма между start и start + len.
Это разрушает миф о том, как функция с несколькими try/catch блоков может обрабатывать несколько исключений, но мы до сих пор не знаем ответ на вопрос: почему для одного landing pad генерируются три объекта? Другие объекты помещаются как вероятное место для действий очистки или landing pad, которые могут быть проброшены.
Продолжение
1 часть
3 часть
C++ exceptions под капотом: милая персональность
Наша поездка в удивительном путешествии изучения работы исключений еще далека от конца, нам еще предстоит изучить что-то называемое "call frame information", помогающая библиотеке Unwind делать разворачивание стэка, а так же что компилятор пишет в чем-то, называемом LSDA, в которой определяется, какие ошибки метод может обрабатывать. А так же мы уже узнали, что большинство магии происходит в персональной функции, которую мы пока еще не видели в действии. Давайте резюмируем, что мы уже знаем о пробросе и отлове ошибок (или, точнее, что мы уже знаем о том, как брошенное будет перехвачено):
- компилятор транслирует throw объявление в пару cxa_allocate_exception/xca_throw
- __cxa_allocate_exception создает исключение в памяти
- __cxa_throw запускает работу разворачивания и передает исключение в низко-уровневую библиотеку разворачивания, вызывая _Unwind_RaiseException
- Разворачивание стэка использует CFI, чтобы узнать, какая сейчас функция в стеке
- Каждая функция имеет LSDA, добавляя что-то, называемое .gcc_except_table
- Разворачивание вызывает персональную функцию с текущим фреймом стэка и LSDA, которая должна продолжить разворачивать стэк, если текущая функция не имеет обработчиков исключения данного типа.
Покуда мы уже узнали все это, наступило подходящее время для написания собственной персональной функции! Наш ABI используется для того, чтобы, когда исключение было выброшено, печатать следующее:
alloc ex 1
__cxa_throw called
no one handled __cxa_throw, terminate!
Вернемся назад в нашу
mycppabi
и добавим что-то типа этого:void __gxx_personality_v0()
{
printf("Personality function FTW\n");
}
Напомню, вы можете посмотреть актуальный код в моем гитхаб репозитории.
И, конечно же, когда мы запустим приложение, наша персональная функция будет вызвана. Мы видим, что на верном пути, а так же знаем, что от нее хотим. Почему бы тогда не использовать правильное определение функции:
_Unwind_Reason_Code __gxx_personality_v0 (
int version, _Unwind_Action actions, uint64_t exceptionClass,
_Unwind_Exception* unwind_exception, _Unwind_Context* context);
Если мы положим это в наш
mycppabi.cpp
, получим:#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
namespace __cxxabiv1 {
struct __class_type_info {
virtual void foo() {}
} ti;
}
#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE];
extern "C" {
void* __cxa_allocate_exception(size_t thrown_size)
{
printf(&"alloc ex %i\n", thrown_size);
if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
return &exception_buff;
}
void __cxa_free_exception(void *thrown_exception);
#include <unwind.h>
typedef void (*unexpected_handler)(void);
typedef void (*terminate_handler)(void);
struct __cxa_exception {
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;
int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;
_Unwind_Exception unwindHeader;
};
void __cxa_throw(void* thrown_exception, struct type_info *tinfo, void (*dest)(void*))
{
printf("__cxa_throw called\n");
__cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
_Unwind_RaiseException(&header->unwindHeader);
// __cxa_throw никогда ничего не вернет
printf("no one handled __cxa_throw, terminate!\n");
exit(0);
}
void __cxa_begin_catch()
{
printf("begin FTW\n");
}
void __cxa_end_catch()
{
printf("end FTW\n");
}
_Unwind_Reason_Code __gxx_personality_v0 (
int version, _Unwind_Action actions, uint64_t exceptionClass,
_Unwind_Exception* unwind_exception, _Unwind_Context* context)
{
printf("Personality function FTW!\n");
}
}
Скомпилируем и слинкуем все, после этого запустим и начнем анализировать каждый параметр этой функции с небольшой помощью gdb:
Breakpoint 1, __gxx_personality_v0 (version=1, actions=1, exceptionClass=134514792, unwind_exception=0x804a060, context=0xbffff0f0)
- version и exceptionClass зависит от языка/ABI/компилятора, тулчейна/нативное-не нативное исключение и т.д. Не стоит волноваться об этом для нашего мини-ABI, мы просто отлавливаем все исключения.
- actions: используется Unwind для того, чтобы сказать персональной функции что ей делать (об этом чуть позже).
- unwind_exception: исключение, аллоцированное через __cxa_allocate_exception (унаследовано от… тут много арифметики, но по этому указателю мы можем получить доступ к нашему оригинальному исключению)
- context: содержит все информацию касательно текущего фрейма стэка, например, LSDA. Мы будем использовать контекст для определения того, что в текущем стэке может обрабатывать исключения (а так же определения, нужно ли вызывать деструктор).
Что ж, мы имеем работающую (нууу, по крайней мере, линкуемую) персональну функцию. Делает она не очень много, так что далее мы будем наполнять её реальным поведением и постараемся заставить её обрабатывать исключения.
C++ exceptions под капотом: двух-фазный проход
Мы закончили предыдущую главу тем, что добавили персональную функцию, которую Unwind может вызывать. Она, в общем то, ничего не делает, пока что. Наш ABI реализует уже половину работы по пробросу исключений и их отлову, но персональная функция еще должна научиться верно выбирать блок (landing pad) обработки ошибок. Начнем эту главу с попытки понять параметры, которые принимает функция __gxx_personality_v0 и добавим ей реальное поведение: будем выводить "да, текущий блок стэка может обрабатывать исключение".
Мы уже говорили, что плевать хотели на версию или класс исключения в нашем mini ABI. Что ж, забьем пока что и на контекст тоже: мы просто обрабатываем каждое исключение в первом же фрейме стэка. Не забудте подставить try/catch блок в функции прям над вызовом метода, выкидывающего исключения, иначе все поломается. Стоит так же помнить, что catch-блок будет игнорировать тип исключения. Как мы дадим функции
_Unwind_
знать, что мы хотим обработать исключение?_Unwind_Reason_Code — возвращаемое значение персональной функцией, которая говорит функции Unwind: нашли ли мы landing pad для обработки ошибки или нет. Вернем же из нашей персональной функции *_URC_HANDLER_FOUND и посмотрим, что произойдет:
alloc ex 1
__cxa_throw called
Personality function FTW
Personality function FTW
no one handled __cxa_throw, terminate!
Видите это? Мы сказали раскрутчику, что нашли обработчик, и он вызвал персональную функцию еще раз! Что за черт тут происходит?
Помните параметр action? Вот как Unwind говорит нам, что конкретно он хочет, потому что обработка исключений происходит в два этапа: поиск и очистка (или _UA_SEARCH_PHASE и _UA_CLEANUP_PHASE). Вернемся обратно к нашему рецепту обработки ошибок:
- __cxa_throw/__cxa_allocate_exception создают исключение и передают их в низкоуровневую библиотеку раскрутки стэка, именуемую _Unwind_RaiseException
- Раскрутчик использует CFI чтобы узнать какая функция в данный момент в стэке (чтобы узнать как начинать разворачивать функцию)
- Каждая функция имеет LSDA-часть, добавленную во что-то, именуемое ".gcc_except_table"
- Раскрутчик пытается определить landing pad для исключения:
- Раскрутчик вызывает персональную функцию с параметром action: _UA_SEARCH_PHASE и параметром contex: указателем на текущий фрейм стэка.
- Персональная функция проверяет, может ли текущий фрейм-стэк обработать это исключение, анализируя LSDA
- Если исключение может быть обработано, она возвращает _URC_HANDLER_FOUND
- Если исключение обрабатывать некому, будет возвращено _URC_CONTINE_UNWIND, и раскрутчик продолжит попытки найти обработчика со следующим фреймом стэка.
- Если ни один landing pad не найден, будет вызван обработчик по-умолчанию (обычно это std::terminate).
- Если landing pad найден:
- Раскрутчик начнет обрабатывать стэк заново, вызывая персональную функцию с параметром action _UA_CLEANUP_PHASE.
- Персональная функция проверяет: может ли текущее окно стэка обрабатывать это исключение, или нет.
- Если не может, то она запускает функции очисти, описанные в LSDA, (и очищает размещенные в текущем фрейме стэка объекты вызовом деструкторов), и говорит раскрутчику продолжить со следующим фреймом.
- Если может обработать, тогда не запускает никакой код очистки, а лишь говорит раскрутчику, что мы хотим продолжить выполнение на такущем landing pad.
Следует обратить внимание на эти две важные вещи:
- Запуск в двух-фазном режиме отлова исключений означает, что мы можем получить оригинальный и полный stack trace исключения (если бы мы разворачивали в один проход вместе с удалением, у нас бы не было стэк-трейса, либо пришлось бы хранить его копию!).
- Запуская _UA_CLEANUP_PHASE и повторно вызывая для каждого фрейма, даже если мы знаем, что фрейм может обрабатывать исключение, так же очень важно: персональная функция имеет возможность вызвать все деструкторы для объектов, созданных в этой области видимости. Это то, что делает исключения RAII (Resource Acquisition Is Initialization, Получение ресурса есть инициализация) safe идиомой.
Теперь мы понимаем как работает фаза поиска обработчика и можем продолжить реализацию своей персональной функции.
C++ exceptions под капотом: ловим наше первое исключение
Мы закончили предыдущую главу, научив персональную функцию отвечать раскрутчику. Настало время добавить реальное поведение __gxx_personality_v9: научим её обрабатывать проход в две фазы.
Наша персональная функция примет вид:
_Unwind_Reason_Code __gxx_personality_v0 (
int version, _Unwind_Action actions, uint64_t exceptionClass,
_Unwind_Exception* unwind_exception, _Unwind_Context* context)
{
if (actions &_UA_SEARCH_PHASE)
{
printf("Personality function, lookup phase\n");
return _URC_HANDLER_FOUND;
} else if (actions & _UA_CLEANUP_PHASE) {
printf("Personality function, cleanup\n");
return _URC_INSTALL_CONTEXT;
} else {
printf("Personality function, error\n");
return _URC_FATAL_PHASE1_ERROR;
}
}
Напомню: исходный код можно обнаружить на моем github репозитории.
Запустим и посмотрим, что произойдет:
alloc ex 1
__cxa_throw called
Personality function, lookup phase
Personality function, cleanup
try_but_dont_catch handled the exception
catchit handled the exception
Это работает, но что-то идет не так: обработчик внутри catch/try блока никогда не запускается! Это происходит из-за того, что персональная функция говорит разкрутчику "установить контекст" (т.е. продолжить выполнение), но никогда не говорит какой именно контекст. В этом случае он, вероятно, продолжает выполнение после блока landing pad, однако это, я полагаю, неопределенное поведение. Далее мы посмотрим, как указать точку, одкуда продолжать выполнение кода (landing pad), используя информацию, доступную в .gcc_except_table (наш старый друг, LSDA).
C++ exceptions под капотом: Unwind информация текущего фрейма
Мы оставили наш мини-ABI, способным пробрасывать исключения, и теперь мы работаем над отловом их. Мы реализовали персональную функцию, способную определять и слушать исключение, но она еще не завершена: даже если она может уведомлять раскрутчик когда ему стоит остановиться, она все еще не может запускать код внутри блока обработчика ошибки. Это уже лучше, чем то, с чего мы начинали, но нам предстоит все еще длинный путь до создания пригодной системы обработки ошибок ABI. Можем ли мы улучшить наш код?
Как мы можем сказать раскрутчику где наш landing pad, чтобы мы могли продолжить исполнение кода внутри нашего catch блока? Если мы вернемся к спецификации ABI, найдем там несколько функций управления контекстом, которые могут быть полезны:
- _Unwind_GetLanguageSpecificData для получения LSDA текущего фрейма. Нам стоит научиться искать landing pads и дестркуторы используя эту функцию
- _Unwind_GetRegionStart для получения указателя на инструкцию в начале текущей функции (указатель на функцию, соответствующую текущему фрейму)
- _Unwind_GetIP для получения указателя инструкции внутри текущего фрейма стэка (instruction pointer) (указатель на место, где вызов функции в следующем фрейме будет завершен. Станет понятнее с примерами далее).
Давайте посмотрим на эти функции с gdb. На моей машине:
Breakpoint 1, __gxx_personality_v0 (version=1, actions=6, exceptionClass=134515400, unwind_exception=0x804a060, context=0xbffff0f0)
at mycppabi.cpp:77
84 const uint8_t* lsda = (const uint8_t*)_Unwind_GetLanguageSpecificData(context);
85 uintptr_t ip = _Unwind_GetIP(context) - 1;
86 uintptr_t funcStart = _Unwind_GetRegionStart(context);
87 uintptr_t ipOffset = ip - funcStart;
Если мы исследуем эти переменные, мы увидим, что _Unwind_GetRegionStart указывает на текущий фрейм стэка (try_but_dont_catch) и что _Unwind_GetIp — IP на на позицию, где был сделан вызов следующего фрейма. _Unwind_GetRegionStart указывает нам на место, где исключение было впервые проброшего, это немного сложно для объяснения, оставим это на потом. Еще мы не видим указателя LSDA тут, но мы можем его предположить, что он сразу за кодом функции, покуда _Unwind_GetLanguageSpecificData ссылается прямиком на строчку после конца функции.
_Unwind_GetIP = (void *) 0x804861d
_Unwind_GetRegionStart = (void *) 0x8048612
_Unwind_GetLanguageSpecificData = (void *) 0x8048e3c
function pointer to try_but_dont_catch = 0x8048612 &<try_but_dont_catch()>
(gdb) disassemble /m try_but_dont_catch
Dump of assembler code for function try_but_dont_catch():
10 void try_but_dont_catch() {
[...]
11 try {
12 raise();
0x08048619 <+7>: call 0x80485e8 <raise()>
13 } catch(Fake_Exception&) {
0x08048651 <+63>: call 0x804874a <__cxa_begin_catch()>
0x08048665 <+83>: call 0x804875e <__cxa_end_catch()>
0x0804866a <+88>: jmp 0x804861e <try_but_dont_catch()+12>
14 printf("Caught a Fake_Exception!\n");
0x08048659 <+71>: movl $0x8048971,(%esp)
0x08048660 <+78>: call 0x80484c0 <puts@plt>
15 }
16
17 printf("try_but_dont_catch handled the exception\n");
0x0804861e <+12>;: movl $0x8048948,(%esp)
0x08048625 <+19>: call 0x80484c0 <puts@plt>
18 }
0x0804862a <+24>: add $0x24,%esp
С помощью Unwind мы теперь можем получить достаточно информации о текущем фрейме стэка чтобы определить, можем ли мы обработать исключение или нет, а так же как мы должны его обработать. Последний шаг, который нам нужен перед тем, как мы определим — можем ли мы определить landing pad или нет — мы должны интерпретировать информацию CFI в конце функции. Это часть спецификации DWARF и её реализация несколько непростая. Как и в нашем ABI, мы будем использовать необходимый минимум.
C++ exceptions под капотом: чтение таблицы CFI
Для правильной обработки исключений наша персональная функция, которую мы реализуем в нашем ABI, должна читать LSDA, чтобы узнать какой фрейм (т.е. какая функция) может обрабатывать исключение, и какое исключение, а так же чтобы узнать где landing pad (catch-блок) может быть найден. Таблица LSDA задана в CFI формате, и в этой главе мы научимся читать её.
Данные CFI могут быть считаны довольно просто, но есть несколько подводных камней, которые нужно учесть. Два, на самом деле:
- Документации о .gcc_except_table очень мало (фактически, я нашел только пару писем о нем), так что нам придется смотреть много исходных кодов, а так же разбираться в дизассемблированном коде.
- Несмотря на то, что сам по себе формат не адски сложный, он использует LEB (Little Endian Base), что делает чтение этой таблицы не особо простой.
Насколько я знаю, большинство данных DWARF кодируются в LEB, что является отличной идеей, чтобы запутать программистов, ну и чтобы сократить место под код для кодирования int-ов произвольной длины. К счастью, мы можем немножко считерить тут: в основном, закодированные в LEB числа читаются простым uint8_t, потому что мы не будем иметь дело с большими таблицами исключений или чего-то типа того.
Как всегда, актуальная версия кода для этой главы в репозитории.
Давайте начнем анализировать CFI напрямую с дизассемблирования и посмотрим, сможем ли мы построить что-то для чтения этих данных в нашей персональной фунции. Я переназвал метки, чтобы сделать их более человеко-удобными. LSDA имеют три секции, попробуйти определить их ниже:
.local_frame_entry:
.globl __gxx_personality_v0
.section .gcc_except_table,"a",@progbits
.align 4
Все очень просто: лишь заголовок, говорящий что мы собираемся использовать __gxx_personality_v0 как глобальную, а так же дает линкеру знать, что мы собираемся определить секцию .gcc_except_table.
Двигаемся дальше:
.local_lsda_1:
# Определение типа кодирования. Нас это не интересует
.byte 0xff
# Это определяет начало landing pads; если 0, func's ptr
# будет присвоено (_Unwind_GetRegionStart)
.byte 0
# Длина зоны LSDA: проверка что LLSDATT1 и LLSDATTD1 указывают
# конец и начало LSDA, соответственно
.uleb128 .local_lsda_end - .local_lsda_call_site_table_header
Тут уже гораздо больше информации. Эти метки весьма расплывчивы, но они следуют шаблону. LSDA означает специфичную для языка зону данных, L в начале означает "локальную", так что это локальные (для транслируемого модуля, .o файл). данные зоны номер один. Прочие метки следуют этому же шаблону, но я не взялся за их описание. Да и они, в общем то, нам и не нужны.
.local_lsda_call_site_table_header:
# Encoding of items in the landing pad table. Again, we don't care.
.byte 0x1.
# The length of the call site table (ie the landing pads)
.uleb128 .local_lsda_call_site_table_end - .local_lsda_call_site_table
Еще один скучный заголовок, идем далее:
.local_lsda_call_site_table:
.uleb128 .LEHB0-.LFB1
.uleb128 .LEHE0-.LEHB0
.uleb128 .L8-.LFB1
.uleb128 0x1
.uleb128 .LEHB1-.LFB1
.uleb128 .LEHE1-.LEHB1
.uleb128 0
.uleb128 0
.uleb128 .LEHB2-.LFB1
.uleb128 .LEHE2-.LEHB2
.uleb128 .L9-.LFB1
.uleb128 0
.local_lsda_call_site_table_end:
Это гораздо более интересно, тут мы видим таблицу вызовов воочию. Каким-то образом, во всех этих записях мы должны найти свой landing pad. В соответствии с какой-то случайной страницей в интернете, формат каждой заиси должен соответствовать структуре:
struct lsda_call_site_entry {
// Старт IP области
size_t cs_start;
// Размер IP области
size_t cs_len;
// Landing pad адрес
size_t cs_lp;
// Смещение в таблице функций
size_t cs_action;
};
Что ж, похоже мы на правильном пути, тем не менее, мы до сих пор не знаем, почему тут 3 точки входа, когда мы определили только один langing pad. В любом случае, мы можем немного считерить: рассматривая дизассемблированный код, мы можем определить все значения CFI будут менее, чем 128, что означает что LEB-кодировка может быть считана как uchars. Это делает наш код чтения CFI гораздо проще, и теперь мы можем посмотреть, как же использовать его в нашей персональной функции далее.
C++ exceptions под капотом: и внезапно, рефлексии в C++
Вспомним, что мы уже сделали: научились пробрасывать ошибки, написали персональную функцию __gxx_personality_v0, умеющую определять и обрабатывать ошибки, так же сообщающую разворотчику стека когда ему стоит остановиться, но она по-прежнему не умеет определять необходимый catch-блок. Так же мы научились читать LSDA, теперь осталось это все совместить!
Давайте сделаем что-то типа этого и посмотрим, на верном ли мы пути (обратите внимание, что этот код может работать только с uint8, а так же вероятно не портируемый):
struct LSDA_Header {
uint8_t lsda_start_encoding;
uint8_t lsda_type_encoding;
uint8_t lsda_call_site_table_length;
};
struct LSDA_Call_Site_Header {
uint8_t encoding;
uint8_t length;
};
struct LSDA_Call_Site {
LSDA_Call_Site(const uint8_t *ptr) {
cs_start = ptr[0];
cs_len = ptr[1];
cs_lp = ptr[2];
cs_action = ptr[3];
}
uint8_t cs_start;
uint8_t cs_len;
uint8_t cs_lp;
uint8_t cs_action;
};
_Unwind_Reason_Code __gxx_personality_v0 (
int version, _Unwind_Action actions, uint64_t exceptionClass,
_Unwind_Exception* unwind_exception, _Unwind_Context* context)
{
if (actions & _UA_SEARCH_PHASE)
{
printf("Personality function, lookup phase\n");
return _URC_HANDLER_FOUND;
} else if (actions & _UA_CLEANUP_PHASE) {
printf("Personality function, cleanup\n");
const uint8_t* lsda = (const uint8_t*)
_Unwind_GetLanguageSpecificData(context);
LSDA_Header *header = (LSDA_Header*)(lsda);
LSDA_Call_Site_Header *cs_header = (LSDA_Call_Site_Header*)
(lsda + sizeof(LSDA_Header));
size_t cs_in_table = cs_header->length / sizeof(LSDA_Call_Site);
// Мы должны определить cs_table_base как uint8, мы рискуем наткнуться
// на невыравненный доступ
const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header)
+ sizeof(LSDA_Call_Site_Header);
// Проходим через всю call site таблицу
for (size_t i=0; i < cs_in_table; ++i)
{
const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)];
LSDA_Call_Site cs(offset);
printf("Found a CS:\n");
printf("\tcs_start: %i\n", cs.cs_start);
printf("\tcs_len: %i\n", cs.cs_len);
printf("\tcs_lp: %i\n", cs.cs_lp);
printf("\tcs_action: %i\n", cs.cs_action);
}
uintptr_t ip = _Unwind_GetIP(context);
uintptr_t funcStart = _Unwind_GetRegionStart(context);
uintptr_t ipOffset = ip - funcStart;
return _URC_INSTALL_CONTEXT;
} else {
printf("Personality function, error\n");
return _URC_FATAL_PHASE1_ERROR;
}
}
}
Актуальный код
Как можете видеть (если запустите этот код) все точки в таблице вызовов относителные. Относительные чего? Старта функции, конечно же. Это означает, что если мы захотим получить EIP (указатель инструкции) для конкретного landing pad, все что нам нужно — это сложить: _Unwind_GetRegionStart + LSDA_Call_Site.cs_Ip!
Наконец, теперь мы способны решить нашу проблему: давайте изменим нашу персональную функцию, чтобы она выполняла корректный landing pad. Теперь нам нужно использовать другую Unwind функцию, чтобы указать, откуда мы хотим продолжить выполнение: _Unwind_SetIP. Изменим нашу персональную функцию снова, чтобы запускать первый landing pad.
const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header)
+ sizeof(LSDA_Call_Site_Header);
for (size_t i=0; i < cs_in_table; ++i)
{
const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)];
LSDA_Call_Site cs(offset);
if (cs.cs_lp)
{
uintptr_t func_start = _Unwind_GetRegionStart(context);
_Unwind_SetIP(context, func_start + cs.cs_lp);
break;
}
}
return _URC_INSTALL_CONTEXT;
Попробуйте запустить этот код и наблюдайте прекрасный вечный цикл. Можете угадать что пошло не так? Ответ в следующей главе!
C++ exceptions под капотом: установка контекста для landing pad
В последней главе мы окончательно написали почти работающую персональную функцию. Мы можем определять каждый фрейм стэка с доступными landing pads, и, затем, говорим Unwind что конкретно мы хотим запустить. Однако мы получили небольшую проблему: для установки контекста Unwind для продолжения выполнения на корректном landing pad мы должны устанавливать текущее исключение в регистр. Это, в общем то, означает, что landing pad не хочет знать какое исключение должно быть обработано, так что он скажет лишь "Я не могу обработать это". Unwind тогда скажет "пожалуйста, попробуй следующий landing pad", однако наш ABI настолько прост, что у него даже нет идей как он должен найти другой landing pad, и просто пробует подсунуть тот же. Снова и снова. Мы, похоже, придумали самый надуманный пример для while(true)!
Исправим контекст для landing pad и чуть улучшим наш ABI:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
namespace __cxxabiv1 {
struct __class_type_info {
virtual void foo() {}
} ti;
}
#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE];
extern "C" {
void* __cxa_allocate_exception(size_t thrown_size)
{
printf("alloc ex %i\n", thrown_size);
if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
return &exception_buff;
}
void __cxa_free_exception(void *thrown_exception);
#include <unwind.h>
typedef void (*unexpected_handler)(void);
typedef void (*terminate_handler)(void);
struct __cxa_exception {
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;
int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;
_Unwind_Exception unwindHeader;
};
void __cxa_throw(void* thrown_exception,
struct type_info *tinfo,
void (*dest)(void*))
{
printf("__cxa_throw called\n");
__cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
_Unwind_RaiseException(&header->unwindHeader);
// __cxa_throw never returns
printf("no one handled __cxa_throw, terminate!\n");
exit(0);
}
void __cxa_begin_catch()
{
printf("begin FTW\n");
}
void __cxa_end_catch()
{
printf("end FTW\n");
}
/**********************************************/
/**
* The LSDA is a read only place in memory; we'll create a typedef for
* this to avoid a const mess later on; LSDA_ptr refers to readonly and
* &LSDA_ptr will be a non-const pointer to a const place in memory
*/
typedef const uint8_t* LSDA_ptr;
struct LSDA_Header {
/**
* Read the LSDA table into a struct; advances the lsda pointer
* as many bytes as read
*/
LSDA_Header(LSDA_ptr *lsda) {
LSDA_ptr read_ptr = *lsda;
// Copy the LSDA fields
start_encoding = read_ptr[0];
type_encoding = read_ptr[1];
ttype = read_ptr[2];
// Advance the lsda pointer
*lsda = read_ptr + sizeof(LSDA_Header);
}
uint8_t start_encoding;
uint8_t type_encoding;
uint8_t ttype;
};
struct LSDA_CS_Header {
// Same as other LSDA constructors
LSDA_CS_Header(LSDA_ptr *lsda) {
LSDA_ptr read_ptr = *lsda;
encoding = read_ptr[0];
length = read_ptr[1];
*lsda = read_ptr + sizeof(LSDA_CS_Header);
}
uint8_t encoding;
uint8_t length;
};
struct LSDA_CS {
// Same as other LSDA constructors
LSDA_CS(LSDA_ptr *lsda) {
LSDA_ptr read_ptr = *lsda;
start = read_ptr[0];
len = read_ptr[1];
lp = read_ptr[2];
action = read_ptr[3];
*lsda = read_ptr + sizeof(LSDA_CS);
}
// Note start, len and lp would be void*'s, but they are actually relative
// addresses: start and lp are relative to the start of the function, len
// is relative to start
// Offset into function from which we could handle a throw
uint8_t start;
// Length of the block that might throw
uint8_t len;
// Landing pad
uint8_t lp;
// Offset into action table + 1 (0 means no action)
// Used to run destructors
uint8_t action;
};
/**********************************************/
_Unwind_Reason_Code __gxx_personality_v0 (
int version,
_Unwind_Action actions,
uint64_t exceptionClass,
_Unwind_Exception* unwind_exception,
_Unwind_Context* context)
{
if (actions & _UA_SEARCH_PHASE)
{
printf("Personality function, lookup phase\n");
return _URC_HANDLER_FOUND;
} else if (actions & _UA_CLEANUP_PHASE) {
printf("Personality function, cleanup\n");
// Pointer to the beginning of the raw LSDA
LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context);
// Read LSDA headerfor the LSDA
LSDA_Header header(&lsda);
// Read the LSDA CS header
LSDA_CS_Header cs_header(&lsda);
// Calculate where the end of the LSDA CS table is
const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length;
// Loop through each entry in the CS table
while (lsda < lsda_cs_table_end)
{
LSDA_CS cs(&lsda);
if (cs.lp)
{
int r0 = __builtin_eh_return_data_regno(0);
int r1 = __builtin_eh_return_data_regno(1);
_Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception));
// Note the following code hardcodes the exception type;
// we'll fix that later on
_Unwind_SetGR(context, r1, (uintptr_t)(1));
uintptr_t func_start = _Unwind_GetRegionStart(context);
_Unwind_SetIP(context, func_start + cs.lp);
break;
}
}
return _URC_INSTALL_CONTEXT;
} else {
printf("Personality function, error\n");
return _URC_FATAL_PHASE1_ERROR;
}
}
}
Актуальная версия кода и гораздо более деталезированное описание LSDA.
Наконец, это работает! Мы должны получить что-то типа этого:
./app
alloc ex 1
__cxa_throw called
Personality function, lookup phase
Personality function, cleanup
begin FTW
Caught a Fake_Exception!
end FTW
try_but_dont_catch handled the exception
catchit handled the exception
Конечно же, мы немного обманули Unwind: мы сказали ему, что он должен обрабатывать все подряд исключения на первом же catch блоке. Это превращает catch(Exception&) в catch(...) и весь ад сокрушится на нас, если первая функция в фрейме не имеет catch-блока. Тем не менее, мы прошли первый этап в создании очень простого ABI!
Можем ли мы улучшить наш код и заставить его корректно обрабатывать исключения в корректном фрейме? Конечно же!
C++ exceptions под капотом: несколько landing pads & учения гуру
После тяжелого пути, мы наконец сделали работающую персональную функцию, которая позволяет нам обрабатывать ошибки без помощи lbstdc++. Она беспорядочно обрабатывает все ошибки, но работает! Корректная обработка исключений — большой вопрос, на который мы еще не ответили, но если мы вернемся к LSDA, мы увидим что-то типа этого:
.local_lsda_call_site_table:
.uleb128 .LEHB0-.LFB1
.uleb128 .LEHE0-.LEHB0
.uleb128 .L8-.LFB1
.uleb128 0x1
.uleb128 .LEHB1-.LFB1
.uleb128 .LEHE1-.LEHB1
.uleb128 0
.uleb128 0
.uleb128 .LEHB2-.LFB1
.uleb128 .LEHE2-.LEHB2
.uleb128 .L9-.LFB1
.uleb128 0
.local_lsda_call_site_table_end:
Тут три landing pads, даже если мы напишем единственный try/catch блок. Что здесь происходит?
Если вы внимательно посмотрите предыдущую главу, то заметите, что я добавил некоторые комментарии в определении структуры LSDA_CS:
struct LSDA_CS {
// len и lp должны быть void*'s, однако они
// адреса: start and lp относительны начала функции, len
// относительна начала
// смещение в функции, которое позволяет обрабатывать ошибку
uint8_t start;
// длина обработчика
uint8_t len;
// Landing pad
uint8_t lp;
// Смещени action table + 1 (0 означает "нет действий")
// используется для запуска деструктора
uint8_t action;
};
Много интересного тут, но сначала взглянем на структуру поле за полем для этого примера:
void foo() {
L0:
try {
do_something();
L1:
} catch (const Exception1& ex) {
...
} catch (const Exception2& ex) {
...
} catch (const ExceptionN& ex) {
...
} catch (...) {
}
L2:
}
- Ip: смещение с начала функции, где landing pad начинаются. Значение в этом примере должно быть L1 — addr_of(foo)
- action: смещение в таблице действий. Используется для действий очистки во время разворачивания стэка. Мы пока не дошли до этого, просто проигнорируем пока что.
- start: смещение с начала функции где try-блок начинается. В нашем примере это должен быть L0 — addr_of(foo)
- len: длина try-блока. В примере это должно быть L1-L0
Интересующие нас поля это start и len: в функциях с множеством try/catch блоков мы можем определить — стоит ли нам обрабатывать исключения, проверяя стоит ли указатель инструкции (IP) для текущего фрейма между start и start + len.
Это разрушает миф о том, как функция с несколькими try/catch блоков может обрабатывать несколько исключений, но мы до сих пор не знаем ответ на вопрос: почему для одного landing pad генерируются три объекта? Другие объекты помещаются как вероятное место для действий очистки или landing pad, которые могут быть проброшены.
Продолжение