本文转载自: http://blog.csdn.net/arvon2012/article/details/7789724
最近在学习windows驱动设计,认真看了些教材后总结了我认为驱动中都会涉及到,也最重要的概念,和大家分享。如果有说的不对的请大家留言指出。谢谢!
这里主要是写概念,代码涉及的不多也不详细,但是我会说出涉及到的API,详细的使用细节大家可以自己动手搜搜。掌握下面的概念之后,看驱动开发的教材里的代码,或者理解教材里说的内容应该就顺利很多!
过滤驱动程序概括:
对于windows驱动程序设计来说,理论上,我们要做的就是创建设备对象(包括完成这个设备对象内部的功能、参数等),然后将这个设备对象绑定到我们要过滤的设备上。一个设备对应一个驱动对象,而一个驱动对象可以生成许多设备对象,这些设备对象是实现功能、完成任务的东西(我们在程序中反复玩弄和折磨的就是他们)。
驱动的层次结构架构:
------------------------------------------------------------------------------------------------------------------------------------------------
上层应用(调用系统API,希望完成一些工作)
------------------------------------------------------------------------------------------------------------------------------------------------
IO控制器(解析上层请求,向下层发IRP包)
------------------------------------------------------------------------------------------------------------------------------------------------
N层过滤驱动(拦截IRP包,做预处理)
------------------------------------------------------------------------------------------------------------------------------------------------
。。。
------------------------------------------------------------------------------------------------------------------------------------------------
目标设备(被过滤设备或者原始设备或者真实设备或者物理设备。。。)(接收IRP包,并按包的指示工作)
------------------------------------------------------------------------------------------------------------------------------------------------
重要概念:
1.设备栈:DeviceStack
设备栈是什么?里面装的是什么东西?怎么工作的?
在编写过滤驱动的过程中,我们每生成一个设备对象(代表一个过滤驱动),就一定会把它绑定到我们要过滤的目标设备上。那么如果把多个过滤驱动的对象绑定到同一个设备上是怎么绑呢?系统怎么管理他们呢?答案就是设备栈。原始的被绑定设备和往他上面绑的过滤设备们组合在一起就成了一个设备组。这个设备组会被系统管理成一个栈的结构,形成的就是设备栈。后绑定的设备原本希望绑最下面的目标设备,而实际上绑在了最外面,就像礼品包装纸一样,一层层往外裹。所以当有请求发给目标设备的时候,请求会按栈中的次序从栈顶(最外层)开始依次被栈中的过滤对象依次处理。最后发给目标设备。
相关的函数:
当新的设备想要加入设备栈(绑定目标设备)的时候,就会调用绑定函数:IoAttachDeviceToDeviceStackSafe,它第三个参数返回被绑定后的对象指针(其实根据上面的说明能看出来,这个这真就是栈顶)。在这个API的使用当中,大家会发现,一般把第三个参数保存到这个过滤驱动的设备扩展的AttachedToDeviceObject字段,那么通过这个字段是不是可以把IRP依次传递下去?当然,因为它代表当前过滤驱动下一层的设备。
绑定设备的三部曲:
第一步:创建设备(准备往其他设备上绑的设备),用IoCreateDevice,第一个参数是设备对象,过滤驱动中填的就是过滤设备。
第二步:为创建的设备设置标志位:把A绑定到B上,就要让AB的标识位一致(这样系统看起来,就以为自己发送IRP到目标设备了),所以创建了A后,要设置一些重要的标识位。
第三步:绑定:调用绑定函数进行绑定,绑上目标设备栈的栈顶设备(这一步结束后,可以通过我们创建的设备对象flag设置设备已经启动)
2.服务请求包:IRP和IRP栈
IRP
包里面是什么?怎么产生?怎么工作的?
包里面存放了接收设备要完成请求的工作所需要的一切信息~~~很大很复杂的一个结构体。在WDK的wdm.h中可以找到他。大家在使用的时候会发现,一般都只涉及到一些常用字段。
当用户程序在用户层(Ring3)调用一些系统API,比如(ReadFile)的时候,这些函数的请求最终会被IO控制器接收,然后发出IRP,这个IRP会被向下发送,然后一次又一次的被各种设备处理然后发送给下一个设备。在这个过程中,IRP包又可以被中间的处理设备修改。
IRP栈:
那么IRP栈中的内容是什么?系统怎么使用的这个栈进行工作的?
大家看一些教材的时候一定会发现在自己创建的设备过滤到IRP包,然后想对IRP包进行操作的时候,总是会访问IRP栈。上一段中说了,一个IRP的生命中,有可能会敬礼许多段和接收到他的设备的爱情,在这个过程中,IRP中的一些字段难免会不断被改变。而一个IRP栈就像是日记本,记录了IRP的改变过程,所以栈顶的肯定是现在最新的IRP。这就是为什么。。。看下面的函数:
IoGetCurrentIrpStackLocation(irp),每当我们想获得IRP的时候,就这样调用。仔细看这个api中的单词Current,就能猜到这个函数的意思就是访问IRP栈顶、最新状态的IRP(这个最新状态的IRP是经过上层设备各种修改的IRP)。
3.下面说下IRP中存的数据
IRP携带数据是通过携带数据所在内存区域的指针来实现的。这样的指针有三个:
UserBuffer,MDLAddress,SystemBuffer。不同的设备会用不同的指针去存要携带的数据。下面是三个指针的原理:
a.SystemBuffer是把用户层空间中的缓冲中的数据复制到内核层空间中使用,这种方式效率很低。
b.UserBuffer是把应用层的缓冲地址直接传递进来,供内核层使用。优点是系统实现的很快,传个地址就行,缺点:内核进程切换,当前的进程被切出去了,访问会结束。为什么会这样?内核空间是共享的,所以内核进程都用公共的空间,内核进程一旦切换,前一个进程遗留的数据肯定就被干掉了~
c.MDL方式是弥补了上面这个缺点,也就是说这个内存描述符表通过添加一个表项,永久的把一段应用层空间和内核空间一段虚拟空间绑定在了一起。这样相当于内核使用访问自己空间的地址,其实是在不知不脚的访问用户空间中的内容。因为这个访问方式是用MDL实现和管理的,所以就算内核当前进程发生了切换啥的,只要MDL表在(这不废话吗?),就永远可以访问到用户空间的那一段内容。而不是简单的把用户空间的一个指针作为值暂时的存到内核空间的一个变量中。
4.IRP的传递
IRP在驱动设备的工作中起着全程指导的作用,他会在一系列的设备中被转发,自己也会不断改变,那么大家经常遇到这个情况:对于一个过滤驱动来说,他拦截了IRP后,仔细的看了看这个IRP后,发现不是自己感兴趣的,就打算原封不懂得扔给下一层设备处理。在一些教材的示例代码中完成这功能的就一对好基友:
如下:
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(DeviceObject,Irp);
上面这对基友的工作机制可有讲究了:IoCallDriver单独工作时,效果是自动把irp栈中当前IRP包的下一个包发送到某个真实设备上。。。(因为这个函数认为之前的操作修改了IRP,并且把修改内容压入到IRP栈中新的栈顶地址上,所以让他来调用下一个设备工作,下一个设备收到的IRP也是目前设备处理后的新的IRP,从地址角度来说,就是当前IRP在IRP栈中的地址的下一个地址(新的栈顶)),而IoSkipCurrentIrpStackLocation写在IoCallDriver前面的效果是让IoCallDriver调用的下一层设备收到的IRP包就是本层设备受到的那个。这就完成了所谓的“跳过当前设备”的操作。【抱歉,杂家表达能力有限,是不是说的很晕?】
5.IRP请求的完成:
比较简单,调用一个函数:IoCompleteRequest 例程表示调用者的已经完成了对指定I/O请求的所有处理操作,并且向I/O管理器返回指定的IRP报文。
6.当然,过滤设备可以绑定也可以解绑Unload:
Unload设备没什么复杂代码,就是调用IoDeleteDeviceIRP。