c-比较2个GDB核心转储



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

我如何比较这些垃圾

关于我的项目的信息:

  • 使用gcc 5.x
  • 用于支持RT的传统第三方程序的插件。没有可用于该项目的资源(对我来说)
  • 遗留项目是C,我的插件是C++

我尝试过的其他事情:

  • 使用地址清理程序->将不起作用,因为遗留程序不会从它们开始
  • 使用未定义的行为消毒剂->相同
  • 弄清楚哪些内存因数据断点而损坏->没有成功,因为损坏的内存不属于我的代码
  • Ran Valgrind->我的代码没有错误

感谢的帮助

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

核心转储由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地址的列表,包括before和after值。

在我们开始之前,我们需要一个与您的测试场景大致相似的测试场景。以下应用程序包含一个释放后使用内存问题,该问题最初不会导致分段错误(具有相同大小的新分配会隐藏该问题)。这里的想法是在每个阶段基于代码触发的断点使用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: %dn", **g_state);
}
printf("no corruptionn");
raise(SIGTRAP);
free(g_state);
char **unrelated = malloc(sizeof(int*));
*unrelated = "val";
if (g_state && *g_state) {
printf("state: %dn", **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: %dn", **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: %dn", **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文件,我们可以简单地解析它并自己生成一个diff。我们要做的:

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

基于elf.h,解析ELF文件相对容易。我创建了一个示例实现,它比较两个转储并打印一个diff,类似于使用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 DUMP2n");
return 1;
}
struct dump dump1;
struct dump dump2;
if (readdump(argv[1], &dump1) == 0 ||
readdump(argv[2], &dump2) == 0) {
fprintf(stderr, "Failed to read dumpsn");
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;
}

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

$ ./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 [................]

diff显示*gstate(地址0x602260)从0x4008c1变为0x1

这就是核心转储差异。现在,这在您的场景中是否有用取决于各种因素,其中一个因素是两个转储之间的时间框架以及在该窗口内发生的活动。一个大的diff可能很难分析,所以目标必须是通过仔细选择diff窗口来最小化其大小。

你的上下文越多,分析就越容易。例如,如果差异的变化与你的情况有关,可以通过将差异限制在有问题的库的.data.bss部分的地址来减少差异的相关范围。

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

最新更新