0%

Chapter 3 Kernel Programming Basics

img

未处理的异常

用户模式情况下会崩溃该进程,内核模式下会崩溃系统,导致蓝屏(实质上是一种保护机制,如果允许代码继续执行可能会造成一些不可逆转的损坏)。

终止

进程会自动释放内存和资源,包括关闭句柄,没有资源泄露。而对于内核驱动来说,如果没有释放资源,这些资源不会被自动释放,只有在下一次系统启动时才能被释放。

函数返回值

要注意处理内核模式下的函数返回值,因为一些简单的API函数也有可能失败,失败造成的结果相比用户模式比较严重。

IRQL

中断请求级别,当一个用户模式代码正在执行时,总是为0;内核模式下大部分时间为0.

C++使用

C++作为一种语言几乎完全支持内核代码,C++的一个术语RAII(资源获取就是初始化),是C++常用的管理资源、避免内存泄漏的方法,保证在任何情况下,使用对象是先构造对象,然后析构对象。但是在内核钟没有C++运行,所以一些C++特性不能被使用:

  • new和delete不被支持并且不会被编译,这是因为它们的正常操作是从用户模式堆中分配,这和内核无关。但是内核API会有替换函数,但是模仿的函数更类似malloc和free,

  • 将不会调用具有非默认构造函数的全局变量,当然这些情况可以通过这些方式避免避免使用构造函数中的任何代码,而是创建一些要从驱动程序代码中显式调用的Init函数只将指针分配为全局变量,并动态地创建实际实例。编译器将生成正确的代码来调用构造函数。

  • C++异常处理关键字(try,catch,throw)不进行编译。这是因为C++异常处理机制需要它自己的运行时,异常处理只能使用结构化异常处理(SEH)来完成——这是一种处理异常的内核机制

  • 在内核中无法使用标准的C++库。

严格地说,驱动程序可以用纯C语编写,没有任何问题。如果您更喜欢使用该路由,请使用具有C扩展名的文件,而不是CPP,这将自动调用C编译器。

2.Debug与Release

​ Debug默认不适用优化但是更易于调试。Release利用编译器优化来尽可能快地生成代码,在内核中是checked\Free.从编译角度看,内核调试定义符号DBG,并将其设置为1(用户模式定_DEBUG),这实际上是KdPrint宏所做的:在debug构建中,它编译为调用DbgPrint,而在Realse中,它什么都不编译,所以vKdPrint调用在Realse中没有影响。

3.内核API

​ 内核驱动程序使用从内核组件导出的函数,大多数都是在内核模块本身(NtOskrnl.exe)中实现的,但有些功能可以由其他内核模块实现,比如HAL(hal.dll)。

​ 大多数函数都以实现该功能地组件前缀开头,比如Nt,Ex。

​ 如果内核驱动程序需要调用系统服务,则它不应该受到对用户模式调用者施加的相同的检查和约束。这就是Zw函数发挥作用的地方。调用Zw函数会将前面的调用者模式设置为KernelMode(0),然后调用本机函数。

img

​ 调用Zw函数将前一个调用者模式设置为KernelMode(0),然后调用本机函数。例如,调用ZwCreateFile将上一个调用方设置为kernelMode,然后调用NtCreateFile,从而使NtCreateFile绕过一些安全性和缓冲区检查,否则将执行这些检查。

内核驱动都推荐用Zw,可以避免不必要的检查。

img

4.函数和错误代码

​ 大多数内核API函数返回一个指示操作成功或失败的状态。这是一个类型NTSTATUS,一个有符号的32位整数。值STATUS_SUCCESS(0)表示成功,负值表示存在某种错误。

​ 大多数代码路径都不关心错误的确切性质,因此测试最重要的位就足够了。这可以通过NT_SUCCESS宏来完成。

img

​ 在某些情况下,NTSTATUS将从函数返回并最终到用户模式。在这些情况下,STATUS_xxx值将通过GetLastError函数转换为用户模式可用的某些ERROR_yyy值。请注意,这些都不是相同的数字。首先,用户模式下的错误代码具有正值,其次,该映射并不是一对一的。

5.字符串

​ _UNICODE_STRING,操作该结构通常用Rtl开头的函数。

img

6.动态分配内存

​ 驱动通常都需要动态分配内存,驱动一般提供两种内存池,

​ 分页池—如果需要,可以调出的内存池。

​ 非分页池—从不分页并保证保留在RAM中的内存池(慎用)

​ 某些函数中的tag参数允许用4字节值标记分配。通常,此值最多由4个ASCII字符组成,这些字符在逻辑上标识驱动程序或驱动程序的某些部分。这些标记可用于指示内存泄漏—如果在卸载驱动程序后仍保留任何带有驱动程序标记的分配。

7.链表

​ 内核在很多内部数据结构中使用循环双链表。系统上的所有进程都由EPROCESS结构管理,连接在一个循环的双链表中,双链表的头存储在内核变量PsActiveProcessHead中。

image-20220425160841942

​ CONTAINING_RECORD是一个宏,根据一个结构体变量成员的地址来获取该结构体变量的地址,成员变量的地址-成员变量和结构体首地址间的偏移量,就是结构体的首地址了。

1
CONTAINING_RECORD (entry, PDO_DEVICE_DATA, Link);

​ entry是一个PDO_DEVICE_DATA结构中Link成员的地址,表达式得到的是其所在结构体变量的地址。
​ LIST_ENTRY是一个链表节点成员,一些结构中会定义一个此类型的成员,以便把很多结构体变量连成一个链表。在自定义结构中,最好把链表节点成员定义在最前,这样该成员的地址就是结构体的地址,不需要这样转换。

8.驱动对象

​ Driver在启动时对Driver_Object初始化,MajorFunction指定驱动支持哪些操作比如读写创建等等,通常这些都需要IRP_MJ前缀以前的MajorFunction队列由内核初始化,一个驱动至少要有Creat和Close操作。

img

9.设备对象

​ 客户端与驱动程序对话的实际通信端点是设备对象,要与Driver_Object通信,必须创建一个DeviceObject与之连接,一个驱动对象可以有多个设备对象,以链表方式存在。

​ CreateFile函数第一个参数是文件名,其实应该是驱动对象名。CreateFile的file其实意味着文件对象。打开一个文件或者设备句柄会创建内核结构FILE_OBJECT的实例。更准确地说,CreateFile接受一个符号链接(一个知道如何指向另外一个内核对象得内核对象。类似文件的快捷方式)。

​ 大部分的符号链接都在这个Global??目录里,指向设备目录下的内部设备名称。用户模式调用者不能直接访问此目录中的名称。但是内核调用者可以使用IoGetDevice对象指针API访问它们。

​ 驱动程序使用IoCreateDevice函数创建设备对象。此函数分配并初始化设备对象结构,并将其指针返回给调用者。设备对象实例存储在DRIVER_OBJECT结构的DeviceObject成员中。如果创建了多个设备对象,它们将形成一个单独的链接列表,其中DEVICE_OBJECT的成员NextDevice指向下一个设备对象。注意,设备对象插入到列表的顶部,因此创建的第一个设备对象最后存储;其NextDevice指向NULL。

img