Когда я работал над своей курсовой про то, как передавать С++ исключения из ядра ОС пользовательским программам, я изучал, как работают исключения в С++ и в этом мне очень помогла эта серия статей с Хабра. Теперь я хочу дополнить некоторые моменты, которые недостают в этой серии статей.

Запуск кода автора изначальной серии статей

Первое, что вы заметите, если будете изучать работу исключений С++ по той серии - это то, что код, приведённый там, падает с Segmentation Fault, если вы используете 64-ёх битный процессор.

Дело в том, что в 64-ёх битном режиме компилятор g++ пишет указатель на структуру std::type_info в совершенно диком виде. Чтобы его раскодировать, нужен такой код:

void* read_int_tbltype(const int* ptr, uint8_t type_encoding) {
    if (type_encoding == 3) {
          return (void *) *ptr; // случай кодировки здорового человека
          // так было в 32-ух битном gcc и в clang
    } else if (type_encoding == 0x9b) { // 64-ёх битный режим курильщика (gcc)
        uintptr_t result = *ptr;
        if (result == 0) {
            return nullptr;
        }
        result += (uintptr_t) ptr;
        result = *((uintptr_t*)result);
        return (void *)result;
    } else {
        exit(0);
    }
}

Дополнения к функциям C++ ABI, чтобы сделать реализацию более корректной

В репозитории автора изначальной серии статей функции __cxa_begin_catch и __cxa_end_catch не делают ничего, кроме отладочного вывода.

В реальности у этих двух функций две задачи: во-первых,__cxa_begin_catch должна получить по указателю структуру _Unwind_Exception и вернуть указатель на выброшенное исключение, чтобы код catch блока мог получить к нему доступ - иначе читать его он не сможет. Во-вторых, эти две функции должны обеспечить вызов деструктора выброшенного исключения и освобождение памяти, выделенной для исключения и информацию об исключении. Для этого первая функция должна положить это исключение на стек исключений, а вторая - вызвать его деструктор, если надо, а потом осводобить память. Стек исключений нужен на случай, если одновременно в обработке будут два исключения. Он должен быть thread_local. Итоговый код получается такой:

  // for freeing exceptions
  thread_local __cxa_exception* last_exception;

  void* __cxa_begin_catch(void* raw_unwind_exception) throw() {
        auto unwind_exception = (_Unwind_Exception *)raw_unwind_exception;
        __cxa_exception* exception_header = (__cxa_exception*)(unwind_exception + 1) - 1;
        exception_header->nextException = last_exception;
        last_exception = exception_header;
        return exception_header + 1;
    }


    void __cxa_end_catch() {
        // we need do destroy the last exception
        __cxa_exception* exception_header = last_exception;
        last_exception = exception_header->nextException;
        if (exception_header->exceptionDestructor) {
            (*exception_header->exceptionDestructor)(exception_header + 1);
        }
        free(exception_header->exceptionTypeName);
        __cxa_free_exception((char *)(exception_header + 1));
    }

Ещё есть функция __cxa_get_exception_ptr она вызывается в том случае, если исключение ловится по ссылке, поэтому код из репозитория автора оригинальной статьи компилируется без неё. Эта функция в основном аналогична функции __cxa_begin_catch и возвращает указатель на оригинальное исключение.

  void* __cxa_get_exception_ptr(_Unwind_Exception* unwind_exception) {
        __cxa_exception* exception_header = (__cxa_exception*)(unwind_exception + 1) - 1;
        return exception_header + 1;
    }

Кажется, это всё, что я дополнил к реализации из репозитория автора оригинальной статьи. Мой код можно посмотреть здесь. Единственное (кажется), что там не реализовано - это то, что catch блок может поймать не только то исключение, которое указано у него, но и те, что от него унаследованы.

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

Комментарии (3)


  1. Justlexa
    20.11.2022 21:44
    +1

    Дело в том, что в 64-ёх битном режиме компилятор g++ пишет указатель на структуру std::type_info в совершенно диком виде.

    Это формат кодировки указателей в отладочной информации DWARF.


    1. vda19999 Автор
      20.11.2022 21:47
      -1

      А почему нельзя писать как есть, как делает clang?


    1. Tuxman
      21.11.2022 02:32
      +1

      Особенно доставляет, когда начинают какие-то оптимизации придумывать на основе знаний, где и что конкретный компилятор положит, или хитрое использование UB. Например, почему нельзя просто взять и собрать Linux kernel не GCC, а clang? Или забавные интервьюеры ещё попадаются, которые очень каверзные вопросы про C++ спрашивают, думают, что если закастить указатель на объект на что-то там, и получить доступ к v-table, и что-то там ещё хитро съоптимизировать.