windows异常
Windows异常是什么
CPU执行写内存指令时发现地址无效,产生异常
|
1 2 3 4 5 |
int* p = nullptr; *p = 10; // `Windows` 将它转换成类似 // EXCEPTION_ACCESS_VIOLATION |
- 异常产生后,
Windows会构造EXCEPTION_RECORD:异常码、异常地址、附加参数CONTEXT:异常发生时的寄存器状态,例如RIP/RSP/RAX
|
1 2 |
EXCEPTION_RECORD CONTEXT |
- 常见异常码
| 异常 | 含义 |
EXCEPTION_ACCESS_VIOLATION |
非法内存访问 |
EXCEPTION_INT_DIVIDE_BY_ZERO |
整数除零 |
EXCEPTION_ILLEGAL_INSTRUCTION |
执行非法指令 |
EXCEPTION_BREAKPOINT |
触发断点 |
EXCEPTION_STACK_OVERFLOW |
栈溢出 |
EXCEPTION_ARRAY_BOUNDS_EXCEEDED |
数组越界异常 |
0xE06D7363 |
MSVC C++ 异常常见异常码 |
SEH:结构化异常处理
Structured Exception Handling,结构化异常处理- 和函数调用栈绑定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// SEH 的基本写法 #include <Windows.h> #include <iostream> int main() { __try { int* p = nullptr; *p = 10; } __except (EXCEPTION_EXECUTE_HANDLER) { std::cout << "捕获到 SEH 异常\n"; } } |
MSVC专用关键字- 它不是标准
C++语法
- 它不是标准
|
1 2 3 |
__try __except __finally |
__except的过滤表达式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 更合理的写法是检查异常类型 __try { int* p = nullptr; *p = 10; } __except ( GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { std::cout << "发生访问违规\n"; } |
|
1 2 3 4 5 |
// 过滤表达式可以返回三个值 EXCEPTION_EXECUTE_HANDLER 当前层处理异常 EXCEPTION_CONTINUE_SEARCH 当前层不处理,继续找外层 EXCEPTION_CONTINUE_EXECUTION 认为问题已修复,重新执行或继续执行 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 例如多层 SEH __try { __try { int* p = nullptr; *p = 10; } __except (EXCEPTION_CONTINUE_SEARCH) { // 不会执行 } } __except (EXCEPTION_EXECUTE_HANDLER) { std::cout << "由外层处理\n"; } |
- 获取异常详细信息
|
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 |
int filter_exception( EXCEPTION_POINTERS* info) { DWORD code = info->ExceptionRecord->ExceptionCode; void* address = info->ExceptionRecord->ExceptionAddress; if (code == EXCEPTION_ACCESS_VIOLATION) { return EXCEPTION_EXECUTE_HANDLER; } return EXCEPTION_CONTINUE_SEARCH; } void test() { __try { int* p = nullptr; *p = 10; } __except ( filter_exception(GetExceptionInformation())) { // 处理异常 } } |
|
1 2 3 4 5 6 7 8 |
// EXCEPTION_POINTERS 中包含 typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS; // 调试器和崩溃转储工具经常使用这些信息生成调用栈和 minidump |
__finally
|
1 2 3 4 5 6 7 8 |
// SEH 还提供 __try { // 执行操作 } __finally { // 无论正常退出还是异常退出都会执行 } |
|
1 2 3 4 5 6 7 8 9 10 |
HANDLE handle = CreateFileW(/* ... */); __try { // 使用 handle } __finally { if (handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); } } |
- 为什么说
SEH是“栈式的”
|
1 2 3 4 5 6 7 8 |
// 从逻辑上看,SEH 处理器与函数调用栈和代码作用域绑定 main └─ function_a └─ function_b ← 这里发生异常 // 系统首先找 function_b 的处理器,然后向外找 // function_b → function_a → main |
- 需要注意实现差异:
32位x86传统实现常通过线程异常处理链管理64位Windows主要使用.pdata/.xdata中的函数表和展开信息
VEH:向量化异常处理
- 概述
- 进程级异常观察器
Vectored Exception Handling,向量化异常处理
VEH不使用__try/__except,而是注册一个全局回调
|
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 |
#include <Windows.h> LONG CALLBACK vectored_handler( PEXCEPTION_POINTERS info) { DWORD code = info->ExceptionRecord->ExceptionCode; if (code == EXCEPTION_ACCESS_VIOLATION) { // 可以记录异常信息 } return EXCEPTION_CONTINUE_SEARCH; } int main() { PVOID handler = AddVectoredExceptionHandler( 1, vectored_handler); int* p = nullptr; *p = 10; RemoveVectoredExceptionHandler(handler); } |
- 注册函数
|
1 2 3 4 5 6 7 |
PVOID AddVectoredExceptionHandler( ULONG First, PVECTORED_EXCEPTION_HANDLER Handler); // First 参数 // 1 // 尽量放在处理器列表前面 // 0 // 放在处理器列表后面 |
VEH处理器通常返回
|
1 2 |
EXCEPTION_CONTINUE_SEARCH EXCEPTION_CONTINUE_EXECUTION |
VEH没有对应某一个函数作用域,它是:- 进程级注册
- 可以看到进程中不同线程产生的异常
- 不依赖异常发生函数是否写了
__try - 在
SEH栈处理器之前得到通知
- 因此可以把
VEH理解成:- 在
Windows正式沿调用栈寻找SEH处理器以前,先调用一组进程级异常回调
- 在
VEH 和 SEH 的执行顺序
- 流程
- 示例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
LONG CALLBACK handler( PEXCEPTION_POINTERS) { OutputDebugStringW(L"VEH\n"); return EXCEPTION_CONTINUE_SEARCH; } int main() { auto token = AddVectoredExceptionHandler(1, handler); __try { int* p = nullptr; *p = 1; } __except (EXCEPTION_EXECUTE_HANDLER) { OutputDebugStringW(L"SEH\n"); } RemoveVectoredExceptionHandler(token); } |
|
1 2 3 4 5 6 7 8 9 |
// 执行顺序通常是: 发生访问违规 ↓ VEH 被调用 ↓ 返回 CONTINUE_SEARCH 搜索 SEH ↓ 进入 __except |
- 如果
VEH返回EXCEPTION_CONTINUE_EXECUTION- 则表示
VEH声称已经修复问题,系统不会继续搜索SEH
- 则表示
- 但是如果没有真正修复异常原因,程序可能立即在原位置再次异常
- 因此不能为了“让程序不崩溃”就随便返回
EXCEPTION_CONTINUE_EXECUTION
- 因此不能为了“让程序不崩溃”就随便返回
|
1 2 3 4 5 6 |
执行错误指令 → VEH → CONTINUE_EXECUTION → 再执行错误指令 → VEH → 无限循环 |
两者最核心的区别
| 对比项 | VEH |
SEH |
| 全称 | Vectored Exception Handling |
Structured Exception Handling |
| 使用方式 | 注册回调函数 | __try/__except |
| 作用范围 | 整个进程 | 当前代码作用域和调用栈 |
| 是否绑定函数栈帧 | 否 | 是 |
| 执行时机 | SEH 搜索之前 |
沿调用栈搜索时 |
| 是否适合局部恢复 | 通常不适合 | 比较适合 |
| 是否适合统一监控 | 适合 | 不方便 |
| 常见用途 | 调试器、监控、Hook、崩溃记录 | 局部处理系统异常 |
| 生命周期管理 | 必须主动移除 | 离开作用域自然消失 |
- 直白地记
VEH是进程入口处的“全局异常观察器”SEH是函数调用栈上的“局部异常处理器”
第一次机会和第二次机会异常
- 在调试器中经常能看到
|
1 2 |
// 它不代表程序一定会崩溃 First-chance exception |
- 第一次机会
- 异常刚刚发生,
Windows首先通知调试器 - 如果程序自己的
VEH/SEH可以处理,程序可以继续运行 Visual Studio中看到“发生异常”后停住,有时只是因为调试器选择在第一次机会异常时中断
- 异常刚刚发生,
|
1 2 3 4 |
异常发生 → 调试器第一次机会 → VEH → SEH |
- 第二次机会
- 如果
VEH和SEH都没有处理 - 第二次机会异常通常意味着程序马上要崩溃
- 如果
|
1 2 3 4 |
没有任何处理器愿意处理 → 调试器第二次机会 → 仍未处理 → 进程终止 |
SEH和 C++异常 try/catch 的关系
- 标准
C++异常
|
1 2 3 4 5 |
try { throw std::runtime_error("error"); } catch (const std::exception& e) { } |
SEH
|
1 2 3 4 5 6 |
__try { int* p = nullptr; *p = 1; } __except (EXCEPTION_EXECUTE_HANDLER) { } |
- 不是同一套语言机制
C++ 异常 |
Windows SEH |
try/catch/throw |
__try/__except |
标准 C++ |
Windows/MSVC 专用 |
| 主要处理程序主动抛出的错误 | 主要处理硬件和系统级异常 |
| 有明确的对象类型 | 主要通过异常码区分 |
自动执行 C++ 栈展开语义 |
行为受编译选项和平台机制影响 |
MSVC底层会借助Windows异常分发机制实现C++异常,但这不意味着二者在语言层面等价
windows异常和C++异常谁先触发
- 在
Windows + MSVC下,不能简单理解为“Windows异常和C++异常各触发一次” - 更准确地说:
MSVC的C++异常底层借助Windows异常分发机制实现,所以底层Windows异常分发先发生,之后才进入对应的C++ catch
- 主动
throw时
|
1 2 3 4 5 |
try { throw std::runtime_error("error"); } catch (const std::exception& e) { } |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 大致流程是 执行 throw → MSVC 调用 _CxxThrowException → 产生 Windows 异常(通常为 0xE06D7363) → 调试器收到第一次机会异常 → VEH 被调用 → Windows 沿调用栈搜索异常处理器 → C++ 运行时识别异常对象和类型 → 执行栈展开和析构函数 → 进入匹配的 C++ catch |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 因此下面的 `VEH` 能先观察到 `C++` 异常: LONG CALLBACK veh_handler( EXCEPTION_POINTERS* info) { if (info->ExceptionRecord->ExceptionCode == 0xE06D7363) { OutputDebugStringW(L"观察到 C++ 异常\n"); } return EXCEPTION_CONTINUE_SEARCH; } AddVectoredExceptionHandler(1, veh_handler); try { throw std::runtime_error("test"); } catch (...) { OutputDebugStringW(L"进入 C++ catch\n"); } // VEH 观察到 C++ 异常 // → C++ catch 处理异常 |
- 访问违规等硬件异常时
|
1 2 3 4 5 6 |
try { int* p = nullptr; *p = 10; } catch (...) { } |
|
1 2 3 4 5 6 7 |
// 首先产生的是 Windows 硬件异常 CPU 检测到非法内存访问 → Windows 转换为 EXCEPTION_ACCESS_VIOLATION → 调试器第一次机会 → VEH → 搜索栈式异常处理器 |
- 能否进入
C++ catch(...),取决于编译选项/EHsc:通常不会把访问违规交给C++ catch(...)/EHa:允许catch(...)捕获SEH异常,但通常不建议捕获访问违规后继续运行
如果同时存在 SEH 和 C++ catch
- 究竟哪个处理,不是简单由“
SEH比C++快”决定,而要看:- 哪个处理器在调用栈上更靠近异常点
- 是否匹配异常类型
SEH过滤器返回什么MSVC的异常编译选项- 是否继续搜索
- 但
VEH比栈式SEH和C++ catch更早
|
1 2 3 4 5 |
异常产生 → 调试器第一次机会 → VEH → 沿调用栈搜索 SEH/C++ 处理器 → 匹配的 __except 或 catch |
Windows其他
消息队列
exe包大小最大多少
- 现代
Windows上一个能够被正常加载执行的64位EXE(PE32+)- 它的加载映像大小理论上最大是
2 GB,准确说必须小于或不超过Windows/链接器允许的2 GB边界 - 微软明确规定,
PE32+虽然使用64位地址空间,但EXE/DLL的单个可执行映像仍限制在2 GB
- 它的加载映像大小理论上最大是
- 磁盘上的
EXE文件大小- 是资源管理器看到的文件大小
|
1 2 |
// 扩展名叫 .exe 的普通文件 // 文件系统并不因为扩展名是 .exe 就设置特殊限制。它和 .txt、.zip 一样,大小上限由文件系统决定 |
| 文件系统 | 单文件理论上限 |
FAT32 |
约 4 GiB,实际最大为 4 GiB - 1 byte |
NTFS |
格式理论值 2^64 - 1 字节,实际受 Windows版本、卷大小等限制 |
exFAT |
格式理论值 2^64 - 1 字节 |
|
1 2 |
// 因此在 NTFS 或 exFAT 上,完全可以创建一个超过 4 GB、文件名以 .exe 结尾的文件 // 但它不一定能被 Windows 当作有效程序执行 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Windows 能够正常加载的 PE EXE // PE 格式中的很多“文件位置”和“数据大小”字段都是32位 DWORD PointerToRawData; DWORD SizeOfRawData; // 例如每个节都通过下面两个字段描述磁盘位置 PointerToRawData:节在文件中的偏移 SizeOfRawData:节在文件中的大小 // 32位无符号整数的范围是: 0 ~ 0xFFFFFFFF // 也就是最多描述前 4 GiB // 对于现代64位 PE32+,限制更严格 // 映射到进程地址空间的 EXE/DLL 映像最大约为 2 GB // 也就是说,如果代码、静态数据和资源都作为正常 PE 节存在,那么一个可正常加载的 x64 EXE 通常不可能接近或超过2 GB |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 为什么有些 EXE 文件可以超过4 GB // 可以在正常 PE 内容后面追加数据 ┌────────────────────────┐ │ DOS头、PE头 │ │ .text │ │ .rdata │ │ .data │ │ .rsrc │ ├────────────────────────┤ │ 追加数据(PE overlay) │ │ 压缩包、安装数据等 │ └────────────────────────┘ // 后面的追加区域称为 overlay // Windows 加载器通常只关心有效的 PE 映像,不会把 overlay 映射成代码或普通 PE 节 // 程序启动后可以通过文件 API 再打开自己的 EXE GetModuleFileNameW(...); CreateFileW(...); SetFilePointerEx(...); ReadFile(...); // 然后读取后面追加的数据 // 所以: // 在 NTFS/exFAT 上,磁盘中的 EXE 文件可以通过追加数据超过4 GB,但超过部分不是正常的 PE 加载映像 |
- 加载映像大小
Windows加载EXE后,各个节按照内存对齐映射
|
1 2 3 4 5 6 7 |
PE头 .text .rdata .data .bss .rsrc .reloc |
|
1 2 3 4 5 6 |
// 它由 PE 头中的字段描述 // EXE 加载进虚拟地址空间后,整个映像占据的地址范围 DWORD SizeOfImage; // 因此 磁盘文件大小 != 内存中的映像大小 |
- 为什么
64位EXE仍限制在2 GB- 为了让映像中的代码、静态数据、字符串、导入表等可以方便地通过相对地址访问,
Windows将单个x64 EXE/DLL映像限制为2 GB
- 为了让映像中的代码、静态数据、字符串、导入表等可以方便地通过相对地址访问,
|
1 2 3 4 5 |
// 虽然 PE32+ 的 ImageBase 是 64 位地址,但大量内容使用 32 位 RVA 或相对位移 实际地址 = ImageBase + RVA // x64 指令也大量使用带符号 32 位相对寻址,范围约为 -2 GB ~ +2 GB |
EXE/DLL文件最大大小
-
磁盘文件大小首先受文件系统限制
fat32 4gb-1ntfs 2^64-1,实际还受Windows版本、卷大小等限制exFat 2^64-1,实际同样受实现和存储设备限制
-
Windows可执行文件还受PE格式限制- 现代
64位PE32+ EXE/DLL,可加载映像最大约为2 GiB - 可以在
PE映像后追加overlay数据,使磁盘上的EXE超过4 GiB
- 现代
单线程怎么用异步方式处理多个下载请求
NSIS
- WOW32
智能指针
- shared_ptr
- weak_ptr
函数
|
1 2 3 4 5 6 |
// 会崩在哪里 void test() { char buf[2]; memset(buf,10); return; } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 2023_02_1502/20
- ♥ 2025_03_1803/18
- ♥ 2023_02_0902/11
- ♥ 2022_03_1603/17
- ♥ 2020_05_11_0105/14
- ♥ 2022_02_2602/26
热评文章
- 2020_05_11_01 0
- 2022_03_07 0
- 2023_02_27 0
- 2020_04_29 0
- 2022_02_26 0
- 2025_03_11 0
