// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "bytes" "debug/elf" "encoding/binary" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64 // relocatable object: sections, the symbol table (bindings, types, values, // sizes) and the .rela.text relocation pair for the static-symbol load, // parsed back with debug/elf. func TestELFLOONG64Object(t *testing.T) { f, errs := parser.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 TEXT ·getanswer(SB), NOSPLIT, $0-8 MOVV answer<>(SB), R4 MOVV R4, ret+0(FP) RET GLOBL answer<>(SB), RODATA, $8 DATA answer<>+0(SB)/8, $42 `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileLOONG64(f) if err != nil { t.Fatalf("AssembleFileLOONG64: %v", err) } obj, err := img.ELFLOONG64Object() if err != nil { t.Fatalf("ELFLOONG64Object: %v", err) } ef, err := elf.NewFile(bytes.NewReader(obj)) if err != nil { t.Fatalf("parse emitted object: %v", err) } defer ef.Close() 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") if text == nil || data == nil { t.Fatal("missing .text or .data section") } if text.Flags&elf.SHF_EXECINSTR == 0 || text.Flags&elf.SHF_ALLOC == 0 { t.Errorf(".text flags = %v", text.Flags) } if data.Flags&elf.SHF_WRITE == 0 { t.Errorf(".data flags = %v", data.Flags) } textData, err := text.Data() if err != nil { t.Fatal(err) } if !bytes.Equal(textData, img.Code) { t.Errorf(".text contents differ from the image code") } dataData, err := data.Data() if err != nil { t.Fatal(err) } syms, err := ef.Symbols() if err != nil { t.Fatalf("symbols: %v", err) } byName := map[string]elf.Symbol{} for _, s := range syms { byName[s.Name] = s } wantSym := func(name string, bind elf.SymBind, typ elf.SymType, section elf.SectionIndex, size uint64) { t.Helper() s, ok := byName[name] if !ok { t.Errorf("symbol %q not found", name) return } if elf.ST_BIND(s.Info) != bind || elf.ST_TYPE(s.Info) != typ { t.Errorf("%s: bind/type = %v/%v, want %v/%v", name, elf.ST_BIND(s.Info), elf.ST_TYPE(s.Info), bind, typ) } if s.Section != section { t.Errorf("%s: section = %v, want %v", name, s.Section, section) } if s.Size != size { t.Errorf("%s: size = %d, want %d", name, s.Size, size) } } if ef.Sections[1].Name != ".text" || ef.Sections[2].Name != ".data" { t.Fatalf("section layout = %s, %s; want .text, .data", ef.Sections[1].Name, ef.Sections[2].Name) } textIdx := elf.SectionIndex(1) dataIdx := elf.SectionIndex(2) wantSym("add", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 20) wantSym("getanswer", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 16) wantSym("answer", elf.STB_LOCAL, elf.STT_OBJECT, dataIdx, 8) // The data section carries 16-byte alignment padding; the answer // symbol sits at its padded offset. ans := byName["answer"] if ans.Value+8 > uint64(len(dataData)) { t.Fatalf("answer value %d outside .data (%d bytes)", ans.Value, len(dataData)) } if got := dataData[ans.Value : ans.Value+8]; !bytes.Equal(got, []byte{42, 0, 0, 0, 0, 0, 0, 0}) { t.Errorf("answer data = % x, want $42", got) } // Relocations: the static-symbol load is a pcalau12i+ld.d pair, so one // R_LARCH_PCALA_HI20 and one R_LARCH_PCALA_LO12, both against the local // data symbol. debug/elf does not surface rela entries, so read the // section directly. relaSec := ef.Section(".rela.text") if relaSec == nil { t.Fatal("missing .rela.text") } raw, err := relaSec.Data() if err != nil { t.Fatal(err) } if len(raw)%24 != 0 || len(raw)/24 != 2 { t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw)) } le := binary.LittleEndian for i := range 2 { e := raw[i*24 : (i+1)*24] off := le.Uint64(e[0:]) info := le.Uint64(e[8:]) typ := info & 0xffffffff sym := int(info >> 32) if i == 0 && (typ != uint64(elf.R_LARCH_PCALA_HI20) || off != 20) { t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_HI20 at 20", i, typ, off) } if i == 1 && (typ != uint64(elf.R_LARCH_PCALA_LO12) || off != 24) { t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_LO12 at 24", i, typ, off) } if sym != 3 { // NULL, .text, .data, then the first local: answer t.Errorf("reloc %d: symbol index %d, want 3 (answer)", i, sym) } } } // TestELFLOONG64ObjectNoRelocations checks a file with no static-symbol // references emits a valid object without a .rela.text section. func TestELFLOONG64ObjectNoRelocations(t *testing.T) { f, errs := parser.Parse("n_loong64.s", ` #include "textflag.h" TEXT ·nop(SB), NOSPLIT, $0 RET `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileLOONG64(f) if err != nil { t.Fatalf("AssembleFileLOONG64: %v", err) } obj, err := img.ELFLOONG64Object() if err != nil { t.Fatalf("ELFLOONG64Object: %v", err) } ef, err := elf.NewFile(bytes.NewReader(obj)) if err != nil { t.Fatalf("parse emitted object: %v", err) } defer ef.Close() if ef.Section(".rela.text") != nil { t.Error("unexpected .rela.text section") } syms, err := ef.Symbols() if err != nil { t.Fatal(err) } found := false for _, s := range syms { if s.Name == "nop" && elf.ST_TYPE(s.Info) == elf.STT_FUNC { found = true } } if !found { t.Error("function symbol nop not found") } } // TestELFLOONG64BranchRelocation checks that the morestack call and an // internal CALL both carry R_LARCH_B26 in the emitted object, matching the // Go linker's mapping of its call relocation. func TestELFLOONG64BranchRelocation(t *testing.T) { f, errs := parser.Parse("k_loong64.s", "TEXT \u00b7callbig(SB), $8192-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileLOONG64(f) if err != nil { t.Fatalf("AssembleFileLOONG64: %v", err) } obj, err := img.ELFLOONG64Object() if err != nil { t.Fatalf("ELFLOONG64Object: %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) } // The guard's morestack call plus the body's CALL to other. if len(raw)%24 != 0 || len(raw)/24 != 2 { t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw)) } le := binary.LittleEndian for i := range 2 { info := le.Uint64(raw[i*24+8:]) if elf.R_LARCH(info&0xffffffff) != elf.R_LARCH_B26 { t.Errorf("relocation %d type = %v, want R_LARCH_B26", i, elf.R_LARCH(info&0xffffffff)) } } } // TestELFLOONG64ObjectDataRelocation checks that a symbol-valued DATA field // ("DATA s+0(SB)/8, $other(SB)") reaches the LoongArch ELF object as a // .rela.data entry: an R_LARCH_64 (R_LARCH_32 for a width-4 field) at the // field's offset within .data, against the named symbol, external targets // included. func TestELFLOONG64ObjectDataRelocation(t *testing.T) { f, errs := parser.Parse("t_loong64.s", `#include "textflag.h" TEXT ·Keep(SB), NOSPLIT, $0-0 RET GLOBL holder(SB), NOPTR, $32 DATA holder+0(SB)/8, $·Keep+5(SB) DATA holder+8(SB)/8, $holder(SB) DATA holder+16(SB)/8, $extvar(SB) DATA holder+24(SB)/4, $Keep(SB) `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileLOONG64(f) if err != nil { t.Fatalf("AssembleFileLOONG64: %v", err) } obj, err := img.ELFLOONG64Object() if err != nil { t.Fatalf("ELFLOONG64Object: %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() relaData := ef.Section(".rela.data") if relaData == nil { t.Fatal("missing .rela.data section") } if relaData.Type != elf.SHT_RELA { t.Errorf(".rela.data type = %v, want SHT_RELA", relaData.Type) } if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" { t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link) } if ef.Sections[relaData.Info].Name != ".data" { t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info) } relas, err := relaData.Data() if err != nil { t.Fatal(err) } var got []struct { off uint64 sym uint32 typ uint32 addend int64 } for i := 0; i+24 <= len(relas); i += 24 { got = append(got, struct { off uint64 sym uint32 typ uint32 addend int64 }{ off: binary.LittleEndian.Uint64(relas[i:]), // r_info packs the type in the low dword and the symbol index // in the high dword. typ: binary.LittleEndian.Uint32(relas[i+8:]), sym: binary.LittleEndian.Uint32(relas[i+12:]), addend: int64(binary.LittleEndian.Uint64(relas[i+16:])), }) } // debug/elf hides the table's null entry, so raw index s names syms[s-1]. syms, err := ef.Symbols() if err != nil { t.Fatal(err) } name := func(idx uint32) string { if idx >= 1 && int(idx) <= len(syms) { return syms[idx-1].Name } return "" } // The offsets are data-section-relative: the field's DATA offset plus // the symbol's position in .data (the layout aligns each symbol to 16). base := uint64(0) for _, d := range img.DataSyms { if d.Name == "holder" { base = uint64(d.Offset) } } want := []struct { off uint64 typ uint32 addend int64 target string }{ {off: base + 0, typ: uint32(elf.R_LARCH_64), addend: 5, target: "Keep"}, {off: base + 8, typ: uint32(elf.R_LARCH_64), addend: 0, target: "holder"}, {off: base + 16, typ: uint32(elf.R_LARCH_64), addend: 0, target: "extvar"}, {off: base + 24, typ: uint32(elf.R_LARCH_32), addend: 0, target: "Keep"}, } if len(got) != len(want) { t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want)) } for i, w := range want { g := got[i] if g.off != w.off || g.typ != w.typ || g.addend != w.addend { t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}", i, g.off, g.typ, g.addend, w.off, w.typ, w.addend) } if n := name(g.sym); n != w.target { t.Errorf("entry %d names %q, want %q", i, n, w.target) } } }