-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Context
I'm working on a program that runs on freestanding. It should be able to debug/introspect itself, like in this blog post. Specifically, for now I'd like to have full stack traces. Currently, this appears to be impossible to implement.
Issue
The blocker is that to get good stack traces, access to dwarf debuginfo is needed at runtime. Normally, this would happen by introspecting the elf file the program was loaded from, but this is not available on freestanding.
On freestanding, we need to get the debuginfo into memory that is loaded at runtime, so that we can call the functions from std.debug/std.dwarf on it. However, by default debuginfo is not loaded because they are not marked ALLOC in the elf file.
The most promising thing we tried (that didn't work)
One thing we tried was to do a two stage build, where:
- we produce an object file with debug info
- use objcopy to mark it as ALLOC
- then do a final linking step
This looks something like this:
zig build-obj -O Debug \
-fno-strip \
-target x86_64-freestanding \
-mno-red-zone \
main.zig
objcopy --set-section-flags ".debug*"=alloc main.o
zig build-exe --name main -fno-strip -T linker.ld main.o
The linkerscript is there to export the addresses of the debuginfo sections to be consumed by the program at runtime:
ENTRY(main)
SECTIONS
{
/* begin putting sections at 1 MiB */
. = 1M;
/* Executable code */
.text : ALIGN(4K)
{
KEEP(*(.multiboot))
*(.text)
*(.text.*)
}
/* Read-only data, e.g. strings */
.rodata : ALIGN(4K)
{
*(.rodata)
*(.rodata.*)
}
/* DWARF debug info */
.debug_info :
{
__debug_info_start = .;
KEEP(*(.debug_info))
__debug_info_end = .;
}
...
/* Read-write data (initialized), e.g. globals */
.data : ALIGN(4K)
{
*(.data)
*(.data.*)
}
/* Read-write data (uninitialized) and stack */
.bss : ALIGN(4K)
{
*(.bss)
*(.bss.*)
*(COMMON)
}
}
This exports __debug_*_{start,end}
constants that can be used to make slices of the debuginfo sections like so:
extern var __debug_info_start: u8;
extern var __debug_info_end: u8;
const debug_info_size = @ptrToInt(&__debug_info_end) - @ptrToInt(&__debug_info_start);
const debug_info = @ptrCast([*]u8, &__debug_info_start)[0..debug_info_size];
...
Now, this successfully gets the debuginfo loaded into memory at runtime, with the correct addresses to the dwarf sections and everything. However, this turns out to not work because the relocations inside the debuginfo are not processed. (verified using llvm-dwarfdump
)
(In fact from what I can tell, the relocation code in LLD seems to have a hardcoded assumption that ALLOC sections and debuginfo sections are mutually exclusive.)
Another thing we were considering is using something like split debug info, and then using a two stage build to include the debuginfo into the final binary. But this would have the issue that the debuginfo would be stale.
Reproducer/minimal setup for experimentation
Here is a paste.
Note that the objcopy invocation breaks gdb as well (for the obvious reason).
What we want
I think it'd be nice if there was a linker option or something along those lines to make the debug info in a zig binary marked as loadable. I'm not sure how feasible this is with lld
, but I'm filing this hoping that the zig linker can meet this use case.
The remaining issue would be of API design: specifically, there's a runtime component in the form of the variable bindings with the debuginfo pointers. My uninformed proposal is that there could be something like std.builtin.debuginfo.section_name
, and you could conditionally do something like @hasDecl(std.builtin, "debuginfo")
to see if it is there.
Thanks!