Исключения в Windows x64. Как это работает. Часть 2

в 8:37, , рубрики: C, c++, exceptions, github, open source, try/except, UEFI, windows, Блог компании Аладдин Р.Д., исключения, системное программирование

Опираясь на материал, описанный в первой части данной статьи, мы продолжим обсуждение темы обработки исключений в Windows x64. И в этой части мы подробно рассмотрим те области PE образа, которые задействуются в процессе обработки исключений. Описываемый материал требует знания базовых понятий, таких, как пролог, эпилог, кадр функции и понимания базовых процессов, таких, как действия пролога и эпилога, передача параметров функции и возврат результата функции. Если читатель не знаком с вышеперечисленным, то перед прочтением рекомендуется ознакомиться с материалом из первой части данной статьи.

К статье прилагается реализация механизма, которая находится в папке exceptions хранилища git по этому адресу.

1. .pdata секция PE образа

Секция содержит таблицу функций PE образа и информацию о раскрутке кадров этих функций. Этой таблицей активно пользуется операционная система во время поиска обработчика исключения. Размер и расположение таблицы описаны в опциональном заголовке директорий данных (optional header data directories) PE образа. В последующих разделах описываются структуры, участвующие в описании функций. Поля этих структур могут хранить адреса, указывающие на различные области образа. Все эти адреса, если иного не обозначено, являются адресами относительно начала образа.

Более подробное описание PE образа вы можете найти в документе «Microsoft Portable Executable and Common Object File Format Specification». В данной статье будет приведена только та информация, которая имеет непосредственное отношение к обсуждаемой теме.

2. Таблица функций

Таблица функций состоит из элементов типа RUNTIME_FUNCTION, определение которой приведено на рисунке 1.

Исключения в Windows x64. Как это работает. Часть 2 - 1


Рисунок 1

BeginAddress и EndAddress содержат адреса начала и конца функции соответственно, а UnwindInfoAddress — адрес структуры информации о раскрутке. BeginAddress содержит адрес первого байта функции, когда EndAddress содержит адрес первого байта сразу после функции. Данной структурой также может описываться не вся функция, а только её часть (chunk). Эта ситуация будет более подробно рассмотрена в разделе 3.

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

3. Информация о раскрутке

Как было упомянуто ранее, поле UnwindInfoAddress структуры RUNTIME_FUNCTION содержит адрес структуры UNWIND_INFO, определение которой приведено ниже, на рисунке 2.

Исключения в Windows x64. Как это работает. Часть 2 - 2


Рисунок 2

Поле Version, как следует из названия, несет в себе версию информации о раскрутке. В настоящий момент последней версией является версия 2.

Поле SizeOfProlog содержит размер пролога в байтах.

Поле FrameRegister обозначает номер регистра (номера регистров будут перечислены ниже), который используется в качестве указателя кадра, а поле FrameOffset содержит значение в 16-байтных блоках, которое складывается со значением RSP в момент установки указателя кадра функции. Фактически, указатель кадра будет установлен в значение, равное RSP + FrameOffset * 16. Результирующее значение смещения может быть от 0 до 240. Использование такого внутрикадрового смещения позволяет увеличить плотность кода, т.к. в таком случае может быть использовано больше коротких инструкций, которые в качестве смещения используют 8-битное знаковое значение. Например, если FrameOffset равен 0, то доступ к первым 128 байтам данных кадра может выполняться при помощи коротких инструкций, а к последующим данным нет, т.к. 8-битное знаковое значение охватывает положительные значения от 0 до 127. Теперь представим, что FrameOffset равен 20, в таком случае доступ к первым 148 байтам данных кадра может выполняться короткими инструкциями, т.к. теперь в качестве смещения могут быть задействованы не только значения от 0 до 127, но и значения от -20 до 0. На рисунке 3 изображены два примера доступа к кадру по одному и тому же смещению, но с разным значением FrameOffset. Обратите внимание на размер инструкции, выполняющей доступ к кадру, в обоих случаях.

Исключения в Windows x64. Как это работает. Часть 2 - 3


Рисунок 3

Если FrameRegister равен нулю, то указатель кадра не используется, а поле FrameOffset не несет никакой информации. В таком случае указателем кадра является RSP.

Массив UnwindCode описывает действия пролога. Подробное описание этих кодов приведено в разделе 4. В целях выравнивания данный массив всегда имеет четное количество записей. Поле CountOfCodes содержит количество элементов данного массива. Если это количество нечетное, то последняя запись массива не используется, т.е., фактически, количество записей массива на единицу больше, чем указано в поле. Элементы массива отсортированы в обратном порядке, т.е. первый элемент будет описывать последнее действие пролога.

Поле Flags может содержать три флага: UNW_FLAG_EHANDLER, UNW_FLAG_UHANDLER и UNW_FLAG_CHAININFO. Флаг UNW_FLAG_EHANDLER обозначает, что функция имеет обработчик исключения (exception handler), который должен быть вызван во время поиска обработчика. Флаг UNW_FLAG_UHANDLER обозначает, что функция имеет обработчик раскрутки (termination handler), который должен быть вызван во время раскрутки. Флаг UNW_FLAG_CHAININFO означает, что данная UNWIND_INFO структура не является первичной, а является продолжением (chained) предыдущей UNWIND_INFO структуры. Установка данного флага исключает установку флагов UNW_FLAG_EHANDLER и UNW_FLAG_UHANDLER, а поля FrameRegister и FrameOffset должны быть идентичны полям первичной UNWIND_INFO структуры.

Сразу после UNWIND_INFO структуры располагается либо EXCEPTION_HANDLER структура, либо RUNTIME_FUNCTION структура. Структура EXCEPTION_HANDLER определена на рисунке 4.

Исключения в Windows x64. Как это работает. Часть 2 - 4


Рисунок 4

Если поле Flags содержит установленный бит UNW_FLAG_EHANDLER или UNW_FLAG_UHANDLER, или оба, то после структуры UNWIND_INFO следует структура EXCEPTION_HANDLER. Поле ExceptionHandlerAddress содержит адрес вызываемого обработчика, а поле LanguageSpecificData содержит данные, специфичные для соответствующего языка программирования. Прототип функции обработчика, а также тип возвращаемого ею значения, представлены ниже, на рисунке 5.

Исключения в Windows x64. Как это работает. Часть 2 - 5


Рисунок 5

Параметр ExceptionRecord несет указатель на структуру, описывающую причину исключения. Параметр EstablisherFrame несет указатель кадра, обработчик которого был вызван. Параметр ContextRecord содержит указатель на структуру, которая содержит контекст процессора в момент возникновения исключения. В процессе поиска обработчика исключения или раскрутки стека содержимое структуры может меняться самими обработчиками. Результатом этих изменений будет целевой контекст процессора, т.е. этот контекст будет применен к выполняемой задаче в том случае, если ее выполнение будет продолжено. Параметр DispatcherContext содержит текущий контекст поиска обработчика исключения или раскрутки стека. Структуры EXCEPTION_RECORD и DISPATHCER_CONTEXT и сам процесс поиска обработчика и раскрутки будут описаны подробнее в следующей части данной статьи. Тип EXCEPTION_DISPOSITION будет также рассмотрен в описании этого процесса. Определение структур EXCEPTION_RECORD, CONTEXT и DISPATHCER_CONTEXT вы можете найти в winnt.h или в прилагаемой к статье реализации данного механизма (под названиями SExceptionRecord, SContext и SDispatcherContext соответственно).

На рисунке 6 приведен пример структур RUNTIME_FUNCTION, UNWIND_INFO и EXCEPTION_HANDLER, которые сгенерирует компилятор для конкретной функции. Пунктирной линией обозначен конец функции. Структура EXCEPTION_HANDLER приведена на рисунке для полноты описания и, несмотря на то, что она не является частью структуры UNWIND_INFO, на рисунке представлена как часть этой структуры, т.к. если она присутствует, то следует сразу после структуры UNWIND_INFO, как это было уже обозначено. Адреса начала образа, начала функции и конца функции абсолютные, все адреса в сгенерированных структурах являются относительными от начала образа.

Исключения в Windows x64. Как это работает. Часть 2 - 6


