Understanding Windows Structured Exception Handling

翻译:

Understanding Windows Structured Exception Handling

Part 1 – The Basics

Introduction

什么是 exception?MSDN 给出了以下解释:

Exception(异常)是一个事件,它发生在程序的执行期间,要求在正常的控制流之外执行代码。存在两种异常:硬件异常和软件异常。硬件异常由 CPU 引发,可能由几个指令的执行引发,例如除0或者尝试访问非法内存地址。软件异常由应用程序或操作系统引发。例如,系统可以抛出异常当参数不合理时。

由于它需要代码“在正常控制流之外”执行,异常相关的事件要特殊处理,它通常意味着一些错误发生。

Exception Handling 就是“在正常控制流之外”执行代码的行为。

然而,似乎很多 Exception Handling。例如 C++ Exception Handling,Structured Exception Handling(SEH),Visual C++ 语言对于 SEH 的扩展实现, C/C++ 之间 Exception Handling 和 SEH 的不兼容,一些编译器不支持 SEH。

在这个系列中,我希望尽可能分清楚这些内容。我的焦点是 SEH,我将给出一些关于内部原理的代码。

我假设读者对于 SEH 有一定程度的熟悉,如果没有我建议从 MSDN documentation.开始。

尽管不打算调试,但调试 SEH 有很大价值。

推荐两篇文章:

这些参考对我的研究帮助很大。Matt Pietrek 的文章虽然已经过去将近 20 年,依然包含了许多基础的 SEH 和内部原理的信息。然而,其中的一些代码无效了。因为这些代码是 Windows NT 4.0 中使用的。

这篇文章的代码可以在GitHub上找到。

What is Structured Exception Handling?

SEH 可以看作一个 Windows 系统中的通用的错误处理机制。这是一个系统的 feature,并不绑定某种语言。它是 Windows Application Binary Interface(ABI)的一部分,是一种应用程序和操作系统间的约定。

它是 C++ Exception Handling 的别名吗?不是。C++ Exception Handling 是一个 ISO 标准,影响所有的 C++ 编译器。

C++ Exception Handling 与 SEH 不兼容。但是,Visual C++ 编译器使用 SEH 来实现它的 C++ Exception Handling 。

SEH 可以用在其它语言(例如 C#)中吗?为了回答这个问题,需要知道 SEH 是一个 OS feature,并不绑定到语言或者编译器。C# 不包含 SEH 的内容,但是 C# 编译器使用 SEH 作为 C# Exception Handling 的底层实现。

因此,使用__try/__except/__finally会使代码失去可移植性。

在余下的内容中,我希望可以讲清楚 SEH。

The Basics

我们来利用 C/C++ 学习 SEH,首先看以下的 C/C++ 代码:

1
2
3
4
5
6
7
8
9
int g_iDividend = 1000;
int g_iDivisor = 0;

int TestDivisionByZero()
{
int iValue = g_iDividend / g_iDivisor;

return iValue;
}

代码执行时,这个除法会抛出一个 Divide by Zero 异常,显示以下对话框:

img

如果我们想处理这个异常并从中恢复,我们可以:当除 0 发生时,我们想把除数变为 1。

下面我们定义了新函数TestDivisionByZeroTryExcept,进行相同的除法,担当错误发生时使用__try/__except块从错误中恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int TestDivisionByZeroFilter()
{
g_iDivisor = 1;

return EXCEPTION_CONTINUE_EXECUTION;
}

int TestDivisionByZeroTryExcept()
{
int iValue = 0;

__try
{
iValue = g_iDividend / g_iDivisor;
}
__except(TestDivisionByZeroFilter())
{
}

return iValue;
}
  • 除法代码放在__try块中,使它成为了一块受保护的代码。
  • 当除法执行时,Division by Zero 异常抛出。
  • 由于除法代码受保护,异常将被__except块捕获。
  • __except块需要 Exception Filter 来处理异常。
  • 在我们的例子中,Exception Filter 是TestDivisionByZeroFilter函数。
  • Exception Filter 的作用是告诉 SEH 引擎在异常结束后如何操作,它返回以下结果之一:
    • EXCEPTION_EXECUTE_HANDLER,执行 __except块中的代码
    • EXCEPTION_CONTINUE_SEARCH,继续查找 handler
    • EXCEPTION_CONTINUE_EXECUTION,重新执行发生异常的代码(例子中是除法)
  • TestDivisionByZeroFilter 返回了 EXCEPTION_CONTINUE_EXECUTION,但是在此之前它把除数修改为了 1
  • 因此当重新执行时,除法成功执行且结果为 100

接下来看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int g_iDividend = 1000;
int g_iDivisor = 0;

EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyDivisionByZero01ExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

g_iDivisor = 1;

return ExceptionContinueExecution;
}

int TestDivisionByZero01SEH()
{
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyDivisionByZero01ExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

int iValue = g_iDividend / g_iDivisor;

TIB->ExceptionList = TIB->ExceptionList->Next;

return iValue;
}

以上代码中,我们定义了新函数TestDivisionByZero01SEH,它的功能与之前的TestDivisionByZeroTryExcept一致,但是用了通常 C++ 开发者不常用的语法。

TestDivisionByZero01SEH包含了底层的代码,这些代码放在受保护的代码(除法)之前。Exception Handler 是 MyDivisionByZero01ExceptionRoutine

MyDivisionByZero01ExceptionRoutine将除数设置为 1 然会返回 ExceptionContinueExecution,要求 SEH 重新执行除法,这次能正常完成。

MyDivisionByZero01ExceptionRoutineTestDivisionByZeroFilter相似,它们的功能是一致的。

为了充分了解发生了什么,我们需要注意 SEH 是基于线程工作的。也就是说,每个线程包含自己的 SEH 机制。为了理解这一点,我们需要了解一些关于 Thread Information Block(TIB)的内容。

每个系统中的线程都有一个 TIB。TIB 是一个数据结构,包含了线程的信息,包括当前线程的 SEH Handlers 的信息。注意 Structured Exception Handlers 是复数,因为每个线程都有不止一个 SEH Handlert,尽管只有一个 Handler 在异常发生时起作用,我们将稍后看到这一点。

The Thread Information Block(TIB)

TIB 是一个很复杂的结构,本文中不会详细讨论,我们只关注和 SEH 相关的部分。

以下结构是从 winnt.h 中取得的 TIB 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
#if defined(_MSC_EXTENSIONS)
union {
PVOID FiberData;
DWORD Version;
};
#else
PVOID FiberData;
#endif
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
typedef NT_TIB *PNT_TIB;

从上面可以看到,TIB 的第一个成员是指向_EXCEPTION_REGISTRATION_RECORD的指针。这是当前线程的 Exception Handler List。

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

每个_EXCEPTION_REGISTRATION_RECORD结构保存一个指向下一个结构的指针,也就是一个 Exception Handler Routine 的链表。在 The Basics 中我们提到过不只有一个 Handler。

Exception Handler 是一个用户定义的函数,签名如下:

1
2
3
4
5
6
7
8
EXCEPTION_DISPOSITION
NTAPI
EXCEPTION_ROUTINE (
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);

当异常发生时,OS 查找发生异常的线程的 TIB,获取_EXCEPTION_REGISTRATION_RECORD结构的指针。然后遍历链表中的EXCEPTION_ROUTIN确定其中有没有能够处理当前异常的。

每个 Handler 通过EXCEPTION_DISPOSITON类型的返回值表明它希望处理的异常。

1
2
3
4
5
6
7
8
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

对于本文中的 demo 来说,我们只考虑 ExceptionContinueSearch 和 ExceptionContinueExecution 返回值。另外两个在更复杂的情况中使用。

当 handler 返回 ExceptionContinueSearch,它表示 handler 不会对当前的异常进行恢复。OS 将查找_EXCEPTION_REGISTRATION_RECORDnext来执行下一个 handler。

如果所有的 Handler 都不能处理当前的异常,OS 将调用当前的 Unhandled Exception Filter。开发者也可以调用UnandledExceptionFilterAPI 来安装一个自定义的 Filter。然而,注意 Unhandled Exception Filter 不是 SEH 链表的一部分,而是当没有 Handler 时会指向的函数。

默认的 Unhandled Exception Filter 是 __scrt_unhandled_exception_filter,它是默认的 C/C++ 运行时库的 Handler,在程序开始时注册。在 Visual Studio 2019 的..\crt\src\vcruntime\utility_desktop.cpp源代码中可以看到这个 filter。

Back to the Basic Example

我们再看TestDivisionByZero01SEH函数,这个函数演示了从除 0 异常中恢复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyDivisionByZero01ExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

g_iDivisor = 1;

return ExceptionContinueExecution;
}

int TestDivisionByZero01SEH()
{
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyDivisionByZero01ExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

int iValue = g_iDividend / g_iDivisor;

TIB->ExceptionList = TIB->ExceptionList->Next;

return iValue;
}

首先调用 NtCurrentTeb 获取指向_TEB结构的指针。_TEB结构的开始就是一个 TIB 结构,因此我们可以直接将这个指针转换为指向NT_TIB的指针。

