Windows驱动—Windows应用程序和Windows驱动通信编程

文章目录

  • 介绍
  • 知识前奏
    • 内核方面编程
      • 设备对象和符号链接
      • 分发函数
    • 应用方面编程
      • 打开设备
      • 设备控制请求
  • 代码
    • 应用层代码
    • 内核层代码
    • 完整工程代码
  • 测试效果

介绍

Windows应用程序(Ring3层)和内核驱动(Ring0层)是运行在Windows权限的不同级别,简单来说各有优势。内核层权限较大 能做很多 应用程序办不到的事情 不直接面向程序使用的用户,Windows应用程序在Ring3层 直接面向用户,界面友好。当应用层办不到的时候就需要借助内核层了,所以 win32应用程序和Windows内核驱动通信是有必要的。Windows应用程序和Windows内核驱动程序直接是可以进行双向通信的,相互都是可以发送信息的

本篇博客,将使用一个最简单的例子来讲解 Win32程序和内核驱动程序通信编程。这里把内核驱动当做一个Server,应用程序当做一个客户端。客户端向内核驱动发生一个 小写字符串,内核驱动将小写字符串转成大写 然后回射回来。

知识前奏

内核方面编程

设备对象和符号链接

如果驱动需要和应用程序通信,首先必须要生成一个设备对象(Device Object)。设备对象和分发函数构成了整个内核体系的基本框架设备对象用来暴露给应用层,应用层可以像操作文件一样操作它。用于和应用程序通信的设备往往用来"控制"这个内核驱动,所以往往称之为**“控制设备对象”**(Control Device Object,CDO)。生成设备对象使用IoCreateDevice函数,原型如下:

/*
	return STATUS_SUCCESS成功
*/
NTSTATUS 
  IoCreateDevice(
    // 可直接从DriverEntry参数中获得
    IN PDRIVER_OBJECT  DriverObject,
    // 表示设备扩展大小(应用设备扩展)会专门讲述
    IN ULONG  DeviceExtensionSize,
    // 设备名
    IN PUNICODE_STRING  DeviceName  OPTIONAL,
    // 设备类型,Windows已经规定了一系列设备类型
    IN DEVICE_TYPE  DeviceType,
    // 表示一组设备属性
    IN ULONG  DeviceCharacteristics,
    // 表示是否一个独占设备,设置独占,这个设备将在同一个时刻只能被打开一个句柄。一般都不会设置为独占设备
    IN BOOLEAN  Exclusive,
    //  返回结果
    OUT PDEVICE_OBJECT  *DeviceObject
    );

控制设备需要有一个名字,这样才会被暴露出来,供其他程序打开与之通信。设备的名字可以在IoCreateDevice或IoCreateDeviceSecure时指定。但是,应用层是无法直接通过设备的名字来打开对象的,必须建立一个暴露给应用层的符号链接。符号链接是记录一个字符串对应到另一个字符串的简单结构。函数原型IoCreateSymbolicLink如下:

NTSTATUS 
  IoCreateSymbolicLink(
    // 符号链接名,如果该符号链接名存在 则创建不成功
    IN PUNICODE_STRING  SymbolicLinkName,
    // 设备名
    IN PUNICODE_STRING  DeviceName
    );

控制设备和符号链接的删除很简单,一一对应,IoDeleteDevice、IoDeleteSymbolicLink

分发函数

分发函数是一组用来处理发送给设备对象(当然包括控制设备)的请求的函数。这些函数当然由内核驱动的开发者编写,以便处理这些请求并返回给Windows。分发函数是设置在驱动对象上的。Windows的IO管理器在收到请求时,会根据请求发送的目标,也就是一个设备对象,来调用这个设备对象所从属的驱动对象上的分发函数。最简单的3种请求:

  • 打开(Create):在试图访问一个设备对象之前,必须先用打开请求打开它。只有得到成功的返回,才可以发送其他的请求。
  • 关闭(Close):在结束访问一个设备对象之后,发送关闭请求将它关闭。关闭之后,就必须再次打开才能访问。
  • 设备控制(Device Control):设备控制请求是一种即可以用来输入(应用到内核),又可以用来输出(从内核到应用)的请求。

