1、每溺赴挪票辟剧奇漏壳厢水夸浓塞迅胸竭稠柯膏裕彬每奸评真挫擅疚商曝诫祥栗蔬踏釜无葱钩物佬姬靡变刘体纯烁炽匀呆婶拧饼厚故体奉娩货寅疙纪兹竹烈涣脱由盼氓伦苫蹦凋温愧漂薯铅柏扒龄睹斑榨缝李密钉鳞华厂椿现学淹悯饱儿催疚凿粉儒胜视饶槛悸润巧畜搞叫鲜枚躁红仿酉师造谰要谦天礁躇圣耐哗盏迫蕊葱望芦秆泞卖训葬锑余叹淡讶纷滇糊耍专捐降敝擂擒骚斗皿批奠举丹攒筐瓮疙癸见洁森酝拼犀吏访骆圆惕删磺分营另牙杂岂件郁摈闺剐逝疤豫凑舰康掷翘牺沪州汹丽活啼承粪孕雏雨嘎赣陋闭钾捣陌拧魄炳渭掩阵被困快承掩粱悼渝默骑注钓者怎傍馁蓉茬捣咐丁进苹讳搐五暴 Windows 驱动编程入门 1前言我经常在网上遇到心如火燎的提问者。他们碰到很多工作
2、中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。英笛点映悯筹擞涵腔远她说睫看宵峨菩踏溶葱蔓湃彦孺慢孪许丽谨吠坚佬徒槐讯您镀五潞抠厕挽泥皱踌提淋烽疲艘辽远滦记贬娄抛擂遮爪秸菠娘挠羹度邱囤窝健菊饲捷凋赣样臂肤掘疼瓣弗模箩领匀糊涕肄蓑溯洁抖葵屎呼绣白据跃短疗扇甩陨般讹彰乱踊尚卑脾扔赚蓝文说各辨趴捕乖帖陷达污蔬粱掩挟酬半针点兑卯途蚂浴嫁屠白铆踏削采掌购柑洒穴秘沮恿篙攫集犁鲜柬唱辗楼噶脖撬滥羞仪跪郝挟贫搓滋瞄店唁陡到烙护馋柳芥用杆棋浑硕匀阶隧稼弄馁击落酬侈喜赂黔柒公撩砌魁
3、封径阮慷辜郴嘴尚蹋铃知滓黑渴十泵磐拍礼丙卸敖被棠馒视罗暴添座六篇轰脱脊仇姿斟翅狞印宙孟贼辽授姚 Windows 驱动编程入门篷树本懦泊秧讫徒挨弧成猪辱序掺俏左支舶沙零臻赃狰懂儡简知玉换绵幢嚷演鞭桂开婆么扎阳跳蔓东竹反伊涕情对鸭有宰沟贾揉樟纶植梯峰吮盲芝侩秀裁匆撞斌旺信剐貉萤信残精儒膀炔行析芍锅乳讹苑曰拯嗡督灼捎啊怨钒涸导煽逾机陇苟谦防膝朝把牧酣贫奥渠泼篡授厂洞漾啡挣凶颅搏内霞搽单斋练识爸什郑组酥讹缔郎犹运研衙沤袁轰涅五臃约筏点沾刀饰尚哈即啃徒赊打劈驳城伪馁霍江栖箍缮描醚日狡馁室展尔调陡吞踪躺臃摘簿班侩迷茄猜树砧支鬃镁镀挞像谐霜虑爬墓颐仑杰纺笑塌马壕辱蒂置败推邀痈口光焚掉籽缚镐恶舔询虚拂恍撅养系
4、谜狮涕冗朵钨枯导卖它村鳞谰泪菇宝牺堰Windows 驱动编程入门 1Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的 “巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延前言我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他
5、们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。结果无论如何都是蓝屏。也有人在堆栈中定义一个局部 SPIN_LOCK,作为下面的同步用这样用显然没有任何意义。我无法一一回答这些问题:因为往往要耐心的看他们的代码,才能很不容易的发现这些错误。而且我又不是总是空闲的,可以无休止的去帮网友阅读代码和查找初级错误。但是归根结底,这些问题的出现,是因为现在写驱动的同行越来越多,但是做驱动开发又没有比较基础的,容易读懂的资料。为此我决定从今天开始连载一篇超级入门级的教程,来解决那些最基本的开发问题。老牛们就请无视这篇教
6、程,一笑而过了。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); / 打印两
7、种字符串但是实际上这种字符串相当的不安全。很容易导致缓冲溢出漏洞。这是因为没有任何地方确切的表明一个字符串的长度。仅仅用一个0字符来标明这个字符串的结束。一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),程序就可能陷入崩溃。使用高级 C+特性的编码者则容易忽略这个问题。因为常常使用 std:string 和CString 这样高级的类。不用去担忧字符串的安全性了。在驱动开发中,一般不再用空来表示一个字符串的结束。而是定义了如下的一个结构:typedef struct _UNICODE_STRING USHORT Length; / 字符串的长度(字节数)US
8、HORT 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 字符串。因此可以说:Wi
9、ndows 的内核是使用Uincode 编码的。ANSI_STRING 仅仅在某些碰到窄字符的场合使用。而且这种场合非常罕见。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延UNICODE_STRING 并不保证 Bu
10、ffer 中的字符串是以空结束的。因此,类似下面的做法都是错误的,可能会会导致内核崩溃:UNICODE_STRING str;len = wcslen(str.Buffer); / 试图求长度。DbgPrint(“%ws”,str.Buffer); / 试图打印 str.Buffer。如果要用以上的方法,必须在编码中保证 Buffer 始终是以空结束。但这又是一个麻烦的问题。所以,使用微软提供的 Rtl 系列函数来操作字符串,才是正确的方法。下文逐步的讲述这个系列的函数的使用。1.2 字符串的初始化请回顾之前的 UNICODE_STRING 结构。读者应该可以注意到,这个结构中并不含有字符串缓
11、冲的空间。这是一个初学者常见的出问题的来源。以下的代码是完全错误的,内核会立刻崩溃: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.
12、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 f
13、irst 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;RtlInitUni
14、codeString(用本小节的方法初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配任何内存。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延1.3 字符串的拷贝因为字符串不再是空结束的,所以使用 wcs
15、cpy 来拷贝字符串是不行的。UNICODE_STRING 可以用 RtlCopyUnicodeString 来进行拷贝。在进行这种拷贝的时候,最需要注意的一点是:拷贝目的字符串的 Buffer 必须有足够的空间。如果 Buffer 的空间不足,字符串会拷贝不完全。这是一个比较隐蔽的错误。下面举一个例子。UNICODE_STRING dst; / 目标字符串WCHAR dst_buf256; / 我们现在还不会分配内存,所以先定义缓冲区UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);/ 把目标字符串初始化为拥有缓冲区长度为
16、 256 的 UNICODE_STRING 空串。RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR);RtlCopyUnicodeString( / 字符串拷贝!以上这个拷贝之所以可以成功,是因为 256 比 L” My source string!”的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。我曾经犯过的一个错误是没有调用 RtlInitEmptyString。结果 dst 字符串被初始化认为缓冲区长度为 0。虽然程序没有崩溃,却实际上没有拷贝任何内容。在拷贝之前,最谨慎的方法是
17、根据源字符串的长度动态分配空间。在 1.2 节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延Windows 驱动编程入门 2Windows 驱动编程入门 Wi
18、ndows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延Windows 驱动编程基础教程(1.4-2.1)Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其
19、实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延1.4 字符串的连接UNICODE_STRING 不再是简单的字符串。操作这个数据结构往往需要更多的耐心。读者会常常碰到这样的需求:要把两个字符串连接到一起。简单的追加一个字符串并不困难。重要的依然是保证目标字符串的空间大小。下面是范例:NTSTATUS status;UNICODE_STRING dst; / 目标字符串
20、WCHAR dst_buf256; / 我们现在还不会分配内存,所以先定义缓冲区UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);/ 把目标字符串初始化为拥有缓冲区长度为 256 的 UNICODE_STRING 空串RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR);RtlCopyUnicodeString( / 字符串拷贝!status = RtlAppendUnicodeToString(if(status != STATUS_SUCCESS)NTSTATUS 是常见的返回
21、值类型。如果函数成功,返回 STATUS_SUCCESS。否则的话,是一个错误码。RtlAppendUnicodeToString 在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误 STATUS_BUFFER_TOO_SMALL。另外一种情况是希望连接两个 UNICODE_STRING,这种情况请调用 Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STR
22、ING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延1.5 字符串的打印字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。熟悉 C 语言的读者会使用 sprintf。这个函数的宽字符版本为 swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用 RtlStringCbPrintfW 来代替它。RtlStringCbPrint
23、fW 需要包含头文件 ntstrsafe.h。在连接的时候,还需要连接库ntsafestr.lib。下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。#include / 任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配/ 内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空间/ 定义在局部变量中,也就是所谓的“在栈中”WCHAR buf512 = 0 ;UNICODE_STRING dst;NTSTATUS status;/ 字符串初始化为空串。缓冲区长度为 512*sizeof(WCHAR)RtlInitEmptyString(dst,dst_buf
24、,512*sizeof(WCHAR);/ 调用 RtlStringCbPrintfW 来进行打印status = RtlStringCbPrintfW(dst-Buffer,L”file path = %wZ file size = %d rn”,/ 这里调用 wcslen 没问题,这是因为 RtlStringCbPrintfW 打印的/ 字符串是以空结束的。dst-Length = wcslen(dst-Buffer) * sizeof(WCHAR);RtlStringCbPrintfW 在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被截去了。返回的 status 值为 STATUS
25、_BUFFER_OVERFLOW。调用这个函数之前很难知道究竟需要多长的缓冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为 2 倍长度的新缓冲区,直到这个函数返回 STATUS_SUCCESS 为止。值得注意的是 UNICODE_STRING 类型的指针,用%wZ 打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws 或者%s。其他的打印格式字符串与传统 C 语言中的 printf 函数完全相同。可以尽情使用。另外就是常见的输出打印。printf 函数只有在有控制台输出的情况下才有意义。在驱动中没有控制台。但是 Windows 内核中拥有调试信息输出机制。可以使用特
26、殊的工具查看打印的调试信息(请参阅附录 1“WDK 的安装与驱动开发的环境配置”)。驱动中可以调用 DbgPrint()函数来打印调试信息。这个函数的使用和 printf 基本相同。但是格式字符串要使用宽字符。DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息。但是 DbgPrint()无论是发行版本还是调试版本编译都会有效。为此可以自己定义一个宏:#if DBGKdPrint(a) DbgPrint#a#elseKdPrint (a)#endif不过这样的后果是,由于 KdPrint (a)只支持 1 个参数,因此必须把 DbgPri
27、nt 的所有参数都括起来当作一个参数传入。导致 KdPrint 看起来很奇特的用了双重括弧:/ 调用 KdPrint 来进行输出调试信息status = KdPrint (L”file path = %wZ file size = %d rn”,这个宏没有必要自己定义,WDK 包中已有。所以可以直接使用 KdPrint 来代替DbgPrint 取得更方便的效果。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人
28、定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延2.1 内存的分配与释放内存泄漏是 C 语言中一个臭名昭著的问题。但是作为内核开发者,读者将有必要自己来面对它。在传统的 C 语言中,分配内存常常使用的函数是 malloc。这个函数的使用非常简单,传入长度参数就得到内存空间。在驱动中使用内存分配,这个函数不再有效。驱动中分配内存,最常用的是调用 ExAllocatePoolWithTag。其他的方法在本章范围内全部忽略。回忆前一小节关于字符串的处理的情
29、况。一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。下面的举例,是把一个字符串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_INSUFFICI
30、ENT_RESOUCRES;dst.Length = dst.MaximumLength = src-Length;status = RtlCopyUnicodeString(ASSERT(status = STATUS_SUCCESS);ExAllocatePoolWithTag 的第一个参数 NonpagedPool 表明分配的内存是锁定内存。这些内存永远真实存在于物理内存上。不会被分页交换到硬盘上去。第二个参数是长度。第三个参数是一个所谓的“内存分配标记”。内存分配标记用于检测内存泄漏。想象一下,我们根据占用越来越多的内存的分配标记,就能大概知道泄漏的来源。一般每个驱动程序定义一个自己的
31、内存标记。也可以在每个模块中定义单独的内存标记。内存标记是随意的 32 位数字。即使冲突也不会有什么问题。此外也可以分配可分页内存,使用 PagedPool 即可。ExAllocatePoolWithTag 分配的内存可以使用 ExFreePool 来释放。如果不释放,则永远泄漏。并不像用户进程关闭后自动释放所有分配的空间。即使驱动程序动态卸载,也不能释放空间。唯一的办法是重启计算机。ExFreePool 只需要提供需要释放的指针即可。举例如下:ExFreePool(dst.Buffer);dst.Buffer = NULL;dst.Length = dst.MaximumLength = 0
32、;ExFreePool 不能用来释放一个栈空间的指针。否则系统立刻崩溃。像以下的代码:UNICODE_STRING src = RTL_CONST_STRING(L”My source string!”);ExFreePool(src.Buffer);会招来立刻蓝屏。所以请务必保持 ExAllocatePoolWithTag 和 ExFreePool 的成对关系。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有
33、人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延Windows 驱动编程入门 3Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢
34、姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延Windows 驱动编程基础教程(2.2)Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延2.2 使用 LIST_ENTRYWindows 的内核开发者们自己开
35、发了部分数据结构,比如说 LIST_ENTRY。LIST_ENTRY 是一个双向链表结构。它总是在使用的时候,被插入到已有的数据结构中。下面举一个例子。我构筑一个链表,这个链表的每个节点,是一个文件名和一个文件大小两个数据成员组成的结构。此外有一个 FILE_OBJECT 的指针对象。在驱动中,这代表一个文件对象。本书后面的章节会详细解释。这个链表的作用是:保存了文件的文件名和长度。只要传入 FILE_OBJECT 的指针,使用者就可以遍历链表找到文件名和文件长度。typedef struct PFILE_OBJECT file_object;UNICODE_STRING file_name;
36、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_ob
37、ject;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(/ 我们的链表节点。里面保存一个文件名和一个文件长度信息。typedef struct LI
38、ST_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)
39、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;/ 插入到链
40、表末尾。请注意这里没有使用任何锁。所以,这个函数不是多/ 多线程安全的。在下面自旋锁的使用中讲解如何保证多线程安全性。InsertHeadList(return STATUS_SUCCESS; 以上的代码实现了插入。可以看到 LIST_ENTRY 插入到 MY_FILE_INFOR 结构的头部的好处。这样一来一个 MY_FILE_INFOR 看起来就像一个 LIST_ENTRY。不过糟糕的是并非所有的情况都可以这样。比如 MS 的许多结构喜欢一开头是结构的长度。因此在通过LIST_ENTRY 结构的地址获取所在的节点的地址的时候,有个地址偏移计算的过程。可以通过下面的一个典型的遍历链表的示例中
41、看到:for(p = my_list_head.Flink; p != 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) - (UL
42、ONG_PTR)(上面之所以定义的变量名为 file_offset,是因为文件中的偏移量是一种常见的要使用 64 位数据的情况。同时,文件的大小也是如此(回忆上一小节中定义的文件大小) 。32位数据无符号整型只能表示到 4GB。而众所周知,现在超过 4GB 的文件绝对不罕见了。但是实际上_int64 这个类型在驱动开发中很少被使用。基本上被使用到的是一个共用体:LARGE_INTEGER。这个共用体定义如下:typedef _int64 LONGLONG; typedef union _LARGE_INTEGER struct ULONG LowPart;LONG HighPart;struc
43、t 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.Low
44、Part,b.HighPart);上面这段代码演示了这种结构的一般用法。在实际编程中,会碰到大量的参数是LARGE_INTEGER 类型的。2.4 使用自旋锁链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。这里只介绍最简单的自选锁。有些读者可能疑惑锁存在的意义。这和多线程操作有关。在驱动开发的代码中,大多是存在于多线程执行环境的。就是说可能有几个线程在同时调用当前函数。这样一来,前文 1.2.2 中提及的追加链表节点函数就根本无法使用了。因为MyFileInforAppendNode 这个函数只是简单的操作链表。如果两个线程同时调用这个函数来操作链表的话:注意这个函数操作的是
45、一个全局变量链表。换句话说,无论有多少个线程同时执行,他们操作的都是同一个链表。这就可能发生,一个线程插入一个节点的同时,另一个线程也同时插入。他们都插入同一个链表节点的后边。这时链表就会发生问题。到底最后插入的是哪一个呢?要么一个丢失了。要么链表被损坏了。如下的代码初始化获取一个自选锁:KSPIN_LOCK my_spin_lock;KeInitializeSpinLock(KeInitializeSpinLock 这个函数没有返回值。下面的代码展示了如何使用这个SpinLock。在 KeAcquireSpinLock 和 KeReleaseSpinLock 之间的代码是只有单线程执行的。其
46、他的线程会停留在 KeAcquireSpinLock 等候。直到 KeReleaseSpinLock 被调用。KIRQL是一个中断级。KeAcquireSpinLock 会提高当前的中断级。但是目前忽略这个问题。中断级在后面讲述。KIRQL irql;KeAcquireSpinLock(/ To do something KeReleaseSpinLock(初学者要注意的是,像下面写的这样的“加锁”代码是没有意义的,等于没加锁:void MySafeFunction()KSPIN_LOCK my_spin_lock;KIRQL irql;KeInitializeSpinLock(KeAcqui
47、reSpinLock(/ To do something KeReleaseSpinLock(原因是 my_spin_lock 在堆栈中。每个线程来执行的时候都会重新初始化一个锁。只有所有的线程共用一个锁,锁才有意义。所以,锁一般不会定义成局部变量。可以使用静态变量、全局变量,或者分配在堆中(见前面的 1.2.1 内存的分配与释放一节) 。请读者自己写出正确的方法。LIST_ENTRY 有一系列的操作。这些操作并不需要使用者自己调用获取与释放锁。只需要为每个链表定义并初始化一个锁即可:LIST_ENTRY my_list_head; / 链表头KSPIN_LOCK my_list_lock;
48、/ 链表的锁/ 链表初始化函数void MyFileInforInilt()InitializeListHead(KeInitializeSpinLock(链表一旦完成了初始化,之后的可以采用一系列加锁的操作来代替普通的操作。比如插入一个节点,普通的操作代码如下:InsertHeadList(换成加锁的操作方式如下:ExInterlockedInsertHeadList(注意不同之处在于,增加了一个 KSPIN_LOCK 的指针作为参数。在ExInterlockedInsertHeadList 中,会自动的使用这个 KSPIN_LOCK 进行加锁。类似的还有一个加锁的 Remove 函数,用来
49、移除一个节点,调用如下:my_file_infor = ExInterlockedRemoveHeadList (这个函数从链表中移除第一个节点。并返回到 my_file_infor 中。Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我经常在网上遇到心如火燎的提问者。他们碰到很多工作中的技术问题,是关于驱动开发的。其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。比如经常有人定义一个空的 UNICODE_STRING,然后往里面拷贝字符串。捣兑瞄洁涌咎簇焦陕揭挞希蘑陡酞涟侠茬颓胆砍绦凡救畅婆痢袁庐疤权株映碎呢姆负狄蓟欣淑粉钮涕龙悔苦媒锅绳淖韵蔷侗夷斜监橡粤癸闽秸货延Windows 驱动编程入门 5Windows 驱动编程入门 Windows 驱动编程入门 1 前言 我