coredump的那些事:02.coredump的分析

前言

在上一篇文章中,我们讲解了 如何配置 Linux 系统,让程序在崩溃时生成 coredump 文件

那么,生成了 coredump 之后,我们该如何使用它来定位问题?

在这一篇文章里,我们就来回答这个问题,在本文中你会了解到:

  • gdb 调试器的基本使用
  • coredump 文件的二进制细节
  • 为什么 coredump 能够辅助调试器重建现场
  • 内核源码如何生成 coredump

1. 从实验开始:生成并使用 coredump

我们先写一个最简单的崩溃程序,模拟空指针异常:

// heap_crash.cpp
void corrupt_heap()
{
    int *p = nullptr;
    *p = 10;   // 崩溃点
}

int main()
{
    corrupt_heap();
    return 0;
}

编译并加上 -g 保留调试信息:

g++ -g -o heap_crash heap_crash.cpp

启用 coredump:

ulimit -c unlimited
echo "/tmp/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

运行:

./heap_crash
段错误 (核心已转储)

/tmp 目录下,我们得到了一个 core 文件:

ls /tmp/core.heap_crash.*
/tmp/core.heap_crash.19642

此时我们就有了三样「调试三件套」:

  • 可执行文件 heap_crash
  • coredump 文件 core.heap_crash.19642
  • 调试信息(通过 -g 选项保留在 ELF 的 .debug_* 段)

2. gdb 调试 coredump

调试之前,需要确认本地已安装 gdb。

我们通过 man gdb 查看手册,可以找到如下说明:

You can also start with both an executable program and a core file specified:

    gdb program core

这意味着我们可以用「可执行文件 + core 文件」启动 gdb:

gdb ./heap_crash /tmp/core.heap_crash.19642

输出结果类似:

Core was generated by `./heap_crash'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000562a3c6e113d in corrupt_heap () at heap_crash.cpp:4
4           *p = 10;

此时,gdb 已经准确还原了崩溃时的现场。

常用调试命令

  • 查看调用栈:
(gdb) bt full
#0  0x0000562a3c6e113d in corrupt_heap () at heap_crash.cpp:4
        p = 0x0
#1  0x0000562a3c6e1153 in main () at heap_crash.cpp:9
  • 查看当前帧:
(gdb) f 0
#0  0x0000562a3c6e113d in corrupt_heap () at heap_crash.cpp:4
4           *p = 10;
  • 查看局部变量:
(gdb) info locals
p = 0x0
  • 查看寄存器:
(gdb) info registers
rip            0x562a3c6e113d      0x562a3c6e113d <corrupt_heap()+20>
rax            0x0
rsp            0x7ffd8a0e6ce0
...

注意:这些寄存器值,正是从 coredump 的 NOTE 段 里读取出来的。

3. coredump 文件的结构

coredump 文件的本质,其实就是一个 特殊类型的 ELF 文件

readelf -h 查看头部:

readelf -h core.heap_crash.19642 | grep Type
  Type:                              CORE (Core file)

readelf -l 查看程序头:

Elf 文件类型为 CORE (Core 文件)
Entry point 0x0
There are 24 program headers, starting at offset 64

程序头:
  Type           Offset   VirtAddr   PhysAddr
                 FileSiz  MemSiz     Flags  Align
  NOTE           0x580    0x0        0x0
                 0x15d0   0x0               0x4
  LOAD           0x2000   0x562a3c6e1000 0x0
                 0x1000   0x1000     R      0x1000
  LOAD           0x3000   0x562a3c6e2000 0x0
                 0x1000   0x1000     R E    0x1000
  ...

其中:

  • PT_NOTE 段
    保存寄存器、信号、线程信息:

    • NT_PRSTATUS → 寄存器状态
    • NT_SIGINFO → 崩溃信号
    • NT_AUXV → 程序辅助向量
    • NT_FPREGSET → 浮点寄存器
  • PT_LOAD 段
    保存进程的内存快照(代码段、堆、栈等)。

4. 内核源码如何生成 coredump

核心逻辑在 fs/binfmt_elf.celf_core_dump() 函数里。

简化版结构如下:

// fs/binfmt_elf.c
static int elf_core_dump(struct coredump_params *cprm)
{
    // 1. 收集线程/寄存器/信号等信息 → NOTE 段
    fill_note_info(&elf, e_phnum, &info, cprm);

    // 2. 遍历进程的虚拟内存区域 → LOAD 段
    for (i = 0; i < cprm->vma_count; i++) {
        struct core_vma_metadata *meta = cprm->vma_meta + i;
        dump_user_range(cprm, meta->start, meta->dump_size);
    }

    // 3. 将 ELF header、Program Header、NOTE、LOAD 写入文件
    dump_emit(...);
}

4.1 填充 NOTE 段

NOTE 段的数据来源于 fill_note_info()

static int fill_note_info(struct elfhdr *elf, int phdrs,
                          struct elf_note_info *info,
                          struct coredump_params *cprm)
{
    // 进程基本信息
    fill_psinfo(psinfo, ...);              // NT_PRPSINFO

    // 信号信息
    fill_siginfo_note(&info->signote, ...);// NT_SIGINFO

    // 辅助向量
    fill_auxv_note(&info->auxv, ...);      // NT_AUXV

    // 每个线程的寄存器
    for (t = info->thread; t != NULL; t = t->next) {
        fill_thread_core_info(t, ...);     // NT_PRSTATUS, NT_FPREGSET
    }
}

其中 fill_thread_core_info() 专门负责写入线程的相关信息,其中就包括寄存器:

static int fill_thread_core_info(struct elf_thread_core_info *t,
                                 const struct user_regset_view *view,
                                 long signr,
                                 struct elf_note_info *info)
{
    // 写入通用寄存器
    regset_get(t->task, &view->regsets[0],
               sizeof(t->prstatus.pr_reg),
               &t->prstatus.pr_reg);

    fill_note(&t->notes[0], "CORE", NT_PRSTATUS,
              sizeof(t->prstatus), &t->prstatus);

    // 写入浮点寄存器、扩展寄存器
    for (view_iter = 1; view_iter < view->n; ++view_iter) {
        const struct user_regset *regset = &view->regsets[view_iter];
        if (regset->core_note_type)
            regset_get_alloc(t->task, regset, ~0U, &data);
            fill_note(...);
    }
}

这正是 gdb 能够在 info registers 中显示寄存器值的原因。

4.2 填充 LOAD 段

LOAD 段就是「内存快照」:

   ......
for (i = 0; i < cprm->vma_count; i++) {
    struct core_vma_metadata *meta = cprm->vma_meta + i;

    struct elf_phdr phdr;
    phdr.p_type   = PT_LOAD;
    phdr.p_vaddr  = meta->start;
    phdr.p_filesz = meta->dump_size;
    phdr.p_memsz  = meta->end - meta->start;
    ......
    // 实际写入内存内容
    dump_user_range(cprm, meta->start, meta->dump_size);
}

当 gdb 执行 p variable 时,它会根据调试信息定位变量地址,再到对应的 PT_LOAD 段读取值。

5. 总结

在本文中,我们通过实验和源码分析,完整走了一遍 “如何利用 coredump 调试程序崩溃” 的流程:

  1. 实验

    • 使用 gdb program core 即可重现崩溃现场。
    • btinfo localsinfo registers 等命令帮助定位问题。
  2. ELF 结构

    • core 文件本质是 ELF,包含 NOTE 段和 LOAD 段。
    • NOTE 段保存寄存器、信号、线程信息。
    • LOAD 段保存内存快照(代码段、堆、栈等)。
  3. 内核源码

    • elf_core_dump() 调用 fill_note_info() 写入 NOTE 段。
    • 遍历 VMA,写入 PT_LOAD 段。
  4. 调试器原理

    • gdb 读取 NOTE 段,恢复寄存器和信号状态。
    • gdb 结合 LOAD 段和 DWARF 调试信息,重建调用栈和变量信息。

一句话总结:

coredump 是进程在崩溃瞬间的“冻结快照”,NOTE 段提供状态,LOAD 段提供内存,调试器则负责把这些还原成人类可理解的源码现场。

posted @ 2025-09-11 08:09  ToBrightmoon  阅读(60)  评论(0)    收藏  举报

© ToBrightmoon. All Rights Reserved.

Powered by Cnblogs & Designed with ❤️ by Gemini.

湘ICP备XXXXXXXX号-X