[Previous] [Next]

Приложение для тестирования драйвера Example.sys

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

////////////////////////////////////////////////////////////////////
// (Файл ExampleTest.cpp)
// Консольное приложение для тестирования драйвера Example.sys
//	22-Feb-2003  1.0.0 SVP
////////////////////////////////////////////////////////////////////

// Заголовочные файлы, которые необходимы в данном приложении:
#include 
#include 
#include 
#include 

// Внимание! Файл Ioctl.h должен быть получен из файла Driver.h
// (см. комментрарии к Driver.h) и размещен в одной директории с
// данным файлом (TestExam.cpp).
#include "Ioctl.h"

// Имя объекта драйвера и местоположение загружаемого файла
#define DRIVERNAME    _T("Example")
//#define DRIVERBINARY  _T("C:\\Example\\Example.sys")
//#define DRIVERBINARY  _T("C:\\Ex\\objchk_w2k\\i386\\Example.sys")
#define DRIVERBINARY  _T("C:\\Ex\\tester\\Example.sys")

// Функция установки драйвера на основе SCM вызовов
BOOL InstallDriver( SC_HANDLE  scm, LPCTSTR DriverName, LPCTSTR driverExec )
{
	SC_HANDLE Service =
			CreateService ( scm,    // открытый дескриптор к SCManager
					DriverName,      // имя сервиса - Example
					DriverName,      // для вывода на экран
					SERVICE_ALL_ACCESS,    // желаемый доступ
					SERVICE_KERNEL_DRIVER, // тип сервиса
					SERVICE_DEMAND_START,  // тип запуска
					SERVICE_ERROR_NORMAL,  // как обрабатывается ошибка
					driverExec,            // путь к бинарному файлу
					// Остальные параметры не используются - укажем NULL
					NULL,    // Не определяем группу загрузки
					NULL, NULL, NULL, NULL);
	if (Service == NULL) // неудача
	{
		DWORD err = GetLastError();
		if (err == ERROR_SERVICE_EXISTS) {/* уже установлен */}
		// более серьезная ощибка:
		else  printf ("ERR: CanТt create service. Err=%d\n",err);
		// (^^ Ётот код ошибки можно подставить в ErrLook):
		return FALSE;
	}
	CloseServiceHandle (Service);
return TRUE;
}

// Функция удаления драйвера на основе SCM вызовов
BOOL RemoveDriver(SC_HANDLE scm, LPCTSTR DriverName)
{
	SC_HANDLE Service =
			OpenService (scm, DriverName, SERVICE_ALL_ACCESS);
	if (Service == NULL) return FALSE;
	BOOL ret = DeleteService (Service);
	if (!ret) { /* неудача при удалении драйвера */ }

	CloseServiceHandle (Service);
return ret;
}

// Функция запуска драйвера на основе SCM вызовов
BOOL StartDriver(SC_HANDLE  scm, LPCTSTR DriverName)
{
	SC_HANDLE Service =
			OpenService(scm, DriverName, SERVICE_ALL_ACCESS);
	if (Service == NULL) return FALSE; /* open failed */
	BOOL ret =
			StartService( Service, // дескриптор
						  0,       // число аргументов
						  NULL  ); // указатель  на аргументы
	if (!ret) // неудача
	{
		DWORD err = GetLastError();
		if (err == ERROR_SERVICE_ALREADY_RUNNING)
			ret = TRUE; // OK, драйвер уже работает!
		else { /* другие проблемы */}
	}

	CloseServiceHandle (Service);
return ret;
}
// Функция останова драйвера на основе SCM вызовов
BOOL StopDriver(SC_HANDLE  scm, LPCTSTR DriverName)
{
	SC_HANDLE Service =
			OpenService (scm, DriverName, SERVICE_ALL_ACCESS );
	if (Service == NULL)  // Невозможно выполнить останов драйвера
	{
		DWORD err = GetLastError();
		return FALSE;
	}
	SERVICE_STATUS serviceStatus;
	BOOL ret =
	ControlService(Service, SERVICE_CONTROL_STOP, &serviceStatus);
	if (!ret)
	{
		DWORD err = GetLastError();
		// дополнительная диагностика
	}

	CloseServiceHandle (Service);
return ret;
}


// Соберем вместе действия по установке, запуску, останову
// и удалению драйвера (для обобщения сведений).
// (Однако пользоваться этой функцией в данном примере не придется.)
/* Закомментируем ее.
void Test_SCM_Installation(void)
{
	SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
	if(scm == NULL) // неудача
	{
		// Получаем код ошибки и ее текстовый эквивалент
		unsigned long err = GetLastError();
		PrintErrorMessage(err); // см. п. 2.1.5
		return;
	}
	BOOL res;
	res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY );
	// Ошибка может оказаться не фатальной. Продолжаем:
	res = StartDriver  (scm, DRIVERNAME );
	if(res)
	{
		//Е Здесь следует разместить функции работы с драйвером
		.. .. ..
		res = StopDriver   (scm, DRIVERNAME );
		if(res) res = RemoveDriver (scm, DRIVERNAME );
	}
CloseServiceHandle(scm);
return;
}
*/

#define SCM_SERVICE
// ^^^^^^^^^^^^^^^^ вводим элемент условной компиляции, при помощи
// которого можно отключать использование SCM установки драйвера
// в тексте данного приложения. (Здесь Ц использование SCM включено.)

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

int __cdecl main(int argc, char* argv[])
{
	#ifdef SCM_SERVICE
	// Используем сервис SCM для запуска драйвера.
	BOOL res; // Получаем доступ к SCM :
	SC_HANDLE scm = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
	if(scm == NULL) return -1; // неудача

	// Делаем попытку установки драйвера
	res = InstallDriver(scm, DRIVERNAME, DRIVERBINARY );
	if(!res) // Неудача, но возможно, он уже инсталлирован
		printf("Cannot install service");

	res = StartDriver  (scm, DRIVERNAME );
	if(!res)
	{
		printf("Cannot start driver!");
		res = RemoveDriver (scm, DRIVERNAME );
		if(!res)
		{
			printf("Cannot remove driver!");
		}
		CloseServiceHandle(scm); // Отключаемся от SCM
		return -1;
	}
	#endif

	HANDLE hHandle =           // Получаем доступ к драйверу
			CreateFile( "\\\\.\\Example",
						GENERIC_READ | GENERIC_WRITE,
						FILE_SHARE_READ | FILE_SHARE_WRITE,
						NULL,
						OPEN_EXISTING,
						FILE_ATTRIBUTE_NORMAL,
						NULL );
	if(hHandle==INVALID_HANDLE_VALUE)
	{
		printf("ERR: can not access driver Example.sys !\n");
		return (-1);
	}
	DWORD BytesReturned;    // Переменная для хранения числа
							// переданных байт
	// Последовательно выполняем обращения к драйверу
	// с различными кодами IOCTL:

	unsigned long ioctlCode=IOCTL_PRINT_DEBUG_MESS;
	if( !DeviceIoControl(   hHandle,
					ioctlCode,
					NULL, 0,	// Input
					NULL, 0,	// Output
					&BytesReturned,
					NULL )  )
	{
		printf( "Error in IOCTL_PRINT_DEBUG_MESS!" );
		return(-1);
	}

	ioctlCode=IOCTL_CHANGE_IRQL;
	if( !DeviceIoControl(   hHandle,
					ioctlCode,
					NULL, 0,	// Input
					NULL, 0,	// Output
					&BytesReturned,
					NULL )  )
	{
		printf( "Error in IOCTL_CHANGE_IRQL!" );
		return(-1);
	}

	ioctlCode=IOCTL_TOUCH_PORT_378H;
	if( !DeviceIoControl(   hHandle,
					ioctlCode,
					NULL, 0,  // Input
					NULL, 0,  // Output
					&BytesReturned,
					NULL )  )
	{
		printf( "Error in IOCTL_TOUCH_PORT_378H!" );
		return(-1);
	}

	// Следующий тест. Получаем 1 байт данных из драйвера.
	// По окончании данного вызова переменная xdata должна
	// содержать значение 33:
	unsigned char xdata = 0x88;
	ioctlCode=IOCTL_SEND_BYTE_TO_USER;
	if( !DeviceIoControl(   hHandle,
					ioctlCode,
					NULL, 0,  // Input
					&xdata, sizeof(xdata),// Output
					&BytesReturned,
					NULL )  )
	{
		printf( "Error in IOCTL_SEND_BYTE_TO_USER!" );
		return(-1);
	}

	// Вывод диагностического сообщения в консольном окне:
	printf("IOCTL_SEND_BYTE_TO_USER: BytesReturned=%d xdata=%d",
		            BytesReturned, xdata);

	// Выполнение следующего теста в Windows NT приведет к
	// фатальному сбою операционной системы (намеренно выполненное
	// падение ОС может быть полезно при изучении, например,
	// организации crash dump файла и работы с отладчиком).
	/*
	ioctlCode=IOCTL_MAKE_SYSTEM_CRASH;
	if( !DeviceIoControl(   hHandle,
					ioctlCode,
					NULL, 0,		        // Input
					NULL, 0,	// Output
					&BytesReturned,
					NULL )  )
	{
		printf( "Error in IOCTL_MAKE_SYSTEM_CRASH!" );
		return(-1);
	}
	*/
	// Закрываем дескриптор доступа к драйверу:
	CloseHandle(hHandle);


	#ifdef SCM_SERVICE
	// Останавливаем и удаляем драйвер. Отключаемся от SCM.
	res = StopDriver   (scm, DRIVERNAME );
	if(!res)
	{
		printf("Cannot stop driver!");
		CloseServiceHandle(scm);
		return -1;

	}

	res = RemoveDriver (scm, DRIVERNAME );
	if(!res)
	{
		printf("Cannot remove driver!");
		CloseServiceHandle(scm);
		return -1;
	}

	CloseServiceHandle(scm);
	#endif

return 0;
}

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

