Windows поставляется с ~300 драйверами, но как показывает практика, устройств в природе ещё больше, а потому приходится разрабатывать свои. Данный цикл из 4-х статей затрагивает такое сложное направление в программировании как драйвера. Продвигаясь от простого мы разберём большинство ключевых моментов и подводных камней, где нас может поджидать засада. Это увлекательное путешествие к центру ядра Ntoskrnl, пройти которое под силам не каждому. Забегая вперёд скажу, что не так страшен чёрт, как его малюют – при содействии отладчика WinDbg всё становится намного прозрачней. Главное тут иметь интерес, а остальное само-собой приложится.
В этой части:
1. Базовые сведения
На сегодняшний день имеем три архитектурных моделей драйверов – это унаследованные от Win2k Legacy (NT4), более продвинутые с технологией PnP WDM (NT5, Windows Driver Model), и современные для Vista+ WDF (NT6 и выше, Driver Foundation).
Под Legacy подразумеваются драйверы режима ядра в стиле «Kernel Services». Они отличаются тем, что не поддерживают технологию PnP. Но если мы хотим написать драйвер, который например отслеживает создание процесса, или просто читает ядерную память, то PnP нам и не нужен, т.к. нет работы с оборудованием и управления его питанием. Нам нужен минимум ненужного хлама, а потому примитивный Legacy будет здесь хорошим выбором. По сути название «устаревший» неверно – более точно было-бы сказать «не-PnP-драйвер», или «драйвер на основе служб».
Но в системе есть много мест, где эти драйвера не подходят по определению, например фильтрация чего-либо в дереве устройств PnP, присоединения своего драйвера в стек уже имеющихся драйверов, и многое другое – здесь без WDM никак не обойтись. В свою очередь, в класс WDF входят подклассы KMDF и UMDF (kernel/user mode). Они полностью абстрагируют нас от прямого взаимодействия с железом, где-то далеко предоставляя свои обёртки. Получается быстро и качественно, но без (так нужного нам) понимания принципов. В свой основе WDF опирается на WDM, а потому мы сделаем акцент именно на последнем.
Взаимодействие с драйверами устройств построено на прерываниях «Interrupt», а ключевым понятием здесь является IRQ или «Interrupt Request». Прерывания драйверам могут посылать как аппаратное оборудование на материнской плате, так и системные процессы, в т.ч. и софт пользовательского режима, например Breakpoint
Прерывания от устройств всегда асинхронны. Это означает, что отправив запрос мы можем ждать ответа хоть целую вечность. Обычно драйвер устройства выполняет только минимум самых важных функций из всей процедуры обработки прерывания, а остальная часть ставится системой в специальную очередь отложенных вызовов DPC (Deferred Procedure Call). Соответственно чем больше запросов, тем длиннее очередь. Зато программные прерывания относятся к синхронным, и выполнятся сразу от начала и до своего логического конца.
2. IRQ – линия запроса на прерывание, и системная таблица IDT
Система прерываний стара как мир – на платах с древним i80286 прерывания от устройств по проводникам IRQ поступали в пару соединённых каскадно контролёров PIC i8259. Каждый из них имел по 8 входных линий и одну на выходе, что в сумме давало всего 15 входов (IRQ2 отводилась для связи между собой). Получив логическую единицу на одной из этих линий, PIC превращал её в вектор, который доставлял к контакту процессора INTR. В результате ЦП прерывал текущую свою работу, и используя вектор как индекс в системной таблице прерываний, выбирал из неё адрес соответствующей процедуры обработки ISR «Interrupt Service Routine».
Но с появлением многопроцессорных симметричных систем SMP (несколько ядер) инженерам пришлось отказаться от грязной затеи каскадных связей, в пользу расширенного контролёра IO-APIC 82093AA (Advanced Programmable Interrupt Controller). Во-первых число его входов IRQ увеличилось с 15 до 24, а во-вторых линия на выходе теперь кодировалась 4-битным значением, что позволило перенаправлять вектор одному из 2^4=16 ядер процессора. Позже по накатаной появился xAPIC (eXtended) с 8-битным выходом =256 ядер, и на данный момент во-всех чипсетах используется его расширение
Для связи с IOAPIC процессоры имеют локальные контролёры LAPIC, способные принимать прерывания от девайсов на шине PCI-Express. Такие прерывания пакуются в сообщения MSI, и в обход IOAPIC доставляются прямиком в специально выделенную для этих целей память. Между собой ядра ЦП общаются посредством прерываний IPI «Inter-Processor Interrupt». Например когда одно ядро занято, оно может поручить обработку прерывания соседним ядрам, выставив его на межпроцессорную шину.
2.1. IDT – Interrupt Dispatch Table
Получив вектор прерывания от LAPIC, процессор обращается к системной таблице диспетчеризации прерываний IDT, чтобы найти в ней адрес обработчика сложившейся ситуации. Таким образом вектор – это просто порядковый номер дескриптора в IDT. Адрес таблицы прописан в регистре процессора
Значение регистра
Командой
2.2. Диспетчеризация прерываний
Обработка любых аппаратных прерываний начинается с шаблона, который реализован в функции KiInterruptTemplate(). Она сохраняет текущее состояние ядра в стеке (известное как контекст), после чего передаёт управление конкретно взятому обработчику прерываний – этот и есть диспетчеризация. Фактически каждый обработчик в таблице IDT (см.второй столбец выше) указывает на шаблон Template, который позже вызывает непосредственно KiInterruptDispatch(). В этом легко убедиться, если дизассемблировать адреса например клавы и мыши по векторам
Как видим, по разным адресам лежит один и тот-же шаблон – он отличается только аргументом инструкции
Однако ключом к диспетчеризации прерываний является структура KINTERRUPT, которая содержит всю необходимую информацию для вызова зареганой драйвером процедуры ISR. Как и в предыдущем случаем, система имеет таблицу указателей на каждую из KINTERRUPT, но поскольку эта структура описывает только аппаратные прерывания, записей в таблице гораздо меньше 256, и расширяется она динамически по числу активного оборудования на данный момент. Адрес этой таблицы хранится в структуре KPCR процессора (Processor Control Region), а конкретно в её поле KPCR->CurrentPrcb->InterruptObjectPool. Вектор прерывания используется как индекс в этой таблице, чтобы получить указатель на соответствующую структуру. Возьмём из лога
Процедура KiInterruptDispatch() получает всего один аргумент от KiInterruptTemplate() – линк на объект прерывания KINTERRUPT. Далее она читает из этой структуры поле(18) ServiceRoutine и передаёт на него управление. Ясно, что это алгоритм только в общих чертах, а нам больше пока и не нужно.
3. IRQL – уровни запросов на прерывание
Теперь задумаемся, что произойдёт, когда в произвольный момент времени сразу несколько устройств запросят у ЦП прерывание. Очевидно возникнет логический тупик и процессору придётся выбрать из всех клиентов одного, ..потом следущего и так, пока не обслужит последнего. Чтобы сбросить баласт подобного рода проблем, инженеры ввели «схему приоритетов прерываний», обозначив её как IRQ-Level. Здесь нужно отметить, что понимание механизма IRQL играет важную роль в разработке драйверов, без которой написать исправно работающий драйвер просто невозможно.
По неизвестной причине, в статьях из гугла (как и у М.Руссиновича в «Устройстве Windows») описываются сразу последствия исполнения кода на разных уровнях, и создаётся впечатление «чего-то тут не хватает». Однако воспользовавшись отладчиком можно копнуть глубже и найти сам тумблер, который задаёт приоритеты прерываниям, т.е. как это дело реализуется на физическом уровне.
На рисунке ниже все прерывания IRQ расставлены по приоритетам – на системах х32 уровней всего 32, а на х64 в два раза меньше 16. Потоки пользовательских приложений обычно исполняются на уровне PASSIVE_LEVEL(0), поэтому работающий на DISPATCH(2) системный планировщик может легко их приостановить, чтобы дать возможность отработать следующим потокам в очереди – именно так и реализуется многозадачность в Win.
Но если во-время работы нашего потока любое из физ.устройств по своей линии IRQ надумает послать прерывание, то c уровня(0) процессор сразу переключится на уровень(3) и выше, которые известны как DIRQL от слова «Device». Это может быть нажатие пимпы на клавиатуре, движ мышью, приход пакета по сети, и многое другое. При этом ладно прервётся исполнение нашего юзер-потока, так ещё и системный планировщик останется за бортом, пока процессор не обработает прерывание, и IRQL опять не станет <=2.
Другими словами, начиная с уровня DIRQL(3) планировщик уже не работает, ..как не работает и подкачка виртуальных страниц в/из Pagefile.sys. Это накладывает ограничения на код драйвера устройств – когда не критично, он должен функцией KeLowerIrql() сбрасывать уровень до PASSIVE(0), а если такой возможности нет, то его процедуры не должны занимать много времени. Важно просто запомнить, что на уровне DIRQL поток обладает уже исключительным правом на исполнение (т.е. по сути отключается многозадачность), и его может вытеснить только авария по питанию, или установивший «спин-блокировку» поток на другом ядре, о чем говорится далее.
А с памятью так вообще беда. Если находясь на уровне(0) драйвер выделит себе вирт.страницу, то обращаться к ней с уровня DIRQL становится опасно, поскольку в самый неподходящий момент система может выгрузить её в своп без возможности вернуть обратно. Поэтому драйвера запрашивают ресурсы из невыгружаемого пула NonPagedPool, чтобы гарантированно иметь к ним доступ с любого уровня. Функция KfRaiseIrql() способна и наоборот повысить IRQL процессора, однако в большинстве случаях ручные манипуляции не требуются, т.к. большинство вызываемых API сами под катом устанавливают нужны себе уровни.
Значение IRQL величина не постоянная и в зависимости от вызываемых драйвером API, процессор может динамически менять её несколько сотен раз. Это потому, что чем выше уровень запроса, тем ниже перечень доступных услуг, и наоборот. Так, оптимальным считается уровень DISPATCH(2), чтобы код драйвера имел приоритет над юзер-потоками, но в основном большую часть времени драйвера проводят на пользовательском уровне(0) (не путать с пространством). Каждое ядро процессора имеет свой собственный IRQL, т.е. когда одно работает на DISPATCH_LEVEL(2), другое может находиться на PASSIVE(0).
Все API, которым требуется менять IRQL, на входе через KeGetCurrentIrql() обязательно сохраняют текущее его значение, а на выходе опять восстанавливают. Если заглянуть на MSDN в поисках прототипа какого-нибудь API, в описании всегда будет требуемый уровень, с которого можно вызывать данную функцию. Вот неплохой
3.1. IRQ - линии прерываний от устройств к IOAPIC
До этого момента мы обсуждали «коня в вакууме», и теперь попытаемся вдохнуть в него жизнь. Ответим на вопросы: кто и на каком этапе распределяет номера линиям IRQ, а так-же как организована поддержка приоритетов прерываний.
Значит основной шиной в архитектуре х86 остаётся параллельная PCI (Peripheral Сomponent Interconnect). Почти всё внешнее и внутреннее оборудование подключается именно к ней. При загрузке ОС, драйвер pci.sys начинает сканирование устройств на шине, передавая эту информацию сразу диспетчеру Plug&Play. Далее диспетчер PnP запрашивает у девайсов их требования к ресурсам, и сохраняет эти данные в своём конфигурационном пространстве «PCI-Config-Space». Каждое из устройств обязано возвратить стандартный блок информации размером в 256-байт. В списке ниже перечислены 4 типа ресурсов, которые девайс может потребовать у системы для правильной своей работы:
Бит(10) в командном регистре называется «Interrupt Disable» и если он взведён, значит устройство не использует линию прерывания IRQ. Таким образом, именно диспетчер PnP нумерует линии по устройствам, в тесном сотрудничестве с диспетчером ACPI (Advanced Config & Power Interface), который отвечает уже за арбитраж и поддержку общих «IRQ Sharing» (иногда одну линию могут делить между собой сразу несколько устройств). На команду
3.2. IRQL – физическая реализация
Прерывания от устройств по линиям IRQ доставляются в IOAPIC, на выходе из которого получаем вектор. Далее этот вектор заходит в Local-APIC, от куда его и считывает процессор. Векторы имеют приоритет IRQL, который явно зависит от номера самого вектора. LAPIC использует IRQL для определения того, когда обслуживать данное прерывание, относительно других действий процессора.
Векторы в диапазоне 0:15 отводятся для ошибок и исключений (генерируются самим процессором), а векторы с номерами 16:31 лежат в резерве для программного обеспечения. Тогда выходит, что векторов, которым можно задать классы остаётся всего 256-32=224 (аппаратные прерывания). Каждый класс приоритета охватывает 16 векторов, а потому всего классов 224/16=14, от IRQL=1 до 14.
Разрядность вектора 8-бит, а класс приоритета кодируется в его битах[7:4]. Самый низкий класс 1, а самый высокий 15. Оставшиеся биты[3:0] определяют относительный приоритет уже внутри класса – чем выше их значение, тем выше приоритет. Таким образом, каждый вектор состоит из двух частей: старшие 4-бита указывают на его класс, а младшие 4-бита определяют ранг в классе.
Кроме того LAPIC имеет 4 внутренних регистра для управления классами приоритетов:
Рассмотрим значения регистров LAPIC, на примере вектора прерывания от клавиатуры
Вектор сразу поступает в 256-битный регистр
Но если во время обработки прерывания в регистр
Векторы прерываний будут поступать в 256-битный регистр
В обход LAPIC регистром
Отметим, что помимо аппаратных прерываний имеются и другие типы, приоритеты которых не подчиняются классам – они обходят регистры
Всё-что касается cpu, Win собирает в области памяти под названием PCR (Processor Control Region). Стандартную область описывает структура KPCR, а сразу за ней следует и блок с расширенной информацией KPRCB (Processor Region Control Block). Размер всей области на моей х64 Win7 составляет ~20КБ, при этом каждое ядро процессора имеет свой отдельный регион такого-же размера. Система сохраняет указатель на него в регистре cpu
Команда отладчика
На скрине выше видно, что значения большинства глобальных регистров во-втором блоке сброшены в нуль. Однако если запросить структуру явно, то отладчик возвращает уже валиндые указатели. В качестве пруфов вышесказанному, я дизассемблировал три функции для работы с IRQL – запрос Get, повышение Raise, и понижение Lower. Как и следовало ожидать, все они обращаются к регистру процессора
Заключение
Любая практика должна начинаться с теории, и думаю в этой статье её получилось достаточно. Это фундаментальная база, на которой держится программирование драйверов. В следующей части рассмотрим софт-составляющую, после чего напишем примитивный дров. Надеюсь после прочтения всех четырёх частей вы сможете самостоятельно создавать и более сложные процедуры обслуживания прерываний, а пока пробьём промежуток между собой и диваном, чтобы выйдя из матрицы войти в зону умеренного комфорта. Всем удачи, до скорого!
В этой части:
1. Базовые сведения
2. IRQ – типы прерываний и системная таблица IDT
3. IRQL – уровни запросов на прерывание
1. Базовые сведения
На сегодняшний день имеем три архитектурных моделей драйверов – это унаследованные от Win2k Legacy (NT4), более продвинутые с технологией PnP WDM (NT5, Windows Driver Model), и современные для Vista+ WDF (NT6 и выше, Driver Foundation).
Под Legacy подразумеваются драйверы режима ядра в стиле «Kernel Services». Они отличаются тем, что не поддерживают технологию PnP. Но если мы хотим написать драйвер, который например отслеживает создание процесса, или просто читает ядерную память, то PnP нам и не нужен, т.к. нет работы с оборудованием и управления его питанием. Нам нужен минимум ненужного хлама, а потому примитивный Legacy будет здесь хорошим выбором. По сути название «устаревший» неверно – более точно было-бы сказать «не-PnP-драйвер», или «драйвер на основе служб».
Но в системе есть много мест, где эти драйвера не подходят по определению, например фильтрация чего-либо в дереве устройств PnP, присоединения своего драйвера в стек уже имеющихся драйверов, и многое другое – здесь без WDM никак не обойтись. В свою очередь, в класс WDF входят подклассы KMDF и UMDF (kernel/user mode). Они полностью абстрагируют нас от прямого взаимодействия с железом, где-то далеко предоставляя свои обёртки. Получается быстро и качественно, но без (так нужного нам) понимания принципов. В свой основе WDF опирается на WDM, а потому мы сделаем акцент именно на последнем.
Взаимодействие с драйверами устройств построено на прерываниях «Interrupt», а ключевым понятием здесь является IRQ или «Interrupt Request». Прерывания драйверам могут посылать как аппаратное оборудование на материнской плате, так и системные процессы, в т.ч. и софт пользовательского режима, например Breakpoint
int-3
. Таким образом имеем две категории драйверов, которые отличаются на уровне функционала – это драйверы устройств «Device Driver», и программные драйвера «Soft Driver».Прерывания от устройств всегда асинхронны. Это означает, что отправив запрос мы можем ждать ответа хоть целую вечность. Обычно драйвер устройства выполняет только минимум самых важных функций из всей процедуры обработки прерывания, а остальная часть ставится системой в специальную очередь отложенных вызовов DPC (Deferred Procedure Call). Соответственно чем больше запросов, тем длиннее очередь. Зато программные прерывания относятся к синхронным, и выполнятся сразу от начала и до своего логического конца.
2. IRQ – линия запроса на прерывание, и системная таблица IDT
Система прерываний стара как мир – на платах с древним i80286 прерывания от устройств по проводникам IRQ поступали в пару соединённых каскадно контролёров PIC i8259. Каждый из них имел по 8 входных линий и одну на выходе, что в сумме давало всего 15 входов (IRQ2 отводилась для связи между собой). Получив логическую единицу на одной из этих линий, PIC превращал её в вектор, который доставлял к контакту процессора INTR. В результате ЦП прерывал текущую свою работу, и используя вектор как индекс в системной таблице прерываний, выбирал из неё адрес соответствующей процедуры обработки ISR «Interrupt Service Routine».
Но с появлением многопроцессорных симметричных систем SMP (несколько ядер) инженерам пришлось отказаться от грязной затеи каскадных связей, в пользу расширенного контролёра IO-APIC 82093AA (Advanced Programmable Interrupt Controller). Во-первых число его входов IRQ увеличилось с 15 до 24, а во-вторых линия на выходе теперь кодировалась 4-битным значением, что позволило перенаправлять вектор одному из 2^4=16 ядер процессора. Позже по накатаной появился xAPIC (eXtended) с 8-битным выходом =256 ядер, и на данный момент во-всех чипсетах используется его расширение
Ссылка скрыта от гостей
вообще с 20-битной линией out. С того времени пин INTR процессора используется по прямому назначению только в момент ребута ОС, доставляя события лишь первому из ядер cpu, которое известно как BSP или «Boot-Strap Processor». Это продемонстрировано на схеме ниже:Для связи с IOAPIC процессоры имеют локальные контролёры LAPIC, способные принимать прерывания от девайсов на шине PCI-Express. Такие прерывания пакуются в сообщения MSI, и в обход IOAPIC доставляются прямиком в специально выделенную для этих целей память. Между собой ядра ЦП общаются посредством прерываний IPI «Inter-Processor Interrupt». Например когда одно ядро занято, оно может поручить обработку прерывания соседним ядрам, выставив его на межпроцессорную шину.
2.1. IDT – Interrupt Dispatch Table
Получив вектор прерывания от LAPIC, процессор обращается к системной таблице диспетчеризации прерываний IDT, чтобы найти в ней адрес обработчика сложившейся ситуации. Таким образом вектор – это просто порядковый номер дескриптора в IDT. Адрес таблицы прописан в регистре процессора
IDTR
, так-что остаётся просто перейти по указанному вектору. В таблице Win всего 256 16-байтных дескрипторов, и соответственно для всей IDT выделяется 256*16=4096 байт, или одна страница вирт.памяти. Дескрипторы описывает структура ядра KIDTENTRY64.Значение регистра
IDTR
читает команда r
отладчика – это будет дескриптор вектора(0). Далее прибавив 16 найдём следующий(1), и т.д. Указатель на обработчик прерывания размазан по трём полям дескриптора OffsetHigh-Middle-Low
. Из атрибутов: селектор секции-кода ядра на системах х64 всегда =10h (на х32 8), Type=0Еh (шлюз прерывания, или 0Fh шлюз ловушки), Dpl=0 (кольцо защиты), и P=1 (находится в памяти): Подробное описание полей дескрипторов можно найти в манах Интела том(3).
Код:
0: kd> r @idtr, @idtl
idtr = 0xfffff80000b91080 idtl = 0x0fff ;<----// адрес и размер таблицы
0: kd> dt _KIDTENTRY64 0xfffff80000b91080
nt!_KIDTENTRY64
+0x000 OffsetLow : 0xcf00
+0x002 Selector : 0x10
+0x004 Type : 0y01110 (0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006 OffsetMiddle : 0x02cc
+0x008 OffsetHigh : 0xfffff800 <--- ISR: 0xfffff800`02cccf00
0: kd> dt _KIDTENTRY64 0xfffff80000b91080 + 0x10
nt!_KIDTENTRY64
+0x000 OffsetLow : 0xd000
+0x002 Selector : 0x10
+0x004 Type : 0y01110 (0xe)
+0x004 Dpl : 0y00
+0x004 Present : 0y1
+0x006 OffsetMiddle : 0x02cс
+0x008 OffsetHigh : 0xfffff800 <--- ISR: 0xfffff800`02ccd000
Командой
!idt
отладчика можно получить указатели на ISR всех прерываний. Первые 20 векторов Win резервирует под исключения, следущие 30 для программных прерываний, а оставшиеся отводятся уже для асинхронных прерываний от физ.оборудования. В первом столбце лога ниже указан вектор, во-втором адрес его обработчика ISR, в третьем имя функции в ядре, и в четвёртом указатель на структуру KINTERRUPT, описывающей детали аппаратных прерываний. Как видим не у каждого вектора имеется своя процедура обслуживания – некоторые из них свободны:
Код:
0: kd> !idt
Dumping IDT:
;-----// Исключения Exception, ошибки Fault, и ловушки Trap: int 00h+
00: fffff80002c7df00 nt!KiDivideErrorFault
01: fffff80002c7e000 nt!KiDebugTrapOrFault
02: fffff80002c7e1c0 nt!KiNmiInterrupt Stack = 0xFFFFF80000BA3000
03: fffff80002c7e540 nt!KiBreakpointTrap
04: fffff80002c7e640 nt!KiOverflowTrap
05: fffff80002c7e740 nt!KiBoundFault
06: fffff80002c7e840 nt!KiInvalidOpcodeFault
07: fffff80002c7ea80 nt!KiNpxNotAvailableFault
08: fffff80002c7eb40 nt!KiDoubleFaultAbort Stack = 0xFFFFF80000BA1000
09: fffff80002c7ec00 nt!KiNpxSegmentOverrunAbort
0a: fffff80002c7ecc0 nt!KiInvalidTssFault
0b: fffff80002c7ed80 nt!KiSegmentNotPresentFault
0c: fffff80002c7eec0 nt!KiStackFault
0d: fffff80002c7f000 nt!KiGeneralProtectionFault
0e: fffff80002c7f140 nt!KiPageFault
10: fffff80002c7f500 nt!KiFloatingErrorFault
11: fffff80002c7f680 nt!KiAlignmentFault
12: fffff80002c7f780 nt!KiMcheckAbort Stack = 0xFFFFF80000BA5000
13: fffff80002c7fb00 nt!KiXmmException
;-----// Синхронные прерывания от программ: int 14h+
1f: fffff80002ccbf10 nt!KiApcInterrupt
2c: fffff80002c7fcc0 nt!KiRaiseAssertion
2d: fffff80002c7fdc0 nt!KiDebugServiceTrap
2f: fffff80002ccc1f0 nt!KiDpcInterrupt
;-----// Асинхронные прерывания от аппаратуры: int 30h+
51: fffffa80022915d0 i8042prt!I8042KeybInterruptService (KINTERRUPT fffffa8002291540)
52: fffffa8002291b10 USBPORT+0x2D344 (KINTERRUPT fffffa8002291a80)
61: fffffa8002291690 nuvserial+0x2B88 (KINTERRUPT fffffa8002291600)
62: fffffa8002291c90 ataport+0xCB4C (KINTERRUPT fffffa8002291c00)
71: fffffa8002291750 nuvserial+0x2B88 (KINTERRUPT fffffa80022916c0)
72: fffffa8002291d50 ataport+0xCB4C (KINTERRUPT fffffa8002291cc0)
92: fffffa8002291e10 HDAudBus+0x3F20 (KINTERRUPT fffffa8002291d80)
b1: fffffa8002291f90 ACPI+0x119C8 (KINTERRUPT fffffa8002291f00)
b2: fffffa8002291510 i8042prt!I8042MouseInterruptService (KINTERRUPT fffffa8002291480)
b3: fffffa8002291990 USBPORT+0x2D344 (KINTERRUPT fffffa8002291900)
c1: fffff800032273b0 hal!HalInitializeProcessor+0x3E8 (KINTERRUPT fffff80003227320)
d1: fffff80003227450 hal!HalReturnToFirmware+0xAA0 (KINTERRUPT fffff800032273c0)
e1: fffff80002c8afa0 nt!KiIpiInterrupt
fe: fffff80003227630 hal!HalInitializeProcessor+0x674 (KINTERRUPT fffff800032275a0)
ff: 0000000000000000
2.2. Диспетчеризация прерываний
Обработка любых аппаратных прерываний начинается с шаблона, который реализован в функции KiInterruptTemplate(). Она сохраняет текущее состояние ядра в стеке (известное как контекст), после чего передаёт управление конкретно взятому обработчику прерываний – этот и есть диспетчеризация. Фактически каждый обработчик в таблице IDT (см.второй столбец выше) указывает на шаблон Template, который позже вызывает непосредственно KiInterruptDispatch(). В этом легко убедиться, если дизассемблировать адреса например клавы и мыши по векторам
0x51
и 0xB2
соответственно (ключ /i возвращает кол-во инструкций):
Код:
0: kd> !idt 51 b2
Dumping IDT:
51: fffffa80022915d0 i8042prt!I8042KeybInterruptService (KINTERRUPT fffffa8002291540)
b2: fffffa8002291510 i8042prt!I8042MouseInterruptService (KINTERRUPT fffffa8002291480)
0: kd> uf /i fffffa80022915d0; uf /i fffffa8002291510
Flow analysis was incomplete. 4 instructions scanned
fffffa80`022915d0 50 push rax
fffffa80`022915d1 55 push rbp
fffffa80`022915d2 488d2d67ffffff lea rbp,[fffffa80`02291540]
fffffa80`022915d9 ff6550 jmp qword ptr [rbp+50h]
Flow analysis was incomplete. 4 instructions scanned
fffffa80`02291510 50 push rax
fffffa80`02291511 55 push rbp
fffffa80`02291512 488d2d67ffffff lea rbp,[fffffa80`02291480]
fffffa80`02291519 ff6550 jmp qword ptr [rbp+50h]
0: kd>
Как видим, по разным адресам лежит один и тот-же шаблон – он отличается только аргументом инструкции
lea rbp,[xx]
. В ядре имеется спец.таблица из 256 таких шаблонов, для векторов в IDT от 0x00
до 0xff
, а указатель на неё хранит переменная ядра nt!KiIsrThunk().Однако ключом к диспетчеризации прерываний является структура KINTERRUPT, которая содержит всю необходимую информацию для вызова зареганой драйвером процедуры ISR. Как и в предыдущем случаем, система имеет таблицу указателей на каждую из KINTERRUPT, но поскольку эта структура описывает только аппаратные прерывания, записей в таблице гораздо меньше 256, и расширяется она динамически по числу активного оборудования на данный момент. Адрес этой таблицы хранится в структуре KPCR процессора (Processor Control Region), а конкретно в её поле KPCR->CurrentPrcb->InterruptObjectPool. Вектор прерывания используется как индекс в этой таблице, чтобы получить указатель на соответствующую структуру. Возьмём из лога
!idt
линк на описатель Keyboard, и вскормим его команде dt
:
Код:
0: kd> dt _KINTERRUPT fffffa8002291540
nt!_KINTERRUPT
+0x000 Type : 0n22
+0x002 Size : 0n160
+0x008 InterruptList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x018 ServiceRoutine : 0xfffff880`01083a04 i8042prt!I8042KeybInterruptService+0
+0x020 MessageRoutine : (null)
+0x028 MessageIndex : 0
+0x030 ServiceContext : 0xfffffa80`02b0f7f0 void
+0x038 SpinLock : 0
+0x040 TickCount : 0
+0x048 ActualLock : 0xfffffa80`02b0f950 -> 0
+0x050 DispatchAddress : 0xfffff800`02c7ce70 void nt!KiInterruptDispatch+0
+0x058 Vector : 0x51
+0x05c Irql : 0x5
+0x05d SynchronizeIrql : 0xb
+0x05f Connected : 0x1
+0x068 Mode : 1 ( Latched )
+0x06c Polarity : 0 ( InterruptPolarityUnknown )
+0x070 ServiceCount : 0
+0x074 DispatchCount : 0
+0x080 TrapFrame : 0xfffff800`00b98ac0 _KTRAP_FRAME
+0x090 DispatchCode : [4] 0x8d485550
0: kd>
;//-------------------
08 – связанный список на случай, когда одну линию IRQ делят несколько устройств
18 – указатель на процедуру обслуживания ISR в драйвере данного устройства
20 – адрес ISR для прерываний MSI (на шине PCI-Ex)
38 – состояние спин-блокировки (обсудим позже)
50 – линк на процедуру диспетчеризации данного прерывания, для последующего вызова ISR
5С – текущий приоритет запроса; и 5D – требуемый приоритет во время выполнения ISR
80 – адрес фрейма в стеке, где лежит сохранённый контекст ядра
90 – код шаблона Template -----------------------------------------+
|
0: kd> uf 0xfffffa8002291540 + 0x90 <-----------------------------+
Flow analysis was incomplete, some code may be missing
fffffa80`022915d0 50 push rax
fffffa80`022915d1 55 push rbp
fffffa80`022915d2 488d2d67ffffff lea rbp,[fffffa80`02291540]
fffffa80`022915d9 ff6550 jmp qword ptr [rbp+50h] <---- поле DispatchAddress
0: kd>
Процедура KiInterruptDispatch() получает всего один аргумент от KiInterruptTemplate() – линк на объект прерывания KINTERRUPT. Далее она читает из этой структуры поле(18) ServiceRoutine и передаёт на него управление. Ясно, что это алгоритм только в общих чертах, а нам больше пока и не нужно.
3. IRQL – уровни запросов на прерывание
Теперь задумаемся, что произойдёт, когда в произвольный момент времени сразу несколько устройств запросят у ЦП прерывание. Очевидно возникнет логический тупик и процессору придётся выбрать из всех клиентов одного, ..потом следущего и так, пока не обслужит последнего. Чтобы сбросить баласт подобного рода проблем, инженеры ввели «схему приоритетов прерываний», обозначив её как IRQ-Level. Здесь нужно отметить, что понимание механизма IRQL играет важную роль в разработке драйверов, без которой написать исправно работающий драйвер просто невозможно.
По неизвестной причине, в статьях из гугла (как и у М.Руссиновича в «Устройстве Windows») описываются сразу последствия исполнения кода на разных уровнях, и создаётся впечатление «чего-то тут не хватает». Однако воспользовавшись отладчиком можно копнуть глубже и найти сам тумблер, который задаёт приоритеты прерываниям, т.е. как это дело реализуется на физическом уровне.
На рисунке ниже все прерывания IRQ расставлены по приоритетам – на системах х32 уровней всего 32, а на х64 в два раза меньше 16. Потоки пользовательских приложений обычно исполняются на уровне PASSIVE_LEVEL(0), поэтому работающий на DISPATCH(2) системный планировщик может легко их приостановить, чтобы дать возможность отработать следующим потокам в очереди – именно так и реализуется многозадачность в Win.
Но если во-время работы нашего потока любое из физ.устройств по своей линии IRQ надумает послать прерывание, то c уровня(0) процессор сразу переключится на уровень(3) и выше, которые известны как DIRQL от слова «Device». Это может быть нажатие пимпы на клавиатуре, движ мышью, приход пакета по сети, и многое другое. При этом ладно прервётся исполнение нашего юзер-потока, так ещё и системный планировщик останется за бортом, пока процессор не обработает прерывание, и IRQL опять не станет <=2.
Другими словами, начиная с уровня DIRQL(3) планировщик уже не работает, ..как не работает и подкачка виртуальных страниц в/из Pagefile.sys. Это накладывает ограничения на код драйвера устройств – когда не критично, он должен функцией KeLowerIrql() сбрасывать уровень до PASSIVE(0), а если такой возможности нет, то его процедуры не должны занимать много времени. Важно просто запомнить, что на уровне DIRQL поток обладает уже исключительным правом на исполнение (т.е. по сути отключается многозадачность), и его может вытеснить только авария по питанию, или установивший «спин-блокировку» поток на другом ядре, о чем говорится далее.
А с памятью так вообще беда. Если находясь на уровне(0) драйвер выделит себе вирт.страницу, то обращаться к ней с уровня DIRQL становится опасно, поскольку в самый неподходящий момент система может выгрузить её в своп без возможности вернуть обратно. Поэтому драйвера запрашивают ресурсы из невыгружаемого пула NonPagedPool, чтобы гарантированно иметь к ним доступ с любого уровня. Функция KfRaiseIrql() способна и наоборот повысить IRQL процессора, однако в большинстве случаях ручные манипуляции не требуются, т.к. большинство вызываемых API сами под катом устанавливают нужны себе уровни.
Значение IRQL величина не постоянная и в зависимости от вызываемых драйвером API, процессор может динамически менять её несколько сотен раз. Это потому, что чем выше уровень запроса, тем ниже перечень доступных услуг, и наоборот. Так, оптимальным считается уровень DISPATCH(2), чтобы код драйвера имел приоритет над юзер-потоками, но в основном большую часть времени драйвера проводят на пользовательском уровне(0) (не путать с пространством). Каждое ядро процессора имеет свой собственный IRQL, т.е. когда одно работает на DISPATCH_LEVEL(2), другое может находиться на PASSIVE(0).
Все API, которым требуется менять IRQL, на входе через KeGetCurrentIrql() обязательно сохраняют текущее его значение, а на выходе опять восстанавливают. Если заглянуть на MSDN в поисках прототипа какого-нибудь API, в описании всегда будет требуемый уровень, с которого можно вызывать данную функцию. Вот неплохой
Ссылка скрыта от гостей
на тему «управление приоритетами».3.1. IRQ - линии прерываний от устройств к IOAPIC
До этого момента мы обсуждали «коня в вакууме», и теперь попытаемся вдохнуть в него жизнь. Ответим на вопросы: кто и на каком этапе распределяет номера линиям IRQ, а так-же как организована поддержка приоритетов прерываний.
Значит основной шиной в архитектуре х86 остаётся параллельная PCI (Peripheral Сomponent Interconnect). Почти всё внешнее и внутреннее оборудование подключается именно к ней. При загрузке ОС, драйвер pci.sys начинает сканирование устройств на шине, передавая эту информацию сразу диспетчеру Plug&Play. Далее диспетчер PnP запрашивает у девайсов их требования к ресурсам, и сохраняет эти данные в своём конфигурационном пространстве «PCI-Config-Space». Каждое из устройств обязано возвратить стандартный блок информации размером в 256-байт. В списке ниже перечислены 4 типа ресурсов, которые девайс может потребовать у системы для правильной своей работы:
1. Номер линий запроса на прерывание IRQ
2. Каналы прямого доступа к памяти DMA
3. Адреса портов ввода-вывода I/O
4. Диапазоны адресов памяти.
Бит(10) в командном регистре называется «Interrupt Disable» и если он взведён, значит устройство не использует линию прерывания IRQ. Таким образом, именно диспетчер PnP нумерует линии по устройствам, в тесном сотрудничестве с диспетчером ACPI (Advanced Config & Power Interface), который отвечает уже за арбитраж и поддержку общих «IRQ Sharing» (иногда одну линию могут делить между собой сразу несколько устройств). На команду
!arbiter
отладчик отзывается логом ниже – как видим, IRQ #10,11,13,17 являются разделяемыми. Аргумент команды фильтрует вывод в лог: 1=порты, 2=память, 4=IRQ, и 8=DMA:
Код:
0: kd> !arbiter 4
DEVNODE fffffa800217c410 (ACPI_HAL\PNP0C08\0)
Interrupt Arbiter "ACPI_IRQ" at fffff88000ec03c0
Allocated ranges:
01 - 01 fffffa800224f280 (i8042prt) <-----// линии IRQ
02 - 02 B fffffa800224fc20
03 - 03 fffffa8002250e40 (Serial)
04 - 04 fffffa8002250060 (Serial)
08 - 08 fffffa800224fa00
09 - 09 S fffffa80018f5720 (ACPI)
0c - 0c fffffa8002250c20 (i8042prt)
0d - 0d B fffffa800224f4a0
0e - 0e fffffa8002283b40 (atapi)
0f - 0f fffffa8002282830 (atapi)
10 - 10 S fffffa8001862060 (pci) <-----// на IRQ.10 висят 4 устройства!
10 - 10 S fffffa8001861060 (igfx)
10 - 10 S fffffa8001861a10 (HDAudBus)
10 - 10 S fffffa8001864a10 (usbuhci)
11 - 11 S fffffa8001862a10 (pci)
11 - 11 S fffffa8002277a10 (RTL8167)
12 - 12 S fffffa8001864060 (usbuhci)
13 - 13 S fffffa8001867060 (intelide)
13 - 13 S fffffa8001863a10 (usbuhci)
17 - 17 S fffffa8001863060 (usbuhci)
17 - 17 S fffffa8001865060 (usbehci)
Possible allocation: < none > <-----// нет переназначений
0: kd>
3.2. IRQL – физическая реализация
Прерывания от устройств по линиям IRQ доставляются в IOAPIC, на выходе из которого получаем вектор. Далее этот вектор заходит в Local-APIC, от куда его и считывает процессор. Векторы имеют приоритет IRQL, который явно зависит от номера самого вектора. LAPIC использует IRQL для определения того, когда обслуживать данное прерывание, относительно других действий процессора.
Векторы в диапазоне 0:15 отводятся для ошибок и исключений (генерируются самим процессором), а векторы с номерами 16:31 лежат в резерве для программного обеспечения. Тогда выходит, что векторов, которым можно задать классы остаётся всего 256-32=224 (аппаратные прерывания). Каждый класс приоритета охватывает 16 векторов, а потому всего классов 224/16=14, от IRQL=1 до 14.
Разрядность вектора 8-бит, а класс приоритета кодируется в его битах[7:4]. Самый низкий класс 1, а самый высокий 15. Оставшиеся биты[3:0] определяют относительный приоритет уже внутри класса – чем выше их значение, тем выше приоритет. Таким образом, каждый вектор состоит из двух частей: старшие 4-бита указывают на его класс, а младшие 4-бита определяют ранг в классе.
Кроме того LAPIC имеет 4 внутренних регистра для управления классами приоритетов:
1.
TPR
(Task Priority) : 64 битный регистр, хранит IRQL исполняемых потоков и процедур (R/W)2.
PPR
(Processor Priority) : 64 битный, определяет текущее значение IRQL процессора (R)3.
IRR
(Interrupt Request) : 256 битный, двоичная маска векторов ожидающих исполнение (R/W)4.
ISR
(In Service) : 256 битный, маска вектора, который исполняется на текущий момент (R)Рассмотрим значения регистров LAPIC, на примере вектора прерывания от клавиатуры
0x51=81
.Вектор сразу поступает в 256-битный регистр
IRR
, в котором тут-же взводится бит(81). Теперь логика контролёра делит значение вектора на две части по 4-бита, и получает маску 81=0101.0001
. Как результат имеем класс приоритета запроса IRQL=5 из возможных 14-ти, и подкласс(1). Далее значение вектора копируется в регистр TPR
, процессор его считывает от туда, и по вектору обращается к системной таблице IDT за указателем на обработчик прерывания. Пока всё ясно, и особых вопросов не вызывает.Но если во время обработки прерывания в регистр
IRR
залетит ещё один вектор, например от мыши 0xB2=178
, то в нём взведётся на этот раз уже бит(178), и получим маску 178=1011.0010
, что трансформируется в IRQL=11 с рангом(2). Поскольку класс приоритета(11) выше, чем у исполняющегося на данный момент класса(5), то LAPIC не дожидаясь сигнала от процессора в своём регистре EOI
(End-Of-Interrupt), через регистр TPR
пошлёт ему значение нового вектора 0xB2
, что вынудит процессор прервать текущую задачу, и сохранив её контекст переключиться на новую.Векторы прерываний будут поступать в 256-битный регистр
IRR
снова и снова, но LAPIC будет выбирать их только в порядке очерёдности слева-направо (от старшего бита к младшему). Как только процессор закончит обработку прерывания, он отчитается перед LAPIC записью в его регистр EOI
. Реакцией LAPIC на это событие будет сброс отработанного бита в нуль в регистре IRR
, освободив тем-самым место для очередного прерывания с таким-же вектором.В обход LAPIC регистром
TPR
система может управлять и программно, для чего предусмотрен внешний по отношению к LAPIC контрольный регистр CR8
. Запись в него тут-же оказывает влияние на TPR
, чем пользуются драйверы для повышения своих привилегий. Надеюсь схема ниже поможет разобраться во-всей этой кухне (позаимствована из манов Интела том 3):Отметим, что помимо аппаратных прерываний имеются и другие типы, приоритеты которых не подчиняются классам – они обходят регистры
TPR/IRR/ISR
и отправляются непосредственно в ядро процессора. Это немаскируемые NMI (Non Maskable), SMI (System Management), и INIT (инициализация при вкл.машины).Всё-что касается cpu, Win собирает в области памяти под названием PCR (Processor Control Region). Стандартную область описывает структура KPCR, а сразу за ней следует и блок с расширенной информацией KPRCB (Processor Region Control Block). Размер всей области на моей х64 Win7 составляет ~20КБ, при этом каждое ядро процессора имеет свой отдельный регион такого-же размера. Система сохраняет указатель на него в регистре cpu
MSR.IA32_GS_BASE
, что представлено на рис.ниже.Команда отладчика
!pcr
дампит общие сведения только из основной структуры, где можно найти линк и на вложенную PRCB исполинских размеров. Обратите внимание на поле IRQL с текущим значением PASSIVE_LEVEL(0) – это ничто иное, как значение регистра PPR
контролёра LAPIC:На скрине выше видно, что значения большинства глобальных регистров во-втором блоке сброшены в нуль. Однако если запросить структуру явно, то отладчик возвращает уже валиндые указатели. В качестве пруфов вышесказанному, я дизассемблировал три функции для работы с IRQL – запрос Get, повышение Raise, и понижение Lower. Как и следовало ожидать, все они обращаются к регистру процессора
CR8
.
Код:
0: kd> dt _KPCR @$pcr
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : 0xfffff800`00b91000 _KGDTENTRY64
+0x008 TssBase : 0xfffff800`00b92080 _KTSS64
+0x010 UserRsp : 0x1cafdd8
+0x018 Self : 0xfffff800`02dffd00 _KPCR
+0x020 CurrentPrcb : 0xfffff800`02dffe80 _KPRCB
+0x028 LockArray : 0xfffff800`02e004f0 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : 0x000007ff`fffde000 void
+0x038 IdtBase : 0xfffff800`00b91080 _KIDTENTRY64 <---------//
+0x050 Irql : 0 <--------------------------------------//
+0x051 SecondCacheType : 0x8
+0x060 MajorVersion : 1
+0x062 MinorVersion : 1
+0x064 StallScaleFactor : 0x9c3
+0x080 KernelReserved : [15] 0
+0x0bc SecondLevelCacheSize : 0x200000
+0x0c0 HalReserved : [16] 0x950283d0
+0x108 KdVersionBlock : (null)
+0x118 PcrAlign1 : [24] 0
+0x180 Prcb : _KPRCB
0: kd> dt _KPRCB @$pcr + 0x180 Interrupt*
nt!_KPRCB
+0x006 InterruptRequest : 0
+0x4700 InterruptCount : 0x11a079f
+0x4710 InterruptTime : 0x27b
+0x4a00 InterruptObjectPool : _SLIST_HEADER
0: kd> dt _KPRCB @$pcr + 0x180 InterruptObjectPool.
nt!_KPRCB
+0x4a00 InterruptObjectPool :
+0x000 Alignment : 0x1b0004
+0x008 Region : 0xfffffa80`02291241
+0x000 HeaderX64 : <unnamed-tag>
;//-----------------------------------------------------
0: kd> uf nt!KeGetCurrentIrql
nt!KeGetCurrentIrql:
fffff800`02cd7180 440f20c0 mov rax,cr8
fffff800`02cd7184 c3 ret
0: kd> uf nt!KfRaiseIrql
nt!KfRaiseIrql:
fffff800`02cd7160 440f20c0 mov rax,cr8
fffff800`02cd7164 0fb6c9 movzx ecx,cl
fffff800`02cd7167 440f22c1 mov cr8,rcx
fffff800`02cd716b c3 ret
0: kd> uf nt!KeLowerIrql
nt!KeLowerIrql:
fffff800`02cd7150 0fb6c1 movzx eax,cl
fffff800`02cd7153 440f22c0 mov cr8,rax
fffff800`02cd7157 c3 ret
Заключение
Любая практика должна начинаться с теории, и думаю в этой статье её получилось достаточно. Это фундаментальная база, на которой держится программирование драйверов. В следующей части рассмотрим софт-составляющую, после чего напишем примитивный дров. Надеюсь после прочтения всех четырёх частей вы сможете самостоятельно создавать и более сложные процедуры обслуживания прерываний, а пока пробьём промежуток между собой и диваном, чтобы выйдя из матрицы войти в зону умеренного комфорта. Всем удачи, до скорого!