分享

手把手跟我学驱动(2)

 创科之龙 2010-11-20

时间过得很快,离我的上一篇文章已经又过了一个星期,本想隔上三两天就开始着手写第二篇的,无奈很忙,也有很多东西要去学习。一个星期没写驱动了,不过对自己上个星期学习的过程到现在的记忆还很清晰,在上一篇文章里面,我提供了一个最基本最基本的驱动程序框架,无非就是一个“main”函数加上一些附属例程,事实上,就算是我们只有一个DriverEntry函数也是可行的,只是如果这样的话,你对它的所有请求(或者说是调用)都会失败,事实上,上个驱动本来就是什么也没做!上一篇文章另外一个内容就是,总结了使用WDK(DDK)与VS2008开发驱动的基本设置,想当初我的第一个驱动框架竟然会连接了kernel32.dll,这是一件多么可笑的事情,显然这样的“驱动”是绝对不可能被正确加载的,所以,如果哪位新手再遇到我这样的问题(这个时候,你连DriverEntry都进不去),请你检查一下你链接得到的目标文件使用了哪些输入库(姑且这么说吧),目前为止,我的驱动可能会用到ntoskrnl.exe和HAL.dll所提供的服务。

是啊,上个驱动什么也不干,可以说是一无是处。但是,据我最初听说的驱动,它主要就是提供一个PC机上的加入的新硬件的功能调用的模块,可以说是主要为应用程序提供基本的输入输出服务,比如说是一个打印机驱动,我现在在用WPS,那么我选择打印,打印机的驱动程序就负责把我WPS上的字传给打印机输出。我的概念最初也就仅限于这么多,再后来,我知道了文件系统驱动,等等,我所知有限就不在这儿贻笑大方了,再申明一点,我写这一系列的文章只是要把我学习的经验拿出来和那些和我一样初学的同志们交流交流,如此而已。好了,开始今天的内容吧。

1. IRP

首先我们得认识的一个东西就是IRP,中文说法是IO请求包,它的英文全称可能是I/O Request Package吧。一个说法是,MS的驱动框架是一个分层的驱动模型,也就是说,当一个用户层的应用程序发出一个IO请求的时候,由I/O管理器(这是一个内核基本组件,在《NT文件系统内幕》这本书里面有讲,只是我感觉不怎么好理解,所以我也没理解好,我只需要知道它给我们提供这些服务即可)将这个I/O请求按一个规定的模式打个包,里面有一些必须的参数,就是一个IRP。原则上,一个I/O请求,I/O管理器只会为我们创建一个IRP包。每个IRP包包括一个IRP头部和一个IO_STACK_LOCATION数组,这个数组的元素个数由这个IRP包即将经历的所有驱动层次来决定,每个数组元素对应一个可能(只所以说是可能,是因为,某个驱动会选择完成一个IRP,从而这个IRP便不便会结束向下一个驱动的传递)会收到这个IRP包的驱动,另一方面,这是一个由数组实现的一个栈,这个栈元素个数在IRP头部有记录。关于IRP我不想再多说,太多了我也说不清楚。另外一个值得一提的是,当一个IRP传递到一个驱动层次的时候,具体它是发给谁了呢?它是发给了这层驱动程序创建的DEVICE_OBJECT上来了,这就是我们的驱动的每一个IO例程的两个参数一个是DEVICE_OBJECT另一个是IRP的原因了,IRP是IO管理器提供的,这个DEVICE_OBJECT是这个IRP的目标设备(如果你的驱动创建了多于一个设备的话),目前,我们的驱动程序里面只是创建了一个虚拟的设备,所以,暂时不必考虑这个问题,不过,我相信可能大家看过别人的驱动里面都有一个IS_MY_DEVICEOBJECT这么一个宏,顾名思义就是判断是否是我们创建的设备对象的意思,加上也无防。