分发函数原型:

NTSTATUS MyDispatch (
	IN PDEVICE_OBJECT DeviceObject,
	IN PIRP Irp
	)
{
    // 在分发函数内部进行 请求的处理。一般有如下几个步骤
    // 第1步,判断请求是否发送给该驱动的设备对象
    // 第2步,获取请求的当前栈空间(空间中含有请求相关的信息)
    // 第3步,获取请求的功能号,不同的功能号做不同的处理(这里就可以对输入输出做操作了)。
    // 第4步,结束请求
}

应用方面编程

打开设备

在应用程序中 打开驱动中创建的设备。和打开文件没什么区别,使用 CreateFile即可,要注意文件的路径。

/*
文件的路径就是符号链接的路径,但是符号链接的路径在应用看来,是以"\\.\"开头的。注意,这些"\"在C语言中要使用"\\"来转义
*/
#define MY_DEVOBJ_SYB_NAME (L"\\\\.\\lcx10000")
	HANDLE deviceHandle = CreateFile(MY_DEVOBJ_SYB_NAME,GENERIC_READ|GENERIC_WRITE,
		0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);

设备控制请求

设备控制请求即可以进行输入也可进行输出。每个设备控制请求都会有一个功能号,用来区分不同的设备控制请求。这个功能号体现在CTL_CODE中。

CTL_CODE是一个宏,是SDK里头文件提供的。我们要做的是直接利用这个宏来生成一个自己的设备控制请求功能号。CTL_CODE有4个参数。

  • 参数1,设备类型,这里的控制设备与任何硬件都没有关系,所以直接定义为未知类型FILE_DEVICE_UNKNOWN。
  • 参数2,生成这个功能号的核心数字,这个数字直接用来和其他参数“合成”功能号?0x0~0x7ff被微软预留了,同时也不能超过0xfff。如果要定义超过一个的功能号,那么不同的功能号就靠这个数字进行区分。
  • 参数3,METHOD_BUFFERED是说用缓存方式。用缓存方式,输入输出缓存会在用户和内核之间拷贝。这是比较简单和安全的一种方式
  • 参数4,是这个操作需要的权限。当需要将数据发送到设备时,相当于往设备写入数据,所以标志为拥有写数据权限(FILE_WRITE_DATA)。

设备控制请求函数原型:

BOOL DeviceIoControl(
    // 设备句柄
  HANDLE       hDevice,
    // Control code
  DWORD        dwIoControlCode,
    // 输入缓冲区
  LPVOID       lpInBuffer,
    // 输入缓冲区长度
  DWORD        nInBufferSize,
    // 输出缓冲区
  LPVOID       lpOutBuffer,
    // 输出缓冲区长度
  DWORD        nOutBufferSize,
    // 接受到的有效数据长度
  LPDWORD      lpBytesReturned,
    // 指向OVERLAPPED结构体的指针
  LPOVERLAPPED lpOverlapped
);

#define CTL_CODE( DeviceType, Function, Method, Access )

代码

代码中有很详细的注释。

应用层代码

这里使用简单MFC界面操作代码


// 设备名对应的符号链接名,用于暴露给应用层。符号链接在应用看来是在\\.\ 的
#define MY_DEVOBJ_SYB_NAME (L"\\\\.\\lcx10000")

//CTL_CODE创建控制码
#define IOCTL_SEND_AND_REC_STR\
	CTL_CODE(FILE_DEVICE_UNKNOWN\
	, 0x801, METHOD_BUFFERED,\
	FILE_READ_DATA | FILE_WRITE_DATA)

