当前位置:首页 > 芯闻号 > 充电吧
[导读]本书非常适合熟悉Windows应用编程的读者转向驱动开发。所有的内容都从最基础的编程方法入手。介绍相关的内核API,然后举出示范的例子。这本书只有不到70页,是一本非常精简的小册子。所以它并不直接指导

本书非常适合熟悉Windows应用编程的读者转向驱动开发。所有的内容都从最基础的编程方法入手。介绍相关的内核API,然后举出示范的例子。这本书只有不到70页,是一本非常精简的小册子。所以它并不直接指导读者开发某种特定类型的驱动程序。

即使都是使用C/C++语言的代码,在不同的应用环境中,常常看起来还是大相径庭。比如用TurboC++编写的DOS程序代码和用VC++编写的MFC应用程序的代码,看起来就几乎不像是同一种语言。这是由于它们所依赖的开发包不相同的缘故。

在任何情况下都以写出避免依赖的代码为最佳。这样可以避免重复劳动。但是我们在学习一种开发包的使用时,必须习惯这个环境的编码方式,以便获得充分利用这个开发包的能力。

本书的代码几乎都依赖于WDK(WindowsDriverKit)。但是不限WDK的版本。WDK还在不断的升级中。这个开发包是由微软公司免费提供的。读者可以在微软的网站上下载。

当然读者必须把WDK安装的计算机上并配置好开发环境。具体的安装和配置方法本书没有提供。因为网上已经有非常多的中文文档介绍它们。

读完这本书之后,读者一定可以更轻松的阅读其他专门的驱动程序开发的文档和相关书籍。而不至于看到大量无法理解的代码而中途放弃。如果有任何关于本书的内容的问题,读者可以随时发邮件到mfc_tan_wen@163.com或者walled_river@hotmail.com。能够回答的问题我一般都会答复。

写本书的时候,我和wowocock合作的一本名为《天书夜读》(在网上有一个大约20%内容的缩减电子版本)正在电子工业出版社编辑。预计还有不到一个月左右就会出版。这也是我自己所见的唯一一本中文原创的从汇编和反汇编角度来学习Windows内核编程和信息安全软件开发的书。希望读者多多支持。有想购买的读者请发邮件给我。我会在本书出版的第一时间,回复邮件告知购买的方法。

此外我正在写另一本关于Windows安全软件的驱动编程的书。但是题目还没有拟好。实际上,读者现在见到的免费版本的《Windows驱动编程基础教程》是从这本书的第一部分中节选出来的。这本书篇幅比较大,大约有600-800页。主要内容如下:

第一章驱动编程基础

第二章磁盘设备驱动

第三章磁盘还原与加密

第四章传统文件系统过滤

第五章小端口文件系统过滤

第六章文件系统保护与加密

第七章协议网络驱动

第八章物理网络驱动

第九章网络防火墙与安全连接

第十章打印机驱动与虚拟打印

第十一章视频驱动与过滤

附录AWDK的安装与驱动开发的环境配置

附录B用WinDbg调试Windows驱动程序

这本书还没有完成。但是肯定要付出巨大的精力,所以请读者不要来邮件索取完整的免费的电子版本。希望读者支持本书的纸版出版。因为没有完成,所以还没有联系出版商。有愿意合作出版本书的读者请发邮件与我联系。

凡是发送邮件给我的读者,我将会发送邮件提供本人作品最新的出版信息,以及最新发布的驱动开发相关的免费电子书。如果不需要这些信息的,请在邮件里注明,或者回复邮件给我来取消订阅。



谭文

2008年6月9日



目录


版权声明 1

作者信息 1

前言 2

目录 4

第一章字符串 6

1.1使用字符串结构 6

1.2字符串的初始化 7

1.3字符串的拷贝 8

1.4字符串的连接 8

1.5字符串的打印 9

第二章内存与链表 11

2.1内存的分配与释放 11

2.2使用LIST_ENTRY 12

2.3使用长长整型数据 14

2.4使用自旋锁 15

第三章文件操作 18

3.1使用OBJECT_ATTRIBUTES 18

3.2打开和关闭文件 18

3.3文件的读写操作 21

