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

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

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

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

Приводимое описание относится к реализации в Windows, и, следовательно, не следует полагать, что прилагаемая к статье реализация данного механизма будет в точности совпадать с ней, хотя концептуально различий нет. Детали прилагаемой реализации в статье рассматриваться не будут, если об этом не будет сказано явно. Поэтому предполагается, что эти детали, по необходимости, следует изучить самостоятельно.

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

1. Исключения и их обработка

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

1.1. Вспомогательные функции

Прежде чем начать обсуждать процесс обработки исключения, следует рассмотреть RtlLookupFunctionEntry и RtlVirtualUnwind функции, которые экспортируются ntoskrnl.exe модулем в пространстве ядра и библиотекой ntdll.dll в пользовательском пространстве.

Функция RtlLookupFunctionEntry, прототип которой изображен на рисунке 1, возвращает указатель на структуру RUNTIME_FUNCTION и адрес начала PE образа, соответствующие коду, адрес которого передается в параметре ControlPc.

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


Рисунок 1

Параметр ImageBase принимает указатель на переменную, куда функция возвращает адрес начала PE образа, а параметр HistoryTable, который является необязательным, принимает указатель на структуру, которая используется для кэширования поиска. Формат структуры последнего параметра вы сможете найти в winnt.h. Если функция возвращает NULL, то либо по переданному указателю кода не удалось найти соответствующий PE образ, либо в таблице функций не было найдено соответствующей записи, что может означать, что функция не имеет кадра.

Функция RtlVirtualUnwind, прототип которой изображен на рисунке 2, выполняет виртуальную раскрутку функции.

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


Рисунок 2

Виртуальной она называется потому, что функция не изменяет состояния физического процессора, и вместо этого принимает в параметре ContextRecord указатель на структуру, которая описывает контекст процессора в конкретный момент времени. Контекст процессора после раскрутки возвращается в ту же структуру. Сама структура CONTEXT изображена ниже, на рисунке 3.

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


Рисунок 3

Поля P1Home-P6Home введены для удобства использования структуры, например, они могут использоваться в качестве области регистровых и стековых параметров. Поле ContextFlags является битовым и описывает состояние всей структуры, т.е. какие поля отражают состояния соответствующих регистров процессора, а какие нет. Поле может содержать следующие флаги:

  • CONTEXT_CONTROL — если установлен то, поля SegSs, Rsp, SegCs, Rip и EFlags отражают состояния соответствующих регистров процессора;
  • CONTEXT_INTEGER — если установлен то, поля Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi и R8-R15 отражают состояния соответствующих регистров процессора;
  • CONTEXT_SEGMENTS — если установлен то, поля SegDs, SegEs, SegFs и SegGs отражают состояния соответствующих регистров процессора;
  • CONTEXT_FLOATING_POINT — если установлен то, поля Xmm0-Xmm15 и MxCsr отражают состояния соответствующих регистров процессора;
  • CONTEXT_DEBUG_REGISTERS — если установлен то, поля Dr0-Dr3 и Dr6-Dr7 отражают состояния соответствующих регистров процессора.

Следует отметить, что поле ContextFlags никак не интерпретируется функцией RtlVirtualUnwind, и она всегда полагает, что структура содержит действительное состояние процессора в конкретный момент времени. Поскольку поля VectorRegister, VectorControl, DebugControl, LastBranchToRip, LastBranchFromRip, LastExceptionToRip и LastExceptionFromRip не имеют непосредственного отношения к обсуждаемой теме, то их назначение здесь описываться не будет. Поле FltSave используется в случае необходимости сохранения состояния полного контекста XMM, включая состояние FPU.

Параметр ImageBase принимает адрес PE образа, код которого выполнялся. Параметр ControlPc несет адрес инструкции, где было прервано выполнение, а параметр FunctionEntry принимает адрес структуры RUNTIME_FUNCTION, соответствующую адресу инструкции.