然后我们将MyDivisionByZero01ExceptionRoutine作为 SEH Exception Handler 注册。通过定义一个EXCEPTION_REGISTRATION_RECORD结构并且将MyDivisionByZero01ExceptionRoutine函数作为EXCEPTIONROUTINE进行,Registration 的 Next 是原来的 TIB ExceptionList。这样一来,我们就将 TIB ExceptionList 指向了 Registration。

注意这不代表MyDivisionByZero01ExceptionRoutine有比链表中靠下的其它 handler 更高的优先级,它只比处理相同异常的链表中靠后的 Handler 更优先。

我们接下来查看MyDivisionByZero01ExceptionRoutine函数。这个函数是发生异常后最先被调用的 Handler。我们希望它处理除 0 异常。因此当TestDivisionByZero01SEH运行时将被调用。它首先展示一些关于当前异常的信息(通过宏DISPLAY_EXCEPTION_INFO),然后后将除数改为 1 并返回 ExceptionContinueExecution。然后重新执行发生异常的除法代码,这一次能够正常执行。

但是还没有结束,接下来的一行

1
TIB->ExceptionList = TIB->ExceptionList->Next;

它把MyDivisionByZero01ExceptionRoutine从 SEH Handler 中移除。这是必须的,因为EXCEPTIONREGISTRATION_RECORD结构定义在了TestDivisionByZero01SEH的栈上,当函数返回后这个结构会被释放掉,导致 TIB ExceptionList 中保存了一个已经被释放的指针。

那么,为什么我们不使用一个全局的EXCEPTION_REGISTRATION_RECORD呢?这没有用。Windows 不会调用自定义的 exception handler。我给出了一些代码演示这个问题:TestSEHGlobalExRegRec

这个函数接收一个指向EXCEPTION_REGISTRATION_RECORD指针的参数,如果它定义为全局变量,Exception HandlerMyTestExceptionRoutine在异常发生时不会被调用。

我们展示了如何定义 SEH Handlers 并且插入到一个函数中。接下来的两节,我们将给出一些高级的演示,展示 SEH 可以做些什么。

Accessing the Arguments of a Faulting Function

之前的示例代码中,我们看到了如何在 Exception Handler 中恢复除 0 的错误。这很容易做到因为g_iDivisor是一个全局变量。如果除数是一个传入这个函数的参数呢?

这可以通过使用 Exception Handler 的 _CONTEXT参数做到。TestDivisionByZero02SEHMyDivisionByZero02ExceptionRoutine演示了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyDivisionByZero02ExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

int* pDividendParam = (int*)((pContextRecord->Ebp) + 8);
int* pDivisorParam = (int*)((pContextRecord->Ebp) + 12);

printf("Dividend : [%d]. Divisor : [%d]\r\n",
*pDividendParam,
*pDivisorParam);

*pDivisorParam = 1;

return ExceptionContinueExecution;
}

int TestDivisionByZero02SEH(int iDividend, int iDivisor)
{
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyDivisionByZero02ExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

int iValue = iDividend / iDivisor;

TIB->ExceptionList = TIB->ExceptionList->Next;

return iValue;
}

MyDivisionByZero02ExceptionRoutine的第三个参数是一个_CONTEXT的指针。这个结构包含了处理器相关的数据,包括发生异常时寄存器的值。使用_CONTEXT结构,我们可以访问发生异常时 EBP 的值,然后通过 EBP 访问TestDivisionByZero02SEH的参数。

EBP 是栈帧 Base Pointer。对于 x86 架构来说,函数的第一个参数总是在比 EBP 高 8字节处,如下图所示:

img

第二个参数在高 12 字节处,以此类推,每个参数比前一个高 4 字节。以下代码访问了两个参数的内存:

1
2
int* pDividendParam = (int*)((pContextRecord->Ebp) + 8);
int* pDivisorParam = (int*)((pContextRecord->Ebp) + 12);

PDivisorParam是指向iDivisor的指针,修改它

1
*pDivisorParam = 1;

MyDivisionByZero02ExceptionRoutine返回 ExceptionContinueExecution,重新执行除法。

这个例子演示了在 SEH Handler 中使用_CONTEXT结构。

Analyzing a Buffer Overflow Situation

缓冲区溢出当局部变量被赋予一个超过它大小的值时发生,例如以下情况:

1
2
char Buffer[8] = { 0 };
strcpy(Buffer, "AAAAAAAAAAAAAAAA");

Buffer 是一个 8 字节大小的缓冲区,strcpy 将 16 个 A 存放到 Buffer 中导致缓冲区溢出。超过 Buffer 大小的 A 将溢出到 Buffer 高字节的空间中。这种情况发生在函数定义的 Char Buffer 中是非常糟糕的。

在上节中提到的 EBP 偏移的图片中,我们可以发现函数的返回值在比 EBP 高 4 字节的位置。如果缓冲区溢出覆盖了返回地址,函数结束时将会崩溃,因为返回地址被覆盖成了不合理的值(AAAA)。

通常缓冲区溢出是不可恢复的。但是可以使用安全函数例如strcpy_s等来防止这种情况发生。

在示例中,我演示了触发缓冲区溢出并通过 SEH 报告的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyBufferOverflowExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

void* pBufferAddress = (void*)(pExceptionRecord->ExceptionInformation[0]);
size_t stSize = pExceptionRecord->ExceptionInformation[1];

printf("Overflowed Buffer : [0x%p]. Size : [%d]\r\n",
pBufferAddress,
stSize);

char* pBuffer = new char[stSize + 1];
memset(pBuffer, 0, stSize + 1);
memcpy(pBuffer, pBufferAddress, stSize);

printf("Overflowed Buffer Contents : [%s]\r\n", pBuffer);

free(pBuffer);
pBuffer = NULL;

printf("iStackGuard Value : [0x%08X].\r\n",
pExceptionRecord->ExceptionInformation[2]);

int* pReturnAddress = (int*)(pExceptionRecord->ExceptionInformation[3]);

printf("Return Address : [0x%08X].\r\n", *pReturnAddress);

return ExceptionContinueSearch;
}

void TestBufferOverflowSEH(const char* pString)
{
int iStackGuard = 0;
char szCopy[8];

NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();
EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyBufferOverflowExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

strcpy(szCopy, pString);

if (iStackGuard != 0)
{
ULONG_PTR args[4] = { 0 };
args[0] = (ULONG_PTR)(szCopy);
args[1] = 8;
args[2] = iStackGuard;
args[3] = (ULONG_PTR)(&iStackGuard + 8);

RaiseException(EXCEPTION_BUFFER_OVERFLOW, EXCEPTION_NONCONTINUABLE, 4, args);
}

TIB->ExceptionList = TIB->ExceptionList->Next;
}

TestBufferOverflowSEH中,我们定义了一个局部变量szCopy,大小是 8 字节。在szCopy上是一个局部整数变量iStackGuard用于检测szCopy是否发生了缓冲区溢出。

基本思想很简单,因为iStackGuardszCopy之前定义,在栈上它是szCopy高字节方向最近的位置。如果szCopy溢出,iStackGuard的值将会改变。

strcpy执行之后,我们检查iStackGuard的值。如果不是 0,则发生了缓冲区溢出。我们使用自定义的 Exception Code(_EXCEPTION_BUFFER_OVERFLOW,0xE0000001)和缓冲区溢出的信息调用RaiseException。额外的信息是一个指向szCopy的指针,它的大小,iStackGuard的值和 return address 的值(可能已经损坏)。

这个信息通过RaiseException API 的第三个和第四个参数传递给 Exception Handler。这些信息保存在_EXCEPTION_RECORD 的 ExceptionInformation 数组中。

MyBufferOverflowExceptionRoutine被 SEH 调用时,会展示这些信息:

img

可以看到,iStackGuard变为了0x41414141,0x41 是 A 的 ASCII 编码,返回值同样是0x41414141

示例代码演示了使用自定义的 Exception Code 和额外信息触发异常。

Summary

这是本系列的第一篇文章,研究了 Structured Exception Handling 的内部原理。在本文中,我们简单了解了如何使用自定义的 SEH Handler。希望示例代码给读者提供了更多想法。

下一步分钟,我们将深入了解 SEH 和它的内部原理。

Notes on Usage of the Sample Code

\1. The sample source code file has been compiled on Visual Studio 2019.

\2. I assume that the Reader will be using a version of Visual Studio.

\3. To ensure simplicity and to focus on SEH, I have set the following compiler settings for the TestSEH01 project :

3.1 The project is a Win32 console program.

3.2 Optimization is turned off. This will ensure that function proglogs and epilogs (i.e. the use of the ebp register as a base pointer) are put in place by the compiler. This is especially important for the TestDivisionByZero02SEH() and MyDivisionByZero02ExceptionRoutine() sample code.

3.3 SAFESEH Setting. The following has been set :

  • Linker | Advanced Properties : set Image Has Safe Exception Handlers to : No (/SAFESEH:NO)

It is important to turn off the SAFESEH setting otherwise the compiled code may not run correctly.

3.4 _CRT_SECURE_NO_WARNINGS Setting.

  • C/C++ | Preprocessor Properties | Preprocessor Definitions settings : add _CRT_SECURE_NO_WARNINGS.

