feat(lint): flag writes to the platform-reserved register
This commit is contained in:
@@ -81,6 +81,7 @@ const (
|
||||
CodeABI0RegisterArgs = "abi0-register-args"
|
||||
CodeNonportableRegister = "nonportable-register-name"
|
||||
CodeUnencodable = "unencodable-instruction"
|
||||
CodeReservedRegister = "reserved-register-write"
|
||||
)
|
||||
|
||||
// knownTextFlags are the flags recognised by the Go assembler's textflag.h.
|
||||
@@ -469,6 +470,12 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
||||
out = append(out, scanNonportableRegisters(t)...)
|
||||
}
|
||||
|
||||
// Writes to the platform-reserved register (arm64 R18), which the ABI
|
||||
// checks cannot observe at runtime.
|
||||
if cfg.Arch == arch.ARM64 && !hasMacro && !cfg.Disable[CodeReservedRegister] {
|
||||
out = append(out, scanReservedRegisterWrites(t, cfg.Arch)...)
|
||||
}
|
||||
|
||||
// FUNCDATA / PCDATA structural validation.
|
||||
out = append(out, checkFuncdata(t, cfg)...)
|
||||
|
||||
|
||||
@@ -436,3 +436,62 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
t.Fatalf("canonical names must not be flagged: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReservedRegisterWrite(t *testing.T) {
|
||||
// A write to R18, the arm64 platform register, is flagged.
|
||||
diags := lintSrcArch(t, "k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVD $1, R18
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeReservedRegister] != 1 {
|
||||
t.Fatalf("want one reserved-register-write, got %+v", diags)
|
||||
}
|
||||
|
||||
// Reading R18 (as a base address) is legitimate.
|
||||
diags = lintSrcArch(t, "k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVD (R18), R0
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeReservedRegister] != 0 {
|
||||
t.Fatalf("reads must not be flagged: %+v", diags)
|
||||
}
|
||||
|
||||
// Other registers are unaffected.
|
||||
diags = lintSrcArch(t, "k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVD $1, R19
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeReservedRegister] != 0 {
|
||||
t.Fatalf("R19 must not be flagged: %+v", diags)
|
||||
}
|
||||
|
||||
// Macro-using files are exempt: an opaque macro may save and restore R18.
|
||||
diags = lintSrcArch(t, "k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
#define SAVE_R18 MOVD R18, R4
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
SAVE_R18
|
||||
MOVD $1, R18
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeReservedRegister] != 0 {
|
||||
t.Fatalf("macro-using files must be exempt: %+v", diags)
|
||||
}
|
||||
|
||||
// The rule is arm64-only: an amd64 file has no reserved register.
|
||||
diags = lintSrc(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVQ $1, AX
|
||||
RET
|
||||
`)
|
||||
if codes(diags)[CodeReservedRegister] != 0 {
|
||||
t.Fatalf("amd64 must not be flagged: %+v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// reservedRegister returns the platform-reserved general-purpose register of
|
||||
// the architecture, or "" when it has none. arm64 reserves R18 for platform
|
||||
// use: the OS may own it (the Windows TEB, the Darwin el0 TLS), the Go
|
||||
// toolchain never allocates it, and the Go assembler offers no spelling that
|
||||
// even addresses it, so a kernel writing R18 cannot be assembled by the tool
|
||||
// it must ship with. riscv64, loong64 and amd64 reserve no register beyond
|
||||
// the ones the Go ABI fixes, which the register-clobber audit covers.
|
||||
func reservedRegister(a arch.Arch) string {
|
||||
if a == arch.ARM64 {
|
||||
return "R18"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// scanReservedRegisterWrites flags instructions whose destination is the
|
||||
// platform-reserved register. Only the Plan 9 destination (the last
|
||||
// operand) is checked: reads such as MOVD (R18), R0 are legitimate address
|
||||
// uses, and multi-register stores (STP) keep the rule silent rather than
|
||||
// guess. Like the label heuristics, the check is suppressed in macro-using
|
||||
// files, where an opaque macro may save and restore the register.
|
||||
func scanReservedRegisterWrites(t *ast.Text, a arch.Arch) []Diagnostic {
|
||||
res := reservedRegister(a)
|
||||
if res == "" {
|
||||
return nil
|
||||
}
|
||||
var out []Diagnostic
|
||||
for _, s := range t.Body {
|
||||
in, ok := s.(*ast.Instr)
|
||||
if !ok || len(in.Operands) == 0 {
|
||||
continue
|
||||
}
|
||||
dst := in.Operands[dstIndex(in)]
|
||||
if r := gprName(dst, a); r != "" && strings.EqualFold(r, res) {
|
||||
out = append(out, Diagnostic{
|
||||
Pos: dst.Pos,
|
||||
Severity: Warning,
|
||||
Code: CodeReservedRegister,
|
||||
Message: fmt.Sprintf("write to %s, the platform-reserved register: the OS may own it and the Go assembler cannot spell it", res),
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user