Win32 SEH异常深度探索_4 编译器对SEH的支持

While I've made occasional reference to _try and _except, just about everything I've written about so far is implemented by the operating system. However, in looking at the contortions of my two small programs that used raw system SEH, a compiler wrapper around this functionality is definitely in order. Let's now see how Visual C++ builds its structured exception handling support on top of the system-level SEH facilities.

前面所讲的都是操作系统的实现,现在看看 VC 如果支持 SEH 的。

 

Before moving on, it's important to remember that another compiler could do something completely different with the raw system-level SEH facilities. Nothing says that a compiler must implement the _try/_except model that the Win32 SDK documentation describes. For example, the upcoming Visual Basic® 5.0 uses structured exception handling in its runtime code, but the data structures and algorithms are completely different from what I'll describe here.

编译器可能会以不同的实现方式来实现 SHE ,而不是 __try/__except 模型。

 

If you read through the Win32 SDK documentation on structured exception handling, you'll come across the following syntax for a so called "frame-based" exception handler:

Win32 SDK 文档中 frame-based” 异常处理语法如下:

try {      // guarded body of code } except (filter-expression) {      // exception-handler block }

 

To be a bit simplistic, all of the code within a try block in a function is protected by an EXCEPTION_REGISTRATION that's built on the function's stack frame. On function entry, the new EXCEPTION_REGISTRATION is put at the head of the linked list of exception handlers. After the end of the _try block, its EXCEPTION_REGISTRATION is removed from the head of the list. As I mentioned earlier, the head of the exception handler chain is kept at FS:[0]. Thus, if you're stepping through assembly language in a debugger and you see instructions such as

try 块中的代码被嵌在函数栈中的 EXCEPTION_REGISTRATION 保护。当进入函数时,一个新的 EXCEPTION_REGISTRATION 被插入异常处理链表, __try 块结束时,这个结构从链表中移除。如果你看到下面代码:

MOV DWORD PTR FS:[00000000],ESP MOV DWORD PTR FS:[00000000],ECX

代表代码进入或销毁一个 try/catch 块。

 

Now that you know that a _try block corresponds to an EXCEPTION_REGISTRATION structure on the stack, what about the callback function within the EXCEPTION_ REGISTRATION? Using Win32 terminology, the exception callback function corresponds to the filter-expression code. To refresh your memory, the filter-expression is the code in parens after the _except keyword. It's this filter-expression code that decides whether the code in the subsequent {} block will execute.

一个 __try 块与栈中一个 EXCEPTION_REGISTRATION 关联。那么 EXCEPTION_REGISTRATION 中的异常回调函数呢?根据 Win32 术语,异常回调函数相当于过滤表达式 filter-expression )。这个代码决定后面 {} 块中的代码是否被执行。

 

Since you write the filter-expression code, you get to decide if a particular exception will be handled at this particular point in your code. Your filter-expression code might be as simple as saying "EXCEPTION_EXECUTE_ HANDLER." Alternatively, the filter-expression might invoke a function that calculates p to 20 million places before returning a code telling the system what to do next. It's your choice. The key point: your filter-expression code is effectively the exception callback that I described earlier.

当你编写过滤代码时,你决定了某个异常是否在这里被处理。这段过滤代码相当于前面提到的异常回调函数。

 

What I've just described, while reasonably simple, is nonetheless a rose-colored-glasses view of the world. The ugly reality is that things are more complicated. For starters, your filter-expression code isn't called directly by the operating system. Rather, the exception handler field in every EXCEPTION_REGISTRATION points to the same function. This function is in the Visual C++ runtime library and is called __except_handler3. It's __except_handler3 that calls your filter-expression code, and I'll come back to it a bit later.

前面描述很简单,但实际中是很复杂的。比如你的过滤表达式并不被 OS 直接调用,而在 EXCEPTION_REGISTRATION 中的指针总是指向同一个叫 __except_handler3 的函数,它在 VC 运行时库中实现,由他调用你的过滤表达式。

 

Another twist to the simplistic view that I described earlier is that EXCEPTION_REGISTRATIONs aren't built and torn down every time a _try block is entered or exits. Instead, there's just one EXCEPTION_REGISTRATION created in any function that uses SEH. In other words, you can have multiple _try/_except constructs in a function, yet only one EXCEPTION_REGISTRATION is created on the stack. Likewise, you might have a _try block nested within another _try block in a function. Still, Visual C++ creates only one EXCEPTION_REGISTRATION.

