windows中,每个线程都关联一个stack,stack的默认大小是1M,用于存放临时变量,函数参数,返回地址等。
但是当一个线程开始运行的时候不是其相关stack的内存就真正被提交,因为如果一个进程有10个线程,那么如果这10个线程的stack的内存都被提交,那么虚拟内存就占用了10M,就需要想对应的页表项等开销,而且这10M到底是否被真的使用还是个未知数,所以系统的策略是只提交几个页面,然后通过一个guard page来实现按需提交。
先看一下GUARD_PAGE:
TEB at 7ffdf000
ExceptionList: 0013fd0c
StackBase: 00140000
StackLimit: 0013e000
0: kd> dt _TEB 7ffdf000
ntdll!_TEB
......
+0xe0c DeallocationStack : 0x00040000
1M stack 范围 StackBase ~ DeallocationStack
0: kd> .formats(0x140000-0x40000)/0n1024
Evaluate expression:
Hex: 00000400
Decimal: 1024
Octal: 00000002000
Binary: 00000000 00000000 00000100 00000000
Chars: ....
Time: Thu Jan 01 08:17:04 1970
Float: low 1.43493e-042 high 0
Double: 5.05923e-321
stack 大小 1024KB --- 1M
StackBase ~ StackLimit: 0013e000 这个是 COMMIT 的页面
StackLimit 下个页面是 MEM_COMMIT | PAGE_READWRITE | PAGE_GUARD
StackLimit 再下一个页面是 MEM_RESERVE
也就是说TEB,确切说是TIB记录着线程的guard page。
当一个函数的局部变量过大,例如:char szBuffer[0x10000] = { 0 },那么线程被系统预先提交的页不满足使用了,那么编译器会在该函数的开头插入_chkstk,用以给该函数提交足够大的stack的页面用以装载很大的局部变量。
_chkstk的核心一个是使ESP减少,另一个就是提交页面,提交页面是个很有趣的过程:
test dword ptr [eax],eax; 可是这行代码仅仅是读了一下eax指向的内存, 这里的读操作将触发一个STATUS_GUARD_PAGE异常, 内核通过捕获这个异常,
从而知道你的线程已经越过了栈中已提交内存区域的边界, 这时应该增加新的页了。操作系统规定栈中的页commit必须逐页提交, 具体的实现是, 对已提交的内存区域的最后一个页设置
PAGE_GUARD属性,当这个页发生 STATUS_GUARD_PAGE异常时(这个异常会自动清除其 PAGE_GUARD属性), 再commit下一个页, 同时设置其 PAGE_GUARD属 性。
typedef struct _NT_TIB
{
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase; // 栈的最高地址 , 栈底
PVOID StackLimit; // 已经commit的栈的内存的最低地址, 栈顶,
.....
} NT_TIB;
栈的内存如此排布:
StackBase ----> |..............| <----- 高
|______| |
|..............| |
|______| |
|..............| Protect 00000004 PAGE_READWRITE
|______| State 00001000 MEM_COMMIT
|..............|
|______| |
|..............| |
|______| |
|..............| |
StackLimit ---> |______| <____|__
|..............| Protect 00000104 PAGE_READWRITE | PAGE_GUARD
|______| <___State 00001000 MEM_COMMIT
|..............| |
|______| |
|..............| |
|______| State 00002000 MEM_RESERVE (没有Commit的页谈不上Protect)
|..............| |
|______| |
|..............| <----
当一个线程被创建的时候, 操作系统会给它的栈reserve一块区域, 通常大小为1M, 然后立刻在栈顶commit n个pages。
前n-1 个Page是供线程立刻可以使用, 第二个page是守护页面(guard page), 当线程用完第一个页面的时候, 需要更多栈内存会访问到守护页面, 操作系统会得到通知。
系统会再commit一个页面,把下一个页面作为新的守护页面。