Commit ba17552b4e

Jakub Konka <kubkon@jakubkonka.com>
2022-03-06 18:35:58
dwarf: move all dwarf into standalone module
Hook up Elf and MachO linkers to the new solution.
1 parent 38c161a
src/arch/arm/Emit.zig
@@ -409,7 +409,7 @@ fn genArgDbgInfo(self: *Emit, inst: Air.Inst.Index, arg_index: u32) !void {
             switch (self.debug_output) {
                 .dwarf => |dbg_out| {
                     try dbg_out.dbg_info.ensureUnusedCapacity(3);
-                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Elf.abbrev_parameter);
+                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Dwarf.abbrev_parameter);
                     dbg_out.dbg_info.appendSliceAssumeCapacity(&[2]u8{ // DW.AT.location, DW.FORM.exprloc
                         1, // ULEB128 dwarf expression length
                         reg.dwarfLocOp(),
@@ -440,7 +440,7 @@ fn genArgDbgInfo(self: *Emit, inst: Air.Inst.Index, arg_index: u32) !void {
                         else => unreachable,
                     };
 
-                    try dbg_out.dbg_info.append(link.File.Elf.abbrev_parameter);
+                    try dbg_out.dbg_info.append(link.File.Dwarf.abbrev_parameter);
 
                     // Get length of the LEB128 stack offset
                     var counting_writer = std.io.countingWriter(std.io.null_writer);
src/arch/riscv64/CodeGen.zig
@@ -1384,7 +1384,7 @@ fn genArgDbgInfo(self: *Self, inst: Air.Inst.Index, mcv: MCValue, arg_index: u32
             switch (self.debug_output) {
                 .dwarf => |dbg_out| {
                     try dbg_out.dbg_info.ensureUnusedCapacity(3);
-                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Elf.abbrev_parameter);
+                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Dwarf.abbrev_parameter);
                     dbg_out.dbg_info.appendSliceAssumeCapacity(&[2]u8{ // DW.AT.location, DW.FORM.exprloc
                         1, // ULEB128 dwarf expression length
                         reg.dwarfLocOp(),
src/arch/x86_64/Emit.zig
@@ -1019,14 +1019,7 @@ fn genArgDbgInfo(emit: *Emit, inst: Air.Inst.Index, mcv: MCValue, max_stack: u32
             switch (emit.debug_output) {
                 .dwarf => |dbg_out| {
                     try dbg_out.dbg_info.ensureUnusedCapacity(3);
-
-                    // TODO this will go away once we pull DWARF into a cross-platform module.
-                    if (emit.bin_file.cast(link.File.MachO)) |_| {
-                        dbg_out.dbg_info.appendAssumeCapacity(link.File.MachO.DebugSymbols.abbrev_parameter);
-                    } else if (emit.bin_file.cast(link.File.Elf)) |_| {
-                        dbg_out.dbg_info.appendAssumeCapacity(link.File.Elf.abbrev_parameter);
-                    } else return emit.fail("TODO DWARF in non-MachO and non-ELF backend", .{});
-
+                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Dwarf.abbrev_parameter);
                     dbg_out.dbg_info.appendSliceAssumeCapacity(&[2]u8{ // DW.AT.location, DW.FORM.exprloc
                         1, // ULEB128 dwarf expression length
                         reg.dwarfLocOp(),
@@ -1049,14 +1042,7 @@ fn genArgDbgInfo(emit: *Emit, inst: Air.Inst.Index, mcv: MCValue, max_stack: u32
                     // for example when -fomit-frame-pointer is set.
                     const disp = @intCast(i32, max_stack) - off + 16;
                     try dbg_out.dbg_info.ensureUnusedCapacity(8);
-
-                    // TODO this will go away once we pull DWARF into a cross-platform module.
-                    if (emit.bin_file.cast(link.File.MachO)) |_| {
-                        dbg_out.dbg_info.appendAssumeCapacity(link.File.MachO.DebugSymbols.abbrev_parameter);
-                    } else if (emit.bin_file.cast(link.File.Elf)) |_| {
-                        dbg_out.dbg_info.appendAssumeCapacity(link.File.Elf.abbrev_parameter);
-                    } else return emit.fail("TODO DWARF in non-MachO and non-ELF backend", .{});
-
+                    dbg_out.dbg_info.appendAssumeCapacity(link.File.Dwarf.abbrev_parameter);
                     const fixup = dbg_out.dbg_info.items.len;
                     dbg_out.dbg_info.appendSliceAssumeCapacity(&[2]u8{ // DW.AT.location, DW.FORM.exprloc
                         1, // we will backpatch it after we encode the displacement in LEB128
src/link/MachO/Atom.zig
@@ -13,6 +13,7 @@ const trace = @import("../../tracy.zig").trace;
 
 const Allocator = mem.Allocator;
 const Arch = std.Target.Cpu.Arch;
+const Dwarf = @import("../Dwarf.zig");
 const MachO = @import("../MachO.zig");
 const Object = @import("Object.zig");
 const StringIndexAdapter = std.hash_map.StringIndexAdapter;
@@ -71,14 +72,7 @@ stab: ?Stab = null,
 next: ?*Atom,
 prev: ?*Atom,
 
-/// Previous/next linked list pointers.
-/// This is the linked list node for this Decl's corresponding .debug_info tag.
-dbg_info_prev: ?*Atom,
-dbg_info_next: ?*Atom,
-/// Offset into .debug_info pointing to the tag for this Decl.
-dbg_info_off: u32,
-/// Size of the .debug_info tag for this Decl, not including padding.
-dbg_info_len: u32,
+dbg_info_atom: Dwarf.DebugInfoAtom,
 
 dirty: bool = true,
 
@@ -188,10 +182,7 @@ pub const empty = Atom{
     .alignment = 0,
     .prev = null,
     .next = null,
-    .dbg_info_prev = null,
-    .dbg_info_next = null,
-    .dbg_info_off = undefined,
-    .dbg_info_len = undefined,
+    .dbg_info_atom = undefined,
 };
 
 pub fn deinit(self: *Atom, allocator: Allocator) void {
src/link/MachO/DebugSymbols.zig
@@ -1,29 +1,27 @@
 const DebugSymbols = @This();
 
 const std = @import("std");
+const build_options = @import("build_options");
 const assert = std.debug.assert;
 const fs = std.fs;
+const link = @import("../../link.zig");
 const log = std.log.scoped(.link);
-const leb128 = std.leb;
 const macho = std.macho;
+const makeStaticString = MachO.makeStaticString;
 const math = std.math;
 const mem = std.mem;
-const DW = std.dwarf;
-const leb = std.leb;
-const Allocator = mem.Allocator;
-
-const build_options = @import("build_options");
+const padToIdeal = MachO.padToIdeal;
 const trace = @import("../../tracy.zig").trace;
-const Module = @import("../../Module.zig");
-const Type = @import("../../type.zig").Type;
-const link = @import("../../link.zig");
+
+const Allocator = mem.Allocator;
+const Dwarf = @import("../Dwarf.zig");
 const MachO = @import("../MachO.zig");
+const Module = @import("../../Module.zig");
 const TextBlock = MachO.TextBlock;
-const SrcFn = MachO.SrcFn;
-const makeStaticString = MachO.makeStaticString;
-const padToIdeal = MachO.padToIdeal;
+const Type = @import("../../type.zig").Type;
 
 base: *MachO,
+dwarf: Dwarf,
 file: fs.File,
 
 /// Table of all load commands
@@ -54,23 +52,6 @@ debug_str_section_index: ?u16 = null,
 debug_aranges_section_index: ?u16 = null,
 debug_line_section_index: ?u16 = null,
 
-debug_abbrev_table_offset: ?u64 = null,
-
-/// A list of `SrcFn` whose Line Number Programs have surplus capacity.
-/// This is the same concept as `text_block_free_list`; see those doc comments.
-dbg_line_fn_free_list: std.AutoHashMapUnmanaged(*SrcFn, void) = .{},
-dbg_line_fn_first: ?*SrcFn = null,
-dbg_line_fn_last: ?*SrcFn = null,
-
-/// A list of `TextBlock` whose corresponding .debug_info tags have surplus capacity.
-/// This is the same concept as `text_block_free_list`; see those doc comments.
-dbg_info_decl_free_list: std.AutoHashMapUnmanaged(*TextBlock, void) = .{},
-dbg_info_decl_first: ?*TextBlock = null,
-dbg_info_decl_last: ?*TextBlock = null,
-
-/// Table of debug symbol names aka the debug string table.
-debug_string_table: std.ArrayListUnmanaged(u8) = .{},
-
 load_commands_dirty: bool = false,
 debug_string_table_dirty: bool = false,
 debug_abbrev_section_dirty: bool = false,
@@ -78,25 +59,6 @@ debug_aranges_section_dirty: bool = false,
 debug_info_header_dirty: bool = false,
 debug_line_header_dirty: bool = false,
 
-pub const abbrev_compile_unit = 1;
-pub const abbrev_subprogram = 2;
-pub const abbrev_subprogram_retvoid = 3;
-pub const abbrev_base_type = 4;
-pub const abbrev_ptr_type = 5;
-pub const abbrev_struct_type = 6;
-pub const abbrev_struct_member = 7;
-pub const abbrev_pad1 = 8;
-pub const abbrev_parameter = 9;
-
-/// The reloc offset for the virtual address of a function in its Line Number Program.
-/// Size is a virtual address integer.
-const dbg_line_vaddr_reloc_index = 3;
-/// The reloc offset for the virtual address of a function in its .debug_info TAG.subprogram.
-/// Size is a virtual address integer.
-const dbg_info_low_pc_reloc_index = 1;
-
-const min_nop_size = 2;
-
 /// You must call this function *after* `MachO.populateMissingMetadata()`
 /// has been called to get a viable debug symbols output.
 pub fn populateMissingMetadata(self: *DebugSymbols, allocator: Allocator) !void {
@@ -193,10 +155,10 @@ pub fn populateMissingMetadata(self: *DebugSymbols, allocator: Allocator) !void
     }
 
     if (self.debug_str_section_index == null) {
-        assert(self.debug_string_table.items.len == 0);
+        assert(self.dwarf.strtab.items.len == 0);
         self.debug_str_section_index = try self.allocateSection(
             "__debug_str",
-            @intCast(u32, self.debug_string_table.items.len),
+            @intCast(u32, self.dwarf.strtab.items.len),
             0,
         );
         self.debug_string_table_dirty = true;
@@ -277,7 +239,7 @@ fn detectAllocCollision(self: *DebugSymbols, start: u64, size: u64) ?u64 {
     return null;
 }
 
-fn findFreeSpace(self: *DebugSymbols, object_size: u64, min_alignment: u64) u64 {
+pub fn findFreeSpace(self: *DebugSymbols, object_size: u64, min_alignment: u64) u64 {
     const seg = self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
     var offset: u64 = seg.inner.fileoff;
     while (self.detectAllocCollision(offset, object_size)) |item_end| {
@@ -290,322 +252,45 @@ pub fn flushModule(self: *DebugSymbols, allocator: Allocator, options: link.Opti
     // TODO This linker code currently assumes there is only 1 compilation unit and it corresponds to the
     // Zig source code.
     const module = options.module orelse return error.LinkingWithoutZigSourceUnimplemented;
-    const init_len_size: usize = 4;
 
     if (self.debug_abbrev_section_dirty) {
-        const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-        const debug_abbrev_sect = &dwarf_segment.sections.items[self.debug_abbrev_section_index.?];
-
-        // These are LEB encoded but since the values are all less than 127
-        // we can simply append these bytes.
-        const abbrev_buf = [_]u8{
-            abbrev_compile_unit, DW.TAG.compile_unit, DW.CHILDREN.yes, // header
-            DW.AT.stmt_list,     DW.FORM.sec_offset,  DW.AT.low_pc,
-            DW.FORM.addr,        DW.AT.high_pc,       DW.FORM.addr,
-            DW.AT.name,          DW.FORM.strp,        DW.AT.comp_dir,
-            DW.FORM.strp,        DW.AT.producer,      DW.FORM.strp,
-            DW.AT.language,      DW.FORM.data2,       0,
-            0, // table sentinel
-            abbrev_subprogram,
-            DW.TAG.subprogram,
-            DW.CHILDREN.yes, // header
-            DW.AT.low_pc,
-            DW.FORM.addr,
-            DW.AT.high_pc,
-            DW.FORM.data4,
-            DW.AT.type,
-            DW.FORM.ref4,
-            DW.AT.name,
-            DW.FORM.string,
-            0,                         0, // table sentinel
-            abbrev_subprogram_retvoid,
-            DW.TAG.subprogram, DW.CHILDREN.yes, // header
-            DW.AT.low_pc,      DW.FORM.addr,
-            DW.AT.high_pc,     DW.FORM.data4,
-            DW.AT.name,        DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_base_type,
-            DW.TAG.base_type,
-            DW.CHILDREN.no, // header
-            DW.AT.encoding,
-            DW.FORM.data1,
-            DW.AT.byte_size,
-            DW.FORM.data1,
-            DW.AT.name,
-            DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_ptr_type,
-            DW.TAG.pointer_type,
-            DW.CHILDREN.no, // header
-            DW.AT.type,
-            DW.FORM.ref4,
-            0,
-            0, // table sentinel
-            abbrev_struct_type,
-            DW.TAG.structure_type,
-            DW.CHILDREN.yes, // header
-            DW.AT.byte_size,
-            DW.FORM.sdata,
-            DW.AT.name,
-            DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_struct_member,
-            DW.TAG.member,
-            DW.CHILDREN.no, // header
-            DW.AT.name,
-            DW.FORM.string,
-            DW.AT.type,
-            DW.FORM.ref4,
-            DW.AT.data_member_location,
-            DW.FORM.sdata,
-            0,
-            0, // table sentinel
-            abbrev_pad1,
-            DW.TAG.unspecified_type,
-            DW.CHILDREN.no, // header
-            0,
-            0, // table sentinel
-            abbrev_parameter,
-            DW.TAG.formal_parameter, DW.CHILDREN.no, // header
-            DW.AT.location,          DW.FORM.exprloc,
-            DW.AT.type,              DW.FORM.ref4,
-            DW.AT.name,              DW.FORM.string,
-            0,
-            0, // table sentinel
-            0,
-            0,
-            0, // section sentinel
-        };
-
-        const needed_size = abbrev_buf.len;
-        const allocated_size = self.allocatedSize(debug_abbrev_sect.offset);
-        if (needed_size > allocated_size) {
-            debug_abbrev_sect.size = 0; // free the space
-            const offset = self.findFreeSpace(needed_size, 1);
-            debug_abbrev_sect.offset = @intCast(u32, offset);
-            debug_abbrev_sect.addr = dwarf_segment.inner.vmaddr + offset - dwarf_segment.inner.fileoff;
-        }
-        debug_abbrev_sect.size = needed_size;
-        log.debug("__debug_abbrev start=0x{x} end=0x{x}", .{
-            debug_abbrev_sect.offset,
-            debug_abbrev_sect.offset + needed_size,
-        });
-
-        const abbrev_offset = 0;
-        self.debug_abbrev_table_offset = abbrev_offset;
-        try self.file.pwriteAll(&abbrev_buf, debug_abbrev_sect.offset + abbrev_offset);
+        try self.dwarf.writeDbgAbbrev(&self.base.base);
         self.load_commands_dirty = true;
         self.debug_abbrev_section_dirty = false;
     }
 
-    if (self.debug_info_header_dirty) debug_info: {
-        // If this value is null it means there is an error in the module;
-        // leave debug_info_header_dirty=true.
-        const first_dbg_info_decl = self.dbg_info_decl_first orelse break :debug_info;
-        const last_dbg_info_decl = self.dbg_info_decl_last.?;
-        const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-        const debug_info_sect = &dwarf_segment.sections.items[self.debug_info_section_index.?];
-
-        // We have a function to compute the upper bound size, because it's needed
-        // for determining where to put the offset of the first `LinkBlock`.
-        const needed_bytes = self.dbgInfoNeededHeaderBytes();
-        var di_buf = try std.ArrayList(u8).initCapacity(allocator, needed_bytes);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_info contribution for this compilation unit,
-        // not including the initial length itself.
-        // We have to come back and write it later after we know the size.
-        const after_init_len = di_buf.items.len + init_len_size;
-        // +1 for the final 0 that ends the compilation unit children.
-        const dbg_info_end = last_dbg_info_decl.dbg_info_off + last_dbg_info_decl.dbg_info_len + 1;
-        const init_len = dbg_info_end - after_init_len;
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len));
-        mem.writeIntLittle(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4); // DWARF version
-        const abbrev_offset = self.debug_abbrev_table_offset.?;
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, abbrev_offset));
-        di_buf.appendAssumeCapacity(8); // address size
-        // Write the form for the compile unit, which must match the abbrev table above.
-        const name_strp = try self.makeDebugString(allocator, module.root_pkg.root_src_path);
-        const comp_dir_strp = try self.makeDebugString(allocator, module.root_pkg.root_src_directory.path orelse ".");
-        const producer_strp = try self.makeDebugString(allocator, link.producer_string);
+    if (self.debug_info_header_dirty) {
         // Currently only one compilation unit is supported, so the address range is simply
         // identical to the main program header virtual address and memory size.
         const text_segment = self.load_commands.items[self.text_segment_cmd_index.?].segment;
         const text_section = text_segment.sections.items[self.text_section_index.?];
         const low_pc = text_section.addr;
         const high_pc = text_section.addr + text_section.size;
-
-        di_buf.appendAssumeCapacity(abbrev_compile_unit);
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), 0); // DW.AT.stmt_list, DW.FORM.sec_offset
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), low_pc);
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), high_pc);
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, name_strp));
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, comp_dir_strp));
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, producer_strp));
-        // We are still waiting on dwarf-std.org to assign DW_LANG_Zig a number:
-        // http://dwarfstd.org/ShowIssue.php?issue=171115.1
-        // Until then we say it is C99.
-        mem.writeIntLittle(u16, di_buf.addManyAsArrayAssumeCapacity(2), DW.LANG.C99);
-
-        if (di_buf.items.len > first_dbg_info_decl.dbg_info_off) {
-            // Move the first N decls to the end to make more padding for the header.
-            @panic("TODO: handle __debug_info header exceeding its padding");
-        }
-        const jmp_amt = first_dbg_info_decl.dbg_info_off - di_buf.items.len;
-        try self.pwriteDbgInfoNops(0, di_buf.items, jmp_amt, false, debug_info_sect.offset);
+        try self.dwarf.writeDbgInfoHeader(&self.base.base, module, low_pc, high_pc);
         self.debug_info_header_dirty = false;
     }
 
     if (self.debug_aranges_section_dirty) {
-        const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-        const debug_aranges_sect = &dwarf_segment.sections.items[self.debug_aranges_section_index.?];
-
-        // Enough for all the data without resizing. When support for more compilation units
-        // is added, the size of this section will become more variable.
-        var di_buf = try std.ArrayList(u8).initCapacity(allocator, 100);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_aranges contribution for this compilation unit,
-        // not including the initial length itself.
-        // We have to come back and write it later after we know the size.
-        const init_len_index = di_buf.items.len;
-        di_buf.items.len += init_len_size;
-        const after_init_len = di_buf.items.len;
-        mem.writeIntLittle(u16, di_buf.addManyAsArrayAssumeCapacity(2), 2); // version
-        // When more than one compilation unit is supported, this will be the offset to it.
-        // For now it is always at offset 0 in .debug_info.
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), 0); // __debug_info offset
-        di_buf.appendAssumeCapacity(@sizeOf(u64)); // address_size
-        di_buf.appendAssumeCapacity(0); // segment_selector_size
-
-        const end_header_offset = di_buf.items.len;
-        const begin_entries_offset = mem.alignForward(end_header_offset, @sizeOf(u64) * 2);
-        di_buf.appendNTimesAssumeCapacity(0, begin_entries_offset - end_header_offset);
-
         // Currently only one compilation unit is supported, so the address range is simply
         // identical to the main program header virtual address and memory size.
         const text_segment = self.load_commands.items[self.text_segment_cmd_index.?].segment;
         const text_section = text_segment.sections.items[self.text_section_index.?];
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), text_section.addr);
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), text_section.size);
-
-        // Sentinel.
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), 0);
-        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), 0);
-
-        // Go back and populate the initial length.
-        const init_len = di_buf.items.len - after_init_len;
-        // initial length - length of the .debug_aranges contribution for this compilation unit,
-        // not including the initial length itself.
-        mem.writeIntLittle(u32, di_buf.items[init_len_index..][0..4], @intCast(u32, init_len));
-
-        const needed_size = di_buf.items.len;
-        const allocated_size = self.allocatedSize(debug_aranges_sect.offset);
-        if (needed_size > allocated_size) {
-            debug_aranges_sect.size = 0; // free the space
-            const new_offset = self.findFreeSpace(needed_size, 16);
-            debug_aranges_sect.addr = dwarf_segment.inner.vmaddr + new_offset - dwarf_segment.inner.fileoff;
-            debug_aranges_sect.offset = @intCast(u32, new_offset);
-        }
-        debug_aranges_sect.size = needed_size;
-        log.debug("__debug_aranges start=0x{x} end=0x{x}", .{
-            debug_aranges_sect.offset,
-            debug_aranges_sect.offset + needed_size,
-        });
-
-        try self.file.pwriteAll(di_buf.items, debug_aranges_sect.offset);
+        try self.dwarf.writeDbgAranges(&self.base.base, text_section.addr, text_section.size);
         self.load_commands_dirty = true;
         self.debug_aranges_section_dirty = false;
     }
-    if (self.debug_line_header_dirty) debug_line: {
-        if (self.dbg_line_fn_first == null) {
-            break :debug_line; // Error in module; leave debug_line_header_dirty=true.
-        }
-        const dbg_line_prg_off = self.getDebugLineProgramOff();
-        const dbg_line_prg_end = self.getDebugLineProgramEnd();
-        assert(dbg_line_prg_end != 0);
 
-        const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-        const debug_line_sect = &dwarf_segment.sections.items[self.debug_line_section_index.?];
-
-        // The size of this header is variable, depending on the number of directories,
-        // files, and padding. We have a function to compute the upper bound size, however,
-        // because it's needed for determining where to put the offset of the first `SrcFn`.
-        const needed_bytes = self.dbgLineNeededHeaderBytes(module);
-        var di_buf = try std.ArrayList(u8).initCapacity(allocator, needed_bytes);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_line contribution for this compilation unit,
-        // not including the initial length itself.
-        const after_init_len = di_buf.items.len + init_len_size;
-        const init_len = dbg_line_prg_end - after_init_len;
-        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len));
-        mem.writeIntLittle(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4); // version
-
-        // Empirically, debug info consumers do not respect this field, or otherwise
-        // consider it to be an error when it does not point exactly to the end of the header.
-        // Therefore we rely on the NOP jump at the beginning of the Line Number Program for
-        // padding rather than this field.
-        const before_header_len = di_buf.items.len;
-        di_buf.items.len += @sizeOf(u32); // We will come back and write this.
-        const after_header_len = di_buf.items.len;
-
-        const opcode_base = DW.LNS.set_isa + 1;
-        di_buf.appendSliceAssumeCapacity(&[_]u8{
-            1, // minimum_instruction_length
-            1, // maximum_operations_per_instruction
-            1, // default_is_stmt
-            1, // line_base (signed)
-            1, // line_range
-            opcode_base,
-
-            // Standard opcode lengths. The number of items here is based on `opcode_base`.
-            // The value is the number of LEB128 operands the instruction takes.
-            0, // `DW.LNS.copy`
-            1, // `DW.LNS.advance_pc`
-            1, // `DW.LNS.advance_line`
-            1, // `DW.LNS.set_file`
-            1, // `DW.LNS.set_column`
-            0, // `DW.LNS.negate_stmt`
-            0, // `DW.LNS.set_basic_block`
-            0, // `DW.LNS.const_add_pc`
-            1, // `DW.LNS.fixed_advance_pc`
-            0, // `DW.LNS.set_prologue_end`
-            0, // `DW.LNS.set_epilogue_begin`
-            1, // `DW.LNS.set_isa`
-            0, // include_directories (none except the compilation unit cwd)
-        });
-        // file_names[0]
-        di_buf.appendSliceAssumeCapacity(module.root_pkg.root_src_path); // relative path name
-        di_buf.appendSliceAssumeCapacity(&[_]u8{
-            0, // null byte for the relative path name
-            0, // directory_index
-            0, // mtime (TODO supply this)
-            0, // file size bytes (TODO supply this)
-            0, // file_names sentinel
-        });
-
-        const header_len = di_buf.items.len - after_header_len;
-        mem.writeIntLittle(u32, di_buf.items[before_header_len..][0..4], @intCast(u32, header_len));
-
-        // We use NOPs because consumers empirically do not respect the header length field.
-        if (di_buf.items.len > dbg_line_prg_off) {
-            // Move the first N files to the end to make more padding for the header.
-            @panic("TODO: handle __debug_line header exceeding its padding");
-        }
-        const jmp_amt = dbg_line_prg_off - di_buf.items.len;
-        try self.pwriteDbgLineNops(0, di_buf.items, jmp_amt, debug_line_sect.offset);
+    if (self.debug_line_header_dirty) {
+        try self.dwarf.writeDbgLineHeader(&self.base.base, module);
         self.debug_line_header_dirty = false;
     }
