Написание драйвера в подробностях №2. Драйвера функции


Иллюстрированный самоучитель по теории операционных систем › Драйверы внешних устройств › Функции драйверов [страница - 215] | Самоучители по программированию

Функции драйверов

Прежде всего, драйвер должен иметь функции, вызываемые ядром при загрузке и выгрузке модуля и при подключении модуля к конкретным устройствам. Например, в Sun Solans это перечисленные функции.

  • int _init(void) – инициализация драйвера. Эта функция вызывается при загрузке модуля. Драйвер должен зарезервировать все необходимые ему системные ресурсы и проинициализировать собственные глобальные переменные. Инициализация устройства на этом этапе не происходит.
  • int probe (dev_info_t *dip) – проверить наличие устройства в системе. Во многих системах эта функция реализуется не самим драйвером, а специальным модулем – сниффером (sniffer – дословно, "нюхач"), используемым программой автоконфигурации.
  • int attach (dev_info_t * dip, ddi_attach_cmd_t crtid) – инициализация копии драйвера, управляющей конкретным устройством. Эту функцию можно рассматривать как аналог конструктора объекта в объектно-ориентированном программировании. Если в системе присутствует несколько устройств, управляемых одним драйвером, некоторые ОС загружают несколько копий кода драйвера, но в системах семейства Unix функция attach просто вызывается многократно.

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

Например, каждый жесткий диск в Unix SVR4 должен иметь 16 записей – по две (далее мы поймем, для чего они нужны) для каждого из восьми допустимых слайсов (логических разделов, см. разд. "Загрузка самой ОС") диска. Другой пример: в большинстве систем семейства Unix лентопротяжные устройства имеют две минорные записи. Одно из этих устройств при открытии перематывает ленту к началу, другое не перематывает. В действительности оба устройства управляются одним и тем же драйвером, который определяет текущий режим работы в зависимости от указанной минорной записи.

Современные Unix системы, в частности Solaris, используют отложенную инициализацию, когда для многих устройств attach вызывается только при первой попытке доступа пользовательской программы к устройству.

  • int detach(dev_info_t *dip, ddi_detach_cmd_t cmd) – аналог деструктора объекта в ООП. Впрочем, в отличие от деструктора, эта операция не безусловна – если не удается нормально завершить обрабатываемые в данный момент операции над устройством, драйвер может и даже обязан отказаться деинициализироваться. При деинициализации драйвер должен освободить все системные ресурсы, которые он занял при инициализации и в процессе работы (в том числе и уничтожить минорную запись) и может, если это необходимо, произвести какие-то операции над устройством, например, выключить приемопередатчик, запарковать головки чтения-записи и т. д. После того, как все устройства, управляемые драйвером, успешно деинициализированы, система может его выгрузить.
  • int _fini (void) – функция, вызываемая системой перед выгрузкой дуля. Драйвер обязан освободить все ресурсы, которые он занял на этапе инициализации модуля, а также все ресурсы, занятые им во время работы на уровне модуля (не привязанные к конкретному управляемому устройству).

