John Gallagher
Some debuggers have interfaces built around pipes, much like Bash and many
other shells do. In these debuggers, as in shells, a sequence of
output values from one command can be fed via a pipe, denoted by |
, into a
subsequent command for further processing. A number of commands can be strung
together in this way to form a pipeline. Examples of debuggers which use this
paradigm include Solaris/illumos's MDB and, for Linux, the new
SDB written by some of my
colleagues at Delphix.
There are a couple features of pipelines that make them well suited to be the interface for debugging tools:
With many other debuggers, answering such questions is often impractical. In many graphical debuggers, each dereference requires a mouse click, making it infeasible to answer questions when constructing the answer involves combing through tens or hundreds of thousands of pieces of state. The same is true for command-line debuggers which have simple expression-oriented interfaces without any sort of looping or aggregation functionality.
There are some non-piped debuggers which can be used to answer such questions, though. For instance, in GDB, users can write and execute Python scripts, which can contain arbitrary logic for examining and aggregating the in-memory state of a process. It's even possible to get an interactive Python session within GDB.
But I typically prefer a piped debugger to GDB's Python scripting when examining a process, for much the same reasons that I prefer to be dropped into a Bash shell instead of Python REPL when I log in to a troublesome machine which I need to begin debugging. Pipelines are more suited to the ad hoc tasks which arise when debugging; one-liners built from left to right are ideal for constructing queries incrementally and experimentally. The readability and error-handing facilities of a more general-purpose language are dropped in favor of making it easy to write throw-away queries of the sort written continually when investigating a thorny problem.
malloc
, causing
the process to panic. For reasons not relevant, the process was unable to grow its heap, but
I could see from the core dump that there was still plenty of space in the
existing heap available to the memory allocator (in this case illumos's libumem); only ~1.4G
of the >2G heap was in use:
> ::vmem
ADDR NAME INUSE TOTAL SUCCEED FAIL
fffffd7ffe1da6c0 sbrk_top 1456545792 2143137792 1932229 21495
fffffd7ffe1db2c8 sbrk_heap 1456545792 1456545792 1932229 0
fffffd7ffe1dbed0 vmem_internal 444039168 444039168 52763 0
fffffd7ffe1dcad8 vmem_seg 431939584 431939584 52727 0
fffffd7ffe1dd6e0 vmem_hash 12059648 12062720 31 0
fffffd7ffe1de2e8 vmem_vmem 46200 55344 15 0
0000000000411000 umem_internal 439447680 439451648 262495 0
0000000000412000 umem_cache 90480 118784 58 0
0000000000413000 umem_hash 12891648 12902400 143 0
0000000000414000 umem_log 0 0 0 0
I suspected that the allocation had failed because the free space in the heap was
fragmented such that there was no contiguous 128K chunk of memory available. To confirm
this suspicion, I wanted to get a mental picture of the free space in the heap (in particular,
the free segments in the arena, to use libumem's terminology). There isn't a built-in MDB
command to extract this information from a corefile, but it was fairly straight forward
to roll one of my own.
I started by taking the address of the struct vmem_t
representing top-level arena,
as displayed by the ::vmem
command above, and then printing the member of the struct
which represented the arena's list of free segments. The free list is represented as
a struct of type vmem_freelist_t
:
> fffffd7ffe1da6c0::print vmem_t vm_freelist[0]
vm_freelist[0] = {
vm_freelist[0].vs_start = 0
vm_freelist[0].vs_end = 0x1
vm_freelist[0].vs_knext = libumem.so.1`vmem0+0x398
vm_freelist[0].vs_kprev = 0
}
Next, I used the ::list
command to walk over the free list. It takes
the vmem_freelist_t
produced by the last command and walks over the list
by iteratively dereferencing the vs_knext
member of the struct. The
output of this part of the pipeline is a list of addresses:
> fffffd7ffe1da6c0::print vmem_t vm_freelist[0] | ::list vmem_freelist_t vs_knext
...
fffffd7ffe1daa58
fffffd7ffe1daa78
fffffd7ffe1daa98
fffffd7ffe1daab8
fffffd7ffe1daad8
fffffd7ffe1daaf8
fffffd7ffe1dab18
fffffd7ffe1dab38
...
Each of these addresses points to a vmem_seg_t
representing a single free
segment. I used the ::print
command again to tell MDB to cast the pointers
to that type, and then print out the members of the struct representing the
starting and ending addresses of the free segment, formatted as decimal numbers,
one segment per line.
> fffffd7ffe1da6c0::print vmem_t vm_freelist[0] | ::list vmem_freelist_t vs_knext | ::printf "%d %d\n" vmem_seg_t vs_start vs_end
...
974807040 974811136
1610952704 1610956800
930967552 930971648
848445440 848449536
1298153472 1298157568
...
Finally, I used !
to switch to Bash and do some aggregation. Everything after
the !
is
delegated to a Bash shell, and the output of the previous command is fed to the shell
via an OS pipe. Here, I filtered out the fake segments with starting
address of 0 which are used as special markers, used some awk to calculate the
segment size in kilobytes, and then counted up how many segments of each size there
are. The right column of the output is the size of a free segment in kilobytes, and
the left column represents the number of segments of the corresponding size.
> fffffd7ffe1da6c0::print vmem_t vm_freelist[0] | ::list vmem_freelist_t vs_knext | ::printf "%d %d\n" vmem_seg_t vs_start vs_end ! grep -v '^0' | awk '{ print ($2 - $1) / 1024 }' | sort -n | uniq -c
432 4
4177 8
3694 12
3371 16
4699 20
13098 24
1989 28
460 32
350 36
272 40
204 44
165 48
98 52
128 56
42 60
8 64
21 68
15 72
This matched my expectations: in the 654M of free space, there were no chunks large
enough to accommodate a 128K allocation. My suspicions confirmed, I was able to move on
with the investigation.
When it comes to debugging, a piped debugger isn't a silver bullet. I still had to figure out which question to ask, and had to learn enough about libumem's inner workings to know where in its data structures I could find the information I needed.
But once I had decided what information I wanted and had figured out where to find it, MDB allowed me to extract it easily, even though it involved examining a data structure containing thousands of items. It may not be particularly elegant (I find switching back and forth between MDB pipelines and Bash pipelines particularly awkward) but it is convenient and powerful, and when in the debugging trenches, I find those traits more valuable.