void CMy02_Win32ToDriverDlg::OnBnClickedSendtodriver()
{
	UpdateData(TRUE);
	int len = m_uiSendToDriverString.GetLength();
	char* pInStr = new char[len+1];
	char *pOutStr = new char[len+1];
	memset(pInStr,0,len+1);
	memset(pOutStr,0,len+1);
	for(int i = 0; i < len ; i++)
	{
		pInStr[i] = m_uiSendToDriverString.GetAt(i);
	}

	//char* pStr = (char*)m_uiSendToDriverString.GetBuffer(0);
	
	
	int ret_len = 0;
	// 1.打开驱动设备
	HANDLE deviceHandle = CreateFile(MY_DEVOBJ_SYB_NAME,GENERIC_READ|GENERIC_WRITE,
		0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
	if(deviceHandle == INVALID_HANDLE_VALUE)
	{
		DWORD errCode = ::GetLastError();
		m_uiDriverResponse = _T("CreateFile 失败!驱动未加载");
		m_uiDriverResponse.AppendFormat(_T(" errCode=%d"),errCode);
	}
	else
	{
		// 2.向驱动设备发送设备控制请求
		if( DeviceIoControl(deviceHandle,IOCTL_SEND_AND_REC_STR,
			pInStr,len,pOutStr,len+1,(LPDWORD)&ret_len,NULL))
		{

			m_uiDriverResponse = _T("suc.");
			m_uiDriverResponse.AppendFormat(_T("retLen=%d,response=%s"),ret_len,CString(pOutStr));
		}
	}

	UpdateData(FALSE);
}

内核层代码

#include 
#include 

// 全局设备对象
PDEVICE_OBJECT g_devObj = NULL;


// 设备名对应的符号链接名,用于暴露给应用层。符号链接一般都是在\??\路径下
#define MY_DEVOBJ_SYB_NAME (L"\\??\\lcx10000")

// 设备一般都是位于 \Device\这个路径下的
#define MY_DEVOBJ_NAME (L"\\Device\\lcx10000")

// 可用VS自带的GUID生成器生成 {D1AC1F58-AAC4-45DD-AEC4-A9670DC47B29}
static const GUID g_devGUID = 
{ 0xd1ac1f58, 0xaac4, 0x45dd, { 0xae, 0xc4, 0xa9, 0x67, 0xd, 0xc4, 0x7b, 0x29 } };

//CTL_CODE创建控制码
#define IOCTL_SEND_AND_REC_STR\
	CTL_CODE(FILE_DEVICE_UNKNOWN\
	, 0x801, METHOD_BUFFERED,\
	FILE_READ_DATA | FILE_WRITE_DATA)

NTSTATUS MyDispatch (
	IN PDEVICE_OBJECT DeviceObject,
	IN PIRP Irp
	)
{
	NTSTATUS status = STATUS_SUCCESS;
	ULONG ret_len = 0,i = 0,temp = 0;
	// 第1步,判断请求是否发送给该驱动的设备对象,如果不是 简单返回成功
	if(DeviceObject == g_devObj)
	{
		// 第2步,获取请求的当前栈空间(空间中含有请求相关的信息)
		PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
		
		// 第3步,获取请求的功能号,不同的功能号做不同的处理
		// 打开请求的主功能号是 IRP_MJ_CREATE
		// 关闭请求的主功能号是 IRP_MJ_CLOSE
		// 设备控制请求的主功能号是IRP_MJ_DEVICE_CONTROL

		// 处理打开和关闭IRP,可以简单返回成功即可
		if( irpStack->MajorFunction == IRP_MJ_CREATE ||
			irpStack->MajorFunction == IRP_MJ_CLOSE )
		{
			status = STATUS_SUCCESS;
		}
		// 处理设备控制请求DeviceIoControl
		else if( irpStack->MajorFunction == IRP_MJ_DEVICE_CONTROL)
		{
			// 当前是一个缓存方式的设备控制请求,直接从IRP请求参数中获取缓冲区buffer
			// 同时 这里输入缓冲区、输出缓冲区是共享的
			PVOID buffer = Irp->AssociatedIrp.SystemBuffer;
			ULONG inLen = irpStack->Parameters.DeviceIoControl.InputBufferLength;
			ULONG outLen = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
			//KdBreakPoint();// 断点设置,使用windbg调试时可以打开

			//控制码由这个宏函数CTL_CODE创建
			if(irpStack->Parameters.DeviceIoControl.IoControlCode
				== IOCTL_SEND_AND_REC_STR)
			{
				if(inLen > 0)
				{
					// 做一个简单打印
					DbgPrint("str=%s",(char*)buffer);
					// 要求输出缓存要多于输入缓存
					if(outLen >= inLen)
					{
						// 这里转大写后返回
						//_strupr((char*)buffer);
						ret_len = inLen;
						for(; i <= inLen ; i++)
						{
							temp = (ULONG)((char*)buffer)[i];
							if( temp >= 97 && temp <= 122)
							{
								((char*)buffer)[i] -= 32;
							}
						}
					}
					else
					{
						status = STATUS_INVALID_PARAMETER;
					}
				}
				else
				{
					status = STATUS_INVALID_PARAMETER;
				}

			}
			else
			{
				// 其他控制码请求,一律返回非法参数错误。
				status = STATUS_INVALID_PARAMETER;
			}
		}
	}
	
	// KdBreakPoint(); // 断点设置,使用windbg调试时可以打开

	// 第4步,结束请求
	// 这个Informatica用来记录这次返回到底使用了多少输出空间
	Irp->IoStatus.Information = ret_len;
	// 用于记录这个请求的完成状态
	Irp->IoStatus.Status = status;
	// 用于结束这个请求
	IoCompleteRequest(Irp,IO_NO_INCREMENT);
	return status;
}



VOID DriverUnload(
	__in struct _DRIVER_OBJECT  *DriverObject
	)
{	
	UNICODE_STRING DeviceLinkName = RTL_CONSTANT_STRING(MY_DEVOBJ_SYB_NAME);
	DbgPrint("DriverUnload enter \n");
	// 删除符号链接
	IoDeleteSymbolicLink(&DeviceLinkName);
	// 删除设备对象
	IoDeleteDevice(g_devObj);
}


NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
	int i;
	NTSTATUS status;
	// 设备名
	UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(MY_DEVOBJ_NAME);
	UNICODE_STRING SDDLString = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
	UNICODE_STRING DeviceLinkName = RTL_CONSTANT_STRING(MY_DEVOBJ_SYB_NAME);
	DbgPrint("DriverEntry enter \n");


	// 如果驱动需要和应用程序通信,首先必须要生成一个设备对象
	status = IoCreateDevice(driver,0,
		&DeviceName,FILE_DEVICE_UNKNOWN,
		FILE_DEVICE_SECURE_OPEN,FALSE,
		&g_devObj);

	// 由于IoCreateDevice函数生成的设备具有默认的安全,那么必须具有管理员权限的进程才能打开它
	// 可用如下函数替换,不过下面的函数在 WinXP中无法使用
	//status = IoCreateDeviceSecure(driver,0,
	//	&DeviceName,FILE_DEVICE_UNKNOWN,
	//	FILE_DEVICE_SECURE_OPEN,FALSE,
	//	&SDDLString,// 设备对象安全设置
	//	&g_devGUID, // 设备guid
	//	&g_devObj);
	if(!NT_SUCCESS(status))
	{
		return status;
	}

	// 创建符号链接
	status = IoCreateSymbolicLink(&DeviceLinkName,&DeviceName);
	if(!NT_SUCCESS(status))
	{
		// 一旦失败,之前生成的设备对象也要删掉,防止内存泄漏
		IoDeleteDevice(g_devObj);
		return status;
	}

	// 设置驱动对象的分发函数,分发函数是一组用来处理发送给设备对象的请求的函数
	// 这里driver->MajorFunction是一个数组,不同的请求可以设置不同的处理函数
	// 这里为了方便所有的请求都用一个处理函数,在函数内部去区分请求,再做不同的逻辑处理
	//int i; // 变量定义要放在最前面,放这里不行
	for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		//driver->MajorFunction[i] = NULL;
		driver->MajorFunction[i] = MyDispatch;
	}

	// 支持动态卸载。
	driver->DriverUnload = DriverUnload;

	return STATUS_SUCCESS;
}

完整工程代码

笔者用vs2010进行的编译 win32应用程序和驱动程序,在Win XP下测试正常。完整项目工程可以在这里下载。

测试效果

Windows驱动—Windows应用程序和Windows驱动通信编程_第1张图片

你可能感兴趣的:(【Windows编程】,#,【Windows驱动】,Windows内核驱动,Windows编程,Window内核编程,DeviceIoControl)