捕捉段错误

段错误是一种很常见的错误,原因很多,比如越界读或写内存,如果没有详细的运行日志供分析,一般不好定位问题出在哪里。

下面介绍通过捕捉段错误信号SIGSEGV,并输出发生段错误时的函数调用堆栈信息,协助查找段错误出现的位置,在不能产生coredump文件的环境下这是一种替换方案。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <execinfo.h>
void OnSigSEGV(int signo, siginfo_t *info, void *act) {
    void *buffer[100];
    int cnt = backtrace(buffer, sizeof(buffer) / sizeof(buffer[0]));
    backtrace_symbols_fd(buffer, cnt, 1);
    abort();
}
void TrapFault() {
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = OnSigSEGV;
    sigaction(SIGSEGV, &act, NULL);
}
void func2() {
    memset(NULL, 0, 123);
}
void func1() {
    func2();
}
void func() {
    func1();
}
int main() {
    TrapFault();
    func();
    return 0;
}

在macbook上的某次执行结果如下:

$ ./x
0   x                                   0x000000010d562e7c OnSigSEGV + 44
1   libsystem_platform.dylib            0x00007fffb03c9bba _sigtramp + 26
2   libSystem.B.dylib                   0x00007fffbe377796 __crashreporter_info__ + 90109150
3   libsystem_c.dylib                   0x00007fffb0276f7f __memset_chk + 22
4   x                                   0x000000010d562f01 func2 + 33
5   x                                   0x000000010d562f19 func1 + 9
6   x                                   0x000000010d562f29 func + 9
7   x                                   0x000000010d562f49 main + 25
8   libdyld.dylib                       0x00007fffb01bc255 start + 1
Abort trap: 6

从结果很容易看出,问题出在func2函数的memset操作上。

同样的代码在Centos7上执行,某次结果如下:

$ ./x
./x[0x4006f3]
/lib64/libc.so.6(+0x35250)[0x7f85ee704250]
/lib64/libc.so.6(+0x89754)[0x7f85ee758754]
./x[0x400781]
./x[0x40079b]
./x[0x4007ab]
./x[0x4007c5]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f85ee6f0b35]
./x[0x4005f9]
已放弃

此时需要addr2line工具将地址做下转换,方便查看。

$ addr2line 0x4007c5 0x4007ab 0x40079b 0x400781 -sCf -e x
main
a.c:32
func
a.c:28
func1
a.c:25
func2
a.c:22

可见,在func2中调用了libc.so.6中的某个库函数后出错了。

有时,为了保证服务不挂掉,通过父子进程的方式来工作,子进程真正干活,而父进程则监视子进程状态,子进程异常退出后由父进程再次产生子进程。如果子进程的业务逻辑比较复杂,可能会出现段错误,可以考虑通过信号来捕捉。

以下是个简单的例子。

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
void CatchSEGV(int signo) {
    fprintf(stderr, "!!! segment fault!!!\n");
    exit(-1);
}
void ChildProcess() {
    signal(SIGSEGV, CatchSEGV);
    char *p = NULL;
    strcpy(p, "hello");
    exit(0);
}
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        ChildProcess();
    } else if (pid > 0) {
        wait(NULL);
    }
    return 0;
}
Table of Contents