Windows内核编程基础(2)

news2024/11/20 2:33:48

上下文环境

应用层应用程序工作在用户模式,内核驱动程序工作在内核模式。这里的用户模式和内核模式是基于CPU的特权环来定义的,CPU提供了0环~3环(ring 0 ~ ring 3)共四个特权环,Windows操作系统使用了其中的0环和3环,0环为内核模式3环为用户模式。不同环之间的代码特权不同,访问地址空间也不同。如对0环的指令来说,可以执行特权指令,访问内核模式的地址空间范围。

 Privilege rings for the x86 available in protected mode

应用程序有独立进程的概念,比如我们开发一个exe程序,当这个程序运行时,我们清楚地知道程序的代码运行在哪一个线程。

但是对于内核开发来说,进程的概念显得相当模糊,初学者往往不清楚自己的驱动代码具体运行在什么进程或线程中,但搞清楚这些细节是驱动入门的重要途径。

上下文(Context)泛指CPU在执行代码时,该代码所处的环境与状态。这些环境状态包括但不限于:当前代码所属线程、中断请求级别、CPU寄存器各状态等。

在前面的MyFirstDriver示例代码中,里面涉及了两个函数DriverEntryDriverUnload,这两个函数都是由系统调用的,那么这两个函数被调用时处于哪一个进程中呢。我们使用PsGetCurrentThreadId函数输出当前进程的Id。

 1 #include<ntddk.h>
 2 
 3 VOID DriverUnload(PDRIVER_OBJECT DriverObject)
 4 {
 5     if (DriverObject != NULL)
 6     {
 7         DbgPrint("[%ws]Driver Unload,Driver Address:%p,CurrentProcessId = 0x%p\n", __FUNCTIONW__, DriverObject, PsGetCurrentProcessId());
 8     }
 9     return;
10 }
11 
12 extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
13 {
14     if (DriverObject != NULL)
15     {
16         DriverObject->DriverUnload = DriverUnload;
17     }
18 
19     if (RegistryPath != NULL)
20     {
21 
22     }
23 
24     DbgPrint("[%ws]Driver Entry,CurrentProcessId = 0x%p\n", __FUNCTIONW__, PsGetCurrentProcessId());
25 
26     return STATUS_SUCCESS;
27 }

运行结果如下:

可以看到无论是驱动入口函数还是卸载函数 ,都隶属于进程Id为4的进程。在资源管理器中可以看到这个Pid为4的进程名为System

System进程其实是操作系统虚拟出来的一个进程,代表系统内核。一般来说内核代码都处于SYSTEM进程空间中,但是驱动对象的派遣例程一般工作在发起请求的进程中。

与上下文概念相关联的是地址空间

对32位系统来说,应用层程序有独立的2GB低地址空间,这2GB地址是虚拟地址,不同进程之间相互独立,互不影响,而高地址的2GB是内核共享的地址空间。

64位系统与32位系统类似,在64位Windows中,虚拟地址空间的理论大小为2^64字节,但实际仅使用2^64字节范围的一小部分,范围从0x000000000000x7FFFFFFFFFF的8TB用于应用层空间,范围从0xFFFF0800000000000xFFFFFFFFFFFFFFFF248TB用于内核空间。

关于独立的应用层地址空间与共享的内核地址空间的区别

比如有两个进程P1P2,在各自进程空间内修改了0x00001234处的内容

对于应用层:P1只能看到自己修改后的内容,P2也只能看到自己修改后的内容,相互不影响

对于内核层:内核空间是共享的,所以对于两个驱动程序来说,驱动P1修改内核某个地址的内容,驱动P2可以读取到驱动P1修改后的内容。

所以我们在书写代码时必须清楚自己的代码在运行时所对应的上下文,以免造成驱动异常。这一点很重要

驱动异常

我们刚开始学习内核开发时,难免会遇到由于代码编写不合规而引发系统崩溃的问题,系统崩溃具体表现为蓝屏(BSOD)。

蓝屏是Windows系统遇到无法处理的异常或错误时,为了避免错误进一步扩大而触发的保护机制。

蓝屏发生时系统将无法继续运行,业务中断,还可能会产生磁盘文件被破坏等一系列问题。

 Windows XP 蓝屏界面

驱动异常的原因有很多,常见的有:高IRQL死锁、内存访问违例、函数堆栈不平衡等等。

不管是由哪种原因导致的蓝屏,系统都会报告一个异常码。

如上图中:0x0000008E为异常码,0x0000008E后面括号内的四个值为附加参数,不同异常码和附加参数

我们可以根据这些信息来初步定位异常的类型与发生的大致原因。具体的细节可以参考WDK文档。

错误检查代码参考 - Windows drivers | Microsoft Learn

除了蓝屏界面显示的异常码,若条件允许,系统会在系统目录生成一个DUMP文件,这个DUMP文件内部保存着蓝屏时刻的异常信息,包括内存、寄存器、异常记录等,我们可以根据DUMP文件中提供的蛛丝马迹来回溯问题。

可以参考下面的文章:

https://www.cnblogs.com/zhaotianff/p/15150244.html

字符串操作

用户模式编程中,使用的字符串主要是UnicodeAscii

内核模式编程中,大部分是使用的Unicode

但与应用层不同的是,内核层一般不直接使用WCHAR类型的Unicode字符串,而是使用UNICODE_STRING类型来表示Unicode

UNICODE_STRING是内核中表示字符串的结构体,定义如下:

1 typedef struct _UNICODE_STRING {
2     USHORT Length;
3     USHORT MaximumLength;
4     PWSTR Buffer
5 }UNICODE_STRING, *PUNICODE_STRING

其中Buffer为一个指针,指向一个UNICODE类型的字符串缓冲区;

MaximumLength表示Buffer所指向缓冲区的总空间大小,一般等于Buffer被分配时的内存大小,单位为字节

Length表示Buffer所指向缓冲区中字符串的长度,单位也是字节。

注意:Buffer指向的字符串,并不要求以'\0'作为结束。

UNICODE_STRING的常用操作

初始化

RtlInitUnicodeString,这个函数的作用是把一个以'\0'结尾的WCHAR类型的Unicode字符串初始化成UNICODE_STRING类型的字符串

1 VOID
2 NTAPI
3 RtlInitUnicodeString(
4     _Out_ PUNICODE_STRING DestinationString,
5     _In_opt_z_ __drv_aliasesMem PCWSTR SourceString
6     );

参数:

DestinationString:指向要初始化 的UNICODE_STRING 结构的指针

SourceString:指向以 null 结尾的宽字符字符串的指针。 此字符串用于初始化 DestinationString 指向的字符串。

下面是RtlInitUnicodeString的简单使用示例:

1     UNICODE_STRING str{};
2     RtlInitUnicodeString(&str, L"HelloWorld");
3     DbgPrint("String:%wZ", &str);

RtlInitUnicodeString函数的使用非常简单,但需要注意的是,RtlInitUnicodeString函数并没有为str.Buffer申请内存,而是令str.Buffer指向字符串L"HelloWorld"的首地址,所以使用RtlInitUnicodeString初始化后,在使用str期间,要保证SourceString有效。

拷贝操作

使用RtlUnicodeStringCopyString函数,可以实现UNICODE_STRING的拷贝操作。

RtlUnicodeStringCopyString函数原型如下:

头文件:ntstrsafe.h

1 NTSTRSAFEDDI
2 RtlUnicodeStringCopyString(
3         _Inout_ PUNICODE_STRING DestinationString,
4         _In_ NTSTRSAFE_PCWSTR pszSrc)

RtlUnicodeStringCopyString函数把以'\0'结尾的字符串pszSrc拷贝到DestinationString中。

虽然这个函数的功能看起来与前面的RtlInitUnicodeString函数功能类似,但是两者存在本质上的区别:

RtlInitUnicodeString函数内部只是简单地使DestinationString.Buffer指向函数的第二个参数SourceString,没有任何的拷贝操作。

RtlUnicodeStringCopyString函数会把pszSrc字符串拷贝到DestinationString所指向的内存中。

下面是RtlUnicodeStringCopyString的简单使用示例

1 WCHAR strBuffer[128]{};
2 UNICODE_STRING str{};
3 RtlInitEmptyUnicodeString(&str, strBuffer, sizeof(strBuffer));
4 RtlUnicodeStringCopyString(&str, L"HelloWorld");
5 DbgPrint("%wZ", &str);

RtlUnicodeStringCopyString返回NTSTATUS类型,只能在PASSIVE_LEVEL级别的IRQL中运行。

链表

链表是内核开发中常见的数据结构。主要分为单向链表和双向链表。

单向链表:只有一个链表节点指针,该指针指向后面一个链表节点

两向链表:有两个链表节点指针,分别指向前一个链表节点以及后一个链表结点 。

这里重点学习双向链表(后续直接简称为“链表”)。

WDK中的链表定义如下:

1 typedef struct _LIST_ENTRY {
2    struct _LIST_ENTRY *Flink;
3    struct _LIST_ENTRY *Blink;
4 } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

这不同于我们在数据结构中学习的链表结构,因为它不包含数据字段。

我们通过下面的例子来对链表进行深入的了解。

假设我们定义了一个ListPos结构体,用于存储位置信息。

1 typedef struct _ListPos
2 {
3     int m_nX;
4     int m_nY;
5 }ListPos, * PListPos;

现在需要把ListPos做为一个链表结点,具体的做法是把LIST_ENTRY作为ListPos的一个成员,如下:

1 typedef struct _ListPos
2 {
3     int m_nX;
4     int m_nY;
5     LIST_ENTRY m_listEntry;
6 }ListPos, * PListPos;

说明:m_listEntry可以放在结构体的任意位置,没有强制要求。

x64系统下,ListPos结构体的内存布局如下:

 ListPos结构体在x64系统下的内存布局

  多个ListPos结构体的关系

 在实际情况下,为了方便操作,会定义一个链表头结点,头节点不包含任何内容,只是一个LIST_ENTRY结构,如下图所示:

  带头节点的链表

下面我们看一下链表的使用方法

头节点初始化

链表头节点没有任何内容,只表示链表的头部(永远都是起始位置),对链表的所有操作都是从头部开始的。

当链表只有一个头节点而没有其它节点时,该链表就是一个空的链表。FlinkBlink指向头节点自身。

使用头节点初始化函数InitializeListHead来对头节点进行初始化,初始化的作用就是修改FlinkBlink的值,使其指向自身。

InitializeListHead定义如下:

 1 FORCEINLINE
 2 VOID
 3 InitializeListHead(
 4     _Out_ PLIST_ENTRY ListHead
 5     )
 6 
 7 {
 8 
 9     ListHead->Flink = ListHead->Blink = ListHead;
10     return;
11 }

使用方法如下:

1     LIST_ENTRY lst_Header{};
2     InitializeListHead(&lst_Header);

节点插入

节点的插入可以分为两种

1、将节点插入到链表头部位置,使用的是InsertHeadList函数,声明如下:

1 FORCEINLINE
2 VOID
3 InsertHeadList(
4     _Inout_ PLIST_ENTRY ListHead,
5     _Out_ __drv_aliasesMem PLIST_ENTRY Entry
6     );

2、将节点插入到尾部位置,使用的是InsertTailList,声明跟上面的函数声明类似,仅仅是函数名不一样。

这里再次需要注意的是,头节点仅表示头部,节点的插入永远都是在头节点的后面(它的身体部分),这跟我们以前在数据结构里学习的链表有一点不同。

下面看一下简单的使用:

 1 LIST_ENTRY lst_Header{};
 2 InitializeListHead(&lst_Header);
 3 
 4 ListPos p_A{};
 5 p_A.m_nX = 0;
 6 p_A.m_nY = 0;
 7 
 8 ListPos p_B{};
 9 p_B.m_nX = 1;
10 p_B.m_nY = 1;
11 
12 ListPos p_C{};
13 p_C.m_nX = 2;
14 p_C.m_nY = 2;
15 
16 InsertHeadList(&lst_Header, &p_B.m_listEntry);
17 InsertHeadList(&lst_Header, &p_A.m_listEntry);
18 InsertTailList(&lst_Header, &p_C.m_listEntry);

这里先对头节点进行初始化,把节点B插入到链表中,在插入前链表为空,插入后链表中存在一个节点B,紧接着使用lnsertHeadList函数把节点A插入到链表最前面,此时链表中的节点为:头节点→节点A→节点B,最后使用InsertTailList函数,把节点C插入到链表的最后面,插入完成后的链表:头节点→节点A→节点B→节点C。(头节点永远在前面)

链表遍历 

其实到这里我们还是有疑问的,因为链表是串连起来了,但是这个数据怎么去取呢。

我们接着往下看。

我们从头节点向后遍历,代码如下:

1     PLIST_ENTRY pListEntry = lst_Header.Flink;
2     while (pListEntry != &lst_Header)
3     {
4         PListPos pListPos = CONTAINING_RECORD(pListEntry, ListPos, m_listEntry);
5         DbgPrint("ListEntry = %p,ListPos = %p,x = %d y = %d",
6             pListEntry, pListPos, pListPos->m_nX, pListPos->m_nY);
7         pListEntry = pListEntry->Flink;
8     }

遍历步骤如下:

1、定义pListEntry指针变量用于遍历。把ListHeader.Flink的值赋给pListEntry,此时pListEntry指向链表中的第一个节点。

2、在 while循环中,首先访问节点的数据,然后让pListEntry指向下一个节点,循环结束的条件是pListEntry ==&ListHeader

3、通过CONTAINING_RECORD宏把 m_ListEntry的地址转换成结构体ListPos的首地址。
pListEntry指向的地址是ListPos结构体中的m_ListEntry地址,而m_ListEntry成员的地址并不是这个结构体的首地址)

第3步是获取数据的关键步骤,主要通过CONTAINING_RECORD宏完成,CONTAINING_RECORD宏的用法如下:

1 #define CONTAINING_RECORD(address, type, field) 

Address:表示LIST_ENTRY的地址,就是上面代码中pListEntry指向的地址

Type:表示类型,ListPos,就是我们前面定义的带数据和节点信息的结构体。

Field:表示结构体中LIST_ENTRY成员的名字。就是上面代码中的m_ListEntry。

CONTAINING_RECORD宏通过Type 与Field这两个成员,计算出Field成员距离结构体顶部的内存距离,然后结合具体的Address成员,算出最终的结构体首地址,WDKCONTAINING_ RECORD宏的定义如下:

1 #define CONTAINING_RECORD(address, type, field) ((type *)( \
2                                                   (PCHAR)(address) - \
3                                                   (ULONG_PTR)(&((type *)0)->field)))

运行上面的代码,输出 结果如下:

可以看到ListEntryListPos的距离(m_ListEntry与结构体首地址的内存差距)为8字节,和实际定义的情况一致。

节点移除

移除节点有三种方式:

1、移除链表中的第一个节点,使用RemoveHeadList,函数声明如下:

1 RemoveHeadList(
2     _Inout_ PLIST_ENTRY ListHead);

ListHead:表示头节点

返回值:成功移除返回从链表移除的节点指针,如果无节点可以移除,返回NULL

使用方法如下:

1     RemoveHeadList(&lst_Header);

2、移除链表中特定的节点,使用RemoveEntryList函数,声明如下:

1 RemoveEntryList(
2     _In_ PLIST_ENTRY Entry
3     )

Entry:表示 要移除的链表节点指针。

返回值:Boolean,如果链表在节点移除后,变成了空链表,就返回TRUE,否则返回FALSE

如果需要判断一个链表是否为空(只有头节点),可以使用IsListEmpty函数,声明如下:

1 IsListEmpty(
2     _In_ const LIST_ENTRY * ListHead
3     );

ListHeader:表示头节点

返回值:Boolean,True表示链表为空,FALSE表示链表非空

3、移除链表中的最后一个节点,使用RemoveTailList,这个函数的声明和使用与RemoveHeadList类似。

参考资料

https://en.wikibooks.org/wiki/Windows_Programming/User_Mode_vs_Kernel_Mode#:~:text=Ring%200%20(also%20known%20as,has%20restricted%20access%20to%20resources.

https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

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

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

相关文章

【深度学习】(7)--保存最优模型

文章目录 保存最优模型一、两种保存方法1. 保存模型参数2. 保存完整模型 二、迭代模型 总结 保存最优模型 我们在迭代模型训练时&#xff0c;随着次数初始的增多&#xff0c;模型的准确率会逐渐的上升&#xff0c;但是同时也随着迭代次数越来越多&#xff0c;由于模型会开始学…

大数据-148 Apache Kudu 从 Flink 下沉数据到 Kudu

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Spring Boot房屋租赁平台:现代化解决方案

1 绪论 1.1 研究背景 中国的科技的不断进步&#xff0c;计算机发展也慢慢的越来越成熟&#xff0c;人们对计算机也是越来越更加的依赖&#xff0c;科研、教育慢慢用于计算机进行管理。从第一台计算机的产生&#xff0c;到现在计算机已经发展到我们无法想象。给我们的生活改变很…

Recaptcha2 图像识别 API 对接说明

Recaptcha2 图像识别 API 对接说明 本文将介绍一种 Recaptcha2 图像识别2 API 对接说明&#xff0c;它可以通过用户输入识别的内容和 Recaptcha2验证码图像&#xff0c;最后返回需要点击的小图像的坐标&#xff0c;完成验证。 接下来介绍下 Recaptcha2 图像识别 API 的对接说…

8.12DoG (Difference of Gaussians)

基本概念 不同尺度的高斯模糊图像之间的差异&#xff08;DoG&#xff09;&#xff0c;用于边缘检测。函数: cv::GaussianBlur() 结合 cv::Laplacian() 或者自定义DoG实现。 在OpenCV中并没有直接提供一个名为“DoG”&#xff08;Difference of Gaussians&#xff09;的函数&a…

【学术会议征稿】第四届人工智能、机器人和通信国际会议(ICAIRC 2024)

第四届人工智能、机器人和通信国际会议&#xff08;ICAIRC 2024&#xff09; 2024 4th International Conference on Artificial Intelligence, Robotics, and Communication 第四届人工智能、机器人和通信国际会议&#xff08;ICAIRC 2024&#xff09;定于2024年12月27-29日…

css 自定义滚动条样式

* { scrollbar-color: auto !important; scrollbar-width: auto; } //滚动条宽高 ::-webkit-scrollbar { width: 4px; height: 4px; background: transparent; } ::-webkit-scrollbar-thumb { //滑块部分 border-radius: 5px; background-color: rgba(32, 224, 254, 1); } ::-…

【Python报错已解决】TypeError: can only concatenate str (not “float“) to str

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

docker compose的使用

docker compose 1.概述 是 Docker 官方提供的一款开源工具&#xff0c;主要用于简化在单个主机上定义和运行多容器 Docker 应用的过程。它的核心作用是容器编排&#xff0c;使得开发者能够在一个统一的环境中以声明式的方式管理多容器应用的服务及其依赖关系。 也就是说Docker…

用 Django 5 快速生成一个简单 进销存 系统 添加 个打印 按钮

一、前置条件&#xff1a; 1.安装好python 【关联网址】 2. 安装好vscode 【关联网址】 插件 3. 登陆海螺AI【关联网址】 4. 安装好 pip install django 【关联网址】 pip install django -i https://mirrors.aliyun.com/pypi/simple/ 二、开始生成 1. 打开vscode 打开…

[数据库实验五] 审计及触发器

