// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "encoding/binary" ) // This file generates the per-function DWARF symbols the linker's DWARF // pass requires of an assembly object, byte-identical to what cmd/asm // emits: the .debug_line state-machine program (SDWARFLINES) and the // subprogram DIE (SDWARFFCN). The linker copies the DIE and line-program // bytes verbatim into .debug_info and .debug_line, fixing up their // relocations, so the formats here must match cmd/internal/dwarf's // DW_ABRV_FUNCTION and generateDebugLinesSymbol exactly. // // DWARF5 is assumed throughout (the toolchain's default on Linux and the // other non-Darwin targets gasm supports). // Line-program parameters (cmd/internal/obj/dwarf.go). const ( dwLineBase = -4 dwLineRange = 10 dwOpcodeBase = 11 dwPCRange = (255 - dwOpcodeBase) / dwLineRange ) // goobjReloc is one relocation attached to an emitter-generated symbol // (the DWARF lines/info symbols), in goobj's on-disk encoding fields. type goobjReloc struct { off int32 siz uint8 typ uint16 add int64 pkg uint32 sym uint32 } // goobjDwarfLines builds the function's .debug_line state-machine program: // an LNE_set_address extended opcode establishing the function's start // address (carrying the R_ADDR relocation), one row per source line // change across the function's instructions, an advance to the end of the // function and an end-of-sequence opcode. The linker appends these bytes // after the unit's line header, so they must start with the address and // leave the state machine terminated. func goobjDwarfLines(fn FuncLayout, fnNpIdx int) ([]byte, []goobjReloc) { // Rows: the prologue, if any, then the body instructions (fn.Lines // covers the body only). The first body offset > 0 means a prologue // precedes it; the toolchain reports the prologue on the TEXT line. 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...) out := []byte{0, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0} // LNE_set_address, address zeroed relocs := []goobjReloc{{ off: 3, siz: 8, typ: relocAddr, pkg: pkgIdxNone, sym: uint32(fnNpIdx), }} // The state machine starts at line 1, pc 0 (function-relative); the // implicit initial pc is the function entry, so the first pc delta is // against 0. line := int64(1) pc := uint64(0) for _, p := range pts { if p.Line == 0 || uint64(p.Offset) < pc { continue } // Rows mark source-line changes only; the pc delta is measured from // the previous row, not the previous instruction. if int64(p.Line) == line { continue } deltaPC := uint64(p.Offset) - pc deltaLC := int64(p.Line) - line out = dwPutPCLCDelta(out, deltaPC, deltaLC) line, pc = int64(p.Line), uint64(p.Offset) } // Cover the rest of the function and close the sequence. if end := uint64(fn.Size) - pc; end > 0 { out = append(out, 2) // DW_LNS_advance_pc out = binary.AppendUvarint(out, end) } out = append(out, 0, 1, 1) // LNE_end_sequence return out, relocs } // dwPutPCLCDelta encodes one (pcDelta, lineDelta) step as the shortest // special opcode plus any standard-opcode remainder, exactly like // cmd/internal/obj's putpclcdelta. func dwPutPCLCDelta(b []byte, deltaPC uint64, deltaLC int64) []byte { opcode := dwSelectOpcode(deltaPC, deltaLC) deltaPC -= uint64((opcode - dwOpcodeBase) / dwLineRange) deltaLC -= (opcode-dwOpcodeBase)%dwLineRange + dwLineBase // The remainder: standard opcodes first, then the special opcode // (which emits the row). if deltaPC != 0 { switch { case deltaPC <= uint64(dwPCRange): opcode -= dwLineRange * int64(uint64(dwPCRange)-deltaPC) b = append(b, 8) // DW_LNS_const_add_pc case (1<<14) <= deltaPC && deltaPC < (1<<16): b = append(b, 9) // DW_LNS_fixed_advance_pc b = binary.LittleEndian.AppendUint16(b, uint16(deltaPC)) default: b = append(b, 2) // DW_LNS_advance_pc b = binary.AppendUvarint(b, deltaPC) } } if deltaLC != 0 { b = append(b, 3) // DW_LNS_advance_line b = binary.AppendVarint(b, deltaLC) } return append(b, byte(opcode)) } // dwSelectOpcode picks the special opcode for (deltaPC, deltaLC) per // cmd/internal/obj's putpclcdelta selection logic. func dwSelectOpcode(deltaPC uint64, deltaLC int64) int64 { switch { case deltaLC < dwLineBase: if deltaPC >= uint64(dwPCRange) { return dwOpcodeBase + dwLineRange*dwPCRange } return dwOpcodeBase + dwLineRange*int64(deltaPC) case deltaLC < dwLineBase+dwLineRange: if deltaPC >= uint64(dwPCRange) { op := int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*dwPCRange if op > 255 { op -= dwLineRange } return op } return int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*int64(deltaPC) default: if deltaPC <= uint64(dwPCRange) { op := int64(dwOpcodeBase) + (dwLineRange - 1) + dwLineRange*int64(deltaPC) if op > 255 { op = 255 } return op } switch deltaPC - uint64(dwPCRange) { case uint64(dwPCRange), (1 << 7) - 1, (1 << 16) - 1, (1 << 21) - 1, (1 << 28) - 1, (1 << 35) - 1, (1 << 42) - 1, (1 << 49) - 1, (1 << 56) - 1, (1 << 63) - 1: return 255 default: // 250: the toolchain's "249" comment is stale. return dwOpcodeBase + dwLineRange*dwPCRange - 1 } } } // goobjDwarfInfo builds the function's DWARF5 subprogram DIE (abbrev // DW_ABRV_FUNCTION): name, low_pc as a .debug_addr index (the // R_DWTXTADDR_U4 relocation), high_pc as the size, the call-frame-CFA // frame base, the decl file/line and the external flag. name is the // symbol's object name (package-qualified unless static). func goobjDwarfInfo(fn FuncLayout, name string, fnNpIdx int) ([]byte, []goobjReloc) { out := []byte{3} // DW_ABRV_FUNCTION out = append(out, name...) out = append(out, 0) addrx := len(out) out = append(out, 0, 0, 0, 0) // DW_AT_low_pc: addrx slot, zeroed out = binary.AppendUvarint(out, uint64(fn.Size)) out = append(out, 1, 0x9c) // DW_AT_frame_base: block1, DW_OP_call_frame_cfa out = binary.LittleEndian.AppendUint32(out, 1) out = binary.AppendUvarint(out, uint64(fn.Line)) if fn.Static { out = append(out, 0) } else { out = append(out, 1) // DW_AT_external } out = append(out, 0) // end of children relocs := []goobjReloc{{ off: int32(addrx), siz: 4, typ: relocDWTXTADDRU4, pkg: pkgIdxNone, sym: uint32(fnNpIdx), }} return out, relocs }