This is necessary because the sample code uses strcpy() (a non-safe character buffer management function) to demonstrate Buffer Overflow.

\4. The full source codes for this article can be found in GitHub.

References

Structured Exception Handling

What Is an Exception?

Fun with low level SEH

A Crash Course on the Depths of Win32™ Structured Exception Handling by Matt Pietrek

CppCon 2018: James McNellis “Unwinding the Stack: Exploring How C++ Exceptions Work on Windows”

Exception Handler not called in C

Part 2 – Digging Deeper

Introduction

在 Part 1 中,我们学习了 Windows SEH 的基础内容,我们知道了如何在 C/C++ 不使用__try/__except建立 SEH Frame,如何使用 SEH 修复错误。

在 Part 2,我们将继续深入学习 SEH。我们将学习 Visual C++ 编译器如何为__try/__except/__finally生成代码,并且知道这些代码如何影响 Exception handling(_except块的执行)和 Termination Handling(__finally块)。

在这个过程中,我尝试提供 SEH 相关的清晰的定义。读者可能听过这些相关的术语,但是可能它们没有被解释清楚。

Producing Assembly Language Code

本文中我们将学习许多汇编代码。可以通过在 Visual C++ 编译器中指定以下选项来生成汇编代码:C/C++ -> Output Files -> Assembler Output: Assembly With Source Code(/FAs)。开启了这个选项后,在每次成功编译后都会生成汇编代码源文件TestSEH02.asm

SEH Frames

SEH Frame 是底层的代码约定,包含在函数中,它启用了 Windows Exception。它也启用了在函数中自动调用 Termination Handlers。

Visual C/C++ 编译器为一个函数建立的 SEH Frame 包含__try/__except和/或__try/__finally块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void TestSEH()
{
__try
{
__try
{
int* pInt = NULL;
*pInt = 100;
}
__finally
{
printf("__finally @ TestSEH()\r\n");
}
}
__except (FilterFunction())
{
printf("__except @ TestSEH()\r\n");
}
}

在以上 TestSEH 函数中,__try/__except/__finally块会使编译器生成底层建立 SEH Frame 的代码。SEH Frame 包含以下内容:

  • 将 SEH Exception Handler Function 添加到当前 TIB 的 Excption Handler 链表头的代码
  • 将一些例如 Scope Table 插入 SEH Frame 的代码
  • 在代码执行中更新例如 Try Level 的代码

Adding a SEH Exception Handler Function

检测到__try/__except/__finally块时,编译器生成在运行时获取 TIB 的代码。在 Part 1 中我们使用如下方式获取 TIB:

1
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

NtCurrentTeb 是一个宏,展开如下:

1
__inline struct _TEB * NtCurrentTeb( void ) { return (struct _TEB *) (ULONG_PTR) __readfsdword (PcTeb); }

它使用了 intrinsic 函数读取了 FS:[0x18] 处的自引用,见 Contents of the TIB on Windows。TEB 第一个成员是 TIB,因此可以将指针转换为 NT_TIB。

在 Part 1 中我们增加了新的 SEH Handler,通过更新 TIB 的 ExceptionList 来实现:

1
2
3
4
EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyDivisionByZero02ExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

以上代码中我们定义了EXCEPTION_REGISTRATION_RECORD,用更新后的信息填充它。

现在,当生成更新 SEH Handler 的代码时,编译器并不产生类似于上面的代码。它使用一种优化后的方法,下面展示了 TestSEH 开始处的汇编代码:

1
2
3
4
5
6
7
8
9
10
void TestSEH()
{
00AD1190 55 push ebp
00AD1191 8B EC mov ebp,esp
00AD1193 6A FF push 0FFFFFFFFh
00AD1195 68 30 5F AD 00 push 0AD5F30h
00AD119A 68 26 3F AD 00 push offset __except_handler3 (0AD3F26h)
00AD119F 64 A1 00 00 00 00 mov eax,dword ptr fs:[00000000h]
00AD11A5 50 push eax
00AD11A6 64 89 25 00 00 00 00 mov dword ptr fs:[0],esp

下面是 asm 文件输出产生的同等的代码:

1
2
3
4
5
6
1	push	-1
2 push OFFSET __sehtable$?TestSEH@@YAXXZ
3 push OFFSET __except_handler3
4 mov eax, DWORD PTR fs:0
5 push eax
6 mov DWORD PTR fs:0, esp

我们可以看看这段代码如何更新 SEH Handler。TestSEH的第三行,我们使用标准的 SEH Exception Handler,编译器选择使用__except_handler3(),注意代码的顺序:

  • 第三行,指向_except_handler3的指针被压栈
  • 第四行,fs:0 的内容被存到 eax 中。这是为了准备将 fs:0 的内容压栈。
  • 第五行,将 eax 的内容压栈,现在栈顶的内容是 fs:0
  • 第六行,esp 寄存器的内容被放到 fs:0 的位置,下面将解释这个过程。

不管你信不信(反正我是信了),这段代码完成了相同的功能。为了理解这段代码如何工作的,需要注意两件事:

  • fs:0 处内存的内容
  • EXCEPTION_REGISTRATION_RECORD结构体的内容

首先注意 FS:0 是由 Windows 使用的,包含了 SEH Handler 链表;其次,EXCEPTION_REGISTRATION_RECORD结构如下:

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

它只有两个成员,一个指向下一个 RECORD,另一个是当前的 SEH Handler。

回到汇编,当第一行到第五行的代码执行后,栈上的内存布局是这样的:

img

我们后面会讨论__sehtable$?TestSEH@YZXXZ符号和-1,但是我们先观察栈的结构。它包含 FS:0 和_except_handler3。注意这些和EXCEPTION_REGISTRATION_RECORD的结构相同,而且因为 FS:0 刚刚被压栈, esp 寄存器正好指向这个地址:

img

因此,当我们执行

1
mov	DWORD PTR fs:0, esp

栈上的情况是这样的:

img

这就是更新 SEH Handler 的方法。

The SEH Scope Table and The TryLevel

每个 SEH Frame 都有一个 ScopeTable。Scope Table 是一个 Scope Table Entries 的数组,结构如下:

1
2
3
4
5
6
7
8
9
10
typedef struct _EH4_SCOPETABLE_RECORD
{
ULONG EnclosingLevel;
PEXCEPTION_FILTER_X86 FilterFunc;
union
{
PEXCEPTION_HANDLER_X86 HandlerAddress;
PTERMINATION_HANDLER_X86 FinallyFunc;
} u;
} EH4_SCOPETABLE_RECORD, *PEH4_SCOPETABLE_RECORD;

以上的EH4_SCOPETABLE_RECORD结构在chandler4.c中。在本文中,我定义了一个等价的结构:

1
2
3
4
5
6
7
8
typedef void (FAR WINAPI* VOIDPROC)();

struct SCOPETABLE_ENTRY
{
int EnclosingLevel;
FARPROC lpfnFilter;
VOIDPROC lpfnHandler;
};

在 C/C++ 函数中,如果包含了__try/__except/__finally块,编译器会将函数分为几个 Scope Entries,,同时也会在内部生成一个整数也就是 TryLevel。

每个 Scope Entry 与一个 try 块对应。函数中只有一个 TryLevel,这个值随着代码进入和退出__try块变化。例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void DemoSEHScoping()
{
printf("TryLevel == -1. No associated ScopeEntry.\r\n");

__try
{
printf("TryLevel == 0. ScopeEntry[0].\r\n");

__try
{
printf("TryLevel == 1. ScopeEntry[1].\r\n");

__try
{
printf("TryLevel == 2. ScopeEntry[2].\r\n");

int* pInt = NULL;
*pInt = 100;
}
__finally
{
printf("__finally for TryLevel == 2. ScopeEntry[2].\r\n");
}

}
__finally
{
printf("__finally for TryLevel == 1. ScopeEntry[1].\r\n");
}
}
__except (FilterFunction())
{
printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");
}

printf("TryLevel == -1. No associated ScopeEntry.\r\n");

__try
{
printf("TryLevel == 3. ScopeEntry[3].\r\n");

__try
{
printf("TryLevel == 4. ScopeEntry[4].\r\n");

int* pInt = NULL;
*pInt = 100;
}
__finally
{
printf("__finally for TryLevel == 4. ScopeEntry[4].\r\n");
}
}
__except (FilterFunction())
{
printf("__except for TryLevel == 3. ScopeEntry[3].\r\n");
}

printf("TryLevel == -1. No associated ScopeEntry.\r\n");
}

在以上的函数中包含 5 个 Scope Entries,每个都与一个__try块对应。

注意 TryLevel 从 -1 开始,在进入和退出_try时变化。如果不在任何_try块中,TryLevel 为 -1。TryLevel 不必每次增加 1 或减少 1。事实上,TryLevel 用于表明当前活动的 Scope Entry(对应当前的 __try)块。当处理 SEH Filters 和 Exceptions 时非常重要。

Scope Entry Table 和 TryLevel 在 SEH 中如何起作用?为了说明这点,我们需要了解 Exception Handler 的参数 EstablisherFrame。

The EstablisherFrame

回忆以下我们在 Part 1 中的 Handler MyDivisionByZero01ExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyDivisionByZero01ExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

g_iDivisor = 1;

return ExceptionContinueExecution;
}

