[转]BDA驱动学习笔记

 
BDA驱动学习笔记(4):IRP 
  

NT中的驱动采用分层结构,一个应用层的IO命令需要通过IO子系统,IO系统服务层,若干层的驱动,最后才能到达硬件,硬件有什么数据需要返回,也需要经过这些层,一点都不能省。某一层的驱动只能和自己相邻层的驱动联系,而且联系都要通过IO Manager,用一个名叫IRP的数据结构完成通信。IRP基本上是NT驱动架构中最重要的一个数据结构了,哪儿都能看到它,哪儿都需要用到它。为了对整个分层架构有个大体的了解,我们先来看一个图,这个图在DDK中也可以看到,是某个文件操作的详细过程图,看起来好像和我们的视频采集没多少关系,但实际上大体步骤还是一样的。

 
  

1. IO子系统调用IO服务组件,要求打开一个文件。

2. IO manager调用object manager寻找文件,并调用安全组件来查看该调用者是否真的有权限

3. IO manager分别在文件系统和磁盘上寻找相关的文件实体,如果没找到,操作挂起;如果找到了,则继续。

4. IO manager生成一个IRP包,初始化一些数据

5. IO manager找到file system driver,并往它传入先前生成的IRP。IRP中有一个栈,指定了哪一层做哪些操作。刚传入的IRP不一定有一个完整的栈信息,因为驱动程序在处理过程中,可以修改它下面层的驱动对应的IRP栈信息。实际上跨层操作并不好,每一层的栈信息基本都是上一层给指定的,而不是一开始指定。驱动也可以把一个命令分成好几个小命令,分别往底层传。

6. 所有层上的驱动都完成自己应该做的工作

7. 所有层上的驱动分别调用complete operation。complete operation是上一层驱动指定的,表示让下层驱动完成操作后就调用该方法,好让它做一些确定是否成功,回收资源之类的操作。

8. 把IO STATUS拷贝到子系统空间中,好让最顶部的调用者知道调用结果

9. IO manager 清空IRP

10. IO manager把文件句柄返回给调用者

IRP中有一堆的数据结构,比较重要的有如下几个
IRP主要数据项 
说明 

IO_STATUS_BLOCK IoStatus 
存放I/O请求的状态

PVOID AssociatedIrp.SystemBuffer 
如果设备执行缓冲I/O,则为指向系统空间缓冲区的指针。 否则为NULL

PMDL MdlAddress  
如果设备执行直接I/O,指向用户空间缓冲区的内存描述表的指针

PVOID UserBuffer 
I/O缓冲区的用户空间地址 

BOOLEAN Cancel 
  指示IRP已被取消 


 

访问硬件的任何设备必须使用某种机制保证驱动程序的不同部分不同时访问相同的硬件。在一个多处理器系统中,“Write”IRP处理程序可以同时在两个不同的处理器上运行。如果它们两个都试图访问相同的硬件,则会出现不可预料的结果。同样,如果一个“Write”IRP正在试图访问硬件的同时发生了中断,那么,两个动作可能会相互影响。 

内核采用两种机制来同步这些冲突操作:

第一种是采用临界段例程,使用这些临界段例程保证代码不会被中断处理程序中断。这些临阶段例程在内部使用了中断自旋锁,所以可以保证多处理器同步。

第二种是使用StartIo例程串行处理IRP,每个设备对象有一内部的IRP队列,驱动程序的派发例程将IRP插入这个队列中。内核I/O管理器从这个队列一个个的取出IRP,并把它们传递到驱动程序的StartIo例程。所以StartIo例程串行的处理IRP,保证不与其它的IRP处理例程冲突。 

如果一个IRP已经在一个队列中,此时用户线程突然中止或其调用Win32函数取消了这次I/O,驱动程序必须取消这个IRP。这可以通过给每一个排队的IRP挂接一个取消回调例程来实现。 

如果用户态程序关闭了设备的文件句柄,而这个设备有重叠请求在等待,则必须要有“清理”例程。清理例程负责取消与一个文件句柄关联的所有IRP。 

BDA驱动学习笔记(5):APP和Driver的通信 

  

上层和驱动通信用DeviceIoControl函数,这是一个Win32 API,在SDK中定义。这个函数都会产生一个IRP_MJ_DEVICE_CONTROL包,如果驱动中注册过相应的例程,那么这个包就会引发该例程的工作。如果是驱动和驱动间的通信,那么用IoBuildDeviceControlRequest函数,该函数在DDK中定义,会产生一个IRP_INTERNAL_DEVICE_CONTROL包,并引发相应的例程。这两个IRP包中都有一个非常重要的结构叫IOCTL(io control code),用于指定通信中的各类细节。该数据结构是一个32比特的数据块,有6个区域,每个区域包含一类信息。IOCTL的结构如下图所示



  

