Windows驱动编程基础教程(转)

我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初 级的问题。比如经常有人定义一个空的UNICODE_STRING,然后往里面拷贝字符串。结果无论如何都是蓝屏。也有人在堆栈中定义一个局部 SPIN_LOCK,作为下面的同步用——这样用显然没有任何意义。我无法一一回答这些问题:因为往往要耐心的看他们的代码,才能很不容易的发现这些错 误。而且我又不是总是空闲的,可以无休止的去帮网友阅读代码和查找初级错误。但是归根结底,这些问题的出现,是因为现在写驱动的同行越来越多,但是做驱动 开发又没有比较基础的,容易读懂的资料。为此我决定从今天开始连载一篇超级入门级的教程,来解决那些最基本的开发问题。老牛们就请无视这篇教程,一笑而过 了。

Windows驱动编程基础教程(1.1-1.3)

1.1 使用字符串结构

    常常使用传统C语言的程序员比较喜欢用如下的方法定义和使用字符串:
  
    char    *str = { “my first string” };            // ansi字符串
    wchar_t    *wstr = { L”my first string” };    // unicode字符串
    size_t len = strlen(str);                    // ansi字符串求长度
    size_t wlen = wcslen(wstr);                // unicode字符串求长度
    printf(“%s %ws %d %d”,str,wstr,len,wlen);    // 打印两种字符串

    但 是实际上这种字符串相当的不安全。很容易导致缓冲溢出漏洞。这是因为没有任何地方确切的表明一个字符串的长度。仅仅用一个’\0’字符来标明这个字符串的 结束。一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),程序就可能陷入崩溃。
    使用高级C++特性的编码者则容易忽略这个问题。因为常常使用std::string和CString这样高级的类。不用去担忧字符串的安全性了。
    在驱动开发中,一般不再用空来表示一个字符串的结束。而是定义了如下的一个结构:

    ty pe def struct _UNICODE_STRING {
        USHORT Length;                // 字符串的长度(字节数)
        USHORT MaximumLength;        // 字符串缓冲区的长度(字节数)
        PWSTR    Buffer;                // 字符串缓冲区
    } UNICODE_STRING, *PUNICODE_STRING;

    以上是Unicode字符串,一个字符为双字节。与之对应的还有一个Ansi字符串。Ansi字符串就是C语言中常用的单字节表示一个字符的窄字符串。

    ty pe def struct _STRING {
        USHORT Length;
        USHORT MaximumLength;
        PSTR Buffer;
    } ANSI_STRING, *PANSI_STRING;

    在驱动开发中四处可见的是Unicode字符串。因此可以说:Windows的 内核 是使用Uincode编码的。ANSI_STRING仅仅在某些碰到窄字符的场合使用。而且这种场合非常罕见。

UNICODE_STRING并不保证Buffer中的字符串是以空结束的。因此,类似下面的做法都是错误的,可能会会导致 内核 崩溃:

    UNICODE_STRING str;
    …
    len = wcslen(str.Buffer);            // 试图求长度。
    DbgPrint(“%ws”,str.Buffer);        // 试图打印str.Buffer。
  
    如果要用以上的方法,必须在编码中保证Buffer始终是以空结束。但这又是一个麻烦的问题。所以,使用微软提供的Rtl系列函数来操作字符串,才是正确的方法。下文逐步的讲述这个系列的函数的使用。

1.2 字符串的初始化

    请回顾之前的UNICODE_STRING结构。读者应该可以注意到,这个结构中并不含有字符串缓冲的空间。这是一个初学者常见的出问题的来源。以下的代码是完全错误的, 内核 会立刻崩溃:
  
    UNICODE_STRING str;
    wcscpy(str.Buffer,L”my first string!”);
    str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR);

    以上的代码定义了一个字符串并试图初始化它的值。但是非常遗憾这样做是不对的。因为str.Buffer只是一个未初始化的指针。它并没有指向有意义的空间。相反以下的方法是正确的:

    // 先定义后,再定义空间
UNICODE_STRING str;
    str.Buffer = L”my first string!”;
    str.Length = str.MaximumLength = wcslen(L”my first string!”) * sizeof(WCHAR);
    … …
  
    上面代码的第二行手写的常数字符串在代码中形成了“常数”内存空间。这个空间位于代码段。将被分配于可执行页面上。一般的情况下不可写。为此,要注意的是这个字符串空间一旦初始化就不要再更改。否则可能引发系统的保护异常。实际上更好的写法如下:

    //请分析一下为何这样写是对的:
    UNICODE_STRING str = {
        sizeof(L”my first string!”) – sizeof((L”my first string!”)[0]),  
        sizeof(L”my first string!”),
        L”my first_string!” };

    但是这样定义一个字符串实在太繁琐了。但是在头文件ntdef.h中有一个宏方便这种定义。使用这个宏之后,我们就可以简单的定义一个常数字符串如下:

    #include <ntdef.h>
    UNICODE_STRING str = RTL_CONSTANT_STRING(L“my first string!”);
  
    这只能在定义这个字符串的时候使用。为了随时初始化一个字符串,可以使用RtlInitUnicodeString。示例如下:
  
    UNICODE_STRING str;
    RtlInitUnicodeString(&str,L”my first string!”);

    用本小节的方法初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配任何内存。

1.3 字符串的拷贝

    因 为字符串不再是空结束的,所以使用wcscpy来拷贝字符串是不行的。UNICODE_STRING可以用RtlCopyUnicodeString来进 行拷贝。在进行这种拷贝的时候,最需要注意的一点是:拷贝目的字符串的Buffer必须有足够的空间。如果Buffer的空间不足,字符串会拷贝不完全。 这是一个比较隐蔽的错误。
    下面举一个例子。

    UNICODE_STRING dst;            // 目标字符串
    WCHAR dst_buf[256];                // 我们现在还不会分配内存,所以先定义缓冲区
    UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);
  
    // 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串。
    RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
    RtlCopyUnicodeString(&dst,&src);    // 字符串拷贝!
  
    以上这个拷贝之所以可以成功,是因为256比L” My source string!”的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。
    我曾经犯过的一个错误是没有调用RtlInitEmptyString。结果dst字符串被初始化认为缓冲区长度为0。虽然程序没有崩溃,却实际上没有拷贝任何内容。
    在拷贝之前,最谨慎的方法是根据源字符串的长度动态分配空间。在1.2节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。


Windows驱动编程基础教程(1.4-2.1)

