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种请求:
分发函数原型:
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个参数。
设备控制请求函数原型:
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下测试正常。完整项目工程可以在这里下载。