首页 > 系统相关 >Windows内核开发-[4]、内核编程基础(1)

Windows内核开发-[4]、内核编程基础(1)

时间:2024-01-27 13:55:20浏览次数:28  
标签:DriverObject 函数 Windows 编程 C++ API 内核 IRQL

在前面的文章中,介绍了如何配置开发环境以及如何进行调试。

接下来的几篇文章,将会重点介绍内核编程中所需要了解的一些理论基础。

我写这个系列文章的主要目的是方便以后自己查阅,同时也给正在学习内核开发的小伙伴一些参考,所以我会尽可能地以最简单的方式进行描述。

如果在阅读过程中遇到不理解的地方,可以查阅书籍、互联网,或私信与我取得联系。

 

与用户模式的区别

内核API由C函数组成,本质上与用户模式开发很像。不过这两者之间还是有很多不同之处,可以参考下表

  用户模式 内核模式
未处理的异常 进程崩溃 系统崩溃
终止 进程终止时,所有私有的内存和资源都自动释放 如果驱动卸载时没有释放用过的所有资源,就会造成泄蒲。只有重启才能解决。
返回值 API错误有时候会被忽略 必须(几乎)从不忽略错误
IRQL 永远是PASSIVE_LEVEL( 0) 可能是 DISPATCH_LEVEL〔2)或者更高
坏代码(Bad Code) 一般局限在进程内 会影响到整个系统
测试和调试 通常在开发者的机器上进行测试和调试 必须在另一台机器上进行调试
能使用几乎全部C/C++库〔例如STL、boost) 无法用大多数标准库
异常处理 可以使用C++异常处理和结构化异常处理(SEH) 只能用SEH
C++用法 可以使用完整的C++运行时支持 没有C++运行时支持

 

未处理的异常

用户模式下,如果出现未捕获的异常,程序会中止。

内核模式下,出现未捕获的异常,会造成系统崩溃,出现蓝屏。

蓝屏实际是一种保护机制,防止用户继续执行接下来的代码。所以在编写内核代码时,必须非常小心,而且不能跳过任何细节和错误检查 。

 

终止

用户模式下,当进程终止时,不管是正常结束 、未处理的异常,还是因为外部代码中止了它,这个进程什么都不会泄漏,所有的资源都会被释放。

内核模式下,如果当驱动被卸载时,仍有资源被占用,那么这么资源不会被自动释放,只有下一次系统重启时才会释放。

所以在进行内核编程时,清除工作是非常重要的。

 

函数返回值

用户模式下,API函数的返回值有时候会被忽略(我就经常这么干)。大部分的API函数都能正常执行,不会造成什么影响。最坏的情况下,会产生未处理的异常,从而导致进程崩溃,但系统不会受影响。

内核模式下,忽略API的返回值会很危险。所以这里的原则就是永远都去检查API函数的返回值。

 

IRQL(Interrupt ReQuest Levels ,中断请求级别)

用户模式下,线程有“优先级”的概念,系统调度器以时间片作为粒度,根据线程的优先级来调试线程,线程优先级越高,获得调度的机会越大。

与线程优先级概念类似,CPU提供了一个被称为IRQL(中断请求级别)的概念,并且规定,高IRQL的代码,可以中断(抢占)低IRQL的代码的执行过程,从而得到执行机会。

不同级别的IRQL对应不同的数值,软件驱动常见的IRQL及其数值如下所示,数值越大,表示级别越高。

 

 

注意:上表只列出了软件驱动所需要用到的IRQL,并不代表IRQL只有这三个值。

 

不同的IRQL限制不同:

PASSIVE_LEVEL(0) : 作为级别最低的IRQL,在这个IRQL中可以无限制使用系统提供的API,并且可以访问分页(Paged)内存和非分页(NonPaged)内存

说明:每个内核API对IRQL有不同的要求,在WDK API的官方文档中可以看到这个。如下图所示

 

APC_LEVEL(1) :这个中断级别可以中断PASSIVE_LEVEL的代码,主要用于APC(Asynchronous Procedure Calls,异步过程调用),在使用系统API时有一定的限制,可以访问分页内存和非分页内存。

 

DISPATCH_LEVEL(2) :存在的限制更多,只有很少一部分API函数可以在这个级别下使用,在内存访问方面,只能使用非分页内存。

 

说明:

分页内存:内存的内容可以被置换到磁盘上(也可以是其他介质),

非分页内存:内存的内容不会被系统置换到磁盘上。

 

 因为不同IRQL的限制不同,所以了解自己代码所处的IRQL就变得非常有意义。

判断代码所在的IRQL有两种方法:

1、静态方法

