【ARM】程序快速定位segmentation fault core dumped错误

1.应用场景

ARM开发过程中经常进程运行着出现段错误,这时候单纯靠加日志打log效率太低。使用gdb的话,由于APP进程太多,生成的core的文件特别大,而且gdb在arm板子也不好单步调试,不太友好还是pass掉。
目前使用段错误捕捉SIGSEGV信号,通过backtrace和backtrace_symbols函数进行堆栈信息定位,再使用addr2line工具将指令的地址和可执行映像转换成文件名、函数名和源代码行数。


root@chenwr-pc:/home/workspace/chenwr/study/test# ./out
./out
[2020/07/21 09:38:15.249517 INFO]: [libcwr.c:0193]:
=========>>>catch signal 11 <<<=========
[2020/07/21 09:38:15.265993 INFO]: [libcwr.c:0170]: dummy_function
/home/workspace/chenwr/study/test/test.c:35

[2020/07/21 09:38:15.270385 INFO]: [libcwr.c:0170]: main
/home/workspace/chenwr/study/test/test.c:44

Segmentation fault (core dumped)

2.实现代码

C代码接口。

/*******************************************************************
** 函数名:     run_shell_cmd
** 函数描述:   shell命令执行判断,保存执行结果。
** 参数:       [in] cmd:        shell命令;
**             [out] result:    shell命令执行结果;
**             [in] len:        result长度
** 返回:       shell命令执行失败返回-1;执行成功返回shell命令结束码
** 说明:
**      如果/bin/sh不能执行,退出状态为127,比如执行命令rename,但系统没有安装rename该命令则返回值为127。
** 详细内容,man system进行查看。
********************************************************************/
INT32S run_shell_cmd(INT8S *cmd, INT8S *result, INT32S len)
{
    INT32S rc     = 0;
    INT32S ret    = -1;
    INT32S offset = 0;
    FILE *fd      = NULL;

    do {
        fd = popen(cmd, "r");
        if (NULL == fd) {
            perror("popen error\n");
            break;
        }

        while (NULL != fgets(result, len, fd)) {
            offset = strlen(result);
            result += offset;
        }

        rc = pclose(fd);

        if (-1 == rc) {
            perror("pclose error\n");
            break;
        }

        if (!WIFEXITED(rc)) {
            perror("Run command failed\n");
            break;
        } else {
            ret = WEXITSTATUS(rc);
        }

    } while (0);

    if (NULL == fd || -1 == rc) {
        strncpy(result, strerror(errno), len);
        //FK_TRACE_INFO("errno = %s\n", strerror(errno));
    }

    fd = NULL;
    return ret;
}

/*******************************************************************
** 函数名:     get_curprocess_name
** 函数描述:   获取当前进程名字
** 参数:       [out] process_name:  进程名
** 返回:       成功返回0,失败返回-1。
********************************************************************/
INT32S get_curprocess_name(INT8S *process_name)
{
    FILE *fd                 = NULL;
    INT8S proc_pid_path[100] = {0};
    INT8S buf[100]           = {0};

    //获取进程名字
    snprintf(proc_pid_path, sizeof(proc_pid_path), "/proc/%d/status", getpid());

    do {
        fd = fopen(proc_pid_path, "r");
        if (NULL == fd) {
            FK_TRACE_ERROR("fopen %s fail\n", proc_pid_path);
            break;
        }

        if (NULL == fgets(buf, sizeof(buf)-1, fd)) {
            fclose(fd);
            FK_TRACE_ERROR("get current process name fail\n");
            break;
        }

        sscanf(buf, "%*s %s", process_name);
        fclose(fd);
        return EXIT_OK;

    } while(0);

    return EXIT_ERROR;
}

