280 lines
8.8 KiB
Go
280 lines
8.8 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package asm
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
|
)
|
|
|
|
// The mid-function literal pool flush, pinned against the toolchain. A
|
|
// kernel of distinct pooled stores walks the conservative displacement bound
|
|
// (asm7.go's maxPCDisp) inside one body, so the pool must drain exactly
|
|
// where cmd/internal/obj/arm64's checkpool drains it: the bytes, the flush
|
|
// points and the reach of every load literal are the toolchain's own.
|
|
|
|
// pooledKernel renders a kernel of n distinct pooled stores: each offset
|
|
// sits a step of 8 past the aligned split band (every fourth one aligned,
|
|
// but 0x2000000+8i+4 never is), so every statement pools its own four-byte
|
|
// word and expands to eight instruction bytes, the densest walk towards the
|
|
// distance bound an arm64 body can make. head carries the TEXT line for the
|
|
// splitting variant, tail the closing statements.
|
|
func pooledKernel(head string, n int, tail string) string {
|
|
var b strings.Builder
|
|
b.WriteString("#include \"textflag.h\"\n")
|
|
if head != "" {
|
|
b.WriteString(head)
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("TEXT \u00b7poolmid(SB), NOSPLIT, $0-0\n")
|
|
for i := range n {
|
|
b.WriteString("\tMOVD\tR1, ")
|
|
b.WriteString(decimal(0x2000000 + 8*i + 4))
|
|
b.WriteString("(R2)\n")
|
|
}
|
|
b.WriteString(tail)
|
|
return b.String()
|
|
}
|
|
|
|
// decimal formats v in decimal.
|
|
func decimal(v int) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for v > 0 {
|
|
i--
|
|
buf[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
// assemblePooled parses and assembles a generated kernel, returning the
|
|
// image and the single function's code words.
|
|
func assemblePooled(t *testing.T, head string, n int, tail string) (*Image, []uint32) {
|
|
t.Helper()
|
|
src := pooledKernel(head, n, tail)
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "poolmid_arm64.s")
|
|
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
if len(errs) > 0 {
|
|
t.Fatalf("parse: %v", errs)
|
|
}
|
|
img, err := AssembleFileARM64(f)
|
|
if err != nil {
|
|
t.Fatalf("AssembleFileARM64: %v", err)
|
|
}
|
|
if len(img.Funcs) != 1 {
|
|
t.Fatalf("functions = %d, want 1", len(img.Funcs))
|
|
}
|
|
fn := img.Funcs[0]
|
|
return img, leWords(img.Code[fn.Offset : fn.Offset+fn.Size])
|
|
}
|
|
|
|
// a64BranchWord reports whether w is an unconditional B: op 000101 in bits
|
|
// 31..26, the encoding the flush guards ride.
|
|
func a64BranchWord(w uint32) bool {
|
|
return w>>26 == 0x05
|
|
}
|
|
|
|
// a64BranchTarget decodes a B word's target byte offset from its own pc.
|
|
func a64BranchTarget(w uint32, pc int) int {
|
|
d := int(w & 0x03FFFFFF)
|
|
if d&(1<<25) != 0 {
|
|
d |= ^0x03FFFFFF
|
|
}
|
|
return pc + 4*d
|
|
}
|
|
|
|
// a64LoadLiteral reports whether w is a LDR W/X literal (the pool's loads
|
|
// into REGTMP: word forms 0x18 and 0x58 in the top byte) and decodes its
|
|
// target byte displacement.
|
|
func a64LoadLiteral(w uint32) (int, bool) {
|
|
if w>>24 != 0x18 && w>>24 != 0x58 {
|
|
return 0, false
|
|
}
|
|
d := int(w>>5) & 0x7FFFF
|
|
if d&(1<<18) != 0 {
|
|
d |= ^0x7FFFF
|
|
}
|
|
return d * 4, true
|
|
}
|
|
|
|
// TestArm64PoolFlushStructure assembles the distance-bound kernel and pins
|
|
// the flush structure on gasm's own image: exactly one mid-body branch over
|
|
// a drained segment, no code words inside it, every load literal inside the
|
|
// conservative displacement bound, and the segment's byte range free of line
|
|
// rows: the pool words carry the flushing statement's source line, so the
|
|
// pc-line tables see no delta across them.
|
|
func TestArm64PoolFlushStructure(t *testing.T) {
|
|
const n = 44000 // one flush: the bound arrives at about 43690 statements
|
|
img, words := assemblePooled(t, "", n, "\tRET\n")
|
|
fn := img.Funcs[0]
|
|
|
|
var branches []int
|
|
for i, w := range words {
|
|
if a64BranchWord(w) {
|
|
branches = append(branches, i*4)
|
|
}
|
|
}
|
|
if len(branches) != 1 {
|
|
t.Fatalf("branch words = %d, want exactly the one flush guard", len(branches))
|
|
}
|
|
branchPC := branches[0]
|
|
target := a64BranchTarget(words[branchPC/4], branchPC)
|
|
segStart, segEnd := branchPC+4, target
|
|
if segEnd <= segStart || segEnd%4 != 0 {
|
|
t.Fatalf("flush branch target %d leaves no legal word range after %d", target, branchPC)
|
|
}
|
|
if segEnd+4 > len(words)*4 {
|
|
t.Fatalf("flush branch target %d runs past the image (%d words)", target, len(words))
|
|
}
|
|
// The drained segment holds pool words only: four-byte constants, no
|
|
// branch opcodes, no literal loads.
|
|
for off := segStart; off < segEnd; off += 4 {
|
|
w := words[off/4]
|
|
if a64BranchWord(w) {
|
|
t.Fatalf("word at %d inside the drained segment is a branch: %08x", off, w)
|
|
}
|
|
if _, lit := a64LoadLiteral(w); lit {
|
|
t.Fatalf("word at %d inside the drained segment is a literal load: %08x", off, w)
|
|
}
|
|
}
|
|
|
|
// Every load literal resolves inside the conservative bound and inside
|
|
// the image: the property the flush exists to keep.
|
|
for i, w := range words {
|
|
d, ok := a64LoadLiteral(w)
|
|
if !ok {
|
|
continue
|
|
}
|
|
pc := i * 4
|
|
if pc+d < 0 || pc+d >= len(words)*4 {
|
|
t.Fatalf("literal load at %d targets %d, outside the image", pc, pc+d)
|
|
}
|
|
if d >= a64MaxPCDisp || d <= -a64MaxPCDisp {
|
|
t.Fatalf("literal load at %d displaces %d, outside the conservative bound", pc, d)
|
|
}
|
|
}
|
|
|
|
// The drained words carry no line rows of their own: the toolchain gives
|
|
// them the flushing statement's Pos so the pc-line tables see no delta.
|
|
for _, l := range fn.Lines {
|
|
if l.Offset >= segStart && l.Offset < segEnd {
|
|
t.Fatalf("line row at %d sits inside the drained segment [%d, %d)", l.Offset, segStart, segEnd)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestArm64PoolFlushDifferential assembles the kernel family with gasm and
|
|
// with the installed toolchain and holds the bytes equal, the toolchain's
|
|
// tail alignment padding excepted: one flush, a fall-through end behind the
|
|
// UNDEF guard, two flushes, and a splitting function whose guard prefix and
|
|
// morestack block sit behind a shifted pool.
|
|
func TestArm64PoolFlushDifferential(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("live go tool asm oracle: skipped in -short mode")
|
|
}
|
|
for _, k := range []struct {
|
|
name string
|
|
head string
|
|
n int
|
|
tail string
|
|
}{
|
|
{"one flush, RET end", "", 44000, "\tRET\n"},
|
|
{"one flush, UNDEF end", "", 44000, ""},
|
|
{"two flushes", "", 90000, "\tRET\n"},
|
|
{"split function", "TEXT \u00b7poolmid(SB), $8-0", 44000, "\tRET\n"},
|
|
} {
|
|
t.Run(k.name, func(t *testing.T) {
|
|
if k.head != "" {
|
|
// The split variant spells its own TEXT: the generator's
|
|
// NOSPLIT line must give way to it.
|
|
runPoolFlushCase(t, pooledKernelFor(k.head, k.n, k.tail))
|
|
return
|
|
}
|
|
runPoolFlushCase(t, pooledKernel("", k.n, k.tail))
|
|
})
|
|
}
|
|
}
|
|
|
|
// pooledKernelFor renders the kernel with an explicit TEXT line, the split
|
|
// variant's shape: no NOSPLIT, so the frame forces the stack-split guard.
|
|
func pooledKernelFor(text string, n int, tail string) string {
|
|
var b strings.Builder
|
|
b.WriteString("#include \"textflag.h\"\n")
|
|
b.WriteString(text)
|
|
b.WriteString("\n")
|
|
for i := range n {
|
|
b.WriteString("\tMOVD\tR1, ")
|
|
b.WriteString(decimal(0x2000000 + 8*i + 4))
|
|
b.WriteString("(R2)\n")
|
|
}
|
|
b.WriteString(tail)
|
|
return b.String()
|
|
}
|
|
|
|
// runPoolFlushCase assembles one generated kernel both ways and compares
|
|
// the function's bytes, relocation sites masked, the toolchain's trailing
|
|
// alignment zeros excepted.
|
|
func runPoolFlushCase(t *testing.T, src string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "poolmid_arm64.s")
|
|
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, errs := parser.Parse(path, src)
|
|
if len(errs) > 0 {
|
|
t.Fatalf("parse: %v", errs)
|
|
}
|
|
img, err := AssembleFileARM64(f)
|
|
if err != nil {
|
|
t.Fatalf("AssembleFileARM64: %v", err)
|
|
}
|
|
gt := oracleFuncCode(t, toolAsmObject(t, path, "arm64"))
|
|
byLocal := make(map[string][]byte, len(gt))
|
|
for name, code := range gt {
|
|
if _, after, ok := strings.Cut(name, "."); ok {
|
|
name = after
|
|
}
|
|
byLocal[name] = code
|
|
}
|
|
for _, fn := range img.Funcs {
|
|
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
|
goCode, ok := byLocal[fn.Name]
|
|
if !ok {
|
|
t.Fatalf("%s: not in the oracle output (%d functions)", fn.Name, len(gt))
|
|
}
|
|
goCode = maskCode(append([]byte(nil), goCode...), fn.Relocs)
|
|
cmpLen := min(len(goCode), len(gasmCode))
|
|
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
|
for w := 0; w < cmpLen/4; w++ {
|
|
g := binary.LittleEndian.Uint32(gasmCode[w*4:])
|
|
o := binary.LittleEndian.Uint32(goCode[w*4:])
|
|
if g != o {
|
|
t.Fatalf("%s: word %d (offset %d) differs: gasm %08x go %08x", fn.Name, w, w*4, g, o)
|
|
}
|
|
}
|
|
t.Fatalf("%s: prefixes equal but lengths differ (gasm %d, oracle %d)", fn.Name, len(gasmCode), len(goCode))
|
|
}
|
|
for _, b := range goCode[len(gasmCode):] {
|
|
if b != 0 {
|
|
t.Fatalf("%s: non-zero trailing bytes in the oracle output", fn.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|