您的位置:首页 > 其它

Windows内核驱动开发入门学习资料 + 内核数据类型和函数

2014-05-22 19:13 555 查看
声明:本文所描述的所有资料和源码均搜集自互联网,版权归原始作者所有,所以在引用资料时我尽量注明原始作者和出处;本文所搜集资料也仅供同学们学习之用,由于用作其他用途引起的责任纠纷,本人不负任何责任。(本资料由代码疯子整理)

一、书籍推荐

《Windows驱动开发技术详解》作者:张帆、史彩成;出版社:电子工业出版社
《天书夜读:从汇编语言到Windows内核编程》作者:谭文、邵坚磊;出版社:电子工业出版社
《寒江独钓:Windows内核安全编程》作者:谭文、杨潇、邵坚磊;出版社:电子工业出版社
其他驱动开发相关书籍

二、源码学习

《Windows驱动开发技术详解》源码下载:http://dl.dbank.com/c0rmlpwkfi
《寒江独钓:Windows内核安全编程》源码下载:http://dl.dbank.com/c0t5kawz2e
DDK 2600 驱动例子源码下载:http://dl.dbank.com/c0oktzwfsf
WDK 7600 驱动例子源码下载:http://dl.dbank.com/c0hj1khp8c

三、学习网站

看雪论坛:http://bbs.pediy.com/
驱网:http://bbs.driverdevelop.com/
帆子内核驱动网:http://bbs.kerneldev.com/
看雪KSSD:http://kssd.pediy.com/
一蓑烟雨:http://www.unpack.cn/
吾爱破解:http://www.52pojie.cn/
DebugMan:http://www.debugman.com/

四、其他资料(整理自看雪)

驱动程序设计基础 http://bbs.pediy.com/showthread.php?t=56631 通俗解析IRP和I/O设备栈在内核程序中的作用 http://bbs.pediy.com/showthread.php?t=111559 我的驱动学习笔记 http://bbs.pediy.com/showthread.php?t=88723 下载附件
驱动入门:从WRK看IRP 理论篇 http://bbs.pediy.com/showthread.php?t=130876 下载附件
驱动入门:从WRK看IRP 实践篇 http://bbs.pediy.com/showthread.php?t=130925 下载附件 附件2

五、楚狂人(谭文)的Free教程

《Windows驱动编程基础教程》 http://dl.dbank.com/c05tt0rxe5 《Windows文件系统过滤驱动开发教程*第二版》 http://dl.dbank.com/c0n3p7haef
1、内核模块位于内核空间,而内核空间又被所有的进程共享。因此,内核模块实际上位于任何一个进程空间中。

  2、PsGetCurrentProcessId函数能得到当前进程的进程号,函数原型为: HANDLE PsGetCurrentProcessId();返回值时间上就是一个进程ID;

  3、当DriverEntry函数被调用时,一般位于系统进程中,因为Windows一般用系统进程来加载内核模块;但是内核模块中分发函数调用时,当前进程一般都不是System进程。

一、基本数据类型

1、基本类型

WDK编程规范中不直接使用unsigned long,而是使用已经重新定义过的ULONG。重新定义的好处是,万一有了什么问题,再重新定义一下就行,这些代码不至于产生不可控的问题

unsigned long重定义为ULONG。

unsigned char重定义为UCHAR。

unsigned int重定义为UINT。

void重定义为VOID。

unsigned long *重定义为PULONG。

unsigned char * 重定义为PUCHAR。

unsigned int *重定义PUINT。

void *重定义为PVOID

2、返回状态

绝大部分内核API的返回值都是一个返回状态,也就是一个错误码。这个类型为NTSTATUS

3、字符串

驱动里字符串一般用一个结构来容纳,这个结构的定义如下:
typedef struct _UNICODE_STRING {
USHORT  Length;
USHORT  MaximumLength;
PWSTR  Buffer;
} UNICODE_STRING *PUNICODE_STRING;    //将_UNICODE_STRING类型重定为UNICODE_STRING, 指针类型重定为PUNICODE_STRING


字符串的字符是宽字符,是双字节的。

在很多情况下不直接使用字符的指针,而是将这个结构作为字符串使用。

这个结构的指针可以直接在DbgPrint中打印,int用%d来打印,char用%c来打印一样,UNICODE_STRING的指针(注意是指针,结构本身不能打印)可以用%wZ来打印;

这个结构也是可以之间打印的:
UNICODE_STRING str = RTL_CONSTANT_STRING(L" first: Hello, my salary! ");
DbgPrint("%wZ",&str);

UNICODE_STRING只要知道这是宽字符串就可以啦~

