From 2c9042d62cc13c5472e8772d395255ffc8ef6921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 21 Sep 2026 22:00:07 +0200 Subject: [PATCH] feat(asm): PCALIGN alignment on amd64 Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 15 ++++---- asm/assemble.go | 71 +++++++++++++++++++++++++++++++++++ testdata/verify/forms_amd64.s | 9 +++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7c7ec1..f01acc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,13 +22,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 relocation, per-GOOS); arm64 accepts the bare-register indirect branch (`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); 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 276 of 353 files assembling for every target architecture - (78.2 %), 91.4 % of the real-code corpus (280 of 303), from 70.8 % - and 82.2 %. + as the toolchain does); `PCALIGN` now aligns on amd64, padding with the + toolchain's greedy single-instruction NOPs; 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 %. ### Added diff --git a/asm/assemble.go b/asm/assemble.go index 3868016..1fbda8d 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -122,6 +122,19 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [ case *ast.Label: offsets[s.Name.Text] = pos case *ast.Instr: + if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" { + // The alignment pseudo-statement: its size is the + // padding to the next boundary at this very position, + // filled with NOPs at emission. + pad, err := pcAlignPad(pcAlignValue(s), pos) + if err != nil { + return nil, nil, nil, nil, nil, nil, fmt.Errorf("PCALIGN: %w", err) + } + sizes[i] = pad + pcs[i] = pos + pos += pad + continue + } sz, err := instrSize(s, fi, long[i], link) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err) @@ -548,6 +561,52 @@ func pcJumpTarget(t *ast.Text, j, n int, pcs []int) (int, bool) { return 0, false } +// x86 NOP encodings, single-instruction no-ops of lengths 1 to 9 (the +// toolchain's asm6.go nop table); longer padding repeats the largest that +// fits, greedy from the end. +var x86Nops = [][]byte{ + {0x90}, + {0x66, 0x90}, + {0x0F, 0x1F, 0x00}, + {0x0F, 0x1F, 0x40, 0x00}, + {0x0F, 0x1F, 0x44, 0x00, 0x00}, + {0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00}, + {0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00}, + {0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}, +} + +// fillNOPs fills p with the greedy largest single-instruction NOPs, exactly +// the toolchain's fillnop. +func fillNOPs(p []byte) { + for len(p) > 0 { + m := min(len(p), len(x86Nops)) + copy(p[:m], x86Nops[m-1]) + p = p[m:] + } +} + +// pcAlignPad computes the padding PCALIGN $align inserts at pos: the +// alignment must be a power of two in [8, 2048] and the padding runs to the +// next boundary (zero when the position is already aligned). +func pcAlignPad(align, pos int) (int, error) { + if align <= 0 || align&(align-1) != 0 || align < 8 || align > 2048 { + return 0, fmt.Errorf("alignment value of an instruction must be a power of two and in the range [8, 2048], got %d", align) + } + if lob := pos & (align - 1); lob != 0 { + return align - lob, nil + } + return 0, nil +} + +// pcAlignValue reads a PCALIGN statement's alignment operand. +func pcAlignValue(s *ast.Instr) int { + if len(s.Operands) == 1 && s.Operands[0].Kind == ast.OpImmediate && s.Operands[0].Imm.HasVal { + return int(s.Operands[0].Imm.Val) + } + return 0 // rejected by pcAlignPad's range check +} + // hasCall reports whether the function body contains a CALL instruction. func hasCall(t *ast.Text) bool { for _, stmt := range t.Body { @@ -760,6 +819,18 @@ func jumpSize(mnem string, long bool) int { func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string, link *linkInfo, numTarget int) ([]byte, []sbPatch, []floatPoolEntry, error) { mnem := strings.ToUpper(s.Mnemonic.Text) + if mnem == "PCALIGN" { + // The layout pass already accounted the padding; emit the same + // amount of NOP bytes for the statement's own position. + pad, err := pcAlignPad(pcAlignValue(s), pc) + if err != nil { + return nil, nil, nil, err + } + out := make([]byte, pad) + fillNOPs(out) + return out, nil, nil, nil + } + var prefix []byte if mnem == "RET" && fi.useFP { prefix = fi.epilogue diff --git a/testdata/verify/forms_amd64.s b/testdata/verify/forms_amd64.s index 8b479f3..5e2e3d8 100644 --- a/testdata/verify/forms_amd64.s +++ b/testdata/verify/forms_amd64.s @@ -41,3 +41,12 @@ TEXT ·Tls(SB), NOSPLIT, $0-8 MOVQ 0(BX)(TLS*1), AX MOVQ AX, ret+0(FP) RET + +// func Aligned() int64 +TEXT ·Aligned(SB), NOSPLIT, $0-8 + MOVQ $1, AX + PCALIGN $16 + MOVQ $2, AX + PCALIGN $32 + MOVQ AX, ret+0(FP) + RET