针对段错误排查
一、 发生段错误情况分类
- 访问不存在的内存
- 访问系统保护的内存
- 指针操作越界(包括数组)
测试代码如下
#include <stdio.h>
void test(int *ptr, int val) {
//对空指针指向的内存区域写,会发生段错误
ptr[10] = val;
}
int main()
{
int *null_ptr = NULL;
//*null_ptr = 10;
int val = 10;
test(null_ptr, val);
return 0;
}
调试方法
在编写过程中,程序启动就出现 core dump
, 直接使用 gdb 调试即可, 主要说明以下两种方法:
- 代码写完之后运行一段时间之后, 或者是一些不确定条件导致的
core dump
, 编译加-g
- 同1, 生产环境 不加
-g
, 特定条件触发core dump
- 栈回溯:
backtrace
和backtrace_symbols
, 可以利用SIGSEGV
打印出栈回溯
说明 gcc 编译后的文件有很多符号之类的, 使用指令strip
可以上文件变小, 并且不影响程序运行, 使用man strip
查看strip
使用手册: GNU strip丢弃对象文件objfile中的所有符号
1. gdb 调试 core 文件
代码写完之后运行一段时间之后, 或者是一些不确定条件导致的 core dump
# 1. 首先, 在编译代码的时候需要加上 `-g` 参数;
# 2. ulimit -c #查看是否开启core文件, 0则不会生成core文件
# 3. ulimit -c [size] #生成限制core文件大小
ulimit -c unlimited #生成core文件不受大小限制, 临时设置, 永久设置在 /etc/security/limits.conf 填写以下两行
@root soft core unlimited
@root hard core unlimited
# 4. 指定core文件保存位置和文件命名方式
`echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern`
# 5. 指定core文件生成是否添加pid作为扩展
`echo 1 > /proc/sys/kernel/core_uses_pid`
# 6. mkdir /corefile 目录
# 7. gcc -g xx.c && a.out
# 8. 会在/corefile 下看生成的 .core 文件
# 9. gdb -core /corefile/xxx.core
# 10 进入gdb之后, 执行 file a.out
# 11. where/bt
# 10. 分析, 可以查看到对应的栈, 参数传递信息等
2. objdump 排查 core dump
在 报错core dump
的时候, 内核中会提示如下错误
[ 6629.248711] a.out[2891]: segfault at 28 ip 000000000040054c sp 00007fffd1e9ee20 error 6 in a.out[400000+1000]
[ 6629.248716] Code: 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa eb 8a 55 48 89 e5 48 89 7d f8 89 75 f4 48 8b 45 f8 48 8d 50 28 8b 45 f4 <89> 02 90 5d c3 55 48 89 e5 48 83 ec 10 48 c7 45 f8 00 00 00 00 c7
其中 打印出来的二进制code就是 编译过后的 二进制执行文件, 即a.out中的内容, objdump 就是将 a.out 二进制文件反汇编成汇编语言, 然后通过汇编语言定位问题
获取信息:
-
segfault at 28 ip 000000000040054c, 这里能获取的就是 000000000040054c, 这个是汇编的指令地址
0x40054c
如果加-g
参数, 直接使用addr2line -e a.out 40054c
,即可定位到问题
-
sp 00007fffd1e9ee20 error 6 in a.out[400000+1000], 这里能获取的就是代码出现错误的所在文件,可sp(rsp)栈所在位置
-
打开二进制文件 a.out, 搜索code打印出来的二进制文件, 就能搜索到对应的汇编行数, 其中 <89>, 也就是在 89 指令出发生的错误
X86汇编代码说明链接:https://www.cnblogs.com/han-guang-xue/p/16525292.html -
生成汇编, objdump -d a.out > a.outDump
-
查看发生的错误位置 grep -n -A 10 -B 10 "40054c" a.outDump
-
根据以下代码能很明确的定位到代码出现的位置:40054c, 就是 *ptr[10] = 10; 报错
-
通过 nm a.out 可以直接看到方法的地址,定位大概问题, 使用 ldd a.out 可以查看链接库
-
问题1: 无法查看参数传递
问题2: 如果优化编译的话,看汇编对比源码会比较困难
3. 通过signal配合栈回溯实现问题定位
在嵌入式中, 程序执行由于内存访问错误会直接导致程序崩溃, 程序崩溃的时候会触发硬件错误中断, 然后通过更改代码中断函数, 实现了对数据寄存器的打印和对汇编指令的打印, 然后通过对二进制bin反编译成汇编定位问题。
在linux系统下不同于嵌入式, 之前做过 signal
对ctrl+C
的截取, 后来查看 signal代码, 发现支持很多 signal,查看源码发现,其中 SIGSEGV Invalid access to storage
对存储的无效访问, 就是 core dump
/* ISO C99 signals. */
#define SIGINT 2 /* Interactive attention signal. */
#define SIGILL 4 /* Illegal instruction. */
#define SIGABRT 6 /* Abnormal termination. */
#define SIGFPE 8 /* Erroneous arithmetic operation. */
#define SIGSEGV 11 /* Invalid access to storage. */
#define SIGTERM 15 /* Termination request. */
/* Historical signals specified by POSIX. */
#define SIGHUP 1 /* Hangup. */
#define SIGQUIT 3 /* Quit. */
#define SIGTRAP 5 /* Trace/breakpoint trap. */
#define SIGKILL 9 /* Killed. */
#define SIGBUS 10 /* Bus error. */
#define SIGSYS 12 /* Bad system call. */
#define SIGPIPE 13 /* Broken pipe. */
#define SIGALRM 14 /* Alarm clock. */
以下方式就是通过接管 signal 实现对栈和寄存器的打印输出
Linux c 代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <execinfo.h>
void ShowStack()
{
int i;
void *buffer[1024];
int n = backtrace(buffer, 1024);
//n, buffer 越大, size越大, 回溯的栈就越多
char **symbols = backtrace_symbols(buffer, n);
for (i = 0; i < n; i++) {
printf("%s\n", symbols[i]);
}
}
void signal_coredump(int sigval) {
if (sigval == SIGSEGV) {
printf("Receive SIGSEGV signal\n");
ShowStack();
exit(-1);
} else {
printf("this is sig %d", sigval);
}
}
void test(int *ptr, int val, unsigned short val1) {
ptr[10] = val + val1;
}
int main() {
signal(SIGSEGV, signal_coredump);
printf("start run \n");
int * null_ptr = NULL;
int val = 0xABCD1234;
int val1 = 0xEAAE1234;
test(null_ptr, val, val1);
printf("end run \n");
return 0;
}
运行结果如下, 其中中括号中就是 objdump 之后反汇编中的代码行数, 如下为 _start -> main -> test -> signal_coredump -> ShowStack
中调用栈
[root@cen8 han]# ./a.out
start run
Receive SIGSEGV signal
./a.out() [0x400725]
./a.out() [0x40079c]
/lib64/libc.so.6(+0x37400) [0x7f6705fe9400]
./a.out() [0x4007df]
./a.out() [0x400832]
/lib64/libc.so.6(__libc_start_main+0xf3) [0x7f6705fd5493]
./a.out() [0x40064e]
如何打印寄存器的值
以下值可以打印数据寄存器的值 64位寄存器地址分析
__asm__ __volatile__ ("movq %%rax, %0;":"=r" (rax));
这个在GUN/linux 系统中有个问题, 因为其本身系统的问题, 在触发signal
信号时, 调用的堆栈已经有两层了, 而寄存器的值都是临时的,值早已变化, 所以该方式不可取, 研究中,后期补上
内存检测工具
使用 memwatch 调试内存检测
memwatch 内存错误检查工具,检查内存泄漏,错误释放,内存溢出等
原理: memwatch.c
文件中重新定义了 malloc
, free
, calloc
等相关函数, 然后引入自己的程序当中, 可以对自己的程序内存的处理做出监听
内存上溢: 内存覆盖超出缓存区上部分
内存下溢: 内存覆盖超出缓存区下部分
类似的内存检测工具还有:
- mtrace: GUN C 的库函数, 他给malloc,realloc,free安装钩子函数,用于检测内存状况
- dmalloc: 使用比较复杂
- yamd: 只能用于 x86
- valgrind: 对strcpy, strncpy, memcpy, strcat,free,malloc等内存操作函数进行检测
资源调用调试工具
使用 strace 调试程序
strace 执行程序时, 它会记录用户空间和内核空间资源边界处理的状况,如对文件的open,read,write,ioctl等
应用场景: 当不熟悉某个程序所依赖哪些文件目录或者其它资源,可以使用该方法
简单示例:
strace cat /dev/null
- -f: 除了跟踪当前进程,还跟踪子进程
- -o: 将输出信息写入文件
- -p: 绑定到一个由pid对应的正在运行的进程,用来调试后台进程
本文来自博客园踩坑狭,作者:韩若明瞳,转载请注明原文链接:https://www.cnblogs.com/han-guang-xue/p/16932168.html