Параметр HandlerType принимает тип ожидаемого обработчика. Если принято значение UNW_FLAG_EHANDLER и раскручиваемая функция имеет обработчик исключения, то функция возвращает адрес этого обработчика исключения. Если принято значение UNW_FLAG_UHANDLER и раскручиваемая функция имеет обработчик раскрутки, то функция возвращает адрес этого обработчика раскрутки. Во всех остальных случаях функция возвращает NULL. Следует также отметить, что если выполнение раскручиваемой функции было прервано в момент выполнения кода пролога или кода эпилога, то функция возвращает NULL. Философия тут в том, что обработчик исключения и/или раскрутки привязывается к телу функции. Если функция возвращает адрес одного из обработчиков, то она также возвращает адрес данных, привязанных к этому обработчику компилятором соответствующего языка программирования. Адрес возвращается по указателю, переданному в параметре HandlerData. Как было описано в разделе 3 второй части данной статьи, адрес обработчика исключения и/или обработчика раскрутки хранится в поле ExceptionHandlerAddress структуры EXCEPTION_HANDLER, когда данные, привязанные к обработчику, хранятся в поле LanguageSpecificData той же структуры.

Параметр EstablisherFrame принимает указатель на переменную, куда функция возвращает указатель кадра до раскрутки функции. Параметр ContextPointers является необязательным и если используется, то содержит адрес структуры, которая повторяет содержимое регистров общего назначения и XMM регистров после раскрутки. Следует отметить, что в структуру попадают значения только тех регистров, которые участвовали в раскрутке.

Ниже, на рисунке 4, изображен пример работы функции.

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


Рисунок 4

Слева изображен пример образа, который содержит три функции: wmain, func2 и func1. Функция wmain вызывает функцию func2, которая, в свою очередь, вызывает func1. Посередине изображены ассемблерные представления каждой из функций, где слева изображен абсолютный адрес инструкции в памяти, посередине значение RSP до выполнения инструкции, когда справа ассемблерный мнемокод самой инструкции. Значения RSP изображены не для всех инструкций, а только для тех, которые выполнялись или для инструкции, выполнение потока на которой было прервано. Справа от ассемблерных представлений функций изображены значение регистров процессора до вызова соответствующих функций, для краткости приведены только регистры общего назначения и регистр указателя инструкций (RIP). Метками обозначены значения параметров, которые передаются функции или возвращаются ею.

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

Как это отражено в примере, параметр ImageBase будет равен 0x7FF6AEAF000 (метка 1); параметр ControlPc будет равен 0x7FF6AEAF104E (метка 2); параметр FunctionEntry будет содержать адрес структуры RUNTIME_FUNCTION (метка 3), соответствующей адресу инструкции, значение которого передается в параметре ControlPc; параметр ContextRecord будет содержать значения регистров прерванной функции (метка 4). Указатель структуры RUNTIME_FUNCTION можно получить при помощи RtlLookupFunctionEntry функции. Функция RtlVirtualUnwind после выполнения вернет: значения регистров после раскрутки (метка 5); из структуры EXCEPTION_HANDLER вернет адрес на поле LanguageSpecificData в параметр HandlerData и вернет указатель на обработчик, который извлекается из поля ExceptionHandlerAddress той же структуры (метка 6); также функция вернет указатель кадра раскрученной функции в параметр EstablisherFrame и его значение будет равно 0x9C5DBCF900.

Функция RtlVirtualUnwind перед раскруткой определяет, выполнял ли процессор пролог, эпилог или тело раскручиваемой функции. Если это было тело, то выполняется раскрутка с использованием структуры UNWIND_INFO. Если это был пролог, то раскрутка выполняется также, с тем исключением, что функция сначала определяет, в каком месте выполнение пролога было прервано, и именно с этого места выполняет раскрутку. Если это был эпилог, то для версии 2 структуры UNWIND_INFO раскрутка выполняется из нее. Также, как и в случае с прологом, перед раскруткой функция определяет, в каком месте выполнение эпилога было прервано, и с этого места выполняется раскрутка. Для версии 1 структуры UNWIND_INFO раскрутка выполняется при помощи анализа последующих инструкций эпилога, т.к. в структуре UNWIND_INFO этой версии не содержится никакой информации об эпилоге. В разделе 1 первой части данной статьи упоминалось, что началом эпилога функций, описываемых структурами UNWIND_INFO версии 1, считаются add rsp, константа или lea rsp, [указатель кадра + константа] инструкции. Дело в том, что анализ XMM инструкций усложняет код функции раскрутки, а если исключение возникнет до условного эпилога, тогда их значения будут восстановлены из UNWIND_INFO структуры, поэтому целостность этих регистров не подвергается повреждению после раскрутки. Единственным побочным эффектом является случай, когда исключение возникнет в момент восстановления XMM регистров, в таком случае функция RtlVirtualUnwind вернет указатель на обработчик исключения, который, следовательно, будет вызван при обработке исключения. Для функций, описываемых структурами UNWIND_INFO версии 2, эта философия немного изменилась, и началом эпилога стали считаться инструкции выталкивания регистров общего назначения из стека, т.к. инструкции add rsp, константа и lea rsp, [указатель кадра + константа] не могут породить исключения в принципе. На рисунке 5 изображен пример кода, справа от которого изображено ассемблерное представление двух его функций: func1 и func2. Адреса инструкций абсолютные, а для компактности их шестнадцатеричное представление отсутствует. В качестве примера рассмотрим раскрутку стека функции func1. На рисунке обозначены три случая: А, В, С. Каждый из них изображает состояние выполнения функции: А — пролога, В — тела, С — эпилога.

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


