异常和中断1

本文深入探讨Windows系统下的异常处理机制,包括SEH、VEH、UEH和VCH等不同层面的处理方式,解析异常处理的优先级及处理过程,并提供SEH原理的详细解释与实践案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异常和中断

  • 中断一般都是由外部硬件引发的。
  • 而异常一般都是由软件触发的(例如 int3,除0中断,访问空指针的指令)
  • IDT表中包含了所有中断处理函数和异常处理函数,当CPU产生异常或中断的时候, 会根据异常编号或中断编号读取IDT表中的处理函数的地址, 然后调用这个函数来处理异常或中断. 这些函数是由操作系统所填充的.
  • 一般分为三类
    • 错误类
    • 陷阱类
    • 终止类

Windows提供的异常处理机制

SEH

终结处理器:

__try__finally

  1. 终结处理器 : 无论try块的代码以何种方式离开, 都会保证finally块的代码被执行.
  2. 如果是以return,break,continue,goto方式离开的, 统称为非正常离开方式, 编译器会产生额外的代码来保证finally块的执行. 因此不建议使用这些方式离开try块.
  3. 如果希望中途离开try块, 可以使用__leave来离开, 这种方式不会产生额外的代码
int main()
{
	printf("进入main函数\n");
	__try {
		printf("进入__try块\n");
		//__leave;
	}
	__finally {
		printf("进入__Finally块\n");
	}
	printf("离开main函数");
}

异常处理器:

__tryexcept

  1. except有一个异常过滤表达式, 这个表达式的值决定了产生异常之后程序的执行位置.
    1. 值为1 , 执行except块的代码 : EXCEPTION_EXCUTE_HANDLER
    2. 值为0, 搜索上一层的except块:EXCEPTION_CONTINUE_SEARCH
    3. 值为-1, 继续执行产生异常的那条指令:EXCEPTION_CONTINUE_EXECUTION
  2. 可以通过GetExceptionInformation来获取异常的信息, 这个函数会返回一个EXCEPTION_POINTERS结构体的指针
typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord; // 异常记录,包含了异常的类型,触发异常的汇编指令的地址
    PCONTEXT ContextRecord; // 产生异常时的线程上下文
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

#include "pch.h"
#include <windows.h>
#include <stdio.h>


DWORD seh_filter()
{
	printf("SEH异常过滤函数seh_filter被调用\n");
	return EXCEPTION_CONTINUE_EXECUTION;	//继续执行产生异常的指令
}

int main()
{
	printf("进入main函数\n");
	// 1. 执行except块
  __try{
	  *(int*)0 = 0;// 触发异常
  }
  /*执行异常处理块(except块)*/
  __except (EXCEPTION_EXECUTE_HANDLER)	//执行except块
  {
	  printf("触发了一个异常\n");
  }


  // 2. 重新执行产生异常的代码
  //__try {
	 // *(int*)0 = 0;// 触发异常
  //}
  // /*先调用函数,然后使用函数的返回值来决定执行方向*/
  //__except (seh_filter()) {	//返回值为EXCEPTION_CONTINUE_EXECUTION,既重复执行产生异常的代码段,
  //并不会执行excpet代码块
	 // printf("触发了一个异常\n");
  //}


  // 3. 搜索上一层
  __try {

	  __try {
		  *(int*)0 = 0;// 触发异常
	  }
	  /*搜索上一层try...except块*/
	  __except (EXCEPTION_CONTINUE_SEARCH) {
//搜索上一层异常处理
	  }

  }
  /*执行异常处理块*/
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
	  printf("main函数的except块被执行\n");
  }



  printf("离开main函数\n");
}
  1. 异常信息结构体中的线程上下文
#include "pch.h"
#include <windows.h>
#include <stdio.h>

int g_nNum;

void test_fun()
{
	MessageBox(0, 0, 0, 0);
	//需要退出进程
	ExitProcess(0);
}

