CSDN博客

img SDNHELIXIN

如何避免驱动程序用光内核模式堆栈?

发表于2008/9/30 14:04:00  400人阅读

如何避免驱动程序用光内核模式堆栈?

内核模式堆栈是一个有限的存储区域,经常用于存储从一个函数传递到另一个函数的信息以及用于局部变量存储。虽然堆栈被映射到系统空间,但是它被视为原始调用例程的线程上下文的一部分,而不是驱动程序本身的一部分。这意味着只要调用线程正在运行,就可以保证存在堆栈,但是它可以随线程一起清除。在任何内核模式线程上运行的代码(不管它是系统线程还是驱动程序创建的线程)都使用该线程的内核模式堆栈(除非代码是 DPC,这种情况下它在某些平台上使用处理器的 DPC 堆栈)。

内核模式堆栈的大小在不同的硬件平台上有所不同。例如:

在基于 x86 的平台上,内核模式堆栈是 12K。

在基于 x64 的平台上,内核模式堆栈是 24K。(基于 x64 的平台包括使用 AMD64 体系结构处理器的系统和使用 Intel EM64T 体系结构处理器的系统)。

在基于 Itanium 的平台上,内核模式堆栈是 32K(带有 32K 的备份存储区)。(如果处理器用光来自其寄存器文件的寄存器,那么它在分配函数返回之前使用备份存储区来存放寄存器的内容。这不会直接影响到堆栈分配,但是操作系统在基于 Itanium 的平台上使用的寄存器比在其它平台上使用的更多,这使得更多的堆栈可用于驱动程序。)

每个线程都分配有一个内核模式堆栈,所以即使少量增加堆栈的大小都会极大地增加系统的内存占用。因此,给定平台上内核模式堆栈的大小由操作系统设置,不能修改。

使用内核模式堆栈的指南。驱动程序应该适当地使用内核模式堆栈并避免深度嵌套或递归的调用。传递许多字节数据的严重递归的函数可能很快用光堆栈空间。驱动程序不应该在堆栈上传递大量数据,而应该分配系统空间的内存(根据数据将被用于何处,从分页内存池和未分页内存池进行分配)并传递数据的指针。对于递归的函数,驱动程序应该限制发生递归调用的次数。

尽可能将函数设计为采用单个结构的指针而不是单个变量作为参数。如果您需要将大量基于堆栈的参数从一个函数传递到另一个函数,那么将局部变量归类到结构中并将结构的本地副本的指针传递给目标函数。这将在随后的调用中节约内核堆栈空间。例如,下面的结构在堆栈上占据单个 PVOID 的大小,而不需要单独传递变量的 6 个 ULONG:

typedef struct _COMPUTE_AXIS_COUNT_PARAMS { ULONG x; ULONG y; ULONG z; ULONG xcount; ULONG ycount; ULONG zcount; } COMPUTE_AXIS_COUNT_PARAMS;

COMPUTE_AXIS_COUNT_PARAMS params;

ComputeAxisCount(&params);

不管函数是否进行嵌套或递归调用,驱动程序都应该通过只将指针或简单计数器声明为局部变量来使内核模式堆栈的占用最小。避免将局部变量声明为字节或字符串数组来用作函数的本地缓冲区。而应该声明已在分页内存池或未分页内存池中进行分配的缓冲区指针。(记住,未分页内存池也是一种有限的资源,请节约使用。)如果您必须在堆栈上声明结构的本地副本,那么请确保结构相对较小。避免在堆栈上声明大型结构或聚合结构(例如 C++ 类)的本地副本。

在驱动程序的中断服务例程 (ISR) 中最小化内核模式堆栈占用非常重要。从 ISR 调用的函数与在任何线程中调用的函数具有相同的堆栈限制。但是,ISR 在任意线程上下文中运行,因此 ISR 会使用它正好在其上运行的线程的内核模式堆栈。这可能是当前线程的堆栈或处理器的 DPC 堆栈(如果 DPC 正在运行的话)。在任何情况下,ISR 可用的堆栈都很少(取决于该线程上堆栈的其它用户)。

在延迟过程调用 (DPC) 中,驱动程序在使用内核模式堆栈方面可以自由一些。DPC 为所有平台(除了基于 Itanium 的平台)上的每个处理器使用一个内核模式堆栈。(在基于 Itanium 的平台上,DPC 使用当前线程的堆栈。)对于每个处理器,操作系统分配单个内核模式堆栈,供运行在该处理器上的任何 DPC 使用。在给定的处理器上,同一时间只有一个 DPC 在运行,所以 DPC 实际上拥有自己的堆栈。

要确定是否存在足够的堆栈空间来调用函数或执行任务,驱动程序可以调用 IoGetStackLimitsIoGetRemainingStackSize 例程。如果没有足够的堆栈空间可用,那么驱动程序可以将任务排队到一个工作项(在单独的线程中运行,因此拥有其自身的内核模式堆栈)。但是,请记住,工作项将以 PASSIVE_LEVEL 级别在系统工作线程中运行。请记住,在原始版本的 Windows Server 2003 RTM 和早期版本的 Windows 中,必须以 IRQL PASSIVE_LEVEL 或 IRQL APC_LEVEL 调用 IoGetStackLimitsIoGetRemainingStackSize,因此不能以 DISPATCH_LEVEL 或更高级别(例如 DPC 例程)从任何例程调用它们。从 Windows Server 2003 SP1 开始,可以以任何 IRQL 调用 IoGetStackLimitsIoGetRemainingStackSize

重要:驱动程序不应该另外分配内存并将其作为内核模式堆栈使用。这绝不是任何平台的建议实践,因为它会影响操作系统的稳定性和可靠性。在基于 x64 的系统上,如果操作系统检测到未经授权的内核模式堆栈,那么它将生成一个错误检查并关闭系统。

在您的驱动程序中调试内核模式堆栈使用情况。 PREfast 是 Windows DDK 提供的带有驱动程序特定规则的静态源代码分析工具,可用来查找正在使用过量内核模式堆栈的函数。使用 PREfast 编译命令开关 /STACKHOGTHRESHOLD 来更改 PREfast 的默认堆栈使用阈值。

堆栈空间耗尽将导致操作系统崩溃和一个或几个可能的错误检查。可能包括下列错误检查:

0x7F:UNEXPECTED_KERNEL_MODE_TRAP(Parm1 设置为 EXCEPTION_DOUBLE_FAULT),由超出内核堆栈结束地址引起。

0x1E:KMODE_EXCEPTION_NOT_HANDLED,0x7E:SYSTEM_THREAD_EXCEPTION_NOT_HANDLED 或 0x8E:KERNEL_MODE_EXCEPTION_NOT_HANDLED 和一个异常码 STATUS_ACCESS_VIOLATION,后者指示内存访问违法。

0x2B:PANIC_STACK_SWITCH,通常在内核模式驱动程序使用过多堆栈空间时发生。

要调试这些问题,请使用 kf(显示堆栈跟踪)调试器命令来显示每个函数消耗的堆栈数量。如果堆栈跟踪显示不完整,那么您可以通过检查指示返回地址的符号的原始堆栈内存来手动确定堆栈跟踪。要为您的搜索建立边界,请使用 !thread 调试器扩展来找出堆栈的限制,然后使用 dps(显示字和符号)调试器命令来检查内存并尝试解析每个指针大小数目的符号。(请注意,在堆栈上找到的符号不一定指示有效的返回地址。)

检查基于 x86 的平台上的堆栈使用情况的另一项技术是检查函数的反汇编并查看函数入口处堆栈指针的移动量。(这种技术还可以在基于 x64 的平台上使用,但是反汇编稍有不同。)例如,下列典型的函数序言指示函数本地堆栈使用 140 字节 (0x8c),不计算函数可能调用的子例程消耗的任何附加堆栈:

1e3bf1de 8bff             mov     edi,edi 1e3bf1e0 55               push    ebp 1e3bf1e1 8bec             mov     ebp,esp 1e3bf1e3 81ec8c000000     sub     esp,0x8c

您可以使用 uf(反汇编函数)调试器命令或 WinDBG 反汇编窗口用汇编语言显示函数代码。

您应该做什么?
在您的驱动程序中最小化堆栈使用率:

只声明一个局部变量(指针或简单计数器)。

决不要在栈上分配大型结构或其他聚合结构(例如 C++ 类)。

不要将局部变量声明为字节或字符串数组来用作函数的本地缓冲区。而应该在分页内存池或未分页内存池中分配缓冲区并声明一个该缓冲区的指针。

避免深度嵌套或递归的调用。

不要将堆栈上的大量数据传递给另一个函数,而应该分配系统空间内存并传递数据的指针。

在递归函数中,限制发生递归调用的次数。

尽可能将函数设计为采用单个结构的指针而不是单个变量作为参数。

使用 IoGetStackLimitsIoGetRemainingStackSize 例程来确定是否存在足够的堆栈空间来调用函数去执行任务,如果没有,将任务排队到一个工作项中。

分析和调试驱动程序中内核模式堆栈的使用情况:

使用 PREfast 来查找过度使用内核模式堆栈的函数。

使用 kf 调试器命令显示每个函数消耗的堆栈数量。如果堆栈显示不完整,那么使用 !threaddps 来识别可能的返回地址。

在基于 x86 和基于 x64 的平台上,检查函数的反汇编来查看函数入口处堆栈指针的移动量。

0 0

相关博文

我的热门文章

img
取 消
img