feat(lint): nonportable amd64 register name rule
This commit is contained in:
@@ -449,6 +449,11 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
|||||||
out = append(out, checkABI0Args(t)...)
|
out = append(out, checkABI0Args(t)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-portable register spellings (RAX/EAX under go tool asm).
|
||||||
|
if cfg.Arch == arch.AMD64 && !cfg.Disable[CodeNonportableRegister] {
|
||||||
|
out = append(out, scanNonportableRegisters(t)...)
|
||||||
|
}
|
||||||
|
|
||||||
// FUNCDATA / PCDATA structural validation.
|
// FUNCDATA / PCDATA structural validation.
|
||||||
out = append(out, checkFuncdata(t, cfg)...)
|
out = append(out, checkFuncdata(t, cfg)...)
|
||||||
|
|
||||||
|
|||||||
@@ -411,3 +411,28 @@ TEXT ·f(SB), NOSPLIT, $0
|
|||||||
t.Fatalf("byte register in L op must be flagged: %+v", diags)
|
t.Fatalf("byte register in L op must be flagged: %+v", diags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNonportableRegisterName(t *testing.T) {
|
||||||
|
diags := lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
MOVQ RAX, RBX
|
||||||
|
MOVQ EAX, R9
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
// RAX and RBX (and EAX) are gasm aliases of AX/BX; R9 is canonical.
|
||||||
|
if got := codes(diags)[CodeNonportableRegister]; got != 3 {
|
||||||
|
t.Fatalf("want 3 nonportable-register-name, got %d: %+v", got, diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
diags = lintSrc(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
MOVQ AX, BX
|
||||||
|
MOVQ (BX)(R9*4), R12
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
if codes(diags)[CodeNonportableRegister] != 0 {
|
||||||
|
t.Fatalf("canonical names must not be flagged: %+v", diags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// 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"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CodeNonportableRegister flags R/E-prefixed amd64 register spellings.
|
||||||
|
const CodeNonportableRegister = "nonportable-register-name"
|
||||||
|
|
||||||
|
// amd64WordNames are the canonical 64-bit register spellings of go tool asm:
|
||||||
|
// the assembler has no RAX/EAX/RBX forms, and a file using them assembles
|
||||||
|
// under gasm but fails under the toolchain it must ship with.
|
||||||
|
var amd64WordNames = map[string]string{
|
||||||
|
"AX": "AX", "BX": "BX", "CX": "CX", "DX": "DX",
|
||||||
|
"SI": "SI", "DI": "DI", "BP": "BP", "SP": "SP",
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonportableRegister maps a gasm-only register alias to the canonical go
|
||||||
|
// tool asm spelling. R or E followed by a canonical word name is the only
|
||||||
|
// alias family; R8–R15 are already canonical.
|
||||||
|
func nonportableRegister(name string) (string, bool) {
|
||||||
|
up := strings.ToUpper(name)
|
||||||
|
if len(up) != 3 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
var rest string
|
||||||
|
switch up[0] {
|
||||||
|
case 'R', 'E':
|
||||||
|
rest = up[1:]
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if canon, ok := amd64WordNames[rest]; ok && canon != up {
|
||||||
|
return canon, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanNonportableRegisters returns one diagnostic per distinct non-portable
|
||||||
|
// register spelling in t. gasm neither requires nor registers these names
|
||||||
|
// (they parse as bare symbols), and go tool asm rejects them outright, so a
|
||||||
|
// kernel containing one only links through the gasm goobj path, never
|
||||||
|
// through a normal go build.
|
||||||
|
func scanNonportableRegisters(t *ast.Text) []Diagnostic {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var out []Diagnostic
|
||||||
|
report := func(name string, pos token.Position) {
|
||||||
|
canon, ok := nonportableRegister(name)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(name)
|
||||||
|
if seen[key] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
out = append(out, Diagnostic{
|
||||||
|
Pos: pos,
|
||||||
|
Severity: Warning,
|
||||||
|
Code: CodeNonportableRegister,
|
||||||
|
Message: fmt.Sprintf("register %q is a gasm extension; go tool asm spells it %s", name, canon),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 == "" {
|
||||||
|
report(op.Addr.Sym.Name, op.Pos)
|
||||||
|
}
|
||||||
|
if op.Addr.Base != "" {
|
||||||
|
report(op.Addr.Base, op.Pos)
|
||||||
|
}
|
||||||
|
if op.Addr.Index != "" {
|
||||||
|
report(op.Addr.Index, op.Pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user