浅析64位Windows的SEH机制

浅析64位Windows的SEH机制

笔者花费了一些时间,学习了64位WindowsSEH机制的底层原理。现写为博客,希望对大家有帮助。

注意:本文是从逆向工程的角度分析64位Windows的SEH机制,阅读本文的前提是能够从程序员(开发)的角度理解SEH机制。

如果读者从未了解过SEH机制,请先阅读以下文章:

Windows SEH机制(一)

Windows SEH之全局展开

Windows SEH机制(二)

要使SEH发挥作用,必须得到编译器、硬件和操作系统的配合支持。SEH 在特定平台上的具体实现方式可能因架构而异。

下图总结了Windows异常处理机制的基本框架(摘自《加密与解密》):

示例程序

这里先给出一个基本的示例程序,便于我们讨论:

cpp 复制代码
// 示例程序 test.c
#include <windows.h>
#include <stdio.h>

int main() {
    __try {
        printf("__try block\n");
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        printf("__except block\n");
    }
    return 0;
}

RUNTIME_FUNCTION结构体

64位中的SEH机制不再把异常处理下相关信息放到栈上,而是由编译器生成相关信息,并在放到PE文件中一个特定的节区中:.pdata。在64位SEH机制中需要学习的第一个结构就是RUNTIME_FUNCTION。若干RUNTIME_FUNCTION结构体组成一个数组,放在.pdata节区中。该结构体定义如下:

cpp 复制代码
  typedef struct _RUNTIME_FUNCTION {
    DWORD BeginAddress;    // Start RVA of SEH code chunk
    DWORD EndAddress;      // End RVA of SEH code chunk
    DWORD UnwindData;      // Rva of an UNWIND_INFO structure that describes this code frame
  } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

该结构体包含三个字段:

  1. BeginAddress:函数的起始地址
  2. EndAddress:函数的结束地址
  3. UnwindData:指向UNWIND_INFO结构体的指针

通过PE工具,我们可以查看到该结构体数组。例如,对于上面的程序,使用cff程序查看异常目录:

这个结构体的数量和__try块的数量无关,而与包含__try块的函数有关(不管函数中有多少个__try块)。并且可以看到,RUNTIME_FUNCTION在.pdata中的排序方式是以BeginAddress为准的升序排序。

为什么这样排列?这是为了支持 O(log n) 时间复杂度的二分查找算法。当异常发生时,内核异常分发器(RtlLookupFunctionEntry)需要通过异常地址 RIP快速定位所属函数。二分查找是唯一能在对数时间内完成此任务的高效算法,而二分查找的前提条件就是数据集有序

注意,上面的三个地址都是相对于ImageBase的偏移量。所以尽管是在64位系统上,这三个地址也只有32位。

我们使用IDA来查看main函数对应的RUNTIME_FUNCTION条目。通过快捷键g就可以快速定位到main函数的RUNTIME_FUNCTION:

txt 复制代码
.pdata:000000014000400C                 dd rva main             ; FunctionStart
.pdata:0000000140004010                 dd rva byte_140001096   ; FunctionEnd
.pdata:0000000140004014                 dd rva stru_140002804   ; UnwindInfo

可以将前两个字段的值放到放到反汇编窗口中查看,就是main函数的起始和结束地址。

接下来说说UnwindData指向的UNWIND_INFO结构体。


UNWIND_INFO结构体

这个结构体十分关键,其中的内容是SEH的核心。UNWIND_INFO结构体定义如下:

cpp 复制代码
// Unwind info flags
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x01
#define UNW_FLAG_UHANDLER 0x02
#define UNW_FLAG_CHAININFO 0x04

