linux/unix段错误捕获

// 段错误捕获,并打印栈信息
static void segvhandler(int sig)
{
#define BACKTRACE_MAX_FRAMES 100

    static bool in_handler = false;
    printf("capture a signal: %d", sig);

    if (!in_handler){
        int j, nptrs;
        void* buffer[BACKTRACE_MAX_FRAMES];
        char** symbols;

        in_handler = true;

        nptrs = backtrace(buffer, BACKTRACE_MAX_FRAMES);
        printf("SIGSEGV captured, stack trace(%d):", nptrs);
        symbols = backtrace_symbols(buffer, nptrs);
        if (symbols != NULL){
            for (j = 0; j < nptrs; j++)
                printf("%s", symbols[j]);
            free(symbols);
        }

        in_handler = false;
    }

    // do with all necessary things before exit
    process_before_exit();
    exit(-1);
}

打印出地址信息,再通过addr2line输出函数信息。

backtrace是库函数引入的应用自调试函数。

系列里的三个函数可以缓冲或输出栈帧。

#include <execinfo.h>

int backtrace(void **buffer, int size);

char **backtrace_symbols(void *const *buffer, int size);

void backtrace_symbols_fd(void *const *buffer, int size, int fd);

参考:backtrace、backtrace_symbols、backtrace_symbols_fd-support for application self-debugging

 

背景知识:

· 在linux/unix中的信号处理机制,知道signal函数与sigaction的区别

· 段错误的概念,CPU中断处理的步骤,中断向量表的分类

· 知道CPU Exception分为Fault、trap和abort,了解他们的基本区别

· 段错误和浮点错误属于Fault,产生Fault时会将出错指令的地址入栈,而不是下一条将执行指令的地址

· 在linux/unix里可以通过调用backstrace来获取栈帧的信息

· 文中用到的几个头文件和函数,都属于glibc,所以不用担心出现找不到头文件和链接错误的情况

· addr2line是个系统自带的小工具,用来转换编译出来的地址和源码行号

·linux/unix下动态链接库的基本原理
·/proc/pid/maps文件的基本格式
·动态链接库:在进程执行过程中动态加载,进程间可以共享代码,可用在发布升级包等场合

一、捕获段错误

     struct sigaction act;
     int sig = SIGSEGV;
     sigemptyset(&act.sa_mask);
     act.sa_sigaction = OnSIGSEGV;
     act.sa_flags = SA_SIGINFO;
     if(sigaction(sig, &act, NULL)<0)
     {
              perror("sigaction:");
     }

void OnSIGSEGV(int signum, siginfo_t *info, void *ptr)
{
//TO DO: 输出堆栈信息
         abort();
}

发生段错误时的函数调用关系体现在栈帧上,可以通过在信号处理函数中调用 backstrace 来获取栈帧信息,backstrace 的具体描述可google之/阅读头文件execinfo.h。

void OnSIGSEGV(int signum, siginfo_t *info, void *ptr)
{
         void * array[25]; /* 25 层,太够了 : ),你也可以自己设定个其他值 */
         int nSize = backtrace(array, sizeof(array)/sizeof(array[0]));
         for (int i=nSize-3; i>=2; i--){ /* 头尾几个地址不必输出,看官要是好奇,输出来看看就知道了 */
/* 修正array使其指向正在执行的代码 */[f1] 
             printf("SIGSEGV catched when running code at %x\n", (char*)array[i] - 1);
         }
         abort();
}

要想输出出错的具体位置,必须用到信号处理函数的第三个参数,在linux/unix环境下,该指针指向一个ucontext_t结构。这个结构的具体情况,可以通过阅读头文件ucontext.h得知。此结构体里面包含了发生段错误时的寄存器现场,其中就包含EIP寄存器,该寄存器的内容正是段错误时的指令地址(因为段错误是一种Fault)。

void OnSIGSEGV(int signum, siginfo_t *info, void *ptr)
{
     void * array[25];
     int nSize = backtrace(array, sizeof(array)/sizeof(array[0]));
     for (int i=nSize-3; i>2; i--){ /* 头尾几个地址不必输出 */
              /* 对array修正一下,使地址指向正在执行的代码 */
              printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1);

     }
    
     if (NULL != ptr){
              ucontext_t* ptrUC = (ucontext_t*)ptr;
              int *pgregs = (int*)(&(ptrUC->uc_mcontext.gregs));
              int eip = pgregs[REG_EIP];
              if (eip != array[i]){ /* 有些处理器会将出错时的 EIP 也入栈 */
                  printf("signal[%d] catched when running code at %x\n", signum, (char*)array[i] - 1);

              }
              printf("signal[%d] catched when running code at %x\n", signum, eip); /* 出错地址 */
     }else{
        printf("signal[%d] catched when running code at unknown address\n", signum);
     }
         abort();
}

 好了,现在栈帧里面的地址和出错位置的地址都已经以十六进制的形式输出了,但是这是编译后的地址,而不是源码的行号,你能看懂么?所以还需要借助一个linux/unix自带的小工具addr2line,将这些打印出来的指令地址转换为行号、函数名。

[root@suse tcpBreak]# ./a.out
signal[11] catched when running code at 804861d
signal[11] catched when running code at 8048578
signal[11] catched when running code at 804855a

[root@ suse tcpBreak]# addr2line 804861d 8048578 804855a -s -C -f -e a.out
main
newsig.cpp:55
oops()
newsig.cpp:32
error(int)
newsig.cpp:27

捕获的信号序号是 11 (SIGSEGV)

执行路径是第52行--第32行--第27行

调用关系是main--oops--error,在error函数内部,即文件的第27行发生了段错误。

——一点讨论

· 你可能已经阅读了 execinfo.h,发现其中有一个 backtrace_symbols,想通过调用这个函数来输出stack frame上面的函数名…你不妨试一下

· 将 backtrace 得到的 array 地址元素减 1 就能得到调用地点么?的确是这样的,减 1 不保证地址落到函数调用时跳转指令的起始处,但可以保证指向了该指令的最后一个字节,而该指令地址经addr2line转换后[f2] ,就对应了发生函数调用的行号。

· 可不可以不调用 backstrace 来得到栈帧中的内容?可以的,因为这些内容都在栈里,你要是明确地知道偏移,就可以得知函数调用栈,但是要费很多心思,而且估计你自己写的模仿 backstrace 的代码,可移植性成了问题。

· 通过 gdb 调试 core文件 不是直接看得到内存映像么,还有必要搞得这么复杂么?一般情况下当然不必要,上面所列解决方法的优点在无法正常产生 core 文件的情况[f3] 下才得以体现。

· 需要在编译时添加选项 -g 么?当然需要了,不在可执行文件中记录行号信息,addr2line上哪里去找行号。否则只能得到函数名称,无法得到行号信息。

二、出错代码在三方库中

有些情况下,我们会采用动态链接库,如果出错代码行恰巧在动态链接库内,原方法仍可得到出错时的地址。例如:
signal[8] catched when running code at 8048ab3
signal[8] catched when running code at 4001771b
signal[8] catched when running code at 400176fd

# addr2line 8048ab3 4001771b 400176fd -s -C -f -e a.out
main
test.cpp:15
??
??:0
??
??:0
 显然,后面两个地址翻译不出来了,因为其实出错代码根本不在可执行文件 a.out 内,而是位于一个动态链接库内。
动态链接库无非就是编译后的代码,里面有一些基本的段、符号信息。如果出错代码行在动态链接库内,那必然可以从动态链接库内找到出错时的代码行号。
[root@redhat tcpBreak]# addr2line 4001771b 400176fd -s -C -f -e libtest.so
??
??:0
??
??:0

还是翻译不出来。当然出不来了,因为进程挂掉时输出的地址,和动态链接库文件内的静态偏移地址根本就不是一回事。所以我们需要知道出错时,所输出的代码地址与动态链接库偏移地址之间的关系。

事实上,每一个进程都对应了一个 /proc/pid 目录,下面记载了诸多与该进程相关的信息,其中有一个maps文件,里面记录了各个动态链接库的加载地址。我们只需要根据所得到的出错地址,以及这个maps文件,就可以得出具体是哪一个库,相应的偏移地址是多少。本文用例产生的输出为:
-------------------------- 进程挂掉时的MAPS文件 --------------------------
08048000-08049000 r-xp 00000000 00:09 17256 /mnt/hgfs/share/net/tcpBreak/a.out
08049000-0804a000 rw-p 00001000 00:09 17256 /mnt/hgfs/share/net/tcpBreak/a.out
0804a000-0804b000 rwxp 00000000 00:00 0
40000000-40015000 r-xp 00000000 08:02 271023 /lib/ld-2.3.2.so
40015000-40016000 rw-p 00014000 08:02 271023 /lib/ld-2.3.2.so
40016000-40017000 rw-p 00000000 00:00 0
40017000-40018000 r-xp 00000000 00:09 17255 /mnt/hgfs/share/net/tcpBreak/libtest.so
40018000-40019000 rw-p 00000000 00:09 17255 /mnt/hgfs/share/net/tcpBreak/libtest.so
40019000-4001b000 rw-p 00000000 00:00 0
40026000-400cf000 r-xp 00000000 08:02 350892 /usr/lib/libstdc++.so.5.0.3
400cf000-400d4000 rw-p 000a9000 08:02 350892 /usr/lib/libstdc++.so.5.0.3
400d4000-400d9000 rw-p 00000000 00:00 0
400d9000-400fa000 r-xp 00000000 08:02 286922 /lib/tls/libm-2.3.2.so
400fa000-400fb000 rw-p 00020000 08:02 286922 /lib/tls/libm-2.3.2.so
400fb000-40102000 r-xp 00000000 08:02 271272 /lib/libgcc_s-3.2.2-20030225.so.1
40102000-40103000 rw-p 00007000 08:02 271272 /lib/libgcc_s-3.2.2-20030225.so.1
40103000-40104000 rw-p 00000000 00:00 0
42000000-4212e000 r-xp 00000000 08:02 286920 /lib/tls/libc-2.3.2.so
4212e000-42131000 rw-p 0012e000 08:02 286920 /lib/tls/libc-2.3.2.so
42131000-42133000 rw-p 00000000 00:00 0
bfffd000-c0000000 rwxp ffffe000 00:00 0
-------------------------------------------------------------------------
--------------------------- 进程挂掉时的栈帧 --------------------------
signal[8] catched when running code at 8048ab3
signal[8] catched when running code at 4001771b
signal[8] catched when running code at 400176fd
-------------------------------------------------------------------------
显然 4001771b 400176fd 对应的库是 libtest.so,偏移地址分别为 71b 6fd。
知道了对应的动态链接库和偏移地址后,我们进一步用 addr2line 将这个偏移地址翻译一下就可以了。
# addr2line 71b 6fd -s -C -f -e libtest.so
a()
lib.cpp:14
b()
lib.cpp:10
至此,大功告成。
不管是否有用到动态链接库,我们将原方法得到的输出,结合进程挂掉时maps文件的内容,就可以得到代码出错时的执行路径。根据代码所在部分,指定相应的文件给 addr2line 的 -e 参数即可。对于上面那个例子:
# addr2line 8048ab3 -s -C -f -e a.out
main
test.cpp:15
# addr2line 71b 6fd -s -C -f -e libtest.so
a()
lib.cpp:14
b()
lib.cpp:10
一个程序启动后,地址是如何进行映射的,MAPS文件是怎么生成的,库又是怎么加载的,自行编写动态链接库时,有什么注意事项...
这些问题我也不甚明了,因为我自己也没深究过,以后有时间可能会陆续补到博客里面。
 
参考资料:
[1] Linux debug : addr2line追踪出错地址, http://www.linuxidc.com/Linux/2011-05/35780.htm
[2] addr2line,可以根据一个地址打印出对应的代码行, http://archive.cnblogs.com/a/1996110/
[3] Linux下 /proc/maps 文件分析,http://bbs.chinaunix.net/viewthread.php?tid=2000825
[4] 《程序员的自我修养—链接、装载与库》,俞甲子,石凡,潘爱民. (PS 此书甚好,推荐大家阅读)
posted @ 2018-05-12 15:54  yuxi_o  阅读(411)  评论(0编辑  收藏  举报