Как писать драйвера

Как писать драйвера (часть 1)

Предисловие.

Драйвера под Windows являются для большей массы программистов, "тайной за семью печатями". И вовсе не потому, что это что-то архисложное, сколько по причине абсолютной недокументированности идеологии.

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

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

Естественно, все коммерческие секреты я рассказывать не буду.

Некоторые требования к чтению.

Эти заметки не ориентированны на людей – проектирующих драйвера, они уже должны знать все, что здесь будет.

Не стоит читать их человеку, слабо знакомому с языками С и С++, а также плохо понимающему объектную структуру и принципы Win32.

Часть первая: "Что нужно для компиляции простейшего драйвера?"

Для разных типов Windows вам понадобиться разный набор программ.

В любом случае надо скачать Win DDK (Driver Development Kit), для той платформы, под которую пишется драйвер. Его можно брать с разных источников, лично я предпочитаю сайт Майкрософта.

Для линейки 9х вам понадобится и SDK (Software Development Kit), который возьмите там же.

Так как под продукцию Билла Гейтса стоит поставить и VC++ , я посоветую ставить в таком порядке.

1. Visual C++ 6

2. SDK

3. DDK

Для более поздних платформ достаточно только DDK.

Установив все эти продукты вы получите возможность собирать и инсталлировать драйвера.

Первая тестовая компиляция.

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

Мы пойдем более простым и надежным путем.

После установки DDK, у вас появились в директории samples или sources, в зависимости от версии, коды примеров драйверов. Это ваша библия, которую стоит изучить.

Проверка, собственно, правильности прописывания путей (environments), и собственно самой DDK и SDK – состоит в пробной компиляции примера.

Компиляция осуществляется посредством стандартных средств.

Программами build, make и link.

Makefile уже написан за вас, и вам надо сделать следующее. Зайдя в Start menu в разделе Programs|DDK|Build environment выберите вариант free и запустите.

Запустится стандартное окно терминала, с установленными переменными среды сборки DDK.

Перейдите в директорию с sources драйвера, который вы выбрали. Компилировать и собирать в корневой директории DDK не стоит.

Это приведет к сборке ВСЕХ драйверов примеров и компиляции всех библиотек, а это очень долго.

В корневой директории примера (драйвер принтера в нашем случае) запустите build с параметрами –cZ.

Если вы получили ошибки перепроверьте ваши установки. В нормальном случае вы получите возможность увидеть надпись об удачной сборки sys или vxd файла – в определенной директории. Чаще всего это Free/i386/

Но бывает и другая. Это говорит о том что ваш драйвер собран правильно и установка прошла успешно

Как писать драйвера (часть 2)

Прежде, чем хвататься за описание самого драйвера, давайте определимся с типами существующих драйверов.

По существующему в DDK разделению сам Microsoft подразделяет драйвера на следующие типы:

– Kernel-Mode Drivers;

– Kernel Streaming Drivers;

– Graphics Drivers;

– Network Drivers;

– Virtual Device Drivers.

С графическими и сетевыми драйверами более менее понятно по названию.

Виртуальные VDD относятся к устройствам, нуждающимся в работе под MS-DOS.

А вот два первых типа мы разберем немного подробнее.

На рисунке видна структура всех драйверов типа Kernel-Mode Drivers. В иерархии этой системы различают драйвера для работы на верхнем уровне(high-level), промежуточные (intermediate) и низкого уровня (low-level).

К первому относятся драйвера файловый систем (FAT, NTFS), ко второму – драйвера фильтры, драйвера виртуальных дисков, I/O устройств, а к третьему – драйвера непосредственно работающие на уровне прерываний.

Kernel Streaming Drivers – это в первую очередь драйвера устройств видео и аудио потока.

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

Более подробную информацию по разделению на типы, и их описание можно найти в Help DDK идущего в поставке DDK, и устанавливаемому вместе с этим пакетом.

Отступим от абстрактной классификации, и перейдем к более земным вещам.

Опишем среднестатистическую структуру драйвера.

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

В каждом драйвере есть входная точка, соответствующая функции main(), в замечательном С, WinMain() в Windows. Функция вызывается при старте системы автоматически в момент инициализации обслуживающей части Windows к которой относится данный драйвер.

Функции выхода нет.

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

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

Отбросив таким образом драйвера, который привязаны к железке мы остановимся на типе Network – сетевых драйверах.

Сетевые драйвера.

Сетевые драйвера тоже как и драйвера типа kernel, делятся на такие же три уровня. Вспомните, что все разделение я назвал условным.

Меняется только название.

– Miniport drivers;

– Protocol drivers;

– Intermediate drivers.

Структура сетевой части драйверного поголовья, похожа на пирог. Давайте посмотрим как это выглядит.

На рисунке четко видно, как структура NDIS (Network Driver Interface Specification) пронизывает все слои драйверного пирога.

Теперь давайте представим как проходит пакет от пользовательской аппликации к посылке через модем или сетевую карту в сеть. Все клиенты Microsoft, такие как браузер IE, или Outlook, пользуются одним и тем же слоем библиотек, откликающихся на имя транспортные библиотеки.

Главным в этом семействе является библиотека сокетов. Про то как она работает вы прочитаете в наших выпусках посвященных Winsock. В любом случае библиотека, оформив запрос отправляет его в NDIS, и о нем совершенно забывает. В структуре нашего пирога, главным управляющим звеном является как раз NDIS. Этот цербер распределяет кому и как передать пакет. Собственно драйвер в сети встроенный в NDIS предоставляет управленцу все необходимые прототипы функций. Начав с первого типа, протокольного, пакет проходит через слой промежуточных драйверов и попадает в драйвер модема (Miniport – это слово я на русский затруднился перевести в контексте, пусть так и останется).

Собственно драйвера протокольного уровня – это сам транспортный стек, а промежуточного – это обычные фильтры.

Для нас важно, что промежуточный драйвер не привязан ни к оборудованию, ни к какому либо стандарту Windows, но соответствует своей структурой любому сетевому драйверу.

Поэтому в качестве примера мы выберем именно его.

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

Как писать драйвера (часть 3)

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

Когда мы программируем под Windows API, мы ставим на обработку сообщений от Windows функцию WindowProc, которая регистрируется в момент создания класса окна. Примерно так же при создании экземпляра драйвера, в системе происходит регистрация всех необходимых функций.

Для регистрации и инициализации всего необходимого используется входная функция, которая так и называется: DriverEntry.

Для тех, кому тяжело качать Win2000 DDK размером 41Мб мы выложили базисный код драйвера в архиве. Скачайте архив для более полного ознакомления с текстом и работы с нашим текстом.

Сама DriverEntry запускается один раз, но важность правильной регистрации, думаю, понятна всем. Рассмотрим ее текст полностью.

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

/*++

Routine Description:

Arguments:

Return Value:

--*/

{

NDIS_STATUS Status;

NDIS_PROTOCOL_CHARACTERISTICS PChars;

NDIS_MINIPORT_CHARACTERISTICS MChars;

PNDIS_CONFIGURATION_PARAMETER Param;

NDIS_STRING Name;

NDIS_HANDLE WrapperHandle;

//

// Register the miniport with NDIS. Note that it is the miniport

// which was started as a driver and not the protocol. Also the miniport

// must be registered prior to the protocol since the protocol's BindAdapter

// handler can be initiated anytime and when it is, it must be ready to

// start driver instances.

//

NdisMInitializeWrapper(&WrapperHandle, DriverObject, RegistryPath, NULL);

NdisZeroMemory(&MChars, sizeof(NDIS_MINIPORT_CHARACTERISTICS));

MChars.MajorNdisVersion = 4;

MChars.MinorNdisVersion = 0;

MChars.InitializeHandler = MPInitialize;

MChars.QueryInformationHandler = MPQueryInformation;

MChars.SetInformationHandler = MPSetInformation;

MChars.ResetHandler = MPReset;

MChars.TransferDataHandler = MPTransferData;

MChars.HaltHandler = MPHalt;

//

// We will disable the check for hang timeout so we do not

// need a check for hang handler!

//

MChars.CheckForHangHandler = NULL;

MChars.SendHandler = MPSend;

MChars.ReturnPacketHandler = MPReturnPacket;

//

// Either the Send or the SendPackets handler should be specified.

// If SendPackets handler is specified, SendHandler is ignored

//

// MChars.SendPacketsHandler = MPSendPackets;

Status = NdisIMRegisterLayeredMiniport(WrapperHandle, &MChars, sizeof(MChars), &DriverHandle);

ASSERT(Status == NDIS_STATUS_SUCCESS);

NdisMRegisterUnloadHandler(WrapperHandle, PtUnload);

//

// Now register the protocol.

//

NdisZeroMemory(&PChars, sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

PChars.MajorNdisVersion = 4;

PChars.MinorNdisVersion = 0;

//

// Make sure the protocol-name matches the service-name under which this protocol is installed.

// This is needed to ensure that NDIS can correctly determine the binding and call us to bind

// to miniports below.

//

NdisInitUnicodeString(&Name, L"SFilter"); // Protocol name

PChars.Name = Name;

PChars.OpenAdapterCompleteHandler = PtOpenAdapterComplete;

PChars.CloseAdapterCompleteHandler = PtCloseAdapterComplete;

PChars.SendCompleteHandler = PtSendComplete;

PChars.TransferDataCompleteHandler = PtTransferDataComplete;

PChars.ResetCompleteHandler = PtResetComplete;

PChars.RequestCompleteHandler = PtRequestComplete;

PChars.ReceiveHandler = PtReceive;

PChars.ReceiveCompleteHandler = PtReceiveComplete;

PChars.StatusHandler = PtStatus;

PChars.StatusCompleteHandler = PtStatusComplete;

PChars.BindAdapterHandler = PtBindAdapter;

PChars.UnbindAdapterHandler = PtUnbindAdapter;

PChars.UnloadHandler = NULL;

PChars.ReceivePacketHandler = PtReceivePacket;

PChars.PnPEventHandler= PtPNPHandler;

NdisRegisterProtocol(&Status, &ProtHandle, &PChars, sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

ASSERT(Status == NDIS_STATUS_SUCCESS);

NdisIMAssociateMiniport(DriverHandle, ProtHandle);

return(Status);

}

Майкрософт утверждает что нам нужны 4 функции минимум, присутствующие в DriverEntry:

NdisMInitializeWrapper;

NdisIMRegisterLayeredMiniport;

NdisRegisterProtocol;

NdisIMAssociateMiniport.

NdisMInitializeWrapper – функция указывает системе NDIS, что пришло время инициировать miniport service в ее системе. Возвращаемое значение необходимо сохранить на будущее. Обязательно надо обратить внимание, что если происходит ошибка при инициализации любого объекта, то при уже нормально отработавшей функции NdisMInitializeWrapper нужно вызвать NdisTerminateWrapper для высвобождения ресурса.

NdisIMRegisterLayeredMiniport Функция, регистрирующая все функции уровня miniport

NdisRegisterProtocol Функция, регистрирующая все функции протокола

NdisIMAssociateMiniport. Функция, информирующая NDIS о том, что есть, существует два уровня, минипорт и протокол, и говорящая об экспорте функций, если таковой присутствует.

Теперь, для полной расшифровки этой шарады, посмотрим, как работает система драйвера, уже на уровне кода.

После того, как драйвер зарегистрирует функции группы miniport, он должен зарегистрировать полученный HANDLE, для правильной un-регистрации этих функций в момент окончания работы драйвера. Это делает функция NdisMRegisterUnloadHandler. Затем мы регистрируем протокольную группу и связываем эти две группы, сообщая NDIS об их существовании – функцией NdisIMAssociateMiniport.

На этом в нашем примере функция DriverEntry закачивается. Стоит немного пояснить еще несколько моментов работы инициализации. При работе на уровне kernel не стоит использовать функции: malloc, realloc, memset и т. д. Для этого существуют NdisZeroMemory, NdisAllocateMemory и др. Они более конкретно работают, и предназначены именно для использования в драйверах связанных с NDIS.

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

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

extern NTSTATUS FilterOpen(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterIoControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

Внесите их в файл passthru.h.

Теперь внесем изменения в нашу функцию DriverEntry.

Объявим массив:

PDRIVER_DISPATCH MajorFunctions [IRP_MJ_MAXIMUM_FUNCTION + 1];

Этот массив будет содержать все указатели функций для регистрации.

И добавим еще несколько переменных:

NDIS_STRING ntDeviceName; //имя для вызова виртуального device-а для NT

NDIS_STRING win32DeviceName; // тоже для win32

PDEVICE_OBJECT deviceObject; // и объект.

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

// Name of control deviceObject.

// DeviceName that names the device object.

NdisInitUnicodeString(&ntDeviceName, L"\\Device\\passthru" );

// SymbolicName that is the Win32-visible name of the device

NdisInitUnicodeString(&win32DeviceName, L"\\DosDevices\\passthru" );

//Создаем строку имени

NdisZeroMemory(MajorFunctions, sizeof(MajorFunctions));

// Регистрируем ее

//Связываем имена функций с массивом

MajorFunctions[IRP_MJ_CREATE] = FilterOpen;

MajorFunctions[IRP_MJ_CLOSE] = FilterClose;

MajorFunctions[IRP_MJ_READ] = FilterRead;

MajorFunctions[IRP_MJ_WRITE] = FilterWrite;

MajorFunctions[IRP_MJ_DEVICE_CONTROL] = FilterIoControl;

//Регистрируем их

Status = NdisMRegisterDevice(WrapperHandle, &ntDeviceName, &win32DeviceName, MajorFunctions, &deviceObject, &GlobalData.NdisDeviceHandle );

// проверяем статус

if (Status != NDIS_STATUS_SUCCESS ) {

if (GlobalData.ProtHandle) NdisDeregisterProtocol(&Status, GlobalData.ProtHandle);

if (GlobalData.NdisDeviceHandle) NdisMDeregisterDevice(GlobalData.NdisDeviceHandle);

if (WrapperHandle) NdisTerminateWrapper(WrapperHandle, NULL);

return (Status);

}

// set access method into deviceObject ( received from NdisMRegisterDevice() )

// объявление буферизации для связывающих операций

deviceObject->Flags |= DO_BUFFERED_IO;

// все.

Тела функций, типа Filter, объявите сразу после тела функции DriverEntry и оставьте пока пустыми, добавив, только возвращение значения

return NDIS_STATUS_SUCCESS;

Как писать драйвера (часть 4)

В прошлый раз мы заготовили списки необходимых функций, зарегистрировали их, а сегодня рассмотрим их поподробнее.

Группа минипорт.

Функции этой группы занимаются обработкой потока данных и событий, происходящих в верхнем уровне драйвера, и вызываемых обращением к NDIS TCP/IP стека.

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

Список функций минипорт:

MPInitialize – инициализация группы.

MPSend

MPSendPackets

MPTransferData

MPReturnPacket

Функции отвечающие за пересылку пакетов данных.

MPQueryInformation

MPSetInformation

MPQueryPNPCapbilities

MPIsSendOID

MPProcessSetPowerOid

Функции работы с питанием состоянием системы и системой PlagNPlay. Сказать особенно нечего. Стандартное отслеживание внутренних событий системы прописанное Microsoft.

MPHalt – отработка выгрузки и де регистрации драйвера при аварийном.

MPReset – как написано у Microsoft – мы не должны ничего делать :)

Работа с системой – необходимость отрабатывать события важные для сервиса корректно.

MPSetMiniportSecondary

MPPromoteSecondary

MPBundleSearchAndSetSecondary

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

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

MPSend

Основная функция вызываемая всегда, при прохождении данных. По правилам работы с данными в NDIS необходимо написать (что в примере и сделано) re-wrap пакету.

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

PADAPT pAdapt = (PADAPT)MiniportAdapterContext;

Контекст адаптера приходящий в качестве параметра. Присвоим его своему типизированному указателю.

NDIS_STATUS Status;

Возвращаемый статус.

PNDIS_PACKET MyPacket;

Наш пакет – пока только указатель.

PRSVD Rsvd;

Резервный указатель.

PVOID MediaSpecificInfo = NULL;

Тип адаптера с которым будем работать.

ULONG MediaSpecificInfoSize = 0;

Размер типа адаптера.

ASSERT (pAdapt->pSecondaryAdapt);

pAdapt = pAdapt->pSecondaryAdapt;

Проверка наличия второго сетевого адаптера. Вверху я говорил, что его наличие необходимо предусматривать.

if (IsIMDeviceStateOn (pAdapt) == FALSE) {

return NDIS_STATUS_FAILURE;

}

Проверка наличия и состояния.

NdisAllocatePacket(&Status, &MyPacket, pAdapt->SendPacketPoolHandle);

Выделение места под размер полученного пакета (Pool) данных.

if (Status == NDIS_STATUS_SUCCESS) {

PNDIS_PACKET_EXTENSION Old, New;

Rsvd = (PRSVD)(MyPacket->ProtocolReserved);

Rsvd->OriginalPkt = Packet;

MyPacket->Private.Flags = Flags;

MyPacket->Private.Head = Packet->Private.Head;

MyPacket->Private.Tail = Packet->Private.Tail;

Собственно копирование всей служебной информации

NdisSetPacketFlags(MyPacket, NDIS_FLAGS_DONT_LOOPBACK);

Установка ее в наш внутренний буфер.

NdisMoveMemory(NDIS_OOB_DATA_FROM_PACKET(MyPacket), NDIS_OOB_DATA_FROM_PACKET(Packet), sizeof(NDIS_PACKET_OOB_DATA));

Перенос данных в сам пакет.

NdisIMCopySendPerPacketInfo(MyPacket, Packet);

Копирование служебных данных по пересылке пакета.

Копирование данных о типе адаптера, куда пересылать данные.

NDIS_GET_PACKET_MEDIA_SPECIFIC_INFO(Packet, &MediaSpecificInfo, &MediaSpecificInfoSize);

if (MediaSpecificInfo || MediaSpecificInfoSize) {

NDIS_SET_PACKET_MEDIA_SPECIFIC_INFO(MyPacket, MediaSpecificInfo, MediaSpecificInfoSize);

}

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

NdisSend(&Status, pAdapt->BindingHandle, MyPacket);

if (Status != NDIS_STATUS_PENDING) {

NdisIMCopySendCompletePerPacketInfo (Packet, MyPacket);

NdisFreePacket(MyPacket);

}

Если нет задержки на отсылке освободить пакет.

} else {

Это говорит об отсутствии пакета в системе – ничего не надо делать.

}

return(Status);

Возврат значения SUCCESS или код ошибки.

Стоит остановиться еще на одном моменте. Когда система ответила, что посылка данных закончена кодом задержки пакета – NDIS_STATUS_PENDING.

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

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

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

{

PADAPT pAdapt =(PADAPT)ProtocolBindingContext;

PNDIS_PACKET Pkt;

PRSVD Rsvd;

pAdapt = pAdapt->pPrimaryAdapt;

Rsvd =(PRSVD)(Packet->ProtocolReserved);

Pkt = Rsvd->OriginalPkt;

NdisIMCopySendCompletePerPacketInfo(Pkt, Packet);

NdisDprFreePacket(Packet);

NdisMSendComplete(pAdapt->MiniportHandle, Pkt, Status);

Группа протокол.

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

PtOpenAdapterComplete

PtCloseAdapterComplete

PtBindAdapter

PtUnbindAdapter

Функции работы с адаптером – в нашем случае адаптером является драйвер модема или сетевой карты. Адаптер соответственно при запуске надо открыть и при окончании работы – закрыть. При обращении к нему привязать (bind) адаптер, захватить ресурс. В конце работы – освободить.

PtResetComplete

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

PtRequestComplete

Функция вызываемая из PtPnPNetEventSetPower.

PtStatus

Функция отвечающая за проверку статуса нижестоящего адаптера, вернее статуса взаимодействия с ним нашего уровня.

PtStatusComplete

Завершение в случае невозможности быстрого ответа, примерно как и в случае MPSend.

PtSendComplete

Описана в секции минипорт

PtTransferDataComplete

PtReceive

PtReceiveComplete

PtReceivePacket

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

PtUnload

Функция заведующая выгрузкой драйвера при выходе.

PtPNPHandler

PtPnPNetEventReconfigure

PtPnPNetEventSetPower

Работа с PnP.

Вновь обратим внимание на симметричную функцию PtReceive.

PADAPT pAdapt =(PADAPT)ProtocolBindingContext;

Контекст адаптера

PNDIS_PACKET MyPacket, Packet; Пакеты.

NDIS_STATUS Status = NDIS_STATUS_SUCCESS;

Статус

if (!pAdapt->MiniportHandle) {

Status = NDIS_STATUS_FAILURE;

} else do {

Эта часть работает при наличии второго адаптера :)

if (pAdapt->isSecondary) {

DBGPRINT("PASSTHRU GETTING RECIEVES ON SECONDARY\n");

ASSERT(0);

}

Забираем указатель на пакет в NDIS.

Packet = NdisGetReceivedPacket(pAdapt->BindingHandle, MacReceiveContext);

Если пакета нет то мы выходим иначе продолжаем как и в случае с отправкой.

if (Packet != NULL) {

Резервируем пакет для себя.

NdisDprAllocatePacket(&Status, &MyPacket, pAdapt->RecvPacketPoolHandle);

if (Status == NDIS_STATUS_SUCCESS) {

Копируем данные, как служебные, так и сами данные передаваемые наверх.

MyPacket->Private.Head = Packet->Private.Head;

MyPacket->Private.Tail = Packet->Private.Tail;

NDIS_SET_ORIGINAL_PACKET(MyPacket, NDIS_GET_ORIGINAL_PACKET(Packet));

NDIS_SET_PACKET_HEADER_SIZE(MyPacket, HeaderBufferSize);

NdisGetPacketFlags(MyPacket) = NdisGetPacketFlags(Packet);

NDIS_SET_PACKET_STATUS(MyPacket, NDIS_STATUS_RESOURCES);

В этом случае мы не посылаем пакет как при отправке, а просто указываем NDIS что MyPacket готов к передаче наверх.

NdisMIndicateReceivePacket(pAdapt->MiniportHandle, &MyPacket, 1);

ASSERT(NDIS_GET_PACKET_STATUS(MyPacket) == NDIS_STATUS_RESOURCES);

Освобождение пакета при нормальной передаче.

NdisDprFreePacket(MyPacket);

break;

}

}

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

pAdapt->IndicateRcvComplete = TRUE;

switch (pAdapt->Medium) {

case NdisMedium802_3:

NdisMEthIndicateReceive(pAdapt->MiniportHandle, MacReceiveContext, HeaderBuffer, HeaderBufferSize, LookAheadBuffer, LookAheadBufferSize, PacketSize);

break;

case NdisMedium802_5:

NdisMTrIndicateReceive(pAdapt->MiniportHandle, MacReceiveContext, HeaderBuffer, HeaderBufferSize, LookAheadBuffer, LookAheadBufferSize, PacketSize);

break;

case NdisMediumFddi:

NdisMFddiIndicateReceive(pAdapt->MiniportHandle, MacReceiveContext, HeaderBuffer, HeaderBufferSize, LookAheadBuffer, LookAheadBufferSize, PacketSize);

break;

default:

Это в случае если тип сети неизвестен.

ASSERT(0);

break;

}

} while(FALSE);

return Status;

Выход с статусом завершения.

Некоторое пояснение.

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

В случае с операцией Send этого не происходит, так как NDIS сама по контексту определяет к какому адаптеру предназначен пакет.

Оставшиеся функции аналогичны по назначению с группой функций минипорта.

Как писать драйвера (часть 5)

Итак, мы возвращаемся к драйверам.

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

Сегодня мы поговорим о коммуникации программы с драйвером.

В одной из предыдущих статей описаны были функции типа Filter:

Вот они:

extern NTSTATUS FilterOpen(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

extern NTSTATUS FilterIoControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);

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

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

Для этого используются вышеназванные функции.

FilterOpen вызывается когда в программе вызвано обращение к драйверу с помощью функции CreateFile

FilterClose – CloseHandle()

FilterRead/FilterWrite – ReadFile/WriteFile

FilterIoControl – DeviceIoControl() соответственно.

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

NTSTATUS FilterOpen(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

// инициализация любых управляющих параметров, например структуры управляющих данных

Irp->IoStatus.Status = NDIS_STATUS_SUCCESS; // Возращаемое значение – ошибка или нормальное. При нормальном завершении – будет передан HANDLE на устройство, в нашем случае на драйвер.

Irp->IoStatus.Information = 0; // Количество переданных вверх байт – в нашем случае 0 потому как нет передаваемых параметров.

IoCompleteRequest(Irp, IO_NO_INCREMENT); // Завершение работы.

return NDIS_STATUS_SUCCESS; // Нормальный код возврата.

}

Эта форма будет использоваться в том или ином виде в каждой из этих функций.

Точно так же выглядит и функция закрытия:

NTSTATUS FilterClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

// Код окончания работы с драйвером.

Irp->IoStatus.Status = NDIS_STATUS_SUCCESS;

Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return NDIS_STATUS_SUCCESS;

}

В драйвере можно создать например просто элемент для нашего примера – например, описать глобальную переменную DWORD Port; Ее будем испрользовать для задания номера порта для перехвата.

Определим Default значение для порта равное 80 (стандартный порт http протокола) и проведем его инициализацию в функции Open и де инициализацию в Close. Так мы сможем контролировать старт активной фазы работы драйвера и ее окончание.

Функции Write & Read можно использовать для передачи любого количества информации в драйвер и обратно.

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

В нашем примере передавать в драйвер и назад нечего поэтому напишем Write/Read функции в виде готовых болванок, но так как наша цель построить работающий пример – то передадим абстрактную последовательность вверх в аппликацию.

Для этого создайте в этом же месте глобальную переменную вот такого вида:

unsigned char TestStr[10] = "abcdefghi";

NTSTATUS FilterRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

NDIS_STATUS Status = NDIS_STATUS_SUCCESS;

ULONG BytesReturned = 0;

////////////////////////////////////////////////////////////////////////

// Get input parameters

PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation( Irp ); // Содержит стек Irp

ULONG BytesRequested = IrpStack->Parameters.Read.Length; // Длина принятых данных –равна параметру максимально запрошенной длины в функции ReadFile

if (BytesRequested <10) // Если запрошено меньше нашей тестовой длины – вернуть ошибку

{

BytesReturned = 0;

Status = STATUS_INVALID_BUFFER_SIZE;

} else {

// Если все в порядке – копировать буфер в выходной буфер стека.

NdisMoveMemory(Irp->AssociatedIrp.SystemBuffer, TestStr, 10);

BytesReturned = 10;

Status = NDIS_STATUS_SUCCESS;

}

// Отправить

Irp->IoStatus.Status = Status;

Irp->IoStatus.Information = BytesReturned;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return Status;

}

///////////////////////////////////////////////////////////////////////////////////////////////

NTSTATUS FilterWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

NDIS_STATUS Status = NDIS_STATUS_SUCCESS;

ULONG BytesReturned = 0;

// Здесь все работает аналогично.

Irp->IoStatus.Status = Status;

Irp->IoStatus.Information = BytesReturned;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return Status;

}

Функции симметричны.

Теперь мы подошли к функции управления.

Она сама работает точно также как и все функции перечисленные выше – с одной разницей: В ней принимаемым параметром является код операции, который надо установить в драйвер.

Можно конечно придумать свой формат данных – передаваемых в WriteFile, который расшифровывать внутри драйвера и так решать, что делать или не делать, однако стоит использовать уже готовый механизм, предоставленный Microsoft-ом.

Описание кода комманды выглядит так:

#define IOCTL_SET_COMMAND1 // наш код управления

CTL_CODE ( FILE_DEVICE_UNKNOWN, // Тип кода

0xF07, // Цифровое значение

METHOD_BUFFERED, // Метод операции

FILE_ANY_ACCESS ) // Права доступа.

Итак мы описали код управления, вызов которого будет выглядеть в программе так:

res = DeviceIoControl(

hFile, // Handle драйвера из функции CreateFile

(DWORD) IOCTL_SET_COMMAND1, // Комманда

(LPVOID)0, (DWORD)0, (LPVOID)NULL, (DWORD)0, (LPDWORD)&bytesReturned, NULL

);