После того, как драйвер проинициализировался и зарегистрировать минорную запись, пользовательские программы могут начинать обращаться к не му и к управляемым им устройствам. Понятно, что обеспечить единый интерфейс к разнообразным категориям устройств, перечисленным в Главе 9 по меньшей мере сложно. Наиболее радикально подошли к этой проблеме разработчики системы UNIX, разделившие все устройства на два класса-блочные (высокоскоростные устройства памяти с произвольным доступом в первую очередь, дисковые устройства) и последовательные или символьные устройства (все остальное) (в действительности, у современных систем семейства Unix типов драйверов несколько больше, но об этом далее).

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

  • int open (char * fnarne, int flags, mode_t mode) – Процедура открытия устройства. В некоторых случаях она может содержать и дополнительные шаги инициализации устройства – например, для лентопротяжек эта процедура может включать в себя перемотку ленты к началу. Функция возвращает целочисленный идентификатор-"ручку" (handle), часто называемый также дескриптором файла, который используется программой при всех последующих обращениях к устройству.
  • int readfint handle, char * where, size_t how_much) – чтение данных с устройства. Если устройство приспособлено только для вывода (например, принтер), эта функция может быть не определена.
  • int write (int handle, char * what, size_t how_much) – запись данных на устройство. Если устройство приспособлено только для ввода, (например, перфоленточный ввод или мышь), эта функция также может быть не определена.
  • void dose (int handle) – процедура закрытия (освобождения) устройства.
  • int ioctitint handle, int cmd,…) – процедура задания специальной команды, которая не может быть сведена к операциям чтения и записи. Набор таких команд зависит от устройства. Например, для растровых графических устройств могут быть определены операции установки видеорежима; для последовательных портов RS232 это могут быть команда установки скорости, количества битов, обработки бита четности и т. д., для дисководов – команды форматирования носителя.
  • off r lseek<int handle, off_t offset, int whence), long seek – команда перемещения головки чтения/записи к заданной позиции. Драйверы устройств, не являющихся устройствами памяти, например модема или Принтера, как правило, не поддерживают эту функцию.Слово long в названии функции появилось по историческим причинам: версиях Unix для 16-разрядных машин индекс позиции не мог обозначать-я словом, потому что это ограничивало бы логическую длину устройства недопустимо малым значением 65334 байт. Поэтому необходимо было использовать двойное слово, что соответствовало типу long языка С. Современные системы используют 64-разрядный off_t.
  • caddr_t rranap (caddr_t addr, size_t len, int prot, int flags, int handle, off_t offset) memory map – отображение устройства в адресное пространство процесса. Параметр prot задает права доступа к отображенному участку: на чтение, на запись и на исполнение. Отображение может происходить на заданный виртуальный адрес, или же система может выбирать адрес для отображения сама.

Эта функция отсутствовала в старых версиях системы, но большинство современных систем семейства (BSD 4.4, ряд наследников BSD 4.3, SVR4 и Linux) поддерживают ее.

samoychiteli.ru

Драйвер устройства и с чем его едят / Блог компании DriverPack Solution / Хабр

Что такое «драйвер»

Как уважаемый хабрапользователь наверняка знает, «драйвер устройства» — это компьютерная программа управляющая строго определенным типом устройства, подключенным к или входящим в состав любого настольного или переносного компьютера.

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

Обычно драйвер общается с устройством через шину или коммуникационную подсистему, к которой подключено непосредственное устройство. Когда программа вызывает процедуру (очередность операций) драйвера – он направляет команды на само устройство. Как только устройство выполнило процедуру («рутину»), данные посылаются обратно в драйвер и уже оттуда в ОС. Любой драйвер является зависимым от самого устройства и специфичен для каждой операционной системы. Обычно драйверы предоставляют схему прерывания для обработки асинхронных процедур в интерфейсе, зависимом от времени ее исполнения.

Любая операционная система обладает «картой устройств» (которую мы видим в диспетчере устройств), для каждого из которых необходим специфический драйвер. Исключения составляют лишь центральный процессор и оперативная память, которой управляет непосредственно ОС. Для всего остального нужен драйвер, который переводит команды операционной системы в последовательность прерываний – пресловутый «двоичный код».

Как работает драйвер и для чего он нужен?

Основное назначение драйвера – это упрощение процесса программирования работы с устройством.

Он служит «переводчиком» между хардовым (железным) интерфейсом и приложениями или операционными системами, которые их используют. Разработчики могут писать, с помощью драйверов, высокоуровневые приложения и программы не вдаваясь в подробности низкоуровневого функционала каждого из необходимых устройств в отдельности.

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

