如何调试没有core文件的coredump

摘要:在我们往期对coredump的分析中,是依赖于core文件的,而core文件中也几乎包含了程序当前的所有状态(堆栈、内存、寄存器等)。然而在实际的线上环境中,由于core文件太大、保存core文件耗时太久,出于线上系统的稳定性与快速恢复考虑,我们往往不会保留core文件。同时,程序堆栈被破坏的情况下,即使我们保留了core文件,也无法准确获取程序崩溃时准确的上下文信息。本文主要介绍在不保留core文件的情况下,如何获取程序崩溃时候的上下文信息(主要是函数调用栈)。

## 1.coredump原理

当程序发生内存越界访问等行为时,会触发OS的保护机制,此时OS会产生一个信号(signal)发送给对应的进程。当进程从内核态到用户态切换时,该进程会处理这个信号。此类信号(比如SEGV)的默认处理行为生成一个coredump文件。

这里会涉及以下几个问题:

1. 保存的core文件在什么地方?

2. core文件,具体会把进程地址空间的哪些内容保存下来?

3. 如何控制core文件的大小?

4. 如果在处理信号的时候,又产生了新的同类信号,该如何处理?

5. 处理信号的代码,是运行在用户态还是内核态?

6. 在一个多线程的程序中,是由哪个线程在处理这个信号?

问题4~问题6是信号处理的相关内容,我们不在这里解释,会在信号处理的章节详细分析。问题1~问题3解释如下

* `/proc/sys/kernel/core_pattern` 指定core文件存储的位置,缺省值是`core`,表示将core文件存储到当前目录。这个pattern是可以定制的,模式如下:

```

%p  出Core进程的PID

%u  出Core进程的UID

%s  造成Core的signal号

%t  出Core的时间,从1970-01-0100:00:00开始的秒数

%e  出Core进程对应的可执行文件名

```

* `/proc/sys/kernel/core_uses_pid` 取值是0或者1,表示是否在core文件名字后面加上进程号

* `/proc/$pid/coredump_filter` 设置那些内存会被dump出来

```

          bit 0  Dump anonymous private mappings.

          bit 1  Dump anonymous shared mappings.

          bit 2  Dump file-backed private mappings.

          bit 3  Dump file-backed shared mappings.

          bit 4 (since Linux 2.6.24)

                  Dump ELF headers.

          bit 5 (since Linux 2.6.28)

                  Dump private huge pages.

          bit 6 (since Linux 2.6.28)

                  Dump shared huge pages.

```

* `ulimit  -c ` 决定save的core文件大小限制

## 2.自定义信号处理函数

我们需要在自定义的信号处理函数中打印出程序崩溃时候的活跃函数堆栈信息。这里我们有两种方式:1.使用backtrace等方法,读取进程堆栈上的信息;2.在函数调用的同时,用户自己维护一套数据结构,用于保存函数调用链,在信号处理函数中,将这个函数调用链打印出来。

### 2.1使用backtrace获取函数调用链