+
     {
         const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
         const debug_strtab_sect = &dwarf_segment.sections.items[self.debug_str_section_index.?];
-        if (self.debug_string_table_dirty or self.debug_string_table.items.len != debug_strtab_sect.size) {
+        if (self.debug_string_table_dirty or self.dwarf.strtab.items.len != debug_strtab_sect.size) {
             const allocated_size = self.allocatedSize(debug_strtab_sect.offset);
-            const needed_size = self.debug_string_table.items.len;
+            const needed_size = self.dwarf.strtab.items.len;
 
             if (needed_size > allocated_size) {
                 debug_strtab_sect.size = 0; // free the space
@@ -620,7 +305,7 @@ pub fn flushModule(self: *DebugSymbols, allocator: Allocator, options: link.Opti
                 debug_strtab_sect.offset + needed_size,
             });
 
-            try self.file.pwriteAll(self.debug_string_table.items, debug_strtab_sect.offset);
+            try self.file.pwriteAll(self.dwarf.strtab.items, debug_strtab_sect.offset);
             self.load_commands_dirty = true;
             self.debug_string_table_dirty = false;
         }
@@ -639,13 +324,11 @@ pub fn flushModule(self: *DebugSymbols, allocator: Allocator, options: link.Opti
 }
 
 pub fn deinit(self: *DebugSymbols, allocator: Allocator) void {
-    self.dbg_info_decl_free_list.deinit(allocator);
-    self.dbg_line_fn_free_list.deinit(allocator);
-    self.debug_string_table.deinit(allocator);
     for (self.load_commands.items) |*lc| {
         lc.deinit(allocator);
     }
     self.load_commands.deinit(allocator);
+    self.dwarf.deinit();
     self.file.close();
 }
 
@@ -777,7 +460,7 @@ fn writeHeader(self: *DebugSymbols) !void {
     try self.file.pwriteAll(mem.asBytes(&header), 0);
 }
 
-fn allocatedSize(self: *DebugSymbols, start: u64) u64 {
+pub fn allocatedSize(self: *DebugSymbols, start: u64) u64 {
     const seg = self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
     assert(start >= seg.inner.fileoff);
     var min_pos: u64 = std.math.maxInt(u64);
@@ -964,813 +647,22 @@ fn writeStringTable(self: *DebugSymbols) !void {
 
 pub fn updateDeclLineNumber(self: *DebugSymbols, module: *Module, decl: *const Module.Decl) !void {
     _ = module;
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    log.debug("updateDeclLineNumber {s}{*}", .{ decl.name, decl });
-
-    const func = decl.val.castTag(.function).?.data;
-    log.debug("  (decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d})", .{
-        decl.src_line,
-        func.lbrace_line,
-        func.rbrace_line,
-    });
-    const line = @intCast(u28, decl.src_line + func.lbrace_line);
-
-    const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-    const shdr = &dwarf_segment.sections.items[self.debug_line_section_index.?];
-    const file_pos = shdr.offset + decl.fn_link.macho.off + getRelocDbgLineOff();
-    var data: [4]u8 = undefined;
-    leb.writeUnsignedFixed(4, &data, line);
-    try self.file.pwriteAll(&data, file_pos);
+    return self.dwarf.updateDeclLineNumber(&self.base.base, decl);
 }
 
-pub const DeclDebugBuffers = struct {
-    dbg_line_buffer: std.ArrayList(u8),
-    dbg_info_buffer: std.ArrayList(u8),
-    dbg_info_type_relocs: link.File.DbgInfoTypeRelocsTable,
-};
-
 /// Caller owns the returned memory.
-pub fn initDeclDebugBuffers(
-    self: *DebugSymbols,
-    allocator: Allocator,
-    module: *Module,
-    decl: *Module.Decl,
-) !DeclDebugBuffers {
-    _ = self;
+pub fn initDeclDebugInfo(self: *DebugSymbols, module: *Module, decl: *Module.Decl) !Dwarf.DeclDebugBuffers {
     _ = module;
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    var dbg_line_buffer = std.ArrayList(u8).init(allocator);
-    var dbg_info_buffer = std.ArrayList(u8).init(allocator);
-    var dbg_info_type_relocs: link.File.DbgInfoTypeRelocsTable = .{};
-
-    assert(decl.has_tv);
-    switch (decl.ty.zigTypeTag()) {
-        .Fn => {
-            // For functions we need to add a prologue to the debug line program.
-            try dbg_line_buffer.ensureTotalCapacity(26);
-
-            const func = decl.val.castTag(.function).?.data;
-            log.debug("updateFunc {s}{*}", .{ decl.name, func.owner_decl });
-            log.debug("  (decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d})", .{
-                decl.src_line,
-                func.lbrace_line,
-                func.rbrace_line,
-            });
-            const line = @intCast(u28, decl.src_line + func.lbrace_line);
-
-            dbg_line_buffer.appendSliceAssumeCapacity(&[_]u8{
-                DW.LNS.extended_op,
-                @sizeOf(u64) + 1,
-                DW.LNE.set_address,
-            });
-            // This is the "relocatable" vaddr, corresponding to `code_buffer` index `0`.
-            assert(dbg_line_vaddr_reloc_index == dbg_line_buffer.items.len);
-            dbg_line_buffer.items.len += @sizeOf(u64);
-
-            dbg_line_buffer.appendAssumeCapacity(DW.LNS.advance_line);
-            // This is the "relocatable" relative line offset from the previous function's end curly
-            // to this function's begin curly.
-            assert(getRelocDbgLineOff() == dbg_line_buffer.items.len);
-            // Here we use a ULEB128-fixed-4 to make sure this field can be overwritten later.
-            leb.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), line);
-
-            dbg_line_buffer.appendAssumeCapacity(DW.LNS.set_file);
-            assert(getRelocDbgFileIndex() == dbg_line_buffer.items.len);
-            // Once we support more than one source file, this will have the ability to be more
-            // than one possible value.
-            const file_index = 1;
-            leb.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), file_index);
-
-            // Emit a line for the begin curly with prologue_end=false. The codegen will
-            // do the work of setting prologue_end=true and epilogue_begin=true.
-            dbg_line_buffer.appendAssumeCapacity(DW.LNS.copy);
-
-            // .debug_info subprogram
-            const decl_name_with_null = decl.name[0 .. mem.sliceTo(decl.name, 0).len + 1];
-            try dbg_info_buffer.ensureUnusedCapacity(25 + decl_name_with_null.len);
-
-            const fn_ret_type = decl.ty.fnReturnType();
-            const fn_ret_has_bits = fn_ret_type.hasRuntimeBits();
-            if (fn_ret_has_bits) {
-                dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram);
-            } else {
-                dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram_retvoid);
-            }
-            // These get overwritten after generating the machine code. These values are
-            // "relocations" and have to be in this fixed place so that functions can be
-            // moved in virtual address space.
-            assert(dbg_info_low_pc_reloc_index == dbg_info_buffer.items.len);
-            dbg_info_buffer.items.len += @sizeOf(u64); // DW.AT.low_pc,  DW.FORM.addr
-            assert(getRelocDbgInfoSubprogramHighPC() == dbg_info_buffer.items.len);
-            dbg_info_buffer.items.len += 4; // DW.AT.high_pc,  DW.FORM.data4
-            if (fn_ret_has_bits) {
-                const gop = try dbg_info_type_relocs.getOrPut(allocator, fn_ret_type);
-                if (!gop.found_existing) {
-                    gop.value_ptr.* = .{
-                        .off = undefined,
-                        .relocs = .{},
-                    };
-                }
-                try gop.value_ptr.relocs.append(allocator, @intCast(u32, dbg_info_buffer.items.len));
-                dbg_info_buffer.items.len += 4; // DW.AT.type,  DW.FORM.ref4
-            }
-            dbg_info_buffer.appendSliceAssumeCapacity(decl_name_with_null); // DW.AT.name, DW.FORM.string
-        },
-        else => {
-            // TODO implement .debug_info for global variables
-        },
-    }
-
-    return DeclDebugBuffers{
-        .dbg_info_buffer = dbg_info_buffer,
-        .dbg_line_buffer = dbg_line_buffer,
-        .dbg_info_type_relocs = dbg_info_type_relocs,
-    };
+    return self.dwarf.initDeclDebugInfo(decl);
 }
 
 pub fn commitDeclDebugInfo(
     self: *DebugSymbols,
-    allocator: Allocator,
     module: *Module,
     decl: *Module.Decl,
-    debug_buffers: *DeclDebugBuffers,
+    debug_buffers: *Dwarf.DeclDebugBuffers,
 ) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    var dbg_line_buffer = &debug_buffers.dbg_line_buffer;
-    var dbg_info_buffer = &debug_buffers.dbg_info_buffer;
-    var dbg_info_type_relocs = &debug_buffers.dbg_info_type_relocs;
-
     const symbol = self.base.locals.items[decl.link.macho.local_sym_index];
