基本的帧布局

最基本的栈帧布局如下,

   

    本地变量

    保存的函数环境(寄存器)

    旧的ebp值

   
返回地址

   
函数参数

 

注意:如果允许了忽略帧指针 (frame pointer
omission)
,则saved ebp可能不存在。

SEH

在使用了编译器级SEH (__try/__except/__finally)的时候,栈的布局变得有一点复杂。


SEH3 Stack Layout

 

当在某函数中没有__except块(只有__finally)时,不再使用saved ebpScopetable是一个记录(record)的数组,每个record描述了一个__try块,以及块之间的关系。

   

struct _SCOPETABLE_ENTRY {
      DWORD EnclosingLevel;
      void* FilterFunc;
      void* HandlerFunc;
}

 

更多的SEH实现细节请看[1]。为了恢复try块,请注意观察try块的层次变量是如何更新的。每一个try块都分配了一个唯一的数作为标识,scopetable表中条目(entry)间的关系则描述了try块的嵌套关系。例如,如果scopetable的第i项的EnclosingLevel等于j,则表示tryj包围了tryi。 函数体自身被认为拥有级别-1。请参看附录1作为例子。

Buffer Overrun Protection

Whidbey
(MSVC2005)
编译器为SEH帧增加了一些缓冲区溢出(overrun)保护。完整的栈帧布局如下:

SEH4 Stack Layout

 

GS cookie只有在编译时打开/GS参数才存在。EH cookie总是存在。SEH4 scopetable基本和SEH3一样,只是加了一个头,

 

    

struct _EH4_SCOPETABLE {
        DWORD GSCookieOffset;
        DWORD GSCookieXOROffset;
        DWORD EHCookieOffset;
        DWORD EHCookieXOROffset;
        _EH4_SCOPETABLE_RECORD ScopeRecord[1];
    };
 
    struct _EH4_SCOPETABLE_RECORD {
        DWORD EnclosingLevel;
        long (*FilterFunc)();
            union {
            void (*HandlerAddress)();
            void (*FinallyFunc)();
        };
    };

 

GSCookieOffset =
-2
意味着没有使用GS
cookie
EH cookie总是存在。偏移量是相对于ebp的。检查按照下列方式进行: (ebp+CookieXOROffset) ^ [ebp+CookieOffset] == _security_cookie。 指向栈中scopetable的指针同样也和__security_cookie进行了异或。而且,在SEH4中最外层的级别是-2,而不是SEH3-1

C++异常模块实现

当函数采用C++异常处理(try/catch)或者有可展开对象时,情形更加复杂。

C++
EH Stack Layout

 

EH handler对每个函数都不相同(SEH正好相反),通常像这样,

   

(VC7+)

   
mov eax, OFFSET __ehfuncinfo

   
jmp ___CxxFrameHandler

 

__ehfuncinfo是一个类型为FuncInfo的结构体,它完整地描述了所有 try/catch块和所有可展开对象。

 

    

struct FuncInfo {
      // compiler version.
      // 0x19930520: up to VC6, 0x19930521: VC7.x(2002-2003), 0x19930522: VC8 (2005)
      DWORD magicNumber;
 
      // number of entries in unwind table
      int maxState;
 
      // table of unwind destructors
      UnwindMapEntry* pUnwindMap;
 
      // number of try blocks in the function
      DWORD nTryBlocks;
 
      // mapping of catch blocks to try blocks
      TryBlockMapEntry* pTryBlockMap;
 
      // not used on x86
      DWORD nIPMapEntries;
 
      // not used on x86
      void* pIPtoStateMap;
 
      // VC7+ only, expected exceptions list (function "throw" specifier)
      ESTypeList* pESTypeList;
 
      // VC8+ only, bit 0 set if function was compiled with /EHs
      int EHFlags;
};

 

Unwind mapSHEscopetable类似,但没有过滤(filter)函数。

  

struct UnwindMapEntry {
      int toState;        // target state
      void (*action)();   // action to perform (unwind funclet address)
};

 

Try块描述子,描述了一个try块及其相关的catch块,

 

struct TryBlockMapEntry {
      int tryLow;
      int tryHigh;    // this try {} covers states ranging from tryLow to tryHigh
      int catchHigh;  // highest state inside catch handlers of this try
      int nCatches;   // number of catch handlers
      HandlerType* pHandlerArray; //catch handlers table
};

 

Catch块描述子,描述了一个try块的某一个catch块(因为一个try可以同时有几个catch块)。

 

struct HandlerType {
  // 0x01: const, 0x02: volatile, 0x08: reference
  DWORD adjectives;
 
  // RTTI descriptor of the exception type. 0=any (ellipsis)
  TypeDescriptor* pType;
 
  // ebp-based offset of the exception object in the function stack.
  // 0 = no object (catch by type)
  int dispCatchObj;
 