DDK中有一个CTL_CODE宏,用这个宏我们可以很方便的定义IOCTL。不管是IRP_MJ_DEVICE_CONTROL还是IRP_INTERNAL_DEVICE_CONTROL包,IOCTL都用如下形式定义:

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

DeviceType:设备类型,和DEVICE_OBJECT结构中的DeviceType必须一致。注意:0x8000以下的数字被微软占用了。

Function Code:功能代码,可以自定义,用来区分操作类型。注意:0x800以下的数字被微软占用了。

Method:IO缓冲类型,有METHOD_BUFFERED,METHOD_IN_DIRECT,METHOD_OUT_DIRECT,METHOD_NEITHER四种类型。

METHOD_BUFFERED表明输入输出都用系统缓冲,这种策略下输入输出指向的是同一个内存块,该内存块有IO Manager管理。输入的时候把数据拷贝到缓冲中,然后缓冲再拷贝到驱动;输出的时候数据拷贝到缓冲中,然后缓冲拷贝到用户空间。由于用的是同一块缓冲,所以调用者自己得管理好里面的数据,防止弄混。缓冲区地址存放在IRP.AssociatedIrp.SystemBuffer中,输入数据大小为Parameter.DeviceIoControl.InputBufferLength,输出数据大小为Parameter.DeviceIoControl.OutputBufferLength,两者都在IO_STACK_LOCATION结构中。

METHOD_IN_DIRECT表明输出用缓冲,输入用直接IO。这种策略下输出和上面的方法一致,而输入则是直接访问指定的内存区域,不通过缓冲。IOManager先把输入数据的内存块锁定,然后把地址存放在IRP.MdlAddress中。输入输出数据块的大小和上面一致。

METHOD_OUT_DIRECT表明输入用缓冲,输出直接IO。IO Manager把输出数据的内存快锁定,存放在IRP.MdlAddress中,驱动直接通过该地址访问数据,输入数据通过系统缓冲,存放在IRP.AssociatedIrp.SystemBuffer中。输入输出数据块的大小和上面一致。

METHOD_NEITHER表明输入输出都不用缓冲,I/O Manager把调用者的输入缓冲区的地址放到IRP当前I/O堆栈单元的Parameters.Devi ceIoControl.TypeInputBuffer域中,把输出缓冲 区的地址存放到IRP的UserBuffer域中。这两个地址都是用户空间地 址。 

从上面的说明可以看出,在执行缓冲I/O时,I/O管理器将在非份页池 中分配内存,如果调用者的缓冲区比较大时,分配的非份页池也将 比较大。非份页池是系统比较宝贵的资源,因此,如果调用者的缓 冲区比较大时,我们一般采用直接I/O的方式(例如磁盘读写请求等), 这样不仅节省系统资源,另一方面由于省去了I/O管理器在系统缓冲 区和调用者缓冲区之间的数据拷贝,也提高了效率,这对存在大量 数据传送的驱动程序尤其明显。不过需要注意的是,直接io要求驱动和IOCTL的发起者运行在同一个线程里。

Access:指明调用者的访问权限,有FILE_ANY_ACCESS,FILE_READ_DATA,FILE_WRITE_DATA三个选项可选。FILE_ANY_ACCESS表明用户拥有所有的权限,FILE_READ_DATA表明权限为只读,FILE_WRITE_DATA表明权限为可写。FILE_WRITE_DATA | FILE_READ_DATA表明权限为可读可写,但还没达到FILE_ANY_ACCESS的权限。

用户定义IOCTL时要注意以下几条原则:

1. FunctionCode总是定义成0x800以上的数字,因为0x800以下的数字被微软占用了。

2. 仔细考虑访问权限,如果指定了你不具备的权限,那么IO Manager会忽略IOCTL

3. 仔细考虑要访问的内存区域,如果去读写一个关键内存,那么系统会重启

驱动内部执行IOCTL时要注意以下几条原则:

1. 接收到IOCTL时,要先检查整个32比特的数据完整性

2. 用IoValidateDeviceIoControlAccess检查访问权限是否有效

3. 严格遵照Parameter.DeviceIoControl.InputBufferLength和Parameter.DeviceIoControl.OutputBufferLength指定的大小访问输入输出区域,否则系统会重启

4. 驱动中申请一块内存后,总是先用RtlZeroMemory清空区域

5. 直接io策略中,用MmGetSystemAddressForMdlSafe获取相应内存区域时,要判断是否为NULL

