为什么 C++ 的 try/catch 捕不到非法内存访问
非法内存访问(空指针解引用、越界写、double-free、use-after-free)不是 C++ 异常,而是未定义行为 + 硬件/操作系统级错误。C++ 的 try/catch 只处理“语言层异常”,不处理“进程级崩溃”。
try {
int* p = nullptr;
*p = 42;
} catch (...) {
// 不会进来
}
此段代码使用空指针非法访问内存,尽管写了try/catch,但程序还是会崩溃,不是 catch 写错了,是这套机制本来就不负责这件事。
一、两套完全独立的“异常”体系
“Exception” 这个词在 C++ 里被用了两次,但根本不是同一个东西。
1. C++ 语言异常
-
由
throw产生 -
由
try/catch捕获 -
对象是
std::exception派生类 -
发生在用户态、语言运行时层
-
栈展开(stack unwinding)由编译器生成
-
例:
std::vector::at()越界抛std::out_of_range
2. 硬件 / OS 异常
-
由 CPU + MMU 触发
-
空指针解引用、写只读页、访问未映射地址
-
Linux:内核发
SIGSEGV/SIGBUS -
Windows:内核抛
EXCEPTION_ACCESS_VIOLATION(0xC0000005) -
属于操作系统和进程之间的事
C++ 标准完全没有规定:
“访问非法地址时必须抛一个 C++ 异常。”
相反,标准说的是:
这是未定义行为(Undefined Behavior)。
二、非法内存访问到底经历了什么
以 *((int*)nullptr) = 1为例:
-
程序执行
mov dword ptr [0], 1 -
CPU 发现虚拟地址 0 没有合法页表项 / 没有写权限
-
MMU 产生页错误(page fault)
-
操作系统内核接管
-
内核判断:这不是合法缺页,是非法访问
-
向进程发送
SIGSEGV(Linux)或派发 SEH(Windows) -
进程没有有效恢复逻辑 → 终止
关键点:
在 C++ 的
try块执行到那一行时,控制权根本没经过 C++ 异常系统。
它是被“硬件中断 → 内核 → 信号/SEH”这条线打断的。
try/catch只在 C++ 抽象机里生效;
页错误发生在 C++ 抽象机底下。
三、为什么 C++ 标准不把它变成异常
这是设计哲学问题,不是实现偷懒。
1. “不为不用的东西付钱”
如果每次指针解引用都要先问一句:
这个地址合法吗?
代价极高:
-
每次读写都加边界检查
-
失去裸指针性能优势
-
和 C ABI 不兼容
-
嵌入式 / 游戏 / HFT / 操作系统内核都没法用
C++ 的底线是:
你写
*p,编译器就假设p合法;不合法就是你自己砸自己脚。
2. 非法内存访问往往意味着状态已腐败
段错误常常不是“单独一条指令错了”,而是:
-
指针早就被写飞了
-
栈被踩了
-
vtable 坏了
-
heap metadata 被覆盖了
这时候就算“捕获”了,程序也已经不可信:
-
析构函数可能再踩一次内存
-
锁状态可能损坏
-
对象生命周期已经乱套
所以标准不鼓励“捕获后继续跑”。
3. C++ 没有托管运行时
Java 有 JVM:
-
所有对象由 GC 管
-
所有数组访问由 VM 检查
-
空指针 →
NullPointerException
C++ 没有 VM:
-
直接跑在裸指令上
-
数组就是一段内存
-
指针就是个整数
-
编译器不保证“访问前先检查”
四、那为什么 Windows 上有时“好像能捕”?
因为 MSVC 有 SEH(Structured Exception Handling):
__try {
*((int*)nullptr) = 1;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
// 能进来,但这是 SEH,不是 C++ 异常
}
或者开 /EHa+ _set_se_translator,把 SEH 转成 C++ 异常。
但注意:
-
这是 Microsoft 扩展
-
不是标准 C++
-
捕获后继续跑依然危险
-
Release 下栈/状态可能已坏
Linux 上对应的是:
signal(SIGSEGV, handler);
// 或
sigaction(SIGSEGV, ...);
但 POSIX 明确态度:
捕获 SIGSEGV 通常用于“写崩溃日志”,不是用来恢复业务。
你用 setjmp/longjmp跳回去,下一条指令还可能再触发 SIGSEGV。
五、为什么 catch (...)也不是万能兜底
catch (...)捕获的是:
-
C++ 异常对象
它不捕获:
-
SIGSEGV
-
SIGFPE
-
SIGILL
-
SIGBUS
-
Windows SEH(默认
/EHsc下) -
std::terminate前的栈展开失败 -
栈溢出(经常连栈都没地方展开)
尤其栈溢出很阴:
栈都坏了,C++ 异常处理本身就需要栈,谁来救谁?
六、正确认知:段错误不是“错误”,是“死刑判决”
|
层级 |
机制 |
是否能恢复 |
|---|---|---|
|
C++ 语言层 |
|
可以 |
|
OS 信号层 |
|
通常只记录,不恢复 |
|
Windows 系统层 |
SEH |
能拦截,但恢复风险极高 |
|
硬件层 |
MMU / page fault |
没有“异常”概念 |
所以工程上正确做法是:
-
用智能指针代替裸指针
-
用
std::vector::at()做边界检查的关键路径 -
用
std::span传递边界信息 -
用 AddressSanitizer / UBSan 在测试期抓 UB
-
用 crash handler 写 minidump / backtrace
-
用子进程 / 沙箱跑不可信代码
而不是:
try {
do_risky_pointer_stuff();
} catch (...) {
// 以为自己救回来了
}
七、一句话总结
-
非法内存访问 = 未定义行为 + 硬件页错误
-
C++ 异常 = 语言层控制流
-
SIGSEGV / Access Violation = 操作系统对进程的强制干预
-
try/catch活在 C++ 抽象机里,段错误发生在抽象机底下 -
标准不捕获,是因为:捕获了也不安全,检查了太慢,恢复了这个进程已经不可信
C++ 的哲学不是“别让它崩”,而是“别写出会让它崩的代码”。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)