驱动开发之三 --- IPC 【译文】

驱动开发之三 --- IPC 【译文】

转自 http://hi.baidu.com/combojiang/blog/item/29913ccd07c6e2540fb345a4.html

理论:

这是第三篇介绍编写驱动的文章。 第一篇帮助你建立了一个简单的驱动程序,并且形成了一个开发驱动程序的简单框架。第二篇我们介绍了怎样使用IOCTLs并且展示了windows nt的内存布局。在本篇中,我们将深入到环境上下文和池中。我们今天写的驱动多少有点意思,我们通过驱动实现了两个用户模式的应用之间的通讯。如图所示。

 

什么是上下文?

这是一个常见的问题,如果你在windows下编程,你就应该理解这个概念。context是一种用户定义的数据结构,这里说的用户是指开发者,系统并不清楚这种结构是什么。系统做的是把context为用户传递到周围,所以在一个事件驱动的系统里,你不需要去使用全局变量或是去了解请求被发送到什么对象,实例、数据结构等等。

在windows里,有一些使用上下文的例子,像SetWindowLong使用GWL_USERDATA的情况,还有如:EnumWindows,CreateThread等等。他们都允许你传递一个上下文参数,在你的应用程序中使用这个上下文参数,可以仅仅用一个函数来实现区分和实现多个功能实例。

设备上下文

回顾第一篇文章,我们学习怎样在我们的驱动中创建一个设备对象。在内存中驱动对象包含着有关这个驱动物理实例的相关信息,每一个驱动程序对应一个驱动对象,驱动对象包含着如MajorFunction等等的信息。一个驱动对象可以使用IoCreateDevice创建多个相关联的设备对象。这也是为什么所有的驱动MajorFunction都传递一个设备对象参数,而不是驱动对象参数,所以你可以确定MajorFunction中的函数调用的是哪个设备的。在设备对象中包含着驱动对象的指针,所以你可以通过设备对象找到驱动对象。

NtStatus = IoCreateDevice(pDriverObject, sizeof(EXAMPLE_DEVICE_CONTEXT),  

                     &usDriverName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN,  

                     FALSE, &pDeviceObject);

......

     /*

     * Per-Device Context, User Defined

     */

pExampleDeviceContext =   (PEXAMPLE_DEVICE_CONTEXT)pDeviceObject->DeviceExtension;   

KeInitializeMutex(&pExampleDeviceContext->kListMutex, 0);
     pExampleDeviceContext->pExampleList   = NULL;


IoCreateDevice函数包含了一个参数,用于描述Device Extension的大小,这个可以用于创建设备对象中的DeviceExtension成员,DeviceExtension代表用户定义的上下文。你可以创建自己的的数据结构作为参数传递进去。如果你在一个驱动中创建了多个设备对象,你可以在所有的设备上下文中设置一个共享成员,用于快速的区分当前哪个设备的这个功能被调用。设备描述为\Device\Name。

对于指定的设备,这个上下文通常包括可以被搜寻的任何类型的链表或者属性和锁。磁盘驱动上的每一个设备的全局数据实例在自由空间。 假设你有三个设备,分别代表三个不同的磁盘驱动映像。对于某个特定的设备独有的属性,对于一个设备的每一个实例是全局的。例如,卷名,自由空间,已用空间等等都是对应于每一个设备的属性,但是对于设备的所有实例来讲,都是全局的。

资源上下文

你可以使用一个由设备自身管理的描述资源的长字符串来打开一个设备。在这样的文件系统下,你可以这样描述一个文件名和文件路径。 例如,可以使用 \Device\A\Program Files\myfile.txt 打开一个设备。然后驱动程序会需要你打开这个资源的所有进程分配一个全局的上下文。

在这个文件系统的例子中,对于一个文件实例可能是全局性的项目集能够确定被缓存,比如文件大小,文件属性,等等。对于每一个文件来说这些都会是唯一的,被所有的指向这个文件的实例句柄共享

