Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22226d59a5 | ||
|
|
a0ae0e4e37 | ||
|
|
94c4756d47 | ||
|
|
a5a59d6503 | ||
|
|
9cb1666b35 | ||
|
|
96cc70731f | ||
|
|
b4da13d0f6 | ||
|
|
41b387e54d | ||
|
|
4171e412b5 | ||
|
|
57c0ca8b09 | ||
|
|
75bd83fd52 | ||
|
|
62f6fb4faf | ||
|
|
8f84dac10b | ||
|
|
6d7f10f13e | ||
|
|
56f8babbce | ||
|
|
6c1c8d9d96 | ||
|
|
93794c02f7 | ||
|
|
56ad158772 | ||
|
|
15e8b88d32 | ||
|
|
9beff4ae85 | ||
|
|
eacf33d0f7 | ||
|
|
9a34733615 | ||
|
|
970df7c32a | ||
|
|
49572efe16 | ||
|
|
568c553986 | ||
|
|
7a30e902fc | ||
|
|
cb398d9498 | ||
|
|
e44162a749 | ||
|
|
6fb9629ab6 | ||
|
|
8bda4066e3 | ||
|
|
685b150ecf | ||
|
|
c92e6bed3a | ||
|
|
d75e6bcae6 | ||
|
|
6699ebd34f | ||
|
|
ba4d961b20 | ||
|
|
78b12dd427 | ||
|
|
19a26e049b | ||
|
|
163480e283 | ||
|
|
0d62818db7 | ||
|
|
be2ceaafb9 | ||
|
|
941f7fa990 | ||
|
|
eb3c79c3c8 | ||
|
|
eade875b53 | ||
|
|
b08005753e | ||
|
|
5e06d6a6aa | ||
|
|
e4c9d78968 | ||
|
|
f5fcaf9fa6 | ||
|
|
94b98468bb | ||
|
|
746cef8100 | ||
|
|
f153be8158 | ||
|
|
1bf95a169e | ||
|
|
f9eb4021d6 | ||
|
|
c4930438fd | ||
|
|
f372e2db75 | ||
|
|
ac02c83a86 | ||
|
|
ce5ec24fa8 | ||
|
|
181d8e508c | ||
|
|
1160c96427 | ||
|
|
de9e211ff1 | ||
|
|
3acbdd6533 | ||
|
|
ba502c9b79 | ||
|
|
874e054ecb | ||
|
|
f1960febdc | ||
|
|
ae550cc05a | ||
|
|
cc50035375 | ||
|
|
8054fff9ac | ||
|
|
6a79c35bf7 | ||
|
|
6f4f2096e9 | ||
|
|
56630f8624 | ||
|
|
459f4a2b6e | ||
|
|
5d66343488 | ||
|
|
48334c4d5a | ||
|
|
7629963cab | ||
|
|
97951cbeb6 | ||
|
|
6e73f59e78 | ||
|
|
4221ec5741 | ||
|
|
01dcc3b86e | ||
|
|
b08885bd31 | ||
|
|
c05c53452f | ||
|
|
681a449c01 | ||
|
|
31a2cee382 | ||
|
|
3bc7c18bc3 | ||
|
|
f0512a4e1c | ||
|
|
373c09f725 | ||
|
|
b78b6c5004 | ||
|
|
9b5c878f9e | ||
|
|
31ee8e7941 | ||
|
|
2d1176e045 | ||
|
|
2a27a3a52b | ||
|
|
cde7d0f96a | ||
|
|
d7ee1b78d4 |
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
go-version: "1.27"
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
go-version: "1.27"
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
go-version: "1.27"
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.26"
|
||||
go-version: "1.27"
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
+12
-3
@@ -1,3 +1,8 @@
|
||||
# Metadata (always first, per repo convention)
|
||||
.idea/
|
||||
.zcode/
|
||||
.mimocode/
|
||||
|
||||
# Binaries
|
||||
/gasm
|
||||
/bin/
|
||||
@@ -7,9 +12,13 @@
|
||||
coverage.out
|
||||
*.test
|
||||
|
||||
# Editor detritus
|
||||
*.swp
|
||||
.DS_Store
|
||||
# Crash dumps
|
||||
core
|
||||
core.*
|
||||
*.core
|
||||
|
||||
# Scratch / temporary work
|
||||
_scratch/
|
||||
|
||||
# ZCode workspace
|
||||
.zcode
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# AGENTS.md — gasm-devkit
|
||||
|
||||
Repository rules for AI agents and contributors. Read before modifying any
|
||||
code in this repository.
|
||||
|
||||
## AI Contribution Policy
|
||||
|
||||
AI agents may assist with code, documentation, tests, and review in this
|
||||
repository. All AI-assisted changes must:
|
||||
|
||||
- Follow the code style and conventions in this file.
|
||||
- Include the trailer `Assisted-by: <model-name>` in every commit message.
|
||||
- Not commit directly to `main` — work on `development`.
|
||||
- Pass the full Definition of Done before any commit.
|
||||
|
||||
## Workflow
|
||||
|
||||
- **Branching.** `development` is the working branch. `main` is
|
||||
release-only: merge from `development`, then tag. Never commit directly
|
||||
to `main`.
|
||||
- **Release procedure.**
|
||||
1. Bump `version` in `justfile` and `cmd/gasm/main.go`.
|
||||
2. Update `CHANGELOG.md` with a new `## [X.Y.Z] — YYYY-MM-DD` section.
|
||||
3. Update `README.md` and `docs/ARCHITECTURE.md` if user-visible
|
||||
behaviour changed.
|
||||
4. Run the Definition of Done (below).
|
||||
5. Commit on `development`.
|
||||
6. `git checkout main && git merge --ff-only development`.
|
||||
7. `git tag vX.Y.Z`.
|
||||
8. `git checkout development`.
|
||||
9. `GOBIN=~/.local/bin just install-bin`.
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Conventional Commits, subject line only, imperative mood, lowercase after
|
||||
the colon:
|
||||
|
||||
```
|
||||
feat(asm): add EVEX gather and scatter with VSIB addressing
|
||||
```
|
||||
|
||||
Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
|
||||
`chore`, `ci`, `build`, `revert`.
|
||||
|
||||
Every commit ends with exactly one trailer, using the model that
|
||||
assisted with the change:
|
||||
|
||||
```
|
||||
Assisted-by: <model-name>
|
||||
```
|
||||
|
||||
Replace `<model-name>` with the actual model (e.g. `DeepSeek V4 Pro`).
|
||||
|
||||
No body, no footers, no trailing period on the subject.
|
||||
|
||||
## Code Style
|
||||
|
||||
Language: Go 1.26 (`toolchain go1.26.5`).
|
||||
|
||||
### Formatter
|
||||
|
||||
`gofmt` — zero diff. Run `just fmt` before committing.
|
||||
|
||||
### Linter
|
||||
|
||||
`go vet` — zero warnings. Run `just build` before committing.
|
||||
|
||||
### Tests
|
||||
|
||||
`go test -race -count=1 ./...` — all green, coverage ≥ 80 % (hard gate,
|
||||
enforced by `just test`).
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **Production code:** standard library only. No third-party imports in
|
||||
shipped code.
|
||||
- **Test code:** `golang.org/x/arch` is the sole test dependency (decode
|
||||
oracle for round-trip validation). It is never linked into the binary.
|
||||
- **No cgo, no C, no external toolchains, no JavaScript.**
|
||||
|
||||
### Error Handling
|
||||
|
||||
Explicit `if err != nil`. Wrap with `fmt.Errorf("context: %w", err)`.
|
||||
No panics outside `main`. The one exception: the JIT trampoline's
|
||||
`recover`-guarded decoder hot path, which converts bounds panics to
|
||||
sentinel errors.
|
||||
|
||||
### Assembly
|
||||
|
||||
Plan 9 syntax (Go's assembler dialect). Hand-written — no code generators
|
||||
except `_gen/gen.go` for instruction tables (which parses the Go
|
||||
toolchain source). Every instruction table is committed; no runtime
|
||||
dependency on the Go toolchain.
|
||||
|
||||
### File Naming
|
||||
|
||||
- `_amd64.s`, `_arm64.s`, `_riscv64.s`, `_loong64.s` for
|
||||
architecture-specific assembly.
|
||||
- `_linux_amd64.go` for platform-specific Go files.
|
||||
- `_test.go` suffix for test files.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
A task is not complete until all of these pass:
|
||||
|
||||
1. `just build` — `go vet` + `gofmt` check, zero errors, zero warnings.
|
||||
2. `just test` — full suite with `-race`, coverage ≥ 80 %.
|
||||
3. `just fmt` — produces no diff.
|
||||
4. Diagnostics — zero warnings across the project.
|
||||
5. Non-trivial changes reviewed.
|
||||
|
||||
## Licence
|
||||
|
||||
BSD-3-Clause. Every source file carries the SPDX header:
|
||||
|
||||
```
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
```
|
||||
+303
-60
@@ -7,7 +7,301 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
||||
|
||||
## [development]
|
||||
|
||||
Unreleased changes on the `development` branch.
|
||||
### Added
|
||||
|
||||
-
|
||||
|
||||
## [0.32.0] — 2026-08-31
|
||||
|
||||
### Added
|
||||
|
||||
- **Multi-architecture debugger.** `gasm debug` carries
|
||||
per-architecture ptrace register access, disassemblers
|
||||
(`golang.org/x/arch`), register display, FP register views and
|
||||
stop-info handlers for arm64, riscv64 and loong64, and the REPL is
|
||||
arch-neutral. Sessions are runtime-validated on amd64; the other
|
||||
hosts execute through the now-working JIT trampolines, but their
|
||||
ptrace loops have not seen hardware yet.
|
||||
- **Headless debugging.** `gasm debug --script` runs REPL commands from a
|
||||
file (or stdin) and exits; `--timeout` kills the debuggee when a run
|
||||
hangs, with the watchdog armed before the ptrace attach.
|
||||
- **Conditional breakpoints.** `break <label> if <reg> <op> <val>` now also
|
||||
compares two registers (`break loop if RAX > RBX`), not only a register
|
||||
against an immediate.
|
||||
- **Instruction-level coverage.** `gasm debug --cover` now sets a
|
||||
breakpoint on every instruction (walked by disassembly length), counts
|
||||
the hits per instruction and reports the executed instructions with
|
||||
their hit counts, with the label coverage derived from the same run.
|
||||
Expect the run to slow to ptrace speed.
|
||||
- **FP register display on riscv64 and loong64.** The debugger `regs`
|
||||
command shows the 32 FP registers plus fcsr (and fcc on loong64) via
|
||||
`PTRACE_GETREGSET`.
|
||||
- **JIT execution trampolines.** `verify.Call` now works on all four
|
||||
architectures via hand-written assembly trampolines
|
||||
(`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch
|
||||
to a prepared stack, and branch to the JIT function.
|
||||
- **ABI checks on arm64, riscv64 and loong64.** `gasm verify -abi` and
|
||||
the ABI half of `-fuzz` now cover the non-amd64 architectures via
|
||||
per-architecture checked trampolines: sentinels planted in the
|
||||
registers the Go ABI fixes across calls (arm64 `R29`/`R28`, riscv64
|
||||
`X27`, loong64 `R22`; amd64 keeps `BP`/`R14`) are verified on return
|
||||
and the saved registers restored before Go code resumes, with the
|
||||
below-SP canary on every architecture. loong64 kernels are verified
|
||||
through the toolchain-comparison path only, pending hardware
|
||||
validation of their trampoline. `verify.Load` assembles each file
|
||||
with the encoder its name suffix calls for. The `ABIReport` fields
|
||||
are the architecture-neutral `FPClobbered` and `GClobbered`.
|
||||
- **Hardware watchpoints on all architectures.** arm64 uses DBGWVR/DBGWCR
|
||||
via `PTRACE_SETREGSET` with `NT_ARM_HW_BREAK`; riscv64 and loong64 use
|
||||
`PTRACE_POKEUSER` to access trigger/debug registers.
|
||||
- **Cross-package GOOBJ resolution on arm64 and loong64.**
|
||||
`AssembleFileARM64` and `AssembleFileLOONG64` mark external
|
||||
relocations and populate `img.Externals`, so GOOBJ output from those
|
||||
architectures resolves cross-package symbols like amd64 and riscv64
|
||||
already do.
|
||||
- **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or
|
||||
`0x` hex) can now be supplied to a `--call` invocation alongside `--buf`
|
||||
buffers, closing the gap where only buffers could be supplied.
|
||||
- **Fuzz corpus save and replay.** `gasm verify --fuzz --save-corpus dir`
|
||||
records every input that crashes or mismatches as replayable JSON (buffer
|
||||
contents and scalars, not raw pointers), and `gasm verify --replay dir`
|
||||
re-runs the saved entries against the kernel in isolated child processes,
|
||||
reporting whether each one reproduces.
|
||||
- **`gasm audit-instructions`.** Black-box diff of a gasm encoder
|
||||
against the installed `go tool asm`, for amd64, arm64, riscv64 and
|
||||
loong64 (`gasm audit-instructions <arch>`): superset encodings
|
||||
(gasm-only, shippable via `gasm asm --format goobj`),
|
||||
known-but-unencodable names (the backlog) and go-only names
|
||||
(feature gaps).
|
||||
- **`gasm scaffold differential`.** Prints a differential test skeleton
|
||||
for every `// func` signature in a kernel file: random seed states, the
|
||||
kernel call and a portable reference (`<name>Portable`), compared
|
||||
byte-for-byte.
|
||||
- **LSP: find references** (`textDocument/references`).
|
||||
- **LSP: rename symbol** (`textDocument/rename`).
|
||||
- **LSP: document formatting** (`textDocument/formatting`) using the
|
||||
`format` package for canonical gofmt-style output.
|
||||
- **LSP: inlay hints** (`textDocument/inlayHint`): frame size hints after
|
||||
the TEXT directive's argument area.
|
||||
- **LSP: workspace symbol search** (`workspace/symbol`): substring search
|
||||
over the TEXT functions and GLOBL/DATA symbols of every open document.
|
||||
- **LSP: code actions.** Quick fixes for `missing-ret` (insert the RET)
|
||||
and `unused-label` (remove the label) diagnostics.
|
||||
- **LSP: signature help** (`textDocument/signatureHelp`): the callee's
|
||||
`// func` signature while the cursor is on a `CALL`.
|
||||
- **LSP: document highlights**: every reference to the function or label
|
||||
under the cursor is highlighted.
|
||||
- **LSP: pull diagnostics** (`textDocument/diagnostic`), **#include
|
||||
document links** (resolved against the document directory, then
|
||||
`$GOROOT/pkg/include`, so `textflag.h` opens) and **folding ranges**
|
||||
(one collapsible region per TEXT function body).
|
||||
- **Lint: `unused-label` rule.** Flags labels that are defined but never
|
||||
referenced by any jump (Hint severity).
|
||||
- **Lint: `invalid-textflag` rule.** Flags TEXT/GLOBL flags not in the
|
||||
known set from `textflag.h` (Warning severity).
|
||||
- **Lint: `stack-imbalance` rule.** Tracks SP changes and flags if the
|
||||
net delta at RET does not match the declared frame size.
|
||||
- **Lint: `register-width-mismatch` rule.** Flags amd64 operands whose
|
||||
register width does not match the width the mnemonic suffix prescribes
|
||||
(for example a 32-bit register in a `MOVQ`).
|
||||
- **Lint: `abi0-register-args` rule.** Flags kernels whose `// func`
|
||||
parameters are never read from the FP frame (for example arguments read
|
||||
from registers instead), which pass every test today and break on a
|
||||
toolchain upgrade. Shipped with the Go assembler's operand-width model.
|
||||
- **Lint: `nonportable-register-name` rule.** Flags the `RAX`/`EAX`-style
|
||||
register aliases gasm accepts but `go tool asm` rejects, so files using
|
||||
them only link through the gasm GOOBJ path.
|
||||
- **Lint: `unencodable-instruction` rule.** Flags mnemonics the
|
||||
architecture table knows but the encoder cannot yet emit, at edit time
|
||||
instead of at assembly time.
|
||||
- **Lint: `reserved-register-write` rule.** Flags writes to arm64 R18,
|
||||
the platform-reserved register the ABI checks cannot observe at runtime
|
||||
and the Go assembler cannot even spell. Reads and macro-using files
|
||||
are exempt.
|
||||
- **DWARF5 debug sections in ELF output.** All four ELF emitters now emit
|
||||
`.debug_abbrev`, `.debug_info`, `.debug_line`, and `.debug_line_str`
|
||||
sections, enabling `addr2line` and GDB/LLDB source-level debugging, and
|
||||
the amd64 emitter adds a `.debug_frame` CFI section for stack unwinding.
|
||||
- **amd64: legacy SSE and conversion coverage.** The encoder now handles
|
||||
the legacy (non-VEX) SSE packed binaries and immediate shuffles, the
|
||||
legacy SSE integer instructions and `BSWAP`, the scalar and packed
|
||||
double conversions (`CVTSS2SD`, `CVTSD2SS`, `CVTPS2PD`, `CVTPD2PS`) and
|
||||
the prefetch hints.
|
||||
- **amd64: `VPCMP` and the full opmask set.** The EVEX compare with an
|
||||
opmask destination and the remaining opmask-register instructions are
|
||||
encoded, byte for byte against the Go assembler.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Parallel verify sweeps.** The `-smoke` and `-abi` per-function checks
|
||||
now run in parallel instead of sequentially.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Reliable ptrace sessions.** The debugger no longer mistakes runtime
|
||||
signal-delivery-stops (a Go tracee reports SIGURG preemption to the
|
||||
tracer) for its launch barrier, runs every ptrace request on the thread
|
||||
that forked the debuggee (requests from another thread fail with
|
||||
ESRCH), prefers the debuggee's reported code base over an RWX scan, and
|
||||
single-steps over a hit breakpoint so resuming cannot re-trap on the
|
||||
same instruction. A ptrace integration test
|
||||
(`debug/ptrace_integration_test.go`) drives a real session end to end.
|
||||
- **arm64 BL relocation.** `BL sym(SB)` now records a `RelArm64Branch`
|
||||
relocation instead of emitting a bare instruction with no relocation.
|
||||
- **GOOBJ R_ADDRARM64 constant.** Corrected from 9 (`R_CALLARM64`) to 3
|
||||
(`R_ADDRARM64`).
|
||||
- **Subprocess-isolated smoke and ABI sweeps.** A function that faults
|
||||
during the `-smoke` or `-abi` sweep is reported without killing the
|
||||
parent; each sweep runs in a child process.
|
||||
- **`BSF`, `BSR` and `POPCNT` encodings.** Corrected to the bytes
|
||||
`go tool asm` emits.
|
||||
- **`MOVQ` immediates.** Compressed to the toolchain's imm32 forms.
|
||||
- **Frame adjustments of 128 to 255 bytes.** Now use the imm8 `ADDQ`
|
||||
stack adjustment.
|
||||
- **amd64 encoding parity.** A broad pass aligned the remaining encoder
|
||||
outputs and operand strictness with `go tool asm`.
|
||||
- **asm help text.** Updated to list arm64 as a supported architecture.
|
||||
|
||||
## [0.31.1] — 2026-08-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Version stamp.** The v0.31.0 release binary reported itself as `0.30.0`
|
||||
because the version variables in `justfile` and `cmd/gasm/main.go` were not
|
||||
bumped during the release commit.
|
||||
|
||||
## [0.31.0] — 2026-08-20
|
||||
|
||||
The arm64 encoder (Phase 5 — complete) ships with ELF64 and GOOBJ emission,
|
||||
verified byte-for-byte against `GOARCH=arm64 go tool asm` and linked into a
|
||||
real `go build`. The encoder covers the full integer instruction set, FP
|
||||
arithmetic, conditional select, CRC32, and the MOV pseudo-instruction with
|
||||
bitmask immediate encoding. The project now requires Go 1.27.
|
||||
|
||||
### Added
|
||||
|
||||
- **arm64 encoder (Phase 5 — complete).** `gasm asm` can now assemble `_arm64.s`
|
||||
files: the AArch64 integer instruction set with the MOV pseudo-instruction and
|
||||
its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with
|
||||
logical bitmask encoding for values like `$1`), data-processing (shifted
|
||||
register and immediate forms), load/store (scaled unsigned and unscaled9-bit
|
||||
immediate), conditional and unconditional branches, FP/SP frame mapping,
|
||||
SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations),
|
||||
jump chain folding, and ELF64 emission (`gasm asm --format elf`). Ground-truth
|
||||
verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. Phase 5
|
||||
(the other architectures — RISC-V, LoongArch, arm64) is now complete.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Go 1.27 required.** The project now requires Go 1.27 (`toolchain go1.27.0`).
|
||||
The `R_DWTXTADDR_U4` relocation type is detected at runtime for backward
|
||||
compatibility.
|
||||
|
||||
## [0.30.0] — 2026-08-13
|
||||
|
||||
The LoongArch encoder (Phase 5) ships with ELF64 and GOOBJ emission, verified
|
||||
byte-for-byte against `GOARCH=loong64 go tool asm` and linked into a real
|
||||
`go build`; the shared GOOBJ emitter now writes the per-function DWARF symbols
|
||||
the linker's DWARF pass reads. The RISC-V encoder reaches byte-for-byte parity
|
||||
with `go tool asm`: the frame model, operand ordering, RVC compression,
|
||||
large-immediate and `MOV $imm` materialisation, branch/jump encodings, and
|
||||
`CALL sym(SB)` (now a `JAL` with an `R_RISCV_JAL` relocation). The debugger
|
||||
tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
||||
|
||||
### Added
|
||||
|
||||
- **LoongArch encoder (Phase 5).** `gasm asm` can now assemble `_loong64.s`
|
||||
files: the full LoongArch64 instruction set with the dual-form arithmetic
|
||||
mnemonics, the 16/21-bit branch families, the MOV pseudo-instruction and
|
||||
its immediate-constant expansions, FP/SP frame mapping, SB/global symbol
|
||||
references (pcalau12i pairs) and ELF64 emission
|
||||
(`gasm asm --format elf`). Ground-truth verification against
|
||||
`GOARCH=loong64 go tool asm` matches byte-for-byte; GOOBJ emission
|
||||
(`gasm asm --format goobj`) is proven end-to-end by linking the object
|
||||
into a cross-compiled `go build`.
|
||||
- **GOOBJ DWARF symbols.** The GOOBJ emitters now write the per-function
|
||||
DWARF symbols the linker requires (the subprogram DIE and the `.debug_line`
|
||||
program, byte-identical to `cmd/asm`'s), and the pc-value table deltas are
|
||||
in the architecture's MinLC units as the runtime expects — the amd64 link
|
||||
test now genuinely substitutes the gasm object, and the amd64/loong64
|
||||
end-to-end GOOBJ link tests pass.
|
||||
- **RISC-V GOOBJ emission via the shared emitter.** RISC-V GOOBJ output is
|
||||
now written by the same shared emitter as amd64 and LoongArch, modelling
|
||||
each AUIPC + second-instruction pair as a single R_RISCV_PCREL_ITYPE/STYPE
|
||||
relocation (the layout `cmd/asm` writes, not the ELF HI20/LO12 pair), so the
|
||||
object links into a cross-compiled `go build` for `GOARCH=riscv64`. An
|
||||
end-to-end link test substitutes the gasm object and reads the symbol back
|
||||
with `go tool nm`; the rewrite also corrects the relocation `after` field.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **RISC-V frame model and RVC encodings.** The riscv64 frame layout now
|
||||
matches `go tool asm`: the prologue/epilogue save and restore the link
|
||||
register (LR) instead of S0, with the correct autosize (locals + 8) and the
|
||||
RVC-compressed prologue/epilogue instructions; `RET` emits the uncompressed
|
||||
`JALR X0, 0(X1)` the toolchain writes; the `C.ADDI`/`C.LI`/`C.LUI`/`C.ADDIW`
|
||||
opcode bit and the `C.ADD` CR-type encoding are fixed; and the `LR`/`TMP`
|
||||
register aliases now resolve to X1 and X31. The pcsp/pcfile/pcline tables
|
||||
are populated from the recorded stack-adjustment and source-line data, and
|
||||
a byte-exact ground-truth test compares framed and leaf functions against
|
||||
`GOARCH=riscv64 go tool asm`.
|
||||
- **RISC-V operand ordering and RVC compression.** R-type instructions now
|
||||
take `rs2, rs1, rd` and I-type arithmetic instructions take `imm12, rs1,
|
||||
rd`, matching the Go assembler's documented operand order (previously both
|
||||
were reversed, so non-commutative R-type instructions such as `SUB` encoded
|
||||
the wrong operation). The two-operand ternary forms (`ADD rs2, rd`,
|
||||
`ADDI $imm, rd`, `SLLI $shamt, rd`) are now accepted. RVC compression is
|
||||
completed for `C.ADDI16SP`, `C.SLLI`, `C.SRLI`, `C.SRAI`, `C.ANDI`,
|
||||
`C.NOP`, `C.EBREAK`, `C.MV` (from `ADDI`/`ADD`) and the commutative
|
||||
`AND`/`OR`/`XOR` forms; the byte-exact ground-truth test now covers these.
|
||||
- **RISC-V compressed loads/stores and word arithmetic.** RVC compression
|
||||
now also covers the register-relative `C.LW`/`C.SW`/`C.LD`/`C.SD`/
|
||||
`C.FLD`/`C.FSD` forms (in addition to the stack-relative `C.LWSP`/`C.SWSP`/
|
||||
`C.LDSP`/`C.SDSP`), plus `C.ADDI4SPN`, `C.ADDW` and `C.SUBW`. The
|
||||
byte-exact ground-truth test exercises these against `GOARCH=riscv64
|
||||
go tool asm`.
|
||||
- **RISC-V large-immediate materialisation.** `ADDI`/`ANDI`/`ORI`/`XORI`
|
||||
with a 32-bit immediate that does not fit 12 bits now expand exactly as
|
||||
`cmd/asm`: two `ADDI`s for the small `ADDI` split range, and
|
||||
`LUI`+`ADDIW`+`<op>` otherwise, with the `LUI` and `ADDIW` compressed to
|
||||
`C.LUI`/`C.ADDIW` when their immediate fits six signed bits. The
|
||||
byte-exact ground-truth test covers positive, negative, and out-of-range
|
||||
immediates against `GOARCH=riscv64 go tool asm`.
|
||||
- **RISC-V `MOV $imm, rd` materialisation.** The immediate-loading
|
||||
pseudo-instruction now uses the toolchain's `Split32BitImmediate` split
|
||||
(previously it rounded the upper 20 bits, producing wrong results for
|
||||
negative and bit-11-set immediates) and compresses the emitted
|
||||
`ADDI`/`LUI`/`ADDIW` to `C.LI`/`C.LUI`/`C.ADDIW` when their immediate
|
||||
fits six signed bits. A byte-exact ground-truth test covers zero, small,
|
||||
negative, and 32-bit immediates against `GOARCH=riscv64 go tool asm`.
|
||||
- **RISC-V branch/jump compression.** `JMP`/`JAL` were being compressed to
|
||||
`C.J` and `BEQ`/`BNE` (with `X0`) to `C.BEQZ`/`C.BNEZ`, but `go tool asm`
|
||||
never emits these compressed forms. They now emit the 32-bit `JAL` and
|
||||
branch encodings the toolchain writes; the dead `C.J`/`C.BEQZ`/`C.BNEZ`
|
||||
encoders were removed, and the `C.LUI` direct-instruction compression now
|
||||
uses the correct six-bit signed range. A byte-exact ground-truth test
|
||||
covers the branch family and jumps against `GOARCH=riscv64 go tool asm`.
|
||||
- **RISC-V `CALL sym(SB)`.** The call pseudo-instruction now emits the
|
||||
toolchain's `JAL X1, sym(SB)` with a single `R_RISCV_JAL` relocation
|
||||
(previously it emitted an `AUIPC`+`JALR` pair against a local branch
|
||||
label, a form `go tool asm` rejects). The GOOBJ and ELF emitters now map
|
||||
that relocation (Go objabi 59 / ELF `R_RISCV_JAL` 17, a 4-byte field), and
|
||||
relocation offsets are recorded relative to the function start (including
|
||||
the prologue). A byte-exact ground-truth test covers a call against
|
||||
`GOARCH=riscv64 go tool asm`.
|
||||
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
||||
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
||||
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
|
||||
`watch` picks the first free slot and reports an error if all four are in
|
||||
use, and `unwatch <slot>` clears one (no argument clears all).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Linux only.** The toolkit, its CI and the released binaries are now
|
||||
Linux-only; cross-compiled to linux/{amd64,arm64,riscv64,loong64}.
|
||||
- **Phase 4 closed.** README's "Remaining" list for the debugger is gone;
|
||||
disassembly at PC, memory-write, watchpoints, and source-line mapping are
|
||||
all shipped.
|
||||
|
||||
## [0.29.0] — 2026-08-07
|
||||
|
||||
@@ -60,20 +354,13 @@ new CLI commands. A signature-parser fix corrects grouped Go parameters.
|
||||
prevent GC from collecting heap objects whose addresses were passed to JIT
|
||||
code via `unsafe.Pointer`; all verify tests pass 100/100 under `-race`.
|
||||
|
||||
### Cleaned up
|
||||
### Changed
|
||||
|
||||
- **Removed external kernel test dependencies** — the verify test suite no
|
||||
longer references production kernels from the separate go-libraries project.
|
||||
The remaining test suite uses only `testdata/verify/*.s` kernels, which are
|
||||
part of this repository. Coverage is identical locally and in CI (80.3 %).
|
||||
|
||||
### Verified
|
||||
|
||||
- `gasm diff` detects byte-level differences; `--map` pairs differently-named
|
||||
functions for comparison.
|
||||
- `gasm verify --call` invokes functions with user-supplied buffers; the arg
|
||||
block is printed before and after the call, showing return values.
|
||||
- LSP go-to-definition resolves labels across functions and files.
|
||||
|
||||
## [0.28.0] — 2026-08-03
|
||||
|
||||
@@ -105,10 +392,6 @@ ground-truth verification against `GOARCH=riscv64 go tool asm`.
|
||||
- RVC: C.LDSP/C.SDSP/FLDSP/FSDSP immediate encoding now matches Go toolchain
|
||||
(bit-interleaved format).
|
||||
|
||||
### Verified
|
||||
|
||||
- 118 RISC-V tests, asm coverage 83.3%.
|
||||
- Ground-truth: C.LDSP, C.SDSP, C.FLDSP, C.FSDSP byte-exact vs Go toolchain.
|
||||
|
||||
## [0.27.0] — 2026-08-01
|
||||
|
||||
@@ -141,7 +424,7 @@ area bit-for-bit.
|
||||
analyze, autocorr) pass; partial functions (decoders that fault on malformed
|
||||
input) should use `--ground-truth` instead.
|
||||
|
||||
### Known limitation
|
||||
### Fixed
|
||||
|
||||
`--fuzz` crashes the process for partial functions (e.g. LZ4 decoders) whose
|
||||
over-copy paths read past the buffer on random garbage input. Subprocess
|
||||
@@ -204,11 +487,6 @@ The remaining go-flac encoder kernels join the differential suite.
|
||||
frames: the four zigzag-fold entropy sums compared against the scalar
|
||||
loop).
|
||||
|
||||
### Verified
|
||||
|
||||
- `gasm fmt` doc-comment indentation confirmed correct: comments before
|
||||
every TEXT are at column 0 (the RET-detection logic handles multi-exit
|
||||
functions).
|
||||
|
||||
## [0.21.0] — 2026-07-26
|
||||
|
||||
@@ -246,7 +524,7 @@ test corpus exercises.
|
||||
argument blocks and collect distinct output fingerprints (the result
|
||||
words); reports path diversity as a lower bound on code coverage.
|
||||
|
||||
### Note
|
||||
### Changed
|
||||
|
||||
INT3-based per-block hit counting was prototyped but deferred: Go's runtime
|
||||
signal management (sigaltstack, handler re-installation) makes raw
|
||||
@@ -311,11 +589,6 @@ toolchain.
|
||||
known-answer LZ4 blocks decode bit-for-bit, wide copies of 0–1024 bytes
|
||||
match, malformed input returns the correct error codes.
|
||||
|
||||
### Verified
|
||||
|
||||
- `just test` (race, 84.6 % total coverage, verify 82.2 %).
|
||||
- `gasm verify` on both go-lz4 kernels: all functions JIT-load and
|
||||
smoke-test clean.
|
||||
|
||||
## [0.16.0] — 2026-07-21
|
||||
|
||||
@@ -441,13 +714,6 @@ Go assembler.
|
||||
(`DATA mask<>+8(SB)/8, $0x800f…`) parse as unsigned and keep their bit
|
||||
pattern, instead of being rejected as non-integer.
|
||||
|
||||
### Verified
|
||||
|
||||
- End-to-end: a gasm-emitted GOOBJ swapped into a `go build` in place of
|
||||
the toolchain's assembly object links and runs with output identical to
|
||||
the baseline binary (stack-argument calls and a `GLOBL` relocation
|
||||
resolved by the Go linker). All 17 go-flac AVX2 kernel functions emit as
|
||||
a GOOBJ that `go tool nm` reads back with every symbol intact.
|
||||
|
||||
## [0.11.0] — 2026-07-16
|
||||
|
||||
@@ -471,7 +737,7 @@ objects.
|
||||
`GLOBL` in the file defines no longer aborts assembly — it is recorded
|
||||
as an external relocation (`Image.Externals`, `FuncLayout.Relocs`) and
|
||||
becomes an undefined global symbol in the object output. The raw image
|
||||
format (`--format raw`, the default) still reports them: only an object
|
||||
s (`--format raw`, the default) still reports them: only an object
|
||||
file can represent a reference the linker must resolve.
|
||||
|
||||
### Changed
|
||||
@@ -506,7 +772,7 @@ verified byte for byte against the Go assembler.
|
||||
arithmetic, the unpacks, VMOVDDUP and the conversions all accept the
|
||||
explicit K1–K7 operand and the `.Z` suffix the way Go writes them.
|
||||
|
||||
### Documented
|
||||
### Changed
|
||||
|
||||
- VCVTPS2PD follows the Go assembler's encoding, which omits the F3
|
||||
mandatory prefix (VEX.pp / EVEX.pp = 00) that Intel's maps prescribe; the
|
||||
@@ -514,15 +780,6 @@ verified byte for byte against the Go assembler.
|
||||
gasm reproduces it exactly (and round-trips through the x86 decoder, which
|
||||
shares the convention).
|
||||
|
||||
### Verified
|
||||
|
||||
- 58 new ground-truth cases — every instruction extracted from the Go
|
||||
toolchain's own assembly (go build + an executable-segment dump), checked
|
||||
byte for byte and round-tripped through the decoder, covering disp8×N for
|
||||
the scalar (×8/×4), duplication (×8/×32/×64) and conversion (×8/×16/×32)
|
||||
memory operands, the 5-bit register fields and the masked/zeroing P2
|
||||
byte. All four go-flac/go-lz4 kernels still assemble byte-identically
|
||||
and lint clean.
|
||||
|
||||
## [0.9.0] — 2026-07-14
|
||||
|
||||
@@ -581,7 +838,7 @@ The formatter behaves like `go fmt` and canonicalises block separation.
|
||||
|
||||
### Changed
|
||||
|
||||
- `format`: canonical blank-line layout — a new block (a label, `TEXT` or
|
||||
- `s`: canonical blank-line layout — a new block (a label, `TEXT` or
|
||||
`GLOBL`) is preceded by exactly one blank line, neither more nor less.
|
||||
Comments leading a block stay with it (the blank line goes before them),
|
||||
stacked labels share their block, the function's first label keeps hugging
|
||||
@@ -616,7 +873,7 @@ the encoder learns the legacy SSE moves.
|
||||
*first* operand on arm64, riscv64 and loong64; Plan 9 spelling puts it last
|
||||
on every architecture Go supports. The def/use and save/restore
|
||||
classification on those architectures was inverted.
|
||||
- `format`: a comment that follows a `RET` (typically the next function's doc
|
||||
- `s`: a comment that follows a `RET` (typically the next function's doc
|
||||
comment) is no longer indented as if it were still inside the finished
|
||||
function body.
|
||||
|
||||
@@ -658,13 +915,6 @@ the Go toolchain, completing the production-kernel coverage.
|
||||
- `asm`: the VEX encoder now rejects vector register indices 16–31 instead of
|
||||
encoding a truncated (wrong) register.
|
||||
|
||||
### Verified
|
||||
|
||||
- All 10 functions of the go-flac `avx512_amd64.s` kernel assemble
|
||||
byte-identically to the Go toolchain's machine code (the disp32 of the one
|
||||
`VMOVDQU32 idx16(SB), Z13` load is linker-filled in Go and resolved within
|
||||
gasm's own image — checked to reach the right constant bytes). The AVX2
|
||||
kernel's 17 functions remain byte-identical.
|
||||
|
||||
## [0.4.0] — 2026-07-09
|
||||
|
||||
@@ -685,13 +935,6 @@ machine code byte for byte.
|
||||
- `gasm asm` prints the data section and symbol map alongside the functions
|
||||
and writes the whole image (code + data) with `-o`.
|
||||
|
||||
### Verified
|
||||
|
||||
- All 17 functions of the go-flac `avx2_amd64.s` kernel assemble
|
||||
byte-identically to the Go toolchain's machine code; the only differing
|
||||
bytes are the displacements of the two `VMOVDQU mask24<>(SB), X15` loads,
|
||||
which the Go linker fills at link time and gasm resolves within its own
|
||||
image (checked to reach the right constant bytes).
|
||||
|
||||
## [0.3.0] — 2026-07-08
|
||||
|
||||
@@ -803,7 +1046,7 @@ Initial release — the Phase 1 foundation.
|
||||
Zero error-severity diagnostics across the 90-file Go runtime corpus and the
|
||||
production go-flac kernels (the `register-clobber` audit additionally reports
|
||||
the go-flac kernels' unsaved callee-saved register use for review).
|
||||
- `format`: an idempotent canonical formatter (operand spacing and per-function
|
||||
- `s`: an idempotent canonical formatter (operand spacing and per-function
|
||||
mnemonic alignment) that preserves comments and round-trips through the
|
||||
parser.
|
||||
- `lsp`: a Language Server Protocol server over stdio providing completion,
|
||||
|
||||
+77
-64
@@ -1,94 +1,107 @@
|
||||
# Contributing to gasm-devkit
|
||||
|
||||
## Prerequisites
|
||||
Thanks for contributing to gasm-devkit.
|
||||
|
||||
- Go 1.26 or later (`toolchain go1.26.5`)
|
||||
- `just` command runner
|
||||
- A Linux, FreeBSD, or macOS host on amd64 or arm64
|
||||
## Development setup
|
||||
|
||||
## Development Setup
|
||||
Requirements: Go 1.27 or later, the [just](https://github.com/casey/just)
|
||||
command runner, and a Linux host on amd64, arm64, riscv64 or loong64.
|
||||
|
||||
```sh
|
||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||
cd gasm-devkit
|
||||
just install # download module dependencies
|
||||
just build # go vet + gofmt check
|
||||
just test # full test suite with race detector
|
||||
just test # full suite, race detector, 80 % coverage gate
|
||||
```
|
||||
|
||||
## Commands
|
||||
## Workflow
|
||||
|
||||
Every just recipe:
|
||||
1. Branch from `development`; never commit directly to `main` (`main` is
|
||||
release-only: merge from `development`, then tag).
|
||||
2. Commit with [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
`type(scope): description`: subject line only, imperative mood,
|
||||
lowercase after the colon, no trailing dot. Allowed types: `feat`,
|
||||
`fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`,
|
||||
`build`, `revert`. The only line after the subject is the trailer:
|
||||
`Assisted-by: <model-name>`. No `Co-Authored-By`, no `Signed-off-by`,
|
||||
no other trailers.
|
||||
3. Record every user-visible change in `CHANGELOG.md` under
|
||||
`## [development]` (categories: Added, Changed, Fixed, Removed,
|
||||
Security).
|
||||
4. Add or update tests; coverage must stay **at or above 80 %** (hard
|
||||
gate, enforced by CI).
|
||||
5. Update the documentation when behaviour, flags or the public surface
|
||||
change.
|
||||
6. Open a pull request against `development`.
|
||||
|
||||
| Recipe | What it does |
|
||||
|--------|-------------|
|
||||
| `just` | List all recipes |
|
||||
| `just install` | `go mod download` |
|
||||
| `just build` | `go vet ./...` + `gofmt -l .` check — zero errors required |
|
||||
| `just test` | `go test -race -count=1 -coverprofile=coverage.out ./...` + 80 % coverage gate |
|
||||
| `just fmt` | `gofmt -w .` |
|
||||
| `just run -- lint file.s` | Run the CLI with `go run` (args after `--`) |
|
||||
| `just install-bin` | Install `gasm` into `$GOBIN` with the release version stamped |
|
||||
| `just gen` | Regenerate `arch/*_gen.go` instruction tables from the Go toolchain |
|
||||
| `just uninstall` | Remove build artefacts (`coverage.out`, `gasm`, `*.test`) |
|
||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`;
|
||||
CI builds and publishes the binaries for all four architectures.
|
||||
|
||||
## Running a Single Test
|
||||
## Code style
|
||||
|
||||
`gofmt` and `go vet` via `just fmt` / `just build`; both must pass with
|
||||
zero output; `go fix -diff ./...` must report nothing on touched packages.
|
||||
|
||||
- Standard library only in production code; `golang.org/x/arch` is used
|
||||
in tests only (round-trip decoding) and is never linked into the `gasm`
|
||||
binary.
|
||||
- No cgo, no C, no external toolchains at runtime.
|
||||
- Explicit `if err != nil`; errors wrapped with
|
||||
`fmt.Errorf("context: %w", err)`; no panics outside `main`.
|
||||
- The parser, lexer and formatter are hand-written; the `arch` instruction
|
||||
tables are generated only via `_gen/gen.go` (`just gen`), never edited.
|
||||
|
||||
## Running a single test
|
||||
|
||||
```sh
|
||||
go test -run TestVexGroundTruth ./asm/
|
||||
go test -run TestDifferentialLZ4Fuzz ./verify/
|
||||
go test -run TestGroundTruthBasic ./verify/
|
||||
go test -run TestGOObjectLinkAndRun ./asm/
|
||||
go test -run TestFuzzWideCopy ./verify/
|
||||
```
|
||||
|
||||
## Testing the Debugger
|
||||
The interactive debugger (`gasm debug`) requires a compiled binary on
|
||||
`$PATH`; `go run` does not work for the traced child process. Install
|
||||
first with `just install-bin`.
|
||||
|
||||
The interactive debugger (`gasm debug`) requires a compiled binary —
|
||||
`go run` does not work for the child process. Install first:
|
||||
## CI (Gitea Actions)
|
||||
|
||||
```sh
|
||||
just install-bin
|
||||
gasm debug --func add testdata/verify/basic_amd64.s
|
||||
```
|
||||
Workflows live in `.gitea/workflows/` and run on self-hosted runners:
|
||||
|
||||
## Code Style
|
||||
| Workflow | Trigger | What it does |
|
||||
|----------|---------|--------------|
|
||||
| Test | push / PR to `development` | gofmt check, `go vet`, `go test -race`, 80 % coverage gate |
|
||||
| Release | tag `v*` | cross-compiles binaries for linux/{amd64,arm64,riscv64,loong64} and publishes the Gitea release |
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for the full style guide. Key points:
|
||||
The Definition of Done (`just build` + `just test` + `just fmt`) must
|
||||
still pass locally before pushing.
|
||||
|
||||
- `gofmt` — zero diff.
|
||||
- `go vet` — zero warnings.
|
||||
- Standard library only in production code; `golang.org/x/arch` in tests.
|
||||
- No cgo, no C, no JavaScript.
|
||||
- Hand-written Plan 9 assembly; tables generated only via `_gen/gen.go`.
|
||||
## AI Contribution Policy
|
||||
|
||||
## Branches and Releases
|
||||
AI tools are welcome as productivity aids. What matters is that
|
||||
contributions remain understandable, reviewable, and genuinely useful.
|
||||
|
||||
- `development` is the working branch.
|
||||
- `main` is release-only: `git merge --ff-only development`, then `git tag vX.Y.Z`.
|
||||
- Conventional Commits: `feat(asm): add EVEX gather and scatter`.
|
||||
- Every commit ends with `Assisted-by: <model-name>`.
|
||||
- **Disclose AI use.** If you used AI to draft or generate any part of a
|
||||
commit, issue, pull request, or code review, say so clearly.
|
||||
- **Commit messages:** end every commit with exactly one trailer:
|
||||
`Assisted-by: <model-name>` (e.g. `Assisted-by: GLM 5.3`).
|
||||
- **Pull requests and issues:** attribute AI assistance in one trailing
|
||||
line, e.g. `_Assisted-by: GLM 5.3_`. Do not paste it into the PR
|
||||
description as a section.
|
||||
- **Take responsibility.** You remain accountable for the accuracy,
|
||||
completeness, and intent of everything you submit.
|
||||
- **Review before marking ready.** Read AI-generated diffs carefully, run
|
||||
them locally, and add or update tests where appropriate.
|
||||
- **Preferred models.** Prefer open-weight models with transparent
|
||||
training data: **GLM**, **DeepSeek**, and **MiMo**.
|
||||
|
||||
## CI
|
||||
|
||||
There is no CI pipeline in this repository. The Definition of Done
|
||||
(`just build` + `just test` + `just fmt`) is enforced locally.
|
||||
|
||||
## AI-Assisted Contributions
|
||||
|
||||
AI agents may assist with code, documentation, tests, and review. All
|
||||
AI-assisted changes must:
|
||||
|
||||
- Include the trailer `Assisted-by: <model-name>` in the commit message
|
||||
(e.g. `Assisted-by: DeepSeek V4 Pro`).
|
||||
- Follow the [AGENTS.md](AGENTS.md) rules.
|
||||
- Pass the Definition of Done before committing.
|
||||
|
||||
Attribute agent authorship in issues and pull requests on one trailing
|
||||
line:
|
||||
|
||||
```
|
||||
_Assisted-by: DeepSeek V4 Pro_
|
||||
```
|
||||
|
||||
## Questions
|
||||
## Reporting bugs
|
||||
|
||||
Open an issue at
|
||||
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues).
|
||||
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues)
|
||||
with the version (`gasm --version`), OS and architecture, the exact
|
||||
command, the full output, and the expected versus actual behaviour.
|
||||
|
||||
**Security issues:** email **opensource@petrbalvin.org** instead of opening
|
||||
a public issue.
|
||||
|
||||
@@ -1,362 +1,158 @@
|
||||
# gasm-devkit
|
||||
|
||||
Developer tooling for **GAsm** — Go's built-in Plan 9 assembler.
|
||||
Developer tooling for **GAsm**, Go's built-in Plan 9 assembler.
|
||||
|
||||
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
||||
|
||||
Go ships an assembler but no tooling for it. There is no syntax highlighting,
|
||||
Go ships an assembler but no tooling for it: there is no syntax highlighting,
|
||||
no autocomplete, no linter, no static analyser, no formatter, no standalone
|
||||
assembler and no debugger for `.s` files. Developers write assembly blind,
|
||||
validate it by benchmark, and debug it by print statement.
|
||||
validate it by benchmark, and debug it by print statement. gasm-devkit is the
|
||||
missing toolkit: a single, self-contained binary, `gasm`, that brings proper
|
||||
developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
||||
|
||||
gasm-devkit is the missing toolkit. It is a single, self-contained binary —
|
||||
`gasm` — that brings proper developer tooling to Plan 9 assembly:
|
||||
## Features
|
||||
|
||||
```
|
||||
gasm tokens dump the lexical token stream
|
||||
gasm parse parse and report syntax errors
|
||||
gasm fmt canonicalise formatting (gofmt for assembly)
|
||||
gasm lint static checks
|
||||
gasm lsp language server (completion, hover, symbols, diagnostics, highlighting)
|
||||
gasm asm standalone assembler (Phase 2)
|
||||
gasm verify dynamic analysis & verification (Phase 3)
|
||||
gasm debug source-level debugger (Phase 4)
|
||||
gasm diff compare machine code of two .s files
|
||||
gasm profile show basic-block structure of functions
|
||||
```
|
||||
- **Front end.** A hand-written lexer and an error-tolerant parser produce a
|
||||
typed AST with source positions; `gasm tokens` and `gasm parse` expose them
|
||||
directly.
|
||||
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
||||
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
||||
operating recursively on directories the way `go fmt` does.
|
||||
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
|
||||
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
||||
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
||||
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`.
|
||||
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
||||
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
||||
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
||||
consumes in place of the toolchain's output.
|
||||
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
||||
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
|
||||
canary), differential fuzzing against the `go tool asm` build, and
|
||||
byte-for-byte ground-truth comparison of the machine code.
|
||||
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
|
||||
breakpoints (optionally conditional), hardware watchpoints, register and
|
||||
memory inspection, and headless script runs with label-level coverage.
|
||||
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
|
||||
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
||||
find references, rename, formatting, inlay hints, code actions, signature
|
||||
help, document highlights, workspace symbol search, #include document
|
||||
links and folding ranges over stdio.
|
||||
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
||||
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
||||
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
||||
and `gasm scaffold` generates a differential test skeleton for a kernel.
|
||||
- **Complete instruction coverage.** The instruction tables are generated
|
||||
from the Go toolchain's own assembler source, so the toolkit recognises
|
||||
every mnemonic the real assembler accepts; `just gen` refreshes them.
|
||||
|
||||
> **Status: Phase 4 — done, Phase 5 underway.** Phase 1 (the language
|
||||
> foundation, linter, formatter and language server) shipped in v0.1.0;
|
||||
> Phase 2 (the standalone assembler — the full amd64 instruction set plus
|
||||
> ELF, Mach-O and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic
|
||||
> analysis — JIT execution, differential testing, ABI checks and coverage
|
||||
> profiling) in v0.25.0; Phase 4 (interactive debugger — ptrace-based,
|
||||
> breakpoints, watchpoints, stepping, vector register display, named buffer
|
||||
> allocation) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC, ELF emission,
|
||||
> ground-truth, GOOBJ) in v0.28.0–v0.29.0. See [Roadmap](#roadmap).
|
||||
### Architecture support
|
||||
|
||||
## Architecture support
|
||||
|
||||
gasm-devkit targets every architecture Go's assembler speaks. The instruction
|
||||
tables are **generated from the Go toolchain's own assembler source**
|
||||
(`cmd/internal/obj/<arch>`), so gasm-devkit recognises *every* mnemonic the
|
||||
real assembler accepts — not a hand-maintained subset that drifts and rots.
|
||||
|
||||
| Architecture | GOARCH | File suffix | Instructions recognised |
|
||||
|--------------|-------------|----------------|------------------------------------|
|
||||
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
||||
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
|
||||
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
|
||||
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
|
||||
| Architecture | GOARCH | File suffix | Instructions recognised |
|
||||
|--------------|-------------|--------------|---------------------------------------------|
|
||||
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
||||
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
|
||||
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
|
||||
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
|
||||
|
||||
"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
|
||||
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
|
||||
…) that the assembler accepts as aliases. Regenerating the tables is one
|
||||
command — `just gen` — and requires only a Go installation; the committed
|
||||
output has no runtime dependency on the toolchain.
|
||||
...) that the assembler accepts as aliases. Regenerating the tables is one
|
||||
command (`just gen`) and requires only a Go installation; the committed output
|
||||
has no runtime dependency on the toolchain.
|
||||
|
||||
The target *architectures* above are what the toolkit analyses. The toolkit
|
||||
itself is portable Go and builds on Linux, FreeBSD and macOS, on amd64 and
|
||||
arm64 hosts.
|
||||
## Install
|
||||
|
||||
## Roadmap
|
||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
||||
linux/loong64 are on the
|
||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
||||
From source (Go 1.27 or later):
|
||||
|
||||
The work is delivered in four phases. Each phase is completed and hardened
|
||||
before the next begins. The ordering follows a dependency chain: understand
|
||||
the code statically (Phase 1), make it runnable (Phase 2), then run it and
|
||||
observe or control it (Phases 3–4).
|
||||
```sh
|
||||
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
||||
```
|
||||
|
||||
### Phase 1 — language foundation, editor tooling and static analysis · *done*
|
||||
Or from a repository checkout, with the development version stamped:
|
||||
|
||||
Everything needed to read, understand, check, format and highlight GAsm —
|
||||
without executing it.
|
||||
|
||||
| Capability | Status |
|
||||
|------------|--------|
|
||||
| Lexer — permissive, position-aware scanner for all four architectures | done |
|
||||
| Parser — line-oriented, error-tolerant, full AST with source positions | done |
|
||||
| Instruction + register tables for amd64, arm64, riscv64, loong64 (generated, complete) | done |
|
||||
| Linter — `unknown-instruction`, `operand-count`, `undefined-label`, `duplicate-label`, `missing-ret`, `missing-textflag-include`, `abi-argsize`, `unreachable-code`, `register-clobber`, `funcdata-pcdata` | done |
|
||||
| Formatter — idempotent, comment-preserving, per-function alignment; a `RET` terminates the body for indentation, so the next function's doc comment stays at column 0; exactly one blank line before every block (label, `TEXT`, `GLOBL`) and runs of blanks collapsed; directory / no-argument mode reformats every `.s` in place, `go fmt`-style | done |
|
||||
| Language server — completion, hover, document symbols, diagnostics, semantic-token highlighting | done |
|
||||
| CLI — `gasm tokens / parse / fmt / lint / lsp` | done |
|
||||
| Real-world validation against production AVX2 / AVX-512 kernels | done |
|
||||
| Lint hardening — zero false positives across the Go runtime corpus (90 files, all four architectures): macro-invocation handling, branch aliases (`B`/`BL`/`JAL`), addressing suffixes (`.P`/`.W`), terminal `UNDEF` | done |
|
||||
| Static analysis — `abi-argsize` (argument/result area computed from the `// func` signature under Go's ABI0 layout and checked against the TEXT declaration) and `unreachable-code` (dead code after `RET`, suppressed where reachability is undecidable: PC-relative jumps, register-indirect branches, `#ifdef`) | done |
|
||||
| Static analysis — register liveness (CFG construction + per-instruction def/use + iterative backward dataflow) driving `register-clobber`, calibrated to the **Go ABI** (not System V): flags writes to the registers Go fixes across calls — the frame pointer and the goroutine pointer (`R14` on amd64, `R28`/`R29` on arm64, `X27` on riscv64, `R22` on loong64, plus the OS-reserved `R18` on arm64) — that are never saved/restored; the goroutine pointer is reported only when the function can reach the runtime (not `NOSPLIT`, or makes calls), matching how the runtime's own assembly uses it. `funcdata-pcdata` structural validation of `FUNCDATA`/`PCDATA` operands and indices | done |
|
||||
|
||||
> **Limitation — macros.** gasm-devkit reads `.s` source as written; it does
|
||||
> **not** run the C preprocessor, so `#define` macros are not expanded. Files
|
||||
> that use macros (the runtime's `asm_*.s`, `race_*.s`, `sys_*.s`, …) parse
|
||||
> cleanly, and macro *invocations* are recognised and never flagged, but the
|
||||
> `undefined-label` and `missing-ret` heuristics are suppressed in macro-using
|
||||
> files because labels a macro defines are invisible without expansion. Full
|
||||
> macro expansion is future work (it pairs naturally with the Phase 2
|
||||
> assembler). Hand-written, macro-free kernels — such as everything in
|
||||
> `go-libraries` — are analysed in full.
|
||||
|
||||
### Phase 2 — standalone assembler · *done*
|
||||
|
||||
Assembly without the Go toolchain in the loop.
|
||||
|
||||
- **`gasm asm`:** a standalone assembler that turns a `.s` file into machine
|
||||
code directly — pure Go, no `go build`, no external toolchain. Useful for
|
||||
fast iteration, for environments without a full Go installation, and as the
|
||||
execution substrate that Phases 3 and 4 build on.
|
||||
|
||||
Done so far:
|
||||
|
||||
- An amd64 (x86-64) **instruction encoder** — REX/ModR-M/SIB/displacement/
|
||||
immediate machinery and the scalar instruction set (MOV, the ALU group, TEST,
|
||||
LEA, INC/DEC/NEG/NOT, shifts, IMUL and IMUL3, PUSH/POP, JMP/CALL/Jcc,
|
||||
CMOVcc, SETcc, LZCNT/TZCNT, the sign/zero-extending moves — MOVBLZX and
|
||||
friends, MOVLQSX — and CVTSL2SD/CVTSQ2SD), validated by round-tripping
|
||||
every encoding through `golang.org/x/arch`'s decoder and byte-for-byte
|
||||
against the Go assembler.
|
||||
- An **assembler** that drives the parser's AST into the encoder with local-
|
||||
label resolution — jumps start in the short (rel8) form and expand to rel32
|
||||
when the displacement does not fit, and jump-to-jump chains are folded the
|
||||
way the Go toolchain folds them — so `gasm asm <file>` emits machine code
|
||||
for each `TEXT` function.
|
||||
- **File-level assembly with static data** — `GLOBL`/`DATA` symbols are laid
|
||||
out in a data section behind the code and references to them (`mask<>(SB)`)
|
||||
are encoded RIP-relative with the displacement resolved within the image,
|
||||
so the output is self-consistent and position-independent. References to
|
||||
symbols no `GLOBL` in the file defines are recorded as relocations and
|
||||
carried into the object-file output.
|
||||
- **GOOBJ emission** — `gasm asm --format goobj -p <pkgpath>` writes the Go
|
||||
toolchain's own object format (the one `cmd/link` consumes directly), so
|
||||
gasm-assembled kernels drop into a `go build` without the Go assembler:
|
||||
the functions as non-package symbols, `GLOBL` data, one `FuncInfo` per
|
||||
function and the pc-value tables (`pcsp` with the real prologue/epilogue
|
||||
stack deltas, `pcfile`, `pcline`, `pcinline`). Verified end-to-end by
|
||||
swapping a gasm-emitted object into a `go build` in place of the
|
||||
toolchain's, linking and running — bit-identical behaviour.
|
||||
- **Object-file emission** — `gasm asm --format elf` / `--format macho`
|
||||
writes a relocatable object (a `.text` and a `.data` section, a symbol
|
||||
table — file-local `<>` symbols local, the rest global — and one
|
||||
`R_X86_64_PC32` / `X86_64_RELOC_SIGNED` relocation per static-symbol
|
||||
reference) that links with the system toolchain: external references
|
||||
resolve against undefined symbols, file-local ones against the data
|
||||
section. Verified end-to-end by linking a gasm-emitted object with a C
|
||||
driver and running it.
|
||||
- **`FP`/`SP` frame mapping** — the pseudo-registers are translated onto the
|
||||
hardware stack pointer (`x+N(FP)` → `(N+8)(SP)` for a zero frame, `(N+frame+
|
||||
16)(SP)` with a frame pointer; locals via `x-N(SP)`), and the Go-style
|
||||
prologue/epilogue is generated for functions with a frame. The output is
|
||||
**byte-identical to the Go assembler** for these cases (verified against
|
||||
`go tool objdump`).
|
||||
- **SIMD (VEX / AVX2)** — the VEX prefix machinery (2-byte C5 and 3-byte C4)
|
||||
with XMM/YMM vector registers, validated by round-trip decoding **and**
|
||||
byte-for-byte against the Go assembler's machine code, across eight operand
|
||||
forms: the three-operand NDS form (VPADDD/Q, VPSUBD/Q, VPXOR, VPOR, VPAND/N,
|
||||
VPCMPEQD, VPCMPGTQ, VPUNPCK*, VPMULLD, VPMULDQ, VPSHUFB, VPACKSSDW,
|
||||
VPERMD), the two-operand reg/rm form (VPMOVSXWD/DQ, VPMOVZXDQ,
|
||||
VPBROADCASTD/Q, VPMOVMSKB, VMOVMSKPS, VCVTDQ2PD), the immediate-shift and
|
||||
variable-count shifts (VPSLLD/Q, VPSRAD, VPSRLD/Q with an immediate or an
|
||||
XMM/memory count), the immediate shuffle (VPSHUFD, VPERMQ), the
|
||||
three-operand-plus-immediate form (VSHUFPD, VPERM2I128, VINSERTI128), the
|
||||
lane extract (VEXTRACTI128, VEXTRACTF128), the direction-sensitive moves
|
||||
(VMOVDQU, VMOVUPD, VMOVD, VMOVQ, VMOVSD), the no-operand VZEROUPPER, and
|
||||
the floating-point set: the packed double arithmetic
|
||||
(VADDPD/VSUBPD/VMULPD/VDIVPD/VMINPD/VMAXPD), the unpacks
|
||||
(VUNPCKHPD/VUNPCKLPD), the scalar SD and SS operations, VMOVDDUP, the
|
||||
width-changing conversions (VCVTDQ2PS, VCVTPS2PD, VCVTDQ2PD and the
|
||||
VCVTPD2DQX/Y / VCVTTPD2DQX/Y spellings, whose VEX.L follows the wider
|
||||
source) and VFMADD231PD.
|
||||
- **SIMD (EVEX / AVX-512)** — the four-byte EVEX prefix with the 5-bit
|
||||
register fields (Z0–Z31, X/Y 16–31), opmask registers (K0–K7 as operands
|
||||
and mask destinations, KMOVW, KTESTW) and the compressed disp8×N
|
||||
displacement, covering every AVX-512 instruction the go-flac kernels use:
|
||||
VPXORD/Q, VPADDD, VPSUBD/Q, VPUNPCK*DQ, VPMULLD/Q, VPERMD, VPSLLD/VPSRAD/
|
||||
VPSRAQ, VALIGND, VPCMPEQD (with a K destination), VMOVDQU32, VMOVUPD,
|
||||
VCVTQQ2PD, VPMOVSXDQ, the narrowing stores VPMOVDW/VPMOVQD, the lane
|
||||
extracts VEXTRACTI64X4/VEXTRACTF64X4, VFMADD231PD, VADDPD, VMULPD,
|
||||
VMOVDQU64 and the broadcasts VPBROADCASTD/Q from a GPR or memory, plus the
|
||||
wider AVX-512 F/BW integer set (VPADDB/W, VPSUBB/W, VPANDD/Q/ND/NQ, VPMULLW,
|
||||
VPMIN*/VPMAX* for B/W/D/Q elements, signed and unsigned, VPAVGB/W, the variable
|
||||
shifts VPSLLV*/VPSRLV*/VPSRAV*, VMOVDQU8/16), the common floating-point
|
||||
and conversion set (the packed double and single arithmetic
|
||||
VADD/VSUB/VMUL/VDIV/VMIN/VMAX PD and PS, the scalar SD/SS operations —
|
||||
whose EVEX forms exist for masked and zeroing use — the VUNPCK{L,H}PD
|
||||
unpacks, VMOVDDUP, VMOVSLDUP/VMOVSHDUP and the VCVT* conversions), and
|
||||
the wider AVX-512 set: ternary logic (VPTERNLOGD/Q), lane shuffles,
|
||||
inserts and extracts (VSHUF{F,I}{32,64}X{2,4}, the VINSERT*/VEXTRACT*
|
||||
{F,I}{32,64}X{2,4,8} family, VPALIGNR), compares with an opmask
|
||||
destination (VCMPPD/PS/SD/SS), the permutes (VPERMB/W, VPERMI2/T2
|
||||
D/Q/PD), the wider integer families (VPMADDWD/UBSW, VPMULHUW, VPACK*,
|
||||
VPABS*, the VPROL*/VPROR* rotates and the word shifts), expand/compress
|
||||
(VEXPAND*/VCOMPRESS*, VPEXPAND*/VPCOMPRESS*), the broadcasts
|
||||
(VPBROADCASTB/W, VBROADCASTSS/SD), the opmask instructions (KAND/KOR/
|
||||
KXNOR/KADD/KUNPCK/KNOT/KSHIFTL/KORTEST, KMOVQ), the aligned moves
|
||||
(VMOVAPS/APD, VMOVDQA32/64, VMOVSS) and the remaining extending and
|
||||
narrowing moves, the floating-point helper and conversion tail
|
||||
(VRCP14*, VRSQRT14*, VGETEXP*, VGETMANT*, VSCALEF*, VRNDSCALE*,
|
||||
VREDUCE*, VFIXUPIMM*, VRANGE*, VFPCLASS* with a K destination, and the
|
||||
VCVT* conversions VCVTQQ2PS, VCVTPD2QQ/UQQ, VCVTPS2QQ, VCVTUDQ2PD/PS,
|
||||
VCVTPH2PS, VCVTPS2PH), and gather/scatter with VSIB addressing
|
||||
(VGATHER*/VPGATHER* in both the VEX mask-register spelling and the EVEX
|
||||
K-mask spelling — where the L'L field follows the VSIB index — plus
|
||||
VSCATTER*/VPSCATTER*). The EVEX mnemonic suffixes the Go assembler
|
||||
accepts are honoured: rounding modes (.RN_SAE, .RD_SAE, .RU_SAE,
|
||||
.RZ_SAE), suppress-all-exceptions (.SAE) and memory broadcast (.BCST,
|
||||
with the element-sized disp8×N), each combinable with the .Z zeroing
|
||||
suffix. Masking is supported the way
|
||||
Go writes it — an explicit K1–K7 operand placed among the operands, and a
|
||||
`.Z` mnemonic suffix for zeroing.
|
||||
- **Legacy SSE moves** — `MOVOU`/`MOVO` (the Plan 9 names for MOVDQU/MOVDQA),
|
||||
`MOVUPS`/`MOVAPS`/`MOVUPD`/`MOVAPD` and the scalar `MOVSD`/`MOVSS`.
|
||||
- **Both go-flac kernels — all 17 AVX2 and all 10 AVX-512 functions —
|
||||
assemble byte-identically to the Go toolchain's machine code**; the only
|
||||
differing bytes are the displacements of the static-constant loads, which
|
||||
the Go linker fills at link time and gasm resolves within its own image
|
||||
(verified to reach the right constant bytes).
|
||||
|
||||
Remaining for Phase 2:
|
||||
|
||||
- External (cross-package) symbol references in the GOOBJ output —
|
||||
**deferred** with a recorded decision and three options; see
|
||||
[`docs/DEFERRED.md`](docs/DEFERRED.md). Single-package objects (no
|
||||
cross-package references) work today, which covers the production
|
||||
kernels. With that item deferred, the amd64 instruction set — scalar,
|
||||
VEX/AVX2 and the full EVEX/AVX-512 set including GPR-interchanging
|
||||
conversions — is complete, and RISC-V encoding (RV64IMAFDC + RVC)
|
||||
including ELF and GOOBJ emission is complete.
|
||||
|
||||
### Phase 3 — dynamic analysis · *done*
|
||||
|
||||
Run the code and check what static analysis cannot. The oracle is the
|
||||
portable Go implementation every kernel is derived from.
|
||||
|
||||
- **`gasm verify`:**
|
||||
- **JIT execution substrate** — *done.* Assemble the kernel, map it into
|
||||
executable memory (`syscall.Mmap`, W^X) and call it through an ABI0
|
||||
trampoline; pure Go, no cgo, no external toolchain.
|
||||
- **Differential testing** — *done.* The JIT-assembled kernel is fuzzed
|
||||
against a portable Go reference, comparing the result bit-for-bit;
|
||||
the automated form of the project's bit-identical contract.
|
||||
- **Runtime ABI checks** — *done.* The ABI-checking trampoline sets
|
||||
sentinels in BP and R14, verifies they survive the call, and fills a
|
||||
128-byte red-zone canary below SP.
|
||||
- **Coverage / basic-block profiling** — *done.* Static block enumeration
|
||||
from the assembler's label map plus multi-input path-diversity
|
||||
measurement: how many observationally distinct execution paths a
|
||||
test corpus exercises.
|
||||
|
||||
### Phase 4 — debugger · *done*
|
||||
|
||||
- **`gasm debug`:** single-step a GAsm function, inspect registers (including
|
||||
YMM vector registers), set breakpoints on labels, allocate and fill named
|
||||
buffers, and hex-dump memory — the interactive counterpart to Phase 3's
|
||||
execution substrate.
|
||||
- **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME +
|
||||
LockOSThread), entry breakpoint (auto-run to function start),
|
||||
single-step, register inspection (GPR + YMM/XMM via PTRACE_GETFPREGS),
|
||||
label resolution, breakpoint management via `/proc/pid/mem`, named
|
||||
buffer allocation with pattern filling (`--buf`), and an interactive REPL.
|
||||
- **Remaining:** disassembly at PC (x86asm decode), memory-write support,
|
||||
watchpoints, source-line mapping, and multi-platform support
|
||||
(FreeBSD/macOS ptrace variants).
|
||||
|
||||
### Phase 5 — the other architectures · *in progress*
|
||||
|
||||
- **RISC-V encoding — done.** RV64IMAFDC instruction set, RVC compression,
|
||||
MOV pseudo-instruction, SB/global symbols (AUIPC pairs), ELF64 and GOOBJ
|
||||
emission, and ground-truth verification against `go tool asm`.
|
||||
- **Remaining:** arm64 and loong64 encoding, plus the same encode-and-verify
|
||||
treatment for each (instruction tables already generated from the toolchain).
|
||||
|
||||
## Principles
|
||||
|
||||
- **Pure Go and GAsm only.** No C, no cgo, no external toolchains, no native
|
||||
binaries, no JavaScript runtimes. The parser is hand-written; there is no
|
||||
parser generator.
|
||||
- **Self-contained.** The toolkit's production code depends only on the
|
||||
standard library; one binary, no runtime data files. The single module
|
||||
dependency, `golang.org/x/arch`, is used **only in tests** to validate the
|
||||
instruction encoder by round-trip decoding — it is never linked into the
|
||||
`gasm` binary.
|
||||
- **Portable.** Builds and runs on Linux, FreeBSD and macOS; amd64 and arm64
|
||||
hosts. Latest stable Go only.
|
||||
- **No vendor lock-in.** The integration surface is the Language Server
|
||||
Protocol and a command-line interface — both open standards. No cloud
|
||||
service, no proprietary API, no dependence on any one editor's internals.
|
||||
- **Complete and verifiable.** Instruction coverage is generated from the
|
||||
assembler's own source and regenerated on demand, so it cannot silently fall
|
||||
behind the toolchain.
|
||||
|
||||
## Components
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `token` | Lexical token kinds and source positions. |
|
||||
| `lexer` | Hand-written scanner for Plan 9 assembly. |
|
||||
| `ast` | The abstract syntax tree. |
|
||||
| `parser` | Line-oriented, error-tolerant parser producing the AST. |
|
||||
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
|
||||
| `lint` | Conservative static checks. |
|
||||
| `format` | A canonical formatter — `gofmt` for assembly. |
|
||||
| `asm` | The standalone assembler: amd64 and RISC-V encoders, linker, object-file emitters (ELF, Mach-O, GOOBJ). |
|
||||
| `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing (Phase 3). |
|
||||
| `debug` | Interactive ptrace debugger with GPR/YMM register display and named buffer allocation (Phase 4). |
|
||||
| `lsp` | Language Server Protocol server. |
|
||||
| `cmd/gasm` | The `gasm` binary tying it all together. |
|
||||
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
|
||||
|
||||
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale and
|
||||
data flow, [`docs/ZED.md`](docs/ZED.md) for the editor-integration story, and
|
||||
[`docs/DEFERRED.md`](docs/DEFERRED.md) for design decisions deliberately
|
||||
postponed (with the analysis needed to pick them up again).
|
||||
```sh
|
||||
just install-bin
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
just install # download dependencies (there are none)
|
||||
just build # go vet + gofmt check — zero errors, zero warnings
|
||||
just test # full suite, race detector, 80 % coverage gate
|
||||
just fmt # gofmt the tree
|
||||
just gen # regenerate the instruction tables from the Go toolchain
|
||||
cat > hello_amd64.s <<'EOF'
|
||||
#include "textflag.h"
|
||||
|
||||
// func add(a, b int) int
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
ADDQ b+8(FP), AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
EOF
|
||||
|
||||
gasm lint hello_amd64.s # static checks
|
||||
gasm asm -o hello.bin hello_amd64.s # assemble to a raw image
|
||||
gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with arguments
|
||||
```
|
||||
|
||||
Install the binary and use it:
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
just install-bin # installs gasm into $GOBIN
|
||||
|
||||
gasm --help # overview of commands and flags
|
||||
gasm tokens kernel_amd64.s # dump the token stream
|
||||
gasm parse kernel_amd64.s # parse, report syntax errors
|
||||
gasm fmt -w kernel_amd64.s # canonicalise in place
|
||||
gasm fmt # reformat every .s below here, like go fmt
|
||||
gasm lint *.s # static checks
|
||||
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
||||
gasm verify kernel_amd64.s # JIT-load and report functions
|
||||
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
||||
gasm verify --call decodeBlockAVX2 --buf src:64:hex...,dst:256:zero k.s
|
||||
gasm debug --func name k.s # interactive debugger
|
||||
gasm diff a.s b.s # compare machine code byte-for-byte
|
||||
gasm fmt # reformat every .s below here, like go fmt
|
||||
gasm fmt -w kernel_amd64.s # canonicalise one file in place
|
||||
gasm lint *.s # static checks
|
||||
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
||||
gasm asm --format goobj -p pkg/path -o k.o k.s # Go object, consumed by go build
|
||||
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
||||
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
|
||||
gasm debug --func name k.s # interactive debugger
|
||||
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
|
||||
gasm debug --func name --cover k.s # which labels did execution reach?
|
||||
gasm diff a.s b.s # compare machine code byte-for-byte
|
||||
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
gasm profile k.s # show basic-block structure
|
||||
gasm profile k.s # show basic-block structure
|
||||
gasm audit-instructions # encoder vs go tool asm name diff
|
||||
gasm scaffold differential k.s # generate a differential test skeleton
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow,
|
||||
[docs/cli.md](docs/cli.md) for the command reference, and
|
||||
[docs/development.md](docs/development.md) for setup and recipes.
|
||||
Run `gasm --help` for the command overview and `gasm <command> -h` for a
|
||||
command's flags. [docs/CLI.md](docs/CLI.md) is the full reference.
|
||||
|
||||
## Editor integration
|
||||
### Editor integration
|
||||
|
||||
`gasm lsp` speaks the Language Server Protocol over standard input/output, so
|
||||
any LSP-capable editor can use it — point your editor's LSP client at the
|
||||
any LSP-capable editor can use it: point your editor's LSP client at the
|
||||
binary and associate it with `.s` files. Syntax highlighting is delivered as
|
||||
**LSP semantic tokens**, so no editor-specific grammar is required. The server
|
||||
LSP semantic tokens, so no editor-specific grammar is required. The server
|
||||
infers the target architecture from the file-name suffix
|
||||
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`).
|
||||
|
||||
Zed users should read [`docs/ZED.md`](docs/ZED.md): Zed's native highlighting
|
||||
engine (Tree-sitter, C/WASM) cannot be fed from pure Go, so the pure-Go path
|
||||
into Zed is the language server and its semantic tokens.
|
||||
## Development
|
||||
|
||||
```sh
|
||||
just install # download module dependencies
|
||||
just build # go vet + gofmt check, zero errors and zero warnings
|
||||
just test # full suite, race detector, 80 % coverage gate
|
||||
just fmt # gofmt the tree
|
||||
just gen # regenerate the instruction tables from the Go toolchain
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and
|
||||
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for setup details and every
|
||||
recipe.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||
- [docs/DECISIONS.md](docs/DECISIONS.md): deferred design decisions
|
||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||
|
||||
## Licence
|
||||
|
||||
BSD-3-Clause — the same licence as Go itself. See [`LICENSE`](LICENSE).
|
||||
BSD-3-Clause — see [LICENSE](LICENSE).
|
||||
|
||||
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
||||
|
||||
+8
-2
@@ -86,7 +86,10 @@ func filterCommon(names []string) []string {
|
||||
func writeCommon(names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\n\n")
|
||||
b.WriteString("package arch\n\n")
|
||||
b.WriteString("// commonGeneratedInstrs is the set of opcodes shared by every architecture\n")
|
||||
b.WriteString("// (RET, JMP, NOP, CALL, TEXT, FUNCDATA, PCDATA, …).\n")
|
||||
@@ -154,7 +157,10 @@ func stringLit(elt ast.Expr) string {
|
||||
func writeGen(arch, sub string, names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\n\n")
|
||||
b.WriteString("package arch\n\n")
|
||||
b.WriteString("// " + arch + "GeneratedInstrs is the complete set of " + arch +
|
||||
" mnemonics accepted by\n// Go's Plan 9 assembler.\n")
|
||||
|
||||
@@ -270,6 +270,8 @@ func amd64Curated() []Instr {
|
||||
"VMINPD", "VMINPS", "VMINSD", "VMINSS", "VMAXPD", "VMAXPS", "VMAXSD", "VMAXSS",
|
||||
"VXORPD", "VXORPS", "VANDPD", "VANDPS", "VANDNPD", "VANDNPS", "VORPD", "VORPS",
|
||||
"VUNPCKHPD", "VUNPCKLPD", "VUNPCKHPS", "VUNPCKLPS",
|
||||
"PSHUFD", "PSHUFHW", "PSHUFLW", "SHUFPS", "SHUFPD",
|
||||
"UNPCKLPS", "UNPCKHPS", "UNPCKLPD", "UNPCKHPD",
|
||||
"VSQRTPD", "VSQRTPS", "VSQRTSD", "VSQRTSS", "VRSQRTPS", "VRCPPS",
|
||||
"VCMPPD", "VCMPPS", "VCMPSD", "VCMPSS",
|
||||
} {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/x86/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/arm64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/util.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/loong64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/riscv/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
||||
// AArch64 GOOBJ: the preamble, the magic, the block offsets and the
|
||||
// non-package symbol definitions.
|
||||
func TestGOObjectAARCH64Structure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.GOObjectAARCH64("testpkg", "k_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
|
||||
// Check preamble.
|
||||
idx := strings.Index(string(obj), "\n!\n")
|
||||
if idx < 0 {
|
||||
t.Fatal("missing preamble separator")
|
||||
}
|
||||
preamble := string(obj[:idx])
|
||||
if !strings.HasPrefix(preamble, "go object") {
|
||||
t.Errorf("preamble = %q, want 'go object ...'", preamble)
|
||||
}
|
||||
|
||||
// Check GOOBJ magic.
|
||||
magicIdx := idx + 3
|
||||
if magicIdx+8 > len(obj) || string(obj[magicIdx:magicIdx+8]) != "\x00go120ld" {
|
||||
t.Error("missing GOOBJ magic")
|
||||
}
|
||||
|
||||
// The object should contain the function's code.
|
||||
if len(img.Code) == 0 {
|
||||
t.Error("no code generated")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectAARCH64Link does an end-to-end link test: it cross-compiles a
|
||||
// Go program for arm64, substitutes the gasm-produced object into the package
|
||||
// archive, re-links with cmd/link, and verifies the symbol appears in the
|
||||
// resulting binary. The binary is not executed (no arm64 host or qemu).
|
||||
// Skipped when no Go toolchain is available.
|
||||
func TestGOObjectAARCH64Link(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_arm64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func add(a, b int64) int64
|
||||
|
||||
func main() {
|
||||
if add(20, 22) != 42 {
|
||||
panic("bad add")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module a64link\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the cross build (GOARCH=arm64): the package archive and the
|
||||
// link line.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_arm64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
|
||||
// Expand $WORK in the object path.
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
|
||||
// Read the toolchain-produced object and assemble the same source with gasm.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse("main_arm64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("a64link", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
|
||||
// Replace the toolchain-produced object with gasm's.
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
|
||||
// Re-link.
|
||||
if linkLine == "" {
|
||||
t.Skip("could not find link command in build log")
|
||||
}
|
||||
// Expand $WORK in the link command.
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
linkCmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Verify the binary exists and contains the symbol.
|
||||
binPath := filepath.Join(dir, "prog")
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
t.Fatalf("binary not found: %v", err)
|
||||
}
|
||||
binData, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read binary: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(binData), "add") && !strings.Contains(string(binData), "a64link") {
|
||||
t.Error("binary does not contain expected symbol")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,687 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
// arm64 (AArch64) instruction encoding.
|
||||
//
|
||||
// The encoder is data-driven: each mnemonic maps to an instruction format and
|
||||
// an opcode constant, and the format selects the bit layout. The opcode
|
||||
// constants and formats are transcribed from the Go toolchain's own arm64
|
||||
// backend (cmd/internal/obj/arm64), so the emitted bytes match `go tool asm`
|
||||
// exactly — the ground-truth oracle for the verify suite.
|
||||
//
|
||||
// All AArch64 instructions are 32 bits, little-endian. The formats used here
|
||||
// (per the ARM Architecture Reference Manual):
|
||||
//
|
||||
// DP-shifted-reg sf<<31 | op<<30 | S<<29 | 0x0b<<24 | shift<<22 | 0<<21 | Rm<<16 | imm6<<10 | Rn<<5 | Rd
|
||||
// DP-immediate sf<<31 | op<<30 | S<<29 | 0x11<<24 | imm12<<10 | Rn<<5 | Rd
|
||||
// Logical-imm sf<<31 | opc<<29 | 0x24<<23 | N<<22 | immr<<16 | imms<<10 | Rn<<5 | Rd
|
||||
// Move-wide sf<<31 | opc<<29 | 0x25<<23 | hw<<21 | imm16<<5 | Rd
|
||||
// Load/store size<<30 | 0x7<<27 | V<<26 | opc<<22 | imm12<<10 | Rn<<5 | Rt
|
||||
// LDST-unscaled size<<30 | 0x7<<27 | V<<26 | opc<<22 | 0<<12 | imm9<<5 | Rt (actually imm9<<12 | Rn<<5 | Rt)
|
||||
// LDST-pair opc<<30 | 0x5<<27 | V<<26 | L<<22 | imm7<<15 | Rt2<<10 | Rn<<5 | Rt
|
||||
// Branch-imm 0<<31 | 0x5<<26 | imm26 (B)
|
||||
// Branch-imm 1<<31 | 0x5<<26 | imm26 (BL)
|
||||
// Branch-cond 0x2A<<25 | imm19<<5 | cond (B.cond)
|
||||
// Uncond-branch 0x6B<<25 | opc<<21 | Rn<<5 | Rd (BR/BLR/RET)
|
||||
// ADR/ADRP p<<31 | 0x10<<24 | immlo<<29 | immhi<<5 | Rd
|
||||
|
||||
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
||||
// R0–R30 (integer), F0–F31 (floating point), and the ABI aliases the
|
||||
// runtime's assembly uses. Returns -1 for an unrecognised name.
|
||||
func arm64RegNum(name string) int {
|
||||
switch name {
|
||||
case "R0":
|
||||
return 0
|
||||
case "R1":
|
||||
return 1
|
||||
case "R2":
|
||||
return 2
|
||||
case "R3":
|
||||
return 3
|
||||
case "R4":
|
||||
return 4
|
||||
case "R5":
|
||||
return 5
|
||||
case "R6":
|
||||
return 6
|
||||
case "R7":
|
||||
return 7
|
||||
case "R8":
|
||||
return 8
|
||||
case "R9":
|
||||
return 9
|
||||
case "R10":
|
||||
return 10
|
||||
case "R11":
|
||||
return 11
|
||||
case "R12":
|
||||
return 12
|
||||
case "R13":
|
||||
return 13
|
||||
case "R14":
|
||||
return 14
|
||||
case "R15":
|
||||
return 15
|
||||
case "R16":
|
||||
return 16
|
||||
case "R17":
|
||||
return 17
|
||||
case "R18":
|
||||
return 18
|
||||
case "R19":
|
||||
return 19
|
||||
case "R20":
|
||||
return 20
|
||||
case "R21":
|
||||
return 21
|
||||
case "R22":
|
||||
return 22
|
||||
case "R23":
|
||||
return 23
|
||||
case "R24":
|
||||
return 24
|
||||
case "R25":
|
||||
return 25
|
||||
case "R26", "REGCTXT", "CTXT":
|
||||
return 26
|
||||
case "R27", "REGTMP", "TMP":
|
||||
return 27
|
||||
case "R28", "REGG", "g":
|
||||
return 28
|
||||
case "R29", "FP":
|
||||
return 29
|
||||
case "R30", "LR", "LINK":
|
||||
return 30
|
||||
case "R31", "ZR":
|
||||
return 31
|
||||
case "SP":
|
||||
return 31 // SP and ZR share encoding 31; context determines meaning
|
||||
}
|
||||
// F0–F31.
|
||||
if len(name) >= 1 && name[0] == 'F' {
|
||||
n := 0
|
||||
for i := 1; i < len(name); i++ {
|
||||
if name[i] < '0' || name[i] > '9' {
|
||||
return -1
|
||||
}
|
||||
n = n*10 + int(name[i]-'0')
|
||||
}
|
||||
if n <= 31 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ---- format helpers ----
|
||||
|
||||
// a64wordLE encodes a uint32 as 4 little-endian bytes.
|
||||
func a64wordLE(w uint32) []byte {
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||
}
|
||||
|
||||
// a64WordsLE concatenates one or more instruction words as little-endian bytes.
|
||||
func a64WordsLE(ws ...uint32) []byte {
|
||||
var out []byte
|
||||
for _, w := range ws {
|
||||
out = append(out, a64wordLE(w)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- data-processing (immediate) ----
|
||||
|
||||
// a64AddSub encodes an ADD/SUB (immediate) instruction:
|
||||
// sf<<31 | op<<30 | S<<29 | 0x11<<24 | sh<<22 | imm12<<10 | Rn<<5 | Rd.
|
||||
func a64AddSub(sf, op, S, sh, imm12, rn, rd uint32) uint32 {
|
||||
return sf<<31 | op<<30 | S<<29 | 0x11<<24 | sh<<22 | imm12<<10 | rn<<5 | rd
|
||||
}
|
||||
|
||||
// ---- move wide ----
|
||||
|
||||
// a64MoveWide encodes a MOVZ/MOVK/MOVN instruction:
|
||||
// sf<<31 | opc<<29 | 0x25<<23 | hw<<21 | imm16<<5 | Rd.
|
||||
func a64MoveWide(sf, opc, hw, imm16, rd uint32) uint32 {
|
||||
return sf<<31 | opc<<29 | 0x25<<23 | hw<<21 | imm16<<5 | rd
|
||||
}
|
||||
|
||||
// ---- load/store (unsigned immediate, scaled) ----
|
||||
|
||||
// a64LSU encodes a load/store register (unsigned immediate, scaled):
|
||||
// size<<30 | 0x39<<24 | V<<26 | opc<<22 | imm12<<10 | Rn<<5 | Rt.
|
||||
// (0x39<<24 encodes bits 29:24 = 111001, the scaled unsigned offset form.)
|
||||
func a64LSU(size, V, opc, imm12, rn, rt uint32) uint32 {
|
||||
return size<<30 | 0x39<<24 | V<<26 | opc<<22 | imm12<<10 | rn<<5 | rt
|
||||
}
|
||||
|
||||
// ---- load/store (unscaled immediate) ----
|
||||
|
||||
// a64LSUnscaled encodes a load/store register (unscaled immediate, 9-bit signed):
|
||||
// size<<30 | 0x7<<27 | V<<26 | opc<<22 | 0<<12 | imm9<<12 | Rn<<5 | Rt.
|
||||
// Note: the 0<<24 distinguishes unscaled from the pre/post-index forms.
|
||||
func a64LSUnscaled(size, V, opc int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | uint32(opc)<<22 |
|
||||
(uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// ---- load/store pair ----
|
||||
|
||||
// a64LSP encodes a load/store pair instruction (signed offset):
|
||||
// opc<<30 | 0x5<<27 | V<<26 | 2<<23 | L<<22 | imm7<<15 | Rt2<<10 | Rn<<5 | Rt.
|
||||
// opc: 0=32-bit, 1=reserved, 2=64-bit. V: 0=integer, 1=FP/SIMD.
|
||||
// L: 0=store, 1=load. imm7 is the signed scaled offset (÷8 for 64-bit pairs).
|
||||
func a64LSP(opc, V, L uint32, imm7 int32, rt2, rn, rt uint32) uint32 {
|
||||
return opc<<30 | 5<<27 | V<<26 | 2<<23 | L<<22 | (uint32(imm7)&0x7F)<<15 | rt2<<10 | rn<<5 | rt
|
||||
}
|
||||
|
||||
// ---- branches ----
|
||||
|
||||
// a64Branch encodes an unconditional branch (B/BL):
|
||||
// op<<31 | 0x5<<26 | imm26.
|
||||
func a64Branch(op uint32, imm26 int32) uint32 {
|
||||
return op<<31 | 5<<26 | (uint32(imm26) & 0x03FFFFFF)
|
||||
}
|
||||
|
||||
// a64BranchCond encodes a conditional branch (B.cond):
|
||||
// 0x2A<<25 | imm19<<5 | cond.
|
||||
func a64BranchCond(imm19 int32, cond uint32) uint32 {
|
||||
return 0x2A<<25 | (uint32(imm19)&0x7FFFF)<<5 | cond&0xF
|
||||
}
|
||||
|
||||
// a64UncondBranch encodes an unconditional branch register (BR/BLR/RET):
|
||||
// 0x6B<<25 | opc<<21 | 0x1F<<16 | Rn<<5 | Rd.
|
||||
// opc: 0=BR, 1=BLR, 2=RET. For RET, Rn defaults to LR(30).
|
||||
func a64UncondBranch(opc, rn, rd uint32) uint32 {
|
||||
return 0x6B<<25 | opc<<21 | 0x1F<<16 | rn<<5 | rd
|
||||
}
|
||||
|
||||
// ---- ADR/ADRP ----
|
||||
|
||||
// a64ADR encodes an ADR instruction (p=0) or ADRP instruction (p=1):
|
||||
// p<<31 | immlo<<29 | 0x10<<24 | immhi<<5 | Rd.
|
||||
func a64ADR(p uint32, immhi int32, immlo uint32, rd uint32) uint32 {
|
||||
return p<<31 | immlo<<29 | 0x10<<24 | (uint32(immhi)&0x7FFFF)<<5 | rd
|
||||
}
|
||||
|
||||
// ---- system ----
|
||||
|
||||
// a64NOP encodes a NOP: 0xd503201f.
|
||||
const a64NOP uint32 = 0xd503201f
|
||||
|
||||
// a64BRK encodes a BRK instruction: 0xd4200000 | imm16<<5.
|
||||
func a64BRK(imm16 uint32) uint32 {
|
||||
return 0xd4200000 | imm16<<5
|
||||
}
|
||||
|
||||
// ---- condition codes ----
|
||||
|
||||
const (
|
||||
a64CondEQ = 0x0
|
||||
a64CondNE = 0x1
|
||||
a64CondCS = 0x2
|
||||
a64CondHS = 0x2
|
||||
a64CondCC = 0x3
|
||||
a64CondLO = 0x3
|
||||
a64CondMI = 0x4
|
||||
a64CondPL = 0x5
|
||||
a64CondVS = 0x6
|
||||
a64CondVC = 0x7
|
||||
a64CondHI = 0x8
|
||||
a64CondLS = 0x9
|
||||
a64CondGE = 0xa
|
||||
a64CondLT = 0xb
|
||||
a64CondGT = 0xc
|
||||
a64CondLE = 0xd
|
||||
)
|
||||
|
||||
// arm64CondMap maps Go assembler condition mnemonics to AArch64 condition codes.
|
||||
var arm64CondMap = map[string]uint32{
|
||||
"EQ": a64CondEQ,
|
||||
"NE": a64CondNE,
|
||||
"CS": a64CondCS,
|
||||
"HS": a64CondHS,
|
||||
"CC": a64CondCC,
|
||||
"LO": a64CondLO,
|
||||
"MI": a64CondMI,
|
||||
"PL": a64CondPL,
|
||||
"VS": a64CondVS,
|
||||
"VC": a64CondVC,
|
||||
"HI": a64CondHI,
|
||||
"LS": a64CondLS,
|
||||
"GE": a64CondGE,
|
||||
"LT": a64CondLT,
|
||||
"GT": a64CondGT,
|
||||
"LE": a64CondLE,
|
||||
}
|
||||
|
||||
// ---- instruction format tags ----
|
||||
|
||||
type a64Format uint8
|
||||
|
||||
const (
|
||||
a64FDPSR a64Format = iota // data-processing (shifted register): ADD, SUB, AND, ORR, EOR, etc.
|
||||
a64FDPIR // data-processing (immediate): ADD/SUB $imm
|
||||
a64FLogImm // logical (immediate): AND/ORR/EOR $imm
|
||||
a64FMovWide // move wide: MOVZ, MOVN, MOVK
|
||||
a64FLSU // load/store (unsigned immediate, scaled)
|
||||
a64FLSUnscaled // load/store (unscaled immediate)
|
||||
a64FLSPair // load/store pair
|
||||
a64FBranch // unconditional branch (B/BL)
|
||||
a64FBranchCond // conditional branch (B.cond)
|
||||
a64FUncondBranch // unconditional branch register (BR/BLR/RET)
|
||||
a64FADR // ADR/ADRP
|
||||
a64FEXTR // EXTR
|
||||
a64FBitfield // bitfield: BFI/BFXIL/SBFM/UBFM/BFM
|
||||
a64FSystem // system: NOP, BRK, etc.
|
||||
a64FFP3 // FP 3-operand (Rm, Rn, Rd): FADD, FSUB, FMUL, FDIV, etc.
|
||||
a64FFPUnary // FP unary (Rn, Rd): FMOV, FABS, FNEG, FSQRT, FCVT, FRINT*
|
||||
a64FFP4 // FP 4-operand FMA (Ra, Rm, Rn, Rd): FMADD, FMSUB, etc.
|
||||
a64FFPCmp // FP compare (Rm, Rn): FCMP, FCMPE
|
||||
a64FFPCCmp // FP conditional compare (Rm, Rn, nzcv, cond): FCCMP, FCCMPE
|
||||
a64FFPCvt // FP↔integer conversion: FCVTZS, SCVTF, etc.
|
||||
a64FFPSel // FP conditional select (Rm, Rn, Rd, cond): FCSEL
|
||||
a64FFMovGR // FMOV between GP and FP registers
|
||||
a64FCRC32 // CRC32
|
||||
a64FCSEL // conditional select: CSEL, CSINC, CSINV, CSNEG
|
||||
a64FExcl // exclusive load/store: LDXR, STXR, LDAXR, STLXR
|
||||
a64FLSE // LSE atomics: LDADD, CAS, SWP
|
||||
a64FSIMD3 // SIMD 3-operand: VADD, VSUB, VMUL
|
||||
)
|
||||
|
||||
// a64Enc is one instruction's encoding: its bit layout (format) and the
|
||||
// opcode constant, positioned at its exact bit range.
|
||||
type a64Enc struct {
|
||||
format a64Format
|
||||
op uint32 // the pre-positioned opcode bits
|
||||
}
|
||||
|
||||
// a64InstrTable maps AArch64 mnemonics (as the Go assembler spells them) to
|
||||
// their encoding. The base integer, memory, floating-point and SIMD
|
||||
// instruction sets are covered.
|
||||
var a64InstrTable = map[string]a64Enc{}
|
||||
|
||||
func init() {
|
||||
// ---- data-processing (shifted register) ----
|
||||
// Format: sf<<31 | op<<30 | S<<29 | 0x0b<<24 | shift<<22 | Rm<<16 | imm6<<10 | Rn<<5 | Rd
|
||||
dpsr := map[string]uint32{
|
||||
// Add/Sub
|
||||
"ADD": 1<<31 | 0<<30 | 0<<29 | 0x0b<<24, // sf=1, op=0, S=0 (64-bit default)
|
||||
"ADDW": 0<<31 | 0<<30 | 0<<29 | 0x0b<<24, // sf=0
|
||||
"ADDS": 1<<31 | 0<<30 | 1<<29 | 0x0b<<24,
|
||||
"ADDSW": 0<<31 | 0<<30 | 1<<29 | 0x0b<<24,
|
||||
"SUB": 1<<31 | 1<<30 | 0<<29 | 0x0b<<24,
|
||||
"SUBW": 0<<31 | 1<<30 | 0<<29 | 0x0b<<24,
|
||||
"SUBS": 1<<31 | 1<<30 | 1<<29 | 0x0b<<24,
|
||||
"SUBSW": 0<<31 | 1<<30 | 1<<29 | 0x0b<<24,
|
||||
// Logical (shifted register)
|
||||
"AND": 1<<31 | 0<<29 | 0x0a<<24,
|
||||
"ANDW": 0<<31 | 0<<29 | 0x0a<<24,
|
||||
"BIC": 1<<31 | 0<<29 | 0x0a<<24 | 1<<21,
|
||||
"BICW": 0<<31 | 0<<29 | 0x0a<<24 | 1<<21,
|
||||
"ORR": 1<<31 | 1<<29 | 0x0a<<24,
|
||||
"ORRW": 0<<31 | 1<<29 | 0x0a<<24,
|
||||
"ORN": 1<<31 | 1<<29 | 0x0a<<24 | 1<<21,
|
||||
"ORNW": 0<<31 | 1<<29 | 0x0a<<24 | 1<<21,
|
||||
"EOR": 1<<31 | 2<<29 | 0x0a<<24,
|
||||
"EORW": 0<<31 | 2<<29 | 0x0a<<24,
|
||||
"EON": 1<<31 | 2<<29 | 0x0a<<24 | 1<<21,
|
||||
"EONW": 0<<31 | 2<<29 | 0x0a<<24 | 1<<21,
|
||||
"ANDS": 1<<31 | 3<<29 | 0x0a<<24,
|
||||
"ANDSW": 0<<31 | 3<<29 | 0x0a<<24,
|
||||
"BICS": 1<<31 | 3<<29 | 0x0a<<24 | 1<<21,
|
||||
"BICSW": 0<<31 | 3<<29 | 0x0a<<24 | 1<<21,
|
||||
// Shift
|
||||
"LSL": 1<<31 | 0<<29 | 0x0a<<24, // alias of UBFM
|
||||
"LSLW": 0<<31 | 0<<29 | 0x0a<<24,
|
||||
"LSR": 1<<31 | 0<<29 | 0x0a<<24,
|
||||
"LSRW": 0<<31 | 0<<29 | 0x0a<<24,
|
||||
"ASR": 1<<31 | 0<<29 | 0x0a<<24,
|
||||
"ASRW": 0<<31 | 0<<29 | 0x0a<<24,
|
||||
"ROR": 1<<31 | 0<<29 | 0x0a<<24,
|
||||
"RORW": 0<<31 | 0<<29 | 0x0a<<24,
|
||||
// Multiply
|
||||
"MADD": 1<<31 | 0<<29 | 0x1b<<24 | 0<<21,
|
||||
"MADDW": 0<<31 | 0<<29 | 0x1b<<24 | 0<<21,
|
||||
"MSUB": 1<<31 | 0<<29 | 0x1b<<24 | 1<<21,
|
||||
"MSUBW": 0<<31 | 0<<29 | 0x1b<<24 | 1<<21,
|
||||
// Divide
|
||||
"SDIV": 1<<31 | 0<<29 | 0x0d<<24,
|
||||
"SDIVW": 0<<31 | 0<<29 | 0x0d<<24,
|
||||
"UDIV": 1<<31 | 0<<29 | 0x0d<<24 | 1<<10,
|
||||
"UDIVW": 0<<31 | 0<<29 | 0x0d<<24 | 1<<10,
|
||||
// CRC
|
||||
"CRC32B": 0<<31 | 0<<29 | 0x1b<<24 | 4<<10,
|
||||
"CRC32H": 0<<31 | 0<<29 | 0x1b<<24 | 5<<10,
|
||||
"CRC32W": 0<<31 | 0<<29 | 0x1b<<24 | 6<<10,
|
||||
"CRC32X": 1<<31 | 0<<29 | 0x1b<<24 | 7<<10,
|
||||
// Conditional select
|
||||
"CSEL": 1<<31 | 0<<29 | 0x1d<<24 | 0<<10,
|
||||
"CSELW": 0<<31 | 0<<29 | 0x1d<<24 | 0<<10,
|
||||
"CSINC": 1<<31 | 0<<29 | 0x1d<<24 | 1<<10,
|
||||
"CSINCW": 0<<31 | 0<<29 | 0x1d<<24 | 1<<10,
|
||||
"CSINV": 1<<31 | 0<<29 | 0x1d<<24 | 2<<10,
|
||||
"CSINVW": 0<<31 | 0<<29 | 0x1d<<24 | 2<<10,
|
||||
"CSNEG": 1<<31 | 0<<29 | 0x1d<<24 | 3<<10,
|
||||
"CSNEGW": 0<<31 | 0<<29 | 0x1d<<24 | 3<<10,
|
||||
}
|
||||
for m, op := range dpsr {
|
||||
a64InstrTable[m] = a64Enc{format: a64FDPSR, op: op}
|
||||
}
|
||||
|
||||
// Aliases that map to the same encoding as their target.
|
||||
a64InstrTable["CMP"] = a64Enc{format: a64FDPSR, op: dpsr["SUBS"]}
|
||||
a64InstrTable["CMPW"] = a64Enc{format: a64FDPSR, op: dpsr["SUBSW"]}
|
||||
a64InstrTable["CMN"] = a64Enc{format: a64FDPSR, op: dpsr["ADDS"]}
|
||||
a64InstrTable["CMNW"] = a64Enc{format: a64FDPSR, op: dpsr["ADDSW"]}
|
||||
a64InstrTable["TST"] = a64Enc{format: a64FDPSR, op: dpsr["ANDS"]}
|
||||
a64InstrTable["TSTW"] = a64Enc{format: a64FDPSR, op: dpsr["ANDSW"]}
|
||||
a64InstrTable["NEG"] = a64Enc{format: a64FDPSR, op: dpsr["SUB"]}
|
||||
a64InstrTable["NEGW"] = a64Enc{format: a64FDPSR, op: dpsr["SUBW"]}
|
||||
a64InstrTable["NEGS"] = a64Enc{format: a64FDPSR, op: dpsr["SUBS"]}
|
||||
a64InstrTable["MVN"] = a64Enc{format: a64FDPSR, op: dpsr["ORN"]}
|
||||
a64InstrTable["MVNW"] = a64Enc{format: a64FDPSR, op: dpsr["ORNW"]}
|
||||
a64InstrTable["MOV"] = a64Enc{format: a64FDPSR, op: dpsr["ORR"]}
|
||||
a64InstrTable["MOVW"] = a64Enc{format: a64FDPSR, op: dpsr["ORRW"]}
|
||||
|
||||
// ---- data-processing (immediate) ----
|
||||
// ADD/SUB $imm, Rn, Rd
|
||||
a64InstrTable["ADDImm"] = a64Enc{format: a64FDPIR, op: 1<<31 | 0<<30 | 0<<29 | 0x11<<24}
|
||||
a64InstrTable["ADDWImm"] = a64Enc{format: a64FDPIR, op: 0<<31 | 0<<30 | 0<<29 | 0x11<<24}
|
||||
a64InstrTable["SUBImm"] = a64Enc{format: a64FDPIR, op: 1<<31 | 1<<30 | 0<<29 | 0x11<<24}
|
||||
a64InstrTable["SUBWImm"] = a64Enc{format: a64FDPIR, op: 0<<31 | 1<<30 | 0<<29 | 0x11<<24}
|
||||
a64InstrTable["ADDSImm"] = a64Enc{format: a64FDPIR, op: 1<<31 | 0<<30 | 1<<29 | 0x11<<24}
|
||||
a64InstrTable["SUBSImm"] = a64Enc{format: a64FDPIR, op: 1<<31 | 1<<30 | 1<<29 | 0x11<<24}
|
||||
|
||||
// ---- move wide ----
|
||||
// MOVZ/MOVN/MOVK
|
||||
a64InstrTable["MOVZ"] = a64Enc{format: a64FMovWide, op: 1<<31 | 2<<29 | 0x25<<23}
|
||||
a64InstrTable["MOVZW"] = a64Enc{format: a64FMovWide, op: 0<<31 | 2<<29 | 0x25<<23}
|
||||
a64InstrTable["MOVN"] = a64Enc{format: a64FMovWide, op: 1<<31 | 0<<29 | 0x25<<23}
|
||||
a64InstrTable["MOVNW"] = a64Enc{format: a64FMovWide, op: 0<<31 | 0<<29 | 0x25<<23}
|
||||
a64InstrTable["MOVK"] = a64Enc{format: a64FMovWide, op: 1<<31 | 3<<29 | 0x25<<23}
|
||||
a64InstrTable["MOVKW"] = a64Enc{format: a64FMovWide, op: 0<<31 | 3<<29 | 0x25<<23}
|
||||
|
||||
// ---- ADR/ADRP ----
|
||||
a64InstrTable["ADR"] = a64Enc{format: a64FADR, op: 0}
|
||||
a64InstrTable["ADRP"] = a64Enc{format: a64FADR, op: 1}
|
||||
|
||||
// ---- load/store (unsigned immediate) ----
|
||||
a64InstrTable["MOVD"] = a64Enc{format: a64FLSU, op: 3<<30 | 7<<27 | 1<<22} // LDR 64-bit
|
||||
a64InstrTable["MOVWU"] = a64Enc{format: a64FLSU, op: 2<<30 | 7<<27 | 1<<22} // LDR 32-bit unsigned
|
||||
a64InstrTable["MOVHU"] = a64Enc{format: a64FLSU, op: 1<<30 | 7<<27 | 1<<22} // LDRH unsigned
|
||||
a64InstrTable["MOVBU"] = a64Enc{format: a64FLSU, op: 0<<30 | 7<<27 | 1<<22} // LDRB unsigned
|
||||
a64InstrTable["MOVW"] = a64Enc{format: a64FLSU, op: 2<<30 | 7<<27 | 2<<22} // LDRSW (signed 32→64)
|
||||
a64InstrTable["MOVH"] = a64Enc{format: a64FLSU, op: 1<<30 | 7<<27 | 2<<22} // LDRSH (signed half)
|
||||
a64InstrTable["MOVB"] = a64Enc{format: a64FLSU, op: 0<<30 | 7<<27 | 2<<22} // LDRSB (signed byte)
|
||||
a64InstrTable["FMOVS"] = a64Enc{format: a64FLSU, op: 2<<30 | 7<<27 | 1<<26 | 1<<22} // FLDR 32-bit FP
|
||||
a64InstrTable["FMOVD"] = a64Enc{format: a64FLSU, op: 3<<30 | 7<<27 | 1<<26 | 1<<22} // FLDR 64-bit FP
|
||||
|
||||
// Store opcodes (load ^ (1<<22)):
|
||||
// STR 64-bit: size=3, V=0, opc=00 → 3<<30 | 7<<27 | 0<<22
|
||||
// STR 32-bit: size=2, V=0, opc=00 → 2<<30 | 7<<27 | 0<<22
|
||||
// STRH: size=1, V=0, opc=00 → 1<<30 | 7<<27 | 0<<22
|
||||
// STRB: size=0, V=0, opc=00 → 0<<30 | 7<<27 | 0<<22
|
||||
|
||||
// ---- branches ----
|
||||
a64InstrTable["B"] = a64Enc{format: a64FBranch, op: 0<<31 | 5<<26}
|
||||
a64InstrTable["BL"] = a64Enc{format: a64FBranch, op: 1<<31 | 5<<26}
|
||||
|
||||
// Conditional branches.
|
||||
condBranches := map[string]uint32{
|
||||
"BEQ": 0x0, "BNE": 0x1, "BCS": 0x2, "BHS": 0x2,
|
||||
"BCC": 0x3, "BLO": 0x3, "BMI": 0x4, "BPL": 0x5,
|
||||
"BVS": 0x6, "BVC": 0x7, "BHI": 0x8, "BLS": 0x9,
|
||||
"BGE": 0xa, "BLT": 0xb, "BGT": 0xc, "BLE": 0xd,
|
||||
}
|
||||
for name, cond := range condBranches {
|
||||
a64InstrTable[name] = a64Enc{format: a64FBranchCond, op: 0x2A<<25 | cond}
|
||||
}
|
||||
|
||||
// Unconditional branch register (BR/BLR/RET).
|
||||
a64InstrTable["BR"] = a64Enc{format: a64FUncondBranch, op: 0x6B<<25 | 0<<21}
|
||||
a64InstrTable["BLR"] = a64Enc{format: a64FUncondBranch, op: 0x6B<<25 | 1<<21}
|
||||
a64InstrTable["RET"] = a64Enc{format: a64FUncondBranch, op: 0x6B<<25 | 2<<21}
|
||||
|
||||
// ---- system ----
|
||||
a64InstrTable["NOP"] = a64Enc{format: a64FSystem, op: a64NOP}
|
||||
a64InstrTable["NOOP"] = a64Enc{format: a64FSystem, op: a64NOP}
|
||||
a64InstrTable["BRK"] = a64Enc{format: a64FSystem, op: 0xd4200000}
|
||||
a64InstrTable["UNDEF"] = a64Enc{format: a64FSystem, op: a64BRK(0)}
|
||||
|
||||
// ---- EXTR ----
|
||||
a64InstrTable["EXTR"] = a64Enc{format: a64FEXTR, op: 1<<31 | 0x27<<23 | 1<<22}
|
||||
a64InstrTable["EXTRW"] = a64Enc{format: a64FEXTR, op: 0<<31 | 0x27<<23 | 0<<22}
|
||||
|
||||
// ---- bitfield ----
|
||||
a64InstrTable["BFM"] = a64Enc{format: a64FBitfield, op: 1<<31 | 1<<29 | 0x26<<23 | 1<<22}
|
||||
a64InstrTable["BFMW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 1<<29 | 0x26<<23 | 0<<22}
|
||||
a64InstrTable["SBFM"] = a64Enc{format: a64FBitfield, op: 1<<31 | 0<<29 | 0x26<<23 | 1<<22}
|
||||
a64InstrTable["SBFMW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 0<<29 | 0x26<<23 | 0<<22}
|
||||
a64InstrTable["UBFM"] = a64Enc{format: a64FBitfield, op: 1<<31 | 2<<29 | 0x26<<23 | 1<<22}
|
||||
a64InstrTable["UBFMW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 2<<29 | 0x26<<23 | 0<<22}
|
||||
a64InstrTable["BFI"] = a64Enc{format: a64FBitfield, op: 1<<31 | 2<<29 | 0x26<<23 | 1<<22}
|
||||
a64InstrTable["BFIW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 2<<29 | 0x26<<23 | 0<<22}
|
||||
a64InstrTable["BFXIL"] = a64Enc{format: a64FBitfield, op: 1<<31 | 1<<29 | 0x26<<23 | 1<<22}
|
||||
a64InstrTable["BFXILW"] = a64Enc{format: a64FBitfield, op: 0<<31 | 1<<29 | 0x26<<23 | 0<<22}
|
||||
|
||||
// ---- FP 3-operand (Rm, Rn, Rd): FADD, FSUB, FMUL, FDIV, FMAX, FMIN, FNMUL ----
|
||||
fp3 := map[string]uint32{
|
||||
"FADDS": 0x1e202800, "FADDD": 0x1e602800,
|
||||
"FSUBS": 0x1e203800, "FSUBD": 0x1e603800,
|
||||
"FMULS": 0x1e200800, "FMULD": 0x1e600800,
|
||||
"FDIVS": 0x1e201800, "FDIVD": 0x1e601800,
|
||||
"FMAXS": 0x1e204800, "FMAXD": 0x1e604800,
|
||||
"FMINS": 0x1e205800, "FMIND": 0x1e605800,
|
||||
"FMAXNMS": 0x1e206800, "FMAXNMD": 0x1e606800,
|
||||
"FMINNMS": 0x1e207800, "FMINNMD": 0x1e607800,
|
||||
"FNMULS": 0x1e208800, "FNMULD": 0x1e608800,
|
||||
}
|
||||
for m, op := range fp3 {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFP3, op: op}
|
||||
}
|
||||
|
||||
// ---- FP unary (Rn, Rd): FMOV reg-reg, FABS, FNEG, FSQRT, FCVT, FRINT* ----
|
||||
fp1 := map[string]uint32{
|
||||
"FMOVS": 0x1e204000, "FMOVD": 0x1e604000,
|
||||
"FABSS": 0x1e20c000, "FABSD": 0x1e60c000,
|
||||
"FNEGS": 0x1e214000, "FNEGD": 0x1e614000,
|
||||
"FSQRTS": 0x1e21c000, "FSQRTD": 0x1e61c000,
|
||||
"FCVTSD": 0x1e22c000, "FCVTDS": 0x1e624000,
|
||||
"FRINTNS": 0x1e244000, "FRINTND": 0x1e644000,
|
||||
"FRINTPS": 0x1e24c000, "FRINTPD": 0x1e64c000,
|
||||
"FRINTMS": 0x1e254000, "FRINTMD": 0x1e654000,
|
||||
"FRINTZS": 0x1e25c000, "FRINTZD": 0x1e65c000,
|
||||
"FRINTAS": 0x1e264000, "FRINTAD": 0x1e664000,
|
||||
"FRINTXS": 0x1e274000, "FRINTXD": 0x1e674000,
|
||||
"FRINTIS": 0x1e27c000, "FRINTID": 0x1e67c000,
|
||||
}
|
||||
for m, op := range fp1 {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFPUnary, op: op}
|
||||
}
|
||||
|
||||
// ---- FP 4-operand FMA (Ra, Rm, Rn, Rd) ----
|
||||
fp4 := map[string]uint32{
|
||||
"FMADDS": 0x1f000000, "FMADDD": 0x1f400000,
|
||||
"FMSUBS": 0x1f008000, "FMSUBD": 0x1f408000,
|
||||
"FNMADDS": 0x1f200000, "FNMADDD": 0x1f600000,
|
||||
"FNMSUBS": 0x1f208000, "FNMSUBD": 0x1f608000,
|
||||
}
|
||||
for m, op := range fp4 {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFP4, op: op}
|
||||
}
|
||||
|
||||
// ---- FP compare (Rm, Rn or #0, Rn) ----
|
||||
fpcmp := map[string]uint32{
|
||||
"FCMPS": 0x1e202000, "FCMPD": 0x1e602000,
|
||||
"FCMPES": 0x1e202010, "FCMPED": 0x1e602010,
|
||||
}
|
||||
for m, op := range fpcmp {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFPCmp, op: op}
|
||||
}
|
||||
|
||||
// ---- FP conditional compare (Rm, Rn, #nzcv, cond) ----
|
||||
fpccmp := map[string]uint32{
|
||||
"FCCMPS": 0x1e200400, "FCCMPD": 0x1e600400,
|
||||
"FCCMPES": 0x1e200410, "FCCMPED": 0x1e600410,
|
||||
}
|
||||
for m, op := range fpccmp {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFPCCmp, op: op}
|
||||
}
|
||||
|
||||
// ---- FP conditional select (Rm, Rn, Rd, cond) ----
|
||||
a64InstrTable["FCSELS"] = a64Enc{format: a64FFPSel, op: 0x1e200c00}
|
||||
a64InstrTable["FCSELD"] = a64Enc{format: a64FFPSel, op: 0x1e600c00}
|
||||
|
||||
// ---- FP ↔ integer conversion ----
|
||||
fpcvt := map[string]uint32{
|
||||
"FCVTZSD": 0x9e780000, "FCVTZSDW": 0x1e780000,
|
||||
"FCVTZSS": 0x9e380000, "FCVTZSSW": 0x1e380000,
|
||||
"FCVTZUD": 0x9e790000, "FCVTZUDW": 0x1e790000,
|
||||
"FCVTZUS": 0x9e390000, "FCVTZUSW": 0x1e390000,
|
||||
"SCVTFD": 0x9e620000, "SCVTFS": 0x9e220000,
|
||||
"SCVTFWD": 0x1e620000, "SCVTFWS": 0x1e220000,
|
||||
"UCVTFD": 0x9e630000, "UCVTFS": 0x9e230000,
|
||||
"UCVTFWD": 0x1e630000, "UCVTFWS": 0x1e230000,
|
||||
}
|
||||
for m, op := range fpcvt {
|
||||
a64InstrTable[m] = a64Enc{format: a64FFPCvt, op: op}
|
||||
}
|
||||
|
||||
// ---- FMOV between GP and FP registers ----
|
||||
a64InstrTable["FMOVGR"] = a64Enc{format: a64FFMovGR, op: 0x1e260000} // placeholder, actual encoding depends on direction
|
||||
|
||||
// ---- conditional select: CSEL, CSINC, CSINV, CSNEG ----
|
||||
csel := map[string]uint32{
|
||||
"CSEL": 0x9a800000, "CSELW": 0x1a800000,
|
||||
"CSINC": 0x9a800400, "CSINCW": 0x1a800400,
|
||||
"CSINV": 0xda800000, "CSINVW": 0x5a800000,
|
||||
"CSNEG": 0xda800400, "CSNEGW": 0x5a800400,
|
||||
}
|
||||
for m, op := range csel {
|
||||
a64InstrTable[m] = a64Enc{format: a64FCSEL, op: op}
|
||||
}
|
||||
// Aliases
|
||||
a64InstrTable["CSET"] = a64Enc{format: a64FCSEL, op: 0x9a800400}
|
||||
a64InstrTable["CSETW"] = a64Enc{format: a64FCSEL, op: 0x1a800400}
|
||||
a64InstrTable["CSETM"] = a64Enc{format: a64FCSEL, op: 0xda800000}
|
||||
a64InstrTable["CSETMW"] = a64Enc{format: a64FCSEL, op: 0x5a800000}
|
||||
a64InstrTable["CINC"] = a64Enc{format: a64FCSEL, op: 0x9a800400}
|
||||
a64InstrTable["CINCW"] = a64Enc{format: a64FCSEL, op: 0x1a800400}
|
||||
a64InstrTable["CINV"] = a64Enc{format: a64FCSEL, op: 0xda800000}
|
||||
a64InstrTable["CINVW"] = a64Enc{format: a64FCSEL, op: 0x5a800000}
|
||||
a64InstrTable["CNEG"] = a64Enc{format: a64FCSEL, op: 0xda800400}
|
||||
a64InstrTable["CNEGW"] = a64Enc{format: a64FCSEL, op: 0x5a800400}
|
||||
|
||||
// ---- CRC32 ----
|
||||
crc32 := map[string]uint32{
|
||||
"CRC32B": 0x1ac04000, "CRC32H": 0x1ac04400,
|
||||
"CRC32W": 0x1ac04800, "CRC32X": 0x9ac04c00,
|
||||
"CRC32CB": 0x1ac05000, "CRC32CH": 0x1ac05400,
|
||||
"CRC32CW": 0x1ac05800, "CRC32CX": 0x9ac05c00,
|
||||
}
|
||||
for m, op := range crc32 {
|
||||
a64InstrTable[m] = a64Enc{format: a64FCRC32, op: op}
|
||||
}
|
||||
|
||||
// ---- exclusive load/store ----
|
||||
a64InstrTable["LDXR"] = a64Enc{format: a64FExcl, op: 0xc85f7c00}
|
||||
a64InstrTable["LDXRB"] = a64Enc{format: a64FExcl, op: 0x085f7c00}
|
||||
a64InstrTable["LDXRH"] = a64Enc{format: a64FExcl, op: 0x485f7c00}
|
||||
a64InstrTable["LDXRW"] = a64Enc{format: a64FExcl, op: 0x885f7c00}
|
||||
a64InstrTable["LDAXR"] = a64Enc{format: a64FExcl, op: 0xc85ffc00}
|
||||
a64InstrTable["LDAXRB"] = a64Enc{format: a64FExcl, op: 0x085ffc00}
|
||||
a64InstrTable["LDAXRH"] = a64Enc{format: a64FExcl, op: 0x485ffc00}
|
||||
a64InstrTable["LDAXRW"] = a64Enc{format: a64FExcl, op: 0x885ffc00}
|
||||
a64InstrTable["STXR"] = a64Enc{format: a64FExcl, op: 0xc8007c00}
|
||||
a64InstrTable["STXRB"] = a64Enc{format: a64FExcl, op: 0x08007c00}
|
||||
a64InstrTable["STXRH"] = a64Enc{format: a64FExcl, op: 0x48007c00}
|
||||
a64InstrTable["STXRW"] = a64Enc{format: a64FExcl, op: 0x88007c00}
|
||||
a64InstrTable["STLXR"] = a64Enc{format: a64FExcl, op: 0xc800fc00}
|
||||
a64InstrTable["STLXRB"] = a64Enc{format: a64FExcl, op: 0x0800fc00}
|
||||
a64InstrTable["STLXRH"] = a64Enc{format: a64FExcl, op: 0x4800fc00}
|
||||
a64InstrTable["STLXRW"] = a64Enc{format: a64FExcl, op: 0x8800fc00}
|
||||
|
||||
// ---- LSE atomics ----
|
||||
a64InstrTable["LDADDD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x1c1<<21 | 0x00<<10}
|
||||
a64InstrTable["LDADDW"] = a64Enc{format: a64FLSE, op: 2<<30 | 0x1c1<<21 | 0x00<<10}
|
||||
a64InstrTable["LDADDB"] = a64Enc{format: a64FLSE, op: 0<<30 | 0x1c1<<21 | 0x00<<10}
|
||||
a64InstrTable["LDADDH"] = a64Enc{format: a64FLSE, op: 1<<30 | 0x1c1<<21 | 0x00<<10}
|
||||
a64InstrTable["CASD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x45<<21 | 0x1f<<10}
|
||||
a64InstrTable["CASW"] = a64Enc{format: a64FLSE, op: 2<<30 | 0x45<<21 | 0x1f<<10}
|
||||
a64InstrTable["SWPD"] = a64Enc{format: a64FLSE, op: 3<<30 | 0x1c1<<21 | 0x20<<10}
|
||||
a64InstrTable["SWPW"] = a64Enc{format: a64FLSE, op: 2<<30 | 0x1c1<<21 | 0x20<<10}
|
||||
|
||||
// ---- SIMD basics ----
|
||||
a64InstrTable["VADD"] = a64Enc{format: a64FSIMD3, op: 0x0e208400}
|
||||
a64InstrTable["VSUB"] = a64Enc{format: a64FSIMD3, op: 0x2e208400}
|
||||
a64InstrTable["VMUL"] = a64Enc{format: a64FSIMD3, op: 0x0e209c00}
|
||||
}
|
||||
|
||||
// ---- load/store helper tables ----
|
||||
|
||||
// a64LSType describes the load/store parameters for a MOV width mnemonic.
|
||||
type a64LSType struct {
|
||||
size int // 0=byte, 1=half, 2=word, 3=dword
|
||||
V int // 0=integer, 1=FP
|
||||
opc int // 00=store/unsigned load, 01=store FP, 10=signed load, 11=load FP
|
||||
}
|
||||
|
||||
// a64LoadTable maps MOV width mnemonics to their load/store encoding parameters.
|
||||
// For loads, opc selects signed vs unsigned; for stores, we flip the opc.
|
||||
var a64LoadTable = map[string]a64LSType{
|
||||
"MOVD": {3, 0, 1}, // LDR X (64-bit, unsigned offset)
|
||||
"MOVWU": {2, 0, 1}, // LDR W (32-bit unsigned)
|
||||
"MOVW": {2, 0, 2}, // LDRSW (32-bit signed → 64-bit)
|
||||
"MOVHU": {1, 0, 1}, // LDRH (16-bit unsigned)
|
||||
"MOVH": {1, 0, 2}, // LDRSH (16-bit signed)
|
||||
"MOVBU": {0, 0, 1}, // LDRB (8-bit unsigned)
|
||||
"MOVB": {0, 0, 2}, // LDRSB (8-bit signed)
|
||||
"FMOVS": {2, 1, 1}, // LDR S (32-bit FP)
|
||||
"FMOVD": {3, 1, 1}, // LDR D (64-bit FP)
|
||||
}
|
||||
|
||||
// a64StoreOpc returns the store opc for a given load type.
|
||||
// For integer: store opc = 00 (the load opc bits cleared).
|
||||
// For FP: store opc = 00 (same pattern).
|
||||
func a64StoreOpc(t a64LSType) int {
|
||||
if t.V == 1 {
|
||||
return 0 // FP store
|
||||
}
|
||||
return 0 // integer store
|
||||
}
|
||||
|
||||
// arm64RegClass discriminates integer (R), floating-point (F) registers for
|
||||
// the MOV pseudo-instruction.
|
||||
type arm64RegClass int
|
||||
|
||||
const (
|
||||
arm64ClsNone arm64RegClass = iota
|
||||
arm64ClsGR
|
||||
arm64ClsFP
|
||||
)
|
||||
|
||||
// arm64RegClassOf reports the register class of a register operand name.
|
||||
func arm64RegClassOf(name string) arm64RegClass {
|
||||
switch {
|
||||
case name == "":
|
||||
return arm64ClsNone
|
||||
case len(name) >= 1 && name[0] == 'F':
|
||||
return arm64ClsFP
|
||||
default:
|
||||
return arm64ClsGR
|
||||
}
|
||||
}
|
||||
|
||||
// arm64Movcon returns the shift (in units of 16 bits) at which a non-zero
|
||||
// 16-bit chunk of v sits, or -1 if v cannot be represented as a single
|
||||
// MOVZ/MOVN immediate. This is the Go toolchain's movcon function.
|
||||
func arm64Movcon(v int64) int {
|
||||
for s := 0; s < 64; s += 16 {
|
||||
if (uint64(v) &^ (uint64(0xFFFF) << uint(s))) == 0 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
func TestArm64LDRSTREncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got uint32
|
||||
want uint32
|
||||
}{
|
||||
{"LDR X4, [SP, #56]", a64LSU(3, 0, 1, 7, 31, 4), 0xf9401fe4},
|
||||
{"STR X4, [SP, #64]", a64LSU(3, 0, 0, 8, 31, 4), 0xf90023e4},
|
||||
{"STR X5, [SP, #32]", a64LSU(3, 0, 0, 4, 31, 5), 0xf90013e5},
|
||||
{"LDR X6, [SP, #32]", a64LSU(3, 0, 1, 4, 31, 6), 0xf94013e6},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("%s: got %08x, want %08x", tt.name, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64PrologueEncoding(t *testing.T) {
|
||||
fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false}
|
||||
pro := arm64Prologue(fi)
|
||||
if len(pro) != 12 {
|
||||
t.Fatalf("prologue length: got %d, want 12", len(pro))
|
||||
}
|
||||
expected := []uint32{0xf81d0ffe, 0xf81f83fd, 0xd10023fd}
|
||||
for i, w := range leWords(pro) {
|
||||
if w != expected[i] {
|
||||
t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64EpilogueSmallEncoding(t *testing.T) {
|
||||
fi := arm64FrameInfo{autosize: 48, frame: 32, leaf: false}
|
||||
ret := arm64Return(fi)
|
||||
if len(ret) != 12 {
|
||||
t.Fatalf("epilogue length: got %d, want 12", len(ret))
|
||||
}
|
||||
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #48; RET
|
||||
expected := []uint32{0xf85f83fd, 0xf84307fe, 0xd65f03c0}
|
||||
for i, w := range leWords(ret) {
|
||||
if w != expected[i] {
|
||||
t.Errorf("epilogue word %d: got %08x, want %08x", i, w, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64LargeFrameEncoding(t *testing.T) {
|
||||
fi := arm64FrameInfo{autosize: 272, frame: 256, leaf: false}
|
||||
pro := arm64Prologue(fi)
|
||||
if len(pro) != 16 {
|
||||
t.Fatalf("prologue length: got %d, want 16", len(pro))
|
||||
}
|
||||
expected := []uint32{0xd10443f4, 0xa93ffa9d, 0x9100029f, 0xd10023fd}
|
||||
for i, w := range leWords(pro) {
|
||||
if w != expected[i] {
|
||||
t.Errorf("prologue word %d: got %08x, want %08x", i, w, expected[i])
|
||||
}
|
||||
}
|
||||
|
||||
epi := arm64Return(fi)
|
||||
if len(epi) != 12 {
|
||||
t.Fatalf("epilogue length: got %d, want 12", len(epi))
|
||||
}
|
||||
eexpected := []uint32{0xa97ffbfd, 0x910443ff, 0xd65f03c0}
|
||||
for i, w := range leWords(epi) {
|
||||
if w != eexpected[i] {
|
||||
t.Errorf("epilogue word %d: got %08x, want %08x", i, w, eexpected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64NoFrame(t *testing.T) {
|
||||
fi := arm64FrameInfo{autosize: 0, frame: 0, leaf: true}
|
||||
pro := arm64Prologue(fi)
|
||||
if len(pro) != 0 {
|
||||
t.Errorf("no-frame prologue: got %d bytes, want 0", len(pro))
|
||||
}
|
||||
ret := arm64Return(fi)
|
||||
if len(ret) != 4 {
|
||||
t.Fatalf("no-frame return: got %d bytes, want 4", len(ret))
|
||||
}
|
||||
if leWord(ret) != 0xd65f03c0 {
|
||||
t.Errorf("no-frame RET: got %08x, want d65f03c0", leWord(ret))
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64RegNum(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want int
|
||||
}{
|
||||
{"R0", 0}, {"R4", 4}, {"R29", 29}, {"R30", 30}, {"R31", 31},
|
||||
{"FP", 29}, {"LR", 30}, {"LINK", 30}, {"SP", 31}, {"ZR", 31},
|
||||
{"F0", 0}, {"F4", 4}, {"F31", 31},
|
||||
{"INVALID", -1}, {"X0", -1}, {"", -1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := arm64RegNum(tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("arm64RegNum(%q) = %d, want %d", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64ComputeFrame(t *testing.T) {
|
||||
src := "TEXT ·f(SB), NOSPLIT, $32-0\n\tADD\tR4, R5\n\tRET\n"
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
fi := arm64ComputeFrame(f.Decls[0].(*ast.Text))
|
||||
if fi.frame != 32 {
|
||||
t.Errorf("frame: got %d, want 32", fi.frame)
|
||||
}
|
||||
if fi.autosize != 48 { // 32+8=40, aligned to48
|
||||
t.Errorf("autosize: got %d, want 48", fi.autosize)
|
||||
}
|
||||
// ADD + RET with no CALL/BL → leaf
|
||||
if !fi.leaf {
|
||||
t.Error("expected leaf")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64IsLeaf(t *testing.T) {
|
||||
src := "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR4, R5\n\tRET\n"
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if !arm64IsLeaf(f.Decls[0].(*ast.Text)) {
|
||||
t.Error("expected leaf")
|
||||
}
|
||||
|
||||
src2 := "TEXT ·f(SB), NOSPLIT, $0-0\n\tBL\tother(SB)\n\tRET\n"
|
||||
f2, errs := parser.Parse("test_arm64.s", src2)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if arm64IsLeaf(f2.Decls[0].(*ast.Text)) {
|
||||
t.Error("expected non-leaf")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64Bitmask(t *testing.T) {
|
||||
tests := []struct {
|
||||
v uint64
|
||||
sf int
|
||||
N, immr, imms uint32
|
||||
ok bool
|
||||
}{
|
||||
{1, 1, 1, 0, 0, true}, // single bit at pos 0
|
||||
{2, 1, 1, 63, 0, true}, // single bit at pos 1 (immr = esize-1)
|
||||
{0, 1, 0, 0, 0, false}, // zero is not a bitmask
|
||||
{0xFFFFFFFFFFFFFFFF, 1, 0, 0, 0, false}, // all ones is not a bitmask
|
||||
{0x5555555555555555, 1, 0, 0, 0x3E, true}, // alternating bits (esize=2, ones=1)
|
||||
{0xFFFFFFFF00000000, 1, 1, 32, 31, true}, // upper 32 bits set (esize=64, ones=32)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("arm64Bitmask(%#x, %d): ok=%v, want %v", tt.v, tt.sf, ok, tt.ok)
|
||||
continue
|
||||
}
|
||||
if ok && (N != tt.N || immr != tt.immr || imms != tt.imms) {
|
||||
t.Errorf("arm64Bitmask(%#x, %d): N=%d immr=%d imms=%d, want N=%d immr=%d imms=%d",
|
||||
tt.v, tt.sf, N, immr, imms, tt.N, tt.immr, tt.imms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AssembleFile(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·simple(SB), NOSPLIT, $0-0
|
||||
MOV R4, R5
|
||||
ADD R4, R5, R6
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("got %d funcs, want 1", len(img.Funcs))
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Name != "simple" {
|
||||
t.Errorf("func name: got %q, want %q", fn.Name, "simple")
|
||||
}
|
||||
//3 instructions ×4 bytes =12
|
||||
if fn.Size != 12 {
|
||||
t.Errorf("func size: got %d, want 12", fn.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AssembleFileWithFrame(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·framed(SB), NOSPLIT, $16-8
|
||||
MOVD arg+0(FP), R4
|
||||
ADD $1, R4, R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("got %d funcs, want 1", len(img.Funcs))
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Frame != 16 {
|
||||
t.Errorf("frame: got %d, want 16", fn.Frame)
|
||||
}
|
||||
// Prologue (3×4=12) + body (3×4=12) + RET epilogue (3×4=12) = 36
|
||||
if fn.Size != 36 {
|
||||
t.Errorf("func size: got %d, want 36", fn.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AssembleFileWithBranches(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·branch(SB), NOSPLIT, $0-0
|
||||
BEQ done
|
||||
BNE skip
|
||||
skip:
|
||||
ADD R4, R5
|
||||
done:
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 16 {
|
||||
t.Errorf("func size: got %d, want 16", fn.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AssembleFileWithJumpChain(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·chain(SB), NOSPLIT, $0-0
|
||||
BNE skip
|
||||
ADD R4, R5
|
||||
RET
|
||||
skip:
|
||||
B target
|
||||
target:
|
||||
ADD R6, R7
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
// BNE should be redirected past skip→target to target directly.
|
||||
if img.Funcs[0].Size != 24 {
|
||||
t.Errorf("func size: got %d, want 24", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AssembleErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
}{
|
||||
{"unsupported", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4, R5\n\tRET\n"},
|
||||
{"undefined label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, errs := parser.Parse("test_arm64.s", tt.src)
|
||||
if len(errs) > 0 {
|
||||
return // parse error, that's fine
|
||||
}
|
||||
_, err := AssembleFileARM64(f)
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64Movcon(t *testing.T) {
|
||||
tests := []struct {
|
||||
v int64
|
||||
want int
|
||||
}{
|
||||
{0, 0}, // 0 fits at shift 0
|
||||
{1, 0}, // single bit at shift 0
|
||||
{0x10000, 16}, // single bit at shift 16
|
||||
{0x100000000, 32}, // single bit at shift 32
|
||||
{0xFF, 0}, // 0xFF fits at shift 0
|
||||
{0x12345, -1}, // multiple chunks, not movcon
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := arm64Movcon(tt.v)
|
||||
if got != tt.want {
|
||||
t.Errorf("arm64Movcon(%#x) = %d, want %d", tt.v, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64RegClassOf(t *testing.T) {
|
||||
if arm64RegClassOf("R4") != arm64ClsGR {
|
||||
t.Error("R4 should be GR")
|
||||
}
|
||||
if arm64RegClassOf("F4") != arm64ClsFP {
|
||||
t.Error("F4 should be FP")
|
||||
}
|
||||
if arm64RegClassOf("") != arm64ClsNone {
|
||||
t.Error("empty should be None")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64ResolvePseudo(t *testing.T) {
|
||||
fi := arm64FrameInfo{autosize: 48, frame: 32}
|
||||
// FP: offset = sym.Offset + autosize +8
|
||||
base, off := arm64ResolvePseudo(&ast.Symbol{Pseudo: "FP", Offset: 0}, fi)
|
||||
if base != 31 || off != 56 {
|
||||
t.Errorf("FP: base=%d off=%d, want 31, 56", base, off)
|
||||
}
|
||||
// SP: offset = sym.Offset + frame +8
|
||||
base, off = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SP", Offset: -8}, fi)
|
||||
if base != 31 || off != 32 {
|
||||
t.Errorf("SP: base=%d off=%d, want 31, 32", base, off)
|
||||
}
|
||||
// SB: unresolved
|
||||
base, _ = arm64ResolvePseudo(&ast.Symbol{Pseudo: "SB"}, fi)
|
||||
if base != -1 {
|
||||
t.Errorf("SB: base=%d, want -1", base)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64FPSel tests FP conditional select encoding.
|
||||
func TestArm64FPSel(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
FCSELD GE, F10, F11, F12
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
// FCSELD should be 4 bytes + RET 4 bytes = 8
|
||||
if img.Funcs[0].Size != 8 {
|
||||
t.Errorf("size: got %d, want 8", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64FPCvt tests FP conversion encoding.
|
||||
func TestArm64FPCvt(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
FCVTZSD F4, R0
|
||||
SCVTFD R4, F8
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if img.Funcs[0].Size != 12 {
|
||||
t.Errorf("size: got %d, want 12", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64CSEL tests conditional select encoding.
|
||||
func TestArm64CSEL(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
CSEL EQ, R0, R1, R2
|
||||
CSET NE, R3
|
||||
CINC GE, R4, R5
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if img.Funcs[0].Size != 16 {
|
||||
t.Errorf("size: got %d, want 16", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64CRC32 tests CRC32 encoding.
|
||||
func TestArm64CRC32(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
CRC32B R0, R2
|
||||
CRC32W R6, R8
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if img.Funcs[0].Size != 12 {
|
||||
t.Errorf("size: got %d, want 12", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64Bitfield tests bitfield/shift encoding.
|
||||
func TestArm64Bitfield(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
ASR $4, R0, R1
|
||||
LSL $12, R4, R5
|
||||
EXTR $8, R0, R1, R2
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if img.Funcs[0].Size != 16 {
|
||||
t.Errorf("size: got %d, want 16", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64SIMD tests SIMD encoding (via the instruction table).
|
||||
func TestArm64SIMD(t *testing.T) {
|
||||
// Verify SIMD instructions are in the table.
|
||||
for _, mnem := range []string{"VADD", "VSUB", "VMUL"} {
|
||||
if _, ok := a64InstrTable[mnem]; !ok {
|
||||
t.Errorf("%s not in instruction table", mnem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64LoadImm64 tests 64-bit immediate loading.
|
||||
func TestArm64LoadImm64(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
MOVD $0x123456789ABCDEF0, R0
|
||||
MOVD $0, R1
|
||||
MOVD $1, R2
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
// $0x123456789ABCDEF0 needs 4 MOVZ/MOVK instructions (16 bytes)
|
||||
// $0 is 1 instruction (4 bytes)
|
||||
// $1 is 1 bitmask instruction (4 bytes)
|
||||
// RET is 1 instruction (4 bytes)
|
||||
if img.Funcs[0].Size != 28 {
|
||||
t.Errorf("size: got %d, want 28", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64BranchCond tests conditional branch encoding.
|
||||
func TestArm64BranchCond(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
BEQ done
|
||||
BNE done
|
||||
BGE done
|
||||
BLT done
|
||||
ADD R4, R5
|
||||
done:
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
// 4 branches + 1 ADD + 1 RET = 24 bytes
|
||||
if img.Funcs[0].Size != 24 {
|
||||
t.Errorf("size: got %d, want 24", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64Errors tests error paths.
|
||||
func TestArm64Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
}{
|
||||
{"bad mnemonic", "TEXT ·f(SB), NOSPLIT, $0-0\n\tINVALID\tR4\n\tRET\n"},
|
||||
{"bad label", "TEXT ·f(SB), NOSPLIT, $0-0\n\tB\tnosuch\n\tRET\n"},
|
||||
{"bad register", "TEXT ·f(SB), NOSPLIT, $0-0\n\tADD\tR99, R0\n\tRET\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, errs := parser.Parse("test_arm64.s", tt.src)
|
||||
if len(errs) > 0 {
|
||||
return
|
||||
}
|
||||
_, err := AssembleFileARM64(f)
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// leWord reads a little-endian uint32 from b.
|
||||
func leWord(b []byte) uint32 {
|
||||
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
|
||||
}
|
||||
|
||||
// leWords reads all little-endian uint32s from b.
|
||||
func leWords(b []byte) []uint32 {
|
||||
n := len(b) / 4
|
||||
w := make([]uint32, n)
|
||||
for i := range w {
|
||||
w[i] = leWord(b[i*4:])
|
||||
}
|
||||
return w
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
// arm64 frame mapping, matching the Go toolchain's arm64 backend.
|
||||
//
|
||||
// Go's arm64 functions use R29 as the frame pointer (FP) and R30 as the link
|
||||
// register (LR). R31 is the stack pointer (SP). FP and SP in the source
|
||||
// are synthetic pseudo-registers resolved against the hardware SP and the
|
||||
// frame size.
|
||||
//
|
||||
// The autosize is the real stack adjustment: the declared local frame plus
|
||||
// 8 bytes for the saved link register, rounded up to a 16-byte multiple.
|
||||
// The toolchain adds an "extrasize" to align: if autosize%16 == 8, add 8;
|
||||
// if autosize%16 == 0, add 16.
|
||||
//
|
||||
// Prologue (autosize > 0, small frame ≤ 0xf0):
|
||||
//
|
||||
// MOVD.W LR, -autosize(SP) // pre-index: SP -= autosize, store LR at SP
|
||||
// MOVD FP, -8(SP) // store FP at SP-8
|
||||
// SUB $8, SP, FP // FP = SP - 8
|
||||
//
|
||||
// Prologue (autosize > 0, large frame > 0xf0):
|
||||
//
|
||||
// SUB $autosize, SP, R20 // R20 = SP - autosize
|
||||
// STP (FP, LR), -8(R20) // store FP,LR at R20-8
|
||||
// MOVD R20, SP // SP = R20
|
||||
// SUB $8, SP, FP // FP = SP - 8
|
||||
//
|
||||
// Epilogue (non-leaf, small frame):
|
||||
//
|
||||
// ADD $autosize-8, SP, FP // restore FP
|
||||
// ADD $autosize, SP, SP // deallocate frame
|
||||
// MOVD -8(SP), FP // (actually the reverse of prologue)
|
||||
// Actually:
|
||||
// MOVD -8(SP), FP // load FP from SP-8
|
||||
// MOVD.P autosize(SP), LR // post-index: load LR, SP += autosize
|
||||
//
|
||||
// Epilogue (non-leaf, large frame):
|
||||
// ADD $autosize-8, SP, FP
|
||||
// ADD $autosize, SP, SP
|
||||
// Actually:
|
||||
// LDP -8(SP), (FP, LR) // load FP,LR
|
||||
// ADD $autosize, SP, SP // deallocate frame
|
||||
//
|
||||
// Epilogue (leaf with frame):
|
||||
// ADD $autosize-8, SP, FP
|
||||
// ADD $autosize, SP, SP
|
||||
//
|
||||
// RET always emits as BR LR (0xd65f03c0).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||
type arm64FrameInfo struct {
|
||||
autosize int // the real SP adjustment (locals + saved LR + alignment)
|
||||
frame int // the declared $framesize
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
leaf bool // no call instructions in the body
|
||||
}
|
||||
|
||||
// arm64ComputeFrame derives the frame layout for a TEXT function.
|
||||
func arm64ComputeFrame(t *ast.Text) arm64FrameInfo {
|
||||
fi := arm64FrameInfo{
|
||||
frame: frameSize(t),
|
||||
args: argsSize(t),
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
if f == "NOSPLIT" {
|
||||
fi.noSplit = true
|
||||
}
|
||||
}
|
||||
fi.leaf = arm64IsLeaf(t)
|
||||
|
||||
if fi.frame != 0 || !fi.leaf {
|
||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||
if fi.autosize%16 != 0 {
|
||||
// The toolchain aligns to 16: if autosize%16 == 8, add 8;
|
||||
// otherwise add whatever is needed.
|
||||
fi.autosize += 16 - (fi.autosize % 16)
|
||||
}
|
||||
}
|
||||
return fi
|
||||
}
|
||||
|
||||
// arm64IsLeaf reports whether a function contains no call instructions
|
||||
// (BL/CALL), matching the toolchain's LEAF mark.
|
||||
func arm64IsLeaf(t *ast.Text) bool {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "BL", "CALL":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// arm64Prologue returns the prologue bytes for an arm64 function.
|
||||
func arm64Prologue(fi arm64FrameInfo) []byte {
|
||||
if fi.autosize == 0 {
|
||||
return nil
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
// Small frame: MOVD.W LR, -autosize(SP); MOVD FP, -8(SP); SUB $8, SP, FP
|
||||
return a64WordsLE(
|
||||
arm64PreStoreImm(3, 0, int32(-fi.autosize), 31, 30), // STR.W LR, -autosize(SP) (pre-index store)
|
||||
arm64UnscaledStore(3, 0, -8, 31, 29), // STUR FP, [SP, #-8]
|
||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||
)
|
||||
}
|
||||
// Large frame: SUB $autosize, SP, R20; STP (FP,LR), -8(R20); ADD $0, R20, SP; SUB $8, SP, FP
|
||||
return a64WordsLE(
|
||||
a64AddSub(1, 1, 0, 0, uint32(fi.autosize), 31, 20), // SUB $autosize, SP, R20
|
||||
a64LSP(2, 0, 0, -1, 30, 20, 29), // STP FP, LR, [R20, #-8] (opc=2 for 64-bit pair)
|
||||
a64AddSub(1, 0, 0, 0, 0, 20, 31), // ADD $0, R20, SP (= MOV R20, SP)
|
||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||
)
|
||||
}
|
||||
|
||||
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
|
||||
// deallocate the frame when present) followed by RET (BR LR).
|
||||
func arm64Return(fi arm64FrameInfo) []byte {
|
||||
var ws []uint32
|
||||
if fi.autosize != 0 {
|
||||
if fi.leaf {
|
||||
// Leaf with frame: ADD $autosize-8, SP, FP; ADD $autosize, SP, SP
|
||||
ws = append(ws,
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize-8), 31, 29), // ADD $autosize-8, SP, FP
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
||||
)
|
||||
} else if fi.autosize <= 0xf0 {
|
||||
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #autosize
|
||||
ws = append(ws,
|
||||
arm64UnscaledLoad(3, 0, -8, 31, 29), // LDR FP, [SP, #-8]
|
||||
arm64PostLoad(3, 0, int32(fi.autosize), 31, 30), // LDR.P LR, [SP], #autosize
|
||||
)
|
||||
} else {
|
||||
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP
|
||||
ws = append(ws,
|
||||
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
||||
)
|
||||
}
|
||||
}
|
||||
// RET: BR LR (0xd65f03c0)
|
||||
ws = append(ws, a64UncondBranch(2, 30, 0)) // opc=2(RET), Rn=LR(30), Rd=0
|
||||
return a64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// arm64PrologueSpadjPC returns the function-relative byte offset where the
|
||||
// prologue has finished decrementing SP (the delta becomes autosize).
|
||||
func arm64PrologueSpadjPC(fi arm64FrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 4 // MOVD.W instruction decrements SP
|
||||
}
|
||||
return 8 // SUB + STP + MOVD (3 instructions, SP updated at the MOVD)
|
||||
}
|
||||
|
||||
// arm64ReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
||||
// (but not including) the final RET instruction.
|
||||
func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
if fi.leaf {
|
||||
return 8 // ADD + ADD
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 8 // LDR + LDR.P
|
||||
}
|
||||
return 8 // LDP + ADD
|
||||
}
|
||||
|
||||
// arm64ResolvePseudo translates a pseudo-register memory reference into a
|
||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||
// x+N(SP) → (N + frame + 8)(SP). Returns base = -1 for an unresolvable
|
||||
// reference (SB: static data, handled by the relocation path).
|
||||
//
|
||||
// The Go toolchain resolves all pseudo-register references against the
|
||||
// hardware stack pointer (R31/SP): FP references add autosize+8 (the
|
||||
// distance from SP after the prologue to the caller's argument area),
|
||||
// SP references add frame+8 (the distance to the local area).
|
||||
func arm64ResolvePseudo(sym *ast.Symbol, fi arm64FrameInfo) (base int, off int32) {
|
||||
if sym == nil {
|
||||
return -1, 0
|
||||
}
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
return 31, int32(sym.Offset) + int32(fi.autosize) + 8
|
||||
case "SP":
|
||||
return 31, int32(sym.Offset) + int32(fi.frame) + 8
|
||||
case "SB":
|
||||
return -1, int32(sym.Offset)
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// arm64PreStoreImm encodes a pre-index store (STR with writeback):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 1<<11 | 1<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64PreStoreImm(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 0<<22 |
|
||||
3<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64UnscaledStore encodes an unscaled store (STUR):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 0<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64UnscaledStore(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 0<<22 |
|
||||
(uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64UnscaledLoad encodes an unscaled load (LDUR):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 0<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64UnscaledLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||
(uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64PostLoad encodes a post-index load (LDR with post-increment):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 1<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64PostLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||
1<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
@@ -261,6 +261,11 @@ func subSP(size int) []byte { // SUBQ $size, SP
|
||||
if size >= -128 && size <= 127 {
|
||||
return []byte{0x48, 0x83, 0xEC, byte(int8(size))}
|
||||
}
|
||||
// 128..255 do not fit SUB's unsigned imm8, but the Go assembler
|
||||
// switches to ADDQ $-size, SP whose sign-extended imm8 does.
|
||||
if size >= -255 && size <= 255 {
|
||||
return []byte{0x48, 0x83, 0xC4, byte(int8(-size))}
|
||||
}
|
||||
return append([]byte{0x48, 0x81, 0xEC}, le32(int64(size))...)
|
||||
}
|
||||
|
||||
|
||||
+35
-1
@@ -62,7 +62,7 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
XORQ AX, AX
|
||||
loop:
|
||||
ADDQ $1, AX
|
||||
CMPQ $10, AX
|
||||
CMPQ AX, $10
|
||||
JLT loop
|
||||
RET
|
||||
`)
|
||||
@@ -317,3 +317,37 @@ end:
|
||||
t.Errorf("jump-folding mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssemblePrefetch(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·pf(SB), NOSPLIT, $0
|
||||
PREFETCHNTA (AX)
|
||||
PREFETCHT0 (BX)
|
||||
PREFETCHT1 8(CX)
|
||||
PREFETCHT2 -1(AX)(R12*1)
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
got := strings.Join(disasm(t, code), "\n")
|
||||
want := strings.Join([]string{
|
||||
"prefetchnta zmmword ptr [rax]",
|
||||
"prefetcht0 zmmword ptr [rbx]",
|
||||
"prefetcht1 zmmword ptr [rcx+0x8]",
|
||||
"prefetcht2 zmmword ptr [rax+r12-0x1]",
|
||||
"ret",
|
||||
}, "\n")
|
||||
if got != want {
|
||||
t.Errorf("prefetch disassembly mismatch:\n got:\n%s\n want:\n%s", got, want)
|
||||
}
|
||||
// Byte-level expectations: 0F 18 with the variant in the reg field.
|
||||
if hex := hexBytes(code[:3]); hex != "0f 18 00" {
|
||||
t.Errorf("PREFETCHNTA bytes: got %s, want 0f 18 00", hex)
|
||||
}
|
||||
if hex := hexBytes(code[3:6]); hex != "0f 18 0b" {
|
||||
t.Errorf("PREFETCHT0 bytes: got %s, want 0f 18 0b", hex)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-5
@@ -17,7 +17,6 @@ import (
|
||||
// and external ones against undefined globals. The output links with the
|
||||
// system toolchain (cc/ld) the way a hand-assembled .o would.
|
||||
|
||||
// ELF constants (ELF64, little-endian, System V).
|
||||
const (
|
||||
elfClass64 = 2
|
||||
elfDataLSB = 1
|
||||
@@ -36,17 +35,13 @@ const (
|
||||
shfAlloc = 2
|
||||
shfExecInstr = 4
|
||||
|
||||
stbLocal = 0
|
||||
stbGlobal = 1
|
||||
|
||||
sttNotype = 0
|
||||
sttObject = 1
|
||||
sttFunc = 2
|
||||
sttSection = 3
|
||||
stInfoShift = 4
|
||||
|
||||
shnUndef = 0
|
||||
|
||||
rX8664PC32 = 2
|
||||
)
|
||||
|
||||
@@ -160,6 +155,9 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are relocations.
|
||||
hasRela := len(relas) > 0
|
||||
@@ -220,6 +218,17 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// DWARF debug sections (no relocations — the linker resolves DWARF fixups).
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
if dw != nil {
|
||||
nSections += 4 // .debug_abbrev, .debug_info, .debug_line, .debug_line_str
|
||||
}
|
||||
|
||||
align(8)
|
||||
shoff := len(out)
|
||||
|
||||
@@ -248,6 +257,17 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
|
||||
// DWARF section headers.
|
||||
if dw != nil {
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// The ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// DWARF5 section generation for ELF output. Unlike the GOOBJ path (where
|
||||
// the linker assembles the final DWARF), the ELF path must emit complete,
|
||||
// self-contained sections because the system linker only performs fixup
|
||||
// relocations, not assembly.
|
||||
|
||||
// dwarfAbbrevTable returns the .debug_abbrev content: a single compilation
|
||||
// unit with DW_TAG_compile_unit and DW_TAG_subprogram entries.
|
||||
func dwarfAbbrevTable() []byte {
|
||||
var b []byte
|
||||
// Abbrev 1: DW_TAG_compile_unit
|
||||
b = append(b, 1) // abbreviation code
|
||||
b = append(b, 0x11) // DW_TAG_compile_unit
|
||||
b = append(b, 1) // DW_CHILDREN_yes
|
||||
b = appendUleb(b, 0x1b) // DW_AT_low_pc
|
||||
b = appendUleb(b, 0x01) // DW_FORM_addr
|
||||
b = appendUleb(b, 0x29) // DW_AT_high_pc
|
||||
b = appendUleb(b, 0x07) // DW_FORM_data8
|
||||
b = appendUleb(b, 0x10) // DW_AT_stmt_list
|
||||
b = appendUleb(b, 0x25) // DW_FORM_sec_offset
|
||||
b = appendUleb(b, 0x01) // DW_AT_name
|
||||
b = appendUleb(b, 0x08) // DW_FORM_string
|
||||
b = appendUleb(b, 0) // end of attributes
|
||||
|
||||
// Abbrev 2: DW_TAG_subprogram
|
||||
b = append(b, 2) // abbreviation code
|
||||
b = append(b, 0x2e) // DW_TAG_subprogram
|
||||
b = append(b, 0) // DW_CHILDREN_no
|
||||
b = appendUleb(b, 0x03) // DW_AT_name
|
||||
b = appendUleb(b, 0x08) // DW_FORM_string
|
||||
b = appendUleb(b, 0x11) // DW_AT_low_pc
|
||||
b = appendUleb(b, 0x01) // DW_FORM_addr
|
||||
b = appendUleb(b, 0x29) // DW_AT_high_pc
|
||||
b = appendUleb(b, 0x07) // DW_FORM_data8
|
||||
b = appendUleb(b, 0x3f) // DW_AT_frame_base
|
||||
b = appendUleb(b, 0x18) // DW_FORM_exprloc
|
||||
b = appendUleb(b, 0x3b) // DW_AT_decl_file
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_data1
|
||||
b = appendUleb(b, 0x37) // DW_AT_decl_line
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_data1
|
||||
b = appendUleb(b, 0x63) // DW_AT_external
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_flag
|
||||
b = appendUleb(b, 0) // end of attributes
|
||||
|
||||
// End of table.
|
||||
b = append(b, 0)
|
||||
return b
|
||||
}
|
||||
|
||||
// dwarfSections holds the generated DWARF section payloads and their
|
||||
// relocations (byte offsets within .debug_info and .debug_line that need
|
||||
// fixup against .text symbols).
|
||||
type dwarfSections struct {
|
||||
debugAbbrev []byte
|
||||
debugInfo []byte
|
||||
debugLine []byte
|
||||
debugLineStr []byte
|
||||
debugFrame []byte
|
||||
// Relocations for .debug_info: (offset, symbol name, addend).
|
||||
infoRelocs []dwarfReloc
|
||||
// Relocations for .debug_line: (offset, symbol name, addend).
|
||||
lineRelocs []dwarfReloc
|
||||
}
|
||||
|
||||
type dwarfReloc struct {
|
||||
off uint64
|
||||
name string
|
||||
addend int64
|
||||
}
|
||||
|
||||
// emitDWARF generates complete DWARF5 sections for the image.
|
||||
func emitDWARF(img *Image, srcFile string) *dwarfSections {
|
||||
ds := &dwarfSections{}
|
||||
ds.debugAbbrev = dwarfAbbrevTable()
|
||||
|
||||
// Build the string table for .debug_line_str.
|
||||
lineStr := newElfStrtab()
|
||||
lineStr.add(srcFile)
|
||||
ds.debugLineStr = lineStr.bytes()
|
||||
|
||||
// Build .debug_line.
|
||||
ds.debugLine = dwarfBuildLineSection(img, ds)
|
||||
|
||||
// Build .debug_info.
|
||||
ds.debugInfo = dwarfBuildInfoSection(img, srcFile, ds)
|
||||
|
||||
// Build .debug_frame.
|
||||
ds.debugFrame = dwarfBuildFrameSection(img)
|
||||
return ds
|
||||
}
|
||||
|
||||
// dwarfBuildLineSection builds a complete .debug_line section.
|
||||
func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
// We'll build the header first, then the programs, then patch the length.
|
||||
headerStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // unit_length (placeholder)
|
||||
b = le.AppendUint16(b, 5) // version (DWARF5)
|
||||
b = append(b, 8) // address_size
|
||||
b = append(b, 0) // segment_selector_size
|
||||
b = append(b, 0, 0, 0, 0) // header_length (placeholder)
|
||||
|
||||
// Line program parameters.
|
||||
b = append(b, 1) // minimum_instruction_length
|
||||
b = append(b, 1) // maximum_ops_per_instruction
|
||||
b = append(b, 1) // default_is_stmt
|
||||
b = append(b, byte(dwLineBase&0xFF)) // line_base (-4 as unsigned)
|
||||
b = append(b, uint8(dwLineRange)) // line_range
|
||||
b = append(b, uint8(dwOpcodeBase)) // opcode_base
|
||||
// Standard opcode lengths (opcode 1..opcode_base-1).
|
||||
b = append(b, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0)
|
||||
|
||||
// Directory table (DWARF5 format).
|
||||
b = append(b, 0) // one directory entry (index 0 = empty)
|
||||
// File table.
|
||||
b = appendUleb(b, 1) // file count
|
||||
// File 1: name index into .debug_line_str, dir index, time, size.
|
||||
b = appendUleb(b, 0) // name (index 0 in line_str)
|
||||
b = appendUleb(b, 0) // directory index
|
||||
b = appendUleb(b, 0) // last modification time
|
||||
b = appendUleb(b, 0) // file size
|
||||
|
||||
headerEnd := len(b)
|
||||
|
||||
// Per-function line programs.
|
||||
for _, fn := range img.Funcs {
|
||||
// LNE_set_address with the function's offset in .text.
|
||||
b = append(b, 0, 9, 2) // extended opcode, length 9, DW_LNE_set_address
|
||||
addrOff := len(b)
|
||||
b = le.AppendUint64(b, 0) // placeholder for address
|
||||
ds.lineRelocs = append(ds.lineRelocs, dwarfReloc{
|
||||
off: uint64(addrOff),
|
||||
name: fn.Name,
|
||||
addend: 0,
|
||||
})
|
||||
|
||||
// Build the line entries.
|
||||
pts := make([]LineEntry, 0, len(fn.Lines)+1)
|
||||
if len(fn.Lines) == 0 || fn.Lines[0].Offset > 0 {
|
||||
pts = append(pts, LineEntry{Offset: 0, Line: fn.Line})
|
||||
}
|
||||
pts = append(pts, fn.Lines...)
|
||||
|
||||
line := int64(1)
|
||||
pc := uint64(0)
|
||||
for _, p := range pts {
|
||||
if p.Line == 0 || uint64(p.Offset) < pc {
|
||||
continue
|
||||
}
|
||||
if int64(p.Line) == line {
|
||||
continue
|
||||
}
|
||||
deltaPC := uint64(p.Offset) - pc
|
||||
deltaLC := int64(p.Line) - line
|
||||
b = dwPutPCLCDelta(b, deltaPC, deltaLC)
|
||||
line, pc = int64(p.Line), uint64(p.Offset)
|
||||
}
|
||||
|
||||
// Advance to end of function.
|
||||
if end := uint64(fn.Size) - pc; end > 0 {
|
||||
b = append(b, 2) // DW_LNS_advance_pc
|
||||
b = appendUleb(b, end)
|
||||
}
|
||||
b = append(b, 0, 1, 1) // LNE_end_sequence
|
||||
}
|
||||
|
||||
// Patch unit_length.
|
||||
le.PutUint32(b[headerStart:], uint32(len(b)-headerStart-4))
|
||||
// Patch header_length.
|
||||
le.PutUint32(b[headerStart+6:], uint32(headerEnd-headerStart-10))
|
||||
return b
|
||||
}
|
||||
|
||||
// dwarfBuildInfoSection builds a complete .debug_info section.
|
||||
func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
cuStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // unit_length (placeholder)
|
||||
b = le.AppendUint16(b, 5) // version (DWARF5)
|
||||
b = append(b, 0x01) // unit_type (DW_UT_compile)
|
||||
b = append(b, 8) // address_size
|
||||
b = le.AppendUint32(b, 0) // debug_abbrev_offset (0 since single CU)
|
||||
|
||||
// DW_TAG_compile_unit (abbrev 1).
|
||||
b = append(b, 1) // abbreviation code
|
||||
// DW_AT_low_pc: address of .text start.
|
||||
infoRelocBase := len(b)
|
||||
b = le.AppendUint64(b, 0) // placeholder
|
||||
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
|
||||
off: uint64(infoRelocBase),
|
||||
name: img.Funcs[0].Name,
|
||||
addend: 0,
|
||||
})
|
||||
// DW_AT_high_pc: size of .text.
|
||||
b = le.AppendUint64(b, uint64(len(img.Code)))
|
||||
// DW_AT_stmt_list: offset into .debug_line (0).
|
||||
b = le.AppendUint32(b, 0)
|
||||
// DW_AT_name: source file name.
|
||||
b = append(b, srcFile...)
|
||||
b = append(b, 0)
|
||||
|
||||
// DW_TAG_subprogram entries (abbrev 2).
|
||||
for _, fn := range img.Funcs {
|
||||
b = append(b, 2) // abbreviation code
|
||||
// DW_AT_name.
|
||||
b = append(b, fn.Name...)
|
||||
b = append(b, 0)
|
||||
// DW_AT_low_pc.
|
||||
addrOff := len(b)
|
||||
b = le.AppendUint64(b, 0) // placeholder
|
||||
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
|
||||
off: uint64(addrOff),
|
||||
name: fn.Name,
|
||||
addend: 0,
|
||||
})
|
||||
// DW_AT_high_pc: function size.
|
||||
b = le.AppendUint64(b, uint64(fn.Size))
|
||||
// DW_AT_frame_base: DW_OP_call_frame_cfa.
|
||||
b = append(b, 1, 0x9c)
|
||||
// DW_AT_decl_file: file index 1.
|
||||
b = append(b, 1)
|
||||
// DW_AT_decl_line.
|
||||
b = append(b, uint8(fn.Line))
|
||||
// DW_AT_external.
|
||||
if fn.Static {
|
||||
b = append(b, 0)
|
||||
} else {
|
||||
b = append(b, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// End of compile unit children.
|
||||
b = append(b, 0)
|
||||
|
||||
// Patch unit_length.
|
||||
le.PutUint32(b[cuStart:], uint32(len(b)-cuStart-4))
|
||||
return b
|
||||
}
|
||||
|
||||
func appendUleb(b []byte, v uint64) []byte {
|
||||
return binary.AppendUvarint(b, v)
|
||||
}
|
||||
|
||||
func appendSleb(b []byte, v int64) []byte {
|
||||
return binary.AppendVarint(b, v)
|
||||
}
|
||||
|
||||
// dwarfBuildFrameSection builds a .debug_frame section with CFI for stack
|
||||
// unwinding. It emits one CIE and one FDE per function, encoding the
|
||||
// CFA (Canonical Frame Address) rule changes at each stack-adjustment
|
||||
// boundary recorded in FuncLayout.Spadj.
|
||||
func dwarfBuildFrameSection(img *Image) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
// CIE (Common Information Entry).
|
||||
cieStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||
b = le.AppendUint32(b, 0xFFFFFFFF) // CIE marker
|
||||
b = append(b, 3) // version (DWARF3, widely supported)
|
||||
b = append(b, 0) // augmentation (empty)
|
||||
b = appendUleb(b, 1) // code alignment
|
||||
b = appendSleb(b, -8) // data alignment (-8 for 64-bit)
|
||||
b = appendUleb(b, 16) // return address register (LR on arm64, RIP on amd64)
|
||||
// Initial CFA rule: DW_CFA_def_cfa (SP, 0)
|
||||
b = append(b, 0x0c) // DW_CFA_def_cfa
|
||||
b = appendUleb(b, 31) // register: SP (RSP=7 on amd64, SP=31 on arm64)
|
||||
b = appendUleb(b, 0) // offset: 0
|
||||
b = append(b, 0) // DW_CFA_nop (padding)
|
||||
// Patch CIE length.
|
||||
le.PutUint32(b[cieStart:], uint32(len(b)-cieStart-4))
|
||||
|
||||
// FDEs (Frame Description Entries) — one per function.
|
||||
for _, fn := range img.Funcs {
|
||||
fdeStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||
b = le.AppendUint32(b, uint32(cieStart)) // CIE pointer (offset from start)
|
||||
// Initial location: function offset in .text (relocated by linker).
|
||||
b = le.AppendUint64(b, uint64(fn.Offset))
|
||||
// Address range: function size.
|
||||
b = le.AppendUint64(b, uint64(fn.Size))
|
||||
|
||||
// Emit CFA rule changes at each Spadj boundary.
|
||||
for _, step := range fn.Spadj {
|
||||
if step.Value == 0 {
|
||||
continue
|
||||
}
|
||||
// DW_CFA_def_cfa_offset: set CFA = SP + |delta|.
|
||||
// The delta is negative (stack grows down), so CFA offset = -delta.
|
||||
offset := -step.Value
|
||||
if offset > 0 {
|
||||
b = append(b, 0x0e) // DW_CFA_def_cfa_offset
|
||||
b = appendUleb(b, uint64(offset))
|
||||
}
|
||||
}
|
||||
|
||||
// Pad to alignment.
|
||||
for len(b)%4 != 0 {
|
||||
b = append(b, 0) // DW_CFA_nop
|
||||
}
|
||||
|
||||
// Patch FDE length.
|
||||
le.PutUint32(b[fdeStart:], uint32(len(b)-fdeStart-4))
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
// dwarfELFSections holds the laid-out DWARF sections ready for inclusion
|
||||
// in an ELF file.
|
||||
type dwarfELFSections struct {
|
||||
abbrevOff, abbrevSize int
|
||||
infoOff, infoSize int
|
||||
lineOff, lineSize int
|
||||
lineStrOff, lineStrSize int
|
||||
frameOff, frameSize int
|
||||
// Relocations for .debug_info address references.
|
||||
infoRelocs []elfDwarfReloc
|
||||
// Relocations for .debug_line address references.
|
||||
lineRelocs []elfDwarfReloc
|
||||
}
|
||||
|
||||
type elfDwarfReloc struct {
|
||||
off uint64
|
||||
sym int // symbol index in .symtab
|
||||
addend int64
|
||||
}
|
||||
|
||||
// appendDWARFSections generates and appends DWARF5 debug sections to the ELF
|
||||
// output. It returns the section offsets/sizes and relocations for the caller
|
||||
// to emit section headers and relocation records.
|
||||
//
|
||||
// symIdx maps function names to their .symtab indices (needed for relocations
|
||||
// against .text symbols). The map uses objectName format (pkg.name); the
|
||||
// DWARF code uses bare function names, so we build a reverse lookup.
|
||||
func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[string]int, align func(int)) *dwarfELFSections {
|
||||
// Build a lookup from bare function name to symbol index.
|
||||
nameToIdx := make(map[string]int, len(symIdx))
|
||||
for name, idx := range symIdx {
|
||||
// Strip package prefix: "pkg.name" → "name".
|
||||
if i := len(name) - 1; i >= 0 {
|
||||
for j := len(name) - 1; j >= 0; j-- {
|
||||
if name[j] == '.' {
|
||||
nameToIdx[name[j+1:]] = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
nameToIdx[name] = idx
|
||||
}
|
||||
ds := emitDWARF(img, srcFile)
|
||||
if ds == nil || len(ds.debugAbbrev) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &dwarfELFSections{}
|
||||
|
||||
// .debug_abbrev
|
||||
align(1)
|
||||
result.abbrevOff = len(*out)
|
||||
result.abbrevSize = len(ds.debugAbbrev)
|
||||
*out = append(*out, ds.debugAbbrev...)
|
||||
|
||||
// .debug_line_str
|
||||
align(1)
|
||||
result.lineStrOff = len(*out)
|
||||
result.lineStrSize = len(ds.debugLineStr)
|
||||
*out = append(*out, ds.debugLineStr...)
|
||||
|
||||
// .debug_line
|
||||
align(1)
|
||||
result.lineOff = len(*out)
|
||||
result.lineSize = len(ds.debugLine)
|
||||
lineBase := len(*out)
|
||||
*out = append(*out, ds.debugLine...)
|
||||
|
||||
// Patch .debug_line relocations: replace placeholder addresses with
|
||||
// actual .text offsets via symbol lookup.
|
||||
for _, dr := range ds.lineRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.lineRelocs = append(result.lineRelocs, elfDwarfReloc{
|
||||
off: uint64(lineBase) + dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// .debug_info
|
||||
align(1)
|
||||
result.infoOff = len(*out)
|
||||
result.infoSize = len(ds.debugInfo)
|
||||
infoBase := len(*out)
|
||||
*out = append(*out, ds.debugInfo...)
|
||||
|
||||
// .debug_frame
|
||||
if len(ds.debugFrame) > 0 {
|
||||
align(1)
|
||||
result.frameOff = len(*out)
|
||||
result.frameSize = len(ds.debugFrame)
|
||||
*out = append(*out, ds.debugFrame...)
|
||||
}
|
||||
|
||||
// Patch .debug_info relocations.
|
||||
for _, dr := range ds.infoRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.infoRelocs = append(result.infoRelocs, elfDwarfReloc{
|
||||
off: uint64(infoBase) + dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// dwarfSectionNames returns the DWARF section names for the string table.
|
||||
var dwarfSectionNames = []string{
|
||||
".debug_abbrev", ".debug_info", ".debug_line", ".debug_line_str",
|
||||
".debug_frame", ".rela.debug_info", ".rela.debug_line",
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
func TestEmitDWARF(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), BX
|
||||
ADDQ BX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
ds := emitDWARF(img, "test_amd64.s")
|
||||
|
||||
// .debug_abbrev must not be empty and must start with abbrev code 1.
|
||||
if len(ds.debugAbbrev) == 0 {
|
||||
t.Fatal("empty .debug_abbrev")
|
||||
}
|
||||
if ds.debugAbbrev[0] != 1 {
|
||||
t.Fatalf(".debug_abbrev first byte = %d, want 1", ds.debugAbbrev[0])
|
||||
}
|
||||
|
||||
// .debug_info must have a compile unit header (DWARF5 version 5).
|
||||
if len(ds.debugInfo) < 12 {
|
||||
t.Fatalf(".debug_info too short: %d bytes", len(ds.debugInfo))
|
||||
}
|
||||
// Version field at offset 4 (after unit_length).
|
||||
if ds.debugInfo[4] != 5 || ds.debugInfo[5] != 0 {
|
||||
t.Fatalf(".debug_info version = %d, want 5", uint16(ds.debugInfo[4])|uint16(ds.debugInfo[5])<<8)
|
||||
}
|
||||
|
||||
// .debug_line must have a header.
|
||||
if len(ds.debugLine) < 20 {
|
||||
t.Fatalf(".debug_line too short: %d bytes", len(ds.debugLine))
|
||||
}
|
||||
// Version at offset 4.
|
||||
if ds.debugLine[4] != 5 || ds.debugLine[5] != 0 {
|
||||
t.Fatalf(".debug_line version = %d, want 5", uint16(ds.debugLine[4])|uint16(ds.debugLine[5])<<8)
|
||||
}
|
||||
|
||||
// .debug_line_str must contain the source file name.
|
||||
if len(ds.debugLineStr) == 0 {
|
||||
t.Fatal("empty .debug_line_str")
|
||||
}
|
||||
|
||||
// Relocations must reference the function.
|
||||
if len(ds.lineRelocs) == 0 {
|
||||
t.Fatal("no .debug_line relocations")
|
||||
}
|
||||
if len(ds.infoRelocs) == 0 {
|
||||
t.Fatal("no .debug_info relocations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDwarfAbbrevTable(t *testing.T) {
|
||||
abbrev := dwarfAbbrevTable()
|
||||
if len(abbrev) == 0 {
|
||||
t.Fatal("empty abbrev table")
|
||||
}
|
||||
// Must end with a zero byte (end of table).
|
||||
if abbrev[len(abbrev)-1] != 0 {
|
||||
t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1])
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -187,7 +187,7 @@ func TestELFObject(t *testing.T) {
|
||||
end := bytes.IndexByte(strtabRaw[stName:], 0)
|
||||
return string(strtabRaw[stName : int(stName)+end])
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
e := raw[i*24 : (i+1)*24]
|
||||
off := binary.LittleEndian.Uint64(e[0:])
|
||||
info := binary.LittleEndian.Uint64(e[8:])
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AArch64 ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emAARCH64 = 183 // EM_AARCH64
|
||||
|
||||
// AArch64 relocation types (the ELF psABI).
|
||||
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
|
||||
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD/STR/LDR page offset)
|
||||
rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction)
|
||||
)
|
||||
|
||||
// ELFAARCH64Object returns the image as an ELF64 relocatable object file for
|
||||
// AArch64 (EM_AARCH64, 64-bit, little-endian). The structure mirrors the
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab and an
|
||||
// optional .rela.text.
|
||||
func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference is an ADRP pair:
|
||||
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21
|
||||
// ADD/LDR/STR → R_AARCH64_ADD_ABS_LO12_NC
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch {
|
||||
case r.Kind == RelArm64Branch:
|
||||
typ = rArm64Call26
|
||||
case r.Kind == RelArm64Addr && r.Off%4 == 4:
|
||||
typ = rArm64AddAbsLo12NC
|
||||
default:
|
||||
typ = rArm64PrelPgHi21
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
for _, r := range relas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// DWARF debug sections.
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
}
|
||||
|
||||
align(8)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
if dw != nil {
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emAARCH64)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
||||
// relocatable object: sections, the symbol table (bindings, types, values,
|
||||
// sizes) and the .rela.text relocation pair for the static-symbol load,
|
||||
// parsed back with debug/elf.
|
||||
func TestELFAARCH64Object(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·getanswer(SB), NOSPLIT, $0-8
|
||||
MOVD answer<>(SB), R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFAARCH64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFAARCH64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Type != elf.ET_REL || ef.Machine != elf.EM_AARCH64 {
|
||||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_AARCH64", ef.Type, ef.Machine)
|
||||
}
|
||||
|
||||
text := ef.Section(".text")
|
||||
data := ef.Section(".data")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing .text or .data section")
|
||||
}
|
||||
if text.Size == 0 {
|
||||
t.Error(".text section is empty")
|
||||
}
|
||||
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatalf("symbols: %v", err)
|
||||
}
|
||||
|
||||
foundAdd, foundGetanswer, foundAnswer := false, false, false
|
||||
for _, s := range syms {
|
||||
switch s.Name {
|
||||
case "add":
|
||||
foundAdd = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL {
|
||||
t.Errorf("add: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info)
|
||||
}
|
||||
case "getanswer":
|
||||
foundGetanswer = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL {
|
||||
t.Errorf("getanswer: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info)
|
||||
}
|
||||
case "answer":
|
||||
foundAnswer = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_OBJECT || elf.SymBind(s.Info>>4) != elf.STB_LOCAL {
|
||||
t.Errorf("answer: info=0x%02x, want STT_OBJECT|STB_LOCAL", s.Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAdd {
|
||||
t.Error("symbol 'add' not found")
|
||||
}
|
||||
if !foundGetanswer {
|
||||
t.Error("symbol 'getanswer' not found")
|
||||
}
|
||||
if !foundAnswer {
|
||||
t.Error("symbol 'answer' not found")
|
||||
}
|
||||
|
||||
// Check that .rela.text exists (getanswer has SB reference).
|
||||
relaText := ef.Section(".rela.text")
|
||||
if relaText == nil {
|
||||
t.Error("missing .rela.text section")
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFAARCH64ObjectNoRelocations checks the ELF output when there are no
|
||||
// static-symbol references (no .rela.text section).
|
||||
func TestELFAARCH64ObjectNoRelocations(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFAARCH64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFAARCH64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Section(".rela.text") != nil {
|
||||
t.Error("unexpected .rela.text section when there are no relocations")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// LoongArch ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emLOONGARCH = 258 // EM_LOONGARCH
|
||||
|
||||
// LoongArch relocation types (the ELF psABI).
|
||||
rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i)
|
||||
rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st)
|
||||
)
|
||||
|
||||
// ELFLOONG64Object returns the image as an ELF64 relocatable object file for
|
||||
// LoongArch (EM_LOONGARCH, 64-bit, little-endian). The structure mirrors the
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab and an
|
||||
// optional .rela.text.
|
||||
func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference is a pcalau12i pair:
|
||||
// pcalau12i rd, 0 → R_LARCH_PCALA_HI20
|
||||
// addi.d/ld/st → R_LARCH_PCALA_LO12
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
typ := uint32(rLarchPCALAHI20)
|
||||
if r.Kind == RelLoong64AddrLo {
|
||||
typ = rLarchPCALALO12
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
for _, r := range relas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
}
|
||||
|
||||
align(8)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
if dw != nil {
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emLOONGARCH)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
||||
// relocatable object: sections, the symbol table (bindings, types, values,
|
||||
// sizes) and the .rela.text relocation pair for the static-symbol load,
|
||||
// parsed back with debug/elf.
|
||||
func TestELFLOONG64Object(t *testing.T) {
|
||||
f, errs := parser.Parse("k_loong64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·getanswer(SB), NOSPLIT, $0-8
|
||||
MOVV answer<>(SB), R4
|
||||
MOVV R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFLOONG64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFLOONG64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Type != elf.ET_REL || ef.Machine != elf.EM_LOONGARCH {
|
||||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_LOONGARCH", ef.Type, ef.Machine)
|
||||
}
|
||||
|
||||
text := ef.Section(".text")
|
||||
data := ef.Section(".data")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing .text or .data section")
|
||||
}
|
||||
if text.Flags&elf.SHF_EXECINSTR == 0 || text.Flags&elf.SHF_ALLOC == 0 {
|
||||
t.Errorf(".text flags = %v", text.Flags)
|
||||
}
|
||||
if data.Flags&elf.SHF_WRITE == 0 {
|
||||
t.Errorf(".data flags = %v", data.Flags)
|
||||
}
|
||||
textData, err := text.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(textData, img.Code) {
|
||||
t.Errorf(".text contents differ from the image code")
|
||||
}
|
||||
dataData, err := data.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatalf("symbols: %v", err)
|
||||
}
|
||||
byName := map[string]elf.Symbol{}
|
||||
for _, s := range syms {
|
||||
byName[s.Name] = s
|
||||
}
|
||||
wantSym := func(name string, bind elf.SymBind, typ elf.SymType, section elf.SectionIndex, size uint64) {
|
||||
t.Helper()
|
||||
s, ok := byName[name]
|
||||
if !ok {
|
||||
t.Errorf("symbol %q not found", name)
|
||||
return
|
||||
}
|
||||
if elf.ST_BIND(s.Info) != bind || elf.ST_TYPE(s.Info) != typ {
|
||||
t.Errorf("%s: bind/type = %v/%v, want %v/%v", name, elf.ST_BIND(s.Info), elf.ST_TYPE(s.Info), bind, typ)
|
||||
}
|
||||
if s.Section != section {
|
||||
t.Errorf("%s: section = %v, want %v", name, s.Section, section)
|
||||
}
|
||||
if s.Size != size {
|
||||
t.Errorf("%s: size = %d, want %d", name, s.Size, size)
|
||||
}
|
||||
}
|
||||
if ef.Sections[1].Name != ".text" || ef.Sections[2].Name != ".data" {
|
||||
t.Fatalf("section layout = %s, %s; want .text, .data", ef.Sections[1].Name, ef.Sections[2].Name)
|
||||
}
|
||||
textIdx := elf.SectionIndex(1)
|
||||
dataIdx := elf.SectionIndex(2)
|
||||
wantSym("add", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 20)
|
||||
wantSym("getanswer", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 16)
|
||||
wantSym("answer", elf.STB_LOCAL, elf.STT_OBJECT, dataIdx, 8)
|
||||
|
||||
// The data section carries 16-byte alignment padding; the answer
|
||||
// symbol sits at its padded offset.
|
||||
ans := byName["answer"]
|
||||
if ans.Value+8 > uint64(len(dataData)) {
|
||||
t.Fatalf("answer value %d outside .data (%d bytes)", ans.Value, len(dataData))
|
||||
}
|
||||
if got := dataData[ans.Value : ans.Value+8]; !bytes.Equal(got, []byte{42, 0, 0, 0, 0, 0, 0, 0}) {
|
||||
t.Errorf("answer data = % x, want $42", got)
|
||||
}
|
||||
|
||||
// Relocations: the static-symbol load is a pcalau12i+ld.d pair, so one
|
||||
// R_LARCH_PCALA_HI20 and one R_LARCH_PCALA_LO12, both against the local
|
||||
// data symbol. debug/elf does not surface rela entries, so read the
|
||||
// section directly.
|
||||
relaSec := ef.Section(".rela.text")
|
||||
if relaSec == nil {
|
||||
t.Fatal("missing .rela.text")
|
||||
}
|
||||
raw, err := relaSec.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(raw)%24 != 0 || len(raw)/24 != 2 {
|
||||
t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw))
|
||||
}
|
||||
le := binary.LittleEndian
|
||||
for i := range 2 {
|
||||
e := raw[i*24 : (i+1)*24]
|
||||
off := le.Uint64(e[0:])
|
||||
info := le.Uint64(e[8:])
|
||||
typ := info & 0xffffffff
|
||||
sym := int(info >> 32)
|
||||
if i == 0 && (typ != uint64(elf.R_LARCH_PCALA_HI20) || off != 20) {
|
||||
t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_HI20 at 20", i, typ, off)
|
||||
}
|
||||
if i == 1 && (typ != uint64(elf.R_LARCH_PCALA_LO12) || off != 24) {
|
||||
t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_LO12 at 24", i, typ, off)
|
||||
}
|
||||
if sym != 3 { // NULL, .text, .data, then the first local: answer
|
||||
t.Errorf("reloc %d: symbol index %d, want 3 (answer)", i, sym)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFLOONG64ObjectNoRelocations checks a file with no static-symbol
|
||||
// references emits a valid object without a .rela.text section.
|
||||
func TestELFLOONG64ObjectNoRelocations(t *testing.T) {
|
||||
f, errs := parser.Parse("n_loong64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·nop(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFLOONG64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFLOONG64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
if ef.Section(".rela.text") != nil {
|
||||
t.Error("unexpected .rela.text section")
|
||||
}
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, s := range syms {
|
||||
if s.Name == "nop" && elf.ST_TYPE(s.Info) == elf.STT_FUNC {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("function symbol nop not found")
|
||||
}
|
||||
}
|
||||
+44
-18
@@ -15,6 +15,7 @@ const (
|
||||
|
||||
// RISC-V relocation types.
|
||||
rRISCV32 = 1
|
||||
rRISCVJAL = 17 // R_RISCV_JAL
|
||||
rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20
|
||||
rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I
|
||||
rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S
|
||||
@@ -79,11 +80,12 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference produces a pair:
|
||||
// AUIPC rd, 0 → R_RISCV_PCREL_HI20
|
||||
// ADDI/LD/SD → R_RISCV_PCREL_LO12_I or _S
|
||||
// For now we record them as individual entries; at link time
|
||||
// the linker must pair HI20 with its matching LO12.
|
||||
// Build relocations. Each SB reference is an AUIPC + second-instruction
|
||||
// pair carrying a single relocation kind; the ELF writer expands it into
|
||||
// the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects.
|
||||
// The HI20 carries the symbol addend; the LO12 addend is zero, matching
|
||||
// cmd/link's own ELF conversion (the LO12 resolves against the HI20's
|
||||
// AUIPC location).
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
@@ -97,22 +99,24 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
// Determine relocation type from the relocation kind.
|
||||
typ := uint32(rRISCVPCRELHI20) // default: AUIPC
|
||||
switch r.Kind {
|
||||
case RelPCRelLO12:
|
||||
typ = rRISCVPCRELLO12I
|
||||
case RelPCRelLO12S:
|
||||
typ = rRISCVPCRELLO12S
|
||||
case RelRISCVPCRELIType:
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: 0},
|
||||
)
|
||||
case RelRISCVPCRELSType:
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: 0},
|
||||
)
|
||||
case RelRISCVJal:
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, sym: idx, addend: r.Addend})
|
||||
case RelPCRelAbs:
|
||||
typ = rRISCV32
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend})
|
||||
default:
|
||||
return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind)
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +129,9 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
@@ -184,6 +191,16 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
}
|
||||
|
||||
align(8)
|
||||
shoff := len(out)
|
||||
|
||||
@@ -210,6 +227,15 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
if dw != nil {
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "strings"
|
||||
|
||||
// Encodable reports whether the amd64 encoder knows how to encode the
|
||||
// mnemonic. It mirrors the dispatch in (*enc).encode: the fixed-name
|
||||
// instructions, conditional jumps, the CMOV/SET condition families, the
|
||||
// VEX/EVEX/opmask/gather/scatter vector paths, the legacy SSE tables and the
|
||||
// explicit scalar cases. A mnemonic that parses (is in the architecture
|
||||
// table) but is not encodable would otherwise surface only at assembly time,
|
||||
// deep inside a build; the linter uses this predicate to flag it at edit
|
||||
// time.
|
||||
func Encodable(mnemonic string) bool {
|
||||
upper := strings.ToUpper(mnemonic)
|
||||
|
||||
// Fixed-name instructions (no size suffix).
|
||||
switch upper {
|
||||
case "RET", "NOP", "CALL", "JMP":
|
||||
return true
|
||||
}
|
||||
if _, ok := condCode(upper); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// VEX/EVEX and friends: the trailing B/W/L/Q/D is part of the mnemonic.
|
||||
base, _, err := parseEvexSuffix(upper)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) ||
|
||||
base == "KMOVW" || base == "KMOVQ" {
|
||||
return true
|
||||
}
|
||||
|
||||
// CMOV carries size then condition (CMOVLGT); SET carries the condition
|
||||
// alone (SETNE).
|
||||
if rest, ok := strings.CutPrefix(upper, "CMOV"); ok && len(rest) >= 2 {
|
||||
if _, ok := jccMap[rest[1:]]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(upper, "SET"); ok {
|
||||
if _, ok := jccMap[rest]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy SSE shuffles and packed binaries dispatch on the full name.
|
||||
if _, ok := sseShufTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseBinTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// The size-suffix split: retry the tables and the scalar switch on the
|
||||
// base.
|
||||
base2, size := splitSize(upper)
|
||||
if size == 0 {
|
||||
size = 8
|
||||
}
|
||||
_ = size
|
||||
if base2 != upper {
|
||||
if _, ok := sseBinTable[base2]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
switch base2 {
|
||||
case "MOV",
|
||||
"ADD", "SUB", "AND", "OR", "XOR", "CMP",
|
||||
"TEST",
|
||||
"LEA",
|
||||
"INC", "DEC", "NEG", "NOT",
|
||||
"SHL", "SHR", "SAR",
|
||||
"IMUL", "IMUL3",
|
||||
"PUSH", "POP",
|
||||
"BSF", "BSR", "LZCNT", "TZCNT", "POPCNT",
|
||||
"BSWAP",
|
||||
"PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2",
|
||||
"MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||
"CVTSL2SD", "CVTSQ2SD",
|
||||
"MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+52
-2
@@ -76,6 +76,21 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
if size == 0 {
|
||||
size = 8 // default operand size in 64-bit mode (e.g. PUSHQ)
|
||||
}
|
||||
// Legacy SSE imm8 shuffles whose names end in W/H (PSHUFLW,
|
||||
// PSHUFHW) must dispatch BEFORE the size-suffix split, and the
|
||||
// others ride along.
|
||||
if m, ok := sseShufTable[upper]; ok {
|
||||
return e.encodeSSEShuf(m, ops)
|
||||
}
|
||||
// Legacy SSE packed binaries dispatch on the full name: the packed
|
||||
// integer mnemonics carry real width suffixes (PADDB/PCMPGTW/...),
|
||||
// which the size split must not eat.
|
||||
if m, ok := sseBinTable[upper]; ok {
|
||||
return e.encodeSSEBin(m, ops)
|
||||
}
|
||||
if m, ok := sseBinTable[base]; ok {
|
||||
return e.encodeSSEBin(m, ops)
|
||||
}
|
||||
switch base {
|
||||
case "MOV":
|
||||
return e.encodeMov(ops, size)
|
||||
@@ -95,8 +110,12 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodePushPop(ops, true)
|
||||
case "POP":
|
||||
return e.encodePushPop(ops, false)
|
||||
case "LZCNT", "TZCNT":
|
||||
case "BSF", "BSR", "LZCNT", "TZCNT", "POPCNT":
|
||||
return e.encodeCount(base, ops, size)
|
||||
case "BSWAP":
|
||||
return e.encodeBswap(ops, size)
|
||||
case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2":
|
||||
return e.encodePrefetch(base, ops)
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX":
|
||||
return e.encodeMovExtend(base, ops)
|
||||
case "CVTSL2SD", "CVTSQ2SD":
|
||||
@@ -107,6 +126,30 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
|
||||
// encodePrefetch emits the 0F 18 /r prefetch hints: the reg field selects
|
||||
// the locality (NTA=0, T0=1, T1=2, T2=3) and the single operand is memory.
|
||||
func (e *enc) encodePrefetch(base string, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects one memory operand", base)
|
||||
}
|
||||
m, ok := ops[0].(Mem)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s requires a memory operand", base)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0x18})
|
||||
if err := setMem(i, prefetchVariant[base], m); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
var prefetchVariant = map[string]int{
|
||||
"PREFETCHNTA": 0,
|
||||
"PREFETCHT0": 1,
|
||||
"PREFETCHT1": 2,
|
||||
"PREFETCHT2": 3,
|
||||
}
|
||||
|
||||
// splitSize separates a trailing B/W/L/Q size suffix from the mnemonic.
|
||||
func splitSize(upper string) (base string, size int) {
|
||||
if upper == "" {
|
||||
@@ -297,6 +340,13 @@ func memComponents(regField int, m Mem) (modrm, sib int, disp []byte, xBit, bBit
|
||||
return regField<<3 | 0x05, -1, le32(m.Disp), 0, 0, nil // mod=00, rm=101
|
||||
}
|
||||
|
||||
// The SIB scale field only encodes 1/2/4/8; the Go assembler rejects
|
||||
// anything else ("bad scale: 16"), so a silent fallback to scale 1 here
|
||||
// would mis-assemble the operand instead of reporting it.
|
||||
if m.HasIndex && m.Scale != 1 && m.Scale != 2 && m.Scale != 4 && m.Scale != 8 {
|
||||
return 0, -1, nil, 0, 0, fmt.Errorf("bad scale: %d", m.Scale)
|
||||
}
|
||||
|
||||
needSIB := m.HasIndex || (m.HasBase && m.Base.idx&7 == 4)
|
||||
|
||||
var mod int
|
||||
@@ -369,7 +419,7 @@ func le16(v int64) []byte {
|
||||
func le64(v int64) []byte {
|
||||
u := uint64(v)
|
||||
b := make([]byte, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
for i := range 8 {
|
||||
b[i] = byte(u >> (8 * i))
|
||||
}
|
||||
return b
|
||||
|
||||
+154
-3
@@ -4,6 +4,7 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -57,8 +58,11 @@ func TestMov(t *testing.T) {
|
||||
checkSyntax(t, "mov qword ptr [rbx], rax", "MOVQ", AX, Ptr(BX, 0, 8))
|
||||
checkSyntax(t, "mov rbx, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), BX)
|
||||
checkSyntax(t, "mov rbx, qword ptr [rsi+4*rbx]", "MOVQ", Idx(SI, BX, 4, 0, 8), BX)
|
||||
checkSyntax(t, "mov rax, 0x5", "MOVQ", Imm(5), AX)
|
||||
checkSyntax(t, "mov r8, 0x5", "MOVQ", Imm(5), Reg{idx: 8, size: 8})
|
||||
// A small positive immediate compresses to the 32-bit zero-extending
|
||||
// form (matching go tool asm), so the disassembler renders the 32-bit
|
||||
// register name even for MOVQ.
|
||||
checkSyntax(t, "mov eax, 0x5", "MOVQ", Imm(5), AX)
|
||||
checkSyntax(t, "mov r8d, 0x5", "MOVQ", Imm(5), Reg{idx: 8, size: 8})
|
||||
checkSyntax(t, "mov qword ptr [rax], 0x5", "MOVQ", Imm(5), Ptr(AX, 0, 8))
|
||||
checkSyntax(t, "mov r12, r13", "MOVQ", Reg{idx: 13, size: 8}, Reg{idx: 12, size: 8})
|
||||
}
|
||||
@@ -74,13 +78,63 @@ func TestALU(t *testing.T) {
|
||||
checkSyntax(t, "cmp rsi, r10", "CMPQ", SI, Reg{idx: 10, size: 8})
|
||||
checkSyntax(t, "add rbx, qword ptr [rax]", "ADDQ", Ptr(AX, 0, 8), BX)
|
||||
checkSyntax(t, "add qword ptr [rax], rbx", "ADDQ", BX, Ptr(AX, 0, 8))
|
||||
checkSyntax(t, "cmp rbx, -0x20", "CMPQ", Imm(-32), BX)
|
||||
// The Go assembler rejects the immediate-first CMP spelling outright,
|
||||
// so Encode errors instead of silently emitting the swapped form.
|
||||
if _, err := Encode("CMPQ", Imm(-32), BX); err == nil {
|
||||
t.Errorf("Encode(CMPQ imm-first) should error, got success")
|
||||
}
|
||||
// The Go assembler's own spelling: immediate second.
|
||||
checkSyntax(t, "cmp ecx, 0x1f", "CMPL", CX, Imm(31))
|
||||
checkSyntax(t, "cmp ecx, -0x80000000", "CMPL", CX, Imm(-2147483648))
|
||||
checkSyntax(t, "cmp r9, -0x80000000", "CMPQ", Reg{idx: 9, size: 8}, Imm(-2147483648))
|
||||
}
|
||||
|
||||
// TestScalarXmmRegMoves pins the Go-assembler byte forms of scalar
|
||||
// MOVQ/MOVL between GPRs and XMM registers (66 REX.W 0F 6E/0F 7E) and the
|
||||
// memory forms (F3 0F 7E load, 66 0F D6 store), all byte-for-byte.
|
||||
func TestScalarXmmRegMoves(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"MOVQ AX,X1", "MOVQ", []Operand{AX, vreg(t, "X1")}, "66480f6ec8"},
|
||||
{"MOVQ DX,X2", "MOVQ", []Operand{DX, vreg(t, "X2")}, "66480f6ed2"},
|
||||
{"MOVQ X1,AX", "MOVQ", []Operand{vreg(t, "X1"), AX}, "66480f7ec8"},
|
||||
{"MOVQ X0,DX", "MOVQ", []Operand{vreg(t, "X0"), DX}, "66480f7ec2"},
|
||||
{"MOVL AX,X1", "MOVL", []Operand{AX, vreg(t, "X1")}, "660f6ec8"},
|
||||
{"MOVL X1,AX", "MOVL", []Operand{vreg(t, "X1"), AX}, "660f7ec8"},
|
||||
{"MOVQ (SI),X1", "MOVQ", []Operand{Ptr(SI, 0, 8), vreg(t, "X1")}, "f30f7e0e"},
|
||||
{"MOVQ X3,(DI)", "MOVQ", []Operand{vreg(t, "X3"), Ptr(DI, 0, 8)}, "660fd61f"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBadScale pins the go-tool-asm parity of rejecting SIB scales the
|
||||
// hardware cannot encode.
|
||||
func TestBadScale(t *testing.T) {
|
||||
for _, sc := range []int{3, 5, 16, 32} {
|
||||
if _, err := Encode("LEAQ", Idx(SI, BX, sc, 0, 8), AX); err == nil {
|
||||
t.Errorf("LEAQ scale %d: expected error, got success", sc)
|
||||
}
|
||||
}
|
||||
for _, sc := range []int{1, 2, 4, 8} {
|
||||
if _, err := Encode("LEAQ", Idx(SI, BX, sc, 0, 8), AX); err != nil {
|
||||
t.Errorf("LEAQ scale %d: %v", sc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLea(t *testing.T) {
|
||||
checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8})
|
||||
checkSyntax(t, "lea rax, ptr [rbx+0x8]", "LEAQ", Ptr(BX, 0x8, 8), AX)
|
||||
@@ -203,6 +257,21 @@ func TestScalarGroundTruth(t *testing.T) {
|
||||
{"LZCNTQ R8,R9", "LZCNTQ", []Operand{r8, r9}, "f34d0fbdc8", "LZCNT"},
|
||||
{"LZCNTW AX,CX", "LZCNTW", []Operand{AX, CX}, "66f30fbdc8", "LZCNT"},
|
||||
{"TZCNTL AX,CX", "TZCNTL", []Operand{AX, CX}, "f30fbcc8", "TZCNT"},
|
||||
// Bit scan: BSF/BSR are the unprefixed forms of TZCNT/LZCNT's map.
|
||||
{"BSFL AX,CX", "BSFL", []Operand{AX, CX}, "0fbcc8", "BSF"},
|
||||
{"BSFQ R8,R9", "BSFQ", []Operand{r8, r9}, "4d0fbcc8", "BSF"},
|
||||
{"BSFW AX,CX", "BSFW", []Operand{AX, CX}, "660fbcc8", "BSF"},
|
||||
{"BSRL AX,CX", "BSRL", []Operand{AX, CX}, "0fbdc8", "BSR"},
|
||||
{"BSRQ AX,CX", "BSRQ", []Operand{AX, CX}, "480fbdc8", "BSR"},
|
||||
{"POPCNTL AX,CX", "POPCNTL", []Operand{AX, CX}, "f30fb8c8", "POPCNT"},
|
||||
{"POPCNTQ R8,R9", "POPCNTQ", []Operand{r8, r9}, "f34d0fb8c8", "POPCNT"},
|
||||
// A 64-bit immediate that fits a signed int32 is compressed exactly
|
||||
// as the Go assembler does: positive via B8+rd without REX.W
|
||||
// (zero-extended), negative via REX.W C7 /0 (sign-extended).
|
||||
{"MOVQ $4,BX", "MOVQ", []Operand{Imm(4), BX}, "bb04000000", "MOV"},
|
||||
{"MOVQ $4,R8", "MOVQ", []Operand{Imm(4), r8}, "41b804000000", "MOV"},
|
||||
{"MOVQ $-1,BX", "MOVQ", []Operand{Imm(-1), BX}, "48c7c3ffffffff", "MOV"},
|
||||
{"MOVQ big,BX", "MOVQ", []Operand{Imm(0x1122334455667788), BX}, "48bb8877665544332211", "MOV"},
|
||||
{"CMOVLGT CX,AX", "CMOVLGT", []Operand{CX, AX}, "0f4fc1", "CMOVG"},
|
||||
{"CMOVLEQ CX,AX", "CMOVLEQ", []Operand{CX, AX}, "0f44c1", "CMOVE"},
|
||||
{"CMOVQGT R9,R8", "CMOVQGT", []Operand{r9, r8}, "4d0f4fc1", "CMOVG"},
|
||||
@@ -294,3 +363,85 @@ func TestScalarErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSEBinGroundTruth checks the legacy packed/scalar binary family
|
||||
// byte for byte (no prefix / 66 / F2 / F3 variants).
|
||||
func TestSSEBinGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"MULPS X0,X1", "MULPS", []Operand{vreg(t, "X0"), vreg(t, "X1")}, "0f59c8"},
|
||||
{"MULPS (DI),X1", "MULPS", []Operand{Ptr(DI, 0, 16), vreg(t, "X1")}, "0f590f"},
|
||||
{"ADDPD X1,X2", "ADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "660f58d1"},
|
||||
{"XORPS X0,X0", "XORPS", []Operand{vreg(t, "X0"), vreg(t, "X0")}, "0f57c0"},
|
||||
{"UNPCKLPS X0,X0", "UNPCKLPS", []Operand{vreg(t, "X0"), vreg(t, "X0")}, "0f14c0"},
|
||||
{"MULSD X1,X2", "MULSD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "f20f59d1"},
|
||||
{"ADDSS (DI),X0", "ADDSS", []Operand{Ptr(DI, 0, 4), vreg(t, "X0")}, "f30f5807"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s = %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSEShuffleGroundTruth checks the imm8 shuffle family: immediate
|
||||
// first in Plan 9 order, encoded last on the wire.
|
||||
func TestSSEShuffleGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"SHUFPS $0,X0,X0", "SHUFPS", []Operand{Imm(0), vreg(t, "X0"), vreg(t, "X0")}, "0fc6c000"},
|
||||
{"SHUFPS $27,X1,X2", "SHUFPS", []Operand{Imm(27), vreg(t, "X1"), vreg(t, "X2")}, "0fc6d11b"},
|
||||
{"PSHUFD $0,X0,X0", "PSHUFD", []Operand{Imm(0), vreg(t, "X0"), vreg(t, "X0")}, "660f70c000"},
|
||||
{"PSHUFLW $3,(DI),X1", "PSHUFLW", []Operand{Imm(3), Ptr(DI, 0, 8), vreg(t, "X1")}, "f20f700f03"},
|
||||
{"PSHUFHW $2,X1,X2", "PSHUFHW", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2")}, "f30f70d102"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s = %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMOVQXMMGroundTruth pins the SSE2 packed-quadword move encodings:
|
||||
// loads and register moves on F3 0F 7E, stores on 66 0F D6 — the forms
|
||||
// the GPR-move fallback silently corrupted.
|
||||
func TestMOVQXMMGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"MOVQ (DI),X0", "MOVQ", []Operand{Ptr(DI, 0, 8), vreg(t, "X0")}, "f30f7e07"},
|
||||
{"MOVQ X1,X2", "MOVQ", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "f30f7ed1"},
|
||||
{"MOVQ X0,(DI)", "MOVQ", []Operand{vreg(t, "X0"), Ptr(DI, 0, 8)}, "660fd607"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s = %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-11
@@ -166,6 +166,19 @@ var evexTable = map[string]evexSpec{
|
||||
"VCMPSD": {1, 0xC2, 1, 3, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
||||
"VCMPSS": {1, 0xC2, 0, 2, -1, vexNDS3Imm, [3]int{4, 4, 4}},
|
||||
|
||||
// EVEX.66.0F3A — integer compares with an opmask destination, the same
|
||||
// NDS3Imm-with-k-reg shape as the floating-point compares; W selects the
|
||||
// operand width (byte/word vs dword/qword), the opcode the signedness.
|
||||
// The memory form takes a full vector, so disp8×N is 16/32/64.
|
||||
"VPCMPB": {3, 0x3F, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPUB": {3, 0x3E, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPW": {3, 0x3F, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPUW": {3, 0x3E, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPD": {3, 0x1F, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPUD": {3, 0x1E, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPQ": {3, 0x1F, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
"VPCMPUQ": {3, 0x1E, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||
|
||||
// EVEX.66.0F38 — permutes (NDS form).
|
||||
"VPERMB": {2, 0x8D, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
"VPERMW": {2, 0x8D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||
@@ -590,12 +603,12 @@ func (s evexSuffix) evexOnly() bool {
|
||||
// broadcast together with rounding/SAE.
|
||||
func parseEvexSuffix(mnem string) (string, evexSuffix, error) {
|
||||
sfx := evexSuffix{rounding: -1}
|
||||
i := strings.IndexByte(mnem, '.')
|
||||
if i < 0 {
|
||||
before, after, ok := strings.Cut(mnem, ".")
|
||||
if !ok {
|
||||
return mnem, sfx, nil
|
||||
}
|
||||
base := mnem[:i]
|
||||
parts := strings.Split(mnem[i+1:], ".")
|
||||
base := before
|
||||
parts := strings.Split(after, ".")
|
||||
seen := map[string]bool{}
|
||||
for j, p := range parts {
|
||||
if seen[p] {
|
||||
@@ -605,7 +618,7 @@ func parseEvexSuffix(mnem string) (string, evexSuffix, error) {
|
||||
switch p {
|
||||
case "Z":
|
||||
if j != len(parts)-1 {
|
||||
return "", sfx, fmt.Errorf("the .Z suffix must come last in %q", mnem[i+1:])
|
||||
return "", sfx, fmt.Errorf("the .Z suffix must come last in %q", after)
|
||||
}
|
||||
sfx.zeroing = true
|
||||
case "SAE":
|
||||
@@ -625,7 +638,7 @@ func parseEvexSuffix(mnem string) (string, evexSuffix, error) {
|
||||
}
|
||||
}
|
||||
if sfx.bcst && (sfx.sae || sfx.rounding >= 0) {
|
||||
return "", sfx, fmt.Errorf("cannot combine .BCST with rounding or SAE in %q", mnem[i+1:])
|
||||
return "", sfx, fmt.Errorf("cannot combine .BCST with rounding or SAE in %q", after)
|
||||
}
|
||||
return base, sfx, nil
|
||||
}
|
||||
@@ -688,7 +701,7 @@ func splitMask(ops []Operand) ([]Operand, int, error) {
|
||||
return nil, 0, fmt.Errorf("at most one mask register operand")
|
||||
}
|
||||
if r.idx == 0 {
|
||||
return nil, 0, fmt.Errorf("K0 is not a usable mask register")
|
||||
return nil, 0, fmt.Errorf("k0 is not a usable mask register")
|
||||
}
|
||||
mask = r.idx
|
||||
continue
|
||||
@@ -1190,7 +1203,7 @@ func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand,
|
||||
// The b bit and the L'L field carry the rounding/SAE/broadcast mode:
|
||||
// a rounding mode replaces L'L with the rc value, plain SAE and
|
||||
// broadcast keep the vector length.
|
||||
b, ll := 0, ll
|
||||
b := 0
|
||||
switch {
|
||||
case sfx.rounding >= 0:
|
||||
b, ll = 1, sfx.rounding
|
||||
@@ -1471,24 +1484,60 @@ type kOpSpec struct {
|
||||
|
||||
var kOpsTable = map[string]kOpSpec{
|
||||
// k ← k OP k: reg = dst, vvvv = src1, rm = src2 (three opmask
|
||||
// registers).
|
||||
// registers). Byte/word widths share W0 and differ by the 66 prefix;
|
||||
// dword/qword share the W bit selection the Go assembler emits.
|
||||
"KANDB": {1, 0x41, 0, 1, 1, vexNDS3},
|
||||
"KANDW": {1, 0x41, 0, 0, 1, vexNDS3},
|
||||
"KANDD": {1, 0x41, 1, 1, 1, vexNDS3},
|
||||
"KANDQ": {1, 0x41, 1, 0, 1, vexNDS3},
|
||||
"KANDNB": {1, 0x42, 0, 1, 1, vexNDS3},
|
||||
"KANDNW": {1, 0x42, 0, 0, 1, vexNDS3},
|
||||
"KANDND": {1, 0x42, 1, 1, 1, vexNDS3},
|
||||
"KANDNQ": {1, 0x42, 1, 0, 1, vexNDS3},
|
||||
"KORB": {1, 0x45, 0, 1, 1, vexNDS3},
|
||||
"KORW": {1, 0x45, 0, 0, 1, vexNDS3},
|
||||
"KORD": {1, 0x45, 1, 1, 1, vexNDS3},
|
||||
"KORQ": {1, 0x45, 1, 0, 1, vexNDS3},
|
||||
"KXNORB": {1, 0x46, 0, 1, 1, vexNDS3},
|
||||
"KXNORW": {1, 0x46, 0, 0, 1, vexNDS3},
|
||||
"KXNORD": {1, 0x46, 1, 1, 1, vexNDS3},
|
||||
"KXNORQ": {1, 0x46, 1, 0, 1, vexNDS3},
|
||||
"KXORB": {1, 0x47, 0, 1, 1, vexNDS3},
|
||||
"KXORW": {1, 0x47, 0, 0, 1, vexNDS3},
|
||||
"KXORD": {1, 0x47, 1, 1, 1, vexNDS3},
|
||||
"KXORQ": {1, 0x47, 1, 0, 1, vexNDS3},
|
||||
"KUNPCKBW": {1, 0x4B, 0, 1, 1, vexNDS3},
|
||||
"KUNPCKDQ": {1, 0x4B, 1, 0, 1, vexNDS3},
|
||||
"KADDB": {1, 0x4A, 0, 1, 1, vexNDS3},
|
||||
"KADDW": {1, 0x4A, 0, 0, 1, vexNDS3},
|
||||
"KADDD": {1, 0x4A, 1, 1, 1, vexNDS3},
|
||||
"KADDQ": {1, 0x4A, 1, 0, 1, vexNDS3},
|
||||
// k ← OP k (KNOT) and flags ← k OP k (KORTEST): reg = dst, rm = src.
|
||||
// k ← OP k (KNOT), k ← k AND~ k (KTEST-style RM) and flags ← k OP k
|
||||
// (KORTEST): reg = dst, rm = src.
|
||||
"KNOTB": {1, 0x44, 0, 1, 0, vexRM},
|
||||
"KNOTW": {1, 0x44, 0, 0, 0, vexRM},
|
||||
"KNOTD": {1, 0x44, 1, 1, 0, vexRM},
|
||||
"KNOTQ": {1, 0x44, 1, 0, 0, vexRM},
|
||||
"KORTESTB": {1, 0x98, 0, 1, 0, vexRM},
|
||||
"KORTESTW": {1, 0x98, 0, 0, 0, vexRM},
|
||||
"KORTESTD": {1, 0x98, 1, 1, 0, vexRM},
|
||||
// OP $imm, src, dst: reg = dst, rm = src, imm8.
|
||||
"KORTESTQ": {1, 0x98, 1, 0, 0, vexRM},
|
||||
"KTESTB": {1, 0x99, 0, 1, 0, vexRM},
|
||||
"KTESTW": {1, 0x99, 0, 0, 0, vexRM},
|
||||
"KTESTD": {1, 0x99, 1, 1, 0, vexRM},
|
||||
"KTESTQ": {1, 0x99, 1, 0, 0, vexRM},
|
||||
// OP $imm, src, dst: reg = dst, rm = src, imm8. The opcodes split by
|
||||
// direction (0x32/0x33 left, 0x30/0x31 right) and within each by
|
||||
// element half (0x32 byte/word, 0x33 dword/qword); W picks byte/dword
|
||||
// (W0) against word/qword (W1).
|
||||
"KSHIFTLB": {3, 0x32, 0, 1, 0, vexImmRM},
|
||||
"KSHIFTLW": {3, 0x32, 1, 1, 0, vexImmRM},
|
||||
"KSHIFTLD": {3, 0x33, 0, 1, 0, vexImmRM},
|
||||
"KSHIFTLQ": {3, 0x33, 1, 1, 0, vexImmRM},
|
||||
"KSHIFTRB": {3, 0x30, 0, 1, 0, vexImmRM},
|
||||
"KSHIFTRW": {3, 0x30, 1, 1, 0, vexImmRM},
|
||||
"KSHIFTRD": {3, 0x31, 0, 1, 0, vexImmRM},
|
||||
"KSHIFTRQ": {3, 0x31, 1, 1, 0, vexImmRM},
|
||||
}
|
||||
|
||||
// isKOp reports whether the mnemonic is an opmask-register instruction.
|
||||
|
||||
@@ -319,6 +319,43 @@ func TestEvexExtendedGroundTruth(t *testing.T) {
|
||||
{"KORTESTD", "KORTESTD", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c4e1f998d1"},
|
||||
{"KMOVQ k,k", "KMOVQ", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c4e1f890d1"},
|
||||
{"KMOVQ gpr,k", "KMOVQ", []Operand{BX, vreg(t, "K1")}, "c4e1fb92cb"},
|
||||
// Completed opmask families (ANDN, NOT, OR/XOR word+qword, TEST,
|
||||
// word-width shifts; byte-exact against go tool asm).
|
||||
{"KANDNW", "KANDNW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec42d9"},
|
||||
{"KANDNB", "KANDNB", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c5d542f4"},
|
||||
{"KANDND", "KANDND", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ed42d9"},
|
||||
{"KANDNQ", "KANDNQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d442f4"},
|
||||
{"KANDD", "KANDD", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ed41d9"},
|
||||
{"KADDD", "KADDD", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d54af4"},
|
||||
{"KNOTW", "KNOTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f844d1"},
|
||||
{"KNOTD", "KNOTD", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c4e1f944e3"},
|
||||
{"KNOTQ", "KNOTQ", []Operand{vreg(t, "K5"), vreg(t, "K6")}, "c4e1f844f5"},
|
||||
{"KORW", "KORW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec45d9"},
|
||||
{"KORQ", "KORQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d445f4"},
|
||||
{"KXNORB", "KXNORB", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ed46d9"},
|
||||
{"KXORW", "KXORW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec47d9"},
|
||||
{"KXORQ", "KXORQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d447f4"},
|
||||
{"KORTESTW", "KORTESTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f898d1"},
|
||||
{"KORTESTB", "KORTESTB", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c5f998e3"},
|
||||
{"KORTESTQ", "KORTESTQ", []Operand{vreg(t, "K5"), vreg(t, "K6")}, "c4e1f898f5"},
|
||||
{"KTESTW", "KTESTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f899d1"},
|
||||
{"KTESTD", "KTESTD", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c4e1f999e3"},
|
||||
{"KSHIFTLB", "KSHIFTLB", []Operand{Imm(1), vreg(t, "K1"), vreg(t, "K2")}, "c4e37932d101"},
|
||||
{"KSHIFTLD", "KSHIFTLD", []Operand{Imm(2), vreg(t, "K3"), vreg(t, "K4")}, "c4e37933e302"},
|
||||
{"KSHIFTLQ", "KSHIFTLQ", []Operand{Imm(3), vreg(t, "K5"), vreg(t, "K6")}, "c4e3f933f503"},
|
||||
{"KSHIFTRB", "KSHIFTRB", []Operand{Imm(4), vreg(t, "K1"), vreg(t, "K2")}, "c4e37930d104"},
|
||||
{"KSHIFTRW", "KSHIFTRW", []Operand{Imm(5), vreg(t, "K3"), vreg(t, "K4")}, "c4e3f930e305"},
|
||||
{"KSHIFTRQ", "KSHIFTRQ", []Operand{Imm(6), vreg(t, "K5"), vreg(t, "K6")}, "c4e3f931f506"},
|
||||
// Integer compares with an opmask destination (0F3A map, the
|
||||
// go-bzip2 partition kernel's classify instructions).
|
||||
{"VPCMPUB", "VPCMPUB", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X0"), vreg(t, "K1")}, "62f37d083ec901"},
|
||||
{"VPCMPB", "VPCMPB", []Operand{Imm(2), vreg(t, "Y2"), vreg(t, "Y3"), vreg(t, "K2")}, "62f365283fd202"},
|
||||
{"VPCMPUW", "VPCMPUW", []Operand{Imm(5), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3")}, "62f3ed483ed905"},
|
||||
{"VPCMPW", "VPCMPW", []Operand{Imm(6), vreg(t, "X3"), vreg(t, "X4"), vreg(t, "K4")}, "62f3dd083fe306"},
|
||||
{"VPCMPD", "VPCMPD", []Operand{Imm(0), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "K1")}, "62f36d281fc900"},
|
||||
{"VPCMPUD", "VPCMPUD", []Operand{Imm(1), vreg(t, "Z2"), vreg(t, "Z3"), vreg(t, "K2")}, "62f365481ed201"},
|
||||
{"VPCMPQ", "VPCMPQ", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K3")}, "62f3ed081fd902"},
|
||||
{"VPCMPUQ", "VPCMPUQ", []Operand{Imm(3), vreg(t, "Y3"), vreg(t, "Y4"), vreg(t, "K4")}, "62f3dd281ee303"},
|
||||
// Lane extract / insert.
|
||||
{"VEXTRACTF32X4", "VEXTRACTF32X4", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "X2")}, "62f37d2819ca01"},
|
||||
{"VEXTRACTI64X2", "VEXTRACTI64X2", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "X2")}, "62f3fd2839ca01"},
|
||||
|
||||
+260
-90
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -22,11 +23,20 @@ import (
|
||||
//
|
||||
// The object carries what the linker requires of an assembly object: the
|
||||
// functions (non-package symbols, as cmd/asm emits them), the GLOBL data,
|
||||
// one FuncInfo per function, and the pc-value tables (pcsp, pcfile,
|
||||
// pcline, pcinline). DWARF and the implicit funcdata symbols are omitted;
|
||||
// the linker fills their defaults.
|
||||
// one FuncInfo per function, the per-function DWARF symbols (the
|
||||
// .debug_line program and the subprogram DIE, which the linker's DWARF
|
||||
// pass reads verbatim), and the pc-value tables (pcsp, pcfile, pcline,
|
||||
// pcinline). The implicit funcdata symbols are omitted; the linker fills
|
||||
// their defaults.
|
||||
//
|
||||
// emitGOObject is architecture-agnostic; the per-architecture GOObject*
|
||||
// methods supply the toolchain preamble, the MinLC (pc-value delta unit)
|
||||
// and the relocation-type mapping for code relocations.
|
||||
|
||||
// GOOBJ block indices (cmd/internal/goobj).
|
||||
// GOOBJ block indices (cmd/internal/goobj). These MUST match the real
|
||||
// archive layout: the emitter writes the header offsets per index and the
|
||||
// reader (groundtruth, goobj_resolve) parses real Go archives with them.
|
||||
// blkAutolib is unused by the emitter but still defines index 0.
|
||||
const (
|
||||
blkAutolib = iota
|
||||
blkPkgIdx
|
||||
@@ -51,9 +61,11 @@ const (
|
||||
|
||||
// Symbol kinds used by assembly objects (cmd/internal/objabi).
|
||||
const (
|
||||
kindSTEXT = 1
|
||||
kindSRODATA = 3
|
||||
kindSDATA = 7
|
||||
kindSTEXT = 1
|
||||
kindSRODATA = 3
|
||||
kindSDATA = 7
|
||||
kindSDWARFFCN = 14
|
||||
kindSDWARFLINES = 20
|
||||
)
|
||||
|
||||
// Symbol flags (cmd/internal/goobj).
|
||||
@@ -66,11 +78,13 @@ const (
|
||||
|
||||
// Aux entry types (cmd/internal/goobj).
|
||||
const (
|
||||
auxFuncInfo = 1
|
||||
auxPcsp = 7
|
||||
auxPcfile = 8
|
||||
auxPcline = 9
|
||||
auxPcinline = 10
|
||||
auxFuncInfo = 1
|
||||
auxDwarfInfo = 3
|
||||
auxDwarfLines = 6
|
||||
auxPcsp = 7
|
||||
auxPcfile = 8
|
||||
auxPcline = 9
|
||||
auxPcinline = 10
|
||||
)
|
||||
|
||||
// FuncInfo flags (internal/abi).
|
||||
@@ -80,7 +94,49 @@ const (
|
||||
)
|
||||
|
||||
// Relocation types (cmd/internal/objabi).
|
||||
const relocPCRel = 14
|
||||
// R_PCREL and R_ADDR are stable across Go versions.
|
||||
const (
|
||||
relocPCRel = 14 // R_PCREL
|
||||
relocAddr = 1 // R_ADDR
|
||||
)
|
||||
|
||||
// relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the
|
||||
// installed Go toolchain. The value shifted between Go 1.26 (103) and
|
||||
// Go 1.27 (106) because new LoongArch relocations were inserted before it.
|
||||
func relocDWTXTADDRU4() uint16 {
|
||||
if isGo127OrLater() {
|
||||
return 106
|
||||
}
|
||||
return 103
|
||||
}
|
||||
|
||||
var (
|
||||
goVersionOnce sync.Once
|
||||
goVersionGT26 bool
|
||||
)
|
||||
|
||||
// isGo127OrLater reports whether the installed Go toolchain is 1.27 or later.
|
||||
func isGo127OrLater() bool {
|
||||
goVersionOnce.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err := exec.Command(goBin, "version").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// "go version go1.27rc1 linux/amd64"
|
||||
s := string(out)
|
||||
for _, prefix := range []string{"go version go1.27", "go version go1.28", "go version go1.29", "go version go2."} {
|
||||
if strings.Contains(s, prefix) {
|
||||
goVersionGT26 = true
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return goVersionGT26
|
||||
}
|
||||
|
||||
// Special package indices for symbol references.
|
||||
const (
|
||||
@@ -110,6 +166,13 @@ func (s goSym) append(b []byte, strOff map[string]uint32) []byte {
|
||||
return binary.LittleEndian.AppendUint32(b, s.align)
|
||||
}
|
||||
|
||||
// dwarfRelocSet attaches emitter-generated relocations (the DWARF
|
||||
// lines/info symbols' address references) to a definition index.
|
||||
type dwarfRelocSet struct {
|
||||
si int
|
||||
relocs []goobjReloc
|
||||
}
|
||||
|
||||
// GOObject returns the image as a GOOBJ object file for the given package
|
||||
// path (the linker qualifies the exported symbols with it, the way cmd/asm
|
||||
// does with its -p flag). srcPath names the source file recorded in the
|
||||
@@ -117,19 +180,90 @@ func (s goSym) append(b []byte, strOff map[string]uint32) []byte {
|
||||
// captured from the installed go tool asm, so the output links with the
|
||||
// toolchain it was produced on — exactly like a real assembly object.
|
||||
func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
if pkgPath == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||
}
|
||||
pre, err := toolchainObjectPreamble()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// amd64: MinLC 1, R_PCREL for the code relocations.
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
|
||||
}
|
||||
|
||||
// The symbol tables. Package definitions: the GLOBL symbols, then one
|
||||
// anonymous FuncInfo symbol per function. Non-package definitions: the
|
||||
// pc-value tables and the functions themselves, as cmd/asm lays them
|
||||
// out. defIdx maps a GLOBL's bare name to its definition index for the
|
||||
// relocations; fnNpIdx maps a function to its non-package index.
|
||||
// emitGOObject assembles the GOOBJ payload for any architecture. pre is
|
||||
// the toolchain's object preamble; minLC is the architecture's minimum
|
||||
// instruction length, the unit of the pc-value table deltas; relocField
|
||||
// maps a code relocation to its objabi relocation type and the width of
|
||||
// the instruction field the linker writes.
|
||||
func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, relocField func(Reloc) (uint16, uint8)) ([]byte, error) {
|
||||
if pkgPath == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||
}
|
||||
|
||||
// The non-package definitions first — the DWARF symbols reference the
|
||||
// functions by these indices: per function the four pc-value tables
|
||||
// and the function itself, as cmd/asm lays them out.
|
||||
type npSym struct {
|
||||
sym goSym
|
||||
data []byte
|
||||
}
|
||||
var nps []npSym
|
||||
type pcRefs struct{ sp, file, line, inl int }
|
||||
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||
fnNpIdx := make([]int, len(img.Funcs))
|
||||
for i, fn := range img.Funcs {
|
||||
tables := []struct {
|
||||
data []byte
|
||||
dst *int
|
||||
}{
|
||||
{pcspTable(fn, minLC), &pcIdx[i].sp},
|
||||
{pcValueFlat(0, fn.Size, minLC), &pcIdx[i].file},
|
||||
{pcValueFlat(int32(fn.Line), fn.Size, minLC), &pcIdx[i].line},
|
||||
{pcValueFlat(-1, fn.Size, minLC), &pcIdx[i].inl},
|
||||
}
|
||||
for _, t := range tables {
|
||||
*t.dst = len(nps)
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||
data: t.data,
|
||||
})
|
||||
}
|
||||
name := fn.Name
|
||||
abi := uint16(0)
|
||||
if fn.Static {
|
||||
abi = symABIStatic
|
||||
} else {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
flag := uint8(0)
|
||||
if fn.NoSplit {
|
||||
flag |= symFlagNoSplit
|
||||
}
|
||||
fnNpIdx[i] = len(nps)
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
for _, r := range fn.Relocs {
|
||||
// Only the amd64 encoder resolves file-local static symbols
|
||||
// into a disp32 field at assemble time; GOOBJ must leave that
|
||||
// field zero for the linker to fill. The RISC-V and LoongArch
|
||||
// encoders emit zero immediates with a relocation instead, and
|
||||
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||
// zeroing r.Off would erase the opcode/register bits the linker
|
||||
// preserves when it patches only the immediate.
|
||||
if r.Kind != RelPCRel32 {
|
||||
continue
|
||||
}
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
|
||||
// The package definitions: the GLOBL symbols, then, per function, the
|
||||
// FuncInfo and the two DWARF symbols (the .debug_line program and the
|
||||
// subprogram DIE). defIdx maps a GLOBL's bare name to its definition
|
||||
// index for the code relocations.
|
||||
var defs []goSym
|
||||
var defData [][]byte
|
||||
defIdx := map[string]int{}
|
||||
@@ -155,74 +289,82 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||
}
|
||||
fnFiIdx := make([]int, len(img.Funcs))
|
||||
for i := range img.Funcs {
|
||||
data := marshalFuncInfo(img.Funcs[i])
|
||||
fnLinesIdx := make([]int, len(img.Funcs))
|
||||
fnDIEIdx := make([]int, len(img.Funcs))
|
||||
var dwarfRelocs []dwarfRelocSet
|
||||
for i, fn := range img.Funcs {
|
||||
data := marshalFuncInfo(fn)
|
||||
fnFiIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
||||
defData = append(defData, data)
|
||||
}
|
||||
|
||||
type npSym struct {
|
||||
sym goSym
|
||||
data []byte
|
||||
}
|
||||
var nps []npSym
|
||||
type pcRefs struct{ sp, file, line, inl int }
|
||||
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||
fnNpIdx := make([]int, len(img.Funcs))
|
||||
for i, fn := range img.Funcs {
|
||||
tables := []struct {
|
||||
data []byte
|
||||
dst *int
|
||||
}{
|
||||
{pcspTable(fn), &pcIdx[i].sp},
|
||||
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
|
||||
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
|
||||
{pcValueFlat(-1, fn.Size), &pcIdx[i].inl},
|
||||
}
|
||||
for _, t := range tables {
|
||||
*t.dst = len(nps)
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||
data: t.data,
|
||||
})
|
||||
}
|
||||
name := fn.Name
|
||||
abi := uint16(0)
|
||||
if fn.Static {
|
||||
abi = symABIStatic
|
||||
} else {
|
||||
if !fn.Static {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
flag := uint8(0)
|
||||
if fn.NoSplit {
|
||||
flag |= symFlagNoSplit
|
||||
|
||||
// The DWARF symbols: the .debug_line state-machine program and the
|
||||
// subprogram DIE, both referencing the function by its non-package
|
||||
// index (package definitions, like cmd/asm's).
|
||||
lines, lrel := goobjDwarfLines(fn, fnNpIdx[i])
|
||||
fnLinesIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDWARFLINES, size: uint32(len(lines))})
|
||||
defData = append(defData, lines)
|
||||
die, drel := goobjDwarfInfo(fn, name, fnNpIdx[i])
|
||||
fnDIEIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDWARFFCN, size: uint32(len(die))})
|
||||
defData = append(defData, die)
|
||||
dwarfRelocs = append(dwarfRelocs,
|
||||
dwarfRelocSet{si: fnLinesIdx[i], relocs: lrel},
|
||||
dwarfRelocSet{si: fnDIEIdx[i], relocs: drel},
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve external symbol references (cross-package). Build the
|
||||
// package index table and determine each external symbol's SymIdx
|
||||
// by reading the target package's export data.
|
||||
var extPkgTable []string
|
||||
var extPkgIdx map[string]int
|
||||
var extSymIdx map[string]int
|
||||
if len(img.Externals) > 0 {
|
||||
var err error
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||
}
|
||||
fnNpIdx[i] = len(nps)
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
for _, r := range fn.Relocs {
|
||||
// The linker writes the resolved displacement into the field;
|
||||
// leave it zero, as cmd/asm's object does.
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
|
||||
// Relocations, per defined symbol in definition order (package defs,
|
||||
// then non-package defs). Only file-local GLOBL references resolve;
|
||||
// external symbols need the import machinery of a later increment.
|
||||
// then non-package defs).
|
||||
nsyms := len(defs) + len(nps)
|
||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||
for i, fn := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
typ, size := relocField(r)
|
||||
if r.External {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name)
|
||||
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
||||
pkg, name := splitQualified(r.Name)
|
||||
if pkg == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name)
|
||||
}
|
||||
pIdx, ok := extPkgIdx[pkg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg)
|
||||
}
|
||||
sIdx, ok := extSymIdx[pkg+"·"+name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
@@ -230,17 +372,30 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = 4 // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], relocPCRel)
|
||||
rec[4] = size // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
}
|
||||
// The DWARF symbols' own relocations (the function address references).
|
||||
for _, ds := range dwarfRelocs {
|
||||
for _, r := range ds.relocs {
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(r.off))
|
||||
rec[4] = r.siz
|
||||
binary.LittleEndian.PutUint16(rec[5:], r.typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.add))
|
||||
binary.LittleEndian.PutUint32(rec[15:], r.pkg)
|
||||
binary.LittleEndian.PutUint32(rec[19:], r.sym)
|
||||
symRelocs[ds.si] = append(symRelocs[ds.si], rec[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Aux entries per function: FuncInfo, then the four pc tables.
|
||||
// References into the non-package table use pkgIdxNone.
|
||||
// Aux entries per function: FuncInfo, the DWARF symbols, then the four
|
||||
// pc tables. References into the non-package table use pkgIdxNone.
|
||||
symAux := make([][]byte, nsyms)
|
||||
for i := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
@@ -252,16 +407,20 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
symAux[si] = append(symAux[si], rec[:]...)
|
||||
}
|
||||
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
||||
aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
|
||||
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
|
||||
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
|
||||
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl))
|
||||
aux(auxDwarfInfo, pkgIdxSelf, uint32(fnDIEIdx[i]))
|
||||
aux(auxDwarfLines, pkgIdxSelf, uint32(fnLinesIdx[i]))
|
||||
// The pc-table references are 0-based within the non-package
|
||||
// definitions; the loader adds the package-definition count itself.
|
||||
aux(auxPcsp, pkgIdxNone, uint32(pcIdx[i].sp))
|
||||
aux(auxPcfile, pkgIdxNone, uint32(pcIdx[i].file))
|
||||
aux(auxPcline, pkgIdxNone, uint32(pcIdx[i].line))
|
||||
aux(auxPcinline, pkgIdxNone, uint32(pcIdx[i].inl))
|
||||
}
|
||||
|
||||
// The string table. Absolute offsets: it starts right after the
|
||||
// 96-byte header (magic, fingerprint, flags, the 19 block offsets).
|
||||
const headerSize = 8 + 8 + 4 + 4*(blkEnd+1)
|
||||
strTab := []byte{}
|
||||
var strTab []byte
|
||||
strOff := map[string]uint32{}
|
||||
addStr := func(s string) {
|
||||
if _, ok := strOff[s]; ok {
|
||||
@@ -291,7 +450,16 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
for _, s := range nps {
|
||||
npdefBlk = s.sym.append(npdefBlk, strOff)
|
||||
}
|
||||
pkgIdxBlk := stringRef(nil, "") // index 0: the dummy invalid package
|
||||
|
||||
// Package index table: index 0 is the dummy invalid package.
|
||||
// External packages follow, in pkgIdx order.
|
||||
for _, pkg := range extPkgTable {
|
||||
addStr(pkg)
|
||||
}
|
||||
pkgIdxBlk := stringRef(nil, "") // index 0: dummy
|
||||
for _, pkg := range extPkgTable {
|
||||
pkgIdxBlk = stringRef(pkgIdxBlk, pkg)
|
||||
}
|
||||
fileBlk := stringRef(nil, srcPath)
|
||||
|
||||
var relocBlk, auxBlk, dataBlk []byte
|
||||
@@ -299,7 +467,7 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
auxIdxBlk := make([]byte, 0, 4*(nsyms+1))
|
||||
dataIdxBlk := make([]byte, 0, 4*(nsyms+1))
|
||||
var nr, na, nd uint32
|
||||
for si := 0; si < nsyms; si++ {
|
||||
for si := range nsyms {
|
||||
relocIdxBlk = binary.LittleEndian.AppendUint32(relocIdxBlk, nr)
|
||||
auxIdxBlk = binary.LittleEndian.AppendUint32(auxIdxBlk, na)
|
||||
dataIdxBlk = binary.LittleEndian.AppendUint32(dataIdxBlk, nd)
|
||||
@@ -340,7 +508,7 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
// The fingerprint stays zero, as cmd/asm leaves it.
|
||||
binary.LittleEndian.PutUint32(payload[16:], 4) // ObjFlagFromAssembly
|
||||
off := uint32(headerSize + len(strTab))
|
||||
for i := 0; i < blkEnd; i++ {
|
||||
for i := range blkEnd {
|
||||
binary.LittleEndian.PutUint32(payload[20+4*i:], off)
|
||||
off += uint32(len(blocks[i]))
|
||||
}
|
||||
@@ -374,19 +542,21 @@ func marshalFuncInfo(fn FuncLayout) []byte {
|
||||
}
|
||||
|
||||
// pcValueFlat encodes a pc-value table holding v over the whole function.
|
||||
func pcValueFlat(v int32, size int) []byte {
|
||||
// The pc deltas are in MinLC units (the runtime scales them by the
|
||||
// architecture's minimum instruction length).
|
||||
func pcValueFlat(v int32, size, minLC int) []byte {
|
||||
// The table is delta-encoded from an implicit value of -1: a varint
|
||||
// value delta, an unsigned pc delta to the end, and a zero terminator.
|
||||
out := binary.AppendVarint(nil, int64(v)+1)
|
||||
out = binary.AppendUvarint(out, uint64(size))
|
||||
out = binary.AppendUvarint(out, uint64(size/minLC))
|
||||
return append(out, 0)
|
||||
}
|
||||
|
||||
// pcspTable encodes the stack-adjustment table: the SP delta in effect at
|
||||
// every pc, from the function's prologue and epilogue boundaries.
|
||||
func pcspTable(fn FuncLayout) []byte {
|
||||
func pcspTable(fn FuncLayout, minLC int) []byte {
|
||||
if len(fn.Spadj) == 0 {
|
||||
return pcValueFlat(0, fn.Size)
|
||||
return pcValueFlat(0, fn.Size, minLC)
|
||||
}
|
||||
pts := make([]SpadjStep, 0, len(fn.Spadj)+1)
|
||||
pts = append(pts, SpadjStep{PC: 0, Value: 0})
|
||||
@@ -394,11 +564,11 @@ func pcspTable(fn FuncLayout) []byte {
|
||||
out := binary.AppendVarint(nil, int64(pts[0].Value)+1)
|
||||
cur, old := pts[0].PC, pts[0].Value
|
||||
for _, p := range pts[1:] {
|
||||
out = binary.AppendUvarint(out, uint64(p.PC-cur))
|
||||
out = binary.AppendUvarint(out, uint64((p.PC-cur)/minLC))
|
||||
out = binary.AppendVarint(out, int64(p.Value-old))
|
||||
cur, old = p.PC, p.Value
|
||||
}
|
||||
out = binary.AppendUvarint(out, uint64(fn.Size-cur))
|
||||
out = binary.AppendUvarint(out, uint64((fn.Size-cur)/minLC))
|
||||
return append(out, 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// This file generates the per-function DWARF symbols the linker's DWARF
|
||||
// pass requires of an assembly object, byte-identical to what cmd/asm
|
||||
// emits: the .debug_line state-machine program (SDWARFLINES) and the
|
||||
// subprogram DIE (SDWARFFCN). The linker copies the DIE and line-program
|
||||
// bytes verbatim into .debug_info and .debug_line, fixing up their
|
||||
// relocations, so the formats here must match cmd/internal/dwarf's
|
||||
// DW_ABRV_FUNCTION and generateDebugLinesSymbol exactly.
|
||||
//
|
||||
// DWARF5 is assumed throughout (the toolchain's default on Linux and the
|
||||
// other non-Darwin targets gasm supports).
|
||||
|
||||
// Line-program parameters (cmd/internal/obj/dwarf.go).
|
||||
const (
|
||||
dwLineBase = -4
|
||||
dwLineRange = 10
|
||||
dwOpcodeBase = 11
|
||||
dwPCRange = (255 - dwOpcodeBase) / dwLineRange
|
||||
)
|
||||
|
||||
// goobjReloc is one relocation attached to an emitter-generated symbol
|
||||
// (the DWARF lines/info symbols), in goobj's on-disk encoding fields.
|
||||
type goobjReloc struct {
|
||||
off int32
|
||||
siz uint8
|
||||
typ uint16
|
||||
add int64
|
||||
pkg uint32
|
||||
sym uint32
|
||||
}
|
||||
|
||||
// goobjDwarfLines builds the function's .debug_line state-machine program:
|
||||
// an LNE_set_address extended opcode establishing the function's start
|
||||
// address (carrying the R_ADDR relocation), one row per source line
|
||||
// change across the function's instructions, an advance to the end of the
|
||||
// function and an end-of-sequence opcode. The linker appends these bytes
|
||||
// after the unit's line header, so they must start with the address and
|
||||
// leave the state machine terminated.
|
||||
func goobjDwarfLines(fn FuncLayout, fnNpIdx int) ([]byte, []goobjReloc) {
|
||||
// Rows: the prologue, if any, then the body instructions (fn.Lines
|
||||
// covers the body only). The first body offset > 0 means a prologue
|
||||
// precedes it; the toolchain reports the prologue on the TEXT line.
|
||||
pts := make([]LineEntry, 0, len(fn.Lines)+1)
|
||||
if len(fn.Lines) == 0 || fn.Lines[0].Offset > 0 {
|
||||
pts = append(pts, LineEntry{Offset: 0, Line: fn.Line})
|
||||
}
|
||||
pts = append(pts, fn.Lines...)
|
||||
|
||||
out := []byte{0, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0} // LNE_set_address, address zeroed
|
||||
relocs := []goobjReloc{{
|
||||
off: 3, siz: 8, typ: relocAddr,
|
||||
pkg: pkgIdxNone, sym: uint32(fnNpIdx),
|
||||
}}
|
||||
|
||||
// The state machine starts at line 1, pc 0 (function-relative); the
|
||||
// implicit initial pc is the function entry, so the first pc delta is
|
||||
// against 0.
|
||||
line := int64(1)
|
||||
pc := uint64(0)
|
||||
for _, p := range pts {
|
||||
if p.Line == 0 || uint64(p.Offset) < pc {
|
||||
continue
|
||||
}
|
||||
// Rows mark source-line changes only; the pc delta is measured from
|
||||
// the previous row, not the previous instruction.
|
||||
if int64(p.Line) == line {
|
||||
continue
|
||||
}
|
||||
deltaPC := uint64(p.Offset) - pc
|
||||
deltaLC := int64(p.Line) - line
|
||||
out = dwPutPCLCDelta(out, deltaPC, deltaLC)
|
||||
line, pc = int64(p.Line), uint64(p.Offset)
|
||||
}
|
||||
|
||||
// Cover the rest of the function and close the sequence.
|
||||
if end := uint64(fn.Size) - pc; end > 0 {
|
||||
out = append(out, 2) // DW_LNS_advance_pc
|
||||
out = binary.AppendUvarint(out, end)
|
||||
}
|
||||
out = append(out, 0, 1, 1) // LNE_end_sequence
|
||||
return out, relocs
|
||||
}
|
||||
|
||||
// dwPutPCLCDelta encodes one (pcDelta, lineDelta) step as the shortest
|
||||
// special opcode plus any standard-opcode remainder, exactly like
|
||||
// cmd/internal/obj's putpclcdelta.
|
||||
func dwPutPCLCDelta(b []byte, deltaPC uint64, deltaLC int64) []byte {
|
||||
opcode := dwSelectOpcode(deltaPC, deltaLC)
|
||||
deltaPC -= uint64((opcode - dwOpcodeBase) / dwLineRange)
|
||||
deltaLC -= (opcode-dwOpcodeBase)%dwLineRange + dwLineBase
|
||||
|
||||
// The remainder: standard opcodes first, then the special opcode
|
||||
// (which emits the row).
|
||||
if deltaPC != 0 {
|
||||
switch {
|
||||
case deltaPC <= uint64(dwPCRange):
|
||||
opcode -= dwLineRange * int64(uint64(dwPCRange)-deltaPC)
|
||||
b = append(b, 8) // DW_LNS_const_add_pc
|
||||
case (1<<14) <= deltaPC && deltaPC < (1<<16):
|
||||
b = append(b, 9) // DW_LNS_fixed_advance_pc
|
||||
b = binary.LittleEndian.AppendUint16(b, uint16(deltaPC))
|
||||
default:
|
||||
b = append(b, 2) // DW_LNS_advance_pc
|
||||
b = binary.AppendUvarint(b, deltaPC)
|
||||
}
|
||||
}
|
||||
if deltaLC != 0 {
|
||||
b = append(b, 3) // DW_LNS_advance_line
|
||||
b = binary.AppendVarint(b, deltaLC)
|
||||
}
|
||||
return append(b, byte(opcode))
|
||||
}
|
||||
|
||||
// dwSelectOpcode picks the special opcode for (deltaPC, deltaLC) per
|
||||
// cmd/internal/obj's putpclcdelta selection logic.
|
||||
func dwSelectOpcode(deltaPC uint64, deltaLC int64) int64 {
|
||||
switch {
|
||||
case deltaLC < dwLineBase:
|
||||
if deltaPC >= uint64(dwPCRange) {
|
||||
return dwOpcodeBase + dwLineRange*dwPCRange
|
||||
}
|
||||
return dwOpcodeBase + dwLineRange*int64(deltaPC)
|
||||
case deltaLC < dwLineBase+dwLineRange:
|
||||
if deltaPC >= uint64(dwPCRange) {
|
||||
op := int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*dwPCRange
|
||||
if op > 255 {
|
||||
op -= dwLineRange
|
||||
}
|
||||
return op
|
||||
}
|
||||
return int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*int64(deltaPC)
|
||||
default:
|
||||
if deltaPC <= uint64(dwPCRange) {
|
||||
op := min(int64(dwOpcodeBase)+(dwLineRange-1)+dwLineRange*int64(deltaPC), 255)
|
||||
return op
|
||||
}
|
||||
switch deltaPC - uint64(dwPCRange) {
|
||||
case uint64(dwPCRange), (1 << 7) - 1, (1 << 16) - 1, (1 << 21) - 1,
|
||||
(1 << 28) - 1, (1 << 35) - 1, (1 << 42) - 1, (1 << 49) - 1,
|
||||
(1 << 56) - 1, (1 << 63) - 1:
|
||||
return 255
|
||||
default:
|
||||
// 250: the toolchain's "249" comment is stale.
|
||||
return dwOpcodeBase + dwLineRange*dwPCRange - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// goobjDwarfInfo builds the function's DWARF5 subprogram DIE (abbrev
|
||||
// DW_ABRV_FUNCTION): name, low_pc as a .debug_addr index (the
|
||||
// R_DWTXTADDR_U4 relocation), high_pc as the size, the call-frame-CFA
|
||||
// frame base, the decl file/line and the external flag. name is the
|
||||
// symbol's object name (package-qualified unless static).
|
||||
func goobjDwarfInfo(fn FuncLayout, name string, fnNpIdx int) ([]byte, []goobjReloc) {
|
||||
out := []byte{3} // DW_ABRV_FUNCTION
|
||||
out = append(out, name...)
|
||||
out = append(out, 0)
|
||||
|
||||
addrx := len(out)
|
||||
out = append(out, 0, 0, 0, 0) // DW_AT_low_pc: addrx slot, zeroed
|
||||
out = binary.AppendUvarint(out, uint64(fn.Size))
|
||||
out = append(out, 1, 0x9c) // DW_AT_frame_base: block1, DW_OP_call_frame_cfa
|
||||
out = binary.LittleEndian.AppendUint32(out, 1)
|
||||
out = binary.AppendUvarint(out, uint64(fn.Line))
|
||||
if fn.Static {
|
||||
out = append(out, 0)
|
||||
} else {
|
||||
out = append(out, 1) // DW_AT_external
|
||||
}
|
||||
out = append(out, 0) // end of children
|
||||
|
||||
relocs := []goobjReloc{{
|
||||
off: int32(addrx), siz: 4, typ: relocDWTXTADDRU4(),
|
||||
pkg: pkgIdxNone, sym: uint32(fnNpIdx),
|
||||
}}
|
||||
return out, relocs
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDWSelectOpcode checks the special-opcode selection against
|
||||
// hand-computed values for the boundary cases: line deltas below, inside
|
||||
// and above the line range, and pc deltas at and beyond PC_RANGE (24).
|
||||
func TestDWSelectOpcode(t *testing.T) {
|
||||
cases := []struct {
|
||||
deltaPC uint64
|
||||
deltaLC int64
|
||||
want int64
|
||||
}{
|
||||
{0, 2, 17}, // the common single-instruction step
|
||||
{0, -4, 11}, // deltaLC == LINE_BASE
|
||||
{0, -5, 11}, // deltaLC below LINE_BASE: opcode adds nothing
|
||||
{0, 6, 20}, // deltaLC == LINE_BASE+LINE_RANGE, remainder via advance_line
|
||||
{4, 1, 56}, // the 4-byte loong64 instruction step
|
||||
{23, 1, 246}, // deltaPC == PC_RANGE-1
|
||||
{24, 1, 246}, // deltaPC == PC_RANGE: wraps past 255
|
||||
{25, 1, 246}, // deltaPC past PC_RANGE (the const_add_pc remainder adjusts it later)
|
||||
{100, 1, 246},
|
||||
{151, 10, 255}, // deltaPC-PC_RANGE == (1<<7)-1, large line delta
|
||||
{100, 10, 250}, // deltaPC-PC_RANGE not on a switch boundary
|
||||
{23, 10, 250}, // large line delta inside PC_RANGE
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := dwSelectOpcode(c.deltaPC, c.deltaLC); got != c.want {
|
||||
t.Errorf("dwSelectOpcode(%d, %d) = %d, want %d", c.deltaPC, c.deltaLC, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decodeDWLineProgram decodes a .debug_line state-machine program (as
|
||||
// emitted by goobjDwarfLines) into (pc, line) rows.
|
||||
func decodeDWLineProgram(t *testing.T, b []byte) (pcs []uint64, lines []int64) {
|
||||
t.Helper()
|
||||
pc, line := uint64(0), int64(1)
|
||||
emit := func() {
|
||||
if len(pcs) == 0 || pcs[len(pcs)-1] != pc || lines[len(lines)-1] != line {
|
||||
pcs = append(pcs, pc)
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
advancePC := func(delta uint64) { pc += delta }
|
||||
advanceLine := func(delta int64) { line += delta }
|
||||
for i := 0; i < len(b); {
|
||||
op := b[i]
|
||||
i++
|
||||
switch {
|
||||
case op == 0: // extended opcode
|
||||
ln, n := binary.Uvarint(b[i:])
|
||||
i += n
|
||||
sub := b[i]
|
||||
i++
|
||||
_ = ln
|
||||
switch sub {
|
||||
case 2: // DW_LNE_set_address: 8-byte address
|
||||
pc = binary.LittleEndian.Uint64(b[i:])
|
||||
i += 8
|
||||
case 1: // DW_LNE_end_sequence
|
||||
// terminates the sequence; no new row
|
||||
}
|
||||
case op == 2: // DW_LNS_advance_pc
|
||||
v, n := binary.Uvarint(b[i:])
|
||||
i += n
|
||||
advancePC(v)
|
||||
case op == 3: // DW_LNS_advance_line
|
||||
v, n := binary.Varint(b[i:])
|
||||
i += n
|
||||
advanceLine(v)
|
||||
case op == 8: // DW_LNS_const_add_pc
|
||||
advancePC(uint64(dwPCRange))
|
||||
case op == 9: // DW_LNS_fixed_advance_pc
|
||||
advancePC(uint64(binary.LittleEndian.Uint16(b[i:])))
|
||||
i += 2
|
||||
case op >= dwOpcodeBase: // special opcode
|
||||
advancePC(uint64((int64(op) - dwOpcodeBase) / dwLineRange))
|
||||
advanceLine((int64(op)-dwOpcodeBase)%dwLineRange + dwLineBase)
|
||||
emit()
|
||||
}
|
||||
}
|
||||
return pcs, lines
|
||||
}
|
||||
|
||||
// TestGoobjDwarfLinesRows checks the emitted line program's rows for
|
||||
// synthetic functions: a zero-frame function with one instruction per
|
||||
// line, a framed function (the prologue row is prepended on the TEXT
|
||||
// line), instructions sharing a line, and a function with a large pc gap
|
||||
// (the const_add_pc remainder path).
|
||||
func TestGoobjDwarfLinesRows(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fn FuncLayout
|
||||
want [][2]int64 // (pc, line)
|
||||
}{
|
||||
{
|
||||
"one instruction per line",
|
||||
FuncLayout{Size: 20, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7}},
|
||||
},
|
||||
{
|
||||
"framed: prologue row on the TEXT line",
|
||||
FuncLayout{Size: 24, Line: 2, Lines: []LineEntry{
|
||||
{12, 3}, {16, 4},
|
||||
}},
|
||||
[][2]int64{{0, 2}, {12, 3}, {16, 4}},
|
||||
},
|
||||
{
|
||||
"instructions sharing a line fold into one row",
|
||||
FuncLayout{Size: 16, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {4, 3}, {8, 4}, {12, 4},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {8, 4}},
|
||||
},
|
||||
{
|
||||
"large gap crosses PC_RANGE",
|
||||
FuncLayout{Size: 60, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {40, 4},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {40, 4}},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
prog, relocs := goobjDwarfLines(c.fn, 0)
|
||||
if len(relocs) != 1 || relocs[0].off != 3 || relocs[0].siz != 8 || relocs[0].typ != relocAddr || relocs[0].sym != 0 {
|
||||
t.Fatalf("relocs = %+v", relocs)
|
||||
}
|
||||
pcs, lines := decodeDWLineProgram(t, prog)
|
||||
if len(pcs) != len(c.want) {
|
||||
t.Fatalf("rows = %d (%v / %v), want %d", len(pcs), pcs, lines, len(c.want))
|
||||
}
|
||||
for i, w := range c.want {
|
||||
if pcs[i] != uint64(w[0]) || lines[i] != w[1] {
|
||||
t.Errorf("row %d = (%d, %d), want (%d, %d)", i, pcs[i], lines[i], w[0], w[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoobjDwarfInfo checks the subprogram DIE for an exported and a
|
||||
// static function: the abbrev, name, high_pc, frame base, decl file/line,
|
||||
// the external flag and the addrx relocation position.
|
||||
func TestGoobjDwarfInfo(t *testing.T) {
|
||||
fn := FuncLayout{Size: 20, Line: 2}
|
||||
die, relocs := goobjDwarfInfo(fn, "pkg.f", 3)
|
||||
want := []byte{
|
||||
0x03,
|
||||
'p', 'k', 'g', '.', 'f', 0,
|
||||
0, 0, 0, 0, // addrx slot at offset 7
|
||||
0x14, // high_pc: 20
|
||||
0x01, 0x9c, // frame_base
|
||||
0x01, 0, 0, 0, // decl_file 1
|
||||
0x02, // decl_line 2
|
||||
0x01, // external
|
||||
0x00, // end of children
|
||||
}
|
||||
if !bytes.Equal(die, want) {
|
||||
t.Errorf("DIE = %x, want %x", die, want)
|
||||
}
|
||||
if len(relocs) != 1 || relocs[0].off != 7 || relocs[0].siz != 4 || relocs[0].typ != relocDWTXTADDRU4() || relocs[0].sym != 3 {
|
||||
t.Errorf("relocs = %+v", relocs)
|
||||
}
|
||||
|
||||
// A static function carries no external flag and no package prefix.
|
||||
fn.Static = true
|
||||
die, _ = goobjDwarfInfo(fn, "f", 1)
|
||||
if die[len(die)-2] != 0 {
|
||||
t.Errorf("static external flag = %d, want 0", die[len(die)-2])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwPutPCLCDeltaRemainders checks the standard-opcode remainders:
|
||||
// const_add_pc and fixed_advance_pc after a special opcode.
|
||||
func TestDwPutPCLCDeltaRemainders(t *testing.T) {
|
||||
// deltaPC 25 past PC_RANGE: opcode 26 covers (1, 1), const_add_pc
|
||||
// covers the remaining 23 pc and 0 line.
|
||||
got := dwPutPCLCDelta(nil, 25, 1)
|
||||
if !bytes.Equal(got, []byte{8, 26}) {
|
||||
t.Errorf("25/1 = %x, want [8 1a]", got)
|
||||
}
|
||||
|
||||
// deltaPC 20000: opcode 246 covers 23, fixed_advance_pc covers the
|
||||
// remaining 19977.
|
||||
got = dwPutPCLCDelta(nil, 20000, 1)
|
||||
if got[0] != 9 || binary.LittleEndian.Uint16(got[1:]) != 19977 || got[3] != 246 {
|
||||
t.Errorf("20000/1 = %x, want fixed_advance_pc 19977 then 246", got)
|
||||
}
|
||||
|
||||
// Line remainder: deltaLC 10 leaves 5 past the opcode's reach, encoded
|
||||
// as advance_line 5 (zigzag 0x0a) before opcode 250.
|
||||
got = dwPutPCLCDelta(nil, 23, 10)
|
||||
if !bytes.Equal(got, []byte{3, 0x0a, 250}) {
|
||||
t.Errorf("23/10 = %x, want [03 0a fa]", got)
|
||||
}
|
||||
|
||||
// Negative line remainder: deltaLC -5 leaves advance_line -1 (zigzag
|
||||
// 0x01) after opcode 11.
|
||||
got = dwPutPCLCDelta(nil, 0, -5)
|
||||
if !bytes.Equal(got, []byte{3, 1, 11}) {
|
||||
t.Errorf("0/-5 = %x, want [03 01 0b]", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// exportPath returns the export file path for a given import path by running
|
||||
// "go list -export". The result is cached so repeated calls for the same
|
||||
// package are fast.
|
||||
func exportPath(importPath string) (string, error) {
|
||||
cmd := exec.Command("go", "list", "-json", "-export", importPath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go list %s: %w", importPath, err)
|
||||
}
|
||||
// Quick JSON extraction: find "Export": "…"
|
||||
const key = `"Export": "`
|
||||
i := bytes.Index(out, []byte(key))
|
||||
if i < 0 {
|
||||
return "", fmt.Errorf("go list %s: no Export field", importPath)
|
||||
}
|
||||
start := i + len(key)
|
||||
end := bytes.IndexByte(out[start:], '"')
|
||||
if end < 0 {
|
||||
return "", fmt.Errorf("go list %s: malformed Export field", importPath)
|
||||
}
|
||||
return string(out[start : start+end]), nil
|
||||
}
|
||||
|
||||
// resolveExternalGOOBJ resolves a set of external symbol references into
|
||||
// (package index, symbol index) pairs suitable for GOOBJ emission.
|
||||
//
|
||||
// refs maps package import paths to the symbol names referenced from that
|
||||
// package. The returned pkgIdx maps each import path to its position in
|
||||
// the blkPkgIdx table (0-based), and symIdx gives each symbol's index within
|
||||
// its package.
|
||||
func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symIdx map[string]int, err error) {
|
||||
pkgIdx = make(map[string]int, len(refs))
|
||||
symIdx = make(map[string]int)
|
||||
|
||||
// Assign package indices in sorted order for determinism.
|
||||
packages := sortedPkgRefs(refs)
|
||||
|
||||
for i, pkg := range packages {
|
||||
pkgIdx[pkg.path] = i
|
||||
exp, err := exportPath(pkg.path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
data, err := os.ReadFile(exp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
gobj, err := extractGOOBJ(data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", pkg.path, err)
|
||||
}
|
||||
for _, name := range pkg.syms {
|
||||
idx := gobj.findSymbol(pkg.path, name)
|
||||
if idx < 0 {
|
||||
return nil, nil, fmt.Errorf("symbol %s·%s not found in export data of %s", pkg.path, name, pkg.path)
|
||||
}
|
||||
symIdx[pkg.path+"·"+name] = idx
|
||||
}
|
||||
}
|
||||
return pkgIdx, symIdx, nil
|
||||
}
|
||||
|
||||
type pkgRef struct {
|
||||
path string
|
||||
syms []string
|
||||
}
|
||||
|
||||
func sortedPkgRefs(refs map[string][]string) []pkgRef {
|
||||
var pkgs []pkgRef
|
||||
for pkg, syms := range refs {
|
||||
pkgs = append(pkgs, pkgRef{pkg, syms})
|
||||
}
|
||||
// Simple insertion sort — the list is tiny (usually 1–3 packages).
|
||||
for i := 1; i < len(pkgs); i++ {
|
||||
for j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- {
|
||||
pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1]
|
||||
}
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// extractGOOBJ finds the GOOBJ data in an ar archive and returns a parsed
|
||||
// goobjFile. The archive member _go_.o contains the "go object …\n!\n"
|
||||
// preamble followed by the GOOBJ payload; __.PKGDEF is the compiler export
|
||||
// data (type information) and is not the GOOBJ object.
|
||||
func extractGOOBJ(data []byte) (*goobjFile, error) {
|
||||
if len(data) < 8 || string(data[:8]) != "!<arch>\n" {
|
||||
return nil, fmt.Errorf("not an ar archive")
|
||||
}
|
||||
pos := 8
|
||||
for pos+60 <= len(data) {
|
||||
hdr := data[pos : pos+60]
|
||||
pos += 60
|
||||
|
||||
// Parse ar header fields.
|
||||
name := strings.TrimRight(string(hdr[:16]), " /")
|
||||
size := parseArDecimal(hdr[48:58])
|
||||
if size < 0 {
|
||||
return nil, fmt.Errorf("invalid ar header: bad size")
|
||||
}
|
||||
if pos+size > len(data) {
|
||||
return nil, fmt.Errorf("ar entry %q extends past end of file", name)
|
||||
}
|
||||
body := data[pos : pos+size]
|
||||
pos += size
|
||||
// ar pads to even bytes.
|
||||
if pos%2 != 0 {
|
||||
pos++
|
||||
}
|
||||
|
||||
if name == "_go_.o" {
|
||||
return parseGOOBJ(body)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("archive contains no _go_.o member")
|
||||
}
|
||||
|
||||
// parseArDecimal parses a decimal number from a space-padded field.
|
||||
func parseArDecimal(b []byte) int {
|
||||
v := 0
|
||||
for _, c := range b {
|
||||
if c == ' ' {
|
||||
continue
|
||||
}
|
||||
if c < '0' || c > '9' {
|
||||
return -1
|
||||
}
|
||||
v = v*10 + int(c-'0')
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// goobjFile is a parsed GOOBJ file: the string table and the symbol-definition
|
||||
// block.
|
||||
type goobjFile struct {
|
||||
strTab []byte // string table, at headerSize + n
|
||||
symdef []byte // blkSymdef raw block
|
||||
npdef []byte // blkNonpkgdef raw block
|
||||
}
|
||||
|
||||
// symbols returns all symbol names in definition order by scanning the
|
||||
// symdef and nonpkgdef blocks and resolving each name through the string
|
||||
// table. Package definitions (blkSymdef) use fully-qualified names like
|
||||
// "runtime.morestack"; non-package definitions (blkNonpkgdef) use bare
|
||||
// names like "morestack". This combined list matches the index the
|
||||
// linker expects for cross-package references.
|
||||
func (f *goobjFile) symbols() []string {
|
||||
return append(f.defNames(), f.npdefNames()...)
|
||||
}
|
||||
|
||||
// findSymbol returns the index of a symbol within the combined symbol list,
|
||||
// or -1 if not found. It first tries the fully-qualified name (pkg.name),
|
||||
// then the bare name.
|
||||
func (f *goobjFile) findSymbol(pkg, name string) int {
|
||||
qualified := pkg + "." + name
|
||||
syms := f.symbols()
|
||||
for i, s := range syms {
|
||||
if s == qualified {
|
||||
return i
|
||||
}
|
||||
}
|
||||
// Try bare name (for non-package definitions).
|
||||
for i, s := range syms {
|
||||
if s == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// defNames returns names from blkSymdef only.
|
||||
func (f *goobjFile) defNames() []string {
|
||||
return f.readSymNames(f.symdef)
|
||||
}
|
||||
|
||||
// npdefNames returns names from blkNonpkgdef.
|
||||
func (f *goobjFile) npdefNames() []string {
|
||||
return f.readSymNames(f.npdef)
|
||||
}
|
||||
|
||||
// readSymNames reads symbol names from a symdef/nonpkgdef block. Each record
|
||||
// is 21 bytes: nameLen (u32), nameOff (u32), abi (u16), typ, flag, flag2,
|
||||
// size (u32), align (u32). nameOff is an absolute offset into the string
|
||||
// table.
|
||||
func (f *goobjFile) readSymNames(block []byte) []string {
|
||||
const recSize = 21
|
||||
if len(block) < recSize {
|
||||
return nil
|
||||
}
|
||||
n := len(block) / recSize
|
||||
names := make([]string, 0, n)
|
||||
for i := range n {
|
||||
rec := block[i*recSize : (i+1)*recSize]
|
||||
nameLen := binary.LittleEndian.Uint32(rec[0:4])
|
||||
nameOff := binary.LittleEndian.Uint32(rec[4:8])
|
||||
// nameOff is an absolute offset into the GOOBJ payload. The string
|
||||
// table we have starts at goobjHeaderSize, so we subtract that.
|
||||
if nameOff < goobjHeaderSize {
|
||||
continue
|
||||
}
|
||||
relOff := nameOff - goobjHeaderSize
|
||||
if relOff >= uint32(len(f.strTab)) || relOff+nameLen > uint32(len(f.strTab)) {
|
||||
continue
|
||||
}
|
||||
names = append(names, string(f.strTab[relOff:relOff+nameLen]))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
const goobjHeaderSize = 8 + 8 + 4 + 4*(blkEnd+1) // magic + fingerprint + flags + 19 block offsets
|
||||
|
||||
// parseGOOBJ parses a raw GOOBJ payload (the data after the "\n!\n" preamble).
|
||||
func parseGOOBJ(data []byte) (*goobjFile, error) {
|
||||
// Find the "\n!\n" separator.
|
||||
sep := []byte("\n!\n")
|
||||
i := bytes.Index(data, sep)
|
||||
if i < 0 {
|
||||
// Maybe the data has no preamble (e.g. a raw .o file).
|
||||
i = -3 // treat as if preamble starts before the data
|
||||
}
|
||||
payload := data[i+len(sep):]
|
||||
|
||||
if len(payload) < goobjHeaderSize {
|
||||
return nil, fmt.Errorf("GOOBJ payload too short (%d bytes)", len(payload))
|
||||
}
|
||||
if string(payload[:8]) != goobjMagic {
|
||||
return nil, fmt.Errorf("bad GOOBJ magic: %q", payload[:8])
|
||||
}
|
||||
|
||||
// Read block offsets. The header layout is:
|
||||
// [0:8] magic
|
||||
// [8:16] fingerprint
|
||||
// [16:20] flags
|
||||
// [20:96] 19 × uint32 offsets
|
||||
var offs [blkEnd + 1]uint32
|
||||
for i := 0; i <= blkEnd; i++ {
|
||||
offs[i] = binary.LittleEndian.Uint32(payload[20+4*i:])
|
||||
}
|
||||
// The string table lives at headerSize.
|
||||
strTabStart := uint32(goobjHeaderSize)
|
||||
|
||||
f := &goobjFile{
|
||||
strTab: payload[strTabStart:offs[0]],
|
||||
symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1),
|
||||
npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1),
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// blockSlice extracts a block from the payload using its offset pair.
|
||||
func blockSlice(payload []byte, offs [blkEnd + 1]uint32, start, end int) []byte {
|
||||
if start < 0 || end > blkEnd || offs[end] < offs[start] {
|
||||
return nil
|
||||
}
|
||||
beg := offs[start]
|
||||
fin := offs[end]
|
||||
if int(fin) > len(payload) || int(beg) > int(fin) {
|
||||
return nil
|
||||
}
|
||||
return payload[beg:fin]
|
||||
}
|
||||
|
||||
// resolveExternalSymbols is the high-level entry point for GOOBJ emission.
|
||||
// Given a list of external symbol names (e.g. ["runtime·morestack",
|
||||
// "runtime·g0"]), it returns the package-index table entries and a map from
|
||||
// full symbol name to GOOBJ {pkgIdx, symIdx}.
|
||||
//
|
||||
// The package table entries should be written into blkPkgIdx, and the
|
||||
// returned indices should replace pkgIdxSelf / placeholder values in the
|
||||
// relocation records.
|
||||
func resolveExternalSymbols(externals []string) (pkgTable []string, pkgIdxMap map[string]int, symIdxMap map[string]int, err error) {
|
||||
// Group references by package.
|
||||
refs := make(map[string]map[string]bool)
|
||||
for _, full := range externals {
|
||||
pkg, name := splitQualified(full)
|
||||
if refs[pkg] == nil {
|
||||
refs[pkg] = make(map[string]bool)
|
||||
}
|
||||
refs[pkg][name] = true
|
||||
}
|
||||
|
||||
// Convert maps to slices.
|
||||
r := make(map[string][]string, len(refs))
|
||||
for pkg, names := range refs {
|
||||
for name := range names {
|
||||
r[pkg] = append(r[pkg], name)
|
||||
}
|
||||
}
|
||||
|
||||
pkgIdx1, symIdx1, err := resolveExternalGOOBJ(r)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Build the package table in pkgIdx order.
|
||||
pkgTable = make([]string, len(pkgIdx1))
|
||||
for pkg, idx := range pkgIdx1 {
|
||||
pkgTable[idx] = pkg
|
||||
}
|
||||
|
||||
return pkgTable, pkgIdx1, symIdx1, nil
|
||||
}
|
||||
|
||||
// splitQualified splits a qualified Go symbol name (pkgpath·name) into its
|
||||
// package path and local name. The separator is the middle dot (U+00B7).
|
||||
// If no separator is found, the symbol is assumed to be in the current
|
||||
// package (empty pkg).
|
||||
func splitQualified(full string) (pkg, name string) {
|
||||
if idx := strings.IndexByte(full, '\u00b7'); idx >= 0 {
|
||||
return full[:idx], full[idx+len("\u00b7"):]
|
||||
}
|
||||
if before, after, ok := strings.Cut(full, "."); ok {
|
||||
return before, after
|
||||
}
|
||||
return "", full
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReadRuntimeSymbols verifies the GOOBJ reader can extract and find
|
||||
// symbols from the runtime package's compiled archive.
|
||||
func TestReadRuntimeSymbols(t *testing.T) {
|
||||
exp, err := exportPath("runtime")
|
||||
if err != nil {
|
||||
t.Skipf("cannot find runtime export: %v (need Go toolchain)", err)
|
||||
}
|
||||
data, err := os.ReadFile(exp)
|
||||
if err != nil {
|
||||
t.Skipf("cannot read runtime export: %v", err)
|
||||
}
|
||||
gobj, err := extractGOOBJ(data)
|
||||
if err != nil {
|
||||
t.Fatalf("extractGOOBJ: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("runtime: %d symbols", len(gobj.symbols()))
|
||||
|
||||
// Verify we can find well-known runtime symbols.
|
||||
for _, tc := range []struct{ pkg, name string }{
|
||||
{"runtime", "g0"},
|
||||
{"runtime", "morestack"},
|
||||
{"runtime", "newstack"},
|
||||
} {
|
||||
idx := gobj.findSymbol(tc.pkg, tc.name)
|
||||
if idx < 0 {
|
||||
t.Errorf("findSymbol(%q, %q) = -1", tc.pkg, tc.name)
|
||||
} else {
|
||||
t.Logf("findSymbol(%q, %q) = %d", tc.pkg, tc.name, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveExternalSymbols verifies end-to-end resolution of external
|
||||
// symbol references.
|
||||
func TestResolveExternalSymbols(t *testing.T) {
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("go toolchain not available")
|
||||
}
|
||||
|
||||
refs := map[string][]string{
|
||||
"runtime": {"g0"},
|
||||
}
|
||||
pkgIdx, symIdx, err := resolveExternalGOOBJ(refs)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveExternalGOOBJ: %v", err)
|
||||
}
|
||||
if len(pkgIdx) != 1 || pkgIdx["runtime"] != 0 {
|
||||
t.Errorf("pkgIdx = %v, want runtime→0", pkgIdx)
|
||||
}
|
||||
if _, ok := symIdx["runtime·g0"]; !ok {
|
||||
t.Errorf("symIdx missing runtime·g0, got %v", symIdx)
|
||||
}
|
||||
t.Logf("runtime·g0 → SymIdx=%d", symIdx["runtime·g0"])
|
||||
}
|
||||
+77
-41
@@ -111,17 +111,26 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags)
|
||||
}
|
||||
|
||||
// Package defs: the static GLOBL, then one anonymous FuncInfo per
|
||||
// function.
|
||||
// Package defs: the static GLOBL, then per function the FuncInfo and the
|
||||
// two DWARF symbols (debug_line program, subprogram DIE).
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 3 {
|
||||
t.Fatalf("symdefs = %d, want 3", len(defs))
|
||||
if len(defs) != 7 {
|
||||
t.Fatalf("symdefs = %d, want 7", len(defs))
|
||||
}
|
||||
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link {
|
||||
t.Errorf("mask symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||||
t.Errorf("funcinfo symbol = %+v", defs[1])
|
||||
t.Errorf("addq funcinfo symbol = %+v", defs[1])
|
||||
}
|
||||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||||
t.Errorf("addq lines symbol = %+v", defs[2])
|
||||
}
|
||||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||||
t.Errorf("addq DIE symbol = %+v", defs[3])
|
||||
}
|
||||
if defs[4].name != "" || defs[4].typ != kindSDATA || defs[4].size != 28 {
|
||||
t.Errorf("loadmask funcinfo symbol = %+v", defs[4])
|
||||
}
|
||||
|
||||
// Non-package defs: four pc tables and the function, per function.
|
||||
@@ -142,45 +151,62 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
// FuncInfo: args 24, FuncFlag Asm, one file, no inline tree.
|
||||
le := binary.LittleEndian
|
||||
data := v.blk(blkData)
|
||||
didx := v.blk(blkDataIdx)
|
||||
fi := data[16:44]
|
||||
if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm ||
|
||||
le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 {
|
||||
t.Errorf("funcinfo bytes %x", fi)
|
||||
}
|
||||
|
||||
// pcsp: a flat zero over the whole function (zero-frame NOSPLIT).
|
||||
if got := data[72:75]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||||
// The pc-value tables of addq (non-package indices 0–3, so global
|
||||
// indices 7–10): pcsp a flat zero over the whole function, pcinline a
|
||||
// flat -1, both with the pc delta in MinLC (1) units.
|
||||
pcsp := data[le.Uint32(didx[4*7:]):]
|
||||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||||
t.Errorf("pcsp = %x, want 021300", got)
|
||||
}
|
||||
// pcinline: a flat -1.
|
||||
if got := data[81:84]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
|
||||
pcinl := data[le.Uint32(didx[4*10:]):]
|
||||
if got := pcinl[:3]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
|
||||
t.Errorf("pcinline = %x, want 001300", got)
|
||||
}
|
||||
|
||||
// The one relocation: R_PCREL, four bytes wide, against the GLOBL,
|
||||
// with the field in the function code left zero. The loadmask code's
|
||||
// offset comes from the data index (symbol 3 defs + 9 non-package).
|
||||
// Relocations: the four DWARF address references (two per function, in
|
||||
// definition order), then the loadmask code's R_PCREL against the
|
||||
// GLOBL, with the field in the function code left zero. The loadmask
|
||||
// code's offset comes from the data index (7 defs + 9 non-package).
|
||||
relocs := v.blk(blkReloc)
|
||||
if len(relocs) != 23 {
|
||||
t.Fatalf("relocs = %d bytes, want one 23-byte entry", len(relocs))
|
||||
if len(relocs) != 5*23 {
|
||||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||||
}
|
||||
off := int32(le.Uint32(relocs[0:]))
|
||||
if off != 4 || relocs[4] != 4 || le.Uint16(relocs[5:]) != relocPCRel ||
|
||||
le.Uint64(relocs[7:]) != 0 || le.Uint32(relocs[15:]) != pkgIdxSelf || le.Uint32(relocs[19:]) != 0 {
|
||||
t.Errorf("reloc = %x", relocs)
|
||||
// addq's DWARF references (defs 2 and 3) against the function, which
|
||||
// is non-package index 4.
|
||||
lr := relocs[:23]
|
||||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||||
t.Errorf("addq lines reloc = %x", lr)
|
||||
}
|
||||
didx := v.blk(blkDataIdx)
|
||||
lm := le.Uint32(didx[4*(3+9):])
|
||||
dr := relocs[23:46]
|
||||
if dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||||
t.Errorf("addq DIE reloc = %x", dr)
|
||||
}
|
||||
cr := relocs[4*23:]
|
||||
off := int32(le.Uint32(cr[0:]))
|
||||
if off != 4 || cr[4] != 4 || le.Uint16(cr[5:]) != relocPCRel ||
|
||||
le.Uint64(cr[7:]) != 0 || le.Uint32(cr[15:]) != pkgIdxSelf || le.Uint32(cr[19:]) != 0 {
|
||||
t.Errorf("loadmask reloc = %x", cr)
|
||||
}
|
||||
lm := le.Uint32(didx[4*16:])
|
||||
code := data[lm : lm+18]
|
||||
if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) {
|
||||
t.Errorf("relocated field = %x, want zeroed", code[4:8])
|
||||
}
|
||||
|
||||
// Aux wiring: FuncInfo (package symbol), then the four pc tables
|
||||
// (non-package symbols).
|
||||
// Aux wiring: FuncInfo, the two DWARF symbols (package symbols), then
|
||||
// the four pc tables (non-package symbols).
|
||||
auxs := v.blk(blkAux)
|
||||
if len(auxs) != 2*5*9 {
|
||||
t.Fatalf("aux = %d bytes, want 10 entries", len(auxs))
|
||||
if len(auxs) != 2*7*9 {
|
||||
t.Fatalf("aux = %d bytes, want 14 entries", len(auxs))
|
||||
}
|
||||
wantAux := []struct {
|
||||
typ uint8
|
||||
@@ -188,15 +214,19 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
idx uint32
|
||||
}{
|
||||
{auxFuncInfo, pkgIdxSelf, 1},
|
||||
{auxPcsp, pkgIdxNone, uint32(len(defs) + 0)},
|
||||
{auxPcfile, pkgIdxNone, uint32(len(defs) + 1)},
|
||||
{auxPcline, pkgIdxNone, uint32(len(defs) + 2)},
|
||||
{auxPcinline, pkgIdxNone, uint32(len(defs) + 3)},
|
||||
{auxFuncInfo, pkgIdxSelf, 2},
|
||||
{auxPcsp, pkgIdxNone, uint32(len(defs) + 5)},
|
||||
{auxPcfile, pkgIdxNone, uint32(len(defs) + 6)},
|
||||
{auxPcline, pkgIdxNone, uint32(len(defs) + 7)},
|
||||
{auxPcinline, pkgIdxNone, uint32(len(defs) + 8)},
|
||||
{auxDwarfInfo, pkgIdxSelf, 3},
|
||||
{auxDwarfLines, pkgIdxSelf, 2},
|
||||
{auxPcsp, pkgIdxNone, 0},
|
||||
{auxPcfile, pkgIdxNone, 1},
|
||||
{auxPcline, pkgIdxNone, 2},
|
||||
{auxPcinline, pkgIdxNone, 3},
|
||||
{auxFuncInfo, pkgIdxSelf, 4},
|
||||
{auxDwarfInfo, pkgIdxSelf, 6},
|
||||
{auxDwarfLines, pkgIdxSelf, 5},
|
||||
{auxPcsp, pkgIdxNone, 5},
|
||||
{auxPcfile, pkgIdxNone, 6},
|
||||
{auxPcline, pkgIdxNone, 7},
|
||||
{auxPcinline, pkgIdxNone, 8},
|
||||
}
|
||||
for i, w := range wantAux {
|
||||
e := auxs[i*9:]
|
||||
@@ -253,7 +283,7 @@ TEXT ·framed(SB), NOSPLIT, $8-0
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
pcs, vals := decodePCValues(pcspTable(fn))
|
||||
pcs, vals := decodePCValues(pcspTable(fn, 1))
|
||||
// Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change),
|
||||
// SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds
|
||||
// ADDQ $8, SP (+8) then POPQ BP (0).
|
||||
@@ -349,7 +379,7 @@ func main() {
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.26\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.27\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -363,7 +393,7 @@ func main() {
|
||||
}
|
||||
var work string
|
||||
var asmObj, pkgArch, linkLine string
|
||||
for _, line := range strings.Split(string(buildLog), "\n") {
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
@@ -401,13 +431,12 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(asmObj, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Rebuild the package archive with our object in place of the
|
||||
// toolchain's (go tool pack has no replace-in-place that dedupes, so
|
||||
// extract, substitute and repack).
|
||||
// extract, substitute and repack). The archive member holding the
|
||||
// assembler's output is named after the asm object file, e.g.
|
||||
// main_amd64.o.
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
@@ -417,6 +446,13 @@ func main() {
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
member := filepath.Join(membersDir, filepath.Base(asmObj))
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -425,7 +461,7 @@ func main() {
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for _, m := range strings.Fields(string(listOut)) {
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is
|
||||
// the shared one in goobj.go — the toolchain preamble, the go120ld header
|
||||
// with its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays — with the arm64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and R_ADDRARM64 relocation types for the
|
||||
// ADRP+ADD/LDR/STR address pairs.
|
||||
func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreambleAARCH64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||
if r.Kind == RelArm64Branch {
|
||||
return relocArm64Branch, 4
|
||||
}
|
||||
return relocArm64Addr, 4
|
||||
})
|
||||
}
|
||||
|
||||
// arm64 relocation types (cmd/internal/objabi).
|
||||
const (
|
||||
relocArm64Addr = 3 // R_ADDRARM64 — ADRP+ADD/LDR/STR pair
|
||||
relocArm64Branch = 9 // R_CALLARM64 — BL instruction
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleAARCH64 returns the "go object ...\n!\n" header
|
||||
// the installed go tool asm writes for arm64, captured by assembling a
|
||||
// one-instruction probe.
|
||||
var (
|
||||
preambleAARCH64Once sync.Once
|
||||
preambleAARCH64 []byte
|
||||
preambleAARCH64Err error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleAARCH64() ([]byte, error) {
|
||||
preambleAARCH64Once.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleAARCH64Err = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-arm64")
|
||||
if err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_arm64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleAARCH64Err = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleAARCH64Err = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleAARCH64 = data[:i+3]
|
||||
})
|
||||
return preambleAARCH64, preambleAARCH64Err
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectLOONG64 emits a GOOBJ object file for LoongArch. The layout is
|
||||
// the shared one in goobj.go — the toolchain preamble, the go120ld header
|
||||
// with its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays — with the loong64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and R_LOONG64_ADDR_HI/LO relocation types for
|
||||
// the pcalau12i+addi.d address pairs.
|
||||
func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreambleLOONG64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||
// A pcalau12i+addi.d pair: the high part carries
|
||||
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO.
|
||||
if r.Kind == RelLoong64AddrLo {
|
||||
return relocLoong64AddrLo, 4
|
||||
}
|
||||
return relocLoong64AddrHi, 4
|
||||
})
|
||||
}
|
||||
|
||||
// Loong64 relocation types (cmd/internal/objabi). R_LOONG64_ADDR_HI
|
||||
// resolves the high 20 bits of a PC-relative address into pcalau12i;
|
||||
// R_LOONG64_ADDR_LO the low 12 bits into addi.d/ld/st.
|
||||
const (
|
||||
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
|
||||
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
|
||||
// the installed go tool asm writes for loong64, captured by assembling a
|
||||
// one-instruction probe (see toolchainObjectPreamble).
|
||||
var (
|
||||
preambleLOONG64Once sync.Once
|
||||
preambleLOONG64 []byte
|
||||
preambleLOONG64Err error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleLOONG64() ([]byte, error) {
|
||||
preambleLOONG64Once.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleLOONG64Err = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-loong64")
|
||||
if err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_loong64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleLOONG64Err = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleLOONG64Err = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleLOONG64 = data[:i+3]
|
||||
})
|
||||
return preambleLOONG64, preambleLOONG64Err
|
||||
}
|
||||
+25
-282
@@ -5,7 +5,6 @@ package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -13,298 +12,42 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectRISCV emits a GOOBJ object file for RISC-V.
|
||||
// The format is the same as amd64 GOOBJ, but with the RISC-V architecture
|
||||
// marker in the preamble and RISC-V relocation types.
|
||||
// GOObjectRISCV emits a GOOBJ object file for RISC-V. The layout is the
|
||||
// shared one in goobj.go — the toolchain preamble, the go120ld header with
|
||||
// its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays — with the RISC-V preamble, the MinLC of 2 for
|
||||
// the pc-value deltas, and the single R_RISCV_PCREL_ITYPE/STYPE relocation
|
||||
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
|
||||
// relocation, not the ELF HI20/LO12 pair).
|
||||
func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||
if pkgPath == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||
}
|
||||
pre, err := toolchainObjectPreambleRISCV()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The symbol tables. Package definitions: the GLOBL symbols, then one
|
||||
// anonymous FuncInfo symbol per function. Non-package definitions: the
|
||||
// pc-value tables and the functions themselves, as cmd/asm lays them
|
||||
// out. defIdx maps a GLOBL's bare name to its definition index for the
|
||||
// relocations; fnNpIdx maps a function to its non-package index.
|
||||
var defs []goSym
|
||||
var defData [][]byte
|
||||
defIdx := map[string]int{}
|
||||
for _, d := range img.DataSyms {
|
||||
name := d.Name
|
||||
if !d.Static {
|
||||
name = pkgPath + "." + name
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) {
|
||||
switch r.Kind {
|
||||
case RelRISCVPCRELSType:
|
||||
return relocRISCVPcrelStype, 8
|
||||
case RelRISCVJal:
|
||||
return relocRISCVJal, 4
|
||||
default:
|
||||
return relocRISCVPcrelItype, 8
|
||||
}
|
||||
typ := uint8(kindSDATA)
|
||||
if d.Rodata {
|
||||
typ = kindSRODATA
|
||||
}
|
||||
flag := uint8(0)
|
||||
if d.Dupok {
|
||||
flag = symFlagDupok
|
||||
}
|
||||
abi := uint16(0)
|
||||
if d.Static {
|
||||
abi = symABIStatic
|
||||
}
|
||||
defIdx[d.Name] = len(defs)
|
||||
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)})
|
||||
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||
}
|
||||
fnFiIdx := make([]int, len(img.Funcs))
|
||||
for i := range img.Funcs {
|
||||
data := marshalFuncInfo(img.Funcs[i])
|
||||
fnFiIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
||||
defData = append(defData, data)
|
||||
}
|
||||
|
||||
type npSym struct {
|
||||
sym goSym
|
||||
data []byte
|
||||
}
|
||||
var nps []npSym
|
||||
type pcRefs struct{ sp, file, line, inl int }
|
||||
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||
fnNpIdx := make([]int, len(img.Funcs))
|
||||
for i, fn := range img.Funcs {
|
||||
tables := []struct {
|
||||
data []byte
|
||||
dst *int
|
||||
}{
|
||||
{pcspTable(fn), &pcIdx[i].sp},
|
||||
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
|
||||
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
|
||||
{pcValueFlat(-1, fn.Size), &pcIdx[i].inl},
|
||||
}
|
||||
for _, t := range tables {
|
||||
*t.dst = len(nps)
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||
data: t.data,
|
||||
})
|
||||
}
|
||||
name := fn.Name
|
||||
abi := uint16(0)
|
||||
if fn.Static {
|
||||
abi = symABIStatic
|
||||
} else {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
flag := uint8(0)
|
||||
if fn.NoSplit {
|
||||
flag |= symFlagNoSplit
|
||||
}
|
||||
fnNpIdx[i] = len(nps)
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
for _, r := range fn.Relocs {
|
||||
// The linker writes the resolved displacement into the field;
|
||||
// leave it zero, as cmd/asm's object does.
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
|
||||
// Relocations, per defined symbol in definition order (package defs,
|
||||
// then non-package defs). Only file-local GLOBL references resolve;
|
||||
// external symbols need the import machinery of a later increment.
|
||||
nsyms := len(defs) + len(nps)
|
||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||
for i, fn := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
if r.External {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name)
|
||||
}
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = 4 // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Aux entries per function: FuncInfo, then the four pc tables.
|
||||
// References into the non-package table use pkgIdxNone.
|
||||
symAux := make([][]byte, nsyms)
|
||||
for i := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
aux := func(typ uint8, pkg, idx uint32) {
|
||||
var rec [9]byte
|
||||
rec[0] = typ
|
||||
binary.LittleEndian.PutUint32(rec[1:], pkg)
|
||||
binary.LittleEndian.PutUint32(rec[5:], idx)
|
||||
symAux[si] = append(symAux[si], rec[:]...)
|
||||
}
|
||||
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
||||
aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
|
||||
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
|
||||
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
|
||||
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl))
|
||||
}
|
||||
|
||||
// --- Serialise ---
|
||||
|
||||
// String table: all symbol names, NUL-terminated.
|
||||
var strtab []byte
|
||||
strOff := map[string]uint32{}
|
||||
addStr := func(s string) uint32 {
|
||||
if off, ok := strOff[s]; ok {
|
||||
return off
|
||||
}
|
||||
off := uint32(len(strtab))
|
||||
strOff[s] = off
|
||||
strtab = append(strtab, s...)
|
||||
strtab = append(strtab, 0)
|
||||
return off
|
||||
}
|
||||
for _, s := range defs {
|
||||
addStr(s.name)
|
||||
}
|
||||
for _, s := range nps {
|
||||
addStr(s.sym.name)
|
||||
}
|
||||
|
||||
// Symbol definition records (21 bytes each).
|
||||
var symdef, nonpkgdef []byte
|
||||
for _, s := range defs {
|
||||
symdef = s.append(symdef, strOff)
|
||||
}
|
||||
for _, s := range nps {
|
||||
nonpkgdef = s.sym.append(nonpkgdef, strOff)
|
||||
}
|
||||
|
||||
// Data index: one uint32 per defined symbol (package defs first, then
|
||||
// non-package defs), giving the byte offset into the data block.
|
||||
var dataIdx []byte
|
||||
var dataBlk []byte
|
||||
off := uint32(0)
|
||||
for _, d := range defData {
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||
dataBlk = append(dataBlk, d...)
|
||||
off += uint32(len(d))
|
||||
}
|
||||
for _, s := range nps {
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||
dataBlk = append(dataBlk, s.data...)
|
||||
off += uint32(len(s.data))
|
||||
}
|
||||
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off) // sentinel
|
||||
|
||||
// Relocation index: one uint32 per symbol, giving the byte offset into
|
||||
// the reloc block.
|
||||
var relocIdx []byte
|
||||
roff := uint32(0)
|
||||
for i := 0; i < nsyms; i++ {
|
||||
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff)
|
||||
roff += uint32(len(symRelocs[i]))
|
||||
}
|
||||
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff) // sentinel
|
||||
var relocBlk []byte
|
||||
for _, r := range symRelocs {
|
||||
relocBlk = append(relocBlk, r...)
|
||||
}
|
||||
|
||||
// Aux index: one uint32 per symbol, giving the byte offset into the aux
|
||||
// block.
|
||||
var auxIdx []byte
|
||||
aoff := uint32(0)
|
||||
for i := 0; i < nsyms; i++ {
|
||||
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff)
|
||||
aoff += uint32(len(symAux[i]))
|
||||
}
|
||||
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff) // sentinel
|
||||
var auxBlk []byte
|
||||
for _, a := range symAux {
|
||||
auxBlk = append(auxBlk, a...)
|
||||
}
|
||||
|
||||
// File table: one entry, the source file.
|
||||
var fileBlk []byte
|
||||
fileOff := addStr(srcPath)
|
||||
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, uint32(len(srcPath)))
|
||||
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, fileOff)
|
||||
|
||||
// Assemble the object.
|
||||
var out bytes.Buffer
|
||||
out.Write(pre)
|
||||
out.WriteString(goobjMagic)
|
||||
|
||||
// Block offsets (20 bytes into the header: 4 magic + 8 go version +
|
||||
// 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
||||
// We'll fill these in after we know the sizes.
|
||||
hdrStart := out.Len()
|
||||
out.Write(make([]byte, 4*(blkEnd+1)))
|
||||
|
||||
writeBlock := func(data []byte) {
|
||||
out.Write(data)
|
||||
}
|
||||
|
||||
// Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef,
|
||||
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
||||
// reloc, aux, data, refname.
|
||||
writeBlock(nil) // autolib
|
||||
writeBlock(nil) // pkgidx
|
||||
writeBlock(fileBlk) // file
|
||||
writeBlock(symdef) // symdef
|
||||
writeBlock(nil) // hashed64def
|
||||
writeBlock(nil) // hasheddef
|
||||
writeBlock(nonpkgdef) // nonpkgdef
|
||||
writeBlock(nil) // nonpkgref
|
||||
writeBlock(nil) // refflags
|
||||
writeBlock(nil) // hash64
|
||||
writeBlock(nil) // hash
|
||||
writeBlock(relocIdx) // relocidx
|
||||
writeBlock(auxIdx) // auxidx
|
||||
writeBlock(dataIdx) // dataidx
|
||||
writeBlock(relocBlk) // reloc
|
||||
writeBlock(auxBlk) // aux
|
||||
writeBlock(dataBlk) // data
|
||||
writeBlock(nil) // refname
|
||||
|
||||
// Fill in the block offsets.
|
||||
le := binary.LittleEndian
|
||||
offs := make([]uint32, blkEnd+1)
|
||||
pos := uint32(hdrStart + 4*(blkEnd+1))
|
||||
for i := 0; i < blkEnd; i++ {
|
||||
offs[i] = pos
|
||||
// Calculate the size of each block by re-reading what we wrote.
|
||||
// This is a simplification; a real implementation would track sizes.
|
||||
}
|
||||
offs[blkEnd] = uint32(out.Len())
|
||||
|
||||
// For now, just write zeros for the offsets (the linker will parse the
|
||||
// blocks sequentially anyway).
|
||||
for i := 0; i <= blkEnd; i++ {
|
||||
le.PutUint32(out.Bytes()[hdrStart+4*i:], offs[i])
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// RISC-V relocation types (cmd/internal/objabi).
|
||||
// RISC-V relocation types (cmd/internal/objabi). The Go linker applies
|
||||
// R_RISCV_PCREL_ITYPE/STYPE to an AUIPC + I/S-type instruction pair as a
|
||||
// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction.
|
||||
const (
|
||||
relocRISCVPcrelHi20 = 23
|
||||
relocRISCVPcrelLo12I = 24
|
||||
relocRISCVPcrelLo12S = 25
|
||||
relocRISCVJal = 59 // R_RISCV_JAL
|
||||
relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE
|
||||
relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleRISCV returns the RISC-V object preamble.
|
||||
// toolchainObjectPreambleRISCV returns the "go object ...\n!\n" header
|
||||
// the installed go tool asm writes for riscv64, captured by assembling a
|
||||
// one-instruction probe (see toolchainObjectPreamble).
|
||||
var (
|
||||
preambleRISCVOnce sync.Once
|
||||
preambleRISCV []byte
|
||||
|
||||
+246
-13
@@ -48,6 +48,57 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
|
||||
// Integer scalar XMM moves: MOVQ with an XMM operand is the SSE2
|
||||
// packed-quadword move, NOT a GPR move: mem→xmm encodes as F3 0F 7E
|
||||
// (reg = dst, no REX.W — the Go assembler's form), xmm→mem as
|
||||
// 66 0F D6 (rm = xmm). Register forms against a GPR use the MOVD
|
||||
// opcodes with REX.W instead: 66 REX.W 0F 6E (gpr→xmm) and
|
||||
// 66 REX.W 0F 7E (xmm→gpr); the memory opcodes with a register r/m
|
||||
// would be undefined forms. MOVL is the packed-dword move:
|
||||
// 66 0F 6E load, 66 0F 7E store, no REX.W. A GPR-move fallback would
|
||||
// silently emit REX.W 8B with the wrong operand meaning.
|
||||
_, srcVec := vecReg(src)
|
||||
dstReg, dstVec := vecReg(dst)
|
||||
if srcVec || dstVec {
|
||||
if dstVec {
|
||||
if g, ok := src.(Reg); ok && !g.isVec() {
|
||||
i := &instr{prefix: 0x66, opcode: []byte{0x0F, 0x6E}, modrm: -1, sib: -1, rexW: size == 8}
|
||||
if err := setRM(i, dstReg, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
i := &instr{prefix: 0xF3, opcode: []byte{0x0F, 0x7E}, modrm: -1, sib: -1}
|
||||
if size == 4 {
|
||||
i.prefix = 0x66
|
||||
i.opcode = []byte{0x0F, 0x6E}
|
||||
}
|
||||
if err := setRM(i, dstReg, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
srcXMM, srcIsXMM := src.(Reg)
|
||||
if !srcIsXMM || !srcXMM.isVec() {
|
||||
return fmt.Errorf("MOV: store needs an XMM source")
|
||||
}
|
||||
if g, ok := dst.(Reg); ok && !g.isVec() {
|
||||
i := &instr{prefix: 0x66, opcode: []byte{0x0F, 0x7E}, modrm: -1, sib: -1, rexW: size == 8}
|
||||
if err := setRM(i, srcXMM, dst, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
i := &instr{prefix: 0x66, opcode: []byte{0x0F, 0xD6}, modrm: -1, sib: -1}
|
||||
if size == 4 {
|
||||
i.opcode = []byte{0x0F, 0x7E}
|
||||
}
|
||||
if err := setRM(i, srcXMM, dst, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
dstReg, dstIsReg := dst.(Reg)
|
||||
switch src := src.(type) {
|
||||
case Reg:
|
||||
@@ -91,7 +142,28 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
||||
|
||||
case Imm:
|
||||
if dstIsReg {
|
||||
// MOV r, imm: 0xB0+reg (8-bit) / 0xB8+reg (16/32/64, imm64 for Q).
|
||||
v := int64(src)
|
||||
// The Go assembler compresses 64-bit moves whose immediate fits
|
||||
// a signed int32, choosing per sign:
|
||||
// v >= 0: B8+rd imm32 without REX.W (zero-extended by the
|
||||
// hardware, REX.B still emitted for R8-R15);
|
||||
// v < 0: REX.W C7 /0 imm32 (sign-extended — the plain B8+rd
|
||||
// form would zero-extend and corrupt the value).
|
||||
// Out-of-range immediates keep the B8+rd imm64 form.
|
||||
if size == 8 && v >= 0 && v <= (1<<31)-1 {
|
||||
i := newInstr(4, []byte{0xB8 + byte(dstReg.idx&7)})
|
||||
i.rexB = dstReg.idx >= 8
|
||||
i.imm = le32(v)
|
||||
return e.emit(i)
|
||||
}
|
||||
if size == 8 && v < 0 && v >= -(1<<31) {
|
||||
i := newInstr(8, []byte{0xC7})
|
||||
if err := setRMDigit(i, 0, dstReg, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
i.imm = le32(v)
|
||||
return e.emit(i)
|
||||
}
|
||||
opBase := byte(0xB8)
|
||||
if size == 1 {
|
||||
opBase = 0xB0
|
||||
@@ -101,7 +173,7 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
||||
if dstReg.needsREX(size) {
|
||||
i.rexForced = true
|
||||
}
|
||||
i.imm = immediate(int64(src), size, true)
|
||||
i.imm = immediate(v, size, true)
|
||||
return e.emit(i)
|
||||
}
|
||||
// MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0.
|
||||
@@ -144,7 +216,13 @@ func (e *enc) encodeALU(op struct {
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
|
||||
// CMP never takes its immediate first: the Go assembler rejects
|
||||
// CMPL $0, AX outright (only CMPL AX, $0 is legal, unlike TEST and the
|
||||
// writing ALU ops whose immediate is naturally the source).
|
||||
if imm, ok := src.(Imm); ok {
|
||||
if op.digit == 7 {
|
||||
return fmt.Errorf("CMP immediate must be the second operand (reg, $imm)")
|
||||
}
|
||||
return e.encodeALUImm(op.digit, dst, int64(imm), size)
|
||||
}
|
||||
|
||||
@@ -235,6 +313,15 @@ func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
|
||||
i.imm = []byte{byte(int8(imm))}
|
||||
return e.emit(i)
|
||||
}
|
||||
// 0x81 /digit, imm16/imm32 — or the Go assembler's accumulator short
|
||||
// form (opcode+5, no ModR/M) when the destination is AX/AL, which it
|
||||
// prefers over the generic form exactly here.
|
||||
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
||||
accOp := map[int]byte{0: 0x05, 1: 0x0D, 2: 0x15, 3: 0x1D, 4: 0x25, 5: 0x2D, 6: 0x35, 7: 0x3D}[digit]
|
||||
i := newInstr(size, []byte{accOp})
|
||||
i.imm = immediate(imm, size, false)
|
||||
return e.emit(i)
|
||||
}
|
||||
// 0x81 /digit, imm16/imm32.
|
||||
i := newInstr(size, []byte{0x81})
|
||||
if err := setRMDigit(i, digit, dst, size); err != nil {
|
||||
@@ -252,7 +339,18 @@ func (e *enc) encodeTest(ops []Operand, size int) error {
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
if imm, ok := src.(Imm); ok {
|
||||
// TEST r/m, imm: 0xF6 (8-bit) / 0xF7 /0.
|
||||
// TEST r/m, imm: 0xF6 (8-bit) / 0xF7 /0 — but the Go assembler
|
||||
// always uses the accumulator forms (A8/A9, no ModR/M) when the
|
||||
// register operand is AL/AX, whatever the immediate's width.
|
||||
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
||||
op := byte(0xA9)
|
||||
if size == 1 {
|
||||
op = 0xA8
|
||||
}
|
||||
i := newInstr(size, []byte{op})
|
||||
i.imm = immediate(int64(imm), size, false)
|
||||
return e.emit(i)
|
||||
}
|
||||
op := byte(0xF7)
|
||||
if size == 1 {
|
||||
op = 0xF6
|
||||
@@ -597,31 +695,60 @@ func (e *enc) encodeSet(upper string, ops []Operand) error {
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- LZCNT / TZCNT ----------------------------------------------------------
|
||||
// --- bit scan / bit count ----------------------------------------------------
|
||||
|
||||
// encodeCount encodes LZCNT/TZCNT (leading / trailing zero count): F3 0F BD
|
||||
// or F3 0F BC, with reg = dst and rm = src. The size suffix selects the
|
||||
// operand width (LZCNTW/LZCNTL/LZCNTQ).
|
||||
// countOp maps the bit-scan and bit-count mnemonics to their opcode byte and
|
||||
// mandatory prefix. TZCNT/LZCNT/POPCNT are the F3-prefixed forms of the
|
||||
// same map as BSF/BSR's 0F BC/BD; POPCNT is F3 0F B8.
|
||||
var countOp = map[string]struct {
|
||||
op byte
|
||||
prefix byte
|
||||
}{
|
||||
"BSF": {0xBC, 0},
|
||||
"BSR": {0xBD, 0},
|
||||
"TZCNT": {0xBC, 0xF3},
|
||||
"LZCNT": {0xBD, 0xF3},
|
||||
"POPCNT": {0xB8, 0xF3},
|
||||
}
|
||||
|
||||
// encodeCount encodes the bit-scan and bit-count family — BSF (0F BC),
|
||||
// BSR (0F BD), TZCNT (F3 0F BC), LZCNT (F3 0F BD) and POPCNT (F3 0F B8) —
|
||||
// with reg = dst and rm = src. The size suffix selects the operand width
|
||||
// (BSFQ, TZCNTL, …). Note BSF/BSR leave the destination undefined when the
|
||||
// source is zero (unlike their F3-prefixed counterparts); callers must
|
||||
// guard non-zero inputs themselves.
|
||||
func (e *enc) encodeCount(base string, ops []Operand, size int) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops))
|
||||
}
|
||||
op := byte(0xBD)
|
||||
if base == "TZCNT" {
|
||||
op = 0xBC
|
||||
}
|
||||
spec := countOp[base]
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s destination must be a register", base)
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, op})
|
||||
i.prefix = 0xF3
|
||||
i := newInstr(size, []byte{0x0F, spec.op})
|
||||
i.prefix = spec.prefix
|
||||
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeBswap encodes BSWAP: the single register operand is encoded in the
|
||||
// opcode byte (0F C8+r), with REX.B for R8-R15 and REX.W for the quad form.
|
||||
func (e *enc) encodeBswap(ops []Operand, size int) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("BSWAP expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
reg, ok := ops[0].(Reg)
|
||||
if !ok {
|
||||
return fmt.Errorf("BSWAP operand must be a register")
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, 0xC8 + byte(reg.idx&7)})
|
||||
i.rexB = reg.idx >= 8
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- mixed-width sign/zero-extending moves -----------------------------------
|
||||
|
||||
// movExtendOp maps Go's mixed-width move names to their opcode and destination
|
||||
@@ -721,6 +848,112 @@ func (e *enc) encodeSSEMove(m sseMove, ops []Operand) error {
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- legacy SSE packed binary and shuffles -----------------------------------
|
||||
|
||||
// sseBin describes a legacy (non-VEX) SSE packed/scalar binary op: an
|
||||
// optional mandatory prefix plus the 0F-prefixed opcode (0F38 for the
|
||||
// SSSE3 integer shuffles). Plan 9 asm lists the source operand first, so
|
||||
// MULPS X0, X1 computes X1 = X1 * X0.
|
||||
type sseBin struct {
|
||||
prefix byte // 0, 0x66, 0xF2 or 0xF3
|
||||
op byte
|
||||
map38 bool // opcode lives under 0F38 instead of 0F
|
||||
}
|
||||
|
||||
var sseBinTable = map[string]sseBin{
|
||||
"ADDPS": {0, 0x58, false}, "ADDPD": {0x66, 0x58, false},
|
||||
"MULPS": {0, 0x59, false}, "MULPD": {0x66, 0x59, false},
|
||||
"SUBPS": {0, 0x5C, false}, "SUBPD": {0x66, 0x5C, false},
|
||||
"DIVPS": {0, 0x5E, false}, "DIVPD": {0x66, 0x5E, false},
|
||||
"ANDPS": {0, 0x54, false}, "ANDPD": {0x66, 0x54, false},
|
||||
"ORPS": {0, 0x56, false}, "ORPD": {0x66, 0x56, false},
|
||||
"XORPS": {0, 0x57, false}, "XORPD": {0x66, 0x57, false},
|
||||
"MINPS": {0, 0x5D, false}, "MINPD": {0x66, 0x5D, false},
|
||||
"MAXPS": {0, 0x5F, false}, "MAXPD": {0x66, 0x5F, false},
|
||||
"ADDSS": {0xF3, 0x58, false}, "ADDSD": {0xF2, 0x58, false},
|
||||
"MULSS": {0xF3, 0x59, false}, "MULSD": {0xF2, 0x59, false},
|
||||
"SUBSS": {0xF3, 0x5C, false}, "SUBSD": {0xF2, 0x5C, false},
|
||||
"DIVSS": {0xF3, 0x5E, false}, "DIVSD": {0xF2, 0x5E, false},
|
||||
"MINSS": {0xF3, 0x5D, false}, "MINSD": {0xF2, 0x5D, false},
|
||||
"MAXSS": {0xF3, 0x5F, false}, "MAXSD": {0xF2, 0x5F, false},
|
||||
"UNPCKLPS": {0, 0x14, false}, "UNPCKHPS": {0, 0x15, false},
|
||||
"UNPCKLPD": {0x66, 0x14, false}, "UNPCKHPD": {0x66, 0x15, false},
|
||||
"CVTSS2SD": {0xF3, 0x5A, false}, "CVTSD2SS": {0xF2, 0x5A, false},
|
||||
"CVTPS2PD": {0, 0x5A, false}, "CVTPD2PS": {0x66, 0x5A, false},
|
||||
// SSE2 packed integers (reg = reg op rm) and the SSSE3 byte shuffle.
|
||||
"PXOR": {0x66, 0xEF, false},
|
||||
"POR": {0x66, 0xEB, false},
|
||||
"PAND": {0x66, 0xDB, false},
|
||||
"PANDN": {0x66, 0xDF, false},
|
||||
"PADDB": {0x66, 0xFC, false}, "PADDW": {0x66, 0xFD, false},
|
||||
"PADDD": {0x66, 0xFE, false}, "PADDQ": {0x66, 0xD4, false},
|
||||
"PSUBB": {0x66, 0xF8, false}, "PSUBW": {0x66, 0xF9, false},
|
||||
"PSUBD": {0x66, 0xFA, false}, "PSUBQ": {0x66, 0xFB, false},
|
||||
"PCMPEQB": {0x66, 0x74, false}, "PCMPEQW": {0x66, 0x75, false},
|
||||
"PCMPEQD": {0x66, 0x76, false},
|
||||
"PCMPGTB": {0x66, 0x64, false}, "PCMPGTW": {0x66, 0x65, false},
|
||||
"PCMPGTD": {0x66, 0x66, false},
|
||||
"PSHUFB": {0x66, 0x00, true},
|
||||
}
|
||||
|
||||
// sseShuf describes a legacy SSE shuffle taking a trailing imm8
|
||||
// (PSHUFD/PSHUFHW/PSHUFLW also carry the packed-int 0x66/F3/F2 prefixes).
|
||||
type sseShuf struct {
|
||||
prefix byte
|
||||
op byte
|
||||
}
|
||||
|
||||
var sseShufTable = map[string]sseShuf{
|
||||
"SHUFPS": {0, 0xC6}, "SHUFPD": {0x66, 0xC6},
|
||||
"PSHUFD": {0x66, 0x70}, "PSHUFHW": {0xF3, 0x70}, "PSHUFLW": {0xF2, 0x70},
|
||||
}
|
||||
|
||||
// encodeSSEBin encodes reg = reg op rm (memory allowed for rm).
|
||||
func (e *enc) encodeSSEBin(m sseBin, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("SSE binary expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
dstReg, ok := dst.(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return fmt.Errorf("SSE binary destination must be a vector register")
|
||||
}
|
||||
opcode := []byte{0x0F, m.op}
|
||||
if m.map38 {
|
||||
opcode = []byte{0x0F, 0x38, m.op}
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: opcode, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSSEShuf encodes an imm8 shuffle: SHUFPS $imm, src, dst.
|
||||
func (e *enc) encodeSSEShuf(m sseShuf, ops []Operand) error {
|
||||
if len(ops) != 3 {
|
||||
return fmt.Errorf("SSE shuffle expects 3 operands, got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("SSE shuffle needs an imm8 first operand")
|
||||
}
|
||||
if imm < -128 || imm > 255 {
|
||||
return fmt.Errorf("SSE shuffle imm8 %d out of range", imm)
|
||||
}
|
||||
src, dst := ops[1], ops[2]
|
||||
dstReg, ok2 := dst.(Reg)
|
||||
if !ok2 || !dstReg.isVec() {
|
||||
return fmt.Errorf("SSE shuffle destination must be a vector register")
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
i.imm = []byte{byte(int8(imm))}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// --- CVTSL2SD / CVTSQ2SD -----------------------------------------------------
|
||||
|
||||
// encodeCvtsi2sd encodes a signed integer to scalar double conversion
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||||
// the symbol tables, the function code bytes and the relocation wiring.
|
||||
func TestGOObjectLOONG64Structure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_loong64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
GLOBL ·table<>(SB), RODATA, $8
|
||||
DATA ·table<>+0(SB)/8, $0x1122334455667788
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.GOObjectLOONG64("testpkg", "k_loong64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
|
||||
// Package defs: the static GLOBL, then the FuncInfo and the two DWARF
|
||||
// symbols (debug_line program, subprogram DIE).
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 4 {
|
||||
t.Fatalf("symdefs = %d, want 4", len(defs))
|
||||
}
|
||||
if defs[0].name != "table" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 8 {
|
||||
t.Errorf("table symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||||
t.Errorf("funcinfo symbol = %+v", defs[1])
|
||||
}
|
||||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||||
t.Errorf("lines symbol = %+v", defs[2])
|
||||
}
|
||||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||||
t.Errorf("DIE symbol = %+v", defs[3])
|
||||
}
|
||||
|
||||
// Non-package defs: four pc tables and the function.
|
||||
nps := v.syms(blkNonpkgdef)
|
||||
if len(nps) != 5 {
|
||||
t.Fatalf("nonpkgdefs = %d, want 5", len(nps))
|
||||
}
|
||||
fn := nps[4]
|
||||
if fn.name != "testpkg.add" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 20 {
|
||||
t.Errorf("add symbol = %+v", fn)
|
||||
}
|
||||
|
||||
// The function code: 20 bytes, the ground-truth encoding. It sits
|
||||
// after the GLOBL, FuncInfo, two DWARF symbols and four pc tables.
|
||||
dataIdx := v.blk(blkDataIdx)
|
||||
dataBlk := v.blk(blkData)
|
||||
le := binary.LittleEndian
|
||||
dOff := le.Uint32(dataIdx[8*4:])
|
||||
code := dataBlk[dOff : dOff+20]
|
||||
want := []byte{
|
||||
0x64, 0x20, 0xc0, 0x28, // ld.d r4, 8(r3)
|
||||
0x65, 0x40, 0xc0, 0x28, // ld.d r5, 16(r3)
|
||||
0x84, 0x94, 0x10, 0x00, // add.d r4, r4, r5
|
||||
0x64, 0x60, 0xc0, 0x29, // st.d r4, 24(r3)
|
||||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||||
}
|
||||
for i := range want {
|
||||
if code[i] != want[i] {
|
||||
t.Fatalf("code byte %d = %02x, want %02x", i, code[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The debug_line program: LNE_set_address (the R_ADDR relocation
|
||||
// carries the function address), then one row per line change — the
|
||||
// TEXT is on line 4 (a leading blank line precedes the include), the
|
||||
// instructions on lines 5–9 — an advance to the 20-byte end and an
|
||||
// end-of-sequence.
|
||||
linesOff := le.Uint32(dataIdx[4*2:])
|
||||
lines := dataBlk[linesOff : linesOff+21]
|
||||
wantLines := []byte{
|
||||
0x00, 0x09, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LNE_set_address
|
||||
0x13, // pc 0, line 5
|
||||
0x38, // pc 4, line 6
|
||||
0x38, // pc 8, line 7
|
||||
0x38, // pc 12, line 8
|
||||
0x38, // pc 16, line 9
|
||||
0x02, 0x04, // advance_pc to 20
|
||||
0x00, 0x01, 0x01, // end_sequence
|
||||
}
|
||||
for i := range wantLines {
|
||||
if lines[i] != wantLines[i] {
|
||||
t.Fatalf("lines byte %d = %02x, want %02x", i, lines[i], wantLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The subprogram DIE: abbrev 3 (FUNCTION), the qualified name, the
|
||||
// addrx low_pc slot (R_DWTXTADDR_U4), the size as high_pc, the
|
||||
// call-frame-CFA frame base, decl file/line and the external flag.
|
||||
dieOff := le.Uint32(dataIdx[4*3:])
|
||||
die := dataBlk[dieOff : dieOff+27]
|
||||
wantDie := []byte{
|
||||
0x03,
|
||||
't', 'e', 's', 't', 'p', 'k', 'g', '.', 'a', 'd', 'd', 0,
|
||||
0x00, 0x00, 0x00, 0x00, // low_pc: addrx slot
|
||||
0x14, // high_pc: 20
|
||||
0x01, 0x9c, // frame_base: DW_OP_call_frame_cfa
|
||||
0x01, 0x00, 0x00, 0x00, // decl_file: 1
|
||||
0x04, // decl_line: 4
|
||||
0x01, // external
|
||||
0x00, // end of children
|
||||
}
|
||||
for i := range wantDie {
|
||||
if die[i] != wantDie[i] {
|
||||
t.Fatalf("DIE byte %d = %02x, want %02x", i, die[i], wantDie[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The DWARF symbols carry the function-address references: R_ADDR for
|
||||
// the line program's set_address, R_DWTXTADDR_U4 for the DIE's addrx
|
||||
// slot, both against the function's non-package index. The reloc
|
||||
// index counts relocations, not bytes.
|
||||
relocIdx := v.blk(blkRelocIdx)
|
||||
relocs := v.blk(blkReloc)
|
||||
if le.Uint32(relocIdx[4*2:]) != 0 || le.Uint32(relocIdx[4*3:]) != 1 || le.Uint32(relocIdx[4*4:]) != 2 {
|
||||
t.Fatalf("dwarf reloc index ranges: %d %d %d", le.Uint32(relocIdx[4*2:]), le.Uint32(relocIdx[4*3:]), le.Uint32(relocIdx[4*4:]))
|
||||
}
|
||||
lr := relocs[:23]
|
||||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||||
t.Errorf("lines reloc = %x", lr)
|
||||
}
|
||||
dr := relocs[23:46]
|
||||
if int32(le.Uint32(dr[0:])) != 13 || dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||||
t.Errorf("die reloc = %x", dr)
|
||||
}
|
||||
|
||||
// The pc-value deltas are in MinLC (4) units: the flat pcsp covers
|
||||
// the whole 20-byte function with a delta of 5.
|
||||
pcspOff := le.Uint32(dataIdx[4*4:])
|
||||
if got := dataBlk[pcspOff : pcspOff+3]; !bytes.Equal(got, []byte{0x02, 0x05, 0x00}) {
|
||||
t.Errorf("pcsp = %x, want 020500", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectLOONG64Link cross-compiles a Go program with the gasm-produced
|
||||
// object substituted into the package archive, proving cmd/link accepts the
|
||||
// emitted GOOBJ. The binary is not executed (no LoongArch host or qemu).
|
||||
// Skipped when no Go toolchain is available.
|
||||
func TestGOObjectLOONG64Link(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_loong64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func add(a, b int64) int64
|
||||
|
||||
func main() {
|
||||
if add(20, 22) != 42 {
|
||||
panic("bad add")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module l64link\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the cross build (GOARCH=loong64): the package archive and the
|
||||
// link line.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var pkgArch, work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_loong64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if pkgArch == "" || linkLine == "" || asmObj == "" {
|
||||
t.Skip("could not locate the archive, asm output or link line in the build log")
|
||||
}
|
||||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||||
// The archive member holding the assembler's output is named after the
|
||||
// asm object file (main_loong64.o), as cmd/go packs it with `pack r`.
|
||||
asmMember := filepath.Base(strings.ReplaceAll(asmObj, "$WORK", work))
|
||||
|
||||
// Assemble the same source with gasm and swap the object in.
|
||||
pf, perrs := parser.Parse(filepath.Join(dir, "main_loong64.s"), asmSrc)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs)
|
||||
}
|
||||
pimg, err := AssembleFileLOONG64(pf)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := pimg.GOObjectLOONG64("main", filepath.Join(dir, "main_loong64.s"))
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||||
}
|
||||
|
||||
// Extract the archive, substitute the object member, repack.
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
extract.Dir = membersDir
|
||||
extract.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
// Substitute the gasm object for the assembler's archive member (pack
|
||||
// extracts members read-only).
|
||||
member := filepath.Join(membersDir, asmMember)
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||||
listCmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, filepath.Join(membersDir, m))
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
pack.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Re-link with our archive in place of the toolchain's. The link line
|
||||
// carries a GOROOT assignment and $WORK placeholders; run it through the
|
||||
// shell with the GOEXPERIMENT and GOARCH the toolchain expects (the
|
||||
// linker compares the object header against its own, experiments
|
||||
// included).
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)), "GOARCH=loong64")
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// The binary is not executed: there is no LoongArch host or qemu here.
|
||||
// The link itself and the symbol table prove cmd/link accepted the gasm
|
||||
// object and laid out the function.
|
||||
nm := exec.Command(goBin, "tool", "nm", filepath.Join(dir, "app2"))
|
||||
nm.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
nmOut, err := nm.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("nm gasm-linked binary: %v\n%s", err, nmOut)
|
||||
}
|
||||
if !strings.Contains(string(nmOut), "main.add") {
|
||||
t.Errorf("main.add not found in linked binary:\n%s", nmOut)
|
||||
}
|
||||
}
|
||||
+116
-9
@@ -80,7 +80,7 @@ func (fl *FuncLayout) LineAt(offset int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Reloc is one static-symbol reference within a function body: the disp32
|
||||
// RelocKind Reloc is one static-symbol reference within a function body: the disp32
|
||||
// field at Off (function-relative) must reach the symbol plus Addend,
|
||||
// measured from After, the address just past the instruction. An External
|
||||
// relocation names a symbol no GLOBL in the file defines; the object-file
|
||||
@@ -89,11 +89,15 @@ func (fl *FuncLayout) LineAt(offset int) int {
|
||||
type RelocKind int
|
||||
|
||||
const (
|
||||
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
||||
RelPCRelHI20 // R_RISCV_PCREL_HI20 (AUIPC)
|
||||
RelPCRelLO12 // R_RISCV_PCREL_LO12_I (ADDI, LD)
|
||||
RelPCRelLO12S // R_RISCV_PCREL_LO12_S (SD)
|
||||
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
||||
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
||||
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
||||
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
||||
RelRISCVJal // R_RISCV_JAL (J-type call)
|
||||
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
||||
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
|
||||
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
|
||||
RelArm64Addr // R_ADDRARM64 (ADRP + ADD/LDR/STR pair)
|
||||
RelArm64Branch // R_CALLARM64 (BL instruction)
|
||||
)
|
||||
|
||||
type Reloc struct {
|
||||
@@ -248,7 +252,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, labels, relocs, err := assembleRISCV(t)
|
||||
code, labels, relocs, lines, spadj, err := assembleRISCV(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
@@ -262,6 +266,8 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Lines: lines,
|
||||
Spadj: spadj,
|
||||
Relocs: relocs,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
@@ -289,7 +295,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||
Name: d.name,
|
||||
Pkg: d.pkg,
|
||||
Offset: pos,
|
||||
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
@@ -297,9 +303,110 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
})
|
||||
}
|
||||
|
||||
markExternals(img, dataSyms)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// AssembleFileLOONG64 assembles every TEXT function of a parsed loong64 file
|
||||
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
||||
// code. SB references in the code are encoded as pcalau12i pairs with zero
|
||||
// immediates; the object-file emitters record R_LOONG64_ADDR_HI/LO
|
||||
// relocations for the linker.
|
||||
func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, labels, relocs, lines, spadj, err := assembleLOONG64(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
fl := FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Pkg: t.Name.Pkg,
|
||||
Static: t.Name.Static,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Frame: frameSize(t),
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Lines: lines,
|
||||
Spadj: spadj,
|
||||
Relocs: relocs,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
case "NOSPLIT":
|
||||
fl.NoSplit = true
|
||||
case "SPWRITE":
|
||||
fl.SPWrite = true
|
||||
}
|
||||
}
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, d := range dataSyms {
|
||||
pos := dataStart + len(img.Data)
|
||||
for pos%16 != 0 {
|
||||
img.Data = append(img.Data, 0)
|
||||
pos++
|
||||
}
|
||||
img.Symbols[d.name] = pos
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||
Name: d.name,
|
||||
Pkg: d.pkg,
|
||||
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
|
||||
markExternals(img, dataSyms)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// markExternals identifies relocations that reference symbols not defined in
|
||||
// the file (neither a GLOBL/DATA symbol nor a TEXT function) and records them
|
||||
// as external. The non-amd64 architectures emit relocations for every SB
|
||||
// reference; this post-processing step distinguishes file-local from external.
|
||||
func markExternals(img *Image, dataSyms []dataSym) {
|
||||
known := make(map[string]bool, len(dataSyms)+len(img.Funcs))
|
||||
for _, d := range dataSyms {
|
||||
known[d.name] = true
|
||||
}
|
||||
for _, fn := range img.Funcs {
|
||||
known[fn.Name] = true
|
||||
}
|
||||
externals := map[string]bool{}
|
||||
for i := range img.Funcs {
|
||||
for j := range img.Funcs[i].Relocs {
|
||||
r := &img.Funcs[i].Relocs[j]
|
||||
if !known[r.Name] {
|
||||
r.External = true
|
||||
externals[r.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for name := range externals {
|
||||
img.Externals = append(img.Externals, name)
|
||||
}
|
||||
sort.Strings(img.Externals)
|
||||
}
|
||||
|
||||
// dataSym is one GLOBL symbol and its DATA initialiser.
|
||||
type dataSym struct {
|
||||
name string
|
||||
@@ -381,7 +488,7 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
for j := 0; j < w; j++ {
|
||||
for j := range w {
|
||||
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,595 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
// loong64 (LoongArch) instruction encoding.
|
||||
//
|
||||
// The encoder is data-driven: each mnemonic maps to an instruction format and
|
||||
// an opcode constant, and the format selects the bit layout. The opcode
|
||||
// constants and formats are transcribed from the Go toolchain's own loong64
|
||||
// backend (cmd/internal/obj/loong64), so the emitted bytes match `go tool asm`
|
||||
// exactly — the ground-truth oracle for the verify suite.
|
||||
//
|
||||
// All LoongArch instructions are 32 bits, little-endian. The formats used
|
||||
// here (per the LoongArch Volume I specification):
|
||||
//
|
||||
// 3R opcode[31:15] | rk[4:0] | rj[4:0] | rd[4:0]
|
||||
// 2R opcode[31:15] | rj[4:0] | rd[4:0]
|
||||
// 2RI12 opcode[31:22] | si12[11:0] | rj[4:0] | rd[4:0]
|
||||
// 2RI14 opcode[31:18] | si14[13:0] | rj[4:0] | rd[4:0]
|
||||
// 2RI16 opcode[31:22] | si16[15:0] | rj[4:0] | rd[4:0]
|
||||
// 2RI20 opcode[31:25] | si20[19:0] | rd[4:0]
|
||||
// 1RI21 opcode[31:26] | si21[20:0] | rj[4:0] (BEQZ/BNEZ, B*Z, BC*Z)
|
||||
// B/BL opcode[31:26] | offs[25:0]
|
||||
// 4R opcode[31:20] | r1[4:0] | r2[4:0] | r3[4:0] | r4[4:0]
|
||||
// IRIR opcode[31:22] | msb[4:0] | rj[4:0] | lsb[4:0] | rd[4:0]
|
||||
// 3RI2 opcode[31:17] | sa2[1:0] | rk[4:0] | rj[4:0] | rd[4:0]
|
||||
//
|
||||
// The opcode constants are pre-positioned (they include the zero bit ranges
|
||||
// of the immediate and register fields), mirroring the toolchain's OP_*
|
||||
// helpers, so each l64* function only ORs its fields in.
|
||||
|
||||
import "maps"
|
||||
|
||||
// loong64RegNum returns the 5-bit register number for a LoongArch register
|
||||
// name: R0–R31 (integer), F0–F31 (floating point), FCC0–FCC7 (condition
|
||||
// flags), FCSR0–FCSR31 (control/status) and the ABI aliases the runtime's
|
||||
// assembly uses. Returns -1 for an unrecognised name.
|
||||
func loong64RegNum(name string) int {
|
||||
switch name {
|
||||
case "R0", "ZERO":
|
||||
return 0
|
||||
case "R1", "RA", "LINK":
|
||||
return 1
|
||||
case "R2", "TP":
|
||||
return 2
|
||||
case "R3", "SP":
|
||||
return 3
|
||||
case "R4", "A0":
|
||||
return 4
|
||||
case "R5", "A1":
|
||||
return 5
|
||||
case "R6", "A2":
|
||||
return 6
|
||||
case "R7", "A3":
|
||||
return 7
|
||||
case "R8", "A4":
|
||||
return 8
|
||||
case "R9", "A5":
|
||||
return 9
|
||||
case "R10", "A6":
|
||||
return 10
|
||||
case "R11", "A7":
|
||||
return 11
|
||||
case "R12", "T0":
|
||||
return 12
|
||||
case "R13", "T1":
|
||||
return 13
|
||||
case "R14", "T2":
|
||||
return 14
|
||||
case "R15", "T3":
|
||||
return 15
|
||||
case "R16", "T4":
|
||||
return 16
|
||||
case "R17", "T5":
|
||||
return 17
|
||||
case "R18", "T6":
|
||||
return 18
|
||||
case "R19", "T7":
|
||||
return 19
|
||||
case "R20", "T8":
|
||||
return 20
|
||||
case "R21":
|
||||
return 21
|
||||
case "R22", "G", "g", "FP":
|
||||
return 22
|
||||
case "R23", "S0":
|
||||
return 23
|
||||
case "R24", "S1":
|
||||
return 24
|
||||
case "R25", "S2":
|
||||
return 25
|
||||
case "R26", "S3":
|
||||
return 26
|
||||
case "R27", "S4":
|
||||
return 27
|
||||
case "R28", "S5":
|
||||
return 28
|
||||
case "R29", "S6", "CTXT":
|
||||
return 29
|
||||
case "R30", "S7", "TMP":
|
||||
return 30
|
||||
case "R31", "S8":
|
||||
return 31
|
||||
}
|
||||
// F0–F31, FCC0–FCC7, FCSR0–FCSR31.
|
||||
if len(name) >= 4 && name[:4] == "FCSR" {
|
||||
return loong64RegSpecial(name[4:], 31)
|
||||
}
|
||||
if len(name) >= 3 && name[:3] == "FCC" {
|
||||
return loong64RegSpecial(name[3:], 7)
|
||||
}
|
||||
if len(name) < 2 {
|
||||
return -1
|
||||
}
|
||||
prefix, digits := name[:1], name[1:]
|
||||
if digits[0] < '0' || digits[0] > '9' {
|
||||
return -1
|
||||
}
|
||||
n := 0
|
||||
for i := 0; i < len(digits); i++ {
|
||||
if digits[i] < '0' || digits[i] > '9' {
|
||||
return -1
|
||||
}
|
||||
n = n*10 + int(digits[i]-'0')
|
||||
}
|
||||
if prefix == "F" && n <= 31 {
|
||||
return n
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// loong64RegSpecial parses a numbered FCC/FCSR register.
|
||||
func loong64RegSpecial(digits string, max int) int {
|
||||
if digits == "" {
|
||||
return -1
|
||||
}
|
||||
n := 0
|
||||
for i := 0; i < len(digits); i++ {
|
||||
if digits[i] < '0' || digits[i] > '9' {
|
||||
return -1
|
||||
}
|
||||
n = n*10 + int(digits[i]-'0')
|
||||
}
|
||||
if n <= max {
|
||||
return n
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ---- format helpers ----
|
||||
|
||||
// l64rrr encodes a 3R instruction: op | rk<<10 | rj<<5 | rd.
|
||||
func l64rrr(op uint32, rk, rj, rd int) uint32 {
|
||||
return op | uint32(rk&0x1f)<<10 | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64rr encodes a 2R instruction: op | rj<<5 | rd.
|
||||
func l64rr(op uint32, rj, rd int) uint32 {
|
||||
return op | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64irr encodes a 2RI12 instruction: op | si12<<10 | rj<<5 | rd.
|
||||
func l64irr(op uint32, imm, rj, rd int) uint32 {
|
||||
return op | (uint32(imm)&0xFFF)<<10 | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64irr14 encodes a 2RI14 instruction: op | si14<<10 | rj<<5 | rd.
|
||||
func l64irr14(op uint32, imm, rj, rd int) uint32 {
|
||||
return op | (uint32(imm)&0x3FFF)<<10 | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64irr16 encodes a 2RI16 instruction: op | si16<<10 | rj<<5 | rd.
|
||||
func l64irr16(op uint32, imm, rj, rd int) uint32 {
|
||||
return op | (uint32(imm)&0xFFFF)<<10 | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64ir encodes a 2RI20 instruction: op | si20<<5 | rd.
|
||||
func l64ir(op uint32, imm, rd int) uint32 {
|
||||
return op | (uint32(imm)&0xFFFFF)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64bbl encodes a B/BL instruction: op | offs[25:0], where offs is the
|
||||
// 4-byte-aligned word distance (the toolchain stores the shifted value).
|
||||
func l64bbl(op uint32, offs int) uint32 {
|
||||
return op | (uint32(offs)&0xFFFF)<<10 | (uint32(offs)>>16)&0x3FF
|
||||
}
|
||||
|
||||
// l64ir21 encodes a 1RI21 branch (BEQZ/BNEZ, BLTZ/BGEZ/BLEZ/BGTZ, BFPT/BFPF):
|
||||
// op | si21[15:0]<<10 | rj<<5 | si21[20:16].
|
||||
func l64ir21(op uint32, offs, rj int) uint32 {
|
||||
v := uint32(offs)
|
||||
return op | (v&0xFFFF)<<10 | uint32(rj&0x1f)<<5 | (v>>16)&0x1F
|
||||
}
|
||||
|
||||
// l64rrrr encodes a 4R instruction: op | r1<<15 | r2<<10 | r3<<5 | r4.
|
||||
func l64rrrr(op uint32, r1, r2, r3, r4 int) uint32 {
|
||||
return op | uint32(r1&0x1f)<<15 | uint32(r2&0x1f)<<10 | uint32(r3&0x1f)<<5 | uint32(r4&0x1f)
|
||||
}
|
||||
|
||||
// l64irir encodes a BSTRINS/BSTRPICK instruction: op | msb<<16 | rj<<5 | lsb<<10 | rd.
|
||||
// The msb/lsb fields are 6 bits wide (0–63) and are validated by the caller.
|
||||
func l64irir(op uint32, msb, rj, lsb, rd int) uint32 {
|
||||
return op | uint32(msb)<<16 | uint32(rj&0x1f)<<5 | uint32(lsb)<<10 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64irrr encodes a 3RI2 instruction (ALSL): op | sa<<15 | rk<<10 | rj<<5 | rd.
|
||||
func l64irrr(op uint32, sa, rk, rj, rd int) uint32 {
|
||||
return op | uint32(sa&0x3)<<15 | uint32(rk&0x1f)<<10 | uint32(rj&0x1f)<<5 | uint32(rd&0x1f)
|
||||
}
|
||||
|
||||
// l64i15 encodes a no-operand system instruction with a 15-bit code field
|
||||
// (SYSCALL, BREAK, DBAR): op | code[14:0].
|
||||
func l64i15(op uint32, code int) uint32 {
|
||||
return op | uint32(code)&0x7FFF
|
||||
}
|
||||
|
||||
// l64irr5i encodes PRELD: op | offs<<10 | rj<<5 | hint.
|
||||
func l64irr5i(op uint32, offs, rj, hint int) uint32 {
|
||||
return op | (uint32(offs)&0xFFF)<<10 | uint32(rj&0x1f)<<5 | uint32(hint&0x1f)
|
||||
}
|
||||
|
||||
// l64wordLE encodes a uint32 as 4 little-endian bytes.
|
||||
func l64wordLE(w uint32) []byte {
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||
}
|
||||
|
||||
// l64WordsLE concatenates one or more instruction words as little-endian bytes.
|
||||
func l64WordsLE(ws ...uint32) []byte {
|
||||
var out []byte
|
||||
for _, w := range ws {
|
||||
out = append(out, l64wordLE(w)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- instruction formats ----
|
||||
|
||||
type l64Format uint8
|
||||
|
||||
const (
|
||||
l64Frrr l64Format = iota // 3R (integer and FP arithmetic)
|
||||
l64Frr // 2R
|
||||
l64Firr // 2RI12 (arithmetic with 12-bit immediate)
|
||||
l64Firr14 // 2RI14 (ldptr/stptr)
|
||||
l64Firr16 // 2RI16 (addu16i.d)
|
||||
l64Fir20 // 2RI20 (lu12i.w, lu32i.d, pcalau12i, pcaddu12i)
|
||||
l64Frrrr // 4R (fmadd/fmsub/fnmadd/fnmsub)
|
||||
l64Firir // bstrins/bstrpick
|
||||
l64Firrr // alsl
|
||||
l64Fi15 // syscall/break/dbar
|
||||
l64Fam // atomic (3R with the AM field order)
|
||||
l64Frdtime // rdtime (rd at bits [9:5], rj at bits [4:0])
|
||||
l64Fshift // 2RI12 with a 5/6-bit shift immediate
|
||||
l64Fpreld // preld (2RI12 + 5-bit hint)
|
||||
)
|
||||
|
||||
// l64Enc is one instruction's encoding: its bit layout (format) and the
|
||||
// opcode constant, positioned at its exact bit range.
|
||||
type l64Enc struct {
|
||||
format l64Format
|
||||
op uint32
|
||||
}
|
||||
|
||||
// l64DualEnc holds both forms of a dual-form mnemonic: the 3R register form
|
||||
// and the 2RI12 immediate form (which is a shift for the shift mnemonics).
|
||||
type l64DualEnc struct {
|
||||
rrr uint32 // 3R register form
|
||||
imm uint32 // 2RI12 immediate form
|
||||
shift bool // the immediate form is a 5/6-bit shift amount
|
||||
}
|
||||
|
||||
// l64DualTable maps the dual-form arithmetic/logic mnemonics to both
|
||||
// encodings; the assembler picks by operand kind.
|
||||
var l64DualTable = map[string]l64DualEnc{}
|
||||
|
||||
// l64InstrTable maps LoongArch mnemonics (as the Go assembler spells them)
|
||||
// to their encoding. SIMD (LSX/LASX: V*/XV*) instructions are not covered
|
||||
// yet; the base integer, memory and floating-point ISA is complete.
|
||||
var l64InstrTable = map[string]l64Enc{}
|
||||
|
||||
func init() {
|
||||
// 3R — integer.
|
||||
rrr := map[string]uint32{
|
||||
"ADD": 0x20 << 15, "ADDW": 0x20 << 15, "ADDV": 0x21 << 15, "ADDVU": 0x21 << 15,
|
||||
"SUB": 0x22 << 15, "SUBW": 0x22 << 15, "SUBV": 0x23 << 15, "SUBVU": 0x23 << 15,
|
||||
"SGT": 0x24 << 15, "SGTU": 0x25 << 15,
|
||||
"MASKEQZ": 0x26 << 15, "MASKNEZ": 0x27 << 15, "SCQ": 0x070AE << 15,
|
||||
"NOR": 0x28 << 15, "AND": 0x29 << 15, "OR": 0x2a << 15, "XOR": 0x2b << 15,
|
||||
"ORN": 0x2c << 15, "ANDN": 0x2d << 15,
|
||||
"SLL": 0x2e << 15, "SRL": 0x2f << 15, "SRA": 0x30 << 15,
|
||||
"SLLV": 0x31 << 15, "SRLV": 0x32 << 15, "SRAV": 0x33 << 15,
|
||||
"ROTR": 0x36 << 15, "ROTRV": 0x37 << 15,
|
||||
"MUL": 0x38 << 15, "MULW": 0x38 << 15, "MULH": 0x39 << 15, "MULHU": 0x3a << 15,
|
||||
"MULV": 0x3b << 15, "MULVU": 0x3b << 15, "MULHV": 0x3c << 15, "MULHVU": 0x3d << 15,
|
||||
"MULWVW": 0x3e << 15, "MULWVWU": 0x3f << 15,
|
||||
"DIV": 0x40 << 15, "DIVW": 0x40 << 15, "REM": 0x41 << 15, "REMW": 0x41 << 15,
|
||||
"DIVU": 0x42 << 15, "DIVWU": 0x42 << 15, "REMU": 0x43 << 15, "REMWU": 0x43 << 15,
|
||||
"DIVV": 0x44 << 15, "REMV": 0x45 << 15, "DIVVU": 0x46 << 15, "REMVU": 0x47 << 15,
|
||||
"CRCWBW": 0x48 << 15, "CRCWHW": 0x49 << 15, "CRCWWW": 0x4a << 15, "CRCWVW": 0x4b << 15,
|
||||
"CRCCWBW": 0x4c << 15, "CRCCWHW": 0x4d << 15, "CRCCWWW": 0x4e << 15, "CRCCWVW": 0x4f << 15,
|
||||
}
|
||||
// 3R — floating point.
|
||||
rrr["MULF"] = 0x209 << 15
|
||||
rrr["MULD"] = 0x20a << 15
|
||||
rrr["DIVF"] = 0x20d << 15
|
||||
rrr["DIVD"] = 0x20e << 15
|
||||
rrr["SUBF"] = 0x205 << 15
|
||||
rrr["SUBD"] = 0x206 << 15
|
||||
rrr["ADDF"] = 0x201 << 15
|
||||
rrr["ADDD"] = 0x202 << 15
|
||||
rrr["CMPEQF"] = 0x0c1<<20 | 0x4<<15
|
||||
rrr["CMPEQD"] = 0x0c2<<20 | 0x4<<15
|
||||
rrr["CMPGED"] = 0x0c2<<20 | 0x7<<15
|
||||
rrr["CMPGEF"] = 0x0c1<<20 | 0x7<<15
|
||||
rrr["CMPGTD"] = 0x0c2<<20 | 0x3<<15
|
||||
rrr["CMPGTF"] = 0x0c1<<20 | 0x3<<15
|
||||
rrr["FMINF"] = 0x215 << 15
|
||||
rrr["FMIND"] = 0x216 << 15
|
||||
rrr["FMAXF"] = 0x211 << 15
|
||||
rrr["FMAXD"] = 0x212 << 15
|
||||
rrr["FMAXAF"] = 0x219 << 15
|
||||
rrr["FMAXAD"] = 0x21a << 15
|
||||
rrr["FMINAF"] = 0x21d << 15
|
||||
rrr["FMINAD"] = 0x21e << 15
|
||||
rrr["FSCALEBF"] = 0x221 << 15
|
||||
rrr["FSCALEBD"] = 0x222 << 15
|
||||
rrr["FCOPYSGF"] = 0x225 << 15
|
||||
rrr["FCOPYSGD"] = 0x226 << 15
|
||||
for m, op := range rrr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frrr, op: op}
|
||||
}
|
||||
|
||||
// 2R.
|
||||
rr := map[string]uint32{
|
||||
"CLOW": 0x4 << 10, "CLZW": 0x5 << 10, "CTOW": 0x6 << 10, "CTZW": 0x7 << 10,
|
||||
"CLOV": 0x8 << 10, "CLZV": 0x9 << 10, "CTOV": 0xa << 10, "CTZV": 0xb << 10,
|
||||
"REVB2H": 0xc << 10, "REVB4H": 0xd << 10, "REVB2W": 0xe << 10, "REVBV": 0xf << 10,
|
||||
"REVH2W": 0x10 << 10, "REVHV": 0x11 << 10,
|
||||
"BITREV4B": 0x12 << 10, "BITREV8B": 0x13 << 10, "BITREVW": 0x14 << 10, "BITREVV": 0x15 << 10,
|
||||
"EXTWH": 0x16 << 10, "EXTWB": 0x17 << 10, "CPUCFG": 0x1b << 10,
|
||||
"TRUNCFV": 0x46a9 << 10, "TRUNCDV": 0x46aa << 10, "TRUNCFW": 0x46a1 << 10, "TRUNCDW": 0x46a2 << 10,
|
||||
"MOVWF": 0x4744 << 10, "MOVVF": 0x4746 << 10, "MOVWD": 0x4748 << 10, "MOVVD": 0x474a << 10,
|
||||
"MOVFW": 0x46c1 << 10, "MOVDW": 0x46c2 << 10, "MOVFV": 0x46c9 << 10, "MOVDV": 0x46ca << 10,
|
||||
"FRINTF": 0x4791 << 10, "FRINTD": 0x4792 << 10,
|
||||
"MOVDF": 0x4646 << 10, "MOVFD": 0x4649 << 10,
|
||||
"ABSF": 0x4501 << 10, "ABSD": 0x4502 << 10,
|
||||
"MOVF": 0x4525 << 10, "MOVD": 0x4526 << 10,
|
||||
"NEGF": 0x4505 << 10, "NEGD": 0x4506 << 10,
|
||||
"SQRTF": 0x4511 << 10, "SQRTD": 0x4512 << 10,
|
||||
"FLOGBF": 0x4509 << 10, "FLOGBD": 0x450a << 10,
|
||||
"FCLASSF": 0x450d << 10, "FCLASSD": 0x450e << 10,
|
||||
"FTINTRMWF": 0x4681 << 10, "FTINTRMWD": 0x4682 << 10,
|
||||
"FTINTRMVF": 0x4689 << 10, "FTINTRMVD": 0x468a << 10,
|
||||
"FTINTRPWF": 0x4691 << 10, "FTINTRPWD": 0x4692 << 10,
|
||||
"FTINTRPVF": 0x4699 << 10, "FTINTRPVD": 0x469a << 10,
|
||||
"FTINTRZWF": 0x46a1 << 10, "FTINTRZWD": 0x46a2 << 10,
|
||||
"FTINTRZVF": 0x46a9 << 10, "FTINTRZVD": 0x46aa << 10,
|
||||
"FTINTRNEWF": 0x46b1 << 10, "FTINTRNEWD": 0x46b2 << 10,
|
||||
"FTINTRNEVF": 0x46b9 << 10, "FTINTRNEVD": 0x46ba << 10,
|
||||
}
|
||||
for m, op := range rr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frr, op: op}
|
||||
}
|
||||
// RDTIME is a 2R instruction with rd and rj in swapped positions.
|
||||
l64InstrTable["RDTIMELW"] = l64Enc{format: l64Frdtime, op: 0x18 << 10}
|
||||
l64InstrTable["RDTIMEHW"] = l64Enc{format: l64Frdtime, op: 0x19 << 10}
|
||||
l64InstrTable["RDTIMED"] = l64Enc{format: l64Frdtime, op: 0x1a << 10}
|
||||
|
||||
// The dual-form arithmetic mnemonics (register 3R + immediate 2RI12),
|
||||
// selected by the operand kind; the shift mnemonics pair the 3R form
|
||||
// with a 5/6-bit shift immediate.
|
||||
maps.Copy(l64DualTable, map[string]l64DualEnc{
|
||||
"ADD": {rrr: 0x20 << 15, imm: 0x00a << 22},
|
||||
"ADDW": {rrr: 0x20 << 15, imm: 0x00a << 22},
|
||||
"ADDV": {rrr: 0x21 << 15, imm: 0x00b << 22},
|
||||
"ADDVU": {rrr: 0x21 << 15, imm: 0x00b << 22},
|
||||
"AND": {rrr: 0x29 << 15, imm: 0x00d << 22},
|
||||
"OR": {rrr: 0x2a << 15, imm: 0x00e << 22},
|
||||
"XOR": {rrr: 0x2b << 15, imm: 0x00f << 22},
|
||||
"SGT": {rrr: 0x24 << 15, imm: 0x008 << 22},
|
||||
"SGTU": {rrr: 0x25 << 15, imm: 0x009 << 22},
|
||||
"SLL": {rrr: 0x2e << 15, imm: 0x00081 << 15, shift: true},
|
||||
"SRL": {rrr: 0x2f << 15, imm: 0x00089 << 15, shift: true},
|
||||
"SRA": {rrr: 0x30 << 15, imm: 0x00091 << 15, shift: true},
|
||||
"ROTR": {rrr: 0x36 << 15, imm: 0x00099 << 15, shift: true},
|
||||
"SLLV": {rrr: 0x31 << 15, imm: 0x0041 << 16, shift: true},
|
||||
"SRLV": {rrr: 0x32 << 15, imm: 0x0045 << 16, shift: true},
|
||||
"SRAV": {rrr: 0x33 << 15, imm: 0x0049 << 16, shift: true},
|
||||
"ROTRV": {rrr: 0x37 << 15, imm: 0x004d << 16, shift: true},
|
||||
})
|
||||
|
||||
// 2RI12 — pure immediate arithmetic (LU52ID has no register form).
|
||||
l64InstrTable["LU52ID"] = l64Enc{format: l64Firr, op: 0x00c << 22}
|
||||
// ADDV16 (addu16i.d): 2RI16 with the immediate shifted right by 16.
|
||||
l64InstrTable["ADDV16"] = l64Enc{format: l64Firr16, op: 0x4 << 26}
|
||||
|
||||
// 2RI14 — LL/SC are aliased by the Go assembler to the pointer loads and
|
||||
// stores (ldptr/stptr), with the offset scaled by 4.
|
||||
l64InstrTable["MOVWP"] = l64Enc{format: l64Firr14, op: 0x25 << 24} // stptr.w
|
||||
l64InstrTable["MOVVP"] = l64Enc{format: l64Firr14, op: 0x27 << 24} // stptr.d
|
||||
l64InstrTable["SC"] = l64Enc{format: l64Firr14, op: 0x21 << 24} // sc.w
|
||||
l64InstrTable["SCW"] = l64Enc{format: l64Firr14, op: 0x21 << 24} // sc.w
|
||||
l64InstrTable["SCV"] = l64Enc{format: l64Firr14, op: 0x23 << 24} // sc.d
|
||||
l64InstrTable["LL"] = l64Enc{format: l64Firr14, op: 0x20 << 24} // ldptr.w (ll.w)
|
||||
l64InstrTable["LLW"] = l64Enc{format: l64Firr14, op: 0x20 << 24} // ldptr.w (ll.w)
|
||||
l64InstrTable["LLV"] = l64Enc{format: l64Firr14, op: 0x22 << 24} // ldptr.d (ll.d)
|
||||
|
||||
// 2RI20.
|
||||
l64InstrTable["LU12IW"] = l64Enc{format: l64Fir20, op: 0x0a << 25}
|
||||
l64InstrTable["LU32ID"] = l64Enc{format: l64Fir20, op: 0x0b << 25}
|
||||
l64InstrTable["PCALAU12I"] = l64Enc{format: l64Fir20, op: 0x0d << 25}
|
||||
l64InstrTable["PCADDU12I"] = l64Enc{format: l64Fir20, op: 0x0e << 25}
|
||||
// LUI is the Plan 9 spelling of lu12i.w.
|
||||
l64InstrTable["LUI"] = l64Enc{format: l64Fir20, op: 0x0a << 25}
|
||||
|
||||
// 4R — fused multiply-add.
|
||||
rrrr := map[string]uint32{
|
||||
"FMADDF": 0x81 << 20, "FMADDD": 0x82 << 20,
|
||||
"FMSUBF": 0x85 << 20, "FMSUBD": 0x86 << 20,
|
||||
"FNMADDF": 0x89 << 20, "FNMADDD": 0x8a << 20,
|
||||
"FNMSUBF": 0x8d << 20, "FNMSUBD": 0x8e << 20,
|
||||
}
|
||||
for m, op := range rrrr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frrrr, op: op}
|
||||
}
|
||||
|
||||
// IRIR — bit-field insert/extract.
|
||||
irir := map[string]uint32{
|
||||
"BSTRINSW": 0x3<<21 | 0x0<<15,
|
||||
"BSTRINSV": 0x2 << 22,
|
||||
"BSTRPICKW": 0x3<<21 | 0x1<<15,
|
||||
"BSTRPICKV": 0x3 << 22,
|
||||
}
|
||||
for m, op := range irir {
|
||||
l64InstrTable[m] = l64Enc{format: l64Firir, op: op}
|
||||
}
|
||||
|
||||
// 3RI2 — ALSL.
|
||||
irrr := map[string]uint32{
|
||||
"ALSLW": 0x2 << 17, "ALSLWU": 0x3 << 17, "ALSLV": 0x16 << 17,
|
||||
}
|
||||
for m, op := range irrr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Firrr, op: op}
|
||||
}
|
||||
|
||||
// 0-operand system instructions.
|
||||
l64InstrTable["SYSCALL"] = l64Enc{format: l64Fi15, op: 0x56 << 15}
|
||||
l64InstrTable["BREAK"] = l64Enc{format: l64Fi15, op: 0x54 << 15}
|
||||
l64InstrTable["DBAR"] = l64Enc{format: l64Fi15, op: 0x70e4 << 15}
|
||||
|
||||
// PRELD.
|
||||
l64InstrTable["PRELD"] = l64Enc{format: l64Fpreld, op: 0x0ab << 22}
|
||||
|
||||
// Atomics — 3R with the AM field order (rk=value, rj=address, rd=result).
|
||||
am := map[string]uint32{
|
||||
"AMSWAPB": 0x070B8 << 15, "AMSWAPH": 0x070B9 << 15,
|
||||
"AMSWAPW": 0x070C0 << 15, "AMSWAPV": 0x070C1 << 15,
|
||||
"AMCASB": 0x070B0 << 15, "AMCASH": 0x070B1 << 15,
|
||||
"AMCASW": 0x070B2 << 15, "AMCASV": 0x070B3 << 15,
|
||||
"AMADDW": 0x070C2 << 15, "AMADDV": 0x070C3 << 15,
|
||||
"AMANDW": 0x070C4 << 15, "AMANDV": 0x070C5 << 15,
|
||||
"AMORW": 0x070C6 << 15, "AMORV": 0x070C7 << 15,
|
||||
"AMXORW": 0x070C8 << 15, "AMXORV": 0x070C9 << 15,
|
||||
"AMMAXW": 0x070CA << 15, "AMMAXV": 0x070CB << 15,
|
||||
"AMMINW": 0x070CC << 15, "AMMINV": 0x070CD << 15,
|
||||
"AMMAXWU": 0x070CE << 15, "AMMAXVU": 0x070CF << 15,
|
||||
"AMMINWU": 0x070D0 << 15, "AMMINVU": 0x070D1 << 15,
|
||||
"AMSWAPDBB": 0x070BC << 15, "AMSWAPDBH": 0x070BD << 15,
|
||||
"AMSWAPDBW": 0x070D2 << 15, "AMSWAPDBV": 0x070D3 << 15,
|
||||
"AMCASDBB": 0x070B4 << 15, "AMCASDBH": 0x070B5 << 15,
|
||||
"AMCASDBW": 0x070B6 << 15, "AMCASDBV": 0x070B7 << 15,
|
||||
}
|
||||
for m, op := range am {
|
||||
l64InstrTable[m] = l64Enc{format: l64Fam, op: op}
|
||||
}
|
||||
}
|
||||
|
||||
// l64FpMovTable maps (mnemonic, from-class, to-class) to the 2R opcode of the
|
||||
// register move between the integer and floating-point register banks — the
|
||||
// MOVW/MOVV specials the Go assembler accepts.
|
||||
var l64FpMovTable = map[string]uint32{
|
||||
"MOVV.R.F": 0x452a << 10, // movgr2fr.d
|
||||
"MOVV.R.FCC": 0x4536 << 10, // movgr2cf
|
||||
"MOVV.R.FCSR": 0x4530 << 10, // movgr2fcsr
|
||||
"MOVV.F.R": 0x452e << 10, // movfr2gr.d
|
||||
"MOVV.F.FCC": 0x4534 << 10, // movfr2cf
|
||||
"MOVV.FCC.R": 0x4537 << 10, // movcf2gr
|
||||
"MOVV.FCC.F": 0x4535 << 10, // movcf2fr
|
||||
"MOVV.FCSR.R": 0x4532 << 10, // movfcsr2gr
|
||||
"MOVW.R.F": 0x4529 << 10, // movgr2fr.w
|
||||
"MOVW.F.R": 0x452d << 10, // movfr2gr.s
|
||||
}
|
||||
|
||||
// l64branchTable holds the 16-bit branch and jump encodings (2RI16).
|
||||
var l64branchTable = map[string]uint32{
|
||||
"BEQ": 0x16 << 26,
|
||||
"BNE": 0x17 << 26,
|
||||
"BLT": 0x18 << 26,
|
||||
"BGE": 0x19 << 26,
|
||||
"BLTU": 0x1a << 26,
|
||||
"BGEU": 0x1b << 26,
|
||||
"JIRL": 0x13 << 26,
|
||||
}
|
||||
|
||||
// l64branch21Table holds the single-register branches with 21-bit offsets:
|
||||
// the negative opcode constants the toolchain uses for the short forms.
|
||||
var l64branch21Table = map[string]uint32{
|
||||
"BEQZ": 0x10 << 26, // beq r0, rj → beqz
|
||||
"BNEZ": 0x11 << 26, // bne r0, rj → bnez
|
||||
"BLTZ": 0x18 << 26, // blt rj, r0 → bltz
|
||||
"BGEZ": 0x19 << 26, // bge rj, r0 → bgez
|
||||
"BGTZ": 0x18 << 26, // blt r0, rj → bgtz
|
||||
"BLEZ": 0x19 << 26, // bge r0, rj → blez
|
||||
"BFPT": 0x12<<26 | 0x1<<8,
|
||||
"BFPF": 0x12<<26 | 0x0<<8,
|
||||
}
|
||||
|
||||
// l64jumpTable maps the jump pseudo-instructions and their aliases to the
|
||||
// B/BL opcode constants.
|
||||
var l64jumpTable = map[string]uint32{
|
||||
"JMP": 0x14 << 26, // b
|
||||
"B": 0x14 << 26, // b
|
||||
"JAL": 0x15 << 26, // bl
|
||||
"CALL": 0x15 << 26, // bl
|
||||
"BL": 0x15 << 26, // bl
|
||||
}
|
||||
|
||||
// l64loadStoreTable maps the MOV width mnemonics to their load and store
|
||||
// 2RI12 opcodes. The load opcode is the negated store opcode, exactly as
|
||||
// the toolchain derives it.
|
||||
var l64loadStoreTable = map[string]struct{ ld, st uint32 }{
|
||||
"MOVB": {0x0a0 << 22, 0x0a4 << 22},
|
||||
"MOVH": {0x0a1 << 22, 0x0a5 << 22},
|
||||
"MOVW": {0x0a2 << 22, 0x0a6 << 22},
|
||||
"MOVV": {0x0a3 << 22, 0x0a7 << 22},
|
||||
"MOVBU": {0x0a8 << 22, 0x0a4 << 22},
|
||||
"MOVHU": {0x0a9 << 22, 0x0a5 << 22},
|
||||
"MOVWU": {0x0aa << 22, 0x0a6 << 22},
|
||||
"MOVF": {0x0ac << 22, 0x0ad << 22},
|
||||
"MOVD": {0x0ae << 22, 0x0af << 22},
|
||||
}
|
||||
|
||||
// l64movRegTable maps a register-to-register MOV mnemonic to its expansion,
|
||||
// matching the toolchain's case-1 encoding: MOVB → ext.w.b, MOVH → ext.w.h,
|
||||
// MOVW → sll.w, MOVV → or, MOVBU → andi. MOVHU/MOVWU expand to bstrpick.d
|
||||
// and are handled separately in the assembler.
|
||||
type l64MovRegEnc struct {
|
||||
rr bool // 2R format (ext.w.b/ext.w.h)
|
||||
op uint32 // opcode constant (rr forms) or 3R/2RI12 opcode
|
||||
imm int // 2RI12 immediate for MOVBU's andi
|
||||
}
|
||||
|
||||
var l64movRegTable = map[string]l64MovRegEnc{
|
||||
"MOVB": {true, 0x17 << 10, 0}, // ext.w.b rd, rj
|
||||
"MOVH": {true, 0x16 << 10, 0}, // ext.w.h rd, rj
|
||||
"MOVW": {false, 0x2e << 15, 0}, // sll.w rd, rj, r0
|
||||
"MOVV": {false, 0x2a << 15, 0}, // or rd, rj, r0
|
||||
"MOVBU": {false, 0x00d << 22, 0xff}, // andi rd, rj, $0xff
|
||||
}
|
||||
|
||||
// l64movFpRegTable maps a floating-point register move mnemonic to its 2R
|
||||
// opcode (fmov.s / fmov.d), used when both operands are F registers.
|
||||
var l64movFpRegTable = map[string]uint32{
|
||||
"MOVF": 0x4525 << 10,
|
||||
"MOVD": 0x4526 << 10,
|
||||
}
|
||||
|
||||
// l64RegClass discriminates integer (R), floating-point (F) and condition
|
||||
// (FCC) registers for the MOV pseudo-instruction's register-move encoding.
|
||||
type l64RegClass int
|
||||
|
||||
const (
|
||||
l64ClsNone l64RegClass = iota
|
||||
l64ClsGR
|
||||
l64ClsFP
|
||||
l64ClsFCC
|
||||
l64ClsFCSR
|
||||
)
|
||||
|
||||
// loong64RegClass reports the register class of a register operand name.
|
||||
func loong64RegClass(name string) l64RegClass {
|
||||
switch {
|
||||
case name == "":
|
||||
return l64ClsNone
|
||||
case len(name) >= 3 && name[:3] == "FCC":
|
||||
return l64ClsFCC
|
||||
case len(name) >= 4 && name[:4] == "FCSR":
|
||||
return l64ClsFCSR
|
||||
case name[0] == 'F':
|
||||
return l64ClsFP
|
||||
default:
|
||||
return l64ClsGR
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
|
||||
func firstTextLOONG64(t *testing.T, src string) *ast.Text {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("f_loong64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
if fn, ok := d.(*ast.Text); ok {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
t.Fatal("no TEXT found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// assembleLOONG64Helper assembles one TEXT function and returns its bytes.
|
||||
func assembleLOONG64Helper(t *testing.T, fn *ast.Text) []byte {
|
||||
t.Helper()
|
||||
code, _, _, _, _, err := assembleLOONG64(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// wantWords checks that code matches the expected little-endian words.
|
||||
func wantWords(t *testing.T, code []byte, want ...uint32) {
|
||||
t.Helper()
|
||||
got := make([]uint32, 0, len(code)/4)
|
||||
for i := 0; i+4 <= len(code); i += 4 {
|
||||
got = append(got, binary.LittleEndian.Uint32(code[i:]))
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("word count = %d, want %d\ncode: % x", len(got), len(want), code)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOONG64_add(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
// 5 instructions: two ld.d, add.d, st.d, jirl r0, r1, 0.
|
||||
wantWords(t, code,
|
||||
0x28C02064, // ld.d r4, 8(r3)
|
||||
0x28C04065, // ld.d r5, 16(r3)
|
||||
0x00109484, // add.d r4, r4, r5
|
||||
0x29C06064, // st.d r4, 24(r3)
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_arithmetic(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·arith(SB), NOSPLIT, $0
|
||||
ADDV R4, R5, R6
|
||||
SUBV R7, R8, R9
|
||||
MULV R10, R11, R12
|
||||
DIVV R13, R14, R15
|
||||
AND R16, R17, R18
|
||||
OR R18, R19, R20
|
||||
XOR R20, R21, R2
|
||||
SLLV R2, R23, R24
|
||||
SRLV R24, R25, R26
|
||||
SRAV R26, R27, R28
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x001090A6, // add.d r6, r5, r4
|
||||
0x00119D09, // sub.d r9, r8, r7
|
||||
0x001DA96C, // mul.d r12, r11, r10
|
||||
0x002235CF, // div.d r15, r14, r13
|
||||
0x0014C232, // and r18, r17, r16
|
||||
0x00154A74, // or r20, r19, r18
|
||||
0x0015D2A2, // xor r2, r21, r20
|
||||
0x00188AF8, // sll.d r24, r23, r2
|
||||
0x0019633A, // srl.d r26, r25, r24
|
||||
0x0019EB7C, // sra.d r28, r27, r26
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_immediates(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·imm(SB), NOSPLIT, $0
|
||||
ADDV $42, R4, R5
|
||||
ADDV $-8, R6
|
||||
AND $0xff, R7, R8
|
||||
OR $1, R9, R10
|
||||
SGT $100, R13, R14
|
||||
SLLV $4, R15, R16
|
||||
MOVV $0x12345, R17
|
||||
MOVV $0, R18
|
||||
MOVW $0, R19
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x02C0A885, // addi.d r5, r4, 42
|
||||
0x02FFE0C6, // addi.d r6, r6, -8
|
||||
0x0343FCE8, // andi r8, r7, 0xff
|
||||
0x0380052A, // ori r10, r9, 1
|
||||
0x020191AE, // slti r14, r13, 100
|
||||
0x004111F0, // slli.d r16, r15, 4
|
||||
0x14000251, // lu12i.w r17, 0x12
|
||||
0x038D1631, // ori r17, r17, 0x345
|
||||
0x00150012, // or r18, r0, r0
|
||||
0x00170013, // sll.w r19, r0, r0
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_loadStore(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·mem(SB), NOSPLIT, $0
|
||||
MOVV (R4), R5
|
||||
MOVV R5, (R6)
|
||||
MOVW 8(R7), R8
|
||||
MOVB R9, -4(R10)
|
||||
MOVV (R11)(R12), R13
|
||||
MOVV R14, (R15)(R16)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x28C00085, // ld.d r5, 0(r4)
|
||||
0x29C000C5, // st.d r5, 0(r6)
|
||||
0x288020E8, // ld.w r8, 8(r7)
|
||||
0x293FF149, // st.b r9, -4(r10)
|
||||
0x380C316D, // ldx.d r13, r11, r12
|
||||
0x381C41EE, // stx.d r14, r15, r16
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_branches(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·br(SB), NOSPLIT, $0
|
||||
BEQ R4, R5, done
|
||||
BNE R6, R7, skip
|
||||
BLT R8, R9, done
|
||||
BGE R10, R11, done
|
||||
BLTU R12, R13, done
|
||||
BGEU R14, R15, done
|
||||
skip:
|
||||
JMP done
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
// skip is at 0x18 (6 words), done at 0x1c.
|
||||
wantWords(t, code,
|
||||
0x58001C85, // beq r5, r4, +7
|
||||
0x5C0018C7, // bne r7, r6, +6
|
||||
0x60001509, // blt r9, r8, +5
|
||||
0x6400114B, // bge r11, r10, +4
|
||||
0x68000D8D, // bltu r13, r12, +3
|
||||
0x6C0009CF, // bgeu r15, r14, +2
|
||||
0x50000400, // b done (+1, chain-folded through skip)
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_frame(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $32-8
|
||||
MOVV R4, R5
|
||||
MOVV arg+0(FP), R6
|
||||
MOVV R7, local-8(SP)
|
||||
MOVV local-8(SP), R8
|
||||
MOVV R9, ret+0(FP)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
// autosize = align8(32+8) = 40; prologue stores LR at -40(SP),
|
||||
// opens the frame, stores LR again at 0(SP). The function is a leaf
|
||||
// (no calls), so the epilogue skips the LR restore. FP args are at
|
||||
// autosize+8; SP locals at autosize+offset.
|
||||
wantWords(t, code,
|
||||
0x29FF6061, // st.d r1, -40(r3)
|
||||
0x02FF6063, // addi.d r3, r3, -40
|
||||
0x29C00061, // st.d r1, 0(r3)
|
||||
0x00150085, // or r5, r4, r0
|
||||
0x28C0C066, // ld.d r6, 48(r3) arg+0(FP) → 0+40+8
|
||||
0x29C08067, // st.d r7, 32(r3) local-8(SP) → 40-8
|
||||
0x28C08068, // ld.d r8, 32(r3)
|
||||
0x29C0C069, // st.d r9, 48(r3) ret+0(FP) → 0+40+8
|
||||
0x02C0A063, // addi.d r3, r3, 40
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_jumpChain(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·jc(SB), NOSPLIT, $0
|
||||
JMP a
|
||||
a:
|
||||
JMP b
|
||||
b:
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x50000800, // b +2 (a, chain-folded to b)
|
||||
0x50000400, // b +1 (b)
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
func TestLOONG64_dconClasses(t *testing.T) {
|
||||
cases := []struct {
|
||||
v int64
|
||||
word int // expected word count
|
||||
}{
|
||||
{0x123456789, 3}, // lu12i.w + ori + lu32i.d
|
||||
{-1, 2}, // addi.d + lu52i.d (the MOV path handles -1 earlier)
|
||||
{0x1000000000000, 2}, // addi.w + lu32i.d
|
||||
{0x123456789abcdef0, 4}, // full sequence
|
||||
{0xFFFFFFFFF, 2}, // lu12i.w + ori
|
||||
{0x1234567800000000, 3}, // addi.w + lu32i.d + lu52i.d
|
||||
}
|
||||
for _, c := range cases {
|
||||
if n := len(l64DconMovWords(0, c.v)); n != c.word {
|
||||
t.Errorf("0x%x: %d words, want %d", c.v, n, c.word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOONG64_regNames(t *testing.T) {
|
||||
cases := map[string]int{
|
||||
"R0": 0, "R31": 31, "F0": 0, "F31": 31, "FCC0": 0, "FCC7": 7,
|
||||
"FCSR0": 0, "FCSR3": 3, "ZERO": 0, "RA": 1, "SP": 3, "g": 22, "G": 22,
|
||||
"R32": -1, "FCC8": -1, "X0": -1, "R": -1, "TMP": 30, "CTXT": 29,
|
||||
}
|
||||
for name, want := range cases {
|
||||
if got := loong64RegNum(name); got != want {
|
||||
t.Errorf("loong64RegNum(%q) = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOONG64_bytesEqualGroundTruth(t *testing.T) {
|
||||
// A spot-check that assembleLOONG64 emits the same bytes the Go
|
||||
// toolchain does for a small kernel (the full comparison lives in
|
||||
// verify's TestGroundTruthLOONG64).
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·k(SB), NOSPLIT, $0-0
|
||||
ADDV R4, R5, R6
|
||||
MOVV $0x100000, R7
|
||||
BEQ R6, R7, done
|
||||
JMP done
|
||||
done:
|
||||
RET
|
||||
`
|
||||
fn := firstTextLOONG64(t, src)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
want := []byte{
|
||||
0xa6, 0x90, 0x10, 0x00, // add.d r6, r5, r4
|
||||
0x07, 0x20, 0x00, 0x14, // lu12i.w r7, 0x100
|
||||
0xc7, 0x08, 0x00, 0x58, // beq r7, r6, +2 (done)
|
||||
0x00, 0x04, 0x00, 0x50, // b +1 (done)
|
||||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||||
}
|
||||
if !bytes.Equal(code, want) {
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
||||
//
|
||||
// Go's loong64 functions have no frame pointer: FP and SP are synthetic
|
||||
// registers resolved against the hardware stack pointer (R3) and the frame
|
||||
// size. The return address lives in R1 (the link register).
|
||||
//
|
||||
// The autosize is the real stack adjustment: the declared local frame plus
|
||||
// the 8 bytes for the saved link register, rounded up to a multiple of 8
|
||||
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
|
||||
// A leaf function (no calls) with a zero frame gets no prologue at all.
|
||||
//
|
||||
// Prologue (autosize > 0), byte-identical to the toolchain:
|
||||
//
|
||||
// MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe)
|
||||
// ADDV $-autosize, R3 // open the frame
|
||||
// MOVV R1, 0(R3) // save LR again at SP (signal-safety)
|
||||
//
|
||||
// Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR
|
||||
// restore); the RET's jirl r0, r1, 0 follows.
|
||||
|
||||
// loong64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||
type loong64FrameInfo struct {
|
||||
autosize int // the real SP adjustment (locals + saved LR, aligned)
|
||||
frame int // the declared $framesize
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
leaf bool // no call instructions in the body
|
||||
}
|
||||
|
||||
// loong64ComputeFrame derives the frame layout for a TEXT function.
|
||||
func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
|
||||
fi := loong64FrameInfo{
|
||||
frame: frameSize(t),
|
||||
args: argsSize(t),
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
if f == "NOSPLIT" {
|
||||
fi.noSplit = true
|
||||
}
|
||||
}
|
||||
fi.leaf = loong64IsLeaf(t)
|
||||
if fi.frame != 0 {
|
||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||
if fi.autosize&4 != 0 {
|
||||
fi.autosize += 4
|
||||
}
|
||||
} else if !fi.leaf {
|
||||
// A zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||
fi.autosize = 8
|
||||
}
|
||||
return fi
|
||||
}
|
||||
|
||||
// loong64IsLeaf reports whether a function contains no call instructions
|
||||
// (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame
|
||||
// and the epilogue shape.
|
||||
func loong64IsLeaf(t *ast.Text) bool {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "JAL", "CALL", "BL":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// loong64Prologue returns the prologue bytes for a loong64 function.
|
||||
func loong64Prologue(fi loong64FrameInfo) []byte {
|
||||
if fi.autosize == 0 {
|
||||
return nil
|
||||
}
|
||||
addiD := l64DualTable["ADDV"].imm
|
||||
return l64WordsLE(
|
||||
l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3)
|
||||
l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3
|
||||
l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3)
|
||||
)
|
||||
}
|
||||
|
||||
// loong64Return returns the bytes for a RET: the epilogue (restore LR and
|
||||
// deallocate the frame when present) followed by jirl r0, r1, 0.
|
||||
func loong64Return(fi loong64FrameInfo) []byte {
|
||||
var ws []uint32
|
||||
if fi.autosize != 0 {
|
||||
if !fi.leaf {
|
||||
// MOVV 0(R3), R1 — restore the link register.
|
||||
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1))
|
||||
}
|
||||
// ADDV $autosize, R3 — close the frame.
|
||||
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3))
|
||||
}
|
||||
// jirl r0, r1, 0 — return.
|
||||
ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0))
|
||||
return l64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// loong64ResolvePseudo translates a pseudo-register memory reference into a
|
||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||
// x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable
|
||||
// reference (SB: static data, handled by the relocation path).
|
||||
func loong64ResolvePseudo(sym *ast.Symbol, fi loong64FrameInfo) (base int, off int32) {
|
||||
if sym == nil {
|
||||
return -1, 0
|
||||
}
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
return 3, int32(sym.Offset) + int32(fi.autosize) + 8
|
||||
case "SP":
|
||||
return 3, int32(fi.autosize) + int32(sym.Offset)
|
||||
case "SB":
|
||||
return -1, int32(sym.Offset)
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||||
// bare-data pseudo-instructions. The words match `go tool asm`
|
||||
// (GOARCH=loong64) for the same source.
|
||||
func TestLOONG64_sys(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·sys(SB), NOSPLIT, $0
|
||||
NOOP
|
||||
UNDEF
|
||||
WORD $0x12345678
|
||||
SYSCALL $0x10
|
||||
BREAK $0x20
|
||||
DBAR $1
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x03400000, // andi r0, r0, 0 (NOOP)
|
||||
0x002A0000, // break 0 (UNDEF)
|
||||
0x12345678, // WORD
|
||||
0x002B0010, // syscall 0x10
|
||||
0x002A0020, // break 0x20
|
||||
0x38720001, // dbar 1
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_branches21 exercises the single-register branch forms: the
|
||||
// 21-bit BEQZ/BNEZ/BLTZ/BGEZ and the rd-field BGTZ/BLEZ.
|
||||
func TestLOONG64_branches21(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·b21(SB), NOSPLIT, $0
|
||||
BEQZ R4, done
|
||||
BNEZ R5, done
|
||||
BLTZ R6, done
|
||||
BGEZ R7, done
|
||||
BGTZ R8, done
|
||||
BLEZ R9, done
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x40001880, // beqz r4, +6
|
||||
0x440014A0, // bnez r5, +5
|
||||
0x600010C0, // bltz r6, +4
|
||||
0x64000CE0, // bgez r7, +3
|
||||
0x60000808, // bgtz r8, +2 (register in the rd field)
|
||||
0x64000409, // blez r9, +1
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_fma exercises the four fused multiply-add forms (4 and 3
|
||||
// operand spellings).
|
||||
func TestLOONG64_fma(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·fma(SB), NOSPLIT, $0
|
||||
FMADDD F0, F1, F2, F3
|
||||
FMSUBD F4, F5, F6
|
||||
FNMADDD F7, F8, F9, F10
|
||||
FNMSUBD F11, F12, F13
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x08200443, // fmadd.d f3, f2, f1, f0
|
||||
0x086214C6, // fmsub.d f6, f5, f5, f4
|
||||
0x08A3A12A, // fnmadd.d f10, f9, f8, f7
|
||||
0x08E5B1AD, // fnmsub.d f13, f12, f12, f11
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_bitops exercises BSTRINS/BSTRPICK (the 6-bit msb/lsb fields)
|
||||
// and ALSL (the sa−1 shift field).
|
||||
func TestLOONG64_bitops(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·bits(SB), NOSPLIT, $0
|
||||
BSTRINSW $3, R4, $0, R5
|
||||
BSTRINSV $3, R4, $1, R6
|
||||
BSTRPICKW $3, R4, $0, R5
|
||||
BSTRPICKV $6, R7, $0, R8
|
||||
ALSLW $1, R4, R5, R6
|
||||
ALSLW $4, R7, R8, R9
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x00630085, // bstrins.w r5, r4, $3, $0
|
||||
0x00830486, // bstrins.d r6, r4, $3, $1
|
||||
0x00638085, // bstrpick.w r5, r4, $3, $0
|
||||
0x00C600E8, // bstrpick.d r8, r7, $6, $0
|
||||
0x00041486, // alsl.w r6, r5, r4, $1 (sa-1)
|
||||
0x0005A0E9, // alsl.w r9, r8, r7, $4
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_ptr exercises the 14-bit-offset memory forms (LL/SC/MOVWP/
|
||||
// MOVVP with the offset scaled by 4) and PRELD.
|
||||
func TestLOONG64_ptr(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·ptr(SB), NOSPLIT, $0
|
||||
LLW 8(R14), R15
|
||||
SCW R16, -4(R17)
|
||||
MOVWP 16(R18), R19
|
||||
MOVVP R20, 24(R21)
|
||||
PRELD 32(R22), $0
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x200009CF, // ll.w r15, 8(r14)
|
||||
0x21FFFE30, // sc.w r16, -4(r17)
|
||||
0x24001253, // ldptr.w r19, 16(r18)
|
||||
0x27001AB4, // stptr.d r20, 24(r21)
|
||||
0x2AC082C0, // preld 32(r22), 0
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_atomics exercises the AM* read-modify-write forms and
|
||||
// RDTIME, plus the MOVV FP→GP move.
|
||||
func TestLOONG64_atomics(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·atoms(SB), NOSPLIT, $0
|
||||
AMADDW R4, (R5), R6
|
||||
RDTIMED R7, R8
|
||||
MOVV F1, R2
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x386110A6, // amadd.w r6, r5, r4
|
||||
0x000068E8, // rdtime.d r8, r7
|
||||
0x0114B822, // movfr2gr.d r2, f1
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_lu52 exercises the LU52I.D immediate form (a gasm extension
|
||||
// the toolchain reaches only through its MOVV expansion).
|
||||
func TestLOONG64_lu52(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·lu52(SB), NOSPLIT, $0
|
||||
LU52ID $0x345, R10
|
||||
LU52ID $0x123, R11, R12
|
||||
ADDV16 $0x10000, R13
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x030D154A, // lu52i.d r10, r10, 0x345
|
||||
0x03048D6C, // lu52i.d r12, r11, 0x123
|
||||
0x100005AD, // addu16i.d r13, r13, 0x10000>>16
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_sbRefs checks the static-symbol reference forms through the
|
||||
// full file assembly: each pcalau12i+addi.d/ld/st pair carries the
|
||||
// R_LOONG64_ADDR_HI/LO relocation pair, and the immediate fields are left
|
||||
// zero for the linker.
|
||||
func TestLOONG64_sbRefs(t *testing.T) {
|
||||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||||
TEXT ·sb(SB), NOSPLIT, $0
|
||||
MOVV $·table(SB), R4
|
||||
MOVV ·table+8(SB), R5
|
||||
MOVV R6, ·table(SB)
|
||||
RET
|
||||
|
||||
GLOBL ·table(SB), RODATA, $8
|
||||
DATA ·table+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 28 {
|
||||
t.Fatalf("function size = %d, want 28", fn.Size)
|
||||
}
|
||||
var hi, lo int
|
||||
// The three references: $·table (0), ·table+8 (8), ·table (0).
|
||||
wantAdd := []int64{0, 0, 8, 8, 0, 0}
|
||||
for i, r := range fn.Relocs {
|
||||
wantKind := RelLoong64AddrHi
|
||||
wantOff := (i / 2) * 8
|
||||
if i%2 == 1 {
|
||||
wantKind = RelLoong64AddrLo
|
||||
wantOff += 4
|
||||
}
|
||||
if r.Kind != wantKind || r.Off != wantOff || r.Name != "table" || r.Addend != wantAdd[i] {
|
||||
t.Errorf("reloc %d = {kind %v off %d name %q addend %d}", i, r.Kind, r.Off, r.Name, r.Addend)
|
||||
}
|
||||
if r.Kind == RelLoong64AddrHi {
|
||||
hi++
|
||||
} else {
|
||||
lo++
|
||||
}
|
||||
}
|
||||
if hi != 3 || lo != 3 {
|
||||
t.Errorf("relocs = %d hi + %d lo, want 3 + 3", hi, lo)
|
||||
}
|
||||
// The image carries the zero-immediate pair encodings (the linker
|
||||
// fills the immediate fields from the relocations).
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
wantWords(t, code,
|
||||
0x1A000004, // pcalau12i r4, 0
|
||||
0x02C00084, // addi.d r4, r4, 0
|
||||
0x1A00001E, // pcalau12i r30, 0
|
||||
0x28C003C5, // ld.d r5, 0(r30)
|
||||
0x1A00001E, // pcalau12i r30, 0
|
||||
0x29C003C6, // st.d r6, 0(r30)
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_errors checks the encoder's error paths: undefined labels,
|
||||
// invalid register operands and operand-count mismatches.
|
||||
func TestLOONG64_errors(t *testing.T) {
|
||||
cases := []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
JMP nowhere
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BEQZ X0, done
|
||||
done:
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
ADDV R4
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
FMADDD F0, F1
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
AMADDW R4, R5
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
WORD
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
PRELD 32(R4)
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
ALSLW $5, R4, R5, R6
|
||||
RET
|
||||
`,
|
||||
}
|
||||
for i, src := range cases {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
||||
// the prologue raises the SP delta by autosize (in effect from the third
|
||||
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
||||
// in MinLC (4) units — byte-identical to `go tool asm`.
|
||||
func TestLOONG64_pcsp(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
"leaf",
|
||||
`#include "textflag.h"
|
||||
TEXT ·leaf(SB), NOSPLIT, $8-0
|
||||
MOVV R4, R5
|
||||
RET
|
||||
`,
|
||||
[]byte{0x02, 0x02, 0x20, 0x03, 0x1f, 0x01, 0x00},
|
||||
},
|
||||
{
|
||||
"nonleaf",
|
||||
`#include "textflag.h"
|
||||
TEXT ·nonleaf(SB), NOSPLIT, $8-0
|
||||
MOVV R4, R5
|
||||
JAL (R12)
|
||||
RET
|
||||
`,
|
||||
[]byte{0x02, 0x02, 0x20, 0x05, 0x1f, 0x01, 0x00},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
f, errs := parser.Parse("pcsp_loong64.s", c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
if got := pcspTable(img.Funcs[0], 4); !bytes.Equal(got, c.want) {
|
||||
t.Errorf("pcsp = % x, want % x", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_sbRefsUndefined checks that a reference to a symbol no GLOBL
|
||||
// defines assembles into a relocation and is rejected at object emission.
|
||||
func TestLOONG64_sbRefsUndefined(t *testing.T) {
|
||||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||||
TEXT ·sb(SB), NOSPLIT, $0
|
||||
MOVV missing(SB), R4
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
if len(img.Funcs[0].Relocs) != 2 {
|
||||
t.Fatalf("relocs = %d, want the HI/LO pair", len(img.Funcs[0].Relocs))
|
||||
}
|
||||
if _, err := img.GOObjectLOONG64("p", "sb_loong64.s"); err == nil {
|
||||
t.Error("expected an unknown-symbol error at emission")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_movImmToFp checks the immediate-to-FP move forms.
|
||||
func TestLOONG64_movImmToFp(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·fpmov(SB), NOSPLIT, $0
|
||||
MOVV $0x1, F0
|
||||
MOVW $0x2, F4
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
want := []byte{
|
||||
0x00, 0x04, 0x80, 0x03, // ori f0, r0, 1
|
||||
0x04, 0x08, 0x80, 0x03, // ori f4, r0, 2
|
||||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||||
}
|
||||
if !bytes.Equal(code, want) {
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
}
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// This file emits Mach-O x86-64 objects (MH_OBJECT) from an assembled
|
||||
// Image, in the shape the Darwin assembler produces: one unnamed segment
|
||||
// carrying a __TEXT,__text and a __DATA,__data section laid out back to
|
||||
// back at addresses zero and len(code), a symbol table (locals first, then
|
||||
// exported definitions, then undefined externals) and one relocation entry
|
||||
// per static-symbol reference, of type X86_64_RELOC_SIGNED.
|
||||
//
|
||||
// The image's own address space carries straight over — the data section
|
||||
// starts immediately after the code, and the layout padding already lives
|
||||
// inside Image.Data — so every symbol keeps its image address as its
|
||||
// n_value, and a local (non-external) relocation leaves the displacement
|
||||
// the assembler resolved in place: the linker only adjusts it by the
|
||||
// section's final movement.
|
||||
|
||||
// Mach-O constants.
|
||||
const (
|
||||
machoMagic64 = 0xfeedfacf
|
||||
machoCPUamd64 = 0x01000007 // CPU_TYPE_X86_64
|
||||
machoCPUSubAll = 3 // CPU_SUBTYPE_X86_64_ALL
|
||||
machoObj = 1 // MH_OBJECT
|
||||
|
||||
machoSegment64 = 0x19 // LC_SEGMENT_64
|
||||
machoSymtab = 0x2 // LC_SYMTAB
|
||||
|
||||
machoSectTextFlags = 0x80000400 // S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS
|
||||
|
||||
nUndf = 0x00 // undefined symbol
|
||||
nSect = 0x0e // defined in section number n_sect
|
||||
nExt = 0x01 // external (exported or undefined-global) bit
|
||||
|
||||
x8664RelocSigned = 1
|
||||
)
|
||||
|
||||
// MachOObject returns the image as a Mach-O x86-64 relocatable object
|
||||
// (MH_OBJECT), the shape the Darwin toolchain links. Symbol names follow
|
||||
// the same rules as the ELF output. Every static-symbol reference becomes
|
||||
// an X86_64_RELOC_SIGNED relocation: external references against their
|
||||
// undefined symbol, file-local ones against the __DATA section with the
|
||||
// resolved displacement carried in the instruction bytes.
|
||||
func (img *Image) MachOObject() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
// Section ordinals (1-based, as Mach-O numbers them).
|
||||
const (
|
||||
sectText = 1
|
||||
sectData = 2
|
||||
)
|
||||
|
||||
// Object address space: code at 0, data immediately after (the layout
|
||||
// padding is already part of img.Data, so image addresses are object
|
||||
// addresses).
|
||||
textAddr := uint64(0)
|
||||
dataAddr := uint64(len(img.Code))
|
||||
vmsize := dataAddr + uint64(len(img.Data))
|
||||
|
||||
// The code, with external displacements primed to addend − 4: the
|
||||
// linker adds the symbol's address to the field as it stands. Local
|
||||
// displacements stay as the assembler resolved them.
|
||||
code := append([]byte(nil), img.Code...)
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
if r.External {
|
||||
// Prime the field to the addend measured from the patch
|
||||
// site: the assembler records it from the instruction end,
|
||||
// After − Off bytes past the field.
|
||||
copy(code[fn.Offset+r.Off:], le32(r.Addend-int64(r.After-r.Off)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Symbols: locals first, then exported definitions, then undefined
|
||||
// externals — the order the classic link editor expects.
|
||||
type machoSym struct {
|
||||
name string
|
||||
typ byte
|
||||
sect byte
|
||||
value uint64
|
||||
}
|
||||
var locals, globals, undefs []machoSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := machoSym{name: objectName(fn.Pkg, fn.Name), typ: nSect, sect: sectText, value: textAddr + uint64(fn.Offset)}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.typ |= nExt
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := machoSym{name: objectName(d.Pkg, d.Name), typ: nSect, sect: sectData, value: dataAddr + uint64(d.Offset)}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.typ |= nExt
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
undefs = append(undefs, machoSym{name: name, typ: nUndf | nExt})
|
||||
}
|
||||
syms := append(append(locals, globals...), undefs...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Relocations, attached to the __text section.
|
||||
type machoReloc struct {
|
||||
addr uint32
|
||||
symnum uint32
|
||||
extern bool
|
||||
}
|
||||
var relocs []machoReloc
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
rel := machoReloc{addr: uint32(fn.Offset + r.Off)}
|
||||
if r.External {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
rel.symnum = uint32(idx)
|
||||
rel.extern = true
|
||||
} else {
|
||||
// Section-relative: r_symbolnum carries the section number
|
||||
// and the resolved displacement stays in the bytes.
|
||||
rel.symnum = sectData
|
||||
}
|
||||
relocs = append(relocs, rel)
|
||||
}
|
||||
}
|
||||
|
||||
// The string table opens with the conventional " \0".
|
||||
strtab := []byte{' ', 0}
|
||||
strOff := map[string]int{}
|
||||
for _, s := range syms {
|
||||
if _, ok := strOff[s.name]; ok {
|
||||
continue
|
||||
}
|
||||
strOff[s.name] = len(strtab)
|
||||
strtab = append(strtab, s.name...)
|
||||
strtab = append(strtab, 0)
|
||||
}
|
||||
|
||||
// File layout: header, the two load commands, section data (code,
|
||||
// data), the relocation table, the symbol table, the string table.
|
||||
const (
|
||||
hdrSize = 32
|
||||
segCmdSize = 72 + 2*80 // segment command with two sections
|
||||
symCmdSize = 24
|
||||
)
|
||||
sizeofcmds := segCmdSize + symCmdSize
|
||||
dataOff := hdrSize + sizeofcmds
|
||||
reloff := dataOff + len(code) + len(img.Data)
|
||||
symoff := reloff + 8*len(relocs)
|
||||
stroff := symoff + 16*len(syms)
|
||||
|
||||
out := make([]byte, stroff+len(strtab))
|
||||
|
||||
// mach_header_64.
|
||||
le.PutUint32(out[0:], machoMagic64)
|
||||
le.PutUint32(out[4:], machoCPUamd64)
|
||||
le.PutUint32(out[8:], machoCPUSubAll)
|
||||
le.PutUint32(out[12:], machoObj)
|
||||
le.PutUint32(out[16:], 2) // ncmds
|
||||
le.PutUint32(out[20:], uint32(sizeofcmds))
|
||||
le.PutUint32(out[24:], 0) // flags
|
||||
le.PutUint32(out[28:], 0) // reserved
|
||||
|
||||
// LC_SEGMENT_64 with the two sections.
|
||||
p := hdrSize
|
||||
le.PutUint32(out[p:], machoSegment64)
|
||||
le.PutUint32(out[p+4:], segCmdSize)
|
||||
// segname: the empty string, zero-padded to 16 bytes.
|
||||
le.PutUint64(out[p+8:], 0)
|
||||
le.PutUint64(out[p+16:], 0)
|
||||
le.PutUint64(out[p+24:], 0) // vmaddr
|
||||
le.PutUint64(out[p+32:], vmsize)
|
||||
le.PutUint64(out[p+40:], uint64(dataOff))
|
||||
le.PutUint64(out[p+48:], vmsize)
|
||||
le.PutUint32(out[p+56:], 7) // maxprot rwx
|
||||
le.PutUint32(out[p+60:], 7) // initprot rwx
|
||||
le.PutUint32(out[p+64:], 2) // nsects
|
||||
le.PutUint32(out[p+68:], 0) // flags
|
||||
|
||||
// __TEXT,__text
|
||||
s := p + 72
|
||||
copy(out[s:], "__text")
|
||||
copy(out[s+16:], "__TEXT")
|
||||
le.PutUint64(out[s+32:], textAddr)
|
||||
le.PutUint64(out[s+40:], uint64(len(code)))
|
||||
le.PutUint32(out[s+48:], uint32(dataOff))
|
||||
le.PutUint32(out[s+52:], 4) // align 2^4
|
||||
le.PutUint32(out[s+56:], uint32(reloff))
|
||||
le.PutUint32(out[s+60:], uint32(len(relocs)))
|
||||
le.PutUint32(out[s+64:], machoSectTextFlags)
|
||||
|
||||
// __DATA,__data
|
||||
s += 80
|
||||
copy(out[s:], "__data")
|
||||
copy(out[s+16:], "__DATA")
|
||||
le.PutUint64(out[s+32:], dataAddr)
|
||||
le.PutUint64(out[s+40:], uint64(len(img.Data)))
|
||||
le.PutUint32(out[s+48:], uint32(dataOff+len(code)))
|
||||
le.PutUint32(out[s+52:], 4) // align 2^4
|
||||
|
||||
// LC_SYMTAB.
|
||||
p = hdrSize + segCmdSize
|
||||
le.PutUint32(out[p:], machoSymtab)
|
||||
le.PutUint32(out[p+4:], symCmdSize)
|
||||
le.PutUint32(out[p+8:], uint32(symoff))
|
||||
le.PutUint32(out[p+12:], uint32(len(syms)))
|
||||
le.PutUint32(out[p+16:], uint32(stroff))
|
||||
le.PutUint32(out[p+20:], uint32(len(strtab)))
|
||||
|
||||
// Section data.
|
||||
copy(out[dataOff:], code)
|
||||
copy(out[dataOff+len(code):], img.Data)
|
||||
|
||||
// Relocation entries.
|
||||
for i, r := range relocs {
|
||||
e := out[reloff+i*8:]
|
||||
le.PutUint32(e[0:], r.addr)
|
||||
bits := r.symnum & 0x00ffffff
|
||||
bits |= 1 << 24 // r_pcrel
|
||||
bits |= 2 << 25 // r_length = 4 bytes
|
||||
if r.extern {
|
||||
bits |= 1 << 27 // r_extern
|
||||
}
|
||||
bits |= x8664RelocSigned << 28
|
||||
le.PutUint32(e[4:], bits)
|
||||
}
|
||||
|
||||
// nlist_64 entries.
|
||||
for i, s := range syms {
|
||||
e := out[symoff+i*16:]
|
||||
le.PutUint32(e[0:], uint32(strOff[s.name]))
|
||||
e[4] = s.typ
|
||||
e[5] = s.sect
|
||||
le.PutUint16(e[6:], 0) // n_desc
|
||||
le.PutUint64(e[8:], s.value)
|
||||
}
|
||||
|
||||
// String table.
|
||||
copy(out[stroff:], strtab)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/macho"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMachOObject checks the structure of the emitted MH_OBJECT: the two
|
||||
// sections and their addresses, the symbol table (types, sections, values)
|
||||
// and the __text relocation entries, parsed back with debug/macho. No
|
||||
// Darwin toolchain is available on the test hosts, so the check is
|
||||
// structural — the ELF output carries the end-to-end link-and-run proof of
|
||||
// the shared symbol and relocation model.
|
||||
func TestMachOObject(t *testing.T) {
|
||||
img := elfTestImage(t)
|
||||
obj, err := img.MachOObject()
|
||||
if err != nil {
|
||||
t.Fatalf("MachOObject: %v", err)
|
||||
}
|
||||
f, err := macho.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if f.Type != macho.TypeObj {
|
||||
t.Errorf("file type = %v, want MH_OBJECT", f.Type)
|
||||
}
|
||||
if f.Cpu != macho.CpuAmd64 {
|
||||
t.Errorf("cpu = %v, want CpuAmd64", f.Cpu)
|
||||
}
|
||||
|
||||
text := f.Section("__text")
|
||||
data := f.Section("__data")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing __text or __data section")
|
||||
}
|
||||
if text.Addr != 0 || text.Size != uint64(len(img.Code)) {
|
||||
t.Errorf("__text addr/size = %#x/%d, want 0/%d", text.Addr, text.Size, len(img.Code))
|
||||
}
|
||||
if data.Addr != uint64(len(img.Code)) {
|
||||
t.Errorf("__data addr = %#x, want %#x", data.Addr, len(img.Code))
|
||||
}
|
||||
|
||||
// Symbol table: locals, exported definitions, undefined externals.
|
||||
syms := f.Symtab.Syms
|
||||
byName := map[string]macho.Symbol{}
|
||||
for _, s := range syms {
|
||||
byName[s.Name] = s
|
||||
}
|
||||
wantSym := func(name string, typ, sect uint8, value uint64) {
|
||||
t.Helper()
|
||||
s, ok := byName[name]
|
||||
if !ok {
|
||||
t.Errorf("symbol %q not found", name)
|
||||
return
|
||||
}
|
||||
if s.Type != typ || s.Sect != sect || s.Value != value {
|
||||
t.Errorf("%s: type/sect/value = %#x/%d/%#x, want %#x/%d/%#x",
|
||||
name, s.Type, s.Sect, s.Value, typ, sect, value)
|
||||
}
|
||||
}
|
||||
const (
|
||||
defined = nSect | nExt
|
||||
local = nSect
|
||||
undefined = nUndf | nExt
|
||||
)
|
||||
wantSym("addq", defined, 1, 0)
|
||||
wantSym("getanswer", defined, 1, 5)
|
||||
wantSym("useextern", defined, 1, 13)
|
||||
answer := byName["answer"]
|
||||
if answer.Type != local || answer.Sect != 2 {
|
||||
t.Errorf("answer: type/sect = %#x/%d, want %#x/2", answer.Type, answer.Sect, local)
|
||||
}
|
||||
wantSym("extvar", undefined, 0, 0)
|
||||
|
||||
// Relocations: both X86_64_RELOC_SIGNED, PC-relative, 4 bytes wide.
|
||||
// The local one carries its section number in Value, the external one
|
||||
// its symbol number.
|
||||
if len(text.Relocs) != 2 {
|
||||
t.Fatalf("__text relocs = %d, want 2", len(text.Relocs))
|
||||
}
|
||||
var sawLocal, sawExternal bool
|
||||
for _, r := range text.Relocs {
|
||||
if !r.Pcrel || r.Len != 2 || r.Type != x8664RelocSigned {
|
||||
t.Errorf("reloc at %#x: pcrel/len/type = %v/%d/%d", r.Addr, r.Pcrel, r.Len, r.Type)
|
||||
}
|
||||
switch {
|
||||
case r.Extern:
|
||||
if name := syms[r.Value].Name; name != "extvar" {
|
||||
t.Errorf("external reloc at %#x names %q, want extvar", r.Addr, name)
|
||||
}
|
||||
sawExternal = true
|
||||
default:
|
||||
if r.Value != 2 { // __data, the second section
|
||||
t.Errorf("local reloc at %#x: section %d, want 2 (__data)", r.Addr, r.Value)
|
||||
}
|
||||
sawLocal = true
|
||||
}
|
||||
}
|
||||
if !sawLocal || !sawExternal {
|
||||
t.Errorf("relocs seen: local=%v external=%v, want both", sawLocal, sawExternal)
|
||||
}
|
||||
|
||||
// The __text bytes are the image code, with the external displacement
|
||||
// primed to addend − 4 and the local one left resolved.
|
||||
textData, err := text.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := append([]byte(nil), img.Code...)
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
if r.Name == "extvar" {
|
||||
binary.LittleEndian.PutUint32(want[fn.Offset+r.Off:], 0xfffffffc) // −4
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(textData, want) {
|
||||
t.Errorf("__text bytes %x, want %x", textData, want)
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,6 @@ func Idx(base, index Reg, scale int, disp int64, size int) Mem {
|
||||
return Mem{Base: base, Index: index, Scale: scale, Disp: disp, Size: size, HasBase: true, HasIndex: true}
|
||||
}
|
||||
|
||||
// Rip builds a RIP-relative memory operand (RIP)+disp.
|
||||
func Rip(disp int64, size int) Mem {
|
||||
return Mem{Disp: disp, Size: size}
|
||||
}
|
||||
|
||||
// sbMem is a memory operand that references a static (SB) symbol. It encodes
|
||||
// as a RIP-relative reference with a placeholder displacement; the encoder
|
||||
// records a patch site so the file-level layout can fill in the true rel32
|
||||
|
||||
+22
-22
@@ -7,6 +7,8 @@
|
||||
// by round-tripping through golang.org/x/arch's decoder in the tests.
|
||||
package asm
|
||||
|
||||
import "maps"
|
||||
|
||||
import "strings"
|
||||
|
||||
// Reg is an x86-64 register. In Plan 9 assembly the classic names (AX, BX, …)
|
||||
@@ -63,28 +65,28 @@ var (
|
||||
CX = Reg{idx: 1, size: 2}
|
||||
DX = Reg{idx: 2, size: 2}
|
||||
BX = Reg{idx: 3, size: 2}
|
||||
SP = Reg{idx: 4, size: 2}
|
||||
BP = Reg{idx: 5, size: 2}
|
||||
_ = Reg{idx: 4, size: 2}
|
||||
_ = Reg{idx: 5, size: 2}
|
||||
SI = Reg{idx: 6, size: 2}
|
||||
DI = Reg{idx: 7, size: 2}
|
||||
|
||||
EAX = Reg{idx: 0, size: 4}
|
||||
ECX = Reg{idx: 1, size: 4}
|
||||
EDX = Reg{idx: 2, size: 4}
|
||||
EBX = Reg{idx: 3, size: 4}
|
||||
ESP = Reg{idx: 4, size: 4}
|
||||
EBP = Reg{idx: 5, size: 4}
|
||||
ESI = Reg{idx: 6, size: 4}
|
||||
EDI = Reg{idx: 7, size: 4}
|
||||
_ = Reg{idx: 0, size: 4}
|
||||
_ = Reg{idx: 1, size: 4}
|
||||
_ = Reg{idx: 2, size: 4}
|
||||
_ = Reg{idx: 3, size: 4}
|
||||
_ = Reg{idx: 4, size: 4}
|
||||
_ = Reg{idx: 5, size: 4}
|
||||
_ = Reg{idx: 6, size: 4}
|
||||
_ = Reg{idx: 7, size: 4}
|
||||
|
||||
RAX = Reg{idx: 0, size: 8}
|
||||
RCX = Reg{idx: 1, size: 8}
|
||||
RDX = Reg{idx: 2, size: 8}
|
||||
RBX = Reg{idx: 3, size: 8}
|
||||
RSP = Reg{idx: 4, size: 8}
|
||||
RBP = Reg{idx: 5, size: 8}
|
||||
RSI = Reg{idx: 6, size: 8}
|
||||
RDI = Reg{idx: 7, size: 8}
|
||||
_ = Reg{idx: 0, size: 8}
|
||||
_ = Reg{idx: 1, size: 8}
|
||||
_ = Reg{idx: 2, size: 8}
|
||||
_ = Reg{idx: 3, size: 8}
|
||||
_ = Reg{idx: 4, size: 8}
|
||||
_ = Reg{idx: 5, size: 8}
|
||||
_ = Reg{idx: 6, size: 8}
|
||||
_ = Reg{idx: 7, size: 8}
|
||||
)
|
||||
|
||||
// regByName maps an assembly register name (case-insensitive) to a Reg.
|
||||
@@ -121,13 +123,11 @@ func buildRegByName() map[string]Reg {
|
||||
}
|
||||
|
||||
// 8-bit: AL..BH, SPL..DIL, R8B..R15B.
|
||||
for n, r := range map[string]Reg{
|
||||
maps.Copy(m, map[string]Reg{
|
||||
"AL": AL, "CL": CL, "DL": DL, "BL": BL,
|
||||
"AH": AH, "CH": CH, "DH": DH, "BH": BH,
|
||||
"SPL": SPL, "BPL": BPL, "SIL": SIL, "DIL": DIL,
|
||||
} {
|
||||
m[n] = r
|
||||
}
|
||||
})
|
||||
for i := 8; i <= 15; i++ {
|
||||
m["R"+itoa(i)+"B"] = Reg{idx: i, size: 1}
|
||||
}
|
||||
|
||||
+393
-161
@@ -5,17 +5,25 @@ package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
||||
// It handles the full RV64IMAFDC instruction set including RVC compression.
|
||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
|
||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
||||
fi := riscvComputeFrame(t)
|
||||
prologue := riscvPrologue(fi)
|
||||
|
||||
var relocs []Reloc
|
||||
var spadj []SpadjStep
|
||||
|
||||
// The prologue raises the SP delta by autosize; the boundary is reported
|
||||
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
|
||||
if fi.autosize != 0 {
|
||||
spadj = append(spadj, SpadjStep{PC: riscvPrologueSpadjPC(fi), Value: fi.autosize})
|
||||
}
|
||||
|
||||
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
|
||||
// per instruction (or 8 for MOV $large-imm). No encoding yet.
|
||||
@@ -33,7 +41,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
recs = append(recs, instrRec{instr: s})
|
||||
pos += riscvInstrSize(s)
|
||||
pos += riscvInstrSize(s, fi)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +50,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
|
||||
for i := range recs {
|
||||
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
||||
}
|
||||
recs[i].code = code
|
||||
pc += len(code)
|
||||
@@ -78,36 +86,52 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
|
||||
out := append([]byte(nil), prologue...)
|
||||
pc = len(prologue)
|
||||
preCount := len(relocs)
|
||||
var lines []LineEntry
|
||||
for _, r := range recs {
|
||||
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
|
||||
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
|
||||
out = append(out, r.code...)
|
||||
pc += len(r.code)
|
||||
} else {
|
||||
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if c16, ok := tryCompressRVC(r.instr, fi); ok {
|
||||
code = []byte{byte(c16), byte(c16 >> 8)}
|
||||
}
|
||||
// Make newly added relocation offsets absolute (subtract prologue to make
|
||||
// them function-relative, then the caller adds fn.Offset).
|
||||
// Make newly added relocation offsets function-relative. Each
|
||||
// instruction records its reloc offset relative to its own start;
|
||||
// the current pc is that instruction's offset from the function
|
||||
// start (which includes the prologue). After is the address just
|
||||
// past the relocated field, shifted by the same amount.
|
||||
for j := preCount; j < len(relocs); j++ {
|
||||
relocs[j].Off += pc - len(prologue)
|
||||
relocs[j].Off += pc
|
||||
relocs[j].After += pc
|
||||
}
|
||||
preCount = len(relocs)
|
||||
// The RET's epilogue closes the frame: the SP delta returns to zero
|
||||
// after its ADDI (restore LR + ADDI).
|
||||
if strings.ToUpper(r.instr.Mnemonic.Text) == "RET" && fi.autosize != 0 {
|
||||
spadj = append(spadj, SpadjStep{PC: pc + riscvReturnEpilogueLen(fi), Value: 0})
|
||||
}
|
||||
out = append(out, code...)
|
||||
pc += len(code)
|
||||
}
|
||||
}
|
||||
return out, offsets, relocs, nil
|
||||
return out, offsets, relocs, lines, spadj, nil
|
||||
}
|
||||
|
||||
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
||||
// Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW).
|
||||
func riscvInstrSize(instr *ast.Instr) int {
|
||||
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
||||
// arithmetic with a large immediate expand to several (possibly compressed)
|
||||
// instructions.
|
||||
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
if mnem == "RET" {
|
||||
return len(riscvReturn(fi))
|
||||
}
|
||||
if mnem == "MOV" && len(ops) == 2 {
|
||||
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
|
||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
|
||||
@@ -121,14 +145,15 @@ func riscvInstrSize(instr *ast.Instr) int {
|
||||
if isMemOperand(ops[1]) && ops[1].Addr.Sym != nil && ops[1].Addr.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
}
|
||||
// MOV $imm, rd → large immediate needs LUI+ADDIW.
|
||||
if isImmOperand(ops[0]) {
|
||||
imm := immFromOperand(ops[0])
|
||||
if imm < -2048 || imm > 2047 {
|
||||
return 8
|
||||
}
|
||||
// MOV $imm, rd → size depends on the immediate and RVC compression.
|
||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil {
|
||||
return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0]))
|
||||
}
|
||||
}
|
||||
// I-type arithmetic with a large immediate expands to several instructions.
|
||||
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
|
||||
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
@@ -151,36 +176,27 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
// Handle pseudo-instructions and special cases first.
|
||||
switch mnem {
|
||||
case "RET":
|
||||
// RET = JALR X0, 0(X1)
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
// RET = epilogue (restore LR and close the frame when present) +
|
||||
// uncompressed JALR X0, 0(X1) (the toolchain never compresses RET).
|
||||
return riscvReturn(fi), nil
|
||||
case "CALL":
|
||||
// CALL target → AUIPC X1, %pcrel_hi + JALR X1, %pcrel_lo(X1).
|
||||
// For now, emit AUIPC X1, 0 + JALR X1, 0(X1) with zero offsets.
|
||||
// The relocation system will fill the actual offsets.
|
||||
if len(ops) >= 1 {
|
||||
target := labelFromOperand(ops[0])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// AUIPC X1, upper 20 bits
|
||||
hi := (offset + 0x800) >> 12
|
||||
word1 := riscvUType(riscvEnc{0x17, 0x0, 0x00}, 1, hi<<12)
|
||||
// JALR X1, lower 12 bits(X1)
|
||||
lo := offset - (hi << 12)
|
||||
word2 := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, 1, lo)
|
||||
var out []byte
|
||||
out = append(out, byte(word1), byte(word1>>8), byte(word1>>16), byte(word1>>24))
|
||||
out = append(out, byte(word2), byte(word2>>8), byte(word2>>16), byte(word2>>24))
|
||||
return out, nil
|
||||
// CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL
|
||||
// relocation. The Go assembler rejects CALL to a local branch label.
|
||||
if len(ops) != 1 {
|
||||
return nil, fmt.Errorf("CALL expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
// CALL with no target: encode as NOP (unsupported).
|
||||
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0)
|
||||
op := ops[0]
|
||||
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
||||
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
||||
}
|
||||
if relocs != nil {
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelRISCVJal, Addend: op.Addr.Sym.Offset})
|
||||
}
|
||||
word = riscvJType(1, 0) // JAL X1, 0 — the linker fills the offset
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
case "JMP":
|
||||
// JMP = JAL X0, target. Try C.J compression.
|
||||
// JMP = JAL X0, target. The Go assembler never compresses this to
|
||||
// C.J, so always emit the 32-bit JAL.
|
||||
var target string
|
||||
if len(ops) >= 1 {
|
||||
target = labelFromOperand(ops[0])
|
||||
@@ -190,11 +206,6 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// C.J: funct3=0x5, offset in ±2 KB, bit 0 must be 0.
|
||||
if offset >= -2048 && offset <= 2046 && offset%2 == 0 {
|
||||
c16 := rvcCJ(0x5, offset)
|
||||
return []byte{byte(c16), byte(c16 >> 8)}, nil
|
||||
}
|
||||
word = riscvJType(0, offset)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
case "JAL":
|
||||
@@ -211,18 +222,13 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
// JAL X0, target → C.J when offset fits.
|
||||
if rd == 0 && offset >= -2048 && offset <= 2046 && offset%2 == 0 {
|
||||
c16 := rvcCJ(0x5, offset)
|
||||
return []byte{byte(c16), byte(c16 >> 8)}, nil
|
||||
}
|
||||
word = riscvJType(rd, offset)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
|
||||
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
||||
// stores, register moves and immediate loads.
|
||||
case "MOV":
|
||||
return encodeRISCVMov(instr, offsets, fi, relocs)
|
||||
return encodeRISCVMov(instr, fi, relocs)
|
||||
|
||||
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
||||
case "JALR":
|
||||
@@ -304,26 +310,44 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
|
||||
switch {
|
||||
// R-type: Plan 9 order is INSTR src1, src2, dst (destination last).
|
||||
// R-type: Go reverses the ISA order, writing rs2, rs1, rd (destination
|
||||
// last); the two-operand form INSTR rs2, rd uses rd as rs1.
|
||||
case len(ops) == 3 && isRTypeInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0]) // source 1 (first operand)
|
||||
rs2 := regFromOperand(ops[1]) // source 2 (second operand)
|
||||
rs2 := regFromOperand(ops[0]) // first operand = rs2
|
||||
rs1 := regFromOperand(ops[1]) // second operand = rs1
|
||||
rd := regFromOperand(ops[2]) // destination (last operand)
|
||||
if rd < 0 || rs1 < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rs1, rs2)
|
||||
|
||||
// I-type shift (SLLI, SRLI, SRAI): INSTR rs, $shamt, rd.
|
||||
case len(ops) == 2 && isRTypeInstr(mnem):
|
||||
rs2 := regFromOperand(ops[0]) // source (first operand)
|
||||
rd := regFromOperand(ops[1]) // destination (second operand)
|
||||
if rd < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rd, rs2)
|
||||
|
||||
// I-type shift (SLLI, SRLI, SRAI): INSTR $shamt, rs1, rd; the two-operand
|
||||
// form INSTR $shamt, rd uses rd as the source.
|
||||
case len(ops) == 3 && isShiftImmInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0])
|
||||
shamt := int(immFromOperand(ops[1]))
|
||||
shamt := int(immFromOperand(ops[0]))
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rs1, shamt)
|
||||
|
||||
case len(ops) == 2 && isShiftImmInstr(mnem):
|
||||
shamt := int(immFromOperand(ops[0]))
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rd, shamt)
|
||||
|
||||
// AMO atomics: Plan 9 order is INSTR src, (addr), dst.
|
||||
case len(ops) == 3 && isAMOInstr(mnem):
|
||||
rs2 := regFromOperand(ops[0]) // source value
|
||||
@@ -334,10 +358,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
word = riscvAMOType(enc, rd, rs1, rs2)
|
||||
|
||||
// FP arithmetic: Plan 9 order is INSTR src1, src2, dst.
|
||||
// FP arithmetic: Go reverses the ISA order, writing rs2, rs1, rd.
|
||||
case len(ops) == 3 && isFPArithInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rs2 := regFromOperand(ops[1])
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd < 0 || rs1 < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid FP register in %s", mnem)
|
||||
@@ -390,25 +414,34 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
word = riscvAMOType(enc, rd, rs1, rs2)
|
||||
|
||||
// FP compare: INSTR src1, src2, dst(int) — result in integer register.
|
||||
// FP compare: Go reverses the ISA order, writing rs2, rs1, rd.
|
||||
case len(ops) == 3 && isFPCmpInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rs2 := regFromOperand(ops[1])
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd < 0 || rs1 < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rs1, rs2)
|
||||
|
||||
// I-type with immediate: Plan 9 order is INSTR src, imm, dst.
|
||||
// I-type with immediate: Plan 9 order is INSTR $imm, rs1, rd; the
|
||||
// two-operand form INSTR $imm, rd uses rd as the source.
|
||||
case len(ops) == 3 && isITypeInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0]) // source register
|
||||
imm := immFromOperand(ops[1]) // immediate
|
||||
imm := immFromOperand(ops[0]) // immediate
|
||||
rs1 := regFromOperand(ops[1]) // source register
|
||||
rd := regFromOperand(ops[2]) // destination
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvIType(enc, rd, rs1, imm)
|
||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
|
||||
|
||||
case len(ops) == 2 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0])
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
||||
|
||||
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
|
||||
case len(ops) == 2 && isLoadInstr(mnem):
|
||||
@@ -442,18 +475,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
|
||||
// Try C.BEQZ / C.BNEZ compression.
|
||||
if (mnem == "BEQ" || mnem == "BNE") && rs2 == 0 && isRVCIntReg(rs1) {
|
||||
if cOff := offset; cOff >= -256 && cOff <= 254 && cOff%2 == 0 {
|
||||
funct3 := uint32(0x6) // C.BEQZ
|
||||
if mnem == "BNE" {
|
||||
funct3 = 0x7 // C.BNEZ
|
||||
}
|
||||
c16 := rvcCB(funct3, rvcReg3(rs1), offset)
|
||||
return []byte{byte(c16), byte(c16 >> 8)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
|
||||
word = riscvBType(enc, rs1, rs2, offset)
|
||||
|
||||
// U-type: rd, imm.
|
||||
@@ -505,7 +527,7 @@ func isImmOperand(op *ast.Operand) bool {
|
||||
// - MOV Rs, (Rd) register-relative store
|
||||
// - MOV Rs, Rd register-to-register move (ADDI $0)
|
||||
// - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW)
|
||||
func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
ops := instr.Operands
|
||||
if len(ops) != 2 {
|
||||
return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops))
|
||||
@@ -585,62 +607,200 @@ func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// encodeRISCVLoadImm encodes loading an immediate into a register.
|
||||
// For 12-bit immediates: ADDI $imm, ZERO, rd.
|
||||
// For larger: LUI $hi, rd + ADDIW $lo, rd, rd.
|
||||
// encodeRISCVLoadImm encodes loading an immediate into a register (MOV $imm,
|
||||
// rd), matching the toolchain's instructionsForMOVConst. For 12-bit
|
||||
// immediates it emits ADDI $imm, ZERO, rd (compressed to C.LI when it fits
|
||||
// six signed bits); for larger immediates it emits LUI + [ADDIW], with the LUI
|
||||
// and ADDIW compressed to C.LUI / C.ADDIW when their immediate fits.
|
||||
func encodeRISCVLoadImm(rd int, imm int32) []byte {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||
if rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return word16(rvcCI(0x2, uint32(rd), uint32(imm)&0x3F)) // C.LI
|
||||
}
|
||||
return wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm))
|
||||
}
|
||||
|
||||
// LUI + ADDIW for larger constants.
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
var out []byte
|
||||
hi := int32((uint32(imm)+0x800)>>12) << 12 // LUI loads upper 20 bits
|
||||
lo := imm - hi
|
||||
wordLUI := riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, hi)
|
||||
out = append(out, byte(wordLUI), byte(wordLUI>>8), byte(wordLUI>>16), byte(wordLUI>>24))
|
||||
if lo != 0 {
|
||||
wordADDIW := riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, lo)
|
||||
out = append(out, byte(wordADDIW), byte(wordADDIW>>8), byte(wordADDIW>>16), byte(wordADDIW>>24))
|
||||
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
|
||||
out = append(out, word16(rvcCI(0x3, uint32(rd), uint32(high)&0x3F))...) // C.LUI
|
||||
} else {
|
||||
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, high<<12))...)
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
out = append(out, word16(rvcCI(0x1, uint32(rd), uint32(low)&0x3F))...) // C.ADDIW
|
||||
} else {
|
||||
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, low))...)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// riscvMovImmSize returns the encoded byte length of MOV $imm, rd, mirroring
|
||||
// encodeRISCVLoadImm's expansion and compression.
|
||||
func riscvMovImmSize(rd int, imm int32) int {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
if rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return 2 // C.LI
|
||||
}
|
||||
return 4 // ADDI
|
||||
}
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
size := 0
|
||||
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
|
||||
size += 2 // C.LUI
|
||||
} else {
|
||||
size += 4 // LUI
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
size += 2 // C.ADDIW
|
||||
} else {
|
||||
size += 4 // ADDIW
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// splitRISCV32Imm splits a signed 32-bit immediate into a signed 12-bit low
|
||||
// part and a signed 20-bit high part, mirroring cmd/internal/obj/riscv's
|
||||
// Split32BitImmediate. The high part is returned unshifted; callers place it
|
||||
// in the upper bits of LUI (or its compressed C.LUI form).
|
||||
func splitRISCV32Imm(imm int32) (low, high int32) {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return imm, 0
|
||||
}
|
||||
h := int64(imm) >> 12
|
||||
if imm&(1<<11) != 0 {
|
||||
h++
|
||||
}
|
||||
low = int32((int64(imm) << 52) >> 52) // sign extend 12 bits
|
||||
high = int32((h << 44) >> 44) // sign extend 20 bits
|
||||
return low, high
|
||||
}
|
||||
|
||||
// encodeRISCVItypeImmediate encodes an I-type arithmetic instruction, expanding
|
||||
// large immediates for ADDI/ANDI/ORI/XORI into LUI+ADDIW+op (or two ADDIs for
|
||||
// ADDI), matching the Go assembler.
|
||||
func encodeRISCVItypeImmediate(mnem string, enc riscvEnc, rd, rs1 int, imm int32) ([]byte, error) {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return wordLE(riscvIType(enc, rd, rs1, imm)), nil
|
||||
}
|
||||
|
||||
var opMn string
|
||||
switch mnem {
|
||||
case "ADDI":
|
||||
opMn = "ADD"
|
||||
case "ANDI":
|
||||
opMn = "AND"
|
||||
case "ORI":
|
||||
opMn = "OR"
|
||||
case "XORI":
|
||||
opMn = "XOR"
|
||||
default:
|
||||
return nil, fmt.Errorf("%s: immediate %d does not fit 12 bits", mnem, imm)
|
||||
}
|
||||
|
||||
// ADDI with a small-ish immediate splits into two ADDIs.
|
||||
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||
imm0 := imm / 2
|
||||
imm1 := imm - imm0
|
||||
var out []byte
|
||||
out = append(out, wordLE(riscvIType(enc, rd, rs1, imm0))...)
|
||||
out = append(out, wordLE(riscvIType(enc, rd, rd, imm1))...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LUI $high, TMP; [ADDIW $low, TMP, TMP]; op TMP, rs1, rd. The LUI and
|
||||
// ADDIW compress to their RVC forms (C.LUI / C.ADDIW) when the immediate
|
||||
// fits 6 signed bits, matching the toolchain's compress pass.
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
tmp := 31 // X31 = T6 = TMP
|
||||
var out []byte
|
||||
if high != 0 && high >= -32 && high <= 31 {
|
||||
out = append(out, word16(rvcCI(0x3, uint32(tmp), uint32(high)&0x3F))...)
|
||||
} else {
|
||||
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, tmp, high<<12))...)
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
out = append(out, word16(rvcCI(0x1, uint32(tmp), uint32(low)&0x3F))...)
|
||||
} else {
|
||||
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, tmp, tmp, low))...)
|
||||
}
|
||||
}
|
||||
opEnc, ok := riscvInstrTable[opMn]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: unsupported operation %q", mnem, opMn)
|
||||
}
|
||||
out = append(out, wordLE(riscvRType(opEnc, rd, rs1, tmp))...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// riscvItypeImmediateSize returns the encoded byte length of an I-type
|
||||
// immediate instruction, accounting for the large-immediate expansion.
|
||||
func riscvItypeImmediateSize(mnem string, imm int32) int {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return 4
|
||||
}
|
||||
switch mnem {
|
||||
case "ADDI", "ANDI", "ORI", "XORI":
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||
return 8
|
||||
}
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
size := 4 // the R-type op (TMP is X31, never compressed)
|
||||
if high != 0 && high >= -32 && high <= 31 {
|
||||
size += 2 // C.LUI
|
||||
} else {
|
||||
size += 4 // LUI
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
size += 2 // C.ADDIW
|
||||
} else {
|
||||
size += 4 // ADDIW
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
|
||||
// symbol into rd. Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I relocs.
|
||||
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
|
||||
// toolchain uses for the pair (the object-file emitters expand or map it).
|
||||
func encodeRISCVSBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
||||
name := sym.Name
|
||||
if relocs != nil {
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
|
||||
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12})
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
|
||||
}
|
||||
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
|
||||
addi := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rd, 0)
|
||||
return append(wordLE(auipc), wordLE(addi)...)
|
||||
}
|
||||
|
||||
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd.
|
||||
// Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I relocs.
|
||||
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd,
|
||||
// recording the single R_RISCV_PCREL_ITYPE relocation for the pair.
|
||||
func encodeRISCVSBLoad(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
||||
name := sym.Name
|
||||
if relocs != nil {
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
|
||||
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12})
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
|
||||
}
|
||||
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
|
||||
ld := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rd, 0)
|
||||
return append(wordLE(auipc), wordLE(ld)...)
|
||||
}
|
||||
|
||||
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol.
|
||||
// Records R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_S relocs.
|
||||
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol,
|
||||
// recording the single R_RISCV_PCREL_STYPE relocation for the pair.
|
||||
func encodeRISCVSBStore(sym *ast.Symbol, rs2 int, relocs *[]Reloc) []byte {
|
||||
tmp := 31 // X31 = T6
|
||||
name := sym.Name
|
||||
if relocs != nil {
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelPCRelHI20})
|
||||
*relocs = append(*relocs, Reloc{Off: 4, After: 4, Name: name, Kind: RelPCRelLO12S})
|
||||
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELSType, Addend: sym.Offset})
|
||||
}
|
||||
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, tmp, 0)
|
||||
sd := riscvSType(riscvEnc{0x23, 0x3, 0x00}, tmp, rs2, 0)
|
||||
@@ -655,6 +815,11 @@ func wordLE(w uint32) []byte {
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||
}
|
||||
|
||||
// word16 encodes a uint16 as 2 little-endian bytes.
|
||||
func word16(w uint16) []byte {
|
||||
return []byte{byte(w), byte(w >> 8)}
|
||||
}
|
||||
|
||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
@@ -686,10 +851,6 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
ops := instr.Operands
|
||||
|
||||
switch mnem {
|
||||
case "RET":
|
||||
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rd=0, rs2=1)
|
||||
return rvcCR(0x8, 0, 1), true
|
||||
|
||||
case "LD", "MOV":
|
||||
// LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits.
|
||||
// MOV name+off(FP), rd → load, same compression.
|
||||
@@ -708,6 +869,10 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcLSP(0x3, uint32(rd), uint32(imm)), true
|
||||
}
|
||||
// Register-relative C.LD: both in prime regs, 8-byte scaled offset.
|
||||
if rs1 != -1 && rd != -1 && isRVCIntReg(rd) && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
|
||||
return rvcCL(0x3, rvcReg3(rd), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
// MOV reg, mem → store, try C.SDSP.
|
||||
if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) {
|
||||
rs2, rs1, imm := extractSDParams(instr, fi)
|
||||
@@ -722,16 +887,46 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
// Register-relative C.SD: base and source in prime regs.
|
||||
if rs1 != -1 && rs2 != -1 && isRVCIntReg(rs1) && isRVCIntReg(rs2) && imm >= 0 && imm < 256 && imm%8 == 0 {
|
||||
return rvcCS(0x7, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "LW":
|
||||
rd, rs1, imm := extractLDParams(instr, fi)
|
||||
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 256 && imm%4 == 0 {
|
||||
return rvcLSP(0x2, uint32(rd), uint32(imm)), true
|
||||
}
|
||||
if rs1 != -1 && rd != -1 && isRVCIntReg(rd) && isRVCIntReg(rs1) && imm >= 0 && imm < 128 && imm%4 == 0 {
|
||||
return rvcCL(0x2, rvcReg3(rd), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "SW":
|
||||
rs2, rs1, imm := extractSDParams(instr, fi)
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 256 && imm%4 == 0 {
|
||||
return rvcSSP(0x6, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
if rs1 != -1 && rs2 != -1 && isRVCIntReg(rs1) && isRVCIntReg(rs2) && imm >= 0 && imm < 128 && imm%4 == 0 {
|
||||
return rvcCS(0x6, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "ADDI":
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
rd, rs1, imm := extractITypeParams(instr)
|
||||
if rd == -1 || rs1 == -1 {
|
||||
return 0, false
|
||||
}
|
||||
if rd == 2 && rs1 == 2 && imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
|
||||
// C.ADDI16SP: ADDI to SP by a nonzero 16-byte multiple.
|
||||
return rvcADDI16SP(2, imm), true
|
||||
}
|
||||
if rd == rs1 && rd != 0 && imm != 0 && imm >= -32 && imm <= 31 {
|
||||
// C.ADDI: funct3=0x0, rs1/rd, nzimm[5:0]
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
if isRVCIntReg(rd) && rs1 == 2 && imm != 0 && imm >= 0 && imm < 1024 && imm%4 == 0 {
|
||||
// C.ADDI4SPN: ADDI $imm, SP, rd for a prime rd.
|
||||
return rvcCIW(0x0, rvcReg3(rd), uint32(imm)), true
|
||||
}
|
||||
if rs1 == 0 && rd != 0 && imm >= -32 && imm <= 31 {
|
||||
// C.LI: funct3=0x2, rd, imm[5:0]
|
||||
return rvcCI(0x2, uint32(rd), uint32(imm)&0x3F), true
|
||||
@@ -740,43 +935,44 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
// C.MV: funct4=0x8, rd, rs1 (CR-type)
|
||||
return rvcCR(0x8, uint32(rd), uint32(rs1)), true
|
||||
}
|
||||
|
||||
case "JAL":
|
||||
// JAL X0, target → C.J when offset fits in ±2KB.
|
||||
if len(ops) >= 1 {
|
||||
// For JAL with implicit rd=0 (JMP alias), check target.
|
||||
// C.J: funct3=0x5
|
||||
// Offset is computed at encode time — we can't check it here.
|
||||
return 0, false
|
||||
if rd == 0 && rs1 == 0 && imm == 0 {
|
||||
// C.NOP
|
||||
return 0x0001, true
|
||||
}
|
||||
|
||||
case "JAL":
|
||||
// JAL/JMP are never compressed to C.J by the Go assembler.
|
||||
return 0, false
|
||||
|
||||
case "JMP":
|
||||
// C.J — handled in encodeRISCVInstr with actual offset.
|
||||
// JAL/JMP are never compressed to C.J by the Go assembler.
|
||||
return 0, false
|
||||
|
||||
case "BEQ":
|
||||
// C.BEQZ — handled in encodeRISCVInstr with actual offset.
|
||||
// Branches are never compressed to C.BEQZ/C.BNEZ.
|
||||
return 0, false
|
||||
|
||||
case "BNE":
|
||||
// C.BNEZ — handled in encodeRISCVInstr with actual offset.
|
||||
// Branches are never compressed to C.BEQZ/C.BNEZ.
|
||||
return 0, false
|
||||
|
||||
case "ADD":
|
||||
// ADD rd, rs2 → C.ADD when rd == rs1 and both in prime regs (rd ≠ 0).
|
||||
// ADD is commutative: if rd == rs2, swap.
|
||||
// ADD rs2, rs1, rd → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD
|
||||
// is commutative, so if rd == rs2, swap. ADD rs2, X0, rd is C.MV.
|
||||
if len(ops) == 3 {
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rs2 := regFromOperand(ops[1])
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
|
||||
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
|
||||
// C.ADD: funct6=0x27, funct2=0x0 (CA-type)
|
||||
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs2)), true
|
||||
if rd == rs1 && rs2 != 0 {
|
||||
return rvcCR(0x9, uint32(rd), uint32(rs2)), true
|
||||
}
|
||||
if rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
|
||||
// Swap: C.ADD rd, rs1
|
||||
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs1)), true
|
||||
if rd == rs2 && rs1 != 0 {
|
||||
return rvcCR(0x9, uint32(rd), uint32(rs1)), true
|
||||
}
|
||||
if rs1 == 0 && rs2 != 0 {
|
||||
// ADD rs2, X0, rd → C.MV rd, rs2.
|
||||
return rvcCR(0x8, uint32(rd), uint32(rs2)), true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,13 +991,38 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
case "AND":
|
||||
funct2 = 0x3
|
||||
}
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rs2 := regFromOperand(ops[1])
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
|
||||
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
|
||||
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs2)), true
|
||||
}
|
||||
// AND/OR/XOR are commutative; SUB is not.
|
||||
if mnem != "SUB" && rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
|
||||
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs1)), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "ADDW", "SUBW":
|
||||
// C.ADDW (0x27,1) / C.SUBW (0x27,0) — CA-type, prime regs.
|
||||
if len(ops) == 3 {
|
||||
funct2 := uint32(0x0)
|
||||
if mnem == "ADDW" {
|
||||
funct2 = 0x1
|
||||
}
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rd := regFromOperand(ops[2])
|
||||
if rd != -1 && rs1 != -1 && rs2 != -1 && isRVCIntReg(rd) {
|
||||
if rd == rs1 && isRVCIntReg(rs2) {
|
||||
return rvcCA(0x27, funct2, rvcReg3(rd), rvcReg3(rs2)), true
|
||||
}
|
||||
// ADDW is commutative; SUBW is not.
|
||||
if mnem == "ADDW" && isRVCIntReg(rs1) && rd == rs2 {
|
||||
return rvcCA(0x27, funct2, rvcReg3(rd), rvcReg3(rs1)), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,6 +1032,10 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
if rs1 == 2 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcLSP(0x1, uint32(rd), uint32(imm)), true
|
||||
}
|
||||
// Register-relative C.FLD: rd in F8-F15, base in X8-X15.
|
||||
if rs1 != -1 && rd != -1 && rd >= 8 && rd <= 15 && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
|
||||
return rvcCL(0x1, uint32(rd-8), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "FSD":
|
||||
// FSD rs2, imm(SP) → C.FSDSP (CSS-type, funct3=0x5).
|
||||
@@ -818,51 +1043,55 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
|
||||
return rvcSSP(0x5, uint32(rs2), uint32(imm)), true
|
||||
}
|
||||
// Register-relative C.FSD: source in F8-F15, base in X8-X15.
|
||||
if rs1 != -1 && rs2 != -1 && rs2 >= 8 && rs2 <= 15 && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
|
||||
return rvcCS(0x5, uint32(rs2-8), rvcReg3(rs1), uint32(imm)), true
|
||||
}
|
||||
|
||||
case "LUI":
|
||||
// LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in 6 bits.
|
||||
// LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in six
|
||||
// signed bits (matching the toolchain's compress pass).
|
||||
if len(ops) == 2 {
|
||||
rd := regFromOperand(ops[0])
|
||||
imm := immFromOperand(ops[1])
|
||||
if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= 1 && imm <= 63 {
|
||||
if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= -32 && imm <= 31 {
|
||||
return rvcCI(0x3, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
}
|
||||
|
||||
case "ADDIW":
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
rd, rs1, imm := extractITypeParams(instr)
|
||||
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
|
||||
case "SLLI", "SRLI", "SRAI":
|
||||
// C.SLLI (funct3=0x0), C.SRLI (funct3=0x4, funct2=0), C.SRAI (funct3=0x4, funct2=1).
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
rd, rs1, imm := extractITypeParams(instr)
|
||||
if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 {
|
||||
if mnem == "SLLI" {
|
||||
// C.SLLI: funct3=0, CI-type with shamt in bits [12|6:2].
|
||||
// For simplicity, use the standard CI format — the shamt is in imm[5:0].
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
// C.SLLI: funct3=0, op=10 quadrant, shamt in bits [12|6:2].
|
||||
return rvcSLLI(uint32(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
if isRVCIntReg(rd) {
|
||||
funct2 := uint32(0x0)
|
||||
if mnem == "SRAI" {
|
||||
funct2 = 0x1
|
||||
}
|
||||
// CB-format shift: funct3=0x4, shamt in bits [12|6:2].
|
||||
// Use simplified encoding for now.
|
||||
_ = funct2
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
// C.SRLI/C.SRAI: CB-type, funct3=0x4.
|
||||
return rvcCBShift(funct2, rvcReg3(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
}
|
||||
|
||||
case "ANDI":
|
||||
rd, rs1, imm := extractITypeParams(instr, fi)
|
||||
rd, rs1, imm := extractITypeParams(instr)
|
||||
if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 {
|
||||
// C.ANDI: funct3=0x4, funct2=0x2 (CB-type).
|
||||
// Simplified encoding for now.
|
||||
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
|
||||
// C.ANDI: CB-type, funct3=0x4, funct2=0x2.
|
||||
return rvcCBShift(0x2, rvcReg3(rd), uint32(imm)&0x3F), true
|
||||
}
|
||||
|
||||
case "EBREAK":
|
||||
// C.EBREAK: CR-type, funct4=0x9, rd=0, rs2=0.
|
||||
return rvcCR(0x9, 0, 0), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
@@ -899,15 +1128,23 @@ func extractSDParams(instr *ast.Instr, fi riscvFrameInfo) (rs2, rs1 int, imm int
|
||||
return
|
||||
}
|
||||
|
||||
// extractITypeParams extracts rd, rs1, and immediate for an I-type instruction.
|
||||
func extractITypeParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) {
|
||||
// extractITypeParams extracts rd, rs1, and immediate for an I-type
|
||||
// instruction. The Plan 9 order is INSTR $imm, rs1, rd (3 operands) or
|
||||
// INSTR $imm, rd (2 operands, rd is also the source).
|
||||
func extractITypeParams(instr *ast.Instr) (rd, rs1 int, imm int32) {
|
||||
ops := instr.Operands
|
||||
if len(ops) != 3 {
|
||||
switch len(ops) {
|
||||
case 3:
|
||||
imm = immFromOperand(ops[0])
|
||||
rs1 = regFromOperand(ops[1])
|
||||
rd = regFromOperand(ops[2])
|
||||
case 2:
|
||||
imm = immFromOperand(ops[0])
|
||||
rd = regFromOperand(ops[1])
|
||||
rs1 = rd
|
||||
default:
|
||||
return -1, -1, 0
|
||||
}
|
||||
rs1 = regFromOperand(ops[0])
|
||||
imm = immFromOperand(ops[1])
|
||||
rd = regFromOperand(ops[2])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1013,11 +1250,6 @@ func isFPCmpInstr(m string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isFPCvtInstr(m string) bool {
|
||||
_, ok := riscvCvtTable[m]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Operand helpers.
|
||||
func regFromOperand(op *ast.Operand) int {
|
||||
// Register is in Addr.Base (from (base) syntax) or Addr.Sym.Name (bare ident).
|
||||
|
||||
+79
-52
@@ -13,7 +13,7 @@ func riscvRegNum(name string) int {
|
||||
// Numbered integer registers.
|
||||
case "X0", "ZERO":
|
||||
return 0
|
||||
case "X1", "RA":
|
||||
case "X1", "RA", "LR":
|
||||
return 1
|
||||
case "X2", "SP":
|
||||
return 2
|
||||
@@ -21,9 +21,9 @@ func riscvRegNum(name string) int {
|
||||
return 3
|
||||
case "X4", "TP":
|
||||
return 4
|
||||
case "X5", "T0", "LR":
|
||||
case "X5", "T0":
|
||||
return 5
|
||||
case "X6", "T1", "TMP":
|
||||
case "X6", "T1":
|
||||
return 6
|
||||
case "X7", "T2":
|
||||
return 7
|
||||
@@ -73,7 +73,7 @@ func riscvRegNum(name string) int {
|
||||
return 29
|
||||
case "X30", "T5":
|
||||
return 30
|
||||
case "X31", "T6":
|
||||
case "X31", "T6", "TMP":
|
||||
return 31
|
||||
// Floating-point registers (F0-F31).
|
||||
case "F0", "FT0":
|
||||
@@ -457,63 +457,84 @@ func rvcCR(funct4, rd, rs2 uint32) uint16 {
|
||||
// rvcCI encodes a CI-type (immediate) compressed instruction.
|
||||
// Used for C.ADDI, C.LI, C.LUI, C.ADDIW — linear 6-bit immediate.
|
||||
func rvcCI(funct3, rd uint32, imm uint32) uint16 {
|
||||
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x2)
|
||||
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1)
|
||||
}
|
||||
|
||||
// rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or
|
||||
// C.FLDSP (funct3=1). offset is the full byte offset; the immediate bits
|
||||
// are interleaved per the RISC-V spec: [5:3|8:6].
|
||||
func rvcLSP(funct3, rd uint32, offset uint32) uint16 {
|
||||
// Bit interleave offset bits [5,4,3,8,7,6] → packed value.
|
||||
// rvcSLLI encodes C.SLLI, which shares funct3=0 with C.ADDI but lives in the
|
||||
// op=10 quadrant (unlike C.ADDI's op=01).
|
||||
func rvcSLLI(rd, shamt uint32) uint16 {
|
||||
return uint16(((shamt>>5)&1)<<12 | (rd << 7) | (shamt&0x1F)<<2 | 0x2)
|
||||
}
|
||||
|
||||
// encodeRVCPattern extracts the bits listed in pattern (MSB first) from imm
|
||||
// into a packed value, matching cmd/internal/obj/riscv's encodeBitPattern.
|
||||
func encodeRVCPattern(imm uint32, pattern []int) uint32 {
|
||||
packed := uint32(0)
|
||||
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||
for _, bit := range pattern {
|
||||
packed = packed<<1 | (imm>>bit)&1
|
||||
}
|
||||
return packed
|
||||
}
|
||||
|
||||
// rvcLSP encodes a stack-relative compressed load (op=10 quadrant): C.LWSP
|
||||
// (funct3=2, 4-byte scale), C.LDSP (funct3=3) or C.FLDSP (funct3=1, 8-byte
|
||||
// scale). offset is the full byte offset.
|
||||
func rvcLSP(funct3, rd uint32, offset uint32) uint16 {
|
||||
pattern := []int{5, 4, 3, 8, 7, 6}
|
||||
if funct3 == 0x2 {
|
||||
pattern = []int{5, 4, 3, 2, 7, 6}
|
||||
}
|
||||
packed := uint32(0)
|
||||
for i, b := range pattern {
|
||||
packed |= ((offset >> b) & 1) << (5 - i)
|
||||
}
|
||||
return uint16((funct3 << 13) | ((packed>>5)&1)<<12 | (rd << 7) | (packed&0x1F)<<2 | 0x2)
|
||||
}
|
||||
|
||||
// rvcSSP encodes a CSS-type stack-relative store: C.SDSP (funct3=7) or
|
||||
// C.FSDSP (funct3=5). offset is the full byte offset; the immediate bits
|
||||
// are interleaved per the RISC-V spec: [5:3|8:6].
|
||||
// rvcSSP encodes a stack-relative compressed store (op=10 quadrant): C.SWSP
|
||||
// (funct3=6, 4-byte scale), C.SDSP (funct3=7) or C.FSDSP (funct3=5, 8-byte
|
||||
// scale). offset is the full byte offset.
|
||||
func rvcSSP(funct3, rs2 uint32, offset uint32) uint16 {
|
||||
// Bit interleave offset bits [5,4,3,8,7,6] → packed value.
|
||||
pattern := []int{5, 4, 3, 8, 7, 6}
|
||||
if funct3 == 0x6 {
|
||||
pattern = []int{5, 4, 3, 2, 7, 6}
|
||||
}
|
||||
packed := uint32(0)
|
||||
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||
for i, b := range pattern {
|
||||
packed |= ((offset >> b) & 1) << (5 - i)
|
||||
}
|
||||
return uint16((funct3 << 13) | (packed << 7) | (rs2 << 2) | 0x2)
|
||||
}
|
||||
|
||||
// rvcCSS encodes a CSS-type (stack store) compressed instruction.
|
||||
func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 {
|
||||
return uint16((funct3 << 13) | (imm << 7) | (rs2 << 2) | 0x2)
|
||||
}
|
||||
|
||||
// rvcCL encodes a CL-type (load) compressed instruction.
|
||||
// imm layout: [5:3] in bits [12:10], [2|6] in bits [6:5].
|
||||
// rvcCL encodes a register-relative compressed load (op=00 quadrant): C.LW
|
||||
// (funct3=2), C.LD (funct3=3) or C.FLD (funct3=1). imm is the full byte
|
||||
// offset; the immediate bits are extracted per the RISC-V CL format.
|
||||
func rvcCL(funct3, rd, rs1 uint32, imm uint32) uint16 {
|
||||
bits := uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rd << 2) | 0x0)
|
||||
return bits
|
||||
pattern := []int{5, 4, 3, 7, 6}
|
||||
if funct3 == 0x2 {
|
||||
pattern = []int{5, 4, 3, 2, 6}
|
||||
}
|
||||
packed := encodeRVCPattern(imm, pattern)
|
||||
return uint16((funct3 << 13) | ((packed>>2)&0x7)<<10 | (rs1 << 7) | ((packed & 0x3) << 5) | (rd << 2))
|
||||
}
|
||||
|
||||
// rvcCS encodes a CS-type (store) compressed instruction.
|
||||
// rvcCS encodes a register-relative compressed store (op=00 quadrant): C.SW
|
||||
// (funct3=6), C.SD (funct3=7) or C.FSD (funct3=5). imm is the full byte
|
||||
// offset; the immediate bits are extracted per the RISC-V CS format.
|
||||
func rvcCS(funct3, rs2, rs1 uint32, imm uint32) uint16 {
|
||||
return uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rs2 << 2) | 0x0)
|
||||
pattern := []int{5, 3, 7, 6}
|
||||
if funct3 == 0x6 {
|
||||
pattern = []int{5, 3, 2, 6}
|
||||
}
|
||||
packed := encodeRVCPattern(imm, pattern)
|
||||
return uint16((funct3 << 13) | ((packed>>2)&0x7)<<10 | (rs1 << 7) | ((packed & 0x3) << 5) | (rs2 << 2))
|
||||
}
|
||||
|
||||
// rvcCJ encodes a CJ-type (jump) compressed instruction.
|
||||
// offset is a 12-bit signed offset (bit 0 is always 0).
|
||||
func rvcCJ(funct3 uint32, offset int32) uint16 {
|
||||
uoff := uint32(offset) & 0xFFE
|
||||
bits := ((uoff >> 11) & 1) << 10
|
||||
bits |= ((uoff >> 4) & 1) << 9
|
||||
bits |= ((uoff >> 9) & 0x3) << 7
|
||||
bits |= ((uoff >> 10) & 1) << 6
|
||||
bits |= ((uoff >> 6) & 1) << 5
|
||||
bits |= ((uoff >> 7) & 1) << 4
|
||||
bits |= ((uoff >> 1) & 0x7) << 1
|
||||
bits |= ((uoff >> 5) & 1)
|
||||
return uint16((funct3 << 13) | (bits << 2) | 0x1)
|
||||
// rvcCIW encodes a CIW-type compressed immediate wide instruction: C.ADDI4SPN
|
||||
// (funct3=0). imm is the raw byte offset.
|
||||
func rvcCIW(funct3, rd uint32, imm uint32) uint16 {
|
||||
packed := encodeRVCPattern(imm, []int{5, 4, 9, 8, 7, 6, 2, 3})
|
||||
return uint16((funct3 << 13) | (packed << 5) | (rd << 2))
|
||||
}
|
||||
|
||||
// rvcCA encodes a CA-type (arithmetic) compressed instruction.
|
||||
@@ -522,16 +543,22 @@ func rvcCA(funct6, funct2, rd, rs2 uint32) uint16 {
|
||||
return uint16((funct6 << 10) | (rd << 7) | (funct2 << 5) | (rs2 << 2) | 0x1)
|
||||
}
|
||||
|
||||
// rvcCB encodes a CB-type (branch) compressed instruction.
|
||||
// Format: funct3[15:13] | offset[8|4:3] | rs1'[9:7] | offset[7:6|2:1|5] | op=01.
|
||||
// Bit pattern for offset: [8|4:3|7:6|2:1|5]
|
||||
func rvcCB(funct3, rs1 uint32, offset int32) uint16 {
|
||||
uoff := uint32(offset) & 0x1FE // bits [8:1]
|
||||
offBits := uint32(0)
|
||||
offBits |= ((uoff >> 8) & 1) << 10 // bit 10 = offset[8]
|
||||
offBits |= ((uoff >> 3) & 0x3) << 8 // bits 9:8 = offset[4:3]
|
||||
offBits |= ((uoff >> 6) & 0x3) << 6 // bits 7:6 = offset[7:6]
|
||||
offBits |= ((uoff >> 1) & 0x3) << 3 // bits 4:3 = offset[2:1]
|
||||
offBits |= ((uoff >> 5) & 1) << 2 // bit 2 = offset[5]
|
||||
return uint16((funct3 << 13) | offBits | (rs1 << 7) | 0x1)
|
||||
// rvcCBShift encodes a CB-type shift/immediate compressed instruction
|
||||
// (C.SRLI, C.SRAI, C.ANDI). rd is the 3-bit prime-register index; imm is
|
||||
// the 6-bit shamt/immediate; funct2 selects the operation (0=SRLI, 1=SRAI,
|
||||
// 2=ANDI).
|
||||
func rvcCBShift(funct2, rd, imm uint32) uint16 {
|
||||
return uint16((0x4 << 13) | ((imm>>5)&1)<<12 | (funct2 << 10) | (rd << 7) | (imm&0x1F)<<2 | 0x1)
|
||||
}
|
||||
|
||||
// rvcADDI16SP encodes C.ADDI16SP: ADDI rd, imm, rd for the stack pointer
|
||||
// with a 10-bit signed, 16-byte-scaled immediate. imm is the raw byte
|
||||
// offset; the immediate bits are extracted in the order [9|4|6|8:7|5].
|
||||
func rvcADDI16SP(rd uint32, imm int32) uint16 {
|
||||
u := uint32(imm)
|
||||
packed := uint32(0)
|
||||
for _, bit := range []uint{9, 4, 6, 8, 7, 5} {
|
||||
packed = packed<<1 | (u>>bit)&1
|
||||
}
|
||||
return uint16((0x3 << 13) | ((packed>>5)&1)<<12 | (rd << 7) | (packed&0x1F)<<2 | 0x1)
|
||||
}
|
||||
|
||||
+171
-157
@@ -4,6 +4,7 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -29,7 +30,7 @@ func firstTextRISCV(t *testing.T, src string) *ast.Text {
|
||||
// assembleRISCVHelper assembles one TEXT function and returns its code bytes.
|
||||
func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte {
|
||||
t.Helper()
|
||||
code, _, _, err := assembleRISCV(fn)
|
||||
code, _, _, _, _, err := assembleRISCV(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
@@ -65,9 +66,9 @@ TEXT ·arith(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 5 R-type instructions + RET compressed = 5*4 + 2 = 22
|
||||
if len(code) != 22 {
|
||||
t.Errorf("expected 22 bytes, got %d", len(code))
|
||||
// 5 R-type instructions + RET = 5*4 + 4 = 24
|
||||
if len(code) != 24 {
|
||||
t.Errorf("expected 24 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,35 +82,35 @@ TEXT ·mem(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 4 loads/stores (4B each) + C.JR RET (2B) = 18
|
||||
if len(code) != 18 {
|
||||
t.Errorf("expected 18 bytes, got %d", len(code))
|
||||
// Four register-relative loads/stores compress (2B each) + JALR (4B) = 12.
|
||||
if len(code) != 12 {
|
||||
t.Errorf("expected 12 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_immediate(t *testing.T) {
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·imm(SB), NOSPLIT, $0
|
||||
ADDI X10, $42, X11
|
||||
ANDI X11, $0xFF, X12
|
||||
ORI X12, $1, X13
|
||||
XORI X13, $0, X14
|
||||
ADDI $42, X10, X11
|
||||
ANDI $0xFF, X11, X12
|
||||
ORI $1, X12, X13
|
||||
XORI $0, X13, X14
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 4 I-type + C.JR = 4*4 + 2 = 18
|
||||
if len(code) != 18 {
|
||||
t.Errorf("expected 18 bytes, got %d", len(code))
|
||||
// 4 I-type + JALR = 4*4 + 4 = 20
|
||||
if len(code) != 20 {
|
||||
t.Errorf("expected 20 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_branches(t *testing.T) {
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·br(SB), NOSPLIT, $0
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
loop:
|
||||
BEQ X10, X11, done
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
JMP loop
|
||||
done:
|
||||
RET
|
||||
@@ -122,28 +123,28 @@ done:
|
||||
}
|
||||
|
||||
func TestRISCV_MOV_imm_small(t *testing.T) {
|
||||
// MOV $42, rd → ADDI (fits in 12 bits). Not RVC-compressed (treated as MOV, not ADDI).
|
||||
// MOV $42, rd → ADDI (fits in 12 bits, but not C.LI's 6-bit immediate).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·small(SB), NOSPLIT, $0
|
||||
MOV $42, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// ADDI (4B) + C.JR (2B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d", len(code))
|
||||
// ADDI (4B) + JALR (4B) = 8
|
||||
if len(code) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_MOV_imm_large(t *testing.T) {
|
||||
// MOV $0x12345, rd → LUI + ADDIW (8 bytes total)
|
||||
// MOV $0x12345, rd → C.LUI $18 (2B) + ADDIW $837 (4B).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·large(SB), NOSPLIT, $0
|
||||
MOV $0x12345, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// LUI (4B) + ADDIW (4B) + C.JR (2B) = 10
|
||||
// C.LUI (2B) + ADDIW (4B) + JALR (4B) = 10
|
||||
if len(code) != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", len(code))
|
||||
}
|
||||
@@ -157,9 +158,9 @@ TEXT ·reg(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.MV (2B) + C.JR (2B) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %d (% x)", len(code), code)
|
||||
// C.MV (2B) + JALR (4B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d (% x)", len(code), code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +173,9 @@ TEXT ·frame(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d", len(code))
|
||||
// C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8
|
||||
if len(code) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,9 +188,9 @@ TEXT ·rvcstore(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d (% x)", len(code), code)
|
||||
// C.LDSP (2B) + C.SDSP (2B) + JALR (4B) = 8
|
||||
if len(code) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %d (% x)", len(code), code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,9 +203,9 @@ TEXT ·amo(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 3 AMO instructions (4B each) + C.JR (2B) = 14
|
||||
if len(code) != 14 {
|
||||
t.Errorf("expected 14 bytes, got %d", len(code))
|
||||
// 3 AMO instructions (4B each) + JALR (4B) = 16
|
||||
if len(code) != 16 {
|
||||
t.Errorf("expected 16 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,9 +220,9 @@ TEXT ·fpadd(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 5 FP instructions (4B each) + C.JR (2B) = 22
|
||||
if len(code) != 22 {
|
||||
t.Errorf("expected 22 bytes, got %d (%d)", len(code), len(code))
|
||||
// 5 FP instructions (4B each) + JALR (4B) = 24
|
||||
if len(code) != 24 {
|
||||
t.Errorf("expected 24 bytes, got %d (%d)", len(code), len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,9 +235,9 @@ TEXT ·csrtest(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 3 CSR instructions (4B each) + C.JR (2B) = 14
|
||||
if len(code) != 14 {
|
||||
t.Errorf("expected 14 bytes, got %d", len(code))
|
||||
// 3 CSR instructions (4B each) + JALR (4B) = 16
|
||||
if len(code) != 16 {
|
||||
t.Errorf("expected 16 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,9 +251,9 @@ TEXT ·fmatest(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 4 FMA instructions (4B each) + C.JR (2B) = 18
|
||||
if len(code) != 18 {
|
||||
t.Errorf("expected 18 bytes, got %d", len(code))
|
||||
// 4 FMA instructions (4B each) + JALR (4B) = 20
|
||||
if len(code) != 20 {
|
||||
t.Errorf("expected 20 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,9 +267,9 @@ TEXT ·cvt(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 4 conversion instructions (4B each) + C.JR (2B) = 18
|
||||
if len(code) != 18 {
|
||||
t.Errorf("expected 18 bytes, got %d", len(code))
|
||||
// 4 conversion instructions (4B each) + JALR (4B) = 20
|
||||
if len(code) != 20 {
|
||||
t.Errorf("expected 20 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +282,9 @@ TEXT ·cmp(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 3 FP compare (4B each) + C.JR (2B) = 14
|
||||
if len(code) != 14 {
|
||||
t.Errorf("expected 14 bytes, got %d", len(code))
|
||||
// 3 FP compare (4B each) + JALR (4B) = 16
|
||||
if len(code) != 16 {
|
||||
t.Errorf("expected 16 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,9 +292,9 @@ func TestRISCV_forwardBranch(t *testing.T) {
|
||||
// Forward label reference — must not fail.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·fwd(SB), NOSPLIT, $0
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
BEQ X10, X11, done
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
@@ -308,13 +309,13 @@ func TestRISCV_RVC_ADDI(t *testing.T) {
|
||||
// ADDI where rd=rs1 and small imm → C.ADDI
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·caddi(SB), NOSPLIT, $0
|
||||
ADDI X10, $5, X10
|
||||
ADDI $5, X10, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.ADDI (2B) + C.JR (2B) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %d", len(code))
|
||||
// C.ADDI (2B) + JALR (4B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,13 +323,13 @@ func TestRISCV_RVC_LI(t *testing.T) {
|
||||
// ADDI X0, $imm, rd → C.LI
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·cli(SB), NOSPLIT, $0
|
||||
ADDI X0, $7, X10
|
||||
ADDI $7, X0, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.LI (2B) + C.JR (2B) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %d", len(code))
|
||||
// C.LI (2B) + JALR (4B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +341,9 @@ TEXT ·clui(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.LUI (2B) + C.JR (2B) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %d", len(code))
|
||||
// C.LUI (2B) + JALR (4B) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,13 +369,13 @@ TEXT ·sub(SB), NOSPLIT, $0
|
||||
if len(img.Funcs) != 2 {
|
||||
t.Fatalf("expected 2 functions, got %d", len(img.Funcs))
|
||||
}
|
||||
// func add: C.LDSP(2) + C.JR(2) = 4
|
||||
if img.Funcs[0].Size != 4 {
|
||||
t.Errorf("add: expected 4 bytes, got %d", img.Funcs[0].Size)
|
||||
// func add: C.LDSP(2) + JALR(4) = 6
|
||||
if img.Funcs[0].Size != 6 {
|
||||
t.Errorf("add: expected 6 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
// func sub: SUB(4) + C.JR(2) = 6
|
||||
if img.Funcs[1].Size != 6 {
|
||||
t.Errorf("sub: expected 6 bytes, got %d", img.Funcs[1].Size)
|
||||
// func sub: SUB(4) + JALR(4) = 8
|
||||
if img.Funcs[1].Size != 8 {
|
||||
t.Errorf("sub: expected 8 bytes, got %d", img.Funcs[1].Size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,34 +385,34 @@ func TestRISCV_encodings(t *testing.T) {
|
||||
name, src string
|
||||
wantBytes int
|
||||
}{
|
||||
{"ADD", "ADD X10, X11, X12\nRET\n", 6},
|
||||
{"SUBW", "SUBW X10, X11, X12\nRET\n", 6},
|
||||
{"MUL", "MUL X10, X11, X12\nRET\n", 6},
|
||||
{"DIVW", "DIVW X10, X11, X12\nRET\n", 6},
|
||||
{"REMUW", "REMUW X10, X11, X12\nRET\n", 6},
|
||||
{"ADDIW", "ADDIW X10, $5, X11\nRET\n", 6},
|
||||
{"SLLI", "SLLI X10, $3, X11\nRET\n", 6}, // ADDI+SLLI? No, SLLI uses I-type
|
||||
{"SRLI", "SRLI X10, $2, X11\nRET\n", 6},
|
||||
{"SRAI", "SRAI X10, $1, X11\nRET\n", 6},
|
||||
{"LB", "LB (X10), X11\nRET\n", 6},
|
||||
{"LBU", "LBU (X10), X11\nRET\n", 6},
|
||||
{"LH", "LH (X10), X11\nRET\n", 6},
|
||||
{"LHU", "LHU (X10), X11\nRET\n", 6},
|
||||
{"LWU", "LWU (X10), X11\nRET\n", 6},
|
||||
{"SB", "SB X10, (X11)\nRET\n", 6},
|
||||
{"SH", "SH X10, (X11)\nRET\n", 6},
|
||||
{"ADD", "ADD X10, X11, X12\nRET\n", 8},
|
||||
{"SUBW", "SUBW X10, X11, X12\nRET\n", 8},
|
||||
{"MUL", "MUL X10, X11, X12\nRET\n", 8},
|
||||
{"DIVW", "DIVW X10, X11, X12\nRET\n", 8},
|
||||
{"REMUW", "REMUW X10, X11, X12\nRET\n", 8},
|
||||
{"ADDIW", "ADDIW $5, X10, X11\nRET\n", 8},
|
||||
{"SLLI", "SLLI $3, X10, X11\nRET\n", 8}, // ADDI+SLLI? No, SLLI uses I-type
|
||||
{"SRLI", "SRLI $2, X10, X11\nRET\n", 8},
|
||||
{"SRAI", "SRAI $1, X10, X11\nRET\n", 8},
|
||||
{"LB", "LB (X10), X11\nRET\n", 8},
|
||||
{"LBU", "LBU (X10), X11\nRET\n", 8},
|
||||
{"LH", "LH (X10), X11\nRET\n", 8},
|
||||
{"LHU", "LHU (X10), X11\nRET\n", 8},
|
||||
{"LWU", "LWU (X10), X11\nRET\n", 8},
|
||||
{"SB", "SB X10, (X11)\nRET\n", 8},
|
||||
{"SH", "SH X10, (X11)\nRET\n", 8},
|
||||
{"SW", "SW X10, (X11)\nRET\n", 6},
|
||||
{"LUI", "LUI X10, $0x12345\nRET\n", 6},
|
||||
{"AUIPC", "AUIPC X10, $0\nRET\n", 6},
|
||||
{"FLW", "FLW (X10), F10\nRET\n", 6},
|
||||
{"FSW", "FSW F10, (X11)\nRET\n", 6},
|
||||
{"FADDS", "FADDS F10, F11, F12\nRET\n", 6},
|
||||
{"FMINS", "FMINS F10, F11, F12\nRET\n", 6},
|
||||
{"FMAXD", "FMAXD F10, F11, F12\nRET\n", 6},
|
||||
{"FCVTSD", "FCVTSD F10, F11\nRET\n", 6},
|
||||
{"FCVTDS", "FCVTDS F10, F11\nRET\n", 6},
|
||||
{"FMVXW", "FMVXW F10, X10\nRET\n", 6},
|
||||
{"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 6},
|
||||
{"LUI", "LUI X10, $0x12345\nRET\n", 8},
|
||||
{"AUIPC", "AUIPC X10, $0\nRET\n", 8},
|
||||
{"FLW", "FLW (X10), F10\nRET\n", 8},
|
||||
{"FSW", "FSW F10, (X11)\nRET\n", 8},
|
||||
{"FADDS", "FADDS F10, F11, F12\nRET\n", 8},
|
||||
{"FMINS", "FMINS F10, F11, F12\nRET\n", 8},
|
||||
{"FMAXD", "FMAXD F10, F11, F12\nRET\n", 8},
|
||||
{"FCVTSD", "FCVTSD F10, F11\nRET\n", 8},
|
||||
{"FCVTDS", "FCVTDS F10, F11\nRET\n", 8},
|
||||
{"FMVXW", "FMVXW F10, X10\nRET\n", 8},
|
||||
{"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 8},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -428,24 +429,24 @@ TEXT ·`+tt.name+`(SB), NOSPLIT, $0
|
||||
}
|
||||
|
||||
func TestRISCV_RVC_branch(t *testing.T) {
|
||||
// BEQ rs, X0, target → C.BEQZ when rs is in prime regs and offset fits.
|
||||
// Branches are never RVC-compressed (no C.BEQZ/C.BNEZ), matching go tool asm.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·cbeqz(SB), NOSPLIT, $0
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
BEQ X10, X0, done
|
||||
ADDI X10, $1, X10
|
||||
ADDI $1, X10, X10
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.ADDI(2) + C.BEQZ(2) + C.ADDI(2) + C.JR(2) = 8 (all compress)
|
||||
if len(code) != 8 {
|
||||
t.Errorf("expected 8 bytes with C.BEQZ, got %d", len(code))
|
||||
// C.ADDI(2) + BEQ(4) + C.ADDI(2) + JALR(4) = 12
|
||||
if len(code) != 12 {
|
||||
t.Errorf("expected 12 bytes with uncompressed BEQ, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_RVC_CJ(t *testing.T) {
|
||||
// JMP target → C.J when offset fits.
|
||||
// JMP target → JAL X0 (never compressed to C.J), matching go tool asm.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·cj(SB), NOSPLIT, $0
|
||||
JMP done
|
||||
@@ -453,9 +454,9 @@ func TestRISCV_RVC_CJ(t *testing.T) {
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.J(2) + C.JR(2) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.J, got %d", len(code))
|
||||
// JAL(4) + JALR(4) = 8
|
||||
if len(code) != 8 {
|
||||
t.Errorf("expected 8 bytes with uncompressed JMP, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,9 +468,9 @@ func TestRISCV_RVC_CADD(t *testing.T) {
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.ADD(2) + C.JR(2) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.ADD, got %d", len(code))
|
||||
// C.ADD(2) + JALR(4) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.ADD, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,35 +482,23 @@ func TestRISCV_RVC_CADD_commute(t *testing.T) {
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.ADD(2) + C.JR(2) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.ADD (commuted), got %d", len(code))
|
||||
// C.ADD(2) + JALR(4) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.ADD (commuted), got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_RVC_CSUB(t *testing.T) {
|
||||
// SUB where rd==rs1 and both in prime regs → C.SUB.
|
||||
// SUB rs2, rs1, rd → C.SUB when rd == rs1 and both in prime regs.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·csub(SB), NOSPLIT, $0
|
||||
SUB X11, X10, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// SUB X11,X10,X10 → rd=X10, rs1=X11 ≠ rd → no C.SUB.
|
||||
// Plan9: INSTR src1, src2, dst. For C.SUB: rd must equal rs1.
|
||||
// So: SUB X10, X11, X10 → rd=10, rs1=10, rs2=11 ✓
|
||||
if len(code) == 4 {
|
||||
return // compressed
|
||||
}
|
||||
// Try with correct operand order.
|
||||
fn2 := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·csub2(SB), NOSPLIT, $0
|
||||
SUB X10, X11, X10
|
||||
RET
|
||||
`)
|
||||
code2 := assembleRISCVHelper(t, fn2)
|
||||
if len(code2) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.SUB, got %d (% x)", len(code2), code2)
|
||||
// SUB X11, X10, X10 → rs2=X11, rs1=X10, rd=X10; rd==rs1 → C.SUB (2B) + JALR (4B) = 6.
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.SUB, got %d (% x)", len(code), code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,8 +509,8 @@ TEXT ·cxor(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.XOR, got %d", len(code))
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.XOR, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,8 +521,8 @@ TEXT ·cor(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.OR, got %d", len(code))
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.OR, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,8 +533,8 @@ TEXT ·cand(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.AND, got %d", len(code))
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.AND, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,9 +545,9 @@ TEXT ·cfldsp(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.FLDSP(2) + C.JR(2) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.FLDSP, got %d", len(code))
|
||||
// C.FLDSP(2) + JALR(4) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.FLDSP, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,9 +558,9 @@ TEXT ·cfsdsp(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// C.FSDSP(2) + C.JR(2) = 4
|
||||
if len(code) != 4 {
|
||||
t.Errorf("expected 4 bytes with C.FSDSP, got %d", len(code))
|
||||
// C.FSDSP(2) + JALR(4) = 6
|
||||
if len(code) != 6 {
|
||||
t.Errorf("expected 6 bytes with C.FSDSP, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,9 +581,9 @@ DATA answer<>+0(SB)/8, $42
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC(4) + ADDI(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
// AUIPC(4) + ADDI(4) + JALR(4) = 12
|
||||
if img.Funcs[0].Size != 12 {
|
||||
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,9 +603,9 @@ GLOBL result<>(SB), NOPTR, $8
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC X31(4) + SD X10,0(X31)(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
// AUIPC X31(4) + SD X10,0(X31)(4) + JALR(4) = 12
|
||||
if img.Funcs[0].Size != 12 {
|
||||
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,9 +685,9 @@ DATA answer<>+0(SB)/8, $42
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
// AUIPC(4) + LD(4) + C.JR(2) = 10
|
||||
if img.Funcs[0].Size != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", img.Funcs[0].Size)
|
||||
// AUIPC(4) + LD(4) + JALR(4) = 12
|
||||
if img.Funcs[0].Size != 12 {
|
||||
t.Errorf("expected 12 bytes, got %d", img.Funcs[0].Size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +701,7 @@ TEXT ·sys(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// 3 system instructions × 4 bytes + C.JR(2) = 14
|
||||
// FENCE(4) + ECALL(4) + C.EBREAK(2) + JALR(4) = 14
|
||||
if len(code) != 14 {
|
||||
t.Errorf("expected 14 bytes, got %d (% x)", len(code), code)
|
||||
}
|
||||
@@ -725,25 +714,50 @@ TEXT ·badfp(SB), NOSPLIT, $0
|
||||
MOV $arg(FP), X10
|
||||
RET
|
||||
`)
|
||||
_, _, _, err := assembleRISCV(fn)
|
||||
_, _, _, _, _, err := assembleRISCV(fn)
|
||||
if err == nil {
|
||||
t.Error("expected error for MOV $arg(FP), got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_CALL(t *testing.T) {
|
||||
// CALL target → AUIPC + JALR (8 bytes).
|
||||
// CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL relocation.
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·calltest(SB), NOSPLIT, $0
|
||||
CALL sub
|
||||
done:
|
||||
CALL ext(SB)
|
||||
RET
|
||||
`)
|
||||
code, _, relocs, _, _, err := assembleRISCV(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
// prologue (8) + JAL (4) + epilogue+JALR (8) = 20
|
||||
if len(code) != 20 {
|
||||
t.Fatalf("expected 20 bytes with CALL sym(SB), got %d", len(code))
|
||||
}
|
||||
if len(relocs) != 1 {
|
||||
t.Fatalf("relocs = %d, want 1", len(relocs))
|
||||
}
|
||||
r := relocs[0]
|
||||
if r.Kind != RelRISCVJal || r.Name != "ext" || r.Off != 8 || r.After != 12 || r.Addend != 0 {
|
||||
t.Errorf("reloc = {kind %v off %d after %d name %q addend %d}", r.Kind, r.Off, r.After, r.Name, r.Addend)
|
||||
}
|
||||
// The JAL instruction itself is JAL X1, 0 at function offset 8.
|
||||
wantJAL := wordLE(riscvJType(1, 0))
|
||||
if !bytes.Equal(code[8:12], wantJAL) {
|
||||
t.Errorf("JAL = % x, want % x", code[8:12], wantJAL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRISCV_CALL_local_error(t *testing.T) {
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·calllocal(SB), NOSPLIT, $0
|
||||
CALL sub
|
||||
sub:
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// CALL(8) + C.JR(2) + C.JR(2) = 12
|
||||
if len(code) != 12 {
|
||||
t.Errorf("expected 12 bytes with CALL, got %d", len(code))
|
||||
_, _, _, _, _, err := assembleRISCV(fn)
|
||||
if err == nil {
|
||||
t.Error("expected error for CALL to local label, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
+149
-84
@@ -3,115 +3,180 @@
|
||||
|
||||
package asm
|
||||
|
||||
import "sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
// RISC-V frame mapping: translates Go's FP/SP pseudo-register addressing
|
||||
// into real RISC-V memory accesses.
|
||||
//
|
||||
// In Go's ABI0 (used by assembly functions), arguments are passed on the
|
||||
// stack. At function entry the return address sits at SP, so the frame
|
||||
// pointer FP == SP+8 and the first argument is at FP+0 == SP+8.
|
||||
//
|
||||
// On RISC-V the hardware registers are:
|
||||
// SP = X2 (stack pointer)
|
||||
// FP = S0 = X8 (frame pointer, by convention)
|
||||
//
|
||||
// For NOSPLIT $0 functions the prologue is omitted and arguments are read
|
||||
// directly from SP+8+offset.
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// riscvFrameInfo holds the frame parameters computed from a TEXT directive.
|
||||
// RISC-V frame mapping, matching the Go toolchain's riscv64 backend.
|
||||
//
|
||||
// Go's riscv64 functions have no hardware frame pointer: FP and SP are
|
||||
// synthetic registers resolved against the hardware stack pointer (X2) and
|
||||
// the frame size. The return address lives in the link register (X1, RA/LR).
|
||||
//
|
||||
// The autosize is the real stack adjustment: the declared local frame plus
|
||||
// the 8 bytes for the saved link register (the toolchain's FixedFrameSize).
|
||||
// A leaf function with a zero frame gets no prologue at all.
|
||||
//
|
||||
// Prologue (autosize > 0), byte-identical to the toolchain:
|
||||
//
|
||||
// MOV LR, -autosize(SP) // save LR below the new SP (traceback-safe)
|
||||
// ADDI $-autosize, SP, SP // open the frame
|
||||
// MOV LR, 0(SP) // save LR again at SP (signal-safety)
|
||||
//
|
||||
// Epilogue (autosize > 0): MOV 0(SP), LR; ADDI $autosize, SP, SP; the RET's
|
||||
// uncompressed JALR X0, 0(X1) follows. The toolchain restores LR on every
|
||||
// frame, leaf or not.
|
||||
|
||||
// riscvFrameInfo holds the frame layout derived from a TEXT directive.
|
||||
type riscvFrameInfo struct {
|
||||
frameSize int // the $framesize from TEXT
|
||||
argsSize int // the -argsize from TEXT
|
||||
noSplit bool // the NOSPLIT flag
|
||||
autosize int // the real SP adjustment (locals + saved LR)
|
||||
}
|
||||
|
||||
// riscvComputeFrame extracts frame information from a TEXT directive.
|
||||
// riscvComputeFrame derives the frame layout for a TEXT function.
|
||||
func riscvComputeFrame(t *ast.Text) riscvFrameInfo {
|
||||
fi := riscvFrameInfo{}
|
||||
fi.frameSize = frameSize(t)
|
||||
fi.argsSize = argsSize(t)
|
||||
for _, f := range t.Flags {
|
||||
if f == "NOSPLIT" {
|
||||
fi.noSplit = true
|
||||
frame := frameSize(t)
|
||||
if frame != 0 || !riscvIsLeaf(t) {
|
||||
// FixedFrameSize = 8: space for the saved link register. A
|
||||
// zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||
return riscvFrameInfo{autosize: frame + 8}
|
||||
}
|
||||
return riscvFrameInfo{}
|
||||
}
|
||||
|
||||
// riscvIsLeaf reports whether a function contains no call instructions.
|
||||
// CALL always links; JAL/JALR link only when their destination register is
|
||||
// the link register (X1), matching cmd/internal/obj/riscv's containsCall.
|
||||
func riscvIsLeaf(t *ast.Text) bool {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "CALL":
|
||||
return false
|
||||
case "JAL":
|
||||
// JAL rd, target — a call only when rd is the link register.
|
||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[0]) == 1 {
|
||||
return false
|
||||
}
|
||||
case "JALR":
|
||||
// JALR rs1, rd — a call when rd is X1; JALR offset(rs1) always
|
||||
// links to X1.
|
||||
if len(in.Operands) == 1 {
|
||||
return false
|
||||
}
|
||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[1]) == 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return fi
|
||||
return true
|
||||
}
|
||||
|
||||
// riscvPrologue returns the prologue bytes for a RISC-V function.
|
||||
// For NOSPLIT $0 functions there is no prologue. For functions with a
|
||||
// frame, we emit: ADDI SP, SP, -framesize; SD S0, (framesize-8)(SP); ...
|
||||
// riscvPrologue returns the prologue bytes for a RISC-V function, matching
|
||||
// the toolchain's compression: the SP adjustment compresses to C.ADDI when
|
||||
// the immediate fits, and the second LR save compresses to C.SDSP.
|
||||
func riscvPrologue(fi riscvFrameInfo) []byte {
|
||||
if fi.noSplit && fi.frameSize == 0 {
|
||||
return nil // no prologue for NOSPLIT $0
|
||||
}
|
||||
var out []byte
|
||||
if fi.frameSize > 0 {
|
||||
// ADDI SP, SP, -framesize
|
||||
out = append(out, riscvITypeLE(0x13, 0x0, 2, 2, int32(-fi.frameSize))...)
|
||||
// Save the frame pointer (S0 = X8) at the top of the new frame.
|
||||
// SD S0, (framesize-8)(SP)
|
||||
out = append(out, riscvSTypeLE(0x23, 0x3, 2, 8, int32(fi.frameSize-8))...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// riscvEpilogue returns the epilogue bytes for a RISC-V function.
|
||||
func riscvEpilogue(fi riscvFrameInfo) []byte {
|
||||
if fi.noSplit && fi.frameSize == 0 {
|
||||
if fi.autosize == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []byte
|
||||
if fi.frameSize > 0 {
|
||||
// Restore the frame pointer: LD S0, (framesize-8)(SP)
|
||||
out = append(out, riscvITypeLE(0x03, 0x3, 8, 2, int32(fi.frameSize-8))...)
|
||||
// ADDI SP, SP, framesize
|
||||
out = append(out, riscvITypeLE(0x13, 0x0, 2, 2, int32(fi.frameSize))...)
|
||||
}
|
||||
// MOV LR, -autosize(SP) — SD X1, -autosize(X2). The negative offset is
|
||||
// not compressible to C.SDSP (unsigned), so it stays 4 bytes.
|
||||
out = append(out, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 2, 1, int32(-fi.autosize)))...)
|
||||
// ADDI $-autosize, SP, SP — open the frame (C.ADDI when it fits).
|
||||
out = append(out, riscvSPAdjust(int32(-fi.autosize))...)
|
||||
// MOV LR, 0(SP) — SD X1, 0(X2) → C.SDSP X1, 0.
|
||||
c := rvcSSP(0x7, 1, 0)
|
||||
out = append(out, byte(c), byte(c>>8))
|
||||
return out
|
||||
}
|
||||
|
||||
// riscvReturn returns the bytes for a RET: the epilogue (restore LR and
|
||||
// deallocate the frame when present) followed by the uncompressed JALR X0,
|
||||
// 0(X1) the toolchain emits for RET (it never compresses RET to C.JR).
|
||||
func riscvReturn(fi riscvFrameInfo) []byte {
|
||||
var out []byte
|
||||
if fi.autosize != 0 {
|
||||
// MOV 0(SP), LR — LD X1, 0(X2) → C.LDSP X1, 0.
|
||||
c := rvcLSP(0x3, 1, 0)
|
||||
out = append(out, byte(c), byte(c>>8))
|
||||
// ADDI $autosize, SP, SP — close the frame (C.ADDI when it fits).
|
||||
out = append(out, riscvSPAdjust(int32(fi.autosize))...)
|
||||
}
|
||||
// JALR X0, 0(X1).
|
||||
return append(out, wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0))...)
|
||||
}
|
||||
|
||||
// riscvSPAdjust emits an ADDI rd, imm, rd for the stack pointer (rd = rs1 =
|
||||
// X2), compressed to C.ADDI16SP when the immediate is a nonzero 16-byte
|
||||
// multiple, else C.ADDI when it fits 6-bit signed.
|
||||
func riscvSPAdjust(imm int32) []byte {
|
||||
if imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
|
||||
c := rvcADDI16SP(2, imm)
|
||||
return []byte{byte(c), byte(c >> 8)}
|
||||
}
|
||||
if riscvFitsCAddi(imm) {
|
||||
c := rvcCI(0x0, 2, uint32(imm)&0x3F)
|
||||
return []byte{byte(c), byte(c >> 8)}
|
||||
}
|
||||
return wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 2, 2, imm))
|
||||
}
|
||||
|
||||
// riscvFitsCAddi reports whether imm compresses to C.ADDI (a nonzero 6-bit
|
||||
// signed immediate).
|
||||
func riscvFitsCAddi(imm int32) bool {
|
||||
return imm != 0 && imm >= -32 && imm <= 31
|
||||
}
|
||||
|
||||
// riscvPrologueSpadjPC returns the function-relative byte offset where the
|
||||
// prologue has finished decrementing SP (the delta becomes autosize).
|
||||
func riscvPrologueSpadjPC(fi riscvFrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
// SD (4 bytes) + ADDI/C.ADDI (2 or 4 bytes).
|
||||
return 4 + riscvSPAdjustLen(int32(-fi.autosize))
|
||||
}
|
||||
|
||||
// riscvReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
||||
// (but not including) the final JALR — the point where SP is restored.
|
||||
func riscvReturnEpilogueLen(fi riscvFrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
// C.LDSP (2 bytes) + ADDI/C.ADDI (2 or 4 bytes).
|
||||
return 2 + riscvSPAdjustLen(int32(fi.autosize))
|
||||
}
|
||||
|
||||
func riscvSPAdjustLen(imm int32) int {
|
||||
if imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
|
||||
return 2
|
||||
}
|
||||
if riscvFitsCAddi(imm) {
|
||||
return 2
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
// riscvResolvePseudo translates a pseudo-register memory reference into a
|
||||
// real base register and offset. It handles name+offset(FP) and
|
||||
// name+offset(SP).
|
||||
//
|
||||
// Returns the base register number and the adjusted offset.
|
||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||
// x+N(SP) → (N + autosize)(SP). Returns base = -1 for an unresolvable
|
||||
// reference (SB: static data, handled by the relocation path).
|
||||
func riscvResolvePseudo(sym *ast.Symbol, fi riscvFrameInfo) (base int, off int32) {
|
||||
if sym == nil {
|
||||
return -1, 0
|
||||
}
|
||||
offset := int32(sym.Offset)
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
// FP == SP+8 for NOSPLIT $0; arguments are at SP+8+offset.
|
||||
if fi.noSplit && fi.frameSize == 0 {
|
||||
return 2, 8 + offset // SP + 8 + argOffset
|
||||
}
|
||||
// With a frame, FP points to the saved frame; args are at FP+offset.
|
||||
return 8, offset // S0 + argOffset
|
||||
return 2, int32(sym.Offset) + int32(fi.autosize) + 8
|
||||
case "SP":
|
||||
// SP-relative; the offset is from the current SP.
|
||||
return 2, offset
|
||||
return 2, int32(fi.autosize) + int32(sym.Offset)
|
||||
case "SB":
|
||||
// Static data reference — needs a relocation (not yet supported).
|
||||
return -1, offset
|
||||
default:
|
||||
return -1, offset
|
||||
return -1, int32(sym.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
// riscvITypeLE encodes an I-type instruction and returns little-endian bytes.
|
||||
func riscvITypeLE(opcode, funct3 uint32, rd, rs1 int, imm int32) []byte {
|
||||
word := (uint32(imm&0xFFF) << 20) | (uint32(rs1) << 15) |
|
||||
(funct3 << 12) | (uint32(rd) << 7) | opcode
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||
}
|
||||
|
||||
// riscvSTypeLE encodes an S-type instruction and returns little-endian bytes.
|
||||
func riscvSTypeLE(opcode, funct3 uint32, rs1, rs2 int, imm int32) []byte {
|
||||
immU := uint32(imm) & 0xFFF
|
||||
word := ((immU >> 5) << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) |
|
||||
(funct3 << 12) | ((immU & 0x1F) << 7) | opcode
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestRISCVFrameSpadjAndLines checks that a framed function records its
|
||||
// stack-adjustment boundaries and source-line table, the inputs the GOOBJ
|
||||
// emitter turns into the pcsp/pcfile/pcline tables.
|
||||
func TestRISCVFrameSpadjAndLines(t *testing.T) {
|
||||
f, errs := parser.Parse("frame_riscv64.s", `#include "textflag.h"
|
||||
|
||||
TEXT ·framed(SB), NOSPLIT, $16-16
|
||||
MOV a+0(FP), X10
|
||||
MOV b+8(FP), X11
|
||||
ADD X11, X10, X10
|
||||
MOV X10, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 24 {
|
||||
t.Fatalf("size = %d, want 24", fn.Size)
|
||||
}
|
||||
|
||||
// autosize = 16 + 8 = 24; the prologue boundary is just past its C.ADDI
|
||||
// (SD 4 + C.ADDI 2 = 6), and the RET restores SP just past its C.ADDI
|
||||
// (RET starts at 16; C.LDSP 2 + C.ADDI 2 = 20).
|
||||
wantSpadj := []SpadjStep{{PC: 6, Value: 24}, {PC: 20, Value: 0}}
|
||||
if len(fn.Spadj) != len(wantSpadj) {
|
||||
t.Fatalf("spadj = %v, want %v", fn.Spadj, wantSpadj)
|
||||
}
|
||||
for i := range wantSpadj {
|
||||
if fn.Spadj[i] != wantSpadj[i] {
|
||||
t.Errorf("spadj[%d] = %v, want %v", i, fn.Spadj[i], wantSpadj[i])
|
||||
}
|
||||
}
|
||||
|
||||
// One line entry per instruction, in emission order.
|
||||
wantLines := []LineEntry{
|
||||
{Offset: 8, Line: 4},
|
||||
{Offset: 10, Line: 5},
|
||||
{Offset: 12, Line: 6},
|
||||
{Offset: 14, Line: 7},
|
||||
{Offset: 16, Line: 8},
|
||||
}
|
||||
if len(fn.Lines) != len(wantLines) {
|
||||
t.Fatalf("lines = %v, want %v", fn.Lines, wantLines)
|
||||
}
|
||||
for i := range wantLines {
|
||||
if fn.Lines[i] != wantLines[i] {
|
||||
t.Errorf("lines[%d] = %v, want %v", i, fn.Lines[i], wantLines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRISCVRegAliases checks the Go ABI register aliases that the toolchain
|
||||
// defines: LR is the link register (X1) and TMP is the assembler scratch
|
||||
// register (X31/T6).
|
||||
func TestRISCVRegAliases(t *testing.T) {
|
||||
for name, want := range map[string]int{
|
||||
"X1": 1, "RA": 1, "LR": 1,
|
||||
"X31": 31, "T6": 31, "TMP": 31,
|
||||
"X2": 2, "SP": 2,
|
||||
} {
|
||||
if got := riscvRegNum(name); got != want {
|
||||
t.Errorf("riscvRegNum(%q) = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectRISCVCallReloc checks that CALL sym(SB) emits a single JAL
|
||||
// instruction carrying an R_RISCV_JAL relocation (4-byte field) in both the
|
||||
// GOOBJ and ELF object emitters.
|
||||
func TestGOObjectRISCVCallReloc(t *testing.T) {
|
||||
f, errs := parser.Parse("k_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·c(SB), NOSPLIT, $0-0
|
||||
CALL callee<>(SB)
|
||||
RET
|
||||
|
||||
GLOBL callee<>(SB), RODATA, $8
|
||||
DATA callee<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if len(fn.Relocs) != 1 {
|
||||
t.Fatalf("relocs = %d, want 1", len(fn.Relocs))
|
||||
}
|
||||
r := fn.Relocs[0]
|
||||
if r.Kind != RelRISCVJal || r.Off != 8 || r.After != 12 || r.Name != "callee" || r.Addend != 0 || r.External {
|
||||
t.Errorf("reloc = {kind %v off %d after %d name %q addend %d external %v}", r.Kind, r.Off, r.After, r.Name, r.Addend, r.External)
|
||||
}
|
||||
|
||||
obj, err := img.GOObjectRISCV("testpkg", "k_riscv64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectRISCV: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
relocIdx := v.blk(blkRelocIdx)
|
||||
relocs := v.blk(blkReloc)
|
||||
// The function is the last non-package symbol: 4 package defs, then the
|
||||
// 4 pc tables and the function.
|
||||
first := int(binary.LittleEndian.Uint32(relocIdx[(4+4)*4:]))
|
||||
if (first+1)*23 > len(relocs) {
|
||||
t.Fatalf("reloc block too short: first=%d len=%d", first, len(relocs))
|
||||
}
|
||||
e := relocs[first*23:]
|
||||
le := binary.LittleEndian
|
||||
if int32(le.Uint32(e[0:])) != 8 || e[4] != 4 || le.Uint16(e[5:]) != relocRISCVJal || le.Uint32(e[15:]) != pkgIdxSelf || le.Uint32(e[19:]) != 0 {
|
||||
t.Errorf("GOOBJ reloc = off %d size %d type %d pkg %d sym %d", int32(le.Uint32(e[0:])), e[4], le.Uint16(e[5:]), le.Uint32(e[15:]), le.Uint32(e[19:]))
|
||||
}
|
||||
|
||||
// The ELF object must carry a single R_RISCV_JAL relocation in .rela.text.
|
||||
elfObj, err := img.ELFRISCVObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFRISCVObject: %v", err)
|
||||
}
|
||||
if !hasELFRISCVJAL(t, elfObj) {
|
||||
t.Error("ELF object missing R_RISCV_JAL relocation")
|
||||
}
|
||||
}
|
||||
func TestGOObjectRISCVStructure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·sb(SB), NOSPLIT, $0-0
|
||||
MOV $answer<>(SB), X10
|
||||
MOV answer<>(SB), X11
|
||||
MOV X12, answer<>(SB)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 28 {
|
||||
t.Fatalf("function size = %d, want 28", fn.Size)
|
||||
}
|
||||
if len(fn.Relocs) != 3 {
|
||||
t.Fatalf("relocs = %d, want 3", len(fn.Relocs))
|
||||
}
|
||||
wantKind := []RelocKind{RelRISCVPCRELIType, RelRISCVPCRELIType, RelRISCVPCRELSType}
|
||||
wantOff := []int{0, 8, 16}
|
||||
for i, r := range fn.Relocs {
|
||||
if r.Kind != wantKind[i] || r.Off != wantOff[i] || r.After != r.Off+8 || r.Name != "answer" || r.Addend != 0 {
|
||||
t.Errorf("reloc %d = {kind %v off %d after %d name %q addend %d}", i, r.Kind, r.Off, r.After, r.Name, r.Addend)
|
||||
}
|
||||
}
|
||||
|
||||
obj, err := img.GOObjectRISCV("testpkg", "k_riscv64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectRISCV: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
|
||||
// Package defs: the static GLOBL, the FuncInfo, then the two DWARF
|
||||
// symbols.
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 4 {
|
||||
t.Fatalf("symdefs = %d, want 4", len(defs))
|
||||
}
|
||||
if defs[0].name != "answer" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 8 {
|
||||
t.Errorf("answer symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[2].typ != kindSDWARFLINES || defs[3].typ != kindSDWARFFCN {
|
||||
t.Errorf("dwarf symbols = %+v, %+v", defs[2], defs[3])
|
||||
}
|
||||
|
||||
// The three code relocations, in definition order: ITYPE, ITYPE, STYPE,
|
||||
// each 8 bytes wide against the GLOBL (package symbol 0).
|
||||
relocIdx := v.blk(blkRelocIdx)
|
||||
relocs := v.blk(blkReloc)
|
||||
if len(relocs) != 5*23 {
|
||||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||||
}
|
||||
// The function is the last non-package symbol; its relocs start after
|
||||
// the DWARF symbols' (defs 2 and 3 each carry one).
|
||||
le := binary.LittleEndian
|
||||
first := int(le.Uint32(relocIdx[4*(4+4):]))
|
||||
wantType := []uint16{relocRISCVPcrelItype, relocRISCVPcrelItype, relocRISCVPcrelStype}
|
||||
wantOffAbs := []int{0, 8, 16}
|
||||
for i := range 3 {
|
||||
e := relocs[(first+i)*23:]
|
||||
if int32(le.Uint32(e[0:])) != int32(wantOffAbs[i]) || e[4] != 8 || le.Uint16(e[5:]) != wantType[i] ||
|
||||
le.Uint32(e[15:]) != pkgIdxSelf || le.Uint32(e[19:]) != 0 {
|
||||
t.Errorf("reloc %d = off %d size %d type %d pkg %d sym %d", i, int32(le.Uint32(e[0:])), e[4], le.Uint16(e[5:]), le.Uint32(e[15:]), le.Uint32(e[19:]))
|
||||
}
|
||||
}
|
||||
|
||||
// The function code: three AUIPC+second-instruction pairs with zero
|
||||
// immediates, then the uncompressed JALR X0, 0(X1) the toolchain emits
|
||||
// for RET.
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
want := append(wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 10, 0)), wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 10, 10, 0))...)
|
||||
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 11, 0))...)
|
||||
want = append(want, wordLE(riscvIType(riscvEnc{0x03, 0x3, 0x00}, 11, 11, 0))...)
|
||||
want = append(want, wordLE(riscvUType(riscvEnc{0x17, 0x0, 0x00}, 31, 0))...)
|
||||
want = append(want, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 31, 12, 0))...)
|
||||
want = append(want, 0x67, 0x80, 0x00, 0x00) // JALR X0, 0(X1)
|
||||
if !bytes.Equal(code, want) {
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
|
||||
// The same bytes must survive into the object's data block intact: the
|
||||
// linker patches only the immediate fields of the AUIPC pairs, so the
|
||||
// opcode/register bits of every instruction must not be zeroed.
|
||||
dataIdx := v.blk(blkDataIdx)
|
||||
dataBlk := v.blk(blkData)
|
||||
dOff := int(le.Uint32(dataIdx[8*4:])) // the function is the last symbol
|
||||
emitted := dataBlk[dOff : dOff+fn.Size]
|
||||
if !bytes.Equal(emitted, want) {
|
||||
t.Errorf("emitted data = % x\nwant % x", emitted, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectRISCVLink cross-compiles a Go program with the gasm-produced
|
||||
// object substituted into the package archive, proving cmd/link accepts the
|
||||
// emitted RISC-V GOOBJ. The binary is not executed (no riscv64 host or
|
||||
// qemu). Skipped when no Go toolchain is available.
|
||||
func TestGOObjectRISCVLink(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOV a+0(FP), X10
|
||||
MOV b+8(FP), X11
|
||||
ADD X11, X10, X10
|
||||
MOV X10, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_riscv64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func add(a, b int64) int64
|
||||
|
||||
func main() {
|
||||
if add(20, 22) != 42 {
|
||||
panic("bad add")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module rvlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var pkgArch, work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_riscv64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if pkgArch == "" || linkLine == "" || asmObj == "" {
|
||||
t.Skip("could not locate the archive, asm output or link line in the build log")
|
||||
}
|
||||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||||
asmMember := filepath.Base(strings.ReplaceAll(asmObj, "$WORK", work))
|
||||
|
||||
pf, perrs := parser.Parse(filepath.Join(dir, "main_riscv64.s"), asmSrc)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs)
|
||||
}
|
||||
pimg, err := AssembleFileRISCV(pf)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
obj, err := pimg.GOObjectRISCV("main", filepath.Join(dir, "main_riscv64.s"))
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectRISCV: %v", err)
|
||||
}
|
||||
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
extract.Dir = membersDir
|
||||
extract.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
member := filepath.Join(membersDir, asmMember)
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||||
listCmd.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, filepath.Join(membersDir, m))
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
pack.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)), "GOARCH=riscv64")
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
nm := exec.Command(goBin, "tool", "nm", filepath.Join(dir, "app2"))
|
||||
nm.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
nmOut, err := nm.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("nm gasm-linked binary: %v\n%s", err, nmOut)
|
||||
}
|
||||
if !strings.Contains(string(nmOut), "main.add") {
|
||||
t.Errorf("main.add not found in linked binary:\n%s", nmOut)
|
||||
}
|
||||
}
|
||||
|
||||
// hasELFRISCVJAL reports whether the ELF object carries an R_RISCV_JAL
|
||||
// relocation in its .rela.text section.
|
||||
func hasELFRISCVJAL(t *testing.T, data []byte) bool {
|
||||
t.Helper()
|
||||
f, err := elf.NewFile(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatalf("parse ELF: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
rela := f.Section(".rela.text")
|
||||
if rela == nil {
|
||||
return false
|
||||
}
|
||||
b, err := rela.Data()
|
||||
if err != nil {
|
||||
t.Fatalf(".rela.text data: %v", err)
|
||||
}
|
||||
const rRISCVJAL = 17
|
||||
for i := 0; i+24 <= len(b); i += 24 {
|
||||
info := binary.LittleEndian.Uint64(b[i+8:])
|
||||
if uint32(info) == rRISCVJAL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -141,6 +141,8 @@ var vexTable = map[string]vexSpec{
|
||||
"VPMOVZXWQ": {2, 0x34, 0, 1, -1, vexRM},
|
||||
"VPBROADCASTD": {2, 0x58, 0, 1, -1, vexRM},
|
||||
"VPBROADCASTQ": {2, 0x59, 0, 1, -1, vexRM},
|
||||
"VPBROADCASTB": {2, 0x78, 0, 1, -1, vexRM},
|
||||
"VPBROADCASTW": {2, 0x79, 0, 1, -1, vexRM},
|
||||
// VEX.128/256.F3.0F.WIG — signed dword to packed double conversion
|
||||
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
||||
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM},
|
||||
|
||||
+1
-3
@@ -123,10 +123,8 @@ type Symbol struct {
|
||||
// OpKind classifies an operand syntactically.
|
||||
type OpKind int
|
||||
|
||||
// Operand kinds.
|
||||
const (
|
||||
OpInvalid OpKind = iota
|
||||
OpImmediate // $value
|
||||
OpImmediate = iota // $value
|
||||
OpAddr // register, memory reference, symbol or label
|
||||
)
|
||||
|
||||
|
||||
+2
-2
@@ -54,11 +54,11 @@ func TestStmtPositions(t *testing.T) {
|
||||
// TestInterfaces confirms the node types satisfy their interfaces, so callers
|
||||
// can range over Decls and Stmts.
|
||||
func TestInterfaces(t *testing.T) {
|
||||
var decls []Decl = []Decl{&Include{}, &Preproc{}, &Text{}, &Globl{}, &Data{}}
|
||||
var decls = []Decl{&Include{}, &Preproc{}, &Text{}, &Globl{}, &Data{}}
|
||||
if len(decls) != 5 {
|
||||
t.Fatal("decl interface set")
|
||||
}
|
||||
var stmts []Stmt = []Stmt{&Label{}, &Instr{}}
|
||||
var stmts = []Stmt{&Label{}, &Instr{}}
|
||||
if len(stmts) != 2 {
|
||||
t.Fatal("stmt interface set")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
||||
// own assembler, probed black-box: every mnemonic in the gasm table is offered
|
||||
// to go tool asm in its bare form, and a mnemonic counts as known to Go when
|
||||
// the error is anything but "unrecognized instruction" (a wrong-shape error
|
||||
// still proves the mnemonic exists in Go's tables). The audit answers three
|
||||
// questions at a glance:
|
||||
//
|
||||
// - which mnemonics gasm can encode that go tool asm does not know
|
||||
// (superset encodings, usable only through the gasm goobj path);
|
||||
// - which mnemonics the architecture table knows but the encoder cannot
|
||||
// emit yet (the implementation backlog);
|
||||
// - which mnemonics go tool asm knows that gasm cannot encode (feature
|
||||
// gaps).
|
||||
//
|
||||
// The amd64 derived families (Jcc, CMOVcc, SETcc) exist on both sides by
|
||||
// construction and are excluded from the diff; the other architectures list
|
||||
// their conditional branches outright.
|
||||
func cmdAuditInstructions(args []string) error {
|
||||
fs := newCommand("audit-instructions", "gasm audit-instructions [amd64|arm64|riscv64|loong64]", `
|
||||
Compare the gasm encoder for the given architecture (default amd64) against
|
||||
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
||||
gasm asm --format goobj), known-but-unencodable names (the backlog) and go-
|
||||
only names (feature gaps). The Go side is probed black-box with a battery
|
||||
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||
provides.
|
||||
`)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
archName := "amd64"
|
||||
switch n := len(fs.Args()); {
|
||||
case n > 1:
|
||||
return fmt.Errorf("audit-instructions takes at most one architecture argument")
|
||||
case n == 1:
|
||||
archName = strings.ToLower(fs.Arg(0))
|
||||
}
|
||||
a, err := auditArch(archName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tab := arch.ForArch(a)
|
||||
var names []string
|
||||
seen := map[string]bool{}
|
||||
for _, in := range tab.Instructions() {
|
||||
name := strings.ToUpper(in.Name)
|
||||
if a == arch.AMD64 && derivedFamily(name) || seen[name] {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
goKnown, err := probeGoAsm(goarchName(a), names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var superset, backlog, shared []string
|
||||
for _, name := range names {
|
||||
switch {
|
||||
case !gasmEncodable(a, name):
|
||||
backlog = append(backlog, name)
|
||||
case !goKnown[name]:
|
||||
superset = append(superset, name)
|
||||
default:
|
||||
shared = append(shared, name)
|
||||
}
|
||||
}
|
||||
// GO-ONLY is not enumerable by probing: Go's table is only visible
|
||||
// through names we already know, so nothing can be reported there.
|
||||
|
||||
slices.Sort(superset)
|
||||
slices.Sort(backlog)
|
||||
slices.Sort(shared)
|
||||
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names))
|
||||
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %d\n", len(shared)+len(superset), countTrue(goKnown))
|
||||
fmt.Fprintf(w, "shared: %d\n", len(shared))
|
||||
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
|
||||
for _, n := range superset {
|
||||
fmt.Fprintf(w, " %s\n", n)
|
||||
}
|
||||
fmt.Fprintf(w, "\nKnown but not encodable (backlog):\n")
|
||||
for _, n := range backlog {
|
||||
fmt.Fprintf(w, " %s\n", n)
|
||||
}
|
||||
fmt.Fprintf(w, "\nGo-only names cannot be enumerated by probing; extend the gasm\n")
|
||||
fmt.Fprintf(w, "table from the Go release notes when a new instruction family ships.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// auditArch resolves the audit's architecture argument.
|
||||
func auditArch(name string) (arch.Arch, error) {
|
||||
switch strings.ToLower(name) {
|
||||
case "amd64":
|
||||
return arch.AMD64, nil
|
||||
case "arm64":
|
||||
return arch.ARM64, nil
|
||||
case "riscv64", "riscv":
|
||||
return arch.RISCV, nil
|
||||
case "loong64", "loong":
|
||||
return arch.LOONG64, nil
|
||||
}
|
||||
return arch.Unknown, fmt.Errorf("unknown architecture %q: want amd64, arm64, riscv64 or loong64", name)
|
||||
}
|
||||
|
||||
// goarchName maps an arch identifier onto its GOARCH spelling.
|
||||
func goarchName(a arch.Arch) string {
|
||||
switch a {
|
||||
case arch.ARM64:
|
||||
return "arm64"
|
||||
case arch.RISCV:
|
||||
return "riscv64"
|
||||
case arch.LOONG64:
|
||||
return "loong64"
|
||||
}
|
||||
return "amd64"
|
||||
}
|
||||
|
||||
func countTrue(m map[string]bool) int {
|
||||
n := 0
|
||||
for _, v := range m {
|
||||
if v {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// derivedFamily reports whether a mnemonic belongs to a family both
|
||||
// assemblers construct from condition codes rather than list exhaustively
|
||||
// (JEQ/CMOVLGT/SETNE and friends). Such names never probe cleanly, so
|
||||
// including them in the diff would be noise. amd64 only: the other
|
||||
// architectures list their conditional branches outright.
|
||||
func derivedFamily(name string) bool {
|
||||
if strings.HasPrefix(name, "J") && name != "JMP" && name != "JMPQ" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(name, "CMOV") || strings.HasPrefix(name, "SET") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var unrecognizedRe = regexp.MustCompile(`unrecognized instruction`)
|
||||
|
||||
// probeGoAsm feeds every mnemonic to go tool asm in one generated file and
|
||||
// classifies the diagnostics. "Unrecognized instruction" is a parse-stage
|
||||
// verdict on the mnemonic alone, so a single bare-instruction probe per
|
||||
// mnemonic decides recognition; the combined file still reports every line's
|
||||
// error even when others fail.
|
||||
func probeGoAsm(goarch string, names []string) (map[string]bool, error) {
|
||||
dir, err := os.MkdirTemp("", "gasm-audit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("TEXT ·probe(SB), 4, $0\n\tRET\n")
|
||||
lineMnemonic := map[int]string{}
|
||||
line := 3
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(&sb, "TEXT ·p%s%d(SB), 4, $0\n", sanitize(name), line)
|
||||
sb.WriteString("\t" + name + "\n\tRET\n")
|
||||
lineMnemonic[line+1] = name // the instruction line, after TEXT
|
||||
line += 3
|
||||
}
|
||||
probePath := filepath.Join(dir, "probe.s")
|
||||
if err := os.WriteFile(probePath, []byte(sb.String()), 0o644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
toolDir, err := exec.Command("go", "env", "GOTOOLDIR").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go env GOTOOLDIR: %w", err)
|
||||
}
|
||||
asmBin := filepath.Join(strings.TrimSpace(string(toolDir)), "asm")
|
||||
if _, err := os.Stat(asmBin); err != nil {
|
||||
return nil, fmt.Errorf("go tool asm not found at %s", asmBin)
|
||||
}
|
||||
cmd := exec.Command(asmBin, "-p", "probe", "-o", filepath.Join(dir, "probe.o"), probePath)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+runtime.GOOS)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
|
||||
result := map[string]bool{}
|
||||
for _, name := range names {
|
||||
result[name] = true // no news = the name parsed fine
|
||||
}
|
||||
reParse := regexp.MustCompile(`probe\.s:(\d+):`)
|
||||
for l := range strings.SplitSeq(string(out), "\n") {
|
||||
m := reParse.FindStringSubmatch(l)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
lineNo, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if name, ok := lineMnemonic[lineNo]; ok && unrecognizedRe.MatchString(l) {
|
||||
result[name] = false
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// probeShapes lists representative operand shapes for the encodability
|
||||
// probe. The assemblers report an unknown mnemonic and a known mnemonic
|
||||
// with no supported form alike ("unsupported <arch> instruction"), so only
|
||||
// a shape that assembles cleanly counts, and the backlog over-approximates:
|
||||
// a name whose real forms the battery misses lands there. amd64 keeps its
|
||||
// exact table-driven check.
|
||||
func probeShapes(a arch.Arch) []string {
|
||||
switch a {
|
||||
case arch.ARM64:
|
||||
return []string{
|
||||
"X0, X1, X2", "X0, X1", "X0", "$1, X0", "X0, (X1)", "(X0), X1",
|
||||
"X0, (X1, 8)", "(SP), X0", "F0, F1, F2", "F0, F1", "F0",
|
||||
"V0.B16, V1.B16, V2.B16", "p2", "X0, p2", "X0, X1, p2",
|
||||
// The conditional select family spells the condition first
|
||||
// and takes R register spellings.
|
||||
"EQ, R0, R1, R2", "EQ, R0, R1", "EQ, R0",
|
||||
"GE, F0, F1, F2", "NE, F0, F1, $0",
|
||||
}
|
||||
case arch.RISCV:
|
||||
return []string{
|
||||
"X5, X6, X7", "X5, X6", "X5", "$1, X5", "X5, (X6)", "$1, X5, X6",
|
||||
"(X5), X6", "F0, F1, F2", "F0, F1", "p2", "X1, p2", "X0, p2",
|
||||
"X5, X6, p2", "p2(SB)",
|
||||
}
|
||||
case arch.LOONG64:
|
||||
return []string{
|
||||
"R4, R5, R6", "R4, R5", "R4", "$1, R4", "R4, (R5)", "(R4), R5",
|
||||
"F0, F1, F2", "F0, F1", "p2", "R1, p2", "R4, p2",
|
||||
"$1, R4, R5, R6", "$65536, R4", "R4, R5, p2", "p2(SB)",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gasmEncodable reports whether the gasm encoder for a can emit the
|
||||
// mnemonic, decided by trial assembly over the shape battery.
|
||||
func gasmEncodable(a arch.Arch, name string) bool {
|
||||
switch a {
|
||||
case arch.ARM64, arch.RISCV, arch.LOONG64:
|
||||
default:
|
||||
return asm.Encodable(name)
|
||||
}
|
||||
for _, shape := range probeShapes(a) {
|
||||
if gasmAssembles(a, name, shape) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// gasmAssembles reports whether a one-instruction probe file containing name
|
||||
// with the given operand shape assembles without error.
|
||||
func gasmAssembles(a arch.Arch, name, shape string) bool {
|
||||
src := "TEXT ·p(SB), NOSPLIT, $0\n\t" + name
|
||||
if shape != "" {
|
||||
src += " " + shape
|
||||
}
|
||||
src += "\n\tRET\np2:\n\tRET\n"
|
||||
f, errs := parser.Parse("probe.s", src)
|
||||
if len(errs) > 0 {
|
||||
return false
|
||||
}
|
||||
var err error
|
||||
switch a {
|
||||
case arch.ARM64:
|
||||
_, err = asm.AssembleFileARM64(f)
|
||||
case arch.RISCV:
|
||||
_, err = asm.AssembleFileRISCV(f)
|
||||
case arch.LOONG64:
|
||||
_, err = asm.AssembleFileLOONG64(f)
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// sanitize makes a mnemonic safe for use in a Go symbol name.
|
||||
func sanitize(name string) string {
|
||||
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
)
|
||||
|
||||
func TestDerivedFamily(t *testing.T) {
|
||||
for _, n := range []string{"JEQ", "JLT", "JCC", "CMOVLGT", "SETNE", "SETA"} {
|
||||
if !derivedFamily(n) {
|
||||
t.Errorf("derivedFamily(%q) = false, want true", n)
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"JMP", "ADDQ", "VPGATHERDD", "MOVBE", "PSHUFB"} {
|
||||
if derivedFamily(n) {
|
||||
t.Errorf("derivedFamily(%q) = true, want false", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitize(t *testing.T) {
|
||||
if got := sanitize("VPCMP.UB"); got != "VPCMP_UB" {
|
||||
t.Errorf("sanitize: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditArch(t *testing.T) {
|
||||
for in, want := range map[string]arch.Arch{
|
||||
"amd64": arch.AMD64, "arm64": arch.ARM64,
|
||||
"riscv64": arch.RISCV, "riscv": arch.RISCV,
|
||||
"loong64": arch.LOONG64, "LOONG": arch.LOONG64,
|
||||
} {
|
||||
got, err := auditArch(in)
|
||||
if err != nil || got != want {
|
||||
t.Errorf("auditArch(%q) = %v, %v; want %v", in, got, err, want)
|
||||
}
|
||||
}
|
||||
if _, err := auditArch("mips"); err == nil {
|
||||
t.Error("auditArch(mips) must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasmEncodable(t *testing.T) {
|
||||
cases := []struct {
|
||||
a arch.Arch
|
||||
yes string
|
||||
no string
|
||||
}{
|
||||
{arch.AMD64, "ADDQ", "NOSUCHMNEMONIC"},
|
||||
{arch.ARM64, "ADD", "NOSUCHMNEMONIC"},
|
||||
{arch.RISCV, "ADD", "NOSUCHMNEMONIC"},
|
||||
{arch.LOONG64, "ADDV", "NOSUCHMNEMONIC"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if !gasmEncodable(c.a, c.yes) {
|
||||
t.Errorf("%s: %s should be encodable", c.a, c.yes)
|
||||
}
|
||||
if gasmEncodable(c.a, c.no) {
|
||||
t.Errorf("%s: %s should not be encodable", c.a, c.no)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
)
|
||||
|
||||
func cmdDebug(args []string) int {
|
||||
fs := newCommand("debug", "gasm debug <file.s> --func <name>", `
|
||||
Interactive debugger for JIT-assembled functions. Launches the
|
||||
function in a traced subprocess (ptrace), then provides a REPL for
|
||||
single-stepping, breakpoints, register and memory inspection.
|
||||
|
||||
REPL commands:
|
||||
break <label|addr> [if <reg> <op> <val>]
|
||||
set a breakpoint, optionally conditional on a
|
||||
register comparison (reg-reg or reg-immediate)
|
||||
delete <label|addr> remove a breakpoint
|
||||
info break list all breakpoints
|
||||
step [n], s single-step n instructions (default 1)
|
||||
next, n step over a CALL
|
||||
finish, fin run until the function returns
|
||||
continue, c run until a breakpoint, watchpoint or exit
|
||||
disas [n], u disassemble n instructions at PC
|
||||
regs print general-purpose and vector registers
|
||||
where show source line and nearest label at PC
|
||||
stack show stack near RSP (return address + ABI0 args)
|
||||
bt, backtrace backtrace (current frame + return address)
|
||||
x [addr] [len] hex-dump memory (default: current PC, 64 bytes)
|
||||
w <addr> <val...> write bytes to memory
|
||||
set <reg> <value> set a register
|
||||
watch <addr> [r|w] [size]
|
||||
set a hardware watchpoint (write by default)
|
||||
unwatch [<slot>] clear one watchpoint, or all without an argument
|
||||
labels, l list function labels and offsets
|
||||
help, h, ? show command help
|
||||
quit, q kill the debuggee and exit
|
||||
`)
|
||||
funcName := fs.String("func", "", "function to debug")
|
||||
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
|
||||
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
|
||||
script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin")
|
||||
cover := fs.Bool("cover", false, "run to completion with a breakpoint on every instruction and report which executed and how often")
|
||||
timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs")
|
||||
fs.Parse(args)
|
||||
|
||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||
if os.Getenv("GASM_DEBUG_TARGET") != "" {
|
||||
tmpDir := os.Getenv("GASM_DEBUG_TMP")
|
||||
if tmpDir == "" || fs.NArg() < 1 || *funcName == "" || *argsFile == "" {
|
||||
fmt.Fprintln(os.Stderr, "gasm debug: internal debuggee mode")
|
||||
return 2
|
||||
}
|
||||
if err := debug.RunTarget(fs.Arg(0), *funcName, *argsFile, tmpDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- Debugger mode (interactive REPL) ---
|
||||
if fs.NArg() < 1 || *funcName == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm debug <file.s> --func <name>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
|
||||
// The watchdog is armed before anything can block: ptrace attach and a
|
||||
// continued kernel loop both hang the run when the environment forbids
|
||||
// tracing or the kernel loops forever, and neither is interruptible from
|
||||
// the inside.
|
||||
if *timeout > 0 {
|
||||
go func() {
|
||||
time.Sleep(*timeout)
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s) — killing the debuggee\n", *timeout)
|
||||
os.Exit(3)
|
||||
}()
|
||||
}
|
||||
|
||||
// Load the kernel to extract function metadata and labels.
|
||||
k, err := verify.Load(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
fl, err := k.Func(*funcName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Build the label list for the REPL.
|
||||
var labels []debug.Label
|
||||
for name, off := range fl.Labels {
|
||||
labels = append(labels, debug.Label{Name: name, Offset: off})
|
||||
}
|
||||
sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset })
|
||||
|
||||
// Launch the debuggee with the argument block.
|
||||
var argBlock []byte
|
||||
var bufAddrs []uint64
|
||||
var sess *debug.Session
|
||||
if *bufSpec != "" {
|
||||
// Parse the function signature to determine argument layout.
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
sig, ok := verify.ExtractFuncSig(src, *funcName)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: no // func signature found for %s\n", *funcName)
|
||||
return 1
|
||||
}
|
||||
layout := verify.ArgLayout(sig)
|
||||
|
||||
// Parse the buffer spec to get buffer names.
|
||||
bufNames := parseBufNames(*bufSpec)
|
||||
|
||||
// Allocate buffers in the debuggee.
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, bufAddrs, err = debug.LaunchWithBuffers("", path, *funcName, argBlock, *bufSpec)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Construct the argument block with buffer pointers at the correct positions.
|
||||
bufIdx := 0
|
||||
for _, arg := range layout {
|
||||
if !arg.IsPtr {
|
||||
continue
|
||||
}
|
||||
// Find the buffer that matches this argument.
|
||||
for i, name := range bufNames {
|
||||
if i < len(bufAddrs) && (name == arg.Name || strings.HasPrefix(arg.Name, name)) {
|
||||
addr := bufAddrs[i]
|
||||
off := arg.Offset
|
||||
if off+8 <= len(argBlock) {
|
||||
argBlock[off] = byte(addr)
|
||||
argBlock[off+1] = byte(addr >> 8)
|
||||
argBlock[off+2] = byte(addr >> 16)
|
||||
argBlock[off+3] = byte(addr >> 24)
|
||||
argBlock[off+4] = byte(addr >> 32)
|
||||
argBlock[off+5] = byte(addr >> 40)
|
||||
argBlock[off+6] = byte(addr >> 48)
|
||||
argBlock[off+7] = byte(addr >> 56)
|
||||
}
|
||||
// For slices, also set the length and capacity.
|
||||
if strings.HasPrefix(arg.Typ, "[]") && off+24 <= len(argBlock) {
|
||||
// Find the buffer size from the spec.
|
||||
size := parseBufSize(*bufSpec, name)
|
||||
// Length at offset+8, capacity at offset+16.
|
||||
for j := range 8 {
|
||||
argBlock[off+8+j] = byte(size >> (j * 8))
|
||||
argBlock[off+16+j] = byte(size >> (j * 8))
|
||||
}
|
||||
}
|
||||
bufIdx++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = bufIdx
|
||||
} else {
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, err = debug.Launch("", path, *funcName, argBlock)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
defer sess.Kill()
|
||||
|
||||
bm := debug.NewBreakpoints(sess)
|
||||
fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid())
|
||||
|
||||
// Convert the line table for the command loop.
|
||||
var srcLines []debug.SourceLine
|
||||
for _, le := range fl.Lines {
|
||||
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
|
||||
}
|
||||
|
||||
// Coverage mode: pre-register a breakpoint on every instruction (walked
|
||||
// by length through the function body while the debuggee is stopped) and
|
||||
// let the kernel run to completion. Each trap counts a hit for that
|
||||
// instruction, so the final report shows exactly which instructions
|
||||
// executed and how often, with the label-level view derived from it.
|
||||
// Expect the run to slow to ptrace speed: one trap per executed
|
||||
// instruction.
|
||||
if *cover {
|
||||
base := sess.CodeBase() + uint64(fl.Offset)
|
||||
type coverInstr struct {
|
||||
off uint64
|
||||
text string
|
||||
}
|
||||
var instrs []coverInstr
|
||||
for off := uint64(0); off < uint64(fl.Size); {
|
||||
text, ln, err := sess.Disassemble(base + off)
|
||||
if err != nil || ln == 0 {
|
||||
break
|
||||
}
|
||||
instrs = append(instrs, coverInstr{off: off, text: text})
|
||||
off += uint64(ln)
|
||||
}
|
||||
for _, in := range instrs {
|
||||
if _, err := bm.SetWithCond(base+in.off, fmt.Sprintf("func+%#x", in.off), nil); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: cover: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
fmt.Printf("gasm debug: coverage run over %d instructions\n", len(instrs))
|
||||
for {
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
if err := sess.Continue(); err != nil {
|
||||
break // debuggee finished or died
|
||||
}
|
||||
if sess.Exited() {
|
||||
break
|
||||
}
|
||||
regs, rerr := sess.GetRegs()
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
// HandleTrap restores the original byte, rewinds PC and counts
|
||||
// the hit on the breakpoint itself. Single-step over the
|
||||
// restored instruction so the reinsertion at the top of the
|
||||
// loop cannot re-trap on the same breakpoint.
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
if err := sess.Step(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
hits := map[uint64]int{}
|
||||
traps := 0
|
||||
for _, bp := range bm.All() {
|
||||
if n := bp.Hits(); n > 0 {
|
||||
hits[bp.Addr-base] = n
|
||||
traps += n
|
||||
}
|
||||
}
|
||||
var hit []string
|
||||
var missed []string
|
||||
for _, l := range labels {
|
||||
if hits[uint64(l.Offset)] > 0 {
|
||||
hit = append(hit, l.Name)
|
||||
} else {
|
||||
missed = append(missed, l.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(hit)
|
||||
sort.Strings(missed)
|
||||
fmt.Printf("coverage: %d/%d instructions executed (%d traps)\n", len(hits), len(instrs), traps)
|
||||
fmt.Printf("coverage: %d/%d labels reached\n", len(hit), len(labels))
|
||||
for _, l := range hit {
|
||||
fmt.Printf(" covered %s\n", l)
|
||||
}
|
||||
for _, l := range missed {
|
||||
fmt.Printf(" MISSED %s\n", l)
|
||||
}
|
||||
fmt.Println("executed instructions:")
|
||||
for _, in := range instrs {
|
||||
if n := hits[in.off]; n > 0 {
|
||||
fmt.Printf(" func+%#04x %4dx %s\n", in.off, n, in.text)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Headless mode: run the script through the normal command loop and
|
||||
// exit. The watchdog armed above covers launch, continue and step.
|
||||
var in io.Reader = os.Stdin
|
||||
if *script != "" {
|
||||
if *script == "-" {
|
||||
in = os.Stdin
|
||||
} else {
|
||||
f, err := os.Open(*script)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
}
|
||||
}
|
||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines, in)
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseBufNames extracts buffer names from a buffer specification.
|
||||
// Format: name:size:pattern[,name:size:pattern...]
|
||||
func parseBufNames(spec string) []string {
|
||||
var names []string
|
||||
for part := range strings.SplitSeq(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 1 && fields[0] != "" {
|
||||
names = append(names, fields[0])
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// parseBufSize extracts the size of a named buffer from a buffer specification.
|
||||
func parseBufSize(spec, name string) int {
|
||||
for part := range strings.SplitSeq(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 2 && fields[0] == name {
|
||||
var size int
|
||||
fmt.Sscanf(fields[1], "%d", &size)
|
||||
return size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
)
|
||||
|
||||
func cmdDebug(args []string) int {
|
||||
fs := newCommand("debug", "gasm debug <file.s> --func <name>", `
|
||||
Interactive debugger for JIT-assembled amd64 functions. Launches the
|
||||
function in a traced subprocess (ptrace), then provides a REPL for
|
||||
single-stepping, breakpoints, register and memory inspection.
|
||||
|
||||
REPL commands:
|
||||
break <label|addr> set a breakpoint at a label or absolute address
|
||||
step [n] single-step n instructions (default 1)
|
||||
continue run until next breakpoint or exit
|
||||
regs print general-purpose registers
|
||||
x [addr] [len] hex-dump memory (default: current PC, 64 bytes)
|
||||
labels list function labels and offsets
|
||||
quit kill the debuggee and exit
|
||||
`)
|
||||
target := fs.Bool("target", false, "") // hidden: debuggee subprocess mode
|
||||
funcName := fs.String("func", "", "function to debug")
|
||||
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
|
||||
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
|
||||
fs.Parse(args)
|
||||
|
||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||
if *target {
|
||||
tmpDir := os.Getenv("GASM_DEBUG_TMP")
|
||||
if tmpDir == "" || fs.NArg() < 1 || *funcName == "" || *argsFile == "" {
|
||||
fmt.Fprintln(os.Stderr, "gasm debug --target: internal mode")
|
||||
return 2
|
||||
}
|
||||
if err := debug.RunTarget(fs.Arg(0), *funcName, *argsFile, tmpDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- Debugger mode (interactive REPL) ---
|
||||
if fs.NArg() < 1 || *funcName == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm debug <file.s> --func <name>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
|
||||
// Load the kernel to extract function metadata and labels.
|
||||
k, err := verify.Load(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
fl, err := k.Func(*funcName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Build the label list for the REPL.
|
||||
var labels []debug.Label
|
||||
for name, off := range fl.Labels {
|
||||
labels = append(labels, debug.Label{Name: name, Offset: off})
|
||||
}
|
||||
sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset })
|
||||
|
||||
// Launch the debuggee with the argument block.
|
||||
var argBlock []byte
|
||||
var bufAddrs []uint64
|
||||
var sess *debug.Session
|
||||
if *bufSpec != "" {
|
||||
// Parse the function signature to determine argument layout.
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
sig, ok := verify.ExtractFuncSig(src, *funcName)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: no // func signature found for %s\n", *funcName)
|
||||
return 1
|
||||
}
|
||||
layout := verify.ArgLayout(sig)
|
||||
|
||||
// Parse the buffer spec to get buffer names.
|
||||
bufNames := parseBufNames(*bufSpec)
|
||||
|
||||
// Allocate buffers in the debuggee.
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, bufAddrs, err = debug.LaunchWithBuffers("", path, *funcName, argBlock, *bufSpec)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Construct the argument block with buffer pointers at the correct positions.
|
||||
bufIdx := 0
|
||||
for _, arg := range layout {
|
||||
if !arg.IsPtr {
|
||||
continue
|
||||
}
|
||||
// Find the buffer that matches this argument.
|
||||
for i, name := range bufNames {
|
||||
if i < len(bufAddrs) && (name == arg.Name || strings.HasPrefix(arg.Name, name)) {
|
||||
addr := bufAddrs[i]
|
||||
off := arg.Offset
|
||||
if off+8 <= len(argBlock) {
|
||||
argBlock[off] = byte(addr)
|
||||
argBlock[off+1] = byte(addr >> 8)
|
||||
argBlock[off+2] = byte(addr >> 16)
|
||||
argBlock[off+3] = byte(addr >> 24)
|
||||
argBlock[off+4] = byte(addr >> 32)
|
||||
argBlock[off+5] = byte(addr >> 40)
|
||||
argBlock[off+6] = byte(addr >> 48)
|
||||
argBlock[off+7] = byte(addr >> 56)
|
||||
}
|
||||
// For slices, also set the length and capacity.
|
||||
if strings.HasPrefix(arg.Typ, "[]") && off+24 <= len(argBlock) {
|
||||
// Find the buffer size from the spec.
|
||||
size := parseBufSize(*bufSpec, name)
|
||||
// Length at offset+8, capacity at offset+16.
|
||||
for j := 0; j < 8; j++ {
|
||||
argBlock[off+8+j] = byte(size >> (j * 8))
|
||||
argBlock[off+16+j] = byte(size >> (j * 8))
|
||||
}
|
||||
}
|
||||
bufIdx++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = bufIdx
|
||||
} else {
|
||||
argBlock = make([]byte, fl.Args)
|
||||
sess, err = debug.Launch("", path, *funcName, argBlock)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
defer sess.Kill()
|
||||
|
||||
bm := debug.NewBreakpoints(sess)
|
||||
fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid())
|
||||
|
||||
// Convert the line table for the REPL.
|
||||
var srcLines []debug.SourceLine
|
||||
for _, le := range fl.Lines {
|
||||
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
|
||||
}
|
||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseBufNames extracts buffer names from a buffer specification.
|
||||
// Format: name:size:pattern[,name:size:pattern...]
|
||||
func parseBufNames(spec string) []string {
|
||||
var names []string
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 1 && fields[0] != "" {
|
||||
names = append(names, fields[0])
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// parseBufSize extracts the size of a named buffer from a buffer specification.
|
||||
func parseBufSize(spec, name string) int {
|
||||
for _, part := range strings.Split(spec, ",") {
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
if len(fields) >= 2 && fields[0] == name {
|
||||
var size int
|
||||
fmt.Sscanf(fields[1], "%d", &size)
|
||||
return size
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(linux && amd64)
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
@@ -11,6 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func cmdDebug(args []string) int {
|
||||
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires linux/amd64 (ptrace)")
|
||||
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires Linux (ptrace)")
|
||||
return 1
|
||||
}
|
||||
|
||||
+642
-147
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,11 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -237,3 +240,55 @@ func TestCmdArgErrors(t *testing.T) {
|
||||
t.Errorf("cmdParse() code = %d, want 2", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySmokeCrashIsolation checks that a function faulting on its
|
||||
// zeroed smoke arguments is reported as CRASH by a child process instead of
|
||||
// killing `gasm verify` itself.
|
||||
func TestVerifySmokeCrashIsolation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("builds the gasm binary")
|
||||
}
|
||||
if runtime.GOARCH != "amd64" {
|
||||
t.Skip("amd64 JIT only")
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("build gasm: %v\n%s", err, out)
|
||||
}
|
||||
src := filepath.Join(t.TempDir(), "crash_amd64.s")
|
||||
kernel := "#include \"textflag.h\"\n" +
|
||||
"\n" +
|
||||
"// func Fault(x []byte) int\n" +
|
||||
"TEXT ·Fault(SB), NOSPLIT, $0-32\n" +
|
||||
"\tMOVQ\tx+0(FP), AX\n" +
|
||||
"\tMOVQ\t(AX), AX // faults on the zeroed nil pointer\n" +
|
||||
"\tMOVQ\tAX, ret+24(FP)\n" +
|
||||
"\tRET\n"
|
||||
if err := os.WriteFile(src, []byte(kernel), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(bin, "verify", "-smoke", src)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("expected a failure report, got success:\n%s", out)
|
||||
}
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
|
||||
t.Fatalf("verify died from %v — the crash was not isolated:\n%s", ws.Signal(), out)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(out), "CRASH") {
|
||||
t.Errorf("output does not report CRASH:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweepCheckLines(t *testing.T) {
|
||||
out := []byte("crash_amd64.s: 1 functions JIT-loaded\n" +
|
||||
" Fault: 21 bytes, args=32, frame=0 NOSPLIT\n" +
|
||||
" smoke: OK\n" +
|
||||
" abi: clean (10 varied inputs)\n")
|
||||
want := " smoke: OK\n abi: clean (10 varied inputs)"
|
||||
if got := sweepCheckLines(out); got != want {
|
||||
t.Errorf("sweepCheckLines = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
gasmast "sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
gasmparser "sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// cmdScaffold generates a differential test skeleton for every kernel in a
|
||||
// file: a Go test that seeds random states, drives both the assembly kernel
|
||||
// and a caller-provided portable reference, and compares the outputs
|
||||
// byte-for-byte. The lesson this encodes: a pipeline-level fuzz cannot see
|
||||
// an unwired kernel — only a direct-call differential against the portable
|
||||
// specification can, so every kernel ships with one.
|
||||
//
|
||||
// The generated file follows two conventions the caller fills in:
|
||||
// - the assembly symbols resolve because the test lives in the kernel's
|
||||
// own package (the //go:noescape declarations reference them);
|
||||
// - each kernel gets a <name>Portable Go function the author implements as
|
||||
// the specification, and the test fails on the first divergent byte.
|
||||
func cmdScaffold(args []string) error {
|
||||
fs := newCommand("scaffold", "gasm scaffold differential <file.s>", `
|
||||
Print a differential test skeleton for every // func signature in FILE.
|
||||
The test seeds random states, drives the kernel and a portable reference
|
||||
(<name>Portable), and compares outputs byte-for-byte. Write the reference
|
||||
bodies, place the file in the kernel's package, and run it in CI.
|
||||
`)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
rest := fs.Args()
|
||||
// The first positional word is the scaffold style; "differential" is the
|
||||
// only one today.
|
||||
if len(rest) > 0 && rest[0] == "differential" {
|
||||
rest = rest[1:]
|
||||
}
|
||||
if len(rest) != 1 {
|
||||
return fmt.Errorf("usage: gasm scaffold differential <file.s>")
|
||||
}
|
||||
path := rest[0]
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, errs := gasmparser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("parse: %v", errs[0])
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.WriteString(headerComment)
|
||||
out.WriteString("package " + packageName + "\n\n")
|
||||
out.WriteString("import (\n\t\"bytes\"\n\t\"math/rand\"\n\t\"testing\"\n)\n\n")
|
||||
out.WriteString(generatedHelpers)
|
||||
|
||||
kernels := 0
|
||||
for _, d := range f.Decls {
|
||||
txt, ok := d.(*gasmast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
params, results, ok := parseSig(txt.Doc)
|
||||
if !ok || len(params) == 0 {
|
||||
continue
|
||||
}
|
||||
kernels++
|
||||
name := txt.Name.Name
|
||||
fmt.Fprintf(&out, "// %sPortable is the specification %s is pinned against:\n", name, name)
|
||||
fmt.Fprintf(&out, "// fill in a straightforward implementation of the same contract.\n")
|
||||
fmt.Fprintf(&out, "func %sPortable(%s) (%s) {\n\tpanic(\"implement the portable specification\")\n}\n\n", name, paramDecl(params), resultDecl(results))
|
||||
|
||||
fmt.Fprintf(&out, "func Test%sDifferential(t *testing.T) {\n", strings.ToUpper(name[:1])+name[1:])
|
||||
fmt.Fprintf(&out, "\trng := rand.New(rand.NewSource(1))\n")
|
||||
fmt.Fprintf(&out, "\tfor range 1000 {\n")
|
||||
// Seed two independent argument sets per iteration: the kernel runs
|
||||
// on set A, the portable reference on set B, so in-place writes
|
||||
// through pointer/slice arguments cannot contaminate the other side.
|
||||
var sliceNames []string
|
||||
seen := map[string]bool{}
|
||||
aArgs := make([]string, 0, len(params))
|
||||
bArgs := make([]string, 0, len(params))
|
||||
for _, p := range params {
|
||||
a, b, slices := genParamSeed(&out, p, seen)
|
||||
aArgs = append(aArgs, a)
|
||||
bArgs = append(bArgs, b)
|
||||
sliceNames = append(sliceNames, slices...)
|
||||
}
|
||||
fmt.Fprintf(&out, "\t\tgot := %s(%s)\n", name, strings.Join(aArgs, ", "))
|
||||
fmt.Fprintf(&out, "\t\twant := %sPortable(%s)\n", name, strings.Join(bArgs, ", "))
|
||||
fmt.Fprintf(&out, "\t\tif !bytes.Equal(outputBytes(got), outputBytes(want)) {\n")
|
||||
fmt.Fprintf(&out, "\t\t\tt.Fatalf(\"kernel diverges from the portable spec (seed 1, deterministic)\")\n")
|
||||
fmt.Fprintf(&out, "\t\t}\n")
|
||||
for _, s := range sliceNames {
|
||||
fmt.Fprintf(&out, "\t\tif !bytes.Equal(outputBytes(%sA), outputBytes(%sB)) {\n", s, s)
|
||||
fmt.Fprintf(&out, "\t\t\tt.Fatalf(\"kernel mutated %%q differently (seed 1, deterministic)\", %q)\n", s)
|
||||
fmt.Fprintf(&out, "\t\t}\n")
|
||||
}
|
||||
fmt.Fprintf(&out, "\t}\n}\n\n")
|
||||
}
|
||||
if kernels == 0 {
|
||||
return fmt.Errorf("%s: no // func signatures found; add one doc comment per kernel", path)
|
||||
}
|
||||
os.Stdout.WriteString(out.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
const packageName = "yourpkg"
|
||||
|
||||
const headerComment = `// Code generated by gasm scaffold differential; EDIT THE PANICS.
|
||||
// Each Test*Differential drives the assembly kernel and its portable
|
||||
// reference over the same random states and compares the outputs.
|
||||
// Place this file in the kernel's own package so the symbols resolve.
|
||||
|
||||
`
|
||||
|
||||
// sigParam is one parsed // func parameter.
|
||||
type sigParam struct {
|
||||
Names []string
|
||||
Type string
|
||||
}
|
||||
|
||||
type sigResult struct {
|
||||
Names []string
|
||||
Type string
|
||||
}
|
||||
|
||||
// parseSig parses the // func signature of a doc comment.
|
||||
func parseSig(doc string) ([]sigParam, []sigResult, bool) {
|
||||
var line string
|
||||
for l := range strings.SplitSeq(doc, "\n") {
|
||||
if t := strings.TrimSpace(l); strings.HasPrefix(t, "func ") {
|
||||
line = t
|
||||
break
|
||||
}
|
||||
}
|
||||
if line == "" {
|
||||
return nil, nil, false
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "sig.go", "package p\n"+line+" {}\n", 0)
|
||||
if err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
fd, ok := f.Decls[0].(*ast.FuncDecl)
|
||||
if !ok || fd.Type == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
var params []sigParam
|
||||
for _, field := range fd.Type.Params.List {
|
||||
typ := exprString(field.Type)
|
||||
if len(field.Names) == 0 {
|
||||
params = append(params, sigParam{Names: []string{""}, Type: typ})
|
||||
continue
|
||||
}
|
||||
// Shared names (`L, result *byte`) expand to one entry per name:
|
||||
// every name is a separate argument at the call site.
|
||||
for _, n := range field.Names {
|
||||
params = append(params, sigParam{Names: []string{n.Name}, Type: typ})
|
||||
}
|
||||
}
|
||||
var results []sigResult
|
||||
if fd.Type.Results != nil {
|
||||
for _, field := range fd.Type.Results.List {
|
||||
results = append(results, sigResult{Names: identNames(field.Names), Type: exprString(field.Type)})
|
||||
}
|
||||
}
|
||||
return params, results, true
|
||||
}
|
||||
|
||||
func identNames(idents []*ast.Ident) []string {
|
||||
var out []string
|
||||
for _, id := range idents {
|
||||
out = append(out, id.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func exprString(e ast.Expr) string {
|
||||
switch t := e.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + exprString(t.X)
|
||||
case *ast.SelectorExpr:
|
||||
return exprString(t.X) + "." + t.Sel.Name
|
||||
case *ast.ArrayType:
|
||||
if t.Len == nil {
|
||||
return "[]" + exprString(t.Elt)
|
||||
}
|
||||
return "[N]" + exprString(t.Elt)
|
||||
}
|
||||
return "interface{}"
|
||||
}
|
||||
|
||||
// paramDecl renders a parameter list for the portable reference signature.
|
||||
func paramDecl(params []sigParam) string {
|
||||
var parts []string
|
||||
for _, p := range params {
|
||||
if len(p.Names) == 0 {
|
||||
parts = append(parts, p.Type)
|
||||
continue
|
||||
}
|
||||
for _, n := range p.Names {
|
||||
parts = append(parts, n+" "+p.Type)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// resultDecl renders a result list; unnamed results keep bare types.
|
||||
func resultDecl(results []sigResult) string {
|
||||
if len(results) == 0 {
|
||||
return ""
|
||||
}
|
||||
var parts []string
|
||||
for _, r := range results {
|
||||
parts = append(parts, r.Type)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// genParamSeed emits the seeding statements for one parameter and returns
|
||||
// the kernel-side (A) and reference-side (B) argument expressions, plus the
|
||||
// names of any slice variables written in place (compared after the calls).
|
||||
func genParamSeed(out *strings.Builder, p sigParam, seen map[string]bool) (aArg, bArg string, slices []string) {
|
||||
name := p.Names[0]
|
||||
elem := strings.TrimPrefix(p.Type, "*")
|
||||
isSlice := strings.HasPrefix(p.Type, "[]")
|
||||
if isSlice {
|
||||
elem = strings.TrimPrefix(p.Type, "[]")
|
||||
}
|
||||
switch {
|
||||
case isSlice:
|
||||
v := uniqueName(seen, name)
|
||||
fmt.Fprintf(out, "\t\t%sA := make([]%s, 1+rng.Intn(512))\n", v, elem)
|
||||
fmt.Fprintf(out, "\t\t%sB := make([]%s, len(%sA))\n", v, elem, v)
|
||||
fmt.Fprintf(out, "\t\tfor i := range %sA {\n", v)
|
||||
fmt.Fprintf(out, "\t\t\tw%s := %s(rng.Intn(256))\n", v, goCast(elem))
|
||||
fmt.Fprintf(out, "\t\t\t%sA[i] = w%s\n", v, v)
|
||||
fmt.Fprintf(out, "\t\t\t%sB[i] = w%s\n", v, v)
|
||||
fmt.Fprintf(out, "\t\t}\n")
|
||||
return v, v, []string{v}
|
||||
case strings.HasPrefix(p.Type, "*"):
|
||||
v := uniqueName(seen, name)
|
||||
fmt.Fprintf(out, "\t\tvar %sA, %sB %s\n", v, v, elem)
|
||||
fmt.Fprintf(out, "\t\tw%s := %s(rng.Intn(256))\n", v, goCast(elem))
|
||||
fmt.Fprintf(out, "\t\t%sA = w%s\n", v, v)
|
||||
fmt.Fprintf(out, "\t\t%sB = w%s\n", v, v)
|
||||
return "&" + v + "A", "&" + v + "B", nil
|
||||
default:
|
||||
v := uniqueName(seen, name)
|
||||
fmt.Fprintf(out, "\t\tw%s := %s(rng.Intn(512))\n", v, goCast(""))
|
||||
fmt.Fprintf(out, "\t\tvar %sA, %sB %s = w%s, w%s\n", v, v, p.Type, v, v)
|
||||
return v + "A", v + "B", nil
|
||||
}
|
||||
}
|
||||
|
||||
// uniqueName de-duplicates seeded variable names when one kernel takes two
|
||||
// parameters of the same name (impossible in Go) or a name repeats across
|
||||
// kernels in one file.
|
||||
func uniqueName(seen map[string]bool, base string) string {
|
||||
if base == "" {
|
||||
base = "arg"
|
||||
}
|
||||
if !seen[base] {
|
||||
seen[base] = true
|
||||
return base
|
||||
}
|
||||
for i := 2; ; i++ {
|
||||
cand := fmt.Sprintf("%s%d", base, i)
|
||||
if !seen[cand] {
|
||||
seen[cand] = true
|
||||
return cand
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// goCast returns the conversion turning rng.Intn into the element type.
|
||||
func goCast(elem string) string {
|
||||
switch elem {
|
||||
case "byte", "uint8":
|
||||
return "byte"
|
||||
case "int8":
|
||||
return "int8"
|
||||
case "uint16":
|
||||
return "uint16"
|
||||
case "int16":
|
||||
return "int16"
|
||||
case "uint32":
|
||||
return "uint32"
|
||||
case "int32":
|
||||
return "int32"
|
||||
case "uint64":
|
||||
return "uint64"
|
||||
default:
|
||||
return "int"
|
||||
}
|
||||
}
|
||||
|
||||
// generatedHelpers is emitted into every generated test file: outputBytes
|
||||
// narrows returned slices and scalars to a byte form for the comparison.
|
||||
// It lives in the template, not in this binary, because only the generated
|
||||
// file ever calls it.
|
||||
const generatedHelpers = `// outputBytes narrows a returned slice or scalar to bytes for the
|
||||
// comparison; extend the switch when a kernel returns a wider type.
|
||||
func outputBytes(v any) []byte {
|
||||
switch t := v.(type) {
|
||||
case []byte:
|
||||
return t
|
||||
case []int32:
|
||||
b := make([]byte, 4*len(t))
|
||||
for i, x := range t {
|
||||
b[i*4] = byte(x)
|
||||
b[i*4+1] = byte(x >> 8)
|
||||
b[i*4+2] = byte(x >> 16)
|
||||
b[i*4+3] = byte(x >> 24)
|
||||
}
|
||||
return b
|
||||
case []uint16:
|
||||
b := make([]byte, 2*len(t))
|
||||
for i, x := range t {
|
||||
b[i*2] = byte(x)
|
||||
b[i*2+1] = byte(x >> 8)
|
||||
}
|
||||
return b
|
||||
case int:
|
||||
b := make([]byte, 8)
|
||||
for i := range 8 {
|
||||
b[i] = byte(uint64(t) >> (8 * i))
|
||||
}
|
||||
return b
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
`
|
||||
+71
-66
@@ -1,8 +1,12 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package debug
|
||||
|
||||
import "strings"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Breakpoint is one INT3 breakpoint in the debuggee.
|
||||
@@ -15,68 +19,54 @@ type Breakpoint struct {
|
||||
hits int
|
||||
}
|
||||
|
||||
// Condition is a simple register-comparison condition evaluated when a
|
||||
// breakpoint is hit. Format: <reg> <op> <value>.
|
||||
// Condition is a register-comparison condition evaluated when a breakpoint
|
||||
// is hit. Supports three forms:
|
||||
// - register vs constant: <reg> <op> <value>
|
||||
// - register vs register: <reg> <op> <reg2>
|
||||
// - register vs memory: <reg> <op> *<addr>
|
||||
type Condition struct {
|
||||
Reg string // register name (rax, rbx, rip, rsp, ...)
|
||||
Op string // comparison operator: ==, !=, <, >, <=, >=
|
||||
Value uint64
|
||||
Reg string // register name (rax, rbx, rip, rsp, ...)
|
||||
Op string // comparison operator: ==, !=, <, >, <=, >=
|
||||
Value uint64 // constant value (when Reg2 == "" and MemAddr == 0)
|
||||
Reg2 string // second register name (for register-register comparison)
|
||||
MemAddr uint64 // memory address (for register-memory comparison, prefixed with *)
|
||||
}
|
||||
|
||||
// Eval checks the condition against the current registers.
|
||||
func (c *Condition) Eval(regs *Regs) bool {
|
||||
var actual uint64
|
||||
switch c.Reg {
|
||||
case "rax", "eax", "ax", "al":
|
||||
actual = regs.RAX
|
||||
case "rbx", "ebx", "bx", "bl":
|
||||
actual = regs.RBX
|
||||
case "rcx", "ecx", "cx", "cl":
|
||||
actual = regs.RCX
|
||||
case "rdx", "edx", "dx", "dl":
|
||||
actual = regs.RDX
|
||||
case "rsi", "esi", "si":
|
||||
actual = regs.RSI
|
||||
case "rdi", "edi", "di":
|
||||
actual = regs.RDI
|
||||
case "rbp", "ebp", "bp":
|
||||
actual = regs.RBP
|
||||
case "rsp", "esp", "sp":
|
||||
actual = regs.RSP
|
||||
case "r8":
|
||||
actual = regs.R8
|
||||
case "r9":
|
||||
actual = regs.R9
|
||||
case "r10":
|
||||
actual = regs.R10
|
||||
case "r11":
|
||||
actual = regs.R11
|
||||
case "r12":
|
||||
actual = regs.R12
|
||||
case "r13":
|
||||
actual = regs.R13
|
||||
case "r14":
|
||||
actual = regs.R14
|
||||
case "r15":
|
||||
actual = regs.R15
|
||||
case "rip", "eip":
|
||||
actual = regs.RIP
|
||||
default:
|
||||
actual, ok := regs.RegValue(c.Reg)
|
||||
if !ok {
|
||||
return true // unknown register — don't block
|
||||
}
|
||||
var expected uint64
|
||||
switch {
|
||||
case c.Reg2 != "":
|
||||
// Register-register comparison.
|
||||
v, ok := regs.RegValue(c.Reg2)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
expected = v
|
||||
case c.MemAddr != 0:
|
||||
// Register-memory comparison — requires a Session, not available here.
|
||||
// Fall back to treating as constant (the caller should resolve).
|
||||
expected = c.Value
|
||||
default:
|
||||
expected = c.Value
|
||||
}
|
||||
switch c.Op {
|
||||
case "==", "=":
|
||||
return actual == c.Value
|
||||
return actual == expected
|
||||
case "!=":
|
||||
return actual != c.Value
|
||||
return actual != expected
|
||||
case "<":
|
||||
return actual < c.Value
|
||||
return actual < expected
|
||||
case ">":
|
||||
return actual > c.Value
|
||||
return actual > expected
|
||||
case "<=":
|
||||
return actual <= c.Value
|
||||
return actual <= expected
|
||||
case ">=":
|
||||
return actual >= c.Value
|
||||
return actual >= expected
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -106,14 +96,18 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
bp.Cond = cond
|
||||
return bp, nil
|
||||
}
|
||||
// Read the original byte.
|
||||
// Read the original bytes.
|
||||
word, err := bm.t.Peek(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orig := byte(word)
|
||||
// Patch with INT3 (0xCC), preserving the rest of the word.
|
||||
patched := (word &^ 0xFF) | 0xCC
|
||||
// Patch with the breakpoint instruction, preserving the rest of the word.
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
if err := bm.t.Poke(addr, patched); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,17 +116,12 @@ func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// Hits returns the number of times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int {
|
||||
return bp.hits
|
||||
}
|
||||
|
||||
// Info returns a formatted list of all breakpoints.
|
||||
func (bm *Breakpoints) Info() string {
|
||||
if len(bm.bps) == 0 {
|
||||
return "no breakpoints set\n"
|
||||
}
|
||||
result := ""
|
||||
var result strings.Builder
|
||||
i := 0
|
||||
for _, bp := range bm.bps {
|
||||
i++
|
||||
@@ -148,9 +137,9 @@ func (bm *Breakpoints) Info() string {
|
||||
if bp.Cond != nil {
|
||||
cond = fmt.Sprintf(" if %s %s %#x", bp.Cond.Reg, bp.Cond.Op, bp.Cond.Value)
|
||||
}
|
||||
result += fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond)
|
||||
result.WriteString(fmt.Sprintf(" %d: %s at %#x [%s, %d hits]%s\n", i, label, bp.Addr, status, bp.hits, cond))
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||
@@ -196,12 +185,15 @@ func (bm *Breakpoints) All() []*Breakpoint {
|
||||
}
|
||||
|
||||
// HandleTrap is called after the debuggee stops on SIGTRAP. It checks
|
||||
// whether the trap was caused by one of our breakpoints (RIP-1 matches
|
||||
// a breakpoint address), restores the original byte, rewinds RIP, and
|
||||
// whether the trap was caused by one of our breakpoints (PC-adjust matches
|
||||
// a breakpoint address), restores the original byte, rewinds PC, and
|
||||
// returns the breakpoint that was hit (or nil if it was a single-step).
|
||||
// Hits returns how many times the breakpoint has been hit.
|
||||
func (bp *Breakpoint) Hits() int { return bp.hits }
|
||||
|
||||
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
// After INT3, RIP points to the byte AFTER the 0xCC.
|
||||
trapAddr := regs.RIP - 1
|
||||
// After a breakpoint trap, PC points past the breakpoint instruction.
|
||||
trapAddr := regs.GetPC() - uint64(breakpointPCAdjust)
|
||||
bp, ok := bm.bps[trapAddr]
|
||||
if !ok || !bp.Enabled {
|
||||
return nil // single-step trap or unknown
|
||||
@@ -225,8 +217,8 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||
bm.t.Poke(trapAddr, restored)
|
||||
}
|
||||
// Rewind RIP to re-execute the original instruction.
|
||||
regs.RIP = trapAddr
|
||||
// Rewind PC to re-execute the original instruction.
|
||||
regs.SetPC(trapAddr)
|
||||
bm.t.SetRegs(regs)
|
||||
return bp
|
||||
}
|
||||
@@ -243,6 +235,19 @@ func (bm *Breakpoints) Reinsert(addr uint64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patched := (word &^ 0xFF) | 0xCC
|
||||
mask := uint64(0)
|
||||
for range breakpointInsn {
|
||||
mask = (mask << 8) | 0xFF
|
||||
}
|
||||
patched := (word &^ mask) | breakpointWord(breakpointInsn)
|
||||
return bm.t.Poke(addr, patched)
|
||||
}
|
||||
|
||||
// breakpointWord converts the breakpoint instruction bytes to a uint64.
|
||||
func breakpointWord(insn []byte) uint64 {
|
||||
var w uint64
|
||||
for i, b := range insn {
|
||||
w |= uint64(b) << (i * 8)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
@@ -271,3 +273,54 @@ func TestBreakpointInfo(t *testing.T) {
|
||||
t.Errorf("Info %q does not contain label", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchpointSlotTracking(t *testing.T) {
|
||||
wpSlots = [4]bool{} // reset
|
||||
s := &Session{}
|
||||
|
||||
// All four slots are free initially.
|
||||
for i := range 4 {
|
||||
if s.IsWatchpointSlotUsed(i) {
|
||||
t.Errorf("slot %d should be free initially", i)
|
||||
}
|
||||
}
|
||||
if got := s.FindFreeWatchpointSlot(); got != 0 {
|
||||
t.Errorf("FindFreeWatchpointSlot() = %d, want 0", got)
|
||||
}
|
||||
|
||||
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
||||
wpSlots[0] = true
|
||||
wpSlots[2] = true
|
||||
|
||||
if !s.IsWatchpointSlotUsed(0) {
|
||||
t.Error("slot 0 should be in use")
|
||||
}
|
||||
if s.IsWatchpointSlotUsed(1) {
|
||||
t.Error("slot 1 should be free")
|
||||
}
|
||||
if !s.IsWatchpointSlotUsed(2) {
|
||||
t.Error("slot 2 should be in use")
|
||||
}
|
||||
if s.IsWatchpointSlotUsed(3) {
|
||||
t.Error("slot 3 should be free")
|
||||
}
|
||||
if got := s.FindFreeWatchpointSlot(); got != 1 {
|
||||
t.Errorf("FindFreeWatchpointSlot() = %d, want 1", got)
|
||||
}
|
||||
|
||||
// Out-of-range slot queries return false.
|
||||
if s.IsWatchpointSlotUsed(-1) {
|
||||
t.Error("slot -1 should be reported as free (out of range)")
|
||||
}
|
||||
if s.IsWatchpointSlotUsed(4) {
|
||||
t.Error("slot 4 should be reported as free (out of range)")
|
||||
}
|
||||
|
||||
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
||||
for i := range 4 {
|
||||
wpSlots[i] = true
|
||||
}
|
||||
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
||||
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
)
|
||||
@@ -34,19 +35,19 @@ func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||
// 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 string
|
||||
var result strings.Builder
|
||||
pc := addr
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result += fmt.Sprintf(" %#08x: <error: %v>\n", pc, err)
|
||||
result.WriteString(fmt.Sprintf(" %#08x: <error: %v>\n", pc, err))
|
||||
break
|
||||
}
|
||||
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||
result.WriteString(fmt.Sprintf(" %#08x: %s\n", pc, text))
|
||||
if length == 0 {
|
||||
length = 1
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/arch/arm64/arm64asm"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
inst, err := arm64asm.Decode(mem)
|
||||
if err != nil {
|
||||
return "???", 4, nil
|
||||
}
|
||||
text := arm64asm.GoSyntax(inst, addr, nil, nil)
|
||||
return text, 4, nil
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result string
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result += fmt.Sprintf(" %#08x: <error: %v>\n", pc, err)
|
||||
break
|
||||
}
|
||||
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||
if length == 0 {
|
||||
length = 4
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && loong64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/arch/loong64/loong64asm"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
inst, err := loong64asm.Decode(mem)
|
||||
if err != nil {
|
||||
return "???", 4, nil
|
||||
}
|
||||
text := loong64asm.GoSyntax(inst, addr, nil)
|
||||
return text, 4, nil
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result string
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result += fmt.Sprintf(" %#08x: <error: %v>\n", pc, err)
|
||||
break
|
||||
}
|
||||
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||
if length == 0 {
|
||||
length = 4
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/arch/riscv64/riscv64asm"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
inst, err := riscv64asm.Decode(mem)
|
||||
if err != nil {
|
||||
return "???", 4, nil
|
||||
}
|
||||
text := riscv64asm.GoSyntax(inst, addr, nil, nil)
|
||||
return text, inst.Len, nil
|
||||
}
|
||||
|
||||
// DisassembleN decodes up to n instructions starting at addr.
|
||||
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||
var result string
|
||||
pc := addr
|
||||
for range n {
|
||||
text, length, err := s.Disassemble(pc)
|
||||
if err != nil {
|
||||
result += fmt.Sprintf(" %#08x: <error: %v>\n", pc, err)
|
||||
break
|
||||
}
|
||||
result += fmt.Sprintf(" %#08x: %s\n", pc, text)
|
||||
if length == 0 {
|
||||
length = 4
|
||||
}
|
||||
pc += uint64(length)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && 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]
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address from the stack (amd64 ABI0 convention).
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return s.Peek(regs.GetSP())
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
func archSPLabel() string { return "RSP" }
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && arm64
|
||||
|
||||
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.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, v.V[i][8], v.V[i][0])
|
||||
fmt.Printf(" V%-2d = %016x%016x\n", i+1, v.V[i+1][8], v.V[i+1][0])
|
||||
}
|
||||
}
|
||||
|
||||
// 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,44 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && loong64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
||||
fmt.Printf(" PC = %#016x (func+%#x)\n", regs.R31, regs.R31-codeBase-funcOff)
|
||||
fmt.Printf(" SP = %#016x FP = %#016x\n", regs.R3, regs.R21)
|
||||
fmt.Printf(" RA = %#016x\n", regs.R1)
|
||||
fmt.Printf(" A0 = %#016x A1 = %#016x\n", regs.R4, regs.R5)
|
||||
fmt.Printf(" A2 = %#016x A3 = %#016x\n", regs.R6, regs.R7)
|
||||
fmt.Printf(" A4 = %#016x A5 = %#016x\n", regs.R8, regs.R9)
|
||||
fmt.Printf(" A6 = %#016x A7 = %#016x\n", regs.R10, regs.R11)
|
||||
fmt.Printf(" T0 = %#016x T1 = %#016x\n", regs.R12, regs.R13)
|
||||
fmt.Printf(" T2 = %#016x T3 = %#016x\n", regs.R14, regs.R15)
|
||||
fmt.Printf(" T4 = %#016x T5 = %#016x\n", regs.R16, regs.R17)
|
||||
fmt.Printf(" T6 = %#016x T7 = %#016x\n", regs.R18, regs.R19)
|
||||
fmt.Printf(" T8 = %#016x\n", regs.R20)
|
||||
fmt.Printf(" S0 = %#016x S1 = %#016x\n", regs.R22, regs.R23)
|
||||
fmt.Printf(" S2 = %#016x S3 = %#016x\n", regs.R24, regs.R25)
|
||||
fmt.Printf(" S4 = %#016x S5 = %#016x\n", regs.R26, regs.R27)
|
||||
fmt.Printf(" S6 = %#016x S7 = %#016x\n", regs.R28, regs.R29)
|
||||
fmt.Printf(" S8 = %#016x\n", regs.R30)
|
||||
}
|
||||
|
||||
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(" FCC = %#016x FCSR = %#x\n", v.FCC, v.FCSR)
|
||||
}
|
||||
|
||||
// archReturnAddr reads the return address from RA (loong64 convention).
|
||||
func archReturnAddr(s *Session, regs *Regs) (uint64, error) {
|
||||
return regs.R1, nil
|
||||
}
|
||||
|
||||
// archSPLabel returns the SP register name for display.
|
||||
func archSPLabel() string { return "SP" }
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && 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)
|
||||
}
|
||||
|
||||
// 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" }
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||
)
|
||||
|
||||
// buildGasm produces the gasm binary the debugger spawns as its debuggee.
|
||||
func buildGasm(t *testing.T) string {
|
||||
t.Helper()
|
||||
if p := os.Getenv("GASM_TEST_BIN"); p != "" {
|
||||
return p
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
cmd := exec.Command("go", "build", "-o", bin, "sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("build gasm: %v: %s", err, out)
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
// TestLaunchAndBreakpoint drives a real ptrace session end to end: launch the
|
||||
// debuggee, break on the first instruction of the function and expect a
|
||||
// breakpoint trap instead of a clean exit.
|
||||
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 (PTRACE_TRACEME
|
||||
// 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 := buildGasm(t)
|
||||
|
||||
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 {
|
||||
st, _ := os.ReadFile(fmt.Sprintf("/proc/%d/stat", sess.Pid()))
|
||||
status, _ := os.ReadFile(fmt.Sprintf("/proc/%d/status", sess.Pid()))
|
||||
t.Fatalf("Continue: %v\nstate: %s\n%s", err, fieldName(st), statusDump(status))
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
func fieldName(stat []byte) string {
|
||||
f := strings.Split(string(stat), " ")
|
||||
if len(f) > 2 {
|
||||
return "state=" + f[2]
|
||||
}
|
||||
return "no stat"
|
||||
}
|
||||
|
||||
func statusDump(b []byte) string {
|
||||
var out []string
|
||||
for l := range strings.SplitSeq(string(b), "\n") {
|
||||
if strings.HasPrefix(l, "State") || strings.HasPrefix(l, "Pid") ||
|
||||
strings.HasPrefix(l, "PPid") || strings.HasPrefix(l, "TracerPid") ||
|
||||
strings.HasPrefix(l, "Threads") || strings.HasPrefix(l, "SigPnd") ||
|
||||
strings.HasPrefix(l, "SigBlk") || strings.HasPrefix(l, "SigIgn") {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Session is a ptrace debugging session controlling one debuggee process.
|
||||
type Session struct {
|
||||
pid int
|
||||
cmd *exec.Cmd
|
||||
stopped bool
|
||||
exited bool
|
||||
codeBase uint64 // base address of the JIT code in the debuggee
|
||||
}
|
||||
|
||||
// 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 PTRACE_TRACEME 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}
|
||||
|
||||
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
|
||||
// exact region the kernel was written to. Scanning /proc/pid/maps for
|
||||
// any RWX region is only the fallback.
|
||||
if data, err := os.ReadFile(filepath.Join(tmpDir, "codebase")); err == nil {
|
||||
fmt.Sscanf(string(data), "%d", &s.codeBase)
|
||||
}
|
||||
if s.codeBase == 0 {
|
||||
s.codeBase = findRWXMapping(s.pid)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// wait waits for the debuggee to stop and returns the wait status.
|
||||
func (s *Session) wait() error {
|
||||
var ws syscall.WaitStatus
|
||||
_, err := syscall.Wait4(s.pid, &ws, 0, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ws.Exited() {
|
||||
s.exited = true
|
||||
return fmt.Errorf("debuggee exited with status %d", ws.ExitStatus())
|
||||
}
|
||||
s.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitStopped consumes ptrace-stop events until one the debugger cares
|
||||
// about arrives: SIGTRAP (a breakpoint or a completed single-step) or the
|
||||
// debuggee's own SIGSTOP. A Go tracee's runtime raises SIGURG for
|
||||
// asynchronous preemption, and every signal on a traced thread surfaces as
|
||||
// a signal-delivery-stop, so those are suppressed and the tracee resumed
|
||||
// without them. Runtime noise is why a single wait can return in the
|
||||
// middle of runtime code and a resume can then fail: the event stream must
|
||||
// be drained by the tracer.
|
||||
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
|
||||
return sig, nil
|
||||
default:
|
||||
// Runtime noise (SIGURG preemption and friends): resume the
|
||||
// tracee without delivering the signal.
|
||||
if _, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_CONT),
|
||||
uintptr(s.pid),
|
||||
0, 0, 0, 0,
|
||||
); errno != 0 {
|
||||
return 0, fmt.Errorf("debug: PTRACE_CONT: %w", errno)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
buf := make([]byte, 8)
|
||||
if _, err := mem.ReadAt(buf, int64(addr)); 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.
|
||||
func (s *Session) Poke(addr, val uint64) error {
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
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 := mem.WriteAt(buf, int64(addr)); 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.
|
||||
func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||
out := make([]byte, length)
|
||||
for i := 0; i < length; i += 8 {
|
||||
word, err := s.Peek(addr + uint64(i))
|
||||
if err != nil {
|
||||
return out[:i], err
|
||||
}
|
||||
for j := 0; j < 8 && i+j < length; j++ {
|
||||
out[i+j] = byte(word >> (8 * j))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr.
|
||||
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||
for i := 0; i < len(data); i += 8 {
|
||||
end := min(i+8, len(data))
|
||||
var word uint64
|
||||
for j := 0; j < end-i; j++ {
|
||||
word |= uint64(data[i+j]) << (8 * j)
|
||||
}
|
||||
if end-i < 8 {
|
||||
existing, err := s.Peek(addr + uint64(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mask := ^((uint64(1) << (8 * (end - i))) - 1)
|
||||
word = (existing & mask) | word
|
||||
}
|
||||
if err := s.Poke(addr+uint64(i), word); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step executes a single instruction in the debuggee.
|
||||
func (s *Session) Step() error {
|
||||
if s.exited {
|
||||
return fmt.Errorf("debug: debuggee has exited")
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SINGLESTEP),
|
||||
uintptr(s.pid),
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_SINGLESTEP: %w", errno)
|
||||
}
|
||||
_, 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")
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_CONT),
|
||||
uintptr(s.pid),
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_CONT: %w", errno)
|
||||
}
|
||||
_, 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.
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// findRWXMapping reads /proc/pid/maps and returns the base address of the
|
||||
// first read-write-execute mapping (the JIT code region).
|
||||
func findRWXMapping(pid int) uint64 {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
perms := fields[1]
|
||||
if len(perms) >= 3 && perms[0] == 'r' && perms[1] == 'w' && perms[2] == 'x' {
|
||||
var start uint64
|
||||
fmt.Sscanf(fields[0], "%x-", &start)
|
||||
return start
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+2
-315
@@ -3,164 +3,14 @@
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
// Package debug implements the interactive debugger for gasm (Phase 4):
|
||||
// single-stepping, breakpoints, register and memory inspection for
|
||||
// JIT-assembled Plan 9 amd64 functions, controlled via ptrace.
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Session is a ptrace debugging session controlling one debuggee process.
|
||||
type Session struct {
|
||||
pid int
|
||||
cmd *exec.Cmd
|
||||
stopped bool
|
||||
exited bool
|
||||
codeBase uint64 // base address of the JIT code in the debuggee
|
||||
}
|
||||
|
||||
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||
// attaches to it via ptrace. The debuggee assembles the file, maps the
|
||||
// JIT code, calls PTRACE_TRACEME and raises SIGSTOP; Launch waits for
|
||||
// that initial stop and returns a ready Session.
|
||||
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
|
||||
// based on the buffer specification. Returns the Session and the buffer
|
||||
// addresses (in the order they appear in the spec).
|
||||
func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) {
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||
}
|
||||
if gasmBin != "" {
|
||||
self = gasmBin
|
||||
}
|
||||
|
||||
// Write the arg block to a temp file (the child reads it).
|
||||
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)
|
||||
}
|
||||
|
||||
// Write the buffer spec if present.
|
||||
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", "--target", "--func", funcName, "--args", argsFile, asmPath)
|
||||
cmd.Env = append(os.Environ(), "GASM_DEBUG_TMP="+tmpDir)
|
||||
cmd.Stdout = nil // output goes to the debugger, not the terminal
|
||||
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}
|
||||
|
||||
// Wait for the child to signal readiness and stop. The child calls
|
||||
// PTRACE_TRACEME then SIGSTOP, so Wait4 with WUNTRACED observes the
|
||||
// ptrace-stop directly (no PTRACE_ATTACH needed).
|
||||
readyFile := filepath.Join(tmpDir, "ready")
|
||||
for i := 0; i < 500; i++ {
|
||||
if _, err := os.Stat(readyFile); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
var ws syscall.WaitStatus
|
||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||
cmd.Process.Kill()
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, nil, fmt.Errorf("debug: wait for stop: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the debuggee to reach the function entry point.
|
||||
entryFile := filepath.Join(tmpDir, "entry")
|
||||
for i := 0; i < 500; i++ {
|
||||
if _, err := os.Stat(entryFile); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Continue the debuggee to the entry point.
|
||||
if err := s.Continue(); err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: continue to entry: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the entry stop.
|
||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||
return nil, nil, fmt.Errorf("debug: wait for entry: %w", err)
|
||||
}
|
||||
|
||||
s.stopped = true
|
||||
|
||||
// Read the code base from /proc/pid/maps (find the RWX mapping).
|
||||
s.codeBase = findRWXMapping(s.pid)
|
||||
if s.codeBase == 0 {
|
||||
// Fallback: try the file the child wrote.
|
||||
baseFile := filepath.Join(tmpDir, "codebase")
|
||||
if data, err := os.ReadFile(baseFile); err == nil {
|
||||
fmt.Sscanf(string(data), "%d", &s.codeBase)
|
||||
}
|
||||
}
|
||||
|
||||
// Read buffer addresses if buffers were allocated.
|
||||
var bufAddrs []uint64
|
||||
if bufSpec != "" {
|
||||
addrFile := filepath.Join(tmpDir, "bufaddrs")
|
||||
if data, err := os.ReadFile(addrFile); err == nil {
|
||||
for _, line := range strings.Split(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
|
||||
}
|
||||
|
||||
// wait waits for the debuggee to stop and returns the wait status.
|
||||
func (s *Session) wait() error {
|
||||
var ws syscall.WaitStatus
|
||||
_, err := syscall.Wait4(s.pid, &ws, 0, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ws.Exited() {
|
||||
s.exited = true
|
||||
return fmt.Errorf("debuggee exited with status %d", ws.ExitStatus())
|
||||
}
|
||||
s.stopped = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var regs Regs
|
||||
@@ -233,179 +83,16 @@ type VectorRegs struct {
|
||||
}
|
||||
|
||||
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
||||
// Falls back to XMM if XSAVE is unavailable.
|
||||
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||
var v VectorRegs
|
||||
fp, err := s.GetFPRegs()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
// PTRACE_GETFPREGS gives XMM registers (lower 128 bits).
|
||||
// For YMM we'd need XSAVE; for now, copy XMM and zero the upper half.
|
||||
for i := 0; i < 16; i++ {
|
||||
for j := 0; j < 16; j++ {
|
||||
for i := range 16 {
|
||||
for j := range 16 {
|
||||
v.YMM[i][j] = fp.XMM[i][j]
|
||||
}
|
||||
// Upper 128 bits would come from XSAVE, not available via GETFPREGS.
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||
// Uses /proc/pid/mem which works reliably with Go's multi-threaded runtime.
|
||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
buf := make([]byte, 8)
|
||||
if _, err := mem.ReadAt(buf, int64(addr)); 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.
|
||||
func (s *Session) Poke(addr, val uint64) error {
|
||||
mem, err := os.OpenFile(fmt.Sprintf("/proc/%d/mem", s.pid), os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("debug: open /proc/%d/mem: %w", s.pid, err)
|
||||
}
|
||||
defer mem.Close()
|
||||
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 := mem.WriteAt(buf, int64(addr)); 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.
|
||||
func (s *Session) ReadMemory(addr uint64, length int) ([]byte, error) {
|
||||
out := make([]byte, length)
|
||||
for i := 0; i < length; i += 8 {
|
||||
word, err := s.Peek(addr + uint64(i))
|
||||
if err != nil {
|
||||
return out[:i], err
|
||||
}
|
||||
for j := 0; j < 8 && i+j < length; j++ {
|
||||
out[i+j] = byte(word >> (8 * j))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WriteMemory writes bytes to the debuggee's memory at addr.
|
||||
func (s *Session) WriteMemory(addr uint64, data []byte) error {
|
||||
for i := 0; i < len(data); i += 8 {
|
||||
end := i + 8
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
var word uint64
|
||||
for j := 0; j < end-i; j++ {
|
||||
word |= uint64(data[i+j]) << (8 * j)
|
||||
}
|
||||
// For partial writes, read-modify-write the existing word.
|
||||
if end-i < 8 {
|
||||
existing, err := s.Peek(addr + uint64(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Clear the bytes we're overwriting and merge.
|
||||
mask := ^((uint64(1) << (8 * (end - i))) - 1)
|
||||
word = (existing & mask) | word
|
||||
}
|
||||
if err := s.Poke(addr+uint64(i), word); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step executes a single instruction in the debuggee.
|
||||
func (s *Session) Step() error {
|
||||
if s.exited {
|
||||
return fmt.Errorf("debug: debuggee has exited")
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SINGLESTEP),
|
||||
uintptr(s.pid),
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_SINGLESTEP: %w", errno)
|
||||
}
|
||||
return s.wait()
|
||||
}
|
||||
|
||||
// Continue resumes execution until the next breakpoint or exit.
|
||||
func (s *Session) Continue() error {
|
||||
if s.exited {
|
||||
return fmt.Errorf("debug: debuggee has exited")
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_CONT),
|
||||
uintptr(s.pid),
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_CONT: %w", errno)
|
||||
}
|
||||
return s.wait()
|
||||
}
|
||||
|
||||
// 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.
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// findRWXMapping reads /proc/pid/maps and returns the base address of the
|
||||
// first read-write-execute mapping (the JIT code region).
|
||||
func findRWXMapping(pid int) uint64 {
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/maps", pid))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
// Format: addr-addr perms offset dev inode pathname
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
perms := fields[1]
|
||||
if len(perms) >= 3 && perms[0] == 'r' && perms[1] == 'w' && perms[2] == 'x' {
|
||||
// Parse the start address.
|
||||
var start uint64
|
||||
fmt.Sscanf(fields[0], "%x-", &start)
|
||||
return start
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var regs Regs
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(®s)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno)
|
||||
}
|
||||
return regs, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(regs)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ntPrFPREG is the NT_PRFPREG note type (ELF NT ver): the FP register set.
|
||||
const ntPrFPREG = 0x2
|
||||
|
||||
// FPRegs holds the arm64 FP/NEON register state, matching the kernel's
|
||||
// user_fpsimd_struct layout (32 128-bit V registers, then FPSR and FPCR).
|
||||
type FPRegs struct {
|
||||
V [32][16]byte // V0-V31 (128-bit NEON/FP registers)
|
||||
FPSR uint32
|
||||
FPCR uint32
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FP register state via PTRACE_GETREGSET with
|
||||
// NT_PRFPREG (this architecture has no PTRACE_GETFPREGS request).
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(&fp)),
|
||||
Len: uint64(unsafe.Sizeof(fp)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntPrFPREG),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fp, fmt.Errorf("debug: PTRACE_GETREGSET (NT_PRFPREG): %w", errno)
|
||||
}
|
||||
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,97 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && loong64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var regs Regs
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(®s)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno)
|
||||
}
|
||||
return regs, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(regs)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ntPrFPREG is the NT_PRFPREG note type (ELF NT ver): the FP register set.
|
||||
const ntPrFPREG = 0x2
|
||||
|
||||
// FPRegs holds the LoongArch FP register state, matching the kernel's
|
||||
// user_fp_struct layout (32 64-bit FP registers, the fcc condition flags,
|
||||
// and fcsr).
|
||||
type FPRegs struct {
|
||||
F [32]uint64 // F0-F31 (64-bit FP registers)
|
||||
FCC uint64 // eight per-register condition flags, packed
|
||||
FCSR uint32
|
||||
}
|
||||
|
||||
// GetFPRegs retrieves the FP register state via PTRACE_GETREGSET with
|
||||
// NT_PRFPREG (this architecture has no PTRACE_GETFPREGS request).
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(&fp)),
|
||||
Len: uint64(unsafe.Sizeof(fp)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntPrFPREG),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fp, fmt.Errorf("debug: PTRACE_GETREGSET (NT_PRFPREG): %w", errno)
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
// VectorRegs holds the FP register state shown by the regs command
|
||||
// (the scalar FP subset: 32 64-bit registers, fcc and fcsr; the LSX/LASX
|
||||
// vector files are not read yet).
|
||||
type VectorRegs struct {
|
||||
F [32]uint64
|
||||
FCC 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, FCC: fp.FCC, FCSR: fp.FCSR}, nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// GetRegs reads the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) GetRegs() (Regs, error) {
|
||||
var regs Regs
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(®s)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return regs, fmt.Errorf("debug: PTRACE_GETREGS: %w", errno)
|
||||
}
|
||||
return regs, nil
|
||||
}
|
||||
|
||||
// SetRegs writes the general-purpose registers of the stopped debuggee.
|
||||
func (s *Session) SetRegs(regs *Regs) error {
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_SETREGS),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(regs)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("debug: PTRACE_SETREGS: %w", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ntPrFPREG is the NT_PRFPREG note type (ELF NT ver): the FP register set.
|
||||
const ntPrFPREG = 0x2
|
||||
|
||||
// FPRegs holds the RISC-V FP register state, matching the kernel's
|
||||
// user_fp_struct layout (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 PTRACE_GETREGSET with
|
||||
// NT_PRFPREG (this architecture has no PTRACE_GETFPREGS request).
|
||||
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||
var fp FPRegs
|
||||
iovec := syscall.Iovec{
|
||||
Base: (*byte)(unsafe.Pointer(&fp)),
|
||||
Len: uint64(unsafe.Sizeof(fp)),
|
||||
}
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETREGSET),
|
||||
uintptr(s.pid),
|
||||
uintptr(ntPrFPREG),
|
||||
uintptr(unsafe.Pointer(&iovec)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return fp, fmt.Errorf("debug: PTRACE_GETREGSET (NT_PRFPREG): %w", errno)
|
||||
}
|
||||
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,95 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux amd64 user_regs_struct layout).
|
||||
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
|
||||
OrigRAX uint64
|
||||
RIP uint64
|
||||
CS uint64
|
||||
RFLAGS uint64
|
||||
RSP uint64
|
||||
SS uint64
|
||||
FSBase uint64
|
||||
GSBase uint64
|
||||
DS uint64
|
||||
ES uint64
|
||||
FS uint64
|
||||
GS 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.
|
||||
const breakpointPCAdjust = 1
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && arm64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux arm64 user_pt_regs layout).
|
||||
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
|
||||
}
|
||||
|
||||
// PC 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.
|
||||
const breakpointPCAdjust = 4
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && loong64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux loong64 user_pt_regs layout).
|
||||
type Regs struct {
|
||||
R0 uint64 // zero
|
||||
R1 uint64 // RA (return address)
|
||||
R2 uint64 // TP (thread pointer)
|
||||
R3 uint64 // SP (stack pointer)
|
||||
R4 uint64 // A0
|
||||
R5 uint64 // A1
|
||||
R6 uint64 // A2
|
||||
R7 uint64 // A3
|
||||
R8 uint64 // A4
|
||||
R9 uint64 // A5
|
||||
R10 uint64 // A6
|
||||
R11 uint64 // A7
|
||||
R12 uint64 // T0
|
||||
R13 uint64 // T1
|
||||
R14 uint64 // T2
|
||||
R15 uint64 // T3
|
||||
R16 uint64 // T4
|
||||
R17 uint64 // T5
|
||||
R18 uint64 // T6
|
||||
R19 uint64 // T7
|
||||
R20 uint64 // T8
|
||||
R21 uint64 // FP (frame pointer)
|
||||
R22 uint64 // S0
|
||||
R23 uint64 // S1
|
||||
R24 uint64 // S2
|
||||
R25 uint64 // S3
|
||||
R26 uint64 // S4
|
||||
R27 uint64 // S5
|
||||
R28 uint64 // S6
|
||||
R29 uint64 // S7
|
||||
R30 uint64 // S8
|
||||
R31 uint64 // PC
|
||||
}
|
||||
|
||||
// GetPC returns the program counter.
|
||||
func (r *Regs) GetPC() uint64 { return r.R31 }
|
||||
|
||||
// SetPC sets the program counter.
|
||||
func (r *Regs) SetPC(pc uint64) { r.R31 = pc }
|
||||
|
||||
// GetSP returns the stack pointer.
|
||||
func (r *Regs) GetSP() uint64 { return r.R3 }
|
||||
|
||||
// RegValue returns the value of the named register, or false if unknown.
|
||||
func (r *Regs) RegValue(name string) (uint64, bool) {
|
||||
switch name {
|
||||
case "r0", "zero":
|
||||
return r.R0, true
|
||||
case "r1", "ra":
|
||||
return r.R1, true
|
||||
case "r2", "tp":
|
||||
return r.R2, true
|
||||
case "r3", "sp":
|
||||
return r.R3, true
|
||||
case "r4", "a0":
|
||||
return r.R4, true
|
||||
case "r5", "a1":
|
||||
return r.R5, true
|
||||
case "r6", "a2":
|
||||
return r.R6, true
|
||||
case "r7", "a3":
|
||||
return r.R7, true
|
||||
case "r8", "a4":
|
||||
return r.R8, true
|
||||
case "r9", "a5":
|
||||
return r.R9, true
|
||||
case "r10", "a6":
|
||||
return r.R10, true
|
||||
case "r11", "a7":
|
||||
return r.R11, true
|
||||
case "r12", "t0":
|
||||
return r.R12, true
|
||||
case "r13", "t1":
|
||||
return r.R13, true
|
||||
case "r14", "t2":
|
||||
return r.R14, true
|
||||
case "r15", "t3":
|
||||
return r.R15, true
|
||||
case "r16", "t4":
|
||||
return r.R16, true
|
||||
case "r17", "t5":
|
||||
return r.R17, true
|
||||
case "r18", "t6":
|
||||
return r.R18, true
|
||||
case "r19", "t7":
|
||||
return r.R19, true
|
||||
case "r20", "t8":
|
||||
return r.R20, true
|
||||
case "r21", "fp":
|
||||
return r.R21, true
|
||||
case "r22", "s0":
|
||||
return r.R22, true
|
||||
case "r23", "s1":
|
||||
return r.R23, true
|
||||
case "r24", "s2":
|
||||
return r.R24, true
|
||||
case "r25", "s3":
|
||||
return r.R25, true
|
||||
case "r26", "s4":
|
||||
return r.R26, true
|
||||
case "r27", "s5":
|
||||
return r.R27, true
|
||||
case "r28", "s6":
|
||||
return r.R28, true
|
||||
case "r29", "s7":
|
||||
return r.R29, true
|
||||
case "r30", "s8":
|
||||
return r.R30, true
|
||||
case "r31", "pc":
|
||||
return r.R31, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// breakpointInsn is the software breakpoint instruction (BRK $0).
|
||||
var breakpointInsn = []byte{0x05, 0x00, 0x2a, 0x00} // break 0
|
||||
|
||||
// breakpointPCAdjust is how far PC is past the breakpoint instruction after a trap.
|
||||
const breakpointPCAdjust = 4
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
// Regs holds the full general-purpose register set of a traced process
|
||||
// (the Linux riscv64 user_regs_struct layout).
|
||||
type Regs struct {
|
||||
PC uint64
|
||||
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.
|
||||
const breakpointPCAdjust = 4
|
||||
+78
-157
@@ -1,14 +1,14 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && amd64
|
||||
//go:build linux
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,19 +26,15 @@ type SourceLine struct {
|
||||
Line int
|
||||
}
|
||||
|
||||
// REPL runs the interactive debugger loop. On entry, the debuggee is
|
||||
// stopped in the Go runtime (after PTRACE_TRACEME + SIGSTOP). The REPL
|
||||
// sets a temporary breakpoint at the function entry, continues to it, and
|
||||
// then presents the prompt — so the user starts debugging at the first
|
||||
// instruction of the assembled function.
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
||||
// REPL runs the interactive debugger loop, reading commands from in (pass
|
||||
// os.Stdin interactively, or a bytes.Reader/script file for headless runs).
|
||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine, in io.Reader) {
|
||||
entryAddr := codeBase + uint64(funcOffset)
|
||||
|
||||
// The debuggee is already stopped at the function entry point.
|
||||
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner := bufio.NewScanner(in)
|
||||
|
||||
for {
|
||||
fmt.Print("(gasm) ")
|
||||
@@ -64,7 +60,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
continue
|
||||
}
|
||||
printRegs(®s, codeBase, uint64(funcOffset))
|
||||
// Also show vector registers.
|
||||
vregs, err := s.GetVectorRegs()
|
||||
if err != nil {
|
||||
fmt.Printf(" (vector regs unavailable: %v)\n", err)
|
||||
@@ -77,7 +72,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
if len(parts) > 1 {
|
||||
n, _ = strconv.Atoi(parts[1])
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
break
|
||||
@@ -89,24 +84,22 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
text, _, _ := s.Disassemble(regs.RIP)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
||||
pc := regs.GetPC()
|
||||
text, _, _ := s.Disassemble(pc)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
||||
}
|
||||
|
||||
case "next", "n":
|
||||
// Step over: if the current instruction is a CALL, set a
|
||||
// breakpoint after it and continue; otherwise single-step.
|
||||
regs, _ := s.GetRegs()
|
||||
text, instLen, _ := s.Disassemble(regs.RIP)
|
||||
if strings.HasPrefix(strings.ToLower(text), "call") {
|
||||
// Set a temporary breakpoint after the CALL.
|
||||
afterAddr := regs.RIP + uint64(instLen)
|
||||
bp, err := bm.Set(afterAddr, "(next)")
|
||||
pc := regs.GetPC()
|
||||
text, instLen, _ := s.Disassemble(pc)
|
||||
if strings.HasPrefix(strings.ToLower(text), "call") || strings.HasPrefix(strings.ToLower(text), "bl") {
|
||||
afterAddr := pc + uint64(instLen)
|
||||
_, err := bm.Set(afterAddr, "(next)")
|
||||
if err != nil {
|
||||
fmt.Printf("cannot set next breakpoint: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Continue until the breakpoint.
|
||||
for _, b := range bm.All() {
|
||||
bm.Reinsert(b.Addr)
|
||||
}
|
||||
@@ -117,9 +110,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
bm.HandleTrap(®s)
|
||||
bm.Clear(afterAddr)
|
||||
_ = bp
|
||||
} else {
|
||||
// Not a CALL — just single-step.
|
||||
if err := s.Step(); err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
@@ -127,26 +118,23 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
if !s.Exited() {
|
||||
regs, _ := s.GetRegs()
|
||||
text, _, _ := s.Disassemble(regs.RIP)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", regs.RIP, regs.RIP-codeBase-uint64(funcOffset), text)
|
||||
pc := regs.GetPC()
|
||||
text, _, _ := s.Disassemble(pc)
|
||||
fmt.Printf("=> %#x (func+%#x): %s\n", pc, pc-codeBase-uint64(funcOffset), text)
|
||||
}
|
||||
|
||||
case "finish", "fin":
|
||||
// Run until the current function returns.
|
||||
// For NOSPLIT frame=0: return address is at [RSP].
|
||||
regs, _ := s.GetRegs()
|
||||
retAddr, err := s.Peek(regs.RSP)
|
||||
retAddr, err := archReturnAddr(s, ®s)
|
||||
if err != nil {
|
||||
fmt.Printf("cannot read return address: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Set a temporary breakpoint at the return address.
|
||||
bp, err := bm.Set(retAddr, "(finish)")
|
||||
_, err = bm.Set(retAddr, "(finish)")
|
||||
if err != nil {
|
||||
fmt.Printf("cannot set finish breakpoint: %v\n", err)
|
||||
continue
|
||||
}
|
||||
// Continue until the breakpoint.
|
||||
for _, b := range bm.All() {
|
||||
bm.Reinsert(b.Addr)
|
||||
}
|
||||
@@ -159,12 +147,11 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
bm.HandleTrap(®s)
|
||||
}
|
||||
bm.Clear(retAddr)
|
||||
_ = bp
|
||||
if s.Exited() {
|
||||
fmt.Println("debuggee exited")
|
||||
} else {
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Printf("finished, now at %#x\n", regs.RIP)
|
||||
fmt.Printf("finished, now at %#x\n", regs.GetPC())
|
||||
}
|
||||
|
||||
case "continue", "c":
|
||||
@@ -172,9 +159,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println("debuggee exited")
|
||||
continue
|
||||
}
|
||||
// Loop: continue until a breakpoint fires (condition met) or exit.
|
||||
for {
|
||||
// Re-insert all breakpoints before continuing.
|
||||
for _, bp := range bm.All() {
|
||||
bm.Reinsert(bp.Addr)
|
||||
}
|
||||
@@ -186,7 +171,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println("debuggee exited")
|
||||
break
|
||||
}
|
||||
// Check for watchpoint hits.
|
||||
reason, wpAddr := s.StopInfo()
|
||||
if reason == StopWatchpoint {
|
||||
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
||||
@@ -194,6 +178,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
if bp := bm.HandleTrap(®s); bp != nil {
|
||||
// Execute the instruction under the restored breakpoint
|
||||
// so the next continue cannot re-trap on the same
|
||||
// breakpoint; the process parks right after it.
|
||||
if err := s.Step(); err != nil {
|
||||
fmt.Println(err)
|
||||
break
|
||||
}
|
||||
name := bp.Label
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("%#x", bp.Addr)
|
||||
@@ -201,7 +192,6 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf("breakpoint hit: %s (func+%#x)\n", name, bp.Addr-codeBase-uint64(funcOffset))
|
||||
break
|
||||
}
|
||||
// Condition not met (or single-step trap) — re-insert and continue.
|
||||
}
|
||||
|
||||
case "break", "b":
|
||||
@@ -209,11 +199,9 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println("usage: break <label|addr|line> [if <reg> <op> <val>]")
|
||||
continue
|
||||
}
|
||||
// Try as a line number first.
|
||||
var addr uint64
|
||||
var label string
|
||||
if lineNum, err := strconv.Atoi(parts[1]); err == nil && lineNum > 0 {
|
||||
// Find the byte offset for this line.
|
||||
off := offsetForLine(lines, lineNum)
|
||||
if off < 0 {
|
||||
fmt.Printf("no instruction at line %d\n", lineNum)
|
||||
@@ -228,17 +216,18 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf("unknown label, address, or line: %s\n", parts[1])
|
||||
continue
|
||||
}
|
||||
// Parse optional condition: "if <reg> <op> <value>"
|
||||
var cond *Condition
|
||||
if len(parts) >= 6 && parts[2] == "if" {
|
||||
val, err := strconv.ParseUint(parts[5], 0, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("invalid condition value: %s\n", parts[5])
|
||||
continue
|
||||
reg := strings.ToLower(parts[3])
|
||||
op := parts[4]
|
||||
operand := parts[5]
|
||||
if val, err := strconv.ParseUint(operand, 0, 64); err == nil {
|
||||
cond = &Condition{Reg: reg, Op: op, Value: val}
|
||||
} else {
|
||||
cond = &Condition{Reg: reg, Op: op, Reg2: strings.ToLower(operand)}
|
||||
}
|
||||
cond = &Condition{Reg: strings.ToLower(parts[3]), Op: parts[4], Value: val}
|
||||
} else if len(parts) >= 4 && parts[2] == "if" {
|
||||
fmt.Println("usage: break <label|addr> if <reg> <op> <value>")
|
||||
fmt.Println("usage: break <label|addr> if <reg> <op> <value|reg>")
|
||||
continue
|
||||
}
|
||||
bp, err := bm.SetWithCond(addr, label, cond)
|
||||
@@ -282,7 +271,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
|
||||
case "x":
|
||||
regs, _ := s.GetRegs()
|
||||
addr := regs.RIP // default: current PC
|
||||
addr := regs.GetPC()
|
||||
length := 64
|
||||
if len(parts) > 1 {
|
||||
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||
@@ -314,9 +303,8 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf("invalid value: %s\n", arg)
|
||||
continue
|
||||
}
|
||||
// Write as 8-byte word if it looks like a large value, else single byte.
|
||||
if v > 255 {
|
||||
for j := 0; j < 8; j++ {
|
||||
for j := range 8 {
|
||||
bytes = append(bytes, byte(v>>(8*j)))
|
||||
}
|
||||
} else {
|
||||
@@ -364,11 +352,11 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
}
|
||||
}
|
||||
regs, _ := s.GetRegs()
|
||||
fmt.Print(s.DisassembleN(regs.RIP, n))
|
||||
fmt.Print(s.DisassembleN(regs.GetPC(), n))
|
||||
|
||||
case "where":
|
||||
regs, _ := s.GetRegs()
|
||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
||||
line := lineAt(lines, funcOff)
|
||||
label := nearestLabel(labels, funcOff)
|
||||
fmt.Printf(" func+%#x", funcOff)
|
||||
@@ -381,32 +369,32 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Println()
|
||||
|
||||
case "help", "h", "?":
|
||||
fmt.Println(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||
fmt.Printf(` break <label|addr> [if <reg> <op> <val>] set a breakpoint
|
||||
delete <label|addr> remove a breakpoint
|
||||
info break list all breakpoints
|
||||
watch <addr> [r|w] set a hardware watchpoint (write by default)
|
||||
unwatch clear all watchpoints
|
||||
watch <addr> [r|w] [size] set a hardware watchpoint (write by default)
|
||||
unwatch [<slot>] clear one or all watchpoints
|
||||
step [n], s single-step n instructions
|
||||
next, n step over CALL
|
||||
next, n step over CALL/BL
|
||||
continue, c run until breakpoint or exit
|
||||
disas [n], u disassemble n instructions at PC
|
||||
regs print registers and RFLAGS
|
||||
regs print registers
|
||||
where show source line and nearest label
|
||||
stack show stack near RSP (args + return address)
|
||||
stack show stack near %s (args + return address)
|
||||
x [addr] [len] hex-dump memory
|
||||
w <addr> <val...> write bytes to memory
|
||||
labels, l list function labels
|
||||
help, h, ? this help
|
||||
quit, q kill debuggee and exit`)
|
||||
quit, q kill debuggee and exit`, archSPLabel())
|
||||
|
||||
case "stack":
|
||||
regs, _ := s.GetRegs()
|
||||
// For NOSPLIT frame=0: [RSP] = return address, [RSP+8..] = args.
|
||||
retAddr, _ := s.Peek(regs.RSP)
|
||||
fmt.Printf(" [RSP] return addr = %#x\n", retAddr)
|
||||
sp := regs.GetSP()
|
||||
retAddr, _ := archReturnAddr(s, ®s)
|
||||
fmt.Printf(" [%s] return addr = %#x\n", archSPLabel(), retAddr)
|
||||
if argsSize > 0 {
|
||||
fmt.Printf(" args (%d bytes at RSP+8):\n", argsSize)
|
||||
argBytes, err := s.ReadMemory(regs.RSP+8, argsSize)
|
||||
fmt.Printf(" args (%d bytes at %s+8):\n", argsSize, archSPLabel())
|
||||
argBytes, err := s.ReadMemory(sp+8, argsSize)
|
||||
if err == nil {
|
||||
for i := 0; i < argsSize; i += 8 {
|
||||
var v uint64
|
||||
@@ -420,7 +408,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
|
||||
case "bt", "backtrace":
|
||||
regs, _ := s.GetRegs()
|
||||
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||
funcOff := int(regs.GetPC() - codeBase - uint64(funcOffset))
|
||||
line := lineAt(lines, funcOff)
|
||||
label := nearestLabel(labels, funcOff)
|
||||
fmt.Printf(" #0 func+%#x", funcOff)
|
||||
@@ -431,7 +419,7 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
fmt.Printf(" [line %d]", line)
|
||||
}
|
||||
fmt.Println()
|
||||
retAddr, _ := s.Peek(regs.RSP)
|
||||
retAddr, _ := archReturnAddr(s, ®s)
|
||||
fmt.Printf(" #1 return to %#x\n", retAddr)
|
||||
|
||||
case "watch":
|
||||
@@ -457,28 +445,39 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
if len(parts) > 3 {
|
||||
size, _ = strconv.Atoi(parts[3])
|
||||
}
|
||||
// Find a free slot (0-3).
|
||||
slot := -1
|
||||
for i := 0; i < 4; i++ {
|
||||
// Simple: use slot 0 for now.
|
||||
slot = i
|
||||
break
|
||||
}
|
||||
slot := s.FindFreeWatchpointSlot()
|
||||
if slot < 0 {
|
||||
fmt.Println("no free watchpoint slots")
|
||||
fmt.Println("no free watchpoint slots (use 'unwatch <slot>' to clear one)")
|
||||
continue
|
||||
}
|
||||
if err := s.SetWatchpoint(slot, addr, typ, size); err != nil {
|
||||
fmt.Printf("watch: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("watchpoint %d set: %#x (%s, %d bytes)\n", slot, addr, parts[2], size)
|
||||
typStr := "w"
|
||||
if typ == WatchRead {
|
||||
typStr = "r"
|
||||
}
|
||||
fmt.Printf("watchpoint %d set: %#x (%s, %d bytes)\n", slot, addr, typStr, size)
|
||||
}
|
||||
|
||||
case "unwatch":
|
||||
if err := s.ClearAllWatchpoints(); err != nil {
|
||||
fmt.Printf("unwatch: %v\n", err)
|
||||
if len(parts) >= 2 {
|
||||
slot, err := strconv.Atoi(parts[1])
|
||||
if err != nil || slot < 0 || slot > 3 {
|
||||
fmt.Println("usage: unwatch [<slot>]")
|
||||
continue
|
||||
}
|
||||
if err := s.ClearWatchpoint(slot); err != nil {
|
||||
fmt.Printf("unwatch: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("watchpoint %d cleared\n", slot)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("all watchpoints cleared")
|
||||
if err := s.ClearAllWatchpoints(); err != nil {
|
||||
fmt.Printf("unwatch: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("all watchpoints cleared")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -488,80 +487,9 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
||||
s.Kill()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// printVectorRegs displays the YMM registers.
|
||||
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) {
|
||||
// Show as 8 32-bit values.
|
||||
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] // trim trailing space
|
||||
}
|
||||
|
||||
func hexDump(addr uint64, data []byte) {
|
||||
for i := 0; i < len(data); i += 16 {
|
||||
end := i + 16
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
end := min(i+16, len(data))
|
||||
fmt.Printf(" %#08x:", addr+uint64(i))
|
||||
for j := i; j < i+16; j++ {
|
||||
if j < end {
|
||||
@@ -583,21 +511,18 @@ func hexDump(addr uint64, data []byte) {
|
||||
}
|
||||
|
||||
func resolveAddr(s string, codeBase, funcOff uint64, labels []Label) (uint64, string) {
|
||||
// Try as a hex address.
|
||||
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
||||
v, err := strconv.ParseUint(s, 0, 64)
|
||||
if err == nil {
|
||||
return v, ""
|
||||
}
|
||||
}
|
||||
// Try as func+offset.
|
||||
if strings.HasPrefix(s, "+") {
|
||||
off, err := strconv.ParseUint(s[1:], 0, 64)
|
||||
if err == nil {
|
||||
return codeBase + funcOff + off, fmt.Sprintf("func+%#x", off)
|
||||
}
|
||||
}
|
||||
// Try as a label name.
|
||||
for _, l := range labels {
|
||||
if l.Name == s {
|
||||
return codeBase + funcOff + uint64(l.Offset), l.Name
|
||||
@@ -606,7 +531,6 @@ func resolveAddr(s string, codeBase, funcOff uint64, labels []Label) (uint64, st
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// lineAt returns the source line for a given function-relative offset.
|
||||
func lineAt(lines []SourceLine, offset int) int {
|
||||
if len(lines) == 0 {
|
||||
return 0
|
||||
@@ -626,8 +550,6 @@ func lineAt(lines []SourceLine, offset int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// offsetForLine returns the byte offset for a given source line number.
|
||||
// Returns -1 if no instruction is at that line.
|
||||
func offsetForLine(lines []SourceLine, line int) int {
|
||||
for _, le := range lines {
|
||||
if le.Line == line {
|
||||
@@ -637,7 +559,6 @@ func offsetForLine(lines []SourceLine, line int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// nearestLabel returns the name of the label at or just before the offset.
|
||||
func nearestLabel(labels []Label, offset int) string {
|
||||
best := ""
|
||||
bestOff := -1
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
// siginfo_t layout (Linux): si_signo, si_errno, si_code, then union.
|
||||
// The si_addr field is at offset 16 on all supported architectures.
|
||||
type siginfoT struct {
|
||||
SiSigno int32
|
||||
SiErrno int32
|
||||
SiCode int32
|
||||
_pad [125]byte
|
||||
}
|
||||
|
||||
const (
|
||||
trapBRKPT = 1 // software breakpoint
|
||||
trapHWBRKPT = 4 // hardware watchpoint
|
||||
)
|
||||
|
||||
// StopInfo returns the reason the debuggee stopped and the faulting address
|
||||
// (for watchpoints, the watched address that was accessed).
|
||||
func (s *Session) StopInfo() (StopReason, uint64) {
|
||||
if s.exited {
|
||||
return StopExited, 0
|
||||
}
|
||||
var info siginfoT
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETSIGINFO),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return StopNone, 0
|
||||
}
|
||||
if info.SiSigno != int32(syscall.SIGTRAP) {
|
||||
return StopSignal, uint64(info.SiCode)
|
||||
}
|
||||
switch info.SiCode {
|
||||
case trapBRKPT:
|
||||
return StopBreakpoint, 0
|
||||
case trapHWBRKPT:
|
||||
addr := *(*uint64)(unsafe.Add(unsafe.Pointer(&info), 16))
|
||||
return StopWatchpoint, addr
|
||||
default:
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
}
|
||||
@@ -5,69 +5,7 @@
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StopReason describes why the debuggee stopped.
|
||||
type StopReason int
|
||||
|
||||
const (
|
||||
StopNone StopReason = iota
|
||||
StopBreakpoint // INT3 breakpoint hit
|
||||
StopWatchpoint // hardware watchpoint triggered
|
||||
StopSingleStep // single-step completed
|
||||
StopSignal // stopped by a signal
|
||||
StopExited // process exited
|
||||
)
|
||||
|
||||
// siginfo_t layout (Linux amd64): si_signo, si_errno, si_code, then union.
|
||||
type siginfoT struct {
|
||||
SiSigno int32
|
||||
SiErrno int32
|
||||
SiCode int32
|
||||
_pad [125]byte
|
||||
}
|
||||
|
||||
const (
|
||||
trapBRKPT = 1 // INT3 breakpoint
|
||||
trapHWBRKPT = 4 // hardware watchpoint
|
||||
)
|
||||
|
||||
// StopInfo returns the reason the debuggee stopped and the faulting address
|
||||
// (for watchpoints, the watched address that was accessed).
|
||||
func (s *Session) StopInfo() (StopReason, uint64) {
|
||||
if s.exited {
|
||||
return StopExited, 0
|
||||
}
|
||||
var info siginfoT
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PTRACE,
|
||||
uintptr(syscall.PTRACE_GETSIGINFO),
|
||||
uintptr(s.pid),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&info)),
|
||||
0, 0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return StopNone, 0
|
||||
}
|
||||
if info.SiSigno != int32(syscall.SIGTRAP) {
|
||||
return StopSignal, uint64(info.SiCode)
|
||||
}
|
||||
switch info.SiCode {
|
||||
case trapBRKPT:
|
||||
return StopBreakpoint, 0
|
||||
case trapHWBRKPT:
|
||||
// The faulting address is in si_addr (offset 16 in siginfo_t on amd64).
|
||||
addr := *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&info)) + 16))
|
||||
return StopWatchpoint, addr
|
||||
default:
|
||||
return StopSingleStep, 0
|
||||
}
|
||||
}
|
||||
import "fmt"
|
||||
|
||||
// SetReg modifies a register value in the debuggee.
|
||||
func (s *Session) SetReg(name string, value uint64) error {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && arm64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && loong64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 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 "r0", "zero":
|
||||
regs.R0 = value
|
||||
case "r1", "ra":
|
||||
regs.R1 = value
|
||||
case "r2", "tp":
|
||||
regs.R2 = value
|
||||
case "r3", "sp":
|
||||
regs.R3 = value
|
||||
case "r4", "a0":
|
||||
regs.R4 = value
|
||||
case "r5", "a1":
|
||||
regs.R5 = value
|
||||
case "r6", "a2":
|
||||
regs.R6 = value
|
||||
case "r7", "a3":
|
||||
regs.R7 = value
|
||||
case "r8", "a4":
|
||||
regs.R8 = value
|
||||
case "r9", "a5":
|
||||
regs.R9 = value
|
||||
case "r10", "a6":
|
||||
regs.R10 = value
|
||||
case "r11", "a7":
|
||||
regs.R11 = value
|
||||
case "r12", "t0":
|
||||
regs.R12 = value
|
||||
case "r13", "t1":
|
||||
regs.R13 = value
|
||||
case "r14", "t2":
|
||||
regs.R14 = value
|
||||
case "r15", "t3":
|
||||
regs.R15 = value
|
||||
case "r16", "t4":
|
||||
regs.R16 = value
|
||||
case "r17", "t5":
|
||||
regs.R17 = value
|
||||
case "r18", "t6":
|
||||
regs.R18 = value
|
||||
case "r19", "t7":
|
||||
regs.R19 = value
|
||||
case "r20", "t8":
|
||||
regs.R20 = value
|
||||
case "r21", "fp":
|
||||
regs.R21 = value
|
||||
case "r22", "s0":
|
||||
regs.R22 = value
|
||||
case "r23", "s1":
|
||||
regs.R23 = value
|
||||
case "r24", "s2":
|
||||
regs.R24 = value
|
||||
case "r25", "s3":
|
||||
regs.R25 = value
|
||||
case "r26", "s4":
|
||||
regs.R26 = value
|
||||
case "r27", "s5":
|
||||
regs.R27 = value
|
||||
case "r28", "s6":
|
||||
regs.R28 = value
|
||||
case "r29", "s7":
|
||||
regs.R29 = value
|
||||
case "r30", "s8":
|
||||
regs.R30 = value
|
||||
case "r31", "pc":
|
||||
regs.R31 = value
|
||||
default:
|
||||
return fmt.Errorf("debug: unknown register %q", name)
|
||||
}
|
||||
return s.SetRegs(®s)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && riscv64
|
||||
|
||||
package debug
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user