feat(asm): materialise frame-relative addresses the way the toolchain does

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 21:34:30 +02:00
1 parent 1120a52a54
commit c1cef7b6e8
4 files changed
+399 -18

No files matched your search

+108 -11
View File
@@ -791,7 +791,7 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
// ADR/ADRP: (label, Rd) with the byte distance split into immlo and
// immhi.
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FADR {
return encodeARM64ADR(mnem, enc.op, ops, pc, offsets, resolve)
return encodeARM64ADR(mnem, enc.op, ops, pc, offsets)
}
// Bitfield extract with wrapping immr: UBFX, SBFX.
@@ -1897,6 +1897,13 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
}
return encodeARM64SBAddr(src.Imm.Sym, rd, relocs), nil
}
// Frame-relative immediate address: MOVD $sym+off(FP|SP), Rd
// materialises the address with ADD/SUB from the hardware SP
// (asm7.go case 4), the shape every runtime address-of-argument
// load is written in.
if src.Imm.Sym != nil && (src.Imm.Sym.Pseudo == "FP" || src.Imm.Sym.Pseudo == "SP") {
return encodeARM64FrameAddr(src.Imm.Sym, dst, mnem, fi)
}
rd := arm64RegNum(operandRegName(dst))
// The FP immediates: FMOVS/FMOVD $f, Fd ride the FMOV (immediate)
// instruction when the 8-bit field carries the value and the
@@ -2066,6 +2073,21 @@ func arm64MovSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
return 8 // ADRP + ADD
}
// The frame-relative immediate address: one ADD/SUB imm12 word in
// the addcon band, the hi<<12 plus lo pair in the 24-bit band, and
// an error (so an irrelevant size) for the pool form beyond either.
if src.Imm.Sym != nil && (src.Imm.Sym.Pseudo == "FP" || src.Imm.Sym.Pseudo == "SP") {
v := arm64FrameAddrValue(src.Imm.Sym, fi)
switch {
case mnem != "MOVD" && mnem != "MOV",
!arm64IsAddcon(v) && !arm64IsAddcon(-v) && (v < 0 || v > 0xFFFFFF):
return 4
case arm64IsAddcon(v) || arm64IsAddcon(-v):
return 4
default:
return 8
}
}
// The con(register) form lowers to the toolchain's ADD/SUB chain:
// one word in the addcon band, two in the 24-bit band, and the two
// pool words (LDR X plus the UXTX add) beyond it.
@@ -2332,6 +2354,17 @@ func encodeARM64RegMove(mnem string, src, dst *ast.Operand) ([]byte, error) {
// (SXTB, SXTH, SXTW), the unsigned byte and halfword forms to UBFM
// (UXTB, UXTH), and only MOVWU to an ORR against WZR. MOVD stays
// ORR Xd, XZR, Xm.
//
// The SP register moves ride the ADD (immediate) form instead: ORR
// cannot address SP and register 31 encodes ZR there (asm7.go case
// 24), whose C_RSP row exists for MOVD alone, so every other width is
// an illegal combination.
if sn, dn := operandRegName(src), operandRegName(dst); sn == "RSP" || sn == "SP" || dn == "RSP" || dn == "SP" {
if mnem != "MOVD" && mnem != "MOV" {
return nil, fmt.Errorf("%s: illegal combination: the SP register move exists for MOVD only", mnem)
}
return a64wordLE(a64AddSub(1, 0, 0, 0, 0, uint32(rs), uint32(rd))), nil
}
if rs != 31 {
switch mnem {
case "MOVB":
@@ -2770,6 +2803,28 @@ func encodeARM64SBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
)
}
// encodeARM64FrameAddr lowers MOVD $sym+off(FP) and its SP spelling: the
// address rides ADD/SUB from the hardware SP, one imm12 word inside the
// addcon band and the hi<<12 plus lo pair inside the 24-bit band (asm7.go
// optab case 4). Beyond either the toolchain pools the constant (case 34),
// and no pool the plain form reaches exists here, so that shape is refused.
// Every other width is an illegal combination: the toolchain's AACON rows
// exist for MOVD alone.
func encodeARM64FrameAddr(sym *ast.Symbol, dst *ast.Operand, mnem string, fi arm64FrameInfo) ([]byte, error) {
if mnem != "MOVD" && mnem != "MOV" {
return nil, fmt.Errorf("%s: illegal combination: the $frame-address form exists for MOVD only", mnem)
}
rd := arm64RegNum(operandRegName(dst))
if rd < 0 {
return nil, fmt.Errorf("%s %s: invalid destination register", mnem, sym.Raw)
}
v := arm64FrameAddrValue(sym, fi)
if !arm64IsAddcon(v) && !arm64IsAddcon(-v) && (v < 0 || v > 0xFFFFFF) {
return nil, fmt.Errorf("%s: %s needs the literal pool, which no arm64 pool reaches here", mnem, sym.Raw)
}
return a64WordsLE(arm64FrameAddrWords(v, rd)...), nil
}
// 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. FMOVQ has no LDST relocation width, so it
@@ -3820,7 +3875,7 @@ func encodeARM64DP1(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, err
// target split into immlo (bits 30:29) and immhi (bits 23:5), with bit 31
// selecting the page form. An n(PC) operand resolves to the instruction's
// own address: the toolchain rewrites it away and encodes displacement 0.
func encodeARM64ADR(mnem string, page uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
func encodeARM64ADR(mnem string, page uint32, ops []*ast.Operand, pc int, offsets map[string]int) ([]byte, error) {
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
@@ -3830,7 +3885,10 @@ func encodeARM64ADR(mnem string, page uint32, ops []*ast.Operand, pc int, offset
}
var rel int64
if _, pcRel := arm64PCRelOffset(ops[0]); !pcRel {
target := resolve(arm64Label(ops[0]))
// The label resolves to its own statement: the toolchain's
// jump-to-jump collapse rewrites p.To alone (obj/pass.go), so an
// ADR/ADRP's From-side label is never chased through a chain.
target := arm64Label(ops[0])
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q", target)
@@ -5816,23 +5874,62 @@ func arm64ResolveAliases(f *ast.File) {
}
aliases := map[string]alias{}
raws := map[string]string{}
// The directive lines the parser records include the dead branches of
// every conditional, and a define inside a disabled region must not
// become an alias (go_tls.h's `#ifdef GOARCH_arm \n #define LR R14`
// must stay dead on arm64, where LR is R30). The liveness walk mirrors
// the preprocessor's: a conditional is live when its name was defined
// by a live define earlier in the file; every other name is undefined,
// the predefines (GOARCH_arm64 and friends) included, which the
// assembler never sees.
seen := map[string]bool{} // every live-defined name, any macro shape
cond := []bool{} // one entry per open #ifdef/#ifndef: its truth
live := func() bool {
for _, c := range cond {
if !c {
return false
}
}
return true
}
for _, d := range f.Decls {
pre, ok := d.(*ast.Preproc)
if !ok {
continue
}
fields := strings.Fields(pre.Raw)
if len(fields) < 3 || fields[0] != "define" {
if len(fields) == 0 {
continue
}
name, body := fields[1], strings.Join(fields[2:], " ")
// A parameterised macro spells its parameter list right after the
// name; a multi-instruction body needs statement expansion.
if strings.ContainsAny(name, "(") || body == "" ||
strings.HasPrefix(body, "(") || strings.ContainsAny(body, "();") {
continue
switch fields[0] {
case "ifdef", "ifndef":
truth := false
if len(fields) >= 2 {
truth = seen[fields[1]] != (fields[0] == "ifndef")
}
cond = append(cond, truth)
case "else":
if len(cond) > 0 {
cond[len(cond)-1] = !cond[len(cond)-1]
}
case "endif":
if len(cond) > 0 {
cond = cond[:len(cond)-1]
}
case "define":
if !live() || len(fields) < 3 {
continue
}
name, body := fields[1], strings.Join(fields[2:], " ")
seen[name] = true
// A parameterised macro spells its parameter list right after
// the name; a multi-instruction body needs statement expansion.
if strings.ContainsAny(name, "(") || body == "" ||
strings.HasPrefix(body, "(") || strings.ContainsAny(body, "();") {
continue
}
raws[name] = body
}
raws[name] = body
}
// Alias bodies may name other aliases (hlp1 → res_ptr → R0): substitute
// transitively until nothing changes, bounded against cycles.