Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d01bb3ecf | ||
|
|
332c63e440 | ||
|
|
b306c210c6 | ||
|
|
b9015e1c2e | ||
|
|
26c5008136 | ||
|
|
74d6b90d69 | ||
|
|
7b11c62f53 | ||
|
|
8eed54b3da | ||
|
|
4be16dcdf5 | ||
|
|
ded9cabdf4 | ||
|
|
cf6bc6987e | ||
|
|
ff7b1452b1 | ||
|
|
517c1cea25 | ||
|
|
a3e3010e0f | ||
|
|
057c4eb545 | ||
|
|
f720381d43 | ||
|
|
2c9042d62c | ||
|
|
82ef289d3a | ||
|
|
7246b0e002 | ||
|
|
8cfd40aac8 | ||
|
|
5382c9a8e4 | ||
|
|
53de91b2df | ||
|
|
8a36af7c7d | ||
|
|
e9789ce3f4 | ||
|
|
837231c068 |
@@ -342,7 +342,10 @@ jobs:
|
|||||||
my @cmd = (q{curl}, q{-sS}, q{-o}, q{/dev/null}, q{-w}, q{%{http_code}},
|
my @cmd = (q{curl}, q{-sS}, q{-o}, q{/dev/null}, q{-w}, q{%{http_code}},
|
||||||
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
||||||
q{-H}, q{Content-Type: application/octet-stream},
|
q{-H}, q{Content-Type: application/octet-stream},
|
||||||
q{-X}, q{POST}, q{--data-binary}, qq{@$path},
|
# The @ must not sit inside a qq{} string: there it starts an
|
||||||
|
# array interpolation and the upload body collapses to empty,
|
||||||
|
# which Gitea stores as a 201-created zero-byte attachment.
|
||||||
|
q{-X}, q{POST}, q{--data-binary}, q{@} . $path,
|
||||||
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases/$id/assets?name=$name});
|
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases/$id/assets?name=$name});
|
||||||
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
||||||
my $code = <$curl>;
|
my $code = <$curl>;
|
||||||
|
|||||||
@@ -56,6 +56,28 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: go build ./...
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: FreeBSD build (amd64)
|
||||||
|
# The debugger's ptrace surface and the JIT substrate are the two
|
||||||
|
# FreeBSD-portable layers the tree carries; the forge has no FreeBSD
|
||||||
|
# runner, so a push can only compile-gate them. Running the ptrace
|
||||||
|
# suite needs real FreeBSD hardware.
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
GOARCH: amd64
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: FreeBSD build (arm64)
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
GOARCH: arm64
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: FreeBSD build (riscv64)
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
GOARCH: riscv64
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: |
|
run: |
|
||||||
perl -e '
|
perl -e '
|
||||||
|
|||||||
+153
-24
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to gasm-devkit are documented here.
|
All notable changes to gasm-sdk are documented here.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
@@ -9,14 +9,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **GOOBJ format specification.** [docs/GOOBJ.md](docs/GOOBJ.md)
|
- **The FreeBSD port of the debugger.** `gasm debug` runs on FreeBSD on
|
||||||
documents the Go object file format in full: both containers, the 96
|
amd64, arm64 and riscv64: the same interactive surface as on Linux —
|
||||||
byte header and all 19 blocks, every structure with its byte
|
breakpoints, hardware watchpoints (x86 debug registers, the arm64 debug
|
||||||
offsets, symbol kinds and flag bits, all 106 relocation types with
|
register file), single-stepping, register and memory access — behind the
|
||||||
the weak variants, aux symbols, the FuncInfo payload, the pc-value
|
kernel's own ptrace requests, with tracee memory through `PT_IO` and
|
||||||
table encoding, the content hashes and the builtin table, all
|
stop reports through `PT_LWPINFO`. The JIT substrate maps executable
|
||||||
verified byte for byte against objects produced by Go 1.27.1's own
|
memory through `golang.org/x/sys/unix`, so `verify` builds on FreeBSD
|
||||||
tools.
|
too. The pipeline compile-gates all three architectures; live
|
||||||
|
validation awaits a FreeBSD machine.
|
||||||
|
- **Workspace-wide navigation in the language server.** `gasm lsp` indexes
|
||||||
|
the `.s` files under the workspace root beyond the documents the editor
|
||||||
|
has open, so go-to-definition, find references and workspace symbol search
|
||||||
|
reach files that were never opened. An open buffer always shadows its
|
||||||
|
disk copy, and watched-file events together with a per-query freshness
|
||||||
|
check keep the index current.
|
||||||
|
- **Quick fixes for the textflag include and the argument area.** The
|
||||||
|
`missing-textflag-include` warning offers to add the include after the
|
||||||
|
last one in the file, and the `abi-argsize` warning offers to set the
|
||||||
|
TEXT argument area to the size the `// func` signature implies, computed
|
||||||
|
by the new `lint.ExpectedArgSize`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The corpus audit assembles like the build.** A file's `//go:build`
|
||||||
|
constraint decides which target architectures attempt it: cpu_x86.s is
|
||||||
|
an x86 build alone, and the msan and goexperiment.runtimesecret trees
|
||||||
|
are compiled by no supported build, so they leave the measured set
|
||||||
|
instead of failing it. The headline now reads "assemble for every
|
||||||
|
applicable target": every real-code GOROOT assembly file, the tree
|
||||||
|
without testdata, assembles for all four architectures (250 of 250,
|
||||||
|
100 %); over the whole tree including testdata the measure is 271 of
|
||||||
|
322 (84.2 %).
|
||||||
|
- **The module moves to `sourcedock.dev/petrbalvin/gasm-sdk`.** The
|
||||||
|
repository and the module rename together with the product, now the
|
||||||
|
GAsm Software Development Kit. Fresh installs become
|
||||||
|
`go install sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm@latest`, and
|
||||||
|
installs pinned to the old `gasm-sdk` path stop resolving once the
|
||||||
|
repository takes the new name: reinstall from the new path. The
|
||||||
|
binary stays `gasm`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Rename edits land in their own documents.** A rename collected the
|
||||||
|
ranges of every reference across the open documents but applied them all
|
||||||
|
to the document that started it, so renaming a symbol used in a second
|
||||||
|
file moved that file's text into the first. Each edit now applies to the
|
||||||
|
document it was collected in.
|
||||||
|
- **Negative numeric PC-relative jumps.** `JMP -3(PC)`, the shape the
|
||||||
|
runtime's exit loops write (sys_linux_amd64.s, sys_netbsd_amd64.s),
|
||||||
|
resolved to nothing: only the forward forms counted. A negative count
|
||||||
|
now walks the same instruction statements backwards, labels excluded,
|
||||||
|
byte-identical with the toolchain.
|
||||||
|
- **The arm64 move-wide family reads its immediate as an unsigned
|
||||||
|
pattern.** `MOVK $(40000<<48)` folds to a negative int64 and was
|
||||||
|
rejected; the toolchain picks the 16-bit lane from the 64-bit bit
|
||||||
|
pattern, so the encoder now does the same, and a zero immediate is
|
||||||
|
rejected where the toolchain rejects it.
|
||||||
|
|
||||||
|
## [0.35.0] - 2026-09-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **The go_asm.h generator.** `gasm asm` generates the package's go_asm.h
|
||||||
|
itself when an assembly file includes it: the Go files beside the source
|
||||||
|
are type-checked for the target architecture and the constants and field
|
||||||
|
offsets become assembler defines, so package-context files assemble with
|
||||||
|
no compiler and no `go build` in the loop. `-GOOS` selects the
|
||||||
|
type-checking GOOS for GOOS-specific files, and the corpus audit derives
|
||||||
|
the GOOS from the file name.
|
||||||
|
- **ELF data relocations on arm64, riscv64 and loong64.** `gasm asm
|
||||||
|
--format elf` emits `.rela.data` for symbol-valued DATA initialisers on
|
||||||
|
every architecture (amd64 carried them already), so standalone ELF
|
||||||
|
objects link on all four targets.
|
||||||
|
- **Corpus failure listing.** `gasm audit-instructions --corpus --list`
|
||||||
|
prints every failing file with its failure reason, per architecture,
|
||||||
|
instead of one representative file per reason.
|
||||||
|
- **DATA with symbol values and relaxed symbol spellings.** DATA
|
||||||
|
initialisers accept `$symbol(SB)` values, laid down as an absolute
|
||||||
|
relocation at the data field (GOOBJ on all four architectures and ELF
|
||||||
|
on all four as of this release), and U+2215 is accepted inside symbol
|
||||||
|
package paths.
|
||||||
- **Macro expansion and include splicing.** `gasm asm`, `gasm diff` and
|
- **Macro expansion and include splicing.** `gasm asm`, `gasm diff` and
|
||||||
`gasm audit-instructions` now preprocess assembly the way the
|
`gasm audit-instructions` now preprocess assembly the way the
|
||||||
toolchain does: object and parameterised `#define` macros expand at
|
toolchain does: object and parameterised `#define` macros expand at
|
||||||
@@ -57,21 +130,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
ranges, index-only VSIB memory operands and bare trailing immediates;
|
ranges, index-only VSIB memory operands and bare trailing immediates;
|
||||||
macro substitution reaches parameters used with element suffixes
|
macro substitution reaches parameters used with element suffixes
|
||||||
(`A.S4`), and `;` separates statements in plain files.
|
(`A.S4`), and `;` separates statements in plain files.
|
||||||
- **`gasm asm -GOOS`.** The go_asm.h generator type-checks per target
|
- **Per-architecture reference pages.** [docs/asm/](docs/asm/README.md)
|
||||||
GOOS, so the darwin-only and windows-only runtime files assemble with
|
gains AMD64, ARM64, RISCV64 and LOONG64: the register files and the
|
||||||
their own defines; the corpus audit derives the GOOS from the file
|
roles the ABI fixes, addressing, operand order with every special form,
|
||||||
name. DATA initialisers accept `$symbol(SB)` values (an absolute
|
constants and materialisation, alignment, fences and the relocations
|
||||||
relocation at the data field, GOOBJ on all four architectures and ELF
|
each target emits. An instruction inventory appendix per architecture
|
||||||
on amd64, arm64, riscv64 and loong64), and U+2215 is accepted inside
|
is generated from the toolchain's own tables by `just gen`, and the
|
||||||
symbol package paths.
|
regenerated tables recognise 147 more mnemonics than the previous
|
||||||
- **The corpus audit measures honestly.** Files named for Go ports gasm
|
release carried (arm64 107, riscv64 31, loong64 9).
|
||||||
does not target (arm, 386, s390x, ...) are no longer attempted for the
|
- **The Plan 9 assembly language reference.** [docs/asm/](docs/asm/README.md)
|
||||||
four supported architectures (no supported build compiles them), and
|
opens the complete language reference with its common core: the lexicon,
|
||||||
the headline rate is reported over attemptable files: 136 of 433 on
|
statement structure and constant expressions, the operand grammar with
|
||||||
the full corpus (31.4 %), 135 of 383 on real code (35.2 %), from the
|
the pseudo-registers and symbol naming, the directives and the function
|
||||||
127 that the previous release measured. The probe battery that
|
flag vocabulary, preprocessing with `#define` and `#include`, and the
|
||||||
decides encodability gained the operand shapes the new families use.
|
Go-embedded layer (ABI0, prototypes, `go_asm.h`, `funcdata.h` and the
|
||||||
-
|
runtime contract). Every claim is verified against `go tool asm` of
|
||||||
|
Go 1.27.1 and gasm's differential tests; the per-architecture pages and
|
||||||
|
generated instruction appendices follow.
|
||||||
|
- **GOOBJ format specification.** [docs/GOOBJ.md](docs/GOOBJ.md)
|
||||||
|
documents the Go object file format in full: both containers, the 96
|
||||||
|
byte header and all 19 blocks, every structure with its byte
|
||||||
|
offsets, symbol kinds and flag bits, all 106 relocation types with
|
||||||
|
the weak variants, aux symbols, the FuncInfo payload, the pc-value
|
||||||
|
table encoding, the content hashes and the builtin table, all
|
||||||
|
verified byte for byte against objects produced by Go 1.27.1's own
|
||||||
|
tools.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The corpus audit measures like a build.** Files named for a Go port
|
||||||
|
gasm does not target (arm, 386, s390x, ...) are never attempted, because
|
||||||
|
no supported build compiles them; the GOOS comes from the file name; and
|
||||||
|
each target's go_asm.h is generated on the fly. The headline is reported
|
||||||
|
over attemptable files: 291 of 353 on the full corpus (82.4 %) assemble
|
||||||
|
for every target architecture and 295 of 303 on real code (97.4 %),
|
||||||
|
against 108 of 627 over all files (17.2 %) that the previous release
|
||||||
|
measured.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **The operand forms GOROOT writes.** Numeric PC-relative jumps
|
||||||
|
(`JEQ 2(PC)`, the park loop `JMP 0(PC)`) resolve with the toolchain's
|
||||||
|
own instruction counting and fold jump-to-jump chains exactly as its
|
||||||
|
branch optimiser does; symbol immediates (`MOVQ $sym(SB), AX`)
|
||||||
|
assemble to the toolchain's RIP-relative LEA with an R_PCREL
|
||||||
|
relocation; negated constant expressions in operands (`ADJSP
|
||||||
|
$-(REGS - 8)`, the shape the cgo ABI macros write) fold; the immediate
|
||||||
|
multiply (`IMULQ $1000000000, AX`) encodes with the toolchain's
|
||||||
|
0x69/0x6B selection; the TLS access pair assembles as the toolchain's
|
||||||
|
one-instruction form (the bare `MOVQ TLS, r` load nops out and
|
||||||
|
`off(r)(TLS*1)` folds to the segment-prefixed absolute whose disp32
|
||||||
|
carries the R_TLSLE relocation, per-GOOS); arm64 accepts the
|
||||||
|
bare-register indirect branch (`BL R9` beside `BL (R9)`, both BLR) and
|
||||||
|
the zero-immediate store (`MOVD $0, mem` through the zero register,
|
||||||
|
rejecting non-zero immediates as the toolchain does); `PCALIGN` now
|
||||||
|
aligns on amd64, padding with the toolchain's greedy
|
||||||
|
single-instruction NOPs; the segment-absolute forms (`MOVQ 0x30(GS),
|
||||||
|
AX` and the store direction) and the absolute crash-store
|
||||||
|
(`MOVL $0xf1, 0xf1`) encode; and `gasm asm` predefines the
|
||||||
|
`GOARCH_<arch>` and `GOOS_<goos>` macros the go command passes to
|
||||||
|
`go tool asm`, so GOROOT headers' `#ifdef GOARCH_amd64` platform
|
||||||
|
blocks (`go_tls.h`'s `get_tls` and friends) select as intended. The
|
||||||
|
GOROOT corpus measure moves to 291 of 353 files assembling for every
|
||||||
|
target architecture (82.4 %), 97.4 % of the real-code corpus, from
|
||||||
|
70.8 % and 82.2 %.
|
||||||
|
- **Tool corrections across the pipeline.** The formatter keeps square
|
||||||
|
brackets in SIMD operands, statement separators and canonical macro
|
||||||
|
bodies; the linter drops false positives on shift counts, SETcc
|
||||||
|
spellings and ABIInternal references; the lexer treats a trailing
|
||||||
|
carriage return as a line end so comment text stays idempotent; and
|
||||||
|
arm64 rejects bare BTI with a diagnostic while accepting the full
|
||||||
|
family.
|
||||||
|
|
||||||
## [0.34.0] - 2026-09-20
|
## [0.34.0] - 2026-09-20
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Contributions to **gasm-devkit** are governed by the Contributor terms
|
Contributions to **gasm-sdk** are governed by the Contributor terms
|
||||||
below; submitting one means you accept them.
|
below; submitting one means you accept them.
|
||||||
|
|
||||||
## Contributor terms
|
## Contributor terms
|
||||||
@@ -29,8 +29,8 @@ compiler (gcc), because `just gates` includes `just race` and the race
|
|||||||
detector needs cgo.
|
detector needs cgo.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git
|
||||||
cd gasm-devkit
|
cd gasm-sdk
|
||||||
just build
|
just build
|
||||||
just gates
|
just gates
|
||||||
```
|
```
|
||||||
@@ -126,7 +126,7 @@ tag, where it would double the time and the memory a shared runner cannot spare.
|
|||||||
|
|
||||||
## Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-devkit/issues` with the
|
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-sdk/issues` with the
|
||||||
version, the operating system and architecture, the exact command, the full output,
|
version, the operating system and architecture, the exact command, the full output,
|
||||||
and the expected against the actual behaviour.
|
and the expected against the actual behaviour.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Plan 9 assembly tooling, inside and outside Go
|
# GAsm: Software Development Kit for Plan 9 Assembly
|
||||||
|
|
||||||
> **Warning: this is an experiment.** gasm-devkit is under active
|
> **Warning: this is an experiment.** gasm-sdk is under active
|
||||||
> development and is not stable. The version is 0.x.x: commands, flags,
|
> development and is not stable. The version is 0.x.x: commands, flags,
|
||||||
> 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
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
there is no formatter, no linter and no debugger for `.s` files, and no
|
there is no formatter, no linter and no debugger for `.s` files, and no
|
||||||
assembler that works without a Go installation. Developers write
|
assembler that works without a Go installation. Developers write
|
||||||
assembly blind, validate it by benchmark, and debug it by print
|
assembly blind, validate it by benchmark, and debug it by print
|
||||||
statement. gasm-devkit is the missing toolkit: a single, self-contained
|
statement. gasm-sdk is the missing toolkit: a single, self-contained
|
||||||
binary, `gasm`, that serves both purposes.
|
binary, `gasm`, that serves both purposes.
|
||||||
|
|
||||||
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
||||||
@@ -58,7 +58,7 @@ Plan 9 (Go): MOVQ AX, total-16(SP)
|
|||||||
|
|
||||||
The same lines, but only one of them tells you what the number is for.
|
The same lines, but only one of them tells you what the number is for.
|
||||||
The syntax is uppercase, regular and boring, which is the highest
|
The syntax is uppercase, regular and boring, which is the highest
|
||||||
compliment a language for machine code can earn. gasm-devkit exists
|
compliment a language for machine code can earn. gasm-sdk exists
|
||||||
to give that syntax the tooling it deserves.
|
to give that syntax the tooling it deserves.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -81,7 +81,10 @@ to give that syntax the tooling it deserves.
|
|||||||
GOOBJ format, which needs the installed toolchain and which `go build`
|
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. The assembler preprocesses
|
||||||
|
like the toolchain (`#define`, `#include` with `-I`, `#ifdef`), generates
|
||||||
|
`go_asm.h` from the package's Go files, and carries `PCALIGN`, the
|
||||||
|
`LOCK`/`REP` prefixes and the literal-data pseudo-ops.
|
||||||
- **Disassembler.** `gasm dis` lists a `.s` file's functions at their real
|
- **Disassembler.** `gasm dis` lists a `.s` file's functions at their real
|
||||||
offsets after assembling, or disassembles raw bytes from a file or stdin.
|
offsets after assembling, or disassembles raw bytes from a file or stdin.
|
||||||
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
||||||
@@ -91,13 +94,16 @@ to give that syntax the tooling it deserves.
|
|||||||
- **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 that report instruction and
|
memory inspection, and headless script runs that report instruction and
|
||||||
label coverage.
|
label coverage; it runs on Linux (all four architectures) and FreeBSD
|
||||||
|
(amd64, arm64, riscv64).
|
||||||
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
|
- **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
|
||||||
help, document highlights, workspace symbol search, #include document
|
help, document highlights, workspace symbol search, #include document
|
||||||
links and folding ranges over stdio; definition, references and rename
|
links and folding ranges over stdio; definition, references and rename
|
||||||
work across every open document.
|
work across every open document and the indexed workspace files beyond
|
||||||
|
them, and the quick fixes add a missing textflag.h include and set the
|
||||||
|
argument area from the // func signature.
|
||||||
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
||||||
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
||||||
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
||||||
@@ -110,9 +116,9 @@ Four architectures, the four that matter in practice:
|
|||||||
| Architecture | GOARCH | File suffix | Instructions recognised |
|
| Architecture | GOARCH | File suffix | Instructions recognised |
|
||||||
|--------------|-------------|--------------|---------------------------------------------|
|
|--------------|-------------|--------------|---------------------------------------------|
|
||||||
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
||||||
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
|
| ARM64 | `arm64` | `_arm64.s` | 645 + common opcodes |
|
||||||
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
|
| RISC-V | `riscv64` | `_riscv64.s` | 992 + common opcodes |
|
||||||
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
|
| LoongArch | `loong64` | `_loong64.s` | 808 + common opcodes |
|
||||||
|
|
||||||
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
||||||
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
||||||
@@ -124,10 +130,13 @@ 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 136 of 433 attemptable files
|
`gasm audit-instructions --corpus` reports every real-code GOROOT assembly
|
||||||
(31.4 %) assembling for every target architecture today (files named for
|
file (the tree without testdata) assembling for every target its build
|
||||||
other Go ports are counted but never attempted), with the top failure
|
admits: 250 of 250, 100 %. Over the whole tree including testdata the
|
||||||
reasons per architecture; the number moves with every release.
|
measure is 271 of 322 attemptable (84.2 %); files named for other Go ports
|
||||||
|
are counted but never attempted, and `//go:build` constraints decide which
|
||||||
|
targets attempt a file at all, exactly as the build does. The number moves
|
||||||
|
with every release.
|
||||||
|
|
||||||
### Validation status
|
### Validation status
|
||||||
|
|
||||||
@@ -142,7 +151,7 @@ actually been executed.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Encoding: byte-for-byte against `go tool asm` | native hardware | native hardware (the toolchain cross-assembles any GOARCH on any host) |
|
| 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 |
|
| Execution: JIT calls, ABI checks, differential fuzzing | native hardware | qemu-user emulation |
|
||||||
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more |
|
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more. FreeBSD (amd64, arm64, riscv64) is in the same position: the port compiles behind the cross-build gate and its integration test is ready, but no FreeBSD machine has executed it |
|
||||||
|
|
||||||
Consequences, stated plainly. An emulator is a model of a CPU, not the
|
Consequences, stated plainly. An emulator is a model of a CPU, not the
|
||||||
CPU: instruction semantics are implemented in software and can differ
|
CPU: instruction semantics are implemented in software and can differ
|
||||||
@@ -186,8 +195,9 @@ verified the way the code is verified: an encoding documented here is
|
|||||||
one that differential tests against `go tool asm` confirm
|
one that differential tests against `go tool asm` confirm
|
||||||
byte-for-byte, and a format field documented here is one the linker
|
byte-for-byte, and a format field documented here is one the linker
|
||||||
demonstrably reads. The work has begun: [docs/GOOBJ.md](docs/GOOBJ.md)
|
demonstrably reads. The work has begun: [docs/GOOBJ.md](docs/GOOBJ.md)
|
||||||
specifies the object file format completely. The assembler language
|
specifies the object file format completely, and
|
||||||
reference follows.
|
[docs/asm/README.md](docs/asm/README.md) opens the language reference
|
||||||
|
with its common core. The per-architecture pages follow.
|
||||||
|
|
||||||
## Direction
|
## Direction
|
||||||
|
|
||||||
@@ -211,9 +221,11 @@ The plan, in the order it is being worked:
|
|||||||
toolchain itself does not support; through ELF, Plan 9 assembly becomes
|
toolchain itself does not support; through ELF, Plan 9 assembly becomes
|
||||||
usable outside Go entirely.
|
usable outside Go entirely.
|
||||||
- **Platforms: Linux and FreeBSD.** Linux is supported today on all four
|
- **Platforms: Linux and FreeBSD.** Linux is supported today on all four
|
||||||
architectures and is where the binary builds. FreeBSD follows: the
|
architectures and is where the binary builds. FreeBSD follows on amd64,
|
||||||
JIT's executable-memory mapping and the ptrace debugger layer are the
|
arm64 and riscv64: the JIT's executable-memory mapping and the ptrace
|
||||||
two pieces of porting work. Other unix systems may follow those two.
|
debugger layer are ported (the debugger's live validation awaits a
|
||||||
|
FreeBSD machine, as the validation status states). Other unix systems
|
||||||
|
may follow those two.
|
||||||
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
||||||
No others are planned.
|
No others are planned.
|
||||||
|
|
||||||
@@ -221,11 +233,11 @@ The plan, in the order it is being worked:
|
|||||||
|
|
||||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
||||||
linux/loong64 are on the
|
linux/loong64 are on the
|
||||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
[releases page](https://sourcedock.dev/petrbalvin/gasm-sdk/releases).
|
||||||
From source (Go 1.27.1):
|
From source (Go 1.27.1):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
go install sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from a repository checkout:
|
Or from a repository checkout:
|
||||||
@@ -315,6 +327,7 @@ recipe.
|
|||||||
them
|
them
|
||||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||||
- [docs/GOOBJ.md](docs/GOOBJ.md): the GOOBJ object file format specification
|
- [docs/GOOBJ.md](docs/GOOBJ.md): the GOOBJ object file format specification
|
||||||
|
- [docs/asm/](docs/asm/README.md): the Plan 9 assembly language reference
|
||||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ releases do not receive them.
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 0.34.0 | yes |
|
| 0.35.0 | yes |
|
||||||
| older releases | no |
|
| older releases | no |
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## Reporting a vulnerability
|
||||||
|
|||||||
+105
-7
@@ -5,9 +5,13 @@
|
|||||||
// toolchain's own assembler source. Go's Plan 9 assembler defines the exact,
|
// toolchain's own assembler source. Go's Plan 9 assembler defines the exact,
|
||||||
// complete set of mnemonics it accepts for each architecture in
|
// complete set of mnemonics it accepts for each architecture in
|
||||||
// $GOROOT/src/cmd/internal/obj/<arch>/anames.go; this tool extracts those
|
// $GOROOT/src/cmd/internal/obj/<arch>/anames.go; this tool extracts those
|
||||||
// names so gasm-devkit supports every instruction the real assembler does,
|
// names so gasm-sdk supports every instruction the real assembler does,
|
||||||
// with no hand-maintained (and therefore inevitably incomplete) lists.
|
// with no hand-maintained (and therefore inevitably incomplete) lists.
|
||||||
//
|
//
|
||||||
|
// The same data feeds the generated instruction appendices of the assembly
|
||||||
|
// language reference, docs/asm/INSTRUCTIONS-<ARCH>.md, so that the reference
|
||||||
|
// cannot drift from the tables it documents.
|
||||||
|
//
|
||||||
// Usage (via the justfile):
|
// Usage (via the justfile):
|
||||||
//
|
//
|
||||||
// just gen
|
// just gen
|
||||||
@@ -26,9 +30,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// archDirs maps a gasm-devkit architecture name to its obj sub-directory.
|
// archDirs maps a gasm-sdk architecture name to its obj sub-directory.
|
||||||
var archDirs = []struct {
|
var archDirs = []struct {
|
||||||
arch string
|
arch string
|
||||||
sub string
|
sub string
|
||||||
@@ -39,11 +46,30 @@ var archDirs = []struct {
|
|||||||
{"loong64", "loong64"},
|
{"loong64", "loong64"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// docPages maps an architecture to its generated appendix in the language
|
||||||
|
// reference. The amd64 page carries a per-mnemonic encodability column,
|
||||||
|
// decided by asm.Encodable, which mirrors the encoder's own dispatch; the
|
||||||
|
// other targets have no single cheap predicate, so their pages carry the
|
||||||
|
// inventory and point at the live measurement instead.
|
||||||
|
var docPages = []struct {
|
||||||
|
arch arch.Arch
|
||||||
|
title string
|
||||||
|
file string
|
||||||
|
anames string
|
||||||
|
encodable bool
|
||||||
|
}{
|
||||||
|
{arch.AMD64, "AMD64", "INSTRUCTIONS-AMD64.md", "cmd/internal/obj/x86/anames.go", true},
|
||||||
|
{arch.ARM64, "ARM64", "INSTRUCTIONS-ARM64.md", "cmd/internal/obj/arm64/anames.go", false},
|
||||||
|
{arch.RISCV, "RISC-V 64", "INSTRUCTIONS-RISCV64.md", "cmd/internal/obj/riscv/anames.go", false},
|
||||||
|
{arch.LOONG64, "LoongArch 64", "INSTRUCTIONS-LOONG64.md", "cmd/internal/obj/loong64/anames.go", false},
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
goroot := strings.TrimSpace(runGoEnvGOROOT())
|
goroot := strings.TrimSpace(runGoEnvGOROOT())
|
||||||
if goroot == "" {
|
if goroot == "" {
|
||||||
fatal("could not determine GOROOT")
|
fatal("could not determine GOROOT")
|
||||||
}
|
}
|
||||||
|
version := strings.TrimSpace(runGoEnv("GOVERSION"))
|
||||||
// The common opcodes shared by every architecture (RET, JMP, NOP, CALL,
|
// The common opcodes shared by every architecture (RET, JMP, NOP, CALL,
|
||||||
// TEXT, FUNCDATA, …) live in cmd/internal/obj/util.go.
|
// TEXT, FUNCDATA, …) live in cmd/internal/obj/util.go.
|
||||||
commonPath := filepath.Join(goroot, "src", "cmd", "internal", "obj", "util.go")
|
commonPath := filepath.Join(goroot, "src", "cmd", "internal", "obj", "util.go")
|
||||||
@@ -57,16 +83,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("%-8s %4d instructions -> arch/common_gen.go\n", "common", len(common))
|
fmt.Printf("%-8s %4d instructions -> arch/common_gen.go\n", "common", len(common))
|
||||||
|
|
||||||
|
names := map[string][]string{}
|
||||||
for _, a := range archDirs {
|
for _, a := range archDirs {
|
||||||
path := filepath.Join(goroot, "src", "cmd", "internal", "obj", a.sub, "anames.go")
|
path := filepath.Join(goroot, "src", "cmd", "internal", "obj", a.sub, "anames.go")
|
||||||
names, err := extractInstrs(path)
|
names[a.arch], err = extractInstrs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal("extract %s: %v", a.arch, err)
|
fatal("extract %s: %v", a.arch, err)
|
||||||
}
|
}
|
||||||
if err := writeGen(a.arch, a.sub, names); err != nil {
|
if err := writeGen(a.arch, a.sub, names[a.arch]); err != nil {
|
||||||
fatal("write %s: %v", a.arch, err)
|
fatal("write %s: %v", a.arch, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%-8s %4d instructions -> arch/%s_gen.go\n", a.arch, len(names), a.arch)
|
fmt.Printf("%-8s %4d instructions -> arch/%s_gen.go\n", a.arch, len(names[a.arch]), a.arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range docPages {
|
||||||
|
if err := writeDocPage(p.arch, p.title, p.file, p.anames, version, p.encodable); err != nil {
|
||||||
|
fatal("write %s: %v", p.file, err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%-8s -> docs/asm/%s\n", p.arch, p.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +119,7 @@ func filterCommon(names []string) []string {
|
|||||||
// writeCommon emits arch/common_gen.go.
|
// writeCommon emits arch/common_gen.go.
|
||||||
func writeCommon(names []string) error {
|
func writeCommon(names []string) error {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||||
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
||||||
b.WriteString("//\n")
|
b.WriteString("//\n")
|
||||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||||
@@ -156,7 +190,7 @@ func stringLit(elt ast.Expr) string {
|
|||||||
// writeGen emits arch/<arch>_gen.go.
|
// writeGen emits arch/<arch>_gen.go.
|
||||||
func writeGen(arch, sub string, names []string) error {
|
func writeGen(arch, sub string, names []string) error {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||||
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
||||||
b.WriteString("//\n")
|
b.WriteString("//\n")
|
||||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||||
@@ -172,6 +206,61 @@ func writeGen(arch, sub string, names []string) error {
|
|||||||
return os.WriteFile(filepath.Join("arch", arch+"_gen.go"), []byte(b.String()), 0o644)
|
return os.WriteFile(filepath.Join("arch", arch+"_gen.go"), []byte(b.String()), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeDocPage emits docs/asm/<file>, the generated instruction appendix of
|
||||||
|
// the language reference for one architecture: every mnemonic the toolchain
|
||||||
|
// accepts, with the curated summary where the architecture table carries one
|
||||||
|
// and, on amd64, a per-mnemonic encodability column.
|
||||||
|
func writeDocPage(a arch.Arch, title, file, anames, version string, encodable bool) error {
|
||||||
|
table := arch.ForArch(a)
|
||||||
|
instrs := table.Instructions()
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("# " + title + ": instruction inventory\n\n")
|
||||||
|
b.WriteString("Generated by gasm-sdk's `_gen` from the Go toolchain's instruction table\n")
|
||||||
|
b.WriteString("(`" + anames + "`, " + version + "); DO NOT EDIT. This page lists every mnemonic\n")
|
||||||
|
b.WriteString("`go tool asm` accepts on this target, which is the upper bound of the\n")
|
||||||
|
b.WriteString("language on it: a name absent here is not an instruction of the target,\n")
|
||||||
|
b.WriteString("and a name present here may still be one gasm's encoder cannot emit yet.\n\n")
|
||||||
|
|
||||||
|
encodableCount := 0
|
||||||
|
if encodable {
|
||||||
|
b.WriteString("The `gasm encodes` column reports whether gasm's encoder can emit the\n")
|
||||||
|
b.WriteString("mnemonic today; the gap is the encoder backlog, measured live by\n")
|
||||||
|
b.WriteString("`gasm audit-instructions`.\n\n")
|
||||||
|
b.WriteString("| Mnemonic | gasm encodes | Notes |\n")
|
||||||
|
b.WriteString("|---|---|---|\n")
|
||||||
|
for _, in := range instrs {
|
||||||
|
ok := asm.Encodable(in.Name)
|
||||||
|
if ok {
|
||||||
|
encodableCount++
|
||||||
|
}
|
||||||
|
b.WriteString("| `" + in.Name + "` | " + yesNo(ok) + " | " + in.Summary + " |\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
fmt.Fprintf(&b, "Recognised: %d mnemonics. gasm encodes: %d.\n", len(instrs), encodableCount)
|
||||||
|
} else {
|
||||||
|
b.WriteString("The inventory carries no per-mnemonic encoder column: on this target\n")
|
||||||
|
b.WriteString("encodability is decided per operand shape, and the live measured\n")
|
||||||
|
b.WriteString("coverage is reported by `gasm audit-instructions`.\n\n")
|
||||||
|
b.WriteString("| Mnemonic | Notes |\n")
|
||||||
|
b.WriteString("|---|---|\n")
|
||||||
|
for _, in := range instrs {
|
||||||
|
b.WriteString("| `" + in.Name + "` | " + in.Summary + " |\n")
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
fmt.Fprintf(&b, "Recognised: %d mnemonics.\n", len(instrs))
|
||||||
|
}
|
||||||
|
return os.WriteFile(filepath.Join("docs", "asm", file), []byte(b.String()), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// yesNo renders a boolean as the word the appendix tables use.
|
||||||
|
func yesNo(v bool) string {
|
||||||
|
if v {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
}
|
||||||
|
|
||||||
func runGoEnvGOROOT() string {
|
func runGoEnvGOROOT() string {
|
||||||
out, err := exec.Command("go", "env", "GOROOT").Output()
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,6 +269,15 @@ func runGoEnvGOROOT() string {
|
|||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runGoEnv runs `go env` for a single variable.
|
||||||
|
func runGoEnv(name string) string {
|
||||||
|
out, err := exec.Command("go", "env", name).Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
func fatal(format string, args ...any) {
|
func fatal(format string, args ...any) {
|
||||||
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
|
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||||
// Source: cmd/internal/obj/x86/anames.go from the Go toolchain.
|
// Source: cmd/internal/obj/x86/anames.go from the Go toolchain.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
|||||||
+108
-1
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||||
// Source: cmd/internal/obj/arm64/anames.go from the Go toolchain.
|
// Source: cmd/internal/obj/arm64/anames.go from the Go toolchain.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
@@ -364,6 +364,8 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"REVW",
|
"REVW",
|
||||||
"ROR",
|
"ROR",
|
||||||
"RORW",
|
"RORW",
|
||||||
|
"RPRFM",
|
||||||
|
"SB",
|
||||||
"SBC",
|
"SBC",
|
||||||
"SBCS",
|
"SBCS",
|
||||||
"SBCSW",
|
"SBCSW",
|
||||||
@@ -477,23 +479,68 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"UXTH",
|
"UXTH",
|
||||||
"UXTHW",
|
"UXTHW",
|
||||||
"UXTW",
|
"UXTW",
|
||||||
|
"VABS",
|
||||||
"VADD",
|
"VADD",
|
||||||
"VADDP",
|
"VADDP",
|
||||||
"VADDV",
|
"VADDV",
|
||||||
"VAND",
|
"VAND",
|
||||||
"VBCAX",
|
"VBCAX",
|
||||||
|
"VBIC",
|
||||||
"VBIF",
|
"VBIF",
|
||||||
"VBIT",
|
"VBIT",
|
||||||
"VBSL",
|
"VBSL",
|
||||||
|
"VCLS",
|
||||||
|
"VCLZ",
|
||||||
"VCMEQ",
|
"VCMEQ",
|
||||||
|
"VCMGE",
|
||||||
|
"VCMGT",
|
||||||
|
"VCMHI",
|
||||||
|
"VCMHS",
|
||||||
|
"VCMLE",
|
||||||
|
"VCMLT",
|
||||||
"VCMTST",
|
"VCMTST",
|
||||||
"VCNT",
|
"VCNT",
|
||||||
"VDUP",
|
"VDUP",
|
||||||
"VEOR",
|
"VEOR",
|
||||||
"VEOR3",
|
"VEOR3",
|
||||||
"VEXT",
|
"VEXT",
|
||||||
|
"VFABS",
|
||||||
|
"VFADD",
|
||||||
|
"VFADDP",
|
||||||
|
"VFCMEQ",
|
||||||
|
"VFCMGE",
|
||||||
|
"VFCMGT",
|
||||||
|
"VFCMLE",
|
||||||
|
"VFCMLT",
|
||||||
|
"VFCVTL",
|
||||||
|
"VFCVTL2",
|
||||||
|
"VFCVTN",
|
||||||
|
"VFCVTN2",
|
||||||
|
"VFCVTZS",
|
||||||
|
"VFCVTZU",
|
||||||
|
"VFDIV",
|
||||||
|
"VFMAX",
|
||||||
|
"VFMAXNM",
|
||||||
|
"VFMAXNMP",
|
||||||
|
"VFMAXNMV",
|
||||||
|
"VFMAXP",
|
||||||
|
"VFMAXV",
|
||||||
|
"VFMIN",
|
||||||
|
"VFMINNM",
|
||||||
|
"VFMINNMP",
|
||||||
|
"VFMINNMV",
|
||||||
|
"VFMINP",
|
||||||
|
"VFMINV",
|
||||||
"VFMLA",
|
"VFMLA",
|
||||||
"VFMLS",
|
"VFMLS",
|
||||||
|
"VFMUL",
|
||||||
|
"VFNEG",
|
||||||
|
"VFRINTM",
|
||||||
|
"VFRINTN",
|
||||||
|
"VFRINTP",
|
||||||
|
"VFRINTZ",
|
||||||
|
"VFSQRT",
|
||||||
|
"VFSUB",
|
||||||
"VLD1",
|
"VLD1",
|
||||||
"VLD1R",
|
"VLD1R",
|
||||||
"VLD2",
|
"VLD2",
|
||||||
@@ -502,11 +549,17 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"VLD3R",
|
"VLD3R",
|
||||||
"VLD4",
|
"VLD4",
|
||||||
"VLD4R",
|
"VLD4R",
|
||||||
|
"VMLA",
|
||||||
|
"VMLS",
|
||||||
"VMOV",
|
"VMOV",
|
||||||
"VMOVD",
|
"VMOVD",
|
||||||
"VMOVI",
|
"VMOVI",
|
||||||
"VMOVQ",
|
"VMOVQ",
|
||||||
"VMOVS",
|
"VMOVS",
|
||||||
|
"VMUL",
|
||||||
|
"VNEG",
|
||||||
|
"VNOT",
|
||||||
|
"VORN",
|
||||||
"VORR",
|
"VORR",
|
||||||
"VPMULL",
|
"VPMULL",
|
||||||
"VPMULL2",
|
"VPMULL2",
|
||||||
@@ -515,14 +568,47 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"VREV16",
|
"VREV16",
|
||||||
"VREV32",
|
"VREV32",
|
||||||
"VREV64",
|
"VREV64",
|
||||||
|
"VSCVTF",
|
||||||
|
"VSHADD",
|
||||||
"VSHL",
|
"VSHL",
|
||||||
|
"VSHRN",
|
||||||
|
"VSHRN2",
|
||||||
"VSLI",
|
"VSLI",
|
||||||
|
"VSMAX",
|
||||||
|
"VSMAXP",
|
||||||
|
"VSMAXV",
|
||||||
|
"VSMIN",
|
||||||
|
"VSMINP",
|
||||||
|
"VSMINV",
|
||||||
|
"VSMLAL",
|
||||||
|
"VSMLAL2",
|
||||||
|
"VSMLSL",
|
||||||
|
"VSMLSL2",
|
||||||
|
"VSMULL",
|
||||||
|
"VSMULL2",
|
||||||
|
"VSQABS",
|
||||||
|
"VSQADD",
|
||||||
|
"VSQNEG",
|
||||||
|
"VSQSHL",
|
||||||
|
"VSQSUB",
|
||||||
|
"VSQXTN",
|
||||||
|
"VSQXTN2",
|
||||||
|
"VSQXTUN",
|
||||||
|
"VSQXTUN2",
|
||||||
|
"VSRHADD",
|
||||||
"VSRI",
|
"VSRI",
|
||||||
|
"VSRSHR",
|
||||||
|
"VSSHL",
|
||||||
|
"VSSHLL",
|
||||||
|
"VSSHLL2",
|
||||||
|
"VSSHR",
|
||||||
"VST1",
|
"VST1",
|
||||||
"VST2",
|
"VST2",
|
||||||
"VST3",
|
"VST3",
|
||||||
"VST4",
|
"VST4",
|
||||||
"VSUB",
|
"VSUB",
|
||||||
|
"VSXTL",
|
||||||
|
"VSXTL2",
|
||||||
"VTBL",
|
"VTBL",
|
||||||
"VTBX",
|
"VTBX",
|
||||||
"VTRN1",
|
"VTRN1",
|
||||||
@@ -530,8 +616,27 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"VUADDLV",
|
"VUADDLV",
|
||||||
"VUADDW",
|
"VUADDW",
|
||||||
"VUADDW2",
|
"VUADDW2",
|
||||||
|
"VUCVTF",
|
||||||
|
"VUHADD",
|
||||||
"VUMAX",
|
"VUMAX",
|
||||||
|
"VUMAXP",
|
||||||
|
"VUMAXV",
|
||||||
"VUMIN",
|
"VUMIN",
|
||||||
|
"VUMINP",
|
||||||
|
"VUMINV",
|
||||||
|
"VUMLAL",
|
||||||
|
"VUMLAL2",
|
||||||
|
"VUMLSL",
|
||||||
|
"VUMLSL2",
|
||||||
|
"VUMULL",
|
||||||
|
"VUMULL2",
|
||||||
|
"VUQADD",
|
||||||
|
"VUQSHL",
|
||||||
|
"VUQSUB",
|
||||||
|
"VUQXTN",
|
||||||
|
"VUQXTN2",
|
||||||
|
"VURHADD",
|
||||||
|
"VUSHL",
|
||||||
"VUSHLL",
|
"VUSHLL",
|
||||||
"VUSHLL2",
|
"VUSHLL2",
|
||||||
"VUSHR",
|
"VUSHR",
|
||||||
@@ -541,6 +646,8 @@ var arm64GeneratedInstrs = []string{
|
|||||||
"VUZP1",
|
"VUZP1",
|
||||||
"VUZP2",
|
"VUZP2",
|
||||||
"VXAR",
|
"VXAR",
|
||||||
|
"VXTN",
|
||||||
|
"VXTN2",
|
||||||
"VZIP1",
|
"VZIP1",
|
||||||
"VZIP2",
|
"VZIP2",
|
||||||
"WFE",
|
"WFE",
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||||
// Source: cmd/internal/obj/util.go from the Go toolchain.
|
// Source: cmd/internal/obj/util.go from the Go toolchain.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||||
// Source: cmd/internal/obj/loong64/anames.go from the Go toolchain.
|
// Source: cmd/internal/obj/loong64/anames.go from the Go toolchain.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
@@ -152,6 +152,8 @@ var loong64GeneratedInstrs = []string{
|
|||||||
"FNMADDF",
|
"FNMADDF",
|
||||||
"FNMSUBD",
|
"FNMSUBD",
|
||||||
"FNMSUBF",
|
"FNMSUBF",
|
||||||
|
"FRINTD",
|
||||||
|
"FRINTF",
|
||||||
"FSCALEBD",
|
"FSCALEBD",
|
||||||
"FSCALEBF",
|
"FSCALEBF",
|
||||||
"FSEL",
|
"FSEL",
|
||||||
@@ -177,7 +179,10 @@ var loong64GeneratedInstrs = []string{
|
|||||||
"FTINTWF",
|
"FTINTWF",
|
||||||
"JIRL",
|
"JIRL",
|
||||||
"LL",
|
"LL",
|
||||||
|
"LLACQV",
|
||||||
|
"LLACQW",
|
||||||
"LLV",
|
"LLV",
|
||||||
|
"LLW",
|
||||||
"LU12IW",
|
"LU12IW",
|
||||||
"LU32ID",
|
"LU32ID",
|
||||||
"LU52ID",
|
"LU52ID",
|
||||||
@@ -248,7 +253,11 @@ var loong64GeneratedInstrs = []string{
|
|||||||
"ROTR",
|
"ROTR",
|
||||||
"ROTRV",
|
"ROTRV",
|
||||||
"SC",
|
"SC",
|
||||||
|
"SCQ",
|
||||||
|
"SCRELV",
|
||||||
|
"SCRELW",
|
||||||
"SCV",
|
"SCV",
|
||||||
|
"SCW",
|
||||||
"SGT",
|
"SGT",
|
||||||
"SGTU",
|
"SGTU",
|
||||||
"SLL",
|
"SLL",
|
||||||
|
|||||||
+32
-1
@@ -1,4 +1,4 @@
|
|||||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||||
// Source: cmd/internal/obj/riscv/anames.go from the Go toolchain.
|
// Source: cmd/internal/obj/riscv/anames.go from the Go toolchain.
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
@@ -81,6 +81,9 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"CLD",
|
"CLD",
|
||||||
"CLDSP",
|
"CLDSP",
|
||||||
"CLI",
|
"CLI",
|
||||||
|
"CLMUL",
|
||||||
|
"CLMULH",
|
||||||
|
"CLMULR",
|
||||||
"CLUI",
|
"CLUI",
|
||||||
"CLW",
|
"CLW",
|
||||||
"CLWSP",
|
"CLWSP",
|
||||||
@@ -95,13 +98,20 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"CSDSP",
|
"CSDSP",
|
||||||
"CSLLI",
|
"CSLLI",
|
||||||
"CSRAI",
|
"CSRAI",
|
||||||
|
"CSRC",
|
||||||
|
"CSRCI",
|
||||||
"CSRLI",
|
"CSRLI",
|
||||||
|
"CSRR",
|
||||||
"CSRRC",
|
"CSRRC",
|
||||||
"CSRRCI",
|
"CSRRCI",
|
||||||
"CSRRS",
|
"CSRRS",
|
||||||
"CSRRSI",
|
"CSRRSI",
|
||||||
"CSRRW",
|
"CSRRW",
|
||||||
"CSRRWI",
|
"CSRRWI",
|
||||||
|
"CSRS",
|
||||||
|
"CSRSI",
|
||||||
|
"CSRW",
|
||||||
|
"CSRWI",
|
||||||
"CSUB",
|
"CSUB",
|
||||||
"CSUBW",
|
"CSUBW",
|
||||||
"CSW",
|
"CSW",
|
||||||
@@ -259,6 +269,7 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"ORCB",
|
"ORCB",
|
||||||
"ORI",
|
"ORI",
|
||||||
"ORN",
|
"ORN",
|
||||||
|
"PAUSE",
|
||||||
"RDCYCLE",
|
"RDCYCLE",
|
||||||
"RDINSTRET",
|
"RDINSTRET",
|
||||||
"RDTIME",
|
"RDTIME",
|
||||||
@@ -322,6 +333,8 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"VADDVI",
|
"VADDVI",
|
||||||
"VADDVV",
|
"VADDVV",
|
||||||
"VADDVX",
|
"VADDVX",
|
||||||
|
"VANDNVV",
|
||||||
|
"VANDNVX",
|
||||||
"VANDVI",
|
"VANDVI",
|
||||||
"VANDVV",
|
"VANDVV",
|
||||||
"VANDVX",
|
"VANDVX",
|
||||||
@@ -329,8 +342,17 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"VASUBUVX",
|
"VASUBUVX",
|
||||||
"VASUBVV",
|
"VASUBVV",
|
||||||
"VASUBVX",
|
"VASUBVX",
|
||||||
|
"VBREV8V",
|
||||||
|
"VBREVV",
|
||||||
|
"VCLMULHVV",
|
||||||
|
"VCLMULHVX",
|
||||||
|
"VCLMULVV",
|
||||||
|
"VCLMULVX",
|
||||||
|
"VCLZV",
|
||||||
"VCOMPRESSVM",
|
"VCOMPRESSVM",
|
||||||
"VCPOPM",
|
"VCPOPM",
|
||||||
|
"VCPOPV",
|
||||||
|
"VCTZV",
|
||||||
"VDIVUVV",
|
"VDIVUVV",
|
||||||
"VDIVUVX",
|
"VDIVUVX",
|
||||||
"VDIVVV",
|
"VDIVVV",
|
||||||
@@ -743,10 +765,16 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"VREMUVX",
|
"VREMUVX",
|
||||||
"VREMVV",
|
"VREMVV",
|
||||||
"VREMVX",
|
"VREMVX",
|
||||||
|
"VREV8V",
|
||||||
"VRGATHEREI16VV",
|
"VRGATHEREI16VV",
|
||||||
"VRGATHERVI",
|
"VRGATHERVI",
|
||||||
"VRGATHERVV",
|
"VRGATHERVV",
|
||||||
"VRGATHERVX",
|
"VRGATHERVX",
|
||||||
|
"VROLVV",
|
||||||
|
"VROLVX",
|
||||||
|
"VRORVI",
|
||||||
|
"VRORVV",
|
||||||
|
"VRORVX",
|
||||||
"VRSUBVI",
|
"VRSUBVI",
|
||||||
"VRSUBVX",
|
"VRSUBVX",
|
||||||
"VS1RV",
|
"VS1RV",
|
||||||
@@ -950,6 +978,9 @@ var riscvGeneratedInstrs = []string{
|
|||||||
"VWMULVX",
|
"VWMULVX",
|
||||||
"VWREDSUMUVS",
|
"VWREDSUMUVS",
|
||||||
"VWREDSUMVS",
|
"VWREDSUMVS",
|
||||||
|
"VWSLLVI",
|
||||||
|
"VWSLLVV",
|
||||||
|
"VWSLLVX",
|
||||||
"VWSUBUVV",
|
"VWSUBUVV",
|
||||||
"VWSUBUVX",
|
"VWSUBUVX",
|
||||||
"VWSUBUWV",
|
"VWSUBUWV",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
||||||
|
|||||||
+38
-15
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
||||||
@@ -637,6 +637,20 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
|||||||
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bare spelling BL R9 is the same indirect branch: the parser reads
|
||||||
|
// a bare identifier as a symbol, and one named for a register is an
|
||||||
|
// indirect branch through it, which the toolchain accepts alongside the
|
||||||
|
// parenthesised form (BL (R3) and BL R3 both encode BLR R3).
|
||||||
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" && op.Addr.Base == "" && op.Addr.Index == "" {
|
||||||
|
if rn := arm64RegNum(op.Addr.Sym.Name); rn >= 0 {
|
||||||
|
opc := uint32(0) // BR
|
||||||
|
if link {
|
||||||
|
opc = 1 // BLR
|
||||||
|
}
|
||||||
|
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
||||||
// relocation (R_CALLARM64 either way).
|
// relocation (R_CALLARM64 either way).
|
||||||
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
||||||
@@ -1454,6 +1468,15 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
|
|||||||
}
|
}
|
||||||
return encodeARM64SBAddr(src.Imm.Sym, rd, relocs), nil
|
return encodeARM64SBAddr(src.Imm.Sym, rd, relocs), nil
|
||||||
}
|
}
|
||||||
|
// Immediate → memory: only storing zero is encodable (the ZR
|
||||||
|
// register); the toolchain rejects any other immediate-to-memory
|
||||||
|
// combination ("illegal combination").
|
||||||
|
if isMemOperand(dst) {
|
||||||
|
if arm64Imm64(src) != 0 {
|
||||||
|
return nil, fmt.Errorf("%s: illegal combination: an immediate store must be zero", mnem)
|
||||||
|
}
|
||||||
|
return encodeARM64MemOp(mnem, dst, 31, false, fi, "")
|
||||||
|
}
|
||||||
rd := arm64RegNum(operandRegName(dst))
|
rd := arm64RegNum(operandRegName(dst))
|
||||||
if rd < 0 {
|
if rd < 0 {
|
||||||
return nil, fmt.Errorf("%s $imm: invalid destination register", mnem)
|
return nil, fmt.Errorf("%s $imm: invalid destination register", mnem)
|
||||||
@@ -3046,29 +3069,29 @@ func encodeARM64MoveWide(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte
|
|||||||
// base, so MOVZ and MOVN come along for free.
|
// base, so MOVZ and MOVN come along for free.
|
||||||
opc := baseOp >> 29 & 3
|
opc := baseOp >> 29 & 3
|
||||||
sf := baseOp >> 31 & 1
|
sf := baseOp >> 31 & 1
|
||||||
v := arm64Imm64(ops[0])
|
// The toolchain's optab case 33, shared by the whole family in both
|
||||||
if v < 0 {
|
// widths: the immediate is one unsigned 64-bit pattern (a high-lane
|
||||||
return nil, fmt.Errorf("%s: negative immediate %d", mnem, v)
|
// constant such as $(40000<<48) arrives negative through int64
|
||||||
|
// folding), it must occupy exactly one 16-bit lane, zero is rejected,
|
||||||
|
// and the W forms cannot reach the top half.
|
||||||
|
u := uint64(arm64Imm64(ops[0]))
|
||||||
|
if u == 0 {
|
||||||
|
return nil, fmt.Errorf("%s: zero immediate cannot be handled", mnem)
|
||||||
}
|
}
|
||||||
hw := -1
|
hw := -1
|
||||||
for i := range 4 {
|
for lane := range 4 {
|
||||||
if v>>(uint(i)*16)&0xFFFF != 0 {
|
if u&^(uint64(0xFFFF)<<(lane*16)) == 0 {
|
||||||
hw = i
|
hw = lane
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hw < 0 {
|
if hw < 0 {
|
||||||
hw = 0 // zero: every chunk is zero, hw = 0 carries it
|
return nil, fmt.Errorf("%s: immediate %#x does not fit one 16-bit chunk", mnem, u)
|
||||||
}
|
|
||||||
for i := hw + 1; i < 4; i++ {
|
|
||||||
if v>>(uint(i)*16)&0xFFFF != 0 {
|
|
||||||
return nil, fmt.Errorf("%s: immediate %d does not fit one 16-bit chunk", mnem, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if sf == 0 && hw > 1 {
|
if sf == 0 && hw > 1 {
|
||||||
return nil, fmt.Errorf("%s: immediate %d out of range for the 32-bit form", mnem, v)
|
return nil, fmt.Errorf("%s: immediate %#x out of range for the 32-bit form", mnem, u)
|
||||||
}
|
}
|
||||||
return a64wordLE(a64MoveWide(sf, opc, uint32(hw), uint32(v>>uint(hw*16)&0xFFFF), uint32(rd))), nil
|
return a64wordLE(a64MoveWide(sf, opc, uint32(hw), uint32(u>>uint(hw*16)&0xFFFF), uint32(rd))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Bitfield/EXTR encoding ----
|
// ---- Bitfield/EXTR encoding ----
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestArm64LDRSTREncoding(t *testing.T) {
|
func TestArm64LDRSTREncoding(t *testing.T) {
|
||||||
@@ -1051,6 +1051,41 @@ func TestArm64MOVK(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestArm64MOVKHighLane pins the shifted high-lane immediate the arm64 test
|
||||||
|
// kernels write: $(40000<<48) folds to a negative int64, and the toolchain
|
||||||
|
// reads the value as an unsigned 64-bit pattern when it picks the lane.
|
||||||
|
func TestArm64MOVKHighLane(t *testing.T) {
|
||||||
|
got := arm64Words(t, "\tMOVK $(40000<<48), R0\n\tMOVK $0x9c40000000000000, R1\n")
|
||||||
|
want := []uint32{
|
||||||
|
0xf2f38800, // MOVK $(40000<<48), R0 (go tool asm: f2f38800)
|
||||||
|
0xf2f38801, // MOVK hw=3
|
||||||
|
0xd65f03c0,
|
||||||
|
}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("word count = %d, want %d", len(got), len(want))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestArm64MoveWideZeroImmediate pins the toolchain's rejection of a zero
|
||||||
|
// immediate in the move-wide family (optab case 33: "zero shifts cannot be
|
||||||
|
// handled"): every lane is zero, so no hw field can carry it.
|
||||||
|
func TestArm64MoveWideZeroImmediate(t *testing.T) {
|
||||||
|
for _, mnem := range []string{"MOVK", "MOVZ", "MOVN"} {
|
||||||
|
f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t"+mnem+" $0, R0\n\tRET\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", mnem, errs)
|
||||||
|
}
|
||||||
|
if _, err := AssembleFileARM64(f); err == nil {
|
||||||
|
t.Errorf("%s $0: expected error, got nil", mnem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestArm64LoadImm64 tests 64-bit immediate loading.
|
// TestArm64LoadImm64 tests 64-bit immediate loading.
|
||||||
func TestArm64LoadImm64(t *testing.T) {
|
func TestArm64LoadImm64(t *testing.T) {
|
||||||
src := `#include "textflag.h"
|
src := `#include "textflag.h"
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseArm64File is a helper assembling one arm64 source file.
|
// parseArm64File is a helper assembling one arm64 source file.
|
||||||
|
|||||||
+365
-16
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assemble encodes the body of a TEXT function into x86-64 machine code,
|
// Assemble encodes the body of a TEXT function into x86-64 machine code,
|
||||||
@@ -38,10 +38,25 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
|||||||
// rejects SB operands outright (single-function assembly cannot resolve
|
// rejects SB operands outright (single-function assembly cannot resolve
|
||||||
// them). When allowExternal is set, a reference to a symbol no GLOBL in the
|
// them). When allowExternal is set, a reference to a symbol no GLOBL in the
|
||||||
// file defines is recorded as an external relocation instead of failing
|
// file defines is recorded as an external relocation instead of failing
|
||||||
// the object-file emitters resolve it at link time.
|
// the object-file emitters resolve it at link time. goos selects the TLS
|
||||||
|
// access form: the empty default behaves as linux.
|
||||||
type linkInfo struct {
|
type linkInfo struct {
|
||||||
symbols map[string]bool
|
symbols map[string]bool
|
||||||
allowExternal bool
|
allowExternal bool
|
||||||
|
goos string
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsOneInsn reports the one-instruction TLS form, obj6.go's
|
||||||
|
// CanUse1InsnTLS for the GOOS gasm supports: the bare TLS load nops out and
|
||||||
|
// the (TLS*1) index folds to a segment-absolute access. Windows and plan9
|
||||||
|
// keep the two-instruction form; shared linux does too, which gasm's raw
|
||||||
|
// path does not model and therefore does not select.
|
||||||
|
func (l *linkInfo) tlsOneInsn() bool {
|
||||||
|
switch l.goos {
|
||||||
|
case "", "linux", "freebsd":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
// sbPatch is a function-relative static-symbol relocation: the disp32 field
|
||||||
@@ -86,6 +101,10 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
// outgrows the short form.
|
// outgrows the short form.
|
||||||
long := make([]bool, len(t.Body))
|
long := make([]bool, len(t.Body))
|
||||||
sizes := make([]int, len(t.Body))
|
sizes := make([]int, len(t.Body))
|
||||||
|
numTargets := make([]int, len(t.Body))
|
||||||
|
for i := range numTargets {
|
||||||
|
numTargets[i] = -1
|
||||||
|
}
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pcs := make([]int, len(t.Body))
|
pcs := make([]int, len(t.Body))
|
||||||
var guardJBlong, guardJBElong, moreJMPlong bool
|
var guardJBlong, guardJBElong, moreJMPlong bool
|
||||||
@@ -94,23 +113,106 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
for {
|
for {
|
||||||
guard := fi.guardLen(guardJBlong, guardJBElong)
|
guard := fi.guardLen(guardJBlong, guardJBElong)
|
||||||
pos := guard + len(fi.prologue)
|
pos := guard + len(fi.prologue)
|
||||||
|
for i := range numTargets {
|
||||||
|
numTargets[i] = -1
|
||||||
|
}
|
||||||
|
idxAtPc := map[int]int{}
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
offsets[s.Name.Text] = pos
|
offsets[s.Name.Text] = pos
|
||||||
case *ast.Instr:
|
case *ast.Instr:
|
||||||
|
if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" {
|
||||||
|
// The alignment pseudo-statement: its size is the
|
||||||
|
// padding to the next boundary at this very position,
|
||||||
|
// filled with NOPs at emission.
|
||||||
|
pad, err := pcAlignPad(pcAlignValue(s), pos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("PCALIGN: %w", err)
|
||||||
|
}
|
||||||
|
sizes[i] = pad
|
||||||
|
pcs[i] = pos
|
||||||
|
pos += pad
|
||||||
|
continue
|
||||||
|
}
|
||||||
sz, err := instrSize(s, fi, long[i], link)
|
sz, err := instrSize(s, fi, long[i], link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
sizes[i] = sz
|
sizes[i] = sz
|
||||||
pcs[i] = pos
|
pcs[i] = pos
|
||||||
|
idxAtPc[pos] = i
|
||||||
pos += sz
|
pos += sz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bodyLen := pos - (guard + len(fi.prologue))
|
bodyLen := pos - (guard + len(fi.prologue))
|
||||||
// Expand any short jump whose displacement no longer fits rel8.
|
// Expand any short jump whose displacement no longer fits rel8.
|
||||||
changed := false
|
changed := false
|
||||||
|
// Numeric ±N(PC) jumps resolve against this iteration's layout; the
|
||||||
|
// emission pass reads the same table after the loop converges. A
|
||||||
|
// target that is itself an unconditional local JMP is chased to the
|
||||||
|
// ultimate target: the toolchain's brloop pass collapses branch-to-
|
||||||
|
// branch chains before it encodes, so matching its bytes requires
|
||||||
|
// the same redirection.
|
||||||
|
for i := range numTargets {
|
||||||
|
numTargets[i] = -1
|
||||||
|
}
|
||||||
|
for i, stmt := range t.Body {
|
||||||
|
s, ok := stmt.(*ast.Instr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(s.Operands) == 1 {
|
||||||
|
if n, isNum := pcJumpOffset(s.Operands[0]); isNum {
|
||||||
|
if target, okT := pcJumpTarget(t, i, n, pcs); okT {
|
||||||
|
numTargets[i] = target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range numTargets {
|
||||||
|
if numTargets[i] < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tgt := numTargets[i]
|
||||||
|
for hop := 0; hop < len(t.Body); hop++ {
|
||||||
|
idx, ok := idxAtPc[tgt]
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
in, ok := t.Body[idx].(*ast.Instr)
|
||||||
|
if !ok || strings.ToUpper(in.Mnemonic.Text) != "JMP" || len(in.Operands) != 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if name, isLabel := labelName(in.Operands[0]); isLabel {
|
||||||
|
tgt = offsets[resolve(name)]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n, isNum := pcJumpOffset(in.Operands[0]); isNum {
|
||||||
|
next, okT := pcJumpTarget(t, idx, n, pcs)
|
||||||
|
if !okT {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tgt = next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break // JMP through a register or memory: the chain ends
|
||||||
|
}
|
||||||
|
numTargets[i] = tgt
|
||||||
|
}
|
||||||
|
for i, stmt := range t.Body {
|
||||||
|
s, ok := stmt.(*ast.Instr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if numTargets[i] >= 0 && !long[i] {
|
||||||
|
rel := int64(numTargets[i] - (pcs[i] + jumpSize(strings.ToUpper(s.Mnemonic.Text), false)))
|
||||||
|
if !fits8(rel) {
|
||||||
|
long[i] = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
s, ok := stmt.(*ast.Instr)
|
s, ok := stmt.(*ast.Instr)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -232,7 +334,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
spadjStep{pos + epi, 0},
|
spadjStep{pos + epi, 0},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
code, ps, pool, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
code, ps, pool, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link, numTargets[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
@@ -428,6 +530,99 @@ func computeFrame(t *ast.Text) frameInfo {
|
|||||||
return fi
|
return fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pcJumpOffset recognises the numeric relative jump operand ±N(PC) and
|
||||||
|
// returns N: the toolchain counts instructions, not bytes, so +2(PC) targets
|
||||||
|
// the second instruction boundary after the branch.
|
||||||
|
func pcJumpOffset(op *ast.Operand) (int, bool) {
|
||||||
|
if op.Kind != ast.OpAddr || op.Addr.Base != "PC" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int(op.Addr.Offset), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// pcJumpTarget resolves a numeric jump at statement index j: N counts the
|
||||||
|
// instruction statements after the jump itself (N = 0 is the jump's own
|
||||||
|
// address, the classic park loop), and the target is the start of the Nth
|
||||||
|
// one. A negative N counts the same way backwards, before the jump: the
|
||||||
|
// exit loops write JMP -3(PC) to land three instructions earlier. Labels
|
||||||
|
// count not, in either direction. It reports false when the count runs
|
||||||
|
// past the end of the function, or before its first instruction.
|
||||||
|
func pcJumpTarget(t *ast.Text, j, n int, pcs []int) (int, bool) {
|
||||||
|
if n == 0 {
|
||||||
|
return pcs[j], true
|
||||||
|
}
|
||||||
|
if n < 0 {
|
||||||
|
seen := 0
|
||||||
|
for k := j - 1; k >= 0; k-- {
|
||||||
|
if _, ok := t.Body[k].(*ast.Instr); !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen--
|
||||||
|
if seen == n {
|
||||||
|
return pcs[k], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
seen := 0
|
||||||
|
for k := j + 1; k < len(t.Body); k++ {
|
||||||
|
if _, ok := t.Body[k].(*ast.Instr); !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen++
|
||||||
|
if seen == n {
|
||||||
|
return pcs[k], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// x86 NOP encodings, single-instruction no-ops of lengths 1 to 9 (the
|
||||||
|
// toolchain's asm6.go nop table); longer padding repeats the largest that
|
||||||
|
// fits, greedy from the end.
|
||||||
|
var x86Nops = [][]byte{
|
||||||
|
{0x90},
|
||||||
|
{0x66, 0x90},
|
||||||
|
{0x0F, 0x1F, 0x00},
|
||||||
|
{0x0F, 0x1F, 0x40, 0x00},
|
||||||
|
{0x0F, 0x1F, 0x44, 0x00, 0x00},
|
||||||
|
{0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00},
|
||||||
|
{0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
{0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
{0x66, 0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillNOPs fills p with the greedy largest single-instruction NOPs, exactly
|
||||||
|
// the toolchain's fillnop.
|
||||||
|
func fillNOPs(p []byte) {
|
||||||
|
for len(p) > 0 {
|
||||||
|
m := min(len(p), len(x86Nops))
|
||||||
|
copy(p[:m], x86Nops[m-1])
|
||||||
|
p = p[m:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pcAlignPad computes the padding PCALIGN $align inserts at pos: the
|
||||||
|
// alignment must be a power of two in [8, 2048] and the padding runs to the
|
||||||
|
// next boundary (zero when the position is already aligned).
|
||||||
|
func pcAlignPad(align, pos int) (int, error) {
|
||||||
|
if align <= 0 || align&(align-1) != 0 || align < 8 || align > 2048 {
|
||||||
|
return 0, fmt.Errorf("alignment value of an instruction must be a power of two and in the range [8, 2048], got %d", align)
|
||||||
|
}
|
||||||
|
if lob := pos & (align - 1); lob != 0 {
|
||||||
|
return align - lob, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pcAlignValue reads a PCALIGN statement's alignment operand.
|
||||||
|
func pcAlignValue(s *ast.Instr) int {
|
||||||
|
if len(s.Operands) == 1 && s.Operands[0].Kind == ast.OpImmediate && s.Operands[0].Imm.HasVal {
|
||||||
|
return int(s.Operands[0].Imm.Val)
|
||||||
|
}
|
||||||
|
return 0 // rejected by pcAlignPad's range check
|
||||||
|
}
|
||||||
|
|
||||||
// hasCall reports whether the function body contains a CALL instruction.
|
// hasCall reports whether the function body contains a CALL instruction.
|
||||||
func hasCall(t *ast.Text) bool {
|
func hasCall(t *ast.Text) bool {
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
@@ -602,7 +797,7 @@ func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, erro
|
|||||||
}
|
}
|
||||||
return jumpSize(mnem, long), nil
|
return jumpSize(mnem, long), nil
|
||||||
}
|
}
|
||||||
code, _, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
code, _, _, err := encodeInstr(s, 0, nil, fi, false, nil, link, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -637,9 +832,21 @@ func jumpSize(mnem string, long bool) int {
|
|||||||
// (relative to pc, the instruction's own offset). A RET in a frame-pointer
|
// (relative to pc, the instruction's own offset). A RET in a frame-pointer
|
||||||
// function is prefixed with the epilogue. resolve, when non-nil, redirects a
|
// function is prefixed with the epilogue. resolve, when non-nil, redirects a
|
||||||
// jump label through the jump-to-jump chain before the offset lookup.
|
// jump label through the jump-to-jump chain before the offset lookup.
|
||||||
func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string, link *linkInfo) ([]byte, []sbPatch, []floatPoolEntry, error) {
|
func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, long bool, resolve func(string) string, link *linkInfo, numTarget int) ([]byte, []sbPatch, []floatPoolEntry, error) {
|
||||||
mnem := strings.ToUpper(s.Mnemonic.Text)
|
mnem := strings.ToUpper(s.Mnemonic.Text)
|
||||||
|
|
||||||
|
if mnem == "PCALIGN" {
|
||||||
|
// The layout pass already accounted the padding; emit the same
|
||||||
|
// amount of NOP bytes for the statement's own position.
|
||||||
|
pad, err := pcAlignPad(pcAlignValue(s), pc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
out := make([]byte, pad)
|
||||||
|
fillNOPs(out)
|
||||||
|
return out, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
var prefix []byte
|
var prefix []byte
|
||||||
if mnem == "RET" && fi.useFP {
|
if mnem == "RET" && fi.useFP {
|
||||||
prefix = fi.epilogue
|
prefix = fi.epilogue
|
||||||
@@ -677,7 +884,7 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
|||||||
}
|
}
|
||||||
return append(prefix, code...), nil, nil, nil
|
return append(prefix, code...), nil, nil, nil
|
||||||
}
|
}
|
||||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve, numTarget)
|
||||||
} else {
|
} else {
|
||||||
code, ps, pool, err = encodeNormal(s, fi, link)
|
code, ps, pool, err = encodeNormal(s, fi, link)
|
||||||
}
|
}
|
||||||
@@ -703,6 +910,41 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
|
|||||||
}
|
}
|
||||||
return code, nil, nil, nil
|
return code, nil, nil, nil
|
||||||
}
|
}
|
||||||
|
// MOVQ $sym±off(SB), r64: the toolchain assembles a symbol immediate as
|
||||||
|
// LEAQ disp32(RIP), r64 with an R_PCREL relocation at the disp32 field,
|
||||||
|
// never as a 64-bit absolute immediate (verified against go tool asm).
|
||||||
|
// MOVD is the MOVQ alias; the narrower widths reject the form outright.
|
||||||
|
if (mnemUpper == "MOVQ" || mnemUpper == "MOVD") && len(s.Operands) == 2 &&
|
||||||
|
s.Operands[0].Kind == ast.OpImmediate && s.Operands[0].Imm.Sym != nil &&
|
||||||
|
s.Operands[0].Imm.Sym.Pseudo == "SB" {
|
||||||
|
mem := &ast.Operand{Kind: ast.OpAddr, Addr: ast.Address{Sym: s.Operands[0].Imm.Sym}}
|
||||||
|
src, err := operandFromAST(mnemUpper, mem, 8, fi, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
dst, err := operandFromAST(mnemUpper, s.Operands[1], 8, fi, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
e := &enc{}
|
||||||
|
if err := e.encodeLea([]Operand{src, dst}, 8); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
ps := make([]sbPatch, len(e.patches))
|
||||||
|
for i, p := range e.patches {
|
||||||
|
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend}
|
||||||
|
}
|
||||||
|
return e.out, ps, nil, nil
|
||||||
|
}
|
||||||
|
// MOVQ/MOVL TLS, r: the bare TLS load. The toolchain's progedit nops
|
||||||
|
// it out on the one-instruction TLS systems (linux and freebsd, not
|
||||||
|
// shared) and encodes the segment-prefixed load elsewhere; get_tls(r),
|
||||||
|
// the macro GOROOT's go_tls.h defines, expands to exactly this
|
||||||
|
// statement, and the toolchain's pairing pass removes it whenever the
|
||||||
|
// following instruction's (TLS*1) index folds.
|
||||||
|
if (mnemUpper == "MOVQ" || mnemUpper == "MOVL") && len(s.Operands) == 2 && isBareTLS(s.Operands[0]) {
|
||||||
|
return encodeTLSBaseLoad(s, fi, link)
|
||||||
|
}
|
||||||
_, size := splitSize(mnemUpper)
|
_, size := splitSize(mnemUpper)
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
size = 8
|
size = 8
|
||||||
@@ -722,10 +964,67 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
|
|||||||
ps := make([]sbPatch, len(e.patches))
|
ps := make([]sbPatch, len(e.patches))
|
||||||
for i, p := range e.patches {
|
for i, p := range e.patches {
|
||||||
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend}
|
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend}
|
||||||
|
if p.tls {
|
||||||
|
ps[i].kind = RelTLSLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e.out, ps, e.floatPoolList(), nil
|
return e.out, ps, e.floatPoolList(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBareTLS reports whether the operand is the bare TLS pseudo-register
|
||||||
|
// load source, the expansion of go_tls.h's get_tls(r) macro.
|
||||||
|
func isBareTLS(op *ast.Operand) bool {
|
||||||
|
return op.Kind == ast.OpAddr && op.Addr.Sym != nil &&
|
||||||
|
op.Addr.Sym.Pseudo == "" && op.Addr.Sym.Name == "TLS" &&
|
||||||
|
op.Addr.Base == "" && op.Addr.Index == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeTLSBaseLoad assembles MOVQ/MOVL TLS, r. On the one-instruction TLS
|
||||||
|
// systems (linux and freebsd outside -shared, obj6.go's CanUse1InsnTLS) the
|
||||||
|
// statement nops out: the following (TLS*1) access folds to a direct
|
||||||
|
// segment-absolute load. The two-instruction systems keep the segment load,
|
||||||
|
// nine bytes with the R_TLSLE patch site at the disp32.
|
||||||
|
func encodeTLSBaseLoad(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, []floatPoolEntry, error) {
|
||||||
|
_, size := splitSize(strings.ToUpper(s.Mnemonic.Text))
|
||||||
|
if size == 0 {
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
dst, err := operandFromAST("MOVQ", s.Operands[1], 8, fi, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
reg, ok := dst.(Reg)
|
||||||
|
if !ok || reg.isVec() {
|
||||||
|
return nil, nil, nil, fmt.Errorf("TLS: destination must be a general register")
|
||||||
|
}
|
||||||
|
if link == nil || link.tlsOneInsn() {
|
||||||
|
return nil, nil, nil, nil // noped out
|
||||||
|
}
|
||||||
|
seg := byte(0x64) // FS
|
||||||
|
if link.goos == "windows" {
|
||||||
|
seg = 0x65 // GS
|
||||||
|
}
|
||||||
|
e := &enc{}
|
||||||
|
i := &instr{
|
||||||
|
prefix: seg,
|
||||||
|
rexW: size == 8,
|
||||||
|
rexR: reg.idx >= 8,
|
||||||
|
opcode: []byte{0x8B},
|
||||||
|
modrm: 0x04 | (reg.idx&7)<<3,
|
||||||
|
sib: 0x25,
|
||||||
|
disp: le32(0),
|
||||||
|
tls: true,
|
||||||
|
}
|
||||||
|
if err := e.emit(i); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
ps := make([]sbPatch, len(e.patches))
|
||||||
|
for i, p := range e.patches {
|
||||||
|
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend, kind: RelTLSLE}
|
||||||
|
}
|
||||||
|
return e.out, ps, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// encodeBookkeeping accepts-and-ignores FUNCDATA and PCDATA at the statement
|
// encodeBookkeeping accepts-and-ignores FUNCDATA and PCDATA at the statement
|
||||||
// level, before operand conversion: the toolchain's shapes are FUNCDATA
|
// level, before operand conversion: the toolchain's shapes are FUNCDATA
|
||||||
// $n, sym(SB) and PCDATA $n, $m, and neither contributes a byte to the
|
// $n, sym(SB) and PCDATA $n, $m, and neither contributes a byte to the
|
||||||
@@ -753,21 +1052,29 @@ func encodeBookkeeping(upper string, s *ast.Instr) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the
|
// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the
|
||||||
// target label, in the short (rel8) or long (rel32) form.
|
// target label or from a numeric ±N(PC) instruction count, in the short
|
||||||
func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long bool, resolve func(string) string) ([]byte, error) {
|
// (rel8) or long (rel32) form. numTarget is the resolved byte offset of a
|
||||||
|
// numeric operand, negative when the operand is not one.
|
||||||
|
func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long bool, resolve func(string) string, numTarget int) ([]byte, error) {
|
||||||
if len(s.Operands) != 1 {
|
if len(s.Operands) != 1 {
|
||||||
return nil, fmt.Errorf("jump expects 1 operand, got %d", len(s.Operands))
|
return nil, fmt.Errorf("jump expects 1 operand, got %d", len(s.Operands))
|
||||||
}
|
}
|
||||||
name, ok := labelName(s.Operands[0])
|
name, isLabel := labelName(s.Operands[0])
|
||||||
if !ok {
|
if !isLabel && numTarget < 0 {
|
||||||
return nil, fmt.Errorf("jump target must be a local label")
|
return nil, fmt.Errorf("jump target must be a local label")
|
||||||
}
|
}
|
||||||
if resolve != nil && mnem != "CALL" {
|
var target int
|
||||||
name = resolve(name)
|
if isLabel {
|
||||||
}
|
if resolve != nil && mnem != "CALL" {
|
||||||
target, ok := offsets[name]
|
name = resolve(name)
|
||||||
if !ok {
|
}
|
||||||
return nil, fmt.Errorf("undefined label %q", name)
|
t, ok := offsets[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("undefined label %q", name)
|
||||||
|
}
|
||||||
|
target = t
|
||||||
|
} else {
|
||||||
|
target = numTarget
|
||||||
}
|
}
|
||||||
rel := int64(target - (pc + jumpSize(mnem, long)))
|
rel := int64(target - (pc + jumpSize(mnem, long)))
|
||||||
|
|
||||||
@@ -841,6 +1148,11 @@ func indirectJumpTarget(s *ast.Instr) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
a := s.Operands[0].Addr
|
a := s.Operands[0].Addr
|
||||||
|
// ±N(PC) is the numeric relative form, the PC counts instructions from
|
||||||
|
// the branch: relative, not indirect.
|
||||||
|
if a.Base == "PC" || a.Index == "PC" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if a.Base != "" || a.Index != "" {
|
if a.Base != "" || a.Index != "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -958,12 +1270,44 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l
|
|||||||
|
|
||||||
// Memory with a real base register: (base), off(base), (base)(index*scale).
|
// Memory with a real base register: (base), off(base), (base)(index*scale).
|
||||||
if a.Base != "" {
|
if a.Base != "" {
|
||||||
|
// Segment-absolute: 0x30(GS) and 0x28(FS), the windows TLS
|
||||||
|
// spellings. The segment override prefixes a disp32 absolute
|
||||||
|
// reference with no relocation.
|
||||||
|
if a.Base == "GS" || a.Base == "FS" {
|
||||||
|
seg := byte(0x64)
|
||||||
|
if a.Base == "GS" {
|
||||||
|
seg = 0x65
|
||||||
|
}
|
||||||
|
return SegAbs{Disp: a.Offset, Size: size, Seg: seg}, nil
|
||||||
|
}
|
||||||
base, ok := ParseReg(a.Base)
|
base, ok := ParseReg(a.Base)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unknown base register %q", a.Base)
|
return nil, fmt.Errorf("unknown base register %q", a.Base)
|
||||||
}
|
}
|
||||||
m := Mem{Base: base, Disp: a.Offset, HasBase: true, Size: size}
|
m := Mem{Base: base, Disp: a.Offset, HasBase: true, Size: size}
|
||||||
if a.Index != "" {
|
if a.Index != "" {
|
||||||
|
if a.Index == "TLS" {
|
||||||
|
// off(base)(TLS*1): the thread-local annotation. The
|
||||||
|
// one-instruction TLS form folds it to off(TLS), the
|
||||||
|
// segment-prefixed absolute whose disp32 carries an
|
||||||
|
// R_TLS_LE patch site; the base register disappears
|
||||||
|
// from the encoding, exactly as the toolchain's
|
||||||
|
// progedit rewrites the address.
|
||||||
|
seg := byte(0x64) // FS on linux, freebsd, plan9
|
||||||
|
if link != nil && link.goos == "windows" {
|
||||||
|
seg = 0x65 // GS
|
||||||
|
}
|
||||||
|
return TLSMem{Disp: a.Offset, Size: size, Seg: seg}, nil
|
||||||
|
}
|
||||||
|
if a.Index == "GS" || a.Index == "FS" {
|
||||||
|
// 0(CX)(GS): the segment annotation rides the base
|
||||||
|
// access as the override prefix.
|
||||||
|
m.Seg = 0x64
|
||||||
|
if a.Index == "GS" {
|
||||||
|
m.Seg = 0x65
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
idx, ok := ParseReg(a.Index)
|
idx, ok := ParseReg(a.Index)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unknown index register %q", a.Index)
|
return nil, fmt.Errorf("unknown index register %q", a.Index)
|
||||||
@@ -984,6 +1328,11 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l
|
|||||||
}
|
}
|
||||||
return Mem{Index: idx, Scale: a.Scale, Disp: a.Offset, HasIndex: true, Size: size}, nil
|
return Mem{Index: idx, Scale: a.Scale, Disp: a.Offset, HasIndex: true, Size: size}, nil
|
||||||
}
|
}
|
||||||
|
// A bare displacement with no base: the absolute address form,
|
||||||
|
// MOVL $0xf1, 0xf1. No segment and no relocation.
|
||||||
|
if a.Sym == nil && a.Base == "" && a.Index == "" && a.HasOff {
|
||||||
|
return SegAbs{Disp: a.Offset, Size: size}, nil
|
||||||
|
}
|
||||||
// Bare register.
|
// Bare register.
|
||||||
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
||||||
if r, ok := ParseReg(a.Sym.Name); ok {
|
if r, ok := ParseReg(a.Sym.Name); ok {
|
||||||
|
|||||||
+54
-2
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// firstText parses src and returns its first TEXT function.
|
// firstText parses src and returns its first TEXT function.
|
||||||
@@ -370,6 +370,58 @@ end:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAssembleNumericPCJumps pins the numeric ±N(PC) branch operands: N
|
||||||
|
// counts instruction statements, skipping labels, in both directions (the
|
||||||
|
// runtime's exit loops write JMP -3(PC)), N = 0 parks on the jump itself.
|
||||||
|
func TestAssembleNumericPCJumps(t *testing.T) {
|
||||||
|
fn := firstText(t, `
|
||||||
|
#include "textflag.h"
|
||||||
|
TEXT ·exit(SB), NOSPLIT, $0
|
||||||
|
MOVB $1, AL
|
||||||
|
lab:
|
||||||
|
MOVB $2, AL
|
||||||
|
MOVB $3, AL
|
||||||
|
JMP -3(PC)
|
||||||
|
MOVB $4, AL
|
||||||
|
park:
|
||||||
|
JMP 0(PC)
|
||||||
|
MOVB $5, AL
|
||||||
|
JMP 2(PC)
|
||||||
|
MOVB $6, AL
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code, _, err := Assemble(fn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Assemble: %v", err)
|
||||||
|
}
|
||||||
|
// From the Go-assembled function:
|
||||||
|
// MOVB $1, AL b001
|
||||||
|
// MOVB $2, AL b002
|
||||||
|
// MOVB $3, AL b003
|
||||||
|
// JMP -3(PC) ebf8 (three instructions back, past lab:)
|
||||||
|
// MOVB $4, AL b004
|
||||||
|
// JMP 0(PC) ebfe (the park loop)
|
||||||
|
// MOVB $5, AL b005
|
||||||
|
// JMP 2(PC) eb02 (over MOVB $6 to the RET)
|
||||||
|
// MOVB $6, AL b006
|
||||||
|
// RET c3
|
||||||
|
want := []byte{
|
||||||
|
0xb0, 0x01,
|
||||||
|
0xb0, 0x02,
|
||||||
|
0xb0, 0x03,
|
||||||
|
0xeb, 0xf8,
|
||||||
|
0xb0, 0x04,
|
||||||
|
0xeb, 0xfe,
|
||||||
|
0xb0, 0x05,
|
||||||
|
0xeb, 0x02,
|
||||||
|
0xb0, 0x06,
|
||||||
|
0xc3,
|
||||||
|
}
|
||||||
|
if hexBytes(code) != hexBytes(want) {
|
||||||
|
t.Errorf("numeric-PC mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAssemblePrefetch(t *testing.T) {
|
func TestAssemblePrefetch(t *testing.T) {
|
||||||
fn := firstText(t, `
|
fn := firstText(t, `
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ulebIter reads ULEB128 values, the .debug_abbrev and line-header
|
// ulebIter reads ULEB128 values, the .debug_abbrev and line-header
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The object-file tests share one source: two exported functions, one
|
// The object-file tests share one source: two exported functions, one
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestELFRISCVObjectDataRelocation checks that a symbol-valued DATA field
|
// TestELFRISCVObjectDataRelocation checks that a symbol-valued DATA field
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ type encPatch struct {
|
|||||||
off int
|
off int
|
||||||
name string
|
name string
|
||||||
addend int64
|
addend int64
|
||||||
|
tls bool // a TLS slot offset: the patch is R_TLSLE with no symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *enc) encode(mnem string, ops []Operand) error {
|
func (e *enc) encode(mnem string, ops []Operand) error {
|
||||||
@@ -578,6 +579,7 @@ type instr struct {
|
|||||||
disp []byte
|
disp []byte
|
||||||
imm []byte
|
imm []byte
|
||||||
sb *sbRef // static-symbol displacement in disp, awaiting resolution
|
sb *sbRef // static-symbol displacement in disp, awaiting resolution
|
||||||
|
tls bool // the displacement is a TLS slot offset, patched R_TLSLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// sbRef records that an instruction's displacement refers to a static symbol
|
// sbRef records that an instruction's displacement refers to a static symbol
|
||||||
@@ -620,6 +622,9 @@ func (e *enc) emit(i *instr) error {
|
|||||||
if i.sb != nil {
|
if i.sb != nil {
|
||||||
e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend})
|
e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend})
|
||||||
}
|
}
|
||||||
|
if i.tls {
|
||||||
|
e.patches = append(e.patches, encPatch{off: len(e.out), tls: true})
|
||||||
|
}
|
||||||
e.out = append(e.out, i.disp...)
|
e.out = append(e.out, i.disp...)
|
||||||
e.out = append(e.out, i.imm...)
|
e.out = append(e.out, i.imm...)
|
||||||
return nil
|
return nil
|
||||||
@@ -674,12 +679,30 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i
|
|||||||
i.disp = le32(0)
|
i.disp = le32(0)
|
||||||
i.sb = &sbRef{name: r.name, addend: r.addend}
|
i.sb = &sbRef{name: r.name, addend: r.addend}
|
||||||
return nil
|
return nil
|
||||||
|
case TLSMem:
|
||||||
|
// off(TLS): the segment-prefixed absolute access, mod=00 with the
|
||||||
|
// SIB escape's disp32 absolute form. The displacement is the TLS
|
||||||
|
// slot offset, patched by the linker's TLS relocation.
|
||||||
|
i.prefix = r.Seg
|
||||||
|
i.modrm = 0x04 | regField<<3
|
||||||
|
i.sib = 0x25
|
||||||
|
i.disp = le32(r.Disp)
|
||||||
|
i.tls = true
|
||||||
|
return nil
|
||||||
|
case SegAbs:
|
||||||
|
// 0x30(GS): the segment override with the SIB escape's disp32
|
||||||
|
// absolute form, no relocation.
|
||||||
|
setSegAbs(i, regField, r)
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid r/m operand %T", rm)
|
return fmt.Errorf("invalid r/m operand %T", rm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMem(i *instr, regField int, m Mem) error {
|
func setMem(i *instr, regField int, m Mem) error {
|
||||||
|
if m.Seg != 0 {
|
||||||
|
i.prefix = m.Seg
|
||||||
|
}
|
||||||
modrm, sib, disp, xBit, bBit, err := memComponents(regField, m)
|
modrm, sib, disp, xBit, bBit, err := memComponents(regField, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -692,6 +715,16 @@ func setMem(i *instr, regField int, m Mem) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setSegAbs assembles a segment-absolute operand, 0x30(GS): the segment
|
||||||
|
// override with the mod=00 SIB escape's disp32 absolute form and no
|
||||||
|
// relocation.
|
||||||
|
func setSegAbs(i *instr, regField int, m SegAbs) {
|
||||||
|
i.prefix = m.Seg
|
||||||
|
i.modrm = 0x04 | regField<<3
|
||||||
|
i.sib = 0x25
|
||||||
|
i.disp = le32(m.Disp)
|
||||||
|
}
|
||||||
|
|
||||||
// memComponents computes the ModR/M byte (with the given reg field), the SIB
|
// memComponents computes the ModR/M byte (with the given reg field), the SIB
|
||||||
// byte (-1 if none), the displacement bytes, and the high index/base bits, for
|
// byte (-1 if none), the displacement bytes, and the high index/base bits, for
|
||||||
// a memory operand. It is shared by the REX (scalar) and VEX (vector) paths.
|
// a memory operand. It is shared by the REX (scalar) and VEX (vector) paths.
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// decode encodes an instruction and decodes it back, returning the decoded
|
// decode encodes an instruction and decodes it back, returning the decoded
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||||||
|
|||||||
+6
-6
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64,
|
// The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64,
|
||||||
@@ -307,12 +307,12 @@ func TestStackGuardBytesLOONG64(t *testing.T) {
|
|||||||
func TestStackGuardGOObjInternalCall(t *testing.T) {
|
func TestStackGuardGOObjInternalCall(t *testing.T) {
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
src string
|
src string
|
||||||
assemble func(*ast.File) (*Image, error)
|
assemble func(*ast.File, ...AssembleOption) (*Image, error)
|
||||||
}{
|
}{
|
||||||
{"g_amd64.s", AssembleFile},
|
{"g_amd64.s", AssembleFile},
|
||||||
{"g_arm64.s", AssembleFileARM64},
|
{"g_arm64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileARM64(f) }},
|
||||||
{"g_riscv64.s", AssembleFileRISCV},
|
{"g_riscv64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileRISCV(f) }},
|
||||||
{"g_loong64.s", AssembleFileLOONG64},
|
{"g_loong64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileLOONG64(f) }},
|
||||||
} {
|
} {
|
||||||
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
|
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
|
|||||||
+74
-21
@@ -192,6 +192,30 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
|||||||
}
|
}
|
||||||
return e.emit(i)
|
return e.emit(i)
|
||||||
|
|
||||||
|
case TLSMem:
|
||||||
|
if !dstIsReg {
|
||||||
|
return fmt.Errorf("MOV: two memory operands")
|
||||||
|
}
|
||||||
|
// MOV r, off(TLS): the segment-prefixed absolute load, reg=dst,
|
||||||
|
// rm=src(tlsMem) through the SIB escape; the disp32 is the TLS slot
|
||||||
|
// offset with its R_TLSLE patch site.
|
||||||
|
i := newInstr(size, []byte{movRR(size)})
|
||||||
|
if err := setRM(i, dstReg, src, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.emit(i)
|
||||||
|
|
||||||
|
case SegAbs:
|
||||||
|
if !dstIsReg {
|
||||||
|
return fmt.Errorf("MOV: two memory operands")
|
||||||
|
}
|
||||||
|
// MOV r, 0x30(GS): the segment-absolute load.
|
||||||
|
i := newInstr(size, []byte{movRR(size)})
|
||||||
|
if err := setRM(i, dstReg, src, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.emit(i)
|
||||||
|
|
||||||
case Imm:
|
case Imm:
|
||||||
if dstIsReg {
|
if dstIsReg {
|
||||||
v := int64(src)
|
v := int64(src)
|
||||||
@@ -232,11 +256,24 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
|||||||
i.imm = imm
|
i.imm = imm
|
||||||
return e.emit(i)
|
return e.emit(i)
|
||||||
}
|
}
|
||||||
// MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0.
|
// MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0. An immediate in the
|
||||||
|
// destination slot is the absolute-address crash-store spelling,
|
||||||
|
// MOVL $0xf1, 0xf1: the parser reads the trailing bare constant
|
||||||
|
// as an immediate, and the store's disp32 carries the address.
|
||||||
op := byte(0xC7)
|
op := byte(0xC7)
|
||||||
if size == 1 {
|
if size == 1 {
|
||||||
op = 0xC6
|
op = 0xC6
|
||||||
}
|
}
|
||||||
|
if d, ok := dst.(Imm); ok {
|
||||||
|
i := newInstr(size, []byte{op})
|
||||||
|
setSegAbs(i, 0, SegAbs{Disp: int64(d)})
|
||||||
|
immBytes, err := immediate(int64(src), size, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.imm = immBytes
|
||||||
|
return e.emit(i)
|
||||||
|
}
|
||||||
i := newInstr(size, []byte{op})
|
i := newInstr(size, []byte{op})
|
||||||
if err := setRMDigit(i, 0, dst, size); err != nil {
|
if err := setRMDigit(i, 0, dst, size); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -627,7 +664,16 @@ func (e *enc) encodeDoubleShift(base string, ops []Operand, size int) error {
|
|||||||
func (e *enc) encodeImul(ops []Operand, size int) error {
|
func (e *enc) encodeImul(ops []Operand, size int) error {
|
||||||
switch len(ops) {
|
switch len(ops) {
|
||||||
case 2:
|
case 2:
|
||||||
// IMUL r, r/m: 0x0F 0xAF.
|
// Two shapes. The leading-immediate spelling IMUL $imm, r multiplies
|
||||||
|
// r in place (dst = rm = r): the shape GOROOT's clock code writes.
|
||||||
|
// Otherwise IMUL r, r/m: 0x0F 0xAF.
|
||||||
|
if imm, ok := ops[0].(Imm); ok {
|
||||||
|
dstReg, isReg := ops[1].(Reg)
|
||||||
|
if !isReg {
|
||||||
|
return fmt.Errorf("IMUL: destination must be a register")
|
||||||
|
}
|
||||||
|
return e.encodeImulImm(imm, dstReg, dstReg, size)
|
||||||
|
}
|
||||||
dstReg, ok := ops[1].(Reg)
|
dstReg, ok := ops[1].(Reg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("IMUL: destination must be a register")
|
return fmt.Errorf("IMUL: destination must be a register")
|
||||||
@@ -647,29 +693,36 @@ func (e *enc) encodeImul(ops []Operand, size int) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("IMUL: immediate operand expected first")
|
return fmt.Errorf("IMUL: immediate operand expected first")
|
||||||
}
|
}
|
||||||
// Plan 9 order: IMUL $imm, src, dst.
|
// Plan 9 order: IMUL $imm, src, dst; the source stays a general
|
||||||
if fits8(int64(imm)) {
|
// r/m operand (setRM takes registers and memory alike).
|
||||||
i := newInstr(size, []byte{0x6B})
|
return e.encodeImulImm(imm, ops[1], dstReg, size)
|
||||||
if err := setRM(i, dstReg, ops[1], size); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i.imm = []byte{byte(int8(imm))}
|
|
||||||
return e.emit(i)
|
|
||||||
}
|
|
||||||
i := newInstr(size, []byte{0x69})
|
|
||||||
if err := setRM(i, dstReg, ops[1], size); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
immBytes, err := immediate(int64(imm), size, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i.imm = immBytes
|
|
||||||
return e.emit(i)
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("IMUL expects 2 or 3 operands, got %d", len(ops))
|
return fmt.Errorf("IMUL expects 2 or 3 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeImulImm emits the immediate multiply: 0x6B with a sign-extended imm8
|
||||||
|
// when the value fits, 0x69 with a 32-bit immediate otherwise.
|
||||||
|
func (e *enc) encodeImulImm(imm Imm, rm Operand, dst Reg, size int) error {
|
||||||
|
if fits8(int64(imm)) {
|
||||||
|
i := newInstr(size, []byte{0x6B})
|
||||||
|
if err := setRM(i, dst, rm, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.imm = []byte{byte(int8(imm))}
|
||||||
|
return e.emit(i)
|
||||||
|
}
|
||||||
|
i := newInstr(size, []byte{0x69})
|
||||||
|
if err := setRM(i, dst, rm, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
immBytes, err := immediate(int64(imm), size, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.imm = immBytes
|
||||||
|
return e.emit(i)
|
||||||
|
}
|
||||||
|
|
||||||
// --- PUSH / POP -------------------------------------------------------------
|
// --- PUSH / POP -------------------------------------------------------------
|
||||||
|
|
||||||
func (e *enc) encodePushPop(ops []Operand, size int, push bool) error {
|
func (e *enc) encodePushPop(ops []Operand, size int, push bool) error {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel;
|
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel;
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||||
}
|
}
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,7 +86,7 @@ func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
|||||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||||
}
|
}
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The differential kernels for the DATA-path and front-end gaps are kept in
|
// The differential kernels for the DATA-path and front-end gaps are kept in
|
||||||
@@ -130,6 +130,7 @@ func TestDifferentialKernels(t *testing.T) {
|
|||||||
{filepath.Join("..", "testdata", "verify", "quadreg_amd64.s"), "", false},
|
{filepath.Join("..", "testdata", "verify", "quadreg_amd64.s"), "", false},
|
||||||
{filepath.Join("..", "testdata", "verify", "floatimm_amd64.s"), "", false},
|
{filepath.Join("..", "testdata", "verify", "floatimm_amd64.s"), "", false},
|
||||||
{filepath.Join("..", "testdata", "verify", "bookkeep_amd64.s"), "", false},
|
{filepath.Join("..", "testdata", "verify", "bookkeep_amd64.s"), "", false},
|
||||||
|
{filepath.Join("..", "testdata", "verify", "forms_amd64.s"), "", false},
|
||||||
{filepath.Join("..", "testdata", "verify", "datarel_arm64.s"), "arm64", true},
|
{filepath.Join("..", "testdata", "verify", "datarel_arm64.s"), "arm64", true},
|
||||||
{filepath.Join("..", "testdata", "verify", "divslash_arm64.s"), "arm64", true},
|
{filepath.Join("..", "testdata", "verify", "divslash_arm64.s"), "arm64", true},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||||||
|
|||||||
+24
-2
@@ -9,7 +9,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Image is an assembled file: the function bodies laid out in source order,
|
// Image is an assembled file: the function bodies laid out in source order,
|
||||||
@@ -150,6 +150,18 @@ func (img *Image) Bytes() []byte {
|
|||||||
return append(out, img.Data...)
|
return append(out, img.Data...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssembleOption adjusts the file-level assembly context.
|
||||||
|
type AssembleOption func(*linkInfo)
|
||||||
|
|
||||||
|
// WithGOOS selects the target operating system for the forms that depend on
|
||||||
|
// it, the TLS access shape above all: linux and freebsd take the
|
||||||
|
// one-instruction form, windows and plan9 keep the two-instruction load.
|
||||||
|
func WithGOOS(goos string) AssembleOption {
|
||||||
|
return func(l *linkInfo) {
|
||||||
|
l.goos = goos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AssembleFile assembles every TEXT function of a parsed file and lays out
|
// AssembleFile assembles every TEXT function of a parsed file and lays out
|
||||||
// its static symbols (GLOBL/DATA) in a data section behind the code. Each
|
// its static symbols (GLOBL/DATA) in a data section behind the code. Each
|
||||||
// reference to a file-local static symbol becomes a RIP-relative load whose
|
// reference to a file-local static symbol becomes a RIP-relative load whose
|
||||||
@@ -157,7 +169,7 @@ func (img *Image) Bytes() []byte {
|
|||||||
// GLOBL defines is recorded as an external relocation (Externals) with its
|
// GLOBL defines is recorded as an external relocation (Externals) with its
|
||||||
// displacement left zero, the object-file emitters resolve it at link
|
// displacement left zero, the object-file emitters resolve it at link
|
||||||
// time, while the raw image (Bytes) cannot represent it.
|
// time, while the raw image (Bytes) cannot represent it.
|
||||||
func AssembleFile(f *ast.File) (*Image, error) {
|
func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) {
|
||||||
dataSyms, err := collectData(f)
|
dataSyms, err := collectData(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -166,7 +178,17 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
for _, d := range dataSyms {
|
for _, d := range dataSyms {
|
||||||
known[d.name] = true
|
known[d.name] = true
|
||||||
}
|
}
|
||||||
|
// TEXT symbols are file-level definitions too: a symbol immediate
|
||||||
|
// ($fn(SB)) may name one, exactly as a data reference names a GLOBL.
|
||||||
|
for _, d := range f.Decls {
|
||||||
|
if t, ok := d.(*ast.Text); ok {
|
||||||
|
known[t.Name.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
link := &linkInfo{symbols: known, allowExternal: true}
|
link := &linkInfo{symbols: known, allowExternal: true}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(link)
|
||||||
|
}
|
||||||
poolSeen := map[string]bool{}
|
poolSeen := map[string]bool{}
|
||||||
|
|
||||||
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAssembleFileStaticData checks the whole-image layout; code, padding
|
// TestAssembleFileStaticData checks the whole-image layout; code, padding
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
|
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
|
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLOONG64RelocOffsetsIncludePrologue pins the function-relative
|
// TestLOONG64RelocOffsetsIncludePrologue pins the function-relative
|
||||||
|
|||||||
@@ -36,6 +36,29 @@ type FloatImm struct {
|
|||||||
|
|
||||||
func (FloatImm) isOperand() {}
|
func (FloatImm) isOperand() {}
|
||||||
|
|
||||||
|
// TLSMem is a thread-local access, the source form off(base)(TLS*1) with the
|
||||||
|
// base dropped: the toolchain's one-instruction TLS rewrite assembles it as
|
||||||
|
// the segment-prefixed absolute whose disp32 carries an R_TLS_LE patch site
|
||||||
|
// (the linker fills the TLS slot offset).
|
||||||
|
type TLSMem struct {
|
||||||
|
Disp int64
|
||||||
|
Size int
|
||||||
|
Seg byte // the segment override: FS (0x64) or GS (0x65) on windows
|
||||||
|
}
|
||||||
|
|
||||||
|
func (TLSMem) isOperand() {}
|
||||||
|
|
||||||
|
// SegAbs is a segment-absolute access, 0x30(GS): the segment override
|
||||||
|
// prefixes a disp32 absolute reference with no relocation. The base
|
||||||
|
// register spellings GS and FS produce it.
|
||||||
|
type SegAbs struct {
|
||||||
|
Disp int64
|
||||||
|
Size int
|
||||||
|
Seg byte // 0x64 FS, 0x65 GS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SegAbs) isOperand() {}
|
||||||
|
|
||||||
// Mem is a memory operand of the form disp(base)(index*scale).
|
// Mem is a memory operand of the form disp(base)(index*scale).
|
||||||
type Mem struct {
|
type Mem struct {
|
||||||
Base Reg
|
Base Reg
|
||||||
@@ -45,6 +68,7 @@ type Mem struct {
|
|||||||
Size int // operand width in bytes
|
Size int // operand width in bytes
|
||||||
HasBase bool
|
HasBase bool
|
||||||
HasIndex bool
|
HasIndex bool
|
||||||
|
Seg byte // segment override prefix (0x64 FS, 0x65 GS); 0 = none
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Mem) isOperand() {}
|
func (Mem) isOperand() {}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// firstTextRISCV parses assembly source and returns the first TEXT function body.
|
// firstTextRISCV parses assembly source and returns the first TEXT function body.
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RISC-V frame mapping, matching the Go toolchain's riscv64 backend.
|
// RISC-V frame mapping, matching the Go toolchain's riscv64 backend.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRISCVFrameSpadjAndLines checks that a framed function records its
|
// TestRISCVFrameSpadjAndLines checks that a framed function records its
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL
|
// TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
// to the arch package; the AST records syntax only.
|
// to the arch package; the AST records syntax only.
|
||||||
package ast
|
package ast
|
||||||
|
|
||||||
import "sourcedock.dev/petrbalvin/gasm-devkit/token"
|
import "sourcedock.dev/petrbalvin/gasm-sdk/token"
|
||||||
|
|
||||||
// File is the parsed representation of one .s source file.
|
// File is the parsed representation of one .s source file.
|
||||||
type File struct {
|
type File struct {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ package ast
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
"sourcedock.dev/petrbalvin/gasm-sdk/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pos(line, col int) token.Position { return token.Position{Line: line, Column: col} }
|
func pos(line, col int) token.Position { return token.Position{Line: line, Column: col} }
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// go_asm.h is the header the Go compiler writes for every package that
|
// go_asm.h is the header the Go compiler writes for every package that
|
||||||
|
|||||||
+57
-3
@@ -5,6 +5,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -392,14 +393,67 @@ func TestRunCorpusAuditGOOS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRunCorpusAuditBuildConstraint covers the //go:build classification end
|
||||||
|
// to end: a generic-named file whose constraint admits one target is
|
||||||
|
// attempted there alone (cpu_x86.s on amd64), and a file whose constraint
|
||||||
|
// admits none of the four targets is never attempted (the msan and
|
||||||
|
// goexperiment trees).
|
||||||
|
func TestRunCorpusAuditBuildConstraint(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
write := func(name, src string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("x86.s", "//go:build 386 || amd64\n\nTEXT \xc2\xb7f(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
write("racey.s", "//go:build race\n\nTEXT \xc2\xb7r(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
write("plain.s", "TEXT \xc2\xb7p(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
|
||||||
|
stats, err := runCorpusAudit(dir, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runCorpusAudit: %v", err)
|
||||||
|
}
|
||||||
|
tally := func(name string) *corpusTally {
|
||||||
|
for i, tg := range stats.targets {
|
||||||
|
if tg.name == name {
|
||||||
|
return stats.tallies[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no tally for %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if stats.narrowed != 1 || stats.excluded != 1 || stats.generic != 1 {
|
||||||
|
t.Errorf("buckets = narrowed %d, excluded %d, generic %d; want 1, 1, 1", stats.narrowed, stats.excluded, stats.generic)
|
||||||
|
}
|
||||||
|
if a := tally("amd64"); a.attempted != 2 || a.assembled != 2 {
|
||||||
|
t.Errorf("amd64 = %d/%d, want 2/2 (x86.s and plain.s)", a.assembled, a.attempted)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"arm64", "riscv64", "loong64"} {
|
||||||
|
if a := tally(name); a.attempted != 1 || a.assembled != 1 {
|
||||||
|
t.Errorf("%s = %d/%d, want 1/1 (plain.s only)", name, a.assembled, a.attempted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stats.full != 2 {
|
||||||
|
t.Errorf("full = %d, want 2 (x86.s over its one target, plain.s over all four)", stats.full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestGenerateGoAsmHeaderRuntime pins the generator against the real thing:
|
// TestGenerateGoAsmHeaderRuntime pins the generator against the real thing:
|
||||||
// the runtime package, whose header the toolchain's own -asmhdr output was
|
// the runtime package of the ambient toolchain, whose header the toolchain's
|
||||||
// sampled from. Skipped in short mode: it type-checks the whole package.
|
// own -asmhdr output was sampled from. Skipped in short mode: it type-checks
|
||||||
|
// the whole package. The GOROOT comes from the go command itself, so the
|
||||||
|
// test follows whatever toolchain the host provides.
|
||||||
func TestGenerateGoAsmHeaderRuntime(t *testing.T) {
|
func TestGenerateGoAsmHeaderRuntime(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("type-checks the whole runtime package")
|
t.Skip("type-checks the whole runtime package")
|
||||||
}
|
}
|
||||||
dir, err := generateGoAsmHeader("/usr/local/go/src/runtime", "", "amd64", t.TempDir())
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("no Go toolchain: %v", err)
|
||||||
|
}
|
||||||
|
runtimeDir := filepath.Join(strings.TrimSpace(string(out)), "src", "runtime")
|
||||||
|
dir, err := generateGoAsmHeader(runtimeDir, "", "amd64", t.TempDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generateGoAsmHeader(runtime): %v", err)
|
t.Fatalf("generateGoAsmHeader(runtime): %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+168
-32
@@ -5,6 +5,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/build/constraint"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -15,9 +16,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
||||||
@@ -38,7 +39,7 @@ import (
|
|||||||
// construction and are excluded from the diff; the other architectures list
|
// construction and are excluded from the diff; the other architectures list
|
||||||
// their conditional branches outright.
|
// their conditional branches outright.
|
||||||
func cmdAuditInstructions(args []string) error {
|
func cmdAuditInstructions(args []string) error {
|
||||||
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [-I dir] [amd64|arm64|riscv64|loong64]", `
|
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [--list] [-I 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) and known-but-unencodable names (the backlog). The
|
gasm asm --format goobj) and known-but-unencodable names (the backlog). The
|
||||||
@@ -55,16 +56,19 @@ toolchain probing. A file whose name carries a recognisable _arch suffix is
|
|||||||
attempted for that architecture; a file without one is attempted for all
|
attempted for that architecture; a file without one is attempted for all
|
||||||
four, exactly as a GOARCH build would compile it. The report gives the
|
four, exactly as a GOARCH build would compile it. The report gives the
|
||||||
per-architecture pass rates and the most common failure reasons, which drive
|
per-architecture pass rates and the most common failure reasons, which drive
|
||||||
the encodability backlog by frequency rather than by table order.
|
the encodability backlog by frequency rather than by table order. With
|
||||||
|
-list the report also prints every failing file with its reason, per
|
||||||
|
architecture.
|
||||||
`)
|
`)
|
||||||
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
||||||
|
list := fs.Bool("list", false, "with --corpus, list every failing file with its reason, per architecture")
|
||||||
var dirs includeDirs
|
var dirs includeDirs
|
||||||
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
|
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if *corpus {
|
if *corpus {
|
||||||
return cmdAuditCorpus(fs.Args(), dirs)
|
return cmdAuditCorpus(fs.Args(), dirs, *list)
|
||||||
}
|
}
|
||||||
archName := "amd64"
|
archName := "amd64"
|
||||||
switch n := len(fs.Args()); {
|
switch n := len(fs.Args()); {
|
||||||
@@ -403,20 +407,30 @@ type corpusTally struct {
|
|||||||
assembled int
|
assembled int
|
||||||
reasons map[string]int // failure reason → count
|
reasons map[string]int // failure reason → count
|
||||||
example map[string]string // failure reason → one representative file
|
example map[string]string // failure reason → one representative file
|
||||||
|
fails []corpusFailure // every failure, in file order, for --list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *corpusTally) fail(path, reason string) {
|
// corpusFailure is one failed attempt, recorded for the --list report.
|
||||||
|
type corpusFailure struct {
|
||||||
|
path string
|
||||||
|
reason string
|
||||||
|
detail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *corpusTally) fail(path string, err error) {
|
||||||
|
reason := corpusReason(err)
|
||||||
t.reasons[reason]++
|
t.reasons[reason]++
|
||||||
if t.example[reason] == "" {
|
if t.example[reason] == "" {
|
||||||
t.example[reason] = path
|
t.example[reason] = path
|
||||||
}
|
}
|
||||||
|
t.fails = append(t.fails, corpusFailure{path: path, reason: reason, detail: firstLine(err.Error())})
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdAuditCorpus implements audit-instructions --corpus. The include
|
// cmdAuditCorpus implements audit-instructions --corpus. The include
|
||||||
// directories carry #include resolution over a corpus whose files refer to
|
// directories carry #include resolution over a corpus whose files refer to
|
||||||
// headers such as GOROOT/pkg/include, the same -I a toolchain comparison
|
// headers such as GOROOT/pkg/include, the same -I a toolchain comparison
|
||||||
// needs.
|
// needs.
|
||||||
func cmdAuditCorpus(args []string, dirs includeDirs) error {
|
func cmdAuditCorpus(args []string, dirs includeDirs, list bool) error {
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
return &usageError{fmt.Errorf("audit-instructions --corpus takes at most one directory argument")}
|
return &usageError{fmt.Errorf("audit-instructions --corpus takes at most one directory argument")}
|
||||||
}
|
}
|
||||||
@@ -454,7 +468,7 @@ func cmdAuditCorpus(args []string, dirs includeDirs) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printCorpusStats(stats)
|
printCorpusStats(stats, list)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,8 +477,10 @@ type corpusStats struct {
|
|||||||
root string
|
root string
|
||||||
files int
|
files int
|
||||||
generic int // files attempted for all four architectures
|
generic int // files attempted for all four architectures
|
||||||
|
narrowed int // files whose //go:build admits a proper subset of the four
|
||||||
|
excluded int // files whose //go:build admits none of the four: never compiled
|
||||||
otherPort int // files named for another Go port: never attempted
|
otherPort int // files named for another Go port: never attempted
|
||||||
full int // files that assembled for every target architecture
|
full int // files that assembled for every applicable target architecture
|
||||||
targets []corpusTarget
|
targets []corpusTarget
|
||||||
tallies []*corpusTally
|
tallies []*corpusTally
|
||||||
}
|
}
|
||||||
@@ -560,6 +576,57 @@ func otherGOOSFile(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildConstraint returns the file's leading //go:build expression, or nil
|
||||||
|
// when the file carries none. The constraint governs the same header block
|
||||||
|
// go/build reads: blank lines and comments may precede it, and the first
|
||||||
|
// line that is neither ends the block. A constraint that does not parse
|
||||||
|
// narrows nothing, so the file stays in the attempted set: the audit must
|
||||||
|
// never exclude a file the toolchain would compile.
|
||||||
|
func buildConstraint(src string) constraint.Expr {
|
||||||
|
for line := range strings.SplitSeq(src, "\n") {
|
||||||
|
t := strings.TrimSpace(line)
|
||||||
|
switch {
|
||||||
|
case t == "":
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(t, "//"):
|
||||||
|
if constraint.IsGoBuild(t) {
|
||||||
|
e, err := constraint.Parse(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unixOS is go/build's unixOS set: the GOOSes the unix build tag admits.
|
||||||
|
var unixOS = map[string]bool{
|
||||||
|
"aix": true, "android": true, "darwin": true, "dragonfly": true,
|
||||||
|
"freebsd": true, "hurd": true, "illumos": true, "ios": true,
|
||||||
|
"linux": true, "netbsd": true, "openbsd": true, "solaris": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// constraintTags answers the build tags a plain `go build` sets for a
|
||||||
|
// target: the GOOS and GOARCH, gc, and unix on the unix-like GOOSes. No
|
||||||
|
// experiment, sanitiser or cgo tag is ever true: the audit models the
|
||||||
|
// default build, and no GOROOT assembly file's constraint hinges on cgo.
|
||||||
|
func constraintTags(goarch, goos string) func(string) bool {
|
||||||
|
return func(tag string) bool {
|
||||||
|
switch tag {
|
||||||
|
case goarch, goos, "gc":
|
||||||
|
return true
|
||||||
|
case "unix":
|
||||||
|
return unixOS[goos]
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
||||||
files, err := asmFiles(root)
|
files, err := asmFiles(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -577,8 +644,8 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
tallies[i] = &corpusTally{reasons: map[string]int{}, example: map[string]string{}}
|
tallies[i] = &corpusTally{reasons: map[string]int{}, example: map[string]string{}}
|
||||||
}
|
}
|
||||||
// full is the north-star number: a file counts when every architecture
|
// full is the north-star number: a file counts when every architecture
|
||||||
// its name allows assembles it.
|
// its build admits assembles it.
|
||||||
full, generic, otherPort := 0, 0, 0
|
full, generic, otherPort, narrowedCount, excluded := 0, 0, 0, 0, 0
|
||||||
|
|
||||||
// Header generation is created on first use, so a corpus with no
|
// Header generation is created on first use, so a corpus with no
|
||||||
// go_asm.h includes never pays for a temp directory.
|
// go_asm.h includes never pays for a temp directory.
|
||||||
@@ -601,28 +668,78 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
// invisible to a file-name rule).
|
// invisible to a file-name rule).
|
||||||
goos := goosFromFilename(path)
|
goos := goosFromFilename(path)
|
||||||
|
|
||||||
|
// The GOOS the header generation type-checks under follows the
|
||||||
|
// file's name when the name carries one; the ambient GOOS is the
|
||||||
|
// honest guess otherwise.
|
||||||
|
namedArch := arch.FromFilename(path)
|
||||||
var wanted []int // indexes into targets
|
var wanted []int // indexes into targets
|
||||||
if a := arch.FromFilename(path); a != arch.Unknown {
|
other := false
|
||||||
|
switch {
|
||||||
|
case namedArch != arch.Unknown:
|
||||||
for i, tg := range targets {
|
for i, tg := range targets {
|
||||||
if tg.a == a {
|
if tg.a == namedArch {
|
||||||
wanted = append(wanted, i)
|
wanted = append(wanted, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if otherPortFile(path) {
|
case otherPortFile(path):
|
||||||
// A file named for a Go port gasm does not support (arm,
|
// A file named for a Go port gasm does not support (arm,
|
||||||
// 386, s390x, ...) or for another GOOS is compiled by no
|
// 386, s390x, ...) or for another GOOS is compiled by no
|
||||||
// supported-arch build, so it is neither generic nor a
|
// supported-arch build, so it is neither generic nor a
|
||||||
// per-arch attempt: counting it as generic would make the
|
// per-arch attempt: counting it as generic would make the
|
||||||
// headline unreachably low for reasons no supported target
|
// headline unreachably low for reasons no supported target
|
||||||
// can fix.
|
// can fix.
|
||||||
|
other = true
|
||||||
otherPort++
|
otherPort++
|
||||||
} else {
|
default:
|
||||||
generic++
|
|
||||||
for i := range targets {
|
for i := range targets {
|
||||||
wanted = append(wanted, i)
|
wanted = append(wanted, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A //go:build constraint narrows the set of targets the file is
|
||||||
|
// assembled for, the way the go command compiles the file only for
|
||||||
|
// the targets the expression admits: cpu_x86.s belongs to the x86
|
||||||
|
// build alone, and a file whose constraint admits none of the four
|
||||||
|
// targets (the goexperiment.runtimesecret and msan trees) is
|
||||||
|
// compiled by no supported build. The tags mirror what a plain
|
||||||
|
// `go build` sets: the GOOS and GOARCH, gc, and unix on the
|
||||||
|
// unix-like GOOSes; no experiment, sanitiser or cgo tag is ever
|
||||||
|
// true. The GOOS is the file's own when the name carries one,
|
||||||
|
// else the ambient one.
|
||||||
|
goosForEval := goos
|
||||||
|
if goosForEval == "" {
|
||||||
|
goosForEval = runtime.GOOS
|
||||||
|
}
|
||||||
|
narrowed := false
|
||||||
|
if len(wanted) > 0 {
|
||||||
|
if ce := buildConstraint(src); ce != nil {
|
||||||
|
kept := make([]int, 0, len(wanted))
|
||||||
|
for _, i := range wanted {
|
||||||
|
tg := targets[i]
|
||||||
|
if ce.Eval(constraintTags(goarchName(tg.a), goosForEval)) {
|
||||||
|
kept = append(kept, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(kept) < len(wanted) {
|
||||||
|
narrowed = true
|
||||||
|
}
|
||||||
|
wanted = kept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case other:
|
||||||
|
// already tallied above
|
||||||
|
case len(wanted) == 0:
|
||||||
|
excluded++
|
||||||
|
case namedArch != arch.Unknown:
|
||||||
|
// a per-arch attempt over the constraint's subset
|
||||||
|
case narrowed:
|
||||||
|
narrowedCount++
|
||||||
|
default:
|
||||||
|
generic++
|
||||||
|
}
|
||||||
|
|
||||||
// A file that includes go_asm.h parses against a per-target header:
|
// A file that includes go_asm.h parses against a per-target header:
|
||||||
// the defines differ per architecture (internal/cpu's layout, for
|
// the defines differ per architecture (internal/cpu's layout, for
|
||||||
// one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
|
// one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
|
||||||
@@ -645,21 +762,22 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
|
hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ok = false
|
ok = false
|
||||||
t.fail(path, corpusReason(err))
|
t.fail(path, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f, errs := parser.ParseWithOptions(path, src, parser.Options{
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{
|
||||||
Expand: true,
|
Expand: true,
|
||||||
IncludeDirs: append(slices.Clone(dirs), hdrDir),
|
IncludeDirs: append(slices.Clone(dirs), hdrDir),
|
||||||
|
Predefines: platformPredefinesFor(goarchName(tg.a), goos),
|
||||||
})
|
})
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
ok = false
|
ok = false
|
||||||
t.fail(path, corpusReason(errs[0]))
|
t.fail(path, errs[0])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := assembleFile(tg.a, f); err != nil {
|
if _, err := assembleFile(tg.a, f, goos); err != nil {
|
||||||
ok = false
|
ok = false
|
||||||
t.fail(path, corpusReason(err))
|
t.fail(path, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t.assembled++
|
t.assembled++
|
||||||
@@ -670,21 +788,28 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs})
|
|
||||||
|
|
||||||
ok := true
|
ok := true
|
||||||
for _, i := range wanted {
|
for _, i := range wanted {
|
||||||
tg, t := targets[i], tallies[i]
|
tg, t := targets[i], tallies[i]
|
||||||
t.attempted++
|
t.attempted++
|
||||||
|
// The parse carries the target's platform predefines, so it
|
||||||
|
// cannot be shared across targets the way a header-free file's
|
||||||
|
// could: a #ifdef GOARCH_arm block must be live on arm64 and
|
||||||
|
// dead everywhere else.
|
||||||
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{
|
||||||
|
Expand: true,
|
||||||
|
IncludeDirs: dirs,
|
||||||
|
Predefines: platformPredefinesFor(goarchName(tg.a), goos),
|
||||||
|
})
|
||||||
var err error
|
var err error
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
err = errs[0] // a parse failure is a failure for every target
|
err = errs[0] // a parse failure is a failure for every target
|
||||||
} else {
|
} else {
|
||||||
_, err = assembleFile(tg.a, f)
|
_, err = assembleFile(tg.a, f, goos)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ok = false
|
ok = false
|
||||||
t.fail(path, corpusReason(err))
|
t.fail(path, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t.assembled++
|
t.assembled++
|
||||||
@@ -698,6 +823,8 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
root: root,
|
root: root,
|
||||||
files: len(files),
|
files: len(files),
|
||||||
generic: generic,
|
generic: generic,
|
||||||
|
narrowed: narrowedCount,
|
||||||
|
excluded: excluded,
|
||||||
otherPort: otherPort,
|
otherPort: otherPort,
|
||||||
full: full,
|
full: full,
|
||||||
targets: targets,
|
targets: targets,
|
||||||
@@ -706,14 +833,16 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// printCorpusStats renders the corpus audit report.
|
// printCorpusStats renders the corpus audit report.
|
||||||
func printCorpusStats(s *corpusStats) {
|
func printCorpusStats(s *corpusStats, list bool) {
|
||||||
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures; %d named for other Go ports, never attempted)\n", s.root, s.files, s.generic, s.otherPort)
|
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures; %d narrowed by //go:build; %d excluded by //go:build; %d named for other Go ports, never attempted)\n",
|
||||||
|
s.root, s.files, s.generic, s.narrowed, s.excluded, s.otherPort)
|
||||||
// The rate is over the files a supported build would attempt: the
|
// The rate is over the files a supported build would attempt: the
|
||||||
// other ports' files sit in the count for completeness but can never
|
// other ports' files and the ones no supported target compiles sit in
|
||||||
// assemble, so counting them in the denominator would report the gap
|
// the count for completeness but can never assemble, so counting them
|
||||||
// of architectures gasm deliberately does not target.
|
// in the denominator would report the gap of platforms gasm
|
||||||
attemptable := max(s.files-s.otherPort, 1)
|
// deliberately does not target.
|
||||||
fmt.Printf(" assemble for every target architecture: %d of %d attemptable (%.1f%%)\n", s.full, attemptable, 100*float64(s.full)/float64(attemptable))
|
attemptable := max(s.files-s.otherPort-s.excluded, 1)
|
||||||
|
fmt.Printf(" assemble for every applicable target: %d of %d attemptable (%.1f%%)\n", s.full, attemptable, 100*float64(s.full)/float64(attemptable))
|
||||||
for i, tg := range s.targets {
|
for i, tg := range s.targets {
|
||||||
t := s.tallies[i]
|
t := s.tallies[i]
|
||||||
fmt.Printf(" %s: %d/%d attempted\n", tg.name, t.assembled, t.attempted)
|
fmt.Printf(" %s: %d/%d attempted\n", tg.name, t.assembled, t.attempted)
|
||||||
@@ -721,6 +850,13 @@ func printCorpusStats(s *corpusStats) {
|
|||||||
fmt.Printf(" %4d %s\n", t.reasons[r], r)
|
fmt.Printf(" %4d %s\n", t.reasons[r], r)
|
||||||
fmt.Printf(" e.g. %s\n", t.example[r])
|
fmt.Printf(" e.g. %s\n", t.example[r])
|
||||||
}
|
}
|
||||||
|
if !list {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, f := range t.fails {
|
||||||
|
fmt.Printf(" FAIL %s\n", f.path)
|
||||||
|
fmt.Printf(" %s: %s\n", f.reason, f.detail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+61
-1
@@ -4,9 +4,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDerivedFamily(t *testing.T) {
|
func TestDerivedFamily(t *testing.T) {
|
||||||
@@ -64,3 +65,62 @@ func TestGasmEncodable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildConstraint pins the //go:build reader: the constraint governs the
|
||||||
|
// leading comment block, the first non-comment line ends it (a tag below a
|
||||||
|
// #include governs nothing, exactly as go/build drops it), and a file
|
||||||
|
// without one admits every target.
|
||||||
|
func TestBuildConstraint(t *testing.T) {
|
||||||
|
admits := func(src, goarch, goos string) bool {
|
||||||
|
t.Helper()
|
||||||
|
e := buildConstraint(src)
|
||||||
|
if e == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return e.Eval(constraintTags(goarch, goos))
|
||||||
|
}
|
||||||
|
const ret = "TEXT \xc2\xb7f(SB), NOSPLIT, $0\n\tRET\n"
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
amd64, arm64 bool
|
||||||
|
}{
|
||||||
|
{"no constraint", ret, true, true},
|
||||||
|
{"x86 only", "//go:build 386 || amd64\n\n" + ret, true, false},
|
||||||
|
{"arm64 and linux", "//go:build arm64 && linux\n\n" + ret, false, true},
|
||||||
|
{"msan never", "//go:build msan\n\n" + ret, false, false},
|
||||||
|
{"experiment never", "//go:build goexperiment.runtimesecret\n\n" + ret, false, false},
|
||||||
|
{"below an include governs nothing", "#include \"textflag.h\"\n//go:build amd64\n" + ret, true, true},
|
||||||
|
{"unparsable narrows nothing", "//go:build (amd64\n" + ret, true, true},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
if got := admits(c.src, "amd64", runtime.GOOS); got != c.amd64 {
|
||||||
|
t.Errorf("amd64 admission = %v, want %v", got, c.amd64)
|
||||||
|
}
|
||||||
|
if got := admits(c.src, "arm64", runtime.GOOS); got != c.arm64 {
|
||||||
|
t.Errorf("arm64 admission = %v, want %v", got, c.arm64)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConstraintTags pins the tag set a plain `go build` sets: the GOOS and
|
||||||
|
// GOARCH, gc, unix on the unix-like GOOSes; nothing else is ever true.
|
||||||
|
func TestConstraintTags(t *testing.T) {
|
||||||
|
ok := constraintTags("amd64", "linux")
|
||||||
|
for _, tag := range []string{"amd64", "linux", "gc", "unix"} {
|
||||||
|
if !ok(tag) {
|
||||||
|
t.Errorf("tag %q = false, want true", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tag := range []string{"arm64", "freebsd", "darwin", "cgo", "race", "msan", "goexperiment.runtimesecret"} {
|
||||||
|
if ok(tag) {
|
||||||
|
t.Errorf("tag %q = true, want false", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fb := constraintTags("arm64", "freebsd")
|
||||||
|
if !fb("unix") {
|
||||||
|
t.Error("unix on freebsd = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build !linux
|
//go:build !(linux || (freebsd && (amd64 || arm64 || riscv64)))
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func cmdDebug(args []string) int {
|
func cmdDebug(args []string) int {
|
||||||
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux (ptrace)")
|
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux or FreeBSD (ptrace)")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
"sourcedock.dev/petrbalvin/gasm-sdk/debug"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmdDebug(args []string) int {
|
func cmdDebug(args []string) int {
|
||||||
+4
-4
@@ -9,9 +9,9 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdDis disassembles machine code: either a raw binary (standard input with
|
// cmdDis disassembles machine code: either a raw binary (standard input with
|
||||||
@@ -84,7 +84,7 @@ func disSource(path string, target arch.Arch) int {
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
img, err := assembleFile(target, f)
|
img, err := assembleFile(target, f, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+43
-20
@@ -27,15 +27,15 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/format"
|
"sourcedock.dev/petrbalvin/gasm-sdk/format"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
"sourcedock.dev/petrbalvin/gasm-sdk/lexer"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
|
"sourcedock.dev/petrbalvin/gasm-sdk/lint"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
|
"sourcedock.dev/petrbalvin/gasm-sdk/lsp"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version reports the release the toolchain recorded for this build: the
|
// version reports the release the toolchain recorded for this build: the
|
||||||
@@ -574,7 +574,7 @@ naming the package.
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
dirs = append(dirs, hdrDir)
|
dirs = append(dirs, hdrDir)
|
||||||
}
|
}
|
||||||
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs})
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs, Predefines: platformPredefinesFor(string(targetArch), goos)})
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
}
|
}
|
||||||
@@ -582,7 +582,7 @@ naming the package.
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := assembleFile(targetArch, f)
|
img, err := assembleFile(targetArch, f, goos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -789,11 +789,34 @@ e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// platformPredefines mirrors the go command's assembler invocation, which
|
||||||
|
// defines GOOS_<goos> and GOARCH_<arch> as -D macros: GOROOT headers
|
||||||
|
// (go_tls.h, asm_riscv64.h) select their platform blocks with #ifdef on
|
||||||
|
// exactly those names, so an assembler without them cannot see the platform
|
||||||
|
// definitions at all.
|
||||||
|
func platformPredefines(goarch, goos string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"GOARCH_" + goarch: "1",
|
||||||
|
"GOOS_" + goos: "1",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// platformPredefinesFor resolves the ambient GOOS the way a build would: a
|
||||||
|
// file whose name carries one (sys_darwin_arm64.s) is compiled for that GOOS
|
||||||
|
// and nothing else.
|
||||||
|
func platformPredefinesFor(goarch string, fileGoos string) map[string]string {
|
||||||
|
goos := fileGoos
|
||||||
|
if goos == "" {
|
||||||
|
goos = runtime.GOOS
|
||||||
|
}
|
||||||
|
return platformPredefines(goarch, goos)
|
||||||
|
}
|
||||||
|
|
||||||
// assembleFile assembles a parsed file for the given architecture and returns the image.
|
// assembleFile assembles a parsed file for the given architecture and returns the image.
|
||||||
func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) {
|
func assembleFile(targetArch arch.Arch, f *ast.File, goos string) (*asm.Image, error) {
|
||||||
switch targetArch {
|
switch targetArch {
|
||||||
case arch.AMD64:
|
case arch.AMD64:
|
||||||
return asm.AssembleFile(f)
|
return asm.AssembleFile(f, asm.WithGOOS(goos))
|
||||||
case arch.RISCV:
|
case arch.RISCV:
|
||||||
return asm.AssembleFileRISCV(f)
|
return asm.AssembleFileRISCV(f)
|
||||||
case arch.ARM64:
|
case arch.ARM64:
|
||||||
@@ -813,18 +836,18 @@ func assemblePath(path string, forced arch.Arch, dirs includeDirs) (*asm.Image,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs})
|
target := forced
|
||||||
|
if target == arch.Unknown {
|
||||||
|
target = arch.FromFilename(path)
|
||||||
|
}
|
||||||
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{Expand: true, IncludeDirs: dirs, Predefines: platformPredefinesFor(string(target), "")})
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
}
|
}
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return nil, fmt.Errorf("parse errors")
|
return nil, fmt.Errorf("parse errors")
|
||||||
}
|
}
|
||||||
target := forced
|
return assembleFile(target, f, "")
|
||||||
if target == arch.Unknown {
|
|
||||||
target = arch.FromFilename(path)
|
|
||||||
}
|
|
||||||
return assembleFile(target, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// printByteDiff shows the first few byte differences between two code blocks.
|
// printByteDiff shows the first few byte differences between two code blocks.
|
||||||
@@ -920,7 +943,7 @@ func cmdVerifyNonJIT(path string, targetArch arch.Arch, groundTruth, profile boo
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
img, err := assembleFile(targetArch, f)
|
img, err := assembleFile(targetArch, f, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const clean = "#include \"textflag.h\"\n" +
|
const clean = "#include \"textflag.h\"\n" +
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
gasmast "sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
gasmast "sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
gasmparser "sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
gasmparser "sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdScaffold generates a differential test skeleton for every kernel in a
|
// cmdScaffold generates a differential test skeleton for every kernel in a
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
// memory and returns its text representation and length in bytes.
|
||||||
|
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||||
|
mem, err := s.ReadMemory(addr, 15)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
ins, err := disasm.Decode(arch.AMD64, mem, addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return ins.Text, ins.Len, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||||
|
// them as a formatted string with addresses and byte offsets.
|
||||||
|
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||||
|
var result strings.Builder
|
||||||
|
pc := addr
|
||||||
|
for range n {
|
||||||
|
text, length, err := s.Disassemble(pc)
|
||||||
|
if err != nil {
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||||
|
if length == 0 {
|
||||||
|
length = 1
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCallInsn reports whether disassembled text (x86asm.IntelSyntax) is a
|
||||||
|
// call. The first token must match exactly: a prefix test would also catch
|
||||||
|
// unrelated mnemonics.
|
||||||
|
func isCallInsn(text string) bool {
|
||||||
|
m, _, _ := strings.Cut(text, " ")
|
||||||
|
return strings.ToLower(m) == "call"
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && arm64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
// memory and returns its text representation and length in bytes.
|
||||||
|
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||||
|
mem, err := s.ReadMemory(addr, 4)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
ins, err := disasm.Decode(arch.ARM64, mem, addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return ins.Text, ins.Len, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||||
|
// them as a formatted string with addresses and byte offsets.
|
||||||
|
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||||
|
var result strings.Builder
|
||||||
|
pc := addr
|
||||||
|
for range n {
|
||||||
|
text, length, err := s.Disassemble(pc)
|
||||||
|
if err != nil {
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||||
|
if length == 0 {
|
||||||
|
length = 1
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCallInsn reports whether disassembled text (arm64asm.GoSyntax) is a
|
||||||
|
// call. GoSyntax renders bl as CALL; the native mnemonic is accepted too.
|
||||||
|
// The first token must match exactly so branches never match.
|
||||||
|
func isCallInsn(text string) bool {
|
||||||
|
m, _, _ := strings.Cut(text, " ")
|
||||||
|
switch strings.ToLower(m) {
|
||||||
|
case "call", "bl":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && riscv64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
// memory and returns its text representation and length in bytes.
|
||||||
|
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||||
|
mem, err := s.ReadMemory(addr, 4)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
ins, err := disasm.Decode(arch.RISCV, mem, addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return ins.Text, ins.Len, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||||
|
// them as a formatted string with addresses and byte offsets.
|
||||||
|
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||||
|
var result strings.Builder
|
||||||
|
pc := addr
|
||||||
|
for range n {
|
||||||
|
text, length, err := s.Disassemble(pc)
|
||||||
|
if err != nil {
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||||
|
if length == 0 {
|
||||||
|
length = 1
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCallInsn reports whether disassembled text (riscv64asm.GoSyntax) is a
|
||||||
|
// call. GoSyntax renders jal and jalr calls as CALL; the native mnemonics
|
||||||
|
// are accepted too. The first token must match exactly: a prefix test on
|
||||||
|
// "bl" would catch branches on other architectures, and jalr as ret prints
|
||||||
|
// RET, which must not be stepped over.
|
||||||
|
func isCallInsn(text string) bool {
|
||||||
|
m, _, _ := strings.Cut(text, " ")
|
||||||
|
switch strings.ToLower(m) {
|
||||||
|
case "call", "jal", "jalr":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||||
|
fmt.Printf(" RIP = %#016x (func+%#x)\n", regs.RIP, regs.RIP-codeBase-funcOff)
|
||||||
|
fmt.Printf(" RSP = %#016x RBP = %#016x\n", regs.RSP, regs.RBP)
|
||||||
|
fmt.Printf(" RAX = %#016x RBX = %#016x\n", regs.RAX, regs.RBX)
|
||||||
|
fmt.Printf(" RCX = %#016x RDX = %#016x\n", regs.RCX, regs.RDX)
|
||||||
|
fmt.Printf(" RSI = %#016x RDI = %#016x\n", regs.RSI, regs.RDI)
|
||||||
|
fmt.Printf(" R8 = %#016x R9 = %#016x\n", regs.R8, regs.R9)
|
||||||
|
fmt.Printf(" R10 = %#016x R11 = %#016x\n", regs.R10, regs.R11)
|
||||||
|
fmt.Printf(" R12 = %#016x R13 = %#016x\n", regs.R12, regs.R13)
|
||||||
|
fmt.Printf(" R14 = %#016x R15 = %#016x\n", regs.R14, regs.R15)
|
||||||
|
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVectorRegs(v *VectorRegs) {
|
||||||
|
fmt.Println("\n Vector registers (YMM):")
|
||||||
|
for i := 0; i < 16; i += 2 {
|
||||||
|
fmt.Printf(" YMM%-2d = ", i)
|
||||||
|
printYMM(v.YMM[i][:])
|
||||||
|
fmt.Printf(" YMM%-2d = ", i+1)
|
||||||
|
printYMM(v.YMM[i+1][:])
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printYMM(b []byte) {
|
||||||
|
for j := 0; j < 32; j += 4 {
|
||||||
|
v := uint32(b[j]) | uint32(b[j+1])<<8 | uint32(b[j+2])<<16 | uint32(b[j+3])<<24
|
||||||
|
fmt.Printf("%08x ", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRflags(f uint64) string {
|
||||||
|
var flags string
|
||||||
|
if f&1 != 0 {
|
||||||
|
flags += "CF "
|
||||||
|
}
|
||||||
|
if f&(1<<2) != 0 {
|
||||||
|
flags += "PF "
|
||||||
|
}
|
||||||
|
if f&(1<<4) != 0 {
|
||||||
|
flags += "AF "
|
||||||
|
}
|
||||||
|
if f&(1<<6) != 0 {
|
||||||
|
flags += "ZF "
|
||||||
|
}
|
||||||
|
if f&(1<<7) != 0 {
|
||||||
|
flags += "SF "
|
||||||
|
}
|
||||||
|
if f&(1<<8) != 0 {
|
||||||
|
flags += "TF "
|
||||||
|
}
|
||||||
|
if f&(1<<9) != 0 {
|
||||||
|
flags += "IF "
|
||||||
|
}
|
||||||
|
if f&(1<<10) != 0 {
|
||||||
|
flags += "DF "
|
||||||
|
}
|
||||||
|
if f&(1<<11) != 0 {
|
||||||
|
flags += "OF "
|
||||||
|
}
|
||||||
|
if flags == "" {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return flags[:len(flags)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReg modifies a register value in the debuggee.
|
||||||
|
func (s *Session) SetReg(name string, value uint64) error {
|
||||||
|
regs, err := s.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "rax", "eax", "ax", "al":
|
||||||
|
regs.RAX = value
|
||||||
|
case "rbx", "ebx", "bx", "bl":
|
||||||
|
regs.RBX = value
|
||||||
|
case "rcx", "ecx", "cx", "cl":
|
||||||
|
regs.RCX = value
|
||||||
|
case "rdx", "edx", "dx", "dl":
|
||||||
|
regs.RDX = value
|
||||||
|
case "rsi", "esi", "si":
|
||||||
|
regs.RSI = value
|
||||||
|
case "rdi", "edi", "di":
|
||||||
|
regs.RDI = value
|
||||||
|
case "rbp", "ebp", "bp":
|
||||||
|
regs.RBP = value
|
||||||
|
case "rsp", "esp", "sp":
|
||||||
|
regs.RSP = value
|
||||||
|
case "r8":
|
||||||
|
regs.R8 = value
|
||||||
|
case "r9":
|
||||||
|
regs.R9 = value
|
||||||
|
case "r10":
|
||||||
|
regs.R10 = value
|
||||||
|
case "r11":
|
||||||
|
regs.R11 = value
|
||||||
|
case "r12":
|
||||||
|
regs.R12 = value
|
||||||
|
case "r13":
|
||||||
|
regs.R13 = value
|
||||||
|
case "r14":
|
||||||
|
regs.R14 = value
|
||||||
|
case "r15":
|
||||||
|
regs.R15 = value
|
||||||
|
case "rip", "eip":
|
||||||
|
regs.RIP = value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: unknown register %q", name)
|
||||||
|
}
|
||||||
|
return s.SetRegs(®s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archReturnAddr reads the return address of the current frame (amd64
|
||||||
|
// ABI0 convention). A function that contains a CALL (or has a frame) is
|
||||||
|
// assembled with the prologue PUSHQ BP; MOVQ SP, BP, so mid-function the
|
||||||
|
// word at SP is the saved caller BP, a stack address, and the return
|
||||||
|
// address sits further up. Walk the stack from SP and take the first word
|
||||||
|
// that lies in an executable mapping: stack and data words never do, a
|
||||||
|
// return address always does. FreeBSD exposes no mapping list, so the
|
||||||
|
// walk degenerates to the raw entry convention, [SP] before any push.
|
||||||
|
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||||
|
return s.Peek(regs.RSP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archSPLabel returns the SP register name for display.
|
||||||
|
func archSPLabel() string { return "RSP" }
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && arm64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||||
|
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
||||||
|
fmt.Printf(" SP = %#016x FP = %#016x\n", regs.SP, regs.X29)
|
||||||
|
fmt.Printf(" LR = %#016x\n", regs.X30)
|
||||||
|
fmt.Printf(" X0 = %#016x X1 = %#016x\n", regs.X0, regs.X1)
|
||||||
|
fmt.Printf(" X2 = %#016x X3 = %#016x\n", regs.X2, regs.X3)
|
||||||
|
fmt.Printf(" X4 = %#016x X5 = %#016x\n", regs.X4, regs.X5)
|
||||||
|
fmt.Printf(" X6 = %#016x X7 = %#016x\n", regs.X6, regs.X7)
|
||||||
|
fmt.Printf(" X8 = %#016x X9 = %#016x\n", regs.X8, regs.X9)
|
||||||
|
fmt.Printf(" X10 = %#016x X11 = %#016x\n", regs.X10, regs.X11)
|
||||||
|
fmt.Printf(" X12 = %#016x X13 = %#016x\n", regs.X12, regs.X13)
|
||||||
|
fmt.Printf(" X14 = %#016x X15 = %#016x\n", regs.X14, regs.X15)
|
||||||
|
fmt.Printf(" X16 = %#016x X17 = %#016x\n", regs.X16, regs.X17)
|
||||||
|
fmt.Printf(" X18 = %#016x X19 = %#016x\n", regs.X18, regs.X19)
|
||||||
|
fmt.Printf(" X20 = %#016x X21 = %#016x\n", regs.X20, regs.X21)
|
||||||
|
fmt.Printf(" X22 = %#016x X23 = %#016x\n", regs.X22, regs.X23)
|
||||||
|
fmt.Printf(" X24 = %#016x X25 = %#016x\n", regs.X24, regs.X25)
|
||||||
|
fmt.Printf(" X26 = %#016x X27 = %#016x\n", regs.X26, regs.X27)
|
||||||
|
fmt.Printf(" X28 = %#016x PSTATE = %#x\n", regs.X28, regs.PSTATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVectorRegs(v *VectorRegs) {
|
||||||
|
fmt.Println("\n Vector registers (V0-V31):")
|
||||||
|
for i := 0; i < 32; i += 2 {
|
||||||
|
fmt.Printf(" V%-2d = %016x%016x\n", i, binary.LittleEndian.Uint64(v.V[i][8:16]), binary.LittleEndian.Uint64(v.V[i][0:8]))
|
||||||
|
fmt.Printf(" V%-2d = %016x%016x\n", i+1, binary.LittleEndian.Uint64(v.V[i+1][8:16]), binary.LittleEndian.Uint64(v.V[i+1][0:8]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReg modifies a register value in the debuggee.
|
||||||
|
func (s *Session) SetReg(name string, value uint64) error {
|
||||||
|
regs, err := s.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "x0":
|
||||||
|
regs.X0 = value
|
||||||
|
case "x1":
|
||||||
|
regs.X1 = value
|
||||||
|
case "x2":
|
||||||
|
regs.X2 = value
|
||||||
|
case "x3":
|
||||||
|
regs.X3 = value
|
||||||
|
case "x4":
|
||||||
|
regs.X4 = value
|
||||||
|
case "x5":
|
||||||
|
regs.X5 = value
|
||||||
|
case "x6":
|
||||||
|
regs.X6 = value
|
||||||
|
case "x7":
|
||||||
|
regs.X7 = value
|
||||||
|
case "x8":
|
||||||
|
regs.X8 = value
|
||||||
|
case "x9":
|
||||||
|
regs.X9 = value
|
||||||
|
case "x10":
|
||||||
|
regs.X10 = value
|
||||||
|
case "x11":
|
||||||
|
regs.X11 = value
|
||||||
|
case "x12":
|
||||||
|
regs.X12 = value
|
||||||
|
case "x13":
|
||||||
|
regs.X13 = value
|
||||||
|
case "x14":
|
||||||
|
regs.X14 = value
|
||||||
|
case "x15":
|
||||||
|
regs.X15 = value
|
||||||
|
case "x16":
|
||||||
|
regs.X16 = value
|
||||||
|
case "x17":
|
||||||
|
regs.X17 = value
|
||||||
|
case "x18":
|
||||||
|
regs.X18 = value
|
||||||
|
case "x19":
|
||||||
|
regs.X19 = value
|
||||||
|
case "x20":
|
||||||
|
regs.X20 = value
|
||||||
|
case "x21":
|
||||||
|
regs.X21 = value
|
||||||
|
case "x22":
|
||||||
|
regs.X22 = value
|
||||||
|
case "x23":
|
||||||
|
regs.X23 = value
|
||||||
|
case "x24":
|
||||||
|
regs.X24 = value
|
||||||
|
case "x25":
|
||||||
|
regs.X25 = value
|
||||||
|
case "x26":
|
||||||
|
regs.X26 = value
|
||||||
|
case "x27":
|
||||||
|
regs.X27 = value
|
||||||
|
case "x28":
|
||||||
|
regs.X28 = value
|
||||||
|
case "x29", "fp":
|
||||||
|
regs.X29 = value
|
||||||
|
case "x30", "lr":
|
||||||
|
regs.X30 = value
|
||||||
|
case "sp":
|
||||||
|
regs.SP = value
|
||||||
|
case "pc":
|
||||||
|
regs.PC = value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: unknown register %q", name)
|
||||||
|
}
|
||||||
|
return s.SetRegs(®s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archReturnAddr reads the return address from LR (arm64 convention).
|
||||||
|
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||||
|
return regs.X30, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archSPLabel returns the SP register name for display.
|
||||||
|
func archSPLabel() string { return "SP" }
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && riscv64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||||
|
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.PC, regs.PC-codeBase-funcOff)
|
||||||
|
fmt.Printf(" SP = %#016x FP = %#016x\n", regs.Sp, regs.S0)
|
||||||
|
fmt.Printf(" RA = %#016x\n", regs.Ra)
|
||||||
|
fmt.Printf(" A0 = %#016x A1 = %#016x\n", regs.A0, regs.A1)
|
||||||
|
fmt.Printf(" A2 = %#016x A3 = %#016x\n", regs.A2, regs.A3)
|
||||||
|
fmt.Printf(" A4 = %#016x A5 = %#016x\n", regs.A4, regs.A5)
|
||||||
|
fmt.Printf(" A6 = %#016x A7 = %#016x\n", regs.A6, regs.A7)
|
||||||
|
fmt.Printf(" T0 = %#016x T1 = %#016x\n", regs.T0, regs.T1)
|
||||||
|
fmt.Printf(" T2 = %#016x T3 = %#016x\n", regs.T2, regs.T3)
|
||||||
|
fmt.Printf(" T4 = %#016x T5 = %#016x\n", regs.T4, regs.T5)
|
||||||
|
fmt.Printf(" T6 = %#016x\n", regs.T6)
|
||||||
|
fmt.Printf(" S1 = %#016x S2 = %#016x\n", regs.S1, regs.S2)
|
||||||
|
fmt.Printf(" S3 = %#016x S4 = %#016x\n", regs.S3, regs.S4)
|
||||||
|
fmt.Printf(" S5 = %#016x S6 = %#016x\n", regs.S5, regs.S6)
|
||||||
|
fmt.Printf(" S7 = %#016x S8 = %#016x\n", regs.S7, regs.S8)
|
||||||
|
fmt.Printf(" S9 = %#016x S10 = %#016x\n", regs.S9, regs.S10)
|
||||||
|
fmt.Printf(" S11 = %#016x\n", regs.S11)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVectorRegs(v *VectorRegs) {
|
||||||
|
fmt.Println("\n FP registers (F0-F31):")
|
||||||
|
for i := 0; i < 32; i += 2 {
|
||||||
|
fmt.Printf(" F%-2d = %#018x F%-2d = %#018x\n", i, v.F[i], i+1, v.F[i+1])
|
||||||
|
}
|
||||||
|
fmt.Printf(" FCSR = %#x\n", v.FCSR)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReg modifies a register value in the debuggee.
|
||||||
|
func (s *Session) SetReg(name string, value uint64) error {
|
||||||
|
regs, err := s.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "pc":
|
||||||
|
regs.PC = value
|
||||||
|
case "ra", "x1":
|
||||||
|
regs.Ra = value
|
||||||
|
case "sp", "x2":
|
||||||
|
regs.Sp = value
|
||||||
|
case "gp", "x3":
|
||||||
|
regs.Gp = value
|
||||||
|
case "tp", "x4":
|
||||||
|
regs.Tp = value
|
||||||
|
case "t0", "x5":
|
||||||
|
regs.T0 = value
|
||||||
|
case "t1", "x6":
|
||||||
|
regs.T1 = value
|
||||||
|
case "t2", "x7":
|
||||||
|
regs.T2 = value
|
||||||
|
case "s0", "fp", "x8":
|
||||||
|
regs.S0 = value
|
||||||
|
case "s1", "x9":
|
||||||
|
regs.S1 = value
|
||||||
|
case "a0", "x10":
|
||||||
|
regs.A0 = value
|
||||||
|
case "a1", "x11":
|
||||||
|
regs.A1 = value
|
||||||
|
case "a2", "x12":
|
||||||
|
regs.A2 = value
|
||||||
|
case "a3", "x13":
|
||||||
|
regs.A3 = value
|
||||||
|
case "a4", "x14":
|
||||||
|
regs.A4 = value
|
||||||
|
case "a5", "x15":
|
||||||
|
regs.A5 = value
|
||||||
|
case "a6", "x16":
|
||||||
|
regs.A6 = value
|
||||||
|
case "a7", "x17":
|
||||||
|
regs.A7 = value
|
||||||
|
case "s2", "x18":
|
||||||
|
regs.S2 = value
|
||||||
|
case "s3", "x19":
|
||||||
|
regs.S3 = value
|
||||||
|
case "s4", "x20":
|
||||||
|
regs.S4 = value
|
||||||
|
case "s5", "x21":
|
||||||
|
regs.S5 = value
|
||||||
|
case "s6", "x22":
|
||||||
|
regs.S6 = value
|
||||||
|
case "s7", "x23":
|
||||||
|
regs.S7 = value
|
||||||
|
case "s8", "x24":
|
||||||
|
regs.S8 = value
|
||||||
|
case "s9", "x25":
|
||||||
|
regs.S9 = value
|
||||||
|
case "s10", "x26":
|
||||||
|
regs.S10 = value
|
||||||
|
case "s11", "x27":
|
||||||
|
regs.S11 = value
|
||||||
|
case "t3", "x28":
|
||||||
|
regs.T3 = value
|
||||||
|
case "t4", "x29":
|
||||||
|
regs.T4 = value
|
||||||
|
case "t5", "x30":
|
||||||
|
regs.T5 = value
|
||||||
|
case "t6", "x31":
|
||||||
|
regs.T6 = value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: unknown register %q", name)
|
||||||
|
}
|
||||||
|
return s.SetRegs(®s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archReturnAddr reads the return address from RA (riscv64 convention).
|
||||||
|
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||||
|
return regs.Ra, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archSPLabel returns the SP register name for display.
|
||||||
|
func archSPLabel() string { return "SP" }
|
||||||
@@ -17,8 +17,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Integration tests beyond the basic entry breakpoint: hardware watchpoints,
|
// Integration tests beyond the basic entry breakpoint: hardware watchpoints,
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session is a ptrace debugging session controlling one debuggee process.
|
||||||
|
// The FreeBSD implementation sits behind the same surface as the Linux one:
|
||||||
|
// PT_TRACE_ME from the debuggee, PT_CONTINUE/PT_STEP from the tracer, and
|
||||||
|
// tracee memory through PT_IO (FreeBSD has no /proc/pid/mem to fall back
|
||||||
|
// on, so PT_IO is the only supported route).
|
||||||
|
type Session struct {
|
||||||
|
pid int
|
||||||
|
cmd *exec.Cmd
|
||||||
|
stopped bool
|
||||||
|
exited bool
|
||||||
|
codeBase uint64 // base address of the JIT code in the debuggee
|
||||||
|
tmpDir string // scratch directory of the session, removed on Kill
|
||||||
|
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 dbw 0-15)
|
||||||
|
// lastSignal holds the signal of the most recent stop when that stop
|
||||||
|
// was a genuine signal-delivery-stop the caller must see (a fault such
|
||||||
|
// as SIGSEGV, SIGBUS, SIGFPE or SIGILL); 0 for breakpoint traps,
|
||||||
|
// single-steps, SIGSTOP and suppressed runtime signals.
|
||||||
|
lastSignal syscall.Signal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||||
|
// attaches to it via ptrace.
|
||||||
|
func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||||
|
sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "")
|
||||||
|
return sess, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee.
|
||||||
|
//
|
||||||
|
// It pins the calling goroutine to its OS thread and leaves it pinned: the
|
||||||
|
// debuggee's PT_TRACE_ME binds the tracer relation to the forking thread,
|
||||||
|
// and every ptrace request on the session must come from that same thread.
|
||||||
|
// All Session methods must therefore be called from the goroutine that
|
||||||
|
// launched the session (the REPL and coverage loops do exactly that).
|
||||||
|
func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) {
|
||||||
|
runtime.LockOSThread() // ptrace requests must stay on the forking thread
|
||||||
|
self, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||||
|
}
|
||||||
|
if gasmBin != "" {
|
||||||
|
self = gasmBin
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "gasm-debug-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("debug: tempdir: %w", err)
|
||||||
|
}
|
||||||
|
argsFile := filepath.Join(tmpDir, "args.bin")
|
||||||
|
if err := os.WriteFile(argsFile, args, 0o644); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return nil, nil, fmt.Errorf("debug: write args: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bufSpec != "" {
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return nil, nil, fmt.Errorf("debug: write bufspec: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(self, "debug", "--func", funcName, "--args", argsFile, asmPath)
|
||||||
|
cmd.Env = append(os.Environ(), "GASM_DEBUG_TARGET=1", "GASM_DEBUG_TMP="+tmpDir)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return nil, nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Session{pid: cmd.Process.Pid, cmd: cmd, tmpDir: tmpDir}
|
||||||
|
|
||||||
|
readyFile := filepath.Join(tmpDir, "ready")
|
||||||
|
for range 500 {
|
||||||
|
if _, err := os.Stat(readyFile); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The debuggee parks itself with SIGSTOP once the JIT code is mapped.
|
||||||
|
// A Go tracee also reports SIGURG preemption as signal-delivery-stops,
|
||||||
|
// so the wait loops until a stop the debugger cares about instead of
|
||||||
|
// assuming the first event is the SIGSTOP.
|
||||||
|
if _, err := s.waitStopped(); err != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return nil, nil, fmt.Errorf("debug: wait for debuggee: %w", err)
|
||||||
|
}
|
||||||
|
s.stopped = true
|
||||||
|
|
||||||
|
// The debuggee reports its JIT mapping in the codebase file; that is
|
||||||
|
// the supported path on FreeBSD, where no /proc/pid/maps exists to
|
||||||
|
// scan for the RWX region as a fallback.
|
||||||
|
if data, err := os.ReadFile(filepath.Join(tmpDir, "codebase")); err == nil {
|
||||||
|
fmt.Sscanf(string(data), "%d", &s.codeBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufAddrs []uint64
|
||||||
|
if bufSpec != "" {
|
||||||
|
addrFile := filepath.Join(tmpDir, "bufaddrs")
|
||||||
|
if data, err := os.ReadFile(addrFile); err == nil {
|
||||||
|
for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
|
||||||
|
var addr uint64
|
||||||
|
if _, err := fmt.Sscanf(line, "%d", &addr); err == nil {
|
||||||
|
bufAddrs = append(bufAddrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, bufAddrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||||
|
// about arrives: SIGTRAP (a breakpoint or a completed single-step), the
|
||||||
|
// debuggee's own SIGSTOP, or a genuine signal-delivery-stop. A Go tracee's
|
||||||
|
// runtime raises SIGURG for asynchronous preemption, and every signal on a
|
||||||
|
// traced thread surfaces as a signal-delivery-stop, so SIGURG is suppressed
|
||||||
|
// and the tracee resumed without it. Every other signal (SIGSEGV, SIGBUS,
|
||||||
|
// SIGFPE, SIGILL, ...) is returned to the caller: resuming with signal 0
|
||||||
|
// would restart the faulting instruction and fault forever, so a faulting
|
||||||
|
// kernel must surface as a stop the caller reports.
|
||||||
|
func (s *Session) waitStopped() (syscall.Signal, error) {
|
||||||
|
for {
|
||||||
|
var ws syscall.WaitStatus
|
||||||
|
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if ws.Exited() {
|
||||||
|
s.exited = true
|
||||||
|
return 0, fmt.Errorf("debuggee exited with status %d", ws.ExitStatus())
|
||||||
|
}
|
||||||
|
if ws.Signaled() {
|
||||||
|
s.exited = true
|
||||||
|
return 0, fmt.Errorf("debuggee killed by signal %v", ws.Signal())
|
||||||
|
}
|
||||||
|
switch sig := ws.StopSignal(); sig {
|
||||||
|
case syscall.SIGTRAP, syscall.SIGSTOP:
|
||||||
|
s.stopped = true
|
||||||
|
s.lastSignal = 0
|
||||||
|
return sig, nil
|
||||||
|
case syscall.SIGURG:
|
||||||
|
// Go runtime asynchronous preemption: resume the tracee
|
||||||
|
// without delivering the signal.
|
||||||
|
s.lastSignal = 0
|
||||||
|
if err := unix.PtraceCont(s.pid, 0); err != nil {
|
||||||
|
return 0, fmt.Errorf("debug: PT_CONTINUE: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// A genuine signal-delivery-stop. Report it; the caller
|
||||||
|
// decides how to proceed.
|
||||||
|
s.stopped = true
|
||||||
|
s.lastSignal = sig
|
||||||
|
return sig, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastSignal returns the signal of the most recent stop when that stop was
|
||||||
|
// a genuine signal-delivery-stop (a fault such as SIGSEGV, SIGFPE, SIGILL
|
||||||
|
// or SIGBUS), and 0 for breakpoint traps, single-steps, SIGSTOP and
|
||||||
|
// suppressed runtime signals.
|
||||||
|
func (s *Session) LastSignal() syscall.Signal { return s.lastSignal }
|
||||||
|
|
||||||
|
// Peek reads a word (8 bytes) from the debuggee's memory at addr, through
|
||||||
|
// PT_IO with PIOD_READ_D.
|
||||||
|
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||||
|
var buf [8]byte
|
||||||
|
if _, err := unix.PtraceIO(unix.PIOD_READ_D, s.pid, uintptr(addr), buf[:], len(buf)); err != nil {
|
||||||
|
return 0, fmt.Errorf("debug: read mem %#x: %w", addr, err)
|
||||||
|
}
|
||||||
|
return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 |
|
||||||
|
uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poke writes a word (8 bytes) to the debuggee's memory at addr, through
|
||||||
|
// PT_IO with PIOD_WRITE_D.
|
||||||
|
func (s *Session) Poke(addr, val uint64) error {
|
||||||
|
buf := []byte{byte(val), byte(val >> 8), byte(val >> 16), byte(val >> 24),
|
||||||
|
byte(val >> 32), byte(val >> 40), byte(val >> 48), byte(val >> 56)}
|
||||||
|
if _, err := unix.PtraceIO(unix.PIOD_WRITE_D, s.pid, uintptr(addr), buf, len(buf)); err != nil {
|
||||||
|
return fmt.Errorf("debug: write mem %#x: %w", addr, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMemory reads len bytes from the debuggee's memory at addr in one
|
||||||
|
// PT_IO request, the shape the request is built for.
|
||||||
|
func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||||
|
out := make([]byte, length)
|
||||||
|
n, err := unix.PtraceIO(unix.PIOD_READ_D, s.pid, uintptr(addr), out, length)
|
||||||
|
return out[:n], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMemory writes bytes to the debuggee's memory at addr in one PT_IO
|
||||||
|
// request.
|
||||||
|
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||||
|
_, err := unix.PtraceIO(unix.PIOD_WRITE_D, s.pid, uintptr(addr), data, len(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step executes a single instruction in the debuggee.
|
||||||
|
func (s *Session) Step() error {
|
||||||
|
if s.exited {
|
||||||
|
return fmt.Errorf("debug: debuggee has exited")
|
||||||
|
}
|
||||||
|
if err := unix.PtraceSingleStep(s.pid); err != nil {
|
||||||
|
return fmt.Errorf("debug: PT_STEP: %w", err)
|
||||||
|
}
|
||||||
|
_, err := s.waitStopped()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue resumes execution until the next breakpoint or exit.
|
||||||
|
func (s *Session) Continue() error {
|
||||||
|
if s.exited {
|
||||||
|
return fmt.Errorf("debug: debuggee has exited")
|
||||||
|
}
|
||||||
|
if err := unix.PtraceCont(s.pid, 0); err != nil {
|
||||||
|
return fmt.Errorf("debug: PT_CONTINUE: %w", err)
|
||||||
|
}
|
||||||
|
_, err := s.waitStopped()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exited returns true if the debuggee has terminated.
|
||||||
|
func (s *Session) Exited() bool { return s.exited }
|
||||||
|
|
||||||
|
// Pid returns the debuggee's process ID.
|
||||||
|
func (s *Session) Pid() int { return s.pid }
|
||||||
|
|
||||||
|
// CodeBase returns the base address of the JIT code in the debuggee.
|
||||||
|
func (s *Session) CodeBase() uint64 { return s.codeBase }
|
||||||
|
|
||||||
|
// Kill terminates the debuggee and removes the session's scratch
|
||||||
|
// directory, so a successful session leaves no gasm-debug-* debris behind.
|
||||||
|
func (s *Session) Kill() {
|
||||||
|
if !s.exited {
|
||||||
|
syscall.Kill(s.pid, syscall.SIGKILL)
|
||||||
|
syscall.Wait4(s.pid, nil, 0, nil)
|
||||||
|
s.exited = true
|
||||||
|
}
|
||||||
|
if s.cmd != nil && s.cmd.Process != nil {
|
||||||
|
s.cmd.Wait()
|
||||||
|
}
|
||||||
|
if s.tmpDir != "" {
|
||||||
|
os.RemoveAll(s.tmpDir)
|
||||||
|
s.tmpDir = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execRange is one executable mapping of the debuggee.
|
||||||
|
type execRange struct {
|
||||||
|
lo, hi uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// execRanges is a stub on FreeBSD: there is no /proc/pid/maps to parse,
|
||||||
|
// and procfs(5) is not guaranteed to be mounted. The callers degrade
|
||||||
|
// gracefully: archReturnAddr falls back to the raw stack convention and
|
||||||
|
// the mapping scan is skipped.
|
||||||
|
func execRanges(pid int) []execRange { return nil }
|
||||||
|
|
||||||
|
// findRWXMapping is a stub on FreeBSD for the same reason: the codebase
|
||||||
|
// handshake file is the supported way the JIT region is located.
|
||||||
|
func findRWXMapping(pid int) uint64 { return 0 }
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||||
|
// converts the FreeBSD struct reg into the portable layout.
|
||||||
|
func (s *Session) GetRegs() (Regs, error) {
|
||||||
|
var ur unix.Reg
|
||||||
|
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||||
|
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||||
|
}
|
||||||
|
return Regs{
|
||||||
|
R15: uint64(ur.R15),
|
||||||
|
R14: uint64(ur.R14),
|
||||||
|
R13: uint64(ur.R13),
|
||||||
|
R12: uint64(ur.R12),
|
||||||
|
R11: uint64(ur.R11),
|
||||||
|
R10: uint64(ur.R10),
|
||||||
|
R9: uint64(ur.R9),
|
||||||
|
R8: uint64(ur.R8),
|
||||||
|
RDI: uint64(ur.Rdi),
|
||||||
|
RSI: uint64(ur.Rsi),
|
||||||
|
RBP: uint64(ur.Rbp),
|
||||||
|
RBX: uint64(ur.Rbx),
|
||||||
|
RDX: uint64(ur.Rdx),
|
||||||
|
RCX: uint64(ur.Rcx),
|
||||||
|
RAX: uint64(ur.Rax),
|
||||||
|
RIP: uint64(ur.Rip),
|
||||||
|
CS: uint64(ur.Cs),
|
||||||
|
RFLAGS: uint64(ur.Rflags),
|
||||||
|
RSP: uint64(ur.Rsp),
|
||||||
|
SS: uint64(ur.Ss),
|
||||||
|
FS: uint64(ur.Fs),
|
||||||
|
GS: uint64(ur.Gs),
|
||||||
|
DS: uint64(ur.Ds),
|
||||||
|
ES: uint64(ur.Es),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||||
|
func (s *Session) SetRegs(regs *Regs) error {
|
||||||
|
// Read-modify-write keeps the fields FreeBSD owns (trapno, err) intact.
|
||||||
|
var ur unix.Reg
|
||||||
|
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||||
|
return fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||||
|
}
|
||||||
|
ur.R15 = int64(regs.R15)
|
||||||
|
ur.R14 = int64(regs.R14)
|
||||||
|
ur.R13 = int64(regs.R13)
|
||||||
|
ur.R12 = int64(regs.R12)
|
||||||
|
ur.R11 = int64(regs.R11)
|
||||||
|
ur.R10 = int64(regs.R10)
|
||||||
|
ur.R9 = int64(regs.R9)
|
||||||
|
ur.R8 = int64(regs.R8)
|
||||||
|
ur.Rdi = int64(regs.RDI)
|
||||||
|
ur.Rsi = int64(regs.RSI)
|
||||||
|
ur.Rbp = int64(regs.RBP)
|
||||||
|
ur.Rbx = int64(regs.RBX)
|
||||||
|
ur.Rdx = int64(regs.RDX)
|
||||||
|
ur.Rcx = int64(regs.RCX)
|
||||||
|
ur.Rax = int64(regs.RAX)
|
||||||
|
ur.Rip = int64(regs.RIP)
|
||||||
|
ur.Cs = int64(regs.CS)
|
||||||
|
ur.Rflags = int64(regs.RFLAGS)
|
||||||
|
ur.Rsp = int64(regs.RSP)
|
||||||
|
ur.Ss = int64(regs.SS)
|
||||||
|
return unix.PtraceSetRegs(s.pid, &ur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPRegs holds the x87 FPU and SSE (XMM) register state, the FXSAVE image
|
||||||
|
// the FreeBSD struct fpreg mirrors: XMM0-15 at the same offsets.
|
||||||
|
type FPRegs struct {
|
||||||
|
XMM [16][16]byte // XMM0-15
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFPRegs retrieves the FPU/SSE register state via PT_GETFPREGS. The
|
||||||
|
// FreeBSD struct fpreg mirrors the FXSAVE image: the x87 environment and
|
||||||
|
// stack in Env/Acc, XMM0-15 in Xacc.
|
||||||
|
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||||
|
var fp FPRegs
|
||||||
|
var fr unix.FpReg
|
||||||
|
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||||
|
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||||
|
}
|
||||||
|
for i := range 16 {
|
||||||
|
copy(fp.XMM[i][:], fr.Xacc[i][:])
|
||||||
|
}
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorRegs holds the YMM register state.
|
||||||
|
type VectorRegs struct {
|
||||||
|
YMM [16][32]byte // YMM0-15 (full 256-bit values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The XSAVE area the PT_GETXSTATE request returns follows the architectural
|
||||||
|
// layout (Intel SDM vol 1, "XSAVE"): the 512-byte legacy FXSAVE image (x87
|
||||||
|
// state in 0-159, XMM0-15 in 160-511), then the 64-byte xsave header whose
|
||||||
|
// first 8 bytes are xstate_bv, then one component per set feature bit, each
|
||||||
|
// 64-byte aligned. The YMM high halves are the first extended component,
|
||||||
|
// at offset 576; XFEATURE_STATE_BIT_AVX is bit 2 of xstate_bv.
|
||||||
|
const (
|
||||||
|
xsaveXMMOffset = 160
|
||||||
|
xsaveHeaderOffset = 512
|
||||||
|
xsaveBVOffset = xsaveHeaderOffset
|
||||||
|
ymmOffset = xsaveHeaderOffset + 64 // 576
|
||||||
|
ymmSize = 256 // 16 registers, 16 bytes each
|
||||||
|
xfeatureMaskYMM = 1 << 2
|
||||||
|
xstateMaxBuffer = 4096 // PT_GETXSTATE_INFO bounds the size far below this
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetVectorRegs retrieves the YMM registers via PT_GETXSTATE. The low
|
||||||
|
// (XMM) halves always come from the legacy image; the high halves are
|
||||||
|
// copied only when xstate_bv reports the AVX state, and read as zero
|
||||||
|
// otherwise. When the request fails the FP image still provides correct
|
||||||
|
// XMM halves, so that is the fallback.
|
||||||
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
|
var v VectorRegs
|
||||||
|
buf := make([]byte, xstateMaxBuffer)
|
||||||
|
n, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS_PTRACE,
|
||||||
|
uintptr(unix.PT_GETXSTATE),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
fp, err := s.GetFPRegs()
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
for i := range 16 {
|
||||||
|
copy(v.YMM[i][:16], fp.XMM[i][:])
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
for i := range 16 {
|
||||||
|
copy(v.YMM[i][:16], buf[xsaveXMMOffset+16*i:xsaveXMMOffset+16*i+16])
|
||||||
|
}
|
||||||
|
if int(n) >= ymmOffset+ymmSize {
|
||||||
|
if binary.LittleEndian.Uint64(buf[xsaveBVOffset:xsaveBVOffset+8])&xfeatureMaskYMM != 0 {
|
||||||
|
for i := range 16 {
|
||||||
|
copy(v.YMM[i][16:], buf[ymmOffset+16*i:ymmOffset+16*i+16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && arm64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||||
|
// converts the FreeBSD struct reg (x[30], lr, sp, elr, spsr) into the
|
||||||
|
// portable layout.
|
||||||
|
func (s *Session) GetRegs() (Regs, error) {
|
||||||
|
var ur unix.Reg
|
||||||
|
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||||
|
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||||
|
}
|
||||||
|
return Regs{
|
||||||
|
X0: ur.X[0],
|
||||||
|
X1: ur.X[1],
|
||||||
|
X2: ur.X[2],
|
||||||
|
X3: ur.X[3],
|
||||||
|
X4: ur.X[4],
|
||||||
|
X5: ur.X[5],
|
||||||
|
X6: ur.X[6],
|
||||||
|
X7: ur.X[7],
|
||||||
|
X8: ur.X[8],
|
||||||
|
X9: ur.X[9],
|
||||||
|
X10: ur.X[10],
|
||||||
|
X11: ur.X[11],
|
||||||
|
X12: ur.X[12],
|
||||||
|
X13: ur.X[13],
|
||||||
|
X14: ur.X[14],
|
||||||
|
X15: ur.X[15],
|
||||||
|
X16: ur.X[16],
|
||||||
|
X17: ur.X[17],
|
||||||
|
X18: ur.X[18],
|
||||||
|
X19: ur.X[19],
|
||||||
|
X20: ur.X[20],
|
||||||
|
X21: ur.X[21],
|
||||||
|
X22: ur.X[22],
|
||||||
|
X23: ur.X[23],
|
||||||
|
X24: ur.X[24],
|
||||||
|
X25: ur.X[25],
|
||||||
|
X26: ur.X[26],
|
||||||
|
X27: ur.X[27],
|
||||||
|
X28: ur.X[28],
|
||||||
|
X29: ur.X[29],
|
||||||
|
X30: ur.Lr,
|
||||||
|
SP: ur.Sp,
|
||||||
|
PC: ur.Elr,
|
||||||
|
PSTATE: uint64(ur.Spsr),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||||
|
func (s *Session) SetRegs(regs *Regs) error {
|
||||||
|
var ur unix.Reg
|
||||||
|
ur.X = [30]uint64{
|
||||||
|
regs.X0, regs.X1, regs.X2, regs.X3, regs.X4, regs.X5, regs.X6,
|
||||||
|
regs.X7, regs.X8, regs.X9, regs.X10, regs.X11, regs.X12, regs.X13,
|
||||||
|
regs.X14, regs.X15, regs.X16, regs.X17, regs.X18, regs.X19, regs.X20,
|
||||||
|
regs.X21, regs.X22, regs.X23, regs.X24, regs.X25, regs.X26, regs.X27,
|
||||||
|
regs.X28, regs.X29,
|
||||||
|
}
|
||||||
|
ur.Lr = regs.X30
|
||||||
|
ur.Sp = regs.SP
|
||||||
|
ur.Elr = regs.PC
|
||||||
|
ur.Spsr = uint32(regs.PSTATE)
|
||||||
|
return unix.PtraceSetRegs(s.pid, &ur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPRegs holds the arm64 FP/NEON register state: the 32 128-bit V
|
||||||
|
// registers, then FPSR and FPCR (the user_fpsimd shape).
|
||||||
|
type FPRegs struct {
|
||||||
|
V [32][16]byte // V0-V31 (128-bit NEON/FP registers)
|
||||||
|
FPSR uint32
|
||||||
|
FPCR uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFPRegs retrieves the FP/NEON register state via PT_GETFPREGS. The
|
||||||
|
// FreeBSD struct fpreg holds the 32 128-bit V registers followed by FPSR
|
||||||
|
// and FPCR, the user_fpsimd shape.
|
||||||
|
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||||
|
var fp FPRegs
|
||||||
|
var fr unix.FpReg
|
||||||
|
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||||
|
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||||
|
}
|
||||||
|
for i := range 32 {
|
||||||
|
copy(fp.V[i][:], fr.Q[i][:])
|
||||||
|
}
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorRegs holds the full SIMD register state.
|
||||||
|
type VectorRegs struct {
|
||||||
|
V [32][16]byte // V0-V31 (128-bit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVectorRegs retrieves the SIMD registers.
|
||||||
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
|
var v VectorRegs
|
||||||
|
fp, err := s.GetFPRegs()
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
copy(v.V[:][:], fp.V[:][:])
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && riscv64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRegs reads the general-purpose registers of the stopped debuggee and
|
||||||
|
// converts the FreeBSD struct reg into the portable layout. Sstatus rides
|
||||||
|
// the kernel's struct but the portable surface carries the GPRs and PC.
|
||||||
|
func (s *Session) GetRegs() (Regs, error) {
|
||||||
|
var ur unix.Reg
|
||||||
|
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||||
|
return Regs{}, fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||||
|
}
|
||||||
|
return Regs{
|
||||||
|
PC: ur.Sepc,
|
||||||
|
Ra: ur.Ra,
|
||||||
|
Sp: ur.Sp,
|
||||||
|
Gp: ur.Gp,
|
||||||
|
Tp: ur.Tp,
|
||||||
|
T0: ur.T[0],
|
||||||
|
T1: ur.T[1],
|
||||||
|
T2: ur.T[2],
|
||||||
|
S0: ur.S[0],
|
||||||
|
S1: ur.S[1],
|
||||||
|
A0: ur.A[0],
|
||||||
|
A1: ur.A[1],
|
||||||
|
A2: ur.A[2],
|
||||||
|
A3: ur.A[3],
|
||||||
|
A4: ur.A[4],
|
||||||
|
A5: ur.A[5],
|
||||||
|
A6: ur.A[6],
|
||||||
|
A7: ur.A[7],
|
||||||
|
S2: ur.S[2],
|
||||||
|
S3: ur.S[3],
|
||||||
|
S4: ur.S[4],
|
||||||
|
S5: ur.S[5],
|
||||||
|
S6: ur.S[6],
|
||||||
|
S7: ur.S[7],
|
||||||
|
S8: ur.S[8],
|
||||||
|
S9: ur.S[9],
|
||||||
|
S10: ur.S[10],
|
||||||
|
S11: ur.S[11],
|
||||||
|
T3: ur.T[3],
|
||||||
|
T4: ur.T[4],
|
||||||
|
T5: ur.T[5],
|
||||||
|
T6: ur.T[6],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||||
|
// Read-modify-write keeps sstatus, which the kernel owns, intact.
|
||||||
|
func (s *Session) SetRegs(regs *Regs) error {
|
||||||
|
var ur unix.Reg
|
||||||
|
if err := unix.PtraceGetRegs(s.pid, &ur); err != nil {
|
||||||
|
return fmt.Errorf("debug: PT_GETREGS: %w", err)
|
||||||
|
}
|
||||||
|
ur.Sepc = regs.PC
|
||||||
|
ur.Ra = regs.Ra
|
||||||
|
ur.Sp = regs.Sp
|
||||||
|
ur.Gp = regs.Gp
|
||||||
|
ur.Tp = regs.Tp
|
||||||
|
ur.T = [7]uint64{regs.T0, regs.T1, regs.T2, regs.T3, regs.T4, regs.T5, regs.T6}
|
||||||
|
ur.S = [12]uint64{regs.S0, regs.S1, regs.S2, regs.S3, regs.S4, regs.S5,
|
||||||
|
regs.S6, regs.S7, regs.S8, regs.S9, regs.S10, regs.S11}
|
||||||
|
ur.A = [8]uint64{regs.A0, regs.A1, regs.A2, regs.A3, regs.A4, regs.A5, regs.A6, regs.A7}
|
||||||
|
return unix.PtraceSetRegs(s.pid, &ur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPRegs holds the RISC-V FP register state (32 64-bit FP registers plus
|
||||||
|
// fcsr).
|
||||||
|
type FPRegs struct {
|
||||||
|
F [32]uint64 // F0-F31 (64-bit FP registers)
|
||||||
|
FCSR uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFPRegs retrieves the FP register state via PT_GETFPREGS. The FreeBSD
|
||||||
|
// struct fpreg carries each 64-bit FP register in a 128-bit slot (fp_x is
|
||||||
|
// the flat [64]-word area the x/sys type renders as [32][2]); the low word
|
||||||
|
// holds the register, and FCSR rides the tail.
|
||||||
|
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||||
|
var fp FPRegs
|
||||||
|
var fr unix.FpReg
|
||||||
|
if err := unix.PtraceGetFpRegs(s.pid, &fr); err != nil {
|
||||||
|
return fp, fmt.Errorf("debug: PT_GETFPREGS: %w", err)
|
||||||
|
}
|
||||||
|
for i := range 32 {
|
||||||
|
fp.F[i] = fr.X[i][0]
|
||||||
|
}
|
||||||
|
fp.FCSR = uint32(fr.Fcsr)
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorRegs holds the FP register state shown by the regs command
|
||||||
|
// (riscv64 has 32 64-bit FP registers and fcsr).
|
||||||
|
type VectorRegs struct {
|
||||||
|
F [32]uint64
|
||||||
|
FCSR uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVectorRegs retrieves the FP registers.
|
||||||
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
|
fp, err := s.GetFPRegs()
|
||||||
|
if err != nil {
|
||||||
|
return VectorRegs{}, err
|
||||||
|
}
|
||||||
|
return VectorRegs{F: fp.F, FCSR: fp.FCSR}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLaunchAndBreakpoint is the FreeBSD twin of the Linux integration
|
||||||
|
// test: it drives the whole launch, breakpoint, trap and register-rewind
|
||||||
|
// flow end to end. It needs a real FreeBSD kernel (ptrace does not work
|
||||||
|
// under emulation), so it only runs where it can.
|
||||||
|
func TestLaunchAndBreakpoint(t *testing.T) {
|
||||||
|
if runtime.GOARCH != "amd64" {
|
||||||
|
t.Skip("runs only on amd64 hosts")
|
||||||
|
}
|
||||||
|
// The tracer is the OS thread that forked the debuggee (PT_TRACE_ME
|
||||||
|
// binds the relation to that thread); every ptrace request must come
|
||||||
|
// from the same thread, so pin the test goroutine to one thread.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
bin := filepath.Join(t.TempDir(), "gasm")
|
||||||
|
out, err := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("build gasm: %v: %s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
const kernelPath = "../testdata/verify/basic_amd64.s"
|
||||||
|
k, err := verify.Load(kernelPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(k.Close)
|
||||||
|
fl, err := k.Func("wideCopy")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Func: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := Launch(bin, kernelPath, "wideCopy", make([]byte, fl.Args))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Launch: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(sess.Kill)
|
||||||
|
|
||||||
|
bm := NewBreakpoints(sess)
|
||||||
|
entry := sess.CodeBase() + uint64(fl.Offset)
|
||||||
|
if _, err := bm.Set(entry, "entry"); err != nil {
|
||||||
|
t.Fatalf("Set: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The INT3 must be visible in the debuggee's memory.
|
||||||
|
word, err := sess.Peek(entry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Peek: %v", err)
|
||||||
|
}
|
||||||
|
if b := word & 0xFF; b != 0xCC {
|
||||||
|
t.Fatalf("int3 not patched: first byte %#02x at %#x", b, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The debuggee raises a second SIGSTOP after the launch barrier (the
|
||||||
|
// child's RunTarget marks its entry), so like the REPL and the cover
|
||||||
|
// mode the test keeps resuming until the breakpoint trap arrives.
|
||||||
|
for range 10 {
|
||||||
|
if err := sess.Continue(); err != nil {
|
||||||
|
t.Fatalf("Continue: %v", err)
|
||||||
|
}
|
||||||
|
if sess.Exited() {
|
||||||
|
t.Fatal("debuggee exited instead of trapping on the breakpoint")
|
||||||
|
}
|
||||||
|
regs, err := sess.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetRegs: %v", err)
|
||||||
|
}
|
||||||
|
if bp := bm.HandleTrap(®s); bp != nil {
|
||||||
|
if bp.Addr != entry {
|
||||||
|
t.Fatalf("trap at %#x, want %#x", bp.Addr, entry)
|
||||||
|
}
|
||||||
|
return // trap on the entry breakpoint: the whole flow works
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("no breakpoint trap after 10 resumes")
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildGasm produces the gasm binary the debugger spawns as its debuggee.
|
// buildGasm produces the gasm binary the debugger spawns as its debuggee.
|
||||||
@@ -24,7 +24,7 @@ func buildGasm(t *testing.T) string {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
bin := filepath.Join(t.TempDir(), "gasm")
|
bin := filepath.Join(t.TempDir(), "gasm")
|
||||||
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm")
|
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("build gasm: %v: %s", err, out)
|
t.Fatalf("build gasm: %v: %s", err, out)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
// Regs holds the full general-purpose register set of a traced process
|
||||||
|
// (the FreeBSD amd64 struct reg layout, sys/x86/include/reg.h). FreeBSD
|
||||||
|
// reports segment selectors (FS/GS/ES/DS), not the bases the Linux ptrace
|
||||||
|
// surface carries, and has no ORIG_RAX slot.
|
||||||
|
type Regs struct {
|
||||||
|
R15 uint64
|
||||||
|
R14 uint64
|
||||||
|
R13 uint64
|
||||||
|
R12 uint64
|
||||||
|
RBP uint64
|
||||||
|
RBX uint64
|
||||||
|
R11 uint64
|
||||||
|
R10 uint64
|
||||||
|
R9 uint64
|
||||||
|
R8 uint64
|
||||||
|
RAX uint64
|
||||||
|
RCX uint64
|
||||||
|
RDX uint64
|
||||||
|
RSI uint64
|
||||||
|
RDI uint64
|
||||||
|
RIP uint64
|
||||||
|
CS uint64
|
||||||
|
RFLAGS uint64
|
||||||
|
RSP uint64
|
||||||
|
SS uint64
|
||||||
|
FS uint64
|
||||||
|
GS uint64
|
||||||
|
DS uint64
|
||||||
|
ES uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPC returns the program counter.
|
||||||
|
func (r *Regs) GetPC() uint64 { return r.RIP }
|
||||||
|
|
||||||
|
// SetPC sets the program counter.
|
||||||
|
func (r *Regs) SetPC(pc uint64) { r.RIP = pc }
|
||||||
|
|
||||||
|
// GetSP returns the stack pointer.
|
||||||
|
func (r *Regs) GetSP() uint64 { return r.RSP }
|
||||||
|
|
||||||
|
// RegValue returns the value of the named register, or false if unknown.
|
||||||
|
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||||
|
switch name {
|
||||||
|
case "rax", "eax", "ax", "al":
|
||||||
|
return r.RAX, true
|
||||||
|
case "rbx", "ebx", "bx", "bl":
|
||||||
|
return r.RBX, true
|
||||||
|
case "rcx", "ecx", "cx", "cl":
|
||||||
|
return r.RCX, true
|
||||||
|
case "rdx", "edx", "dx", "dl":
|
||||||
|
return r.RDX, true
|
||||||
|
case "rsi", "esi", "si":
|
||||||
|
return r.RSI, true
|
||||||
|
case "rdi", "edi", "di":
|
||||||
|
return r.RDI, true
|
||||||
|
case "rbp", "ebp", "bp":
|
||||||
|
return r.RBP, true
|
||||||
|
case "rsp", "esp", "sp":
|
||||||
|
return r.RSP, true
|
||||||
|
case "r8":
|
||||||
|
return r.R8, true
|
||||||
|
case "r9":
|
||||||
|
return r.R9, true
|
||||||
|
case "r10":
|
||||||
|
return r.R10, true
|
||||||
|
case "r11":
|
||||||
|
return r.R11, true
|
||||||
|
case "r12":
|
||||||
|
return r.R12, true
|
||||||
|
case "r13":
|
||||||
|
return r.R13, true
|
||||||
|
case "r14":
|
||||||
|
return r.R14, true
|
||||||
|
case "r15":
|
||||||
|
return r.R15, true
|
||||||
|
case "rip", "eip":
|
||||||
|
return r.RIP, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// breakpointInsn is the software breakpoint instruction.
|
||||||
|
var breakpointInsn = []byte{0xCC} // INT3
|
||||||
|
|
||||||
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
|
// a trap. INT3 leaves the hardware PC on the following instruction (Intel
|
||||||
|
// SDM vol 3, "Debug Exceptions") and the FreeBSD T_BPTFLT path delivers
|
||||||
|
// that frame unmodified (sys/amd64/amd64/trap.c), so the trap address is
|
||||||
|
// PC-1, the same correction the Linux side applies.
|
||||||
|
const breakpointPCAdjust = 1
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && arm64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
// Regs holds the full general-purpose register set of a traced process
|
||||||
|
// (the FreeBSD arm64 struct reg layout, sys/arm64/include/reg.h: x[30], lr,
|
||||||
|
// sp, elr, spsr).
|
||||||
|
type Regs struct {
|
||||||
|
X0 uint64
|
||||||
|
X1 uint64
|
||||||
|
X2 uint64
|
||||||
|
X3 uint64
|
||||||
|
X4 uint64
|
||||||
|
X5 uint64
|
||||||
|
X6 uint64
|
||||||
|
X7 uint64
|
||||||
|
X8 uint64
|
||||||
|
X9 uint64
|
||||||
|
X10 uint64
|
||||||
|
X11 uint64
|
||||||
|
X12 uint64
|
||||||
|
X13 uint64
|
||||||
|
X14 uint64
|
||||||
|
X15 uint64
|
||||||
|
X16 uint64
|
||||||
|
X17 uint64
|
||||||
|
X18 uint64
|
||||||
|
X19 uint64
|
||||||
|
X20 uint64
|
||||||
|
X21 uint64
|
||||||
|
X22 uint64
|
||||||
|
X23 uint64
|
||||||
|
X24 uint64
|
||||||
|
X25 uint64
|
||||||
|
X26 uint64
|
||||||
|
X27 uint64
|
||||||
|
X28 uint64
|
||||||
|
X29 uint64 // FP (frame pointer)
|
||||||
|
X30 uint64 // LR (link register)
|
||||||
|
SP uint64
|
||||||
|
PC uint64
|
||||||
|
PSTATE uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPC returns the program counter.
|
||||||
|
func (r *Regs) GetPC() uint64 { return r.PC }
|
||||||
|
|
||||||
|
// SetPC sets the program counter.
|
||||||
|
func (r *Regs) SetPC(pc uint64) { r.PC = pc }
|
||||||
|
|
||||||
|
// GetSP returns the stack pointer.
|
||||||
|
func (r *Regs) GetSP() uint64 { return r.SP }
|
||||||
|
|
||||||
|
// RegValue returns the value of the named register, or false if unknown.
|
||||||
|
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||||
|
switch name {
|
||||||
|
case "x0":
|
||||||
|
return r.X0, true
|
||||||
|
case "x1":
|
||||||
|
return r.X1, true
|
||||||
|
case "x2":
|
||||||
|
return r.X2, true
|
||||||
|
case "x3":
|
||||||
|
return r.X3, true
|
||||||
|
case "x4":
|
||||||
|
return r.X4, true
|
||||||
|
case "x5":
|
||||||
|
return r.X5, true
|
||||||
|
case "x6":
|
||||||
|
return r.X6, true
|
||||||
|
case "x7":
|
||||||
|
return r.X7, true
|
||||||
|
case "x8":
|
||||||
|
return r.X8, true
|
||||||
|
case "x9":
|
||||||
|
return r.X9, true
|
||||||
|
case "x10":
|
||||||
|
return r.X10, true
|
||||||
|
case "x11":
|
||||||
|
return r.X11, true
|
||||||
|
case "x12":
|
||||||
|
return r.X12, true
|
||||||
|
case "x13":
|
||||||
|
return r.X13, true
|
||||||
|
case "x14":
|
||||||
|
return r.X14, true
|
||||||
|
case "x15":
|
||||||
|
return r.X15, true
|
||||||
|
case "x16":
|
||||||
|
return r.X16, true
|
||||||
|
case "x17":
|
||||||
|
return r.X17, true
|
||||||
|
case "x18":
|
||||||
|
return r.X18, true
|
||||||
|
case "x19":
|
||||||
|
return r.X19, true
|
||||||
|
case "x20":
|
||||||
|
return r.X20, true
|
||||||
|
case "x21":
|
||||||
|
return r.X21, true
|
||||||
|
case "x22":
|
||||||
|
return r.X22, true
|
||||||
|
case "x23":
|
||||||
|
return r.X23, true
|
||||||
|
case "x24":
|
||||||
|
return r.X24, true
|
||||||
|
case "x25":
|
||||||
|
return r.X25, true
|
||||||
|
case "x26":
|
||||||
|
return r.X26, true
|
||||||
|
case "x27":
|
||||||
|
return r.X27, true
|
||||||
|
case "x28":
|
||||||
|
return r.X28, true
|
||||||
|
case "x29", "fp":
|
||||||
|
return r.X29, true
|
||||||
|
case "x30", "lr":
|
||||||
|
return r.X30, true
|
||||||
|
case "sp":
|
||||||
|
return r.SP, true
|
||||||
|
case "pc":
|
||||||
|
return r.PC, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// breakpointInsn is the software breakpoint instruction (BRK #0).
|
||||||
|
var breakpointInsn = []byte{0x00, 0x00, 0x20, 0xD4} // BRK #0
|
||||||
|
|
||||||
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
|
// a trap: 0. The BRK synchronous exception leaves ELR_EL0 on the BRK
|
||||||
|
// itself (ARM DDI 0487), and the FreeBSD EXCP_BRKPT_EL0 handler delivers
|
||||||
|
// the frame's elr unmodified (sys/arm64/arm64/trap.c), so the trap address
|
||||||
|
// is the PC as reported.
|
||||||
|
const breakpointPCAdjust = 0
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && riscv64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
// Regs holds the full general-purpose register set of a traced process
|
||||||
|
// (the FreeBSD riscv64 struct reg layout: ra, sp, gp, tp, t0-t6, s0-s11,
|
||||||
|
// a0-a7, sepc, sstatus).
|
||||||
|
type Regs struct {
|
||||||
|
PC uint64 // sepc
|
||||||
|
Ra uint64 // x1 (return address)
|
||||||
|
Sp uint64 // x2
|
||||||
|
Gp uint64 // x3
|
||||||
|
Tp uint64 // x4
|
||||||
|
T0 uint64 // x5
|
||||||
|
T1 uint64 // x6
|
||||||
|
T2 uint64 // x7
|
||||||
|
S0 uint64 // x8 (frame pointer)
|
||||||
|
S1 uint64 // x9
|
||||||
|
A0 uint64 // x10
|
||||||
|
A1 uint64 // x11
|
||||||
|
A2 uint64 // x12
|
||||||
|
A3 uint64 // x13
|
||||||
|
A4 uint64 // x14
|
||||||
|
A5 uint64 // x15
|
||||||
|
A6 uint64 // x16
|
||||||
|
A7 uint64 // x17
|
||||||
|
S2 uint64 // x18
|
||||||
|
S3 uint64 // x19
|
||||||
|
S4 uint64 // x20
|
||||||
|
S5 uint64 // x21
|
||||||
|
S6 uint64 // x22
|
||||||
|
S7 uint64 // x23
|
||||||
|
S8 uint64 // x24
|
||||||
|
S9 uint64 // x25
|
||||||
|
S10 uint64 // x26
|
||||||
|
S11 uint64 // x27
|
||||||
|
T3 uint64 // x28
|
||||||
|
T4 uint64 // x29
|
||||||
|
T5 uint64 // x30
|
||||||
|
T6 uint64 // x31
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPC returns the program counter.
|
||||||
|
func (r *Regs) GetPC() uint64 { return r.PC }
|
||||||
|
|
||||||
|
// SetPC sets the program counter.
|
||||||
|
func (r *Regs) SetPC(pc uint64) { r.PC = pc }
|
||||||
|
|
||||||
|
// GetSP returns the stack pointer.
|
||||||
|
func (r *Regs) GetSP() uint64 { return r.Sp }
|
||||||
|
|
||||||
|
// RegValue returns the value of the named register, or false if unknown.
|
||||||
|
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||||
|
switch name {
|
||||||
|
case "pc":
|
||||||
|
return r.PC, true
|
||||||
|
case "ra", "x1":
|
||||||
|
return r.Ra, true
|
||||||
|
case "sp", "x2":
|
||||||
|
return r.Sp, true
|
||||||
|
case "gp", "x3":
|
||||||
|
return r.Gp, true
|
||||||
|
case "tp", "x4":
|
||||||
|
return r.Tp, true
|
||||||
|
case "t0", "x5":
|
||||||
|
return r.T0, true
|
||||||
|
case "t1", "x6":
|
||||||
|
return r.T1, true
|
||||||
|
case "t2", "x7":
|
||||||
|
return r.T2, true
|
||||||
|
case "s0", "fp", "x8":
|
||||||
|
return r.S0, true
|
||||||
|
case "s1", "x9":
|
||||||
|
return r.S1, true
|
||||||
|
case "a0", "x10":
|
||||||
|
return r.A0, true
|
||||||
|
case "a1", "x11":
|
||||||
|
return r.A1, true
|
||||||
|
case "a2", "x12":
|
||||||
|
return r.A2, true
|
||||||
|
case "a3", "x13":
|
||||||
|
return r.A3, true
|
||||||
|
case "a4", "x14":
|
||||||
|
return r.A4, true
|
||||||
|
case "a5", "x15":
|
||||||
|
return r.A5, true
|
||||||
|
case "a6", "x16":
|
||||||
|
return r.A6, true
|
||||||
|
case "a7", "x17":
|
||||||
|
return r.A7, true
|
||||||
|
case "s2", "x18":
|
||||||
|
return r.S2, true
|
||||||
|
case "s3", "x19":
|
||||||
|
return r.S3, true
|
||||||
|
case "s4", "x20":
|
||||||
|
return r.S4, true
|
||||||
|
case "s5", "x21":
|
||||||
|
return r.S5, true
|
||||||
|
case "s6", "x22":
|
||||||
|
return r.S6, true
|
||||||
|
case "s7", "x23":
|
||||||
|
return r.S7, true
|
||||||
|
case "s8", "x24":
|
||||||
|
return r.S8, true
|
||||||
|
case "s9", "x25":
|
||||||
|
return r.S9, true
|
||||||
|
case "s10", "x26":
|
||||||
|
return r.S10, true
|
||||||
|
case "s11", "x27":
|
||||||
|
return r.S11, true
|
||||||
|
case "t3", "x28":
|
||||||
|
return r.T3, true
|
||||||
|
case "t4", "x29":
|
||||||
|
return r.T4, true
|
||||||
|
case "t5", "x30":
|
||||||
|
return r.T5, true
|
||||||
|
case "t6", "x31":
|
||||||
|
return r.T6, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// breakpointInsn is the software breakpoint instruction (EBREAK).
|
||||||
|
var breakpointInsn = []byte{0x73, 0x00, 0x10, 0x00} // ebreak
|
||||||
|
|
||||||
|
// breakpointPCAdjust is how far PC is past the breakpoint instruction after
|
||||||
|
// a trap: 0. The EBREAK synchronous exception leaves sepc on the ebreak
|
||||||
|
// itself (RISC-V privileged architecture), so the trap address is the PC as
|
||||||
|
// reported.
|
||||||
|
const breakpointPCAdjust = 0
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FreeBSD TRAP_* si_code values (sys/signal.h). A breakpoint (INT3, BRK,
|
||||||
|
// EBREAK) arrives as TRAP_BRKPT on every supported architecture; TRAP_TRACE
|
||||||
|
// is shared by the completed single-step and the hardware watchpoint hit,
|
||||||
|
// so the watchpoint layer disambiguates from the debug registers.
|
||||||
|
const (
|
||||||
|
trapBRKPT = 1 // TRAP_BRKPT
|
||||||
|
trapTRACE = 2 // TRAP_TRACE
|
||||||
|
)
|
||||||
|
|
||||||
|
// StopReason describes why the debuggee stopped.
|
||||||
|
type StopReason int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StopNone StopReason = iota
|
||||||
|
StopBreakpoint // software breakpoint hit
|
||||||
|
StopWatchpoint // hardware watchpoint triggered
|
||||||
|
StopSingleStep // single-step completed
|
||||||
|
StopSignal // stopped by a signal
|
||||||
|
StopExited // process exited
|
||||||
|
)
|
||||||
|
|
||||||
|
// StopInfo returns the reason the debuggee stopped and the faulting address
|
||||||
|
// (for watchpoints, the watched address that was accessed). FreeBSD has no
|
||||||
|
// PTRACE_GETSIGINFO; the stop's signal information comes from PT_LWPINFO,
|
||||||
|
// whose pl_siginfo carries the siginfo the kernel delivered. A ptrace stop
|
||||||
|
// with no signal behind it (a completed single-step, the initial attach)
|
||||||
|
// fills no siginfo at all.
|
||||||
|
func (s *Session) StopInfo() (StopReason, uint64) {
|
||||||
|
if s.exited {
|
||||||
|
return StopExited, 0
|
||||||
|
}
|
||||||
|
var info unix.PtraceLwpInfoStruct
|
||||||
|
if err := unix.PtraceLwpInfo(s.pid, &info); err != nil {
|
||||||
|
return StopNone, 0
|
||||||
|
}
|
||||||
|
// The siginfo layout is the FreeBSD siginfo_t: three leading ints
|
||||||
|
// (signo, errno, code), then the union, 8-byte aligned, whose _fault
|
||||||
|
// member puts the address at byte offset 16. The read is byte-wise
|
||||||
|
// because the blob's alignment is not guaranteed.
|
||||||
|
si := (*[64]byte)(unsafe.Pointer(&info.Siginfo))
|
||||||
|
signo := int32(binary.LittleEndian.Uint32(si[0:4]))
|
||||||
|
code := int32(binary.LittleEndian.Uint32(si[8:12]))
|
||||||
|
switch {
|
||||||
|
case signo == 0:
|
||||||
|
// A pure ptrace stop: single-step completion, attach, or the
|
||||||
|
// events the kernel resolves internally.
|
||||||
|
return StopSingleStep, 0
|
||||||
|
case signo != int32(syscall.SIGTRAP):
|
||||||
|
return StopSignal, uint64(code)
|
||||||
|
case code == trapBRKPT:
|
||||||
|
return StopBreakpoint, 0
|
||||||
|
case code == trapTRACE:
|
||||||
|
addr := binary.LittleEndian.Uint64(si[16:24])
|
||||||
|
return archStopTrace(s, addr)
|
||||||
|
default:
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && (amd64 || arm64 || riscv64)
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mapRWX maps code into a read-write-execute region.
|
||||||
|
func mapRWX(code []byte) ([]byte, error) {
|
||||||
|
const pageSize = 4096
|
||||||
|
size := (len(code) + pageSize - 1) &^ (pageSize - 1)
|
||||||
|
mem, err := syscall.Mmap(-1, 0, size,
|
||||||
|
syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC,
|
||||||
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copy(mem, code)
|
||||||
|
return mem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupBuffers allocates buffers in the debuggee's memory.
|
||||||
|
func setupBuffers(spec string, args []byte, tmpDir string) ([]byte, error) {
|
||||||
|
type bufSpec struct {
|
||||||
|
name string
|
||||||
|
size int
|
||||||
|
pattern string
|
||||||
|
}
|
||||||
|
var specs []bufSpec
|
||||||
|
for part := range strings.SplitSeq(spec, ",") {
|
||||||
|
fields := strings.SplitN(part, ":", 3)
|
||||||
|
if len(fields) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size, err := strconv.Atoi(fields[1])
|
||||||
|
if err != nil || size <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(specs) == 0 {
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var bufAddrs []uint64
|
||||||
|
for _, s := range specs {
|
||||||
|
buf, err := syscall.Mmap(-1, 0, s.size,
|
||||||
|
syscall.PROT_READ|syscall.PROT_WRITE,
|
||||||
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err)
|
||||||
|
}
|
||||||
|
fillBuffer(buf, s.pattern)
|
||||||
|
bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0]))))
|
||||||
|
}
|
||||||
|
|
||||||
|
addrFile, err := os.Create(tmpDir + "/bufaddrs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, addr := range bufAddrs {
|
||||||
|
fmt.Fprintf(addrFile, "%d\n", addr)
|
||||||
|
}
|
||||||
|
addrFile.Close()
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillBuffer fills a buffer with the specified pattern.
|
||||||
|
func fillBuffer(buf []byte, pattern string) {
|
||||||
|
switch pattern {
|
||||||
|
case "zero":
|
||||||
|
case "ones":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = 0xFF
|
||||||
|
}
|
||||||
|
case "seq":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = byte(i)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = data[i%len(data)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
|
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||||
|
src, err := os.ReadFile(asmPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: %w", err)
|
||||||
|
}
|
||||||
|
file, errs := parser.Parse(asmPath, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||||
|
}
|
||||||
|
img, err := asm.AssembleFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: assemble: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fl *asm.FuncLayout
|
||||||
|
for i := range img.Funcs {
|
||||||
|
if img.Funcs[i].Name == funcName {
|
||||||
|
fl = &img.Funcs[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fl == nil {
|
||||||
|
return fmt.Errorf("debug target: function %q not found", funcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := img.Bytes()
|
||||||
|
exec, err := mapRWX(code)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: mmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
||||||
|
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("debug target: write codebase: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
|
||||||
|
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
||||||
|
|
||||||
|
labelsFile, _ := os.Create(tmpDir + "/labels")
|
||||||
|
if labelsFile != nil {
|
||||||
|
for label, off := range fl.Labels {
|
||||||
|
fmt.Fprintf(labelsFile, "%s %d\n", label, off)
|
||||||
|
}
|
||||||
|
labelsFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := os.ReadFile(argsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: read args: %w", err)
|
||||||
|
}
|
||||||
|
if len(args) < fl.Args {
|
||||||
|
padded := make([]byte, fl.Args)
|
||||||
|
copy(padded, args)
|
||||||
|
args = padded
|
||||||
|
}
|
||||||
|
|
||||||
|
bufSpecFile := tmpDir + "/bufspec"
|
||||||
|
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
||||||
|
args, err = setupBuffers(string(bufSpec), args, tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: setup buffers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
if _, _, errno := unix.RawSyscall(unix.SYS_PTRACE, uintptr(unix.PT_TRACE_ME), 0, 0); errno != 0 {
|
||||||
|
return fmt.Errorf("debug target: PT_TRACE_ME: %v", errno)
|
||||||
|
}
|
||||||
|
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
||||||
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
|
stackArgs := make([]byte, fl.Args)
|
||||||
|
copy(stackArgs, args)
|
||||||
|
|
||||||
|
if _, callErr := verify.Call(fnAddr, stackArgs); callErr != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Success returns to the caller, which exits with status 0; the JIT
|
||||||
|
// code has already run to its own trampoline by the time Call returns.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-sdk/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunTarget is the debuggee entry point (gasm debug --target).
|
// RunTarget is the debuggee entry point (gasm debug --target).
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux || (freebsd && (amd64 || arm64 || riscv64))
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && amd64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7),
|
||||||
|
// read and written as one blob through PT_GETDBREGS/PT_SETDBREGS. The
|
||||||
|
// FreeBSD struct dbreg is the raw DR file: dr[16], where DR0-DR3 are the
|
||||||
|
// address registers, DR6 the status and DR7 the control (sys/x86/include/
|
||||||
|
// reg.h; the DBREG_DRX accessor indexes the same array).
|
||||||
|
|
||||||
|
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||||||
|
type dbreg struct {
|
||||||
|
Dr [16]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbreg indices of the registers the watchpoint layer drives.
|
||||||
|
const (
|
||||||
|
drStatus = 6 // DR6: the trap status register
|
||||||
|
drControl = 7 // DR7: the debug control register
|
||||||
|
)
|
||||||
|
|
||||||
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
|
type WatchpointType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchWrite WatchpointType = 1 // trigger on write
|
||||||
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||||
|
// architecture provides: four address registers, DR0-DR3.
|
||||||
|
func maxWatchpoints() int { return 4 }
|
||||||
|
|
||||||
|
// getDbRegs reads the debug register file of the stopped debuggee.
|
||||||
|
func (s *Session) getDbRegs() (*dbreg, error) {
|
||||||
|
var dr dbreg
|
||||||
|
if _, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS_PTRACE,
|
||||||
|
uintptr(unix.PT_GETDBREGS),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&dr)),
|
||||||
|
0, 0,
|
||||||
|
); errno != 0 {
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
return &dr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDbRegs writes the debug register file of the stopped debuggee.
|
||||||
|
func (s *Session) setDbRegs(dr *dbreg) error {
|
||||||
|
if _, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS_PTRACE,
|
||||||
|
uintptr(unix.PT_SETDBREGS),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(dr)),
|
||||||
|
0, 0,
|
||||||
|
); errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archStopTrace classifies a TRAP_TRACE stop. On amd64 the kernel
|
||||||
|
// delivers both the completed single-step and the debug-register hit
|
||||||
|
// through T_TRCTRAP with TRAP_TRACE (sys/amd64/amd64/trap.c), and DR6's
|
||||||
|
// B0-B3 bits name the watchpoint that fired.
|
||||||
|
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
if status := dr.Dr[drStatus]; status&0xF != 0 {
|
||||||
|
for slot := range 4 {
|
||||||
|
if status&(1<<slot) != 0 && dr.Dr[slot] != 0 {
|
||||||
|
return StopWatchpoint, dr.Dr[slot]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||||||
|
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||||||
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
|
for i := range 4 {
|
||||||
|
if !s.wpSlots[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWatchpointSlotUsed reports whether slot (0-3) currently holds a watchpoint.
|
||||||
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
|
if slot < 0 || slot > 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.wpSlots[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
|
// DR7's encoding is architectural: a 2-bit local/global enable pair per
|
||||||
|
// slot at bit 2*slot, the R/W field at 16+4*slot and the length field at
|
||||||
|
// 18+4*slot (Intel SDM vol 3, "Debug Registers").
|
||||||
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
|
if slot < 0 || slot > 3 {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
|
}
|
||||||
|
if s.wpSlots[slot] {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lenBits uint64
|
||||||
|
switch size {
|
||||||
|
case 1:
|
||||||
|
lenBits = 0
|
||||||
|
case 2:
|
||||||
|
lenBits = 1
|
||||||
|
case 4:
|
||||||
|
lenBits = 3
|
||||||
|
case 8:
|
||||||
|
lenBits = 2
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
|
}
|
||||||
|
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug: read debug registers: %w", err)
|
||||||
|
}
|
||||||
|
dr.Dr[slot] = addr
|
||||||
|
|
||||||
|
dr7 := dr.Dr[drControl]
|
||||||
|
enableBit := uint64(1) << (2 * slot)
|
||||||
|
rwBits := uint64(typ) << (16 + 4*slot)
|
||||||
|
lenField := lenBits << (18 + 4*slot)
|
||||||
|
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||||
|
dr.Dr[drControl] = (dr7 & mask) | enableBit | rwBits | lenField
|
||||||
|
|
||||||
|
if err := s.setDbRegs(dr); err != nil {
|
||||||
|
return fmt.Errorf("debug: set debug registers: %w", err)
|
||||||
|
}
|
||||||
|
s.wpSlots[slot] = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearWatchpoint removes a hardware watchpoint.
|
||||||
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
|
if slot < 0 || slot > 3 {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
|
}
|
||||||
|
if !s.wpSlots[slot] {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
|
}
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dr.Dr[slot] = 0
|
||||||
|
dr.Dr[drControl] &^= uint64(1) << (2 * slot)
|
||||||
|
if err := s.setDbRegs(dr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.wpSlots[slot] = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
|
for slot := range maxWatchpoints() {
|
||||||
|
if s.wpSlots[slot] {
|
||||||
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && arm64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hardware watchpoint support via arm64 debug registers, read and written
|
||||||
|
// as one blob through PT_GETDBREGS/PT_SETDBREGS. The FreeBSD struct dbreg
|
||||||
|
// (sys/arm64/include/reg.h) opens with the debug-facility header and then
|
||||||
|
// carries 16 breakpoint and 16 watchpoint pairs of {address, control}.
|
||||||
|
|
||||||
|
// dbreg mirrors FreeBSD's struct dbreg for PT_GETDBREGS/PT_SETDBREGS.
|
||||||
|
type dbreg struct {
|
||||||
|
DbDebugVer uint8
|
||||||
|
DbNbkpts uint8
|
||||||
|
DbNwtpts uint8
|
||||||
|
_ [5]byte
|
||||||
|
DbBreakregs [16]struct {
|
||||||
|
Addr uint64
|
||||||
|
Ctrl uint32
|
||||||
|
_ uint32
|
||||||
|
}
|
||||||
|
DbWatchregs [16]struct {
|
||||||
|
Addr uint64
|
||||||
|
Ctrl uint32
|
||||||
|
_ uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
|
type WatchpointType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchWrite WatchpointType = 1 // trigger on write
|
||||||
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||||
|
// architecture provides: DBGWVR0-DBGWCR15.
|
||||||
|
func maxWatchpoints() int { return 16 }
|
||||||
|
|
||||||
|
// getDbRegs reads the debug register file of the stopped debuggee.
|
||||||
|
func (s *Session) getDbRegs() (*dbreg, error) {
|
||||||
|
var dr dbreg
|
||||||
|
if _, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS_PTRACE,
|
||||||
|
uintptr(unix.PT_GETDBREGS),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&dr)),
|
||||||
|
0, 0,
|
||||||
|
); errno != 0 {
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
return &dr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDbRegs writes the debug register file of the stopped debuggee.
|
||||||
|
func (s *Session) setDbRegs(dr *dbreg) error {
|
||||||
|
if _, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS_PTRACE,
|
||||||
|
uintptr(unix.PT_SETDBREGS),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(dr)),
|
||||||
|
0, 0,
|
||||||
|
); errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archStopTrace classifies a TRAP_TRACE stop. On arm64 the kernel
|
||||||
|
// delivers both the software single step and the watchpoint hit through
|
||||||
|
// EXCP_SOFTSTP_EL0/EXCP_WATCHPT_EL0 with TRAP_TRACE (sys/arm64/arm64/
|
||||||
|
// trap.c); the watchpoint address rides the FAR register, so a stop whose
|
||||||
|
// reported address falls inside an armed watchpoint's byte range is a
|
||||||
|
// watchpoint and everything else is a single step.
|
||||||
|
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
for slot := range 16 {
|
||||||
|
ctrl := uint64(dr.DbWatchregs[slot].Ctrl)
|
||||||
|
if ctrl&1 == 0 || dr.DbWatchregs[slot].Addr == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if bas := (ctrl >> 5) & 0xFF; bas != 0 && siAddr >= dr.DbWatchregs[slot].Addr && siAddr < dr.DbWatchregs[slot].Addr+8 {
|
||||||
|
return StopWatchpoint, siAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint
|
||||||
|
// slot, or -1 if all of them are in use.
|
||||||
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
|
for i := range maxWatchpoints() {
|
||||||
|
if !s.wpSlots[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWatchpointSlotUsed reports whether slot currently holds a watchpoint.
|
||||||
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
||||||
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.wpSlots[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWatchpoint installs a hardware watchpoint on the given address. The
|
||||||
|
// control word is the architectural DBGWCR (ARM DDI 0487): bit 0 enables,
|
||||||
|
// bits 3-4 select the access type (10 store, 11 load+store) and bits 5-12
|
||||||
|
// are the byte-address select, so the watch stays 8-byte aligned and names
|
||||||
|
// its watched bytes through BAS.
|
||||||
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
|
}
|
||||||
|
if s.wpSlots[slot] {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
|
}
|
||||||
|
var bas uint64
|
||||||
|
switch size {
|
||||||
|
case 1:
|
||||||
|
bas = 0x01
|
||||||
|
case 2:
|
||||||
|
bas = 0x03
|
||||||
|
case 4:
|
||||||
|
bas = 0x0F
|
||||||
|
case 8:
|
||||||
|
bas = 0xFF
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
|
}
|
||||||
|
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug: read debug registers: %w", err)
|
||||||
|
}
|
||||||
|
if uint8(slot) >= dr.DbNwtpts && dr.DbNwtpts != 0 {
|
||||||
|
return fmt.Errorf("debug: slot %d exceeds available watchpoints (%d)", slot, dr.DbNwtpts)
|
||||||
|
}
|
||||||
|
ctrl := uint64(1) // enable
|
||||||
|
switch typ {
|
||||||
|
case WatchWrite:
|
||||||
|
ctrl |= 2 << 3 // store only
|
||||||
|
case WatchRead:
|
||||||
|
ctrl |= 3 << 3 // load+store
|
||||||
|
}
|
||||||
|
ctrl |= bas << 5
|
||||||
|
dr.DbWatchregs[slot].Addr = addr
|
||||||
|
dr.DbWatchregs[slot].Ctrl = uint32(ctrl)
|
||||||
|
|
||||||
|
if err := s.setDbRegs(dr); err != nil {
|
||||||
|
return fmt.Errorf("debug: set debug registers: %w", err)
|
||||||
|
}
|
||||||
|
s.wpSlots[slot] = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearWatchpoint removes a hardware watchpoint.
|
||||||
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
|
if slot < 0 || slot >= maxWatchpoints() {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints()-1)
|
||||||
|
}
|
||||||
|
if !s.wpSlots[slot] {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
|
}
|
||||||
|
dr, err := s.getDbRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dr.DbWatchregs[slot].Addr = 0
|
||||||
|
dr.DbWatchregs[slot].Ctrl = 0
|
||||||
|
if err := s.setDbRegs(dr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.wpSlots[slot] = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
|
for slot := range maxWatchpoints() {
|
||||||
|
if s.wpSlots[slot] {
|
||||||
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build freebsd && riscv64
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// The architecture has hardware watchpoint triggers, but FreeBSD exposes
|
||||||
|
// no PT_GETDBREGS request for riscv64, so there is no supported way to arm
|
||||||
|
// one: the watchpoint layer is honestly empty here.
|
||||||
|
|
||||||
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
|
type WatchpointType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchWrite WatchpointType = 1 // trigger on write
|
||||||
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxWatchpoints reports the number of hardware watchpoint slots the
|
||||||
|
// platform provides: FreeBSD exposes none for riscv64.
|
||||||
|
func maxWatchpoints() int { return 0 }
|
||||||
|
|
||||||
|
// archStopTrace classifies a TRAP_TRACE stop; with no watchpoint layer a
|
||||||
|
// trace stop is always a completed single step.
|
||||||
|
func archStopTrace(s *Session, siAddr uint64) (StopReason, uint64) {
|
||||||
|
return StopSingleStep, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindFreeWatchpointSlot returns -1: no slots exist.
|
||||||
|
func (s *Session) FindFreeWatchpointSlot() int { return -1 }
|
||||||
|
|
||||||
|
// IsWatchpointSlotUsed reports whether slot currently holds a watchpoint.
|
||||||
|
func (s *Session) IsWatchpointSlotUsed(slot int) bool { return false }
|
||||||
|
|
||||||
|
// SetWatchpoint is unsupported: FreeBSD exposes no debug register request
|
||||||
|
// for riscv64.
|
||||||
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
|
return fmt.Errorf("debug: hardware watchpoints are not supported on freebsd/riscv64")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearWatchpoint is unsupported for the same reason.
|
||||||
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
|
return fmt.Errorf("debug: hardware watchpoints are not supported on freebsd/riscv64")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllWatchpoints is a no-op: no watchpoint can be armed.
|
||||||
|
func (s *Session) ClearAllWatchpoints() error { return nil }
|
||||||
+1
-1
@@ -15,7 +15,7 @@ import (
|
|||||||
"golang.org/x/arch/riscv64/riscv64asm"
|
"golang.org/x/arch/riscv64/riscv64asm"
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Instruction is one decoded instruction: its text form, its length in bytes
|
// Instruction is one decoded instruction: its text form, its length in bytes
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecodeKnownBytes(t *testing.T) {
|
func TestDecodeKnownBytes(t *testing.T) {
|
||||||
|
|||||||
+30
-7
@@ -1,8 +1,8 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
How gasm-devkit is put together and why.
|
How gasm-sdk is put together and why.
|
||||||
|
|
||||||
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
Repository: [sourcedock.dev/petrbalvin/gasm-sdk](https://sourcedock.dev/petrbalvin/gasm-sdk)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -119,6 +119,20 @@ identifier is a register or a label is an *architecture* question, so it is
|
|||||||
left to `arch` and resolved in the lint/lsp layers. This keeps the parser
|
left to `arch` and resolved in the lint/lsp layers. This keeps the parser
|
||||||
arch-agnostic and its output deterministic.
|
arch-agnostic and its output deterministic.
|
||||||
|
|
||||||
|
### Optional preprocessing
|
||||||
|
|
||||||
|
With `Options{Expand: true}` the parser runs a pre-parse pass
|
||||||
|
(`preproc.go`) that splices `#include` files (the source directory, then the
|
||||||
|
`-I` directories), expands object and parameterised `#define` macros,
|
||||||
|
applies `#undef` and the `#ifdef`/`#ifndef`/`#else`/`#endif` family, and
|
||||||
|
folds constant expressions left in operands. The go command's platform
|
||||||
|
macros (`GOARCH_<arch>`, `GOOS_<goos>`) arrive through `Options.Predefines`.
|
||||||
|
The assembly path (`asm`, `diff`, `audit`) expands; `lint`, `fmt` and the
|
||||||
|
language server read the raw file. The command layer adds the go_asm.h
|
||||||
|
generator (`asmhdr.go`): a file that includes go_asm.h gets the package's
|
||||||
|
defines type-checked out of its Go files for the target architecture and
|
||||||
|
GOOS, with no compiler in the loop.
|
||||||
|
|
||||||
### `arch`
|
### `arch`
|
||||||
|
|
||||||
Register files are generated programmatically (the regular `R8`-`R15`,
|
Register files are generated programmatically (the regular `R8`-`R15`,
|
||||||
@@ -232,11 +246,18 @@ document store, republishes diagnostics on every change, and provides:
|
|||||||
pseudo-registers, labels, immediates, comments, directives, textflag macros);
|
pseudo-registers, labels, immediates, comments, directives, textflag macros);
|
||||||
- **navigation**: go-to-definition from a label reference to its definition,
|
- **navigation**: go-to-definition from a label reference to its definition,
|
||||||
find references, document highlights of every use of the symbol under the
|
find references, document highlights of every use of the symbol under the
|
||||||
cursor, rename, and workspace symbol search over the open documents;
|
cursor, rename, and workspace symbol search over the open documents and
|
||||||
|
the indexed workspace files: the `.s` files under the workspace root that
|
||||||
|
the editor has never opened, where an open buffer shadows its disk copy
|
||||||
|
and watched-file events plus a per-query freshness check keep the index
|
||||||
|
current;
|
||||||
- **assists**: document formatting through the `format` package, inlay hints
|
- **assists**: document formatting through the `format` package, inlay hints
|
||||||
(the frame size after the TEXT argument area), signature help (the callee's
|
(the frame size after the TEXT argument area), signature help (the callee's
|
||||||
`// func` signature while the cursor is on a `CALL`), and code actions
|
`// func` signature while the cursor is on a `CALL`), and code actions
|
||||||
offering quick fixes for the `missing-ret` and `unused-label` diagnostics.
|
offering quick fixes for the `missing-ret` and `unused-label` diagnostics,
|
||||||
|
the `missing-textflag-include` warning (the include after the last one in
|
||||||
|
the file) and the `abi-argsize` warning (the argument area set to the size
|
||||||
|
the `// func` signature implies).
|
||||||
- **document information**: pull diagnostics (`textDocument/diagnostic`),
|
- **document information**: pull diagnostics (`textDocument/diagnostic`),
|
||||||
#include document links (resolved against the document directory, then
|
#include document links (resolved against the document directory, then
|
||||||
`$GOROOT/pkg/include`) and folding ranges (one collapsible region per
|
`$GOROOT/pkg/include`) and folding ranges (one collapsible region per
|
||||||
@@ -558,7 +579,9 @@ AST, so neither depends on an encoding.
|
|||||||
--ground-truth`, `go list -json -export` locates the archives of the packages
|
--ground-truth`, `go list -json -export` locates the archives of the packages
|
||||||
a GOOBJ object references, and `_gen` parses
|
a GOOBJ object references, and `_gen` parses
|
||||||
`$GOROOT/src/cmd/internal/obj/<arch>/anames.go` to rebuild the tables.
|
`$GOROOT/src/cmd/internal/obj/<arch>/anames.go` to rebuild the tables.
|
||||||
- **Linux process interfaces** for the dynamic work: `mmap` and `mprotect` for
|
- **Linux and FreeBSD process interfaces** for the dynamic work: `mmap` and
|
||||||
the JIT mapping, ptrace with `/proc/pid/mem` for the debugger. That is why
|
`mprotect` for the JIT mapping, ptrace for the debugger — with tracee
|
||||||
|
memory through `/proc/pid/mem` on Linux and through `PT_IO` on FreeBSD,
|
||||||
|
and the tracee's stop reports read from `PT_LWPINFO` there. That is why
|
||||||
`verify` runs a JIT check only when the host architecture matches the
|
`verify` runs a JIT check only when the host architecture matches the
|
||||||
kernel's, and why `debug` is Linux-only.
|
kernel's, and why `debug` is bounded to those two kernels.
|
||||||
|
|||||||
+13
-4
@@ -287,7 +287,8 @@ Usage: gasm debug <file.s> --func <name>
|
|||||||
The debugger re-executes the binary it is running as (`os.Executable()`) for the
|
The debugger re-executes the binary it is running as (`os.Executable()`) for the
|
||||||
traced child, so the child is the same `gasm`, whether it is installed on `$PATH`
|
traced child, so the child is the same `gasm`, whether it is installed on `$PATH`
|
||||||
or run with `go run ./cmd/gasm`; nothing has to be installed first. Requires
|
or run with `go run ./cmd/gasm`; nothing has to be installed first. Requires
|
||||||
Linux (ptrace), and all four architectures are supported.
|
Linux or FreeBSD (ptrace): all four architectures on Linux, amd64, arm64 and
|
||||||
|
riscv64 on FreeBSD.
|
||||||
|
|
||||||
REPL commands:
|
REPL commands:
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ add: 16 bytes, args=24, frame=0 NOSPLIT
|
|||||||
## audit-instructions
|
## audit-instructions
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Usage: gasm audit-instructions [--corpus [dir]] [-I dir] [amd64|arm64|riscv64|loong64]
|
Usage: gasm audit-instructions [--corpus [dir]] [--list] [-I 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
|
||||||
@@ -403,7 +404,9 @@ architecture; a file without one is attempted for all four, exactly as a
|
|||||||
The report gives the headline number (files
|
The report gives the headline number (files
|
||||||
that assemble for every target architecture), the per-architecture pass rates
|
that assemble for every target architecture), the per-architecture pass rates
|
||||||
and the most common failure reasons with one representative file each, which
|
and the most common failure reasons with one representative file each, which
|
||||||
drive the encodability backlog by frequency rather than by table order. A run
|
drive the encodability backlog by frequency rather than by table order. With
|
||||||
|
`--list` the report additionally prints every failing file with its failure
|
||||||
|
reason, per architecture. A run
|
||||||
over GOROOT takes under a second.
|
over GOROOT takes under a second.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -453,7 +456,13 @@ semantic tokens, go-to-definition, find references, rename, document
|
|||||||
formatting, inlay hints, code actions, signature help, document highlights,
|
formatting, inlay hints, code actions, signature help, document highlights,
|
||||||
workspace symbol search, #include document links, and folding ranges for
|
workspace symbol search, #include document links, and folding ranges for
|
||||||
function bodies. Definition, references and rename work across every open
|
function bodies. Definition, references and rename work across every open
|
||||||
document.
|
document and the wider workspace on disk: the server indexes the `.s` files
|
||||||
|
under the workspace root that the editor has never opened, an open buffer
|
||||||
|
always shadows its disk copy, and watched-file events together with a
|
||||||
|
per-query freshness check keep the index current. The quick fixes add the
|
||||||
|
missing `#include "textflag.h"`, set the TEXT argument area to the size the
|
||||||
|
`// func` signature implies, add a missing `RET`, and remove an unused
|
||||||
|
label.
|
||||||
|
|
||||||
## version
|
## version
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Development Guide
|
# Development Guide
|
||||||
|
|
||||||
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
Repository: [sourcedock.dev/petrbalvin/gasm-sdk](https://sourcedock.dev/petrbalvin/gasm-sdk)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git
|
||||||
cd gasm-devkit
|
cd gasm-sdk
|
||||||
just build # compile bin/gasm, zero errors and zero warnings
|
just build # compile bin/gasm, zero errors and zero warnings
|
||||||
just gates # build, fmt-check, vet, test, race: the definition of done
|
just gates # build, fmt-check, vet, test, race: the definition of done
|
||||||
```
|
```
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ internal interface with no stability promise, and it can change in any
|
|||||||
release. This specification was therefore produced by reverse engineering
|
release. This specification was therefore produced by reverse engineering
|
||||||
that source and by parsing real objects produced by `go tool asm` and
|
that source and by parsing real objects produced by `go tool asm` and
|
||||||
`go tool compile`, byte for byte, against the layout described here. Within
|
`go tool compile`, byte for byte, against the layout described here. Within
|
||||||
gasm-devkit it is kept honest by the differential tests in `asm/goobj_test.go`
|
gasm-sdk it is kept honest by the differential tests in `asm/goobj_test.go`
|
||||||
and `asm/link_test.go`, which compare `gasm asm --format goobj` output against
|
and `asm/link_test.go`, which compare `gasm asm --format goobj` output against
|
||||||
the toolchain's own products and feed gasm objects to `go build`.
|
the toolchain's own products and feed gasm objects to `go build`.
|
||||||
|
|
||||||
@@ -615,7 +615,7 @@ objects, in one place:
|
|||||||
8. Compute the content hashes exactly as the toolchain does, or emit no
|
8. Compute the content hashes exactly as the toolchain does, or emit no
|
||||||
hashed definitions at all.
|
hashed definitions at all.
|
||||||
|
|
||||||
## How gasm-devkit implements and verifies it
|
## How gasm-sdk implements and verifies it
|
||||||
|
|
||||||
The writer lives in `asm/goobj.go`, which carries the shared container and the
|
The writer lives in `asm/goobj.go`, which carries the shared container and the
|
||||||
amd64 relocation emission, with per-architecture relocation emitters in
|
amd64 relocation emission, with per-architecture relocation emitters in
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# AMD64
|
||||||
|
|
||||||
|
Layer 1, target page. Verified against `go tool asm` of Go 1.27.1 and against
|
||||||
|
gasm's encoder, whose output is compared byte for byte with the toolchain's
|
||||||
|
and executed on real hardware (`gasm verify`). The complete mnemonic
|
||||||
|
inventory lives in the generated appendix
|
||||||
|
[INSTRUCTIONS-AMD64.md](INSTRUCTIONS-AMD64.md); this page is the grammar and
|
||||||
|
the conventions.
|
||||||
|
|
||||||
|
## Registers
|
||||||
|
|
||||||
|
| Group | Names | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| General purpose, 64-bit | `AX` `BX` `CX` `DX` `SI` `DI` `BP` `SP` `R8` to `R15` | bare names, no prefix |
|
||||||
|
| Sub-registers | `AL` `CL` `DL` `BL` `AH` family; `R8B` `R8W` `R8D` for the byte, word and double word of `R8` | width rides the mnemonic as well |
|
||||||
|
| Vector | `X0` to `X15` (128-bit), `Y0` to `Y15` (256-bit), `Z0` to `Z31` (512-bit) | SSE, AVX and AVX-512 |
|
||||||
|
| Mask | `K0` to `K7` | AVX-512 opmask |
|
||||||
|
| System | `TLS` | the thread pointer, see below |
|
||||||
|
|
||||||
|
Roles the calling convention fixes, which assembly must respect and can rely
|
||||||
|
on:
|
||||||
|
|
||||||
|
- `SP` is the hardware stack pointer; the virtual frame pointer of the
|
||||||
|
common language is the pseudo-register SP of OPERANDS.md, a different
|
||||||
|
spelling with a different meaning.
|
||||||
|
- `BP` is callee-save. The assembler inserts the save and restore whenever
|
||||||
|
the function has a non-zero frame, so using BP as a general register
|
||||||
|
interferes with sampling profilers that walk the frame chain.
|
||||||
|
- `R14` holds `g`, the goroutine pointer, in the register ABI; `RDX` holds
|
||||||
|
the closure context; `R12` and `R13` are the register ABI's scratch pair
|
||||||
|
and `R15` its GOT temporary; `X15` is the zeroing register the compiler
|
||||||
|
uses. An ABI0 assembly function called from Go sees none of these live
|
||||||
|
across the call, but runtime assembly reads them directly.
|
||||||
|
- The legacy spellings for the goroutine pointer are the macros of
|
||||||
|
`runtime/go_tls.h`: `get_tls(r)` expands to `MOVQ TLS, r` and `g(r)` to
|
||||||
|
`0(r)(TLS*1)`, the segment base riding the index field.
|
||||||
|
|
||||||
|
## Addressing
|
||||||
|
|
||||||
|
The common forms of OPERANDS.md, with the amd64 specifics:
|
||||||
|
|
||||||
|
```text
|
||||||
|
offset(base) MOVQ 16(BX), AX
|
||||||
|
offset(base)(index*scale) MOVL foo+32(SP)(R9*8), CX
|
||||||
|
scale is 1, 2, 4 or 8
|
||||||
|
name±offset(SB) MOVQ ·table(SB), CX
|
||||||
|
```
|
||||||
|
|
||||||
|
- Global references assemble as absolute addresses and produce R_ADDR
|
||||||
|
relocations; branch targets produce R_PCREL.
|
||||||
|
- Vector indexed memory, the VSIB form with an X, Y or Z register in the
|
||||||
|
index position, exists for the gather and scatter families.
|
||||||
|
- There are no segment overrides in source; the one segment-flavoured form
|
||||||
|
is the TLS base in the index field shown above.
|
||||||
|
|
||||||
|
## The frame and the split check
|
||||||
|
|
||||||
|
The assembler manages the frame, not the programmer:
|
||||||
|
|
||||||
|
- It inserts the `BP` save and restore for any non-zero frame.
|
||||||
|
- It inserts the stack-split check for any function that is not NoSplit:
|
||||||
|
the check compares SP against the guard, and on exhaustion calls
|
||||||
|
`runtime.morestack_noctxt`. Frames at or below 128 bytes, StackSmall, use
|
||||||
|
the small compare; frames at or below 4096 bytes, StackBig, use the
|
||||||
|
adjusted form; larger frames compare in two steps.
|
||||||
|
- On amd64 the assembler marks a function NoSplit itself when the frame is
|
||||||
|
under StackSmall and the body calls nothing that needs stack: such a
|
||||||
|
function carries the NoSplit flag in the object without the source ever
|
||||||
|
writing NOSPLIT.
|
||||||
|
|
||||||
|
Results and arguments are stack-only in ABI0: the caller's frame carries
|
||||||
|
them at FP offsets, per the Go prototype.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
The inventory counts 1654 recognised mnemonics today, of which the encoder
|
||||||
|
emits 1113; both numbers are generated in the appendix, and the gap is the
|
||||||
|
encoder backlog that `gasm audit-instructions` measures. The families:
|
||||||
|
|
||||||
|
- **Integer base.** The ALU and move set with width suffixes, `MOVB`,
|
||||||
|
`MOVW`, `MOVL`, `MOVQ`; the extension moves `MOVBLZX`, `MOVWLSX`,
|
||||||
|
`MOVLQSX` and their siblings, which the compiler's output leans on;
|
||||||
|
`LEA`; `PUSH` and `POP`; the shifts and rotates; the bit operations `BT`
|
||||||
|
through `BTC`, `BSF`, `BSR`, `LZCNT`, `TZCNT`, `POPCNT`, `BSWAP`; the
|
||||||
|
string primitives `MOVS` and `STOS`.
|
||||||
|
- **Exchange and atomics.** `XCHG`, `CMPXCHG`, `XADD`; the extended-carry
|
||||||
|
pair `ADCX` and `ADOX`; `CRC32`.
|
||||||
|
- **Scalar floating point.** The SSE2 scalar moves and arithmetic
|
||||||
|
(`MOVSD`, `MOVSS`, `ADDSD`, and the `CVT` family). Floating-point
|
||||||
|
immediates are not encodable on this target, so the assembler
|
||||||
|
materialises them: the constant lands in a synthesised read-only pool,
|
||||||
|
and a positive zero collapses to `XORPS` of the register with itself,
|
||||||
|
exactly as the toolchain does.
|
||||||
|
- **Legacy SIMD, SSE.** The `MOVO`, `MOVOU`, `MOVAPS` family and the packed
|
||||||
|
integer and floating operations, shuffles, lane extracts and inserts and
|
||||||
|
the imm8-controlled forms.
|
||||||
|
- **VEX and EVEX.** The `V`-prefixed forms for 256 and 512-bit work,
|
||||||
|
opmask operations on `K0` to `K7`, gathers and scatters, and the
|
||||||
|
quad-register families 4FMAPS, 4FNMADD, 4VNNIW, VP4DPWSSD and VP4DPWSSDS,
|
||||||
|
whose register list rides the inverted V′VVV field. Mixing VEX and legacy
|
||||||
|
SSE in one loop pays the AVX-SSE transition penalty on every switch: keep
|
||||||
|
a loop in one dialect.
|
||||||
|
- **Cryptographic and counting extensions.** AES-NI, SHA-1 and SHA-256,
|
||||||
|
PCLMULQDQ, GFNI.
|
||||||
|
- **System.** `CPUID`, `RDTSC`, `SYSCALL`, the fences, `LDMXCSR` and
|
||||||
|
`STMXCSR`, the prefetch family.
|
||||||
|
- **Pseudo-operations.** `BYTE`, `WORD`, `LONG`, `QUAD` lay raw bytes or
|
||||||
|
words into the stream for encodings the assembler does not know; `ADJSP`
|
||||||
|
adjusts the stack pointer; `DUFFCOPY` and `DUFFZERO` and `GETCALLERPC`
|
||||||
|
are compiler-side names the table recognises but an encoder need not
|
||||||
|
emit.
|
||||||
|
|
||||||
|
A mnemonic the appendix lists with `gasm encodes: no` assembles nowhere:
|
||||||
|
gasm reports it as an explicit error, never as wrong bytes, and the
|
||||||
|
`unencodable-instruction` lint flags it at edit time.
|
||||||
|
|
||||||
|
## Relocations
|
||||||
|
|
||||||
|
The relocations an amd64 object carries, all specified in
|
||||||
|
[GOOBJ.md](../GOOBJ.md): `R_ADDR` for absolute globals, `R_PCREL` for
|
||||||
|
relative addresses, `R_CALL` for direct calls, `R_TLS_LE` and `R_TLS_IE` for
|
||||||
|
thread local access and `R_GOTPCREL` for GOT relative sequences, plus
|
||||||
|
`R_DWTXTADDR_U4` inside the DWARF records, which the assembler always
|
||||||
|
emits in the four-byte flavour.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# ARM64
|
||||||
|
|
||||||
|
Layer 1, target page. Verified against `go tool asm` of Go 1.27.1, against the
|
||||||
|
toolchain's own arm64 assembler manual (`cmd/internal/obj/arm64/doc.go`) and
|
||||||
|
against gasm's encoder, whose output is compared byte for byte with the
|
||||||
|
toolchain's. The complete mnemonic inventory lives in the generated appendix
|
||||||
|
[INSTRUCTIONS-ARM64.md](INSTRUCTIONS-ARM64.md).
|
||||||
|
|
||||||
|
## Registers
|
||||||
|
|
||||||
|
- General purpose: `R0` to `R30`, plus `ZR`, the zero register, and `RSP`,
|
||||||
|
the stack pointer. There is no R31: thirty-one names and ZR.
|
||||||
|
- Floating-point and SIMD share one file written `Vn`; where an instruction
|
||||||
|
is scalar floating point the operand may be written `Fn` (`F0` to `F31`).
|
||||||
|
- SVE register names (`Z0` to `Z31`, `P0` to `P15`) exist in the assembler's
|
||||||
|
tables.
|
||||||
|
- Roles the convention fixes: `RSP` is the stack pointer, `R29` the frame
|
||||||
|
pointer, `R30` the link register, `R26` the closure context and `R27` the
|
||||||
|
assembler's scratch register. The goroutine pointer lives in `R28` and is
|
||||||
|
written `g` in source, its fields as `g_m(g)`, `g_sched(g)`; `R18` is the
|
||||||
|
platform-reserved register and the Go toolchain never addresses it.
|
||||||
|
|
||||||
|
## Loads, stores and the width suffixes
|
||||||
|
|
||||||
|
The MOV series is the load and store interface, with the width in the
|
||||||
|
mnemonic rather than the register name:
|
||||||
|
|
||||||
|
| Mnemonic | Machine instruction |
|
||||||
|
|---|---|
|
||||||
|
| `MOVD` | ldr, str, stur, 64-bit |
|
||||||
|
| `MOVW` | ldrsw, str, stur, 32-bit sign extending |
|
||||||
|
| `MOVWU` | ldr, 32-bit zero extending |
|
||||||
|
| `MOVH` | ldrsh, strh, sturh |
|
||||||
|
| `MOVHU` | ldrh |
|
||||||
|
| `MOVB` | ldrsb, strb, sturb |
|
||||||
|
| `MOVBU` | ldrb |
|
||||||
|
|
||||||
|
Post-index and pre-index addressing take the `.P` and `.W` suffixes on the
|
||||||
|
mnemonic: `MOVD.P -8(R10), R8` is `ldr x8, [x10],#-8`, and `MOVB.W
|
||||||
|
16(R16), R10` is `ldrsb x10, [x16,#16]!`.
|
||||||
|
|
||||||
|
## Addressing
|
||||||
|
|
||||||
|
```text
|
||||||
|
imm(Rn|RSP) 28(R17)
|
||||||
|
(Rn|RSP) (R22)
|
||||||
|
(Rn)(Rm) (R27)(R23)
|
||||||
|
(Rn)(Rm<<scale) (R4)(R12<<2)
|
||||||
|
(Rn)(Rm.UXTW<<3) extended and shifted index
|
||||||
|
(Rt1, Rt2) register pair for LDP, STP and the exclusive pair forms
|
||||||
|
```
|
||||||
|
|
||||||
|
Branch targets are labels, `(R3)` for indirect, `name(SB)` for static.
|
||||||
|
|
||||||
|
## Operand order and the special forms
|
||||||
|
|
||||||
|
Most instructions appear in left-to-right assignment order: `ADD R11,
|
||||||
|
RSP, R25` computes into R25. The exceptions the toolchain's manual lists,
|
||||||
|
each with its own order:
|
||||||
|
|
||||||
|
- stores and `CBZ`, `CBNZ` keep the GNU order: `MOVD R29, 384(R19)`.
|
||||||
|
- The multiply-accumulate family `MADD`, `MSUB`, `SMADDL` and friends are
|
||||||
|
`<Rm>, <Ra>, <Rn>, <Rd>`.
|
||||||
|
- The scalar FMA family `FMADDD` and friends are `<Fm>, <Fa>, <Fn>, <Fd>`.
|
||||||
|
- The bitfield family `BFI`, `BFXIL`, `SBFIZ`, `SBFX`, `UBFIZ`, `UBFX` is
|
||||||
|
`$<lsb>, <Rn>, $<width>, <Rd>`.
|
||||||
|
- The conditional compare and select families carry the condition as the
|
||||||
|
**first** operand: `CSEL GT, R0, R19, R1`, `CCMP MI, R22, $12, $13`,
|
||||||
|
`FCCMPD AL, F8, F26, $0`.
|
||||||
|
- The exclusive stores are `<Rf>, (<Rn>), <Rs>` with the status register
|
||||||
|
last: `STLXR ZR, (R15), R16`.
|
||||||
|
- `TBZ` and `TBNZ` are `$<imm>, <Rt>, <label>`.
|
||||||
|
|
||||||
|
Shifted and extended register operands ride the register: `R19>>30`,
|
||||||
|
`R26->24` for arithmetic right shift, `@>` for rotate, and the extend forms
|
||||||
|
`R19.UXTB<<4`, `R14.SXTX` with extend operators UXTB, UXTH, UXTW, UXTX,
|
||||||
|
SXTB, SXTH, SXTW, SXTX.
|
||||||
|
|
||||||
|
## Conditions, branches and names
|
||||||
|
|
||||||
|
- Conditions ride the branch mnemonic: `B.EQ`, or the canonical
|
||||||
|
per-condition names such as `BEQ`. Both spellings exist; the canonical
|
||||||
|
names are what the generated inventory lists.
|
||||||
|
- `br` is `JMP` and `blr` is `CALL` in this dialect; indirect branches are
|
||||||
|
`JMP (R3)` and `CALL (R17)`.
|
||||||
|
- `NOP` is a zero-width pseudo-instruction; the hardware nop is `NOOP`,
|
||||||
|
an alias of `HINT $0`.
|
||||||
|
- `umov` is written as `VMOV`.
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
- A 16-bit immediate optionally shifted: `MOVK $(10<<32), R20`, with
|
||||||
|
`MOVZ`, `MOVN` and their W variants; a zero shift is rejected by the
|
||||||
|
assembler.
|
||||||
|
- Large integer constants: `MOV` materialises any 64-bit constant, the
|
||||||
|
closest-instruction way.
|
||||||
|
- Vector constants: `VMOVS`, `VMOVD` and `VMOVQ`, the last taking two
|
||||||
|
64-bit halves for a 128-bit value:
|
||||||
|
`VMOVQ $0x1122334455667788, $0x99aabbccddeeff00, V2`.
|
||||||
|
|
||||||
|
## SIMD
|
||||||
|
|
||||||
|
Floating-point and SIMD instructions mostly carry a `V` prefix
|
||||||
|
(`VADD`, `VFMLA`), the cryptographic extensions (`AESD`, `SHA256H`) and the
|
||||||
|
scalar floating-point instructions being the exceptions. Operands carry an
|
||||||
|
arrangement suffix, `V5.H8`, and structure loads and stores use bracket
|
||||||
|
lists, `[V21.B16]`, with element selection as `V9.S[1]`.
|
||||||
|
|
||||||
|
## Alignment
|
||||||
|
|
||||||
|
`PCALIGN $n` pads to a power-of-two boundary between 8 and 2048 and also
|
||||||
|
raises the function's alignment to the coarsest boundary any of its PCALIGN
|
||||||
|
directives asks for. Functions default to 16-byte alignment on this target.
|
||||||
|
|
||||||
|
## Relocations
|
||||||
|
|
||||||
|
`R_ADDRARM64` for the adrp-plus-add pair, `R_ARM64_PCREL` and the
|
||||||
|
`R_ARM64_PCREL_LDST` family for PC relative addressing, `R_ARM64_LDST` for
|
||||||
|
the load and store immediates, `R_ARM64_GOTPCREL` and `R_ARM64_GOT` for the
|
||||||
|
GOT, `R_ARM64_TLS_LE` and `R_ARM64_TLS_IE` for thread local storage and
|
||||||
|
`R_CALLARM64` for direct calls, all specified in
|
||||||
|
[GOOBJ.md](../GOOBJ.md).
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Directives: TEXT, DATA, GLOBL and the annotations
|
||||||
|
|
||||||
|
Layer 1, the common language, with the flag vocabulary both layers share.
|
||||||
|
Verified against `go tool asm` of Go 1.27.1, against the shipped headers
|
||||||
|
`textflag.h` and `funcdata.h` in `$GOROOT/pkg/include`, and against gasm's
|
||||||
|
parser. Where gasm extends a directive, the extension says so and is marked.
|
||||||
|
|
||||||
|
Six directives exist. Three define things: TEXT, DATA, GLOBL. Three
|
||||||
|
annotate: FUNCDATA, PCDATA, PCALIGN.
|
||||||
|
|
||||||
|
## TEXT
|
||||||
|
|
||||||
|
```text
|
||||||
|
// func Add(a, b int64) int64
|
||||||
|
TEXT ·Add(SB), NOSPLIT, $0-24
|
||||||
|
...instructions...
|
||||||
|
RET
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
TEXT symbol(SB), [flags,] $framesize[-argsize]
|
||||||
|
```
|
||||||
|
|
||||||
|
- The symbol is an `·Name(SB)` reference into the current package, or a
|
||||||
|
fully qualified name.
|
||||||
|
- The optional flag argument is a constant expression, normally an OR of the
|
||||||
|
names from `textflag.h`, the table below. Without `#include "textflag.h"`
|
||||||
|
the names are not macros and the assembler reports the misleading error
|
||||||
|
`illegal or missing addressing mode for symbol NOSPLIT`: include the
|
||||||
|
header first.
|
||||||
|
- `$framesize-argsize` is two constants, not a subtraction: the local frame
|
||||||
|
size in bytes, and the caller's argument area in bytes. The argument size
|
||||||
|
may be omitted entirely, `$16`, which marks the argument size unknown
|
||||||
|
(0x80000000 in the object, the value of `ArgsSizeUnknown` from
|
||||||
|
`funcdata.h`); a frame size may be negative only in the generated ABI
|
||||||
|
wrappers.
|
||||||
|
- A function whose last instruction is not a branch cannot fall through into
|
||||||
|
the next TEXT: the toolchain appends a jump to itself, so end functions
|
||||||
|
with `RET` deliberately.
|
||||||
|
- One TEXT per symbol; redeclaring is an error. The TEXT line also fixes the
|
||||||
|
function's source line for traceback: it is the line number that pcln
|
||||||
|
reports for the function's start.
|
||||||
|
|
||||||
|
The framesize and argsize fields do real work: the framesize drives the
|
||||||
|
stack-split preamble (RUNTIME.md carries the contract), and both travel into
|
||||||
|
the FuncInfo record of the object (GOOBJ.md carries its layout).
|
||||||
|
|
||||||
|
### The flag table
|
||||||
|
|
||||||
|
Values from `textflag.h`, in agreement with `cmd/internal/obj/textflag.go`:
|
||||||
|
|
||||||
|
| Name | Value | Applies to | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| NOPROF | 1 | both | do not profile; deprecated |
|
||||||
|
| DUPOK | 2 | both | the linker may keep one of several duplicates |
|
||||||
|
| NOSPLIT | 4 | TEXT | no stack-split preamble |
|
||||||
|
| RODATA | 8 | data | put the data in a read-only section |
|
||||||
|
| NOPTR | 16 | data | the data contains no pointers |
|
||||||
|
| WRAPPER | 32 | TEXT | a wrapper; must not disable `recover` |
|
||||||
|
| NEEDCTXT | 64 | TEXT | a closure consuming the context register |
|
||||||
|
| TLSBSS | 256 | data | a thread local word in BSS |
|
||||||
|
| NOFRAME | 512 | TEXT | no frame setup; only valid with a frame size of 0 |
|
||||||
|
| REFLECTMETHOD | 1024 | TEXT | the function calls `reflect.Type.Method` or `MethodByName` |
|
||||||
|
| TOPFRAME | 2048 | TEXT | the outermost frame; unwinders stop here |
|
||||||
|
| ABIWRAPPER | 4096 | TEXT | an ABI transition wrapper |
|
||||||
|
|
||||||
|
Rules with teeth:
|
||||||
|
|
||||||
|
- `NOSPLIT` removes the split check, so the frame plus everything the
|
||||||
|
function calls must fit in the stack segment that remains. It exists to
|
||||||
|
protect the splitting code itself; reaching for it to save two instructions
|
||||||
|
is how stack overflows corrupt memory. On amd64 the assembler additionally
|
||||||
|
marks small leaf functions NoSplit itself and omits the check, so the
|
||||||
|
absence of the preamble is not proof the flag was written.
|
||||||
|
- A TEXT whose symbol is declared `ABIInternal` must carry NOSPLIT: the
|
||||||
|
assembler rejects it otherwise, because it cannot generate
|
||||||
|
the split path for a register-ABI function.
|
||||||
|
- `RODATA` implies NOPTR for the garbage collector.
|
||||||
|
|
||||||
|
## DATA
|
||||||
|
|
||||||
|
```text
|
||||||
|
DATA ·table+0(SB)/8, $0x0102030405060708
|
||||||
|
DATA ·msg+0(SB)/14, $"hello, world\n"
|
||||||
|
GLOBL ·msg(SB), RODATA, $14
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
DATA symbol+offset(SB)/width, value
|
||||||
|
```
|
||||||
|
|
||||||
|
- `width` is exactly 1, 2, 4 or 8: the initialiser is written into the data
|
||||||
|
image at `symbol+offset` in that many bytes.
|
||||||
|
- The value is an integer or character constant of the width, or a string
|
||||||
|
literal whose byte length equals the width exactly; escapes count. Long
|
||||||
|
data is written as successive DATA lines at increasing offsets; bytes the
|
||||||
|
directives never name are zero.
|
||||||
|
- Every symbol initialised with DATA ends with a GLOBL line declaring its
|
||||||
|
total size, after all of its DATA lines.
|
||||||
|
|
||||||
|
A symbol containing pointers cannot be defined in assembly, because the
|
||||||
|
collector cannot see into it: define it in Go and refer to it by name. As a
|
||||||
|
rule, data that is not read-only belongs in Go.
|
||||||
|
|
||||||
|
Extension, gasm only: a DATA initialiser may name a symbol,
|
||||||
|
`DATA ·fn+0(SB)/8, $·handler(SB)`, which gasm lays down as an absolute
|
||||||
|
relocation on that field. The toolchain offers no ground truth for this
|
||||||
|
form; gasm's behaviour is verified by linking and execution.
|
||||||
|
|
||||||
|
## GLOBL
|
||||||
|
|
||||||
|
```text
|
||||||
|
GLOBL symbol(SB), [flags,] $size
|
||||||
|
```
|
||||||
|
|
||||||
|
Declares the symbol global with its total size in bytes. The useful flags
|
||||||
|
are RODATA, NOPTR, DUPOK and TLSBSS from the table above. Uninitialised
|
||||||
|
bytes are zero, which makes GLOBL with no DATA the language's BSS.
|
||||||
|
|
||||||
|
## FUNCDATA and PCDATA
|
||||||
|
|
||||||
|
```text
|
||||||
|
FUNCDATA $functypeid, symbol(SB)
|
||||||
|
PCDATA $pctypeid, $value
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiler's annotations for the garbage collector and traceback, named by
|
||||||
|
the ids in `funcdata.h`: FUNCDATA 0 to 7 (args pointer maps, locals pointer
|
||||||
|
maps, stack objects, inline tree, open-coded defer info, argument info,
|
||||||
|
argument liveness, wrap info), PCDATA 0 to 4 (unsafe point, stack map index,
|
||||||
|
inline tree index, argument liveness index, panic bounds). Assembly code
|
||||||
|
normally reaches them only through the macro forms in `funcdata.h`, which
|
||||||
|
RUNTIME.md explains. Outside the macros, hand-written PCDATA is meaningless:
|
||||||
|
the values are pc-value tables the compiler builds from its own view of the
|
||||||
|
program.
|
||||||
|
|
||||||
|
## PCALIGN
|
||||||
|
|
||||||
|
```text
|
||||||
|
PCALIGN $32
|
||||||
|
```
|
||||||
|
|
||||||
|
Pads the code so that the next instruction lands on the given boundary,
|
||||||
|
which must be a power of two and at least the target's instruction
|
||||||
|
alignment. Supported on amd64, arm64, ppc64, loong64 and riscv64. The
|
||||||
|
padding instructions are the target's NOP encoding, so the bytes between
|
||||||
|
functions differ from what the instruction stream alone would produce, which
|
||||||
|
matters to anyone comparing encodings byte for byte.
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user