feat(lint): add stack-imbalance rule
Assisted-by: MiMo V2.5 Pro
This commit is contained in:
+121
@@ -76,6 +76,7 @@ const (
|
|||||||
CodeFuncdata = "funcdata-pcdata"
|
CodeFuncdata = "funcdata-pcdata"
|
||||||
CodeUnusedLabel = "unused-label"
|
CodeUnusedLabel = "unused-label"
|
||||||
CodeInvalidFlag = "invalid-textflag"
|
CodeInvalidFlag = "invalid-textflag"
|
||||||
|
CodeStackImbalance = "stack-imbalance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
|
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
|
||||||
@@ -350,6 +351,27 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stack imbalance: track SP changes and flag if the net delta at RET
|
||||||
|
// does not match the declared frame size. Only checked for functions
|
||||||
|
// with a declared frame, no macros, and no indirect branches.
|
||||||
|
if doLabelChecks && !cfg.Disable[CodeStackImbalance] && !hasMacro && !hasIndirect {
|
||||||
|
frameSize := int64(0)
|
||||||
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||||
|
frameSize = t.Frame.Imm.Val
|
||||||
|
}
|
||||||
|
if frameSize > 0 {
|
||||||
|
delta := stackDelta(t, cfg.Arch)
|
||||||
|
if delta != 0 && delta != -frameSize {
|
||||||
|
out = append(out, Diagnostic{
|
||||||
|
Pos: t.Keyword.Pos,
|
||||||
|
Severity: Warning,
|
||||||
|
Code: CodeStackImbalance,
|
||||||
|
Message: fmt.Sprintf("function %q has net SP delta %d (frame size %d)", t.Name.Name, delta, frameSize),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ABI conformance: the argument area declared in the TEXT directive should
|
// ABI conformance: the argument area declared in the TEXT directive should
|
||||||
// match the size computed from the // func signature in the doc comment.
|
// match the size computed from the // func signature in the doc comment.
|
||||||
// Only applies to stack-argument (ABI0) functions, which reference their
|
// Only applies to stack-argument (ABI0) functions, which reference their
|
||||||
@@ -594,6 +616,105 @@ func isUnconditionalJump(a arch.Arch, upper string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stackDelta computes the net SP change across a function body.
|
||||||
|
// It tracks PUSH/POP and SUB/ADD on SP. Returns the net delta (negative
|
||||||
|
// means SP decreased, which is the normal direction for stack growth).
|
||||||
|
func stackDelta(t *ast.Text, a arch.Arch) int64 {
|
||||||
|
var delta int64
|
||||||
|
for _, s := range t.Body {
|
||||||
|
in, ok := s.(*ast.Instr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
upper := strings.ToUpper(in.Mnemonic.Text)
|
||||||
|
switch a {
|
||||||
|
case arch.AMD64:
|
||||||
|
switch upper {
|
||||||
|
case "PUSHQ", "PUSHL", "PUSHW":
|
||||||
|
delta -= 8
|
||||||
|
case "POPQ", "POPL", "POPW":
|
||||||
|
delta += 8
|
||||||
|
case "SUBQ", "SUBL":
|
||||||
|
if len(in.Operands) >= 2 && isSPReg(in.Operands[1], a) {
|
||||||
|
if in.Operands[0].Imm.HasVal {
|
||||||
|
delta -= in.Operands[0].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ADDQ", "ADDL":
|
||||||
|
if len(in.Operands) >= 2 && isSPReg(in.Operands[1], a) {
|
||||||
|
if in.Operands[0].Imm.HasVal {
|
||||||
|
delta += in.Operands[0].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case arch.ARM64:
|
||||||
|
switch upper {
|
||||||
|
case "STP":
|
||||||
|
// STP with pre-index: STP Xt1, Xt2, [SP, #imm]!
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
// Could be pre-index decrement; skip for simplicity.
|
||||||
|
}
|
||||||
|
case "SUB":
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
if in.Operands[1].Imm.HasVal {
|
||||||
|
delta -= in.Operands[1].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ADD":
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
if in.Operands[1].Imm.HasVal {
|
||||||
|
delta += in.Operands[1].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case arch.RISCV:
|
||||||
|
switch upper {
|
||||||
|
case "ADDI":
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
if in.Operands[1].Imm.HasVal {
|
||||||
|
delta += in.Operands[1].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case arch.LOONG64:
|
||||||
|
switch upper {
|
||||||
|
case "ADDI.D", "ADDI.W":
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
if in.Operands[1].Imm.HasVal {
|
||||||
|
delta += in.Operands[1].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ADD.D", "ADD.W":
|
||||||
|
if len(in.Operands) >= 3 && isSPReg(in.Operands[2], a) {
|
||||||
|
if in.Operands[1].Imm.HasVal {
|
||||||
|
delta += in.Operands[1].Imm.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSPReg reports whether the operand is the stack pointer register.
|
||||||
|
func isSPReg(op *ast.Operand, a arch.Arch) bool {
|
||||||
|
if op == nil || op.Addr.Sym == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name := strings.ToUpper(op.Addr.Sym.Name)
|
||||||
|
switch a {
|
||||||
|
case arch.AMD64:
|
||||||
|
return name == "RSP" || name == "ESP" || name == "SP"
|
||||||
|
case arch.ARM64:
|
||||||
|
return name == "R31" || name == "SP"
|
||||||
|
case arch.RISCV:
|
||||||
|
return name == "X2" || name == "SP"
|
||||||
|
case arch.LOONG64:
|
||||||
|
return name == "R3" || name == "SP"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func countRange(min, max int) string {
|
func countRange(min, max int) string {
|
||||||
if min == max {
|
if min == max {
|
||||||
return fmt.Sprintf("%d operand(s)", min)
|
return fmt.Sprintf("%d operand(s)", min)
|
||||||
|
|||||||
@@ -282,3 +282,30 @@ TEXT ·f(SB), NOSPLIT|NOFRAME|DUPOK, $0
|
|||||||
t.Fatalf("valid flags must not be flagged: %+v", diags)
|
t.Fatalf("valid flags must not be flagged: %+v", diags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStackImbalance(t *testing.T) {
|
||||||
|
// Function with frame size 16 but only SUB 8, SP — imbalance.
|
||||||
|
diags := lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $16-0
|
||||||
|
SUBQ $8, SP
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeStackImbalance] != 1 {
|
||||||
|
t.Fatalf("want one stack-imbalance, got %+v", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackBalanced(t *testing.T) {
|
||||||
|
// Function with frame size 16 and matching SUB/ADD — balanced.
|
||||||
|
diags := lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $16-0
|
||||||
|
SUBQ $16, SP
|
||||||
|
ADDQ $16, SP
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeStackImbalance] != 0 {
|
||||||
|
t.Fatalf("balanced stack must not be flagged: %+v", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user