feat(lint): flag writes to the platform-reserved register

This commit is contained in:
2026-08-30 21:42:12 +02:00
parent 62f6fb4faf
commit 75bd83fd52
4 changed files with 126 additions and 0 deletions
+4
View File
@@ -84,6 +84,10 @@ Unreleased changes on the `development` branch.
- **Lint: `unencodable-instruction` rule.** Flags mnemonics the
architecture table knows but the encoder cannot yet emit, at edit time
instead of at assembly time.
- **Lint: `reserved-register-write` rule.** Flags writes to arm64 R18,
the platform-reserved register the ABI checks cannot observe at runtime
and the Go assembler cannot even spell. Reads and macro-using files
are exempt.
- **DWARF5 debug sections in ELF output.** All four ELF emitters now emit
`.debug_abbrev`, `.debug_info`, `.debug_line`, and `.debug_line_str`
sections, enabling `addr2line` and GDB/LLDB source-level debugging, and
+7
View File
@@ -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)...)
+59
View File
@@ -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)
}
}
+56
View File
@@ -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
}