Если вы хотите отформатировать жесткий диск, то, упрощенно, этот процесс выглядит следующим образом и имеет определенную последовательность: (1) сначала ОС отправляет команду в драйвер устройства используя команду, которую понимает и драйвер, и операционная система. (2) После этого драйвер конкретного устройства переводит команду в формат, который понимает уже только устройство. (3) Жесткий диск форматирует себя, возвращает результат драйверу, который уже впоследствии переводит эту команду на «язык» операционной системы и выдает результат её пользователю (4).

Как создается драйвер устройства

Для каждого устройства существует свой строгий порядок выполнения команд, называемой «инструкцией». Не зная инструкцию к устройству, невозможно написать для него драйвер, так как низкоуровневые машинные команды являются двоичным кодом (прерываниями) которые на выходе отправляют в драйвер результат, полученный в ходе выполнения этой самой инструкции.

При создании драйвера для Линукса, вам необходимо знать не только тип шины и ее адрес, но и схематику самого устройства, а также весь набор электрических прерываний, в ходе исполнения которых устройство отдает результат драйверу.

Написание любого драйвера начинается с его «скелета» — то есть самых основных команд вроде «включения/выключения» и заканчивая специфическими для данного устройства параметрами.

И чем драйвер не является

Часто драйвер устройства сравнивается с другими программами, выполняющими роль «посредника» между софтом и/или железом. Для того, чтобы расставить точки над «i», уточняем:
  • Драйвер не является интерпретатором, так как не исполняется напрямую в софтовом слое приложения или операционной системы.
  • Драйвер не является компилятором, так как не переводит команды из одного софтового слоя в другой, такой же.

Ну и на правах рекламы – вы всегда знаете, где скачать новейшие драйвера для любых устройств под ОС Windows.

habr.com

Драйверы Устройств | Информатика

У контроллера каждого устройства есть набор регистров, используемых для того, чтобы давать управляемому устройству команды и считывать состояние устройства. Число таких регистров и выдаваемые команды зависят от конкретного устройства. Например, программа управления мышью должна получать от мыши информацию о том, насколько далеко она продвинулась по горизонтали и вертикали, а также о нажатых кнопках мыши. Программа управления диском должна знать о секторах, дорожках, цилиндрах, головках, их перемещении и времени установки, двигателях и тому подобных вещах, необходимых для правильной работы диска. Очевидно, что эти программы управления будут сильно различаться. Такая программа управления каждым устройством ввода-вывода, подключенным к компьютеру, называется драйвером устройства. Она обычно пишетсяпроизводителем и распространяется вместе с устройством. Поскольку для каждой ОС требуются специальные драйверы, производители устройств обычно поставляют драйверы для нескольких наиболее популярных операционных систем.

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

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

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

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

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

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

С появлением персональных компьютеров с их огромным разнообразием устройств ввода-вывода такая модель перестала работать. Далеко не все пользователи могли самостоятельно перекомпилировать и собрать ядро даже при наличии исходных текстов или объектных модулей. Поэтому операционные системы, начиная с MS - DOS , перешли к модели динамической подгрузки драйверов. Различные системы выполняют эту процедуру по-разному.

Драйвер устройства выполняет несколько функций:

1) обработку абстрактных запросов чтения и записи независимого от устройств и расположенного над ними программного обеспечения;

2) инициализацию устройства;

3) управление энергопотреблением устройства и регистрацией событий;

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

5) проверку использования устройства в данный момент. Если устройство занято, запрос может быть поставлен в очередь. Если устройство свободно, проверяется его состояние. Возможно, требуетсявключить устройство или запустить двигатель, прежде чем начнется перенос данных. Как только устройство готово, может начинаться собственно управление устройством.

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

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

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

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

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

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

ibrain.kz

Написание драйвера в подробностях №2

Вот и пришло время второй статьи из цикла о написании драйверов под Windows. Сейчас я тебя немного расстрою: в предыдущей статье я обещала, что в этой мы приступим собственно к практике. Но данная статья это, скорее, «полупрактика». Глупо «с места в карьер» бросаться разрабатывать драйвера режима ядра(так как, если ты не забыл, в данном цикле мы разбираем именно этот тип драйверов), хотя бы поверхностно не изучив особенности и приёмы программирования в режиме ядра, что мы и сделаем в первой части этой статьи. Ну а во второй мы наконец — то разберём структуру настоящего драйвера под Windows(Legacy и немного WDM) : его основные функции, их взаимодействие, а также параметры, принимаемые ими. Так что к третьей статье этого цикла ты уже, надо думать, будешь основательно подготовлен к написанию своего первого драйвера. Начинаем.

Особенности и приёмы программирования в режиме ядра

Программирование в режиме ядра имеет массу особенностей, для прикладников очень непривычных, а для новичков довольно сложных. Во-первых, у режима ядра своё, отличное от такового в пользовательском режиме, API. Кроме того, для кода, выполняющегося в режиме ядра, имеет очень большое значение его уровень IRQL, так как приложениям, выполняющимся на высоких уровнях IRQL, недоступны многие функции, к которым имеют доступ приложения низких IRQL уровней, и наоборот. Всё это необходимо учитывать.Во-вторых, в режиме ядра есть свои дополнительные описатели типов. Полный их список можно найти в заголовочном файле ntdef.h. Его содержание примерно таково:

typedef unsigned char USHARtypedef unsigned short USHORTtypedef unsigned long ULONG…………

Зачем это нужно? Ну, во-первых, для красоты… тьфу, для унификации стиля классических C — типов данных и нововведённых — таких, как WCHAR(двухбайтный Unicode символ), LARGE_INTEGER (который, на самом деле, является объединением) и т.д. А также для унификации исходников для 32 — разрядных платформ и надвигающихся 64 — разрядных.

В исходниках драйверов часто встречаются макроопределенияIN, OUT, OPTIONAL. Что они означают? А ровным счётом ничего, и введены они только для повышения удобочитаемости исходника. OPTIONAL обозначает необязательные параметры, IN — параметры, передаваемые внутрь функции, например, OUT — соответственно, наоборот. А вот IN OUT означает, что параметр передаётся внутрь функции, а затем возвращается из неё обратно.

Есть изменения и в типах возвращаемых значений функций. Ты наверняка знаешь, что C-ишные функции либо не возвращают значения(void), либо возвращают значение определённого типа(char, int etc). При программировании драйверов ты столкнёшься с ещё одним типом — NT_STATUS. Этот тип включает в себя информацию о коде завершения функции(определение этого типа можно посмотреть в файле ntdef.h). NT_STATUS является переопределённым типом long integer. Неотрицательные значения переменных этого типасоответствуют успешному завершению функции, отрицательные — наоборот(файл NTSTATUS содержит символьные обозначения всех кодов возврата). Сообщение об удачном завершении имеет код 0 и символьное обозначение STATUS_SUCCESS. Остальные коды возврата, соответствующие разнообразным вариантам ошибок, транслируются в системные коды ошибок и передаются вызывающей программе. Для работы с типом NT_STATUS существует несколько макроопредений(описанные в файле ntdef.h), например NT_SUCCESS(), проверяющий код возврата на успешность завершения.

Функции драйвера, за исключением DriverEntry (главная процедура драйвера, подробнее см. во второй части статьи), могут называться как угодно, тем не менее, существуют определённые «правила хорошего тона» при разработке драйверов, в том числе и для именования процедур: например, все функции, относящиеся к HAL, желательно предварять префиксом HAL и т.д. Сама Microsoft практически постоянно следует этому правилу. А имена типов данных и макроопределения в листингах DDK написаны сплошь заглавными буквами. Советую тебе поступать также при разработке своих драйверов. Это и в самом деле во много раз повышает удобство работы с листингом.

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

Начнём с функций для работы с памятью, а для начала поговорим собственно об устройстве и работе с памятью в Windows. Единое 4-х гигабайтное адресное пространство памяти Windows(я имею в виду 32-х разрядные версии Windows) делится на две части: 2 гигабайта для пользовательского пространства и 2 гигабайта для системного. 2 гигабайта системного пространства доступны для всех потоков режима ядра. Системное адресное пространство делится на следующие части:

Видов адресов в режиме ядра три: физические(реально указывающие на область физической памяти), виртуальные(которые перед использованием транслируются в физические), и логические(используемые HAL уровнем при общении с устройствами; он же и отвечает за работу с такими адресами). Функции режима ядра, отвечающие за выделение и освобождение виртуальной памяти, отличаются от таковых в пользовательском режиме. Также, находясь на уровне режима ядра, становится возможным использовать функции выделения и освобождения физически непрерывной памяти. Разберём все эти функции поподробнее.

1) PVOID ExAllocatePool (уровень IRQL, на котором может выполняться эта функция — < DISPATCH_LEVEL) — выделяет область памяти. Принимает два параметра: параметр(POOL_TYPE) , в котором содержится значение, означающее, какого типа область памяти нужно выделить: PagedPool — страничная, NonPagedPool — нестраничная(в этом случае функцию можно вызвать с любого IRQL уровня). Второй параметр(ULONG) — размер запрашиваемой области памяти. Функция возвращает указатель на выделенную область памяти, и NULL, если выделить память не удалось.

2) VOID ExFreePool (IRQL<DISPATCH_LEVEL) — освобождает область памяти. Принимает параметр(PVOID) — указатель на освобождаемую область памяти. Если высвобождается нестраничная память, то данная функция может быть вызвана сDISPATCH_LEVEL. Возвращаемое значение — void.

3) PVOID MmAllocateContiguousMemory (IRQL==PASSIVE_LEVEL) — выделяет физически непрерывную область памяти. Принимает два параметра. Первый параметр(ULONG) — размер запрашиваемой области памяти, второй — параметр(PHYSICAL_ADDRESS), означающий верхний предел адресов для запрашиваемой области. Возвращаемое значение: виртуальный адрес выделенной области памяти или NULL(при неудаче).

4) VOID MmFreeContiguousMemory (IRQL==PASSIVE_LEVEL) — освобождает физически непрерывную область памяти. Принимает единственный параметр(PVOID) — указатель на область памяти, выделенную ранее с использованием функции MmAllocateContiguousMemory. Возвращаемое значение —void.

5) BOOLEAN MmIsAddressValid (IRQL<=DISPATCH_LEVEL) — делает проверку виртуального адреса. Принимает параметр(PVOID) — виртуальный адрес, нуждающийся в проверке. Функция возвращает TRUE, если адрес «валидный»(т.е. присутствует в виртуальной памяти), и FALSE — в противном случае.

6) PHYSICAL_ADDRESS MmGetPhysicalAddress (IRQL — любой) — определяет физический адрес по виртуальному. Принимает параметр(PVOID), содержащий анализируемый виртуальный адрес. Возвращаемое значение — полученный физический адрес.

Основные функции для работы с памятью рассмотрели, перейдём к таковым для работы с реестром. Сначала поговорим о функциях доступа к реестру, предоставляемых диспетчером ввода — вывода, потом о драйверных функциях прямого доступа к реестру, а затем о самом богатом по возможностям и удобству семействе функций для работы с реестром —Zw~.

Драйверные функции, предоставляемые диспетчером ввода — вывода. 

1) IoRegisterDeviceInterface — данная функция регистрирует интерфейс устройства. Диспетчер ввода — вывода создаёт подразделы реестра для всех зарегистрированных интерфейсов. После этого можно создавать и хранить в этом подразделе нужные драйверу параметры с помощью вызова функции IoOpenDeviceInterfaceRegistryKey, которая возвращает дескриптор доступа к подразделу реестра для зарегистрированного интерфейса устройства.