-    const text_block = &decl.link.macho;
-    // If the Decl is a function, we need to update the __debug_line program.
-    assert(decl.has_tv);
-    switch (decl.ty.zigTypeTag()) {
-        .Fn => {
-            // Perform the relocations based on vaddr.
-            {
-                const ptr = dbg_line_buffer.items[dbg_line_vaddr_reloc_index..][0..8];
-                mem.writeIntLittle(u64, ptr, symbol.n_value);
-            }
-            {
-                const ptr = dbg_info_buffer.items[dbg_info_low_pc_reloc_index..][0..8];
-                mem.writeIntLittle(u64, ptr, symbol.n_value);
-            }
-            {
-                const ptr = dbg_info_buffer.items[getRelocDbgInfoSubprogramHighPC()..][0..4];
-                mem.writeIntLittle(u32, ptr, @intCast(u32, text_block.size));
-            }
-
-            try dbg_line_buffer.appendSlice(&[_]u8{ DW.LNS.extended_op, 1, DW.LNE.end_sequence });
-
-            // Now we have the full contents and may allocate a region to store it.
-
-            // This logic is nearly identical to the logic below in `updateDeclDebugInfo` for
-            // `TextBlock` and the .debug_info. If you are editing this logic, you
-            // probably need to edit that logic too.
-
-            const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-            const debug_line_sect = &dwarf_segment.sections.items[self.debug_line_section_index.?];
-            const src_fn = &decl.fn_link.macho;
-            src_fn.len = @intCast(u32, dbg_line_buffer.items.len);
-            if (self.dbg_line_fn_last) |last| blk: {
-                if (src_fn == last) break :blk;
-                if (src_fn.next) |next| {
-                    // Update existing function - non-last item.
-                    if (src_fn.off + src_fn.len + min_nop_size > next.off) {
-                        // It grew too big, so we move it to a new location.
-                        if (src_fn.prev) |prev| {
-                            self.dbg_line_fn_free_list.put(allocator, prev, {}) catch {};
-                            prev.next = src_fn.next;
-                        }
-                        next.prev = src_fn.prev;
-                        src_fn.next = null;
-                        // Populate where it used to be with NOPs.
-                        const file_pos = debug_line_sect.offset + src_fn.off;
-                        try self.pwriteDbgLineNops(0, &[0]u8{}, src_fn.len, file_pos);
-                        // TODO Look at the free list before appending at the end.
-                        src_fn.prev = last;
-                        last.next = src_fn;
-                        self.dbg_line_fn_last = src_fn;
-
-                        src_fn.off = last.off + padToIdeal(last.len);
-                    }
-                } else if (src_fn.prev == null) {
-                    // Append new function.
-                    // TODO Look at the free list before appending at the end.
-                    src_fn.prev = last;
-                    last.next = src_fn;
-                    self.dbg_line_fn_last = src_fn;
-
-                    src_fn.off = last.off + padToIdeal(last.len);
-                }
-            } else {
-                // This is the first function of the Line Number Program.
-                self.dbg_line_fn_first = src_fn;
-                self.dbg_line_fn_last = src_fn;
-
-                src_fn.off = padToIdeal(self.dbgLineNeededHeaderBytes(module));
-            }
-
-            const last_src_fn = self.dbg_line_fn_last.?;
-            const needed_size = last_src_fn.off + last_src_fn.len;
-            if (needed_size != debug_line_sect.size) {
-                if (needed_size > self.allocatedSize(debug_line_sect.offset)) {
-                    const new_offset = self.findFreeSpace(needed_size, 1);
-                    const existing_size = last_src_fn.off;
-
-                    log.debug("moving __debug_line section: {} bytes from 0x{x} to 0x{x}", .{
-                        existing_size,
-                        debug_line_sect.offset,
-                        new_offset,
-                    });
-
-                    try MachO.copyRangeAllOverlappingAlloc(
-                        self.base.base.allocator,
-                        self.file,
-                        debug_line_sect.offset,
-                        new_offset,
-                        existing_size,
-                    );
-
-                    debug_line_sect.offset = @intCast(u32, new_offset);
-                    debug_line_sect.addr = dwarf_segment.inner.vmaddr + new_offset - dwarf_segment.inner.fileoff;
-                }
-                debug_line_sect.size = needed_size;
-                self.load_commands_dirty = true; // TODO look into making only the one section dirty
-                self.debug_line_header_dirty = true;
-            }
-            const prev_padding_size: u32 = if (src_fn.prev) |prev| src_fn.off - (prev.off + prev.len) else 0;
-            const next_padding_size: u32 = if (src_fn.next) |next| next.off - (src_fn.off + src_fn.len) else 0;
-
-            // We only have support for one compilation unit so far, so the offsets are directly
-            // from the .debug_line section.
-            const file_pos = debug_line_sect.offset + src_fn.off;
-            try self.pwriteDbgLineNops(prev_padding_size, dbg_line_buffer.items, next_padding_size, file_pos);
-
-            // .debug_info - End the TAG.subprogram children.
-            try dbg_info_buffer.append(0);
-        },
-        else => {},
-    }
-
-    if (dbg_info_buffer.items.len == 0)
-        return;
-
-    // We need this for the duration of this function only so that for composite
-    // types such as []const u32, if the type *u32 is non-existent, we create
-    // it synthetically and store the backing bytes in this arena. After we are
-    // done with the relocations, we can safely deinit the entire memory slab.
-    // TODO currently, we do not store the relocations for future use, however,
-    // if that is the case, we should move memory management to a higher scope,
-    // such as linker scope, or whatnot.
-    var dbg_type_arena = std.heap.ArenaAllocator.init(allocator);
-    defer dbg_type_arena.deinit();
-
-    {
-        // Now we emit the .debug_info types of the Decl. These will count towards the size of
-        // the buffer, so we have to do it before computing the offset, and we can't perform the actual
-        // relocations yet.
-        var it: usize = 0;
-        while (it < dbg_info_type_relocs.count()) : (it += 1) {
-            const ty = dbg_info_type_relocs.keys()[it];
-            const value_ptr = dbg_info_type_relocs.getPtr(ty).?;
-            value_ptr.off = @intCast(u32, dbg_info_buffer.items.len);
-            try self.addDbgInfoType(dbg_type_arena.allocator(), ty, dbg_info_buffer, dbg_info_type_relocs);
-        }
-    }
-
-    try self.updateDeclDebugInfoAllocation(allocator, text_block, @intCast(u32, dbg_info_buffer.items.len));
-
-    {
-        // Now that we have the offset assigned we can finally perform type relocations.
-        for (dbg_info_type_relocs.values()) |value| {
-            for (value.relocs.items) |off| {
-                mem.writeIntLittle(
-                    u32,
-                    dbg_info_buffer.items[off..][0..4],
-                    text_block.dbg_info_off + value.off,
-                );
-            }
-        }
-    }
-
-    try self.writeDeclDebugInfo(text_block, dbg_info_buffer.items);
-}
-
-/// Asserts the type has codegen bits.
-fn addDbgInfoType(
-    self: *DebugSymbols,
-    arena: Allocator,
-    ty: Type,
-    dbg_info_buffer: *std.ArrayList(u8),
-    dbg_info_type_relocs: *link.File.DbgInfoTypeRelocsTable,
-) !void {
-    const target = self.base.base.options.target;
-    var relocs = std.ArrayList(struct { ty: Type, reloc: u32 }).init(arena);
-
-    switch (ty.zigTypeTag()) {
-        .NoReturn => unreachable,
-        .Void => {
-            try dbg_info_buffer.append(abbrev_pad1);
-        },
-        .Bool => {
-            try dbg_info_buffer.appendSlice(&[_]u8{
-                abbrev_base_type,
-                DW.ATE.boolean, // DW.AT.encoding ,  DW.FORM.data1
-                1, // DW.AT.byte_size,  DW.FORM.data1
-                'b', 'o', 'o', 'l', 0, // DW.AT.name,  DW.FORM.string
-            });
-        },
-        .Int => {
-            const info = ty.intInfo(target);
-            try dbg_info_buffer.ensureUnusedCapacity(12);
-            dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
-            // DW.AT.encoding, DW.FORM.data1
-            dbg_info_buffer.appendAssumeCapacity(switch (info.signedness) {
-                .signed => DW.ATE.signed,
-                .unsigned => DW.ATE.unsigned,
-            });
-            // DW.AT.byte_size,  DW.FORM.data1
-            dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
-            // DW.AT.name,  DW.FORM.string
-            try dbg_info_buffer.writer().print("{}\x00", .{ty});
-        },
-        .Optional => {
-            if (ty.isPtrLikeOptional()) {
-                try dbg_info_buffer.ensureUnusedCapacity(12);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
-                // DW.AT.encoding, DW.FORM.data1
-                dbg_info_buffer.appendAssumeCapacity(DW.ATE.address);
-                // DW.AT.byte_size,  DW.FORM.data1
-                dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
-                // DW.AT.name,  DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-            } else {
-                // Non-pointer optionals are structs: struct { .maybe = *, .val = * }
-                var buf = try arena.create(Type.Payload.ElemType);
-                const payload_ty = ty.optionalChild(buf);
-                // DW.AT.structure_type
-                try dbg_info_buffer.append(abbrev_struct_type);
-                // DW.AT.byte_size, DW.FORM.sdata
-                const abi_size = ty.abiSize(target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
-                // DW.AT.name, DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(7);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("maybe");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = Type.bool, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(6);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.member
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("val");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = payload_ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                const offset = abi_size - payload_ty.abiSize(target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), offset);
-                // DW.AT.structure_type delimit children
-                try dbg_info_buffer.append(0);
-            }
-        },
-        .Pointer => {
-            if (ty.isSlice()) {
-                // Slices are structs: struct { .ptr = *, .len = N }
-                // DW.AT.structure_type
-                try dbg_info_buffer.ensureUnusedCapacity(2);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_type);
-                // DW.AT.byte_size, DW.FORM.sdata
-                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize) * 2);
-                // DW.AT.name, DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(5);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("ptr");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                var buf = try arena.create(Type.SlicePtrFieldTypeBuffer);
-                const ptr_ty = ty.slicePtrFieldType(buf);
-                try relocs.append(.{ .ty = ptr_ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(6);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.member
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("len");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = Type.initTag(.usize), .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(2);
-                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize));
-                // DW.AT.structure_type delimit children
-                dbg_info_buffer.appendAssumeCapacity(0);
-            } else {
-                try dbg_info_buffer.ensureUnusedCapacity(5);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_ptr_type);
-                // DW.AT.type, DW.FORM.ref4
-                const index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = ty.childType(), .reloc = @intCast(u32, index) });
-            }
-        },
-        .Struct => blk: {
-            // try dbg_info_buffer.ensureUnusedCapacity(23);
-            // DW.AT.structure_type
-            try dbg_info_buffer.append(abbrev_struct_type);
-            // DW.AT.byte_size, DW.FORM.sdata
-            const abi_size = ty.abiSize(target);
-            try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
-            // DW.AT.name, DW.FORM.string
-            const struct_name = try ty.nameAlloc(arena);
-            try dbg_info_buffer.ensureUnusedCapacity(struct_name.len + 1);
-            dbg_info_buffer.appendSliceAssumeCapacity(struct_name);
-            dbg_info_buffer.appendAssumeCapacity(0);
-
-            const struct_obj = ty.castTag(.@"struct").?.data;
-            if (struct_obj.layout == .Packed) {
-                log.debug("TODO implement .debug_info for packed structs", .{});
-                break :blk;
-            }
-
-            const fields = ty.structFields();
-            for (fields.keys()) |field_name, field_index| {
-                const field = fields.get(field_name).?;
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(field_name.len + 2);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity(field_name);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = field.ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                const field_off = ty.structFieldOffset(field_index, target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), field_off);
-            }
-
-            // DW.AT.structure_type delimit children
-            try dbg_info_buffer.append(0);
-        },
-        else => {
-            log.debug("TODO implement .debug_info for type '{}'", .{ty});
-            try dbg_info_buffer.append(abbrev_pad1);
-        },
-    }
-
-    for (relocs.items) |rel| {
-        const gop = try dbg_info_type_relocs.getOrPut(self.base.base.allocator, rel.ty);
-        if (!gop.found_existing) {
-            gop.value_ptr.* = .{
-                .off = undefined,
-                .relocs = .{},
-            };
-        }
-        try gop.value_ptr.relocs.append(self.base.base.allocator, rel.reloc);
-    }
-}
-
-fn updateDeclDebugInfoAllocation(
-    self: *DebugSymbols,
-    allocator: Allocator,
-    text_block: *TextBlock,
-    len: u32,
-) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    // This logic is nearly identical to the logic above in `updateDecl` for
-    // `SrcFn` and the line number programs. If you are editing this logic, you
-    // probably need to edit that logic too.
-
-    const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-    const debug_info_sect = &dwarf_segment.sections.items[self.debug_info_section_index.?];
-    text_block.dbg_info_len = len;
-    if (self.dbg_info_decl_last) |last| blk: {
-        if (text_block == last) break :blk;
-        if (text_block.dbg_info_next) |next| {
-            // Update existing Decl - non-last item.
-            if (text_block.dbg_info_off + text_block.dbg_info_len + min_nop_size > next.dbg_info_off) {
-                // It grew too big, so we move it to a new location.
-                if (text_block.dbg_info_prev) |prev| {
-                    self.dbg_info_decl_free_list.put(allocator, prev, {}) catch {};
-                    prev.dbg_info_next = text_block.dbg_info_next;
-                }
-                next.dbg_info_prev = text_block.dbg_info_prev;
-                text_block.dbg_info_next = null;
-                // Populate where it used to be with NOPs.
-                const file_pos = debug_info_sect.offset + text_block.dbg_info_off;
-                try self.pwriteDbgInfoNops(0, &[0]u8{}, text_block.dbg_info_len, false, file_pos);
-                // TODO Look at the free list before appending at the end.
-                text_block.dbg_info_prev = last;
-                last.dbg_info_next = text_block;
-                self.dbg_info_decl_last = text_block;
-
-                text_block.dbg_info_off = last.dbg_info_off + padToIdeal(last.dbg_info_len);
-            }
-        } else if (text_block.dbg_info_prev == null) {
-            // Append new Decl.
-            // TODO Look at the free list before appending at the end.
-            text_block.dbg_info_prev = last;
-            last.dbg_info_next = text_block;
-            self.dbg_info_decl_last = text_block;
-
-            text_block.dbg_info_off = last.dbg_info_off + padToIdeal(last.dbg_info_len);
-        }
-    } else {
-        // This is the first Decl of the .debug_info
-        self.dbg_info_decl_first = text_block;
-        self.dbg_info_decl_last = text_block;
-
-        text_block.dbg_info_off = padToIdeal(self.dbgInfoNeededHeaderBytes());
-    }
-}
-
-fn writeDeclDebugInfo(self: *DebugSymbols, text_block: *TextBlock, dbg_info_buf: []const u8) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    // This logic is nearly identical to the logic above in `updateDecl` for
-    // `SrcFn` and the line number programs. If you are editing this logic, you
-    // probably need to edit that logic too.
-
-    const dwarf_segment = &self.load_commands.items[self.dwarf_segment_cmd_index.?].segment;
-    const debug_info_sect = &dwarf_segment.sections.items[self.debug_info_section_index.?];
-
-    const last_decl = self.dbg_info_decl_last.?;
-    // +1 for a trailing zero to end the children of the decl tag.
-    const needed_size = last_decl.dbg_info_off + last_decl.dbg_info_len + 1;
-    if (needed_size != debug_info_sect.size) {
-        if (needed_size > self.allocatedSize(debug_info_sect.offset)) {
-            const new_offset = self.findFreeSpace(needed_size, 1);
-            const existing_size = last_decl.dbg_info_off;
-
-            log.debug("moving __debug_info section: {} bytes from 0x{x} to 0x{x}", .{
-                existing_size,
-                debug_info_sect.offset,
-                new_offset,
-            });
-
-            try MachO.copyRangeAllOverlappingAlloc(
-                self.base.base.allocator,
-                self.file,
-                debug_info_sect.offset,
-                new_offset,
-                existing_size,
-            );
-
-            debug_info_sect.offset = @intCast(u32, new_offset);
-            debug_info_sect.addr = dwarf_segment.inner.vmaddr + new_offset - dwarf_segment.inner.fileoff;
-        }
-        debug_info_sect.size = needed_size;
-        self.load_commands_dirty = true; // TODO look into making only the one section dirty
-        self.debug_info_header_dirty = true;
-    }
-    const prev_padding_size: u32 = if (text_block.dbg_info_prev) |prev|
-        text_block.dbg_info_off - (prev.dbg_info_off + prev.dbg_info_len)
-    else
-        0;
-    const next_padding_size: u32 = if (text_block.dbg_info_next) |next|
-        next.dbg_info_off - (text_block.dbg_info_off + text_block.dbg_info_len)
-    else
-        0;
-
-    // To end the children of the decl tag.
-    const trailing_zero = text_block.dbg_info_next == null;
-
-    // We only have support for one compilation unit so far, so the offsets are directly
-    // from the .debug_info section.
-    const file_pos = debug_info_sect.offset + text_block.dbg_info_off;
-    try self.pwriteDbgInfoNops(prev_padding_size, dbg_info_buf, next_padding_size, trailing_zero, file_pos);
-}
-
-fn getDebugLineProgramOff(self: DebugSymbols) u32 {
-    return self.dbg_line_fn_first.?.off;
-}
-
-fn getDebugLineProgramEnd(self: DebugSymbols) u32 {
-    return self.dbg_line_fn_last.?.off + self.dbg_line_fn_last.?.len;
-}
-
-/// TODO Improve this to use a table.
-fn makeDebugString(self: *DebugSymbols, allocator: Allocator, bytes: []const u8) !u32 {
-    try self.debug_string_table.ensureUnusedCapacity(allocator, bytes.len + 1);
-    const result = self.debug_string_table.items.len;
-    self.debug_string_table.appendSliceAssumeCapacity(bytes);
-    self.debug_string_table.appendAssumeCapacity(0);
-    return @intCast(u32, result);
-}
-
-/// The reloc offset for the line offset of a function from the previous function's line.
-/// It's a fixed-size 4-byte ULEB128.
-fn getRelocDbgLineOff() usize {
-    return dbg_line_vaddr_reloc_index + @sizeOf(u64) + 1;
-}
-
-fn getRelocDbgFileIndex() usize {
-    return getRelocDbgLineOff() + 5;
-}
-
-fn getRelocDbgInfoSubprogramHighPC() u32 {
-    return dbg_info_low_pc_reloc_index + @sizeOf(u64);
-}
-
-fn dbgLineNeededHeaderBytes(self: DebugSymbols, module: *Module) u32 {
-    _ = self;
-    const directory_entry_format_count = 1;
-    const file_name_entry_format_count = 1;
-    const directory_count = 1;
-    const file_name_count = 1;
-    const root_src_dir_path_len = if (module.root_pkg.root_src_directory.path) |p| p.len else 1; // "."
-    return @intCast(u32, 53 + directory_entry_format_count * 2 + file_name_entry_format_count * 2 +
-        directory_count * 8 + file_name_count * 8 +
-        // These are encoded as DW.FORM.string rather than DW.FORM.strp as we would like
-        // because of a workaround for readelf and gdb failing to understand DWARFv5 correctly.
-        root_src_dir_path_len +
-        module.root_pkg.root_src_path.len);
-}
-
-fn dbgInfoNeededHeaderBytes(self: DebugSymbols) u32 {
-    _ = self;
-    return 120;
-}
-
-/// Writes to the file a buffer, prefixed and suffixed by the specified number of
-/// bytes of NOPs. Asserts each padding size is at least `min_nop_size` and total padding bytes
-/// are less than 126,976 bytes (if this limit is ever reached, this function can be
-/// improved to make more than one pwritev call, or the limit can be raised by a fixed
-/// amount by increasing the length of `vecs`).
-fn pwriteDbgLineNops(
-    self: *DebugSymbols,
-    prev_padding_size: usize,
-    buf: []const u8,
-    next_padding_size: usize,
-    offset: u64,
-) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const page_of_nops = [1]u8{DW.LNS.negate_stmt} ** 4096;
-    const three_byte_nop = [3]u8{ DW.LNS.advance_pc, 0b1000_0000, 0 };
-    var vecs: [32]std.os.iovec_const = undefined;
-    var vec_index: usize = 0;
-    {
-        var padding_left = prev_padding_size;
-        if (padding_left % 2 != 0) {
-            vecs[vec_index] = .{
-                .iov_base = &three_byte_nop,
-                .iov_len = three_byte_nop.len,
-            };
-            vec_index += 1;
-            padding_left -= three_byte_nop.len;
-        }
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    vecs[vec_index] = .{
-        .iov_base = buf.ptr,
-        .iov_len = buf.len,
-    };
-    vec_index += 1;
-
-    {
-        var padding_left = next_padding_size;
-        if (padding_left % 2 != 0) {
-            vecs[vec_index] = .{
-                .iov_base = &three_byte_nop,
-                .iov_len = three_byte_nop.len,
-            };
-            vec_index += 1;
-            padding_left -= three_byte_nop.len;
-        }
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-    try self.file.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
-}
-
-/// Writes to the file a buffer, prefixed and suffixed by the specified number of
-/// bytes of padding.
-fn pwriteDbgInfoNops(
-    self: *DebugSymbols,
-    prev_padding_size: usize,
-    buf: []const u8,
-    next_padding_size: usize,
-    trailing_zero: bool,
-    offset: u64,
-) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const page_of_nops = [1]u8{abbrev_pad1} ** 4096;
-    var vecs: [32]std.os.iovec_const = undefined;
-    var vec_index: usize = 0;
-    {
-        var padding_left = prev_padding_size;
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    vecs[vec_index] = .{
-        .iov_base = buf.ptr,
-        .iov_len = buf.len,
-    };
-    vec_index += 1;
-
-    {
-        var padding_left = next_padding_size;
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    if (trailing_zero) {
-        var zbuf = [1]u8{0};
-        vecs[vec_index] = .{
-            .iov_base = &zbuf,
-            .iov_len = zbuf.len,
-        };
-        vec_index += 1;
-    }
-
-    try self.file.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
+    const atom = &decl.link.macho;
+    return self.dwarf.commitDeclDebugInfo(&self.base.base, module, decl, symbol.n_value, atom.size, debug_buffers);
 }
src/link/Dwarf.zig
@@ -0,0 +1,1625 @@
+const Dwarf = @This();
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const fs = std.fs;
+const leb128 = std.leb;
+const log = std.log.scoped(.dwarf);
+const mem = std.mem;
+
+const link = @import("../link.zig");
+const trace = @import("../tracy.zig").trace;
+
+const Allocator = mem.Allocator;
+const DW = std.dwarf;
+const File = link.File;
+const LinkBlock = File.LinkBlock;
+const LinkFn = File.LinkFn;
+const Module = @import("../Module.zig");
+const Value = @import("../value.zig").Value;
+const Type = @import("../type.zig").Type;
+
+allocator: Allocator,
+tag: File.Tag,
+ptr_width: PtrWidth,
+target: std.Target,
+
+/// A list of `File.LinkFn` whose Line Number Programs have surplus capacity.
+/// This is the same concept as `text_block_free_list`; see those doc comments.
+dbg_line_fn_free_list: std.AutoHashMapUnmanaged(*SrcFn, void) = .{},
+dbg_line_fn_first: ?*SrcFn = null,
+dbg_line_fn_last: ?*SrcFn = null,
+
+/// A list of `TextBlock` whose corresponding .debug_info tags have surplus capacity. /// This is the same concept as `text_block_free_list`; see those doc comments.
+dbg_info_decl_free_list: std.AutoHashMapUnmanaged(*DebugInfoAtom, void) = .{},
+dbg_info_decl_first: ?*DebugInfoAtom = null,
+dbg_info_decl_last: ?*DebugInfoAtom = null,
+
+abbrev_table_offset: ?u64 = null,
+
+/// Table of debug symbol names.
+strtab: std.ArrayListUnmanaged(u8) = .{},
+
+pub const DebugInfoAtom = struct {
+    /// Previous/next linked list pointers.
+    /// This is the linked list node for this Decl's corresponding .debug_info tag.
+    prev: ?*DebugInfoAtom,
+    next: ?*DebugInfoAtom,
+    /// Offset into .debug_info pointing to the tag for this Decl.
+    off: u32,
+    /// Size of the .debug_info tag for this Decl, not including padding.
+    len: u32,
+};
+
+pub const SrcFn = struct {
+    /// Offset from the beginning of the Debug Line Program header that contains this function.
+    off: u32,
+    /// Size of the line number program component belonging to this function, not
+    /// including padding.
+    len: u32,
+
+    /// Points to the previous and next neighbors, based on the offset from .debug_line.
+    /// This can be used to find, for example, the capacity of this `SrcFn`.
+    prev: ?*SrcFn,
+    next: ?*SrcFn,
+
+    pub const empty: SrcFn = .{
+        .off = 0,
+        .len = 0,
+        .prev = null,
+        .next = null,
+    };
+};
+
+pub const PtrWidth = enum { p32, p64 };
+
+pub const abbrev_compile_unit = 1;
+pub const abbrev_subprogram = 2;
+pub const abbrev_subprogram_retvoid = 3;
+pub const abbrev_base_type = 4;
+pub const abbrev_ptr_type = 5;
+pub const abbrev_struct_type = 6;
+pub const abbrev_struct_member = 7;
+pub const abbrev_pad1 = 8;
+pub const abbrev_parameter = 9;
+
+/// The reloc offset for the virtual address of a function in its Line Number Program.
+/// Size is a virtual address integer.
+const dbg_line_vaddr_reloc_index = 3;
+/// The reloc offset for the virtual address of a function in its .debug_info TAG.subprogram.
+/// Size is a virtual address integer.
+const dbg_info_low_pc_reloc_index = 1;
+
+const min_nop_size = 2;
+
+/// When allocating, the ideal_capacity is calculated by
+/// actual_capacity + (actual_capacity / ideal_factor)
+const ideal_factor = 3;
+
+pub fn init(allocator: Allocator, tag: File.Tag, target: std.Target) Dwarf {
+    const ptr_width: PtrWidth = switch (target.cpu.arch.ptrBitWidth()) {
+        0...32 => .p32,
+        33...64 => .p64,
+        else => unreachable,
+    };
+    return Dwarf{
+        .allocator = allocator,
+        .tag = tag,
+        .ptr_width = ptr_width,
+        .target = target,
+    };
+}
+
+pub fn deinit(self: *Dwarf) void {
+    const gpa = self.allocator;
+    self.dbg_line_fn_free_list.deinit(gpa);
+    self.dbg_info_decl_free_list.deinit(gpa);
+    self.strtab.deinit(gpa);
+}
+
+pub const DeclDebugBuffers = struct {
+    dbg_line_buffer: std.ArrayList(u8),
+    dbg_info_buffer: std.ArrayList(u8),
+    dbg_info_type_relocs: File.DbgInfoTypeRelocsTable,
+};
+
+pub fn initDeclDebugInfo(self: *Dwarf, decl: *Module.Decl) !DeclDebugBuffers {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const decl_name = try decl.getFullyQualifiedName(self.allocator);
+    defer self.allocator.free(decl_name);
+
+    log.debug("initDeclDebugInfo {s}{*}", .{ decl_name, decl });
+
+    const gpa = self.allocator;
+    var dbg_line_buffer = std.ArrayList(u8).init(gpa);
+    var dbg_info_buffer = std.ArrayList(u8).init(gpa);
+    var dbg_info_type_relocs: File.DbgInfoTypeRelocsTable = .{};
+
+    assert(decl.has_tv);
+
+    switch (decl.ty.zigTypeTag()) {
+        .Fn => {
+            // For functions we need to add a prologue to the debug line program.
+            try dbg_line_buffer.ensureTotalCapacity(26);
+
+            const func = decl.val.castTag(.function).?.data;
+            log.debug("decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d}", .{
+                decl.src_line,
+                func.lbrace_line,
+                func.rbrace_line,
+            });
+            const line = @intCast(u28, decl.src_line + func.lbrace_line);
+
+            const ptr_width_bytes = self.ptrWidthBytes();
+            dbg_line_buffer.appendSliceAssumeCapacity(&[_]u8{
+                DW.LNS.extended_op,
+                ptr_width_bytes + 1,
+                DW.LNE.set_address,
+            });
+            // This is the "relocatable" vaddr, corresponding to `code_buffer` index `0`.
+            assert(dbg_line_vaddr_reloc_index == dbg_line_buffer.items.len);
+            dbg_line_buffer.items.len += ptr_width_bytes;
+
+            dbg_line_buffer.appendAssumeCapacity(DW.LNS.advance_line);
+            // This is the "relocatable" relative line offset from the previous function's end curly
+            // to this function's begin curly.
+            assert(self.getRelocDbgLineOff() == dbg_line_buffer.items.len);
+            // Here we use a ULEB128-fixed-4 to make sure this field can be overwritten later.
+            leb128.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), line);
+
+            dbg_line_buffer.appendAssumeCapacity(DW.LNS.set_file);
+            assert(self.getRelocDbgFileIndex() == dbg_line_buffer.items.len);
+            // Once we support more than one source file, this will have the ability to be more
+            // than one possible value.
+            const file_index = 1;
+            leb128.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), file_index);
+
+            // Emit a line for the begin curly with prologue_end=false. The codegen will
+            // do the work of setting prologue_end=true and epilogue_begin=true.
+            dbg_line_buffer.appendAssumeCapacity(DW.LNS.copy);
+
+            // .debug_info subprogram
+            const decl_name_with_null = decl_name[0 .. decl_name.len + 1];
+            try dbg_info_buffer.ensureUnusedCapacity(25 + decl_name_with_null.len);
+
+            const fn_ret_type = decl.ty.fnReturnType();
+            const fn_ret_has_bits = fn_ret_type.hasRuntimeBits();
+            if (fn_ret_has_bits) {
+                dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram);
+            } else {
+                dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram_retvoid);
+            }
+            // These get overwritten after generating the machine code. These values are
+            // "relocations" and have to be in this fixed place so that functions can be
+            // moved in virtual address space.
+            assert(dbg_info_low_pc_reloc_index == dbg_info_buffer.items.len);
+            dbg_info_buffer.items.len += ptr_width_bytes; // DW.AT.low_pc,  DW.FORM.addr
+            assert(self.getRelocDbgInfoSubprogramHighPC() == dbg_info_buffer.items.len);
+            dbg_info_buffer.items.len += 4; // DW.AT.high_pc,  DW.FORM.data4
+            if (fn_ret_has_bits) {
+                const gop = try dbg_info_type_relocs.getOrPut(gpa, fn_ret_type);
+                if (!gop.found_existing) {
+                    gop.value_ptr.* = .{
+                        .off = undefined,
+                        .relocs = .{},
+                    };
+                }
+                try gop.value_ptr.relocs.append(gpa, @intCast(u32, dbg_info_buffer.items.len));
+                dbg_info_buffer.items.len += 4; // DW.AT.type,  DW.FORM.ref4
+            }
+            dbg_info_buffer.appendSliceAssumeCapacity(decl_name_with_null); // DW.AT.name, DW.FORM.string
+
+        },
+        else => {
+            // TODO implement .debug_info for global variables
+        },
+    }
+
+    return DeclDebugBuffers{
+        .dbg_info_buffer = dbg_info_buffer,
+        .dbg_line_buffer = dbg_line_buffer,
+        .dbg_info_type_relocs = dbg_info_type_relocs,
+    };
+}
+
+pub fn commitDeclDebugInfo(
+    self: *Dwarf,
+    file: *File,
+    module: *Module,
+    decl: *Module.Decl,
+    sym_addr: u64,
+    sym_size: u64,
+    debug_buffers: *DeclDebugBuffers,
+) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const gpa = self.allocator;
+    var dbg_line_buffer = &debug_buffers.dbg_line_buffer;
+    var dbg_info_buffer = &debug_buffers.dbg_info_buffer;
+    var dbg_info_type_relocs = &debug_buffers.dbg_info_type_relocs;
+
+    const target_endian = self.target.cpu.arch.endian();
+
+    assert(decl.has_tv);
+    switch (decl.ty.zigTypeTag()) {
+        .Fn => {
+            // Since the Decl is a function, we need to update the .debug_line program.
+            // Perform the relocations based on vaddr.
+            switch (self.ptr_width) {
+                .p32 => {
+                    {
+                        const ptr = dbg_line_buffer.items[dbg_line_vaddr_reloc_index..][0..4];
+                        mem.writeInt(u32, ptr, @intCast(u32, sym_addr), target_endian);
+                    }
+                    {
+                        const ptr = dbg_info_buffer.items[dbg_info_low_pc_reloc_index..][0..4];
+                        mem.writeInt(u32, ptr, @intCast(u32, sym_addr), target_endian);
+                    }
+                },
+                .p64 => {
+                    {
+                        const ptr = dbg_line_buffer.items[dbg_line_vaddr_reloc_index..][0..8];
+                        mem.writeInt(u64, ptr, sym_addr, target_endian);
+                    }
+                    {
+                        const ptr = dbg_info_buffer.items[dbg_info_low_pc_reloc_index..][0..8];
+                        mem.writeInt(u64, ptr, sym_addr, target_endian);
+                    }
+                },
+            }
+            {
+                const ptr = dbg_info_buffer.items[self.getRelocDbgInfoSubprogramHighPC()..][0..4];
+                mem.writeInt(u32, ptr, @intCast(u32, sym_size), target_endian);
+            }
+
+            try dbg_line_buffer.appendSlice(&[_]u8{ DW.LNS.extended_op, 1, DW.LNE.end_sequence });
+
+            // Now we have the full contents and may allocate a region to store it.
+
+            // This logic is nearly identical to the logic below in `updateDeclDebugInfo` for
+            // `TextBlock` and the .debug_info. If you are editing this logic, you
+            // probably need to edit that logic too.
+            const src_fn = switch (self.tag) {
+                .elf => &decl.fn_link.elf,
+                .macho => &decl.fn_link.macho,
+                else => unreachable, // TODO
+            };
+            src_fn.len = @intCast(u32, dbg_line_buffer.items.len);
+
+            if (self.dbg_line_fn_last) |last| blk: {
+                if (src_fn == last) break :blk;
+                if (src_fn.next) |next| {
+                    // Update existing function - non-last item.
+                    if (src_fn.off + src_fn.len + min_nop_size > next.off) {
+                        // It grew too big, so we move it to a new location.
+                        if (src_fn.prev) |prev| {
+                            self.dbg_line_fn_free_list.put(gpa, prev, {}) catch {};
+                            prev.next = src_fn.next;
+                        }
+                        next.prev = src_fn.prev;
+                        src_fn.next = null;
+                        // Populate where it used to be with NOPs.
+                        switch (self.tag) {
+                            .elf => {
+                                const elf_file = file.cast(File.Elf).?;
+                                const debug_line_sect = &elf_file.sections.items[elf_file.debug_line_section_index.?];
+                                const file_pos = debug_line_sect.sh_offset + src_fn.off;
+                                try pwriteDbgLineNops(elf_file.base.file.?, file_pos, 0, &[0]u8{}, src_fn.len);
+                            },
+                            .macho => {
+                                const macho_file = file.cast(File.MachO).?;
+                                const d_sym = &macho_file.d_sym.?;
+                                const dwarf_segment = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+                                const debug_line_sect = &dwarf_segment.sections.items[d_sym.debug_line_section_index.?];
+                                const file_pos = debug_line_sect.offset + src_fn.off;
+                                try pwriteDbgLineNops(d_sym.file, file_pos, 0, &[0]u8{}, src_fn.len);
+                            },
+                            else => unreachable,
+                        }
+                        // TODO Look at the free list before appending at the end.
+                        src_fn.prev = last;
+                        last.next = src_fn;
+                        self.dbg_line_fn_last = src_fn;
+
+                        src_fn.off = last.off + padToIdeal(last.len);
+                    }
+                } else if (src_fn.prev == null) {
+                    // Append new function.
+                    // TODO Look at the free list before appending at the end.
+                    src_fn.prev = last;
+                    last.next = src_fn;
+                    self.dbg_line_fn_last = src_fn;
+
+                    src_fn.off = last.off + padToIdeal(last.len);
+                }
+            } else {
+                // This is the first function of the Line Number Program.
+                self.dbg_line_fn_first = src_fn;
+                self.dbg_line_fn_last = src_fn;
+
+                src_fn.off = padToIdeal(self.dbgLineNeededHeaderBytes(module));
+            }
+
+            const last_src_fn = self.dbg_line_fn_last.?;
+            const needed_size = last_src_fn.off + last_src_fn.len;
+            const prev_padding_size: u32 = if (src_fn.prev) |prev| src_fn.off - (prev.off + prev.len) else 0;
+            const next_padding_size: u32 = if (src_fn.next) |next| next.off - (src_fn.off + src_fn.len) else 0;
+
+            // We only have support for one compilation unit so far, so the offsets are directly
+            // from the .debug_line section.
+            switch (self.tag) {
+                .elf => {
+                    const elf_file = file.cast(File.Elf).?;
+                    const debug_line_sect = &elf_file.sections.items[elf_file.debug_line_section_index.?];
+                    if (needed_size != debug_line_sect.sh_size) {
+                        if (needed_size > elf_file.allocatedSize(debug_line_sect.sh_offset)) {
+                            const new_offset = elf_file.findFreeSpace(needed_size, 1);
+                            const existing_size = last_src_fn.off;
+                            log.debug("moving .debug_line section: {d} bytes from 0x{x} to 0x{x}", .{
+                                existing_size,
+                                debug_line_sect.sh_offset,
+                                new_offset,
+                            });
+                            const amt = try elf_file.base.file.?.copyRangeAll(
+                                debug_line_sect.sh_offset,
+                                elf_file.base.file.?,
+                                new_offset,
+                                existing_size,
+                            );
+                            if (amt != existing_size) return error.InputOutput;
+                            debug_line_sect.sh_offset = new_offset;
+                        }
+                        debug_line_sect.sh_size = needed_size;
+                        elf_file.shdr_table_dirty = true; // TODO look into making only the one section dirty
+                        elf_file.debug_line_header_dirty = true;
+                    }
+                    const file_pos = debug_line_sect.sh_offset + src_fn.off;
+                    try pwriteDbgLineNops(
+                        elf_file.base.file.?,
+                        file_pos,
+                        prev_padding_size,
+                        dbg_line_buffer.items,
+                        next_padding_size,
+                    );
+                },
+                .macho => {
+                    const macho_file = file.cast(File.MachO).?;
+                    const d_sym = &macho_file.d_sym.?;
+                    const dwarf_segment = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+                    const debug_line_sect = &dwarf_segment.sections.items[d_sym.debug_line_section_index.?];
+                    if (needed_size != debug_line_sect.size) {
+                        if (needed_size > d_sym.allocatedSize(debug_line_sect.offset)) {
+                            const new_offset = d_sym.findFreeSpace(needed_size, 1);
+                            const existing_size = last_src_fn.off;
+
+                            log.debug("moving __debug_line section: {} bytes from 0x{x} to 0x{x}", .{
+                                existing_size,
+                                debug_line_sect.offset,
+                                new_offset,
+                            });
+
+                            try File.MachO.copyRangeAllOverlappingAlloc(
+                                gpa,
+                                d_sym.file,
+                                debug_line_sect.offset,
+                                new_offset,
+                                existing_size,
+                            );
+
+                            debug_line_sect.offset = @intCast(u32, new_offset);
+                            debug_line_sect.addr = dwarf_segment.inner.vmaddr + new_offset - dwarf_segment.inner.fileoff;
+                        }
+                        debug_line_sect.size = needed_size;
+                        d_sym.load_commands_dirty = true; // TODO look into making only the one section dirty
+                        d_sym.debug_line_header_dirty = true;
+                    }
+                    const file_pos = debug_line_sect.offset + src_fn.off;
+                    try pwriteDbgLineNops(
+                        d_sym.file,
+                        file_pos,
+                        prev_padding_size,
+                        dbg_line_buffer.items,
+                        next_padding_size,
+                    );
+                },
+                else => unreachable,
+            }
+
+            // .debug_info - End the TAG.subprogram children.
+            try dbg_info_buffer.append(0);
+        },
+        else => {},
+    }
+
+    if (dbg_info_buffer.items.len == 0)
+        return;
+
+    // We need this for the duration of this function only so that for composite
+    // types such as []const u32, if the type *u32 is non-existent, we create
+    // it synthetically and store the backing bytes in this arena. After we are
+    // done with the relocations, we can safely deinit the entire memory slab.
+    // TODO currently, we do not store the relocations for future use, however,
+    // if that is the case, we should move memory management to a higher scope,
+    // such as linker scope, or whatnot.
+    var dbg_type_arena = std.heap.ArenaAllocator.init(gpa);
+    defer dbg_type_arena.deinit();
+
+    {
+        // Now we emit the .debug_info types of the Decl. These will count towards the size of
+        // the buffer, so we have to do it before computing the offset, and we can't perform the actual
+        // relocations yet.
+        var it: usize = 0;
+        while (it < dbg_info_type_relocs.count()) : (it += 1) {
+            const ty = dbg_info_type_relocs.keys()[it];
+            const value_ptr = dbg_info_type_relocs.getPtr(ty).?;
+            value_ptr.off = @intCast(u32, dbg_info_buffer.items.len);
+            try self.addDbgInfoType(dbg_type_arena.allocator(), ty, dbg_info_buffer, dbg_info_type_relocs);
+        }
+    }
+
+    const atom = switch (self.tag) {
+        .elf => &decl.link.elf.dbg_info_atom,
+        .macho => &decl.link.macho.dbg_info_atom,
+        else => unreachable,
+    };
+    try self.updateDeclDebugInfoAllocation(file, atom, @intCast(u32, dbg_info_buffer.items.len));
+
+    {
+        // Now that we have the offset assigned we can finally perform type relocations.
+        for (dbg_info_type_relocs.values()) |value| {
+            for (value.relocs.items) |off| {
+                mem.writeIntLittle(
+                    u32,
+                    dbg_info_buffer.items[off..][0..4],
+                    atom.off + value.off,
+                );
+            }
+        }
+    }
+
+    try self.writeDeclDebugInfo(file, atom, dbg_info_buffer.items);
+}
+
+fn updateDeclDebugInfoAllocation(self: *Dwarf, file: *File, atom: *DebugInfoAtom, len: u32) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    // This logic is nearly identical to the logic above in `updateDecl` for
+    // `SrcFn` and the line number programs. If you are editing this logic, you
+    // probably need to edit that logic too.
+    const gpa = self.allocator;
+
+    atom.len = len;
+    if (self.dbg_info_decl_last) |last| blk: {
+        if (atom == last) break :blk;
+        if (atom.next) |next| {
+            // Update existing Decl - non-last item.
+            if (atom.off + atom.len + min_nop_size > next.off) {
+                // It grew too big, so we move it to a new location.
+                if (atom.prev) |prev| {
+                    self.dbg_info_decl_free_list.put(gpa, prev, {}) catch {};
+                    prev.next = atom.next;
+                }
+                next.prev = atom.prev;
+                atom.next = null;
+                // Populate where it used to be with NOPs.
+                switch (self.tag) {
+                    .elf => {
+                        const elf_file = file.cast(File.Elf).?;
+                        const debug_info_sect = &elf_file.sections.items[elf_file.debug_info_section_index.?];
+                        const file_pos = debug_info_sect.sh_offset + atom.off;
+                        try pwriteDbgInfoNops(elf_file.base.file.?, file_pos, 0, &[0]u8{}, atom.len, false);
+                    },
+                    .macho => {
+                        const macho_file = file.cast(File.MachO).?;
+                        const d_sym = &macho_file.d_sym.?;
+                        const dwarf_segment = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+                        const debug_info_sect = &dwarf_segment.sections.items[d_sym.debug_info_section_index.?];
+                        const file_pos = debug_info_sect.offset + atom.off;
+                        try pwriteDbgInfoNops(d_sym.file, file_pos, 0, &[0]u8{}, atom.len, false);
+                    },
+                    else => unreachable,
+                }
+                // TODO Look at the free list before appending at the end.
+                atom.prev = last;
+                last.next = atom;
+                self.dbg_info_decl_last = atom;
+
+                atom.off = last.off + padToIdeal(last.len);
+            }
+        } else if (atom.prev == null) {
+            // Append new Decl.
+            // TODO Look at the free list before appending at the end.
+            atom.prev = last;
+            last.next = atom;
+            self.dbg_info_decl_last = atom;
+
+            atom.off = last.off + padToIdeal(last.len);
+        }
+    } else {
+        // This is the first Decl of the .debug_info
+        self.dbg_info_decl_first = atom;
+        self.dbg_info_decl_last = atom;
+
+        atom.off = @intCast(u32, padToIdeal(self.dbgInfoHeaderBytes()));
+    }
+}
+
+fn writeDeclDebugInfo(self: *Dwarf, file: *File, atom: *DebugInfoAtom, dbg_info_buf: []const u8) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    // This logic is nearly identical to the logic above in `updateDecl` for
+    // `SrcFn` and the line number programs. If you are editing this logic, you
+    // probably need to edit that logic too.
+    const gpa = self.allocator;
+
+    const last_decl = self.dbg_info_decl_last.?;
+    // +1 for a trailing zero to end the children of the decl tag.
+    const needed_size = last_decl.off + last_decl.len + 1;
+    const prev_padding_size: u32 = if (atom.prev) |prev| atom.off - (prev.off + prev.len) else 0;
+    const next_padding_size: u32 = if (atom.next) |next| next.off - (atom.off + atom.len) else 0;
+
+    // To end the children of the decl tag.
+    const trailing_zero = atom.next == null;
+
+    // We only have support for one compilation unit so far, so the offsets are directly
+    // from the .debug_info section.
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const debug_info_sect = &elf_file.sections.items[elf_file.debug_info_section_index.?];
+            if (needed_size != debug_info_sect.sh_size) {
+                if (needed_size > elf_file.allocatedSize(debug_info_sect.sh_offset)) {
+                    const new_offset = elf_file.findFreeSpace(needed_size, 1);
+                    const existing_size = last_decl.off;
+                    log.debug("moving .debug_info section: {d} bytes from 0x{x} to 0x{x}", .{
+                        existing_size,
+                        debug_info_sect.sh_offset,
+                        new_offset,
+                    });
+                    const amt = try elf_file.base.file.?.copyRangeAll(
+                        debug_info_sect.sh_offset,
+                        elf_file.base.file.?,
+                        new_offset,
+                        existing_size,
+                    );
+                    if (amt != existing_size) return error.InputOutput;
+                    debug_info_sect.sh_offset = new_offset;
+                }
+                debug_info_sect.sh_size = needed_size;
+                elf_file.shdr_table_dirty = true; // TODO look into making only the one section dirty
+                elf_file.debug_info_header_dirty = true;
+            }
+            const file_pos = debug_info_sect.sh_offset + atom.off;
+            try pwriteDbgInfoNops(
+                elf_file.base.file.?,
+                file_pos,
+                prev_padding_size,
+                dbg_info_buf,
+                next_padding_size,
+                trailing_zero,
+            );
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = &macho_file.d_sym.?;
+            const dwarf_segment = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const debug_info_sect = &dwarf_segment.sections.items[d_sym.debug_info_section_index.?];
+            if (needed_size != debug_info_sect.size) {
+                if (needed_size > d_sym.allocatedSize(debug_info_sect.offset)) {
+                    const new_offset = d_sym.findFreeSpace(needed_size, 1);
+                    const existing_size = last_decl.off;
+
+                    log.debug("moving __debug_info section: {} bytes from 0x{x} to 0x{x}", .{
+                        existing_size,
+                        debug_info_sect.offset,
+                        new_offset,
+                    });
+
+                    try File.MachO.copyRangeAllOverlappingAlloc(
+                        gpa,
+                        d_sym.file,
+                        debug_info_sect.offset,
+                        new_offset,
+                        existing_size,
+                    );
+
+                    debug_info_sect.offset = @intCast(u32, new_offset);
+                    debug_info_sect.addr = dwarf_segment.inner.vmaddr + new_offset - dwarf_segment.inner.fileoff;
+                }
+                debug_info_sect.size = needed_size;
+                d_sym.load_commands_dirty = true; // TODO look into making only the one section dirty
+                d_sym.debug_line_header_dirty = true;
+            }
+            const file_pos = debug_info_sect.offset + atom.off;
+            try pwriteDbgInfoNops(
+                d_sym.file,
+                file_pos,
+                prev_padding_size,
+                dbg_info_buf,
+                next_padding_size,
+                trailing_zero,
+            );
+        },
+        else => unreachable,
+    }
+}
+
+pub fn updateDeclLineNumber(self: *Dwarf, file: *File, decl: *const Module.Decl) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const func = decl.val.castTag(.function).?.data;
+    log.debug("decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d}", .{
+        decl.src_line,
+        func.lbrace_line,
+        func.rbrace_line,
+    });
+    const line = @intCast(u28, decl.src_line + func.lbrace_line);
+    var data: [4]u8 = undefined;
+    leb128.writeUnsignedFixed(4, &data, line);
+
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const shdr = elf_file.sections.items[elf_file.debug_line_section_index.?];
+            const file_pos = shdr.sh_offset + decl.fn_link.elf.off + self.getRelocDbgLineOff();
+            try elf_file.base.file.?.pwriteAll(&data, file_pos);
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = macho_file.d_sym.?;
+            const dwarf_seg = d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const sect = dwarf_seg.sections.items[d_sym.debug_line_section_index.?];
+            const file_pos = sect.offset + decl.fn_link.macho.off + self.getRelocDbgLineOff();
+            try d_sym.file.pwriteAll(&data, file_pos);
+        },
+        else => unreachable,
+    }
+}
+
+pub fn freeAtom(self: *Dwarf, atom: *DebugInfoAtom) void {
+    if (self.dbg_info_decl_first == atom) {
+        self.dbg_info_decl_first = atom.next;
+    }
+    if (self.dbg_info_decl_last == atom) {
+        // TODO shrink the .debug_info section size here
+        self.dbg_info_decl_last = atom.prev;
+    }
+
+    if (atom.prev) |prev| {
+        prev.next = atom.next;
+
+        // TODO the free list logic like we do for text blocks above
+    } else {
+        atom.prev = null;
+    }
+
+    if (atom.next) |next| {
+        next.prev = atom.prev;
+    } else {
+        atom.next = null;
+    }
+}
+
+pub fn freeDecl(self: *Dwarf, decl: *Module.Decl) void {
+    // TODO make this logic match freeTextBlock. Maybe abstract the logic out since the same thing
+    // is desired for both.
+    const gpa = self.allocator;
+    const fn_link = switch (self.tag) {
+        .elf => &decl.fn_link.elf,
+        .macho => &decl.fn_link.macho,
+        else => unreachable,
+    };
+    _ = self.dbg_line_fn_free_list.remove(fn_link);
+
+    if (fn_link.prev) |prev| {
+        self.dbg_line_fn_free_list.put(gpa, prev, {}) catch {};
+        prev.next = fn_link.next;
+        if (fn_link.next) |next| {
+            next.prev = prev;
+        } else {
+            self.dbg_line_fn_last = prev;
+        }
+    } else if (fn_link.next) |next| {
+        self.dbg_line_fn_first = next;
+        next.prev = null;
+    }
+    if (self.dbg_line_fn_first == fn_link) {
+        self.dbg_line_fn_first = fn_link.next;
+    }
+    if (self.dbg_line_fn_last == fn_link) {
+        self.dbg_line_fn_last = fn_link.prev;
+    }
+}
+
+/// Asserts the type has codegen bits.
+fn addDbgInfoType(
+    self: *Dwarf,
+    arena: Allocator,
+    ty: Type,
+    dbg_info_buffer: *std.ArrayList(u8),
+    dbg_info_type_relocs: *File.DbgInfoTypeRelocsTable,
+) error{OutOfMemory}!void {
+    const target = self.target;
+    var relocs = std.ArrayList(struct { ty: Type, reloc: u32 }).init(arena);
+
+    switch (ty.zigTypeTag()) {
+        .NoReturn => unreachable,
+        .Void => {
+            try dbg_info_buffer.append(abbrev_pad1);
+        },
+        .Bool => {
+            try dbg_info_buffer.appendSlice(&[_]u8{
+                abbrev_base_type,
+                DW.ATE.boolean, // DW.AT.encoding ,  DW.FORM.data1
+                1, // DW.AT.byte_size,  DW.FORM.data1
+                'b', 'o', 'o', 'l', 0, // DW.AT.name,  DW.FORM.string
+            });
+        },
+        .Int => {
+            const info = ty.intInfo(target);
+            try dbg_info_buffer.ensureUnusedCapacity(12);
+            dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
+            // DW.AT.encoding, DW.FORM.data1
+            dbg_info_buffer.appendAssumeCapacity(switch (info.signedness) {
+                .signed => DW.ATE.signed,
+                .unsigned => DW.ATE.unsigned,
+            });
+            // DW.AT.byte_size,  DW.FORM.data1
+            dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
+            // DW.AT.name,  DW.FORM.string
+            try dbg_info_buffer.writer().print("{}\x00", .{ty});
+        },
+        .Optional => {
+            if (ty.isPtrLikeOptional()) {
+                try dbg_info_buffer.ensureUnusedCapacity(12);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
+                // DW.AT.encoding, DW.FORM.data1
+                dbg_info_buffer.appendAssumeCapacity(DW.ATE.address);
+                // DW.AT.byte_size,  DW.FORM.data1
+                dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
+                // DW.AT.name,  DW.FORM.string
+                try dbg_info_buffer.writer().print("{}\x00", .{ty});
+            } else {
+                // Non-pointer optionals are structs: struct { .maybe = *, .val = * }
+                var buf = try arena.create(Type.Payload.ElemType);
+                const payload_ty = ty.optionalChild(buf);
+                // DW.AT.structure_type
+                try dbg_info_buffer.append(abbrev_struct_type);
+                // DW.AT.byte_size, DW.FORM.sdata
+                const abi_size = ty.abiSize(target);
+                try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
+                // DW.AT.name, DW.FORM.string
+                try dbg_info_buffer.writer().print("{}\x00", .{ty});
+                // DW.AT.member
+                try dbg_info_buffer.ensureUnusedCapacity(7);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
+                // DW.AT.name, DW.FORM.string
+                dbg_info_buffer.appendSliceAssumeCapacity("maybe");
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.type, DW.FORM.ref4
+                var index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                try relocs.append(.{ .ty = Type.bool, .reloc = @intCast(u32, index) });
+                // DW.AT.data_member_location, DW.FORM.sdata
+                try dbg_info_buffer.ensureUnusedCapacity(6);
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.member
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
+                // DW.AT.name, DW.FORM.string
+                dbg_info_buffer.appendSliceAssumeCapacity("val");
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.type, DW.FORM.ref4
+                index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                try relocs.append(.{ .ty = payload_ty, .reloc = @intCast(u32, index) });
+                // DW.AT.data_member_location, DW.FORM.sdata
+                const offset = abi_size - payload_ty.abiSize(target);
+                try leb128.writeULEB128(dbg_info_buffer.writer(), offset);
+                // DW.AT.structure_type delimit children
+                try dbg_info_buffer.append(0);
+            }
+        },
+        .Pointer => {
+            if (ty.isSlice()) {
+                // Slices are structs: struct { .ptr = *, .len = N }
+                // DW.AT.structure_type
+                try dbg_info_buffer.ensureUnusedCapacity(2);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_type);
+                // DW.AT.byte_size, DW.FORM.sdata
+                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize) * 2);
+                // DW.AT.name, DW.FORM.string
+                try dbg_info_buffer.writer().print("{}\x00", .{ty});
+                // DW.AT.member
+                try dbg_info_buffer.ensureUnusedCapacity(5);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
+                // DW.AT.name, DW.FORM.string
+                dbg_info_buffer.appendSliceAssumeCapacity("ptr");
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.type, DW.FORM.ref4
+                var index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                var buf = try arena.create(Type.SlicePtrFieldTypeBuffer);
+                const ptr_ty = ty.slicePtrFieldType(buf);
+                try relocs.append(.{ .ty = ptr_ty, .reloc = @intCast(u32, index) });
+                // DW.AT.data_member_location, DW.FORM.sdata
+                try dbg_info_buffer.ensureUnusedCapacity(6);
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.member
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
+                // DW.AT.name, DW.FORM.string
+                dbg_info_buffer.appendSliceAssumeCapacity("len");
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.type, DW.FORM.ref4
+                index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                try relocs.append(.{ .ty = Type.initTag(.usize), .reloc = @intCast(u32, index) });
+                // DW.AT.data_member_location, DW.FORM.sdata
+                try dbg_info_buffer.ensureUnusedCapacity(2);
+                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize));
+                // DW.AT.structure_type delimit children
+                dbg_info_buffer.appendAssumeCapacity(0);
+            } else {
+                try dbg_info_buffer.ensureUnusedCapacity(5);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_ptr_type);
+                // DW.AT.type, DW.FORM.ref4
+                const index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                try relocs.append(.{ .ty = ty.childType(), .reloc = @intCast(u32, index) });
+            }
+        },
+        .Struct => blk: {
+            // try dbg_info_buffer.ensureUnusedCapacity(23);
+            // DW.AT.structure_type
+            try dbg_info_buffer.append(abbrev_struct_type);
+            // DW.AT.byte_size, DW.FORM.sdata
+            const abi_size = ty.abiSize(target);
+            try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
+            // DW.AT.name, DW.FORM.string
+            const struct_name = try ty.nameAlloc(arena);
+            try dbg_info_buffer.ensureUnusedCapacity(struct_name.len + 1);
+            dbg_info_buffer.appendSliceAssumeCapacity(struct_name);
+            dbg_info_buffer.appendAssumeCapacity(0);
+
+            const struct_obj = ty.castTag(.@"struct").?.data;
+            if (struct_obj.layout == .Packed) {
+                log.debug("TODO implement .debug_info for packed structs", .{});
+                break :blk;
+            }
+
+            const fields = ty.structFields();
+            for (fields.keys()) |field_name, field_index| {
+                const field = fields.get(field_name).?;
+                // DW.AT.member
+                try dbg_info_buffer.ensureUnusedCapacity(field_name.len + 2);
+                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
+                // DW.AT.name, DW.FORM.string
+                dbg_info_buffer.appendSliceAssumeCapacity(field_name);
+                dbg_info_buffer.appendAssumeCapacity(0);
+                // DW.AT.type, DW.FORM.ref4
+                var index = dbg_info_buffer.items.len;
+                try dbg_info_buffer.resize(index + 4);
+                try relocs.append(.{ .ty = field.ty, .reloc = @intCast(u32, index) });
+                // DW.AT.data_member_location, DW.FORM.sdata
+                const field_off = ty.structFieldOffset(field_index, target);
+                try leb128.writeULEB128(dbg_info_buffer.writer(), field_off);
+            }
+
+            // DW.AT.structure_type delimit children
+            try dbg_info_buffer.append(0);
+        },
+        else => {
+            log.debug("TODO implement .debug_info for type '{}'", .{ty});
+            try dbg_info_buffer.append(abbrev_pad1);
+        },
+    }
+
+    for (relocs.items) |rel| {
+        const gop = try dbg_info_type_relocs.getOrPut(self.allocator, rel.ty);
+        if (!gop.found_existing) {
+            gop.value_ptr.* = .{
+                .off = undefined,
+                .relocs = .{},
+            };
+        }
+        try gop.value_ptr.relocs.append(self.allocator, rel.reloc);
+    }
+}
+
+pub fn writeDbgAbbrev(self: *Dwarf, file: *File) !void {
+    // These are LEB encoded but since the values are all less than 127
+    // we can simply append these bytes.
+    const abbrev_buf = [_]u8{
+        abbrev_compile_unit, DW.TAG.compile_unit, DW.CHILDREN.yes, // header
+        DW.AT.stmt_list,     DW.FORM.sec_offset,  DW.AT.low_pc,
+        DW.FORM.addr,        DW.AT.high_pc,       DW.FORM.addr,
+        DW.AT.name,          DW.FORM.strp,        DW.AT.comp_dir,
+        DW.FORM.strp,        DW.AT.producer,      DW.FORM.strp,
+        DW.AT.language,      DW.FORM.data2,       0,
+        0, // table sentinel
+        abbrev_subprogram,
+        DW.TAG.subprogram,
+        DW.CHILDREN.yes, // header
+        DW.AT.low_pc,
+        DW.FORM.addr,
+        DW.AT.high_pc,
+        DW.FORM.data4,
+        DW.AT.type,
+        DW.FORM.ref4,
+        DW.AT.name,
+        DW.FORM.string,
+        0,                         0, // table sentinel
+        abbrev_subprogram_retvoid,
+        DW.TAG.subprogram, DW.CHILDREN.yes, // header
+        DW.AT.low_pc,      DW.FORM.addr,
+        DW.AT.high_pc,     DW.FORM.data4,
+        DW.AT.name,        DW.FORM.string,
+        0,
+        0, // table sentinel
+        abbrev_base_type,
+        DW.TAG.base_type,
+        DW.CHILDREN.no, // header
+        DW.AT.encoding,
+        DW.FORM.data1,
+        DW.AT.byte_size,
+        DW.FORM.data1,
+        DW.AT.name,
+        DW.FORM.string,
+        0,
+        0, // table sentinel
+        abbrev_ptr_type,
+        DW.TAG.pointer_type,
+        DW.CHILDREN.no, // header
+        DW.AT.type,
+        DW.FORM.ref4,
+        0,
+        0, // table sentinel
+        abbrev_struct_type,
+        DW.TAG.structure_type,
+        DW.CHILDREN.yes, // header
+        DW.AT.byte_size,
+        DW.FORM.sdata,
+        DW.AT.name,
+        DW.FORM.string,
+        0,
+        0, // table sentinel
+        abbrev_struct_member,
+        DW.TAG.member,
+        DW.CHILDREN.no, // header
+        DW.AT.name,
+        DW.FORM.string,
+        DW.AT.type,
+        DW.FORM.ref4,
+        DW.AT.data_member_location,
+        DW.FORM.sdata,
+        0,
+        0, // table sentinel
+        abbrev_pad1,
+        DW.TAG.unspecified_type,
+        DW.CHILDREN.no, // header
+        0,
+        0, // table sentinel
+        abbrev_parameter,
+        DW.TAG.formal_parameter, DW.CHILDREN.no, // header
+        DW.AT.location,          DW.FORM.exprloc,
+        DW.AT.type,              DW.FORM.ref4,
+        DW.AT.name,              DW.FORM.string,
+        0,
+        0, // table sentinel
+        0,
+        0,
+        0, // section sentinel
+    };
+    const abbrev_offset = 0;
+    self.abbrev_table_offset = abbrev_offset;
+
+    const needed_size = abbrev_buf.len;
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const debug_abbrev_sect = &elf_file.sections.items[elf_file.debug_abbrev_section_index.?];
+            const allocated_size = elf_file.allocatedSize(debug_abbrev_sect.sh_offset);
+            if (needed_size > allocated_size) {
+                debug_abbrev_sect.sh_size = 0; // free the space
+                debug_abbrev_sect.sh_offset = elf_file.findFreeSpace(needed_size, 1);
+            }
+            debug_abbrev_sect.sh_size = needed_size;
+            log.debug(".debug_abbrev start=0x{x} end=0x{x}", .{
+                debug_abbrev_sect.sh_offset,
+                debug_abbrev_sect.sh_offset + needed_size,
+            });
+
+            const file_pos = debug_abbrev_sect.sh_offset + abbrev_offset;
+            try elf_file.base.file.?.pwriteAll(&abbrev_buf, file_pos);
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = &macho_file.d_sym.?;
+            const dwarf_segment = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const debug_abbrev_sect = &dwarf_segment.sections.items[d_sym.debug_abbrev_section_index.?];
+            const allocated_size = d_sym.allocatedSize(debug_abbrev_sect.offset);
+            if (needed_size > allocated_size) {
+                debug_abbrev_sect.size = 0; // free the space
+                const offset = d_sym.findFreeSpace(needed_size, 1);
+                debug_abbrev_sect.offset = @intCast(u32, offset);
+                debug_abbrev_sect.addr = dwarf_segment.inner.vmaddr + offset - dwarf_segment.inner.fileoff;
+            }
+            debug_abbrev_sect.size = needed_size;
+            log.debug("__debug_abbrev start=0x{x} end=0x{x}", .{
+                debug_abbrev_sect.offset,
+                debug_abbrev_sect.offset + needed_size,
+            });
+            const file_pos = debug_abbrev_sect.offset + abbrev_offset;
+            try d_sym.file.pwriteAll(&abbrev_buf, file_pos);
+        },
+        else => unreachable,
+    }
+}
+
+fn dbgInfoHeaderBytes(self: *Dwarf) usize {
+    _ = self;
+    return 120;
+}
+
+pub fn writeDbgInfoHeader(self: *Dwarf, file: *File, module: *Module, low_pc: u64, high_pc: u64) !void {
+    // If this value is null it means there is an error in the module;
+    // leave debug_info_header_dirty=true.
+    const first_dbg_info_off = self.getDebugInfoOff() orelse return;
+
+    // We have a function to compute the upper bound size, because it's needed
+    // for determining where to put the offset of the first `LinkBlock`.
+    const needed_bytes = self.dbgInfoHeaderBytes();
+    var di_buf = try std.ArrayList(u8).initCapacity(self.allocator, needed_bytes);
+    defer di_buf.deinit();
+
+    const target_endian = self.target.cpu.arch.endian();
+    const init_len_size: usize = if (self.tag == .macho)
+        4
+    else switch (self.ptr_width) {
+        .p32 => @as(usize, 4),
+        .p64 => 12,
+    };
+
+    // initial length - length of the .debug_info contribution for this compilation unit,
+    // not including the initial length itself.
+    // We have to come back and write it later after we know the size.
+    const after_init_len = di_buf.items.len + init_len_size;
+    // +1 for the final 0 that ends the compilation unit children.
+    const dbg_info_end = self.getDebugInfoEnd().? + 1;
+    const init_len = dbg_info_end - after_init_len;
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len));
+    } else switch (self.ptr_width) {
+        .p32 => {
+            mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len), target_endian);
+        },
+        .p64 => {
+            di_buf.appendNTimesAssumeCapacity(0xff, 4);
+            mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), init_len, target_endian);
+        },
+    }
+    mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4, target_endian); // DWARF version
+    const abbrev_offset = self.abbrev_table_offset.?;
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, abbrev_offset));
+        di_buf.appendAssumeCapacity(8); // address size
+    } else switch (self.ptr_width) {
+        .p32 => {
+            mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, abbrev_offset), target_endian);
+            di_buf.appendAssumeCapacity(4); // address size
+        },
+        .p64 => {
+            mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), abbrev_offset, target_endian);
+            di_buf.appendAssumeCapacity(8); // address size
+        },
+    }
+    // Write the form for the compile unit, which must match the abbrev table above.
+    const name_strp = try self.makeString(module.root_pkg.root_src_path);
+    const comp_dir_strp = try self.makeString(module.root_pkg.root_src_directory.path orelse ".");
+    const producer_strp = try self.makeString(link.producer_string);
+
+    di_buf.appendAssumeCapacity(abbrev_compile_unit);
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), 0); // DW.AT.stmt_list, DW.FORM.sec_offset
+        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), low_pc);
+        mem.writeIntLittle(u64, di_buf.addManyAsArrayAssumeCapacity(8), high_pc);
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, name_strp));
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, comp_dir_strp));
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, producer_strp));
+    } else {
+        self.writeAddrAssumeCapacity(&di_buf, 0); // DW.AT.stmt_list, DW.FORM.sec_offset
+        self.writeAddrAssumeCapacity(&di_buf, low_pc);
+        self.writeAddrAssumeCapacity(&di_buf, high_pc);
+        self.writeAddrAssumeCapacity(&di_buf, name_strp);
+        self.writeAddrAssumeCapacity(&di_buf, comp_dir_strp);
+        self.writeAddrAssumeCapacity(&di_buf, producer_strp);
+    }
+    // We are still waiting on dwarf-std.org to assign DW_LANG_Zig a number:
+    // http://dwarfstd.org/ShowIssue.php?issue=171115.1
+    // Until then we say it is C99.
+    mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), DW.LANG.C99, target_endian);
+
+    if (di_buf.items.len > first_dbg_info_off) {
+        // Move the first N decls to the end to make more padding for the header.
+        @panic("TODO: handle .debug_info header exceeding its padding");
+    }
+    const jmp_amt = first_dbg_info_off - di_buf.items.len;
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const debug_info_sect = elf_file.sections.items[elf_file.debug_info_section_index.?];
+            const file_pos = debug_info_sect.sh_offset;
+            try pwriteDbgInfoNops(elf_file.base.file.?, file_pos, 0, di_buf.items, jmp_amt, false);
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = &macho_file.d_sym.?;
+            const dwarf_seg = d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const debug_info_sect = dwarf_seg.sections.items[d_sym.debug_info_section_index.?];
+            const file_pos = debug_info_sect.offset;
+            try pwriteDbgInfoNops(d_sym.file, file_pos, 0, di_buf.items, jmp_amt, false);
+        },
+        else => unreachable,
+    }
+}
+
+fn writeAddrAssumeCapacity(self: *Dwarf, buf: *std.ArrayList(u8), addr: u64) void {
+    const target_endian = self.target.cpu.arch.endian();
+    switch (self.ptr_width) {
+        .p32 => mem.writeInt(u32, buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, addr), target_endian),
+        .p64 => mem.writeInt(u64, buf.addManyAsArrayAssumeCapacity(8), addr, target_endian),
+    }
+}
+
+/// Writes to the file a buffer, prefixed and suffixed by the specified number of
+/// bytes of NOPs. Asserts each padding size is at least `min_nop_size` and total padding bytes
+/// are less than 1044480 bytes (if this limit is ever reached, this function can be
+/// improved to make more than one pwritev call, or the limit can be raised by a fixed
+/// amount by increasing the length of `vecs`).
+fn pwriteDbgLineNops(
+    file: fs.File,
+    offset: u64,
+    prev_padding_size: usize,
+    buf: []const u8,
+    next_padding_size: usize,
+) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const page_of_nops = [1]u8{DW.LNS.negate_stmt} ** 4096;
+    const three_byte_nop = [3]u8{ DW.LNS.advance_pc, 0b1000_0000, 0 };
+    var vecs: [512]std.os.iovec_const = undefined;
+    var vec_index: usize = 0;
+    {
+        var padding_left = prev_padding_size;
+        if (padding_left % 2 != 0) {
+            vecs[vec_index] = .{
+                .iov_base = &three_byte_nop,
+                .iov_len = three_byte_nop.len,
+            };
+            vec_index += 1;
+            padding_left -= three_byte_nop.len;
+        }
+        while (padding_left > page_of_nops.len) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = page_of_nops.len,
+            };
+            vec_index += 1;
+            padding_left -= page_of_nops.len;
+        }
+        if (padding_left > 0) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = padding_left,
+            };
+            vec_index += 1;
+        }
+    }
+
+    vecs[vec_index] = .{
+        .iov_base = buf.ptr,
+        .iov_len = buf.len,
+    };
+    vec_index += 1;
+
+    {
+        var padding_left = next_padding_size;
+        if (padding_left % 2 != 0) {
+            vecs[vec_index] = .{
+                .iov_base = &three_byte_nop,
+                .iov_len = three_byte_nop.len,
+            };
+            vec_index += 1;
+            padding_left -= three_byte_nop.len;
+        }
+        while (padding_left > page_of_nops.len) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = page_of_nops.len,
+            };
+            vec_index += 1;
+            padding_left -= page_of_nops.len;
+        }
+        if (padding_left > 0) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = padding_left,
+            };
+            vec_index += 1;
+        }
+    }
+    try file.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
+}
+
+/// Writes to the file a buffer, prefixed and suffixed by the specified number of
+/// bytes of padding.
+fn pwriteDbgInfoNops(
+    file: fs.File,
+    offset: u64,
+    prev_padding_size: usize,
+    buf: []const u8,
+    next_padding_size: usize,
+    trailing_zero: bool,
+) !void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const page_of_nops = [1]u8{abbrev_pad1} ** 4096;
+    var vecs: [32]std.os.iovec_const = undefined;
+    var vec_index: usize = 0;
+    {
+        var padding_left = prev_padding_size;
+        while (padding_left > page_of_nops.len) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = page_of_nops.len,
+            };
+            vec_index += 1;
+            padding_left -= page_of_nops.len;
+        }
+        if (padding_left > 0) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = padding_left,
+            };
+            vec_index += 1;
+        }
+    }
+
+    vecs[vec_index] = .{
+        .iov_base = buf.ptr,
+        .iov_len = buf.len,
+    };
+    vec_index += 1;
+
+    {
+        var padding_left = next_padding_size;
+        while (padding_left > page_of_nops.len) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = page_of_nops.len,
+            };
+            vec_index += 1;
+            padding_left -= page_of_nops.len;
+        }
+        if (padding_left > 0) {
+            vecs[vec_index] = .{
+                .iov_base = &page_of_nops,
+                .iov_len = padding_left,
+            };
+            vec_index += 1;
+        }
+    }
+
+    if (trailing_zero) {
+        var zbuf = [1]u8{0};
+        vecs[vec_index] = .{
+            .iov_base = &zbuf,
+            .iov_len = zbuf.len,
+        };
+        vec_index += 1;
+    }
+
+    try file.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
+}
+
+pub fn writeDbgAranges(self: *Dwarf, file: *File, addr: u64, size: u64) !void {
+    const target_endian = self.target.cpu.arch.endian();
+    const init_len_size: usize = if (self.tag == .macho)
+        4
+    else switch (self.ptr_width) {
+        .p32 => @as(usize, 4),
+        .p64 => 12,
+    };
+    const ptr_width_bytes: u8 = self.ptrWidthBytes();
+
+    // Enough for all the data without resizing. When support for more compilation units
+    // is added, the size of this section will become more variable.
+    var di_buf = try std.ArrayList(u8).initCapacity(self.allocator, 100);
+    defer di_buf.deinit();
+
+    // initial length - length of the .debug_aranges contribution for this compilation unit,
+    // not including the initial length itself.
+    // We have to come back and write it later after we know the size.
+    const init_len_index = di_buf.items.len;
+    di_buf.items.len += init_len_size;
+    const after_init_len = di_buf.items.len;
+    mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 2, target_endian); // version
+    // When more than one compilation unit is supported, this will be the offset to it.
+    // For now it is always at offset 0 in .debug_info.
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), 0); // __debug_info offset
+    } else {
+        self.writeAddrAssumeCapacity(&di_buf, 0); // .debug_info offset
+    }
+    di_buf.appendAssumeCapacity(ptr_width_bytes); // address_size
+    di_buf.appendAssumeCapacity(0); // segment_selector_size
+
+    const end_header_offset = di_buf.items.len;
+    const begin_entries_offset = mem.alignForward(end_header_offset, ptr_width_bytes * 2);
+    di_buf.appendNTimesAssumeCapacity(0, begin_entries_offset - end_header_offset);
+
+    // Currently only one compilation unit is supported, so the address range is simply
+    // identical to the main program header virtual address and memory size.
+    self.writeAddrAssumeCapacity(&di_buf, addr);
+    self.writeAddrAssumeCapacity(&di_buf, size);
+
+    // Sentinel.
+    self.writeAddrAssumeCapacity(&di_buf, 0);
+    self.writeAddrAssumeCapacity(&di_buf, 0);
+
+    // Go back and populate the initial length.
+    const init_len = di_buf.items.len - after_init_len;
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.items[init_len_index..][0..4], @intCast(u32, init_len));
+    } else switch (self.ptr_width) {
+        .p32 => {
+            mem.writeInt(u32, di_buf.items[init_len_index..][0..4], @intCast(u32, init_len), target_endian);
+        },
+        .p64 => {
+            // initial length - length of the .debug_aranges contribution for this compilation unit,
+            // not including the initial length itself.
+            di_buf.items[init_len_index..][0..4].* = [_]u8{ 0xff, 0xff, 0xff, 0xff };
+            mem.writeInt(u64, di_buf.items[init_len_index + 4 ..][0..8], init_len, target_endian);
+        },
+    }
+
+    const needed_size = di_buf.items.len;
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const debug_aranges_sect = &elf_file.sections.items[elf_file.debug_aranges_section_index.?];
+            const allocated_size = elf_file.allocatedSize(debug_aranges_sect.sh_offset);
+            if (needed_size > allocated_size) {
+                debug_aranges_sect.sh_size = 0; // free the space
+                debug_aranges_sect.sh_offset = elf_file.findFreeSpace(needed_size, 16);
+            }
+            debug_aranges_sect.sh_size = needed_size;
+            log.debug(".debug_aranges start=0x{x} end=0x{x}", .{
+                debug_aranges_sect.sh_offset,
+                debug_aranges_sect.sh_offset + needed_size,
+            });
+            const file_pos = debug_aranges_sect.sh_offset;
+            try elf_file.base.file.?.pwriteAll(di_buf.items, file_pos);
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = &macho_file.d_sym.?;
+            const dwarf_seg = &d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const debug_aranges_sect = &dwarf_seg.sections.items[d_sym.debug_aranges_section_index.?];
+            const allocated_size = d_sym.allocatedSize(debug_aranges_sect.offset);
+            if (needed_size > allocated_size) {
+                debug_aranges_sect.size = 0; // free the space
+                const new_offset = d_sym.findFreeSpace(needed_size, 16);
+                debug_aranges_sect.addr = dwarf_seg.inner.vmaddr + new_offset - dwarf_seg.inner.fileoff;
+                debug_aranges_sect.offset = @intCast(u32, new_offset);
+            }
+            debug_aranges_sect.size = needed_size;
+            log.debug("__debug_aranges start=0x{x} end=0x{x}", .{
+                debug_aranges_sect.offset,
+                debug_aranges_sect.offset + needed_size,
+            });
+            const file_pos = debug_aranges_sect.offset;
+            try d_sym.file.pwriteAll(di_buf.items, file_pos);
+        },
+        else => unreachable,
+    }
+}
+
+pub fn writeDbgLineHeader(self: *Dwarf, file: *File, module: *Module) !void {
+    const ptr_width_bytes: u8 = self.ptrWidthBytes();
+    const target_endian = self.target.cpu.arch.endian();
+    const init_len_size: usize = if (self.tag == .macho)
+        4
+    else switch (self.ptr_width) {
+        .p32 => @as(usize, 4),
+        .p64 => 12,
+    };
+
+    const dbg_line_prg_off = self.getDebugLineProgramOff() orelse return;
+    const dbg_line_prg_end = self.getDebugLineProgramEnd().?;
+    assert(dbg_line_prg_end != 0);
+
+    // The size of this header is variable, depending on the number of directories,
+    // files, and padding. We have a function to compute the upper bound size, however,
+    // because it's needed for determining where to put the offset of the first `SrcFn`.
+    const needed_bytes = self.dbgLineNeededHeaderBytes(module);
+    var di_buf = try std.ArrayList(u8).initCapacity(self.allocator, needed_bytes);
+    defer di_buf.deinit();
+
+    // initial length - length of the .debug_line contribution for this compilation unit,
+    // not including the initial length itself.
+    const after_init_len = di_buf.items.len + init_len_size;
+    const init_len = dbg_line_prg_end - after_init_len;
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len));
+    } else switch (self.ptr_width) {
+        .p32 => {
+            mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len), target_endian);
+        },
+        .p64 => {
+            di_buf.appendNTimesAssumeCapacity(0xff, 4);
+            mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), init_len, target_endian);
+        },
+    }
+
+    mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4, target_endian); // version
+
+    // Empirically, debug info consumers do not respect this field, or otherwise
+    // consider it to be an error when it does not point exactly to the end of the header.
+    // Therefore we rely on the NOP jump at the beginning of the Line Number Program for
+    // padding rather than this field.
+    const before_header_len = di_buf.items.len;
+    di_buf.items.len += if (self.tag == .macho) @sizeOf(u32) else ptr_width_bytes; // We will come back and write this.
+    const after_header_len = di_buf.items.len;
+
+    const opcode_base = DW.LNS.set_isa + 1;
+    di_buf.appendSliceAssumeCapacity(&[_]u8{
+        1, // minimum_instruction_length
+        1, // maximum_operations_per_instruction
+        1, // default_is_stmt
+        1, // line_base (signed)
+        1, // line_range
+        opcode_base,
+
+        // Standard opcode lengths. The number of items here is based on `opcode_base`.
+        // The value is the number of LEB128 operands the instruction takes.
+        0, // `DW.LNS.copy`
+        1, // `DW.LNS.advance_pc`
+        1, // `DW.LNS.advance_line`
+        1, // `DW.LNS.set_file`
+        1, // `DW.LNS.set_column`
+        0, // `DW.LNS.negate_stmt`
+        0, // `DW.LNS.set_basic_block`
+        0, // `DW.LNS.const_add_pc`
+        1, // `DW.LNS.fixed_advance_pc`
+        0, // `DW.LNS.set_prologue_end`
+        0, // `DW.LNS.set_epilogue_begin`
+        1, // `DW.LNS.set_isa`
+        0, // include_directories (none except the compilation unit cwd)
+    });
+    // file_names[0]
+    di_buf.appendSliceAssumeCapacity(module.root_pkg.root_src_path); // relative path name
+    di_buf.appendSliceAssumeCapacity(&[_]u8{
+        0, // null byte for the relative path name
+        0, // directory_index
+        0, // mtime (TODO supply this)
+        0, // file size bytes (TODO supply this)
+        0, // file_names sentinel
+    });
+
+    const header_len = di_buf.items.len - after_header_len;
+    if (self.tag == .macho) {
+        mem.writeIntLittle(u32, di_buf.items[before_header_len..][0..4], @intCast(u32, header_len));
+    } else switch (self.ptr_width) {
+        .p32 => {
+            mem.writeInt(u32, di_buf.items[before_header_len..][0..4], @intCast(u32, header_len), target_endian);
+        },
+        .p64 => {
+            mem.writeInt(u64, di_buf.items[before_header_len..][0..8], header_len, target_endian);
+        },
+    }
+
+    // We use NOPs because consumers empirically do not respect the header length field.
+    if (di_buf.items.len > dbg_line_prg_off) {
+        // Move the first N files to the end to make more padding for the header.
+        @panic("TODO: handle .debug_line header exceeding its padding");
+    }
+    const jmp_amt = dbg_line_prg_off - di_buf.items.len;
+    switch (self.tag) {
+        .elf => {
+            const elf_file = file.cast(File.Elf).?;
+            const debug_line_sect = elf_file.sections.items[elf_file.debug_line_section_index.?];
+            const file_pos = debug_line_sect.sh_offset;
+            try pwriteDbgLineNops(elf_file.base.file.?, file_pos, 0, di_buf.items, jmp_amt);
+        },
+        .macho => {
+            const macho_file = file.cast(File.MachO).?;
+            const d_sym = &macho_file.d_sym.?;
+            const dwarf_seg = d_sym.load_commands.items[d_sym.dwarf_segment_cmd_index.?].segment;
+            const debug_line_sect = dwarf_seg.sections.items[d_sym.debug_line_section_index.?];
+            const file_pos = debug_line_sect.offset;
+            try pwriteDbgLineNops(d_sym.file, file_pos, 0, di_buf.items, jmp_amt);
+        },
+        else => unreachable,
+    }
+}
+
+fn getDebugInfoOff(self: Dwarf) ?u32 {
+    const first = self.dbg_info_decl_first orelse return null;
+    return first.off;
+}
+
+fn getDebugInfoEnd(self: Dwarf) ?u32 {
+    const last = self.dbg_info_decl_last orelse return null;
+    return last.off + last.len;
+}
+
+fn getDebugLineProgramOff(self: Dwarf) ?u32 {
+    const first = self.dbg_line_fn_first orelse return null;
+    return first.off;
+}
+
+fn getDebugLineProgramEnd(self: Dwarf) ?u32 {
+    const last = self.dbg_line_fn_last orelse return null;
+    return last.off + last.len;
+}
+
+/// Always 4 or 8 depending on whether this is 32-bit or 64-bit format.
+fn ptrWidthBytes(self: Dwarf) u8 {
+    return switch (self.ptr_width) {
+        .p32 => 4,
+        .p64 => 8,
+    };
+}
+
+fn dbgLineNeededHeaderBytes(self: Dwarf, module: *Module) u32 {
+    _ = self;
+    const directory_entry_format_count = 1;
+    const file_name_entry_format_count = 1;
+    const directory_count = 1;
+    const file_name_count = 1;
+    const root_src_dir_path_len = if (module.root_pkg.root_src_directory.path) |p| p.len else 1; // "."
+    return @intCast(u32, 53 + directory_entry_format_count * 2 + file_name_entry_format_count * 2 +
+        directory_count * 8 + file_name_count * 8 +
+        // These are encoded as DW.FORM.string rather than DW.FORM.strp as we would like
+        // because of a workaround for readelf and gdb failing to understand DWARFv5 correctly.
+        root_src_dir_path_len +
+        module.root_pkg.root_src_path.len);
+}
+
+/// The reloc offset for the line offset of a function from the previous function's line.
+/// It's a fixed-size 4-byte ULEB128.
+fn getRelocDbgLineOff(self: Dwarf) usize {
+    return dbg_line_vaddr_reloc_index + self.ptrWidthBytes() + 1;
+}
+
+fn getRelocDbgFileIndex(self: Dwarf) usize {
+    return self.getRelocDbgLineOff() + 5;
+}
+
+fn getRelocDbgInfoSubprogramHighPC(self: Dwarf) u32 {
+    return dbg_info_low_pc_reloc_index + self.ptrWidthBytes();
+}
+
+/// TODO Improve this to use a table.
+fn makeString(self: *Dwarf, bytes: []const u8) !u32 {
+    try self.strtab.ensureUnusedCapacity(self.allocator, bytes.len + 1);
+    const result = self.strtab.items.len;
+    self.strtab.appendSliceAssumeCapacity(bytes);
+    self.strtab.appendAssumeCapacity(0);
+    return @intCast(u32, result);
+}
+
+fn padToIdeal(actual_size: anytype) @TypeOf(actual_size) {
+    // TODO https://github.com/ziglang/zig/issues/1284
+    return std.math.add(@TypeOf(actual_size), actual_size, actual_size / ideal_factor) catch
+        std.math.maxInt(@TypeOf(actual_size));
+}
src/link/Elf.zig
@@ -9,11 +9,10 @@ const Allocator = std.mem.Allocator;
 const fs = std.fs;
 const elf = std.elf;
 const log = std.log.scoped(.link);
