authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-03 13:58:41+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-30 13:44:50+01:00
logdd9cb1beead2d0b9a22decf089aadf51cfe90da8
tree13ea9b98876cf8409bb2123eccccf9cf7014a6fa
parent5e6a1919c730f7c3ad27b9e5c3ddc8938fc2f43f
signaturelock-open Commit is signed but in an unrecognized format.

doc comments


2 files changed, 97 insertions(+), 47 deletions(-)

lib/std/debug/Dwarf/Unwind.zig+93-45
...@@ -1,4 +1,24 @@...@@ -1,4 +1,24 @@
1//! MLUGG TODO DOCUMENT THIS1//! Contains state relevant to stack unwinding through the DWARF `.debug_frame` section, or the
2//! `.eh_frame` section which is an extension of the former specified by Linux Standard Base Core.
3//! Like `Dwarf`, no assumptions are made about the host's relationship to the target of the unwind
4//! information -- unwind data for any target can be read by any host.
5//!
6//! `Unwind` specifically deals with loading the data from CIEs and FDEs in the section, and with
7//! performing fast lookups of a program counter's corresponding FDE. The CFI instructions in the
8//! CIEs and FDEs can be interpreted by `VirtualMachine`.
9//!
10//! The typical usage of `Unwind` is as follows:
11//!
12//! * Initialize with `initEhFrameHdr` or `initSection`, depending on the available data
13//! * Call `prepareLookup` to construct a search table if necessary
14//! * Call `lookupPc` to find the section offset of the FDE corresponding to a PC
15//! * Call `getFde` to load the corresponding FDE and CIE
16//! * Check that the PC does indeed fall in that range (`lookupPc` may return a false positive)
17//! * Interpret the embedded CFI instructions using `VirtualMachine`
18//!
19//! In some cases, such as when using the "compact unwind" data in Mach-O binaries, the FDE offsets
20//! may already be known. In that case, no call to `lookupPc` is necessary, which means the call to
21//! `prepareLookup` can also be omitted.
222
3pub const VirtualMachine = @import("Unwind/VirtualMachine.zig");23pub const VirtualMachine = @import("Unwind/VirtualMachine.zig");
424
...@@ -8,7 +28,8 @@ frame_section: struct {...@@ -8,7 +28,8 @@ frame_section: struct {
8 /// the binary (e.g. `sh_addr` in an ELF file); the equivalent runtime address may be relocated28 /// the binary (e.g. `sh_addr` in an ELF file); the equivalent runtime address may be relocated
9 /// in position-independent binaries.29 /// in position-independent binaries.
10 vaddr: u64,30 vaddr: u64,
11 /// The full contents of the section. May have imprecise bounds depending on `section`.31 /// The full contents of the section. May have imprecise bounds depending on `section`. This
32 /// memory is externally managed.
12 ///33 ///
13 /// For `.debug_frame`, the slice length is exactly equal to the section length. This is needed34 /// For `.debug_frame`, the slice length is exactly equal to the section length. This is needed
14 /// to know the number of CIEs and FDEs.35 /// to know the number of CIEs and FDEs.
...@@ -22,13 +43,18 @@ frame_section: struct {...@@ -22,13 +43,18 @@ frame_section: struct {
22 bytes: []const u8,43 bytes: []const u8,
23},44},
2445
46/// A structure allowing fast lookups of the FDE corresponding to a particular PC. We use a binary
47/// search table for the lookup; essentially, a list of all FDEs ordered by PC range. `null` means
48/// the lookup data is not yet populated, so `prepareLookup` must be called before `lookupPc`.
25lookup: ?union(enum) {49lookup: ?union(enum) {
50 /// The `.eh_frame_hdr` section contains a pre-computed search table which we can use.
26 eh_frame_hdr: struct {51 eh_frame_hdr: struct {
27 /// Virtual address of the `.eh_frame_hdr` section.52 /// Virtual address of the `.eh_frame_hdr` section.
28 vaddr: u64,53 vaddr: u64,
29 table: EhFrameHeader.SearchTable,54 table: EhFrameHeader.SearchTable,
30 },55 },
31 /// Offsets into `frame_section` of FDEs, sorted by ascending `pc_begin`.56 /// There is no pre-computed search table, so we have built one ourselves.
57 /// Allocated into `gpa` and freed by `deinit`.
32 sorted_fdes: []SortedFdeEntry,58 sorted_fdes: []SortedFdeEntry,
33},59},
3460
...@@ -39,29 +65,13 @@ const SortedFdeEntry = struct {...@@ -39,29 +65,13 @@ const SortedFdeEntry = struct {
39 fde_offset: u64,65 fde_offset: u64,
40};66};
4167
42const Section = enum { debug_frame, eh_frame };68pub const Section = enum { debug_frame, eh_frame };
43
44/// Initialize with unwind information from the contents of a `.debug_frame` or `.eh_frame` section.
45///
46/// If the `.eh_frame_hdr` section is available, consider instead using `initEhFrameHdr`. This
47/// allows the implementation to use a search table embedded in that section if it is available.
48pub fn initSection(section: Section, section_vaddr: u64, section_bytes: []const u8) Unwind {
49 return .{
50 .frame_section = .{
51 .id = section,
52 .bytes = section_bytes,
53 .vaddr = section_vaddr,
54 },
55 .lookup = null,
56 };
57}
5869
59/// Initialize with unwind information from a header loaded from an `.eh_frame_hdr` section, and a70/// Initialize with unwind information from a header loaded from an `.eh_frame_hdr` section, and a
60/// pointer to the contents of the `.eh_frame` section.71/// pointer to the contents of the `.eh_frame` section.
61///72///
62/// This differs from `loadFromSection` because `.eh_frame_hdr` may embed a binary search table, and73/// `.eh_frame_hdr` may embed a binary search table of FDEs. If it does, we will use that table for
63/// if it does, this function will use that for address lookups instead of constructing our own74/// PC lookups rather than spending time constructing our own search table.
64/// search table.
65pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_ptr: [*]const u8) Unwind {75pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_ptr: [*]const u8) Unwind {
66 return .{76 return .{
67 .frame_section = .{77 .frame_section = .{
...@@ -76,6 +86,23 @@ pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_p...@@ -76,6 +86,23 @@ pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_p
76 };86 };
77}87}
7888
89/// Initialize with unwind information from the contents of a `.debug_frame` or `.eh_frame` section.
90///
91/// If the `.eh_frame_hdr` section is available, consider instead using `initEhFrameHdr`, which
92/// allows the implementation to use a search table embedded in that section if it is available.
93pub fn initSection(section: Section, section_vaddr: u64, section_bytes: []const u8) Unwind {
94 return .{
95 .frame_section = .{
96 .id = section,
97 .bytes = section_bytes,
98 .vaddr = section_vaddr,
99 },
100 .lookup = null,
101 };
102}
103
104/// Technically, it is only necessary to call this if `prepareLookup` has previously been called,
105/// since no other function here allocates resources.
79pub fn deinit(unwind: *Unwind, gpa: Allocator) void {106pub fn deinit(unwind: *Unwind, gpa: Allocator) void {
80 if (unwind.lookup) |lookup| switch (lookup) {107 if (unwind.lookup) |lookup| switch (lookup) {
81 .eh_frame_hdr => {},108 .eh_frame_hdr => {},
...@@ -83,8 +110,12 @@ pub fn deinit(unwind: *Unwind, gpa: Allocator) void {...@@ -83,8 +110,12 @@ pub fn deinit(unwind: *Unwind, gpa: Allocator) void {
83 };110 };
84}111}
85112
86/// This represents the decoded .eh_frame_hdr header113/// Decoded version of the `.eh_frame_hdr` section.
87pub const EhFrameHeader = struct {114pub const EhFrameHeader = struct {
115 /// The virtual address (i.e. as given in the binary, before relocations) of the `.eh_frame`
116 /// section. This value is important when using `.eh_frame_hdr` to find debug information for
117 /// the current binary, because it allows locating where the `.eh_frame` section is loaded in
118 /// memory (by adding it to the ELF module's base address).
88 eh_frame_vaddr: u64,119 eh_frame_vaddr: u64,
89 search_table: ?SearchTable,120 search_table: ?SearchTable,
90121
...@@ -93,6 +124,8 @@ pub const EhFrameHeader = struct {...@@ -93,6 +124,8 @@ pub const EhFrameHeader = struct {
93 offset: u8,124 offset: u8,
94 encoding: EH.PE,125 encoding: EH.PE,
95 fde_count: usize,126 fde_count: usize,
127 /// The actual table entries are viewed as a plain byte slice because `encoding` causes the
128 /// size of entries in the table to vary.
96 entries: []const u8,129 entries: []const u8,
97130
98 /// Returns the vaddr of the FDE for `pc`, or `null` if no matching FDE was found.131 /// Returns the vaddr of the FDE for `pc`, or `null` if no matching FDE was found.
...@@ -104,7 +137,7 @@ pub const EhFrameHeader = struct {...@@ -104,7 +137,7 @@ pub const EhFrameHeader = struct {
104 endian: Endian,137 endian: Endian,
105 ) !?u64 {138 ) !?u64 {
106 const table_vaddr = eh_frame_hdr_vaddr + table.offset;139 const table_vaddr = eh_frame_hdr_vaddr + table.offset;
107 const entry_size = try EhFrameHeader.entrySize(table.encoding, addr_size_bytes);140 const entry_size = try entrySize(table.encoding, addr_size_bytes);
108 var left: usize = 0;141 var left: usize = 0;
109 var len: usize = table.fde_count;142 var len: usize = table.fde_count;
110 while (len > 1) {143 while (len > 1) {
...@@ -131,18 +164,18 @@ pub const EhFrameHeader = struct {...@@ -131,18 +164,18 @@ pub const EhFrameHeader = struct {
131 }, endian);164 }, endian);
132 return fde_ptr;165 return fde_ptr;
133 }166 }
134 };
135167
136 pub fn entrySize(table_enc: EH.PE, addr_size_bytes: u8) !u8 {168 fn entrySize(table_enc: EH.PE, addr_size_bytes: u8) !u8 {
137 return switch (table_enc.type) {169 return switch (table_enc.type) {
138 .absptr => 2 * addr_size_bytes,170 .absptr => 2 * addr_size_bytes,
139 .udata2, .sdata2 => 4,171 .udata2, .sdata2 => 4,
140 .udata4, .sdata4 => 8,172 .udata4, .sdata4 => 8,
141 .udata8, .sdata8 => 16,173 .udata8, .sdata8 => 16,
142 .uleb128, .sleb128 => return bad(), // this is a binary search table; all entries must be the same size174 .uleb128, .sleb128 => return bad(), // this is a binary search table; all entries must be the same size
143 _ => return bad(),175 _ => return bad(),
144 };176 };
145 }177 }
178 };
146179
147 pub fn parse(180 pub fn parse(
148 eh_frame_hdr_vaddr: u64,181 eh_frame_hdr_vaddr: u64,
...@@ -169,7 +202,7 @@ pub const EhFrameHeader = struct {...@@ -169,7 +202,7 @@ pub const EhFrameHeader = struct {
169 const fde_count = try readEhPointer(&r, fde_count_enc, addr_size_bytes, .{202 const fde_count = try readEhPointer(&r, fde_count_enc, addr_size_bytes, .{
170 .pc_rel_base = eh_frame_hdr_vaddr + r.seek,203 .pc_rel_base = eh_frame_hdr_vaddr + r.seek,
171 }, endian);204 }, endian);
172 const entry_size = try entrySize(table_enc, addr_size_bytes);205 const entry_size = try SearchTable.entrySize(table_enc, addr_size_bytes);
173 const bytes_offset = r.seek;206 const bytes_offset = r.seek;
174 const bytes_len = cast(usize, fde_count * entry_size) orelse return error.EndOfStream;207 const bytes_len = cast(usize, fde_count * entry_size) orelse return error.EndOfStream;
175 const bytes = try r.take(bytes_len);208 const bytes = try r.take(bytes_len);
...@@ -188,7 +221,15 @@ pub const EhFrameHeader = struct {...@@ -188,7 +221,15 @@ pub const EhFrameHeader = struct {
188 }221 }
189};222};
190223
191pub const EntryHeader = union(enum) {224/// The shared header of an FDE/CIE, containing a length in bytes (DWARF's "initial length field")
225/// and a value which differentiates CIEs from FDEs and maps FDEs to their corresponding CIEs. The
226/// `.eh_frame` format also includes a third variation, here called `.terminator`, which acts as a
227/// sentinel for the whole section.
228///
229/// `CommonInformationEntry.parse` and `FrameDescriptionEntry.parse` expect the `EntryHeader` to
230/// have been parsed first: they accept data stored in the `EntryHeader`, and only read the bytes
231/// following this header.
232const EntryHeader = union(enum) {
192 cie: struct {233 cie: struct {
193 format: Format,234 format: Format,
194 /// Remaining bytes in the CIE. These are parseable by `CommonInformationEntry.parse`.235 /// Remaining bytes in the CIE. These are parseable by `CommonInformationEntry.parse`.
...@@ -206,7 +247,7 @@ pub const EntryHeader = union(enum) {...@@ -206,7 +247,7 @@ pub const EntryHeader = union(enum) {
206 /// keep track of how many section bytes remain when parsing all entries in `.debug_frame`.247 /// keep track of how many section bytes remain when parsing all entries in `.debug_frame`.
207 terminator,248 terminator,
208249
209 pub fn read(r: *Reader, header_section_offset: u64, section: Section, endian: Endian) !EntryHeader {250 fn read(r: *Reader, header_section_offset: u64, section: Section, endian: Endian) !EntryHeader {
210 const unit_header = try Dwarf.readUnitHeader(r, endian);251 const unit_header = try Dwarf.readUnitHeader(r, endian);
211 if (unit_header.unit_length == 0) return .terminator;252 if (unit_header.unit_length == 0) return .terminator;
212253
...@@ -284,7 +325,7 @@ pub const CommonInformationEntry = struct {...@@ -284,7 +325,7 @@ pub const CommonInformationEntry = struct {
284 ///325 ///
285 /// `length_offset` specifies the offset of this CIE's length field in the326 /// `length_offset` specifies the offset of this CIE's length field in the
286 /// .eh_frame / .debug_frame section.327 /// .eh_frame / .debug_frame section.
287 pub fn parse(328 fn parse(
288 cie_bytes: []const u8,329 cie_bytes: []const u8,
289 section: Section,330 section: Section,
290 default_addr_size_bytes: u8,331 default_addr_size_bytes: u8,
...@@ -364,7 +405,7 @@ pub const FrameDescriptionEntry = struct {...@@ -364,7 +405,7 @@ pub const FrameDescriptionEntry = struct {
364405
365 /// This function expects to read the FDE starting at the PC Begin field.406 /// This function expects to read the FDE starting at the PC Begin field.
366 /// The returned struct references memory backed by `fde_bytes`.407 /// The returned struct references memory backed by `fde_bytes`.
367 pub fn parse(408 fn parse(
368 /// The virtual address of the FDE we're parsing, *excluding* its entry header (i.e. the409 /// The virtual address of the FDE we're parsing, *excluding* its entry header (i.e. the
369 /// address is after the header). If `fde_bytes` is backed by the memory of a loaded410 /// address is after the header). If `fde_bytes` is backed by the memory of a loaded
370 /// module's `.eh_frame` section, this will equal `fde_bytes.ptr`.411 /// module's `.eh_frame` section, this will equal `fde_bytes.ptr`.
...@@ -405,6 +446,9 @@ pub const FrameDescriptionEntry = struct {...@@ -405,6 +446,9 @@ pub const FrameDescriptionEntry = struct {
405 }446 }
406};447};
407448
449/// Builds the PC FDE lookup table if it is not already built. It is required to call this function
450/// at least once before calling `lookupPc`. Once this function is called, memory has been allocated
451/// and so `deinit` (matching this `gpa`) is required to free it.
408pub fn prepareLookup(unwind: *Unwind, gpa: Allocator, addr_size_bytes: u8, endian: Endian) !void {452pub fn prepareLookup(unwind: *Unwind, gpa: Allocator, addr_size_bytes: u8, endian: Endian) !void {
409 if (unwind.lookup != null) return;453 if (unwind.lookup != null) return;
410454
...@@ -443,22 +487,24 @@ pub fn prepareLookup(unwind: *Unwind, gpa: Allocator, addr_size_bytes: u8, endia...@@ -443,22 +487,24 @@ pub fn prepareLookup(unwind: *Unwind, gpa: Allocator, addr_size_bytes: u8, endia
443 .debug_frame => if (saw_terminator) return bad(), // `.debug_frame` uses the section bounds and does not specify a sentinel entry487 .debug_frame => if (saw_terminator) return bad(), // `.debug_frame` uses the section bounds and does not specify a sentinel entry
444 }488 }
445489
446 const fde_slice = try fde_list.toOwnedSlice(gpa);490 std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
447 errdefer comptime unreachable;
448 std.mem.sortUnstable(SortedFdeEntry, fde_slice, {}, struct {
449 fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {491 fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
450 ctx;492 ctx;
451 return a.pc_begin < b.pc_begin;493 return a.pc_begin < b.pc_begin;
452 }494 }
453 }.lessThan);495 }.lessThan);
454 unwind.lookup = .{ .sorted_fdes = fde_slice };496
497 // This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
498 const final_fdes = try fde_list.toOwnedSlice(gpa);
499 unwind.lookup = .{ .sorted_fdes = final_fdes };
455}500}
456501
457/// Given a program counter value, returns the offset of the corresponding FDE, or `null` if no502/// Given a program counter value, returns the offset of the corresponding FDE, or `null` if no
458/// matching FDE was found. The returned offset can be passed to `getFde` to load the data503/// matching FDE was found. The returned offset can be passed to `getFde` to load the data
459/// associated with the FDE.504/// associated with the FDE.
460///505///
461/// Before calling this function, `prepareLookup` must return successfully.506/// Before calling this function, `prepareLookup` must return successfully at least once, to ensure
507/// that `unwind.lookup` is populated.
462///508///
463/// The return value may be a false positive. After loading the FDE with `loadFde`, the caller must509/// The return value may be a false positive. After loading the FDE with `loadFde`, the caller must
464/// validate that `pc` is indeed in its range -- if it is not, then no FDE matches `pc`.510/// validate that `pc` is indeed in its range -- if it is not, then no FDE matches `pc`.
...@@ -486,6 +532,8 @@ pub fn lookupPc(unwind: *const Unwind, pc: u64, addr_size_bytes: u8, endian: End...@@ -486,6 +532,8 @@ pub fn lookupPc(unwind: *const Unwind, pc: u64, addr_size_bytes: u8, endian: End
486 return sorted_fdes[first_bad_idx - 1].fde_offset;532 return sorted_fdes[first_bad_idx - 1].fde_offset;
487}533}
488534
535/// Get the FDE at a given offset, as well as its associated CIE. This offset typically comes from
536/// `lookupPc`. The CFI instructions within can be evaluated with `VirtualMachine`.
489pub fn getFde(unwind: *const Unwind, fde_offset: u64, addr_size_bytes: u8, endian: Endian) !struct { Format, CommonInformationEntry, FrameDescriptionEntry } {537pub fn getFde(unwind: *const Unwind, fde_offset: u64, addr_size_bytes: u8, endian: Endian) !struct { Format, CommonInformationEntry, FrameDescriptionEntry } {
490 const section = unwind.frame_section;538 const section = unwind.frame_section;
491539
lib/std/debug/SelfInfo.zig+4-2
...@@ -290,8 +290,10 @@ pub const UnwindContext = struct {...@@ -290,8 +290,10 @@ pub const UnwindContext = struct {
290 ) orelse return error.MissingDebugInfo;290 ) orelse return error.MissingDebugInfo;
291 const format, const cie, const fde = try unwind.getFde(fde_offset, @sizeOf(usize), native_endian);291 const format, const cie, const fde = try unwind.getFde(fde_offset, @sizeOf(usize), native_endian);
292292
293 // Check if this FDE *actually* includes the address.293 // Check if the FDE *actually* includes the pc (`lookupPc` can return false positives).
294 if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) return error.MissingDebugInfo;294 if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) {
295 return error.MissingDebugInfo;
296 }
295297
296 // Do not set `compile_unit` because the spec states that CFIs298 // Do not set `compile_unit` because the spec states that CFIs
297 // may not reference other debug sections anyway.299 // may not reference other debug sections anyway.