Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4f949c1f | ||
|
|
f0d5238c47 | ||
|
|
2931bbd6b2 | ||
|
|
63562a503a | ||
|
|
e836d6150d | ||
|
|
8a51b060da | ||
|
|
d3d47db727 | ||
|
|
0758556b7d | ||
|
|
ddb8440340 | ||
|
|
f15ff66fb1 | ||
|
|
187e4856d3 | ||
|
|
d315a998ce |
@@ -55,6 +55,25 @@ jobs:
|
||||
print qq{tag $v\n};
|
||||
'
|
||||
|
||||
- name: Security policy names this release
|
||||
# The supported-versions table is the one part of SECURITY.md that
|
||||
# carries a version, so it goes stale the moment a tag is cut. Fail
|
||||
# here rather than publish a policy naming the previous release.
|
||||
env:
|
||||
VERSION: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
perl -e '
|
||||
my $v = $ENV{VERSION} // q{};
|
||||
(my $nv = $v) =~ s/^v//;
|
||||
open(my $f, q{<}, q{SECURITY.md}) or die qq{SECURITY.md: $!\n};
|
||||
local $/;
|
||||
my $t = <$f>;
|
||||
close $f;
|
||||
$t =~ m{^\|\s*\Q$nv\E\s*\|\s*yes\s*\|}m
|
||||
or die qq{ERROR: SECURITY.md does not name $nv as supported; update the table before releasing.\n};
|
||||
print qq{SECURITY.md names $nv\n};
|
||||
'
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
||||
@@ -78,6 +97,11 @@ jobs:
|
||||
# The same command as in test.yml, so the floor is the same number everywhere.
|
||||
run: go test -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 same command as in test.yml: the CLI's exit codes and manual-page guard,
|
||||
# and the debugger's architecture-neutral units, run outside the floor.
|
||||
run: go test -count=1 -timeout 10m ./cmd/... ./debug/...
|
||||
|
||||
- name: Coverage floor
|
||||
run: |
|
||||
perl -e '
|
||||
|
||||
@@ -84,6 +84,14 @@ jobs:
|
||||
# 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/...
|
||||
|
||||
- name: Tests outside the coverage set
|
||||
# The CLI and the debugger sit outside `packages` because a thin main and a
|
||||
# ptrace-bound package pull the total under the floor, but their tests guard
|
||||
# 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.
|
||||
|
||||
+101
-47
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
-
|
||||
|
||||
## [0.34.0] - 2026-09-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Indirect JMP and CALL on all four architectures.** `JMP AX`,
|
||||
`CALL AX`, `JMP (BX)` and the memory forms encode at byte parity with
|
||||
the toolchain (FF /2 and FF /4 on amd64); arm64 lowers `JMP (R0)` to
|
||||
@@ -16,8 +22,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
|
||||
assembler cannot express. A frameless amd64 function containing a
|
||||
CALL now receives the toolchain's forced base-pointer frame. The
|
||||
verify trampolines join the ground-truth lists, and a lint check for
|
||||
control flow through registers and memory extends to the new forms.
|
||||
riscv64 and loong64 verify trampolines join their ground-truth lists,
|
||||
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
|
||||
architecture can be named explicitly instead of inferred from the
|
||||
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||
@@ -25,38 +32,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`gasm audit-instructions --corpus [dir]`.** Assembles every `.s`
|
||||
file under a directory (default GOROOT/src) with the gasm encoder
|
||||
only: suffixed files for their architecture, suffix-less files for
|
||||
all four, as a GOARCH build would. Reports the headline number (108
|
||||
of 627 GOROOT files, 17.2 %, assemble for every target architecture,
|
||||
against 23 in the previous release), the per-architecture pass rates
|
||||
and the most common failure reasons with a representative file each,
|
||||
all four, as a GOARCH build would. Reports the headline number (127
|
||||
of 627 GOROOT files, 20.3 %, assemble for every target architecture),
|
||||
the per-architecture pass rates and the most common failure reasons
|
||||
with a representative file each,
|
||||
which drive the encodability backlog by frequency.
|
||||
- **Fuzz targets for the parser and the formatter.** FuzzParse (no
|
||||
panic, always a usable file) and FuzzFormatIdempotency (formatting
|
||||
twice equals formatting once; clean input stays clean) seed
|
||||
themselves from the repository's kernels, so the plain test suite
|
||||
replays every seed in CI and `just fuzz` runs the mutation engine on
|
||||
demand.
|
||||
- **Oracle parity as its own CI step.** The push pipeline already ran
|
||||
the live go-tool-asm comparison inside the suite; a dedicated step
|
||||
now names that gate when it fails.
|
||||
- **Man pages.** docs/man carries gasm(1) and one page per command,
|
||||
written in roff: synopsis, description, every flag with its default,
|
||||
exit status, worked examples and cross-references.
|
||||
- **The parser and the formatter are fuzzed.** Two targets carry the
|
||||
guarantee: no input makes the parser panic, and every input yields a
|
||||
file the rest of the toolkit can work on; formatting twice equals
|
||||
formatting once, and clean input stays clean. They seed from the
|
||||
repository's own kernels, and `just fuzz` drives the mutation engine
|
||||
on demand.
|
||||
- **Man pages.** docs/man carries gasm(1) and a page for every command
|
||||
except `version`, which gasm(1) documents itself, 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
|
||||
overrides) and `just uninstall-man` removes them. A test builds the
|
||||
binary and compares every command's `-h` output with its page, so the
|
||||
pages cannot drift from the CLI.
|
||||
binary and compares each page's flags and synopsis with its own `-h`
|
||||
output, so the pages cannot drift from the CLI.
|
||||
|
||||
### 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
|
||||
(build, fmt-check, vet, test, race). `install` now builds and copies
|
||||
the binary into `~/.local/bin` (`BINDIR` overrides) instead of
|
||||
downloading module dependencies, and `install-bin` is gone. The test
|
||||
gate sweeps the logic packages (arch through verify; the hardware-bound
|
||||
`debug` and the thin `cmd/gasm` sit outside it), so the coverage floor
|
||||
is computed over the product code and the number is identical locally
|
||||
and in CI. `fuzz` requires its target package.
|
||||
gate sweeps the logic packages (arch through verify; the ptrace-bound
|
||||
`debug` and the thin `cmd/gasm` sit outside the coverage profile), so
|
||||
the coverage floor is computed over the product code and the number is
|
||||
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`
|
||||
prints the version the toolchain recorded: the tag on a tagged
|
||||
checkout, a pseudo-version naming the commit below one, `+dirty` on a
|
||||
@@ -78,8 +87,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
|
||||
worked examples; `CONTRIBUTING.md` carries the Contributor terms and
|
||||
states the commit trailer form, the one-logical-change rule and the
|
||||
licence header rule. The repository's own assembly (the `verify`
|
||||
trampolines and the test kernels) is in `gasm fmt` canonical form.
|
||||
licence header rule; `SECURITY.md` states how a vulnerability is
|
||||
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
|
||||
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,
|
||||
@@ -88,7 +99,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
|
||||
assembly section, and carries a Direction section: extended
|
||||
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
|
||||
|
||||
@@ -125,13 +139,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
|
||||
UTF-8 no longer inflates byte offsets, and CRLF files format to
|
||||
uniform LF.
|
||||
- **A frameless amd64 function containing a CALL read its arguments from
|
||||
the wrong stack slot.** The forced base-pointer frame shifted
|
||||
FP references by eight bytes (`x+0(FP)` resolved to SP+0x18 where the
|
||||
toolchain emits SP+0x10), so such functions loaded garbage. The
|
||||
class-2 stack guard had the sibling defect: whenever the underflow
|
||||
branch relaxed to its 32-bit form, its displacement ran four bytes
|
||||
past the target and into the morestack CALL.
|
||||
- **The class-2 stack guard branched four bytes past its target.** When
|
||||
the underflow branch relaxed to its 32-bit form, its displacement was
|
||||
still computed as if the branch were two bytes long, so it landed
|
||||
inside the morestack CALL instead of the compare that decides it.
|
||||
The long form is reachable once a large frame carries a body of roughly
|
||||
a hundred bytes.
|
||||
- **Immediate operands wrapped silently on amd64.** Shift counts,
|
||||
immediates beyond the operand's width and displacements beyond int32
|
||||
truncated without a diagnostic (`SHLQ $300` assembled as `$44`); they
|
||||
@@ -263,6 +276,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
exit-2 contract now holds, `asm -o` no longer prints the hex dump it
|
||||
claimed to replace, and `verify --ground-truth` works for amd64
|
||||
kernels on non-amd64 hosts instead of refusing with JIT advice.
|
||||
- **arm64 store-exclusive instructions read their operands in the
|
||||
toolchain's order.** `STXR` treated the first register as the status
|
||||
register where `go tool asm` reads it as the data register, so the
|
||||
same source assembled to different code in the two assemblers; the
|
||||
pair forms (`STXP`, `LDXP` and their acquire/release variants) are
|
||||
accepted now, in the toolchain spelling.
|
||||
- **Large arm64 frames matched the toolchain's sequences.** A frame
|
||||
beyond the immediate range that is not a movcon constant (roughly
|
||||
64 KiB and up) made `gasm verify` report a false mismatch: the
|
||||
toolchain splits the prologue subtraction into two 12-bit immediates
|
||||
and materialises the non-leaf epilogue addition through the temporary
|
||||
register; gasm emits the same sequences and the spadj boundaries
|
||||
follow the real word counts.
|
||||
- **The width spellings GOROOT uses assemble.** `MOVLQZX` (four uses in
|
||||
`runtime/asm_amd64.s`), `MOVBQSX`, `MOVWQSX`, `MOVBLSX`, `MOVBWSX`,
|
||||
`MOVBWZX` and `PMOVMSKB` (the bytealg kernels) encode byte-identically
|
||||
with `go tool asm`, and the linter reports them encodable; a
|
||||
`MOVLQZX` is the plain 32-bit move, exactly as the toolchain lowers
|
||||
it.
|
||||
- **`verify --ground-truth` no longer reports a mismatch for functions
|
||||
whose size is not a multiple of 16.** The toolchain pads text symbols
|
||||
to 16-byte boundaries; the comparison now checks the padding is zero
|
||||
instead of comparing it, the same rule the test suite applies.
|
||||
- **riscv64 accepts the `g` spelling of the goroutine register**, like
|
||||
the other architectures, and the abi kernels use it; every verify
|
||||
kernel is now ground-truth checkable (the numeric `X27` spelling the
|
||||
kernels used is one `go tool asm` rejects).
|
||||
- **Two more spellings GOROOT uses now assemble.** riscv64 `FCLASSD`
|
||||
(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
|
||||
validated end to end under qemu-user emulation (plain and ABI-checked
|
||||
calls, goroutine-clobber detection), so `gasm verify` runs the JIT
|
||||
checks on loong64 hosts instead of forcing every loong64 kernel down
|
||||
the ground-truth path. The arm64 and riscv64 trampolines carry the
|
||||
same validation; the arm64 ABI test now seeds its kernel arguments
|
||||
(a zeroed block made the passthrough check meaningless), and the
|
||||
loong64 basic kernel's branch maze terminates on every path so the
|
||||
smoke sweep cannot spin on leftover register values.
|
||||
|
||||
## [0.33.0] - 2026-09-14
|
||||
|
||||
@@ -481,7 +534,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [0.31.0] - 2026-08-20
|
||||
|
||||
The arm64 encoder (Phase 5; complete) ships with ELF64 and GOOBJ emission,
|
||||
The arm64 encoder ships with ELF64 and GOOBJ emission,
|
||||
verified byte-for-byte against `GOARCH=arm64 go tool asm` and linked into a
|
||||
real `go build`. The encoder covers the full integer instruction set, FP
|
||||
arithmetic, conditional select, CRC32, and the MOV pseudo-instruction with
|
||||
@@ -489,7 +542,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
||||
|
||||
### Added
|
||||
|
||||
- **arm64 encoder (Phase 5; complete).** `gasm asm` can now assemble `_arm64.s`
|
||||
- **arm64 encoder.** `gasm asm` can now assemble `_arm64.s`
|
||||
files: the AArch64 integer instruction set with the MOV pseudo-instruction and
|
||||
its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with
|
||||
logical bitmask encoding for values like `$1`), data-processing (shifted
|
||||
@@ -497,8 +550,9 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
||||
immediate), conditional and unconditional branches, FP/SP frame mapping,
|
||||
SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations),
|
||||
jump chain folding, and ELF64 emission (`gasm asm --format elf`). Ground-truth
|
||||
verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. Phase 5
|
||||
(the other architectures; RISC-V, LoongArch, arm64) is now complete.
|
||||
verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. The
|
||||
encoder set for the remaining architectures (RISC-V, LoongArch, arm64) is
|
||||
complete.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -508,7 +562,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
||||
|
||||
## [0.30.0] - 2026-08-13
|
||||
|
||||
The LoongArch encoder (Phase 5) ships with ELF64 and GOOBJ emission, verified
|
||||
The LoongArch encoder ships with ELF64 and GOOBJ emission, verified
|
||||
byte-for-byte against `GOARCH=loong64 go tool asm` and linked into a real
|
||||
`go build`; the shared GOOBJ emitter now writes the per-function DWARF symbols
|
||||
the linker's DWARF pass reads. The RISC-V encoder reaches byte-for-byte parity
|
||||
@@ -519,7 +573,7 @@ tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
||||
|
||||
### Added
|
||||
|
||||
- **LoongArch encoder (Phase 5).** `gasm asm` can now assemble `_loong64.s`
|
||||
- **LoongArch encoder.** `gasm asm` can now assemble `_loong64.s`
|
||||
files: the full LoongArch64 instruction set with the dual-form arithmetic
|
||||
mnemonics, the 16/21-bit branch families, the MOV pseudo-instruction and
|
||||
its immediate-constant expansions, FP/SP frame mapping, SB/global symbol
|
||||
@@ -608,7 +662,7 @@ tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
||||
|
||||
- **Linux only.** The toolkit, its CI and the released binaries are now
|
||||
Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}.
|
||||
- **Phase 4 closed.** README's "Remaining" list for the debugger is gone;
|
||||
- **Debugger complete.** README's "Remaining" list for the debugger is gone;
|
||||
disassembly at PC, memory-write, watchpoints, and source-line mapping are
|
||||
all shipped.
|
||||
|
||||
@@ -818,7 +872,7 @@ exposes the full dynamic-analysis toolkit.
|
||||
|
||||
## [0.20.0] - 2026-07-25
|
||||
|
||||
Coverage profiling: the third pillar of Phase 3. Static basic-block
|
||||
Coverage profiling. Static basic-block
|
||||
enumeration from the assembler's label map, combined with multi-input path
|
||||
diversity measurement; how many observationally distinct execution paths a
|
||||
test corpus exercises.
|
||||
@@ -843,7 +897,7 @@ execute) without fighting the runtime.
|
||||
|
||||
## [0.19.0] - 2026-07-24
|
||||
|
||||
Runtime ABI checks: the second pillar of Phase 3. The JIT trampoline now
|
||||
Runtime ABI checks. The JIT trampoline now
|
||||
has an ABI-checking variant that sets sentinels in the callee-saved registers
|
||||
(BP, R14) before entering the assembled function and verifies they survive on
|
||||
return, plus a red-zone canary (128 bytes below SP filled with 0xA5) that
|
||||
@@ -878,7 +932,7 @@ codes. This is the automated form of the project's bit-identical contract.
|
||||
|
||||
## [0.17.0] - 2026-07-22
|
||||
|
||||
Phase 3 begins: dynamic analysis. A JIT execution substrate that assembles
|
||||
Dynamic analysis. A JIT execution substrate that assembles
|
||||
Plan 9 amd64 kernels into executable memory and calls them directly; pure Go
|
||||
(stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external
|
||||
toolchain.
|
||||
@@ -1285,8 +1339,8 @@ support).
|
||||
|
||||
## [0.2.0] - 2026-07-07
|
||||
|
||||
The Phase 2 assembler grows the SIMD set: shuffles, extract/insert, permute
|
||||
and the moves, on top of the Phase 1 VEX forms.
|
||||
The assembler grows the SIMD set: shuffles, extract/insert, permute
|
||||
and the moves, on top of the VEX forms of the first release.
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1322,7 +1376,7 @@ and the moves, on top of the Phase 1 VEX forms.
|
||||
|
||||
## [0.1.0] - 2026-07-06
|
||||
|
||||
Initial release; the Phase 1 foundation.
|
||||
Initial release: the foundation.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+15
-9
@@ -24,7 +24,9 @@ below; submitting one means you accept them.
|
||||
## Development setup
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
`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
|
||||
file names what it contains. Errors are checked explicitly, wrapped as
|
||||
`fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The recipe file holds
|
||||
the commands, and the language and standard-library surface is the one the `go` directive
|
||||
in `go.mod` pins.
|
||||
warnings tolerated. `just vet` is two gates, `go vet ./...` and `go fix -diff ./...`,
|
||||
so the modernisation rewrites are enforced too. `just gates` is the definition of done in
|
||||
one command, and the recipe file names what it contains. Errors are checked explicitly,
|
||||
wrapped as `fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The
|
||||
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:
|
||||
`gasm dis` and the debugger's listings decode through it. Everything else is the
|
||||
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
|
||||
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
|
||||
@@ -111,8 +117,8 @@ Workflows live in `.gitea/workflows/` and run on the project's own runners:
|
||||
|
||||
| Workflow | Trigger | What it does |
|
||||
|---|---|---|
|
||||
| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor |
|
||||
| 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 |
|
||||
| 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 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
|
||||
race detector also has its own workflow, dispatched by hand; it never runs on a push or a
|
||||
|
||||
@@ -5,23 +5,25 @@
|
||||
> 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
|
||||
> 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:
|
||||
there is no formatter, no linter, no static analyser, no standalone
|
||||
assembler and no debugger for `.s` files. Developers write assembly
|
||||
blind, validate it by benchmark, and debug it by print statement.
|
||||
gasm-devkit is the missing toolkit: a single, self-contained binary,
|
||||
`gasm`, that serves both purposes.
|
||||
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
|
||||
binary, `gasm`, that serves both purposes.
|
||||
|
||||
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
||||
dynamic verification, a source-level debugger and a language server,
|
||||
for `.s` files in Go programs.
|
||||
- **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes
|
||||
on its own, with no Go installation in the loop, and writes raw
|
||||
images, linkable ELF objects with DWARF5 debug sections, or the Go
|
||||
on its own and writes raw images or linkable ELF objects with DWARF5
|
||||
debug sections, with no Go installation in the loop; the Go
|
||||
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
|
||||
|
||||
@@ -69,12 +71,14 @@ to give that syntax the tooling it deserves.
|
||||
operating recursively on directories the way `go fmt` does. `-l` lists
|
||||
files whose formatting differs and `-d` prints a unified diff.
|
||||
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
|
||||
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
||||
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
||||
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`.
|
||||
`undefined-label`, `abi-argsize` (declared argument area vs the `// func`
|
||||
signature), `register-clobber` (Go ABI register liveness over the
|
||||
control-flow graph), `stack-imbalance`, `abi0-register-args` and
|
||||
`unencodable-instruction`.
|
||||
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
||||
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
||||
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
||||
the Go toolchain and writes raw images or linkable ELF objects (with DWARF5
|
||||
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
|
||||
stack-split guard and the morestack block, byte-identical to the
|
||||
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.
|
||||
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
|
||||
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,
|
||||
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
||||
find references, rename, formatting, inlay hints, code actions, signature
|
||||
@@ -119,10 +124,38 @@ can emit today is narrower, and a recognised but unencodable instruction is
|
||||
reported as an explicit error, never as a wrong byte.
|
||||
|
||||
The same measurement runs over GOROOT's whole assembly corpus:
|
||||
`gasm audit-instructions --corpus` reports 108 of 627 files (17.2 %)
|
||||
`gasm audit-instructions --corpus` reports 127 of 627 files (20.3 %)
|
||||
assembling for every target architecture today, with the top failure
|
||||
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
|
||||
|
||||
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
|
||||
toolchain-derived tables stay generated and untouched; only the
|
||||
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
|
||||
on every architecture, amd64 included.
|
||||
- **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 debug --func name k.s # interactive debugger
|
||||
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 --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
gasm profile k.s # show basic-block structure
|
||||
@@ -243,7 +277,8 @@ recipe.
|
||||
|
||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||
- 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
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@ releases do not receive them.
|
||||
|
||||
| Version | Supported |
|
||||
|---|---|
|
||||
| 0.33.0 | yes |
|
||||
| 0.34.0 | yes |
|
||||
| older releases | no |
|
||||
|
||||
## Reporting a vulnerability
|
||||
@@ -29,7 +29,8 @@ Include:
|
||||
- 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
|
||||
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
|
||||
|
||||
|
||||
+60
-18
@@ -314,7 +314,8 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
|
||||
return encodeARM64CRC32(mnem, enc.op, ops)
|
||||
}
|
||||
|
||||
// Exclusive load/store (LDXR, STXR, LDAXR, STLXR).
|
||||
// Exclusive load/store (LDXR, STXR, LDAXR, STLXR and the register-pair
|
||||
// forms LDXP, STXP).
|
||||
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FExcl {
|
||||
return encodeARM64Excl(mnem, enc.op, ops)
|
||||
}
|
||||
@@ -771,14 +772,17 @@ func encodeARM64LoadImm(rd int, v int64, mnem string) ([]byte, error) {
|
||||
return a64wordLE(op | 31<<16 | 31<<5 | uint32(rd)), nil
|
||||
}
|
||||
|
||||
// The Go toolchain classifies immediates:
|
||||
// - C_ABCON0 (0 < v ≤ 4095): bitmask first for positive values
|
||||
// - Negative values: MOVN first, then bitmask
|
||||
// - C_MOVCON (movcon-eligible, outside ABCON range): MOVZ/MOVN first
|
||||
tryBitmaskFirst := d > 0 && d <= 0xFFF
|
||||
// The Go toolchain classifies immediates (asm7.go conclass):
|
||||
// - inside the imm12/shifted-imm12 "addcon" band (C_ABCON0/C_ABCON,
|
||||
// 0 < v ≤ 4095 or a 4096 multiple up to 0xFFF000): bitmask first, so
|
||||
// `MOVD $4096, R27` is ORR $4096, not MOVZ $(1<<12)
|
||||
// - outside that band: MOVZ/MOVN first (C_MOVCON before C_BITCON), and
|
||||
// negative values reach MOVN before the bitmask test
|
||||
tryBitmaskFirst := d > 0 && (d <= 0xFFF || (d&0xFFF == 0 && d <= 0xFFF000))
|
||||
|
||||
if tryBitmaskFirst {
|
||||
// Small immediate: try bitmask first (Go uses ORR for values like $1, $256).
|
||||
// Addcon-band immediate: try bitmask first (Go uses ORR for values
|
||||
// like $1, $256 and $65536).
|
||||
N, immr, imms, ok := arm64Bitmask(uint64(d), int(sf))
|
||||
if ok {
|
||||
return a64wordLE(sf<<31 | 1<<29 | 0x24<<23 | N<<22 | immr<<16 | imms<<10 | 31<<5 | uint32(rd)), nil
|
||||
@@ -1354,14 +1358,43 @@ func arm64ExclMem(mnem string, op *ast.Operand) (int, error) {
|
||||
return rn, nil
|
||||
}
|
||||
|
||||
// encodeARM64Excl encodes an exclusive load/store instruction.
|
||||
// LDXR (Rn), Rt → LDXR Rt, [Rn] (2 operands: mem, reg)
|
||||
// STXR Rs, (Rn), Rt → STXR Rs, Rt, [Rn] (3 operands: Rs, mem, Rt-status)
|
||||
// arm64PairOf parses a register-pair operand `(R1, R2)`, reporting false
|
||||
// when the operand is not a pair. The toolchain takes the second register of
|
||||
// the pair from the operand's Offset (its C_PAIR class,
|
||||
// cmd/internal/obj/arm64/asm7.go cases 58/59).
|
||||
func arm64PairOf(op *ast.Operand) (int, int, bool) {
|
||||
raw := strings.TrimSpace(op.Raw)
|
||||
if !strings.HasPrefix(raw, "(") || !strings.HasSuffix(raw, ")") {
|
||||
return -1, -1, false
|
||||
}
|
||||
parts := strings.Split(raw[1:len(raw)-1], ",")
|
||||
if len(parts) != 2 {
|
||||
return -1, -1, false
|
||||
}
|
||||
r1 := arm64RegNum(strings.TrimSpace(parts[0]))
|
||||
r2 := arm64RegNum(strings.TrimSpace(parts[1]))
|
||||
if r1 < 0 || r2 < 0 {
|
||||
return -1, -1, false
|
||||
}
|
||||
return r1, r2, true
|
||||
}
|
||||
|
||||
// encodeARM64Excl encodes the exclusive load/store family with the operand
|
||||
// order the toolchain parses (cmd/internal/obj/arm64/asm7.go cases 58 and 59,
|
||||
// and its own spellings in arm64enc.s):
|
||||
//
|
||||
// STXR Rt, (Rn), Rs store, single register
|
||||
// STXP (Rt1, Rt2), (Rn), Rs store, register pair
|
||||
// LDXR (Rn), Rt load, single register
|
||||
// LDXP (Rn), (Rt1, Rt2) load, register pair
|
||||
//
|
||||
// Decoded toolchain evidence: `STXR R1, (R2), R3` assembles to 0xc8037c41,
|
||||
// whose fields are Rs=3, Rn=2, Rt=1: the FIRST register operand is the data
|
||||
// register and the LAST the status register.
|
||||
func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
||||
// LDXR/STXR have different operand forms.
|
||||
isLoad := strings.HasPrefix(mnem, "LD")
|
||||
if isLoad {
|
||||
// LDXR (Rn), Rt → 2 operands: mem, reg
|
||||
// LDXR (Rn), Rt / LDXP (Rn), (Rt1, Rt2): 2 operands.
|
||||
if len(ops) != 2 {
|
||||
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
@@ -1369,25 +1402,34 @@ func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rt1, rt2, ok := arm64PairOf(ops[1]); ok {
|
||||
// The single-register opcodes pre-set the unused Rs (bits 20:16)
|
||||
// and Rt2 (bits 14:10) fields to 31; the pair forms carry a real
|
||||
// Rt2 and keep Rs at 31.
|
||||
return a64wordLE(baseOp | 0x1F<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
|
||||
}
|
||||
rt := arm64RegNum(operandRegName(ops[1]))
|
||||
if rt < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rt)), nil
|
||||
}
|
||||
// STXR Rs, (Rn), Rt → 3 operands: Rs, mem, Rt
|
||||
// STXR Rt, (Rn), Rs / STXP (Rt1, Rt2), (Rn), Rs: 3 operands.
|
||||
if len(ops) != 3 {
|
||||
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
rs := arm64RegNum(operandRegName(ops[0]))
|
||||
if rs < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
rn, err := arm64ExclMem(mnem, ops[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rt := arm64RegNum(operandRegName(ops[2]))
|
||||
rs := arm64RegNum(operandRegName(ops[2]))
|
||||
if rs < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
if rt1, rt2, ok := arm64PairOf(ops[0]); ok {
|
||||
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
|
||||
}
|
||||
rt := arm64RegNum(operandRegName(ops[0]))
|
||||
if rt < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
|
||||
+23
-3
@@ -98,8 +98,12 @@ func arm64RegNum(name string) int {
|
||||
return 30
|
||||
case "R31", "ZR":
|
||||
return 31
|
||||
case "SP":
|
||||
return 31 // SP and ZR share encoding 31; context determines meaning
|
||||
case "SP", "RSP":
|
||||
// RSP is the toolchain's spelling for register 31 (it rejects
|
||||
// R31 in an operand); SP stays for sources that spell it the
|
||||
// amd64 way. SP and ZR share encoding 31; context determines
|
||||
// the meaning.
|
||||
return 31
|
||||
}
|
||||
// F0-F31.
|
||||
if len(name) >= 1 && name[0] == 'F' {
|
||||
@@ -282,7 +286,7 @@ const (
|
||||
a64FFPSel // FP conditional select (Rm, Rn, Rd, cond): FCSEL
|
||||
a64FCRC32 // CRC32
|
||||
a64FCSEL // conditional select: CSEL, CSINC, CSINV, CSNEG
|
||||
a64FExcl // exclusive load/store: LDXR, STXR, LDAXR, STLXR
|
||||
a64FExcl // exclusive load/store: LDXR, STXR, LDAXR, STLXR and pair forms LDXP, STXP
|
||||
a64FLSE // LSE atomics: LDADD, CAS, SWP
|
||||
a64FSIMD3 // SIMD 3-operand: VADD, VSUB, VMUL
|
||||
)
|
||||
@@ -575,6 +579,10 @@ func init() {
|
||||
}
|
||||
|
||||
// ---- exclusive load/store ----
|
||||
// Single-register forms pre-set the unused Rs and Rt2 fields to 31 (the
|
||||
// 0x7c00/0x1f0000 halves of the constants below); the register-pair
|
||||
// forms carry a real Rt2 in bits 14:10, so their opcodes pre-set
|
||||
// neither field.
|
||||
a64InstrTable["LDXR"] = a64Enc{format: a64FExcl, op: 0xc85f7c00}
|
||||
a64InstrTable["LDXRB"] = a64Enc{format: a64FExcl, op: 0x085f7c00}
|
||||
a64InstrTable["LDXRH"] = a64Enc{format: a64FExcl, op: 0x485f7c00}
|
||||
@@ -583,6 +591,12 @@ func init() {
|
||||
a64InstrTable["LDAXRB"] = a64Enc{format: a64FExcl, op: 0x085ffc00}
|
||||
a64InstrTable["LDAXRH"] = a64Enc{format: a64FExcl, op: 0x485ffc00}
|
||||
a64InstrTable["LDAXRW"] = a64Enc{format: a64FExcl, op: 0x885ffc00}
|
||||
// Pair loads, LDSTX(sz, 0, l=1, o1=1, o0) in asm7.go: LDXP/ LDXPW have
|
||||
// o0=0, LDAXP/LDAXPW o0=1 (bit 15). Rs (bits 20:16) stays 31.
|
||||
a64InstrTable["LDXP"] = a64Enc{format: a64FExcl, op: 0xc8600000}
|
||||
a64InstrTable["LDXPW"] = a64Enc{format: a64FExcl, op: 0x88600000}
|
||||
a64InstrTable["LDAXP"] = a64Enc{format: a64FExcl, op: 0xc8608000}
|
||||
a64InstrTable["LDAXPW"] = a64Enc{format: a64FExcl, op: 0x88608000}
|
||||
a64InstrTable["STXR"] = a64Enc{format: a64FExcl, op: 0xc8007c00}
|
||||
a64InstrTable["STXRB"] = a64Enc{format: a64FExcl, op: 0x08007c00}
|
||||
a64InstrTable["STXRH"] = a64Enc{format: a64FExcl, op: 0x48007c00}
|
||||
@@ -591,6 +605,12 @@ func init() {
|
||||
a64InstrTable["STLXRB"] = a64Enc{format: a64FExcl, op: 0x0800fc00}
|
||||
a64InstrTable["STLXRH"] = a64Enc{format: a64FExcl, op: 0x4800fc00}
|
||||
a64InstrTable["STLXRW"] = a64Enc{format: a64FExcl, op: 0x8800fc00}
|
||||
// Pair stores, LDSTX(sz, 0, l=0, o1=1, o0): STXP/STXPW have o0=0,
|
||||
// STLXP/STLXPW o0=1 (bit 15). Both Rs and Rt2 are real fields.
|
||||
a64InstrTable["STXP"] = a64Enc{format: a64FExcl, op: 0xc8200000}
|
||||
a64InstrTable["STXPW"] = a64Enc{format: a64FExcl, op: 0x88200000}
|
||||
a64InstrTable["STLXP"] = a64Enc{format: a64FExcl, op: 0xc8208000}
|
||||
a64InstrTable["STLXPW"] = a64Enc{format: a64FExcl, op: 0x88208000}
|
||||
|
||||
// ---- LSE atomics ----
|
||||
a64InstrTable["LDADDD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x1c1<<21 | 0x00<<10}
|
||||
|
||||
@@ -820,15 +820,28 @@ func TestArm64ExclOffsetErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64ExclNoOffset pins the plain (Rn) forms. gasm parses the store
|
||||
// with the status register first (ARM ARM order); go tool asm parses the
|
||||
// same text with the data register first, so the two spellings differ and
|
||||
// the store word below is gasm's own.
|
||||
// TestArm64ExclNoOffset pins the plain (Rn) forms, byte-for-byte against
|
||||
// go tool asm. The toolchain parses the FIRST register of a store as the
|
||||
// data register and the LAST as the status register (asm7.go case 59), and
|
||||
// the pair forms as (Rt1, Rt2) (case 58/59):
|
||||
//
|
||||
// STXR R3, (R1), R4 → c8047c23 (Rt=3, Rn=1, Rs=4)
|
||||
// STXP (R3, R4), (R1), R5 → c8251023 (Rt=3, Rt2=4, Rn=1, Rs=5)
|
||||
// LDXP (R1), (R3, R4) → c87f1023 (Rn=1, Rt=3, Rt2=4)
|
||||
func TestArm64ExclNoOffset(t *testing.T) {
|
||||
got := arm64Words(t, "\tLDXR (R1), R2\n\tSTXR R3, (R1), R4\n")
|
||||
got := arm64Words(t, "\tLDXR (R1), R2\n\tSTXR R3, (R1), R4\n"+
|
||||
"\tSTXP (R3, R4), (R1), R5\n\tSTXPW (R3, R4), (R1), R5\n"+
|
||||
"\tLDXP (R1), (R3, R4)\n\tLDXPW (R1), (R3, R4)\n"+
|
||||
"\tSTXR R3, (RSP), R4\n\tLDXR (RSP), R2\n")
|
||||
want := []uint32{
|
||||
0xc85f7c22, // LDXR X2, [X1]
|
||||
0xc8037c24, // STXR W3, X4, [X1] with Rs = R3, Rt = R4
|
||||
0xc8047c23, // STXR W3, [X1], W4 with Rt = R3, Rs = R4
|
||||
0xc8251023, // STXP (R3, R4), [X1], R5
|
||||
0x88251023, // STXPW (R3, R4), [X1], R5
|
||||
0xc87f1023, // LDXP [X1], (R3, R4)
|
||||
0x887f1023, // LDXPW [X1], (R3, R4)
|
||||
0xc8047fe3, // STXR R3, [SP], R4
|
||||
0xc85f7fe2, // LDXR [SP], R2
|
||||
0xd65f03c0, // RET
|
||||
}
|
||||
for i := range want {
|
||||
@@ -920,3 +933,58 @@ func TestArm64LargeFrameSpadj(t *testing.T) {
|
||||
t.Errorf("final RET word at byte 60 = %08x, want d65f03c0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64SplitFrameSpadj pins the addcon2 band, where neither imm12 form
|
||||
// nor a single MOVZ carries the autosize and the toolchain splits the
|
||||
// prologue SUB into two imm12 instructions (asm7.go case 48) while the
|
||||
// non-leaf RET still materialises the value into REGTMP (obj7.go ARET,
|
||||
// issue 73259). $65664 rounds the autosize to 65680 = 144 + 16<<12:
|
||||
//
|
||||
// [SUB $144, RSP, R20][SUB $(16<<12), R20, R20][STP][MOVD R20, SP][SUB $8]
|
||||
// [CALL]
|
||||
// [LDP][MOVD $144, R27][MOVK $(1<<16), R27][ADD R27, RSP, RSP][RET]
|
||||
//
|
||||
// SP moves at the fourth word (byte 12) and returns to zero at the final
|
||||
// RET (byte 40); the words are go tool asm's own for the same source.
|
||||
func TestArm64SplitFrameSpadj(t *testing.T) {
|
||||
f, errs := parser.Parse("frame_arm64.s", "#include \"textflag.h\"\n\nTEXT ·framed(SB), NOSPLIT, $65664-0\n\tCALL ·other(SB)\n\tRET\n\nTEXT ·other(SB), NOSPLIT, $0\n\tRET\n")
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
wantSpadj := []SpadjStep{{PC: 12, Value: 65680}, {PC: 40, Value: 0}}
|
||||
if len(fn.Spadj) != len(wantSpadj) {
|
||||
t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj)
|
||||
}
|
||||
for i := range wantSpadj {
|
||||
if fn.Spadj[i] != wantSpadj[i] {
|
||||
t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i])
|
||||
}
|
||||
}
|
||||
want := []uint32{
|
||||
0xd10243f4, // SUB $144, RSP, R20
|
||||
0xd1404294, // SUB $(16<<12), R20, R20
|
||||
0xa93ffa9d, // STP (R29, R30), -8(R20)
|
||||
0x9100029f, // MOVD R20, RSP
|
||||
0xd10023fd, // SUB $8, RSP, R29
|
||||
0x94000000, // CALL (relocation masked at link time)
|
||||
0xa97ffbfd, // LDP -8(RSP), (R29, R30)
|
||||
0xd280121b, // MOVD $144, R27
|
||||
0xf2a0003b, // MOVK $(1<<16), R27
|
||||
0x8b3b63ff, // ADD R27, RSP, RSP
|
||||
0xd65f03c0, // RET
|
||||
}
|
||||
words := leWords(img.Code[fn.Offset : fn.Offset+fn.Size])
|
||||
if len(words) != len(want) {
|
||||
t.Fatalf("framed = %d words, want %d", len(words), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if words[i] != w {
|
||||
t.Errorf("word %d = %08x, want %08x", i, words[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+75
-21
@@ -187,10 +187,32 @@ func arm64Prologue(fi arm64FrameInfo) []byte {
|
||||
return a64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value
|
||||
// fits the imm12 field (plain, or shifted left by 12 when it is a multiple
|
||||
// of 4096); otherwise the toolchain materialises it into REGTMP (R27) and
|
||||
// subtracts the register in the extended-register form.
|
||||
// arm64SplitImm12 reports whether the toolchain decomposes ADD/SUB $imm into
|
||||
// two imm12 instructions instead of materialising it into REGTMP
|
||||
// (asm7.go case 48, the C_ADDCON2 class): the value must fit 24 bits
|
||||
// unsigned and be neither encodable as one imm12 (checked by the callers
|
||||
// first), nor loadable into a register in a single MOVZ/MOVN word, nor a
|
||||
// logical immediate, because conclass tests all three before C_ADDCON2.
|
||||
func arm64SplitImm12(imm uint32) bool {
|
||||
if imm > 0xFFFFFF {
|
||||
return false
|
||||
}
|
||||
if _, _, _, ok := arm64Bitmask(uint64(imm), 1); ok {
|
||||
return false
|
||||
}
|
||||
return arm64Movcon(int64(imm)) < 0 && arm64Movcon(^int64(imm)) < 0
|
||||
}
|
||||
|
||||
// arm64SubImmWords emits SUB $imm, SP, Rd with the toolchain's ladder for an
|
||||
// ADD/SUB constant (asm7.go conclass and cases 2, 48, 62 and 13): the
|
||||
// immediate form when the value fits imm12 (plain, or shifted left by 12
|
||||
// when it is a multiple of 4096); a value with a single 16-bit chunk, a
|
||||
// logical immediate, or one wider than 24 bits is materialised into REGTMP
|
||||
// (R27) and subtracted in the extended-register form; everything else up to
|
||||
// 0xFFFFFF is split into two imm12 instructions:
|
||||
//
|
||||
// SUB $(imm&0xfff), SP, Rd
|
||||
// SUB $((imm&0xfff000)>>12)<<12, Rd, Rd
|
||||
func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
|
||||
if imm <= 0xFFF {
|
||||
return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)}
|
||||
@@ -198,15 +220,21 @@ func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
|
||||
if imm <= 4095<<12 && imm&0xFFF == 0 {
|
||||
return []uint32{a64AddSub(1, 1, 0, 1, imm>>12, 31, rd)}
|
||||
}
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
if !arm64SplitImm12(imm) {
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpSub, 27, 31, rd))
|
||||
}
|
||||
return []uint32{
|
||||
a64AddSub(1, 1, 0, 0, imm&0xFFF, 31, rd),
|
||||
a64AddSub(1, 1, 0, 1, (imm&0xFFF000)>>12, rd, rd),
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpSub, 27, 31, rd))
|
||||
}
|
||||
|
||||
// arm64AddImmWords emits ADD $imm, SP, Rd with the same imm12, shifted-imm12
|
||||
// and REGTMP fallback ladder.
|
||||
// arm64AddImmWords emits ADD $imm, SP, Rd with the same imm12, shifted-imm12,
|
||||
// split and REGTMP ladder as arm64SubImmWords.
|
||||
func arm64AddImmWords(imm uint32, rd uint32) []uint32 {
|
||||
if imm <= 0xFFF {
|
||||
return []uint32{a64AddSub(1, 0, 0, 0, imm, 31, rd)}
|
||||
@@ -214,11 +242,35 @@ func arm64AddImmWords(imm uint32, rd uint32) []uint32 {
|
||||
if imm <= 4095<<12 && imm&0xFFF == 0 {
|
||||
return []uint32{a64AddSub(1, 0, 0, 1, imm>>12, 31, rd)}
|
||||
}
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if !arm64SplitImm12(imm) {
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, rd))
|
||||
}
|
||||
return []uint32{
|
||||
a64AddSub(1, 0, 0, 0, imm&0xFFF, 31, rd),
|
||||
a64AddSub(1, 0, 0, 1, (imm&0xFFF000)>>12, rd, rd),
|
||||
}
|
||||
}
|
||||
|
||||
// arm64RetAddWords emits the frame deallocation of a non-leaf RET with a
|
||||
// large frame. The toolchain adds the frame back with a single instruction:
|
||||
// a plain imm12 ADD when autosize fits 12 bits, otherwise the value is
|
||||
// materialised into REGTMP and added as a register, so the epilogue never
|
||||
// leaves a partially deallocated frame (obj7.go ARET, issue 73259). The
|
||||
// shifted-imm12 and split-imm12 forms are therefore never used here, unlike
|
||||
// the leaf epilogue's plain ADD instructions.
|
||||
func arm64RetAddWords(autosize uint32) []uint32 {
|
||||
if autosize < 1<<12 {
|
||||
return []uint32{a64AddSub(1, 0, 0, 0, autosize, 31, 31)}
|
||||
}
|
||||
mov, err := encodeARM64LoadImm(27, int64(autosize), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, rd))
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, 31))
|
||||
}
|
||||
|
||||
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
|
||||
@@ -237,11 +289,11 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
||||
arm64PostLoad(3, 0, int32(fi.autosize), 31, 30), // LDR.P LR, [SP], #autosize
|
||||
)
|
||||
} else {
|
||||
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP
|
||||
// Large frame: LDP -8(SP), (FP, LR), then deallocate.
|
||||
ws = append(ws,
|
||||
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
||||
)
|
||||
ws = append(ws, arm64AddImmWords(uint32(fi.autosize), 31)...)
|
||||
ws = append(ws, arm64RetAddWords(uint32(fi.autosize))...)
|
||||
}
|
||||
}
|
||||
// RET: BR LR (0xd65f03c0)
|
||||
@@ -260,15 +312,16 @@ func arm64PrologueSpadjPC(fi arm64FrameInfo) int {
|
||||
}
|
||||
// Large frame: [SUB words][STP][ADD R20, SP]; SP moves at the ADD, whose
|
||||
// position depends on how many words the SUB itself took (immediate,
|
||||
// shifted immediate, or a materialised REGTMP sequence).
|
||||
// shifted immediate, the two-word imm12 split, or a materialised REGTMP
|
||||
// sequence).
|
||||
return 4 * (len(arm64SubImmWords(uint32(fi.autosize), 20)) + 1)
|
||||
}
|
||||
|
||||
// arm64ReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
||||
// (but not including) the final RET instruction. The ADD sequences share the
|
||||
// prologue's immediate ladder, so their length is read from the same helper
|
||||
// rather than assumed: a materialised autosize costs its MOV words plus the
|
||||
// ADD itself.
|
||||
// (but not including) the final RET instruction. The lengths are read from
|
||||
// the same word-emitting helpers the epilogue uses rather than assumed: the
|
||||
// leaf path shares the prologue's immediate ladder, and a materialised
|
||||
// autosize costs its MOV words plus the ADD itself.
|
||||
func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
@@ -280,8 +333,9 @@ func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 8 // LDR + LDR.P
|
||||
}
|
||||
// LDP + the ADD ladder that deallocates the frame.
|
||||
return 4 + 4*len(arm64AddImmWords(uint32(fi.autosize), 31))
|
||||
// LDP + the deallocation emitted by arm64RetAddWords, so the length
|
||||
// tracks whatever the MOVD ladder needs.
|
||||
return 4 + 4*len(arm64RetAddWords(uint32(fi.autosize)))
|
||||
}
|
||||
|
||||
// arm64ResolvePseudo translates a pseudo-register memory reference into a
|
||||
|
||||
@@ -86,9 +86,16 @@ func Encodable(mnemonic string) bool {
|
||||
"BSWAP",
|
||||
"PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2",
|
||||
"MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||
"MOVBWZX", "MOVBWSX", "MOVBLSX", "MOVBQSX", "MOVWQSX", "MOVLQZX",
|
||||
"CVTSL2SD", "CVTSQ2SD",
|
||||
"MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
return true
|
||||
}
|
||||
// Full-name dispatches the size split would eat (a trailing width
|
||||
// letter that is part of the mnemonic).
|
||||
switch upper {
|
||||
case "PMOVMSKB":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+7
-1
@@ -101,6 +101,11 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
if m, ok := sseBinTable[base]; ok {
|
||||
return e.encodeSSEBin(m, ops)
|
||||
}
|
||||
// PMOVMSKB ends in a width letter the size split would eat, so it
|
||||
// dispatches on the full name like the packed binaries above.
|
||||
if upper == "PMOVMSKB" {
|
||||
return e.encodePmovmskb(upper, ops)
|
||||
}
|
||||
switch base {
|
||||
case "MOV":
|
||||
return e.encodeMov(ops, size)
|
||||
@@ -126,7 +131,8 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodeBswap(ops, size)
|
||||
case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2":
|
||||
return e.encodePrefetch(base, ops)
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX":
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||
"MOVBWZX", "MOVBWSX", "MOVBLSX", "MOVBQSX", "MOVWQSX", "MOVLQZX":
|
||||
return e.encodeMovExtend(base, ops)
|
||||
case "CVTSL2SD", "CVTSQ2SD":
|
||||
return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops)
|
||||
|
||||
@@ -357,6 +357,18 @@ func TestScalarGroundTruth(t *testing.T) {
|
||||
{"MOVBQZX AL,R8", "MOVBQZX", []Operand{AL, r8}, "4c0fb6c0", "MOVZX"},
|
||||
{"MOVWLZX AX,CX", "MOVWLZX", []Operand{AX, CX}, "0fb7c8", "MOVZX"},
|
||||
{"MOVWQZX AX,R8", "MOVWQZX", []Operand{AX, r8}, "4c0fb7c0", "MOVZX"},
|
||||
// The width pairs the toolchain accepts and GOROOT uses; bytes
|
||||
// pinned from go tool asm (see testdata/verify/widen_amd64.s).
|
||||
{"MOVBWZX (BX),R11W", "MOVBWZX", []Operand{Ptr(BX, 0, 1), Reg{idx: 11, size: 2}}, "66440fb61b", "MOVZX"},
|
||||
{"MOVBWSX (BX),R11W", "MOVBWSX", []Operand{Ptr(BX, 0, 1), Reg{idx: 11, size: 2}}, "66440fbe1b", "MOVSX"},
|
||||
{"MOVBLSX (BX),AX", "MOVBLSX", []Operand{Ptr(BX, 0, 1), AX}, "0fbe03", "MOVSX"},
|
||||
{"MOVBQSX (BX),R8", "MOVBQSX", []Operand{Ptr(BX, 0, 1), r8}, "4c0fbe03", "MOVSX"},
|
||||
{"MOVWQSX (BX),R9", "MOVWQSX", []Operand{Ptr(BX, 0, 2), r9}, "4c0fbf0b", "MOVSX"},
|
||||
// A long to quad zero-extend is a plain 32-bit move.
|
||||
{"MOVLQZX (BX),DX", "MOVLQZX", []Operand{Ptr(BX, 0, 4), DX}, "8b13", "MOV"},
|
||||
{"MOVLQZX AX,DX", "MOVLQZX", []Operand{AX, DX}, "8bd0", "MOV"},
|
||||
{"PMOVMSKB X1,AX", "PMOVMSKB", []Operand{vreg(t, "X1"), AX}, "660fd7c1", "PMOVMSKB"},
|
||||
{"PMOVMSKB X11,CX", "PMOVMSKB", []Operand{vreg(t, "X11"), CX}, "66410fd7cb", "PMOVMSKB"},
|
||||
{"CVTSL2SD R8,X13", "CVTSL2SD", []Operand{r8, vreg(t, "X13")}, "f2450f2ae8", "CVTSI2SD"},
|
||||
{"CVTSL2SD AX,X0", "CVTSL2SD", []Operand{AX, vreg(t, "X0")}, "f20f2ac0", "CVTSI2SD"},
|
||||
{"CVTSQ2SD R8,X13", "CVTSQ2SD", []Operand{r8, vreg(t, "X13")}, "f24d0f2ae8", "CVTSI2SD"},
|
||||
|
||||
+40
-13
@@ -838,15 +838,24 @@ func (e *enc) encodeBswap(ops []Operand, size int) error {
|
||||
// width. The source is narrower than the destination, so the plain size-suffix
|
||||
// convention does not apply to these names.
|
||||
var movExtendOp = map[string]struct {
|
||||
op []byte
|
||||
dst64 bool
|
||||
op []byte
|
||||
dstSize int
|
||||
}{
|
||||
"MOVBLZX": {[]byte{0x0F, 0xB6}, false}, // byte → long, zero-extend
|
||||
"MOVBQZX": {[]byte{0x0F, 0xB6}, true}, // byte → quad, zero-extend
|
||||
"MOVWLZX": {[]byte{0x0F, 0xB7}, false}, // word → long, zero-extend
|
||||
"MOVWQZX": {[]byte{0x0F, 0xB7}, true}, // word → quad, zero-extend
|
||||
"MOVWLSX": {[]byte{0x0F, 0xBF}, false}, // word → long, sign-extend
|
||||
"MOVLQSX": {[]byte{0x63}, true}, // long → quad, sign-extend (MOVSXD)
|
||||
"MOVBLZX": {[]byte{0x0F, 0xB6}, 4}, // byte → long, zero-extend
|
||||
"MOVBQZX": {[]byte{0x0F, 0xB6}, 8}, // byte → quad, zero-extend
|
||||
"MOVWLZX": {[]byte{0x0F, 0xB7}, 4}, // word → long, zero-extend
|
||||
"MOVWQZX": {[]byte{0x0F, 0xB7}, 8}, // word → quad, zero-extend
|
||||
"MOVWLSX": {[]byte{0x0F, 0xBF}, 4}, // word → long, sign-extend
|
||||
"MOVLQSX": {[]byte{0x63}, 8}, // long → quad, sign-extend (MOVSXD)
|
||||
"MOVBWZX": {[]byte{0x0F, 0xB6}, 2}, // byte → word, zero-extend
|
||||
"MOVBWSX": {[]byte{0x0F, 0xBE}, 2}, // byte → word, sign-extend
|
||||
"MOVBLSX": {[]byte{0x0F, 0xBE}, 4}, // byte → long, sign-extend
|
||||
"MOVBQSX": {[]byte{0x0F, 0xBE}, 8}, // byte → quad, sign-extend
|
||||
"MOVWQSX": {[]byte{0x0F, 0xBF}, 8}, // word → quad, sign-extend
|
||||
// A long → quad zero-extend is a plain 32-bit move: every 32-bit
|
||||
// operation zero-extends its result into the full register, so the
|
||||
// toolchain lowers MOVLQZX to the plain MOVL encoding.
|
||||
"MOVLQZX": {[]byte{0x8B}, 4},
|
||||
}
|
||||
|
||||
// encodeMovExtend encodes a mixed-width extending move: reg = dst (the wider
|
||||
@@ -860,12 +869,30 @@ func (e *enc) encodeMovExtend(base string, ops []Operand) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("%s destination must be a register", base)
|
||||
}
|
||||
size := 4
|
||||
if spec.dst64 {
|
||||
size = 8
|
||||
i := newInstr(spec.dstSize, spec.op)
|
||||
if err := setRM(i, dstReg, ops[0], spec.dstSize); err != nil {
|
||||
return err
|
||||
}
|
||||
i := newInstr(size, spec.op)
|
||||
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodePmovmskb encodes PMOVMSKB, the legacy SSE2 byte mask extract: the
|
||||
// XMM source's sign bytes pack into a GP destination, 66 0F D7 /r.
|
||||
func (e *enc) encodePmovmskb(base string, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops))
|
||||
}
|
||||
srcReg, srcVec := vecReg(ops[0])
|
||||
if !srcVec {
|
||||
return fmt.Errorf("%s source must be an XMM register", base)
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s destination must be a register", base)
|
||||
}
|
||||
i := newInstr(4, []byte{0x0F, 0xD7})
|
||||
i.prefix = 0x66
|
||||
if err := setRM(i, dstReg, srcReg, 4); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ func riscvRegNum(name string) int {
|
||||
return 25
|
||||
case "X26", "S10":
|
||||
return 26
|
||||
case "X27", "S11":
|
||||
case "X27", "S11", "g":
|
||||
return 27
|
||||
case "X28", "T3":
|
||||
return 28
|
||||
|
||||
+8
-5
@@ -40,10 +40,13 @@ func cmdAuditInstructions(args []string) error {
|
||||
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", `
|
||||
Compare the gasm encoder for the given architecture (default amd64) against
|
||||
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-
|
||||
only names (feature gaps). The Go side is probed black-box with a battery
|
||||
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||
provides.
|
||||
gasm asm --format goobj) and known-but-unencodable names (the backlog). The
|
||||
Go side is probed black-box one bare mnemonic at a time, so the audit tracks
|
||||
whatever toolchain `+"`go env GOROOT`"+` provides; the gasm side answers from
|
||||
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
|
||||
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
|
||||
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, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
|
||||
for _, n := range superset {
|
||||
|
||||
@@ -24,9 +24,10 @@ function in a traced subprocess (ptrace), then provides a REPL for
|
||||
single-stepping, breakpoints, register and memory inspection.
|
||||
|
||||
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
|
||||
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
|
||||
info break list all breakpoints
|
||||
step [n], s single-step n instructions (default 1)
|
||||
|
||||
+50
-20
@@ -486,10 +486,13 @@ func cmdAsm(args []string) int {
|
||||
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
|
||||
section into one self-consistent image; elf emits a relocatable object
|
||||
(.text/.data sections, a symbol table and one PC32 relocation per
|
||||
static-symbol reference) that links with the 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).
|
||||
(.text/.data sections, a symbol table and 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; 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")
|
||||
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>", `
|
||||
Show the basic-block structure of functions in an assembly file.
|
||||
Lists each function's labels, their offsets, and the block boundaries.
|
||||
This is the static structure; for runtime execution counts, use
|
||||
gasm verify --fuzz which exercises the code paths.
|
||||
This is the static structure; for runtime execution counts use
|
||||
gasm debug --cover, and for input coverage gasm verify --fuzz.
|
||||
`)
|
||||
flagSet.Parse(args)
|
||||
if flagSet.NArg() != 1 {
|
||||
@@ -938,11 +941,40 @@ func compareGroundTruth(img *asm.Image, gt map[string][]byte) (matched, total, d
|
||||
goCmp[j] = 0
|
||||
}
|
||||
}
|
||||
if bytes.Equal(gasmCmp, goCmp) {
|
||||
// The toolchain pads text symbols to 16-byte boundaries with
|
||||
// zeros, so a function whose size is not a multiple of 16
|
||||
// carries trailing zeros in the ground truth that are not part
|
||||
// of the encoding. Compare up to the shorter side and require
|
||||
// the remainder of whichever is longer to be zero, so padding
|
||||
// never masks a real difference.
|
||||
cmpLen := min(len(gasmCmp), len(goCmp))
|
||||
equal := bytes.Equal(gasmCmp[:cmpLen], goCmp[:cmpLen])
|
||||
if equal {
|
||||
for _, b := range gasmCmp[cmpLen:] {
|
||||
if b != 0 {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if equal {
|
||||
for _, b := range goCmp[cmpLen:] {
|
||||
if b != 0 {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if equal {
|
||||
matched++
|
||||
if len(fn.Relocs) > 0 {
|
||||
switch {
|
||||
case len(fn.Relocs) > 0 && len(goCmp) > cmpLen:
|
||||
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked, %d padding)\n", fn.Name, fn.Size, len(fn.Relocs), len(goCmp)-cmpLen)
|
||||
case len(fn.Relocs) > 0:
|
||||
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
||||
} else {
|
||||
case len(goCmp) > cmpLen:
|
||||
fmt.Printf(" %s: MATCH (%d bytes, %d padding)\n", fn.Name, fn.Size, len(goCmp)-cmpLen)
|
||||
default:
|
||||
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
||||
}
|
||||
} else {
|
||||
@@ -990,8 +1022,8 @@ that tolerate nil pointers and zero lengths in their arguments.
|
||||
With -abi, each function is called with sentinel values in the registers
|
||||
the Go ABI fixes across calls (the frame pointer and the goroutine
|
||||
pointer) plus a canary below SP; violations are reported. JIT-based
|
||||
checks run when the host matches the file's architecture (all but
|
||||
loong64, which is ground-truth only for now).
|
||||
checks run when the host matches the file's architecture, on all four
|
||||
architectures.
|
||||
|
||||
With -fuzz, each function with a // func signature is differentially fuzzed
|
||||
against the go-tool-asm version in a subprocess (so a crash on a partial
|
||||
@@ -1004,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)
|
||||
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
|
||||
written to the directory as replayable JSON. -replay re-runs saved
|
||||
@@ -1033,16 +1066,13 @@ each entry reproduces.
|
||||
path := set.Arg(0)
|
||||
targetArch := arch.FromFilename(path)
|
||||
// JIT execution runs when the host CPU matches the kernel's
|
||||
// architecture, except loong64: its trampoline is implemented but not
|
||||
// yet validated against real hardware (the Go runtime cannot start
|
||||
// under the available loong64 emulators), so those kernels take the
|
||||
// toolchain-comparison path.
|
||||
if targetArch != hostArch() || targetArch == arch.LOONG64 {
|
||||
// architecture; every trampoline is validated end to end under
|
||||
// qemu-user emulation (the loong64 one included, via the raw-address
|
||||
// leave handoff).
|
||||
if targetArch != hostArch() {
|
||||
// No JIT on this host: ground truth and profile remain available for
|
||||
// every architecture, because cmdVerifyNonJIT assembles and compares
|
||||
// against the toolchain without executing anything. (loong64 is
|
||||
// ground-truth-only everywhere for now: its trampoline is implemented
|
||||
// but not yet validated against real hardware.)
|
||||
// against the toolchain without executing anything.
|
||||
switch targetArch {
|
||||
case arch.AMD64, arch.RISCV, arch.LOONG64, arch.ARM64:
|
||||
return cmdVerifyNonJIT(path, targetArch, *groundTruth, *profile)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
)
|
||||
|
||||
const clean = "#include \"textflag.h\"\n" +
|
||||
@@ -440,3 +441,22 @@ func TestRunCorpusAudit(t *testing.T) {
|
||||
t.Errorf("arm64 unencodable reasons = %d, want 1", r)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompareGroundTruthPadding pins the padding-aware ground-truth
|
||||
// comparison: the toolchain pads text symbols to 16-byte boundaries, so
|
||||
// trailing zeros in the reference must not read as a mismatch, while any
|
||||
// non-zero tail still must.
|
||||
func TestCompareGroundTruthPadding(t *testing.T) {
|
||||
code := []byte{0x48, 0x8b, 0x07, 0xc3} // 4 bytes, not a multiple of 16
|
||||
img := &asm.Image{Code: code, Funcs: []asm.FuncLayout{{Name: "f", Offset: 0, Size: len(code)}}}
|
||||
padded := append(append([]byte(nil), code...), 0, 0, 0)
|
||||
matched, total, diffs := compareGroundTruth(img, map[string][]byte{"f": padded})
|
||||
if matched != 1 || total != 1 || diffs != 0 {
|
||||
t.Fatalf("zero padding should match: matched=%d total=%d diffs=%d", matched, total, diffs)
|
||||
}
|
||||
dirty := append(append([]byte(nil), code...), 0, 0x90, 0)
|
||||
matched, _, diffs = compareGroundTruth(img, map[string][]byte{"f": dirty})
|
||||
if matched != 0 || diffs != 1 {
|
||||
t.Fatalf("non-zero padding must mismatch: matched=%d diffs=%d", matched, diffs)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
entryAddr := codeBase + uint64(funcOffset)
|
||||
|
||||
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)
|
||||
|
||||
@@ -233,7 +233,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
|
||||
case "break", "b":
|
||||
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
|
||||
}
|
||||
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" {
|
||||
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
|
||||
}
|
||||
bp, err := bm.SetWithCond(addr, label, cond)
|
||||
@@ -422,7 +422,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println()
|
||||
|
||||
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
|
||||
register compared to a constant, a register, or the
|
||||
8-byte word at *addr
|
||||
|
||||
+70
-42
@@ -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
|
||||
produce a typed AST with source positions on every node.
|
||||
2. **Architecture as data, not code.** Per-architecture differences (amd64,
|
||||
arm64, riscv64, loong64) live in register and instruction *tables* (`arch`),
|
||||
never in `if arch == …` branches scattered through the logic. The
|
||||
instruction tables are generated from the Go toolchain's own assembler
|
||||
source (`just gen`), so adding or refreshing an architecture is a data
|
||||
operation, not a coding one.
|
||||
arm64, riscv64, loong64) live in register and instruction *tables* (`arch`)
|
||||
and per-architecture encoders, rather than in `if arch == …` branches
|
||||
threaded through the analysis; the arch tests that remain are dispatch and
|
||||
policy points, such as which encoder a file name selects and which
|
||||
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
|
||||
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.
|
||||
@@ -35,10 +37,19 @@ flowchart TD
|
||||
ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT
|
||||
ARCH --> 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"]
|
||||
LINT --> CLI
|
||||
PAR --> CLI
|
||||
LEX --> CLI
|
||||
ASM --> CLI
|
||||
VER --> CLI
|
||||
DBG --> CLI
|
||||
DIS --> CLI
|
||||
LSP --> EDITOR["any LSP editor"]
|
||||
```
|
||||
|
||||
@@ -70,9 +81,11 @@ assembler provides.
|
||||
|
||||
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
|
||||
parser stays architecture-agnostic. `asm` and `verify` are the only packages
|
||||
that touch machine code and executable memory, and `cmd/gasm` owns no logic
|
||||
beyond flags and output.
|
||||
parser stays architecture-agnostic. `asm` produces the machine code, `verify`
|
||||
and `debug` are the two packages that map it executable (read-execute in
|
||||
`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`
|
||||
|
||||
@@ -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
|
||||
named registers listed explicitly. Instruction names are **generated from the
|
||||
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
|
||||
arm64 `B`/`BL` branches and the `.P`/`.W` load-store addressing suffixes) by
|
||||
`just gen`, so the tables always match what the real assembler accepts. Each
|
||||
mnemonic maps to a summary and an optional operand-count range; counts are
|
||||
recorded only where unambiguous (`-1` 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) only a few
|
||||
fixed-arity instructions (`RET`, `NOP`, `JMP`, `CALL`) carry counts at all.
|
||||
plus the common opcodes in `cmd/internal/obj/util.go`) by `just gen`, so the
|
||||
tables always match what the real assembler accepts. The spellings the
|
||||
toolchain's tables do not carry are hand-maintained instead: the front-end
|
||||
alias lists in `arch/arm64.go`, `arch/amd64.go` and `arch/loong64.go` (the
|
||||
arm64 `B`/`BL` branches among them), and the arm64 `.P`/`.W` load-store suffix
|
||||
stripping in `arch/arch.go`. Each mnemonic maps to a summary and an optional
|
||||
operand-count range; counts are recorded only where unambiguous (`-1`
|
||||
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`
|
||||
|
||||
@@ -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
|
||||
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
|
||||
registers) across eight operand forms: the three-operand NDS form, the
|
||||
two-operand reg/rm form, the immediate-shift form (plus the variable-count
|
||||
shifts, which share the NDS shape with the count in an XMM register or
|
||||
memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the
|
||||
three-operand-plus-immediate form (`VSHUFPD`,
|
||||
registers) over nine operand forms plus a dedicated move encoder: the
|
||||
three-operand NDS form, the two-operand reg/rm form, the immediate-shift form
|
||||
(plus the variable-count shifts, which share the NDS shape with the count in
|
||||
an XMM register or memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`),
|
||||
the three-operand-plus-immediate form (`VSHUFPD`,
|
||||
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
|
||||
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
|
||||
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
|
||||
suffix. Every encoding is validated two ways: by
|
||||
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
|
||||
whole functions: all 27 functions of both kernels assemble to exactly the Go
|
||||
toolchain's bytes, the lone exception being the displacements of the
|
||||
static-constant loads, which the Go linker fills at link time.
|
||||
the machine code the real Go assembler emits; the parity suites carry that
|
||||
comparison over whole kernel files on all four architectures, with the
|
||||
relocation fields masked because the Go linker fills those displacements at
|
||||
link time.
|
||||
|
||||
File-level assembly (`AssembleFile`) goes beyond single functions: it
|
||||
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
|
||||
@@ -410,9 +426,9 @@ every architecture too: `enterJITChecked` plants sentinels in the registers
|
||||
the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
|
||||
`X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
|
||||
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
||||
saved registers before Go code resumes. riscv64 is validated end to
|
||||
end under qemu-user emulation; arm64 shares the same stack convention and
|
||||
fix; loong64 stays ground-truth-only until hardware validation.
|
||||
saved registers before Go code resumes. All three non-amd64 trampolines
|
||||
are validated end to end under qemu-user emulation, the loong64 one
|
||||
through its raw-address leave handoff.
|
||||
`gasm verify` runs the JIT checks when the host
|
||||
matches the kernel's architecture and the toolchain comparisons
|
||||
elsewhere.
|
||||
@@ -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
|
||||
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
|
||||
faults is reported without ending the sweep. `gasm verify --fuzz` combines
|
||||
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz
|
||||
testing, comparing the JIT-assembled kernel against the portable Go reference
|
||||
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz
|
||||
faults is reported without ending the sweep; `-abi` is where the ABI check
|
||||
lives, fuzzing each function with sentinel values in the registers the Go ABI
|
||||
fixes across calls and a canary below `SP`, and reporting a violation on any
|
||||
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
|
||||
for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
|
||||
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
|
||||
function in a child process that maps the JIT code, calls
|
||||
`PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls
|
||||
execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem`
|
||||
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
||||
execution. Breakpoints are patched through `/proc/pid/mem`: the one-byte
|
||||
`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`
|
||||
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
|
||||
(`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint
|
||||
management. Breakpoints accept conditions
|
||||
(`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
|
||||
stdin) and exits, `--timeout` kills the debuggee when a run hangs (the
|
||||
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
|
||||
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
|
||||
and returns a non-zero exit code. The formatter and the linter take the same
|
||||
AST by a different route: `gasm fmt` re-spaces the token stream and `gasm lint`
|
||||
walks the parsed file, so neither depends on an encoding.
|
||||
and returns a non-zero exit code. The formatter and the linter take different
|
||||
inputs from the assembler: `gasm fmt` re-spaces the token stream
|
||||
(`format.Source` lexes the source text itself) and `gasm lint` walks the parsed
|
||||
AST, so neither depends on an encoding.
|
||||
|
||||
## State and lifetime
|
||||
|
||||
@@ -522,9 +551,8 @@ walks the parsed file, so neither depends on an encoding.
|
||||
## Dependencies
|
||||
|
||||
- **`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
|
||||
of the register metadata the encoder consults (`asm/reg.go`, `asm/vex.go`).
|
||||
The tests additionally decode through it to validate the encodings.
|
||||
disassembler backend (`gasm dis` and the debugger's listings). The tests
|
||||
additionally decode through it to validate the encodings.
|
||||
- **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
|
||||
--ground-truth`, `go list -json -export` locates the archives of the packages
|
||||
|
||||
+41
-27
@@ -3,9 +3,11 @@
|
||||
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.
|
||||
|
||||
The same reference is installed as man pages: `just install-man` puts gasm(1) and one
|
||||
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each
|
||||
page against the binary so the two cannot drift apart.
|
||||
The same reference is installed as man pages: `just install-man` puts gasm(1) and a page
|
||||
for every command except `version` (which gasm(1) itself documents) into
|
||||
~/.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
|
||||
|
||||
@@ -58,7 +60,8 @@ Usage: gasm parse <file>
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
self-consistent image; `elf` emits a relocatable object that links with the
|
||||
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
|
||||
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 |
|
||||
| `--profile` | off | list the basic-block structure per function |
|
||||
| `--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]` |
|
||||
| `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
|
||||
| `--repeat` | 1 | number of times to repeat a `--call` invocation |
|
||||
@@ -219,8 +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
|
||||
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`
|
||||
invokes such a function with valid data. loong64 stays on the ground-truth path
|
||||
until hardware validation.
|
||||
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
|
||||
gasm verify --ground-truth hello_amd64.s
|
||||
@@ -263,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 |
|
||||
| `-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
|
||||
it first with `just install`; `go run` does not work for the traced child.
|
||||
Requires Linux (ptrace) and all four architectures are supported.
|
||||
The debugger re-executes the binary it is running as (`os.Executable()`) for the
|
||||
traced child, so the child is the same `gasm`, whether it is installed on `$PATH`
|
||||
or run with `go run ./cmd/gasm`; nothing has to be installed first. Requires
|
||||
Linux (ptrace), and all four architectures are supported.
|
||||
|
||||
REPL commands:
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `break <label\|addr> [if <reg> <op> <val>]` | set a breakpoint, optionally conditional |
|
||||
| `delete <label\|addr>` | remove a breakpoint |
|
||||
| `info break` | list the breakpoints |
|
||||
| `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>`, `d` | remove a breakpoint |
|
||||
| `info break`, `info breakpoints`, `info b` | list the breakpoints |
|
||||
| `step [n]`, `s` | single-step n instructions |
|
||||
| `next`, `n` | step over a CALL |
|
||||
| `finish`, `fin` | run until the function returns |
|
||||
@@ -347,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
|
||||
installed `go tool asm` and print the diff: superset encodings (gasm-only
|
||||
spellings, shippable via `gasm asm --format goobj`), known-but-unencodable
|
||||
names (the encoder backlog) and go-only names (feature gaps). The Go side is
|
||||
probed black-box with a battery of operand shapes per mnemonic, so the audit
|
||||
tracks whatever toolchain `go env GOROOT` provides. On non-amd64
|
||||
architectures the backlog is 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.
|
||||
spellings, shippable via `gasm asm --format goobj`) and known-but-unencodable
|
||||
names (the encoder backlog). The Go side is probed black-box one bare mnemonic
|
||||
at a time, classified by the toolchain's diagnostic for an instruction it does
|
||||
not know, so the audit tracks whatever toolchain `go env GOROOT` provides; the
|
||||
gasm side answers from the encoder table on amd64 and from trial assembly over a
|
||||
battery of operand shapes on the other architectures. On non-amd64
|
||||
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
|
||||
gasm audit-instructions amd64
|
||||
@@ -361,8 +372,9 @@ gasm audit-instructions amd64
|
||||
|
||||
```text
|
||||
gasm table (amd64, families excluded): 1542 mnemonics
|
||||
gasm encodable: 580 go tool asm recognized: 1542
|
||||
shared: 580
|
||||
gasm encodable: 587 go tool asm recognised: 1542
|
||||
shared: 587
|
||||
...
|
||||
```
|
||||
|
||||
With `--corpus` the audit changes shape: it assembles every `.s` file under
|
||||
@@ -382,10 +394,12 @@ gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
|
||||
|
||||
```text
|
||||
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
|
||||
assemble for every target architecture: 108 (17.2%)
|
||||
amd64: 77/464 attempted
|
||||
148 instruction not encodable
|
||||
assemble for every target architecture: 127 (20.3%)
|
||||
amd64: 82/464 attempted
|
||||
165 unsupported operand form
|
||||
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
|
||||
...
|
||||
```
|
||||
|
||||
@@ -469,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:
|
||||
|
||||
```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
@@ -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
|
||||
- **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
|
||||
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
|
||||
|
||||
@@ -25,8 +31,9 @@ Every recipe in the `justfile`, and 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 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 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 |
|
||||
@@ -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 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 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 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 |
|
||||
@@ -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
|
||||
counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser,
|
||||
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
|
||||
the coverage total under the floor. The floor fails if the total is
|
||||
below 80 %. CI runs the same command with the same ten-minute bound, so
|
||||
glue, so both sit outside the profile sweep, and a thin `cmd/` in it
|
||||
would drag the coverage total under the floor. Their tests still run, in
|
||||
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.
|
||||
|
||||
### `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`,
|
||||
which triggers the release workflow: it builds the portable Linux targets,
|
||||
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
|
||||
toolchain recorded in the build information: the tag on a tagged
|
||||
|
||||
+17
-6
@@ -20,13 +20,24 @@ flag selects what is written:
|
||||
self-consistent image;
|
||||
.B elf
|
||||
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;
|
||||
.B goobj
|
||||
emits the Go toolchain's own object format, which cmd/link consumes
|
||||
directly (it requires
|
||||
.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
|
||||
Framed functions receive the stack-split guard and the trailing
|
||||
morestack block, byte-identical to the toolchain's output, so split
|
||||
@@ -51,10 +62,10 @@ Exits 0 on success, 1 when parsing or assembly fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
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 goobj \-p pkg/path \-o k.o k.s Go object for go build
|
||||
gasm asm \-GOARCH amd64 cpu_x86.s arch override
|
||||
gasm asm \-o hello.bin hello_amd64.s raw image
|
||||
gasm asm \-\-format elf \-o k.o k_amd64.s linkable ELF object
|
||||
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
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
|
||||
@@ -8,12 +8,17 @@ Compare the gasm encoder for the given architecture (default amd64)
|
||||
against
|
||||
.B go tool asm
|
||||
and print the diff: superset encodings (gasm-only, shippable via
|
||||
.BR "gasm asm \-\-format goobj" ),
|
||||
known-but-unencodable names (the backlog) and go-only names (feature
|
||||
gaps). The Go side is probed black-box with a battery of bare
|
||||
mnemonics, so the audit tracks whatever toolchain
|
||||
.BR "gasm asm \-\-format goobj" )
|
||||
and known-but-unencodable names (the backlog). The Go side is probed
|
||||
black-box one bare mnemonic at a time, so the audit tracks whatever
|
||||
toolchain
|
||||
.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
|
||||
With
|
||||
.BR \-\-corpus ,
|
||||
|
||||
@@ -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.
|
||||
.SH REPL COMMANDS
|
||||
.TP
|
||||
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR]
|
||||
Set a breakpoint, optionally conditional on a register comparison
|
||||
(register against register or immediate).
|
||||
.B break \fIlabel|addr|line\fR [\fBif \fIreg op val|reg|*addr\fR], b
|
||||
Set a breakpoint at a label, an address or a source line number, optionally
|
||||
conditional on a register comparison: against a constant, against another
|
||||
register, or against the 8-byte word at
|
||||
.BR *addr .
|
||||
.TP
|
||||
.B delete \fIlabel|addr\fR
|
||||
.B delete \fIlabel|addr\fR, d
|
||||
Remove a breakpoint.
|
||||
.TP
|
||||
.B info break
|
||||
.B info break, info breakpoints, info b
|
||||
List all breakpoints.
|
||||
.TP
|
||||
.BR step " [" n ], " s
|
||||
|
||||
@@ -28,7 +28,7 @@ differs; a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
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
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.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
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
|
||||
@@ -41,6 +41,13 @@ List files whose formatting differs from gasm's.
|
||||
.TP
|
||||
.B \-w
|
||||
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
|
||||
.nf
|
||||
gasm fmt reformat every .s below here
|
||||
|
||||
+17
-5
@@ -33,7 +33,11 @@ The function can fall off its end without a terminator.
|
||||
TEXT flags are used without including textflag.h.
|
||||
.TP
|
||||
.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
|
||||
signature.
|
||||
.TP
|
||||
@@ -52,7 +56,8 @@ FUNCDATA and PCDATA indices are malformed.
|
||||
A label no jump reaches.
|
||||
.TP
|
||||
.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
|
||||
.B stack-imbalance
|
||||
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.
|
||||
.TP
|
||||
.B abi0-register-args
|
||||
A call passes arguments in registers where ABI0 expects the stack
|
||||
frame.
|
||||
A function whose
|
||||
.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
|
||||
.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
|
||||
.B unencodable-instruction
|
||||
The mnemonic is known to the table but the encoder cannot assemble it
|
||||
|
||||
@@ -6,9 +6,10 @@ gasm-profile \- show the basic-block structure of functions
|
||||
.SH DESCRIPTION
|
||||
Show the basic-block structure of functions in an assembly file: each
|
||||
function's labels, their offsets, and the block boundaries. This is
|
||||
the static structure; for runtime execution counts, use
|
||||
.BR "gasm verify \-fuzz" ,
|
||||
which exercises the code paths.
|
||||
the static structure; for runtime execution counts use
|
||||
.BR "gasm debug \-\-cover" ,
|
||||
and for input coverage
|
||||
.BR "gasm verify \-\-fuzz" .
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success and 1 when the file cannot be assembled.
|
||||
.SH SEE ALSO
|
||||
|
||||
@@ -20,8 +20,7 @@ With
|
||||
each function is called with sentinel values in the registers the Go
|
||||
ABI fixes across calls (the frame pointer and the goroutine pointer)
|
||||
plus a canary below SP; violations are reported. JIT-based checks run
|
||||
when the host matches the file's architecture (all but loong64, which
|
||||
is ground-truth only for now).
|
||||
when the host matches the file's architecture, on all four architectures.
|
||||
.PP
|
||||
With
|
||||
.BR \-fuzz ,
|
||||
@@ -46,7 +45,8 @@ a single function is invoked with user-supplied buffers
|
||||
.RB ( \-buf )
|
||||
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.
|
||||
data. The function named must be NOSPLIT: a function with a stack frame
|
||||
is refused with a diagnostic and exits 1.
|
||||
.PP
|
||||
With
|
||||
.B \-save\-corpus
|
||||
@@ -74,7 +74,7 @@ Buffer spec for -call: name:size:pattern[,name:size:pattern] where
|
||||
pattern is zero, ones, seq, or hex.
|
||||
.TP
|
||||
.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
|
||||
.B \-fuzz
|
||||
Differential fuzz: JIT both the gasm and the go-tool-asm versions and
|
||||
@@ -108,8 +108,8 @@ a file that cannot be assembled exits 1 and a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
|
||||
gasm verify \-\-ground\-truth k.s
|
||||
gasm verify \-\-fuzz \-n 500 k.s
|
||||
gasm verify \-\-ground\-truth k_amd64.s
|
||||
gasm verify \-\-fuzz \-n 500 k_amd64.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
|
||||
+8
-4
@@ -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
|
||||
serves two purposes: it brings developer tooling to the
|
||||
.I .s
|
||||
files of Go programs, and it assembles Plan 9 assembly without the Go
|
||||
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug
|
||||
sections, or the Go toolchain's own GOOBJ format, which
|
||||
files of Go programs, and it assembles Plan 9 assembly to raw images or
|
||||
linkable ELF objects with DWARF5 debug sections without the Go toolchain at
|
||||
all, plus the Go toolchain's own GOOBJ format, which
|
||||
.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
|
||||
Four architectures are covered: amd64 (including VEX/AVX2 and
|
||||
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
|
||||
|
||||
@@ -31,6 +31,13 @@ test:
|
||||
system(q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m},
|
||||
q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0
|
||||
or die qq{the test suite failed\n};
|
||||
# The packages outside the coverage set carry tests of their own: the CLI's
|
||||
# exit codes and manual-page guard, and the debugger's architecture-neutral
|
||||
# units. They run without a profile, because a thin main and a ptrace-bound
|
||||
# package would drag the floor down rather than measure the product.
|
||||
system(q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m},
|
||||
q{./cmd/...}, q{./debug/...}) == 0
|
||||
or die qq{the tests outside the coverage set failed\n};
|
||||
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
|
||||
my $total;
|
||||
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
|
||||
|
||||
Vendored
+2
-2
@@ -22,9 +22,9 @@ TEXT ·dirtyFP(SB), NOSPLIT, $0-16
|
||||
RET
|
||||
|
||||
// func dirtyG(a int64) int64
|
||||
// Deliberately clobbers R28, the goroutine pointer (a serious ABI violation).
|
||||
// Deliberately clobbers g, the goroutine pointer (R28; a serious ABI violation).
|
||||
TEXT ·dirtyG(SB), NOSPLIT, $0-16
|
||||
MOVD $0x5678, R28
|
||||
MOVD $0x5678, g
|
||||
MOVD a+0(FP), R0
|
||||
MOVD R0, ret+8(FP)
|
||||
RET
|
||||
|
||||
Vendored
+2
-2
@@ -13,9 +13,9 @@ TEXT ·cleanAdd(SB), NOSPLIT, $0-24
|
||||
RET
|
||||
|
||||
// func dirtyG(a int64) int64
|
||||
// Deliberately clobbers R22, the goroutine pointer (a serious ABI violation).
|
||||
// Deliberately clobbers g, the goroutine pointer (R22; a serious ABI violation).
|
||||
TEXT ·dirtyG(SB), NOSPLIT, $0-16
|
||||
MOVV $0x5678, R22
|
||||
MOVV $0x5678, g
|
||||
MOVV a+0(FP), R4
|
||||
MOVV R4, ret+8(FP)
|
||||
RET
|
||||
|
||||
Vendored
+2
-2
@@ -13,9 +13,9 @@ TEXT ·cleanAdd(SB), NOSPLIT, $0-24
|
||||
RET
|
||||
|
||||
// func dirtyG(a int64) int64
|
||||
// Deliberately clobbers X27, the goroutine pointer (a serious ABI violation).
|
||||
// Deliberately clobbers g, the goroutine pointer (X27; a serious ABI violation).
|
||||
TEXT ·dirtyG(SB), NOSPLIT, $0-16
|
||||
MOV $0x5678, X27
|
||||
MOV $0x5678, g
|
||||
MOV a+0(FP), X5
|
||||
MOV X5, ret+8(FP)
|
||||
RET
|
||||
|
||||
Vendored
+8
-2
@@ -37,7 +37,10 @@ TEXT ·imm(SB), NOSPLIT, $0-0
|
||||
MOVV $0x12345, R17
|
||||
RET
|
||||
|
||||
// branch exercises conditional and unconditional control flow.
|
||||
// branch exercises conditional and unconditional control flow. Every
|
||||
// path must terminate: the smoke harness calls functions with a zeroed
|
||||
// argument block, and a $0-0 function's registers carry whatever the
|
||||
// caller left, so a branch maze can reach any label.
|
||||
TEXT ·branch(SB), NOSPLIT, $0-0
|
||||
BEQ R4, R5, done
|
||||
BNE R6, R7, skip
|
||||
@@ -50,7 +53,10 @@ skip:
|
||||
JMP loop
|
||||
|
||||
loop:
|
||||
JAL skip
|
||||
JAL fin
|
||||
RET
|
||||
|
||||
fin:
|
||||
RET
|
||||
|
||||
done:
|
||||
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Large frames across every immediate band of the prologue SUB and the RET
|
||||
// epilogue, byte-parity-checked against go tool asm:
|
||||
//
|
||||
// $5000 autosize 5024 one 16-bit chunk, materialised into REGTMP
|
||||
// $65664 autosize 65680 split into two imm12 instructions
|
||||
// $70000 autosize 70016 split into two imm12 instructions
|
||||
// $65520 autosize 65536 one shifted imm12 in the prologue, a logical
|
||||
// immediate (ORR) in the non-leaf epilogue
|
||||
// $16777232 autosize 16777248 wider than 24 bits, MOVZ/MOVK into REGTMP
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·leaf5000(SB), NOSPLIT, $5000-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
RET
|
||||
|
||||
TEXT ·leaf65664(SB), NOSPLIT, $65664-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
RET
|
||||
|
||||
TEXT ·leaf70000(SB), NOSPLIT, $70000-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
RET
|
||||
|
||||
TEXT ·leaf65520(SB), NOSPLIT, $65520-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
MOVD R2, R3
|
||||
MOVD R3, R4
|
||||
RET
|
||||
|
||||
TEXT ·nl5000(SB), NOSPLIT, $5000-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·nl65664(SB), NOSPLIT, $65664-0
|
||||
MOVD R0, R1
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·nl70000(SB), NOSPLIT, $70000-0
|
||||
MOVD R0, R1
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·nl65520(SB), NOSPLIT, $65520-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
MOVD R2, R3
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·nlhuge(SB), NOSPLIT, $16777232-0
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·other(SB), NOSPLIT, $0-0
|
||||
MOVD R0, R1
|
||||
MOVD R1, R2
|
||||
MOVD R2, R3
|
||||
RET
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Exclusive load/store family, LSE atomics and their memory operands,
|
||||
// byte-parity-checked against go tool asm. The toolchain parses the FIRST
|
||||
// register of an exclusive store as the data register and the LAST as the
|
||||
// status register, and takes register pairs as (Rt1, Rt2) operands.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·loads(SB), NOSPLIT, $0-0
|
||||
LDXR (R1), R2
|
||||
LDXRB (R1), R3
|
||||
LDXRH (R1), R4
|
||||
LDXRW (R1), R5
|
||||
LDAXR (R1), R2
|
||||
LDAXRB (R1), R3
|
||||
LDAXRH (R1), R4
|
||||
LDAXRW (R1), R5
|
||||
LDXR (RSP), R2
|
||||
LDXRB (RSP), R3
|
||||
LDXRH (RSP), R4
|
||||
LDXRW (RSP), R5
|
||||
LDAXR (RSP), R2
|
||||
LDAXRB (RSP), R3
|
||||
LDAXRH (RSP), R4
|
||||
RET
|
||||
|
||||
TEXT ·stores(SB), NOSPLIT, $0-0
|
||||
STXR R2, (R1), R6
|
||||
STXRB R3, (R1), R6
|
||||
STXRH R4, (R1), R6
|
||||
STXRW R5, (R1), R6
|
||||
STLXR R2, (R1), R6
|
||||
STLXRB R3, (R1), R6
|
||||
STLXRH R4, (R1), R6
|
||||
STLXRW R5, (R1), R6
|
||||
STXR R2, (RSP), R6
|
||||
STXRB R3, (RSP), R6
|
||||
STXRH R4, (RSP), R6
|
||||
STXRW R5, (RSP), R6
|
||||
STLXR R2, (RSP), R6
|
||||
STLXRB R3, (RSP), R6
|
||||
STLXRH R4, (RSP), R6
|
||||
RET
|
||||
|
||||
TEXT ·pairs(SB), NOSPLIT, $0-0
|
||||
LDXP (R1), (R2, R3)
|
||||
LDXPW (R1), (R2, R3)
|
||||
LDAXP (R1), (R2, R3)
|
||||
LDAXPW (R1), (R2, R3)
|
||||
STXP (R2, R3), (R1), R6
|
||||
STXPW (R2, R3), (R1), R6
|
||||
STLXP (R2, R3), (R1), R6
|
||||
STLXPW (R2, R3), (R1), R6
|
||||
LDXP (RSP), (R2, R3)
|
||||
LDXPW (RSP), (R2, R3)
|
||||
LDAXP (RSP), (R4, R5)
|
||||
LDAXPW (RSP), (R4, R5)
|
||||
STXP (R2, R3), (RSP), R6
|
||||
STXPW (R2, R3), (RSP), R6
|
||||
STLXP (R4, R5), (RSP), R7
|
||||
RET
|
||||
|
||||
TEXT ·atomics(SB), NOSPLIT, $0-0
|
||||
LDADDB R2, (R1), R3
|
||||
LDADDH R2, (R1), R3
|
||||
LDADDW R2, (R1), R3
|
||||
LDADDD R2, (R1), R3
|
||||
LDADDB R2, (R1), ZR
|
||||
LDADDH R2, (R1), ZR
|
||||
LDADDW R2, (R1), ZR
|
||||
LDADDD R2, (R1), ZR
|
||||
CASW R2, (R1), R3
|
||||
CASD R2, (R1), R3
|
||||
CASW R2, (R1), ZR
|
||||
CASD R2, (R1), ZR
|
||||
SWPW R2, (R1), R3
|
||||
SWPD R2, (R1), R3
|
||||
SWPD R2, (R1), ZR
|
||||
RET
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
// Mixed-width sign- and zero-extending moves plus PMOVMSKB, the spellings
|
||||
// GOROOT's runtime and bytealg kernels use. Every result is folded back so
|
||||
// no instruction is dead.
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func widen(p *byte) uint64
|
||||
TEXT ·widen(SB), NOSPLIT, $0-16
|
||||
MOVBQZX 0(DI), AX
|
||||
MOVWQZX 2(DI), CX
|
||||
ADDQ CX, AX
|
||||
MOVLQZX 4(DI), DX
|
||||
ADDQ DX, AX
|
||||
MOVBQSX 8(DI), R8
|
||||
ADDQ R8, AX
|
||||
MOVWQSX 12(DI), R9
|
||||
ADDQ R9, AX
|
||||
MOVBLSX 16(DI), R10
|
||||
ADDL R10, AX
|
||||
MOVLQSX 20(DI), R11
|
||||
ADDQ R11, AX
|
||||
MOVQ AX, ret+8(FP)
|
||||
RET
|
||||
|
||||
// func widenw(p *byte) int32
|
||||
TEXT ·widenw(SB), NOSPLIT, $0-16
|
||||
MOVBWZX 0(DI), AX
|
||||
MOVBWSX 1(DI), CX
|
||||
ADDL CX, AX
|
||||
MOVLQZX AX, DX
|
||||
MOVL DX, ret+8(FP)
|
||||
RET
|
||||
|
||||
// func mask(x *XMM) int
|
||||
TEXT ·mask(SB), NOSPLIT, $0-16
|
||||
MOVOU 0(DI), X1
|
||||
PMOVMSKB X1, AX
|
||||
MOVQ AX, ret+8(FP)
|
||||
RET
|
||||
+14
-4
@@ -42,8 +42,12 @@ func TestABIArm64(t *testing.T) {
|
||||
t.Errorf("cleanAdd: %s", report)
|
||||
}
|
||||
|
||||
// dirtyFP clobbers the frame pointer (R29).
|
||||
out, report, err = k.CallFuncChecked("dirtyFP", make([]byte, 16))
|
||||
// dirtyFP clobbers the frame pointer (R29). The kernel passes its
|
||||
// argument through, so the argument must carry the expected value the
|
||||
// way the amd64 twin test seeds it.
|
||||
args = make([]byte, 16)
|
||||
PutUint64(args, 0, 0x1234)
|
||||
out, report, err = k.CallFuncChecked("dirtyFP", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFuncChecked: %v", err)
|
||||
}
|
||||
@@ -57,11 +61,17 @@ func TestABIArm64(t *testing.T) {
|
||||
t.Errorf("dirtyFP: only R29 should be clobbered: %s", report)
|
||||
}
|
||||
|
||||
// dirtyG clobbers the goroutine pointer (R28).
|
||||
_, report, err = k.CallFuncChecked("dirtyG", make([]byte, 16))
|
||||
// dirtyG clobbers the goroutine pointer (R28) and still returns its
|
||||
// argument.
|
||||
args = make([]byte, 16)
|
||||
PutUint64(args, 0, 0x5678)
|
||||
out, report, err = k.CallFuncChecked("dirtyG", args)
|
||||
if err != nil {
|
||||
t.Fatalf("CallFuncChecked: %v", err)
|
||||
}
|
||||
if got := int64(GetUint64(out, 8)); got != 0x5678 {
|
||||
t.Errorf("dirtyG returned %d, want %d", got, int64(0x5678))
|
||||
}
|
||||
if !report.GClobbered {
|
||||
t.Error("dirtyG: expected g clobbered, but report says clean")
|
||||
}
|
||||
|
||||
+7
-4
@@ -2,13 +2,16 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// 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,
|
||||
// runtime ABI checks and basic-block coverage profiling.
|
||||
//
|
||||
// The execution model is pure Go (stdlib only): machine code is mapped with
|
||||
// syscall.Mmap and invoked through an assembly trampoline that switches to a
|
||||
// prepared ABI0 stack. No cgo, no external toolchain.
|
||||
// The execution model is pure Go, stdlib only, with no cgo: machine code is
|
||||
// mapped with syscall.Mmap and invoked through an assembly trampoline that
|
||||
// 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
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user