С++ exception handling под капотом. Часть 2

в 18:07, , рубрики: c++, exception, Алгоритмы

Продолжаем перевод серии статей об обработки исключений в С++

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 FTWn");
}

Напомню, вы можете посмотреть код в моем гитхаб репозитории.

И, конечно же, когда мы запустим приложение, наша персональная функция будет вызвана. Мы знаем, что мы на верном пути и у нас есть идея, что мы от нее хотим. Почему бы не использовать правильное определение функции:

_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 %in", 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 calledn");

    __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 FTWn");
}

void __cxa_end_catch()
{
    printf("end FTWn");
}

_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)

  • Версия и exceptionClass зависит от языка/ABI/компилятора, тулчейна/ нативное-не нативное исключение и т.д. Нам не стоит волноваться об этом для нашего мини-ABI, мы просто отлавливаем все исключения.
  • Actions: используется Unwind для того, чтобы сказать персональной функции что ей делать (об этом чуть позже).
  • unwind_exception: исключение, аллоцированное через __cxa_allocate_exception (унаследовано от… тут много арифметики, но по этому указателю мы можем получить доступ к нашему оригинальному исключению)
  • context: содержит все информацию касательно текущего фрейма стэка, например, LSDA. Мы будем использовать контекст для определения что в текущем стэке может обрабатывать исключения (а так же определения, нужно ли вызывать деструктор).

Что ж, мы имеет работающую (нууу, по крайней мере, линкуемую) персональну функцию. Делает она не очень много, так что далее мы будем наполнять её реальным поведением и постараемся заставить её обрабатывать исключения.

C++ exceptions под капотом: двух-фазный проход

Мы закончили предыдущую главу тем, что добавили персональную функцию, которую Unwind может вызывать. Она, в общем то, ничего не делает, пока что. Наш ABI реализует уже половину пути по бробросу исключений и их отлову, но персональная функция еще должна научиться верно выбирать блок (landing pad) обработки ошибок. Начнем эту главу с попытки понять параметры, которые принимает функция __gxx_personality_v0 и добавим ей реальное поведение: будем выводить "да, текущий блок стэка может обрабатывать исключение".

Мы уже говорили, что плевать хотели на версию или exceptionClass в нашем 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.

Следует обратить внимание на эти две важные вещи:

  1. Запуск в двух-фазном режиме отлова исключений означает, что мы можем получить оригинальный и полный stack trace исключения (если бы мы разворачивали в один проход вместе с удалением, у нас бы не было стэк-трейса, либо пришлось бы хранить его копию!).
  2. Запуская _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 phasen");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanupn");
        return _URC_INSTALL_CONTEXT;
    } else {
        printf("Personality function, errorn");
        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 exceptionn");
   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 могут быть считаны довольно просто, но есть несколько подводных камней, которые нужно учесть. Два, на самом деле:

  1. Документации о .gcc_except_table очень мало (фактически, я нашел только пару писем о нем), так что нам придется смотреть много исходных кодов, а так же разбираться в дизассемблированном коде.
  2. Несмотря на то, что сам по себе формат не адски сложный, он использует 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 phasen");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanupn");

        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: %in", cs.cs_start);
            printf("tcs_len: %in", cs.cs_len);
            printf("tcs_lp: %in", cs.cs_lp);
            printf("tcs_action: %in", 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, errorn");
        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 %in", 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 calledn");

    __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 FTWn");
}

void __cxa_end_catch()
{
    printf("end FTWn");
}

/**********************************************/

/**
 * 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 phasen");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanupn");

        // 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, errorn");
        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, которые могут быть проброшены.

Продолжение

Автор: kahi4

Источник

Поделиться новостью

* - обязательные к заполнению поля