1.4 字符串的连接

    UNICODE_STRING不再是简单的字符串。操作这个数据结构往往需要更多的耐心。读者会常常碰到这样的需求:要把两个字符串连接到一起。简单的追加一个字符串并不困难。重要的依然是保证目标字符串的空间大小。下面是范例:

    NTSTATUS status;
    UNICODE_STRING dst;            // 目标字符串
    WCHAR dst_buf[256];                // 我们现在还不会分配内存,所以先定义缓冲区
    UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);
  
    // 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
    RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
    RtlCopyUnicodeString(&dst,&src);    // 字符串拷贝!
  
    status = RtlAp pe ndUnicodeToString(
            &dst,L”my second string!”);
    if(status != STATUS_SUCCESS)
    {
        ……
    }


    NTSTATUS是常见的返回值类型。如果函数成功,返回STATUS_SUCCESS。否则的话,是一个错误码。RtlAp pe ndUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。
    另外一种情况是希望连接两个UNICODE_STRING,这种情况请调用 
1.5 字符串的打印

    字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。
    熟 悉C语言的读者会使用sprintf。这个函数的宽字符版本为swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用 RtlStringCbPrintfW来代替它。RtlStringCbPrintfW需要包含头文件ntstrsafe.h。在连接的时候,还需要连接 库ntsafestr.lib。
    下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。
  
    #include <ntstrsafe.h>
    // 任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配
    // 内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空间
    // 定义在局部变量中,也就是所谓的“在栈中”
    WCHAR buf[512] = { 0 };
    UNICODE_STRING dst;
    NTSTATUS status;
    ……

    // 字符串初始化为空串。缓冲区长度为512*sizeof(WCHAR)
    RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));
  
    // 调用RtlStringCbPrintfW来进行打印
    status = RtlStringCbPrintfW(
        dst->Buffer,L”file path = %wZ file size = %d \r\n”,
        &file_path,file_size);
    // 这里调用wcslen没问题,这是因为RtlStringCbPrintfW打印的
    // 字符串是以空结束的。
    dst->Length = wcslen(dst->Buffer) * sizeof(WCHAR);

    RtlStringCbPrintfW 在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被截去了。返回的status值为STATUS_BUFFER_OVERFLOW。调用这个函数 之前很难知道究竟需要多长的缓冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为2倍长度的新缓冲区,直到这个函数返回 STATUS_SUCCESS为止。
    值得注意的是UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws或者%s。其他的打印格式字符串与传统C语言中的printf函数完全相同。可以尽情使用。
    另外就是常见的输出打印。printf函数只有在有控制台输出的情况下才有意义。在驱动中没有控制台。但是Windows 内核 中拥有调试信息输出机制。可以使用特殊的工具查看打印的调试信息(请参阅附录1“WDK的安装与驱动开发的环境配置”)。
    驱 动中可以调用DbgPrint()函数来打印调试信息。这个函数的使用和printf基本相同。但是格式字符串要使用宽字符。DbgPrint()的一个 缺点在于,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息。但是DbgPrint()无论是发行版本还是调试版本编译都会有 效。为此可以自己定义一个宏:

    #if DBG
        KdPrint(a)    DbgPrint##a
    #else
        KdPrint (a)
    #endif
      
    不过这样的后果是,由于KdPrint (a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。导致KdPrint看起来很奇特的用了双重括弧:

    // 调用KdPrint来进行输出调试信息
    status = KdPrint ((
        L”file path = %wZ file size = %d \r\n”,
        &file_path,file_size));

    这个宏没有必要自己定义,WDK包中已有。所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。

2.1内存的分配与释放

    内存泄漏是C语言中一个臭名昭著的问题。但是作为 内核 开 发者,读者将有必要自己来面对它。在传统的C语言中,分配内存常常使用的函数是malloc。这个函数的使用非常简单,传入长度参数就得到内存空间。在驱 动中使用内存分配,这个函数不再有效。驱动中分配内存,最常用的是调用ExAllocatePoolWithTag。其他的方法在本章范围内全部忽略。回 忆前一小节关于字符串的处理的情况。一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。下面的举例,是把一个字 符串src拷贝到字符串dst。
  
    // 定义一个内存分配标记
    #define MEM_TAG    ‘MyTt’
    // 目标字符串,接下来它需要分配空间。
    UNICODE_STRING dst = { 0 };
    // 分配空间给目标字符串。根据源字符串的长度。
    dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,MEM_TAG);
    if(dst.Buffer == NULL)
    {
        // 错误处理
        status = STATUS_INSUFFICIENT_RESOUCRES;
        ……
    }
    dst.Length = dst.MaximumLength = src->Length;
    status = RtlCopyUnicodeString(&dst,&src);
    ASSERT(status == STATUS_SUCCESS);

    ExAllocatePoolWithTag的第一个参数NonpagedPool表明分配的内存是锁定内存。这些内存永远真实存在于物理内存上。不会被分页交换到硬盘上去。第二个参数是长度。第三个参数是一个所谓的“内存分配标记”。
    内存分配标记用于检测内存泄漏。想象一下,我们根据占用越来越多的内存的分配标记,就能大概知道泄漏的来源。一般每个驱动程序定义一个自己的内存标记。也可以在每个模块中定义单独的内存标记。内存标记是随意的32位数字。即使冲突也不会有什么问题。
    此外也可以分配可分页内存,使用PagedPool即可。
    ExAllocatePoolWithTag分配的内存可以使用ExFreePool来释放。如果不释放,则永远泄漏。并不像用户进程关闭后自动释放所有分配的空间。即使驱动程序动态卸载,也不能释放空间。唯一的办法是重启计算机。
    ExFreePool只需要提供需要释放的指针即可。举例如下:

    ExFreePool(dst.Buffer);
    dst.Buffer = NULL;
    dst.Length = dst.MaximumLength = 0;
  
    ExFreePool不能用来释放一个栈空间的指针。否则系统立刻崩溃。像以下的代码:

    UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);
    ExFreePool(src.Buffer);

    会招来立刻蓝屏。所以请务必保持ExAllocatePoolWithTag和ExFreePool的成对关系。

Windows驱动编程基础教程(2.2)

