99 lines
3.1 KiB
Go
99 lines
3.1 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package lint
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
)
|
|
|
|
// checkABI0Args flags TEXT functions whose // func signature declares
|
|
// parameters that are never read from the FP frame. Under ABI0 every
|
|
// argument lives on the stack and is addressed as name+offset(FP); a kernel
|
|
// that never references a parameter's frame slot is almost certainly
|
|
// consuming the caller's register contents instead, which produces
|
|
// value-dependent garbage that only a direct-call differential test (not a
|
|
// pipeline-level fuzz) can observe.
|
|
//
|
|
// The check requires a parseable signature; functions without one, and
|
|
// functions whose parameters are all covered by frame reads, stay silent.
|
|
func checkABI0Args(t *ast.Text) []Diagnostic {
|
|
// An explicit <ABIInternal> TEXT reads its arguments from the register
|
|
// file by declaration (runtime·memmove<ABIInternal> is the canonical
|
|
// example), so the ABI0 frame contract does not apply to it.
|
|
if t.Name != nil && t.Name.ABI != "" {
|
|
return nil
|
|
}
|
|
params, ok := abiParamNames(t.Doc)
|
|
if !ok || len(params) == 0 {
|
|
return nil
|
|
}
|
|
// Collect every FP frame slot the body references, by symbol name (e.g.
|
|
// "text" for text+0(FP)). Frame reads and writes both count: writing a
|
|
// parameter slot is as wrong as ignoring it, but reads dominate, and the
|
|
// distinction is not worth the precision here.
|
|
frameRefs := make(map[string]bool)
|
|
for _, s := range t.Body {
|
|
in, ok := s.(*ast.Instr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, op := range in.Operands {
|
|
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "FP" {
|
|
continue
|
|
}
|
|
frameRefs[strings.ToUpper(op.Addr.Sym.Name)] = true
|
|
}
|
|
}
|
|
if len(frameRefs) == 0 {
|
|
// No FP reference at all: the function ignores the frame entirely.
|
|
return []Diagnostic{{
|
|
Pos: t.Keyword.Pos,
|
|
Severity: Warning,
|
|
Code: CodeABI0RegisterArgs,
|
|
Message: fmt.Sprintf("function %q declares %d parameter(s) but never reads the FP frame; "+
|
|
"ABI0 passes arguments as name+offset(FP), not in registers", t.Name.Name, len(params)),
|
|
}}
|
|
}
|
|
var unread []string
|
|
for _, p := range params {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if !frameSlotCovers(frameRefs, p) {
|
|
unread = append(unread, p)
|
|
}
|
|
}
|
|
if len(unread) == 0 {
|
|
return nil
|
|
}
|
|
return []Diagnostic{{
|
|
Pos: t.Keyword.Pos,
|
|
Severity: Warning,
|
|
Code: CodeABI0RegisterArgs,
|
|
Message: fmt.Sprintf("function %q never reads parameter slot(s) %s from the FP frame; "+
|
|
"suspected register-args port bug (ABI0 arguments arrive as name+offset(FP))",
|
|
t.Name.Name, strings.Join(unread, ", ")),
|
|
}}
|
|
}
|
|
|
|
// frameSlotCovers reports whether any referenced FP slot belongs to the
|
|
// parameter: the slot either spells the parameter name itself or the
|
|
// compound form Go's ABI0 uses for multi-word types (swin_base, swin_len,
|
|
// swin_cap for a parameter named swin).
|
|
func frameSlotCovers(frameRefs map[string]bool, param string) bool {
|
|
p := strings.ToUpper(param)
|
|
if frameRefs[p] {
|
|
return true
|
|
}
|
|
for slot := range frameRefs {
|
|
if strings.HasPrefix(slot, p+"_") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|