docs: state the validation status and correct claims the material contradicts

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-20 01:40:51 +02:00
parent 2931bbd6b2
commit f0d5238c47
22 changed files with 356 additions and 191 deletions
+44 -35
View File
@@ -16,8 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
JALR; loong64 accepts the raw `JIRL rd, rj, off` spelling the Go JALR; loong64 accepts the raw `JIRL rd, rj, off` spelling the Go
assembler cannot express. A frameless amd64 function containing a assembler cannot express. A frameless amd64 function containing a
CALL now receives the toolchain's forced base-pointer frame. The CALL now receives the toolchain's forced base-pointer frame. The
verify trampolines join the ground-truth lists, and a lint check for riscv64 and loong64 verify trampolines join their ground-truth lists,
control flow through registers and memory extends to the new forms. and a lint check for control flow through registers and memory extends
to the new forms.
- **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target - **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target
architecture can be named explicitly instead of inferred from the architecture can be named explicitly instead of inferred from the
file-name suffix, which is how the suffix-less majority of GOROOT's file-name suffix, which is how the suffix-less majority of GOROOT's
@@ -26,37 +27,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
file under a directory (default GOROOT/src) with the gasm encoder file under a directory (default GOROOT/src) with the gasm encoder
only: suffixed files for their architecture, suffix-less files for only: suffixed files for their architecture, suffix-less files for
all four, as a GOARCH build would. Reports the headline number (127 all four, as a GOARCH build would. Reports the headline number (127
of 627 GOROOT files, 20.3 %, assemble for every target architecture, of 627 GOROOT files, 20.3 %, assemble for every target architecture),
against 23 in the previous release), the per-architecture pass rates the per-architecture pass rates and the most common failure reasons
and the most common failure reasons with a representative file each, with a representative file each,
which drive the encodability backlog by frequency. which drive the encodability backlog by frequency.
- **Fuzz targets for the parser and the formatter.** FuzzParse (no - **The parser and the formatter are fuzzed.** Two targets carry the
panic, always a usable file) and FuzzFormatIdempotency (formatting guarantee: no input makes the parser panic, and every input yields a
twice equals formatting once; clean input stays clean) seed file the rest of the toolkit can work on; formatting twice equals
themselves from the repository's kernels, so the plain test suite formatting once, and clean input stays clean. They seed from the
replays every seed in CI and `just fuzz` runs the mutation engine on repository's own kernels, and `just fuzz` drives the mutation engine
demand. on demand.
- **Oracle parity as its own CI step.** The push pipeline already ran - **Man pages.** docs/man carries gasm(1) and a page for every command
the live go-tool-asm comparison inside the suite; a dedicated step except `version`, which gasm(1) documents itself, written in roff:
now names that gate when it fails. synopsis, description, every flag with its default, exit status,
- **Man pages.** docs/man carries gasm(1) and one page per command, worked examples and cross-references.
written in roff: synopsis, description, every flag with its default,
exit status, worked examples and cross-references.
`just install-man` compresses them into ~/.local/share/man (MANDIR `just install-man` compresses them into ~/.local/share/man (MANDIR
overrides) and `just uninstall-man` removes them. A test builds the overrides) and `just uninstall-man` removes them. A test builds the
binary and compares every command's `-h` output with its page, so the binary and compares each page's flags and synopsis with its own `-h`
pages cannot drift from the CLI. output, so the pages cannot drift from the CLI.
### Changed ### Changed
- **Go 1.27.1 required.** The module declares `go 1.27.1`, so building
from source needs that patch release or newer.
- **Canonical just recipes.** `just gates` is the definition of done - **Canonical just recipes.** `just gates` is the definition of done
(build, fmt-check, vet, test, race). `install` now builds and copies (build, fmt-check, vet, test, race). `install` now builds and copies
the binary into `~/.local/bin` (`BINDIR` overrides) instead of the binary into `~/.local/bin` (`BINDIR` overrides) instead of
downloading module dependencies, and `install-bin` is gone. The test downloading module dependencies, and `install-bin` is gone. The test
gate sweeps the logic packages (arch through verify; the hardware-bound gate sweeps the logic packages (arch through verify; the ptrace-bound
`debug` and the thin `cmd/gasm` sit outside it), so the coverage floor `debug` and the thin `cmd/gasm` sit outside the coverage profile), so
is computed over the product code and the number is identical locally the coverage floor is computed over the product code and the number is
and in CI. `fuzz` requires its target package. identical locally and in CI; the two excluded packages' own tests run
in the gate and in the pipelines, outside the floor. `fuzz` requires
its target package.
- **The reported version comes from the build.** `gasm --version` - **The reported version comes from the build.** `gasm --version`
prints the version the toolchain recorded: the tag on a tagged prints the version the toolchain recorded: the tag on a tagged
checkout, a pseudo-version naming the commit below one, `+dirty` on a checkout, a pseudo-version naming the commit below one, `+dirty` on a
@@ -78,8 +81,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
synopsis, the commands, every flag with its default, the exit codes and synopsis, the commands, every flag with its default, the exit codes and
worked examples; `CONTRIBUTING.md` carries the Contributor terms and worked examples; `CONTRIBUTING.md` carries the Contributor terms and
states the commit trailer form, the one-logical-change rule and the states the commit trailer form, the one-logical-change rule and the
licence header rule. The repository's own assembly (the `verify` licence header rule; `SECURITY.md` states how a vulnerability is
trampolines and the test kernels) is in `gasm fmt` canonical form. reported and what to expect. The repository's own assembly (the
`verify` trampolines and the test kernels) is in `gasm fmt` canonical
form.
- **The README states the project's purpose and status.** It opens with - **The README states the project's purpose and status.** It opens with
a warning that the tool is an experiment under active development, a warning that the tool is an experiment under active development,
version 0.x.x, free to change without warning, with 1.0.0 far off, version 0.x.x, free to change without warning, with 1.0.0 far off,
@@ -88,7 +93,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Go toolchain), argues the case for the syntax in a new Why Plan 9 Go toolchain), argues the case for the syntax in a new Why Plan 9
assembly section, and carries a Direction section: extended assembly section, and carries a Direction section: extended
instruction support, full GOOBJ and ELF compilation, Linux and instruction support, full GOOBJ and ELF compilation, Linux and
FreeBSD, and the four architectures. FreeBSD, and the four architectures. A Validation status section
states what has been executed where: amd64 on real hardware, the other
three architectures under qemu-user emulation, the encoding parity on
the host for all four, and the debugger's ptrace path on amd64 only.
### Fixed ### Fixed
@@ -125,13 +133,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(everything after it was dropped); it is an illegal token now, invalid (everything after it was dropped); it is an illegal token now, invalid
UTF-8 no longer inflates byte offsets, and CRLF files format to UTF-8 no longer inflates byte offsets, and CRLF files format to
uniform LF. uniform LF.
- **A frameless amd64 function containing a CALL read its arguments from - **The class-2 stack guard branched four bytes past its target.** When
the wrong stack slot.** The forced base-pointer frame shifted the underflow branch relaxed to its 32-bit form, its displacement was
FP references by eight bytes (`x+0(FP)` resolved to SP+0x18 where the still computed as if the branch were two bytes long, so it landed
toolchain emits SP+0x10), so such functions loaded garbage. The inside the morestack CALL instead of the compare that decides it.
class-2 stack guard had the sibling defect: whenever the underflow The long form is reachable once a large frame carries a body of roughly
branch relaxed to its 32-bit form, its displacement ran four bytes a hundred bytes.
past the target and into the morestack CALL.
- **Immediate operands wrapped silently on amd64.** Shift counts, - **Immediate operands wrapped silently on amd64.** Shift counts,
immediates beyond the operand's width and displacements beyond int32 immediates beyond the operand's width and displacements beyond int32
truncated without a diagnostic (`SHLQ $300` assembled as `$44`); they truncated without a diagnostic (`SHLQ $300` assembled as `$44`); they
@@ -290,8 +297,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the other architectures, and the abi kernels use it; every verify the other architectures, and the abi kernels use it; every verify
kernel is now ground-truth checkable (the numeric `X27` spelling the kernel is now ground-truth checkable (the numeric `X27` spelling the
kernels used is one `go tool asm` rejects). kernels used is one `go tool asm` rejects).
- The GOROOT corpus number rose to 127 of 627 files (20.3 %) assembling - **Two more spellings GOROOT uses now assemble.** riscv64 `FCLASSD`
for every target architecture, from 108. (classify a float64 into an integer mask) is encodable, and a
displacement written as a product (`0*8(X5)`, the toolchain's own
spelling in several kernels) parses instead of being rejected.
- **loong64 JIT execution enabled.** The loong64 trampoline is now - **loong64 JIT execution enabled.** The loong64 trampoline is now
validated end to end under qemu-user emulation (plain and ABI-checked validated end to end under qemu-user emulation (plain and ABI-checked
calls, goroutine-clobber detection), so `gasm verify` runs the JIT calls, goroutine-clobber detection), so `gasm verify` runs the JIT
+15 -9
View File
@@ -24,7 +24,9 @@ below; submitting one means you accept them.
## Development setup ## Development setup
Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod` Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod`
declares, and [just](https://github.com/casey/just) for the recipes. declares, [just](https://github.com/casey/just) for the recipes, and a C
compiler (gcc), because `just gates` includes `just race` and the race
detector needs cgo.
```sh ```sh
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
@@ -54,16 +56,20 @@ workflow builds the assets and publishes the release and its notes.
## Code style ## Code style
`gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero `gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero
warnings tolerated. `just gates` is the definition of done in one command, and the recipe warnings tolerated. `just vet` is two gates, `go vet ./...` and `go fix -diff ./...`,
file names what it contains. Errors are checked explicitly, wrapped as so the modernisation rewrites are enforced too. `just gates` is the definition of done in
`fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The recipe file holds one command, and the recipe file names what it contains. Errors are checked explicitly,
the commands, and the language and standard-library surface is the one the `go` directive wrapped as `fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The
in `go.mod` pins. recipe file holds the commands, and the language and standard-library surface is the one
the `go` directive in `go.mod` pins.
- `golang.org/x/arch` is the one module dependency, and it is linked into the binary: - `golang.org/x/arch` is the one module dependency, and it is linked into the binary:
`gasm dis` and the debugger's listings decode through it. Everything else is the `gasm dis` and the debugger's listings decode through it. Everything else is the
standard library. standard library.
- No cgo, no C, no external toolchain at runtime. - No cgo and no C. The standalone encoder paths (`gasm asm --format raw` and `--format
elf`) need no Go installation; `gasm verify --ground-truth`, `gasm verify --fuzz`,
`gasm audit-instructions` and `gasm asm --format goobj` resolve through the installed
Go toolchain.
- The parser, lexer and formatter are hand-written; the `arch` instruction tables are - The parser, lexer and formatter are hand-written; the `arch` instruction tables are
generated only by `_gen/gen.go` (`just gen`) and never edited by hand. generated only by `_gen/gen.go` (`just gen`) and never edited by hand.
- Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a - Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a
@@ -111,8 +117,8 @@ Workflows live in `.gitea/workflows/` and run on the project's own runners:
| Workflow | Trigger | What it does | | Workflow | Trigger | What it does |
|---|---|---| |---|---|---|
| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor | | Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor, the CLI and debugger tests outside the profile, then the oracle-parity rerun against `go tool asm` |
| Release | a `v*` tag | the same gates as Test, then the matrix build, the proven version and the release itself; the race detector runs locally in `just gates` before the tag is cut | | Release | a `v*` tag | the same gates as Test minus the oracle-parity step, then the matrix build, the version smoke test and the release itself; the race detector runs locally in `just gates` before the tag is cut |
The local equivalent is `just gates`, which is the same set plus the race detector. The The local equivalent is `just gates`, which is the same set plus the race detector. The
race detector also has its own workflow, dispatched by hand; it never runs on a push or a race detector also has its own workflow, dispatched by hand; it never runs on a push or a
+53 -18
View File
@@ -5,23 +5,25 @@
> output formats and behaviour can change without warning at any time. > 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 > A 1.0.0 release is light years away. Nothing in this document is a
> stability promise. For all of that, this is not a paper project: gasm > stability promise. For all of that, this is not a paper project: gasm
> is already in active use and is tested on real assembly work. > is already in active use and is tested on real assembly work. Only
> amd64 is validated on real hardware; the other three architectures run
> under emulation ([Validation status](#validation-status)).
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling: **GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
there is no formatter, no linter, no static analyser, no standalone there is no formatter, no linter and no debugger for `.s` files, and no
assembler and no debugger for `.s` files. Developers write assembly assembler that works without a Go installation. Developers write
blind, validate it by benchmark, and debug it by print statement. assembly blind, validate it by benchmark, and debug it by print
gasm-devkit is the missing toolkit: a single, self-contained binary, statement. gasm-devkit is the missing toolkit: a single, self-contained
`gasm`, that serves both purposes. binary, `gasm`, that serves both purposes.
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly, - **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
dynamic verification, a source-level debugger and a language server, dynamic verification, a source-level debugger and a language server,
for `.s` files in Go programs. for `.s` files in Go programs.
- **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes - **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes
on its own, with no Go installation in the loop, and writes raw on its own and writes raw images or linkable ELF objects with DWARF5
images, linkable ELF objects with DWARF5 debug sections, or the Go debug sections, with no Go installation in the loop; the Go
toolchain's own GOOBJ format, which `go build` consumes in place of toolchain's own GOOBJ format, which `go build` consumes in place of
the toolchain's output. the toolchain's output, needs the installed toolchain.
## Why Plan 9 assembly ## Why Plan 9 assembly
@@ -69,12 +71,14 @@ to give that syntax the tooling it deserves.
operating recursively on directories the way `go fmt` does. `-l` lists operating recursively on directories the way `go fmt` does. `-l` lists
files whose formatting differs and `-d` prints a unified diff. files whose formatting differs and `-d` prints a unified diff.
- **Linter.** `gasm lint` runs 18 conservative static checks, among them - **Linter.** `gasm lint` runs 18 conservative static checks, among them
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature), `undefined-label`, `abi-argsize` (declared argument area vs the `// func`
`register-clobber` (Go ABI register liveness over the control-flow graph), signature), `register-clobber` (Go ABI register liveness over the
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`. control-flow graph), `stack-imbalance`, `abi0-register-args` and
`unencodable-instruction`.
- **Standalone assembler.** `gasm asm` encodes all four architectures without - **Standalone assembler.** `gasm asm` encodes all four architectures without
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5 the Go toolchain and writes raw images or linkable ELF objects (with DWARF5
debug sections) or the Go toolchain's own GOOBJ format, which `go build` debug sections) with no Go installation needed, or the Go toolchain's own
GOOBJ format, which needs the installed toolchain and which `go build`
consumes in place of the toolchain's output. Framed functions get the consumes in place of the toolchain's output. Framed functions get the
stack-split guard and the morestack block, byte-identical to the stack-split guard and the morestack block, byte-identical to the
toolchain's, so split functions link too. toolchain's, so split functions link too.
@@ -86,7 +90,8 @@ to give that syntax the tooling it deserves.
byte-for-byte ground-truth comparison of the machine code. byte-for-byte ground-truth comparison of the machine code.
- **Debugger.** `gasm debug` is a source-level ptrace debugger with - **Debugger.** `gasm debug` is a source-level ptrace debugger with
breakpoints (optionally conditional), hardware watchpoints, register and breakpoints (optionally conditional), hardware watchpoints, register and
memory inspection, and headless script runs with label-level coverage. memory inspection, and headless script runs that report instruction and
label coverage.
- **Language server.** `gasm lsp` serves completion, hover, document symbols, - **Language server.** `gasm lsp` serves completion, hover, document symbols,
push and pull diagnostics, semantic-token highlighting, go-to-definition, push and pull diagnostics, semantic-token highlighting, go-to-definition,
find references, rename, formatting, inlay hints, code actions, signature find references, rename, formatting, inlay hints, code actions, signature
@@ -123,6 +128,34 @@ The same measurement runs over GOROOT's whole assembly corpus:
assembling for every target architecture today, with the top failure assembling for every target architecture today, with the top failure
reasons per architecture; the number moves with every release. reasons per architecture; the number moves with every release.
### Validation status
**Only amd64 is validated on real hardware.** The other three
architectures are validated under qemu-user emulation, because the
project owns no arm64, riscv64 or loong64 machine, and emulation is the
only substitute available for the hardware. The distinction matters and
is stated rather than implied: everything below is a claim about what has
actually been executed.
| Layer | amd64 | arm64, riscv64, loong64 |
|---|---|---|
| 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 |
Consequences, stated plainly. An emulator is a model of a CPU, not the
CPU: instruction semantics are implemented in software and can differ
from silicon in ways a test suite does not reveal. A kernel that passes
under qemu-user is therefore not proven correct on real hardware, and a
discrepancy found on real hardware is a defect in gasm, reported like any
other. Encoding parity is the exception: the byte comparison against the
toolchain runs on the host for every architecture, so no emulator stands
between the claim and the evidence. The debugger is the weakest case: on
the three emulated architectures its per-architecture ptrace code has
been compiled and read, never executed. Its architecture-neutral units
run under `go test ./...`, which the race workflow and a manual run
perform; the default `just test` gate does not sweep `./debug/...`.
## Direction ## Direction
The plan, in the order it is being worked: The plan, in the order it is being worked:
@@ -134,7 +167,8 @@ The plan, in the order it is being worked:
an extended instruction set the toolchain does not know at all. The an extended instruction set the toolchain does not know at all. The
toolchain-derived tables stay generated and untouched; only the toolchain-derived tables stay generated and untouched; only the
extended instructions are hand-maintained, with their own spellings extended instructions are hand-maintained, with their own spellings
and encoders, verified by execution on real hardware because the and encoders, verified by execution (on real hardware for amd64, under
emulation for the rest, per the validation status above) because the
toolchain offers no ground truth to compare against. The gaps exist toolchain offers no ground truth to compare against. The gaps exist
on every architecture, amd64 included. on every architecture, amd64 included.
- **Full GOOBJ and ELF compilation.** The destination is a complete, - **Full GOOBJ and ELF compilation.** The destination is a complete,
@@ -205,7 +239,7 @@ gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
gasm debug --func name k.s # interactive debugger gasm debug --func name k.s # interactive debugger
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
gasm debug --func name --cover k.s # which labels did execution reach? gasm debug --func name --cover k.s # instruction and label coverage
gasm diff a.s b.s # compare machine code byte-for-byte gasm diff a.s b.s # compare machine code byte-for-byte
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
gasm profile k.s # show basic-block structure gasm profile k.s # show basic-block structure
@@ -243,7 +277,8 @@ recipe.
- [docs/CLI.md](docs/CLI.md): full command reference - [docs/CLI.md](docs/CLI.md): full command reference
- man pages: `just install-man` installs gasm(1) and one page per command - man pages: `just install-man` installs gasm(1) and one page per command
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes except `version`, which is documented inside gasm(1) instead, into
~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
them them
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes - [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
+2 -1
View File
@@ -29,7 +29,8 @@ Include:
- You are kept informed while the fix is being made, and told when it ships. - You are kept informed while the fix is being made, and told when it ships.
- The fix is released before the details are published, and the timing is agreed with - The fix is released before the details are published, and the timing is agreed with
you. you.
- The reporter is credited in the release notes unless they ask otherwise. - The fix ships without naming you: the project keeps no credits list, so the release
notes, the changelog and the commits name no reporter.
## Out of scope ## Out of scope
+8 -5
View File
@@ -40,10 +40,13 @@ func cmdAuditInstructions(args []string) error {
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", ` fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", `
Compare the gasm encoder for the given architecture (default amd64) against Compare the gasm encoder for the given architecture (default amd64) against
go tool asm and print the diff: superset encodings (gasm-only, shippable via go tool asm and print the diff: superset encodings (gasm-only, shippable via
gasm asm --format goobj), known-but-unencodable names (the backlog) and go- gasm asm --format goobj) and known-but-unencodable names (the backlog). The
only names (feature gaps). The Go side is probed black-box with a battery Go side is probed black-box one bare mnemonic at a time, so the audit tracks
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` whatever toolchain `+"`go env GOROOT`"+` provides; the gasm side answers from
provides. the encoder table on amd64 and from trial assembly over a battery of operand
shapes elsewhere. Names go tool asm knows and gasm does not cannot be
enumerated by probing, because Go's table is visible only through names
already in the gasm table; the report closes with a note saying so.
With --corpus the audit changes shape: it assembles every .s file under the With --corpus the audit changes shape: it assembles every .s file under the
given directory (default GOROOT/src) with the gasm encoder only, no given directory (default GOROOT/src) with the gasm encoder only, no
@@ -109,7 +112,7 @@ the encodability backlog by frequency rather than by table order.
w := os.Stdout w := os.Stdout
fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names)) fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names))
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %d\n", len(shared)+len(superset), countTrue(goKnown)) fmt.Fprintf(w, "gasm encodable: %d go tool asm recognised: %d\n", len(shared)+len(superset), countTrue(goKnown))
fmt.Fprintf(w, "shared: %d\n", len(shared)) fmt.Fprintf(w, "shared: %d\n", len(shared))
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n") fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
for _, n := range superset { for _, n := range superset {
+3 -2
View File
@@ -24,9 +24,10 @@ function in a traced subprocess (ptrace), then provides a REPL for
single-stepping, breakpoints, register and memory inspection. single-stepping, breakpoints, register and memory inspection.
REPL commands: REPL commands:
break <label|addr> [if <reg> <op> <val>] break <label|addr|line> [if <reg> <op> <val|reg|*addr>]
set a breakpoint, optionally conditional on a set a breakpoint, optionally conditional on a
register comparison (reg-reg or reg-immediate) comparison of one register against a constant,
another register, or the 8-byte word at *addr
delete <label|addr> remove a breakpoint delete <label|addr> remove a breakpoint
info break list all breakpoints info break list all breakpoints
step [n], s single-step n instructions (default 1) step [n], s single-step n instructions (default 1)
+11 -7
View File
@@ -486,10 +486,13 @@ func cmdAsm(args []string) int {
With -o the output is written to a file instead. The --format flag selects With -o the output is written to a file instead. The --format flag selects
what is written: raw (the default) concatenates the functions and the data what is written: raw (the default) concatenates the functions and the data
section into one self-consistent image; elf emits a relocatable object section into one self-consistent image; elf emits a relocatable object
(.text/.data sections, a symbol table and one PC32 relocation per (.text/.data sections, a symbol table and one relocation per static-symbol
static-symbol reference) that links with the system toolchain; goobj emits reference, in the architecture's own form: R_X86_64_PC32 on amd64,
the Go toolchain's own object format, which cmd/link consumes directly (it R_AARCH64_*, R_RISCV_* or R_LARCH_* on the others) that links with the
requires -p, the package path, and the installed Go toolchain). system toolchain; goobj emits the Go toolchain's own object format, which
cmd/link consumes directly (it requires -p, the package path, and the
installed Go toolchain: the object preamble is captured from go tool asm
and the format version from go version).
`) `)
out := fs.String("o", "", "write the output to this file") out := fs.String("o", "", "write the output to this file")
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
@@ -793,8 +796,8 @@ func cmdProfile(args []string) int {
flagSet := newCommand("profile", "gasm profile <file.s>", ` flagSet := newCommand("profile", "gasm profile <file.s>", `
Show the basic-block structure of functions in an assembly file. Show the basic-block structure of functions in an assembly file.
Lists each function's labels, their offsets, and the block boundaries. Lists each function's labels, their offsets, and the block boundaries.
This is the static structure; for runtime execution counts, use This is the static structure; for runtime execution counts use
gasm verify --fuzz which exercises the code paths. gasm debug --cover, and for input coverage gasm verify --fuzz.
`) `)
flagSet.Parse(args) flagSet.Parse(args)
if flagSet.NArg() != 1 { if flagSet.NArg() != 1 {
@@ -1033,7 +1036,8 @@ With -profile, the static basic-block structure is listed for each function.
With -call, a single function is invoked with user-supplied buffers (-buf) With -call, a single function is invoked with user-supplied buffers (-buf)
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g. instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
decoders) that crash on random input but should succeed on valid data. decoders) that crash on random input but should succeed on valid data. The
function named must be NOSPLIT: a function with a stack frame is refused.
With -save-corpus (and -fuzz), every input that crashes or mismatches is With -save-corpus (and -fuzz), every input that crashes or mismatches is
written to the directory as replayable JSON. -replay re-runs saved written to the directory as replayable JSON. -replay re-runs saved
+4 -4
View File
@@ -33,7 +33,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
entryAddr := codeBase + uint64(funcOffset) entryAddr := codeBase + uint64(funcOffset)
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize) fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit") fmt.Println("commands: break <label|addr|line> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
scanner := bufio.NewScanner(in) scanner := bufio.NewScanner(in)
@@ -233,7 +233,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
case "break", "b": case "break", "b":
if len(parts) < 2 { if len(parts) < 2 {
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]") fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val|reg|*addr>]")
continue continue
} }
var addr uint64 var addr uint64
@@ -278,7 +278,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
} }
} }
} else if len(parts) >= 4 && parts[2] == "if" { } else if len(parts) >= 4 && parts[2] == "if" {
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg|*addr>") fmt.Println("usage: break <label|addr|line> if <reg> <op> <value|reg|*addr>")
continue continue
} }
bp, err := bm.SetWithCond(addr, label, cond) bp, err := bm.SetWithCond(addr, label, cond)
@@ -422,7 +422,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println() fmt.Println()
case "help", "h", "?": case "help", "h", "?":
fmt.Printf(` break <label|addr> [if <reg> <op> <val|reg|*addr>] fmt.Printf(` break <label|addr|line> [if <reg> <op> <val|reg|*addr>]
set a breakpoint, optionally conditional on a set a breakpoint, optionally conditional on a
register compared to a constant, a register, or the register compared to a constant, a register, or the
8-byte word at *addr 8-byte word at *addr
+67 -39
View File
@@ -13,11 +13,13 @@ Three design goals shape everything below.
So the centre of the toolkit is a hand-written lexer and a parser that So the centre of the toolkit is a hand-written lexer and a parser that
produce a typed AST with source positions on every node. produce a typed AST with source positions on every node.
2. **Architecture as data, not code.** Per-architecture differences (amd64, 2. **Architecture as data, not code.** Per-architecture differences (amd64,
arm64, riscv64, loong64) live in register and instruction *tables* (`arch`), arm64, riscv64, loong64) live in register and instruction *tables* (`arch`)
never in `if arch == …` branches scattered through the logic. The and per-architecture encoders, rather than in `if arch == …` branches
instruction tables are generated from the Go toolchain's own assembler threaded through the analysis; the arch tests that remain are dispatch and
source (`just gen`), so adding or refreshing an architecture is a data policy points, such as which encoder a file name selects and which
operation, not a coding one. registers the liveness pass audits. The instruction tables are generated
from the Go toolchain's own assembler source (`just gen`), so refreshing an
architecture is a data operation, not a coding one.
3. **Open integration surface.** Everything the toolkit can do is reachable 3. **Open integration surface.** Everything the toolkit can do is reachable
through two vendor-neutral interfaces: a CLI and an LSP server. No editor through two vendor-neutral interfaces: a CLI and an LSP server. No editor
owns the toolkit; the toolkit is offered to editors on standard terms. owns the toolkit; the toolkit is offered to editors on standard terms.
@@ -35,10 +37,19 @@ flowchart TD
ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT
ARCH --> LSP ARCH --> LSP
LINT --> LSP LINT --> LSP
PAR --> ASM["asm<br/>encoders, image, object emitters"]
ASM --> VER["verify<br/>JIT mapping, ABI checks, fuzzing"]
ASM --> DBG["debug<br/>ptrace session"]
VER --> DBG
DIS["disasm<br/>golang.org/x/arch"] --> DBG
FMT --> CLI["gasm CLI"] FMT --> CLI["gasm CLI"]
LINT --> CLI LINT --> CLI
PAR --> CLI PAR --> CLI
LEX --> CLI LEX --> CLI
ASM --> CLI
VER --> CLI
DBG --> CLI
DIS --> CLI
LSP --> EDITOR["any LSP editor"] LSP --> EDITOR["any LSP editor"]
``` ```
@@ -70,9 +81,11 @@ assembler provides.
The boundaries matter as much as the responsibilities: `ast` records syntax The boundaries matter as much as the responsibilities: `ast` records syntax
only, and whether a name is a register or a label is left to `arch`, so the only, and whether a name is a register or a label is left to `arch`, so the
parser stays architecture-agnostic. `asm` and `verify` are the only packages parser stays architecture-agnostic. `asm` produces the machine code, `verify`
that touch machine code and executable memory, and `cmd/gasm` owns no logic and `debug` are the two packages that map it executable (read-execute in
beyond flags and output. `verify`, read-write-execute in the debuggee), and `cmd/gasm` is the CLI, with
the verify sweep orchestration and the audit, scaffold and unified-diff
helpers beside its flags and output.
### `token` and `lexer` ### `token` and `lexer`
@@ -112,14 +125,17 @@ Register files are generated programmatically (the regular `R8`-`R15`,
`X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly `X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly
named registers listed explicitly. Instruction names are **generated from the named registers listed explicitly. Instruction names are **generated from the
Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`, Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`,
plus the common opcodes and the per-architecture front-end aliases such as the plus the common opcodes in `cmd/internal/obj/util.go`) by `just gen`, so the
arm64 `B`/`BL` branches and the `.P`/`.W` load-store addressing suffixes) by tables always match what the real assembler accepts. The spellings the
`just gen`, so the tables always match what the real assembler accepts. Each toolchain's tables do not carry are hand-maintained instead: the front-end
mnemonic maps to a summary and an optional operand-count range; counts are alias lists in `arch/arm64.go`, `arch/amd64.go` and `arch/loong64.go` (the
recorded only where unambiguous (`-1` disables the operand-count lint for that arm64 `B`/`BL` branches among them), and the arm64 `.P`/`.W` load-store suffix
instruction) so the linter stays silent rather than guess. For architectures stripping in `arch/arch.go`. Each mnemonic maps to a summary and an optional
with highly variable operand forms (arm64, riscv64, loong64) only a few operand-count range; counts are recorded only where unambiguous (`-1`
fixed-arity instructions (`RET`, `NOP`, `JMP`, `CALL`) carry counts at all. disables the operand-count lint for that instruction) so the linter stays
silent rather than guess. For architectures with highly variable operand
forms (arm64, riscv64, loong64) `relaxCounts` clears those counts, leaving
`RET` and `NOP` with a range (`RET` alone on riscv64).
### `lint` ### `lint`
@@ -291,11 +307,11 @@ registers are translated onto the hardware stack pointer: `x+N(FP)` becomes
pointer is set up, with the matching Go prologue/epilogue generated, so the pointer is set up, with the matching Go prologue/epilogue generated, so the
output is byte-identical to the Go assembler for these cases. SIMD is handled output is byte-identical to the Go assembler for these cases. SIMD is handled
by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM
registers) across eight operand forms: the three-operand NDS form, the registers) over nine operand forms plus a dedicated move encoder: the
two-operand reg/rm form, the immediate-shift form (plus the variable-count three-operand NDS form, the two-operand reg/rm form, the immediate-shift form
shifts, which share the NDS shape with the count in an XMM register or (plus the variable-count shifts, which share the NDS shape with the count in
memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the an XMM register or memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`),
three-operand-plus-immediate form (`VSHUFPD`, the three-operand-plus-immediate form (`VSHUFPD`,
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`, `VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or `VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
@@ -340,10 +356,10 @@ b bit and the L'L rounding-control field (broadcast keeps the vector length
and scales disp8 by the element size), and combine with the .Z zeroing and scales disp8 by the element size), and combine with the .Z zeroing
suffix. Every encoding is validated two ways: by suffix. Every encoding is validated two ways: by
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
the machine code the real Go assembler emits, a comparison that holds for the machine code the real Go assembler emits; the parity suites carry that
whole functions: all 27 functions of both kernels assemble to exactly the Go comparison over whole kernel files on all four architectures, with the
toolchain's bytes, the lone exception being the displacements of the relocation fields masked because the Go linker fills those displacements at
static-constant loads, which the Go linker fills at link time. link time.
File-level assembly (`AssembleFile`) goes beyond single functions: it File-level assembly (`AssembleFile`) goes beyond single functions: it
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
@@ -426,10 +442,13 @@ The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
arguments to confirm the trampoline round-trips. The `-smoke` and `-abi` arguments to confirm the trampoline round-trips. The `-smoke` and `-abi`
sweeps run in parallel and each inside a child process, so a function that sweeps run in parallel and each inside a child process, so a function that
faults is reported without ending the sweep. `gasm verify --fuzz` combines faults is reported without ending the sweep; `-abi` is where the ABI check
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz lives, fuzzing each function with sentinel values in the registers the Go ABI
testing, comparing the JIT-assembled kernel against the portable Go reference fixes across calls and a canary below `SP`, and reporting a violation on any
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz iteration. `gasm verify --fuzz` is the differential campaign instead: it
JIT-loads the kernel and the `go tool asm` build of the same kernel and
compares the output argument areas bit-for-bit, one child process per function
so a crash on a partial function is reported rather than fatal. When a fuzz
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
for reproducibility. `gasm verify --call <func> --buf name:size:pattern` for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
invokes a single function with user-supplied buffers (patterns: zero, ones, invokes a single function with user-supplied buffers (patterns: zero, ones,
@@ -449,16 +468,25 @@ masked), reporting any encoding drift.
The interactive debugger (all four architectures). It launches the target The interactive debugger (all four architectures). It launches the target
function in a child process that maps the JIT code, calls function in a child process that maps the JIT code, calls
`PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls `PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls
execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem` execution. Breakpoints are patched through `/proc/pid/mem`: the one-byte
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime). `INT3` on amd64, the four-byte break instruction on the other three (arm64
`BRK #0`, riscv64 `ebreak`, loong64 `break 0`).
The child pins its goroutine to the OS thread with `runtime.LockOSThread` The child pins its goroutine to the OS thread with `runtime.LockOSThread`
so the traced thread is the one executing JIT code. The REPL provides so the traced thread is the one executing JIT code. The REPL provides
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`), single-step, register inspection (the GPRs on every architecture; on amd64 the
XMM set through `PTRACE_GETFPREGS` and the YMM set through `PTRACE_GETREGSET`
on `NT_X86_XSTATE`; on the other three the FP/SIMD regset through
`PTRACE_GETREGSET` on `NT_PRFPREG`),
label resolution, named buffer allocation with pattern filling label resolution, named buffer allocation with pattern filling
(`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint (`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint
management. Breakpoints accept conditions management. Breakpoints accept conditions
(`break <label> if <reg> <op> <val>`, including register-against-register (`break <label> if <reg> <op> <val>`, including register-against-register
comparisons), and hardware watchpoints work on all four architectures. comparisons), and hardware watchpoints work on amd64 (the DR0-DR3 debug
registers), arm64 (`NT_ARM_HW_WATCH`) and loong64 (`NT_LOONGARCH_HW_WATCH`);
riscv64 reports that its kernel ptrace interface exposes no trigger regset.
The ptrace path is validated at run time on amd64, where the session tests are
built; arm64, riscv64 and loong64 compile and are covered by the
architecture-neutral units (label and line tables, the breakpoint manager).
For non-interactive use, `--script` runs REPL commands from a file (or For non-interactive use, `--script` runs REPL commands from a file (or
stdin) and exits, `--timeout` kills the debuggee when a run hangs (the stdin) and exits, `--timeout` kills the debuggee when a run hangs (the
watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot
@@ -499,9 +527,10 @@ sequenceDiagram
Errors are produced where the parse or the encoding fails and become values at Errors are produced where the parse or the encoding fails and become values at
the CLI boundary: the parser returns a diagnostic list and never aborts a file, the CLI boundary: the parser returns a diagnostic list and never aborts a file,
`AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr `AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr
and returns a non-zero exit code. The formatter and the linter take the same and returns a non-zero exit code. The formatter and the linter take different
AST by a different route: `gasm fmt` re-spaces the token stream and `gasm lint` inputs from the assembler: `gasm fmt` re-spaces the token stream
walks the parsed file, so neither depends on an encoding. (`format.Source` lexes the source text itself) and `gasm lint` walks the parsed
AST, so neither depends on an encoding.
## State and lifetime ## State and lifetime
@@ -522,9 +551,8 @@ walks the parsed file, so neither depends on an encoding.
## Dependencies ## Dependencies
- **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the - **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the
disassembler backend (`gasm dis` and the debugger's listings) and the source disassembler backend (`gasm dis` and the debugger's listings). The tests
of the register metadata the encoder consults (`asm/reg.go`, `asm/vex.go`). additionally decode through it to validate the encodings.
The tests additionally decode through it to validate the encodings.
- **The Go toolchain**, as an oracle and never as a library: `go tool asm` - **The Go toolchain**, as an oracle and never as a library: `go tool asm`
supplies the object preamble and the ground truth for `gasm verify supplies the object preamble and the ground truth for `gasm verify
--ground-truth`, `go list -json -export` locates the archives of the packages --ground-truth`, `go list -json -export` locates the archives of the packages
+41 -26
View File
@@ -3,9 +3,11 @@
The reference below is taken from the program's own `--help`. If the two disagree, the The reference below is taken from the program's own `--help`. If the two disagree, the
program is right and this file is a defect. program is right and this file is a defect.
The same reference is installed as man pages: `just install-man` puts gasm(1) and one The same reference is installed as man pages: `just install-man` puts gasm(1) and a page
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each for every command except `version` (which gasm(1) itself documents) into
page against the binary so the two cannot drift apart. ~/.local/share/man (`MANDIR` overrides). A test in `cmd/gasm` keeps the two from
drifting: it compares each page's flag set and SYNOPSIS line with the binary's own `-h`
output, and gasm(1)'s COMMANDS list with the top-level help. The prose is not compared.
## Synopsis ## Synopsis
@@ -58,7 +60,8 @@ Usage: gasm parse <file>
``` ```
Parse FILE and report syntax errors on stderr. On success, print how many Parse FILE and report syntax errors on stderr. On success, print how many
declarations and TEXT functions the file contains. declarations and TEXT functions the file contains. FILE may be `-` to read
standard input.
```sh ```sh
gasm parse hello_amd64.s gasm parse hello_amd64.s
@@ -155,7 +158,9 @@ recognisable suffix (most of GOROOT's, for example `cpu_x86.s`) are
assembled. `raw` concatenates the functions and the data section into one assembled. `raw` concatenates the functions and the data section into one
self-consistent image; `elf` emits a relocatable object that links with the self-consistent image; `elf` emits a relocatable object that links with the
system toolchain; `goobj` emits the Go toolchain's own object format, which system toolchain; `goobj` emits the Go toolchain's own object format, which
`cmd/link` consumes directly. `cmd/link` consumes directly, and is the one format that needs the toolchain
installed: the object preamble is captured from `go tool asm` and the format
version from `go version`. `raw` and `elf` need no toolchain at all.
```sh ```sh
gasm asm hello_amd64.s gasm asm hello_amd64.s
@@ -208,7 +213,7 @@ Usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <f
| `--abi-n` | 100 | ABI check iterations with varied inputs | | `--abi-n` | 100 | ABI check iterations with varied inputs |
| `--profile` | off | list the basic-block structure per function | | `--profile` | off | list the basic-block structure per function |
| `--smoke` | off | call each NOSPLIT function with zeroed arguments | | `--smoke` | off | call each NOSPLIT function with zeroed arguments |
| `--call` | empty | invoke a single function with `--buf` instead of the sweeps | | `--call` | empty | invoke a single NOSPLIT function with `--buf` instead of the sweeps |
| `--buf` | empty | buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` | | `--buf` | empty | buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
| `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) | | `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
| `--repeat` | 1 | number of times to repeat a `--call` invocation | | `--repeat` | 1 | number of times to repeat a `--call` invocation |
@@ -219,7 +224,8 @@ The JIT checks run when the host matches the file's architecture; the
toolchain comparison works everywhere. `--fuzz`, `--smoke` and `--abi` run each toolchain comparison works everywhere. `--fuzz`, `--smoke` and `--abi` run each
function in its own child process, so a partial function that faults on random function in its own child process, so a partial function that faults on random
input is reported as `CRASH` instead of ending the sweep; `--call` with `--buf` input is reported as `CRASH` instead of ending the sweep; `--call` with `--buf`
invokes such a function with valid data. invokes such a function with valid data. The function named by `--call` must be
NOSPLIT: a function with a stack frame is refused with a diagnostic and exits 1.
```sh ```sh
gasm verify --ground-truth hello_amd64.s gasm verify --ground-truth hello_amd64.s
@@ -262,17 +268,18 @@ Usage: gasm debug <file.s> --func <name>
| `-timeout` | 0 | kill the debuggee after this duration, for headless `-script` runs; a timeout exits 3 | | `-timeout` | 0 | kill the debuggee after this duration, for headless `-script` runs; a timeout exits 3 |
| `-cover` | off | run to completion with a breakpoint on every instruction and report which executed | | `-cover` | off | run to completion with a breakpoint on every instruction and report which executed |
The debugger spawns the debuggee from the `gasm` binary on `$PATH`, so install The debugger re-executes the binary it is running as (`os.Executable()`) for the
it first with `just install`; `go run` does not work for the traced child. traced child, so the child is the same `gasm`, whether it is installed on `$PATH`
Requires Linux (ptrace) and all four architectures are supported. or run with `go run ./cmd/gasm`; nothing has to be installed first. Requires
Linux (ptrace), and all four architectures are supported.
REPL commands: REPL commands:
| Command | Effect | | Command | Effect |
|---|---| |---|---|
| `break <label\|addr> [if <reg> <op> <val>]` | set a breakpoint, optionally conditional | | `break <label\|addr\|line> [if <reg> <op> <val\|reg\|*addr>]`, `b` | set a breakpoint; the condition compares a register with a constant, another register or the 8-byte word at `*addr` |
| `delete <label\|addr>` | remove a breakpoint | | `delete <label\|addr>`, `d` | remove a breakpoint |
| `info break` | list the breakpoints | | `info break`, `info breakpoints`, `info b` | list the breakpoints |
| `step [n]`, `s` | single-step n instructions | | `step [n]`, `s` | single-step n instructions |
| `next`, `n` | step over a CALL | | `next`, `n` | step over a CALL |
| `finish`, `fin` | run until the function returns | | `finish`, `fin` | run until the function returns |
@@ -346,13 +353,18 @@ Usage: gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]
Compare the gasm encoder for the given architecture (default amd64) against the Compare the gasm encoder for the given architecture (default amd64) against the
installed `go tool asm` and print the diff: superset encodings (gasm-only installed `go tool asm` and print the diff: superset encodings (gasm-only
spellings, shippable via `gasm asm --format goobj`), known-but-unencodable spellings, shippable via `gasm asm --format goobj`) and known-but-unencodable
names (the encoder backlog) and go-only names (feature gaps). The Go side is names (the encoder backlog). The Go side is probed black-box one bare mnemonic
probed black-box with a battery of operand shapes per mnemonic, so the audit at a time, classified by the toolchain's diagnostic for an instruction it does
tracks whatever toolchain `go env GOROOT` provides. On non-amd64 not know, so the audit tracks whatever toolchain `go env GOROOT` provides; the
architectures the backlog is an over-approximation: a name counts as encodable gasm side answers from the encoder table on amd64 and from trial assembly over a
only when a probe shape assembles cleanly, so a name whose real forms the battery of operand shapes on the other architectures. On non-amd64
battery misses lands in the backlog. architectures the backlog is therefore an over-approximation: a name counts as
encodable only when a probe shape assembles cleanly, so a name whose real forms
the battery misses lands in the backlog. Names the toolchain knows and gasm does
not cannot be enumerated by probing at all, because Go's table is visible only
through names already in the gasm table; the report closes with a note saying
so rather than listing them.
```sh ```sh
gasm audit-instructions amd64 gasm audit-instructions amd64
@@ -360,8 +372,9 @@ gasm audit-instructions amd64
```text ```text
gasm table (amd64, families excluded): 1542 mnemonics gasm table (amd64, families excluded): 1542 mnemonics
gasm encodable: 580 go tool asm recognized: 1542 gasm encodable: 587 go tool asm recognised: 1542
shared: 580 shared: 587
...
``` ```
With `--corpus` the audit changes shape: it assembles every `.s` file under With `--corpus` the audit changes shape: it assembles every `.s` file under
@@ -381,10 +394,12 @@ gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
```text ```text
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures) corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
assemble for every target architecture: 108 (17.2%) assemble for every target architecture: 127 (20.3%)
amd64: 77/464 attempted amd64: 82/464 attempted
148 instruction not encodable 165 unsupported operand form
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s
109 instruction not encodable
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386.s
... ...
``` ```
@@ -468,5 +483,5 @@ gasm asm --format goobj -p example.com/kernel -o kernel.o kernel_amd64.s
Find which labels a failing kernel reaches, headlessly: Find which labels a failing kernel reaches, headlessly:
```sh ```sh
gasm debug --func decodeBlockAVX2 --cover --script cmds.txt --timeout 30s kernel_amd64.s gasm debug --func decodeBlockAVX2 --cover --timeout 30s kernel_amd64.s
``` ```
+24 -7
View File
@@ -6,9 +6,15 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
- **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares - **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares
- **just**, the command runner; every task below is a just recipe - **just**, the command runner; every task below is a just recipe
- **A C compiler** (`gcc`): `just race` runs the suite under the race detector,
which needs cgo
- **Perl**: the `test`, `fmt-check`, `install-man` and `uninstall-man` recipes
are Perl programs
- **`gzip`**: `install-man` compresses the man pages with it
- A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace - A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace
and the JIT checks of `gasm verify` need executable memory and the JIT checks of `gasm verify` need executable memory
- No external dependencies beyond the Go toolchain - **`golang.org/x/arch`**, the one module dependency, which the Go toolchain
fetches; nothing else sits outside the standard library
## Setup ## Setup
@@ -25,8 +31,9 @@ Every recipe in the `justfile`, and what it does.
| Recipe | What it does | | Recipe | What it does |
|---|---| |---|---|
| `default` (bare `just`) | prints the recipe list (`@just --list`) |
| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings | | `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings |
| `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor | | `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile |
| `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` | | `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` |
| `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly | | `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly |
| `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one | | `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one |
@@ -36,7 +43,7 @@ Every recipe in the `justfile`, and what it does.
| `just vet` | both static gates: `go vet` and `go fix -diff` | | `just vet` | both static gates: `go vet` and `go fix -diff` |
| `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done | | `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done |
| `just clean` | removes the build artefacts, `bin/` and `coverage.out` | | `just clean` | removes the build artefacts, `bin/` and `coverage.out` |
| `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`); `gasm debug` needs an installed binary, because it spawns the debuggee from `$PATH` | | `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`) |
| `just uninstall` | removes the installed binary from `bindir` | | `just uninstall` | removes the installed binary from `bindir` |
| `just install-man` | installs the man pages under `docs/man` into `~/.local/share/man/man1` (`MANDIR` overrides), gzip-compressed; not a gate | | `just install-man` | installs the man pages under `docs/man` into `~/.local/share/man/man1` (`MANDIR` overrides), gzip-compressed; not a gate |
| `just uninstall-man` | removes the installed man pages | | `just uninstall-man` | removes the installed man pages |
@@ -55,9 +62,18 @@ go test -count=1 -timeout 10m -coverprofile=coverage.out \
The suite runs over the logic packages (`-count=1`, so no cached pass The suite runs over the logic packages (`-count=1`, so no cached pass
counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser, counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser,
token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI
glue, so both sit outside the sweep, and a thin `cmd/` in it would drag glue, so both sit outside the profile sweep, and a thin `cmd/` in it
the coverage total under the floor. The floor fails if the total is would drag the coverage total under the floor. Their tests still run, in
below 80 %. CI runs the same command with the same ten-minute bound, so a second invocation without a profile:
```sh
go test -count=1 -timeout 10m ./cmd/... ./debug/...
```
That covers the CLI's exit codes and the guard that compares the manual
pages with the binary's own help, and the debugger's architecture-neutral
units. The floor fails if the total is below 80 %. CI runs the same two
commands with the same ten-minute bound, so
the number is the same everywhere. the number is the same everywhere.
### `just run` ### `just run`
@@ -131,7 +147,8 @@ therefore the fastest way to a green pipeline.
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`, Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`,
which triggers the release workflow: it builds the portable Linux targets, which triggers the release workflow: it builds the portable Linux targets,
takes the notes from the matching `CHANGELOG.md` section and uploads the takes the notes from the matching `CHANGELOG.md` section and uploads the
assets. assets. `SECURITY.md` carries the supported-versions table, so that table
moves with the release; the pipeline refuses a tag the policy does not name.
The version is never injected. `gasm --version` prints what the The version is never injected. `gasm --version` prints what the
toolchain recorded in the build information: the tag on a tagged toolchain recorded in the build information: the tag on a tagged
+15 -4
View File
@@ -20,13 +20,24 @@ flag selects what is written:
self-consistent image; self-consistent image;
.B elf .B elf
emits a relocatable object (.text/.data sections, a symbol table and emits a relocatable object (.text/.data sections, a symbol table and
one PC32 relocation per static-symbol reference) that links with the one relocation per static-symbol reference, in the architecture's own
form: R_X86_64_PC32 on amd64, R_AARCH64_*, R_RISCV_* or R_LARCH_* on the
others) that links with the
system toolchain; system toolchain;
.B goobj .B goobj
emits the Go toolchain's own object format, which cmd/link consumes emits the Go toolchain's own object format, which cmd/link consumes
directly (it requires directly (it requires
.BR \-p , .BR \-p ,
the package path, and the installed Go toolchain). the package path, and the installed Go toolchain: the object preamble is
captured from
.B go tool asm
and the format version from
.BR "go version" ).
.PP
.B raw
and
.B elf
need no toolchain at all.
.PP .PP
Framed functions receive the stack-split guard and the trailing Framed functions receive the stack-split guard and the trailing
morestack block, byte-identical to the toolchain's output, so split morestack block, byte-identical to the toolchain's output, so split
@@ -52,8 +63,8 @@ error.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm asm \-o hello.bin hello_amd64.s raw image gasm asm \-o hello.bin hello_amd64.s raw image
gasm asm \-\-format elf \-o k.o k.s linkable ELF object gasm asm \-\-format elf \-o k.o k_amd64.s linkable ELF object
gasm asm \-\-format goobj \-p pkg/path \-o k.o k.s Go object for go build gasm asm \-\-format goobj \-p pkg/path \-o k.o k_amd64.s Go object for go build
gasm asm \-GOARCH amd64 cpu_x86.s arch override gasm asm \-GOARCH amd64 cpu_x86.s arch override
.fi .fi
.SH SEE ALSO .SH SEE ALSO
+10 -5
View File
@@ -8,12 +8,17 @@ Compare the gasm encoder for the given architecture (default amd64)
against against
.B go tool asm .B go tool asm
and print the diff: superset encodings (gasm-only, shippable via and print the diff: superset encodings (gasm-only, shippable via
.BR "gasm asm \-\-format goobj" ), .BR "gasm asm \-\-format goobj" )
known-but-unencodable names (the backlog) and go-only names (feature and known-but-unencodable names (the backlog). The Go side is probed
gaps). The Go side is probed black-box with a battery of bare black-box one bare mnemonic at a time, so the audit tracks whatever
mnemonics, so the audit tracks whatever toolchain toolchain
.B go env GOROOT .B go env GOROOT
provides. provides; the gasm side answers from the encoder table on amd64 and from
trial assembly over a battery of operand shapes elsewhere. Names
.B go tool asm
knows and gasm does not cannot be enumerated by probing, because Go's
table is visible only through names already in the gasm table; the report
closes with a note saying so rather than listing them.
.PP .PP
With With
.BR \-\-corpus , .BR \-\-corpus ,
+7 -5
View File
@@ -17,14 +17,16 @@ runs to completion with a breakpoint on every instruction and reports
which executed and how often, the label-level coverage view. which executed and how often, the label-level coverage view.
.SH REPL COMMANDS .SH REPL COMMANDS
.TP .TP
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR] .B break \fIlabel|addr|line\fR [\fBif \fIreg op val|reg|*addr\fR], b
Set a breakpoint, optionally conditional on a register comparison Set a breakpoint at a label, an address or a source line number, optionally
(register against register or immediate). conditional on a register comparison: against a constant, against another
register, or against the 8-byte word at
.BR *addr .
.TP .TP
.B delete \fIlabel|addr\fR .B delete \fIlabel|addr\fR, d
Remove a breakpoint. Remove a breakpoint.
.TP .TP
.B info break .B info break, info breakpoints, info b
List all breakpoints. List all breakpoints.
.TP .TP
.BR step " [" n ], " s .BR step " [" n ], " s
+1 -1
View File
@@ -28,7 +28,7 @@ differs; a usage error exits 2.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm diff hello_amd64.s hello_amd64.s gasm diff hello_amd64.s hello_amd64.s
gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2_amd64.s avx512_amd64.s
.fi .fi
.SH SEE ALSO .SH SEE ALSO
.BR gasm (1), .BR gasm (1),
+1 -1
View File
@@ -28,7 +28,7 @@ Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
error. error.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm dis k.s assemble, then list each function gasm dis k_amd64.s assemble, then list each function
gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin
.fi .fi
.SH SEE ALSO .SH SEE ALSO
+7
View File
@@ -41,6 +41,13 @@ List files whose formatting differs from gasm's.
.TP .TP
.B \-w .B \-w
Write the result to the source file. Write the result to the source file.
.SH EXIT STATUS
Exits 0 on success, 1 when a path cannot be read or written, and 2 on a
usage error (combining
.B \-l
and
.BR \-d ,
or an unknown flag).
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm fmt reformat every .s below here gasm fmt reformat every .s below here
+17 -5
View File
@@ -33,7 +33,11 @@ The function can fall off its end without a terminator.
TEXT flags are used without including textflag.h. TEXT flags are used without including textflag.h.
.TP .TP
.B abi-argsize .B abi-argsize
The declared frame or argument size disagrees with the The declared argument area (the
.I \-args
part of
.IR $frame\-args )
disagrees with the
.B //\ function .B //\ function
signature. signature.
.TP .TP
@@ -52,7 +56,8 @@ FUNCDATA and PCDATA indices are malformed.
A label no jump reaches. A label no jump reaches.
.TP .TP
.B invalid-textflag .B invalid-textflag
A TEXT flag combination the toolchain rejects. An unknown TEXT or GLOBL flag, reported one flag at a time; numeric flags
are accepted as textflag.h constants.
.TP .TP
.B stack-imbalance .B stack-imbalance
The function does not restore the stack pointer on every path. The function does not restore the stack pointer on every path.
@@ -61,11 +66,18 @@ The function does not restore the stack pointer on every path.
An operand register has the wrong width for the instruction. An operand register has the wrong width for the instruction.
.TP .TP
.B abi0-register-args .B abi0-register-args
A call passes arguments in registers where ABI0 expects the stack A function whose
frame. .B //\ function
parameters are never read from their
.IR name+offset(FP)
frame slots, which usually means the body takes its arguments from
registers instead.
.TP .TP
.B nonportable-register-name .B nonportable-register-name
A register spelling that does not exist on the target architecture. An amd64 register alias gasm accepts but
.B go tool asm
rejects (the RAX/EAX family); the canonical spelling is named in the
diagnostic.
.TP .TP
.B unencodable-instruction .B unencodable-instruction
The mnemonic is known to the table but the encoder cannot assemble it The mnemonic is known to the table but the encoder cannot assemble it
+4 -3
View File
@@ -6,9 +6,10 @@ gasm-profile \- show the basic-block structure of functions
.SH DESCRIPTION .SH DESCRIPTION
Show the basic-block structure of functions in an assembly file: each Show the basic-block structure of functions in an assembly file: each
function's labels, their offsets, and the block boundaries. This is function's labels, their offsets, and the block boundaries. This is
the static structure; for runtime execution counts, use the static structure; for runtime execution counts use
.BR "gasm verify \-fuzz" , .BR "gasm debug \-\-cover" ,
which exercises the code paths. and for input coverage
.BR "gasm verify \-\-fuzz" .
.SH EXIT STATUS .SH EXIT STATUS
Exits 0 on success and 1 when the file cannot be assembled. Exits 0 on success and 1 when the file cannot be assembled.
.SH SEE ALSO .SH SEE ALSO
+5 -4
View File
@@ -45,7 +45,8 @@ a single function is invoked with user-supplied buffers
.RB ( \-buf ) .RB ( \-buf )
instead of the smoke/abi/fuzz sweeps. Useful for partial functions instead of the smoke/abi/fuzz sweeps. Useful for partial functions
(e.g. decoders) that crash on random input but should succeed on valid (e.g. decoders) that crash on random input but should succeed on valid
data. data. The function named must be NOSPLIT: a function with a stack frame
is refused with a diagnostic and exits 1.
.PP .PP
With With
.B \-save\-corpus .B \-save\-corpus
@@ -73,7 +74,7 @@ Buffer spec for -call: name:size:pattern[,name:size:pattern] where
pattern is zero, ones, seq, or hex. pattern is zero, ones, seq, or hex.
.TP .TP
.B \-call \fIname\fR .B \-call \fIname\fR
Call a single function with -buf instead of the sweeps. Call a single NOSPLIT function with -buf instead of the sweeps.
.TP .TP
.B \-fuzz .B \-fuzz
Differential fuzz: JIT both the gasm and the go-tool-asm versions and Differential fuzz: JIT both the gasm and the go-tool-asm versions and
@@ -107,8 +108,8 @@ a file that cannot be assembled exits 1 and a usage error exits 2.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
gasm verify \-\-ground\-truth k.s gasm verify \-\-ground\-truth k_amd64.s
gasm verify \-\-fuzz \-n 500 k.s gasm verify \-\-fuzz \-n 500 k_amd64.s
.fi .fi
.SH SEE ALSO .SH SEE ALSO
.BR gasm (1), .BR gasm (1),
+8 -4
View File
@@ -17,11 +17,15 @@ bundles a lexer, parser, formatter, linter, standalone assembler and
language server for Plan 9 assembly into one self-contained binary. It language server for Plan 9 assembly into one self-contained binary. It
serves two purposes: it brings developer tooling to the serves two purposes: it brings developer tooling to the
.I .s .I .s
files of Go programs, and it assembles Plan 9 assembly without the Go files of Go programs, and it assembles Plan 9 assembly to raw images or
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug linkable ELF objects with DWARF5 debug sections without the Go toolchain at
sections, or the Go toolchain's own GOOBJ format, which all, plus the Go toolchain's own GOOBJ format, which
.B go build .B go build
consumes directly. consumes directly. GOOBJ is the one format that needs the toolchain
installed: the object preamble is captured from
.B go tool asm
and the format version from
.BR "go version" .
.PP .PP
Four architectures are covered: amd64 (including VEX/AVX2 and Four architectures are covered: amd64 (including VEX/AVX2 and
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
+7 -4
View File
@@ -2,13 +2,16 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package verify provides the dynamic-analysis substrate for gasm: it // Package verify provides the dynamic-analysis substrate for gasm: it
// JIT-assembles Plan 9 amd64 kernels into executable memory and calls them // JIT-assembles Plan 9 kernels for all four supported architectures
// (amd64, arm64, riscv64, loong64) into executable memory and calls them
// directly, enabling differential testing against portable Go references, // directly, enabling differential testing against portable Go references,
// runtime ABI checks and basic-block coverage profiling. // runtime ABI checks and basic-block coverage profiling.
// //
// The execution model is pure Go (stdlib only): machine code is mapped with // The execution model is pure Go, stdlib only, with no cgo: machine code is
// syscall.Mmap and invoked through an assembly trampoline that switches to a // mapped with syscall.Mmap and invoked through an assembly trampoline that
// prepared ABI0 stack. No cgo, no external toolchain. // switches to a prepared ABI0 stack. The toolchain-comparison helpers in
// this package (groundtruth.go) are the one exception, shelling out to the
// installed Go toolchain and using its assembler as the differential oracle.
package verify package verify
import ( import (