Исключения в UEFI приложении
Любому программисту, который знаком с UEFI, известно, что встроенного механизма обработки исключений там нет. Речь идет о try/except блоках, которые являются расширением Microsoft C/C++ компиляторов. Бывает очень полезно иметь такой механизм и в полном объеме пользоваться теми плюсами, которые он дает. Поэтому в данной статье речь пойдет именно о решении этой проблемы. Также к статье прилагается полная реализация механизма с его демонстрацией на базе UEFI приложения. Затронуты только 64х битные процессоры фирмы Intel, и в обсуждении подразумеваются только они. Реализация механизма находится в папке exceptions хранилища git по адресу: https://github.com/anatolymik/machineries.git.
Сначала немного поговорим о том, как вообще обрабатываются исключения. Прежде всего, исключение это особая ситуация. Как правило, исключение возникает из-за того, что процессор не может выполнить конкретную инструкцию. Когда такое событие имеет место, процессор вызывает обработчик исключения, точка входа которого находится в IDT таблице. Исключений у процессора множество, поэтому в соответствии с его типом вызывается соответствующий обработчик.
Как видно из рисунка выше, обработчиков исключений всего 256. Причем первые 32 зарезервированы под исключения. Остальные используются для обработки прерываний, о которых здесь мы говорить не будем. Как уже было упомянуто, в соответствии с типом исключения вызывается соответствующий обработчик. Например, при делении числа на 0, вызовется обработчик исключения 0, или если процессор встретит в потоке незнакомую ему инструкцию, то будет вызван обработчик исключения 6. Более подробно об этом можно узнать в «Intel 64 and IA-32 Architectures Software Developer’s Manual». Мы только что затронули аппаратные средства процессора, без которых обработка аппаратных ошибок невозможна, но это не все.
Говоря о try/except блоках, следует упомянуть, что когда компилятор встречает их, то он генерирует метаинформацию, которая описывает расположения каждого блока в исполняемом файле. Эта информация располагается в обособленном месте, говоря точнее, в .pdata секции PE образа.
Как видно из рисунка, код, данные и разносортная информация об исполняемом файле (например, как в нашем случае описатели try/except блоков) располагаются в едином образе. Все эти данные распределены по так называемым секциям, в соответствии с типом самих данных. Возвращаясь к обсуждению .pdata секции, следует упомянуть, что для каждого try/except блока, кроме его расположения, хранится также и адрес его обработчика. Как правило, это код, заключенный в блоки except и finally. Подробнее о PE формате файла можно узнать в «Microsoft Portable Executable and Common Object File Format Specification». Мы только что затронули программные средства, необходимые для функционирования рассматриваемого механизма, теперь посмотрим на все это еще раз комплексно.
Несмотря на то, что try/except блоки обрабатываются компилятором в процессе генерации кода, и вообще они являются механизмом самого языка, их функционирование требует поддержки со стороны операционной системы. Причем, следует отметить, что часть обработки выполняется операционной системой, а часть выполняется самим исполняемым файлом. Также существует некоторая часть, которая выполняется самим исполняемым файлом, но не генерируется компилятором, и вместо этого её требуется реализовать. Например, зарезервированное имя __c_specific_handler является именем функции, которая отвечает за обработку исключения. В случае отсутствия её реализации, компоновщик не сможет сгенерировать исполняемый файл. В средствах разработки для Windows реализации этих функций включены в библиотеки, которые компонуются с приложением.
Как показано на рисунке выше, в момент возникновения исключения процессор вызывает соответствующий обработчик из IDT таблицы. Обработчики исключений устанавливает операционная система во время своей инициализации, т.е. в момент возникновения исключения управление получает сама операционная система. Обработчики всех исключений очень похожи, они сохраняют всю необходимую информацию для обработки и информацию, уникальную для конкретного типа исключения. Затем все эти обработчики вызывают функцию поиска и вызова обработчика. Функция сканирует .pdata секцию с целью найти обработчик, соответствующий конкретному участку кода, в котором возникло исключение. И если обработчик найден, то операционная система передает ему управление, в противном случае приложение завершается с ошибкой. Мы только что рассмотрели процесс возникновения и обработки исключений, поэтому можно перейти к обсуждению того, что необходимо сделать в UEFI приложении для того, чтобы исключения полноценно функционировали. Также, следует отметить, что приведенное описание является очень упрощенным.
Поскольку в UEFI нет ничего, что имело бы хоть какое-то отношение к исключениям, то очевидно, что все вышеописанное необходимо реализовать. Далее в процессе перечисления, в целях облегчения изучения исходных кодов из прилагаемой к статье реализации, мы будем также ссылаться на её функции, файлы и папки. В первую очередь, необходимо реализовать обработчики исключений. Они располагаются в exccpu.asm файле из папки exc. Также необходимо установить их адреса в IDT, это выполняет функция CpuInitializeExceptionHandlers, располагающаяся в exccpu.cpp файле из папки exc. Почти все из этих обработчиков вызывают CpuDispatchException функцию из файла exccpu.cpp, которая фактически и является отправной точкой в поиске и вызове обработчика исключения. Функции DispatchException и UnwindStack, располагающиеся в файле excdsptch.cpp из папки exc, отвечают за поиск и вызов обработчиков исключений. Чтобы сканировать .pdata секцию, необходима реализация функций обработки PE образа. Эти функции используются ранее упомянутыми функциями поиска и вызова обработчиков исключений. Реализация функций сканирования PE образа сгруппированы в отдельную папку — pe. И, наконец, необходимы реализации зарезервированных функций __C_specific_handler и _local_unwind. Обе функции реализованы в excchandler.cpp файле из папки exc. Точкой входа в приложение является функция EfiMain, располагающаяся в файле efi.cpp из корня проекта. Следует отметить, что установка адресов обработчиков в IDT упрощенна в демонстрационных целях. Если говорить о полноценной реализации, то вам потребуется изолированная IDT.
Пожалуй, на этом все. Стоит только добавить, что описание данного механизма многократно освещалось на просторах Интернета в самых разных деталях. И данную статью уникальной делает скорее прилагаемая реализация и демонстрационный пример. Хотя, изначально когда возникла потребность в реализации данного механизма для проектов, работающих вне среды Windows, ранее упомянутых описаний оказалось недостаточно. В том числе приходилось реверсить Windows, а описание UNWIND_INFO версии 2 не удалось найти вообще. Поэтому изначально статья задумывалась в ином формате, в очень подробном, после прочтения которой не осталось бы никаких вопросов. На практике, её написание выродилось в написание документации, а не статьи, читать которую может оказаться скучным занятием, т.к. с самого начала неясно, какая проблема решается. Поэтому был написан именно такой, простой и легкий вариант, без всяких деталей. А подробный вариант планируется публиковать по частям.