С exception handling под капотом или как же работают исключения в C

C exception код ошибки

Для начала нужно спросить себя: как это все работает? Это первая статья из длинной серии, которую я пишу о том, как реализованы исключения под капотом в C++ (под платформу gcc под x86, но должно быть применимо для других платформ так же). В этих статьях процес выброса и отлова ошибок будет объяснен во всех подробностях, но для нетерпеливых: короткий бриф всех статей о пробросе исключений в gcc/x86:

Даже сейчас это выглядит сложно, а ведь мы даже не начали, это было лишь короткое и неточное описание сложностей, необходимых для обработки исключений.

Если вы слишком любопытны, можете начинать тут. Это — полная спецификация того, что мы будем реализовывать в следующих частях. Я же попытаюсь сделать эту статью поучительной и более простой, чтобы в следующий раз вам было проще начинать с вашим собственным ABI (application binary interface, Двоичный интерфейс приложений — прим. переводчика).

Примечания (отказ от ответственности):
Я ни в коей мере не сведую в том, какая вуду-магия происходит, когда пробрасывается исключение. В этой статье я попытаюсь разоблачить тайное и узнать, как же оно устроено. Какие-то мелочи и тонкости будут не соответствовать действительности. Пожалуйста, дайте мне знать, если где-то что-то неправильно.

Прим. переводчика: это актуально и для перевода.

C++ exceptions под капотом: маленький ABI

Если мы попытаемся понять, почему исключения такие сложные и как они работают, мы можем либо утонуть в тоннах мануалов и документаций, либо попытаться отловить исключения самостоятельно. В действительности, я был удивлен отстутствием качественной информации по теме (прим. переводчика — я, к слову, тоже): все, что можно найти либо чересчур детально, либо слишком уж простое. Конечно же, есть спецификации (наиболее документировано: ABI for C++, но так же CFI, DWARF и libstdc), но обособленного чтение документации недостаточно, если вы действительно хотите понять, что происходит внутри.

Давайте начнем с очевидного: с переизобретения колеса! Мы знаем, что в чистом C нет исключений, так что попытаемся слинковать C++ программу линкером чистого C и посмотрим, что произойдет! Я начал с чего-то простого типа этого:

И очень простой main:

Что случится, если мы попытаемся скомпилировать и слинковать этот франкинкод?

Заметка: вы можете загрузить весь исходный код для этого проекта с моего гит-репозитория.

Пока что все хорошо. Оба, g++ и gcc, счастливы в своем маленьком мире. Хаос начнется сразу, как только мы попробуем их слинковать вместе:

C++ exceptions под капотом: угождаем линкеру, подпихнув ему ABI

Тем не менее, мы хотим понять как именно работают исключения, так что попробуем реализовать свой собственный mini-ABI, обеспечивающий механизм пробрасывания ошибок. Чтобы сделать это, нам понадобится лишь RTFM, однако полный интерфейс может быть найден тут, для LLVM. Вспомним-ка, каких конкретно функций недостает:

__cxa_allocate_exception

После изучения того, как исключения выбрасываются, мы оказались на пути изучения, как они отлавливаются. В предыдущей главе мы добавили в наш пример приложения try-catch-блок, чтобы увидеть что делает компилятор, а так же получили ошибки линкера прямо как в прошлый раз, когда мы смотрели, что произойдет если добавить throw-блок. Вот что пишет линкер:

Напомню, что код вы можете получить на моем гит-репозитории.

В теории (в нашей теории, разумеется), catch-блок транслируется в пару __cxa_begin_catch/end_catch из libstdc++, но и во что-то новое, называемое персональной функцией, о который мы пока еще ничего не знаем.

Все идет замечательно: мы получили такое же определение для raise(), лишь выброс исключения:

Определение для try_but_dont_catch() обрезано компилятором. Это что-то новое: ссылка на __gxx_personality_v0 и что-то другое, называемое LSDA. Это выглядит незначительным определением, однако в действительности это очень важно:

Лишь обычный возврат функции… с некоторым мусором CFI в нем.

Это все для обработки ошибок, тем не менее, мы до сих пор не знаем, как работают __cxa_begin/end_catch; у нас есть идеи как эта пара формирует то, что называет landing pad — место в функции, где располагаются обраотчики исключений. Что мы пока не знаем — как landing pads ищутся. Unwind должен как-то пройти все вызовы в стеке, проверить: имеет ли какой-либо вызов (фрейм стека для точности) валидный блок с landing pad, который может обрабатывать это исключение, и продолжить выполнение в нем.

Это немаловажное достижение, и как это работает мы выясним в следующей главе.

C++ exceptions под капотом: gcc_except_table и персональная функция

Ранее мы выяснили, что throw транслируется в пару __cxa_allocate_exception/throw, а catch-блок транслируется в __cxa_begin/end_catch, а также во что-то, именуемое CFI (call frame information) для поиска landing pads — точки входа обработчиков ошибок.

Что мы не знаем до сих пор, это как _Unwind узнает, где этот landing pads. Когда исключение пробрасывается сквозь связку функций в стэке, все CFI позволяют программе разворачивания стэка узнать, что за функция сейчас исполняется, а так же это необходимо, чтобы узнать, какой из landing pads функции позволяет нам обрабатывать данное исключение (и, к слову, мы игнорируем функции с множественными try/catch блоками!).

Чтобы выяснить, где же этот landing pads находится, используется что-то, зовущее себя gcc_except_table. Таблица эта может быть найдена (с мусором CFI) после конца функции:

Эта секция .gcc_except_table — где хранится вся информация для обнаружения landing pads, мы поговорим об этом позже, когда будем анализировать персональную функцию. Пока что мы лишь скажем, что LSDA означает — зона с специфичными для языка данными, которые персональная функция проверяет на наличие landing pads для функции (она также используется для запуска деструкторов в процессе разворачивания стэка).

Подытожим: для каждой функции, где есть по крайней мере один catch-блок, компилятор транслирует его в пару вызовов cxa_begin_catch/cxa_end_catch и, затем, персональная функция, вызываемая __cxa_throw, читает gcc_except_table для каждого метода в стэке для поиска чего-то, называемого LSDA. Персональная функция затем проверяет, есть ли в LSDA блок, обрабатывающий данное исключение, а так же есть ли какой-то код очистки (который запускает деструкторы когда нужно).

Еще мы можем сделать интересный вывод: если мы используем nothrow (или пустой оператор throw), компилятор может опустить gcc_except_table для метода. Этот способ реализации исключений в gcc, не сильно влияющий на производительность, в действительности сильно влияет на размер кода. Что касается catch-блоков? Если исключение пробрасывается, когда объявлен спецификатор nothrow, LSDA не генерируется и персональная функция не знает, что ей делать. Когда персональная функция не знает, что ей делать, она вызывает обработчик ошибок по-умолчанию, что, в большинстве случаев, означает, что выброс ошибки из nothrow метода закончится std::terminate.

Теперь, когда у нас есть идеи, что делает персональная функция, сможем ли мы реализовать её? Что ж, посмотрим!

Сначала давайте посмотрим на следующий иллюстративный пример:

В этом примере есть часть кода c ошибкой, которая получается из-за деления на 0. Деление на 0 вызывает исключение: DivideByZeroException

Мы будем модифицировать код примера выше:

4- Блок try-catch-finally

5- Обернуть Exception в другом Exception

6- Распространенные исключения

6.1- NullReferenceException

На самом деле, подобно обработке других исключений, вы можете использовать try-catch, чтобы поймать и обработать это исключение. Тем не менее, это механически, как правило, мы должны проверить, чтобы значение объекта не было null до его использования.

Вы можете исправить приведенный выше код как приведено ниже, избегая NullReferenceException:

Источники:

https://habr. com/ru/post/279111/

https://betacode. net/10445/csharp-exception-handling

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: