[Previous] [Next]

Процедура DriverEntry и предварительные объявления

Все приведенные ниже отрывки кода следует последовательно поместить в один файл (обычно, файл, содержащий описание DriverEntry, разработчики называют Init.c). Редактирование, разумеется, удобнее всего выполнять в каком-нибудь редакторе интегрированной среды. Рекомендуется использовать редактор из среды Visual Studio, поскольку в нем производится динамический контроль синтаксиса и типов данных языка С. В главе 2 приводится содержимое файлов настройки проекта для драйвера Example, соблюдение которых позволит воспользоваться динамическими подсказками среды во время редактирования и позволит также выполнять контрольную компиляцию кода. Последнее весьма удобно, поскольку в интегрированной среде легко перейти к месту возникновения ошибки по диагностическому сообщению.

Окончательную компиляцию драйвера (как чистовую, так и отладочную) категорически рекомендуется выполнять утилитой Build из среды DDK, поскольку иные способы компиляции могут быть источником необъяснимых странностей в поведении драйвера.

/////////////////////////////////////////////////////////////////////
// init.cpp: Инициализация драйвера
// Замечание. Рабочая версия данного драйвера должна быть
// скомпилирована как не-WDM версия. В противном случае - драйвер
// не сможет корректно загружаться и выгружаться с использованием
// программы monitor (пакет Numega Driver Studio) и сервисов SCM
// Менеджера.

/////////////////////////////////////////////////////////////////////
// DriverEntry	          Главная точка входа в драйвер
// UnloadRoutine          Процедура выгрузки драйвера
// DeviceControlRoutine   Обработчик DeviceIoControl IRP пакетов
/////////////////////////////////////////////////////////////////////
#include "Driver.h"

// Предварительные объявления функций:
NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp );
VOID     UnloadRoutine(IN PDRIVER_OBJECT DriverObject);
NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp );
NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);
NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo, IN PIRP Irp);

// Хотя и нехорошо делать глобальные переменные в драйвере...
KSPIN_LOCK MySpinLock;
#pragma code_seg("INIT") // начало секции INIT
/////////////////////////////////////////////////////////////////////
// (Файл init.cpp)
// DriverEntry - инициализация драйвера и необходимых объектов
// Аргументы:  указатель на объект драйвера
//             раздел реестра (driver service key) в UNICODE
// Возвращает: STATUS_Xxx

extern "C"
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject,
                      IN PUNICODE_STRING RegistryPath  )
{
	NTSTATUS status = STATUS_SUCCESS;
	PDEVICE_OBJECT  fdo;
	UNICODE_STRING  devName;

	#if DBG
	DbgPrint("=Example= In DriverEntry.");
	DbgPrint("=Example= RegistryPath = %ws.", RegistryPath->Buffer);
	#endif

	// Экспорт точек входа в драйвер (AddDevice объявлять не будем)
	// DriverObject->DriverExtension->AddDevice= OurAddDeviceRoutine;
	DriverObject->DriverUnload = UnloadRoutine;
	DriverObject->MajorFunction[IRP_MJ_CREATE]= Create_File_IRPprocessing;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = Close_HandleIRPprocessing;
	DriverObject->MajorFunction[IRP_MJ_READ]  = ReadWrite_IRPhandler;
	DriverObject->MajorFunction[IRP_MJ_WRITE] = ReadWrite_IRPhandler;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]= DeviceControlRoutine;
	//========================================================
	// Действия по созданию символьной ссылки
	// (их нужно было бы делать в OurAddDeviceRoutine, но у нас
	// очень простой драйвер и эта процедура отсутствует):
	RtlInitUnicodeString( &devName, L"\\Device\\EXAMPLE" );

	// Создаем наш Functional Device Object (FDO) и получаем
	// указатель на созданный FDO в нашей переменной fdo.	
	// (В WDM драйвере эту работу также следовало бы выполнять
	// в процедуре OurAddDeviceRoutine.) При создании FDO
	// будет выделено место и под структуру расширения устройства
	// EXAMPLE_DEVICE_EXTENSION (для этого мы передаем в вызов
	// ее размер, вычисляемый оператором sizeof):
	status = IoCreateDevice(DriverObject,
                            sizeof(EXAMPLE_DEVICE_EXTENSION),
                            &devName, // может быть и NULL
                            FILE_DEVICE_UNKNOWN,
                            0,
                            FALSE, // без эксклюзивного доступа
                            &fdo);
	if(!NT_SUCCESS(status)) return status;

	// Получаем указатель на область, предназначенную под
	// структуру расширение устройства
	PEXAMPLE_DEVICE_EXTENSION dx = (PEXAMPLE_DEVICE_EXTENSION)fdo->DeviceExtension;
	dx->fdo = fdo;  // Сохраняем обратный указатель

	// Применяя прием условной компиляции, вводим функцию DbgPrint,
	// сообщения которой мы сможем увидеть в окне DebugView, если
	// выполним сборку нашего драйвера как checked (отладочную)
	// версию:
	#if DBG
	DbgPrint("=Example= FDO %X, DevExt=%X.",fdo,dx);
	#endif

	//=======================================
	// Действия по созданию символьной ссылки
	// (их нужно было бы делать в OurAddDeviceRoutine, но у нас
	// очень простой драйвер):
	UNICODE_STRING symLinkName;   // Сформировать символьное имя:
	// #define   SYM_LINK_NAME   L"\\??\\Example"
	// Такого типа символьные ссылки ^^ проходят только в NT.
	// (То есть, если перенести бинарный файл драйвера в
	// Windows 98, то пользовательские приложения заведомо
	// не смогут открыть файл по такой символьной ссылке.)
	// Для того, чтобы ссылка работала в и Windows 98 и в NT,
	// необходимо поступать следующим образом:
	#define	SYM_LINK_NAME	L"\\DosDevices\\Example"
	RtlInitUnicodeString( &symLinkName, SYM_LINK_NAME );
	dx->ustrSymLinkName = symLinkName;
	
	// Создаем символьную ссылку
	status = IoCreateSymbolicLink( &symLinkName, &devName );
	if (!NT_SUCCESS(status))
	{ // при неудаче √ удалить Device Object и вернуть управление
		IoDeleteDevice( fdo );
		return status;
        } // Теперь можно вызывать CreateFile("\\\\.\\Example",...);
          // в пользовательских приложениях

        // Объект спин-блокировки, который будем использовать для
        // разнесения во времени выполнения кода обработчика
        // IOCTL запросов. Инициализируем его:
        KeInitializeSpinLock(&MySpinLock);

        // Снова используем условную компиляцию, чтобы выделить код,
        // компилируемый в отладочной версии и не компилируемый в
        // версии free (релизной):
        #if DBG
        DbgPrint("=Example= DriverEntry successfully completed.");
        #endif
        return status;
}
#pragma code_seg() // end INIT section

Функция CompleteIrp

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

//
// (Файл init.cpp)
// CompleteIrp: Устанавливает IoStatus и завершает обработку IRP
// Первый аргумент - указатель на объект нашего FDO.
//
NTSTATUS CompleteIrp( PIRP Irp, NTSTATUS status, ULONG info)
{
	Irp->IoStatus.Status = status;
	Irp->IoStatus.Information = info;
	IoCompleteRequest(Irp,IO_NO_INCREMENT);
	return status;
}

Рабочая процедура обработки запросов read/write

Процедура ReadWrite_IRPhandler предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодами IRP_MJ_READ/IRP_MJ_WRITE по результатам обращения к драйверу из пользовательских приложений с вызовами read/write или из кода режима ядра с вызовами ZwReadFile или ZwWriteFile. В данном примере наша функция обработки запросов чтения/записи ничего полезного не делает, и ее регистрация выполнена только для демонстрации, как это могло бы быть в более "развитом" драйвере.

Описание прототипов рабочих процедура драйвера (параметров их вызова) можно найти в документации DDK (2000, ХР, 2003), если в режиме указателя задать ключевые слова Dispatch..., например, DispatchRead. Если ваша программа просмотра файлов справки не поддерживает переходов между разными .chm файлами (представляющими полную документацию по DDK), то можно сразу обратиться к файлу kmarch.chm (который, собственно, и содержит информацию по рабочим процедурам). Там же можно узнать, на каком уровне IRQL происходит вызов конкретной функции.
//
// (Файл init.cpp)
// ReadWrite_IRPhandler: Берет на себя обработку запросов
// чтения/записи и завершает обработку IRP вызовом CompleteIrp
// с числом переданных/полученных байт (BytesTxd) равным нулю.
// Аргументы:
// Указатель на объект нашего FDO
// Указатель на структуру IRP, поступившего от Диспетчера ввода/вывода
NTSTATUS ReadWrite_IRPhandler( IN PDEVICE_OBJECT fdo, IN PIRP Irp )
{
	ULONG BytesTxd = 0;
	NTSTATUS status = STATUS_SUCCESS; //Завершение с кодом status
	// Задаем печать отладочных сообщений √ если сборка отладочная
	#if DBG
	DbgPrint("-Example- in ReadWrite_IRPhandler.");
	#endif
	return CompleteIrp(Irp,status,BytesTxd);
}

Рабочая процедура обработки запросов открытия драйвера

Процедура Create_File_IRPprocessing предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодами IRP_MJ_CREATE по результатам обращения к драйверу из пользовательских приложений с вызовами CreateFile или из кода режима ядра с вызовами ZwCreateFile. В нашем примере эта функция не выполняет никаких особых действий (хотя можно было бы завести счетчик открытых дескрипторов и т.п.), однако без регистрации данной процедуры система просто не позволила бы клиенту "открыть" драйвер для работы с ним (хотя сам драйвер мог бы успешно загружаться и стартовать).

//
// (Файл init.cpp)
// Create_File_IRPprocessing: Берет на себя обработку запросов с
// кодом IRP_MJ_CREATE.
// Аргументы:
// Указатель на объект нашего FDO
// Указатель на структуру IRP, поступившего от Диспетчера ВВ
//
NTSTATUS Create_File_IRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp)
{
	PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
	// Задаем печать отладочных сообщений - если сборка отладочная
	#if DBG
	DbgPrint("-Example- Create File is %ws",
		&(IrpStack->FileObject->FileName.Buffer));
	#endif
return CompleteIrp(Irp,STATUS_SUCCESS,0); // Успешное завершение
}

Рабочая процедура обработки запросов закрытия драйвера

Процедура Close_File_IRPprocessing предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодом IRP_MJ_CLOSE по результатам обращения к драйверу из пользовательских приложений с вызовами CloseHandle или из кода режима ядра с вызовами ZwClose. В нашем примере эта функция не выполняет никаких особых действий, однако, выполнив регистрацию процедуры открытия файла, мы теперь просто обязаны зарегистрировать процедуру завершения работы клиента с открытым дескриптором. Заметим, что если клиент пользовательского режима забывает закрыть полученный при открытии доступа к драйверу дескриптор, то за него эти запросы выполняет операционная система (впрочем, как и в отношении всех открытых приложениями файлов, когда приложения завершаются без явного закрытия открытых файлов).

// (Файл init.cpp)
// Close_File_IRPprocessing: Берет на себя обработку запросов с
// кодом IRP_MJ_CLOSE.
// Аргументы:
// Указатель на объект нашего FDO
// Указатель на структуру IRP, поступившего от Диспетчера ввода/вывода
NTSTATUS Close_HandleIRPprocessing(IN PDEVICE_OBJECT fdo,IN PIRP Irp)
{
	#if DBG
	// Задаем печать отладочных сообщений - если сборка отладочная
	DbgPrint("-Example- In Close handler.");
	#endif
return CompleteIrp(Irp,STATUS_SUCCESS,0);// Успешное завершение
}

Рабочая процедура обработки IOCTL запросов

Процедура DeviceControlRoutine предназначена для обработки запросов Диспетчера ввода/вывода, которые он формирует в виде IRP пакетов с кодом IRP_MJ_DEVICE_CONTROL по результатам обращения к драйверу из пользовательских приложений с вызовами DeviceIoControl.

В нашем примере это самая важная функция. Она реализует обработку пяти IOCTL запросов:

Эти IOCTL коды являются пользовательскими — они определены с помощью макроса CTL_CODE в файле Driver.h, который является частью данного проекта, и речь о котором пойдет ниже.

Определения используемых ниже непривычных для программистов Win32 типов данных (например, UCHAR или PUCHAR) можно найти в DDK в файле Windef.h .
// (Файл init.cpp)
// DeviceControlRoutine: обработчик IRP_MJ_DEVICE_CONTROL запросов
// Аргументы:
// Указатель на объект нашего FDO
// Указатель на структуру IRP, поступившего от Диспетчера ВВ
//	Возвращает:  STATUS_XXX
// #define SMALL_VERSION
// В том случае, если не закомментировать верхнюю строчку √ будет
// выполнена компиляция версии, в которой будет обрабатываться только
// один тип IOCTL запросов -- IOCTL_MAKE_SYSTEM_CRASH

NTSTATUS DeviceControlRoutine( IN PDEVICE_OBJECT fdo, IN PIRP Irp )
{
	NTSTATUS status = STATUS_SUCCESS;
	ULONG BytesTxd =0; // Число переданных/полученных байт (пока 0)
	PIO_STACK_LOCATION IrpStack=IoGetCurrentIrpStackLocation(Irp);

	// Получаем указатель на расширение устройства
	PEXAMPLE_DEVICE_EXTENSION dx =
				 (PEXAMPLE_DEVICE_EXTENSION)fdo->DeviceExtension;
	//-------------------------------
	// Выделяем из IRP собственно значение IOCTL кода, по поводу
	// которого случился вызов:
	ULONG ControlCode =
	IrpStack->Parameters.DeviceIoControl.IoControlCode;
	ULONG method = ControlCode & 0x03;

	// Получаем текущее значение уровня IRQL √ приоритета,
	// на котором выполняется поток (вообще говоря, целое число):
	KIRQL irql,
	currentIrql = KeGetCurrentIrql();

	#if DBG
	DbgPrint("-Example- In DeviceControlRoutine (fdo= %X)\n",fdo);
	DbgPrint("-Example- DeviceIoControl: IOCTL %x.", ControlCode );
	if(currentIrql==PASSIVE_LEVEL)
		DbgPrint("-Example- PASSIVE_LEVEL (val=%d)",currentIrql);
	#endif
	// Запрашиваем владение объектом спин-блокировки. В данном
	// примере не выполняется никаких критичных действий, но,
	// вообще говоря, этот прием может быть полезен и даже
	// незаменим, если в приведенном ниже коде должны будут
	// выполнены манипуляции, которые можно делать только
	// эксклюзивно. Пока потоку выделен объект спин-блокировки √
	// никакой другой поток не сможет войти в оператор switch:
	KeAcquireSpinLock(&MySpinLock,&irql);

	// Диспетчеризация по IOCTL кодам:
	switch( ControlCode) {

	#ifndef SMALL_VERSION
	case IOCTL_PRINT_DEBUG_MESS:
	{     // Только вводим сообщение и только в отладочной версии
		#if DBG
		DbgPrint("-Example- IOCTL_PRINT_DEBUG_MESS.");
		#endif
		break;
	}
	case IOCTL_CHANGE_IRQL:
	{
		#if DBG
		// Эксперименты по искусственному повышению
		// IRQL √ только в отладочной версии!
		DbgPrint("-Example- IOCTL_CHANGE_IRQL.");
		KIRQL dl = DISPATCH_LEVEL, // только для распечатки (2)
		oldIrql,
		newIrql=25; // Новый уровень IRQL (например, 25)
		// Устанавливаем newIrql, сохраняя текущий в oldIrql:
		KeRaiseIrql(newIrql,&oldIrql);
		newIrql=KeGetCurrentIrql(); // Что реально получили?

		DbgPrint("-Example- DISPATCH_LEVEL value =%d",dl);
		DbgPrint("-Example- IRQLs are old=%d new=%d",
						oldIrql,newIrql);
		KeLowerIrql(oldIrql); // Возвращаем старое значение
		#endif
		break;
	}
	#endif // SMALL_VERSION


	case IOCTL_MAKE_SYSTEM_CRASH:
	{
		int errDetected=0;
		char x = (char)0xFF;

		#if	DBG  // Вообще говоря, под NT мы этого уже не увидим:
		DbgPrint("-Example- IOCTL_MAKE_SYSTEM_CRASH.");
		#endif
		// Вызываем системный сбой обращением по нулевому адресу
		__try {
		x = *(char*)0x0L; // ошибочная ситуация
			//^^^^^^^^^^^^ здесь случится сбой NT, но не Win98
		}
		__except(EXCEPTION_EXECUTE_HANDLER)
		{   // Перехват исключения не работает!
			// Эта занимательная ситуация объяснена в 10.2.6,
			// при рассмотрении объектов спин-блокировок.
			errDetected=1;
		};
		#if DBG
		DbgPrint("-Example- Value of x is %X.",x);
		if(errDetected)
			DbgPrint("-Example- Except detected in Example driver.");
		#endif
		break;
	}

	#ifndef SMALL_VERSION
	case IOCTL_TOUCH_PORT_378H:
	{	
		unsigned short ECRegister = 0x378+0x402;
		#if DBG
		DbgPrint("-Example- IOCTL_TOUCH_PORT_378H.");
		#endif
		// Пробуем программно перевести параллельный порт 378,
		// сконфигурированный средствами BIOS как ECP+EPP, в
		// режим EPP.
		_asm  {
			mov dx,ECRegister ;
			xor al,al    ;
			out dx,al    ;    Установить EPP mode 000
			mov al,095h  ;    Биты 7:5 = 100
			out dx,al    ;    Установить EPP mode 100
		}
		// Подобные действия в приложении пользовательского
		// режима под NT  обязательно привело бы к аварийной
		// выгрузке приложения с сообщением об ошибке!
		// Практически эти пять строк демонстрируют, что можно
		// работать с LPT портом под Windows NT !
		break;
	}

	case IOCTL_SEND_BYTE_TO_USER:
	{	
		// Размер данных, поступивших от пользователя:
		ULONG InputLength = //только лишь для примера
			IrpStack->Parameters.DeviceIoControl.InputBufferLength;
		// Размер буфера для данных, ожидаемых пользователем
		ULONG OutputLength =
		IrpStack->Parameters.DeviceIoControl.OutputBufferLength;
		#if DBG
		DbgPrint("-Example- Buffer outlength %d",OutputLength);
		#endif

		if( OutputLength<1 )
		{// Если не предоставлен буфер √ завершить IRP с ошибкой
			status = STATUS_INVALID_PARAMETER;
			break;
		}
		UCHAR *buff; // unsigned char, привыкаем к новой нотации
		if(method==METHOD_BUFFERED)
		{
			buff = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;
			#if DBG
			DbgPrint("-Example- Method : BUFFERED.");
			#endif
		}
		else
			if (method==METHOD_NEITHER)
			{
				buff=(unsigned char*)Irp->UserBuffer;
				#if DBG
				DbgPrint("-Example- Method : NEITHER.");
				#endif
			}
			else
			{
				#if DBG
				DbgPrint("-Example- Method : unsupported.");
				#endif
				status = STATUS_INVALID_DEVICE_REQUEST;
				break;
			}
		#if DBG
		DbgPrint("-Example- Buffer address is %08X",buff);
		#endif
		*buff=33;     // Любимое число Штирлица
		BytesTxd = 1; // Передали 1 байт
		break;
	}
	#endif // SMALL_VERSION
	// Ошибочный запрос (код IOCTL, который не обрабатывается):
	default: status = STATUS_INVALID_DEVICE_REQUEST;
	}
	// Освобождение спин-блокировки
	KeReleaseSpinLock(&MySpinLock,irql);

	#if DBG
	DbgPrint("-Example- DeviceIoControl: %d bytes written.", (int)BytesTxd);
	#endif

return CompleteIrp(Irp,status,BytesTxd); // Завершение IRP
}

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

