1、简介
普通的Win32线程有两个栈:一个是用户栈,另一个是内核栈;而如果是内核中创建的系统工作线程,则只有内核栈。只要代码在内核中运行,线程就一定是使用其内核栈的。栈的主要作用是维护函数调用帧,以及为局部变量提供空间。
在Windows里,一个线程的用户空间的信息都记录在了TEB中,而TEB中又有一个域叫做NtTib,这里面就存放着有关用户站的信息。由于TEB结构过于复杂,这里只列举涉及到的,完整的定义在这一部分的末尾给出。
typedef struct PEBTEB_STRUCT(_TEB) {
PEBTEB_STRUCT(NT_TIB) NtTib;
….
…
…
}
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
其中NtTib中的StackBase就是用户栈的基地址了。用户栈的大小只要用通过栈底地址和栈顶地址简单相减就可以得到了。
而内核线程的一些信息存储在了KTHREAD结构体中,KTHREAD的大致结构如下:
typedef struct _KTHREAD {
....
// The following fields are referenced during trap, interrupts, or
// context switches.
//
// N.B. The Teb address and TlsArray are loaded as a quadword quantity
// on MIPS and therefore must be on a quadword boundary.
PVOID InitialStack;
PVOID StackLimit;
.....
PVOID StackBase;
KAPC SuspendApc;
KSEMAPHORE SuspendSemaphore;
LIST_ENTRY ThreadListEntry;
....
} KTHREAD, *PKTHREAD, *RESTRICTED_POINTER PRKTHREAD;
2、一些需要注意的地方
用户栈可以指定其大小,默认是1MB,通过编译指令/stack可改设其他值。
普通内核栈的大小是固定的,由系统根据CPU架构而定,x86系统上为12KB,x64系统上为24KB,安腾系统上为32KB。对于GUI线程,普通内核栈空间可能不够,所以系统又定义了“大内核栈”概念,可以在需要的时候增长栈空间。只有GUI线程才能使用大内核栈,这也是系统规定的。
Windows将GDI和USER模块,即“窗口与图形模块”的实现移到了内核中,称为Windows子系统内核服务,并形成一个win32k.sys内核文件。而用户层仅留调用接口,由User32.dll和GDI32.dll两个文件暴露出来。判断一个线程是不是GUI线程的依据,竟非常的简单:线程初建时,都是普通线程,第一次调用Windows子系统内核服务(只要用户程序调用了User32.dll和GDI32.dll中的函数,并导致相关内核服务在内核中被执行),系统即立刻将之转变为GUI线程,并从而切换到“大内核栈”;倘若至线程结束,并未有任何一个子系统内核服务被调用,那么它一直都是普通线程,一直使用普通内核栈。
内核栈的问题,正是内核中使用C++的一个最大障碍。在实际编程时,为了尽量避免发生栈溢出错误,需要经常对栈剩余空间保持一份警惕,尤其在可能形成很深的调用栈(如递归调用)的情况下。内核函数IoGetStackLimits与IoGetRemainingStackSize分别用来获取当前内核栈的边界与剩余空间,可使用这两个函数实时控制栈状况。可在函数入口处包含下列代码。
// 如果当前内核栈空间小于150字节,就让函数返回
if(IoGetRemainingStackSize() < 150)
return; // 如有可能,可指定一个特殊的错误值