一、实验目的与要求&#xff1a; 1.了解MySQL审计功能及实现方式 2.掌握触发器的工作原理、定义及操作方法 二、实验内容&#xff1a; 注&#xff1a; 在同一个触发器内编写多行代码&#xff0c;需要用结构begin ……end 函数current_user()获得当前登录用户名 1.自动保存…

Linux 应用层自定义协议与序列化

文章目录 一、应用层1、协议2、序列化 && 反序列化3、通过Json库进行数据的序列化 && 反序列化Json::Value类Json::Reader类Json::Writer类 二、为什么read、write、recv、send和Tcp支持全双工&#xff1f;发数据的本质&#xff1a;tcp支持全双工通信的原因&am…

gitlab-runner集成CI/CD完整项目部署

目录 1.环境安装 2.gitlab代码仓库搭建 3.gitlab-runner-安装以及注册 4..gitlab-ci.yml脚本 5.脚本说明 6.build.sh 7.test.sh 8. deploy.sh 9.运行流水线 10.选择流水线分支 11.查看运行阶段 12.查看运行日志 13.查看服务器真实日志 1.环境安装 确保服务器的Java环…

Python_异常机制

软件程序在运行过程中&#xff0c;非常可能遇到刚刚提到的这些问题&#xff0c;我们称之为异常&#xff0c;英文是&#xff1a;Exception&#xff0c;意思是例外。遇到这些例外情况&#xff0c;或者叫异常&#xff0c;我们怎么让写的程序做出合理的处理&#xff0c;安全的退出&…

Footprint Growthly Quest 工具:赋能 Telegram 社区实现 Web3 飞速增长

作者&#xff1a;Stella L (stellafootprint.network) 在 Web3 的快节奏世界里&#xff0c;社区互动是关键。而众多 Web3 社区之所以能够蓬勃发展&#xff0c;很大程度上得益于 Telegram 平台。正因如此&#xff0c;Footprint Analytics 精心打造了 Growthly —— 一款专为 Tel…

Tkinter制作登录界面以及登陆后页面切换

Tkinter制作登录界面以及登陆后页面切换 前言序言1. 由来2. 思路3. 项目结构描述4. 项目实战1. 登录界面实现&#xff08;代码&#xff09;2. 首页界面实现&#xff08;代码&#xff09;3. 打包build.py&#xff08;与main.py同级目录&#xff09;4. 打包安装包 前言 本帖子&a…

【nrm】npm 注册表管理器

nrm是什么 nrm&#xff08;NPM Registry Manager&#xff09;是一个用于管理 Node.js 包管理器&#xff08;如 npm 和 Yarn&#xff09;的注册表工具。它可以帮助用户快速切换不同的 npm 源&#xff0c;以便于提高包安装的速度和效率&#xff0c;特别是在中国大陆地区&#xf…

Ubuntu23.10下处理libncurses5-dev包的安装问题

Ubuntu23.10下处理libncurses5-dev包的安装问题 导语环境准备问题和解决方案总结参考文献 导语 使用Ubuntu23.10的时候&#xff0c;遇到需要termios的场景&#xff0c;结果发现无论是codeblocks还是系统本身的gcc都无法找到term.h和curse.h&#xff0c;网上找了很多解决方案都…

了解云计算工作负载保护的重要性,确保数据和应用程序安全

云计算de小白 云计算技术的快速发展使数据和应用程序安全成为一种关键需求&#xff0c;而不仅仅是一种偏好。随着越来越多的客户公司将业务迁移到云端&#xff0c;保护他们的云工作负载&#xff08;指所有部署的应用程序和服务&#xff09;变得越来越重要。云工作负载保护&…

【stm32】TIM定时器输出比较-PWM驱动LED呼吸灯/舵机/直流电机

TIM定时器输出比较 一、输出比较简介1、OC&#xff08;Output Compare&#xff09;输出比较2、PWM简介3、输出比较通道(高级)4、输出比较通道(通用)5、输出比较模式6、PWM基本结构配置步骤&#xff1a;程序代码&#xff1a;PWM驱动LED呼吸灯 7、参数计算8、舵机简介程序代码&am…