zoukankan      html  css  js  c++  java
  • 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一个页面,把下一个页面作为新的守护页面。

  • 相关阅读:
    对easyui-validatebox的验证类型的扩展,值必须在某个区间
    从零开始学springboot笔记(六)-Spring boot之Spring Boot Spring Data JPA介绍
    从零开始学springboot笔记(五)-Spring boot之Spring Data- JPA 笔记
    从零开始学springboot笔记(四)-Spring boot之热部署之Springbootdevtools(工作中使用)
    JQuery给指定的表格的输入框或其他组件赋值
    oracle开发技巧
    数据库开发要求
    user_tab_columns和user_tab_cols的区别
    windows xp显示文件的后缀
    vue学习遇到的问题
  • 原文地址:https://www.cnblogs.com/happylong/p/4501485.html
Copyright © 2011-2022 走看看