From a829b8f3211c2f5b0851cdd7e0f2417d9ab06f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 02:09:46 +0200 Subject: [PATCH] fix(parser): read the segment-absolute rendering FS:0 The toolchain's disassembler prints the segment-prefixed disp32 absolute as FS:0, but the bare-name branch read the segment register alone and dropped the offset, so MOVQ FS:0, DX silently encoded a register move. The colon-offset form now lowers to the same segment-absolute operand the 0(FS) spelling takes. Assisted-by: GLM 5.3 Flash --- asm/rendered_text_test.go | 48 +++++++++++++++++++++++++++++++++++++++ parser/parser.go | 15 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 asm/rendered_text_test.go diff --git a/asm/rendered_text_test.go b/asm/rendered_text_test.go new file mode 100644 index 0000000..e3fe9e1 --- /dev/null +++ b/asm/rendered_text_test.go @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "encoding/hex" + "strings" + "testing" + + "sourcedock.dev/petrbalvin/gasm-sdk/parser" +) + +// TestRenderedTextEncodes pins the encoder against the disassembler's +// renderings: every line here is a text the toolchain's own decoder prints +// for bytes its assembler produced (the disasm package's parity fixtures), +// so assembling the printed text must reproduce those bytes. Each row was +// read back through the toolchain's decoder; the hex is the fixture's. +func TestRenderedTextEncodes(t *testing.T) { + for _, tt := range []struct { + text string + want string + }{ + // The segment-absolute rendering lowers to the FS-prefixed disp32 + // absolute, exactly what the 0(FS) spelling encodes. + {"MOVQ FS:0, DX", "64488b142500000000"}, + } { + src := "TEXT \u00b7k(SB), NOSPLIT, $0\n\t" + tt.text + "\n\tRET\n" + f, errs := parser.Parse("k.s", src) + if len(errs) > 0 { + t.Errorf("%s: parse: %v", tt.text, errs[0]) + continue + } + img, err := AssembleFile(f) + if err != nil { + t.Errorf("%s: assemble: %v", tt.text, err) + continue + } + fn := img.Funcs[0] + code := img.Code[fn.Offset : fn.Offset+fn.Size] + // The no-fallthrough rule appends the RET; the pinned instruction is + // the prefix. + got := hex.EncodeToString(code) + if !strings.HasPrefix(got, tt.want) { + t.Errorf("%s: bytes %s, want prefix %s", tt.text, got, tt.want) + } + } +} diff --git a/parser/parser.go b/parser/parser.go index f0e4c38..c49e629 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -678,6 +678,21 @@ func parseAddress(g []token.Token) ast.Address { i++ } } + // Segment-absolute in the rendering spelling: FS:0 and GS:0. The + // colon-offset form is what the toolchain's disassembler prints for the + // segment-prefixed disp32 absolute, so the rendered text must re-encode; + // without it the segment register alone parsed and the offset vanished. + // The base field naming the segment lowers to the same operand the + // 0(FS) spelling takes. + if len(g) >= 3 && g[0].Kind == token.Ident && g[1].Kind == token.Colon && + (g[0].Text == "FS" || g[0].Text == "GS") { + if v, rest, ok := foldExpr(g[2:]); ok && len(rest) == 0 { + addr.Base = g[0].Text + addr.Offset = v + addr.HasOff = true + return addr + } + } // Bare name (register, label or symbol) possibly with an arm64 shift. if addr.Base == "" && addr.Sym == nil && g[0].Kind == token.Ident { sym := &ast.Symbol{Pos: g[0].Pos}