Compare commits

..
12 Commits
Author SHA1 Message Date
petrbalvin 9f4f949c1f chore: prepare release v0.34.0
Test / test (push) Successful in 2m11s
Release / gates (push) Successful in 2m11s
Release / build (amd64, linux) (push) Successful in 1m13s
Release / build (arm64, linux) (push) Successful in 1m10s
Release / build (loong64, linux) (push) Successful in 1m12s
Release / build (riscv64, linux) (push) Successful in 1m33s
Release / release (push) Successful in 58s
2026-09-20 01:44:23 +02:00
petrbalvin f0d5238c47 docs: state the validation status and correct claims the material contradicts
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin 2931bbd6b2 ci(release): refuse a tag the security policy does not name
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin 63562a503a test(justfile): run the CLI and debugger tests outside the coverage set
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin e836d6150d docs: changelog entry for the loong64 JIT enablement
Test / test (push) Successful in 2m7s
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin 8a51b060da feat(cmd): enable loong64 JIT execution, all trampolines qemu-validated
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin d3d47db727 test(verify): seed the arm64 ABI kernel arguments
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin 0758556b7d docs: changelog entries for the parity round and corpus number
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin ddb8440340 fix(cmd): padding-aware ground-truth comparison
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin f15ff66fb1 fix(riscv64): accept the g spelling of the goroutine register
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin 187e4856d3 feat(amd64): encode the mixed-width extend family and PMOVMSKB
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin d315a998ce fix(arm64): store-exclusive operand order and large-frame parity
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
43 changed files with 1033 additions and 299 deletions
+24
View File
@@ -55,6 +55,25 @@ jobs:
print qq{tag $v\n}; 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 - name: Build
run: go build ./... run: go build ./...
@@ -78,6 +97,11 @@ jobs:
# The same command as in test.yml, so the floor is the same number everywhere. # 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/... 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 - name: Coverage floor
run: | run: |
perl -e ' perl -e '
+8
View File
@@ -84,6 +84,14 @@ jobs:
# suites); the runner's Go setup provides both the tool and GOROOT. # 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/... 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 - name: Oracle parity
# Re-run the live go-tool-asm comparison as its own step so that a 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. # regression names the gate that failed instead of hiding inside the suite.
+101 -47
View File
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
-
## [0.34.0] - 2026-09-20
### Added
- **Indirect JMP and CALL on all four architectures.** `JMP AX`, - **Indirect JMP and CALL on all four architectures.** `JMP AX`,
`CALL AX`, `JMP (BX)` and the memory forms encode at byte parity with `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 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 JALR; loong64 accepts the raw `JIRL rd, rj, off` spelling the Go
assembler cannot express. A frameless amd64 function containing a assembler cannot express. A frameless amd64 function containing a
CALL now receives the toolchain's forced base-pointer frame. The CALL now receives the toolchain's forced base-pointer frame. The
verify trampolines join the ground-truth lists, and a lint check for riscv64 and loong64 verify trampolines join their ground-truth lists,
control flow through registers and memory extends to the new forms. and a lint check for control flow through registers and memory extends
to the new forms.
- **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target - **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target
architecture can be named explicitly instead of inferred from the architecture can be named explicitly instead of inferred from the
file-name suffix, which is how the suffix-less majority of GOROOT's file-name suffix, which is how the suffix-less majority of GOROOT's
@@ -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` - **`gasm audit-instructions --corpus [dir]`.** Assembles every `.s`
file under a directory (default GOROOT/src) with the gasm encoder file under a directory (default GOROOT/src) with the gasm encoder
only: suffixed files for their architecture, suffix-less files for only: suffixed files for their architecture, suffix-less files for
all four, as a GOARCH build would. Reports the headline number (108 all four, as a GOARCH build would. Reports the headline number (127
of 627 GOROOT files, 17.2 %, assemble for every target architecture, of 627 GOROOT files, 20.3 %, assemble for every target architecture),
against 23 in the previous release), the per-architecture pass rates the per-architecture pass rates and the most common failure reasons
and the most common failure reasons with a representative file each, with a representative file each,
which drive the encodability backlog by frequency. which drive the encodability backlog by frequency.
- **Fuzz targets for the parser and the formatter.** FuzzParse (no - **The parser and the formatter are fuzzed.** Two targets carry the
panic, always a usable file) and FuzzFormatIdempotency (formatting guarantee: no input makes the parser panic, and every input yields a
twice equals formatting once; clean input stays clean) seed file the rest of the toolkit can work on; formatting twice equals
themselves from the repository's kernels, so the plain test suite formatting once, and clean input stays clean. They seed from the
replays every seed in CI and `just fuzz` runs the mutation engine on repository's own kernels, and `just fuzz` drives the mutation engine
demand. on demand.
- **Oracle parity as its own CI step.** The push pipeline already ran - **Man pages.** docs/man carries gasm(1) and a page for every command
the live go-tool-asm comparison inside the suite; a dedicated step except `version`, which gasm(1) documents itself, written in roff:
now names that gate when it fails. synopsis, description, every flag with its default, exit status,
- **Man pages.** docs/man carries gasm(1) and one page per command, worked examples and cross-references.
written in roff: synopsis, description, every flag with its default,
exit status, worked examples and cross-references.
`just install-man` compresses them into ~/.local/share/man (MANDIR `just install-man` compresses them into ~/.local/share/man (MANDIR
overrides) and `just uninstall-man` removes them. A test builds the overrides) and `just uninstall-man` removes them. A test builds the
binary and compares every command's `-h` output with its page, so the binary and compares each page's flags and synopsis with its own `-h`
pages cannot drift from the CLI. output, so the pages cannot drift from the CLI.
### Changed ### Changed
- **Go 1.27.1 required.** The module declares `go 1.27.1`, so building
from source needs that patch release or newer.
- **Canonical just recipes.** `just gates` is the definition of done - **Canonical just recipes.** `just gates` is the definition of done
(build, fmt-check, vet, test, race). `install` now builds and copies (build, fmt-check, vet, test, race). `install` now builds and copies
the binary into `~/.local/bin` (`BINDIR` overrides) instead of the binary into `~/.local/bin` (`BINDIR` overrides) instead of
downloading module dependencies, and `install-bin` is gone. The test downloading module dependencies, and `install-bin` is gone. The test
gate sweeps the logic packages (arch through verify; the hardware-bound gate sweeps the logic packages (arch through verify; the ptrace-bound
`debug` and the thin `cmd/gasm` sit outside it), so the coverage floor `debug` and the thin `cmd/gasm` sit outside the coverage profile), so
is computed over the product code and the number is identical locally the coverage floor is computed over the product code and the number is
and in CI. `fuzz` requires its target package. identical locally and in CI; the two excluded packages' own tests run
in the gate and in the pipelines, outside the floor. `fuzz` requires
its target package.
- **The reported version comes from the build.** `gasm --version` - **The reported version comes from the build.** `gasm --version`
prints the version the toolchain recorded: the tag on a tagged prints the version the toolchain recorded: the tag on a tagged
checkout, a pseudo-version naming the commit below one, `+dirty` on a checkout, a pseudo-version naming the commit below one, `+dirty` on a
@@ -78,8 +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 synopsis, the commands, every flag with its default, the exit codes and
worked examples; `CONTRIBUTING.md` carries the Contributor terms and worked examples; `CONTRIBUTING.md` carries the Contributor terms and
states the commit trailer form, the one-logical-change rule and the states the commit trailer form, the one-logical-change rule and the
licence header rule. The repository's own assembly (the `verify` licence header rule; `SECURITY.md` states how a vulnerability is
trampolines and the test kernels) is in `gasm fmt` canonical form. reported and what to expect. The repository's own assembly (the
`verify` trampolines and the test kernels) is in `gasm fmt` canonical
form.
- **The README states the project's purpose and status.** It opens with - **The README states the project's purpose and status.** It opens with
a warning that the tool is an experiment under active development, a warning that the tool is an experiment under active development,
version 0.x.x, free to change without warning, with 1.0.0 far off, version 0.x.x, free to change without warning, with 1.0.0 far off,
@@ -88,7 +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 Go toolchain), argues the case for the syntax in a new Why Plan 9
assembly section, and carries a Direction section: extended assembly section, and carries a Direction section: extended
instruction support, full GOOBJ and ELF compilation, Linux and instruction support, full GOOBJ and ELF compilation, Linux and
FreeBSD, and the four architectures. FreeBSD, and the four architectures. A Validation status section
states what has been executed where: amd64 on real hardware, the other
three architectures under qemu-user emulation, the encoding parity on
the host for all four, and the debugger's ptrace path on amd64 only.
### Fixed ### Fixed
@@ -125,13 +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 (everything after it was dropped); it is an illegal token now, invalid
UTF-8 no longer inflates byte offsets, and CRLF files format to UTF-8 no longer inflates byte offsets, and CRLF files format to
uniform LF. uniform LF.
- **A frameless amd64 function containing a CALL read its arguments from - **The class-2 stack guard branched four bytes past its target.** When
the wrong stack slot.** The forced base-pointer frame shifted the underflow branch relaxed to its 32-bit form, its displacement was
FP references by eight bytes (`x+0(FP)` resolved to SP+0x18 where the still computed as if the branch were two bytes long, so it landed
toolchain emits SP+0x10), so such functions loaded garbage. The inside the morestack CALL instead of the compare that decides it.
class-2 stack guard had the sibling defect: whenever the underflow The long form is reachable once a large frame carries a body of roughly
branch relaxed to its 32-bit form, its displacement ran four bytes a hundred bytes.
past the target and into the morestack CALL.
- **Immediate operands wrapped silently on amd64.** Shift counts, - **Immediate operands wrapped silently on amd64.** Shift counts,
immediates beyond the operand's width and displacements beyond int32 immediates beyond the operand's width and displacements beyond int32
truncated without a diagnostic (`SHLQ $300` assembled as `$44`); they truncated without a diagnostic (`SHLQ $300` assembled as `$44`); they
@@ -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 exit-2 contract now holds, `asm -o` no longer prints the hex dump it
claimed to replace, and `verify --ground-truth` works for amd64 claimed to replace, and `verify --ground-truth` works for amd64
kernels on non-amd64 hosts instead of refusing with JIT advice. 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 ## [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 ## [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 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 real `go build`. The encoder covers the full integer instruction set, FP
arithmetic, conditional select, CRC32, and the MOV pseudo-instruction with 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 ### 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 files: the AArch64 integer instruction set with the MOV pseudo-instruction and
its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with
logical bitmask encoding for values like `$1`), data-processing (shifted 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, immediate), conditional and unconditional branches, FP/SP frame mapping,
SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations), SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations),
jump chain folding, and ELF64 emission (`gasm asm --format elf`). Ground-truth 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 verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. The
(the other architectures; RISC-V, LoongArch, arm64) is now complete. encoder set for the remaining architectures (RISC-V, LoongArch, arm64) is
complete.
### Changed ### Changed
@@ -508,7 +562,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
## [0.30.0] - 2026-08-13 ## [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 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 `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 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 ### 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 files: the full LoongArch64 instruction set with the dual-form arithmetic
mnemonics, the 16/21-bit branch families, the MOV pseudo-instruction and mnemonics, the 16/21-bit branch families, the MOV pseudo-instruction and
its immediate-constant expansions, FP/SP frame mapping, SB/global symbol 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.** The toolkit, its CI and the released binaries are now
Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}. 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 disassembly at PC, memory-write, watchpoints, and source-line mapping are
all shipped. all shipped.
@@ -818,7 +872,7 @@ exposes the full dynamic-analysis toolkit.
## [0.20.0] - 2026-07-25 ## [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 enumeration from the assembler's label map, combined with multi-input path
diversity measurement; how many observationally distinct execution paths a diversity measurement; how many observationally distinct execution paths a
test corpus exercises. test corpus exercises.
@@ -843,7 +897,7 @@ execute) without fighting the runtime.
## [0.19.0] - 2026-07-24 ## [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 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 (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 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 ## [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 Plan 9 amd64 kernels into executable memory and calls them directly; pure Go
(stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external (stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external
toolchain. toolchain.
@@ -1285,8 +1339,8 @@ support).
## [0.2.0] - 2026-07-07 ## [0.2.0] - 2026-07-07
The Phase 2 assembler grows the SIMD set: shuffles, extract/insert, permute The assembler grows the SIMD set: shuffles, extract/insert, permute
and the moves, on top of the Phase 1 VEX forms. and the moves, on top of the VEX forms of the first release.
### Added ### Added
@@ -1322,7 +1376,7 @@ and the moves, on top of the Phase 1 VEX forms.
## [0.1.0] - 2026-07-06 ## [0.1.0] - 2026-07-06
Initial release; the Phase 1 foundation. Initial release: the foundation.
### Added ### Added
+15 -9
View File
@@ -24,7 +24,9 @@ below; submitting one means you accept them.
## Development setup ## Development setup
Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod` Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod`
declares, and [just](https://github.com/casey/just) for the recipes. declares, [just](https://github.com/casey/just) for the recipes, and a C
compiler (gcc), because `just gates` includes `just race` and the race
detector needs cgo.
```sh ```sh
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
@@ -54,16 +56,20 @@ workflow builds the assets and publishes the release and its notes.
## Code style ## Code style
`gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero `gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero
warnings tolerated. `just gates` is the definition of done in one command, and the recipe warnings tolerated. `just vet` is two gates, `go vet ./...` and `go fix -diff ./...`,
file names what it contains. Errors are checked explicitly, wrapped as so the modernisation rewrites are enforced too. `just gates` is the definition of done in
`fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The recipe file holds one command, and the recipe file names what it contains. Errors are checked explicitly,
the commands, and the language and standard-library surface is the one the `go` directive wrapped as `fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The
in `go.mod` pins. recipe file holds the commands, and the language and standard-library surface is the one
the `go` directive in `go.mod` pins.
- `golang.org/x/arch` is the one module dependency, and it is linked into the binary: - `golang.org/x/arch` is the one module dependency, and it is linked into the binary:
`gasm dis` and the debugger's listings decode through it. Everything else is the `gasm dis` and the debugger's listings decode through it. Everything else is the
standard library. standard library.
- No cgo, no C, no external toolchain at runtime. - No cgo and no C. The standalone encoder paths (`gasm asm --format raw` and `--format
elf`) need no Go installation; `gasm verify --ground-truth`, `gasm verify --fuzz`,
`gasm audit-instructions` and `gasm asm --format goobj` resolve through the installed
Go toolchain.
- The parser, lexer and formatter are hand-written; the `arch` instruction tables are - The parser, lexer and formatter are hand-written; the `arch` instruction tables are
generated only by `_gen/gen.go` (`just gen`) and never edited by hand. generated only by `_gen/gen.go` (`just gen`) and never edited by hand.
- Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a - Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a
@@ -111,8 +117,8 @@ Workflows live in `.gitea/workflows/` and run on the project's own runners:
| Workflow | Trigger | What it does | | Workflow | Trigger | What it does |
|---|---|---| |---|---|---|
| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor | | Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor, the CLI and debugger tests outside the profile, then the oracle-parity rerun against `go tool asm` |
| Release | a `v*` tag | the same gates as Test, then the matrix build, the proven version and the release itself; the race detector runs locally in `just gates` before the tag is cut | | Release | a `v*` tag | the same gates as Test minus the oracle-parity step, then the matrix build, the version smoke test and the release itself; the race detector runs locally in `just gates` before the tag is cut |
The local equivalent is `just gates`, which is the same set plus the race detector. The The local equivalent is `just gates`, which is the same set plus the race detector. The
race detector also has its own workflow, dispatched by hand; it never runs on a push or a race detector also has its own workflow, dispatched by hand; it never runs on a push or a
+54 -19
View File
@@ -5,23 +5,25 @@
> output formats and behaviour can change without warning at any time. > output formats and behaviour can change without warning at any time.
> A 1.0.0 release is light years away. Nothing in this document is a > A 1.0.0 release is light years away. Nothing in this document is a
> stability promise. For all of that, this is not a paper project: gasm > stability promise. For all of that, this is not a paper project: gasm
> is already in active use and is tested on real assembly work. > is already in active use and is tested on real assembly work. Only
> amd64 is validated on real hardware; the other three architectures run
> under emulation ([Validation status](#validation-status)).
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling: **GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
there is no formatter, no linter, no static analyser, no standalone there is no formatter, no linter and no debugger for `.s` files, and no
assembler and no debugger for `.s` files. Developers write assembly assembler that works without a Go installation. Developers write
blind, validate it by benchmark, and debug it by print statement. assembly blind, validate it by benchmark, and debug it by print
gasm-devkit is the missing toolkit: a single, self-contained binary, statement. gasm-devkit is the missing toolkit: a single, self-contained
`gasm`, that serves both purposes. binary, `gasm`, that serves both purposes.
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly, - **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
dynamic verification, a source-level debugger and a language server, dynamic verification, a source-level debugger and a language server,
for `.s` files in Go programs. for `.s` files in Go programs.
- **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes - **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes
on its own, with no Go installation in the loop, and writes raw on its own and writes raw images or linkable ELF objects with DWARF5
images, linkable ELF objects with DWARF5 debug sections, or the Go debug sections, with no Go installation in the loop; the Go
toolchain's own GOOBJ format, which `go build` consumes in place of toolchain's own GOOBJ format, which `go build` consumes in place of
the toolchain's output. the toolchain's output, needs the installed toolchain.
## Why Plan 9 assembly ## Why Plan 9 assembly
@@ -69,12 +71,14 @@ to give that syntax the tooling it deserves.
operating recursively on directories the way `go fmt` does. `-l` lists operating recursively on directories the way `go fmt` does. `-l` lists
files whose formatting differs and `-d` prints a unified diff. files whose formatting differs and `-d` prints a unified diff.
- **Linter.** `gasm lint` runs 18 conservative static checks, among them - **Linter.** `gasm lint` runs 18 conservative static checks, among them
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature), `undefined-label`, `abi-argsize` (declared argument area vs the `// func`
`register-clobber` (Go ABI register liveness over the control-flow graph), signature), `register-clobber` (Go ABI register liveness over the
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`. control-flow graph), `stack-imbalance`, `abi0-register-args` and
`unencodable-instruction`.
- **Standalone assembler.** `gasm asm` encodes all four architectures without - **Standalone assembler.** `gasm asm` encodes all four architectures without
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5 the Go toolchain and writes raw images or linkable ELF objects (with DWARF5
debug sections) or the Go toolchain's own GOOBJ format, which `go build` debug sections) with no Go installation needed, or the Go toolchain's own
GOOBJ format, which needs the installed toolchain and which `go build`
consumes in place of the toolchain's output. Framed functions get the consumes in place of the toolchain's output. Framed functions get the
stack-split guard and the morestack block, byte-identical to the stack-split guard and the morestack block, byte-identical to the
toolchain's, so split functions link too. toolchain's, so split functions link too.
@@ -86,7 +90,8 @@ to give that syntax the tooling it deserves.
byte-for-byte ground-truth comparison of the machine code. byte-for-byte ground-truth comparison of the machine code.
- **Debugger.** `gasm debug` is a source-level ptrace debugger with - **Debugger.** `gasm debug` is a source-level ptrace debugger with
breakpoints (optionally conditional), hardware watchpoints, register and breakpoints (optionally conditional), hardware watchpoints, register and
memory inspection, and headless script runs with label-level coverage. memory inspection, and headless script runs that report instruction and
label coverage.
- **Language server.** `gasm lsp` serves completion, hover, document symbols, - **Language server.** `gasm lsp` serves completion, hover, document symbols,
push and pull diagnostics, semantic-token highlighting, go-to-definition, push and pull diagnostics, semantic-token highlighting, go-to-definition,
find references, rename, formatting, inlay hints, code actions, signature find references, rename, formatting, inlay hints, code actions, signature
@@ -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. reported as an explicit error, never as a wrong byte.
The same measurement runs over GOROOT's whole assembly corpus: 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 assembling for every target architecture today, with the top failure
reasons per architecture; the number moves with every release. reasons per architecture; the number moves with every release.
### Validation status
**Only amd64 is validated on real hardware.** The other three
architectures are validated under qemu-user emulation, because the
project owns no arm64, riscv64 or loong64 machine, and emulation is the
only substitute available for the hardware. The distinction matters and
is stated rather than implied: everything below is a claim about what has
actually been executed.
| Layer | amd64 | arm64, riscv64, loong64 |
|---|---|---|
| Encoding: byte-for-byte against `go tool asm` | native hardware | native hardware (the toolchain cross-assembles any GOARCH on any host) |
| Execution: JIT calls, ABI checks, differential fuzzing | native hardware | qemu-user emulation |
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more |
Consequences, stated plainly. An emulator is a model of a CPU, not the
CPU: instruction semantics are implemented in software and can differ
from silicon in ways a test suite does not reveal. A kernel that passes
under qemu-user is therefore not proven correct on real hardware, and a
discrepancy found on real hardware is a defect in gasm, reported like any
other. Encoding parity is the exception: the byte comparison against the
toolchain runs on the host for every architecture, so no emulator stands
between the claim and the evidence. The debugger is the weakest case: on
the three emulated architectures its per-architecture ptrace code has
been compiled and read, never executed. Its architecture-neutral units
run under `go test ./...`, which the race workflow and a manual run
perform; the default `just test` gate does not sweep `./debug/...`.
## Direction ## Direction
The plan, in the order it is being worked: The plan, in the order it is being worked:
@@ -134,7 +167,8 @@ The plan, in the order it is being worked:
an extended instruction set the toolchain does not know at all. The an extended instruction set the toolchain does not know at all. The
toolchain-derived tables stay generated and untouched; only the toolchain-derived tables stay generated and untouched; only the
extended instructions are hand-maintained, with their own spellings extended instructions are hand-maintained, with their own spellings
and encoders, verified by execution on real hardware because the and encoders, verified by execution (on real hardware for amd64, under
emulation for the rest, per the validation status above) because the
toolchain offers no ground truth to compare against. The gaps exist toolchain offers no ground truth to compare against. The gaps exist
on every architecture, amd64 included. on every architecture, amd64 included.
- **Full GOOBJ and ELF compilation.** The destination is a complete, - **Full GOOBJ and ELF compilation.** The destination is a complete,
@@ -205,7 +239,7 @@ gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
gasm debug --func name k.s # interactive debugger gasm debug --func name k.s # interactive debugger
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
gasm debug --func name --cover k.s # which labels did execution reach? gasm debug --func name --cover k.s # instruction and label coverage
gasm diff a.s b.s # compare machine code byte-for-byte gasm diff a.s b.s # compare machine code byte-for-byte
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
gasm profile k.s # show basic-block structure gasm profile k.s # show basic-block structure
@@ -243,7 +277,8 @@ recipe.
- [docs/CLI.md](docs/CLI.md): full command reference - [docs/CLI.md](docs/CLI.md): full command reference
- man pages: `just install-man` installs gasm(1) and one page per command - man pages: `just install-man` installs gasm(1) and one page per command
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes except `version`, which is documented inside gasm(1) instead, into
~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
them them
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes - [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
+3 -2
View File
@@ -7,7 +7,7 @@ releases do not receive them.
| Version | Supported | | Version | Supported |
|---|---| |---|---|
| 0.33.0 | yes | | 0.34.0 | yes |
| older releases | no | | older releases | no |
## Reporting a vulnerability ## Reporting a vulnerability
@@ -29,7 +29,8 @@ Include:
- You are kept informed while the fix is being made, and told when it ships. - You are kept informed while the fix is being made, and told when it ships.
- The fix is released before the details are published, and the timing is agreed with - The fix is released before the details are published, and the timing is agreed with
you. you.
- The reporter is credited in the release notes unless they ask otherwise. - The fix ships without naming you: the project keeps no credits list, so the release
notes, the changelog and the commits name no reporter.
## Out of scope ## Out of scope
+60 -18
View File
@@ -314,7 +314,8 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
return encodeARM64CRC32(mnem, enc.op, ops) 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 { if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FExcl {
return encodeARM64Excl(mnem, enc.op, ops) 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 return a64wordLE(op | 31<<16 | 31<<5 | uint32(rd)), nil
} }
// The Go toolchain classifies immediates: // The Go toolchain classifies immediates (asm7.go conclass):
// - C_ABCON0 (0 < v ≤ 4095): bitmask first for positive values // - inside the imm12/shifted-imm12 "addcon" band (C_ABCON0/C_ABCON,
// - Negative values: MOVN first, then bitmask // 0 < v ≤ 4095 or a 4096 multiple up to 0xFFF000): bitmask first, so
// - C_MOVCON (movcon-eligible, outside ABCON range): MOVZ/MOVN first // `MOVD $4096, R27` is ORR $4096, not MOVZ $(1<<12)
tryBitmaskFirst := d > 0 && d <= 0xFFF // - 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 { 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)) N, immr, imms, ok := arm64Bitmask(uint64(d), int(sf))
if ok { if ok {
return a64wordLE(sf<<31 | 1<<29 | 0x24<<23 | N<<22 | immr<<16 | imms<<10 | 31<<5 | uint32(rd)), nil 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 return rn, nil
} }
// encodeARM64Excl encodes an exclusive load/store instruction. // arm64PairOf parses a register-pair operand `(R1, R2)`, reporting false
// LDXR (Rn), Rt → LDXR Rt, [Rn] (2 operands: mem, reg) // when the operand is not a pair. The toolchain takes the second register of
// STXR Rs, (Rn), Rt → STXR Rs, Rt, [Rn] (3 operands: Rs, mem, Rt-status) // 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) { func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
// LDXR/STXR have different operand forms.
isLoad := strings.HasPrefix(mnem, "LD") isLoad := strings.HasPrefix(mnem, "LD")
if isLoad { if isLoad {
// LDXR (Rn), Rt → 2 operands: mem, reg // LDXR (Rn), Rt / LDXP (Rn), (Rt1, Rt2): 2 operands.
if len(ops) != 2 { if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) 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 { if err != nil {
return nil, err 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])) rt := arm64RegNum(operandRegName(ops[1]))
if rt < 0 { if rt < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem) return nil, fmt.Errorf("invalid operand in %s", mnem)
} }
return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rt)), nil 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 { if len(ops) != 3 {
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops)) 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]) rn, err := arm64ExclMem(mnem, ops[1])
if err != nil { if err != nil {
return nil, err 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 { if rt < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem) return nil, fmt.Errorf("invalid operand in %s", mnem)
} }
+23 -3
View File
@@ -98,8 +98,12 @@ func arm64RegNum(name string) int {
return 30 return 30
case "R31", "ZR": case "R31", "ZR":
return 31 return 31
case "SP": case "SP", "RSP":
return 31 // SP and ZR share encoding 31; context determines meaning // 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. // F0-F31.
if len(name) >= 1 && name[0] == 'F' { if len(name) >= 1 && name[0] == 'F' {
@@ -282,7 +286,7 @@ const (
a64FFPSel // FP conditional select (Rm, Rn, Rd, cond): FCSEL a64FFPSel // FP conditional select (Rm, Rn, Rd, cond): FCSEL
a64FCRC32 // CRC32 a64FCRC32 // CRC32
a64FCSEL // conditional select: CSEL, CSINC, CSINV, CSNEG 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 a64FLSE // LSE atomics: LDADD, CAS, SWP
a64FSIMD3 // SIMD 3-operand: VADD, VSUB, VMUL a64FSIMD3 // SIMD 3-operand: VADD, VSUB, VMUL
) )
@@ -575,6 +579,10 @@ func init() {
} }
// ---- exclusive load/store ---- // ---- 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["LDXR"] = a64Enc{format: a64FExcl, op: 0xc85f7c00}
a64InstrTable["LDXRB"] = a64Enc{format: a64FExcl, op: 0x085f7c00} a64InstrTable["LDXRB"] = a64Enc{format: a64FExcl, op: 0x085f7c00}
a64InstrTable["LDXRH"] = a64Enc{format: a64FExcl, op: 0x485f7c00} a64InstrTable["LDXRH"] = a64Enc{format: a64FExcl, op: 0x485f7c00}
@@ -583,6 +591,12 @@ func init() {
a64InstrTable["LDAXRB"] = a64Enc{format: a64FExcl, op: 0x085ffc00} a64InstrTable["LDAXRB"] = a64Enc{format: a64FExcl, op: 0x085ffc00}
a64InstrTable["LDAXRH"] = a64Enc{format: a64FExcl, op: 0x485ffc00} a64InstrTable["LDAXRH"] = a64Enc{format: a64FExcl, op: 0x485ffc00}
a64InstrTable["LDAXRW"] = a64Enc{format: a64FExcl, op: 0x885ffc00} 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["STXR"] = a64Enc{format: a64FExcl, op: 0xc8007c00}
a64InstrTable["STXRB"] = a64Enc{format: a64FExcl, op: 0x08007c00} a64InstrTable["STXRB"] = a64Enc{format: a64FExcl, op: 0x08007c00}
a64InstrTable["STXRH"] = a64Enc{format: a64FExcl, op: 0x48007c00} a64InstrTable["STXRH"] = a64Enc{format: a64FExcl, op: 0x48007c00}
@@ -591,6 +605,12 @@ func init() {
a64InstrTable["STLXRB"] = a64Enc{format: a64FExcl, op: 0x0800fc00} a64InstrTable["STLXRB"] = a64Enc{format: a64FExcl, op: 0x0800fc00}
a64InstrTable["STLXRH"] = a64Enc{format: a64FExcl, op: 0x4800fc00} a64InstrTable["STLXRH"] = a64Enc{format: a64FExcl, op: 0x4800fc00}
a64InstrTable["STLXRW"] = a64Enc{format: a64FExcl, op: 0x8800fc00} 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 ---- // ---- LSE atomics ----
a64InstrTable["LDADDD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x1c1<<21 | 0x00<<10} a64InstrTable["LDADDD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x1c1<<21 | 0x00<<10}
+74 -6
View File
@@ -820,15 +820,28 @@ func TestArm64ExclOffsetErrors(t *testing.T) {
} }
} }
// TestArm64ExclNoOffset pins the plain (Rn) forms. gasm parses the store // TestArm64ExclNoOffset pins the plain (Rn) forms, byte-for-byte against
// with the status register first (ARM ARM order); go tool asm parses the // go tool asm. The toolchain parses the FIRST register of a store as the
// same text with the data register first, so the two spellings differ and // data register and the LAST as the status register (asm7.go case 59), and
// the store word below is gasm's own. // 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) { 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{ want := []uint32{
0xc85f7c22, // LDXR X2, [X1] 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 0xd65f03c0, // RET
} }
for i := range want { 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) 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
View File
@@ -187,10 +187,32 @@ func arm64Prologue(fi arm64FrameInfo) []byte {
return a64WordsLE(ws...) return a64WordsLE(ws...)
} }
// arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value // arm64SplitImm12 reports whether the toolchain decomposes ADD/SUB $imm into
// fits the imm12 field (plain, or shifted left by 12 when it is a multiple // two imm12 instructions instead of materialising it into REGTMP
// of 4096); otherwise the toolchain materialises it into REGTMP (R27) and // (asm7.go case 48, the C_ADDCON2 class): the value must fit 24 bits
// subtracts the register in the extended-register form. // 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 { func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
if imm <= 0xFFF { if imm <= 0xFFF {
return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)} 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 { if imm <= 4095<<12 && imm&0xFFF == 0 {
return []uint32{a64AddSub(1, 1, 0, 1, imm>>12, 31, rd)} return []uint32{a64AddSub(1, 1, 0, 1, imm>>12, 31, rd)}
} }
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD") if !arm64SplitImm12(imm) {
if err != nil { mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
mov = nil 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 // arm64AddImmWords emits ADD $imm, SP, Rd with the same imm12, shifted-imm12,
// and REGTMP fallback ladder. // split and REGTMP ladder as arm64SubImmWords.
func arm64AddImmWords(imm uint32, rd uint32) []uint32 { func arm64AddImmWords(imm uint32, rd uint32) []uint32 {
if imm <= 0xFFF { if imm <= 0xFFF {
return []uint32{a64AddSub(1, 0, 0, 0, imm, 31, rd)} 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 { if imm <= 4095<<12 && imm&0xFFF == 0 {
return []uint32{a64AddSub(1, 0, 0, 1, imm>>12, 31, rd)} 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 { if err != nil {
mov = 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 // 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 arm64PostLoad(3, 0, int32(fi.autosize), 31, 30), // LDR.P LR, [SP], #autosize
) )
} else { } else {
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP // Large frame: LDP -8(SP), (FP, LR), then deallocate.
ws = append(ws, ws = append(ws,
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair) 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) // 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 // 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, // 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) return 4 * (len(arm64SubImmWords(uint32(fi.autosize), 20)) + 1)
} }
// arm64ReturnEpilogueLen returns the byte length of the RET's epilogue up to // arm64ReturnEpilogueLen returns the byte length of the RET's epilogue up to
// (but not including) the final RET instruction. The ADD sequences share the // (but not including) the final RET instruction. The lengths are read from
// prologue's immediate ladder, so their length is read from the same helper // the same word-emitting helpers the epilogue uses rather than assumed: the
// rather than assumed: a materialised autosize costs its MOV words plus the // leaf path shares the prologue's immediate ladder, and a materialised
// ADD itself. // autosize costs its MOV words plus the ADD itself.
func arm64ReturnEpilogueLen(fi arm64FrameInfo) int { func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
if fi.autosize == 0 { if fi.autosize == 0 {
return 0 return 0
@@ -280,8 +333,9 @@ func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
if fi.autosize <= 0xf0 { if fi.autosize <= 0xf0 {
return 8 // LDR + LDR.P return 8 // LDR + LDR.P
} }
// LDP + the ADD ladder that deallocates the frame. // LDP + the deallocation emitted by arm64RetAddWords, so the length
return 4 + 4*len(arm64AddImmWords(uint32(fi.autosize), 31)) // tracks whatever the MOVD ladder needs.
return 4 + 4*len(arm64RetAddWords(uint32(fi.autosize)))
} }
// arm64ResolvePseudo translates a pseudo-register memory reference into a // arm64ResolvePseudo translates a pseudo-register memory reference into a
+7
View File
@@ -86,9 +86,16 @@ func Encodable(mnemonic string) bool {
"BSWAP", "BSWAP",
"PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2", "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2",
"MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX", "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
"MOVBWZX", "MOVBWSX", "MOVBLSX", "MOVBQSX", "MOVWQSX", "MOVLQZX",
"CVTSL2SD", "CVTSQ2SD", "CVTSL2SD", "CVTSQ2SD",
"MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS": "MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
return true 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 return false
} }
+7 -1
View File
@@ -101,6 +101,11 @@ func (e *enc) encode(mnem string, ops []Operand) error {
if m, ok := sseBinTable[base]; ok { if m, ok := sseBinTable[base]; ok {
return e.encodeSSEBin(m, ops) 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 { switch base {
case "MOV": case "MOV":
return e.encodeMov(ops, size) return e.encodeMov(ops, size)
@@ -126,7 +131,8 @@ func (e *enc) encode(mnem string, ops []Operand) error {
return e.encodeBswap(ops, size) return e.encodeBswap(ops, size)
case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2": case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2":
return e.encodePrefetch(base, ops) 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) return e.encodeMovExtend(base, ops)
case "CVTSL2SD", "CVTSQ2SD": case "CVTSL2SD", "CVTSQ2SD":
return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops) return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops)
+12
View File
@@ -357,6 +357,18 @@ func TestScalarGroundTruth(t *testing.T) {
{"MOVBQZX AL,R8", "MOVBQZX", []Operand{AL, r8}, "4c0fb6c0", "MOVZX"}, {"MOVBQZX AL,R8", "MOVBQZX", []Operand{AL, r8}, "4c0fb6c0", "MOVZX"},
{"MOVWLZX AX,CX", "MOVWLZX", []Operand{AX, CX}, "0fb7c8", "MOVZX"}, {"MOVWLZX AX,CX", "MOVWLZX", []Operand{AX, CX}, "0fb7c8", "MOVZX"},
{"MOVWQZX AX,R8", "MOVWQZX", []Operand{AX, r8}, "4c0fb7c0", "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 R8,X13", "CVTSL2SD", []Operand{r8, vreg(t, "X13")}, "f2450f2ae8", "CVTSI2SD"},
{"CVTSL2SD AX,X0", "CVTSL2SD", []Operand{AX, vreg(t, "X0")}, "f20f2ac0", "CVTSI2SD"}, {"CVTSL2SD AX,X0", "CVTSL2SD", []Operand{AX, vreg(t, "X0")}, "f20f2ac0", "CVTSI2SD"},
{"CVTSQ2SD R8,X13", "CVTSQ2SD", []Operand{r8, vreg(t, "X13")}, "f24d0f2ae8", "CVTSI2SD"}, {"CVTSQ2SD R8,X13", "CVTSQ2SD", []Operand{r8, vreg(t, "X13")}, "f24d0f2ae8", "CVTSI2SD"},
+40 -13
View File
@@ -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 // width. The source is narrower than the destination, so the plain size-suffix
// convention does not apply to these names. // convention does not apply to these names.
var movExtendOp = map[string]struct { var movExtendOp = map[string]struct {
op []byte op []byte
dst64 bool dstSize int
}{ }{
"MOVBLZX": {[]byte{0x0F, 0xB6}, false}, // byte → long, zero-extend "MOVBLZX": {[]byte{0x0F, 0xB6}, 4}, // byte → long, zero-extend
"MOVBQZX": {[]byte{0x0F, 0xB6}, true}, // byte → quad, zero-extend "MOVBQZX": {[]byte{0x0F, 0xB6}, 8}, // byte → quad, zero-extend
"MOVWLZX": {[]byte{0x0F, 0xB7}, false}, // word → long, zero-extend "MOVWLZX": {[]byte{0x0F, 0xB7}, 4}, // word → long, zero-extend
"MOVWQZX": {[]byte{0x0F, 0xB7}, true}, // word → quad, zero-extend "MOVWQZX": {[]byte{0x0F, 0xB7}, 8}, // word → quad, zero-extend
"MOVWLSX": {[]byte{0x0F, 0xBF}, false}, // word → long, sign-extend "MOVWLSX": {[]byte{0x0F, 0xBF}, 4}, // word → long, sign-extend
"MOVLQSX": {[]byte{0x63}, true}, // long → quad, sign-extend (MOVSXD) "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 // 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 { if !ok {
return fmt.Errorf("%s destination must be a register", base) return fmt.Errorf("%s destination must be a register", base)
} }
size := 4 i := newInstr(spec.dstSize, spec.op)
if spec.dst64 { if err := setRM(i, dstReg, ops[0], spec.dstSize); err != nil {
size = 8 return err
} }
i := newInstr(size, spec.op) return e.emit(i)
if err := setRM(i, dstReg, ops[0], size); err != nil { }
// 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 err
} }
return e.emit(i) return e.emit(i)
+1 -1
View File
@@ -65,7 +65,7 @@ func riscvRegNum(name string) int {
return 25 return 25
case "X26", "S10": case "X26", "S10":
return 26 return 26
case "X27", "S11": case "X27", "S11", "g":
return 27 return 27
case "X28", "T3": case "X28", "T3":
return 28 return 28
+8 -5
View File
@@ -40,10 +40,13 @@ func cmdAuditInstructions(args []string) error {
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", ` fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", `
Compare the gasm encoder for the given architecture (default amd64) against Compare the gasm encoder for the given architecture (default amd64) against
go tool asm and print the diff: superset encodings (gasm-only, shippable via go tool asm and print the diff: superset encodings (gasm-only, shippable via
gasm asm --format goobj), known-but-unencodable names (the backlog) and go- gasm asm --format goobj) and known-but-unencodable names (the backlog). The
only names (feature gaps). The Go side is probed black-box with a battery Go side is probed black-box one bare mnemonic at a time, so the audit tracks
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` whatever toolchain `+"`go env GOROOT`"+` provides; the gasm side answers from
provides. the encoder table on amd64 and from trial assembly over a battery of operand
shapes elsewhere. Names go tool asm knows and gasm does not cannot be
enumerated by probing, because Go's table is visible only through names
already in the gasm table; the report closes with a note saying so.
With --corpus the audit changes shape: it assembles every .s file under the With --corpus the audit changes shape: it assembles every .s file under the
given directory (default GOROOT/src) with the gasm encoder only, no given directory (default GOROOT/src) with the gasm encoder only, no
@@ -109,7 +112,7 @@ the encodability backlog by frequency rather than by table order.
w := os.Stdout w := os.Stdout
fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names)) fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names))
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %d\n", len(shared)+len(superset), countTrue(goKnown)) fmt.Fprintf(w, "gasm encodable: %d go tool asm recognised: %d\n", len(shared)+len(superset), countTrue(goKnown))
fmt.Fprintf(w, "shared: %d\n", len(shared)) fmt.Fprintf(w, "shared: %d\n", len(shared))
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n") fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
for _, n := range superset { for _, n := range superset {
+3 -2
View File
@@ -24,9 +24,10 @@ function in a traced subprocess (ptrace), then provides a REPL for
single-stepping, breakpoints, register and memory inspection. single-stepping, breakpoints, register and memory inspection.
REPL commands: REPL commands:
break <label|addr> [if <reg> <op> <val>] break <label|addr|line> [if <reg> <op> <val|reg|*addr>]
set a breakpoint, optionally conditional on a set a breakpoint, optionally conditional on a
register comparison (reg-reg or reg-immediate) comparison of one register against a constant,
another register, or the 8-byte word at *addr
delete <label|addr> remove a breakpoint delete <label|addr> remove a breakpoint
info break list all breakpoints info break list all breakpoints
step [n], s single-step n instructions (default 1) step [n], s single-step n instructions (default 1)
+50 -20
View File
@@ -486,10 +486,13 @@ func cmdAsm(args []string) int {
With -o the output is written to a file instead. The --format flag selects With -o the output is written to a file instead. The --format flag selects
what is written: raw (the default) concatenates the functions and the data what is written: raw (the default) concatenates the functions and the data
section into one self-consistent image; elf emits a relocatable object section into one self-consistent image; elf emits a relocatable object
(.text/.data sections, a symbol table and one PC32 relocation per (.text/.data sections, a symbol table and one relocation per static-symbol
static-symbol reference) that links with the system toolchain; goobj emits reference, in the architecture's own form: R_X86_64_PC32 on amd64,
the Go toolchain's own object format, which cmd/link consumes directly (it R_AARCH64_*, R_RISCV_* or R_LARCH_* on the others) that links with the
requires -p, the package path, and the installed Go toolchain). system toolchain; goobj emits the Go toolchain's own object format, which
cmd/link consumes directly (it requires -p, the package path, and the
installed Go toolchain: the object preamble is captured from go tool asm
and the format version from go version).
`) `)
out := fs.String("o", "", "write the output to this file") out := fs.String("o", "", "write the output to this file")
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
@@ -793,8 +796,8 @@ func cmdProfile(args []string) int {
flagSet := newCommand("profile", "gasm profile <file.s>", ` flagSet := newCommand("profile", "gasm profile <file.s>", `
Show the basic-block structure of functions in an assembly file. Show the basic-block structure of functions in an assembly file.
Lists each function's labels, their offsets, and the block boundaries. Lists each function's labels, their offsets, and the block boundaries.
This is the static structure; for runtime execution counts, use This is the static structure; for runtime execution counts use
gasm verify --fuzz which exercises the code paths. gasm debug --cover, and for input coverage gasm verify --fuzz.
`) `)
flagSet.Parse(args) flagSet.Parse(args)
if flagSet.NArg() != 1 { if flagSet.NArg() != 1 {
@@ -938,11 +941,40 @@ func compareGroundTruth(img *asm.Image, gt map[string][]byte) (matched, total, d
goCmp[j] = 0 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++ 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)) 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) fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
} }
} else { } 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 With -abi, each function is called with sentinel values in the registers
the Go ABI fixes across calls (the frame pointer and the goroutine the Go ABI fixes across calls (the frame pointer and the goroutine
pointer) plus a canary below SP; violations are reported. JIT-based pointer) plus a canary below SP; violations are reported. JIT-based
checks run when the host matches the file's architecture (all but checks run when the host matches the file's architecture, on all four
loong64, which is ground-truth only for now). architectures.
With -fuzz, each function with a // func signature is differentially fuzzed 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 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) With -call, a single function is invoked with user-supplied buffers (-buf)
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g. instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
decoders) that crash on random input but should succeed on valid data. decoders) that crash on random input but should succeed on valid data. The
function named must be NOSPLIT: a function with a stack frame is refused.
With -save-corpus (and -fuzz), every input that crashes or mismatches is With -save-corpus (and -fuzz), every input that crashes or mismatches is
written to the directory as replayable JSON. -replay re-runs saved written to the directory as replayable JSON. -replay re-runs saved
@@ -1033,16 +1066,13 @@ each entry reproduces.
path := set.Arg(0) path := set.Arg(0)
targetArch := arch.FromFilename(path) targetArch := arch.FromFilename(path)
// JIT execution runs when the host CPU matches the kernel's // JIT execution runs when the host CPU matches the kernel's
// architecture, except loong64: its trampoline is implemented but not // architecture; every trampoline is validated end to end under
// yet validated against real hardware (the Go runtime cannot start // qemu-user emulation (the loong64 one included, via the raw-address
// under the available loong64 emulators), so those kernels take the // leave handoff).
// toolchain-comparison path. if targetArch != hostArch() {
if targetArch != hostArch() || targetArch == arch.LOONG64 {
// No JIT on this host: ground truth and profile remain available for // No JIT on this host: ground truth and profile remain available for
// every architecture, because cmdVerifyNonJIT assembles and compares // every architecture, because cmdVerifyNonJIT assembles and compares
// against the toolchain without executing anything. (loong64 is // against the toolchain without executing anything.
// ground-truth-only everywhere for now: its trampoline is implemented
// but not yet validated against real hardware.)
switch targetArch { switch targetArch {
case arch.AMD64, arch.RISCV, arch.LOONG64, arch.ARM64: case arch.AMD64, arch.RISCV, arch.LOONG64, arch.ARM64:
return cmdVerifyNonJIT(path, targetArch, *groundTruth, *profile) return cmdVerifyNonJIT(path, targetArch, *groundTruth, *profile)
+20
View File
@@ -15,6 +15,7 @@ import (
"testing" "testing"
"sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
) )
const clean = "#include \"textflag.h\"\n" + const clean = "#include \"textflag.h\"\n" +
@@ -440,3 +441,22 @@ func TestRunCorpusAudit(t *testing.T) {
t.Errorf("arm64 unencodable reasons = %d, want 1", r) 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
View File
@@ -33,7 +33,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
entryAddr := codeBase + uint64(funcOffset) entryAddr := codeBase + uint64(funcOffset)
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize) fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit") fmt.Println("commands: break <label|addr|line> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
scanner := bufio.NewScanner(in) scanner := bufio.NewScanner(in)
@@ -233,7 +233,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
case "break", "b": case "break", "b":
if len(parts) < 2 { if len(parts) < 2 {
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]") fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val|reg|*addr>]")
continue continue
} }
var addr uint64 var addr uint64
@@ -278,7 +278,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
} }
} }
} else if len(parts) >= 4 && parts[2] == "if" { } else if len(parts) >= 4 && parts[2] == "if" {
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg|*addr>") fmt.Println("usage: break <label|addr|line> if <reg> <op> <value|reg|*addr>")
continue continue
} }
bp, err := bm.SetWithCond(addr, label, cond) bp, err := bm.SetWithCond(addr, label, cond)
@@ -422,7 +422,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
fmt.Println() fmt.Println()
case "help", "h", "?": case "help", "h", "?":
fmt.Printf(` break <label|addr> [if <reg> <op> <val|reg|*addr>] fmt.Printf(` break <label|addr|line> [if <reg> <op> <val|reg|*addr>]
set a breakpoint, optionally conditional on a set a breakpoint, optionally conditional on a
register compared to a constant, a register, or the register compared to a constant, a register, or the
8-byte word at *addr 8-byte word at *addr
+70 -42
View File
@@ -13,11 +13,13 @@ Three design goals shape everything below.
So the centre of the toolkit is a hand-written lexer and a parser that So the centre of the toolkit is a hand-written lexer and a parser that
produce a typed AST with source positions on every node. produce a typed AST with source positions on every node.
2. **Architecture as data, not code.** Per-architecture differences (amd64, 2. **Architecture as data, not code.** Per-architecture differences (amd64,
arm64, riscv64, loong64) live in register and instruction *tables* (`arch`), arm64, riscv64, loong64) live in register and instruction *tables* (`arch`)
never in `if arch == …` branches scattered through the logic. The and per-architecture encoders, rather than in `if arch == …` branches
instruction tables are generated from the Go toolchain's own assembler threaded through the analysis; the arch tests that remain are dispatch and
source (`just gen`), so adding or refreshing an architecture is a data policy points, such as which encoder a file name selects and which
operation, not a coding one. registers the liveness pass audits. The instruction tables are generated
from the Go toolchain's own assembler source (`just gen`), so refreshing an
architecture is a data operation, not a coding one.
3. **Open integration surface.** Everything the toolkit can do is reachable 3. **Open integration surface.** Everything the toolkit can do is reachable
through two vendor-neutral interfaces: a CLI and an LSP server. No editor through two vendor-neutral interfaces: a CLI and an LSP server. No editor
owns the toolkit; the toolkit is offered to editors on standard terms. owns the toolkit; the toolkit is offered to editors on standard terms.
@@ -35,10 +37,19 @@ flowchart TD
ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT ARCH["arch tables<br/>amd64 / arm64 / riscv64 / loong64"] --> LINT
ARCH --> LSP ARCH --> LSP
LINT --> LSP LINT --> LSP
PAR --> ASM["asm<br/>encoders, image, object emitters"]
ASM --> VER["verify<br/>JIT mapping, ABI checks, fuzzing"]
ASM --> DBG["debug<br/>ptrace session"]
VER --> DBG
DIS["disasm<br/>golang.org/x/arch"] --> DBG
FMT --> CLI["gasm CLI"] FMT --> CLI["gasm CLI"]
LINT --> CLI LINT --> CLI
PAR --> CLI PAR --> CLI
LEX --> CLI LEX --> CLI
ASM --> CLI
VER --> CLI
DBG --> CLI
DIS --> CLI
LSP --> EDITOR["any LSP editor"] LSP --> EDITOR["any LSP editor"]
``` ```
@@ -70,9 +81,11 @@ assembler provides.
The boundaries matter as much as the responsibilities: `ast` records syntax The boundaries matter as much as the responsibilities: `ast` records syntax
only, and whether a name is a register or a label is left to `arch`, so the only, and whether a name is a register or a label is left to `arch`, so the
parser stays architecture-agnostic. `asm` and `verify` are the only packages parser stays architecture-agnostic. `asm` produces the machine code, `verify`
that touch machine code and executable memory, and `cmd/gasm` owns no logic and `debug` are the two packages that map it executable (read-execute in
beyond flags and output. `verify`, read-write-execute in the debuggee), and `cmd/gasm` is the CLI, with
the verify sweep orchestration and the audit, scaffold and unified-diff
helpers beside its flags and output.
### `token` and `lexer` ### `token` and `lexer`
@@ -112,14 +125,17 @@ Register files are generated programmatically (the regular `R8`-`R15`,
`X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly `X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly
named registers listed explicitly. Instruction names are **generated from the named registers listed explicitly. Instruction names are **generated from the
Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`, Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`,
plus the common opcodes and the per-architecture front-end aliases such as the plus the common opcodes in `cmd/internal/obj/util.go`) by `just gen`, so the
arm64 `B`/`BL` branches and the `.P`/`.W` load-store addressing suffixes) by tables always match what the real assembler accepts. The spellings the
`just gen`, so the tables always match what the real assembler accepts. Each toolchain's tables do not carry are hand-maintained instead: the front-end
mnemonic maps to a summary and an optional operand-count range; counts are alias lists in `arch/arm64.go`, `arch/amd64.go` and `arch/loong64.go` (the
recorded only where unambiguous (`-1` disables the operand-count lint for that arm64 `B`/`BL` branches among them), and the arm64 `.P`/`.W` load-store suffix
instruction) so the linter stays silent rather than guess. For architectures stripping in `arch/arch.go`. Each mnemonic maps to a summary and an optional
with highly variable operand forms (arm64, riscv64, loong64) only a few operand-count range; counts are recorded only where unambiguous (`-1`
fixed-arity instructions (`RET`, `NOP`, `JMP`, `CALL`) carry counts at all. disables the operand-count lint for that instruction) so the linter stays
silent rather than guess. For architectures with highly variable operand
forms (arm64, riscv64, loong64) `relaxCounts` clears those counts, leaving
`RET` and `NOP` with a range (`RET` alone on riscv64).
### `lint` ### `lint`
@@ -291,11 +307,11 @@ registers are translated onto the hardware stack pointer: `x+N(FP)` becomes
pointer is set up, with the matching Go prologue/epilogue generated, so the pointer is set up, with the matching Go prologue/epilogue generated, so the
output is byte-identical to the Go assembler for these cases. SIMD is handled output is byte-identical to the Go assembler for these cases. SIMD is handled
by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM
registers) across eight operand forms: the three-operand NDS form, the registers) over nine operand forms plus a dedicated move encoder: the
two-operand reg/rm form, the immediate-shift form (plus the variable-count three-operand NDS form, the two-operand reg/rm form, the immediate-shift form
shifts, which share the NDS shape with the count in an XMM register or (plus the variable-count shifts, which share the NDS shape with the count in
memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the an XMM register or memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`),
three-operand-plus-immediate form (`VSHUFPD`, the three-operand-plus-immediate form (`VSHUFPD`,
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`, `VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or `VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
@@ -340,10 +356,10 @@ b bit and the L'L rounding-control field (broadcast keeps the vector length
and scales disp8 by the element size), and combine with the .Z zeroing and scales disp8 by the element size), and combine with the .Z zeroing
suffix. Every encoding is validated two ways: by suffix. Every encoding is validated two ways: by
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
the machine code the real Go assembler emits, a comparison that holds for the machine code the real Go assembler emits; the parity suites carry that
whole functions: all 27 functions of both kernels assemble to exactly the Go comparison over whole kernel files on all four architectures, with the
toolchain's bytes, the lone exception being the displacements of the relocation fields masked because the Go linker fills those displacements at
static-constant loads, which the Go linker fills at link time. link time.
File-level assembly (`AssembleFile`) goes beyond single functions: it File-level assembly (`AssembleFile`) goes beyond single functions: it
materialises the file's static symbols (`GLOBL`/`DATA`) in a data section materialises the file's static symbols (`GLOBL`/`DATA`) in a data section
@@ -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 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 `X27`, loong64 `R22`; the latter two keep no hardware frame pointer) and the
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
saved registers before Go code resumes. riscv64 is validated end to saved registers before Go code resumes. All three non-amd64 trampolines
end under qemu-user emulation; arm64 shares the same stack convention and are validated end to end under qemu-user emulation, the loong64 one
fix; loong64 stays ground-truth-only until hardware validation. through its raw-address leave handoff.
`gasm verify` runs the JIT checks when the host `gasm verify` runs the JIT checks when the host
matches the kernel's architecture and the toolchain comparisons matches the kernel's architecture and the toolchain comparisons
elsewhere. 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 available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
arguments to confirm the trampoline round-trips. The `-smoke` and `-abi` arguments to confirm the trampoline round-trips. The `-smoke` and `-abi`
sweeps run in parallel and each inside a child process, so a function that sweeps run in parallel and each inside a child process, so a function that
faults is reported without ending the sweep. `gasm verify --fuzz` combines faults is reported without ending the sweep; `-abi` is where the ABI check
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz lives, fuzzing each function with sentinel values in the registers the Go ABI
testing, comparing the JIT-assembled kernel against the portable Go reference fixes across calls and a canary below `SP`, and reporting a violation on any
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz iteration. `gasm verify --fuzz` is the differential campaign instead: it
JIT-loads the kernel and the `go tool asm` build of the same kernel and
compares the output argument areas bit-for-bit, one child process per function
so a crash on a partial function is reported rather than fatal. When a fuzz
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
for reproducibility. `gasm verify --call <func> --buf name:size:pattern` for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
invokes a single function with user-supplied buffers (patterns: zero, ones, invokes a single function with user-supplied buffers (patterns: zero, ones,
@@ -449,16 +468,25 @@ masked), reporting any encoding drift.
The interactive debugger (all four architectures). It launches the target The interactive debugger (all four architectures). It launches the target
function in a child process that maps the JIT code, calls function in a child process that maps the JIT code, calls
`PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls `PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls
execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem` execution. Breakpoints are patched through `/proc/pid/mem`: the one-byte
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime). `INT3` on amd64, the four-byte break instruction on the other three (arm64
`BRK #0`, riscv64 `ebreak`, loong64 `break 0`).
The child pins its goroutine to the OS thread with `runtime.LockOSThread` The child pins its goroutine to the OS thread with `runtime.LockOSThread`
so the traced thread is the one executing JIT code. The REPL provides so the traced thread is the one executing JIT code. The REPL provides
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`), single-step, register inspection (the GPRs on every architecture; on amd64 the
XMM set through `PTRACE_GETFPREGS` and the YMM set through `PTRACE_GETREGSET`
on `NT_X86_XSTATE`; on the other three the FP/SIMD regset through
`PTRACE_GETREGSET` on `NT_PRFPREG`),
label resolution, named buffer allocation with pattern filling label resolution, named buffer allocation with pattern filling
(`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint (`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint
management. Breakpoints accept conditions management. Breakpoints accept conditions
(`break <label> if <reg> <op> <val>`, including register-against-register (`break <label> if <reg> <op> <val>`, including register-against-register
comparisons), and hardware watchpoints work on all four architectures. comparisons), and hardware watchpoints work on amd64 (the DR0-DR3 debug
registers), arm64 (`NT_ARM_HW_WATCH`) and loong64 (`NT_LOONGARCH_HW_WATCH`);
riscv64 reports that its kernel ptrace interface exposes no trigger regset.
The ptrace path is validated at run time on amd64, where the session tests are
built; arm64, riscv64 and loong64 compile and are covered by the
architecture-neutral units (label and line tables, the breakpoint manager).
For non-interactive use, `--script` runs REPL commands from a file (or For non-interactive use, `--script` runs REPL commands from a file (or
stdin) and exits, `--timeout` kills the debuggee when a run hangs (the stdin) and exits, `--timeout` kills the debuggee when a run hangs (the
watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot
@@ -499,9 +527,10 @@ sequenceDiagram
Errors are produced where the parse or the encoding fails and become values at Errors are produced where the parse or the encoding fails and become values at
the CLI boundary: the parser returns a diagnostic list and never aborts a file, the CLI boundary: the parser returns a diagnostic list and never aborts a file,
`AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr `AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr
and returns a non-zero exit code. The formatter and the linter take the same and returns a non-zero exit code. The formatter and the linter take different
AST by a different route: `gasm fmt` re-spaces the token stream and `gasm lint` inputs from the assembler: `gasm fmt` re-spaces the token stream
walks the parsed file, so neither depends on an encoding. (`format.Source` lexes the source text itself) and `gasm lint` walks the parsed
AST, so neither depends on an encoding.
## State and lifetime ## State and lifetime
@@ -522,9 +551,8 @@ walks the parsed file, so neither depends on an encoding.
## Dependencies ## Dependencies
- **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the - **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the
disassembler backend (`gasm dis` and the debugger's listings) and the source disassembler backend (`gasm dis` and the debugger's listings). The tests
of the register metadata the encoder consults (`asm/reg.go`, `asm/vex.go`). additionally decode through it to validate the encodings.
The tests additionally decode through it to validate the encodings.
- **The Go toolchain**, as an oracle and never as a library: `go tool asm` - **The Go toolchain**, as an oracle and never as a library: `go tool asm`
supplies the object preamble and the ground truth for `gasm verify supplies the object preamble and the ground truth for `gasm verify
--ground-truth`, `go list -json -export` locates the archives of the packages --ground-truth`, `go list -json -export` locates the archives of the packages
+41 -27
View File
@@ -3,9 +3,11 @@
The reference below is taken from the program's own `--help`. If the two disagree, the The reference below is taken from the program's own `--help`. If the two disagree, the
program is right and this file is a defect. program is right and this file is a defect.
The same reference is installed as man pages: `just install-man` puts gasm(1) and one The same reference is installed as man pages: `just install-man` puts gasm(1) and a page
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each for every command except `version` (which gasm(1) itself documents) into
page against the binary so the two cannot drift apart. ~/.local/share/man (`MANDIR` overrides). A test in `cmd/gasm` keeps the two from
drifting: it compares each page's flag set and SYNOPSIS line with the binary's own `-h`
output, and gasm(1)'s COMMANDS list with the top-level help. The prose is not compared.
## Synopsis ## Synopsis
@@ -58,7 +60,8 @@ Usage: gasm parse <file>
``` ```
Parse FILE and report syntax errors on stderr. On success, print how many Parse FILE and report syntax errors on stderr. On success, print how many
declarations and TEXT functions the file contains. declarations and TEXT functions the file contains. FILE may be `-` to read
standard input.
```sh ```sh
gasm parse hello_amd64.s gasm parse hello_amd64.s
@@ -155,7 +158,9 @@ recognisable suffix (most of GOROOT's, for example `cpu_x86.s`) are
assembled. `raw` concatenates the functions and the data section into one assembled. `raw` concatenates the functions and the data section into one
self-consistent image; `elf` emits a relocatable object that links with the self-consistent image; `elf` emits a relocatable object that links with the
system toolchain; `goobj` emits the Go toolchain's own object format, which system toolchain; `goobj` emits the Go toolchain's own object format, which
`cmd/link` consumes directly. `cmd/link` consumes directly, and is the one format that needs the toolchain
installed: the object preamble is captured from `go tool asm` and the format
version from `go version`. `raw` and `elf` need no toolchain at all.
```sh ```sh
gasm asm hello_amd64.s gasm asm hello_amd64.s
@@ -208,7 +213,7 @@ Usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <f
| `--abi-n` | 100 | ABI check iterations with varied inputs | | `--abi-n` | 100 | ABI check iterations with varied inputs |
| `--profile` | off | list the basic-block structure per function | | `--profile` | off | list the basic-block structure per function |
| `--smoke` | off | call each NOSPLIT function with zeroed arguments | | `--smoke` | off | call each NOSPLIT function with zeroed arguments |
| `--call` | empty | invoke a single function with `--buf` instead of the sweeps | | `--call` | empty | invoke a single NOSPLIT function with `--buf` instead of the sweeps |
| `--buf` | empty | buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` | | `--buf` | empty | buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
| `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) | | `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
| `--repeat` | 1 | number of times to repeat a `--call` invocation | | `--repeat` | 1 | number of times to repeat a `--call` invocation |
@@ -219,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 toolchain comparison works everywhere. `--fuzz`, `--smoke` and `--abi` run each
function in its own child process, so a partial function that faults on random function in its own child process, so a partial function that faults on random
input is reported as `CRASH` instead of ending the sweep; `--call` with `--buf` input is reported as `CRASH` instead of ending the sweep; `--call` with `--buf`
invokes such a function with valid data. loong64 stays on the ground-truth path invokes such a function with valid data. The function named by `--call` must be
until hardware validation. NOSPLIT: a function with a stack frame is refused with a diagnostic and exits 1.
```sh ```sh
gasm verify --ground-truth hello_amd64.s gasm verify --ground-truth hello_amd64.s
@@ -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 | | `-timeout` | 0 | kill the debuggee after this duration, for headless `-script` runs; a timeout exits 3 |
| `-cover` | off | run to completion with a breakpoint on every instruction and report which executed | | `-cover` | off | run to completion with a breakpoint on every instruction and report which executed |
The debugger spawns the debuggee from the `gasm` binary on `$PATH`, so install The debugger re-executes the binary it is running as (`os.Executable()`) for the
it first with `just install`; `go run` does not work for the traced child. traced child, so the child is the same `gasm`, whether it is installed on `$PATH`
Requires Linux (ptrace) and all four architectures are supported. or run with `go run ./cmd/gasm`; nothing has to be installed first. Requires
Linux (ptrace), and all four architectures are supported.
REPL commands: REPL commands:
| Command | Effect | | Command | Effect |
|---|---| |---|---|
| `break <label\|addr> [if <reg> <op> <val>]` | set a breakpoint, optionally conditional | | `break <label\|addr\|line> [if <reg> <op> <val\|reg\|*addr>]`, `b` | set a breakpoint; the condition compares a register with a constant, another register or the 8-byte word at `*addr` |
| `delete <label\|addr>` | remove a breakpoint | | `delete <label\|addr>`, `d` | remove a breakpoint |
| `info break` | list the breakpoints | | `info break`, `info breakpoints`, `info b` | list the breakpoints |
| `step [n]`, `s` | single-step n instructions | | `step [n]`, `s` | single-step n instructions |
| `next`, `n` | step over a CALL | | `next`, `n` | step over a CALL |
| `finish`, `fin` | run until the function returns | | `finish`, `fin` | run until the function returns |
@@ -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 Compare the gasm encoder for the given architecture (default amd64) against the
installed `go tool asm` and print the diff: superset encodings (gasm-only installed `go tool asm` and print the diff: superset encodings (gasm-only
spellings, shippable via `gasm asm --format goobj`), known-but-unencodable spellings, shippable via `gasm asm --format goobj`) and known-but-unencodable
names (the encoder backlog) and go-only names (feature gaps). The Go side is names (the encoder backlog). The Go side is probed black-box one bare mnemonic
probed black-box with a battery of operand shapes per mnemonic, so the audit at a time, classified by the toolchain's diagnostic for an instruction it does
tracks whatever toolchain `go env GOROOT` provides. On non-amd64 not know, so the audit tracks whatever toolchain `go env GOROOT` provides; the
architectures the backlog is an over-approximation: a name counts as encodable gasm side answers from the encoder table on amd64 and from trial assembly over a
only when a probe shape assembles cleanly, so a name whose real forms the battery of operand shapes on the other architectures. On non-amd64
battery misses lands in the backlog. architectures the backlog is therefore an over-approximation: a name counts as
encodable only when a probe shape assembles cleanly, so a name whose real forms
the battery misses lands in the backlog. Names the toolchain knows and gasm does
not cannot be enumerated by probing at all, because Go's table is visible only
through names already in the gasm table; the report closes with a note saying
so rather than listing them.
```sh ```sh
gasm audit-instructions amd64 gasm audit-instructions amd64
@@ -361,8 +372,9 @@ gasm audit-instructions amd64
```text ```text
gasm table (amd64, families excluded): 1542 mnemonics gasm table (amd64, families excluded): 1542 mnemonics
gasm encodable: 580 go tool asm recognized: 1542 gasm encodable: 587 go tool asm recognised: 1542
shared: 580 shared: 587
...
``` ```
With `--corpus` the audit changes shape: it assembles every `.s` file under With `--corpus` the audit changes shape: it assembles every `.s` file under
@@ -382,10 +394,12 @@ gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
```text ```text
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures) corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
assemble for every target architecture: 108 (17.2%) assemble for every target architecture: 127 (20.3%)
amd64: 77/464 attempted amd64: 82/464 attempted
148 instruction not encodable 165 unsupported operand form
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s
109 instruction not encodable
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386.s
... ...
``` ```
@@ -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: Find which labels a failing kernel reaches, headlessly:
```sh ```sh
gasm debug --func decodeBlockAVX2 --cover --script cmds.txt --timeout 30s kernel_amd64.s gasm debug --func decodeBlockAVX2 --cover --timeout 30s kernel_amd64.s
``` ```
+24 -7
View File
@@ -6,9 +6,15 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
- **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares - **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares
- **just**, the command runner; every task below is a just recipe - **just**, the command runner; every task below is a just recipe
- **A C compiler** (`gcc`): `just race` runs the suite under the race detector,
which needs cgo
- **Perl**: the `test`, `fmt-check`, `install-man` and `uninstall-man` recipes
are Perl programs
- **`gzip`**: `install-man` compresses the man pages with it
- A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace - A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace
and the JIT checks of `gasm verify` need executable memory and the JIT checks of `gasm verify` need executable memory
- No external dependencies beyond the Go toolchain - **`golang.org/x/arch`**, the one module dependency, which the Go toolchain
fetches; nothing else sits outside the standard library
## Setup ## Setup
@@ -25,8 +31,9 @@ Every recipe in the `justfile`, and what it does.
| Recipe | What it does | | Recipe | What it does |
|---|---| |---|---|
| `default` (bare `just`) | prints the recipe list (`@just --list`) |
| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings | | `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings |
| `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor | | `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile |
| `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` | | `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` |
| `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly | | `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly |
| `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one | | `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one |
@@ -36,7 +43,7 @@ Every recipe in the `justfile`, and what it does.
| `just vet` | both static gates: `go vet` and `go fix -diff` | | `just vet` | both static gates: `go vet` and `go fix -diff` |
| `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done | | `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done |
| `just clean` | removes the build artefacts, `bin/` and `coverage.out` | | `just clean` | removes the build artefacts, `bin/` and `coverage.out` |
| `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`); `gasm debug` needs an installed binary, because it spawns the debuggee from `$PATH` | | `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`) |
| `just uninstall` | removes the installed binary from `bindir` | | `just uninstall` | removes the installed binary from `bindir` |
| `just install-man` | installs the man pages under `docs/man` into `~/.local/share/man/man1` (`MANDIR` overrides), gzip-compressed; not a gate | | `just install-man` | installs the man pages under `docs/man` into `~/.local/share/man/man1` (`MANDIR` overrides), gzip-compressed; not a gate |
| `just uninstall-man` | removes the installed man pages | | `just uninstall-man` | removes the installed man pages |
@@ -55,9 +62,18 @@ go test -count=1 -timeout 10m -coverprofile=coverage.out \
The suite runs over the logic packages (`-count=1`, so no cached pass The suite runs over the logic packages (`-count=1`, so no cached pass
counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser, counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser,
token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI
glue, so both sit outside the sweep, and a thin `cmd/` in it would drag glue, so both sit outside the profile sweep, and a thin `cmd/` in it
the coverage total under the floor. The floor fails if the total is would drag the coverage total under the floor. Their tests still run, in
below 80 %. CI runs the same command with the same ten-minute bound, so a second invocation without a profile:
```sh
go test -count=1 -timeout 10m ./cmd/... ./debug/...
```
That covers the CLI's exit codes and the guard that compares the manual
pages with the binary's own help, and the debugger's architecture-neutral
units. The floor fails if the total is below 80 %. CI runs the same two
commands with the same ten-minute bound, so
the number is the same everywhere. the number is the same everywhere.
### `just run` ### `just run`
@@ -131,7 +147,8 @@ therefore the fastest way to a green pipeline.
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`, Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`,
which triggers the release workflow: it builds the portable Linux targets, which triggers the release workflow: it builds the portable Linux targets,
takes the notes from the matching `CHANGELOG.md` section and uploads the takes the notes from the matching `CHANGELOG.md` section and uploads the
assets. assets. `SECURITY.md` carries the supported-versions table, so that table
moves with the release; the pipeline refuses a tag the policy does not name.
The version is never injected. `gasm --version` prints what the The version is never injected. `gasm --version` prints what the
toolchain recorded in the build information: the tag on a tagged toolchain recorded in the build information: the tag on a tagged
+17 -6
View File
@@ -20,13 +20,24 @@ flag selects what is written:
self-consistent image; self-consistent image;
.B elf .B elf
emits a relocatable object (.text/.data sections, a symbol table and emits a relocatable object (.text/.data sections, a symbol table and
one PC32 relocation per static-symbol reference) that links with the one relocation per static-symbol reference, in the architecture's own
form: R_X86_64_PC32 on amd64, R_AARCH64_*, R_RISCV_* or R_LARCH_* on the
others) that links with the
system toolchain; system toolchain;
.B goobj .B goobj
emits the Go toolchain's own object format, which cmd/link consumes emits the Go toolchain's own object format, which cmd/link consumes
directly (it requires directly (it requires
.BR \-p , .BR \-p ,
the package path, and the installed Go toolchain). the package path, and the installed Go toolchain: the object preamble is
captured from
.B go tool asm
and the format version from
.BR "go version" ).
.PP
.B raw
and
.B elf
need no toolchain at all.
.PP .PP
Framed functions receive the stack-split guard and the trailing Framed functions receive the stack-split guard and the trailing
morestack block, byte-identical to the toolchain's output, so split morestack block, byte-identical to the toolchain's output, so split
@@ -51,10 +62,10 @@ Exits 0 on success, 1 when parsing or assembly fails, and 2 on a usage
error. error.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm asm \-o hello.bin hello_amd64.s raw image gasm asm \-o hello.bin hello_amd64.s raw image
gasm asm \-\-format elf \-o k.o k.s linkable ELF object gasm asm \-\-format elf \-o k.o k_amd64.s linkable ELF object
gasm asm \-\-format goobj \-p pkg/path \-o k.o k.s Go object for go build gasm asm \-\-format goobj \-p pkg/path \-o k.o k_amd64.s Go object for go build
gasm asm \-GOARCH amd64 cpu_x86.s arch override gasm asm \-GOARCH amd64 cpu_x86.s arch override
.fi .fi
.SH SEE ALSO .SH SEE ALSO
.BR gasm (1), .BR gasm (1),
+10 -5
View File
@@ -8,12 +8,17 @@ Compare the gasm encoder for the given architecture (default amd64)
against against
.B go tool asm .B go tool asm
and print the diff: superset encodings (gasm-only, shippable via and print the diff: superset encodings (gasm-only, shippable via
.BR "gasm asm \-\-format goobj" ), .BR "gasm asm \-\-format goobj" )
known-but-unencodable names (the backlog) and go-only names (feature and known-but-unencodable names (the backlog). The Go side is probed
gaps). The Go side is probed black-box with a battery of bare black-box one bare mnemonic at a time, so the audit tracks whatever
mnemonics, so the audit tracks whatever toolchain toolchain
.B go env GOROOT .B go env GOROOT
provides. provides; the gasm side answers from the encoder table on amd64 and from
trial assembly over a battery of operand shapes elsewhere. Names
.B go tool asm
knows and gasm does not cannot be enumerated by probing, because Go's
table is visible only through names already in the gasm table; the report
closes with a note saying so rather than listing them.
.PP .PP
With With
.BR \-\-corpus , .BR \-\-corpus ,
+7 -5
View File
@@ -17,14 +17,16 @@ runs to completion with a breakpoint on every instruction and reports
which executed and how often, the label-level coverage view. which executed and how often, the label-level coverage view.
.SH REPL COMMANDS .SH REPL COMMANDS
.TP .TP
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR] .B break \fIlabel|addr|line\fR [\fBif \fIreg op val|reg|*addr\fR], b
Set a breakpoint, optionally conditional on a register comparison Set a breakpoint at a label, an address or a source line number, optionally
(register against register or immediate). conditional on a register comparison: against a constant, against another
register, or against the 8-byte word at
.BR *addr .
.TP .TP
.B delete \fIlabel|addr\fR .B delete \fIlabel|addr\fR, d
Remove a breakpoint. Remove a breakpoint.
.TP .TP
.B info break .B info break, info breakpoints, info b
List all breakpoints. List all breakpoints.
.TP .TP
.BR step " [" n ], " s .BR step " [" n ], " s
+1 -1
View File
@@ -28,7 +28,7 @@ differs; a usage error exits 2.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm diff hello_amd64.s hello_amd64.s gasm diff hello_amd64.s hello_amd64.s
gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2_amd64.s avx512_amd64.s
.fi .fi
.SH SEE ALSO .SH SEE ALSO
.BR gasm (1), .BR gasm (1),
+1 -1
View File
@@ -28,7 +28,7 @@ Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
error. error.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm dis k.s assemble, then list each function gasm dis k_amd64.s assemble, then list each function
gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin
.fi .fi
.SH SEE ALSO .SH SEE ALSO
+7
View File
@@ -41,6 +41,13 @@ List files whose formatting differs from gasm's.
.TP .TP
.B \-w .B \-w
Write the result to the source file. Write the result to the source file.
.SH EXIT STATUS
Exits 0 on success, 1 when a path cannot be read or written, and 2 on a
usage error (combining
.B \-l
and
.BR \-d ,
or an unknown flag).
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm fmt reformat every .s below here gasm fmt reformat every .s below here
+17 -5
View File
@@ -33,7 +33,11 @@ The function can fall off its end without a terminator.
TEXT flags are used without including textflag.h. TEXT flags are used without including textflag.h.
.TP .TP
.B abi-argsize .B abi-argsize
The declared frame or argument size disagrees with the The declared argument area (the
.I \-args
part of
.IR $frame\-args )
disagrees with the
.B //\ function .B //\ function
signature. signature.
.TP .TP
@@ -52,7 +56,8 @@ FUNCDATA and PCDATA indices are malformed.
A label no jump reaches. A label no jump reaches.
.TP .TP
.B invalid-textflag .B invalid-textflag
A TEXT flag combination the toolchain rejects. An unknown TEXT or GLOBL flag, reported one flag at a time; numeric flags
are accepted as textflag.h constants.
.TP .TP
.B stack-imbalance .B stack-imbalance
The function does not restore the stack pointer on every path. The function does not restore the stack pointer on every path.
@@ -61,11 +66,18 @@ The function does not restore the stack pointer on every path.
An operand register has the wrong width for the instruction. An operand register has the wrong width for the instruction.
.TP .TP
.B abi0-register-args .B abi0-register-args
A call passes arguments in registers where ABI0 expects the stack A function whose
frame. .B //\ function
parameters are never read from their
.IR name+offset(FP)
frame slots, which usually means the body takes its arguments from
registers instead.
.TP .TP
.B nonportable-register-name .B nonportable-register-name
A register spelling that does not exist on the target architecture. An amd64 register alias gasm accepts but
.B go tool asm
rejects (the RAX/EAX family); the canonical spelling is named in the
diagnostic.
.TP .TP
.B unencodable-instruction .B unencodable-instruction
The mnemonic is known to the table but the encoder cannot assemble it The mnemonic is known to the table but the encoder cannot assemble it
+4 -3
View File
@@ -6,9 +6,10 @@ gasm-profile \- show the basic-block structure of functions
.SH DESCRIPTION .SH DESCRIPTION
Show the basic-block structure of functions in an assembly file: each Show the basic-block structure of functions in an assembly file: each
function's labels, their offsets, and the block boundaries. This is function's labels, their offsets, and the block boundaries. This is
the static structure; for runtime execution counts, use the static structure; for runtime execution counts use
.BR "gasm verify \-fuzz" , .BR "gasm debug \-\-cover" ,
which exercises the code paths. and for input coverage
.BR "gasm verify \-\-fuzz" .
.SH EXIT STATUS .SH EXIT STATUS
Exits 0 on success and 1 when the file cannot be assembled. Exits 0 on success and 1 when the file cannot be assembled.
.SH SEE ALSO .SH SEE ALSO
+6 -6
View File
@@ -20,8 +20,7 @@ With
each function is called with sentinel values in the registers the Go each function is called with sentinel values in the registers the Go
ABI fixes across calls (the frame pointer and the goroutine pointer) ABI fixes across calls (the frame pointer and the goroutine pointer)
plus a canary below SP; violations are reported. JIT-based checks run plus a canary below SP; violations are reported. JIT-based checks run
when the host matches the file's architecture (all but loong64, which when the host matches the file's architecture, on all four architectures.
is ground-truth only for now).
.PP .PP
With With
.BR \-fuzz , .BR \-fuzz ,
@@ -46,7 +45,8 @@ a single function is invoked with user-supplied buffers
.RB ( \-buf ) .RB ( \-buf )
instead of the smoke/abi/fuzz sweeps. Useful for partial functions instead of the smoke/abi/fuzz sweeps. Useful for partial functions
(e.g. decoders) that crash on random input but should succeed on valid (e.g. decoders) that crash on random input but should succeed on valid
data. data. The function named must be NOSPLIT: a function with a stack frame
is refused with a diagnostic and exits 1.
.PP .PP
With With
.B \-save\-corpus .B \-save\-corpus
@@ -74,7 +74,7 @@ Buffer spec for -call: name:size:pattern[,name:size:pattern] where
pattern is zero, ones, seq, or hex. pattern is zero, ones, seq, or hex.
.TP .TP
.B \-call \fIname\fR .B \-call \fIname\fR
Call a single function with -buf instead of the sweeps. Call a single NOSPLIT function with -buf instead of the sweeps.
.TP .TP
.B \-fuzz .B \-fuzz
Differential fuzz: JIT both the gasm and the go-tool-asm versions and Differential fuzz: JIT both the gasm and the go-tool-asm versions and
@@ -108,8 +108,8 @@ a file that cannot be assembled exits 1 and a usage error exits 2.
.SH EXAMPLES .SH EXAMPLES
.nf .nf
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
gasm verify \-\-ground\-truth k.s gasm verify \-\-ground\-truth k_amd64.s
gasm verify \-\-fuzz \-n 500 k.s gasm verify \-\-fuzz \-n 500 k_amd64.s
.fi .fi
.SH SEE ALSO .SH SEE ALSO
.BR gasm (1), .BR gasm (1),
+8 -4
View File
@@ -17,11 +17,15 @@ bundles a lexer, parser, formatter, linter, standalone assembler and
language server for Plan 9 assembly into one self-contained binary. It language server for Plan 9 assembly into one self-contained binary. It
serves two purposes: it brings developer tooling to the serves two purposes: it brings developer tooling to the
.I .s .I .s
files of Go programs, and it assembles Plan 9 assembly without the Go files of Go programs, and it assembles Plan 9 assembly to raw images or
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug linkable ELF objects with DWARF5 debug sections without the Go toolchain at
sections, or the Go toolchain's own GOOBJ format, which all, plus the Go toolchain's own GOOBJ format, which
.B go build .B go build
consumes directly. consumes directly. GOOBJ is the one format that needs the toolchain
installed: the object preamble is captured from
.B go tool asm
and the format version from
.BR "go version" .
.PP .PP
Four architectures are covered: amd64 (including VEX/AVX2 and Four architectures are covered: amd64 (including VEX/AVX2 and
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
+7
View File
@@ -31,6 +31,13 @@ test:
system(q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m}, system(q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m},
q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0 q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0
or die qq{the test suite failed\n}; 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: $!}; open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
my $total; my $total;
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} } while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
+2 -2
View File
@@ -22,9 +22,9 @@ TEXT ·dirtyFP(SB), NOSPLIT, $0-16
RET RET
// func dirtyG(a int64) int64 // 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 TEXT ·dirtyG(SB), NOSPLIT, $0-16
MOVD $0x5678, R28 MOVD $0x5678, g
MOVD a+0(FP), R0 MOVD a+0(FP), R0
MOVD R0, ret+8(FP) MOVD R0, ret+8(FP)
RET RET
+2 -2
View File
@@ -13,9 +13,9 @@ TEXT ·cleanAdd(SB), NOSPLIT, $0-24
RET RET
// func dirtyG(a int64) int64 // 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 TEXT ·dirtyG(SB), NOSPLIT, $0-16
MOVV $0x5678, R22 MOVV $0x5678, g
MOVV a+0(FP), R4 MOVV a+0(FP), R4
MOVV R4, ret+8(FP) MOVV R4, ret+8(FP)
RET RET
+2 -2
View File
@@ -13,9 +13,9 @@ TEXT ·cleanAdd(SB), NOSPLIT, $0-24
RET RET
// func dirtyG(a int64) int64 // 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 TEXT ·dirtyG(SB), NOSPLIT, $0-16
MOV $0x5678, X27 MOV $0x5678, g
MOV a+0(FP), X5 MOV a+0(FP), X5
MOV X5, ret+8(FP) MOV X5, ret+8(FP)
RET RET
+8 -2
View File
@@ -37,7 +37,10 @@ TEXT ·imm(SB), NOSPLIT, $0-0
MOVV $0x12345, R17 MOVV $0x12345, R17
RET 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 TEXT ·branch(SB), NOSPLIT, $0-0
BEQ R4, R5, done BEQ R4, R5, done
BNE R6, R7, skip BNE R6, R7, skip
@@ -50,7 +53,10 @@ skip:
JMP loop JMP loop
loop: loop:
JAL skip JAL fin
RET
fin:
RET RET
done: done:
+69
View File
@@ -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
+81
View File
@@ -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
+39
View File
@@ -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
View File
@@ -42,8 +42,12 @@ func TestABIArm64(t *testing.T) {
t.Errorf("cleanAdd: %s", report) t.Errorf("cleanAdd: %s", report)
} }
// dirtyFP clobbers the frame pointer (R29). // dirtyFP clobbers the frame pointer (R29). The kernel passes its
out, report, err = k.CallFuncChecked("dirtyFP", make([]byte, 16)) // 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 { if err != nil {
t.Fatalf("CallFuncChecked: %v", err) t.Fatalf("CallFuncChecked: %v", err)
} }
@@ -57,11 +61,17 @@ func TestABIArm64(t *testing.T) {
t.Errorf("dirtyFP: only R29 should be clobbered: %s", report) t.Errorf("dirtyFP: only R29 should be clobbered: %s", report)
} }
// dirtyG clobbers the goroutine pointer (R28). // dirtyG clobbers the goroutine pointer (R28) and still returns its
_, report, err = k.CallFuncChecked("dirtyG", make([]byte, 16)) // argument.
args = make([]byte, 16)
PutUint64(args, 0, 0x5678)
out, report, err = k.CallFuncChecked("dirtyG", args)
if err != nil { if err != nil {
t.Fatalf("CallFuncChecked: %v", err) 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 { if !report.GClobbered {
t.Error("dirtyG: expected g clobbered, but report says clean") t.Error("dirtyG: expected g clobbered, but report says clean")
} }
+7 -4
View File
@@ -2,13 +2,16 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package verify provides the dynamic-analysis substrate for gasm: it // Package verify provides the dynamic-analysis substrate for gasm: it
// JIT-assembles Plan 9 amd64 kernels into executable memory and calls them // JIT-assembles Plan 9 kernels for all four supported architectures
// (amd64, arm64, riscv64, loong64) into executable memory and calls them
// directly, enabling differential testing against portable Go references, // directly, enabling differential testing against portable Go references,
// runtime ABI checks and basic-block coverage profiling. // runtime ABI checks and basic-block coverage profiling.
// //
// The execution model is pure Go (stdlib only): machine code is mapped with // The execution model is pure Go, stdlib only, with no cgo: machine code is
// syscall.Mmap and invoked through an assembly trampoline that switches to a // mapped with syscall.Mmap and invoked through an assembly trampoline that
// prepared ABI0 stack. No cgo, no external toolchain. // switches to a prepared ABI0 stack. The toolchain-comparison helpers in
// this package (groundtruth.go) are the one exception, shelling out to the
// installed Go toolchain and using its assembler as the differential oracle.
package verify package verify
import ( import (