В данной работе функция __MyFilterAddDevice создает одно функциональное устройство с именем \Device\kbd_filter. Происходит резервирование места для хранения адреса устройства, расположенного ниже в стеке драйверов. Это сделано для того, чтобы при разрушении стека драйверов передать запрос PnP на демонтаж нижестоящему драйверу. Созданное устройство подключается к стеку драйверов клавиатуры. Это делается с помощью функции IoAttachDeviceToDeviceStack. Это стандартная функция Windows, она принимает PDO и указатель на структуру подключаемого FDO. FDO занимает место в стеке драйверов сразу после объекта, находящегося в вершине стека. Теперь подключаемый FDO становится вершиной стека. Очередность загрузки драйверов описана в реестре Windows.
Для того чтобы пользовательское приложение смогло обратиться к драйверу для FDO должно быть зарегистрировано DOS имя. Используя это имя, приложение сможет послать драйверу IOCTL-запрос. Для регистрации такого имени создается строка-юникод со значением \DosDevices\kbd_filter и применяется функция IoCreateSymbolicLink. Ее параметрами является только что созданная строка и имя FDO, которое обслуживает наш драйвер. Теперь \DosDevices\kbd_filter - это DOS имя созданного FDO устройства.
В этой функции происходит также инициализация других объектов, используемых в работе драйвера-фильтра:
PinInit() – инициализация модуля, который работает с пином.
KeyMidiInit() – инициализация таблицы, в которой хранится информация о музыкальных параметрах клавиш.
Создание системного потока PlayThread, который отправляет музыкальные команды аудиоустройству.
Создание двух очередей, используемых в работе двух потоков.
Создание объектов синхронизации потоков .
Поскольку данный фильтр является PnP драйвером, то на процедуру DriverUnload ничего не возложено.
2.9.2 Функции обработки пакетов IRP
Разрабатываемый драйвер-фильтр осуществляет обработку следующих пакетов IRP:
IRP_MJ_DEVICE_CONTROL
IRP_MJ_READ
IRP_MJ_PNP
IRP_MJ_POWER
Остальные IRP пакеты пропускаются ниже по стеку драйверов.
Функция обработки пакетов IRP_MJ_DEVICE_CONTROL
В данной работе пользовательское приложение должно иметь возможность посылать IOCTL-запросы драйверу. Приложение должно иметь возможность отправить драйверу объект открытого музыкального пина и музыкальные параметры клавиши.
Для этого в теле драйвера определены две 32-битные константы:
#define IOCTL_SHARE_PIN \
CTL_CODE(FILE_DEVICE_KEYBOARD, 0x810, METHOD_BUFFERED, FILE_ANY_ACCESS)
По этому коду в драйвер передаётся 4 байта, которые являются HANDLE объекта пина, открытого в пользовательской программе.
#define IOCTL_MIDI_NOTE \
CTL_CODE(FILE_DEVICE_KEYBOARD, 0x811, METHOD_BUFFERED, FILE_ANY_ACCESS)
По этому коду в драйвер передаётся 7 байт, которые можно описать структурой:
typedef struct _KEY_MIDI_INFO
{UCHAR ScanCode; // Скан-код клавиши, генерируемый клавиатурой
UCHAR Flag; // Флаг клавиши, генерируемый клавиатурой
UCHAR Position; // Позиция на клавиатуре (всего 104 клавиши)
UCHAR Channel; // Музыкальный канал
UCHAR Instrument; // Музыкальный инструмент
UCHAR Note; // Музыкальная нота
UCHAR Used; // Для клавиши используется нота или нет
} KEY_MIDI_INFO, * PKEY_MIDI_INFO;
Это коды IOCTL-запросов, которые не используются драйверами стека клавиатуры. Поэтому в данном проекте они могут быть использованы безо всяких опасений.
Применяется способ передачи данных METHOD_BUFFERED. Т.к. передаётся буфер в 4 или 7 байт, то его размер не повредит системному пулу, и копироваться пользовательский буфер в системный будет очень быстро. Нет необходимости применять более сложные методы METHOD_IN_DIRECT или METHOD_NEITHER, которые используются при передаче больших объемов данных.
Функция обработки пакетов IRP_MJ_READ
Данная функция осуществляет обработку пакетов на чтение. IRP-пакет сначала попадает в разрабатываемый драйвер. Вызовется зарегистрированная в DriverEntry функция __MyFilterDispatchRead. К моменту вызова __MyFilterDispatchRead, буфер не содержит кодов считанных клавиш. Для того чтобы получить доступ к ним __MyFilterDispatchRead должна установить CallBack процедуру __MyFilterReadComplete. Она получит управление, когда буфер IRP-пакета будет содержать информацию о нажатых клавишах. Пакет будет подниматься вверх по стеку драйверов и вызывать CallBack функции на каждом уровне стека. CallBack процедура устанавливается с помощью функции IoSetCompletionRoutine.
MyFilterReadComplete получает буфер нажатых клавиш, как массив структур KEYBOARD_INPUT_DATA, функция выполняется на IRQL <= DISPATCH_LEVEL.
Далее в MyFilterDispatchRead происходит копирование текущей ячейки IRP-пакета в следующую ячейку. Таким образом происходит передача неизмененных параметров в Kbdclass.
Функция обработки пакетов IRP_MJ_PNP
Драйвер-фильтр должен обрабатывать только запросы IRP_MN_REMOVE_DEVICE и IRP_MN_SURPRISE_REMOVAL. При этом функция посылает данный пакет менеджера PnP нижестоящему в стеке устройству. В обработчиках этих запросов происходит освобождение памяти, которая выделялась для модуля работы с пином, для таблицы музыкальных нот, происходит завершение работы музыкального потока, освобождение очередей и объектов синхронизации, используемых в драйвере.
В обработчике IRP_MN_REMOVE_DEVICE дополнительно происходит:
отключение устройства от стека драйверов вызовом функции IoDetachDevice,
удаление устройства FDO вызовом функции IoDeleteDevice,
удаление символьной ссылки вызовом IoDeleteSymbolicLink.
Остальные пакеты пропускаются ниже по стеку.
Функция обработки пакетов IRP_MJ_POWER
Т.к. разрабатываемый драйвер является фильтром, задача которого – получить информацию о нажатых клавишах, то в нём не производится никаких действий, связанных с изменением питания. Поэтому эти IRP-пакеты пропускаются ниже по стеку.
Обработка остальных пактов IRP
Остальные IRP-пакеты, которые не обрабатываются в данном фильтре, пропускаются ниже по стеку. Функции данного драйвера-фильтра не в праве самостоятельно обрабатывать эти запросы, так как это могут запросы, адресованные нижестоящим драйверам. Примером одного из таких запросов является IOCTL-запрос, адресованный драйверу i8042prt и предназначенный для перепрограммирования котроллера клавиатуры и для зажжения лампочек на клавиатуре.
В данной работе за пропускание пакетов вниз отвечает процедура __MyFilterDispatchGeneral. Она передает IRP пакет нижестоящему драйверу с помощью функции IoCallDriver. При этом нижестоящий драйвер должен считывать текущую ячейку IRP пакета. Это достигается за счет использования функции IoSkipCurrentIrpStackLocation.
2.9.3 Функции работы с аудио-устройством
В модуле midi_pin реализованы все функции, которые обеспечивают работу с музыкальным пином на уровне ядра. В работе с музыкальным пином используются запросы на установку состояния пина и отправление пакетов с музыкальными данными.
NTSTATUS PinInit()
В этой функции происходит инициализация полей заголовка IRP-пакета и MIDI-данных, которые используются во время отправления музыкальных команд в пин. Далее в функциях PinMidiNoteOn и PinMidiNoteOff используется инициализированный заголовок.
Инициализация заголовка IRP-пакета происходит следующим образом:
typedef __declspec(align(16)) struct _MIDI_DATA
{KSMUSICFORMAT InstrumentFormat; // include <ksmedia.h>
union
{UCHAR InstrumentByte[4];
UCHAR NoteOffByte[4];};
KSMUSICFORMAT NoteFormat;
UCHAR NoteOnByte[4];
} MIDI_DATA, * PMIDI_DATA;
MIDI_DATA PinMidiData;
KSSTREAM_HEADER PinWriteHeader; // include <ks.h>
RtlZeroMemory(&PinWriteHeader, sizeof(PinWriteHeader));
// 12 байт
PinMidiData.InstrumentFormat.TimeDeltaMs = 0;
PinMidiData.InstrumentFormat.ByteCount = 3;
PinMidiData.InstrumentByte[0] = 0x00;
PinMidiData.InstrumentByte[1] = 0x00;
PinMidiData.InstrumentByte[2] = 0x00;
PinMidiData.InstrumentByte[3] = 0x00;
// ещё 12 байт
PinMidiData.NoteFormat.TimeDeltaMs = 0;
PinMidiData.NoteFormat.ByteCount = 3;
PinMidiData.NoteOnByte[0] = 0x00;
PinMidiData.NoteOnByte[1] = 0x00;
PinMidiData.NoteOnByte[2] = 0x00;
PinMidiData.NoteOnByte[3] = 0x00;
PinWriteHeader.Size = sizeof(PinWriteHeader);
PinWriteHeader.TypeSpecificFlags = 0;
PinWriteHeader.PresentationTime.Time = 0;
PinWriteHeader.PresentationTime.Numerator = 1;
PinWriteHeader.PresentationTime.Denominator = 1;
PinWriteHeader.Duration = 0;
PinWriteHeader.FrameExtent = 24; // всего 24 байта
PinWriteHeader.DataUsed = 24; // всего 24 байта
PinWriteHeader.Data = &PinMidiData;
NTSTATUS PinOpenStream(IN HANDLE UserPin)
Когда через запрос IOCTL_SHARE_PIN драйвер получает объект открытого пина, то необходимо вызвать эту функцию. В ней происходит вызов функции ObReferenceObjectByHandle для того, чтобы получить указатель на объект пина в режиме ядра и увеличить число ссылок на объект. Это делается для того, чтобы объект пина не был удалён из таблицы объектов ОС после заверешения работы пользовательского приложения, в котором был создан объект пина. Также здесь происходит установка флага, что пин открыт для драйвера.
UserPin – HANDLE того пина, который содержится в буфере IRP-пакета.
NTSTATUS PinIsOpenedStream()
Возвращает STATUS_SUCCESS если пин открыт.
NTSTATUS PinFree()
Здесь происходит вызов функции ObDereferenceObject, которая уменьшает число ссылок на объект пина.
NTSTATUS PinSetState(IN KSSTATE State)
Устанавливает состояние пина в State (KSSTATE_RUN, KSSTATE_PAUSE или KSSTATE_STOP). Перед тем, как воспроизводить ноты, необходимо установить состояние пина в KSSTATE_RUN. Функция работает только при IRQL = PASSIVE_LEVEL.
NTSTATUS PinWriteData(IN KSSTREAM_HEADER * Pheader)
Отправляет заголовок в открытый пин посредством IOCTL_KS_WRITE_STREAM.
Функция работает только при IRQL = PASSIVE_LEVEL.
NTSTATUS PinMidiNoteOn(IN UCHAR Channel,
IN UCHAR Instrument, IN UCHAR Note)
Если пин открыт, то отправляет команду на воспроизведение ноты Note с использованием инструмента Instrument в канале Channel.
Модификация инициализированного заголовка:
PinMidiData.InstrumentByte[0] = 0xC0 | Channel;
PinMidiData.InstrumentByte[1] = Instrument;
PinMidiData.NoteOnByte[0] = 0x90 | Channel;
PinMidiData.NoteOnByte[1] = Note;
PinMidiData.NoteOnByte[2] = 0x7F;
// 24 байта отправляем, т.к. в одном
PinWriteHeader.FrameExtent = 24; // пакете 2 команды: устанавливаем
PinWriteHeader.DataUsed = 24; // инструмент и отправляем ноту
PinWriteData(&PinWriteHeader);
Функция работает только при IRQL = PASSIVE_LEVEL.
NTSTATUS PinMidiNoteOff(IN UCHAR Channel, IN UCHAR Note)
Выключает ноту Note в канале Channel.
Модификация инициализированного заголовка:
PinMidiData.NoteOffByte[0] = 0x80 | Channel;
PinMidiData.NoteOffByte[1] = Note;
PinWriteHeader.FrameExtent = 12; // 12 байт шлём, т.к. в одном пакете