在[从汇编语言看函数调用](http://www.uufool.com/?p=54)和[栈破坏下的coredump分析方法](http://www.uufool.com/?p=78)两篇文章中,我们知道进程堆栈上保存了rbp寄存器对应的list。backtrace本质上就是利用进程堆栈上的数据,推断出来的当前函数调用链。这里我们不分析backtrace的源码,直接给出关键性质的代码。

```cpp

void dump_trace(int Signal)

{

    const int len = 200;

    void* buffer[len];

    printf("dump_trace\n");

    int nptrs = ::backtrace(buffer, len);

    printf("backtrace\n");

    char** buffer_array = ::backtrace_symbols(buffer, nptrs);

    printf("sig:%d nptrs:%d\n", Signal, nptrs);

    if (buffer_array) {

        for (int i = 0; i < nptrs; ++i) {

            printf("frame=%d||trace_back=%s||\n", i, buffer_array[i]);

        }

        free(buffer_array);

    }

    exit(0);

}

signal(SIGSEGV, dump_trace);//注册信号处理函数

```

完整的代码可以参考[这里](https://github.com/yukun89/draft/tree/master/dump)。利用signal函数,我们将dump_trace注册为SIGSEGV的信号处理函数,来取代默认的保存core文件的行为。

### 2.2 用户自己维护一个函数调用链

为什么我们需要费力自己去维护一个函数调用链而不是直接调用backtrace呢? 因为遇到进程堆栈被写花的时候,我们是无法找到完整的函数调用栈信息的。自己去维护函数调用链的原理如下:维护一个堆栈,在函数调用的时候,将调用的函数入栈;函数调用结束时,将这个函数出栈。这样当coredump发生时,即使进程堆栈被破坏的情况下,这个用户自定义的函数堆栈中依然保存了函数调用链的信息。

那么如何在函数调用的开始和结束执行对应的操作呢?g++/gcc正好提供了这种功能,能够让我们在函数的开始和结束嵌入对应的代码。我们需要做的仅仅是实现两个预先声明的函数,核心代码如下

```cpp

#ifdef __cplusplus

extern "C" {

#endif

void __attribute__((no_instrument_function))

__cyg_profile_func_enter(void *this_func, void *call_site);

void __attribute__((no_instrument_function))

__cyg_profile_func_exit(void *this_func, void *call_site);

#ifdef __cplusplus

};

#endif

void __cyg_profile_func_enter(void *this_func, void *call_site)

{

    char buffer[64] = {0};

    int len = snprintf(buffer, 60, "%p call %p", this_func, call_site);

    std::string content = std::string(buffer);

    call_list.push(content);

    return ;

}

void __cyg_profile_func_exit(void *this_func, void *call_site)

{

    call_list.pop();

    return ;

}

```

代码解释:`void __cyg_profile_func_enter(void *this_func, void *call_site)` 函数有两个参数,第一参数表示调用方的地址,第二个参数表示被调用方的地址。需要注意的有以下几点:

* `__cyg_profile_func_enter`和`__cyg_profile_func_exit`这两个函数本身是需要设置属性`no_instrument_function`的;否则会陷入对这两个函数本身的无限递归调用。

* 为了将上述代码嵌入到每个函数的开始和结束,需要在编译代码的时候使用特定的编译参数`-finstrument-functions`

* 上述代码所在的编译单元是必须不能使用`-finstrument-functions`的:否则会陷入循环调用(想想为什么)

相关demo代码的说明在[这里](https://github.com/yukun89/draft/tree/master/dump),大家可以自行测试。我们只给出函数在crash时候的输出结果。对于文中的地址信息,我们可以使用addr2line获得这些地址对应的源文件地址。

```shell

sig:11

frame_0: 0x401172 call 0x4011f0

frame_1: 0x401172 call 0x4011e1

frame_2: 0x401172 call 0x4011f0

frame_3: 0x401172 call 0x4011e1

frame_4: 0x401172 call 0x4011f0

frame_5: 0x401172 call 0x40129f

frame_6: 0x40123e call 0x7f6e0bf52b15

```

## 3.coredump的各种可能性

综合前面所讲解的所有coredump的种类,我们总结coredump的各种可能性如下:

- 内存访问越界

  + 下标导致的数组访问越界

+ 字符串不包含对应结束符导致的越界访问

+ 使用strcpy, strcat, sprintf, strcmp,strcasecmp等字符串操作函数,将目标字符串读/写爆

2. 多线程数据未进行加锁保护:STL容器vector、map等都是非线程安全的

3. 指针相关

  + 空指针解引用

  + 非法的指针转化

  + use after free

  + double free

4. 堆栈相关

  + 栈变量过大导致的堆栈溢出

  + 栈变量的非法写入,导致程序调用栈被破坏无法回溯

原文发表于:[如何调试没有core文件的coredump](http://www.uufool.com/?p=151) 更多内容请访问[优孚网](www.uufool.com)

你可能感兴趣的:(如何调试没有core文件的coredump)