DWORD seh_filter(EXCEPTION_POINTERS* exp)
{
	printf("SEH异常过滤函数seh_filter被调用\n");
	//异常代码和产生异常的地址
	printf("异常代码:%08X , 异常地址:%08X\n",
		exp->ExceptionRecord->ExceptionCode,
		exp->ExceptionRecord->ExceptionAddress);
	//线程上下文
	printf("EIP=%08X EAX=%08X ECX=%08X\n",
		exp->ContextRecord->Eip,
		exp->ContextRecord->Eax,
		exp->ContextRecord->Ecx);

	// 修改线程上下文, 修改的其实是内存中的一个记录
	// 异常处理完毕之后,会执行iret指令, 这条指令会
	// 将内存中的线程上下文恢复到硬件的寄存器中.
	// 因此改掉了此处的eax的值,就改掉了硬件中的寄存器.
	exp->ContextRecord->Eax = (DWORD)&g_nNum;
	// 在异常过滤函数中,修改eip,到时候返回到触发异常的
	// 指令时, 实际是由eip的值来决定大的, eip指向了哪里
	// 异常出来完毕之后就会回到哪里
	exp->ContextRecord->Eip = (DWORD)&test_fun;
	//返回继续执行异常的代码块
	return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{
	printf("进入main函数\n");
	// 1. 执行except块
  __try{
	  *(int*)0 = 0;// 触发异常
  }
  /*执行异常处理块(except块)*/
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
	  printf("触发了一个异常\n");
  }


  // 2. 重新执行产生异常的代码
  __try {
	  //*(int*)0 = 0;// 触发异常
	  _asm xor eax, eax;
	  _asm mov [eax], 0;
  }
   /*先调用函数,然后使用函数的返回值来决定执行方向*/
  __except (seh_filter(GetExceptionInformation())) {
	  printf("触发了一个异常\n");
  }


  // 3. 搜索上一层
  __try {

	  __try {
		  *(int*)0 = 0;// 触发异常
	  }
	  /*搜索上一层try...except块*/
	  __except (EXCEPTION_CONTINUE_SEARCH) {

	  }

  }
  /*执行异常处理块*/
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
	  printf("main函数的except块被执行\n");
  }
  printf("离开main函数\n");
}

VEH

  1. 通过AddVectoredExceptionHandler函数来注册异常处理函数.
  2. 产生异常时, 系统会遍历VEH链表来调用异常处理函数. 异常函数必须返回两种值, 来决定异常处理的下一步的执行位置.
    1. 继续执行(-1) : EXCEPTION_CONTINUE_EXECUTION
    2. 继续搜索(0) : EXCEPTION_CONTINUE_SEARCH
      1. 系统就会找到VEH链表中的下一个处理函数.当VEH链没有下一个的时候,系统才会去找SEH
#include "pch.h"
#include <windows.h>
#include <stdio.h>

LONG WINAPI veh_handler(EXCEPTION_POINTERS* exp)
{
	printf("veh_handler(继续执行)\n");
	return EXCEPTION_CONTINUE_EXECUTION;
}

LONG WINAPI veh_handler2(EXCEPTION_POINTERS* exp)
{
	printf("veh_handler(继续搜索)\n");
	return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
	//先往头部插入veh_handler2这个异常处理函数
	AddVectoredExceptionHandler(TRUE, veh_handler2);
	//再往头部插入veh_handler这个异常处理函数
	AddVectoredExceptionHandler(TRUE, veh_handler);
	//最终顺序就是veh_handler->veh_handler2
	//因为veh_handler的返回值是EXCEPTION_CONTINUE_EXECUTION
	//所以压根不会执行veh_handler2
	*(int*)0 = 0;
}

UEH

  1. 顶级异常处理函数 : 当VEH,SEH的异常处理函数都返回搜索上一个的话, 才会找到UEH.
  2. 在64位系统下, 程序处于调试状态, UEH的过滤函数不会被调用.但是在运行态会被调用.在32为系统下没有影响.
#include "pch.h"
#include <windows.h>
#include <stdio.h>

