祝各位道友念头通达
GitHub Gitee 语雀 打赏

针对段错误排查

一、 发生段错误情况分类

  1. 访问不存在的内存
  2. 访问系统保护的内存
  3. 指针操作越界(包括数组)

测试代码如下

#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 调试即可, 主要说明以下两种方法:

  1. 代码写完之后运行一段时间之后, 或者是一些不确定条件导致的 core dump, 编译加 -g
  2. 同1, 生产环境 不加 -g, 特定条件触发 core dump
  3. 栈回溯: backtracebacktrace_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. 分析, 可以查看到对应的栈, 参数传递信息等

image

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 二进制文件反汇编成汇编语言, 然后通过汇编语言定位问题
获取信息:

  1. segfault at 28 ip 000000000040054c, 这里能获取的就是 000000000040054c, 这个是汇编的指令地址 0x40054c
    如果加-g参数, 直接使用 addr2line -e a.out 40054c,即可定位到问题
    image

  2. sp 00007fffd1e9ee20 error 6 in a.out[400000+1000], 这里能获取的就是代码出现错误的所在文件,可sp(rsp)栈所在位置

  3. 打开二进制文件 a.out, 搜索code打印出来的二进制文件, 就能搜索到对应的汇编行数, 其中 <89>, 也就是在 89 指令出发生的错误
    X86汇编代码说明链接:https://www.cnblogs.com/han-guang-xue/p/16525292.html

  4. 生成汇编, objdump -d a.out > a.outDump

  5. 查看发生的错误位置 grep -n -A 10 -B 10 "40054c" a.outDump

  6. 根据以下代码能很明确的定位到代码出现的位置:40054c, 就是 *ptr[10] = 10; 报错

  7. 通过 nm a.out 可以直接看到方法的地址,定位大概问题, 使用 ldd a.out 可以查看链接库

  8. 问题1: 无法查看参数传递
    问题2: 如果优化编译的话,看汇编对比源码会比较困难

3. 通过signal配合栈回溯实现问题定位

在嵌入式中, 程序执行由于内存访问错误会直接导致程序崩溃, 程序崩溃的时候会触发硬件错误中断, 然后通过更改代码中断函数, 实现了对数据寄存器的打印和对汇编指令的打印, 然后通过对二进制bin反编译成汇编定位问题。

在linux系统下不同于嵌入式, 之前做过 signalctrl+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对应的正在运行的进程,用来调试后台进程
posted @ 2022-11-28 20:38  韩若明瞳  阅读(388)  评论(0编辑  收藏  举报