// UNWIND_CODE 3 bytes structure
typedef union _UNWIND_CODE {
  struct {
    UBYTE CodeOffset;
    UBYTE UnwindOp : 4;
    UBYTE OpInfo : 4;
  };
  USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

typedef struct _UNWIND_INFO {
  UBYTE Version : 3;          // + 0x00 - Unwind info structure version
  UBYTE Flags : 5;            // + 0x00 - Flags (see above)
  UBYTE SizeOfProlog;         // + 0x01
  UBYTE CountOfCodes;         // + 0x02 - Count of unwind codes
  UBYTE FrameRegister : 4;    // + 0x03
  UBYTE FrameOffset : 4;      // + 0x03
  UNWIND_CODE UnwindCode[1];  // + 0x04 - Unwind code array
  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
  union {
    OPTIONAL ULONG ExceptionHandler;    // Exception handler routine
    OPTIONAL ULONG FunctionEntry;
  };
  OPTIONAL ULONG ExceptionData[];       // C++ Scope table structure
} UNWIND_INFO, *PUNWIND_INFO;

UNWIND_INFO结构体定义较为复杂,我们只关注其中的关键部分。

Flags字段

根据Flags的不同,UNWIND_INFO结构体的匿名联合体字段有不同的解释,ExceptionData字段是否有效也于Flags字段有关。
UNW_FLAG_EHANDLER
UNW_FLAG_UHANDLER
UNW_FLAG_CHAININFO
Flags = 0
UNWIND_INFO 起始地址
Header
UNWIND_CODE 数组

长度 = CountOfCodes * 2 字节
检查 Flags 值
附加异常处理信息

  1. 异常处理程序的 RVA

(如 __C_specific_handler)
2. 异常数据(变长)

#try块范围 + 处理程序地址
附加终止处理信息

  1. 终止处理程序的 RVA

(如 __C_specific_handler)
2. 异常数据(变长)

#try块范围 + __finally地址
附加链信息

  1. 起始地址(RVA)
  2. 结束地址(RVA)
  3. 链UNWIND_INFO的RVA
    结构结束

(无附加信息)

下面的UNWIND_INFO结构体的定义更好的展示了Flags字段的影响:

cpp 复制代码
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4

typedef struct _UNWIND_INFO {
    UBYTE Version         : 3;
    UBYTE Flags           : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister  : 4;
    UBYTE FrameOffset    : 4;
    UNWIND_CODE UnwindCode[1];
    union {
        //
        // If (Flags & UNW_FLAG_EHANDLER)
        //
        OPTIONAL ULONG ExceptionHandler;
        //
        // Else if (Flags & UNW_FLAG_CHAININFO)
        //
        OPTIONAL ULONG FunctionEntry;
    };
    //
    // If (Flags & UNW_FLAG_EHANDLER)
    //
    OPTIONAL ULONG ExceptionData[];
} UNWIND_INFO, *PUNWIND_INFO;

**为了快速讲清楚重点,这里我们只讨论Flags设置为UNW_FLAG_EHANDLER的情况。**一个典型的Flags被设置为UNW_FLAG_EHANDLER的函数如下:

cpp 复制代码
void ExceptFunc() { __try {} __except(1) {} }

当Flags设置为UNW_FLAG_EHANDLER时,此时union中的ExceptionHandler(编程语言相关)字段有效,由编译器负责填写。如果发生异常且指令指针为>= BeginAddress和< EndAddress,就调用该ExceptionHandler

ExceptionHandler会解析ExceptionData字段来确定如何处理发生的异常。

C_SCOPE_TABLE结构体

SCOPE译为范围,能力

ExceptionData是指向C_SCOPE_TABLE结构体的指针的偏移量(也就是指向C_SCOPE_TABLE的指针),该结构体定义如下(务必仔细阅读):

cpp 复制代码
// C Scope table entry
typedef struct _C_SCOPE_TABLE_ENTRY {
  ULONG Begin;        // +0x00 - Begin of guarded code block,__try块第一条指令的偏移量
  ULONG End;          // +0x04 - End of target code block,__try块内最后一条指令之后指令的偏移量
  ULONG Handler;      // +0x08 - Exception filter function (or "__finally" handler)
  ULONG Target;       // +0x0C - Exception handler pointer (the code inside __except block)
} C_SCOPE_TABLE_ENTRY, *PC_SCOPE_TABLE_ENTRY;

// C Scope table
typedef struct _C_SCOPE_TABLE {
  ULONG NumEntries;               // +0x00 - Number of entries
  C_SCOPE_TABLE_ENTRY Table[1];   // +0x04 - Scope table array
} C_SCOPE_TABLE, *PC_SCOPE_TABLE;

RUNTIME_FUNCTION描述了包含SEH的函数的整个范围,而SCOPE_TABLE描述了函数内每个单独的__try/__except块。

整体展示

cpp 复制代码
VOID FrobThePointer(PUCHAR UserAddress)
{
		__try
		{
        	*UserAddress = 0;
        	*UserAddress = 1;
 		}
 		__except (EXCEPTION_EXECUTE_HANDLER)
 		{
        	DbgPrint("Bad Address\n");
 		}
 }

以上面的代码为例,生成的汇编代码大致如下:

asm 复制代码
<00> mov     [rsp+0x8],rcx
<05> sub     rsp,0x28
<09> mov     rax,[rsp+0x30]        // Move UserAddress into RAX
<0e> mov     byte ptr [rax],0x0    // *UserAddress = 0;
<11> mov     rax,[rsp+0x30]        // Move UserAddress into RAX
<16> mov     byte ptr [rax],0x1    // *UserAddress = 1;
<19> jmp     FrobThePointer+0x28   // Success!
<1b> lea     rcx,"Bad Address\n"   // Begin of code in except block...
                                             //  prepare to DbgPrint
<22> call    DbgPrint
<27> nop
<28> add     rsp,0x28
<2c> ret

套到RUMTIME_FUNCTION和UNWIND_INFO结构体如下:

定位到异常位置的流程(以除0异常为例)

到现在为止,我们可以总结一下x64的SEH机制如何定位到发生异常的地方:假设发生异常的地址为EA,先通过二分查找在RUNTIME_FUNCTION(比较规则:BeginAddress < EA < EndAddress)定位到发生异常的函数,然后遍历ExceptionData指向的C_SCOPE_TABLE_ENTRY数组,定位到发生异常的try块(比较规则:Begin < EA < End)。

这是查找的基本流程,但是我们可以再深入一些。

每当发生异常时,就会调用内部 Windows 函数 RtlDispatchException。该函数在用户模式异常的 NTDLL 模块中实现,在内核模式异常的 NTOSKRNL 模块中实现,方式略有不同。该函数通过执行一些初始检查来开始执行:如果存在用户模式VEH,则将调用该异常处理程序;否则将进行标准 SEH 处理。
标准SEH处理:复制异常时的线程上下文,并利用RtlLookupFunctionEntry函数来执行一项重要任务:获取PE文件的ImageBase和RUNTIME_FUNCTION结构。


现在,我们来追踪一个 64 位 Windows 程序发生除零异常 (#DE) 后,从 CPU 陷阱到最终执行 __except 块的完整、详细的系统级调用链和数据结构操作。这将深入到内核和运行时库的内部。


第一阶段:硬件陷阱与内核接管

1. 触发异常

asm 复制代码
; 在用户态执行
mov eax, 1
cdq
idiv dword ptr [rcx]  ; 假设 [rcx]=0,触发 #DE

2. CPU 硬件操作

  • CPU 检测到除零,中断号 0 (#DE) 被触发。
  • CPU 自动切换到内核模式 (通过预设的 TSS 或 MSR 加载 GS 基址)。
  • CPU 将关键的用户态上下文 压入当前线程的内核栈 ,形成一个 KTRAP_FRAME 结构。这包括:
    • RIP:指向 idiv 的下一条指令。
    • RSP:用户态栈指针。
    • RFLAGSCSSS 等段寄存器。
    • 通用寄存器 RAXRCXRDX 等。
  • CPU 根据 IDT(中断描述符表) 的条目 0,跳转到预设的中断处理程序入口。在 Windows 中,这是 KiDivideErrorFault(或类似的陷阱处理程序)。

3. 内核陷阱处理 (KiDivideErrorFault)

c 复制代码
// 伪代码,位于 ntoskrnl.exe
VOID KiDivideErrorFault() {
    // 1. 建立更完整的陷阱帧
    KTRAP_FRAME* pTrapFrame = GetCurrentTrapFrame();
    
    // 2. 将异常信息打包为 EXCEPTION_RECORD
    EXCEPTION_RECORD ExceptionRecord = {0};
    ExceptionRecord.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO; // 0xC0000094
    ExceptionRecord.ExceptionAddress = pTrapFrame->Rip;
    ExceptionRecord.NumberParameters = 0;
    
    // 3. 调用公共的异常分发例程
    KiExceptionDispatch(&ExceptionRecord, pTrapFrame);
}

第二阶段:内核异常分发 (KiExceptionDispatch / KiDispatchException)

这是最核心的调度器。其内部逻辑复杂,但关键步骤如下:

1. 构建 CONTEXTEXCEPTION_POINTERS

c 复制代码
// 在 KiDispatchException 内部
CONTEXT ContextRecord = {0};
// 从 KTRAP_FRAME 填充 ContextRecord 的所有寄存器
RtlpCaptureContext(&ContextRecord, pTrapFrame);

EXCEPTION_POINTERS ExceptionPointers = {0};
ExceptionPointers.ExceptionRecord = &ExceptionRecord;
ExceptionPointers.ContextRecord = &ContextRecord;

2. 首次尝试:用户模式异常分发

内核检查异常地址,发现 RIP 在用户空间。于是调用 RtlDispatchException 函数(这是用户态异常分发的内核入口)。

c 复制代码
BOOLEAN RtlDispatchException(PEXCEPTION_RECORD pExceptionRecord, PCONTEXT pContextRecord) {
    // 【关键点1】检查 VEH(向量化异常处理程序)
    if (RtlCallVectoredExceptionHandlers(pExceptionRecord, pContextRecord)) {
        return TRUE; // VEH 处理了异常
    }
    
    // 【关键点2】定位 RUNTIME_FUNCTION
    PRUNTIME_FUNCTION pRuntimeFunction = RtlLookupFunctionEntry(
        pContextRecord->Rip,          // 控制点
        &ImageBase,                   // 输出:模块基址
        NULL                          // 历史表
    );
    
    if (pRuntimeFunction == NULL) {
        // 没有函数表,通常是 JIT 代码,无法展开
        return FALSE;
    }
    
    // 【关键点3】获取 UNWIND_INFO
    PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRuntimeFunction->UnwindInfoAddress);
    
    // 【关键点4】调用语言特定的异常处理程序(例如 __C_specific_handler)
    if (pUnwindInfo->Flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER)) {
        // 计算 ExceptionData 地址
        PVOID pHandlerData = RtlpGetHandlerData(pUnwindInfo);
        
        // 这个函数是 MSVC 运行时提供的,负责解析 SCOPE_RECORD
        EXCEPTION_DISPOSITION disposition = __C_specific_handler(
            pExceptionRecord,
            (PVOID)pContextRecord->Rip,
            pContextRecord,
            pHandlerData
        );
        
        if (disposition == ExceptionExecuteHandler) {
            // 找到了处理器,准备展开
            return TRUE;
        }
    }
    
    // 【关键点5】如果没有处理器,尝试展开一帧(用于 C++ 异常传播)
    RtlUnwindEx(...);
    return FALSE;
}

3. RtlLookupFunctionEntry 的查找过程

这个函数是第一阶段查找的具体实现。

c 复制代码
PRUNTIME_FUNCTION RtlLookupFunctionEntry(
    IN ULONG64 ControlPc,
    OUT PULONG64 ImageBase,
    IN OUT PT_RUNTIME_FUNCTION* HistoryTable
) {
    // 1. 通过 ControlPc 找到所属的 PE 模块 (DLL/EXE)
    PLDR_DATA_TABLE_ENTRY pModule = LdrFindEntryForAddress(ControlPc);
    *ImageBase = pModule->DllBase;
    
    // 2. 从 PE 头定位 .pdata 节
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(*ImageBase + ((PIMAGE_DOS_HEADER)*ImageBase)->e_lfanew);
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++, pSection++) {
        if (memcmp(pSection->Name, ".pdata", 6) == 0) {
            break;
        }
    }
    
    // 3. 计算 RUNTIME_FUNCTION 数组的起始和结束
    PRUNTIME_FUNCTION pFunctionTable = (PRUNTIME_FUNCTION)(*ImageBase + pSection->VirtualAddress);
    ULONG NumberOfFunctions = pSection->Misc.VirtualSize / sizeof(RUNTIME_FUNCTION);
    
    // 4. 【核心】二分查找
    return RtlpBinarySearch(pFunctionTable, ControlPc - *ImageBase, NumberOfFunctions);
}

