From 3acbdd6533bcd8ba5141b610840e570dbcb7f9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 20 Aug 2026 23:38:29 +0200 Subject: [PATCH] feat(asm): add DWARF5 debug info generation for ELF output Assisted-by: MiMo V2.5 Pro --- asm/elf_dwarf.go | 250 ++++++++++++++++++++++++++++++++++++++++++ asm/elf_dwarf_test.go | 81 ++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 asm/elf_dwarf.go create mode 100644 asm/elf_dwarf_test.go diff --git a/asm/elf_dwarf.go b/asm/elf_dwarf.go new file mode 100644 index 0000000..c221d1d --- /dev/null +++ b/asm/elf_dwarf.go @@ -0,0 +1,250 @@ +// 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 + // 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) + 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) +} diff --git a/asm/elf_dwarf_test.go b/asm/elf_dwarf_test.go new file mode 100644 index 0000000..eec74f5 --- /dev/null +++ b/asm/elf_dwarf_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "testing" + + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +func TestEmitDWARF(t *testing.T) { + src := `#include "textflag.h" +TEXT ·add(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX + MOVQ b+8(FP), BX + ADDQ BX, AX + MOVQ AX, ret+16(FP) + RET +` + f, errs := parser.Parse("test_amd64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("assemble: %v", err) + } + + ds := emitDWARF(img, "test_amd64.s") + + // .debug_abbrev must not be empty and must start with abbrev code 1. + if len(ds.debugAbbrev) == 0 { + t.Fatal("empty .debug_abbrev") + } + if ds.debugAbbrev[0] != 1 { + t.Fatalf(".debug_abbrev first byte = %d, want 1", ds.debugAbbrev[0]) + } + + // .debug_info must have a compile unit header (DWARF5 version 5). + if len(ds.debugInfo) < 12 { + t.Fatalf(".debug_info too short: %d bytes", len(ds.debugInfo)) + } + // Version field at offset 4 (after unit_length). + if ds.debugInfo[4] != 5 || ds.debugInfo[5] != 0 { + t.Fatalf(".debug_info version = %d, want 5", uint16(ds.debugInfo[4])|uint16(ds.debugInfo[5])<<8) + } + + // .debug_line must have a header. + if len(ds.debugLine) < 20 { + t.Fatalf(".debug_line too short: %d bytes", len(ds.debugLine)) + } + // Version at offset 4. + if ds.debugLine[4] != 5 || ds.debugLine[5] != 0 { + t.Fatalf(".debug_line version = %d, want 5", uint16(ds.debugLine[4])|uint16(ds.debugLine[5])<<8) + } + + // .debug_line_str must contain the source file name. + if len(ds.debugLineStr) == 0 { + t.Fatal("empty .debug_line_str") + } + + // Relocations must reference the function. + if len(ds.lineRelocs) == 0 { + t.Fatal("no .debug_line relocations") + } + if len(ds.infoRelocs) == 0 { + t.Fatal("no .debug_info relocations") + } +} + +func TestDwarfAbbrevTable(t *testing.T) { + abbrev := dwarfAbbrevTable() + if len(abbrev) == 0 { + t.Fatal("empty abbrev table") + } + // Must end with a zero byte (end of table). + if abbrev[len(abbrev)-1] != 0 { + t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1]) + } +}