Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab6b9eb84 | ||
|
|
f41a86b660 | ||
|
|
7721353d44 | ||
|
|
243b087116 | ||
|
|
eee7a6d4a4 | ||
|
|
801fb963c9 | ||
|
|
a2acc9b5a3 | ||
|
|
f860bf8ce6 | ||
|
|
e7df5e5225 | ||
|
|
d114b3412c | ||
|
|
c77d68018c | ||
|
|
89d633f4bb | ||
|
|
f20e0bf1e7 | ||
|
|
1a45b66139 | ||
|
|
382efe538a | ||
|
|
f8d28a42ba | ||
|
|
51a2854d7f |
@@ -0,0 +1,157 @@
|
|||||||
|
# Release — gasm binaries. Runs on version tags (v0.28.0) pushed to main.
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: fedora
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
- goos: linux
|
||||||
|
goarch: riscv64
|
||||||
|
- goos: linux
|
||||||
|
goarch: loong64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26"
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Validate tag and build
|
||||||
|
id: build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ gitea.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! echo "$VERSION" | grep -qE '^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$'; then
|
||||||
|
echo "ERROR: expected a semver tag like v1.2.3, got: '$VERSION'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
echo "version_no_v=${VERSION_NO_V}" >> "$GITEA_OUTPUT"
|
||||||
|
|
||||||
|
mkdir -p bin
|
||||||
|
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 \
|
||||||
|
go build -ldflags "-s -w -X main.version=${VERSION_NO_V}" \
|
||||||
|
-o "bin/gasm-${VERSION_NO_V}-${{ matrix.goos }}-${{ matrix.goarch }}" \
|
||||||
|
./cmd/gasm
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: gasm-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Smoke test
|
||||||
|
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
|
||||||
|
run: |
|
||||||
|
chmod +x bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
./bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }} --version
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: fedora
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
releases: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Extract CHANGELOG section
|
||||||
|
env:
|
||||||
|
VERSION: ${{ gitea.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
|
||||||
|
sed -n "/^## \[${VERSION_NO_V}\] /,/^## \[/p" CHANGELOG.md \
|
||||||
|
| sed '$d' \
|
||||||
|
| tail -n +2 \
|
||||||
|
> release-body.md
|
||||||
|
|
||||||
|
if [ ! -s release-body.md ]; then
|
||||||
|
echo "ERROR: no CHANGELOG section found for ${VERSION_NO_V}"
|
||||||
|
echo "Expected a heading like: ## [${VERSION_NO_V}] — YYYY-MM-DD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BODY=$(sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' -e 's/\r//g' release-body.md | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||||
|
BODY="\"${BODY}\""
|
||||||
|
|
||||||
|
response=$(curl -sS -w '\n%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
||||||
|
-d "{\"tag_name\":\"${GITEA_REF_NAME}\",\"name\":\"${GITEA_REF_NAME}\",\"body\":${BODY},\"draft\":false,\"prerelease\":false}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -1)
|
||||||
|
payload=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
echo "HTTP ${http_code}"
|
||||||
|
if [ "$http_code" != "201" ]; then
|
||||||
|
echo "Failed to create release: ${payload}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$payload" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
||||||
|
echo "Created release ID=${RELEASE_ID}"
|
||||||
|
printf '%s' "${RELEASE_ID}" > release-id.txt
|
||||||
|
|
||||||
|
- name: Upload assets
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
RELEASE_ID=$(cat release-id.txt)
|
||||||
|
|
||||||
|
for binary in dist/gasm-*/gasm-*; do
|
||||||
|
[ -f "$binary" ] || continue
|
||||||
|
fname=$(basename "$binary")
|
||||||
|
echo "Uploading ${fname}..."
|
||||||
|
http_code=$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
-X POST \
|
||||||
|
--data-binary "@${binary}" \
|
||||||
|
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${fname}")
|
||||||
|
echo " HTTP ${http_code}"
|
||||||
|
if [ "$http_code" != "201" ]; then
|
||||||
|
echo "Failed to upload ${fname}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Release ${GITEA_REF_NAME} is live."
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Test — gasm-devkit. Runs on push and pull request to development.
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [development]
|
||||||
|
pull_request:
|
||||||
|
branches: [development]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vet:
|
||||||
|
runs-on: fedora
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26"
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: gofmt
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
unformatted=$(gofmt -l .)
|
||||||
|
if [ -n "$unformatted" ]; then
|
||||||
|
echo "These files need gofmt:"
|
||||||
|
echo "$unformatted"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: go vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: fedora
|
||||||
|
needs: vet
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26"
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Install gcc
|
||||||
|
run: dnf install -y gcc
|
||||||
|
|
||||||
|
- name: go test -race
|
||||||
|
run: go test -race -count=1 ./...
|
||||||
|
|
||||||
|
- name: Coverage gate — 80 % minimum
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Exclude packages inherently untestable without hardware:
|
||||||
|
# debug — interactive ptrace, requires a live process
|
||||||
|
# cmd/gasm — CLI glue, covered by integration tests
|
||||||
|
go test -coverprofile=coverage.out \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/arch \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/asm \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/ast \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/format \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lexer \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lint \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lsp \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/parser \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/token \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/verify
|
||||||
|
coverage=$(go tool cover -func=coverage.out | awk '/^total:/ { gsub("%", "", $3); print $3 }')
|
||||||
|
echo "Total coverage: ${coverage}%"
|
||||||
|
if awk -v c="$coverage" 'BEGIN { exit !(c+0 < 80) }'; then
|
||||||
|
echo "ERROR: coverage ${coverage}% is below the 80% threshold"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: fedora
|
||||||
|
needs: test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26"
|
||||||
|
|
||||||
|
- name: Download dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -ldflags="-s -w" -o bin/gasm ./cmd/gasm
|
||||||
|
|
||||||
|
- name: Smoke test
|
||||||
|
run: ./bin/gasm --version
|
||||||
@@ -10,3 +10,6 @@ coverage.out
|
|||||||
# Editor detritus
|
# Editor detritus
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Scratch / temporary work
|
||||||
|
_scratch/
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
+757
@@ -0,0 +1,757 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to gasm-devkit are documented here.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Conventional Commits](https://www.conventionalcommits.org/).
|
||||||
|
|
||||||
|
## [development]
|
||||||
|
|
||||||
|
Unreleased changes on the `development` branch.
|
||||||
|
|
||||||
|
## [0.28.0] — 2026-08-03
|
||||||
|
|
||||||
|
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
||||||
|
pseudo-instruction, SB/global symbol references, ELF64 object emission, and
|
||||||
|
ground-truth verification against `GOARCH=riscv64 go tool asm`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **RISC-V encoder** — RV64I, RV64M, RV64A, RV64F/D, FMA, CSR, JALR.
|
||||||
|
- **MOV pseudo-instruction** — load, store, reg-to-reg, immediate, frame mapping.
|
||||||
|
- **RVC compression** — 22 compressed instruction types (C.LDSP, C.SDSP, C.FLDSP,
|
||||||
|
C.FSDSP, C.ADDI, C.LI, C.LUI, C.ADDIW, C.MV, C.ADD, C.SUB, C.XOR, C.OR, C.AND,
|
||||||
|
C.SLLI, C.SRLI, C.SRAI, C.ANDI, C.BEQZ, C.BNEZ, C.J, C.JR).
|
||||||
|
- **SB/global symbols** — `MOV $sym(SB)`, `MOV sym(SB)`, `MOV rd, sym(SB)`
|
||||||
|
encoded as AUIPC pairs with R_RISCV_PCREL_HI20/LO12 relocations.
|
||||||
|
- **GLOBL/DATA** — data section layout in `AssembleFileRISCV`.
|
||||||
|
- **ELF64 emission** — `gasm asm --format elf` produces EM_RISCV objects
|
||||||
|
(.text, .data, .symtab, .rela.text).
|
||||||
|
- **`gasm verify --ground-truth`** — byte-exact comparison against
|
||||||
|
`GOARCH=riscv64 go tool asm`.
|
||||||
|
- **`gasm verify --profile`** — function layout listing for RISC-V.
|
||||||
|
- **CALL** — AUIPC + JALR pair encoding.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Parser: bare-number offset before `(SP)` no longer misidentified as pseudo.
|
||||||
|
- MOV: `MOV $sym(FP/SP), rd` now returns an explicit error instead of silent fallback.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Subprocess isolation for `--fuzz`: each function is fuzzed in its own child
|
||||||
|
process, so a partial function (decoder) that faults on random garbage is
|
||||||
|
reported as "CRASH (partial function, use --ground-truth)" without killing
|
||||||
|
the parent. CRASH is informational (exit 0); only MISMATCH is an error.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `gasm verify --fuzz` no longer crashes the process on partial functions.
|
||||||
|
|
||||||
|
## [0.26.0] — 2026-07-31
|
||||||
|
|
||||||
|
Universal differential fuzzing: `gasm verify --fuzz` needs no hand-written
|
||||||
|
reference. It parses the `// func` signature from the assembly source,
|
||||||
|
generates typed random inputs (slices with random content, ints, pointers to
|
||||||
|
fixed arrays), JIT-executes BOTH the gasm-assembled and the go-tool-asm-
|
||||||
|
assembled versions with independent buffer copies, and compares the result
|
||||||
|
area bit-for-bit.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `FuzzFunc` / `ExtractSignatures` / `parseFuncSig` — universal
|
||||||
|
differential fuzz driven by the conventional `// func` comment. Each
|
||||||
|
version gets its own buffer set (deep copy) so functions that write to
|
||||||
|
their arguments (histogram increments) don't corrupt the other's input.
|
||||||
|
- `gasm verify --fuzz [-n N]`: runs the differential fuzz for every function
|
||||||
|
with a parseable signature. Total functions (wideCopy, pack16, decorrelate,
|
||||||
|
analyze, autocorr) pass; partial functions (decoders that fault on malformed
|
||||||
|
input) should use `--ground-truth` instead.
|
||||||
|
|
||||||
|
### Known limitation
|
||||||
|
|
||||||
|
`--fuzz` crashes the process for partial functions (e.g. LZ4 decoders) whose
|
||||||
|
over-copy paths read past the buffer on random garbage input. Subprocess
|
||||||
|
isolation (fork per function) is planned. Use `--ground-truth` for decoders.
|
||||||
|
|
||||||
|
## [0.25.0] — 2026-07-30
|
||||||
|
|
||||||
|
Universal ground-truth verification: `gasm verify --ground-truth` assembles
|
||||||
|
any `.s` file with both gasm and `go tool asm`, then compares the machine
|
||||||
|
code byte-for-byte per function (relocation sites masked). No hand-written
|
||||||
|
reference needed — the Go toolchain IS the oracle.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `GroundTruth` — shells out to `go tool asm`, parses the GOOBJ
|
||||||
|
output (minimal reader: block offsets, nonpkg symbol table, data index)
|
||||||
|
and returns per-function code bytes.
|
||||||
|
- `gasm verify --ground-truth`: compares gasm's output against the Go
|
||||||
|
assembler's, reporting MATCH/MISMATCH per function with the first
|
||||||
|
differing byte. Relocation disp32 fields (static-symbol references the
|
||||||
|
linker fills) are masked before comparison.
|
||||||
|
- Verified: go-lz4 AVX2 2/2, go-flac AVX2 17/17 functions byte-identical.
|
||||||
|
|
||||||
|
## [0.24.0] — 2026-07-29
|
||||||
|
|
||||||
|
The full analyze family and stereo PCM decode are now differentially tested.
|
||||||
|
15 of 17 go-flac AVX2 kernels have bit-for-bit differential coverage; the
|
||||||
|
two remaining (autocorrAVX2 — FMA reassociation, lpcResidualAVX2 — complex
|
||||||
|
multi-arg) are deferred.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `analyzeO3RangeAVX2` and `analyzeO4RangeAVX2` differential tests
|
||||||
|
(200 iterations each, same harness as O1/O2/Res).
|
||||||
|
- `verify`: `decodeStereo16AVX2` differential test (500 random interleaved
|
||||||
|
stereo PCM buffers, both channels compared sample-by-sample).
|
||||||
|
|
||||||
|
## [0.23.0] — 2026-07-28
|
||||||
|
|
||||||
|
The analyze family and 24-bit PCM decode join the differential suite.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `analyzeO2RangeAVX2` and `analyzeResRangeAVX2` differential
|
||||||
|
tests (200 iterations each, shared harness with O1: zigzag fold, partial
|
||||||
|
sum, overflow flag and Len32 histogram).
|
||||||
|
- `verify`: `decodeMono24AVX2` differential test (500 random 24-bit PCM
|
||||||
|
buffers, sign-extension compared sample-by-sample).
|
||||||
|
|
||||||
|
## [0.22.0] — 2026-07-27
|
||||||
|
|
||||||
|
The remaining go-flac encoder kernels join the differential suite.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `analyzeO1RangeAVX2` differential test (300 random partitions:
|
||||||
|
zigzag fold, partial sum, overflow flag and the 32-bin Len32 histogram
|
||||||
|
compared element-by-element against the portable Go reference).
|
||||||
|
- `verify`: `fastStereoSumsAVX2` differential test (300 random stereo
|
||||||
|
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
|
||||||
|
|
||||||
|
Differential testing extended to all four production kernels and the CLI
|
||||||
|
exposes the full dynamic-analysis toolkit.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: go-flac AVX2 differential tests — `decodeMono16AVX2` (500
|
||||||
|
random PCM buffers), `pack16AVX2` (500 random int32→int16 packings) and
|
||||||
|
all four decorrelation kernels (200 iterations each: left-side, side-right,
|
||||||
|
mid-side, interleave) compared bit-for-bit against the portable Go
|
||||||
|
references.
|
||||||
|
- `verify`: go-lz4 AVX-512 differential tests — `decodeBlockAVX512` (3 000
|
||||||
|
fuzzed LZ4 blocks + known answers) and `wideCopyAVX512` (0–1024 bytes)
|
||||||
|
against the same portable oracle as the AVX2 suite.
|
||||||
|
- `gasm verify --abi`: runs each NOSPLIT function with sentinel registers
|
||||||
|
and a red-zone canary, reporting violations.
|
||||||
|
- `gasm verify --profile`: lists the static basic-block count per function.
|
||||||
|
|
||||||
|
## [0.20.0] — 2026-07-25
|
||||||
|
|
||||||
|
Coverage profiling: the third pillar of Phase 3. Static basic-block
|
||||||
|
enumeration from the assembler's label map, combined with multi-input path
|
||||||
|
diversity measurement — how many observationally distinct execution paths a
|
||||||
|
test corpus exercises.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `Kernel.Blocks` / `Kernel.BlockCount` — enumerate basic blocks
|
||||||
|
from the assembler's local-label map (every jump target is a block
|
||||||
|
boundary; the function entry is always a block). `decodeBlockAVX2` has
|
||||||
|
27 blocks.
|
||||||
|
- `verify`: `Kernel.ProfilePaths` — run the function with a corpus of
|
||||||
|
argument blocks and collect distinct output fingerprints (the result
|
||||||
|
words); reports path diversity as a lower bound on code coverage.
|
||||||
|
|
||||||
|
### Note
|
||||||
|
|
||||||
|
INT3-based per-block hit counting was prototyped but deferred: Go's runtime
|
||||||
|
signal management (sigaltstack, handler re-installation) makes raw
|
||||||
|
rt_sigaction handlers fragile in a Go process. The static + path-diversity
|
||||||
|
approach delivers the project's goal (proving the SIMD path and tail handling
|
||||||
|
execute) without fighting the runtime.
|
||||||
|
|
||||||
|
## [0.19.0] — 2026-07-24
|
||||||
|
|
||||||
|
Runtime ABI checks: the second pillar of Phase 3. The JIT trampoline now
|
||||||
|
has an ABI-checking variant that sets sentinels in the callee-saved registers
|
||||||
|
(BP, R14) before entering the assembled function and verifies they survive on
|
||||||
|
return, plus a red-zone canary (128 bytes below SP filled with 0xA5) that
|
||||||
|
detects any illegal write below the stack pointer.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: `CallChecked` / `Kernel.CallFuncChecked` — ABI-checking JIT call
|
||||||
|
with sentinel registers and red-zone canary; returns an `ABIReport`
|
||||||
|
(BPClobbered, R14Clobbered, RedZoneHit).
|
||||||
|
- `verify`: the raw `leaveJITCheckedRaw` trampoline — a TEXT symbol with no
|
||||||
|
ABIInternal wrapper (address obtained via GLOBL/DATA), so the JIT
|
||||||
|
function's RET lands directly in the check code and sees the registers
|
||||||
|
exactly as the function left them.
|
||||||
|
- Tests: deliberate BP/R14 clobberers detected; both go-lz4 kernels
|
||||||
|
confirmed ABI-clean (BP preserved, R14 preserved, red zone intact).
|
||||||
|
|
||||||
|
## [0.18.0] — 2026-07-23
|
||||||
|
|
||||||
|
Differential testing: the JIT-assembled go-lz4 `decodeBlockAVX2` kernel is
|
||||||
|
fuzzed against a portable Go reference — 5 000 valid LZ4 blocks compared
|
||||||
|
bit-for-bit, plus 2 000 hostile (random garbage) inputs with matching error
|
||||||
|
codes. This is the automated form of the project's bit-identical contract.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify`: differential fuzz tests — a random LZ4 block generator produces
|
||||||
|
valid blocks (literals, overlapping matches, extension bytes) and the
|
||||||
|
JIT-assembled kernel's output is compared byte-for-byte against a portable
|
||||||
|
Go decoder; a hostile-input suite confirms error-code agreement on random
|
||||||
|
garbage (no crashes, same classification).
|
||||||
|
|
||||||
|
## [0.17.0] — 2026-07-22
|
||||||
|
|
||||||
|
Phase 3 begins: dynamic analysis. A JIT execution substrate that assembles
|
||||||
|
Plan 9 amd64 kernels into executable memory and calls them directly — pure Go
|
||||||
|
(stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external
|
||||||
|
toolchain.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `verify` package: JIT infrastructure — `Map` copies machine code into a
|
||||||
|
W^X memory mapping, `Call` invokes it through an ABI0 trampoline that
|
||||||
|
switches to a prepared stack and back. `Load`/`LoadSource`/`LoadAST`
|
||||||
|
parse, assemble and map a `.s` file in one step; `Kernel.CallFunc`
|
||||||
|
marshals the argument block and returns results.
|
||||||
|
- `gasm verify` subcommand: assembles a file, JIT-loads it and reports the
|
||||||
|
available functions; with `-smoke`, calls each NOSPLIT function with
|
||||||
|
zeroed arguments to confirm the trampoline works end-to-end.
|
||||||
|
- Integration tests: the go-lz4 `decodeBlockAVX2` and `wideCopyAVX2`
|
||||||
|
kernels (699 and 146 bytes) assemble, map and execute correctly —
|
||||||
|
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
|
||||||
|
|
||||||
|
The scalar conversions between vector and general-purpose registers — the
|
||||||
|
last of the amd64 EVEX instruction set.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the GPR-interchanging conversions, byte for byte against the Go
|
||||||
|
assembler (28 ground-truth cases including memory sources and extended
|
||||||
|
GPRs): vector to GPR — the signed and truncated VCVT{,T}S{D,S}2SI{,Q}
|
||||||
|
in both VEX and EVEX, and the unsigned VCVT{,T}S{D,S}2USI{L,Q}
|
||||||
|
(EVEX only); GPR to vector — VCVTSI2SD{L,Q}/VCVTSI2SS{L,Q} (VEX and
|
||||||
|
EVEX) and VCVTUSI2SD{L,Q}/VCVTUSI2SS{L,Q} (EVEX only), whose preserved
|
||||||
|
vector source sits in vvvv (three Plan 9 operands).
|
||||||
|
|
||||||
|
## [0.15.0] — 2026-07-20
|
||||||
|
|
||||||
|
The last of the EVEX conversions and narrowing/extending moves — the EVEX
|
||||||
|
instruction set is now complete save for the GPR-interchanging forms.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the unsigned and truncating conversions — VCVTPD2PS (and the X/Y
|
||||||
|
spellings, whose length the spelling fixes), VCVTPD2UDQ (X/Y),
|
||||||
|
VCVTTPD2UDQ (X/Y), VCVTTPD2UQQ, VCVTPS2UDQ, VCVTTPS2UDQ, VCVTPS2UQQ,
|
||||||
|
VCVTTPS2UQQ, VCVTTPD2QQ, VCVTTPS2QQ, VCVTUQQ2PD, VCVTUQQ2PS (X/Y) and
|
||||||
|
VCVTQQ2PS X/Y.
|
||||||
|
- `asm`: the remaining sign/zero-extending moves (VPMOVSXBD/BQ/WQ and
|
||||||
|
VPMOVZXBD/BQ/WD/WQ, VEX and EVEX) and the complete signed and unsigned
|
||||||
|
narrowing stores (VPMOVS{DB,QB,DW,QW,QD,WB}, VPMOVUS{DB,QB,DW,QW,QD,WB},
|
||||||
|
VPMOVDB, VPMOVQW).
|
||||||
|
- `asm`: the mask/vector conversions (VPMOVM2B/W/D/Q and VPMOVB2M/W2M/
|
||||||
|
D2M/Q2M), whose K register is a genuine operand rather than a mask and
|
||||||
|
which therefore take no masking suffixes.
|
||||||
|
|
||||||
|
## [0.14.0] — 2026-07-19
|
||||||
|
|
||||||
|
The floating-point helper and conversion tail of the AVX-512 set, plus
|
||||||
|
gather and scatter with VSIB addressing — every encoding verified byte for
|
||||||
|
byte against the Go assembler.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the floating-point helpers — reciprocals and reciprocal square
|
||||||
|
roots (VRCP14/VRSQRT14 PD/PS/SD/SS), exponents and mantissas (VGETEXP*,
|
||||||
|
VGETMANT*), scaling by powers of two (VSCALEF*), rounding (VRNDSCALE*),
|
||||||
|
reduction (VREDUCE*), immediate fixup (VFIXUPIMM*) and range selection
|
||||||
|
(VRANGE*), and floating-point class tests (VFPCLASSPD/PS X/Y/Z and
|
||||||
|
VFPCLASSSD/SS — a new immediate form whose reg field carries the opmask
|
||||||
|
destination).
|
||||||
|
- `asm`: **gather and scatter with VSIB addressing.** The gathers take
|
||||||
|
both Go spellings: the VEX form with a vector mask register (OP mask,
|
||||||
|
vsib, dst) and the EVEX form with an explicit K mask (OP vsib, K, dst),
|
||||||
|
where the EVEX L'L field follows the VSIB index register rather than the
|
||||||
|
data register (a ZMM index with an YMM destination encodes L'L = 10, as
|
||||||
|
the Go assembler emits). The scatters (VSCATTER*/VPSCATTER*) are EVEX
|
||||||
|
only (OP src, K, vsib). All eight gather and eight scatter widths.
|
||||||
|
- `asm`: the remaining conversions — VCVTQQ2PS (the 512-bit source sets
|
||||||
|
the length), VCVTPD2QQ/UQQ, VCVTPS2QQ, VCVTUDQ2PD/PS, the half-precision
|
||||||
|
VCVTPH2PS and VCVTPS2PH (the extract layout with an immediate).
|
||||||
|
|
||||||
|
## [0.13.0] — 2026-07-18
|
||||||
|
|
||||||
|
The wider AVX-512 set: ternary logic, permutes, compares, expand/compress,
|
||||||
|
the opmask instructions and the EVEX rounding/SAE/broadcast suffixes — every
|
||||||
|
encoding verified byte for byte against the Go assembler.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the wider EVEX/AVX-512 set, across roughly sixty new ground-truth
|
||||||
|
cases: ternary logic (VPTERNLOGD/Q), the lane shuffles/inserts/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 —
|
||||||
|
a new NDS-plus-immediate form with the K register in the reg field), the
|
||||||
|
permutes (VPERMB/W, VPERMI2/T2 D/Q/PD), the wider integer families
|
||||||
|
(VPMADDWD/UBSW, VPMULHUW, VPACKSSWB/USWB/SSDW/USDW, VPABS B/W/D/Q, the
|
||||||
|
VPROL*/VPROR* rotates, the word shifts and the EVEX W1 qword shifts),
|
||||||
|
expand/compress (VEXPANDPD/PS, VPEXPANDD/Q, VCOMPRESSPD/PS, VPCOMPRESSD/
|
||||||
|
Q), the broadcasts (VPBROADCASTB/W from a GPR or memory, VBROADCASTSS/
|
||||||
|
SD), the opmask-register instructions (KAND/KOR/KXNOR/KADD/KUNPCK/KNOT/
|
||||||
|
KSHIFTL/KORTEST B/W/D/Q and KMOVQ, whose width the L/W/pp bits select),
|
||||||
|
the packed single arithmetic (VADD/VSUB/VMUL/VDIV/VMIN/VMAX PS), the
|
||||||
|
aligned moves (VMOVAPS/APD, VMOVDQA32/64, VMOVSS), the replicating moves
|
||||||
|
(VMOVSLDUP/VMOVSHDUP), the conversions (VCVTPS2DQ, VCVTTPS2DQ) and the
|
||||||
|
remaining extending and narrowing moves (VPMOVSXBW, VPMOVZXBW, VPMOVWB,
|
||||||
|
VPMOVQB).
|
||||||
|
- `asm`: the EVEX mnemonic suffixes the Go assembler accepts — the rounding
|
||||||
|
modes `.RN_SAE`, `.RD_SAE`, `.RU_SAE`, `.RZ_SAE` (the EVEX b bit with the
|
||||||
|
rounding control in L'L), suppress-all-exceptions `.SAE`, and memory
|
||||||
|
broadcast `.BCST` (the b bit, the vector length preserved, disp8×N scaled
|
||||||
|
by the element size) — each combinable with the `.Z` zeroing suffix,
|
||||||
|
validated against the Go assembler's bytes, and rejected on instructions
|
||||||
|
that do not support them.
|
||||||
|
|
||||||
|
## [0.12.0] — 2026-07-17
|
||||||
|
|
||||||
|
GOOBJ emission: gasm-assembled functions drop into a `go build` without the
|
||||||
|
Go assembler.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: **GOOBJ object output.** `gasm asm --format goobj -p <pkgpath>`
|
||||||
|
writes the Go toolchain's own object format — the one `cmd/link` consumes
|
||||||
|
directly: the functions as non-package symbols qualified with the package
|
||||||
|
path (exactly as `cmd/asm` records assembly symbols), the `GLOBL` data,
|
||||||
|
one serialized `FuncInfo` per function (argument/frame sizes, the asm
|
||||||
|
func flag, the start line, the file table) and the four pc-value tables
|
||||||
|
(`pcsp`, `pcfile`, `pcline`, `pcinline`). The `pcsp` table carries the
|
||||||
|
real stack deltas: the assembler now tracks every stack-adjustment
|
||||||
|
boundary through the prologue (`PUSHQ BP`, `SUBQ $frame, SP`) and each
|
||||||
|
`RET`'s epilogue, so frame-pointer functions unwind correctly. The
|
||||||
|
object preamble — the version-and-experiment header the linker compares
|
||||||
|
verbatim — is captured from the installed `go tool asm`, so the output is
|
||||||
|
always consistent with the toolchain that links it.
|
||||||
|
- `asm`: relocations against file-local `GLOBL` symbols become `R_PCREL`
|
||||||
|
entries in the GOOBJ output, with the instruction's displacement field
|
||||||
|
left zero for the linker to fill (as `cmd/asm` leaves it).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `parser`: 64-bit `DATA` literals above `MaxInt64`
|
||||||
|
(`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
|
||||||
|
|
||||||
|
Linkable object output: external symbols and relocatable ELF / Mach-O
|
||||||
|
objects.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: **object-file emission.** `gasm asm --format elf` writes an
|
||||||
|
ELF64 relocatable object and `--format macho` a Mach-O x86-64
|
||||||
|
`MH_OBJECT`: a code section (`.text` / `__TEXT,__text`) and a data
|
||||||
|
section (`.data` / `__DATA,__data`), a symbol table with one symbol per
|
||||||
|
`TEXT` and `GLOBL` (file-local `<>` symbols local, the rest global), and
|
||||||
|
one PC-relative relocation per static-symbol reference
|
||||||
|
(`R_X86_64_PC32` / `X86_64_RELOC_SIGNED`, the −4 addend the form needs).
|
||||||
|
The ELF output is verified end-to-end: a gasm-emitted object links with
|
||||||
|
a C driver and runs, resolving both a file-local constant and an
|
||||||
|
external symbol; the Mach-O output is verified structurally with
|
||||||
|
`debug/macho`.
|
||||||
|
- `asm`: **external symbol references.** A reference to a symbol no
|
||||||
|
`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
|
||||||
|
file can represent a reference the linker must resolve.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `gasm asm` takes a `--format raw|elf|macho` flag selecting what `-o`
|
||||||
|
writes; without `--format` the behaviour is unchanged (the concatenated
|
||||||
|
image).
|
||||||
|
|
||||||
|
## [0.10.0] — 2026-07-15
|
||||||
|
|
||||||
|
The EVEX floating-point and conversion set: the packed-double arithmetic,
|
||||||
|
the scalar SD/SS forms, VMOVDDUP and the width-changing conversions, each
|
||||||
|
verified byte for byte against the Go assembler.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the rest of the common EVEX/VEX floating-point set — packed double
|
||||||
|
arithmetic (VSUBPD, VDIVPD, VMINPD, VMAXPD, VUNPCKLPD and the EVEX form of
|
||||||
|
VUNPCKHPD), the scalar double and single operations (VSUBSD, VDIVSD,
|
||||||
|
VMINSD, VMAXSD and the full VADDSS/VSUBSS/VMULSS/VDIVSS/VMINSS/VMAXSS
|
||||||
|
family in both VEX and EVEX — the EVEX scalar forms exist for masked and
|
||||||
|
zeroing use), and VMOVDDUP (lane duplication, VEX and EVEX).
|
||||||
|
- `asm`: the width-changing conversions — VCVTDQ2PS and VCVTPS2PD (VEX and
|
||||||
|
EVEX; the destination sets the length for PS→PD), the EVEX form of
|
||||||
|
VCVTDQ2PD, and the packed-double → dword family: VCVTPD2DQ/VCVTTPD2DQ
|
||||||
|
(EVEX-512 only, a ZMM source and an XMM destination) and their X/Y
|
||||||
|
spellings (VCVTPD2DQX/Y, VCVTTPD2DQX/Y), whose length follows the wider
|
||||||
|
source — a new operand form, since the destination is always XMM while
|
||||||
|
VEX.L / EVEX.L'L ride with the source (fixed by the spelling even for a
|
||||||
|
memory source).
|
||||||
|
- `asm`: masking and zeroing on every new form — the scalar SD/SS
|
||||||
|
arithmetic, the unpacks, VMOVDDUP and the conversions all accept the
|
||||||
|
explicit K1–K7 operand and the `.Z` suffix the way Go writes them.
|
||||||
|
|
||||||
|
### Documented
|
||||||
|
|
||||||
|
- VCVTPS2PD follows the Go assembler's encoding, which omits the F3
|
||||||
|
mandatory prefix (VEX.pp / EVEX.pp = 00) that Intel's maps prescribe; the
|
||||||
|
Go toolchain's machine code is the project's byte-for-byte oracle, and
|
||||||
|
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
|
||||||
|
|
||||||
|
AVX-512 masking and a wider EVEX integer set.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: **EVEX masking** the way Go writes it — an explicit `K1`–`K7`
|
||||||
|
operand placed among the operands (merging mask), and a `.Z` mnemonic
|
||||||
|
suffix for zeroing (`VPADDD.Z Z1, Z2, K2, Z3`). Supported across the NDS,
|
||||||
|
reg/rm, immediate-shift, align, extract, convert and move forms, including
|
||||||
|
masked comparisons with a K destination (`VPCMPEQD Z0, Z3, K2, K1`). K0 is
|
||||||
|
rejected as an explicit mask, and `.Z` without a mask is an error, matching
|
||||||
|
the Go assembler.
|
||||||
|
- `asm`: the common AVX-512 F/BW integer set — VPADDB/W, VPSUBB/W, VPANDD/Q,
|
||||||
|
VPANDND/Q, VPMULLW, VPAVGB/W, the signed/unsigned min/max family for
|
||||||
|
B/W/D/Q elements, the variable shifts VPSLLVD/Q, VPSRLVD/Q, VPSRAVD/Q, the
|
||||||
|
EVEX forms of VPSHUFD/VPSHUFB, and the VMOVDQU8/VMOVDQU16 move aliases.
|
||||||
|
Register indices 16–31 encode correctly (the mod=11 quirk carries rm[4]
|
||||||
|
in X̄). All verified byte for byte against the Go assembler.
|
||||||
|
- `lint`: masked EVEX forms (`.Z` suffix, K operands) are recognised by
|
||||||
|
`unknown-instruction` and exempted from `operand-count`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `asm`: EVEX register–register operands with indices 16–31 encoded rm[4]
|
||||||
|
into B̄ instead of X̄ (the EVEX mod=11 extension quirk), producing wrong
|
||||||
|
prefix bytes for X16+/Y16+ r/m operands.
|
||||||
|
|
||||||
|
## [0.8.0] — 2026-07-13
|
||||||
|
|
||||||
|
Standard CLI ergonomics.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `gasm --help` prints a proper top-level help (description, commands,
|
||||||
|
flags, examples), and every subcommand now answers `-h`/`--help` with its
|
||||||
|
own usage block (usage line, description, flag defaults), exiting 0. An
|
||||||
|
unknown command points at `gasm --help` instead of dumping the whole usage.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The version is primarily available as the standard `gasm --version` / `-V`
|
||||||
|
flag; the `gasm version` spelling remains as an alias.
|
||||||
|
|
||||||
|
## [0.7.0] — 2026-07-12
|
||||||
|
|
||||||
|
The formatter behaves like `go fmt` and canonicalises block separation.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `gasm fmt` now works like `go fmt`: with no arguments — or with a directory
|
||||||
|
argument — it reformats every `.s` file below it in place and lists the
|
||||||
|
changed files, skipping `.` and `_` directories (`.git`, `_refs`, …).
|
||||||
|
Explicit file arguments keep the `-w` / standard-output behaviour.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `format`: 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
|
||||||
|
its `TEXT`, and runs of blank lines collapse to one. The output remains
|
||||||
|
idempotent and round-trips through the parser. All four go-flac/go-lz4
|
||||||
|
kernels were reformatted with this release and remain byte-identical when
|
||||||
|
assembled.
|
||||||
|
|
||||||
|
## [0.6.0] — 2026-07-11
|
||||||
|
|
||||||
|
Calibrated to the Go ABI: `register-clobber` stops reporting legal code, and
|
||||||
|
the encoder learns the legacy SSE moves.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `lint`: **`register-clobber` is now calibrated to the Go ABI**
|
||||||
|
(`cmd/compile/abi-internal.md`), not the platform ABI. Go's stack-based
|
||||||
|
ABI0 has no System V style callee-saved registers — amd64 `BX`, `R12`–`R15`
|
||||||
|
and the arm64/riscv64/loong64 scratch sets are caller-saved or permanent
|
||||||
|
scratch, and hand-written kernels may clobber them freely. The rule now
|
||||||
|
audits only the registers Go fixes across calls: the frame pointer and the
|
||||||
|
goroutine pointer (amd64 `BP`/`R14`, arm64 `R18`/`R28`/`R29`, riscv64
|
||||||
|
`X27`, loong64 `R22`), and the goroutine pointer is reported only when the
|
||||||
|
function can reach the runtime (is not `NOSPLIT` or makes a call) — the
|
||||||
|
ABI0 transition restores it on those paths, and NOSPLIT call-free leaves
|
||||||
|
may use it, exactly as the runtime's own assembly does. Both go-flac
|
||||||
|
kernels now lint with zero diagnostics.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `lint`: the liveness analysis took the destination operand to be the
|
||||||
|
*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
|
||||||
|
comment) is no longer indented as if it were still inside the finished
|
||||||
|
function body.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the legacy (non-VEX) SSE moves — `MOVOU`/`MOVO` (the Plan 9 names
|
||||||
|
for MOVDQU/MOVDQA), `MOVUPS`/`MOVAPS`/`MOVUPD`/`MOVAPD` and the scalar
|
||||||
|
`MOVSD`/`MOVSS` — and `VMOVDQU64` in the EVEX set. All verified byte for
|
||||||
|
byte against the Go assembler.
|
||||||
|
|
||||||
|
## [0.5.0] — 2026-07-10
|
||||||
|
|
||||||
|
EVEX / AVX-512: the go-flac AVX-512 kernel now assembles, byte-identically to
|
||||||
|
the Go toolchain, completing the production-kernel coverage.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: **EVEX (AVX-512) encoding** — the four-byte EVEX prefix with the
|
||||||
|
5-bit register fields (Z0–Z31, X/Y 16–31, with the reg-r/m X̄ quirk and
|
||||||
|
V'̄ shared between vvvv and the SIB index), opmask registers (K0–K7) as
|
||||||
|
operands and as mask destinations, and the compressed disp8×N displacement
|
||||||
|
(the multiplier follows the memory operand's size, as the Go assembler's
|
||||||
|
opcode tables prescribe). Covers every AVX-512 instruction the go-flac
|
||||||
|
kernels use: VPXORD/Q, VPADDD, VPSUBD/Q, VPUNPCK*DQ, VPMULLD/Q, VPERMD,
|
||||||
|
VPSLLD/VPSRAD/VPSRAQ, VALIGND, VPCMPEQD (K destination), VMOVDQU32,
|
||||||
|
VMOVUPD, VCVTQQ2PD, VPMOVSXDQ, the narrowing stores VPMOVDW/VPMOVQD, the
|
||||||
|
extracts VEXTRACTI64X4/VEXTRACTF64X4, VFMADD231PD, VADDPD, VMULPD, the
|
||||||
|
broadcasts VPBROADCASTD/Q (GPR and memory sources take different opcodes)
|
||||||
|
and the mask moves KMOVW/KTESTW. Masking/zeroing suffixes are out of scope
|
||||||
|
— the kernels use neither.
|
||||||
|
- `asm`: `AssembleFile` now accepts file-defined global (`non-<>`) symbols
|
||||||
|
too; a reference is external only when no `GLOBL` in the file defines it.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `asm`: registers X16–Y31 force the EVEX encoding of dual-form mnemonics;
|
||||||
|
previously a `VPBROADCASTD AX, Y30` fell into the VEX encoder, which cannot
|
||||||
|
represent indices above 15 and silently truncated them.
|
||||||
|
- `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
|
||||||
|
|
||||||
|
The standalone assembler reaches the whole go-flac AVX2 kernel: static
|
||||||
|
symbols assemble, and all 17 kernel functions now match the Go toolchain's
|
||||||
|
machine code byte for byte.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: **file-level assembly** — `AssembleFile` turns a parsed file into an
|
||||||
|
`Image`: the function bodies in source order followed by a data section
|
||||||
|
built from the file's `GLOBL`/`DATA` directives (each symbol 16-aligned).
|
||||||
|
- `asm`: **static-symbol (`SB`) operands** — `mask<>(SB)` references encode as
|
||||||
|
RIP-relative loads with a patched disp32, resolved against the image layout
|
||||||
|
so the output is self-consistent and position-independent. External
|
||||||
|
(non-file-local) symbols are rejected with a clear error: they need
|
||||||
|
object-file emission.
|
||||||
|
- `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
|
||||||
|
|
||||||
|
The assembler reaches byte-identical parity with the Go toolchain on the
|
||||||
|
production go-flac AVX2 kernels: every one of the 15 kernel functions that
|
||||||
|
avoid global symbols now assembles to exactly the Go assembler's bytes (the
|
||||||
|
two holdouts load a file-local constant through `SB` and wait on relocation
|
||||||
|
support).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: the scalar instruction families the kernels use — `CMOVcc` and
|
||||||
|
`SETcc` (conditions spelled exactly like the jumps), `LZCNT`/`TZCNT`
|
||||||
|
(legacy `F3 0F BD/BC`), the sign/zero-extending moves (`MOVBLZX`, `MOVBQZX`,
|
||||||
|
`MOVWLZX`, `MOVWQZX`, `MOVWLSX`, `MOVLQSX`), `CVTSL2SD`/`CVTSQ2SD` (the
|
||||||
|
legacy SSE encoding, as the Go assembler emits it), the traditional
|
||||||
|
three-operand `IMUL3{W,L,Q}`, and the variable-count vector shifts
|
||||||
|
(`VPSRLQ X0, Y8, Y8` — the count in an XMM register or memory takes the
|
||||||
|
ordinary NDS form).
|
||||||
|
- `asm`: **jump relaxation** — jumps start in the short (rel8) form and
|
||||||
|
expand to rel32 when the settled displacement does not fit, iterating the
|
||||||
|
layout to a fixed point (CALL is always rel32).
|
||||||
|
- `asm`: **jump-to-jump folding** — a conditional jump to a label whose only
|
||||||
|
instruction is an unconditional jump is redirected to the ultimate
|
||||||
|
target, replicating the Go toolchain's linker, which chases such chains
|
||||||
|
before it encodes branches.
|
||||||
|
- `parser`: leading negative displacements with a base and index
|
||||||
|
(`LEAQ -4(DX)(R9*4), R9`) parse into a fully populated address.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `asm`: `CMP` with a register or memory operand computed **second − first**
|
||||||
|
instead of first − second, silently inverting every condition that followed
|
||||||
|
(`CMPQ SI, R10; JGE` tested R10 ≥ SI). The encoding now always records
|
||||||
|
first − second — `CMP r/m, r` with the first operand in r/m, `CMP r, r/m`
|
||||||
|
with the first operand in reg — and is byte-identical to the Go assembler.
|
||||||
|
- `asm`: register-to-register `MOV` now uses the `r/m ← r` opcode (reg =
|
||||||
|
source), the Go assembler's choice; the output is byte-identical.
|
||||||
|
|
||||||
|
## [0.2.0] — 2026-07-07
|
||||||
|
|
||||||
|
The Phase 2 assembler grows the SIMD set: shuffles, extract/insert, permute
|
||||||
|
and the moves, on top of the Phase 1 VEX forms.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `asm`: four new VEX (AVX/AVX2) operand forms, each validated by round-trip
|
||||||
|
decoding through `golang.org/x/arch` **and** byte-for-byte against the
|
||||||
|
machine code the real Go assembler emits:
|
||||||
|
- the immediate shuffle (`VPSHUFD`, `VPERMQ`),
|
||||||
|
- the three-operand-plus-immediate form (`VSHUFPD`, `VPERM2I128`,
|
||||||
|
`VINSERTI128`),
|
||||||
|
- the lane extract (`VEXTRACTI128`, `VEXTRACTF128` — the YMM source occupies
|
||||||
|
the ModRM.reg field, the XMM/memory destination the r/m field),
|
||||||
|
- the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, `VMOVD`, `VMOVQ`,
|
||||||
|
`VMOVSD` — each direction picks its own opcode and VEX.W; a vector→vector
|
||||||
|
move uses the store-form layout, matching the Go assembler),
|
||||||
|
- the no-operand `VZEROUPPER`, and `VPERMD` in the NDS form,
|
||||||
|
- the floating-point and FMA set (`VADDPD`, `VMULPD`, `VXORPD`,
|
||||||
|
`VUNPCKHPD`, the scalar `VADDSD`/`VMULSD`, `VCVTDQ2PD`, `VFMADD231PD`).
|
||||||
|
With the scalar set and the earlier NDS / reg-rm / immediate-shift forms,
|
||||||
|
the encoder now covers every integer, shuffle and FP instruction the
|
||||||
|
go-flac AVX2 kernels use.
|
||||||
|
- `asm`: `CMP` accepts the immediate in the second operand position
|
||||||
|
(`CMPL CX, $31`) — the spelling the Go assembler accepts — encoding it
|
||||||
|
identically to the immediate-first form.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `asm`: an unused VEX.vvvv field is now stored as `1111` (v̄vvv = 1111), as
|
||||||
|
the hardware requires — the previous value (`0000`) made the two-operand
|
||||||
|
reg/rm forms (VPMOVSXWD, VPBROADCASTD, VMOVMSKPS, …) raise #UD on real CPUs
|
||||||
|
and differ from the Go assembler's bytes. The round-trip decoder ignores
|
||||||
|
the field on these instructions, which is why the byte-for-byte Go
|
||||||
|
comparison (added this release) is now part of the test suite.
|
||||||
|
|
||||||
|
## [0.1.0] — 2026-07-06
|
||||||
|
|
||||||
|
Initial release — the Phase 1 foundation.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `token`, `lexer`, `ast`, `parser`: a hand-written, error-tolerant front end
|
||||||
|
for Plan 9 assembly. The lexer splices C-preprocessor line continuations
|
||||||
|
(`\` before a newline) so multi-line `#define` macros parse as one opaque
|
||||||
|
directive. Validated against the production AVX2/AVX-512 kernels in
|
||||||
|
`go-libraries/go-flac` and the Go runtime's `src/runtime/*.s` for all four
|
||||||
|
architectures, with zero parse errors.
|
||||||
|
- `arch`: register files and **complete** instruction tables for amd64,
|
||||||
|
arm64, riscv64 and loong64, with the middle-dot symbol separator and static
|
||||||
|
(`<>`) symbols. Instruction names are generated from the Go toolchain's own
|
||||||
|
assembler source (`just gen`) — the `anames` opcode lists plus the common
|
||||||
|
opcodes and the per-architecture front-end aliases (arm64 `B`/`BL`, the
|
||||||
|
`.P`/`.W` addressing suffixes, loong64 `JAL`, the x86 conditional-jump
|
||||||
|
spellings) — so every mnemonic the real assembler accepts is recognised.
|
||||||
|
- `lint`: conservative rules — `unknown-instruction`, `operand-count`,
|
||||||
|
`undefined-label`, `duplicate-label`, `missing-ret`,
|
||||||
|
`missing-textflag-include`, `abi-argsize` and `unreachable-code`. Macro
|
||||||
|
invocations are recognised (in-file `#define` names and underscore
|
||||||
|
identifiers) and the label/RET heuristics are suppressed in macro-using
|
||||||
|
files. `abi-argsize` parses the `// func` signature with the Go parser and
|
||||||
|
checks the declared TEXT argument size against Go's ABI0 layout;
|
||||||
|
`unreachable-code` flags dead code after `RET`, suppressed where reachability
|
||||||
|
is undecidable (PC-relative jumps, register-indirect branches, `#ifdef`).
|
||||||
|
Register liveness is computed by dataflow over the control-flow graph (basic
|
||||||
|
blocks, def/use, iterative backward iteration) and drives `register-clobber`,
|
||||||
|
an audit that flags a callee-saved register written but never saved/restored.
|
||||||
|
`funcdata-pcdata` validates the structure of `FUNCDATA`/`PCDATA` directives.
|
||||||
|
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
|
||||||
|
mnemonic alignment) that preserves comments and round-trips through the
|
||||||
|
parser.
|
||||||
|
- `lsp`: a Language Server Protocol server over stdio providing completion,
|
||||||
|
hover documentation, document symbols, publish-diagnostics and semantic-token
|
||||||
|
highlighting.
|
||||||
|
- `asm`: a standalone amd64 (x86-64) assembler — an instruction encoder (REX/
|
||||||
|
ModR-M/SIB/displacement/immediate plus the scalar instruction set, and VEX/
|
||||||
|
AVX2 SIMD across three operand forms — NDS, reg/rm and immediate-shift —
|
||||||
|
covering the bulk of the integer SIMD set) validated by round-trip decoding
|
||||||
|
against `golang.org/x/arch`, and an assembler that drives the parser's AST
|
||||||
|
into the encoder with local-label resolution and `FP`/`SP` frame mapping
|
||||||
|
(plus Go prologue/epilogue generation), producing output byte-identical to the
|
||||||
|
Go assembler for the supported operand forms.
|
||||||
|
- `cmd/gasm`: the `gasm` binary with `tokens`, `parse`, `fmt`, `lint`, `asm`
|
||||||
|
and `lsp` subcommands.
|
||||||
|
- `_gen`: the generator that rebuilds the architecture instruction tables from
|
||||||
|
the Go toolchain source (`just gen`).
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Contributing to gasm-devkit
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.26 or later (`toolchain go1.26.5`)
|
||||||
|
- `just` command runner
|
||||||
|
- A Linux, FreeBSD, or macOS host on amd64 or arm64
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Every just recipe:
|
||||||
|
|
||||||
|
| 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`) |
|
||||||
|
|
||||||
|
## Running a Single Test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -run TestVexGroundTruth ./asm/
|
||||||
|
go test -run TestDifferentialLZ4Fuzz ./verify/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Debugger
|
||||||
|
|
||||||
|
The interactive debugger (`gasm debug`) requires a compiled binary —
|
||||||
|
`go run` does not work for the child process. Install first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just install-bin
|
||||||
|
gasm debug --func add testdata/verify/basic_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
See [AGENTS.md](AGENTS.md) for the full style guide. Key points:
|
||||||
|
|
||||||
|
- `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`.
|
||||||
|
|
||||||
|
## Branches and Releases
|
||||||
|
|
||||||
|
- `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>`.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
Open an issue at
|
||||||
|
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues).
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
# gasm-devkit
|
||||||
|
|
||||||
|
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,
|
||||||
|
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.
|
||||||
|
|
||||||
|
gasm-devkit is the missing toolkit. It is a single, self-contained binary —
|
||||||
|
`gasm` — that brings proper developer tooling to Plan 9 assembly:
|
||||||
|
|
||||||
|
```
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Status: Phase 4 — done.** 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) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC,
|
||||||
|
> ELF emission, ground-truth) in v0.28.0. See [Roadmap](#roadmap).
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|
||||||
|
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
||||||
|
`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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### Phase 1 — language foundation, editor tooling and static analysis · *done*
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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. Both go-lz4 kernels
|
||||||
|
(AVX2, 845 bytes total) JIT-load and execute correctly.
|
||||||
|
- **Differential testing** — *done.* The JIT-assembled kernel is fuzzed
|
||||||
|
with random valid LZ4 blocks and hostile garbage, comparing the result
|
||||||
|
**bit-for-bit** against a portable Go reference; 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; both go-lz4 kernels pass clean.
|
||||||
|
- **Coverage / basic-block profiling** — *done.* Static block enumeration
|
||||||
|
from the assembler's label map (27 blocks in `decodeBlockAVX2`) plus
|
||||||
|
multi-input path-diversity measurement: how many observationally distinct
|
||||||
|
execution paths a test corpus exercises.
|
||||||
|
|
||||||
|
### Phase 4 — debugger · *in progress*
|
||||||
|
|
||||||
|
- **`gasm debug`:** single-step a GAsm function, inspect registers, set
|
||||||
|
breakpoints on labels, 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, label resolution, breakpoint
|
||||||
|
management via `/proc/pid/mem`, 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
|
||||||
|
|
||||||
|
- **Encoding for arm64, riscv64 and loong64.** The lexer, parser, linter
|
||||||
|
and formatter already cover all four architectures; the assembler today
|
||||||
|
encodes amd64 only. Phase 5 brings the same encode-and-verify treatment
|
||||||
|
(instruction tables already generated from the toolchain, every encoding
|
||||||
|
checked byte for byte against `go tool asm`) to the remaining three.
|
||||||
|
|
||||||
|
## 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 amd64 assembler: encoder, linker, object-file emitters. |
|
||||||
|
| `verify` | JIT execution substrate for dynamic analysis (Phase 3). |
|
||||||
|
| `debug` | Interactive ptrace debugger for amd64 (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).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the binary and use it:
|
||||||
|
|
||||||
|
```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 debug --func name k.s # interactive debugger
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
binary and associate it with `.s` files. Syntax highlighting is delivered as
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
BSD-3-Clause — the same licence as Go itself. See [`LICENSE`](LICENSE).
|
||||||
+8
-6
@@ -23,7 +23,7 @@ import (
|
|||||||
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
|
||||||
// integer and shuffle/extract/permute/move set is in.
|
// integer and shuffle/extract/permute/move set is in.
|
||||||
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
||||||
code, _, labels, _, err := assemble(t, nil)
|
code, _, labels, _, _, err := assemble(t, nil)
|
||||||
return code, labels, err
|
return code, labels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ type spadjStep struct {
|
|||||||
// assemble encodes a TEXT body, returning the machine code, the static-symbol
|
// assemble encodes a TEXT body, returning the machine code, the static-symbol
|
||||||
// patch sites (for the file-level layout to resolve), the label table and the
|
// patch sites (for the file-level layout to resolve), the label table and the
|
||||||
// stack-adjustment boundaries.
|
// stack-adjustment boundaries.
|
||||||
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) {
|
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, []LineEntry, error) {
|
||||||
fi := computeFrame(t)
|
fi := computeFrame(t)
|
||||||
chain := jumpChain(t)
|
chain := jumpChain(t)
|
||||||
resolve := func(name string) string {
|
resolve := func(name string) string {
|
||||||
@@ -84,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
case *ast.Instr:
|
case *ast.Instr:
|
||||||
sz, err := instrSize(s, fi, long[i], link)
|
sz, err := instrSize(s, fi, long[i], link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
sizes[i] = sz
|
sizes[i] = sz
|
||||||
pcs[i] = pos
|
pcs[i] = pos
|
||||||
@@ -125,6 +125,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
out := append([]byte(nil), fi.prologue...)
|
out := append([]byte(nil), fi.prologue...)
|
||||||
var patches []sbPatch
|
var patches []sbPatch
|
||||||
var steps []spadjStep
|
var steps []spadjStep
|
||||||
|
var lines []LineEntry
|
||||||
if fi.useFP {
|
if fi.useFP {
|
||||||
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
||||||
// changes nothing; SUBQ $size, SP completes the frame.
|
// changes nothing; SUBQ $size, SP completes the frame.
|
||||||
@@ -150,16 +151,17 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
}
|
}
|
||||||
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
if len(code) != sizes[i] {
|
if len(code) != sizes[i] {
|
||||||
return nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||||
}
|
}
|
||||||
patches = append(patches, ps...)
|
patches = append(patches, ps...)
|
||||||
|
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pos += len(code)
|
pos += len(code)
|
||||||
}
|
}
|
||||||
return out, patches, offsets, steps, nil
|
return out, patches, offsets, steps, lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
|
||||||
|
|||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RISC-V ELF64 relocatable object emission.
|
||||||
|
|
||||||
|
const (
|
||||||
|
emRISCV = 243 // EM_RISCV
|
||||||
|
|
||||||
|
// RISC-V relocation types.
|
||||||
|
rRISCV32 = 1
|
||||||
|
rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20
|
||||||
|
rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I
|
||||||
|
rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S
|
||||||
|
)
|
||||||
|
|
||||||
|
// ELFRISCVObject returns the image as an ELF64 relocatable object file for
|
||||||
|
// RISC-V (EM_RISCV, 64-bit, little-endian). The structure mirrors the amd64
|
||||||
|
// ELF emission: .text, .data, .symtab, .strtab and optional .rela.text.
|
||||||
|
func (img *Image) ELFRISCVObject() ([]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 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.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// Determine relocation type from the relocation kind.
|
||||||
|
typ := uint32(rRISCVPCRELHI20) // default: AUIPC
|
||||||
|
switch r.Kind {
|
||||||
|
case RelPCRelLO12:
|
||||||
|
typ = rRISCVPCRELLO12I
|
||||||
|
case RelPCRelLO12S:
|
||||||
|
typ = rRISCVPCRELLO12S
|
||||||
|
case RelPCRelAbs:
|
||||||
|
typ = rRISCV32
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()...)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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:], emRISCV)
|
||||||
|
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
|
||||||
|
}
|
||||||
+114
-1
@@ -41,6 +41,7 @@ type FuncLayout struct {
|
|||||||
Labels map[string]int // local labels, function-relative
|
Labels map[string]int // local labels, function-relative
|
||||||
Relocs []Reloc // static-symbol references, in emission order
|
Relocs []Reloc // static-symbol references, in emission order
|
||||||
Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC
|
Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC
|
||||||
|
Lines []LineEntry // source-line table: byte offset → source line
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the
|
// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the
|
||||||
@@ -50,17 +51,58 @@ type SpadjStep struct {
|
|||||||
Value int
|
Value int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LineEntry maps a byte offset (function-relative) to a source line number.
|
||||||
|
type LineEntry struct {
|
||||||
|
Offset int
|
||||||
|
Line int
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineAt returns the source line number for the given function-relative byte
|
||||||
|
// offset, using a binary search on the line table. Returns 0 if the offset
|
||||||
|
// is before the first instruction or the table is empty.
|
||||||
|
func (fl *FuncLayout) LineAt(offset int) int {
|
||||||
|
if len(fl.Lines) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Binary search: find the last entry with Offset <= offset.
|
||||||
|
lo, hi := 0, len(fl.Lines)-1
|
||||||
|
for lo < hi {
|
||||||
|
mid := (lo + hi + 1) / 2
|
||||||
|
if fl.Lines[mid].Offset <= offset {
|
||||||
|
lo = mid
|
||||||
|
} else {
|
||||||
|
hi = mid - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fl.Lines[lo].Offset <= offset {
|
||||||
|
return fl.Lines[lo].Line
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Reloc is one static-symbol reference within a function body: the disp32
|
// Reloc is one static-symbol reference within a function body: the disp32
|
||||||
// field at Off (function-relative) must reach the symbol plus Addend,
|
// field at Off (function-relative) must reach the symbol plus Addend,
|
||||||
// measured from After, the address just past the instruction. An External
|
// measured from After, the address just past the instruction. An External
|
||||||
// relocation names a symbol no GLOBL in the file defines; the object-file
|
// relocation names a symbol no GLOBL in the file defines; the object-file
|
||||||
// emitters carry it into the output's relocation table.
|
// emitters carry it into the output's relocation table.
|
||||||
|
// RelocKind discriminates the type of relocation needed.
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
type Reloc struct {
|
type Reloc struct {
|
||||||
Off int
|
Off int
|
||||||
After int
|
After int
|
||||||
Name string
|
Name string
|
||||||
Addend int64
|
Addend int64
|
||||||
External bool
|
External bool
|
||||||
|
Kind RelocKind
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
||||||
@@ -110,7 +152,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
code, patches, labels, steps, err := assemble(t, link)
|
code, patches, labels, steps, lines, err := assemble(t, link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||||
}
|
}
|
||||||
@@ -124,6 +166,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
Args: argsSize(t),
|
Args: argsSize(t),
|
||||||
Line: t.Pos().Line,
|
Line: t.Pos().Line,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
|
Lines: lines,
|
||||||
}
|
}
|
||||||
for _, f := range t.Flags {
|
for _, f := range t.Flags {
|
||||||
switch f {
|
switch f {
|
||||||
@@ -189,11 +232,80 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file
|
||||||
|
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
||||||
|
// code. SB references in the code are encoded as AUIPC pairs with zero
|
||||||
|
// immediates; the object-file emitters record relocations for the linker.
|
||||||
|
func AssembleFileRISCV(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, err := assembleRISCV(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,
|
||||||
|
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: pos,
|
||||||
|
Size: d.size,
|
||||||
|
Static: d.static,
|
||||||
|
Rodata: d.rodata,
|
||||||
|
Dupok: d.dupok,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
// dataSym is one GLOBL symbol and its DATA initialiser.
|
// dataSym is one GLOBL symbol and its DATA initialiser.
|
||||||
type dataSym struct {
|
type dataSym struct {
|
||||||
name string
|
name string
|
||||||
pkg string
|
pkg string
|
||||||
buf []byte
|
buf []byte
|
||||||
|
size int
|
||||||
static bool
|
static bool
|
||||||
rodata bool
|
rodata bool
|
||||||
dupok bool
|
dupok bool
|
||||||
@@ -223,6 +335,7 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
|||||||
name: name,
|
name: name,
|
||||||
pkg: dd.Name.Pkg,
|
pkg: dd.Name.Pkg,
|
||||||
buf: make([]byte, size),
|
buf: make([]byte, size),
|
||||||
|
size: size,
|
||||||
static: dd.Name.Static,
|
static: dd.Name.Static,
|
||||||
}
|
}
|
||||||
for _, f := range dd.Flags {
|
for _, f := range dd.Flags {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,537 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
// RISC-V register encoding: maps register names to their 5-bit numbers.
|
||||||
|
// The Go assembler uses the standard RISC-V ABI naming.
|
||||||
|
|
||||||
|
// riscvRegNum returns the 5-bit register number for a RISC-V register name.
|
||||||
|
// Returns -1 if the register is not recognized.
|
||||||
|
func riscvRegNum(name string) int {
|
||||||
|
switch name {
|
||||||
|
// Numbered integer registers.
|
||||||
|
case "X0", "ZERO":
|
||||||
|
return 0
|
||||||
|
case "X1", "RA":
|
||||||
|
return 1
|
||||||
|
case "X2", "SP":
|
||||||
|
return 2
|
||||||
|
case "X3", "GP":
|
||||||
|
return 3
|
||||||
|
case "X4", "TP":
|
||||||
|
return 4
|
||||||
|
case "X5", "T0", "LR":
|
||||||
|
return 5
|
||||||
|
case "X6", "T1", "TMP":
|
||||||
|
return 6
|
||||||
|
case "X7", "T2":
|
||||||
|
return 7
|
||||||
|
case "X8", "S0", "FP":
|
||||||
|
return 8
|
||||||
|
case "X9", "S1":
|
||||||
|
return 9
|
||||||
|
case "X10", "A0":
|
||||||
|
return 10
|
||||||
|
case "X11", "A1":
|
||||||
|
return 11
|
||||||
|
case "X12", "A2":
|
||||||
|
return 12
|
||||||
|
case "X13", "A3":
|
||||||
|
return 13
|
||||||
|
case "X14", "A4":
|
||||||
|
return 14
|
||||||
|
case "X15", "A5":
|
||||||
|
return 15
|
||||||
|
case "X16", "A6":
|
||||||
|
return 16
|
||||||
|
case "X17", "A7":
|
||||||
|
return 17
|
||||||
|
case "X18", "S2":
|
||||||
|
return 18
|
||||||
|
case "X19", "S3":
|
||||||
|
return 19
|
||||||
|
case "X20", "S4":
|
||||||
|
return 20
|
||||||
|
case "X21", "S5":
|
||||||
|
return 21
|
||||||
|
case "X22", "S6":
|
||||||
|
return 22
|
||||||
|
case "X23", "S7":
|
||||||
|
return 23
|
||||||
|
case "X24", "S8":
|
||||||
|
return 24
|
||||||
|
case "X25", "S9":
|
||||||
|
return 25
|
||||||
|
case "X26", "S10":
|
||||||
|
return 26
|
||||||
|
case "X27", "S11":
|
||||||
|
return 27
|
||||||
|
case "X28", "T3":
|
||||||
|
return 28
|
||||||
|
case "X29", "T4":
|
||||||
|
return 29
|
||||||
|
case "X30", "T5":
|
||||||
|
return 30
|
||||||
|
case "X31", "T6":
|
||||||
|
return 31
|
||||||
|
// Floating-point registers (F0-F31).
|
||||||
|
case "F0", "FT0":
|
||||||
|
return 0
|
||||||
|
case "F1", "FT1":
|
||||||
|
return 1
|
||||||
|
case "F2", "FT2":
|
||||||
|
return 2
|
||||||
|
case "F3", "FT3":
|
||||||
|
return 3
|
||||||
|
case "F4", "FT4":
|
||||||
|
return 4
|
||||||
|
case "F5", "FT5":
|
||||||
|
return 5
|
||||||
|
case "F6", "FT6":
|
||||||
|
return 6
|
||||||
|
case "F7", "FT7":
|
||||||
|
return 7
|
||||||
|
case "F8", "FS0":
|
||||||
|
return 8
|
||||||
|
case "F9", "FS1":
|
||||||
|
return 9
|
||||||
|
case "F10", "FA0":
|
||||||
|
return 10
|
||||||
|
case "F11", "FA1":
|
||||||
|
return 11
|
||||||
|
case "F12", "FA2":
|
||||||
|
return 12
|
||||||
|
case "F13", "FA3":
|
||||||
|
return 13
|
||||||
|
case "F14", "FA4":
|
||||||
|
return 14
|
||||||
|
case "F15", "FA5":
|
||||||
|
return 15
|
||||||
|
case "F16", "FA6":
|
||||||
|
return 16
|
||||||
|
case "F17", "FA7":
|
||||||
|
return 17
|
||||||
|
case "F18", "FS2":
|
||||||
|
return 18
|
||||||
|
case "F19", "FS3":
|
||||||
|
return 19
|
||||||
|
case "F20", "FS4":
|
||||||
|
return 20
|
||||||
|
case "F21", "FS5":
|
||||||
|
return 21
|
||||||
|
case "F22", "FS6":
|
||||||
|
return 22
|
||||||
|
case "F23", "FS7":
|
||||||
|
return 23
|
||||||
|
case "F24", "FS8":
|
||||||
|
return 24
|
||||||
|
case "F25", "FS9":
|
||||||
|
return 25
|
||||||
|
case "F26", "FS10":
|
||||||
|
return 26
|
||||||
|
case "F27", "FS11":
|
||||||
|
return 27
|
||||||
|
case "F28", "FT8":
|
||||||
|
return 28
|
||||||
|
case "F29", "FT9":
|
||||||
|
return 29
|
||||||
|
case "F30", "FT10":
|
||||||
|
return 30
|
||||||
|
case "F31", "FT11":
|
||||||
|
return 31
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RISC-V instruction encoding parameters.
|
||||||
|
type riscvEnc struct {
|
||||||
|
opcode uint32 // bits [6:0]
|
||||||
|
funct3 uint32 // bits [14:12]
|
||||||
|
funct7 uint32 // bits [31:25]
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvInstrTable maps RISC-V mnemonics to their encoding.
|
||||||
|
var riscvInstrTable = map[string]riscvEnc{
|
||||||
|
// RV64I — R-type arithmetic/logic.
|
||||||
|
"ADD": {0x33, 0x0, 0x00},
|
||||||
|
"SUB": {0x33, 0x0, 0x20},
|
||||||
|
"SLL": {0x33, 0x1, 0x00},
|
||||||
|
"SLT": {0x33, 0x2, 0x00},
|
||||||
|
"SLTU": {0x33, 0x3, 0x00},
|
||||||
|
"XOR": {0x33, 0x4, 0x00},
|
||||||
|
"SRL": {0x33, 0x5, 0x00},
|
||||||
|
"SRA": {0x33, 0x5, 0x20},
|
||||||
|
"OR": {0x33, 0x6, 0x00},
|
||||||
|
"AND": {0x33, 0x7, 0x00},
|
||||||
|
// RV64I — 32-bit variants (W suffix).
|
||||||
|
"ADDW": {0x3B, 0x0, 0x00},
|
||||||
|
"SUBW": {0x3B, 0x0, 0x20},
|
||||||
|
"SLLW": {0x3B, 0x1, 0x00},
|
||||||
|
"SRLW": {0x3B, 0x5, 0x00},
|
||||||
|
"SRAW": {0x3B, 0x5, 0x20},
|
||||||
|
// RV64I — I-type shift-immediate (shamt in rs2 field).
|
||||||
|
"SLLI": {0x13, 0x1, 0x00},
|
||||||
|
"SRLI": {0x13, 0x5, 0x00},
|
||||||
|
"SRAI": {0x13, 0x5, 0x20},
|
||||||
|
"SLLIW": {0x1B, 0x1, 0x00},
|
||||||
|
"SRLIW": {0x1B, 0x5, 0x00},
|
||||||
|
"SRAIW": {0x1B, 0x5, 0x20},
|
||||||
|
// RV64M — multiply/divide.
|
||||||
|
"MUL": {0x33, 0x0, 0x01},
|
||||||
|
"MULH": {0x33, 0x1, 0x01},
|
||||||
|
"MULHSU": {0x33, 0x2, 0x01},
|
||||||
|
"MULHU": {0x33, 0x3, 0x01},
|
||||||
|
"DIV": {0x33, 0x4, 0x01},
|
||||||
|
"DIVU": {0x33, 0x5, 0x01},
|
||||||
|
"REM": {0x33, 0x6, 0x01},
|
||||||
|
"REMU": {0x33, 0x7, 0x01},
|
||||||
|
// RV64M — 32-bit variants.
|
||||||
|
"MULW": {0x3B, 0x0, 0x01},
|
||||||
|
"DIVW": {0x3B, 0x4, 0x01},
|
||||||
|
"DIVUW": {0x3B, 0x5, 0x01},
|
||||||
|
"REMW": {0x3B, 0x6, 0x01},
|
||||||
|
"REMUW": {0x3B, 0x7, 0x01},
|
||||||
|
// RV64I — I-type arithmetic.
|
||||||
|
"ADDI": {0x13, 0x0, 0x00},
|
||||||
|
"ADDIW": {0x1B, 0x0, 0x00},
|
||||||
|
"SLTI": {0x13, 0x2, 0x00},
|
||||||
|
"SLTIU": {0x13, 0x3, 0x00},
|
||||||
|
"XORI": {0x13, 0x4, 0x00},
|
||||||
|
"ORI": {0x13, 0x6, 0x00},
|
||||||
|
"ANDI": {0x13, 0x7, 0x00},
|
||||||
|
// Loads (I-type).
|
||||||
|
"LB": {0x03, 0x0, 0x00},
|
||||||
|
"LH": {0x03, 0x1, 0x00},
|
||||||
|
"LW": {0x03, 0x2, 0x00},
|
||||||
|
"LD": {0x03, 0x3, 0x00},
|
||||||
|
"LBU": {0x03, 0x4, 0x00},
|
||||||
|
"LHU": {0x03, 0x5, 0x00},
|
||||||
|
"LWU": {0x03, 0x6, 0x00},
|
||||||
|
// Stores (S-type).
|
||||||
|
"SB": {0x23, 0x0, 0x00},
|
||||||
|
"SH": {0x23, 0x1, 0x00},
|
||||||
|
"SW": {0x23, 0x2, 0x00},
|
||||||
|
"SD": {0x23, 0x3, 0x00},
|
||||||
|
// Branches (B-type).
|
||||||
|
"BEQ": {0x63, 0x0, 0x00},
|
||||||
|
"BNE": {0x63, 0x1, 0x00},
|
||||||
|
"BLT": {0x63, 0x4, 0x00},
|
||||||
|
"BGE": {0x63, 0x5, 0x00},
|
||||||
|
"BLTU": {0x63, 0x6, 0x00},
|
||||||
|
"BGEU": {0x63, 0x7, 0x00},
|
||||||
|
// U-type.
|
||||||
|
"LUI": {0x37, 0x0, 0x00},
|
||||||
|
"AUIPC": {0x17, 0x0, 0x00},
|
||||||
|
// System.
|
||||||
|
"ECALL": {0x73, 0x0, 0x00},
|
||||||
|
"EBREAK": {0x73, 0x0, 0x00},
|
||||||
|
"FENCE": {0x0F, 0x0, 0x00},
|
||||||
|
// JALR — indirect jump/call (I-type).
|
||||||
|
"JALR": {0x67, 0x0, 0x00},
|
||||||
|
|
||||||
|
// RV64A — atomics (AMO opcode 0x2F).
|
||||||
|
// funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27].
|
||||||
|
"AMOSWAPW": {0x2F, 0x2, 0x01 << 2},
|
||||||
|
"AMOSWAPD": {0x2F, 0x3, 0x01 << 2},
|
||||||
|
"AMOADDW": {0x2F, 0x2, 0x00 << 2},
|
||||||
|
"AMOADDD": {0x2F, 0x3, 0x00 << 2},
|
||||||
|
"AMOANDW": {0x2F, 0x2, 0x0C << 2},
|
||||||
|
"AMOANDD": {0x2F, 0x3, 0x0C << 2},
|
||||||
|
"AMOORW": {0x2F, 0x2, 0x06 << 2},
|
||||||
|
"AMOORD": {0x2F, 0x3, 0x06 << 2},
|
||||||
|
"AMOXORW": {0x2F, 0x2, 0x04 << 2},
|
||||||
|
"AMOXORD": {0x2F, 0x3, 0x04 << 2},
|
||||||
|
"AMOMAXW": {0x2F, 0x2, 0x14 << 2},
|
||||||
|
"AMOMAXD": {0x2F, 0x3, 0x14 << 2},
|
||||||
|
"AMOMINW": {0x2F, 0x2, 0x10 << 2},
|
||||||
|
"AMOMIND": {0x2F, 0x3, 0x10 << 2},
|
||||||
|
"AMOMAXUW": {0x2F, 0x2, 0x1C << 2},
|
||||||
|
"AMOMAXUD": {0x2F, 0x3, 0x1C << 2},
|
||||||
|
"AMOMINUW": {0x2F, 0x2, 0x18 << 2},
|
||||||
|
"AMOMINUD": {0x2F, 0x3, 0x18 << 2},
|
||||||
|
|
||||||
|
// RV64F/D — floating-point arithmetic.
|
||||||
|
"FADDS": {0x53, 0x0, 0x00},
|
||||||
|
"FSUBS": {0x53, 0x0, 0x04},
|
||||||
|
"FMULS": {0x53, 0x0, 0x08},
|
||||||
|
"FDIVS": {0x53, 0x0, 0x0C},
|
||||||
|
"FADDD": {0x53, 0x0, 0x01},
|
||||||
|
"FSUBD": {0x53, 0x0, 0x05},
|
||||||
|
"FMULD": {0x53, 0x0, 0x09},
|
||||||
|
"FDIVD": {0x53, 0x0, 0x0D},
|
||||||
|
"FSQRTS": {0x53, 0x0, 0x2C},
|
||||||
|
"FSQRTD": {0x53, 0x0, 0x2D},
|
||||||
|
// FP loads/stores.
|
||||||
|
"FLW": {0x07, 0x2, 0x00},
|
||||||
|
"FLD": {0x07, 0x3, 0x00},
|
||||||
|
"FSW": {0x27, 0x2, 0x00},
|
||||||
|
"FSD": {0x27, 0x3, 0x00},
|
||||||
|
// FP min/max.
|
||||||
|
"FMINS": {0x53, 0x0, 0x14},
|
||||||
|
"FMAXS": {0x53, 0x1, 0x14},
|
||||||
|
"FMIND": {0x53, 0x0, 0x15},
|
||||||
|
"FMAXD": {0x53, 0x1, 0x15},
|
||||||
|
|
||||||
|
// RV64A — load-reserved / store-conditional (funct5 0x02 / 0x03).
|
||||||
|
"LRW": {0x2F, 0x2, 0x02 << 2},
|
||||||
|
"LRD": {0x2F, 0x3, 0x02 << 2},
|
||||||
|
"SCW": {0x2F, 0x2, 0x03 << 2},
|
||||||
|
"SCD": {0x2F, 0x3, 0x03 << 2},
|
||||||
|
|
||||||
|
// FP compare — result in integer register (funct7 0x50/0x51).
|
||||||
|
"FEQS": {0x53, 0x2, 0x50},
|
||||||
|
"FLTS": {0x53, 0x1, 0x50},
|
||||||
|
"FLES": {0x53, 0x0, 0x50},
|
||||||
|
"FEQD": {0x53, 0x2, 0x51},
|
||||||
|
"FLTD": {0x53, 0x1, 0x51},
|
||||||
|
"FLED": {0x53, 0x0, 0x51},
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvRType encodes an R-type instruction: funct7 | rs2 | rs1 | funct3 | rd | opcode.
|
||||||
|
func riscvRType(enc riscvEnc, rd, rs1, rs2 int) uint32 {
|
||||||
|
return (enc.funct7 << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) |
|
||||||
|
(enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvAMOType encodes an atomic (AMO) instruction.
|
||||||
|
// Layout: funct5 | aq | rl | rs2 | rs1 | funct3 | rd | opcode.
|
||||||
|
// The funct5 is stored in the upper bits of enc.funct7 (shifted left by 2).
|
||||||
|
func riscvAMOType(enc riscvEnc, rd, rs1, rs2 int) uint32 {
|
||||||
|
funct5 := enc.funct7 >> 2 // extract funct5 from the stored value
|
||||||
|
return (funct5 << 27) | (uint32(rs2) << 20) | (uint32(rs1) << 15) |
|
||||||
|
(enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// FP conversion instructions (FCVT, FMV). These use the rs2 field to
|
||||||
|
// encode the conversion type rather than a register, so they are handled
|
||||||
|
// separately from the general instruction table.
|
||||||
|
type riscvCvtEnc struct {
|
||||||
|
funct7 uint32 // bits [31:25]
|
||||||
|
rs2 uint32 // conversion-type code in bits [24:20]
|
||||||
|
opcode uint32 // always 0x53 (OP-FP)
|
||||||
|
}
|
||||||
|
|
||||||
|
var riscvCvtTable = map[string]riscvCvtEnc{
|
||||||
|
// float → int (rs2 selects the integer width/sign).
|
||||||
|
"FCVTWS": {0x60, 0x0, 0x53}, // float32 → int32
|
||||||
|
"FCVTWUS": {0x60, 0x1, 0x53}, // float32 → uint32
|
||||||
|
"FCVTLS": {0x60, 0x2, 0x53}, // float32 → int64
|
||||||
|
"FCVTLUS": {0x60, 0x3, 0x53}, // float32 → uint64
|
||||||
|
"FCVTWD": {0x61, 0x0, 0x53}, // float64 → int32
|
||||||
|
"FCVTWUD": {0x61, 0x1, 0x53}, // float64 → uint32
|
||||||
|
"FCVTLD": {0x61, 0x2, 0x53}, // float64 → int64
|
||||||
|
"FCVTLUD": {0x61, 0x3, 0x53}, // float64 → uint64
|
||||||
|
// int → float (rs2 selects the integer width/sign).
|
||||||
|
"FCVTSW": {0x68, 0x0, 0x53}, // int32 → float32
|
||||||
|
"FCVTSWU": {0x68, 0x1, 0x53}, // uint32 → float32
|
||||||
|
"FCVTSL": {0x68, 0x2, 0x53}, // int64 → float32
|
||||||
|
"FCVTSLU": {0x68, 0x3, 0x53}, // uint64 → float32
|
||||||
|
"FCVTDW": {0x69, 0x0, 0x53}, // int32 → float64
|
||||||
|
"FCVTDWU": {0x69, 0x1, 0x53}, // uint32 → float64
|
||||||
|
"FCVTDL": {0x69, 0x2, 0x53}, // int64 → float64
|
||||||
|
"FCVTDLU": {0x69, 0x3, 0x53}, // uint64 → float64
|
||||||
|
// float → float width conversion.
|
||||||
|
"FCVTSD": {0x20, 0x1, 0x53}, // float64 → float32
|
||||||
|
"FCVTDS": {0x21, 0x0, 0x53}, // float32 → float64
|
||||||
|
// Bit moves between integer and FP registers (no conversion).
|
||||||
|
"FMVXD": {0x71, 0x0, 0x53}, // float64 → int64 (bit move)
|
||||||
|
"FMVDX": {0x79, 0x0, 0x53}, // int64 → float64 (bit move)
|
||||||
|
"FMVXW": {0x70, 0x0, 0x53}, // float32 → int32 (bit move)
|
||||||
|
"FMVWX": {0x78, 0x0, 0x53}, // int32 → float32 (bit move)
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvCvtType encodes an FP conversion instruction.
|
||||||
|
// Layout: funct7 | rs2(convtype) | rs1 | funct3(0) | rd | opcode.
|
||||||
|
func riscvCvtType(enc riscvCvtEnc, rd, rs1 int) uint32 {
|
||||||
|
return (enc.funct7 << 25) | (enc.rs2 << 20) | (uint32(rs1) << 15) |
|
||||||
|
(uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// R4-type fused multiply-add instructions (FMADD/FMSUB/FNMSUB/FNMADD).
|
||||||
|
// These take 4 register operands: rs1, rs2, rs3, rd.
|
||||||
|
// Layout: rs3 | fmt | rs2 | rs1 | rm | rd | opcode.
|
||||||
|
type riscvFmaEnc struct {
|
||||||
|
fmt uint32 // bits [26:25]: 0x0 = single, 0x1 = double
|
||||||
|
opcode uint32 // bits [6:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var riscvFmaTable = map[string]riscvFmaEnc{
|
||||||
|
"FMADDS": {0x0, 0x43}, // rd = rs1*rs2 + rs3
|
||||||
|
"FMADDD": {0x1, 0x43},
|
||||||
|
"FMSUBS": {0x0, 0x47}, // rd = rs1*rs2 - rs3
|
||||||
|
"FMSUBD": {0x1, 0x47},
|
||||||
|
"FNMSUBS": {0x0, 0x4B}, // rd = -(rs1*rs2) + rs3
|
||||||
|
"FNMSUBD": {0x1, 0x4B},
|
||||||
|
"FNMADDS": {0x0, 0x4F}, // rd = -(rs1*rs2) - rs3
|
||||||
|
"FNMADDD": {0x1, 0x4F},
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvFmaType encodes an R4-type fused multiply-add instruction.
|
||||||
|
func riscvFmaType(enc riscvFmaEnc, rd, rs1, rs2, rs3 int) uint32 {
|
||||||
|
return (uint32(rs3) << 27) | (enc.fmt << 25) | (uint32(rs2) << 20) |
|
||||||
|
(uint32(rs1) << 15) | (0x0 << 12) /* rm=dynamic */ | (uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSR (Control and Status Register) instructions.
|
||||||
|
// Format: csr[11:0] | rs1/zimm | funct3 | rd | opcode (0x73).
|
||||||
|
type riscvCsrEnc struct {
|
||||||
|
funct3 uint32 // bits [14:12]
|
||||||
|
imm bool // true for CSRRWI/CSRRSI/CSRRCI (5-bit uimm variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
var riscvCsrTable = map[string]riscvCsrEnc{
|
||||||
|
"CSRRW": {0x1, false}, // rd=CSR, CSR=rs1
|
||||||
|
"CSRRS": {0x2, false}, // rd=CSR, CSR |= rs1
|
||||||
|
"CSRRC": {0x3, false}, // rd=CSR, CSR &= ~rs1
|
||||||
|
"CSRRWI": {0x5, true}, // rd=CSR, CSR=uimm
|
||||||
|
"CSRRSI": {0x6, true}, // rd=CSR, CSR |= uimm
|
||||||
|
"CSRRCI": {0x7, true}, // rd=CSR, CSR &= ~uimm
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvCsrType encodes a CSR instruction.
|
||||||
|
// csr is the 12-bit CSR address; src is either a register number or a 5-bit
|
||||||
|
// unsigned immediate (depending on enc.imm).
|
||||||
|
func riscvCsrType(enc riscvCsrEnc, rd, src int, csr int32) uint32 {
|
||||||
|
return (uint32(csr&0xFFF) << 20) | (uint32(src&0x1F) << 15) |
|
||||||
|
(enc.funct3 << 12) | (uint32(rd) << 7) | 0x73
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvIType encodes an I-type instruction: imm[11:0] | rs1 | funct3 | rd | opcode.
|
||||||
|
func riscvIType(enc riscvEnc, rd, rs1 int, imm int32) uint32 {
|
||||||
|
return (uint32(imm&0xFFF) << 20) | (uint32(rs1) << 15) |
|
||||||
|
(enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvSType encodes an S-type instruction: imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode.
|
||||||
|
func riscvSType(enc riscvEnc, rs1, rs2 int, imm int32) uint32 {
|
||||||
|
immU := uint32(imm) & 0xFFF
|
||||||
|
return ((immU >> 5) << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) |
|
||||||
|
(enc.funct3 << 12) | ((immU & 0x1F) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvBType encodes a B-type instruction (branches).
|
||||||
|
func riscvBType(enc riscvEnc, rs1, rs2 int, offset int32) uint32 {
|
||||||
|
imm := uint32(offset) & 0x1FFE // bits [12:1], bit 0 is always 0
|
||||||
|
return (((imm >> 12) & 1) << 31) | // imm[12]
|
||||||
|
(((imm >> 5) & 0x3F) << 25) | // imm[10:5]
|
||||||
|
(uint32(rs2) << 20) | (uint32(rs1) << 15) |
|
||||||
|
(enc.funct3 << 12) |
|
||||||
|
(((imm >> 1) & 0xF) << 8) | // imm[4:1]
|
||||||
|
(((imm >> 11) & 1) << 7) | // imm[11]
|
||||||
|
enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvUType encodes a U-type instruction: imm[31:12] | rd | opcode.
|
||||||
|
func riscvUType(enc riscvEnc, rd int, imm int32) uint32 {
|
||||||
|
return (uint32(imm) & 0xFFFFF000) | (uint32(rd) << 7) | enc.opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvJType encodes a J-type instruction (JAL).
|
||||||
|
func riscvJType(rd int, offset int32) uint32 {
|
||||||
|
imm := uint32(offset) & 0x1FFFFE // bits [20:1]
|
||||||
|
return (((imm >> 20) & 1) << 31) | // imm[20]
|
||||||
|
(((imm >> 1) & 0x3FF) << 21) | // imm[10:1]
|
||||||
|
(((imm >> 11) & 1) << 20) | // imm[11]
|
||||||
|
(((imm >> 12) & 0xFF) << 12) | // imm[19:12]
|
||||||
|
(uint32(rd) << 7) |
|
||||||
|
0x6F // JAL opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RVC (compressed) encoding helpers ----
|
||||||
|
|
||||||
|
// isRVCIntReg reports whether a register number can be encoded in the 3-bit
|
||||||
|
// prime register field used by compressed instructions (x8–x15).
|
||||||
|
func isRVCIntReg(r int) bool { return r >= 8 && r <= 15 }
|
||||||
|
|
||||||
|
// rvcReg3 returns the 3-bit encoding for registers x8–x15 (0–7).
|
||||||
|
func rvcReg3(r int) uint32 { return uint32(r - 8) }
|
||||||
|
|
||||||
|
// rvcCR encodes a CR-type (register) compressed instruction.
|
||||||
|
// Format: funct4 | rd/rs1 | rs2 | op=2.
|
||||||
|
func rvcCR(funct4, rd, rs2 uint32) uint16 {
|
||||||
|
return uint16((funct4 << 12) | (rd << 7) | (rs2 << 2) | 0x2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
packed := uint32(0)
|
||||||
|
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||||
|
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].
|
||||||
|
func rvcSSP(funct3, rs2 uint32, offset uint32) uint16 {
|
||||||
|
// Bit interleave offset bits [5,4,3,8,7,6] → packed value.
|
||||||
|
packed := uint32(0)
|
||||||
|
for i, b := range []int{5, 4, 3, 8, 7, 6} {
|
||||||
|
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].
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// rvcCS encodes a CS-type (store) compressed instruction.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rvcCA encodes a CA-type (arithmetic) compressed instruction.
|
||||||
|
// Format: funct6[15:10] | rd'/rs1'[9:7] | funct2[6:5] | rs2'[4:2] | op=01.
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// firstTextRISCV parses assembly source and returns the first TEXT function body.
|
||||||
|
func firstTextRISCV(t *testing.T, src string) *ast.Text {
|
||||||
|
t.Helper()
|
||||||
|
f, errs := parser.Parse("f_riscv64.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble: %v", err)
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_add(t *testing.T) {
|
||||||
|
// func add(a, b int64) int64
|
||||||
|
fn := firstTextRISCV(t, `#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
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
// should be 12 bytes with RVC: C.LDSP + C.LDSP + ADD + C.SDSP + C.JR
|
||||||
|
_ = code
|
||||||
|
if len(code) == 0 {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_arithmetic(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·arith(SB), NOSPLIT, $0
|
||||||
|
ADD X10, X11, X12
|
||||||
|
SUB X12, X13, X14
|
||||||
|
MUL X14, X15, X16
|
||||||
|
DIV X16, X17, X18
|
||||||
|
REM X18, X19, X20
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_loadStore(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·mem(SB), NOSPLIT, $0
|
||||||
|
LD (X10), X11
|
||||||
|
SD X11, (X12)
|
||||||
|
LW (X13), X14
|
||||||
|
SW X14, (X15)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_branches(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·br(SB), NOSPLIT, $0
|
||||||
|
ADDI X10, $1, X10
|
||||||
|
loop:
|
||||||
|
BEQ X10, X11, done
|
||||||
|
ADDI X10, $1, X10
|
||||||
|
JMP loop
|
||||||
|
done:
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
_ = code
|
||||||
|
if len(code) == 0 {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_MOV_imm_small(t *testing.T) {
|
||||||
|
// MOV $42, rd → ADDI (fits in 12 bits). Not RVC-compressed (treated as MOV, not ADDI).
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_MOV_imm_large(t *testing.T) {
|
||||||
|
// MOV $0x12345, rd → LUI + ADDIW (8 bytes total)
|
||||||
|
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
|
||||||
|
if len(code) != 10 {
|
||||||
|
t.Errorf("expected 10 bytes, got %d", len(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_MOV_reg(t *testing.T) {
|
||||||
|
// MOV rs, rd → ADDI $0, rs, rd, compresses to C.MV
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·reg(SB), NOSPLIT, $0
|
||||||
|
MOV X10, X11
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_MOV_frame(t *testing.T) {
|
||||||
|
// MOV name+off(FP), rd → load with frame mapping
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·frame(SB), NOSPLIT, $0-8
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
MOV X10, ret+0(FP)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_loadStore(t *testing.T) {
|
||||||
|
// Verify that loads/stores from SP are compressed.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·rvcstore(SB), NOSPLIT, $0
|
||||||
|
LD 0(SP), X10
|
||||||
|
SD X10, 8(SP)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_atomics(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·amo(SB), NOSPLIT, $0
|
||||||
|
AMOADDD X10, (X11), X12
|
||||||
|
LRD (X13), X14
|
||||||
|
SCD X15, (X16), X17
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_fpArith(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·fpadd(SB), NOSPLIT, $0
|
||||||
|
FADDD F10, F11, F12
|
||||||
|
FSUBD F12, F13, F14
|
||||||
|
FMULD F14, F15, F16
|
||||||
|
FDIVD F16, F17, F18
|
||||||
|
FSQRTD F18, F19
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_csr(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·csrtest(SB), NOSPLIT, $0
|
||||||
|
CSRRS $0x300, X0, X10
|
||||||
|
CSRRW $0x305, X10, X11
|
||||||
|
CSRRSI $0x304, $5, X12
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_fma(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·fmatest(SB), NOSPLIT, $0
|
||||||
|
FMADDD F10, F11, F12, F13
|
||||||
|
FMSUBD F13, F14, F15, F16
|
||||||
|
FNMSUBD F16, F17, F18, F19
|
||||||
|
FNMADDD F19, F10, F11, F12
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_conversions(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cvt(SB), NOSPLIT, $0
|
||||||
|
FCVTDL X10, F10
|
||||||
|
FCVTLD F10, X11
|
||||||
|
FMVXD F10, X12
|
||||||
|
FMVDX X12, F11
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_fpCmp(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cmp(SB), NOSPLIT, $0
|
||||||
|
FEQD F10, F11, X10
|
||||||
|
FLTD F12, F13, X11
|
||||||
|
FLED F14, F15, X12
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
BEQ X10, X11, done
|
||||||
|
ADDI X10, $1, X10
|
||||||
|
done:
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
_ = code
|
||||||
|
if len(code) == 0 {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_LUI(t *testing.T) {
|
||||||
|
// LUI rd, small nonzero imm → C.LUI
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·clui(SB), NOSPLIT, $0
|
||||||
|
LUI X10, $1
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_AssembleFile(t *testing.T) {
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·add(SB), NOSPLIT, $0-24
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
RET
|
||||||
|
|
||||||
|
TEXT ·sub(SB), NOSPLIT, $0
|
||||||
|
SUB X10, X11, X12
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||||
|
}
|
||||||
|
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 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 TestRISCV_encodings(t *testing.T) {
|
||||||
|
// Smoke test that all known RISC-V mnemonics encode successfully.
|
||||||
|
tests := []struct {
|
||||||
|
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},
|
||||||
|
{"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},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·`+tt.name+`(SB), NOSPLIT, $0
|
||||||
|
`+tt.src)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
if len(code) != tt.wantBytes {
|
||||||
|
t.Errorf("expected %d bytes, got %d", tt.wantBytes, len(code))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_branch(t *testing.T) {
|
||||||
|
// BEQ rs, X0, target → C.BEQZ when rs is in prime regs and offset fits.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cbeqz(SB), NOSPLIT, $0
|
||||||
|
ADDI X10, $1, X10
|
||||||
|
BEQ X10, X0, done
|
||||||
|
ADDI X10, $1, 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CJ(t *testing.T) {
|
||||||
|
// JMP target → C.J when offset fits.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cj(SB), NOSPLIT, $0
|
||||||
|
JMP done
|
||||||
|
done:
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CADD(t *testing.T) {
|
||||||
|
// ADD where rd==rs1 and both in prime regs → C.ADD.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cadd(SB), NOSPLIT, $0
|
||||||
|
ADD X10, X11, X10
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CADD_commute(t *testing.T) {
|
||||||
|
// ADD where rd==rs2 (commutative swap) → C.ADD.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cadd2(SB), NOSPLIT, $0
|
||||||
|
ADD X11, X10, X10
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CSUB(t *testing.T) {
|
||||||
|
// SUB where rd==rs1 and both in prime regs → C.SUB.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CXOR(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cxor(SB), NOSPLIT, $0
|
||||||
|
XOR X10, X11, X10
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
if len(code) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes with C.XOR, got %d", len(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_COR(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cor(SB), NOSPLIT, $0
|
||||||
|
OR X10, X11, X10
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
if len(code) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes with C.OR, got %d", len(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CAND(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cand(SB), NOSPLIT, $0
|
||||||
|
AND X10, X11, X10
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
if len(code) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes with C.AND, got %d", len(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CFLDSP(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cfldsp(SB), NOSPLIT, $0-8
|
||||||
|
FLD a+0(FP), F10
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_RVC_CFSDSP(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·cfsdsp(SB), NOSPLIT, $0-8
|
||||||
|
FSD F10, ret+0(FP)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_SB_addr(t *testing.T) {
|
||||||
|
// MOV $sym<>(SB), rd → AUIPC + ADDI (8 bytes for SB).
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
TEXT ·sbaddr(SB), NOSPLIT, $0
|
||||||
|
MOV $answer<>(SB), X10
|
||||||
|
RET
|
||||||
|
GLOBL answer<>(SB), RODATA, $8
|
||||||
|
DATA answer<>+0(SB)/8, $42
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_SB_store(t *testing.T) {
|
||||||
|
// MOV rd, sym<>(SB) → AUIPC + SD (8 bytes for SB).
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
TEXT ·sbstore(SB), NOSPLIT, $0
|
||||||
|
MOV X10, result<>(SB)
|
||||||
|
RET
|
||||||
|
GLOBL result<>(SB), NOPTR, $8
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_ELF(t *testing.T) {
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
TEXT ·simple(SB), NOSPLIT, $0
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||||
|
}
|
||||||
|
obj, err := img.ELFRISCVObject()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ELFRISCVObject: %v", err)
|
||||||
|
}
|
||||||
|
if len(obj) < 4 || obj[0] != 0x7f || obj[1] != 'E' || obj[2] != 'L' || obj[3] != 'F' {
|
||||||
|
t.Fatal("not a valid ELF file")
|
||||||
|
}
|
||||||
|
if len(obj) >= 20 {
|
||||||
|
machine := uint16(obj[18]) | uint16(obj[19])<<8
|
||||||
|
if machine != 243 {
|
||||||
|
t.Errorf("e_machine = %d, want 243 (EM_RISCV)", machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_ELF_withData(t *testing.T) {
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
TEXT ·get(SB), NOSPLIT, $0
|
||||||
|
RET
|
||||||
|
GLOBL val<>(SB), RODATA, $4
|
||||||
|
DATA val<>+0(SB)/4, $7
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||||
|
}
|
||||||
|
if len(img.DataSyms) != 1 {
|
||||||
|
t.Fatalf("expected 1 data symbol, got %d", len(img.DataSyms))
|
||||||
|
}
|
||||||
|
if img.DataSyms[0].Name != "val" {
|
||||||
|
t.Errorf("data symbol name = %q, want val", img.DataSyms[0].Name)
|
||||||
|
}
|
||||||
|
if img.DataSyms[0].Size != 4 {
|
||||||
|
t.Errorf("data symbol size = %d, want 4", img.DataSyms[0].Size)
|
||||||
|
}
|
||||||
|
obj, err := img.ELFRISCVObject()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ELFRISCVObject: %v", err)
|
||||||
|
}
|
||||||
|
_ = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_SB_load(t *testing.T) {
|
||||||
|
// MOV sym<>(SB), rd → AUIPC + LD (8 bytes for SB).
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
TEXT ·sbload(SB), NOSPLIT, $0
|
||||||
|
MOV answer<>(SB), X10
|
||||||
|
RET
|
||||||
|
GLOBL answer<>(SB), RODATA, $8
|
||||||
|
DATA answer<>+0(SB)/8, $42
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_riscv64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_system_instrs(t *testing.T) {
|
||||||
|
// Test FENCE, ECALL, EBREAK encoding.
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·sys(SB), NOSPLIT, $0
|
||||||
|
FENCE
|
||||||
|
ECALL
|
||||||
|
EBREAK
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
// 3 system instructions × 4 bytes + C.JR(2) = 14
|
||||||
|
if len(code) != 14 {
|
||||||
|
t.Errorf("expected 14 bytes, got %d (% x)", len(code), code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRISCV_MOV_sym_FP_error(t *testing.T) {
|
||||||
|
// MOV $sym(FP), rd should return an error (unsupported).
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·badfp(SB), NOSPLIT, $0
|
||||||
|
MOV $arg(FP), X10
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
_, _, _, 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).
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·calltest(SB), NOSPLIT, $0
|
||||||
|
CALL sub
|
||||||
|
done:
|
||||||
|
RET
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import "sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// riscvFrameInfo holds the frame parameters computed from a TEXT directive.
|
||||||
|
type riscvFrameInfo struct {
|
||||||
|
frameSize int // the $framesize from TEXT
|
||||||
|
argsSize int // the -argsize from TEXT
|
||||||
|
noSplit bool // the NOSPLIT flag
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvComputeFrame extracts frame information from a TEXT directive.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fi
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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); ...
|
||||||
|
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 {
|
||||||
|
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))...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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
|
||||||
|
case "SP":
|
||||||
|
// SP-relative; the offset is from the current SP.
|
||||||
|
return 2, offset
|
||||||
|
case "SB":
|
||||||
|
// Static data reference — needs a relocation (not yet supported).
|
||||||
|
return -1, offset
|
||||||
|
default:
|
||||||
|
return -1, 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)}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"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")
|
||||||
|
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 a zeroed argument block.
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cmdDebug(args []string) int {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm debug: the interactive debugger requires linux/amd64 (ptrace)")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
+354
-34
@@ -8,13 +8,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
@@ -29,7 +33,7 @@ import (
|
|||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version is the release version, stamped at build time via
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||||
var version = "0.22.0"
|
var version = "0.28.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -49,6 +53,8 @@ func main() {
|
|||||||
os.Exit(cmdAsm(os.Args[2:]))
|
os.Exit(cmdAsm(os.Args[2:]))
|
||||||
case "verify":
|
case "verify":
|
||||||
os.Exit(cmdVerify(os.Args[2:]))
|
os.Exit(cmdVerify(os.Args[2:]))
|
||||||
|
case "debug":
|
||||||
|
os.Exit(cmdDebug(os.Args[2:]))
|
||||||
case "lsp":
|
case "lsp":
|
||||||
os.Exit(cmdLSP(os.Args[2:]))
|
os.Exit(cmdLSP(os.Args[2:]))
|
||||||
case "version", "--version", "-V":
|
case "version", "--version", "-V":
|
||||||
@@ -67,39 +73,76 @@ func cmdVersion() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ANSI color helpers for terminal output.
|
||||||
|
const (
|
||||||
|
colorReset = "\033[0m"
|
||||||
|
colorBold = "\033[1m"
|
||||||
|
colorCyan = "\033[36m"
|
||||||
|
colorYellow = "\033[33m"
|
||||||
|
colorGray = "\033[90m"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isTTY reports whether the writer is a terminal (for color output).
|
||||||
|
func isTTY(w io.Writer) bool {
|
||||||
|
if f, ok := w.(*os.File); ok {
|
||||||
|
stat, _ := f.Stat()
|
||||||
|
return (stat.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
|
useColor := isTTY(w)
|
||||||
|
bold, cyan, yellow, gray, reset := "", "", "", "", ""
|
||||||
|
if useColor {
|
||||||
|
bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset
|
||||||
|
}
|
||||||
|
|
||||||
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
|
fmt.Fprintf(w, "%sgasm %s%s — developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset)
|
||||||
language server for Plan 9 assembly into one self-contained binary.
|
fmt.Fprintf(w, "gasm bundles a lexer, parser, formatter, linter, standalone assembler and\n")
|
||||||
|
fmt.Fprintf(w, "language server for Plan 9 assembly into one self-contained binary.\n\n")
|
||||||
|
|
||||||
Usage:
|
fmt.Fprintf(w, "%sUsage:%s\n", yellow, reset)
|
||||||
gasm <command> [arguments]
|
fmt.Fprintf(w, " gasm <command> [arguments]\n")
|
||||||
gasm [flags]
|
fmt.Fprintf(w, " gasm [flags]\n\n")
|
||||||
|
|
||||||
Commands:
|
fmt.Fprintf(w, "%sCommands:%s\n", yellow, reset)
|
||||||
tokens print the lexical token stream
|
commands := []struct{ name, desc string }{
|
||||||
parse parse and report syntax errors
|
{"tokens", "print the lexical token stream"},
|
||||||
fmt canonicalise formatting (gofmt for assembly)
|
{"parse", "parse and report syntax errors"},
|
||||||
lint run static checks
|
{"fmt", "canonicalise formatting (gofmt for assembly)"},
|
||||||
asm assemble .s files to machine code (amd64)
|
{"lint", "run static checks"},
|
||||||
verify JIT-assemble and run dynamic checks (amd64)
|
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
||||||
lsp run the language server over stdio
|
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
||||||
version print the version (same as --version)
|
{"debug", "interactive source-level debugger (amd64)"},
|
||||||
|
{"lsp", "run the language server over stdio"},
|
||||||
|
{"version", "print the version (same as --version)"},
|
||||||
|
}
|
||||||
|
for _, c := range commands {
|
||||||
|
fmt.Fprintf(w, " %s%-10s%s %s%s%s\n", cyan, c.name, reset, gray, c.desc, reset)
|
||||||
|
}
|
||||||
|
|
||||||
Flags:
|
fmt.Fprintf(w, "\n%sFlags:%s\n", yellow, reset)
|
||||||
-h, --help show this help
|
fmt.Fprintf(w, " %s-h, --help%s %sshow this help%s\n", cyan, reset, gray, reset)
|
||||||
-V, --version print the version
|
fmt.Fprintf(w, " %s-V, --version%s %sprint the version%s\n", cyan, reset, gray, reset)
|
||||||
|
|
||||||
Run "gasm <command> -h" for a command's usage and flags.
|
fmt.Fprintf(w, "\nRun \"gasm <command> -h\" for a command's usage and flags.\n\n")
|
||||||
|
|
||||||
Examples:
|
fmt.Fprintf(w, "%sExamples:%s\n", yellow, reset)
|
||||||
gasm fmt reformat every .s below the current directory
|
examples := []struct{ cmd, desc string }{
|
||||||
gasm lint go-flac/*.s run static checks over the kernels
|
{"gasm fmt", "reformat every .s below the current directory"},
|
||||||
gasm asm -o k.bin kern_amd64.s
|
{"gasm lint go-flac/*.s", "run static checks over the kernels"},
|
||||||
gasm asm --format elf -o k.o kern_amd64.s
|
{"gasm asm -o k.bin kern_amd64.s", ""},
|
||||||
gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s
|
{"gasm asm --format elf -o k.o kern_amd64.s", ""},
|
||||||
`, version)
|
{"gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s", ""},
|
||||||
|
}
|
||||||
|
for _, e := range examples {
|
||||||
|
if e.desc != "" {
|
||||||
|
fmt.Fprintf(w, " %s%s%s %s%s%s\n", cyan, e.cmd, reset, gray, e.desc, reset)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, " %s%s%s\n", cyan, e.cmd, reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
|
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
|
||||||
@@ -346,7 +389,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
|||||||
|
|
||||||
func cmdAsm(args []string) int {
|
func cmdAsm(args []string) int {
|
||||||
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
|
||||||
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
|
Assemble FILE (amd64 or riscv64) without the Go toolchain: every TEXT function is
|
||||||
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
|
||||||
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
|
||||||
resolved RIP-relative — and printed as a hex dump.
|
resolved RIP-relative — and printed as a hex dump.
|
||||||
@@ -368,8 +411,9 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
if arch.FromFilename(path) != arch.AMD64 {
|
targetArch := arch.FromFilename(path)
|
||||||
fmt.Fprintln(os.Stderr, "gasm asm: only amd64 is supported in this Phase 2 increment")
|
if targetArch != arch.AMD64 && targetArch != arch.RISCV {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm asm: only amd64 and riscv64 are supported")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
src, err := readSource(path)
|
src, err := readSource(path)
|
||||||
@@ -385,7 +429,12 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := asm.AssembleFile(f)
|
var img *asm.Image
|
||||||
|
if targetArch == arch.RISCV {
|
||||||
|
img, err = asm.AssembleFileRISCV(f)
|
||||||
|
} else {
|
||||||
|
img, err = asm.AssembleFile(f)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -446,7 +495,11 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
}
|
}
|
||||||
obj, kind = img.Bytes(), "raw image"
|
obj, kind = img.Bytes(), "raw image"
|
||||||
case "elf":
|
case "elf":
|
||||||
obj, err = img.ELFObject()
|
if targetArch == arch.RISCV {
|
||||||
|
obj, err = img.ELFRISCVObject()
|
||||||
|
} else {
|
||||||
|
obj, err = img.ELFObject()
|
||||||
|
}
|
||||||
kind = "ELF object"
|
kind = "ELF object"
|
||||||
case "macho":
|
case "macho":
|
||||||
obj, err = img.MachOObject()
|
obj, err = img.MachOObject()
|
||||||
@@ -471,6 +524,97 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
||||||
|
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
||||||
|
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, src)
|
||||||
|
for _, e := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
img, err := asm.AssembleFileRISCV(f)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if groundTruth {
|
||||||
|
gt, err := verify.GroundTruthRISCV(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
matched, total := 0, 0
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
goCode, ok := gt[fn.Name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
gasmCmp := make([]byte, len(gasmCode))
|
||||||
|
goCmp := make([]byte, len(goCode))
|
||||||
|
copy(gasmCmp, gasmCode)
|
||||||
|
copy(goCmp, goCode)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
||||||
|
gasmCmp[j] = 0
|
||||||
|
}
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
||||||
|
goCmp[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Equal(gasmCmp, goCmp) {
|
||||||
|
matched++
|
||||||
|
if len(fn.Relocs) > 0 {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
||||||
|
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
||||||
|
var gb, gs string
|
||||||
|
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
||||||
|
gb += fmt.Sprintf(" %02x", gasmCode[j])
|
||||||
|
}
|
||||||
|
for j := i; j < i+16 && j < len(goCode); j++ {
|
||||||
|
gs += fmt.Sprintf(" %02x", goCode[j])
|
||||||
|
}
|
||||||
|
fmt.Printf(" %04x: gasm:%s\n", i, gb)
|
||||||
|
fmt.Printf(" %04x: gt: %s\n", i, gs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
||||||
|
if matched < total {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile {
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func cmdVerify(args []string) int {
|
func cmdVerify(args []string) int {
|
||||||
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
||||||
Assemble FILE (amd64), map it into executable memory and report the available
|
Assemble FILE (amd64), map it into executable memory and report the available
|
||||||
@@ -490,17 +634,27 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
||||||
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
||||||
profile := fs.Bool("profile", false, "list basic-block structure per function")
|
profile := fs.Bool("profile", false, "list basic-block structure per function")
|
||||||
|
groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm")
|
||||||
|
fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
|
||||||
|
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
|
||||||
|
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
if arch.FromFilename(path) != arch.AMD64 {
|
targetArch := arch.FromFilename(path)
|
||||||
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported")
|
if targetArch != arch.AMD64 && targetArch != arch.RISCV {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 and riscv64 are supported")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
||||||
|
if targetArch == arch.RISCV {
|
||||||
|
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
||||||
|
}
|
||||||
|
|
||||||
k, err := verify.Load(path)
|
k, err := verify.Load(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
@@ -511,6 +665,135 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
names := k.FuncNames()
|
names := k.FuncNames()
|
||||||
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
||||||
rc := 0
|
rc := 0
|
||||||
|
|
||||||
|
// Subprocess mode: fuzz a single function and exit.
|
||||||
|
if *fuzzOne != "" {
|
||||||
|
gt, err := verify.GroundTruth(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
sigs := verify.ExtractSignatures(src)
|
||||||
|
sig, ok := sigs[*fuzzOne]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("%s: no signature\n", *fuzzOne)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
goCode, ok := gt[*fuzzOne]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("%s: not in go tool asm\n", *fuzzOne)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
res := k.FuzzFunc(*fuzzOne, sig, goCode, *fuzzN, 42)
|
||||||
|
fmt.Printf("%s\n", res)
|
||||||
|
if !res.OK() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ground-truth comparison: assemble with go tool asm and compare bytes.
|
||||||
|
if *groundTruth {
|
||||||
|
gt, err := verify.GroundTruth(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
matched, total := 0, 0
|
||||||
|
for _, name := range names {
|
||||||
|
fl, _ := k.Func(name)
|
||||||
|
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
|
||||||
|
goCode, ok := gt[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
// Compare, masking relocation sites (disp32 fields that the
|
||||||
|
// Go linker fills at link time — gasm resolves them internally).
|
||||||
|
gasmCmp := make([]byte, len(gasmCode))
|
||||||
|
goCmp := make([]byte, len(goCode))
|
||||||
|
copy(gasmCmp, gasmCode)
|
||||||
|
copy(goCmp, goCode)
|
||||||
|
for _, r := range fl.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
||||||
|
gasmCmp[j] = 0
|
||||||
|
}
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
||||||
|
goCmp[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Equal(gasmCmp, goCmp) {
|
||||||
|
matched++
|
||||||
|
if len(fl.Relocs) > 0 {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
|
||||||
|
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
|
||||||
|
if gasmCmp[i] != goCmp[i] {
|
||||||
|
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
|
||||||
|
if matched < total {
|
||||||
|
rc = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Differential fuzz: JIT both gasm and go-tool-asm, compare outputs.
|
||||||
|
// Each function runs in a subprocess so a crash (partial functions like
|
||||||
|
// decoders that fault on malformed input) doesn't kill the whole run.
|
||||||
|
if *fuzz {
|
||||||
|
gt, err := verify.GroundTruth(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: fuzz: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
sigs := verify.ExtractSignatures(src)
|
||||||
|
fuzzed := 0
|
||||||
|
for _, name := range names {
|
||||||
|
sig, ok := sigs[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
goCode, ok := gt[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Run in a subprocess: if the function crashes on random
|
||||||
|
// input (partial function), we report it and move on.
|
||||||
|
res := fuzzInSubprocess(path, name, *fuzzN)
|
||||||
|
if res != "" {
|
||||||
|
fmt.Printf(" %s\n", res)
|
||||||
|
if strings.Contains(res, "MISMATCH") {
|
||||||
|
rc = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = sig
|
||||||
|
_ = goCode
|
||||||
|
fuzzed++
|
||||||
|
}
|
||||||
|
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
|
||||||
|
}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
fl, _ := k.Func(name)
|
fl, _ := k.Func(name)
|
||||||
flags := ""
|
flags := ""
|
||||||
@@ -555,3 +838,40 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
}
|
}
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fuzzInSubprocess runs the fuzz for a single function in a child process.
|
||||||
|
// If the child is killed by a signal (e.g. SIGSEGV from a partial function
|
||||||
|
// faulting on random input), it returns a CRASH report instead of dying.
|
||||||
|
func fuzzInSubprocess(path, funcName string, n int) string {
|
||||||
|
self, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("%s: cannot find self: %v", funcName, err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(self, "verify", "--fuzz-one="+funcName, "-n", strconv.Itoa(n), path)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// Check if the child was killed by a signal.
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
ws := exitErr.Sys().(syscall.WaitStatus)
|
||||||
|
if ws.Signaled() {
|
||||||
|
return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-zero exit without a signal: the fuzz reported mismatches.
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.Contains(l, funcName) {
|
||||||
|
return strings.TrimSpace(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: FAIL (exit %v)", funcName, err)
|
||||||
|
}
|
||||||
|
// Success: extract the result line.
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.Contains(l, funcName) {
|
||||||
|
return strings.TrimSpace(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Breakpoint is one INT3 breakpoint in the debuggee.
|
||||||
|
type Breakpoint struct {
|
||||||
|
Addr uint64 // absolute address in the debuggee
|
||||||
|
Label string // source label ("" for raw addresses)
|
||||||
|
Orig byte // original byte at Addr (restored on removal)
|
||||||
|
Enabled bool
|
||||||
|
Cond *Condition // optional condition (nil = unconditional)
|
||||||
|
hits int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition is a simple register-comparison condition evaluated when a
|
||||||
|
// breakpoint is hit. Format: <reg> <op> <value>.
|
||||||
|
type Condition struct {
|
||||||
|
Reg string // register name (rax, rbx, rip, rsp, ...)
|
||||||
|
Op string // comparison operator: ==, !=, <, >, <=, >=
|
||||||
|
Value uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
return true // unknown register — don't block
|
||||||
|
}
|
||||||
|
switch c.Op {
|
||||||
|
case "==", "=":
|
||||||
|
return actual == c.Value
|
||||||
|
case "!=":
|
||||||
|
return actual != c.Value
|
||||||
|
case "<":
|
||||||
|
return actual < c.Value
|
||||||
|
case ">":
|
||||||
|
return actual > c.Value
|
||||||
|
case "<=":
|
||||||
|
return actual <= c.Value
|
||||||
|
case ">=":
|
||||||
|
return actual >= c.Value
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breakpoints manages the set of breakpoints for a Session.
|
||||||
|
// Breakpoints manages software breakpoints for a debuggee.
|
||||||
|
type Breakpoints struct {
|
||||||
|
t tracer
|
||||||
|
bps map[uint64]*Breakpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBreakpoints creates a new breakpoint manager.
|
||||||
|
func NewBreakpoints(t tracer) *Breakpoints {
|
||||||
|
return &Breakpoints{t: t, bps: make(map[uint64]*Breakpoint)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set installs a breakpoint at addr (replaces any existing one).
|
||||||
|
func (bm *Breakpoints) Set(addr uint64, label string) (*Breakpoint, error) {
|
||||||
|
return bm.SetWithCond(addr, label, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWithCond installs a breakpoint with an optional condition.
|
||||||
|
func (bm *Breakpoints) SetWithCond(addr uint64, label string, cond *Condition) (*Breakpoint, error) {
|
||||||
|
if bp, ok := bm.bps[addr]; ok {
|
||||||
|
bp.Enabled = true
|
||||||
|
bp.Cond = cond
|
||||||
|
return bp, nil
|
||||||
|
}
|
||||||
|
// Read the original byte.
|
||||||
|
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
|
||||||
|
if err := bm.t.Poke(addr, patched); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bp := &Breakpoint{Addr: addr, Label: label, Orig: orig, Enabled: true, Cond: cond}
|
||||||
|
bm.bps[addr] = bp
|
||||||
|
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 := ""
|
||||||
|
i := 0
|
||||||
|
for _, bp := range bm.bps {
|
||||||
|
i++
|
||||||
|
status := "enabled"
|
||||||
|
if !bp.Enabled {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
label := bp.Label
|
||||||
|
if label == "" {
|
||||||
|
label = fmt.Sprintf("%#x", bp.Addr)
|
||||||
|
}
|
||||||
|
cond := ""
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes the breakpoint at addr, restoring the original byte.
|
||||||
|
func (bm *Breakpoints) Clear(addr uint64) error {
|
||||||
|
bp, ok := bm.bps[addr]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("debug: no breakpoint at %#x", addr)
|
||||||
|
}
|
||||||
|
word, err := bm.t.Peek(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||||
|
if err := bm.t.Poke(addr, restored); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(bm.bps, addr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAll removes all breakpoints.
|
||||||
|
func (bm *Breakpoints) ClearAll() error {
|
||||||
|
for addr := range bm.bps {
|
||||||
|
if err := bm.Clear(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the breakpoint at addr, if any.
|
||||||
|
func (bm *Breakpoints) At(addr uint64) *Breakpoint {
|
||||||
|
return bm.bps[addr]
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns all breakpoints.
|
||||||
|
func (bm *Breakpoints) All() []*Breakpoint {
|
||||||
|
out := make([]*Breakpoint, 0, len(bm.bps))
|
||||||
|
for _, bp := range bm.bps {
|
||||||
|
out = append(out, bp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// returns the breakpoint that was hit (or nil if it was a single-step).
|
||||||
|
func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
||||||
|
// After INT3, RIP points to the byte AFTER the 0xCC.
|
||||||
|
trapAddr := regs.RIP - 1
|
||||||
|
bp, ok := bm.bps[trapAddr]
|
||||||
|
if !ok || !bp.Enabled {
|
||||||
|
return nil // single-step trap or unknown
|
||||||
|
}
|
||||||
|
// Check the condition (if any).
|
||||||
|
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||||
|
// Condition not met — restore the byte but do NOT rewind RIP.
|
||||||
|
// The process continues from the next instruction (past the INT3).
|
||||||
|
word, err := bm.t.Peek(trapAddr)
|
||||||
|
if err == nil {
|
||||||
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||||
|
bm.t.Poke(trapAddr, restored)
|
||||||
|
}
|
||||||
|
// RIP is already past the INT3 (trapAddr + 1). Don't rewind.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bp.hits++
|
||||||
|
// Restore the original byte.
|
||||||
|
word, err := bm.t.Peek(trapAddr)
|
||||||
|
if err == nil {
|
||||||
|
restored := (word &^ 0xFF) | uint64(bp.Orig)
|
||||||
|
bm.t.Poke(trapAddr, restored)
|
||||||
|
}
|
||||||
|
// Rewind RIP to re-execute the original instruction.
|
||||||
|
regs.RIP = trapAddr
|
||||||
|
bm.t.SetRegs(regs)
|
||||||
|
return bp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinsert re-inserts the breakpoint at addr after a single-step past it.
|
||||||
|
// Called after Step() when we want the breakpoint to fire again on the
|
||||||
|
// next Continue().
|
||||||
|
func (bm *Breakpoints) Reinsert(addr uint64) error {
|
||||||
|
bp, ok := bm.bps[addr]
|
||||||
|
if !ok || !bp.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
word, err := bm.t.Peek(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
patched := (word &^ 0xFF) | 0xCC
|
||||||
|
return bm.t.Poke(addr, patched)
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConditionEval(t *testing.T) {
|
||||||
|
regs := &Regs{
|
||||||
|
RAX: 42,
|
||||||
|
RBX: 0,
|
||||||
|
RCX: 100,
|
||||||
|
RIP: 0x1000,
|
||||||
|
RSP: 0x2000,
|
||||||
|
R8: 8,
|
||||||
|
R15: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
cond Condition
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{Condition{Reg: "rax", Op: "==", Value: 42}, true},
|
||||||
|
{Condition{Reg: "rax", Op: "==", Value: 43}, false},
|
||||||
|
{Condition{Reg: "rax", Op: "!=", Value: 43}, true},
|
||||||
|
{Condition{Reg: "rax", Op: "!=", Value: 42}, false},
|
||||||
|
{Condition{Reg: "rax", Op: "<", Value: 50}, true},
|
||||||
|
{Condition{Reg: "rax", Op: "<", Value: 40}, false},
|
||||||
|
{Condition{Reg: "rax", Op: ">", Value: 40}, true},
|
||||||
|
{Condition{Reg: "rax", Op: ">", Value: 50}, false},
|
||||||
|
{Condition{Reg: "rax", Op: "<=", Value: 42}, true},
|
||||||
|
{Condition{Reg: "rax", Op: ">=", Value: 42}, true},
|
||||||
|
{Condition{Reg: "rbx", Op: "==", Value: 0}, true},
|
||||||
|
{Condition{Reg: "rcx", Op: ">", Value: 50}, true},
|
||||||
|
{Condition{Reg: "rip", Op: "==", Value: 0x1000}, true},
|
||||||
|
{Condition{Reg: "rsp", Op: ">", Value: 0x1000}, true},
|
||||||
|
{Condition{Reg: "r8", Op: "==", Value: 8}, true},
|
||||||
|
{Condition{Reg: "r15", Op: "==", Value: 15}, true},
|
||||||
|
{Condition{Reg: "eax", Op: "==", Value: 42}, true}, // 32-bit alias
|
||||||
|
{Condition{Reg: "ax", Op: "==", Value: 42}, true}, // 16-bit alias
|
||||||
|
{Condition{Reg: "unknown", Op: "==", Value: 0}, true}, // unknown reg → don't block
|
||||||
|
{Condition{Reg: "rax", Op: "??", Value: 0}, true}, // unknown op → don't block
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := tt.cond.Eval(regs)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Condition{%q %q %d}.Eval() = %v, want %v",
|
||||||
|
tt.cond.Reg, tt.cond.Op, tt.cond.Value, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLineAt(t *testing.T) {
|
||||||
|
lines := []SourceLine{
|
||||||
|
{Offset: 0, Line: 5},
|
||||||
|
{Offset: 5, Line: 6},
|
||||||
|
{Offset: 10, Line: 7},
|
||||||
|
{Offset: 15, Line: 8},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
offset int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{0, 5},
|
||||||
|
{1, 5},
|
||||||
|
{4, 5},
|
||||||
|
{5, 6},
|
||||||
|
{7, 6},
|
||||||
|
{10, 7},
|
||||||
|
{12, 7},
|
||||||
|
{15, 8},
|
||||||
|
{20, 8},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := lineAt(lines, tt.offset)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("lineAt(lines, %d) = %d, want %d", tt.offset, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty table.
|
||||||
|
if lineAt(nil, 5) != 0 {
|
||||||
|
t.Error("lineAt(nil, 5) should return 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOffsetForLine(t *testing.T) {
|
||||||
|
lines := []SourceLine{
|
||||||
|
{Offset: 0, Line: 5},
|
||||||
|
{Offset: 5, Line: 6},
|
||||||
|
{Offset: 10, Line: 7},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
line int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{5, 0},
|
||||||
|
{6, 5},
|
||||||
|
{7, 10},
|
||||||
|
{99, -1}, // not found
|
||||||
|
{0, -1}, // not found
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := offsetForLine(lines, tt.line)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("offsetForLine(lines, %d) = %d, want %d", tt.line, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeRflags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
flags uint64
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0x202, "IF"}, // only IF set (bit 9)
|
||||||
|
{0x246, "PF ZF IF"}, // PF(2) + ZF(6) + IF(9)
|
||||||
|
{0x001, "CF"}, // carry flag
|
||||||
|
{0x080, "SF"}, // sign flag
|
||||||
|
{0x800, "OF"}, // overflow flag
|
||||||
|
{0x000, "none"}, // no flags
|
||||||
|
{0x202 | 0x001, "CF IF"}, // CF + IF
|
||||||
|
{0x3F7, "CF PF AF ZF SF TF IF"}, // all arithmetic flags
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := decodeRflags(tt.flags)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("decodeRflags(%#x) = %q, want %q", tt.flags, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNearestLabel(t *testing.T) {
|
||||||
|
labels := []Label{
|
||||||
|
{Name: "start", Offset: 0},
|
||||||
|
{Name: "loop", Offset: 10},
|
||||||
|
{Name: "done", Offset: 20},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
offset int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{0, "start"},
|
||||||
|
{5, "start"},
|
||||||
|
{10, "loop"},
|
||||||
|
{15, "loop"},
|
||||||
|
{20, "done"},
|
||||||
|
{25, "done"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := nearestLabel(labels, tt.offset)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("nearestLabel(labels, %d) = %q, want %q", tt.offset, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBreakpointsSetAndClear(t *testing.T) {
|
||||||
|
tr := newMockTracer()
|
||||||
|
bm := NewBreakpoints(tr)
|
||||||
|
|
||||||
|
// Set a breakpoint at address 0x1000.
|
||||||
|
bp, err := bm.Set(0x1000, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Set: %v", err)
|
||||||
|
}
|
||||||
|
if !bp.Enabled {
|
||||||
|
t.Error("breakpoint not enabled")
|
||||||
|
}
|
||||||
|
if bp.Label != "test" {
|
||||||
|
t.Errorf("label = %q, want test", bp.Label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Peek was called.
|
||||||
|
if len(tr.peeks) != 1 || tr.peeks[0] != 0x1000 {
|
||||||
|
t.Errorf("peeks = %v, want [0x1000]", tr.peeks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Poke wrote INT3.
|
||||||
|
if len(tr.pokes) != 1 || tr.pokes[0].addr != 0x1000 {
|
||||||
|
t.Errorf("pokes = %v", tr.pokes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At should find it.
|
||||||
|
if bm.At(0x1000) == nil {
|
||||||
|
t.Error("At(0x1000) returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should return it.
|
||||||
|
all := bm.All()
|
||||||
|
if len(all) != 1 {
|
||||||
|
t.Errorf("All() = %d breakpoints, want 1", len(all))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear it.
|
||||||
|
if err := bm.Clear(0x1000); err != nil {
|
||||||
|
t.Fatalf("Clear: %v", err)
|
||||||
|
}
|
||||||
|
if bm.At(0x1000) != nil {
|
||||||
|
t.Error("At(0x1000) after Clear should be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBreakpointsSetWithCond(t *testing.T) {
|
||||||
|
tr := newMockTracer()
|
||||||
|
bm := NewBreakpoints(tr)
|
||||||
|
|
||||||
|
cond := &Condition{Reg: "rax", Op: "==", Value: 42}
|
||||||
|
bp, err := bm.SetWithCond(0x2000, "cond_test", cond)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetWithCond: %v", err)
|
||||||
|
}
|
||||||
|
if bp.Cond == nil || bp.Cond.Value != 42 {
|
||||||
|
t.Error("condition not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-setting the same address should update the condition.
|
||||||
|
cond2 := &Condition{Reg: "rbx", Op: "<", Value: 100}
|
||||||
|
bp2, err := bm.SetWithCond(0x2000, "cond_test2", cond2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetWithCond (update): %v", err)
|
||||||
|
}
|
||||||
|
if bp2.Cond.Value != 100 {
|
||||||
|
t.Error("condition not updated")
|
||||||
|
}
|
||||||
|
// Should have only 1 Peek (first Set), second is update (no Peek needed).
|
||||||
|
if len(tr.peeks) != 1 {
|
||||||
|
t.Errorf("expected 1 Peek, got %d", len(tr.peeks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBreakpointsClearAll(t *testing.T) {
|
||||||
|
tr := newMockTracer()
|
||||||
|
bm := NewBreakpoints(tr)
|
||||||
|
|
||||||
|
bm.Set(0x1000, "a")
|
||||||
|
bm.Set(0x2000, "b")
|
||||||
|
bm.Set(0x3000, "c")
|
||||||
|
|
||||||
|
if len(bm.All()) != 3 {
|
||||||
|
t.Fatalf("expected 3 breakpoints, got %d", len(bm.All()))
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.ClearAll()
|
||||||
|
if len(bm.All()) != 0 {
|
||||||
|
t.Errorf("ClearAll: expected 0 breakpoints, got %d", len(bm.All()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBreakpointInfo(t *testing.T) {
|
||||||
|
tr := newMockTracer()
|
||||||
|
bm := NewBreakpoints(tr)
|
||||||
|
bm.Set(0x4000, "info_test")
|
||||||
|
|
||||||
|
info := bm.Info()
|
||||||
|
if info == "" {
|
||||||
|
t.Error("Info returned empty string")
|
||||||
|
}
|
||||||
|
if !strings.Contains(info, "info_test") {
|
||||||
|
t.Errorf("Info %q does not contain label", info)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Read up to 15 bytes (max x86 instruction length).
|
||||||
|
mem, err := s.ReadMemory(addr, 15)
|
||||||
|
if err != nil {
|
||||||
|
// Try a shorter read if we're near a page boundary.
|
||||||
|
mem, err = s.ReadMemory(addr, 1)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inst, err := x86asm.Decode(mem, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "???", 1, nil
|
||||||
|
}
|
||||||
|
text := x86asm.IntelSyntax(inst, addr, nil)
|
||||||
|
return text, inst.Len, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||||
|
// them as a formatted string with addresses and byte offsets.
|
||||||
|
func (s *Session) DisassembleN(addr uint64, n int) string {
|
||||||
|
var result string
|
||||||
|
pc := addr
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
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 = 1
|
||||||
|
}
|
||||||
|
pc += uint64(length)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//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) {
|
||||||
|
self, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return 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, 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, fmt.Errorf("debug: write args: %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, 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, fmt.Errorf("debug: wait for debuggee: %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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, 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
|
||||||
|
_, _, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
+638
@@ -0,0 +1,638 @@
|
|||||||
|
// 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 (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Label is a named address within the debugged function.
|
||||||
|
type Label struct {
|
||||||
|
Name string
|
||||||
|
Offset int // function-relative offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceLine maps a byte offset to a source line number.
|
||||||
|
type SourceLine struct {
|
||||||
|
Offset int
|
||||||
|
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) {
|
||||||
|
entryAddr := codeBase + uint64(funcOffset)
|
||||||
|
|
||||||
|
// Run to the function entry.
|
||||||
|
bp, err := bm.Set(entryAddr, "(entry)")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("warning: cannot set entry breakpoint: %v\n", err)
|
||||||
|
} else {
|
||||||
|
if err := s.Continue(); err != nil {
|
||||||
|
fmt.Printf("warning: continue to entry: %v\n", err)
|
||||||
|
}
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
bm.HandleTrap(®s)
|
||||||
|
// Remove the temporary entry breakpoint.
|
||||||
|
bm.Clear(entryAddr)
|
||||||
|
_ = bp
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Print("(gasm) ")
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
cmd := parts[0]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "q", "quit":
|
||||||
|
s.Kill()
|
||||||
|
return
|
||||||
|
|
||||||
|
case "regs":
|
||||||
|
regs, err := s.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
printRegs(®s, codeBase, uint64(funcOffset))
|
||||||
|
|
||||||
|
case "step", "s":
|
||||||
|
n := 1
|
||||||
|
if len(parts) > 1 {
|
||||||
|
n, _ = strconv.Atoi(parts[1])
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if s.Exited() {
|
||||||
|
fmt.Println("debuggee exited")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := s.Step(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := s.Continue(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
bm.Clear(afterAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bm.HandleTrap(®s)
|
||||||
|
bm.Clear(afterAddr)
|
||||||
|
_ = bp
|
||||||
|
} else {
|
||||||
|
// Not a CALL — just single-step.
|
||||||
|
if err := s.Step(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := s.Continue(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
bm.Clear(retAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !s.Exited() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "continue", "c":
|
||||||
|
if s.Exited() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if err := s.Continue(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if s.Exited() {
|
||||||
|
fmt.Println("debuggee exited")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Check for watchpoint hits.
|
||||||
|
reason, wpAddr := s.StopInfo()
|
||||||
|
if reason == StopWatchpoint {
|
||||||
|
fmt.Printf("watchpoint hit at %#x\n", wpAddr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
if bp := bm.HandleTrap(®s); bp != nil {
|
||||||
|
name := bp.Label
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprintf("%#x", bp.Addr)
|
||||||
|
}
|
||||||
|
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":
|
||||||
|
if len(parts) < 2 {
|
||||||
|
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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr = codeBase + uint64(funcOffset) + uint64(off)
|
||||||
|
label = fmt.Sprintf("line %d", lineNum)
|
||||||
|
} else {
|
||||||
|
addr, label = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
|
}
|
||||||
|
if addr == 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bp, err := bm.SetWithCond(addr, label, cond)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
condStr := ""
|
||||||
|
if cond != nil {
|
||||||
|
condStr = fmt.Sprintf(" if %s %s %#x", cond.Reg, cond.Op, cond.Value)
|
||||||
|
}
|
||||||
|
fmt.Printf("breakpoint set: %s at %#x (func+%#x)%s\n", bp.Label, bp.Addr, bp.Addr-codeBase-uint64(funcOffset), condStr)
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
if len(parts) < 2 {
|
||||||
|
fmt.Println("usage: info break")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch parts[1] {
|
||||||
|
case "break", "breakpoints", "b":
|
||||||
|
fmt.Print(bm.Info())
|
||||||
|
default:
|
||||||
|
fmt.Printf("unknown info target: %s\n", parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete", "d":
|
||||||
|
if len(parts) < 2 {
|
||||||
|
fmt.Println("usage: delete <label|addr>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
|
if addr == 0 {
|
||||||
|
fmt.Printf("unknown: %s\n", parts[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := bm.Clear(addr); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("breakpoint removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "x":
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
addr := regs.RIP // default: current PC
|
||||||
|
length := 64
|
||||||
|
if len(parts) > 1 {
|
||||||
|
addr, _ = resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
|
}
|
||||||
|
if len(parts) > 2 {
|
||||||
|
length, _ = strconv.Atoi(parts[2])
|
||||||
|
}
|
||||||
|
mem, err := s.ReadMemory(addr, length)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hexDump(addr, mem)
|
||||||
|
|
||||||
|
case "w":
|
||||||
|
if len(parts) < 3 {
|
||||||
|
fmt.Println("usage: w <addr> <byte|0x...> [byte...]")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
|
if addr == 0 {
|
||||||
|
fmt.Printf("unknown address: %s\n", parts[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var bytes []byte
|
||||||
|
for _, arg := range parts[2:] {
|
||||||
|
v, err := strconv.ParseUint(arg, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
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++ {
|
||||||
|
bytes = append(bytes, byte(v>>(8*j)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bytes = append(bytes, byte(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(bytes) > 0 {
|
||||||
|
if err := s.WriteMemory(addr, bytes); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("wrote %d bytes at %#x\n", len(bytes), addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "set":
|
||||||
|
if len(parts) < 3 {
|
||||||
|
fmt.Println("usage: set <reg> <value>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseUint(parts[2], 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("invalid value: %s\n", parts[2])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.SetReg(strings.ToLower(parts[1]), val); err != nil {
|
||||||
|
fmt.Printf("set: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s = %#x\n", parts[1], val)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "labels", "l":
|
||||||
|
sorted := make([]Label, len(labels))
|
||||||
|
copy(sorted, labels)
|
||||||
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Offset < sorted[j].Offset })
|
||||||
|
for _, l := range sorted {
|
||||||
|
fmt.Printf(" func+%#04x %s\n", l.Offset, l.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "disas", "u":
|
||||||
|
n := 5
|
||||||
|
if len(parts) > 1 {
|
||||||
|
n, _ = strconv.Atoi(parts[1])
|
||||||
|
if n <= 0 {
|
||||||
|
n = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
fmt.Print(s.DisassembleN(regs.RIP, n))
|
||||||
|
|
||||||
|
case "where":
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||||
|
line := lineAt(lines, funcOff)
|
||||||
|
label := nearestLabel(labels, funcOff)
|
||||||
|
fmt.Printf(" func+%#x", funcOff)
|
||||||
|
if label != "" {
|
||||||
|
fmt.Printf(" (near %s)", label)
|
||||||
|
}
|
||||||
|
if line > 0 {
|
||||||
|
fmt.Printf(" line %d", line)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
case "help", "h", "?":
|
||||||
|
fmt.Println(` 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
|
||||||
|
step [n], s single-step n instructions
|
||||||
|
next, n step over CALL
|
||||||
|
continue, c run until breakpoint or exit
|
||||||
|
disas [n], u disassemble n instructions at PC
|
||||||
|
regs print registers and RFLAGS
|
||||||
|
where show source line and nearest label
|
||||||
|
stack show stack near RSP (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`)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if argsSize > 0 {
|
||||||
|
fmt.Printf(" args (%d bytes at RSP+8):\n", argsSize)
|
||||||
|
argBytes, err := s.ReadMemory(regs.RSP+8, argsSize)
|
||||||
|
if err == nil {
|
||||||
|
for i := 0; i < argsSize; i += 8 {
|
||||||
|
var v uint64
|
||||||
|
for j := 0; j < 8 && i+j < len(argBytes); j++ {
|
||||||
|
v |= uint64(argBytes[i+j]) << (8 * j)
|
||||||
|
}
|
||||||
|
fmt.Printf(" [%+3d] %#016x\n", i+8, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "bt", "backtrace":
|
||||||
|
regs, _ := s.GetRegs()
|
||||||
|
funcOff := int(regs.RIP - codeBase - uint64(funcOffset))
|
||||||
|
line := lineAt(lines, funcOff)
|
||||||
|
label := nearestLabel(labels, funcOff)
|
||||||
|
fmt.Printf(" #0 func+%#x", funcOff)
|
||||||
|
if label != "" {
|
||||||
|
fmt.Printf(" (%s)", label)
|
||||||
|
}
|
||||||
|
if line > 0 {
|
||||||
|
fmt.Printf(" [line %d]", line)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
retAddr, _ := s.Peek(regs.RSP)
|
||||||
|
fmt.Printf(" #1 return to %#x\n", retAddr)
|
||||||
|
|
||||||
|
case "watch":
|
||||||
|
if len(parts) < 2 {
|
||||||
|
fmt.Println("usage: watch <addr> [r|w] [size]")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, _ := resolveAddr(parts[1], codeBase, uint64(funcOffset), labels)
|
||||||
|
if addr == 0 {
|
||||||
|
fmt.Printf("unknown address: %s\n", parts[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ := WatchWrite
|
||||||
|
size := 8
|
||||||
|
if len(parts) > 2 {
|
||||||
|
switch parts[2] {
|
||||||
|
case "r":
|
||||||
|
typ = WatchRead
|
||||||
|
case "w":
|
||||||
|
typ = WatchWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if slot < 0 {
|
||||||
|
fmt.Println("no free watchpoint slots")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "unwatch":
|
||||||
|
if err := s.ClearAllWatchpoints(); err != nil {
|
||||||
|
fmt.Printf("unwatch: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("all watchpoints cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Printf("unknown command: %s\n", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
fmt.Printf(" %#08x:", addr+uint64(i))
|
||||||
|
for j := i; j < i+16; j++ {
|
||||||
|
if j < end {
|
||||||
|
fmt.Printf(" %02x", data[j])
|
||||||
|
} else {
|
||||||
|
fmt.Print(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(" ")
|
||||||
|
for j := i; j < end; j++ {
|
||||||
|
if data[j] >= 0x20 && data[j] < 0x7f {
|
||||||
|
fmt.Printf("%c", data[j])
|
||||||
|
} else {
|
||||||
|
fmt.Print(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
lo, hi := 0, len(lines)-1
|
||||||
|
for lo < hi {
|
||||||
|
mid := (lo + hi + 1) / 2
|
||||||
|
if lines[mid].Offset <= offset {
|
||||||
|
lo = mid
|
||||||
|
} else {
|
||||||
|
hi = mid - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lines[lo].Offset <= offset {
|
||||||
|
return lines[lo].Line
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return le.Offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
for _, l := range labels {
|
||||||
|
if l.Offset <= offset && l.Offset > bestOff {
|
||||||
|
best = l.Name
|
||||||
|
bestOff = l.Offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// 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"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReg modifies a register value in the debuggee.
|
||||||
|
func (s *Session) SetReg(name string, value uint64) error {
|
||||||
|
regs, err := s.GetRegs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "rax", "eax", "ax", "al":
|
||||||
|
regs.RAX = value
|
||||||
|
case "rbx", "ebx", "bx", "bl":
|
||||||
|
regs.RBX = value
|
||||||
|
case "rcx", "ecx", "cx", "cl":
|
||||||
|
regs.RCX = value
|
||||||
|
case "rdx", "edx", "dx", "dl":
|
||||||
|
regs.RDX = value
|
||||||
|
case "rsi", "esi", "si":
|
||||||
|
regs.RSI = value
|
||||||
|
case "rdi", "edi", "di":
|
||||||
|
regs.RDI = value
|
||||||
|
case "rbp", "ebp", "bp":
|
||||||
|
regs.RBP = value
|
||||||
|
case "rsp", "esp", "sp":
|
||||||
|
regs.RSP = value
|
||||||
|
case "r8":
|
||||||
|
regs.R8 = value
|
||||||
|
case "r9":
|
||||||
|
regs.R9 = value
|
||||||
|
case "r10":
|
||||||
|
regs.R10 = value
|
||||||
|
case "r11":
|
||||||
|
regs.R11 = value
|
||||||
|
case "r12":
|
||||||
|
regs.R12 = value
|
||||||
|
case "r13":
|
||||||
|
regs.R13 = value
|
||||||
|
case "r14":
|
||||||
|
regs.R14 = value
|
||||||
|
case "r15":
|
||||||
|
regs.R15 = value
|
||||||
|
case "rip", "eip":
|
||||||
|
regs.RIP = value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: unknown register %q", name)
|
||||||
|
}
|
||||||
|
return s.SetRegs(®s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// 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"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunTarget is the debuggee entry point (gasm debug --target). It
|
||||||
|
// assembles the file, maps the JIT code, registers itself for ptrace,
|
||||||
|
// stops, and then executes the named function. The parent debugger
|
||||||
|
// controls execution from there.
|
||||||
|
func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
||||||
|
// Parse and assemble.
|
||||||
|
src, err := os.ReadFile(asmPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: %w", err)
|
||||||
|
}
|
||||||
|
file, errs := parser.Parse(asmPath, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return fmt.Errorf("debug target: parse: %v", errs[0])
|
||||||
|
}
|
||||||
|
img, err := asm.AssembleFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: assemble: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the function.
|
||||||
|
var fl *asm.FuncLayout
|
||||||
|
for i := range img.Funcs {
|
||||||
|
if img.Funcs[i].Name == funcName {
|
||||||
|
fl = &img.Funcs[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fl == nil {
|
||||||
|
return fmt.Errorf("debug target: function %q not found", funcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the entire image RWX (we need write access for breakpoints).
|
||||||
|
code := img.Bytes()
|
||||||
|
exec, err := mapRWX(code)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: mmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the code base address for the parent.
|
||||||
|
codeBase := uintptr(unsafe.Pointer(&exec[0]))
|
||||||
|
if err := os.WriteFile(tmpDir+"/codebase", []byte(fmt.Sprintf("%d", codeBase)), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("debug target: write codebase: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write function metadata (offset, size, args) for the parent.
|
||||||
|
meta := fmt.Sprintf("%d %d %d", fl.Offset, fl.Size, fl.Args)
|
||||||
|
os.WriteFile(tmpDir+"/funcmeta", []byte(meta), 0o644)
|
||||||
|
|
||||||
|
// Write label table for breakpoint resolution.
|
||||||
|
labelsFile, _ := os.Create(tmpDir + "/labels")
|
||||||
|
if labelsFile != nil {
|
||||||
|
for label, off := range fl.Labels {
|
||||||
|
fmt.Fprintf(labelsFile, "%s %d\n", label, off)
|
||||||
|
}
|
||||||
|
labelsFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the argument block.
|
||||||
|
args, err := os.ReadFile(argsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: read args: %w", err)
|
||||||
|
}
|
||||||
|
if len(args) < fl.Args {
|
||||||
|
padded := make([]byte, fl.Args)
|
||||||
|
copy(padded, args)
|
||||||
|
args = padded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock this goroutine to the current OS thread so the parent's
|
||||||
|
// ptrace (attached to this thread) controls the JIT execution.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
// Request tracing by the parent, then stop. PTRACE_TRACEME makes
|
||||||
|
// the subsequent SIGSTOP a ptrace-stop (not a group-stop), giving
|
||||||
|
// the parent full control from the start.
|
||||||
|
if _, _, errno := syscall.Syscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0); errno != 0 {
|
||||||
|
return fmt.Errorf("debug target: PTRACE_TRACEME: %v", errno)
|
||||||
|
}
|
||||||
|
os.WriteFile(tmpDir+"/ready", []byte("ok"), 0o644)
|
||||||
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
|
// --- Execution resumes here after the parent continues us ---
|
||||||
|
|
||||||
|
// Prepare the ABI0 stack and call the function.
|
||||||
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
|
stackArgs := make([]byte, fl.Args)
|
||||||
|
copy(stackArgs, args)
|
||||||
|
|
||||||
|
_, callErr := verify.Call(fnAddr, stackArgs)
|
||||||
|
if callErr != nil {
|
||||||
|
// The function returned an error (shouldn't happen for valid code).
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapRWX maps code into a read-write-execute region (needed for
|
||||||
|
// breakpoint patching via ptrace POKETEXT, though ptrace can write
|
||||||
|
// to any mapping regardless of permissions).
|
||||||
|
func mapRWX(code []byte) ([]byte, error) {
|
||||||
|
const pageSize = 4096
|
||||||
|
size := (len(code) + pageSize - 1) &^ (pageSize - 1)
|
||||||
|
mem, err := syscall.Mmap(-1, 0, size,
|
||||||
|
syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC,
|
||||||
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copy(mem, code)
|
||||||
|
return mem, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// tracer abstracts the minimal ptrace operations needed by the breakpoint
|
||||||
|
// manager and the stop-information helpers. The live implementation is
|
||||||
|
// *Session (ptrace_linux_amd64.go); tests supply a mock.
|
||||||
|
type tracer interface {
|
||||||
|
Peek(addr uint64) (uint64, error)
|
||||||
|
Poke(addr uint64, val uint64) error
|
||||||
|
SetRegs(regs *Regs) error
|
||||||
|
Pid() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockTracer records Peek/Poke calls and provides fake register state.
|
||||||
|
type mockTracer struct {
|
||||||
|
mem map[uint64]byte
|
||||||
|
peeks []uint64
|
||||||
|
pokes []struct {
|
||||||
|
addr uint64
|
||||||
|
val uint64
|
||||||
|
}
|
||||||
|
regs *Regs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockTracer() *mockTracer {
|
||||||
|
return &mockTracer{
|
||||||
|
mem: make(map[uint64]byte),
|
||||||
|
regs: &Regs{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTracer) Peek(addr uint64) (uint64, error) {
|
||||||
|
m.peeks = append(m.peeks, addr)
|
||||||
|
var val uint64
|
||||||
|
for i := uint64(0); i < 8; i++ {
|
||||||
|
val |= uint64(m.mem[addr+i]) << (i * 8)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTracer) Poke(addr uint64, val uint64) error {
|
||||||
|
m.pokes = append(m.pokes, struct {
|
||||||
|
addr uint64
|
||||||
|
val uint64
|
||||||
|
}{addr, val})
|
||||||
|
for i := uint64(0); i < 8; i++ {
|
||||||
|
m.mem[addr+i] = byte(val >> (i * 8))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTracer) SetRegs(regs *Regs) error {
|
||||||
|
m.regs = regs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockTracer) Pid() int { return 42 }
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// 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"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hardware watchpoint support via x86-64 debug registers (DR0-DR3, DR7).
|
||||||
|
//
|
||||||
|
// DR0-DR3 hold the watched addresses. DR7 is the control register:
|
||||||
|
// bits 0,2,4,6: local enable for DR0-DR3
|
||||||
|
// bits 16-17,20-21,24-25,28-29: R/W type (00=exec, 01=write, 11=read/write)
|
||||||
|
// bits 18-19,22-23,26-27,30-31: length (00=1, 01=2, 10=8, 11=4)
|
||||||
|
|
||||||
|
// WatchpointType selects what triggers the watchpoint.
|
||||||
|
type WatchpointType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchWrite WatchpointType = 1 // trigger on write
|
||||||
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
|
// slot is 0-3 (four hardware watchpoints available).
|
||||||
|
func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size int) error {
|
||||||
|
if slot < 0 || slot > 3 {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the length encoding.
|
||||||
|
var lenBits uint64
|
||||||
|
switch size {
|
||||||
|
case 1:
|
||||||
|
lenBits = 0
|
||||||
|
case 2:
|
||||||
|
lenBits = 1
|
||||||
|
case 4:
|
||||||
|
lenBits = 3
|
||||||
|
case 8:
|
||||||
|
lenBits = 2
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("debug: watchpoint size must be 1, 2, 4, or 8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the watched address to DR0-DR3.
|
||||||
|
var drAddr uintptr
|
||||||
|
switch slot {
|
||||||
|
case 0:
|
||||||
|
drAddr = 0x0 // DR0 offset in user_regs_struct
|
||||||
|
case 1:
|
||||||
|
drAddr = 0x8 // DR1
|
||||||
|
case 2:
|
||||||
|
drAddr = 0x10 // DR2
|
||||||
|
case 3:
|
||||||
|
drAddr = 0x18 // DR3
|
||||||
|
}
|
||||||
|
|
||||||
|
// PTRACE_POKEUSER writes to the debuggee's user area (includes debug regs).
|
||||||
|
if err := ptracePokeUser(s.pid, drAddr, addr); err != nil {
|
||||||
|
return fmt.Errorf("debug: set DR%d: %w", slot, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the current DR7, set the enable and type bits, write it back.
|
||||||
|
dr7, err := ptracePeekUser(s.pid, 0x38) // DR7 offset
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug: read DR7: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableBit := uint64(1) << (2 * slot) // local enable
|
||||||
|
rwBits := uint64(typ) << (16 + 4*slot) // R/W type
|
||||||
|
lenField := lenBits << (18 + 4*slot) // length
|
||||||
|
|
||||||
|
// Clear the existing bits for this slot, then set the new ones.
|
||||||
|
mask := ^((uint64(1) << (2 * slot)) | (uint64(3) << (16 + 4*slot)) | (uint64(3) << (18 + 4*slot)))
|
||||||
|
dr7 = (dr7 & mask) | enableBit | rwBits | lenField
|
||||||
|
|
||||||
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearWatchpoint removes a hardware watchpoint.
|
||||||
|
func (s *Session) ClearWatchpoint(slot int) error {
|
||||||
|
if slot < 0 || slot > 3 {
|
||||||
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
|
}
|
||||||
|
// Read DR7, clear the enable bit for this slot.
|
||||||
|
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dr7 &^= uint64(1) << (2 * slot) // disable
|
||||||
|
return ptracePokeUser(s.pid, 0x38, dr7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
|
for slot := 0; slot < 4; slot++ {
|
||||||
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ptracePokeUser writes a value to the debuggee's user area at the given offset.
|
||||||
|
func ptracePokeUser(pid int, offset uintptr, val uint64) error {
|
||||||
|
const ptracePokeuser = 6 // PTRACE_POKEUSER
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_PTRACE,
|
||||||
|
uintptr(ptracePokeuser),
|
||||||
|
uintptr(pid),
|
||||||
|
offset,
|
||||||
|
uintptr(val),
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ptracePeekUser reads a value from the debuggee's user area at the given offset.
|
||||||
|
func ptracePeekUser(pid int, offset uintptr) (uint64, error) {
|
||||||
|
const ptracePeekuser = 3 // PTRACE_PEEKUSER
|
||||||
|
val, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_PTRACE,
|
||||||
|
uintptr(ptracePeekuser),
|
||||||
|
uintptr(pid),
|
||||||
|
offset,
|
||||||
|
0, 0, 0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
return uint64(val), nil
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
How gasm-devkit is put together and why.
|
How gasm-devkit is put together and why.
|
||||||
|
|
||||||
|
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
||||||
|
|
||||||
## Design goals
|
## Design goals
|
||||||
|
|
||||||
1. **A real AST, not a grammar hack.** The linter, analyser, assembler and
|
1. **A real AST, not a grammar hack.** The linter, analyser, assembler and
|
||||||
@@ -309,6 +311,18 @@ The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
|
|||||||
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
||||||
arguments to confirm the trampoline round-trips.
|
arguments to confirm the trampoline round-trips.
|
||||||
|
|
||||||
|
### `debug`
|
||||||
|
|
||||||
|
The interactive debugger (Phase 4, linux/amd64). It launches the target
|
||||||
|
function in a child process that maps the JIT code, calls
|
||||||
|
`PTRACE_TRACEME`, and stops; the parent attaches via ptrace and controls
|
||||||
|
execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem`
|
||||||
|
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
||||||
|
The child pins its goroutine to the OS thread with `runtime.LockOSThread`
|
||||||
|
so the traced thread is the one executing JIT code. The REPL provides
|
||||||
|
single-step, register inspection, label resolution, and breakpoint
|
||||||
|
management.
|
||||||
|
|
||||||
## Extension points
|
## Extension points
|
||||||
|
|
||||||
- **New architecture:** add an entry to the generator in `_gen`, run
|
- **New architecture:** add an entry to the generator in `_gen`, run
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# CLI Reference
|
||||||
|
|
||||||
|
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
||||||
|
|
||||||
|
`gasm` is a single binary with subcommands. Run `gasm --help` for an
|
||||||
|
overview, or `gasm <command> -h` for a command's usage and flags.
|
||||||
|
|
||||||
|
## Global Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-h`, `--help` | Show help |
|
||||||
|
| `-V`, `--version` | Print the version |
|
||||||
|
|
||||||
|
## `gasm tokens <file>`
|
||||||
|
|
||||||
|
Print the lexical token stream of FILE: position, token kind, and text,
|
||||||
|
one token per line. FILE may be `-` to read standard input.
|
||||||
|
|
||||||
|
## `gasm parse <file>`
|
||||||
|
|
||||||
|
Parse FILE and report syntax errors on stderr. On success, prints how
|
||||||
|
many declarations and TEXT functions the file contains.
|
||||||
|
|
||||||
|
## `gasm fmt [-w] [path...]`
|
||||||
|
|
||||||
|
Canonicalise the formatting of Plan 9 assembly sources: indentation,
|
||||||
|
operand spacing, per-function mnemonic alignment, and blank-line layout.
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-w` | Write result to the source file (default: print to stdout) |
|
||||||
|
|
||||||
|
With no arguments, or with a directory argument, every `.s` file below
|
||||||
|
it is reformatted in place and the names of changed files are listed
|
||||||
|
(`go fmt` style). `.` and `_` directories are skipped.
|
||||||
|
|
||||||
|
## `gasm lint <file...>`
|
||||||
|
|
||||||
|
Run static checks and print diagnostics as
|
||||||
|
`file:line:col: severity: message [code]`. Exit status is non-zero when
|
||||||
|
an error-severity diagnostic is found.
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-disable` | Comma-separated rule codes to disable |
|
||||||
|
|
||||||
|
Rules: `unknown-instruction`, `operand-count`, `undefined-label`,
|
||||||
|
`duplicate-label`, `missing-ret`, `missing-textflag-include`,
|
||||||
|
`abi-argsize`, `unreachable-code`, `register-clobber`,
|
||||||
|
`funcdata-pcdata`.
|
||||||
|
|
||||||
|
## `gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>`
|
||||||
|
|
||||||
|
Assemble FILE (amd64) to machine code.
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--format` | Output format: `raw` (default), `elf`, `macho`, `goobj` |
|
||||||
|
| `-p` | Package path (required for `--format goobj`) |
|
||||||
|
| `-o` | Write output to file (default: hex dump to stdout) |
|
||||||
|
|
||||||
|
## `gasm verify [flags] <file.s>`
|
||||||
|
|
||||||
|
Assemble FILE, map it into executable memory, and run dynamic checks.
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--ground-truth` | Compare machine code byte-for-byte against `go tool asm` |
|
||||||
|
| `--fuzz` | Differential fuzz: JIT both gasm and go-tool-asm, compare outputs |
|
||||||
|
| `-n` | Fuzz iterations per function (default: 1000) |
|
||||||
|
| `--abi` | Run ABI-checking calls (sentinel registers + red zone) |
|
||||||
|
| `--profile` | List basic-block structure per function |
|
||||||
|
| `--smoke` | Call each NOSPLIT function with zeroed args |
|
||||||
|
|
||||||
|
The `--fuzz` mode runs each function in a subprocess; a partial function
|
||||||
|
(e.g. a decoder that faults on malformed input) is reported as
|
||||||
|
`CRASH` without killing the parent. Use `--ground-truth` for decoders.
|
||||||
|
|
||||||
|
## `gasm debug --func <name> <file.s>`
|
||||||
|
|
||||||
|
Interactive debugger for JIT-assembled amd64 functions. Requires a
|
||||||
|
compiled binary on `$PATH` (not `go run`).
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--func` | Function to debug (required) |
|
||||||
|
|
||||||
|
REPL commands:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `break <label\|addr>` | Set a breakpoint |
|
||||||
|
| `step [n]` | Single-step n instructions |
|
||||||
|
| `continue` | Run until next breakpoint or exit |
|
||||||
|
| `regs` | Print general-purpose registers |
|
||||||
|
| `x [addr] [len]` | Hex-dump memory |
|
||||||
|
| `labels` | List function labels and offsets |
|
||||||
|
| `quit` | Kill the debuggee and exit |
|
||||||
|
|
||||||
|
## `gasm lsp`
|
||||||
|
|
||||||
|
Run the language server over standard input/output (JSON-RPC 2.0 with
|
||||||
|
Content-Length framing). Point an LSP-capable editor at the binary and
|
||||||
|
associate it with `.s` files. The target architecture is inferred from
|
||||||
|
the file-name suffix (`_amd64.s`, `_arm64.s`, `_riscv64.s`,
|
||||||
|
`_loong64.s`).
|
||||||
|
|
||||||
|
Provides: completion, hover, document symbols, diagnostics, and
|
||||||
|
semantic-token highlighting.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Development Guide
|
||||||
|
|
||||||
|
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Go** 1.26+ with `toolchain go1.26.5`
|
||||||
|
- **just** — the command runner; every task below is a just recipe
|
||||||
|
- No external dependencies beyond the Go toolchain
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||||
|
cd gasm-devkit
|
||||||
|
just install # go mod download
|
||||||
|
just build # go vet + gofmt — must pass with zero output
|
||||||
|
just test # full suite, race detector, 80 % coverage gate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Just Recipes
|
||||||
|
|
||||||
|
### `just build`
|
||||||
|
|
||||||
|
Runs `go vet ./...` and checks `gofmt -l .` produces no output. This is
|
||||||
|
the minimum bar before any commit.
|
||||||
|
|
||||||
|
### `just test`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -race -count=1 -coverprofile=coverage.out ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus an `awk` gate that fails if total coverage is below 80 %.
|
||||||
|
|
||||||
|
### `just fmt`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gofmt -w .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run after editing any Go source. The output must be idempotent.
|
||||||
|
|
||||||
|
### `just run -- <args>`
|
||||||
|
|
||||||
|
Runs the CLI via `go run` with the version string stamped:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just run -- lint kernel_amd64.s
|
||||||
|
just run -- fmt -w kernel_amd64.s
|
||||||
|
just run -- verify --ground-truth kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
### `just install-bin`
|
||||||
|
|
||||||
|
Installs the `gasm` binary into `$GOBIN` with the release version
|
||||||
|
embedded via `-ldflags "-X main.version=..."`.
|
||||||
|
|
||||||
|
### `just gen`
|
||||||
|
|
||||||
|
Regenerates the architecture instruction tables in `arch/` by parsing
|
||||||
|
the Go toolchain's own assembler source
|
||||||
|
(`$GOROOT/src/cmd/internal/obj/<arch>/anames.go`). Requires a Go
|
||||||
|
installation. Output is committed — no runtime dependency on the
|
||||||
|
toolchain.
|
||||||
|
|
||||||
|
### `just uninstall`
|
||||||
|
|
||||||
|
Removes `coverage.out`, the `gasm` binary, and `*.test` artefacts.
|
||||||
|
|
||||||
|
## Running Individual Tests
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go test -run TestVexGroundTruth ./asm/
|
||||||
|
go test -run TestDifferentialLZ4Fuzz ./verify/
|
||||||
|
go test -run TestFLACDecorrelate ./verify/
|
||||||
|
go test -run TestGOObjectLinkAndRun ./asm/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugger Note
|
||||||
|
|
||||||
|
`gasm debug` spawns a child process from the binary on `$PATH`. It does
|
||||||
|
not work with `go run` — install first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just install-bin
|
||||||
|
gasm debug --func decodeBlockAVX2 path/to/kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/gasm/ CLI entry point (subcommands)
|
||||||
|
token/ Lexical token kinds and positions
|
||||||
|
lexer/ Hand-written scanner
|
||||||
|
ast/ Abstract syntax tree
|
||||||
|
parser/ Line-oriented parser
|
||||||
|
arch/ Register and instruction tables (generated)
|
||||||
|
lint/ Static analysis rules
|
||||||
|
format/ Canonical formatter
|
||||||
|
lsp/ Language Server Protocol server
|
||||||
|
asm/ Standalone assembler, encoder, object emitters
|
||||||
|
verify/ JIT execution, differential testing, ABI checks
|
||||||
|
debug/ Interactive ptrace debugger (linux/amd64)
|
||||||
|
_gen/ Instruction table generator
|
||||||
|
testdata/ Test fixtures
|
||||||
|
docs/ Architecture, development, CLI reference
|
||||||
|
```
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||||
|
|
||||||
version := "0.22.0"
|
version := "0.28.0"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|||||||
+7
-2
@@ -398,10 +398,15 @@ func parseAddress(g []token.Token) ast.Address {
|
|||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
// Symbol-with-pseudo form: name[<>][+off](PSEUDO).
|
// Symbol-with-pseudo form: name[<>][+off](PSEUDO).
|
||||||
|
// When the prefix is not a valid symbol name (e.g. a bare number like
|
||||||
|
// 0(SP) in RISC-V), sym is nil and we fall through to regular memory
|
||||||
|
// operand parsing instead of returning an empty address.
|
||||||
if idx := findPseudoParen(g); idx >= 0 {
|
if idx := findPseudoParen(g); idx >= 0 {
|
||||||
sym, _ := parseSymbolPrefix(g[:idx+3])
|
sym, _ := parseSymbolPrefix(g[:idx+3])
|
||||||
addr.Sym = sym
|
if sym != nil {
|
||||||
return addr
|
addr.Sym = sym
|
||||||
|
return addr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
|
|||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func add(a, b int64) int64
|
||||||
|
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
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func atomicAdd(ptr *int64, val int64) int64
|
||||||
|
TEXT ·atomicAdd(SB), NOSPLIT, $0-24
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
MOV b+8(FP), X11
|
||||||
|
AMOADDD X11, (X10), X12
|
||||||
|
MOV X12, ret+16(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func fpAdd(a, b float64) float64
|
||||||
|
TEXT ·fpAdd(SB), NOSPLIT, $0-24
|
||||||
|
FLD a+0(FP), F10
|
||||||
|
FLD b+8(FP), F11
|
||||||
|
FADDD F10, F11, F12
|
||||||
|
FSD F12, ret+16(FP)
|
||||||
|
RET
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func readCSR(csr int64) int64
|
||||||
|
TEXT ·readCSR(SB), NOSPLIT, $0-16
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
CSRRS $0x300, X0, X11
|
||||||
|
MOV X11, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func setCSRBit(csr, bit int64) int64
|
||||||
|
TEXT ·setCSRBit(SB), NOSPLIT, $0-24
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
MOV b+8(FP), X11
|
||||||
|
CSRRS $0x304, X11, X12
|
||||||
|
MOV X12, ret+16(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func writeCSR(val int64) int64
|
||||||
|
TEXT ·writeCSR(SB), NOSPLIT, $0-16
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
CSRRW $0x305, X10, X11
|
||||||
|
MOV X11, ret+8(FP)
|
||||||
|
RET
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func fma(a, b, c float64) float64
|
||||||
|
TEXT ·fma(SB), NOSPLIT, $0-32
|
||||||
|
FLD a+0(FP), F10
|
||||||
|
FLD b+8(FP), F11
|
||||||
|
FLD c+16(FP), F12
|
||||||
|
FMADDD F10, F11, F12, F13
|
||||||
|
FSD F13, ret+24(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func fms(a, b, c float64) float64
|
||||||
|
TEXT ·fms(SB), NOSPLIT, $0-32
|
||||||
|
FLD a+0(FP), F10
|
||||||
|
FLD b+8(FP), F11
|
||||||
|
FLD c+16(FP), F12
|
||||||
|
FMSUBD F10, F11, F12, F13
|
||||||
|
FSD F13, ret+24(FP)
|
||||||
|
RET
|
||||||
Vendored
+36
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func casLoop(ptr *int64, old, new int64) bool
|
||||||
|
TEXT ·casLoop(SB), NOSPLIT, $0-32
|
||||||
|
cas_retry:
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
LRD (X10), X11
|
||||||
|
MOV b+8(FP), X12
|
||||||
|
BNE X11, X12, cas_fail
|
||||||
|
MOV c+16(FP), X13
|
||||||
|
SCD X13, (X10), X14
|
||||||
|
BNE X14, X0, cas_retry
|
||||||
|
ADDI X0, $1, X15
|
||||||
|
MOV X15, ret+24(FP)
|
||||||
|
RET
|
||||||
|
cas_fail:
|
||||||
|
MOV X0, ret+24(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func intToFloat(x int64) float64
|
||||||
|
TEXT ·intToFloat(SB), NOSPLIT, $0-16
|
||||||
|
MOV a+0(FP), X10
|
||||||
|
FCVTDL X10, F10
|
||||||
|
FSD F10, ret+8(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func compare(a, b float64) bool
|
||||||
|
TEXT ·compare(SB), NOSPLIT, $0-24
|
||||||
|
FLD a+0(FP), F10
|
||||||
|
FLD b+8(FP), F11
|
||||||
|
FLTD F10, F11, X10
|
||||||
|
MOV X10, ret+16(FP)
|
||||||
|
RET
|
||||||
@@ -97,6 +97,64 @@ func analyzeO1RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum ui
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func analyzeO2RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
|
||||||
|
swin = swin[:len(dstP)+2]
|
||||||
|
for j := 0; j+2 < len(swin); j++ {
|
||||||
|
r := swin[j+2] - 2*swin[j+1] + swin[j]
|
||||||
|
if r == -2147483648 {
|
||||||
|
overflow = true
|
||||||
|
}
|
||||||
|
f := uint32(r<<1) ^ uint32(r>>31)
|
||||||
|
dstP[j] = f
|
||||||
|
partSum += uint64(f)
|
||||||
|
bl := 0
|
||||||
|
for v := f; v > 0; v >>= 1 {
|
||||||
|
bl++
|
||||||
|
}
|
||||||
|
if bl > 31 {
|
||||||
|
bl = 31
|
||||||
|
}
|
||||||
|
hist[bl]++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeResRangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
|
||||||
|
for j := 0; j < len(swin); j++ {
|
||||||
|
r := swin[j]
|
||||||
|
if r == -2147483648 {
|
||||||
|
overflow = true
|
||||||
|
}
|
||||||
|
f := uint32(r<<1) ^ uint32(r>>31)
|
||||||
|
dstP[j] = f
|
||||||
|
partSum += uint64(f)
|
||||||
|
bl := 0
|
||||||
|
for v := f; v > 0; v >>= 1 {
|
||||||
|
bl++
|
||||||
|
}
|
||||||
|
if bl > 31 {
|
||||||
|
bl = 31
|
||||||
|
}
|
||||||
|
hist[bl]++
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMono24Go(src []byte, dst []int32) {
|
||||||
|
for i := 0; i < len(dst); i++ {
|
||||||
|
off := 3 * i
|
||||||
|
u := uint32(src[off]) | uint32(src[off+1])<<8 | uint32(src[off+2])<<16
|
||||||
|
dst[i] = int32(u<<8) >> 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeStereo16Go(src []byte, left, right []int32) {
|
||||||
|
for i := 0; i < len(left); i++ {
|
||||||
|
left[i] = int32(int16(uint16(src[4*i]) | uint16(src[4*i+1])<<8))
|
||||||
|
right[i] = int32(int16(uint16(src[4*i+2]) | uint16(src[4*i+3])<<8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Differential tests ---
|
// --- Differential tests ---
|
||||||
|
|
||||||
func TestFLACDecodeMono16(t *testing.T) {
|
func TestFLACDecodeMono16(t *testing.T) {
|
||||||
@@ -328,3 +386,200 @@ func TestFLACFastStereoSums(t *testing.T) {
|
|||||||
func foldAbs(v int32) uint64 {
|
func foldAbs(v int32) uint64 {
|
||||||
return uint64(uint32(v<<1) ^ uint32(v>>31))
|
return uint64(uint32(v<<1) ^ uint32(v>>31))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runAnalyzeTest is the shared harness for the analyzeO*Range family.
|
||||||
|
func runAnalyzeTest(t *testing.T, k *Kernel, name string, order int, ref func([]int32, []uint32, *[32]uint16) (uint64, bool)) {
|
||||||
|
t.Helper()
|
||||||
|
rng := rand.New(rand.NewSource(int64(order)*100 + 7))
|
||||||
|
for iter := 0; iter < 200; iter++ {
|
||||||
|
n := 1 + rng.Intn(128)
|
||||||
|
swin := make([]int32, n+order)
|
||||||
|
for i := range swin {
|
||||||
|
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
|
||||||
|
}
|
||||||
|
|
||||||
|
goDstP := make([]uint32, n)
|
||||||
|
var goHist [32]uint16
|
||||||
|
goSum, goOvf := ref(swin, goDstP, &goHist)
|
||||||
|
|
||||||
|
jitDstP := make([]uint32, n)
|
||||||
|
var jitHist [32]uint16
|
||||||
|
args := make([]byte, 72)
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
|
||||||
|
PutUint64(args, 8, uint64(len(swin)))
|
||||||
|
PutUint64(args, 16, uint64(cap(swin)))
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(jitDstP)))
|
||||||
|
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
|
||||||
|
|
||||||
|
out, err := k.CallFunc(name, args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
jitSum := GetUint64(out, 56)
|
||||||
|
jitOvf := out[64] != 0
|
||||||
|
|
||||||
|
if jitSum != goSum {
|
||||||
|
t.Fatalf("iter %d: partSum: JIT=%d Go=%d", iter, jitSum, goSum)
|
||||||
|
}
|
||||||
|
if jitOvf != goOvf {
|
||||||
|
t.Fatalf("iter %d: overflow: JIT=%v Go=%v", iter, jitOvf, goOvf)
|
||||||
|
}
|
||||||
|
for i := range goDstP {
|
||||||
|
if jitDstP[i] != goDstP[i] {
|
||||||
|
t.Fatalf("iter %d: dstP[%d]: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if jitHist != goHist {
|
||||||
|
t.Fatalf("iter %d: hist mismatch", iter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACAnalyzeO2Range(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
runAnalyzeTest(t, k, "analyzeO2RangeAVX2", 2, analyzeO2RangeGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACAnalyzeResRange(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
// analyzeResRange has order 0: swin IS the residual (no prediction).
|
||||||
|
runAnalyzeTest(t, k, "analyzeResRangeAVX2", 0, analyzeResRangeGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACAnalyzeO3Range(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
|
||||||
|
swin = swin[:len(dstP)+3]
|
||||||
|
var partSum uint64
|
||||||
|
var overflow bool
|
||||||
|
for j := 0; j+3 < len(swin); j++ {
|
||||||
|
r := swin[j+3] - 3*swin[j+2] + 3*swin[j+1] - swin[j]
|
||||||
|
if r == -2147483648 {
|
||||||
|
overflow = true
|
||||||
|
}
|
||||||
|
f := uint32(r<<1) ^ uint32(r>>31)
|
||||||
|
dstP[j] = f
|
||||||
|
partSum += uint64(f)
|
||||||
|
bl := 0
|
||||||
|
for v := f; v > 0; v >>= 1 {
|
||||||
|
bl++
|
||||||
|
}
|
||||||
|
if bl > 31 {
|
||||||
|
bl = 31
|
||||||
|
}
|
||||||
|
hist[bl]++
|
||||||
|
}
|
||||||
|
return partSum, overflow
|
||||||
|
}
|
||||||
|
runAnalyzeTest(t, k, "analyzeO3RangeAVX2", 3, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACAnalyzeO4Range(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
|
||||||
|
swin = swin[:len(dstP)+4]
|
||||||
|
var partSum uint64
|
||||||
|
var overflow bool
|
||||||
|
for j := 0; j+4 < len(swin); j++ {
|
||||||
|
r := swin[j+4] - 4*swin[j+3] + 6*swin[j+2] - 4*swin[j+1] + swin[j]
|
||||||
|
if r == -2147483648 {
|
||||||
|
overflow = true
|
||||||
|
}
|
||||||
|
f := uint32(r<<1) ^ uint32(r>>31)
|
||||||
|
dstP[j] = f
|
||||||
|
partSum += uint64(f)
|
||||||
|
bl := 0
|
||||||
|
for v := f; v > 0; v >>= 1 {
|
||||||
|
bl++
|
||||||
|
}
|
||||||
|
if bl > 31 {
|
||||||
|
bl = 31
|
||||||
|
}
|
||||||
|
hist[bl]++
|
||||||
|
}
|
||||||
|
return partSum, overflow
|
||||||
|
}
|
||||||
|
runAnalyzeTest(t, k, "analyzeO4RangeAVX2", 4, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACDecodeMono24(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(55))
|
||||||
|
|
||||||
|
for iter := 0; iter < 500; iter++ {
|
||||||
|
n := rng.Intn(256)
|
||||||
|
src := make([]byte, 3*n)
|
||||||
|
rng.Read(src)
|
||||||
|
|
||||||
|
goDst := make([]int32, n)
|
||||||
|
decodeMono24Go(src, goDst)
|
||||||
|
|
||||||
|
jitDst := make([]int32, n)
|
||||||
|
args := make([]byte, 48)
|
||||||
|
if len(src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(src)))
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(jitDst)))
|
||||||
|
|
||||||
|
_, err := k.CallFunc("decodeMono24AVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
for i := range goDst {
|
||||||
|
if jitDst[i] != goDst[i] {
|
||||||
|
t.Fatalf("iter %d: dst[%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFLACDecodeStereo16(t *testing.T) {
|
||||||
|
k := loadFLACKernel(t)
|
||||||
|
rng := rand.New(rand.NewSource(66))
|
||||||
|
|
||||||
|
for iter := 0; iter < 500; iter++ {
|
||||||
|
n := rng.Intn(256)
|
||||||
|
src := make([]byte, 4*n) // [L0,R0,L1,R1,...]
|
||||||
|
rng.Read(src)
|
||||||
|
|
||||||
|
goLeft := make([]int32, n)
|
||||||
|
goRight := make([]int32, n)
|
||||||
|
decodeStereo16Go(src, goLeft, goRight)
|
||||||
|
|
||||||
|
jitLeft := make([]int32, n)
|
||||||
|
jitRight := make([]int32, n)
|
||||||
|
args := make([]byte, 72)
|
||||||
|
if len(src) > 0 {
|
||||||
|
PutPtr(args, 0, unsafe.Pointer(&src[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 8, uint64(len(src)))
|
||||||
|
PutUint64(args, 16, uint64(cap(src)))
|
||||||
|
if n > 0 {
|
||||||
|
PutPtr(args, 24, unsafe.Pointer(&jitLeft[0]))
|
||||||
|
PutPtr(args, 48, unsafe.Pointer(&jitRight[0]))
|
||||||
|
}
|
||||||
|
PutUint64(args, 32, uint64(n))
|
||||||
|
PutUint64(args, 40, uint64(cap(jitLeft)))
|
||||||
|
PutUint64(args, 56, uint64(n))
|
||||||
|
PutUint64(args, 64, uint64(cap(jitRight)))
|
||||||
|
|
||||||
|
_, err := k.CallFunc("decodeStereo16AVX2", args)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("iter %d: %v", iter, err)
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if jitLeft[i] != goLeft[i] || jitRight[i] != goRight[i] {
|
||||||
|
t.Fatalf("iter %d: [%d] L: JIT=%d Go=%d; R: JIT=%d Go=%d",
|
||||||
|
iter, i, jitLeft[i], goLeft[i], jitRight[i], goRight[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+367
@@ -0,0 +1,367 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzResult reports the outcome of a differential fuzz campaign for one
|
||||||
|
// function.
|
||||||
|
type FuzzResult struct {
|
||||||
|
Func string
|
||||||
|
Iterations int
|
||||||
|
Matches int
|
||||||
|
Mismatches int
|
||||||
|
FirstFail string // description of the first mismatch ("" if none)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK returns true when all iterations matched.
|
||||||
|
func (r FuzzResult) OK() bool { return r.Mismatches == 0 }
|
||||||
|
|
||||||
|
// String returns a human-readable summary.
|
||||||
|
func (r FuzzResult) String() string {
|
||||||
|
if r.OK() {
|
||||||
|
return fmt.Sprintf("%s: %d/%d iterations match", r.Func, r.Matches, r.Iterations)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
|
||||||
|
r.Func, r.Matches, r.Iterations, r.Mismatches, r.FirstFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcSig is a parsed // func signature from the assembly source.
|
||||||
|
type funcSig struct {
|
||||||
|
name string
|
||||||
|
params []param
|
||||||
|
results []param
|
||||||
|
}
|
||||||
|
|
||||||
|
type param struct {
|
||||||
|
name string
|
||||||
|
typ string // "[]byte", "[]int32", "int", "*[32]uint16", etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcSigRe matches the conventional "// func name(...)" comment.
|
||||||
|
var funcSigRe = regexp.MustCompile(`^//\s*func\s+(\w+)\(([^)]*)\)\s*(.*)$`)
|
||||||
|
|
||||||
|
// parseFuncSig extracts the function signature from a "// func ..." comment.
|
||||||
|
func parseFuncSig(comment string) (funcSig, bool) {
|
||||||
|
m := funcSigRe.FindStringSubmatch(strings.TrimSpace(comment))
|
||||||
|
if m == nil {
|
||||||
|
return funcSig{}, false
|
||||||
|
}
|
||||||
|
sig := funcSig{name: m[1]}
|
||||||
|
sig.params = parseParams(m[2])
|
||||||
|
// Results may be "(a int, b int)" or "int" or "(int, error)".
|
||||||
|
res := strings.TrimSpace(m[3])
|
||||||
|
res = strings.TrimPrefix(res, "(")
|
||||||
|
res = strings.TrimSuffix(res, ")")
|
||||||
|
if res != "" {
|
||||||
|
sig.results = parseParams(res)
|
||||||
|
}
|
||||||
|
return sig, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParams splits "a []byte, b []int32" into typed parameters, handling
|
||||||
|
// shared types ("a, b []int32").
|
||||||
|
func parseParams(s string) []param {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []param
|
||||||
|
for _, field := range strings.Split(s, ",") {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(field)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
// Unnamed: "int" or "[]byte".
|
||||||
|
out = append(out, param{typ: parts[0]})
|
||||||
|
} else {
|
||||||
|
// Named: "a []byte" or shared "a, b []int32" (handled by the
|
||||||
|
// comma split above — "a" alone means the type follows in the
|
||||||
|
// next field; this is a simplification that covers the common
|
||||||
|
// case where each param has its own type).
|
||||||
|
out = append(out, param{name: parts[0], typ: parts[1]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractSignatures scans assembly source for "// func name(...)" comments
|
||||||
|
// that immediately precede a TEXT directive, and returns the parsed
|
||||||
|
// signatures keyed by the function's short name.
|
||||||
|
func ExtractSignatures(src string) map[string]funcSig {
|
||||||
|
lines := strings.Split(src, "\n")
|
||||||
|
sigs := make(map[string]funcSig)
|
||||||
|
var comments []string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "//") {
|
||||||
|
comments = append(comments, trimmed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "TEXT") {
|
||||||
|
// Search the comment block for the // func line.
|
||||||
|
for _, c := range comments {
|
||||||
|
if sig, ok := parseFuncSig(c); ok {
|
||||||
|
sigs[sig.name] = sig
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comments = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if trimmed != "" {
|
||||||
|
comments = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzFunc runs a differential fuzz campaign: it JIT-executes both the
|
||||||
|
// gasm-assembled and the go-tool-asm-assembled versions of the named
|
||||||
|
// function with random inputs derived from the // func signature, and
|
||||||
|
// compares the output argument area bit-for-bit.
|
||||||
|
//
|
||||||
|
// The signature comment must appear immediately above the TEXT directive
|
||||||
|
// in the source (the conventional Go assembly layout).
|
||||||
|
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult {
|
||||||
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
|
|
||||||
|
rng := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
// Map the Go-assembled code into a second executable region.
|
||||||
|
goExec, err := Map(goCode)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches = iterations
|
||||||
|
result.FirstFail = fmt.Sprintf("map go code: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer goExec.Unmap()
|
||||||
|
|
||||||
|
fl, err := k.Func(name)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches = iterations
|
||||||
|
result.FirstFail = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
// Generate inputs and build TWO independent arg blocks (one per
|
||||||
|
// version) so that functions which write to their arguments
|
||||||
|
// (e.g. histogram increments) don't corrupt the other's input.
|
||||||
|
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
|
||||||
|
|
||||||
|
// Call the gasm version.
|
||||||
|
gasmOut, err := k.CallFunc(name, gasmArgs)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches++
|
||||||
|
if result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
|
||||||
|
}
|
||||||
|
releaseBufs(bufs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the Go version (same function, independent buffers).
|
||||||
|
goOut, err := Call(goExec.FuncAddr(0), goArgs)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches++
|
||||||
|
if result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
|
||||||
|
}
|
||||||
|
releaseBufs(bufs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare only the result area (after all input parameters).
|
||||||
|
// Pointers in the arg block differ (separate buffers), so we
|
||||||
|
// compare from resultOff to the end.
|
||||||
|
resultOff := paramsSize(sig)
|
||||||
|
gasmRes := gasmOut[resultOff:]
|
||||||
|
goRes := goOut[resultOff:]
|
||||||
|
if !equalBytes(gasmRes, goRes) {
|
||||||
|
result.Mismatches++
|
||||||
|
if result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("iter %d: output mismatch at result offset %d", i, resultOff)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.Matches++
|
||||||
|
}
|
||||||
|
releaseBufs(bufs)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// genDualArgs generates two independent ABI0 argument blocks (for gasm and
|
||||||
|
// go) with identical logical content but separate backing buffers, so that
|
||||||
|
// functions which write to their arguments don't corrupt the other's input.
|
||||||
|
func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []byte, bufs [][]byte) {
|
||||||
|
gasmArgs = make([]byte, argSize)
|
||||||
|
goArgs = make([]byte, argSize)
|
||||||
|
off := 0
|
||||||
|
sliceIdx := 0
|
||||||
|
|
||||||
|
for _, p := range sig.params {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(p.typ, "[]"):
|
||||||
|
elemSize := elemSizeFor(p.typ)
|
||||||
|
n := 1 + rng.Intn(127)
|
||||||
|
var declaredLen int
|
||||||
|
if sliceIdx == 0 {
|
||||||
|
declaredLen = n
|
||||||
|
} else {
|
||||||
|
declaredLen = n + 512
|
||||||
|
}
|
||||||
|
// Allocate a buffer comfortably larger than declaredLen*elemSize so
|
||||||
|
// that SIMD over-reads and functions that write slightly past len
|
||||||
|
// (e.g. decoders that trust len(src)) never touch unmapped memory.
|
||||||
|
bufBytes := declaredLen*elemSize + 8192
|
||||||
|
// Two independent buffers with identical random content.
|
||||||
|
buf1 := make([]byte, bufBytes)
|
||||||
|
buf2 := make([]byte, bufBytes)
|
||||||
|
rng.Read(buf1[:n*elemSize])
|
||||||
|
copy(buf2, buf1)
|
||||||
|
bufs = append(bufs, buf1, buf2)
|
||||||
|
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
||||||
|
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
|
||||||
|
// len and cap both equal declaredLen — the buffer is guaranteed
|
||||||
|
// to hold at least declaredLen elements plus safety margin.
|
||||||
|
putU64(gasmArgs, off+8, uint64(declaredLen))
|
||||||
|
putU64(gasmArgs, off+16, uint64(declaredLen))
|
||||||
|
putU64(goArgs, off+8, uint64(declaredLen))
|
||||||
|
putU64(goArgs, off+16, uint64(declaredLen))
|
||||||
|
off += 24
|
||||||
|
sliceIdx++
|
||||||
|
|
||||||
|
case strings.HasPrefix(p.typ, "*["):
|
||||||
|
nElem := arrayLen(p.typ)
|
||||||
|
elem := elemSizeFor("[]" + p.typ[strings.Index(p.typ, "]")+1:])
|
||||||
|
size := nElem * elem
|
||||||
|
if size < 8 {
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
buf1 := make([]byte, size)
|
||||||
|
buf2 := make([]byte, size)
|
||||||
|
rng.Read(buf1)
|
||||||
|
copy(buf2, buf1)
|
||||||
|
bufs = append(bufs, buf1, buf2)
|
||||||
|
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
|
||||||
|
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
|
||||||
|
off += 8
|
||||||
|
|
||||||
|
case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
|
||||||
|
v := uint64(rng.Intn(256))
|
||||||
|
putU64(gasmArgs, off, v)
|
||||||
|
putU64(goArgs, off, v)
|
||||||
|
off += 8
|
||||||
|
|
||||||
|
default:
|
||||||
|
v := rng.Uint64()
|
||||||
|
putU64(gasmArgs, off, v)
|
||||||
|
putU64(goArgs, off, v)
|
||||||
|
off += 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gasmArgs, goArgs, bufs
|
||||||
|
}
|
||||||
|
|
||||||
|
func elemSizeFor(sliceType string) int {
|
||||||
|
switch strings.TrimPrefix(sliceType, "[]") {
|
||||||
|
case "byte", "uint8", "int8":
|
||||||
|
return 1
|
||||||
|
case "uint16", "int16":
|
||||||
|
return 2
|
||||||
|
case "uint32", "int32", "float32":
|
||||||
|
return 4
|
||||||
|
case "uint64", "int64", "float64":
|
||||||
|
return 8
|
||||||
|
default:
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// paramsSize returns the ABI0 stack size occupied by the input parameters.
|
||||||
|
func paramsSize(sig funcSig) int {
|
||||||
|
size := 0
|
||||||
|
for _, p := range sig.params {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(p.typ, "[]"):
|
||||||
|
size += 24 // slice header
|
||||||
|
case strings.HasPrefix(p.typ, "*["):
|
||||||
|
size += 8 // pointer
|
||||||
|
case p.typ == "bool":
|
||||||
|
size += 1
|
||||||
|
default:
|
||||||
|
size += 8 // int, uint, etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func arrayLen(typ string) int {
|
||||||
|
// "*[32]uint16" → 32
|
||||||
|
start := strings.Index(typ, "[")
|
||||||
|
end := strings.Index(typ, "]")
|
||||||
|
if start < 0 || end < 0 || end <= start {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
n, _ := strconv.Atoi(typ[start+1 : end])
|
||||||
|
if n <= 0 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func putPtr(buf []byte, off int, p unsafe.Pointer) {
|
||||||
|
if off+8 <= len(buf) {
|
||||||
|
u64 := uint64(uintptr(p))
|
||||||
|
buf[off] = byte(u64)
|
||||||
|
buf[off+1] = byte(u64 >> 8)
|
||||||
|
buf[off+2] = byte(u64 >> 16)
|
||||||
|
buf[off+3] = byte(u64 >> 24)
|
||||||
|
buf[off+4] = byte(u64 >> 32)
|
||||||
|
buf[off+5] = byte(u64 >> 40)
|
||||||
|
buf[off+6] = byte(u64 >> 48)
|
||||||
|
buf[off+7] = byte(u64 >> 56)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putU64(buf []byte, off int, v uint64) {
|
||||||
|
if off+8 <= len(buf) {
|
||||||
|
buf[off] = byte(v)
|
||||||
|
buf[off+1] = byte(v >> 8)
|
||||||
|
buf[off+2] = byte(v >> 16)
|
||||||
|
buf[off+3] = byte(v >> 24)
|
||||||
|
buf[off+4] = byte(v >> 32)
|
||||||
|
buf[off+5] = byte(v >> 40)
|
||||||
|
buf[off+6] = byte(v >> 48)
|
||||||
|
buf[off+7] = byte(v >> 56)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalBytes(a, b []byte) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseBufs(bufs [][]byte) {
|
||||||
|
// Keep buffers alive until after the call; nothing to free in Go,
|
||||||
|
// but this prevents the compiler from collecting them too early.
|
||||||
|
_ = bufs
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractSignatures(t *testing.T) {
|
||||||
|
src := `// func add(a int64, b int64) int64
|
||||||
|
TEXT ·add(SB), NOSPLIT, $0-24
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func wideCopy(dst []byte, src []byte)
|
||||||
|
TEXT ·wideCopy(SB), NOSPLIT, $0-48
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
sigs := ExtractSignatures(src)
|
||||||
|
if len(sigs) != 2 {
|
||||||
|
t.Fatalf("expected 2 signatures, got %d: %v", len(sigs), sigs)
|
||||||
|
}
|
||||||
|
add, ok := sigs["add"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("add not found")
|
||||||
|
}
|
||||||
|
if len(add.params) != 2 {
|
||||||
|
t.Errorf("add params: got %d, want 2", len(add.params))
|
||||||
|
}
|
||||||
|
wc, ok := sigs["wideCopy"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("wideCopy not found")
|
||||||
|
}
|
||||||
|
if len(wc.params) != 2 {
|
||||||
|
t.Errorf("wideCopy params: got %d, want 2", len(wc.params))
|
||||||
|
}
|
||||||
|
if wc.params[0].typ != "[]byte" {
|
||||||
|
t.Errorf("wideCopy param[0].typ = %q, want []byte", wc.params[0].typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuzzWideCopy(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruth: %v", err)
|
||||||
|
}
|
||||||
|
goCode, ok := gt["wideCopy"]
|
||||||
|
if !ok {
|
||||||
|
t.Skip("wideCopy not in ground truth")
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := funcSig{
|
||||||
|
name: "wideCopy",
|
||||||
|
params: []param{
|
||||||
|
{name: "dst", typ: "[]byte"},
|
||||||
|
{name: "src", typ: "[]byte"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res := k.FuzzFunc("wideCopy", sig, goCode, 200, 42)
|
||||||
|
if !res.OK() {
|
||||||
|
t.Errorf("wideCopy fuzz: %s", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFuncSig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
comment string
|
||||||
|
name string
|
||||||
|
nParams int
|
||||||
|
}{
|
||||||
|
{"// func add(a int64, b int64) int64", "add", 2},
|
||||||
|
{"// func wideCopy(dst []byte, src []byte)", "wideCopy", 2},
|
||||||
|
{"// func analyzeO1RangeAVX2(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool)", "analyzeO1RangeAVX2", 3},
|
||||||
|
{"// not a func", "", 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
sig, ok := parseFuncSig(tt.comment)
|
||||||
|
if tt.name == "" {
|
||||||
|
if ok {
|
||||||
|
t.Errorf("parseFuncSig(%q): expected not ok", tt.comment)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("parseFuncSig(%q): expected ok", tt.comment)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sig.name != tt.name {
|
||||||
|
t.Errorf("parseFuncSig(%q).name = %q, want %q", tt.comment, sig.name, tt.name)
|
||||||
|
}
|
||||||
|
if len(sig.params) != tt.nParams {
|
||||||
|
t.Errorf("parseFuncSig(%q): %d params, want %d", tt.comment, len(sig.params), tt.nParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroundTruth assembles the given .s file with the Go toolchain's own
|
||||||
|
// assembler and returns the machine code bytes for each TEXT function,
|
||||||
|
// keyed by the function's short name (the part after the middle dot).
|
||||||
|
// This is the universal oracle: any file that `go tool asm` accepts can
|
||||||
|
// be verified, with no hand-written reference.
|
||||||
|
//
|
||||||
|
// For RISC-V sources the assembler is invoked with GOARCH=riscv64;
|
||||||
|
// the caller must set the architecture via GroundTruthArch.
|
||||||
|
func GroundTruth(path string) (map[string][]byte, error) {
|
||||||
|
return groundTruthArch(path, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroundTruthRISCV assembles the given .s file with the Go toolchain in
|
||||||
|
// RISC-V cross-assembly mode (GOARCH=riscv64).
|
||||||
|
func GroundTruthRISCV(path string) (map[string][]byte, error) {
|
||||||
|
return groundTruthArch(path, "riscv64")
|
||||||
|
}
|
||||||
|
|
||||||
|
func groundTruthArch(path, goarch string) (map[string][]byte, error) {
|
||||||
|
goroot := runtime.GOROOT()
|
||||||
|
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
|
||||||
|
if _, err := os.Stat(asmBin); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: go tool asm not found at %s: %w", asmBin, err)
|
||||||
|
}
|
||||||
|
includeDir := filepath.Join(goroot, "pkg", "include")
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: tempdir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
objPath := filepath.Join(tmpDir, "out.o")
|
||||||
|
|
||||||
|
base := filepath.Base(path)
|
||||||
|
pkg := strings.TrimSuffix(base, ".s")
|
||||||
|
pkg = strings.TrimSuffix(pkg, "_amd64")
|
||||||
|
pkg = strings.TrimSuffix(pkg, "_riscv64")
|
||||||
|
|
||||||
|
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||||
|
if goarch != "" {
|
||||||
|
cmd.Env = append(os.Environ(), "GOARCH="+goarch)
|
||||||
|
}
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: go tool asm (%s): %w\n%s", goarch, err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
objData, err := os.ReadFile(objPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: read object: %w", err)
|
||||||
|
}
|
||||||
|
return extractGOOBJCode(objData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOBJ block indices (cmd/internal/goobj).
|
||||||
|
const (
|
||||||
|
blkAutolib = iota
|
||||||
|
blkPkgIdx
|
||||||
|
blkFile
|
||||||
|
blkSymdef
|
||||||
|
blkHashed64def
|
||||||
|
blkHasheddef
|
||||||
|
blkNonpkgdef
|
||||||
|
blkNonpkgref
|
||||||
|
blkRefFlags
|
||||||
|
blkHash64
|
||||||
|
blkHash
|
||||||
|
blkRelocIdx
|
||||||
|
blkAuxIdx
|
||||||
|
blkDataIdx
|
||||||
|
blkReloc
|
||||||
|
blkAux
|
||||||
|
blkData
|
||||||
|
blkRefName
|
||||||
|
blkEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
const goobjMagic = "\x00go120ld"
|
||||||
|
|
||||||
|
// extractGOOBJCode parses a GOOBJ payload and returns the code bytes for
|
||||||
|
// each non-package STEXT symbol (the functions).
|
||||||
|
func extractGOOBJCode(data []byte) (map[string][]byte, error) {
|
||||||
|
// Find the GOOBJ header (after the "go object ..." preamble).
|
||||||
|
i := bytes.Index(data, []byte(goobjMagic))
|
||||||
|
if i < 0 {
|
||||||
|
return nil, fmt.Errorf("verify: no GOOBJ magic in object file")
|
||||||
|
}
|
||||||
|
b := data[i:]
|
||||||
|
le := binary.LittleEndian
|
||||||
|
|
||||||
|
// Read block offsets (20 bytes into the header: 4 magic + 8 go version
|
||||||
|
// + 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
||||||
|
var offs [blkEnd + 1]uint32
|
||||||
|
for j := 0; j <= blkEnd; j++ {
|
||||||
|
offs[j] = le.Uint32(b[20+4*j:])
|
||||||
|
}
|
||||||
|
blk := func(idx int) []byte { return b[offs[idx]:offs[idx+1]] }
|
||||||
|
|
||||||
|
// Parse non-package symbol definitions (blkNonpkgdef): each entry is
|
||||||
|
// 21 bytes: [nameLen:4][nameOff:4][abi:2][type:1][flag:1][flag2:1][size:4][align:4].
|
||||||
|
const symSize = 21
|
||||||
|
nonpkg := blk(blkNonpkgdef)
|
||||||
|
nSyms := len(nonpkg) / symSize
|
||||||
|
|
||||||
|
// Data index (blkDataIdx): one uint32 per defined symbol across ALL
|
||||||
|
// definition blocks (blkSymdef + blkHashed64def + blkHasheddef +
|
||||||
|
// blkNonpkgdef), in that order. We need the offset for the nonpkg
|
||||||
|
// symbols, which come last.
|
||||||
|
dataIdx := blk(blkDataIdx)
|
||||||
|
dataBlk := blk(blkData)
|
||||||
|
|
||||||
|
// Count symbols in the preceding definition blocks.
|
||||||
|
preceding := 0
|
||||||
|
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
|
||||||
|
preceding += len(blk(bi)) / symSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol name offsets in the GOOBJ symbol table are absolute byte
|
||||||
|
// offsets from the start of the GOOBJ payload (the magic).
|
||||||
|
readStr := func(off, ln uint32) string {
|
||||||
|
if int(off+ln) > len(b) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b[off : off+ln])
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string][]byte)
|
||||||
|
const kindSTEXT = 1
|
||||||
|
for s := 0; s < nSyms; s++ {
|
||||||
|
x := nonpkg[s*symSize:]
|
||||||
|
nameLen := le.Uint32(x[0:])
|
||||||
|
nameOff := le.Uint32(x[4:])
|
||||||
|
typ := x[10]
|
||||||
|
size := le.Uint32(x[13:])
|
||||||
|
|
||||||
|
if typ != kindSTEXT || size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := readStr(nameOff, nameLen)
|
||||||
|
// Strip the package prefix (everything up to and including the
|
||||||
|
// last middle dot or period-dot).
|
||||||
|
name = stripPkg(name)
|
||||||
|
|
||||||
|
// Data offset from the index (nonpkg symbols follow the preceding blocks).
|
||||||
|
diIdx := preceding + s
|
||||||
|
if (diIdx+1)*4 > len(dataIdx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dOff := le.Uint32(dataIdx[diIdx*4:])
|
||||||
|
if int(dOff+size) > len(dataBlk) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
code := make([]byte, size)
|
||||||
|
copy(code, dataBlk[dOff:dOff+size])
|
||||||
|
result[name] = code
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripPkg removes the package path prefix from a symbol name, leaving
|
||||||
|
// just the function name. "pkg/path·FuncName" → "FuncName".
|
||||||
|
func stripPkg(name string) string {
|
||||||
|
if i := strings.LastIndex(name, "\u00B7"); i >= 0 {
|
||||||
|
return name[i+len("\u00B7"):]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndex(name, "\"."); i >= 0 {
|
||||||
|
return name[i+2:]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGroundTruthBasic(t *testing.T) {
|
||||||
|
// Use the simple test kernel — it assembles with go tool asm.
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruth: %v", err)
|
||||||
|
}
|
||||||
|
if len(gt) == 0 {
|
||||||
|
t.Fatal("no functions extracted from ground truth")
|
||||||
|
}
|
||||||
|
// The "add" function should be present and non-empty.
|
||||||
|
code, ok := gt["add"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("function 'add' not found in ground truth; got: %v", keys(gt))
|
||||||
|
}
|
||||||
|
if len(code) == 0 {
|
||||||
|
t.Fatal("add: zero-length code")
|
||||||
|
}
|
||||||
|
t.Logf("ground truth functions: %v", keys(gt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroundTruthComparison(t *testing.T) {
|
||||||
|
// Assemble with gasm and compare against go tool asm.
|
||||||
|
k, err := Load("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruth: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range k.FuncNames() {
|
||||||
|
fl, _ := k.Func(name)
|
||||||
|
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
|
||||||
|
goCode, ok := gt[name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: not in ground truth", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(gasmCode) != len(goCode) {
|
||||||
|
t.Errorf("%s: size mismatch: gasm=%d go=%d", name, len(gasmCode), len(goCode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range gasmCode {
|
||||||
|
if gasmCode[i] != goCode[i] {
|
||||||
|
t.Errorf("%s: byte %d differs: gasm=%02x go=%02x", name, i, gasmCode[i], goCode[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroundTruthBadFile(t *testing.T) {
|
||||||
|
_, err := GroundTruth("/nonexistent/file_amd64.s")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(m map[string][]byte) []string {
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -120,3 +120,8 @@ func (k *Kernel) Close() {
|
|||||||
k.exec.Unmap()
|
k.exec.Unmap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image returns the assembled image (code + data + metadata).
|
||||||
|
func (k *Kernel) Image() *asm.Image {
|
||||||
|
return k.img
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user