Windows内核编程基础(1)

news2024/9/25 13:04:44

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

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

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

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

与用户模式的区别

内核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操作,才能打开该驱动程序设备对象的一个句柄。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2163612.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在线PDF转图片怎么转?4种简单转换的方法分享

在线PDF转图片怎么转&#xff1f;在线PDF转图片不仅简化了文档处理流程&#xff0c;还极大地提升了工作效率。无论是教师准备教学材料、学生整理笔记&#xff0c;还是职场人士分享报告&#xff0c;都能通过这一功能轻松实现PDF到图片的转换&#xff0c;确保内容的高清展示与便捷…

OLED(3)字库篇

文章目录 1 显示图片1.1 图片取模 2 字符集与编码2.1 字符编码2.2 ASCII2.3 中文编码2.3.1 GB2312 标准2.3.2 GBK 编码2.3.3 GB18030 2.4 Unicode 字符集和编码2.4.1 UTF-322.4.2 UTF-162.4.3 UTF-8 3 字库 DIY3.1 生成字库3.2 烧录到 W25Qxx 1 显示图片 1.1 图片取模 1&#…

【完整梳理验证】企业微信第三方应用接入全流程java版

企业微信第三方应用接入全流程java版 1. 概念与流程1.1 概念1、企业内部应用2、`第三方应用`3、代开发自建应用1.2 流程1.2.1 全局流程1.2.2 应用配置1.2.3 数据流程2. 核心文档2.1 理解第三方应用开发流程和概念2.1.1 应用开发阶段2.1.2 应用推广阶段2.1.3 基本流程1)前期应用…

【VUE_ruoyi-vue】基于ruoyi-vue框架实现简单的系统通用文件模块

基于ruoyi-vue框架&#xff0c;新增一个简单的系统通用文件模块&#xff0c;服务与各个模块涉及到文件上传信息的记录和相关展示 运行sql,创建数据库表 DROP TABLE IF EXISTS sys_file_info; CREATE TABLE sys_file_info (id int(11) NOT NULL AUTO_INCREMENT COMMENT id,lin…

怎样才能远程了解在iPhone、iPad上看了什么网站、用了什么APP?

有不少家长在网上吐槽&#xff1a; ——自家小孩每天抱着手机看&#xff0c;一看就两三个小时&#xff0c;到底在看什么&#xff1f; ——没有不允许小孩玩手机&#xff0c;但他一玩就一整天&#xff0c;用什么户外活动、家庭活动都吸引不回来。 ——每次问小孩在手机上看什…

【C++掌中宝】用最少的话让你全方位理解内联函数

文章目录 引言1. 什么是内联函数2. 工作原理3. 内联函数的编程风格4. 使用限制5. 内联函数与宏的比较6. 优缺点7. 何时使用内联函数8. 补充9. 总结结语 引言 在C编程中&#xff0c;函数的调用开销是程序运行效率的一个重要影响因素。为了解决频繁调用函数时的性能问题&#xf…

8080时序

通过RS来区分是命令还是数据 在WR高电平时&#xff0c;将数据放入D[0:15]数据线上 在WR上升沿&#xff0c;读取D[0:15]数据线上的数据 //提前把默认信号设置为对应电平 static inline void LcdSendCmd(uint16_t cmdVal) {LCD_CS_RESET();//cs输出低电平&#xff0c;表示片选…

【Diffusion分割】MedSegDiff-v2:Diffusion模型进行医学图像分割

MedSegDiff-V2: Diffusion-Based Medical Image Segmentation with Transformer 摘要&#xff1a; 最近的研究揭示了 DPM 在医学图像分析领域的实用性&#xff0c;医学图像分割模型在各种任务中表现出的出色性能就证明了这一点。尽管这些模型最初是以 UNet 架构为基础的&…

低代码BPA(业务流程自动化)技术探讨

一、BPA流程设计平台的特点 可视化设计工具 大多数BPA流程设计平台提供直观的拖拽式界面&#xff0c;用户可以通过图形化方式设计、修改及优化业务流程。这种可视化的方式不仅降低了门槛&#xff0c;还便于非技术人员理解和参与流程设计。集成能力 现代BPA平台通常具备与其他系…

My_String完善

#include "my_string_ok.h" My_string_Ok::My_string_Ok():size(20) { len 0; ptr new char[size]; ptr[len] \0; } My_string_Ok::My_string_Ok(int num,char c) { cout<<"有参构造"<<endl; ptr new char [20] ; len 0; for…

