[原创]windows内核编程之[串口过滤]
嗯,昨天发过一篇有关【应用-内核通信】的内核基础贴:http://bbs.pediy.com/showthread.php?t=212683 请大家多多提意见。
今天的内容是串口过滤,都是很基础的,但我会认真写的。
过滤概念:
过滤,顾名思义,水流从A点流向B点,当我们想要去掉水中的泥沙,却又想不影响水的流向时,就可以在A,B两点间插入一个过滤网C。串口过滤其实也和这差不多,我们可以在串口设备的外面加一层过滤网,这样就保证了总是我们的程序率先收到应用发向串口的信息。如果是写串口操作,还可以截获写往串口的数据。当我们的“过滤网”对截获的信息操作完毕之后,为了不影响内核正常工作,还需要将截获的信息发出去(至于怎么发就是你们自己的事了)
技术原理:
上一篇帖子介绍了如何生成驱动程序自己的设备对象,那么现在我们可以用自己生成的“虚拟设备对象”来绑定到“串口设备对象”上。这样绑定过后,原本发给串口的信息,都会先发到我们自己的设备对象上。
用于绑定的内核API很多,这里我使用的是IoAttachDeviceToDeviceStack,与这个相似的API还有一个:IoAttachDeviceToDeviceStackSafe,只不过有兼容性问题。贴出IoAttachDeviceToDeviceStack原型:
PDEVICE_OBJECT IoAttachDeviceToDeviceStack( _In_ PDEVICE_OBJECT SourceDevice, _In_ PDEVICE_OBJECT TargetDevice );
第一个参数是我们生成的过滤设备,第二个参数是要被绑定的设备,我们将设备绑定之后,就可以收到对串口的IRP请求了。那么如何获得需要绑定的设备对象呢?可以使用IoGetDeviceObjectPointer函数来获取设备对象。原型:
NTSTATUS IoGetDeviceObjectPointer( _In_ PUNICODE_STRING ObjectName, _In_ ACCESS_MASK DesiredAccess, _Out_ PFILE_OBJECT *FileObject, _Out_ PDEVICE_OBJECT *DeviceObject );
这个API可以通过设备名称获取设备对象,由于在windows中,串口是有固定名字的,所以能很方便的获取设备对象。第一个参数是设备名称,第二个是访问权限,第三个参数用于返回文件对象(在这里我们不用管他,获得文件对象之后直接解除引用就好了),第四个参数用于返回设备对象。得到设备对象,就可以和自己生成的过滤设备进行绑定了。
绑定之后,对消息处理完毕,我们需要把消息发往原始设备,使用以下API来完成此功能:
IoSkipCurrentIrpStackLocation
IoCallDriver
这里就不具体讲解这两个API了。
在卸载函数里,我们需要把绑定的设备进行解绑,然后删除掉。使用以下两个API:
VOID IoDetachDevice( _Inout_ PDEVICE_OBJECT TargetDevice ); VOID IoDeleteDevice( _In_ PDEVICE_OBJECT DeviceObject );
以上两个函数,所需要提供的参数都是我们生成的过滤设备。
至此,技术原理就讲解完毕。
简单的串口过滤驱动设计思路:
首先从第一个问题开始:获得设备对象并绑定。
我们并不知道计算机上究竟有多少个串口,只能一个个的去尝试打开,判断是否成功,如果成功,就绑定
for (i = 0; i < 32; i++)
{
//首先将打开的端口保存到临时变量中
KS_Ports = KS_OpenPort(i, &status);
//判定是否打开成功
if (KS_Ports == NULL)
continue;
//然后绑定
KS_deviceAttach(KS_driver, KS_Ports, &KS_MyPorts[i], &KS_OtherPorts[i]);
}
正如大家所见,我这里粗略的指定了一个值:32,也就是说最多打开32个串口,最后调用的那个函数KS_deviceAttach是我自己编写的一个绑定函数。我申请了两个数组全局变量,分别是KS_MyPorts与KS_OtherPorts,前者用于保存程序自己的过滤设备指针,后者用于保存原始设备的指针。在KS_deviceAttach中,我们传入当前设备对象KS_driver,和我们打开的端口设备对象 KS_Ports。最后他会把绑定的过滤设备指针返回到KS_MyPorts[i]中,把原始设备返回到KS_OtherPorts[i]中。
函数 KS_OpenPort是我编写的一个打开串口设备函数。传递一个整数i,表示尝试打开第几个串口,结果返回到status中。下面贴出KS_OpenPort代码:
//打开串口设备函数
PDEVICE_OBJECT KS_OpenPort(ULONG id, NTSTATUS *status)
{
WCHAR Port_Name[32] = { 0 };
UNICODE_STRING KS_Name;
PFILE_OBJECT KS_FileObj = NULL;
PDEVICE_OBJECT KS_Dev = NULL;
RtlStringCchPrintfW(Port_Name, 32, L"\\Device\\Serial%d", id);
RtlInitUnicodeString(&KS_Name, Port_Name);
*status = IoGetDeviceObjectPointer(&KS_Name, FILE_ALL_ACCESS, &KS_FileObj, &KS_Dev);
if (*status == STATUS_SUCCESS)
ObDereferenceObject(KS_FileObj);
return KS_Dev;
}
如上所示:RtlStringCchPrintfW把串口号转换成设备名称,最后打开。
绑定完毕之后,设置了分发函数,就可以截获信息了。
在这里我们只截获对串口的写请求,其他的一律直接发送给下层设备。
设计思路讲完了,下面贴出完整代码。
#include <ntddk.h>
#include <ntstrsafe.h>
//保存过滤设备指针
PDEVICE_OBJECT KS_MyPorts[32];
//保存原始设备指针
PDEVICE_OBJECT KS_OtherPorts[32];
//卸载函数,这里需要把绑定的设备都解绑,然后删除
VOID KS_dirverUnload(PDRIVER_OBJECT KS_driver)
{
ULONG i;
for (i = 0; i < 32; i++)
{
if (KS_MyPorts[i] != NULL)
IoDetachDevice(KS_MyPorts[i]);
}
//这里我们直接删除设备,但这是不安全的,
//因为有可能还会有未完成的irp
for (i = 0; i < 32; i++)
{
if (KS_MyPorts[i] != NULL)
IoDeleteDevice(KS_MyPorts[i]);
}
DbgPrint("My driver is unloading...");
}
//生成一个设备并绑定KS_Lobj指向的设备,
//KS_Nobj用于返回过滤设备指针,next用于返回原始设备指针
NTSTATUS KS_deviceAttach(
PDRIVER_OBJECT driver,
PDEVICE_OBJECT KS_Lobj,
PDEVICE_OBJECT *KS_Nobj,
PDEVICE_OBJECT *next)
{
NTSTATUS status;
PDEVICE_OBJECT KS_newDev;
//生成设备
status = IoCreateDevice(driver, 0, NULL, KS_Lobj->DeviceType, 0, FALSE, KS_Nobj);
if (!NT_SUCCESS(status))
{
DbgPrint("Create device failed!\r\n");
status = STATUS_UNSUCCESSFUL;
return status;
}
if (KS_Lobj->Flags&DO_BUFFERED_IO)
(*KS_Nobj)->Flags |= DO_BUFFERED_IO;
if (KS_Lobj->Flags&DO_DIRECT_IO)
(*KS_Nobj)->Flags |= DO_DIRECT_IO;
if (KS_Lobj->Flags&FILE_DEVICE_SECURE_OPEN)
(*KS_Nobj)->Flags |= FILE_DEVICE_SECURE_OPEN;
(*KS_Nobj)->Flags |= DO_POWER_PAGABLE;
//绑定设备
KS_newDev = IoAttachDeviceToDeviceStack(*KS_Nobj, KS_Lobj);
if (KS_newDev == NULL)
{
IoDeleteDevice(*KS_Nobj);
*KS_Nobj = NULL;
status = STATUS_UNSUCCESSFUL;
return status;
}
*next = KS_newDev;
//设备启动
(*KS_Nobj)->Flags = (*KS_Nobj)->Flags & ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
//打开串口设备函数
PDEVICE_OBJECT KS_OpenPort(ULONG id, NTSTATUS *status)
{
WCHAR Port_Name[32] = { 0 };
UNICODE_STRING KS_Name;
PFILE_OBJECT KS_FileObj = NULL;
PDEVICE_OBJECT KS_Dev = NULL;
RtlStringCchPrintfW(Port_Name, 32, L"\\Device\\Serial%d", id);
RtlInitUnicodeString(&KS_Name, Port_Name);
*status = IoGetDeviceObjectPointer(&KS_Name, FILE_ALL_ACCESS, &KS_FileObj, &KS_Dev);
if (*status == STATUS_SUCCESS)
ObDereferenceObject(KS_FileObj);
return KS_Dev;
}
//分发函数,当串口有IRP请求时会被此函数截获。
NTSTATUS KS_Dispatch(PDEVICE_OBJECT dev, PIRP irp)
{
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status;
ULONG i, j;
//遍历绑定的所有串口
//判断究竟是发给哪个串口的
for (i = 0; i < 32; i++)
{
//电源操作直接发送
if (IrpStack->MajorFunction == IRP_MJ_POWER)
{
PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return PoCallDriver(KS_OtherPorts[i], irp);
}
//过滤写请求
if (IrpStack->MajorFunction == IRP_MJ_WRITE)
{
//在这里就可以对数据进行操作,这里我们
//仅仅是打印出数据然后原封不动的发给下一层
ULONG len = IrpStack->Parameters.Write.Length;
PUCHAR buffer = NULL;
//因为不知道用的什么储存方式,所以需要进行这样的判断
if (irp->MdlAddress != NULL)
buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
else
buffer = (PUCHAR)irp->UserBuffer;
if (buffer == NULL)
buffer = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
for (j = 0; j < len; j++)
{
DbgPrint("The data:%2x\r\n", buffer[j]);
}
//当我们截获数据后,直接下发这些操作就可以了
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(KS_MyPorts[i], irp);
}
}
//如果这段代码被执行,表示没有找到设备,这里我们粗略的返回错误
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT KS_driver, PUNICODE_STRING KS_UNS)
{
ULONG i;
NTSTATUS status;
PDEVICE_OBJECT KS_Ports;
//设置分发函数
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
KS_driver->MajorFunction[i] = KS_Dispatch;
}
//设置卸载函数
KS_driver->DriverUnload = KS_dirverUnload;
//绑定所有串口
for (i = 0; i < 32; i++)
{
//首先将打开的端口保存到临时变量中
KS_Ports = KS_OpenPort(i, &status);
if (KS_Ports == NULL)
continue;
//然后绑定
KS_deviceAttach(KS_driver, KS_Ports, &KS_MyPorts[i], &KS_OtherPorts[i]);
}
return STATUS_SUCCESS;
}
以上就是一个简单的串口过滤全部代码,对于截获的写操作数据,我们仅仅是简单的把他用dbgprint打印出来,然后不做任何修改的发送到下一层设备。
全文除函数原型是从MSDN文档复制之外,其余的都是纯手打,难免有错,还请多多指教。
最后,欢迎大家加我qq37794802,一起讨论学习内核编程