  // address of the catch handler code.
  // returns address where to continues execution (i.e. code after the try block)
  void* addressOfHandler;
};

 

可预期异常链表(expected exceptions)(默认情况下,MSVC实现了它但没有打开,可以用/d1ESrt使之生效)。

 

   

 struct ESTypeList {
      // number of entries in the list
      int nCount;
 
      // list of exceptions; it seems only pType field in HandlerType is used
      HandlerType* pTypeArray;
};

 

RTTI类型描述子。描述了单个的C++类型。在这里用它来匹配抛出的异常类型。

 

struct TypeDescriptor {
  // vtable of type_info class
  const void * pVFTable;
 
  // used to keep the demangled name returned by type_info::name()
  void* spare;
 
  // mangled type name, e.g. ".H" = "int", ".?AUA@@" = "struct A", ".?AVA@@" = "class A"
  char name[0];
};

 

不似SEH,每个try块并没有一个与之相关的状态值。编译器不仅在进入和退出try块时修改状态值,还在每次构造和析构对象时修改。这样它就有可能在发生异常时知道哪个对象需要展开。你仍然可以通过检查与之关联的状态范围和由catch handler返回的地址来恢复try块的边界

抛出C++异常

Throw语句被转换为对_CxxThrowException()的调用,后者才真正的抛出一个Win32异常,以及异常代码0xE06D7363
(‘msc’|0xE0000000)
可自定义的Win32异常参数包括指向异常对象的指针,和它的ThrowInfo结构,使用该结构可以让异常处理程序(handler)检查catch处理程序(handler)期待的类型和抛出异常的类型是否匹配。

 

   

struct ThrowInfo {
      // 0x01: const, 0x02: volatile
      DWORD attributes;
 
      // exception destructor
      void (*pmfnUnwind)();
 
      // forward compatibility handler
      int (*pForwardCompat)();
 
      // list of types that can catch this exception.
      // i.e. the actual type and all its ancestors.
      CatchableTypeArray* pCatchableTypeArray;
    };
 
    struct CatchableTypeArray {
      // number of entries in the following array
      int nCatchableTypes;
      CatchableType* arrayOfCatchableTypes[0];
};

 

下面描述了一个可以捕获该异常的类型。

 

   

struct CatchableType {
      // 0x01: simple type (can be copied by memmove), 0x02: can be caught by reference only, 0x04: has virtual bases
      DWORD properties;
 
      // see above
      TypeDescriptor* pType;
 
      // how to cast the thrown object to this type
      PMD thisDisplacement;
 
      // object size
      int sizeOrOffset;
 
      // copy constructor address
      void (*copyFunction)();
    };
 
    // Pointer-to-member descriptor.
    struct PMD {
      // member offset
      int mdisp;
 
      // offset of the vbtable (-1 if not a virtual base)
      int pdisp;
 
      // offset to the displacement value inside the vbtable
      int vdisp;
};

函数开始和结束

相对于在函数体内生成代码来建立栈帧的方法,编译器可能会选择调用特定的prologepilog函数。它们有若干变种,每一种用于特定的函数类型。

 

 

Name

Type

EH Cookie

GS Cookie

Catch Handlers

_SEH_prolog/_SEH_epilog

SEH3

_SEH_prolog4/_SEH_epilog4 S

EH4

+

_SEH_prolog4_GS/_SEH_epilog4_GS

SEH4

+

+

_EH_prolog

C++ EH

+/-

_EH_prolog3/_EH_epilog3

C++ EH

+

_EH_prolog3_catch/_EH_epilog3

C++ EH

+

+

_EH_prolog3_GS/_EH_epilog3_GS

C++ EH

+

+

_EH_prolog3_catch_GS/_EH_epilog3_catch_GS

C++ EH

+

+

+

 

SEH2

显然,在过去它用于MSVC 1.XX编译器(由crtdll.dll导出)。可能会在一些老的NT程序中碰到它。

   

   
Saved edi

   
Saved esi

   
Saved ebx

   
Next SEH frame

   
Current SEH handler (__except_handler2)

   
Pointer to the scopetable

   
Try level

   
Saved ebp (of this function)

   
Exception pointers

   
Local variables

   
Saved ESP

   
Local variables

   
Callee EBP

   
Return address

   
Function arguments

附录 I: SEH 样例

让我们思考下面的反汇编代码。

 

func1           proc near
 