前面的另一个曲解就是 EXCEPTION_REGISTRATION 并不是在进入每个 __try 块时创建,相反,每个函数只有一个 EXCEPTION_REGISTRATION ,而不管函数中有多个 __try 块,他们如何相互嵌套。

 

If a single exception handler (that is, __except_handler3) suffices for the whole EXE or DLL, and if a single EXCEPTION_REGISTRATION handles multiple _try blocks, there's obviously more going on here than meets the eye. This magic is accomplished through data in tables that you don't normally see. However, since the whole purpose of this article is to dissect structured exception handling, let's take a look at these data structures.

如果想通过一个异常处理函数 __except_handler3 )处理整个 EXE DLL, 一个 EXCEPTION_REGISTRATION 处理多个 __try 块,那么需要特殊的表数据结构处理这些数据。

 

 

The Extended Exception Handling Frame

The Visual C++ SEH implementation doesn't use the raw EXCEPTION_REGISTRATION structure. Instead, it adds additional data fields to the end of the structure. This additional data is critical to allowing a single function (__except_handler3) to handle all exceptions and route control to the appropriate filter-expressions and _except blocks throughout the code. A hint to the format of the Visual C++ extended EXCEPTION_REGISTRATION structure is found in the EXSUP.INC file from the Visual C++ runtime library sources. In this file, you'll find the following (commented out) definition:

VC 在实现中加入了很多额外数据在 EXCEPTION_REGISTRATION 之后,用于帮助 __except_handler3 处理各种异常,并将控制转给适当的过滤表达式。

可以参见 EXSUP.INC 中的扩展 EXCEPTION_REGISTRATION 结构

;struct _EXCEPTION_REGISTRATION{

;     struct _EXCEPTION_REGISTRATION *prev;

;     void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);

;     struct scopetable_entry *scopetable;

;     int trylevel;

;     int _ebp;

;     PEXCEPTION_POINTERS xpointers;

;};

 

The scopetable field points to an array of structures of type scopetable_entries, while the trylevel field is essentially an index into this array. The last field, _ebp, is the value of the stack frame pointer (EBP) before the EXCEPTION_REGISTRATION was created.

Scopetable 指向一个数组,数组元素类型为 scopetable_entry Trylevel 为这个数组的一个索引。 _ebp 代表创建 EXCEPTION_REGISTRATION 前的 EBP 值。

 

It's not coincidental that the _ebp field becomes part of the extended EXCEPTION_REGISTRATION structure. It's included in the structure via the PUSH EBP instruction that most functions begin with. This has the effect of making all of the other EXCEPTION_REGISTRATION fields accessible as negative displacements from the frame pointer. For example, the trylevel field is at [EBP-04], the scopetable pointer is at [EBP-08], and so on.

这样 EXCEPTION_REGISTRATION 的各个成员可以通过 EBP 加个偏移访问,例如 trylevel [EBP-04] scopetable 指针在 [EBP-08]

 

Immediately below its extended EXCEPTION_REGISTRATION structure, Visual C++ pushes two additional values. In the DWORD immediately below, it reserves space for a pointer to an EXCEPTION_POINTERS structure (a standard Win32 structure). This is the pointer returned when you call the GetExceptionInformation API. While the SDK documentation implies that GetExceptionInformation is a standard Win32 API, the truth is that GetExceptionInformation is a compiler-intrinsic function. When you call the function, Visual C++ generates the following:

紧接着 EXCEPTION_REGISTRATION 结构, VC 插入了两个额外的值,其紧接着的大小为 DWORD 的空间预留了一个指针空间,可以用来指向一个 EXCEPTION_POINTERS 结构。当调用 GetExceptionInformation API 时,它其实会返回这个指针:

MOV EAX,DWORD PTR [EBP-14]

 

Just as GetExceptionInformation is a compiler intrinsic function, so is the related function GetExceptionCode. GetExceptionCode just reaches in and returns the value of a field from one of the data structures that GetExceptionInformation returns. I'll leave it as an exercise for the reader to figure out exactly what's going on when Visual C++ generates the following instructions for GetExceptionCode:

GetExceptionInformation GetExceptionCode 都是编译器指令函数。 GetExceptionCode 的生成代码如下:

MOV EAX,DWORD PTR [EBP-14] MOV EAX,DWORD PTR [EAX] MOV EAX,DWORD PTR [EAX]

