注:本篇大部分是机翻,后面有机会再补充吧
本章主要讲:
- 通用内核编程指南
- 调试与发布版本
- 内核 API
- 功能和错误代码
- 字符串
- 动态内存分配
- 列表 • 驱动程序对象
- 设备对象
通用内核编程指南
- 用户模式编程和内核模式编程不同
- 用户模式编程影响范围仅限于进程,就算写的垃圾代码,也只会让进程崩溃,系统没事;但是内核模式编程如果出问题,则会导致系统崩溃和蓝屏。所以在进行内核编程的时候,一定要小心谨慎。
- 用户程序中止后,不会内存泄漏,所有私有内存都会被释放,所有句柄都会关闭;但内核驱动程序没这种保证,如果动程序卸载时仍保留分配的内存或打开的内核句柄 - 这些资源不会自动释放,只会在下一次系统启动时释放。所以一定要处理好清理工作。
- 应该尽量避免内核api返回值,例如,驱动程序可以分配一些缓冲区,然后将其传递给另一个与之合作的驱动程序。 第二个驱动程序可能会使用内存缓冲区并最终释放它。 如果内核在第一个驱动程序卸载时试图释放缓冲区,则第二个驱动程序在访问现在释放的缓冲区时会导致访问冲突,从而导致系统崩溃。
- 内核编程支持C++语法,但是也有一些不同,具体为:
- 不支持new和delete,当然你可以通过重载来使用
- 不会调用具有非默认构造函数的全局变量
- 不能使用try catch
- 标准C++ 库在内核中不可用
- 严格来说,驱动程序可以用纯 C 编写,没有任何问题。 如果您更喜欢走这条路,请使用带有 C 扩展名的文件而不是 CPP。 这将自动调用 C 编译器。
- 还有一些没看懂,后面看懂了再补充。— 。—
- 内核驱动,测试通常在另一台机器上完成,通常是托管在开发人员机器上的虚拟机。这确保了如果发生 BSOD,开发人员的机器不受影响。
- 调试内核代码必须在另一台执行实际驱动程序的机器上完成。 这是因为在内核模式下,命中断点会冻结整个机器,而不仅仅是特定进程。
- 这意味着开发人员的机器自己托管调试器,而第二台机器(同样,通常是虚拟机)执行驱动程序代码。 这两台机器必须通过某种机制连接,以便数据可以在主机(运行调试器的地方)和目标之间流动。
调试与发布版本
- KdPrint在debug构建中,会编译调用DbgPrint,而在 Release 构建中它编译为空,导致 KdPrint 调用在 Release 构建中无效。
内核 API
- 内核驱动程序使用从内核组件导出的函数。 这些函数将被称为内核 API。 大多数功能都在内核模块本身 (NtOskrnl.exe) 内实现,但有些功能可能由其他内核模块实现,例如 HAL (hal.dll)。
- 常用内核 API 前缀【巴拉巴拉一大堆,不列了】
- 很牛逼的Zw 前缀函数,牛逼在哪里还不是很了解
函数和错误代码
大多数内核 API 函数返回一个状态,指示操作成功或失败。
值 STATUS_SUCCESS (0) 表示成功, 负值表示某种错误。 以下是示例代码:
NTSTATUS DoWork() {
NTSTATUS status = CallSomeKernelFunction();
if(!NT_SUCCESS(Statue)) {
KdPirnt((L"Error occurred: 0x%08X\n", status));
return status;
}
// continue with more operations
return STATUS_SUCCESS;
}
可以在文件 ntstatus.h 中找到所有定义的 NTSTATUS 值。
字符串
字符串是简单的 Unicode 指针(wchar_t* 或它们的 typedef 之一,例如 WCHAR),但大多数处理字符串的函数都需要 UNICODE_STRING 类型的结构。
本书中使用的术语 Unicode 大致相当于 UTF-16,即每个字符 2 个字节。 这就是字符串在内核组件内部的存储方式。下面是字符串结构简化定义:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;
然后就是一些常见的字符串处理函数,和一些C字符串指针函数。
动态内存分配
内核提供了两个通用内存池供驱动程序使用(内核本身也使用它们):
- 分页池- 如果需要,可以分页的内存池。
- 非分页池-从不换出并保证保留在RAM 中的内存池。
显然,非分页池是一个“更好”的内存池,因为它永远不会发生页面错误。 我们将在本书后面看到,有些情况需要从非分页池中进行分配。
驱动程序只应使用三种:PagedPool、NonPagedPool、NonPagedPoolNx(没有执行权限的非页面池)。【不是说好了两种的嘛?】
然后是列举了一些内存池分配函数,这里不计。
可以使用 Poolmon WDK 工具或本书作者自己的 PoolMonX 工具(可从 http://www.github.com/zodiacon/AllTools 下载)查看这些池分配。下面是书中截图:
以下代码示例显示内存分配和字符串复制以保存传递给 DriverEntry 的注册表路径,并在 Unload 例程中释放该字符串:
// define a tag (because of little endianess, viewed in PoolMon as 'abcd')
#define DRIVER_TAG 'dcba'
UNICODE_STRING g_RegistryPath;
extern "C" NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
DriverObject->DriverUnload = SampleUnload;
g_RegistryPath.Buffer = (WCHAR*)ExAllocatePoolWithTag(PagedPool,
RegistryPath->Length, DRIVER_TAG);
if (g_RegistryPath.Buffer == nullptr) {
KdPrint(("Failed to allocate memory\n"));
return STATUS_INSUFFICIENT_RESOURCES;
}
g_RegistryPath.MaximumLength = RegistryPath->Length;
RtlCopyUnicodeString(&g_RegistryPath, (PCUNICODE_STRING)RegistryPath);
// %wZ is for UNICODE_STRING objects
KdPrint(("Copied registry path: %wZ\n", &g_RegistryPath));
//...
return STATUS_SUCCESS;
}
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
UNREFERENCED_PARAMETER(DriverObject);
ExFreePool(g_RegistryPath.Buffer);
KdPrint(("Sample driver Unload called\n"));
}
看不懂,先抄上再说吧。
链表
内核在其许多内部数据结构中使用循环双向链表。 例如,系统上的所有进程都由 EPROCESS 结构管理,连接在一个循环双向链表中,它的头存储在内核变量 PsActiveProcessHead 中。 所有这些列表都以类似的方式构建,围绕 LIST_ENTRY 结构定义如下:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
图 3-2 描述了一个包含一个头部和三个实例的列表的示例。
下面是个示例,定义了一个名为MyDataItem 的结构:
struct MyDataItem {
// some data members
LIST_ENTRY Link;
// more data members
};
MyDataItem* GetItem(LIST_ENTRY* pEntry) {
return CONTAINING_RECORD(pEntry, MyDataItem, Link);
}
没看太明白,之后看明白后再补充吧。
然后就是列举了一些常用函数,不计。
驱动程序对象
我们已经看到 DriverEntry 函数接受两个参数,第一个是某种类型的driver object。比如第二章这个例子:
#include <ntddk.h>
void SampleUnload(_In_ PDRIVER_OBJECT DriverObject) {
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("Sample driver Unload called\n"));
}
extern "C"
NTSTATUS
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = SampleUnload;
KdPrint(("Sample driver initialized successfully\n"));
return STATUS_SUCCESS;
}
后面讲的Semi-documented看不懂,先不写了。
设备对象
尽管驱动程序对象可能看起来像是客户与之交谈的好候选对象,但事实并非如此。 客户端与驱动程序对话的实际通信端点是设备对象。 设备对象是半文档化的 DEVICE_OBJECT 结构的实例。 没有设备对象,就没有人可以交谈。 这意味着驱动程序应至少创建一个设备对象并为其命名,以便客户端可以联系它。
主要是CreateFile 函数,CreateFile 这个名字有点误导——这里的“文件”这个词实际上是指文件对象。打开文件或设备的句柄会创建一个内核结构 FILE_OBJECT 的实例,这是另一个半文档化的结构。 更准确地说,CreateFile 接受一个符号链接,一个知道如何指向另一个内核对象的内核对象。下面是示例代码:
HANDLE hDevice = CreateFile(L"\\\\.\\PROCEXP152",
GENERIC_WRITE | GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr);
具体也没看明白,用到后再补充把。