4. __C_specific_handler 的工作(用户态回调!)

这是 MSVC 运行时库 (vcruntimexxx.dll) 提供的函数。注意:此时 CPU 仍在内核模式,但该函数是用户态代码,内核会临时切换到用户态执行它。

c 复制代码
EXCEPTION_DISPOSITION __C_specific_handler(
    PEXCEPTION_RECORD pExceptionRecord,
    PVOID EstablisherFrame,  // 实际上是发生异常的函数栈帧指针
    PCONTEXT pContextRecord,
    PEXCEPTION_REGISTRATION_RECORD pDispatcherContext
) {
    // 1. 从 DispatcherContext 中提取 SCOPE_RECORD 数组
    PSCOPE_RECORD pScopeRecords = (PSCOPE_RECORD)pDispatcherContext;
    DWORD scopeCount = pScopeRecords->Count;
    pScopeRecords++; // 指向第一个 SCOPE_RECORD
    
    // 2. 计算异常在函数内的偏移
    DWORD_PTR ImageBase = ...;
    PRUNTIME_FUNCTION pRF = ...;
    DWORD offsetInFunction = (DWORD)(pContextRecord->Rip - ImageBase - pRF->BeginAddress);
    
    // 3. 线性遍历 SCOPE_RECORD
    for (DWORD i = 0; i < scopeCount; i++) {
        if (offsetInFunction >= pScopeRecords[i].BeginAddress && 
            offsetInFunction < pScopeRecords[i].EndAddress) {
            // 4. 找到匹配的 try 块!调用过滤器
            if (pScopeRecords[i].HandlerAddress) {
                int filterResult = ((FILTER_FUNC)pScopeRecords[i].HandlerAddress)();
                if (filterResult == EXCEPTION_EXECUTE_HANDLER) {
                    // 5. 设置要跳转的目标地址
                    pDispatcherContext->TargetIp = ImageBase + pRF->BeginAddress + pScopeRecords[i].JumpTarget;
                    return ExceptionExecuteHandler;
                }
            }
        }
    }
    return ExceptionContinueSearch;
}

