比较 2 个 GDB-Core 转储
Compare 2 GDB-Core Dumps
我遇到了 heap/stack 腐败的严重麻烦。为了能够设置数据断点并找到问题的根源,我想使用 gdb 进行两个核心转储,然后进行比较。
第一个是当我认为堆和堆栈仍然正常时,第二个是在我的程序崩溃前不久。
我如何比较这些转储?
我的项目信息:
- 使用 gcc 5.x
- 具有 RT 支持的旧式第 3 方程序的插件。该项目(对我而言)没有可用的资源。
- 遗留项目是 C,我的插件是 C++。
我尝试过的其他事情:
- 使用地址清理器 -> 将不起作用,因为遗留程序无法从它们开始。
- 使用未定义的行为消毒剂 -> 相同
- 找出哪些内存因数据断点而损坏 -> 没有成功,因为损坏的内存不属于我的代码。
- 运行 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 |................|
结果很少有用,因为您缺少上下文。更具体地说,没有直接的方法可以从文件中值更改的偏移量到对应于进程虚拟内存地址 space.
的偏移量
因此,如果需要,请提供更多上下文。最佳输出将是包含前后值的 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
= (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。我们将做什么:
- 打开转储
- 识别转储的
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
。
你有它,一个核心转储差异。现在,这在您的场景中是否有用取决于多种因素,其中之一是两次转储之间的时间范围以及 activity 发生在 window 内。较大的差异可能难以分析,因此目标必须是通过仔细选择差异来最小化其大小。window。
您拥有的上下文越多,分析就越容易。例如,如果其中的更改与您的情况相关,可以通过将差异限制在相关库的 .data
和 .bss
部分的地址来缩小差异的相关范围。
另一种缩小范围的方法:排除库未引用的内存更改。任意堆分配和特定库之间的关系不是很明显。根据初始差异中更改的地址,您可以在差异实现中搜索库的 .data
和 .bss
部分中的指针。这并没有考虑到所有可能的引用(最明显的是来自其他分配的间接引用、库拥有的线程的寄存器和堆栈引用),但这是一个开始。
我遇到了 heap/stack 腐败的严重麻烦。为了能够设置数据断点并找到问题的根源,我想使用 gdb 进行两个核心转储,然后进行比较。 第一个是当我认为堆和堆栈仍然正常时,第二个是在我的程序崩溃前不久。
我如何比较这些转储?
我的项目信息:
- 使用 gcc 5.x
- 具有 RT 支持的旧式第 3 方程序的插件。该项目(对我而言)没有可用的资源。
- 遗留项目是 C,我的插件是 C++。
我尝试过的其他事情:
- 使用地址清理器 -> 将不起作用,因为遗留程序无法从它们开始。
- 使用未定义的行为消毒剂 -> 相同
- 找出哪些内存因数据断点而损坏 -> 没有成功,因为损坏的内存不属于我的代码。
- 运行 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 |................|
结果很少有用,因为您缺少上下文。更具体地说,没有直接的方法可以从文件中值更改的偏移量到对应于进程虚拟内存地址 space.
的偏移量因此,如果需要,请提供更多上下文。最佳输出将是包含前后值的 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
= (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。我们将做什么:
- 打开转储
- 识别转储的
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
。
你有它,一个核心转储差异。现在,这在您的场景中是否有用取决于多种因素,其中之一是两次转储之间的时间范围以及 activity 发生在 window 内。较大的差异可能难以分析,因此目标必须是通过仔细选择差异来最小化其大小。window。
您拥有的上下文越多,分析就越容易。例如,如果其中的更改与您的情况相关,可以通过将差异限制在相关库的 .data
和 .bss
部分的地址来缩小差异的相关范围。
另一种缩小范围的方法:排除库未引用的内存更改。任意堆分配和特定库之间的关系不是很明显。根据初始差异中更改的地址,您可以在差异实现中搜索库的 .data
和 .bss
部分中的指针。这并没有考虑到所有可能的引用(最明显的是来自其他分配的间接引用、库拥有的线程的寄存器和堆栈引用),但这是一个开始。