Работа с драйвером Example.sys

Как уже было сказано, из всех возможных способов инсталляции и запуска драйвера Example.sys, ниже будет использован способ тестирования с применением тестирующего консольного приложения, которое само будет выполнять инсталляцию и удаление драйвера (прибегая к вызовам SCM Менеджера). Для поэтапного ознакомления с процессом взаимодействия драйвера и обращающегося к нему приложения рекомендуется запустить программу ExampleTest под отладчиком (например, Visual Studio) в пошаговом режиме.

Перед запуском тестирующей программы ExampleTest рекомендуется загрузить программу DebugView, чтобы в ее рабочем окне наблюдать сообщения, поступающие непосредственно из кода драйвера Example.sys (отладочной сборки).

Однако прежде чем перейти к рассмотрению сообщений от драйвера, после выполнения установки и запуска драйвера (программным кодом консольного приложения ExampleTest в пошаговом режиме), прежде следует обратиться к программам WinObj и DeviceTree или аналогичным им программным средствам для того, чтобы удостовериться, присутствует ли в них информация об установленном драйвере (как, например, после инсталляции Мастером Установки нового оборудования.)

Как уже было сказано в главе 2, протокол полученных программой DebugView отладочных сообщений драйвера можно сохранить в файле для последующего анализа. Ниже приведена информация из такого файла, отражающая события в драйвере Example.sys с момента его загрузки и вызова процедуры DriverEntry до момента выгрузки и вызова процедуры UnloadRoutine. Рассмотрим содержание этого файла подробнее.

Сообщения, отправляемые драйвером из процедуры DriverEntry (первое число — номер сообщения, второе — относительное время, не имеющие большого значения в данном случае), выглядят следующим образом:

00000000  0.00000000 =Example= In DriverEntry.	
00000001  0.00003743 =Example= RegistryPath = \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Example.	
00000002  0.00012823 =Example= FDO FF919C68, DevExt=FF919D20.	
00000003  0.00021176 =Example= DriverEntry successfully completed.	

В переменной RegistryPath содержится поступающий от системы путь (в формате UNICODE) внутри Системного Реестра, где можно найти информацию о запускаемом драйвере. Видим также, что созданный драйвером функциональный объект устройства (FDO) имеет адрес 80E57BE0, а недалеко от него (а именно — внутри) находится структура расширения устройства, которая была определена в файле Driver.h.

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

00000004  0.00172536 -Example- Create File is 

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

Реакция на запрос IOCTL_PRINT_DEBUG_MESS:

00000005  0.00178850 -Example- In DeviceControlRoutine (fdo= FF919C68)	
00000006 0.00180554 -Example- DeviceIoControl: IOCTL 222004.
00000007 0.00182230 -Example- PASSIVE_LEVEL (val=0)
00000008 0.00183515 -Example- IOCTL_PRINT_DEBUG_MESS.
00000009 0.00184940 -Example- DeviceIoControl: 0 bytes written.

