处理异常
操作系统使用结构化异常处理来向某些类型的错误发出信号。 驱动程序调用的例程可能会引发驱动程序必须处理的异常。
系统会捕获以下常规类型的异常:
1. 硬件定义的故障或陷阱,例如,
- 访问冲突 ;
- 数据类型不对齐 ,例如在奇字节边界上对齐的 16 位数据;
- 非法和特权指令‘’
- 尝试在代码的互锁节内执行无效指令序列的锁序列无效;
- 整数除以零和溢出;
- 浮点除以零、溢出、下溢和保留操作数;
- 支持调试器的断点和单步执行 ;
2. 系统软件定义的异常,例如,
- 尝试在保护页内某个位置加载或存储数据;
- 页面读取错误 ,尝试将页面读取到内存中,并遇到并发 I/O 错误;
3. 访问冲突是尝试在当前页面保护设置下不允许的页面上执行操作。 在以下情况下会发生访问冲突:
- 读取或写入操作无效,例如写入只读页面;
- 若要访问超出当前程序地址空间限制的内存, 称为长度冲突);
- 访问当前驻留但专用于使用系统组件的页面。 例如,不允许用户模式代码访问内核正在使用的页面;
如果操作可能导致异常,驱动程序应将操作包含在 try/except 块中。 在用户模式下访问位置是异常的典型原因。 例如, ProbeForWrite 例程检查驱动程序是否实际可以写入用户模式缓冲区。 如果不能,例程将引发STATUS_ACCESS_VIOLATION异常。 在下面的代码示例中,驱动程序在 try/except 中调用 ProbeForWrite,以便它可以处理生成的异常:
try {
...
ProbeForWrite(Buffer, BufferSize, BufferAlignment);
/* Note that any access (not just the probe, which must come first,
* by the way) to Buffer must also be within a try-except.
*/
...
} except (EXCEPTION_EXECUTE_HANDLER) {
/* Error handling code */
...
}
驱动程序必须处理任何引发的异常。 未处理的异常会导致系统检查 bug。 导致引发异常的驱动程序必须处理它:较低级别的驱动程序不能依赖较高级别的驱动程序来处理异常。
驱动程序可以使用 ExRaiseAccessViolation、 ExRaiseDatatypeMisalignment 或 ExRaiseStatus 例程直接引发异常。 驱动程序必须处理这些例程引发的任何异常。
下面是部分例程列表,这些例程函数至少在某些情况下可能会引发异常:
- MmMapLockedPages
- MmProbeAndLockPages
- ProbeForRead
- ProbeForWrite
对用户模式缓冲区的内存访问也可能导致访问冲突。 例如下面的几种情况:
任何驱动程序,无论是支持 IRP 还是快速 I/O 操作都应在尝试使用之前验证用户空间中的任何地址。 I/O 管理器不会验证此类地址,也不会验证传递到驱动程序的缓冲区中嵌入的指针。
1. 未能验证在 METHOD_NEITHER IOCTL 和 FSCL 中传递的地址:I/O 管理器不对METHOD_NEITHER IOCTL 和 FSCTL 执行任何验证。 若要确保用户空间地址有效,驱动程序必须使用 ProbeForRead 和 ProbeForWrite 例程,将所有缓冲区引用括在 try/except 块中。
在以下示例中,驱动程序假定 在 Type3InputBuffer 中传递的值表示有效的地址:
case IOCTL_GET_HANDLER:
{
PULONG EntryPoint;
EntryPoint =
IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
*EntryPoint = (ULONG)DriverEntryPoint;
...
}
正确的代码如下:
case IOCTL_GET_HANDLER:
{
PULONG_PTR EntryPoint;
EntryPoint =
IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
try
{
if (Irp->RequestorMode != KernelMode)
{
ProbeForWrite(EntryPoint,
sizeof(ULONG_PTR),
TYPE_ALIGNMENT(ULONG_PTR));
}
*EntryPoint = (ULONG_PTR)DriverEntryPoint;
}
except(EXCEPTION_EXECUTE_HANDLER)
{
...
}
...
}
请注意,正确的代码将 DriverEntryPoint 强制转换为ULONG_PTR,而不是 ULONG。 此更改允许在 64 位 Windows 环境中使用。
2. 无法验证缓冲 I/O 请求中嵌入的指针:
通常,驱动程序在缓冲请求中嵌入指针,如以下示例所示:
struct ret_buf
{
void *arg; // Pointer embedded in request
int rval;
};
pBuf = Irp->AssociatedIrp.SystemBuffer;
...
arg = pBuf->arg; // Fetch the embedded pointer
...
// If the arg pointer is not valid, the following
// statement can corrupt the system:
RtlMoveMemory(arg, &info, sizeof(info));
在此示例中,驱动程序应使用 try/except 块中包含的 ProbeXxx 例程来验证嵌入的指针,其方式与前面所述的METHOD_NEITHER IOCTL 相同。 尽管嵌入指针允许驱动程序返回额外信息,但驱动程序可以使用相对偏移量或可变长度缓冲区更高效地实现相同的结果。
警告: 结构化异常处理不同于 C++ 异常。 内核不支持 C++ 异常。
KeBugCheckEx
当调用方发现不可恢复的不一致性时, KeBugCheckEx 例程以受控方式关闭系统,如果调用方继续运行,该不一致性会损坏系统。这个函数的原型如下:
void KeBugCheckEx(
[in] ULONG BugCheckCode,
[in] ULONG_PTR BugCheckParameter1,
[in] ULONG_PTR BugCheckParameter2,
[in] ULONG_PTR BugCheckParameter3,
[in] ULONG_PTR BugCheckParameter4
);
bug 检查是系统检测到的错误,导致系统立即受控关闭。 各种内核模式组件执行运行时一致性检查。 当此类组件发现不可恢复的不一致时,会导致生成 bug 检查。
如果可能,所有内核模式组件都应记录错误并继续运行,而不是调用 KeBugCheckEx。 例如,如果驱动程序无法分配所需的资源,它应记录错误,以便系统继续运行;它不得检查生成 bug。
驱动程序或其他内核模式组件应仅在出现可能损坏系统本身的致命、不可恢复的错误时调用此例程。
KeBugCheckEx 在开发驱动程序的早期阶段或正在进行测试时非常有用。 在这些情况下,传递给此例程的 BugCheckCode 应不同于 Windows 或其驱动程序已在使用的代码。 有关这些代码的列表,请参阅 Bug 检查代码。
但是,即使在驱动程序开发期间,此例程也只是有限的效用,因为它会导致系统完全关闭。 更有效的调试方法是将内核调试器附加到系统,然后使用将消息发送到调试器或中断调试器的例程。