(/*

   我们使用一个UINICODE字符串来标示 "管道上下文",并且相互通讯的应用程序使用相同的名字来打开驱动。通过在设备扩展上下文中保存一个所有打开实例的全局链表来实现它。 然后,我们使用引用计数,这样以来,当所有实例的引用为0后,我们才从列表中删除一个实例。我们也可以把它放到IRP的FsContext中,所以所有的IRP都会和它一起返回,这样我们可以不用实现搜索它就可以轻松地使用它。

     */

   if(RtlCompareUnicodeString(&pExampleList->usPipeName,  

                                     &pFileObject->FileName, TRUE) == 0)

          {

              bNeedsToCreate = FALSE;

              pExampleList->uiRefCount++;

              pFileObject->FsContext = (PVOID)pExampleList;


实例上下文

这是你可以创建的最独特的上下文,对于在系统中创建的每一个句柄它都是唯一的。所以,如果进程1和进程2都打开对于同一个文件的新的句柄,尽管他们的资源上下文是一样的,但他们的实例上下文却是不同的。一个简单的例子中,对于每个实例是唯一的项,它可能是文件指针。虽然两个进程都打开同一个文件,但他们或许不是从同一个位置来读这个文件。这就是说,对于这个文件,每个打开的实例句柄必须维护着各自的一个上下文数据,这个上下文数据记录了各个句柄读取文件的当前位置。

实例上下文和任何一种上下文通常都有指针指向资源上下文和设备上下文。就像设备对象中有指针指向驱动对象一样。这样,在必要的时候,可以避免使用查表或者链表搜索来找出需要的上下文。

大图

下面的这个图表概括了上面讲的这些他们之间的关系。这个可以帮助你形象的了解怎样在你的驱动中构建这些关系。上下文关系可以按照任何一种你需要的形式构建,这里仅仅是一个例子。你甚至可以创建在这里提及的这三个上下文之外上下文,你自己定义他们各自的域。


驱动开发之三 --- IPC 【译文】_第1张图片

 


我们的实现

在我们驱动的实现中,我们需要一个设备上下文和一个资源上下文,而不需要指明我们在做什么的实例上下文。

我们首先使用IoCreateDevice创建一个设备扩展,这个数据结构将被用来维护资源上下文链表,于是所有对“创建“的调用都能关联一个适当的资源上下文。

第二步,创建资源上下文。我们首先搜索创建的链表来确定这个资源是否已经存在。如果存在,我们将增加引用计数并分配给它一个实例句柄。如果没有存在,我们将创建一个新的,并把它添加到链表中。

关闭时执行相反的操作,我们将简单的减少引用计数,如果引用计数为0,我们将把它从资源上下文链表中剔除,并删掉它。

在IRP的IO_STACK_LOCATION中提供了一个指向FILE_OBJECT的指针 ,我们可以把它作为一个实例句柄。它包含了两个域,  

    +0x00c FsContext         : Ptr32 Void

    +0x010 FsContext2        : Ptr32 Void

我们可以用来存储上下文,以及简单的使用他们中的一个来存储我们的资源上下文。我们还可以用他们来存储我们选定的实例上下文。对于特定的驱动都有自己的规则,可以使用他们来做不同的事情。但是我们开发的这个驱动在任何框架外,并且没有其他驱动与之通讯。这就意味着,我们可以自主的做任何我们想要的事情。但是,如果你选择实现一个特殊类别的驱动,你就需要确定哪些是你可用的。

我们使用传递进的设备名字符串来分配资源。现在我们在设备名的后面追加一个新的字符串来创建不同的资源。如果两个应用程序打开了同一个资源字符串,他们将被分配同一个资源上下文。我们创建的这个资源上下文简单的维护它自己的锁和一个环形缓冲区。这个环形缓冲区,位于内核内存区,可以被任何进程访问。因此,我们可以从一个进程拷贝内存到另一个进程。

内存池

最后,在这个驱动中,我们开始分配内存。在驱动中,我们在称为“池“的地方申请内存。在用户模式下,我们从堆中申请内存。从这个方面来讲,他们本质是相同的。内存管理器负责管理这些内存分配,并且提供给你内存。在用户模式,虽然可以有多个堆,但是他们本质上是同种类型的内存。同时,用户模式下,一个进程的堆仅仅被这个进程访问,两个进程不允许共享同一个堆。

pExampleList = (PEXAMPLE_LIST)ExAllocatePoolWithTag(NonPagedPool,  

                              sizeof(EXAMPLE_LIST), EXAMPLE_POOL_TAG);

     if(pExampleList)

     {

在内核中,事情多少有些不同。有两种类型的池,分页池和非分页池。分页池内存页面可以被换出到磁盘,并且只能在IRQL < DISPATCH_LEVEL的情况下使用。非分页池内存不同,你可以在任何时间任何地点访问它,因为它从来不会被换页到磁盘。需要注意的是,如果没有显而易见的理由,不要使用非分页池内存。

在所有的驱动之间,池是共享的。出于这个原因,你可以用一个“pool tag”来区分内存。这四个字节的标识被放在你申请的内存池的头中。这种情况下,如果说,你越过了你的内存边界,整个的文件系统就会被意外的破坏。你可以在访问池中内存前,先看看这个内存的访问是否合法,并且注意你的驱动可能会破坏下一个池的入口。

在我们的驱动中,我们从非分页内存池中分配内存,因为我们的数据结构中包含一个KMUTEX ,KMUTEX 对象必须要在非分页内存中。当然你也可以单独的给它分配内存,然后在这里放一个指针。出于简单期间,在这里我们就一起申请内存。

内核互斥

互斥在内核中和在用户模式中实际上是一样的。每一个进程都有一个被称为“句柄表“的东西。它可以简单的映射用户模式的句柄和内核对象。当你在用户模式下创建一个互斥时,实际上你得到了一个在内核中创建的互斥对象。

不同一点,我们需要在内核中建立的互斥句柄实际上是我们在内核中的一个数据结构,这个结构必须在非分页内存中。并且等待互斥的参数也多少有点复杂。

MSDN中介绍的KeWaitForMutexObject 是一个宏,实际是KeWaitForSingleObject。

#define KeWaitForMutexObject KeWaitForSingleObject

参数是什么意思?这些在msdn中都有解释。

LARGE_INTEGER TimeOut;

  

TimeOut.QuadPart = 10000000L;

TimeOut.QuadPart *= NumberOfSeconds;

TimeOut.QuartPart = -(TimeOut.QuartPart);

NtStatus = KeWaitForMutexObject(&pExampleDeviceContext->kListMutex,  

                                    Executive, KernelMode, FALSE, NULL);

     if(NT_SUCCESS(NtStatus))

     {

接上

简单的管道实现

这个工程的实现非常简单,在这部分,我们要看看这个驱动是怎样运作的?并且思考下我们怎样来提高它的执行效率?另外还会包括如何使用这个样本驱动?

安全

很简单,这里没有!驱动本身完全没设置安全,所以我们不需要关心谁被允许从缓冲区中读写数据。既然我们不关心,这个IPC(进程间的通讯)可以被用在任何进程之间,不管用户和他们的权限。

环形缓冲区

环形缓冲是一种简单的实现,它永远不会阻塞读或者写。缓冲区的大小是不可配置的,因此我们在应用程序中使用硬编码。我们也可以创建我们自己的IOCTL来发送请求给驱动。在IOCTL中可以实现一些配置,例如设置缓冲区的大小等等。

示例流程图

这是一个简单的流程图,CreateFile() API会使用符号连接"Example"来引用这个对象。 I/O管理器映射 DOS设备名为NT设备"\Device\Example",并且会追加上我们放在后面的字符串"\TestPipe"。我们得到由I/O管理器创建的IRP, 首先用这个设备字符串查询是否已经创建了资源上下文。如果是,我们使用栈单元中的FileObject,在增加其引用后,来放置我们的资源上下文。如果没有创建资源上下文,我们需要首先来创建它。

驱动开发之三 --- IPC 【译文】_第2张图片

 

在FILE_OBJECT的FileName中 实际上仅仅包含了额外的"\TestPipe",例如下面的例子:

dt _FILE_OBJECT ff6f3ac0

    +0x000 Type              : 5

    +0x002 Size              : 112

    +0x004 DeviceObject      : 0x80deea48  

    +0x008 Vpb               : (null)  

    +0x00c FsContext         : (null)  

    +0x010 FsContext2        : (null)  

    +0x014 SectionObjectPointer : (null)  

    +0x018 PrivateCacheMap   : (null)  

    +0x01c FinalStatus       : 0

    +0x020 RelatedFileObject : (null)  

    +0x024 LockOperation     : 0 ''

    +0x025 DeletePending     : 0 ''

    +0x026 ReadAccess        : 0 ''

    +0x027 WriteAccess       : 0 ''

    +0x028 DeleteAccess      : 0 ''

    +0x029 SharedRead        : 0 ''

    +0x02a SharedWrite       : 0 ''

    +0x02b SharedDelete      : 0 ''

    +0x02c Flags             : 2

    +0x030 FileName          : _UNICODE_STRING "\HELLO"

    +0x038 CurrentByteOffset : _LARGE_INTEGER 0x0

    +0x040 Waiters           : 0

    +0x044 Busy              : 0

    +0x048 LastLock          : (null)  

    +0x04c Lock              : _KEVENT

    +0x05c Event             : _KEVENT

    +0x06c CompletionContext : (null)

下图是关于ReadFile的操作流程,既然我们在 FILE_OBJECT中分配了我们的上下文,当我们读数据的时候,我们可以直接访问环形缓冲区。

驱动开发之三 --- IPC 【译文】_第3张图片

 

下图是关于WrteFile 的操作流程,既然我们在 FILE_OBJECT中分配了我们的上下文,当我们写数据的时候,我们可以直接访问环形缓冲区。

驱动开发之三 --- IPC 【译文】_第4张图片

 

关 闭时,我们释放资源上下文的引用。如果上下文引用计数为0,我们就从全局链中将它删除。如果不是0,我们就不需要做什么。这里是一个简单的流程,我们正在 处理的是IRP_MJ_CLOSE而不是 IRP_MJ_CLEANUP。这个代码可以放在任何一个不与用户模式应用程序交互的地方。然而,如果我们需要 在应用程序的上下文中释放资源,我们就需要把这个放到IRP_MJ_CLEANUP中。由于IRP_MJ_CLOSE不保证运行在进程上下文中,这个流程更像是处理IRP_MJ_CLEANUP发生的事情。

驱动开发之三 --- IPC 【译文】_第5张图片

 

使用例子

本 例中分为两个用户进程,usedriver2 和usedriver3。userdriver2 允许你输入数据,并将他发送到驱动。   userdriver3 输入回车并且从驱动中读取数据。很明显,目前的实现方式,如果他读了多个字符串,你只会看到只有第一个字符串显示出来。

需 要提供一个参数,即: 要打开的资源的名字。这是个任意的名字,用来允许驱动将两个实例句柄绑定在一起,这样,多个应用可以在同一时间共享数据。 “usedriver 2 HELLO” “usedriver3 HELLO” “userdriver2 Temp” “usedriver3 Temp” 会打开\Device\Example\HELLO和

“\Device\Example\Temp” 。当前的实现创建资源是大小写无关的。RtlCompareUnicodeString 的最后一个参数指明了是大小写敏感还是大小写无关。

本篇,我们学习了一些关于用户模式与内核模式的交互,了解了怎样实现一个简单的进程间通讯。我们学习了在设备驱动中创建上下文以及怎样分配内存和怎样在内核中使用同步对象。

你可能感兴趣的:(驱动开发之三 --- IPC 【译文】)