Реакция на запрос IOCTL_CHANGE_IRQL:

00000010  0.00187594 -Example- In DeviceControlRoutine (fdo= FF919C68)	
00000011 0.00189074 -Example- DeviceIoControl: IOCTL 222008.
00000012 0.00190331 -Example- PASSIVE_LEVEL (val=0)
00000013 0.00191477 -Example- IOCTL_CHANGE_IRQL.
00000014 0.00193125 -Example- DISPATCH_LEVEL value =2
00000015 0.00194466 -Example- IRQLs are old=2 new=10
00000016 0.00196226 -Example- DeviceIoControl: 0 bytes written.

Интересно, что попытки установить в Windows XP текущее значение IRQL, равное 25 (в середине диапазона аппаратных прерывания) не дала результата: было позволено только значение 10, что ниже границы начала аппаратных прерываний. В то же время, в Windows 98 и Windows Server 2003 (для сравнения) в этом месте выводится значение 25.

Реакция на запрос IOCTL_TOUCH_PORT_378H выглядит следующим образом:

00000017  0.00198377 -Example- In DeviceControlRoutine (fdo= FF919C68)	
00000018 0.00199858 -Example- DeviceIoControl: IOCTL 222010.
00000019 0.00201115 -Example- PASSIVE_LEVEL (val=0)
00000020 0.00202344 -Example- IOCTL_TOUCH_PORT_378H.

Реакция на запрос IOCTL_SEND_BYTE_TO_USER:

00000021  0.00204104 -Example- DeviceIoControl: 0 bytes written.	
00000022 0.00206870 -Example- In DeviceControlRoutine (fdo= FF919C68)
00000023 0.00208378 -Example- DeviceIoControl: IOCTL 222014.
00000024 0.00209664 -Example- PASSIVE_LEVEL (val=0)
00000025 0.00210921 -Example- Buffer outlength 1
00000026 0.00212066 -Example- Method : BUFFERED.
00000027 0.00213575 -Example- Buffer address is FF978828
00000028 0.00214944 -Example- DeviceIoControl: 1 bytes written.

Как видно из сообщений, здесь используется метод буферизации METHOD_BUFFERED, что и было описано в файле Driver.h при создании данного кода IOCTL. В соответствии с данным методом буферизации драйвер получает адрес буфера с явно системным значением (80E87E78 — адрес в системном пуле), что и должно было произойти при данном методе буферизации. Если бы в файле Driver.h при описании данного IOCTL кода был задан метод METHOD_NEITHER, то драйвер получил бы точный адрес переменной xdata из приложения ExampleTest (с типичным для пользовательского приложения адресом, типа 0002EFA0). Подробнее о том, как передаются данные при разных методах буферизации в IOCTL запросах, будет рассказано позже, в главе 6.

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

Сообщения, отправляемые драйвером из функции Close_File_IRPprocessing, обработчика запросов Диспетчера ввода/вывода, которые тот делает к драйверу в результате обращения из тестирующего приложения с вызовом CloseHandle :

00000029  0.00263888 -Example- In Close handler.

Сообщения, отправляемые драйвером из функции UnloadRoutine, обработчика запросов Диспетчера ввода/вывода, которые тот делает к драйверу при необходимости выполнить выгрузку драйвера:

00000030  0.00447794 -Example- In Unload Routine.	
00000031 0.00451985 -Example- Deleted device (0) : pointer to FDO = FF919C68.
00000032 0.00454220 -Example- Deleted symlink = \DosDevices\Example.

Выполнение IOCTL запроса с кодом IOCTL_MAKE_SYSTEM_CRASH, разумеется, здесь не приводится, потому что после такого действия система, например, Windows XP начинает перезагрузку (или получает управление отладчик SoftIce, если он установлен и активирован). Однако под Windows 98 данный вызов проходит как ни в чем не бывало, и драйвер выводит в рабочем окне DebugView сообщение о том, что ему удалось получить байт данных (равный, кстати, 0) по нулевому виртуальному адресу.

Чтобы ввести элемент интриги, можно добавить, что при устранении небольшого затруднения, мешающего перехвату обращения по нулевому виртуальному адресу, в Windows NT можно наблюдать, как это действительно срабатывает и переменная 'x' остается со своим значением 0xFF. Подробнее об этом можно узнать в 10 главе.