6. 直接io中,用ProbeForRead 和ProbeForWrite检查内存是否可以访问。

 

 
BDA驱动学习笔记(6):错误处理,内存管理以及字符串 

  

错误处理:错误处理分为状态代码返回,异常处理和bug check三种,第三种(bug check)也就是我们在98系统里经常见到的蓝屏,nt系统里不常见但也会发生,真是很让人讨厌。不过万一驱动代码执行过程中发现了及其严重的错误,那么给个蓝屏应该是最好的选择,因为既然是“及其严重”的错误,继续执行下去操作系统会被搞坏。

状态代码NTSTATUS是一个32位的整数,表征代码执行成功与否,它的结构如下:

 



Sev表示严重程度,C表明该状态代码要被原封不动的传回给用户,Facility指出错误由哪个部件产生,Code保存了错误信息。一般而言是否执行成功应该用NT_SUCCESS宏来判断,直接看NTSTATUS是否为0是不对的。

驱动开发中也可以用类似try catch的方法处理异常,不过驱动中的异常处理机制和c++中的机制并不是同一套。而且值得注意的是,开发过程中要严格区分异常和错误,不能吧错误当异常,也最好别把异常当错误。按照我的理解两者的差别是,错误出现后程序就应该退出了,而异常出现的时候你还有补救的机会。

驱动程序中用保存在程序堆栈里的异常帧来处理异常。每次异常发生的时候,系统扫描堆栈,找到异常帧。异常帧中保留了一个过滤程序和一个异常处理程序。如果过滤程序中指定的条件都符合,那么异常处理程序被执行到,否则就跳过。如果没有过滤程序,系统会指派一个默认的;如果没有异常处理程序,系统也会指派一个默认的动作,那就是死机。

驱动程序中的__try , __except和__finally关键字的意义和c++中的差不多。__finally块中的代码无论如何都会被执行一遍,即使你的__try块中有return或者goto这样的语句。__except语句中要求有过滤代码,一般是以下三个之一:

EXCEPTION_EXECUTE_HANDLER:告诉系统要执行异常处理程序。如果处理程序中有return或者goto,那么接下来要执行的是__except块之后的代码,而不是返回或者跳转到指定地点。这一点很奇怪,DDK中的描述并非如此,而某本书的作者信誓旦旦的说他做过实验,他是对的,DDK写错了。我选择相信做过实验的人。

EXCEPTION_CONTINUE_SEARCH:告诉系统继续找下一个异常处理程序。如果找不到,系统崩溃。

EXCEPTION_CONTINUE_EXECUTION:告诉系统返回到发生异常的地方继续执行。一般来讲没什么意义,除非你确实可以改变出异常的情况,让异常消失。

值得注意的是,算术异常(除0),页故障和非法指针都不能用异常处理机制处理,其实它们都算是错误。并且能用NTSTATUS处理掉的事情,尽量不要用异常,因为异常处理机制非常耗资源。

bug check是调用KeBugCheckEx()函数来实现的,这个函数从来不返回,因为已经蓝屏了。

我们都知道NT中的内存分为两块,按照安全性和完整性分可以分为用户模式地址和内核模式地址,按照分页能力分可以分为分页内存和非分页内存。内核模式驱动中不能随便使用用户模式下的内存,因为用户传给你一个地址是虚拟地址,指向的内容不定是在内存里还是在磁盘上,如果贸然访问,会引发一个页错误。用户模式可以随便访问用户模式,因为技术指向的内容在磁盘里,虚拟内存组件也会帮你把它倒回到内存中,内核模式就没那么好运了,它什么事情都得自己干。

内核模式内存分为分页内存和非分页内存两种,用户模式全是分页内存,是没权限访问非分页内存的。非分页内存区域永远在内存里,不会被倒到磁盘上去,所以它是一种很宝贵的资源,内存容量怎么说也比磁盘小太多。你可以用alloc_text编译指令指示某个程序段运行在分页程序中,比如#pragma alloc_text(PAGE, AddDevice)指明AddDevice程序段要运行在分页内存里,如果没有指明,那么默认是在非分页内存中。同样的编译指令还有data_seg(“PAGE”),指明让变量存放在分页内存里;code_seg(“PAGE”),指明让程序段运行在分页内存里。记得要用data_seg()和code_set()回复默认行为。

内核中申请动态内存的方法是ExAllocatePoll(type, size),和malloc差不多,不过这里需要指定申请内存的类型,常见的类型如下

内存池类型 
描述

NonPagedPool 
从非分页内存池中分配内存

PagedPool 
从分页内存池中分配内存

NonPagedPoolMustSucceed 
从非分页内存池中分配内存,如果不能分配则产生bugcheck

NonPagedPoolCacheAligned 
从非分页内存池中分配内存,并确保内存与CPU cache对齐

NonPagedPoolCacheAlignedMustS 
与NonPagedPoolCacheAligned类似,但如果不能分配则产生bugcheck

PagedPoolCacheAligned 
从分页内存池中分配内存,并确保内存与CPU cache对齐



注意,申请来的内存至少应该是8字节对齐的。

释放动态内存用ExFreePool(PVOID),动态内存的申请释放千万要小心对付,内核里有内存泄露那是很可怕的事情。另一个申请动态内存的函数是

ExAllocatePoolWithTag(type, size, tag); 和ExAllocatePoll不同的是,这个函数还需要指定一个标签,放在申请来的内容的开头。微软建议我们一直使用ExAllocatePoolWithTag,为申请来的内存加上我们自己的标签。

驱动中开提供了一组数据结构以方便我们保存数据。Stl中的list,queue等数据结构不能随便用在驱动开发中,很危险,相反的我们应该一直使用内置的数据结构。以链表为例,内核中提供了一个LIST_ENTRY的数据结构好让我们生成需要的链表。这个结构只是用来构造链表和链接链表用的,链表中存放什么数据还得自己定义。这里有个很奇特的地方在于,你创建一个链表Node结构后,LIST_ENTRY是作为它的子项存放的,而并不是LIST_ENTRY中有一个指针可以指向我们的数据块。比如下面这个Node:

Struct Node

{

Int value;
}

如何把这个Node放到LIST_ENTRY里?正确的做法是

 

Struct Node

{

Int value;

LIST_ENTRY linkField;
}

而不是什么类似linkField->Data = node之类的操作。
生成的list里保存的都是LIST_ENTRY,而想通过LIST_ENTRY访问到它所在的Node,则需要使用CONTAINING_RECORD宏。 比如:
Node psElement = (Node) CONTAINING_RECORD(psLink, Node, linkfield);

这里顺便要提一下,if或者else语句之后的操作最好用大括号括起来,因为操作到底是函数还是宏定义你是没办法确定的,万一是宏,那就会造成不必要的麻烦。

内核中操作字符串也有自己的一套机制,各函数说明如下:

操作 
ANSI串函数 
Unicode串函数

Length 
strlen 
wcslen

Concatenate 
strcat, strncat 
wcscat, wcsncat, RtlAppendUnicodeStringToString, RtlAppendUnicodeToString

Copy 
strcpy, strncpy, RtlCopyString 
wcscpy, wcsncpy, RtlCopyUnicodeString

Reverse 
_strrev 
_wcsrev

Compare 
strcmp, strncmp, _stricmp, _strnicmp, RtlCompareString, RtlEqualString 
wcscmp, wcsncmp, _wcsicmp, _wcsnicmp, RtlCompareUnicodeString, RtlEqualUnicodeString, RtlPrefixUnicodeString

Initialize 
_strset, _strnset, RtlInitAnsiString, RtlInitString 
_wcsnset, RtlInitUnicodeString

Search 
strchr, strrchr, strspn, strstr 
wcschr, wcsrchr, wcsspn, wcsstr

Upper/lowercase 
_strlwr, _strupr, RtlUpperString 
_wcslwr, _wcsupr, RtlUpcaseUnicodeString

Character 
isdigit, islower, isprint, isspace, isupper, isxdigit, tolower, toupper, RtlUpperChar 
towlower, towupper, RtlUpcaseUnicodeChar

Format 
sprintf, vsprintf, _snprintf, _vsnprintf 
swprintf, _snwprintf

String conversion 
atoi, atol, _itoa 
_itow, RtlIntegerToUnicodeString, RtlUnicodeStringToInteger

Type conversion 
RtlAnsiStringToUnicodeSize, RtlAnsiStringToUnicodeString 
RtlUnicodeStringToAnsiString

Memory release 
RtlFreeAnsiString 
RtlFreeUnicodeString



提醒一句,千万小心区分Ansi和Unicode。

内核中操作大内存块也有自己的一套,具体说明如下:

服务函数或宏 
描述

memchr 
在blob中寻找一个字节

memcpy, RtlCopyBytes, RtlCopyMemory 
复制字节,不允许重叠

memmove, RtlMoveMemory 
复制字节,允许重叠

memset, RtlFillBytes, RtlFillMemory 
用给定的值填充blob

memcmp, RtlCompareMemory, RtlEqualMemory 
比较两个blob

memset, RtlZeroBytes, RtlZeroMemory 
blob清零



 
 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值