Returning to the extended EXCEPTION_REGISTRATION structure, 8 bytes before the start of the structure, Visual C++ reserves a DWORD to hold the final stack pointer (ESP) after all the prologue code has executed. This DWORD is the normal value of the ESP register as the function executes (except of course when parameters are being pushed in preparation for calling another function).

另外在 EXCEPTION_REGISTRATION 结构底 8 byte VC 预留了 DWORD 大小空间保存 ESP

 

If it seems like I've dumped a ton of information onto you, I have. Before moving on, let's pause for just a moment and review the standard exception frame that Visual C++ generates for a function that uses structured exception handling:

好了,最终 VC 生成的扩展异常帧结构如下:

EBP-00 _ebp EBP-04 trylevel EBP-08 scopetable pointer EBP-0C handler function address EBP-10 previous EXCEPTION_REGISTRATION EBP-14 GetExceptionPointers EBP-18 Standard ESP in frame

 

From the operating system's point of view, the only fields that exist are the two fields that make up a raw EXCEPTION_REGISTRATION: the prev pointer at [EBP-10] and the handler function pointer at [EBP-0Ch]. Everything else in the frame is specific to the Visual C++ implementation. With this in mind, let's look at the Visual C++ runtime library routine that embodies compiler level SEH, __except_handler3.

从操作系统角度看来,只有原始 EXCEPTION_REGISTRATION 的两个成员:在 [EBP-10] 处的前一个节点指针,在 [EBP-0Ch] 的回调函数。其他数据都是由 VC 运行库加入的。

 

 

__except_handler3 and the scopetable

While I'd dearly love to point you to the Visual C++ runtime library sources and have you check out the __except_handler3 function yourself, I can't. It's conspicuously absent. In its place, you'll have to make due with some pseudocode for __except_handler3 that I cobbled together (see Figure 9 ).

由于 __except_handler3 的源代码无法获得,这里给出了伪代码:

 

 

While __except_handler3 looks like a lot of code, remember that it's just an exception callback like I described at the beginning of this article. It takes the identical four parameters as my homegrown exception callbacks in MYSEH.EXE and MYSEH2.EXE. At the topmost level, __except_handler3 is split into two parts by an if statement. This is because the function can be called twice, once normally and once during the unwind phase. The larger portion of the function is devoted to the non-unwinding callback.

从上层看, __except_handler3 通过 if 语句划分成了两部分,分别代表他被两次调用的访问过程。一次判断是否处理异常,一次进行清理工作。

 

The beginning of this code first creates an EXCEPTION_POINTERS structure on the stack, initializing it with two of the __except_handler3 parameters. The address of this structure, which I've called exceptPtrs in the pseudocode, is placed at [EBP-14]. This initializes the pointer that the GetExceptionInformation and GetExceptionCode functions use.

在开头创建并初始化了一个 EXCEPTION_POINTERS 结构,它位于 [EBP-14] ,可用于 GetExceptionInformation GetExceptionCode 作为返回地址。

 

typedef struct _EXCEPTION_POINTERS {

    PEXCEPTION_RECORD ExceptionRecord;

    PCONTEXT ContextRecord;

} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

 

