Стоит начать с одной картинки
Обычно это ставят в конце, но у нас будет в начале
Источники информации:
Intel Manual
Developing Drivers With Windows Driver Foundation: Interrupt Request Levels
Windows Internals: Interrupt Request Levels And Deferred Procedure Calls
Managing Hardware Priorities
OSDev Wiki: APIC
Not everything is executable
Термины:
Вот некоторые термины, которые я буду использовать, если вы чувствуете, что не понимаете конкретного термина, просто найдите его.
IRQL - это сокращение от уровней запросов на прерывание. Они определяют приоритет, в котором работает процессор, на какое прерывание следует обратить внимание раньше других, что следует обработать в первую очередь и что следует отложить.
Основная идея IRQL и приоритета задач заключается в том, что прерывание с более низким значением IRQL не может заменить прерывание с более высоким значением IRQL.
Пример: если процессор получил два прерывания для обработки первый с помощью Dispatch IRQL, значение которого равно 2, а второй имеет passive IRQL, значение которого равно нулю, процессор обработает сначала с Dispatch, потому что он имеет более высокое значение IRQL численно.
В x64 Windows существует 16 различных уровней IRQL:
Passive, Dispatch и APC - это IRQL для программного обеспечения. Остальное предназначено для использования аппаратного обеспечения.
Сейчас мы сосредоточимся на IRQL программного обеспечения.
Passive: обычный IRQL, который кодирует режим пользователя / ядра, имеет нулевое значение. Код пользовательского режима всегда пассивен,
но код режима ядра может быть поднят до apc или Dispatch.
APC: IRQL со значением 1, используемый для обработчиков ошибок apc и страниц.
Dispatch: уровень IRQL, на котором работает планировщик потоков ядра. Он имеет значение 2.
Потоки, которые отправляют IRQL, имеют бесконечное квантовое время, поэтому они не могут быть прерваны другими потоками или
приостановлены.
Процессы, которые обладают Dispatch IRQL, такие как называемые DPC (отложенные вызовы процедур), не должны обращаться или вызывать маршруты с IRQL < Dispatch, и они должны обращаться только к нестраничным пулам, попытка обращения к страничным пулам вызывает ошибку ( BSOD %^) ), потому что на уровне Dispatch есть обработка страничных сбоев, так что, предположим, память стала страничной, поэтому она находится не в оперативной памяти, а на диске, и драйвер попытался получить к ней доступ. Это вызывает ошибку страницы, и нет ничего, чтобы обработать ошибку страницы, поэтому windows просто вызывает BSOD, чтобы исправить случившееся.
Это объясняет появление кода остановки IRQL_NOT_LESS_OR_EQUAL. Этот код остановки происходит, когда ошибка страницы происходит на уровне Dispatch (2), но эта ошибка страницы может быть обработана только на уровне Passive (0) или APC (1), поэтому Dispatch (2) не равен и не меньше, чем Passive (1) или APC (0). Если вы пишете драйвер для windows, чтобы получить текущий IRQL, вы можете использовать функцию KeGetCurrentIrql.
В ассемблерном виде она выглядит очень просто.
Мы видим, что она просто перемещает значение регистра управления CR8 в RAX в качестве возвращаемого значения функции.
Таким образом, вызов этой функции - это то же самое, что и чтение CR8 следующим образом:
Это означает, что текущий IRQL / приоритет хранится в CR8, но задумывались ли вы, почему используется CR8 ? и как он
имеет отношение к IRQL и приоритету прерываний процессора. Именно это мы и выясним, покопавшись в APIC.
Введение в APIC и обработку прерываний x64:
Заметки: APIC очень хорошо объясняется в мануалах интела (глава 10 ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (APIC)). Я не хочу повторяться и не буду акцентировать внимание на все детали. Поэтому я просто подытожу то, что важно, и объясню, как это связано с IRQL, чтобы получить более глубокое понимание происходящего.
APIC (расширенный программируемый контроллер прерываний) принимает прерывания от внутренних и внешних устройств ввода/вывода, затем передает их в блок управления. устройств, затем он передает их ядрам процессора в виде сообщений о прерываниях для их обработки.
В SMP он отправляет и получает сообщения IPI (межпроцессорное прерывание) на логические процессоры и от них по системной шине. Сообщения IPI используются для распределения прерываний между процессорами системы, если процессору требуется внимание другого процессора (например, загрузка процессоров, распределение работы между группой процессоров, выключение системы, TLBs и т.д.).
Поскольку в SMP-системах все процессоры равны и используют общую память, процессор может изменить распределение памяти (т.е. процессор может изменить базовый адрес памяти, с которым он работает), это приведет к прерыванию от процессора, изменившего распределение памяти, другим процессорам.
Процессоры Intel имеют два типа APIC:
Локальный APIC имеет свои собственные регистры, они используются для управления сообщениями, определения приоритета прерывания и т.д. Их рамзер составляет 4кб. И программное обеспечение может взаимодействовать с локальным APIC через его регистры.
Например: мы можем изменить отображение памяти регистров путем изменения MSR IA32_APIC_BASE на другой базовый адрес вместо адреса по умолчанию (FEE00000h), однако, системное программное обеспечение не должно изменять что-либо в системной памяти. Попытка сделать это вызовет исключение #UD (исключение недопустимого опкода).
Номера векторов прерываний и приоритет задач:
Каждое прерывание имеет специальный идентификатор, называемый "номером вектора прерывания". Это набор индексов в диапазоне 0-255, которые используются как индексы в IDT, чтобы узнать, какой обработчик прерывания следует вызвать.
Пример: если прерывание имеет номер вектора 0x1, при индексации IDT это относится к nt!KiDivideErrorFaultShadow. Эта функция является лишь тенью или оберткой для nt!KiDivideError, которая используется для обработки ошибок деления. Она имеет идентификатор "0x0".
Если вы попытаетесь разобрать nt!KiDivideErrorFaultShadow, то увидите, что она просто переходит к nt!KiDivideError, все тени работают подобным образом, просто уберите слово "Shadow" и вы получите настоящий обработчик прерывания.
Для получения списка этих теней вы можете выполнить команду "!idt" в windbg:
Мы также должны различать прерывания и исключения, поскольку первые 32 записи IDT не все являются прерываниями, прерывания и исключения разделяют одну и ту же концепцию, что это событие, которое требует внимания процессора. Прерывания могут возникать в произвольное время во время выполнения программы и происходят, скажем, из-за нажатия клавиши, запроса ввода-вывода или выполнения инструкции "int n", с другой стороны, исключения происходят из-за ошибок, таких как деление на ноль или ошибка страницы. При возникновении прерывания процессор сохраняет свое состояние и начинает переключение контекста, а затем начинает выполнять обработчик прерывания в случае прерывания или обработчик исключения, если это исключение.
Intel резервирует первые 32 [0:31] номера векторов прерываний, такие как #DE (ошибка деления), а номера с 32 по 255 определяются пользователем (т.е. создаются ОС) и не резервируются.
Поскольку мы говорим в основном о приоритете прерывания, каждое прерывание имеет класс приоритета прерывания, основанный на номере вектора прерывания, который представляет собой просто биты[7:4] из номера вектора прерывания. Самый низкий класс приоритета прерывания - 1, а самый высокий - 15. Чем выше класс приоритета прерывания, тем выше его приоритет.
Для хранения текущего приоритета CPU использует специальный регистр TPR (регистр приоритета задачи) в локальном APIC.
Он имеет следующую структуру:
Биты TaskPriorityClass [7:4] используются для хранения текущего приоритета задачи (IRQL в windows). Когда локальный APIC получает прерывание, он сравнивает свое значение TaskPriorityClass с полученным классом приоритета прерывания. Если TaskPriorityClass меньше, чем полученный класс приоритета прерывания, он передает его ядрам CPU, в противном случае он откладывает его.
Также в локальном APIC есть регистр PPR, который используется для определения текущего приоритета процессора, его значение основано на значении TPR, он имеет такую структуру:
Как мы уже говорили, значение PPR основано на значении регистра TPR и на ISRV (это номер вектора самого приоритетного бита, который установлен в ISR, или 0x0, если в ISR не установлен ни один бит). Таким образом, значение PPR попадает в 3 условия:
If TPR[7:4] > ISRV[7:4], PPR[3:0] = TPR[3:0]
If TPR[7:4] < ISRV[7:4], PPR[3:0] = 0
If TPR[7:4] = ISRV[7:4], PPR[3:0] = TPR[3:0] || 0
После получения некоторой информации о том, как работает приоритет задачи в целом, нам нужно обсудить, как программное обеспечение может взаимодействовать с TPR и изменять приоритет задачи.
Intel предлагает два способа:
Структура CR8 выглядит следующим образом:
Мы видим, что только четыре бита (TaskPriority) могут быть использованы программным обеспечением. Они используются для представления IRQL / значение приоритета.
Если посмотреть на CR8 с помощью windbg:
Мы видим, что младшие 4 бита установлены в 1, а в десятичном исчислении это 15, что является наивысшим классом IRQL / приоритета задачи
в архитектуре x64.
При использовании модели CR8 локальный APIC при получении прерывания сравнивает значение класса приоритета прерывания
биты[7:4] со значением CR8.
Мы видим, что биты [7:4] установлены в 1. Это значения TaskPriorityClass. Таким образом, мы делаем вывод, что:
Обычно это ставят в конце, но у нас будет в начале
Источники информации:
Intel Manual
Developing Drivers With Windows Driver Foundation: Interrupt Request Levels
Windows Internals: Interrupt Request Levels And Deferred Procedure Calls
Managing Hardware Priorities
OSDev Wiki: APIC
Not everything is executable
Термины:
Вот некоторые термины, которые я буду использовать, если вы чувствуете, что не понимаете конкретного термина, просто найдите его.
- IRQL: IRQL определяет приоритет оборудования, при котором процессор работает в любой момент времени.
- Прерывания: сообщения от программного/аппаратного обеспечения, которые прерывают центральный процессор для выполнения определенной операции, заставляя центральный процессор
изменять указатель команд, указывая на ISR, который является функцией, которая будет обрабатывать прерывания. (например, нажатие клавиши или
операция системного вызова). - Обработчики прерываний: также называемые ISR (Процедуры обслуживания прерываний). ISR-это функция, которая вызывается при возникновении прерывания
для его обработки (например, обработка ошибок разделения). - TPR: регистр приоритетов задач-это регистр в локальном APIC, в котором хранится текущий приоритет задачи.
- MSR: типовые регистры, используемые для хранения информации о процессоре и управления определенными функциями.
- CR8: управляющий регистр, обеспечивающий интерфейс с TPR
- Ошибка страницы: ошибка доступа к памяти, вызванная при попытке доступа к адресу, которого нет в физической памяти, или при попытке
доступ к странице, запись таблицы страниц которой имеет свой NX-бит (т. е. содержимое памяти не может быть выполнено), не очищен. - APIC: Усовершенствованный Программируемый контроллер прерываний.
- Квантовое время: время, отведенное потоку для запуска, перед запуском любого потока после него.
- Внешний APIC ввода-вывода: получает прерывания от внешних устройств ввода-вывода и передает их локальному APIC.
- Локальная точка доступа: получает прерывания от внешней точки доступа ввода-вывода и передает их ядрам процессора.
- XAPIC/x2APIC: расширения для архитектуры APIC.
- IDT (Таблица дескрипторов прерываний): таблица указателей функций для обработчиков прерываний.
- Номер вектора прерывания: идентификатор, присвоенный каждому прерыванию, используемому для индексирования в IDT.
- IA32_x2APIC_TPR: MSR в x2APIC, который можно использовать для чтения/записи TPR.
- EOI (Завершение прерывания): используется программным обеспечением для подачи сигнала / указания на то, что прерывание завершило свою работу или завершилось.
- IRR (Регистр запросов на прерывание): содержит принятые прерывания локальным APIC, но еще не отправленные ядрам ЦП
для обработки. - ISR (регистр состояния прерывания): локальный регистр APIC, в котором хранятся прерывания, которые были приняты, но все еще
жду ВЗ. Локальный APIC либо ставит прерывание в очередь в IRR, либо в ISR. - ISRV: номер вектора прерывания бита с наивысшим приоритетом, заданного в ISR, или 0x0, если в ISR не задан бит.
- Пулы подкачки: память, которую можно перенести / выгрузить на диск
- Пул без подкачки: память, которая не может быть перенесена / выгружена на диск.
- NMI (немаскируемые прерывания): прерывания, которые нельзя отбросить, и локальный APIC передает их непосредственно ядрам процессора.
- SMI (прерывание управления системой): как и NMI, нельзя отбросить, но они имеют более высокий приоритет и обрабатываются в SMM
(режим управления системой). - DPC (отложенные вызовы процедур): процедуры, которые имеют IRQL отправки, используемые драйверами аппаратных устройств для обслуживания
прерываний.
IRQL - это сокращение от уровней запросов на прерывание. Они определяют приоритет, в котором работает процессор, на какое прерывание следует обратить внимание раньше других, что следует обработать в первую очередь и что следует отложить.
Основная идея IRQL и приоритета задач заключается в том, что прерывание с более низким значением IRQL не может заменить прерывание с более высоким значением IRQL.
Пример: если процессор получил два прерывания для обработки первый с помощью Dispatch IRQL, значение которого равно 2, а второй имеет passive IRQL, значение которого равно нулю, процессор обработает сначала с Dispatch, потому что он имеет более высокое значение IRQL численно.
В x64 Windows существует 16 различных уровней IRQL:
Passive, Dispatch и APC - это IRQL для программного обеспечения. Остальное предназначено для использования аппаратного обеспечения.
Сейчас мы сосредоточимся на IRQL программного обеспечения.
Passive: обычный IRQL, который кодирует режим пользователя / ядра, имеет нулевое значение. Код пользовательского режима всегда пассивен,
но код режима ядра может быть поднят до apc или Dispatch.
APC: IRQL со значением 1, используемый для обработчиков ошибок apc и страниц.
Dispatch: уровень IRQL, на котором работает планировщик потоков ядра. Он имеет значение 2.
Потоки, которые отправляют IRQL, имеют бесконечное квантовое время, поэтому они не могут быть прерваны другими потоками или
приостановлены.
Процессы, которые обладают Dispatch IRQL, такие как называемые DPC (отложенные вызовы процедур), не должны обращаться или вызывать маршруты с IRQL < Dispatch, и они должны обращаться только к нестраничным пулам, попытка обращения к страничным пулам вызывает ошибку ( BSOD %^) ), потому что на уровне Dispatch есть обработка страничных сбоев, так что, предположим, память стала страничной, поэтому она находится не в оперативной памяти, а на диске, и драйвер попытался получить к ней доступ. Это вызывает ошибку страницы, и нет ничего, чтобы обработать ошибку страницы, поэтому windows просто вызывает BSOD, чтобы исправить случившееся.
Это объясняет появление кода остановки IRQL_NOT_LESS_OR_EQUAL. Этот код остановки происходит, когда ошибка страницы происходит на уровне Dispatch (2), но эта ошибка страницы может быть обработана только на уровне Passive (0) или APC (1), поэтому Dispatch (2) не равен и не меньше, чем Passive (1) или APC (0). Если вы пишете драйвер для windows, чтобы получить текущий IRQL, вы можете использовать функцию KeGetCurrentIrql.
В ассемблерном виде она выглядит очень просто.
Мы видим, что она просто перемещает значение регистра управления CR8 в RAX в качестве возвращаемого значения функции.
Таким образом, вызов этой функции - это то же самое, что и чтение CR8 следующим образом:
Это означает, что текущий IRQL / приоритет хранится в CR8, но задумывались ли вы, почему используется CR8 ? и как он
имеет отношение к IRQL и приоритету прерываний процессора. Именно это мы и выясним, покопавшись в APIC.
Введение в APIC и обработку прерываний x64:
Заметки: APIC очень хорошо объясняется в мануалах интела (глава 10 ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (APIC)). Я не хочу повторяться и не буду акцентировать внимание на все детали. Поэтому я просто подытожу то, что важно, и объясню, как это связано с IRQL, чтобы получить более глубокое понимание происходящего.
APIC (расширенный программируемый контроллер прерываний) принимает прерывания от внутренних и внешних устройств ввода/вывода, затем передает их в блок управления. устройств, затем он передает их ядрам процессора в виде сообщений о прерываниях для их обработки.
В SMP он отправляет и получает сообщения IPI (межпроцессорное прерывание) на логические процессоры и от них по системной шине. Сообщения IPI используются для распределения прерываний между процессорами системы, если процессору требуется внимание другого процессора (например, загрузка процессоров, распределение работы между группой процессоров, выключение системы, TLBs и т.д.).
Поскольку в SMP-системах все процессоры равны и используют общую память, процессор может изменить распределение памяти (т.е. процессор может изменить базовый адрес памяти, с которым он работает), это приведет к прерыванию от процессора, изменившего распределение памяти, другим процессорам.
Процессоры Intel имеют два типа APIC:
- локальный APIC
- APIC внешнего ввода-вывода
Локальный APIC имеет свои собственные регистры, они используются для управления сообщениями, определения приоритета прерывания и т.д. Их рамзер составляет 4кб. И программное обеспечение может взаимодействовать с локальным APIC через его регистры.
Например: мы можем изменить отображение памяти регистров путем изменения MSR IA32_APIC_BASE на другой базовый адрес вместо адреса по умолчанию (FEE00000h), однако, системное программное обеспечение не должно изменять что-либо в системной памяти. Попытка сделать это вызовет исключение #UD (исключение недопустимого опкода).
Номера векторов прерываний и приоритет задач:
Каждое прерывание имеет специальный идентификатор, называемый "номером вектора прерывания". Это набор индексов в диапазоне 0-255, которые используются как индексы в IDT, чтобы узнать, какой обработчик прерывания следует вызвать.
Пример: если прерывание имеет номер вектора 0x1, при индексации IDT это относится к nt!KiDivideErrorFaultShadow. Эта функция является лишь тенью или оберткой для nt!KiDivideError, которая используется для обработки ошибок деления. Она имеет идентификатор "0x0".
Если вы попытаетесь разобрать nt!KiDivideErrorFaultShadow, то увидите, что она просто переходит к nt!KiDivideError, все тени работают подобным образом, просто уберите слово "Shadow" и вы получите настоящий обработчик прерывания.
Для получения списка этих теней вы можете выполнить команду "!idt" в windbg:
Мы также должны различать прерывания и исключения, поскольку первые 32 записи IDT не все являются прерываниями, прерывания и исключения разделяют одну и ту же концепцию, что это событие, которое требует внимания процессора. Прерывания могут возникать в произвольное время во время выполнения программы и происходят, скажем, из-за нажатия клавиши, запроса ввода-вывода или выполнения инструкции "int n", с другой стороны, исключения происходят из-за ошибок, таких как деление на ноль или ошибка страницы. При возникновении прерывания процессор сохраняет свое состояние и начинает переключение контекста, а затем начинает выполнять обработчик прерывания в случае прерывания или обработчик исключения, если это исключение.
Intel резервирует первые 32 [0:31] номера векторов прерываний, такие как #DE (ошибка деления), а номера с 32 по 255 определяются пользователем (т.е. создаются ОС) и не резервируются.
Поскольку мы говорим в основном о приоритете прерывания, каждое прерывание имеет класс приоритета прерывания, основанный на номере вектора прерывания, который представляет собой просто биты[7:4] из номера вектора прерывания. Самый низкий класс приоритета прерывания - 1, а самый высокий - 15. Чем выше класс приоритета прерывания, тем выше его приоритет.
Для хранения текущего приоритета CPU использует специальный регистр TPR (регистр приоритета задачи) в локальном APIC.
Он имеет следующую структуру:
Биты TaskPriorityClass [7:4] используются для хранения текущего приоритета задачи (IRQL в windows). Когда локальный APIC получает прерывание, он сравнивает свое значение TaskPriorityClass с полученным классом приоритета прерывания. Если TaskPriorityClass меньше, чем полученный класс приоритета прерывания, он передает его ядрам CPU, в противном случае он откладывает его.
Также в локальном APIC есть регистр PPR, который используется для определения текущего приоритета процессора, его значение основано на значении TPR, он имеет такую структуру:
Как мы уже говорили, значение PPR основано на значении регистра TPR и на ISRV (это номер вектора самого приоритетного бита, который установлен в ISR, или 0x0, если в ISR не установлен ни один бит). Таким образом, значение PPR попадает в 3 условия:
If TPR[7:4] > ISRV[7:4], PPR[3:0] = TPR[3:0]
If TPR[7:4] < ISRV[7:4], PPR[3:0] = 0
If TPR[7:4] = ISRV[7:4], PPR[3:0] = TPR[3:0] || 0
После получения некоторой информации о том, как работает приоритет задачи в целом, нам нужно обсудить, как программное обеспечение может взаимодействовать с TPR и изменять приоритет задачи.
Intel предлагает два способа:
- Взаимодействовать с ним напрямую через локальный APIC
- Использовать регистр CR8 в качестве интерфейса, и любое изменение в нем будет отражено в TPR. (Windows использует эту технику, как мы видели в KeGetCurrentIrql)
Структура CR8 выглядит следующим образом:
Мы видим, что только четыре бита (TaskPriority) могут быть использованы программным обеспечением. Они используются для представления IRQL / значение приоритета.
Если посмотреть на CR8 с помощью windbg:
Мы видим, что младшие 4 бита установлены в 1, а в десятичном исчислении это 15, что является наивысшим классом IRQL / приоритета задачи
в архитектуре x64.
При использовании модели CR8 локальный APIC при получении прерывания сравнивает значение класса приоритета прерывания
биты[7:4] со значением CR8.
- if "CR8" < значения класса приоритета прерывания "bits[7:4]", он отправляет прерывание на обработку ядрам процессора.
- else оно откладывается в ISR или IRR до тех пор, пока его приоритет не станет "> CR8".
Мы видим, что биты [7:4] установлены в 1. Это значения TaskPriorityClass. Таким образом, мы делаем вывод, что:
- CR8[3:0] == TPR[7:4]
- CR8 предоставляет интерфейс к TPR, а его биты [3:0] представляют IRQL / приоритет задачи.
Последнее редактирование: