首页 > 系统相关 >Windows内核编程基础(1)

Windows内核编程基础(1)

时间:2024-09-24 15:51:57浏览次数:9  
标签: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_前缀的常量。部分定义如下所示:

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

处理 IRP - Windows drivers | Microsoft Learn

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

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

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

标签:DriverObject,函数,Windows,编程,C++,API,内核,IRQL
From: https://blog.csdn.net/zhaotianff/article/details/142492275

相关文章

  • 腾讯通升级迁移解决方案:兼容linux内核国产系统及移动端
    一、继续使用RTX腾讯通面临的核心痛点自RTX腾讯通停止更新和官网下架以来,用户不仅无法再获取技术支持和更新服务,还面临一系列影响日常使用的重大问题:国产系统及移动端不兼容:RTX腾讯通仅适用于Windows和Mac系统,这使得在国产操作系统和移动设备上的使用成为难题,无法满足信创政策要......
  • Windows 11设置柯能卡打印机通过SMB扫描到电脑
    1、查看打印机扫描仪的网络IP。2、在“网络和Internet访问”中打开网络发现,并通过”计算机管理“将Guest帐户的禁用选项取消。3、设置共享目录的来宾访问权限已开启。4、在添加windows功能中增加SMB服务,并开启之。5、建立共享扫描文件夹并设置其具有Guest及Everyone用户有可读......
  • VMWare安装Ubuntu之后与Windows系统共享文件夹的设置步骤
    1.首先在Windows系统中新建一个需要共享的文件夹,并设置文件夹的共享属性,如下图: 2.VMWare软件开启【共享文件夹】功能,如图所示3.进入Ubuntu系统,查看是否存在/mnt/hgfs目录,若是没有,先要以root权限建立该目录sudomkdir/mnt/hgfs4.挂载目录sudovmhgfs-fuse.host:......
  • WIP在编程中的意思是什么?
    在编程和软件开发中,WIP是"WorkInProgress"的缩写,表示某个任务、功能或项目正在进行中,尚未完成。WIP通常用于以下几个场景:代码注释:在代码中,开发者可能会使用WIP作为注释,表示某部分代码还在开发中,尚未完成或需要进一步完善。#WIP:这部分代码还需要进一步测试和优化de......
  • 【Java】并发编程的艺术:悲观锁、乐观锁与死锁管理
    目录一、乐观锁和悲观锁二、ReadWriteLock三、StampedLock四、Semaphore五、死锁的条件六、如何发现死锁七、如何避免死锁一、乐观锁和悲观锁        悲观锁(PessimisticLocking)具有强烈的独占和排他特性。它指的是对数据被外界修改持保守态度。因此,在整......
  • Python函数艺术:掌握编程中的“乐高积木”
    引言函数是程序设计的基本单元之一,它使得代码模块化,提高了重用性和可读性。无论是处理数据、操作文件还是实现特定业务逻辑,掌握好函数的设计与使用都是至关重要的技能。在Python中,定义一个函数非常直观且强大,这使得即使是初学者也能快速上手,并随着经验积累不断发掘其深层价......
  • 故障排查之利器:Windows系统的日志功能与管理
    文章目录前言一、系统日志的基本概念二、系统日志的类型与详细分析(一)事件日志的详细结构(二)事件日志类型(三)事件日志类型三、如何查看和分析系统日志(一)事件查看器(二)可靠性监视器(三)筛选与过滤(四)自定义视图(五)常见事件ID(六)自动化工具与策略四、系统日志的管理与维护(一)设置日......
  • OpenHarmony Linux内核的config配置
    鸿蒙系统对Linux内核的使用方式对于传统的Linux内核和驱动开发者来说已经发生了很大的变化,首先就是内核config选项的配置方式。传统上,我们直接进入到linux内核目录进行makemenuconfig就可以了,最终会生成1个.config文件,但是鸿蒙不是这样子的。为了解决传统的一平台或一领域产品......
  • C++和OpenGL实现3D游戏编程【连载11】——光照效果进阶
    1、本节要实现的内容我们在前面的章节里内容简单的介绍了一下光照,随着后期对纹理内容的增加,我们需要了解更多的光照知识,本节我们回顾一下光照相关内容,并了解一下怎样实现纹理的光照效果。下面这个图就是我们借助于纹理文字产生的半透明光照效果。半透明纹理文字光照演......
  • 写文档-画UML图-编程的秘密武器:Kimi智能助手
    在快速发展的软件开发领域,如何高效地编写需求分析文档、软件设计文档以及代码,成为每位程序员和架构师面临的重要挑战。今天,我要向大家介绍一款强大的工具——Kimi智能助手,它将帮助你提升工作效率,优化开发流程。Kimi的强大功能需求分析文档编写Kimi能够快速梳理项目背景、目......