比较 2 个 GDB-Core 转储

Compare 2 GDB-Core Dumps

我遇到了 heap/stack 腐败的严重麻烦。为了能够设置数据断点并找到问题的根源,我想使用 gdb 进行两个核心转储,然后进行比较。 第一个是当我认为堆和堆栈仍然正常时,第二个是在我的程序崩溃前不久。

我如何比较这些转储?

我的项目信息:

我尝试过的其他事情:

感谢您的帮助

独立于你的潜在动机,我想谈谈你的问题。您询问如何识别两个核心转储之间的差异。这会很长,但希望能给你答案。

核心转储由一个 ELF 文件表示,该文件包含映射到给定进程的元数据和一组特定的内存区域(在 Linux 上,这可以通过 /proc/[pid]/coredump_filter 控制)在创建转储时。

比较转储的明显方法是比较十六进制表示:

$ diff -u <(hexdump -C dump1) <(hexdump -C dump2)
--- /dev/fd/63  2020-05-17 10:01:40.370524170 +0000
+++ /dev/fd/62  2020-05-17 10:01:40.370524170 +0000
@@ -90,8 +90,9 @@
 000005e0  00 00 00 00 00 00 00 00  00 00 00 00 80 1f 00 00  |................|
 000005f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

结果很少有用,因为您缺少上下文。更具体地说,没有直接的方法可以从文件中值更改的偏移量到对应于进程虚拟内存地址 space.

的偏移量

因此,如果需要,请提供更多上下文。最佳输出将是包含前后值的 VM 地址列表。

在开始之前,我们需要一个大致类似于您的测试场景。以下应用程序包含一个释放后使用内存问题,该问题最初不会导致分段错误(具有相同大小的新分配隐藏了该问题)。这里的想法是在每个阶段基于代码触发的断点使用 gdb (generate) 创建一个核心转储:

  1. dump1:正确状态
  2. dump2:状态不正确,没有分段错误
  3. dump3: 分段错误

代码:

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

int **g_state;

int main()
{
  int value = 1;
  g_state = malloc(sizeof(int*));
  *g_state = &value;
  if (g_state && *g_state) {
    printf("state: %d\n", **g_state);
  }
  printf("no corruption\n");
  raise(SIGTRAP);
  free(g_state);
  char **unrelated = malloc(sizeof(int*));
  *unrelated = "val";
  if (g_state && *g_state) {
    printf("state: %d\n", **g_state);
  }
  printf("use-after-free hidden by new allocation (invalid value)\n");
  raise(SIGTRAP);
  printf("use-after-free (segfault)\n");
  free(unrelated);
  int *unrelated2 = malloc(sizeof(intptr_t));
  *unrelated2 = 1;
  if (g_state && *g_state) {
    printf("state: %d\n", **g_state);
  }
  return 0;
}

现在,可以生成转储:

Starting program: test 
state: 1
no corruption

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffff7a488df in raise () from /lib64/libc.so.6
(gdb) generate dump1
Saved corefile dump1
(gdb) cont
Continuing.
state: 7102838
use-after-free hidden by new allocation (invalid value)

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00007ffff7a488df in raise () from /lib64/libc.so.6
(gdb) generate dump2
Saved corefile dump2
(gdb) cont
Continuing.
use-after-free (segfault)

Program received signal SIGSEGV, Segmentation fault.
main () at test.c:31
31          printf("state: %d\n", **g_state);
(gdb) generate dump3
Saved corefile dump3

快速手动检查显示相关差异:

# dump1
(gdb) print g_state
 = (int **) 0x602260
(gdb) print *g_state
 = (int *) 0x7fffffffe2bc
# dump2
(gdb) print g_state
 = (int **) 0x602260
(gdb) print *g_state
 = (int *) 0x4008c1
# dump3
 = (int **) 0x602260
(gdb) print *g_state
 = (int *) 0x1