第二个参数 EstablisherFrame 事实上是一个指向EXCEPTION_REGISTRATION_RECORD的指针。然而,这个指针实际上是一个更大的结构体EH4_EXCEPTION_REGISTRATION_RECORD的一部分。

1
2
3
4
5
6
7
8
typedef struct _EH4_EXCEPTION_REGISTRATION_RECORD
{
PVOID SavedESP;
PEXCEPTION_POINTERS ExceptionPointers;
EXCEPTION_REGISTRATION_RECORD SubRecord;
UINT_PTR EncodedScopeTable;
ULONG TryLevel;
} EH4_EXCEPTION_REGISTRATION_RECORD, *PEH4_EXCEPTION_REGISTRATION_RECORD;

这个结构在 chandler4.c 中定义。EstablisherFrame 指向 SubRecord 成员。因此每个 SEH Handler 都可以访问 ScopeTable 和当前的 TryLevel。这可以在 chandler4.c 的 _except_handler4中看到:

1
2
3
4
5
6
7
8
9
//
// We are passed a registration record which is a field offset from the
// start of our true registration record.
//

RegistrationNode =
(PEH4_EXCEPTION_REGISTRATION_RECORD)
( (PCHAR)EstablisherFrame -
FIELD_OFFSET(EH4_EXCEPTION_REGISTRATION_RECORD, SubRecord) );

FIELD_OFFSET宏定义如下:

1
#define FIELD_OFFSET(type, field)    ((LONG)(LONG_PTR)&(((type *)0)->field))

以上代码中,获得的结果时一个指向整个EH4_EXCEPTION_REGISTRATION_RECORD结构的指针。

DemoSEHScoping的汇编代码中,EH4_EXCEPTION_REGISTRATION_RECORD结构在一开始就生成了,何在TestSEH中一样。以下代码时从DemoSEHScoping的汇编中截取的:

1
2
3
4
5
6
7
1	push	-1
2 push OFFSET __sehtable$?DemoSEHScoping@@YAXXZ
3 push OFFSET __except_handler3
4 mov eax, DWORD PTR fs:0
5 push eax
6 mov DWORD PTR fs:0, esp
7 add esp, -16

我们可以看到和 TestSEH 一样的建立 SEH Frame 的过程。__except_handler3函数作为 Handler。这时我们可以看到 -1 和__sehtable$?DemoSEHScoping@@YAXXZ被压栈。这些形成了 TryLevel 和 EncodedScopeTable 的值。

The Scope Table Entries

__sehtable$?DemoSEHScopping@@YAXXZ是一个DemoSEHScoping函数中的 Scope Table,可以看到它的汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__sehtable$?DemoSEHScoping@@YAXXZ DD 0ffffffffH
DD FLAT:$LN36@DemoSEHSco
DD FLAT:$LN10@DemoSEHSco
DD 00H
DD 00H
DD FLAT:$LN35@DemoSEHSco
DD 01H
DD 00H
DD FLAT:$LN34@DemoSEHSco
DD 0ffffffffH
DD FLAT:$LN38@DemoSEHSco
DD FLAT:$LN22@DemoSEHSco
DD 03H
DD 00H
DD FLAT:$LN37@DemoSEHSco

在这里我们可以直接看到DemoSEHScoping的 Scope Table 的内存布局。它可以分成 5 组,每组 3 个 DWORD 值,以下面这组开始:

1
2
3
DD 	0ffffffffH
DD FLAT:$LN36@DemoSEHSco
DD FLAT:$LN10@DemoSEHSco

对照 SCOPETABLE_ENTRY的字段可以知道:

  • EnclosingLevel == -1
  • lpfnFilter == FLAT$LN36@DemoSEHSco
  • lpfnHandler == FLAT:$LN10@DemoSEHSco

这五组SCOPETABLE_ENTRY对应 5 个__try块。回到DemoSEHScoping函数,可以看到第一个__try块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   ...
printf("TryLevel == -1. No associated ScopeEntry.\r\n");

__try
{
printf("TryLevel == 0. ScopeEntry[0].\r\n");
...
...
...
}
__except (FilterFunction())
{
printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");
}

注意虽然它的 TryLevel 是 0,但它的 enclosing try level 是 -1。

然后我们可以看到lpfnFilter__except的 filter function。lpfnHandler__except或者__finally块的代码。

查看 TestSEH02.cpp 文件生成的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$LN36@DemoSEHSco:

; 89 : __except (FilterFunction())

call ?FilterFunction@@YAHXZ ; FilterFunction
$LN11@DemoSEHSco:
$LN31@DemoSEHSco:
ret 0
$LN10@DemoSEHSco:
mov esp, DWORD PTR __$SEHRec$[ebp]

; 90 : {
; 91 : printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");

push OFFSET ??_C@_0CN@NNAANJNA@__except?5for?5TryLevel?5?$DN?$DN?50?4?5Sco@
call _printf
add esp, 4

; 87 : }

$LN36@DemoSEHSco$LN10@DemoSEHSco是指向特定位置的标签,前者指向调用 FilterFunction 的代码,后者指向__except块的开始。这些代码的位置在编译时存储为标签,因为编译时不知道代码的地址。

一些其他值得注意的点:

  • $LN36@DemoSEHSco指向将要返回的代码
  • 因此SCOPETABLE_ENTRYlpfnFilter成员可以作为函数被调用,并能返回到调用者
  • C/C++ 中的 Filter Function 会返回EXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_SEARCHEXCEPTION_CONTINUE_EXECUTION
  • 因此$LN36@DemoSEHSco指向一个 mini function。
  • $LN10@DemoSEHSco指向不会返回的代码
  • 因为__except块在执行结束时应该继续执行下面的代码

我们观察以下SCOPETABLE_ENTRY

1
2
3
DD	00H
DD 00H
DD FLAT:$LN35@DemoSEHSco

我们可以看到 EnclosingLevel 是 0,lpfnFilter 是 0,lpfnHandler 指向$LN35@DemoSEHSco

SCOPETABLE_ENTRY与第二个__try块对应,所以 TryLevel 是 1,但是 enclosing level 是之前的__try块的 TryLevel 也就是 0。lpfnFilter 是 0 表示这个 __try块没有 Filter Function,也就是这是一个__try/__finally块。这里 lpfnHandler 将指向__finally块的开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$LN35@DemoSEHSco:
$LN15@DemoSEHSco:

; 84 : __finally
; 85 : {
; 86 : printf("__finally for TryLevel == 1. ScopeEntry[1].\r\n");

push OFFSET ??_C@_0CO@EMPMPPGM@__finally?5for?5TryLevel?5?$DN?$DN?51?4?5Sc@
call _printf
add esp, 4
$LN14@DemoSEHSco:
$LN30@DemoSEHSco:
ret 0
$LN16@DemoSEHSco:

; 87 : }

在我们结束这一节之前,注意尽管SCOPETABLE_ENTRY数组被定义为全局数组,它也可以定义为局部数组。事实上,如果定义为局部数组会更节省空间,因为SCOPETABLE_ENTRY数组在函数结束后就不再需要了。

下面一节,我们研究 TryLevel 如何影响 SCOPETABLE_ENTRY

The TryLevel

EH4_EXCEPTION_REGISTRATION_RECORD的 TryLevel 跟踪当前正在执行的__try块。我们已经知道在函数开始时,TryLevel 是 -1。再次引用 TestSEH02.asm 的 DemoSEHScoping 函数的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__try
mov DWORD PTR __$SEHRec$[ebp+20], 0
{
printf("TryLevel == 0. ScopeEntry[0].\r\n");

__try
mov DWORD PTR __$SEHRec$[ebp+20], 1
{
printf("TryLevel == 1. ScopeEntry[1].\r\n");

__try
mov DWORD PTR __$SEHRec$[ebp+20], 2
{
printf("TryLevel == 2. ScopeEntry[2].\r\n");
}
mov DWORD PTR __$SEHRec$[ebp+20], 1
__finally
{
printf("__finally for TryLevel == 2. ScopeEntry[2].\r\n");
}
}
mov DWORD PTR __$SEHRec$[ebp+20], 0
__finally
{
printf("__finally for TryLevel == 1. ScopeEntry[1].\r\n");
}
}
mov DWORD PTR __$SEHRec$[ebp+20], -1
__except (FilterFunction())
{
printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");
}

mov DWORD PTR __$SEHRec$[ebp+20], -1

printf("TryLevel == -1. No associated ScopeEntry.\r\n");

__$SEHRec$[ebp+20]结果是[ebp-4]指向了 EH4_EXCEPTION_REGISTRATION_RECORD 的 TryLevel 成员。我们可以看到当代码进入__try块时,TryLevel 增加 1,当退出时,TryLevel 减 1.

Source Codes

GitHub可以找到 Part 2 的代码。注意我已经进行了一些设置来简化生成的汇编代码来帮助学习,这些设置包括:

  • 不优化
  • C/C++ -> Code Generation -> Basic Runtime Checks: Default
  • C/C++ -> Code Generation -> Security Checks: Disable Security Checks(/GS-)

关闭了优化来生成与上面类似的代码,这些代码更容易看懂。使用 Default Basic Runtime Checking 避免生成额外的运行时代码,这些代码可能影响我们阅读生成的汇编。禁用 Security Checks,编译器会使用__except_handler3作为 Handler,否则编译器会使用__except_handler4,这时一个更复杂的 Handler ,包含了一些安全特性,不包括在本文的内容中。

Summary

Part 2 中我们学习了非常底层的 SEH 代码。这可以帮助我们理解当 Structured Exception 出现时会发生什么,Filter Function(如果有)如何执行,__except__finally块如何执行。

References

  1. Win32 Thread Information Block

  2. Microsoft-specific exception handling mechanisms

Part 3 – Under The Hood

Introduction

Part 1 和 Part 2 中,我们学习了 Visual C++ 编译器中,函数中的 SEH 是如何建立的。我们也看到了__try/__except/__finally块如何跟踪和SCOPETABLE_ENTRY数组如何实现它们的执行的意义。

在 Part 3 中,我们将学习异常发生时底层 OS 如何进行 SEH。我们将学习 Viusal C++ 提供的 __except_handler3函数,这时默认的 Handler。我们也会讨论一个成为 Stack Unwinding 的复杂过程。

__except_handler3

以下是 __except_handler3 的一些特性:

  • 这是一个通用的异常处理函数,由 Visual C++ 编译器生成
  • 它不是一个 Windows API
  • 当被调用时,它只和一个函数中的 SEH Frame 交互,即使 __except_handler3 被放置在了多个函数中也是一样
  • 它调用 SEH Frame 中的__except filter 函数并检查返回值
  • 它执行__except__finally中的代码
  • 由于它是一个通用的 Handler,它没有任何关于 Filter 函数或者__except/__finally块的信息

Matt Pietrek 的 article 中给出了__except_handler3的伪代码。然而,我发现 James McNells 的伪代码要好很多。可以在 YouTube video at 26:57,它的 CppCon 的 PPT 可以在here这里找到。

根据我的理解,我对 James McNells 的伪代码进行了一些修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// The pseudocode for __except_handler3().
EXCEPTION_DISPOSITION NTAPI __except_handler3_pseudocode
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
PEH4_EXCEPTION_REGISTRATION_RECORD RegistrationNode = NULL;

// Obtain the RegistrationNode of the Current SEH Frame via offset from the EstablisherFrame pointer.
RegistrationNode
= (PEH4_EXCEPTION_REGISTRATION_RECORD)
((PCHAR)EstablisherFrame - FIELD_OFFSET(EH4_EXCEPTION_REGISTRATION_RECORD, SubRecord));
// Declare a EXCEPTION_POINTERS structure,
// fill its members with actual values from the function parameter,
// and then assign its address to RegistrationNode's ExceptionPointers member.
EXCEPTION_POINTERS ExceptionPointers{ pExceptionRecord, pContextRecord };
RegistrationNode->ExceptionPointers = &ExceptionPointers;

if (pExceptionRecord->ExceptionFlags includes EXCEPTION_UNWINDING == false)
{
// __except_handler3() is being called to perform Exception Handling
// in the function that it is registered for.

// Get a pointer to teh SCOPETABLE_ENTRY array of the Current Function
// in which __except_handler3() has been registered as the SEH Handler.
SCOPETABLE_ENTRY* pScopeTable = (SCOPETABLE_ENTRY*)(RegistrationNode->EncodedScopeTable);

// Loop through the SCOPETABLE_ENTRIES of the Current Function
// in which __except_handler3() has been registered as the SEH Handler.
//
// We are now searching for any available __except block Filter Function
// starting from the current TryLevel.
for
(
int i = RegistrationNode->TryLevel; // Start from the current TryLevel
i != -1; // and work our way downwards
i = pScopeTable[i].EnclosingLevel // until we reach TryLevel == -1.
{
if (pScopeTable[i].lpfnFilter == NULL)
{
// The current TryLevel does not have an __except Filter Function.
// We skip this TryLevel and continue to the TryLevel of the
// Enclosing __try block.
continue;
}

// If there is a Filter Function, call it and get the result.
int iFilterResult = pScopeTable[i].lpfnFilter();

switch (iFilterResult)
{
case EXCEPTION_CONTINUE_SEARCH:
{
// Move on to the next enclosing TryLevel's Scope Table.
continue;
}

case EXCEPTION_CONTINUE_EXECUTION:
{
// The Filter has resolved the Exception Cause.
// We can now continue execution at the point
// of the original Exception.
return ExceptionContinueExecution;
}

case EXCEPTION_EXECUTE_HANDLER:
{
// First do a Global Unwind. This is to inform all SEH Exception Handlers
// which have been installed -AFTER- the current SEH Handler to do Unwinding.
//
// Here, RegistrationNode->SubRecord is the TIB's ExceptionList Item
// which points to the current SEH Exception Handler.
//
// DoGlobalUnwind() will perform the following :
// 1. Get each of these handlers to do Local Unwinding.
// 2. Uninstall each of these handlers off the TIB's ExceptionList.
//
// Note that the first parameter indicates to DoGlobalUnwind() to
// do Unwinding for all SEH Handlers -UP TO- the current SEH Handler.
//
DoGlobalUnwind(&(RegistrationNode->SubRecord), pExceptionRecord);

// Next, we do a Local Unwind. This is to ensure that if there are any
// __finally blocks installed -AFTER- the current TryLevel (i.e. of a
// greater TryLevel value), they are all to be executed.
DoLocalUnwind(&(RegistrationNode->SubRecord), RegistrationNode->TryLevel);

// Do a Non-Local-Goto to call the __except handler (i.e. pScopeTable[i].lpfnHandler())
// This call must not return here.
CallAndNeverReturn(pScopeTable[i].lpfnHandler());

break;
}
}
}
}
else
{
// __except_handler3() is being called to perform Local Unwind
// in the function that it is registered for.
// The Local Unwind() will call the __finally blocks from the
// highest TryLevel down to -1.
DoLocalUnwind(&(RegistrationNode->SubRecord), -1);
}

return ExceptionContinueSearch;
}

__except_handler3是一个通用的异常处理函数,可以用于多个目的:可以用于进行当前 SEH Frame 的异常处理,也可以用于进行 local unwinding。

注意当函数中发生异常时,OS 使用 TIB->ExceptionList 中注册的第一个 SEH Exception Handler 处理,OS 并不知道这个 Handler 是哪个函数。

回忆一下 Part 1 中 Handler 会返回枚举EXCEPTION_DISPOSITION中的一个值:

1
2
3
4
5
6
7
8
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

所以说 _exception_handler3最终也会返回以上值之一。下面是伪代码的分析:

  • __except_handler3首先获取当前 SEH Frame 的EH4_EXCEPTION_REGISTRATION_RECORD指针。这个指针保存在一个局部变量RegistrationNode中。
  • 当前的 SEH Frame 在 C/C++ 函数中的__try/__except/__finally块中建立,__except_handler3也是在这里被调用
  • 接下来,一个局部EXCEPTION_POINTERS结构被定义和填充。RegistrationNode 结构的 ExceptionPointers 被设置为指向EXCEPTION_POINTERS的指针。
  • 然后检查ExceptionFlags是否包含了EXCEPTION_UNWINDING这个 flag。如果包含,意味着__except_handler3需要做的是 Local Unwinding,于是调用DoLocalUnwind。如果不包含,就假设调用它来做异常处理。
  • 异常处理中将从索引为 x 处开始遍历SCOPETABLE_ENTRY,x 为RegistratorNode->TryLevel的值。获取到的是最近的 TryLevel。
  • 如果 Filter Function 的指针不为 NULL,这意味着当前的__try块是一个__try/__finally块,会跳过这里,因为我们正在查找__except Filter。
  • 遍历SCOPETABLE_ENTRY知道找到一个__except Filter Function。当找到时,它将调用这个 Filter。
  • 当 Filter Function 返回时,就按察它的返回值:
    • 如果返回EXCEPTION_CONTINUE_SEARCH__except_handler3将继续遍历SCOPETABLE_ENTRY来查找并调用同一个 SEH Frame 中其它的 Filter Function。
    • 如果返回EXCEPTION_CONTINUE_EXECUTION,这意味着 Filter Function 修复了导致异常发生的问题,并告诉 Handler 继续执行出现异常的位置。这种情况下__except_handler3会返回ExceptionContinueExecution,然后由 OS 接管。
    • 如果返回EXCEPTION_EXECUTE_HANDLER,情况会有一些复杂,具体查看查看 Executing an __except Block

必须设置RegistrationNode->ExceptionPointerspExceptionRecordpContextRecord参数,在之后的 Global 和 Local Unwinding 中会引用它们。

Local Unwinding 是一个在 SEH Frame 中调用__finally块的过程,它在两种情况下发生:当异常已经被 SEH Handler 处理并且更高层级的 Handler 不处理这个异常;当有一个 SEH Handler 决定处理异常(在某个 TryLevel)并且在更高的 TryLevel 有__finally块需要执行。

