fix(asm): make arm64 and loong64 relocations match the toolchain
This commit is contained in:
+24
-15
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user