2.2 使用LIST_ENTRY

    Windows的 内核 开发者们自己开发了部分数据结构,比如说LIST_ENTRY。
    LIST_ENTRY 是一个双向链表结构。它总是在使用的时候,被插入到已有的数据结构中。下面举一个例子。我构筑一个链表,这个链表的每个节点,是一个文件名和一个文件大小 两个数据成员组成的结构。此外有一个FILE_OBJECT的指针对象。在驱动中,这代表一个文件对象。本书后面的章节会详细解释。这个链表的作用是:保 存了文件的文件名和长度。只要传入FILE_OBJECT的指针,使用者就可以遍历链表找到文件名和文件长度。

    ty pe def struct {
        PFILE_OBJECT file_object;
        UNICODE_STRING file_name;
        LARGE_INTEGER file_length;
    } MY_FILE_INFOR, *PMY_FILE_INFOR;

    一些读者会马上注意到文件的长度用LARGE_INTEGER表示。这是一个代表长长整型的数据结构。这个结构我们在下一小小节“使用长长整型数据”中介绍。
    为了让上面的结构成为链表节点,我必须在里面插入一个LIST_ENTRY结构。至于插入的位置并无所谓。可以放在最前,也可以放中间,或者最后面。但是实际上读者很快会发现把LIST_ENTRY放在开头是最简单的做法:

    ty pe def struct {
        LIST_ENTRY list_entry;
        PFILE_OBJECT file_object;
        UNICODE_STRING file_name;
        LARGE_INTEGER file_length;
    } MY_FILE_INFOR, *PMY_FILE_INFOR;    

    list_entry如果是作为链表的头,在使用之前,必须调用InitializeListHead来初始化。下面是示例的代码:
// 我们的链表头
    LIST_ENTRY        my_list_head;

    // 链表头初始化。一般的说在应该在程序入口处调用一下
    void MyFileInforInilt()
    {
        InitializeListHead(&my_list_head);
    }

    // 我们的链表节点。里面保存一个文件名和一个文件长度信息。
    ty pe def struct {
        LIST_ENTRY list_entry;
        PFILE_OBJECT file_object;
        PUNICODE_STRING file_name;
        LARGE_INTEGER file_length;
    } MY_FILE_INFOR, *PMY_FILE_INFOR;

    // 追加一条信息。也就是增加一个链表节点。请注意file_name是外面分配的。
    // 内存由使用者管理。本链表并不管理它。
    NTSTATUS MyFileInforAp pe ndNode(
        PFILE_OBJECT file_object, 
        PUNICODE_STRING file_name,
        PLARGE_INTEGER file_length)
    {
        PMY_FILE_INFOR my_file_infor = 
            (PMY_FILE_INFOR)ExAllocatePoolWithTag(
                PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);
        if(my_file_infor == NULL)
            return STATUS_INSUFFICIENT_RESOURES;

        // 填写数据成员。
        my_file_infor->file_object = file_object;
        my_file_infor->file_name = file_name;
        my_file_infor->file_length = file_length;

        // 插入到链表末尾。请注意这里没有使用任何锁。所以,这个函数不是多
        // 多线程安全的。在下面自旋锁的使用中讲解如何保证多线程安全性。
        InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor);
        return STATUS_SUCCESS;    
    } 以上的代码实现了插入。可以看到LIST_ENTRY插入到MY_FILE_INFOR结构的头部的好处。这样一来一个MY_FILE_INFOR看起来 就像一个LIST_ENTRY。不过糟糕的是并非所有的情况都可以这样。比如MS的许多结构喜欢一开头是结构的长度。因此在通过LIST_ENTRY结构 的地址获取所在的节点的地址的时候,有个地址偏移计算的过程。可以通过下面的一个典型的遍历链表的示例中看到:

    for(p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink)
    {
        PMY_FILE_INFOR elem = 
            CONTAINING_RECORD(p,MY_FILE_INFOR, list_entry);
        // To do something here…
        }
    }

    其中的CONTAINING_RECORD是一个WDK中已经定义的宏,作用是通过一个LIST_ENTRY结构的指针,找到这个结构所在的节点的指针。定义如下:
    
    #define CONTAINING_RECORD(address, ty pe , field) ((ty pe  *)( \
            (PCHAR)(address) - \
            (ULONG_PTR)(&((ty pe  *)0)->field)))

    从上面的代码中可以总结如下的信息:
    LIST_ENTRY中的数据成员Flink指向下一个LIST_ENTRY。
    整个链表中的最后一个LIST_ENTRY的Flink不是空。而是指向头节点。
    得到LIST_ENTRY之后,要用CONTAINING_RECORD来得到链表节点中的数据。


Windows驱动编程基础教程(2.3-2.4)

2.3 使用长长整型数据
    这里解释前面碰到的LARGE_INTEGER结构。与可能的误解不同,64位数据并非要在64位操作系统下才能使用。在VC中,64位数据的类型为__int64。定义写法如下:

    __int64 file_offset;

    上 面之所以定义的变量名为file_offset,是因为文件中的偏移量是一种常见的要使用64位数据的情况。同时,文件的大小也是如此(回忆上一小节中定 义的文件大小)。32位数据无符号整型只能表示到4GB。而众所周知,现在超过4GB的文件绝对不罕见了。但是实际上__int64这个类型在驱动开发中 很少被使用。基本上被使用到的是一个共用体:LARGE_INTEGER。这个共用体定义如下:

    ty pe def __int64 LONGLONG;    
    ty pe def union _LARGE_INTEGER {
        struct {
            ULONG LowPart;
            LONG HighPart;
        };
        struct {
            ULONG LowPart;
            LONG HighPart;
        } u;
        LONGLONG QuadPart;
    } LARGE_INTEGER;

    这个共用体的方便之处在于,既可以很方便的得到高32位,低32位,也可以方便的得到整个64位。进行运算和比较的时候,使用QuadPart即可。

    LARGE_INTEGER a,b;
    a.QuadPart = 100;
    a.QuadPart *= 100;
    b.QuadPart = a.QuadPart;
    if(b.QuadPart > 1000)
    {
        KdPrint(“b.QuadPart < 1000, LowPart = %x HighPart = %x”, b.LowPart,b.HighPart);
    }
    
    上面这段代码演示了这种结构的一般用法。在实际编程中,会碰到大量的参数是LARGE_INTEGER类型的。
