Linux上段错误(SegFault)的9种实用调试方法

引言:什么是段错误

每个在Linux环境下工作的程序员,都遇到过段错误(segmentation fault)

所谓段错误,本质上是程序访问了非法内存地址而引起的一种错误类型。

导致程序访问非法地址的原因有很多,如野指针、内存被踩、栈溢出、访问没有权限的内存等。

之前更新调试专题文章时,有朋友问到段错误的调试方法,我承诺会更新文章专门介绍,本文就是来填这个坑的。

本文将介绍9种非常实用的段错误调试方法。

1. 日志

日志是一种非常实用的调试手段,我们可以从系统日志中获得很多非常有用的信息,从而反推问题出现的前后系统中究竟发生了什么异常状况。

printf可能是最简单的日志记录方法,大家都懂的,不再赘述。

2. GDB

GDB的强大无需多言,对于段错误,利用GDB很容易就能定位到触发问题的那一行代码。如下图示例代码:

void test_3(int *p)
{
    *p = 1;
}

void test_2(int *p)
{
    test_3(p);
}

void test_1(int *p)
{
    test_2(p);
}

int main(int argc, char *argv[])
{
    int *p = (int *)0x12345678;

    test_1(p);
    return 0;
}

编译时加上-g选项:

gcc -g test.c -o test

在GDB中运行程序:

root@ubuntu:debug# gdb test
Reading symbols from test...
(gdb) r
Starting program: /opt/data/workspace/test/debug/test 

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555139 in test_3 (p=0x12345678) at test.c:3
3           *p = 1;
(gdb) bt
#0  0x0000555555555139 in test_3 (p=0x12345678) at test.c:3
#1  0x000055555555515e in test_2 (p=0x12345678) at test.c:8
#2  0x000055555555517d in test_1 (p=0x12345678) at test.c:13
#3  0x00005555555551a7 in main (argc=1, argv=0x7fffffffe498) at test.c:20
(gdb) 

段错误触发时,GDB会直接告诉我们问题出现在哪一行代码,并且可以利用backtrace命令查看完整调用栈信息。此外,还可以利用其他常规调试命令来查看参数、变量、内存等数据。

这种方式虽然非常有效,但很多时候,问题并不是100%必现的,我们不可能一直把程序运行在GDB中,这对程序的执行性能等会有很大的影响。

这时,我们可以让程序在异常终止时生成core dump文件,然后用调试工具对它进行离线调试。

3. Core Dump + GDB

Core dump是Linux提供的一种非常实用的程序调试手段,在程序异常终止时,Linux会把程序的上下文信息记录在一个core文件中,然后可以利用GDB等调试工具对core文件进行离线调试。

很多系统中,根据默认配置,程序异常退出时不会产生core dump文件。可以通过下面这条命令查看:

ulimit -c

如果值是0,则默认不会产生core dump文件。可以用下面命令设置生成core dump文件的大小:

ulimit -c 10240

上面命令把core dump文件大小设置为10MB。如果存储空间不受限的话,可以直接取消大小限制:

ulimit -c unlimited

然后重新运行示例程序,段错误触发后,默认会在当前目录下生产一个core文件:

root@ubuntu:debug# ./test
Segmentation fault (core dumped)
root@ubuntu:debug# ls
core-test-2113875-1705030770  test  test.c
root@ubuntu:debug# 

然后用GDB加载调试core文件。调试时,除了core dump文件外,GDB还需要从可执行文件中加载调试信息。

gdb ./test ./core-test-2113875-1705030770

结果如下图:

root@ubuntu:debug# gdb ./test ./core-test-2113875-1705030770
Reading symbols from ./test...
[New LWP 2113875]
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000055ccfa65b139 in test_3 (p=0x12345678) at test.c:3
3           *p = 1;
(gdb) bt
#0  0x000055ccfa65b139 in test_3 (p=0x12345678) at test.c:3
#1  0x000055ccfa65b15e in test_2 (p=0x12345678) at test.c:8
#2  0x000055ccfa65b17d in test_1 (p=0x12345678) at test.c:13
#3  0x000055ccfa65b1a7 in main (argc=1, argv=0x7ffeac135938) at test.c:20
(gdb) 

与直接在GDB运行程序类似,core dump文件加载起来之后,GDB会直接显示触发问题的那一行代码,也可以使用backtrace、print等常规命令从core dump文件中获取信息。

在大多数系统中,这种core dump + GDB的手段非常有效,而且应该优先考虑使用。

但是有时候,由于某种原因,系统可能无法生存core dump文件。比如出于安全考虑,core dump功能可能是被彻底禁止的,或者在一些存储空间受限的嵌入式系统中,也无法生成core dump文件。

此时,我们就不得不考虑其它的调试手段了。

4. signal capture + backtrace

4.1 段错误在Linux系统上的处理过程

在Linux系统中,程序访问非法地址时,会被CPU捕获后触发硬件异常处理机制,并通知Linux kernel程序运行出现异常,kernel会对各种异常进行区分,然后向应用程序发送不同的signal,由应用程序自己进行故障恢复处理。

对于访问非法地址引起的段错误,Linux kernel会向应用程序发送11号signal,也就是SIGSEGV信号,该信号的默认处理是终止程序运行。

