fix(asm): make arm64 and loong64 relocations match the toolchain

This commit is contained in:
2026-09-14 18:22:00 +02:00
parent c6f0286732
commit 953c258d6a
9 changed files with 286 additions and 45 deletions
+24 -15
View File
@@ -12,14 +12,17 @@ import (
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine // assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
// code. Every instruction is 4 bytes; the MOV pseudo-instruction and the // code. Every instruction is 4 bytes; the MOV pseudo-instruction and the
// immediate-arithmetic forms expand to 2–4 instructions when the immediate // immediate-arithmetic forms expand to 2-4 instructions when the immediate
// does not fit, so the layout is computed in two passes (sizes, then encoding // does not fit, so the layout is computed in two passes (sizes, then encoding
// with resolved branch targets). // with resolved branch targets).
// //
// The emitted bytes match the Go toolchain's arm64 assembler, which is the // The emitted bytes match the Go toolchain's arm64 assembler, which is the
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch // ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
// encodings and the MOV immediate expansions all follow cmd/internal/obj/ // encodings and the MOV immediate expansions all follow cmd/internal/obj/
// arm64's asmout cases. // arm64's asmout cases. One deliberate difference: the stack-growth guard
// (the morestack check in the prologue and the call back into the runtime in
// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions
// or zero-frame leaves, where the toolchain emits no guard either.
func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) { func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := arm64ComputeFrame(t) fi := arm64ComputeFrame(t)
prologue := arm64Prologue(fi) prologue := arm64Prologue(fi)
@@ -67,7 +70,12 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err) return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
} }
for j := preCount; j < len(relocs); j++ { for j := preCount; j < len(relocs); j++ {
relocs[j].Off += pc - len(prologue) // Make the relocation offsets function-relative: each instruction
// records its reloc offset relative to its own start, and pc is
// that instruction's offset from the function start (prologue
// included). After shifts by the same amount.
relocs[j].Off += pc
relocs[j].After += pc
} }
preCount = len(relocs) preCount = len(relocs)
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line}) lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
@@ -462,7 +470,7 @@ func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
// ---- MOV pseudo-instruction ---- // ---- MOV pseudo-instruction ----
// encodeARM64Mov encodes the MOV family — the load/store/immediate workhorse // encodeARM64Mov encodes the MOV family, the load/store/immediate workhorse
// of Go's arm64 assembly. MOV is an alias of MOVD (the width mnemonics // of Go's arm64 assembly. MOV is an alias of MOVD (the width mnemonics
// select the access width). The forms, mirroring the toolchain: // select the access width). The forms, mirroring the toolchain:
// //
@@ -839,7 +847,9 @@ func encodeARM64SBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
) )
} }
// encodeARM64SBLoad emits ADRP R20, 0; LDR Rd, [R20, 0] with relocations. // encodeARM64SBLoad emits ADRP R27, 0; LDR Rd, [R27, 0] with relocations,
// matching the toolchain: the scratch register is REGTMP (R27) and the pair
// carries R_ARM64_PCREL_LDST64.
func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([]byte, error) { func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([]byte, error) {
lt, ok := a64LoadTable[mnem] lt, ok := a64LoadTable[mnem]
if !ok { if !ok {
@@ -847,17 +857,17 @@ func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([
} }
if relocs != nil { if relocs != nil {
*relocs = append(*relocs, *relocs = append(*relocs,
Reloc{Off: 0, After: 0, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset}, Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
Reloc{Off: 4, After: 4, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
) )
} }
return a64WordsLE( return a64WordsLE(
a64ADR(1, 0, 0, 20), // ADRP R20, 0 a64ADR(1, 0, 0, 27), // ADRP R27, 0
a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), 0, 20, uint32(rd)), // LDR Rd, [R20, #0] a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), 0, 27, uint32(rd)), // LDR Rd, [R27, #0]
), nil ), nil
} }
// encodeARM64SBStore emits ADRP R20, 0; STR Rs, [R20, 0] with relocations. // encodeARM64SBStore emits ADRP R27, 0; STR Rs, [R27, 0] with relocations,
// matching the toolchain's R27 scratch and R_ARM64_PCREL_LDST64 pair.
func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) ([]byte, error) { func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) ([]byte, error) {
lt, ok := a64LoadTable[mnem] lt, ok := a64LoadTable[mnem]
if !ok { if !ok {
@@ -866,13 +876,12 @@ func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) (
storeOpc := a64StoreOpc(lt) storeOpc := a64StoreOpc(lt)
if relocs != nil { if relocs != nil {
*relocs = append(*relocs, *relocs = append(*relocs,
Reloc{Off: 0, After: 0, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset}, Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
Reloc{Off: 4, After: 4, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
) )
} }
return a64WordsLE( return a64WordsLE(
a64ADR(1, 0, 0, 20), // ADRP R20, 0 a64ADR(1, 0, 0, 27), // ADRP R27, 0
a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), 0, 20, uint32(rs)), // STR Rs, [R20, #0] a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), 0, 27, uint32(rs)), // STR Rs, [R27, #0]
), nil ), nil
} }
@@ -1092,7 +1101,7 @@ func encodeARM64CSEL(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
return a64wordLE(baseOp | uint32(rn)<<16 | invCond<<12 | uint32(rn)<<5 | uint32(rd)), nil return a64wordLE(baseOp | uint32(rn)<<16 | invCond<<12 | uint32(rn)<<5 | uint32(rd)), nil
} }
// CSEL cond, Rn, Rm, Rd (4 operands) — condition first. // CSEL cond, Rn, Rm, Rd (4 operands), condition first.
// Go assembler syntax: CSEL cond, Rn, Rm, Rd // Go assembler syntax: CSEL cond, Rn, Rm, Rd
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5], Rd in bits[4:0]. // ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5], Rd in bits[4:0].
if len(ops) != 4 { if len(ops) != 4 {
+123
View File
@@ -0,0 +1,123 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"encoding/binary"
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// parseArm64File is a helper assembling one arm64 source file.
func parseArm64File(t *testing.T, src string) *Image {
t.Helper()
f, errs := parser.Parse("k_arm64.s", src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileARM64(f)
if err != nil {
t.Fatalf("assemble: %v", err)
}
return img
}
// TestArm64RelocOffsetsIncludePrologue pins the function-relative relocation
// offsets of a framed function: the offsets used to exclude the prologue, so
// every relocation landed on a prologue instruction in the GOOBJ/ELF output.
func TestArm64RelocOffsetsIncludePrologue(t *testing.T) {
img := parseArm64File(t, "TEXT \u00b7f(SB), $16-0\n"+
"\tBL ext\u00b7foo(SB)\n"+
"\tMOVD $gdata(SB), R5\n"+
"\tMOVD $extsym(SB), R6\n"+
"\tRET\n"+
"GLOBL gdata(SB), $8\n")
fn := img.Funcs[0]
// Layout: 12-byte prologue, BL (12), ADRP+ADD (16, 20), ADRP+ADD (24, 28),
// 12-byte epilogue with RET.
want := []struct {
off int
after int
name string
kind RelocKind
external bool
}{
{12, 16, "foo", RelArm64Branch, true},
{16, 16, "gdata", RelArm64Addr, false},
{20, 20, "gdata", RelArm64Addr, false},
{24, 24, "extsym", RelArm64Addr, true},
{28, 28, "extsym", RelArm64Addr, true},
}
if len(fn.Relocs) != len(want) {
t.Fatalf("relocs = %d, want %d", len(fn.Relocs), len(want))
}
for i, w := range want {
r := fn.Relocs[i]
if r.Off != w.off || r.After != w.after || r.Name != w.name || r.Kind != w.kind || r.External != w.external {
t.Errorf("reloc %d = {off %d after %d name %q kind %d ext %v}, want {off %d after %d name %q kind %d ext %v}",
i, r.Off, r.After, r.Name, r.Kind, r.External, w.off, w.after, w.name, w.kind, w.external)
}
}
// The BL with a zero offset sits exactly at the first reloc site.
code := img.Code[fn.Offset : fn.Offset+fn.Size]
if w := binary.LittleEndian.Uint32(code[12:16]); w != 0x94000000 {
t.Errorf("BL word = %08x, want 94000000", w)
}
}
// TestArm64SBLoadStoreMatchesToolchain pins the ADRP scratch register
// (REGTMP, R27) and the LDST64 relocation kind for sym loads and stores,
// against the bytes go tool asm emits for MOVD sym(SB), R5.
func TestArm64SBLoadStoreMatchesToolchain(t *testing.T) {
img := parseArm64File(t, "TEXT \u00b7ld(SB), NOSPLIT, $0\n"+
"\tMOVD sym(SB), R5\n"+
"\tMOVD R5, sym(SB)\n"+
"\tRET\n"+
"GLOBL sym(SB), $8\n")
fn := img.Funcs[0]
code := img.Code[fn.Offset : fn.Offset+fn.Size]
// go tool asm: ADRP 0(PC), R27 (9000001b); MOVD (R27), R5 (f9400365);
// ADRP 0(PC), R27; MOVD R5, (R27) (f9000365).
for off, want := range map[int]uint32{0: 0x9000001b, 4: 0xf9400365, 8: 0x9000001b, 12: 0xf9000365} {
if got := binary.LittleEndian.Uint32(code[off : off+4]); got != want {
t.Errorf("word at %d = %08x, want %08x", off, got, want)
}
}
if len(fn.Relocs) != 2 {
t.Fatalf("relocs = %d, want 2", len(fn.Relocs))
}
for i, w := range []struct{ off, after int }{{0, 8}, {8, 16}} {
r := fn.Relocs[i]
if r.Kind != RelArm64LDST64 {
t.Errorf("reloc %d kind = %d, want RelArm64LDST64 (%d)", i, r.Kind, RelArm64LDST64)
}
if r.Off != w.off || r.After != w.after {
t.Errorf("reloc %d = {off %d after %d}, want {off %d after %d}", i, r.Off, r.After, w.off, w.after)
}
}
}
// TestArm64GOObjRelocTypes checks that GOOBJ emission succeeds with the new
// relocation kinds in play; the detailed layout is covered by the goobj tests.
func TestArm64GOObjRelocTypes(t *testing.T) {
img := parseArm64File(t, "TEXT \u00b7ld(SB), NOSPLIT, $0\n"+
"\tMOVD sym(SB), R5\n"+
"\tMOVD R5, sym(SB)\n"+
"\tRET\n"+
"GLOBL sym(SB), $8\n")
obj, err := img.GOObjectAARCH64("testpkg", "k_arm64.s")
if err != nil {
t.Fatalf("GOObjectAARCH64: %v", err)
}
if len(obj) == 0 {
t.Fatal("empty object")
}
// The detailed layout is covered by the goobj tests; here we only pin
// that emission succeeds with the new relocation kinds in play.
}
+12 -3
View File
@@ -15,8 +15,9 @@ const (
// AArch64 relocation types (the ELF psABI). // AArch64 relocation types (the ELF psABI).
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page) rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD/STR/LDR page offset) rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD page offset)
rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction) rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction)
rArm64Ldst64Lo12NC = 286 // R_AARCH64_LDST64_ABS_LO12_NC (64-bit LDR/STR page offset)
) )
// ELFAARCH64Object returns the image as an ELF64 relocatable object file for // ELFAARCH64Object returns the image as an ELF64 relocatable object file for
@@ -81,7 +82,13 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
// Build relocations. Each SB reference is an ADRP pair: // Build relocations. Each SB reference is an ADRP pair:
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21 // ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21
// ADD/LDR/STR → R_AARCH64_ADD_ABS_LO12_NC // ADD → R_AARCH64_ADD_ABS_LO12_NC
// LDR/STR X → R_AARCH64_LDST64_ABS_LO12_NC
// BL → R_AARCH64_CALL26
// 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
// amd64 R_PCREL convention) would misplace every branch by 4 bytes.
type elfRela struct { type elfRela struct {
off uint64 off uint64
typ uint32 typ uint32
@@ -99,6 +106,8 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
switch { switch {
case r.Kind == RelArm64Branch: case r.Kind == RelArm64Branch:
typ = rArm64Call26 typ = rArm64Call26
case r.Kind == RelArm64LDST64 && r.Off%4 == 4:
typ = rArm64Ldst64Lo12NC
case r.Kind == RelArm64Addr && r.Off%4 == 4: case r.Kind == RelArm64Addr && r.Off%4 == 4:
typ = rArm64AddAbsLo12NC typ = rArm64AddAbsLo12NC
default: default:
@@ -108,7 +117,7 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
off: uint64(fn.Offset + r.Off), off: uint64(fn.Offset + r.Off),
typ: typ, typ: typ,
sym: idx, sym: idx,
addend: r.Addend - int64(r.After-r.Off), addend: r.Addend,
}) })
} }
} }
+1 -1
View File
@@ -102,7 +102,7 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
off: uint64(fn.Offset + r.Off), off: uint64(fn.Offset + r.Off),
typ: typ, typ: typ,
sym: idx, sym: idx,
addend: r.Addend - int64(r.After-r.Off), addend: r.Addend,
}) })
} }
} }
+13 -8
View File
@@ -13,28 +13,33 @@ import (
) )
// GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is // GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is
// the shared one in goobj.go — the toolchain preamble, the go120ld header // the shared one in goobj.go, the toolchain preamble, the go120ld header
// with its block offsets, the string table, the symbol definitions and the // with its block offsets, the string table, the symbol definitions and the
// reloc/aux/data index arrays — with the arm64 preamble, the MinLC of 4 // reloc/aux/data index arrays, with the arm64 preamble, the MinLC of 4
// for the pc-value deltas, and R_ADDRARM64 relocation types for the // for the pc-value deltas, and the arm64 relocation types for the ADRP
// ADRP+ADD/LDR/STR address pairs. // pairs and BL calls.
func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) { func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) {
pre, err := toolchainObjectPreambleAARCH64() pre, err := toolchainObjectPreambleAARCH64()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) { return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
if r.Kind == RelArm64Branch { switch r.Kind {
case RelArm64Branch:
return relocArm64Branch, 4 return relocArm64Branch, 4
case RelArm64LDST64:
return relocArm64LDST64, 4
default:
return relocArm64Addr, 4
} }
return relocArm64Addr, 4
}) })
} }
// arm64 relocation types (cmd/internal/objabi). // arm64 relocation types (cmd/internal/objabi).
const ( const (
relocArm64Addr = 3 // R_ADDRARM64 — ADRP+ADD/LDR/STR pair relocArm64Addr = 3 // R_ADDRARM64, ADRP+ADD pair
relocArm64Branch = 9 // R_CALLARM64 — BL instruction relocArm64Branch = 9 // R_CALLARM64, BL instruction
relocArm64LDST64 = 40 // R_ARM64_PCREL_LDST64, ADRP+LDR/STR pair
) )
// toolchainObjectPreambleAARCH64 returns the "go object ...\n!\n" header // toolchainObjectPreambleAARCH64 returns the "go object ...\n!\n" header
+17 -10
View File
@@ -6,6 +6,7 @@ package asm
import ( import (
"fmt" "fmt"
"sort" "sort"
"strconv"
"sourcedock.dev/petrbalvin/gasm-devkit/ast" "sourcedock.dev/petrbalvin/gasm-devkit/ast"
) )
@@ -15,7 +16,7 @@ import (
// file-local static symbols are encoded RIP-relative and resolved within the // file-local static symbols are encoded RIP-relative and resolved within the
// image, so the raw bytes are self-consistent and executable at any base // image, so the raw bytes are self-consistent and executable at any base
// address; references to external symbols are recorded as relocations // address; references to external symbols are recorded as relocations
// (Funcs[i].Relocs, Externals) and left unresolved — the object-file // (Funcs[i].Relocs, Externals) and left unresolved, the object-file
// emitters turn them into linker relocations. // emitters turn them into linker relocations.
type Image struct { type Image struct {
Code []byte // concatenated function bodies Code []byte // concatenated function bodies
@@ -96,8 +97,9 @@ const (
RelPCRelAbs // 32-bit absolute (R_RISCV_32) RelPCRelAbs // 32-bit absolute (R_RISCV_32)
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
RelArm64Addr // R_ADDRARM64 (ADRP + ADD/LDR/STR pair) RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair)
RelArm64Branch // R_CALLARM64 (BL instruction) RelArm64Branch // R_CALLARM64 (BL instruction)
RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair)
) )
type Reloc struct { type Reloc struct {
@@ -132,7 +134,7 @@ func (img *Image) Bytes() []byte {
// reference to a file-local static symbol becomes a RIP-relative load whose // reference to a file-local static symbol becomes a RIP-relative load whose
// displacement is resolved against that layout; a reference to a symbol no // displacement is resolved against that layout; a reference to a symbol no
// GLOBL defines is recorded as an external relocation (Externals) with its // GLOBL defines is recorded as an external relocation (Externals) with its
// displacement left zero — the object-file emitters resolve it at link // displacement left zero, the object-file emitters resolve it at link
// time, while the raw image (Bytes) cannot represent it. // time, while the raw image (Bytes) cannot represent it.
func AssembleFile(f *ast.File) (*Image, error) { func AssembleFile(f *ast.File) (*Image, error) {
dataSyms, err := collectData(f) dataSyms, err := collectData(f)
@@ -451,13 +453,18 @@ func collectData(f *ast.File) ([]dataSym, error) {
ds.rodata = true ds.rodata = true
case "DUPOK": case "DUPOK":
ds.dupok = true ds.dupok = true
case "1": default:
ds.dupok = true // Legacy numeric flag constants (runtime/textflag.h):
case "8": // DUPOK is 2, RODATA is 8; combinations arrive as one
ds.rodata = true // number (e.g. 10 = RODATA|DUPOK).
case "9": if n, err := strconv.Atoi(f); err == nil {
ds.dupok = true if n&2 != 0 {
ds.rodata = true ds.dupok = true
}
if n&8 != 0 {
ds.rodata = true
}
}
} }
} }
syms = append(syms, ds) syms = append(syms, ds)
+38
View File
@@ -128,3 +128,41 @@ DATA x<>+0(SB)/4, $1
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err) t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
} }
} }
// TestCollectDataNumericFlags pins the numeric GLOBL flag constants from
// runtime/textflag.h: DUPOK is 2, RODATA is 8, and combinations arrive as
// one number (9 = NOPROF|RODATA, 10 = RODATA|DUPOK).
func TestCollectDataNumericFlags(t *testing.T) {
tests := []struct {
flags string
rodata bool
dupok bool
}{
{"2", false, true},
{"8", true, false},
{"9", true, false}, // NOPROF|RODATA, not DUPOK
{"10", true, true}, // RODATA|DUPOK
{"RODATA", true, false},
{"DUPOK", false, true},
{"RODATA|DUPOK", true, true},
}
for _, tt := range tests {
src := "TEXT \u00b7f(SB), NOSPLIT, $0\n\tRET\nGLOBL sym(SB), " + tt.flags + ", $8\n"
f, errs := parser.Parse("f_amd64.s", src)
if len(errs) > 0 {
t.Fatalf("parse %q: %v", tt.flags, errs)
}
img, err := AssembleFile(f)
if err != nil {
t.Fatalf("assemble %q: %v", tt.flags, err)
}
if len(img.DataSyms) != 1 {
t.Fatalf("%q: data syms = %d, want 1", tt.flags, len(img.DataSyms))
}
d := img.DataSyms[0]
if d.Rodata != tt.rodata || d.Dupok != tt.dupok {
t.Errorf("flags %q: rodata=%v dupok=%v, want rodata=%v dupok=%v",
tt.flags, d.Rodata, d.Dupok, tt.rodata, tt.dupok)
}
}
}
+16 -8
View File
@@ -13,14 +13,17 @@ import (
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into // assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
// machine code. Every instruction is 4 bytes; the MOV pseudo-instruction and // machine code. Every instruction is 4 bytes; the MOV pseudo-instruction and
// the immediate-arithmetic forms expand to 2–5 instructions when the // the immediate-arithmetic forms expand to 2-5 instructions when the
// immediate does not fit, so the layout is computed in two passes (sizes, // immediate does not fit, so the layout is computed in two passes (sizes,
// then encoding with resolved branch targets). // then encoding with resolved branch targets).
// //
// The emitted bytes match the Go toolchain's loong64 assembler, which is the // The emitted bytes match the Go toolchain's loong64 assembler, which is the
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch // ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
// encodings and the MOV immediate expansions all follow cmd/internal/obj/ // encodings and the MOV immediate expansions all follow cmd/internal/obj/
// loong64's asmout cases. // loong64's asmout cases. One deliberate difference: the stack-growth guard
// (the morestack check in the prologue and the call back into the runtime in
// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions
// or zero-frame leaves, where the toolchain emits no guard either.
func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) { func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := loong64ComputeFrame(t) fi := loong64ComputeFrame(t)
prologue := loong64Prologue(fi) prologue := loong64Prologue(fi)
@@ -69,7 +72,12 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err) return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
} }
for j := preCount; j < len(relocs); j++ { for j := preCount; j < len(relocs); j++ {
relocs[j].Off += pc - len(prologue) // Make the relocation offsets function-relative: each instruction
// records its reloc offset relative to its own start, and pc is
// that instruction's offset from the function start (prologue
// included). After shifts by the same amount.
relocs[j].Off += pc
relocs[j].After += pc
} }
preCount = len(relocs) preCount = len(relocs)
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line}) lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
@@ -417,7 +425,7 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
case l64Firrr: case l64Firrr:
// ALSL: INSTR $sa, rj, rk, rd (the toolchain's optab places rj in // ALSL: INSTR $sa, rj, rk, rd (the toolchain's optab places rj in
// the second register position); the source amount is 1–4, encoded // the second register position); the source amount is 1-4, encoded
// as sa-1. // as sa-1.
if len(ops) != 4 { if len(ops) != 4 {
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops)) return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
@@ -578,8 +586,8 @@ func encodeLOONG64Branch16(mnem string, op uint32, ops []*ast.Operand, pc int, o
// encodeLOONG64Branch21 encodes a single-register branch: BLTZ/BGEZ and // encodeLOONG64Branch21 encodes a single-register branch: BLTZ/BGEZ and
// BFPT/BFPF use the 21-bit offset form (register in the rj field), while // BFPT/BFPF use the 21-bit offset form (register in the rj field), while
// BGTZ/BLEZ — which the toolchain encodes with the register in the rd field // BGTZ/BLEZ, which the toolchain encodes with the register in the rd field
// and a 16-bit offset — are handled separately. // and a 16-bit offset, are handled separately.
func encodeLOONG64Branch21(mnem string, op uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) { func encodeLOONG64Branch21(mnem string, op uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
if len(ops) != 2 { if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
@@ -692,7 +700,7 @@ func encodeLOONG64ImmArith(mnem string, de l64DualEnc, ops []*ast.Operand) ([]by
} }
// isLoong64ShiftD reports whether a shift-immediate opcode constant is one of // isLoong64ShiftD reports whether a shift-immediate opcode constant is one of
// the 6-bit (.d) variants — the toolchain distinguishes them by the bit // the 6-bit (.d) variants, the toolchain distinguishes them by the bit
// position of the opcode field (bits [25:16]). // position of the opcode field (bits [25:16]).
func isLoong64ShiftD(op uint32) bool { func isLoong64ShiftD(op uint32) bool {
return op&0x03ff0000 != 0 && op>>25 == 0 return op&0x03ff0000 != 0 && op>>25 == 0
@@ -740,7 +748,7 @@ func l64MemOperands(ops []*ast.Operand, fi loong64FrameInfo) (rd, rj int, off in
// ---- the MOV pseudo-instruction ---- // ---- the MOV pseudo-instruction ----
// encodeLOONG64Mov encodes the MOV family — the load/store/immediate // encodeLOONG64Mov encodes the MOV family, the load/store/immediate
// workhorse of Go's loong64 assembly. MOV is an alias of MOVV (the width // workhorse of Go's loong64 assembly. MOV is an alias of MOVV (the width
// mnemonics MOVB/MOVH/MOVW/MOVV/MOVBU/MOVHU/MOVWU/MOVF/MOVD select the // mnemonics MOVB/MOVH/MOVW/MOVV/MOVBU/MOVHU/MOVWU/MOVF/MOVD select the
// access width). The forms, mirroring the toolchain: // access width). The forms, mirroring the toolchain:
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// TestLOONG64RelocOffsetsIncludePrologue pins the function-relative
// relocation offsets of a framed loong64 function: the offsets used to
// exclude the prologue, so every relocation landed on a prologue
// instruction in the GOOBJ/ELF output.
func TestLOONG64RelocOffsetsIncludePrologue(t *testing.T) {
f, errs := parser.Parse("k_loong64.s", "TEXT \u00b7f(SB), $16-0\n"+
"\tMOVV $gdata(SB), R4\n"+
"\tRET\n"+
"GLOBL gdata(SB), $8\n")
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileLOONG64(f)
if err != nil {
t.Fatalf("assemble: %v", err)
}
fn := img.Funcs[0]
// Layout: 12-byte prologue (autosize 32), pcalau12i+addi.d (12, 16),
// epilogue with RET.
if len(fn.Relocs) != 2 {
t.Fatalf("relocs = %d, want 2", len(fn.Relocs))
}
hi, lo := fn.Relocs[0], fn.Relocs[1]
if hi.Kind != RelLoong64AddrHi || hi.Off != 12 || hi.After != 12 {
t.Errorf("hi reloc = {off %d after %d kind %d}, want {off 12 after 12 kind RelLoong64AddrHi}", hi.Off, hi.After, hi.Kind)
}
if lo.Kind != RelLoong64AddrLo || lo.Off != 16 || lo.After != 16 {
t.Errorf("lo reloc = {off %d after %d kind %d}, want {off 16 after 16 kind RelLoong64AddrLo}", lo.Off, lo.After, lo.Kind)
}
}