Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e02918c17b | ||
|
|
02a6359c1f | ||
|
|
b4c1e133c0 | ||
|
|
1107928870 | ||
|
|
bc4ac93fd9 | ||
|
|
c1bca7ce7e | ||
|
|
fefb76beb9 | ||
|
|
42bc1669d7 | ||
|
|
69dcbec8ef | ||
|
|
f405cea5bc | ||
|
|
f57377abb9 | ||
|
|
dd1782c538 | ||
|
|
17cc49fee4 | ||
|
|
bafb2fd130 | ||
|
|
2f679326c2 | ||
|
|
96f2dd65b4 | ||
|
|
ca887d3927 | ||
|
|
6570709226 | ||
|
|
9a5217d9c1 | ||
|
|
4d01bb3ecf | ||
|
|
332c63e440 | ||
|
|
b306c210c6 | ||
|
|
b9015e1c2e |
@@ -0,0 +1,42 @@
|
||||
# FreeBSD compile gates. Dispatched by hand, never on a push.
|
||||
#
|
||||
# The debugger's ptrace surface and the JIT substrate are the two
|
||||
# FreeBSD-portable layers the tree carries; the forge has no FreeBSD runner,
|
||||
# so they can only be compile-gated, and three foreign-GOOS builds of the
|
||||
# whole module are minutes of one-core work the push pipeline's budget cannot
|
||||
# carry. The push pipeline stays fast and light; this workflow is the
|
||||
# deliberate run, before a release or after touching the ported layers.
|
||||
# Running the ptrace suite itself needs real FreeBSD hardware.
|
||||
#
|
||||
# A dispatched workflow takes no concurrency block: it is one deliberate run.
|
||||
name: FreeBSD build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# One core: parallelism buys no speed here and costs memory the box does not have.
|
||||
GOFLAGS: -p=1
|
||||
GOMAXPROCS: "2"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: fedora
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
# The module is the source of truth for the version, so it cannot drift.
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: FreeBSD build (amd64)
|
||||
run: GOOS=freebsd GOARCH=amd64 go build ./...
|
||||
|
||||
- name: FreeBSD build (arm64)
|
||||
run: GOOS=freebsd GOARCH=arm64 go build ./...
|
||||
|
||||
- name: FreeBSD build (riscv64)
|
||||
run: GOOS=freebsd GOARCH=riscv64 go build ./...
|
||||
+19
-11
@@ -6,6 +6,11 @@
|
||||
# everything runs in one job. Extra jobs would duplicate the checkout, the Go setup and
|
||||
# the dependency download three times without buying any parallelism.
|
||||
#
|
||||
# The budget is part of the contract: a push run is fast and light, about two minutes,
|
||||
# and nothing that cannot run natively on the runner belongs here. The FreeBSD compile
|
||||
# gates live in freebsd.yml behind workflow_dispatch for that reason; the GOOBJ link
|
||||
# parity campaign is an opt-in local verification (just link-parity).
|
||||
#
|
||||
# Every step is one command, so the step that fails is the gate that failed, and no shell
|
||||
# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and
|
||||
# not Python: Perl behaves the same on both runner images, there is no bashism to trip over
|
||||
@@ -46,10 +51,9 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Install Perl
|
||||
# The runner images are minimal and Perl is not guaranteed. The install is a
|
||||
# no-op where it is already present; drop this step once verified on the box.
|
||||
run: dnf install -y perl
|
||||
# No Install Perl step: the fedora image carries perl (verified by run
|
||||
# 76: the install degraded into a package upgrade costing ~50 s), and a
|
||||
# dnf on the push path is network work the budget does not need.
|
||||
|
||||
# The steps follow the `gates` order of the justfile contract: build, format,
|
||||
# vet, test. The vet gate is go vet and go fix -diff, two steps here.
|
||||
@@ -82,7 +86,11 @@ jobs:
|
||||
# command, so the floor is the same number everywhere. ./verify/... carries the
|
||||
# live oracle-parity comparison against `go tool asm` (the TestGroundTruth
|
||||
# suites); the runner's Go setup provides both the tool and GOROOT.
|
||||
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||
# -short skips the deliberate-run categories inside the suites (the live
|
||||
# ptrace sessions above all): they need the machine to themselves and a
|
||||
# starved single-core runner turns each into a timeout the budget cannot
|
||||
# carry. The local `just test` gate runs everything, in full.
|
||||
run: go test -short -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||
|
||||
- name: Tests outside the coverage set
|
||||
# The CLI and the debugger sit outside `packages` because a thin main and a
|
||||
@@ -90,12 +98,12 @@ jobs:
|
||||
# shipped surfaces: the command exit codes, the manual pages against the
|
||||
# binary's own help, and the debugger's architecture-neutral units. They run
|
||||
# here so the floor stays a product measure and nothing is left untested.
|
||||
run: go test -count=1 -timeout 10m ./cmd/... ./debug/...
|
||||
|
||||
- name: Oracle parity
|
||||
# Re-run the live go-tool-asm comparison as its own step so that a parity
|
||||
# regression names the gate that failed instead of hiding inside the suite.
|
||||
run: go test -count=1 -timeout 10m -run 'TestGroundTruth' ./verify/...
|
||||
# -short skips the debugger's live ptrace sessions, the deliberate-run
|
||||
# category the runner cannot starve-proof. The live go-tool-asm oracle
|
||||
# comparison (TestGroundTruth in ./verify/...) runs inside the coverage
|
||||
# sweep above; it is not re-run as its own step, because every second on
|
||||
# this box is budget.
|
||||
run: go test -short -count=1 -timeout 10m ./cmd/... ./debug/...
|
||||
|
||||
- name: Coverage floor
|
||||
run: |
|
||||
|
||||
+110
-3
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to gasm-devkit are documented here.
|
||||
All notable changes to gasm-sdk are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- **The FreeBSD port of the debugger.** `gasm debug` runs on FreeBSD on
|
||||
amd64, arm64 and riscv64 with the same interactive surface as on Linux:
|
||||
breakpoints, hardware watchpoints (x86 debug registers, the arm64 debug
|
||||
register file), single-stepping, register and memory access, all behind
|
||||
the kernel's own ptrace requests, with tracee memory through `PT_IO` and
|
||||
stop reports through `PT_LWPINFO`. The JIT substrate maps executable
|
||||
memory through `golang.org/x/sys/unix`, so `verify` builds on FreeBSD
|
||||
too. The pipeline compile-gates all three architectures; live
|
||||
validation awaits a FreeBSD machine.
|
||||
- **Workspace-wide navigation in the language server.** `gasm lsp` indexes
|
||||
the `.s` files under the workspace root beyond the documents the editor
|
||||
has open, so go-to-definition, find references and workspace symbol search
|
||||
@@ -20,6 +29,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
last one in the file, and the `abi-argsize` warning offers to set the
|
||||
TEXT argument area to the size the `// func` signature implies, computed
|
||||
by the new `lint.ExpectedArgSize`.
|
||||
- **Eleven new lint rules over the directives, the data section and the
|
||||
sharpest addressing edges.** `missing-argsize` flags a TEXT that
|
||||
declares no argument area its `// func` signature implies;
|
||||
`noframe-frame-size` a NOFRAME with a positive frame;
|
||||
`unnamed-fp-reference` a nameless `0(FP)`, which both assemblers
|
||||
reject; `hardware-sp-addressing` a negative offset off the hardware SP
|
||||
rather than the virtual frame; `vex-sse-mixing` a kernel that mixes VEX
|
||||
and legacy SSE spellings and pays the transition penalty;
|
||||
`unnamed-result` a `ret+N(FP)` the signature names; `data-width`,
|
||||
`data-value-overflow`, `data-string-width`, `data-without-globl` and
|
||||
`data-exceeds-globl` police the DATA width against its value type and
|
||||
the GLOBL size behind it. `missing-ret` now also flags a function
|
||||
whose tail can fall off its end even though a RET sits somewhere in the
|
||||
body, and `invalid-textflag` also reports a flag misplaced between TEXT
|
||||
and GLOBL.
|
||||
- **Hover documentation and wider completions in the language server.**
|
||||
Hovering a directive or pseudo-operation (TEXT, DATA, GLOBL, PCALIGN,
|
||||
FUNCDATA, PCDATA, the BYTE family) shows its grammar and rules in
|
||||
preference to the empty instruction-table entry, and completion offers
|
||||
those names beside the instruction set. The `missing-argsize` warning
|
||||
carries a quick fix that declares the argument area the signature
|
||||
implies (`$0` becomes `$0-16`).
|
||||
- **The GOOBJ link parity gate.** A regression test assembles a kernel
|
||||
per architecture through `gasm asm --format goobj`, substitutes the
|
||||
object into a real `go build`'s package archive, proves the archive
|
||||
carries it byte for byte and re-links with cmd/link on amd64, arm64,
|
||||
riscv64 and loong64. The binaries run, natively on amd64 and under
|
||||
qemu-user on arm64 and riscv64, and their output must match the
|
||||
toolchain-built baseline; loong64 is link-only. The pipeline installs
|
||||
qemu-user and runs the gate on every push.
|
||||
- **The amd64 and loong64 encoders close four more corpus files.** The
|
||||
whole-tree measure moves to 272 of 322 (84.5 %): amd64 gains the
|
||||
one-operand IMUL, the SSE compare family (CMPPD, CMPPS, CMPSS), RETFL,
|
||||
the LOOP family, the MMX register bank with its bank-crossing moves,
|
||||
MOVNTDQ, the CR and DR register moves, PUSH and POP of FS and GS, the
|
||||
`(TLS)` pseudo-base, the wait and cache controls (CLWB, CLDEMOTE,
|
||||
TPAUSE, UMONITOR, UMWAIT, RDPID, ENDBR64), indirect branches with the
|
||||
star spelling (`JMP *(R12)(R13*4)`), `RET sym(SB)` as the tail jump,
|
||||
the colon shift spelling (`SHLL CX, R11:AX`) and the EVEX and VEX forms
|
||||
of the rounds, AES key assist, string compares, extracts, blends and
|
||||
permutes; loong64 gains the acquire and release pair (LLACQ, SCREL,
|
||||
with the vector widths), the VMOVQ and XVMOVQ lane forms and the BYTE
|
||||
literal-data escape hatch the other architectures already take.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -30,8 +82,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
instead of failing it. The headline now reads "assemble for every
|
||||
applicable target": every real-code GOROOT assembly file, the tree
|
||||
without testdata, assembles for all four architectures (250 of 250,
|
||||
100 %); over the whole tree including testdata the measure is 271 of
|
||||
322 (84.2 %).
|
||||
100 %); over the whole tree including testdata the measure is 272 of
|
||||
322 (84.5 %).
|
||||
- **The module moves to `sourcedock.dev/petrbalvin/gasm-sdk`.** The
|
||||
repository and the module rename together with the product, now the
|
||||
GAsm Software Development Kit. Fresh installs become
|
||||
`go install sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm@latest`, and
|
||||
installs pinned to the old `gasm-sdk` path stop resolving once the
|
||||
repository takes the new name: reinstall from the new path. The
|
||||
binary stays `gasm`.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -50,6 +109,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
rejected; the toolchain picks the 16-bit lane from the 64-bit bit
|
||||
pattern, so the encoder now does the same, and a zero immediate is
|
||||
rejected where the toolchain rejects it.
|
||||
- **NOPTR data emits its own symbol kind.** A GLOBL with NOPTR was
|
||||
emitted as plain SDATA, the kind the linker holds to its Go type
|
||||
information requirement, so every gasm object carrying runtime-shaped
|
||||
data (`GLOBL ·x(SB), NOPTR, ...`) died in cmd/link with "missing Go
|
||||
type information". NOPTR data is now SNOPTRDATA, the toolchain's kind
|
||||
for pointer-free globals, and RODATA still wins where both flags
|
||||
appear, exactly as the toolchain chooses. The three end-to-end link
|
||||
tests that should have caught this substituted the gasm object after
|
||||
the package archive was already packed, so they passed vacuously; they
|
||||
now substitute inside the archive, prove the substitution byte for
|
||||
byte and re-link.
|
||||
- **Latent encoder divergences against the toolchain, found by the
|
||||
whole-file differential harness.** On loong64, `MOVx $off(reg)` lost
|
||||
its base register, SCQ swapped its operand fields, the vector lane
|
||||
inserts did not scale offsets by the element width and two immediate
|
||||
opcodes were mistyped; on amd64, NOP with operands encoded 0x90 where
|
||||
the toolchain emits nothing at all, the VEX gather length bit ignored
|
||||
the VSIB index width, four permute and extract families were pushed
|
||||
into EVEX where the toolchain stays VEX, and a reference to a static
|
||||
symbol no GLOBL defines failed where the toolchain defers it to the
|
||||
linker as an external relocation.
|
||||
- **Seven front-end defects found by fuzzing.** The formatter swallowed
|
||||
the statement after a leading block comment (`/* head */ MOVQ AX, BX`
|
||||
formatted to the comment alone), dropped trailing tokens after an
|
||||
`#include` header, and trimmed the trailing whitespace inside a string
|
||||
literal; the parser peeled only one label of a stacked pair (`a: b:`
|
||||
parsed differently than it formatted); deeply nested parentheses in an
|
||||
immediate overflowed the stack through the constant folder, which now
|
||||
stops at a bounded depth and falls back to the ordinary operand paths;
|
||||
macro expansion is linear in the invocation count instead of
|
||||
quadratic; and amplifying macros (the billion-laughs shape) stop at a
|
||||
work budget sized by the line and the macro table, reported as a
|
||||
diagnostic instead of running for hours.
|
||||
- **The debugger handles the edges its tests now reach.** Memory reads
|
||||
and writes cover exactly the requested bytes, so a request ending in
|
||||
the last page of a mapping no longer fails on the unmapped page behind
|
||||
it; disassembly shrinks its instruction window at a mapping's end
|
||||
instead of failing; a debuggee that dies before signalling readiness
|
||||
writes a failure notice the launcher reads, so launch fails fast with
|
||||
the reason instead of after the whole poll (and the poll no longer
|
||||
waits on the child, which could consume the SIGSTOP park and hang the
|
||||
launch); amd64 watchpoints acknowledge the sticky DR6 hit bits and
|
||||
clear the address register on release; the breakpoint listing is
|
||||
ordered by address so its numbering is stable; the REPL rejects bad
|
||||
counts, sizes and watchpoint types instead of silently guessing,
|
||||
reports stops on signals during stepping, and a breakpoint-class trap
|
||||
that matches no breakpoint and leaves the PC in place surfaces instead
|
||||
of spinning the continue loop and the coverage run forever.
|
||||
|
||||
## [0.35.0] - 2026-09-22
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||
Contributions to **gasm-devkit** are governed by the Contributor terms
|
||||
Contributions to **gasm-sdk** are governed by the Contributor terms
|
||||
below; submitting one means you accept them.
|
||||
|
||||
## Contributor terms
|
||||
@@ -29,8 +29,8 @@ compiler (gcc), because `just gates` includes `just race` and the race
|
||||
detector needs cgo.
|
||||
|
||||
```sh
|
||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||
cd gasm-devkit
|
||||
git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git
|
||||
cd gasm-sdk
|
||||
just build
|
||||
just gates
|
||||
```
|
||||
@@ -126,7 +126,7 @@ tag, where it would double the time and the memory a shared runner cannot spare.
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-devkit/issues` with the
|
||||
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-sdk/issues` with the
|
||||
version, the operating system and architecture, the exact command, the full output,
|
||||
and the expected against the actual behaviour.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plan 9 assembly tooling, inside and outside Go
|
||||
# GAsm: Software Development Kit for Plan 9 Assembly
|
||||
|
||||
> **Warning: this is an experiment.** gasm-devkit is under active
|
||||
> **Warning: this is an experiment.** gasm-sdk is under active
|
||||
> development and is not stable. The version is 0.x.x: commands, flags,
|
||||
> output formats and behaviour can change without warning at any time.
|
||||
> A 1.0.0 release is light years away. Nothing in this document is a
|
||||
@@ -13,7 +13,7 @@
|
||||
there is no formatter, no linter and no debugger for `.s` files, and no
|
||||
assembler that works without a Go installation. Developers write
|
||||
assembly blind, validate it by benchmark, and debug it by print
|
||||
statement. gasm-devkit is the missing toolkit: a single, self-contained
|
||||
statement. gasm-sdk is the missing toolkit: a single, self-contained
|
||||
binary, `gasm`, that serves both purposes.
|
||||
|
||||
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
||||
@@ -58,7 +58,7 @@ Plan 9 (Go): MOVQ AX, total-16(SP)
|
||||
|
||||
The same lines, but only one of them tells you what the number is for.
|
||||
The syntax is uppercase, regular and boring, which is the highest
|
||||
compliment a language for machine code can earn. gasm-devkit exists
|
||||
compliment a language for machine code can earn. gasm-sdk exists
|
||||
to give that syntax the tooling it deserves.
|
||||
|
||||
## Features
|
||||
@@ -94,7 +94,8 @@ to give that syntax the tooling it deserves.
|
||||
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
|
||||
breakpoints (optionally conditional), hardware watchpoints, register and
|
||||
memory inspection, and headless script runs that report instruction and
|
||||
label coverage.
|
||||
label coverage; it runs on Linux (all four architectures) and FreeBSD
|
||||
(amd64, arm64, riscv64).
|
||||
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
|
||||
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
||||
find references, rename, formatting, inlay hints, code actions, signature
|
||||
@@ -150,7 +151,7 @@ actually been executed.
|
||||
|---|---|---|
|
||||
| Encoding: byte-for-byte against `go tool asm` | native hardware | native hardware (the toolchain cross-assembles any GOARCH on any host) |
|
||||
| Execution: JIT calls, ABI checks, differential fuzzing | native hardware | qemu-user emulation |
|
||||
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more |
|
||||
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more. FreeBSD (amd64, arm64, riscv64) is in the same position: the port compiles behind the cross-build gate and its integration test is ready, but no FreeBSD machine has executed it |
|
||||
|
||||
Consequences, stated plainly. An emulator is a model of a CPU, not the
|
||||
CPU: instruction semantics are implemented in software and can differ
|
||||
@@ -220,9 +221,11 @@ The plan, in the order it is being worked:
|
||||
toolchain itself does not support; through ELF, Plan 9 assembly becomes
|
||||
usable outside Go entirely.
|
||||
- **Platforms: Linux and FreeBSD.** Linux is supported today on all four
|
||||
architectures and is where the binary builds. FreeBSD follows: the
|
||||
JIT's executable-memory mapping and the ptrace debugger layer are the
|
||||
two pieces of porting work. Other unix systems may follow those two.
|
||||
architectures and is where the binary builds. FreeBSD follows on amd64,
|
||||
arm64 and riscv64: the JIT's executable-memory mapping and the ptrace
|
||||
debugger layer are ported (the debugger's live validation awaits a
|
||||
FreeBSD machine, as the validation status states). Other unix systems
|
||||
may follow those two.
|
||||
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
||||
No others are planned.
|
||||
|
||||
@@ -230,11 +233,11 @@ The plan, in the order it is being worked:
|
||||
|
||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
||||
linux/loong64 are on the
|
||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-sdk/releases).
|
||||
From source (Go 1.27.1):
|
||||
|
||||
```sh
|
||||
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
||||
go install sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm@latest
|
||||
```
|
||||
|
||||
Or from a repository checkout:
|
||||
|
||||
+7
-7
@@ -5,7 +5,7 @@
|
||||
// toolchain's own assembler source. Go's Plan 9 assembler defines the exact,
|
||||
// complete set of mnemonics it accepts for each architecture in
|
||||
// $GOROOT/src/cmd/internal/obj/<arch>/anames.go; this tool extracts those
|
||||
// names so gasm-devkit supports every instruction the real assembler does,
|
||||
// names so gasm-sdk supports every instruction the real assembler does,
|
||||
// with no hand-maintained (and therefore inevitably incomplete) lists.
|
||||
//
|
||||
// The same data feeds the generated instruction appendices of the assembly
|
||||
@@ -31,11 +31,11 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
)
|
||||
|
||||
// archDirs maps a gasm-devkit architecture name to its obj sub-directory.
|
||||
// archDirs maps a gasm-sdk architecture name to its obj sub-directory.
|
||||
var archDirs = []struct {
|
||||
arch string
|
||||
sub string
|
||||
@@ -119,7 +119,7 @@ func filterCommon(names []string) []string {
|
||||
// writeCommon emits arch/common_gen.go.
|
||||
func writeCommon(names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
@@ -190,7 +190,7 @@ func stringLit(elt ast.Expr) string {
|
||||
// writeGen emits arch/<arch>_gen.go.
|
||||
func writeGen(arch, sub string, names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
@@ -216,7 +216,7 @@ func writeDocPage(a arch.Arch, title, file, anames, version string, encodable bo
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("# " + title + ": instruction inventory\n\n")
|
||||
b.WriteString("Generated by gasm-devkit's `_gen` from the Go toolchain's instruction table\n")
|
||||
b.WriteString("Generated by gasm-sdk's `_gen` from the Go toolchain's instruction table\n")
|
||||
b.WriteString("(`" + anames + "`, " + version + "); DO NOT EDIT. This page lists every mnemonic\n")
|
||||
b.WriteString("`go tool asm` accepts on this target, which is the upper bound of the\n")
|
||||
b.WriteString("language on it: a name absent here is not an instruction of the target,\n")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/x86/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/arm64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/util.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/loong64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/riscv/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+15
-66
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
||||
@@ -178,26 +178,10 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_arm64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
st := parseBuildLog(t, buildLog, "main_arm64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
// Expand $WORK in the object path.
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
|
||||
// Read the toolchain-produced object and assemble the same source with gasm.
|
||||
// Assemble the same source with gasm and substitute the object.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -210,30 +194,16 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("a64link", "main_arm64.s")
|
||||
// The package path is "main", the prefix the Go code's references carry.
|
||||
gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
|
||||
// Replace the toolchain-produced object with gasm's.
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
|
||||
// Re-link.
|
||||
if linkLine == "" {
|
||||
t.Skip("could not find link command in build log")
|
||||
}
|
||||
// Expand $WORK in the link command.
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
linkCmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"),
|
||||
gasmObj, "GOARCH=arm64")
|
||||
|
||||
// Verify the binary exists and contains the symbol.
|
||||
binPath := filepath.Join(dir, "prog")
|
||||
binPath := filepath.Join(dir, "prog2")
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
t.Fatalf("binary not found: %v", err)
|
||||
}
|
||||
@@ -296,23 +266,8 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_arm64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" || linkLine == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
st := parseBuildLog(t, buildLog, "main_arm64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
@@ -326,19 +281,13 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("a64dlink", "main_arm64.s")
|
||||
gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
linkCmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
binData, err := os.ReadFile(filepath.Join(dir, "prog"))
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"),
|
||||
gasmObj, "GOARCH=arm64")
|
||||
binData, err := os.ReadFile(filepath.Join(dir, "prog2"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
||||
@@ -4003,6 +4003,7 @@ func AssembleFileARM64(f *ast.File) (*Image, error) {
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
func TestArm64LDRSTREncoding(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ package asm
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// parseArm64File is a helper assembling one arm64 source file.
|
||||
|
||||
+215
-18
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Assemble encodes the body of a TEXT function into x86-64 machine code,
|
||||
@@ -808,17 +808,43 @@ func isJumpMnemonic(mnem string) bool {
|
||||
if mnem == "JMP" || mnem == "CALL" {
|
||||
return true
|
||||
}
|
||||
if isLoopMnemonic(mnem) {
|
||||
return true
|
||||
}
|
||||
_, ok := condCode(mnem)
|
||||
return ok
|
||||
}
|
||||
|
||||
// isLoopMnemonic reports the LOOP family, rel8 alone (E0-E2).
|
||||
func isLoopMnemonic(mnem string) bool {
|
||||
switch mnem {
|
||||
case "LOOP", "LOOPE", "LOOPNE":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loopOpcode maps the LOOP family to its E0-E2 opcode.
|
||||
func loopOpcode(mnem string) byte {
|
||||
switch mnem {
|
||||
case "LOOPE":
|
||||
return 0xE1
|
||||
case "LOOPNE":
|
||||
return 0xE0
|
||||
}
|
||||
return 0xE2
|
||||
}
|
||||
|
||||
// jumpSize returns the length of a jump instruction in the requested form:
|
||||
// short (rel8) where available, otherwise the rel32 form. CALL is always
|
||||
// rel32.
|
||||
// rel32; the LOOP family is rel8 alone.
|
||||
func jumpSize(mnem string, long bool) int {
|
||||
if mnem == "CALL" {
|
||||
return 5 // opcode + rel32
|
||||
}
|
||||
if isLoopMnemonic(mnem) {
|
||||
return 2 // opcode + rel8, the only form
|
||||
}
|
||||
if !long {
|
||||
return 2 // opcode + rel8
|
||||
}
|
||||
@@ -945,6 +971,16 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
|
||||
if (mnemUpper == "MOVQ" || mnemUpper == "MOVL") && len(s.Operands) == 2 && isBareTLS(s.Operands[0]) {
|
||||
return encodeTLSBaseLoad(s, fi, link)
|
||||
}
|
||||
// The old paired-register shift spelling, SHLL CX, R11:AX (a colon
|
||||
// between the two registers), is the toolchain's SHLD family: SHLDL CL,
|
||||
// AX, R11 with the count register first, the paired source in the reg
|
||||
// field and the pair's head in r/m.
|
||||
if code, ps, err := encodeColonShift(s, mnemUpper, fi, link); code != nil || err != nil {
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return code, ps, nil, nil
|
||||
}
|
||||
_, size := splitSize(mnemUpper)
|
||||
if size == 0 {
|
||||
size = 8
|
||||
@@ -1051,6 +1087,66 @@ func encodeBookkeeping(upper string, s *ast.Instr) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// encodeColonShift encodes the paired-register shift spellings, SHLx CX,
|
||||
// dst:src: the toolchain reads them as the SHLD family (double-precision
|
||||
// shift by CL), reg = the paired source, r/m = the pair's head. The second
|
||||
// operand's raw text carries the colon; ok reports the spelling was found.
|
||||
func encodeColonShift(s *ast.Instr, mnemUpper string, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, error) {
|
||||
base, _ := strings.CutPrefix(mnemUpper, "SHL")
|
||||
if base == mnemUpper || len(s.Operands) != 2 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
_, size := splitSize(mnemUpper)
|
||||
raw := strings.ReplaceAll(s.Operands[1].Raw, " ", "")
|
||||
head, tail, ok := strings.Cut(raw, ":")
|
||||
if !ok || head == "" || tail == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
headReg, ok1 := ParseReg(head)
|
||||
srcReg, ok2 := ParseReg(tail)
|
||||
if !ok1 || !ok2 {
|
||||
return nil, nil, fmt.Errorf("%s: invalid paired register %q", mnemUpper, s.Operands[1].Raw)
|
||||
}
|
||||
cnt, err := operandFromAST(mnemUpper, s.Operands[0], size, fi, link)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cntReg, ok := cnt.(Reg)
|
||||
if !ok || cntReg.idx != 1 {
|
||||
return nil, nil, fmt.Errorf("%s: the paired-register form counts in CL", mnemUpper)
|
||||
}
|
||||
// SHLD r/m, reg, CL: 0F A5 (REX.W for the 64-bit width).
|
||||
e := &enc{}
|
||||
i := &instr{rexW: size == 8, opcode: []byte{0x0F, 0xA5}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, srcReg, headReg, size); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := e.emit(i); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ps := make([]sbPatch, len(e.patches))
|
||||
for j, p := range e.patches {
|
||||
ps[j] = sbPatch{off: p.off, name: p.name, addend: p.addend}
|
||||
}
|
||||
return e.out, ps, nil
|
||||
}
|
||||
|
||||
// trailingIndexGroup recovers a trailing "(index*scale)" or "(index)" group
|
||||
// from an operand's raw text: the symbol-pseudo parse returns before the
|
||||
// index group, so foo(SP)(AX*1) keeps its index only in the spelling.
|
||||
func trailingIndexGroup(raw string) (string, int, bool) {
|
||||
compact := strings.ReplaceAll(raw, " ", "")
|
||||
if !strings.HasSuffix(compact, ")") {
|
||||
return "", 0, false
|
||||
}
|
||||
open := strings.LastIndex(compact, "(")
|
||||
if open < 2 || !strings.Contains(compact[:open], ")") {
|
||||
return "", 0, false // one group alone: no trailing index
|
||||
}
|
||||
name, scale, _, ok := cutParenGroup(compact[open:])
|
||||
return name, scale, ok
|
||||
}
|
||||
|
||||
// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the
|
||||
// target label or from a numeric ±N(PC) instruction count, in the short
|
||||
// (rel8) or long (rel32) form. numTarget is the resolved byte offset of a
|
||||
@@ -1085,9 +1181,15 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long
|
||||
if mnem == "JMP" {
|
||||
return []byte{0xEB, byte(int8(rel))}, nil
|
||||
}
|
||||
if isLoopMnemonic(mnem) {
|
||||
return []byte{loopOpcode(mnem), byte(int8(rel))}, nil
|
||||
}
|
||||
cc, _ := condCode(mnem)
|
||||
return []byte{0x70 + byte(cc), byte(int8(rel))}, nil
|
||||
}
|
||||
if isLoopMnemonic(mnem) {
|
||||
return nil, fmt.Errorf("%s has no long form", mnem)
|
||||
}
|
||||
switch mnem {
|
||||
case "JMP":
|
||||
return append([]byte{0xE9}, le32(rel)...), nil
|
||||
@@ -1139,6 +1241,72 @@ func labelName(op *ast.Operand) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// jumpOperand returns the branch-target operand of a JMP/CALL, rewriting the
|
||||
// `*`-prefixed indirect spellings (JMP *(R12), JMP *4(SP)) into their plain
|
||||
// memory form. The star marks an indirect target and changes no bytes; the
|
||||
// address parser leaves the operand's address empty because of the leading
|
||||
// star, so the fields are rebuilt from the raw text onto a copy of the
|
||||
// operand, never on the shared syntax tree.
|
||||
func jumpOperand(s *ast.Instr) *ast.Operand {
|
||||
if len(s.Operands) != 1 {
|
||||
return nil
|
||||
}
|
||||
op := s.Operands[0]
|
||||
compact := strings.ReplaceAll(op.Raw, " ", "")
|
||||
inner, ok := strings.CutPrefix(compact, "*")
|
||||
if !ok {
|
||||
return op
|
||||
}
|
||||
var addr ast.Address
|
||||
if i := strings.IndexByte(inner, '('); i > 0 {
|
||||
v, err := strconv.ParseInt(inner[:i], 0, 64)
|
||||
if err != nil {
|
||||
return op
|
||||
}
|
||||
addr.Offset, addr.HasOff = v, true
|
||||
inner = inner[i:]
|
||||
}
|
||||
base, _, rest, ok := cutParenGroup(inner)
|
||||
if !ok {
|
||||
return op
|
||||
}
|
||||
if base != "" {
|
||||
addr.Base = base
|
||||
}
|
||||
if rest != "" {
|
||||
idx, scale, _, ok := cutParenGroup(rest)
|
||||
if ok && idx != "" {
|
||||
addr.Index = idx
|
||||
addr.Scale = scale
|
||||
}
|
||||
}
|
||||
c := *op
|
||||
c.Addr = addr
|
||||
return &c
|
||||
}
|
||||
|
||||
// cutParenGroup splits a leading "(name)" or "(name*n)" off s, returning the
|
||||
// inner text, the scale it names (1 when the group spells no multiplier) and
|
||||
// the remainder.
|
||||
func cutParenGroup(s string) (name string, scale int, rest string, ok bool) {
|
||||
if !strings.HasPrefix(s, "(") {
|
||||
return "", 0, "", false
|
||||
}
|
||||
i := strings.IndexByte(s, ')')
|
||||
if i < 0 {
|
||||
return "", 0, "", false
|
||||
}
|
||||
inner, rest := s[1:i], s[i+1:]
|
||||
if before, after, ok := strings.Cut(inner, "*"); ok {
|
||||
n, err := strconv.Atoi(after)
|
||||
if err != nil {
|
||||
return "", 0, "", false
|
||||
}
|
||||
return before, n, rest, true
|
||||
}
|
||||
return inner, 1, rest, true
|
||||
}
|
||||
|
||||
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||
// register or a memory location rather than a label or a static symbol.
|
||||
// A bare identifier is a register when the register table knows the name and
|
||||
@@ -1147,7 +1315,14 @@ func indirectJumpTarget(s *ast.Instr) bool {
|
||||
if len(s.Operands) != 1 || s.Operands[0].Kind != ast.OpAddr {
|
||||
return false
|
||||
}
|
||||
a := s.Operands[0].Addr
|
||||
op := jumpOperand(s)
|
||||
if op == nil {
|
||||
return false
|
||||
}
|
||||
if op != s.Operands[0] {
|
||||
return true // the star marker spells an indirect target
|
||||
}
|
||||
a := op.Addr
|
||||
// ±N(PC) is the numeric relative form, the PC counts instructions from
|
||||
// the branch: relative, not indirect.
|
||||
if a.Base == "PC" || a.Index == "PC" {
|
||||
@@ -1165,18 +1340,20 @@ func indirectJumpTarget(s *ast.Instr) bool {
|
||||
}
|
||||
|
||||
// encodeIndirectJump assembles a JMP/CALL through a register or memory
|
||||
// operand, which carries no relocation and no label to resolve.
|
||||
// operand, which carries no relocation and no label to resolve. The
|
||||
// `*`-prefixed spellings go through jumpOperand first, their star rebuilt
|
||||
// into a plain memory operand.
|
||||
func encodeIndirectJump(s *ast.Instr, mnem string) ([]byte, error) {
|
||||
ops := make([]Operand, len(s.Operands))
|
||||
for i, op := range s.Operands {
|
||||
op := s.Operands[0]
|
||||
if cleaned := jumpOperand(s); cleaned != nil {
|
||||
op = cleaned
|
||||
}
|
||||
o, err := operandFromAST(mnem, op, 8, frameInfo{}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops[i] = o
|
||||
}
|
||||
e := &enc{}
|
||||
if err := e.encodeIndirectBranch(mnem, ops); err != nil {
|
||||
if err := e.encodeIndirectBranch(mnem, []Operand{o}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.out, nil
|
||||
@@ -1245,31 +1422,51 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l
|
||||
off := a.Sym.Offset + fi.fpAdjust
|
||||
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
|
||||
}
|
||||
// SP-relative local: x-N(SP) → (spAdjust + offset)(SP).
|
||||
// SP-relative local: x-N(SP) → (spAdjust + offset)(SP), keeping a scaled
|
||||
// index beside the virtual stack pointer (foo(SP)(AX*1)). The
|
||||
// symbol-pseudo parse returns before the index group, so the index
|
||||
// is recovered from the raw text when the address lacks it.
|
||||
if a.Sym != nil && a.Sym.Pseudo == "SP" && a.Base == "" {
|
||||
off := fi.spAdjust + a.Sym.Offset
|
||||
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
|
||||
m := Mem{Base: spReg, Disp: off, HasBase: true, Size: size}
|
||||
if name, scale, ok := trailingIndexGroup(op.Raw); ok {
|
||||
idx, ok := ParseReg(name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown index register %q", name)
|
||||
}
|
||||
m.Index = idx
|
||||
m.Scale = scale
|
||||
m.HasIndex = true
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
// SB (global symbol): a symbol defined in the same file (GLOBL) is
|
||||
// encoded RIP-relative and resolved by the file-level layout;
|
||||
// anything not defined here needs object-file emission.
|
||||
// anything not defined here needs object-file emission. A static
|
||||
// (file-local) spelling of an undefined symbol defers the same way
|
||||
// the toolchain does: the relocation names it and the linker decides.
|
||||
if a.Sym != nil && a.Sym.Pseudo == "SB" {
|
||||
if link == nil || link.symbols == nil {
|
||||
return nil, fmt.Errorf("symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name)
|
||||
}
|
||||
if !link.symbols[a.Sym.Name] {
|
||||
if a.Sym.Static {
|
||||
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
|
||||
}
|
||||
if !link.allowExternal {
|
||||
if !link.symbols[a.Sym.Name] && !link.allowExternal {
|
||||
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
|
||||
}
|
||||
}
|
||||
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
|
||||
}
|
||||
|
||||
// Memory with a real base register: (base), off(base), (base)(index*scale).
|
||||
if a.Base != "" {
|
||||
// The TLS pseudo-base, off(TLS): the segment-prefixed absolute
|
||||
// the thread-local access lowers to, 64 8B 04 25 with its
|
||||
// R_TLS_LE patch site on the disp32.
|
||||
if a.Base == "TLS" {
|
||||
seg := byte(0x64) // FS on linux, freebsd, plan9
|
||||
if link != nil && link.goos == "windows" {
|
||||
seg = 0x65 // GS
|
||||
}
|
||||
return TLSMem{Disp: a.Offset, Size: size, Seg: seg}, nil
|
||||
}
|
||||
// Segment-absolute: 0x30(GS) and 0x28(FS), the windows TLS
|
||||
// spellings. The segment override prefixes a disp32 absolute
|
||||
// reference with no relocation.
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// firstText parses src and returns its first TEXT function.
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// ulebIter reads ULEB128 values, the .debug_abbrev and line-header
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The object-file tests share one source: two exported functions, one
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFRISCVObjectDataRelocation checks that a symbol-valued DATA field
|
||||
|
||||
@@ -20,11 +20,24 @@ func Encodable(mnemonic string) bool {
|
||||
switch upper {
|
||||
case "RET", "NOP", "CALL", "JMP",
|
||||
"POPFQ", "PUSHFQ", "INT", "LDMXCSR", "STMXCSR", "CMPSD", "SHA256RNDS2",
|
||||
// The SSE compare family sharing CMPSD's predicate-last shape, the
|
||||
// far return with its stack pop, the loop family, the bank-crossing
|
||||
// MMX moves and the one-operand system controls.
|
||||
"CMPSS", "CMPPS", "CMPPD", "RETFL",
|
||||
"LOOP", "LOOPE", "LOOPNE",
|
||||
"MOVDQ2Q", "MOVQ2DQ",
|
||||
"ENDBR64", "CLWB", "TPAUSE", "UMONITOR", "UMWAIT", "RDPID", "CLDEMOTE",
|
||||
// The literal-data pseudo-ops, the accepted-and-ignored END and
|
||||
// bookkeeping statements, and the SP adjust.
|
||||
"BYTE", "WORD", "LONG", "QUAD", "END", "ADJSP", "FUNCDATA", "PCDATA":
|
||||
return true
|
||||
}
|
||||
if _, ok := sysUnaryTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseStoreOnly[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := noOperandTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
+75
-3
@@ -73,9 +73,23 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
// Fixed-name instructions (no size suffix).
|
||||
switch {
|
||||
case upper == "RET":
|
||||
// RET sym(SB), the absolute return: the toolchain encodes it as a
|
||||
// tail jump, E9 rel32 with a call relocation against the symbol.
|
||||
if len(ops) == 1 {
|
||||
if m, ok := ops[0].(sbMem); ok {
|
||||
return e.emit(&instr{opcode: []byte{0xE9}, modrm: -1, sib: -1, disp: le32(0), sb: &sbRef{name: m.name, addend: m.addend}})
|
||||
}
|
||||
return fmt.Errorf("RET: unsupported operand")
|
||||
}
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("RET expects no operands, got %d", len(ops))
|
||||
}
|
||||
return e.encodeRet()
|
||||
case upper == "NOP":
|
||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||
// The toolchain consumes every NOP statement as a pseudo and emits
|
||||
// nothing for it, operands included (a bare NOP, NOP AX and
|
||||
// NOP sym(SB) all vanish from the object).
|
||||
return nil
|
||||
case upper == "CALL" || upper == "JMP":
|
||||
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||
// Anything else is a rel32 against a label resolved by the assembler.
|
||||
@@ -102,6 +116,15 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
}
|
||||
return e.emit(&instr{opcode: op, modrm: -1, sib: -1})
|
||||
}
|
||||
// One-operand system instructions whose reg field is a fixed digit:
|
||||
// the cache and wait controls under 0F AE/0F 1C and the RDPID read.
|
||||
if m, ok := sysUnaryTable[upper]; ok {
|
||||
return e.encodeSysUnary(upper, m, ops)
|
||||
}
|
||||
// The store-only SSE moves (the non-temporal store).
|
||||
if m, ok := sseStoreOnly[upper]; ok {
|
||||
return e.encodeSSEStoreOnly(upper, m, ops)
|
||||
}
|
||||
// POPFQ/PUSHFQ are exact names: the bare POPF/PUSHF and the L spellings
|
||||
// are rejected by go tool asm in 64-bit mode, so they stay unsupported.
|
||||
switch upper {
|
||||
@@ -117,14 +140,63 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.emit(&instr{opcode: []byte{0x9C}, modrm: -1, sib: -1})
|
||||
case "INT":
|
||||
return e.encodeInt(ops)
|
||||
// The LOOP family outside the assembler's label settlement: the operand
|
||||
// is the already-computed rel8 (E0-E2).
|
||||
case "LOOP", "LOOPE", "LOOPNE":
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", upper, len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok || !fits8(int64(imm)) {
|
||||
return fmt.Errorf("%s: relative offset must be a signed byte", upper)
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{loopOpcode(upper)}, modrm: -1, sib: -1, imm: []byte{byte(int8(imm))}})
|
||||
case "LDMXCSR":
|
||||
return e.encodeMxcsr(2, ops)
|
||||
case "STMXCSR":
|
||||
return e.encodeMxcsr(3, ops)
|
||||
// CMPSD is the scalar double compare, whose predicate immediate comes
|
||||
// LAST in Plan 9 order (src, dst, $imm).
|
||||
// LAST in Plan 9 order (src, dst, $imm); the family shares the shape.
|
||||
case "CMPSD":
|
||||
return e.encodeCmpsd(ops)
|
||||
return e.encodeSSECmp("CMPSD", 0xF2, ops)
|
||||
case "CMPSS":
|
||||
return e.encodeSSECmp("CMPSS", 0xF3, ops)
|
||||
case "CMPPS":
|
||||
return e.encodeSSECmp("CMPPS", 0x00, ops)
|
||||
case "CMPPD":
|
||||
return e.encodeSSECmp("CMPPD", 0x66, ops)
|
||||
// RETFL pops the immediate's worth of bytes after the far return
|
||||
// (LRET iw: CA imm16), the toolchain's RETF spelling with a stack
|
||||
// adjustment.
|
||||
case "RETFL":
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("RETFL expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("RETFL expects an immediate")
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0xCA}, modrm: -1, sib: -1, imm: le16(int64(imm))})
|
||||
// MOVDQ2Q/MOVQ2DQ cross the MMX and XMM banks (F2 0F D6), the register
|
||||
// in the reg field, the other bank's in r/m.
|
||||
case "MOVDQ2Q", "MOVQ2DQ":
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, ok1 := ops[0].(Reg)
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok1 || !ok2 {
|
||||
return fmt.Errorf("%s takes register operands alone", upper)
|
||||
}
|
||||
if upper == "MOVDQ2Q" && (!srcReg.isVec() || !dstReg.mmx) ||
|
||||
upper == "MOVQ2DQ" && (!srcReg.mmx || !dstReg.isVec()) {
|
||||
return fmt.Errorf("%s crosses the XMM and MMX banks in that order", upper)
|
||||
}
|
||||
i := &instr{prefix: 0xF2, opcode: []byte{0x0F, 0xD6}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, srcReg, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
// SHA256RNDS2 carries the round constant in a literal X0 first operand.
|
||||
case "SHA256RNDS2":
|
||||
return e.encodeSha256rnds2(ops)
|
||||
|
||||
+158
-4
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// decode encodes an instruction and decodes it back, returning the decoded
|
||||
@@ -265,7 +265,11 @@ func TestImul(t *testing.T) {
|
||||
|
||||
func TestControl(t *testing.T) {
|
||||
checkSyntax(t, "ret", "RET")
|
||||
checkSyntax(t, "nop", "NOP")
|
||||
// NOP contributes nothing on amd64, consumed whole by the toolchain as
|
||||
// a pseudo; only the bytes pin it (no decodable instruction remains).
|
||||
if code, err := Encode("NOP"); err != nil || len(code) != 0 {
|
||||
t.Errorf("NOP: bytes %x (err %v), want empty", code, err)
|
||||
}
|
||||
checkOp(t, x86asm.JMP, "JMP", Imm(0))
|
||||
checkOp(t, x86asm.CALL, "CALL", Imm(0))
|
||||
checkOp(t, x86asm.JGE, "JGE", Imm(0))
|
||||
@@ -1193,7 +1197,7 @@ func TestBookkeepingGroundTruth(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
want := "90c3"
|
||||
want := "c3"
|
||||
if got := hexCompact(img.Code); got != want {
|
||||
t.Errorf("body %s, want %s (the bookkeeping lines contribute nothing)", got, want)
|
||||
}
|
||||
@@ -1221,3 +1225,153 @@ func mustParse(t *testing.T, src string) *ast.File {
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// TestCorpusTailSystem pins the system and control forms the toolchain's own
|
||||
// amd64 testdata carries, byte for byte: the one-operand IMUL, the compare
|
||||
// family, the far return, the loop, the MMX moves, the CR/DR and segment
|
||||
// register moves, the TLS pseudo-base and the 0F AE/1C/C7 controls.
|
||||
func TestCorpusTailSystem(t *testing.T) {
|
||||
regBPT := Reg{idx: 5, size: 8}
|
||||
X0, X1, X2 := vreg(t, "X0"), vreg(t, "X1"), vreg(t, "X2")
|
||||
Y1, Y2, Y7 := vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y7")
|
||||
X5, X20 := vreg(t, "X5"), vreg(t, "X20")
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"IMUL one-op byte", "IMULB", []Operand{DX}, "f6ea"},
|
||||
{"IMUL one-op long", "IMULL", []Operand{AX}, "f7e8"},
|
||||
{"CMPPD", "CMPPD", []Operand{X1, X2, Imm(4)}, "660fc2d104"},
|
||||
{"CMPSS", "CMPSS", []Operand{X1, X2, Imm(4)}, "f30fc2d104"},
|
||||
{"CMPPS", "CMPPS", []Operand{X1, X2, Imm(4)}, "0fc2d104"},
|
||||
{"RETFL", "RETFL", []Operand{Imm(4)}, "ca0400"},
|
||||
{"LOOP", "LOOP", []Operand{Imm(-2)}, "e2fe"},
|
||||
{"LOOPE", "LOOPE", []Operand{Imm(-2)}, "e1fe"},
|
||||
{"LOOPNE", "LOOPNE", []Operand{Imm(-2)}, "e0fe"},
|
||||
{"PADDD MMX", "PADDD", []Operand{Reg{idx: 2, size: 8, mmx: true}, Reg{idx: 1, size: 8, mmx: true}}, "0ffeca"},
|
||||
{"MOVDQ2Q", "MOVDQ2Q", []Operand{X1, Reg{idx: 1, size: 8, mmx: true}}, "f20fd6c9"},
|
||||
{"MOVNTDQ", "MOVNTDQ", []Operand{X1, Ptr(AX, 0, 16)}, "660fe708"},
|
||||
{"MOVQ mmx load", "MOVQ", []Operand{Ptr(AX, 0, 8), Reg{idx: 0, size: 8, mmx: true}}, "0f6f00"},
|
||||
{"MOVQ mmx store", "MOVQ", []Operand{Reg{idx: 0, size: 8, mmx: true}, Ptr(SI, 0, 8)}, "0f7f06"},
|
||||
{"MOVQ CR0 load", "MOVQ", []Operand{Reg{idx: 0, size: 8, ctl: 1}, AX}, "0f20c0"},
|
||||
{"MOVQ CR4 load", "MOVQ", []Operand{Reg{idx: 4, size: 8, ctl: 1}, DI}, "0f20e7"},
|
||||
{"MOVQ CR0 store", "MOVQ", []Operand{AX, Reg{idx: 0, size: 8, ctl: 1}}, "0f22c0"},
|
||||
{"MOVQ DR0 load", "MOVQ", []Operand{Reg{idx: 0, size: 8, ctl: 2}, AX}, "0f21c0"},
|
||||
{"MOVQ DR7 load", "MOVQ", []Operand{Reg{idx: 7, size: 8, ctl: 2}, SI}, "0f21fe"},
|
||||
{"PUSHQ FS", "PUSHQ", []Operand{Reg{idx: 4, size: 2, seg: 5}}, "0fa0"},
|
||||
{"PUSHQ GS", "PUSHQ", []Operand{Reg{idx: 5, size: 2, seg: 6}}, "0fa8"},
|
||||
{"POPQ FS", "POPQ", []Operand{Reg{idx: 4, size: 2, seg: 5}}, "0fa1"},
|
||||
{"POPQ GS", "POPQ", []Operand{Reg{idx: 5, size: 2, seg: 6}}, "0fa9"},
|
||||
{"ENDBR64", "ENDBR64", nil, "f30f1efa"},
|
||||
{"CLWB", "CLWB", []Operand{Ptr(BX, 0, 8)}, "660fae33"},
|
||||
{"CLDEMOTE", "CLDEMOTE", []Operand{Ptr(BX, 0, 8)}, "0f1c03"},
|
||||
{"TPAUSE", "TPAUSE", []Operand{BX}, "660faef3"},
|
||||
{"UMONITOR", "UMONITOR", []Operand{BX}, "f30faef3"},
|
||||
{"UMWAIT", "UMWAIT", []Operand{BX}, "f20faef3"},
|
||||
{"RDPID", "RDPID", []Operand{DX}, "f30fc7fa"},
|
||||
{"RDPID r11", "RDPID", []Operand{Reg{idx: 11, size: 8}}, "f3410fc7fb"},
|
||||
{"LEAL wide disp", "LEAL", []Operand{Idx(regBPT, Reg{idx: 10, size: 8}, 1, 0x8f1bbcdc, 8), regBPT}, "428dac15dcbc1b8f"},
|
||||
{"VPERMPD", "VPERMPD", []Operand{Imm(0xd8), Y7, Y7}, "c4e3fd01ffd8"},
|
||||
{"VPERMILPD", "VPERMILPD", []Operand{Imm(0xff), X1, X2}, "c4e37905d1ff"},
|
||||
{"VPERMILPS", "VPERMILPS", []Operand{Imm(0xff), X1, X2}, "c4e37904d1ff"},
|
||||
{"VROUNDPD", "VROUNDPD", []Operand{Imm(-1), X1, X2}, "c4e37909d1ff"},
|
||||
{"VROUNDPS", "VROUNDPS", []Operand{Imm(-1), Y1, Y2}, "c4e37d08d1ff"},
|
||||
{"VAESKEYGENASSIST", "VAESKEYGENASSIST", []Operand{Imm(-1), X1, X2}, "c4e379dfd1ff"},
|
||||
{"VPCMPESTRI", "VPCMPESTRI", []Operand{Imm(-1), X1, X2}, "c4e37961d1ff"},
|
||||
{"VPCMPESTRM", "VPCMPESTRM", []Operand{Imm(-1), X1, X2}, "c4e37960d1ff"},
|
||||
{"VPCMPISTRI", "VPCMPISTRI", []Operand{Imm(-1), X1, X2}, "c4e37963d1ff"},
|
||||
{"VPCMPISTRM", "VPCMPISTRM", []Operand{Imm(-1), X1, X2}, "c4e37962d1ff"},
|
||||
{"VEXTRACTPS", "VEXTRACTPS", []Operand{Imm(-1), X1, AX}, "c4e37917c8ff"},
|
||||
{"VPEXTRW", "VPEXTRW", []Operand{Imm(0xff), X1, AX}, "c4e37915c8ff"},
|
||||
{"VPBLENDVB", "VPBLENDVB", []Operand{X0, Ptr(BX, 0, 16), X1, X2}, "c4e3714c1300"},
|
||||
{"VMOVHPD load", "VMOVHPD", []Operand{Ptr(AX, 0, 8), X5, X5}, "c5d11628"},
|
||||
{"VMOVHPD load disp", "VMOVHPD", []Operand{Ptr(DX, 7, 8), X5, X5}, "c5d1166a07"},
|
||||
{"VMOVHPD store", "VMOVHPD", []Operand{X5, Ptr(AX, 0, 8)}, "c5f91728"},
|
||||
{"VMOVLPD load", "VMOVLPD", []Operand{Ptr(AX, 0, 8), X5, X5}, "c5d11228"},
|
||||
{"VMOVLPD store", "VMOVLPD", []Operand{X5, Ptr(AX, 0, 8)}, "c5f91328"},
|
||||
{"VMOVQ EVEX gpr load", "VMOVQ", []Operand{Reg{idx: 4, size: 8}, X20}, "62e1fd086ee4"},
|
||||
{"VMOVQ EVEX mem store", "VMOVQ", []Operand{X20, Ptr(AX, 0, 8)}, "62e1fd087e20"},
|
||||
{"VMOVQ EVEX mem load", "VMOVQ", []Operand{Ptr(AX, 0, 8), X20}, "62e1fd086e20"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Encode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := hexCompact(code); got != c.want {
|
||||
t.Errorf("%s: bytes %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCorpusTailFileForms pins the file-level forms the toolchain's amd64
|
||||
// testdata carries: the star-marked indirect jumps, the TLS pseudo-base, the
|
||||
// paired-register shift spelling, the absolute RET and the jump to an
|
||||
// undefined static symbol (its displacement and the TLS slot offsets are
|
||||
// relocation sites, zeroed here as the kernel parity suites do).
|
||||
func TestCorpusTailFileForms(t *testing.T) {
|
||||
mask32 := func(b []byte, at int) { b[at], b[at+1], b[at+2], b[at+3] = 0, 0, 0, 0 }
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string // hex, with X marking a masked 32-bit relocation site
|
||||
}{
|
||||
{"star reg jump", "\tJMP *(R12)\n\tRET\n", "41ff2424c3"},
|
||||
{"star sp jump", "\tJMP *4(SP)\n\tRET\n", "ff642404c3"},
|
||||
{"star indexed jump", "\tJMP *(R12)(R13*4)\n\tRET\n", "43ff24acc3"},
|
||||
{"TLS load", "\tMOVQ (TLS), AX\n\tRET\n", "64488b0425XXXXXXXXc3"},
|
||||
{"TLS load offset", "\tMOVQ 8(TLS), DX\n\tRET\n", "64488b1425XXXXXXXXc3"},
|
||||
{"colon shift", "\tSHLL CX, R11:AX\n\tRET\n", "410fa5c3c3"},
|
||||
{"SP indexed local", "\tMOVQ foo(SP)(AX*1), BX\n\tRET\n", "488b1c04c3"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
f, errs := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n"+c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("%s: parse: %v", c.name, errs)
|
||||
continue
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Errorf("%s: assemble: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
if at := strings.Index(c.want, "XXXXXXXX"); at >= 0 {
|
||||
mask32(code, at/2) // the masked relocation site
|
||||
}
|
||||
if got := hexCompact(code); got != strings.ReplaceAll(c.want, "X", "0") {
|
||||
t.Errorf("%s: bytes %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
// JMP to an undefined static symbol and the absolute RET: their rel32
|
||||
// carries a call relocation against the symbol, masked to zero here.
|
||||
for _, c := range []struct{ name, src string }{
|
||||
{"static external jump", "\tJMP bar<>+4(SB)\n\tRET\n"},
|
||||
{"static external indexed jump", "\tJMP bar<>+4(SB)(R11*4)\n\tRET\n"},
|
||||
{"absolute ret", "\tRET\n\tRET foo(SB)\n"},
|
||||
} {
|
||||
f, errs := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n"+c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("%s: parse: %v", c.name, errs)
|
||||
continue
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Errorf("%s: assemble: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
code := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
want := "e900000000c3"
|
||||
if c.name == "absolute ret" {
|
||||
want = "c3e900000000"
|
||||
}
|
||||
if got := hexCompact(code); got != want {
|
||||
t.Errorf("%s: bytes %s, want %s", c.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-17
@@ -856,37 +856,41 @@ type evexMoveSpec struct {
|
||||
vecOK bool // the non-memory operand may be a vector register
|
||||
xmmOnly bool // wider than XMM registers are rejected
|
||||
nds3 bool // a three-operand register form exists (VMOVSD/VMOVSS)
|
||||
gprOK bool // the r/m side may be a general-purpose register (VMOVQ)
|
||||
}
|
||||
|
||||
// evexMoveTable maps an upper-case EVEX move mnemonic to its encoding.
|
||||
var evexMoveTable = map[string]evexMoveSpec{
|
||||
// EVEX.128/256/512.F3.0F.W0, unaligned integer move.
|
||||
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512.F3.0F.W1, unaligned qword move.
|
||||
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512.F2.0F.W0, unaligned byte move (byte/word moves use the
|
||||
// F2 prefix, dword/qword moves F3; the element size only changes the tuple
|
||||
// semantics).
|
||||
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512.F2.0F.W1, unaligned word move (shares the qword
|
||||
// encoding).
|
||||
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512.66.0F.W1, unaligned packed double move.
|
||||
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512, aligned packed moves.
|
||||
"VMOVAPS": {1, 0, 0x28, 0x29, 0, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVAPD": {1, 1, 0x28, 0x29, 1, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVAPS": {1, 0, 0x28, 0x29, 0, [3]int{16, 32, 64}, true, false, false, false},
|
||||
"VMOVAPD": {1, 1, 0x28, 0x29, 1, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128/256/512.66.0F, aligned integer moves.
|
||||
"VMOVDQA32": {1, 1, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQA64": {1, 1, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVDQA32": {1, 1, 0x6F, 0x7F, 0, [3]int{16, 32, 64}, true, false, false, false},
|
||||
"VMOVDQA64": {1, 1, 0x6F, 0x7F, 1, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128.F3.0F.W0, scalar single move, memory operands (the
|
||||
// three-operand register form is not supported).
|
||||
"VMOVSS": {1, 2, 0x10, 0x11, 0, [3]int{4, 4, 4}, false, true, true},
|
||||
"VMOVSS": {1, 2, 0x10, 0x11, 0, [3]int{4, 4, 4}, false, true, true, false},
|
||||
// EVEX.128.F2.0F.W1, scalar double move: memory operands and the
|
||||
// three-operand register form (VMOVSD dst, src1, src2).
|
||||
"VMOVSD": {1, 3, 0x10, 0x11, 1, [3]int{8, 8, 8}, false, true, true},
|
||||
"VMOVSD": {1, 3, 0x10, 0x11, 1, [3]int{8, 8, 8}, false, true, true, false},
|
||||
// EVEX.128/256/512.0F.W0, unaligned packed single move.
|
||||
"VMOVUPS": {1, 0, 0x10, 0x11, 0, [3]int{16, 32, 64}, true, false, false},
|
||||
"VMOVUPS": {1, 0, 0x10, 0x11, 0, [3]int{16, 32, 64}, true, false, false, false},
|
||||
// EVEX.128.66.0F.W1, the 64-bit GPR/memory ↔ XMM move (VMOVQ RSP, X20
|
||||
// and friends, the EVEX spelling the high registers demand).
|
||||
"VMOVQ": {1, 1, 0x6E, 0x7E, 1, [3]int{8, 8, 8}, true, true, false, true},
|
||||
}
|
||||
|
||||
// isEvex reports whether the mnemonic has an EVEX encoding we handle.
|
||||
@@ -900,6 +904,9 @@ func isEvex(mnemUpper string) bool {
|
||||
if _, ok := evexMoveTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := evexHptrTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
return isEvexQuad(mnemUpper)
|
||||
}
|
||||
|
||||
@@ -911,8 +918,14 @@ func evexRequired(upper string, ops []Operand) bool {
|
||||
_, inVex := vexTable[upper]
|
||||
_, inVexMove := vexMoveTable[upper]
|
||||
if !inVex && !inVexMove {
|
||||
// The dual-shape moves pick their VEX form by operand count, so
|
||||
// they are not EVEX-only either.
|
||||
switch upper {
|
||||
case "VMOVHPD", "VMOVLPD":
|
||||
default:
|
||||
return true // EVEX-only mnemonic
|
||||
}
|
||||
}
|
||||
// The byte-quad shifts have VEX register forms but EVEX-only memory
|
||||
// forms: a memory count source forces the EVEX encoding.
|
||||
if upper == "VPSLLDQ" || upper == "VPSRLDQ" {
|
||||
@@ -1018,6 +1031,8 @@ var evexRound = map[string]bool{
|
||||
"VCVTTSD2USIL": true, "VCVTTSD2USIQ": true, "VCVTTSS2USIL": true, "VCVTTSS2USIQ": true,
|
||||
"VCVTSI2SDQ": true, "VCVTSI2SSL": true, "VCVTSI2SSQ": true,
|
||||
"VCVTUSI2SDQ": true, "VCVTUSI2SSL": true, "VCVTUSI2SSQ": true,
|
||||
// The scalar compares suppress exceptions on their LIG encoding.
|
||||
"VCMPSD": true, "VCMPSS": true,
|
||||
}
|
||||
|
||||
// evexBcstN maps an instruction accepting .BCST to the broadcast element
|
||||
@@ -1082,6 +1097,14 @@ func (e *enc) encodeEvex(mnemUpper string, ops []Operand, sfx evexSuffix) error
|
||||
return e.encodeEvexRM(spec, ops, 0, sfx)
|
||||
}
|
||||
spec, inTable := evexTable[mnemUpper]
|
||||
// A high/low half move that lives in the hptr table alone (the packed
|
||||
// double twins) reaches the same inTable block below, which completes
|
||||
// its spec from the hptr entry.
|
||||
if !inTable {
|
||||
if _, ok := evexHptrTable[mnemUpper]; ok {
|
||||
inTable = true
|
||||
}
|
||||
}
|
||||
if q, ok := evexQuadTable[mnemUpper]; ok {
|
||||
// The quad-register family carries no rounding, SAE or broadcast;
|
||||
// only masking and zeroing apply.
|
||||
@@ -1446,6 +1469,19 @@ func (e *enc) encodeEvexExtractGPR(spec evexSpec, ops []Operand, mask int, sfx e
|
||||
// assembler. The scalar moves also carry a three-operand register form
|
||||
// (VMOVSD dst, src1, src2: the load opcode with vvvv = src1), which ms.nds3
|
||||
// opens.
|
||||
// validEvexMoveOther reports whether the non-vector side of an EVEX move may
|
||||
// take the operand: memory always, a general-purpose register when gprOK.
|
||||
func validEvexMoveOther(ms evexMoveSpec, op Operand) bool {
|
||||
if memOperand(op) {
|
||||
return true
|
||||
}
|
||||
if !ms.gprOK {
|
||||
return false
|
||||
}
|
||||
r, ok := op.(Reg)
|
||||
return ok && !r.isVec() && !r.mask && r.ctl == 0 && !r.mmx && !r.fp
|
||||
}
|
||||
|
||||
func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask int, sfx evexSuffix) error {
|
||||
if len(ops) == 3 {
|
||||
if !ms.nds3 {
|
||||
@@ -1453,7 +1489,7 @@ func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask i
|
||||
}
|
||||
// The masked scalar register form keeps the Go assembler's own
|
||||
// layout: the store opcode with reg = op0, vvvv = op1 and the
|
||||
// destination in r/m (op2) — the bytes go tool asm emits, not
|
||||
// destination in r/m (op2), the bytes go tool asm emits, not
|
||||
// the manual's NDS reading.
|
||||
src, src1, dst := ops[0], ops[1], ops[2]
|
||||
reg, ok := src.(Reg)
|
||||
@@ -1494,12 +1530,12 @@ func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask i
|
||||
}
|
||||
reg, rm = srcReg, dst
|
||||
case srcIsVec:
|
||||
if !memOperand(dst) {
|
||||
if !validEvexMoveOther(ms, dst) {
|
||||
return fmt.Errorf("%s: invalid destination operand", mnem)
|
||||
}
|
||||
reg, rm = srcReg, dst
|
||||
case dstIsVec:
|
||||
if !memOperand(src) {
|
||||
if !validEvexMoveOther(ms, src) {
|
||||
return fmt.Errorf("%s: invalid source operand", mnem)
|
||||
}
|
||||
op = ms.load
|
||||
@@ -1890,6 +1926,15 @@ var evexHptrTable = map[string]evexHptrSpec{
|
||||
"VMOVLHPS": {
|
||||
insert: evexSpec{mapSel: 1, opcode: 0x16, w: 0, pp: 0, opdigit: -1, form: vexNDS3, n: [3]int{8, 0, 0}},
|
||||
},
|
||||
// The packed-double twins, 66-prefixed.
|
||||
"VMOVHPD": {
|
||||
insert: evexSpec{mapSel: 1, opcode: 0x16, w: 1, pp: 1, opdigit: -1, form: vexNDS3, n: [3]int{8, 0, 0}},
|
||||
store: evexSpec{mapSel: 1, opcode: 0x17, w: 1, pp: 1, opdigit: -1, form: vexRMRev, n: [3]int{8, 0, 0}},
|
||||
},
|
||||
"VMOVLPD": {
|
||||
insert: evexSpec{mapSel: 1, opcode: 0x12, w: 1, pp: 1, opdigit: -1, form: vexNDS3, n: [3]int{8, 0, 0}},
|
||||
store: evexSpec{mapSel: 1, opcode: 0x13, w: 1, pp: 1, opdigit: -1, form: vexRMRev, n: [3]int{8, 0, 0}},
|
||||
},
|
||||
}
|
||||
|
||||
// encodeEvexPrefGather encodes a gather/scatter prefetch hint: OP K, vsib.
|
||||
@@ -1954,7 +1999,7 @@ func (e *enc) encodeGather(upper string, gs gatherSpec, ops []Operand, sfx evexS
|
||||
if !ok || !maskReg.isVec() {
|
||||
return fmt.Errorf("%s: mask must be a vector register", upper)
|
||||
}
|
||||
vsib, _, err := vsibLen(rest[1], upper)
|
||||
vsib, idxLen, err := vsibLen(rest[1], upper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1962,12 +2007,16 @@ func (e *enc) encodeGather(upper string, gs gatherSpec, ops []Operand, sfx evexS
|
||||
if !ok || !dst.isVec() {
|
||||
return fmt.Errorf("%s: destination must be a vector register", upper)
|
||||
}
|
||||
// The L bit is the wider of the data register and the VSIB index
|
||||
// lengths (a YMM index under an XMM destination selects 256-bit, the
|
||||
// bytes go tool asm emits).
|
||||
ll := max(idxLen, dst.vecLenBit())
|
||||
spec := vexSpec{mapSel: 2, opcode: gs.opcode, w: gs.w, pp: 1, opdigit: -1}
|
||||
rBit := 0
|
||||
if dst.idx >= 8 {
|
||||
rBit = 1
|
||||
}
|
||||
return e.emitVexFields(spec, dst.vecLenBit(), dst.idx&7, rBit, 15-maskReg.idx, vsib)
|
||||
return e.emitVexFields(spec, ll, dst.idx&7, rBit, 15-maskReg.idx, vsib)
|
||||
}
|
||||
|
||||
// encodeScatter encodes a scatter (EVEX only): OP src, K, vsib, reg = src,
|
||||
|
||||
@@ -63,6 +63,7 @@ const (
|
||||
const (
|
||||
kindSTEXT = 1
|
||||
kindSRODATA = 3
|
||||
kindSNOPTRDATA = 5
|
||||
kindSDATA = 7
|
||||
kindSDWARFFCN = 14
|
||||
kindSDWARFLINES = 20
|
||||
@@ -305,9 +306,16 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
if !d.Static {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
// RODATA implies no pointers, so it wins over NOPTR: the kind is
|
||||
// SRODATA either way, exactly as the toolchain chooses it. Plain
|
||||
// NOPTR data is SNOPTRDATA, which the linker keeps out of the GC's
|
||||
// type scan; a plain SDATA symbol would demand Go type information
|
||||
// no assembly file can supply, and the link would fail.
|
||||
typ := uint8(kindSDATA)
|
||||
if d.Rodata {
|
||||
typ = kindSRODATA
|
||||
} else if d.Noptr {
|
||||
typ = kindSNOPTRDATA
|
||||
}
|
||||
flag := uint8(0)
|
||||
if d.Dupok {
|
||||
|
||||
@@ -45,6 +45,9 @@ func TestReadRuntimeSymbols(t *testing.T) {
|
||||
// TestResolveExternalSymbols verifies end-to-end resolution of external
|
||||
// symbol references.
|
||||
func TestResolveExternalSymbols(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("resolves through a live go list -export: skipped in -short mode")
|
||||
}
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("go toolchain not available")
|
||||
}
|
||||
|
||||
+143
-1
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||||
@@ -514,6 +514,145 @@ func fieldAfter(line, flag string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildLogSteps is what a substitution test needs from a `go build -x -work`
|
||||
// log: the work directory, the assembler's object, the package archive and
|
||||
// the link command line.
|
||||
type buildLogSteps struct {
|
||||
work string
|
||||
asmObj string // $WORK expanded
|
||||
pkgArch string // $WORK expanded
|
||||
linkLine string // still carries $WORK placeholders
|
||||
}
|
||||
|
||||
// parseBuildLog extracts the build steps from a `go build -x -work` log.
|
||||
// asmFile names the assembly file whose object the test substitutes. A
|
||||
// missing step is a failure, not a skip: the toolchain changed shape and the
|
||||
// substitution would silently test nothing.
|
||||
func parseBuildLog(t *testing.T, log []byte, asmFile string) buildLogSteps {
|
||||
t.Helper()
|
||||
var st buildLogSteps
|
||||
for line := range strings.SplitSeq(string(log), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
st.work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, asmFile) && !strings.Contains(line, "-gensymabis"):
|
||||
st.asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
rest := strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
st.pkgArch = strings.Fields(strings.SplitN(rest, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
st.linkLine = line
|
||||
}
|
||||
}
|
||||
if st.work == "" || st.asmObj == "" || st.pkgArch == "" || st.linkLine == "" {
|
||||
t.Fatalf("could not locate the build steps (work=%q asmObj=%q pkgArch=%q link=%q):\n%s",
|
||||
st.work, st.asmObj, st.pkgArch, st.linkLine, log)
|
||||
}
|
||||
st.asmObj = strings.ReplaceAll(st.asmObj, "$WORK", st.work)
|
||||
st.pkgArch = strings.ReplaceAll(st.pkgArch, "$WORK", st.work)
|
||||
return st
|
||||
}
|
||||
|
||||
// substituteAndRelink swaps the gasm object into the package archive the
|
||||
// baseline build produced and re-runs the captured link line against the
|
||||
// rebuilt archive, writing the binary to outBin (the -x log's link step
|
||||
// always targets the action graph's internal a.out, which the helper
|
||||
// redirects; the copy to the -o target is a separate build action the helper
|
||||
// does not need). The archive handed to the linker is proven to carry the
|
||||
// gasm object byte for byte, so a build-layout change that skipped the
|
||||
// substitution fails here instead of passing vacuously.
|
||||
func substituteAndRelink(t *testing.T, goBin, dir string, st buildLogSteps, outBin string, gasmObj []byte, extraEnv ...string) {
|
||||
t.Helper()
|
||||
|
||||
// The deliberate-run boundary: this path drives a real `go build` and
|
||||
// cmd/link per invocation, minutes-scale work on the small single-core
|
||||
// CI runner. Under -short (the push pipeline's mode) it skips; the
|
||||
// local test gate and the dispatched workflows run it in full.
|
||||
if testing.Short() {
|
||||
t.Skip("end-to-end go build and link: skipped in -short mode")
|
||||
}
|
||||
|
||||
// Extract the archive, overwrite the assembler's member with the gasm
|
||||
// object and repack (go tool pack has no replace-in-place).
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", st.pkgArch)
|
||||
extract.Dir = membersDir
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
member := filepath.Join(membersDir, filepath.Base(st.asmObj))
|
||||
if _, err := os.Stat(member); err != nil {
|
||||
t.Fatalf("the assembler's archive member was not extracted: %v", err)
|
||||
}
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, gasmObj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", st.pkgArch)
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, m)
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Prove the substitution: the archive the linker is about to consume
|
||||
// holds the gasm object, byte for byte.
|
||||
checkDir := filepath.Join(dir, "check")
|
||||
if err := os.MkdirAll(checkDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := exec.Command(goBin, "tool", "pack", "x", newArch)
|
||||
check.Dir = checkDir
|
||||
if out, err := check.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x (verification): %v\n%s", err, out)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(checkDir, filepath.Base(st.asmObj)))
|
||||
if err != nil {
|
||||
t.Fatalf("read the substituted member back: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, gasmObj) {
|
||||
t.Fatal("the repacked archive does not carry the gasm object")
|
||||
}
|
||||
|
||||
// Re-link. The line carries a GOROOT assignment and $WORK placeholders;
|
||||
// GOEXPERIMENT must match the toolchain's own, because the linker
|
||||
// compares the object header against its configuration.
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
linkLine := strings.ReplaceAll(st.linkLine, "$WORK", st.work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "exe", "a.out"), outBin)
|
||||
env := append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)))
|
||||
env = append(env, extraEnv...)
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
link.Env = env
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with the gasm object: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectExternalPackageLink is the cross-package end-to-end check: a
|
||||
// GOOBJ whose code references a real external package symbol (runtime's
|
||||
// morestack, a plain reference rather than the builtin noctxt form) must
|
||||
@@ -524,6 +663,9 @@ func fieldAfter(line, flag string) string {
|
||||
// failed. The binary is not run: morestack returns to the call site's
|
||||
// stack check, which a hand-written caller has none of.
|
||||
func TestGOObjectExternalPackageLink(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("end-to-end go build and link: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64,
|
||||
|
||||
+200
-7
@@ -90,6 +90,40 @@ var noOperandTable = map[string][]byte{
|
||||
"LOCK": {0xF0},
|
||||
"REP": {0xF3},
|
||||
"REPN": {0xF2},
|
||||
"ENDBR64": {0xF3, 0x0F, 0x1E, 0xFA},
|
||||
}
|
||||
|
||||
// sysUnaryTable maps the one-operand system instructions to their bytes:
|
||||
// the prefix, the opcode and the /digit the reg field carries. The operand
|
||||
// is a register or memory in r/m.
|
||||
var sysUnaryTable = map[string]struct {
|
||||
prefix byte
|
||||
opcode []byte
|
||||
digit int
|
||||
}{
|
||||
"CLWB": {0x66, []byte{0x0F, 0xAE}, 6},
|
||||
"TPAUSE": {0x66, []byte{0x0F, 0xAE}, 6},
|
||||
"UMONITOR": {0xF3, []byte{0x0F, 0xAE}, 6},
|
||||
"UMWAIT": {0xF2, []byte{0x0F, 0xAE}, 6},
|
||||
"RDPID": {0xF3, []byte{0x0F, 0xC7}, 7},
|
||||
"CLDEMOTE": {0x00, []byte{0x0F, 0x1C}, 0},
|
||||
}
|
||||
|
||||
// encodeSysUnary emits a one-operand system instruction: the operand in r/m
|
||||
// under the fixed /digit, no REX.W.
|
||||
func (e *enc) encodeSysUnary(mnem string, m struct {
|
||||
prefix byte
|
||||
opcode []byte
|
||||
digit int
|
||||
}, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: m.opcode, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- MOV --------------------------------------------------------------------
|
||||
@@ -111,6 +145,76 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
||||
// silently emit REX.W 8B with the wrong operand meaning.
|
||||
_, srcVec := vecReg(src)
|
||||
dstReg, dstVec := vecReg(dst)
|
||||
|
||||
// Control and debug register moves: 0F 20 (CRn→r64), 0F 22 (r64→CRn),
|
||||
// 0F 21 (DRn→r64) and 0F 23 (r64→DRn). The CR/DR number rides the reg
|
||||
// field, the general register r/m; CR8+/DR8+ take REX.R.
|
||||
if c, ok := src.(Reg); ok && c.ctl != 0 {
|
||||
g, ok := dst.(Reg)
|
||||
if !ok || g.isVec() || g.ctl != 0 {
|
||||
return fmt.Errorf("MOV: control/debug register load needs a general register destination")
|
||||
}
|
||||
opc := byte(0x20)
|
||||
if c.ctl == 2 {
|
||||
opc = 0x21
|
||||
}
|
||||
return e.emit(&instr{
|
||||
opcode: []byte{0x0F, opc},
|
||||
modrm: 0xC0 | (c.idx&7)<<3 | (g.idx & 7),
|
||||
sib: -1, rexR: c.idx >= 8, rexB: g.idx >= 8,
|
||||
})
|
||||
}
|
||||
if c, ok := dst.(Reg); ok && c.ctl != 0 {
|
||||
g, ok := src.(Reg)
|
||||
if !ok || g.isVec() || g.ctl != 0 {
|
||||
return fmt.Errorf("MOV: control/debug register store needs a general register source")
|
||||
}
|
||||
opc := byte(0x22)
|
||||
if c.ctl == 2 {
|
||||
opc = 0x23
|
||||
}
|
||||
return e.emit(&instr{
|
||||
opcode: []byte{0x0F, opc},
|
||||
modrm: 0xC0 | (c.idx&7)<<3 | (g.idx & 7),
|
||||
sib: -1, rexR: c.idx >= 8, rexB: g.idx >= 8,
|
||||
})
|
||||
}
|
||||
|
||||
// MMX register moves: MOVQ M0, mem and MOVQ mem, M0 are the MMX
|
||||
// load/store pair 0F 6F/0F 7F (no prefix); a register pair takes the
|
||||
// load opcode. The XMM MOVQ forms follow below.
|
||||
if m, ok := src.(Reg); ok && m.mmx {
|
||||
switch d := dst.(type) {
|
||||
case Reg:
|
||||
if !d.mmx {
|
||||
return fmt.Errorf("MOV: MMX register moves stay inside the M bank")
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, 0x6F}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, d, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
case Mem:
|
||||
i := &instr{opcode: []byte{0x0F, 0x7F}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, m, d, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
return fmt.Errorf("MOV: invalid MMX destination")
|
||||
}
|
||||
if m, ok := dst.(Reg); ok && m.mmx {
|
||||
srcM, ok := src.(Mem)
|
||||
if !ok {
|
||||
return fmt.Errorf("MOV: MMX load takes a memory source")
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, 0x6F}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, m, srcM, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
if srcVec || dstVec {
|
||||
if dstVec {
|
||||
if g, ok := src.(Reg); ok && !g.isVec() {
|
||||
@@ -518,6 +622,14 @@ func (e *enc) encodeLea(ops []Operand, size int) error {
|
||||
default:
|
||||
return fmt.Errorf("LEA: source must be a memory operand")
|
||||
}
|
||||
// LEA accepts the full unsigned 32-bit displacement span where the
|
||||
// loads and stores reject it beyond the signed one; the wide values
|
||||
// ride the same disp32 bytes as their two's-complement bit pattern.
|
||||
if m, ok := src.(Mem); ok && m.Disp >= 1<<31 && m.Disp <= (1<<32)-1 {
|
||||
c := m
|
||||
c.Disp = int64(int32(uint32(m.Disp)))
|
||||
src = c
|
||||
}
|
||||
i := newInstr(size, []byte{0x8D})
|
||||
if err := setRM(i, dstReg, src, size); err != nil {
|
||||
return err
|
||||
@@ -663,6 +775,18 @@ func (e *enc) encodeDoubleShift(base string, ops []Operand, size int) error {
|
||||
|
||||
func (e *enc) encodeImul(ops []Operand, size int) error {
|
||||
switch len(ops) {
|
||||
case 1:
|
||||
// The one-operand form, IMUL r/m: F6/F7 /5 with AL/AX/EAX/RAX as the
|
||||
// implied destination (the toolchain's one-register shape).
|
||||
opc := byte(0xF7)
|
||||
if size == 1 {
|
||||
opc = 0xF6
|
||||
}
|
||||
i := newInstr(size, []byte{opc})
|
||||
if err := setRMDigit(i, 5, ops[0], size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
case 2:
|
||||
// Two shapes. The leading-immediate spelling IMUL $imm, r multiplies
|
||||
// r in place (dst = rm = r): the shape GOROOT's clock code writes.
|
||||
@@ -697,7 +821,7 @@ func (e *enc) encodeImul(ops []Operand, size int) error {
|
||||
// r/m operand (setRM takes registers and memory alike).
|
||||
return e.encodeImulImm(imm, ops[1], dstReg, size)
|
||||
}
|
||||
return fmt.Errorf("IMUL expects 2 or 3 operands, got %d", len(ops))
|
||||
return fmt.Errorf("IMUL expects 1, 2 or 3 operands, got %d", len(ops))
|
||||
}
|
||||
|
||||
// encodeImulImm emits the immediate multiply: 0x6B with a sign-extended imm8
|
||||
@@ -742,6 +866,27 @@ func (e *enc) encodePushPop(ops []Operand, size int, push bool) error {
|
||||
w16 := size == 2
|
||||
switch op := ops[0].(type) {
|
||||
case Reg:
|
||||
// Segment registers: FS and GS carry their own one-byte opcodes
|
||||
// under 0F (A0/A8 push, A1/A9 pop); the other four spellings are
|
||||
// not pushable in 64-bit mode.
|
||||
if n, isSeg := op.segNumber(); isSeg {
|
||||
switch n {
|
||||
case 4: // FS
|
||||
if push {
|
||||
return e.emit(&instr{opcode: []byte{0x0F, 0xA0}, modrm: -1, sib: -1})
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0x0F, 0xA1}, modrm: -1, sib: -1})
|
||||
case 5: // GS
|
||||
if push {
|
||||
return e.emit(&instr{opcode: []byte{0x0F, 0xA8}, modrm: -1, sib: -1})
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0x0F, 0xA9}, modrm: -1, sib: -1})
|
||||
}
|
||||
return fmt.Errorf("PUSH/POP: only FS and GS are encodable in 64-bit mode")
|
||||
}
|
||||
if op.mmx || op.isVec() || op.fp || op.ctl != 0 {
|
||||
return fmt.Errorf("PUSH/POP: invalid register operand")
|
||||
}
|
||||
base := byte(0x50) // PUSH r; POP is 0x58
|
||||
if !push {
|
||||
base = 0x58
|
||||
@@ -1093,6 +1238,38 @@ var sseMoveTable = map[string]sseMove{
|
||||
"MOVSS": {0xF3, 0x10, 0x11}, // scalar single
|
||||
}
|
||||
|
||||
// sseStoreOnly holds the store-only SSE forms, OP xmm, mem: the XMM register
|
||||
// rides the reg field and memory r/m (the non-temporal store).
|
||||
var sseStoreOnly = map[string]struct {
|
||||
prefix byte
|
||||
op byte
|
||||
}{
|
||||
"MOVNTDQ": {0x66, 0xE7},
|
||||
}
|
||||
|
||||
// encodeSSEStoreOnly encodes OP xmm, mem (reg = the XMM source, r/m = the
|
||||
// destination memory).
|
||||
func (e *enc) encodeSSEStoreOnly(mnem string, m struct {
|
||||
prefix byte
|
||||
op byte
|
||||
}, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
srcReg, ok := ops[0].(Reg)
|
||||
if !ok || !srcReg.isVec() {
|
||||
return fmt.Errorf("%s source must be a vector register", mnem)
|
||||
}
|
||||
if !isX86Mem(ops[1]) {
|
||||
return fmt.Errorf("%s destination must be a memory operand", mnem)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, srcReg, ops[1], 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSSEMove encodes a legacy SSE move: a vector-to-vector move uses the
|
||||
// load form (reg = destination), matching the Go assembler.
|
||||
func (e *enc) encodeSSEMove(m sseMove, ops []Operand) error {
|
||||
@@ -1308,14 +1485,23 @@ func (e *enc) encodeSSEBin(m sseBin, ops []Operand) error {
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
if !ok || (!dstReg.isVec() && !dstReg.mmx) {
|
||||
return fmt.Errorf("SSE binary destination must be a vector register")
|
||||
}
|
||||
// The MMX twins of the packed-integer SSE2 ops drop the 0x66 prefix:
|
||||
// PADDD M2, M1 is 0F FE where the XMM form is 66 0F FE.
|
||||
prefix := m.prefix
|
||||
if dstReg.mmx {
|
||||
if prefix != 0x66 {
|
||||
return fmt.Errorf("SSE binary: this form takes no MMX register operand")
|
||||
}
|
||||
prefix = 0
|
||||
}
|
||||
opcode := []byte{0x0F, m.op}
|
||||
if m.map38 {
|
||||
opcode = []byte{0x0F, 0x38, m.op}
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: opcode, modrm: -1, sib: -1}
|
||||
i := &instr{prefix: prefix, opcode: opcode, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1791,12 +1977,19 @@ func (e *enc) encodeSSEShift(name string, ops []Operand) error {
|
||||
// immediate LAST in Plan 9 order (src, dst, $imm), unlike the shuffle family:
|
||||
// F2 0F C2 with reg = dst, rm = src.
|
||||
func (e *enc) encodeCmpsd(ops []Operand) error {
|
||||
return e.encodeSSECmp("CMPSD", 0xF2, ops)
|
||||
}
|
||||
|
||||
// encodeSSECmp encodes the SSE compare family (CMPSD/CMPSS/CMPPS/CMPPD):
|
||||
// 0F C2 /r ib with the predicate immediate last in Plan 9 order
|
||||
// (src, dst, $imm) and the packed forms' prefixes.
|
||||
func (e *enc) encodeSSECmp(mnem string, prefix byte, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("CMPSD expects 3 operands (src, dst, $imm), got %d", len(ops))
|
||||
return fmt.Errorf("%s expects 3 operands (src, dst, $imm), got %d", mnem, len(ops))
|
||||
}
|
||||
imm, ok := ops[2].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("CMPSD predicate must be an immediate")
|
||||
return fmt.Errorf("%s predicate must be an immediate", mnem)
|
||||
}
|
||||
immByte, err := imm8(int64(imm))
|
||||
if err != nil {
|
||||
@@ -1804,9 +1997,9 @@ func (e *enc) encodeCmpsd(ops []Operand) error {
|
||||
}
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok2 || !dstReg.isVec() {
|
||||
return fmt.Errorf("CMPSD destination must be a vector register")
|
||||
return fmt.Errorf("%s destination must be a vector register", mnem)
|
||||
}
|
||||
i := &instr{prefix: 0xF2, opcode: []byte{0x0F, 0xC2}, modrm: -1, sib: -1}
|
||||
i := &instr{prefix: prefix, opcode: []byte{0x0F, 0xC2}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, ops[0], 8); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel;
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -86,7 +86,7 @@ func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The differential kernels for the DATA-path and front-end gaps are kept in
|
||||
@@ -23,9 +23,18 @@ import (
|
||||
// bytes must agree with the relocation sites masked on both sides.
|
||||
|
||||
// toolAsmObject assembles path with the installed toolchain's assembler for
|
||||
// goarch ("" = the host) and returns the object bytes.
|
||||
// goarch ("" = the host) and returns the object bytes. Every live-oracle
|
||||
// comparison funnels through here, so this is also where the deliberate-run
|
||||
// boundary sits: under -short (the push pipeline's mode) the comparisons
|
||||
// skip, because each spawns a go tool asm subprocess and the small single-
|
||||
// core runner pays seconds per spawn. The encodings stay pinned by the
|
||||
// golden-byte tests in every mode; the live oracle runs in the local test
|
||||
// gate and the dispatched workflows.
|
||||
func toolAsmObject(t *testing.T, path, goarch string) []byte {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("live go tool asm oracle: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||||
|
||||
+14
-4
@@ -9,7 +9,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Image is an assembled file: the function bodies laid out in source order,
|
||||
@@ -134,7 +134,8 @@ type DataSymbol struct {
|
||||
Offset int // byte offset within Data
|
||||
Size int
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Rodata bool // the RODATA flag: read-only data
|
||||
Rodata bool // the RODATA flag: read-only data (implies no pointers)
|
||||
Noptr bool // the NOPTR flag: data with no pointers, kept out of GC scanning
|
||||
Dupok bool // the DUPOK flag: duplicate-OK
|
||||
// Relocs carries the symbol-valued DATA initialisers ("DATA s+0(SB)/8,
|
||||
// $other(SB)"): fields of this symbol's data that hold another symbol's
|
||||
@@ -269,6 +270,7 @@ func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) {
|
||||
Size: len(d.buf),
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
@@ -413,6 +415,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
@@ -485,6 +488,7 @@ func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
@@ -546,6 +550,7 @@ type dataSym struct {
|
||||
size int
|
||||
static bool
|
||||
rodata bool
|
||||
noptr bool
|
||||
dupok bool
|
||||
// relocs are the symbol-valued DATA fields, in declaration order; Off
|
||||
// is relative to the symbol's data start.
|
||||
@@ -587,12 +592,14 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
switch f {
|
||||
case "RODATA":
|
||||
ds.rodata = true
|
||||
case "NOPTR":
|
||||
ds.noptr = true
|
||||
case "DUPOK":
|
||||
ds.dupok = true
|
||||
default:
|
||||
// Legacy numeric flag constants (runtime/textflag.h):
|
||||
// DUPOK is 2, RODATA is 8; combinations arrive as one
|
||||
// number (e.g. 10 = RODATA|DUPOK).
|
||||
// DUPOK is 2, RODATA is 8, NOPTR is 16; combinations arrive
|
||||
// as one number (e.g. 10 = RODATA|DUPOK).
|
||||
if n, err := strconv.Atoi(f); err == nil {
|
||||
if n&2 != 0 {
|
||||
ds.dupok = true
|
||||
@@ -600,6 +607,9 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
if n&8 != 0 {
|
||||
ds.rodata = true
|
||||
}
|
||||
if n&16 != 0 {
|
||||
ds.noptr = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-38
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestAssembleFileStaticData checks the whole-image layout; code, padding
|
||||
@@ -60,23 +60,15 @@ DATA small<>+0(SB)/4, $0x1234
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFileErrors checks the static-symbol error paths.
|
||||
// TestAssembleFileErrors checks the static-symbol error paths. A reference
|
||||
// to a static symbol no GLOBL defines defers to the linker exactly as the
|
||||
// toolchain does (an external relocation), so it is not an error here.
|
||||
func TestAssembleFileErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string // substring of the error
|
||||
}{
|
||||
{
|
||||
"undefined symbol",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
VMOVDQU nope<>(SB), X0
|
||||
RET
|
||||
`,
|
||||
"undefined symbol",
|
||||
},
|
||||
{
|
||||
"DATA without GLOBL",
|
||||
`
|
||||
@@ -369,23 +361,8 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" || linkLine == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
st := parseBuildLog(t, buildLog, "main_amd64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
// Assemble the same source with gasm and substitute the object.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_amd64.s"))
|
||||
@@ -400,21 +377,18 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObject("dlink", "main_amd64.s")
|
||||
// The package path is "main": the linker resolves the Go code's
|
||||
// references against main.<name>, so the object must define the symbols
|
||||
// under that prefix whatever the module is called.
|
||||
gasmObj, err := img.GOObject("main", "main_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"), gasmObj)
|
||||
|
||||
// The linked program must run and find the right function behind the
|
||||
// data word.
|
||||
out, err := exec.Command(filepath.Join(dir, "prog")).CombinedOutput()
|
||||
out, err := exec.Command(filepath.Join(dir, "prog2")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("linked program failed: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
+258
-7
@@ -9,7 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
|
||||
@@ -371,6 +371,13 @@ func loong64InstrSize(instr *ast.Instr, fi loong64FrameInfo) int {
|
||||
if mnem == "RET" {
|
||||
return len(loong64Return(fi))
|
||||
}
|
||||
// BYTE lays down one raw byte per operand, a front-end pseudo-op the
|
||||
// toolchain spells only on x86 but accepts here the same way the arm64
|
||||
// and riscv64 encoders do (a superset spelling, shippable via the goobj
|
||||
// path).
|
||||
if mnem == "BYTE" {
|
||||
return len(ops)
|
||||
}
|
||||
switch mnem {
|
||||
case "END", "FUNCDATA", "PCDATA":
|
||||
return 0 // bookkeeping statements contribute no bytes
|
||||
@@ -484,6 +491,18 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
||||
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
return l64wordLE(uint32(immFromOperand(ops[0]))), nil
|
||||
case "BYTE":
|
||||
// BYTE $b lays down one raw byte per operand, the same front-end
|
||||
// pseudo-op the arm64 and riscv64 encoders accept.
|
||||
var out []byte
|
||||
for _, op := range ops {
|
||||
b := l64Imm64(op)
|
||||
if b < 0 || b > 0xFF {
|
||||
return nil, fmt.Errorf("BYTE: immediate %d does not fit a byte", b)
|
||||
}
|
||||
out = append(out, byte(b))
|
||||
}
|
||||
return out, nil
|
||||
case "END", "FUNCDATA", "PCDATA", "GETCALLERPC":
|
||||
// The assembler's bookkeeping statements. END, FUNCDATA and PCDATA
|
||||
// contribute no bytes, the same shapes GOARCH=loong64 go tool asm
|
||||
@@ -701,6 +720,35 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
||||
}
|
||||
return l64wordLE(l64rr(enc.op, rj, rd)), nil
|
||||
|
||||
case l64Fllsc:
|
||||
// LLACQ{W,V} (Rj), Rd loads and SCREL{W,V} Rd, (Rj) stores, both
|
||||
// 2R encodings op | rj<<5 | rd against a zero-offset memory operand
|
||||
// (the toolchain's C_ZOREG, which rejects any displacement).
|
||||
rd, rj, off, _, err := l64MemOperands(ops, fi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", mnem, err)
|
||||
}
|
||||
if off != 0 {
|
||||
return nil, fmt.Errorf("%s: only a zero-offset memory operand is allowed", mnem)
|
||||
}
|
||||
return l64wordLE(l64rr(enc.op, rj, rd)), nil
|
||||
|
||||
case l64Fscq:
|
||||
// SCQ first, middle, (base): op | middle<<10 | base<<5 | first,
|
||||
// against a zero-offset memory operand as with the LL/SC pair.
|
||||
if len(ops) != 3 || !isMemOperand(ops[2]) || isMemOperand(ops[0]) || isMemOperand(ops[1]) {
|
||||
return nil, fmt.Errorf("%s expects reg, reg, (reg)", mnem)
|
||||
}
|
||||
first, middle := l64Reg(ops[0]), l64Reg(ops[1])
|
||||
rj, off := l64MemWithFrame(ops[2], fi)
|
||||
if first < 0 || middle < 0 || rj < 0 {
|
||||
return nil, fmt.Errorf("%s: invalid register operand", mnem)
|
||||
}
|
||||
if off != 0 {
|
||||
return nil, fmt.Errorf("%s: only a zero-offset memory operand is allowed", mnem)
|
||||
}
|
||||
return l64wordLE(l64rrr(enc.op, middle, rj, first)), nil
|
||||
|
||||
case l64Firr:
|
||||
// LU52ID: INSTR $imm, rd or INSTR $imm, rj, rd.
|
||||
if len(ops) < 2 || !isImmOperand(ops[0]) {
|
||||
@@ -1229,6 +1277,31 @@ func l64MemOperands(ops []*ast.Operand, fi loong64FrameInfo) (rd, rj int, off in
|
||||
return rd, rj, off, load, nil
|
||||
}
|
||||
|
||||
// l64ImmMem reads the `$off(rj)` immediate form off an operand's raw text:
|
||||
// the shared immediate parse reduces it to the bare number and keeps only
|
||||
// the text as a witness of the base register. ok reports the form was
|
||||
// found, with the base's register number (or -1 when the name is not a
|
||||
// general register).
|
||||
func l64ImmMem(op *ast.Operand) (off int32, base int, ok bool) {
|
||||
if op.Kind != ast.OpImmediate || !op.Imm.HasVal {
|
||||
return 0, 0, false
|
||||
}
|
||||
raw := strings.ReplaceAll(op.Raw, " ", "")
|
||||
if !strings.HasPrefix(raw, "$") || !strings.HasSuffix(raw, ")") {
|
||||
return 0, 0, false
|
||||
}
|
||||
open := strings.LastIndexByte(raw, '(')
|
||||
if open < 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
base = loong64RegNum(raw[open+1 : len(raw)-1])
|
||||
v := op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
return int32(v), base, base >= 0
|
||||
}
|
||||
|
||||
// ---- the MOV pseudo-instruction ----
|
||||
|
||||
// encodeLOONG64Mov encodes the MOV family, the load/store/immediate
|
||||
@@ -1262,6 +1335,29 @@ func encodeLOONG64Mov(instr *ast.Instr, mnem string, fi loong64FrameInfo, relocs
|
||||
}
|
||||
return encodeLOONG64SBAddr(src.Imm.Sym, rd, relocs), nil
|
||||
}
|
||||
// MOVx $off(rj), rd computes an address: the toolchain's `mov
|
||||
// $soreg, r` case, a plain addi.d whatever the move's width (both
|
||||
// MOVW and MOVV $4(R4), R5 encode the same addi.d in its testdata).
|
||||
// A wider offset materialises in R30 first (lu12i.w + ori + add.d,
|
||||
// its case 10). The immediate's Raw carries the base register,
|
||||
// which the shared immediate parse reduces to the bare number.
|
||||
if off, base, ok := l64ImmMem(src); ok {
|
||||
rd := l64Reg(dst)
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("%s $imm(rj): invalid destination register", mnem)
|
||||
}
|
||||
if loong64RegClass(operandRegName(dst)) == l64ClsFP {
|
||||
return nil, fmt.Errorf("%s $imm(rj): illegal combination with an F register destination", mnem)
|
||||
}
|
||||
if off >= -2048 && off <= 2047 {
|
||||
return l64wordLE(l64irr(l64DualTable["ADDV"].imm, int(off), base, rd)), nil
|
||||
}
|
||||
return l64WordsLE(
|
||||
l64ir(l64InstrTable["LU12IW"].op, int(off)>>12, 30),
|
||||
l64irr(l64DualTable["OR"].imm, int(off)&0xFFF, 30, 30),
|
||||
l64rrr(l64DualTable["ADDV"].rrr, 30, base, rd),
|
||||
), nil
|
||||
}
|
||||
rd := l64Reg(dst)
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("%s $imm: invalid destination register", mnem)
|
||||
@@ -1356,6 +1452,14 @@ func loong64MovSize(mnem string, ops []*ast.Operand, fi loong64FrameInfo) int {
|
||||
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
||||
return 8 // pcalau12i + addi.d
|
||||
}
|
||||
// The $off(rj) address immediate: addi.d in the 12-bit window,
|
||||
// lu12i.w + ori + add.d beyond it (the toolchain's case 10).
|
||||
if off, _, ok := l64ImmMem(src); ok {
|
||||
if off >= -2048 && off <= 2047 {
|
||||
return 4
|
||||
}
|
||||
return 12
|
||||
}
|
||||
if loong64RegClass(operandRegName(dst)) == l64ClsFP {
|
||||
return 8 // ori/addi.w r30 + movgr2fr.w (an encode-time diagnostic when invalid)
|
||||
}
|
||||
@@ -1868,6 +1972,19 @@ func l64MemWithFrame(op *ast.Operand, fi loong64FrameInfo) (rj int, off int32) {
|
||||
return l64Mem(op)
|
||||
}
|
||||
|
||||
// l64VmovqMem resolves a VMOVQ/XVMOVQ memory operand. The toolchain's
|
||||
// vector table falls back to the zero register as the FP-relative base
|
||||
// (`VMOVQ V2, y+16(FP)` stores through R0 while MOVW reads the same operand
|
||||
// through R3), so the vector moves keep the resolved offset but the zero
|
||||
// base, exactly as `go tool asm` emits them.
|
||||
func l64VmovqMem(op *ast.Operand, fi loong64FrameInfo) (rj int, off int32) {
|
||||
rj, off = l64MemWithFrame(op, fi)
|
||||
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "FP" {
|
||||
rj = 0
|
||||
}
|
||||
return rj, off
|
||||
}
|
||||
|
||||
// l64MemOffset returns the resolved byte offset of a memory operand.
|
||||
func l64MemOffset(op *ast.Operand, fi loong64FrameInfo) int32 {
|
||||
_, off := l64MemWithFrame(op, fi)
|
||||
@@ -1892,7 +2009,7 @@ func l64Label(op *ast.Operand) string {
|
||||
type l64VecOperand struct {
|
||||
num int // 5-bit register number
|
||||
lasx bool // X bank (LASX) rather than V (LSX)
|
||||
width byte // suffix width letter (B/H/W/V), 0 on a bare register
|
||||
width byte // suffix width letter (B/H/W/V/Q), 0 on a bare register
|
||||
lanes int // lane count of a width suffix (B16 → 16)
|
||||
elem int // element index of a .T[i] suffix
|
||||
hasEl bool // the suffix names an element (.T[i])
|
||||
@@ -1932,7 +2049,7 @@ func l64ParseVecOperand(op *ast.Operand) (v l64VecOperand, ok bool) {
|
||||
}
|
||||
i++
|
||||
w := name[i]
|
||||
if w != 'B' && w != 'H' && w != 'W' && w != 'V' {
|
||||
if w != 'B' && w != 'H' && w != 'W' && w != 'V' && w != 'Q' {
|
||||
return v, false
|
||||
}
|
||||
v.width, v.hasSuf = w, true
|
||||
@@ -2174,6 +2291,10 @@ func encodeLOONG64Vector(instr *ast.Instr, mnem string, fi loong64FrameInfo) ([]
|
||||
// VMOVQ rj, vd.T vreplgr2vr (duplicate a general register)
|
||||
// VMOVQ vj.T[i], rd vpickve2gr (extract one element)
|
||||
// VMOVQ rj, vd.T[i] vinsgr2vr (insert one element)
|
||||
// VMOVQ vj.T[i], vd.T vreplvei (broadcast one element, LSX)
|
||||
// XVMOVQ xj, xd.T xvreplve0 (broadcast element zero, LASX)
|
||||
// XVMOVQ xj, xd.T[i] xvinsve0 (insert element zero, LASX)
|
||||
// XVMOVQ xj.T[i], xd xvpickve (extract one element, LASX)
|
||||
func encodeLOONG64Vmovq(lasx bool, ops []*ast.Operand, fi loong64FrameInfo) ([]byte, error) {
|
||||
enc := l64VmovqTable[lasx]
|
||||
bank := "V"
|
||||
@@ -2200,6 +2321,118 @@ func encodeLOONG64Vmovq(lasx bool, ops []*ast.Operand, fi loong64FrameInfo) ([]b
|
||||
return loong64RegNum(name), nil
|
||||
}
|
||||
|
||||
// Element broadcast: VMOVQ vj.T[i], vd.T (vreplvei.{b,h,w,d}), the
|
||||
// source element width matching the destination arrangement. An LSX-only
|
||||
// form: the toolchain's table gives vreplvei no LASX counterpart.
|
||||
if srcVec && dstVec && src.hasEl && dst.hasSuf && !dst.hasEl {
|
||||
if lasx || src.lasx || dst.lasx {
|
||||
return nil, fmt.Errorf("VMOVQ: vreplvei has no %s-bank form", bank)
|
||||
}
|
||||
if src.unsig {
|
||||
return nil, fmt.Errorf("VMOVQ: vreplvei takes no unsigned element suffix")
|
||||
}
|
||||
if src.width != dst.width {
|
||||
return nil, fmt.Errorf("VMOVQ: element width does not match arrangement %q", ops[1].Raw)
|
||||
}
|
||||
if _, ok := l64VecSuffixWidth(false, dst); !ok {
|
||||
return nil, fmt.Errorf("VMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
var op uint32
|
||||
limit := 0
|
||||
switch src.width {
|
||||
case 'B':
|
||||
op, limit = enc.rveiB, 15
|
||||
case 'H':
|
||||
op, limit = enc.rveiH, 7
|
||||
case 'W':
|
||||
op, limit = enc.rveiW, 3
|
||||
default:
|
||||
op, limit = enc.rveiD, 1
|
||||
}
|
||||
if src.elem > limit {
|
||||
return nil, fmt.Errorf("VMOVQ: element index %d out of range [0, %d]", src.elem, limit)
|
||||
}
|
||||
return l64wordLE(op | uint32(src.elem)<<10 | uint32(src.num)<<5 | uint32(dst.num)), nil
|
||||
}
|
||||
|
||||
// Broadcast of element zero: XVMOVQ xj, xd.T (xvreplve0.{b,h,w,d,q}),
|
||||
// a bare X source into an arranged X destination. LASX only.
|
||||
if srcVec && dstVec && !src.hasSuf && dst.hasSuf && !dst.hasEl {
|
||||
if !lasx || src.lasx != lasx || dst.lasx != lasx {
|
||||
return nil, fmt.Errorf("XVMOVQ: xvreplve0 is the %s-bank form alone", bank)
|
||||
}
|
||||
var op uint32
|
||||
switch dst.width {
|
||||
case 'B':
|
||||
if dst.lanes != 32 {
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
op = enc.rve0B
|
||||
case 'H':
|
||||
if dst.lanes != 16 {
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
op = enc.rve0H
|
||||
case 'W':
|
||||
if dst.lanes != 8 {
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
op = enc.rve0W
|
||||
case 'V':
|
||||
if dst.lanes != 4 {
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
op = enc.rve0D
|
||||
case 'Q':
|
||||
if dst.lanes != 2 {
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
op = enc.rve0Q
|
||||
default:
|
||||
return nil, fmt.Errorf("XVMOVQ: invalid arrangement %q", ops[1].Raw)
|
||||
}
|
||||
return l64wordLE(op | uint32(src.num)<<5 | uint32(dst.num)), nil
|
||||
}
|
||||
|
||||
// Insert of element zero: XVMOVQ xj, xd.T[i] (xvinsve0.{w,d}), a bare X
|
||||
// source into one word or double-word lane. LASX only.
|
||||
if srcVec && dstVec && !src.hasSuf && dst.hasEl {
|
||||
if !lasx || src.lasx != lasx || dst.lasx != lasx {
|
||||
return nil, fmt.Errorf("XVMOVQ: xvinsve0 is the %s-bank form alone", bank)
|
||||
}
|
||||
op, limit := enc.xinsW, 7
|
||||
if dst.width != 'W' {
|
||||
op, limit = enc.xinsD, 3
|
||||
if dst.width != 'V' {
|
||||
return nil, fmt.Errorf("XVMOVQ: xvinsve0 takes word or double-word lanes, got %q", ops[1].Raw)
|
||||
}
|
||||
}
|
||||
if dst.elem > limit {
|
||||
return nil, fmt.Errorf("XVMOVQ: element index %d out of range [0, %d]", dst.elem, limit)
|
||||
}
|
||||
return l64wordLE(op | uint32(dst.elem)<<10 | uint32(src.num)<<5 | uint32(dst.num)), nil
|
||||
}
|
||||
|
||||
// Element extract into a vector register: XVMOVQ xj.T[i], xd
|
||||
// (xvpickve.{w,d}), one word or double-word lane out to a bare X
|
||||
// register. LASX only.
|
||||
if srcVec && src.hasEl && dstVec && !dst.hasSuf {
|
||||
if !lasx || src.lasx != lasx || dst.lasx != lasx {
|
||||
return nil, fmt.Errorf("XVMOVQ: xvpickve is the %s-bank form alone", bank)
|
||||
}
|
||||
op, limit := enc.xpickW, 7
|
||||
if src.width != 'W' {
|
||||
op, limit = enc.xpickD, 3
|
||||
if src.width != 'V' {
|
||||
return nil, fmt.Errorf("XVMOVQ: xvpickve takes word or double-word lanes, got %q", ops[0].Raw)
|
||||
}
|
||||
}
|
||||
if src.elem > limit {
|
||||
return nil, fmt.Errorf("XVMOVQ: element index %d out of range [0, %d]", src.elem, limit)
|
||||
}
|
||||
return l64wordLE(op | uint32(src.elem)<<10 | uint32(src.num)<<5 | uint32(dst.num)), nil
|
||||
}
|
||||
|
||||
// Register move: VMOVQ vj, vd (vori.b/xvori.b with the zero constant),
|
||||
// both operands bare registers of the same bank.
|
||||
if srcVec && dstVec {
|
||||
@@ -2224,7 +2457,7 @@ func encodeLOONG64Vmovq(lasx bool, ops []*ast.Operand, fi loong64FrameInfo) ([]b
|
||||
}
|
||||
return l64wordLE(l64rrr(enc.stx, rk, rj, src.num)), nil
|
||||
}
|
||||
rj, off := l64MemWithFrame(ops[1], fi)
|
||||
rj, off := l64VmovqMem(ops[1], fi)
|
||||
if rj < 0 || off < -2048 || off > 2047 {
|
||||
return nil, fmt.Errorf("VMOVQ: store offset out of range [-2048, 2047]")
|
||||
}
|
||||
@@ -2247,9 +2480,9 @@ func encodeLOONG64Vmovq(lasx bool, ops []*ast.Operand, fi loong64FrameInfo) ([]b
|
||||
}
|
||||
return l64wordLE(l64rrr(enc.ldx, rk, rj, dst.num)), nil
|
||||
}
|
||||
rj, off := l64MemWithFrame(ops[0], fi)
|
||||
if rj < 0 || off < -2048 || off > 2047 {
|
||||
return nil, fmt.Errorf("VMOVQ: load offset out of range [-2048, 2047]")
|
||||
rj, off := l64VmovqMem(ops[0], fi)
|
||||
if rj < 0 {
|
||||
return nil, fmt.Errorf("VMOVQ: invalid load operand")
|
||||
}
|
||||
op := enc.ld
|
||||
if dst.hasSuf {
|
||||
@@ -2257,16 +2490,34 @@ func encodeLOONG64Vmovq(lasx bool, ops []*ast.Operand, fi loong64FrameInfo) ([]b
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("VMOVQ: invalid replicate width suffix %q", ops[1].Raw)
|
||||
}
|
||||
// vldrepl keeps the byte offset raw for bytes and scales it by
|
||||
// the element width for the wider forms, the immediate field
|
||||
// shrinking a bit per scale exactly as the toolchain encodes it
|
||||
// (the field mask keeps the two's complement inside its width).
|
||||
scale, mask, lo, hi := 1, int32(0xFFF), -2048, 2047
|
||||
switch w {
|
||||
case 0:
|
||||
op = enc.replB
|
||||
case 1:
|
||||
op = enc.replH
|
||||
scale, mask, lo, hi = 2, 0x7FF, -1024, 1023
|
||||
case 2:
|
||||
op = enc.replW
|
||||
scale, mask, lo, hi = 4, 0x3FF, -512, 511
|
||||
default:
|
||||
op = enc.replD
|
||||
scale, mask, lo, hi = 8, 0x1FF, -256, 255
|
||||
}
|
||||
if off%int32(scale) != 0 {
|
||||
return nil, fmt.Errorf("VMOVQ: offset %d must be a multiple of %d", off, scale)
|
||||
}
|
||||
off /= int32(scale)
|
||||
if off < int32(lo) || off > int32(hi) {
|
||||
return nil, fmt.Errorf("VMOVQ: offset out of range [%d, %d]", lo*scale, hi*scale)
|
||||
}
|
||||
off &= mask
|
||||
} else if off < -2048 || off > 2047 {
|
||||
return nil, fmt.Errorf("VMOVQ: load offset out of range [-2048, 2047]")
|
||||
}
|
||||
return l64wordLE(l64irr(op, int(off), rj, dst.num)), nil
|
||||
}
|
||||
|
||||
+32
-5
@@ -279,6 +279,8 @@ const (
|
||||
l64Fvvv // 3R vector (LSX/LASX): op | vk<<10 | vj<<5 | vd
|
||||
l64Fvcf // vector-to-condition: op | subop<<10 | vj<<5 | fcc
|
||||
l64Fvvvv // 4R vector shuffle: op | va<<15 | vk<<10 | vj<<5 | vd
|
||||
l64Fllsc // acquire/release LL/SC (2R against a zero-offset memory operand)
|
||||
l64Fscq // sc.q: op | middle<<10 | base<<5 | first against a zero-offset memory operand
|
||||
)
|
||||
|
||||
// l64Enc is one instruction's encoding: its bit layout (format) and the
|
||||
@@ -341,8 +343,9 @@ var l64Vec2R = map[string]bool{}
|
||||
// such as vshuf.b).
|
||||
var l64Vec4R = map[string]bool{}
|
||||
|
||||
// l64VmovqOps holds the VMOVQ/XVMOVQ opcode constants (pre-shifted to bit
|
||||
// 15), read off `go tool objdump` of GOARCH=loong64 `go tool asm` kernels.
|
||||
// l64VmovqOps holds the VMOVQ/XVMOVQ opcode constants, each pre-shifted to
|
||||
// its exact bit range, read off `go tool objdump` of GOARCH=loong64
|
||||
// `go tool asm` kernels and the toolchain's specialLsxMovInst table.
|
||||
type l64VmovqEnc struct {
|
||||
ld, st, ldx, stx uint32 // plain and indexed load/store
|
||||
replB, replH, replW, replD uint32 // vldrepl: load and replicate element
|
||||
@@ -350,6 +353,11 @@ type l64VmovqEnc struct {
|
||||
ins uint32 // vinsgr2vr element insert
|
||||
dup uint32 // vreplgr2vr duplicate (width in [11:10])
|
||||
move uint32 // vori.b/xvori.b $0 register move
|
||||
rveiB, rveiH, rveiW, rveiD uint32 // vreplvei: broadcast one element (LSX)
|
||||
rve0B, rve0H, rve0W uint32 // xvreplve0 broadcast of element zero (LASX)
|
||||
rve0D, rve0Q uint32 // xvreplve0.{d,q}, ditto
|
||||
xinsW, xinsD uint32 // xvinsve0: insert element zero (LASX)
|
||||
xpickW, xpickD uint32 // xvpickve: extract element (LASX)
|
||||
}
|
||||
|
||||
var l64VmovqTable = map[bool]l64VmovqEnc{
|
||||
@@ -358,12 +366,17 @@ var l64VmovqTable = map[bool]l64VmovqEnc{
|
||||
replB: 0x6100 << 15, replH: 0x6080 << 15, replW: 0x6040 << 15, replD: 0x6020 << 15,
|
||||
pickS: 0xE5DF << 15, pickU: 0xE5E7 << 15,
|
||||
ins: 0xE5D7 << 15, dup: 0xE53E << 15, move: 0xE65A << 15,
|
||||
rveiB: 0x01CBDE << 14, rveiH: 0x0397BE << 13, rveiW: 0x072F7E << 12, rveiD: 0x0E5EFE << 11,
|
||||
},
|
||||
true: { // XVMOVQ, the LASX (X) bank
|
||||
ld: 0x5900 << 15, st: 0x5980 << 15, ldx: 0x7090 << 15, stx: 0x7098 << 15,
|
||||
replB: 0x6500 << 15, replH: 0x6480 << 15, replW: 0x6440 << 15, replD: 0x6420 << 15,
|
||||
pickS: 0xEDDF << 15, pickU: 0xEDE7 << 15,
|
||||
ins: 0xEDD7 << 15, dup: 0xED3E << 15, move: 0xEE5A << 15,
|
||||
rve0B: 0x1DC1C0 << 10, rve0H: 0x1DC1E0 << 10, rve0W: 0x1DC1F0 << 10,
|
||||
rve0D: 0x1DC1F8 << 10, rve0Q: 0x1DC1FC << 10,
|
||||
xinsW: 0x03B7FE << 13, xinsD: 0x076FFE << 12,
|
||||
xpickW: 0x03B81E << 13, xpickD: 0x07703E << 12,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -373,7 +386,7 @@ func init() {
|
||||
"ADD": 0x20 << 15, "ADDW": 0x20 << 15, "ADDV": 0x21 << 15, "ADDVU": 0x21 << 15,
|
||||
"SUB": 0x22 << 15, "SUBW": 0x22 << 15, "SUBV": 0x23 << 15, "SUBVU": 0x23 << 15,
|
||||
"SGT": 0x24 << 15, "SGTU": 0x25 << 15,
|
||||
"MASKEQZ": 0x26 << 15, "MASKNEZ": 0x27 << 15, "SCQ": 0x070AE << 15,
|
||||
"MASKEQZ": 0x26 << 15, "MASKNEZ": 0x27 << 15,
|
||||
"NOR": 0x28 << 15, "AND": 0x29 << 15, "OR": 0x2a << 15, "XOR": 0x2b << 15,
|
||||
"ORN": 0x2c << 15, "ANDN": 0x2d << 15,
|
||||
"SLL": 0x2e << 15, "SRL": 0x2f << 15, "SRA": 0x30 << 15,
|
||||
@@ -467,6 +480,20 @@ func init() {
|
||||
l64InstrTable["RDTIMEHW"] = l64Enc{format: l64Frdtime, op: 0x19 << 10}
|
||||
l64InstrTable["RDTIMED"] = l64Enc{format: l64Frdtime, op: 0x1a << 10}
|
||||
|
||||
// Acquire/release LL/SC (2R against a zero-offset memory operand):
|
||||
// LLACQV (Rj), Rd loads, SCRELV Rd, (Rj) stores, both encoding
|
||||
// op | rj<<5 | rd. Opcodes from cmd/internal/obj/loong64/instOp.go
|
||||
// (ll.acq.{w,d}, sc.rel.{w,d}).
|
||||
l64InstrTable["LLACQW"] = l64Enc{format: l64Fllsc, op: 0x0E15E0 << 10}
|
||||
l64InstrTable["SCRELW"] = l64Enc{format: l64Fllsc, op: 0x0E15E1 << 10}
|
||||
l64InstrTable["LLACQV"] = l64Enc{format: l64Fllsc, op: 0x0E15E2 << 10}
|
||||
l64InstrTable["SCRELV"] = l64Enc{format: l64Fllsc, op: 0x0E15E3 << 10}
|
||||
|
||||
// SCQ (sc.q first, middle, (base)) keeps its own operand order: the
|
||||
// encoding is op | middle<<10 | base<<5 | first, the memory operand's
|
||||
// base in the rj field, not the toolchain's generic 3R layout.
|
||||
l64InstrTable["SCQ"] = l64Enc{format: l64Fscq, op: 0x070AE << 15}
|
||||
|
||||
// The dual-form arithmetic mnemonics (register 3R + immediate 2RI12),
|
||||
// selected by the operand kind; the shift mnemonics pair the 3R form
|
||||
// with a 5/6-bit shift immediate.
|
||||
@@ -868,7 +895,7 @@ func init() {
|
||||
"VNORB": {0xE7B8 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVNORB": {0xEFB8 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VSEQB": {0xE500 << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSEQB": {0xE900 << 15, true, -16, 15, 0, 0x1F},
|
||||
"XVSEQB": {0xED00 << 15, true, -16, 15, 0, 0x1F},
|
||||
// vseqi.h/w accept the same si5 window as vseqi.b; vseqi.d carries a
|
||||
// 7-bit field, but the toolchain range-checks it down to si5 as well
|
||||
// (GOARCH=loong64 go tool asm rejects VSEQV $32 and VSEQV $-64).
|
||||
@@ -877,7 +904,7 @@ func init() {
|
||||
"VSEQW": {0xE502 << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSEQW": {0xED02 << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSEQV": {0xE503 << 15, false, -16, 15, 0, 0x7F},
|
||||
"XVSEQV": {0xE903 << 15, true, -16, 15, 0, 0x7F},
|
||||
"XVSEQV": {0xED03 << 15, true, -16, 15, 0, 0x7F},
|
||||
// vslti compares against a signed (or, in the U spellings, unsigned)
|
||||
// si5/ui5 constant.
|
||||
"VSLTB": {0xE50C << 15, false, -16, 15, 0, 0x1F},
|
||||
|
||||
+229
-2
@@ -8,8 +8,8 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
|
||||
@@ -831,3 +831,230 @@ TEXT ·atoms(SB), NOSPLIT, $0
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_llacqScrel pins the acquire/release LL/SC pair. The oracle
|
||||
// words come from GOARCH=loong64 go tool objdump and the toolchain's own
|
||||
// loong64enc1.s golden bytes.
|
||||
func TestLOONG64_llacqScrel(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·llsc(SB), NOSPLIT, $0
|
||||
LLACQW (R5), R4
|
||||
LLACQV (R5), R4
|
||||
SCRELW R4, (R6)
|
||||
SCRELV R4, (R6)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x385780A4, // ll.acq.w r4, r5
|
||||
0x385788A4, // ll.acq.d r4, r5
|
||||
0x385784C4, // sc.rel.w r4, r6
|
||||
0x38578CC4, // sc.rel.d r4, r6
|
||||
0x4C000020,
|
||||
)
|
||||
// The toolchain accepts the zero-offset memory form alone.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
LLACQW 4(R5), R4
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
SCRELV R4, 8(R6)
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_vmovqSuffixed pins the element-broadcast and element-move
|
||||
// VMOVQ/XVMOVQ forms, with the oracle words lifted verbatim from the
|
||||
// toolchain's loong64enc1.s.
|
||||
func TestLOONG64_vmovqSuffixed(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·vmovq(SB), NOSPLIT, $0
|
||||
VMOVQ V1.B[3], V9.B16
|
||||
VMOVQ V2.H[2], V8.H8
|
||||
VMOVQ V3.W[1], V7.W4
|
||||
VMOVQ V4.V[0], V6.V2
|
||||
XVMOVQ X0, X31.B32
|
||||
XVMOVQ X1, X30.H16
|
||||
XVMOVQ X2, X29.W8
|
||||
XVMOVQ X3, X28.V4
|
||||
XVMOVQ X3, X27.Q2
|
||||
XVMOVQ X0, X31.W[7]
|
||||
XVMOVQ X1, X29.W[0]
|
||||
XVMOVQ X3, X28.V[3]
|
||||
XVMOVQ X4, X27.V[0]
|
||||
XVMOVQ X31.W[7], X0
|
||||
XVMOVQ X29.W[0], X1
|
||||
XVMOVQ X28.V[3], X8
|
||||
XVMOVQ X27.V[0], X9
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x72F78C29, // vreplvei.b v9, v1, 3
|
||||
0x72F7C848, // vreplvei.h v8, v2, 2
|
||||
0x72F7E467, // vreplvei.w v7, v3, 1
|
||||
0x72F7F086, // vreplvei.d v6, v4, 0
|
||||
0x7707001F, // xvreplve0.b x31, x0
|
||||
0x7707803E, // xvreplve0.h x30, x1
|
||||
0x7707C05D, // xvreplve0.w x29, x2
|
||||
0x7707E07C, // xvreplve0.d x28, x3
|
||||
0x7707F07B, // xvreplve0.q x27, x3
|
||||
0x76FFDC1F, // xvinsve0.w x31, x0, 7
|
||||
0x76FFC03D, // xvinsve0.w x29, x1, 0
|
||||
0x76FFEC7C, // xvinsve0.d x28, x3, 3
|
||||
0x76FFE09B, // xvinsve0.d x27, x4, 0
|
||||
0x7703DFE0, // xvpickve.w x0, x31, 7
|
||||
0x7703C3A1, // xvpickve.w x1, x29, 0
|
||||
0x7703EF88, // xvpickve.d x8, x28, 3
|
||||
0x7703E369, // xvpickve.d x9, x27, 0
|
||||
0x4C000020,
|
||||
)
|
||||
// The rejected shapes: a width mismatch between the element and the
|
||||
// arrangement, an element index past the lane count, a wrong-bank
|
||||
// vreplvei and an arrangement the LASX bank does not spell.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ V1.H[3], V9.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ V1.B[16], V9.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X1.B[3], X9.B32
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X0, X31.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X0, X31.W[8]
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_parityFixes pins the operand forms whose encodings were found
|
||||
// diverging from the toolchain by the loong64enc1.s differential: the
|
||||
// $off(reg) address immediate (addi.d), the SCQ operand order, the scaled
|
||||
// vldrepl offsets (with their field masks), the XVSEQB/XVSEQV immediate
|
||||
// opcodes and the zero-register base the toolchain gives FP-relative
|
||||
// VMOVQ/XVMOVQ memory operands. Golden words from loong64enc1.s.
|
||||
func TestLOONG64_parityFixes(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·parity(SB), NOSPLIT, $0-32
|
||||
MOVW $4(R4), R5
|
||||
MOVV $4(R4), R5
|
||||
MOVW $65536(R4), R5
|
||||
MOVW $-4096(R4), R5
|
||||
SCQ R4, R5, (R6)
|
||||
VMOVQ 2(R4), V1.H8
|
||||
VMOVQ -6(R4), V1.H8
|
||||
VMOVQ -12(R4), V2.W4
|
||||
VMOVQ -16(R4), V3.V2
|
||||
XVMOVQ -10(R4), X1.H16
|
||||
XVSEQB $0, X2, X4
|
||||
XVSEQH $3, X2, X4
|
||||
XVSEQW $12, X2, X4
|
||||
XVSEQV $15, X2, X4
|
||||
XVSEQV $-15, X2, X4
|
||||
VMOVQ V2, y+16(FP)
|
||||
VMOVQ y+16(FP), V2
|
||||
VMOVQ V2, x+2030(FP)
|
||||
XVMOVQ X6, y+16(FP)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x02C01085, // addi.d $4, r4, r5
|
||||
0x02C01085, // addi.d $4, r4, r5 (MOVW keeps the 64-bit addi.d)
|
||||
0x1400021E, // lu12i.w $16, r30
|
||||
0x038003DE, // ori $0, r30, r30
|
||||
0x0010F885, // add.d r5, r4, r30
|
||||
0x15FFFFFE, // lu12i.w $-1, r30
|
||||
0x038003DE, // ori $0, r30, r30
|
||||
0x0010F885, // add.d r5, r4, r30
|
||||
0x385714C4, // sc.q r4, r5, (r6): middle<<10 | base<<5 | first
|
||||
0x30400481, // vldrepl.h v1, 2(r4)
|
||||
0x305FF481, // vldrepl.h v1, -6(r4)
|
||||
0x302FF482, // vldrepl.w v2, -12(r4)
|
||||
0x3017F883, // vldrepl.d v3, -16(r4)
|
||||
0x325FEC81, // xvldrepl.h x1, -10(r4)
|
||||
0x76800044, // xvseqi.b x4, x2, 0
|
||||
0x76808C44, // xvseqi.h x4, x2, 3
|
||||
0x76813044, // xvseqi.w x4, x2, 12
|
||||
0x7681BC44, // xvseqi.d x4, x2, 15
|
||||
0x7681C444, // xvseqi.d x4, x2, -15
|
||||
0x2C406002, // vst v2, 24(r0): FP-relative keeps the zero base
|
||||
0x2C006002, // vld v2, 24(r0)
|
||||
0x2C5FD802, // vst v2, 2038(r0)
|
||||
0x2CC06006, // xvst x6, 24(r0)
|
||||
0x4C000020,
|
||||
)
|
||||
// Misaligned vldrepl offsets are rejected, as the toolchain does.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ 3(R4), V1.H8
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
MOVW $4(R4), F1
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_bytePseudo pins the BYTE literal-data pseudo-op, which the
|
||||
// loong64 toolchain does not spell but the arm64 and riscv64 encoders of
|
||||
// this package already accept for byte-exact data layout (a superset
|
||||
// spelling, shippable via the goobj path).
|
||||
func TestLOONG64_bytePseudo(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·bytes(SB), NOSPLIT, $0
|
||||
BYTE $2
|
||||
BYTE $1; BYTE $0
|
||||
BYTE $255
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
// Four literal bytes, then RET (jirl r0, r1, 0); the trailing bytes pad
|
||||
// the final word the way any sub-word tail does.
|
||||
want := []byte{2, 1, 0, 0xFF, 0x20, 0x00, 0x00, 0x4C}
|
||||
if !bytes.Equal(code[:len(want)], want) {
|
||||
t.Errorf("bytes = % x, want % x", code, want)
|
||||
}
|
||||
for _, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BYTE $256
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BYTE $-1
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%q: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package asm
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||||
|
||||
@@ -6,7 +6,7 @@ package asm
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestLOONG64RelocOffsetsIncludePrologue pins the function-relative
|
||||
|
||||
+30
-1
@@ -17,13 +17,28 @@ import "strings"
|
||||
// size. The high flag marks the legacy high-byte registers AH/CH/DH/BH, which
|
||||
// occupy indices 4-7 yet take no REX prefix, unlike SPL/BPL/SIL/DIL that share
|
||||
// those indices but require one. The mask flag marks the AVX-512 opmask
|
||||
// registers K0-K7, the fp flag the x87 stack registers F0-F7.
|
||||
// registers K0-K7, the fp flag the x87 stack registers F0-F7, the mmx flag the
|
||||
// MMX registers M0-M7, the seg field a bare segment register (FS, GS) and the
|
||||
// ctl field the control and debug registers, whose number rides an
|
||||
// instruction's reg field rather than r/m.
|
||||
type Reg struct {
|
||||
idx int
|
||||
size int // informational width implied by the name; the mnemonic decides
|
||||
high bool // AH/CH/DH/BH
|
||||
mask bool // K0-K7 opmask register
|
||||
fp bool // F0-F7 x87 stack register
|
||||
mmx bool // M0-M7 MMX register
|
||||
seg int // segment register number plus one (ES=1..GS=6); 0 = not one
|
||||
ctl byte // 0 none, 1 CRn control register, 2 DRn debug register
|
||||
}
|
||||
|
||||
// segNumber returns the segment register number (ES=0..GS=5) when r names a
|
||||
// bare segment register.
|
||||
func (r Reg) segNumber() (int, bool) {
|
||||
if r.seg == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return r.seg - 1, true
|
||||
}
|
||||
|
||||
// Index returns the register number (0-15 for GPRs, 0-31 for vectors).
|
||||
@@ -149,6 +164,20 @@ func buildRegByName() map[string]Reg {
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["F"+itoa(i)] = Reg{idx: i, size: 8, fp: true}
|
||||
}
|
||||
// MMX: M0..M7.
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["M"+itoa(i)] = Reg{idx: i, size: 8, mmx: true}
|
||||
}
|
||||
// Bare segment registers: ES, CS, SS, DS, FS, GS (the memory-base and
|
||||
// index spellings of FS and GS are handled before register lookup).
|
||||
for i, n := range []string{"ES", "CS", "SS", "DS", "FS", "GS"} {
|
||||
m[n] = Reg{idx: i, size: 2, seg: i + 1}
|
||||
}
|
||||
// Control and debug registers: CR0..CR15, DR0..DR15.
|
||||
for i := 0; i <= 15; i++ {
|
||||
m["CR"+itoa(i)] = Reg{idx: i, size: 8, ctl: 1}
|
||||
m["DR"+itoa(i)] = Reg{idx: i, size: 8, ctl: 2}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// firstTextRISCV parses assembly source and returns the first TEXT function body.
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// RISC-V frame mapping, matching the Go toolchain's riscv64 backend.
|
||||
|
||||
@@ -6,7 +6,7 @@ package asm
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestRISCVFrameSpadjAndLines checks that a framed function records its
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL
|
||||
|
||||
+122
-3
@@ -76,6 +76,10 @@ const (
|
||||
// carries a vector length, so the register the L'L field follows is the
|
||||
// XMM source.
|
||||
vexExtractGPR
|
||||
// vexBlend4 is the four-operand variable blend `OP mask, src2, src1,
|
||||
// dst` (VPBLENDVB): ModRM.reg = dst (op3), VEX.vvvv = src1 (op2),
|
||||
// ModRM.rm = src2 (op1) and the mask register in the /is4 byte (op0).
|
||||
vexBlend4
|
||||
)
|
||||
|
||||
// vexSpec describes one VEX instruction's encoding parameters.
|
||||
@@ -202,8 +206,28 @@ var vexTable = map[string]vexSpec{
|
||||
|
||||
// VEX.128/256.66.0F.WIG, immediate shuffle (reg=dst, rm=src, imm8).
|
||||
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM},
|
||||
// VEX.256.66.0F3A.W1, qword permute (reg=dst, rm=src, imm8).
|
||||
// VEX.256.66.0F3A.W1, qword permute (reg=dst, rm=src, imm8), and its
|
||||
// double twin under op 01; the in-lane permutes under 04/05.
|
||||
"VPERMQ": {3, 0x00, 1, 1, -1, vexImmRM},
|
||||
"VPERMPD": {3, 0x01, 1, 1, -1, vexImmRM},
|
||||
"VPERMILPS": {3, 0x04, 0, 1, -1, vexImmRM},
|
||||
"VPERMILPD": {3, 0x05, 0, 1, -1, vexImmRM},
|
||||
// VEX.66.0F3A.W0, the immediate-controlled AVX tail: the rounding
|
||||
// pair, the AES key assistant and the string compares.
|
||||
"VROUNDPD": {3, 0x09, 0, 1, -1, vexImmRM},
|
||||
"VROUNDPS": {3, 0x08, 0, 1, -1, vexImmRM},
|
||||
"VAESKEYGENASSIST": {3, 0xDF, 0, 1, -1, vexImmRM},
|
||||
"VPCMPESTRI": {3, 0x61, 0, 1, -1, vexImmRM},
|
||||
"VPCMPESTRM": {3, 0x60, 0, 1, -1, vexImmRM},
|
||||
"VPCMPISTRI": {3, 0x63, 0, 1, -1, vexImmRM},
|
||||
"VPCMPISTRM": {3, 0x62, 0, 1, -1, vexImmRM},
|
||||
// VEX.128.66.0F3A.W0, the scalar lane extract to a GPR or memory
|
||||
// (reg = the XMM source, r/m = the destination).
|
||||
"VEXTRACTPS": {3, 0x17, 0, 1, -1, vexExtractGPR},
|
||||
"VPEXTRW": {3, 0x15, 0, 1, -1, vexExtractGPR},
|
||||
// VEX.128.66.0F3A.W0, the four-operand variable blend with its mask
|
||||
// register in the /is4 byte.
|
||||
"VPBLENDVB": {3, 0x4C, 0, 1, -1, vexBlend4},
|
||||
|
||||
// VEX.128/256.66.0F.WIG, two-source shuffle (reg=dst, vvvv=src1, rm=src2,
|
||||
// imm8).
|
||||
@@ -524,8 +548,16 @@ func isVex(mnemUpper string) bool {
|
||||
if _, ok := vexTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := vexMoveTable[mnemUpper]
|
||||
return ok
|
||||
if _, ok := vexMoveTable[mnemUpper]; ok {
|
||||
return true
|
||||
}
|
||||
// The dual-shape moves (VMOVHPD/VMOVLPD) pick their VEX form by operand
|
||||
// count in encodeVex.
|
||||
switch mnemUpper {
|
||||
case "VMOVHPD", "VMOVLPD":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeVex encodes a VEX instruction with operands in Plan 9 order.
|
||||
@@ -559,6 +591,22 @@ func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
||||
return e.encodeVexNDS3(vexSpec{mapSel: 1, opcode: op, pp: 1, opdigit: -1, form: vexNDS3}, ops)
|
||||
}
|
||||
}
|
||||
// The high/low double moves split by operand count: three operands
|
||||
// load-and-insert (mem, src, dst, an NDS form), two store (xmm, m64,
|
||||
// the reversed store layout).
|
||||
if mnemUpper == "VMOVHPD" || mnemUpper == "VMOVLPD" {
|
||||
loadOp, storeOp := byte(0x16), byte(0x17)
|
||||
if mnemUpper == "VMOVLPD" {
|
||||
loadOp, storeOp = 0x12, 0x13
|
||||
}
|
||||
switch len(ops) {
|
||||
case 3:
|
||||
return e.encodeVexNDS3(vexSpec{mapSel: 1, opcode: loadOp, w: 0, pp: 1, opdigit: -1, form: vexNDS3}, ops)
|
||||
case 2:
|
||||
return e.encodeVexRMRev(vexSpec{mapSel: 1, opcode: storeOp, w: 0, pp: 1, opdigit: -1, form: vexRMRev}, ops)
|
||||
}
|
||||
return fmt.Errorf("%s expects 2 or 3 operands, got %d", mnemUpper, len(ops))
|
||||
}
|
||||
spec := vexTable[mnemUpper]
|
||||
switch spec.form {
|
||||
case vexNDS3:
|
||||
@@ -573,6 +621,10 @@ func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
||||
return e.encodeVexNDS3Imm(spec, ops)
|
||||
case vexExtract:
|
||||
return e.encodeVexExtract(spec, ops)
|
||||
case vexExtractGPR:
|
||||
return e.encodeVexExtractGPR(spec, ops)
|
||||
case vexBlend4:
|
||||
return e.encodeVexBlend4(spec, ops)
|
||||
case vexRMSrcLen:
|
||||
return e.encodeVexRMSrcLen(mnemUpper, spec, ops)
|
||||
case vexZero:
|
||||
@@ -964,6 +1016,73 @@ func (e *enc) encodeVexRMRev(spec vexSpec, ops []Operand) error {
|
||||
return e.emitVexFields(spec, srcReg.vecLenBit(), srcReg.idx&7, rBit, 15, ops[1])
|
||||
}
|
||||
|
||||
// encodeVexExtractGPR encodes the lane extract to a general-purpose register
|
||||
// or memory (VEXTRACTPS): OP $imm, xsrc, gpr/mem with the XMM source in
|
||||
// ModRM.reg and the destination in r/m, L = 0.
|
||||
func (e *enc) encodeVexExtractGPR(spec vexSpec, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("extract expects 3 operands ($imm, xsrc, dst), got %d", len(ops))
|
||||
}
|
||||
imm, src, dst := ops[0], ops[1], ops[2]
|
||||
immVal, ok := imm.(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("extract lane must be an immediate")
|
||||
}
|
||||
srcReg, ok := src.(Reg)
|
||||
if !ok || !srcReg.isVec() || srcReg.size != 16 {
|
||||
return fmt.Errorf("extract source must be an XMM register")
|
||||
}
|
||||
if _, isReg := dst.(Reg); !isReg && !memOperand(dst) {
|
||||
return fmt.Errorf("extract destination must be a register or memory")
|
||||
}
|
||||
rBit := 0
|
||||
if srcReg.idx >= 8 {
|
||||
rBit = 1
|
||||
}
|
||||
if err := e.emitVexFields(spec, 0, srcReg.idx&7, rBit, 15, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
immByte, err := imm8(int64(immVal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.out = append(e.out, immByte)
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeVexBlend4 encodes the four-operand variable blend (VPBLENDVB):
|
||||
// OP mask, src2, src1, dst with ModRM.reg = dst, VEX.vvvv = src1, r/m =
|
||||
// src2 and the mask XMM register in the trailing /is4 byte.
|
||||
func (e *enc) encodeVexBlend4(spec vexSpec, ops []Operand) error {
|
||||
if len(ops) != 4 {
|
||||
return fmt.Errorf("blend expects 4 operands (mask, src2, src1, dst), got %d", len(ops))
|
||||
}
|
||||
mask, src2, src1, dst := ops[0], ops[1], ops[2], ops[3]
|
||||
maskReg, ok := mask.(Reg)
|
||||
if !ok || !maskReg.isVec() || maskReg.size != 16 {
|
||||
return fmt.Errorf("blend mask must be an XMM register")
|
||||
}
|
||||
vvvvReg, ok := src1.(Reg)
|
||||
if !ok || !vvvvReg.isVec() {
|
||||
return fmt.Errorf("blend second source must be a vector register")
|
||||
}
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("blend destination must be a vector register")
|
||||
}
|
||||
rBit := 0
|
||||
if dstReg.idx >= 8 {
|
||||
rBit = 1
|
||||
}
|
||||
if err := e.emitVexFields(spec, dstReg.vecLenBit(), dstReg.idx&7, rBit, 15-(vvvvReg.idx&15), src2); err != nil {
|
||||
return err
|
||||
}
|
||||
// The /is4 byte names the mask register: bits [3:0] its low nibble,
|
||||
// bit 7 the fourth register bit (X8-X15).
|
||||
e.out = append(e.out, byte(maskReg.idx&7)|byte((maskReg.idx&8)<<4))
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeVexMove encodes a two-operand move (VMOVDQU, VMOVUPD, VMOVD, VMOVQ,
|
||||
// VMOVSD), picking the direction-specific opcode and VEX.W. A vector→vector
|
||||
// move uses the store-form layout (reg = source, rm = destination), matching
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// to the arch package; the AST records syntax only.
|
||||
package ast
|
||||
|
||||
import "sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||
import "sourcedock.dev/petrbalvin/gasm-sdk/token"
|
||||
|
||||
// File is the parsed representation of one .s source file.
|
||||
type File struct {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ package ast
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/token"
|
||||
)
|
||||
|
||||
func pos(line, col int) token.Position { return token.Position{Line: line, Column: col} }
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// go_asm.h is the header the Go compiler writes for every package that
|
||||
|
||||
+3
-3
@@ -16,9 +16,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
func TestDerivedFamily(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
//go:build !(linux || (freebsd && (amd64 || arm64 || riscv64)))
|
||||
|
||||
package main
|
||||
|
||||
@@ -11,6 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func cmdDebug(args []string) int {
|
||||
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux (ptrace)")
|
||||
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux or FreeBSD (ptrace)")
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||
|
||||
package main
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/debug"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
func cmdDebug(args []string) int {
|
||||
@@ -239,10 +239,12 @@ REPL commands:
|
||||
fmt.Printf("gasm debug: cover: stopped on signal %v\n", sig)
|
||||
break
|
||||
}
|
||||
reason, _ := sess.StopInfo()
|
||||
regs, rerr := sess.GetRegs()
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
trapPC := regs.GetPC()
|
||||
// HandleTrap restores the original byte, rewinds PC and counts
|
||||
// the hit on the breakpoint itself. Single-step over the
|
||||
// restored instruction so the reinsertion at the top of the
|
||||
@@ -251,6 +253,12 @@ REPL commands:
|
||||
if err := sess.Step(); err != nil {
|
||||
break
|
||||
}
|
||||
} else if debug.TrapStray(sess, reason, trapPC) {
|
||||
// A breakpoint-class trap that matches none of ours and left
|
||||
// the PC in place: resuming would re-execute the trapping
|
||||
// instruction forever, so the coverage run stops here.
|
||||
fmt.Printf("gasm debug: cover: SIGTRAP at %#x matches no breakpoint; the PC did not advance\n", trapPC)
|
||||
break
|
||||
}
|
||||
}
|
||||
hits := map[uint64]int{}
|
||||
+3
-3
@@ -9,9 +9,9 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// cmdDis disassembles machine code: either a raw binary (standard input with
|
||||
|
||||
+9
-9
@@ -27,15 +27,15 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/format"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/format"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/lexer"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/lint"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/lsp"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// version reports the release the toolchain recorded for this build: the
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
)
|
||||
|
||||
const clean = "#include \"textflag.h\"\n" +
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
gasmast "sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
gasmparser "sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
gasmast "sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
gasmparser "sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// cmdScaffold generates a differential test skeleton for every kernel in a
|
||||
|
||||
+31
-7
@@ -1,13 +1,17 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||
|
||||
package debug
|
||||
|
||||
import "strings"
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Breakpoint is one software breakpoint in the debuggee.
|
||||
type Breakpoint struct {
|
||||
@@ -149,15 +153,16 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// Info returns a formatted list of all breakpoints.
|
||||
// Info returns a formatted list of all breakpoints, ordered by address so
|
||||
// the numbering is stable across calls (map iteration order is not).
|
||||
func (bm *Breakpoints) Info() string {
|
||||
if len(bm.bps) == 0 {
|
||||
return "no breakpoints set\n"
|
||||
}
|
||||
var result strings.Builder
|
||||
i := 0
|
||||
for _, bp := range bm.bps {
|
||||
i++
|
||||
bps := bm.All()
|
||||
slices.SortFunc(bps, func(a, b *Breakpoint) int { return cmp.Compare(a.Addr, b.Addr) })
|
||||
for i, bp := range bps {
|
||||
status := "enabled"
|
||||
if !bp.Enabled {
|
||||
status = "disabled"
|
||||
@@ -170,7 +175,7 @@ func (bm *Breakpoints) Info() string {
|
||||
if bp.Cond != nil {
|
||||
cond = " if " + bp.Cond.String()
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
||||
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i+1, label, bp.Addr, status, bp.hits, cond))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
@@ -238,6 +243,25 @@ func (bm *Breakpoints) All() []*Breakpoint {
|
||||
// Hits returns how many times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int { return bp.hits }
|
||||
|
||||
// TrapStray reports whether a stop is a breakpoint-class trap that matches
|
||||
// no breakpoint of ours and cannot be resumed: the PC still stands on the
|
||||
// trapping instruction (the kernel's own BRK, EBREAK or break, on an
|
||||
// architecture that reports the trap in place), so the next resume would
|
||||
// re-execute it and trap forever. trapPC is the PC the stop reported,
|
||||
// before any HandleTrap rewinding; reason is the stop's StopInfo class.
|
||||
// The debuggee's SIGSTOP barriers also stop without PC movement, and they
|
||||
// never carry the breakpoint class, so they are unaffected.
|
||||
func TrapStray(s *Session, reason StopReason, trapPC uint64) bool {
|
||||
if reason != StopBreakpoint {
|
||||
return false
|
||||
}
|
||||
after, err := s.GetRegs()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return after.GetPC() <= trapPC-uint64(breakpointPCAdjust)
|
||||
}
|
||||
|
||||
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
// On amd64 the kernel reports the trap with RIP past the INT3; on the
|
||||
// other supported architectures the PC still stands on the trap
|
||||
|
||||
@@ -10,6 +10,8 @@ package debug
|
||||
// build on every supported linux architecture.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -263,3 +265,45 @@ func TestConditionString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBreakpointsInfoOrdered proves the listing is ordered by address: the
|
||||
// numbers it prints are map keys rendered in iteration order otherwise, so
|
||||
// the same set of breakpoints would renumber itself between calls.
|
||||
func TestBreakpointsInfoOrdered(t *testing.T) {
|
||||
tr := newMockTracer()
|
||||
bm := NewBreakpoints(tr)
|
||||
addrs := []uint64{0x9000, 0x1000, 0x7000, 0x3000, 0x8000, 0x2000,
|
||||
0x6000, 0x4000, 0x5000, 0xa000}
|
||||
for i, a := range addrs {
|
||||
if _, err := bm.Set(a, fmt.Sprintf("bp%d", i)); err != nil {
|
||||
t.Fatalf("Set(%#x): %v", a, err)
|
||||
}
|
||||
}
|
||||
sorted := append([]uint64(nil), addrs...)
|
||||
slices.Sort(sorted)
|
||||
info := bm.Info()
|
||||
for i, a := range sorted {
|
||||
want := fmt.Sprintf(" %d: bp%d at %#x", i+1, indexOf(addrs, a), a)
|
||||
if !strings.Contains(info, want) {
|
||||
t.Errorf("Info() missing %q; listing:\n%s", want, info)
|
||||
}
|
||||
}
|
||||
// The numbers themselves must ascend: "1:" before "2" ... "10".
|
||||
pos := 0
|
||||
for i := range len(addrs) {
|
||||
next := strings.Index(info[pos:], fmt.Sprintf(" %d: ", i+1))
|
||||
if next < 0 {
|
||||
t.Fatalf("Info() has no entry %d; listing:\n%s", i+1, info)
|
||||
}
|
||||
pos += next
|
||||
}
|
||||
}
|
||||
|
||||
func indexOf(addrs []uint64, a uint64) int {
|
||||
for i, v := range addrs {
|
||||
if v == a {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Regression tests for the debugger audit: memory access at mapping
|
||||
// boundaries, watchpoint slot attribution, launch failure latency, stray
|
||||
// trap instructions and the REPL's argument validation. All drive a real
|
||||
// ptrace session, so they run on amd64 hosts only.
|
||||
|
||||
// memMap is one line of /proc/pid/maps.
|
||||
type memMap struct {
|
||||
lo, hi uint64
|
||||
perms string
|
||||
name string
|
||||
}
|
||||
|
||||
// readMaps parses the debuggee's memory map.
|
||||
func readMaps(t *testing.T, pid int) []memMap {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid))
|
||||
if err != nil {
|
||||
t.Fatalf("read maps: %v", err)
|
||||
}
|
||||
var out []memMap
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
var lo, hi uint64
|
||||
if _, err := fmt.Sscanf(fields[0], "%x-%x", &lo, &hi); err != nil {
|
||||
continue
|
||||
}
|
||||
m := memMap{lo: lo, hi: hi, perms: fields[1]}
|
||||
if len(fields) >= 6 {
|
||||
m.name = fields[5]
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// boundaryByte returns the last byte of a writable, ordinary mapping that is
|
||||
// followed by an unmapped gap: an access there is inside the mapping, while
|
||||
// the 8-byte word starting at it crosses into unmapped memory.
|
||||
func boundaryByte(t *testing.T, pid int) uint64 {
|
||||
t.Helper()
|
||||
maps := readMaps(t, pid)
|
||||
for i, m := range maps {
|
||||
if !strings.Contains(m.perms, "rw") ||
|
||||
strings.Contains(m.name, "vvar") || strings.Contains(m.name, "vdso") ||
|
||||
strings.Contains(m.name, "vsyscall") {
|
||||
continue
|
||||
}
|
||||
gap := uint64(1) << 62
|
||||
if i+1 < len(maps) {
|
||||
gap = maps[i+1].lo - m.hi
|
||||
}
|
||||
if gap >= 4096 {
|
||||
return m.hi - 1
|
||||
}
|
||||
}
|
||||
t.Skip("no writable mapping followed by a hole; cannot construct the boundary")
|
||||
return 0
|
||||
}
|
||||
|
||||
// TestReadMemoryPageBoundary proves ReadMemory never reads past the requested
|
||||
// range: one byte at the end of a mapping followed by a hole must be
|
||||
// readable, which the old word-at-a-time tail read failed because its final
|
||||
// 8-byte Peek crossed into the unmapped page.
|
||||
func TestReadMemoryPageBoundary(t *testing.T) {
|
||||
sess, _, _ := launchKernel(t, buildGasm(t), boundaryKernel(t), "boundary", nil)
|
||||
addr := boundaryByte(t, sess.Pid())
|
||||
mem, err := sess.ReadMemory(addr, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMemory(%#x, 1): %v (the read must not cross into the unmapped page)", addr, err)
|
||||
}
|
||||
if len(mem) != 1 {
|
||||
t.Fatalf("ReadMemory returned %d bytes, want 1", len(mem))
|
||||
}
|
||||
// A request whose own range crosses into the hole must still fail.
|
||||
if _, err := sess.ReadMemory(addr, 8); err == nil {
|
||||
t.Fatal("ReadMemory past the mapping end should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDisassemblePageBoundary proves the disassembler shrinks its read
|
||||
// window at a mapping end instead of failing: the instruction stream cannot
|
||||
// be decoded at all when the fixed 15-byte read crosses into the hole.
|
||||
func TestDisassemblePageBoundary(t *testing.T) {
|
||||
sess, _, _ := launchKernel(t, buildGasm(t), boundaryKernel(t), "boundary", nil)
|
||||
addr := boundaryByte(t, sess.Pid())
|
||||
if _, _, err := sess.Disassemble(addr); err != nil {
|
||||
t.Fatalf("Disassemble(%#x): %v (the read window must shrink at the mapping end)", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteMemoryPageBoundary proves WriteMemory writes exactly the bytes it
|
||||
// is given: one byte at the end of a mapping followed by a hole must be
|
||||
// writable, which the old read-modify-write of the final partial word failed
|
||||
// because its Peek crossed into the unmapped page.
|
||||
func TestWriteMemoryPageBoundary(t *testing.T) {
|
||||
sess, _, _ := launchKernel(t, buildGasm(t), boundaryKernel(t), "boundary", nil)
|
||||
addr := boundaryByte(t, sess.Pid())
|
||||
orig, err := sess.ReadMemory(addr, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMemory(%#x, 1): %v", addr, err)
|
||||
}
|
||||
if err := sess.WriteMemory(addr, []byte{orig[0]}); err != nil {
|
||||
t.Fatalf("WriteMemory(%#x, 1): %v (the write must not read past the range)", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchpointSlotAttribution proves a hit is attributed to the slot that
|
||||
// fired, not to an earlier one whose DR6 status bit is still set: the B0-B3
|
||||
// bits are sticky, so they must be acknowledged when read.
|
||||
func TestWatchpointSlotAttribution(t *testing.T) {
|
||||
bin := buildGasm(t)
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func wptwo(x, y int64) (a, b int64)
|
||||
TEXT ·wptwo(SB), NOSPLIT, $0-32
|
||||
MOVQ $0x1111, AX
|
||||
MOVQ AX, a+16(FP)
|
||||
MOVQ $0x2222, BX
|
||||
MOVQ BX, b+24(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "wptwo", nil)
|
||||
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
regs, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
// FP sits one word above the entry stack pointer (the return address
|
||||
// occupies [RSP]), so a+16(FP) = RSP+24 and b+24(FP) = RSP+32.
|
||||
watchA := regs.RSP + 24
|
||||
watchB := regs.RSP + 32
|
||||
|
||||
if err := sess.SetWatchpoint(0, watchA, WatchWrite, 8); err != nil {
|
||||
t.Fatalf("SetWatchpoint(0): %v", err)
|
||||
}
|
||||
if err := sess.SetWatchpoint(1, watchB, WatchWrite, 8); err != nil {
|
||||
t.Fatalf("SetWatchpoint(1): %v", err)
|
||||
}
|
||||
|
||||
for i, want := range []uint64{watchA, watchB} {
|
||||
if err := sess.Continue(); err != nil {
|
||||
t.Fatalf("Continue (hit %d): %v", i+1, err)
|
||||
}
|
||||
reason, addr := sess.StopInfo()
|
||||
if reason != StopWatchpoint {
|
||||
t.Fatalf("hit %d: stop reason = %v, want StopWatchpoint", i+1, reason)
|
||||
}
|
||||
if addr != want {
|
||||
t.Fatalf("hit %d reported %#x, want %#x (the sticky DR6 bit misattributes the slot)", i+1, addr, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing a watchpoint must zero its address register: a stale
|
||||
// address in a disabled slot turns any sticky status bit into a
|
||||
// misattributed report later.
|
||||
if err := sess.ClearWatchpoint(0); err != nil {
|
||||
t.Fatalf("ClearWatchpoint(0): %v", err)
|
||||
}
|
||||
dr0, err := ptracePeekUser(sess.Pid(), drOffset)
|
||||
if err != nil {
|
||||
t.Fatalf("read DR0: %v", err)
|
||||
}
|
||||
if dr0 != 0 {
|
||||
t.Fatalf("DR0 = %#x after ClearWatchpoint, want 0 (the address register must be cleared)", dr0)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchFailsFastOnDeadDebuggee proves a debuggee that dies before
|
||||
// signalling readiness surfaces promptly: the ready poll used to run its
|
||||
// full 2.5 seconds before the wait discovered the exit.
|
||||
func TestLaunchFailsFastOnDeadDebuggee(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
bin := buildGasm(t)
|
||||
path := boundaryKernel(t)
|
||||
start := time.Now()
|
||||
sess, err := Launch(bin, path, "nosuchfunction", nil)
|
||||
elapsed := time.Since(start)
|
||||
if err == nil {
|
||||
sess.Kill()
|
||||
t.Fatal("Launch with an unknown function should fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "before signalling readiness") &&
|
||||
!strings.Contains(err.Error(), "debuggee exited") {
|
||||
t.Errorf("error does not name the dead debuggee: %v", err)
|
||||
}
|
||||
if elapsed >= 1500*time.Millisecond {
|
||||
t.Fatalf("Launch took %v to report the dead debuggee; the readiness poll must detect the exit, not time out", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStrayTrapRunsThrough proves the continue loop survives a trap
|
||||
// instruction planted in the kernel itself (BYTE $0xCC, the same byte the
|
||||
// debugger patches in): on architectures that report the trap in place the
|
||||
// loop must surface the stop, and on amd64 it runs through to the exit. A
|
||||
// regression here hangs, so a watchdog fails the run.
|
||||
func TestStrayTrapRunsThrough(t *testing.T) {
|
||||
bin := buildGasm(t)
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func stray() int64
|
||||
TEXT ·stray(SB), NOSPLIT, $0-8
|
||||
MOVQ $7, AX
|
||||
BYTE $0xCC
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, _ := launchKernel(t, bin, path, "stray", nil)
|
||||
|
||||
timer := time.AfterFunc(time.Minute, func() {
|
||||
panic("watchdog: the continue loop hung on the stray trap instruction")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
REPL(sess, bm, sess.CodeBase(), 0, 0, 0, nil, nil,
|
||||
strings.NewReader("continue\nquit\n"))
|
||||
})
|
||||
if !strings.Contains(out, "debuggee exited") {
|
||||
t.Errorf("the stray trap wedged the continue loop; output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStepIntoFaultReportsSignal proves the step command reports a genuine
|
||||
// signal-delivery-stop instead of silently printing the faulting
|
||||
// instruction as if the step had succeeded.
|
||||
func TestStepIntoFaultReportsSignal(t *testing.T) {
|
||||
bin := buildGasm(t)
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func crash() int64
|
||||
TEXT ·crash(SB), NOSPLIT, $0-8
|
||||
XORQ AX, AX
|
||||
MOVQ (AX), AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
path := writeKernel(t, kernel)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "crash", nil)
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, nil, nil,
|
||||
strings.NewReader("step 2\nquit\n"))
|
||||
})
|
||||
if !strings.Contains(out, "stopped on signal") {
|
||||
t.Errorf("stepping into the fault did not report the signal; output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestREPLRejectsBadArguments proves the command loop reports malformed
|
||||
// input instead of silently defaulting: an unknown label for x would read
|
||||
// address 0, and a malformed count would silently step one instruction.
|
||||
func TestREPLRejectsBadArguments(t *testing.T) {
|
||||
bin := buildGasm(t)
|
||||
path := boundaryKernel(t)
|
||||
sess, bm, fl := launchKernel(t, bin, path, "boundary", nil)
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
runToEntry(t, sess, bm, entry)
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, nil, nil,
|
||||
strings.NewReader("x nosuchlabel\nstep abc\ndisas abc\nwatch 0x1000 q 8\nquit\n"))
|
||||
})
|
||||
for _, want := range []string{
|
||||
"unknown address: nosuchlabel",
|
||||
"invalid count: abc",
|
||||
"unknown watchpoint type: q",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if got := strings.Count(out, "invalid count: abc"); got != 2 {
|
||||
t.Errorf("invalid count reported %d times, want 2 (step and disas):\n%s", got, out)
|
||||
}
|
||||
}
|
||||
|
||||
// boundaryKernel is a minimal kernel for the boundary tests, which only need
|
||||
// a live, stopped debuggee.
|
||||
func boundaryKernel(t *testing.T) string {
|
||||
t.Helper()
|
||||
const kernel = `#include "textflag.h"
|
||||
|
||||
// func boundary() int64
|
||||
TEXT ·boundary(SB), NOSPLIT, $0-8
|
||||
MOVQ $1, AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
return writeKernel(t, kernel)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes. An amd64
|
||||
// instruction is up to 15 bytes long, but the read must not reach past the
|
||||
// end of the mapping: when the full 15-byte window crosses into unmapped
|
||||
// memory the window shrinks, because an instruction at the mapping's end is
|
||||
// by construction no longer than the readable bytes that hold it.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
var lastErr error
|
||||
for _, n := range []int{15, 8, 4, 2, 1} {
|
||||
mem, err := s.ReadMemory(addr, n)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
ins, derr := disasm.Decode(arch.AMD64, mem, addr)
|
||||
if derr != nil {
|
||||
return "", 0, derr
|
||||
}
|
||||
return ins.Text, ins.Len, nil
|
||||
}
|
||||
return "", 0, lastErr
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||
// them as a formatted string with addresses and byte offsets.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result strings.Builder
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||
break
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||
if length == 0 {
|
||||
length = 1
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (x86asm.IntelSyntax) is a
|
||||
// call. The first token must match exactly: a prefix test would also catch
|
||||
// unrelated mnemonics.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
return strings.ToLower(m) == "call"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
mem, err := s.ReadMemory(addr, 4)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
ins, err := disasm.Decode(arch.ARM64, mem, addr)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return ins.Text, ins.Len, nil
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||
// them as a formatted string with addresses and byte offsets.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result strings.Builder
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||
break
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||
if length == 0 {
|
||||
length = 1
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (arm64asm.GoSyntax) is a
|
||||
// call. GoSyntax renders bl as CALL; the native mnemonic is accepted too.
|
||||
// The first token must match exactly so branches never match.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
switch strings.ToLower(m) {
|
||||
case "call", "bl":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes. The read
|
||||
// shrinks from 4 to 2 bytes when the full word crosses into unmapped memory:
|
||||
// a compressed instruction at the mapping's end still fits the shorter
|
||||
// window, and an instruction can never extend past the mapping that holds it.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
var lastErr error
|
||||
for _, n := range []int{4, 2} {
|
||||
mem, err := s.ReadMemory(addr, n)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
ins, derr := disasm.Decode(arch.RISCV, mem, addr)
|
||||
if derr != nil {
|
||||
return "", 0, derr
|
||||
}
|
||||
return ins.Text, ins.Len, nil
|
||||
}
|
||||
return "", 0, lastErr
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||
// them as a formatted string with addresses and byte offsets.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result strings.Builder
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||
break
|
||||
}
|
||||
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||
if length == 0 {
|
||||
length = 1
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// isCallInsn reports whether disassembled text (riscv64asm.GoSyntax) is a
|
||||
// call. GoSyntax renders jal and jalr calls as CALL; the native mnemonics
|
||||
// are accepted too. The first token must match exactly: a prefix test on
|
||||
// "bl" would catch branches on other architectures, and jalr as ret prints
|
||||
// RET, which must not be stepped over.
|
||||
func isCallInsn(text string) bool {
|
||||
m, _, _ := strings.Cut(text, " ")
|
||||
switch strings.ToLower(m) {
|
||||
case "call", "jal", "jalr":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -9,22 +9,31 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes.
|
||||
// memory and returns its text representation and length in bytes. An amd64
|
||||
// instruction is up to 15 bytes long, but the read must not reach past the
|
||||
// end of the mapping: when the full 15-byte window crosses into unmapped
|
||||
// memory the window shrinks, because an instruction at the mapping's end is
|
||||
// by construction no longer than the readable bytes that hold it.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
mem, err := s.ReadMemory(addr, 15)
|
||||
var lastErr error
|
||||
for _, n := range []int{15, 8, 4, 2, 1} {
|
||||
mem, err := s.ReadMemory(addr, n)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
ins, err := disasm.Decode(arch.AMD64, mem, addr)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
ins, derr := disasm.Decode(arch.AMD64, mem, addr)
|
||||
if derr != nil {
|
||||
return "", 0, derr
|
||||
}
|
||||
return ins.Text, ins.Len, nil
|
||||
}
|
||||
return "", 0, lastErr
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
|
||||
@@ -9,22 +9,30 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||
)
|
||||
|
||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||
// memory and returns its text representation and length in bytes.
|
||||
// memory and returns its text representation and length in bytes. The read
|
||||
// shrinks from 4 to 2 bytes when the full word crosses into unmapped memory:
|
||||
// a compressed instruction at the mapping's end still fits the shorter
|
||||
// window, and an instruction can never extend past the mapping that holds it.
|
||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
mem, err := s.ReadMemory(addr, 4)
|
||||
var lastErr error
|
||||
for _, n := range []int{4, 2} {
|
||||
mem, err := s.ReadMemory(addr, n)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
ins, err := disasm.Decode(arch.RISCV, mem, addr)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
ins, derr := disasm.Decode(arch.RISCV, mem, addr)
|
||||
if derr != nil {
|
||||
return "", 0, derr
|
||||
}
|
||||
return ins.Text, ins.Len, nil
|
||||
}
|
||||
return "", 0, lastErr
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr.
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" RIP = %#016x (func+%#x)\n", regs.RIP, regs.RIP-codeBase-funcOff)
|
||||
fmt.Printf(" RSP = %#016x RBP = %#016x\n", regs.RSP, regs.RBP)
|
||||
fmt.Printf(" RAX = %#016x RBX = %#016x\n", regs.RAX, regs.RBX)
|
||||
fmt.Printf(" RCX = %#016x RDX = %#016x\n", regs.RCX, regs.RDX)
|
||||
fmt.Printf(" RSI = %#016x RDI = %#016x\n", regs.RSI, regs.RDI)
|
||||
fmt.Printf(" R8 = %#016x R9 = %#016x\n", regs.R8, regs.R9)
|
||||
fmt.Printf(" R10 = %#016x R11 = %#016x\n", regs.R10, regs.R11)
|
||||
fmt.Printf(" R12 = %#016x R13 = %#016x\n", regs.R12, regs.R13)
|
||||
fmt.Printf(" R14 = %#016x R15 = %#016x\n", regs.R14, regs.R15)
|
||||
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
||||
}
|
||||
|
||||
func printVectorRegs(v *VectorRegs) {
|
||||
fmt.Println("\n Vector registers (YMM):")
|
||||
for i := 0; i < 16; i += 2 {
|
||||
fmt.Printf(" YMM%-2d = ", i)
|
||||
printYMM(v.YMM[i][:])
|
||||
fmt.Printf(" YMM%-2d = ", i+1)
|
||||
printYMM(v.YMM[i+1][:])
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func printYMM(b []byte) {
|
||||
for j := 0; j < 32; j += 4 {
|
||||
v := uint32(b[j]) | uint32(b[j+1])<<8 | uint32(b[j+2])<<16 | uint32(b[j+3])<<24
|
||||
fmt.Printf("%08x ", v)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRflags(f uint64) string {
|
||||
var flags string
|
||||
if f&1 != 0 {
|
||||
flags += "CF "
|
||||
}
|
||||
if f&(1<<2) != 0 {
|
||||
flags += "PF "
|
||||
}
|
||||
if f&(1<<4) != 0 {
|
||||
flags += "AF "
|
||||
}
|
||||
if f&(1<<6) != 0 {
|
||||
flags += "ZF "
|
||||
}
|
||||
if f&(1<<7) != 0 {
|
||||
flags += "SF "
|
||||
}
|
||||
if f&(1<<8) != 0 {
|
||||
flags += "TF "
|
||||
}
|
||||
if f&(1<<9) != 0 {
|
||||
flags += "IF "
|
||||
}
|
||||
if f&(1<<10) != 0 {
|
||||
flags += "DF "
|
||||
}
|
||||
if f&(1<<11) != 0 {
|
||||
flags += "OF "
|
||||
}
|
||||
if flags == "" {
|
||||
return "none"
|
||||
}
|
||||
return flags[:len(flags)-1]
|
||||
}
|
||||
|
||||
// SetReg modifies a register value in the debuggee.
|
||||
func (s *Session) SetReg(name string, value uint64) error {
|
||||
regs, err := s.GetRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "rax", "eax", "ax", "al":
|
||||
regs.RAX = value
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
regs.RBX = value
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
regs.RCX = value
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
regs.RDX = value
|
||||
case "rsi", "esi", "si":
|
||||
regs.RSI = value
|
||||
case "rdi", "edi", "di":
|
||||
regs.RDI = value
|
||||
case "rbp", "ebp", "bp":
|
||||
regs.RBP = value
|
||||
case "rsp", "esp", "sp":
|
||||
regs.RSP = value
|
||||
case "r8":
|
||||
regs.R8 = value
|
||||
case "r9":
|
||||
regs.R9 = value
|
||||
case "r10":
|
||||
regs.R10 = value
|
||||
case "r11":
|
||||
regs.R11 = value
|
||||
case "r12":
|
||||
regs.R12 = value
|
||||
case "r13":
|
||||
regs.R13 = value
|
||||
case "r14":
|
||||
regs.R14 = value
|
||||
case "r15":
|
||||
regs.R15 = value
|
||||
case "rip", "eip":
|
||||
regs.RIP = value
|
||||
default:
|
||||
return fmt.Errorf("debug: unknown register %q", name)
|
||||
}
|
||||
return s.SetRegs(®s)
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address of the current frame (amd64
|
||||
// ABI0 convention). A function that contains a CALL (or has a frame) is
|
||||
// assembled with the prologue PUSHQ BP; MOVQ SP, BP, so mid-function the
|
||||
// word at SP is the saved caller BP, a stack address, and the return
|
||||
// address sits further up. Walk the stack from SP and take the first word
|
||||
// that lies in an executable mapping: stack and data words never do, a
|
||||
// return address always does. FreeBSD exposes no mapping list, so the
|
||||
// walk degenerates to the raw entry convention, [SP] before any push.
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return s.Peek(regs.RSP)
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
func archSPLabel() string { return "RSP" }
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
||||
fmt.Printf(" SP = %#016x FP = %#016x\n", regs.SP, regs.X29)
|
||||
fmt.Printf(" LR = %#016x\n", regs.X30)
|
||||
fmt.Printf(" X0 = %#016x X1 = %#016x\n", regs.X0, regs.X1)
|
||||
fmt.Printf(" X2 = %#016x X3 = %#016x\n", regs.X2, regs.X3)
|
||||
fmt.Printf(" X4 = %#016x X5 = %#016x\n", regs.X4, regs.X5)
|
||||
fmt.Printf(" X6 = %#016x X7 = %#016x\n", regs.X6, regs.X7)
|
||||
fmt.Printf(" X8 = %#016x X9 = %#016x\n", regs.X8, regs.X9)
|
||||
fmt.Printf(" X10 = %#016x X11 = %#016x\n", regs.X10, regs.X11)
|
||||
fmt.Printf(" X12 = %#016x X13 = %#016x\n", regs.X12, regs.X13)
|
||||
fmt.Printf(" X14 = %#016x X15 = %#016x\n", regs.X14, regs.X15)
|
||||
fmt.Printf(" X16 = %#016x X17 = %#016x\n", regs.X16, regs.X17)
|
||||
fmt.Printf(" X18 = %#016x X19 = %#016x\n", regs.X18, regs.X19)
|
||||
fmt.Printf(" X20 = %#016x X21 = %#016x\n", regs.X20, regs.X21)
|
||||
fmt.Printf(" X22 = %#016x X23 = %#016x\n", regs.X22, regs.X23)
|
||||
fmt.Printf(" X24 = %#016x X25 = %#016x\n", regs.X24, regs.X25)
|
||||
fmt.Printf(" X26 = %#016x X27 = %#016x\n", regs.X26, regs.X27)
|
||||
fmt.Printf(" X28 = %#016x PSTATE = %#x\n", regs.X28, regs.PSTATE)
|
||||
}
|
||||
|
||||
func printVectorRegs(v *VectorRegs) {
|
||||
fmt.Println("\n Vector registers (V0-V31):")
|
||||
for i := 0; i < 32; i += 2 {
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i, binary.LittleEndian.Uint64(v.V[i][8:16]), binary.LittleEndian.Uint64(v.V[i][0:8]))
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i+1, binary.LittleEndian.Uint64(v.V[i+1][8:16]), binary.LittleEndian.Uint64(v.V[i+1][0:8]))
|
||||
}
|
||||
}
|
||||
|
||||
// SetReg modifies a register value in the debuggee.
|
||||
func (s *Session) SetReg(name string, value uint64) error {
|
||||
regs, err := s.GetRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "x0":
|
||||
regs.X0 = value
|
||||
case "x1":
|
||||
regs.X1 = value
|
||||
case "x2":
|
||||
regs.X2 = value
|
||||
case "x3":
|
||||
regs.X3 = value
|
||||
case "x4":
|
||||
regs.X4 = value
|
||||
case "x5":
|
||||
regs.X5 = value
|
||||
case "x6":
|
||||
regs.X6 = value
|
||||
case "x7":
|
||||
regs.X7 = value
|
||||
case "x8":
|
||||
regs.X8 = value
|
||||
case "x9":
|
||||
regs.X9 = value
|
||||
case "x10":
|
||||
regs.X10 = value
|
||||
case "x11":
|
||||
regs.X11 = value
|
||||
case "x12":
|
||||
regs.X12 = value
|
||||
case "x13":
|
||||
regs.X13 = value
|
||||
case "x14":
|
||||
regs.X14 = value
|
||||
case "x15":
|
||||
regs.X15 = value
|
||||
case "x16":
|
||||
regs.X16 = value
|
||||
case "x17":
|
||||
regs.X17 = value
|
||||
case "x18":
|
||||
regs.X18 = value
|
||||
case "x19":
|
||||
regs.X19 = value
|
||||
case "x20":
|
||||
regs.X20 = value
|
||||
case "x21":
|
||||
regs.X21 = value
|
||||
case "x22":
|
||||
regs.X22 = value
|
||||
case "x23":
|
||||
regs.X23 = value
|
||||
case "x24":
|
||||
regs.X24 = value
|
||||
case "x25":
|
||||
regs.X25 = value
|
||||
case "x26":
|
||||
regs.X26 = value
|
||||
case "x27":
|
||||
regs.X27 = value
|
||||
case "x28":
|
||||
regs.X28 = value
|
||||
case "x29", "fp":
|
||||
regs.X29 = value
|
||||
case "x30", "lr":
|
||||
regs.X30 = value
|
||||
case "sp":
|
||||
regs.SP = value
|
||||
case "pc":
|
||||
regs.PC = value
|
||||
default:
|
||||
return fmt.Errorf("debug: unknown register %q", name)
|
||||
}
|
||||
return s.SetRegs(®s)
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address from LR (arm64 convention).
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return regs.X30, nil
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
func archSPLabel() string { return "SP" }
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
||||
fmt.Printf(" SP = %#016x FP = %#016x\n", regs.Sp, regs.S0)
|
||||
fmt.Printf(" RA = %#016x\n", regs.Ra)
|
||||
fmt.Printf(" A0 = %#016x A1 = %#016x\n", regs.A0, regs.A1)
|
||||
fmt.Printf(" A2 = %#016x A3 = %#016x\n", regs.A2, regs.A3)
|
||||
fmt.Printf(" A4 = %#016x A5 = %#016x\n", regs.A4, regs.A5)
|
||||
fmt.Printf(" A6 = %#016x A7 = %#016x\n", regs.A6, regs.A7)
|
||||
fmt.Printf(" T0 = %#016x T1 = %#016x\n", regs.T0, regs.T1)
|
||||
fmt.Printf(" T2 = %#016x T3 = %#016x\n", regs.T2, regs.T3)
|
||||
fmt.Printf(" T4 = %#016x T5 = %#016x\n", regs.T4, regs.T5)
|
||||
fmt.Printf(" T6 = %#016x\n", regs.T6)
|
||||
fmt.Printf(" S1 = %#016x S2 = %#016x\n", regs.S1, regs.S2)
|
||||
fmt.Printf(" S3 = %#016x S4 = %#016x\n", regs.S3, regs.S4)
|
||||
fmt.Printf(" S5 = %#016x S6 = %#016x\n", regs.S5, regs.S6)
|
||||
fmt.Printf(" S7 = %#016x S8 = %#016x\n", regs.S7, regs.S8)
|
||||
fmt.Printf(" S9 = %#016x S10 = %#016x\n", regs.S9, regs.S10)
|
||||
fmt.Printf(" S11 = %#016x\n", regs.S11)
|
||||
}
|
||||
|
||||
func printVectorRegs(v *VectorRegs) {
|
||||
fmt.Println("\n FP registers (F0-F31):")
|
||||
for i := 0; i < 32; i += 2 {
|
||||
fmt.Printf(" F%-2d = %#018x F%-2d = %#018x\n", i, v.F[i], i+1, v.F[i+1])
|
||||
}
|
||||
fmt.Printf(" FCSR = %#x\n", v.FCSR)
|
||||
}
|
||||
|
||||
// SetReg modifies a register value in the debuggee.
|
||||
func (s *Session) SetReg(name string, value uint64) error {
|
||||
regs, err := s.GetRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "pc":
|
||||
regs.PC = value
|
||||
case "ra", "x1":
|
||||
regs.Ra = value
|
||||
case "sp", "x2":
|
||||
regs.Sp = value
|
||||
case "gp", "x3":
|
||||
regs.Gp = value
|
||||
case "tp", "x4":
|
||||
regs.Tp = value
|
||||
case "t0", "x5":
|
||||
regs.T0 = value
|
||||
case "t1", "x6":
|
||||
regs.T1 = value
|
||||
case "t2", "x7":
|
||||
regs.T2 = value
|
||||
case "s0", "fp", "x8":
|
||||
regs.S0 = value
|
||||
case "s1", "x9":
|
||||
regs.S1 = value
|
||||
case "a0", "x10":
|
||||
regs.A0 = value
|
||||
case "a1", "x11":
|
||||
regs.A1 = value
|
||||
case "a2", "x12":
|
||||
regs.A2 = value
|
||||
case "a3", "x13":
|
||||
regs.A3 = value
|
||||
case "a4", "x14":
|
||||
regs.A4 = value
|
||||
case "a5", "x15":
|
||||
regs.A5 = value
|
||||
case "a6", "x16":
|
||||
regs.A6 = value
|
||||
case "a7", "x17":
|
||||
regs.A7 = value
|
||||
case "s2", "x18":
|
||||
regs.S2 = value
|
||||
case "s3", "x19":
|
||||
regs.S3 = value
|
||||
case "s4", "x20":
|
||||
regs.S4 = value
|
||||
case "s5", "x21":
|
||||
regs.S5 = value
|
||||
case "s6", "x22":
|
||||
regs.S6 = value
|
||||
case "s7", "x23":
|
||||
regs.S7 = value
|
||||
case "s8", "x24":
|
||||
regs.S8 = value
|
||||
case "s9", "x25":
|
||||
regs.S9 = value
|
||||
case "s10", "x26":
|
||||
regs.S10 = value
|
||||
case "s11", "x27":
|
||||
regs.S11 = value
|
||||
case "t3", "x28":
|
||||
regs.T3 = value
|
||||
case "t4", "x29":
|
||||
regs.T4 = value
|
||||
case "t5", "x30":
|
||||
regs.T5 = value
|
||||
case "t6", "x31":
|
||||
regs.T6 = value
|
||||
default:
|
||||
return fmt.Errorf("debug: unknown register %q", name)
|
||||
}
|
||||
return s.SetRegs(®s)
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address from RA (riscv64 convention).
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return regs.Ra, nil
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
func archSPLabel() string { return "SP" }
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// Integration tests beyond the basic entry breakpoint: hardware watchpoints,
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Session is a ptrace debugging session controlling one debuggee process.
|
||||
// The FreeBSD implementation sits behind the same surface as the Linux one:
|
||||
// PT_TRACE_ME from the debuggee, PT_CONTINUE/PT_STEP from the tracer, and
|
||||
// tracee memory through PT_IO (FreeBSD has no /proc/pid/mem to fall back
|
||||
// on, so PT_IO is the only supported route).
|
||||
type Session struct {
|
||||
pid int
|
||||
cmd *exec.Cmd
|
||||
stopped bool
|
||||
exited bool
|
||||
codeBase uint64 // base address of the JIT code in the debuggee
|
||||
tmpDir string // scratch directory of the session, removed on Kill
|
||||
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 dbw 0-15)
|
||||
// lastSignal holds the signal of the most recent stop when that stop
|
||||
// was a genuine signal-delivery-stop the caller must see (a fault such
|
||||
// as SIGSEGV, SIGBUS, SIGFPE or SIGILL); 0 for breakpoint traps,
|
||||
// single-steps, SIGSTOP and suppressed runtime signals.
|
||||
lastSignal syscall.Signal
|
||||
}
|
||||
|
||||
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||
// attaches to it via ptrace.
|
||||
func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||
sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "")
|
||||
return sess, err
|
||||
}
|
||||
|
||||
// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee.
|
||||
//
|
||||
// It pins the calling goroutine to its OS thread and leaves it pinned: the
|
||||
// debuggee's PT_TRACE_ME binds the tracer relation to the forking thread,
|
||||
// and every ptrace request on the session must come from that same thread.
|
||||
// All Session methods must therefore be called from the goroutine that
|
||||
// launched the session (the REPL and coverage loops do exactly that).
|
||||
func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) {
|
||||
runtime.LockOSThread() // ptrace requests must stay on the forking thread
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||
}
|
||||
if gasmBin != "" {
|
||||
self = gasmBin
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "gasm-debug-*")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: tempdir: %w", err)
|
||||
}
|
||||
argsFile := filepath.Join(tmpDir, "args.bin")
|
||||
if err := os.WriteFile(argsFile, args, 0o644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: write args: %w", err)
|
||||
}
|
||||
|
||||
if bufSpec != "" {
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: write bufspec: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(self, "debug", "--func", funcName, "--args", argsFile, asmPath)
|
||||
cmd.Env = append(os.Environ(), "GASM_DEBUG_TARGET=1", "GASM_DEBUG_TMP="+tmpDir)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||
}
|
||||
|
||||
s := &Session{pid: cmd.Process.Pid, cmd: cmd, tmpDir: tmpDir}
|
||||
|
||||
readyFile := filepath.Join(tmpDir, "ready")
|
||||
for range 500 {
|
||||
if _, err := os.Stat(readyFile); err == nil {
|
||||
break
|
||||
}
|
||||
// A debuggee that died before signalling readiness (unknown
|
||||
// function, unparseable source) writes its failure notice to the
|
||||
// handshake directory; read it and fail fast. The poll never
|
||||
// waits on the child: a wait here could consume the SIGSTOP park
|
||||
// that waitStopped below must receive, hanging the launch.
|
||||
if err := s.deadReason(); err != nil {
|
||||
cmd.Wait()
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, err
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
// The debuggee parks itself with SIGSTOP once the JIT code is mapped.
|
||||
// A Go tracee also reports SIGURG preemption as signal-delivery-stops,
|
||||
// so the wait loops until a stop the debugger cares about instead of
|
||||
// assuming the first event is the SIGSTOP.
|
||||
if _, err := s.waitStopped(); err != nil {
|
||||
cmd.Process.Kill()
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: wait for debuggee: %w", err)
|
||||
}
|
||||
s.stopped = true
|
||||
|
||||
// The debuggee reports its JIT mapping in the codebase file; that is
|
||||
// the supported path on FreeBSD, where no /proc/pid/maps exists to
|
||||
// scan for the RWX region as a fallback.
|
||||
if data, err := os.ReadFile(filepath.Join(tmpDir, "codebase")); err == nil {
|
||||
fmt.Sscanf(string(data), "%d", &s.codeBase)
|
||||
}
|
||||
|
||||
var bufAddrs []uint64
|
||||
if bufSpec != "" {
|
||||
addrFile := filepath.Join(tmpDir, "bufaddrs")
|
||||
if data, err := os.ReadFile(addrFile); err == nil {
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
|
||||
var addr uint64
|
||||
if _, err := fmt.Sscanf(line, "%d", &addr); err == nil {
|
||||
bufAddrs = append(bufAddrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, bufAddrs, nil
|
||||
}
|
||||
|
||||
// deadReason reports the debuggee's own failure notice, the file its
|
||||
// failure paths write before exiting. A debuggee killed without a notice
|
||||
// (a crash, SIGKILL) surfaces through waitStopped after the poll instead,
|
||||
// which is why the poll's budget stays finite.
|
||||
func (s *Session) deadReason() error {
|
||||
data, err := os.ReadFile(filepath.Join(s.tmpDir, "dead"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("debug: debuggee failed before signalling readiness: %s", strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step), the
|
||||
// debuggee's own SIGSTOP, or a genuine signal-delivery-stop. A Go tracee's
|
||||
// runtime raises SIGURG for asynchronous preemption, and every signal on a
|
||||
// traced thread surfaces as a signal-delivery-stop, so SIGURG is suppressed
|
||||
// and the tracee resumed without it. Every other signal (SIGSEGV, SIGBUS,
|
||||
// SIGFPE, SIGILL, ...) is returned to the caller: resuming with signal 0
|
||||
// would restart the faulting instruction and fault forever, so a faulting
|
||||
// kernel must surface as a stop the caller reports.
|
||||
func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||
for {
|
||||
var ws syscall.WaitStatus
|
||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ws.Exited() {
|
||||
s.exited = true
|
||||
return 0, fmt.Errorf("debuggee exited with status %d", ws.ExitStatus())
|
||||
}
|
||||
if ws.Signaled() {
|
||||
s.exited = true
|
||||
return 0, fmt.Errorf("debuggee killed by signal %v", ws.Signal())
|
||||
}
|
||||
switch sig := ws.StopSignal(); sig {
|
||||
case syscall.SIGTRAP, syscall.SIGSTOP:
|
||||
s.stopped = true
|
||||
s.lastSignal = 0
|
||||
return sig, nil
|
||||
case syscall.SIGURG:
|
||||
// Go runtime asynchronous preemption: resume the tracee
|
||||
// without delivering the signal.
|
||||
s.lastSignal = 0
|
||||
if err := unix.PtraceCont(s.pid, 0); err != nil {
|
||||
return 0, fmt.Errorf("debug: PT_CONTINUE: %w", err)
|
||||
}
|
||||
default:
|
||||
// A genuine signal-delivery-stop. Report it; the caller
|
||||
// decides how to proceed.
|
||||
s.stopped = true
|
||||
s.lastSignal = sig
|
||||
return sig, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LastSignal returns the signal of the most recent stop when that stop was
|
||||
// a genuine signal-delivery-stop (a fault such as SIGSEGV, SIGFPE, SIGILL
|
||||
// or SIGBUS), and 0 for breakpoint traps, single-steps, SIGSTOP and
|
||||
// suppressed runtime signals.
|
||||
func (s *Session) LastSignal() syscall.Signal { return s.lastSignal }
|
||||
|
||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr, through
|
||||
// PT_IO with PIOD_READ_D.
|
||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||
var buf [8]byte
|
||||
if _, err := unix.PtraceIO(unix.PIOD_READ_D, s.pid, uintptr(addr), buf[:], len(buf)); err != nil {
|
||||
return 0, fmt.Errorf("debug: read mem %#x: %w", addr, err)
|
||||
}
|
||||
return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 |
|
||||
uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil
|
||||
}
|
||||
|
||||
// Poke writes a word (8 bytes) to the debuggee's memory at addr, through
|
||||
// PT_IO with PIOD_WRITE_D.
|
||||
func (s *Session) Poke(addr, val uint64) error {
|
||||
buf := []byte{byte(val), byte(val >> 8), byte(val >> 16), byte(val >> 24),
|
||||
byte(val >> 32), byte(val >> 40), byte(val >> 48), byte(val >> 56)}
|
||||
if _, err := unix.PtraceIO(unix.PIOD_WRITE_D, s.pid, uintptr(addr), buf, len(buf)); err != nil {
|
||||
return fmt.Errorf("debug: write mem %#x: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadMemory reads len bytes from the debuggee's memory at addr in one
|
||||
// PT_IO request, the shape the request is built for.
|
||||
func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||
out := make([]byte, length)
|
||||
n, err := unix.PtraceIO(unix.PIOD_READ_D, s.pid, uintptr(addr), out, length)
|
||||
return out[:n], err
|
||||
}
|
||||
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr in one PT_IO
|
||||
// request.
|
||||
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||
_, err := unix.PtraceIO(unix.PIOD_WRITE_D, s.pid, uintptr(addr), data, len(data))
|
||||
return err
|
||||
}
|
||||
|
||||
// Step executes a single instruction in the debuggee.
|
||||
func (s *Session) Step() error {
|
||||
if s.exited {
|
||||
return fmt.Errorf("debug: debuggee has exited")
|
||||
}
|
||||
if err := unix.PtraceSingleStep(s.pid); err != nil {
|
||||
return fmt.Errorf("debug: PT_STEP: %w", err)
|
||||
}
|
||||
_, err := s.waitStopped()
|
||||
return err
|
||||
}
|
||||
|
||||
// Continue resumes execution until the next breakpoint or exit.
|
||||
func (s *Session) Continue() error {
|
||||
if s.exited {
|
||||
return fmt.Errorf("debug: debuggee has exited")
|
||||
}
|
||||
if err := unix.PtraceCont(s.pid, 0); err != nil {
|
||||
return fmt.Errorf("debug: PT_CONTINUE: %w", err)
|
||||
}
|
||||
_, err := s.waitStopped()
|
||||
return err
|
||||
}
|
||||
|
||||
// Exited returns true if the debuggee has terminated.
|
||||
func (s *Session) Exited() bool { return s.exited }
|
||||
|
||||
// Pid returns the debuggee's process ID.
|
||||
func (s *Session) Pid() int { return s.pid }
|
||||
|
||||
// CodeBase returns the base address of the JIT code in the debuggee.
|
||||
func (s *Session) CodeBase() uint64 { return s.codeBase }
|
||||
|
||||
// Kill terminates the debuggee and removes the session's scratch
|
||||
// directory, so a successful session leaves no gasm-debug-* debris behind.
|
||||
func (s *Session) Kill() {
|
||||
if !s.exited {
|
||||
syscall.Kill(s.pid, syscall.SIGKILL)
|
||||
syscall.Wait4(s.pid, nil, 0, nil)
|
||||
s.exited = true
|
||||
}
|
||||
if s.cmd != nil && s.cmd.Process != nil {
|
||||
s.cmd.Wait()
|
||||
}
|
||||
if s.tmpDir != "" {
|
||||
os.RemoveAll(s.tmpDir)
|
||||
s.tmpDir = ""
|
||||
}
|
||||
}
|
||||
|
||||
// execRange is one executable mapping of the debuggee.
|
||||
type execRange struct {
|
||||
lo, hi uint64
|
||||
}
|
||||
|
||||
// execRanges is a stub on FreeBSD: there is no /proc/pid/maps to parse,
|
||||
// and procfs(5) is not guaranteed to be mounted. The callers degrade
|
||||
// gracefully: archReturnAddr falls back to the raw stack convention and
|
||||
// the mapping scan is skipped.
|
||||
func execRanges(pid int) []execRange { return nil }
|
||||
|
||||
// findRWXMapping is a stub on FreeBSD for the same reason: the codebase
|
||||
// handshake file is the supported way the JIT region is located.
|
||||
func findRWXMapping(pid int) uint64 { return 0 }
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||
// converts the FreeBSD struct reg into the portable layout.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var ur unix.Reg
|
||||
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||
}
|
||||
return Regs{
|
||||
R15: uint64(ur.R15),
|
||||
R14: uint64(ur.R14),
|
||||
R13: uint64(ur.R13),
|
||||
R12: uint64(ur.R12),
|
||||
R11: uint64(ur.R11),
|
||||
R10: uint64(ur.R10),
|
||||
R9: uint64(ur.R9),
|
||||
R8: uint64(ur.R8),
|
||||
RDI: uint64(ur.Rdi),
|
||||
RSI: uint64(ur.Rsi),
|
||||
RBP: uint64(ur.Rbp),
|
||||
RBX: uint64(ur.Rbx),
|
||||
RDX: uint64(ur.Rdx),
|
||||
RCX: uint64(ur.Rcx),
|
||||
RAX: uint64(ur.Rax),
|
||||
RIP: uint64(ur.Rip),
|
||||
CS: uint64(ur.Cs),
|
||||
RFLAGS: uint64(ur.Rflags),
|
||||
RSP: uint64(ur.Rsp),
|
||||
SS: uint64(ur.Ss),
|
||||
FS: uint64(ur.Fs),
|
||||
GS: uint64(ur.Gs),
|
||||
DS: uint64(ur.Ds),
|
||||
ES: uint64(ur.Es),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
// Read-modify-write keeps the fields FreeBSD owns (trapno, err) intact.
|
||||
var ur unix.Reg
|
||||
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||
return fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||
}
|
||||
ur.R15 = int64(regs.R15)
|
||||
ur.R14 = int64(regs.R14)
|
||||
ur.R13 = int64(regs.R13)
|
||||
ur.R12 = int64(regs.R12)
|
||||
ur.R11 = int64(regs.R11)
|
||||
ur.R10 = int64(regs.R10)
|
||||
ur.R9 = int64(regs.R9)
|
||||
ur.R8 = int64(regs.R8)
|
||||
ur.Rdi = int64(regs.RDI)
|
||||
ur.Rsi = int64(regs.RSI)
|
||||
ur.Rbp = int64(regs.RBP)
|
||||
ur.Rbx = int64(regs.RBX)
|
||||
ur.Rdx = int64(regs.RDX)
|
||||
ur.Rcx = int64(regs.RCX)
|
||||
ur.Rax = int64(regs.RAX)
|
||||
ur.Rip = int64(regs.RIP)
|
||||
ur.Cs = int64(regs.CS)
|
||||
ur.Rflags = int64(regs.RFLAGS)
|
||||
ur.Rsp = int64(regs.RSP)
|
||||
ur.Ss = int64(regs.SS)
|
||||
return unix.PtraceSetRegs(s.pid, &ur)
|
||||
}
|
||||
|
||||
// FPRegs holds the x87 FPU and SSE (XMM) register state, the FXSAVE image
|
||||
// the FreeBSD struct fpreg mirrors: XMM0-15 at the same offsets.
|
||||
type FPRegs struct {
|
||||
XMM [16][16]byte // XMM0-15
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FPU/SSE register state via PT_GETFPREGS. The
|
||||
// FreeBSD struct fpreg mirrors the FXSAVE image: the x87 environment and
|
||||
// stack in Env/Acc, XMM0-15 in Xacc.
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
var fr unix.FpReg
|
||||
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||
}
|
||||
for i := range 16 {
|
||||
copy(fp.XMM[i][:], fr.Xacc[i][:])
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
// VectorRegs holds the YMM register state.
|
||||
type VectorRegs struct {
|
||||
YMM [16][32]byte // YMM0-15 (full 256-bit values)
|
||||
}
|
||||
|
||||
// The XSAVE area the PT_GETXSTATE request returns follows the architectural
|
||||
// layout (Intel SDM vol 1, "XSAVE"): the 512-byte legacy FXSAVE image (x87
|
||||
// state in 0-159, XMM0-15 in 160-511), then the 64-byte xsave header whose
|
||||
// first 8 bytes are xstate_bv, then one component per set feature bit, each
|
||||
// 64-byte aligned. The YMM high halves are the first extended component,
|
||||
// at offset 576; XFEATURE_STATE_BIT_AVX is bit 2 of xstate_bv.
|
||||
const (
|
||||
xsaveXMMOffset = 160
|
||||
xsaveHeaderOffset = 512
|
||||
xsaveBVOffset = xsaveHeaderOffset
|
||||
ymmOffset = xsaveHeaderOffset + 64 // 576
|
||||
ymmSize = 256 // 16 registers, 16 bytes each
|
||||
xfeatureMaskYMM = 1 << 2
|
||||
xstateMaxBuffer = 4096 // PT_GETXSTATE_INFO bounds the size far below this
|
||||
)
|
||||
|
||||
// GetVectorRegs retrieves the YMM registers via PT_GETXSTATE. The low
|
||||
// (XMM) halves always come from the legacy image; the high halves are
|
||||
// copied only when xstate_bv reports the AVX state, and read as zero
|
||||
// otherwise. When the request fails the FP image still provides correct
|
||||
// XMM halves, so that is the fallback.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
var v VectorRegs
|
||||
buf := make([]byte, xstateMaxBuffer)
|
||||
n, _, errno := unix.Syscall6(
|
||||
unix.SYS_PTRACE,
|
||||
uintptr(unix.PT_GETXSTATE),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
for i := range 16 {
|
||||
copy(v.YMM[i][:16], fp.XMM[i][:])
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
for i := range 16 {
|
||||
copy(v.YMM[i][:16], buf[xsaveXMMOffset+16*i:xsaveXMMOffset+16*i+16])
|
||||
}
|
||||
if int(n) >= ymmOffset+ymmSize {
|
||||
if binary.LittleEndian.Uint64(buf[xsaveBVOffset:xsaveBVOffset+8])&xfeatureMaskYMM != 0 {
|
||||
for i := range 16 {
|
||||
copy(v.YMM[i][16:], buf[ymmOffset+16*i:ymmOffset+16*i+16])
|
||||
}
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||
// converts the FreeBSD struct reg (x[30], lr, sp, elr, spsr) into the
|
||||
// portable layout.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var ur unix.Reg
|
||||
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||
}
|
||||
return Regs{
|
||||
X0: ur.X[0],
|
||||
X1: ur.X[1],
|
||||
X2: ur.X[2],
|
||||
X3: ur.X[3],
|
||||
X4: ur.X[4],
|
||||
X5: ur.X[5],
|
||||
X6: ur.X[6],
|
||||
X7: ur.X[7],
|
||||
X8: ur.X[8],
|
||||
X9: ur.X[9],
|
||||
X10: ur.X[10],
|
||||
X11: ur.X[11],
|
||||
X12: ur.X[12],
|
||||
X13: ur.X[13],
|
||||
X14: ur.X[14],
|
||||
X15: ur.X[15],
|
||||
X16: ur.X[16],
|
||||
X17: ur.X[17],
|
||||
X18: ur.X[18],
|
||||
X19: ur.X[19],
|
||||
X20: ur.X[20],
|
||||
X21: ur.X[21],
|
||||
X22: ur.X[22],
|
||||
X23: ur.X[23],
|
||||
X24: ur.X[24],
|
||||
X25: ur.X[25],
|
||||
X26: ur.X[26],
|
||||
X27: ur.X[27],
|
||||
X28: ur.X[28],
|
||||
X29: ur.X[29],
|
||||
X30: ur.Lr,
|
||||
SP: ur.Sp,
|
||||
PC: ur.Elr,
|
||||
PSTATE: uint64(ur.Spsr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
var ur unix.Reg
|
||||
ur.X = [30]uint64{
|
||||
regs.X0, regs.X1, regs.X2, regs.X3, regs.X4, regs.X5, regs.X6,
|
||||
regs.X7, regs.X8, regs.X9, regs.X10, regs.X11, regs.X12, regs.X13,
|
||||
regs.X14, regs.X15, regs.X16, regs.X17, regs.X18, regs.X19, regs.X20,
|
||||
regs.X21, regs.X22, regs.X23, regs.X24, regs.X25, regs.X26, regs.X27,
|
||||
regs.X28, regs.X29,
|
||||
}
|
||||
ur.Lr = regs.X30
|
||||
ur.Sp = regs.SP
|
||||
ur.Elr = regs.PC
|
||||
ur.Spsr = uint32(regs.PSTATE)
|
||||
return unix.PtraceSetRegs(s.pid, &ur)
|
||||
}
|
||||
|
||||
// FPRegs holds the arm64 FP/NEON register state: the 32 128-bit V
|
||||
// registers, then FPSR and FPCR (the user_fpsimd shape).
|
||||
type FPRegs struct {
|
||||
V [32][16]byte // V0-V31 (128-bit NEON/FP registers)
|
||||
FPSR uint32
|
||||
FPCR uint32
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FP/NEON register state via PT_GETFPREGS. The
|
||||
// FreeBSD struct fpreg holds the 32 128-bit V registers followed by FPSR
|
||||
// and FPCR, the user_fpsimd shape.
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
var fr unix.FpReg
|
||||
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||
}
|
||||
for i := range 32 {
|
||||
copy(fp.V[i][:], fr.Q[i][:])
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
// VectorRegs holds the full SIMD register state.
|
||||
type VectorRegs struct {
|
||||
V [32][16]byte // V0-V31 (128-bit)
|
||||
}
|
||||
|
||||
// GetVectorRegs retrieves the SIMD registers.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
var v VectorRegs
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
copy(v.V[:][:], fp.V[:][:])
|
||||
return v, nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||
// converts the FreeBSD struct reg into the portable layout. Sstatus rides
|
||||
// the kernel's struct but the portable surface carries the GPRs and PC.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var ur unix.Reg
|
||||
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||
}
|
||||
return Regs{
|
||||
PC: ur.Sepc,
|
||||
Ra: ur.Ra,
|
||||
Sp: ur.Sp,
|
||||
Gp: ur.Gp,
|
||||
Tp: ur.Tp,
|
||||
T0: ur.T[0],
|
||||
T1: ur.T[1],
|
||||
T2: ur.T[2],
|
||||
S0: ur.S[0],
|
||||
S1: ur.S[1],
|
||||
A0: ur.A[0],
|
||||
A1: ur.A[1],
|
||||
A2: ur.A[2],
|
||||
A3: ur.A[3],
|
||||
A4: ur.A[4],
|
||||
A5: ur.A[5],
|
||||
A6: ur.A[6],
|
||||
A7: ur.A[7],
|
||||
S2: ur.S[2],
|
||||
S3: ur.S[3],
|
||||
S4: ur.S[4],
|
||||
S5: ur.S[5],
|
||||
S6: ur.S[6],
|
||||
S7: ur.S[7],
|
||||
S8: ur.S[8],
|
||||
S9: ur.S[9],
|
||||
S10: ur.S[10],
|
||||
S11: ur.S[11],
|
||||
T3: ur.T[3],
|
||||
T4: ur.T[4],
|
||||
T5: ur.T[5],
|
||||
T6: ur.T[6],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
// Read-modify-write keeps sstatus, which the kernel owns, intact.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
var ur unix.Reg
|
||||
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||
return fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||
}
|
||||
ur.Sepc = regs.PC
|
||||
ur.Ra = regs.Ra
|
||||
ur.Sp = regs.Sp
|
||||
ur.Gp = regs.Gp
|
||||
ur.Tp = regs.Tp
|
||||
ur.T = [7]uint64{regs.T0, regs.T1, regs.T2, regs.T3, regs.T4, regs.T5, regs.T6}
|
||||
ur.S = [12]uint64{regs.S0, regs.S1, regs.S2, regs.S3, regs.S4, regs.S5,
|
||||
regs.S6, regs.S7, regs.S8, regs.S9, regs.S10, regs.S11}
|
||||
ur.A = [8]uint64{regs.A0, regs.A1, regs.A2, regs.A3, regs.A4, regs.A5, regs.A6, regs.A7}
|
||||
return unix.PtraceSetRegs(s.pid, &ur)
|
||||
}
|
||||
|
||||
// FPRegs holds the RISC-V FP register state (32 64-bit FP registers plus
|
||||
// fcsr).
|
||||
type FPRegs struct {
|
||||
F [32]uint64 // F0-F31 (64-bit FP registers)
|
||||
FCSR uint32
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FP register state via PT_GETFPREGS. The FreeBSD
|
||||
// struct fpreg carries each 64-bit FP register in a 128-bit slot (fp_x is
|
||||
// the flat [64]-word area the x/sys type renders as [32][2]); the low word
|
||||
// holds the register, and FCSR rides the tail.
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
var fr unix.FpReg
|
||||
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||
}
|
||||
for i := range 32 {
|
||||
fp.F[i] = fr.X[i][0]
|
||||
}
|
||||
fp.FCSR = uint32(fr.Fcsr)
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
// VectorRegs holds the FP register state shown by the regs command
|
||||
// (riscv64 has 32 64-bit FP registers and fcsr).
|
||||
type VectorRegs struct {
|
||||
F [32]uint64
|
||||
FCSR uint32
|
||||
}
|
||||
|
||||
// GetVectorRegs retrieves the FP registers.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return VectorRegs{}, err
|
||||
}
|
||||
return VectorRegs{F: fp.F, FCSR: fp.FCSR}, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// TestLaunchAndBreakpoint is the FreeBSD twin of the Linux integration
|
||||
// test: it drives the whole launch, breakpoint, trap and register-rewind
|
||||
// flow end to end. It needs a real FreeBSD kernel (ptrace does not work
|
||||
// under emulation), so it only runs where it can.
|
||||
func TestLaunchAndBreakpoint(t *testing.T) {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
t.Skip("runs only on amd64 hosts")
|
||||
}
|
||||
// The tracer is the OS thread that forked the debuggee (PT_TRACE_ME
|
||||
// binds the relation to that thread); every ptrace request must come
|
||||
// from the same thread, so pin the test goroutine to one thread.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
out, err := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("build gasm: %v: %s", err, out)
|
||||
}
|
||||
|
||||
const kernelPath = "../testdata/verify/basic_amd64.s"
|
||||
k, err := verify.Load(kernelPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
t.Cleanup(k.Close)
|
||||
fl, err := k.Func("wideCopy")
|
||||
if err != nil {
|
||||
t.Fatalf("Func: %v", err)
|
||||
}
|
||||
|
||||
sess, err := Launch(bin, kernelPath, "wideCopy", make([]byte, fl.Args))
|
||||
if err != nil {
|
||||
t.Fatalf("Launch: %v", err)
|
||||
}
|
||||
t.Cleanup(sess.Kill)
|
||||
|
||||
bm := NewBreakpoints(sess)
|
||||
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||
t.Fatalf("Set: %v", err)
|
||||
}
|
||||
|
||||
// The INT3 must be visible in the debuggee's memory.
|
||||
word, err := sess.Peek(entry)
|
||||
if err != nil {
|
||||
t.Fatalf("Peek: %v", err)
|
||||
}
|
||||
if b := word & 0xFF; b != 0xCC {
|
||||
t.Fatalf("int3 not patched: first byte %#02x at %#x", b, entry)
|
||||
}
|
||||
|
||||
// The debuggee raises a second SIGSTOP after the launch barrier (the
|
||||
// child's RunTarget marks its entry), so like the REPL and the cover
|
||||
// mode the test keeps resuming until the breakpoint trap arrives.
|
||||
for range 10 {
|
||||
if err := sess.Continue(); err != nil {
|
||||
t.Fatalf("Continue: %v", err)
|
||||
}
|
||||
if sess.Exited() {
|
||||
t.Fatal("debuggee exited instead of trapping on the breakpoint")
|
||||
}
|
||||
regs, err := sess.GetRegs()
|
||||
if err != nil {
|
||||
t.Fatalf("GetRegs: %v", err)
|
||||
}
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
if bp.Addr != entry {
|
||||
t.Fatalf("trap at %#x, want %#x", bp.Addr, entry)
|
||||
}
|
||||
return // trap on the entry breakpoint: the whole flow works
|
||||
}
|
||||
}
|
||||
t.Fatal("no breakpoint trap after 10 resumes")
|
||||
}
|
||||
@@ -14,17 +14,26 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// buildGasm produces the gasm binary the debugger spawns as its debuggee.
|
||||
// Every live ptrace test funnels through here, so this is also where the
|
||||
// deliberate-run boundary sits: under -short (the push pipeline's mode) the
|
||||
// live sessions skip, because a real debuggee's launch handshake needs the
|
||||
// machine to itself and a starved single-core runner turns each one into a
|
||||
// timeout that burns the step's whole budget. The local test gate and the
|
||||
// dispatched workflows run them in full.
|
||||
func buildGasm(t *testing.T) string {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("live ptrace session: skipped in -short mode")
|
||||
}
|
||||
if p := os.Getenv("GASM_TEST_BIN"); p != "" {
|
||||
return p
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm")
|
||||
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("build gasm: %v: %s", err, out)
|
||||
|
||||
+45
-22
@@ -91,6 +91,16 @@ func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec s
|
||||
if _, err := os.Stat(readyFile); err == nil {
|
||||
break
|
||||
}
|
||||
// A debuggee that died before signalling readiness (unknown
|
||||
// function, unparseable source) writes its failure notice to the
|
||||
// handshake directory; read it and fail fast. The poll never
|
||||
// waits on the child: a wait here could consume the SIGSTOP park
|
||||
// that waitStopped below must receive, hanging the launch.
|
||||
if err := s.deadReason(); err != nil {
|
||||
cmd.Wait()
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, err
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -131,6 +141,18 @@ func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec s
|
||||
return s, bufAddrs, nil
|
||||
}
|
||||
|
||||
// deadReason reports the debuggee's own failure notice, the file its
|
||||
// failure paths write before exiting. A debuggee killed without a notice
|
||||
// (a crash, SIGKILL) surfaces through waitStopped after the poll instead,
|
||||
// which is why the poll's budget stays finite.
|
||||
func (s *Session) deadReason() error {
|
||||
data, err := os.ReadFile(filepath.Join(s.tmpDir, "dead"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("debug: debuggee failed before signalling readiness: %s", strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step), the
|
||||
// debuggee's own SIGSTOP, or a genuine signal-delivery-stop. A Go tracee's
|
||||
@@ -219,40 +241,41 @@ func (s *Session) Poke(addr, val uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadMemory reads len bytes from the debuggee's memory at addr.
|
||||
// ReadMemory reads len bytes from the debuggee's memory at addr. The read
|
||||
// covers exactly the requested range: the old word-at-a-time loop read a
|
||||
// whole 8-byte word for the final partial word, so a request that ended
|
||||
// inside the last mapped page failed whenever the following page was
|
||||
// unmapped, even though every requested byte was readable.
|
||||
func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||
out := make([]byte, length)
|
||||
for i := 0; i < length; i += 8 {
|
||||
word, err := s.Peek(addr + uint64(i))
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return out[:i], err
|
||||
}
|
||||
for j := 0; j < 8 && i+j < length; j++ {
|
||||
out[i+j] = byte(word >> (8 * j))
|
||||
return out, fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
n, err := mem.ReadAt(out, int64(addr))
|
||||
if err != nil {
|
||||
return out[:n], fmt.Errorf("debug: read mem %#x: %w", addr, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr.
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr. The write
|
||||
// covers exactly the given bytes: /proc/pid/mem accepts writes of any
|
||||
// length at any offset, so the word loop's read-modify-write of the final
|
||||
// partial word (which read past the requested range and failed on an
|
||||
// unmapped following page) is unnecessary.
|
||||
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||
for i := 0; i < len(data); i += 8 {
|
||||
end := min(i+8, len(data))
|
||||
var word uint64
|
||||
for j := 0; j < end-i; j++ {
|
||||
word |= uint64(data[i+j]) << (8 * j)
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if end-i < 8 {
|
||||
existing, err := s.Peek(addr + uint64(i))
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mask := ^((uint64(1) << (8 * (end - i))) - 1)
|
||||
word = (existing & mask) | word
|
||||
}
|
||||
if err := s.Poke(addr+uint64(i), word); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
if _, err := mem.WriteAt(data, int64(addr)); err != nil {
|
||||
return fmt.Errorf("debug: write mem %#x: %w", addr, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the FreeBSD amd64 struct reg layout, sys/x86/include/reg.h). FreeBSD
|
||||
// reports segment selectors (FS/GS/ES/DS), not the bases the Linux ptrace
|
||||
// surface carries, and has no ORIG_RAX slot.
|
||||
type Regs struct {
|
||||
R15 uint64
|
||||
R14 uint64
|
||||
R13 uint64
|
||||
R12 uint64
|
||||
RBP uint64
|
||||
RBX uint64
|
||||
R11 uint64
|
||||
R10 uint64
|
||||
R9 uint64
|
||||
R8 uint64
|
||||
RAX uint64
|
||||
RCX uint64
|
||||
RDX uint64
|
||||
RSI uint64
|
||||
RDI uint64
|
||||
RIP uint64
|
||||
CS uint64
|
||||
RFLAGS uint64
|
||||
RSP uint64
|
||||
SS uint64
|
||||
FS uint64
|
||||
GS uint64
|
||||
DS uint64
|
||||
ES uint64
|
||||
}
|
||||
|
||||
// GetPC returns the program counter.
|
||||
func (r *Regs) GetPC() uint64 { return r.RIP }
|
||||
|
||||
// SetPC sets the program counter.
|
||||
func (r *Regs) SetPC(pc uint64) { r.RIP = pc }
|
||||
|
||||
// GetSP returns the stack pointer.
|
||||
func (r *Regs) GetSP() uint64 { return r.RSP }
|
||||
|
||||
// RegValue returns the value of the named register, or false if unknown.
|
||||
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
switch name {
|
||||
case "rax", "eax", "ax", "al":
|
||||
return r.RAX, true
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
return r.RBX, true
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
return r.RCX, true
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
return r.RDX, true
|
||||
case "rsi", "esi", "si":
|
||||
return r.RSI, true
|
||||
case "rdi", "edi", "di":
|
||||
return r.RDI, true
|
||||
case "rbp", "ebp", "bp":
|
||||
return r.RBP, true
|
||||
case "rsp", "esp", "sp":
|
||||
return r.RSP, true
|
||||
case "r8":
|
||||
return r.R8, true
|
||||
case "r9":
|
||||
return r.R9, true
|
||||
case "r10":
|
||||
return r.R10, true
|
||||
case "r11":
|
||||
return r.R11, true
|
||||
case "r12":
|
||||
return r.R12, true
|
||||
case "r13":
|
||||
return r.R13, true
|
||||
case "r14":
|
||||
return r.R14, true
|
||||
case "r15":
|
||||
return r.R15, true
|
||||
case "rip", "eip":
|
||||
return r.RIP, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// breakpointInsn is the software breakpoint instruction.
|
||||
var breakpointInsn = []byte{0xCC} // INT3
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap. INT3 leaves the hardware PC on the following instruction (Intel
|
||||
// SDM vol 3, "Debug Exceptions") and the FreeBSD T_BPTFLT path delivers
|
||||
// that frame unmodified (sys/amd64/amd64/trap.c), so the trap address is
|
||||
// PC-1, the same correction the Linux side applies.
|
||||
const breakpointPCAdjust = 1
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the FreeBSD arm64 struct reg layout, sys/arm64/include/reg.h: x[30], lr,
|
||||
// sp, elr, spsr).
|
||||
type Regs struct {
|
||||
X0 uint64
|
||||
X1 uint64
|
||||
X2 uint64
|
||||
X3 uint64
|
||||
X4 uint64
|
||||
X5 uint64
|
||||
X6 uint64
|
||||
X7 uint64
|
||||
X8 uint64
|
||||
X9 uint64
|
||||
X10 uint64
|
||||
X11 uint64
|
||||
X12 uint64
|
||||
X13 uint64
|
||||
X14 uint64
|
||||
X15 uint64
|
||||
X16 uint64
|
||||
X17 uint64
|
||||
X18 uint64
|
||||
X19 uint64
|
||||
X20 uint64
|
||||
X21 uint64
|
||||
X22 uint64
|
||||
X23 uint64
|
||||
X24 uint64
|
||||
X25 uint64
|
||||
X26 uint64
|
||||
X27 uint64
|
||||
X28 uint64
|
||||
X29 uint64 // FP (frame pointer)
|
||||
X30 uint64 // LR (link register)
|
||||
SP uint64
|
||||
PC uint64
|
||||
PSTATE uint64
|
||||
}
|
||||
|
||||
// GetPC returns the program counter.
|
||||
func (r *Regs) GetPC() uint64 { return r.PC }
|
||||
|
||||
// SetPC sets the program counter.
|
||||
func (r *Regs) SetPC(pc uint64) { r.PC = pc }
|
||||
|
||||
// GetSP returns the stack pointer.
|
||||
func (r *Regs) GetSP() uint64 { return r.SP }
|
||||
|
||||
// RegValue returns the value of the named register, or false if unknown.
|
||||
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
switch name {
|
||||
case "x0":
|
||||
return r.X0, true
|
||||
case "x1":
|
||||
return r.X1, true
|
||||
case "x2":
|
||||
return r.X2, true
|
||||
case "x3":
|
||||
return r.X3, true
|
||||
case "x4":
|
||||
return r.X4, true
|
||||
case "x5":
|
||||
return r.X5, true
|
||||
case "x6":
|
||||
return r.X6, true
|
||||
case "x7":
|
||||
return r.X7, true
|
||||
case "x8":
|
||||
return r.X8, true
|
||||
case "x9":
|
||||
return r.X9, true
|
||||
case "x10":
|
||||
return r.X10, true
|
||||
case "x11":
|
||||
return r.X11, true
|
||||
case "x12":
|
||||
return r.X12, true
|
||||
case "x13":
|
||||
return r.X13, true
|
||||
case "x14":
|
||||
return r.X14, true
|
||||
case "x15":
|
||||
return r.X15, true
|
||||
case "x16":
|
||||
return r.X16, true
|
||||
case "x17":
|
||||
return r.X17, true
|
||||
case "x18":
|
||||
return r.X18, true
|
||||
case "x19":
|
||||
return r.X19, true
|
||||
case "x20":
|
||||
return r.X20, true
|
||||
case "x21":
|
||||
return r.X21, true
|
||||
case "x22":
|
||||
return r.X22, true
|
||||
case "x23":
|
||||
return r.X23, true
|
||||
case "x24":
|
||||
return r.X24, true
|
||||
case "x25":
|
||||
return r.X25, true
|
||||
case "x26":
|
||||
return r.X26, true
|
||||
case "x27":
|
||||
return r.X27, true
|
||||
case "x28":
|
||||
return r.X28, true
|
||||
case "x29", "fp":
|
||||
return r.X29, true
|
||||
case "x30", "lr":
|
||||
return r.X30, true
|
||||
case "sp":
|
||||
return r.SP, true
|
||||
case "pc":
|
||||
return r.PC, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// breakpointInsn is the software breakpoint instruction (BRK #0).
|
||||
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap: 0. The BRK synchronous exception leaves ELR_EL0 on the BRK
|
||||
// itself (ARM DDI 0487), and the FreeBSD EXCP_BRKPT_EL0 handler delivers
|
||||
// the frame's elr unmodified (sys/arm64/arm64/trap.c), so the trap address
|
||||
// is the PC as reported.
|
||||
const breakpointPCAdjust = 0
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the FreeBSD riscv64 struct reg layout: ra, sp, gp, tp, t0-t6, s0-s11,
|
||||
// a0-a7, sepc, sstatus).
|
||||
type Regs struct {
|
||||
PC uint64 // sepc
|
||||
Ra uint64 // x1 (return address)
|
||||
Sp uint64 // x2
|
||||
Gp uint64 // x3
|
||||
Tp uint64 // x4
|
||||
T0 uint64 // x5
|
||||
T1 uint64 // x6
|
||||
T2 uint64 // x7
|
||||
S0 uint64 // x8 (frame pointer)
|
||||
S1 uint64 // x9
|
||||
A0 uint64 // x10
|
||||
A1 uint64 // x11
|
||||
A2 uint64 // x12
|
||||
A3 uint64 // x13
|
||||
A4 uint64 // x14
|
||||
A5 uint64 // x15
|
||||
A6 uint64 // x16
|
||||
A7 uint64 // x17
|
||||
S2 uint64 // x18
|
||||
S3 uint64 // x19
|
||||
S4 uint64 // x20
|
||||
S5 uint64 // x21
|
||||
S6 uint64 // x22
|
||||
S7 uint64 // x23
|
||||
S8 uint64 // x24
|
||||
S9 uint64 // x25
|
||||
S10 uint64 // x26
|
||||
S11 uint64 // x27
|
||||
T3 uint64 // x28
|
||||
T4 uint64 // x29
|
||||
T5 uint64 // x30
|
||||
T6 uint64 // x31
|
||||
}
|
||||
|
||||
// GetPC returns the program counter.
|
||||
func (r *Regs) GetPC() uint64 { return r.PC }
|
||||
|
||||
// SetPC sets the program counter.
|
||||
func (r *Regs) SetPC(pc uint64) { r.PC = pc }
|
||||
|
||||
// GetSP returns the stack pointer.
|
||||
func (r *Regs) GetSP() uint64 { return r.Sp }
|
||||
|
||||
// RegValue returns the value of the named register, or false if unknown.
|
||||
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
switch name {
|
||||
case "pc":
|
||||
return r.PC, true
|
||||
case "ra", "x1":
|
||||
return r.Ra, true
|
||||
case "sp", "x2":
|
||||
return r.Sp, true
|
||||
case "gp", "x3":
|
||||
return r.Gp, true
|
||||
case "tp", "x4":
|
||||
return r.Tp, true
|
||||
case "t0", "x5":
|
||||
return r.T0, true
|
||||
case "t1", "x6":
|
||||
return r.T1, true
|
||||
case "t2", "x7":
|
||||
return r.T2, true
|
||||
case "s0", "fp", "x8":
|
||||
return r.S0, true
|
||||
case "s1", "x9":
|
||||
return r.S1, true
|
||||
case "a0", "x10":
|
||||
return r.A0, true
|
||||
case "a1", "x11":
|
||||
return r.A1, true
|
||||
case "a2", "x12":
|
||||
return r.A2, true
|
||||
case "a3", "x13":
|
||||
return r.A3, true
|
||||
case "a4", "x14":
|
||||
return r.A4, true
|
||||
case "a5", "x15":
|
||||
return r.A5, true
|
||||
case "a6", "x16":
|
||||
return r.A6, true
|
||||
case "a7", "x17":
|
||||
return r.A7, true
|
||||
case "s2", "x18":
|
||||
return r.S2, true
|
||||
case "s3", "x19":
|
||||
return r.S3, true
|
||||
case "s4", "x20":
|
||||
return r.S4, true
|
||||
case "s5", "x21":
|
||||
return r.S5, true
|
||||
case "s6", "x22":
|
||||
return r.S6, true
|
||||
case "s7", "x23":
|
||||
return r.S7, true
|
||||
case "s8", "x24":
|
||||
return r.S8, true
|
||||
case "s9", "x25":
|
||||
return r.S9, true
|
||||
case "s10", "x26":
|
||||
return r.S10, true
|
||||
case "s11", "x27":
|
||||
return r.S11, true
|
||||
case "t3", "x28":
|
||||
return r.T3, true
|
||||
case "t4", "x29":
|
||||
return r.T4, true
|
||||
case "t5", "x30":
|
||||
return r.T5, true
|
||||
case "t6", "x31":
|
||||
return r.T6, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// breakpointInsn is the software breakpoint instruction (EBREAK).
|
||||
var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||
// a trap: 0. The EBREAK synchronous exception leaves sepc on the ebreak
|
||||
// itself (RISC-V privileged architecture), so the trap address is the PC as
|
||||
// reported.
|
||||
const breakpointPCAdjust = 0
|
||||
+63
-15
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||
|
||||
package debug
|
||||
|
||||
@@ -71,7 +71,12 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
case "step", "s":
|
||||
n := 1
|
||||
if len(parts) > 1 {
|
||||
n, _ = strconv.Atoi(parts[1])
|
||||
v, err := strconv.Atoi(parts[1])
|
||||
if err != nil || v < 0 {
|
||||
fmt.Printf("invalid count: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
n = v
|
||||
}
|
||||
for range n {
|
||||
if s.Exited() {
|
||||
@@ -82,8 +87,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
break
|
||||
}
|
||||
if !s.Exited() {
|
||||
}
|
||||
if !s.Exited() && s.LastSignal() == 0 {
|
||||
regs, _ := s.GetRegs()
|
||||
pc := regs.GetPC()
|
||||
text, _, _ := s.Disassemble(pc)
|
||||
@@ -106,16 +116,16 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
bm.Clear(afterAddr)
|
||||
clearNextBp(bm, afterAddr)
|
||||
continue
|
||||
}
|
||||
if s.Exited() {
|
||||
bm.Clear(afterAddr)
|
||||
clearNextBp(bm, afterAddr)
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
bm.Clear(afterAddr)
|
||||
clearNextBp(bm, afterAddr)
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
continue
|
||||
@@ -125,12 +135,17 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
// snapshot, and a stale SetRegs would clobber live state.
|
||||
regs, _ = s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(afterAddr)
|
||||
clearNextBp(bm, afterAddr)
|
||||
} else {
|
||||
if err := s.Step(); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
@@ -156,16 +171,16 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
if err := s.Continue(); err != nil {
|
||||
fmt.Println(err)
|
||||
bm.Clear(retAddr)
|
||||
clearNextBp(bm, retAddr)
|
||||
continue
|
||||
}
|
||||
if s.Exited() {
|
||||
bm.Clear(retAddr)
|
||||
clearNextBp(bm, retAddr)
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
if sig := s.LastSignal(); sig != 0 {
|
||||
bm.Clear(retAddr)
|
||||
clearNextBp(bm, retAddr)
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("stopped on signal %v at %#x\n", sig, regs.GetPC())
|
||||
continue
|
||||
@@ -174,7 +189,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
// does: HandleTrap must see the PC the trap left behind.
|
||||
regs, _ = s.GetRegs()
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(retAddr)
|
||||
clearNextBp(bm, retAddr)
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
} else {
|
||||
@@ -214,6 +229,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
break
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
trapPC := regs.GetPC()
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
// Execute the instruction under the restored breakpoint
|
||||
// so the next continue cannot re-trap on the same
|
||||
@@ -229,6 +245,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
||||
break
|
||||
}
|
||||
// The trap matched no breakpoint of ours. When the PC still
|
||||
// stands on the trapping instruction, resuming would re-execute
|
||||
// it and trap forever, so surface the stop instead of spinning.
|
||||
if TrapStray(s, reason, trapPC) {
|
||||
fmt.Printf("SIGTRAP at %#x matches no breakpoint; the PC did not advance\n", trapPC-uint64(breakpointPCAdjust))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "break", "b":
|
||||
@@ -326,6 +349,10 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
length := 64
|
||||
if len(parts) > 1 {
|
||||
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
if addr == 0 {
|
||||
fmt.Printf("unknown address: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
// A malformed or non-positive length would panic
|
||||
@@ -399,9 +426,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
case "disas", "u":
|
||||
n := 5
|
||||
if len(parts) > 1 {
|
||||
n, _ = strconv.Atoi(parts[1])
|
||||
if n <= 0 {
|
||||
n = 5
|
||||
v, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
fmt.Printf("invalid count: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
if v > 0 {
|
||||
n = v
|
||||
}
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
@@ -496,10 +527,18 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
typ = WatchRead
|
||||
case "w":
|
||||
typ = WatchWrite
|
||||
default:
|
||||
fmt.Printf("unknown watchpoint type: %s (want r or w)\n", parts[2])
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(parts) > 3 {
|
||||
size, _ = strconv.Atoi(parts[3])
|
||||
v, err := strconv.Atoi(parts[3])
|
||||
if err != nil || v <= 0 {
|
||||
fmt.Printf("invalid size: %s\n", parts[3])
|
||||
continue
|
||||
}
|
||||
size = v
|
||||
}
|
||||
slot := s.FindFreeWatchpointSlot()
|
||||
if slot < 0 {
|
||||
@@ -543,6 +582,15 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
s.Kill()
|
||||
}
|
||||
|
||||
// clearNextBp removes one of the temporary breakpoints the next and finish
|
||||
// commands plant, reporting a failure instead of silently leaving the trap
|
||||
// instruction behind in the debuggee.
|
||||
func clearNextBp(bm *Breakpoints, addr uint64) {
|
||||
if err := bm.Clear(addr); err != nil {
|
||||
fmt.Printf("cannot remove temporary breakpoint at %#x: %v\n", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func hexDump(addr uint64, data []byte) {
|
||||
for i := 0; i < len(data); i += 16 {
|
||||
end := min(i+16, len(data))
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// FreeBSD TRAP_* si_code values (sys/signal.h). A breakpoint (INT3, BRK,
|
||||
// EBREAK) arrives as TRAP_BRKPT on every supported architecture; TRAP_TRACE
|
||||
// is shared by the completed single-step and the hardware watchpoint hit,
|
||||
// so the watchpoint layer disambiguates from the debug registers.
|
||||
const (
|
||||
trapBRKPT = 1 // TRAP_BRKPT
|
||||
trapTRACE = 2 // TRAP_TRACE
|
||||
)
|
||||
|
||||
// StopReason describes why the debuggee stopped.
|
||||
type StopReason int
|
||||
|
||||
const (
|
||||
StopNone StopReason = iota
|
||||
StopBreakpoint // software breakpoint hit
|
||||
StopWatchpoint // hardware watchpoint triggered
|
||||
StopSingleStep // single-step completed
|
||||
StopSignal // stopped by a signal
|
||||
StopExited // process exited
|
||||
)
|
||||
|
||||
// StopInfo returns the reason the debuggee stopped and the faulting address
|
||||
// (for watchpoints, the watched address that was accessed). FreeBSD has no
|
||||
// PTRACE_GETSIGINFO; the stop's signal information comes from PT_LWPINFO,
|
||||
// whose pl_siginfo carries the siginfo the kernel delivered. A ptrace stop
|
||||
// with no signal behind it (a completed single-step, the initial attach)
|
||||
// fills no siginfo at all.
|
||||
func (s *Session) StopInfo() (StopReason, uint64) {
|
||||
if s.exited {
|
||||
return StopExited, 0
|
||||
}
|
||||
var info unix.PtraceLwpInfoStruct
|
||||
if err := unix.PtraceLwpInfo(s.pid, &info); err != nil {
|
||||
return StopNone, 0
|
||||
}
|
||||
// The siginfo layout is the FreeBSD siginfo_t: three leading ints
|
||||
// (signo, errno, code), then the union, 8-byte aligned, whose _fault
|
||||
// member puts the address at byte offset 16. The read is byte-wise
|
||||
// because the blob's alignment is not guaranteed.
|
||||
si := (*[64]byte)(unsafe.Pointer(&info.Siginfo))
|
||||
signo := int32(binary.LittleEndian.Uint32(si[0:4]))
|
||||
code := int32(binary.LittleEndian.Uint32(si[8:12]))
|
||||
switch {
|
||||
case signo == 0:
|
||||
// A pure ptrace stop: single-step completion, attach, or the
|
||||
// events the kernel resolves internally.
|
||||
return StopSingleStep, 0
|
||||
case signo != int32(syscall.SIGTRAP):
|
||||
return StopSignal, uint64(code)
|
||||
case code == trapBRKPT:
|
||||
return StopBreakpoint, 0
|
||||
case code == trapTRACE:
|
||||
addr := binary.LittleEndian.Uint64(si[16:24])
|
||||
return archStopTrace(s, addr)
|
||||
default:
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// mapRWX maps code into a read-write-execute region.
|
||||
func mapRWX(code []byte) ([]byte, error) {
|
||||
const pageSize = 4096
|
||||
size := (len(code) + pageSize - 1) &^ (pageSize - 1)
|
||||
mem, err := syscall.Mmap(-1, 0, size,
|
||||
syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC,
|
||||
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(mem, code)
|
||||
return mem, nil
|
||||
}
|
||||
|
||||
// setupBuffers allocates buffers in the debuggee's memory.
|
||||
func setupBuffers(spec string, args []byte, tmpDir string) ([]byte, error) {
|
||||
type bufSpec struct {
|
||||
name string
|
||||
size int
|
||||
pattern string
|
||||
}
|
||||
var specs []bufSpec
|
||||
for part := range strings.SplitSeq(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) != 3 {
|
||||
continue
|
||||
}
|
||||
size, err := strconv.Atoi(fields[1])
|
||||
if err != nil || size <= 0 {
|
||||
continue
|
||||
}
|
||||
specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]})
|
||||
}
|
||||
|
||||
if len(specs) == 0 {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
var bufAddrs []uint64
|
||||
for _, s := range specs {
|
||||
buf, err := syscall.Mmap(-1, 0, s.size,
|
||||
syscall.PROT_READ|syscall.PROT_WRITE,
|
||||
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err)
|
||||
}
|
||||
fillBuffer(buf, s.pattern)
|
||||
bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0]))))
|
||||
}
|
||||
|
||||
addrFile, err := os.Create(tmpDir + "/bufaddrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range bufAddrs {
|
||||
fmt.Fprintf(addrFile, "%d\n", addr)
|
||||
}
|
||||
addrFile.Close()
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// fillBuffer fills a buffer with the specified pattern.
|
||||
func fillBuffer(buf []byte, pattern string) {
|
||||
switch pattern {
|
||||
case "zero":
|
||||
case "ones":
|
||||
for i := range buf {
|
||||
buf[i] = 0xFF
|
||||
}
|
||||
case "seq":
|
||||
for i := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
default:
|
||||
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||
for i := range buf {
|
||||
buf[i] = data[i%len(data)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunTarget is the debuggee entry point (gasm debug --target). A failure
|
||||
// is marked in the handshake directory before the process exits, so the
|
||||
// debugger's readiness poll fails fast on a dead debuggee instead of
|
||||
// waiting out its whole budget.
|
||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
err := runTarget(asmPath, funcName, argsFile, tmpDir)
|
||||
if err != nil {
|
||||
markDead(tmpDir, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
src, err := os.ReadFile(asmPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: %w", err)
|
||||
}
|
||||
file, errs := parser.Parse(asmPath, string(src))
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||
}
|
||||
img, err := asm.AssembleFile(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: assemble: %w", err)
|
||||
}
|
||||
|
||||
var fl *asm.FuncLayout
|
||||
for i := range img.Funcs {
|
||||
if img.Funcs[i].Name == funcName {
|
||||
fl = &img.Funcs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if fl == nil {
|
||||
return fmt.Errorf("debug target: function %q not found", funcName)
|
||||
}
|
||||
|
||||
code := img.Bytes()
|
||||
exec, err := mapRWX(code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: mmap: %w", err)
|
||||
}
|
||||
|
||||
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
||||
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
||||
return fmt.Errorf("debug target: write codebase: %w", err)
|
||||
}
|
||||
|
||||
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
|
||||
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
||||
|
||||
labelsFile, _ := os.Create(tmpDir + "/labels")
|
||||
if labelsFile != nil {
|
||||
for label, off := range fl.Labels {
|
||||
fmt.Fprintf(labelsFile, "%s %d\n", label, off)
|
||||
}
|
||||
labelsFile.Close()
|
||||
}
|
||||
|
||||
args, err := os.ReadFile(argsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: read args: %w", err)
|
||||
}
|
||||
if len(args) < fl.Args {
|
||||
padded := make([]byte, fl.Args)
|
||||
copy(padded, args)
|
||||
args = padded
|
||||
}
|
||||
|
||||
bufSpecFile := tmpDir + "/bufspec"
|
||||
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
||||
args, err = setupBuffers(string(bufSpec), args, tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: setup buffers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
runtime.LockOSThread()
|
||||
|
||||
if _, _, errno := unix.RawSyscall(unix.SYS_PTRACE, uintptr(unix.PT_TRACE_ME), 0, 0); errno != 0 {
|
||||
return fmt.Errorf("debug target: PT_TRACE_ME: %v", errno)
|
||||
}
|
||||
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||
|
||||
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||
|
||||
fnAddr := codeBase + uintptr(fl.Offset)
|
||||
stackArgs := make([]byte, fl.Args)
|
||||
copy(stackArgs, args)
|
||||
|
||||
if _, callErr := verify.Call(fnAddr, stackArgs); callErr != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
// Success returns to the caller, which exits with status 0; the JIT
|
||||
// code has already run to its own trampoline by the time Call returns.
|
||||
return nil
|
||||
}
|
||||
@@ -12,13 +12,24 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||
// RunTarget is the debuggee entry point (gasm debug --target). A failure
|
||||
// is marked in the handshake directory before the process exits, so the
|
||||
// debugger's readiness poll fails fast on a dead debuggee instead of
|
||||
// waiting out its whole budget.
|
||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
err := runTarget(asmPath, funcName, argsFile, tmpDir)
|
||||
if err != nil {
|
||||
markDead(tmpDir, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
src, err := os.ReadFile(asmPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: %w", err)
|
||||
|
||||
@@ -12,13 +12,24 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||
// RunTarget is the debuggee entry point (gasm debug --target). A failure
|
||||
// is marked in the handshake directory before the process exits, so the
|
||||
// debugger's readiness poll fails fast on a dead debuggee instead of
|
||||
// waiting out its whole budget.
|
||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
err := runTarget(asmPath, funcName, argsFile, tmpDir)
|
||||
if err != nil {
|
||||
markDead(tmpDir, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
src, err := os.ReadFile(asmPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: %w", err)
|
||||
|
||||
@@ -12,13 +12,24 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||
// RunTarget is the debuggee entry point (gasm debug --target). A failure
|
||||
// is marked in the handshake directory before the process exits, so the
|
||||
// debugger's readiness poll fails fast on a dead debuggee instead of
|
||||
// waiting out its whole budget.
|
||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
err := runTarget(asmPath, funcName, argsFile, tmpDir)
|
||||
if err != nil {
|
||||
markDead(tmpDir, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
src, err := os.ReadFile(asmPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: %w", err)
|
||||
|
||||
@@ -12,13 +12,24 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||
)
|
||||
|
||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||
// RunTarget is the debuggee entry point (gasm debug --target). A failure
|
||||
// is marked in the handshake directory before the process exits, so the
|
||||
// debugger's readiness poll fails fast on a dead debuggee instead of
|
||||
// waiting out its whole budget.
|
||||
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
err := runTarget(asmPath, funcName, argsFile, tmpDir)
|
||||
if err != nil {
|
||||
markDead(tmpDir, err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||
src, err := os.ReadFile(asmPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug target: %w", err)
|
||||
|
||||
+17
-1
@@ -1,10 +1,26 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// markDead records the target's failure reason in the handshake directory,
|
||||
// the death notice the debugger's readiness poll reads. The poll must not
|
||||
// wait on the child while it polls: a wait there could consume the SIGSTOP
|
||||
// park the debugger's own waitStopped must receive, hanging the launch, so
|
||||
// the file is the only fast death notice that is safe to read. A debuggee
|
||||
// killed without a notice (a crash, SIGKILL) surfaces through the ordinary
|
||||
// wait after the poll instead.
|
||||
func markDead(tmpDir, reason string) {
|
||||
os.WriteFile(filepath.Join(tmpDir, "dead"), []byte(reason), 0o644)
|
||||
}
|
||||
|
||||
// tracer abstracts the minimal ptrace operations needed by the breakpoint
|
||||
// manager and the stop-information helpers. The live implementation is
|
||||
// *Session (ptrace_linux_amd64.go); tests supply a mock.
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7),
|
||||
// read and written as one blob through PT_GETDBREGS/PT_SETDBREGS. The
|
||||
// FreeBSD struct dbreg is the raw DR file: dr[16], where DR0-DR3 are the
|
||||
// address registers, DR6 the status and DR7 the control (sys/x86/include/
|
||||
// reg.h; the DBREG_DRX accessor indexes the same array).
|
||||
|
||||
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||||
type dbreg struct {
|
||||
Dr [16]uint64
|
||||
}
|
||||
|
||||
// dbreg indices of the registers the watchpoint layer drives.
|
||||
const (
|
||||
drStatus = 6 // DR6: the trap status register
|
||||
drControl = 7 // DR7: the debug control register
|
||||
)
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
|
||||
const (
|
||||
WatchWrite WatchpointType = 1 // trigger on write
|
||||
WatchRead WatchpointType = 3 // trigger on read or write
|
||||
)
|
||||
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// architecture provides: four address registers, DR0-DR3.
|
||||
func maxWatchpoints() int { return 4 }
|
||||
|
||||
// getDbRegs reads the debug register file of the stopped debuggee.
|
||||
func (s *Session) getDbRegs() (*dbreg, error) {
|
||||
var dr dbreg
|
||||
if _, _, errno := unix.Syscall6(
|
||||
unix.SYS_PTRACE,
|
||||
uintptr(unix.PT_GETDBREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&dr)),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
return &dr, nil
|
||||
}
|
||||
|
||||
// setDbRegs writes the debug register file of the stopped debuggee.
|
||||
func (s *Session) setDbRegs(dr *dbreg) error {
|
||||
if _, _, errno := unix.Syscall6(
|
||||
unix.SYS_PTRACE,
|
||||
uintptr(unix.PT_SETDBREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(dr)),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// archStopTrace classifies a TRAP_TRACE stop. On amd64 the kernel
|
||||
// delivers both the completed single-step and the debug-register hit
|
||||
// through T_TRCTRAP with TRAP_TRACE (sys/amd64/amd64/trap.c), and DR6's
|
||||
// B0-B3 bits name the watchpoint that fired. The bits are sticky ("the
|
||||
// processor never clears DR6", Intel SDM vol 3, "Debug Registers"), so
|
||||
// they are acknowledged here: cleared once read, or the next hit on a
|
||||
// different slot would still see this slot's bit set and report this
|
||||
// slot's address again.
|
||||
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
status := dr.Dr[drStatus] & 0xF
|
||||
if status == 0 {
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
// Best effort: the write-back only fails for a dead debuggee, for
|
||||
// which no further watchpoint can fire anyway.
|
||||
dr.Dr[drStatus] &^= 0xF
|
||||
_ = s.setDbRegs(dr)
|
||||
for slot := range 4 {
|
||||
if status&(1<<slot) != 0 && dr.Dr[slot] != 0 {
|
||||
return StopWatchpoint, dr.Dr[slot]
|
||||
}
|
||||
}
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
|
||||
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||||
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||||
func (s *Session) FindFreeWatchpointSlot() int {
|
||||
for i := range 4 {
|
||||
if !s.wpSlots[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// IsWatchpointSlotUsed reports whether slot (0-3) currently holds a watchpoint.
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
if slot < 0 || slot > 3 {
|
||||
return false
|
||||
}
|
||||
return s.wpSlots[slot]
|
||||
}
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||
// DR7's encoding is architectural: a 2-bit local/global enable pair per
|
||||
// slot at bit 2*slot, the R/W field at 16+4*slot and the length field at
|
||||
// 18+4*slot (Intel SDM vol 3, "Debug Registers").
|
||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||
if slot < 0 || slot > 3 {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||
}
|
||||
if s.wpSlots[slot] {
|
||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||
}
|
||||
|
||||
var lenBits uint64
|
||||
switch size {
|
||||
case 1:
|
||||
lenBits = 0
|
||||
case 2:
|
||||
lenBits = 1
|
||||
case 4:
|
||||
lenBits = 3
|
||||
case 8:
|
||||
lenBits = 2
|
||||
default:
|
||||
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read debug registers: %w", err)
|
||||
}
|
||||
dr.Dr[slot] = addr
|
||||
|
||||
dr7 := dr.Dr[drControl]
|
||||
enableBit := uint64(1) << (2 * slot)
|
||||
rwBits := uint64(typ) << (16 + 4*slot)
|
||||
lenField := lenBits << (18 + 4*slot)
|
||||
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||
dr.Dr[drControl] = (dr7 & mask) | enableBit | rwBits | lenField
|
||||
|
||||
if err := s.setDbRegs(dr); err != nil {
|
||||
return fmt.Errorf("debug: set debug registers: %w", err)
|
||||
}
|
||||
s.wpSlots[slot] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearWatchpoint removes a hardware watchpoint.
|
||||
func (s *Session) ClearWatchpoint(slot int) error {
|
||||
if slot < 0 || slot > 3 {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||
}
|
||||
if !s.wpSlots[slot] {
|
||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||
}
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dr.Dr[slot] = 0
|
||||
dr.Dr[drControl] &^= uint64(1) << (2 * slot)
|
||||
if err := s.setDbRegs(dr); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := range maxWatchpoints() {
|
||||
if s.wpSlots[slot] {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Hardware watchpoint support via arm64 debug registers, read and written
|
||||
// as one blob through PT_GETDBREGS/PT_SETDBREGS. The FreeBSD struct dbreg
|
||||
// (sys/arm64/include/reg.h) opens with the debug-facility header and then
|
||||
// carries 16 breakpoint and 16 watchpoint pairs of {address, control}.
|
||||
|
||||
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||||
type dbreg struct {
|
||||
DbDebugVer uint8
|
||||
DbNbkpts uint8
|
||||
DbNwtpts uint8
|
||||
_ [5]byte
|
||||
DbBreakregs [16]struct {
|
||||
Addr uint64
|
||||
Ctrl uint32
|
||||
_ uint32
|
||||
}
|
||||
DbWatchregs [16]struct {
|
||||
Addr uint64
|
||||
Ctrl uint32
|
||||
_ uint32
|
||||
}
|
||||
}
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
|
||||
const (
|
||||
WatchWrite WatchpointType = 1 // trigger on write
|
||||
WatchRead WatchpointType = 3 // trigger on read or write
|
||||
)
|
||||
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// architecture provides: DBGWVR0-DBGWCR15.
|
||||
func maxWatchpoints() int { return 16 }
|
||||
|
||||
// getDbRegs reads the debug register file of the stopped debuggee.
|
||||
func (s *Session) getDbRegs() (*dbreg, error) {
|
||||
var dr dbreg
|
||||
if _, _, errno := unix.Syscall6(
|
||||
unix.SYS_PTRACE,
|
||||
uintptr(unix.PT_GETDBREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&dr)),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
return &dr, nil
|
||||
}
|
||||
|
||||
// setDbRegs writes the debug register file of the stopped debuggee.
|
||||
func (s *Session) setDbRegs(dr *dbreg) error {
|
||||
if _, _, errno := unix.Syscall6(
|
||||
unix.SYS_PTRACE,
|
||||
uintptr(unix.PT_SETDBREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(dr)),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// archStopTrace classifies a TRAP_TRACE stop. On arm64 the kernel
|
||||
// delivers both the software single step and the watchpoint hit through
|
||||
// EXCP_SOFTSTP_EL0/EXCP_WATCHPT_EL0 with TRAP_TRACE (sys/arm64/arm64/
|
||||
// trap.c); the watchpoint address rides the FAR register, so a stop whose
|
||||
// reported address falls inside an armed watchpoint's byte range is a
|
||||
// watchpoint and everything else is a single step. The byte range comes
|
||||
// from DBGWCR's byte-address-select bits (ARM DDI 0487, DBGWCR<n>_EL1):
|
||||
// bit i watches the address plus i, so the range spans the lowest set bit
|
||||
// to the highest set bit inclusive.
|
||||
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
for slot := range 16 {
|
||||
ctrl := uint64(dr.DbWatchregs[slot].Ctrl)
|
||||
if ctrl&1 == 0 || dr.DbWatchregs[slot].Addr == 0 {
|
||||
continue
|
||||
}
|
||||
bas := uint8((ctrl >> 5) & 0xFF)
|
||||
if bas == 0 {
|
||||
continue
|
||||
}
|
||||
lo := bits.TrailingZeros8(bas)
|
||||
hi := 7 - bits.LeadingZeros8(bas)
|
||||
addr := dr.DbWatchregs[slot].Addr
|
||||
if siAddr >= addr+uint64(lo) && siAddr < addr+uint64(hi)+1 {
|
||||
return StopWatchpoint, siAddr
|
||||
}
|
||||
}
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
|
||||
// FindFreeWatchpointSlot returns the index of the first free watchpoint
|
||||
// slot, or -1 if all of them are in use.
|
||||
func (s *Session) FindFreeWatchpointSlot() int {
|
||||
for i := range maxWatchpoints() {
|
||||
if !s.wpSlots[i] {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// IsWatchpointSlotUsed reports whether slot currently holds a watchpoint.
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return false
|
||||
}
|
||||
return s.wpSlots[slot]
|
||||
}
|
||||
|
||||
// SetWatchpoint installs a hardware watchpoint on the given address. The
|
||||
// control word is the architectural DBGWCR (ARM DDI 0487): bit 0 enables,
|
||||
// bits 3-4 select the access type (10 store, 11 load+store) and bits 5-12
|
||||
// are the byte-address select, so the watch stays 8-byte aligned and names
|
||||
// its watched bytes through BAS.
|
||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||
}
|
||||
if s.wpSlots[slot] {
|
||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||
}
|
||||
var bas uint64
|
||||
switch size {
|
||||
case 1:
|
||||
bas = 0x01
|
||||
case 2:
|
||||
bas = 0x03
|
||||
case 4:
|
||||
bas = 0x0F
|
||||
case 8:
|
||||
bas = 0xFF
|
||||
default:
|
||||
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||
}
|
||||
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: read debug registers: %w", err)
|
||||
}
|
||||
if uint8(slot) >= dr.DbNwtpts && dr.DbNwtpts != 0 {
|
||||
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, dr.DbNwtpts)
|
||||
}
|
||||
ctrl := uint64(1) // enable
|
||||
switch typ {
|
||||
case WatchWrite:
|
||||
ctrl |= 2 << 3 // store only
|
||||
case WatchRead:
|
||||
ctrl |= 3 << 3 // load+store
|
||||
}
|
||||
ctrl |= bas << 5
|
||||
dr.DbWatchregs[slot].Addr = addr
|
||||
dr.DbWatchregs[slot].Ctrl = uint32(ctrl)
|
||||
|
||||
if err := s.setDbRegs(dr); err != nil {
|
||||
return fmt.Errorf("debug: set debug registers: %w", err)
|
||||
}
|
||||
s.wpSlots[slot] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearWatchpoint removes a hardware watchpoint.
|
||||
func (s *Session) ClearWatchpoint(slot int) error {
|
||||
if slot < 0 || slot >= maxWatchpoints() {
|
||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||
}
|
||||
if !s.wpSlots[slot] {
|
||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||
}
|
||||
dr, err := s.getDbRegs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dr.DbWatchregs[slot].Addr = 0
|
||||
dr.DbWatchregs[slot].Ctrl = 0
|
||||
if err := s.setDbRegs(dr); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||
func (s *Session) ClearAllWatchpoints() error {
|
||||
for slot := range maxWatchpoints() {
|
||||
if s.wpSlots[slot] {
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build freebsd && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The architecture has hardware watchpoint triggers, but FreeBSD exposes
|
||||
// no PT_GETDBREGS request for riscv64, so there is no supported way to arm
|
||||
// one: the watchpoint layer is honestly empty here.
|
||||
|
||||
// WatchpointType selects what triggers the watchpoint.
|
||||
type WatchpointType int
|
||||
|
||||
const (
|
||||
WatchWrite WatchpointType = 1 // trigger on write
|
||||
WatchRead WatchpointType = 3 // trigger on read or write
|
||||
)
|
||||
|
||||
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||
// platform provides: FreeBSD exposes none for riscv64.
|
||||
func maxWatchpoints() int { return 0 }
|
||||
|
||||
// archStopTrace classifies a TRAP_TRACE stop; with no watchpoint layer a
|
||||
// trace stop is always a completed single step.
|
||||
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
|
||||
// FindFreeWatchpointSlot returns -1: no slots exist.
|
||||
func (s *Session) FindFreeWatchpointSlot() int { return -1 }
|
||||
|
||||
// IsWatchpointSlotUsed reports whether slot currently holds a watchpoint.
|
||||
func (s *Session) IsWatchpointSlotUsed(slot int) bool { return false }
|
||||
|
||||
// SetWatchpoint is unsupported: FreeBSD exposes no debug register request
|
||||
// for riscv64.
|
||||
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||
return fmt.Errorf("debug: hardware watchpoints are not supported on freebsd/riscv64")
|
||||
}
|
||||
|
||||
// ClearWatchpoint is unsupported for the same reason.
|
||||
func (s *Session) ClearWatchpoint(slot int) error {
|
||||
return fmt.Errorf("debug: hardware watchpoints are not supported on freebsd/riscv64")
|
||||
}
|
||||
|
||||
// ClearAllWatchpoints is a no-op: no watchpoint can be armed.
|
||||
func (s *Session) ClearAllWatchpoints() error { return nil }
|
||||
@@ -30,13 +30,25 @@ const (
|
||||
// (arch/x86/kernel/ptrace.c send_sigtrap passes regs->ip), so the watched
|
||||
// data address is recovered from DR6's slot bits (B0-B3, positive polarity
|
||||
// through PEEKUSER) and the matching DR0-DR3.
|
||||
//
|
||||
// The B0-B3 bits are sticky ("the processor never clears DR6", Intel SDM
|
||||
// vol 3, "Debug Registers"), so they are acknowledged here: cleared once
|
||||
// read, or the next hit on a different slot would still see this slot's bit
|
||||
// set and report this slot's address again.
|
||||
func archWatchpointAddr(s *Session, siAddr uint64) uint64 {
|
||||
dr6, err := ptracePeekUser(s.pid, dr6Off)
|
||||
if err != nil {
|
||||
return siAddr
|
||||
}
|
||||
status := dr6 & 0xF
|
||||
if status == 0 {
|
||||
return siAddr
|
||||
}
|
||||
// Best effort: the write-back only fails for a debuggee that died, in
|
||||
// which case no further watchpoint can fire anyway.
|
||||
_ = ptracePokeUser(s.pid, dr6Off, dr6&^0xF)
|
||||
for slot := range 4 {
|
||||
if dr6&(1<<slot) != 0 {
|
||||
if status&(1<<slot) != 0 {
|
||||
addr, err := ptracePeekUser(s.pid, drOffset+uintptr(slot*8))
|
||||
if err == nil && addr != 0 {
|
||||
return addr
|
||||
@@ -139,6 +151,11 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
||||
if err := ptracePokeUser(s.pid, dr7Off, dr7); err != nil {
|
||||
return err
|
||||
}
|
||||
// Zero the address register too: a stale address in a disabled slot
|
||||
// turns any sticky DR6 bit into a misattributed watchpoint report.
|
||||
if err := ptracePokeUser(s.pid, drOffset+uintptr(slot*8), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.wpSlots[slot] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
"golang.org/x/arch/riscv64/riscv64asm"
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// Instruction is one decoded instruction: its text form, its length in bytes
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
func TestDecodeKnownBytes(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user