微软Visual C++是Win32最广泛使用的编译器,因此Win32反向器对其内部工作非常熟悉。能够识别编译器生成的粘合代码有助于快速集中于程序员编写的实际代码。它还有助于恢复程序的高级结构。我将集中讨论MSVC编译程序中的堆栈布局、异常处理和相关结构。假设对汇编程序、寄存器、调用约定等有一定的了解。

名词解释:

  • Stack frame---堆栈帧,函数使用的堆栈段的片段。通常包含函数参数、返回调用方地址、保存的寄存器、局部变量和其他特定于此函数的数据。在x86(和大多数其他架构)上,调用者和被调用者堆栈帧是连续的。
  • Frame pointer---帧指针,指向堆栈帧内固定位置的寄存器或其他变量。通常堆栈帧中的所有数据都是相对于帧指针寻址的。在x86上,它通常是ebp,通常指向返回地址的正下方。
  • Object---对象,(C++)类的一个实例。
  • Unwindable Object---不可逆转的对象,具有自动存储类说明符的本地对象,在堆栈上分配,超出范围时需要销毁。
  • Stack UInwinding---堆栈展开:当控制由于异常而离开作用域时,自动销毁此类对象。

win32里C/C++异常分类

在C或C++程序中可以使用两种类型的异常。

  • SEH异常(来自“结构化异常处理”)。也称为Win32或系统异常。著名的Matt Pietrek文章[1]详尽地介绍了这些内容。它们是C程序唯一可用的例外。编译器级别的支持包括关键字try、except、finally和其他一些关键字。
  • C++异常(有时称为“EH”)。在SEH之上实现,C++异常允许任意类型的抛出和捕获。C++的一个非常重要的特点是在异常处理过程中自动堆栈展开,而MSVC使用一个非常复杂的底层框架来保证它在所有情况下都能正常工作。

栈基本布局

在下图中,内存地址从上到下递增,因此堆栈“向上”增长这是在IDA中表示堆栈的方式,与大多数其他出版物相反。 最基本的堆栈帧如下所示:
    ...
Local variables
Other saved registers
Saved ebp
Return address
Function arguments
..

注意:如果启用帧指针省略,则保存的ebp可能不存在。

SEH栈

在使用编译器级SEH(__try/__except/__finally)的情况下,堆栈布局会变得更复杂一些。

当函数中只有__finally块而没有__except块时,保存ESP是没有用的。Scopetable是一个记录数组,它描述每个__try块及其之间的关系:

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

要恢复try块,请查看try level变量的更新方式。它为每个try块分配一个唯一的数字,嵌套由scopetable条目之间的关系描述。例如,如果scopetable条目i包含EnclosingLevel=j,则try块j包含try块i。函数体被认为具有try level-1。

缓冲区溢出保护

Whidbey(MSVC 2005)编译器为SEH帧添加了一些缓冲区溢出保护。其中的完整堆栈帧布局如下所示:

只有使用/GS开关编译函数时,才存在GS cookie。EHcookie总是存在的。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]==指向堆栈中scopetable的“_security_cookie"指针也与“_security_cookie"进行异或。所以,在SEH4中,最外层的作用域级别是-2,而不是像SEH3中的-1。

C++异常模型的实现

当C++异常处理(try/catch)或不可展开对象出现在函数中时,情况变得非常复杂。

EH处理程序对于每个函数都是不同的(与SEH情况不同),通常如下所示:

     (VC7+)
mov eax, OFFSET __ehfuncinfo
jmp ___CxxFrameHandler __ehfuncinfo is a structure of type FuncInfo which fully describes all try/catch blocks and unwindable objects in the function. 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 map is similar to the SEH scopetable, only without filter functions: struct UnwindMapEntry {
int toState; // target state
void (*action)(); // action to perform (unwind funclet address)
}; Try block descriptor. Describes a try{} block with associated catches. 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 block descriptor. Describes a single catch() of a try block. 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;
}; List of expected exceptions (implemented but not enabled in MSVC by default, use /d1ESrt to enable). 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 type descriptor. Describes a single C++ type. Used here to match the thrown exception type with catch type. 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处理程序返回的地址来恢复try块边界。

抛出C++异常

throw语句被转换为_CxxThrowException()的调用,这实际上引发了一个Win32(SEH)异常,代码为0xE06D7363('msc'| 0xe000000)。Win32异常的自定义参数包括指向异常对象及其ThrowInfo结构的指针,使用这些指针,异常处理程序可以将抛出的异常类型与catch处理程序所期望的类型相匹配。

    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];
}; Describes a type that can catch this exception. 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;
};

Prologs and Epilogs

编译器可以选择调用特定的prolog和epilog函数,而不是在函数体中发出用于设置堆栈帧的代码。有几个变体,每个都用于特定的函数类型:

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
...

