引言:什么是段错误
每个在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
版权归原作者 原点技术 所有, 如有侵权,请联系我们删除。