/*******************************************************************
** 函数名:     print_coredumped_msg
** 函数描述:   输出段错误详细信息,具体到函数行数。
** 参数:       [in] symbols: 符号信息 (backtrace_symbols的返回值);[in] catch_num:  捕获信息元素个数(backtrace的返回值)
** 返回:       成功返回0,失败返回-1。
** 说明:
**      int backtrace(void **buffer, int size);
**      该函数获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针数组,参数size用来指定buffer中可以保存多少个void*元素。
** 函数的返回值是实际返回的void*元素个数。buffer中的void*元素实际是从堆栈中获取的返回地址。
**
**      char **backtrace_symbols(void *const *buffer, int size);
**      该函数将backtrace函数获取的信息转化为一个字符串数组,参数buffer是backtrace获取的堆栈指针,size是backtrace返回值。
** 函数返回值是一个指向字符串数组的指针,它包含char*元素个数为size。
** 每个字符串包含了一个相对于buffer中对应元素的可打印信息,包括函数名、函数偏移地址和实际返回地址。
********************************************************************/
INT32S print_coredumped_msg(INT8S **symbols, INT32S catch_num)
{
    INT8S tools[100]          = "addr2line";
    INT8S process_name[100]   = {0};
    INT8S cmd[100]            = {0};
    INT8S buf[100]            = {0};
    INT8S result[MAXBUF_SIZE] = {0};
    INT8S *front_position     = NULL;
    INT8S *back_position      = NULL;
    INT8S *tmp                = NULL;
    INT32S ret, i;

    //判断addr2line工具是否存在
    ret = run_shell_cmd(tools, result, MAXBUF_SIZE);
    if (127 == ret) {
        FK_TRACE_ERROR("%s is not exist\n", tools);
        return EXIT_ERROR;
    }

    //获取当前进程名
    ret = get_curprocess_name(process_name);
    if (ret) {
        return EXIT_ERROR;
    }

    //打印段错误详细行数
    for (i = 0; i < catch_num; i++) {
    	memset(buf, 0, sizeof(buf));
        memset(cmd, 0, sizeof(cmd));
        memset(result, 0, sizeof(result));
        //处理字符串,找到地址
        front_position = strstr(symbols[i], "[");
        back_position = strstr(symbols[i], "]");
        memcpy(&buf, front_position + 1, back_position - front_position - 1);

        snprintf(cmd, 100, "%s -Cfe %s %s", tools, process_name, buf);

        ret = run_shell_cmd(cmd, result, MAXBUF_SIZE);
        if (-1 == ret) {
            return EXIT_ERROR;
        }

        //结果包含??全部过滤掉
        tmp = strstr(result, "??");
        if (!tmp) {
            FK_TRACE_INFO("%s\n", result);
        }
    }

    front_position = NULL;
    back_position = NULL;
    tmp = NULL;

    return EXIT_OK;
}

