feat(lint): eleven new rules over directives, data and addressing
Assisted-by: GLM 5.3
This commit is contained in:
+143
-10
@@ -10,6 +10,7 @@ package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -84,6 +85,17 @@ const (
|
||||
CodeNonportableRegister = "nonportable-register-name"
|
||||
CodeUnencodable = "unencodable-instruction"
|
||||
CodeReservedRegister = "reserved-register-write"
|
||||
CodeMissingArgSize = "missing-argsize"
|
||||
CodeNoFrameFrameSize = "noframe-frame-size"
|
||||
CodeUnnamedFPRef = "unnamed-fp-reference"
|
||||
CodeHardwareSP = "hardware-sp-addressing"
|
||||
CodeVEXSSEMixing = "vex-sse-mixing"
|
||||
CodeUnnamedResult = "unnamed-result"
|
||||
CodeDataWidth = "data-width"
|
||||
CodeDataValueOverflow = "data-value-overflow"
|
||||
CodeDataStringWidth = "data-string-width"
|
||||
CodeDataNoGlobl = "data-without-globl"
|
||||
CodeDataExceedsGlobl = "data-exceeds-globl"
|
||||
)
|
||||
|
||||
// knownTextFlags are the flags recognised by runtime/textflag.h, plus the
|
||||
@@ -95,6 +107,19 @@ var knownTextFlags = map[string]bool{
|
||||
"TOPFRAME": true, "ABIWRAPPER": true,
|
||||
}
|
||||
|
||||
// textOnlyFlags and dataOnlyFlags carry the placement half of the textflag.h
|
||||
// table: the names that exist on one directive but not the other. NOPROF and
|
||||
// DUPOK apply to both, and every name outside these two sets is either
|
||||
// shared or already reported by the unknown-flag check.
|
||||
var textOnlyFlags = map[string]bool{
|
||||
"NOSPLIT": true, "WRAPPER": true, "NEEDCTXT": true, "NOFRAME": true,
|
||||
"REFLECTED": true, "REFLECTMETHOD": true, "TOPFRAME": true, "ABIWRAPPER": true,
|
||||
}
|
||||
|
||||
var dataOnlyFlags = map[string]bool{
|
||||
"RODATA": true, "NOPTR": true, "TLSBSS": true,
|
||||
}
|
||||
|
||||
// pseudoOps are assembler pseudo-operations that are valid instruction-position
|
||||
// tokens but are not machine instructions and so absent from the arch tables.
|
||||
var pseudoOps = map[string]bool{
|
||||
@@ -165,12 +190,15 @@ func File(f *ast.File, cfg Config) []Diagnostic {
|
||||
for _, d := range f.Decls {
|
||||
var flags []string
|
||||
var pos token.Position
|
||||
var directive string
|
||||
switch dd := d.(type) {
|
||||
case *ast.Text:
|
||||
flags = dd.Flags
|
||||
pos = dd.Keyword.Pos
|
||||
directive = "TEXT"
|
||||
case *ast.Globl:
|
||||
flags = dd.Flags
|
||||
directive = "GLOBL"
|
||||
if dd.Name != nil {
|
||||
pos = dd.Name.Pos
|
||||
}
|
||||
@@ -188,11 +216,28 @@ func File(f *ast.File, cfg Config) []Diagnostic {
|
||||
Code: CodeInvalidFlag,
|
||||
Message: fmt.Sprintf("unknown TEXT/GLOBL flag %q", fl),
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Placement: the textflag.h table binds half the names to
|
||||
// one directive. Both assemblers accept a misplaced name
|
||||
// silently, so the flag simply does nothing.
|
||||
if (directive == "TEXT" && dataOnlyFlags[fl]) ||
|
||||
(directive == "GLOBL" && textOnlyFlags[fl]) {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: pos,
|
||||
Severity: Warning,
|
||||
Code: CodeInvalidFlag,
|
||||
Message: fmt.Sprintf("flag %q does not apply to %s", fl, directive),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DATA and GLOBL structure: widths, values that fit them, and the
|
||||
// GLOBL declaration that sizes each initialised symbol.
|
||||
out = append(out, checkDataDecls(f, cfg)...)
|
||||
|
||||
sortDiagnostics(out)
|
||||
return out
|
||||
}
|
||||
@@ -376,17 +421,33 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
}
|
||||
}
|
||||
|
||||
// Missing RET heuristic. Functions that invoke a macro are skipped: the
|
||||
// macro body (opaque to us) may supply the RET. A TEXT whose symbol is
|
||||
// missing (already reported by the parser) is skipped too.
|
||||
// Missing RET. Two shapes of the same defect, a function that can run
|
||||
// off the end of its body: one with no RET anywhere, and one that has a
|
||||
// RET (or a terminator) somewhere but whose last instruction is not a
|
||||
// terminator, so the tail falls through into whatever follows the TEXT.
|
||||
// Functions that invoke a macro are skipped: the macro body (opaque to
|
||||
// us) may supply the RET. A TEXT whose symbol is missing (already
|
||||
// reported by the parser) is skipped too.
|
||||
if doLabelChecks && !cfg.Disable[CodeMissingRet] && t.Name != nil &&
|
||||
instrCount > 0 && !hasRet && !lastTerminal && !hasMacro {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: t.Keyword.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeMissingRet,
|
||||
Message: fmt.Sprintf("function %q has no RET", t.Name.Name),
|
||||
})
|
||||
instrCount > 0 && !hasMacro {
|
||||
if !trailingTerminal(t, cfg.Arch) {
|
||||
if !hasRet && !lastTerminal {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: t.Keyword.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeMissingRet,
|
||||
Message: fmt.Sprintf("function %q has no RET", t.Name.Name),
|
||||
})
|
||||
} else {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: t.Keyword.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeMissingRet,
|
||||
Message: fmt.Sprintf("function %q can fall off its end: the last instruction is neither "+
|
||||
"RET nor an unconditional branch, so execution continues into the next TEXT", t.Name.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stack imbalance: track SP changes and flag if the net delta at RET
|
||||
@@ -485,9 +546,81 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
// FUNCDATA / PCDATA structural validation.
|
||||
out = append(out, checkFuncdata(t, cfg)...)
|
||||
|
||||
// TEXT directive hygiene: the NOFRAME flag against a non-zero frame, and
|
||||
// a missing argument area where the // func signature implies one.
|
||||
out = append(out, checkTextDirectives(t, cfg)...)
|
||||
|
||||
// Addressing edges: unnamed FP references and hardware stack pointer
|
||||
// offsets, the one-character spellings OPERANDS.md calls the sharpest
|
||||
// edge in the language.
|
||||
out = append(out, scanAddressing(t, cfg)...)
|
||||
|
||||
// Encoding mixing: VEX and legacy SSE in one kernel pay a transition
|
||||
// penalty on every switch (amd64 only).
|
||||
if cfg.Arch == arch.AMD64 && !hasMacro && !cfg.Disable[CodeVEXSSEMixing] {
|
||||
out = append(out, checkVectorEncoding(t, tab, macros)...)
|
||||
}
|
||||
|
||||
// Result naming: the // func signature names its results, but the body
|
||||
// addresses a result slot with the generic ret spelling.
|
||||
if !cfg.Disable[CodeUnnamedResult] {
|
||||
out = append(out, checkResultNaming(t)...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// trailingTerminal reports whether the function's last real instruction ends
|
||||
// control flow: RET, UNDEF, an arch-conditional unconditional branch, or an
|
||||
// architectural trap. Trailing labels, GC annotations and code/data padding
|
||||
// pseudo-ops are skipped, so a terminator followed by padding (the goexit
|
||||
// traceback pattern) still counts. A body of nothing but skipped statements
|
||||
// counts as terminal: there is no fall-through instruction to report.
|
||||
func trailingTerminal(t *ast.Text, a arch.Arch) bool {
|
||||
for _, s := range slices.Backward(t.Body) {
|
||||
in, ok := s.(*ast.Instr)
|
||||
if !ok {
|
||||
continue // a trailing label
|
||||
}
|
||||
upper := strings.ToUpper(in.Mnemonic.Text)
|
||||
switch upper {
|
||||
case "FUNCDATA", "PCDATA", "GO_ARGS", "PCALIGN",
|
||||
"BYTE", "WORD", "LONG", "QUAD", "FLOAT":
|
||||
continue // annotations and padding carry no control flow
|
||||
}
|
||||
return upper == "RET" || upper == "UNDEF" ||
|
||||
isUnconditionalJump(a, upper) || isTrap(a, upper)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isTrap reports whether the mnemonic is an architectural trap: an
|
||||
// instruction whose execution cannot continue (a breakpoint or an
|
||||
// unconditional fault). The runtime ends its abort paths with these.
|
||||
func isTrap(a arch.Arch, upper string) bool {
|
||||
switch a {
|
||||
case arch.AMD64:
|
||||
return upper == "INT"
|
||||
case arch.ARM64:
|
||||
return upper == "BRK"
|
||||
case arch.RISCV:
|
||||
return upper == "EBREAK"
|
||||
case arch.LOONG64:
|
||||
return upper == "BREAK"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasTextFlag reports whether the TEXT carries the named flag.
|
||||
func hasTextFlag(t *ast.Text, name string) bool {
|
||||
for _, f := range t.Flags {
|
||||
if strings.EqualFold(f, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// reachesRuntime reports whether a function can reach the Go runtime: it is
|
||||
// not NOSPLIT (so the stack-split and traceback machinery runs) or it makes a
|
||||
// CALL. Goroutine-pointer registers must survive such functions; a NOSPLIT
|
||||
|
||||
Reference in New Issue
Block a user