Рисунок 6

Если поле Flags содержит установленный бит UNW_FLAG_CHAININFO, то структура UNWIND_INFO является вторичной (также она называется связанной, chained) и после нее следует структура RUNTIME_FUNCTION. Поле UnwindInfoAddress структуры RUNTIME_FUNCTION содержит адрес на предыдущую UNWIND_INFO структуру. Предыдущая UNWIND_INFO структура, в свою очередь, тоже может быть вторичной, но в конечном счете в этом связанном списке окажется UNWIND_INFO структура, которая не имеет установленного флага UNW_FLAG_CHAININFO. Это будет структура, принадлежащая прологу точки входа функции, которая называется также первичной структурой. Количество связанных структур может быть до 32 включительно.

Связанные структуры полезны в двух ситуациях.

Первая ситуация: компилятор может выполнить оптимизацию, вследствие которой он может отложить сохранение некоторых постоянных регистров. Т.е. их сохранение будет выполняться не в прологе точки входа функции, а в теле функции. Для таких участков кода (в разделе 2 уже упоминалось, что структура RUNTIME_FUNCTION может описывать не всю функцию, а только ее часть, речь была именно о таких участках кода) компилятор сгенерирует RUNTIME_FUNCTION структуру, которая будет указывать на соответствующую структуру UNWIND_INFO, которая будет описывать сохранение этих регистров. В таком случае сохранение этих регистров будет выполняться через обычную запись в память. Заталкивание в стек в такой ситуации не поддерживается. Как уже ранее упоминалось, данная UNWIND_INFO структура является вторичной, после которой следует RUNTIME_FUNCTION структура, которая содержит адрес на предыдущую структуру UNWIND_INFO или первичную UNWIND_INFO структуру.

Вторая ситуация: вытекает из первой, посредством связанных UNWIND_INFO структур размер информации о раскрутке может быть уменьшен, т.к. не придется дублировать массив кодов раскрутки из первичной и/или предыдущих UNWIND_INFO структур.

На рисунке 7 приведен пример структур RUNTIME_FUNCTION и UNWIND_INFO, которые сгенерирует компилятор для функции, к которой применяется ранее описанная оптимизация. Пунктирной линией обозначен конец функции. Структуры RUNTIME_FUNCTION, которые следуют после структур UNWIND_INFO, несмотря на то, что не являются частью этих структур, представлены, как их часть, т.к. их присутствие зависит от полей структур UNWIND_INFO. Структуры RUNTIME_FUNCTION, которые обозначены на рисунке отдельно, являются элементами таблицы функций и отсортированы в таблице так, как это было обозначено в разделе 2, в порядке возрастания в соответствии с начальными адресами частей этой функции. На рисунке они представлены также в порядке возрастания снизу вверх. Под каждой из них представлена UNWIND_INFO структура, на которую они ссылаются. Самая нижняя структура RUNTIME_FUNCTION ссылается на первичную UNWIND_INFO структуру. Остальные ссылаются на UNWIND_INFO структуры, которые являются вторичными. Следовательно, после этих UNWIND_INFO структур располагаются RUNTIME_FUNCTION структуры, которые по своему содержанию полностью повторяют самую нижнюю RUNTIME_FUNCTION структуру и, таким образом, ссылаются на первичную UNWIND_INFO структуру. Также из примера видно, что код, сгенерированный компилятором, сохраняет RDI регистр не в прологе точки входа функции, а в теле функции. Эта часть кода описывается средней RUNTIME_FUNCTION структурой, а в соответствующей UNWIND_INFO структуре присутствуют соответствующие записи, характерные для пролога этого участка кода. Адреса начала образа, начала функции и конца функции абсолютные, все адреса в сгенерированных структурах являются относительными от начала образа.

Исключения в Windows x64. Как это работает. Часть 2 - 7


Рисунок 7

Если поле Flags не содержит ни одного установленного бита, то после структуры UNWIND_INFO не следует никаких структур.

4. Коды раскрутки

В предыдущем разделе был упомянут массив UnwindCode структуры UNWIND_INFO, который описывает действия пролога. Здесь мы рассмотрим структуру элементов этого массива, которая изображена на рисунке 8, а также каждый код, который используется в описании действий пролога.