2) IoGetDeviceProperty — данная функция запрашивает из реестра установочную информацию об устройстве.

3) IoOpenDeviceRegistryKey — возвращает дескриптор доступа к подразделу реестра для драйвера или устройства по указателю на его объект.

4) IoSetDeviceInterfaceState — с помощью данной функции можно разрешить или запретить доступ к зарегистрированному интерфейсу устройства. Компоненты системы могут получать доступ только к разрешённым интерфейсам.

Драйверные функции для прямого доступа к реестру.

1) RtlCheckRegistryKey — проверяет, существует ли указанныйподраздел внутри подраздела, переданного первым параметром. Что и какимобразом передавать в первом параметре — в рамках статьи всё не перечислить, отсылаю к ntddk.h и wdm.h. Если существует — возвращаетсяSTATUS_SUCCESS.

2) RtlCreateRegistryKey — создаёт подраздел внутри раздела реестра, указанного вторым параметром. Далее — всё то же самое, что и уRtlCheckRegistryKey.

3) RtlWriteRegistryValue — записывает значение параметра реестра. Первый параметр — куда пишем, второй — в какой подраздел(если его нет, то он будет создан), а третий — какой параметр создаём.

4) RtlDeleteRegistryValue — удаляет параметр из подраздела. Параметры те же самые, что и у RtlWriteRegistryValue(только с необходимыми поправками, конечно).

5) RtlQueryRegistryValues — данная функция позволяет за один вызов получить значения сразу нескольких параметров указанного подраздела.

И напоследок функции для работы с реестром семействаZw~.

1) ZwCreateKey — открывает доступ к подразделу реестра. Если такового нет — создаёт новый. Возвращает дескриптор открытого объекта.

2) ZwOpenKey — открывает доступ к существующему подразделу реестра.

3) ZwQueryKey — возвращает информацию о подразделе.

4) ZwEnumerateKey — возвращает информацию о вложенных подразделах уже открытого ранее подраздела.

5) ZwEnumerateValueKey — возвращает информацию о параметрах и их значениях открытого ранее подраздела.

6) ZwQueryValueKey — возвращает информацию о значении параметра в открытом ранее разделе реестра. Полнота возвращаемой информации определяется третьим параметром, передаваемым функции, который может принимать следующие значения(дополнительные разъяснения не требуются, так как они имеют «говорящие» имена):KeyValueBasicInformation, KeyValuePartialInformation и KeyValueFullInformation.

7) ZwSetValueKey — создаёт или изменяет значение параметра в открытом ранее подразделе реестра.

8) ZwFlushKey — принудительно сохраняет на диск изменения, сделанные в открытых функциями ZwCreateKey и ZwSetValueKey подразделах.

9) ZwDeleteKey — удаляет открытый подраздел из реестра.

10) ZwClose — закрывает дескриптор открытого ранее подраздела реестра, предварительно сохранив сделанные изменения на диске.

Практически все вышеперечисленные функции для работы с реестром должны вызываться с уровня IRQLPASSIVE_LEVEL.

Думаю, пока достаточно. Конечно, у всех вышеперечисленных функций есть масса нюансов в применении. Да и вообще функций режима ядра — великое множество, их ничуть не меньше, чем в пользовательском режиме. Но моя задача была не рассказать обо всех API — функциях режима ядра(что даже в рамках цикла невозможно сделать), а продемонстрировать отличия функций режима ядра, от таковых в пользовательском режиме, и хоть немного рассказать о нюансах их применения(взять, к примеру, то, что в пользовательском режиме не имеет значения, в потоке какого приоритета будет выполняться приложение: оно будет иметь такой же полный доступ ко всем API функциям пользовательского режима, как и любые другие приложения; на уровне ядра, как ты только что, убедился, это не так). Ну а за более или менее полным списком и описанием всех этих API — функций советую обратиться к библии Гарри Нэббета.Ну вот и всё, теперь ты готов к разговору о структуре драйвера, который мы сейчас и начнём. 