Рисунок 5

Ниже, на рисунках 6-8, отдельно рассмотрен каждый случай. Слева изображены регистры процессора до раскрутки, а справа — после. Для компактности изображены только регистры общего назначения и регистр указателя инструкции (RIP), т.к. в описываемых случаях только эти регистры подвергаются изменениям.

На рисунке 6 изображен случай А. В этом случае процессор выполнял пролог функции func1, выполнение которого было прервано на инструкции заталкивания RDI регистра. Раскрутка стека в данном случае будет выполнена при помощи данных структуры UNWIND_INFO. Сначала будет восстановлено значение регистра RSI, затем восстановлен адрес возврата. Поэтому изменены будут регистры RSI, RSP, RIP.

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


Рисунок 6

На рисунке 7 изображен случай В. В этом случае процессор выполнял тело функции func1, выполнение которого было прервано на инструкции чтения 8 байт из вершины стека в регистр RAX. Раскрутка стека в данном случае будет выполнена при помощи данных структуры UNWIND_INFO. Сначала будет освобождена память из стека, выделенная прологом для локальных переменных функции, затем будут восстановлены значения регистров RDI и RSI, затем восстановлен адрес возврата. Поэтому изменены будут регистры RSI, RDI, RSP, RIP.

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


Рисунок 7

На рисунке 8 изображен случай С. В этом случае процессор выполнял эпилог функции func1, выполнение которого было прервано на инструкции выталкивания RDI регистра. В зависимости от версии структуры UNWIND_INFO, раскрутка будет выполнена либо при помощи самой структуры, если это структура версии 2, либо посредством анализа кода эпилога, если это структура версии 1. Сначала будут восстановлены значения регистров RDI и RSI, затем восстановлен адрес возврата. Поэтому изменены будут регистры RSI, RDI, RSP, RIP.

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


Рисунок 8

Параметр ControlPc для случая А будет равен 0x7FF70C131036, для случая В — 0x7FF70C13104E, для случая С — 0x7FF70C131078. Параметр ImageBase для всех трех случаев будет равен 0x7FF70C130000.

Параметр EstablisherFrame для случая А примет значение 0x6DE73AF6E0, для случая В — 0x7E68EFF860, для случая С — 0x979BB9FAD8. Во всех трех случаях это будет значение регистра RSP перед раскруткой. Отдельно рассмотрим значения EstablisherFrame для функции, которая имеет указатель кадра. На рисунке 9 изображен пример такой функции, где адреса инструкций абсолютные, а вместо их шестнадцатеричного представления изображен указатель стека (RSP) до выполнения инструкции.

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


Рисунок 9

Если выполнение функции было прервано перед выполнением инструкции по адресу 0x7FF6D76C1101, то EstablisherFrame примет значение 0x9E84B9FAA0. Если выполнение было прервано перед выполнением инструкции по адресу 0x7FF6D76C110A, то EstablisherFrame примет значение 0x9E84B9FA90. Если выполнение было прервано перед выполнением инструкции по адресу 0x7FF6D76C1118, то EstablisherFrame также примет значение 0x9E84B9FA90. Следует отметить, что если функция имеет указатель кадра, как и в данном примере, и он был установлен до того момента, когда выполнение функции было прервано, то EstablisherFrame примет значение указателя стека в момент установки указателя кадра, а не текущий указатель стека. В данном примере установка указателя кадра выполнялась инструкцией по адресу 0x7FF6D76C1106.

2. Обработка