第四章操作注册表 25

4.1注册键的打开操作 25

4.2注册值的读 26

4.3注册值的写 29

第五章时间与定时器 30

5.1获得当前滴答数 30

5.2获得当前系统时间 31

5.3使用定时器 32

第六章内核线程 35

6.1使用线程 35

6.2在线程中睡眠 36

6.3使用事件通知 37

第七章驱动与设备 41

7.1驱动入口与驱动对象 41

7.2分发函数与卸载函数 41

7.3设备与符号链接 42

7.4设备的生成安全性限制 44

7.5符号链接的用户相关性 46

第八章处理请求 47

8.1IRP与IO_STACK_LOCATION 47

8.2打开与关闭的处理 48

8.3应用层信息传入 49

8.4驱动层信息传出 51

后记:我的闲言碎语 54









第一章字符串


1.1使用字符串结构

常常使用传统C语言的程序员比较喜欢用如下的方法定义和使用字符串:


char *str={“myfirststring”}; //ansi字符串

wchar_t *wstr={L”myfirststring”}; //unicode字符串

size_tlen=strlen(str); //ansi字符串求长度

size_twlen=wcslen(wstr); //unicode字符串求长度

printf(“%s%ws%d%d”,str,wstr,len,wlen); //打印两种字符串


但是实际上这种字符串相当的不安全。很容易导致缓冲溢出漏洞。这是因为没有任何地方确切的表明一个字符串的长度。仅仅用一个’’字符来标明这个字符串的结束。一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),程序就可能陷入崩溃。

使用高级C++特性的编码者则容易忽略这个问题。因为常常使用std::string和CString这样高级的类。不用去担忧字符串的安全性了。

在驱动开发中,一般不再用空来表示一个字符串的结束。而是定义了如下的一个结构:


typedefstruct_UNICODE_STRING{

USHORTLength; //字符串的长度(字节数)

USHORTMaximumLength; //字符串缓冲区的长度(字节数)

PWSTR Buffer; //字符串缓冲区

}UNICODE_STRING,*PUNICODE_STRING;


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


typedefstruct_STRING{

USHORTLength;

USHORTMaximumLength;

PSTRBuffer;

}ANSI_STRING,*PANSI_STRING;


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

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


UNICODE_STRINGstr;

len=wcslen(str.Buffer); //试图求长度。

DbgPrint(“%ws”,str.Buffer); //试图打印str.Buffer。


如果要用以上的方法,必须在编码中保证Buffer始终是以空结束。但这又是一个麻烦的问题。所以,使用微软提供的Rtl系列函数来操作字符串,才是正确的方法。下文逐步的讲述这个系列的函数的使用。

1.2字符串的初始化

请回顾之前的UNICODE_STRING结构。读者应该可以注意到,这个结构中并不含有字符串缓冲的空间。这是一个初学者常见的出问题的来源。以下的代码是完全错误的,内核会立刻崩溃:


UNICODE_STRINGstr;

wcscpy(str.Buffer,L”myfirststring!”);

str.Length=str.MaximumLength=wcslen(L”myfirststring!”)*sizeof(WCHAR);


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


//先定义后,再定义空间

UNICODE_STRINGstr;

str.Buffer=L”myfirststring!”;

str.Length=str.MaximumLength=wcslen(L”myfirststring!”)*sizeof(WCHAR);

……


上面代码的第二行手写的常数字符串在代码中形成了“常数”内存空间。这个空间位于代码段。将被分配于可执行页面上。一般的情况下不可写。为此,要注意的是这个字符串空间一旦初始化就不要再更改。否则可能引发系统的保护异常。实际上更好的写法如下:


//请分析一下为何这样写是对的:

UNICODE_STRINGstr={

sizeof(L”myfirststring!”)–sizeof((L”myfirststring!”)[0]),

sizeof(L”myfirststring!”),

L”myfirst_string!”};


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


#include

UNICODE_STRINGstr=RTL_CONSTANT_STRING(L“myfirststring!”);


这只能在定义这个字符串的时候使用。为了随时初始化一个字符串,可以使用RtlInitUnicodeString。示例如下:


