Linux下段错误调试技巧

20次阅读

共计 2481 个字符,预计需要花费 7 分钟才能阅读完成。

更新于 2019.04.16
我们写的程序, 尤其是 C /C++ 程序有时候会段错误, 而且往往发生在部署环境而非调试环境, 对问题定位带来很大困难. 这时一般有两种方法来解决问题, 一种是生成 core dump 文件, 然后用 gdb 调试这个文件; 另一种是不生成 core dump 文件, 而使用其他工具来定位问题.
生成 core dump 文件并用 gdb 调试
最简单的方法是运行 ulimit -c unlimited 命令, 然后在错误发生后用 gdb 调试这个文件. 但这种方法往往不好用, 比如段错误发生时没有生成 core dump 文件, 或 core dump 文件过大不完整等等.
使用 dmesg 和 addr2line 命令
这种方法是使用 dmesg 和 addr2line 命令. 这需要代码以 - g 选项编译 比如以下代码故意产生段错误:
#include <stdio.h>

int main(void)
{
int *p = NULL;
*p = 0;
printf(“bad\n”);
return 0;
}
编译运行这个程序, 立即会发生段错误:
$ gcc -O3 -g -o test test.c
$ ./test
段错误(吐核)
这时先调用 dmesg 命令查看段错误信息:
$ dmesg | grep segfault
[422855.897248] test[63448]: segfault at 0 ip 0000000000400449 sp 00007ffd06202b70 error 6 in test[400000+1000]
注意其中指令指针寄存器 (IP) 的值, 接下来调用 addr2line 命令, 把 IP 所指的地址转换为源码行号:
$addr2line -e test 0000000000400449
/root/tmp/test.c:6
可见排查出源码第 6 号有错.
如果段错误出在动态库而非可执行程序中, 在调用 addr2line 命令时, 需要将 dmesg 输出中 IP 的值减去行最后的地址值, 比如下面一行中
[422855.897248] test[63448]: segfault at 0 ip 0000000000400449 sp 00007ffd06202b70 error 6 in test[400000+1000]
addr2line 的参数 = 400449 -400000.
下面是动态库段错误的示例, 假设我们有 3 个文件, 主程序 test.c, 动态库头文件 foo.h 和实现文件 foo.c, 内容分别如下.
test.c:
#include “foo.h”

int main(void)
{
foo();
return 0;
}
foo.h:
#ifndef __FOO_LIB_H__
#define __FOO_LIB_H__

int foo(void);

#endif
foo.c:
#include “foo.h”

int foo()
{
int *p = 0;
*p = 0;
return 0;
}
先编译动态库, 再编译主程序, 让它链接动态库, 最后运行之:
$ gcc -O3 -g -o libfoo.so -shared -fPIC foo.c
$ gcc -O3 -g -o test test.c -L. -lfoo
$ export LD_LIBRARY_PATH=.
$ ./test
段错误(吐核)
调用 dmesg:
$dmesg | grep segfault
[423801.507232] test[63800]: segfault at 0 ip 00007f8adeb08680 sp 00007ffeb7f29ab8 error 6 in libfoo.so[7f8adeb08000+1000]
IP 值减去动态库地址值(00007f8adeb08680 -7f8adeb08000=680), 调用 addr2line, 注意 - e 参数后文件名改为动态库名:
$ addr2line -e libfoo.so 680
/root/tmp/foo.c:6
让程序段错误时自动输出堆栈信息并调试
以上方法还是显得不够方便, 还要调用 dmesg 命令等. 我们可以使用 execinfo.h 里的 backtrace 函数及信号处理机制, 来让程序在发生段错误时自动打印调用堆栈:
#include <unistd.h>
#include <signal.h>
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdarg.h>

#define BACKTRACE_SIZE 256

void segv_handler(int sig)
{
void *func[BACKTRACE_SIZE];
char **symb = NULL;
int size;

size = backtrace(func, BACKTRACE_SIZE);
backtrace_symbols_fd(func, size, STDERR_FILENO);
exit(1);
}

int main(void)
{
int *p = NULL;
signal(SIGSEGV, segv_handler);
*p = 0xdeadbeef;
return 0;
}
然后以 - g 选项编译, 运行:
$ gcc -O3 -g -o auto auto.c
$ ./auto
./auto[0x400654]
/lib64/libc.so.6(+0x35250)[0x7fbbe4283250]
./auto[0x400543]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7fbbe426fb35]
./auto[0x40057e]
输出的调用堆栈逆序打印, 最先调用的在最下面, 我们可以判断出是第 3 行的 ./auto[0x400543] 导致了段错误, 因为前面 2 行是信号 SIGSEGV 的处理函数调用. 运行 addr2line:
$ addr2line -e auto 0x400543
/root/tmp/auto.c:29
可知第 29 行代码 *p = 0xdeadbeef; 引起错误.
参考

How to automatically generate a stacktrace when my program crashes
拒绝超大 coredump – 用 backtrace 和 addr2line 搞定异常函数栈

正文完
 0