第三阶段:栈展开与最终派发

1. 内核收到 ExceptionExecuteHandler 后的操作

内核的 KiDispatchException 收到 RtlDispatchException 返回 TRUE,知道找到了处理程序。

c 复制代码
// 在 KiDispatchException 中
if (FirstPassSuccess) { // 即 RtlDispatchException 返回 TRUE
    // 1. 获取目标地址(由 __C_specific_handler 设置)
    ULONG64 TargetIp = GetTargetIpFromDispatcherContext();
    
    // 2. 执行栈展开
    RtlUnwindEx(
        TargetFrame,          // 展开到哪个帧
        TargetIp,            // 要跳转的目标地址
        pExceptionRecord,
        ReturnValue,         // 返回值
        pContextRecord,
        HistoryTable
    );
    
    // 3. 修改陷阱帧中的 RIP
    pTrapFrame->Rip = TargetIp;
    
    // 4. 【关键】恢复执行
    KiContinuePreviousMode(pContextRecord, pTrapFrame, PreviousMode);
}

2. RtlUnwindEx 的展开过程

这是实际的栈帧展开器,它解释执行 UNWIND_CODE 数组。

c 复制代码
VOID RtlUnwindEx(...) {
    // 循环展开每一帧,直到到达目标帧
    while (CurrentFrame < TargetFrame) {
        // 对当前帧,查找其 RUNTIME_FUNCTION
        PRUNTIME_FUNCTION pRF = RtlLookupFunctionEntry(CurrentRip, &ImageBase, NULL);
        PUNWIND_INFO pUnwindInfo = (PUNWIND_INFO)(ImageBase + pRF->UnwindInfoAddress);
        
        // 解释执行 UNWIND_CODE
        for (int i = 0; i < pUnwindInfo->CountOfCodes; i++) {
            switch (pUnwindInfo->UnwindCode[i].UnwindOp) {
                case UWOP_PUSH_NONVOL:
                    // 模拟 pop 操作,恢复寄存器
                    Context->Rsp += 8;
                    Context->Rbx = *(PULONG64)(Context->Rsp - 8);
                    break;
                case UWOP_ALLOC_SMALL:
                    Context->Rsp += (pUnwindInfo->UnwindCode[i].OpInfo * 8) + 8;
                    break;
                // ... 其他展开操作码
            }
        }
        
        // 如果有终止处理器(__finally),现在调用它
        if (pUnwindInfo->Flags & UNW_FLAG_UHANDLER) {
            CallTerminationHandler(pUnwindInfo, Context);
        }
        
        // 移动到调用者帧
        CurrentRip = *(PULONG64)Context->Rsp;
        Context->Rsp += 8;
    }
}