UNICODE_STRINGstr;

RtlInitUnicodeString(&str,L”myfirststring!”);


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


1.3字符串的拷贝

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

下面举一个例子。


UNICODE_STRINGdst; //目标字符串

WCHARdst_buf[256]; //我们现在还不会分配内存,所以先定义缓冲区

UNICODE_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!”);


//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串。

RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));

RtlCopyUnicodeString(&dst,&src); //字符串拷贝!


以上这个拷贝之所以可以成功,是因为256比L”Mysourcestring!”的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。

我曾经犯过的一个错误是没有调用RtlInitEmptyString。结果dst字符串被初始化认为缓冲区长度为0。虽然程序没有崩溃,却实际上没有拷贝任何内容。

在拷贝之前,最谨慎的方法是根据源字符串的长度动态分配空间。在1.2节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。


1.4字符串的连接

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


NTSTATUSstatus;

UNICODE_STRINGdst; //目标字符串

WCHARdst_buf[256]; //我们现在还不会分配内存,所以先定义缓冲区

UNICODE_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!”);


//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串

RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));

RtlCopyUnicodeString(&dst,&src); //字符串拷贝!


status=RtlAppendUnicodeToString(

&dst,L”mysecondstring!”);

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

//任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配

//内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空间

//定义在局部变量中,也就是所谓的“在栈中”

WCHARbuf[512]={0};

UNICODE_STRINGdst;

NTSTATUSstatus;

……


//字符串初始化为空串。缓冲区长度为512*sizeof(WCHAR)

RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));


//调用RtlStringCbPrintfW来进行打印

status=RtlStringCbPrintfW(

dst->Buffer,L”filepath=%wZfilesize=%drn”,

&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()无论是发行版本还是调试版本编译都会有效。为此可以自己定义一个宏:


#ifDBG

KdPrint(a) DbgPrint##a

#else

KdPrint(a)

#endif


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


//调用KdPrint来进行输出调试信息

status=KdPrint((

L”filepath=%wZfilesize=%drn”,

&file_path,file_size));


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


第二章内存与链表


2.1内存的分配与释放

内存泄漏是C语言中一个臭名昭著的问题。但是作为内核开发者,读者将有必要自己来面对它。在传统的C语言中,分配内存常常使用的函数是malloc。这个函数的使用非常简单,传入长度参数就得到内存空间。在驱动中使用内存分配,这个函数不再有效。驱动中分配内存,最常用的是调用ExAllocatePoolWithTag。其他的方法在本章范围内全部忽略。回忆前一小节关于字符串的处理的情况。一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。下面的举例,是把一个字符串src拷贝到字符串dst。


//定义一个内存分配标记

#defineMEM_TAG ‘MyTt’

//目标字符串,接下来它需要分配空间。

UNICODE_STRINGdst={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_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!”);

ExFreePool(src.Buffer);


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


2.2使用LIST_ENTRY

Windows的内核开发者们自己开发了部分数据结构,比如说LIST_ENTRY。

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


typedefstruct{

PFILE_OBJECTfile_object;

UNICODE_STRINGfile_name;

LARGE_INTEGERfile_length;

}MY_FILE_INFOR,*PMY_FILE_INFOR;


一些读者会马上注意到文件的长度用LARGE_INTEGER表示。这是一个代表长长整型的数据结构。这个结构我们在下一小小节“使用长长整型数据”中介绍。

为了让上面的结构成为链表节点,我必须在里面插入一个LIST_ENTRY结构。至于插入的位置并无所谓。可以放在最前,也可以放中间,或者最后面。但是实际上读者很快会发现把LIST_ENTRY放在开头是最简单的做法:


typedefstruct{

LIST_ENTRYlist_entry;

PFILE_OBJECTfile_object;

UNICODE_STRINGfile_name;

LARGE_INTEGERfile_length;

}MY_FILE_INFOR,*PMY_FILE_INFOR;


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


//我们的链表头

LIST_ENTRY my_list_head;


//链表头初始化。一般的说在应该在程序入口处调用一下

voidMyFileInforInilt()

{

InitializeListHead(&my_list_head);

}


//我们的链表节点。里面保存一个文件名和一个文件长度信息。

typedefstruct{

LIST_ENTRYlist_entry;

PFILE_OBJECTfile_object;

PUNICODE_STRINGfile_name;

LARGE_INTEGERfile_length;

}MY_FILE_INFOR,*PMY_FILE_INFOR;


//追加一条信息。也就是增加一个链表节点。请注意file_name是外面分配的。

//内存由使用者管理。本链表并不管理它。

NTSTATUSMyFileInforAppendNode(

PFILE_OBJECTfile_object,

PUNICODE_STRINGfile_name,

PLARGE_INTEGERfile_length)

{//NT是网络终端的意思这个函数是在内存中开辟空间创建一个节点的函数

PMY_FILE_INFORmy_file_infor=

(PMY_FILE_INFOR)ExAllocatePoolWithTag(

PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);

if(my_file_infor==NULL)

returnSTATUS_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);

returnSTATUS_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)

{//遍历查找FlinkBlink

PMY_FILE_INFORelem=

CONTAINING_RECORD(p,MY_FILE_INFOR,list_entry);

//在这里做需要做的事…

}

}


其中的CONTAINING_RECORD是一个WDK中已经定义的宏,作用是通过一个LIST_ENTRY结构的指针,找到这个结构所在的节点的指针。定义如下:



#defineCONTAINING_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。定义写法如下:


__int64file_offset;


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


typedef__int64LONGLONG;

typedefunion_LARGE_INTEGER{

struct{

ULONGLowPart;

LONGHighPart;

};

struct{

ULONGLowPart;

LONGHighPart;

}u;

LONGLONGQuadPart;

}LARGE_INTEGER;


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


LARGE_INTEGERa,b;

a.QuadPart=100;

a.QuadPart*=100;

b.QuadPart=a.QuadPart;

if(b.QuadPart>1000)

{

KdPrint(“b.QuadPart<1000,LowPart=%xHighPart=%x”,b.LowPart,b.HighPart);

}


上面这段代码演示了这种结构的一般用法。在实际编程中,会碰到大量的参数是LARGE_INTEGER类型的。

2.4使用自旋锁

链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。这里只介绍最简单的自选锁。

有些读者可能疑惑锁存在的意义。这和多线程操作有关。在驱动开发的代码中,大多是存在于多线程执行环境的。就是说可能有几个线程在同时调用当前函数。

这样一来,前文1.2.2中提及的追加链表节点函数就根本无法使用了。因为MyFileInforAppendNode这个函数只是简单的操作链表。如果两个线程同时调用这个函数来操作链表的话:注意这个函数操作的是一个全局变量链表。换句话说,无论有多少个线程同时执行,他们操作的都是同一个链表。这就可能发生,一个线程插入一个节点的同时,另一个线程也同时插入。他们都插入同一个链表节点的后边。这时链表就会发生问题。到底最后插入的是哪一个呢?要么一个丢失了。要么链表被损坏了。

如下的代码初始化获取一个自选锁:


KSPIN_LOCKmy_spin_lock;

KeInitializeSpinLock(&my_spin_lock);


KeInitializeSpinLock这个函数没有返回值。下面的代码展示了如何使用这个SpinLock。在KeAcquireSpinLock和KeReleaseSpinLock之间的代码是只有单线程执行的。其他的线程会停留在KeAcquireSpinLock等候。直到KeReleaseSpinLock被调用。KIRQL是一个中断级。KeAcquireSpinLock会提高当前的中断级。但是目前忽略这个问题。中断级在后面讲述。


KIRQLirql;

KeAcquireSpinLock(&my_spin_lock,&irql);

//Todosomething…

KeReleaseSpinLock(&my_spin_lock,irql);


初学者要注意的是,像下面写的这样的“加锁”代码是没有意义的,等于没加锁:


voidMySafeFunction()

{

KSPIN_LOCKmy_spin_lock;

KIRQLirql;

KeInitializeSpinLock(&my_spin_lock);

KeAcquireSpinLock(&my_spin_lock,&irql);

//在这里做要做的事情…

KeReleaseSpinLock(&my_spin_lock,irql);

}


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