Исключения в Windows x64. Как это работает. Часть 2 - 8


Рисунок 8

Как видно из рисунка, содержимое элемента, в зависимости от ситуации, интерпретируется в одном из трех вариантов.

Верхняя структура используется при описании действия пролога.

Поле CodeOffset содержит смещение относительно начала пролога на инструкцию после инструкции, которая выполняет описываемое действие.

Поле OpCode содержит код выполняемого действия. Разные действия занимают разное количество записей таблицы, от 1 до 3. Первая запись всегда в формате верхней структуры. Формат и количество остальных записей зависит от кода действия (все коды будут описаны ниже). Если присутствует только одна дополнительная запись, то эта запись интерпретируется в виде поля FrameOffset. Если дополнительных записей две, то эти записи также интерпретируются в виде поля FrameOffset, но первая запись несет младшие 16 бит, а вторая старшие 16 бит 32-битного значения. Порядок следования байт в этих записях — прямой (little endian). Назначение этих значений (в случае с одним и двумя дополнительными записями) описаны в соответcтвующих кодах действий. Если поле OpCode несет UWOP_EPILOG код, то структура имеет формат Epilogue структуры. Эта ситуация будет подробно описана далее. Следует отметить, что этот код актуален только для структуры UNWIND_INFO версии 2.

Поле OpInfo зависит от значения поля OpCode. Оно может содержать номер регистра общего назначения, который задействован в действии, номер XMM регистра, или числовое значение, назначение которого описано в соответствующих кодах действий. Не перечисленные здесь варианты интерпретаций поля OpInfo описаны в соответствующих кодах действий. На рисунке 9 описано сопоставление значений поля OpInfo с регистрами общего назначения и XMM регистрами.

Исключения в Windows x64. Как это работает. Часть 2 - 9


Рисунок 9

Некоторые коды действий содержат беззнаковое смещение на области памяти внутри кадра функции. Это смещение является относительным от начала кадра функции. Если указатель кадра функции не используется, тогда это смещение относительно значения RSP. Если указатель кадра используется, тогда это смещение относительно значения RSP, которое было в момент установки указателя кадра. Значение этого RSP будет равно Указатель кадра — FrameOffset из UNWIND_INFO структуры * 16. Все смещения внутри кадра кратны 8 или 16, в зависимости от кода выполняемого действия. Для UWOP_SAVE_NONVOL и UWOP_SAVE_NONVOL_FAR смещения кратны 8, т.к. эти коды сохраняют 8-байтные регистры. Для UWOP_SAVE_XMM128 и UWOP_SAVE_XMM128_FAR смещения кратны 16, т.к. эти коды сохраняют 16-байтные регистры. Как было упомянуто в разделе 1 в первой части данной статьи, если у функции есть указатель кадра, сохранение регистров общего назначения и XMM регистров выполняется после установления указателя кадра, и, следовательно, любые коды раскрутки, которые содержат смещения на области кадра, всегда появляются в массиве UnwindCode перед кодом раскрутки UWOP_SET_FPREG.

Ниже описаны все допустимые коды. Описание кодов начинается с их названия, в скобках приведены их соответствующие числовые значения и количество занимаемых ими записей.

UWOP_PUSH_NONVOL (0; 1 запись). Заталкивает в стек регистр общего назначения, уменьшая значение RSP на 8. Номер регистра обозначен в поле OpInfo. Как было описано в разделе 1 первой части данной статьи, пролог сначала выполняет именно эти действия, поэтому эти коды появляются в массиве UnwindCode последними.

UWOP_ALLOC_LARGE (1; 2 или 3 записи). Выделяет в стеке область большого размера. У этого кода две формы. Если OpInfo равно 0, тогда выделяемый размер, деленный на 8, хранится в следующей записи, что позволяет выделять до 512Кб — 8. Если OpInfo равно 1, тогда выделяемый размер хранится в следующих двух записях, что позволяет выделять до 4Гб — 8.

UWOP_ALLOC_SMALL (2; 1 запись). Выделяет в стеке область маленького размера. Выделяемый размер хранится в поле OpInfo и вычисляется следующим образом — OpInfo * 8 + 8, что позволяет выделять от 8 до 128 байт.