第四阶段:返回用户态,执行处理块

1. 最终返回 (KiContinuePreviousMode)

内核将修改后的 CONTEXT 写回陷阱帧,然后执行 iretq(或 sysret)指令,返回到用户态 。但返回的 RIP 不是原来的 idiv 之后,而是 __except 块的地址。

2. 用户态恢复执行

asm 复制代码
; 这是用户态,但 RIP 已指向 __except 块
; 假设 __except 块地址是 0x140001050
0x140001050: mov rcx, str_inside_except
0x140001057: call printf
; ... 异常处理代码

关键数据结构和调用链总结:

阶段 关键函数 关键数据结构 操作
硬件陷阱 KiDivideErrorFault KTRAP_FRAME 保存用户上下文
内核分发 KiDispatchException EXCEPTION_RECORD, CONTEXT 构建异常信息
用户分发 RtlDispatchException - 协调分发流程
函数查找 RtlLookupFunctionEntry RUNTIME_FUNCTION 数组 二分查找函数
展开信息 - UNWIND_INFO, UNWIND_CODE 描述栈帧布局
作用域查找 __C_specific_handler SCOPE_RECORD 数组 线性查找 try 块
栈展开 RtlUnwindEx UNWIND_CODE 解释执行展开指令
返回 KiContinuePreviousMode KTRAP_FRAME 恢复用户态上下文