LIST_ENTRY有一系列的操作。这些操作并不需要使用者自己调用获取与释放锁。只需要为每个链表定义并初始化一个锁即可:



LIST_ENTRY my_list_head; //链表头

KSPIN_LOCK my_list_lock; //链表的锁


//链表初始化函数

voidMyFileInforInilt()

{

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中。



第三章文件操作


在内核中不能调用用户层的Win32API函数来操作文件。在这里必须改用一系列与之对应的内核函数。

3.1使用OBJECT_ATTRIBUTES

一般的想法是,打开文件应该传入这个文件的路径。但是实际上这个函数并不直接接受一个字符串。使用者必须首先填写一个OBJECT_ATTRIBUTES结构。在文档中并没有公开这个OBJECT_ATTRIBUTES结构。这个结构总是被InitializeObjectAttributes初始化。

下面专门说明InitializeObjectAttributes。


VOIDInitializeObjectAttributes(

OUTPOBJECT_ATTRIBUTESInitializedAttributes,

INPUNICODE_STRINGObjectName,

INULONGAttributes,

INHANDLERootDirectory,

INPSECURITY_DESCRIPTORSecurityDescriptor);


读者需要注意的以下的几点:InitializedAttributes是要初始化的OBJECT_ATTRIBUTES结构的指针。ObjectName则是对象名字字符串。也就是前文所描述的文件的路径(如果要打开的对象是一个文件的话)。

Attributes则只需要填写OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE即可(如果读者是想要方便的简洁的打开一个文件的话)。OBJ_CASE_INSENSITIVE意味着名字字符串是不区分大小写的。由于Windows的文件系统本来就不区分字母大小写,所以笔者并没有尝试过如果不设置这个标记会有什么后果。OBJ_KERNEL_HANDLE表明打开的文件句柄一个“内核句柄”。内核文件句柄比应用层句柄使用更方便,可以不受线程和进程的限制。在任何线程中都可以读写。同时打开内核文件句柄不需要顾及当前进程是否有权限访问该文件的问题(如果是有安全权限限制的文件系统)。如果不使用内核句柄,则有时不得不填写后面的的SecurityDescriptor参数。

RootDirectory用于相对打开的情况。目前省略。请读者传入NULL即可。

SecurityDescriptor用于设置安全描述符。由于笔者总是打开内核句柄,所以很少设置这个参数。

3.2打开和关闭文件

下面的函数用于打开一个文件:


NTSTATUSZwCreateFile(

OUTPHANDLEFileHandle,

INACCESS_MASKDesiredAccess,

INPOBJECT_ATTRIBUTESObjectAttribute,

OUTPIO_STATUS_BLOCKIoStatusBlock,

INPLARGE_INTEGERAllocationSizeOPTIONAL,

INULONGFileAttributes,

INULONGShareAccess,

INULONGCreateDisposition,

INULONGcreateOptions,

INPVOIDEaBufferOPTIONAL,

INULONGEaLength);


这个函数的参数异常复杂。下面逐个的说明如下:

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


typedefstruct_IO_STATUS_BLOCK{

union{

NTSTATUSStatus;

PVOIDPointer;

};

ULONG_PTRInformation;

}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字节)对齐。否则会返回错误。

这个函数是如此的繁琐,以至于再多的文档也不如一个可以利用的例子。早期笔者调用这个函数往往因为参数设置不对而导致打开失败。非常渴望找到一个实际可以使用的参数的范例。下面举例如下:


//要返回的文件句柄

HANDLEfile_handle=NULL;

//返回值

NTSTATUSstatus;

//首先初始化含有文件路径的OBJECT_ATTRIBUTES

OBJECT_ATTRIBUTESobject_attributes;

UNICODE_STRINGufile_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);

3.3文件的读写操作

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


NTSTATUS

ZwReadFile(

INHANDLEFileHandle,

INHANDLEEventOPTIONAL,

INPIO_APC_ROUTINEApcRoutineOPTIONAL,

INPVOIDApcContextOPTIONAL,

OUTPIO_STATUS_BLOCKIoStatusBlock,

<p style="background:rgb(

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