| author | |
| committer | |
| log | 099a95041054e456ebefbd75f6a4f9f6961002be |
| tree | 6293805ebdf2664d5b5906e614297972a7c8da6f |
| parent | 9c1821d3bfadc5eddd4dff271a4920c03ee0ffea |
| signature |
This has been a TODO for ages, but in the past it didn't really matter
because stack traces are typically printed to stderr for which a mutex
is held so in practice there was a mutex guarding usage of `SelfInfo`.
However, now that `SelfInfo` is also used for simply capturing traces,
thread safety is needed. Instead of just a single mutex, though, there
are a couple of different mutexes involved; this helps make critical
sections smaller, particularly when unwinding the stack as `unwindFrame`
doesn't typically need to hold any lock at all.6 files changed, 136 insertions(+), 44 deletions(-)
lib/std/debug.zig+5-5| ... | ... | @@ -238,7 +238,6 @@ pub fn print(comptime fmt: []const u8, args: anytype) void { |
| 238 | 238 | nosuspend bw.print(fmt, args) catch return; |
| 239 | 239 | } |
| 240 | 240 | |
| 241 | /// TODO multithreaded awareness | |
| 242 | 241 | /// Marked `inline` to propagate a comptime-known error to callers. |
| 243 | 242 | pub inline fn getSelfDebugInfo() !*SelfInfo { |
| 244 | 243 | if (!SelfInfo.target_supported) return error.UnsupportedTarget; |
| ... | ... | @@ -1169,7 +1168,8 @@ test printLineFromFile { |
| 1169 | 1168 | } |
| 1170 | 1169 | } |
| 1171 | 1170 | |
| 1172 | /// TODO multithreaded awareness | |
| 1171 | /// The returned allocator should be thread-safe if the compilation is multi-threaded, because | |
| 1172 | /// multiple threads could capture and/or print stack traces simultaneously. | |
| 1173 | 1173 | fn getDebugInfoAllocator() Allocator { |
| 1174 | 1174 | // Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`. |
| 1175 | 1175 | if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) { |
| ... | ... | @@ -1177,10 +1177,10 @@ fn getDebugInfoAllocator() Allocator { |
| 1177 | 1177 | } |
| 1178 | 1178 | // Otherwise, use a global arena backed by the page allocator |
| 1179 | 1179 | const S = struct { |
| 1180 | var arena: ?std.heap.ArenaAllocator = null; | |
| 1180 | var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); | |
| 1181 | var ts_arena: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena.allocator() }; | |
| 1181 | 1182 | }; |
| 1182 | if (S.arena == null) S.arena = .init(std.heap.page_allocator); | |
| 1183 | return S.arena.?.allocator(); | |
| 1183 | return S.ts_arena.allocator(); | |
| 1184 | 1184 | } |
| 1185 | 1185 | |
| 1186 | 1186 | /// Whether or not the current target can print useful debug information when a segfault occurs. |
lib/std/debug/Dwarf.zig+1-1| ... | ... | @@ -346,7 +346,7 @@ pub fn deinit(di: *Dwarf, gpa: Allocator) void { |
| 346 | 346 | di.* = undefined; |
| 347 | 347 | } |
| 348 | 348 | |
| 349 | pub fn getSymbolName(di: *Dwarf, address: u64) ?[]const u8 { | |
| 349 | pub fn getSymbolName(di: *const Dwarf, address: u64) ?[]const u8 { | |
| 350 | 350 | // Iterate the function list backwards so that we see child DIEs before their parents. This is |
| 351 | 351 | // important because `DW_TAG_inlined_subroutine` DIEs will have a range which is a sub-range of |
| 352 | 352 | // their caller, and we want to return the callee's name, not the caller's. |
lib/std/debug/SelfInfo.zig+47-12| ... | ... | @@ -18,7 +18,21 @@ const root = @import("root"); |
| 18 | 18 | |
| 19 | 19 | const SelfInfo = @This(); |
| 20 | 20 | |
| 21 | modules: if (target_supported) std.AutoArrayHashMapUnmanaged(usize, Module.DebugInfo) else void, | |
| 21 | /// Locks access to `modules`. However, does *not* lock the `Module.DebugInfo`, nor `lookup_cache` | |
| 22 | /// the implementation is responsible for locking as needed in its exposed methods. | |
| 23 | /// | |
| 24 | /// TODO: to allow `SelfInfo` to work on freestanding, we currently just don't use this mutex there. | |
| 25 | /// That's a bad solution, but a better one depends on the standard library's general support for | |
| 26 | /// "bring your own OS" being improved. | |
| 27 | modules_mutex: switch (builtin.os.tag) { | |
| 28 | else => std.Thread.Mutex, | |
| 29 | .freestanding, .other => struct { | |
| 30 | fn lock(_: @This()) void {} | |
| 31 | fn unlock(_: @This()) void {} | |
| 32 | }, | |
| 33 | }, | |
| 34 | /// Value is allocated into gpa to give it a stable pointer. | |
| 35 | modules: if (target_supported) std.AutoArrayHashMapUnmanaged(usize, *Module.DebugInfo) else void, | |
| 22 | 36 | lookup_cache: if (target_supported) Module.LookupCache else void, |
| 23 | 37 | |
| 24 | 38 | pub const Error = error{ |
| ... | ... | @@ -43,12 +57,16 @@ pub const supports_unwinding: bool = target_supported and Module.supports_unwind |
| 43 | 57 | pub const UnwindContext = if (supports_unwinding) Module.UnwindContext; |
| 44 | 58 | |
| 45 | 59 | pub const init: SelfInfo = .{ |
| 60 | .modules_mutex = .{}, | |
| 46 | 61 | .modules = .empty, |
| 47 | 62 | .lookup_cache = if (Module.LookupCache != void) .init, |
| 48 | 63 | }; |
| 49 | 64 | |
| 50 | 65 | pub fn deinit(self: *SelfInfo, gpa: Allocator) void { |
| 51 | for (self.modules.values()) |*di| di.deinit(gpa); | |
| 66 | for (self.modules.values()) |di| { | |
| 67 | di.deinit(gpa); | |
| 68 | gpa.destroy(di); | |
| 69 | } | |
| 52 | 70 | self.modules.deinit(gpa); |
| 53 | 71 | if (Module.LookupCache != void) self.lookup_cache.deinit(gpa); |
| 54 | 72 | } |
| ... | ... | @@ -56,21 +74,35 @@ pub fn deinit(self: *SelfInfo, gpa: Allocator) void { |
| 56 | 74 | pub fn unwindFrame(self: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize { |
| 57 | 75 | comptime assert(supports_unwinding); |
| 58 | 76 | const module: Module = try .lookup(&self.lookup_cache, gpa, context.pc); |
| 59 | const gop = try self.modules.getOrPut(gpa, module.key()); | |
| 60 | self.modules.lockPointers(); | |
| 61 | defer self.modules.unlockPointers(); | |
| 62 | if (!gop.found_existing) gop.value_ptr.* = .init; | |
| 63 | return module.unwindFrame(gpa, gop.value_ptr, context); | |
| 77 | const di: *Module.DebugInfo = di: { | |
| 78 | self.modules_mutex.lock(); | |
| 79 | defer self.modules_mutex.unlock(); | |
| 80 | const gop = try self.modules.getOrPut(gpa, module.key()); | |
| 81 | if (gop.found_existing) break :di gop.value_ptr.*; | |
| 82 | errdefer _ = self.modules.pop().?; | |
| 83 | const di = try gpa.create(Module.DebugInfo); | |
| 84 | di.* = .init; | |
| 85 | gop.value_ptr.* = di; | |
| 86 | break :di di; | |
| 87 | }; | |
| 88 | return module.unwindFrame(gpa, di, context); | |
| 64 | 89 | } |
| 65 | 90 | |
| 66 | 91 | pub fn getSymbolAtAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { |
| 67 | 92 | comptime assert(target_supported); |
| 68 | 93 | const module: Module = try .lookup(&self.lookup_cache, gpa, address); |
| 69 | const gop = try self.modules.getOrPut(gpa, module.key()); | |
| 70 | self.modules.lockPointers(); | |
| 71 | defer self.modules.unlockPointers(); | |
| 72 | if (!gop.found_existing) gop.value_ptr.* = .init; | |
| 73 | return module.getSymbolAtAddress(gpa, gop.value_ptr, address); | |
| 94 | const di: *Module.DebugInfo = di: { | |
| 95 | self.modules_mutex.lock(); | |
| 96 | defer self.modules_mutex.unlock(); | |
| 97 | const gop = try self.modules.getOrPut(gpa, module.key()); | |
| 98 | if (gop.found_existing) break :di gop.value_ptr.*; | |
| 99 | errdefer _ = self.modules.pop().?; | |
| 100 | const di = try gpa.create(Module.DebugInfo); | |
| 101 | di.* = .init; | |
| 102 | gop.value_ptr.* = di; | |
| 103 | break :di di; | |
| 104 | }; | |
| 105 | return module.getSymbolAtAddress(gpa, di, address); | |
| 74 | 106 | } |
| 75 | 107 | |
| 76 | 108 | pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 { |
| ... | ... | @@ -88,6 +120,9 @@ pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) |
| 88 | 120 | /// be valid to consider the entire application one module, or on the other hand to consider each |
| 89 | 121 | /// object file a module. |
| 90 | 122 | /// |
| 123 | /// Because different threads can collect stack traces concurrently, the implementation must be able | |
| 124 | /// to tolerate concurrent calls to any method it implements. | |
| 125 | /// | |
| 91 | 126 | /// This type must must expose the following declarations: |
| 92 | 127 | /// |
| 93 | 128 | /// ``` |
lib/std/debug/SelfInfo/DarwinModule.zig+22-2| ... | ... | @@ -252,6 +252,15 @@ fn loadMachO(module: *const DarwinModule, gpa: Allocator) !DebugInfo.LoadedMachO |
| 252 | 252 | }; |
| 253 | 253 | } |
| 254 | 254 | pub fn getSymbolAtAddress(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol { |
| 255 | // We need the lock for a few things: | |
| 256 | // * loading the Mach-O module | |
| 257 | // * loading the referenced object file | |
| 258 | // * scanning the DWARF of that object file | |
| 259 | // * building the line number table of that object file | |
| 260 | // That's enough that it doesn't really seem worth scoping the lock more tightly than the whole function.. | |
| 261 | di.mutex.lock(); | |
| 262 | defer di.mutex.unlock(); | |
| 263 | ||
| 255 | 264 | if (di.loaded_macho == null) di.loaded_macho = module.loadMachO(gpa) catch |err| switch (err) { |
| 256 | 265 | error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| return e, |
| 257 | 266 | else => return error.ReadFailed, |
| ... | ... | @@ -341,8 +350,12 @@ pub fn unwindFrame(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, |
| 341 | 350 | }; |
| 342 | 351 | } |
| 343 | 352 | fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize { |
| 344 | if (di.unwind == null) di.unwind = module.loadUnwindInfo(); | |
| 345 | const unwind = &di.unwind.?; | |
| 353 | const unwind: *const DebugInfo.Unwind = u: { | |
| 354 | di.mutex.lock(); | |
| 355 | defer di.mutex.unlock(); | |
| 356 | if (di.unwind == null) di.unwind = module.loadUnwindInfo(); | |
| 357 | break :u &di.unwind.?; | |
| 358 | }; | |
| 346 | 359 | |
| 347 | 360 | const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo; |
| 348 | 361 | if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo; |
| ... | ... | @@ -649,10 +662,17 @@ fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, |
| 649 | 662 | return ret_addr; |
| 650 | 663 | } |
| 651 | 664 | pub const DebugInfo = struct { |
| 665 | /// Held while checking and/or populating `unwind` or `loaded_macho`. | |
| 666 | /// Once a field is populated and the pointer `&di.loaded_macho.?` or `&di.unwind.?` has been | |
| 667 | /// gotten, the lock is released; i.e. it is not held while *using* the loaded info. | |
| 668 | mutex: std.Thread.Mutex, | |
| 669 | ||
| 652 | 670 | unwind: ?Unwind, |
| 653 | 671 | loaded_macho: ?LoadedMachO, |
| 654 | 672 | |
| 655 | 673 | pub const init: DebugInfo = .{ |
| 674 | .mutex = .{}, | |
| 675 | ||
| 656 | 676 | .unwind = null, |
| 657 | 677 | .loaded_macho = null, |
| 658 | 678 | }; |
lib/std/debug/SelfInfo/ElfModule.zig+42-20| ... | ... | @@ -7,16 +7,26 @@ gnu_eh_frame: ?[]const u8, |
| 7 | 7 | pub const LookupCache = void; |
| 8 | 8 | |
| 9 | 9 | pub const DebugInfo = struct { |
| 10 | /// Held while checking and/or populating `loaded_elf`/`scanned_dwarf`/`unwind`. | |
| 11 | /// Once data is populated and a pointer to the field has been gotten, the lock | |
| 12 | /// is released; i.e. it is not held while *using* the loaded debug info. | |
| 13 | mutex: std.Thread.Mutex, | |
| 14 | ||
| 10 | 15 | loaded_elf: ?ElfFile, |
| 11 | 16 | scanned_dwarf: bool, |
| 12 | 17 | unwind: [2]?Dwarf.Unwind, |
| 13 | 18 | pub const init: DebugInfo = .{ |
| 19 | .mutex = .{}, | |
| 14 | 20 | .loaded_elf = null, |
| 15 | 21 | .scanned_dwarf = false, |
| 16 | 22 | .unwind = @splat(null), |
| 17 | 23 | }; |
| 18 | 24 | pub fn deinit(di: *DebugInfo, gpa: Allocator) void { |
| 19 | 25 | if (di.loaded_elf) |*loaded_elf| loaded_elf.deinit(gpa); |
| 26 | for (di.unwind) |*opt_unwind| { | |
| 27 | const unwind = &(opt_unwind orelse continue); | |
| 28 | unwind.deinit(gpa); | |
| 29 | } | |
| 20 | 30 | } |
| 21 | 31 | }; |
| 22 | 32 | |
| ... | ... | @@ -145,34 +155,41 @@ fn loadElf(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void |
| 145 | 155 | } |
| 146 | 156 | } |
| 147 | 157 | pub fn getSymbolAtAddress(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol { |
| 148 | if (di.loaded_elf == null) try module.loadElf(gpa, di); | |
| 149 | 158 | const vaddr = address - module.load_offset; |
| 150 | if (di.loaded_elf.?.dwarf) |*dwarf| { | |
| 151 | if (!di.scanned_dwarf) { | |
| 152 | dwarf.open(gpa, native_endian) catch |err| switch (err) { | |
| 159 | { | |
| 160 | di.mutex.lock(); | |
| 161 | defer di.mutex.unlock(); | |
| 162 | if (di.loaded_elf == null) try module.loadElf(gpa, di); | |
| 163 | const loaded_elf = &di.loaded_elf.?; | |
| 164 | // We need the lock if using DWARF, as we might scan the DWARF or build a line number table. | |
| 165 | if (loaded_elf.dwarf) |*dwarf| { | |
| 166 | if (!di.scanned_dwarf) { | |
| 167 | dwarf.open(gpa, native_endian) catch |err| switch (err) { | |
| 168 | error.InvalidDebugInfo, | |
| 169 | error.MissingDebugInfo, | |
| 170 | error.OutOfMemory, | |
| 171 | => |e| return e, | |
| 172 | error.EndOfStream, | |
| 173 | error.Overflow, | |
| 174 | error.ReadFailed, | |
| 175 | error.StreamTooLong, | |
| 176 | => return error.InvalidDebugInfo, | |
| 177 | }; | |
| 178 | di.scanned_dwarf = true; | |
| 179 | } | |
| 180 | return dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) { | |
| 153 | 181 | error.InvalidDebugInfo, |
| 154 | 182 | error.MissingDebugInfo, |
| 155 | 183 | error.OutOfMemory, |
| 156 | 184 | => |e| return e, |
| 185 | error.ReadFailed, | |
| 157 | 186 | error.EndOfStream, |
| 158 | 187 | error.Overflow, |
| 159 | error.ReadFailed, | |
| 160 | 188 | error.StreamTooLong, |
| 161 | 189 | => return error.InvalidDebugInfo, |
| 162 | 190 | }; |
| 163 | di.scanned_dwarf = true; | |
| 164 | 191 | } |
| 165 | return dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) { | |
| 166 | error.InvalidDebugInfo, | |
| 167 | error.MissingDebugInfo, | |
| 168 | error.OutOfMemory, | |
| 169 | => |e| return e, | |
| 170 | error.ReadFailed, | |
| 171 | error.EndOfStream, | |
| 172 | error.Overflow, | |
| 173 | error.StreamTooLong, | |
| 174 | => return error.InvalidDebugInfo, | |
| 175 | }; | |
| 192 | // Otherwise, we're just going to scan the symtab, which we don't need the lock for; fall out of this block. | |
| 176 | 193 | } |
| 177 | 194 | // When there's no DWARF available, fall back to searching the symtab. |
| 178 | 195 | return di.loaded_elf.?.searchSymtab(gpa, vaddr) catch |err| switch (err) { |
| ... | ... | @@ -231,9 +248,14 @@ fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Erro |
| 231 | 248 | } |
| 232 | 249 | } |
| 233 | 250 | pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize { |
| 234 | if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di); | |
| 235 | std.debug.assert(di.unwind[0] != null); | |
| 236 | for (&di.unwind) |*opt_unwind| { | |
| 251 | const unwinds: *const [2]?Dwarf.Unwind = u: { | |
| 252 | di.mutex.lock(); | |
| 253 | defer di.mutex.unlock(); | |
| 254 | if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di); | |
| 255 | std.debug.assert(di.unwind[0] != null); | |
| 256 | break :u &di.unwind; | |
| 257 | }; | |
| 258 | for (unwinds) |*opt_unwind| { | |
| 237 | 259 | const unwind = &(opt_unwind.* orelse break); |
| 238 | 260 | return context.unwindFrame(gpa, unwind, module.load_offset, null) catch |err| switch (err) { |
| 239 | 261 | error.MissingDebugInfo => continue, // try the next one |
lib/std/debug/SelfInfo/WindowsModule.zig+19-4| ... | ... | @@ -9,14 +9,14 @@ pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) std.debug.Sel |
| 9 | 9 | if (lookupInCache(cache, address)) |m| return m; |
| 10 | 10 | { |
| 11 | 11 | // Check a new module hasn't been loaded |
| 12 | cache.rwlock.lock(); | |
| 13 | defer cache.rwlock.unlock(); | |
| 12 | 14 | cache.modules.clearRetainingCapacity(); |
| 13 | ||
| 14 | 15 | const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0); |
| 15 | 16 | if (handle == windows.INVALID_HANDLE_VALUE) { |
| 16 | 17 | return windows.unexpectedError(windows.GetLastError()); |
| 17 | 18 | } |
| 18 | 19 | defer windows.CloseHandle(handle); |
| 19 | ||
| 20 | 20 | var entry: windows.MODULEENTRY32 = undefined; |
| 21 | 21 | entry.dwSize = @sizeOf(windows.MODULEENTRY32); |
| 22 | 22 | if (windows.kernel32.Module32First(handle, &entry) != 0) { |
| ... | ... | @@ -30,12 +30,18 @@ pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) std.debug.Sel |
| 30 | 30 | return error.MissingDebugInfo; |
| 31 | 31 | } |
| 32 | 32 | pub fn getSymbolAtAddress(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, address: usize) std.debug.SelfInfo.Error!std.debug.Symbol { |
| 33 | // The `Pdb` API doesn't really allow us *any* thread-safe access, and the `Dwarf` API isn't | |
| 34 | // great for it either; just lock the whole thing. | |
| 35 | di.mutex.lock(); | |
| 36 | defer di.mutex.unlock(); | |
| 37 | ||
| 33 | 38 | if (!di.loaded) module.loadDebugInfo(gpa, di) catch |err| switch (err) { |
| 34 | 39 | error.OutOfMemory, error.InvalidDebugInfo, error.MissingDebugInfo, error.Unexpected => |e| return e, |
| 35 | 40 | error.FileNotFound => return error.MissingDebugInfo, |
| 36 | 41 | error.UnknownPDBVersion => return error.UnsupportedDebugInfo, |
| 37 | 42 | else => return error.ReadFailed, |
| 38 | 43 | }; |
| 44 | ||
| 39 | 45 | // Translate the runtime address into a virtual address into the module |
| 40 | 46 | const vaddr = address - module.base_address; |
| 41 | 47 | |
| ... | ... | @@ -50,7 +56,9 @@ pub fn getSymbolAtAddress(module: *const WindowsModule, gpa: Allocator, di: *Deb |
| 50 | 56 | |
| 51 | 57 | return error.MissingDebugInfo; |
| 52 | 58 | } |
| 53 | fn lookupInCache(cache: *const LookupCache, address: usize) ?WindowsModule { | |
| 59 | fn lookupInCache(cache: *LookupCache, address: usize) ?WindowsModule { | |
| 60 | cache.rwlock.lockShared(); | |
| 61 | defer cache.rwlock.unlockShared(); | |
| 54 | 62 | for (cache.modules.items) |*entry| { |
| 55 | 63 | const base_address = @intFromPtr(entry.modBaseAddr); |
| 56 | 64 | if (address >= base_address and address < base_address + entry.modBaseSize) { |
| ... | ... | @@ -182,13 +190,19 @@ fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) ! |
| 182 | 190 | di.loaded = true; |
| 183 | 191 | } |
| 184 | 192 | pub const LookupCache = struct { |
| 193 | rwlock: std.Thread.RwLock, | |
| 185 | 194 | modules: std.ArrayListUnmanaged(windows.MODULEENTRY32), |
| 186 | pub const init: LookupCache = .{ .modules = .empty }; | |
| 195 | pub const init: LookupCache = .{ | |
| 196 | .rwlock = .{}, | |
| 197 | .modules = .empty, | |
| 198 | }; | |
| 187 | 199 | pub fn deinit(lc: *LookupCache, gpa: Allocator) void { |
| 188 | 200 | lc.modules.deinit(gpa); |
| 189 | 201 | } |
| 190 | 202 | }; |
| 191 | 203 | pub const DebugInfo = struct { |
| 204 | mutex: std.Thread.Mutex, | |
| 205 | ||
| 192 | 206 | loaded: bool, |
| 193 | 207 | |
| 194 | 208 | coff_image_base: u64, |
| ... | ... | @@ -205,6 +219,7 @@ pub const DebugInfo = struct { |
| 205 | 219 | coff_section_headers: []coff.SectionHeader, |
| 206 | 220 | |
| 207 | 221 | pub const init: DebugInfo = .{ |
| 222 | .mutex = .{}, | |
| 208 | 223 | .loaded = false, |
| 209 | 224 | .coff_image_base = undefined, |
| 210 | 225 | .mapped_file = null, |