根据该输出,我们可以清楚地看到 *g_state 已更改,但仍然是 dump2 中的有效指针。在dump3中,指针失效。当然,我们希望自动进行这种比较。

知道核心转储是一个ELF文件,我们可以简单地解析它并自己生成diff。我们将做什么:

  1. 打开转储
  2. 识别转储的 PROGBITS 部分
  3. 记住数据和地址信息
  4. 对第二个转储重复该过程
  5. 比较两个数据集并打印差异

基于elf.h,解析ELF文件相对容易。我创建了一个示例实现来比较两个转储并打印类似于使用 diff 比较两个 hexdump 输出的差异。该样本做了一些假设(x86_64,映射要么在地址和大小方面匹配,要么它们只存在于 dump1 或 dump2 中),省略了大多数错误处理,并且为了简洁起见总是选择一种简单的实现方法。

#include <elf.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define MAX_MAPPINGS 1024

struct dump
{
  char *base;
  Elf64_Shdr *mappings[MAX_MAPPINGS];
};

unsigned readdump(const char *path, struct dump *dump)
{
  unsigned count = 0;
  int fd = open(path, O_RDONLY);
  if (fd != -1) {
    struct stat stat;
    fstat(fd, &stat);
    dump->base = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    Elf64_Ehdr *header = (Elf64_Ehdr *)dump->base;
    Elf64_Shdr *secs = (Elf64_Shdr*)(dump->base+header->e_shoff);
    for (unsigned secinx = 0; secinx < header->e_shnum; secinx++) {
      if (secs[secinx].sh_type == SHT_PROGBITS) {
        if (count == MAX_MAPPINGS) {
          count = 0;
          break;
        }
        dump->mappings[count] = &secs[secinx];
        count++;
      }
    }
    dump->mappings[count] = NULL;
  }
  return count;
}

#define DIFFWINDOW 16

void printsection(struct dump *dump, Elf64_Shdr *sec, const char mode,
  unsigned offset, unsigned sizelimit)
{
  unsigned char *data = (unsigned char *)(dump->base+sec->sh_offset);
  uintptr_t addr = sec->sh_addr+offset;
  unsigned size = sec->sh_size;
  data += offset;
  if (sizelimit) {
    size = sizelimit;
  }
  unsigned start = 0;
  for (unsigned i = 0; i < size; i++) {
    if (i%DIFFWINDOW == 0) {
      printf("%c%016x ", mode, addr+i);
      start = i;
    }
    printf(" %02x", data[i]);
    if ((i+1)%DIFFWINDOW == 0 || i + 1 == size) {
      printf(" [");
      for (unsigned j = start; j <= i; j++) {
        putchar((data[j] >= 32 && data[j] < 127)?data[j]:'.');
      }
      printf("]\n");
    }
    addr++;
  }
}

void printdiff(struct dump *dump1, Elf64_Shdr *sec1,
  struct dump *dump2, Elf64_Shdr *sec2)
{
  unsigned char *data1 = (unsigned char *)(dump1->base+sec1->sh_offset);
  unsigned char *data2 = (unsigned char *)(dump2->base+sec2->sh_offset);
  unsigned difffound = 0;
  unsigned start = 0;
  for (unsigned i = 0; i < sec1->sh_size; i++) {
    if (i%DIFFWINDOW == 0) {
      start = i;
      difffound = 0;
    }
    if (!difffound && data1[i] != data2[i]) {
      difffound = 1;
    }
    if ((i+1)%DIFFWINDOW == 0 || i + 1 == sec1->sh_size) {
      if (difffound) {
        printsection(dump1, sec1, '-', start, DIFFWINDOW);
        printsection(dump2, sec2, '+', start, DIFFWINDOW);
      }
    }
  }
}