Рабочая процедура выгрузки драйвера

Процедура UnloadRoutine выполняет завершающую работу перед тем как драйвер растворится в небытии.

При следовании WDM модели, драйвер должен был бы зарегистрировать обработчик PnP запросов (то есть IRP_MJ_PNP) и перед вызовом UnloadRoutine получал бы IRP пакеты с кодом IRP_MJ_PNP и суб-кодом IRP_MN_STOP_DEVICE (например, когда пользователь решил отключить устройство, воспользовавшись окном Диспетчера Устройств в Настройках системы). В этом обработчике и следует выполнять действия, предшествующие удалению WDM драйвера.
//
// (Файл init.cpp)
// UnloadRoutine: Выгружает драйвер, освобождая оставшиеся объекты
// Вызывается системой, когда необходимо выгрузить драйвер.
// Как и процедура AddDevice, регистрируется иначе чем
// все остальные рабочие процедуры и не получает никаких IRP.
// Arguments:  указатель на объект драйвера
//

#pragma code_seg("PAGE")
// Допускает размещение в странично организованной памяти
//
VOID UnloadRoutine(IN PDRIVER_OBJECT pDriverObject)
{
	PDEVICE_OBJECT	pNextDevObj;
	int i;

	// Задаем печать отладочных сообщений √ если сборка отладочная
	#if DBG
	DbgPrint("-Example- In Unload Routine.");
	#endif
	//==========================================================
	// Нижеприведенные  операции в полномасштабном WDM драйвере
	// следовало бы поместить в обработчике IRP_MJ_PNP запросов
	// с субкодом IRP_MN_REMOVE_DEVICE, но в силу простоты
	// драйвера, сделаем это здесь.
	// Проходим по всем объектам устройств, контролируемым
	// драйвером
	pNextDevObj = pDriverObject->DeviceObject;

	for(i=0; pNextDevObj!=NULL; i++)
	{
		PEXAMPLE_DEVICE_EXTENSION dx =
				(PEXAMPLE_DEVICE_EXTENSION)pNextDevObj->DeviceExtension;
		// Удаляем символьную ссылку и уничтожаем FDO:
		UNICODE_STRING *pLinkName = & (dx->ustrSymLinkName);
		// !!! сохраняем указатель:
		pNextDevObj = pNextDevObj->NextDevice;

		#if DBG
		DbgPrint("-Example- Deleted device (%d) : pointer to FDO = %X.",
							i,dx->fdo);
		DbgPrint("-Example- Deleted symlink = %ws.", pLinkName->Buffer);
		#endif

		IoDeleteSymbolicLink(pLinkName);
		IoDeleteDevice( dx->fdo);
	}
}
#pragma code_seg() // end PAGE section