-const DW = std.dwarf;
-const leb128 = std.leb;
 
 const Module = @import("../Module.zig");
 const Compilation = @import("../Compilation.zig");
+const Dwarf = @import("Dwarf.zig");
 const codegen = @import("../codegen.zig");
 const lldMain = @import("../main.zig").lldMain;
 const trace = @import("../tracy.zig").trace;
@@ -37,6 +36,7 @@ const default_entry_addr = 0x8000000;
 pub const base_tag: File.Tag = .elf;
 
 base: File,
+dwarf: ?Dwarf = null,
 
 ptr_width: PtrWidth,
 
@@ -67,7 +67,6 @@ phdr_shdr_table: std.AutoHashMapUnmanaged(u16, u16) = .{},
 entry_addr: ?u64 = null,
 page_size: u16,
 
-debug_strtab: std.ArrayListUnmanaged(u8) = std.ArrayListUnmanaged(u8){},
 shstrtab: std.ArrayListUnmanaged(u8) = std.ArrayListUnmanaged(u8){},
 shstrtab_index: ?u16 = null,
 
@@ -82,8 +81,6 @@ debug_str_section_index: ?u16 = null,
 debug_aranges_section_index: ?u16 = null,
 debug_line_section_index: ?u16 = null,
 
-debug_abbrev_table_offset: ?u64 = null,
-
 /// The same order as in the file. ELF requires global symbols to all be after the
 /// local symbols, they cannot be mixed. So we must buffer all the global symbols and
 /// write them at the end. These are only the local symbols. The length of this array