2.4使用自旋锁

    链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。这里只介绍最简单的自选锁。
    有些读者可能疑惑锁存在的意义。这和多线程操作有关。在驱动开发的代码中,大多是存在于多线程执行环境的。就是说可能有几个线程在同时调用当前函数。
    这样一来,前文1.2.2中提及的追加链表节点函数就根本无法使用了。因为MyFileInforAp pe ndNode 这个函数只是简单的操作链表。如果两个线程同时调用这个函数来操作链表的话:注意这个函数操作的是一个全局变量链表。换句话说,无论有多少个线程同时执 行,他们操作的都是同一个链表。这就可能发生,一个线程插入一个节点的同时,另一个线程也同时插入。他们都插入同一个链表节点的后边。这时链表就会发生问 题。到底最后插入的是哪一个呢?要么一个丢失了。要么链表被损坏了。
    如下的代码初始化获取一个自选锁:

    KSPIN_LOCK my_spin_lock;
    KeInitializeSpinLock(&my_spin_lock);

    KeInitializeSpinLock 这个函数没有返回值。下面的代码展示了如何使用这个SpinLock。在KeAcquireSpinLock和KeReleaseSpinLock之间的 代码是只有单线程执行的。其他的线程会停留在KeAcquireSpinLock等候。直到KeReleaseSpinLock被调用。KIRQL是一个 中断级。KeAcquireSpinLock会提高当前的中断级。但是目前忽略这个问题。中断级在后面讲述。
    
    KIRQL irql;
    KeAcquireSpinLock(&my_spin_lock,&irql);
    // To do something …
    KeReleaseSpinLock(&my_spin_lock,irql);
    
    初学者要注意的是,像下面写的这样的“加锁”代码是没有意义的,等于没加锁:
    
    void MySafeFunction()
    {
        KSPIN_LOCK my_spin_lock;
        KIRQL irql;
        KeInitializeSpinLock(&my_spin_lock);
        KeAcquireSpinLock(&my_spin_lock,&irql);
        // To do something …
        KeReleaseSpinLock(&my_spin_lock,irql);
    }

    原 因是my_spin_lock在堆栈中。每个线程来执行的时候都会重新初始化一个锁。只有所有的线程共用一个锁,锁才有意义。所以,锁一般不会定义成局部 变量。可以使用静态变量、全局变量,或者分配在堆中(见前面的1.2.1内存的分配与释放一节)。请读者自己写出正确的方法。
LIST_ENTRY有一系列的操作。这些操作并不需要使用者自己调用获取与释放锁。只需要为每个链表定义并初始化一个锁即可:

    
    LIST_ENTRY        my_list_head;        // 链表头
    KSPIN_LOCK    my_list_lock;        // 链表的锁
    
    // 链表初始化函数
    void MyFileInforInilt()
    {
        InitializeListHead(&my_list_head);
        KeInitializeSpinLock(&my_list_lock);
    }

    链表一旦完成了初始化,之后的可以采用一系列加锁的操作来代替普通的操作。比如插入一个节点,普通的操作代码如下:

    InsertHeadList(&my_list_head, (PLIST_ENTRY)& my_file_infor);

    换成加锁的操作方式如下:

    ExInterlockedInsertHeadList(
        &my_list_head, 
        (PLIST_ENTRY)& my_file_infor,
        &my_list_lock);

    注意不同之处在于,增加了一个KSPIN_LOCK的指针作为参数。在ExInterlockedInsertHeadList中,会自动的使用这个KSPIN_LOCK进行加锁。类似的还有一个加锁的Remove函数,用来移除一个节点,调用如下:

    my_file_infor = ExInterlockedRemoveHeadList (
        &my_list_head, 
        &my_list_lock);

    这个函数从链表中移除第一个节点。并返回到my_file_infor中。

Windows驱动编程基础教程(3.1-3.2)

3.1 使用OBJECT_ATTRIBUTES

    一 般的想法是,打开文件应该传入这个文件的路径。但是实际上这个函数并不直接接受一个字符串。使用者必须首先填写一个OBJECT_ATTRIBUTES结 构。在文档中并没有公开这个OBJECT_ATTRIBUTES结构。这个结构总是被InitializeObjectAttributes初始化。
    下面专门说明InitializeObjectAttributes。
    
    VOID InitializeObjectAttributes(
        OUT POBJECT_ATTRIBUTES InitializedAttributes,
        IN PUNICODE_STRING ObjectName,
        IN ULONG Attributes,
        IN HANDLE RootDirectory,
        IN PSECURITY_DESCRIPTOR SecurityDescriptor);

    读者需要注意的以下的几点:InitializedAttributes是要初始化的OBJECT_ATTRIBUTES结构的指针。ObjectName则是对象名字字符串。也就是前文所描述的文件的路径(如果要打开的对象是一个文件的话)。
    Attributes 则只需要填写OBJ_CASE_INSENSITIVE| OBJ_KERNEL_HANDLE即可(如果读者是想要方便的简洁的打开一个文件的话)。 OBJ_CASE_INSENSITIVE意味着名字字符串是不区分大小写的。由于Windows的文件系统本来就不区分字母大小写,所以笔者并没有尝试 过如果不设置这个标记会有什么后果。OBJ_KERNEL_HANDLE表明打开的文件句柄一个“ 内核 句柄”。 内核 文件句柄比应用层句柄使用更方便,可以不受线程和进程的限制。在任何线程中都可以读写。同时打开 内核 文件句柄不需要顾及当前进程是否有权限访问该文件的问题(如果是有安全权限限制的文件系统)。如果不使用 内核 句柄,则有时不得不填写后面的的SecurityDescriptor参数。
    RootDirectory用于相对打开的情况。目前省略。请读者传入NULL即可。
    SecurityDescriptor用于设置安全描述符。由于笔者总是打开 内核 句柄,所以很少设置这个参数。

3.2 打开和关闭文件

    下面的函数用于打开一个文件:
    
    NTSTATUS ZwCreateFile(
        OUT PHANDLE FileHandle,                            
        IN ACCESS_MASK DesiredAccess,                    
        IN POBJECT_ATTRIBUTES ObjectAttribute,    
        OUT PIO_STATUS_BLOCK IoStatusBlock,
        IN PLARGE_INTEGER AllocationSize OPTIONAL,
        IN ULONG FileAttributes,
        IN ULONG ShareAccess,
        IN ULONG CreateDisposition,
        IN ULONG createOptions,
        IN PVOID EaBuffer OPTIONAL,
        IN ULONG EaLength);
这个函数的参数异常复杂。下面逐个的说明如下:
    FileHandle:是一个句柄的指针。如果这个函数调用返回成成功(STATUS_SUCCESS),那就么打开的文件句柄就返回在这个地址内。