Весь процесс обработки исключения условно можно разбить на две части.

Первая часть заключается в поиске и вызове обработчика исключения. Эту часть выполняет операционная система. Концептуальная схема обработки исключения изображена ниже, на рисунке 10.

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


Рисунок 10

На рисунке выше изображено пространство всего процесса. Слева изображено пользовательское пространство, когда справа — пространство ядра. В каждом из пространств размещаются модули. Для любого приложения всегда отображается ntdll.dll модуль, который выполняет необходимые вспомогательные задачи для пользовательского пространства. В пространстве ядра всегда присутствует ntoskrnl.exe, который является ядром Windows. Остальные модули изображены исключительно для примера. В момент возникновения исключения процессор вызывает функцию из соответствующего дескриптора шлюза, который является элементом таблицы дескрипторов прерываний (Interrupt Descriptor Table). Эта функция является функцией ядра. Более подробно о таблице прерываний можно ознакомиться в «Intel 64 and IA-32 Architectures Software Developer’s Manual». Далее эта функция совместно с функцией KiExceptionDispatch подготавливают все необходимые данные для обработки, после чего вызывается функция KiDispatchException, которая выполняет дополнительные действия перед обработкой, одна из которых заключается в том, что, если исключение возникло в пользовательском пространстве, тогда обработка этого исключения перенаправляется в пользовательское пространство. За обработку в пользовательском пространстве отвечает модуль ntdll.dll. Когда вся необходимая подготовка к обработке выполнена, вызывается функция RtlDispatchException, которая выполняет поиск и вызов обработчика путем сканирования .pdata секций образов, и если обработчик найден, то функция вызывает его. Следует также отметить, что функция не раскручивает стек, она только выполняет поиск обработчика.

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

В данной статье мы рассматриваем конструкции try/except и try/finally языков C/C++, поэтому при описании второй части речь пойдет о формате поля LanguageSpecificData структуры EXCEPTION_HANDLER, генерируемого компилятором для этих конструкций.

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

2.1 Подготовка к обработке

Как это уже обозначалось ранее, в момент возникновения исключения, процессор вызывает функцию из соответствующего дескриптора шлюза, который является элементом таблицы дескрипторов прерываний. Функцией шлюза деления на ноль является функция ядра KiDivideErrorFault. Ниже, на рисунке 11, изображено ассемблерное представление функции. Для краткости, отображена только та часть кода, которая имеет непосредственное отношение к обсуждаемой теме.

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


Рисунок 11

Как видно из рисунка, сначала функция симулирует пустой код ошибки, т.к. для исключения деления на ноль процессор не заталкивает код в стек. Далее функция заталкивает регистры общего назначения, выделяет память в стеке и устанавливает указатель кадра. На этом пролог функции заканчивается. Сохранение непостоянных регистров общего назначения и XMM регистров выполняется в теле функции. Также функция сохраняет в переменной стека тип вызванного обработчика. Значение 1 всегда устанавливается для исключений, 0 для прерываний и 2 для сервисов, т.е. вызов сервисов ядра из пользовательского пространства. Последнее действие функции — это вызов функции KiExceptionDispatch. Перед вызовом функция также сбрасывает флаг направления (direction flag), сохраняет MXCSR регистр XMM блока, после чего загружает его стандартным значением. Об этом будет рассказано более подробно ниже. Обратите внимание на то, что у функции нет эпилога. Дело в том, что работа потока, после обработки исключения, не возобновляется обычным путем, т.е. KiExceptionDispatch не возвращает управления, и, следовательно, эпилог не нужен. После инструкции вызова функции следует холостая инструкция. Это, так называемый, заполнитель. Ему отведена особая роль, его наличие позволяет функции RtlVirtualUnwind надежно определить, что исключение произошло во время выполнения тела функции. Т.е., если такого заполнителя не будет, тогда функция RtlVirtualUnwind, при раскрутке функции KiExceptionDispatch, извлечет адрес возврата на инструкцию retn, вместо инструкции nop. И, следовательно, на следующей итерации раскрутки (т.е. при раскрутке уже KiDivideErrorFault функции) функция RtlVirtualUnwind выполнит анализ, выполнялся ли пролог, эпилог или тело. Как было уже отмечено в разделе 1 первой части данной статьи, выполнялся ли эпилог, определяется по коду самой функции (или при помощи записей типа UWOP_EPILOG, структуры UNWIND_INFO версии 2, что большой разницы не делает, т.к. в данном случае уже играет роль адрес инструкции, а не поток байт кода), а поскольку retn инструкция используется только в эпилогах, функция RtlVirtualUnwind сделает ошибочное предположение о том, что выполнялся эпилог, а не тело. Следовательно, это приведет к тому, что при раскрутке KiDivideErrorFault функции, пролог не будет раскручен, и адрес кадра, следующей по стеку выше функции, будет определен неверно.