Структура драйвера

Я уже говорила, что драйвер фактически можно представить как довольно-таки обычную dll-ку уровня ядра. Таким образом, далее можно представить драйвер просто как набор процедур, периодически вызываемых внешними программами. Несмотря на то, что процедуры драйверов для разных устройств сильно отличаются, есть общая структура и общие функции для всех драйверов. Главные из них мы сейчас и рассмотрим. 

Входная точка любого драйвера — функция DriverEntry(по поводу названий вообще всех функций — смотри соглашение в первой части статьи), которая фактически играет ту же самую роль для драйвера, что и main для проги на C. Эта функция вызывается при загрузке драйвера(неважно, загружается ли он динамически или при запуске системы). Данная функция выполняет некоторые действия, нужные для нормальной работы драйвера(например, регистрирует в специальном массиве адреса всех остальных функций драйвера, чтобы диспетчер ввода — вывода мог вызывать их по этим адресам). Если это не WDM драйвер, то в этой функции происходит локализация обслуживаемого оборудования, выделение и/или подтверждение используемых аппаратных ресурсов, выдача видимых для системы имён всем найденным обслуживаемым устройствам и т.д. WDM драйвера эту работу перекладывают на функцию AddDevice. Функция DriverEntry может вызываться с уровня IRQL == PASSIVE_LEVEL. Функция возвращает значение типа NTSTATUS, и принимает два параметра: адрес объекта драйвера(PDRIVER_OBJECT) и путь в реестре к подразделу драйвера(PUNICODE_STRING). Получив от диспетчера ввода — вывода указатель на структуру DRIVER_OBJECT драйвер должен заполнить в ней некоторые поля:

1) Поле DriverUnload — для регистрации собственной функции Unload, вызываемой при выгрузке драйвера. Подробнее о ней.Эта функция вызывается только при динамической выгрузке драйвера(т.е. происшедшей не в результате завершения работы системы). Legacy драйвера в этой функции выполняют полное освобождение всех занятых драйвером системных ресурсов. WDM драйвера выполняют такое освобождение в функции RemoveDevice при удалении каждого устройства(если драйвер обслуживает несколько устройств). Функция Unload вызывается с уровня IRQL PASSIVE_LEVEL, принимает единственный параметр(PDRIVER_OBJECT) — указатель на объект драйвера, и возвращаетvoid.

2) Поле DriverStartIo — для регистрации собственной функцииStartIo. Вкратце, регистрация функции StartIo нужна для участия в System Queuing — создании очередей необработанных запросов системными средствами, в отличие от DriverQueuing — когда то же самое реализуется средствами самого драйвера.

3) Поле AddDevice в подструктуре DriverExtension — для регистрации WDM драйвером своей процедурыAddDevice.

4) Поле MajorFunction — для регистрации драйвером точек входа в свои рабочие процедуры.

Бывают ситуации, когда при первоначальной загрузке драйвер не может до конца окончить процедуру инициализации(например, если необходимы какие-либо системные объекты или другие драйвера, ещё не загруженные). В этом случае драйвер регистрирует свою процедуру для завершения инициализации позднее. Регистрация этой процедуры выполняется вызовом IoRegisterDriverReinitialization с уровня IRQL PASSIVE_LEVEL, принимающей следующие параметры: указатель на объект драйвера(PDRIVER_OBJECT), указатель на процедуру реинициализации, предоставляемую драйвером(PDRIVER_REINITIALIZE) и контекстный указатель, получаемый регистрируемой функцией при вызове, и возвращающейvoid.

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

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

Я перечислила основные функции драйвера для его загрузки и выгрузки. Теперь перейдём к рабочим процедурам драйвера.Поговорим о следующих рабочих процедурах: обслуживания ввода — вывода(включающих в себя процедуры передачи данных и обслуживания прерываний), callback процедуры для синхронизации доступа к объектам и некоторые другие.

