176 lines
5.9 KiB
Go
176 lines
5.9 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package lint
|
||
|
||
import "testing"
|
||
|
||
// TestRegisterClobber checks the register-clobber audit is calibrated to the
|
||
// Go ABI (cmd/compile/abi-internal.md), not the platform ABI: Go's
|
||
// stack-based ABI0 — which hand-written assembly uses — has no System V
|
||
// style callee-saved registers, so argument and scratch registers may be
|
||
// clobbered freely. Only the registers the ABI fixes across calls (the
|
||
// frame pointer, the goroutine pointer, OS-reserved registers) are audited.
|
||
func TestRegisterClobber(t *testing.T) {
|
||
// amd64: BX, R12, R13 and R15 are argument/permanent-scratch registers in
|
||
// Go ABI0 — writing them unsaved is legal (a System V calibration would
|
||
// report all of these).
|
||
scratch := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVQ CX, BX\n"+
|
||
"\tXORL R12, R12\n"+
|
||
"\tXORL R13, R13\n"+
|
||
"\tXORL R15, R15\n"+
|
||
"\tRET\n")
|
||
if codes(scratch)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("Go ABI0 scratch registers must not be flagged: %+v", scratch)
|
||
}
|
||
|
||
// amd64: R14 (the goroutine pointer) in a NOSPLIT function without calls
|
||
// is the runtime's own pattern — the ABI0 transition restores it — so it
|
||
// is not flagged.
|
||
leaf := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tXORL R14, R14\n"+
|
||
"\tRET\n")
|
||
if codes(leaf)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("R14 in a NOSPLIT leaf must not be flagged: %+v", leaf)
|
||
}
|
||
|
||
// amd64: R14 in a function that makes a call is a genuine hazard.
|
||
withCall := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tXORL R14, R14\n"+
|
||
"\tCALL ·g(SB)\n"+
|
||
"\tRET\n")
|
||
if codes(withCall)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved R14 with a call should be flagged: %+v", withCall)
|
||
}
|
||
|
||
// amd64: R14 in a non-NOSPLIT function is a hazard regardless of calls.
|
||
split := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), $0\n"+
|
||
"\tMOVQ CX, R14\n"+
|
||
"\tRET\n")
|
||
if codes(split)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved R14 in a non-NOSPLIT function should be flagged: %+v", split)
|
||
}
|
||
|
||
// amd64: R14 saved and restored around the call is preserved.
|
||
saved := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $8\n"+
|
||
"\tPUSHQ R14\n"+
|
||
"\tXORL R14, R14\n"+
|
||
"\tCALL ·g(SB)\n"+
|
||
"\tPOPQ R14\n"+
|
||
"\tRET\n")
|
||
if codes(saved)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("saved/restored R14 must not be flagged: %+v", saved)
|
||
}
|
||
|
||
// amd64: BP maintains the frame chain and is always audited.
|
||
bp := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVQ CX, BP\n"+
|
||
"\tRET\n")
|
||
if codes(bp)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved BP write should be flagged: %+v", bp)
|
||
}
|
||
|
||
// arm64: R20 is scratch; R28 (goroutine pointer) and R18 (OS-reserved)
|
||
// are fixed by the Go ABI.
|
||
armScratch := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVD R0, R20\n"+
|
||
"\tRET\n")
|
||
if codes(armScratch)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("arm64 scratch register must not be flagged: %+v", armScratch)
|
||
}
|
||
armG := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVD R0, R28\n"+
|
||
"\tRET\n")
|
||
if codes(armG)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved arm64 R28 write should be flagged: %+v", armG)
|
||
}
|
||
armReserved := lintSrcArch(t, "t_arm64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVD R0, R18\n"+
|
||
"\tRET\n")
|
||
if codes(armReserved)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("arm64 R18 write should be flagged: %+v", armReserved)
|
||
}
|
||
|
||
// riscv64: X27 holds the goroutine; X5–X7 are scratch.
|
||
riscScratch := lintSrcArch(t, "t_riscv64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOV X5, X6\n"+
|
||
"\tRET\n")
|
||
if codes(riscScratch)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("riscv64 scratch register must not be flagged: %+v", riscScratch)
|
||
}
|
||
riscG := lintSrcArch(t, "t_riscv64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOV X5, X27\n"+
|
||
"\tRET\n")
|
||
if codes(riscG)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved riscv64 X27 write should be flagged: %+v", riscG)
|
||
}
|
||
|
||
// loong64: R22 holds the goroutine; R5–R19 are argument/scratch.
|
||
loongScratch := lintSrcArch(t, "t_loong64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVV R5, R6\n"+
|
||
"\tRET\n")
|
||
if codes(loongScratch)[CodeRegisterClobber] != 0 {
|
||
t.Fatalf("loong64 scratch register must not be flagged: %+v", loongScratch)
|
||
}
|
||
loongG := lintSrcArch(t, "t_loong64.s", "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tMOVV R5, R22\n"+
|
||
"\tRET\n")
|
||
if codes(loongG)[CodeRegisterClobber] != 1 {
|
||
t.Fatalf("unsaved loong64 R22 write should be flagged: %+v", loongG)
|
||
}
|
||
}
|
||
|
||
// TestFuncdata validates the FUNCDATA/PCDATA structural checks.
|
||
func TestFuncdata(t *testing.T) {
|
||
// Well formed: no findings.
|
||
good := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tFUNCDATA $0, gclocals·abc(SB)\n"+
|
||
"\tPCDATA $1, $0\n"+
|
||
"\tRET\n")
|
||
if codes(good)[CodeFuncdata] != 0 {
|
||
t.Fatalf("well-formed FUNCDATA/PCDATA must not be flagged: %+v", good)
|
||
}
|
||
|
||
// FUNCDATA with one operand.
|
||
bad1 := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tFUNCDATA $0\n"+
|
||
"\tRET\n")
|
||
if codes(bad1)[CodeFuncdata] == 0 {
|
||
t.Fatal("FUNCDATA with one operand should be flagged")
|
||
}
|
||
|
||
// PCDATA with a non-immediate value.
|
||
bad2 := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tPCDATA $0, AX\n"+
|
||
"\tRET\n")
|
||
if codes(bad2)[CodeFuncdata] == 0 {
|
||
t.Fatal("PCDATA with a register value should be flagged")
|
||
}
|
||
|
||
// FUNCDATA index out of range.
|
||
bad3 := lintSrc(t, "#include \"textflag.h\"\n"+
|
||
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
||
"\tFUNCDATA $99, gclocals·abc(SB)\n"+
|
||
"\tRET\n")
|
||
if codes(bad3)[CodeFuncdata] == 0 {
|
||
t.Fatal("out-of-range FUNCDATA index should be flagged")
|
||
}
|
||
}
|