// 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-devkit/parser" ) // TestELFAARCH64Object checks the structure of the emitted AArch64 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 TestELFAARCH64Object(t *testing.T) { f, errs := parser.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 TEXT ·getanswer(SB), NOSPLIT, $0-8 MOVD answer<>(SB), R4 MOVD $answer<>(SB), R5 MOVD 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 := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } obj, err := img.ELFAARCH64Object() if err != nil { t.Fatalf("ELFAARCH64Object: %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_AARCH64 { t.Errorf("type/machine = %v/%v, want ET_REL/EM_AARCH64", ef.Type, ef.Machine) } text := ef.Section(".text") data := ef.Section(".data") if text == nil || data == nil { t.Fatal("missing .text or .data section") } if text.Size == 0 { t.Error(".text section is empty") } syms, err := ef.Symbols() if err != nil { t.Fatalf("symbols: %v", err) } foundAdd, foundGetanswer, foundAnswer := false, false, false for _, s := range syms { switch s.Name { case "add": foundAdd = true if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL { t.Errorf("add: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info) } case "getanswer": foundGetanswer = true if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL { t.Errorf("getanswer: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info) } case "answer": foundAnswer = true if elf.SymType(s.Info&0xf) != elf.STT_OBJECT || elf.SymBind(s.Info>>4) != elf.STB_LOCAL { t.Errorf("answer: info=0x%02x, want STT_OBJECT|STB_LOCAL", s.Info) } } } if !foundAdd { t.Error("symbol 'add' not found") } if !foundGetanswer { t.Error("symbol 'getanswer' not found") } if !foundAnswer { t.Error("symbol 'answer' not found") } // Check that .rela.text exists (getanswer has SB reference). relaText := ef.Section(".rela.text") if relaText == nil { 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 // static-symbol references (no .rela.text section). func TestELFAARCH64ObjectNoRelocations(t *testing.T) { f, errs := parser.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 `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFileARM64(f) if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } obj, err := img.ELFAARCH64Object() if err != nil { t.Fatalf("ELFAARCH64Object: %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 when there are no relocations") } }