_excCode        = dword ptr -28h
buf             = byte ptr -24h
_saved_esp      = dword ptr -18h
_exception_info = dword ptr -14h
_next           = dword ptr -10h
_handler        = dword ptr -0Ch
_scopetable     = dword ptr -8
_trylevel       = dword ptr -4
str             = dword ptr  8
 
  push    ebp
  mov     ebp, esp
  push    -1
  push    offset _func1_scopetable
  push    offset _except_handler3
  mov     eax, large fs:0
  push    eax
  mov     large fs:0, esp
  add     esp, -18h
  push    ebx
  push    esi
  push    edi
 
  ; --- end of prolog ---
 
  mov     [ebp+_trylevel], 0 ;trylevel -1 -> 0: beginning of try block 0
  mov     [ebp+_trylevel], 1 ;trylevel 0 -> 1: beginning of try block 1
  mov     large dword ptr ds:123, 456
  mov     [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1
  jmp     short _endoftry1
 
_func1_filter1:                         ; __except() filter of try block 1
  mov     ecx, [ebp+_exception_info]
  mov     edx, [ecx+EXCEPTION_POINTERS.ExceptionRecord]
  mov     eax, [edx+EXCEPTION_RECORD.ExceptionCode]
  mov     [ebp+_excCode], eax
  mov     ecx, [ebp+_excCode]
  xor     eax, eax
  cmp     ecx, EXCEPTION_ACCESS_VIOLATION
  setz    al
  retn
 
_func1_handler1:                        ; beginning of handler for try block 1
  mov     esp, [ebp+_saved_esp]
  push    offset aAccessViolatio ; "Access violation"
  call    _printf
  add     esp, 4
  mov     [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1
 
_endoftry1:
  mov     edx, [ebp+str]
  push    edx
  lea     eax, [ebp+buf]
  push    eax
  call    _strcpy
  add     esp, 8
  mov     [ebp+_trylevel], -1 ; trylevel 0 -> -1: end of try block 0
  call    _func1_handler0     ; execute __finally of try block 0
  jmp     short _endoftry0
 
_func1_handler0:                        ; __finally handler of try block 0
  push    offset aInFinally ; "in finally"
  call    _puts
  add     esp, 4
  retn
 
_endoftry0:
  ; --- epilog ---
  mov     ecx, [ebp+_next]
  mov     large fs:0, ecx
  pop     edi
  pop     esi
  pop     ebx
  mov     esp, ebp
  pop     ebp
  retn
func1           endp
 
_func1_scopetable
  ;try block 0
  dd -1                      ;EnclosingLevel
  dd 0                       ;FilterFunc
  dd offset _func1_handler0  ;HandlerFunc
 
  ;try block 1
  dd 0                       ;EnclosingLevel
  dd offset _func1_filter1   ;FilterFunc
  dd offset _func1_handler1  ;HandlerFunc

 

Try0没有filter,因此它的handler是一个__finally块。Try1EnclosingLevel0,所以它被置于try0内部。考虑到这些,我们就可以试着重构出函数的结构:

void func1 (char* str)
    {
      char buf[12];
      __try // try block 0
      {
         __try // try block 1
         {
           *(int*)123=456;
         }
         __except(GetExceptCode() == EXCEPTION_ACCESS_VIOLATION)
         {
            printf("Access violation");
         }
         strcpy(buf,str);
      }
      __finally
      {
         puts("in finally");
      }
    }

附录 II: C++异常样例

func1           proc near
 
_a1             = dword ptr -24h
_exc            = dword ptr -20h
e               = dword ptr -1Ch
a2              = dword ptr -18h
a1              = dword ptr -14h
_saved_esp      = dword ptr -10h
_next           = dword ptr -0Ch
_handler        = dword ptr -8
_state          = dword ptr -4
 
  push    ebp
  mov     ebp, esp
  push    0FFFFFFFFh
  push    offset func1_ehhandler
  mov     eax, large fs:0
  push    eax
  mov     large fs:0, esp
  push    ecx
  sub     esp, 14h
  push    ebx
  push    esi
  push    edi
  mov     [ebp+_saved_esp], esp
 
  ; --- end of prolog ---
 
  lea     ecx, [ebp+a1]
  call    A::A(void)
  mov     [ebp+_state], 0          ; state -1 -> 0: a1 constructed
  mov     [ebp+a1], 1              ; a1.m1 = 1
  mov     byte ptr [ebp+_state], 1 ; state 0 -> 1: try {
  lea     ecx, [ebp+a2]
  call    A::A(void)
  mov     [ebp+_a1], eax
  mov     byte ptr [ebp+_state], 2 ; state 2: a2 constructed
  mov     [ebp+a2], 2              ; a2.m1 = 2
  mov     eax, [ebp+a1]
  cmp     eax, [ebp+a2]            ; a1.m1 == a2.m1?
  jnz     short loc_40109F
  mov     [ebp+_exc], offset aAbc  ; _exc = "abc"
  push    offset __TI1?PAD         ; char *
  lea     ecx, [ebp+_exc]
  push    ecx
  call    _CxxThrowException       ; throw "abc";
 
loc_40109F:
  mov     byte ptr [ebp+_state], 1 ; state 2 -> 1: destruct a2
  lea     ecx, [ebp+a2]
  call    A::~A(void)
  jmp     short func1_try0end
 
; catch (char * e)
func1_try0handler_pchar:
  mov     edx, [ebp+e]
  push    edx
  push    offset aCaughtS ; "Caught %s\n"
  call    ds:printf       ;
  add     esp, 8
  mov     eax, offset func1_try0end
  retn
 
; catch (...)
func1_try0handler_ellipsis:
  push    offset aCaught___ ; "Caught ...\n"
  call    ds:printf
  add     esp, 4
  mov     eax, offset func1_try0end
  retn
 
func1_try0end:
  mov     [ebp+_state], 0          ; state 1 -> 0: }//try
  push    offset aAfterTry ; "after try\n"
  call    ds:printf
  add     esp, 4
  mov     [ebp+_state], -1         ; state 0 -> -1: destruct a1
  lea     ecx, [ebp+a1]
  call    A::~A(void)
  ; --- epilog ---
  mov     ecx, [ebp+_next]
  mov     large fs:0, ecx
  pop     edi
  pop     esi
  pop     ebx
  mov     esp, ebp
  pop     ebp
  retn
func1           endp
 
func1_ehhandler proc near
  mov     eax, offset func1_funcinfo
  jmp     __CxxFrameHandler
func1_ehhandler endp
 
func1_funcinfo
  dd 19930520h            ; magicNumber
  dd 4                    ; maxState
  dd offset func1_unwindmap ; pUnwindMap
  dd 1                    ; nTryBlocks
  dd offset func1_trymap  ; pTryBlockMap
  dd 0                    ; nIPMapEntries
  dd 0                    ; pIPtoStateMap
  dd 0                    ; pESTypeList
 
func1_unwindmap
  dd -1
  dd offset func1_unwind_1tobase ; action
  dd 0                    ; toState
  dd 0                    ; action
  dd 1                    ; toState
  dd offset func1_unwind_2to1 ; action
  dd 0                    ; toState
  dd 0                    ; action
 
func1_trymap
  dd 1                    ; tryLow
  dd 2                    ; tryHigh
  dd 3                    ; catchHigh
  dd 2                    ; nCatches
  dd offset func1_tryhandlers_0 ; pHandlerArray
  dd 0
 
func1_tryhandlers_0
dd 0                    ; adjectives
dd offset char * `RTTI Type Descriptor' ; pType
dd -1Ch                 ; dispCatchObj
dd offset func1_try0handler_pchar ; addressOfHandler
dd 0                    ; adjectives
dd 0                    ; pType
dd 0                    ; dispCatchObj
dd offset func1_try0handler_ellipsis ; addressOfHandler
 
func1_unwind_1tobase proc near
a1 = byte ptr -14h
  lea     ecx, [ebp+a1]
  call    A::~A(void)
  retn
func1_unwind_1tobase endp
 
func1_unwind_2to1 proc near
a2 = byte ptr -18h
  lea     ecx, [ebp+a2]
  call    A::~A(void)
  retn
func1_unwind_2to1 endp

 

我们看看能找到些什么。FuncInfo结构的maxState域是4,表示我们在unwind
map
中有4项,从03。通过检查这个map,我们看到下列动作在栈展开中被执行:

 

Ÿ   state 3 -> state 0 (no
action)

Ÿ   state 2 -> state 1 (destruct
a2)

Ÿ   state 1 -> state 0 (no
action)

Ÿ   state 0 -> state -1
(destruct a1)

 

再看看try map,我们可以推断状态12对应于try块,状态3对应于catch块。这样,从状态0转换到1指明了try块的开始,从10表示try块执行完毕。从函数代码,我们也可以看到从-10是构造a1,从12是构造a2。所以状态图应该象这样:

那箭头13从何而来?我们在函数代码中看不到,在FuncInfo也看不到,因为它是异常handler完成的。如果一个异常发生在try块内部,异常handler首先展开栈到tryLow表示的状态(这里指状态1),然后在调用catch handler前设置状态值为tryHigh+12+1=3)。

这个try块有两个catch
handlers
。第一个指定了一个期待的异常类型(char*),并从栈中获得异常对象e-1Ch=e)。第二个没有指定类型(比如那个省略号)。它们都返回用于恢复执行流的地址,例如,刚好在try块后面的那个地址。现在,我们恢复的函数代码如下:


 void func1 ()
    {
      A a1;
      a1.m1 = 1;
      try {
        A a2;
        a2.m1 = 2;
        if (a1.m1 == a1.m2) throw "abc";
      }
      catch(char* e)
      {
        printf("Caught %s\n",e);
      }
      catch(...)
      {
        printf("Caught ...\n");
      }
      printf("after try\n");
    }

打赏