stack 扩展机制

  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减少,另一个就是提交页面,提交页面是个很有趣的过程:

public  _alloca_probe

_chkstk proc

_alloca_probe    =  _chkstk

        push    ecx

; Calculate new TOS.

        lea     ecx, [esp] + 8 - 4      ; TOS before entering function + size for ret value
        sub     ecx, eax                ; new TOS

; Handle allocation size that results in wraparound.
; Wraparound will result in StackOverflow exception.

        sbb     eax, eax                ; 0 if CF==0, ~0 if CF==1
        not     eax                     ; ~0 if TOS did not wrapped around, 0 otherwise
        and     ecx, eax                ; set to 0 if wraparound

        mov     eax, esp                ; current TOS
        and     eax, not ( _PAGESIZE_ - 1) ; Round down to current page boundary

cs10:
        cmp     ecx, eax                ; Is new TOS
        jb      short cs20              ; in probed page?
        mov     eax, ecx                ; yes.
        pop     ecx
        xchg    esp, eax                ; update esp
        mov     eax, dword ptr [eax]    ; get return address
        mov     dword ptr [esp], eax    ; and put it at new TOS
        ret

; Find next lower page and probe
cs20:
        sub     eax, _PAGESIZE_         ; decrease by PAGESIZE
        test    dword ptr [eax],eax     ; probe page.
        jmp     short cs10

_chkstk endp

        end

 

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

posted @ 2015-05-13 19:46  嗨皮龙  阅读(793)  评论(0编辑  收藏  举报