可以看一下两个 Demo,Demo Global UnwindingDemo Local Unwinding

当前的RegistrationNode->TryLevel是 SEH Frame 中最近的 TryLevel,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void ExceptionCausingFunction()
{
int* pInt = NULL;
*pInt = 100;
}

void DemoMostRelevantTryLevel()
{
printf("TryLevel == -1. No associated ScopeEntry.\r\n");

__try
{
printf("TryLevel == 0. ScopeEntry[0].\r\n");

__try
{
printf("TryLevel == 1. ScopeEntry[1].\r\n");

ExceptionCausingFunction();

__try
{
printf("TryLevel == 2. ScopeEntry[2].\r\n");
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("__except for TryLevel == 2. ScopeEntry[2].\r\n");
}

}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("__except for TryLevel == 1. ScopeEntry[1].\r\n");
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");
}

printf("TryLevel == -1. No associated ScopeEntry.\r\n");
}

以上代码中,该函数运行时,调用了ExceptionCausingFunction,这时会发生一个异常。因为这里没有为ExceptionCausingFunction创建 Handler,OS 将调用DemoMostRelevantTryLevel__except_handler3

__except_handler3执行时,最接近的 TryLevel 是 1,这是因为 1 是当异常发生时的 TryLevel。从高的 TryLevel 到低 TryLevel 遍历SCOPETABLE_ENTRY,但是我们不能假设每次 TryLevel 都是减 1,例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
...
...
__try
{
printf("TryLevel == 3. ScopeEntry[3].\r\n");

__try
{
printf("TryLevel == 4. ScopeEntry[4].\r\n");

int* pInt = NULL;
*pInt = 100;
}
__finally
{
printf("__finally for TryLevel == 4. ScopeEntry[4].\r\n");
}
}
__finally
{
printf("__finally for TryLevel == 3. ScopeEntry[3].\r\n");
}

printf("TryLevel == -1. No associated ScopeEntry.\r\n");

在这里 TryLevel 为 3 的__try块结束后的 TryLevel 是 -1,因此遍历的是接下来的 Enclosing Level。另外需要注意EXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_SEARCHEXCEPTION_CONTINUE_EXECUTION是 Filter Function 返回的值并且在 Exception Handler 内部使用(例如__except_handler3,它们不被返回给 OS。OS 只关心返回的EXCEPTION_DISPOSITION值。

1
2
3
4
5
6
7
8
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

EXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_SEARCHEXCEPTION_CONTINUE_EXECUTIONI一般被认为是 Visual C++ 编译器定义的常量(包括__except_handler3)。其他编译器或许会使用其它的 Exception Handler 和其它的语法(而不是__try/__except/__finally)。

Executing an __except Block

__except Filter 返回 EXCEPTION_EXECUTE_HANDLER 时,它关联的块会被执行。但是在执行之前需要执行几个操作:要进行 Global Unwinding,并且要在 Local Unwinding 之后执行。执行这两个操作之后再能执行__except块的内容。

我们后面会深入了解 Global Unwinding 和 Local Unwinding,现在我们只需要了解即可。

当 SEH Frame 的 Exception Handler 决定处理异常,它可能并不是 TIB->ExceptionList 中的第一个 Handler。ExceptionList 是一个 LIFO 的链表。后注册的 SEH Frame 在链表中会更靠前。也就是说,一个更低的 SEH Frame 会直接或者简介调用一个高的 SEH Frame。我们将在下面的章节看到这一点。

在进行了 Global 和 Local Unwinding 之后,我们调用 ExceptionHandler,也就是SCOPETABLE_ENTRY[i].lpHandler,这里的 i 是对应的 TryLevel。

这个调用特殊在它不会返回。逻辑上来说就是,在__except执行之后,应该继续执行下面的代码而不是返回到 Exception Handler。

在最后,Stack Pointer 会被立即更新为EH4_EXCEPTION_REGISTRATION_RECORD中存储的栈指针。回忆一下 Part 2 中这个结构体的定义:

1
2
3
4
5
6
7
8
typedef struct _EH4_EXCEPTION_REGISTRATION_RECORD
{
PVOID SavedESP;
PEXCEPTION_POINTERS ExceptionPointers;
EXCEPTION_REGISTRATION_RECORD SubRecord;
UINT_PTR EncodedScopeTable;
ULONG TryLevel;
} EH4_EXCEPTION_REGISTRATION_RECORD, *PEH4_EXCEPTION_REGISTRATION_RECORD;

SavedESP 成员在 SEH Frame 建立时填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
?DemoLocalUnwind@@YAXXZ PROC				; DemoLocalUnwind

; 115 : {

push ebp
mov ebp, esp
push -1
push OFFSET __sehtable$?DemoLocalUnwind@@YAXXZ
push OFFSET __except_handler3
mov eax, DWORD PTR fs:0
push eax
mov DWORD PTR fs:0, esp
sub esp, 8
push ebx
push esi
push edi
mov DWORD PTR __$SEHRec$[ebp], esp

SavedESP 用于在执行__except块时恢复 ESP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; 140  :     __except (EXCEPTION_EXECUTE_HANDLER)

mov eax, 1
$LN9@DemoLocalU:
$LN21@DemoLocalU:
ret 0
$LN8@DemoLocalU:
mov esp, DWORD PTR __$SEHRec$[ebp]

; 141 : {
; 142 : printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");

push OFFSET ??_C@_0CN@NNAANJNA@__except?5for?5TryLevel?5?$DN?$DN?50?4?5Sco@
call _printf
add esp, 4

; 138 : }
; 139 : }

这对于执行__except块来说时必须的,因为要保证它在正确的栈帧中执行。

然而,如何将控制流执行到__except块开始的地方还不是完全清楚。__except_handler3调用了一个神秘的无文档函数NLG_Notify,它将控制流交给 eax 寄存器中的地址处。

另一个没有完全明白的点在于进入__except块时 EBP 时如何更新的。EBP 没有和 ESP 一样存储在EH4_EXCEPTION_REGISTRATION_RECORD中。

然而有一件事情时很清楚的,在这个过程中执行了称为Non-Local-Goto(也就是_NLG_Notify中的 NLG)的步骤。C/C++ 标准提供了 setjmp()longjmp() 函数,它们是有文档的并且很容易使用,不清楚的是_NLG_Notify如何工作,我希望有熟悉它的读者告诉我它是如何工作的。

Demo Global Unwinding

Global Unwinding 意味着三件事:

  • 在 TIB->ExceptionList 中在当前 Frame 之前的所有 SEH Handler 被使用EXCEPTION_UNWINDING调用,这启用了这些 Frame 的 Local Unwinding(也就是调用__finally块)。
  • 这些 SEH Frames 从 TIB->ExceptionList 中注销,这时因为它们与当前发生的异常已经没有关系了
  • 当最终__except块执行时,与注销的 SEH Frame 相关的这些函数的栈帧将被丢弃,这通过修改 ESP 实现,非常简单。

以下代码来自于 Matt Pietrek 的 MYSEH2.CPP,来自 article。这也是一个 TestDivisionByZero01SEH 和 MyDivisionByZero01ExceptionRoutine 的修改版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#define DISPLAY_EXCEPTION_INFO(pExceptionRecord) \
printf("An excepton occured at address : [0x%p]. Exception Code : [0x%08X]. Exception Flags : [0x%08X]\r\n", \
pExceptionRecord->ExceptionAddress, \
pExceptionRecord->ExceptionCode, \
pExceptionRecord->ExceptionFlags); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) \
printf(" EXCEPTION_NONCONTINUABLE\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_UNWINDING) \
printf(" EXCEPTION_UNWINDING\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_EXIT_UNWIND) \
printf(" EXCEPTION_EXIT_UNWIND\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_STACK_INVALID) \
printf(" EXCEPTION_STACK_INVALID\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_NESTED_CALL) \
printf(" EXCEPTION_NESTED_CALL\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_TARGET_UNWIND) \
printf(" EXCEPTION_TARGET_UNWIND\r\n"); \
\
if (pExceptionRecord->ExceptionFlags & EXCEPTION_COLLIDED_UNWIND) \
printf(" EXCEPTION_COLLIDED_UNWIND\r\n");

int g_iDividend = 1000;
int g_iDivisor = 0;

EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyDivisionByZero01ExceptionRoutine
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

return ExceptionContinueSearch;
}

int TestDivisionByZero01SEH()
{
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

EXCEPTION_REGISTRATION_RECORD Registration;
Registration.Handler = (PEXCEPTION_ROUTINE)(&MyDivisionByZero01ExceptionRoutine);
Registration.Next = TIB->ExceptionList;
TIB->ExceptionList = &Registration;

int iValue = g_iDividend / g_iDivisor;

TIB->ExceptionList = TIB->ExceptionList->Next;

return iValue;
}

void DemoGlobalUnwinding()
{
__try
{
TestDivisionByZero01SEH();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("Caught the exception in DemoGlobalUnwinding()\n");
}
}

在这个函数中,__try/__except 块使得编译器生成使用 _except_handler3 的 SEH Frame。在__try块中,TestDivisionByZero01SEH在另一个 SEH Frame 和自定义的 Handler MyDivisionByZero01ExceptionRoutine中。

DemoGlobalUnwinding执行时,除 0 异常发生。当前 TIB->ExceptionList 中最高的 SEH Frame 是 TestDivisionByZero01SEH建立的,它的 Handler 是MyDivisionByZero01ExceptionRoutine,它将被调用来处理异常。

MyDivisionByZero01ExceptionRoutine返回 ExceptionContinueSearch,表示它拒绝处理这个异常。Windows 继续查找下一个 SEH Frame(由 DemoGloablUnwinding 建立),并且调用它的 Filter Function。Filter Function 返回EXCEPTION_EXECUTE_HANDLER,表示要执行_except handler 需要被调用。但是在这之前,MyDivisionByZeroExceptionRoutine将被再次调用

第二次调用就是 Global Unwinding 的一部分,后面将会介绍。当它第二次被调用时,pExceptionRecord->ExceptionFlags将包含EXCEPTION_UNWINDING。这个标志告诉 Handler 现在是在执行 Unwinding。当 SEH Frame 需要 Unwind 时,它表示需要执行 Local Unwind。

现在观察DemoGlobalUnwinding的输出:

img

注意 Unwinding 是在 _except handler 之前执行的。

现在来演示 Executing an __except Block 中提到的关于建立 SEH Frame 的部分,也就是后建立的函数的 Handler 在 TIB->ExceptionList 中更靠前,我们在运行时观察TestDivisionByZero01SEH

img

在这里我们使用 Visual Studio 的 QuickWatch 窗口观潮 TIB 的 ExceptionList 链表。这里演示的是 77 行中 TestDivisionByZero01SEHEXCEPTION_REGISTRATION_RECORD被插入为 SEH Frame 的部分。其中显示:

  • MyDivisionByZero01ExceptionRoutine是最新的 SEH Handler
  • 下一个是__exception_handler3,它是由DemoGlobalUnwinding插入的
  • 再下面是__exception_handler4,是由__scrt_common_main_seb(它间接调用DemoGlobalUnwinding

James McNells 在 CPP Con 中提供了一个很好的总结性伪代码,我加入了一些注释,贴在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void RtlUnwindPseudocode
(
EXCEPTION_REGISTRATION_RECORD* TargetFrame,
void* TargetIp,
EXCEPTION_RECORD* ExceptionRecord,
void* ReturnValue
)
{
// Include the EXCEPTION_UNWINDING in ExceptionFlags
ExceptionRecord->ExceptionFlags |= EXCEPTION_UNWINDING;

// Obtain the TIB so that we can traverse the ExceptionList.
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();
// Traverse the ExceptionList from the topmost SEH Frame
// and move downwards until we reach the TargetFrame.
while (TIB->ExceptionList != TargetFrame)
{
// For each inner SEH Frame, call its Exception Handler.
// The Exception Handler is supposed to call LocalUnwind().
TIB->ExceptionList->Handler(ExceptionRecord, TIB->ExceptionList);
// Not only move downwards. Also change the ExceptionList
// so that it no longer point to the current SEH Frame.
TIB->ExceptionList = CurrentRecord->Next;
}
}

Demo Local Unwinding

Local Unwinding 的主要目的是执行函数中的__finally块,观察以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void DemoLocalUnwind()
{
__try
{
printf("TryLevel == 0. ScopeEntry[0].\r\n");

__try
{
printf("TryLevel == 1. ScopeEntry[1].\r\n");

__try
{
printf("TryLevel == 2. ScopeEntry[2].\r\n");

ExceptionCausingFunction();
}
__finally
{
printf("__finally for TryLevel == 2. ScopeEntry[2].\r\n");
}
}
__finally
{
printf("__finally for TryLevel == 1. ScopeEntry[1].\r\n");
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("__except for TryLevel == 0. ScopeEntry[0].\r\n");
}
}

DemoLocalUnwind中,ExceptionCausingFunction在 TryLevel 2 引发一个异常。然而,这个异常在 TryLevel 0 处理,因此在 TryLevel 2 和 0 之间,也许有SCOPETABLE_ENTRY包含了__finally块,这些块需要被调用。注意是可能,因为可能不存在__finally块,而是存在不处理异常的__except块。

在 TryLevel 0 的 Exception Handler 执行之前,进行 Local Unwinding。从当前发生异常的 TryLevel开始遍历SCOPETABLE_ENTRY数组,直到遇到一个Stop Point。这里的 Stop Point 是 TryLevel 0 因为它不是 Unwinding 过程的一部分。

DemoLocalUnwind中,当异常发生时,TryLevel 为 2。因此下面的__finally被执行:

1
2
3
4
__finally
{
printf("__finally for TryLevel == 2. ScopeEntry[2].\r\n");
}

然后 TryLevel 被设置成SCOPETABLE_ENTRY[2]的 Eclosing Level,也就是 1,然后执行下面的__finally

1
2
3
4
__finally
{
printf("__finally for TryLevel == 1. ScopeEntry[1].\r\n");
}

然后 TryLevel 被设置成SCOPETABLE_ENTRY[1]的 Enclosing Level,这里是 0(Stop Point)。Local Unwinding 过程结束。

James McNells 提供了 Local Unwinding 的伪代码,我做了一些修改并加入了注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _local_unwind_pseudcode
(
EH4_EXCEPTION_REGISTRATION_RECORD* RN,
int Stop
)
{
// We assume that RN's TryLevel is currently at the
// TryLevel at which the exception occurred.
while (RN->TryLevel != Stop)
{
// Access the SCOPETABLE_ENTRY of the TryLevel.
SCOPETABLE_ENTRY* CurrentEntry = &RN->ScopeTable[RN->TryLevel];
// CurrentEntry->Filter == NULL means this is a __finally block
// which is what we want.
if (CurrentEntry->Filter == nullptr)
{
// Call the __finally block code.
CurrentEntry->Handler();
}
// Move onto the next EnclosingLevel.
RN->TryLevel = CurrentEntry->EnclosingLevel;
}
}

Source Codes

这部分源代码可以在GitHub 上找到,使用 VS 2019 Community 编译,需要将目标设置为 x86 而不是 x64。

注意我进行了一些设置来贱货学习过程和生成的汇编代码,包括:

  • 不优化
  • C/C++ -> Code Generation -> Basic Runtime Checks: Default
  • C/C++ -> Code Generation -> Security Checks: Disable Security Checks(/GS-)

关闭了优化来生成与上面类似的代码,这些代码更容易看懂。使用 Default Basic Runtime Checking 避免生成额外的运行时代码,这些代码可能影响我们阅读生成的汇编。禁用 Security Checks,编译器会使用__except_handler3作为 Handler,否则编译器会使用__except_handler4,这时一个更复杂的 Handler ,包含了一些安全特性,不包括在本文的内容中。

Summary

在 Part 3 中,我们对__except_handler3进行了严谨的学习,我们知道了 Local Unwinding 和 Global Unwinding,并进行了一些有限的学习去理解控制流时如何转换的。

目前为止,我们通过修改 TIB->ExceptionList 来创建了自己的 SEH Frame。然而,我们没有给出相应的自定义的 Exception Filter 函数和自定义的 finally 函数。

下一章,我们将用我们目前学到的知识来创建自定义的 Exception Filters,except 和 finally 函数。

Part 4 – Pseudo __try/__except

Introduction

在前面的章节中,我们做了许多细致的研究。在 Part 4,我提供了一个如何建立自定义的 SEH Frame 和如何使用我们自己的__exception Filter 和 __except 块。这为希望更深入研究 SEH 和建立更多自定义的代码结构的研究者提供一些方法。

源代码可以在GitHub找到。

Defining a Customized SEH Frame

我们在 Part 1 中已经看到如何建立自定义的 SEH Frame。我们也看到了如何将自定义的 Exception Handler 与 SEH Frame 关联。

示例代码更深入了一部,加入了SCOPETABLE_ENTR,exception filters 和 exception 块。主要使用了两个函数,DoCustomExceptionHandlingMyExceptionHandler

下面是DoCustomExceptionHandling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void DoCustomExceptionHandling()
{
NT_TIB* TIB = (NT_TIB*)NtCurrentTeb();

SCOPETABLE_ENTRY scopeentry[1];
scopeentry[0].EnclosingLevel = -1;
scopeentry[0].lpfnFilter = MyFilter;
scopeentry[0].lpfnHandler = MyHandler;

EH4_EXCEPTION_REGISTRATION_RECORD Registration;
Registration.EncodedScopeTable = (UINT_PTR)scopeentry;
Registration.SubRecord.Handler = (PEXCEPTION_ROUTINE)(MyExceptionHandler);
Registration.SubRecord.Next = TIB->ExceptionList;
TIB->ExceptionList = &(Registration.SubRecord);

// Exception raising code below.
int jmpret = setjmp(mark);

if (jmpret == 0)
{
// Raise the TryLevel.
Registration.TryLevel = 0;
// Exception raising code below.
Foo();
}
else
{
printf("Exception occurred\r\n");
}

// Restore the TryLevel.
Registration.TryLevel = -1;

TIB->ExceptionList = TIB->ExceptionList->Next;
}

它的工作原理如下:

  • 使用EH4_EXCEPTIOIN_REGISTRATION_RECORD代替EXCEPTION_REGISTRATION_RECORD,它包含一个指向SCOPETABLE_ENTRY数组的指针。
  • 和之前一样使用 NT_TIB结构完成注册EH4_EXCEPTION_REGISTRATION_RECORD的工作
  • 然而这次我们定义了一个包含一个元素的SCOPETABLE_ENTRY局部数组,其中的元素代表了一个__try块,它的 TryLevel 是 0,EnclosingLevel 是 -1。
  • SCOPETABLE_ENTRY__except Filter 是 MyFilter__except Handler 是 MyHandler
  • 调用 setjmp() 函数来标记受保护的代码的开始处
  • 根据 MSDN 文档,setjmp保存调用时的栈环境到全局的 jmp_buf 对象中,名称为 mark。
  • setjmp第一次调用时,它总是返回 0.后面它可能会被调用 longjmp().触发。
  • 为了模拟一个真实的 SEH Frame,我们把 TryLevel 设置为 0.
  • 然后调用Foo函数,触发 Access Violation 异常。
  • 这个Foo函数本身也包含了一个__try/__finally块,我们稍后将看到它的 finally 块在 Local Unwind 过程中执行。
  • 当异常在Foo中发生时,将调用longjmp并且将控制返回到setjmp
  • 之前在 mark 中保存的栈环境被恢复,setjmp返回
  • longjmp也提供了setjmp的返回值(-1),这一次 if 语句的 else 部分被执行,也就是 printf。
  • TryLevel 重置为 -1 并且移除 TIB->ExceptionList 中的 SEH Frame。

接下来我们看以下MyExceptionHandler函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
EXCEPTION_DISPOSITION NTAPI _Function_class_(EXCEPTION_ROUTINE) MyExceptionHandler
(
_Inout_ struct _EXCEPTION_RECORD* pExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT* pContextRecord,
_In_ PVOID DispatcherContext
)
{
DISPLAY_EXCEPTION_INFO(pExceptionRecord)

PEH4_EXCEPTION_REGISTRATION_RECORD RegistrationNode =
(PEH4_EXCEPTION_REGISTRATION_RECORD)
((PCHAR)EstablisherFrame -
FIELD_OFFSET(EH4_EXCEPTION_REGISTRATION_RECORD, SubRecord));

EXCEPTION_POINTERS exception_pointers;
exception_pointers.ExceptionRecord = pExceptionRecord;
exception_pointers.ContextRecord = pContextRecord;
RegistrationNode->ExceptionPointers = &exception_pointers;

int iCurrentTryLevel = RegistrationNode->TryLevel;
SCOPETABLE_ENTRY* scopetable = (SCOPETABLE_ENTRY*)(RegistrationNode->EncodedScopeTable);

if (pExceptionRecord->ExceptionFlags == 0)
{
int iFilterFuncRet = 0;

for
(
int i = iCurrentTryLevel;
i != -1;
i = scopetable[i].EnclosingLevel
)
{
if (scopetable[i].lpfnFilter == NULL)
{
// The current TryLevel does not have an __except Filter Function.
// We skip this TryLevel and continue to the TryLevel of the
// Enclosing __try block.
continue;
}

iFilterFuncRet = scopetable[iCurrentTryLevel].lpfnFilter();

switch (iFilterFuncRet)
{
case EXCEPTION_CONTINUE_SEARCH:
{
// Move on to the next enclosing TryLevel's Scope Table.
continue;
}

case EXCEPTION_CONTINUE_EXECUTION:
{
// The Filter has resolved the Exception Cause.
// We can now continue execution at the point
// of the original Exception.
return ExceptionContinueExecution;
}

case EXCEPTION_EXECUTE_HANDLER:
{
if (scopetable[iCurrentTryLevel].lpfnHandler != NULL)
{
// First do a Global Unwind. This is to inform all SEH Exception Handlers
// which have been installed -AFTER- the current SEH Handler to do Unwinding.
//
// Here, RegistrationNode->SubRecord is the TIB's ExceptionList Item
// which points to the current SEH Exception Handler.
//
// RtlUnwind() will perform the following :
// 1. Get each of these handlers to do Local Unwinding.
// 2. Uninstall each of these handlers off the TIB's ExceptionList.
//
// Note that the first parameter indicates to RtlUnwind() to
// do Unwinding for all SEH Handlers -UP TO- the current SEH Handler.
//
RtlUnwind((EXCEPTION_REGISTRATION_RECORD*)(&(RegistrationNode->SubRecord)), NULL, pExceptionRecord, NULL);

// Do a local unwind up to the current TryLevel.
// Local unwinding is necessary for the SEH frame that contains a SEH Handler.
// This is because there could be a __finally block beneath the __except block.
_local_unwind2((EXCEPTION_REGISTRATION_RECORD*)(&(RegistrationNode->SubRecord)), iCurrentTryLevel);

// Execute the Exception
scopetable[iCurrentTryLevel].lpfnHandler();

// Jump to the location of setjmp()
// and never return.
longjmp(mark, -1);
}
else
{
continue;
}
}
}
}
}
else if (pExceptionRecord->ExceptionFlags == EXCEPTION_UNWINDING)
{
_local_unwind2((EXCEPTION_REGISTRATION_RECORD*)(&(RegistrationNode->SubRecord)), -1);
}

return ExceptionContinueSearch;
}

MyExceptionHandler类似于__except_handler3,以下是工作流程:

  • 从 EstablisherFrame 访问EH4_EXCEPTION_REGISTRATION_RECORD的指针,并保存到局部变量 RegistrationNode 中
  • pExceptionRecordpContextRecord指针被保存到EXCEPTION_POINTERS结构中,放到RegistrationNode->ExceptionPointers备用
  • 异常发生时的 TryLevel 放到iCurrentTryLevel
  • SCOPETABLE_ENTRY数组的指针放到局部变量scopetable
  • 检查ExceptionFlags来查看MyExceptionHandler是用于处理异常还是用于 Unwinding
  • 如果是要进行 Unwinding,遍历当前 SEH Frame 中的SCOPETABLE_ENTRY,从索引为iCurrentTryLevel处开始。对于每个项目,我们检查它是否包含一个__except Filter 函数。如果包含,我们调用这个 Filter 并检查iFilterFuncRet中的返回值;如果不包含,我们将索引设置为 Enclosing Level 的值再次搜索。
  • 接上面,如果iFilterFuncRetEXCEPTION_CONTINUE_SEARCH,我们继续查找可用的SCOPETABLE_ENTRY,如果所有的元素都被处理了,Handler 返回ExceptionContinueSearch;如果iFilterFuncRetEXCEPTION_CONTINUE_EXECUTION,Handler返回ExceptionContinueExecution;如果iFilterFuncRetEXCEPTION_EXECUTE_HANDLER,我们进行与_except_handler3类似的操作,假设_except Handler 不为 NULL:
    • 调用RtlUnwind来进行 Global Unwinding
    • 执行_local_unwind2来在当前 SEH Frame 中执行 Local Unwinding
    • Scopetable[CurrentTryLevel]指向的假定的_except Handler被调用
    • 当 handler 结束时,控制返回给MyExceptionHandler。然而我们模拟__except_handler3non-local-goto,使用 -1 调用longjmp

Noteworthy Points

因为Foo本身包含了 SEH Frame,并且被DoCustomExceptionHandling调用,它的 SEH Frame 被设置在TIB->ExceptionList中比DoCustomExceptionHandling更高的位置。因此,在MyExceptionHandler中进行 Global Unwinding 时,Foo的 SEH Handler 将被使用EXCEPTION_UNWINDING标志调用。它的__finally块被调用,会打印__finally @ Foo()

我在 MyFilter 中注释了返回EXCEPTION_CONTINUE_SEARCH的代码:

1
2
3
4
5
6
7
int __stdcall MyFilter()
{
printf("MyFilter()\r\n");

return EXCEPTION_EXECUTE_HANDLER;
//return EXCEPTION_CONTINUE_SEARCH;
}

如果将返回值换为EXCEPTION_CONTINUE_SEARCH,就会出现以下变化:

  • 这一次,MyExceptionHandler不会处理这个异常
  • OS 将移动到下一个 Handler(在 main 的 SEH Frame 中)
  • main 的 SEH Handler 来进行 Global Unwind 因此Foo__finally将在使用EXCEPTION_UNWINDING调用MyExceptionHandler之后执行
  • MyExceptionHandler将尝试为DoCustomExceptionHandling的 SEH Frame 做 Local Unwind。
  • DoCustomExceptionHandling没有假的__finally块,所以什么都不会发生。
  • 最后main__except块被执行,打印__except @ main()

Summary

我希望读者对这些代码感兴趣,也希望有人进行更深入的研究,发现一些有趣的事情。我目前正在做包括创建多个假的__try/__except/__finally的研究。所以可能会有 Part 5。

Understanding Windows Structured Exception Handling

http://example.com/2023/03/21/understanding-seh/

作者

lll

发布于

2023-03-21

更新于

2023-03-22

许可协议