From f3c8510a58212776efbddfc022f54d94fa70c947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 19 Sep 2026 23:49:13 +0200 Subject: [PATCH] fix(elf): relocation records, DWARF tables and per-architecture frame data Assisted-by: GLM 5.3 --- asm/elf.go | 56 ++++- asm/elf_dwarf.go | 235 +++++++++++++------- asm/elf_dwarf_sections.go | 108 +++++++--- asm/elf_dwarf_test.go | 315 +++++++++++++++++++++++++-- asm/elf_test.go | 442 ++++++++++++++++++++++++++++++++++++++ asm/elfarm64.go | 96 +++++++-- asm/elfarm64_test.go | 59 ++++- asm/elfloong64.go | 52 ++++- asm/elfloong64_test.go | 5 + asm/elfriscv.go | 71 +++++- asm/link.go | 27 ++- 11 files changed, 1302 insertions(+), 164 deletions(-) diff --git a/asm/elf.go b/asm/elf.go index 09b5db7..b7ef9cc 100644 --- a/asm/elf.go +++ b/asm/elf.go @@ -42,8 +42,10 @@ const ( sttSection = 3 stInfoShift = 4 - rX8664PC32 = 2 - rX8664TPOFF32 = 20 + rX8664PC32 = 2 + // R_X86_64_TPOFF32 (debug/elf): the local-exec TLS offset the stack + // guard loads from FS. 20 is R_X86_64_TLSLD, a different relocation. + rX8664TPOFF32 = 23 ) // elfSym is one symbol-table entry in construction. @@ -219,7 +221,7 @@ func (img *Image) ELFObject() ([]byte, error) { for _, r := range relas { var b [24]byte le.PutUint64(b[0:], r.off) - le.PutUint64(b[8:], uint64(r.sym)<<32|rX8664PC32) + le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ)) le.PutUint64(b[16:], uint64(r.addend)) out = append(out, b[:]...) } @@ -228,15 +230,32 @@ func (img *Image) ELFObject() ([]byte, error) { shstrOff := len(out) out = append(out, stSections.bytes()...) - // DWARF debug sections (no relocations, the linker resolves DWARF fixups). + // DWARF debug sections; the address placeholders they leave are carried + // as .rela.debug_info/.rela.debug_line entries the system linker applies. dwAlign := func(n int) { for len(out)%n != 0 { out = append(out, 0) } } - dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign) + dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiAMD64) + dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present if dw != nil { - nSections += 4 // .debug_abbrev, .debug_info, .debug_line, .debug_line_str + // Five DWARF sections: .debug_abbrev, .debug_info, .debug_line, + // .debug_line_str and .debug_frame (the CIE is unconditional, so + // the frame section is always present), plus the relocation + // sections below when they carry entries. + dwarfStart = nSections + nSections += 5 + appendDWARFRelas(&out, dw, rX8664Abs64, dwAlign) + if dw.infoRelaCount > 0 { + nSections++ + } + if dw.lineRelaCount > 0 { + nSections++ + } + if dw.frameRelaCount > 0 { + nSections++ + } } align(8) @@ -267,14 +286,37 @@ func (img *Image) ELFObject() ([]byte, error) { } putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0) - // DWARF section headers. + // DWARF section headers; their indices follow the write order. if dw != nil { + // secIdx is a running section index: each putSh below emits the + // next header, and the sh_info of a .rela section names the index + // of the section it relocates. + secIdx := dwarfStart putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0) + secIdx++ putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0) + secInfoIdx := secIdx + secIdx++ + if dw.infoRelaCount > 0 { + putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24) + secIdx++ + } putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0) + secLineIdx := secIdx + secIdx++ + if dw.lineRelaCount > 0 { + putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24) + secIdx++ + } putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0) + secIdx++ if dw.frameSize > 0 { putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0) + secFrameIdx := secIdx + secIdx++ + if dw.frameRelaCount > 0 { + putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24) + } } } diff --git a/asm/elf_dwarf.go b/asm/elf_dwarf.go index 2589433..884baf3 100644 --- a/asm/elf_dwarf.go +++ b/asm/elf_dwarf.go @@ -12,43 +12,74 @@ import ( // 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. +// 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 = 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 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 = 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 + // 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) @@ -68,6 +99,9 @@ type dwarfSections struct { 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 { @@ -76,8 +110,9 @@ type dwarfReloc struct { addend int64 } -// emitDWARF generates complete DWARF5 sections for the image. -func emitDWARF(img *Image, srcFile string) *dwarfSections { +// 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() @@ -86,19 +121,21 @@ func emitDWARF(img *Image, srcFile string) *dwarfSections { lineStr.add(srcFile) ds.debugLineStr = lineStr.bytes() - // Build .debug_line. - ds.debugLine = dwarfBuildLineSection(img, ds) + // 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) + ds.debugFrame = dwarfBuildFrameSection(img, cfi, ds) return ds } -// dwarfBuildLineSection builds a complete .debug_line section. -func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte { +// 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 @@ -120,15 +157,25 @@ func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte { // 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 + // 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) @@ -176,8 +223,11 @@ func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte { // Patch unit_length. le.PutUint32(b[headerStart:], uint32(len(b)-headerStart-4)) - // Patch header_length. - le.PutUint32(b[headerStart+6:], uint32(headerEnd-headerStart-10)) + // 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 } @@ -195,14 +245,16 @@ func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte // DW_TAG_compile_unit (abbrev 1). b = append(b, 1) // abbreviation code - // DW_AT_low_pc: address of .text start. - infoRelocBase := len(b) + // 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 - ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{ - off: uint64(infoRelocBase), - name: img.Funcs[0].Name, - addend: 0, - }) + 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). @@ -229,8 +281,9 @@ func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte 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_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. @@ -253,32 +306,61 @@ 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 { - return binary.AppendVarint(b, v) + 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//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) []byte { +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, 16) // return address register (LR on arm64, RIP on amd64) + 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, 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) + 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)) @@ -287,7 +369,12 @@ func dwarfBuildFrameSection(img *Image) []byte { 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). + // 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)) diff --git a/asm/elf_dwarf_sections.go b/asm/elf_dwarf_sections.go index 79e4507..b133894 100644 --- a/asm/elf_dwarf_sections.go +++ b/asm/elf_dwarf_sections.go @@ -3,6 +3,17 @@ package asm +import "encoding/binary" + +// Absolute 64-bit relocation types for the DWARF address fixups, one per +// supported architecture (the numbers debug/elf carries). +const ( + rX8664Abs64 = 1 // R_X86_64_64 + rAARCH64Abs64 = 257 // R_AARCH64_ABS64 + rRISCVAbs64 = 2 // R_RISCV_64 + rLarchAbs64 = 2 // R_LARCH_64 +) + // dwarfELFSections holds the laid-out DWARF sections ready for inclusion // in an ELF file. type dwarfELFSections struct { @@ -11,15 +22,23 @@ type dwarfELFSections struct { lineOff, lineSize int lineStrOff, lineStrSize int frameOff, frameSize int - // Relocations for .debug_info address references. + // .rela.debug_info and .rela.debug_line contents: file offsets and + // entry counts (zero count: the section is absent). + infoRelaOff, infoRelaCount int + lineRelaOff, lineRelaCount int + frameRelaOff, frameRelaCount int + // Relocations for .debug_info address references, offsets relative to + // the section start (what an r_offset in .rela.debug_info means). infoRelocs []elfDwarfReloc - // Relocations for .debug_line address references. + // Relocations for .debug_line address references, section-relative. lineRelocs []elfDwarfReloc + // Relocations for .debug_frame FDE initial locations, section-relative. + frameRelocs []elfDwarfReloc } type elfDwarfReloc struct { - off uint64 - sym int // symbol index in .symtab + off uint64 // offset within the target section + sym int // symbol index in .symtab addend int64 } @@ -29,8 +48,9 @@ type elfDwarfReloc struct { // // symIdx maps function names to their .symtab indices (needed for relocations // against .text symbols). The map uses objectName format (pkg.name); the -// DWARF code uses bare function names, so we build a reverse lookup. -func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[string]int, align func(int)) *dwarfELFSections { +// DWARF code uses bare function names, so we build a reverse lookup. cfi +// carries the architecture's .debug_frame register conventions. +func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[string]int, align func(int), cfi cfiArch) *dwarfELFSections { // Build a lookup from bare function name to symbol index. nameToIdx := make(map[string]int, len(symIdx)) for name, idx := range symIdx { @@ -45,7 +65,7 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str } nameToIdx[name] = idx } - ds := emitDWARF(img, srcFile) + ds := emitDWARF(img, srcFile, cfi) if ds == nil || len(ds.debugAbbrev) == 0 { return nil } @@ -68,15 +88,11 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str align(1) result.lineOff = len(*out) result.lineSize = len(ds.debugLine) - lineBase := len(*out) *out = append(*out, ds.debugLine...) - - // Patch .debug_line relocations: replace placeholder addresses with - // actual .text offsets via symbol lookup. for _, dr := range ds.lineRelocs { if idx, ok := nameToIdx[dr.name]; ok { result.lineRelocs = append(result.lineRelocs, elfDwarfReloc{ - off: uint64(lineBase) + dr.off, + off: dr.off, sym: idx, addend: dr.addend, }) @@ -87,33 +103,77 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str align(1) result.infoOff = len(*out) result.infoSize = len(ds.debugInfo) - infoBase := len(*out) *out = append(*out, ds.debugInfo...) - - // .debug_frame - if len(ds.debugFrame) > 0 { - align(1) - result.frameOff = len(*out) - result.frameSize = len(ds.debugFrame) - *out = append(*out, ds.debugFrame...) - } - - // Patch .debug_info relocations. for _, dr := range ds.infoRelocs { if idx, ok := nameToIdx[dr.name]; ok { result.infoRelocs = append(result.infoRelocs, elfDwarfReloc{ - off: uint64(infoBase) + dr.off, + off: dr.off, sym: idx, addend: dr.addend, }) } } + // .debug_frame: the section header declares alignment 8, so the data is + // padded to 8, matching it. + if len(ds.debugFrame) > 0 { + align(8) + result.frameOff = len(*out) + result.frameSize = len(ds.debugFrame) + *out = append(*out, ds.debugFrame...) + for _, dr := range ds.frameRelocs { + if idx, ok := nameToIdx[dr.name]; ok { + result.frameRelocs = append(result.frameRelocs, elfDwarfReloc{ + off: dr.off, + sym: idx, + addend: dr.addend, + }) + } + } + } + return result } +// appendDWARFRelas writes the .rela.debug_info and .rela.debug_line section +// bodies from the relocations appendDWARFSections recorded, with the +// architecture's absolute 64-bit relocation type, and records their file +// offsets and entry counts on dw. Called after the DWARF sections +// themselves so the r_offsets (section-relative) need no adjustment. +func appendDWARFRelas(out *[]byte, dw *dwarfELFSections, abs64 uint32, align func(int)) { + le := binary.LittleEndian + write := func(relas []elfDwarfReloc) (off, count int) { + if len(relas) == 0 { + return 0, 0 + } + align(8) + off = len(*out) + for _, r := range relas { + var b [24]byte + le.PutUint64(b[0:], r.off) + le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(abs64)) + le.PutUint64(b[16:], uint64(r.addend)) + *out = append(*out, b[:]...) + } + return off, len(relas) + } + dw.infoRelaOff, dw.infoRelaCount = write(dw.infoRelocs) + dw.lineRelaOff, dw.lineRelaCount = write(dw.lineRelocs) + dw.frameRelaOff, dw.frameRelaCount = write(dw.frameRelocs) +} + +// dwarfSourceName returns the source name the DWARF sections record: the +// image's source path when the assembler captured one, "gasm.s" otherwise. +func dwarfSourceName(img *Image) string { + if img.SourcePath != "" { + return img.SourcePath + } + return "gasm.s" +} + // dwarfSectionNames returns the DWARF section names for the string table. var dwarfSectionNames = []string{ ".debug_abbrev", ".debug_info", ".debug_line", ".debug_line_str", ".debug_frame", ".rela.debug_info", ".rela.debug_line", + ".rela.debug_frame", } diff --git a/asm/elf_dwarf_test.go b/asm/elf_dwarf_test.go index eec74f5..e476f00 100644 --- a/asm/elf_dwarf_test.go +++ b/asm/elf_dwarf_test.go @@ -4,11 +4,313 @@ package asm import ( + "bytes" + "encoding/binary" "testing" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) +// ulebIter reads ULEB128 values, the .debug_abbrev and line-header +// encoding. +type ulebIter struct { + b []byte + i int +} + +func (r *ulebIter) uleb(t *testing.T) uint64 { + t.Helper() + v, n := binary.Uvarint(r.b[r.i:]) + if n <= 0 { + t.Fatalf("bad ULEB at %d", r.i) + } + r.i += n + return v +} + +func (r *ulebIter) byteAt(t *testing.T) byte { + t.Helper() + if r.i >= len(r.b) { + t.Fatalf("read past end at %d", r.i) + } + c := r.b[r.i] + r.i++ + return c +} + +func (r *ulebIter) uint32At(t *testing.T) uint32 { + t.Helper() + v := binary.LittleEndian.Uint32(r.b[r.i:]) + r.i += 4 + return v +} + +// sleb reads a signed LEB128, the DWARF encoding (sign-extended two's +// complement, not Go's zigzag varint). +func (r *ulebIter) sleb(t *testing.T) int64 { + t.Helper() + var v int64 + var shift uint + for { + c := r.byteAt(t) + v |= int64(c&0x7f) << shift + shift += 7 + if c&0x80 == 0 { + if c&0x40 != 0 { + v |= -1 << shift + } + return v + } + } +} + +// dwarfAttr is one attribute/form pair of an abbreviation. +type dwarfAttr struct{ attr, form uint64 } + +// dwarfAbbrev is one parsed abbreviation declaration. +type dwarfAbbrev struct { + code uint64 + tag uint64 + children bool + attrs []dwarfAttr +} + +// parseAbbrevs walks a .debug_abbrev table: abbreviation code, tag, +// children flag, then attr/form ULEB pairs terminated by a double zero. +func parseAbbrevs(t *testing.T, b []byte) map[uint64]dwarfAbbrev { + t.Helper() + out := map[uint64]dwarfAbbrev{} + r := &ulebIter{b: b} + for { + code := r.uleb(t) + if code == 0 { + return out + } + ab := dwarfAbbrev{code: code, tag: r.uleb(t)} + ab.children = r.byteAt(t) == 1 + for { + attr := r.uleb(t) + form := r.uleb(t) + if attr == 0 && form == 0 { + break + } + if attr == 0 || form == 0 { + t.Fatalf("abbrev %d: half-terminated attr/form pair (%d, %d)", code, attr, form) + } + ab.attrs = append(ab.attrs, dwarfAttr{attr, form}) + } + out[code] = ab + } +} + +func eqAttrs(t *testing.T, ab dwarfAbbrev, want []dwarfAttr) { + t.Helper() + if len(ab.attrs) != len(want) { + t.Fatalf("abbrev %d attrs = %v, want %v", ab.code, ab.attrs, want) + } + for i, w := range want { + if ab.attrs[i] != w { + t.Fatalf("abbrev %d attr %d = (%#x, %#x), want (%#x, %#x)", ab.code, i, ab.attrs[i].attr, ab.attrs[i].form, w.attr, w.form) + } + } +} + +// TestDwarfAbbrevTable walks the abbreviation table as a consumer does and +// checks the attribute/form sets against the constants the toolchain uses +// (cmd/internal/dwarf/dwarf_defs.go). A wrong constant here renames an +// attribute (0x1b is comp_dir, not low_pc; 0x29 and 0x37 are bounds and +// count) and a wrong form desynchronises the DIE parse: 0x25 is strx1, one +// byte, where the writer emits four for a section offset. +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]) + } + abs := parseAbbrevs(t, abbrev) + if len(abs) != 2 { + t.Fatalf("abbreviations = %d, want 2", len(abs)) + } + cu, ok := abs[1] + if !ok { + t.Fatal("missing abbreviation 1 (compile unit)") + } + if cu.tag != dwTagCompUnit || !cu.children { + t.Errorf("abbrev 1: tag %#x children %v, want compile unit with children", cu.tag, cu.children) + } + eqAttrs(t, cu, []dwarfAttr{ + {dwAtLowPC, dwFormAddr}, + {dwAtHighPC, dwFormData8}, + {dwAtStmtList, dwFormSecOff}, + {dwAtName, dwFormString}, + }) + sp, ok := abs[2] + if !ok { + t.Fatal("missing abbreviation 2 (subprogram)") + } + if sp.tag != dwTagSubprog || sp.children { + t.Errorf("abbrev 2: tag %#x children %v, want subprogram without children", sp.tag, sp.children) + } + eqAttrs(t, sp, []dwarfAttr{ + {dwAtName, dwFormString}, + {dwAtLowPC, dwFormAddr}, + {dwAtHighPC, dwFormData8}, + {dwAtFrameBase, dwFormExprloc}, + {dwAtDeclFile, dwFormData1}, + {dwAtDeclLine, dwFormData1}, + {dwAtExternal, 0x0c}, // DW_FORM_flag + }) +} + +// TestDwarfLineHeaderV5 parses the .debug_line header under DWARF5 rules: +// the directory and file tables are format-descriptor lists, not the +// DWARF2-4 shape of null-terminated strings, and the file entry references +// the source name through .debug_line_str. +func TestDwarfLineHeaderV5(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", cfiAMD64) + r := &ulebIter{b: ds.debugLine} + r.uint32At(t) // unit_length + if v := binary.LittleEndian.Uint16(ds.debugLine[4:]); v != 5 { + t.Fatalf("version = %d, want 5", v) + } + r.i = 6 + r.byteAt(t) // address_size + r.byteAt(t) // segment_selector_size + r.uint32At(t) // header_length + r.byteAt(t) // minimum_instruction_length + r.byteAt(t) // maximum_ops_per_instruction + r.byteAt(t) // default_is_stmt + r.byteAt(t) // line_base + r.byteAt(t) // line_range + opcodeBase := r.byteAt(t) + for range int(opcodeBase) - 1 { + r.byteAt(t) // standard opcode lengths + } + + // Directory table (DWARF5 §6.2.4). + if n := r.byteAt(t); n != 1 { + t.Fatalf("directory_entry_format_count = %d, want 1", n) + } + if lnct := r.uleb(t); lnct != dwLnctPath { + t.Errorf("directory content type = %#x, want DW_LNCT_path", lnct) + } + if form := r.uleb(t); form != dwFormLineStrp { + t.Errorf("directory form = %#x, want DW_FORM_line_strp", form) + } + if n := r.uleb(t); n != 1 { + t.Fatalf("directories_count = %d, want 1", n) + } + if off := r.uint32At(t); off != 0 { + t.Errorf("compilation directory line_strp = %d, want 0 (the empty string)", off) + } + + // File table (DWARF5 §6.2.5). + if n := r.byteAt(t); n != 2 { + t.Fatalf("file_name_entry_format_count = %d, want 2", n) + } + if lnct := r.uleb(t); lnct != dwLnctPath { + t.Errorf("file content type = %#x, want DW_LNCT_path", lnct) + } + if form := r.uleb(t); form != dwFormLineStrp { + t.Errorf("file path form = %#x, want DW_FORM_line_strp", form) + } + if lnct := r.uleb(t); lnct != dwLnctDirIndex { + t.Errorf("file content type = %#x, want DW_LNCT_directory_index", lnct) + } + if form := r.uleb(t); form != dwFormUdata { + t.Errorf("file dir-index form = %#x, want DW_FORM_udata", form) + } + if n := r.uleb(t); n != 1 { + t.Fatalf("file_names_count = %d, want 1", n) + } + strOff := r.uint32At(t) + if dirIdx := r.uleb(t); dirIdx != 0 { + t.Errorf("file directory index = %d, want 0", dirIdx) + } + + // The file entry's line_strp must resolve to the source name. + end := int(strOff) + len("test_amd64.s") + if int(strOff) >= len(ds.debugLineStr) || !bytes.Equal(ds.debugLineStr[strOff:end], []byte("test_amd64.s")) { + t.Errorf("file entry line_strp %d does not name the source: %q", strOff, ds.debugLineStr) + } + + // The fixed header fields: address_size 8 and a header_length that + // points just past the file table (the patch site is offset 8 in the + // v5 header, and the field counts from its own end). + if ds.debugLine[6] != 8 || ds.debugLine[7] != 0 { + t.Errorf("address_size/segment_selector = %d/%d, want 8/0", ds.debugLine[6], ds.debugLine[7]) + } + if hl := binary.LittleEndian.Uint32(ds.debugLine[8:]); hl != uint32(r.i-12) { + t.Errorf("header_length = %d, want %d (the byte after the file table is %d)", hl, r.i-12, r.i) + } +} + +// TestDwarfFrameCIEArch checks the shared CIE carries each architecture's +// stack-pointer and return-address registers: the values the Go linker +// writes (cmd/link/internal//l.go dwarfRegSP/dwarfRegLR). +func TestDwarfFrameCIEArch(t *testing.T) { + for _, tc := range []struct { + name string + cfi cfiArch + }{ + {"amd64", cfiAMD64}, + {"arm64", cfiARM64}, + {"riscv64", cfiRISCV64}, + {"loong64", cfiLOONG64}, + } { + frame := dwarfBuildFrameSection(&Image{}, tc.cfi, &dwarfSections{}) + r := &ulebIter{b: frame} + r.uint32At(t) // length + if cid := r.uint32At(t); cid != 0xFFFFFFFF { + t.Errorf("%s: CIE id = %#x, want 0xffffffff", tc.name, cid) + } + if v := r.byteAt(t); v != 3 { + t.Errorf("%s: CIE version = %d, want 3", tc.name, v) + } + if aug := r.byteAt(t); aug != 0 { + t.Errorf("%s: CIE augmentation = %d, want 0", tc.name, aug) + } + if ca := r.uleb(t); ca != 1 { + t.Errorf("%s: code alignment = %d, want 1", tc.name, ca) + } + if da := r.sleb(t); da != -8 { + t.Errorf("%s: data alignment = %d, want -8 (signed LEB128, not zigzag)", tc.name, da) + } + if ra := r.uleb(t); ra != uint64(tc.cfi.raReg) { + t.Errorf("%s: return-address register = %d, want %d", tc.name, ra, tc.cfi.raReg) + } + if op := r.byteAt(t); op != 0x0c { + t.Errorf("%s: expected DW_CFA_def_cfa, got opcode %#x", tc.name, op) + } + if cfa := r.uleb(t); cfa != uint64(tc.cfi.cfaReg) { + t.Errorf("%s: CFA register = %d, want %d", tc.name, cfa, tc.cfi.cfaReg) + } + if off := r.uleb(t); off != 0 { + t.Errorf("%s: CFA offset = %d, want 0", tc.name, off) + } + } +} + func TestEmitDWARF(t *testing.T) { src := `#include "textflag.h" TEXT ·add(SB), NOSPLIT, $0-24 @@ -27,7 +329,7 @@ TEXT ·add(SB), NOSPLIT, $0-24 t.Fatalf("assemble: %v", err) } - ds := emitDWARF(img, "test_amd64.s") + ds := emitDWARF(img, "test_amd64.s", cfiAMD64) // .debug_abbrev must not be empty and must start with abbrev code 1. if len(ds.debugAbbrev) == 0 { @@ -68,14 +370,3 @@ TEXT ·add(SB), NOSPLIT, $0-24 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]) - } -} diff --git a/asm/elf_test.go b/asm/elf_test.go index 8bd4e0a..f5a9b17 100644 --- a/asm/elf_test.go +++ b/asm/elf_test.go @@ -12,6 +12,7 @@ import ( "path/filepath" "testing" + "sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) @@ -211,6 +212,75 @@ func TestELFObject(t *testing.T) { } } +// TestELFObjectTLSGuardReloc checks that a non-NOSPLIT function's stack +// guard carries an R_X86_64_TPOFF32 relocation against the null symbol in +// .rela.text. The serialisation must honour the record's type field: a +// hardcoded R_X86_64_PC32 mislinks the TLS load as an ordinary +// PC-relative reference. +func TestELFObjectTLSGuardReloc(t *testing.T) { + f, errs := parser.Parse("g_amd64.s", ` +#include "textflag.h" + +TEXT ·grow(SB), $0 + CALL ·other(SB) + RET + +TEXT ·other(SB), NOSPLIT, $0 + RET +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + var haveTLS bool + for _, fn := range img.Funcs { + for _, r := range fn.Relocs { + if r.Kind == RelTLSLE { + haveTLS = true + } + } + } + if !haveTLS { + t.Fatal("test source produced no RelTLSLE relocation") + } + obj, err := img.ELFObject() + if err != nil { + t.Fatalf("ELFObject: %v", err) + } + ef, err := elf.NewFile(bytes.NewReader(obj)) + if err != nil { + t.Fatalf("parse emitted object: %v", err) + } + defer ef.Close() + relaSec := ef.Section(".rela.text") + if relaSec == nil { + t.Fatal("missing .rela.text") + } + raw, err := relaSec.Data() + if err != nil { + t.Fatal(err) + } + found := false + for i := 0; i+24 <= len(raw); i += 24 { + e := raw[i:] + info := binary.LittleEndian.Uint64(e[8:]) + typ := info & 0xffffffff + sym := int(info >> 32) + if typ == uint64(elf.R_X86_64_TPOFF32) { + found = true + if sym != 0 { + t.Errorf("TPOFF32 relocation against symbol %d, want 0 (the null symbol)", sym) + } + } + } + if !found { + t.Errorf("no R_X86_64_TPOFF32 relocation in .rela.text (%d bytes)", len(raw)) + } +} + // TestELFObjectNoRelocations checks a file with no static-symbol references // emits a valid object without a .rela.text section. func TestELFObjectNoRelocations(t *testing.T) { @@ -253,6 +323,238 @@ TEXT ·nop(SB), NOSPLIT, $0 } } +// elfSectionHeaderCount returns the e_shnum the ELF header declares. +func elfSectionHeaderCount(t *testing.T, obj []byte) int { + t.Helper() + return int(binary.LittleEndian.Uint16(obj[60:])) +} + +// checkELFSectionAccounting verifies the number of section headers the +// writer physically laid out equals e_shnum: every DWARF section written +// after .shstrtab must be counted, or the last ones (always .debug_frame) +// are invisible to every consumer, debug/elf included. +func checkELFSectionAccounting(t *testing.T, obj []byte) { + t.Helper() + shoff := int(binary.LittleEndian.Uint64(obj[40:])) + shentsize := int(binary.LittleEndian.Uint16(obj[58:])) + shnum := elfSectionHeaderCount(t, obj) + if shentsize != 64 { + t.Fatalf("e_shentsize = %d, want 64", shentsize) + } + if (len(obj)-shoff)%shentsize != 0 { + t.Fatalf("section header table is not a whole number of entries: shoff=%d len=%d", shoff, len(obj)) + } + if present := (len(obj) - shoff) / shentsize; present != shnum { + t.Errorf("e_shnum = %d but %d section headers are laid out", shnum, present) + } +} + +// TestELFDWARFSectionAccounting runs the header accounting check over all +// four architecture emitters, and additionally checks the .debug_frame +// section is visible (its data aligned as its header declares). +func TestELFDWARFSectionAccounting(t *testing.T) { + parse := func(name, src string) *ast.File { + f, errs := parser.Parse(name, src) + if len(errs) > 0 { + t.Fatalf("parse %s: %v", name, errs) + } + return f + } + cases := []struct { + name string + img *Image + emit func(*Image) ([]byte, error) + }{ + {"amd64", elfTestImage(t), (*Image).ELFObject}, + {"arm64", mustImage(t, func() (*Image, error) { + return AssembleFileARM64(parse("k_arm64.s", ` +#include "textflag.h" + +TEXT ·add(SB), NOSPLIT, $0-24 + MOVD a+0(FP), R4 + MOVD b+8(FP), R5 + ADD R5, R4, R4 + MOVD R4, ret+16(FP) + RET +`)) + }), (*Image).ELFAARCH64Object}, + {"riscv64", mustImage(t, func() (*Image, error) { + return AssembleFileRISCV(parse("k_riscv64.s", ` +#include "textflag.h" + +TEXT ·sb(SB), NOSPLIT, $0-0 + MOV $answer<>(SB), X10 + RET + +GLOBL answer<>(SB), RODATA, $8 +DATA answer<>+0(SB)/8, $42 +`)) + }), (*Image).ELFRISCVObject}, + {"loong64", mustImage(t, func() (*Image, error) { + return AssembleFileLOONG64(parse("k_loong64.s", ` +#include "textflag.h" + +TEXT ·add(SB), NOSPLIT, $0-24 + MOVV a+0(FP), R4 + MOVV b+8(FP), R5 + ADDV R5, R4, R4 + MOVV R4, ret+16(FP) + RET +`)) + }), (*Image).ELFLOONG64Object}, + } + for _, tc := range cases { + obj, err := tc.emit(tc.img) + if err != nil { + t.Fatalf("%s: emit: %v", tc.name, err) + } + checkELFSectionAccounting(t, obj) + ef, err := elf.NewFile(bytes.NewReader(obj)) + if err != nil { + t.Fatalf("%s: parse emitted object: %v", tc.name, err) + } + frame := ef.Section(".debug_frame") + if frame == nil { + t.Errorf("%s: .debug_frame invisible to debug/elf (e_shnum too small?)", tc.name) + ef.Close() + continue + } + if frame.Offset%8 != 0 || frame.Addralign != 8 { + t.Errorf("%s: .debug_frame offset %d align %d, want offset%%8==0 align 8", tc.name, frame.Offset, frame.Addralign) + } + ef.Close() + } +} + +func mustImage(t *testing.T, f func() (*Image, error)) *Image { + t.Helper() + img, err := f() + if err != nil { + t.Fatal(err) + } + return img +} + +// TestELFDWARFRelocations checks the .rela.debug_info and .rela.debug_line +// sections exist and carry absolute 64-bit relocations against the +// function symbols, with r_offsets inside their target sections. +func TestELFDWARFRelocations(t *testing.T) { + img := elfTestImage(t) + obj, err := img.ELFObject() + if err != nil { + t.Fatalf("ELFObject: %v", err) + } + ef, err := elf.NewFile(bytes.NewReader(obj)) + if err != nil { + t.Fatalf("parse emitted object: %v", err) + } + defer ef.Close() + // The DWARF must record the assembled file's path (threaded through + // Image.SourcePath), not a placeholder name. + info, err := ef.Section(".debug_info").Data() + if err != nil { + t.Fatal(err) + } + if img.SourcePath != "t_amd64.s" || !bytes.Contains(info, []byte(img.SourcePath)) { + t.Errorf("DWARF compilation unit does not name the source %q", img.SourcePath) + } + for _, tc := range []struct { + rela string + target string + want uint32 + }{ + {".rela.debug_info", ".debug_info", rX8664Abs64}, + {".rela.debug_line", ".debug_line", rX8664Abs64}, + {".rela.debug_frame", ".debug_frame", rX8664Abs64}, + } { + rs := ef.Section(tc.rela) + if rs == nil { + t.Fatalf("missing %s", tc.rela) + } + if rs.Type != elf.SHT_RELA { + t.Errorf("%s: type %v, want SHT_RELA", tc.rela, rs.Type) + } + target := ef.Section(tc.target) + if target == nil { + t.Fatalf("missing %s", tc.target) + } + if rs.Link == 0 || ef.Sections[rs.Info] != target { + t.Errorf("%s: link %d info %d, want the symtab and %s", tc.rela, rs.Link, rs.Info, tc.target) + } + b, err := rs.Data() + if err != nil { + t.Fatal(err) + } + // .debug_line has one address per function; .debug_info adds the + // compile unit's own low_pc. + want := len(img.Funcs) + if tc.target == ".debug_info" { + want++ + } + if len(b)/24 != want { + t.Errorf("%s: %d entries, want %d", tc.rela, len(b)/24, want) + } + for i := 0; i+24 <= len(b); i += 24 { + r_offset := binary.LittleEndian.Uint64(b[i:]) + info := binary.LittleEndian.Uint64(b[i+8:]) + typ := uint32(info) + sym := int(info >> 32) + if typ != tc.want { + t.Errorf("%s entry %d: type %d, want R_X86_64_64 (%d)", tc.rela, i/24, typ, tc.want) + } + if r_offset >= uint64(target.Size) { + t.Errorf("%s entry %d: r_offset %d outside %s (%d bytes)", tc.rela, i/24, r_offset, tc.target, target.Size) + } + if sym == 0 { + t.Errorf("%s entry %d: against the null symbol", tc.rela, i/24) + } + } + } +} + +// TestELFDataOnly checks a source with GLOBL data and no TEXT emits a valid +// ELF object: the DWARF compilation unit of a code-less image has no +// function to relocate against and must not reach for one. +func TestELFDataOnly(t *testing.T) { + f, errs := parser.Parse("d0_amd64.s", ` +GLOBL table<>(SB), RODATA, $8 +DATA table<>+0(SB)/8, $12345 +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + obj, err := img.ELFObject() + if err != nil { + t.Fatalf("ELFObject: %v", err) + } + checkELFSectionAccounting(t, obj) + ef, err := elf.NewFile(bytes.NewReader(obj)) + if err != nil { + t.Fatalf("parse emitted object: %v", err) + } + defer ef.Close() + syms, err := ef.Symbols() + if err != nil { + t.Fatal(err) + } + found := false + for _, s := range syms { + if s.Name == "table" && s.Size == 8 { + found = true + } + } + if !found { + t.Errorf("data symbol table missing: %v", syms) + } + if ef.Section(".rela.debug_info") != nil || ef.Section(".rela.debug_line") != nil { + t.Error("data-only image must not emit DWARF address relocations") + } +} + // TestELFLinkAndRun is the end-to-end check: assemble the test functions, // link the emitted object with a C driver that defines the external symbol, // and run the result. Skipped when no C compiler is available. @@ -307,4 +609,144 @@ int main(void) { if got := string(run); got != "42 42 7\n" { t.Errorf("output %q, want \"42 42 7\\n\"", got) } + + // The DWARF addresses must have resolved at link time: the .debug_info + // placeholders were carried by .rela.debug_info, so every subprogram's + // low_pc must now equal its linked symbol address. + bin, err := os.ReadFile(appPath) + if err != nil { + t.Fatal(err) + } + lef, err := elf.NewFile(bytes.NewReader(bin)) + if err != nil { + t.Fatalf("parse linked binary: %v", err) + } + defer lef.Close() + syms, err := lef.Symbols() + if err != nil { + t.Fatal(err) + } + addrByName := map[string]uint64{} + for _, s := range syms { + if elf.ST_TYPE(s.Info) == elf.STT_FUNC && s.Value != 0 { + addrByName[s.Name] = s.Value + } + } + lowPCs := dwarfSubprogramLowPCs(t, lef) + if len(lowPCs) == 0 { + t.Fatal("no subprogram DW_AT_low_pc parsed from the linked binary") + } + for name, pc := range lowPCs { + addr, ok := addrByName[name] + if !ok { + t.Errorf("subprogram %q not in the linked symbol table", name) + continue + } + if pc != addr { + t.Errorf("subprogram %q: DW_AT_low_pc = %#x, linked address %#x (DWARF relocation unresolved)", name, pc, addr) + } + } +} + +// dwarfSubprogramLowPCs walks the linked binary's .debug_info with its own +// .debug_abbrev and returns each DW_TAG_subprogram's DW_AT_low_pc by name. +func dwarfSubprogramLowPCs(t *testing.T, ef *elf.File) map[string]uint64 { + t.Helper() + abbrevSec := ef.Section(".debug_abbrev") + infoSec := ef.Section(".debug_info") + if abbrevSec == nil || infoSec == nil { + t.Fatal("linked binary lacks .debug_abbrev or .debug_info") + } + abbrev, err := abbrevSec.Data() + if err != nil { + t.Fatal(err) + } + info, err := infoSec.Data() + if err != nil { + t.Fatal(err) + } + abs := parseAbbrevs(t, abbrev) + le := binary.LittleEndian + out := map[string]uint64{} + r := &ulebIter{b: info} + r.uint32At(t) // unit_length + if v := le.Uint16(info[4:]); v != 5 { + t.Fatalf(".debug_info version %d, want 5", v) + } + r.i = 6 + r.byteAt(t) // unit_type + r.byteAt(t) // address_size + r.uint32At(t) // debug_abbrev_offset + var name string + var lowPC uint64 + for r.i < len(r.b) { + code := r.uleb(t) + if code == 0 { + continue // end of the CU's children + } + ab, ok := abs[code] + if !ok { + t.Fatalf("unknown abbreviation code %d", code) + } + name, lowPC = "", 0 + for _, a := range ab.attrs { + switch a.attr { + case dwAtName: + readFormKeep(t, r, a.form, &name, nil) + case dwAtLowPC: + readFormKeep(t, r, a.form, nil, &lowPC) + default: + readFormSkip(t, r, a.form) + } + } + if ab.tag == dwTagSubprog && name != "" { + out[name] = lowPC + } + } + return out +} + +// readFormKeep reads one DIE attribute value, keeping a string or an +// address into the pointer it was given (nil keeps nothing). +func readFormKeep(t *testing.T, r *ulebIter, form uint64, name *string, addr *uint64) { + t.Helper() + switch form { + case dwFormString: + end := r.i + for end < len(r.b) && r.b[end] != 0 { + end++ + } + if name != nil { + *name = string(r.b[r.i:end]) + } + r.i = end + 1 + case dwFormAddr: + if addr != nil { + *addr = binary.LittleEndian.Uint64(r.b[r.i:]) + } + r.i += 8 + default: + readFormSkip(t, r, form) + } +} + +func readFormSkip(t *testing.T, r *ulebIter, form uint64) { + t.Helper() + switch form { + case dwFormString: + for r.i < len(r.b) && r.b[r.i] != 0 { + r.i++ + } + r.i++ + case dwFormAddr, dwFormData8: + r.i += 8 + case dwFormSecOff: + r.i += 4 + case dwFormExprloc: + r.i += int(r.uleb(t)) + case dwFormData1, 0x0c: + r.i++ + default: + t.Fatalf("unsupported form %#x", form) + } } diff --git a/asm/elfarm64.go b/asm/elfarm64.go index 733d94e..0cfdc55 100644 --- a/asm/elfarm64.go +++ b/asm/elfarm64.go @@ -81,10 +81,15 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) { } // Build relocations. Each SB reference is an ADRP pair: - // ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21 - // ADD → R_AARCH64_ADD_ABS_LO12_NC - // LDR/STR X → R_AARCH64_LDST64_ABS_LO12_NC + // ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21 at the ADRP + // ADD → R_AARCH64_ADD_ABS_LO12_NC at the ADD word + // LDR/STR X → R_AARCH64_LDST64_ABS_LO12_NC at the LDR/STR word // BL → R_AARCH64_CALL26 + // cmd/link's own conversion emits the HI21 at sectoff and the LO12 at + // sectoff+4 (cmd/link/internal/arm64/asm.go), so the ADD or load word + // carries the page-offset relocation, never a second HI21. The + // assembler records two RelArm64Addr relocs per ADRP+ADD pair (one per + // word), so the second of the pair is consumed here. // Addends stay raw: ADR_PREL_PG_HI21 and the ABS_LO12_NC forms resolve // against S+A, and CALL26 branches take the branch instruction's own // place as the PC-relative base, so subtracting the field width (the @@ -97,28 +102,34 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) { } var relas []elfRela for _, fn := range img.Funcs { - for _, r := range fn.Relocs { + for i := 0; i < len(fn.Relocs); i++ { + r := fn.Relocs[i] idx, ok := symIdx[r.Name] if !ok { return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) } - var typ uint32 - switch { - case r.Kind == RelArm64Branch: - typ = rArm64Call26 - case r.Kind == RelArm64LDST64 && r.Off%4 == 4: - typ = rArm64Ldst64Lo12NC - case r.Kind == RelArm64Addr && r.Off%4 == 4: - typ = rArm64AddAbsLo12NC + switch r.Kind { + case RelArm64Branch: + relas = append(relas, elfRela{ + off: uint64(fn.Offset + r.Off), typ: rArm64Call26, sym: idx, addend: r.Addend, + }) + case RelArm64Addr: + // ADRP+ADD: the pair's second reloc (at Off+4) is the + // assembler's twin of the same pair; skip it. + relas = append(relas, + elfRela{off: uint64(fn.Offset + r.Off), typ: rArm64PrelPgHi21, sym: idx, addend: r.Addend}, + elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rArm64AddAbsLo12NC, sym: idx, addend: r.Addend}, + ) + i++ + case RelArm64LDST64: + // ADRP+LDR/STR: one assembler reloc covers the pair. + relas = append(relas, + elfRela{off: uint64(fn.Offset + r.Off), typ: rArm64PrelPgHi21, sym: idx, addend: r.Addend}, + elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rArm64Ldst64Lo12NC, sym: idx, addend: r.Addend}, + ) default: - typ = rArm64PrelPgHi21 + return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind) } - relas = append(relas, elfRela{ - off: uint64(fn.Offset + r.Off), - typ: typ, - sym: idx, - addend: r.Addend, - }) } } @@ -193,15 +204,32 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) { shstrOff := len(out) out = append(out, stSections.bytes()...) - // DWARF debug sections. + // DWARF debug sections; the address placeholders they leave are carried + // as .rela.debug_info/.rela.debug_line entries the system linker applies. dwAlign := func(n int) { for len(out)%n != 0 { out = append(out, 0) } } - dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign) + dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiARM64) + dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present if dw != nil { - nSections += 4 + // Five DWARF sections: .debug_abbrev, .debug_info, .debug_line, + // .debug_line_str and .debug_frame (the CIE is unconditional, so + // the frame section is always present), plus the relocation + // sections below when they carry entries. + dwarfStart = nSections + nSections += 5 + appendDWARFRelas(&out, dw, rAARCH64Abs64, dwAlign) + if dw.infoRelaCount > 0 { + nSections++ + } + if dw.lineRelaCount > 0 { + nSections++ + } + if dw.frameRelaCount > 0 { + nSections++ + } } align(8) @@ -230,13 +258,37 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) { putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24) } putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0) + // DWARF section headers; their indices follow the write order. if dw != nil { + // secIdx is a running section index: each putSh below emits the + // next header, and the sh_info of a .rela section names the index + // of the section it relocates. + secIdx := dwarfStart putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0) + secIdx++ putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0) + secInfoIdx := secIdx + secIdx++ + if dw.infoRelaCount > 0 { + putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24) + secIdx++ + } putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0) + secLineIdx := secIdx + secIdx++ + if dw.lineRelaCount > 0 { + putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24) + secIdx++ + } putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0) + secIdx++ if dw.frameSize > 0 { putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0) + secFrameIdx := secIdx + secIdx++ + if dw.frameRelaCount > 0 { + putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24) + } } } diff --git a/asm/elfarm64_test.go b/asm/elfarm64_test.go index 74697f4..96a8607 100644 --- a/asm/elfarm64_test.go +++ b/asm/elfarm64_test.go @@ -6,6 +6,7 @@ package asm import ( "bytes" "debug/elf" + "encoding/binary" "testing" "sourcedock.dev/petrbalvin/gasm-devkit/parser" @@ -28,6 +29,7 @@ TEXT ·add(SB), NOSPLIT, $0-24 TEXT ·getanswer(SB), NOSPLIT, $0-8 MOVD answer<>(SB), R4 + MOVD $answer<>(SB), R5 MOVD R4, ret+0(FP) RET @@ -102,8 +104,63 @@ DATA answer<>+0(SB)/8, $42 // Check that .rela.text exists (getanswer has SB reference). relaText := ef.Section(".rela.text") if relaText == nil { - t.Error("missing .rela.text section") + t.Fatal("missing .rela.text section") } + + // The SB references of getanswer form two ADRP pairs: the load + // (MOVD answer<>(SB), R4) is ADRP+LDR carrying HI21 at the ADRP and + // LDST64_ABS_LO12_NC at the LDR word, and the address-of + // (MOVD $answer<>(SB), R5) is ADRP+ADD carrying HI21 and + // ADD_ABS_LO12_NC. cmd/link's own conversion emits exactly this + // sectoff / sectoff+4 pairing; a second HI21 at the ADD or LDR word + // corrupts the pair. + raw, err := relaText.Data() + if err != nil { + t.Fatal(err) + } + if len(raw)%24 != 0 || len(raw)/24 != 4 { + t.Fatalf(".rela.text has %d bytes, want four 24-byte entries", len(raw)) + } + wantRela := []struct { + typ elf.R_AARCH64 + off uint64 // relative to the getanswer function start + }{ + {elf.R_AARCH64_ADR_PREL_PG_HI21, 0}, + {elf.R_AARCH64_LDST64_ABS_LO12_NC, 4}, + {elf.R_AARCH64_ADR_PREL_PG_HI21, 8}, + {elf.R_AARCH64_ADD_ABS_LO12_NC, 12}, + } + getanswer := byNameElf(t, ef, "getanswer") + for i, w := range wantRela { + e := raw[i*24 : (i+1)*24] + off := binary.LittleEndian.Uint64(e[0:]) + info := binary.LittleEndian.Uint64(e[8:]) + typ := elf.R_AARCH64(info & 0xffffffff) + sym := int(info >> 32) + if typ != w.typ || off != getanswer.Value+w.off { + t.Errorf("reloc %d: type %v off %d, want %v at %d", i, typ, off, w.typ, getanswer.Value+w.off) + } + if sym != 3 { // NULL, .text, .data, then the first local: answer + t.Errorf("reloc %d: symbol index %d, want 3 (answer)", i, sym) + } + } +} + +// byNameElf returns the symbol table entry for name from the raw .symtab, +// which carries every entry including the null and section symbols in order. +func byNameElf(t *testing.T, ef *elf.File, name string) elf.Symbol { + t.Helper() + syms, err := ef.Symbols() + if err != nil { + t.Fatalf("symbols: %v", err) + } + for _, s := range syms { + if s.Name == name { + return s + } + } + t.Fatalf("symbol %q not found", name) + return elf.Symbol{} } // TestELFAARCH64ObjectNoRelocations checks the ELF output when there are no diff --git a/asm/elfloong64.go b/asm/elfloong64.go index e6ec9f1..3df47d3 100644 --- a/asm/elfloong64.go +++ b/asm/elfloong64.go @@ -13,6 +13,12 @@ import ( const ( emLOONGARCH = 258 // EM_LOONGARCH + // EF_LOONGARCH_ABI_DOUBLE_FLOAT | EF_LOONGARCH_OBJABI_V1: the flags the + // Go toolchain writes (cmd/link/internal/ld/elf.go: Flags = 0x43 for + // Loong64). System linkers refuse to merge ET_REL objects whose float + // ABI differs, so 0 (soft-float) would make the object unlinkable. + efLarchAbiDoubleObjV1 = 0x43 + // LoongArch relocation types (the ELF psABI). rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i) rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st) @@ -187,9 +193,25 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) { out = append(out, 0) } } - dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign) + dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiLOONG64) + dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present if dw != nil { - nSections += 4 + // Five DWARF sections: .debug_abbrev, .debug_info, .debug_line, + // .debug_line_str and .debug_frame (the CIE is unconditional, so + // the frame section is always present), plus the relocation + // sections below when they carry entries. + dwarfStart = nSections + nSections += 5 + appendDWARFRelas(&out, dw, rLarchAbs64, dwAlign) + if dw.infoRelaCount > 0 { + nSections++ + } + if dw.lineRelaCount > 0 { + nSections++ + } + if dw.frameRelaCount > 0 { + nSections++ + } } align(8) @@ -218,13 +240,37 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) { putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24) } putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0) + // DWARF section headers; their indices follow the write order. if dw != nil { + // secIdx is a running section index: each putSh below emits the + // next header, and the sh_info of a .rela section names the index + // of the section it relocates. + secIdx := dwarfStart putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0) + secIdx++ putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0) + secInfoIdx := secIdx + secIdx++ + if dw.infoRelaCount > 0 { + putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24) + secIdx++ + } putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0) + secLineIdx := secIdx + secIdx++ + if dw.lineRelaCount > 0 { + putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24) + secIdx++ + } putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0) + secIdx++ if dw.frameSize > 0 { putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0) + secFrameIdx := secIdx + secIdx++ + if dw.frameRelaCount > 0 { + putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24) + } } } @@ -237,7 +283,7 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) { le.PutUint64(hdr[24:], 0) le.PutUint64(hdr[32:], 0) le.PutUint64(hdr[40:], uint64(shoff)) - le.PutUint32(hdr[48:], 0) + le.PutUint32(hdr[48:], efLarchAbiDoubleObjV1) le.PutUint16(hdr[52:], 64) le.PutUint16(hdr[54:], 0) le.PutUint16(hdr[56:], 0) diff --git a/asm/elfloong64_test.go b/asm/elfloong64_test.go index 227f8a1..4c65e1d 100644 --- a/asm/elfloong64_test.go +++ b/asm/elfloong64_test.go @@ -55,6 +55,11 @@ DATA answer<>+0(SB)/8, $42 if ef.Type != elf.ET_REL || ef.Machine != elf.EM_LOONGARCH { t.Errorf("type/machine = %v/%v, want ET_REL/EM_LOONGARCH", ef.Type, ef.Machine) } + // The double-float ABI plus OBJABI_V1 flags the Go toolchain writes; + // system linkers refuse ABI-mismatched merges. + if flags := binary.LittleEndian.Uint32(obj[48:]); flags != efLarchAbiDoubleObjV1 { + t.Errorf("e_flags = %#x, want %#x (double-float, OBJABI_V1)", flags, efLarchAbiDoubleObjV1) + } text := ef.Section(".text") data := ef.Section(".data") diff --git a/asm/elfriscv.go b/asm/elfriscv.go index 5ff3a6c..b3c5abc 100644 --- a/asm/elfriscv.go +++ b/asm/elfriscv.go @@ -13,8 +13,13 @@ import ( const ( emRISCV = 243 // EM_RISCV + // EF_RISCV_FLOAT_ABI_DOUBLE: the double-precision float ABI the Go + // toolchain targets (cmd/link/internal/ld/elf.go writes Flags = 0x4 for + // RISCV64). System linkers refuse to merge ET_REL objects whose float + // ABI differs, so 0 (soft-float) would make the object unlinkable. + efRISCVFloatAbiDouble = 0x4 + // RISC-V relocation types. - rRISCV32 = 1 rRISCVJAL = 17 // R_RISCV_JAL rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20 rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I @@ -83,9 +88,14 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { // Build relocations. Each SB reference is an AUIPC + second-instruction // pair carrying a single relocation kind; the ELF writer expands it into // the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects. - // The HI20 carries the symbol addend; the LO12 addend is zero, matching - // cmd/link's own ELF conversion (the LO12 resolves against the HI20's - // AUIPC location). + // The HI20 carries the symbol and its addend. The LO12's symbol must + // denote the AUIPC site the HI20 relocates (psABI §8.4.9: the pair is + // resolved against the label of the AUIPC, not the target symbol; + // cmd/link generates one local text symbol per AUIPC for exactly this, + // cmd/link/internal/riscv64/asm.go). The .text section symbol with the + // AUIPC's section-relative offset as addend gives S + A = the AUIPC + // address, which is that label. + const secSymText = 1 // syms[1], the .text section symbol type elfRela struct { off uint64 typ uint32 @@ -99,21 +109,20 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { if !ok { return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) } + auipc := int64(fn.Offset + r.Off) switch r.Kind { case RelRISCVPCRELIType: relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend}, - elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: 0}, + elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: secSymText, addend: auipc}, ) case RelRISCVPCRELSType: relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend}, - elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: 0}, + elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: secSymText, addend: auipc}, ) case RelRISCVJal: relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, sym: idx, addend: r.Addend}) - case RelPCRelAbs: - relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend}) default: return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind) } @@ -196,9 +205,25 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { out = append(out, 0) } } - dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign) + dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiRISCV64) + dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present if dw != nil { - nSections += 4 + // Five DWARF sections: .debug_abbrev, .debug_info, .debug_line, + // .debug_line_str and .debug_frame (the CIE is unconditional, so + // the frame section is always present), plus the relocation + // sections below when they carry entries. + dwarfStart = nSections + nSections += 5 + appendDWARFRelas(&out, dw, rRISCVAbs64, dwAlign) + if dw.infoRelaCount > 0 { + nSections++ + } + if dw.lineRelaCount > 0 { + nSections++ + } + if dw.frameRelaCount > 0 { + nSections++ + } } align(8) @@ -227,13 +252,37 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24) } putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0) + // DWARF section headers; their indices follow the write order. if dw != nil { + // secIdx is a running section index: each putSh below emits the + // next header, and the sh_info of a .rela section names the index + // of the section it relocates. + secIdx := dwarfStart putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0) + secIdx++ putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0) + secInfoIdx := secIdx + secIdx++ + if dw.infoRelaCount > 0 { + putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24) + secIdx++ + } putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0) + secLineIdx := secIdx + secIdx++ + if dw.lineRelaCount > 0 { + putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24) + secIdx++ + } putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0) + secIdx++ if dw.frameSize > 0 { putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0) + secFrameIdx := secIdx + secIdx++ + if dw.frameRelaCount > 0 { + putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24) + } } } @@ -246,7 +295,7 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { le.PutUint64(hdr[24:], 0) le.PutUint64(hdr[32:], 0) le.PutUint64(hdr[40:], uint64(shoff)) - le.PutUint32(hdr[48:], 0) + le.PutUint32(hdr[48:], efRISCVFloatAbiDouble) le.PutUint16(hdr[52:], 64) le.PutUint16(hdr[54:], 0) le.PutUint16(hdr[56:], 0) diff --git a/asm/link.go b/asm/link.go index 70a3494..40c2ff6 100644 --- a/asm/link.go +++ b/asm/link.go @@ -25,6 +25,10 @@ type Image struct { Symbols map[string]int // static symbol → byte offset within the image DataSyms []DataSymbol // GLOBL symbols, in layout order Externals []string // referenced but undefined symbols, sorted + // SourcePath is the assembled file's path, recorded in the DWARF + // sections in place of a placeholder name. Empty when the image was + // not built from a named file. + SourcePath string } // FuncLayout describes one assembled function within an Image. @@ -81,12 +85,9 @@ func (fl *FuncLayout) LineAt(offset int) int { return 0 } -// RelocKind Reloc is one static-symbol reference within a function body: the disp32 -// field at Off (function-relative) must reach the symbol plus Addend, -// measured from After, the address just past the instruction. An External -// relocation names a symbol no GLOBL in the file defines; the object-file -// emitters carry it into the output's relocation table. -// RelocKind discriminates the type of relocation needed. +// RelocKind discriminates the relocation a static-symbol reference needs; +// the encoders record one per SB reference, and the object-file emitters map +// it to their format's relocation type. type RelocKind int const ( @@ -96,7 +97,6 @@ const ( RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair) RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair) RelRISCVJal // R_RISCV_JAL (J-type call) - RelPCRelAbs // 32-bit absolute (R_RISCV_32) RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair) @@ -106,6 +106,13 @@ const ( ) type Reloc struct { + // Off is the function-relative offset of the field the linker patches + // and After the address just past the instruction, the base the + // assembler measures PC-relative displacements from. Name plus + // Addend select the target: the symbol plus the byte offset. An + // External relocation names a symbol no GLOBL in the file defines; + // the object-file emitters carry it into the output's relocation + // table. Off int After int Name string @@ -150,7 +157,7 @@ func AssembleFile(f *ast.File) (*Image, error) { } link := &linkInfo{symbols: known, allowExternal: true} - img := &Image{Symbols: map[string]int{}} + img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} textOff := map[string]int{} type asmFunc struct { name string @@ -267,7 +274,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { return nil, err } - img := &Image{Symbols: map[string]int{}} + img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} for _, d := range f.Decls { t, ok := d.(*ast.Text) if !ok { @@ -339,7 +346,7 @@ func AssembleFileLOONG64(f *ast.File) (*Image, error) { return nil, err } - img := &Image{Symbols: map[string]int{}} + img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} for _, d := range f.Decls { t, ok := d.(*ast.Text) if !ok {