MXCSR регистр XMM блока не упоминался в разделе 3 первой части данной статьи, но в соглашениях о вызовах также регламентируется его использование при вызове функций. Этот регистр делится на постоянную и непостоянную часть, как это отражено ниже, на рисунке 12.

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


Рисунок 12

Непостоянная часть состоит из 6 флагов состояний, биты 0-5. Остальная часть регистра, состоящая из управляющих бит с 6-15, считается постоянной. Если вызываемая функция изменяет состояние постоянной части, то она должна восстанавливать ее перед возвратом. Более того, вызывающая функция перед вызовом других функций должна загрузить в постоянную часть стандартные значения, если она была ею изменена. Стандартные значения полей постоянной части:

  • Бит 6 равен 0: денормальные операнды являются нулями;
  • Биты 7-12 равны 1: все исключения замаскированы;
  • Биты 13-14 равны 0: округление до ближайшего;
  • Бит 15 равен 0: сброс результата в ноль при нижнем переполнении.

Эти правила могут быть нарушены только в двух случаях:

  1. если целевое назначение функции является изменение постоянной части регистра;
  2. если нарушения этих правил не приводит к изменению поведения программы, т.е. программа будет вести себя так, как если бы правила не были бы нарушены.

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

Что же касается флага направления (DF), то его стандартное значение 0. Если флаг был установлен, то он должен быть сброшен перед вызовом функции или перед возвратом из функции.

Функция KiExceptionDispatch принимает 8 параметров. ECX содержит код возникшего исключения; EDX количество параметров, характерных для данного исключения; R8 содержит адрес инструкции, которая породила исключение; регистры R9, R10, R11 содержат значения параметров, характерных для данного исключения; RBP и RSP являются указателями на сохраненные непостоянные регистры. Как это было отмечено ранее, функция не возвращает управления. Ниже, на рисунке 13, изображено ассемблерное представление функции. Для краткости приведены только те участки кода, которые имеют непосредственное отношение к обсуждаемой теме.

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


Рисунок 13

Как видно из рисунка, пролог функции сначала выделяет память в стеке, после чего сохраняются постоянные регистры XMM и регистры общего назначения. На этом пролог функции заканчивается. Далее функция инициализирует EXCEPTION_RECORD структуру в области памяти, выделенной в стеке, и вызывает функцию KiDispatchException. После возврата из функции восстанавливаются: постоянные регистры общего назначения, постоянные XMM регистры, MXCSR регистр, непостоянные регистры общего назначения и непостоянные XMM регистры. Далее освобождается память в стеке, выделенная функцией шлюза (в данном примере, память, выделенная функцией KiDivideErrorFault), и выполняется возврат в прерванный поток. Структура EXCEPTION_RECORD определена ниже, на рисунке 14.

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


Рисунок 14

Поле ExceptionCode содержит код исключения. Поле ExceptionFlags является битовым и описывает тип и состояние обработки исключения. Его флаги будут подробно рассмотрены в процессе обсуждения поиска и вызова обработчика, а также в процессе обсуждения раскрутки стека. Поле ExceptionRecord в некоторых случаях содержит указатель на другую структуру такого же типа. Например, если в процессе поиска обработчика исключения или обработчика раскрутки, обнаружена недопустимая ситуация (например, обработчик вернул неверный результат обработки), тогда будет порождено новое исключение, структура EXCEPTION_RECORD которого будет содержать указатель на EXCEPTION_RECORD структуру для исключения, в контексте которого возникла данная ситуация. В остальных случаях поле равно NULL. Стоит отметить что данное утверждение справедливо для 32х битных версий Windows, и в 64х битных версиях практически оно всегда равно NULL. Поле ExceptionAddress содержит адрес инструкции, породившей исключение. Поле NumberParameters содержит количество параметров в ExceptionInformation массиве, характерных для конкретного типа исключения, а определение EXCEPTION_MAXIMUM_PARAMETERS равно 15, т.е. это максимально допустимое количество параметров для всех типов исключений.

