DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Detecting and Solving Segmentation Faults in Linux Containers
  • Debugging and Performance Tuning in Pega Using PAL, Tracer, and Clipboard
  • Give Your AI Assistant Long-Term Memory With perag
  • Persistent Memory for AI Agents Using LangChain's Deep Agents

Trending

  • The Middleware Gap in AI Agent Frameworks
  • The Inter-Agent Protocol Problem
  • Machine Identity Debt: Why Human Identity Is No Longer Cloud Security's Primary Boundary
  • One of Waterfall's Most Resilient Artifacts

How to Compare Core Dumps for Simple Time Travel Debugging

Use glibc's elf.h to open two dumps and compare PROGBITS sections with minimal code.

By 
George R user avatar
George R
·
Updated Apr. 22, 21 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
8.5K Views

Join the DZone community and get the full member experience.

Join For Free

How can the difference between two Linux core dumps be identified and why would this even come up? This is going to be lengthy, but will hopefully give you your answer to both of those questions.

The Case for Comparing Core Dumps

Comparing two core dumps is only meaningful if they represent the same process at different points in time. If that's the case, they could be thought of as process snapshots. Consider an application that triggers a segmentation fault after a random uptime. If the root cause is suspected to be memory corruption and post-mortem debugging does not provide any hints, it would be helpful to go back in time to inspect the memory state before the fatal error.

In the best case, all of that should be done with minimal overhead because the issue only occurs in production in our thought experiment. Also, the actual memory locations of interest are unknown, so being able to visualize relevant memory changes before the fatal error would be desirable. A set of core dumps could provide simple low-overhead time travel debugging in respect to process memory.

In most real-world debugging scenarios involving memory corruption, a memory diff would be too large to be useful. In specific cases involving mostly read-only memory and a limited set of debugging alternatives, going the diff route might just be what's needed to identify the constellation leading to the corruption. With all that talk about comparing two core dumps, How can this diff even be generated?

Naive Comparison

A core dump is represented by an ELF file that contains metadata and a specific set of memory regions (on Linux, this can be controlled via /proc/[pid]/coredump_filter) that were mapped into the given process at the time of dump creation.

The obvious way to compare the dumps would be to compare a hex-representation:

Plain Text
 




x


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



The result is rarely useful because you're missing the context. More specifically, there's no straightforward way to get from the offset of a value change in the file to the offset corresponding to the process virtual memory address space.

So, more context if needed. The optimal output would be a list of VM addresses including before and after values.

Creating a Test Scenario

Before we can get on that, we need a test scenario to validate our comparison approach. The following sample includes a use-after-free memory issue that does not lead to a segmentation fault at first (a new allocation with the same size hides the issue). The idea here is to create a core dump using GDB (generate) during each phase based on break points triggered by the code:

  1. dump1: Correct state
  2. dump2: Incorrect state, no segmentation fault
  3. dump3: Segmentation fault

The sample code:

C
 




xxxxxxxxxx
1
35


 
1
#include <stdlib.h>
2
#include <unistd.h>
3
#include <signal.h>
4
#include <stdio.h>
5
 
          
6
int **g_state;
7
 
          
8
int main()
9
{
10
  int value = 1;
11
  g_state = malloc(sizeof(int*));
12
  *g_state = &value;
13
  if (g_state && *g_state) {
14
    printf("state: %d\n", **g_state);
15
  }
16
  printf("no corruption\n");
17
  raise(SIGTRAP);
18
  free(g_state);
19
  char **unrelated = malloc(sizeof(int*));
20
  *unrelated = "val";
21
  if (g_state && *g_state) {
22
    printf("state: %d\n", **g_state);
23
  }
24
  printf("use-after-free hidden by new allocation (invalid value)\n");
25
  raise(SIGTRAP);
26
  printf("use-after-free (segfault)\n");
27
  free(unrelated);
28
  int *unrelated2 = malloc(sizeof(intptr_t));
29
  *unrelated2 = 1;
30
  if (g_state && *g_state) {
31
    printf("state: %d\n", **g_state);
32
  }
33
  return 0;
34
}



Now, the dumps can be generated:

Plain Text
 




xxxxxxxxxx
1
27


 
1
Starting program: test
2
state: 1
3
no corruption
4
 
          
5
Program received signal SIGTRAP, Trace/breakpoint trap.
6
0x00007ffff7a488df in raise () from /lib64/libc.so.6
7
(gdb) generate dump1
8
Saved corefile dump1
9
(gdb) cont
10
Continuing.
11
state: 7102838
12
use-after-free hidden by new allocation (invalid value)
13
 
          
14
Program received signal SIGTRAP, Trace/breakpoint trap.
15
0x00007ffff7a488df in raise () from /lib64/libc.so.6
16
(gdb) generate dump2
17
Saved corefile dump2
18
(gdb) cont
19
Continuing.
20
use-after-free (segfault)
21
 
          
22
Program received signal SIGSEGV, Segmentation fault.
23
main () at test.c:31
24
31          printf("state: %d\n", **g_state);
25
(gdb) generate dump3
26
Saved corefile dump3



A quick manual inspection shows the relevant differences:

Plain Text
 




x


 
1
# dump1
2
(gdb) print g_state
3
$1 = (int **) 0x602260
4
(gdb) print *g_state
5
$2 = (int *) 0x7fffffffe2bc
6
             ^^^^^^^^^^^^^^
7
# dump2
8
(gdb) print g_state
9
$1 = (int **) 0x602260
10
(gdb) print *g_state
11
$2 = (int *) 0x4008c1
12
             ^^^^^^^^
13
# dump3
14
$2 = (int **) 0x602260
15
(gdb) print *g_state
16
$3 = (int *) 0x1
17
             ^^^



Based on that output, we can clearly see that *g_state changed but is still a valid pointer in dump2. In dump3, the pointer becomes invalid. Of course, we'd like to automate this comparison.

Context-Aware Comparison

Knowing that a core dump is an ELF file, we can simply parse it and generate a diff ourselves. What we'll do:

  1. Open a dump
  2. Identify PROGBITS sections of the dump
  3. Remember the data and address information
  4. Repeat the process with the second dump
  5. Compare the two data sets and print the diff

Based on elf.h, it's relatively easy to parse ELF files. I created a sample implementation that compares two dumps and prints a diff that is similar to comparing two hexdump outputs using diff. The sample makes some assumptions (x86_64, mappings either match in terms of address and size or they only exist in dump1 or dump2), omits most error handling and always chooses a simple implementation approach for the sake of brevity.

C
 




xxxxxxxxxx
1
39
144


 
1
#include <elf.h>
2
#include <fcntl.h>
3
#include <stdio.h>
4
#include <sys/mman.h>
5
#include <sys/stat.h>
6
 
          
7
#define MAX_MAPPINGS 1024
8
 
          
9
struct dump
10
{
11
  char *base;
12
  Elf64_Shdr *mappings[MAX_MAPPINGS];
13
};
14
 
          
15
unsigned readdump(const char *path, struct dump *dump)
16
{
17
  unsigned count = 0;
18
  int fd = open(path, O_RDONLY);
19
  if (fd != -1) {
20
    struct stat stat;
21
    fstat(fd, &stat);
22
    dump->base = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
23
    Elf64_Ehdr *header = (Elf64_Ehdr *)dump->base;
24
    Elf64_Shdr *secs = (Elf64_Shdr*)(dump->base+header->e_shoff);
25
    for (unsigned secinx = 0; secinx < header->e_shnum; secinx++) {
26
      if (secs[secinx].sh_type == SHT_PROGBITS) {
27
        if (count == MAX_MAPPINGS) {
28
          count = 0;
29
          break;
30
        }
31
        dump->mappings[count] = &secs[secinx];
32
        count++;
33
      }
34
    }
35
    dump->mappings[count] = NULL;
36
  }
37
  return count;
38
}
39
 
          
40
#define DIFFWINDOW 16
41
 
          
42
void printsection(struct dump *dump, Elf64_Shdr *sec, const char mode,
43
  unsigned offset, unsigned sizelimit)
44
{
45
  unsigned char *data = (unsigned char *)(dump->base+sec->sh_offset);
46
  uintptr_t addr = sec->sh_addr+offset;
47
  unsigned size = sec->sh_size;
48
  data += offset;
49
  if (sizelimit) {
50
    size = sizelimit;
51
  }
52
  unsigned start = 0;
53
  for (unsigned i = 0; i < size; i++) {
54
    if (i%DIFFWINDOW == 0) {
55
      printf("%c%016x ", mode, addr+i);
56
      start = i;
57
    }
58
    printf(" %02x", data[i]);
59
    if ((i+1)%DIFFWINDOW == 0 || i + 1 == size) {
60
      printf(" [");
61
      for (unsigned j = start; j <= i; j++) {
62
        putchar((data[j] >= 32 && data[j] < 127)?data[j]:'.');
63
      }
64
      printf("]\n");
65
    }
66
    addr++;
67
  }
68
}
69
 
          
70
void printdiff(struct dump *dump1, Elf64_Shdr *sec1,
71
  struct dump *dump2, Elf64_Shdr *sec2)