int main(int argc, char **argv)
{
  if (argc != 3) {
    fprintf(stderr, "Usage: compare DUMP1 DUMP2\n");
    return 1;
  }
  struct dump dump1;
  struct dump dump2;
  if (readdump(argv[1], &dump1) == 0 ||
      readdump(argv[2], &dump2) == 0) {
    fprintf(stderr, "Failed to read dumps\n");
    return 1;
  }
  unsigned sinx1 = 0;
  unsigned sinx2 = 0;
  while (dump1.mappings[sinx1] || dump2.mappings[sinx2]) {
    Elf64_Shdr *sec1 = dump1.mappings[sinx1];
    Elf64_Shdr *sec2 = dump2.mappings[sinx2];
    if (sec1 && sec2) {
      if (sec1->sh_addr == sec2->sh_addr) {
        // in both
        printdiff(&dump1, sec1, &dump2, sec2);
        sinx1++;
        sinx2++;
      }
      else if (sec1->sh_addr < sec2->sh_addr) {
        // in 1, not 2
        printsection(&dump1, sec1, '-', 0, 0);
        sinx1++;
      }
      else {
        // in 2, not 1
        printsection(&dump2, sec2, '+', 0, 0);
        sinx2++;
      }
    }
    else if (sec1) {
      // in 1, not 2
      printsection(&dump1, sec1, '-', 0, 0);
      sinx1++;
    }
    else {
      // in 2, not 1
      printsection(&dump2, sec2, '+', 0, 0);
      sinx2++;
    }
  } 
  return 0;
}

通过示例实现,我们可以重新评估上面的场景。 A 除了第一个差异:

$ ./compare dump1 dump2
-0000000000601020  86 05 40 00 00 00 00 00 50 3e a8 f7 ff 7f 00 00 [..@.....P>......]
+0000000000601020  00 6f a9 f7 ff 7f 00 00 50 3e a8 f7 ff 7f 00 00 [.o......P>......]
-0000000000602260  bc e2 ff ff ff 7f 00 00 00 00 00 00 00 00 00 00 [................]
+0000000000602260  c1 08 40 00 00 00 00 00 00 00 00 00 00 00 00 00 [..@.............]
-0000000000602280  6e 6f 20 63 6f 72 72 75 70 74 69 6f 6e 0a 00 00 [no corruption...]
+0000000000602280  75 73 65 2d 61 66 74 65 72 2d 66 72 65 65 20 68 [use-after-free h]
-0000000000602290  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
+0000000000602290  69 64 64 65 6e 20 62 79 20 6e 65 77 20 61 6c 6c [idden by new all]

差异显示 *gstate(地址 0x602260)已从 0x7fffffffe2bc 更改为 0x4008c1

-0000000000602260  bc e2 ff ff ff 7f 00 00 00 00 00 00 00 00 00 00 [................]
+0000000000602260  c1 08 40 00 00 00 00 00 00 00 00 00 00 00 00 00 [..@.............]

只有相关偏移量的第二个差异:

$ ./compare dump1 dump2
-0000000000602260  c1 08 40 00 00 00 00 00 00 00 00 00 00 00 00 00 [..@.............]
+0000000000602260  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]

差异显示 *gstate(地址 0x602260)已从 0x4008c1 更改为 0x1

你有它,一个核心转储差异。现在,这在您的场景中是否有用取决于多种因素,其中之一是两次转储之间的时间范围以及 activity 发生在 window 内。较大的差异可能难以分析,因此目标必须是通过仔细选择差异来最小化其大小。window。

您拥有的上下文越多,分析就越容易。例如,如果其中的更改与您的情况相关,可以通过将差异限制在相关库的 .data.bss 部分的地址来缩小差异的相关范围。

另一种缩小范围的方法:排除库未引用的内存更改。任意堆分配和特定库之间的关系不是很明显。根据初始差异中更改的地址,您可以在差异实现中搜索库的 .data.bss 部分中的指针。这并没有考虑到所有可能的引用(最明显的是来自其他分配的间接引用、库拥有的线程的寄存器和堆栈引用),但这是一个开始。