feat(lint): flag writes to the platform-reserved register
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)...)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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