From f720381d434b5ef2085640d3facbb6a43c6976a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 21 Sep 2026 22:19:26 +0200 Subject: [PATCH] feat(asm): the segment-absolute and crash-store forms GOROOT writes Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 10 ++++++---- asm/assemble.go | 24 ++++++++++++++++++++++++ asm/encode.go | 18 ++++++++++++++++++ asm/instrs.go | 26 +++++++++++++++++++++++++- asm/operand.go | 12 ++++++++++++ parser/parser.go | 20 ++++++++++++++++++++ 6 files changed, 105 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f01acc2..3a0e94e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,13 +23,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`BL R9` beside `BL (R9)`, both BLR) and the zero-immediate store (`MOVD $0, mem` through the zero register, rejecting non-zero immediates as the toolchain does); `PCALIGN` now aligns on amd64, padding with the - toolchain's greedy single-instruction NOPs; and `gasm asm` predefines + toolchain's greedy single-instruction NOPs; the segment-absolute forms + (`MOVQ 0x30(GS), AX` and the store direction) and the absolute + crash-store (`MOVL $0xf1, 0xf1`) encode; and `gasm asm` predefines the `GOARCH_` and `GOOS_` macros the go command passes to `go tool asm`, so GOROOT headers' `#ifdef GOARCH_amd64` platform blocks (`go_tls.h`'s `get_tls` and friends) select as intended. The GOROOT - corpus measure moves to 285 of 353 files assembling for every target - architecture (80.7 %), 91.4 % of the real-code corpus (280 of 303), - from 70.8 % and 82.2 %. + corpus measure moves to 291 of 353 files assembling for every target + architecture (82.4 %), 97.4 % of the real-code corpus, from 70.8 % + and 82.2 %. ### Added diff --git a/asm/assemble.go b/asm/assemble.go index 1fbda8d..3b46849 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -1254,6 +1254,16 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l // Memory with a real base register: (base), off(base), (base)(index*scale). if a.Base != "" { + // Segment-absolute: 0x30(GS) and 0x28(FS), the windows TLS + // spellings. The segment override prefixes a disp32 absolute + // reference with no relocation. + if a.Base == "GS" || a.Base == "FS" { + seg := byte(0x64) + if a.Base == "GS" { + seg = 0x65 + } + return SegAbs{Disp: a.Offset, Size: size, Seg: seg}, nil + } base, ok := ParseReg(a.Base) if !ok { return nil, fmt.Errorf("unknown base register %q", a.Base) @@ -1273,6 +1283,15 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l } return TLSMem{Disp: a.Offset, Size: size, Seg: seg}, nil } + if a.Index == "GS" || a.Index == "FS" { + // 0(CX)(GS): the segment annotation rides the base + // access as the override prefix. + m.Seg = 0x64 + if a.Index == "GS" { + m.Seg = 0x65 + } + return m, nil + } idx, ok := ParseReg(a.Index) if !ok { return nil, fmt.Errorf("unknown index register %q", a.Index) @@ -1293,6 +1312,11 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l } return Mem{Index: idx, Scale: a.Scale, Disp: a.Offset, HasIndex: true, Size: size}, nil } + // A bare displacement with no base: the absolute address form, + // MOVL $0xf1, 0xf1. No segment and no relocation. + if a.Sym == nil && a.Base == "" && a.Index == "" && a.HasOff { + return SegAbs{Disp: a.Offset, Size: size}, nil + } // Bare register. if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" { if r, ok := ParseReg(a.Sym.Name); ok { diff --git a/asm/encode.go b/asm/encode.go index 795cbed..6019cac 100644 --- a/asm/encode.go +++ b/asm/encode.go @@ -689,12 +689,20 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i i.disp = le32(r.Disp) i.tls = true return nil + case SegAbs: + // 0x30(GS): the segment override with the SIB escape's disp32 + // absolute form, no relocation. + setSegAbs(i, regField, r) + return nil default: return fmt.Errorf("invalid r/m operand %T", rm) } } func setMem(i *instr, regField int, m Mem) error { + if m.Seg != 0 { + i.prefix = m.Seg + } modrm, sib, disp, xBit, bBit, err := memComponents(regField, m) if err != nil { return err @@ -707,6 +715,16 @@ func setMem(i *instr, regField int, m Mem) error { return nil } +// setSegAbs assembles a segment-absolute operand, 0x30(GS): the segment +// override with the mod=00 SIB escape's disp32 absolute form and no +// relocation. +func setSegAbs(i *instr, regField int, m SegAbs) { + i.prefix = m.Seg + i.modrm = 0x04 | regField<<3 + i.sib = 0x25 + i.disp = le32(m.Disp) +} + // memComponents computes the ModR/M byte (with the given reg field), the SIB // byte (-1 if none), the displacement bytes, and the high index/base bits, for // a memory operand. It is shared by the REX (scalar) and VEX (vector) paths. diff --git a/asm/instrs.go b/asm/instrs.go index afd6676..4f40c12 100644 --- a/asm/instrs.go +++ b/asm/instrs.go @@ -205,6 +205,17 @@ func (e *enc) encodeMov(ops []Operand, size int) error { } return e.emit(i) + case SegAbs: + if !dstIsReg { + return fmt.Errorf("MOV: two memory operands") + } + // MOV r, 0x30(GS): the segment-absolute load. + 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 { v := int64(src) @@ -245,11 +256,24 @@ func (e *enc) encodeMov(ops []Operand, size int) error { i.imm = imm return e.emit(i) } - // MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0. + // MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0. An immediate in the + // destination slot is the absolute-address crash-store spelling, + // MOVL $0xf1, 0xf1: the parser reads the trailing bare constant + // as an immediate, and the store's disp32 carries the address. op := byte(0xC7) if size == 1 { op = 0xC6 } + if d, ok := dst.(Imm); ok { + i := newInstr(size, []byte{op}) + setSegAbs(i, 0, SegAbs{Disp: int64(d)}) + immBytes, err := immediate(int64(src), size, false) + if err != nil { + return err + } + i.imm = immBytes + return e.emit(i) + } i := newInstr(size, []byte{op}) if err := setRMDigit(i, 0, dst, size); err != nil { return err diff --git a/asm/operand.go b/asm/operand.go index 11d0998..9de5aa4 100644 --- a/asm/operand.go +++ b/asm/operand.go @@ -48,6 +48,17 @@ type TLSMem struct { func (TLSMem) isOperand() {} +// SegAbs is a segment-absolute access, 0x30(GS): the segment override +// prefixes a disp32 absolute reference with no relocation. The base +// register spellings GS and FS produce it. +type SegAbs struct { + Disp int64 + Size int + Seg byte // 0x64 FS, 0x65 GS +} + +func (SegAbs) isOperand() {} + // Mem is a memory operand of the form disp(base)(index*scale). type Mem struct { Base Reg @@ -57,6 +68,7 @@ type Mem struct { Size int // operand width in bytes HasBase bool HasIndex bool + Seg byte // segment override prefix (0x64 FS, 0x65 GS); 0 = none } func (Mem) isOperand() {} diff --git a/parser/parser.go b/parser/parser.go index 9f7cf45..9739256 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -684,6 +684,26 @@ func parseAddress(g []token.Token) ast.Address { if i > 0 && i < len(g) { addr.Shift = joinRaw(g[i:]) } + // A lone (possibly signed) number is an absolute address: MOVL $0xf1, + // 0xf1 stores through the bare displacement with no base at all. In + // operand position a number without $ is an address, never a value. + if addr.Sym == nil && addr.Base == "" && addr.Index == "" && !addr.HasOff { + neg := false + j := 0 + if j < len(g) && (g[j].Kind == token.Minus || g[j].Kind == token.Plus) { + neg = g[j].Kind == token.Minus + j++ + } + if j == len(g)-1 && g[j].Kind == token.Number { + v := parseInt(g[j].Text) + if neg { + v = -v + } + addr.Offset = v + addr.HasOff = true + return addr + } + } return addr }