我们可以注册一个信号处理函数,当接受到Linux kernel发送过来的SIGSEGV信号后,在信号处理函数中把当前程序的上下文信息记录下来,方面后续问题定位。

4.2 两个有用的函数

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

backtrace获取程序的调用栈地址信息,并存储在buffer指定的一个数组中,数组大小为size。

backtrace_symbols_fd根据backtrace得到的调用栈地址数据,获取地址对应的符号信息,并把结果写到fd指定的文件中。

4.3 示例

对上面的示例做下修改,增加一个信号处理函数,如下:

#define _GNU_SOURCE
#include <ucontext.h>
#include <stdio.h>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>

static void signal_handler(int sig, siginfo_t *info, void *ctx)
{
    ucontext_t *context = (ucontext_t *)ctx;

    /* dump registers, x64 CPU specific */
    printf( "Signal = %d  Memory location = %p\n"
            "RIP = %016X  RSP = %016X  RBP = %016X\n"
            "RAX = %016X  RBX = %016X  RCX = %016X\n"
            "RDX = %016X  RSI = %016X  RDI = %016X\n"
            "R8  = %016X  R9  = %016X  R10 = %016X\n"
            "R11 = %016X  R12 = %016X  R13 = %016X\n"
            "R14 = %016X  R15 = %016X  RFLAGS = %016X\n\n",
            sig, info->si_addr,
            context->uc_mcontext.gregs[REG_RIP],
            context->uc_mcontext.gregs[REG_RSP],
            context->uc_mcontext.gregs[REG_RBP],
            context->uc_mcontext.gregs[REG_RAX],
            context->uc_mcontext.gregs[REG_RBX],
            context->uc_mcontext.gregs[REG_RCX],
            context->uc_mcontext.gregs[REG_RDX],
            context->uc_mcontext.gregs[REG_RSI],
            context->uc_mcontext.gregs[REG_RDI],
            context->uc_mcontext.gregs[REG_R8],
            context->uc_mcontext.gregs[REG_R9],
            context->uc_mcontext.gregs[REG_R10],
            context->uc_mcontext.gregs[REG_R11],
            context->uc_mcontext.gregs[REG_R12],
            context->uc_mcontext.gregs[REG_R13],
            context->uc_mcontext.gregs[REG_R14],
            context->uc_mcontext.gregs[REG_R15],
            context->uc_mcontext.gregs[REG_EFL]);

    /* get call stack and write to stdout */
    void *buf[256] = {0};
    int size = backtrace(buf, 256);
    backtrace_symbols_fd(buf, size, fileno(stdout));
    exit(-1);
}

在信号处理函数signal_handler中,先把寄存器信息打印出来,然后用backtrace和backtrace_symbols_fd获取调用栈信息,并写入stdout。

然后,在main函数中注册SIGSEGV的信号处理函数,如下:

void test_3(int *p)
{
    *p = 1;
}

void test_2(int *p)
{
    test_3(p);
}

void test_1(int *p)
{
    test_2(p);
}

int main(int argc, char *argv[])
{
    int *p = 0x12345678;

    struct sigaction action;
    sigemptyset(&action.sa_mask);
    action.sa_sigaction = signal_handler;
    action.sa_flags = SA_SIGINFO;

    sigaction(SIGSEGV, &action, NULL);

    test_1(p);
    return 0;
}

编译一下:

gcc -g -rdynamic test1.c -o test1

看下运行结果:

root@ubuntu:debug# ./test1
Signal = 11  Memory location = 0x12345678
RIP = 000000008F57C44F  RSP = 0000000090348BF0  RBP = 0000000090348BF0
RAX = 0000000012345678  RBX = 000000008F57C540  RCX = 00000000BD0F7166
RDX = 0000000000000000  RSI = 0000000090348AE0  RDI = 0000000012345678
R8  = 0000000000000000  R9  = 0000000000000000  R10 = 0000000000000008
R11 = 0000000000000246  R12 = 000000008F57C140  R13 = 0000000090348DE0
R14 = 0000000000000000  R15 = 0000000000000000  RFLAGS = 0000000000010206

./test1(+0x1407)[0x555b8f57c407]
/lib/x86_64-linux-gnu/libc.so.6(+0x43090)[0x7f97bd0f7090]
./test1(test_3+0x10)[0x555b8f57c44f]
./test1(test_2+0x1c)[0x555b8f57c474]
./test1(test_1+0x1c)[0x555b8f57c493]
./test1(main+0x86)[0x555b8f57c51c]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7f97bd0d8083]
./test1(_start+0x2e)[0x555b8f57c16e]
root@ubuntu:debug# 

为了方便演示,示例中的信号处理函数只记录了寄存器和调用栈信息,实际项目中根据需求,可以同时记录其它重要信息,如stack dump、全局变量、数据段dump等。

有两点需要注意:

  • 示例信号处理函数中打印寄存器的部分是针对x64 CPU的,其它CPU请参考sys/ucontext.h文件中对mcontext_t的定义。
  • 编译时需要加上-rdynamic选项,否则backtrace_symbols_fd无法正确获取符号信息。

5. signal capture + GDB

有些问题很难重现,直接在GDB里运行调试的话,可能要浪费很多时间去不停的尝试重现它。

那有没有一种方式,可以让问题重现时自动启动GDB呢?当然有!

与上面的一种方法类似,我们仍然利用signal capture的方式。只不过,在信号处理函数中,我们不再使用backtrace获取调用栈信息,而是直接启动GDB。

对信号处理函数作一些修改,如下:

static void signal_handler(int sig, siginfo_t *info, void *ctx)
{
    char cmd[256];

    printf("\n*** Segmentation fault happened, starting GDB ... \n\n");

    snprintf(cmd, 256, "gdb --pid=%d -ex bt -q", getpid());
    system(cmd);

    printf("\n*** Finish debugging, now quit! \n");
    exit(-1);
}

原理很简单,就是段错误发生时,在SIGSEGV信号处理函数中执行命令:

gdb --pid=xxx -ex bt -q

启动GDB,并attach到当前进程,然后执行backtrace命令打印调用栈信息。-q选项只是让GDB启动时不要打印版本信息,避免视觉干扰。

编译一下,需要加上-g选项:

gcc -g siggdb.c -o siggdb

运行,结果如下图:

root@ubuntu:debug# ./siggdb
attach: No such file or directory.
Attaching to process 2114093
Reading symbols from /opt/data/workspace/articles/debug/siggdb...
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
(No debugging symbols found in /lib/x86_64-linux-gnu/libc.so.6)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
0x00007f26e68f3c3a in wait4 () from /lib/x86_64-linux-gnu/libc.so.6
#0  0x00007f26e68f3c3a in wait4 () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007f26e6862f67 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x0000559bb65ff1fb in signal_handler (sig=11, info=0x7ffd8bbee570, ctx=0x7ffd8bbee440) at siggdb.c:16
#3  <signal handler called>
#4  0x0000559bb65ff211 in test_3 (p=0x12345678) at siggdb.c:23
#5  0x0000559bb65ff232 in test_2 (p=0x12345678) at siggdb.c:28
#6  0x0000559bb65ff24d in test_1 (p=0x12345678) at siggdb.c:33
#7  0x0000559bb65ff2d2 in main (argc=1, argv=0x7ffd8bbeebc8) at siggdb.c:47
(gdb) 

注意:这种方法只能在测试环境中使用,且要确保GDB可以正常使用。生产环境中不要使用!

6. libSegFault.so

除了上面提到的几种方式外,其实glibc也已经很贴心地提供了一种问题定位的方案:libSegFault.so

libSegFault.so是glibc提供的一个动态链接库,用于捕捉程序运行异常并记录调用栈等调试信息。

它的实现原理和上面提到的第4种方法是一样的,即通过signal capture的方式,程序发生异常时,在信号处理函数中记录调试信息。

使用时,先确定系统中是否存在这个动态链接库。在我的系统中,有这么几个:

root@ubuntu:debug# find / -name libSegFault.so
/snap/snapd/20671/lib/x86_64-linux-gnu/libSegFault.so
/snap/snapd/20290/lib/x86_64-linux-gnu/libSegFault.so
/snap/core20/2105/usr/lib/i386-linux-gnu/libSegFault.so
/snap/core20/2105/usr/lib/x86_64-linux-gnu/libSegFault.so
/snap/core20/2015/usr/lib/i386-linux-gnu/libSegFault.so
/snap/core20/2015/usr/lib/x86_64-linux-gnu/libSegFault.so
/snap/core18/2812/lib/i386-linux-gnu/libSegFault.so
/snap/core18/2812/lib/x86_64-linux-gnu/libSegFault.so
/snap/core18/2796/lib/i386-linux-gnu/libSegFault.so
/snap/core18/2796/lib/x86_64-linux-gnu/libSegFault.so
/usr/lib32/libSegFault.so
/usr/lib/x86_64-linux-gnu/libSegFault.so
root@ubuntu:debug# 

根据自己的实际情况,选择一个使用。比如我的测试环境是x64的,我选择使用:

/usr/lib/x86_64-linux-gnu/libSegFault.so

然后利用环境变量LD_PRELOAD,在测试程序运行前,把libSegFault.so链接进来。

LD_PRELOAD=/usr/lib/debug/lib/x86_64-linux-gnu/libSegFault.so   ./myapp

仍以本文第一个测试程序为例:

void test_3(int *p)
{
    *p = 1;
}

void test_2(int *p)
{
    test_3(p);
}

void test_1(int *p)
{
    test_2(p);
}

int main(int argc, char *argv[])
{
    int *p = 0x12345678;

    struct sigaction action;
    sigemptyset(&action.sa_mask);
    action.sa_sigaction = signal_handler;
    action.sa_flags = SA_SIGINFO;

    sigaction(SIGSEGV, &action, NULL);

    test_1(p);
    return 0;
}

编译:

gcc -rdynamic test.c -o test

运行:

LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libSegFault.so ./test

测试程序触发段错误后,libSegFault.so中的信号处理函数会把寄存器、调用栈、内存映射全部dump出来。结果如下:

root@ubuntu:debug# LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libSegFault.so ./test
*** Segmentation fault
Register dump:

 RAX: 0000000012345678   RBX: 0000557bb87bb1b0   RCX: 0000557bb87bb1b0
 RDX: 00007fff1d833198   RSI: 00007fff1d833188   RDI: 0000000012345678
 RBP: 00007fff1d833030   R8 : 0000000000000000   R9 : 00007f61cdce9d60
 R10: 0000000000000008   R11: 0000000000000246   R12: 0000557bb87bb040
 R13: 00007fff1d833180   R14: 0000000000000000   R15: 0000000000000000
 RSP: 00007fff1d833030

 RIP: 0000557bb87bb139   EFLAGS: 00010202

 CS: 0033   FS: 0000   GS: 0000

 Trap: 0000000e   Error: 00000006   OldMask: 00000000   CR2: 12345678

 FPUCW: 0000037f   FPUSW: 00000000   TAG: 00000000
 RIP: 00000000   RDP: 00000000

 ST(0) 0000 0000000000000000   ST(1) 0000 0000000000000000
 ST(2) 0000 0000000000000000   ST(3) 0000 0000000000000000
 ST(4) 0000 0000000000000000   ST(5) 0000 0000000000000000
 ST(6) 0000 0000000000000000   ST(7) 0000 0000000000000000
 mxcsr: 1f80
 XMM0:  00000000000000000000000000000000 XMM1:  00000000000000000000000000000000
 XMM2:  00000000000000000000000000000000 XMM3:  00000000000000000000000000000000
 XMM4:  00000000000000000000000000000000 XMM5:  00000000000000000000000000000000
 XMM6:  00000000000000000000000000000000 XMM7:  00000000000000000000000000000000
 XMM8:  00000000000000000000000000000000 XMM9:  00000000000000000000000000000000
 XMM10: 00000000000000000000000000000000 XMM11: 00000000000000000000000000000000
 XMM12: 00000000000000000000000000000000 XMM13: 00000000000000000000000000000000
 XMM14: 00000000000000000000000000000000 XMM15: 00000000000000000000000000000000

Backtrace:
./test(+0x1139)[0x557bb87bb139]
./test(+0x115e)[0x557bb87bb15e]
./test(+0x117d)[0x557bb87bb17d]
./test(+0x11a7)[0x557bb87bb1a7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7f61cdaf4083]
./test(+0x106e)[0x557bb87bb06e]

Memory map:

557bb87ba000-557bb87bb000 r--p 00000000 fc:10 550237                     /opt/data/workspace/test/debug/test
557bb87bb000-557bb87bc000 r-xp 00001000 fc:10 550237                     /opt/data/workspace/test/debug/test
557bb87bc000-557bb87bd000 r--p 00002000 fc:10 550237                     /opt/data/workspace/test/debug/test
557bb87bd000-557bb87be000 r--p 00002000 fc:10 550237                     /opt/data/workspace/test/debug/test
557bb87be000-557bb87bf000 rw-p 00003000 fc:10 550237                     /opt/data/workspace/test/debug/test
557bba6d5000-557bba6f6000 rw-p 00000000 00:00 0                          [heap]
7f61cdab2000-7f61cdab5000 r--p 00000000 fc:01 24328                      /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f61cdab5000-7f61cdac7000 r-xp 00003000 fc:01 24328                      /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f61cdac7000-7f61cdacb000 r--p 00015000 fc:01 24328                      /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f61cdacb000-7f61cdacc000 r--p 00018000 fc:01 24328                      /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f61cdacc000-7f61cdacd000 rw-p 00019000 fc:01 24328                      /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7f61cdacd000-7f61cdad0000 rw-p 00000000 00:00 0 
7f61cdad0000-7f61cdaf2000 r--p 00000000 fc:01 17718                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f61cdaf2000-7f61cdc6a000 r-xp 00022000 fc:01 17718                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f61cdc6a000-7f61cdcb8000 r--p 0019a000 fc:01 17718                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f61cdcb8000-7f61cdcbc000 r--p 001e7000 fc:01 17718                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f61cdcbc000-7f61cdcbe000 rw-p 001eb000 fc:01 17718                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f61cdcbe000-7f61cdcc2000 rw-p 00000000 00:00 0 
7f61cdccf000-7f61cdcd0000 r--p 00000000 fc:01 17711                      /usr/lib/x86_64-linux-gnu/libSegFault.so
7f61cdcd0000-7f61cdcd3000 r-xp 00001000 fc:01 17711                      /usr/lib/x86_64-linux-gnu/libSegFault.so
7f61cdcd3000-7f61cdcd4000 r--p 00004000 fc:01 17711                      /usr/lib/x86_64-linux-gnu/libSegFault.so
7f61cdcd4000-7f61cdcd5000 r--p 00004000 fc:01 17711                      /usr/lib/x86_64-linux-gnu/libSegFault.so
7f61cdcd5000-7f61cdcd6000 rw-p 00005000 fc:01 17711                      /usr/lib/x86_64-linux-gnu/libSegFault.so
7f61cdcd6000-7f61cdcd8000 rw-p 00000000 00:00 0 
7f61cdcd8000-7f61cdcd9000 r--p 00000000 fc:01 17690                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f61cdcd9000-7f61cdcfc000 r-xp 00001000 fc:01 17690                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f61cdcfc000-7f61cdd04000 r--p 00024000 fc:01 17690                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f61cdd05000-7f61cdd06000 r--p 0002c000 fc:01 17690                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f61cdd06000-7f61cdd07000 rw-p 0002d000 fc:01 17690                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f61cdd07000-7f61cdd08000 rw-p 00000000 00:00 0 
7fff1d813000-7fff1d834000 rw-p 00000000 00:00 0                          [stack]
7fff1d967000-7fff1d96a000 r--p 00000000 00:00 0                          [vvar]
7fff1d96a000-7fff1d96b000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
Segmentation fault (core dumped)
root@ubuntu:debug# 

libSegFault.so默认只捕捉SIGSEGV,可以通过设置环境变量SEGFAULT_SIGNALS指定要捕捉的信号,如:

export SEGFAULT_SIGNALS="all"  # "all" signals
export SEGFAULT_SIGNALS="segv bus abrt "  #SIGSEGV, SIGBUS and SIGABRT

环境变量SEGFAULT_USE_ALTSTACK可以指定是否让信号处理函数使用独立的栈,这在程序发送栈溢出时会很有用。

export SEGFAULT_USE_ALTSTACK=1

libSegFault.so默认把调试信息输出到stderr,可以通过设置环境变量SEGFAULT_OUTPUT_NAME,指定调试信息记录到一个文件中。比如:

export SEGFAULT_OUTPUT_NAME="./debug.log"

此外,为了方便用户使用,很多系统中还提供了一个名为catchsegv的脚本:

catchsegv ./test

其效果与通过LD_PRELOAD加载libSegFault.so是相同的:

root@ubuntu:debug# whereis catchsegv
catchsegv: /usr/bin/catchsegv /usr/share/man/man1/catchsegv.1.gz
root@ubuntu:debug# 
root@ubuntu:debug# catchsegv ./test
Segmentation fault (core dumped)
*** Segmentation fault
Register dump:

 RAX: 0000000012345678   RBX: 0000556c4e8d91b0   RCX: 0000556c4e8d91b0
 RDX: 00007ffdd20b2a68   RSI: 00007ffdd20b2a58   RDI: 0000000012345678
 RBP: 00007ffdd20b2900   R8 : 0000000000000000   R9 : 00007fdd23dc4d60
 R10: 00007fdd23daa730   R11: 00007fdd23d97be0   R12: 0000556c4e8d9040
 R13: 00007ffdd20b2a50   R14: 0000000000000000   R15: 0000000000000000
 RSP: 00007ffdd20b2900

 RIP: 0000556c4e8d9139   EFLAGS: 00010202

 CS: 0033   FS: 0000   GS: 0000

 Trap: 0000000e   Error: 00000006   OldMask: 00000000   CR2: 12345678

 FPUCW: 0000037f   FPUSW: 00000000   TAG: 00000000
 RIP: 00000000   RDP: 00000000

 ST(0) 0000 0000000000000000   ST(1) 0000 0000000000000000
 ST(2) 0000 0000000000000000   ST(3) 0000 0000000000000000
 ST(4) 0000 0000000000000000   ST(5) 0000 0000000000000000
 ST(6) 0000 0000000000000000   ST(7) 0000 0000000000000000
 mxcsr: 1f80
 XMM0:  00000000000000000000000000000000 XMM1:  00000000000000000000000000000000
 XMM2:  00000000000000000000000000000000 XMM3:  00000000000000000000000000000000
 XMM4:  00000000000000000000000000000000 XMM5:  00000000000000000000000000000000
 XMM6:  00000000000000000000000000000000 XMM7:  00000000000000000000000000000000
 XMM8:  00000000000000000000000000000000 XMM9:  00000000000000000000000000000000
 XMM10: 00000000000000000000000000000000 XMM11: 00000000000000000000000000000000
 XMM12: 00000000000000000000000000000000 XMM13: 00000000000000000000000000000000
 XMM14: 00000000000000000000000000000000 XMM15: 00000000000000000000000000000000

Backtrace:
./test(+0x1139)[0x556c4e8d9139]
./test(+0x115e)[0x556c4e8d915e]
./test(+0x117d)[0x556c4e8d917d]
./test(+0x11a7)[0x556c4e8d91a7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7fdd23bcf083]
./test(+0x106e)[0x556c4e8d906e]

Memory map:

556c4e8d8000-556c4e8d9000 r--p 00000000 fc:10 550237 /opt/data/workspace/test/debug/test
556c4e8d9000-556c4e8da000 r-xp 00001000 fc:10 550237 /opt/data/workspace/test/debug/test
556c4e8da000-556c4e8db000 r--p 00002000 fc:10 550237 /opt/data/workspace/test/debug/test
556c4e8db000-556c4e8dc000 r--p 00002000 fc:10 550237 /opt/data/workspace/test/debug/test
556c4e8dc000-556c4e8dd000 rw-p 00003000 fc:10 550237 /opt/data/workspace/test/debug/test
556c4ead1000-556c4eaf2000 rw-p 00000000 00:00 0 [heap]
7fdd23b8d000-7fdd23b90000 r--p 00000000 fc:01 24328 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7fdd23b90000-7fdd23ba2000 r-xp 00003000 fc:01 24328 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7fdd23ba2000-7fdd23ba6000 r--p 00015000 fc:01 24328 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7fdd23ba6000-7fdd23ba7000 r--p 00018000 fc:01 24328 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7fdd23ba7000-7fdd23ba8000 rw-p 00019000 fc:01 24328 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
7fdd23ba8000-7fdd23bab000 rw-p 00000000 00:00 0
7fdd23bab000-7fdd23bcd000 r--p 00000000 fc:01 17718 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7fdd23bcd000-7fdd23d45000 r-xp 00022000 fc:01 17718 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7fdd23d45000-7fdd23d93000 r--p 0019a000 fc:01 17718 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7fdd23d93000-7fdd23d97000 r--p 001e7000 fc:01 17718 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7fdd23d97000-7fdd23d99000 rw-p 001eb000 fc:01 17718 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7fdd23d99000-7fdd23d9d000 rw-p 00000000 00:00 0
7fdd23daa000-7fdd23dab000 r--p 00000000 fc:01 17711 /usr/lib/x86_64-linux-gnu/libSegFault.so
7fdd23dab000-7fdd23dae000 r-xp 00001000 fc:01 17711 /usr/lib/x86_64-linux-gnu/libSegFault.so
7fdd23dae000-7fdd23daf000 r--p 00004000 fc:01 17711 /usr/lib/x86_64-linux-gnu/libSegFault.so
7fdd23daf000-7fdd23db0000 r--p 00004000 fc:01 17711 /usr/lib/x86_64-linux-gnu/libSegFault.so
7fdd23db0000-7fdd23db1000 rw-p 00005000 fc:01 17711 /usr/lib/x86_64-linux-gnu/libSegFault.so
7fdd23db1000-7fdd23db3000 rw-p 00000000 00:00 0
7fdd23db3000-7fdd23db4000 r--p 00000000 fc:01 17690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7fdd23db4000-7fdd23dd7000 r-xp 00001000 fc:01 17690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7fdd23dd7000-7fdd23ddf000 r--p 00024000 fc:01 17690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7fdd23de0000-7fdd23de1000 r--p 0002c000 fc:01 17690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7fdd23de1000-7fdd23de2000 rw-p 0002d000 fc:01 17690 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7fdd23de2000-7fdd23de3000 rw-p 00000000 00:00 0
7ffdd2093000-7ffdd20b4000 rw-p 00000000 00:00 0 [stack]
7ffdd21dd000-7ffdd21e0000 r--p 00000000 00:00 0 [vvar]
7ffdd21e0000-7ffdd21e1000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
root@ubuntu:debug# 

7. Valgrind

Valgrind是一个很强大的工具集,它可以检测内存泄露、栈溢出、非法内存访问等多种内存相关的错误,还可以对程序进行性能剖析、生成函数调用关系图、统计Cache命中率、监测多线程竞争等,是程序调试的利器。

Valgrind功能非常强大,但文章篇幅有限,不对其展开讨论,后续会更新文章专门讲解它的各种功能,感兴趣的朋友可以右上角关注一下。

下面演示用Valgrind检测示例程序的内存访问错误。

编译时加上-g选项:

gcc -g test.c -o test

然后用Valgrind启动示例程序:

valgrind --tool=memcheck --leak-check=yes -v --leak-check=full --show-reachable=yes ./test

显示数据较多, 如下图所示:

root@ubuntu:debug# valgrind --tool=memcheck --leak-check=yes -v --leak-check=full --show-reachable=yes ./test
==2114522== Memcheck, a memory error detector
==2114522== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2114522== Using Valgrind-3.15.0-608cb11914-20190413 and LibVEX; rerun with -h for copyright info
==2114522== Command: ./test
==2114522== 
--2114522-- Valgrind options:
--2114522--    --tool=memcheck
--2114522--    --leak-check=yes
--2114522--    -v
--2114522--    --leak-check=full
--2114522--    --show-reachable=yes
--2114522-- Contents of /proc/version:
--2114522--   Linux version 5.4.0-90-generic (buildd@lgw01-amd64-054) (gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)) #101-Ubuntu SMP Fri Oct 15 20:00:55 UTC 2021
--2114522-- 
--2114522-- Arch and hwcaps: AMD64, LittleEndian, amd64-cx16-lzcnt-rdtscp-sse3-ssse3-avx-avx2-bmi-f16c-rdrand
--2114522-- Page sizes: currently 4096, max supported 4096
--2114522-- Valgrind library directory: /usr/lib/x86_64-linux-gnu/valgrind
--2114522-- Reading syms from /opt/data/workspace/test/debug/test
--2114522-- Reading syms from /usr/lib/x86_64-linux-gnu/ld-2.31.so
--2114522--   Considering /usr/lib/debug/.build-id/7a/e2aaae1a0e5b262df913ee0885582d2e327982.debug ..
--2114522--   .. build-id is valid
--2114522-- Reading syms from /usr/lib/x86_64-linux-gnu/valgrind/memcheck-amd64-linux
--2114522--    object doesn't have a symbol table
--2114522--    object doesn't have a dynamic symbol table
--2114522-- Scheduler: using generic scheduler lock implementation.
--2114522-- Reading suppressions file: /usr/lib/x86_64-linux-gnu/valgrind/default.supp
==2114522== embedded gdbserver: reading from /tmp/vgdb-pipe-from-vgdb-to-2114522-by-root-on-???
==2114522== embedded gdbserver: writing to   /tmp/vgdb-pipe-to-vgdb-from-2114522-by-root-on-???
==2114522== embedded gdbserver: shared mem   /tmp/vgdb-pipe-shared-mem-vgdb-2114522-by-root-on-???
==2114522== 
==2114522== TO CONTROL THIS PROCESS USING vgdb (which you probably
==2114522== don't want to do, unless you know exactly what you're doing,
==2114522== or are doing some strange experiment):
==2114522==   /usr/lib/x86_64-linux-gnu/valgrind/../../bin/vgdb --pid=2114522 ...command...
==2114522== 
==2114522== TO DEBUG THIS PROCESS USING GDB: start GDB like this
==2114522==   /path/to/gdb ./test
==2114522== and then give GDB the following command
==2114522==   target remote | /usr/lib/x86_64-linux-gnu/valgrind/../../bin/vgdb --pid=2114522
==2114522== --pid is optional if only one valgrind process is running
==2114522== 
--2114522-- REDIR: 0x4022e20 (ld-linux-x86-64.so.2:strlen) redirected to 0x580c9ce2 (???)
--2114522-- REDIR: 0x4022bf0 (ld-linux-x86-64.so.2:index) redirected to 0x580c9cfc (???)
--2114522-- Reading syms from /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_core-amd64-linux.so
--2114522--    object doesn't have a symbol table
--2114522-- Reading syms from /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so
--2114522--    object doesn't have a symbol table
==2114522== WARNING: new redirection conflicts with existing -- ignoring it
--2114522--     old: 0x04022e20 (strlen              ) R-> (0000.0) 0x580c9ce2 ???
--2114522--     new: 0x04022e20 (strlen              ) R-> (2007.0) 0x0483f060 strlen
--2114522-- REDIR: 0x401f600 (ld-linux-x86-64.so.2:strcmp) redirected to 0x483ffd0 (strcmp)
--2114522-- REDIR: 0x4023380 (ld-linux-x86-64.so.2:mempcpy) redirected to 0x4843a20 (mempcpy)
--2114522-- Reading syms from /usr/lib/x86_64-linux-gnu/libc-2.31.so
--2114522--   Considering /usr/lib/debug/.build-id/ee/be5d5f4b608b8a53ec446b63981bba373ca0ca.debug ..
--2114522--   .. build-id is valid
--2114522-- REDIR: 0x48f7480 (libc.so.6:memmove) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6780 (libc.so.6:strncpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f77b0 (libc.so.6:strcasecmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f60a0 (libc.so.6:strcat) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f67e0 (libc.so.6:rindex) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f8c50 (libc.so.6:rawmemchr) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x4913ce0 (libc.so.6:wmemchr) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x4913820 (libc.so.6:wcscmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f75e0 (libc.so.6:mempcpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7410 (libc.so.6:bcmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6710 (libc.so.6:strncmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6150 (libc.so.6:strcmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7540 (libc.so.6:memset) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x49137e0 (libc.so.6:wcschr) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6670 (libc.so.6:strnlen) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6230 (libc.so.6:strcspn) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7800 (libc.so.6:strncasecmp) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f61d0 (libc.so.6:strcpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7950 (libc.so.6:memcpy@@GLIBC_2.14) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x4914f50 (libc.so.6:wcsnlen) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x4913860 (libc.so.6:wcscpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6820 (libc.so.6:strpbrk) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6100 (libc.so.6:index) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6630 (libc.so.6:strlen) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48ffbb0 (libc.so.6:memrchr) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7850 (libc.so.6:strcasecmp_l) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f73d0 (libc.so.6:memchr) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x4913930 (libc.so.6:wcslen) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f6ae0 (libc.so.6:strspn) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f7750 (libc.so.6:stpncpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f76f0 (libc.so.6:stpcpy) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f8c90 (libc.so.6:strchrnul) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x48f78a0 (libc.so.6:strncasecmp_l) redirected to 0x48311d0 (_vgnU_ifunc_wrapper)
--2114522-- REDIR: 0x49df730 (libc.so.6:__strrchr_avx2) redirected to 0x483ea10 (rindex)
==2114522== Invalid write of size 4
==2114522==    at 0x109139: test_3 (test.c:3)
==2114522==    by 0x10915D: test_2 (test.c:8)
==2114522==    by 0x10917C: test_1 (test.c:13)
==2114522==    by 0x1091A6: main (test.c:20)
==2114522==  Address 0x12345678 is not stack'd, malloc'd or (recently) free'd
==2114522== 
==2114522== 
==2114522== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==2114522==  Access not within mapped region at address 0x12345678
==2114522==    at 0x109139: test_3 (test.c:3)
==2114522==    by 0x10915D: test_2 (test.c:8)
==2114522==    by 0x10917C: test_1 (test.c:13)
==2114522==    by 0x1091A6: main (test.c:20)
==2114522==  If you believe this happened as a result of a stack
==2114522==  overflow in your program's main thread (unlikely but
==2114522==  possible), you can try to increase the size of the
==2114522==  main thread stack using the --main-stacksize= flag.
==2114522==  The main thread stack size used in this run was 8388608.
--2114522-- REDIR: 0x48f16d0 (libc.so.6:free) redirected to 0x483c9d0 (free)
==2114522== 
==2114522== HEAP SUMMARY:
==2114522==     in use at exit: 0 bytes in 0 blocks
==2114522==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==2114522== 
==2114522== All heap blocks were freed -- no leaks are possible
==2114522== 
==2114522== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
==2114522== 
==2114522== 1 errors in context 1 of 1:
==2114522== Invalid write of size 4
==2114522==    at 0x109139: test_3 (test.c:3)
==2114522==    by 0x10915D: test_2 (test.c:8)
==2114522==    by 0x10917C: test_1 (test.c:13)
==2114522==    by 0x1091A6: main (test.c:20)
==2114522==  Address 0x12345678 is not stack'd, malloc'd or (recently) free'd
==2114522== 
==2114522== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Segmentation fault
root@ubuntu:debug# 

Valgrind成功检测出地址0x12345678既不是栈地址,也不是malloc分配的动态内存。并且它也会把调用栈信息dump出来。

Valgrind虽然在检测内存相关的错误时非常强大,但是它有一个致命的缺点,就是。据统计,通过Valgrind运行程序时,速度会降低10倍。这在调试大型项目时,尤其是对实时性非常敏感的程序,是无法接受的。

不过,我们还有一个更好的选择 — AddressSanitizer。

8. AddressSanitizer

AddressSanitizer最初是Google开发的一个检测多种内存相关问题的工具,AddressSanitizer现在已经集成到GCC和LLVM中。它最大的特点是:

  • 功能强大。它可以检测内存泄露、访问越界、栈溢出、多次释放等各种内存问题。
  • 。使用AddressSanitizer检测内存问题时,原始程序运行速度只会降低2倍左右,相比Vagrind来说,运行效率有了很大的提升。

本文只简单演示用AddressSanitizer检测示例程序中的内存访问错误,后续会专门更新文章详细讲解它的各种功能,感兴趣的朋友可以关注一下。

AddressSanitizer的使用方法也非常简单,只需要在编译时加上相应的编译选项,然后正常运行程序即可。

这里,我只使用最简单的一个编译选项-fsanitize=address开启AddressSanitizer功能。

gcc -g -fsanitize=address test.c -o test

然后正常运行即可,如下图:

root@ubuntu:debug# gcc -g -fsanitize=address test.c -o test
root@ubuntu:debug# ./test
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2114531==ERROR: AddressSanitizer: SEGV on unknown address 0x000012345678 (pc 0x55669475e1d4 bp 0x7ffcf6b43ad0 sp 0x7ffcf6b43ac0 T0)
==2114531==The signal is caused by a WRITE memory access.
    #0 0x55669475e1d3 in test_3 /opt/data/workspace/test/debug/test.c:3
    #1 0x55669475e1f8 in test_2 /opt/data/workspace/test/debug/test.c:8
    #2 0x55669475e217 in test_1 /opt/data/workspace/test/debug/test.c:13
    #3 0x55669475e241 in main /opt/data/workspace/test/debug/test.c:20
    #4 0x7f02b03ea082 in __libc_start_main ../csu/libc-start.c:308
    #5 0x55669475e0cd in _start (/opt/data/workspace/test/debug/test+0x10cd)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /opt/data/workspace/test/debug/test.c:3 in test_3
==2114531==ABORTING
root@ubuntu:debug# 

9. dmesg + objdump

有时,可能由于各种原因,以上几种方法都不适用,比如程序中无法添加调试信息、程序无法重新编译、没有GDB和Valgrind等调试工具等。

这种情况下,调试起来,会相对比较困难一些,但也并不是完全不可能。

大多数情况下,程序发生segmentation fault而异常退出时,会在系统日志中记录一些信息,可以用dmesg查看:

root@ubuntu:debug# dmesg
[68302968.931073] test[2113875]: segfault at 12345678 ip 000055ccfa65b139 sp 00007ffeac1357e0 error 6 in test[55ccfa65b000+1000]
[68302968.931091] Code: 2e 00 00 01 5d c3 0f 1f 00 c3 0f 1f 80 00 00 00 00 f3 0f 1e fa e9 77 ff ff ff f3 0f 1e fa 55 48 89 e5 48 89 7d f8 48 8b 45 f8 <c7> 00 01 00 00 00 90 5d c3 f3 0f 1e fa 55 48 89 e5 48 83 ec 08 48
root@ubuntu:debug# 

可以从中得到触发异常的指令地址和被访问的内存地址,然后利用系统中现有的一些工具进行调试,如利用objdump对可执行文件进行反汇编,然后从汇编代码入手进行分析,限于篇幅,不再展开讨论,后续会有专门文章详细讲解。

Linux下有很多非常有用的工具,如binutils工具集(objdump、nm、readelf等)、strace等,熟悉并善用这些工具,会事半功倍。

欢迎关注微信公众号:【原点技术】,分享真正有用的东西!

进技术交流群,欢迎添加作者微信:CreCoding

原创文章,未经允许禁止转载,转载请联系作者:CreCoding

  • 34
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值