LONG WINAPI ueh_filter(EXCEPTION_POINTERS* exp)
{
	printf("UEH过滤函数被调用\n");
	return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
	// 设置UEH
	// 顶级异常处理器
	SetUnhandledExceptionFilter(ueh_filter);//注册UEH过滤函数
	*(int*)0 = 0;
	printf("main\n");
}

VCH

  1. 注册和VEH是一样:,但是注册的API不一样, 它使用API来注册AddVectoredContinueHandler
  2. 不是一个正经的异常处理器.

异常处理机制的优先级

VEH -> SEH -> UEH

异常处理机制的处理过程

在这里插入图片描述
如果UEH都无法处理的话就会提交给系统,让系统处理。

SEH的原理

  1. 异常过滤函数和异常处理函数的区别:
    1. SEH的异常处理函数是由系统直接调用,由编译器生成并注册。
    2. SEH的异常过滤函数其实是由编译器生成的代码所调用,异常过滤函数就是__except(异常过滤函数)。
      流程:
      1. 系统调用SEH的异常处理函数(由编译器生成) , 异常处理函数再调用异常过滤函数.
  2. 如何直接注册一个SEH的异常处理函数(而不是异常过滤函数)
    1. SEH的异常处理函数保存在哪里? 怎么找到?
      1. 这条链表的首地址就保存在TEB结构中.
      2. 保存的位置只能是栈空间. 使用一个链表来保存, 每个节点的结构:
    typedef struct _EXCEPTION_REGISTRATION_RECORD {
        struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个SEH结构体的指针
        PEXCEPTION_ROUTINE Handler; // SEH的异常处理函数
    } EXCEPTION_REGISTRATION_RECORD;
    

TEB

  1. 线程环境块(Thread Environment Block)
  2. 保存了和一个线程相关的大量的信息.
  3. Windows将TEB结构的变量保存在内存中的一个位置上. 这个地址是无法直接获取到的. 但是通过FS段寄存器可以找到这个结构体. FS段寄存器的段基地址正是这个结构体的首地址.
  4. SEH异常链最后一个节点的地址是-1;

遍历SEH异常链

EXCEPTION_REGISTRATION_RECORD* head = NULL;
//1. 得到头节点地址
	_asm {
		push dword ptr fs : [0];
		pop dword ptr head;
	}
	printf("节点地址 | 处理函数 \n");
	while (head !=
		(EXCEPTION_REGISTRATION_RECORD*)-1)
	{
		printf("%08X | %08X\n",
			head,
			head->Handler);

		head = head->Next;
	}

手工安装SEH节点

//SEH处理函数的格式
EXCEPTION_DISPOSITION NTAPI seh_handler(
	_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
	_In_ PVOID EstablisherFrame,
	_Inout_ struct _CONTEXT *ContextRecord,
	_In_ PVOID DispatcherContext
)
{
	printf("seh_handler\n");
	return ExceptionContinueExecution;
}
int main{

// 纯汇编:
	_asm
	{
		push seh_handler;        // Handler
		push dword ptr fs : [0]; // Next
		//这个时候esp指向NEXT
		//esp+4指向异常处理函数地址,所以将esp赋值给fs:[0]
		//头节点就成为我们创建的异常处理函数了。
		mov fs : [0], esp;
	}
	*(int*)0 = 0;

	// 卸载seh节点
	_asm
	{
		pop dword ptr fs : [0];
		add esp, 4;
	}
}

C++内联汇编版本:

// 安装一个处理函数
	EXCEPTION_REGISTRATION_RECORD seh_node = { 0 };
	seh_node.Handler = seh_handler;
	printf("seh_handler : %08X\n", seh_handler);
	// seh_node.Handler; = fs:[0];
	_asm
	{
		push  dword ptr fs : [0];
		pop dword ptr[seh_node.Next];
	}

	// fs:[0] = &seh_node;
	_asm
	{
		lea eax, seh_node;
		mov fs : [0], eax;
	}

找出程序的SEH处理函数

  • 编译器生成的__try...__except代码是默认使用except_handler4来作为异常处理函数. 异常过滤表达式, except块的代码都是被except_handler4函数所间接调用的.
  • 在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值