K8s安装部署(v1.28)--超详细(cri-docker作为运行时)

1、准备环境 ip角色系统主机名cpumem192.168.40.129mastercentos7.9k8smaster48192.168.40.130node1centos7.9k8snode148192.168.40.131node2centos7.9k8snode248192.168.40.132node3centos7.9k8snode348 2、系统配置&#xff08;所有节点&#xff09; 重要&#xff1a;首先…

怎么更换自己的ip地址?多种方法可实现

在当今的数字化时代&#xff0c;IP地址作为我们在网络世界中的“身份证”&#xff0c;扮演着举足轻重的角色。然而&#xff0c;有时候出于隐私保护、网络安全或是访问特定服务等需求&#xff0c;我们可能需要更换自己的IP地址。那么&#xff0c;如何实现这一目标呢&#xff1f;…

2024年10月2-4日(星期三-星期五)骑行(石林-老圭山)

2024年10月2-4日 (星期六&#xff09;骑行&#xff08;石林-大草坪---长坪---石头寨---海邑镇---老圭山&#xff09;&#xff0c;早8:30到9:00&#xff0c;新螺蛳湾客运站正门&#xff0c;地铁二号线南部客运站站出口集合&#xff0c;9:30装车&#xff0c;10:00出发。偶遇地点:…

网络安全前景大好,转行这些职位成了“香饽饽”

网络安全就业前景 大数据、人工智能、云计算、物联网、5G等新兴技术的高速发展&#xff0c;蒸蒸日上。但是随之也出现了许多问题&#xff0c;比如&#xff1a;政府单位、企业、个人信息泄露&#xff0c;网络安全问题日益严峻&#xff0c;网络空间安全建设刻不容缓。 网络安全…

【unity进阶知识3】封装一个事件管理系统

前言 框架的事件系统主要负责高效的方法调用与数据传递&#xff0c;实现各功能之间的解耦&#xff0c;通常在调用某个实例的方法时&#xff0c;必须先获得这个实例的引用或者新实例化一个对象&#xff0c;低耦合度的框架结构希望程序本身不去关注被调用的方法所依托的实例对象…

亲测好用,吐血整理 ChatGPT 3.5/4.0新手使用手册~

都知道ChatGPT很强大&#xff0c;聊聊天、写论文、搞翻译、写代码、写文案、审合同等等&#xff0c;无所不能~ 那么到底怎么使用呢&#xff1f;其实很简单了&#xff0c;国内AI产品发展也很快&#xff0c;很多都很好用了~ 我一直在用&#xff0c;建议收藏下来~ 有最先进、最…

古代的“契丹人”在今天属于哪个民族

在中国古代&#xff0c;自从我国历史上的夏朝建立以来&#xff0c;一共出现了二十多个朝代。随着朝代的不断更替&#xff0c;社会也在不断前进。如今&#xff0c;封建社会已经成为过去&#xff0c;朝代和政权已不再是同一个概念。例如&#xff0c;在三国时期&#xff0c;魏、蜀…

可视掏耳勺鸡肋吗?高清可视掏耳勺牌子推荐!

很多人习惯在洗漱完顺手拿一根棉签掏耳朵&#xff0c;但是棉签的表面直径大且粗糙&#xff0c;不易将耳朵深处的耳垢挖出&#xff0c;耳垢堆积在耳道深处长时间不清理会导致堵塞耳道&#xff0c;引起耳鸣甚至感染。而可视掏耳勺作为一种新型的挖耳工具&#xff0c;它的安全性也…

羽毛球场馆预约系统,便捷管理预约

全国羽毛球运动的热度不断上升&#xff0c;在健身行业中掀起了一股羽毛球热潮。同时羽毛球运动的风靡&#xff0c;也吸引了不少人入局&#xff0c;各种大大小小的羽毛球馆不断出现&#xff0c;为大众的羽毛球喜好提供了场地。 随着互联网的发展&#xff0c;羽毛球馆也开始向线…

程序员转型攻略:数据分析师、AI大模型工程师、产品经理、云计算工程师,哪个更适合你?

先给结论再说理由&#xff1a;数据分析师、AI大模型工程师、产品经理和云计算工程师。 这些领域不仅因应了当前技术发展的趋势&#xff0c;也为程序员提供了转型的广阔舞台和职业发展的新机遇。 一起来看看吧&#xff01; 数据分析师&#xff1a;数据驱动决策的关键 程序员…