feat(asm): the TLS access pair in the toolchain's one-instruction form

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 21:35:15 +02:00
parent 8cfd40aac8
commit 7246b0e002
11 changed files with 180 additions and 21 deletions
+10 -7
View File
@@ -15,13 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
branch optimiser does; symbol immediates (`MOVQ $sym(SB), AX`) branch optimiser does; symbol immediates (`MOVQ $sym(SB), AX`)
assemble to the toolchain's RIP-relative LEA with an R_PCREL assemble to the toolchain's RIP-relative LEA with an R_PCREL
relocation; negated constant expressions in operands (`ADJSP relocation; negated constant expressions in operands (`ADJSP
$-(REGS - 8)`, the shape the cgo ABI macros write) fold; and `gasm $-(REGS - 8)`, the shape the cgo ABI macros write) fold; the TLS
asm` predefines the `GOARCH_<arch>` and `GOOS_<goos>` macros the go access pair assembles as the toolchain's one-instruction form (the
command passes to `go tool asm`, so GOROOT headers' `#ifdef bare `MOVQ TLS, r` load nops out and `off(r)(TLS*1)` folds to the
GOARCH_amd64` platform blocks (`go_tls.h`'s `get_tls` and friends) segment-prefixed absolute whose disp32 carries the R_TLSLE
select as intended. The GOROOT corpus measure moves to 261 of 353 relocation, per-GOOS); and `gasm asm` predefines the `GOARCH_<arch>`
files assembling for every target architecture (73.9 %), 87.5 % of and `GOOS_<goos>` macros the go command passes to `go tool asm`, so
the real-code corpus, from 70.8 % and 82.2 %. GOROOT headers' `#ifdef GOARCH_amd64` platform blocks (`go_tls.h`'s
`get_tls` and friends) select as intended. The GOROOT corpus measure
moves to 267 of 353 files assembling for every target architecture
(75.6 %), 89.4 % of the real-code corpus, from 70.8 % and 82.2 %.
### Added ### Added
+95 -1
View File
@@ -38,10 +38,25 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
// rejects SB operands outright (single-function assembly cannot resolve // rejects SB operands outright (single-function assembly cannot resolve
// them). When allowExternal is set, a reference to a symbol no GLOBL in the // them). When allowExternal is set, a reference to a symbol no GLOBL in the
// file defines is recorded as an external relocation instead of failing // file defines is recorded as an external relocation instead of failing
// the object-file emitters resolve it at link time. // the object-file emitters resolve it at link time. goos selects the TLS
// access form: the empty default behaves as linux.
type linkInfo struct { type linkInfo struct {
symbols map[string]bool symbols map[string]bool
allowExternal bool allowExternal bool
goos string
}
// tlsOneInsn reports the one-instruction TLS form, obj6.go's
// CanUse1InsnTLS for the GOOS gasm supports: the bare TLS load nops out and
// the (TLS*1) index folds to a segment-absolute access. Windows and plan9
// keep the two-instruction form; shared linux does too, which gasm's raw
// path does not model and therefore does not select.
func (l *linkInfo) tlsOneInsn() bool {
switch l.goos {
case "", "linux", "freebsd":
return true
}
return false
} }
// sbPatch is a function-relative static-symbol relocation: the disp32 field // sbPatch is a function-relative static-symbol relocation: the disp32 field
@@ -834,6 +849,15 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
} }
return e.out, ps, nil, nil return e.out, ps, nil, nil
} }
// MOVQ/MOVL TLS, r: the bare TLS load. The toolchain's progedit nops
// it out on the one-instruction TLS systems (linux and freebsd, not
// shared) and encodes the segment-prefixed load elsewhere; get_tls(r),
// the macro GOROOT's go_tls.h defines, expands to exactly this
// statement, and the toolchain's pairing pass removes it whenever the
// following instruction's (TLS*1) index folds.
if (mnemUpper == "MOVQ" || mnemUpper == "MOVL") && len(s.Operands) == 2 && isBareTLS(s.Operands[0]) {
return encodeTLSBaseLoad(s, fi, link)
}
_, size := splitSize(mnemUpper) _, size := splitSize(mnemUpper)
if size == 0 { if size == 0 {
size = 8 size = 8
@@ -853,10 +877,67 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
ps := make([]sbPatch, len(e.patches)) ps := make([]sbPatch, len(e.patches))
for i, p := range e.patches { for i, p := range e.patches {
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend} ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend}
if p.tls {
ps[i].kind = RelTLSLE
}
} }
return e.out, ps, e.floatPoolList(), nil return e.out, ps, e.floatPoolList(), nil
} }
// isBareTLS reports whether the operand is the bare TLS pseudo-register
// load source, the expansion of go_tls.h's get_tls(r) macro.
func isBareTLS(op *ast.Operand) bool {
return op.Kind == ast.OpAddr && op.Addr.Sym != nil &&
op.Addr.Sym.Pseudo == "" && op.Addr.Sym.Name == "TLS" &&
op.Addr.Base == "" && op.Addr.Index == ""
}
// encodeTLSBaseLoad assembles MOVQ/MOVL TLS, r. On the one-instruction TLS
// systems (linux and freebsd outside -shared, obj6.go's CanUse1InsnTLS) the
// statement nops out: the following (TLS*1) access folds to a direct
// segment-absolute load. The two-instruction systems keep the segment load,
// nine bytes with the R_TLSLE patch site at the disp32.
func encodeTLSBaseLoad(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, []floatPoolEntry, error) {
_, size := splitSize(strings.ToUpper(s.Mnemonic.Text))
if size == 0 {
size = 8
}
dst, err := operandFromAST("MOVQ", s.Operands[1], 8, fi, link)
if err != nil {
return nil, nil, nil, err
}
reg, ok := dst.(Reg)
if !ok || reg.isVec() {
return nil, nil, nil, fmt.Errorf("TLS: destination must be a general register")
}
if link == nil || link.tlsOneInsn() {
return nil, nil, nil, nil // noped out
}
seg := byte(0x64) // FS
if link.goos == "windows" {
seg = 0x65 // GS
}
e := &enc{}
i := &instr{
prefix: seg,
rexW: size == 8,
rexR: reg.idx >= 8,
opcode: []byte{0x8B},
modrm: 0x04 | (reg.idx&7)<<3,
sib: 0x25,
disp: le32(0),
tls: true,
}
if err := e.emit(i); err != nil {
return nil, 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, kind: RelTLSLE}
}
return e.out, ps, nil, nil
}
// encodeBookkeeping accepts-and-ignores FUNCDATA and PCDATA at the statement // encodeBookkeeping accepts-and-ignores FUNCDATA and PCDATA at the statement
// level, before operand conversion: the toolchain's shapes are FUNCDATA // level, before operand conversion: the toolchain's shapes are FUNCDATA
// $n, sym(SB) and PCDATA $n, $m, and neither contributes a byte to the // $n, sym(SB) and PCDATA $n, $m, and neither contributes a byte to the
@@ -1108,6 +1189,19 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l
} }
m := Mem{Base: base, Disp: a.Offset, HasBase: true, Size: size} m := Mem{Base: base, Disp: a.Offset, HasBase: true, Size: size}
if a.Index != "" { if a.Index != "" {
if a.Index == "TLS" {
// off(base)(TLS*1): the thread-local annotation. The
// one-instruction TLS form folds it to off(TLS), the
// segment-prefixed absolute whose disp32 carries an
// R_TLS_LE patch site; the base register disappears
// from the encoding, exactly as the toolchain's
// progedit rewrites the address.
seg := byte(0x64) // FS on linux, freebsd, plan9
if link != nil && link.goos == "windows" {
seg = 0x65 // GS
}
return TLSMem{Disp: a.Offset, Size: size, Seg: seg}, nil
}
idx, ok := ParseReg(a.Index) idx, ok := ParseReg(a.Index)
if !ok { if !ok {
return nil, fmt.Errorf("unknown index register %q", a.Index) return nil, fmt.Errorf("unknown index register %q", a.Index)
+15
View File
@@ -64,6 +64,7 @@ type encPatch struct {
off int off int
name string name string
addend int64 addend int64
tls bool // a TLS slot offset: the patch is R_TLSLE with no symbol
} }
func (e *enc) encode(mnem string, ops []Operand) error { func (e *enc) encode(mnem string, ops []Operand) error {
@@ -578,6 +579,7 @@ type instr struct {
disp []byte disp []byte
imm []byte imm []byte
sb *sbRef // static-symbol displacement in disp, awaiting resolution sb *sbRef // static-symbol displacement in disp, awaiting resolution
tls bool // the displacement is a TLS slot offset, patched R_TLSLE
} }
// sbRef records that an instruction's displacement refers to a static symbol // sbRef records that an instruction's displacement refers to a static symbol
@@ -620,6 +622,9 @@ func (e *enc) emit(i *instr) error {
if i.sb != nil { if i.sb != nil {
e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend}) e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend})
} }
if i.tls {
e.patches = append(e.patches, encPatch{off: len(e.out), tls: true})
}
e.out = append(e.out, i.disp...) e.out = append(e.out, i.disp...)
e.out = append(e.out, i.imm...) e.out = append(e.out, i.imm...)
return nil return nil
@@ -674,6 +679,16 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i
i.disp = le32(0) i.disp = le32(0)
i.sb = &sbRef{name: r.name, addend: r.addend} i.sb = &sbRef{name: r.name, addend: r.addend}
return nil return nil
case TLSMem:
// off(TLS): the segment-prefixed absolute access, mod=00 with the
// SIB escape's disp32 absolute form. The displacement is the TLS
// slot offset, patched by the linker's TLS relocation.
i.prefix = r.Seg
i.modrm = 0x04 | regField<<3
i.sib = 0x25
i.disp = le32(r.Disp)
i.tls = true
return nil
default: default:
return fmt.Errorf("invalid r/m operand %T", rm) return fmt.Errorf("invalid r/m operand %T", rm)
} }
+4 -4
View File
@@ -307,12 +307,12 @@ func TestStackGuardBytesLOONG64(t *testing.T) {
func TestStackGuardGOObjInternalCall(t *testing.T) { func TestStackGuardGOObjInternalCall(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
src string src string
assemble func(*ast.File) (*Image, error) assemble func(*ast.File, ...AssembleOption) (*Image, error)
}{ }{
{"g_amd64.s", AssembleFile}, {"g_amd64.s", AssembleFile},
{"g_arm64.s", AssembleFileARM64}, {"g_arm64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileARM64(f) }},
{"g_riscv64.s", AssembleFileRISCV}, {"g_riscv64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileRISCV(f) }},
{"g_loong64.s", AssembleFileLOONG64}, {"g_loong64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileLOONG64(f) }},
} { } {
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n") f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
if len(errs) > 0 { if len(errs) > 0 {
+13
View File
@@ -192,6 +192,19 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
} }
return e.emit(i) return e.emit(i)
case TLSMem:
if !dstIsReg {
return fmt.Errorf("MOV: two memory operands")
}
// MOV r, off(TLS): the segment-prefixed absolute load, reg=dst,
// rm=src(tlsMem) through the SIB escape; the disp32 is the TLS slot
// offset with its R_TLSLE patch site.
i := newInstr(size, []byte{movRR(size)})
if err := setRM(i, dstReg, src, size); err != nil {
return err
}
return e.emit(i)
case Imm: case Imm:
if dstIsReg { if dstIsReg {
v := int64(src) v := int64(src)
+16 -1
View File
@@ -150,6 +150,18 @@ func (img *Image) Bytes() []byte {
return append(out, img.Data...) return append(out, img.Data...)
} }
// AssembleOption adjusts the file-level assembly context.
type AssembleOption func(*linkInfo)
// WithGOOS selects the target operating system for the forms that depend on
// it, the TLS access shape above all: linux and freebsd take the
// one-instruction form, windows and plan9 keep the two-instruction load.
func WithGOOS(goos string) AssembleOption {
return func(l *linkInfo) {
l.goos = goos
}
}
// AssembleFile assembles every TEXT function of a parsed file and lays out // 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 // its static symbols (GLOBL/DATA) in a data section behind the code. Each
// reference to a file-local static symbol becomes a RIP-relative load whose // reference to a file-local static symbol becomes a RIP-relative load whose
@@ -157,7 +169,7 @@ func (img *Image) Bytes() []byte {
// GLOBL defines is recorded as an external relocation (Externals) with its // GLOBL defines is recorded as an external relocation (Externals) with its
// displacement left zero, the object-file emitters resolve it at link // displacement left zero, the object-file emitters resolve it at link
// time, while the raw image (Bytes) cannot represent it. // time, while the raw image (Bytes) cannot represent it.
func AssembleFile(f *ast.File) (*Image, error) { func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) {
dataSyms, err := collectData(f) dataSyms, err := collectData(f)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -174,6 +186,9 @@ func AssembleFile(f *ast.File) (*Image, error) {
} }
} }
link := &linkInfo{symbols: known, allowExternal: true} link := &linkInfo{symbols: known, allowExternal: true}
for _, o := range opts {
o(link)
}
poolSeen := map[string]bool{} poolSeen := map[string]bool{}
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
+12
View File
@@ -36,6 +36,18 @@ type FloatImm struct {
func (FloatImm) isOperand() {} func (FloatImm) isOperand() {}
// TLSMem is a thread-local access, the source form off(base)(TLS*1) with the
// base dropped: the toolchain's one-instruction TLS rewrite assembles it as
// the segment-prefixed absolute whose disp32 carries an R_TLS_LE patch site
// (the linker fills the TLS slot offset).
type TLSMem struct {
Disp int64
Size int
Seg byte // the segment override: FS (0x64) or GS (0x65) on windows
}
func (TLSMem) isOperand() {}
// Mem is a memory operand of the form disp(base)(index*scale). // Mem is a memory operand of the form disp(base)(index*scale).
type Mem struct { type Mem struct {
Base Reg Base Reg
+2 -2
View File
@@ -671,7 +671,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
t.fail(path, errs[0]) t.fail(path, errs[0])
continue continue
} }
if _, err := assembleFile(tg.a, f); err != nil { if _, err := assembleFile(tg.a, f, goos); err != nil {
ok = false ok = false
t.fail(path, err) t.fail(path, err)
continue continue
@@ -701,7 +701,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
if len(errs) > 0 { if len(errs) > 0 {
err = errs[0] // a parse failure is a failure for every target err = errs[0] // a parse failure is a failure for every target
} else { } else {
_, err = assembleFile(tg.a, f) _, err = assembleFile(tg.a, f, goos)
} }
if err != nil { if err != nil {
ok = false ok = false
+1 -1
View File
@@ -84,7 +84,7 @@ func disSource(path string, target arch.Arch) int {
if len(errs) > 0 { if len(errs) > 0 {
return 1 return 1
} }
img, err := assembleFile(target, f) img, err := assembleFile(target, f, "")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err) fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err)
return 1 return 1
+5 -5
View File
@@ -582,7 +582,7 @@ naming the package.
return 1 return 1
} }
img, err := assembleFile(targetArch, f) img, err := assembleFile(targetArch, f, goos)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err) fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
return 1 return 1
@@ -813,10 +813,10 @@ func platformPredefinesFor(goarch string, fileGoos string) map[string]string {
} }
// assembleFile assembles a parsed file for the given architecture and returns the image. // assembleFile assembles a parsed file for the given architecture and returns the image.
func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) { func assembleFile(targetArch arch.Arch, f *ast.File, goos string) (*asm.Image, error) {
switch targetArch { switch targetArch {
case arch.AMD64: case arch.AMD64:
return asm.AssembleFile(f) return asm.AssembleFile(f, asm.WithGOOS(goos))
case arch.RISCV: case arch.RISCV:
return asm.AssembleFileRISCV(f) return asm.AssembleFileRISCV(f)
case arch.ARM64: case arch.ARM64:
@@ -847,7 +847,7 @@ func assemblePath(path string, forced arch.Arch, dirs includeDirs) (*asm.Image,
if len(errs) > 0 { if len(errs) > 0 {
return nil, fmt.Errorf("parse errors") return nil, fmt.Errorf("parse errors")
} }
return assembleFile(target, f) return assembleFile(target, f, "")
} }
// printByteDiff shows the first few byte differences between two code blocks. // printByteDiff shows the first few byte differences between two code blocks.
@@ -943,7 +943,7 @@ func cmdVerifyNonJIT(path string, targetArch arch.Arch, groundTruth, profile boo
if len(errs) > 0 { if len(errs) > 0 {
return 1 return 1
} }
img, err := assembleFile(targetArch, f) img, err := assembleFile(targetArch, f, "")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err) fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1 return 1
+7
View File
@@ -34,3 +34,10 @@ TEXT ·Frame(SB), NOSPLIT, $0
ADJSP $-(64 - 8) ADJSP $-(64 - 8)
POPFQ POPFQ
RET RET
// func Tls() int64
TEXT ·Tls(SB), NOSPLIT, $0-8
MOVQ TLS, BX
MOVQ 0(BX)(TLS*1), AX
MOVQ AX, ret+0(FP)
RET