调用链总结如下:

txt 复制代码
KiUserExceptionDispatcher // Windows Kernel Internal (KI) API
  -> RtlDispatchException // main logic for exception handling
    -> RtlpCallVectoredHandlers // call any VEH
    -> RtlLookupFunctionEntry // look for valid PRUNTIME_FUNCTION entry in ExceptionDirectory
      -> RtlpLookupDynamicFunctionEntry // if no valid PRUNTIME_FUNCTION, run any dynamic callbacks
    -> RtlVirtualUnwind / RtlpxVirtualUnwind // perform stack frame unwinding
    -> RtlpExecuteHandlerForException // execute exception handler!

UNWIND_CODE结构体

在UNWIND_INFO结构体中有一个UnwindCode字段,指向UNWIND_CODE结构体。

UNWIND_CODE 结构体是 64 位 SEH 机制中的"展开脚本"或"逆操作指令集" 。它不直接参与异常处理的决策 (这是 SCOPE_RECORD 的工作),而是负责异常处理流程中的执行环节------即如何安全、正确地将栈帧恢复到函数调用前的状态。这是实现栈展开(Stack Unwind)的基石。


一、UNWIND_CODE 的核心角色:栈帧的"构建蓝图"与"拆卸手册"

你可以将函数调用过程想象成搭积木:

  • 函数序言 (Prolog):是"搭积木"的过程。它在栈上分配空间、保存寄存器。
  • 函数尾声 (Epilog):是"拆积木"的过程。它恢复寄存器、释放栈空间,然后返回。
  • 异常发生时 :程序突然中断,积木搭到一半。UNWIND_CODE 就是一份紧急拆卸手册,告诉系统如何从这个"半成品"状态,安全地拆回到调用前的样子,而不管积木搭到了哪一步。

关键点 :异常可能发生在函数序言之后、尾声之前的任何位置 。系统必须能够从任意点 将栈恢复到函数入口之前的状态。UNWIND_CODE 提供了完成此操作所需的精确、逐步的逆操作指令


二、UNWIND_CODE 结构体详解

定义位于 winnt.h

c 复制代码
typedef union _UNWIND_CODE {
    struct {
        UBYTE CodeOffset;  // 距离序言开始的偏移量(以字节为单位)
        UBYTE UnwindOp:4;  // 展开操作码
        UBYTE OpInfo:4;    // 操作码附加信息
    };
    USHORT FrameOffset;    // 当 UnwindOp 为某些值时,整个USHORT表示帧偏移
} UNWIND_CODE, *PUNWIND_CODE;

字段含义

  • CodeOffset这是最关键、最精妙的设计。 它表示本展开操作应该在函数序言的哪个"进度点"之后执行 。系统通过比较异常地址(RIP)与函数起始地址的偏移量,来决定执行 UNWIND_CODE 数组中的哪些指令。这确保了无论异常发生在序言后、函数体中还是尾声前,栈都能被正确恢复。
  • UnwindOp :展开操作码。定义了具体的恢复操作(如 pop 寄存器、增加 RSP 等)。
  • OpInfo:操作信息,通常是寄存器编号或大小参数。

三、UNWIND_CODE 在异常处理流程中的具体工作

让我们结合一个具体例子,跟踪流程。假设有以下函数:

c 复制代码
// 对应的汇编序言
MyFunc:
    push    rbx        ; 保存非易失寄存器
    push    rsi
    sub     rsp, 0x20  ; 分配局部变量空间
    ...                ; 函数体

编译器会为它生成以下逻辑的 UNWIND_CODE 数组(伪代码表示):

c 复制代码
UNWIND_CODE UnwindCode[] = {
    {CodeOffset: 0x0, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RBX}, // 对应 push rbx
    {CodeOffset: 0x1, UnwindOp: UWOP_PUSH_NONVOL, OpInfo: RSI}, // 对应 push rsi
    {CodeOffset: 0x2, UnwindOp: UWOP_ALLOC_SMALL, OpInfo: 4}    // 对应 sub rsp,0x20 (4*8=0x20)
};

注意CodeOffset 是累加的。第一条对应序言第0字节(push rbx),第二条对应第1字节(push rsi),第三条对应第2字节(sub rsp,0x20)。

场景分析:异常发生在不同位置