附录一: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 -
_trylevel = dword ptr -
str = dword ptr push ebp
mov ebp, esp
push -
push offset _func1_scopetable
push offset _except_handler3
mov eax, large fs:
push eax
mov large fs:, esp
add esp, -18h
push ebx
push esi
push edi ; --- end of prolog --- mov [ebp+_trylevel], ;trylevel -1 -> 0: beginning of try block 0
mov [ebp+_trylevel], ;trylevel 0 -> 1: beginning of try block 1
mov large dword ptr ds:,
mov [ebp+_trylevel], ;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,
mov [ebp+_trylevel], ;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,
mov [ebp+_trylevel], - ; 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,
retn _endoftry0:
; --- epilog ---
mov ecx, [ebp+_next]
mov large fs:, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
retn
func1 endp _func1_scopetable
;try block 0
dd - ;EnclosingLevel
dd ;FilterFunc
dd offset _func1_handler0 ;HandlerFunc ;try block 1
dd ;EnclosingLevel
dd offset _func1_filter1 ;FilterFunc
dd offset _func1_handler1 ;HandlerFunc

0号try块没有筛选器,因此其处理程序是一个__finally{}块。1号try块的封闭级别为0,因此它位于0号try块中。考虑到这一点,我们可以尝试重建功能结构:

void func1 (char* str)
{
char buf[];
__try // try block 0
{
__try // try block 1
{
*(int*)=;
}
__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 -
_state = dword ptr - push ebp
mov ebp, esp
push 0FFFFFFFFh
push offset func1_ehhandler
mov eax, large fs:
push eax
mov large fs:, 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], ; state -1 -> 0: a1 constructed
mov [ebp+a1], ; a1.m1 = 1
mov byte ptr [ebp+_state], ; state 0 -> 1: try {
lea ecx, [ebp+a2]
call A::A(void)
mov [ebp+_a1], eax
mov byte ptr [ebp+_state], ; state 2: a2 constructed
mov [ebp+a2], ; 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], ; 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,
mov eax, offset func1_try0end
retn ; catch (...)
func1_try0handler_ellipsis:
push offset aCaught___ ; "Caught ...\n"
call ds:printf
add esp,
mov eax, offset func1_try0end
retn func1_try0end:
mov [ebp+_state], ; state 1 -> 0: }//try
push offset aAfterTry ; "after try\n"
call ds:printf
add esp,
mov [ebp+_state], - ; state 0 -> -1: destruct a1
lea ecx, [ebp+a1]
call A::~A(void)
; --- epilog ---
mov ecx, [ebp+_next]
mov large fs:, 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 ; maxState
dd offset func1_unwindmap ; pUnwindMap
dd ; nTryBlocks
dd offset func1_trymap ; pTryBlockMap
dd ; nIPMapEntries
dd ; pIPtoStateMap
dd ; pESTypeList func1_unwindmap
dd -
dd offset func1_unwind_1tobase ; action
dd ; toState
dd ; action
dd ; toState
dd offset func1_unwind_2to1 ; action
dd ; toState
dd ; action func1_trymap
dd ; tryLow
dd ; tryHigh
dd ; catchHigh
dd ; nCatches
dd offset func1_tryhandlers_0 ; pHandlerArray
dd func1_tryhandlers_0
dd ; 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,这意味着展开映射中有4个条目,从0到3。通过检查映射,我们可以看到在展开期间执行以下操作:

  • 状态3->状态0(无操作)
  • 状态2->状态1(销毁a2)
  • 状态1->状态0(无操作)
  • 状态0->状态-1(析构函数a1)

检查try map,我们可以推断状态1和2对应于try块体,状态3对应于catch块体。因此,从状态0更改为状态1表示try块的开始,从1更改为0表示try块的结束。从函数代码中我们还可以看到-1->0是a1的构造,1->2是a2的构造。所以状态图如下:

箭头1->3是从哪里来的?我们无法在函数代码或FuncInfo结构中看到它,因为它是由异常处理程序完成的。如果异常发生在try块中,异常处理程序首先将堆栈展开为tryLow值(在本例中为1),然后在调用catch处理程序之前将状态值设置为tryHigh+1(2+1=3)。

try块有两个catch处理程序。第一个具有catch类型(char*)并获取堆栈上的异常对象(-1Ch=e)。第二个没有类型(即省略catch)。两个处理程序都返回恢复执行的地址,即try块之后的位置。现在我们可以恢复函数代码:
void func1 ()
{
A a1;
a1.m1 = ;
try {
A a2;
a2.m1 = ;
if (a1.m1 == a1.m2) throw "abc";
}
catch(char* e)
{
printf("Caught %s\n",e);
}
catch(...)
{
printf("Caught ...\n");
}
printf("after try\n");
}