72
{
73
  unsigned char *data1 = (unsigned char *)(dump1->base+sec1->sh_offset);
74
  unsigned char *data2 = (unsigned char *)(dump2->base+sec2->sh_offset);
75
  unsigned difffound = 0;
76
  unsigned start = 0;
77
  for (unsigned i = 0; i < sec1->sh_size; i++) {
78
    if (i%DIFFWINDOW == 0) {
79
      start = i;
80
      difffound = 0;
81
    }
82
    if (!difffound && data1[i] != data2[i]) {
83
      difffound = 1;
84
    }
85
    if ((i+1)%DIFFWINDOW == 0 || i + 1 == sec1->sh_size) {
86
      if (difffound) {
87
        printsection(dump1, sec1, '-', start, DIFFWINDOW);
88
        printsection(dump2, sec2, '+', start, DIFFWINDOW);
89
      }
90
    }
91
  }
92
}
93
 
          
94
int main(int argc, char **argv)
95
{
96
  if (argc != 3) {
97
    fprintf(stderr, "Usage: compare DUMP1 DUMP2\n");
98
    return 1;
99
  }
100
  struct dump dump1;
101
  struct dump dump2;
102
  if (readdump(argv[1], &dump1) == 0 ||
103
      readdump(argv[2], &dump2) == 0) {
104
    fprintf(stderr, "Failed to read dumps\n");
105
    return 1;
106
  }
107
  unsigned sinx1 = 0;
108
  unsigned sinx2 = 0;
109
  while (dump1.mappings[sinx1] || dump2.mappings[sinx2]) {
110
    Elf64_Shdr *sec1 = dump1.mappings[sinx1];
111
    Elf64_Shdr *sec2 = dump2.mappings[sinx2];
112
    if (sec1 && sec2) {
113
      if (sec1->sh_addr == sec2->sh_addr) {
114
        // in both
115
        printdiff(&dump1, sec1, &dump2, sec2);
116
        sinx1++;
117
        sinx2++;
118
      }
119
      else if (sec1->sh_addr < sec2->sh_addr) {
120
        // in 1, not 2
121
        printsection(&dump1, sec1, '-', 0, 0);
122
        sinx1++;
123
      }
124
      else {
125
        // in 2, not 1
126
        printsection(&dump2, sec2, '+', 0, 0);
127
        sinx2++;
128
      }
129
    }
130
    else if (sec1) {
131
      // in 1, not 2
132
      printsection(&dump1, sec1, '-', 0, 0);
133
      sinx1++;
134
    }
135
    else {
136
      // in 2, not 1
137
      printsection(&dump2, sec2, '+', 0, 0);
138
      sinx2++;
139
    }
140
  }
141
  return 0;
142
}



With the sample implementation, we can re-evaluate our scenario above. A excerpt from the first diff:

Plain Text
 




xxxxxxxxxx
1


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



The diff shows that *gstate (address 0x602260) was changed from 0x7fffffffe2bc to 0x4008c1:

Plain Text
 




xxxxxxxxxx
1


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



The second diff with only the relevant offset:

Plain Text
 




xxxxxxxxxx
1


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



The diff shows that *gstate (address 0x602260) was changed from 0x4008c1 to 0x1.

Conclusion

There you have it: a core dump diff. Now, whether or not that can prove to be useful depends on various factors, one being the timeframe between the two dumps and the activity that takes place within that window. A large diff will possibly be difficult to analyze, so the aim must be to minimize its size by choosing the diff window carefully.

The more context you have, the easier the analysis will turn out to be. For example, the relevant scope of the diff could be reduced by limiting it to addresses of the .data and .bss sections of the executable or library to be debugged if changes in there are relevant to the debugging scenario.

Another approach to reduce the scope: excluding changes to memory that is not referenced by the debugging subject. The relationship between arbitrary heap allocations and the executable or specific libraries is not immediately apparent. Based on the the addresses of changes in your initial diff, you could search for pointers in the .data and .bss sections of the executable or library right in the diff implementation. This does not take every possible reference into account (most notably indirect references from other allocations, register and stack references of library-owned threads), but it's a start.

Dump (program) Diff Plain text Memory (storage engine) Segmentation fault Travel

Published at DZone with permission of George R. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Detecting and Solving Segmentation Faults in Linux Containers
  • Debugging and Performance Tuning in Pega Using PAL, Tracer, and Clipboard
  • Give Your AI Assistant Long-Term Memory With perag
  • Persistent Memory for AI Agents Using LangChain's Deep Agents

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook