WIP: Add DWARF unwinding, and an external debug info loader for ELF
This supercedes (and includes the changes from) https://github.com/ziglang/zig/pull/15531.
These changes were developed in tandem with this PR, which might be a good reference for reviewers: https://github.com/kubkon/zig-dwarfdump/pull/1
Stack unwinding using DWARF unwind info
This change allows stacks to be unwound (and thus stack traces printed) for code that is compiled without -fomit-frame-pointer. This is accomplished by utilizing the DWARF debug info that is written into the .eh_frame section (or .debug_frame section). Conceptually, those sections contain a large lookup table mapping program addresses to a set of rules per-register that allow you to recover the value of that register in the previous frame - this allows you to recover the return address and previous stack frame values. Instead of actually storing the full lookup table (which would be large), it's stored as a set of instructions (not native code, but a special DWARF CFI bytecode), that allow you to build this table as needed. This system is how libunwind and debuggers themselves can unwind stacks.
This system is described in section 6.4.1 of this document: https://dwarfstd.org/doc/DWARF5.pdf
Changes:
- Added parsers for the information contained in
.eh_frame. I've utilizedzig-dwarfdumpas a testbed for the parsers (see linked PR above), and it's able to output an exact match ofllvm-dwarfdumpfor Ubuntu's libc currently. - Added
dwarf.call_frame.VirtualMachinewhich can run the CFI instructions and build the aforementioned register-rule lookup table. - Updated
debug.StackIteratorto utilize the DWARF debug info to unwind the stack, if it is available. It falls back to standard FP-based unwinding if it encounters any errors or missing info. I've tried to fail gracefully as much as possible - ie. a bad subtraction or overflow while running the DWARF opcodes results in an error (and fallback to FP unwinding) instead of panicing while panicing.
An example of how this improves the output, in this case calling puts with a bad pointer:
Before:
Segmentation fault at address 0x1234
???:?:?: 0x7f3103c0d97d in ??? (???)
src/lib.c:10:12: 0x7f3103ca1560 in add_mult2 (/home/kcbanner/temp/stack/src/lib.c)
return add_mult3(x, y, n);
^
src/lib.c:14:12: 0x7f3103ca1590 in add_mult1 (/home/kcbanner/temp/stack/src/lib.c)
return add_mult2(x, y, n);
^
src/lib.c:18:12: 0x7f3103ca15c0 in add_mult (/home/kcbanner/temp/stack/src/lib.c)
return add_mult1(x, y, n);
^
/home/kcbanner/temp/stack/src/main.zig:6:45: 0x20c5d7 in main (main)
std.debug.print("add: {}\n", .{ add_mult(5, 3, null) });
^
/home/kcbanner/kit/zig-linux-x86_64-0.11.0-dev.3031+f40539e5d/lib/std/start.zig:609:37: 0x20cbc6 in main (main)
const result = root.main() catch |err| {
^
Aborted (core dumped)
After:
Segmentation fault at address 0x1234
../sysdeps/x86_64/multiarch/strlen-avx2.S:74:0: 0x7f99cc88d97d in ??? (../sysdeps/x86_64/multiarch/strlen-avx2.S)
???:?:?: 0x7f99cc770ee7 in ??? (???)
src/lib.c:5:5: 0x7f99cc9214cb in add_mult3 (/home/kcbanner/temp/stack/src/lib.c)
puts((const char*)0x1234);
^
src/lib.c:10:12: 0x7f99cc921560 in add_mult2 (/home/kcbanner/temp/stack/src/lib.c)
return add_mult3(x, y, n);
^
src/lib.c:14:12: 0x7f99cc921590 in add_mult1 (/home/kcbanner/temp/stack/src/lib.c)
return add_mult2(x, y, n);
^
src/lib.c:18:12: 0x7f99cc9215c0 in add_mult (/home/kcbanner/temp/stack/src/lib.c)
return add_mult1(x, y, n);
^
/home/kcbanner/temp/stack/src/main.zig:6:45: 0x217eb7 in main (main)
std.debug.print("add: {}\n", .{ add_mult(5, 3, null) });
^
/mnt/c/cygwin64/home/kcbanner/kit/zig/lib/std/start.zig:609:37: 0x2184a6 in main (main)
const result = root.main() catch |err| {
^
Aborted (core dumped)
Note the ???:?:?: 0x7f99cc770ee7 in ??? (???) line can also be improved. It's currently not printing this puts frame due to a separate bug in the code that searches for line information in the debug info.
External debug info
Some distributions (ie. Ubuntu) have their libc debug info in separate files. This scheme allows for shipping debug info separately, which, can save space if the user doesn't actually need to debug the program / library. I've added support for reading external debug info:
- Add support for reading compressed ELF sections
- Support reading the build-id from the elf headers in order to lookup external debug info
- Support reading the
.gnu_debuglinksection to look up external debug info - Rework how sections are loaded from disk in order to support merging the list of sections that are present in the binary itself and the ones from the external debug info.
- Fixed up some memory management issues with the existing system.
As an example, in the stack traces above, their debug info actually comes from /usr/lib/debug/.build-id/69/389d485a9793dbe873f0ea2c93e02efaa9aa3d.debug.
Remaining Work
This is a work in progress, but it's at the point where I'd like to get it running against the CIs for arches / OSes I don't have access to, so this will be in draft status until these items are complete:
- [ ] Verify the unwinding code works on platforms other than
x86_64-linux - [ ] Figure out a way to build test cases for exercising the FDE/CIE parsers
- [x] Parse the
.eh_frame_hdrsection. This is an optimization that will speed up unwinding - it contains a sorted lookup table for the information in.eh_frame, which is binary searchable. - [x] Not all sections need to be read from disk (ie.
eh_frameandeh_frame_hdrare already mapped into the program's address space). Support using these already-mapped sections directly. This is particularly important for things that just measure stack traces and don't need to write them yet (ie. GPA capturing backtraces - with this improvement it could support-fomit-frame-pointertraces without touching the disk). - [ ] Support
.dwarf_frame. This is a very similar section to.eh_framewith some slight changes. - [ ] Implement the DWARF expression interpreter. Expressions are another type of bytecode that are used to encode certain register values. Currently expressions and their operands are parsed, but the interpreter itself doesn't have any logic yet.
- [ ] Update all uses in std of
StackIteratorto use the DWARF unwinding version, if available - [ ] Squash commits