@@ -168,18 +165,6 @@ atom_by_index_table: std.AutoHashMapUnmanaged(u32, *TextBlock) = .{},
 /// with `Decl` `main`, and lives as long as that `Decl`.
 unnamed_const_atoms: UnnamedConstTable = .{},
 
-/// A list of `SrcFn` whose Line Number Programs have surplus capacity.
-/// This is the same concept as `text_block_free_list`; see those doc comments.
-dbg_line_fn_free_list: std.AutoHashMapUnmanaged(*SrcFn, void) = .{},
-dbg_line_fn_first: ?*SrcFn = null,
-dbg_line_fn_last: ?*SrcFn = null,
-
-/// A list of `TextBlock` whose corresponding .debug_info tags have surplus capacity.
-/// This is the same concept as `text_block_free_list`; see those doc comments.
-dbg_info_decl_free_list: std.AutoHashMapUnmanaged(*TextBlock, void) = .{},
-dbg_info_decl_first: ?*TextBlock = null,
-dbg_info_decl_last: ?*TextBlock = null,
-
 /// A table of relocations indexed by the owning them `TextBlock`.
 /// Note that once we refactor `TextBlock`'s lifetime and ownership rules,
 /// this will be a table indexed by index into the list of Atoms.
@@ -222,24 +207,14 @@ pub const TextBlock = struct {
     prev: ?*TextBlock,
     next: ?*TextBlock,
 
-    /// Previous/next linked list pointers.
-    /// This is the linked list node for this Decl's corresponding .debug_info tag.
-    dbg_info_prev: ?*TextBlock,
-    dbg_info_next: ?*TextBlock,
-    /// Offset into .debug_info pointing to the tag for this Decl.
-    dbg_info_off: u32,
-    /// Size of the .debug_info tag for this Decl, not including padding.
-    dbg_info_len: u32,
+    dbg_info_atom: Dwarf.DebugInfoAtom,
 
     pub const empty = TextBlock{
         .local_sym_index = 0,
         .offset_table_index = undefined,
         .prev = null,
         .next = null,
-        .dbg_info_prev = null,
-        .dbg_info_next = null,
-        .dbg_info_off = undefined,
-        .dbg_info_len = undefined,
+        .dbg_info_atom = undefined,
     };
 
     /// Returns how much room there is to grow in virtual address space.
@@ -273,26 +248,6 @@ pub const Export = struct {
     sym_index: ?u32 = null,
 };
 
-pub const SrcFn = struct {
-    /// Offset from the beginning of the Debug Line Program header that contains this function.
-    off: u32,
-    /// Size of the line number program component belonging to this function, not
-    /// including padding.
-    len: u32,
-
-    /// Points to the previous and next neighbors, based on the offset from .debug_line.
-    /// This can be used to find, for example, the capacity of this `SrcFn`.
-    prev: ?*SrcFn,
-    next: ?*SrcFn,
-
-    pub const empty: SrcFn = .{
-        .off = 0,
-        .len = 0,
-        .prev = null,
-        .next = null,
-    };
-};
-
 pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Options) !*Elf {
     assert(options.object_format == .elf);
 
@@ -351,6 +306,11 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*Elf {
     errdefer gpa.destroy(self);
     const page_size: u16 = 0x1000; // TODO ppc64le requires 64KB
 
+    var dwarf: ?Dwarf = if (!options.strip and options.module != null)
+        Dwarf.init(gpa, .elf, options.target)
+    else
+        null;
+
     self.* = .{
         .base = .{
             .tag = .elf,
@@ -358,6 +318,7 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*Elf {
             .allocator = gpa,
             .file = null,
         },
+        .dwarf = dwarf,
         .ptr_width = ptr_width,
         .page_size = page_size,
     };
@@ -377,14 +338,11 @@ pub fn deinit(self: *Elf) void {
     self.sections.deinit(self.base.allocator);
     self.program_headers.deinit(self.base.allocator);
     self.shstrtab.deinit(self.base.allocator);
-    self.debug_strtab.deinit(self.base.allocator);
     self.local_symbols.deinit(self.base.allocator);
     self.global_symbols.deinit(self.base.allocator);
     self.global_symbol_free_list.deinit(self.base.allocator);
     self.local_symbol_free_list.deinit(self.base.allocator);
     self.offset_table_free_list.deinit(self.base.allocator);
-    self.dbg_line_fn_free_list.deinit(self.base.allocator);
-    self.dbg_info_decl_free_list.deinit(self.base.allocator);
     self.offset_table.deinit(self.base.allocator);
     self.phdr_shdr_table.deinit(self.base.allocator);
     self.decls.deinit(self.base.allocator);
@@ -420,6 +378,10 @@ pub fn deinit(self: *Elf) void {
     }
 
     self.atom_by_index_table.deinit(self.base.allocator);
+
+    if (self.dwarf) |*dw| {
+        dw.deinit();
+    }
 }
 
 pub fn getDeclVAddr(self: *Elf, decl: *const Module.Decl, reloc_info: File.RelocInfo) !u64 {
@@ -443,14 +405,6 @@ pub fn getDeclVAddr(self: *Elf, decl: *const Module.Decl, reloc_info: File.Reloc
     return vaddr;
 }
 
-fn getDebugLineProgramOff(self: Elf) u32 {
-    return self.dbg_line_fn_first.?.off;
-}
-
-fn getDebugLineProgramEnd(self: Elf) u32 {
-    return self.dbg_line_fn_last.?.off + self.dbg_line_fn_last.?.len;
-}
-
 /// Returns end pos of collision, if any.
 fn detectAllocCollision(self: *Elf, start: u64, size: u64) ?u64 {
     const small_ptr = self.ptr_width == .p32;
@@ -497,7 +451,7 @@ fn detectAllocCollision(self: *Elf, start: u64, size: u64) ?u64 {
     return null;
 }
 
-fn allocatedSize(self: *Elf, start: u64) u64 {
+pub fn allocatedSize(self: *Elf, start: u64) u64 {
     if (start == 0)
         return 0;
     var min_pos: u64 = std.math.maxInt(u64);
@@ -518,7 +472,7 @@ fn allocatedSize(self: *Elf, start: u64) u64 {
     return min_pos - start;
 }
 
-fn findFreeSpace(self: *Elf, object_size: u64, min_alignment: u16) u64 {
+pub fn findFreeSpace(self: *Elf, object_size: u64, min_alignment: u16) u64 {
     var start: u64 = 0;
     while (self.detectAllocCollision(start, object_size)) |item_end| {
         start = mem.alignForwardGeneric(u64, item_end, min_alignment);
@@ -535,15 +489,6 @@ fn makeString(self: *Elf, bytes: []const u8) !u32 {
     return @intCast(u32, result);
 }
 
-/// TODO Improve this to use a table.
-fn makeDebugString(self: *Elf, bytes: []const u8) !u32 {
-    try self.debug_strtab.ensureUnusedCapacity(self.base.allocator, bytes.len + 1);
-    const result = self.debug_strtab.items.len;
-    self.debug_strtab.appendSliceAssumeCapacity(bytes);
-    self.debug_strtab.appendAssumeCapacity(0);
-    return @intCast(u32, result);
-}
-
 fn getString(self: *Elf, str_off: u32) []const u8 {
     assert(str_off < self.shstrtab.items.len);
     return mem.sliceTo(@ptrCast([*:0]const u8, self.shstrtab.items.ptr + str_off), 0);
@@ -806,14 +751,14 @@ pub fn populateMissingMetadata(self: *Elf) !void {
 
     if (self.debug_str_section_index == null) {
         self.debug_str_section_index = @intCast(u16, self.sections.items.len);
-        assert(self.debug_strtab.items.len == 0);
+        assert(self.dwarf.?.strtab.items.len == 0);
         try self.sections.append(self.base.allocator, .{
             .sh_name = try self.makeString(".debug_str"),
             .sh_type = elf.SHT_PROGBITS,
             .sh_flags = elf.SHF_MERGE | elf.SHF_STRINGS,
             .sh_addr = 0,
             .sh_offset = 0,
-            .sh_size = self.debug_strtab.items.len,
+            .sh_size = 0,
             .sh_link = 0,
             .sh_info = 0,
             .sh_addralign = 1,
@@ -977,16 +922,6 @@ pub fn populateMissingMetadata(self: *Elf) !void {
     }
 }
 
-pub const abbrev_compile_unit = 1;
-pub const abbrev_subprogram = 2;
-pub const abbrev_subprogram_retvoid = 3;
-pub const abbrev_base_type = 4;
-pub const abbrev_ptr_type = 5;
-pub const abbrev_struct_type = 6;
-pub const abbrev_struct_member = 7;
-pub const abbrev_pad1 = 8;
-pub const abbrev_parameter = 9;
-
 pub fn flush(self: *Elf, comp: *Compilation) !void {
     if (self.base.options.emit == null) {
         if (build_options.have_llvm) {
@@ -1022,11 +957,6 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
 
     const target_endian = self.base.options.target.cpu.arch.endian();
     const foreign_endian = target_endian != builtin.cpu.arch.endian();
-    const ptr_width_bytes: u8 = self.ptrWidthBytes();
-    const init_len_size: usize = switch (self.ptr_width) {
-        .p32 => 4,
-        .p64 => 12,
-    };
 
     {
         var it = self.relocs.iterator();
@@ -1068,349 +998,38 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
     try self.writeAllGlobalSymbols();
 
     if (self.debug_abbrev_section_dirty) {
-        const debug_abbrev_sect = &self.sections.items[self.debug_abbrev_section_index.?];
-
-        // These are LEB encoded but since the values are all less than 127
-        // we can simply append these bytes.
-        const abbrev_buf = [_]u8{
-            abbrev_compile_unit, DW.TAG.compile_unit, DW.CHILDREN.yes, // header
-            DW.AT.stmt_list,     DW.FORM.sec_offset,  DW.AT.low_pc,
-            DW.FORM.addr,        DW.AT.high_pc,       DW.FORM.addr,
-            DW.AT.name,          DW.FORM.strp,        DW.AT.comp_dir,
-            DW.FORM.strp,        DW.AT.producer,      DW.FORM.strp,
-            DW.AT.language,      DW.FORM.data2,       0,
-            0, // table sentinel
-            abbrev_subprogram,
-            DW.TAG.subprogram,
-            DW.CHILDREN.yes, // header
-            DW.AT.low_pc,
-            DW.FORM.addr,
-            DW.AT.high_pc,
-            DW.FORM.data4,
-            DW.AT.type,
-            DW.FORM.ref4,
-            DW.AT.name,
-            DW.FORM.string,
-            0,                         0, // table sentinel
-            abbrev_subprogram_retvoid,
-            DW.TAG.subprogram, DW.CHILDREN.yes, // header
-            DW.AT.low_pc,      DW.FORM.addr,
-            DW.AT.high_pc,     DW.FORM.data4,
-            DW.AT.name,        DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_base_type,
-            DW.TAG.base_type,
-            DW.CHILDREN.no, // header
-            DW.AT.encoding,
-            DW.FORM.data1,
-            DW.AT.byte_size,
-            DW.FORM.data1,
-            DW.AT.name,
-            DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_ptr_type,
-            DW.TAG.pointer_type,
-            DW.CHILDREN.no, // header
-            DW.AT.type,
-            DW.FORM.ref4,
-            0,
-            0, // table sentinel
-            abbrev_struct_type,
-            DW.TAG.structure_type,
-            DW.CHILDREN.yes, // header
-            DW.AT.byte_size,
-            DW.FORM.sdata,
-            DW.AT.name,
-            DW.FORM.string,
-            0,
-            0, // table sentinel
-            abbrev_struct_member,
-            DW.TAG.member,
-            DW.CHILDREN.no, // header
-            DW.AT.name,
-            DW.FORM.string,
-            DW.AT.type,
-            DW.FORM.ref4,
-            DW.AT.data_member_location,
-            DW.FORM.sdata,
-            0,
-            0, // table sentinel
-            abbrev_pad1,
-            DW.TAG.unspecified_type,
-            DW.CHILDREN.no, // header
-            0,
-            0, // table sentinel
-            abbrev_parameter,
-            DW.TAG.formal_parameter, DW.CHILDREN.no, // header
-            DW.AT.location,          DW.FORM.exprloc,
-            DW.AT.type,              DW.FORM.ref4,
-            DW.AT.name,              DW.FORM.string,
-            0,
-            0, // table sentinel
-            0,
-            0,
-            0, // section sentinel
-        };
-
-        const needed_size = abbrev_buf.len;
-        const allocated_size = self.allocatedSize(debug_abbrev_sect.sh_offset);
-        if (needed_size > allocated_size) {
-            debug_abbrev_sect.sh_size = 0; // free the space
-            debug_abbrev_sect.sh_offset = self.findFreeSpace(needed_size, 1);
-        }
-        debug_abbrev_sect.sh_size = needed_size;
-        log.debug(".debug_abbrev start=0x{x} end=0x{x}", .{
-            debug_abbrev_sect.sh_offset,
-            debug_abbrev_sect.sh_offset + needed_size,
-        });
-
-        const abbrev_offset = 0;
-        self.debug_abbrev_table_offset = abbrev_offset;
-        try self.base.file.?.pwriteAll(&abbrev_buf, debug_abbrev_sect.sh_offset + abbrev_offset);
+        try self.dwarf.?.writeDbgAbbrev(&self.base);
         if (!self.shdr_table_dirty) {
             // Then it won't get written with the others and we need to do it.
             try self.writeSectHeader(self.debug_abbrev_section_index.?);
         }
-
         self.debug_abbrev_section_dirty = false;
     }
 
-    if (self.debug_info_header_dirty) debug_info: {
-        // If this value is null it means there is an error in the module;
-        // leave debug_info_header_dirty=true.
-        const first_dbg_info_decl = self.dbg_info_decl_first orelse break :debug_info;
-        const last_dbg_info_decl = self.dbg_info_decl_last.?;
-        const debug_info_sect = &self.sections.items[self.debug_info_section_index.?];
-
-        // We have a function to compute the upper bound size, because it's needed
-        // for determining where to put the offset of the first `LinkBlock`.
-        const needed_bytes = self.dbgInfoNeededHeaderBytes();
-        var di_buf = try std.ArrayList(u8).initCapacity(self.base.allocator, needed_bytes);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_info contribution for this compilation unit,
-        // not including the initial length itself.
-        // We have to come back and write it later after we know the size.
-        const after_init_len = di_buf.items.len + init_len_size;
-        // +1 for the final 0 that ends the compilation unit children.
-        const dbg_info_end = last_dbg_info_decl.dbg_info_off + last_dbg_info_decl.dbg_info_len + 1;
-        const init_len = dbg_info_end - after_init_len;
-        switch (self.ptr_width) {
-            .p32 => {
-                mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len), target_endian);
-            },
-            .p64 => {
-                di_buf.appendNTimesAssumeCapacity(0xff, 4);
-                mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), init_len, target_endian);
-            },
-        }
-        mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4, target_endian); // DWARF version
-        const abbrev_offset = self.debug_abbrev_table_offset.?;
-        switch (self.ptr_width) {
-            .p32 => {
-                mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, abbrev_offset), target_endian);
-                di_buf.appendAssumeCapacity(4); // address size
-            },
-            .p64 => {
-                mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), abbrev_offset, target_endian);
-                di_buf.appendAssumeCapacity(8); // address size
-            },
-        }
-        // Write the form for the compile unit, which must match the abbrev table above.
-        const name_strp = try self.makeDebugString(module.root_pkg.root_src_path);
-        const comp_dir_strp = try self.makeDebugString(module.root_pkg.root_src_directory.path orelse ".");
-        const producer_strp = try self.makeDebugString(link.producer_string);
+    if (self.debug_info_header_dirty) {
         // Currently only one compilation unit is supported, so the address range is simply
         // identical to the main program header virtual address and memory size.
         const text_phdr = &self.program_headers.items[self.phdr_load_re_index.?];
         const low_pc = text_phdr.p_vaddr;
         const high_pc = text_phdr.p_vaddr + text_phdr.p_memsz;
-
-        di_buf.appendAssumeCapacity(abbrev_compile_unit);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, 0); // DW.AT.stmt_list, DW.FORM.sec_offset
-        self.writeDwarfAddrAssumeCapacity(&di_buf, low_pc);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, high_pc);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, name_strp);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, comp_dir_strp);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, producer_strp);
-        // We are still waiting on dwarf-std.org to assign DW_LANG_Zig a number:
-        // http://dwarfstd.org/ShowIssue.php?issue=171115.1
-        // Until then we say it is C99.
-        mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), DW.LANG.C99, target_endian);
-
-        if (di_buf.items.len > first_dbg_info_decl.dbg_info_off) {
-            // Move the first N decls to the end to make more padding for the header.
-            @panic("TODO: handle .debug_info header exceeding its padding");
-        }
-        const jmp_amt = first_dbg_info_decl.dbg_info_off - di_buf.items.len;
-        try self.pwriteDbgInfoNops(0, di_buf.items, jmp_amt, false, debug_info_sect.sh_offset);
+        try self.dwarf.?.writeDbgInfoHeader(&self.base, module, low_pc, high_pc);
         self.debug_info_header_dirty = false;
     }
 
     if (self.debug_aranges_section_dirty) {
-        const debug_aranges_sect = &self.sections.items[self.debug_aranges_section_index.?];
-
-        // Enough for all the data without resizing. When support for more compilation units
-        // is added, the size of this section will become more variable.
-        var di_buf = try std.ArrayList(u8).initCapacity(self.base.allocator, 100);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_aranges contribution for this compilation unit,
-        // not including the initial length itself.
-        // We have to come back and write it later after we know the size.
-        const init_len_index = di_buf.items.len;
-        di_buf.items.len += init_len_size;
-        const after_init_len = di_buf.items.len;
-        mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 2, target_endian); // version
-        // When more than one compilation unit is supported, this will be the offset to it.
-        // For now it is always at offset 0 in .debug_info.
-        self.writeDwarfAddrAssumeCapacity(&di_buf, 0); // .debug_info offset
-        di_buf.appendAssumeCapacity(ptr_width_bytes); // address_size
-        di_buf.appendAssumeCapacity(0); // segment_selector_size
-
-        const end_header_offset = di_buf.items.len;
-        const begin_entries_offset = mem.alignForward(end_header_offset, ptr_width_bytes * 2);
-        di_buf.appendNTimesAssumeCapacity(0, begin_entries_offset - end_header_offset);
-
         // Currently only one compilation unit is supported, so the address range is simply
         // identical to the main program header virtual address and memory size.
         const text_phdr = &self.program_headers.items[self.phdr_load_re_index.?];
-        self.writeDwarfAddrAssumeCapacity(&di_buf, text_phdr.p_vaddr);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, text_phdr.p_memsz);
-
-        // Sentinel.
-        self.writeDwarfAddrAssumeCapacity(&di_buf, 0);
-        self.writeDwarfAddrAssumeCapacity(&di_buf, 0);
-
-        // Go back and populate the initial length.
-        const init_len = di_buf.items.len - after_init_len;
-        switch (self.ptr_width) {
-            .p32 => {
-                mem.writeInt(u32, di_buf.items[init_len_index..][0..4], @intCast(u32, init_len), target_endian);
-            },
-            .p64 => {
-                // initial length - length of the .debug_aranges contribution for this compilation unit,
-                // not including the initial length itself.
-                di_buf.items[init_len_index..][0..4].* = [_]u8{ 0xff, 0xff, 0xff, 0xff };
-                mem.writeInt(u64, di_buf.items[init_len_index + 4 ..][0..8], init_len, target_endian);
-            },
-        }
-
-        const needed_size = di_buf.items.len;
-        const allocated_size = self.allocatedSize(debug_aranges_sect.sh_offset);
-        if (needed_size > allocated_size) {
-            debug_aranges_sect.sh_size = 0; // free the space
-            debug_aranges_sect.sh_offset = self.findFreeSpace(needed_size, 16);
-        }
-        debug_aranges_sect.sh_size = needed_size;
-        log.debug(".debug_aranges start=0x{x} end=0x{x}", .{
-            debug_aranges_sect.sh_offset,
-            debug_aranges_sect.sh_offset + needed_size,
-        });
-
-        try self.base.file.?.pwriteAll(di_buf.items, debug_aranges_sect.sh_offset);
+        try self.dwarf.?.writeDbgAranges(&self.base, text_phdr.p_vaddr, text_phdr.p_memsz);
         if (!self.shdr_table_dirty) {
             // Then it won't get written with the others and we need to do it.
             try self.writeSectHeader(self.debug_aranges_section_index.?);
         }
-
         self.debug_aranges_section_dirty = false;
     }
-    if (self.debug_line_header_dirty) debug_line: {
-        if (self.dbg_line_fn_first == null) {
-            break :debug_line; // Error in module; leave debug_line_header_dirty=true.
-        }
-        const dbg_line_prg_off = self.getDebugLineProgramOff();
-        const dbg_line_prg_end = self.getDebugLineProgramEnd();
-        assert(dbg_line_prg_end != 0);
-
-        const debug_line_sect = &self.sections.items[self.debug_line_section_index.?];
-
-        // The size of this header is variable, depending on the number of directories,
-        // files, and padding. We have a function to compute the upper bound size, however,
-        // because it's needed for determining where to put the offset of the first `SrcFn`.
-        const needed_bytes = self.dbgLineNeededHeaderBytes();
-        var di_buf = try std.ArrayList(u8).initCapacity(self.base.allocator, needed_bytes);
-        defer di_buf.deinit();
-
-        // initial length - length of the .debug_line contribution for this compilation unit,
-        // not including the initial length itself.
-        const after_init_len = di_buf.items.len + init_len_size;
-        const init_len = dbg_line_prg_end - after_init_len;
-        switch (self.ptr_width) {
-            .p32 => {
-                mem.writeInt(u32, di_buf.addManyAsArrayAssumeCapacity(4), @intCast(u32, init_len), target_endian);
-            },
-            .p64 => {
-                di_buf.appendNTimesAssumeCapacity(0xff, 4);
-                mem.writeInt(u64, di_buf.addManyAsArrayAssumeCapacity(8), init_len, target_endian);
-            },
-        }
-
-        mem.writeInt(u16, di_buf.addManyAsArrayAssumeCapacity(2), 4, target_endian); // version
-
-        // Empirically, debug info consumers do not respect this field, or otherwise
-        // consider it to be an error when it does not point exactly to the end of the header.
-        // Therefore we rely on the NOP jump at the beginning of the Line Number Program for
-        // padding rather than this field.
-        const before_header_len = di_buf.items.len;
-        di_buf.items.len += ptr_width_bytes; // We will come back and write this.
-        const after_header_len = di_buf.items.len;
-
-        const opcode_base = DW.LNS.set_isa + 1;
-        di_buf.appendSliceAssumeCapacity(&[_]u8{
-            1, // minimum_instruction_length
-            1, // maximum_operations_per_instruction
-            1, // default_is_stmt
-            1, // line_base (signed)
-            1, // line_range
-            opcode_base,
-
-            // Standard opcode lengths. The number of items here is based on `opcode_base`.
-            // The value is the number of LEB128 operands the instruction takes.
-            0, // `DW.LNS.copy`
-            1, // `DW.LNS.advance_pc`
-            1, // `DW.LNS.advance_line`
-            1, // `DW.LNS.set_file`
-            1, // `DW.LNS.set_column`
-            0, // `DW.LNS.negate_stmt`
-            0, // `DW.LNS.set_basic_block`
-            0, // `DW.LNS.const_add_pc`
-            1, // `DW.LNS.fixed_advance_pc`
-            0, // `DW.LNS.set_prologue_end`
-            0, // `DW.LNS.set_epilogue_begin`
-            1, // `DW.LNS.set_isa`
-            0, // include_directories (none except the compilation unit cwd)
-        });
-        // file_names[0]
-        di_buf.appendSliceAssumeCapacity(module.root_pkg.root_src_path); // relative path name
-        di_buf.appendSliceAssumeCapacity(&[_]u8{
-            0, // null byte for the relative path name
-            0, // directory_index
-            0, // mtime (TODO supply this)
-            0, // file size bytes (TODO supply this)
-            0, // file_names sentinel
-        });
 
-        const header_len = di_buf.items.len - after_header_len;
-        switch (self.ptr_width) {
-            .p32 => {
-                mem.writeInt(u32, di_buf.items[before_header_len..][0..4], @intCast(u32, header_len), target_endian);
-            },
-            .p64 => {
-                mem.writeInt(u64, di_buf.items[before_header_len..][0..8], header_len, target_endian);
-            },
-        }
-
-        // We use NOPs because consumers empirically do not respect the header length field.
-        if (di_buf.items.len > dbg_line_prg_off) {
-            // Move the first N files to the end to make more padding for the header.
-            @panic("TODO: handle .debug_line header exceeding its padding");
-        }
-        const jmp_amt = dbg_line_prg_off - di_buf.items.len;
-        try self.pwriteDbgLineNops(0, di_buf.items, jmp_amt, debug_line_sect.sh_offset);
+    if (self.debug_line_header_dirty) {
+        try self.dwarf.?.writeDbgLineHeader(&self.base, module);
         self.debug_line_header_dirty = false;
     }
 
@@ -1481,11 +1100,13 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
             self.shstrtab_dirty = false;
         }
     }
