有关 C/C++ 的异常处理方式笔记

C 的异常处理

在 C 语言中最常使用 setjmp.h 库处理我们的异常。

最简单的异常处理,我们会使用其中的两个函数:setjmplongjmp

jmp_buf 数组

jmp_buf 在 MSVC 中被定义为 DWORD 数组。

jmp_buf 被用来保存跳转现场,即用来保存当前调用的 ESP、EIP、EBP、EBX、EDI、ESI 等寄存器和标志位的信息。

setjmp 函数

setjmp 需要接收一个 jmp_buf 数组指针,将这个时候调用 setjmp 时的寄存器状态保存在 jmp_buf 里,然后返回 0 代表跳转点设置成功。

long_jmp 被调用后,会让 setjmp 函数返回一个值代表此次运行为跳转运行。

代码示例:

1
2
jmp_buf jb;
setjmp(jb);

long_jmp 函数

long_jmp 需要接收一个 jmp_buf 数组指针和一个自定义 int 类型信息 (会作为跳转后 set_jmp 的返回值)。

它会根据传入的 jmp_buf 数组还原寄存器信息,即跳转到上一次调用 set_jmp(jmp_buf) 的时刻,然后将传入的 int 类型值作为 set_jmp 的返回值返回。

注意非必要不要传 0,否则无法分辨 set_jmp 是跳转回来的还是正常运行的。

代码示例:

1
longjmp(jb, -1)

完整示例

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <setjmp.h>

jmp_buf jb;

int calc(int x)
{
if (x == 0)
{
longjmp(jb, 1);
}
else if (x == 10)
{
longjmp(jb, 2);
}
else
{
return 10 / x;
}
}

int main()
{
int num;
int msg;

printf("Pls. input a number to calc: ");
scanf("%d", &num);

msg = setjmp(jb);
if (msg == 0)
{
printf("the value is %d.\n", calc(num));
}
else if (msg == 1)
{
printf("number can not be 0...\n");
}
else if (msg == 2)
{
printf("calc() do not like 10...\n");
}
else
{
printf("I do not know what happened...\n");
}

return 0;
}

C++ 的异常处理

在 C++ 中最常使用 C++ 标准所提供的 try - catch - throw 异常处理模块来处理我们的异常。

try - catch

try 块中的代码被称为保护代码,后面通常跟着一个或多个 catch 块。

catch 块跟在 try 块后面,用于捕获异常。可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

throw 语句

当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

其中 throw 后跟着的信息与 catch 的接收有关。(即 throw 什么就 catch 什么)

代码示例:

1
2
3
4
5
6
7
8
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

完整示例

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
#include <iostream>
using namespace std;

double division(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (a / b);
}

int main()
{
int x = 50;
int y = 0;
double z = 0;

try {
z = division(x, y);
cout << z << endl;
}
catch (const char* msg) {
cerr << msg << endl;
}

return 0;
}

标准异常

C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

C++ 异常的层次结构

下表是对上面层次结构中出现的每个异常的说明:

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过 new 抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vectorstd::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。

同时我们也可以自定义异常,代码示例:

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
29
#include <iostream>
#include <exception>
using namespace std;

// 这里可以不继承 exception 来创建自己的异常标准。
struct MyException : public exception
{
const char* what() const throw ()
{
return "C++ Exception";
}
};

int main()
{
try
{
throw MyException();
}
catch (MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch (std::exception& e)
{
//其他的错误
}
}

Windows SEH 结构化异常处理机制

Windows SEH 结构化异常处理机制核心是由系统维护的一个单向链表,链表中保存每个异常处理函数的指针。

调用机制类似 C++ 的异常处理:

1
2
3
4
5
6
7
8
__try
{
// 保护代码
}
__except ()
{
// 异常处理后的内容块
}

其中 __except 需要一个宏定义常量,通常是 EXCEPTION_CONTINUE_EXECUTIONEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_SEARCH

宏定义常量 含义
EXCEPTION_CONTINUE_EXECUTION 异常处理完毕,程序继续正常运行异常出现处的下一条指令
EXCEPTION_EXECUTE_HANDLER 异常处理完毕,但是直接执行 __except 块的内容
EXCEPTION_CONTINUE_SEARCH 异常无法处理,告诉 Windows 寻找下一个异常处理函数来处理异常

这里我们通常通过函数实现:

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
29
30
31
32
33
#include <iostream>
#include <windows.h>
using namespace std;

long ExceptionFilter(int nRet)
{
if (nRet == 0)
{
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}

int main()
{
int x = 50;
int y = 0;
double z = 0;

cout << "Pls. input a number: ";
cin >> y;

__try {
z = x / y;
cout << "the value is " << z << endl;
}
__except (ExceptionFilter(y)) {
cout << "could not div 0" << endl;
}


return 0;
}

其中我们可以通过 GetExceptionCode() 来获取系统所抛出的异常。

在 ida 中我们可以观察到,__excpet 被分析为:

ida_view

Windows VEH 向量化异常处理机制

Windows VEH 向量化异常处理机制