65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (27投票s)

2017 年 10 月 26 日

CPOL

7分钟阅读

viewsIcon

37999

downloadIcon

1228

滚出一个实际解决方案

引言

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 提供了一种挂钩此回调函数的方法,只需在 PROCFRAME 属性后添加某个处理程序的名称即可。

在 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

    此结构中有两个字段特别有趣,即 ExceptionCodeExceptionAddress 字段。

  • 第二个参数是指向建立者帧的指针。尽管它可能对调试很重要(实际上在汇编中不如在 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 的名称添加到该 PROCFRAME 属性时,就会将一个 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 位资源中找到它。

参考文献

  1. MASM 的展开辅助程序
  2. 如何在 ML64 (MASM) 中实现 __try __except
  3. 用于异常处理、调试器支持的展开数据

历史

  • 2017 年 10 月 26 日:初始发布
  • 2017 年 11 月 2 日:更新 - 测试集 (Proc1)、C/C++ 示例以及文本中的各种错别字已修复
© . All rights reserved.