diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index efa408e..591e9a0 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -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. diff --git a/asm/arm64_frame.go b/asm/arm64_frame.go index 231f70c..14fc25f 100644 --- a/asm/arm64_frame.go +++ b/asm/arm64_frame.go @@ -403,6 +403,59 @@ func arm64ResolvePseudo(sym *ast.Symbol, fi arm64FrameInfo) (base int, off int32 return -1, 0 } +// arm64FrameAddrValue returns the SP-relative displacement a $sym+off(FP) +// or $sym+off(SP) immediate-address operand stands for, the toolchain's +// aclass arithmetic (asm7.go): a parameter reference sits autosize+8 above +// the hardware SP, and a pseudo-SP reference sits frame+8 above it, the +// alignment padding cancelling out of the autosize. +func arm64FrameAddrValue(sym *ast.Symbol, fi arm64FrameInfo) int64 { + switch sym.Pseudo { + case "FP": + return sym.Offset + int64(fi.autosize) + 8 + default: // SP + return sym.Offset + int64(fi.frame) + 8 + } +} + +// arm64IsAddcon reports whether v is an addcon value (asm7.go isaddcon): an +// unsigned imm12, or a multiple of 4096 whose shifted form fits imm12. +func arm64IsAddcon(v int64) bool { + if v < 0 { + return false + } + if v&0xFFF == 0 { + v >>= 12 + } + return v <= 0xFFF +} + +// arm64FrameAddrWords returns the word sequence of the toolchain's optab +// case 4 for a frame-relative address (the C_AACON and C_AACON2 rows): one +// ADD/SUB imm12 word inside the addcon band, SUB carrying a negative +// displacement, and otherwise the hi<<12 word from SP followed by the low +// word added in place. The 24-bit band never reaches the REGTMP +// materialisation the ADD/SUB immediate ladder uses: aclass classifies the +// address straight into C_AACON2. +func arm64FrameAddrWords(v int64, rd int) []uint32 { + word := func(v int64, rn int) uint32 { + op := uint32(0) // ADD + if v < 0 { + op = 1 // SUB + v = -v + } + sh := uint32(0) + if v&0xFFF000 != 0 { // asm7.go oaddi: the shift form when low 12 bits are clear + sh = 1 + v >>= 12 + } + return a64AddSub(1, op, 0, sh, uint32(v), uint32(rn), uint32(rd)) + } + if arm64IsAddcon(v) || arm64IsAddcon(-v) { + return []uint32{word(v, 31)} + } + return []uint32{word(v&^int64(0xFFF), 31), word(v&0xFFF, rd)} +} + // arm64PreStoreImm encodes a pre-index store (STR with writeback): // size<<30 | 7<<27 | V<<26 | opc<<22 | 1<<11 | 1<<10 | imm9<<12 | Rn<<5 | Rt. func arm64PreStoreImm(size, V int, imm9 int32, rn, rt int) uint32 { diff --git a/asm/arm64_frameaddr_test.go b/asm/arm64_frameaddr_test.go new file mode 100644 index 0000000..9343dc4 --- /dev/null +++ b/asm/arm64_frameaddr_test.go @@ -0,0 +1,236 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "strings" + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/ast" + "sourcedock.dev/petrbalvin/gasm-sdk/parser" +) + +// assembleArm64Source parses src and assembles it for arm64, returning the +// first function's words little-endian. +func assembleArm64Source(t *testing.T, src string) []uint32 { + t.Helper() + f, errs := parser.Parse("test_arm64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFileARM64(f) + if err != nil { + t.Fatalf("AssembleFileARM64: %v", err) + } + if len(img.Funcs) != 1 { + t.Fatalf("got %d funcs, want 1", len(img.Funcs)) + } + return wordsOf(img.Code[img.Funcs[0].Offset : img.Funcs[0].Offset+img.Funcs[0].Size]) +} + +func TestArm64FrameAddrEncoding(t *testing.T) { + src := `#include "textflag.h" + +TEXT ·fr(SB), NOSPLIT, $432-24 + MOVD $argframe+0(FP), R3 + MOVD $big+4096(FP), R4 + MOVD $ret-8(FP), R2 + MOVD $x-64(FP), R5 + MOVD RSP, R19 + MOVD R20, RSP + RET +` + ws := assembleArm64Source(t, src) + // Prologue (4: large frame) then the body at words 4..9, the toolchain's + // own encodings for the same statements: + // ADD $456, RSP, R3 (456 = 448 + 8 + 0) + // ADD $(1<<12), RSP, R4 (4552, the hi<<12 half) + // ADD $456, R4, R4 (then the lo half) + // ADD $448, RSP, R2 (448 - 8 + 8) + // ADD $392, RSP, R5 (448 - 64 + 8) + // ADD $0, RSP, R19 (the SP register move) + // ADD $0, R20, RSP + want := []uint32{ + 0xd10703f4, 0xa93ffa9d, 0x9100029f, 0xd10023fd, + 0x910723e3, 0x914007e4, 0x91072084, 0x910703e2, + 0x910623e5, 0x910003f3, 0x9100029f, + } + if len(ws) < len(want) { + t.Fatalf("got %d words, want at least %d", len(ws), len(want)) + } + for i, w := range want { + if ws[i] != w { + t.Errorf("word %d: got %08x, want %08x", i, ws[i], w) + } + } +} + +func TestArm64FrameAddrSizes(t *testing.T) { + // One imm12 word inside the addcon band, two inside the 24-bit band. + tests := []struct { + v int64 + verb int + }{ + {456, 1}, + {0xFFF, 1}, + {0x1000, 1}, // the shifted imm12 form + {0x1005, 2}, // the hi<<12 plus lo pair + {0xFFFFFF, 2}, + } + for _, tt := range tests { + got := len(arm64FrameAddrWords(tt.v, 3)) + if got != tt.verb { + t.Errorf("arm64FrameAddrWords(%d) took %d words, want %d", tt.v, got, tt.verb) + } + } + if ws := arm64FrameAddrWords(0x1000, 3); len(ws) != 1 || ws[0] != 0x914007e3 { + t.Errorf("arm64FrameAddrWords(0x1000) = %08x, want the shifted ADD 914007e3", ws[0]) + } + if !arm64IsAddcon(0xFFF) || arm64IsAddcon(0x1001) || arm64IsAddcon(-1) { + t.Error("arm64IsAddcon misclassifies the band edges") + } + fp := &ast.Symbol{Name: "x", Pseudo: "FP", Offset: 8} + if v := arm64FrameAddrValue(fp, arm64FrameInfo{autosize: 448}); v != 464 { + t.Errorf("FP address: got %d, want 464", v) + } + sp := &ast.Symbol{Name: "x", Pseudo: "SP", Offset: 8} + if v := arm64FrameAddrValue(sp, arm64FrameInfo{frame: 432}); v != 448 { + t.Errorf("SP address: got %d, want 448", v) + } +} + +func TestArm64FrameAddrRejects(t *testing.T) { + tests := []struct { + name string + src string + want string + }{ + { + "width", `TEXT ·f(SB), NOSPLIT, $16-8 + MOVW $x+0(FP), R7 + RET +`, "illegal combination", + }, + { + "pool band", `TEXT ·f(SB), NOSPLIT, $20000000-8 + MOVD $x+20000000(FP), R7 + RET +`, "literal pool", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, errs := parser.Parse("test_arm64.s", tt.src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + _, err := AssembleFileARM64(f) + if err == nil { + t.Fatalf("%s: no error, want one naming %q", tt.name, tt.want) + } + if !strings.Contains(err.Error(), tt.want) { + t.Errorf("%s: error %q, want it to name %q", tt.name, err, tt.want) + } + }) + } +} + +func TestArm64SPMoveEncoding(t *testing.T) { + src := `TEXT ·f(SB), NOSPLIT, $0-8 + MOVD RSP, R19 + MOVD R20, RSP + MOVD ZR, R4 + MOVD RSP, RSP + RET +` + ws := assembleArm64Source(t, src) + // The SP register moves ride ADD $0; the zero move stays ORR. + want := []uint32{ + 0x910003f3, // ADD $0, RSP, R19 + 0x9100029f, // ADD $0, R20, RSP + 0xaa1f03e4, // ORR R4, ZR, ZR + 0x910003ff, // ADD $0, RSP, RSP + 0xd65f03c0, // RET + } + if len(ws) != len(want) { + t.Fatalf("got %d words, want %d", len(ws), len(want)) + } + for i, w := range want { + if ws[i] != w { + t.Errorf("word %d: got %08x, want %08x", i, ws[i], w) + } + } + + rej := `TEXT ·f(SB), NOSPLIT, $0-8 + MOVW RSP, R7 + RET +` + f, errs := parser.Parse("test_arm64.s", rej) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + if _, err := AssembleFileARM64(f); err == nil || !strings.Contains(err.Error(), "illegal combination") { + t.Errorf("MOVW RSP: error %v, want an illegal-combination refusal", err) + } +} + +func TestArm64AliasLiveness(t *testing.T) { + // A define inside a dead conditional branch must not become an alias: + // go_tls.h's `#ifdef GOARCH_arm` block defines LR as R14, which would + // silently renumber the link register on arm64, where LR is R30. + src := `#define RARG R5 +#ifdef GOARCH_arm +#define LR R14 +#endif +TEXT ·f(SB), NOSPLIT, $0-8 + MOVD LR, R0 + MOVD RARG, R1 + MOVD R14, R2 + RET +` + ws := assembleArm64Source(t, src) + want := []uint32{ + 0xaa1e03e0, // ORR R0, ZR, R30: LR stayed the link register + 0xaa0503e1, // ORR R1, ZR, R5: the live alias applied + 0xaa0e03e2, // ORR R2, ZR, R14: R14 is R14 + 0xd65f03c0, + } + if len(ws) != len(want) { + t.Fatalf("got %d words, want %d", len(ws), len(want)) + } + for i, w := range want { + if ws[i] != w { + t.Errorf("word %d: got %08x, want %08x", i, ws[i], w) + } + } +} + +func TestArm64ADRNoChainChase(t *testing.T) { + // The toolchain's jump-to-jump collapse rewrites branch targets only: + // a B to a chain-leading label is redirected, an ADR to the same label + // resolves to the label itself. + src := `TEXT ·adrchain(SB), NOSPLIT, $0-0 + B a +a: + B b +b: + ADR a, R0 + RET +` + ws := assembleArm64Source(t, src) + want := []uint32{ + 0x14000002, // B +2: chased through a to b + 0x14000001, // B +1: a's own jump to b + 0x10ffffe0, // ADR a, R0: -4, the label itself, unchased + 0xd65f03c0, + } + if len(ws) != len(want) { + t.Fatalf("got %d words, want %d", len(ws), len(want)) + } + for i, w := range want { + if ws[i] != w { + t.Errorf("word %d: got %08x, want %08x", i, ws[i], w) + } + } +} diff --git a/asm/arm64_goroot_test.go b/asm/arm64_goroot_test.go index b94f89f..610f126 100644 --- a/asm/arm64_goroot_test.go +++ b/asm/arm64_goroot_test.go @@ -55,12 +55,9 @@ var gorootARM64Packages = []string{ // not silently dropped: the gaps are findings, and closing one is a matter // of removing its entry and watching the file pin itself. var gorootARM64GapFiles = map[string]string{ - "runtime/asm_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge, and MOVD $sym+off(FP) materialises from ZR", - "runtime/asan_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", - "runtime/libfuzzer_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", - "runtime/msan_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", + "runtime/asm_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge", + "runtime/sys_linux_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge (cgoSigtramp, clone)", "runtime/tls_arm64.s": "a branch distance diverges: an earlier statement's expansion is sized differently", - "runtime/valgrind_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", "crypto/internal/fips140/aes/aes_arm64.s": "a tested immediate bit in a TBZ diverges", "crypto/internal/fips140/aes/gcm/gcm_arm64.s": "an ADD immediate's value diverges mid-function", "crypto/internal/fips140/nistec/p256_asm_arm64.s": "a branch distance and a logical-immediate lowering diverge", @@ -68,9 +65,7 @@ var gorootARM64GapFiles = map[string]string{ "runtime/preempt_arm64.s": "the prologue shape for the function's frame diverges at the first word", "runtime/race_arm64.s": "the flag list and prologue shape diverge at the first words", "runtime/rt0_linux_arm64.s": "the #ifdef GOOS selection diverges: gasm keeps a word the toolchain drops", - "runtime/sys_linux_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", "internal/runtime/syscall/linux/asm_linux_arm64.s": "a branch distance diverges: an earlier statement's expansion is sized differently", - "reflect/asm_arm64.s": "MOVD $sym+off(FP) materialises MOV from ZR where the toolchain emits ADD $off, SP, Rd", } // gorootOtherGOOS matches the file names of the ports the linux build never