lib/choir/src/backends/debug.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const ir = @import("../core/root.zig");
3 const sys = @import("sys");
4
5 const Allocator = std.mem.Allocator;
6
7 pub const LineInfo = struct {
8 file: ?[]const u8,
9 line: u32,
10 column: u32,
11 name: ?[]const u8,
12 op_name: []const u8,
13 };
14
15 pub const LineEntry = struct {
16 offset: u32,
17 info: LineInfo,
18 };
19
20 const LocationView = struct {
21 file: ?[]const u8 = null,
22 line: u32 = 0,
23 column: u32 = 0,
24 name: ?[]const u8 = null,
25 };
26
27 const StringTable = struct {
28 allocator: Allocator,
29 items: std.ArrayListUnmanaged([]const u8),
30 index: std.StringHashMapUnmanaged(usize),
31
32 fn init(allocator: Allocator) StringTable {
33 return .{
34 .allocator = allocator,
35 .items = .empty,
36 .index = .{},
37 };
38 }
39
40 fn deinit(self: *StringTable) void {
41 for (self.items.items) |item| {
42 self.allocator.free(item);
43 }
44 self.items.deinit(self.allocator);
45 self.index.deinit(self.allocator);
46 }
47
48 fn reset(self: *StringTable) void {
49 for (self.items.items) |item| {
50 self.allocator.free(item);
51 }
52 self.items.clearRetainingCapacity();
53 self.index.clearRetainingCapacity();
54 }
55
56 fn take(self: *StringTable) StringTable {
57 const out = self.*;
58 self.* = StringTable.init(self.allocator);
59 return out;
60 }
61
62 fn intern(self: *StringTable, s: []const u8) Allocator.Error![]const u8 {
63 if (s.len == 0) return s;
64 if (self.index.get(s)) |idx| {
65 return self.items.items[idx];
66 }
67 const owned = try self.allocator.dupe(u8, s);
68 errdefer self.allocator.free(owned);
69 try self.items.append(self.allocator, owned);
70 errdefer _ = self.items.pop();
71 try self.index.put(self.allocator, owned, self.items.items.len - 1);
72 return owned;
73 }
74 };
75
76 pub const LineTable = struct {
77 allocator: Allocator,
78 entries: []LineEntry,
79 strings: StringTable,
80
81 pub fn deinit(self: *LineTable) void {
82 self.strings.deinit();
83 if (self.entries.len != 0) {
84 self.allocator.free(self.entries);
85 }
86 self.* = undefined;
87 }
88
89 pub fn isEmpty(self: *const LineTable) bool {
90 return self.entries.len == 0;
91 }
92
93 pub fn lookup(self: *const LineTable, offset: u32) ?LineInfo {
94 if (self.entries.len == 0) return null;
95
96 var lo: usize = 0;
97 var hi: usize = self.entries.len;
98 while (lo < hi) {
99 const mid = lo + (hi - lo) / 2;
100 if (self.entries[mid].offset > offset) {
101 hi = mid;
102 } else {
103 lo = mid + 1;
104 }
105 }
106
107 if (lo == 0) return null;
108 return self.entries[lo - 1].info;
109 }
110 };
111
112 pub const LineTableBuilder = struct {
113 allocator: Allocator,
114 entries: std.ArrayListUnmanaged(LineEntry),
115 strings: StringTable,
116
117 pub fn init(allocator: Allocator) LineTableBuilder {
118 return .{
119 .allocator = allocator,
120 .entries = .empty,
121 .strings = StringTable.init(allocator),
122 };
123 }
124
125 pub fn deinit(self: *LineTableBuilder) void {
126 self.entries.deinit(self.allocator);
127 self.strings.deinit();
128 }
129
130 pub fn reset(self: *LineTableBuilder) void {
131 self.entries.clearRetainingCapacity();
132 self.strings.reset();
133 }
134
135 pub fn record(self: *LineTableBuilder, offset: u32, loc: ir.Location, op_name: []const u8) Allocator.Error!void {
136 const view = pickLocation(loc);
137
138 const info = LineInfo{
139 .file = if (view.file) |file| try self.strings.intern(file) else null,
140 .line = view.line,
141 .column = view.column,
142 .name = if (view.name) |name| try self.strings.intern(name) else null,
143 .op_name = try self.strings.intern(op_name),
144 };
145
146 if (self.entries.items.len > 0) {
147 const last = &self.entries.items[self.entries.items.len - 1];
148 if (last.offset == offset) {
149 last.* = .{ .offset = offset, .info = info };
150 return;
151 }
152 if (lineInfoEql(last.info, info)) return;
153 }
154
155 try self.entries.append(self.allocator, .{ .offset = offset, .info = info });
156 }
157
158 pub fn finish(self: *LineTableBuilder) Allocator.Error!LineTable {
159 var entries: []LineEntry = &.{};
160 if (self.entries.items.len != 0) {
161 entries = try self.entries.toOwnedSlice(self.allocator);
162 } else if (self.entries.capacity != 0) {
163 self.entries.deinit(self.allocator);
164 } else {
165 self.entries.clearRetainingCapacity();
166 }
167 const strings = self.strings.take();
168 self.entries = .empty;
169 return .{
170 .allocator = self.allocator,
171 .entries = entries,
172 .strings = strings,
173 };
174 }
175 };
176
177 fn lineInfoEql(a: LineInfo, b: LineInfo) bool {
178 return optionalStrEql(a.file, b.file) and
179 a.line == b.line and
180 a.column == b.column and
181 optionalStrEql(a.name, b.name) and
182 std.mem.eql(u8, a.op_name, b.op_name);
183 }
184
185 fn optionalStrEql(a: ?[]const u8, b: ?[]const u8) bool {
186 if (a == null and b == null) return true;
187 if (a == null or b == null) return false;
188 return std.mem.eql(u8, a.?, b.?);
189 }
190
191 fn pickLocation(loc: ir.Location) LocationView {
192 return switch (loc) {
193 .unknown => .{},
194 .file => |f| .{
195 .file = f.filename,
196 .line = f.line,
197 .column = f.column,
198 },
199 .file_range => |range| .{
200 .file = range.filename,
201 .line = range.start.line,
202 .column = range.start.column,
203 },
204 .name => |n| blk: {
205 var view = if (n.child) |child| pickLocation(child.*) else LocationView{};
206 view.name = n.name;
207 break :blk view;
208 },
209 .fused => |f| blk: {
210 var fallback: LocationView = .{};
211 for (f.locations) |item| {
212 const view = pickLocation(item);
213 if (view.file != null) break :blk view;
214 if (fallback.name == null and view.name != null) {
215 fallback = view;
216 }
217 }
218 break :blk fallback;
219 },
220 .call_site => |c| blk: {
221 const caller = pickLocation(c.caller.*);
222 if (caller.file != null or caller.name != null) break :blk caller;
223 break :blk pickLocation(c.callee.*);
224 },
225 };
226 }
227
228 const dwarf = struct {
229 const version: u16 = 4;
230 const addr_size: u8 = 8;
231
232 const TAG_compile_unit: u64 = 0x11;
233 const TAG_subprogram: u64 = 0x2e;
234
235 const CHILDREN_no: u8 = 0;
236 const CHILDREN_yes: u8 = 1;
237
238 const AT_name: u64 = 0x03;
239 const AT_stmt_list: u64 = 0x10;
240 const AT_low_pc: u64 = 0x11;
241 const AT_high_pc: u64 = 0x12;
242 const AT_comp_dir: u64 = 0x1b;
243 const AT_decl_file: u64 = 0x3a;
244 const AT_decl_line: u64 = 0x3b;
245
246 const FORM_addr: u64 = 0x01;
247 const FORM_data1: u64 = 0x0b;
248 const FORM_data4: u64 = 0x06;
249 const FORM_sec_offset: u64 = 0x17;
250 const FORM_string: u64 = 0x08;
251
252 const LNS_copy: u8 = 1;
253 const LNS_advance_pc: u8 = 2;
254 const LNS_advance_line: u8 = 3;
255 const LNS_set_file: u8 = 4;
256 const LNS_set_column: u8 = 5;
257 const LNS_const_add_pc: u8 = 8;
258 const LNS_fixed_advance_pc: u8 = 9;
259
260 const LNE_end_sequence: u8 = 1;
261 const LNE_set_address: u8 = 2;
262 };
263
264 const ElfStringTable = struct {
265 data: std.ArrayListUnmanaged(u8) = .empty,
266
267 fn init(allocator: Allocator) !ElfStringTable {
268 var table = ElfStringTable{};
269 try table.data.append(allocator, 0);
270 return table;
271 }
272
273 fn deinit(self: *ElfStringTable, allocator: Allocator) void {
274 self.data.deinit(allocator);
275 }
276
277 fn add(self: *ElfStringTable, allocator: Allocator, name: []const u8) !u32 {
278 const offset: u32 = @intCast(self.data.items.len);
279 try self.data.appendSlice(allocator, name);
280 try self.data.append(allocator, 0);
281 return offset;
282 }
283 };
284
285 pub const JitDebugHandle = struct {
286 entry: ?*JitCodeEntry,
287 object: []u8,
288
289 pub fn deinit(self: *JitDebugHandle, allocator: Allocator) void {
290 if (self.entry) |entry| {
291 unregisterGdbEntry(entry);
292 allocator.destroy(entry);
293 }
294 allocator.free(self.object);
295 self.* = undefined;
296 }
297 };
298
299 pub const JitDebugSupport = struct {
300 supported: bool,
301 reason: ?[]const u8,
302 note: ?[]const u8,
303 };
304
305 pub fn jitDebugSupportFor(os_tag: std.Target.Os.Tag, arch: std.Target.Cpu.Arch) JitDebugSupport {
306 const support = (sys.capabilities.Capabilities{ .os = os_tag, .arch = arch }).jitDebugSupport();
307 return .{ .supported = support.supported, .reason = support.reason, .note = support.note };
308 }
309
310 pub fn jitDebugSupport(arch: std.Target.Cpu.Arch) JitDebugSupport {
311 return jitDebugSupportFor(sys.capabilities.current.os, arch);
312 }
313
314 pub fn supportsJitDebugInfo(arch: std.Target.Cpu.Arch) bool {
315 return jitDebugSupport(arch).supported;
316 }
317
318 pub fn registerJitDebugInfo(
319 allocator: Allocator,
320 arch: std.Target.Cpu.Arch,
321 code_addr: usize,
322 code: []const u8,
323 table: *const LineTable,
324 func_name: []const u8,
325 ) Allocator.Error!?JitDebugHandle {
326 if (!supportsJitDebugInfo(arch) or table.isEmpty()) return null;
327
328 const object = try buildElfDebugObject(allocator, arch, code_addr, code, table, func_name);
329 errdefer allocator.free(object);
330
331 const entry = try allocator.create(JitCodeEntry);
332 errdefer allocator.destroy(entry);
333
334 registerGdbEntry(entry, object);
335
336 return .{
337 .entry = entry,
338 .object = object,
339 };
340 }
341
342 const JitCodeEntry = extern struct {
343 next: ?*JitCodeEntry,
344 prev: ?*JitCodeEntry,
345 symfile_addr: [*]const u8,
346 symfile_size: u64,
347 };
348
349 const JitDescriptor = extern struct {
350 version: u32,
351 action_flag: u32,
352 relevant_entry: ?*JitCodeEntry,
353 first_entry: ?*JitCodeEntry,
354 };
355
356 const JitAction = enum(u32) {
357 no_action = 0,
358 register_fn = 1,
359 unregister_fn = 2,
360 };
361
362 /// The GDB JIT interface descriptor. A debugger finds it by this exact symbol name and reads it
363 /// when it stops at `__jit_debug_register_code`. Every runtime in this process shares it, and
364 /// `registration_mutex` guards every change.
365 export var __jit_debug_descriptor: JitDescriptor = .{
366 .version = 1,
367 .action_flag = @backingInt(JitAction.no_action),
368 .relevant_entry = null,
369 .first_entry = null,
370 };
371
372 /// The GDB JIT interface breakpoint. A debugger finds it by this exact symbol name and stops here
373 /// to read `__jit_debug_descriptor`. The body keeps a call to it from being dropped, and a caller
374 /// reaches it through `@call(.never_inline, ...)`, so that an optimized build keeps the call.
375 export fn __jit_debug_register_code() callconv(.c) void {
376 std.mem.doNotOptimizeAway(&__jit_debug_descriptor);
377 }
378
379 /// Serializes every change to the entry list and the notification that follows it. Every runtime
380 /// in this process registers into that one list.
381 var registration_mutex: sys.thread.Mutex = .{};
382
383 fn registerGdbEntry(entry: *JitCodeEntry, object: []const u8) void {
384 std.debug.assert(object.len != 0);
385 registration_mutex.lock();
386 defer registration_mutex.unlock();
387 std.debug.assert(__jit_debug_descriptor.action_flag == @backingInt(JitAction.no_action));
388
389 entry.* = .{
390 .next = __jit_debug_descriptor.first_entry,
391 .prev = null,
392 .symfile_addr = object.ptr,
393 .symfile_size = object.len,
394 };
395 if (__jit_debug_descriptor.first_entry) |first| {
396 first.prev = entry;
397 }
398 __jit_debug_descriptor.first_entry = entry;
399 notifyDebugger(entry, .register_fn);
400 }
401
402 fn unregisterGdbEntry(entry: *JitCodeEntry) void {
403 registration_mutex.lock();
404 defer registration_mutex.unlock();
405 std.debug.assert(__jit_debug_descriptor.action_flag == @backingInt(JitAction.no_action));
406
407 if (entry.prev) |prev| {
408 prev.next = entry.next;
409 } else {
410 __jit_debug_descriptor.first_entry = entry.next;
411 }
412 if (entry.next) |next| {
413 next.prev = entry.prev;
414 }
415 notifyDebugger(entry, .unregister_fn);
416 }
417
418 /// Notifies a debugger of `action` on `entry` by calling `__jit_debug_register_code`, and leaves
419 /// `action_flag` at `no_action`. The caller must hold `registration_mutex`.
420 fn notifyDebugger(entry: *JitCodeEntry, action: JitAction) void {
421 std.debug.assert(action != .no_action);
422 __jit_debug_descriptor.relevant_entry = entry;
423 __jit_debug_descriptor.action_flag = @backingInt(action);
424 @call(.never_inline, __jit_debug_register_code, .{});
425 __jit_debug_descriptor.action_flag = @backingInt(JitAction.no_action);
426 }
427
428 const DebugLineSection = struct {
429 bytes: []u8,
430 primary_name: []const u8,
431 primary_dir: []const u8,
432 primary_file_index: u32,
433 primary_line: u32,
434 };
435
436 const FileEntry = struct {
437 name: []const u8,
438 dir_index: u32,
439 };
440
441 pub fn buildElfDebugObject(
442 allocator: Allocator,
443 arch: std.Target.Cpu.Arch,
444 code_addr: usize,
445 code: []const u8,
446 table: *const LineTable,
447 func_name: []const u8,
448 ) Allocator.Error![]u8 {
449 const machine = elfMachineForArch(arch) orelse return error.OutOfMemory;
450
451 var line_section = try buildDebugLineSection(allocator, code_addr, table);
452 defer allocator.free(line_section.bytes);
453
454 const abbrev = try buildDebugAbbrev(allocator);
455 defer allocator.free(abbrev);
456
457 const debug_info = try buildDebugInfo(
458 allocator,
459 code_addr,
460 code.len,
461 &line_section,
462 func_name,
463 );
464 defer allocator.free(debug_info);
465
466 var shstrtab = try ElfStringTable.init(allocator);
467 defer shstrtab.deinit(allocator);
468
469 const sh_name_text = try shstrtab.add(allocator, ".text");
470 const sh_name_debug_info = try shstrtab.add(allocator, ".debug_info");
471 const sh_name_debug_abbrev = try shstrtab.add(allocator, ".debug_abbrev");
472 const sh_name_debug_line = try shstrtab.add(allocator, ".debug_line");
473 const sh_name_symtab = try shstrtab.add(allocator, ".symtab");
474 const sh_name_strtab = try shstrtab.add(allocator, ".strtab");
475 const sh_name_shstrtab = try shstrtab.add(allocator, ".shstrtab");
476
477 var strtab = try ElfStringTable.init(allocator);
478 defer strtab.deinit(allocator);
479 const func_name_offset = try strtab.add(allocator, func_name);
480
481 var symbols = std.ArrayListUnmanaged(std.elf.Elf64_Sym).empty;
482 defer symbols.deinit(allocator);
483 try symbols.append(allocator, .{
484 .st_name = 0,
485 .st_info = 0,
486 .st_other = 0,
487 .st_shndx = 0,
488 .st_value = 0,
489 .st_size = 0,
490 });
491 try symbols.append(allocator, .{
492 .st_name = 0,
493 .st_info = (@as(u8, std.elf.STB_LOCAL) << 4) | @as(u8, std.elf.STT_SECTION),
494 .st_other = 0,
495 .st_shndx = 1,
496 .st_value = 0,
497 .st_size = 0,
498 });
499 try symbols.append(allocator, .{
500 .st_name = func_name_offset,
501 .st_info = (@as(u8, std.elf.STB_GLOBAL) << 4) | @as(u8, std.elf.STT_FUNC),
502 .st_other = 0,
503 .st_shndx = 1,
504 .st_value = @intCast(code_addr),
505 .st_size = @intCast(code.len),
506 });
507
508 const symtab_bytes = std.mem.sliceAsBytes(symbols.items);
509 const strtab_bytes = strtab.data.items;
510 const shstrtab_bytes = shstrtab.data.items;
511
512 const header_size = @sizeOf(std.elf.Elf64_Ehdr);
513 const shdr_size = @sizeOf(std.elf.Elf64_Shdr);
514
515 var offset: usize = alignForward(header_size, 16);
516 const text_offset = offset;
517 offset += code.len;
518 offset = alignForward(offset, 8);
519
520 const debug_info_offset = offset;
521 offset += debug_info.len;
522 offset = alignForward(offset, 8);
523
524 const debug_abbrev_offset = offset;
525 offset += abbrev.len;
526 offset = alignForward(offset, 8);
527
528 const debug_line_offset = offset;
529 offset += line_section.bytes.len;
530 offset = alignForward(offset, 8);
531
532 const symtab_offset = offset;
533 offset += symtab_bytes.len;
534 offset = alignForward(offset, 8);
535
536 const strtab_offset = offset;
537 offset += strtab_bytes.len;
538 offset = alignForward(offset, 8);
539
540 const shstrtab_offset = offset;
541 offset += shstrtab_bytes.len;
542 offset = alignForward(offset, 8);
543
544 const shoff = offset;
545 const shnum: u16 = 8;
546 const total_size = shoff + shdr_size * shnum;
547
548 var buffer = try allocator.alloc(u8, total_size);
549 @memset(buffer, 0);
550
551 std.mem.copyForwards(u8, buffer[text_offset .. text_offset + code.len], code);
552 std.mem.copyForwards(u8, buffer[debug_info_offset .. debug_info_offset + debug_info.len], debug_info);
553 std.mem.copyForwards(u8, buffer[debug_abbrev_offset .. debug_abbrev_offset + abbrev.len], abbrev);
554 std.mem.copyForwards(u8, buffer[debug_line_offset .. debug_line_offset + line_section.bytes.len], line_section.bytes);
555 std.mem.copyForwards(u8, buffer[symtab_offset .. symtab_offset + symtab_bytes.len], symtab_bytes);
556 std.mem.copyForwards(u8, buffer[strtab_offset .. strtab_offset + strtab_bytes.len], strtab_bytes);
557 std.mem.copyForwards(u8, buffer[shstrtab_offset .. shstrtab_offset + shstrtab_bytes.len], shstrtab_bytes);
558
559 var ident: [std.elf.EI_NIDENT]u8 = @splat(0);
560 ident[0] = 0x7f;
561 ident[1] = 'E';
562 ident[2] = 'L';
563 ident[3] = 'F';
564 ident[std.elf.EI_CLASS] = std.elf.ELFCLASS64;
565 ident[std.elf.EI_DATA] = std.elf.ELFDATA2LSB;
566 ident[std.elf.EI_VERSION] = 1;
567
568 const header = std.elf.Elf64_Ehdr{
569 .e_ident = ident,
570 .e_type = std.elf.ET.DYN,
571 .e_machine = machine,
572 .e_version = 1,
573 .e_entry = 0,
574 .e_phoff = 0,
575 .e_shoff = @intCast(shoff),
576 .e_flags = 0,
577 .e_ehsize = @intCast(header_size),
578 .e_phentsize = 0,
579 .e_phnum = 0,
580 .e_shentsize = @intCast(shdr_size),
581 .e_shnum = shnum,
582 .e_shstrndx = 7,
583 };
584
585 std.mem.copyForwards(u8, buffer[0..header_size], std.mem.asBytes(&header));
586
587 var shdrs: [8]std.elf.Elf64_Shdr = undefined;
588 shdrs[0] = .{
589 .sh_name = 0,
590 .sh_type = std.elf.SHT_NULL,
591 .sh_flags = 0,
592 .sh_addr = 0,
593 .sh_offset = 0,
594 .sh_size = 0,
595 .sh_link = 0,
596 .sh_info = 0,
597 .sh_addralign = 0,
598 .sh_entsize = 0,
599 };
600 shdrs[1] = .{
601 .sh_name = sh_name_text,
602 .sh_type = std.elf.SHT_PROGBITS,
603 .sh_flags = std.elf.SHF_ALLOC | std.elf.SHF_EXECINSTR,
604 .sh_addr = @intCast(code_addr),
605 .sh_offset = @intCast(text_offset),
606 .sh_size = @intCast(code.len),
607 .sh_link = 0,
608 .sh_info = 0,
609 .sh_addralign = 16,
610 .sh_entsize = 0,
611 };
612 shdrs[2] = .{
613 .sh_name = sh_name_debug_info,
614 .sh_type = std.elf.SHT_PROGBITS,
615 .sh_flags = 0,
616 .sh_addr = 0,
617 .sh_offset = @intCast(debug_info_offset),
618 .sh_size = @intCast(debug_info.len),
619 .sh_link = 0,
620 .sh_info = 0,
621 .sh_addralign = 1,
622 .sh_entsize = 0,
623 };
624 shdrs[3] = .{
625 .sh_name = sh_name_debug_abbrev,
626 .sh_type = std.elf.SHT_PROGBITS,
627 .sh_flags = 0,
628 .sh_addr = 0,
629 .sh_offset = @intCast(debug_abbrev_offset),
630 .sh_size = @intCast(abbrev.len),
631 .sh_link = 0,
632 .sh_info = 0,
633 .sh_addralign = 1,
634 .sh_entsize = 0,
635 };
636 shdrs[4] = .{
637 .sh_name = sh_name_debug_line,
638 .sh_type = std.elf.SHT_PROGBITS,
639 .sh_flags = 0,
640 .sh_addr = 0,
641 .sh_offset = @intCast(debug_line_offset),
642 .sh_size = @intCast(line_section.bytes.len),
643 .sh_link = 0,
644 .sh_info = 0,
645 .sh_addralign = 1,
646 .sh_entsize = 0,
647 };
648 shdrs[5] = .{
649 .sh_name = sh_name_symtab,
650 .sh_type = std.elf.SHT_SYMTAB,
651 .sh_flags = 0,
652 .sh_addr = 0,
653 .sh_offset = @intCast(symtab_offset),
654 .sh_size = @intCast(symtab_bytes.len),
655 .sh_link = 6,
656 .sh_info = 2,
657 .sh_addralign = 8,
658 .sh_entsize = @sizeOf(std.elf.Elf64_Sym),
659 };
660 shdrs[6] = .{
661 .sh_name = sh_name_strtab,
662 .sh_type = std.elf.SHT_STRTAB,
663 .sh_flags = 0,
664 .sh_addr = 0,
665 .sh_offset = @intCast(strtab_offset),
666 .sh_size = @intCast(strtab_bytes.len),
667 .sh_link = 0,
668 .sh_info = 0,
669 .sh_addralign = 1,
670 .sh_entsize = 0,
671 };
672 shdrs[7] = .{
673 .sh_name = sh_name_shstrtab,
674 .sh_type = std.elf.SHT_STRTAB,
675 .sh_flags = 0,
676 .sh_addr = 0,
677 .sh_offset = @intCast(shstrtab_offset),
678 .sh_size = @intCast(shstrtab_bytes.len),
679 .sh_link = 0,
680 .sh_info = 0,
681 .sh_addralign = 1,
682 .sh_entsize = 0,
683 };
684
685 var shdr_offset = shoff;
686 for (shdrs) |shdr| {
687 std.mem.copyForwards(u8, buffer[shdr_offset .. shdr_offset + shdr_size], std.mem.asBytes(&shdr));
688 shdr_offset += shdr_size;
689 }
690
691 return buffer;
692 }
693
694 fn buildDebugLineSection(
695 allocator: Allocator,
696 code_addr: usize,
697 table: *const LineTable,
698 ) Allocator.Error!DebugLineSection {
699 var dirs = std.ArrayListUnmanaged([]const u8).empty;
700 defer dirs.deinit(allocator);
701 var files = std.ArrayListUnmanaged(FileEntry).empty;
702 defer files.deinit(allocator);
703
704 var dir_index = std.StringHashMapUnmanaged(u32){};
705 defer dir_index.deinit(allocator);
706 var file_index = std.StringHashMapUnmanaged(u32){};
707 defer file_index.deinit(allocator);
708
709 for (table.entries) |entry| {
710 const full_path = entry.info.file orelse "<unknown>";
711 if (file_index.contains(full_path)) continue;
712
713 const dir = std.fs.path.dirname(full_path) orelse "";
714 const base = std.fs.path.basename(full_path);
715
716 var dir_idx: u32 = 0;
717 if (dir.len != 0) {
718 if (dir_index.get(dir)) |idx| {
719 dir_idx = idx;
720 } else {
721 try dirs.append(allocator, dir);
722 dir_idx = @intCast(dirs.items.len);
723 try dir_index.put(allocator, dir, dir_idx);
724 }
725 }
726
727 try files.append(allocator, .{ .name = base, .dir_index = dir_idx });
728 try file_index.put(allocator, full_path, @intCast(files.items.len));
729 }
730
731 if (files.items.len == 0) {
732 try files.append(allocator, .{ .name = "<unknown>", .dir_index = 0 });
733 try file_index.put(allocator, "<unknown>", 1);
734 }
735
736 const primary_file = files.items[0];
737 const primary_name = primary_file.name;
738 const primary_dir = if (primary_file.dir_index > 0) dirs.items[primary_file.dir_index - 1] else ".";
739
740 var out = std.ArrayListUnmanaged(u8).empty;
741 errdefer out.deinit(allocator);
742
743 const unit_length_offset = out.items.len;
744 try appendInt(&out, allocator, u32, 0);
745 try appendInt(&out, allocator, u16, dwarf.version);
746 const header_length_offset = out.items.len;
747 try appendInt(&out, allocator, u32, 0);
748
749 const header_start = out.items.len;
750 try appendInt(&out, allocator, u8, 1);
751 try appendInt(&out, allocator, u8, 1);
752 try appendInt(&out, allocator, u8, 1);
753 try appendInt(&out, allocator, u8, @bitCast(@as(i8, -5)));
754 try appendInt(&out, allocator, u8, 14);
755 try appendInt(&out, allocator, u8, 13);
756 const std_op_lengths = [_]u8{ 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1 };
757 try out.appendSlice(allocator, &std_op_lengths);
758
759 for (dirs.items) |dir| {
760 try appendString(&out, allocator, dir);
761 }
762 try out.append(allocator, 0);
763
764 for (files.items) |file| {
765 try appendString(&out, allocator, file.name);
766 try appendUleb128(&out, allocator, file.dir_index);
767 try appendUleb128(&out, allocator, 0);
768 try appendUleb128(&out, allocator, 0);
769 }
770 try out.append(allocator, 0);
771
772 const header_end = out.items.len;
773 const header_length: u32 = @intCast(header_end - header_start);
774 const header_bytes = out.items[header_length_offset .. header_length_offset + 4];
775 var header_buf: [4]u8 = undefined;
776 std.mem.writeInt(u32, &header_buf, header_length, .little);
777 @memcpy(header_bytes, &header_buf);
778
779 var current_line: i64 = 1;
780 var current_file: u32 = 1;
781 var current_column: u32 = 0;
782
783 for (table.entries) |entry| {
784 const info = entry.info;
785 const full_path = info.file orelse "<unknown>";
786 const file_idx = file_index.get(full_path) orelse 1;
787 if (file_idx != current_file) {
788 try out.append(allocator, dwarf.LNS_set_file);
789 try appendUleb128(&out, allocator, file_idx);
790 current_file = file_idx;
791 }
792
793 if (info.column != current_column) {
794 try out.append(allocator, dwarf.LNS_set_column);
795 try appendUleb128(&out, allocator, info.column);
796 current_column = info.column;
797 }
798
799 const addr = code_addr + entry.offset;
800 try appendExtendedOpcode(&out, allocator, dwarf.LNE_set_address, dwarf.addr_size, addr);
801
802 const next_line: i64 = if (info.line == 0) 1 else @intCast(info.line);
803 const line_delta = next_line - current_line;
804 if (line_delta != 0) {
805 try out.append(allocator, dwarf.LNS_advance_line);
806 try appendSleb128(&out, allocator, line_delta);
807 current_line = next_line;
808 }
809
810 try out.append(allocator, dwarf.LNS_copy);
811 }
812
813 try appendExtendedOpcode(&out, allocator, dwarf.LNE_end_sequence, dwarf.addr_size, 0);
814
815 const unit_length: u32 = @intCast(out.items.len - unit_length_offset - 4);
816 const unit_bytes = out.items[unit_length_offset .. unit_length_offset + 4];
817 var unit_buf: [4]u8 = undefined;
818 std.mem.writeInt(u32, &unit_buf, unit_length, .little);
819 @memcpy(unit_bytes, &unit_buf);
820
821 return .{
822 .bytes = try out.toOwnedSlice(allocator),
823 .primary_name = primary_name,
824 .primary_dir = primary_dir,
825 .primary_file_index = if (files.items.len > 0) 1 else 0,
826 .primary_line = if (table.entries.len > 0) @max(@as(u32, 1), table.entries[0].info.line) else 1,
827 };
828 }
829
830 fn buildDebugAbbrev(allocator: Allocator) Allocator.Error![]u8 {
831 var out = std.ArrayListUnmanaged(u8).empty;
832 errdefer out.deinit(allocator);
833
834 try appendUleb128(&out, allocator, 1);
835 try appendUleb128(&out, allocator, dwarf.TAG_compile_unit);
836 try out.append(allocator, dwarf.CHILDREN_yes);
837 try appendUleb128(&out, allocator, dwarf.AT_name);
838 try appendUleb128(&out, allocator, dwarf.FORM_string);
839 try appendUleb128(&out, allocator, dwarf.AT_comp_dir);
840 try appendUleb128(&out, allocator, dwarf.FORM_string);
841 try appendUleb128(&out, allocator, dwarf.AT_low_pc);
842 try appendUleb128(&out, allocator, dwarf.FORM_addr);
843 try appendUleb128(&out, allocator, dwarf.AT_high_pc);
844 try appendUleb128(&out, allocator, dwarf.FORM_addr);
845 try appendUleb128(&out, allocator, dwarf.AT_stmt_list);
846 try appendUleb128(&out, allocator, dwarf.FORM_sec_offset);
847 try appendUleb128(&out, allocator, 0);
848 try appendUleb128(&out, allocator, 0);
849
850 try appendUleb128(&out, allocator, 2);
851 try appendUleb128(&out, allocator, dwarf.TAG_subprogram);
852 try out.append(allocator, dwarf.CHILDREN_no);
853 try appendUleb128(&out, allocator, dwarf.AT_name);
854 try appendUleb128(&out, allocator, dwarf.FORM_string);
855 try appendUleb128(&out, allocator, dwarf.AT_low_pc);
856 try appendUleb128(&out, allocator, dwarf.FORM_addr);
857 try appendUleb128(&out, allocator, dwarf.AT_high_pc);
858 try appendUleb128(&out, allocator, dwarf.FORM_addr);
859 try appendUleb128(&out, allocator, dwarf.AT_decl_file);
860 try appendUleb128(&out, allocator, dwarf.FORM_data1);
861 try appendUleb128(&out, allocator, dwarf.AT_decl_line);
862 try appendUleb128(&out, allocator, dwarf.FORM_data4);
863 try appendUleb128(&out, allocator, 0);
864 try appendUleb128(&out, allocator, 0);
865
866 try appendUleb128(&out, allocator, 0);
867
868 return out.toOwnedSlice(allocator);
869 }
870
871 fn buildDebugInfo(
872 allocator: Allocator,
873 code_addr: usize,
874 code_size: usize,
875 line_section: *const DebugLineSection,
876 func_name: []const u8,
877 ) Allocator.Error![]u8 {
878 var out = std.ArrayListUnmanaged(u8).empty;
879 errdefer out.deinit(allocator);
880
881 const unit_length_offset = out.items.len;
882 try appendInt(&out, allocator, u32, 0);
883 try appendInt(&out, allocator, u16, dwarf.version);
884 try appendInt(&out, allocator, u32, 0);
885 try appendInt(&out, allocator, u8, dwarf.addr_size);
886
887 try appendUleb128(&out, allocator, 1);
888 const cu_name = if (line_section.primary_name.len == 0) func_name else line_section.primary_name;
889 try appendString(&out, allocator, cu_name);
890 try appendString(&out, allocator, line_section.primary_dir);
891 try appendAddress(&out, allocator, code_addr);
892 try appendAddress(&out, allocator, code_addr + code_size);
893 try appendInt(&out, allocator, u32, 0);
894
895 try appendUleb128(&out, allocator, 2);
896 try appendString(&out, allocator, func_name);
897 try appendAddress(&out, allocator, code_addr);
898 try appendAddress(&out, allocator, code_addr + code_size);
899 const decl_file: u8 = if (line_section.primary_file_index <= std.math.maxInt(u8))
900 @intCast(line_section.primary_file_index)
901 else
902 0;
903 try appendInt(&out, allocator, u8, decl_file);
904 try appendInt(&out, allocator, u32, line_section.primary_line);
905
906 try out.append(allocator, 0);
907
908 const unit_length: u32 = @intCast(out.items.len - unit_length_offset - 4);
909 const unit_bytes = out.items[unit_length_offset .. unit_length_offset + 4];
910 var unit_buf: [4]u8 = undefined;
911 std.mem.writeInt(u32, &unit_buf, unit_length, .little);
912 @memcpy(unit_bytes, &unit_buf);
913
914 return out.toOwnedSlice(allocator);
915 }
916
917 fn elfMachineForArch(arch: std.Target.Cpu.Arch) ?std.elf.EM {
918 return switch (arch) {
919 .x86_64 => std.elf.EM.X86_64,
920 .aarch64 => std.elf.EM.AARCH64,
921 else => null,
922 };
923 }
924
925 fn appendInt(list: *std.ArrayListUnmanaged(u8), allocator: Allocator, comptime T: type, value: T) Allocator.Error!void {
926 var buf: [@sizeOf(T)]u8 = undefined;
927 std.mem.writeInt(T, &buf, value, .little);
928 try list.appendSlice(allocator, &buf);
929 }
930
931 fn appendString(list: *std.ArrayListUnmanaged(u8), allocator: Allocator, value: []const u8) Allocator.Error!void {
932 try list.appendSlice(allocator, value);
933 try list.append(allocator, 0);
934 }
935
936 fn appendAddress(list: *std.ArrayListUnmanaged(u8), allocator: Allocator, value: usize) Allocator.Error!void {
937 var buf: [dwarf.addr_size]u8 = undefined;
938 std.mem.writeInt(u64, &buf, @intCast(value), .little);
939 try list.appendSlice(allocator, &buf);
940 }
941
942 fn appendUleb128(list: *std.ArrayListUnmanaged(u8), allocator: Allocator, value: u64) Allocator.Error!void {
943 var val = value;
944 while (true) {
945 var byte: u8 = @intCast(val & 0x7f);
946 val >>= 7;
947 if (val != 0) byte |= 0x80;
948 try list.append(allocator, byte);
949 if (val == 0) break;
950 }
951 }
952
953 fn appendSleb128(list: *std.ArrayListUnmanaged(u8), allocator: Allocator, value: i64) Allocator.Error!void {
954 var val = value;
955 while (true) {
956 var byte: u8 = @intCast(val & 0x7f);
957 const sign_bit = (byte & 0x40) != 0;
958 val >>= 7;
959 const done = (val == 0 and !sign_bit) or (val == -1 and sign_bit);
960 if (!done) byte |= 0x80;
961 try list.append(allocator, byte);
962 if (done) break;
963 }
964 }
965
966 fn appendExtendedOpcode(
967 list: *std.ArrayListUnmanaged(u8),
968 allocator: Allocator,
969 opcode: u8,
970 addr_size: u8,
971 addr: usize,
972 ) Allocator.Error!void {
973 try list.append(allocator, 0);
974 const payload_len: u64 = if (opcode == dwarf.LNE_set_address) 1 + addr_size else 1;
975 try appendUleb128(list, allocator, payload_len);
976 try list.append(allocator, opcode);
977 if (opcode == dwarf.LNE_set_address) {
978 var buf: [dwarf.addr_size]u8 = undefined;
979 std.mem.writeInt(u64, &buf, @intCast(addr), .little);
980 try list.appendSlice(allocator, &buf);
981 }
982 }
983
984 fn alignForward(value: usize, alignment: usize) usize {
985 const mask = alignment - 1;
986 return (value + mask) & ~mask;
987 }
988
989 fn sliceSectionByName(data: []const u8, name: []const u8) ?[]const u8 {
990 if (data.len < @sizeOf(std.elf.Elf64_Ehdr)) return null;
991 const header = std.mem.bytesToValue(std.elf.Elf64_Ehdr, data[0..@sizeOf(std.elf.Elf64_Ehdr)]);
992 const shoff: usize = @intCast(header.e_shoff);
993 const shnum = header.e_shnum;
994 const shstrndx = header.e_shstrndx;
995 if (shnum == 0 or shstrndx >= shnum) return null;
996
997 const shdr_size = @sizeOf(std.elf.Elf64_Shdr);
998 const shstr_off = shoff + shdr_size * shstrndx;
999 if (shstr_off + shdr_size > data.len) return null;
1000 const shstr_hdr = std.mem.bytesToValue(std.elf.Elf64_Shdr, data[shstr_off .. shstr_off + shdr_size]);
1001 const shstr_start: usize = @intCast(shstr_hdr.sh_offset);
1002 const shstr_size: usize = @intCast(shstr_hdr.sh_size);
1003 const shstr_end: usize = shstr_start + shstr_size;
1004 if (shstr_end > data.len) return null;
1005 const shstrtab = data[shstr_start..shstr_end];
1006
1007 var idx: usize = 0;
1008 while (idx < shnum) : (idx += 1) {
1009 const shdr_off = shoff + shdr_size * idx;
1010 if (shdr_off + shdr_size > data.len) return null;
1011 const shdr = std.mem.bytesToValue(std.elf.Elf64_Shdr, data[shdr_off .. shdr_off + shdr_size]);
1012 const name_off: usize = @intCast(shdr.sh_name);
1013 if (name_off >= shstrtab.len) continue;
1014 const section_name = std.mem.sliceTo(shstrtab[name_off..], 0);
1015 if (!std.mem.eql(u8, section_name, name)) continue;
1016 const start: usize = @intCast(shdr.sh_offset);
1017 const size: usize = @intCast(shdr.sh_size);
1018 const end = start + size;
1019 if (end > data.len) return null;
1020 return data[start..end];
1021 }
1022 return null;
1023 }
1024
1025 const ValidationError = error{
1026 OutOfMemory,
1027 InvalidElfHeader,
1028 InvalidElfSection,
1029 OverlappingSections,
1030 MissingSection,
1031 InvalidSymtab,
1032 InvalidDebugLine,
1033 InvalidDebugInfo,
1034 };
1035
1036 const ElfSection = struct {
1037 index: usize,
1038 header: std.elf.Elf64_Shdr,
1039 name: []const u8,
1040 };
1041
1042 const ElfView = struct {
1043 allocator: Allocator,
1044 header: std.elf.Elf64_Ehdr,
1045 sections: []ElfSection,
1046 shstrtab: []const u8,
1047
1048 fn deinit(self: *ElfView) void {
1049 self.allocator.free(self.sections);
1050 self.* = undefined;
1051 }
1052
1053 fn findSection(self: *const ElfView, name: []const u8) ?ElfSection {
1054 for (self.sections) |section| {
1055 if (std.mem.eql(u8, section.name, name)) return section;
1056 }
1057 return null;
1058 }
1059 };
1060
1061 const ByteReader = struct {
1062 data: []const u8,
1063 offset: usize = 0,
1064
1065 const ReadError = error{Truncated};
1066
1067 fn init(data: []const u8) ByteReader {
1068 return .{ .data = data, .offset = 0 };
1069 }
1070
1071 fn readBytes(self: *ByteReader, len: usize) ReadError![]const u8 {
1072 if (self.offset + len > self.data.len) return error.Truncated;
1073 const out = self.data[self.offset .. self.offset + len];
1074 self.offset += len;
1075 return out;
1076 }
1077
1078 fn readInt(self: *ByteReader, comptime T: type) ReadError!T {
1079 const bytes = try self.readBytes(@sizeOf(T));
1080 return std.mem.readInt(T, bytes[0..@sizeOf(T)], .little);
1081 }
1082
1083 fn readByte(self: *ByteReader) ReadError!u8 {
1084 return self.readInt(u8);
1085 }
1086
1087 fn readCString(self: *ByteReader) ReadError![]const u8 {
1088 if (self.offset >= self.data.len) return error.Truncated;
1089 const start = self.offset;
1090 const rel = std.mem.indexOfScalar(u8, self.data[start..], 0) orelse return error.Truncated;
1091 self.offset = start + rel + 1;
1092 return self.data[start .. start + rel];
1093 }
1094
1095 fn readUleb(self: *ByteReader) ReadError!u64 {
1096 var result: u64 = 0;
1097 var shift: u6 = 0;
1098 while (true) {
1099 const byte = try self.readByte();
1100 result |= (@as(u64, byte & 0x7f) << shift);
1101 if ((byte & 0x80) == 0) break;
1102 shift += 7;
1103 if (shift >= 63) return error.Truncated;
1104 }
1105 return result;
1106 }
1107
1108 fn readSleb(self: *ByteReader) ReadError!i64 {
1109 var result: i64 = 0;
1110 var shift: u6 = 0;
1111 var byte: u8 = 0;
1112 while (true) {
1113 byte = try self.readByte();
1114 result |= (@as(i64, byte & 0x7f) << shift);
1115 shift += 7;
1116 if ((byte & 0x80) == 0) break;
1117 if (shift >= 63) return error.Truncated;
1118 }
1119 if ((shift < 64) and ((byte & 0x40) != 0)) {
1120 result |= -(@as(i64, 1) << shift);
1121 }
1122 return result;
1123 }
1124 };
1125
1126 fn parseElfView(allocator: Allocator, data: []const u8) ValidationError!ElfView {
1127 if (data.len < @sizeOf(std.elf.Elf64_Ehdr)) return ValidationError.InvalidElfHeader;
1128 const header = std.mem.bytesToValue(std.elf.Elf64_Ehdr, data[0..@sizeOf(std.elf.Elf64_Ehdr)]);
1129
1130 const shoff: usize = @intCast(header.e_shoff);
1131 const shnum: usize = header.e_shnum;
1132 const shentsize: usize = header.e_shentsize;
1133 if (shnum == 0 or shentsize != @sizeOf(std.elf.Elf64_Shdr)) return ValidationError.InvalidElfHeader;
1134 if (header.e_shstrndx >= header.e_shnum) return ValidationError.InvalidElfHeader;
1135 if (shoff + shentsize * shnum > data.len) return ValidationError.InvalidElfSection;
1136
1137 const shstr_off = shoff + shentsize * header.e_shstrndx;
1138 if (shstr_off + shentsize > data.len) return ValidationError.InvalidElfSection;
1139 const shstr_hdr = std.mem.bytesToValue(std.elf.Elf64_Shdr, data[shstr_off .. shstr_off + shentsize]);
1140 const shstr_start: usize = @intCast(shstr_hdr.sh_offset);
1141 const shstr_size: usize = @intCast(shstr_hdr.sh_size);
1142 const shstr_end = shstr_start + shstr_size;
1143 if (shstr_end > data.len) return ValidationError.InvalidElfSection;
1144 const shstrtab = data[shstr_start..shstr_end];
1145
1146 var sections = try allocator.alloc(ElfSection, shnum);
1147 errdefer allocator.free(sections);
1148
1149 var idx: usize = 0;
1150 while (idx < shnum) : (idx += 1) {
1151 const shdr_off = shoff + shentsize * idx;
1152 if (shdr_off + shentsize > data.len) return ValidationError.InvalidElfSection;
1153 const shdr = std.mem.bytesToValue(std.elf.Elf64_Shdr, data[shdr_off .. shdr_off + shentsize]);
1154 const name_off: usize = @intCast(shdr.sh_name);
1155 if (name_off >= shstrtab.len) return ValidationError.InvalidElfSection;
1156 const name = std.mem.sliceTo(shstrtab[name_off..], 0);
1157 sections[idx] = .{ .index = idx, .header = shdr, .name = name };
1158 }
1159
1160 return .{
1161 .allocator = allocator,
1162 .header = header,
1163 .sections = sections,
1164 .shstrtab = shstrtab,
1165 };
1166 }
1167
1168 fn validateElfHeader(header: std.elf.Elf64_Ehdr, arch: std.Target.Cpu.Arch) ValidationError!void {
1169 if (header.e_ident[0] != 0x7f or header.e_ident[1] != 'E' or header.e_ident[2] != 'L' or header.e_ident[3] != 'F') {
1170 return ValidationError.InvalidElfHeader;
1171 }
1172 if (header.e_ident[std.elf.EI_CLASS] != std.elf.ELFCLASS64) return ValidationError.InvalidElfHeader;
1173 if (header.e_ident[std.elf.EI_DATA] != std.elf.ELFDATA2LSB) return ValidationError.InvalidElfHeader;
1174 if (header.e_ident[std.elf.EI_VERSION] != 1) return ValidationError.InvalidElfHeader;
1175 if (header.e_ehsize != @sizeOf(std.elf.Elf64_Ehdr)) return ValidationError.InvalidElfHeader;
1176 if (header.e_shentsize != @sizeOf(std.elf.Elf64_Shdr)) return ValidationError.InvalidElfHeader;
1177 const machine = elfMachineForArch(arch) orelse return ValidationError.InvalidElfHeader;
1178 if (header.e_machine != machine) return ValidationError.InvalidElfHeader;
1179 if (header.e_shstrndx >= header.e_shnum) return ValidationError.InvalidElfHeader;
1180 }
1181
1182 fn validateSectionRanges(view: *const ElfView, data_len: usize) ValidationError!void {
1183 for (view.sections) |section| {
1184 const size: usize = @intCast(section.header.sh_size);
1185 const start: usize = @intCast(section.header.sh_offset);
1186 if (size == 0) continue;
1187 const end = start + size;
1188 if (end > data_len) return ValidationError.InvalidElfSection;
1189 }
1190
1191 var i: usize = 0;
1192 while (i < view.sections.len) : (i += 1) {
1193 const a = view.sections[i];
1194 const a_size: usize = @intCast(a.header.sh_size);
1195 if (a_size == 0) continue;
1196 const a_start: usize = @intCast(a.header.sh_offset);
1197 const a_end = a_start + a_size;
1198
1199 var j: usize = i + 1;
1200 while (j < view.sections.len) : (j += 1) {
1201 const b = view.sections[j];
1202 const b_size: usize = @intCast(b.header.sh_size);
1203 if (b_size == 0) continue;
1204 const b_start: usize = @intCast(b.header.sh_offset);
1205 const b_end = b_start + b_size;
1206 if (a_start < b_end and b_start < a_end) {
1207 return ValidationError.OverlappingSections;
1208 }
1209 }
1210 }
1211 }
1212
1213 fn validateSymtab(
1214 view: *const ElfView,
1215 data: []const u8,
1216 func_name: []const u8,
1217 code_addr: usize,
1218 code_len: usize,
1219 ) ValidationError!void {
1220 const symtab = view.findSection(".symtab") orelse return ValidationError.MissingSection;
1221 const strtab = view.findSection(".strtab") orelse return ValidationError.MissingSection;
1222
1223 if (symtab.header.sh_link != strtab.index) return ValidationError.InvalidSymtab;
1224 if (symtab.header.sh_entsize != @sizeOf(std.elf.Elf64_Sym)) return ValidationError.InvalidSymtab;
1225 if ((symtab.header.sh_size % @sizeOf(std.elf.Elf64_Sym)) != 0) return ValidationError.InvalidSymtab;
1226
1227 const sym_offset: usize = @intCast(symtab.header.sh_offset);
1228 const sym_size: usize = @intCast(symtab.header.sh_size);
1229 if (sym_offset + sym_size > data.len) return ValidationError.InvalidSymtab;
1230 const sym_count = sym_size / @sizeOf(std.elf.Elf64_Sym);
1231 if (sym_count == 0) return ValidationError.InvalidSymtab;
1232
1233 const str_offset: usize = @intCast(strtab.header.sh_offset);
1234 const str_size: usize = @intCast(strtab.header.sh_size);
1235 if (str_offset + str_size > data.len) return ValidationError.InvalidSymtab;
1236 const strtab_bytes = data[str_offset .. str_offset + str_size];
1237
1238 const first_global = symtab.header.sh_info;
1239 if (first_global == 0 or first_global > sym_count) return ValidationError.InvalidSymtab;
1240
1241 var found_func = false;
1242 var idx: usize = 0;
1243 while (idx < sym_count) : (idx += 1) {
1244 const start = sym_offset + idx * @sizeOf(std.elf.Elf64_Sym);
1245 const sym = std.mem.bytesToValue(std.elf.Elf64_Sym, data[start .. start + @sizeOf(std.elf.Elf64_Sym)]);
1246 const bind = sym.st_info >> 4;
1247 if (idx < first_global and bind != std.elf.STB_LOCAL) return ValidationError.InvalidSymtab;
1248 if (idx >= first_global and bind == std.elf.STB_LOCAL) return ValidationError.InvalidSymtab;
1249
1250 if (sym.st_name != 0) {
1251 const name_off: usize = @intCast(sym.st_name);
1252 if (name_off >= strtab_bytes.len) return ValidationError.InvalidSymtab;
1253 const name = std.mem.sliceTo(strtab_bytes[name_off..], 0);
1254 if (std.mem.eql(u8, name, func_name)) {
1255 const typ = sym.st_info & 0x0f;
1256 if (typ != std.elf.STT_FUNC) return ValidationError.InvalidSymtab;
1257 if (sym.st_value != code_addr) return ValidationError.InvalidSymtab;
1258 if (sym.st_size != code_len) return ValidationError.InvalidSymtab;
1259 found_func = true;
1260 }
1261 }
1262 }
1263
1264 if (!found_func) return ValidationError.InvalidSymtab;
1265 }
1266
1267 fn validateDebugLine(section: []const u8) ValidationError!void {
1268 var reader = ByteReader.init(section);
1269 const unit_length = reader.readInt(u32) catch return ValidationError.InvalidDebugLine;
1270 if (unit_length + 4 != section.len) return ValidationError.InvalidDebugLine;
1271 const version = reader.readInt(u16) catch return ValidationError.InvalidDebugLine;
1272 if (version != dwarf.version) return ValidationError.InvalidDebugLine;
1273 const header_length = reader.readInt(u32) catch return ValidationError.InvalidDebugLine;
1274 const header_end = reader.offset + header_length;
1275 if (header_end > section.len) return ValidationError.InvalidDebugLine;
1276
1277 _ = reader.readByte() catch return ValidationError.InvalidDebugLine;
1278 _ = reader.readByte() catch return ValidationError.InvalidDebugLine;
1279 _ = reader.readByte() catch return ValidationError.InvalidDebugLine;
1280 _ = reader.readByte() catch return ValidationError.InvalidDebugLine;
1281 _ = reader.readByte() catch return ValidationError.InvalidDebugLine;
1282 const opcode_base = reader.readByte() catch return ValidationError.InvalidDebugLine;
1283 if (opcode_base == 0) return ValidationError.InvalidDebugLine;
1284 const std_op_lengths = reader.readBytes(opcode_base - 1) catch return ValidationError.InvalidDebugLine;
1285
1286 if (header_end < reader.offset) return ValidationError.InvalidDebugLine;
1287 reader.offset = header_end;
1288
1289 var last_was_end_sequence = false;
1290 while (reader.offset < section.len) {
1291 const opcode = reader.readByte() catch return ValidationError.InvalidDebugLine;
1292 if (opcode == 0) {
1293 const payload_len = reader.readUleb() catch return ValidationError.InvalidDebugLine;
1294 if (payload_len == 0) return ValidationError.InvalidDebugLine;
1295 if (reader.offset + payload_len > section.len) return ValidationError.InvalidDebugLine;
1296 const subopcode = reader.readByte() catch return ValidationError.InvalidDebugLine;
1297 const remaining = payload_len - 1;
1298 if (subopcode == dwarf.LNE_end_sequence) {
1299 if (remaining != 0) return ValidationError.InvalidDebugLine;
1300 last_was_end_sequence = true;
1301 } else {
1302 last_was_end_sequence = false;
1303 }
1304 reader.offset += remaining;
1305 continue;
1306 }
1307
1308 last_was_end_sequence = false;
1309 if (opcode < opcode_base) {
1310 switch (opcode) {
1311 dwarf.LNS_copy => {},
1312 dwarf.LNS_advance_pc => {
1313 _ = reader.readUleb() catch return ValidationError.InvalidDebugLine;
1314 },
1315 dwarf.LNS_advance_line => {
1316 _ = reader.readSleb() catch return ValidationError.InvalidDebugLine;
1317 },
1318 dwarf.LNS_set_file => {
1319 _ = reader.readUleb() catch return ValidationError.InvalidDebugLine;
1320 },
1321 dwarf.LNS_set_column => {
1322 _ = reader.readUleb() catch return ValidationError.InvalidDebugLine;
1323 },
1324 dwarf.LNS_const_add_pc => {},
1325 dwarf.LNS_fixed_advance_pc => {
1326 _ = reader.readInt(u16) catch return ValidationError.InvalidDebugLine;
1327 },
1328 else => {
1329 const op_index: usize = opcode - 1;
1330 if (op_index >= std_op_lengths.len) return ValidationError.InvalidDebugLine;
1331 var count = std_op_lengths[op_index];
1332 while (count > 0) : (count -= 1) {
1333 _ = reader.readUleb() catch return ValidationError.InvalidDebugLine;
1334 }
1335 },
1336 }
1337 continue;
1338 }
1339 }
1340
1341 if (!last_was_end_sequence) return ValidationError.InvalidDebugLine;
1342 if (reader.offset != section.len) return ValidationError.InvalidDebugLine;
1343 }
1344
1345 fn validateDebugInfo(
1346 section: []const u8,
1347 code_addr: usize,
1348 code_len: usize,
1349 func_name: []const u8,
1350 expected_file: []const u8,
1351 expected_dir: []const u8,
1352 expected_line: u32,
1353 ) ValidationError!void {
1354 var reader = ByteReader.init(section);
1355 const unit_length = reader.readInt(u32) catch return ValidationError.InvalidDebugInfo;
1356 if (unit_length + 4 != section.len) return ValidationError.InvalidDebugInfo;
1357 const version = reader.readInt(u16) catch return ValidationError.InvalidDebugInfo;
1358 if (version != dwarf.version) return ValidationError.InvalidDebugInfo;
1359 const abbrev_offset = reader.readInt(u32) catch return ValidationError.InvalidDebugInfo;
1360 if (abbrev_offset != 0) return ValidationError.InvalidDebugInfo;
1361 const addr_size = reader.readByte() catch return ValidationError.InvalidDebugInfo;
1362 if (addr_size != dwarf.addr_size) return ValidationError.InvalidDebugInfo;
1363
1364 const cu_abbrev = reader.readUleb() catch return ValidationError.InvalidDebugInfo;
1365 if (cu_abbrev != 1) return ValidationError.InvalidDebugInfo;
1366
1367 const cu_name = reader.readCString() catch return ValidationError.InvalidDebugInfo;
1368 const comp_dir = reader.readCString() catch return ValidationError.InvalidDebugInfo;
1369 const low_pc = reader.readInt(u64) catch return ValidationError.InvalidDebugInfo;
1370 const high_pc = reader.readInt(u64) catch return ValidationError.InvalidDebugInfo;
1371 const stmt_list = reader.readInt(u32) catch return ValidationError.InvalidDebugInfo;
1372 if (!std.mem.eql(u8, cu_name, expected_file)) return ValidationError.InvalidDebugInfo;
1373 if (!std.mem.eql(u8, comp_dir, expected_dir)) return ValidationError.InvalidDebugInfo;
1374 if (low_pc != code_addr or high_pc != code_addr + code_len) return ValidationError.InvalidDebugInfo;
1375 if (stmt_list != 0) return ValidationError.InvalidDebugInfo;
1376
1377 const sub_abbrev = reader.readUleb() catch return ValidationError.InvalidDebugInfo;
1378 if (sub_abbrev != 2) return ValidationError.InvalidDebugInfo;
1379 const sub_name = reader.readCString() catch return ValidationError.InvalidDebugInfo;
1380 if (!std.mem.eql(u8, sub_name, func_name)) return ValidationError.InvalidDebugInfo;
1381 const sub_low = reader.readInt(u64) catch return ValidationError.InvalidDebugInfo;
1382 const sub_high = reader.readInt(u64) catch return ValidationError.InvalidDebugInfo;
1383 const decl_file = reader.readInt(u8) catch return ValidationError.InvalidDebugInfo;
1384 const decl_line = reader.readInt(u32) catch return ValidationError.InvalidDebugInfo;
1385 if (sub_low != code_addr or sub_high != code_addr + code_len) return ValidationError.InvalidDebugInfo;
1386 if (decl_file == 0) return ValidationError.InvalidDebugInfo;
1387 if (decl_line != expected_line) return ValidationError.InvalidDebugInfo;
1388
1389 const terminator = reader.readUleb() catch return ValidationError.InvalidDebugInfo;
1390 if (terminator != 0) return ValidationError.InvalidDebugInfo;
1391 if (reader.offset != section.len) return ValidationError.InvalidDebugInfo;
1392 }
1393
1394 fn validateElfDebugObject(
1395 allocator: Allocator,
1396 data: []const u8,
1397 arch: std.Target.Cpu.Arch,
1398 code_addr: usize,
1399 code_len: usize,
1400 func_name: []const u8,
1401 expected_file: []const u8,
1402 expected_dir: []const u8,
1403 expected_line: u32,
1404 ) ValidationError!void {
1405 var view = try parseElfView(allocator, data);
1406 defer view.deinit();
1407
1408 try validateElfHeader(view.header, arch);
1409 try validateSectionRanges(&view, data.len);
1410 try validateSymtab(&view, data, func_name, code_addr, code_len);
1411
1412 const debug_line = view.findSection(".debug_line") orelse return ValidationError.MissingSection;
1413 const debug_info = view.findSection(".debug_info") orelse return ValidationError.MissingSection;
1414 const debug_abbrev = view.findSection(".debug_abbrev") orelse return ValidationError.MissingSection;
1415
1416 if (debug_abbrev.header.sh_size == 0) return ValidationError.InvalidDebugInfo;
1417
1418 const line_offset: usize = @intCast(debug_line.header.sh_offset);
1419 const line_size: usize = @intCast(debug_line.header.sh_size);
1420 const info_offset: usize = @intCast(debug_info.header.sh_offset);
1421 const info_size: usize = @intCast(debug_info.header.sh_size);
1422 if (line_offset + line_size > data.len) return ValidationError.InvalidDebugLine;
1423 if (info_offset + info_size > data.len) return ValidationError.InvalidDebugInfo;
1424
1425 try validateDebugLine(data[line_offset .. line_offset + line_size]);
1426 try validateDebugInfo(
1427 data[info_offset .. info_offset + info_size],
1428 code_addr,
1429 code_len,
1430 func_name,
1431 expected_file,
1432 expected_dir,
1433 expected_line,
1434 );
1435 }
1436
1437 test "LineTableBuilder lookup returns nearest entry" {
1438 var builder = LineTableBuilder.init(std.testing.allocator);
1439 defer builder.deinit();
1440
1441 try builder.record(0, ir.Location.getFile("test.choir", 1, 1), "arith.add");
1442 try builder.record(8, ir.Location.getFile("test.choir", 2, 5), "arith.mul");
1443
1444 var table = try builder.finish();
1445 defer table.deinit();
1446
1447 const at0_opt = table.lookup(0);
1448 try std.testing.expect(at0_opt != null);
1449 const at0 = at0_opt.?;
1450 try std.testing.expectEqualStrings("test.choir", at0.file.?);
1451 try std.testing.expectEqual(@as(u32, 1), at0.line);
1452
1453 const at4_opt = table.lookup(4);
1454 try std.testing.expect(at4_opt != null);
1455 const at4 = at4_opt.?;
1456 try std.testing.expectEqual(@as(u32, 1), at4.line);
1457
1458 const at8_opt = table.lookup(8);
1459 try std.testing.expect(at8_opt != null);
1460 const at8 = at8_opt.?;
1461 try std.testing.expectEqual(@as(u32, 2), at8.line);
1462 }
1463
1464 test "LineTableBuilder preserves name location" {
1465 var builder = LineTableBuilder.init(std.testing.allocator);
1466 defer builder.deinit();
1467
1468 const loc = ir.Location.getName("generated", null);
1469 try builder.record(0, loc, "arith.add");
1470
1471 var table = try builder.finish();
1472 defer table.deinit();
1473
1474 const info_opt = table.lookup(0);
1475 try std.testing.expect(info_opt != null);
1476 const info = info_opt.?;
1477 try std.testing.expectEqualStrings("generated", info.name.?);
1478 try std.testing.expectEqualStrings("arith.add", info.op_name);
1479 }
1480
1481 test "LineTableBuilder finish handles empty table" {
1482 var builder = LineTableBuilder.init(std.testing.allocator);
1483 defer builder.deinit();
1484
1485 var table = try builder.finish();
1486 defer table.deinit();
1487
1488 try std.testing.expect(table.isEmpty());
1489 }
1490
1491 test "buildElfDebugObject emits debug sections" {
1492 var builder = LineTableBuilder.init(std.testing.allocator);
1493 defer builder.deinit();
1494
1495 try builder.record(0, ir.Location.getFile("test.choir", 3, 1), "arith.add");
1496
1497 var table = try builder.finish();
1498 defer table.deinit();
1499
1500 const obj = try buildElfDebugObject(
1501 std.testing.allocator,
1502 .x86_64,
1503 0x1000,
1504 &[_]u8{ 0x90, 0x90 },
1505 &table,
1506 "jit_fn",
1507 );
1508 defer std.testing.allocator.free(obj);
1509
1510 const debug_line = sliceSectionByName(obj, ".debug_line");
1511 try std.testing.expect(debug_line != null);
1512 try std.testing.expect(std.mem.containsAtLeast(u8, debug_line.?, 1, "test.choir"));
1513
1514 const debug_info = sliceSectionByName(obj, ".debug_info");
1515 try std.testing.expect(debug_info != null);
1516 try std.testing.expect(debug_info.?.len > 0);
1517
1518 const symtab = sliceSectionByName(obj, ".symtab");
1519 try std.testing.expect(symtab != null);
1520 try std.testing.expect(symtab.?.len > 0);
1521 }
1522
1523 test "buildElfDebugObject emits valid ELF + DWARF structure" {
1524 var builder = LineTableBuilder.init(std.testing.allocator);
1525 defer builder.deinit();
1526
1527 try builder.record(0, ir.Location.getFile("test.choir", 3, 1), "arith.add");
1528
1529 var table = try builder.finish();
1530 defer table.deinit();
1531
1532 const code_addr = 0x1000;
1533 const code = [_]u8{ 0x90, 0x90 };
1534
1535 const obj = try buildElfDebugObject(
1536 std.testing.allocator,
1537 .x86_64,
1538 code_addr,
1539 &code,
1540 &table,
1541 "jit_fn",
1542 );
1543 defer std.testing.allocator.free(obj);
1544
1545 try validateElfDebugObject(
1546 std.testing.allocator,
1547 obj,
1548 .x86_64,
1549 code_addr,
1550 code.len,
1551 "jit_fn",
1552 "test.choir",
1553 ".",
1554 3,
1555 );
1556 }
1557
1558 test "validateElfDebugObject rejects malformed debug_line" {
1559 var builder = LineTableBuilder.init(std.testing.allocator);
1560 defer builder.deinit();
1561
1562 try builder.record(0, ir.Location.getFile("test.choir", 3, 1), "arith.add");
1563
1564 var table = try builder.finish();
1565 defer table.deinit();
1566
1567 const code_addr = 0x1000;
1568 const code = [_]u8{ 0x90, 0x90 };
1569
1570 const obj = try buildElfDebugObject(
1571 std.testing.allocator,
1572 .x86_64,
1573 code_addr,
1574 &code,
1575 &table,
1576 "jit_fn",
1577 );
1578 defer std.testing.allocator.free(obj);
1579
1580 var corrupted = try std.testing.allocator.dupe(u8, obj);
1581 defer std.testing.allocator.free(corrupted);
1582
1583 var view = try parseElfView(std.testing.allocator, corrupted);
1584 const debug_line = view.findSection(".debug_line") orelse {
1585 view.deinit();
1586 return error.TestFailure;
1587 };
1588 const line_offset: usize = @intCast(debug_line.header.sh_offset);
1589 const line_size: usize = @intCast(debug_line.header.sh_size);
1590 view.deinit();
1591
1592 if (line_size == 0 or line_offset + line_size > corrupted.len) {
1593 return error.TestFailure;
1594 }
1595
1596 corrupted[line_offset + line_size - 1] = 0;
1597
1598 try std.testing.expectError(
1599 ValidationError.InvalidDebugLine,
1600 validateElfDebugObject(
1601 std.testing.allocator,
1602 corrupted,
1603 .x86_64,
1604 code_addr,
1605 code.len,
1606 "jit_fn",
1607 "test.choir",
1608 ".",
1609 3,
1610 ),
1611 );
1612 }
1613
1614 test "validateElfDebugObject rejects malformed debug_info" {
1615 var builder = LineTableBuilder.init(std.testing.allocator);
1616 defer builder.deinit();
1617
1618 try builder.record(0, ir.Location.getFile("test.choir", 3, 1), "arith.add");
1619
1620 var table = try builder.finish();
1621 defer table.deinit();
1622
1623 const code_addr = 0x1000;
1624 const code = [_]u8{ 0x90, 0x90 };
1625
1626 const obj = try buildElfDebugObject(
1627 std.testing.allocator,
1628 .x86_64,
1629 code_addr,
1630 &code,
1631 &table,
1632 "jit_fn",
1633 );
1634 defer std.testing.allocator.free(obj);
1635
1636 var corrupted = try std.testing.allocator.dupe(u8, obj);
1637 defer std.testing.allocator.free(corrupted);
1638
1639 var view = try parseElfView(std.testing.allocator, corrupted);
1640 const debug_info = view.findSection(".debug_info") orelse {
1641 view.deinit();
1642 return error.TestFailure;
1643 };
1644 const info_offset: usize = @intCast(debug_info.header.sh_offset);
1645 const info_size: usize = @intCast(debug_info.header.sh_size);
1646 view.deinit();
1647
1648 if (info_size <= 4 or info_offset + info_size > corrupted.len) {
1649 return error.TestFailure;
1650 }
1651
1652 std.mem.writeInt(u32, corrupted[info_offset..][0..4], 0, .little);
1653
1654 try std.testing.expectError(
1655 ValidationError.InvalidDebugInfo,
1656 validateElfDebugObject(
1657 std.testing.allocator,
1658 corrupted,
1659 .x86_64,
1660 code_addr,
1661 code.len,
1662 "jit_fn",
1663 "test.choir",
1664 ".",
1665 3,
1666 ),
1667 );
1668 }
1669
1670 test "jitDebugSupportFor reports macOS note" {
1671 const linux_support = jitDebugSupportFor(.linux, .x86_64);
1672 try std.testing.expect(linux_support.supported);
1673 try std.testing.expect(linux_support.note == null);
1674
1675 const mac_support = jitDebugSupportFor(.macos, .aarch64);
1676 try std.testing.expect(mac_support.supported);
1677 try std.testing.expect(mac_support.note != null);
1678 try std.testing.expect(std.mem.containsAtLeast(u8, mac_support.note.?, 1, "lldb"));
1679
1680 const windows_support = jitDebugSupportFor(.windows, .x86_64);
1681 try std.testing.expect(!windows_support.supported);
1682 try std.testing.expect(windows_support.reason != null);
1683 }
1684
1685 test "registerJitDebugInfo registers and unregisters entries when supported" {
1686 if (!supportsJitDebugInfo(sys.capabilities.current.arch)) return;
1687
1688 var builder = LineTableBuilder.init(std.testing.allocator);
1689 defer builder.deinit();
1690
1691 try builder.record(0, ir.Location.getFile("jit_test.choir", 1, 1), "arith.add");
1692
1693 var table = try builder.finish();
1694 defer table.deinit();
1695
1696 const prev_first = __jit_debug_descriptor.first_entry;
1697
1698 const code_addr = 0x1000;
1699 const code = [_]u8{ 0x90, 0x90 };
1700 const handle_opt = try registerJitDebugInfo(
1701 std.testing.allocator,
1702 sys.capabilities.current.arch,
1703 code_addr,
1704 &code,
1705 &table,
1706 "jit_fn",
1707 );
1708 try std.testing.expect(handle_opt != null);
1709 var handle = handle_opt.?;
1710 try std.testing.expect(handle.entry != null);
1711 try std.testing.expect(__jit_debug_descriptor.first_entry == handle.entry);
1712
1713 handle.deinit(std.testing.allocator);
1714
1715 try std.testing.expect(__jit_debug_descriptor.first_entry == prev_first);
1716 try std.testing.expectEqual(@backingInt(JitAction.no_action), __jit_debug_descriptor.action_flag);
1717 }
1718
1719 test "GDB JIT registration exports its descriptor and its breakpoint by name" {
1720 const descriptor = @extern(*JitDescriptor, .{ .name = "__jit_debug_descriptor" });
1721 try std.testing.expectEqual(&__jit_debug_descriptor, descriptor);
1722 try std.testing.expectEqual(@as(u32, 1), descriptor.version);
1723
1724 const register = @extern(
1725 *const fn () callconv(.c) void,
1726 .{ .name = "__jit_debug_register_code" },
1727 );
1728 try std.testing.expectEqual(&__jit_debug_register_code, register);
1729 }
1730
1731 const registration_worker_entries = 8;
1732
1733 /// One thread of the registration race test. It registers or unregisters its own entries once
1734 /// every worker of its party has arrived.
1735 const RegistrationWorker = struct {
1736 object: []const u8,
1737 started: *std.atomic.Value(usize),
1738 party: usize,
1739 unregistering: bool = false,
1740 entries: [registration_worker_entries]JitCodeEntry = undefined,
1741
1742 fn run(self: *RegistrationWorker) void {
1743 _ = self.started.fetchAdd(1, .acq_rel);
1744 while (self.started.load(.acquire) < self.party) std.atomic.spinLoopHint();
1745 if (!self.unregistering) {
1746 for (&self.entries) |*entry| registerGdbEntry(entry, self.object);
1747 return;
1748 }
1749 var remaining = self.entries.len;
1750 while (remaining != 0) : (remaining -= 1) {
1751 unregisterGdbEntry(&self.entries[remaining - 1]);
1752 }
1753 }
1754 };
1755
1756 /// Runs one register or unregister phase across `workers`, and joins every thread it spawns.
1757 fn runRegistrationPhase(workers: []RegistrationWorker, unregistering: bool) !void {
1758 var started = std.atomic.Value(usize).init(0);
1759 var threads: [8]sys.thread.JoinHandle = undefined;
1760 std.debug.assert(workers.len <= threads.len);
1761 for (workers) |*worker| {
1762 worker.started = &started;
1763 worker.party = workers.len;
1764 worker.unregistering = unregistering;
1765 }
1766
1767 var spawned: usize = 0;
1768 errdefer {
1769 _ = started.fetchAdd(workers.len - spawned, .acq_rel);
1770 for (threads[0..spawned]) |thread| thread.join();
1771 }
1772 while (spawned < workers.len) : (spawned += 1) {
1773 threads[spawned] = try sys.thread.spawn(RegistrationWorker.run, .{&workers[spawned]});
1774 }
1775 for (threads[0..workers.len]) |thread| thread.join();
1776 }
1777
1778 /// Returns the length of the entry list. Fails when a link disagrees with its neighbor or when the
1779 /// list holds more than `limit` entries.
1780 fn registeredEntryCount(limit: usize) error{CorruptEntryList}!usize {
1781 var length: usize = 0;
1782 var previous: ?*JitCodeEntry = null;
1783 var node = __jit_debug_descriptor.first_entry;
1784 while (node) |entry| : (node = entry.next) {
1785 if (entry.prev != previous) return error.CorruptEntryList;
1786 length += 1;
1787 if (length > limit) return error.CorruptEntryList;
1788 previous = entry;
1789 }
1790 return length;
1791 }
1792
1793 test "GDB JIT registration serializes concurrent register and unregister" {
1794 if (!sys.thread.threadsSupported()) return error.SkipZigTest;
1795
1796 const rounds = 64;
1797 const object = [_]u8{0x7f};
1798 var workers: [4]RegistrationWorker = undefined;
1799 for (&workers) |*worker| {
1800 worker.* = .{ .object = &object, .started = undefined, .party = workers.len };
1801 }
1802
1803 const registered = workers.len * registration_worker_entries;
1804 const limit = registered + 64;
1805 const initial = try registeredEntryCount(limit);
1806 for (0..rounds) |_| {
1807 try runRegistrationPhase(&workers, false);
1808 try std.testing.expectEqual(initial + registered, try registeredEntryCount(limit));
1809 try runRegistrationPhase(&workers, true);
1810 try std.testing.expectEqual(initial, try registeredEntryCount(limit));
1811 }
1812
1813 const action_flag = __jit_debug_descriptor.action_flag;
1814 try std.testing.expectEqual(@backingInt(JitAction.no_action), action_flag);
1815 }