Предыдущая часть была посвящена линиям запроса на прерывания IRQ, и приоритетам IRQL. Но пока ещё к практике приступать рано, поскольку нужно дать определение самому драйверу, и его объектам устройств. Эти два понятия занимают огромную нишу в теме, и уложить материал в рамки одной статьи врядли получится. Поэтому сконцентрируем внимание только на основных моментах, оставив за бортом детали.
В этой части:
1. Схема передачи управления драйверам
Вспомним организацию виртуальной памяти системы.
Значит вся физическая ОЗУ делится на фреймы по 4 Кбайт. Такого-же размера кластер на жёстком диске, а так-же одна страница вирт.памяти. Суть виртуализации в том, что разным фреймам можно назначить одинаковый вирт.адрес, поэтому обращаясь по указателю например
При создании процесса система выделяет ему вирт.память в виде фреймов. Указатели на эту память хранятся в каталоге процесса PageDirectory, а линк на сам каталог в регистре
64-битный адрес, которым мы оперируем в коде считается линейным и состоит из 5-ти частей, что представлено на рисунке ниже (из 64 используются только 48-бит). Обратите внимание, что для адресации записей Entry, во-всех таблицах выделяются по 9-бит, а значит всего записей в каждой из таблиц по 2^9=512. Другими словами, корневая таблица PML4E одна, а вот кол-во таблиц PDPT, PD и PT уже может достигать 512-ти штук. Это позволяет в архитектуре х64 адресовать:
Значения записей PTE (PageTableEntry) всегда кратны
Все 64-битные версии Win распределяют память ядра следующим образом:
В арсенале отладчика WinDbg есть несколько расширений для просмотра информации в каталогах любых процессов, только нужно передавать им в качестве базы значение регистра
Это к тому, что когда для поддержки многозадачности планировщик переключает потоки, то системе приходится сохранять не только значения всех регистров, но и номера фреймов физ.памяти PFN, чтобы позже вернуться в прежнее состояние. Иначе поток не сможет найти свои данные, и план с треском провалится. Этот процесс известен как смена контекста потоков, или Context Switching. На склад отправляется так-же вложенная в EPROCESS основная структура процесса PCB «Process Control Block». На х32 контекст переключался аппаратно через сегмент состояния задачи TSS «Task State Segment», но сейчас реализация исключительно программная, а в TSS остался только указатель на стек ядра текущего потока:
Драйверы не имеют своих потоков в системе, а всегда исполняются только в контексте чужих. Например точка-входа DriverEntry() садится на один из многочисленных потоков процесса System – она исполняется только при загрузке драйвера, и по окончании инициализации освобождает System от своего присутствия. Зато остальные процедуры драйвера типа обработки прерываний ISR выполняются уже в контексте того потока, который был активным в момент возникновения самого прерывания. Часто в качестве транспорта драйвера используют потоки пользовательских приложений, что демонстрирует рис.ниже.
Смена контекста в пространстве пользователя может произойти всего по трём причинам:
2. Объект драйвера, и объект устройства
То, что мы привыкли называть драйвером, по факту является структурой DRIVER_OBJECT. Это основная база, которая (явно или через указатели) описывает буквально весь функционал и окружение драйвера. В нёдрах системы структуру создаёт диспетчер ввода-вывода, а в нашу процедуру DriverEntry() передаёт лишь её адрес. Радует то, что диспетчер берёт на себя все проблемы по оформлению связанных с драйвером структур, при этом большая часть их полей доступна только на чтение. Таким образом, соло играет именно диспетчер, а мы ему лишь в терцию подпеваем.
У любого драйвера WDM должно быть как минимум одно физическое или виртуальное/псевдо устройство, которым он будет управлять. Эти устройства драйвер создаёт сам функцией IoCreateDevice(). Например у pci.sys аж 20 подчинённых устройств, а у acpi вообще 44 девайса, и каждого из них олицетворяет своя структура DEVICE_OBJECT.
Однако вновь испечённое устройство пока скрыто от всех, и чтобы сделать его видимым тому-же пользователю, необходимо через IoCreateSymbolicLink() зарегать девайс в системном пространстве имён, обозвав его хоть именем любимого кота. Только после этого можно будет открыть устройство функцией юзера CreateFile(), и посылать ему команды IOCTL посредством DeviceIoControl().
WinDbg имеет следующие команды и расширения для сбора информации о драйверах:
•
•
•
•
•
•
Рассмотрим их на примере драйвера порта PS/2 i8042prt.sys.
В списке List видно, что дров обслуживает всего 2 устройства – проводную мышь и клаву.
Пока нам нужен только указатель на структуру DRIVER_OBJECT, чтобы сдампить её содержимое:
Все поля до смещения
Здесь видим точки-входа в процедуры DriverEntry(), Unload(), AddDevice(), после которых представлен массив запросов от клиентов IRP_MJ_xx (в структуре driver_object он начинается со-смещения 0x70, т.е. является её частью). Диспетчер сам заполняет все 28 полей данного массива, прописывая в них линки на свою заглушку IoCompleteRequest(), с кодом возврата невалидного запроса к устройству
Если в коде нашего драйвера мы планируем обрабатывать запросы API типа Create-Close-Read-WriteFile() и другие, то должны прописать в этот массив указатели на свои процедуры коллбек, тупо затирая предоставленные диспетчером заглушки. Все драйверы WDM обязаны обрабатывать коды POWER и PNP, а остальные уже на своё усмотрение. Если отсеить из этого списка всё лишнее, то получается, что дров i8042prt.sys может принимать только следующие запросы:
2.1. Объект устройства
Драйвер и его устройство всегда ходят парой. Если в функцию DriverEntry() диспетчер передаёт указатель на DRIVER_OBJECT, то все процедуры обработки запросов IRP_MJ_xx аргументом принимают уже указатель на DEVICE_OBJECT. Каждый девайс имеет свои свойства, которые задаются при его создании функцией IoCreateDevice(). Свойства включают в себя следущие поля в структуре:
Запросим у отладчика объект устройства подопытного i8042prt.sys, чтобы ознакомиться с полным его прототипом:
После инициализации драйвер бездействует. Пробудить его из глубокого сна может только приход пакета IRP от диспетчера ввода-вывода (Input/Output Request Packet). Физически это происходит в момент записи указателя в поле DEVICE_OBJECT --> CurrentIrp. Драйвер без объекта устройства не получает никаких IRP !!!
Структура IRP содержит буквально всю информацию о запросе, предписывая драйверу дальнейший фронт работы. Как только драйвер обработает текущий запрос и пошлёт IoCompleteRequest(), диспетчер удалит пакет этого запроса, и войдёт в цикл ожидания следующего. Таким образом IRP можно считать джокером во-всей подсистеме ввода-вывода, поскольку именно он задаёт всем темп.
IRP одна из немногих структур, поля которой доступны нам на запись. Это особенно актуально при двустороннем взаимодействии с кодом пользовательского режима. Например в аргументах DeviceIoControl() юзер передаёт адреса двух своих буферов – один на приём, другой на передачу. Так вот адреса этих буферов драйвер может получить только прочитав поля структуры IRP. Поскольку пакет создаётся самим диспетчером где-то в нёдрах системы (и мы не знаем где точно), он передаёт указатель на IRP во все наши процедуры обратного вызова DispatchRoutine().
Все тонкости пакета запроса оставим до лучших времён, а пока посмотрим на общую схему взаимосвязей структур в подсистеме ввода-вывода. Здесь в серые блоки заключены наиболее важные для разработчиков поля, и в некоторые из них мы можем явно или коссвенно производить запись. Структуры, которые не предваряются указателем
3. Стеки драйверов
Ещё одна базовая концепция драйвера – это стек (не путать с программным стеком).
В модели драйверов WDM каждое аппаратное устройство имеет как минимум 2 драйвера устройств. Первый числится в литературе как FDO «Functional Device Object», а второй PDO или «Physical Device». Есть ещё FiDO «Filter Driver», но он прилагается в качестве бонуса, без которого вполне можно обойтись. Выстраиваясь сверху-вниз в порядке FiDO-FDO-PDO, эти три драйвера и образуют свой стек.
Важно понять, что драйвер физ.устройства PDO в стеке единственный, а вот число функциональных драйверов FDO ограничено только фантазией разработчика. Однако следует учитывать, что чем глубже стек, тем дольше обрабатывается запрос. Поэтому Microsoft советует создавать стек не более чем из трёх драйверов, и в большинстве случаев на практике это именно так.
Полезный объём работы выполняет функциональный драйвер FDO – он в курсе всех тонкостей общения со-своим оборудованием. В свою очередь PDO обычно драйвер шины, но не всегда. Он находится на уровне ниже, и отвечает за программную поддержку подключения девайса, например, в слот PCI или порт USB. Таким образом PDO и FDO работают в тесном сотрудничестве, только выполняют разные задачи. Как сверху (upper), так и с низу (lower) драйвера FDO, в цепочку может вклиниться фильтр FiDO по большей части для того, чтобы с благими намерениями расширить его возможности.
Драйверы PDO входят в штатную поставку Win, а мы будем писать исключительно фильтры, или драйверы функций FDO. Для большинства известных устройств, Win имеет драйверы класса, которые (как и следует из их названия) отвечают за поддержку не одного, а целого класса устройств. Например для клавы и мыши имеются kbdclass и mouclass.sys, которым без разницы, проводной девайс или беспроводной. А вообще понятия PDO/FDO на логическом уровне сильно переплетаются – здесь всё зависит от типа устройства. То-есть, что для драйвера файловой системы является PDO, то для драйвера сетевой карты может преобразиться в FDO, и наоборот.
Диспетчер ввода-вывода передаёт пакет запроса IRP самому верхнему драйверу в стеке. Пощупав пакет (и может что-то сделав с ним) верхний драйвер передаёт его на уровень ниже, и т.д. При этом любой из FDO в цепочке может самостоятельно обработать, и таким образом прервать передачу на следующий уровень, просто послав диспетчеру сигнал IoCompleteRequest().
После создания устройства чз IoCreateDevice(), драйвер добавляет себя в стек уже существующих драйверов вызовом IoAttachDeviceToDeviceStackSafe(). Функция подключит нас самым первым на вершину стека устройств, и вернёт указатель на тот девайс, который раньше был первым. Последующие вызовы этой функции делают тоже самое – всегда аттачат девайс только на макушку стека. Возвращённый указатель является обязательным аргументом для функции передачи пакета IRP следующему драйверу IoCallDriver(). На случай, если мы забыли сохранить его при первом подключении в стек, можно потянуть за IoGetDeviceObjectPointer() – она возвращает линк на низлежащий драйвер.
Всё вышесказанное касается только многоуровневых драйверов, которые решают проблему сообща. Но в природе встречаются и монолитные драйверы – весь функционал они реализуют сами, и не нуждаются в посторонней поддержке. В эту категорию подпадают, например, драйверы различных контроллёров типа Arduino, или самопальных устройств эпохи неолита. Всё-что им нужно – это просто подключиться к корневой шине компьютера. Соответственно монолитные драйверы не имеют стека устройств, что представлено на рис.ниже. Здесь вариант, когда FDO устройства сам обработал запрос IRP, не передавая его на более низкие уровни.
Просмотреть стек драйверов в отладчике можно специальной командой
Если хотим копнуть глубже, можно запросить у команды
Раньше
Заключение
Следующие две части полностью посвятим практике – сначала напишем Legacy драйвер, а потом и WDM. Особое внимание уделим отложенным вызовам DPC, объектам синхронизации типа Mutant, блокировкам доступа к структурам SpinLock, работе с памятью, и конечно взаимодействию из кернела с пользовательским софтом. Всем удачи, пока!
i8042prt fdo-driver:
Keyb&Mouse PS/2 class-driver:
Keyb&Mouse USB class-driver:
В этой части:
1. Схема передачи управления драйверам
2. Driver & Device object
3. Стеки драйверов
1. Схема передачи управления драйверам
Вспомним организацию виртуальной памяти системы.
Значит вся физическая ОЗУ делится на фреймы по 4 Кбайт. Такого-же размера кластер на жёстком диске, а так-же одна страница вирт.памяти. Суть виртуализации в том, что разным фреймам можно назначить одинаковый вирт.адрес, поэтому обращаясь по указателю например
0x00400000
, процесс(А) читает именно свою память, а не процесса(В). Когда фреймы физ.памяти заканчиваются, какой-нибудь неиспользуемый можно выгрузить в кластер жёсткого диска, а по требованию вернуть обратно. Диспетчер памяти Win держит под контролем 2 типа фреймов – которые можно сбрасывать на диск в файл подкачки PagedPool, и которые нельзя NonPagedPool.При создании процесса система выделяет ему вирт.память в виде фреймов. Указатели на эту память хранятся в каталоге процесса PageDirectory, а линк на сам каталог в регистре
CR3
. При смене контекста обновляется и CR3
, указывая таким образом на память следущего процесса, и т.д.64-битный адрес, которым мы оперируем в коде считается линейным и состоит из 5-ти частей, что представлено на рисунке ниже (из 64 используются только 48-бит). Обратите внимание, что для адресации записей Entry, во-всех таблицах выделяются по 9-бит, а значит всего записей в каждой из таблиц по 2^9=512. Другими словами, корневая таблица PML4E одна, а вот кол-во таблиц PDPT, PD и PT уже может достигать 512-ти штук. Это позволяет в архитектуре х64 адресовать:
(512*512*512*512)*4096=256
ТераБайт памяти (юзер и кернел делят их пополам по 127 каждому). Зато оффсет 12-битный – он выбирает смещение внутри 4 КБайтного фрейма (отмечен зелёным).Значения записей PTE (PageTableEntry) всегда кратны
0x1000
– они адресуют фреймы физ.памяти, а потому младшие 12-бит отводятся под флаги. В частности бит под кличкой Durty является индикатором того, что фрейм (в терминологии майков) «грязный», т.е. в его ячейки производилась запись новых значений. При нехватке ОЗУ, в своп выгружаются только фреймы с атрибутом D=1
, а нетронутые с D=0
сразу отдаются клиентам на растерзание – чем гонять их на диск и обратно, проще загрузить оригинал.Все 64-битные версии Win распределяют память ядра следующим образом:
Код:
0: kd> !cmkd.kvas
### Start End Length Type
000 ffff0800`00000000 fffff67f`ffffffff ee8000000000 ( 238 TB) SystemSpace
001 fffff680`00000000 fffff6ff`ffffffff 8000000000 ( 512 GB) PageTables
002 fffff700`00000000 fffff77f`ffffffff 8000000000 ( 512 GB) HyperSpace
003 fffff780`00000000 fffff780`00000fff 1000 ( 4 KB) SharedSystemPage
004 fffff780`00001000 fffff7ff`ffffffff 7ffffff000 ( 511 GB) CacheWorkingSet
005 fffff800`00000000 fffff87f`ffffffff 8000000000 ( 512 GB) LoaderMappings
006 fffff880`00000000 fffff89f`ffffffff 2000000000 ( 128 GB) SystemPTEs
007 fffff8a0`00000000 fffff8bf`ffffffff 2000000000 ( 128 GB) PagedPool
008 fffff900`00000000 fffff97f`ffffffff 8000000000 ( 512 GB) SessionSpace
009 fffff980`00000000 fffffa7f`ffffffff 10000000000 ( 1 TB) DynamicKernelVa
010 fffffa80`00000000 fffffa80`017fffff 1800000 ( 24 MB) PfnDatabase
011 fffffa80`01600000 fffffa80`5e5fffff 5d000000 ( 1 GB) NonPagedPool
012 ffffffff`ffc00000 ffffffff`ffffffff 400000 ( 4 MB) HalReserved
В арсенале отладчика WinDbg есть несколько расширений для просмотра информации в каталогах любых процессов, только нужно передавать им в качестве базы значение регистра
CR3
. Вот их перечень:•
!vtop
преобразует вирт.адрес в соответствующий физ.адрес (Virtual-To-Physical).•
!pfn
отображает информацию о конкретном фрейме (Page-Frame-Number).•
!pte
отображает записи всех четырёх каталогов для указанного вирт.адреса.•
!pte2va
отображает вирт.адрес, соответствующий указанной записи PTE.
Код:
0: kd> !process 0 0 Codeby.exe
PROCESS fffffa8001fd9b10
Peb : 7fffffdd000 DirBase: 754e3000 <----// линк на каталог = CR3
Image: Codeby.EXE
0: kd> dt _peb 7fffffdd000 ImageBase* <----------// запросим вирт.базу загрузки образа
ntdll!_PEB
+0x010 ImageBaseAddress : 0x00000000`00400000 void
0: kd> !vtop 754e3000 00400000 <----------------// получить физ.адрес по виртуальному!
Amd64VtoP: PML4E 754e3000
Amd64VtoP: PDPE 69543000
Amd64VtoP: PDE 6a244010
Amd64VtoP: PTE 2ac05000
Virtual address 400000 translates to physical address 70747000.
0: kd> !pte 7fffffdd000
VA 000007fffffdd000
PXE at FFFFF6FB7DBED078 PPE at FFFFF6FB7DA0FFF8 PDE at FFFFF6FB41FFFFF8 PTE at FFFFF683FFFFFEF8 ---+
pfn 2f000 ---DA--UWEV pfn 72d01 ---DA--UWEV pfn 2a902 ---DA--UWEV pfn 6d2c3 ---D---UW-V |
|
0: kd> !pfn 6d2c3 |
PFN 0006D2C3 at address FFFFFA8001478490 |
flink 0000000B share count 00000001 pteaddress FFFFF683FFFFFEF8 <-------------------------+
reference count 0001 used count 0000 Active Modified <----------------// Бит(D)=1
------------------------------------------ PTE flags ---
V – Valid D – Durty A – Accesed
E – Execute G – Global L – Large page (1Gb)
W/R – Writeable/ReadOnly U/K – User/Kernel
Это к тому, что когда для поддержки многозадачности планировщик переключает потоки, то системе приходится сохранять не только значения всех регистров, но и номера фреймов физ.памяти PFN, чтобы позже вернуться в прежнее состояние. Иначе поток не сможет найти свои данные, и план с треском провалится. Этот процесс известен как смена контекста потоков, или Context Switching. На склад отправляется так-же вложенная в EPROCESS основная структура процесса PCB «Process Control Block». На х32 контекст переключался аппаратно через сегмент состояния задачи TSS «Task State Segment», но сейчас реализация исключительно программная, а в TSS остался только указатель на стек ядра текущего потока:
Код:
0: kd> dt _kpcr @$pcr TssBase
hal!_KPCR
+0x008 TssBase : 0xfffff800`03ff7080 _KTSS64
0: kd> dt _ktss64 0xfffff800`03ff7080
hal!_KTSS64
+0x000 Reserved0 : 0
+0x004 Rsp0 : 0xfffff800`03ffdd70
+0x00c Rsp1 : 0
+0x014 Rsp2 : 0
+0x01c Ist : [8] 0
+0x05c Reserved1 : 0
+0x064 Reserved2 : 0
+0x066 IoMapBase : 0x68
0: kd>
Драйверы не имеют своих потоков в системе, а всегда исполняются только в контексте чужих. Например точка-входа DriverEntry() садится на один из многочисленных потоков процесса System – она исполняется только при загрузке драйвера, и по окончании инициализации освобождает System от своего присутствия. Зато остальные процедуры драйвера типа обработки прерываний ISR выполняются уже в контексте того потока, который был активным в момент возникновения самого прерывания. Часто в качестве транспорта драйвера используют потоки пользовательских приложений, что демонстрирует рис.ниже.
Смена контекста в пространстве пользователя может произойти всего по трём причинам:
1. Когда у юзер-потока истёк выделенный ему квант времени
2. Когда его вытесняет более приоритетный (например диспетчер задач)
3. Когда внезапно какое-то из устройств послало процессору прерывание.
2. Объект драйвера, и объект устройства
То, что мы привыкли называть драйвером, по факту является структурой DRIVER_OBJECT. Это основная база, которая (явно или через указатели) описывает буквально весь функционал и окружение драйвера. В нёдрах системы структуру создаёт диспетчер ввода-вывода, а в нашу процедуру DriverEntry() передаёт лишь её адрес. Радует то, что диспетчер берёт на себя все проблемы по оформлению связанных с драйвером структур, при этом большая часть их полей доступна только на чтение. Таким образом, соло играет именно диспетчер, а мы ему лишь в терцию подпеваем.
У любого драйвера WDM должно быть как минимум одно физическое или виртуальное/псевдо устройство, которым он будет управлять. Эти устройства драйвер создаёт сам функцией IoCreateDevice(). Например у pci.sys аж 20 подчинённых устройств, а у acpi вообще 44 девайса, и каждого из них олицетворяет своя структура DEVICE_OBJECT.
Однако вновь испечённое устройство пока скрыто от всех, и чтобы сделать его видимым тому-же пользователю, необходимо через IoCreateSymbolicLink() зарегать девайс в системном пространстве имён, обозвав его хоть именем любимого кота. Только после этого можно будет открыть устройство функцией юзера CreateFile(), и посылать ему команды IOCTL посредством DeviceIoControl().
WinDbg имеет следующие команды и расширения для сбора информации о драйверах:
•
lmsm
– список всех драйверов•
lmsm m a*
– список всех драйверов, имена которых начинаются на «А»•
!drvobj
– базовая информация из структуры объекта драйвера•
!devobj
– базовая информация из структуры объекта устройства•
!devstack
– позиция драйвера в стеке драйверов•
!devnode
– сведения о корне древа устройствРассмотрим их на примере драйвера порта PS/2 i8042prt.sys.
В списке List видно, что дров обслуживает всего 2 устройства – проводную мышь и клаву.
Пока нам нужен только указатель на структуру DRIVER_OBJECT, чтобы сдампить её содержимое:
Код:
0: kd> lmsm m i*
start end module name
fffff880`04934000 fffff880`04952000 i8042prt c:\symbols\i8042prt.pdb
fffff880`01999000 fffff880`019a3000 IaNVMeF (deferred)
fffff880`04015000 fffff880`045f9fe0 igdkmd64 (deferred)
fffff880`00e89000 fffff880`00e91000 intelide (deferred)
fffff880`03bd7000 fffff880`03bed000 intelppm (deferred)
fffff880`00e40000 fffff880`00e4a000 iusb3hcs (deferred)
0: kd> !drvobj i8042prt
Driver Object (fffffa8002c1c8a0) is for: \Driver\i8042prt
Device Object list:
----------------------------------
fffffa8002bf89f0 fffffa8002c939f0 <-----// линки на структуры DEVICE_OBJECT
0: kd> dt _driver_object fffffa8002c1c8a0
nt!_DRIVER_OBJECT
+0x000 Type : 0n4
+0x002 Size : 0n336
+0x008 DeviceObject : 0xfffffa80`02bf89f0 _DEVICE_OBJECT
+0x010 Flags : 0x12
+0x018 DriverStart : 0xfffff880`04934000 void
+0x020 DriverSize : 0x1e000
+0x028 DriverSection : 0xfffffa80`02b181c0 void
+0x030 DriverExtension : 0xfffffa80`02c1c9f0 _DRIVER_EXTENSION
+0x038 DriverName : _UNICODE_STRING "\Driver\i8042prt"
+0x048 HardwareDatabase : 0xfffff800`03195568 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x050 FastIoDispatch : (null)
+0x058 DriverInit : 0xfffff880`0494c070 long i8042prt!GsDriverEntry+0
+0x060 DriverStartIo : 0xfffff880`049366f8 void i8042prt!I8xStartIo+0
+0x068 DriverUnload : 0xfffff880`04948ae0 void i8042prt!I8xUnload+0
+0x070 MajorFunction : [28] 0xfffff880`04943cf8 long i8042prt!I8xCreate+0
Все поля до смещения
0x60
диспетчер заполнил сам, а мы явно должны будем прописать только адрес своей процедуры DispatchUnload(), чтобы драйвер не остался в памяти до следущего ребута. Так-же в массиве указателей MajorFunction нужно выбрать лишь те коды, которые планируем реализовать во-внутренних процедурах обратного вызова. Просмотреть мажорные коды подопытного i8042prt можно той-же командой !drvobj
с ключом(3):
Код:
0: kd> !drvobj i8042prt 3
Driver object (fffffa8002c1c8a0) is for: \Driver\i8042prt
Device Object list:
----------------------------------
fffffa8002bf89f0 fffffa8002c939f0
DriverEntry: fffff8800494c070 i8042prt!GsDriverEntry
DriverStartIo: fffff880049366f8 i8042prt!I8xStartIo
DriverUnload: fffff88004948ae0 i8042prt!I8xUnload
AddDevice: fffff88004947f80 i8042prt!I8xAddDevice
Dispatch routines:
[00] IRP_MJ_CREATE fffff88004943cf8 i8042prt!I8xCreate
[01] IRP_MJ_CREATE_NAMED_PIPE fffff80002ca8c60 nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE fffff88004948390 i8042prt!I8xClose
[03] IRP_MJ_READ fffff80002ca8c60 nt!IopInvalidDeviceRequest
[04] IRP_MJ_WRITE fffff80002ca8c60 nt!IopInvalidDeviceRequest
[05] IRP_MJ_QUERY_INFORMATION fffff80002ca8c60 nt!IopInvalidDeviceRequest
[06] IRP_MJ_SET_INFORMATION fffff80002ca8c60 nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA fffff80002ca8c60 nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA fffff80002ca8c60 nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS fffff8800493a660 i8042prt!I8xFlush
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION fffff80002ca8c60 nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION fffff80002ca8c60 nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL fffff80002ca8c60 nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL fffff80002ca8c60 nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL fffff880049487c0 i8042prt!I8xDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff88004935920 i8042prt!I8xInternalDeviceControl
[10] IRP_MJ_SHUTDOWN fffff80002ca8c60 nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL fffff80002ca8c60 nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP fffff80002ca8c60 nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT fffff80002ca8c60 nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY fffff80002ca8c60 nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY fffff80002ca8c60 nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER fffff88004943f40 i8042prt!I8xPower
[17] IRP_MJ_SYSTEM_CONTROL fffff88004943c7c i8042prt!I8xSystemControl
[18] IRP_MJ_DEVICE_CHANGE fffff80002ca8c60 nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA fffff80002ca8c60 nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA fffff80002ca8c60 nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP fffff88004943680 i8042prt!I8xPnP
0: kd>
Здесь видим точки-входа в процедуры DriverEntry(), Unload(), AddDevice(), после которых представлен массив запросов от клиентов IRP_MJ_xx (в структуре driver_object он начинается со-смещения 0x70, т.е. является её частью). Диспетчер сам заполняет все 28 полей данного массива, прописывая в них линки на свою заглушку IoCompleteRequest(), с кодом возврата невалидного запроса к устройству
0xC0000010
.
Код:
0: kd> uf /i nt!IopInvalidDeviceRequest
8 instructions scanned
nt!IopInvalidDeviceRequest:
fffff800`02ca8c60 4883ec28 sub rsp,28h
fffff800`02ca8c64 488bca mov rcx,rdx
fffff800`02ca8c67 c74230100000c0 mov dword ptr [rdx+30h],0C0000010h
fffff800`02ca8c6e 33d2 xor edx,edx
fffff800`02ca8c70 ff1582672500 call qword ptr [nt!pIofCompleteRequest (fffff800`02eff3f8)]
fffff800`02ca8c76 b8100000c0 mov eax,0C0000010h
fffff800`02ca8c7b 4883c428 add rsp,28h
fffff800`02ca8c7f c3 ret
0: kd>
Если в коде нашего драйвера мы планируем обрабатывать запросы API типа Create-Close-Read-WriteFile() и другие, то должны прописать в этот массив указатели на свои процедуры коллбек, тупо затирая предоставленные диспетчером заглушки. Все драйверы WDM обязаны обрабатывать коды POWER и PNP, а остальные уже на своё усмотрение. Если отсеить из этого списка всё лишнее, то получается, что дров i8042prt.sys может принимать только следующие запросы:
Код:
Dispatch routines:
[00] IRP_MJ_CREATE fffff88004943cf8 i8042prt!I8xCreate
[02] IRP_MJ_CLOSE fffff88004948390 i8042prt!I8xClose
[09] IRP_MJ_FLUSH_BUFFERS fffff8800493a660 i8042prt!I8xFlush
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL fffff88004935920 i8042prt!I8xInternalDeviceControl
[16] IRP_MJ_POWER fffff88004943f40 i8042prt!I8xPower
[17] IRP_MJ_SYSTEM_CONTROL fffff88004943c7c i8042prt!I8xSystemControl
[1b] IRP_MJ_PNP fffff88004943680 i8042prt!I8xPnP
2.1. Объект устройства
Драйвер и его устройство всегда ходят парой. Если в функцию DriverEntry() диспетчер передаёт указатель на DRIVER_OBJECT, то все процедуры обработки запросов IRP_MJ_xx аргументом принимают уже указатель на DEVICE_OBJECT. Каждый девайс имеет свои свойства, которые задаются при его создании функцией IoCreateDevice(). Свойства включают в себя следущие поля в структуре:
• DeviceType – тип оборудования (у нас будет всегда file_device_unknown)
• Flags – текущие настройки (см.инклуд wdm в скрепке)
• Characteristics – флаги с доп.информацией об устройстве (см.инклуд)
• SecurityDesc – дескриптор безопасности, для контроля доступа к устройству (обычно нуль = в дефолте).
Запросим у отладчика объект устройства подопытного i8042prt.sys, чтобы ознакомиться с полным его прототипом:
Код:
0: kd> !drvobj i8042prt
Driver object (fffffa8002ae0550) is for: \Driver\i8042prt
Device object list: fffffa8002ae58c0, fffffa8002ae09f0 <-----// создаёт 2 устройства
0: kd> dt _device_object fffffa8002ae58c0
ntdll!_DEVICE_OBJECT
+0x000 Type : 0n3 <-----------// тип самой структуры IO_TYPE_DEVICE
+0x002 Size : 0x598
+0x004 ReferenceCount : 0n0
+0x008 DriverObject : 0xfffffa80`02ae0550 _DRIVER_OBJECT <---// родитель
+0x010 NextDevice : 0xfffffa80`02ae09f0 _DEVICE_OBJECT <---// линк на структуру сл.устройства (если есть)
+0x018 AttachedDevice : 0xfffffa80`02ae7060 _DEVICE_OBJECT <---// к кому приаттачен в стеке драйверов
+0x020 CurrentIrp : (null) <------------------------------// линк на пакет запроса IRP
+0x028 Timer : (null)
+0x030 Flags : 0x2004 <-----------// DO_POWER_PAGABLE + DO_BUFFERED_IO
+0x034 Characteristics : 0
+0x038 Vpb : (null)
+0x040 DeviceExtension : 0xfffffa80`02ae5a10
+0x048 DeviceType : 0x27 <-----------// FILE_DEVICE_8042_PORT
+0x04c StackSize : 6
+0x050 Queue : <unnamed-tag>
+0x098 AlignRequirement : 0
+0x0a0 DeviceQueue : _KDEVICE_QUEUE
+0x0c8 Dpc : _KDPC
+0x108 ActiveThreads : 0
+0x110 SecurityDesc : (null)
+0x118 DeviceLock : _KEVENT
+0x130 SectorSize : 0
+0x132 Spare1 : 1
+0x138 DeviceObjectExt : 0xfffffa80`02ae5e58 _DEVOBJ_EXTENSION <----// обсудим позже
+0x140 Reserved : (null)
0: kd>
После инициализации драйвер бездействует. Пробудить его из глубокого сна может только приход пакета IRP от диспетчера ввода-вывода (Input/Output Request Packet). Физически это происходит в момент записи указателя в поле DEVICE_OBJECT --> CurrentIrp. Драйвер без объекта устройства не получает никаких IRP !!!
Структура IRP содержит буквально всю информацию о запросе, предписывая драйверу дальнейший фронт работы. Как только драйвер обработает текущий запрос и пошлёт IoCompleteRequest(), диспетчер удалит пакет этого запроса, и войдёт в цикл ожидания следующего. Таким образом IRP можно считать джокером во-всей подсистеме ввода-вывода, поскольку именно он задаёт всем темп.
IRP одна из немногих структур, поля которой доступны нам на запись. Это особенно актуально при двустороннем взаимодействии с кодом пользовательского режима. Например в аргументах DeviceIoControl() юзер передаёт адреса двух своих буферов – один на приём, другой на передачу. Так вот адреса этих буферов драйвер может получить только прочитав поля структуры IRP. Поскольку пакет создаётся самим диспетчером где-то в нёдрах системы (и мы не знаем где точно), он передаёт указатель на IRP во все наши процедуры обратного вызова DispatchRoutine().
Все тонкости пакета запроса оставим до лучших времён, а пока посмотрим на общую схему взаимосвязей структур в подсистеме ввода-вывода. Здесь в серые блоки заключены наиболее важные для разработчиков поля, и в некоторые из них мы можем явно или коссвенно производить запись. Структуры, которые не предваряются указателем
Ptr64
(pointer) являются вложенными, например UNICODE_STRING, LIST_ENTRY и прочие.
Код:
0: kd> !drvobj acpi
Driver object (fffffa8001851e70) is for: \Driver\ACPI <-----// создаёт 44 устройства!
Device object list:
----------------------------------
fffffa800265a520 fffffa8002657e40 fffffa80022de520 fffffa800233eb20
fffffa800233fe40 fffffa80022c4a90 fffffa80022c4c90 fffffa800224ca00
fffffa800224cc20 fffffa800224ce40 fffffa800224c060 fffffa800224b310
fffffa800224b530 fffffa800224b750 fffffa800224b970 fffffa800224b060
fffffa800224bb90 fffffa800224be40 fffffa80022460f0 fffffa80022463a0
fffffa8002246650 fffffa8002246870 fffffa8002246b90 fffffa8001863440
fffffa8001863640 fffffa8001863840 fffffa8001863a40 fffffa8001863c40
fffffa8001863e40 fffffa8001863040 fffffa80018627f0 fffffa80018617f0
fffffa80018607f0 fffffa800185f7f0 fffffa800185e7f0 fffffa800185d7f0
fffffa800185c730 fffffa800186d190 fffffa8002614b80 fffffa8002614da0
fffffa8002291200 fffffa8002291420 fffffa80022916f0 fffffa80018515a0
0: kd> !drvobj pci
Driver object (fffffa8002291960) is for: \Driver\pci <-----// создаёт 20 устройств
Device object list:
----------------------------------
fffffa80022b6a10 fffffa80022449f0 fffffa8002244040 fffffa8001864cb0
fffffa8001862a10 fffffa8001862060 fffffa8001861a10 fffffa8001861060
fffffa8001860a10 fffffa8001860060 fffffa800185fa10 fffffa800185f060
fffffa800185ea10 fffffa800185e060 fffffa800185da10 fffffa800185d060
fffffa800185ca10 fffffa800185c060 fffffa800185b900 fffffa8001857750
0: kd> !cmkd.kvas fffffa80022b6a10 <----------// Kernel Virtual Address Space
### Start End Length Type
011 fffffa80`01600000 fffffa80`5e5fffff 5d000000 (1GB) NonPagedPool <----// нельзя выгружать в своп
0: kd>
3. Стеки драйверов
Ещё одна базовая концепция драйвера – это стек (не путать с программным стеком).
В модели драйверов WDM каждое аппаратное устройство имеет как минимум 2 драйвера устройств. Первый числится в литературе как FDO «Functional Device Object», а второй PDO или «Physical Device». Есть ещё FiDO «Filter Driver», но он прилагается в качестве бонуса, без которого вполне можно обойтись. Выстраиваясь сверху-вниз в порядке FiDO-FDO-PDO, эти три драйвера и образуют свой стек.
Важно понять, что драйвер физ.устройства PDO в стеке единственный, а вот число функциональных драйверов FDO ограничено только фантазией разработчика. Однако следует учитывать, что чем глубже стек, тем дольше обрабатывается запрос. Поэтому Microsoft советует создавать стек не более чем из трёх драйверов, и в большинстве случаев на практике это именно так.
Полезный объём работы выполняет функциональный драйвер FDO – он в курсе всех тонкостей общения со-своим оборудованием. В свою очередь PDO обычно драйвер шины, но не всегда. Он находится на уровне ниже, и отвечает за программную поддержку подключения девайса, например, в слот PCI или порт USB. Таким образом PDO и FDO работают в тесном сотрудничестве, только выполняют разные задачи. Как сверху (upper), так и с низу (lower) драйвера FDO, в цепочку может вклиниться фильтр FiDO по большей части для того, чтобы с благими намерениями расширить его возможности.
Драйверы PDO входят в штатную поставку Win, а мы будем писать исключительно фильтры, или драйверы функций FDO. Для большинства известных устройств, Win имеет драйверы класса, которые (как и следует из их названия) отвечают за поддержку не одного, а целого класса устройств. Например для клавы и мыши имеются kbdclass и mouclass.sys, которым без разницы, проводной девайс или беспроводной. А вообще понятия PDO/FDO на логическом уровне сильно переплетаются – здесь всё зависит от типа устройства. То-есть, что для драйвера файловой системы является PDO, то для драйвера сетевой карты может преобразиться в FDO, и наоборот.
Диспетчер ввода-вывода передаёт пакет запроса IRP самому верхнему драйверу в стеке. Пощупав пакет (и может что-то сделав с ним) верхний драйвер передаёт его на уровень ниже, и т.д. При этом любой из FDO в цепочке может самостоятельно обработать, и таким образом прервать передачу на следующий уровень, просто послав диспетчеру сигнал IoCompleteRequest().
После создания устройства чз IoCreateDevice(), драйвер добавляет себя в стек уже существующих драйверов вызовом IoAttachDeviceToDeviceStackSafe(). Функция подключит нас самым первым на вершину стека устройств, и вернёт указатель на тот девайс, который раньше был первым. Последующие вызовы этой функции делают тоже самое – всегда аттачат девайс только на макушку стека. Возвращённый указатель является обязательным аргументом для функции передачи пакета IRP следующему драйверу IoCallDriver(). На случай, если мы забыли сохранить его при первом подключении в стек, можно потянуть за IoGetDeviceObjectPointer() – она возвращает линк на низлежащий драйвер.
Всё вышесказанное касается только многоуровневых драйверов, которые решают проблему сообща. Но в природе встречаются и монолитные драйверы – весь функционал они реализуют сами, и не нуждаются в посторонней поддержке. В эту категорию подпадают, например, драйверы различных контроллёров типа Arduino, или самопальных устройств эпохи неолита. Всё-что им нужно – это просто подключиться к корневой шине компьютера. Соответственно монолитные драйверы не имеют стека устройств, что представлено на рис.ниже. Здесь вариант, когда FDO устройства сам обработал запрос IRP, не передавая его на более низкие уровни.
Просмотреть стек драйверов в отладчике можно специальной командой
!devstack
, передав ей предварительно взятый у !drvobj
указатель на объект устройства. Помнится наш подопытный кролик i8042prt.sys создавал 2 девайса, и теперь можем заглянуть в их стеки драйверов так. В данном случае acpi.sys является нижним PDO в стеке, i8042 это FDO, а класс-драйвера должны быть тоже FDO, но почему-то софт «PCI-Scope» считает, что это фильтры верхнего уровня FiDO. По аналогии можно зайти и в стек самого класс-драйвера, и обнаружить в качестве нижнего, диспетчера PnP:
Код:
0: kd> !drvobj i8042prt
Driver object (fffffa8002ac8e70) is for: \Driver\i8042prt
Device object list: fffffa8002aa1800, fffffa8002ac1040
0: kd> !devstack fffffa8002aa1800
!DevObj !DrvObj !DevExt ObjectName
fffffa8002aa2060 \Driver\mouclass fffffa8002aa21b0 PointerClass0
> fffffa8002aa1800 \Driver\i8042prt fffffa8002aa1950
fffffa800224b750 \Driver\ACPI fffffa80018686b0 00000077
0: kd> !devstack fffffa8002ac1040
!DevObj !DrvObj !DevExt ObjectName
fffffa8002abe060 \Driver\kbdclass fffffa8002abe1b0 KeyboardClass0
> fffffa8002ac1040 \Driver\i8042prt fffffa8002ac1190
fffffa800224b970 \Driver\ACPI fffffa8001868a00 00000076
***********************************************************************
0: kd> !drvobj kbdclass
Driver object (fffffa8002a9f7c0) is for: \Driver\kbdclass
Device object list: fffffa8002b03060, fffffa8002abe060
0: kd> !devstack fffffa8002b03060
!DevObj !DrvObj !DevExt ObjectName
> fffffa8002b03060 \Driver\kbdclass fffffa8002b031b0 KeyboardClass1
fffffa8002b04040 \Driver\TermDD fffffa8002b04190
fffffa80018f14d0 \Driver\PnpManager fffffa80018f1620 0000005c
Если хотим копнуть глубже, можно запросить у команды
!pcitree
устройства на всех шинах PCI. У меня всего одна корневая Bus#0, и три подключаются к ней через мосты Bridge (в зависимости от топологии у вас может быть больше/меньше). В скобках первого столбца получим Device/Function на шине, во-втором столбце Vid/Did устройства, и в предпоследнем Class/Subclass. Вся эта информация хранится в конфигурационном пространстве «PCI-Config-Space». Далее прямо из этого лога можно просматривать стеки драйверов любых из перечисленных устройств – очень удобно:
Код:
0: kd> !pcitree
Bus 0x0 (FDO Ext fffffa80018578a0)
(d=0, f=0) 808629c0 devext 0xfffffa800185ba50 devstack 0xfffffa800185b900 0600 Bridge/HOST to PCI
(d=2, f=0) 808629c2 devext 0xfffffa800185c1b0 devstack 0xfffffa800185c060 0300 Display Controller/VGA
(d=1b, f=0) 808627d8 devext 0xfffffa800185cb60 devstack 0xfffffa800185ca10 0403 Multimedia Device/Unknown Sub Class
(d=1c, f=0) 808627d0 devext 0xfffffa800185d1b0 devstack 0xfffffa800185d060 0604 Bridge/PCI to PCI
Bus 0x1 (FDO Ext fffffa8001864e00)
(d=1c, f=1) 808627d2 devext 0xfffffa800185db60 devstack 0xfffffa800185da10 0604 Bridge/PCI to PCI
Bus 0x2 (FDO Ext fffffa8002244190)
(d=0, f=0) 10ec8136 devext 0xfffffa80022b6b60 devstack 0xfffffa80022b6a10 0200 Network Controller/Ethernet
(d=1d, f=0) 808627c8 devext 0xfffffa800185e1b0 devstack 0xfffffa800185e060 0c03 Serial Bus Controller/USB
(d=1d, f=1) 808627c9 devext 0xfffffa800185eb60 devstack 0xfffffa800185ea10 0c03 Serial Bus Controller/USB
(d=1d, f=2) 808627ca devext 0xfffffa800185f1b0 devstack 0xfffffa800185f060 0c03 Serial Bus Controller/USB
(d=1d, f=3) 808627cb devext 0xfffffa800185fb60 devstack 0xfffffa800185fa10 0c03 Serial Bus Controller/USB
(d=1d, f=7) 808627cc devext 0xfffffa80018601b0 devstack 0xfffffa8001860060 0c03 Serial Bus Controller/USB
(d=1e, f=0) 8086244e devext 0xfffffa8001860b60 devstack 0xfffffa8001860a10 0604 Bridge/PCI to PCI
Bus 0x3 (FDO Ext fffffa8002244b40)
(d=1f, f=0) 808627b8 devext 0xfffffa80018611b0 devstack 0xfffffa8001861060 0601 Bridge/PCI to ISA
(d=1f, f=1) 808627df devext 0xfffffa8001861b60 devstack 0xfffffa8001861a10 0101 Mass Storage Controller/IDE
(d=1f, f=2) 808627c0 devext 0xfffffa80018621b0 devstack 0xfffffa8001862060 0101 Mass Storage Controller/IDE
(d=1f, f=3) 808627da devext 0xfffffa8001862b60 devstack 0xfffffa8001862a10 0c05 Serial Bus Controller/COM
Total PCI Root busses processed = 1
Total PCI Segments processed = 1
0: kd> !devstack 0xfffffa8001862060
!DevObj !DrvObj !DevExt ObjectName
fffffa80022c4060 \Driver\intelide fffffa80022c41b0 PciIde0
fffffa80018617f0 \Driver\ACPI fffffa800186b360
> fffffa8001862060 \Driver\pci fffffa80018621b0 NTPNP_PCI0013
0: kd> !devext 0xfffffa80018621b0
PDO Extension, Bus 0x0, Device 1f, Function 2.
DevObj 0xfffffa8001862060, Parent FDO DevExt 0xfffffa80018578a0
Device State = PciStarted
VID 8086, DID 27C0
Class Base/Sub 01/01 (Mass Storage Controller/IDE)
ProgInterface: 8f, Revision: 01, IntPin: 02, RawLine 13
Logical Device Power State: D0
Device Wake Level: D3
WaitWakeIrp: <none>
Requirements: Alignment Length Minimum Maximum
BAR0 Io: 00000008 00000008 0000000000000000 000000000000ffff
BAR1 Io: 00000004 00000004 0000000000000000 000000000000ffff
BAR2 Io: 00000008 00000008 0000000000000000 000000000000ffff
BAR3 Io: 00000004 00000004 0000000000000000 000000000000ffff
BAR4 Io: 00000010 00000010 0000000000000000 000000000000ffff
Resources: Start Length
BAR0 Io: 000000000000d080 00000008
BAR1 Io: 000000000000d000 00000004
BAR2 Io: 000000000000cc00 00000008
BAR3 Io: 000000000000c880 00000004
BAR4 Io: 000000000000c800 00000010
Interrupt Resource: Type - LineBased, Irq = 0x13
0: kd>
Раньше
Ссылка скрыта от гостей
лежала крутая утилита для сбора информации о драйверах "DeviceTree", но сейчас почему-то линк указывает в никуда. Не смотря на то, что интерфейс её пахнет нафталином, функционал конечно мощный, ..а главное есть поддержка х64 (по крайней мере на моей Win7 воркает). На всякий положу её в скрепку – можете потестить. Парсит содержимое структур DRIVER & DEVICE_OBJECT, и даже видит нижнее от себя Attached устройство в стеке: Заключение
Следующие две части полностью посвятим практике – сначала напишем Legacy драйвер, а потом и WDM. Особое внимание уделим отложенным вызовам DPC, объектам синхронизации типа Mutant, блокировкам доступа к структурам SpinLock, работе с памятью, и конечно взаимодействию из кернела с пользовательским софтом. Всем удачи, пока!
i8042prt fdo-driver:
Ссылка скрыта от гостей
Keyb&Mouse PS/2 class-driver:
Ссылка скрыта от гостей
Keyb&Mouse USB class-driver:
Ссылка скрыта от гостей