Вызов такой функции приведет к обращению драйвером к функции FilterIoControl!

NTSTATUS FilterIoControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {

NDIS_STATUS Status = NDIS_STATUS_SUCCESS;

ULONG BytesReturned = 0;

PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);

ULONG IoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;

PVOID InfoBuffer = Irp->AssociatedIrp.SystemBuffer;

ULONG InputBufferLen = IrpStack->Parameters.DeviceIoControl.InputBufferLength;

ULONG OutputBufferLen = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;

switch(IoControlCode) {

case IOCTL_SET_COMMAND1:

// Здесь мы можем сменить наш номер порта с 80 на, к примеру, 25.

break;

}

Irp->IoStatus.Status = Status;

Irp->IoStatus.Information = BytesReturned;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return Status;

}

Описания Input/Output буферов привожу для того, чтобы при необходимости получения и еще каких либо сопутствующих параметров, было ясно, где их получать, скажем, в драйвере нашего примера, команда 1 может нести в качестве параметра новый номер порта для перехвата.

Давайте теперь опишем логику управления драйвером перехватчиком.

Для перехвата определяются в начале параметры перехвата, адрес, порт и т.д.

Далее вносится тип рабочего состояния – перехват – прозрачный режим.

Вносится список возможных портов к перехвату (по необходимости).

В процессе работы системы, пока управляющая аппликация не запущена, то драйверу необходимо указать стартовые параметры.

Например, тип режима – прозрачный, в этом случае не перехватывается ничего.

Когда стартует управляющая программа – то она открывает драйвер CreateFile() и запускает, если это необходимо, другие стартовые условия, например перевод в режим перехвата и номер порта для этого.

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

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

На сегодня пока все.

Content

Как писать драйвера

Как писать драйвера (часть 1)

Предисловие.

Часть первая: "Что нужно для компиляции простейшего драйвера?"

Первая тестовая компиляция.

Как писать драйвера (часть 2)

Как писать драйвера (часть 3)

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

Как писать драйвера (часть 4)

Группа минипорт.

Группа протокол.

Как писать драйвера (часть 5)