From 56ecc3953949863623141338c79c7745cadcc54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 9 Jul 2026 15:56:03 +0200 Subject: [PATCH] feat(asm): assemble static symbols and the whole go-flac AVX2 kernel byte-identically Assisted-by: Qwen 3.8 Max Preview --- asm/assemble.go | 92 +++++++++++++++----- asm/encode.go | 29 ++++++- asm/instrs.go | 18 +++- asm/link.go | 162 +++++++++++++++++++++++++++++++++++ asm/link_test.go | 196 +++++++++++++++++++++++++++++++++++++++++++ asm/operand.go | 12 +++ asm/vex.go | 15 +++- cmd/gasm/main.go | 57 ++++++++----- docs/ARCHITECTURE.md | 24 ++++-- justfile | 2 +- 10 files changed, 553 insertions(+), 54 deletions(-) create mode 100644 asm/link.go create mode 100644 asm/link_test.go diff --git a/asm/assemble.go b/asm/assemble.go index f4f360f..d4595e9 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -23,6 +23,31 @@ import ( // operands require relocations and are not yet supported; the SIMD (VEX/AVX2) // integer and shuffle/extract/permute/move set is in. func Assemble(t *ast.Text) ([]byte, map[string]int, error) { + code, _, labels, err := assemble(t, nil) + return code, labels, err +} + +// linkInfo carries file-level symbol context into a single-function assembly: +// the set of static symbols a GLOBL in the same file defines. A nil link +// rejects SB operands outright (single-function assembly cannot resolve +// them). +type linkInfo struct { + symbols map[string]bool +} + +// sbPatch is a function-relative static-symbol relocation: the disp32 field +// at off must become the symbol's address minus after, where after is the +// function-relative address just past the instruction. +type sbPatch struct { + off int + after int + name string + addend int64 +} + +// assemble encodes a TEXT body, returning the machine code, the static-symbol +// patch sites (for the file-level layout to resolve) and the label table. +func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, error) { fi := computeFrame(t) chain := jumpChain(t) resolve := func(name string) string { @@ -44,9 +69,9 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) { case *ast.Label: offsets[s.Name.Text] = pos case *ast.Instr: - sz, err := instrSize(s, fi, long[i]) + sz, err := instrSize(s, fi, long[i], link) if err != nil { - return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) } sizes[i] = sz pcs[i] = pos @@ -85,23 +110,25 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) { // Pass 2: emit. out := append([]byte(nil), fi.prologue...) + var patches []sbPatch pos := len(fi.prologue) for i, stmt := range t.Body { s, ok := stmt.(*ast.Instr) if !ok { continue } - code, err := encodeInstr(s, pos, offsets, fi, long[i], resolve) + code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link) if err != nil { - return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) } if len(code) != sizes[i] { - return nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i]) + return nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i]) } + patches = append(patches, ps...) out = append(out, code...) pos += len(code) } - return out, offsets, nil + return out, patches, offsets, nil } // jumpChain precomputes jump-to-jump folding: a label whose first instruction @@ -214,12 +241,12 @@ func addSP(size int) []byte { // ADDQ $size, SP // instrSize returns the encoded length of an instruction (layout pass). // encodeInstr already includes the epilogue for a RET in a frame-pointer // function; jumps use their short or long form (never an epilogue). -func instrSize(s *ast.Instr, fi frameInfo, long bool) (int, error) { +func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, error) { mnem := strings.ToUpper(s.Mnemonic.Text) if isJumpMnemonic(mnem) { return jumpSize(mnem, long), nil } - code, err := encodeInstr(s, 0, nil, fi, false, nil) + code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link) if err != nil { return 0, err } @@ -254,7 +281,7 @@ func jumpSize(mnem string, long bool) int { // (relative to pc, the instruction's own offset). A RET in a frame-pointer // function is prefixed with the epilogue. resolve, when non-nil, redirects a // jump label through the jump-to-jump chain before the offset lookup. -func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string) ([]byte, error) { +func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string, link *linkInfo) ([]byte, []sbPatch, error) { mnem := strings.ToUpper(s.Mnemonic.Text) var prefix []byte @@ -263,32 +290,48 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon } var code []byte + var ps []sbPatch var err error if isJumpMnemonic(mnem) { code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve) } else { - code, err = encodeNormal(s, fi) + code, ps, err = encodeNormal(s, fi, link) } if err != nil { - return nil, err + return nil, nil, err } - return append(prefix, code...), nil + // Anchor the patch fields at function-relative positions: off indexes the + // disp32 field, after is the address just past the instruction. + body := pc + len(prefix) + for i := range ps { + ps[i].off += body + ps[i].after = body + len(code) + } + return append(prefix, code...), ps, nil } -func encodeNormal(s *ast.Instr, fi frameInfo) ([]byte, error) { +func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, error) { _, size := splitSize(strings.ToUpper(s.Mnemonic.Text)) if size == 0 { size = 8 } ops := make([]Operand, len(s.Operands)) for i, op := range s.Operands { - o, err := operandFromAST(op, size, fi) + o, err := operandFromAST(op, size, fi, link) if err != nil { - return nil, err + return nil, nil, err } ops[i] = o } - return Encode(s.Mnemonic.Text, ops...) + e := &enc{} + if err := e.encode(s.Mnemonic.Text, ops); err != nil { + return nil, nil, err + } + ps := make([]sbPatch, len(e.patches)) + for i, p := range e.patches { + ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend} + } + return e.out, ps, nil } // encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the @@ -345,7 +388,7 @@ var spReg = Reg{idx: 4, size: 8} // operandFromAST converts a parsed operand into an encoder Operand, applying // the frame translation to FP/SP pseudo-register operands. -func operandFromAST(op *ast.Operand, size int, fi frameInfo) (Operand, error) { +func operandFromAST(op *ast.Operand, size int, fi frameInfo, link *linkInfo) (Operand, error) { switch op.Kind { case ast.OpImmediate: if op.Imm.HasVal { @@ -371,9 +414,20 @@ func operandFromAST(op *ast.Operand, size int, fi frameInfo) (Operand, error) { off := fi.spAdjust + a.Sym.Offset return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil } - // SB (global symbol) needs a relocation — not yet supported. + // SB (global symbol): a static (file-local, <>) symbol becomes a + // RIP-relative reference resolved by the file-level layout; anything + // external needs object-file emission. if a.Sym != nil && a.Sym.Pseudo == "SB" { - return nil, fmt.Errorf("SB (global symbol) operands need relocation support (pending)") + if !a.Sym.Static { + return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name) + } + if link == nil || link.symbols == nil { + return nil, fmt.Errorf("static symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name) + } + if !link.symbols[a.Sym.Name] { + return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name) + } + return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil } // Memory with a real base register: (base), off(base), (base)(index*scale). diff --git a/asm/encode.go b/asm/encode.go index eb85f8e..cf9651c 100644 --- a/asm/encode.go +++ b/asm/encode.go @@ -19,7 +19,16 @@ func Encode(mnemonic string, ops ...Operand) ([]byte, error) { } type enc struct { - out []byte + out []byte + patches []encPatch // disp32 fields awaiting static-symbol resolution +} + +// encPatch marks a 4-byte displacement field in enc.out that must receive the +// RIP-relative offset of a static symbol once the file layout is settled. +type encPatch struct { + off int + name string + addend int64 } func (e *enc) encode(mnem string, ops []Operand) error { @@ -120,6 +129,14 @@ type instr struct { sib int // -1 if absent disp []byte imm []byte + sb *sbRef // static-symbol displacement in disp, awaiting resolution +} + +// sbRef records that an instruction's displacement refers to a static symbol +// rather than holding a literal value. +type sbRef struct { + name string + addend int64 } func (e *enc) emit(i *instr) error { @@ -152,6 +169,9 @@ func (e *enc) emit(i *instr) error { if i.sib >= 0 { e.out = append(e.out, byte(i.sib)) } + if i.sb != nil { + e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend}) + } e.out = append(e.out, i.disp...) e.out = append(e.out, i.imm...) return nil @@ -199,6 +219,13 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i return nil case Mem: return setMem(i, regField, r) + case sbMem: + // RIP-relative reference; the displacement is patched once the static + // symbol's address is known. + i.modrm = regField<<3 | 0x05 // mod=00, rm=101 → (RIP)+disp32 + i.disp = le32(0) + i.sb = &sbRef{name: r.name, addend: r.addend} + return nil default: return fmt.Errorf("invalid r/m operand %T", rm) } diff --git a/asm/instrs.go b/asm/instrs.go index 364742b..ac7dc4d 100644 --- a/asm/instrs.go +++ b/asm/instrs.go @@ -78,6 +78,17 @@ func (e *enc) encodeMov(ops []Operand, size int) error { } return e.emit(i) + case sbMem: + if !dstIsReg { + return fmt.Errorf("MOV: two memory operands") + } + // MOV r, r/m: reg=dst, rm=src(static symbol). + i := newInstr(size, []byte{movRR(size)}) + if err := setRM(i, dstReg, src, size); err != nil { + return err + } + return e.emit(i) + case Imm: if dstIsReg { // MOV r, imm: 0xB0+reg (8-bit) / 0xB8+reg (16/32/64, imm64 for Q). @@ -280,12 +291,13 @@ func (e *enc) encodeLea(ops []Operand, size int) error { if !ok { return fmt.Errorf("LEA: destination must be a register") } - mem, ok := src.(Mem) - if !ok { + switch src.(type) { + case Mem, sbMem: + default: return fmt.Errorf("LEA: source must be a memory operand") } i := newInstr(size, []byte{0x8D}) - if err := setRM(i, dstReg, mem, size); err != nil { + if err := setRM(i, dstReg, src, size); err != nil { return err } return e.emit(i) diff --git a/asm/link.go b/asm/link.go new file mode 100644 index 0000000..b65a3ec --- /dev/null +++ b/asm/link.go @@ -0,0 +1,162 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "fmt" + + "sourcedock.dev/petrbalvin/gasm-devkit/ast" +) + +// Image is an assembled file: the function bodies laid out in source order, +// followed by the file's static data section (GLOBL/DATA). Static-symbol +// references are encoded RIP-relative and resolved within the image, so the +// bytes are self-consistent and executable at any base address. +type Image struct { + Code []byte // concatenated function bodies + Data []byte // static data section + Funcs []FuncLayout // function positions, in source order + Symbols map[string]int // static symbol → byte offset within the image +} + +// FuncLayout describes one assembled function within an Image. +type FuncLayout struct { + Name string + Offset int // start offset within the image (== offset within Code) + Size int + Labels map[string]int // local labels, function-relative +} + +// Bytes returns the whole image: code, then data. +func (img *Image) Bytes() []byte { + out := make([]byte, 0, len(img.Code)+len(img.Data)) + out = append(out, img.Code...) + return append(out, img.Data...) +} + +// AssembleFile assembles every TEXT function of a parsed file and lays out +// its static symbols (GLOBL/DATA) in a data section behind the code. Each +// static-symbol reference becomes a RIP-relative load whose displacement is +// resolved against that layout. External (non-file-local) symbol references +// are rejected: they need object-file emission. +func AssembleFile(f *ast.File) (*Image, error) { + syms, order, err := collectData(f) + if err != nil { + return nil, err + } + known := make(map[string]bool, len(syms)) + for name := range syms { + known[name] = true + } + link := &linkInfo{symbols: known} + + img := &Image{Symbols: map[string]int{}} + type asmFunc struct { + name string + patches []sbPatch + } + var funcs []asmFunc + for _, d := range f.Decls { + t, ok := d.(*ast.Text) + if !ok { + continue + } + code, patches, labels, err := assemble(t, link) + if err != nil { + return nil, fmt.Errorf("%s: %w", t.Name.Name, err) + } + img.Funcs = append(img.Funcs, FuncLayout{ + Name: t.Name.Name, + Offset: len(img.Code), + Size: len(code), + Labels: labels, + }) + img.Code = append(img.Code, code...) + funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches}) + } + + // Lay out the data section behind the code, each symbol 16-aligned. + dataStart := len(img.Code) + for _, name := range order { + if pos := dataStart + len(img.Data); pos != align16(pos) { + img.Data = append(img.Data, make([]byte, align16(pos)-pos)...) + } + img.Symbols[name] = dataStart + len(img.Data) + img.Data = append(img.Data, syms[name]...) + } + + // Resolve the RIP-relative displacements now that every address is known. + for i, fn := range funcs { + base := img.Funcs[i].Offset + code := img.Code[base : base+img.Funcs[i].Size] + for _, p := range fn.patches { + rel := int64(img.Symbols[p.name]) + p.addend - int64(base+p.after) + if rel < -1<<31 || rel >= 1<<31 { + return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name) + } + copy(code[p.off:p.off+4], le32(rel)) + } + } + return img, nil +} + +// collectData gathers the file's static symbols (GLOBL) and their initial +// contents (DATA) into byte buffers, in declaration order. +func collectData(f *ast.File) (map[string][]byte, []string, error) { + syms := map[string][]byte{} + var order []string + for _, d := range f.Decls { + switch dd := d.(type) { + case *ast.Globl: + if dd.Name == nil || dd.Name.Pseudo != "SB" { + continue + } + name := dd.Name.Name + if _, dup := syms[name]; dup { + return nil, nil, fmt.Errorf("duplicate GLOBL %q", name) + } + size := 0 + if dd.Size != nil && dd.Size.Imm.HasVal { + size = int(dd.Size.Imm.Val) + } + syms[name] = make([]byte, size) + order = append(order, name) + + case *ast.Data: + if dd.Name == nil || dd.Name.Pseudo != "SB" { + continue + } + buf, ok := syms[dd.Name.Name] + if !ok { + return nil, nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name) + } + if dd.Value == nil || !dd.Value.Imm.HasVal { + return nil, nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name) + } + w := dd.Width + switch w { + case 1, 2, 4, 8: + default: + return nil, nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w) + } + off := dd.Name.Offset + if off < 0 || off+int64(w) > int64(len(buf)) { + return nil, nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf)) + } + v := dd.Value.Imm.Val + if dd.Value.Imm.Neg { + v = -v + } + for i := 0; i < w; i++ { + buf[off+int64(i)] = byte(v >> (8 * i)) + } + } + } + return syms, order, nil +} + +// align16 rounds n up to the next multiple of 16. +func align16(n int) int { + return (n + 15) &^ 15 +} diff --git a/asm/link_test.go b/asm/link_test.go new file mode 100644 index 0000000..8d1fed8 --- /dev/null +++ b/asm/link_test.go @@ -0,0 +1,196 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "bytes" + "os" + "strings" + "testing" + + "golang.org/x/arch/x86/x86asm" + + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// TestAssembleFileStaticData checks the whole-image layout — code, padding +// and the data section — and that the RIP-relative displacements of static +// symbol loads resolve to the right bytes. +func TestAssembleFileStaticData(t *testing.T) { + f, errs := parser.Parse("d_amd64.s", ` +#include "textflag.h" +TEXT ·load(SB), NOSPLIT, $0 + VMOVDQU mask<>(SB), X15 + MOVL small<>(SB), AX + RET +GLOBL mask<>(SB), RODATA, $16 +DATA mask<>+0(SB)/4, $0x80020100 +DATA mask<>+4(SB)/4, $0x80050403 +DATA mask<>+8(SB)/4, $0x80080706 +DATA mask<>+12(SB)/4, $0x800B0A09 +GLOBL small<>(SB), RODATA, $4 +DATA small<>+0(SB)/4, $0x1234 +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + + // Code (15 bytes) + 1 pad byte to align the data section to 16: + // VMOVDQU mask<>(SB), X15 c5 7a 6f 3d 08 00 00 00 (disp = 16 − 8) + // MOVL small<>(SB), AX 8b 05 12 00 00 00 (disp = 32 − 14) + // RET c3 + // Data: pad, mask (16 bytes), small (4 bytes). + want := "c57a6f3d080000008b0512000000c300" + + "000102800304058006070880090a0b80" + + "34120000" + if got := strings.ReplaceAll(hexBytes(img.Bytes()), " ", ""); got != want { + t.Errorf("image bytes:\n got %s\n want %s", got, want) + } + if img.Symbols["mask"] != 16 || img.Symbols["small"] != 32 { + t.Errorf("symbol offsets = %v, want mask=16 small=32", img.Symbols) + } + if len(img.Funcs) != 1 || img.Funcs[0].Name != "load" || img.Funcs[0].Size != 15 { + t.Errorf("funcs = %+v", img.Funcs) + } +} + +// TestAssembleFileErrors checks the static-symbol error paths. +func TestAssembleFileErrors(t *testing.T) { + cases := []struct { + name string + src string + want string // substring of the error + }{ + { + "undefined symbol", + ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + VMOVDQU nope<>(SB), X0 + RET +`, + "undefined symbol", + }, + { + "DATA without GLOBL", + ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + RET +DATA orphan<>+0(SB)/4, $1 +`, + "no matching GLOBL", + }, + { + "DATA exceeds size", + ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + RET +GLOBL tiny<>(SB), RODATA, $4 +DATA tiny<>+0(SB)/8, $1 +`, + "exceeds GLOBL size", + }, + { + "DATA bad width", + ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + RET +GLOBL odd<>(SB), RODATA, $4 +DATA odd<>+0(SB)/3, $1 +`, + "invalid width", + }, + } + for _, c := range cases { + f, errs := parser.Parse("e_amd64.s", c.src) + if len(errs) > 0 { + t.Fatalf("%s: parse: %v", c.name, errs) + } + if _, err := AssembleFile(f); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: error %v, want substring %q", c.name, err, c.want) + } + } + + // A static-symbol operand is unresolvable in single-function assembly. + fn := firstText(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 + MOVQ x<>(SB), AX + RET +GLOBL x<>(SB), RODATA, $8 +DATA x<>+0(SB)/4, $1 +`) + if _, _, err := Assemble(fn); err == nil || !strings.Contains(err.Error(), "file-level assembly") { + t.Errorf("single-function SB: error %v, want a file-level-assembly error", err) + } +} + +// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel — +// all functions plus the file-local mask24 constant — and checks that every +// static-symbol load resolves to the right bytes in the image. Skipped when +// the sibling repository is not checked out. +func TestAssembleGoFlacAVX2Kernel(t *testing.T) { + path := "../../go-libraries/go-flac/avx2_amd64.s" + if _, err := os.Stat(path); err != nil { + t.Skip("go-libraries repository not present next to gasm-devkit") + } + src, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + f, errs := parser.Parse(path, string(src)) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + if len(img.Funcs) != 17 { + t.Errorf("functions = %d, want 17", len(img.Funcs)) + } + + // mask24 as the DATA directives define it. + mask := []byte{ + 0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80, + 0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80, + } + image := img.Bytes() + if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) { + t.Errorf("mask24 contents %x, want %x", got, mask) + } + + // Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU + // with a RIP-relative r/m) must land on the mask bytes within the image. + loads := 0 + for _, fn := range img.Funcs { + code := img.Code[fn.Offset : fn.Offset+fn.Size] + for pc := 0; pc < len(code); { + inst, err := x86asm.Decode(code[pc:], 64) + if err != nil { + t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err) + } + // mod=00, rm=101 → RIP-relative. + if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 { + rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24) + target := fn.Offset + pc + 8 + int(rel) + if !bytes.Equal(image[target:target+16], mask) { + t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask) + } + loads++ + } + pc += inst.Len + } + } + if loads != 2 { + t.Errorf("mask loads found = %d, want 2", loads) + } +} diff --git a/asm/operand.go b/asm/operand.go index c591747..743327b 100644 --- a/asm/operand.go +++ b/asm/operand.go @@ -41,3 +41,15 @@ func Idx(base, index Reg, scale int, disp int64, size int) Mem { func Rip(disp int64, size int) Mem { return Mem{Disp: disp, Size: size} } + +// sbMem is a memory operand that references a static (SB) symbol. It encodes +// as a RIP-relative reference with a placeholder displacement; the encoder +// records a patch site so the file-level layout can fill in the true rel32 +// once the symbol's address is known. +type sbMem struct { + size int + name string // static symbol name (the GLOBL identifier) + addend int64 // byte offset within the symbol +} + +func (sbMem) isOperand() {} diff --git a/asm/vex.go b/asm/vex.go index 3934bfa..1e5799d 100644 --- a/asm/vex.go +++ b/asm/vex.go @@ -507,7 +507,8 @@ func vecReg(op Operand) (Reg, bool) { // vecOrMem reports whether op is a vector register or a memory reference. func vecOrMem(op Operand) bool { - if _, ok := op.(Mem); ok { + switch op.(type) { + case Mem, sbMem: return true } r, ok := op.(Reg) @@ -518,7 +519,7 @@ func vecOrMem(op Operand) bool { // acceptable: memory always is, a GPR only for VMOVD/VMOVQ. func validMoveOther(ms vexMoveSpec, op Operand) bool { switch o := op.(type) { - case Mem: + case Mem, sbMem: return true case Reg: return ms.gprOK && !o.isVec() @@ -533,6 +534,7 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope var modrm, sib int var disp []byte var xBit, bBit int + var sb *sbRef switch r := rm.(type) { case Reg: modrm = 0xC0 | regField<<3 | (r.idx & 7) @@ -546,6 +548,12 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope if err != nil { return err } + case sbMem: + // RIP-relative static-symbol reference; disp32 patched at link time. + modrm = regField<<3 | 0x05 + sib = -1 + disp = le32(0) + sb = &sbRef{name: r.name, addend: r.addend} default: return fmt.Errorf("invalid VEX r/m operand") } @@ -561,6 +569,9 @@ func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Ope if sib >= 0 { e.out = append(e.out, byte(sib)) } + if sb != nil { + e.patches = append(e.patches, encPatch{off: len(e.out), name: sb.name, addend: sb.addend}) + } e.out = append(e.out, disp...) return nil } diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 6c4f5fc..8748010 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -26,7 +26,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.3.0" +var version = "0.4.0" func main() { if len(os.Args) < 2 { @@ -238,20 +238,18 @@ func cmdAsm(args []string) int { return 1 } - var all []byte - functions := 0 - for _, d := range f.Decls { - txt, ok := d.(*ast.Text) - if !ok { - continue - } - code, _, err := asm.Assemble(txt) - if err != nil { - fmt.Fprintf(os.Stderr, "%s: %s: %v\n", path, txt.Name.Name, err) - return 1 - } - functions++ - fmt.Printf("%s: %d bytes\n", txt.Name.Name, len(code)) + img, err := asm.AssembleFile(f) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) + return 1 + } + if len(img.Funcs) == 0 { + fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found") + return 1 + } + for _, fn := range img.Funcs { + code := img.Code[fn.Offset : fn.Offset+fn.Size] + fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size) for i := 0; i < len(code); i += 16 { end := i + 16 if end > len(code) { @@ -263,13 +261,34 @@ func cmdAsm(args []string) int { } fmt.Println() } - all = append(all, code...) } - if functions == 0 { - fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found") - return 1 + if len(img.Data) > 0 { + fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code)) + for _, d := range f.Decls { + g, ok := d.(*ast.Globl) + if !ok || g.Name == nil || g.Name.Pseudo != "SB" { + continue + } + size := 0 + if g.Size != nil && g.Size.Imm.HasVal { + size = int(g.Size.Imm.Val) + } + fmt.Printf(" %s: %d bytes at 0x%x\n", g.Name.Name, size, img.Symbols[g.Name.Name]) + } + for i := 0; i < len(img.Data); i += 16 { + end := i + 16 + if end > len(img.Data) { + end = len(img.Data) + } + fmt.Printf(" %04x:", len(img.Code)+i) + for _, b := range img.Data[i:end] { + fmt.Printf(" %02x", b) + } + fmt.Println() + } } if *out != "" { + all := img.Bytes() if err := os.WriteFile(*out, all, 0o644); err != nil { fmt.Fprintln(os.Stderr, "gasm asm:", err) return 1 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cd44212..e419643 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -206,15 +206,21 @@ memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, `VMULPD`, `VXORPD`, `VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`, `VFMADD231PD`) and the no-operand `VZEROUPPER` — together with `VPERMD` and the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves, -`CVTSx2SD`, `IMUL3`), covering every instruction the go-flac AVX2 kernels use -apart from global-symbol loads. Every encoding is validated two ways: by -round-trip decoding through `golang.org/x/arch`, and byte-for-byte against the -machine code the real Go assembler emits — a comparison that now holds for -whole functions: every kernel function that avoids `SB` operands assembles to -exactly the Go toolchain's bytes. This increment covers register / memory / -immediate / FP-frame operands, local-label jumps and these VEX SIMD forms; -EVEX / AVX-512, `SB` (global symbol) operands (relocations) and object-file -emission are the rest of Phase 2. +`CVTSx2SD`, `IMUL3`), covering every instruction the go-flac AVX2 kernels +use. Every encoding is validated two ways: by round-trip decoding through +`golang.org/x/arch`, and byte-for-byte against the machine code the real Go +assembler emits — a comparison that holds for the whole kernel: all 17 +functions of the go-flac AVX2 file assemble to exactly the Go toolchain's +bytes, the lone exception being the displacements of the static-constant +loads, which the Go linker fills at link time. + +File-level assembly (`AssembleFile`) goes beyond single functions: it +materialises the file's static symbols (`GLOBL`/`DATA`) in a data section +behind the code and resolves references to them (`mask<>(SB)`) to +RIP-relative loads whose displacements point inside the resulting image, so +the bytes are self-consistent at any base address. External (non-file-local) +symbols are rejected: they need object-file emission, which — together with +EVEX / AVX-512 and the other architectures — is the rest of Phase 2. ## Extension points diff --git a/justfile b/justfile index 1f1654d..186dd1c 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.3.0" +version := "0.4.0" default: @just --list