2.基本数据传输

 在今天,我想要说明的基本数据传输是使用ReadFile和WriteFile来进行用户程序于内核驱动之间的最最基本的数据传输问题。众所周知,32位系统中的可用地址空间最多可达4GB,不过,默认情况下,只有2GB是供应用程序使用,而另外的2GB是供操作系统内核使用的。对于供应用程序使用的2GB的地址空间,对不同的应用程序来说,尽管有相同的虚拟地址值,但实际对应的物理地址可以不同,具体内容就更可以不同了。但是,供操作系统内核使用的2GB内容对所有的应用程序来说都是相同的,只是对每个应用程序都是拒绝访问的。一般情况下,为了保护系统安全,内核驱动与应用程序中间不能直接进行数据传输,如果需要,只有IO管理器和虚拟内存管理器的介入才行(可能是这样)。那么,操作系统内核为我们的驱动程序与应用程序提供了三种数据传输方式,①是直接IO;②是缓冲IO;③两者都不IO(即非①非②)。由于Read和Write请求中数据的传输都是单方面的,所以,对于一种IO模式来说,数据传输的载体是用相同的方式使用的。

直接IO:当采用直接IO是使用一个系统内核分配的一个MDL来完成的,所谓的MDL就是一个内存描述符,它的细节我们可以参考相关资料,事实上我也解释不清楚,但有一点我可以肯定的是,它可能是因为直接指向对应的物理内存,所以相应的IO类型被称做直接IO。在驱动里,我们可以使用MmGetSystemAddressForMdlSafe函数来返回一个和我们常用的线性地址一样的地址来供我们使用,它实际上是一个使用了MmMapLockedPagesSpecifyCache宏,使用它锁住了物理内存从而使得在它有效期间不会被换出物理内存从而保证了以后的访问不会引发缺页异常而损失效率。而系统可用的像这样的物理内存是相当有限的,所以对它的使用应该采用保守的态度。而说法说,一般情况下我们应该避免使用直接IO,除非迫不得已。当使用直接IO时,在需要IO操作时,IO管理器会分配一个相关的MDL放在IRP的MdlAddress成员中供我们的IRP例程使用。

缓冲IO:这种IO方式和名字很像哦,系统会分配一个与应用程序传入的地址不同的地址,当写入(比如WriteFile)时,系统自动将用户传入的数据复制到这个缓冲区传入IRP例程,当是读出(如ReadFile)时,系统在这次IO操作完成并且在返回应用程序之前,将这个缓冲区的数据复制到应用程序的接收缓冲区中。这个缓冲区地址保存在IRP->AssociatedIrp->SystemBuffer成员中。

两者都不IO:这是一种和以上两种IO方式都不同的操作,IO管理器不会为我们创建任何缓冲区或MDL,而是,直接传入应用程序提供的接收或是写入地址。这里有点更应该命名为直接IO,对不?但是,已经这样命名了,我们就应该搞清楚它和真正的直接IO的区别,直接IO锁定了实际的物理内存而使之不会被换出物理内存,而此处的两者都不IO则没有这个操作。这样,这个用户程序传入的地址值保存在IRP结构的UserBuffer域中。

这里所说的IO方式仅限于IRP_MJ_READ和IRP_MJ_WRITE例程,并且,只能使用上面的三者之一。并且,这个IO方式是对应于DEVICE_OBJECT的,很容易理解,每个IRP例程都只接收PDEVICE_OBJECT和PIRP两个参数,我们创建的设备使用哪一种IO方式在设备创建后初始化中指定,当一个IRP包发送给设备时,IO管理器会检查对应设备需要的IO方式而采用符合相应IO方式的操作。这个IO方式在DEVICE_OBJECT的Flags域中指定。不管是读写,都有读写的字节长度,这个长度保存在当前对应的驱动栈中,在代码中会有注释。好了,不多说了,我们来试一试上面的说法(有一点小问题,注意看清楚了)。

3. Example2驱动实现

我们还是使用和上一例中相同的方式,我只是在(1)的基础上做出扩充,先复制相关的代码吧。主框架代码和Example1没有太大区别,只是添加了我们这次将要加入的IRP_MJ_READ例程,另外,在我们的设备创建成功之后,还应该为它设定READ和WRITE使用的IO方式,其它的都在主程序了.当然,和Example1一样,我没有将这些代码分开在若干文件中,这是为了方便我往这里贴(嘿嘿),以后,代码逐渐增多的时候,会分开的。下面是代码部分:

#include <wdm.h>

