feat(lint): abi0 register-args rule and go-asm width model

This commit is contained in:
2026-08-29 13:36:28 +02:00
parent 6699ebd34f
commit d75e6bcae6
5 changed files with 244 additions and 21 deletions
+33 -14
View File
@@ -77,6 +77,7 @@ const (
CodeInvalidFlag = "invalid-textflag"
CodeStackImbalance = "stack-imbalance"
CodeRegisterWidthMismatch = "register-width-mismatch"
CodeABI0RegisterArgs = "abi0-register-args"
)
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
@@ -438,6 +439,16 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
}
}
// ABI0 argument-read check: every parameter the // func signature
// declares must be read from the frame (name+offset(FP)). A kernel that
// declares parameters but never touches their frame slots is almost
// certainly reading its arguments from registers (AX, BX, …), which is
// the classic ABI0 port bug: Go pushes the arguments on the stack and
// the register content is whatever the caller left behind.
if cfg.Arch == arch.AMD64 && !cfg.Disable[CodeABI0RegisterArgs] && !hasMacro && instrCount > 0 {
out = append(out, checkABI0Args(t)...)
}
// FUNCDATA / PCDATA structural validation.
out = append(out, checkFuncdata(t, cfg)...)
@@ -730,9 +741,15 @@ func isSPReg(op *ast.Operand, a arch.Arch) bool {
return false
}
// checkRegisterWidth detects amd64 register-width mismatches: a Q-suffix
// instruction (64-bit) using a 32-bit register, or an L/W/B-suffix
// instruction using a 64-bit register.
// checkRegisterWidth detects amd64 register-width mismatches. The naming
// truth of the Go assembler governs: AX, BX, CX, DX, SI, DI, BP, SP and
// R8–R15 ARE the 64-bit register names (there are no separate EAX/RAX
// spellings in go tool asm), and AL–DH are the byte forms. The width comes
// from the opcode suffix, so an L/W operation over a canonical 64-bit name is
// the normal, correct spelling — flagging it is pure noise on real kernels.
// What remains worth flagging: a Q (64-bit) operation over a narrower spelled
// register (EAX under the gasm alias extension, or a byte form), and byte
// registers in L/W operations.
func checkRegisterWidth(mnem string, ops []*ast.Operand) string {
// Determine expected width from mnemonic suffix.
var expected int // 0=unknown, 8/4/2/1=bytes
@@ -757,28 +774,30 @@ func checkRegisterWidth(mnem string, ops []*ast.Operand) string {
if regWidth == 0 {
continue // not a register or unknown
}
if expected == 8 && regWidth == 4 {
return fmt.Sprintf("%s uses 32-bit register %s (expected 64-bit)", mnem, op.Addr.Sym.Name)
if expected == 8 && regWidth < 8 {
return fmt.Sprintf("%s uses %d-bit register %s (expected 64-bit)", mnem, regWidth*8, op.Addr.Sym.Name)
}
if expected == 4 && regWidth == 8 {
return fmt.Sprintf("%s uses 64-bit register %s (expected 32-bit)", mnem, op.Addr.Sym.Name)
if (expected == 4 || expected == 2) && regWidth == 1 {
return fmt.Sprintf("%s uses 8-bit register %s (expected a wider register)", mnem, op.Addr.Sym.Name)
}
}
return ""
}
// amd64RegWidth returns the width in bytes of an amd64 register name.
// amd64RegWidth returns the width in bytes of an amd64 register name under
// the Go assembler's naming model: the canonical word names (AX…SP, R8–R15)
// are 64-bit, AL–DH are the 8-bit forms, and the R/E-prefixed spellings are
// the gasm alias extension with their intuitive widths.
func amd64RegWidth(name string) int {
switch name {
case "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15":
case "ax", "bx", "cx", "dx", "si", "di", "bp", "sp",
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp":
return 8
case "eax", "ebx", "ecx", "edx", "esi", "edi", "ebp", "esp":
return 4
case "ax", "bx", "cx", "dx", "si", "di", "bp", "sp":
return 2
case "al", "bl", "cl", "dl", "ah", "bh", "ch", "dh":
return 1
case "eax", "ebx", "ecx", "edx", "esi", "edi", "ebp", "esp":
return 4
}
return 0
}