这种方法更多是根据微软WDK帮助文档来判断,比如说驱动的入口函数DriverEntry,系统在调用这个入口函数时,IRQL为PASSIVE_LEVEL,这个是由系统保证的,WDK对这点也有明确的说明。

 

2、动态方法

如某些回调函数在被系统调用时,IRQL可能是PAS-SIVE_LEVEL至DISPATCH_LEVEL级别范围,对于这种情况,我们可以在该回调函数中,通过调用KeGetCurrentIrql函数来获取当前的IRQL。如下面的示例代码:

 1 #include <ntddk.h>
 2 
 3 VOID DriverUnload(PDRIVER_OBJECT DriverObject)
 4 {
 5     if (DriverObject != NULL)
 6         DbgPrint("Driver Upload,Driver 0bject Address :%p,CurrentIRQL = 0x%u\n", DriverObject, KeGetCurrentIrql());
 7     return;
 8 }
 9 
10 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
11 {
12     DbgPrint("Hello Kernel world,CurrentIRQL = 0x%u\n", KeGetCurrentIrql());
13 
14     if (RegistryPath != NULL)
15     {
16 
17     }
18 
19     if (DriverObject != NULL)
20     {
21         DriverObject->DriverUnload = DriverUnload;
22     }
23 
24     return STATUS_SUCCESS;
25 }

 

运行输出如下:

 

提升和降低IRQL

在用户模式下,IRQL这个概念从不被提及,也没有办法改变它。

在内核模式下,IRQL能用KeRaiseIrql函数提升并用KeLowerIrql函数降回来。

下面的示例代码将IRQL提升到DISPATCH_LEVEL(2),在此IRQL上执行一些操作,然后降回到原来的IRQL。

 1 #include<ntddk.h>
 2 
 3 VOID DriverUnload(PDRIVER_OBJECT DriverObject)
 4 {
 5     if (DriverObject != NULL)
 6     {
 7 
 8     }
 9 
10     return;
11 }
12 
13 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
14 {
15     if (DriverObject != nullptr)
16     {
17         DriverObject->DriverUnload = DriverUnload;
18     }
19 
20     if (RegistryPath != NULL)
21     {
22 
23     }
24 
25     KIRQL oldIrql;
26 
27     DbgPrint("[%ws]Before Raise IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
28 
29     KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
30 
31     DbgPrint("[%ws]After Raise IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
32 
33     KeLowerIrql(oldIrql);
34 
35     DbgPrint("[%ws]After Lower IRQL:CurrentIRQL = 0x%u", __FUNCTIONW__, KeGetCurrentIrql());
36 
37     return STATUS_SUCCESS;
38 }

 

运行结果如下:

注意:

如果提升了IRQL,请确保在同一个函数里将它降低。如果函数返回时的IRQL比进入时要高,那么这种情况是很危险的。

另外 ,要确保KeRaiseIrql确实提升了IRQL,KeLowerIrql确实降低了IRQL,否则,系统随便就会崩溃。

 

C++用法

C++几乎全部内容都能用在内核代码里,但是在内核模式下,没有C++运行时,因此一些C++特性没办法使用

1、不支持new和delete操作符

使用它们会导致编译失败。这是由于它们的正常操作是从用户模式堆分配内存,而在内核模式里这显然毫无意义。内核API里有接近于malloc和free这些C函数的“替代”函数,在后面的文章中将会详细介绍内核模式下的内存分配和释放。然而,用类似于用户模式的C++的方式重载这些操作符,并调用内核的分配和释放函数这是可以的。在后面的文章中也会提及到。

2、非默认构造函数中的全局变量将不会被调用

因为没有C++运行时,所以构造函数不会被调用。这些情况可以通过以下方式避免:

  1. 避免把代码放到构造函数中,而是创建一些Init函数,并显式地从驱动 程序代码(如DriverEntry)中调用
  2. 仅仅把类指针定义成全局变量,然后动态分配其实例,编译器会生成正确的代码调用构造函数。但是调用的前提是已经重载了new和delete操作符,如前面描述的那样。

3、 不支持异常处理的关键字(try、catch、throw)

C++的异常处理机制需要它自己的运行时,而在内核中没有这个运行时。异常处理只能通过结构化异常处理(SEH,内核的异常处理机制)来完成。

4、 不能使用标准C++库

虽然标准库里的大部分内容是基于模板的,但它依赖用户模式及其语义,所以无法使用。

但是C++模板作为语言特性,在内核 里是可以使用的。

 

内核API

内核驱动程序使用的是从内核的组件输出的函数,这些函数被称为内核API。大多数函数则在内核本身模块(NtOskrnl.exe)里实现。

但还有些是在别的模块中实现,比如在HAL.dll中。

 

 NtOskrnl.exe中的内核API(部分截图)

 

内核API是大量C函数的集合,其中多数的名称前有一个前缀,这个前缀指示了实现该函数的模块

 

这里有一组函数值得讨论一下,以Zw开头的函数(ZwCreateFile、ZwReadFile等)。这组函数作为NTDLL.dll中的原生API的镜像,是从原生API到位于执行体中的实现之间的网关。

当用户模式调用了Nt函数,比如NtCreateFile(ZwCreateFile)时,最终将到达执行体中实际的NtCreateFile实现。此时,基于原始调用来自用户模式这个事实,NtCreateFile可能会做各种合法性检查。这里调用者的信息以线程为基础保存在每个线程对应的KTHREAD结构中未公开的PreviousMode字段里。

When an Nt function is called from user mode, such as NtCreateFile, it reaches the
Executive at the actual NtCreateFile implementation. At this point, NtCreateFile might do various
checks based on the fact that the original caller is from user mode. This caller information is stored
on a thread-by-thread basis, in the undocumented PreviousMode member in the KTHREAD structure
for each thread.

另一方面,如果内核驱动程序要调用某个系统服务,它就没必要做跟用户模式一样的检查以及接受用户模式调用者所受的限制。这就是为什么要有Zw系列函数。调用Zw函数会将PreviousMode设置成KernelMode ( o ),然后调用原生函数。举个例子,调用ZwCreateFile会将前一个调用者的模式设置为KernelMode,然后调用NtCreateFile,这使得NtCreateFile绕过一些安全性和缓冲区的检查。底线是,驱动程序必须调用Zw系列函数。

On the other hand, if a kernel driver needs to call a system service, it should not be subjected to the
same checks and constraints imposed on user-mode callers. This is where the Zw functions come into
play. Calling a Zw function sets the previous caller mode to KernelMode (0) and then invokes the
native function. For example, calling ZwCreateFile sets the previous caller to KernelMode and then
calls NtCreateFile, causing NtCreateFile to bypass some security and buffer checks that would
otherwise be performed. The bottom line is that kernel drivers should call the Zw functions unless
there is a compelling reason to do otherwise.

说明:这里我贴出《Windows Kernel Programming 2nd》英文原版,因为中文翻译得不是非常通俗易懂

 

函数和错误代码

多数内核API函数会返回一个状态,用来指示操作成功或者失败。这个状态被定义为NTSTATUS

1 typedef _Return_type_success_(return >= 0) LONG NTSTATUS;

在文件ntstatus.h中可以找到所有定义的NTSTATUS值

ntstatus.h

 1 #define STATUS_SUCCESS                   ((NTSTATUS)0x00000000L)    // ntsubauth
 2 
 3 //
 4 // MessageId: STATUS_WAIT_1
 5 //
 6 // MessageText:
 7 //
 8 //  STATUS_WAIT_1
 9 //
10 #define STATUS_WAIT_1                    ((NTSTATUS)0x00000001L)
11 
12 //
13 // MessageId: STATUS_WAIT_2
14 //
15 // MessageText:
16 //
17 //  STATUS_WAIT_2
18 //
19 #define STATUS_WAIT_2                    ((NTSTATUS)0x00000002L)
...
...
...

大部分的代码并不关心确切的错误值,只要测试最高位就行,可以使用NT_SUCCESS宏来完成,就像使用SUCCEED(HRESULT)宏一样。

在某些情况下,从函数返回的NTSTATUS值最终会返回到用户模式。这时候STATUS_XXX值会被转换成ERROR_XXX值,在用户模式中可以通过GetLastError函数得到这些值。

 

驱动程序对象

在前面的示例代码中,我们可以看到DriverEntry函数会接收一个DRIVER_OBJECT的参数

1 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)

这个结构由内核分配,并且进行了部分初始化,然后传递给DriverEntry(在驱动程序卸载之前,还会传递给Unload)。此时驱动程序需要进一步对这个结构进行初始化,从而指明该驱动能支持哪些操作。

在前面的代码中,我们见过了这些操作之一 ---- Unload函数,如下所示:

1 if (DriverObject != NULL)
2 {
3     DbgPrint("Driver Object Address: %p\n", DriverObject);
4     DriverObject->DriverUnload = DriverUnload;
5 }

另外那些需要初始化的重要操作集合被称为分发例程。它是一个函数指针数组,位于DRIVER_OBJECT的MajorFunction字段。这个集合指明驱动程序支持哪些操作。如创建、读取、写入等。数组的索引被定义成带有IRP_MJ_前缀的常量。部分定义如下所示:

说明:详细的介绍可以参考以下链接:

https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/handling-irps

 

起初MajorFunction数组会被内核初始化成指向内核的内部例程IopInvalidDeviceRequest,它给调用者返回一个错误的状态,以表明不支持所请求的操作。

举一个例子,前面的示例代码中,到现在为止还不支持任何分发例程,因此现在无法与驱动程序通信。

驱动程序必须至少支持IRP_MJ_CREATEIRP_MJ_CLOSE操作,才能打开该驱动程序设备对象的一个句柄。

 

标签:DriverObject,函数,Windows,编程,C++,API,内核,IRQL
From: https://www.cnblogs.com/zhaotianff/p/17952761

相关文章

  • C# 面向对象编程进阶:构造函数详解与访问修饰符应用
    C#构造函数构造函数是一种特殊的方法,用于初始化对象。构造函数的优势在于,在创建类的对象时调用它。它可以用于为字段设置初始值:示例获取您自己的C#服务器创建一个构造函数://创建一个Car类classCar{publicstringmodel;//创建一个字段//为Car类创建一个......
  • 在 Python 的 `glob` 模块中,文件名的大小写敏感性取决于你的操作系统。在 Unix 和 Lin
    在Python的`glob`模块中,文件名的大小写敏感性取决于你的操作系统。在Unix和Linux系统中,`glob`是区分大小写的。然而,在Windows和MacOS中,`glob`是不区分大小写的。例如,如果你在Unix或Linux系统中运行以下代码:```pythonimportglobfiles=glob.glob('*.xlsx')......
  • windows下java启动失败
    windows下java启动失败一、问题现象微服务原来在linux下部署,现在有客户环境为windowsserver,把linux的包拷到windowsserver下,使用java-jar命令无法启动。同样的包在linux可以启动,就是在windows启动不了。问题提示:现象一:2023-10-2713:40:05.682ERROR11736---[main]o.......
  • C# 面向对象编程进阶:构造函数详解与访问修饰符应用
    C#构造函数构造函数是一种特殊的方法,用于初始化对象。构造函数的优势在于,在创建类的对象时调用它。它可以用于为字段设置初始值:示例获取您自己的C#服务器创建一个构造函数://创建一个Car类classCar{publicstringmodel;//创建一个字段//为Car类创建一......
  • Windows内核开发-[2]、创建第一个驱动程序
    使用VisualStudio2022创建一个EmptyWDMDriver工程  工程创建后,添加一个MyFirstDriver.cpp文件,输入以下内容1#include<ntddk.h>23VOIDDriverUnload(PDRIVER_OBJECTDriverObject)4{5if(DriverObject!=NULL)6{7DbgPrint("Driver......
  • Windows内核开发-[3]、驱动调试方法
    单步调试驱动驱动的调试不能直接在本机上进行,而是要放在虚拟机(或其它设备)中。这是因为在内核模式下,一个断点的触发将会停下整个系统而不只是单个进程。在前面的文章里,使用了DbgPrint函数来进行日志的输出,但这种方法不能进行单步调试。下面介绍两种调试方法。 基于VisualStud......
  • ZSH!在 Windows 上使用 WSL+ZSH
    ZSH!在Windows上使用WSL+ZSH1.安装WSL关于如何安装WSL这里就不介绍了,大家可以去找找相关的教程,很多。最直接的就是去微软官方:https://learn.microsoft.com/en-us/windows/wsl/install最简单的方法是从MicrosoftStore安装Ubuntu2.ubuntu在开始菜单中搜索Ubuntu图标并打开终端......
  • .NET Core 6.0 Windows部署
    varoptions=newWebApplicationOptions{Args=args,//这是因为从Windows中调用GetCurrentDirectory会返回:C:\WINDOWS\system32//需要注意使用了WindowsService部署,就不能使用Console类,否则会报错ContentRootPath=WindowsServiceHelpers.IsWindowsServi......
  • 记windows自定义bat脚本自启动
    自定义Windows启动脚本简化版在本指南中,我们将使用一个简化的批处理文件(.bat)来演示如何创建自定义的Windows启动脚本。以下是一个基本的模板,您只需根据需要在:begin部分添加您的代码。 @echooffif"%1"=="h"gotobeginrem获取脚本路径set"scriptPath=%~dp0"......
  • windows使用VMware安装macOS
    1.准备工作笔记本型号:dellG33579i5-8300H款VMware:15.5(VMwareWorkstation的安装流程省略...)macOS:10.15.1Catalina(来自:https://www.bilibili.com/video/BV1zK4y1b7hU/?spm_id_from=333.999.0.0&vd_source=619d0f384650adc67c5cff8a3767b490)unlocker427(来自github大佬......