//因为只支持三种IO方式,所以,下面两个都不定义的话,就是使用NEITHER_IO了

//#define EXAMPLE2_BUFFERED_IO

#define EXAMPLE2_DIRECT_IO

//定义IO方式的选取及相及的例程

#ifdef EXAMPLE2_DIRECT_IO

#define EXAMPLE2_IO_TYPE DO_DIRECT_IO

#define Example2Read Example2DirectRead

#elif defined(EXAMPLE2_BUFFERED_IO)

#define EXAMPLE2_IO_TYPE DO_BUFFERED_IO

#define Example2Read Example2BufferedRead

#else //defined(EXAPME2_NEITHER_IO)

#define EXAMPLE2_IO_TYPE 0

#define Example2Read Example2NeitherRead

#endif

NTSTATUS Example2DirectRead(PDEVICE_OBJECT pDevObj,PIRP pIrp);

NTSTATUS Example2BufferedRead(PDEVICE_OBJECT pDevObj,PIRP pIrp);

NTSTATUS Example2NeitherRead(PDEVICE_OBJECT pDevObj,PIRP pIrp);

//驱动入口函数

NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj,PUNICODE_STRING pUsRegPath)

{

 NTSTATUS status = STATUS_UNSUCCESSFUL;//初始化为不成功

 UNICODE_STRING usDevName;//我们的设备名

 UNICODE_STRING usDosDevName;//Dos符号链接

 PDEVICE_OBJECT pDevObj = NULL;//设备对象

 unsigned int nIndex;//一个计数器,循环的时候可以用到

 DbgPrint("Example2: Driver entry is called.\n");

 //初始化两个Unicodestring

 RtlInitUnicodeString(&usDevName,L"\\Device\\Example2");

 RtlInitUnicodeString(&usDosDevName,L"\\DosDevices\\Example2");

 //现在我们创建设备

 status = IoCreateDevice(pDrvObj,0,&usDevName,FILE_DEVICE_UNKNOWN,

  FILE_DEVICE_SECURE_OPEN,FALSE,&pDevObj);

 //只有成功创建设备我们才有必要继续

 if(NT_SUCCESS(status)){//测试成功与否一般用这个宏,而不是让它直接与STATUS_SUCCESS比

//较,查看一下这个宏定义就知道了

  //成功创建设备之后,我们要作的一件事就是初使化驱动的IRP例程,现在,我们的驱动什么都不   //干,所以,每个例程也还是什么都不做

  //每个设备最多有IRP_MJ_MAXIMUM_FUNCTION个IRP例程,现在,我们都把它们初始//化成   //一个相同的入口

  for(nIndex=0;nIndex<IRP_MJ_MAXIMUM_FUNCTION;++nIndex)

   pDrvObj->MajorFunction[nIndex] = Example2IrpRoutine;

  //======现在我们已经提供了实质性的Read例程,所以,不使用默认例程了

  pDrvObj->MajorFunction[IRP_MJ_READ] = Example2Read;

  //接下来,我再安装一个卸载例程

  pDrvObj->DriverUnload = Example2Unload;

  //把创建的设备保存起来吧,否则以后便不能引用啦

  pDrvObj->DeviceObject = pDevObj;

  //====下面这一行代码是在Example1的基础上添加的,指定我们的读写例程使用的IO方式

  pDevObj->Flags |= EXAMPLE2_IO_TYPE;

  //好了,创建符号链接,

  status = IoCreateSymbolicLink(&usDosDevName,&usDevName);

  if(!NT_SUCCESS(status)){

   IoDeleteDevice(pDevObj);//删除创建的设备对象

  }

 }

 return status;//

}

NTSTATUS Example2DirectRead(PDEVICE_OBJECT pDevObj,PIRP pIrp)

