Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4f949c1f | ||
|
|
f0d5238c47 | ||
|
|
2931bbd6b2 | ||
|
|
63562a503a | ||
|
|
e836d6150d | ||
|
|
8a51b060da | ||
|
|
d3d47db727 | ||
|
|
0758556b7d | ||
|
|
ddb8440340 | ||
|
|
f15ff66fb1 | ||
|
|
187e4856d3 | ||
|
|
d315a998ce |
@@ -55,6 +55,25 @@ jobs:
|
|||||||
print qq{tag $v\n};
|
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 '
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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 ,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.]+)%} }
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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
|
||||||
|
|||||||
Vendored
+8
-2
@@ -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:
|
||||||
|
|||||||
Vendored
+69
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Large frames across every immediate band of the prologue SUB and the RET
|
||||||
|
// epilogue, byte-parity-checked against go tool asm:
|
||||||
|
//
|
||||||
|
// $5000 autosize 5024 one 16-bit chunk, materialised into REGTMP
|
||||||
|
// $65664 autosize 65680 split into two imm12 instructions
|
||||||
|
// $70000 autosize 70016 split into two imm12 instructions
|
||||||
|
// $65520 autosize 65536 one shifted imm12 in the prologue, a logical
|
||||||
|
// immediate (ORR) in the non-leaf epilogue
|
||||||
|
// $16777232 autosize 16777248 wider than 24 bits, MOVZ/MOVK into REGTMP
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·leaf5000(SB), NOSPLIT, $5000-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·leaf65664(SB), NOSPLIT, $65664-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·leaf70000(SB), NOSPLIT, $70000-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·leaf65520(SB), NOSPLIT, $65520-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
MOVD R2, R3
|
||||||
|
MOVD R3, R4
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·nl5000(SB), NOSPLIT, $5000-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
CALL ·other(SB)
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·nl65664(SB), NOSPLIT, $65664-0
|
||||||
|
MOVD R0, R1
|
||||||
|
CALL ·other(SB)
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·nl70000(SB), NOSPLIT, $70000-0
|
||||||
|
MOVD R0, R1
|
||||||
|
CALL ·other(SB)
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·nl65520(SB), NOSPLIT, $65520-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
MOVD R2, R3
|
||||||
|
CALL ·other(SB)
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·nlhuge(SB), NOSPLIT, $16777232-0
|
||||||
|
CALL ·other(SB)
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·other(SB), NOSPLIT, $0-0
|
||||||
|
MOVD R0, R1
|
||||||
|
MOVD R1, R2
|
||||||
|
MOVD R2, R3
|
||||||
|
RET
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Exclusive load/store family, LSE atomics and their memory operands,
|
||||||
|
// byte-parity-checked against go tool asm. The toolchain parses the FIRST
|
||||||
|
// register of an exclusive store as the data register and the LAST as the
|
||||||
|
// status register, and takes register pairs as (Rt1, Rt2) operands.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·loads(SB), NOSPLIT, $0-0
|
||||||
|
LDXR (R1), R2
|
||||||
|
LDXRB (R1), R3
|
||||||
|
LDXRH (R1), R4
|
||||||
|
LDXRW (R1), R5
|
||||||
|
LDAXR (R1), R2
|
||||||
|
LDAXRB (R1), R3
|
||||||
|
LDAXRH (R1), R4
|
||||||
|
LDAXRW (R1), R5
|
||||||
|
LDXR (RSP), R2
|
||||||
|
LDXRB (RSP), R3
|
||||||
|
LDXRH (RSP), R4
|
||||||
|
LDXRW (RSP), R5
|
||||||
|
LDAXR (RSP), R2
|
||||||
|
LDAXRB (RSP), R3
|
||||||
|
LDAXRH (RSP), R4
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·stores(SB), NOSPLIT, $0-0
|
||||||
|
STXR R2, (R1), R6
|
||||||
|
STXRB R3, (R1), R6
|
||||||
|
STXRH R4, (R1), R6
|
||||||
|
STXRW R5, (R1), R6
|
||||||
|
STLXR R2, (R1), R6
|
||||||
|
STLXRB R3, (R1), R6
|
||||||
|
STLXRH R4, (R1), R6
|
||||||
|
STLXRW R5, (R1), R6
|
||||||
|
STXR R2, (RSP), R6
|
||||||
|
STXRB R3, (RSP), R6
|
||||||
|
STXRH R4, (RSP), R6
|
||||||
|
STXRW R5, (RSP), R6
|
||||||
|
STLXR R2, (RSP), R6
|
||||||
|
STLXRB R3, (RSP), R6
|
||||||
|
STLXRH R4, (RSP), R6
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·pairs(SB), NOSPLIT, $0-0
|
||||||
|
LDXP (R1), (R2, R3)
|
||||||
|
LDXPW (R1), (R2, R3)
|
||||||
|
LDAXP (R1), (R2, R3)
|
||||||
|
LDAXPW (R1), (R2, R3)
|
||||||
|
STXP (R2, R3), (R1), R6
|
||||||
|
STXPW (R2, R3), (R1), R6
|
||||||
|
STLXP (R2, R3), (R1), R6
|
||||||
|
STLXPW (R2, R3), (R1), R6
|
||||||
|
LDXP (RSP), (R2, R3)
|
||||||
|
LDXPW (RSP), (R2, R3)
|
||||||
|
LDAXP (RSP), (R4, R5)
|
||||||
|
LDAXPW (RSP), (R4, R5)
|
||||||
|
STXP (R2, R3), (RSP), R6
|
||||||
|
STXPW (R2, R3), (RSP), R6
|
||||||
|
STLXP (R4, R5), (RSP), R7
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·atomics(SB), NOSPLIT, $0-0
|
||||||
|
LDADDB R2, (R1), R3
|
||||||
|
LDADDH R2, (R1), R3
|
||||||
|
LDADDW R2, (R1), R3
|
||||||
|
LDADDD R2, (R1), R3
|
||||||
|
LDADDB R2, (R1), ZR
|
||||||
|
LDADDH R2, (R1), ZR
|
||||||
|
LDADDW R2, (R1), ZR
|
||||||
|
LDADDD R2, (R1), ZR
|
||||||
|
CASW R2, (R1), R3
|
||||||
|
CASD R2, (R1), R3
|
||||||
|
CASW R2, (R1), ZR
|
||||||
|
CASD R2, (R1), ZR
|
||||||
|
SWPW R2, (R1), R3
|
||||||
|
SWPD R2, (R1), R3
|
||||||
|
SWPD R2, (R1), ZR
|
||||||
|
RET
|
||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
// Mixed-width sign- and zero-extending moves plus PMOVMSKB, the spellings
|
||||||
|
// GOROOT's runtime and bytealg kernels use. Every result is folded back so
|
||||||
|
// no instruction is dead.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func widen(p *byte) uint64
|
||||||
|
TEXT ·widen(SB), NOSPLIT, $0-16
|
||||||
|
MOVBQZX 0(DI), AX
|
||||||
|
MOVWQZX 2(DI), CX
|
||||||
|
ADDQ CX, AX
|
||||||
|
MOVLQZX 4(DI), DX
|
||||||
|
ADDQ DX, AX
|
||||||
|
MOVBQSX 8(DI), R8
|
||||||
|
ADDQ R8, AX
|
||||||
|
MOVWQSX 12(DI), R9
|
||||||
|
ADDQ R9, AX
|
||||||
|
MOVBLSX 16(DI), R10
|
||||||
|
ADDL R10, AX
|
||||||
|
MOVLQSX 20(DI), R11
|
||||||
|
ADDQ R11, AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func widenw(p *byte) int32
|
||||||
|
TEXT ·widenw(SB), NOSPLIT, $0-16
|
||||||
|
MOVBWZX 0(DI), AX
|
||||||
|
MOVBWSX 1(DI), CX
|
||||||
|
ADDL CX, AX
|
||||||
|
MOVLQZX AX, DX
|
||||||
|
MOVL DX, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func mask(x *XMM) int
|
||||||
|
TEXT ·mask(SB), NOSPLIT, $0-16
|
||||||
|
MOVOU 0(DI), X1
|
||||||
|
PMOVMSKB X1, AX
|
||||||
|
MOVQ AX, ret+8(FP)
|
||||||
|
RET
|
||||||
+14
-4
@@ -42,8 +42,12 @@ func TestABIArm64(t *testing.T) {
|
|||||||
t.Errorf("cleanAdd: %s", report)
|
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
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user