From 0920edb092fddbde870658ff3def6ae3a16ae99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Fri, 17 Jul 2026 18:57:04 +0200 Subject: [PATCH] feat(asm): emit GOOBJ objects that link directly with the Go toolchain Assisted-by: Qwen 3.8 Max Preview --- asm/assemble.go | 42 +++- asm/goobj.go | 453 ++++++++++++++++++++++++++++++++++++++++ asm/goobj_test.go | 477 +++++++++++++++++++++++++++++++++++++++++++ asm/link.go | 90 ++++++-- cmd/gasm/main.go | 20 +- docs/ARCHITECTURE.md | 17 +- justfile | 2 +- parser/parser.go | 5 + 8 files changed, 1075 insertions(+), 31 deletions(-) create mode 100644 asm/goobj.go create mode 100644 asm/goobj_test.go diff --git a/asm/assemble.go b/asm/assemble.go index ecab68e..71887ea 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -23,7 +23,7 @@ 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) + code, _, labels, _, err := assemble(t, nil) return code, labels, err } @@ -48,9 +48,19 @@ type sbPatch struct { addend int64 } +// spadjStep is one stack-adjustment boundary within a function: Value is the +// SP delta from the entry state (just below the return address) in effect +// from PC (function-relative) until the next step. The steps feed the +// pcsp table of the object-file emitters. +type spadjStep struct { + pc int + value int +} + // 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) { +// patch sites (for the file-level layout to resolve), the label table and the +// stack-adjustment boundaries. +func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) { fi := computeFrame(t) chain := jumpChain(t) resolve := func(name string) string { @@ -74,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e case *ast.Instr: sz, err := instrSize(s, fi, long[i], link) if err != nil { - return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) } sizes[i] = sz pcs[i] = pos @@ -114,24 +124,42 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e // Pass 2: emit. out := append([]byte(nil), fi.prologue...) var patches []sbPatch + var steps []spadjStep + if fi.useFP { + // PUSHQ BP saves the return-address-relative base (+8); the MOVQ + // changes nothing; SUBQ $size, SP completes the frame. + steps = append(steps, + spadjStep{1, 8}, + spadjStep{len(fi.prologue), 8 + fi.size}, + ) + } pos := len(fi.prologue) for i, stmt := range t.Body { s, ok := stmt.(*ast.Instr) if !ok { continue } + if strings.ToUpper(s.Mnemonic.Text) == "RET" && fi.useFP { + // The RET's epilogue prefix unwinds: ADDQ $size, SP restores + // the saved-BP-only stack, POPQ BP the entry state. + epi := len(fi.epilogue) + steps = append(steps, + spadjStep{pos + epi - 1, 8}, + spadjStep{pos + epi, 0}, + ) + } code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link) if err != nil { - return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) + return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) } if len(code) != sizes[i] { - return nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i]) + return nil, 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, patches, offsets, nil + return out, patches, offsets, steps, nil } // jumpChain precomputes jump-to-jump folding: a label whose first instruction diff --git a/asm/goobj.go b/asm/goobj.go new file mode 100644 index 0000000..986bb17 --- /dev/null +++ b/asm/goobj.go @@ -0,0 +1,453 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "os/exec" + "path/filepath" + "sync" +) + +// This file emits GOOBJ — the Go toolchain's object format, which cmd/link +// consumes directly — so gasm-assembled functions drop into a go build +// without the Go assembler. The layout follows cmd/internal/goobj: a +// toolchain preamble ("go object ...\n!\n"), the go120ld header with its +// block offsets, a string table, symbol definitions, the relocation / +// aux / data index arrays, and the three blocks themselves. +// +// The object carries what the linker requires of an assembly object: the +// functions (non-package symbols, as cmd/asm emits them), the GLOBL data, +// one FuncInfo per function, and the pc-value tables (pcsp, pcfile, +// pcline, pcinline). DWARF and the implicit funcdata symbols are omitted; +// the linker fills their defaults. + +// GOOBJ block indices (cmd/internal/goobj). +const ( + blkAutolib = iota + blkPkgIdx + blkFile + blkSymdef + blkHashed64def + blkHasheddef + blkNonpkgdef + blkNonpkgref + blkRefFlags + blkHash64 + blkHash + blkRelocIdx + blkAuxIdx + blkDataIdx + blkReloc + blkAux + blkData + blkRefName + blkEnd +) + +// Symbol kinds used by assembly objects (cmd/internal/objabi). +const ( + kindSTEXT = 1 + kindSRODATA = 3 + kindSDATA = 7 +) + +// Symbol flags (cmd/internal/goobj). +const ( + symFlagDupok = 0x01 + symFlagNoSplit = 0x10 + symFlag2Link = 0x10 // asm objects flag every named symbol as linkname + symABIStatic = 0xffff +) + +// Aux entry types (cmd/internal/goobj). +const ( + auxFuncInfo = 1 + auxPcsp = 7 + auxPcfile = 8 + auxPcline = 9 + auxPcinline = 10 +) + +// FuncInfo flags (internal/abi). +const ( + funcFlagSPWrite = 2 + funcFlagAsm = 4 +) + +// Relocation types (cmd/internal/objabi). +const relocPCRel = 14 + +// Special package indices for symbol references. +const ( + pkgIdxNone = 0x7fffffff + pkgIdxSelf = 0x7ffffffb +) + +const goobjMagic = "\x00go120ld" + +// goSym is one symbol definition under construction. +type goSym struct { + name string + abi uint16 + typ uint8 + flag uint8 + flag2 uint8 + size uint32 + align uint32 +} + +func (s goSym) append(b []byte, strOff map[string]uint32) []byte { + b = binary.LittleEndian.AppendUint32(b, uint32(len(s.name))) + b = binary.LittleEndian.AppendUint32(b, strOff[s.name]) + b = binary.LittleEndian.AppendUint16(b, s.abi) + b = append(b, s.typ, s.flag, s.flag2) + b = binary.LittleEndian.AppendUint32(b, s.size) + return binary.LittleEndian.AppendUint32(b, s.align) +} + +// GOObject returns the image as a GOOBJ object file for the given package +// path (the linker qualifies the exported symbols with it, the way cmd/asm +// does with its -p flag). srcPath names the source file recorded in the +// object's file table and line tables. The toolchain's object preamble is +// captured from the installed go tool asm, so the output links with the +// toolchain it was produced on — exactly like a real assembly object. +func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) { + if pkgPath == "" { + return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)") + } + pre, err := toolchainObjectPreamble() + if err != nil { + return nil, err + } + + // The symbol tables. Package definitions: the GLOBL symbols, then one + // anonymous FuncInfo symbol per function. Non-package definitions: the + // pc-value tables and the functions themselves, as cmd/asm lays them + // out. defIdx maps a GLOBL's bare name to its definition index for the + // relocations; fnNpIdx maps a function to its non-package index. + var defs []goSym + var defData [][]byte + defIdx := map[string]int{} + for _, d := range img.DataSyms { + name := d.Name + if !d.Static { + name = pkgPath + "." + name + } + typ := uint8(kindSDATA) + if d.Rodata { + typ = kindSRODATA + } + flag := uint8(0) + if d.Dupok { + flag = symFlagDupok + } + abi := uint16(0) + if d.Static { + abi = symABIStatic + } + defIdx[d.Name] = len(defs) + defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)}) + defData = append(defData, img.Data[d.Offset:d.Offset+d.Size]) + } + fnFiIdx := make([]int, len(img.Funcs)) + for i := range img.Funcs { + data := marshalFuncInfo(img.Funcs[i]) + fnFiIdx[i] = len(defs) + defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))}) + defData = append(defData, data) + } + + type npSym struct { + sym goSym + data []byte + } + var nps []npSym + type pcRefs struct{ sp, file, line, inl int } + pcIdx := make([]pcRefs, len(img.Funcs)) + fnNpIdx := make([]int, len(img.Funcs)) + for i, fn := range img.Funcs { + tables := []struct { + data []byte + dst *int + }{ + {pcspTable(fn), &pcIdx[i].sp}, + {pcValueFlat(0, fn.Size), &pcIdx[i].file}, + {pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line}, + {pcValueFlat(-1, fn.Size), &pcIdx[i].inl}, + } + for _, t := range tables { + *t.dst = len(nps) + nps = append(nps, npSym{ + sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1}, + data: t.data, + }) + } + name := fn.Name + abi := uint16(0) + if fn.Static { + abi = symABIStatic + } else { + name = pkgPath + "." + name + } + flag := uint8(0) + if fn.NoSplit { + flag |= symFlagNoSplit + } + fnNpIdx[i] = len(nps) + code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...) + for _, r := range fn.Relocs { + // The linker writes the resolved displacement into the field; + // leave it zero, as cmd/asm's object does. + if r.Off >= 0 && r.Off+4 <= len(code) { + code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0 + } + } + nps = append(nps, npSym{ + sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)}, + data: code, + }) + } + + // Relocations, per defined symbol in definition order (package defs, + // then non-package defs). Only file-local GLOBL references resolve; + // external symbols need the import machinery of a later increment. + nsyms := len(defs) + len(nps) + symRelocs := make([][]byte, nsyms) // flat 23-byte records + for i, fn := range img.Funcs { + si := len(defs) + fnNpIdx[i] + for _, r := range fn.Relocs { + if r.External { + return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name) + } + di, ok := defIdx[r.Name] + if !ok { + return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name) + } + var rec [23]byte + binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) + rec[4] = 4 // field width + binary.LittleEndian.PutUint16(rec[5:], relocPCRel) + binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) + binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf) + binary.LittleEndian.PutUint32(rec[19:], uint32(di)) + symRelocs[si] = append(symRelocs[si], rec[:]...) + } + } + + // Aux entries per function: FuncInfo, then the four pc tables. + // References into the non-package table use pkgIdxNone. + symAux := make([][]byte, nsyms) + for i := range img.Funcs { + si := len(defs) + fnNpIdx[i] + aux := func(typ uint8, pkg, idx uint32) { + var rec [9]byte + rec[0] = typ + binary.LittleEndian.PutUint32(rec[1:], pkg) + binary.LittleEndian.PutUint32(rec[5:], idx) + symAux[si] = append(symAux[si], rec[:]...) + } + aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i])) + aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp)) + aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file)) + aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line)) + aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl)) + } + + // The string table. Absolute offsets: it starts right after the + // 96-byte header (magic, fingerprint, flags, the 19 block offsets). + const headerSize = 8 + 8 + 4 + 4*(blkEnd+1) + strTab := []byte{} + strOff := map[string]uint32{} + addStr := func(s string) { + if _, ok := strOff[s]; ok { + return + } + strOff[s] = uint32(headerSize + len(strTab)) + strTab = append(strTab, s...) + } + addStr("") + addStr(srcPath) + for _, s := range defs { + addStr(s.name) + } + for _, s := range nps { + addStr(s.sym.name) + } + stringRef := func(b []byte, s string) []byte { + b = binary.LittleEndian.AppendUint32(b, uint32(len(s))) + return binary.LittleEndian.AppendUint32(b, strOff[s]) + } + + // Serialise the block bodies. + var symdefBlk, npdefBlk []byte + for _, s := range defs { + symdefBlk = s.append(symdefBlk, strOff) + } + for _, s := range nps { + npdefBlk = s.sym.append(npdefBlk, strOff) + } + pkgIdxBlk := stringRef(nil, "") // index 0: the dummy invalid package + fileBlk := stringRef(nil, srcPath) + + var relocBlk, auxBlk, dataBlk []byte + relocIdxBlk := make([]byte, 0, 4*(nsyms+1)) + auxIdxBlk := make([]byte, 0, 4*(nsyms+1)) + dataIdxBlk := make([]byte, 0, 4*(nsyms+1)) + var nr, na, nd uint32 + for si := 0; si < nsyms; si++ { + relocIdxBlk = binary.LittleEndian.AppendUint32(relocIdxBlk, nr) + auxIdxBlk = binary.LittleEndian.AppendUint32(auxIdxBlk, na) + dataIdxBlk = binary.LittleEndian.AppendUint32(dataIdxBlk, nd) + relocBlk = append(relocBlk, symRelocs[si]...) + auxBlk = append(auxBlk, symAux[si]...) + var d []byte + if si < len(defData) { + d = defData[si] + } else { + d = nps[si-len(defData)].data + } + dataBlk = append(dataBlk, d...) + nr += uint32(len(symRelocs[si])) / 23 + na += uint32(len(symAux[si])) / 9 + nd += uint32(len(d)) + } + relocIdxBlk = binary.LittleEndian.AppendUint32(relocIdxBlk, nr) + auxIdxBlk = binary.LittleEndian.AppendUint32(auxIdxBlk, na) + dataIdxBlk = binary.LittleEndian.AppendUint32(dataIdxBlk, nd) + + blocks := [blkEnd][]byte{ + blkPkgIdx: pkgIdxBlk, + blkFile: fileBlk, + blkSymdef: symdefBlk, + blkNonpkgdef: npdefBlk, + blkRelocIdx: relocIdxBlk, + blkAuxIdx: auxIdxBlk, + blkDataIdx: dataIdxBlk, + blkReloc: relocBlk, + blkAux: auxBlk, + blkData: dataBlk, + } + + // Assemble the payload: header (offsets filled once known), string + // table, blocks in order. + payload := make([]byte, headerSize) + copy(payload, goobjMagic) + // The fingerprint stays zero, as cmd/asm leaves it. + binary.LittleEndian.PutUint32(payload[16:], 4) // ObjFlagFromAssembly + off := uint32(headerSize + len(strTab)) + for i := 0; i < blkEnd; i++ { + binary.LittleEndian.PutUint32(payload[20+4*i:], off) + off += uint32(len(blocks[i])) + } + binary.LittleEndian.PutUint32(payload[20+4*blkEnd:], off) + payload = append(payload, strTab...) + for _, blk := range blocks { + payload = append(payload, blk...) + } + + out := make([]byte, 0, len(pre)+len(payload)) + out = append(out, pre...) + return append(out, payload...), nil +} + +// marshalFuncInfo serialises a function's goobj.FuncInfo: sizes, flags, +// start line, the one-element file table and an empty inline tree. +func marshalFuncInfo(fn FuncLayout) []byte { + flag := uint8(funcFlagAsm) + if fn.SPWrite { + flag |= funcFlagSPWrite + } + b := make([]byte, 0, 28) + b = binary.LittleEndian.AppendUint32(b, uint32(fn.Args)) + b = binary.LittleEndian.AppendUint32(b, uint32(fn.Frame)) + b = append(b, 0, flag, 0, 0) // FuncID normal, flags, padding + b = binary.LittleEndian.AppendUint32(b, uint32(int32(fn.Line))) + b = binary.LittleEndian.AppendUint32(b, 1) // one file + b = binary.LittleEndian.AppendUint32(b, 0) // file index 0 + b = binary.LittleEndian.AppendUint32(b, 0) // no inline tree + return b +} + +// pcValueFlat encodes a pc-value table holding v over the whole function. +func pcValueFlat(v int32, size int) []byte { + // The table is delta-encoded from an implicit value of -1: a varint + // value delta, an unsigned pc delta to the end, and a zero terminator. + out := binary.AppendVarint(nil, int64(v)+1) + out = binary.AppendUvarint(out, uint64(size)) + return append(out, 0) +} + +// pcspTable encodes the stack-adjustment table: the SP delta in effect at +// every pc, from the function's prologue and epilogue boundaries. +func pcspTable(fn FuncLayout) []byte { + if len(fn.Spadj) == 0 { + return pcValueFlat(0, fn.Size) + } + pts := make([]SpadjStep, 0, len(fn.Spadj)+1) + pts = append(pts, SpadjStep{PC: 0, Value: 0}) + pts = append(pts, fn.Spadj...) + out := binary.AppendVarint(nil, int64(pts[0].Value)+1) + cur, old := pts[0].PC, pts[0].Value + for _, p := range pts[1:] { + out = binary.AppendUvarint(out, uint64(p.PC-cur)) + out = binary.AppendVarint(out, int64(p.Value-old)) + cur, old = p.PC, p.Value + } + out = binary.AppendUvarint(out, uint64(fn.Size-cur)) + return append(out, 0) +} + +// toolchainObjectPreamble returns the "go object ...\n!\n" header the +// installed go tool asm writes, captured by assembling a one-instruction +// probe. The linker compares this string verbatim against its own, so it +// must come from the toolchain itself, not be reconstructed. +var ( + preambleOnce sync.Once + preamble []byte + preambleErr error +) + +func toolchainObjectPreamble() ([]byte, error) { + preambleOnce.Do(func() { + goBin, err := exec.LookPath("go") + if err != nil { + preambleErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err) + return + } + dir, err := os.MkdirTemp("", "gasm-preamble") + if err != nil { + preambleErr = err + return + } + defer os.RemoveAll(dir) + src := filepath.Join(dir, "probe_amd64.s") + if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil { + preambleErr = err + return + } + obj := filepath.Join(dir, "probe.o") + cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src) + cmd.Env = append(os.Environ(), "GOARCH=amd64") + if out, err := cmd.CombinedOutput(); err != nil { + preambleErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out) + return + } + data, err := os.ReadFile(obj) + if err != nil { + preambleErr = err + return + } + i := bytes.Index(data, []byte("\n!\n")) + if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) { + preambleErr = fmt.Errorf("unrecognised assembler object layout") + return + } + preamble = data[:i+3] + }) + return preamble, preambleErr +} diff --git a/asm/goobj_test.go b/asm/goobj_test.go new file mode 100644 index 0000000..b895aa7 --- /dev/null +++ b/asm/goobj_test.go @@ -0,0 +1,477 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "bytes" + "encoding/binary" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// goobjView is a minimal parsed view of a GOOBJ payload, enough to check +// the emitter's output block by block. +type goobjView struct { + t *testing.T + b []byte + offs [blkEnd + 1]uint32 + strOff uint32 +} + +func openGoobj(t *testing.T, data []byte) *goobjView { + t.Helper() + i := bytes.Index(data, []byte(goobjMagic)) + if i < 0 { + t.Fatal("no GOOBJ magic in output") + } + v := &goobjView{t: t, b: data[i:], strOff: uint32(i + 96)} + for j := 0; j <= blkEnd; j++ { + v.offs[j] = binary.LittleEndian.Uint32(v.b[20+4*j:]) + } + return v +} + +func (v *goobjView) blk(i int) []byte { return v.b[v.offs[i]:v.offs[i+1]] } + +func (v *goobjView) str(off, ln uint32) string { + return string(v.b[off : off+ln]) +} + +type goobjSymView struct { + name string + abi uint16 + typ uint8 + flag uint8 + flag2 uint8 + size uint32 + align uint32 +} + +func (v *goobjView) syms(i int) []goobjSymView { + var out []goobjSymView + for x := v.blk(i); len(x) >= 21; x = x[21:] { + le := binary.LittleEndian + out = append(out, goobjSymView{ + name: v.str(le.Uint32(x[4:]), le.Uint32(x[0:])), + abi: le.Uint16(x[8:]), + typ: x[10], + flag: x[11], + flag2: x[12], + size: le.Uint32(x[13:]), + align: le.Uint32(x[17:]), + }) + } + return out +} + +// TestGOObjectStructure checks the emitted object's blocks against the +// ground truth captured from go tool asm: the symbol tables, the FuncInfo +// contents, the pc-value tables, the relocation and the aux wiring. +func TestGOObjectStructure(t *testing.T) { + f, errs := parser.Parse("t_amd64.s", ` +#include "textflag.h" + +TEXT ·addq(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX + MOVQ b+8(FP), CX + ADDQ CX, AX + MOVQ AX, ret+16(FP) + RET + +TEXT ·loadmask(SB), NOSPLIT, $0-8 + VMOVDQU mask<>(SB), X0 + VPMOVMSKB X0, AX + MOVQ AX, ret+0(FP) + RET + +GLOBL mask<>(SB), RODATA, $16 +DATA mask<>+0(SB)/8, $0x0807060504030201 +DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09 +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + obj, err := img.GOObject("testpkg", "t_amd64.s") + if err != nil { + t.Fatalf("GOObject: %v", err) + } + v := openGoobj(t, obj) + + if flags := binary.LittleEndian.Uint32(v.b[16:]); flags != 4 { + t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags) + } + + // Package defs: the static GLOBL, then one anonymous FuncInfo per + // function. + defs := v.syms(blkSymdef) + if len(defs) != 3 { + t.Fatalf("symdefs = %d, want 3", len(defs)) + } + if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link { + t.Errorf("mask symbol = %+v", defs[0]) + } + if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 { + t.Errorf("funcinfo symbol = %+v", defs[1]) + } + + // Non-package defs: four pc tables and the function, per function. + nps := v.syms(blkNonpkgdef) + if len(nps) != 10 { + t.Fatalf("nonpkgdefs = %d, want 10", len(nps)) + } + fn := nps[4] + if fn.name != "testpkg.addq" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 19 { + t.Errorf("addq symbol = %+v", fn) + } + for i, s := range []int{0, 1, 2, 3, 5, 6, 7, 8} { + if nps[s].typ != kindSRODATA || nps[s].align != 1 || nps[s].name != "" { + t.Errorf("pc table %d = %+v", i, nps[s]) + } + } + + // FuncInfo: args 24, FuncFlag Asm, one file, no inline tree. + le := binary.LittleEndian + data := v.blk(blkData) + fi := data[16:44] + if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm || + le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 { + t.Errorf("funcinfo bytes %x", fi) + } + + // pcsp: a flat zero over the whole function (zero-frame NOSPLIT). + if got := data[72:75]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) { + t.Errorf("pcsp = %x, want 021300", got) + } + // pcinline: a flat -1. + if got := data[81:84]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) { + t.Errorf("pcinline = %x, want 001300", got) + } + + // The one relocation: R_PCREL, four bytes wide, against the GLOBL, + // with the field in the function code left zero. The loadmask code's + // offset comes from the data index (symbol 3 defs + 9 non-package). + relocs := v.blk(blkReloc) + if len(relocs) != 23 { + t.Fatalf("relocs = %d bytes, want one 23-byte entry", len(relocs)) + } + off := int32(le.Uint32(relocs[0:])) + if off != 4 || relocs[4] != 4 || le.Uint16(relocs[5:]) != relocPCRel || + le.Uint64(relocs[7:]) != 0 || le.Uint32(relocs[15:]) != pkgIdxSelf || le.Uint32(relocs[19:]) != 0 { + t.Errorf("reloc = %x", relocs) + } + didx := v.blk(blkDataIdx) + lm := le.Uint32(didx[4*(3+9):]) + code := data[lm : lm+18] + if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) { + t.Errorf("relocated field = %x, want zeroed", code[4:8]) + } + + // Aux wiring: FuncInfo (package symbol), then the four pc tables + // (non-package symbols). + auxs := v.blk(blkAux) + if len(auxs) != 2*5*9 { + t.Fatalf("aux = %d bytes, want 10 entries", len(auxs)) + } + wantAux := []struct { + typ uint8 + pkg uint32 + idx uint32 + }{ + {auxFuncInfo, pkgIdxSelf, 1}, + {auxPcsp, pkgIdxNone, uint32(len(defs) + 0)}, + {auxPcfile, pkgIdxNone, uint32(len(defs) + 1)}, + {auxPcline, pkgIdxNone, uint32(len(defs) + 2)}, + {auxPcinline, pkgIdxNone, uint32(len(defs) + 3)}, + {auxFuncInfo, pkgIdxSelf, 2}, + {auxPcsp, pkgIdxNone, uint32(len(defs) + 5)}, + {auxPcfile, pkgIdxNone, uint32(len(defs) + 6)}, + {auxPcline, pkgIdxNone, uint32(len(defs) + 7)}, + {auxPcinline, pkgIdxNone, uint32(len(defs) + 8)}, + } + for i, w := range wantAux { + e := auxs[i*9:] + if e[0] != w.typ || le.Uint32(e[1:]) != w.pkg || le.Uint32(e[5:]) != w.idx { + t.Errorf("aux[%d] = {%d,%d,%d}, want {%d,%d,%d}", i, e[0], le.Uint32(e[1:]), le.Uint32(e[5:]), w.typ, w.pkg, w.idx) + } + } +} + +// decodePCValues decodes a pc-value table into (pc, value) steps. The +// table ends with a final unsigned pc delta covering the rest of the +// function, followed by a zero byte that carries no value delta. +func decodePCValues(b []byte) (pcs, vals []int64) { + val, n := binary.Varint(b) + b = b[n:] + val-- // the first delta is against the implicit -1 + var pc int64 + pcs = append(pcs, pc) + vals = append(vals, val) + for { + pcd, n := binary.Uvarint(b) + b = b[n:] + if pcd == 0 { // zero pc delta terminates the table + break + } + pc += int64(pcd) + if len(b) == 1 && b[0] == 0 { // final coverage, no value change + break + } + vd, n := binary.Varint(b) + b = b[n:] + val += vd + pcs = append(pcs, pc) + vals = append(vals, val) + } + return pcs, vals +} + +// TestGOObjectPcspFrame checks the pcsp table of a frame-pointer function: +// the prologue raises the stack delta to 8+frame, the RET's epilogue +// restores it to zero. +func TestGOObjectPcspFrame(t *testing.T) { + f, errs := parser.Parse("frame_amd64.s", ` +#include "textflag.h" +TEXT ·framed(SB), NOSPLIT, $8-0 + MOVQ BP, AX + RET +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + fn := img.Funcs[0] + pcs, vals := decodePCValues(pcspTable(fn)) + // Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change), + // SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds + // ADDQ $8, SP (+8) then POPQ BP (0). + wantPCs := []int64{0, 1, 8} + wantVals := []int64{0, 8, 16} + if len(pcs) < len(wantPCs) { + t.Fatalf("pcsp pcs = %v vals = %v", pcs, vals) + } + for i := range wantPCs { + if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] { + t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d) — all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals) + } + } + // The last two steps unwind the epilogue to zero. + n := len(pcs) + if vals[n-1] != 0 || vals[n-2] != 8 { + t.Errorf("epilogue steps = %v %v, want …8, 0", pcs, vals) + } + // The table covers the whole function. + if last := pcs[n-1]; last >= int64(fn.Size) { + t.Errorf("last pc %d beyond function size %d", last, fn.Size) + } +} + +// TestGOObjectExternalRejected checks that a reference to a symbol no GLOBL +// defines is reported: GOOBJ emission resolves only file-local symbols so +// far. +func TestGOObjectExternalRejected(t *testing.T) { + f, errs := parser.Parse("ext_amd64.s", ` +#include "textflag.h" +TEXT ·useext(SB), NOSPLIT, $0-8 + MOVQ elsewhere(SB), AX + MOVQ AX, ret+0(FP) + RET +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + if _, err := img.GOObject("p", "ext_amd64.s"); err == nil || !strings.Contains(err.Error(), "external") { + t.Errorf("error = %v, want an external-symbol error", err) + } +} + +// TestGOObjectLinkAndRun is the end-to-end check: assemble the test +// functions to a GOOBJ, swap it into a go build in place of the toolchain's +// assembly object, link, and run — the output must match the baseline +// binary the Go assembler produced. Skipped when no Go toolchain is +// available. +func TestGOObjectLinkAndRun(t *testing.T) { + goBin, err := exec.LookPath("go") + if err != nil { + t.Skip("no Go toolchain available") + } + dir := t.TempDir() + + const asmSrc = ` +#include "textflag.h" + +TEXT ·addq(SB), NOSPLIT, $0-24 + MOVQ a+0(FP), AX + MOVQ b+8(FP), CX + ADDQ CX, AX + MOVQ AX, ret+16(FP) + RET + +TEXT ·loadmask(SB), NOSPLIT, $0-8 + VMOVDQU mask<>(SB), X0 + VPMOVMSKB X0, AX + MOVQ AX, ret+0(FP) + RET + +GLOBL mask<>(SB), RODATA, $16 +DATA mask<>+0(SB)/8, $0x0807060504030201 +DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09 +` + const mainSrc = `package main + +func addq(a, b int64) int64 +func loadmask() int64 + +func main() { + println(addq(41, 1)) + println(loadmask()) +} +` + if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.26\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Baseline build with the toolchain's assembler; keep the work + // directory and the commands the build used. + cmd := exec.Command(goBin, "build", "-x", "-work", "-o", "app", ".") + cmd.Dir = dir + buildLog, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("baseline build: %v\n%s", err, buildLog) + } + var work string + var asmObj, pkgArch, linkLine string + for _, line := range strings.Split(string(buildLog), "\n") { + switch { + case strings.HasPrefix(line, "WORK="): + work = strings.TrimPrefix(line, "WORK=") + case strings.Contains(line, "/asm ") && strings.Contains(line, "-o ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"): + asmObj = fieldAfter(line, "-o") + case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"): + pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1]) + pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0] + case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): + linkLine = line + } + } + if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" { + t.Fatalf("could not locate the build steps:\n%s", buildLog) + } + asmObj = strings.ReplaceAll(asmObj, "$WORK", work) + pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work) + + // The baseline's answer. + baseOut, err := exec.Command(filepath.Join(dir, "app")).CombinedOutput() + if err != nil { + t.Fatalf("run baseline: %v\n%s", err, baseOut) + } + + // Assemble the same source with gasm and swap the object in. + pf, perrs := parser.Parse(filepath.Join(dir, "main_amd64.s"), asmSrc) + if len(perrs) > 0 { + t.Fatalf("parse: %v", perrs) + } + img, err := AssembleFile(pf) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + obj, err := img.GOObject("main", filepath.Join(dir, "main_amd64.s")) + if err != nil { + t.Fatalf("GOObject: %v", err) + } + if err := os.WriteFile(asmObj, obj, 0o644); err != nil { + t.Fatal(err) + } + + // Rebuild the package archive with our object in place of the + // toolchain's (go tool pack has no replace-in-place that dedupes, so + // extract, substitute and repack). + extract := exec.Command(goBin, "tool", "pack", "x", pkgArch) + membersDir := filepath.Join(dir, "members") + if err := os.MkdirAll(membersDir, 0o755); err != nil { + t.Fatal(err) + } + extract.Dir = membersDir + if out, err := extract.CombinedOutput(); err != nil { + t.Fatalf("pack x: %v\n%s", err, out) + } + listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch) + listOut, err := listCmd.CombinedOutput() + if err != nil { + t.Fatalf("pack t: %v\n%s", err, listOut) + } + newArch := filepath.Join(dir, "pkg.a") + args := []string{"tool", "pack", "c", newArch} + seen := map[string]bool{} + for _, m := range strings.Fields(string(listOut)) { + if seen[m] { + continue + } + seen[m] = true + if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil { + t.Fatal(err) + } + args = append(args, filepath.Join(membersDir, m)) + } + pack := exec.Command(goBin, args...) + pack.Dir = membersDir + if out, err := pack.CombinedOutput(); err != nil { + t.Fatalf("pack c: %v\n%s", err, out) + } + + // Link with our archive. The link line carries a GOROOT assignment + // and $WORK placeholders; run it through the shell with the + // GOEXPERIMENT the toolchain expects (the linker compares the object + // header against its own, experiments included). + goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output() + linkLine = strings.ReplaceAll(linkLine, "$WORK", work) + linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch) + linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2")) + link := exec.Command("sh", "-c", linkLine) + link.Dir = dir + link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp))) + if out, err := link.CombinedOutput(); err != nil { + t.Fatalf("link with gasm object: %v\n%s", err, out) + } + got, err := exec.Command(filepath.Join(dir, "app2")).CombinedOutput() + if err != nil { + t.Fatalf("run gasm-linked binary: %v\n%s", err, got) + } + if !bytes.Equal(got, baseOut) { + t.Errorf("gasm-linked output %q, want baseline %q", got, baseOut) + } +} + +// fieldAfter returns the whitespace-delimited field following the first +// occurrence of flag in line. +func fieldAfter(line, flag string) string { + fields := strings.Fields(line) + for i, f := range fields { + if f == flag && i+1 < len(fields) { + return fields[i+1] + } + } + return "" +} diff --git a/asm/link.go b/asm/link.go index 676f32a..96b4aeb 100644 --- a/asm/link.go +++ b/asm/link.go @@ -28,13 +28,26 @@ type Image struct { // FuncLayout describes one assembled function within an Image. type FuncLayout struct { - Name string - Pkg string // explicit package prefix ("" = the current package) - Static bool // the <> marker: file-local, not exported - Offset int // start offset within the image (== offset within Code) - Size int - Labels map[string]int // local labels, function-relative - Relocs []Reloc // static-symbol references, in emission order + Name string + Pkg string // explicit package prefix ("" = the current package) + Static bool // the <> marker: file-local, not exported + Offset int // start offset within the image (== offset within Code) + Size int + Args int // declared argument/result area (the TEXT size suffix) + Frame int // local frame size (the TEXT $framesize) + NoSplit bool // the NOSPLIT flag + SPWrite bool // the SPWRITE flag: writes an arbitrary value to SP + Line int // source line of the TEXT directive + Labels map[string]int // local labels, function-relative + Relocs []Reloc // static-symbol references, in emission order + Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC +} + +// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the +// entry state in effect from PC (function-relative) until the next step. +type SpadjStep struct { + PC int + Value int } // Reloc is one static-symbol reference within a function body: the disp32 @@ -57,6 +70,8 @@ type DataSymbol struct { Offset int // byte offset within Data Size int Static bool // the <> marker: file-local, not exported + Rodata bool // the RODATA flag: read-only data + Dupok bool // the DUPOK flag: duplicate-OK } // Bytes returns the whole image: code, then data. @@ -95,18 +110,33 @@ func AssembleFile(f *ast.File) (*Image, error) { if !ok { continue } - code, patches, labels, err := assemble(t, link) + code, patches, labels, steps, err := assemble(t, link) if err != nil { return nil, fmt.Errorf("%s: %w", t.Name.Name, err) } - img.Funcs = append(img.Funcs, FuncLayout{ + fl := FuncLayout{ Name: t.Name.Name, Pkg: t.Name.Pkg, Static: t.Name.Static, Offset: len(img.Code), Size: len(code), + Frame: frameSize(t), + Args: argsSize(t), + Line: t.Pos().Line, Labels: labels, - }) + } + for _, f := range t.Flags { + switch f { + case "NOSPLIT": + fl.NoSplit = true + case "SPWRITE": + fl.SPWrite = true + } + } + for _, s := range steps { + fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value}) + } + img.Funcs = append(img.Funcs, fl) img.Code = append(img.Code, code...) funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches}) } @@ -124,6 +154,8 @@ func AssembleFile(f *ast.File) (*Image, error) { Offset: len(img.Data), Size: len(d.buf), Static: d.static, + Rodata: d.rodata, + Dupok: d.dupok, }) img.Data = append(img.Data, d.buf...) } @@ -163,6 +195,8 @@ type dataSym struct { pkg string buf []byte static bool + rodata bool + dupok bool } // collectData gathers the file's static symbols (GLOBL) and their initial @@ -185,12 +219,28 @@ func collectData(f *ast.File) ([]dataSym, error) { size = int(dd.Size.Imm.Val) } index[name] = len(syms) - syms = append(syms, dataSym{ + ds := dataSym{ name: name, pkg: dd.Name.Pkg, buf: make([]byte, size), static: dd.Name.Static, - }) + } + for _, f := range dd.Flags { + switch f { + case "RODATA": + ds.rodata = true + case "DUPOK": + ds.dupok = true + case "1": + ds.dupok = true + case "8": + ds.rodata = true + case "9": + ds.dupok = true + ds.rodata = true + } + } + syms = append(syms, ds) case *ast.Data: if dd.Name == nil || dd.Name.Pseudo != "SB" { @@ -230,3 +280,19 @@ func collectData(f *ast.File) ([]dataSym, error) { func align16(n int) int { return (n + 15) &^ 15 } + +// frameSize returns the local frame size declared on the TEXT directive. +func frameSize(t *ast.Text) int { + if t.Frame != nil && t.Frame.Imm.HasVal { + return int(t.Frame.Imm.Val) + } + return 0 +} + +// argsSize returns the argument/result area declared on the TEXT directive. +func argsSize(t *ast.Text) int { + if t.Args != nil && t.Args.Imm.HasVal { + return int(t.Args.Imm.Val) + } + return 0 +} diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 5b6034b..c6b5904 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -28,7 +28,7 @@ import ( // version is the release version, stamped at build time via // -ldflags "-X main.version=…" (defaulting to the current release). -var version = "0.11.0" +var version = "0.12.0" func main() { if len(os.Args) < 2 { @@ -94,6 +94,7 @@ Examples: gasm lint go-flac/*.s run static checks over the kernels gasm asm -o k.bin kern_amd64.s gasm asm --format elf -o k.o kern_amd64.s + gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s `, version) } @@ -340,7 +341,7 @@ hover, document symbols, diagnostics and semantic-token highlighting. } func cmdAsm(args []string) int { - fs := newCommand("asm", "gasm asm [--format raw|elf|macho] [-o out] ", ` + fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] ", ` Assemble FILE (amd64) without the Go toolchain: every TEXT function is encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions, FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA) @@ -350,14 +351,16 @@ With -o the output is written to a file instead. The --format flag selects what is written: raw (the default) concatenates the functions and the data section into one self-consistent image; elf and macho emit a relocatable object (.text/.data sections, a symbol table and one PC32 relocation per -static-symbol reference) that links with the system toolchain — references -to symbols no GLOBL in the file defines become undefined external symbols. +static-symbol reference) that links with the system toolchain; goobj emits +the Go toolchain's own object format, which cmd/link consumes directly (it +requires -p, the package path, and the installed Go toolchain). `) out := fs.String("o", "", "write the output to this file") - format := fs.String("format", "raw", "output format: raw (concatenated image), elf or macho (relocatable object)") + format := fs.String("format", "raw", "output format: raw (concatenated image), elf, macho or goobj (Go object)") + pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)") fs.Parse(args) if fs.NArg() != 1 { - fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho] [-o out] ") + fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] ") return 2 } path := fs.Arg(0) @@ -444,8 +447,11 @@ to symbols no GLOBL in the file defines become undefined external symbols. case "macho": obj, err = img.MachOObject() kind = "Mach-O object" + case "goobj": + obj, err = img.GOObject(*pkg, path) + kind = "Go object" default: - fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or macho)\n", *format) + fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format) return 2 } if err != nil { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7c490d1..cb41b45 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -250,10 +250,19 @@ and Mach-O writers (`gasm asm --format elf|macho`) lay the code and data out as `.text`/`.data` (or `__text`/`__data`) sections, export a symbol per `TEXT` and `GLOBL` (the `<>` ones local, the rest global) and emit one PC-relative relocation per static-symbol reference — undefined external -symbols included, so the output links with the system toolchain. GOOBJ -emission, the format the Go linker consumes directly, is the remaining -piece of Phase 2 (together with the rest of the EVEX set and the other -architectures). +symbols included, so the output links with the system toolchain. The GOOBJ +emitter (`gasm asm --format goobj`) writes the format the Go linker consumes +directly: the functions as non-package symbols (the way `cmd/asm` records +assembly symbols), the `GLOBL` data, one `FuncInfo` per function and the +pc-value tables — `pcsp` built from the prologue and epilogue stack +boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a +gasm-assembled object drops into a `go build` in place of the toolchain's. +The object preamble (the version-and-experiment header the linker compares +verbatim) is captured from the installed `go tool asm`, so the output is +always consistent with the toolchain that links it. External cross-package +references and the implicit funcdata/DWARF symbols remain future work (the +linker fills the latter's defaults); the rest of Phase 2 is those, the +remaining EVEX forms and the other architectures. ## Extension points diff --git a/justfile b/justfile index ba07edb..e778886 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.11.0" +version := "0.12.0" default: @just --list diff --git a/parser/parser.go b/parser/parser.go index 16ec4c3..c7a2143 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -375,6 +375,11 @@ func parseImmediate(g []token.Token) ast.Immediate { if v, ok := tryInt(text); ok { imm.Val = v imm.HasVal = true + } else if u, err := strconv.ParseUint(text, 0, 64); err == nil && !imm.Neg { + // Unsigned 64-bit literals (DATA mask<>+8(SB)/8, $0x8000…) + // overflow int64; keep the bit pattern. + imm.Val = int64(u) + imm.HasVal = true } else { imm.Float = text }