二、重要数据类型

1、驱动对象


一个驱动对象代表了一个驱动程序,或者说一个内核模块。驱动对象的结构如下(这个结构的定义取自WDK中的wdm.h)(其中有一些域被笔者用省略号代替了):
typedef struct _DRIVER_OBJECT {
// 结构的类型和大小。
CSHORT Type;
CSHORT Size;

// 设备对象,这里实际上是一个设备对象的链表的开始。因为DeviceObject

// 中有相关链表信息。阅读下一小节“设备对象”会得到更多的信息

PDEVICE_OBJECT DeviceObject;

……

// 这个内核模块在内核空间中的开始地址和大小

PVOID DriverStart;

ULONG DriverSize;

……

// 驱动的名字

UNICODE_STRING DriverName;

……

// 快速IO分发函数

PFAST_IO_DISPATCH FastIoDispatch;

……

// 驱动的卸载函数

PDRIVER_UNLOAD DriverUnload;

// 普通分发函数

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

} DRIVER_OBJECT;

实际上,如果写一个驱动程序,或者说编写一个内核模块,要在Windows中加载,则必须填写这样一个结构,来告诉Windows程序提供哪些功能。

    内核模块并不生成一个进程,只是填写一组回调函数让Windows来调用,而且这组回调函数必须符合Windows内核规定。
2、设备对象
设备对象(DEVICE_OBJECT)是唯一可以接收请求的实体,任何一个“请求”(IRP)都是发送给某个设备对象的。内核程序是用一个驱动对象表
示的,所以一个设备对象总是属于一个驱动对象。在WDK的wdm.h中可以看到结构定义如下(这里省略了许多读者现在不需要了解的域):

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT {

// 和驱动对象一样
CSHORT Type;
USHORT Size;

// 引用计数
ULONG ReferenceCount;

// 这个设备所属的驱动对象
struct _DRIVER_OBJECT *DriverObject;

// 下一个设备对象。在一个驱动对象中有n个设备,这些设备用这个指针连接
// 起来作为一个单向的链表
struct _DEVICE_OBJECT *NextDevice;

// 设备类型
DEVICE_TYPE DeviceType;

// IRP栈大小
HAR StackSize;

……
}DEVICE_OBJECT;

从这个结构来看,读者应该发现驱动对象(DRIVER_OBJECT)和设备对象(DEVICE_OBJECT)之间的联系,这非常重要。驱动对象生成多个设备对象。而Windows向设备对象发送请求,但是这些请求如何处理呢?实际上,这些请求是被驱动对象的分发函数所捕获的。

当Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。分发函数原型如下:
// 一个典型的分发函数,第一个参数device是请求的目标设备,第二个参数irp是请求的指针
NTSTATUS MyDispatch(PDEVICE_OBJECT deivce, PIRP irp);

而具体做什么由用户编写啦。

3、请求
大部分请求以IRP的形式发送。IRP也是一个内核数据结构,这个结构非常复杂,这是因为这个结构要表示无数种实际请求的缘故。

4、函数调用
  哪些函数可以调用呢?答案是:WDK的帮助中有的函数,在内核编程中我们都可以调用(除去少部分有说明为用户态API的函数)。
    大部分内核API都有前缀,主要的函数以Io-、Ex-、Rtl-、Ke-、Zw-、Nt-和Ps-开头。
    此外,与NDIS网络驱动开发相关的函数几乎都是以Ndis-开头的,与开发WDF驱动相关的函数都是以Wdf-开头的。
    Io:以Io-开头的系列函数非常重要,如表2-5所示。因为涉及到IO管理器,IO管理器就是将用户调用的API函数翻译成IRP或者将等价请求发送到内核各个不同的设备的关键组件;
    Rtl:在进行字符串操作时,使用到大量的Rtl-函数;
    Zw:用于操作文件的Zw-系列函数(对应Nt-系列函数);
    Ex:常用的几个分配内存、获取互斥体等几个函数。

  WDK的帮助中没有的函数:


  基本上可以认为,大家常用的C运行时库中的函数,如果只涉及字符串和内存数据(而不涉及内存管理,比如内存的分配和释放),则是可以在内核程序里调用的。但是MS并不提倡这样做。

如果这些函数涉及内存管理、文件操作、网络操作、线程等,则往往在头文件中虽然有这个函数存在,甚至编译都能通过,但是连接却会出问题。

5、内核程序的主要调用源

  调用源并不是一个常用的概念,而是笔者为了读者理解的方便而自己设定的一个概念。首先我们假定函数B调用了函数A,则称函数B为A的调用者。

  那么任意指定一段代码,假设这段代码在函数A中。现在我们寻找A的调用者。假设找到一个调用者B,我们再继续寻找B的调用者。如此递归地寻找下去,直到在代码可见的范围内,最后一个调用者函数M。函数M无法在可见范围内的代码内找到调用者,那么就称函数M为函数A的调用源。换句话说,一段代码的调用源是指,调用的这段代码,编程者所能见到的最初始的源头的那个函数。

  同时,从调用源出发,嵌套地调用一系列函数,最后到调用函数B,函数B调用函数A这是一条“路径”,我们称之为函数A的调用路径。

  一个函数的调用路径显然可能不止一条。同时,一个函数的调用源也可能不止一个。  内核编程的一个显著特点是,任意一个函数往往可能有多个调用源。主要可以追溯到的调用源如下:

    (1)入口函数DriverEntry和卸载函数DriverUnload。

    (2)各种分发函数(包括普通分发函数和快速IO分发函数)。

    (3)处理请求时设置的完成函数。也就是说,该请求完成后会被系统调用的回调函数。

    (4)其他回调函数(如各类NDIS驱动程序的特征函数,NDIS驱动程序见本书的的第11~13章)。    还可能包括其他的调用源。
6、函数的多线程安全性


  一般的说,函数的多线程安全性是指,一个函数在被调用过程中,还未返回时,又再次被其他线程调用的情况下,函数执行结果

的可靠性。如果结果是可靠的,则称这个函数是多线程安全的;如果结果是不可靠的,则称这个函数是非多线程安全的,原因是多线

程冲突。

  函数的多线程冲突在内核编程中远比用户态应用程序的编程要常见。因此读者会常常听到忠告:要注意函数的多线程安全性。但

是严格地去保证每个函数的多线程安全性是浪费的,甚至有时是不可能的。因此我们需要判断何时需要保证函数的多线程安全性。读

者可以通过下面几条规则来简单地判断:

  规则1:可能运行于多线程环境的函数,必须是多线程安全的。只运行于单线程环境的函数,则不需要多线程安全性。

  这是因为存在多线程同时调用该函数的可能,此时必须保证函数的可靠性。那么如何判断函数的运行环境是否是多线程环境呢?可以根据规则2和规则3进行判断。

  规则2:如果函数A的所有的调用源只运行于同一单线程环境,则函数A也是只运行在单线程环境的。

  规则3:如果函数A的其中一个调用源是可能运行在多线程环境的,或者多个调用源可能运行于不同的可并发的多个线程环境,而且调用路径上没有采取多线程序列化

      成单线程的强制措施,则函数A也是可能运行在多线程环境的。

  规则4:如果在函数A所有的可能运行于多线程环境的调用路径上,都有多线程序列化成单线程的强制措施,则函数A是运行在单线程环境的。

  这里所谓的“多线程序列化成单线程的强制措施”是指如互斥体、自旋锁(读者在后面会见到简单自旋锁的使用)等同步手段。

  从上面的规则中可以得到一条推论,就是如果要在多线程环境下调用一个不可重入的函数,则必须在调用路径上采取多线程序列化成单线程的强制措施。

从上面的规则可以看出,是否要保证可重入性最终由调用源和调用路径决定。

  下面是内核编程中主要调用源的运行环境,如表2-6所示。

表2-6 内核代码主要调用源的运行环境
调用源
运行环境
原 因
DriverEntry,DriverUnload
单线程
这两个函数由系统进程的单一线程调用。不会出现多线程同时调用的情况
各种分发函数
多线程
没有任何文档保证分发函数是不会被多线程同时调用的。此外,分发函数不会和DriverEntry并发,但可能和DriverUnload并发
完成函数
多线程
完成函数随时可能被未知的线程调用
各种NDIS回调函数
多线程
和完成函数相同
  前面的规则结合这个表,基本上可以判断任意一段代码是否需要多线程安全性。 那么如何保证多线程安全性呢?规则如下:

  规则5:只使用函数内部资源,完全不使用全局变量、静态变量或者其他全局性资源的函数是多线程安全的。

  规则6:如果对某个全局变量或者静态变量的所有访问都被强制的同步手段限制为同一时刻只有一个线程访问,则即使使用了这些全局变量

      和静态变量,对函数的多线程安全性也是没有影响的。可以等同于内部变量,然后根据规则5判定。
 7、代码的中断级

  读者现在需要了解的中断级主要有Passive级和Dispatch级两种,Dispatch级比Passive级高。在实际编程时,许多具有比较复杂功能的内核
API都要求必须在Passive级执行,这一点在WDK的文档上有说明。比如下面的例子:

Callers of IoCreateFile must be running at IRQL?= PASSIVE_LEVEL.

  只有比较简单的函数能在Dispatch级执行。调用任何一个内核API之前,必须查看WDK文档,了解这个内核API的中断级要求。那么如何判断

我们正在编写的代码可能的中断级呢?读者暂时可以简单地根据下面的规则来处理,可以处理大部分情况;遇到特殊情况再特殊处理。

  规则1:如果在调用路径上没有特殊的情况(导致中断级的提高或者降低),则一个函数执行时的中断级和它的调用源的中断级相同。

  规则2:如果在调用路径上有获取自旋锁,则中断级随之升高;如果调用路径上有释放自旋锁,则中断级随之下降。

     和判断多线程安全性一样,读者会发现当前代码的中断级基本上取决于调用源的中断级和调用路径。表2-7列出了内核代码主要调用源

     的运行中断级。

表2-7 内核代码主要调用源的运行中断级
调用源
一般的运行中断级
DriverEntry,DriverUnload
Passive级
各种分发函数
Passive级
完成函数
Dispatch级
各种NDIS回调函数
Dispatch级
8、WDK中的特殊代码
  WDK示例代码中见到的特殊形式编码。这些代码在Win32应用程序的编程中很少见到,读者需要首先熟悉一下。


  首先是参数说明宏。参数说明宏一般都是空宏,最常见的是IN和OUT。其实定义很简单,如下所示:
#define IN
#define OUT

这样一来,IN和OUT就被定义成了空。无论出现在代码中的任何地方,对代码都不会有什么实质的影响。在WDK的代码中,用来作为
函数的说明。IN表示这个参数用于输入;OUT表示这个参数用来返回结果。比如下面的例子:

NTSTATUS
ZwQueryInformationFile(
IN HANDLE  FileHandle,
OUT PIO_STATUS_BLOCK  IoStatusBlock,
OUT PVOID  FileInformation,
IN ULONG  Length,
IN FILE_INFORMATION_CLASS  FileInformationClass
);

IN和OUT是比较传统的参数说明宏。在WDK中到处可见更复杂的参数说明宏,比如下面的例子:

VOID
NdisProtStatus(
IN NDIS_HANDLE                          ProtocolBindingContext,
IN NDIS_STATUS                          GeneralStatus,
__in_bcount(StatusBufferSize) IN PVOID  StatusBuffer,
IN UINT                                 StatusBufferSize
)

其中的__in_bcount不但说明参数StatusBuffer是一个输入参数,而且说明了StatusBuffer作为一个缓冲区,它的字节长度被另一个

参数StatusBufferSize所指定。读者再见到类似的说明宏,就以字面意思理解即可。

然后是指定函数位置的预编译指令。比如下面的例子:
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, NdisProtUnload)
#pragma alloc_text(PAGE, NdisProtOpen)
#pragma alloc_text(PAGE, NdisProtClose)

#pragma alloc_text这个宏仅仅用来指定某个函数的可执行代码在编译出来后在sys文件中的位置。内核模块编译出来之后是一个PE格式的sys文件,这个文件的代码段(text段)中有不同的节(Section),不同的节被加载到内存中之后处理情况不同。读者需要关心的主要是3种节:INIT节的特点是在初始化完毕之后就被释放。也就是说,就不再占用内存空间了。PAGE节的特点是位于可以进行分页交换的内存空间,这些空间在内存紧张时可以被交换到硬盘上以节省内存。如果未用上述的预编译指令处理,则代码默认位于PAGELK节,加载后位于不可分页交换的内存空间中。

函数DriverEntry显然只需要在初始化阶段执行一次,因此这个函数一般都用#pragma alloc_text(INIT, DriverEntry)使之位于初始化后立刻释放的空间内。

为了节约内存,可以把很多函数放在PAGE节中。但是要注意:放在PAGE节中的函数不可以在Dispatch级调用,因为这种函数的调用可能诱发缺页中断。但

是缺页中断处理不能在Dispatch级完成。为此,一般都用一个宏PAGED_CODE()进行测试。如果发现当前中断级为Dispatch级,则程序直接报异常,让程序

员及早发现。示例如下:
#pragma alloc_text(PAGE, SfAttachToMountedDevice)
……
NTSTATUS
SfAttachToMountedDevice (
IN PDEVICE_OBJECT DeviceObject,
IN PDEVICE_OBJECT SFilterDeviceObject
)
{
PSFILTER_DEVICE_EXTENSION newDevExt =
SFilterDeviceObject->DeviceExtension;
NTSTATUS status;
ULONG i;

PAGED_CODE();
…


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: