64 位结构化异常处理 (SEH) 在 ASM 中





5.00/5 (27投票s)
滚出一个实际解决方案
引言
Windows 操作系统提供了一个结构化异常处理 (SEH) 基础设施。
另一方面,一些高级语言提供了对它的内部支持,即运行时库,可以轻松处理操作系统实现。
当抛出异常时,会发生自动的展开,这意味着通过函数调用堆栈向后搜索,直到找到异常处理程序。
这个过程对开发者来说是完全透明的(而且内部工作原理也大多未公开),开发者通常只需要关注界定异常处理程序的范围。范围使用 __try/__except
或类似子句来定义。
在 C/C++ 或 .NET 中编程时,这对于开发者期望的操作来说是可以的,即插入 __try/__except
子句然后忽略它。
汇编语言的情况
ASM 程序员没有可用的运行时库来处理SEH。特别是对于MASM(我们从现在开始只关注MASM模型,尽管其他汇编器,尤其是下面提到的那些,情况大致相同),他们所能获得的是一组原始伪指令(也有一组宏,但几乎模仿了原始伪指令的功能),它们会在 PE Coff 的 .pdata
和 .xdata
段中创建展开信息(参见下面的参考文献 1)。
在描述原始伪指令的说明中提供了一个基本示例,但它隐含地假设 PROC(实际上是 PROCEDURE)将从 C/C++ 程序调用,否则将无法按预期工作。
构建我们自己的运行时
1. 挂钩回调函数
当发生异常时,操作系统会提供一个回调函数,在MASM中,其原型可以简单地声明为
_except_handler PROTO :PTR, :PTR, :PTR, :PTR
MASM 提供了一种挂钩此回调函数的方法,只需在 PROC 的 FRAME 属性后添加某个处理程序的名称即可。
在 C/C++ 中,编译器会在您建立 __try/__except
受保护块范围时在后台挂钩回调函数——但在MASM中,事情并非如此简单。也就是说,您还需要找到一种方法来在每个 PROC 中布局受保护块范围(这也有些困难,尽管可以像我一样自动化)。最终结果是必须在回调处理程序本身中找到正确的受保护块,因为它们是独立布局的(稍后将详细介绍,并在我们的源代码中进行说明)。
2. 理解回调函数
从其原型可以看出,回调函数提供了四个参数指针(其中三个指向结构)。
- 第一个参数是指向
EXCEPTION_RECORD
结构的指针,该结构定义为EXCEPTION_RECORD STRUCT ExceptionCode DWORD ? ExceptionFlags DWORD ? ExceptionRecord LPVOID ? ExceptionAddress LPVOID ? NumberParameters DWORD ? ExceptionInformation QWORD EXCEPTION_MAXIMUM_PARAMETERS dup (?) EXCEPTION_RECORD ENDS
此结构中有两个字段特别有趣,即
ExceptionCode
和ExceptionAddress
字段。 - 第二个参数是指向建立者帧的指针。尽管它可能对调试很重要(实际上在汇编中不如在 C/C++ 中),但我们不会使用它,因为我们的方法是尝试恢复执行而不是调试异常。
- 第三个参数是指向
CONTEXT
结构的指针,该结构代表异常发生时线程的 CPU 寄存器值。这是一个很大的结构,无法在此处全部显示,但可在附带的源代码中找到。 - 第四个参数是指向
DISPATCHER_CONTEXT
结构的指针,定义为DISPATCHER_CONTEXT STRUCT ControlPc QWORD ? ImageBase QWORD ? FunctionEntry LPVOID ? EstablisherFrame QWORD ? TargetIp QWORD ? ContextRecord LPVOID ? LanguageHandler LPVOID ? HandlerData LPVOID ? HistoryTable LPVOID ? ScopeIndex DWORD ? Fill0 DWORD ? DISPATCHER_CONTEXT ENDS
我不会在这里详细介绍所有这些结构的细节,它们非常庞大且大多未公开。
3. 回调函数的处理程序
微软将回调函数的处理程序指定为语言特定处理程序 (LSH)。虽然我更喜欢“异常处理程序”这个名称,但这个名称已经被用于展开过程结束后恢复执行的部分。
如前所述,LSH 接收大量信息,可以以多种方式处理。
我们的目标仅仅是(在可能的情况下)恢复程序的执行,因此我们将重点关注必要的内容并采取最可靠的行动。
从现在开始,我们的解释将基于我们自己的 MASM LSH 的工作方式,但请注意,存在其他替代方法,甚至可能更好,尽管我并不知晓。
因此,在进行一些准备工作后,LSH 首先要定位异常发生的位置。最佳选择是依赖 DISPATCHER_CONTEXT
结构中的 ControlPc
字段。它告诉我们异常发生在该 PROC 内的地址的 RIP 寄存器值,或者如果异常无法在该 PROC 中处理,则是它离开该 PROC 的地址。
然后它调用 RtlLookupFunctionEntry
API,以获取与 ControlPc
RIP 寄存器值对应的函数表中的一个条目。每当将某个 LSH 的名称添加到该 PROC 的 FRAME 属性时,就会将一个 PROC 添加到函数表中。如果 RtlLookupFunctionEntry
返回一个有效值(通常如此),我们将从中获得某个 PROC 的开始地址和结束地址。
有了这些信息,LSH 所要做的就是查找受保护块(如前所述,对于 MASM,需要手动建立)。
如果找不到受保护块,LSH 将返回一个值,告诉操作系统继续搜索。
如果找到受保护块,LSH 将调用 RtlUnwindEx
API 来处理展开。此函数执行大量展开工作(参见下面的参考文献 3),否则手动执行将非常容易出错。
最后,请注意,对于每个异常,LSH 将被调用多次,主要用于满足 C/C++ 的清理需求。
我们的代码
defAsmSpecificHandler
我们的源代码提供了一个 LSH,我将其命名为 defAsmSpecificHandler
。我可以为不同的 PROC 使用不同的 LSH,但我决定将所有内容集中到一个处理程序中。
defAsmSpecificHandler
执行上一章中提到的所有任务。此外,它还会收集一些可选信息,以便在 faulty program 中恢复执行时进行报告。请注意,如果 LSH 设计为线程安全,则不应以这种方式收集信息。这留作练习,我不想通过使事情复杂化来分散对重点的注意力。
这是 defAsmSpecificHandler
; ***This is a "catch-all" Language Specific Handler for all our PROCs***
defAsmSpecificHandler PROC USES rbx rsi rdi r12 r13 r14 r15 pExceptionRecord:PTR, pEstablisherFrame:PTR, pContextRecord:PTR, pDispatcherContext:PTR
LOCAL imgBase : PTR
LOCAL targetGp : PTR
LOCAL BeginAddress : PTR
LOCAL EndAddress : PTR
LOCAL catchHandler : PTR
mov pExceptionRecord, rcx
mov pEstablisherFrame, rdx
mov pContextRecord, r8
mov pDispatcherContext, r9
; Copy Contexts as they unwind. This serves also for reporting purposes.
mov rdi, OFFSET originalExceptContext
mov rax, pDispatcherContext
mov rsi, (DISPATCHER_CONTEXT ptr [rax]).ContextRecord
mov rcx, SIZEOF CONTEXT / 8
rep movsq
mov rcx,pExceptionRecord
cmp DWORD PTR [rcx].EXCEPTION_RECORD.ExceptionFlags, EXCEPTION_NONCONTINUABLE
jne @F
; Bail out
mov rcx,0
call ExitProcess
@@:
test DWORD PTR [rcx].EXCEPTION_RECORD.ExceptionFlags, EXCEPTION_UNWIND
jnz @F
; On first pass of each exception, save data structures
; pointed to by arguments, so we can report if wanted,
; otherwise may be skipped.
sub rsp, 20h
mov rcx, pExceptionRecord
mov rdx, pContextRecord
call saveContextAndExceptRecs
add rsp, 20h
@@:
; De-nest Catch Blocks.
cmp blocksDenested,0 ; Previously done?
jne @F
sub rsp, 20h
call denestCatchBlocks
add rsp, 20h
mov blocksDenested, 1
@@:
mov rcx,pExceptionRecord
test DWORD PTR [rcx].EXCEPTION_RECORD.ExceptionFlags, EXCEPTION_UNWIND
mov eax, ExceptionContinueSearch
jnz @exit
; Search for a valid IMAGE_RUNTIME_FUNCTION_ENTRY
; that corresponds to the RIP value of the exception
; or where it left the PROCEDURE.
mov rax, pDispatcherContext
mov rcx, (DISPATCHER_CONTEXT PTR [rax]).ControlPc
lea rdx, imgBase
lea r8, targetGp
sub rsp, 20h
call RtlLookupFunctionEntry
add rsp, 20h
cmp rax, 0 ; Is return value valid?
jnz @F
; We shouldn't come here (even with leaf functions).
mov ecx, 1
call ExitProcess
@@:
mov r13, imgBase
mov r11d, (IMAGE_RUNTIME_FUNCTION_ENTRY PTR [rax]).BeginAddress
add r11, r13
mov BeginAddress, r11
mov r11d, (IMAGE_RUNTIME_FUNCTION_ENTRY PTR [rax]).EndAddress
add r11, r13
mov EndAddress, r11
; Search for the (innermost) Catch Block in range (in case of nested blocks).
mov rsi, dataexcpStart
mov r11, pDispatcherContext
mov r11, (DISPATCHER_CONTEXT ptr [r11]).ControlPc
mov catchHandler,0 ; Zero it, in order to know in the end if we got something.
mov r12, 7FFFFFFFh ; Enter a big enough number in r12.
@loopStart:
cmp QWORD PTR [rsi], 204E4445h ; End of all Blocks?
je @loopEnd
cmp QWORD PTR [rsi], 544F4C53h ; New slot signature?
jne @ifEnd
mov r14, QWORD PTR [rsi].CATCHBLOCKS.try
cmp r14, BeginAddress
jb @ifEnd
cmp r14, r11
ja @ifEnd
mov r13, QWORD PTR [rsi].CATCHBLOCKS.catch
cmp r13, EndAddress
ja @ifEnd
cmp r13, r11
jb @ifEnd
mov rax, r13
sub rax, r14
cmp r12, rax
jbe @ifEnd
; Got one
mov r12, rax
mov catchHandler, r13
jmp @ifEnd
@ifEnd:
add rsi, SIZEOF CATCHBLOCKS
jmp @loopStart
@loopEnd:
cmp catchHandler, 0
jne @F
; No Catch Block, continue searching in parent procedures.
mov eax, ExceptionContinueSearch
jmp @exit
@@:
mov rcx, pEstablisherFrame
mov rdx, catchHandler
mov r8, pExceptionRecord
mov LSHretValue, 66h ; Any value. Let's test it in Proc1.
lea r9, LSHretValue
sub rsp, 30h
mov rax, pDispatcherContext
mov rax, [rax].DISPATCHER_CONTEXT.HistoryTable
mov [rsp+28h], rax
lea rax, originalExceptContext
mov [rsp+20h], rax
call RtlUnwindEx ; Must not return. If it returns there is an error.
add rsp, 30h ; We don't expect to come here, but anyway.
mov ecx, 1
call ExitProcess
@exit:
; The default epilog will restore non-volatile registers.
ret
defAsmSpecificHandler ENDP
受保护块是如何布局的?
我使用一组三个宏来定义受保护块(在代码中,它们由一个名为 CATCHBLOCKS
的结构定义),并使用我们自己的PE 段,称为 dataexcp
,来存储有关它们的信息。
这是我们的PE 段的开头
dataexcp SEGMENT PARA ".data"
blocksDenested QWORD 0
dataexcpStart LABEL near
QWORD 204E4445h ; Signature for END of all blocks
ORG $-8 ; Overwrite END, if there are catch blocks
dataexcp ENDS
这是那三个宏
_ExceptionBlock TEXTEQU <0>
__TRY MACRO
LOCAL tryPos, level
tryPos EQU $
level TEXTEQU @SizeStr(%_ExceptionBlock)
level TEXTEQU %(level -1)
_ExceptionBlock CATSTR _ExceptionBlock, level
dataexcp SEGMENT
QWORD 544F4C53h ;; Signature for new slot
QWORD level
QWORD tryPos
dataexcp ENDS
ENDM
__EXCEPT MACRO
LOCAL catchPos, level
catchPos EQU $+5 ;; 5 is the size of the "jmp near" instruction
level TEXTEQU @SizeStr(%_ExceptionBlock)
level TEXTEQU %(level -2)
dataexcp SEGMENT
QWORD level
QWORD catchPos
dataexcp ENDS
.code
%jmp near ptr @catch&_ExceptionBlock&_end
%@catch&_ExceptionBlock&_start:
ENDM
__FINALLY MACRO
LOCAL endCatchPos, count, level, temp
endCatchPos EQU $
level TEXTEQU @SizeStr(%_ExceptionBlock)
level TEXTEQU %(level -2)
dataexcp SEGMENT
QWORD level
QWORD endCatchPos
QWORD 204E4445h ;; Signature for end of all blocks
ORG $-8 ;; Ready to be overwriten, if more Blocks
dataexcp ENDS
count TEXTEQU @SizeStr(%_ExceptionBlock)
count TEXTEQU %(count -1)
.code
%@catch&_ExceptionBlock&_end:
.data
_ExceptionBlock TEXTEQU @SubStr(%_ExceptionBlock, 1, count)
IF count EQ 1
temp TEXTEQU %(_ExceptionBlock +1)
BYTE temp
IF temp EQ 9
_ExceptionBlock TEXTEQU <0>
ELSE
_ExceptionBlock TEXTEQU <temp>
ENDIF
ENDIF
.code
ENDM
有了这些小巧的宏,我们在 PROC 中要做的就是插入这三个宏来界定一个受保护块,类似于这样
someProcedure PROC FRAME:defAsmSpecificHandler
push rbp
.pushreg rbp
mov rbp, rsp
.setframe rbp, 0
.endprolog
__TRY
; Guarded Section of code (level 0)
_TRY
; Another Guarded Section of code (level 1)
_EXCEPT
; Another Exception Handler (level 1)
_FINALLY
__EXCEPT
; Exception Handler (level 0)
__FINALLY
mov rsp, rbp
pop rbp
ret
someProcedure ENDP
非常简单,而且与 C/C++ 的语义非常相似。
执行的测试
我设计了一系列六个测试,几乎涵盖了 ASM 程序员在现实生活中遇到的所有情况。还有一些我没有处理的边缘情况。不过,如果任何读者认为它们相关并提供代码来说明他们的情况,我可能会更新文章以处理它们。
如前所述,defAsmSpecificHandler
不是线程安全的,但使其线程安全的更改很少。这留作练习。
测试的源代码在本文的附件中。
与 C/C++ 的兼容性
defAsmSpecificHandler
与 Visual Studio C 和 C++ **SEH** 机制兼容。如果异常无法在 ASM 模块中处理,它将被传递到 C/C++ 中,在那里可以被处理。
我在附件的一个示例中对此进行了说明,这是相关的 C/C++ 部分
#include <stdio.h>
#include <excpt.h>
#include <windows.h> // for EXCEPTION_ACCESS_VIOLATION
#ifdef __cplusplus
extern "C"
{
#endif
void proc1();
void proc2_0();
void proc3_0();
void proc4();
void proc5();
void proc6_0();
#ifdef __cplusplus
}
#endif
int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep) {
#ifdef __cplusplus
printf("***Exception 0x%x at 0x%.8llx trapped in main***\n",
code, reinterpret_cast<intptr_t>(ep->ExceptionRecord->ExceptionAddress));
#else
printf("***Exception 0x%x at 0x%.8llx trapped in main***\n",
code, (unsigned __int64)(ep->ExceptionRecord->ExceptionAddress));
#endif
return EXCEPTION_EXECUTE_HANDLER;
}
int main()
{
__try
{
proc1();
proc2_0();
proc3_0();
proc4();
proc5();
proc6_0();
}
__except(filter(GetExceptionCode(), GetExceptionInformation()))
{
printf("\nExecuting Handler\n");
}
return 0;
}
最终注释
- 虽然此 ASM 代码主要针对使用 **MASM** 进行汇编,但它也可以在不更改的情况下使用 **JWASM** 或 **UASM**(2.43 及更高版本)进行汇编。 **JWASM** 和 **UASM** 具有其他 **MASM** 不支持的功能,但本质上向后兼容 **MASM**。
- 该代码已在从 Windows XP-64bit 到 Windows 10 64bit 的所有操作系统上进行了测试。但是,对于像 Windows XP-64bit 这样老的操作系统,您将需要一个合适的链接器(严格来说,您甚至可以使用一个最近的链接器),您可以在历史悠久的 MASM32.COM 网站提供的 64 位资源中找到它。
参考文献
历史
- 2017 年 10 月 26 日:初始发布
- 2017 年 11 月 2 日:更新 - 测试集 (
Proc1
)、C/C++ 示例以及文本中的各种错别字已修复