typedef struct _EXCEPTION_RECORD {

    DWORD    ExceptionCode;

    DWORD ExceptionFlags;

    struct _EXCEPTION_RECORD *ExceptionRecord;

    PVOID ExceptionAddress;

    DWORD NumberParameters;

    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD;

 

Next, __except_handler3 retrieves the current trylevel from the EXCEPTION_REGISTRATION frame (at [EBP-04]). The trylevel variable acts as an index into the scopetable array, which allows a single EXCEPTION_REGISTRATION to be used for multiple _try blocks within a function, as well as nested _try blocks. Each scopetable entry looks like this:

接下来 __except_handler3 获取当前的 trylevel ([EBP-04]) ,他用来做 scopetable 数组的索引。

每个 scopetable 元素定义如下:

 typedef struct _SCOPETABLE

 {

     DWORD       previousTryLevel;

     DWORD       lpfnFilter

     DWORD       lpfnHandler

 } SCOPETABLE, *PSCOPETABLE;

 

The second and third parameters in a SCOPETABLE are easy to understand. They're the addresses of your filter-expression and the corresponding _except block code. The previous tryLevel field is bit trickier. In a nutshell, it's for nested try blocks. The important point here is that there's one SCOPETABLE entry for each _try block in a function.

第二和第三个元素很好理解,第一个元素 previousTryLevel 指的是在嵌套 __try 中的外层 __try 块。

 

As I mentioned earlier, the current trylevel specifies the scopetable array entry to be used. This, in turn, specifies the filter-expression and _except block addresses. Now, consider a scenario with a _try block nested within another _try block. If the inner _try block's filter-expression doesn't handle the exception, the outer _try block's filter-expression must get a crack at it. How does __except_handler3 know which SCOPETABLE entry corresponds to the outer _try block? Its index is given by the previousTryLevel field in a SCOPETABLE entry. Using this scheme, you can create arbitrarily nested _try blocks. The previousTryLevel field acts as a node in a linked list of possible exception handlers within the function. The end of the list is indicated by a trylevel of 0xFFFFFFFF.

通过 previousTryLevel ,编译器可以找到外层的 __try 块,这样一个函数中可以随意嵌套 __try 块。最外围的 __try 块的 previousTryLevel 值为 0xFFFFFFFF.

 

Returning to the __except_handler3 code, after it retrieves the current trylevel the code points to the corresponding SCOPETABLE entry and calls the filter- expression code. If the filter-expression returns EXCEPTION_CONTINUE_SEARCH, __except_handler3 moves on to the next SCOPETABLE entry, which is specified in the previousTryLevel field. If no handler is found by traversing the list, __except_handler3 returns DISPOSITION_CONTINUE_SEARCH, which causes the system to move on to the next EXCEPTION_REGISTRATION frame.

函数首先获取当前的 SCOPETABLE 元素,调用他的过滤表达式 ( lpfnFilter ) ,如果返回 EXCEPTION_CONTINUE_SEARCH ,表示不处理,于是 __except_handler3 移到下一个 SCOPETABLE 元素 (由 previousTryLevel 指定 )。如果遍历链表还找不到能处理的表达式, __except_handler3 返回 DISPOSITION_CONTINUE_SEARCH ,让系统移到下一个 EXCEPTION_REGISTRATION 块。

 

If the filter-expression returns EXCEPTION_EXECUTE_HANDLER, it means that the exception should be handled by the corresponding _except block code. This means that any previous EXCEPTION_REGISTRATION frames have to be removed from the list and the _except block needs to be executed. The first of these chores is handled by calling __global_unwind2, which I'll describe later on. After some other intervening cleanup code that I'll ignore for the moment, execution leaves __except_handler3 and goes to the _except block. What's strange is that control never returns from the _except block, even though __except_handler3 makes a CALL to it.

当过滤表达式返回 EXCEPTION_EXECUTE_HANDLER 时,代表他能处理这个异常,首先调用 __global_unwind2 清除之前的 EXCEPTION_REGISTRATION 帧(并进行清理工作),再调用 __local_unwind2 处理当前帧内之前的 SCOPETABLE 块。最后调用当前的 __except ( lpfnHandler 函数 ) 。要注意的是,函数不会再从 lpfnHandler 返回。

 

How is the current trylevel set? This is handled implicitly by the compiler, which performs on-the-fly modifications of the trylevel field in the extended EXCEPTION_REGISTRATION structure. If you examine the assembler code generated for a function that uses SEH, you'll see code that modifies the current trylevel at [EBP-04] at different points in the function's code.

Trylevel 是如何被设置的呢?它是由编译器隐含处理,你可以查看你自己函数的汇编代码,可以看到很多修改 [EBP-4] 的地方 ( 比如在 __try 块前后 ) ,这就是修改当前 Trylevel

 

How does __except_handler3 make a CALL to the _except block code, yet control never returns? Since a CALL instruction pushes a return address onto the stack, you'd think this would mess up the stack. If you examine the generated code for an _except block, you'll find that the first thing it does is load the ESP register from the DWORD that's 8 bytes below the EXCEPTION_REGISTRATION structure. As part of its prologue code, the function saves the ESP value away so that an _except block can retrieve it later.

为什么 __except_handler3 调用了 __except 块的代码却不返回,函数调用时会往栈中插入返回地址,这样会不会弄乱栈呢。如果你检查 _except 块生成的汇编代码,你会发现他做的第一件事就是从 EXCEPTION_REGISTRATION 结构之下的 8 位处载入之前保存的 ESP 值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值