DesiredAccess: 申请的权限。如果打开写文件内容,请使用FILE_WRITE_DATA。如果需要读文件内容,请使用FILE_READ_DATA。如果需要删除文件或 者把文件改名,请使用Delete。如果想设置文件属性,请使用FILE_WRITE_ATTRIBUTES。反之,读文件属性则使用 FILE_READ_ATTRIBUTES。这些条件可以用|(位或)来组合。有两个宏分别组合了常用的读权限和常用的写权限。分别为 GENERIC_READ和GENERIC_WRITE。此外还有一个宏代表全部权限,是GENERIC_ALL。此外,如果想同步的打开文件,请加上 SYNCHRONIZE。同步打开文件详见后面对CreateOptions的说明。
    ObjectAttribute:对象描述。见前一小节。
    IoStatusBlock也是一个结构。这个结构在 内核 开发中经常使用。它往往用于表示一个操作的结果。这个结构在文档中是公开的,如下:

    ty pe def struct _IO_STATUS_BLOCK {
        union {
            NTSTATUS Status;
            PVOID Pointer;
        };
        ULONG_PTR Information;
    } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

    实 际编程中很少用到Pointer。一般的说,返回的结果在Status中。成功则为STATUS_SUCCESS。否则则是一个错误码。进一步的信息在 Information中。不同的情况下返回的Information的信息意义不同。针对ZwCreateFile调用的情况,Information 的返回值有以下几种可能:
    FILE_CreateD:文件被成功的新建了。
    FILE_OPENED:    文件被打开了。
    FILE_OVERWRITTEN:文件被覆盖了。
    FILE_SUPERSEDED:    文件被替代了。
    FILE_EXISTS:文件已存在。(因而打开失败了)。
    FILE_DOES_NOT_EXIST:文件不存在。(因而打开失败了)。
    这些返回值和打开文件的意图有关(有时希望打开已存在的文件,有时则希望建立新的文件等等。这些意图在本小节稍后的内容中详细说明。
    ZwCreateFile 的下一个参数是AllocationSize。这个参数很少使用,请设置为NULL。    再接下来的一个参数为FileAttributes。这个参 数控制新建立的文件的属性。一般的说,设置为FILE_ATTRIBUTE_NORMAL即可。在实际编程中,笔者没有尝试过其他的值。
    ShareAccess 是一个非常容易被人误解的参数。实际上,这是在本代码打开这个文件的时候,允许别的代码同时打开这个文件所持有的权限。所以称为共享访问。一共有三种共享 标记可以设置:FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_Delete。这三个标记可以用|(位或)来 组合。举例如下:如果本次打开只使用了FILE_SHARE_READ,那么这个文件在本次打开之后,关闭之前,别次打开试图以读权限打开,则被允许,可 以成功打开。如果别次打开试图以写权限打开,则一定失败。返回共享冲突。
    同时,如果本次打开只只用了FILE_SHARE_READ,而之前这个文件已经被另一次打开用写权限打开着。那么本次打开一定失败,返回共享冲突。其中的逻辑关系貌似比较复杂,读者应耐心理解。
    CreateDisposition参数说明了这次打开的意图。可能的选择如下(请注意这些选择不能组合):
    FILE_Create:新建文件。如果文件已经存在,则这个请求失败。
    FILE_OPEN:打开文件。如果文件不存在,则请求失败。
    FILE_OPEN_IF:打开或新建。如果文件存在,则打开。如果不存在,则失败。
    FILE_OVERWRITE:覆盖。如果文件存在,则打开并覆盖其内容。如果文件不存在,这个请求返回失败。
    FILE_OVERWRITE_IF:新建或覆盖。如果要打开的文件已存在,则打开它,并覆盖其内存。如果不存在,则简单的新建新文件。
    FILE_SUPERSEDE:新建或取代。如果要打开的文件已存在。则生成一个新文件替代之。如果不存在,则简单的生成新文件。
    请联系上面的IoStatusBlock参数中的Information的说明。
最 后一个重要的参数是CreateOptions。在惯常的编程中,笔者使用 FILE_NON_DIRECTORY_FILE| FILE_SYNCHRONOUS_IO_NONALERT。此时文件被同步的打开。而且打开的是文 件(而不是目录。创建目录请用FILE_ DIRECTORY_FILE)。所谓同步的打开的意义在于,以后每次操作文件的时候,比如写入文件,调用 ZwWriteFile,在ZwWriteFile返回时,文件写操作已经得到了完成。而不会有返回STATUS_PENDING(未决)的情况。在非同 步文件的情况下,返回未决是常见的。此时文件请求没有完成,使用者需要等待事件来等待请求的完成。当然,好处是使用者可以先去做别的事情。
    要同步打开,前面的DesiredAccess必须含有SYNCHRONIZE。
    此 外还有一些其他的情况。比如不通过缓冲操作文件。希望每次读写文件都是直接往磁盘上操作的。此时CreateOptions中应该带标记 FILE_NO_INTERMEDIATE_BUFFERING。带了这个标记后,请注意操作文件每次读写都必须以磁盘扇区大小(最常见的是512字节) 对齐。否则会返回错误。
    这个函数是如此的繁琐,以至于再多的文档也不如一个可以利用的例子。早期笔者调用这个函数往往因为参数设置不对而导致打开失败。非常渴望找到一个实际可以使用的参数的范例。下面举例如下:

    // 要返回的文件句柄
    HANDLE file_handle = NULL;
    // 返回值
    NTSTATUS status;
    // 首先初始化含有文件路径的OBJECT_ATTRIBUTES
    OBJECT_ATTRIBUTES object_attributes;
    UNICODE_STRING ufile_name = RTL_CONST_STRING(L”\\??\\C:\\a.dat”);
    InitializeObjectAttributes(
        &object_attributes,
        &ufile_name,
        OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
        NULL,
                NULL);
    // 以OPEN_IF方式打开文件。
    status = ZwCreateFile(
                &file_handle,
                GENERIC_READ | GENERIC_WRITE,
                &object_attributes,
                &io_status,
                NULL,
                FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ,
                FILE_OPEN_IF,
                FILE_NON_DIRECTORY_FILE |
                FILE_RANDOM_ACCESS |
                FILE_SYNCHRONOUS_IO_NONALERT,
                NULL,
                0);

    值得注意的是路径的写法。并不是像应用层一样直接写“C:\\a.dat”。而是写成了“\\??\\C:\\a.dat”。这是因为ZwCreateFile使用的是对象路径。“C:”是一个符号链接对象。符号链接对象一般都在“\\??\\”路径下。
    这种文件句柄的关闭非常简单。调用ZwClose即可。 内核 句柄的关闭不需要和打开在同一进程中。示例如下:

    ZwClose(file_handle);

Windows驱动编程基础教程(3.3-4.1)

3.3 文件的读写操作
    打开文件之后,最重要的操作是对文件的读写。读与写的方法是对称的。只是参数输入与输出的方向不同。读取文件内容一般用ZwReadFile,写文件一般使用ZwWriteFile。

    NTSTATUS 
      ZwReadFile(
          IN HANDLE  FileHandle,
            IN HANDLE  Event  OPTIONAL,
            IN PIO_APC_ROUTINE  ApcRoutine  OPTIONAL,
            IN PVOID  ApcContext  OPTIONAL,
            OUT PIO_STATUS_BLOCK  IoStatusBlock,
            OUT PVOID  Buffer,
            IN ULONG  Length,
            IN PLARGE_INTEGER  ByteOffset  OPTIONAL,
            IN PULONG  Key  OPTIONAL);

    FileHandle:是前面ZwCreateFile成功后所得到的FileHandle。如果是 内核 句柄,ZwReadFile和ZwCreateFile并不需要在同一个进程中。句柄是各进程通用的。
    Event :一个事件。用于异步完成读时。下面的举例始终用同步读,所以忽略这个参数。请始终填写NULL。
    ApcRoutine Apc:回调例程。用于异步完成读时。下面的举例始终用同步读,所以忽略这个参数。请始终填写NULL。
    IoStatusBlock:返回结果状态。同ZwCreateFile中的同名参数。
    Buffer:缓冲区。如果读文件的内容成功,则内容被被读到这个缓冲里。
    Length:描述缓冲区的长度。这个长度也就是试图读取文件的长度。
    ByteOffset:要读取的文件的偏移量。也就是要读取的内容在文件中的位置。一般的说,不要设置为NULL。文件句柄不一定支持直接读取当前偏移。
    Key:读取文件时用的一种附加信息,一般不使用。设置NULL。
    返 回值:成功的返回值是STATUS_SUCCESS。只要读取到任意多个字节(不管是否符合输入的Length的要求),返回值都是 STATUS_SUCCESS。即使试图读取的长度范围超出了文件本来的大小。但是,如果仅读取文件长度之外的部分,则返回 STATUS_END_OF_FILE。
    ZwWriteFile的参数与ZwReadFile完全相同。当然,除了读写文件外,有的读者 可能会问是否提供一个ZwCopyFile用来拷贝一个文件。这个要求未能被满足。如果有这个需求,这个函数必须自己来编写。下面是一个例子,用来拷贝一 个文件。利用到了ZwCreateFile,ZwReadFile和ZwWrite这三个函数。
不过作为本节的例子,只举出ZwReadFile和ZwWriteFile的部分:

    NTSTATUS MyCopyFile(
        PUNICODE_STRING target_path,
        PUNICODE_STRING source_path)
    {
        // 源和目标的文件句柄
        HANDLE target = NULL,source = NULL;
        // 用来拷贝的缓冲区
        PVOID buffer = NULL;
        LARGE_INTEGER offset = { 0 };
        IO_STATUS_BLOCK io_status = { 0 };
    
        do {
            // 这里请用前一小节说到的例子打开target_path和source_path所对应的
            // 句柄target和source,并为buffer分配一个页面也就是4k的内存。
            … …
            // 然后用一个循环来读取文件。每次从源文件中读取4k内容,然后往
            // 目标文件中写入4k,直到拷贝结束为止。 
            while(1) {
                length = 4*1024;        // 每次读取4k。
                // 读取旧文件。注意status。
                status = ZwReadFile (
                    source,NULL,NULL,NULL,
                    &my_io_status,buffer, length,&offset,
                    NULL);
                if(!NT_SUCCESS(status))
                {
                    // 如果状态为STATUS_END_OF_FILE,则说明文件
                    // 的拷贝已经成功的结束了。
                    if(status == STATUS_END_OF_FILE)
                        status = STATUS_SUCCESS;
                    break;
                }
                // 获得实际读取到的长度。
                length = IoStatus.Information;

                // 现在读取了内容。读出的长度为length.那么我写入
                // 的长度也应该是length。写入必须成功。如果失败,
                // 则返回错误。
                status = ZwWriteFile(
                    target,NULL,NULL,NULL,
                    &my_io_status,
                    buffer,length,&offset,
                    NULL);
                if(!NT_SUCCESS(status))
                    break;
                
                // offset移动,然后继续。直到出现STATUS_END_OF_FILE
                // 的时候才结束。
                offset.QuadPart += length;
            }
        } while(0);

        // 在退出之前,释放资源,关闭所有的句柄。
        if(target != NULL)
            ZwClose(target);
        if(source != NULL)
            ZwClose(source);
        if(buffer != NULL)
            ExFreePool(buffer);
        return STATUS_SUCCESS;
    }
    
    除了读写之外,文件还有很多的操作。比如删除、重新命名、枚举。这些操作将在后面实例中用到时,再详细讲解。
.1注册键的打开操作

    和在应用程序中编程的方式类似,注册表是一个巨大的树形结构。操作一般都是打开某个子键。子键下有若干个值可以获得。每一个值有一个名字。值有不同的类型。一般需要查询才能获得其类型。
    子键一般用一个路径来表示。和应用程序编程的一点重大不同是这个路径的写法不一样。一般应用编程中需要提供一个根子键的句柄。而驱动中则全部用路径表示。相应的有一张表表示如下:

    应用编程中对应的子键    驱动编程中的路径写法
    HKEY_LOCAL_MACHINE    \Registry\Machine
    HKEY_USERS        \Registry\User
    HKEY_CLASSES_ROOT    没有对应的路径
    HKEY_CURRENT_USER    没有简单的对应路径,但是可以求得
    
    实 际上应用程序和驱动程序很大的一个不同在于应用程序总是由某个“当前用户”启动的。因此可以直接读取HKEY_CLASSES_ROOT和 HKEY_CURRENT_USER。而驱动程序和用户无关,所以直接去打开HKEY_CURRENT_USER也就不符合逻辑了。
    打开注册表键使用函数ZwO pe nKey。新建或者打开则使用ZwCreateKey。一般在驱动编程中,使用ZwO pe nKey的情况比较多见。下面以此为例讲解。ZwO pe nKey的原型如下:

    NTSTATUS 
          ZwO pe nKey(
            OUT PHANDLE  KeyHandle,
              IN ACCESS_MASK  DesiredAccess,
            IN POBJECT_ATTRIBUTES  ObjectAttributes
        );
    
    这个函数和ZwCreateFile是类似的。它并不接受直接传入一个字符串来表示一个子键。而是要求输入一个OBJECT_ATTRIBUTES的指针。如何初始化一个OBJECT_ATTRIBUTES请参考前面的讲解ZwCreateFile的章节。
    DesiredAccess支持一系列的组合权限。可以是下表中所有权限的任何组合:
    KEY_QUERY_VALUE:读取键下的值。
    KEY_SET_VALUE:设置键下的值。
    KEY_Create_SUB_KEY:生成子键。
    KEY_ENUMERATE_SUB_KEYS:枚举子键。
    不过实际上可以用KEY_READ来做为通用的读权限组合。这是一个组合宏。此外对应的有KEY_WRITE。如果需要获得全部的权限,可以使用KEY_ALL_ACCESS。
    下面是一个例子,这个例子非常的有实用价值。它读取注册表中保存的Windows系统目录(指Windows目录)的位置。不过这里只涉及打开子键。并不读取值。读取具体的值在后面的小节中再完成。
    Windows 目录的位置被称为SystemRoot,这一值保存在注册表中,路径是“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft \Windows NT\CurrentVersion”。当然,请注意注意在驱动编程中的写法有所不同。下面的代码初始化一个 OBJECT_ATTRIBUTES。

    HANDLE my_key = NULL;
    NTSTATUS status;

    // 定义要获取的路径    
    UNICODE_STRING my_key_path = 
        RTL_CONSTANT_STRING(
            L” \\ Registry\\Machine\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion”);
    OBJECT_ATTRIBUTE my_obj_attr = { 0 };

    // 初始化OBJECT_ATTRIBUTE
    InitializeObjectAttributes(
        &my_obj_attr,
        &my_key_path,
        OBJ_CASE_INSENSITIVE,
        NULL,
                NULL);
    // 接下来是打开Key
    status = ZwO pe nKey(&my_key,KEY_READ,&my_obj_attr);
    if(!NT_SUCCESS(status))
    {
        // 失败处理
        ……
    }

    上面的代码得到了my_key。子键已经打开。然后的步骤是读取下面的SystemRoot值。这在后面一个小节中讲述。


Windows 文件过滤驱动经验总结

1、获得文件全路径以及判断时机

除 在所有 IRP_MJ_XXX 之前自己从头创建 IRP 发送到下层设备查询全路径外,不要尝试在 IRP_MJ_Create 以外的地方获得全路 径,因为只有在 IRP_MJ_Create中才会使用 ObCreateObject() 来建立一个有效的 FILE_OBJECT。而 在 IRP_READ,IRP_WRITE 中它们是直接操作 FCB (File Control Block)的。


2、从头建立 IRP 发送关注点

无 论你建立什么样的 IRP,是 IRP_MJ_Create 也好还是 IRP_MJ_DIRECTORY_CONTROL也罢,最要提醒的就是一些标 志。不同的标志会代来不同的结果,有些结果是直接返回失败。这里指的标志不光是 IRP->Flags,还要考 虑 IO_STACK_LOCATION->Flags还有其它等等。尤其是你要达到一些特殊目的,这时候更需要注意, 如 IRP_MN_QUERY_DIRECTORY,不同的标志结果有很大的不同。


3、从头建立 IRP 获取全路径注意点

自 己从头建立一个 IRP_MJ_QUERY_INFORMATION 的 IRP 获取全路径时需要注意,不仅在 IRP_MJ_Create 要做区别 处理,在 IRP_MJ_CLOSE 也要做同样的处理,否则如果目标是 NTFS 文件系统的话可能产生 deadlock。如果是 NTFS 那么在 IRP_MJ_CLEANUP 的时候也需要对 FO_STREAM_FILE 类型的文件做同样处理。



4、获得本地/远程访问用户名(域名/SID)

方 法只有在 IRP_MJ_Create 中才可用,那是因为 IO_SECURITY_CONTEXT 只有 在 IO_STACK_LOCATION->Parameters.Create.SecurityContext 才会有效。这样你才有可能 从 IO_SECURITY_CONTEXT->SecurityContext->AccessState->SubjectSecurityContext.XXXToken 中 获得访问 TOKEN,从而进一步得到用户名或 SID。记得 IFS 中有一个库,它的 LIB 导出一个函数可以让你在获得以上信息后得到用户名与域 名。但如果你想兼容 NT4 的话,只能自己分析来得出本地和远程的 SID。


5、文件与目录的判断

正确的方法 在楚狂人的文档里已经说过了,再补充一句。如果你的文件过滤驱动要兼容所有文件系统,那么不要十分相信 从 FileObject->FsContext 里取得的数据。正确的方法还是在你传递下去 IRP_MJ_Create 后从最下层文件系统延 设备栈返回到你这里后再获得。


6、加/解密中判断点

只判 断 IRP_PAGING_IO,IRP_SYNCHRONOUS_PAGING_IO,IRP_NOCACHE 是没错的。如果有问题,相信是自己的问 题。关于有人提到在 FILE_OBJECT->Flags中的 FO_NO_INTERMEDIATE_BUFFERING 是否需要判断,对此 问题的回答是只要你判断了IRP_NOCACHE 就不用再判断 FILE_OBJECT 中的,因为它最终会设 置 IRP->Flags 为 IRP_NOCACHE。关于你看到的诸如 IRP_DEFER_IO_COMPLETION等 IRP 不要去管 它,因为它只是一个过程。最终读写还是如上所介绍。至于以上这些 IRP 哪个是由 CC MGR 发送的,哪些是由 I/O MGR 发送和在什么时候 发送的,这个已经有很多讨论了,相信可以找到。



7、举例说明关于 IRP 传递与完成注意事项

只 看 Walter Oney 的那本 《Programming the Microsoft Windows driver model》里介绍的流 程,自己没有实际的体会还是不够的,那里只介绍了基础概念,让自己有了知识。知道如何用,在什么情况下用,用哪种方法,能够用的稳定这叫有了技术。我们从 另一个角度出发,把问题分为两段来看,这样利于总结。一个 IRP 在过滤驱动中,把它分为需要安装 CompleteRoutine 的与无需安 装 CompleteRoutine 的。那么在不需要安装 CompleteRoutine 的有以下几类情况。

(1) 拿到这个 IRP 后什么都不做,直接调用 IoCompleteRequest() 来返回。
(2) 拿到这个 IRP 后什么都不做,直接传递到底层设备,使用 IoSkipCurrentIrpStackLocation() 后调用 IoCallDriver() 传递。
(3) 使用 IoBuildSynchronousFsdRequest() 或 IoBuildDeviceIoControlRequest()来建立 IRP 的。

以 上几种根据需要直接使用即可,除了一些参数与标志需要注意外,没有什么系统机制相关的东西需要注意了。那么再来看需要安 装 CompleteRoutine 的情况。我们把这种情况再细分为两种,一是在 CompleteRoutine 中返回标志为 STATUS_MORE_PROCESSING_REQUIRED 的情况。二是返回处这个外的标志,需要使用函数 IoMarkIrpPending() 的情况。在 CompleteRoutine 中绝大多数就这么两种情况,你需要使用其中的一种情况。那么为什么 需要安装 CompleteRoutine 呢?那是因为我们对其 IRP 从上层驱动,经过我们驱动,在经过底层设备栈返回到我们这一层驱动时需要得到 其中内容作为参考依据的,还有对其中内容需要进行修改的。再有一种情况是没有经过上层驱动,而 IRP 的产生是在我们驱动直接下发到底层驱动,而经过设 备栈后返回到我们这一层,且我们不在希望它继续向上返回的,因为这个 IRP 本身就不是从上层来的。综上所述,先来看 下 IoMarkIrpPending() 的情况。


(1) 在 CompleteRoutine 中判 断 Irp->PendingReturned 并使用 IoMarkIrpPending()然后返回。这种方法在没有使 用 KeSetEvent() 的情况下,且不是自建 IRP 发送到底层驱动返回时使用。也就是说有可能我所做的工作都是 在 CompleteRoutine 中进行的。比如加/解密时,我在这里对下层驱动返回数据的判断并修改。修改后因为没有使 用 STATUS_MORE_PROCESSING_REQUIRED 标志,它会延设备堆一直向上返回并到用户得到数据为止。这里一定要注意,在这种情 况下 CompleteRoutine返回后,不要在碰这个 IRP。也就是说如果这个时候你使用了 IoCompleteRequest()的话会出现 一个 MULTIPLE_IRP_COMPLIETE_REQUEST 的 BSOD 错误。


(2) 在 CompleteRoutine 中直接返回 STATUS_MORE_PROCESSING_REQUIRED 标志。这种情况在使用了 KeSetEvent() 的函数下出现。这里又有两个小小的分之。

  1) 出 现于上层发送到我这里,当我这里使用 IoCallDriver() 后,底层返回数据经过我这一层时,我想让它暂时停止继续向上传递,让这 个 IRP 稍微歇息一会,等我对这个 IRP 返回的数据操作完成后(一般是没有在 CompleteRoutine中对返回数据进行操作情况下,也就 是说等到完成例程返回后再进行操作),由我来调用 IoCompleteRequest() 让它延着设备栈继续返回。这里要注意,我们是想让它返回的, 所以调用了 IoCompleteRequest()。这个可不同于下面所讲的自己从头分配 IRP 时在 CompleteRoutine 中已经调 用 IoFreeIrp() 释放了当前 IRP 的情况。比如我在做一个改变文件大小,向文件头写入加密标志的驱动时,在上层发来 了 IRP_MJ_QUERY_INFORMATION 查询文件,我想在这个时候获得文件信息进行判断,然后根据我的判断结果再移动文件指针。注意:上 面是两步,第一步是先获得文件大小,那么在这个时候我就需要用到上述办法,先让这个 IRP传递下去,得到我想要的东西后在进行对比。等待适当时机完成这 个 IRP,让数据继续传递,直到用户收到为止。第二步我会结合下面小节来讲。

  2) 出现于自己从头建立 IRP,当使 用 IoAllocate() 或 IoBuildAsynchronousFsdRequest()创建 IRP 调 用 IoCallDriver() 后,底层返回数据到我这一层时,我不想让这 个 IRP 继续向上延设备栈传递。因为这个 IRP 就是在我这层次建 立的,上层本就不知道有这么一个 IRP。那么到这里我就要在 CompleteRoutine 中使用 IoFreeIrp()来释放掉这个 IRP, 并不让它继续传递。这里一定要注意,在 CompleteRoutine函数返回后,这个 IRP 已经释放了,如果这个时候在有任何关于这 个 IRP 的操作那么后果是灾难性的,必定导致 BSOD 错误。前面 1) 小节给出的例子只完成了第一步这里继续讲第二步,第一步我重用这 个 IRP 得到了文件大小,那么这个时候虽 然知道大小,但我还是无法知道这个文件是否被我加过密。这时,我就需要在这里自己从头建立一 个 IRP_MJ_READ 的 IRP 来读取文件来判断是否我加密过了的文件,如果是,则要减少相应的大小,然后继续返回。注意:这里的返回是指让第 一步的 IRP 返回。而不是我们自己创建的。我们创建的都已经在 CompleteRoutine 中销 毁了。



8、关于完成 IRP 的动作简介

当 一个底层驱动调用了 IoCompleteRequest() 函数时,基本上所有设备栈相关 IRP 处理工作都是在它那里完成的。包 括 IRP->Flags 的一些标志的判断,对 APC 的处理,抛出MULTIPLE_IRP_COMPLETE_REQUESTS 错误等。 当它延设备栈一直调用驱动所安装的 CompleteRoutine时,如果发现 STATUS_MORE_PROCESSING_REQUIRED 这 个标志,则会停止向上继续回滚。这也是为什么在 CompleteRoutine 中使用这个标志即可暂停 IRP 的原因。



9、关于 ObQueryNameString 的使用

这 个函数的使用,在有些环境下会有问题。它的上层函数是 ZwQueryObject()。在某些情况下会导致系统挂起,或者直接 BSOD。它是从 对象 管理器中的 ObpRootDirectoryObject开始遍历,通过 OBJECT_HEADER_TO_NAME_INFO 获得对象名称。今天 问了下 PolyMeta好象是在处理 PIPE 时会挂启,这个问题出现在 2000 系统。在 XP 上好象补丁了。


10、关于重入问题

其 实这个问题在很久前的 IFS FAQ 里已经介绍的很清楚,包括处理方法以及每种方法可能带来的问题。IFS FAQ 里的 Q34 一共介绍了四种方 法,包括自己从头建立 IRP发送,使用 ShadowDevice,使用特征字符串,根据线程 ID,在 XP 下使用IoCreateFileS pe cifyDeviceObjectHint() 函数。并且把以上几种在不同环境下使用要处理的问题也做了简单的介绍。且在 Q33 里介绍了在 CIFS 碰到的 FILE_COMPLETE_IF_OPLOCKED 问题的解决方法。

你可能感兴趣的:(Windows驱动编程基础教程(转))