在C语言中捕获段错误

12
我有一个程序,有时会因为指针算术而导致段错误。我知道这种情况会发生,但我不能轻易地预先检查它是否会段错误 - 要么我可以“预扫描”输入数据以查看是否会导致段错误(这可能无法确定),要么我可以重新设计它以不使用指针算术,但这需要大量的工作,或者我可以尝试捕获到段错误。所以我的问题是:

1)在C中,如何捕获段错误?我知道操作系统中的某些东西会引起段错误,但如果程序发生段错误,C程序可以做些什么来比单纯的“Segmentation fault”更优雅地结束程序?

2)这有多具可移植性?

我想象这是非常不具可移植性的行为,因此如果您发布任何捕获段错误的代码,请告诉我它适用于什么平台。我在Mac OS X上,但我希望我的程序能够在尽可能多的平台上运行,并且我想了解我的选择。

不要担心 - 我只想打印一条更友好的错误消息并释放一些已经使用malloc()分配的内存,然后结束程序。我不打算忽略所有的段错误并继续前进。


1
打印错误信息;不要释放内存,因为当段错误发生时,你的内存系统很可能已经混乱了。 - Jonathan Leffler
8个回答

23

好的,SIGSEGV是可以被捕获的,而且这是POSIX标准,因此从这个意义上说它是可移植的。

但我担心你似乎想处理段错误而不是修复导致段错误的问题。如果我必须选择是操作系统有问题还是我的代码有问题,我知道该选哪个。我建议你找到那个bug,修复它,然后编写一个测试用例来确保它永远不会再次发生。


不深入讨论细节, 默认模式是安全的,并保护用户免于内存溢出(它使用链表)。用户必须明确选择使用"不太安全"的版本,并了解这样做可能带来的后果。 - Chris Lutz
“不那么安全”的版本是为了与其他程序兼容而提供的,这些程序不像我的程序一样提供无限的链接列表。此外,如果没有其他办法,我也可以学到一些新东西。” - Chris Lutz
我认为他的意思是“某些东西”在操作系统中捕获了违规行为,而不是操作系统是故障的原因。 - ijw

18
您可以使用函数signal为信号安装新的信号处理程序:
   #include <signal.h>
   void (*signal(int signum, void (*sighandler)(int)))(int);

类似以下代码:

signal(SIGINT , clean_exit_on_sig);
signal(SIGABRT , clean_exit_on_sig);
signal(SIGILL , clean_exit_on_sig);
signal(SIGFPE , clean_exit_on_sig);
signal(SIGSEGV, clean_exit_on_sig); // <-- this one is for segmentation fault
signal(SIGTERM , clean_exit_on_sig);

void 
clean_exit_on_sig(int sig_num)
{
        printf ("\n Signal %d received",sig_num);
}

1
请注意,当您遇到SEGV(指针读/写恰好命中不可访问的内存)时,很可能已经覆盖了分配的可访问内存,其中包含您的数据和空闲块列表。因此,请不要期望alloc起作用,并且不要期望内存中的任何数据是健全的。 - ijw

10

你需要定义一个信号处理函数。在Unix系统上,可以使用sigaction 函数来实现这一点。我已经在 Fedora 64 位和 32 位以及 Sun Solaris 上使用相同的代码完成了此操作。


5
在信号处理程序中安全的操作非常有限。调用任何未知可重入性的库函数都是不安全的,这将排除例如free()printf()。最佳实践是设置一个变量并返回,但这对你没有太大帮助。同时,使用系统调用如write()是安全的。
需要注意的是,在这里给出的两个回溯示例中,backtrace_symbols_fd()函数将是安全的,因为它直接使用原始fd,但对fprintf()的调用是不正确的,应该替换为使用write()

哇,那很艰难。我会记住这些严格的规则。也许我真的不应该试图捕获段错误... - Chris Lutz
1
我完全同意其他评论者的建议,即正确的做法是修复导致segv的错误。 - Dale Hagglund

1

信号处理在unix机器上(包括Mac和Linux)是(相对)可移植的。重要的区别在于异常细节,这些异常细节作为参数传递给信号处理程序。很抱歉,如果您想打印更合理的错误消息(例如故障发生的位置和原因),可能需要一堆#ifdefs。

好的,下面是一个代码片段供您参考:

#include <signal.h>

/* reached when a segv occurrs */
void
SEGVFunction( SIGARGS )
{
     ...
}

...
main(...) {
    signal(SIGSEGV, SEGVFunction); /* tell the OS, where to go in case... */
    ...
    ... do your work ...
}

你的任务是:

  • 检查SIGARGS是什么(因为与操作系统相关,所以请使用ifdef)
  • 查看如何从sigArgs中提取故障地址和pc信息
  • 打印合理的消息
  • 退出

理论上,你甚至可以在信号处理程序中修补pc(到故障指令之后),然后继续执行。然而,典型的信号处理程序要么退出(),要么longjmp()回到主函数中保存的位置。

此致


1

网站已经消失了! - vincenzopalazzo

1

这里有一个使用glibc的backtrace()函数来捕获SIGSEGV并打印堆栈跟踪的示例:

如何在C++应用程序崩溃时生成堆栈跟踪

您可以使用此方法来捕获段错误并进行清理,但请注意:您不应该在信号处理程序中执行太多操作,特别是涉及像malloc()这样的调用。有很多调用不是信号安全的,如果您从malloc内部发出调用,可能会自食其果。


链接已失效,请考虑更新或删除此答案。 - slayton

0

我认为你正在试图解决一个不存在的问题。至少你在错误的方向上努力。你无法“捕获”分段错误,因为这个错误/异常是由操作系统抛出的(它是由你的程序引起的,操作系统只是“捕获”它)。

我建议你重新考虑输入策略:为什么无法对其进行清理?最重要的是进行大小检查,C标准库有适当的函数来完成此任务。然后当然你需要检查内容是否有效。是的,这可能会导致很多工作,但这是编写健壮程序的唯一方法。

编辑:我不是C语言专家,不知道甚至分段错误也可以通过信号处理器来处理。尽管如此,我仍然认为出于上述原因,这不是正确的方法。


检查大小并不是问题-用户输入一个程序(用brainf * ck编写),我的程序运行它。检查有效输入会遇到停机问题。另外,有些程序只在某些时候崩溃(即有更多的数据输入),而它们是否会取决于用户输入。 - Chris Lutz
@Chris Lutz: 啊哈...在这种情况下,你的问题看起来更加合理。我现在可以看出为什么你想要 - 实际上,为什么它是你唯一可行的选择 - 捕获SIGSEGV。 - paprika
这不是唯一“可行”的选择。我可以重写它,使用一个数组和一个指向该数组元素的变量,然后轻松检查指针是否越界(即<0或> MAX),但这看起来更加丑陋(从我的角度来看)。 - Chris Lutz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接