Все драйверы должны иметь обработчик CreateDispatch, обрабатывающий пользовательский запрос CreateFile. Если драйверу нужно обрабатывать пользовательский запрос CloseHandle, то он должен иметь обработчикCloseDispatch.

Перейдём к процедурам передачи данных. Это обработчики пользовательских запросов ReadFile, WriteFile иDeviceIoControl.

Процедуру StartIo я уже рассмотрела, поэтому перейдём к процедуре обслуживания прерываний(ISR — Interrupt Service Routine, напоминаю на всякий случай). Данная процедура вызвается диспетчером прерываний ядра(Kernel`s Interrupt Dispatcher) при каждой генерации прерывания устройством, и она обязана полностью обслужить это прерывание.

Теперь о callback процедурах сихронизации доступа к объектам. Для начала разберёмся, в чём различия принципов сихронизации доступа к объектам в пользовательском и ядерном режимах. Например, в пользовательском режиме, если какой — либо поток обратился к объекту, уже занятому другим потоком, то он(первый поток) запросто может быть заблокирован до лучших времён. Как ты сам понимаешь, в режиме ядра такая внеплановая «заморозка» потоков неприемлема, поэтому и применяется другая технология сихронизации. И заключается она в следующем. Когда какой-либо поток обращается к объекту, уже занятому другим потоком, то он оставляет свой запрос в очереди запросов. Если драйвер предварительно зарегистрировал особую callback функцию, то диспетчер ввода — вывода при освобождениитребуемого ресурса, уведомит об этом драйвер, вызвав callback — функцию. Таким образом, обеспечивается гарантия ответа на любой запрос к ресурсу, даже если он(ответ) будет состоять только в том, чтобы уведомить о задержке в обработке и помещении запроса в очередь. Функции, это реализующие: IoAllocateController(использующаяся для синхронизации доступа к контроллеру), AdapterControl(использующаяся для синхронизации доступа к DMA каналам(чаще всего)) и SynchCritSection (использующаяся для корректного обращения к ресурсам; точнее — эта функция позволяет коду с низким уровнем IRQL сделать работу при уровне DIRQL устройства без опасения возникновения конфликтов с ISR). 

Также можно упомянуть ещё таймерные процедуры(нужные для драйверов, выполняющих точный отсчёт временных интервалов; обычно реализуется с использованием IoTimer(но не всегда)), процедуру IoCompletion (позволяющую WDM драйверу, работающему внутри многослойной драйверной структуры, получать уведомление о завершении обработки IRP запроса, направленного к драйверу нижнего уровня) и CancelRoutine(если драйвер зарегистрирует эту callback процедуру при помощи вызова IoSetCancelRoutine, то диспетчер ввода — вывода сможет уведомить его об удалении IRP запросов, находящихся в ожидании обработки, что может понадобиться диспетчеру, если пользовательское приложение, инициировавшее эти IRP запросы, неожиданно завершит свою работу после снятия задачи диспетчером задач(прошу прощения за необходимую тавтологию)).

Ну вот и всё, мы закончили обзор структуры драйвера и основных его процедур. Примеры их практического применения(а также некоторых других функций) ты увидишь в третьей, заключительной статье этого цикла, в которой мы напишем свой полноценный Legacy драйвер.

Заключение

Уфф, наконец-то мы покончили со скучной теорией. В этой статье ты узнал про некоторые приёмы программирования в режиме ядра, изучил структуру драйвера и его основные функции. Теперь ты полностью подготовлен(на сей раз уже окончательно) к написанию своего первого(или двадцать первого — не знаю) полноценного Legacy драйвера под Windows. Это мы проделаем в заключительной части моего рассказа о программировании драйверов под Windows. А пока что слегка отдохнём. Да не облысеют твои пятки!

xakep.ru


Смотрите также