feat(asm): add DWARF5 debug info generation for ELF output
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
@@ -0,0 +1,250 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user