{

 NTSTATUS status = STATUS_UNSUCCESSFUL;

 PIO_STACK_LOCATION pIoStack = NULL;

 unsigned int nBytesRead = 0;

 charpUserBuffer = NULL;//用来

 charszFromDriver = "This is the string data from Example2 driver:DirectIO.";//用户程序的读请求将从此      //串中填充

 DbgPrint("This is calling of Example2 driver using directIo.\n");

 pIoStack = IoGetCurrentIrpStackLocation(pIrp);

 if(pIoStack!=NULL&&pIrp->MdlAddress!=NULL){//因为直接IO使用系统提供的Mdl数据,所以,必须           //检查这个值,

  //如果对一个空的MdlAddress地址使用以下操作,而我们又没采取其它防范措施,将会导致一次   //蓝屏

  pUserBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);

  //如果以上操作成功执行,pUserBuffer应该是一个可用的线性虚拟地址

  if(pUserBuffer!=NULL){

  //现在是时候检查用户接收缓冲区长度了

  nBytesRead = pIoStack->Parameters.Read.Length;//可见,如果是写的话,此处应该是       //Parameters.Write.Length

  if(nBytesRead>56)//因为我们的内核数据源只有56个字节数据

   nBytesRead = 56;

  //好了,可以复制了

  RtlCopyMemory(pUserBuffer,szFromDriver,nBytesRead);

  status = STATUS_SUCCESS;//表示操作成功完成

  }//end if pUserBuffer!=NULL

 }

 //好了,该返回给用户程序一些相关的信息了,这些信息保存在IRP的IoStatus域中

 pIrp->IoStatus.Status = status;//操作成功还是失败

 pIrp->IoStatus.Information = nBytesRead;//读写操作完成长度

 //现在,还应该通知IO管理器,这个IRP已经完成,不再继续处理

 IoCompleteRequest(pIrp,IO_NO_INCREMENT);

 return status;

}

NTSTATUS Example2BufferedRead(PDEVICE_OBJECT pDevObj,PIRP pIrp)

{

 NTSTATUS status = STATUS_UNSUCCESSFUL;

 PIO_STACK_LOCATION pIoStack = NULL;

 unsigned int nBytesRead = 0;

 charpUserBuffer = NULL;//用来

 charszFromDriver = "This is the string data from Example2 driver:BufferedIo.";//用户程序的读请求将从      //此串中填充

 DbgPrint("This is calling of Example2 driver using BufferedIo.\n");

 pIoStack = IoGetCurrentIrpStackLocation(pIrp);

 pUserBuffer = pIrp->AssociatedIrp.SystemBuffer;//这是系统为我们准备的缓冲区,我们在它上面操作即    //,.此IO完成后,系统会自动将其中的数据复制给用户程序

 if(pIoStack!=NULL&&pUserBuffer!=NULL){//我们必须确保不在空地址上操作,

  //现在是时候检查用户接收缓冲区长度了

  nBytesRead = pIoStack->Parameters.Read.Length;//可见,如果是写的话,此处应该是       //Parameters.Write.Length

  if(nBytesRead>58)//因为我们的内核数据源只有个字节数据

   nBytesRead = 58;

   //好了,可以复制了

   RtlCopyMemory(pUserBuffer,szFromDriver,nBytesRead);

   status = STATUS_SUCCESS;//表示操作成功完成  

 }

 //好了,该返回给用户程序一些相关的信息了,这些信息保存在IRP的IoStatus域中

 pIrp->IoStatus.Status = status;//操作成功还是失败

 pIrp->IoStatus.Information = nBytesRead;//读写操作完成长度

 //现在,还应该通知IO管理器,这个IRP已经完成,不再继续处理

 IoCompleteRequest(pIrp,IO_NO_INCREMENT);

 return status;

}

NTSTATUS Example2NeitherRead(PDEVICE_OBJECT pDevObj,PIRP pIrp)