Коды раскрутки, выделяющие область в стеке, всегда используют самую короткую форму кодирования. Если выделяется область от 8 до 128 байт, то используется UWOP_ALLOC_SMALL. Если выделяется область от 136 байт до 512Кб — 8, то используется UWOP_ALLOC_LARGE со значением поля OpInfo, равным 0. Если выделяется область от 512Кб до 4Гб — 8, то используется UWOP_ALLOC_LARGE со значением поля OpInfo, равным 1.

UWOP_SET_FPREG (3; 1 запись). Устанавливает указатель кадра. Поле OpInfo зарезервировано и не используется, а сам процесс подробно описан в разделе 3, при описании поля FrameRegister структуры UNWIND_INFO.

UWOP_SAVE_NONVOL (4; 2 записи). Сохраняет регистр общего назначения в стеке инструкциями записи в память. Значение сохраняется в ранее выделенную область. Номер сохраняемого регистра указан в OpInfo. Смещение от начала кадра, деленное на 8, хранится в следующей записи.

UWOP_SAVE_NONVOL_FAR (5; 3 записи). Сохраняет регистр общего назначения в стеке, используя длинное смещение, инструкциями записи в память. Значение сохраняется в ранее выделенную область. Номер сохраняемого регистра указан в OpInfo. Смещение от начала кадра хранится в следующих двух записях.

UWOP_EPILOG (6; 2 записи). Для версии 1 структуры UNWIND_INFO этот код именовался UWOP_SAVE_XMM и занимал 2 записи, он сохранял младшие 64 бита XMM регистра, но позже был изъят и ныне пропускается. Практически этот код никогда не использовался. Для версии 2 структуры UNWIND_INFO этот код именуется UWOP_EPILOG, занимает 2 записи и описывает эпилог функции. Подробное описание этого кода будет приведено ниже.

UWOP_SPARE_CODE (7; 3 записи). Для версии 1 структуры UNWIND_INFO этот код именовался UWOP_SAVE_XMM_FAR и занимал 3 записи, он сохранял младшие 64 бита XMM регистра, но позже был изъят и ныне пропускается. Практически этот код никогда не использовался. Для версии 2 структуры UNWIND_INFO этот код именуется UWOP_SPARE_CODE, занимает 3 записи и не имеет никакого смысла.

UWOP_SAVE_XMM128 (8; 2 записи). Сохраняет все 128 бит XMM регистра в стеке. Номер сохраняемого регистра указан в OpInfo. Смещение от начала кадра, деленное на 16, хранится в следующей записи.

UWOP_SAVE_XMM128_FAR (9; 3 записи). Сохраняет все 128 бит XMM регистра в стеке. Номер сохраняемого регистра указан в OpInfo. Смещение от начала кадра хранится в следующих двух записях.

UWOP_PUSH_MACHFRAME (10; 1 запись). Заталкивает машинный кадр. Запись используется для того, чтобы обозначить действие аппаратного прерывания или исключения. У этого кода есть две формы. Если OpInfo равно 0, то это означает, что процессор затолкнул в стек последовательно следующие регистры: SS, старый RSP, EFLAGS, CS, RIP. Если OpInfo равно 1, то это означает, что процессор затолкнул в стек те же регистры, как если бы OpInfo был равен 0, но перед их заталкиванием затолкнул в стек код ошибки. Каждое из значений после заталкивания располагается по адресу, кратному 8. Если значение меньше 8 байт, то старшие неиспользуемые байты обнулены. Если этот код используется, то в массиве UnwindCode он появляется самым последним. Если OpInfo равно 0, то значение RSP уменьшается на 40, иначе на 48. На рисунке 10 изображены оба случая, а стрелкой указано направление роста стека.

Исключения в Windows x64. Как это работает. Часть 2 - 10


Рисунок 10

На рисунке 11 изображен пример UnwindCode массива для пролога, изображенного на рисунке 1 в первой части данной статьи.

Исключения в Windows x64. Как это работает. Часть 2 - 11


Рисунок 11