Visual C++ 里的异常处理的更多相关文章

  1. vs里 .sln和.suo 文件 Visual Studio里*.sln和*.suo文件的作用

    Visual Studio里*.sln和*.suo文件的作用      VS项目采用两种文件类型(.sln   和   .suo)来存储特定于解决方案的设置.这些文件总称为解决方案文件,为解决方案资源 ...

  2. [R]R语言里的异常处理与错误控制

    之前一直只是在写小程序脚本工具,几乎不会对异常和错误进行控制和处理. 随着脚本结构和逻辑更复杂,脚本输出结果的准确性验证困难,同时已发布脚本的维护也变得困难.所以也开始考虑引入异常处理和测试工具的事情 ...

  3. 在Visual Studio里配置及查看IL(转载)

    原文地址:http://www.myext.cn/other/a_25162.html 在之前的版本VS2010中,在Tools下有IL Disassembler(IL中间语言查看器),但是我想直接集 ...

  4. 在Visual Studio里配置及查看IL

    原文地址:http://www.myext.cn/other/a_25162.html 在之前的版本VS2010中,在Tools下有IL Disassembler(IL中间语言查看器),但是我想直接集 ...

  5. c++ 11 国标标准方面的异常处理与微软在Visual Studio 2012的异常处理的区别

    这段代码: __try { } __except(GetErrorCode()) { } 可以捕获空指针,但是包围在其中的代码不能有自带析构函数的对象.c++ 11 标准里面的auto_ptr关键字, ...

  6. Rotor里的异常处理

    我看到了一些关于Rotor(和CLR)中使用的异常处理机制的问题.下面是关于Rotor异常处理的另一个注意事项列表.目的是帮助Rotor开发人员调试和理解CLR中的异常. 异常生成和抛出 此步骤在很大 ...

  7. Visual C++ 里的 Classes, Methods and RTTI

    类的基本布局 为了说明以下内容,让我们考虑这个简单的例子: class A { int a1; public: virtual int A_virt1(); virtual int A_virt2() ...

  8. 在 Visual Studio 里一秒打开 ILSpy,并反编译当前项目

    下载 ILSpy(如果已有 ILSpy,忽略此步骤) 1.打开官方git 仓库 - https://github.com/icsharpcode/ILSpy 2.点击右侧的 Releases 最新版, ...

  9. int main(int argc, char * argv[]) 里的异常处理

    #import <UIKit/UIKit.h> #import "AppDelegate.h" int main(int argc, char * argv[]) { ...

随机推荐

  1. GoLang的概述

    GoLang的概述 1.什么是程序 完成某个功能的指令的集合 2.Go语言的诞生小故事 2.1. 开发团队-三个大牛 2.2.Google创造Golang的原因 2.3.Golang 的发展历程 20 ...

  2. python_三目运算

    首先确定三目运算的使用条件, if只有两个才能用三目  只有 if:else:    先写个if else的小例子: if push == "lpush": self.conn.l ...

  3. 基于Spark2.X系列的累加器和Streaming基础

    一.累加器API 关于累加器,前面我也写了一篇博客,顺便粘贴这儿,对比学习,Spark学习之编程进阶总结(一).Spark 2.0系列引入了一个更加简单和更高性能的累加器API,如在1.X版本中可以这 ...

  4. Java11新特性 - 标准Java异步HTTP客户端

    Java9开始引入的一个处理 HTTP 请求的的 HTTP Client API,该 API 支持同步和异步,而在 Java 11 中已经为正式可用状态,你可以在 java.net 包中找到这个 AP ...

  5. streamController

  6. 珠宝juelrye宝石

    juelrye n.珠宝 late 14c., juelrye "precious ornaments, jewel juelrye (uncountable) Adornment with ...

  7. springboot使用RestHighLevelClient7简单操作ElasticSearch7增删查改/索引创建

    本次操作是在  Windows上安装ElasticSearch7  进行操作 导入依赖 <?xml version="1.0" encoding="UTF-8&qu ...

  8. SQL与NoSQL区别--商业SQL数据库衰落--oracle面临困境

    转自:商用数据库之死:Oracle 面临困境 这二十年来,商业数据库市场仍然是 IT 行业最稳定.最具黏性的领域之一,Oracle.IBM 和微软三家厂商瓜分了 80% 的份额.然而,我们认为这个领域 ...

  9. HotSpot虚拟机的锁优化

    面试中多次被问到synchronized关键字的实现原理,一直认为仅是monitorenter与monitorexit两条指令而已,原来底层涉及到多种锁优化策略,包括:自旋锁,轻量锁,偏向锁. 1.自 ...

  10. Guava Cache 工具类

    maven依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava&l ...