{

 NTSTATUS status = STATUS_UNSUCCESSFUL;

 PIO_STACK_LOCATION pIoStack = NULL;

 unsigned int nBytesRead = 0;

 charpUserBuffer = NULL;//用来

 charszFromDriver = "This is the string data from Example2 driver:NeitherIo.";//用户程序的读请求将从      //此串中填充

 DbgPrint("This is calling of Example2 driver using NeitherIo.\n");

 pIoStack = IoGetCurrentIrpStackLocation(pIrp);

 pUserBuffer = pIrp->UserBuffer;//这是应用程序提供的接收缓冲区

 __try{

  if(pIoStack!=NULL&&pUserBuffer!=NULL){//我们必须确保不在空地址上操作,

   //缓冲区长度

   nBytesRead = pIoStack->Parameters.Read.Length;

   //因为这个pUserBuffer是用户程序传来的,我们得检查它的读写属性,用ProbeForWrite检查是   //否可写,如果不可写,将引发一个异常,注意,如果你使用用户程序提供的地址读写,所以你一   //定得记住使用异常机制,否则,你志遭遇的将不是一个遇到问题需要关闭的错误,而是一个   //恐怖的蓝屏!

   ProbeForWrite(pUserBuffer,nBytesRead,TYPE_ALIGNMENT(char));

   //现在是时候检查用户接收缓冲区长度了

   if(nBytesRead>57)//因为我们的内核数据源只有57个字节数据

    nBytesRead = 57;

   //好了,可以复制了

   RtlCopyMemory(pUserBuffer,szFromDriver,nBytesRead);

   status = STATUS_SUCCESS;//表示操作成功完成

  }

 }__except(EXCEPTION_EXECUTE_HANDLER){

  status = STATUS_UNSUCCESSFUL;

  DbgPrint("An exception occurred:Example2NeitherRead.\n");//打印异常发生这个事实

 }

 //好了,该返回给用户程序一些相关的信息了,这些信息保存在IRP的IoStatus域中

 pIrp->IoStatus.Status = status;//操作成功还是失败

 pIrp->IoStatus.Information = nBytesRead;//读写操作完成长度

 //现在,还应该通知IO管理器,这个IRP已经完成,不再继续处理

 IoCompleteRequest(pIrp,IO_NO_INCREMENT);

 return status;

}

在上面的代码中,和Example1几乎完全相同的例程我就没有重复了。OK,现在可以写用户态的测试代码了,下面贴出我的一个很简单的测试代码,

int main(int argcchar *argv[])

{

 HANDLE hFile = INVALID_HANDLE_VALUE;

 DWORD dwRet = 0;

 hFileCreateFile(_T("\\\\.\\example2"),GENERIC_READ|GENERIC_WRITE,

FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL);

 if(hFile!=INVALID_HANDLE_VALUE){

  char readData[128];

  DWORD dwReadBytes = 0;

  if(ReadFile(hFile,readData,128,&dwReadBytes,NULL)){

   readData[dwReadBytes] = '\0';

   printf("%s\n",readData);

  }

  CloseHandle(hFile);

 }

 return 0;

}

4. Example2驱动调试

我们用一段简单的代码,测试了Example2的IRP_MJ_READ例程的正确性。很好,如果代码什么地方出现问题,我们还需要调试,那,接下来再介绍一下我所使用的最基本的调试,我建议在虚拟机里面调试,这样比较安全可靠,我使用的VMware,调试器肯定就用WinDbg了啊。至于驱动环境的设置,我也不说了,百度上随便一搜就是一大把。这里,我要说的是,我们生成的sys文件,用调试版,因为这样WinDbg才能根据调试符号。当在虚拟系统里面加载驱动之后,就在WinDbg里面暂停虚拟系统,使用!drvobj example2 2来查看这里的example2加载后的信息,现在要调试的是Read例程,所以,找到IRP_MJ_READ例程的地址,使用bp命令就在这个地方设下了断点,当然,也可以用合函数名下断,得靠你对WinDbg的学习了。更多的东西吧,还需要查看更多的文档,我这里所介绍的,只是一些最基本最基本的东西。

5.实验

在我的代码里面,我没有加入IRP_MJ_WRITE的实现,有兴趣的自己实现一个试试吧!

6.总结

在本例中,我主要介绍了驱动在Read和Write处理时采用的三种不同的IO方式,并给出了Read例程使用这三种方式的样例代码,不过,我们还知道,用户层的应用程序不仅仅可以使用ReadFile和WriteFile同内核驱动进行数据交换,并且这两个函数的数据流向还局限于单方向的,如果用过DeviceIoControl我们会发现,DevicdIoControl同样也能与内核驱动进行数据交换,更重要的是,它的数据流向还可以是双向的!那么,在下一篇中,我们将学习DeviceIoControl的响应实现。现在,可以放松一下了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多