您的位置:首页 > 理论基础 > 数据结构算法

Windows内核中的数据结构与函数调用

2016-05-19 15:09 239 查看
2.3重要的数据结构
2.3.1驱动对象:
Windows内核认为许多东西都是“对象”,比如一个驱动、一个设备、一个文件,甚至其他的一些东西。(采用面对对象的编程方式,但是使用的是C语言)
一个驱动对象代表了一个驱动程序,或者说一个内核模块。
驱动对象的结构如下:
typdef 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直接从main()函数开始执行来生成一个进程。内核模块并不生成一个进程,而是填写一组回调函数让Windows来调用,而且这组回调函数必须符合Windows内核规定。
⑶这组回调函数包括上面的“普通分发函数”和“快速IO分发函数”。这些函数用来处理发送给这个内核模块的请求。一个内核模块所有的功能都由它们提供给Windows。
⑷如果编写内核程序,能找到这些关键的驱动对象结构(比如NTFS文件系统),然后改写下面的分发函数,替换成我们自己的函数,可能就可以捕获Windows的文件操作,让我们的内核程序处理完毕后,再交给NTFS文件系统处理。这样就可以加入我们自己的功能(比如扫描病毒,文件加密等)。这就是所谓的分发函数Hook技术,本书后面会有所描述。

2.3.2设备对象:
important:
在内核世界里,大部分“消息”都以请求(IRP)的方式传递。而设备对象(DEVICE_OBJECT)是唯一可以接受请求的实体,任何一个“请求”(IRP)都是发送给某个设备对象的。
设备对象的结构是DEVICE_OBJECT,简称DO。
因为我们总是在内核程序中生成一个DO,而一个内核程序是用一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象。
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;

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

2.3.3请求:
⑴何为请求?举一个浅显的例子:如果要求网卡发送一个数据包,或者向网卡请求把已经存在缓冲区里接收到的包读出来,这就是一个请求;如果读取一个文件从0开始的512个字节,这也是一个请求;如果在磁盘的64MB位置写入长达512字节的一组数据,这还是一个请求。
⑵应用程序的开发者是看不到这些请求的,只需调用API即可。但是这些操作最终在内核中会被IO管理器翻译成请求(IRP或者与之等效的其他形式,比如快速IO调用)发送往某个设备对象。
⑶大部分请求以IRP的形式发送。IRP也是一个内核数据结构,非常复杂,因为这个结构要表示无数种实际请求的缘故。在WDK的wdm.h中能找到IRP的结构如下:

typedef struct _IRP {
  // 类型和大小
  CSHORT Type;
  USHORT Size;
  // 内存描述符链表指针。实际上,这里用来描述一个缓冲区。可以想象
  // 一个内核请求一般都需要一个缓冲区(如读硬盘需要有读出缓冲区)
  PMDL  MdlAddress;
  ...
  // 下面这个共用体中也有一个SystemBuffer。这是比MdlAddress稍微简单
  // 的表示缓冲区的一种方式。IRP用MdlAddress还是用SystemBuffer取决于
  // 这次请求的IO方式。总之二者都有可能。
  union {
    struct _IRP  *MasterIrp;
    __volatile LONG IrpCount;
    PVOID  SystemBuffer;

  } AssociatedIrp;
  .
  // IO状态,一般请求完成之后的返回情况放在这里
  IO_STATUS_BLOCK  IoStatus;
  // IRP栈空间大小
  CHAR StackCount;
  // IRP当前栈空间
  CHAR CurrentLocation;
  ...
  // 用来取消一个未决请求的函数
  PDRIVER_CANCEL  CancelRoutine;
  // 与MdlAddress和SystemBuffer一样都可以表示缓冲区,
  // 但是缓冲区的特性稍有不同。以后再详细解释
  PVOID UserBuffer;
  union {

    struct {

    .

    .

    union {

      KDEVICE_QUEUE_ENTRY DeviceQueueEntry;

      struct {

        PVOID  DriverContext[4];

      };

    };
    .
    // 发出这个请求的线程
    PETHREAD  Thread;

    .

    .

    LIST_ENTRY  ListEntry;

    .

    .

    } Overlay;

  .

  .

  } Tail;
} IRP, *PIRP;

⑷这里值得注意的是IRP栈空间。因为一个IRP往往需要传递n个设备才得以完成(第三章有描述)。可以想象,在传递过程中,有可能又些“中间变换”,导致请求的参数变化。为了保存这种参数变化,我们给每次“中转”都留一个“栈空间”,用来保存中间参数。所以一个请求并非简单的一个输入,并等待一个输出,而是经过许多中转才得以完成。而且在中转的每个步骤,输入都可以改变,所以可变部分的输入信息保存在一个栈似的结构中。每中转一次,都使用其中一个位置。域CurrentLocation表示当前使用了哪一个。
⑸本书后面内容所使用的词语“请求”如不特殊说明,就是指IRP。衍生说法如下:
生成请求:主功能号为 IRP_MJ_CREATE 的 IRP
查询请求:主功能号为 IRP_MJ_QUERY_INFORMATION 的 IRP
设置请求:主功能号为 IRP_MJ_SET_INFORMATION 的 IRP
控制请求:主功能号为 IRP_MJ_DEVICE_CONTROL,或者是仅仅在本书的第10章TDI过滤中出现的 IRP_MJ_INTERAL_DEVICE_CONTROL 的 IRP
关闭请求:主功能号为 IRP_MJ_CLOSE 的 IRP
请求指针:IRP 的指针。类型写作 PIRP 或者 IRP*。

2.4函数调用
2.4.1查阅帮助:
查阅WDK Documentation

2.4.2帮助中有的几类函数:
⑴大部分内核API都有前缀,主要的函数以Io-,Ex-,Rtl-,Ke-,Zw-,Nt-和Ps-开头。此外,与NDIS网络驱动开发的相关函数几乎都是以Ndis-开头的,与开发WDF驱动相关的函数都是以Wdf-开头的。
⑵部分函数相关介绍:看课本P30
有分配内存、获取互斥体
文件操作、注册表操作
字符串操作、获取当前Windows版本
IO管理器操作、IRP操作
进程线程操作

2.4.3帮助中没有的函数:
⑴并不是所有可以调用的函数都在帮助里。比如C运行时库中的 stdlib.h  stdio.h 和 memory.h 三个头文件里有很多函数可以使用。也并非全部,比如printf,scanf,fopen,fclose,fwrite,fread就不行,因为内核里没有控制台,而且读/写文件也不是那么轻松。但是,如spirntf,strlen,strcpy,wcslen,wcscpy,memcpy,memset都是可以的,相应的malloc,free,strdup是不行的。
⑵基本上可以认为,C运行时库中的函数,如果只涉及字符串和内存数据(不涉及内存管理,比如内存的分配和释放),则是可以在内核程序里调用的。但是MS不提倡这样做。
⑶如果函数涉及内存管理,文件操作,网络操作,线程等。则往往头文件中有这个函数存在,甚至编译可以通过,但是连接的时候会出问题。

2.5 Windows的驱动开发模型
⑴“模型”源于单词“Mode”
⑵在Windows NT上,驱动程序被称为 Kernel Driver Mode 驱动程序。
⑶在Windows 9x上的驱动程序,都叫做VXD。
⑷Windows 98~2000这个时期出现的新模型叫做WDM。
⑸Windows的驱动模型概念,本来是就驱动程序的行为而言的。比如WDM驱动,必须要满足n种被要求的特性(如电源管理、即插即用)才被称为WDM驱动。如果不提供这些功能,那么统一称为NT式驱动。
⑹本书采用简单的区分方法。将一切在Windows 2000 ~ Windows Vista下能正常运动且未调用WDF相关的内核API函数的驱动都称为传统型驱动(包括NT式和WDM)。如果调用了WDF相关的内核API则称为WDF驱动。
WDF驱动是可以调用传统型驱动所调用的内核API的,WDF可以视为传统型的升级版。
WDF与其说是新的驱动开发模型,还不如说是在已有的内核API和数据结构的基础上,又封装出一套让使用者觉得更简单、更易用的 Wdf- 开头的一组API。从KDM到WDM再到WDF是一脉相承的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: