Переопределение назначений клавиш на клавиатуре в операционной системе Windows
Факультет: Информатика и системы управления
Кафедра: Программное обеспечение ЭВМ и информационные технологии
РАСЧЕТНО-ПОЯСНИТЕЛЬНАЯ ЗАПИСКА
к курсовому проекту на тему:
"Переопределение назначений клавиш на клавиатуре
в операционной системе Windows"
2009 г.
Оглавление
Введение 4
1. Аналитический раздел 5
1.1 Постановка задачи 5
1.2 Архитектура Windows XP 5
1.3 Модель WDM 8
1.4 Стек клавиатуры 9
1.5 Структура драйвера 12
DiverEntry 12
AddDevice 12
DriverUnload 13
Процедуры обработки пакетов IRP 13
Функция обработки пакетов IRP_MJ_DEVICE_CONTROL 15
Функция обработки пакетов IRP_MJ_READ 16
Функция обработки пакетов IRP_MJ_PNP 16
ISR 16
1.6 Анализ методов решения задачи 17
2. Конструкторский раздел 19
2.1 Точки входа разрабатываемого драйвера-фильтра 19
DiverEntry 19
AddDevice 20
DriverUnload 21
Процедуры обработки пакетов IRP 21
Функция обработки пакетов IRP_MJ_DEVICE_CONTROL 21
Функция обработки пакетов IRP_MJ_READ 22
Функция обработки пакетов IRP_MJ_PNP 22
Обработка остальных пактов IRP 23
2.2 Взаимодействие компонентов системы 24
2.3 Размещение драйвера в памяти 24
2.4 Переопределение клавиш 25
2.5 Установка драйвера в системе 27
3. Технологический раздел 30
3.1 Выбор языка программирования и средств программирования 30
Драйвер-фильтр 30
Программа для установки драйвера 31
Программа для управления драйвером 31
3.2 Структуры данных проекта 31
Информация о нажатиях и отпусканиях клавиш 31
Структура списка замен 33
Дополнительная информация 33
3.3 Интерфейс управляющего приложения 34
3.4 Тестирование драйвера-фильтра клавиатуры 35
Заключение 37
Список литературы 38
Введение
Очень часто возникает необходимость в переназначении клавиш на клавиатуре.
Стандартное расположение клавиш неудобно при работе с определенным программным обеспечением
При переходе с одного типа клавиатуры на другой трудно привыкнуть к новому расположению клавиш.
Во многих случаях определенные клавиши могут помешать работе. Случайное нажатие на такие клавиши замедляет работу пользователя. Желательно, чтобы нажатие этих клавиш не приводило ни к каким действиям.
Поэтому задача переопределения и отключения клавиш является на сегодняшний день актуальной.
Существуют программы, решающие поставленную задачу. Примерами таких программ являются программа Марка Руссиновича Ctrl2 и программа MagicKey.
Программа Ctrl2 заменяет клавишу CapsLock на клавишу «левый Ctrl». Она реализована в виде драйвера.
Программа MagicKey, позволяет менять местами алфавитно-цифровые клавиши. Она реализована в виде драйвера и пользовательского приложения для управления этим драйвером.
1. Аналитический раздел
1.1 Постановка задачи
В соответствии с заданием на курсовую работу, необходимо разработать программное обеспечение, позволяющее переопределять назначение клавиш на клавиатуре.
Программное обеспечение должно позволять переопределять все клавиши клавиатуры
Программное обеспечение должно позволять отключать клавиши.
Программное обеспечение должно позволять устанавливать список осуществляемых замен.
Программное обеспечение не должно нарушать и замедлять работу системы.
1.2 Архитектура Windows XP
Windows XP является следующей – после Windows 2000 и Windows Millennium – версией операционной системы Microsoft Windows. Наиболее распространены реализации данной ОС для платформы Intel x86 в одно- или многопроцессорных конфигурациях, однако существуют также версии для DEC Alpha и MIPS. Данная операционная система использует защищённый режим центрального процессора, реализует механизмы виртуальной памяти и многозадачности.
Windows XP использует два уровня привилегий: уровень привилегий 0, соответствующий коду режима ядра и уровень привилегий 3, соответствующий коду прикладных задач (всего существует четыре уровня привилегий). Уровень привилегий накладывает определённые ограничения: в пользовательском режиме не могут выполняться привилегированные инструкции процессора и не разрешено обращение к защищённым страницам в памяти. Эти ограничения накладываются для обеспечения безопасности работы системы. Пользовательское приложение не должно иметь возможность – в результате ошибки или преднамеренно – вносить изменения в системные таблицы или в память других приложений. В частности, такие ограничения запрещают пользовательскому приложению напрямую управлять внешними устройствами.
В Windows XP обеспечение обмена данными и управление доступом к внешнему устройству возлагается на его драйвер. Ввод и вывод в драйверах осуществляется пакетами – IRP (Input/Output Request Packet). Запросы на ввод / вывод, посылаемые приложениями или другими драйверами, обрабатываются драйвером, после чего запрашивающей программе в том же пакете посылается статус завершения операции.
Архитектура ввода / вывода в Windows XP имеет иерархическую структуру. Для осуществления операции ввода / вывода пользовательское приложение должно вызвать одну из функций API. Эта функция создает необходимый IRP пакет и направляет его подсистеме ввода / вывода. Подсистема ввода / вывода направляет IRP пакет необходимому драйверу. Драйвер осуществляет обращение к устройствам, используя функции HAL.
HAL (Hardware Abstraction Layer) – это слой программного обеспечения, который скрывает специфику аппаратной платформы от остальных компонентов системы. Он обеспечивает малые затраты при переносе системы или элементов программного обеспечения. На Рис. 1. показана архитектура ввода / вывода Windows XP.
Рис. 1. Архитектура ввода / вывода Windows XP
Управление внешним устройством в общем случае сводится к заполнению регистров контроллера необходимыми данными. Монопольный доступ драйвера к этим регистрам гарантируется операционной системой. Очевидно, что при данных обстоятельствах требуется, чтобы драйвер устройства выполняется в режиме ядра. В архитектуре Windows XP существуют так называемые драйверы виртуальных устройств для поддержки DOS приложений, исполняющиеся в реальном режиме.
Классификация драйверов Windows XP
Драйверы режима ядра
Драйверы файловых систем
Унаследованные
Драйверы Plug and Play
Видеодрайверы
Драйверы пользовательского режима
Драйверы виртуальных устройств
1.3 Модель WDM
Windows Driver Model (WDM) – это стандартная спецификация Microsoft для разработчиков драйверов устройств. Она поддерживается в операционных системах Windows 98/Me/2000/XP. Компания Microsoft требует, чтобы все новые драйверы под эти операционные системы создавались по этой спецификации. Для этого от них требуется чёткое следование структуре WDM, поддержка Plug and Play и управления электропитанием.
Драйверная модель WDM построена на организации и манипулировании слоями «Объектов физических устройств» и «Объектов функциональных устройств». Объект-устройство – это особая структура данных, создаваемая системой для взаимодействия программного и аппаратного обеспечения. Объект PDO создается для каждого найденного элемента аппаратуры, подключенного к шине данных. Объект FDO предлагает «олицетворение» каждой логической функции, которую видит в устройстве программное обеспечение верхних уровней.
Функциональным Объектам устройств разрешается окружать себя Объектами Фильтрами (Filter Device objects). Соответственно, каждому FiDO объекту сопоставлен драйвер, который выполняет определенную работу. Объукты фильтры подразделяются на фильтры нижнего уровня и фильтры верхнего уровня. И тех и других может существовать произвольное количество. Они модифицируют процесс обработки запросов ввода / вывода. Объекты FDO и FiDO отличаются только в смысловом отношении. FDO объект и его драйвер считаются главными. Они обычно обеспечивают управление устройством, а объекты FiDO являются вспомогательными.
Все объекты FDO и FiDO позиционируют себя в стеке устройств. Порядок объектов в стеке определяет порядок обработки запросов ввода-вывода. Если необходимо перехватить и обработать запрос, непосредственно идущий от пользователя, то нужно устанавливать верхний фильтр. Если же нужно отслеживать обращение к портам ввода вывода, обрабатывать прерывания, то нужен нижний фильтр. Данная модель позволяет драйверу устанавливать callback процедуры. Когда запрос начинает обрабатываться, то он обрабатывается последовательно всеми драйверами стека устройства (исключая ситуацию, когда какой-либо драйвер сам завершит обработку запроса). После этого диспетчер ввода-вывода передает запрос callback процедуре каждого драйвера стека. Сначала запрос передается callback процедуре последнего драйвера который в стеке, потом процедуре предпоследнего драйвера и.т. д. Callback процедуры нужны для того, чтобы обработать прочитанную из устройства информацию. Если фильтр обрабатывает запросы на чтение, то когда этот запрос поступит в драйвер информация еще не будет считана. Поэтому драйверу необходимо установить callback функцию. При ее вызове запрос уже будет содержать считанные данные.
1.4 Стек клавиатуры
Физическую связь клавиатуры с шиной осуществляет микроконтроллер клавиатуры Intel 8042. На современных компьютерах он интегрирован в чипсет материнской платы. Этот контроллер может работать в двух режимах: AT совместимом и PS/2 совместимом. Почти все клавиатуры уже давно являются PS/2 совместимыми. В PS/2 совместимом режиме микроконтроллер клавиатуры также связывает с шиной и PS/2 совместимую мышь. Данным микроконтроллером управляет функциональный драйвер i8042prt. Драйвер i8042prt создает два безымянных объекта «устройство» и подключает один к стеку клавиатуры, а другой к стеку мыши. Поверх драйвера i8042prt, точнее, поверх его устройств, располагаются именованные объекты «устройство» драйверов Kbdclass и Mouclass. Драйверы Kbdclass и Mouclass являются так называемыми драйверами класса и реализуют общую функциональность для всех типов клавиатур и мышей, т.е. для всего класса этих устройств. Оба эти драйвера устанавливаются как высокоуровневые драйверы.
Стек клавиатуры обрабатывает несколько типов. В данной курсовой работе необходимо рассмотреть только IRP типа IRP_MJ_READ, которые несут с собой коды клавиш. Генератором этих IRP является поток необработанного ввода RawInputThread системного процесса csrcc.exe. Этот поток открывает объект «устройство» драйвера класса клавиатуры для эксклюзивного использования и направляет ему IRP типа IRP_MJ_READ. Получив IRP, драйвер Kbdclass отмечает его как ожидающий завершения (pending), ставит в очередь и возвращает STATUS_PENDING. Потоку необработанного ввода придется ждать завершения IRP. Подключаясь к стеку, драйвер Kbdclass регистрирует у драйвера i8042prt процедуру обратного вызова KeyboardClassServiceCallback, направляя ему IRP IOCTL_INTERNAL_KEYBOARD_CONNECT. Драйвер i8042prt тоже регистрирует у системы свою процедуру обработки прерывания (ISR) I8042KeyboardInterruptService, вызовом функции IoConnectInterrupt. Когда будет нажата или отпущена клавиша, контроллер клавиатуры выработает аппаратное прерывание. Его обработчик вызовет I8042KeyboardInterruptService, которая прочитает из внутренней очереди контроллера клавиатуры необходимые данные. Т.к. обработка аппаратного прерывания происходит на повышенном IRQL, ISR делает только самую неотложную работу и ставит в очередь вызов отложенной процедуры (DPC). DPC работает при IRQL = DISPATCH_LEVEL. Когда IRQL понизится до DISPATCH_LEVEL, система вызовет процедуру I8042KeyboardIsrDpc, которая вызовет зарегистрированную драйвером Kbdclass процедуру обратного вызова KeyboardClassServiceCallback (также выполняется на IRQL = DISPATCH_LEVEL). KeyboardClassServiceCallback извлечет из своей очереди ожидающий завершения IRP, заполнит структуру KEYBOARD_INPUT_DATA, несущую всю необходимую информацию о нажатиях / отпусканиях клавиш, и завершит IRP. Поток необработанного ввода пробуждается, обрабатывает полученную информацию и вновь посылает IRP типа IRP_MJ_READ драйверу класса, который опять ставится в очередь до следующего нажатия / отпускания клавиши. Таким образом, у стека клавиатуры всегда есть, по крайней мере, один, ожидающий завершения IRP и находится он в очереди драйвера Kbdclass.
Разрабатываемый драйвер-фильтр устанавливается над фильтром Kbdclass. Так как IRP типа IRP_MJ_READ является фактически запросом на чтение данных, то когда он идет вниз по стеку его буфер, естественно пуст. Прочитанный данные буфер будет содержать только после завершения IRP. Для того, чтобы эти данные увидеть, фильтр должен установить в каждый IRP (точнее в свой блок стека) процедуру завершения. В этой процедуре как раз и будут осуществляться операции по преобразованию скэнкодов.
1.5 Структура драйвера
Драйвер имеет следующие точки входа:
DriverEntry
DriverUnload
AddDevice
Функции для обработки пакетов IRP
ISR
DiverEntry
Процедура DiverEntry должна присутствовать в любом драйвере. На данную процедуру возложена функция начальной инициализации и определение остальных точек входа в драйвер. Она выполняется в момент загрузки драйвера. В драйверах WDM значение этой функции значительно уменьшилось, большая часть работы возлагается на функцию AddDevice. Для регистрации точек входа в драйвер DriverEntry должна заполнить соответствующие поля в структуре объекта драйвера. Указатель на эту структуру передается в функцию.
Поле DriverUnload необходимо заполнить адресом процедуры, вызывающейся при выгрузке драйвера.
Поле DriverExtension->AddDevice необходимо заполнить адресом процедуры AddDevice.
Массив MajorFunctions заполняется адресами процедур обработки IRP пакетов. Процедура, зарегистрированная под номером N, обрабатывает IRP пакет с кодом N. Обычно драйверы используют не все эти процедуры, а регистрируют только нужные. Остальные же элементы массива заполняются адресом процедуры, выполняющей передачу пакета ниже по стеку драйверов.
AddDevice
Данная функция регистрируется, если драйвер поддерживает PnP. Одна из главных обязанностей AddDevice – это создание объекта устройства FDO и если необходимо подключение его к стеку драйверов устройства. Данная функция может создать несколько объектов устройств и подключить их к разным стекам. Более того, некоторые устройства FDO могут существовать, не будучи в связке с PDO. Они часто содаются для управления драйвером.
DriverUnload
Процедура DriverUnload необходима для того, чтобы сделать драйвер выгружаемым. В драйверах «в стиле NT» на эту процедуру возложен весь процесс выгрузки. Она обязана удалить все символьные ссылки, все объекты устройств, отключить (если нужно) прерывания от объектов, очистить память за собой. В PnP драйверах все эти действия возложены на обработчик пакетов IRP_MJ_PNP.
Процедуры обработки пакетов IRP
Функции, адреса которых записаны в массиве MajorFunctions, вызываются диспетчером ввода / вывода для обработки соответствующих запросов от клиентского драйвера (пользовательских приложений или модулей уровня ядра). Эти запросы оформляются в виде специальных структур – IRP пакетов.
При любом запросе Диспетчер формирует IRP. память для структуры IRP выделяется в нестраничной памяти. В IRP записывается код операции ввода вывода. Пакет IRP состоит из заголовка, который имеет постоянный размер и стека IRP. Стек имеет переменную длину.
Заголовок IRP пакета:
Поле IoStatus типа IO_STATUS_BLOCK содержит два подполя
Status содержит значение, которое устанавливает драйвер после обработки пакета.
В Information чаще всего помещается число переданных или полученных байт.
Поле AssociatedIrp. SystemBuffer типа void* содержит указатель на системный буфер для случая если устройство поддерживает буферизованный ввод / вывод.
Поле MdlAddress типа PMDL содержит указатель на MDL список, если устройство поддерживает прямой ввод вывод.
Поле UserBuffer типа void* содержит адрес пользовательского буфера для ввода / вывода.
Типа Cancel типа BOOLEAN – это индикатор того, что пакет IRP должен быть аннулирован.
Стек IRP пакета
Основное значение ячеек стека IRP пакета состоит в том, чтобы хранить функциональный код и параметры запроса на ввод / вывод. Для запроса, который адресован драйверу самого нижнего уровня, соответствующий IRP пакет имеет только одну ячейку стека. Для запроса, который послан драйверу верхнего уровня, Диспетчер ввода / вывода создает пакет IRP с несколькими стековыми ячейками – по одной для каждого FDO.
Каждая ячейка стека IRP содержит:
MajorFunction типа UCHAR – это код, описывающий назначение операции
MinorFunction типа UCHAR – это код, описывающий суб-код операции
DeviceObject типа PDEVICE_OBJECT – это указатель на объект устройства, которому был адресован данный запрос IRP
FileObject типа PFILE_OBJECT – файловый объект для данного запроса
Диспетчер ввода / вывода использует поле MajorFunction для того, чтобы извлечь из массива MajorFunction нужную для обработки запроса процедуру.
Каждая процедура обработки IRP пакетов должна в качестве параметров иметь:
Указатель на объект устройства, для которого предназначен IRP запрос
Указатель на пакет IRP, описывающий этот запрос
Возвращает такая функция значение типа NTSTATUS, содержащее результат обработки.
Функция обработки пакетов IRP_MJ_DEVICE_CONTROL
Эти функции позволяют обрабатывать расширенные запросы от клиентов пользовательского режима. Такой запрос может быть сформирован посредством вызова функции DeviceIoControl. Каждый IOCTL запрос имеет свой код. Этот код передается как параметр функции DeviceIoControl. Код IOCTL – это 32 битное число.
Запросы IOCTL служат чаще всего для обмена данными между драйвером и приложением. Для передачи данных в Windows предусмотрены 4 способа
METHOD_BUFFERED
Входной пользовательский буфер копируется в системный, а по окончании обработки системный копируется в в выходной пользовательский буфер.
METHOD_IN_DIRECT и METHOD_OUT_DIRECT
Необходимые страницы пользовательского буфера загружаются с диска в оперативную память и блокируются. И с помощью DMA осуществляется передача данных между устройством и пользователем.
METHOD_NEITHER
При данном методе передачи не производится проверка доступности памяти, не выделяются промежуточные буфера и не создаются MDL. В пакете IRP передаются виртуальные адреса буферов в пространстве памяти инициатора запроса ввода / вывода.
Функция обработки пакетов IRP MJ READ
Данная функция должна обрабатывать запросы на чтение информации из устройства.
Функция обработки пакетов IRP MJ PNP
Данная функция должна обрабатывать запросы от менеджера PnP.
ISR
Данная точка входа вызовется при, когда произойдет прерывание, на которое зарегистрирована эта ISR функция. Вызов может произойти в любом контексте: как ядра, так и пользовательского процесса. Здесь драйвер может либо дожидаться следующего прерывания (с какой-либо целью), либо запросить отложенный вызов процедуры (Deferred Procedure Call), DPC
1.6 Анализ методов решения задачи
В соответствии с заданием на курсовую работу, необходимо разработать программное обеспечение, позволяющее переопределять назначение клавиш на клавиатуре. Из анализа архитектуры Windows XP следует, что для доступа к информации, содержащей скэнкоды нажатых или отпущенных клавишей необходимо написать драйвер. Драйвер может получить доступ к скэнкодам нажатых или отпущенных клавиш двумя способами. Либо перехватывая IRP пакеты от других драйверов, либо самостоятельно обрабатывая прерывания от клавиатуры. Оптимальным является написание драйвера, который перехватывал бы IRP пакеты от драйвера клавиатуры, то есть написание драйвера-фильтра. В этом случае нет необходимости переписывать уже сделанный драйвер клавиатуры, нужно написать драйвер, изменяющий информацию в приходящих IRP пакетах.
Существует два типа драйверов фильтров: драйвер-фильтр верхнего и нижнего уровня. В рамкой данной задачи не имеет значения на каком этапе будет производится переопределение. Но предпочтительнее выбрать драйвер-фильтр верхнего уровня, поскольку информация возвращаемая драйвером клавиатуры хорошо документирована и описана в литературе.
Ввиду того, что все современные драйверы, рекомендуется писать согласно стандарту PnP, поскольку они обладают большей функциональностью, то разрабатываемый драйвер должен быть драйвером PnP.
Формализация постановки задачи:
Необходимо написать драйвер-фильтр верхнего уровня для драйвера клавиатуры.
Драйвер-фильтр должен перехватывать IRP пакеты, содержащие скэнкоды нажатых и отпущенных клавиш, переопределять, если необходимо, скэнкоды на скэнкоды других клавиш. Он должен удалять записи, соответствующие отключенным клавишам.
Решение о том, какие клавиши должны быть переопределены или отключены, принимается в соответствии со списком замен, который хранится в памяти ядра.
Для установки списка замен используется пользовательское приложение, которое пересылает в драйвер список замен.
Драйвер должен быть драйвером PnP.
Драйвер-фильтр не должен тормозить ввод с клавиатуры и работу всей системы в целом. Драйвер-фильтр должен обеспечить надежную работу системы.
2. Конструкторский раздел
2.1 Точки входа разрабатываемого драйвера-фильтра
Поскольку разрабатываемый драйвер-фильтр является драйвером PnP, то должен иметь следующие точки входа:
DriverEntry
DriverUnload
AddDevice
Функции для обработки пакетов IRP
Функции для обработки прерываний в данной работе не регистрируются, поскольку драйвер не работает с прерываниями.
DiverEntry
В данной работе процедура DriverEntry выполняет следующие действия:
Заполнение массива MajorFunctions. Регистрируется процедура обработки пакета на чтение, процедура обработки IOCTL запросов, процедуры обработки запросов от менеджера PnP и менеджера питания. Остальные элементы массива заполняются адресом функции MyPassNext, которая пропускает пакеты ниже по стеку.
Регистрация процедуры AddDevice. В данной работе она называется MyAddDevice.
Регистрация процедуры DriverUnload, называющейся MyUnload.
Выделение памяти для хранения массива замен клавиш. Одна запись массива занимает 4 байта, а максимум может быть только 103 замены (клавиша Pause/Break не в счет). Значит максимальный объем массива равен 412 байт. DriverEntry сразу выделяет при загрузке эти 412 байт. Не имеет смысла экономить и выделять память динамически при каждой инициализации массива, поскольку 412 – это очень мало, и система не будет тратить время на освобождение и выделение памяти при каждой инициализации.
Инициализация некоторых глобальных переменных: AltPressed, CtrlPressed, KeyPause.
DriverEntry регистрирует только необходимые процедуры. Поскольку проект представляет собой драйвер-фильтр верхнего уровня, и в нем нет необходимости обрабатывать прерывания, то не производится регистрация DriverStartIo, процедур ISR и DPC.
AddDevice
В данной работе функция MyAddDevice создает одно функциональное устройство с именем \\Device\\MyFilter. При создании устройства происходит резервирования места для хранения адреса устройства, расположенного ниже в стеке драйверов. Это сделано для того, чтобы при разрушении стека драйверов передать запрос PnP на демонтаж нижестоящему драйверу. Созданное устройство подключается к стеку драйверов клавиатуры. Это делается с помощью функции IoAttachDeviceToDeviceStack. Это стандартная функция Windows, она принимает PDO и указатель на структуру подключаемого FDO. FDO занимает место в стеке драйверов сразу после объекта, находящегося в вершине стека. Теперь подключаемый FDO становится вершиной стека. Нельзя подключится к стеку когда он уже сформирован, поэтому необходимо подключится к нему в определенный момент. Очередность загрузки драйверов описана в реестре Windows. Программа установки производит необходимую регистрацию. Структура этой программы описана ниже.
Для того чтобы пользовательское приложение смогло обратиться к драйверу (для загрузки в драйвер списка замен или для получения списка замен, которые драйвер осуществляет в данный момент) для FDO должно быть зарегистрировано DOS имя. Используя это имя приложение сможет послать драйверу IOCTL запрос. Для регистрации такого имени создается строка юникод со значением \\DosDevices\\MyFilter и применяется функция IoCreateSymbolicLink. Ее параметрами является только что созданная строка и имя FDO, которое обслуживает наш драйвер. Теперь \\DosDevices\\MyFilter – это DOS имя созданного FDO устройства.
DriverUnload
Поскольку данный фильтр является PnP драйвером, то на процедуру DriverUnload ничего не возложено.
Процедуры обработки пакетов IRP
Разрабатываемый драйвер-фильтр осуществляет обработку следующих пакетов IRP:
IRP_MJ_DEVICE_CONTROL
IRP_MJ_READ
IRP_MJ_PNP
Остальные IRP пакеты пропускаются ниже по стеку драйверов.
Функция обработки пакетов IRP_MJ_DEVICE_CONTROL
В данной работе пользовательское приложение должно иметь возможность посылать IOCTL запросы драйверу. Приложение должно иметь возможность получить список текущих замен осуществляемых драйвером и передать драйверу новый список замен.
Для этого в теле драйвера определены две 32 битные константы.
GetKeys
CTL_CODE (FILE_DEVICE_KEYBOARD, 0x810, METHOD_BUFFERED, FILE_ANY_ACCESS)
SetKeys
CTL_CODE (FILE_DEVICE_KEYBOARD, 0x811, METHOD_BUFFERED, FILE_ANY_ACCESS)
Это коды IOCTL запросов не использующиеся драйверами стека клавиатуры. Поэтому в данном проекте они могут быть использованы безо всяких опасений. Запросы с первым кодом используется для получения списка текущих замен, со вторым для его установки.
Поскольку в первом случае драйверу необходимо получить буфер с данными, а во втором передать его, то необходимо использовать один из четырех способов передачи данных. В проекте применяется способ METHOD_BUFFERED. Поскольку список замен занимает всего 500 байт, то его размер не повредит системному пулу, и копироваться пользовательский буфер в системный будет очень быстро. Нет необходимости применять более сложные методы METHOD_IN_DIRECT или METHOD_NEITHER использующиеся при передаче больших объемов данных.
При обработке запроса на получение списка замен процедура копирует данные из буфера драйвера в системный буфер. После завершения обработки запроса менеджер ввода / вывода скопирует системный буфер в выходной пользовательский. Таким образом приложение сможет получить список замен. При установке списка замен менеджер скопирует пользовательский буфер в системный, а функция обработки IOCTL скопирует системный буфер в буфер драйвера. После этого драйвер начинает производить замену сканкодов, ориентируясь на новые данные.
Функция обработки пакетов IRP_MJ_READ
Данная функция осуществляет обработку пакетов на чтение. IRP пакет сначала будет попадать в разрабатываемый драйвер. Вызовется зарегистрированная в DriverEntry функция MyRead. К моменту вызова MyRead, буфер не содержит кодов считанных клавиш. Для того чтобы получить доступ к ним. MyRead должна установить CallBack процедуру. Она получит управление когда буфер IRP пакета будет содержать информацию о нажатых клавишах и будет подниматься вверх по стеку драйверов и будет вызывать CallBack функции на каждом уровне стека. CallBack процедура устанавливается с помощью функции IoSetCompletionRoutine. Далее в MyRead происходит копирование текущей ячейки IRP пакета в следующую ячейку, таким образом. Таким образом происходит передача неизмененных параметров в Kbdclass.
Функция обработки пакетов IRP_MJ_PNP
Драйвер-фильтр должен обрабатывать только запрос IRP_MN_REMOVE_DEVICE. При этом функция посылает данный пакет менеджера PnP нижестоящему в стеке устройству. Затем она производит необходимые завершающие действия:
отключает устройство от стека драйверов вызовом функции IoDetachDevice
удаляет устройство FDO вызовом функции IoDeleteDevice
удаляет символьную ссылку вызовом IoDeleteSymbolicLink
Остальные пакеты пропускаются ниже по стеку.
Обработка остальных пактов IRP
Остальные пакета IRP, которые за ненадобностью не обрабатываются в данном фильтре, пропускаются ниже по стеку. Процедуры данного фильтра не в праве самостоятельно обрабатывать эти запросы, так как это могут запросы, адресованные нижестоящим драйверам. Примером одного из таких запросов является IOCTL запрос, адресованный драйверу i8042prt и предназначенный для перепрограммирования котроллера клавиатуры и для зажжения лампочек на клавиатуре. Такие запросы посылает Windows при обнаружении нажатия CapsLock, NumLock или ScrollLock.
В данной работе за пропускание пакетов вниз отвечает процедура MyPassNext. Она передает IRP пакет нижестоящему драйверу с помощью функции IoCallDriver. При этом нижестоящий драйвер должен считывать текущую ячейку IRP пакета. Это достигается за счет использования функции IoSkipCurrentIrpStackLocation
2.2 Взаимодействие компонентов системы
2.3 Размещение драйвера в памяти
Некоторые процедуры драйвера, те которые выполняют инициализацию, выгодно выполнить и освободить память после выполнения. Поскольку процедуры инициализации выполняются всего один раз при загрузке системы, а после этого находятся в памяти, занимая ценное место. В языке C есть специальная директива, позволяющая разместить инициализирующий код в специальной секции. Память из под этой секции будет возвращена системе после выполнения. Это директива #pragma alloc_text («INIT», имя). Параметром директивы является имя функции.
В данном фильтре функцией, которая выполняет инициализацию, является только DriverEntry. Ее имя и является параметром директивы.
По умолчанию функции драйвера размещаются в нестраничной памяти. Эта память является очень ценной, поскольку она не может быть выгружена на жесткий диск. Экономней было бы разместить код драйвера в странично организованной памяти. Для этого в C предусмотрена директива #pragma alloc_text («PAGE», имя). Параметром директивы является имя функции, которая должна быть размещена в странично организованной памяти. В данном драйвере-фильтре все процедуры кроме DriverEntry размещаются там.
2.4 Переопределение клавиш
Поскольку функция MyRead, которая обрабатывает пакеты IRP_MJ_READ, получает пакет IRP без прочитанных данных, то она устанавливает CallBack процедуру. Эта процедура вызывается, когда буфер получает данные.
Реализация процедуры MyReadBack
N – это количество записей в буфере.
K – количество записей в массиве замен.
S[i] – скэнкод клавиши, соответствующей i ой записи буфера.
F1 [j] – скэнкод заменяемой клавиши (j ая запись массива замен).
F2 [j] – скэнкод клавиши, на которую происходит замена (j ая запись массива замен).
Клавиша Pause
Драйвер не позволяет заменить клавишу Pause на какую либо другую клавишу, посколько она сигнализирует только при нажатии. Но эта клавиша может выдавать различные записи в буфере, в зависимости от того, нажат Ctrl или нет.
Предположим, пользователь поменял правый Ctrl и «a» местами. В этом случае при нажатии на «a» + Pause, клавиша Pause должна выдать код, как будто бы была нажата клавиша Ctrl. И наоборот выдать обычный код Pause при нажатии Ctrl + Pause.
Для решения этой задачи необходимо завести переменную, которая будет хранить информацию, о том, нажата ли клавиша, отвечающая на данный момент за Ctrl. Эта переменная называется CtrlPressed. Если клавиша нажата, то переменная равна 1, иначе 0.
Алгоритм работы с клавишей Pause
Встречена последовательность записей Ctrl + Pause
Если CtrlPressed=1, то
последовательность пропускается в неизмененном состоянии
Если CtrlPressed=0, то
последовательность, заменяется на последовательность, соответствующую обычной Pause
Встречена последовательность записей Pause
Если CtrlPressed=1, то
последовательность, заменяется на последовательность, соответствующую Ctrl + Pause
Если CtrlPressed=0, то
последовательность пропускается в неизмененном состоянии
Клавиша PrintScreen
Поскольку PrintScreen выдает разные записи, в зависимости от того, нажата ли клавиша Alt. Клавиша PrintScreen, в отличие от Pause может быть заменена на другую клавишу. И она всегда выдает по одной записи при нажатии и тпускании.
Как и в случае с Pause вводится переменная AltPressed, которая равна 1, если нажата клавиша, отвечающая за Alt.
Произведем унификацию. При встрече записи, соответствующей PrintScreen или Alt + PrintScreen будем заменять ее на запись, соответствующую PrintScreen.
Теперь необходимо проверить, есть ли PrintScreen в списке замен, и если нужно, заменить его на другую клавишу или вообще удалить из буфера (если клавиша отключена).
Если PrintScreen был заменен на другую клавишу, то
никакие действия над ним не производятся
Если PrintScreen не был заменен, то
Если AltPressed=1, то
запись заменяется на запись, соответствующую Alt + PrintScreen
Если AltPressed=1, то
запись заменяется на запись, соответствующую Alt + PrintScreen
2.5 Установка драйвера в системе
Для установки драйвера необходимо вызвать функции драйвера в определенный момент загрузки системы. Это необходимо для того, чтобы драйвер занял нужное место в стеке драйверов. Операционная система Windows осуществляет загрузку драйверов в порядке, прописанном в системном реестре.
Каждое устройство имеет свой раздел в реестре. Все эти разделы находятся в HKEY_LOCAL_MACHINE\SYSTEM\CurrentControleSet\Control\Class. Клавиатуре соответствует раздел {4D36E96B-E325–11CE-BFC1–08002BE10318}. У каждого устройства в его разделе есть ключи UpperFilters и LowerFilters. Это ключи типа MultiString. Они содержат имена верхних и нижних драйверов-фильтров данного устройства. Драйверы-фильтры загружаются в систему в том порядке, в каком они записаны в этих ключах.
Для регистрации разрабатывавемого драйвера как фильтра необходимо поместить его имя в первым в ключе UpperFilters.
Для регистрации нового драйвера необходимо создать раздел с именем этого драйвера в системном реестре по адресу HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. Этот раздел должен содержать следующие ключи.
Type типа двойное слово
Определяет тип подключаемого модуля. Для драйверов режима ядра значение всегда равно единице.
Start типа двойное слово
Определяет метод загрузки драйвера. Может принимать одно из следующих значений:
SERVICE_BOOT_START (0) – во время начальной загрузки ОС. Данное значение применяется, когда драйвер используется загрузчиком системы;
SERVICE_SYSTEM_START (1) – после начальной загрузки ОС. Применяется для драйверов, которые самостоятельно осуществляют поиск оборудования, но не используются загрузчиком системы (нумераторы).
SERVICE_AUTO_START (2) – автоматическая загрузка с помощью диспетчера управления сервисами (Service Control Manager). Применяется для драйверов, не поддерживающих Plug and Play.
SERVICE_DEMAND_START (3) – загрузка «по требованию» либо диспетчера Plug and Play при обнаружении устройства, либо диспетчера управления сервисами при поступлении от пользователя команды на загрузку.
SERVICE_DISABLED (4) – драйвер не загружается.
ErrorControl типа двойное слово
Определяет уровень контроля ошибок. Может принимать одно из следующих значений:
SERVICE_ERROR_IGNORE (0). В случае сбоя при загрузке драйвера игнорировать его и продолжить работу.
SERVICE_ERROR_NORMAL (1). В случае сбоя при загрузке драйвера продолжить работу, но выдать при этом соответствующее предупреждение.
SERVICE_ERROR_SEVERE (2). В случае сбоя при загрузке драйвера переключиться на последнюю конфигурацию, при которой работа системы происходила без ошибок.
SERVICE_ERROR_CRITICAL (3). Аналогично (2), но в случае повторного сбоя выдать «синий экран».
DisplayName типа строка ASCII
Название драйвера или устройства в том виде, в котором оно будет отображаться для пользователя.
ImagePath типа строка Unicode
Полный путь к файлу с драйвером на диске. Обычно это поле устанавливается в значение % windir%\system32\Drivers\DriverName.sys, где % windir% – папка, в которую установлена Windows, а DriverName – имя файла.
3. Технологический раздел
3.1 Выбор языка программирования и средств программирования
Данный проект состоит из трех программ:
Драйвер-фильтр
Программа для установки драйвера
Программа для управления драйвером
Каждая из программ, осуществляет общение с операционной системой на разном уровне. Соответственно необходим разный подход к этим задачам и специальный подбор средств разработки.
Драйвер-фильтр
От разрабатываемого драйвера-фильтра требуется высокая скорость работы и надежность. При написании такой задачи осуществляется множество манипуляций с памятью, операций с указателями, преобразований типов. Программисту важно представлять структуру скомпилированного продукта, чтобы правильно представить себе логику его работы. Среди языков программирования, удовлетворяющих этим требованиям, известны С и ассемблер. Для написания драйвера предпочтение было отдано языку C. Выбор сделан, основываясь на следующих причинах:
Существует специальный компилятор C, поставляемый вместе с пакетом DDK, предназначенный специально для компиляции драйверов. Он содержит множество макроопределений и библиотек, позволяющих сделать процесс написания драйвера более легким. Microsoft рекомендует его как основную среду для разработки драйверов для Windows. Компилятор содержит специальные функции, позволяющие уменьшить размер исходного кода. Специального компилятора ассемблера Microsoft не выпускает.
Программы на ассемблере работают, конечно, быстрее, чем программы на C. Но разница в скорости между этими языками не очень велика. Зато производительность труда при использовании C намного выше, чем при использовании ассемблера.
Программа для установки драйвера
Программа для установки представляет собой консольное предложение. На это приложение не накладывается особых требований по скорости и по размеру. Поэтому и была выбрана среда C++ Builder. Она позволяет легко создавать консольные предложения. Программы, написанные в этой среде, занимают мало места, и имеют компактный текст.
При написании программы также была использована технология структурного программирования, хотя для таких маленьких проектов использовать ее невыгодно. Будет только падение быстродействия.
Программа для управления драйвером
Программа для управления драйвером представляет собой оконное приложение. Написана она была в среде C++ Builder. Поскольку приложение предназначено для пользователя., то оно должно иметь красивый и понятный интерфейс. Данная среда содержит множество стандартных визуальных компонент. Позволяющих осуществить поставленную задачу. Для приложения скорость и объем не является критичным параметром. Поэтому выбор C++ Builder можно считать оптимальным.
В приложении используется технология объектно-ориентированного программирования. Без ее использования организация оконного интерфейса выглядела бы очень запутанной.
3.2 Структуры данных проекта
Информация о нажатиях и опусканиях клавиш
Скэнкоды нажатых или отпущенных клавиш хранятся в системном буфере. Доступ к этому буферу можно получить, если прочитать данные по адресу, хранящемуся в IRP пакете. Адрес хранится в поле AssociatedIrp. SystemBuffer. Буфер представляет собой массив структур типа KEYBOARD_INPUT_DATA. Она состоит из следующих полей:
UnitId типа USHORT. В нем хранится номер устройства, на котором нажали или отпустили клавишу.
MakeCode типа USHORT. В этом поле хранится скэнкод нажатой или отпущенной клавиши. Скэнкод, переданный в этом поле, не определяет клавишу однозначно. Для полной идентификации клавиши необходимо значение поля Flags.
Flags типа USHORT. Это поле несет дополнительную информацию о клавише, а так же определяет, нажали клавишу или отпустили. Если нулевой бит равен 0, то значит клавиша нажата, если же он равен 1, то она отпущена. Биты 1 и 2 используются для идентификации клавиши.
Reserved типа USHORT. Поля является зарезервированным.
ExtraInformation типа ULONG. Это поле содержит дополнительную информацию об устройстве.
Все клавиши, кроме клавиши Pause, вырабатывают одну запись при нажатии и одну при отпускании.
Клавиша Pause вырабатывает 4 записи, если она была нажата без Ctrl:
Скан код равен 29, флаги равны 4
Скан код равен 69, флаги равны 0
Скан код равен 29, флаги равны 5
Скан код равен 69, флаги равны 1
Если клавиша Pause была нажата вместе с Ctrl, то она выдает 2 записи:
Скан код равен 70, флаги равны 2
Скан код равен 70, флаги равны 3
Бит 2 равен 0, а бит 1 равен 1 в поле Flags у следующих клавиш: Insert, Delete, PageUp, PageDown, Home, End, PrintScreen, правй Ctrl, правй Alt, Enter на дополнительной клавиатуре, обратный слеш на дополнительной, стрелки, клавиши Windows, клавиша всплывающего меню.
У всех остальных клавиш биты 1 и 2 поля Flags равны 0.
Клавиша PrintScreen – это единственная клавиша, которая меняет выдаваемый скэнкод в зависимости от того, нажата ли другая. PrintScreen меняет скэнкод в зависимости от Alt.
Вместе с Alt PrintScreen выдает:
Скан код равен 84, флаги равны 0 (при нажатии)
Скан код равен 84, флаги равны 1 (при отпускании)
Без Alt PrintScreen выдает:
Скан код равен 55, флаги равны 2 (при нажатии)
Скан код равен 55, флаги равны 3 (при отпускании)
Структура списка замен
Список замен замен – это массив структур ChangeStruct. Она состоит из следующих полей:
f1 типа UCHAR. Поле содержит дополнительную, идентифицирующую заменяемую клавишу.
Scan1 типа UCHAR. Поле содержит скэнкод заменяемой клавиши
f2 типа UCHAR. Поле содержит информацию о клавише, на которую заменяют.
Scan2 типа UCHAR. Поле содержит скэнкод клавиши, на которую заменяют.
Фактически поля f1 и f2 содержат 1 и 2 бит поля Flags структуры KEYBOARD_INPUT_DATA. Они помогают идентифицировать клавишу, которая заменяется, и на которую происходит замена.
Дополнительная информация
При создании FDO выделяется память под структуру DEVICE_EXTENSION, в эта структура состоит всего из одного поля. Это поле TopOfStack типа PDEVICE_OBJECT. Оно содержит указатель на FDO, расположенное ниже в стеке клавиатуры, и используется для передачи IRP пакетов ниже по стеку. Адрес этой структуры находится в поле DeviceExtension нашего FDO.
3.3 Интерфейс управляющего приложения
Интерфейс управляющего приложения состоит из двух окон. Первое окно представляет собой модель клавиатуры, оно является главным в программе. Второе окно предназначено для вывода информации и называется информационным.
При нажатии на интересующую кнопку на главном окне она вдавливается, и красным цветом подсвечивается кнопка, на которую заменяется выбранная кнопка. При этом появляется информационное окно, содержащее следующие параметры:
Название выбранной кнопки
Название кнопки, на которую происходит замена
Состояние кнопки (включена или выключена)
Если необходимо переопределить кнопку, то нужно щелкнуть по этой кнопке. В информационном окне появится информация об активизированной кнопке. Далее необходимо нажать на кнопку «Заменить», она расположена в информационном окне. И затем щелкнуть по кнопке, на которую необходимо произвести замену.
После того как желаема конфигурация определена, нужно нажать кнопку «Установить». После этого информация будет отослана драйверу, и изменения вступят в силу. Если необходимо узнать, какие замены производит драйвер на данный момент, то нужно щелкнуть по кнопке «Принять».
Для быстрого восстановления стандартных настроек клавиатуры предусмотрена кнопка «По умолчанию». Она загружает стандартную конфигурацию. Чтобы она вступила в силу, необходимо нажать на кнопку «Установить».
3.4 Тестирование драйвера-фильтра клавиатуры
Драйвер был протестирован с использованием стандартной тестирующей утилиты DriverVerifier пакета DDK. С его помощью были проведены следующие тесты:
Операции с пулами памяти
Корректность уровней IRQL, на которых выполняется код драйвера
Обнаружение взаимоблокировок
Нехватка ресурсов
Нетипичные запросы к драйверу
Все тесты прошли успешно. Память в системе распределялась правильно, ошибок с ней не возникало. На нехватку ресурсов драйвер реагировал корректно. Нетипичные запросы к драйверу не обрабатывались им.
Для отладки драйвера использовалась программа DebugView. С помощью этой программы любой драйвер может выводить на экран отладочную информацию. При отладке эта программа позволила отследить процесс переопределения клавиш.
Драйвер использовали трое человек в течение месяца, и ошибок за этот период ими обнаружено не было.
Заключение
В данной работе рассмотрен вопрос, связанный с разработкой драйверов устройств в системе Windows, и реализован драйвер-фильтр клавиатуры.
Разрабатываемый драйвер позволяет
переопределять значение любых клавиш клавиатуры
отключать клавиши клавиатуры
Драйвер-фильтр отвечает всем современным требованиям, накладываемым Windows на драйверы.
Драйвер был протестирован с помощью тестирующих утилит Microsoft и успешно выдержал все тесты. Тестирование показало корректную работу.
Список литературы
Кузьмин И. Драйверы уровня ядра системы Windows XP // Программист, – М.:ООО «Викфилд». – №11, 2003
Солдатов В.П. Программирование драйверов Windows. Изд. 2-е, перераб. и доп. М.: ООО «Бином-Пресс», 2004 г.
MSDN Library, Copyright 1987–2004 Microsoft Corporation
Форум и публикации проекта «Первые шаги» – www.firststeps.ru.