关于派遣函数对IRP的处理和驱动程序同步小结

难得闲暇时间,看了windows驱动关于派遣函数对IRP的处理和驱动程序同步,写一些个人的理解吧,如果有误还希望大侠们帮忙指点下,提前谢过了:

派遣函数对IRP的处理:

IRP即为I/O输入输出请求包,应用程序和底层驱动程序通信时,应用程序会发出I/O请求,操作系统会将I/O请求转换成对应的IRP。在驱动装载完毕的时候,Driver_Entry函数已经

指定了对应IRP的派遣函数,IRP共有0x1b个,这个很容易查找。这个时候要对IRP和IO_STACK_LOCATION数据结构有个基本的认识,MSDN或网上都有,不再赘述。对绝大多

数的派遣函数处理是比较简单,一般设置如下:

pIrp->IoStatus.Status = STATUS_SUCCESS;//设置IRP的完成状态

pIrp->IoStatus.Information = 0;//设置IRP操作了多少字节,设置为0字节

IoCompleteRequest(pIrp, IO_NO_INCREMENT);//对于指定的IRP以何种优先级完成线程的恢复运行

应用程序与驱动程序的交互式通过Native API(ntdll.dll)作为ring3到ring0的接口,通过Native API创建IRP来完成ring3和ring0的通信。下面需要加深介绍设备文件与应用程序的通信

方式:缓冲区方式、直接方式、其他方式、IO设备控制操作

<1>、缓冲区方式

在应用层分配10字节缓冲区并出初始化数据WriteFile(hDevice, buffer, 0, &ulWrite, NULL);

在派遣函数对IRP_MJ_WRITE处理函数中:

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

ULONG ulWriteLength = stack->Parameters.Write.Length;//获取存储长度

ULONG ulWriteOffset = stack->Parameters.Write.ByteOffset.QuadPart;//获取存储的偏移量

memcpy(pDevExt->buffer+ulWriteOffset, pIrp->AssociatedIrp.SystemBuffer, ulWriteLength);//将写入的数据,存储在设备扩展结构体成员buffer的缓冲区内

然后就是设置IRP的完成状态、实际操作字节数和将IRP请求结束。

以上是写设备,验证正确性就是将扩展设备的缓冲区中的内容读出来,定义一个buffer(初始化为空)用来读取设备中的数据

ReadFile(hDevice, buffer, 10, &ulRead, NULL);//读操作的应用层代码

在派遣函数对IRP_MJ_READ处理函数中和派遣函数对IRP_MJ_WRITE处理函数的过程相类似:

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

ULONG ulReadLength = stack->Parameters.Read.Length;

ULONG ulReadOffset = stack->Parameters.Read.ByteOffset.QuadPart;

memcpy(pIrp->AssociatedIrp.SystemBuffer, pDevExt->buffer+ulReadOffset, ulReadLength);//将数据存储在AssociatedIrp.SystemBuffer,以便应用程序应用

至此,缓冲区方式读写设备大体流程就走完了,很多细节我都没有去写,读者自己关注.

<2>、直接方式读写操作

在代码解读之前,先进行一些理论的理解,便于对代码的解读。在创建设备的时候,设备对象的标志位为DO_DIRECT_IO,而不是DO_BUFFERED_IO。

了解一个概念,MDL:内存描述符表。当用直接方式读写设备的时候,OS会将用户模式下的缓冲区锁住,然后OS在内核模式下将该缓冲区的地址在映射一遍,那么用户模式和

内核模式指向的是同一区域的物理内存。MDL记录着这一段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址为mdl->StartVa, 这段虚拟

地址mdl->ByteOffset,因此这段虚拟内存的首地址应该是mdl->StartVal+mdl->ByteOffset,下面进行代码解读:

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);

ULONG ulReadLength = stack->Parameters.Read.Length;//获得指定的字节数

ULONG mdl_length = MmGetMdllByteCount(pIrp->MdlAddress);//得到锁定缓冲区的长度

PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);//得到锁定缓冲区的首地址

PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);//得到MDL在内核模式下的映射

memset(kernel_address, 0xAA, ulReadLength);//填充内存

剩下的就是设置IRP的完成状态、操作字节数和IRP请求结束。

<3>、其他方式读写操作

用其他方式读写时,ReadFile和WriteFile提供的缓冲区内存地址,可以在派遣函数中通过IRP的pIrp->UserBuffer字段得到。读取的字节数在stack->Parameters.Read.Length字

字段得到。使用用户模式的内存, 有可能是空指针地址或者非法地址,所以在将内存传递给驱动程序时候,要对这段内存进行是否可读或可写进行探测,这里我们就会用到try

