Files
gasm-sdk/asm/elf_dwarf.go

407 lines
14 KiB
Go

// 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.
// DWARF5 attribute, form and line-table constants (the values the
// toolchain uses, cmd/internal/dwarf/dwarf_defs.go; the DIE streams below
// are written against these forms).
const (
dwAtName = 0x03 // DW_AT_name
dwAtStmtList = 0x10 // DW_AT_stmt_list
dwAtLowPC = 0x11 // DW_AT_low_pc
dwAtHighPC = 0x12 // DW_AT_high_pc
dwAtDeclFile = 0x3a // DW_AT_decl_file
dwAtDeclLine = 0x3b // DW_AT_decl_line
dwAtExternal = 0x3f // DW_AT_external
dwAtFrameBase = 0x40 // DW_AT_frame_base
dwTagSubprog = 0x2e // DW_TAG_subprogram
dwTagCompUnit = 0x11 // DW_TAG_compile_unit
dwFormAddr = 0x01 // DW_FORM_addr
dwFormData8 = 0x07 // DW_FORM_data8
dwFormString = 0x08 // DW_FORM_string
dwFormData1 = 0x0b // DW_FORM_data1
dwFormUdata = 0x0f // DW_FORM_udata
dwFormSecOff = 0x17 // DW_FORM_sec_offset
dwFormExprloc = 0x18 // DW_FORM_exprloc
dwFormLineStrp = 0x1f // DW_FORM_line_strp
dwLnctPath = 0x01 // DW_LNCT_path
dwLnctDirIndex = 0x02 // DW_LNCT_directory_index
)
// dwarfAbbrevTable returns the .debug_abbrev content: a single compilation
// unit with DW_TAG_compile_unit and DW_TAG_subprogram entries. The
// attribute/form pairs must match the DIE streams dwarfBuildInfoSection
// writes byte for byte, in the same order, or every consumer's parse of
// .debug_info desynchronises.
func dwarfAbbrevTable() []byte {
var b []byte
// Abbrev 1: DW_TAG_compile_unit.
b = append(b, 1) // abbreviation code
b = appendUleb(b, dwTagCompUnit) // DW_TAG_compile_unit
b = append(b, 1) // DW_CHILDREN_yes
b = appendUleb(b, dwAtLowPC) // DW_AT_low_pc
b = appendUleb(b, dwFormAddr) // DW_FORM_addr
b = appendUleb(b, dwAtHighPC) // DW_AT_high_pc
b = appendUleb(b, dwFormData8) // DW_FORM_data8
b = appendUleb(b, dwAtStmtList) // DW_AT_stmt_list
b = appendUleb(b, dwFormSecOff) // DW_FORM_sec_offset (4 bytes here)
b = appendUleb(b, dwAtName) // DW_AT_name
b = appendUleb(b, dwFormString) // DW_FORM_string
b = appendUleb(b, 0) // end of attributes: attr 0
b = appendUleb(b, 0) // ... paired with form 0
// Abbrev 2: DW_TAG_subprogram.
b = append(b, 2) // abbreviation code
b = appendUleb(b, dwTagSubprog) // DW_TAG_subprogram
b = append(b, 0) // DW_CHILDREN_no
b = appendUleb(b, dwAtName) // DW_AT_name
b = appendUleb(b, dwFormString) // DW_FORM_string
b = appendUleb(b, dwAtLowPC) // DW_AT_low_pc
b = appendUleb(b, dwFormAddr) // DW_FORM_addr
b = appendUleb(b, dwAtHighPC) // DW_AT_high_pc
b = appendUleb(b, dwFormData8) // DW_FORM_data8
b = appendUleb(b, dwAtFrameBase) // DW_AT_frame_base
b = appendUleb(b, dwFormExprloc) // DW_FORM_exprloc
b = appendUleb(b, dwAtDeclFile) // DW_AT_decl_file
b = appendUleb(b, dwFormData1) // DW_FORM_data1
b = appendUleb(b, dwAtDeclLine) // DW_AT_decl_line
b = appendUleb(b, dwFormData1) // DW_FORM_data1
b = appendUleb(b, dwAtExternal) // DW_AT_external
b = appendUleb(b, 0x0c) // DW_FORM_flag (one byte, 0 or 1)
b = appendUleb(b, 0) // end of attributes: attr 0
b = appendUleb(b, 0) // ... paired with form 0
// 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
// Relocations for .debug_frame: (offset, symbol name, addend), one per
// FDE initial_location.
frameRelocs []dwarfReloc
}
type dwarfReloc struct {
off uint64
name string
addend int64
}
// emitDWARF generates complete DWARF5 sections for the image. cfi carries
// the architecture's .debug_frame register conventions.
func emitDWARF(img *Image, srcFile string, cfi cfiArch) *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; the file table references the source name through
// its offset in .debug_line_str.
ds.debugLine = dwarfBuildLineSection(img, uint32(lineStr.at(srcFile)), ds)
// Build .debug_info.
ds.debugInfo = dwarfBuildInfoSection(img, srcFile, ds)
// Build .debug_frame.
ds.debugFrame = dwarfBuildFrameSection(img, cfi, ds)
return ds
}
// dwarfBuildLineSection builds a complete .debug_line section. srcStrOff is
// the source file name's offset in .debug_line_str.
func dwarfBuildLineSection(img *Image, srcStrOff uint32, 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 §6.2.4): entry format descriptors followed by
// the entries. One directory, the compilation directory, whose path is
// the empty string at .debug_line_str offset 0.
b = append(b, 1) // directory_entry_format_count
b = appendUleb(b, dwLnctPath) // DW_LNCT_path
b = appendUleb(b, dwFormLineStrp) // DW_FORM_line_strp
b = appendUleb(b, 1) // directories_count
b = le.AppendUint32(b, 0) // .debug_line_str offset of ""
// File table (DWARF5 §6.2.5). v5 indexes files from 0, so the source
// file is entry 0, matching the DW_AT_decl_file value 0 the DIEs carry.
b = append(b, 2) // file_name_entry_format_count
b = appendUleb(b, dwLnctPath) // DW_LNCT_path
b = appendUleb(b, dwFormLineStrp) // DW_FORM_line_strp
b = appendUleb(b, dwLnctDirIndex) // DW_LNCT_directory_index
b = appendUleb(b, dwFormUdata) // DW_FORM_udata
b = appendUleb(b, 1) // file_names_count
b = le.AppendUint32(b, srcStrOff) // .debug_line_str offset of the source name
b = appendUleb(b, 0) // directory index 0 (the compilation directory)
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. In the v5 header it follows the one-byte
// address_size and segment_selector_size (offset 8, not the DWARF2-4
// offset 6), and counts from just past itself to the first program
// byte.
le.PutUint32(b[headerStart+8:], uint32(headerEnd-headerStart-12))
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. A data-only image has no
// functions to relocate against; its CU covers no code, so the base
// stays zero (the DWARF "no base address" value) with no relocation.
b = le.AppendUint64(b, 0) // placeholder
if len(img.Funcs) > 0 {
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
off: uint64(len(b) - 8),
name: img.Funcs[0].Name,
})
}
// 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: the single file-table entry, index 0 (v5 indexes
// files from 0).
b = append(b, 0)
// 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)
}
// appendSleb appends v in signed LEB128, the encoding DWARF specifies:
// two's-complement sign extension, which is NOT Go's zigzag varint
// (binary.AppendVarint(-8) encodes 15, where DWARF wants 0x78).
func appendSleb(b []byte, v int64) []byte {
for {
c := byte(v & 0x7f)
v >>= 7
if (v == 0 && c&0x40 == 0) || (v == -1 && c&0x40 != 0) {
return append(b, c)
}
b = append(b, c|0x80)
}
}
// cfiArch carries the .debug_frame CIE parameters that differ per
// architecture: the DWARF register numbers of the stack pointer the initial
// CFA rule names and of the return address. The values are the ones the Go
// linker writes into its own CIE (cmd/link/internal/ld/dwarf.go uses
// Dwarfregsp and Dwarfreglr; the per-architecture constants live in
// cmd/link/internal/<arch>/l.go).
type cfiArch struct {
name string
cfaReg byte // the stack-pointer register the initial CFA rule names
raReg byte // the return-address register
}
var (
cfiAMD64 = cfiArch{"amd64", 7, 16} // RSP, RIP
cfiARM64 = cfiArch{"arm64", 31, 30} // SP (X31), LR (X30)
cfiRISCV64 = cfiArch{"riscv64", 2, 1} // X2 (sp), X1 (ra)
cfiLOONG64 = cfiArch{"loong64", 3, 1} // $r3 (sp), $r1 (ra)
)
// 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, cfi cfiArch, ds *dwarfSections) []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, uint64(cfi.raReg)) // return address register
// Initial CFA rule: DW_CFA_def_cfa (SP, 0)
b = append(b, 0x0c) // DW_CFA_def_cfa
b = appendUleb(b, uint64(cfi.cfaReg)) // the architecture's stack pointer
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, referenced through
// the function's symbol so the linker relocates it.
ds.frameRelocs = append(ds.frameRelocs, dwarfReloc{
off: uint64(fdeStart + 8),
name: fn.Name,
})
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
}