Windows驱动编程入门.docx
- 文档编号:10617509
- 上传时间:2023-02-21
- 格式:DOCX
- 页数:26
- 大小:31.26KB
Windows驱动编程入门.docx
《Windows驱动编程入门.docx》由会员分享,可在线阅读,更多相关《Windows驱动编程入门.docx(26页珍藏版)》请在冰豆网上搜索。
Windows驱动编程入门
Windows驱动编程入门1
前言
我经常在网上遇到心如火燎的提问者。
他们碰到很多工作中的技术问题,是关于驱动开发的。
其实绝大部分他们碰到的“巨大困难”是被老牛们看成初级得不能再初级的问题。
比如经常有人定义一个空的UNICODE_STRING,然后往里面拷贝字符串。
结果无论如何都是蓝屏。
也有人在堆栈中定义一个局部SPIN_LOCK,作为下面的同步用——这样用显然没有任何意义。
我无法一一回答这些问题:
因为往往要耐心的看他们的代码,才能很不容易的发现这些错误。
而且我又不是总是空闲的,可以无休止的去帮网友阅读代码和查找初级错误。
但是归根结底,这些问题的出现,是因为现在写驱动的同行越来越多,但是做驱动开发又没有比较基础的,容易读懂的资料。
为此我决定从今天开始连载一篇超级入门级的教程,来解决那些最基本的开发问题。
老牛们就请无视这篇教程,一笑而过了。
Windows驱动编程基础教程(1.1-1.3)
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); //打印两种字符串
但是实际上这种字符串相当的不安全。
很容易导致缓冲溢出漏洞。
这是因为没有任何地方确切的表明一个字符串的长度。
仅仅用一个’\0’字符来标明这个字符串的结束。
一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),程序就可能陷入崩溃。
使用高级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节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。
Windows驱动编程入门2
Windows驱动编程基础教程(1.4-2.1)
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,这种情况请调用
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=%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()无论是发行版本还是调试版本编译都会有效。
为此可以自己定义一个宏:
#ifDBG
KdPrint(a) DbgPrint##a
#else
KdPrint(a)
#endif
不过这样的后果是,由于KdPrint(a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。
导致KdPrint看起来很奇特的用了双重括弧:
//调用KdPrint来进行输出调试信息
status=KdPrint((
L”filepath=%wZfilesize=%d\r\n”,
&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的成对关系。
Windows驱动编程入门3
Windows驱动编程基础教程(2.2)
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)
{
PMY_FILE_INFORmy_file_infor=
(PMY_FILE_INFOR)ExAllocatePoolWithTag(
PagedPool,sizeof(MY_FILE_INFOR),MEM_TAG);
if(my_file_infor==
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Windows 驱动 编程 入门