Заголовочный файл Driver.h

Ниже приводится полный текст файла Driver.h, содержащий объявления, необходимые для компиляции драйвера Example.sys.

#ifndef _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_
#define _DRIVER_H_04802_BASHBD_1UIWQ1_8239_1NJKDH832_901_
// Выше приведены две строки (в конце файла имеется еще #endif),
// которые в больших проектах запрещают повторные  проходы по тексту,
// который находится внутри h-файла (что весьма удобно для повышения
// скорости компиляции).
// (Файл Driver.h)

#ifdef __cplusplus
extern "C"
{
#endif

#include "ntddk.h"

//#include "wdm.h"
// ^^^^^^^^^^^^^^ если выбрать эту строку и закомментировать
// предыдущую, то компиляция  в среде DDK (при помощи утилиты Build)
// также пройдет успешно, однако драйвер Example не станет от этого
// настоящим WDM драйвером.

#ifdef __cplusplus
}
#endif
// Определяем структуру расширения устройства. Включим в нее
// указатель на FDO (для удобства последующей работы UnloadRoutine) и
// имя символьной ссылки в формате UNOCODE_STRING.

typedef struct _EXAMPLE_DEVICE_EXTENSION
{
	PDEVICE_OBJECT	fdo;
	UNICODE_STRING	ustrSymLinkName; // L"\\DosDevices\\Example"
} EXAMPLE_DEVICE_EXTENSION, *PEXAMPLE_DEVICE_EXTENSION;

// Определяем собственные коды IOCTL, с которыми можно будет
// обращаться к драйверу при помощи вызова DeviceIoControl.
// Определение макроса CTL_CODE можно найти в файле DDK Winioctl.h.
// Там же можно найти и численные значения, скрывающиеся под именами
// METHOD_BUFFERED и METHOD_NEITHER.

// Внимание! Текст приведенный ниже должен войти в файл Ioctl.h,
// который будет необходим для компиляции тестового приложения.
// (Разумеется, за исключением последней строки с "#endif".)

#define IOCTL_PRINT_DEBUG_MESS CTL_CODE( \
	FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_CHANGE_IRQL CTL_CODE(\
	FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_MAKE_SYSTEM_CRASH CTL_CODE( \
	FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_TOUCH_PORT_378H CTL_CODE( \
	FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)

#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \
	FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)

// Вариант :
//#define IOCTL_SEND_BYTE_TO_USER CTL_CODE( \
//    FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#endif

Третий параметр CTL_CODE называется Function и при составлении собственных (пользовательских) IOCTL кодов его значение не должно быть менее 0x800. Пересечение пользовательских IOCTL кодов со значениями IOCTL кодов других драйверов не имеет никакого значения, поскольку они действуют только в пределах конкретного драйвера.