/*******************************************************************
** 函数名:     catch_core_dump
** 函数描述:   捕捉段错误信息
** 参数:       [in] signo:  信号
** 返回:       无
********************************************************************/
void catch_core_dump(INT32S signo)
{
    INT32S catch_num;
    void *buf[MAXBUF_SIZE] = {0};
    INT8S **symbols        = NULL;

    FK_TRACE_INFO("\n=========>>>catch signal %d <<<=========\n", signo);

    catch_num = backtrace(buf, MAXBUF_SIZE);
    symbols = backtrace_symbols(buf, catch_num);

    if (NULL == symbols) {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    print_coredumped_msg(symbols, catch_num);

    signal(signo, SIG_DFL); //恢复信号默认处理
    raise(signo);           //重新发送信号
    free(symbols);
    symbols = NULL;
}

/*******************************************************************
** 函数名:     catch_core_dump_fd
** 函数描述:   捕捉段错误信息保存在文件中
** 参数:       [in] signo:  信号
** 返回:       无
********************************************************************/
void catch_core_dump_fd(INT32S signo)
{
    INT32S catch_num;
    const INT8S file_path[] = "./core_dump.txt"; 
    void *buf[MAXBUF_SIZE]  = {0};
    INT32S fd               = 0;

    fd = open(file_path, O_RDWR | O_CREAT, 777);
    if (-1 == fd) {
        FK_TRACE_ERROR("open %s fail\n", file_path);
    }

    FK_TRACE_INFO("\n=========>>>catch signal %d <<<=========\n", signo);

    catch_num = backtrace(buf, MAXBUF_SIZE);
    backtrace_symbols_fd(buf, catch_num, fd);

    signal(signo, SIG_DFL); //恢复信号默认处理
    raise(signo);           //重新发送信号

    close(fd);
}

makefile文件记得添加-g -rdynamic -funwind-tables -ffunction-sections -finstrument-functions这几个编译选项。

其次如果在ARM板子测试,需要交叉编译addr2line这个工具。

3.代码分析

3.1接口说明

头文件:
#include <execinfo.h>

函数原型:
int backtrace (void **buffer, int size);
char **backtrace_symbols (void *const *buffer, int size);
void backtrace_symbols_fd (void *const *buffer, int size, int fd);

函数说明:
backtrace()函数,backtrace 英文释义回溯,向后追踪。
获取函数调用堆栈帧数据,即回溯函数调用列表。数据将放在buffer中。我的理解是发生段错误时的函数调用堆栈信息存放在buffer中,size为这个buffer数组下标,表示最多能保存多少个函数信息,如果内存富裕的情况下,这个buffer和size最好还是搞大点。

backtrace_symbols()函数,symbols为符号表。参数buffer是从backtrace()函数获取的数组指针,size是该数组中的元素个数(backtrace()函数的返回值)。
该函数主要功能:
将从backtrace()函数获取的地址转为描述这些地址的字符串数组。每个地址的字符串信息包含对应函数的名字、在函数内的十六进制偏移地址、以及实际的返回地址(十六进制)。需注意的是,当前,只有使用elf二进制格式的程序才能获取函数名称和偏移地址,此外,为支持函数名功能,可能需要添加相应的编译链接选项如-rdynamic;否则,只有十六进制的返回地址能被获取。backtrace_symbols()函数返回值是一个字符串指针,是通过malloc函数申请的空间,使用完后,调用者必需把它释放掉(直接free二级指针)。注:如果不能为字符串获取足够的空间,该函数的返回值为NULL。
我的理解是backtrace_symbols将backtrace获取的字符串通过符号表转化成实际地址,程序猿能看懂的内容。

[2020/07/20 15:22:28.657537 INFO]: [test.c:0044]:
=========>>>catch signal 11 <<<=========
[cwr] buf = H▒▒A▒Ɖ▒▒▒▒▒E▒▒I▒▒▒▒
[cwr] symbols = ./out(catch_core_dump_t+0x54) [0x4012a4]
[cwr] buf = H▒▒
[cwr] symbols = /lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7fd22965dcb0]
[cwr] buf =%
[cwr] symbols = ./out(dummy_function+0x13) [0x401063]
[cwr] buf = H▒t▒
[cwr] symbols = ./out(main+0x4f) [0x400f4f]
[cwr] buf = ▒▒蔢
[cwr] symbols = /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fd229648f45]
[cwr] buf = ▒▒ `
[cwr] symbols = ./out() [0x400f8e]
Segmentation fault (core dumped)

root@chenwr-pc:/home/workspace/chenwr/study/test# objdump -T out

out:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000400f00 g    DF .text  0000000000000065  Base        main
0000000000401050 g    DF .text  000000000000002e  Base        dummy_function
0000000000400d38 g    DF .init  0000000000000000  Base        _init

[cwr] symbols = ./out(dummy_function+0x13) [0x401063] 这下就能够搞清楚段错误的地址,然后通过addr2line这个工具转换成实际的地址。

=========>>>catch signal 11 <<<=========
[2020/07/20 15:33:33.156955 INFO]: [libcwr.c:0169]: dummy_function
/home/workspace/chenwr/study/test/test.c:70

[2020/07/20 15:33:33.162320 INFO]: [libcwr.c:0169]: main
/home/workspace/chenwr/study/test/test.c:78

backtrace_symbols_fd()函数,与backtrace_symbols()函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行。它不会调用malloc函数,因此,它可以应用在函数调用可能失败的情况下。

cat core_dump.txt
libcwr.so(catch_core_dump_fd+0xdc)[0x7f11cc3ada0c]
/lib/x86_64-linux-gnu/libc.so.6(+0x36cb0)[0x7f11cc019cb0]
./out(dummy_function+0x13)[0x400d43]
./out(main+0x27)[0x400c27]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7f11cc004f45]
./out[0x400c66]

参考资料:
linux backtrace()详细使用说明
objdump 符号表输出格式
gcc选项-g与-rdynamic的异同

现在就是有个疑问添加-g -rdynamic这些编译选项后,整个进程空间占用变得很大。实际应用过程中,大家是否都是开发debug阶段的时候开启,正式release版本关闭呢?

3.2 signal

头文件 #include <signal.h>
signal(参数1,参数2);
参数1:要处理的信号
参数2:信号处理方式
分三种,系统默认(SIG_DFL)、忽略(SIG_ING)和捕获。

signal(SIGSEGV, catch_core_dump_fd);为SIGSEGV信号安装新的处理函数。

SIGSEGV信号表示非法内存访问

Signal	Description
SIGABRT	由调用abort函数产生,进程非正常退出
SIGALRM	用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS	某种特定的硬件异常,通常由内存访问引起
SIGCANCEL	由Solaris Thread Library内部使用,通常不会使用
SIGCHLD	进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT	当被stop的进程恢复运行的时候,自动发送
SIGEMT	和实现相关的硬件异常
SIGFPE	数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE	Solaris专用,Hiberate或者Suspended时候发送
SIGHUP	发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL	非法指令异常
SIGINFO	BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT	由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO	异步IO事件
SIGIOT	实现相关的硬件异常,一般对应SIGABRT
SIGKILL	无法处理和忽略。中止某个进程
SIGLWP	由Solaris Thread Libray内部使用
SIGPIPE	在reader中止之后写Pipe的时候发送
SIGPOLL	当某个事件发送给Pollable Device的时候发送
SIGPROF	Setitimer指定的Profiling Interval Timer所产生
SIGPWR	和系统相关。和UPS相关。
SIGQUIT	输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV	非法内存访问
SIGSTKFLT	Linux专用,数学协处理器的栈异常
SIGSTOP	中止进程。无法处理和忽略。
SIGSYS	非法系统调用
SIGTERM	请求中止进程,kill命令缺省发送
SIGTHAW	Solaris专用,从Suspend恢复时候发送
SIGTRAP	实现相关的硬件异常。一般是调试异常
SIGTSTP	Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN	当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU	当Background Group的进程尝试写Terminal的时候发送
SIGURG	当out-of-band data接收的时候可能发送
SIGUSR1	用户自定义signal 1
SIGUSR2	用户自定义signal 2
SIGVTALRM	setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING	Solaris Thread Library内部实现专用
SIGWINCH	当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU	当CPU时间限制超时的时候
SIGXFSZ	进程超过文件大小限制
SIGXRES	Solaris专用,进程超过资源限制的时候发送

关于信号的内容,就不总结了,参考其他大神的博客吧。
signal() 函数详解
Linux 信号(signal)
linux SIGSEGV 信号捕捉,保证发生段错误后程序不崩溃

3.3 编译选项

在Ubuntu gcc编译中不添加-funwind-tables -ffunction-sections -finstrument-functions也能够正常打印地址,但是在ARM开发板环境下不添加这几个选项就无法打印。

[2020/07/13 13:31:08.116897 INFO]: [libcwr.c:0192]:
=========>>>catch signal 11 <<<=========
Segmentation fault

-funwind-tables
unwind table,这个表记录了与函数相关的信息,共三个字段:函数的起始地址,函数的结束地址,一个 info block 指针。

-ffunction-sections
将每个函数或符号创建为一个sections,其中每个sections名与function或data名保持一致。即使compiler为每个function和data item分配独立的section。

-finstrument-functions
在每个函数的入口和出口处会各增加一个额外的hook函数的调用,增加的这两个函数分别为:

void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit  (void *this_fn, void *call_site);

其中第一个参数为当前函数的起始地址,第二个参数为返回地址,即caller函数中的地址。
GCC的几个重要选项解释
使用gcc的-finstrument-functions选项进行函数跟踪

3.4 其他

获取当前进程名的方式。

printf("%s\n", argv[0]);

结果为./out。(out为我创建的进程名)
必须int main(int argc, char **argv) 这种形式才能打印而且还得字符串处理掉./ 换种兼容性比较好的方式。
通过获取进程信息来获取进程名。

sscanf 中%*s 作用是忽略字符串,直至到空格作为结束字样。刚好Name: 后面有个空格。

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux segmentation fault core dumped是一个常见的错误提示,它通常意味着程序在运行时出现了严重的问题,导致操作系统无法继续执行进程并产生了core dump文件。core dump文件是系统在发生异常时自动生成的文件,它包含了发生异常时的内存状态,可以帮助开发者进行问题排查和调试。 Segmentation fault通常是由于程序访问了不属于它的内存区域所导致的。这可能是由于程序中的指针错误、数组越界访问、非法内存访问等原因引起的。当程序发生segmentation fault时,操作系统会将进程的状态保存到一个core dump文件中,以便后续进行调试和分析。 要查看core dump文件,可以使用以下命令: ```shell $ gdb <program_name> <core_dump_file> ``` 其中,`<program_name>`是发生segmentation fault程序名称,`<core_dump_file>`是生成的core dump文件的路径。使用gdb工具可以打开core dump文件并进行调试,以找出导致segmentation fault的具体原因。 要解决segmentation fault问题,可以采取以下步骤: 1. 检查程序中的指针和内存访问是否正确,避免越界访问和非法内存访问。 2. 检查程序是否使用了动态分配的内存,并确保在使用完毕后释放了所有分配的内存。 3. 调试程序,使用gdb工具打开core dump文件并逐步执行程序,查看在发生segmentation fault时的内存状态,找出问题所在。 4. 如果问题仍然无法解决,可以尝试使用其他工具或方法进行调试和分析,例如使用valgrind等内存检测工具。 总之,Linux segmentation fault core dumped是一个常见的错误提示,它通常是由于程序访问了不属于它的内存区域所导致的。通过查看core dump文件并进行调试和分析,可以找出导致segmentation fault的具体原因并加以解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值