mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* gcasm: O(1) jump tables — init-scanned address tables replace the compare tree gc's jump-table dispatch (LEAQ table + JMP (base)(idx*8)) could not be reproduced in hand-written Plan9 asm because DATA cannot hold address-of-label; emitJumpTree degraded it to an O(log n) compare tree, a measured pessimization on interpreter-style guests (spidermonkey PBL, CPython) — and the pure-fallback alternative (#34) regressed consumer cold-build time up to +49% by handing the largest functions back to gc. This restores gc's exact O(1) shape. The dispatch is retargeted at a Go-declared table var — LEAQ ·fnN_jtOFF(SB), base; JMP (base)(idx*8) (arm64: the same MOVD/MOVD/JMP triple) — clobbering only the registers gc's own dispatch clobbered and, like gc's dispatch, writing NO flags, so the tree's whole flag-replay machinery (and its unreplayable-flags pure-fallback class) does not apply to the pad. Table entries are the ABSOLUTE addresses of per-target stubs emitted right after the dispatch, each opening with an executable no-op signature (amd64: 0F 1F 80 sig32 + complement twin; arm64: a MOVZ/MOVK/MOVK triple into the R17 scratch) followed by JMP pcN. Because the consumer's assembler picks final encodings, addresses are never baked: a generated package init() scans the function's code bytes for the signatures (via a tiny fnN_jtpc address helper) and fills the tables — PIE-safe, toolchain-version-safe, and a missing signature panics at init instead of ever wild-jumping. Plumbing follows the ConstPool/TypeTable pattern: TransformOptions.JT accumulates per-file metadata; Build appends JT.EmitAsm() to each .s and JT.EmitGo() (tables + init + scanner) to the decls file. JT=nil keeps the legacy compare tree (finders take a flagTransparent flag the pad sets to skip the moot NZCV/EFLAGS analysis). Gates now run the pad end to end: the 64-case amd64 dispatch gate, the flag-consuming replay gates on BOTH arches (arm64 executes natively; its fixture previously could not transform at all on this toolchain — flag transparency resolves it), and the cg_bigdispatch / cg_brtable64 / cg_dispatchif fixture gates. Full go test ./... and make lint green. Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: inline the FNV-1a signature hash (errcheck-clean) Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: arm64 pad register discipline + expected-set signature filter Two consumer-suite findings on the real spidermonkey bundle: 1. arm64 dispatch correctness. The first pad hardcoded R16/R17 — allocatable under gc, so live values could be clobbered — and re-assembled the captured (Rb)(Ri<<3) as the 64-bit X-form when gc's original load is ldr xt,[xb,w_i,uxtw #3]: selector high-bit garbage (legal under gc) poisoned the address and crashed the arm64 lane with SIGSEGV. The pad now clobbers exactly the captured table-base and target registers (what gc's own dispatch clobbered) and re-establishes uxtw with an explicit MOVWU (br_table selectors are wasm i32). Signature MOVZ/MOVK writes follow the target register; the scanner masks Rd out. 2. Scanner false positives. Real arm64 code materializes 48-bit constants with the same MOVZ/MOVK/MOVK shape; those matches consumed the scan budget and ended the scan before the true stubs, panicking init with "signature missing" (the fail-fast doing its job). Both scanners now count only signatures the spec expects. go-spidermonkey matrix green on the regenerated bundle: amd64/arm64, plain and -race, all exit 0. cpubench arm64-native interleaved A/B (5 pairs, ranges disjoint): compare tree 60.29 ms -> pad 55.56 ms (-7.9%). Cold build of the bundle: 25.5s vs 23.7s baseline (the pure-fallback approach measured 35.3s). Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
380 lines
12 KiB
Go
380 lines
12 KiB
Go
package gcasm
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// a64StripPrologueEpilogue removes the captured arm64 stack-check,
|
|
// frame setup, frame teardown and morestack tail so the assembler
|
|
// regenerates them from the declared frame size. Handles gc's three
|
|
// shapes: LEAF (no frame), small-frame (MOVD.W R30 / MOVD R29), and
|
|
// large-frame (SUB/STP pair).
|
|
func a64StripPrologueEpilogue(insns []Insn) ([]Insn, error) {
|
|
drop := map[int]bool{}
|
|
i := 0
|
|
splitTarget := -1
|
|
|
|
// Stack check. The exact instruction sequence varies with frame
|
|
// size (small: CMP RSP; large: SUB/SUBS into R17 with an optional
|
|
// materialised constant and a wraparound BLO guard), so rather than
|
|
// enumerate every variant, drop the whole check region — from
|
|
// `MOVD 16(g),R16` up to (not including) the frame setup — and take
|
|
// splitTarget as the FARTHEST branch target seen (the morestack
|
|
// tail sits at the very end of the function).
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "MOVD\t16(g),") {
|
|
drop[i] = true
|
|
i++
|
|
for i < len(insns) {
|
|
t := insns[i].Text
|
|
// Stop at the frame setup (small: MOVD.W R30 / large: SUB
|
|
// ...,RSP,R20) or any real body instruction.
|
|
if strings.HasPrefix(t, "MOVD.W\tR30,") ||
|
|
(strings.HasPrefix(t, "SUB\t") && strings.HasSuffix(t, ", RSP, R20")) {
|
|
break
|
|
}
|
|
// Within the check: comparisons, the RSP-frame computation
|
|
// (into R17), the frame-constant materialisation, and the
|
|
// guard branches. Branches record their target.
|
|
// Check temps are R17 (RSP-frame) and R27 (materialised
|
|
// frame constant); R20 belongs to the frame setup and must
|
|
// NOT be swept here.
|
|
isCheck := strings.HasPrefix(t, "CMP") ||
|
|
(strings.HasSuffix(t, ", R17") && (strings.HasPrefix(t, "SUB") || strings.HasPrefix(t, "MOVD"))) ||
|
|
(strings.HasPrefix(t, "MOVD\t$") && strings.HasSuffix(t, ", R27"))
|
|
if m := a64BranchRe.FindStringSubmatch(t); m != nil {
|
|
var tt int
|
|
if _, err := fmt.Sscanf(m[3], "%d", &tt); err == nil && tt > splitTarget {
|
|
splitTarget = tt
|
|
}
|
|
drop[i] = true
|
|
i++
|
|
continue
|
|
}
|
|
if !isCheck {
|
|
break
|
|
}
|
|
drop[i] = true
|
|
i++
|
|
}
|
|
}
|
|
|
|
// Frame setup. Small: MOVD.W R30,-N(RSP) / MOVD R29,-8(RSP) / SUB $8,RSP,R29.
|
|
// Large: SUB $N,RSP,R20 / STP (R29,R30),-8(R20) / MOVD R20,RSP / SUB $8,RSP,R29.
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "MOVD.W\tR30,") {
|
|
drop[i] = true
|
|
i++
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "MOVD\tR29,") {
|
|
drop[i] = true
|
|
i++
|
|
}
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "SUB\t$8, RSP, R29") {
|
|
drop[i] = true
|
|
}
|
|
} else if i < len(insns) && strings.HasPrefix(insns[i].Text, "SUB\t$") && strings.HasSuffix(insns[i].Text, ", RSP, R20") {
|
|
drop[i] = true
|
|
i++
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "STP\t(R29, R30),") {
|
|
drop[i] = true
|
|
i++
|
|
}
|
|
if i < len(insns) && insns[i].Text == "MOVD\tR20, RSP" {
|
|
drop[i] = true
|
|
i++
|
|
}
|
|
if i < len(insns) && strings.HasPrefix(insns[i].Text, "SUB\t$8, RSP, R29") {
|
|
drop[i] = true
|
|
}
|
|
}
|
|
|
|
// Frame teardown before each RET. Small: MOVD -8(RSP),R29 / MOVD.P
|
|
// N(RSP),R30. Large: LDP -8(RSP),(R29,R30) / ADD $N,RSP — or, when
|
|
// N doesn't fit an add immediate, MOVD $N,R27 / ADD R27,RSP,RSP.
|
|
for k, in := range insns {
|
|
if !strings.HasPrefix(in.Text, "RET") {
|
|
continue
|
|
}
|
|
for back := k - 1; back >= 0 && back >= k-4; back-- {
|
|
t := insns[back].Text
|
|
// SP restore: `ADD $N, RSP` (imm) or `ADD R27, RSP` (reg,
|
|
// for frame sizes needing a materialised constant).
|
|
isSPrestore := strings.HasPrefix(t, "ADD\t") && strings.HasSuffix(t, ", RSP")
|
|
isFrameConst := strings.HasPrefix(t, "MOVD\t$") && strings.HasSuffix(t, ", R27")
|
|
if strings.HasPrefix(t, "MOVD\t-8(RSP), R29") ||
|
|
(strings.HasPrefix(t, "MOVD.P\t") && strings.HasSuffix(t, "(RSP), R30")) ||
|
|
strings.HasPrefix(t, "LDP\t-8(RSP), (R29, R30)") ||
|
|
isSPrestore || isFrameConst {
|
|
drop[back] = true
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Morestack tail: from the split target to the end.
|
|
if splitTarget >= 0 {
|
|
on := false
|
|
for k, in := range insns {
|
|
if in.Off == splitTarget {
|
|
on = true
|
|
}
|
|
if on {
|
|
drop[k] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var out []Insn
|
|
for k, in := range insns {
|
|
if !drop[k] {
|
|
out = append(out, in)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// a64FindJumpTables detects gc arm64 jump-table dispatch triples
|
|
// (`MOVD $tab(SB),Rb; MOVD (Rb)(Ri<<3),Rt; JMP (Rt)`) and resolves the
|
|
// table's R_ADDR relocs to run-compressed target lists.
|
|
//
|
|
// flagTransparent declares that the dispatch REWRITE preserves NZCV
|
|
// (the O(1) jump pad — arm64 ADD writes no flags), making the whole
|
|
// flag-liveness analysis moot: no replay is captured and no site can
|
|
// fail as unreplayable. The compare tree passes false.
|
|
func a64FindJumpTables(fnName string, insns []Insn, datas map[string]*DataSym, flagTransparent bool) (map[int]*jtSite, error) {
|
|
sites := map[int]*jtSite{}
|
|
for i, in := range insns {
|
|
lm := a64JtLeaqRe.FindStringSubmatch(in.Text)
|
|
if lm == nil {
|
|
continue
|
|
}
|
|
// Next non-NOP: the indexed load.
|
|
j := i + 1
|
|
for j < len(insns) && insns[j].Text == "NOP" {
|
|
j++
|
|
}
|
|
if j >= len(insns) {
|
|
return nil, fmt.Errorf("jump-table load %q at end of body", in.Text)
|
|
}
|
|
dm := a64JtLdrRe.FindStringSubmatch(insns[j].Text)
|
|
if dm == nil || dm[1] != lm[2] {
|
|
return nil, fmt.Errorf("jump-table %q not followed by indexed load (got %q)", in.Text, insns[j].Text)
|
|
}
|
|
idxReg := dm[2]
|
|
tReg := dm[3]
|
|
k := j + 1
|
|
for k < len(insns) && insns[k].Text == "NOP" {
|
|
k++
|
|
}
|
|
jm := a64JtJmpRe.FindStringSubmatch(insns[k].Text)
|
|
if jm == nil || jm[1] != tReg {
|
|
return nil, fmt.Errorf("jump-table load %q not followed by indirect JMP (got %q)", insns[j].Text, insns[k].Text)
|
|
}
|
|
tab, ok := datas[lm[1]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("jump table %s not captured", lm[1])
|
|
}
|
|
if len(tab.Relocs) == 0 || len(tab.Relocs)*8 != tab.Size {
|
|
return nil, fmt.Errorf("jump table %s: %d relocs for size %d", lm[1], len(tab.Relocs), tab.Size)
|
|
}
|
|
site := &jtSite{idx: i, jmpIdx: k, idxReg: idxReg, baseReg: lm[2], tReg: tReg, entryCount: len(tab.Relocs)}
|
|
for ri, r := range tab.Relocs {
|
|
if r.Off != ri*8 {
|
|
return nil, fmt.Errorf("jump table %s: reloc %d at offset %d", lm[1], ri, r.Off)
|
|
}
|
|
if r.Sym != fnName {
|
|
return nil, fmt.Errorf("jump table %s: reloc targets %s, not %s", lm[1], r.Sym, fnName)
|
|
}
|
|
if n := len(site.runs); n > 0 && site.runs[n-1].target == r.Addend {
|
|
continue
|
|
}
|
|
site.runs = append(site.runs, jtRun{start: ri, target: r.Addend})
|
|
}
|
|
// NZCV liveness across the dispatch — the arm64 port of the
|
|
// amd64 findJumpTables replay logic. gc's captured dispatch
|
|
// (MOVD $tab / indexed MOVD / indirect JMP) PRESERVES NZCV,
|
|
// and gc exploits that by letting a dispatch target consume
|
|
// flags set before the dispatch (typically the bounds-check
|
|
// `CMPW $max, Ri` ahead of the table jump). The compare tree
|
|
// a64EmitJumpTree emits (CMPW/BHS) CLOBBERS NZCV, so the
|
|
// pre-dispatch flag state must be RESTORED at every tree leaf.
|
|
// Detection of which targets consume flags is unsound (a
|
|
// target can reach a flag read across an unconditional JMP or
|
|
// an unrecorded offset — the exact false-negative hole that
|
|
// produced the amd64 wild-pointer SIGSEGV, and, unported, the
|
|
// arm64 go-python SIGSEGV in Fn3705's dispatch), so: if a
|
|
// CLEAN, replayable flag-setter sits immediately before the
|
|
// dispatch, ALWAYS replay it at the leaves. Replaying a
|
|
// compare nothing reads is harmless — it writes only flags.
|
|
//
|
|
// Clean means: a pure register/immediate CMP/CMN/TST (no
|
|
// memory operand — leaves get no frame-shift rewrite) whose
|
|
// operands exclude the table-base and target registers, which
|
|
// the captured dispatch clobbers between the compare and the
|
|
// leaf (the tree itself only READS the selector register).
|
|
if flagTransparent {
|
|
sites[i] = site
|
|
continue
|
|
}
|
|
replay, replayClean := "", false
|
|
for k2 := i - 1; k2 >= 0 && i-k2 <= 8; k2-- {
|
|
t := insns[k2].Text
|
|
if a64WritesFlags(t) {
|
|
if (strings.HasPrefix(t, "CMP") || strings.HasPrefix(t, "CMN") || strings.HasPrefix(t, "TST")) &&
|
|
!strings.Contains(t, "(") &&
|
|
!a64MentionsReg(t, lm[2]) && !a64MentionsReg(t, tReg) {
|
|
replay, replayClean = t, true
|
|
}
|
|
break
|
|
}
|
|
// An unconditional JMP breaks linear fall-through: the
|
|
// flag state at the dispatch then depends on the jump-in
|
|
// path, not on this scan's instructions.
|
|
if strings.HasPrefix(t, "CALL") || strings.HasPrefix(t, "RET") || strings.HasPrefix(t, "JMP") {
|
|
break
|
|
}
|
|
}
|
|
if replayClean {
|
|
site.replay = replay
|
|
} else {
|
|
// No cleanly-replayable flag-setter. Fall back to pure
|
|
// unless we can prove NO target consumes flags
|
|
// (conservative: any unverifiable target counts as
|
|
// consuming).
|
|
offIndex := map[int]int{}
|
|
for k2, in2 := range insns {
|
|
offIndex[in2.Off] = k2
|
|
}
|
|
consumes := false
|
|
for _, r := range site.runs {
|
|
k2, ok := offIndex[r.target]
|
|
if !ok {
|
|
consumes = true
|
|
break
|
|
}
|
|
for ; k2 < len(insns); k2++ {
|
|
t := insns[k2].Text
|
|
if a64ReadsFlags(t) {
|
|
consumes = true
|
|
break
|
|
}
|
|
if a64WritesFlags(t) || strings.HasPrefix(t, "CALL") || strings.HasPrefix(t, "RET") || strings.HasPrefix(t, "JMP") {
|
|
break
|
|
}
|
|
}
|
|
if consumes {
|
|
break
|
|
}
|
|
}
|
|
if consumes {
|
|
return nil, fmt.Errorf("%w: jump table at +%d: flags consumed by targets but no replayable flag-setter found", errUnsupportedJumpTable, in.Off)
|
|
}
|
|
}
|
|
sites[i] = site
|
|
}
|
|
owned := map[int]bool{}
|
|
for _, s := range sites {
|
|
owned[s.jmpIdx] = true
|
|
}
|
|
for i, in := range insns {
|
|
if a64JtJmpRe.MatchString(in.Text) && !owned[i] {
|
|
return nil, fmt.Errorf("unmatched indirect JMP %q at +%d", in.Text, in.Off)
|
|
}
|
|
}
|
|
return sites, nil
|
|
}
|
|
|
|
// a64EmitJumpTree emits a binary search tree over the site's runs
|
|
// dispatching to pcN labels (O(log n)). The selector is bounds-checked
|
|
// by the captured code (CMP/BHI to default) before the table jump.
|
|
func a64EmitJumpTree(b *strings.Builder, site *jtSite, leaqOff int) {
|
|
labelID := 0
|
|
var rec func(lo, hi int)
|
|
rec = func(lo, hi int) {
|
|
if lo == hi {
|
|
// Restore the dispatch-entry NZCV state clobbered by the
|
|
// tree's compares (see the replay scan in
|
|
// a64FindJumpTables); the tree writes nothing but flags,
|
|
// so the compare's operands are unchanged.
|
|
if site.replay != "" {
|
|
fmt.Fprintf(b, "\t%s\n", site.replay)
|
|
}
|
|
fmt.Fprintf(b, "\tJMP pc%d\n", site.runs[lo].target)
|
|
return
|
|
}
|
|
mid := (lo + hi + 1) / 2
|
|
labelID++
|
|
lbl := fmt.Sprintf("jt%d_%d", leaqOff, labelID)
|
|
// CMPW imm, reg then BHS (unsigned >=) to the high half.
|
|
fmt.Fprintf(b, "\tCMPW $%d, %s\n", site.runs[mid].start, site.idxReg)
|
|
fmt.Fprintf(b, "\tBHS %s\n", lbl)
|
|
rec(lo, mid-1)
|
|
fmt.Fprintf(b, "%s:\n", lbl)
|
|
rec(mid, hi)
|
|
}
|
|
rec(0, len(site.runs)-1)
|
|
}
|
|
|
|
// a64WritesFlags reports whether an arm64 instruction writes NZCV:
|
|
// compares (CMP/CMN/TST, conditional CCMP/CCMN, float FCMP) and the
|
|
// flag-setting S-suffixed arithmetic forms. CBZ/CBNZ/TBZ/TBNZ test a
|
|
// register directly and do NOT touch NZCV.
|
|
func a64WritesFlags(txt string) bool {
|
|
mn := txt
|
|
if i := strings.IndexByte(mn, '\t'); i >= 0 {
|
|
mn = mn[:i]
|
|
}
|
|
for _, p := range []string{"CMP", "CMN", "TST", "CCMP", "CCMN", "FCMP",
|
|
"ADDS", "SUBS", "ANDS", "BICS", "ADCS", "SBCS", "NEGS", "NGCS"} {
|
|
if strings.HasPrefix(mn, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// a64ReadsFlags reports whether an arm64 instruction reads NZCV:
|
|
// conditional branches, conditional selects/sets, conditional
|
|
// compares, and the carry-consuming arithmetic family.
|
|
func a64ReadsFlags(txt string) bool {
|
|
mn := txt
|
|
if i := strings.IndexByte(mn, '\t'); i >= 0 {
|
|
mn = mn[:i]
|
|
}
|
|
switch mn {
|
|
case "BEQ", "BNE", "BHS", "BCS", "BLO", "BCC", "BMI", "BPL",
|
|
"BVS", "BVC", "BHI", "BLS", "BGE", "BLT", "BGT", "BLE":
|
|
return true
|
|
}
|
|
for _, p := range []string{"CSEL", "CSET", "CSETM", "CSINC", "CSINV", "CSNEG",
|
|
"CINC", "CINV", "CNEG", "CCMP", "CCMN", "FCSEL", "ADC", "SBC", "NGC"} {
|
|
if strings.HasPrefix(mn, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// a64MentionsReg reports whether the instruction text references reg
|
|
// as a whole token (R1 must not match R16).
|
|
func a64MentionsReg(txt, reg string) bool {
|
|
for i := 0; ; {
|
|
j := strings.Index(txt[i:], reg)
|
|
if j < 0 {
|
|
return false
|
|
}
|
|
j += i
|
|
end := j + len(reg)
|
|
beforeOK := j == 0 || !isAlnumByte(txt[j-1])
|
|
afterOK := end >= len(txt) || !isAlnumByte(txt[end])
|
|
if beforeOK && afterOK {
|
|
return true
|
|
}
|
|
i = end
|
|
}
|
|
}
|
|
|
|
func isAlnumByte(c byte) bool {
|
|
return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
|
|
}
|