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

原点技术 2024-07-28 12:07:02 阅读 74

引言:什么是段错误

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

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

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

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

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

1. 日志

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

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

2. GDB

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

<code>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" signalscode>

export SEGFAULT_SIGNALS="segv bus abrt " #SIGSEGV, SIGBUS and SIGABRTcode>

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

export SEGFAULT_USE_ALTSTACK=1

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

export SEGFAULT_OUTPUT_NAME="./debug.log"code>

此外,为了方便用户使用,很多系统中还提供了一个名为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等,熟悉并善用这些工具,会事半功倍。

欢迎关注



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。