块和ProbeForWrite函数。直接代码解读:

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);//得到当前的堆栈

ULONG ulReadLength = stack->Parameters.Read.Length;//得到读的长度

ULONG ulReadOffset = stack->Parameters.Read.ByteOffset.QuadPart;//得到读的偏移量

PVOID user_address = pIrp->UserBuffer;//得到用户模式地址

__try{ProbeWrite(user_address, ulReadLength, 4); memset(user_address, 0xAA, ulReadLength);}

__except(EXCEPTION_EXECUTE_HANDLER){status = STATUS_UNSUCCESSFUL;}

然后就是设置IRP的完成状态、操作字节数和结束IRP请求。

<4>、IO设备控制操作

在进行此部分的讲解之前,我们自行解读DeviceIoControl函数的定义,根据控制码的定义,将IO设备控制操作分为缓冲区内存模式IOCTL、直接内存模式IOCTL、其他内存模式IOCTL。首先应用CTL_CODE宏定义IOCTL码:

#define IOCTL_TEST1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0X800, METHOD_BUFFERED, FILE_ANY_ACCESS)

定义完毕后,根据宏定义的第三个参数,可以判断为缓存内存模式IOCTL,代码解读如下:

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);//得到当前的堆栈

ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;//得到输入缓冲区的大小

ULONG cbout = stack->Parameters.DeviceIoControl,OutputBufferLength;//得到输出缓冲区的大小

ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;//得到IOCTL码

然后swtich case判断code是否为IOCTL_TEST1,如果是:

UCHAR* InputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;//打印出InputBuffer中的元素,类似于写设备

UCHAR* OutputBuffer = (UCHAR*)pIrp->AssociatedIrp.SystemBuffer;//类似于读设备

memset(OutputBuffer, 0xAA, cbout);

对于直接内存模式IOCTL和其他内存模式IOCTL处理过程相似,不在赘述,至此关于派遣函数对IRP的处理就结束了。

驱动程序的同步小结:

首先,我们对"不可重入"这个概念要有个概念,自己先行理解,从而引出了同步的必要性。

<1>、自旋锁,它可以用于驱动程序中的同步处理,初始化自旋锁时,处于解锁状态,这时可以被程序获取,使用完毕释放以后,才能再次被获取。它不等同于线程中的休眠,

如果当前线程没有获得自旋锁,则一直处于自旋状态,占用CPU资源,因此,对自旋锁的占用时间不宜过长。单CPU获取自旋锁仅仅是将当前的IRQL从PASSIVE_LEVEL级别

提升到DISPATCH_LEVEL级别,但多CPU较为复杂,自行研究,以后我在讲解,目前还没有研究。一般将自旋锁定义在设备扩展里,而不是全局变量,KSPIN_LOCK

My_SpinLock;在使用自旋锁之前,使用KeInitilizeSpinLock()内核函数,获取自旋锁使用KeAcquireSpinLock()函数,释放自旋锁使用KeReleaseSpinLock()函数。

<2>、一般多线程中才需要同步处理机制,内核函数PsCreateSystemThread负责创建新线程,创建的线程一种是用户线程、一种是系统线程。至于如何区分用户线程还是系统

线程,看第四个形参。其次,创建的线程必须用函数PsTerminalSystemThread()函数强制线程结束,否则该线程无法退出。线程的创建已经解决,事件对象是同步驱动程序较

为常用的方法,KEVENT结构体表示一个事件对象,内核函数KeInitializeEvent负责对事件进行初始化,事件对象的运用同用户模式下的事件应用的逻辑是一致的,注意设置事

件函数为KeSetEvent函数,等待函数为KeWaitForSingleObject和KeWaitForMultipleObject。应用程序与驱动程序共用一个事件对象,为此内核函数提供一个将句柄转换为指针

的函数ObReferenceObjectByHandle,用完之后会调用ObDereferenceObject,维护计数器的平衡。至于驱动程序与驱动程序交互事件对象,则是用有名字的时间对象,创建有

名事件对象可以通过IoCreateNotificationEvent和IoCreateSynchronizationEvent函数,一个是同步事件,另一个是通知事件,其余的方法参照应用程序与驱动程序的方法。

<3>、在第二点中提到的创建多线程和事件对象的共同运用,以达到同步处理,是较为常用的方法,其次还有信号灯和互斥体,也是较为常见的方法。在应用的过程中,信号灯

和互斥体的方法参照事件对象的方法。


总的来说,内核模式下的多线程的同步处理与用户模式下的同步处理,逻辑上基本是一致的。至此此片文章就结尾了,如果你感觉文章中有不妥之处,敬请指正,thx


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值