На рисунке 12 изображен пример UnwindCode массива для пролога, изображенного на рисунке 3 в первой части данной статьи. Следует упомянуть, что для кода UWOP_SET_FPREG вся необходимая информация находится в полях FrameRegister и FrameOffset структуры UNWIND_INFO.

Исключения в Windows x64. Как это работает. Часть 2 - 12


Рисунок 12

Теперь рассмотрим UWOP_EPILOG более детально.

Как и было отмечено ранее, эта запись присутствует только в структуре UNWIND_INFO версии 2. Запись имеет формат структуры Epilogue, которая изображена на рисунке 8. Этот код описывает расположение эпилога функции. Это позволяет определять, выполнял ли процессор код эпилога в момент возникновения прерывания/исключения не по программному коду, как это было описано в разделе 1 в первой части данной статьи, а из массива UnwindCode.

У функции может быть несколько эпилогов, следовательно, на каждый эпилог будет приходиться одна UWOP_EPILOG запись. Если код UWOP_EPILOG используется, тогда в массиве UnwindCode эта запись появляется первой, за которой следует, как минимум, еще одна UWOP_EPILOG запись. Первая UWOP_EPILOG запись, в поле OffsetLowOrSize, описывает размер эпилога. Если бит 0 поля OffsetHighOrFlags первой UWOP_EPILOG записи установлен, то поле OffsetLowOrSize является не только размером, но и смещением на эпилог функции, что может быть только тогда, когда эпилог находится в конце функции/части функции. Смещение на эпилог в этих записях является обратным, т.е. оно не прибавляется к адресу начала функции/части функции, а отнимается от адреса конца функции/части функции, чтобы вычислить адрес начала эпилога. Как было уже отмечено в разделе 2, адреса начала и конца функции/части функции содержатся в полях BeginAddress и EndAddress структуры RUNTIME_FUNCTION. Если бит 0 поля OffsetHighOrFlags первой UWOP_EPILOG записи не установлен, то за этой записью следует следующая UWOP_EPILOG запись, поля OffsetLowOrSize и OffsetHighOrFlags которой содержат младшие 8 бит и старшие 4 бита смещения соответственно. Так же, как было уже отмечено, на каждый эпилог функции приходится дополнительная UWOP_EPILOG запись, в этом случае, как и в предыдущем, поля OffsetLowOrSize и OffsetHighOrFlags формируют 12-битное смещение от конца функции/части функции.

Т.к. UWOP_SAVE_XMM код занимает две записи, количество записей, которые занимает UWOP_EPILOG, всегда четное, при этом последняя запись может не использоваться. Если такой случай имеет место, тогда поля OffsetLowOrSize и OffsetHighOrFlags записи UWOP_EPILOG равны 0.

На рисунке 13 приведен пример UnwindCode массива для функции, которая имеет один эпилог, расположенный в конце функции. Адреса начала и конца функции абсолютные.

Исключения в Windows x64. Как это работает. Часть 2 - 13


Рисунок 13

Как отражено на рисунке 13, несмотря на то, что функция выделяет в прологе область в стеке, началом эпилога считается место, где начинается выталкивание регистров общего назначения из стека, а не освобождение памяти из стека. Объяснение этому будет дано в следующей части данной статьи.

Это заложено в концепцию UWOP_EPILOG кодов. Эпилог может состоять из инструкций выталкивания регистров общего назначения, после которых может следовать инструкция, которая увеличивает RSP на 8 (как это уже было отмечено в разделе 1 первой части данной статьи), и инструкции возврата.

На рисунке 14 приведен пример UnwindCode массива для функции, которая имеет три эпилога. Адреса начала и конца функции абсолютные.

Исключения в Windows x64. Как это работает. Часть 2 - 14


Рисунок 14

Заключение

В этой части статьи мы закончили рассматривать необходимый теоретический материал. В следующей части статьи мы обсудим вспомогательные функции и структуры, которые введены в операционную систему для упрощения работы со структурами PE образа. Затем мы рассмотрим процесс возникновения и обработки исключения, а также то, как эти вспомогательные функции используются в этом процессе.

Автор: Аладдин Р.Д.

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js