// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "encoding/binary" ) // DWARF5 section generation for ELF output. Unlike the GOOBJ path (where // the linker assembles the final DWARF), the ELF path must emit complete, // self-contained sections because the system linker only performs fixup // relocations, not assembly. // dwarfAbbrevTable returns the .debug_abbrev content: a single compilation // unit with DW_TAG_compile_unit and DW_TAG_subprogram entries. func dwarfAbbrevTable() []byte { var b []byte // Abbrev 1: DW_TAG_compile_unit b = append(b, 1) // abbreviation code b = append(b, 0x11) // DW_TAG_compile_unit b = append(b, 1) // DW_CHILDREN_yes b = appendUleb(b, 0x1b) // DW_AT_low_pc b = appendUleb(b, 0x01) // DW_FORM_addr b = appendUleb(b, 0x29) // DW_AT_high_pc b = appendUleb(b, 0x07) // DW_FORM_data8 b = appendUleb(b, 0x10) // DW_AT_stmt_list b = appendUleb(b, 0x25) // DW_FORM_sec_offset b = appendUleb(b, 0x01) // DW_AT_name b = appendUleb(b, 0x08) // DW_FORM_string b = appendUleb(b, 0) // end of attributes // Abbrev 2: DW_TAG_subprogram b = append(b, 2) // abbreviation code b = append(b, 0x2e) // DW_TAG_subprogram b = append(b, 0) // DW_CHILDREN_no b = appendUleb(b, 0x03) // DW_AT_name b = appendUleb(b, 0x08) // DW_FORM_string b = appendUleb(b, 0x11) // DW_AT_low_pc b = appendUleb(b, 0x01) // DW_FORM_addr b = appendUleb(b, 0x29) // DW_AT_high_pc b = appendUleb(b, 0x07) // DW_FORM_data8 b = appendUleb(b, 0x3f) // DW_AT_frame_base b = appendUleb(b, 0x18) // DW_FORM_exprloc b = appendUleb(b, 0x3b) // DW_AT_decl_file b = appendUleb(b, 0x0b) // DW_FORM_data1 b = appendUleb(b, 0x37) // DW_AT_decl_line b = appendUleb(b, 0x0b) // DW_FORM_data1 b = appendUleb(b, 0x63) // DW_AT_external b = appendUleb(b, 0x0b) // DW_FORM_flag b = appendUleb(b, 0) // end of attributes // End of table. b = append(b, 0) return b } // dwarfSections holds the generated DWARF section payloads and their // relocations (byte offsets within .debug_info and .debug_line that need // fixup against .text symbols). type dwarfSections struct { debugAbbrev []byte debugInfo []byte debugLine []byte debugLineStr []byte debugFrame []byte // Relocations for .debug_info: (offset, symbol name, addend). infoRelocs []dwarfReloc // Relocations for .debug_line: (offset, symbol name, addend). lineRelocs []dwarfReloc } type dwarfReloc struct { off uint64 name string addend int64 } // emitDWARF generates complete DWARF5 sections for the image. func emitDWARF(img *Image, srcFile string) *dwarfSections { ds := &dwarfSections{} ds.debugAbbrev = dwarfAbbrevTable() // Build the string table for .debug_line_str. lineStr := newElfStrtab() lineStr.add(srcFile) ds.debugLineStr = lineStr.bytes() // Build .debug_line. ds.debugLine = dwarfBuildLineSection(img, srcFile, ds) // Build .debug_info. ds.debugInfo = dwarfBuildInfoSection(img, srcFile, ds) // Build .debug_frame. ds.debugFrame = dwarfBuildFrameSection(img) return ds } // dwarfBuildLineSection builds a complete .debug_line section. func dwarfBuildLineSection(img *Image, srcFile string, ds *dwarfSections) []byte { var b []byte le := binary.LittleEndian // We'll build the header first, then the programs, then patch the length. headerStart := len(b) b = append(b, 0, 0, 0, 0) // unit_length (placeholder) b = le.AppendUint16(b, 5) // version (DWARF5) b = append(b, 8) // address_size b = append(b, 0) // segment_selector_size b = append(b, 0, 0, 0, 0) // header_length (placeholder) // Line program parameters. b = append(b, 1) // minimum_instruction_length b = append(b, 1) // maximum_ops_per_instruction b = append(b, 1) // default_is_stmt b = append(b, byte(dwLineBase&0xFF)) // line_base (-4 as unsigned) b = append(b, uint8(dwLineRange)) // line_range b = append(b, uint8(dwOpcodeBase)) // opcode_base // Standard opcode lengths (opcode 1..opcode_base-1). b = append(b, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0) // Directory table (DWARF5 format). b = append(b, 0) // one directory entry (index 0 = empty) // File table. b = appendUleb(b, 1) // file count // File 1: name index into .debug_line_str, dir index, time, size. b = appendUleb(b, 0) // name (index 0 in line_str) b = appendUleb(b, 0) // directory index b = appendUleb(b, 0) // last modification time b = appendUleb(b, 0) // file size headerEnd := len(b) // Per-function line programs. for _, fn := range img.Funcs { // LNE_set_address with the function's offset in .text. b = append(b, 0, 9, 2) // extended opcode, length 9, DW_LNE_set_address addrOff := len(b) b = le.AppendUint64(b, 0) // placeholder for address ds.lineRelocs = append(ds.lineRelocs, dwarfReloc{ off: uint64(addrOff), name: fn.Name, addend: 0, }) // Build the line entries. pts := make([]LineEntry, 0, len(fn.Lines)+1) if len(fn.Lines) == 0 || fn.Lines[0].Offset > 0 { pts = append(pts, LineEntry{Offset: 0, Line: fn.Line}) } pts = append(pts, fn.Lines...) line := int64(1) pc := uint64(0) for _, p := range pts { if p.Line == 0 || uint64(p.Offset) < pc { continue } if int64(p.Line) == line { continue } deltaPC := uint64(p.Offset) - pc deltaLC := int64(p.Line) - line b = dwPutPCLCDelta(b, deltaPC, deltaLC) line, pc = int64(p.Line), uint64(p.Offset) } // Advance to end of function. if end := uint64(fn.Size) - pc; end > 0 { b = append(b, 2) // DW_LNS_advance_pc b = appendUleb(b, end) } b = append(b, 0, 1, 1) // LNE_end_sequence } // Patch unit_length. le.PutUint32(b[headerStart:], uint32(len(b)-headerStart-4)) // Patch header_length. le.PutUint32(b[headerStart+6:], uint32(headerEnd-headerStart-10)) return b } // dwarfBuildInfoSection builds a complete .debug_info section. func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte { var b []byte le := binary.LittleEndian cuStart := len(b) b = append(b, 0, 0, 0, 0) // unit_length (placeholder) b = le.AppendUint16(b, 5) // version (DWARF5) b = append(b, 0x01) // unit_type (DW_UT_compile) b = append(b, 8) // address_size b = le.AppendUint32(b, 0) // debug_abbrev_offset (0 since single CU) // DW_TAG_compile_unit (abbrev 1). b = append(b, 1) // abbreviation code // DW_AT_low_pc: address of .text start. infoRelocBase := len(b) b = le.AppendUint64(b, 0) // placeholder ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{ off: uint64(infoRelocBase), name: img.Funcs[0].Name, addend: 0, }) // DW_AT_high_pc: size of .text. b = le.AppendUint64(b, uint64(len(img.Code))) // DW_AT_stmt_list: offset into .debug_line (0). b = le.AppendUint32(b, 0) // DW_AT_name: source file name. b = append(b, srcFile...) b = append(b, 0) // DW_TAG_subprogram entries (abbrev 2). for _, fn := range img.Funcs { b = append(b, 2) // abbreviation code // DW_AT_name. b = append(b, fn.Name...) b = append(b, 0) // DW_AT_low_pc. addrOff := len(b) b = le.AppendUint64(b, 0) // placeholder ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{ off: uint64(addrOff), name: fn.Name, addend: 0, }) // DW_AT_high_pc: function size. b = le.AppendUint64(b, uint64(fn.Size)) // DW_AT_frame_base: DW_OP_call_frame_cfa. b = append(b, 1, 0x9c) // DW_AT_decl_file: file index 1. b = append(b, 1) // DW_AT_decl_line. b = append(b, uint8(fn.Line)) // DW_AT_external. if fn.Static { b = append(b, 0) } else { b = append(b, 1) } } // End of compile unit children. b = append(b, 0) // Patch unit_length. le.PutUint32(b[cuStart:], uint32(len(b)-cuStart-4)) return b } func appendUleb(b []byte, v uint64) []byte { return binary.AppendUvarint(b, v) } func appendSleb(b []byte, v int64) []byte { return binary.AppendVarint(b, v) } // dwarfBuildFrameSection builds a .debug_frame section with CFI for stack // unwinding. It emits one CIE and one FDE per function, encoding the // CFA (Canonical Frame Address) rule changes at each stack-adjustment // boundary recorded in FuncLayout.Spadj. func dwarfBuildFrameSection(img *Image) []byte { var b []byte le := binary.LittleEndian // CIE (Common Information Entry). cieStart := len(b) b = append(b, 0, 0, 0, 0) // length (placeholder) b = le.AppendUint32(b, 0xFFFFFFFF) // CIE marker b = append(b, 3) // version (DWARF3, widely supported) b = append(b, 0) // augmentation (empty) b = appendUleb(b, 1) // code alignment b = appendSleb(b, -8) // data alignment (-8 for 64-bit) b = appendUleb(b, 16) // return address register (LR on arm64, RIP on amd64) // Initial CFA rule: DW_CFA_def_cfa (SP, 0) b = append(b, 0x0c) // DW_CFA_def_cfa b = appendUleb(b, 31) // register: SP (RSP=7 on amd64, SP=31 on arm64) b = appendUleb(b, 0) // offset: 0 b = append(b, 0) // DW_CFA_nop (padding) // Patch CIE length. le.PutUint32(b[cieStart:], uint32(len(b)-cieStart-4)) // FDEs (Frame Description Entries) — one per function. for _, fn := range img.Funcs { fdeStart := len(b) b = append(b, 0, 0, 0, 0) // length (placeholder) b = le.AppendUint32(b, uint32(cieStart)) // CIE pointer (offset from start) // Initial location: function offset in .text (relocated by linker). b = le.AppendUint64(b, uint64(fn.Offset)) // Address range: function size. b = le.AppendUint64(b, uint64(fn.Size)) // Emit CFA rule changes at each Spadj boundary. for _, step := range fn.Spadj { if step.Value == 0 { continue } // DW_CFA_def_cfa_offset: set CFA = SP + |delta|. // The delta is negative (stack grows down), so CFA offset = -delta. offset := -step.Value if offset > 0 { b = append(b, 0x0e) // DW_CFA_def_cfa_offset b = appendUleb(b, uint64(offset)) } } // Pad to alignment. for len(b)%4 != 0 { b = append(b, 0) // DW_CFA_nop } // Patch FDE length. le.PutUint32(b[fdeStart:], uint32(len(b)-fdeStart-4)) } return b }