C 语言开发中,Segement Fault 段错误一定是老相识,错误释放内存、访问悬空指针、内存越界,总有一款适合你。一般情况下,一旦出现段错误,程序就会抛出异常,然后彻底罢工了,用户非常崩溃,开发者似乎只能尴尬的说 “怪我,对不起咯”。
实际上,除了眼睁睁的看着段错误发生,还是可以做一些补救工作的,比如,让一切不要那么尴尬。
两个函数
实际上,当出现内存异常的时候,会抛出 SIGSEGV 信号,默认情况下操作系统会将对应的进程停止。如果我们捕获了进程的 SIGSEGV 信号,就可以自己决定发生段错误时怎么办。如果想结果信号处理,只需要借助 signal 函数就可以实现目的。可是,这只能让进程不尴尬的退出,如果希望进程可以从错误中恢复,就需要借助两个帮手:
- int sigsetjmp(sigjmp_buf env, int savesigs)
- void siglongjmp(sigjmp_buf env, int savesigs)
sigsetjmp 会保存调用时所有寄存器状态到 env 中,之后某个时刻可以再借助 siglongjmp 和 env 跳转回刚才的位置。sigsetjmp 相当于对 CPU 进行快照,siglongjmp 则会将这个快照恢复给 CPU,就像从未被打断一样。效果上与 goto 语句非常接近,goto 只能在函数内跳转,siglongjmp 可以在函数间 “goto”。
注意到 sigsetjmp 会被执行两次(如果调用了 siglongjmp),可以通过返回值确定当前是哪一次执行:
- 第一次进行 “快照” 执行, 返回值为 0;
- 从 siglongjmp 处 “跳转” 回来执行,返回值 !0;
从异常中恢复
对于一段可能出现异常的代码:
char * ptr = NULL;
char c = *ptr;
复制代码
显然执行到第二行会触发 SIGSEGV 信号。
我们可以在这两行代码前注册 SIGSEGV 信号处理回调,接管系统默认行为;在异常代码前调用sigsetjmp, 再巧妙的将 siglongjump 安排到信号处理回调函数中,就可以实现从异常中恢复。
第一次执行 flag = sigsetjmp(env, 1),flag 为 0,执行异常代码,当访问 NULL 指针触发段错误时,由信号处理回调接手,并借助 siglongjump 再一次回到 flag = sigsetjmp(env, 1), flag 不为 0,这样我们就知道,应该执行恢复代码 printf(“exception!\n”),并继续执行之后的代码。
一点 tricks
大概总结一下,如何在 C 中实现 try-catch 的效果:
- 异常代码块前,保留 CPU 寄存器状态,根据 flag 决定之后的路径;
- flag 为 0 则第一次执行,运行异常代码;
- flag 非 0,则已经发生异常,并从信号处理回调返回,运行恢复代码。
我们将前面提到的一些函数改写为宏,可以看起来像这样:
TRY
char * a = NULL;
*a = 'x';
/*catch segement falut*/
CATCH_ALL
/* segement fault catch */
printf("exception!\n");
ENDTRY
复制代码
这里只实现了捕获段错误,实际上可以添加更多信号类型,支持更多异常捕获。
你可以在 c_exception 中看到完整的实现,其中还实现了除零异常、用户抛出异常。