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 <Wdm.h>
#include <wdmsec.h>

// 全局设备对象
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下测试正常。完整项目工程可以在这里下载

测试效果

在这里插入图片描述

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
驱动程序是为了控制计算机硬件而编写的程序。Windows驱动程序是在Windows操作系统上编写的,可以控制各种硬件设备,如网络适配器、打印机、USB设备等等。 编写Windows驱动程序需要了解Windows内核编程和Win32 API编程,以及硬件设备的工作原理和通信协议。以下是编写Windows驱动程序的基本步骤: 1. 了解Windows内核编程和Win32 API编程Windows内核编程包括了解Windows内核的基本结构和API,以及内核对象和内核模式驱动程序的创建和管理。Win32 API编程Windows应用程序的编程接口,可以用于与Windows系统进行通信。 2. 确定要编写的驱动程序的类型。Windows驱动程序分为内核模式驱动程序和用户模式驱动程序两种类型。内核模式驱动程序直接与系统内核交互,可以访问所有硬件资源,但编写和调试难度较大;用户模式驱动程序通过系统API进行通信,可以访问一部分硬件资源,但编写和调试相对容易。 3. 确定要控制的硬件设备的类型和通信协议。不同类型的硬件设备有不同的工作原理和通信协议,需要根据硬件设备的手册和规范进行编程。 4. 编写驱动程序代码。根据编写驱动程序的类型和要控制的硬件设备的通信协议,编写相应的驱动程序代码。 5. 编译、链接和测试驱动程序。将编写好的驱动程序代码编译成可执行文件,链接到操作系统中,并进行测试和调试。 6. 安装和配置驱动程序。将编译好的驱动程序安装到计算机中,并进行相关配置。 注:以上步骤为编写Windows驱动程序的基本步骤,具体实现过程可能有所不同,需要根据具体情况进行调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值