stack 扩展机制

来源:互联网 发布:sqlserver 视图 union 编辑:程序博客网 时间:2024/05/17 01:06

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一个页面,把下一个页面作为新的守护页面。