| ... | @@ -1,4 +1,24 @@ | ... | @@ -1,4 +1,24 @@ |
| 1 | //! MLUGG TODO DOCUMENT THIS | 1 | //! 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. |
| 2 | | 22 | |
| 3 | pub const VirtualMachine = @import("Unwind/VirtualMachine.zig"); | 23 | pub const VirtualMachine = @import("Unwind/VirtualMachine.zig"); |
| 4 | | 24 | |
| ... | @@ -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 relocated | 28 | /// 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 needed | 34 | /// 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 | }, |
| 24 | | 45 | |
| | 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`. |
| 25 | lookup: ?union(enum) { | 49 | lookup: ?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 | }, |
| 34 | | 60 | |
| ... | @@ -39,29 +65,13 @@ const SortedFdeEntry = struct { | ... | @@ -39,29 +65,13 @@ const SortedFdeEntry = struct { |
| 39 | fde_offset: u64, | 65 | fde_offset: u64, |
| 40 | }; | 66 | }; |
| 41 | | 67 | |
| 42 | const Section = enum { debug_frame, eh_frame }; | 68 | pub 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. | | |
| 48 | pub 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 | } | | |
| 58 | | 69 | |
| 59 | /// Initialize with unwind information from a header loaded from an `.eh_frame_hdr` section, and a | 70 | /// 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, and | 73 | /// `.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 own | 74 | /// PC lookups rather than spending time constructing our own search table. |
| 64 | /// search table. | | |
| 65 | pub fn initEhFrameHdr(header: EhFrameHeader, section_vaddr: u64, section_bytes_ptr: [*]const u8) Unwind { | 75 | pub 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 | } |
| 78 | | 88 | |
| | 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. |
| | 93 | pub 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. |
| 79 | pub fn deinit(unwind: *Unwind, gpa: Allocator) void { | 106 | pub 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 | } |
| 85 | | 112 | |
| 86 | /// This represents the decoded .eh_frame_hdr header | 113 | /// Decoded version of the `.eh_frame_hdr` section. |
| 87 | pub const EhFrameHeader = struct { | 114 | pub 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, |
| 90 | | 121 | |
| ... | @@ -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, |
| 97 | | 130 | |
| 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 | }; | | |
| 135 | | 167 | |
| 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 size | 174 | .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 | }; |
| 146 | | 179 | |
| 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 | }; |
| 190 | | 223 | |
| 191 | pub 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. |
| | 232 | const 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, |
| 208 | | 249 | |
| 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; |
| 212 | | 253 | |
| ... | @@ -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 the | 326 | /// `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 { |
| 364 | | 405 | |
| 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. the | 409 | /// 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 loaded | 410 | /// 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 | }; |
| 407 | | 448 | |
| | 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. |
| 408 | pub fn prepareLookup(unwind: *Unwind, gpa: Allocator, addr_size_bytes: u8, endian: Endian) !void { | 452 | pub 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; |
| 410 | | 454 | |
| ... | @@ -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 entry | 487 | .debug_frame => if (saw_terminator) return bad(), // `.debug_frame` uses the section bounds and does not specify a sentinel entry |
| 444 | } | 488 | } |
| 445 | | 489 | |
| 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 | } |
| 456 | | 501 | |
| 457 | /// Given a program counter value, returns the offset of the corresponding FDE, or `null` if no | 502 | /// 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 data | 503 | /// 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 must | 509 | /// 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 | } |
| 488 | | 534 | |
| | 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`. |
| 489 | pub fn getFde(unwind: *const Unwind, fde_offset: u64, addr_size_bytes: u8, endian: Endian) !struct { Format, CommonInformationEntry, FrameDescriptionEntry } { | 537 | pub 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; |
| 491 | | 539 | |