场景1:异常发生在序言之后,函数体内部 (例如在 sub rsp, 0x20 之后)

  1. 系统计算 RIP 偏移,假设为 0x05
  2. 遍历 UNWIND_CODE 数组,执行所有 CodeOffset <= 0x05 的指令(即全部三条):
    • 执行逆操作:add rsp, 0x20 (恢复栈分配)
    • 执行逆操作:模拟 pop rsi (从栈上恢复 rsi 的值到 CONTEXT 结构)
    • 执行逆操作:模拟 pop rbx (恢复 rbx
  3. 栈指针 RSP 和寄存器 RBXRSI 被恢复到进入函数时的状态。

场景2:异常发生在序言执行过程中 (例如在 push rsi 之后,sub rsp,0x20 之前)

  1. RIP 偏移假设为 0x01
  2. 只执行 CodeOffset <= 0x01 的指令(前两条):
    • 执行逆操作:模拟 pop rsi
    • 执行逆操作:模拟 pop rbx
  3. 注意UWOP_ALLOC_SMALL 对应的操作(add rsp,0x20不会被执行 ,因为对应的 sub rsp,0x20 指令还没执行!这就是 CodeOffset 机制的精髓:只撤销那些已经执行了的序言操作

场景3:用于 __finally 的展开

当为 __finally 展开时,过程相同。UNWIND_CODE 确保栈被正确恢复,然后系统会跳转到 __finally 块执行清理代码,最后继续展开。


四、主要的 UnwindOp 操作码及其逆操作

操作码 含义 对应的序言指令示例 展开时的逆操作(由 RtlUnwindEx 执行)
UWOP_PUSH_NONVOL 压入非易失寄存器 push rbx 从栈上弹出值,并恢复 CONTEXT 中对应寄存器的值RSP += 8
UWOP_ALLOC_SMALL 分配小栈空间 sub rsp, 0x20 RSP += 分配大小
UWOP_ALLOC_LARGE 分配大栈空间 sub rsp, 0x1000 RSP += 分配大小
UWOP_SAVE_NONVOL 保存非易失寄存器到栈上 mov [rsp+0x10], rbx 从栈上指定偏移处读取值,恢复 CONTEXT 中对应寄存器的值
UWOP_SAVE_XMM128 保存 XMM 寄存器 movaps [rsp+0x20], xmm6 恢复 CONTEXT 中的 XMM 寄存器。
UWOP_SET_FPREG 建立帧指针 mov rbp, rsp RBP 恢复为 CONTEXT 中保存的调用者值。
UWOP_PUSH_MACHFRAME 压入机器帧(用于硬件中断) 由硬件中断自动完成 特殊中断恢复。

重要提示 :展开时的"pop"操作是逻辑上的。实际过程是:

  1. 从当前 RSPCodeOffset 计算原始值在栈上的位置。
  2. 将该值读入 CONTEXT 结构中的对应寄存器字段。
  3. 更新 CONTEXT 中的 RSP 值,模拟栈指针移动。

栈内存本身的内容不会被修改 ,只是 CONTEXT 被更新,为后续恢复执行做准备。


相关推荐
jessecyj3 分钟前
【RabbitMQ】超详细Windows系统下RabbitMQ的安装配置
windows·分布式·rabbitmq
开开心心就好3 小时前
系统重装前必备的智能驱动备份工具
windows·计算机视觉·计算机外设·excel·模块测试·csdn开发云·威胁分析
男孩李4 小时前
Windows 系统下WorkBuddy安装指南
windows·语言模型
香蕉鼠片13 小时前
跨平台开发到底是什么
linux·windows·macos
心一信息15 小时前
Windows 计算机管理 · 事件日志完整运维指南
windows
不吃香菜56719 小时前
cloudcode入门学习
java·windows·cloudcode
liu****20 小时前
LangGraph-AI应用开发框架(二)
windows·langchain·大模型·工作流·langgraph
一个人旅程~1 天前
linuxmint如何使用iphone手机上网以及如何管理iphone手机的照片和文件?需要下载哪些基础包和依赖?
linux·windows·经验分享·电脑
黑风风1 天前
在 Windows 上设置 MAVEN_HOME 环境变量(完整指南)
java·windows·maven
seabirdssss1 天前
Flutter 开发环境配置
android·windows·flutter·adb