Функция KiDispatchException принимает 5 параметров: ExceptionRecord — указатель на EXCEPTION_RECORD структуру, описывающая причину исключения; NonvolatileRegisters — указатель на постоянные регистры; VolatileRegisters — указатель на непостоянные регистры; PreviousMode — контекст потока, в котором произошло исключение (пользовательский или контекст ядра); FirstChance — первая попытка обработки (TRUE или FALSE). Функция не возвращает никакого значения.

ExceptionRecord описывает причину исключения. VolatileRegisters формируется функцией шлюза (в данном примере, функцией KiDivideErrorFault). NonvolatileRegisters формируется функцией KiExceptionDispatch. Следует также отметить, что обе структуры содержат не только значения регистров в момент возникновения исключения, но и другую разносортную информацию, которая не будет обсуждаться в данной статье, т.к. она не имеет непосредственного отношения к обсуждаемой теме. PreviousMode несет информацию о контексте, в котором возникло исключение, и равно, либо KernelMode, либо UserMode. FirstChance является булевым значением, означающим, является ли данная попытка обработки исключения первой.

Функция KiDispatchException отвечает за обработку исключения без вовлечения самих обработчиков исключений, если это возможно. Также, если исключение возникло в пользовательском пространстве, тогда обработка исключения перенаправляется в него. Упрощенная блок-схема функции изображена ниже, на рисунке 15.

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


Рисунок 15

Как отражено на рисунке, в начале своей работы функция формирует структуру CONTEXT из структур по указателям NonvolatileRegisters и VolatileRegisters, а также инициализируются стандартными значениями те поля, отражающие регистры процессора, которые не содержатся в данных структурах (например, сегментные регистры). Следовательно, данная структура будет отражать значения регистров процессора в момент возникновения исключения.

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

После того, как функция RtlDispatchException завершила свою работу, и поскольку поля структуры CONTEXT могли быть изменены обработчиками исключений, поля этой структуры копируются обратно в структуры по указателям NonvolatileRegisters и VolatileRegisters посредством KeContextToKframes функции, таким образом модифицируя контекст прерванного потока.

Если исключение возникло в пользовательском контексте, то вызов обработчиков по причинам безопасности функцией не выполняется, и вместо этого функция копирует значение RSP и RIP в момент возникновения исключения в пользовательский стек, копирует EXCEPTION_RECORD и CONTEXT структуры в пользовательский стек и модифицирует машинный кадр ядра так, чтобы при возврате из функции управление было передано обработчику пользовательского режима.

Указатель на обработчик пользовательского режима регистрируется в момент инициализации системы. Функция, отвечающая за обработку исключений в пользовательском контексте, располагается в ntdll.dll библиотеке под названием KiUserExceptionDispatch. Несмотря на то, что для пользовательского пространства вызывается собственный обработчик исключений, он очень схож с обработчиком режима ядра, и поэтому дополнительные разъяснения относительно его работы не требуются.

2.2 Поиск и вызов обработчика

Как это уже упоминалось ранее, поиск и вызов обработчика осуществляет функция RtlDispatchException. Функция принимает два параметра: ExceptionRecord — указатель на EXCEPTION_RECORD структуру, описывающая причину исключения; ContextRecord — указатель на CONTEXT структуру, которая описывает состояние регистров процессора в момент возникновения исключения. Функция возвращает булевое значение, TRUE, если исключение было обработано, и FALSE — в противном случае.

Функция RtlDispatchException выполняет последовательное сканирование в стеке вызванных функций. Если функция имеет обработчик, то функция RtlDispatchException вызывает его. Если обработчик возвращает ExceptionContinueExecution, то работа функции RtlDispatchException прекращается, в противном случае поиск обработчика продолжается. Ниже, на рисунке 16, изображена блок-схема функции.

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


Рисунок 16

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

Далее функция формирует первоначальное значение поля ExceptionFlags для структуры EXCEPTION_RECORD. Следует обратить внимание на то, что поле переданной структуры может содержать установленный флаг EXCEPTION_NONCONTINUABLE, который обозначает, что продолжение выполнения прерванного потока невозможно. Поэтому функция при инициализации первоначального значения копирует этот флаг из переданной структуры в локальную переменную. Затем функция обнуляет указатель кадра функции, обработчик исключения которой, в процессе своего выполнения, породил исключение (т.е. вложенное исключение) и копирует из переданной структуры EXCEPTION_RECORD адрес инструкции, породившей исключение, в локальную переменную.

