windows-kernel-programming 中文读后感-第三章

本文档介绍了内核编程的注意事项,包括用户模式与内核模式的区别、调试方法、内核API的使用、错误代码处理、动态内存分配和链表管理。强调了内核编程的严谨性和可能导致系统崩溃的风险。同时,讨论了驱动程序对象和设备对象在通信中的角色,以及如何创建和管理设备对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:本篇大部分是机翻,后面有机会再补充吧

本章主要讲:

  • 通用内核编程指南
  • 调试与发布版本
  • 内核 API
  • 功能和错误代码
  • 字符串
  • 动态内存分配
  • 列表 • 驱动程序对象
  • 设备对象

通用内核编程指南

  1.    用户模式编程和内核模式编程不同
    1. 用户模式编程影响范围仅限于进程,就算写的垃圾代码,也只会让进程崩溃,系统没事;但是内核模式编程如果出问题,则会导致系统崩溃和蓝屏。所以在进行内核编程的时候,一定要小心谨慎。
    2. 用户程序中止后,不会内存泄漏,所有私有内存都会被释放,所有句柄都会关闭;但内核驱动程序没这种保证,如果动程序卸载时仍保留分配的内存或打开的内核句柄 - 这些资源不会自动释放,只会在下一次系统启动时释放。所以一定要处理好清理工作。
    3. 应该尽量避免内核api返回值,例如,驱动程序可以分配一些缓冲区,然后将其传递给另一个与之合作的驱动程序。 第二个驱动程序可能会使用内存缓冲区并最终释放它。 如果内核在第一个驱动程序卸载时试图释放缓冲区,则第二个驱动程序在访问现在释放的缓冲区时会导致访问冲突,从而导致系统崩溃。
    4. 内核编程支持C++语法,但是也有一些不同,具体为:
      1. 不支持new和delete,当然你可以通过重载来使用
      2. 不会调用具有非默认构造函数的全局变量
      3. 不能使用try catch
      4. 标准C++ 库在内核中不可用
      5. 严格来说,驱动程序可以用纯 C 编写,没有任何问题。 如果您更喜欢走这条路,请使用带有 C 扩展名的文件而不是 CPP。 这将自动调用 C 编译器。
      6. 还有一些没看懂,后面看懂了再补充。— 。—
    5. 内核驱动,测试通常在另一台机器上完成,通常是托管在开发人员机器上的虚拟机。这确保了如果发生 BSOD,开发人员的机器不受影响。
    6. 调试内核代码必须在另一台执行实际驱动程序的机器上完成。 这是因为在内核模式下,命中断点会冻结整个机器,而不仅仅是特定进程。
    7. 这意味着开发人员的机器自己托管调试器,而第二台机器(同样,通常是虚拟机)执行驱动程序代码。 这两台机器必须通过某种机制连接,以便数据可以在主机(运行调试器的地方)和目标之间流动。

调试与发布版本

  1. KdPrint在debug构建中,会编译调用DbgPrint,而在 Release 构建中它编译为空,导致 KdPrint 调用在 Release 构建中无效。

内核 API

  1. 内核驱动程序使用从内核组件导出的函数。 这些函数将被称为内核 API。 大多数功能都在内核模块本身 (NtOskrnl.exe) 内实现,但有些功能可能由其他内核模块实现,例如 HAL (hal.dll)。
  2. 常用内核 API 前缀【巴拉巴拉一大堆,不列了】
  3. 很牛逼的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);

具体也没看明白,用到后再补充把。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值