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 - **Lint: `unencodable-instruction` rule.** Flags mnemonics the
architecture table knows but the encoder cannot yet emit, at edit time architecture table knows but the encoder cannot yet emit, at edit time
instead of at assembly 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 - **DWARF5 debug sections in ELF output.** All four ELF emitters now emit
`.debug_abbrev`, `.debug_info`, `.debug_line`, and `.debug_line_str` `.debug_abbrev`, `.debug_info`, `.debug_line`, and `.debug_line_str`
sections, enabling `addr2line` and GDB/LLDB source-level debugging, and sections, enabling `addr2line` and GDB/LLDB source-level debugging, and
+7
View File
@@ -81,6 +81,7 @@ const (
CodeABI0RegisterArgs = "abi0-register-args" CodeABI0RegisterArgs = "abi0-register-args"
CodeNonportableRegister = "nonportable-register-name" CodeNonportableRegister = "nonportable-register-name"
CodeUnencodable = "unencodable-instruction" CodeUnencodable = "unencodable-instruction"
CodeReservedRegister = "reserved-register-write"
) )
// knownTextFlags are the flags recognised by the Go assembler's textflag.h. // 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)...) 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. // FUNCDATA / PCDATA structural validation.
out = append(out, checkFuncdata(t, cfg)...) 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) 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
}