Далее функция, посредством RtlLookupFunctionEntry функции, получает адрес PE образа и указатель на RUNTIME_FUNCTION структуру функции этого образа, в процессе выполнения которой возникло исключение. Если функция не вернула указателя, тогда считается, что исключение возникло в процессе выполнения простой функции, которые, как это обсуждалось ранее, не имеют никакой информации о раскрутке. Т.к. простые функции не выделяют память в стеке, значение их RSP будет указывать на адрес возврата, следовательно, для таких функций, функция RtlDispatchException извлекает этот адрес, копирует его значение в поле Rip локальной структуры CONTEXT и увеличивает значение поля Rsp той же структуры на 8, таким образом симулируя раскрутку простой функции. Теперь содержимое локальной структуры CONTEXT описывает состояние выполнения следующей по стеку выше функции. Далее функция из локальной структуры CONTEXT копирует в локальную переменную адрес инструкции, принадлежащий уже следующей по стеку выше функции, и выполнит проверку посредством RtlpIsFrameInBounds функции, что новый указатель RSP находится в пределах лимита стека. Если указатель выходит за эти пределы, то это означает что, обработчик исключения не был найден, и, следовательно, функция RtlDispatchException вернет FALSE. В противном случае функция продолжит свою работу, начиная с получения адреса PE образа и указателя на RUNTIME_FUNCTION структуру, для адреса новой инструкции, уже для следующей по стеку выше функции.

Для кадровых функций, функция RtlLookupFunctionEntry вернет указатель на RUNTIME_FUNCTION структуру. В таком случае раскрутка таких функций выполняется при помощи функции RtlVirtualUnwind, которая вернет указатель кадра для раскрученной функции. Сразу после раскрутки выполняется проверка, что указатель кадра находится в пределах лимита стека. Если указатель кадра выходит за эти пределы, то функция RtlDispatchException устанавливает EXCEPTION_STACK_INVALID флаг в поле ExceptionFlags переданной структуры CONTEXT и возвращает FALSE. В противном случае, если функция RtlVirtualUnwind не вернула указатель на обработчик исключения для раскрученной функции, то функция RtlDispatchException продолжит раскрутку следующей по стеку выше функции, предварительно скопировав адрес инструкции, принадлежащий этой функции, и проверив значение поля Rsp локальной структуры CONTEXT на выход за пределы лимита стека.

Если функция RtlVirtualUnwind вернула указатель на обработчик исключения, тогда функция RtlDispatchException вызовет его. Перед его вызовом функция обновит содержимое поля ExceptionFlags переданной структуры EXCEPTION_RECORD из своей локальной копии. Обработчик исключения впервые обсуждался в разделе 3 второй части данной статьи, а его прототип изображен на рисунке 5. Перед вызовом обработчика функция подготавливает структуру DISPATCHER_CONTEXT, которая активно используется в случаях вложенных исключений (nested exception) и активной раскрутки (collided unwind). Определение структуры изображено ниже, на рисунке 17.

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


Рисунок 17

Поле ControlPc содержит адрес, принадлежащий телу функции, для которой был вызван обработчик. Поле ImageBase содержит адрес начала PE образа, в котором содержится функция и ее обработчик. Поле FunctionEntry содержит адрес RUNTIME_FUNCTION структуры этой же функции. Поле EstablisherFrame содержит указатель кадра функции. Поле TargetIp используется при раскрутке, и будет подробно рассмотрено при ее обсуждении. Поле ContextRecord содержит указатель на CONTEXT структуру, отражающую текущее состояние поиска обработчика, т.е. указатель на локальную переменную функции RtlDispatchException. Поле LanguageHandler содержит адрес вызываемого обработчика. Поле HandlerData содержит адрес на данные, специфичные для соответствующего языка программирования. Поле HistoryTable содержит указатель на таблицу кеширования поиска. Поле ScopeIndex содержит текущее значение локальной переменной функции RtlDispatchException, и ее назначение будет подробно рассмотрено при обсуждении раскрутки. Поле Fill0 никак не используется и присутствует в целях выравнивания.

Функция RtlDispatchException не вызывает обработчик исключения напрямую, и вместо этого использует вспомогательную функцию RtlpExecuteHandlerForException, которая принимает такие же параметры, как и сам обработчик, а также возвращает такое же значение. Данная функция фактически является оберткой над функцией обработчика исключений и используется для того, чтобы перехватывать исключения, возникшие в процессе выполнения самого обработчика исключений. Ассемблерное представление функции представлено ниже на рисунке 18.

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


Рисунок 18

Как отражено на рисунке, сначала функция выделяет память в стеке для регистровых переменных и одной переменой, сохраняет указатель на переданную DISPATCHER_CONTEXT структуру в этой переменной и вызывает обработчик исключения, адрес которого хранится в поле LanguageHandler структуры DISPATCHER_CONTEXT. Также обратите внимание на присутствие заполнителя тела функции. Кроме описанных ранее причин его необходимости, к ним добавляется еще одна: поскольку обработчик исключения привязывается к телу функции, то в случае отсутствия заполнителя, он не будет вызван, и, следовательно, работа функции RtlDispatchException будет нарушена дополнительно по этой причине. Ассемблерное представление функции обработчика исключения представлено ниже на рисунке 19.

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


Рисунок 19

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

После того, как структура DISPATCHER_CONTEXT была подготовлена, функция RtlDispatchException вызывает обработчик исключения. Сразу после вызова обработчика, функция устанавливает флаг EXCEPTION_NONCONTINUABLE в своей локальной копии флагов, если он был установлен в переданной структуре EXCEPTION_RECORD обработчиком. Далее функция сбрасывает флаг EXCEPTION_NESTED_CALL в локальной копии и обнуляет указатель кадра для функции, обработчик исключения которой, в процессе своего выполнения, породил исключение, если указатель кадра этой функции совпадает с ранее зафиксированным. Ниже описаны соответствующие действия функций в зависимости от полученного результата.

Если обработчик вернул ExceptionContinueSearch, то функция продолжит раскрутку следующей по стеку выше функции, предварительно скопировав адрес, принадлежащий этой функции и проверив значение поля Rsp локальной структуры CONTEXT на выход за пределы лимита стека.

Если обработчик вернул ExceptionContinueExecution, то функция немедленно прекратит свою работу и вернет TRUE. Предварительно функция проверит, что флаг EXCEPTION_NONCONTINUABLE не установлен, в противном случае функция сгенерирует исключение STATUS_NONCONTINUABLE_EXCEPTION.

Если обработчик вернул ExceptionNestedException, то это означает, что в процессе поиска был обнаружен другой, незавершенный процесс поиска обработчика исключения, в контексте которого возникло новое исключение. В этом случае поле EstablisherFrame структуры DISPATCHER_CONTEXT будет содержать указатель кадра функции, обработчик исключения которой породил исключение. Как это было упомянуто выше, это значение копирует туда обработчик исключения RtlpExecuteHandlerForException функции. Функция RtlDispatchException установит флаг EXCEPTION_NESTED_CALL для поля ExceptionFlags, а также обновит указатель кадра функции, обработчик которой породил исключение. Это значение будет обновлено только если текущее значение указателя равно 0 (не было вложенных исключений), или поле EstablisherFrame структуры DISPATCHER_CONTEXT содержит указатель кадра функции, которая располагается по стеку выше, чем функция, в контексте которой возникло новое исключение.

Если обработчик вернул ExceptionCollidedUnwind, то это означает, что в процессе поиска была обнаружена активная раскрутка, в контексте которой возникло исключение. Этот случай будет подробнее описан при описании раскрутки стека, здесь стоит только обозначить, что в ответ на этот результат функция RtlDispatchException обновит структуру DISPATCHER_CONTEXT и локальную структуру CONTEXT так, что поиск обработчика будет возобновлен с того места, где была прервана раскрутка.

Во всех остальных случаях функция RtlDispatchException сгенерирует исключение STATUS_INVALID_DISPOSITION.

Заключение

Как уже упоминалось в разделе 2, весь процесс условно можно поделить на две части, и мы полностью рассмотрели первую. В следующей части статьи будет рассмотрена вторая часть, в которую входит раскрутка стека и принцип работы try/except и try/finally блоков.

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

Источник


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


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