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这样高级的类。不用去担忧字符串的安全性了。
在驱动开发中,一般不再用空来表示一个字符串的结束。而是定义了如下的一个结构:

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

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

typedef 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 = RtlAppendUnicodeToString(
&dst,L”my second string!”);
if(status != STATUS_SUCCESS)
{
……
}


NTSTATUS是常见的返回值类型。如果函数成功,返回STATUS_SUCCESS。否则的话,是一个错误码。RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误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的指针,使用者就可以遍历链表找到文件名和文件长度。

typedef 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放在开头是最简单的做法:

typedef 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);
}

// 我们的链表节点。里面保存一个文件名和一个文件长度信息。
typedef 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 MyFileInforAppendNode(
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, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)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。这个共用体定义如下:

typedef __int64 LONGLONG;
typedef 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中提及的追加链表节点函数就根本无法使用了。因为MyFileInforAppendNode这个函数只是简单的操作链表。如果两个线程同时调用这个函数来操作链表的话:注意这个函数操作的是一个全局变量链表。换句话说,无论有多少个线程同时执行,他们操作的都是同一个链表。这就可能发生,一个线程插入一个节点的同时,另一个线程也同时插入。他们都插入同一个链表节点的后边。这时链表就会发生问题。到底最后插入的是哪一个呢?要么一个丢失了。要么链表被损坏了。
如下的代码初始化获取一个自选锁:

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也是一个结构。这个结构在内核开发中经常使用。它往往用于表示一个操作的结果。这个结构在文档中是公开的,如下:

typedef 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也就不符合逻辑了。
打开注册表键使用函数ZwOpenKey。新建或者打开则使用ZwCreateKey。一般在驱动编程中,使用ZwOpenKey的情况比较多见。下面以此为例讲解。ZwOpenKey的原型如下:

NTSTATUS
ZwOpenKey(
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 = ZwOpenKey(&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 下使用IoCreateFileSpecifyDeviceObjectHint() 函数。并且把以上几种在不同环境下使用要处理的问题也做了简单的介绍。且在 Q33 里介绍了在 CIFS 碰到的 FILE_COMPLETE_IF_OPLOCKED 问题的解决方法。

文章来自: 本站原创
Tags:
评论: 0 | 查看次数: 8576