Windows驱动编程基础教程

上传人:沈*** 文档编号:64485499 上传时间:2022-03-21 格式:DOC 页数:55 大小:241KB
返回 下载 相关 举报
Windows驱动编程基础教程_第1页
第1页 / 共55页
Windows驱动编程基础教程_第2页
第2页 / 共55页
Windows驱动编程基础教程_第3页
第3页 / 共55页
点击查看更多>>
资源描述
楚狂人Windows驱动编程基础教程 版权声明本书是免费电子书。作者保留一切权利。但在保证本书完整性(包括版权声明、前言、正文内容、后记、以及作者的信息),并不增删、改变其中任何文字内容的前提下,欢迎任何读者以任何形式(包括各种格式的文档)复制和转载本书。同时不限制利用此书赢利的行为(如收费注册下载,或者出售光盘或打印版本)。不满足此前提的任何转载、复制、赢利行为则是侵犯版权的行为。发现本书的错漏之处,请联系作者。请不要修改本文中任何内容,不经过作者的同意发布修改后的版本。作者信息作者网名楚狂人。真名谭文。在上海从事Windows驱动开发相关的工作。对本书任何内容有任何疑问的读者,可以用下列方式和作者取得联系:QQ:16191935MSN:walled_riverhotmaEmail:mfc_tan_wen,walled_riverhotma前言本书非常适合熟悉Windows应用编程的读者转向驱动开发。所有的内容都从最基础的编程方法入手。介绍相关的内核API,然后举出示范的例子。这本书只有不到70页,是一本非常精简的小册子。所以它并不直接指导读者开发某种特定类型的驱动程序。而是起到一个入门指导的作用。即使都是使用C/C+语言的代码,在不同的应用环境中,常常看起来还是大相径庭。比如用TurboC+编写的DOS程序代码和用VC+编写的MFC应用程序的代码,看起来就几乎不像是同一种语言。这是由于它们所依赖的开发包不相同的缘故。在任何情况下都以写出避免依赖的代码为最佳。这样可以避免重复劳动。但是我们在学习一种开发包的使用时,必须习惯这个环境的编码方式,以便获得充分利用这个开发包的能力。本书的代码几乎都依赖于WDK(Windows Driver Kit)。但是不限WDK的版本。WDK还在不断的升级中。这个开发包是由微软公司免费提供的。读者可以在微软的网站上下载。当然读者必须把WDK安装的计算机上并配置好开发环境。具体的安装和配置方法本书没有提供。因为网上已经有非常多的中文文档介绍它们。读完这本书之后,读者一定可以更轻松的阅读其他专门的驱动程序开发的文档和相关书籍。而不至于看到大量无法理解的代码而中途放弃。如果有任何关于本书的内容的问题,读者可以随时发邮件到mfc_tan_wen或者walled_riverhotma。能够回答的问题我一般都会答复。写本书的时候,我和wowocock合作的一本名为天书夜读(在网上有一个大约20%内容的缩减电子版本)正在电子工业出版社编辑。预计还有不到一个月左右就会出版。这也是我自己所见的唯一一本中文原创的从汇编和反汇编角度来学习Windows内核编程和信息安全软件开发的书。希望读者多多支持。有想购买的读者请发邮件给我。我会在本书出版的第一时间,回复邮件告知购买的方法。此外我正在写另一本关于Windows安全软件的驱动编程的书。但是题目还没有拟好。实际上,读者现在见到的免费版本的Windows驱动编程基础教程是从这本书的第一部分中节选出来的。这本书篇幅比较大,大约有600-800页。主要内容如下:第一章驱动编程基础第二章磁盘设备驱动第三章磁盘还原与加密第四章传统文件系统过滤第五章小端口文件系统过滤第六章文件系统保护与加密第七章协议网络驱动第八章物理网络驱动第九章网络防火墙与安全连接第十章打印机驱动与虚拟打印第十一章视频驱动与过滤附录A WDK的安装与驱动开发的环境配置附录B 用WinDbg调试Windows驱动程序这本书还没有完成。但是肯定要付出巨大的精力,所以请读者不要来邮件索取完整的免费的电子版本。希望读者支持本书的纸版出版。因为没有完成,所以还没有联系出版商。有愿意合作出版本书的读者请发邮件与我联系。凡是发送邮件给我的读者,我将会发送邮件提供本人作品最新的出版信息,以及最新发布的驱动开发相关的免费电子书。如果不需要这些信息的,请在邮件里注明,或者回复邮件给我来取消订阅。谭文2008年6月9日目录版权声明1作者信息1前言2目录4第一章 字符串61.1 使用字符串结构61.2 字符串的初始化71.3 字符串的拷贝81.4 字符串的连接81.5 字符串的打印9第二章 内存与链表112.1内存的分配与释放112.2 使用LIST_ENTRY122.3 使用长长整型数据142.4使用自旋锁15第三章 文件操作183.1 使用OBJECT_ATTRIBUTES183.2 打开和关闭文件183.3 文件的读写操作21第四章 操作注册表254.1 注册键的打开操作254.2 注册值的读264.3 注册值的写29第五章 时间与定时器305.1 获得当前滴答数305.2 获得当前系统时间315.3 使用定时器32第六章 内核线程356.1 使用线程356.2 在线程中睡眠366.3 使用事件通知37第七章 驱动与设备417.1 驱动入口与驱动对象417.2 分发函数与卸载函数417.3 设备与符号链接427.4 设备的生成安全性限制447.5 符号链接的用户相关性46第八章 处理请求478.1 IRP与IO_STACK_LOCATION478.2 打开与关闭的处理488.3 应用层信息传入498.4 驱动层信息传出51后记:我的闲言碎语54第一章 字符串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;/ 字符串缓冲区的长度(字节数)PWSTRBuffer;/ 字符串缓冲区 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 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_buf256;/ 我们现在还不会分配内存,所以先定义缓冲区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节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。1.4 字符串的连接UNICODE_STRING不再是简单的字符串。操作这个数据结构往往需要更多的耐心。读者会常常碰到这样的需求:要把两个字符串连接到一起。简单的追加一个字符串并不困难。重要的依然是保证目标字符串的空间大小。下面是范例:NTSTATUS status;UNICODE_STRING dst;/ 目标字符串WCHAR dst_buf256;/ 我们现在还不会分配内存,所以先定义缓冲区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,这种情况请调用RtlAppendUnicodeStringToString。这个函数的第二个参数也是一个UNICODE_STRING的指针。1.5 字符串的打印字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。熟悉C语言的读者会使用sprintf。这个函数的宽字符版本为swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用RtlStringCbPrintfW来代替它。RtlStringCbPrintfW需要包含头文件ntstrsafe.h。在连接的时候,还需要连接库ntsafestr.lib。下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。#include / 任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配/ 内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空间/ 定义在局部变量中,也就是所谓的“在栈中”WCHAR buf512 = 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 rn”,&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 DBGKdPrint(a)DbgPrint#a#elseKdPrint (a)#endif不过这样的后果是,由于KdPrint (a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。导致KdPrint看起来很奇特的用了双重括弧:/ 调用KdPrint来进行输出调试信息status = KdPrint (L”file path = %wZ file size = %d rn”,&file_path,file_size);这个宏没有必要自己定义,WDK包中已有。所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。第二章 内存与链表2.1内存的分配与释放内存泄漏是C语言中一个臭名昭著的问题。但是作为内核开发者,读者将有必要自己来面对它。在传统的C语言中,分配内存常常使用的函数是malloc。这个函数的使用非常简单,传入长度参数就得到内存空间。在驱动中使用内存分配,这个函数不再有效。驱动中分配内存,最常用的是调用ExAllocatePoolWithTag。其他的方法在本章范围内全部忽略。回忆前一小节关于字符串的处理的情况。一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。下面的举例,是把一个字符串src拷贝到字符串dst。/ 定义一个内存分配标记#define MEM_TAGMyTt/ 目标字符串,接下来它需要分配空间。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的成对关系。2.2 使用LIST_ENTRYWindows的内核开发者们自己开发了部分数据结构,比如说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_ENTRYmy_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);/ 在这里做需要做的事其中的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来得到链表节点中的数据。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);/ 在这里做要做的事情KeReleaseSpinLock(&my_spin_lock,irql);原因是my_spin_lock在堆栈中。每个线程来执行的时候都会重新初始化一个锁。只有所有的线程共用一个锁,锁才有意义。所以,锁一般不会定义成局部变量。可以使用静态变量、全局变量,或者分配在堆中(见前面的1.2.1内存的分配与释放一节)。请读者自己写出正确的方法。LIST_ENTRY有一系列的操作。这些操作并不需要使用者自己调用获取与释放锁。只需要为每个链表定义并初始化一个锁即可:LIST_ENTRYmy_list_head;/ 链表头KSPIN_LOCKmy_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中。第三章 文件操作在内核中不能调用用户层的Win32 API函数来操作文件。在这里必须改用一系列与之对应的内核函数。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:文件不存在。(因而打开失败了)。这些返回值和打开文件的意图有关(有时希望打开已存在的文件,有时则希望建立新的文件等等。这些意图在本小节稍后
展开阅读全文
相关资源
相关搜索

最新文档


当前位置:首页 > 办公文档 > 工作计划


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!