天天看点

驱动篇——内核编程基础

驱动篇之内核编程基础,详细介绍一些编写驱动的一些注意事项和一些细节。

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客o'o地址即可,但必须事先通知我。

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问个问题,你明确学驱动的目的了吗?你的开发环境准备好了吗?上一节的内容学会了吗? 没有的话就不要继续了,请重新学习前面驱动篇的教程内容继续。

🔒 华丽的分割线 🔒

  在应用层编程我们可以使用<code>WINDOWS</code>提供的各种<code>API</code>函数,只要导入头文件<code>windows.h</code>就可以了。但是在内核编程的时候,微软为内核程序提供了专用的<code>API</code>,只要在程序中包含相应的头文件就可以使用了,如:<code>#include &lt;ntddk.h&gt;</code>,前提你必须安装了<code>WDK</code>。

  遇到不会的函数或者不知道如何使用函数怎么办?在应用层编程的时候,我们通过<code>MSDN</code>来了解函数的详细信息,在内核编程的时候,要使用<code>WDK</code>自己的帮助文档。

  然而<code>WDK</code>说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。如果要使用未导出的函数,只要自己定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。有两种办法都可以获取为导出的函数地址:特征码搜索和解析内核<code>PDB</code>文件。对于第一种方法,每个函数不可能是一模一样的,它们的硬编码具有不同的特征,通过这个特定的独一无二的硬编码可以搜到我想要的函数。对于最后一种方法,我们思考一下<code>WinDbg</code>为什么那么强大。为什么<code>WinDbg</code>可以轻松分析一些结构体,或者函数名称?本质原因它有符号文件并且能够解析它,也就是<code>PDB</code>文件。也就是为什么我们之前要为它配备符号文件路径。

  在内核编程的时候,强烈建议大家遵守<code>WDK</code>的编码习惯,建议不要这样写:<code>unsigned long length;</code>,建议这样写:<code>ULONG length</code>。

  如下是<code>WDK</code>习惯与我们常规的习惯:

WDK 习惯

SDK 习惯

ULONG

unsigned long

PULONG

unsigned long*

UCHAR

unsigned char

PUCHAR

unsigned char*

UINT

unsigned int

PUNIT

unsigned int*

VOID

void

PVOID

void*

  大部分内核函数的返回值都是<code>NTSTATUS</code>类型,如:

  这个值能说明函数执行的结果,比如:

  当你调用的内核函数,如果返回的结果不是<code>STATUS_SUCCESS</code>,就说明函数执行中遇到了问题,具体是什么问题,可以在<code>ntstatus.h</code>文件中查看。

  在内核中,一个小小的错误就可能导致蓝屏,比如:读写一个无效的内存地址。为了让自己的内核程序更加健壮,强烈建议大家在编写内核程序时,使用异常处理,降低蓝屏的可能性。不过错误大了该蓝屏的还是蓝屏。

  <code>Windows</code>提供了结构化异常处理机制,一般的编译器都是支持的,如下:

  出现异常时,可根据<code>filter_value</code>的值来决定程序该如果执行,当<code>filter_value</code>的值为:

1️⃣ <code>EXCEPTION_EXECUTE_HANDLER(1)</code>:代码进入<code>except</code>块

2️⃣ <code>EXCEPTION_CONTINUE_SEARCH(0)</code>:不处理异常,由上一层调用函数处理

3️⃣ <code>EXCEPTION_CONTINUE_EXECUTION(-1)</code>:回去继续执行错误处的代码

  对内存的使用,主要就是:申请、设置、拷贝以及释放。我们在编写3环的应用程序和内核对应的函数举例如下,具体使用请查看<code>MSDN</code>和<code>WDK</code>的帮助文档:

普通程序

内核中

malloc

ExAllocatePoolWithTag

memset

RtlFillMemory

memcpy

RtlMoveMemory

free

ExFreePool

  当然<code>malloc</code>对应的内核函数有很多,但是有很多已经被废弃掉了,下面是说明:

  The ExAllocatePool routine is obsolete, and is exported only for existing binaries. Use ExAllocatePoolWithTag instead.

  当我们进行内存申请时,比如遇到<code>ExAllocatePoolWithTag</code>函数时,会有<code>POOL_TYPE PoolType</code>这个参数。那么什么是<code>POOL_TYPE</code>,我们查一下<code>WDK</code>:

  其实我们用的成员也就前两项目<code>NonPagedPool</code>和<code>PagedPool</code>,分别申请非分页内存和分页内存。那么什么是非分页内存?什么是分页内存?我们在前面介绍过申请的物理页并不是永久属于你的,这个申请的页就是分页内存,也就是可以随时被操作系统撤走转到虚拟内存交换文件。而非分页内存就是告诉操作系统,不要把我的申请的物理页撤走,这就是我独享的物理页。操作系统就不会把它给撤走转到文件中了。

  在编写3环程序我们经常用:<code>CHAR(char)</code>/<code>WCHAR(wchar_t)</code>来分别表示宅字符串和宽字符串,用0表示结尾。但是在内核中,我们常用:<code>ANSI_STRING</code>/<code>UNICODE_STRING</code>来分别表示宅字符串和宽字符串。它们的结构如下:

  <code>ANSI_STRING</code>字符串:

  <code>UNICODE_STRING</code>字符串:

  为什么内核要用这样的字符串呢?主要是为了安全考虑。我们初学<code>C语言</code>的时候经常打印出<code>烫烫烫</code>之类的字符串,那是因为它打印没用0结尾的字符串的结果。如果内核出现了这个问题,很容易导致蓝屏。故使用改结构体保证安全性。当然,处理这样的字符串内核就有专门处理的函数,接下来我将继续介绍。

  字符串常用的功能无非就是:创建、复制、比较以及转换等等。它们的函数如下,具体使用请查看<code>WDK</code>的帮助文档:

ANSI_STRING

UNICODE_STRING

RtlInitAnsiString

RtlInitUnicodeString

RtlCopyString

RtlCopyUnicodeString

RtlCompareString

RtlCompareUnicodeString

RtlAnsiStringToUnicodeString

RtlUnicodeStringToAnsiString

  上一篇教程我们用了一段代码,用来测试驱动是否能够加载并执行,下面我们就来解析它,上次使用的代码如下:

  <code>DriverEntry</code>是驱动程序的入口,如果驱动加载成功后,就像<code>Dll</code>加载成功调用<code>DllMain</code>函数一样,调用该函数。

  是指向<code>DRIVER_OBJECT</code>结构体的指针。一个驱动文件被加载后,它的完整信息将会返回给我们。我们来看看<code>DRIVER_OBJECT</code>这个结构体存了什么,下面是头文件里面的定义:

  既然是讲解基础,我们就挑最重要的几个来讲解。不过为了方便学习驱动,我们对上面的代码进行小小的修改:

  然后编译,让虚拟机加载这个驱动。如下图所示,然后我们得到了它的首地址:

驱动篇——内核编程基础

  然后我们再<code>dt</code>一下:

  驱动对象加载后的起始地址。

  驱动对象加载后的内存大小。

  它是一个存储目前所有已加载的驱动程序信息相关的<code>LDR_DATA_TABLE_ENTRY</code>结构体的双向循环链表。通过这个东西来实现把它们全部串起来,通过这个我们也可以进行遍历。我们通过<code>WinDbg</code>来看看。我们先<code>dt</code>一下我们自己编写的驱动的<code>DriverSection</code>:

  然后我们继续<code>dt</code>下一个成员:

  可以看出,我们可以通过这个链表实现遍历驱动程序的信息。

  指示驱动对象的名字,是一个<code>_UNICODE_STRING</code>的结构体。

  驱动对象的卸载地址,如果存在则会调用它。它的定义:

  剩下的未介绍的成员,自己感兴趣的自行继续探索。

  <code>IRQL</code>全称<code>Interrupt Request Level</code>,即中断执行的优先级。它是<code>Windows</code>自己定义的一套优先级方案,与<code>CPU</code>无关,数值越大权限越高。中断包括了硬中断和软中断,硬中断是由硬件产生,而软中断则是完全虚拟出来的。处理器在一个<code>IRQL</code>上执行线程代码,每个处理器的<code>IRQL</code>决定了它如何处理中断,以及允许接收哪些中断。在同一处理器上,线程只能被更高级别<code>IRQL</code>的线程能中断。每个处理器都有自己的中断<code>IRQL</code>。常见的<code>IRQL</code>级别有四个:<code>Passive</code>、<code>APC</code>、<code>Dispatch</code>、<code>DIRQL</code>。<code>PASSIVE_LEVEL</code>是最低级别,没有被屏蔽的中断,线程执行用户模式,可以访问分页内存。<code>APC_LEVEL</code>只有<code>APC</code>级别的中断被屏蔽,可以访问分页内存。当有<code>APC</code>发生时,处理器提升到<code>APC</code>级别,就屏蔽掉其它<code>APC</code>。<code>DISPATCH_LEVEL</code>可以屏蔽<code>DPC</code>(延迟过程) 和更低的中断,不能访问分页内存。因为只能处理分页内存,所以在这个级别,能够访问的<code>API</code>大大减少。对于我们内核安全来讲,了解这些就够了,如下是<code>IRQL</code>的示意图:

驱动篇——内核编程基础

  在进行内核程序编写的时候,尤其注意<code>IRQL</code>这个东西。有很多的蓝屏因此而起。

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

  俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。

1️⃣ 编写驱动,申请一块内存,并在内存中存储<code>GDT</code>表的所有数据。然后在<code>DebugView</code>中显示出来,最后释放内存。

2️⃣ 编写驱动,实现如下功能:

&lt;1&gt; 初始化一个字符串;

&lt;2&gt; 拷贝一个字符串;

&lt;3&gt; 比较两个字符串是否相等;

&lt;4&gt; <code>ANSI_STRING</code>与<code>UNICODE_STRING</code>字符串相互转换;

3️⃣ 思考题:为什么<code>DISPATCH_LEVEL</code>不能访问分页内存。

  驱动篇——内核空间与内核模块

驱动篇——内核编程基础
驱动篇——内核编程基础

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可

本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟

转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15491543.html

驱动篇——内核编程基础