比较 2 个 GDB 核心转储

Posted

技术标签:

【中文标题】比较 2 个 GDB 核心转储【英文标题】:Compare 2 GDB-Core Dumps 【发布时间】:2020-04-21 08:57:25 【问题描述】:

我遇到了堆/堆栈损坏的严重问题。为了能够设置数据断点并找到问题的根源,我想使用 gdb 进行两个核心转储,然后比较它们。 第一个当我认为堆和堆栈还可以时,第二个在我的程序崩溃前不久。

如何比较这些转储?

关于我的项目的信息:

使用 gcc 5.x 具有 RT 支持的旧版第 3 方程序的插件。该项目没有可用的资源(对我而言)。 旧项目是 C,我的插件是 C++。

我尝试过的其他事情:

使用地址清理器 -> 不起作用,因为旧版程序不会从它们开始。 使用未定义的行为清理器 -> 相同 找出数据断点损坏的内存 -> 没有成功,因为损坏的内存不属于我的代码。 Ran Valgrind -> 我的代码没有错误。

感谢您的帮助

【问题讨论】:

试过 Valgrind 吗?如果这对您不起作用,请使用自定义的验证堆分配器。做起来不简单,但也不是火箭科学。您可能可以使用现有的,但我目前没有任何具体建议。 试过了。 Valgrind 没有打印任何相关内容。 【参考方案1】:

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

核心转储由一个 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  |................|

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

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

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

    dump1:正确的状态 dump2:状态不正确,没有分段错误 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
$1 = (int **) 0x602260
(gdb) print *g_state
$2 = (int *) 0x7fffffffe2bc
# dump2
(gdb) print g_state
$1 = (int **) 0x602260
(gdb) print *g_state
$2 = (int *) 0x4008c1
# dump3
$2 = (int **) 0x602260
(gdb) print *g_state
$3 = (int *) 0x1

基于该输出,我们可以清楚地看到*g_state 发生了变化,但仍然是dump2 中的有效指针。在dump3 中,指针变为无效。当然,我们希望自动进行这种比较。

知道核心转储是一个 ELF 文件,我们可以简单地解析它并自己生成一个差异。我们要做的:

    打开转储 识别转储的PROGBITS 部分 记住数据和地址信息 使用第二个转储重复该过程 比较两个数据集并打印差异

基于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

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

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

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

【讨论】:

以上是关于比较 2 个 GDB 核心转储的主要内容,如果未能解决你的问题,请参考以下文章

分析分段错误核心转储 (gdb)

gdb 调试远程核心转储

gdb 搜索核心转储内存

使用 gdb 分析核心转储帧

在 Linux 上使用核心转储和 gdb 如何使用近似虚拟内存 (VSZ)?

在核心转储文件上使用 gdb 获取变量的值