+
     {
         const debug_strtab_sect = &self.sections.items[self.debug_str_section_index.?];
-        if (self.debug_strtab_dirty or self.debug_strtab.items.len != debug_strtab_sect.sh_size) {
+        const dwarf = self.dwarf.?;
+        if (self.debug_strtab_dirty or dwarf.strtab.items.len != debug_strtab_sect.sh_size) {
             const allocated_size = self.allocatedSize(debug_strtab_sect.sh_offset);
-            const needed_size = self.debug_strtab.items.len;
+            const needed_size = dwarf.strtab.items.len;
 
             if (needed_size > allocated_size) {
                 debug_strtab_sect.sh_size = 0; // free the space
@@ -1494,7 +1115,7 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
             debug_strtab_sect.sh_size = needed_size;
             log.debug("debug_strtab start=0x{x} end=0x{x}", .{ debug_strtab_sect.sh_offset, debug_strtab_sect.sh_offset + needed_size });
 
-            try self.base.file.?.pwriteAll(self.debug_strtab.items, debug_strtab_sect.sh_offset);
+            try self.base.file.?.pwriteAll(dwarf.strtab.items, debug_strtab_sect.sh_offset);
             if (!self.shdr_table_dirty) {
                 // Then it won't get written with the others and we need to do it.
                 try self.writeSectHeader(self.debug_str_section_index.?);
@@ -1502,6 +1123,7 @@ pub fn flushModule(self: *Elf, comp: *Compilation) !void {
             self.debug_strtab_dirty = false;
         }
     }
+
     if (self.shdr_table_dirty) {
         const shsize: u64 = switch (self.ptr_width) {
             .p32 => @sizeOf(elf.Elf32_Shdr),
@@ -2340,7 +1962,6 @@ fn freeTextBlock(self: *Elf, text_block: *TextBlock, phdr_index: u16) void {
             i += 1;
         }
     }
-    // TODO process free list for dbg info just like we do above for vaddrs
 
     if (self.atoms.getPtr(phdr_index)) |last_block| {
         if (last_block.* == text_block) {
@@ -2353,14 +1974,6 @@ fn freeTextBlock(self: *Elf, text_block: *TextBlock, phdr_index: u16) void {
         }
     }
 
-    if (self.dbg_info_decl_first == text_block) {
-        self.dbg_info_decl_first = text_block.dbg_info_next;
-    }
-    if (self.dbg_info_decl_last == text_block) {
-        // TODO shrink the .debug_info section size here
-        self.dbg_info_decl_last = text_block.dbg_info_prev;
-    }
-
     if (text_block.prev) |prev| {
         prev.next = text_block.next;
 
@@ -2379,18 +1992,8 @@ fn freeTextBlock(self: *Elf, text_block: *TextBlock, phdr_index: u16) void {
         text_block.next = null;
     }
 
-    if (text_block.dbg_info_prev) |prev| {
-        prev.dbg_info_next = text_block.dbg_info_next;
-
-        // TODO the free list logic like we do for text blocks above
-    } else {
-        text_block.dbg_info_prev = null;
-    }
-
-    if (text_block.dbg_info_next) |next| {
-        next.dbg_info_prev = text_block.dbg_info_prev;
-    } else {
-        text_block.dbg_info_next = null;
+    if (self.dwarf) |*dw| {
+        dw.freeAtom(&text_block.dbg_info_atom);
     }
 }
 
@@ -2619,26 +2222,9 @@ pub fn freeDecl(self: *Elf, decl: *Module.Decl) void {
 
         self.offset_table_free_list.append(self.base.allocator, decl.link.elf.offset_table_index) catch {};
     }
-    // TODO make this logic match freeTextBlock. Maybe abstract the logic out since the same thing
-    // is desired for both.
-    _ = self.dbg_line_fn_free_list.remove(&decl.fn_link.elf);
-    if (decl.fn_link.elf.prev) |prev| {
-        self.dbg_line_fn_free_list.put(self.base.allocator, prev, {}) catch {};
-        prev.next = decl.fn_link.elf.next;
-        if (decl.fn_link.elf.next) |next| {
-            next.prev = prev;
-        } else {
-            self.dbg_line_fn_last = prev;
-        }
-    } else if (decl.fn_link.elf.next) |next| {
-        self.dbg_line_fn_first = next;
-        next.prev = null;
-    }
-    if (self.dbg_line_fn_first == &decl.fn_link.elf) {
-        self.dbg_line_fn_first = decl.fn_link.elf.next;
-    }
-    if (self.dbg_line_fn_last == &decl.fn_link.elf) {
-        self.dbg_line_fn_last = decl.fn_link.elf.prev;
+
+    if (self.dwarf) |*dw| {
+        dw.freeDecl(decl);
     }
 }
 
@@ -2739,62 +2325,6 @@ fn updateDeclCode(self: *Elf, decl: *Module.Decl, code: []const u8, stt_bits: u8
     return local_sym;
 }
 
-fn finishUpdateDecl(
-    self: *Elf,
-    module: *Module,
-    decl: *Module.Decl,
-    dbg_info_type_relocs: *File.DbgInfoTypeRelocsTable,
-    dbg_info_buffer: *std.ArrayList(u8),
-) !void {
-    // We need this for the duration of this function only so that for composite
-    // types such as []const u32, if the type *u32 is non-existent, we create
-    // it synthetically and store the backing bytes in this arena. After we are
-    // done with the relocations, we can safely deinit the entire memory slab.
-    // TODO currently, we do not store the relocations for future use, however,
-    // if that is the case, we should move memory management to a higher scope,
-    // such as linker scope, or whatnot.
-    var dbg_type_arena = std.heap.ArenaAllocator.init(self.base.allocator);
-    defer dbg_type_arena.deinit();
-
-    // Now we emit the .debug_info types of the Decl. These will count towards the size of
-    // the buffer, so we have to do it before computing the offset, and we can't perform the actual
-    // relocations yet.
-    {
-        var it: usize = 0;
-        while (it < dbg_info_type_relocs.count()) : (it += 1) {
-            const ty = dbg_info_type_relocs.keys()[it];
-            const value_ptr = dbg_info_type_relocs.getPtr(ty).?;
-            value_ptr.off = @intCast(u32, dbg_info_buffer.items.len);
-            try self.addDbgInfoType(dbg_type_arena.allocator(), ty, dbg_info_buffer, dbg_info_type_relocs);
-        }
-    }
-
-    const text_block = &decl.link.elf;
-    try self.updateDeclDebugInfoAllocation(text_block, @intCast(u32, dbg_info_buffer.items.len));
-
-    const target_endian = self.base.options.target.cpu.arch.endian();
-
-    {
-        // Now that we have the offset assigned we can finally perform type relocations.
-        for (dbg_info_type_relocs.values()) |value| {
-            for (value.relocs.items) |off| {
-                mem.writeInt(
-                    u32,
-                    dbg_info_buffer.items[off..][0..4],
-                    text_block.dbg_info_off + value.off,
-                    target_endian,
-                );
-            }
-        }
-    }
-
-    try self.writeDeclDebugInfo(text_block, dbg_info_buffer.items);
-
-    // Since we updated the vaddr and the size, each corresponding export symbol also needs to be updated.
-    const decl_exports = module.decl_exports.get(decl) orelse &[0]*Module.Export{};
-    return self.updateDeclExports(module, decl, decl_exports);
-}
-
 pub fn updateFunc(self: *Elf, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void {
     if (build_options.skip_non_native and builtin.object_format != .elf) {
         @panic("Attempted to compile for object format that was disabled by build configuration");
@@ -2809,96 +2339,33 @@ pub fn updateFunc(self: *Elf, module: *Module, func: *Module.Fn, air: Air, liven
     var code_buffer = std.ArrayList(u8).init(self.base.allocator);
     defer code_buffer.deinit();
 
-    // For functions we need to add a prologue to the debug line program.
-    var dbg_line_buffer = try std.ArrayList(u8).initCapacity(self.base.allocator, 26);
-    defer dbg_line_buffer.deinit();
-
-    var dbg_info_buffer = std.ArrayList(u8).init(self.base.allocator);
-    defer dbg_info_buffer.deinit();
-
-    var dbg_info_type_relocs: File.DbgInfoTypeRelocsTable = .{};
-    defer deinitRelocs(self.base.allocator, &dbg_info_type_relocs);
-
     const decl = func.owner_decl;
     self.freeUnnamedConsts(decl);
 
-    const decl_name = try decl.getFullyQualifiedName(self.base.allocator);
-    defer self.base.allocator.free(decl_name);
-
-    log.debug("updateFunc {s}{*}", .{ decl_name, func.owner_decl });
-    log.debug("  (decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d})", .{
-        decl.src_line,
-        func.lbrace_line,
-        func.rbrace_line,
-    });
-    const line = @intCast(u28, decl.src_line + func.lbrace_line);
-
-    const ptr_width_bytes = self.ptrWidthBytes();
-    dbg_line_buffer.appendSliceAssumeCapacity(&[_]u8{
-        DW.LNS.extended_op,
-        ptr_width_bytes + 1,
-        DW.LNE.set_address,
-    });
-    // This is the "relocatable" vaddr, corresponding to `code_buffer` index `0`.
-    assert(dbg_line_vaddr_reloc_index == dbg_line_buffer.items.len);
-    dbg_line_buffer.items.len += ptr_width_bytes;
-
-    dbg_line_buffer.appendAssumeCapacity(DW.LNS.advance_line);
-    // This is the "relocatable" relative line offset from the previous function's end curly
-    // to this function's begin curly.
-    assert(self.getRelocDbgLineOff() == dbg_line_buffer.items.len);
-    // Here we use a ULEB128-fixed-4 to make sure this field can be overwritten later.
-    leb128.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), line);
-
-    dbg_line_buffer.appendAssumeCapacity(DW.LNS.set_file);
-    assert(self.getRelocDbgFileIndex() == dbg_line_buffer.items.len);
-    // Once we support more than one source file, this will have the ability to be more
-    // than one possible value.
-    const file_index = 1;
-    leb128.writeUnsignedFixed(4, dbg_line_buffer.addManyAsArrayAssumeCapacity(4), file_index);
-
-    // Emit a line for the begin curly with prologue_end=false. The codegen will
-    // do the work of setting prologue_end=true and epilogue_begin=true.
-    dbg_line_buffer.appendAssumeCapacity(DW.LNS.copy);
-
-    // .debug_info subprogram
-    const decl_name_with_null = decl_name[0 .. decl_name.len + 1];
-    try dbg_info_buffer.ensureUnusedCapacity(25 + decl_name_with_null.len);
-
-    const fn_ret_type = decl.ty.fnReturnType();
-    const fn_ret_has_bits = fn_ret_type.hasRuntimeBits();
-    if (fn_ret_has_bits) {
-        dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram);
-    } else {
-        dbg_info_buffer.appendAssumeCapacity(abbrev_subprogram_retvoid);
-    }
-    // These get overwritten after generating the machine code. These values are
-    // "relocations" and have to be in this fixed place so that functions can be
-    // moved in virtual address space.
-    assert(dbg_info_low_pc_reloc_index == dbg_info_buffer.items.len);
-    dbg_info_buffer.items.len += ptr_width_bytes; // DW.AT.low_pc,  DW.FORM.addr
-    assert(self.getRelocDbgInfoSubprogramHighPC() == dbg_info_buffer.items.len);
-    dbg_info_buffer.items.len += 4; // DW.AT.high_pc,  DW.FORM.data4
-    if (fn_ret_has_bits) {
-        const gop = try dbg_info_type_relocs.getOrPut(self.base.allocator, fn_ret_type);
-        if (!gop.found_existing) {
-            gop.value_ptr.* = .{
-                .off = undefined,
-                .relocs = .{},
-            };
+    var debug_buffers_buf: Dwarf.DeclDebugBuffers = undefined;
+    const debug_buffers = if (self.dwarf) |*dw| blk: {
+        debug_buffers_buf = try dw.initDeclDebugInfo(decl);
+        break :blk &debug_buffers_buf;
+    } else null;
+    defer {
+        if (debug_buffers) |dbg| {
+            dbg.dbg_line_buffer.deinit();
+            dbg.dbg_info_buffer.deinit();
+            deinitRelocs(self.base.allocator, &dbg.dbg_info_type_relocs);
         }
-        try gop.value_ptr.relocs.append(self.base.allocator, @intCast(u32, dbg_info_buffer.items.len));
-        dbg_info_buffer.items.len += 4; // DW.AT.type,  DW.FORM.ref4
     }
-    dbg_info_buffer.appendSliceAssumeCapacity(decl_name_with_null); // DW.AT.name, DW.FORM.string
 
-    const res = try codegen.generateFunction(&self.base, decl.srcLoc(), func, air, liveness, &code_buffer, .{
-        .dwarf = .{
-            .dbg_line = &dbg_line_buffer,
-            .dbg_info = &dbg_info_buffer,
-            .dbg_info_type_relocs = &dbg_info_type_relocs,
-        },
-    });
+    const res = if (debug_buffers) |dbg|
+        try codegen.generateFunction(&self.base, decl.srcLoc(), func, air, liveness, &code_buffer, .{
+            .dwarf = .{
+                .dbg_line = &dbg.dbg_line_buffer,
+                .dbg_info = &dbg.dbg_info_buffer,
+                .dbg_info_type_relocs = &dbg.dbg_info_type_relocs,
+            },
+        })
+    else
+        try codegen.generateFunction(&self.base, decl.srcLoc(), func, air, liveness, &code_buffer, .none);
+
     const code = switch (res) {
         .appended => code_buffer.items,
         .fail => |em| {
@@ -2907,128 +2374,14 @@ pub fn updateFunc(self: *Elf, module: *Module, func: *Module.Fn, air: Air, liven
             return;
         },
     };
-
     const local_sym = try self.updateDeclCode(decl, code, elf.STT_FUNC);
-
-    const target_endian = self.base.options.target.cpu.arch.endian();
-
-    // Since the Decl is a function, we need to update the .debug_line program.
-    // Perform the relocations based on vaddr.
-    switch (self.ptr_width) {
-        .p32 => {
-            {
-                const ptr = dbg_line_buffer.items[dbg_line_vaddr_reloc_index..][0..4];
-                mem.writeInt(u32, ptr, @intCast(u32, local_sym.st_value), target_endian);
-            }
-            {
-                const ptr = dbg_info_buffer.items[dbg_info_low_pc_reloc_index..][0..4];
-                mem.writeInt(u32, ptr, @intCast(u32, local_sym.st_value), target_endian);
-            }
-        },
-        .p64 => {
-            {
-                const ptr = dbg_line_buffer.items[dbg_line_vaddr_reloc_index..][0..8];
-                mem.writeInt(u64, ptr, local_sym.st_value, target_endian);
-            }
-            {
-                const ptr = dbg_info_buffer.items[dbg_info_low_pc_reloc_index..][0..8];
-                mem.writeInt(u64, ptr, local_sym.st_value, target_endian);
-            }
-        },
+    if (debug_buffers) |dbg| {
+        try self.dwarf.?.commitDeclDebugInfo(&self.base, module, decl, local_sym.st_value, local_sym.st_size, dbg);
     }
-    {
-        const ptr = dbg_info_buffer.items[self.getRelocDbgInfoSubprogramHighPC()..][0..4];
-        mem.writeInt(u32, ptr, @intCast(u32, local_sym.st_size), target_endian);
-    }
-
-    try dbg_line_buffer.appendSlice(&[_]u8{ DW.LNS.extended_op, 1, DW.LNE.end_sequence });
-
-    // Now we have the full contents and may allocate a region to store it.
-
-    // This logic is nearly identical to the logic below in `updateDeclDebugInfoAllocation` for
-    // `TextBlock` and the .debug_info. If you are editing this logic, you
-    // probably need to edit that logic too.
-
-    const debug_line_sect = &self.sections.items[self.debug_line_section_index.?];
-    const src_fn = &decl.fn_link.elf;
-    src_fn.len = @intCast(u32, dbg_line_buffer.items.len);
-    if (self.dbg_line_fn_last) |last| not_first: {
-        if (src_fn.next) |next| {
-            // Update existing function - non-last item.
-            if (src_fn.off + src_fn.len + min_nop_size > next.off) {
-                // It grew too big, so we move it to a new location.
-                if (src_fn.prev) |prev| {
-                    self.dbg_line_fn_free_list.put(self.base.allocator, prev, {}) catch {};
-                    prev.next = src_fn.next;
-                }
-                assert(src_fn.prev != next);
-                next.prev = src_fn.prev;
-                src_fn.next = null;
-                // Populate where it used to be with NOPs.
-                const file_pos = debug_line_sect.sh_offset + src_fn.off;
-                try self.pwriteDbgLineNops(0, &[0]u8{}, src_fn.len, file_pos);
-                // TODO Look at the free list before appending at the end.
-                src_fn.prev = last;
-                last.next = src_fn;
-                self.dbg_line_fn_last = src_fn;
-
-                src_fn.off = last.off + padToIdeal(last.len);
-            }
-        } else if (src_fn.prev == null) {
-            if (src_fn == last) {
-                // Special case: there is only 1 function and it is being updated.
-                // In this case there is nothing to do. The function's length has
-                // already been updated, and the logic below takes care of
-                // resizing the .debug_line section.
-                break :not_first;
-            }
-            // Append new function.
-            // TODO Look at the free list before appending at the end.
-            src_fn.prev = last;
-            last.next = src_fn;
-            self.dbg_line_fn_last = src_fn;
 
-            src_fn.off = last.off + padToIdeal(last.len);
-        }
-    } else {
-        // This is the first function of the Line Number Program.
-        self.dbg_line_fn_first = src_fn;
-        self.dbg_line_fn_last = src_fn;
-
-        src_fn.off = padToIdeal(self.dbgLineNeededHeaderBytes());
-    }
-
-    const last_src_fn = self.dbg_line_fn_last.?;
-    const needed_size = last_src_fn.off + last_src_fn.len;
-    if (needed_size != debug_line_sect.sh_size) {
-        if (needed_size > self.allocatedSize(debug_line_sect.sh_offset)) {
-            const new_offset = self.findFreeSpace(needed_size, 1);
-            const existing_size = last_src_fn.off;
-            log.debug("moving .debug_line section: {d} bytes from 0x{x} to 0x{x}", .{
-                existing_size,
-                debug_line_sect.sh_offset,
-                new_offset,
-            });
-            const amt = try self.base.file.?.copyRangeAll(debug_line_sect.sh_offset, self.base.file.?, new_offset, existing_size);
-            if (amt != existing_size) return error.InputOutput;
-            debug_line_sect.sh_offset = new_offset;
-        }
-        debug_line_sect.sh_size = needed_size;
-        self.shdr_table_dirty = true; // TODO look into making only the one section dirty
-        self.debug_line_header_dirty = true;
-    }
-    const prev_padding_size: u32 = if (src_fn.prev) |prev| src_fn.off - (prev.off + prev.len) else 0;
-    const next_padding_size: u32 = if (src_fn.next) |next| next.off - (src_fn.off + src_fn.len) else 0;
-
-    // We only have support for one compilation unit so far, so the offsets are directly
-    // from the .debug_line section.
-    const file_pos = debug_line_sect.sh_offset + src_fn.off;
-    try self.pwriteDbgLineNops(prev_padding_size, dbg_line_buffer.items, next_padding_size, file_pos);
-
-    // .debug_info - End the TAG.subprogram children.
-    try dbg_info_buffer.append(0);
-
-    return self.finishUpdateDecl(module, decl, &dbg_info_type_relocs, &dbg_info_buffer);
+    // Since we updated the vaddr and the size, each corresponding export symbol also needs to be updated.
+    const decl_exports = module.decl_exports.get(decl) orelse &[0]*Module.Export{};
+    return self.updateDeclExports(module, decl, decl_exports);
 }
 
 pub fn updateDecl(self: *Elf, module: *Module, decl: *Module.Decl) !void {
@@ -3057,29 +2410,42 @@ pub fn updateDecl(self: *Elf, module: *Module, decl: *Module.Decl) !void {
     var code_buffer = std.ArrayList(u8).init(self.base.allocator);
     defer code_buffer.deinit();
 
-    var dbg_line_buffer = std.ArrayList(u8).init(self.base.allocator);
-    defer dbg_line_buffer.deinit();
-
-    var dbg_info_buffer = std.ArrayList(u8).init(self.base.allocator);
-    defer dbg_info_buffer.deinit();
-
-    var dbg_info_type_relocs: File.DbgInfoTypeRelocsTable = .{};
-    defer deinitRelocs(self.base.allocator, &dbg_info_type_relocs);
+    var debug_buffers_buf: Dwarf.DeclDebugBuffers = undefined;
+    const debug_buffers = if (self.dwarf) |*dw| blk: {
+        debug_buffers_buf = try dw.initDeclDebugInfo(decl);
+        break :blk &debug_buffers_buf;
+    } else null;
+    defer {
+        if (debug_buffers) |dbg| {
+            dbg.dbg_line_buffer.deinit();
+            dbg.dbg_info_buffer.deinit();
+            deinitRelocs(self.base.allocator, &dbg.dbg_info_type_relocs);
+        }
+    }
 
     // TODO implement .debug_info for global variables
     const decl_val = if (decl.val.castTag(.variable)) |payload| payload.data.init else decl.val;
-    const res = try codegen.generateSymbol(&self.base, decl.srcLoc(), .{
-        .ty = decl.ty,
-        .val = decl_val,
-    }, &code_buffer, .{
-        .dwarf = .{
-            .dbg_line = &dbg_line_buffer,
-            .dbg_info = &dbg_info_buffer,
-            .dbg_info_type_relocs = &dbg_info_type_relocs,
-        },
-    }, .{
-        .parent_atom_index = decl.link.elf.local_sym_index,
-    });
+    const res = if (debug_buffers) |dbg|
+        try codegen.generateSymbol(&self.base, decl.srcLoc(), .{
+            .ty = decl.ty,
+            .val = decl_val,
+        }, &code_buffer, .{
+            .dwarf = .{
+                .dbg_line = &dbg.dbg_line_buffer,
+                .dbg_info = &dbg.dbg_info_buffer,
+                .dbg_info_type_relocs = &dbg.dbg_info_type_relocs,
+            },
+        }, .{
+            .parent_atom_index = decl.link.elf.local_sym_index,
+        })
+    else
+        try codegen.generateSymbol(&self.base, decl.srcLoc(), .{
+            .ty = decl.ty,
+            .val = decl_val,
+        }, &code_buffer, .none, .{
+            .parent_atom_index = decl.link.elf.local_sym_index,
+        });
+
     const code = switch (res) {
         .externally_managed => |x| x,
         .appended => code_buffer.items,
@@ -3090,8 +2456,14 @@ pub fn updateDecl(self: *Elf, module: *Module, decl: *Module.Decl) !void {
         },
     };
 
-    _ = try self.updateDeclCode(decl, code, elf.STT_OBJECT);
-    return self.finishUpdateDecl(module, decl, &dbg_info_type_relocs, &dbg_info_buffer);
+    const local_sym = try self.updateDeclCode(decl, code, elf.STT_OBJECT);
+    if (debug_buffers) |dbg| {
+        try self.dwarf.?.commitDeclDebugInfo(&self.base, module, decl, local_sym.st_value, local_sym.st_size, dbg);
+    }
+
+    // Since we updated the vaddr and the size, each corresponding export symbol also needs to be updated.
+    const decl_exports = module.decl_exports.get(decl) orelse &[0]*Module.Export{};
+    return self.updateDeclExports(module, decl, decl_exports);
 }
 
 pub fn lowerUnnamedConst(self: *Elf, typed_value: TypedValue, decl: *Module.Decl) !u32 {
@@ -3170,304 +2542,6 @@ pub fn lowerUnnamedConst(self: *Elf, typed_value: TypedValue, decl: *Module.Decl
     return atom.local_sym_index;
 }
 
-/// Asserts the type has codegen bits.
-fn addDbgInfoType(
-    self: *Elf,
-    arena: Allocator,
-    ty: Type,
-    dbg_info_buffer: *std.ArrayList(u8),
-    dbg_info_type_relocs: *File.DbgInfoTypeRelocsTable,
-) error{OutOfMemory}!void {
-    const target = self.base.options.target;
-    var relocs = std.ArrayList(struct { ty: Type, reloc: u32 }).init(arena);
-
-    switch (ty.zigTypeTag()) {
-        .NoReturn => unreachable,
-        .Void => {
-            try dbg_info_buffer.append(abbrev_pad1);
-        },
-        .Bool => {
-            try dbg_info_buffer.appendSlice(&[_]u8{
-                abbrev_base_type,
-                DW.ATE.boolean, // DW.AT.encoding ,  DW.FORM.data1
-                1, // DW.AT.byte_size,  DW.FORM.data1
-                'b', 'o', 'o', 'l', 0, // DW.AT.name,  DW.FORM.string
-            });
-        },
-        .Int => {
-            const info = ty.intInfo(target);
-            try dbg_info_buffer.ensureUnusedCapacity(12);
-            dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
-            // DW.AT.encoding, DW.FORM.data1
-            dbg_info_buffer.appendAssumeCapacity(switch (info.signedness) {
-                .signed => DW.ATE.signed,
-                .unsigned => DW.ATE.unsigned,
-            });
-            // DW.AT.byte_size,  DW.FORM.data1
-            dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
-            // DW.AT.name,  DW.FORM.string
-            try dbg_info_buffer.writer().print("{}\x00", .{ty});
-        },
-        .Optional => {
-            if (ty.isPtrLikeOptional()) {
-                try dbg_info_buffer.ensureUnusedCapacity(12);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_base_type);
-                // DW.AT.encoding, DW.FORM.data1
-                dbg_info_buffer.appendAssumeCapacity(DW.ATE.address);
-                // DW.AT.byte_size,  DW.FORM.data1
-                dbg_info_buffer.appendAssumeCapacity(@intCast(u8, ty.abiSize(target)));
-                // DW.AT.name,  DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-            } else {
-                // Non-pointer optionals are structs: struct { .maybe = *, .val = * }
-                var buf = try arena.create(Type.Payload.ElemType);
-                const payload_ty = ty.optionalChild(buf);
-                // DW.AT.structure_type
-                try dbg_info_buffer.append(abbrev_struct_type);
-                // DW.AT.byte_size, DW.FORM.sdata
-                const abi_size = ty.abiSize(target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
-                // DW.AT.name, DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(7);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("maybe");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = Type.bool, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(6);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.member
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("val");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = payload_ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                const offset = abi_size - payload_ty.abiSize(target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), offset);
-                // DW.AT.structure_type delimit children
-                try dbg_info_buffer.append(0);
-            }
-        },
-        .Pointer => {
-            if (ty.isSlice()) {
-                // Slices are structs: struct { .ptr = *, .len = N }
-                // DW.AT.structure_type
-                try dbg_info_buffer.ensureUnusedCapacity(2);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_type);
-                // DW.AT.byte_size, DW.FORM.sdata
-                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize) * 2);
-                // DW.AT.name, DW.FORM.string
-                try dbg_info_buffer.writer().print("{}\x00", .{ty});
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(5);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("ptr");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                var buf = try arena.create(Type.SlicePtrFieldTypeBuffer);
-                const ptr_ty = ty.slicePtrFieldType(buf);
-                try relocs.append(.{ .ty = ptr_ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(6);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.member
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity("len");
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = Type.initTag(.usize), .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                try dbg_info_buffer.ensureUnusedCapacity(2);
-                dbg_info_buffer.appendAssumeCapacity(@sizeOf(usize));
-                // DW.AT.structure_type delimit children
-                dbg_info_buffer.appendAssumeCapacity(0);
-            } else {
-                try dbg_info_buffer.ensureUnusedCapacity(5);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_ptr_type);
-                // DW.AT.type, DW.FORM.ref4
-                const index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = ty.childType(), .reloc = @intCast(u32, index) });
-            }
-        },
-        .Struct => blk: {
-            // try dbg_info_buffer.ensureUnusedCapacity(23);
-            // DW.AT.structure_type
-            try dbg_info_buffer.append(abbrev_struct_type);
-            // DW.AT.byte_size, DW.FORM.sdata
-            const abi_size = ty.abiSize(target);
-            try leb128.writeULEB128(dbg_info_buffer.writer(), abi_size);
-            // DW.AT.name, DW.FORM.string
-            const struct_name = try ty.nameAlloc(arena);
-            try dbg_info_buffer.ensureUnusedCapacity(struct_name.len + 1);
-            dbg_info_buffer.appendSliceAssumeCapacity(struct_name);
-            dbg_info_buffer.appendAssumeCapacity(0);
-
-            const struct_obj = ty.castTag(.@"struct").?.data;
-            if (struct_obj.layout == .Packed) {
-                log.debug("TODO implement .debug_info for packed structs", .{});
-                break :blk;
-            }
-
-            const fields = ty.structFields();
-            for (fields.keys()) |field_name, field_index| {
-                const field = fields.get(field_name).?;
-                // DW.AT.member
-                try dbg_info_buffer.ensureUnusedCapacity(field_name.len + 2);
-                dbg_info_buffer.appendAssumeCapacity(abbrev_struct_member);
-                // DW.AT.name, DW.FORM.string
-                dbg_info_buffer.appendSliceAssumeCapacity(field_name);
-                dbg_info_buffer.appendAssumeCapacity(0);
-                // DW.AT.type, DW.FORM.ref4
-                var index = dbg_info_buffer.items.len;
-                try dbg_info_buffer.resize(index + 4);
-                try relocs.append(.{ .ty = field.ty, .reloc = @intCast(u32, index) });
-                // DW.AT.data_member_location, DW.FORM.sdata
-                const field_off = ty.structFieldOffset(field_index, target);
-                try leb128.writeULEB128(dbg_info_buffer.writer(), field_off);
-            }
-
-            // DW.AT.structure_type delimit children
-            try dbg_info_buffer.append(0);
-        },
-        else => {
-            log.debug("TODO implement .debug_info for type '{}'", .{ty});
-            try dbg_info_buffer.append(abbrev_pad1);
-        },
-    }
-
-    for (relocs.items) |rel| {
-        const gop = try dbg_info_type_relocs.getOrPut(self.base.allocator, rel.ty);
-        if (!gop.found_existing) {
-            gop.value_ptr.* = .{
-                .off = undefined,
-                .relocs = .{},
-            };
-        }
-        try gop.value_ptr.relocs.append(self.base.allocator, rel.reloc);
-    }
-}
-
-fn updateDeclDebugInfoAllocation(self: *Elf, text_block: *TextBlock, len: u32) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    // This logic is nearly identical to the logic above in `updateDecl` for
-    // `SrcFn` and the line number programs. If you are editing this logic, you
-    // probably need to edit that logic too.
-
-    const debug_info_sect = &self.sections.items[self.debug_info_section_index.?];
-    text_block.dbg_info_len = len;
-    if (self.dbg_info_decl_last) |last| not_first: {
-        if (text_block.dbg_info_next) |next| {
-            // Update existing Decl - non-last item.
-            if (text_block.dbg_info_off + text_block.dbg_info_len + min_nop_size > next.dbg_info_off) {
-                // It grew too big, so we move it to a new location.
-                if (text_block.dbg_info_prev) |prev| {
-                    self.dbg_info_decl_free_list.put(self.base.allocator, prev, {}) catch {};
-                    prev.dbg_info_next = text_block.dbg_info_next;
-                }
-                next.dbg_info_prev = text_block.dbg_info_prev;
-                text_block.dbg_info_next = null;
-                // Populate where it used to be with NOPs.
-                const file_pos = debug_info_sect.sh_offset + text_block.dbg_info_off;
-                try self.pwriteDbgInfoNops(0, &[0]u8{}, text_block.dbg_info_len, false, file_pos);
-                // TODO Look at the free list before appending at the end.
-                text_block.dbg_info_prev = last;
-                last.dbg_info_next = text_block;
-                self.dbg_info_decl_last = text_block;
-
-                text_block.dbg_info_off = last.dbg_info_off + padToIdeal(last.dbg_info_len);
-            }
-        } else if (text_block.dbg_info_prev == null) {
-            if (text_block == last) {
-                // Special case: there is only 1 .debug_info block and it is being updated.
-                // In this case there is nothing to do. The block's length has
-                // already been updated, and logic in writeDeclDebugInfo takes care of
-                // resizing the .debug_info section.
-                break :not_first;
-            }
-            // Append new Decl.
-            // TODO Look at the free list before appending at the end.
-            text_block.dbg_info_prev = last;
-            last.dbg_info_next = text_block;
-            self.dbg_info_decl_last = text_block;
-
-            text_block.dbg_info_off = last.dbg_info_off + padToIdeal(last.dbg_info_len);
-        }
-    } else {
-        // This is the first Decl of the .debug_info
-        self.dbg_info_decl_first = text_block;
-        self.dbg_info_decl_last = text_block;
-
-        text_block.dbg_info_off = padToIdeal(self.dbgInfoNeededHeaderBytes());
-    }
-}
-
-fn writeDeclDebugInfo(self: *Elf, text_block: *TextBlock, dbg_info_buf: []const u8) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    // This logic is nearly identical to the logic above in `updateDecl` for
-    // `SrcFn` and the line number programs. If you are editing this logic, you
-    // probably need to edit that logic too.
-
-    const debug_info_sect = &self.sections.items[self.debug_info_section_index.?];
-
-    const last_decl = self.dbg_info_decl_last.?;
-    // +1 for a trailing zero to end the children of the decl tag.
-    const needed_size = last_decl.dbg_info_off + last_decl.dbg_info_len + 1;
-    if (needed_size != debug_info_sect.sh_size) {
-        if (needed_size > self.allocatedSize(debug_info_sect.sh_offset)) {
-            const new_offset = self.findFreeSpace(needed_size, 1);
-            const existing_size = last_decl.dbg_info_off;
-            log.debug("moving .debug_info section: {} bytes from 0x{x} to 0x{x}", .{
-                existing_size,
-                debug_info_sect.sh_offset,
-                new_offset,
-            });
-            const amt = try self.base.file.?.copyRangeAll(debug_info_sect.sh_offset, self.base.file.?, new_offset, existing_size);
-            if (amt != existing_size) return error.InputOutput;
-            debug_info_sect.sh_offset = new_offset;
-        }
-        debug_info_sect.sh_size = needed_size;
-        self.shdr_table_dirty = true; // TODO look into making only the one section dirty
-        self.debug_info_header_dirty = true;
-    }
-    const prev_padding_size: u32 = if (text_block.dbg_info_prev) |prev|
-        text_block.dbg_info_off - (prev.dbg_info_off + prev.dbg_info_len)
-    else
-        0;
-    const next_padding_size: u32 = if (text_block.dbg_info_next) |next|
-        next.dbg_info_off - (text_block.dbg_info_off + text_block.dbg_info_len)
-    else
-        0;
-
-    // To end the children of the decl tag.
-    const trailing_zero = text_block.dbg_info_next == null;
-
-    // We only have support for one compilation unit so far, so the offsets are directly
-    // from the .debug_info section.
-    const file_pos = debug_info_sect.sh_offset + text_block.dbg_info_off;
-    try self.pwriteDbgInfoNops(prev_padding_size, dbg_info_buf, next_padding_size, trailing_zero, file_pos);
-}
-
 pub fn updateDeclExports(
     self: *Elf,
     module: *Module,
@@ -3568,20 +2642,9 @@ pub fn updateDeclLineNumber(self: *Elf, module: *Module, decl: *const Module.Dec
     log.debug("updateDeclLineNumber {s}{*}", .{ decl_name, decl });
 
     if (self.llvm_object) |_| return;
-
-    const func = decl.val.castTag(.function).?.data;
-    log.debug("  (decl.src_line={d}, func.lbrace_line={d}, func.rbrace_line={d})", .{
-        decl.src_line,
-        func.lbrace_line,
-        func.rbrace_line,
-    });
-    const line = @intCast(u28, decl.src_line + func.lbrace_line);
-
-    const shdr = &self.sections.items[self.debug_line_section_index.?];
-    const file_pos = shdr.sh_offset + decl.fn_link.elf.off + self.getRelocDbgLineOff();
-    var data: [4]u8 = undefined;
-    leb128.writeUnsignedFixed(4, &data, line);
-    try self.base.file.?.pwriteAll(&data, file_pos);
+    if (self.dwarf) |*dw| {
+        try dw.updateDeclLineNumber(&self.base, decl);
+    }
 }
 
 pub fn deleteExport(self: *Elf, exp: Export) void {
@@ -3806,201 +2869,6 @@ fn archPtrWidthBytes(self: Elf) u8 {
     return @intCast(u8, self.base.options.target.cpu.arch.ptrBitWidth() / 8);
 }
 
-/// The reloc offset for the virtual address of a function in its Line Number Program.
-/// Size is a virtual address integer.
-const dbg_line_vaddr_reloc_index = 3;
-/// The reloc offset for the virtual address of a function in its .debug_info TAG.subprogram.
-/// Size is a virtual address integer.
-const dbg_info_low_pc_reloc_index = 1;
-
-/// The reloc offset for the line offset of a function from the previous function's line.
-/// It's a fixed-size 4-byte ULEB128.
-fn getRelocDbgLineOff(self: Elf) usize {
-    return dbg_line_vaddr_reloc_index + self.ptrWidthBytes() + 1;
-}
-
-fn getRelocDbgFileIndex(self: Elf) usize {
-    return self.getRelocDbgLineOff() + 5;
-}
-
-fn getRelocDbgInfoSubprogramHighPC(self: Elf) u32 {
-    return dbg_info_low_pc_reloc_index + self.ptrWidthBytes();
-}
-
-fn dbgLineNeededHeaderBytes(self: Elf) u32 {
-    const directory_entry_format_count = 1;
-    const file_name_entry_format_count = 1;
-    const directory_count = 1;
-    const file_name_count = 1;
-    const root_src_dir_path_len = if (self.base.options.module.?.root_pkg.root_src_directory.path) |p| p.len else 1; // "."
-    return @intCast(u32, 53 + directory_entry_format_count * 2 + file_name_entry_format_count * 2 +
-        directory_count * 8 + file_name_count * 8 +
-        // These are encoded as DW.FORM.string rather than DW.FORM.strp as we would like
-        // because of a workaround for readelf and gdb failing to understand DWARFv5 correctly.
-        root_src_dir_path_len +
-        self.base.options.module.?.root_pkg.root_src_path.len);
-}
-
-fn dbgInfoNeededHeaderBytes(self: Elf) u32 {
-    _ = self;
-    return 120;
-}
-
-const min_nop_size = 2;
-
-/// Writes to the file a buffer, prefixed and suffixed by the specified number of
-/// bytes of NOPs. Asserts each padding size is at least `min_nop_size` and total padding bytes
-/// are less than 1044480 bytes (if this limit is ever reached, this function can be
-/// improved to make more than one pwritev call, or the limit can be raised by a fixed
-/// amount by increasing the length of `vecs`).
-fn pwriteDbgLineNops(
-    self: *Elf,
-    prev_padding_size: usize,
-    buf: []const u8,
-    next_padding_size: usize,
-    offset: u64,
-) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const page_of_nops = [1]u8{DW.LNS.negate_stmt} ** 4096;
-    const three_byte_nop = [3]u8{ DW.LNS.advance_pc, 0b1000_0000, 0 };
-    var vecs: [512]std.os.iovec_const = undefined;
-    var vec_index: usize = 0;
-    {
-        var padding_left = prev_padding_size;
-        if (padding_left % 2 != 0) {
-            vecs[vec_index] = .{
-                .iov_base = &three_byte_nop,
-                .iov_len = three_byte_nop.len,
-            };
-            vec_index += 1;
-            padding_left -= three_byte_nop.len;
-        }
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    vecs[vec_index] = .{
-        .iov_base = buf.ptr,
-        .iov_len = buf.len,
-    };
-    vec_index += 1;
-
-    {
-        var padding_left = next_padding_size;
-        if (padding_left % 2 != 0) {
-            vecs[vec_index] = .{
-                .iov_base = &three_byte_nop,
-                .iov_len = three_byte_nop.len,
-            };
-            vec_index += 1;
-            padding_left -= three_byte_nop.len;
-        }
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-    try self.base.file.?.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
-}
-
-/// Writes to the file a buffer, prefixed and suffixed by the specified number of
-/// bytes of padding.
-fn pwriteDbgInfoNops(
-    self: *Elf,
-    prev_padding_size: usize,
-    buf: []const u8,
-    next_padding_size: usize,
-    trailing_zero: bool,
-    offset: u64,
-) !void {
-    const tracy = trace(@src());
-    defer tracy.end();
-
-    const page_of_nops = [1]u8{abbrev_pad1} ** 4096;
-    var vecs: [32]std.os.iovec_const = undefined;
-    var vec_index: usize = 0;
-    {
-        var padding_left = prev_padding_size;
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    vecs[vec_index] = .{
-        .iov_base = buf.ptr,
-        .iov_len = buf.len,
-    };
-    vec_index += 1;
-
-    {
-        var padding_left = next_padding_size;
-        while (padding_left > page_of_nops.len) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = page_of_nops.len,
-            };
-            vec_index += 1;
-            padding_left -= page_of_nops.len;
-        }
-        if (padding_left > 0) {
-            vecs[vec_index] = .{
-                .iov_base = &page_of_nops,
-                .iov_len = padding_left,
-            };
-            vec_index += 1;
-        }
-    }
-
-    if (trailing_zero) {
-        var zbuf = [1]u8{0};
-        vecs[vec_index] = .{
-            .iov_base = &zbuf,
-            .iov_len = zbuf.len,
-        };
-        vec_index += 1;
-    }
-
-    try self.base.file.?.pwritevAll(vecs[0..vec_index], offset - prev_padding_size);
-}
-
 fn progHeaderTo32(phdr: elf.Elf64_Phdr) elf.Elf32_Phdr {
     return .{
         .p_type = phdr.p_type,
src/link/MachO.zig
@@ -295,26 +295,6 @@ pub const Export = struct {
     sym_index: ?u32 = null,
 };
 
-pub const SrcFn = struct {
-    /// Offset from the beginning of the Debug Line Program header that contains this function.
-    off: u32,
-    /// Size of the line number program component belonging to this function, not
-    /// including padding.
-    len: u32,
-
-    /// Points to the previous and next neighbors, based on the offset from .debug_line.
-    /// This can be used to find, for example, the capacity of this `SrcFn`.
-    prev: ?*SrcFn,
-    next: ?*SrcFn,
-
-    pub const empty: SrcFn = .{
-        .off = 0,
-        .len = 0,
-        .prev = null,
-        .next = null,
-    };
-};
-
 pub fn openPath(allocator: Allocator, options: link.Options) !*MachO {
     assert(options.object_format == .macho);
 
@@ -376,6 +356,7 @@ pub fn openPath(allocator: Allocator, options: link.Options) !*MachO {
 
         self.d_sym = .{
             .base = self,
+            .dwarf = link.File.Dwarf.init(allocator, .macho, options.target),
             .file = d_sym_file,
         };
     }
@@ -3523,7 +3504,6 @@ fn freeAtom(self: *MachO, atom: *Atom, match: MatchingSection, owns_atom: bool)
             i += 1;
         }
     }
-    // TODO process free list for dbg info just like we do above for vaddrs
 
     if (self.atoms.getPtr(match)) |last_atom| {
         if (last_atom.* == atom) {
@@ -3536,16 +3516,6 @@ fn freeAtom(self: *MachO, atom: *Atom, match: MatchingSection, owns_atom: bool)
         }
     }
 
-    if (self.d_sym) |*d_sym| {
-        if (d_sym.dbg_info_decl_first == atom) {
-            d_sym.dbg_info_decl_first = atom.dbg_info_next;
-        }
-        if (d_sym.dbg_info_decl_last == atom) {
-            // TODO shrink the .debug_info section size here
-            d_sym.dbg_info_decl_last = atom.dbg_info_prev;
-        }
-    }
-
     if (atom.prev) |prev| {
         prev.next = atom.next;
 
@@ -3564,18 +3534,8 @@ fn freeAtom(self: *MachO, atom: *Atom, match: MatchingSection, owns_atom: bool)
         atom.next = null;
     }
 
-    if (atom.dbg_info_prev) |prev| {
-        prev.dbg_info_next = atom.dbg_info_next;
-
-        // TODO the free list logic like we do for atoms above
-    } else {
-        atom.dbg_info_prev = null;
-    }
-
-    if (atom.dbg_info_next) |next| {
-        next.dbg_info_prev = atom.dbg_info_prev;
-    } else {
-        atom.dbg_info_next = null;
+    if (self.d_sym) |*d_sym| {
+        d_sym.dwarf.freeAtom(&atom.dbg_info_atom);
     }
 }
 
@@ -3725,9 +3685,9 @@ pub fn updateFunc(self: *MachO, module: *Module, func: *Module.Fn, air: Air, liv
     var code_buffer = std.ArrayList(u8).init(self.base.allocator);
     defer code_buffer.deinit();
 
-    var debug_buffers_buf: DebugSymbols.DeclDebugBuffers = undefined;
+    var debug_buffers_buf: link.File.Dwarf.DeclDebugBuffers = undefined;
     const debug_buffers = if (self.d_sym) |*d_sym| blk: {
-        debug_buffers_buf = try d_sym.initDeclDebugBuffers(self.base.allocator, module, decl);
+        debug_buffers_buf = try d_sym.initDeclDebugInfo(module, decl);
         break :blk &debug_buffers_buf;
     } else null;
     defer {
@@ -3766,7 +3726,7 @@ pub fn updateFunc(self: *MachO, module: *Module, func: *Module.Fn, air: Air, liv
 
     if (debug_buffers) |db| {
         if (self.d_sym) |*d_sym| {
-            try d_sym.commitDeclDebugInfo(self.base.allocator, module, decl, db);
+            try d_sym.commitDeclDebugInfo(module, decl, db);
         }
     }
 
@@ -3805,9 +3765,7 @@ pub fn lowerUnnamedConst(self: *MachO, typed_value: TypedValue, decl: *Module.De
     const atom = try self.createEmptyAtom(local_sym_index, @sizeOf(u64), math.log2(required_alignment));
     try self.atom_by_index_table.putNoClobber(self.base.allocator, local_sym_index, atom);
 
-    const res = try codegen.generateSymbol(&self.base, decl.srcLoc(), typed_value, &code_buffer, .{
-        .none = .{},
-    }, .{
+    const res = try codegen.generateSymbol(&self.base, decl.srcLoc(), typed_value, &code_buffer, .none, .{
         .parent_atom_index = local_sym_index,
     });
     const code = switch (res) {
@@ -3869,9 +3827,9 @@ pub fn updateDecl(self: *MachO, module: *Module, decl: *Module.Decl) !void {
     var code_buffer = std.ArrayList(u8).init(self.base.allocator);
     defer code_buffer.deinit();
 
-    var debug_buffers_buf: DebugSymbols.DeclDebugBuffers = undefined;
+    var debug_buffers_buf: link.File.Dwarf.DeclDebugBuffers = undefined;
     const debug_buffers = if (self.d_sym) |*d_sym| blk: {
-        debug_buffers_buf = try d_sym.initDeclDebugBuffers(self.base.allocator, module, decl);
+        debug_buffers_buf = try d_sym.initDeclDebugInfo(module, decl);
         break :blk &debug_buffers_buf;
     } else null;
     defer {
@@ -4364,27 +4322,7 @@ pub fn freeDecl(self: *MachO, decl: *Module.Decl) void {
         decl.link.macho.local_sym_index = 0;
     }
     if (self.d_sym) |*d_sym| {
-        // TODO make this logic match freeAtom. Maybe abstract the logic
-        // out since the same thing is desired for both.
-        _ = d_sym.dbg_line_fn_free_list.remove(&decl.fn_link.macho);
-        if (decl.fn_link.macho.prev) |prev| {
-            d_sym.dbg_line_fn_free_list.put(self.base.allocator, prev, {}) catch {};
-            prev.next = decl.fn_link.macho.next;
-            if (decl.fn_link.macho.next) |next| {
-                next.prev = prev;
-            } else {
-                d_sym.dbg_line_fn_last = prev;
-            }
-        } else if (decl.fn_link.macho.next) |next| {
-            d_sym.dbg_line_fn_first = next;
-            next.prev = null;
-        }
-        if (d_sym.dbg_line_fn_first == &decl.fn_link.macho) {
-            d_sym.dbg_line_fn_first = decl.fn_link.macho.next;
-        }
-        if (d_sym.dbg_line_fn_last == &decl.fn_link.macho) {
-            d_sym.dbg_line_fn_last = decl.fn_link.macho.prev;
-        }
+        d_sym.dwarf.freeDecl(decl);
     }
 }
 
src/link.zig
@@ -221,9 +221,9 @@ pub const File = struct {
     };
 
     pub const LinkFn = union {
-        elf: Elf.SrcFn,
+        elf: Dwarf.SrcFn,
         coff: Coff.SrcFn,
-        macho: MachO.SrcFn,
+        macho: Dwarf.SrcFn,
         plan9: void,
         c: void,
         wasm: Wasm.FnData,
@@ -915,6 +915,7 @@ pub const File = struct {
     pub const SpirV = @import("link/SpirV.zig");
     pub const Wasm = @import("link/Wasm.zig");
     pub const NvPtx = @import("link/NvPtx.zig");
+    pub const Dwarf = @import("link/Dwarf.zig");
 };
 
 pub fn determineMode(options: Options) fs.File.Mode {
src/Module.zig
@@ -4439,8 +4439,8 @@ pub fn clearDecl(
             };
             decl.fn_link = switch (mod.comp.bin_file.tag) {
                 .coff => .{ .coff = {} },
-                .elf => .{ .elf = link.File.Elf.SrcFn.empty },
-                .macho => .{ .macho = link.File.MachO.SrcFn.empty },
+                .elf => .{ .elf = link.File.Dwarf.SrcFn.empty },
+                .macho => .{ .macho = link.File.Dwarf.SrcFn.empty },
                 .plan9 => .{ .plan9 = {} },
                 .c => .{ .c = {} },
                 .wasm => .{ .wasm = link.File.Wasm.FnData.empty },
@@ -4776,8 +4776,8 @@ pub fn allocateNewDecl(
         },
         .fn_link = switch (mod.comp.bin_file.tag) {
             .coff => .{ .coff = {} },
-            .elf => .{ .elf = link.File.Elf.SrcFn.empty },
-            .macho => .{ .macho = link.File.MachO.SrcFn.empty },
+            .elf => .{ .elf = link.File.Dwarf.SrcFn.empty },
+            .macho => .{ .macho = link.File.Dwarf.SrcFn.empty },
             .plan9 => .{ .plan9 = {} },
             .c => .{ .c = {} },
             .wasm => .{ .wasm = link.File.Wasm.FnData.empty },