Compare commits
45
Commits
v0.32.0
...
93c47a312a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c47a312a | ||
|
|
708d0a0a5e | ||
|
|
3c8f7cb411 | ||
|
|
7c5b7a1419 | ||
|
|
bc3f448738 | ||
|
|
f37f183577 | ||
|
|
1e77e58250 | ||
|
|
1d0969ed64 | ||
|
|
23c001be51 | ||
|
|
96e81cc98d | ||
|
|
c834d98210 | ||
|
|
03d6d4da54 | ||
|
|
0b42ce7952 | ||
|
|
288a64ccd2 | ||
|
|
5fddfa704b | ||
|
|
a2bb5eeb4e | ||
|
|
48449b7a7f | ||
|
|
3de043c494 | ||
|
|
0078f7be5c | ||
|
|
6a7317d141 | ||
|
|
d08523caa5 | ||
|
|
20e4b8d9c4 | ||
|
|
61f4247cef | ||
|
|
049872ddff | ||
|
|
3669f64ff6 | ||
|
|
fff9f75595 | ||
|
|
40476546df | ||
|
|
70218e84ba | ||
|
|
db50b98179 | ||
|
|
2e2c0b82a0 | ||
|
|
8dc1e98ca1 | ||
|
|
1d8e68c574 | ||
|
|
89fa6ea15e | ||
|
|
edc20ffa97 | ||
|
|
50db6615b2 | ||
|
|
95f1d6f083 | ||
|
|
f43e791e5a | ||
|
|
1691c81095 | ||
|
|
2db563be07 | ||
|
|
4f190ee1a2 | ||
|
|
909f874797 | ||
|
|
e307bf830f | ||
|
|
953c258d6a | ||
|
|
c6f0286732 | ||
|
|
f5fc22d390 |
@@ -0,0 +1,37 @@
|
|||||||
|
# Race, Go. Dispatched by hand, and never a gate on a push or a tag: the release tag is
|
||||||
|
# cut only after `just gates` has already raced the tree, so this workflow is the
|
||||||
|
# explicit second opinion, not a step of the release.
|
||||||
|
#
|
||||||
|
# The race detector roughly doubles both time and memory, which the shared runner box
|
||||||
|
# cannot afford on every push. Locally it belongs to `just gates`, which runs it once per
|
||||||
|
# task; here it is a decision rather than a routine.
|
||||||
|
#
|
||||||
|
# Every step is one command, so the step that fails is the gate that failed.
|
||||||
|
name: Race
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# One core: parallelism buys no speed here and costs memory the box does not have.
|
||||||
|
GOFLAGS: -p=1
|
||||||
|
GOMAXPROCS: "2"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
race:
|
||||||
|
runs-on: fedora
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install gcc
|
||||||
|
# The race detector needs cgo and the runner image carries no C compiler.
|
||||||
|
run: dnf install -y gcc
|
||||||
|
|
||||||
|
- name: Race
|
||||||
|
run: go test -race -count=1 -timeout 10m ./...
|
||||||
+255
-86
@@ -1,73 +1,198 @@
|
|||||||
# Release — gasm binaries. Runs on version tags (v0.28.0) pushed to main.
|
# Release, Go binaries. Runs on version tags (v1.2.3) pushed to main.
|
||||||
|
#
|
||||||
|
# The module sits at the repository root: the toolchain records a version only for a root
|
||||||
|
# module, measured on go1.27.1, so a build of a module in a subdirectory reports (devel)
|
||||||
|
# even at its own <module>/vX.Y.Z tag and this workflow's smoke test can never pass for
|
||||||
|
# it. A Go repository is one module at the root.
|
||||||
|
#
|
||||||
|
# The version contract these steps implement is in the `release` skill, and its point is
|
||||||
|
# that nothing is injected: the toolchain records the tag into the binary's build
|
||||||
|
# information, so the build simply has to happen at the tag, which the trigger guarantees.
|
||||||
|
#
|
||||||
|
# The gates run in their own job, once, before the matrix, minus the race detector: race
|
||||||
|
# never runs on a push path or a tag, and the local gate raced this tree before the tag
|
||||||
|
# was cut. Putting the gates inside the matrix would run the whole suite once per target
|
||||||
|
# on the box that also hosts the forge. Each job validates the tag for itself rather than
|
||||||
|
# passing a value between jobs, so no workflow feature has to be trusted for the version
|
||||||
|
# to reach the file name.
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# The box is shared with the forge, so parallelism is bounded on purpose. The gates job
|
||||||
|
# needs it most; the build jobs inherit it for their parallel compilation.
|
||||||
|
GOFLAGS: -p=1
|
||||||
|
GOMAXPROCS: "2"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
gates:
|
||||||
|
runs-on: fedora
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install Perl
|
||||||
|
# Perl for the steps below. The install is a no-op where the package
|
||||||
|
# is already present.
|
||||||
|
run: dnf install -y perl
|
||||||
|
|
||||||
|
- name: Validate the tag
|
||||||
|
env:
|
||||||
|
VERSION: ${{ gitea.ref_name }}
|
||||||
|
run: |
|
||||||
|
perl -e '
|
||||||
|
my $v = $ENV{VERSION} // q{};
|
||||||
|
$v =~ m{^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$}
|
||||||
|
or die qq{ERROR: expected a semver tag like v1.2.3, got: $v\n};
|
||||||
|
print qq{tag $v\n};
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: |
|
||||||
|
perl -e '
|
||||||
|
open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!};
|
||||||
|
my @bad = <$g>;
|
||||||
|
close($g);
|
||||||
|
print @bad;
|
||||||
|
exit(@bad ? 1 : 0);
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Modernise
|
||||||
|
run: go fix -diff ./...
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
# The same command as in test.yml, so the floor is the same number everywhere.
|
||||||
|
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||||
|
|
||||||
|
- name: Coverage floor
|
||||||
|
run: |
|
||||||
|
perl -e '
|
||||||
|
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
|
||||||
|
my $total;
|
||||||
|
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
|
||||||
|
close($c);
|
||||||
|
die qq{no total line in coverage.out\n} unless defined $total;
|
||||||
|
printf qq{Total coverage: %s%%\n}, $total;
|
||||||
|
exit($total < 80 ? 1 : 0);
|
||||||
|
'
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: fedora
|
runs-on: fedora
|
||||||
|
timeout-minutes: 25
|
||||||
|
needs: gates
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
# Portable targets: amd64, arm64, loong64 and riscv64 on Linux, at the toolchain
|
||||||
|
# default level. No 32-bit, no wasm, no macOS, no Windows. FreeBSD stays out until
|
||||||
|
# verify/jit.go ports off syscall.Mprotect: the Go syscall package defines no
|
||||||
|
# Mprotect for freebsd, and verify/jit.go:50 calls it to drop the write bit from
|
||||||
|
# the JIT mapping, so every freebsd target fails to build with "undefined:
|
||||||
|
# syscall.Mprotect" (verified for amd64, arm64 and riscv64 on go1.27.1).
|
||||||
include:
|
include:
|
||||||
- goos: linux
|
- goos: linux
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- goos: linux
|
- goos: linux
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
- goos: linux
|
|
||||||
goarch: riscv64
|
|
||||||
- goos: linux
|
- goos: linux
|
||||||
goarch: loong64
|
goarch: loong64
|
||||||
|
- goos: linux
|
||||||
|
goarch: riscv64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.27"
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
- name: Download dependencies
|
- name: Install Perl
|
||||||
run: go mod download
|
run: dnf install -y perl
|
||||||
|
|
||||||
- name: Validate tag and build
|
- name: Validate the tag
|
||||||
id: build
|
id: version
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ gitea.ref_name }}
|
VERSION: ${{ gitea.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
perl -e '
|
||||||
|
my $v = $ENV{VERSION} // q{};
|
||||||
|
$v =~ m{^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$}
|
||||||
|
or die qq{ERROR: expected a semver tag like v1.2.3, got: $v\n};
|
||||||
|
(my $nv = $v) =~ s{^v}{};
|
||||||
|
open(my $o, q{>>}, $ENV{GITEA_OUTPUT}) or die qq{GITEA_OUTPUT: $!};
|
||||||
|
print $o qq{version_no_v=$nv\n};
|
||||||
|
close($o);
|
||||||
|
print qq{version $nv\n};
|
||||||
|
'
|
||||||
|
|
||||||
if ! echo "$VERSION" | grep -qE '^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$'; then
|
- name: Build
|
||||||
echo "ERROR: expected a semver tag like v1.2.3, got: '$VERSION'"
|
env:
|
||||||
exit 1
|
VERSION_NO_V: ${{ steps.version.outputs.version_no_v }}
|
||||||
fi
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
VERSION_NO_V="${VERSION#v}"
|
CGO_ENABLED: "0"
|
||||||
echo "version_no_v=${VERSION_NO_V}" >> "$GITEA_OUTPUT"
|
run: |
|
||||||
|
# Nothing is injected. The toolchain records the tag into the binary's build
|
||||||
mkdir -p bin
|
# information, so the version is right because this build happens at the tag, and
|
||||||
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 \
|
# there is no path for anyone to get wrong. -s -w only strips symbols.
|
||||||
go build -ldflags "-s -w -X main.version=${VERSION_NO_V}" \
|
go build -ldflags "-s -w" -o "bin/gasm-${VERSION_NO_V}-${GOOS}-${GOARCH}" ./cmd/gasm
|
||||||
-o "bin/gasm-${VERSION_NO_V}-${{ matrix.goos }}-${{ matrix.goarch }}" \
|
|
||||||
./cmd/gasm
|
|
||||||
|
|
||||||
|
# Artifacts stay on v3: v4 and later detect Gitea as GHES and abort.
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: gasm-${{ matrix.goos }}-${{ matrix.goarch }}
|
name: gasm-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
path: bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
path: bin/gasm-${{ steps.version.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Smoke test
|
- name: Smoke test
|
||||||
|
# Only a binary matching the runner can be run here. The check is not that --version
|
||||||
|
# exits cleanly but that it reports the tag and nothing more: a build outside version
|
||||||
|
# control reports (devel), and a build whose tree was dirty reports +dirty, and both
|
||||||
|
# would otherwise be published.
|
||||||
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
BIN: bin/gasm-${{ steps.version.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
run: |
|
run: |
|
||||||
chmod +x bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
perl -e '
|
||||||
./bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }} --version
|
my $want = $ENV{TAG} // die qq{ERROR: no tag\n};
|
||||||
|
open(my $bin, q{-|}, $ENV{BIN}, q{--version}) or die qq{$ENV{BIN}: $!};
|
||||||
|
my $got = <$bin>;
|
||||||
|
close($bin);
|
||||||
|
$got = defined $got ? $got : q{};
|
||||||
|
chomp $got;
|
||||||
|
index($got, $want) >= 0
|
||||||
|
or die qq{ERROR: the binary printed "$got", which does not contain $want. Version control was disabled, so there is no recorded version.\n};
|
||||||
|
index($got, q{+dirty}) < 0
|
||||||
|
or die qq{ERROR: the binary printed "$got". The tree was dirty at build time, which means the checkout was not the tag, or the build artefacts are not ignored.\n};
|
||||||
|
print qq{$ENV{BIN} reports $got\n};
|
||||||
|
'
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: fedora
|
runs-on: fedora
|
||||||
|
timeout-minutes: 15
|
||||||
needs: build
|
needs: build
|
||||||
permissions:
|
permissions:
|
||||||
|
# contents: read is required for the checkout: a job that declares any
|
||||||
|
# permissions gets a token scoped to exactly those, and releases: write
|
||||||
|
# alone leaves the fetch with no read access, which Gitea answers with
|
||||||
|
# a 404 "Repository not found". Verified on the instance 2026-09-16.
|
||||||
|
contents: read
|
||||||
releases: write
|
releases: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
@@ -77,81 +202,125 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Extract CHANGELOG section
|
- name: Install Perl
|
||||||
|
run: dnf install -y perl
|
||||||
|
|
||||||
|
- name: Extract the CHANGELOG section
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ gitea.ref_name }}
|
VERSION: ${{ gitea.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
# Each step derives what it needs from the tag, so no value has to travel between
|
||||||
VERSION_NO_V="${VERSION#v}"
|
# jobs.
|
||||||
|
perl -e '
|
||||||
|
my $v = $ENV{VERSION} // q{};
|
||||||
|
$v =~ s{^v}{};
|
||||||
|
open(my $vout, q{>}, q{version-no-v.txt}) or die qq{version-no-v.txt: $!};
|
||||||
|
print $vout $v;
|
||||||
|
close($vout);
|
||||||
|
open(my $in, q{<}, q{CHANGELOG.md}) or die qq{CHANGELOG.md: $!};
|
||||||
|
my @lines = <$in>;
|
||||||
|
close($in);
|
||||||
|
my ($start, $end) = (-1, scalar @lines);
|
||||||
|
for my $i (0 .. $#lines) {
|
||||||
|
if ($start < 0) { $start = $i if $lines[$i] =~ m{^##\s+\[\Q$v\E\]} }
|
||||||
|
elsif ($lines[$i] =~ m{^##\s+\[}) { $end = $i; last }
|
||||||
|
}
|
||||||
|
$start >= 0 or die qq{ERROR: no CHANGELOG section for $v, expected a heading like: ## [$v] - YYYY-MM-DD\n};
|
||||||
|
my @body = grep { m{\S} } @lines[$start + 1 .. $end - 1];
|
||||||
|
@body or die qq{ERROR: the CHANGELOG section for $v is empty\n};
|
||||||
|
open(my $out, q{>}, q{release-body.md}) or die qq{release-body.md: $!};
|
||||||
|
print $out @body;
|
||||||
|
close($out);
|
||||||
|
printf qq{notes for %s: %d lines\n}, $v, scalar @body;
|
||||||
|
'
|
||||||
|
|
||||||
sed -n "/^## \[${VERSION_NO_V}\] /,/^## \[/p" CHANGELOG.md \
|
- name: Build the release request
|
||||||
| sed '$d' \
|
run: |
|
||||||
| tail -n +2 \
|
perl -e '
|
||||||
> release-body.md
|
open(my $vin, q{<}, q{version-no-v.txt}) or die qq{version-no-v.txt: $!};
|
||||||
|
my $v = <$vin>;
|
||||||
|
close($vin);
|
||||||
|
chomp $v;
|
||||||
|
open(my $in, q{<:raw}, q{release-body.md}) or die qq{release-body.md: $!};
|
||||||
|
my $body = do { local $/; <$in> };
|
||||||
|
close($in);
|
||||||
|
# Byte-oriented escaping: JSON is UTF-8, so non-ASCII passes through and only the
|
||||||
|
# characters JSON forbids are rewritten.
|
||||||
|
$body =~ s/([\\"])/\\$1/g;
|
||||||
|
$body =~ s/\t/\\t/g;
|
||||||
|
$body =~ s/\r//g;
|
||||||
|
$body =~ s/\n/\\n/g;
|
||||||
|
$body =~ s/([\x00-\x08\x0b\x0c\x0e-\x1f])/sprintf(q{\u%04x}, ord($1))/ge;
|
||||||
|
my $json = sprintf(qq{{"tag_name":"v%s","name":"v%s","body":"%s","draft":false,"prerelease":false}}, $v, $v, $body);
|
||||||
|
open(my $out, q{>}, q{release.json}) or die qq{release.json: $!};
|
||||||
|
print $out $json;
|
||||||
|
close($out);
|
||||||
|
print qq{release.json written for v$v\n};
|
||||||
|
'
|
||||||
|
|
||||||
if [ ! -s release-body.md ]; then
|
- name: Create the release
|
||||||
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:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
perl -e '
|
||||||
|
my @cmd = (q{curl}, q{-sS}, q{-o}, q{response.json}, q{-w}, q{%{http_code}},
|
||||||
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')
|
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
||||||
BODY="\"${BODY}\""
|
q{-H}, q{Content-Type: application/json},
|
||||||
|
q{-X}, q{POST},
|
||||||
response=$(curl -sS -w '\n%{http_code}' \
|
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases},
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
q{--data-binary}, q{@release.json});
|
||||||
-H "Content-Type: application/json" \
|
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
||||||
-X POST \
|
my $code = <$curl>;
|
||||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
my $ok = close($curl);
|
||||||
-d "{\"tag_name\":\"${GITEA_REF_NAME}\",\"name\":\"${GITEA_REF_NAME}\",\"body\":${BODY},\"draft\":false,\"prerelease\":false}")
|
my $exit = $? >> 8;
|
||||||
|
$code = defined $code ? $code : q{};
|
||||||
http_code=$(echo "$response" | tail -1)
|
$ok or die qq{ERROR: curl failed (exit $exit) calling $ENV{GITEA_SERVER_URL}\n};
|
||||||
payload=$(echo "$response" | sed '$d')
|
open(my $r, q{<:raw}, q{response.json}) or die qq{response.json: $!};
|
||||||
|
my $body = do { local $/; <$r> };
|
||||||
echo "HTTP ${http_code}"
|
close($r);
|
||||||
if [ "$http_code" != "201" ]; then
|
$code eq q{201} or die qq{ERROR: the release was not created, HTTP $code: $body\n};
|
||||||
echo "Failed to create release: ${payload}"
|
$body =~ m{"id"\s*:\s*([0-9]+)} or die qq{ERROR: no release id in the response: $body\n};
|
||||||
exit 1
|
open(my $o, q{>}, q{release-id.txt}) or die qq{release-id.txt: $!};
|
||||||
fi
|
print $o $1;
|
||||||
|
close($o);
|
||||||
RELEASE_ID=$(echo "$payload" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
print qq{release id $1\n};
|
||||||
echo "Created release ID=${RELEASE_ID}"
|
'
|
||||||
printf '%s' "${RELEASE_ID}" > release-id.txt
|
|
||||||
|
|
||||||
- name: Upload assets
|
- name: Upload assets
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
perl -e '
|
||||||
RELEASE_ID=$(cat release-id.txt)
|
open(my $f, q{<}, q{release-id.txt}) or die qq{release-id.txt: $!};
|
||||||
|
my $id = <$f>;
|
||||||
for binary in dist/gasm-*/gasm-*; do
|
close($f);
|
||||||
[ -f "$binary" ] || continue
|
chomp $id;
|
||||||
fname=$(basename "$binary")
|
my @files = grep { -f $_ } glob(q{dist/*/*});
|
||||||
echo "Uploading ${fname}..."
|
@files or die qq{ERROR: no assets under dist/\n};
|
||||||
http_code=$(curl -sS -o /dev/null -w '%{http_code}' \
|
my $bad = 0;
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
for my $path (@files) {
|
||||||
-H "Content-Type: application/octet-stream" \
|
(my $name = $path) =~ s{.*/}{};
|
||||||
-X POST \
|
my @cmd = (q{curl}, q{-sS}, q{-o}, q{/dev/null}, q{-w}, q{%{http_code}},
|
||||||
--data-binary "@${binary}" \
|
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
||||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${fname}")
|
q{-H}, q{Content-Type: application/octet-stream},
|
||||||
echo " HTTP ${http_code}"
|
q{-X}, q{POST}, q{--data-binary}, qq{@$path},
|
||||||
if [ "$http_code" != "201" ]; then
|
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases/$id/assets?name=$name});
|
||||||
echo "Failed to upload ${fname}"
|
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
||||||
exit 1
|
my $code = <$curl>;
|
||||||
fi
|
my $ok = close($curl);
|
||||||
done
|
my $exit = $? >> 8;
|
||||||
|
$code = defined $code ? $code : q{};
|
||||||
echo "Release ${GITEA_REF_NAME} is live."
|
unless ($ok) {
|
||||||
|
printf qq{%s: curl failed (exit %d)\n}, $name, $exit;
|
||||||
|
$bad = 1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
printf qq{%s: HTTP %s\n}, $name, $code;
|
||||||
|
$bad = 1 if $code ne q{201};
|
||||||
|
}
|
||||||
|
exit($bad ? 1 : 0);
|
||||||
|
'
|
||||||
|
|||||||
+82
-76
@@ -1,4 +1,17 @@
|
|||||||
# Test — gasm-devkit. Runs on push and pull request to development.
|
# Test, Go. Push and pull request to development. Never on main.
|
||||||
|
#
|
||||||
|
# The gates are the ones the justfile's `gates` recipe runs, minus race: the shared
|
||||||
|
# runner box cannot afford the race detector on every push, so it lives in race.yml.
|
||||||
|
# The box is one core and 2 GB beside Gitea, so parallelism is bounded on purpose and
|
||||||
|
# everything runs in one job. Extra jobs would duplicate the checkout, the Go setup and
|
||||||
|
# the dependency download three times without buying any parallelism.
|
||||||
|
#
|
||||||
|
# Every step is one command, so the step that fails is the gate that failed, and no shell
|
||||||
|
# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and
|
||||||
|
# not Python: Perl behaves the same on both runner images, there is no bashism to trip over
|
||||||
|
# on ash, and it is one language instead of two. The Perl uses builtins only, because
|
||||||
|
# Fedora packages the Perl modules separately and nothing beyond `perl` itself may be
|
||||||
|
# assumed present.
|
||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -7,90 +20,83 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [development]
|
branches: [development]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# One core: parallelism buys no speed here and costs memory the box does not have.
|
||||||
|
GOFLAGS: -p=1
|
||||||
|
GOMAXPROCS: "2"
|
||||||
|
|
||||||
|
# A superseded run of the same ref is cancelled instead of queueing behind one that
|
||||||
|
# no longer matters. Verified on Gitea 1.27.1 on 2026-09-17: a queued run whose ref
|
||||||
|
# moved on is cancelled before it ever reaches the runner, while a run already
|
||||||
|
# dispatched there runs to completion.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ gitea.workflow }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
vet:
|
|
||||||
runs-on: fedora
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version: "1.27"
|
|
||||||
|
|
||||||
- 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:
|
test:
|
||||||
runs-on: fedora
|
runs-on: fedora
|
||||||
needs: vet
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.27"
|
# The module is the source of truth for the version, so it cannot drift.
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
|
||||||
- name: Download dependencies
|
- name: Install Perl
|
||||||
run: go mod download
|
# The runner images are minimal and Perl is not guaranteed. The install is a
|
||||||
|
# no-op where it is already present; drop this step once verified on the box.
|
||||||
- name: Install gcc
|
run: dnf install -y perl
|
||||||
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.27"
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
|
# The steps follow the `gates` order of the justfile contract: build, format,
|
||||||
|
# vet, test. The vet gate is go vet and go fix -diff, two steps here.
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -ldflags="-s -w" -o bin/gasm ./cmd/gasm
|
run: go build ./...
|
||||||
|
|
||||||
- name: Smoke test
|
- name: Format
|
||||||
run: ./bin/gasm --version
|
run: |
|
||||||
|
perl -e '
|
||||||
|
open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!};
|
||||||
|
my @bad = <$g>;
|
||||||
|
close($g);
|
||||||
|
print @bad;
|
||||||
|
exit(@bad ? 1 : 0);
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Modernise
|
||||||
|
# Exits non-zero when it has something to rewrite, so it needs no output capture.
|
||||||
|
run: go fix -diff ./...
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
# The suite must be fast: a push pipeline that cannot finish in a few minutes moves
|
||||||
|
# its heavy part behind a dispatch. The inner timeout matches the job's, so a
|
||||||
|
# hanging test reports its own goroutine dump rather than a silent job kill.
|
||||||
|
# The pattern is `packages` in the project's justfile: the logic packages, since a
|
||||||
|
# thin cmd/ would drag the total under the floor. release.yml runs the same
|
||||||
|
# command, so the floor is the same number everywhere. ./verify/... carries the
|
||||||
|
# live oracle-parity comparison against `go tool asm` (the TestGroundTruth
|
||||||
|
# suites); the runner's Go setup provides both the tool and GOROOT.
|
||||||
|
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||||
|
|
||||||
|
- name: Oracle parity
|
||||||
|
# Re-run the live go-tool-asm comparison as its own step so that a parity
|
||||||
|
# regression names the gate that failed instead of hiding inside the suite.
|
||||||
|
run: go test -count=1 -timeout 10m -run 'TestGroundTruth' ./verify/...
|
||||||
|
|
||||||
|
- name: Coverage floor
|
||||||
|
run: |
|
||||||
|
perl -e '
|
||||||
|
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
|
||||||
|
my $total;
|
||||||
|
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
|
||||||
|
close($c);
|
||||||
|
die qq{no total line in coverage.out\n} unless defined $total;
|
||||||
|
printf qq{Total coverage: %s%%\n}, $total;
|
||||||
|
exit($total < 80 ? 1 : 0);
|
||||||
|
'
|
||||||
|
|||||||
+3
-14
@@ -1,24 +1,13 @@
|
|||||||
# Metadata (always first, per repo convention)
|
|
||||||
.idea/
|
.idea/
|
||||||
.zcode/
|
.zcode/
|
||||||
.mimocode/
|
|
||||||
|
|
||||||
# Binaries
|
# Build output
|
||||||
/gasm
|
|
||||||
/bin/
|
/bin/
|
||||||
*.exe
|
/gasm
|
||||||
|
|
||||||
# Test and coverage artefacts
|
|
||||||
coverage.out
|
coverage.out
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# Crash dumps
|
# Crash dumps from the emulator runs
|
||||||
core
|
core
|
||||||
core.*
|
core.*
|
||||||
*.core
|
*.core
|
||||||
|
|
||||||
# Scratch / temporary work
|
|
||||||
_scratch/
|
|
||||||
|
|
||||||
# ZCode workspace
|
|
||||||
.zcode
|
|
||||||
|
|||||||
+294
-139
@@ -3,15 +3,170 @@
|
|||||||
All notable changes to gasm-devkit are documented here.
|
All notable changes to gasm-devkit are documented here.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Conventional Commits](https://www.conventionalcommits.org/).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [development]
|
## [development]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
-
|
- **Indirect JMP and CALL on all four architectures.** `JMP AX`,
|
||||||
|
`CALL AX`, `JMP (BX)` and the memory forms encode at byte parity with
|
||||||
|
the toolchain (FF /2 and FF /4 on amd64); arm64 lowers `JMP (R0)` to
|
||||||
|
BR and accepts the raw BR/BLR spellings; riscv64 lowers `JMP (X5)` to
|
||||||
|
JALR; loong64 accepts the raw `JIRL rd, rj, off` spelling the Go
|
||||||
|
assembler cannot express. A frameless amd64 function containing a
|
||||||
|
CALL now receives the toolchain's forced base-pointer frame. The
|
||||||
|
verify trampolines join the ground-truth lists, and a lint check for
|
||||||
|
control flow through registers and memory extends to the new forms.
|
||||||
|
- **`gasm asm -GOARCH` and `gasm diff -GOARCH`.** The target
|
||||||
|
architecture can be named explicitly instead of inferred from the
|
||||||
|
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||||
|
`.s` files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||||
|
- **`gasm audit-instructions --corpus [dir]`.** Assembles every `.s`
|
||||||
|
file under a directory (default GOROOT/src) with the gasm encoder
|
||||||
|
only: suffixed files for their architecture, suffix-less files for
|
||||||
|
all four, as a GOARCH build would. Reports the headline number (108
|
||||||
|
of 627 GOROOT files, 17.2 %, assemble for every target architecture,
|
||||||
|
against 23 in the previous release), the per-architecture pass rates
|
||||||
|
and the most common failure reasons with a representative file each,
|
||||||
|
which drive the encodability backlog by frequency.
|
||||||
|
- **Fuzz targets for the parser and the formatter.** FuzzParse (no
|
||||||
|
panic, always a usable file) and FuzzFormatIdempotency (formatting
|
||||||
|
twice equals formatting once; clean input stays clean) seed
|
||||||
|
themselves from the repository's kernels, so the plain test suite
|
||||||
|
replays every seed in CI and `just fuzz` runs the mutation engine on
|
||||||
|
demand.
|
||||||
|
- **Oracle parity as its own CI step.** The push pipeline already ran
|
||||||
|
the live go-tool-asm comparison inside the suite; a dedicated step
|
||||||
|
now names that gate when it fails.
|
||||||
|
- **Man pages.** docs/man carries gasm(1) and one page per command,
|
||||||
|
written in roff: synopsis, description, every flag with its default,
|
||||||
|
exit status, worked examples and cross-references.
|
||||||
|
`just install-man` compresses them into ~/.local/share/man (MANDIR
|
||||||
|
overrides) and `just uninstall-man` removes them. A test builds the
|
||||||
|
binary and compares every command's `-h` output with its page, so the
|
||||||
|
pages cannot drift from the CLI.
|
||||||
|
|
||||||
## [0.32.0] — 2026-08-31
|
### Changed
|
||||||
|
|
||||||
|
- **Canonical just recipes.** `just gates` is the definition of done
|
||||||
|
(build, fmt-check, vet, test, race). `install` now builds and copies
|
||||||
|
the binary into `~/.local/bin` (`BINDIR` overrides) instead of
|
||||||
|
downloading module dependencies, and `install-bin` is gone. The test
|
||||||
|
gate sweeps the logic packages (arch through verify; the hardware-bound
|
||||||
|
`debug` and the thin `cmd/gasm` sit outside it), so the coverage floor
|
||||||
|
is computed over the product code and the number is identical locally
|
||||||
|
and in CI. `fuzz` requires its target package.
|
||||||
|
- **The reported version comes from the build.** `gasm --version`
|
||||||
|
prints the version the toolchain recorded: the tag on a tagged
|
||||||
|
checkout, a pseudo-version naming the commit below one, `+dirty` on a
|
||||||
|
dirty tree and `(devel)` outside version control. Nothing is
|
||||||
|
injected with `-ldflags -X` any more.
|
||||||
|
- **CI realigned with the gate set.** The push pipeline runs the gates
|
||||||
|
minus race in one job, in the `gates` order, with a cached Go setup and
|
||||||
|
the module as the version source; a superseded run of the same branch
|
||||||
|
is cancelled instead of queueing; every `go test` runs under a
|
||||||
|
ten-minute bound that matches its job's; the race detector moved to a
|
||||||
|
hand-dispatched workflow and runs in the local gate before a tag is
|
||||||
|
cut, never on a push or a tag; the release builds without injection and
|
||||||
|
its smoke test requires the recorded tag and rejects `+dirty`.
|
||||||
|
- **The documents follow the standard set.** `docs/ARCHITECTURE.md` is
|
||||||
|
organised as Overview, Packages, Data flow, State and lifetime and
|
||||||
|
Dependencies, and carries a sequence diagram of the assembly path;
|
||||||
|
`docs/DEVELOPMENT.md` lists every recipe in one table and documents the
|
||||||
|
coverage floor, the CI and the release flow; `docs/CLI.md` gives the
|
||||||
|
synopsis, the commands, every flag with its default, the exit codes and
|
||||||
|
worked examples; `CONTRIBUTING.md` carries the Contributor terms and
|
||||||
|
states the commit trailer form, the one-logical-change rule and the
|
||||||
|
licence header rule. The repository's own assembly (the `verify`
|
||||||
|
trampolines and the test kernels) is in `gasm fmt` canonical form.
|
||||||
|
- **The README states the project's purpose and status.** It opens with
|
||||||
|
a warning that the tool is an experiment under active development,
|
||||||
|
version 0.x.x, free to change without warning, with 1.0.0 far off,
|
||||||
|
and already in active use on real assembly work. It describes both
|
||||||
|
goals (tooling for Plan 9 assembly, and Plan 9 assembly outside the
|
||||||
|
Go toolchain), argues the case for the syntax in a new Why Plan 9
|
||||||
|
assembly section, and carries a Direction section: extended
|
||||||
|
instruction support, full GOOBJ and ELF compilation, Linux and
|
||||||
|
FreeBSD, and the four architectures.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **riscv64 JALR silently jumped to the wrong register.** The trampoline
|
||||||
|
form `JALR X0, 0(X5)` read the memory operand's base as the destination,
|
||||||
|
encoding a jump to X0 with no diagnostic; the destination is the first
|
||||||
|
operand. The leaf detection shared the confusion, so affected functions
|
||||||
|
also grew a bogus prologue. `JALR X0, 0(X1)` as written in the verify
|
||||||
|
trampoline was mis-encoded since its introduction.
|
||||||
|
- **DATA lines demanded their GLOBL first.** collectData processed the
|
||||||
|
declarations in file order, but the Plan 9 convention puts every DATA
|
||||||
|
line before its symbol's GLOBL; correctly ordered files (most of
|
||||||
|
GOROOT's) failed with "no matching GLOBL". Two passes: symbols are
|
||||||
|
registered before initialisers are applied.
|
||||||
|
- **The formatter lost idempotency on degenerate lines.** Illegal tokens
|
||||||
|
survived into the output, a label sharing its line with a
|
||||||
|
non-instruction split into a line the parser rejects, stray-operand
|
||||||
|
lines entered the alignment width computation, and rendered `/ *`,
|
||||||
|
`> >` sequences re-lexed as comments and shifts. The label, width and
|
||||||
|
spacing rules now agree between passes.
|
||||||
|
|
||||||
|
## [0.33.0] - 2026-09-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Stack-split guards in `gasm asm`.** Every framed function now gets
|
||||||
|
the morestack prologue check and the trailing morestack block
|
||||||
|
(`CALL runtime.morestack_noctxt`), byte-identical to the toolchain's
|
||||||
|
`stacksplit` output on all four architectures: the small, medium and
|
||||||
|
big frame classes, auto-NOSPLIT leaves, the materialised constants of
|
||||||
|
large frames (arm64 `R27`, riscv64 `X31`, loong64 `R30`) and the
|
||||||
|
arm64 extrasize rule. Assembled objects are therefore linkable for
|
||||||
|
split functions, not only `NOSPLIT` leaves.
|
||||||
|
- **`gasm dis`.** Standalone disassembly through `golang.org/x/arch`:
|
||||||
|
a `.s` file is assembled and listed per `TEXT` function with local
|
||||||
|
labels at their real offsets, or raw bytes from a file or stdin are
|
||||||
|
disassembled linearly (`-a` selects the architecture). The debugger
|
||||||
|
shares the same decoder instead of carrying its own.
|
||||||
|
- **`gasm fmt -l` and `-d`.** Check mode lists files whose formatting
|
||||||
|
differs; diff mode prints a unified diff from the project's own
|
||||||
|
LCS-based differ, with GNU header semantics.
|
||||||
|
- **LSP cross-file navigation.** Go-to-definition and find references
|
||||||
|
fall back from local labels to the `TEXT` functions of every open
|
||||||
|
document, and rename follows the same cross-file matching.
|
||||||
|
- **Large frame offsets on riscv64 and arm64.** Frame-relative loads
|
||||||
|
and stores beyond the signed 12-bit immediate range materialise the
|
||||||
|
address through the toolchain temp register (riscv64 `X31`, arm64
|
||||||
|
`R27`) instead of silently truncating the offset (riscv64) or
|
||||||
|
rejecting the instruction (arm64); arm64 frame sizes now add the
|
||||||
|
toolchain's extrasize exactly (+8 when the frame leaves an alignment
|
||||||
|
gap, +16 when it is already aligned).
|
||||||
|
- **Tail calls `JMP sym(SB)`** on all four architectures (amd64 `E9`,
|
||||||
|
arm64 `B`, riscv64 `JAL X0`, loong64 `B`) with the call relocation.
|
||||||
|
- **Live oracle-parity tests.** Kernel files covering every guard
|
||||||
|
class, large-offset pattern and tail call are assembled by gasm and
|
||||||
|
by the installed `go tool asm` and compared byte-for-byte on all four
|
||||||
|
architectures, alongside the existing pinned-byte tests.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The v0.32.0 review findings: the parser rejects malformed `TEXT`
|
||||||
|
frames and parses signed frame sizes; amd64 frame adjustments above
|
||||||
|
127 bytes encode with imm32; arm64 and loong64 relocation encodings
|
||||||
|
match the toolchain; the linter guards unnamed `TEXT` directives and
|
||||||
|
refreshes its textflag table; the LSP recovers from handler panics
|
||||||
|
and decodes client URIs; watchpoint slot state moved into the debug
|
||||||
|
session; dead verify code removed; em and en dashes replaced across
|
||||||
|
sources.
|
||||||
|
- `gasm asm --format goobj`: internal calls to `TEXT` symbols of the
|
||||||
|
same file resolve on every architecture (the reference check accepted
|
||||||
|
only the amd64 call kind).
|
||||||
|
- `gasm asm --format elf` on loong64: branch relocations now map to
|
||||||
|
`R_LARCH_B26` instead of falling into `R_LARCH_PCALA_HI20`.
|
||||||
|
- arm64 large-prologue `ADD`/`SUB` use the extended-register encoding
|
||||||
|
the toolchain picks, and the morestack block saves the link register
|
||||||
|
with the toolchain's `OR` form on loong64.
|
||||||
|
|
||||||
|
## [0.32.0] - 2026-08-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -162,7 +317,7 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
|||||||
outputs and operand strictness with `go tool asm`.
|
outputs and operand strictness with `go tool asm`.
|
||||||
- **asm help text.** Updated to list arm64 as a supported architecture.
|
- **asm help text.** Updated to list arm64 as a supported architecture.
|
||||||
|
|
||||||
## [0.31.1] — 2026-08-20
|
## [0.31.1] - 2026-08-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -170,9 +325,9 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
|||||||
because the version variables in `justfile` and `cmd/gasm/main.go` were not
|
because the version variables in `justfile` and `cmd/gasm/main.go` were not
|
||||||
bumped during the release commit.
|
bumped during the release commit.
|
||||||
|
|
||||||
## [0.31.0] — 2026-08-20
|
## [0.31.0] - 2026-08-20
|
||||||
|
|
||||||
The arm64 encoder (Phase 5 — complete) ships with ELF64 and GOOBJ emission,
|
The arm64 encoder (Phase 5; complete) ships with ELF64 and GOOBJ emission,
|
||||||
verified byte-for-byte against `GOARCH=arm64 go tool asm` and linked into a
|
verified byte-for-byte against `GOARCH=arm64 go tool asm` and linked into a
|
||||||
real `go build`. The encoder covers the full integer instruction set, FP
|
real `go build`. The encoder covers the full integer instruction set, FP
|
||||||
arithmetic, conditional select, CRC32, and the MOV pseudo-instruction with
|
arithmetic, conditional select, CRC32, and the MOV pseudo-instruction with
|
||||||
@@ -180,7 +335,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **arm64 encoder (Phase 5 — complete).** `gasm asm` can now assemble `_arm64.s`
|
- **arm64 encoder (Phase 5; complete).** `gasm asm` can now assemble `_arm64.s`
|
||||||
files: the AArch64 integer instruction set with the MOV pseudo-instruction and
|
files: the AArch64 integer instruction set with the MOV pseudo-instruction and
|
||||||
its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with
|
its immediate-constant expansions (MOVZ/MOVN/MOVK for wide immediates, ORR with
|
||||||
logical bitmask encoding for values like `$1`), data-processing (shifted
|
logical bitmask encoding for values like `$1`), data-processing (shifted
|
||||||
@@ -189,7 +344,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
|||||||
SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations),
|
SB/global symbol references (ADRP+ADD pairs with `R_ADDRARM64` relocations),
|
||||||
jump chain folding, and ELF64 emission (`gasm asm --format elf`). Ground-truth
|
jump chain folding, and ELF64 emission (`gasm asm --format elf`). Ground-truth
|
||||||
verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. Phase 5
|
verification against `GOARCH=arm64 go tool asm` matches byte-for-byte. Phase 5
|
||||||
(the other architectures — RISC-V, LoongArch, arm64) is now complete.
|
(the other architectures; RISC-V, LoongArch, arm64) is now complete.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -197,7 +352,7 @@ bitmask immediate encoding. The project now requires Go 1.27.
|
|||||||
The `R_DWTXTADDR_U4` relocation type is detected at runtime for backward
|
The `R_DWTXTADDR_U4` relocation type is detected at runtime for backward
|
||||||
compatibility.
|
compatibility.
|
||||||
|
|
||||||
## [0.30.0] — 2026-08-13
|
## [0.30.0] - 2026-08-13
|
||||||
|
|
||||||
The LoongArch encoder (Phase 5) ships with ELF64 and GOOBJ emission, verified
|
The LoongArch encoder (Phase 5) ships with ELF64 and GOOBJ emission, verified
|
||||||
byte-for-byte against `GOARCH=loong64 go tool asm` and linked into a real
|
byte-for-byte against `GOARCH=loong64 go tool asm` and linked into a real
|
||||||
@@ -222,7 +377,7 @@ tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
|||||||
- **GOOBJ DWARF symbols.** The GOOBJ emitters now write the per-function
|
- **GOOBJ DWARF symbols.** The GOOBJ emitters now write the per-function
|
||||||
DWARF symbols the linker requires (the subprogram DIE and the `.debug_line`
|
DWARF symbols the linker requires (the subprogram DIE and the `.debug_line`
|
||||||
program, byte-identical to `cmd/asm`'s), and the pc-value table deltas are
|
program, byte-identical to `cmd/asm`'s), and the pc-value table deltas are
|
||||||
in the architecture's MinLC units as the runtime expects — the amd64 link
|
in the architecture's MinLC units as the runtime expects; the amd64 link
|
||||||
test now genuinely substitutes the gasm object, and the amd64/loong64
|
test now genuinely substitutes the gasm object, and the amd64/loong64
|
||||||
end-to-end GOOBJ link tests pass.
|
end-to-end GOOBJ link tests pass.
|
||||||
- **RISC-V GOOBJ emission via the shared emitter.** RISC-V GOOBJ output is
|
- **RISC-V GOOBJ emission via the shared emitter.** RISC-V GOOBJ output is
|
||||||
@@ -291,7 +446,7 @@ tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
|||||||
`GOARCH=riscv64 go tool asm`.
|
`GOARCH=riscv64 go tool asm`.
|
||||||
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
||||||
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
||||||
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
|
the first. Watchpoint slots are now tracked in the `Session` (DR0-DR3);
|
||||||
`watch` picks the first free slot and reports an error if all four are in
|
`watch` picks the first free slot and reports an error if all four are in
|
||||||
use, and `unwatch <slot>` clears one (no argument clears all).
|
use, and `unwatch <slot>` clears one (no argument clears all).
|
||||||
|
|
||||||
@@ -303,7 +458,7 @@ tracks four hardware watchpoint slots, and the toolkit is Linux-only.
|
|||||||
disassembly at PC, memory-write, watchpoints, and source-line mapping are
|
disassembly at PC, memory-write, watchpoints, and source-line mapping are
|
||||||
all shipped.
|
all shipped.
|
||||||
|
|
||||||
## [0.29.0] — 2026-08-07
|
## [0.29.0] - 2026-08-07
|
||||||
|
|
||||||
RISC-V GOOBJ emission, YMM vector register display, named buffer allocation
|
RISC-V GOOBJ emission, YMM vector register display, named buffer allocation
|
||||||
in the debugger, two new CLI commands (`diff`, `profile`), go-to-definition in
|
in the debugger, two new CLI commands (`diff`, `profile`), go-to-definition in
|
||||||
@@ -313,30 +468,30 @@ new CLI commands. A signature-parser fix corrects grouped Go parameters.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **RISC-V GOOBJ emission** — `gasm asm --format goobj` for RISC-V produces
|
- **RISC-V GOOBJ emission**; `gasm asm --format goobj` for RISC-V produces
|
||||||
linkable Go objects with funcdata, pc-value tables, and RISC-V relocation
|
linkable Go objects with funcdata, pc-value tables, and RISC-V relocation
|
||||||
types (same format as amd64 GOOBJ, with the RISC-V architecture marker).
|
types (same format as amd64 GOOBJ, with the RISC-V architecture marker).
|
||||||
- **`gasm diff`** — compare the machine code of two assembly files byte-for-byte;
|
- **`gasm diff`**; compare the machine code of two assembly files byte-for-byte;
|
||||||
shows which functions differ and the first few differing bytes.
|
shows which functions differ and the first few differing bytes.
|
||||||
- **`gasm profile`** — show the basic-block structure of each function: labels,
|
- **`gasm profile`**; show the basic-block structure of each function: labels,
|
||||||
offsets, frame size, and NOSPLIT flag.
|
offsets, frame size, and NOSPLIT flag.
|
||||||
- **LSP go-to-definition** — `textDocument/definition` navigates from a label
|
- **LSP go-to-definition**; `textDocument/definition` navigates from a label
|
||||||
reference to its definition.
|
reference to its definition.
|
||||||
- **did-you-mean** — when the RISC-V assembler encounters an undefined label, it
|
- **did-you-mean**; when the RISC-V assembler encounters an undefined label, it
|
||||||
suggests the closest existing label using Levenshtein distance.
|
suggests the closest existing label using Levenshtein distance.
|
||||||
- **YMM vector register display** — `regs` in the debugger now shows YMM
|
- **YMM vector register display**; `regs` in the debugger now shows YMM
|
||||||
registers via `PTRACE_GETFPREGS` (falls back to XMM when XSAVE is unavailable).
|
registers via `PTRACE_GETFPREGS` (falls back to XMM when XSAVE is unavailable).
|
||||||
- **Named buffer allocation** — `gasm debug --buf name:size:pattern` allocates
|
- **Named buffer allocation**; `gasm debug --buf name:size:pattern` allocates
|
||||||
buffers in the debuggee filled with `zero`, `ones`, `seq`, or a hex pattern;
|
buffers in the debuggee filled with `zero`, `ones`, `seq`, or a hex pattern;
|
||||||
buffer pointers are placed into the argument block at the matching positions.
|
buffer pointers are placed into the argument block at the matching positions.
|
||||||
- **Crash input storage** — `FuzzResult.CrashInput` stores the input that caused
|
- **Crash input storage**; `FuzzResult.CrashInput` stores the input that caused
|
||||||
a crash or mismatch for reproducibility.
|
a crash or mismatch for reproducibility.
|
||||||
- **ABI + fuzz combined** — `gasm verify --fuzz` now runs ABI checks (sentinel
|
- **ABI + fuzz combined**; `gasm verify --fuzz` now runs ABI checks (sentinel
|
||||||
registers, canary, stack bounds) alongside differential fuzz testing.
|
registers, canary, stack bounds) alongside differential fuzz testing.
|
||||||
- **`gasm diff --map`** — compare functions whose names differ between files
|
- **`gasm diff --map`**; compare functions whose names differ between files
|
||||||
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs two variants regardless
|
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs two variants regardless
|
||||||
of suffix). Unmapped functions fall back to the original name match.
|
of suffix). Unmapped functions fall back to the original name match.
|
||||||
- **`gasm verify --call`** — invoke a single function with user-supplied buffers
|
- **`gasm verify --call`**; invoke a single function with user-supplied buffers
|
||||||
(`--buf name:size:pattern`) instead of the smoke/abi/fuzz sweeps. Patterns:
|
(`--buf name:size:pattern`) instead of the smoke/abi/fuzz sweeps. Patterns:
|
||||||
`zero`, `ones`, `seq`, or a hex blob. Useful for partial functions (e.g.
|
`zero`, `ones`, `seq`, or a hex blob. Useful for partial functions (e.g.
|
||||||
decoders) that crash on random input but should succeed on valid data.
|
decoders) that crash on random input but should succeed on valid data.
|
||||||
@@ -346,23 +501,23 @@ new CLI commands. A signature-parser fix corrects grouped Go parameters.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Signature parser** — grouped Go parameters like `dst, src []byte` are now
|
- **Signature parser**; grouped Go parameters like `dst, src []byte` are now
|
||||||
parsed correctly (both get type `[]byte`). Previously the first name was
|
parsed correctly (both get type `[]byte`). Previously the first name was
|
||||||
treated as its own type (`dst` with size 8), causing wrong ABI0 arg-block
|
treated as its own type (`dst` with size 8), causing wrong ABI0 arg-block
|
||||||
layout in both `verify --call` and the fuzzer.
|
layout in both `verify --call` and the fuzzer.
|
||||||
- **Flaky JIT tests** — `runtime.KeepAlive` guards and package-level buffers
|
- **Flaky JIT tests**; `runtime.KeepAlive` guards and package-level buffers
|
||||||
prevent GC from collecting heap objects whose addresses were passed to JIT
|
prevent GC from collecting heap objects whose addresses were passed to JIT
|
||||||
code via `unsafe.Pointer`; all verify tests pass 100/100 under `-race`.
|
code via `unsafe.Pointer`; all verify tests pass 100/100 under `-race`.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Removed external kernel test dependencies** — the verify test suite no
|
- **Removed external kernel test dependencies**; the verify test suite no
|
||||||
longer references production kernels from the separate go-libraries project.
|
longer references production kernels from the separate go-libraries project.
|
||||||
The remaining test suite uses only `testdata/verify/*.s` kernels, which are
|
The remaining test suite uses only `testdata/verify/*.s` kernels, which are
|
||||||
part of this repository. Coverage is identical locally and in CI (80.3 %).
|
part of this repository. Coverage is identical locally and in CI (80.3 %).
|
||||||
|
|
||||||
|
|
||||||
## [0.28.0] — 2026-08-03
|
## [0.28.0] - 2026-08-03
|
||||||
|
|
||||||
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
||||||
pseudo-instruction, SB/global symbol references, ELF64 object emission, and
|
pseudo-instruction, SB/global symbol references, ELF64 object emission, and
|
||||||
@@ -370,20 +525,20 @@ ground-truth verification against `GOARCH=riscv64 go tool asm`.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **RISC-V encoder** — RV64I, RV64M, RV64A, RV64F/D, FMA, CSR, JALR.
|
- **RISC-V encoder**; RV64I, RV64M, RV64A, RV64F/D, FMA, CSR, JALR.
|
||||||
- **MOV pseudo-instruction** — load, store, reg-to-reg, immediate, frame mapping.
|
- **MOV pseudo-instruction**; load, store, reg-to-reg, immediate, frame mapping.
|
||||||
- **RVC compression** — 22 compressed instruction types (C.LDSP, C.SDSP, C.FLDSP,
|
- **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.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).
|
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)`
|
- **SB/global symbols**; `MOV $sym(SB)`, `MOV sym(SB)`, `MOV rd, sym(SB)`
|
||||||
encoded as AUIPC pairs with R_RISCV_PCREL_HI20/LO12 relocations.
|
encoded as AUIPC pairs with R_RISCV_PCREL_HI20/LO12 relocations.
|
||||||
- **GLOBL/DATA** — data section layout in `AssembleFileRISCV`.
|
- **GLOBL/DATA**; data section layout in `AssembleFileRISCV`.
|
||||||
- **ELF64 emission** — `gasm asm --format elf` produces EM_RISCV objects
|
- **ELF64 emission**; `gasm asm --format elf` produces EM_RISCV objects
|
||||||
(.text, .data, .symtab, .rela.text).
|
(.text, .data, .symtab, .rela.text).
|
||||||
- **`gasm verify --ground-truth`** — byte-exact comparison against
|
- **`gasm verify --ground-truth`**; byte-exact comparison against
|
||||||
`GOARCH=riscv64 go tool asm`.
|
`GOARCH=riscv64 go tool asm`.
|
||||||
- **`gasm verify --profile`** — function layout listing for RISC-V.
|
- **`gasm verify --profile`**; function layout listing for RISC-V.
|
||||||
- **CALL** — AUIPC + JALR pair encoding.
|
- **CALL**; AUIPC + JALR pair encoding.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -393,7 +548,7 @@ ground-truth verification against `GOARCH=riscv64 go tool asm`.
|
|||||||
(bit-interleaved format).
|
(bit-interleaved format).
|
||||||
|
|
||||||
|
|
||||||
## [0.27.0] — 2026-08-01
|
## [0.27.0] - 2026-08-01
|
||||||
|
|
||||||
Subprocess isolation for `--fuzz`: each function is fuzzed in its own child
|
Subprocess isolation for `--fuzz`: each function is fuzzed in its own child
|
||||||
process, so a partial function (decoder) that faults on random garbage is
|
process, so a partial function (decoder) that faults on random garbage is
|
||||||
@@ -404,7 +559,7 @@ the parent. CRASH is informational (exit 0); only MISMATCH is an error.
|
|||||||
|
|
||||||
- `gasm verify --fuzz` no longer crashes the process on partial functions.
|
- `gasm verify --fuzz` no longer crashes the process on partial functions.
|
||||||
|
|
||||||
## [0.26.0] — 2026-07-31
|
## [0.26.0] - 2026-07-31
|
||||||
|
|
||||||
Universal differential fuzzing: `gasm verify --fuzz` needs no hand-written
|
Universal differential fuzzing: `gasm verify --fuzz` needs no hand-written
|
||||||
reference. It parses the `// func` signature from the assembly source,
|
reference. It parses the `// func` signature from the assembly source,
|
||||||
@@ -415,7 +570,7 @@ area bit-for-bit.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: `FuzzFunc` / `ExtractSignatures` / `parseFuncSig` — universal
|
- `verify`: `FuzzFunc` / `ExtractSignatures` / `parseFuncSig`; universal
|
||||||
differential fuzz driven by the conventional `// func` comment. Each
|
differential fuzz driven by the conventional `// func` comment. Each
|
||||||
version gets its own buffer set (deep copy) so functions that write to
|
version gets its own buffer set (deep copy) so functions that write to
|
||||||
their arguments (histogram increments) don't corrupt the other's input.
|
their arguments (histogram increments) don't corrupt the other's input.
|
||||||
@@ -430,16 +585,16 @@ area bit-for-bit.
|
|||||||
over-copy paths read past the buffer on random garbage input. Subprocess
|
over-copy paths read past the buffer on random garbage input. Subprocess
|
||||||
isolation (fork per function) is planned. Use `--ground-truth` for decoders.
|
isolation (fork per function) is planned. Use `--ground-truth` for decoders.
|
||||||
|
|
||||||
## [0.25.0] — 2026-07-30
|
## [0.25.0] - 2026-07-30
|
||||||
|
|
||||||
Universal ground-truth verification: `gasm verify --ground-truth` assembles
|
Universal ground-truth verification: `gasm verify --ground-truth` assembles
|
||||||
any `.s` file with both gasm and `go tool asm`, then compares the machine
|
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
|
code byte-for-byte per function (relocation sites masked). No hand-written
|
||||||
reference needed — the Go toolchain IS the oracle.
|
reference needed; the Go toolchain IS the oracle.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: `GroundTruth` — shells out to `go tool asm`, parses the GOOBJ
|
- `verify`: `GroundTruth`; shells out to `go tool asm`, parses the GOOBJ
|
||||||
output (minimal reader: block offsets, nonpkg symbol table, data index)
|
output (minimal reader: block offsets, nonpkg symbol table, data index)
|
||||||
and returns per-function code bytes.
|
and returns per-function code bytes.
|
||||||
- `gasm verify --ground-truth`: compares gasm's output against the Go
|
- `gasm verify --ground-truth`: compares gasm's output against the Go
|
||||||
@@ -448,11 +603,11 @@ reference needed — the Go toolchain IS the oracle.
|
|||||||
linker fills) are masked before comparison.
|
linker fills) are masked before comparison.
|
||||||
- Verified: go-lz4 AVX2 2/2, go-flac AVX2 17/17 functions byte-identical.
|
- Verified: go-lz4 AVX2 2/2, go-flac AVX2 17/17 functions byte-identical.
|
||||||
|
|
||||||
## [0.24.0] — 2026-07-29
|
## [0.24.0] - 2026-07-29
|
||||||
|
|
||||||
The full analyze family and stereo PCM decode are now differentially tested.
|
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
|
15 of 17 go-flac AVX2 kernels have bit-for-bit differential coverage; the
|
||||||
two remaining (autocorrAVX2 — FMA reassociation, lpcResidualAVX2 — complex
|
two remaining (autocorrAVX2: FMA reassociation, lpcResidualAVX2: complex
|
||||||
multi-arg) are deferred.
|
multi-arg) are deferred.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -462,7 +617,7 @@ multi-arg) are deferred.
|
|||||||
- `verify`: `decodeStereo16AVX2` differential test (500 random interleaved
|
- `verify`: `decodeStereo16AVX2` differential test (500 random interleaved
|
||||||
stereo PCM buffers, both channels compared sample-by-sample).
|
stereo PCM buffers, both channels compared sample-by-sample).
|
||||||
|
|
||||||
## [0.23.0] — 2026-07-28
|
## [0.23.0] - 2026-07-28
|
||||||
|
|
||||||
The analyze family and 24-bit PCM decode join the differential suite.
|
The analyze family and 24-bit PCM decode join the differential suite.
|
||||||
|
|
||||||
@@ -474,7 +629,7 @@ The analyze family and 24-bit PCM decode join the differential suite.
|
|||||||
- `verify`: `decodeMono24AVX2` differential test (500 random 24-bit PCM
|
- `verify`: `decodeMono24AVX2` differential test (500 random 24-bit PCM
|
||||||
buffers, sign-extension compared sample-by-sample).
|
buffers, sign-extension compared sample-by-sample).
|
||||||
|
|
||||||
## [0.22.0] — 2026-07-27
|
## [0.22.0] - 2026-07-27
|
||||||
|
|
||||||
The remaining go-flac encoder kernels join the differential suite.
|
The remaining go-flac encoder kernels join the differential suite.
|
||||||
|
|
||||||
@@ -488,39 +643,39 @@ The remaining go-flac encoder kernels join the differential suite.
|
|||||||
loop).
|
loop).
|
||||||
|
|
||||||
|
|
||||||
## [0.21.0] — 2026-07-26
|
## [0.21.0] - 2026-07-26
|
||||||
|
|
||||||
Differential testing extended to all four production kernels and the CLI
|
Differential testing extended to all four production kernels and the CLI
|
||||||
exposes the full dynamic-analysis toolkit.
|
exposes the full dynamic-analysis toolkit.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: go-flac AVX2 differential tests — `decodeMono16AVX2` (500
|
- `verify`: go-flac AVX2 differential tests; `decodeMono16AVX2` (500
|
||||||
random PCM buffers), `pack16AVX2` (500 random int32→int16 packings) and
|
random PCM buffers), `pack16AVX2` (500 random int32→int16 packings) and
|
||||||
all four decorrelation kernels (200 iterations each: left-side, side-right,
|
all four decorrelation kernels (200 iterations each: left-side, side-right,
|
||||||
mid-side, interleave) compared bit-for-bit against the portable Go
|
mid-side, interleave) compared bit-for-bit against the portable Go
|
||||||
references.
|
references.
|
||||||
- `verify`: go-lz4 AVX-512 differential tests — `decodeBlockAVX512` (3 000
|
- `verify`: go-lz4 AVX-512 differential tests; `decodeBlockAVX512` (3 000
|
||||||
fuzzed LZ4 blocks + known answers) and `wideCopyAVX512` (0–1024 bytes)
|
fuzzed LZ4 blocks + known answers) and `wideCopyAVX512` (0-1024 bytes)
|
||||||
against the same portable oracle as the AVX2 suite.
|
against the same portable oracle as the AVX2 suite.
|
||||||
- `gasm verify --abi`: runs each NOSPLIT function with sentinel registers
|
- `gasm verify --abi`: runs each NOSPLIT function with sentinel registers
|
||||||
and a red-zone canary, reporting violations.
|
and a red-zone canary, reporting violations.
|
||||||
- `gasm verify --profile`: lists the static basic-block count per function.
|
- `gasm verify --profile`: lists the static basic-block count per function.
|
||||||
|
|
||||||
## [0.20.0] — 2026-07-25
|
## [0.20.0] - 2026-07-25
|
||||||
|
|
||||||
Coverage profiling: the third pillar of Phase 3. Static basic-block
|
Coverage profiling: the third pillar of Phase 3. Static basic-block
|
||||||
enumeration from the assembler's label map, combined with multi-input path
|
enumeration from the assembler's label map, combined with multi-input path
|
||||||
diversity measurement — how many observationally distinct execution paths a
|
diversity measurement; how many observationally distinct execution paths a
|
||||||
test corpus exercises.
|
test corpus exercises.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: `Kernel.Blocks` / `Kernel.BlockCount` — enumerate basic blocks
|
- `verify`: `Kernel.Blocks` / `Kernel.BlockCount`; enumerate basic blocks
|
||||||
from the assembler's local-label map (every jump target is a block
|
from the assembler's local-label map (every jump target is a block
|
||||||
boundary; the function entry is always a block). `decodeBlockAVX2` has
|
boundary; the function entry is always a block). `decodeBlockAVX2` has
|
||||||
27 blocks.
|
27 blocks.
|
||||||
- `verify`: `Kernel.ProfilePaths` — run the function with a corpus of
|
- `verify`: `Kernel.ProfilePaths`; run the function with a corpus of
|
||||||
argument blocks and collect distinct output fingerprints (the result
|
argument blocks and collect distinct output fingerprints (the result
|
||||||
words); reports path diversity as a lower bound on code coverage.
|
words); reports path diversity as a lower bound on code coverage.
|
||||||
|
|
||||||
@@ -532,7 +687,7 @@ 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
|
approach delivers the project's goal (proving the SIMD path and tail handling
|
||||||
execute) without fighting the runtime.
|
execute) without fighting the runtime.
|
||||||
|
|
||||||
## [0.19.0] — 2026-07-24
|
## [0.19.0] - 2026-07-24
|
||||||
|
|
||||||
Runtime ABI checks: the second pillar of Phase 3. The JIT trampoline now
|
Runtime ABI checks: the second pillar of Phase 3. The JIT trampoline now
|
||||||
has an ABI-checking variant that sets sentinels in the callee-saved registers
|
has an ABI-checking variant that sets sentinels in the callee-saved registers
|
||||||
@@ -542,41 +697,41 @@ detects any illegal write below the stack pointer.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: `CallChecked` / `Kernel.CallFuncChecked` — ABI-checking JIT call
|
- `verify`: `CallChecked` / `Kernel.CallFuncChecked`; ABI-checking JIT call
|
||||||
with sentinel registers and red-zone canary; returns an `ABIReport`
|
with sentinel registers and red-zone canary; returns an `ABIReport`
|
||||||
(BPClobbered, R14Clobbered, RedZoneHit).
|
(BPClobbered, R14Clobbered, RedZoneHit).
|
||||||
- `verify`: the raw `leaveJITCheckedRaw` trampoline — a TEXT symbol with no
|
- `verify`: the raw `leaveJITCheckedRaw` trampoline; a TEXT symbol with no
|
||||||
ABIInternal wrapper (address obtained via GLOBL/DATA), so the JIT
|
ABIInternal wrapper (address obtained via GLOBL/DATA), so the JIT
|
||||||
function's RET lands directly in the check code and sees the registers
|
function's RET lands directly in the check code and sees the registers
|
||||||
exactly as the function left them.
|
exactly as the function left them.
|
||||||
- Tests: deliberate BP/R14 clobberers detected; both go-lz4 kernels
|
- Tests: deliberate BP/R14 clobberers detected; both go-lz4 kernels
|
||||||
confirmed ABI-clean (BP preserved, R14 preserved, red zone intact).
|
confirmed ABI-clean (BP preserved, R14 preserved, red zone intact).
|
||||||
|
|
||||||
## [0.18.0] — 2026-07-23
|
## [0.18.0] - 2026-07-23
|
||||||
|
|
||||||
Differential testing: the JIT-assembled go-lz4 `decodeBlockAVX2` kernel is
|
Differential testing: the JIT-assembled go-lz4 `decodeBlockAVX2` kernel is
|
||||||
fuzzed against a portable Go reference — 5 000 valid LZ4 blocks compared
|
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
|
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.
|
codes. This is the automated form of the project's bit-identical contract.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify`: differential fuzz tests — a random LZ4 block generator produces
|
- `verify`: differential fuzz tests; a random LZ4 block generator produces
|
||||||
valid blocks (literals, overlapping matches, extension bytes) and the
|
valid blocks (literals, overlapping matches, extension bytes) and the
|
||||||
JIT-assembled kernel's output is compared byte-for-byte against a portable
|
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
|
Go decoder; a hostile-input suite confirms error-code agreement on random
|
||||||
garbage (no crashes, same classification).
|
garbage (no crashes, same classification).
|
||||||
|
|
||||||
## [0.17.0] — 2026-07-22
|
## [0.17.0] - 2026-07-22
|
||||||
|
|
||||||
Phase 3 begins: dynamic analysis. A JIT execution substrate that assembles
|
Phase 3 begins: dynamic analysis. A JIT execution substrate that assembles
|
||||||
Plan 9 amd64 kernels into executable memory and calls them directly — pure Go
|
Plan 9 amd64 kernels into executable memory and calls them directly; pure Go
|
||||||
(stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external
|
(stdlib only, `syscall.Mmap` + an assembly trampoline), no cgo, no external
|
||||||
toolchain.
|
toolchain.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `verify` package: JIT infrastructure — `Map` copies machine code into a
|
- `verify` package: JIT infrastructure; `Map` copies machine code into a
|
||||||
W^X memory mapping, `Call` invokes it through an ABI0 trampoline that
|
W^X memory mapping, `Call` invokes it through an ABI0 trampoline that
|
||||||
switches to a prepared stack and back. `Load`/`LoadSource`/`LoadAST`
|
switches to a prepared stack and back. `Load`/`LoadSource`/`LoadAST`
|
||||||
parse, assemble and map a `.s` file in one step; `Kernel.CallFunc`
|
parse, assemble and map a `.s` file in one step; `Kernel.CallFunc`
|
||||||
@@ -585,34 +740,34 @@ toolchain.
|
|||||||
available functions; with `-smoke`, calls each NOSPLIT function with
|
available functions; with `-smoke`, calls each NOSPLIT function with
|
||||||
zeroed arguments to confirm the trampoline works end-to-end.
|
zeroed arguments to confirm the trampoline works end-to-end.
|
||||||
- Integration tests: the go-lz4 `decodeBlockAVX2` and `wideCopyAVX2`
|
- Integration tests: the go-lz4 `decodeBlockAVX2` and `wideCopyAVX2`
|
||||||
kernels (699 and 146 bytes) assemble, map and execute correctly —
|
kernels (699 and 146 bytes) assemble, map and execute correctly;
|
||||||
known-answer LZ4 blocks decode bit-for-bit, wide copies of 0–1024 bytes
|
known-answer LZ4 blocks decode bit-for-bit, wide copies of 0-1024 bytes
|
||||||
match, malformed input returns the correct error codes.
|
match, malformed input returns the correct error codes.
|
||||||
|
|
||||||
|
|
||||||
## [0.16.0] — 2026-07-21
|
## [0.16.0] - 2026-07-21
|
||||||
|
|
||||||
The scalar conversions between vector and general-purpose registers — the
|
The scalar conversions between vector and general-purpose registers; the
|
||||||
last of the amd64 EVEX instruction set.
|
last of the amd64 EVEX instruction set.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the GPR-interchanging conversions, byte for byte against the Go
|
- `asm`: the GPR-interchanging conversions, byte for byte against the Go
|
||||||
assembler (28 ground-truth cases including memory sources and extended
|
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}
|
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}
|
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 only); GPR to vector; VCVTSI2SD{L,Q}/VCVTSI2SS{L,Q} (VEX and
|
||||||
EVEX) and VCVTUSI2SD{L,Q}/VCVTUSI2SS{L,Q} (EVEX only), whose preserved
|
EVEX) and VCVTUSI2SD{L,Q}/VCVTUSI2SS{L,Q} (EVEX only), whose preserved
|
||||||
vector source sits in vvvv (three Plan 9 operands).
|
vector source sits in vvvv (three Plan 9 operands).
|
||||||
|
|
||||||
## [0.15.0] — 2026-07-20
|
## [0.15.0] - 2026-07-20
|
||||||
|
|
||||||
The last of the EVEX conversions and narrowing/extending moves — the EVEX
|
The last of the EVEX conversions and narrowing/extending moves; the EVEX
|
||||||
instruction set is now complete save for the GPR-interchanging forms.
|
instruction set is now complete save for the GPR-interchanging forms.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the unsigned and truncating conversions — VCVTPD2PS (and the X/Y
|
- `asm`: the unsigned and truncating conversions; VCVTPD2PS (and the X/Y
|
||||||
spellings, whose length the spelling fixes), VCVTPD2UDQ (X/Y),
|
spellings, whose length the spelling fixes), VCVTPD2UDQ (X/Y),
|
||||||
VCVTTPD2UDQ (X/Y), VCVTTPD2UQQ, VCVTPS2UDQ, VCVTTPS2UDQ, VCVTPS2UQQ,
|
VCVTTPD2UDQ (X/Y), VCVTTPD2UQQ, VCVTPS2UDQ, VCVTTPS2UDQ, VCVTPS2UQQ,
|
||||||
VCVTTPS2UQQ, VCVTTPD2QQ, VCVTTPS2QQ, VCVTUQQ2PD, VCVTUQQ2PS (X/Y) and
|
VCVTTPS2UQQ, VCVTTPD2QQ, VCVTTPS2QQ, VCVTUQQ2PD, VCVTUQQ2PS (X/Y) and
|
||||||
@@ -625,20 +780,20 @@ instruction set is now complete save for the GPR-interchanging forms.
|
|||||||
D2M/Q2M), whose K register is a genuine operand rather than a mask and
|
D2M/Q2M), whose K register is a genuine operand rather than a mask and
|
||||||
which therefore take no masking suffixes.
|
which therefore take no masking suffixes.
|
||||||
|
|
||||||
## [0.14.0] — 2026-07-19
|
## [0.14.0] - 2026-07-19
|
||||||
|
|
||||||
The floating-point helper and conversion tail of the AVX-512 set, plus
|
The floating-point helper and conversion tail of the AVX-512 set, plus
|
||||||
gather and scatter with VSIB addressing — every encoding verified byte for
|
gather and scatter with VSIB addressing; every encoding verified byte for
|
||||||
byte against the Go assembler.
|
byte against the Go assembler.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the floating-point helpers — reciprocals and reciprocal square
|
- `asm`: the floating-point helpers; reciprocals and reciprocal square
|
||||||
roots (VRCP14/VRSQRT14 PD/PS/SD/SS), exponents and mantissas (VGETEXP*,
|
roots (VRCP14/VRSQRT14 PD/PS/SD/SS), exponents and mantissas (VGETEXP*,
|
||||||
VGETMANT*), scaling by powers of two (VSCALEF*), rounding (VRNDSCALE*),
|
VGETMANT*), scaling by powers of two (VSCALEF*), rounding (VRNDSCALE*),
|
||||||
reduction (VREDUCE*), immediate fixup (VFIXUPIMM*) and range selection
|
reduction (VREDUCE*), immediate fixup (VFIXUPIMM*) and range selection
|
||||||
(VRANGE*), and floating-point class tests (VFPCLASSPD/PS X/Y/Z and
|
(VRANGE*), and floating-point class tests (VFPCLASSPD/PS X/Y/Z and
|
||||||
VFPCLASSSD/SS — a new immediate form whose reg field carries the opmask
|
VFPCLASSSD/SS; a new immediate form whose reg field carries the opmask
|
||||||
destination).
|
destination).
|
||||||
- `asm`: **gather and scatter with VSIB addressing.** The gathers take
|
- `asm`: **gather and scatter with VSIB addressing.** The gathers take
|
||||||
both Go spellings: the VEX form with a vector mask register (OP mask,
|
both Go spellings: the VEX form with a vector mask register (OP mask,
|
||||||
@@ -647,14 +802,14 @@ byte against the Go assembler.
|
|||||||
data register (a ZMM index with an YMM destination encodes L'L = 10, as
|
data register (a ZMM index with an YMM destination encodes L'L = 10, as
|
||||||
the Go assembler emits). The scatters (VSCATTER*/VPSCATTER*) are EVEX
|
the Go assembler emits). The scatters (VSCATTER*/VPSCATTER*) are EVEX
|
||||||
only (OP src, K, vsib). All eight gather and eight scatter widths.
|
only (OP src, K, vsib). All eight gather and eight scatter widths.
|
||||||
- `asm`: the remaining conversions — VCVTQQ2PS (the 512-bit source sets
|
- `asm`: the remaining conversions; VCVTQQ2PS (the 512-bit source sets
|
||||||
the length), VCVTPD2QQ/UQQ, VCVTPS2QQ, VCVTUDQ2PD/PS, the half-precision
|
the length), VCVTPD2QQ/UQQ, VCVTPS2QQ, VCVTUDQ2PD/PS, the half-precision
|
||||||
VCVTPH2PS and VCVTPS2PH (the extract layout with an immediate).
|
VCVTPH2PS and VCVTPS2PH (the extract layout with an immediate).
|
||||||
|
|
||||||
## [0.13.0] — 2026-07-18
|
## [0.13.0] - 2026-07-18
|
||||||
|
|
||||||
The wider AVX-512 set: ternary logic, permutes, compares, expand/compress,
|
The wider AVX-512 set: ternary logic, permutes, compares, expand/compress,
|
||||||
the opmask instructions and the EVEX rounding/SAE/broadcast suffixes — every
|
the opmask instructions and the EVEX rounding/SAE/broadcast suffixes; every
|
||||||
encoding verified byte for byte against the Go assembler.
|
encoding verified byte for byte against the Go assembler.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -662,7 +817,7 @@ encoding verified byte for byte against the Go assembler.
|
|||||||
- `asm`: the wider EVEX/AVX-512 set, across roughly sixty new ground-truth
|
- `asm`: the wider EVEX/AVX-512 set, across roughly sixty new ground-truth
|
||||||
cases: ternary logic (VPTERNLOGD/Q), the lane shuffles/inserts/extracts
|
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}
|
(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 —
|
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
|
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
|
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
|
(VPMADDWD/UBSW, VPMULHUW, VPACKSSWB/USWB/SSDW/USDW, VPABS B/W/D/Q, the
|
||||||
@@ -676,15 +831,15 @@ encoding verified byte for byte against the Go assembler.
|
|||||||
(VMOVSLDUP/VMOVSHDUP), the conversions (VCVTPS2DQ, VCVTTPS2DQ) and the
|
(VMOVSLDUP/VMOVSHDUP), the conversions (VCVTPS2DQ, VCVTTPS2DQ) and the
|
||||||
remaining extending and narrowing moves (VPMOVSXBW, VPMOVZXBW, VPMOVWB,
|
remaining extending and narrowing moves (VPMOVSXBW, VPMOVZXBW, VPMOVWB,
|
||||||
VPMOVQB).
|
VPMOVQB).
|
||||||
- `asm`: the EVEX mnemonic suffixes the Go assembler accepts — the rounding
|
- `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
|
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
|
rounding control in L'L), suppress-all-exceptions `.SAE`, and memory
|
||||||
broadcast `.BCST` (the b bit, the vector length preserved, disp8×N scaled
|
broadcast `.BCST` (the b bit, the vector length preserved, disp8×N scaled
|
||||||
by the element size) — each combinable with the `.Z` zeroing suffix,
|
by the element size); each combinable with the `.Z` zeroing suffix,
|
||||||
validated against the Go assembler's bytes, and rejected on instructions
|
validated against the Go assembler's bytes, and rejected on instructions
|
||||||
that do not support them.
|
that do not support them.
|
||||||
|
|
||||||
## [0.12.0] — 2026-07-17
|
## [0.12.0] - 2026-07-17
|
||||||
|
|
||||||
GOOBJ emission: gasm-assembled functions drop into a `go build` without the
|
GOOBJ emission: gasm-assembled functions drop into a `go build` without the
|
||||||
Go assembler.
|
Go assembler.
|
||||||
@@ -692,7 +847,7 @@ Go assembler.
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: **GOOBJ object output.** `gasm asm --format goobj -p <pkgpath>`
|
- `asm`: **GOOBJ object output.** `gasm asm --format goobj -p <pkgpath>`
|
||||||
writes the Go toolchain's own object format — the one `cmd/link` consumes
|
writes the Go toolchain's own object format; the one `cmd/link` consumes
|
||||||
directly: the functions as non-package symbols qualified with the package
|
directly: the functions as non-package symbols qualified with the package
|
||||||
path (exactly as `cmd/asm` records assembly symbols), the `GLOBL` data,
|
path (exactly as `cmd/asm` records assembly symbols), the `GLOBL` data,
|
||||||
one serialized `FuncInfo` per function (argument/frame sizes, the asm
|
one serialized `FuncInfo` per function (argument/frame sizes, the asm
|
||||||
@@ -701,8 +856,8 @@ Go assembler.
|
|||||||
real stack deltas: the assembler now tracks every stack-adjustment
|
real stack deltas: the assembler now tracks every stack-adjustment
|
||||||
boundary through the prologue (`PUSHQ BP`, `SUBQ $frame, SP`) and each
|
boundary through the prologue (`PUSHQ BP`, `SUBQ $frame, SP`) and each
|
||||||
`RET`'s epilogue, so frame-pointer functions unwind correctly. The
|
`RET`'s epilogue, so frame-pointer functions unwind correctly. The
|
||||||
object preamble — the version-and-experiment header the linker compares
|
object preamble; the version-and-experiment header the linker compares
|
||||||
verbatim — is captured from the installed `go tool asm`, so the output is
|
verbatim; is captured from the installed `go tool asm`, so the output is
|
||||||
always consistent with the toolchain that links it.
|
always consistent with the toolchain that links it.
|
||||||
- `asm`: relocations against file-local `GLOBL` symbols become `R_PCREL`
|
- `asm`: relocations against file-local `GLOBL` symbols become `R_PCREL`
|
||||||
entries in the GOOBJ output, with the instruction's displacement field
|
entries in the GOOBJ output, with the instruction's displacement field
|
||||||
@@ -715,7 +870,7 @@ Go assembler.
|
|||||||
pattern, instead of being rejected as non-integer.
|
pattern, instead of being rejected as non-integer.
|
||||||
|
|
||||||
|
|
||||||
## [0.11.0] — 2026-07-16
|
## [0.11.0] - 2026-07-16
|
||||||
|
|
||||||
Linkable object output: external symbols and relocatable ELF / Mach-O
|
Linkable object output: external symbols and relocatable ELF / Mach-O
|
||||||
objects.
|
objects.
|
||||||
@@ -734,7 +889,7 @@ objects.
|
|||||||
external symbol; the Mach-O output is verified structurally with
|
external symbol; the Mach-O output is verified structurally with
|
||||||
`debug/macho`.
|
`debug/macho`.
|
||||||
- `asm`: **external symbol references.** A reference to a symbol no
|
- `asm`: **external symbol references.** A reference to a symbol no
|
||||||
`GLOBL` in the file defines no longer aborts assembly — it is recorded
|
`GLOBL` in the file defines no longer aborts assembly; it is recorded
|
||||||
as an external relocation (`Image.Externals`, `FuncLayout.Relocs`) and
|
as an external relocation (`Image.Externals`, `FuncLayout.Relocs`) and
|
||||||
becomes an undefined global symbol in the object output. The raw image
|
becomes an undefined global symbol in the object output. The raw image
|
||||||
s (`--format raw`, the default) still reports them: only an object
|
s (`--format raw`, the default) still reports them: only an object
|
||||||
@@ -746,7 +901,7 @@ objects.
|
|||||||
writes; without `--format` the behaviour is unchanged (the concatenated
|
writes; without `--format` the behaviour is unchanged (the concatenated
|
||||||
image).
|
image).
|
||||||
|
|
||||||
## [0.10.0] — 2026-07-15
|
## [0.10.0] - 2026-07-15
|
||||||
|
|
||||||
The EVEX floating-point and conversion set: the packed-double arithmetic,
|
The EVEX floating-point and conversion set: the packed-double arithmetic,
|
||||||
the scalar SD/SS forms, VMOVDDUP and the width-changing conversions, each
|
the scalar SD/SS forms, VMOVDDUP and the width-changing conversions, each
|
||||||
@@ -754,23 +909,23 @@ verified byte for byte against the Go assembler.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the rest of the common EVEX/VEX floating-point set — packed double
|
- `asm`: the rest of the common EVEX/VEX floating-point set; packed double
|
||||||
arithmetic (VSUBPD, VDIVPD, VMINPD, VMAXPD, VUNPCKLPD and the EVEX form of
|
arithmetic (VSUBPD, VDIVPD, VMINPD, VMAXPD, VUNPCKLPD and the EVEX form of
|
||||||
VUNPCKHPD), the scalar double and single operations (VSUBSD, VDIVSD,
|
VUNPCKHPD), the scalar double and single operations (VSUBSD, VDIVSD,
|
||||||
VMINSD, VMAXSD and the full VADDSS/VSUBSS/VMULSS/VDIVSS/VMINSS/VMAXSS
|
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
|
family in both VEX and EVEX; the EVEX scalar forms exist for masked and
|
||||||
zeroing use), and VMOVDDUP (lane duplication, VEX and EVEX).
|
zeroing use), and VMOVDDUP (lane duplication, VEX and EVEX).
|
||||||
- `asm`: the width-changing conversions — VCVTDQ2PS and VCVTPS2PD (VEX and
|
- `asm`: the width-changing conversions; VCVTDQ2PS and VCVTPS2PD (VEX and
|
||||||
EVEX; the destination sets the length for PS→PD), the EVEX form of
|
EVEX; the destination sets the length for PS→PD), the EVEX form of
|
||||||
VCVTDQ2PD, and the packed-double → dword family: VCVTPD2DQ/VCVTTPD2DQ
|
VCVTDQ2PD, and the packed-double → dword family: VCVTPD2DQ/VCVTTPD2DQ
|
||||||
(EVEX-512 only, a ZMM source and an XMM destination) and their X/Y
|
(EVEX-512 only, a ZMM source and an XMM destination) and their X/Y
|
||||||
spellings (VCVTPD2DQX/Y, VCVTTPD2DQX/Y), whose length follows the wider
|
spellings (VCVTPD2DQX/Y, VCVTTPD2DQX/Y), whose length follows the wider
|
||||||
source — a new operand form, since the destination is always XMM while
|
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
|
VEX.L / EVEX.L'L ride with the source (fixed by the spelling even for a
|
||||||
memory source).
|
memory source).
|
||||||
- `asm`: masking and zeroing on every new form — the scalar SD/SS
|
- `asm`: masking and zeroing on every new form; the scalar SD/SS
|
||||||
arithmetic, the unpacks, VMOVDDUP and the conversions all accept the
|
arithmetic, the unpacks, VMOVDDUP and the conversions all accept the
|
||||||
explicit K1–K7 operand and the `.Z` suffix the way Go writes them.
|
explicit K1-K7 operand and the `.Z` suffix the way Go writes them.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -781,35 +936,35 @@ verified byte for byte against the Go assembler.
|
|||||||
shares the convention).
|
shares the convention).
|
||||||
|
|
||||||
|
|
||||||
## [0.9.0] — 2026-07-14
|
## [0.9.0] - 2026-07-14
|
||||||
|
|
||||||
AVX-512 masking and a wider EVEX integer set.
|
AVX-512 masking and a wider EVEX integer set.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: **EVEX masking** the way Go writes it — an explicit `K1`–`K7`
|
- `asm`: **EVEX masking** the way Go writes it; an explicit `K1`-`K7`
|
||||||
operand placed among the operands (merging mask), and a `.Z` mnemonic
|
operand placed among the operands (merging mask), and a `.Z` mnemonic
|
||||||
suffix for zeroing (`VPADDD.Z Z1, Z2, K2, Z3`). Supported across the NDS,
|
suffix for zeroing (`VPADDD.Z Z1, Z2, K2, Z3`). Supported across the NDS,
|
||||||
reg/rm, immediate-shift, align, extract, convert and move forms, including
|
reg/rm, immediate-shift, align, extract, convert and move forms, including
|
||||||
masked comparisons with a K destination (`VPCMPEQD Z0, Z3, K2, K1`). K0 is
|
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
|
rejected as an explicit mask, and `.Z` without a mask is an error, matching
|
||||||
the Go assembler.
|
the Go assembler.
|
||||||
- `asm`: the common AVX-512 F/BW integer set — VPADDB/W, VPSUBB/W, VPANDD/Q,
|
- `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
|
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
|
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.
|
EVEX forms of VPSHUFD/VPSHUFB, and the VMOVDQU8/VMOVDQU16 move aliases.
|
||||||
Register indices 16–31 encode correctly (the mod=11 quirk carries rm[4]
|
Register indices 16-31 encode correctly (the mod=11 quirk carries rm[4]
|
||||||
in X̄). All verified byte for byte against the Go assembler.
|
in X̄). All verified byte for byte against the Go assembler.
|
||||||
- `lint`: masked EVEX forms (`.Z` suffix, K operands) are recognised by
|
- `lint`: masked EVEX forms (`.Z` suffix, K operands) are recognised by
|
||||||
`unknown-instruction` and exempted from `operand-count`.
|
`unknown-instruction` and exempted from `operand-count`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- `asm`: EVEX register–register operands with indices 16–31 encoded rm[4]
|
- `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
|
into B̄ instead of X̄ (the EVEX mod=11 extension quirk), producing wrong
|
||||||
prefix bytes for X16+/Y16+ r/m operands.
|
prefix bytes for X16+/Y16+ r/m operands.
|
||||||
|
|
||||||
## [0.8.0] — 2026-07-13
|
## [0.8.0] - 2026-07-13
|
||||||
|
|
||||||
Standard CLI ergonomics.
|
Standard CLI ergonomics.
|
||||||
|
|
||||||
@@ -825,20 +980,20 @@ Standard CLI ergonomics.
|
|||||||
- The version is primarily available as the standard `gasm --version` / `-V`
|
- The version is primarily available as the standard `gasm --version` / `-V`
|
||||||
flag; the `gasm version` spelling remains as an alias.
|
flag; the `gasm version` spelling remains as an alias.
|
||||||
|
|
||||||
## [0.7.0] — 2026-07-12
|
## [0.7.0] - 2026-07-12
|
||||||
|
|
||||||
The formatter behaves like `go fmt` and canonicalises block separation.
|
The formatter behaves like `go fmt` and canonicalises block separation.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `gasm fmt` now works like `go fmt`: with no arguments — or with a directory
|
- `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
|
argument; it reformats every `.s` file below it in place and lists the
|
||||||
changed files, skipping `.` and `_` directories (`.git`, `_refs`, …).
|
changed files, skipping `.` and `_` directories (`.git`, `_refs`, …).
|
||||||
Explicit file arguments keep the `-w` / standard-output behaviour.
|
Explicit file arguments keep the `-w` / standard-output behaviour.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `s`: canonical blank-line layout — a new block (a label, `TEXT` or
|
- `s`: canonical blank-line layout; a new block (a label, `TEXT` or
|
||||||
`GLOBL`) is preceded by exactly one blank line, neither more nor less.
|
`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),
|
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
|
stacked labels share their block, the function's first label keeps hugging
|
||||||
@@ -847,7 +1002,7 @@ The formatter behaves like `go fmt` and canonicalises block separation.
|
|||||||
kernels were reformatted with this release and remain byte-identical when
|
kernels were reformatted with this release and remain byte-identical when
|
||||||
assembled.
|
assembled.
|
||||||
|
|
||||||
## [0.6.0] — 2026-07-11
|
## [0.6.0] - 2026-07-11
|
||||||
|
|
||||||
Calibrated to the Go ABI: `register-clobber` stops reporting legal code, and
|
Calibrated to the Go ABI: `register-clobber` stops reporting legal code, and
|
||||||
the encoder learns the legacy SSE moves.
|
the encoder learns the legacy SSE moves.
|
||||||
@@ -856,13 +1011,13 @@ the encoder learns the legacy SSE moves.
|
|||||||
|
|
||||||
- `lint`: **`register-clobber` is now calibrated to the Go ABI**
|
- `lint`: **`register-clobber` is now calibrated to the Go ABI**
|
||||||
(`cmd/compile/abi-internal.md`), not the platform ABI. Go's stack-based
|
(`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`
|
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
|
and the arm64/riscv64/loong64 scratch sets are caller-saved or permanent
|
||||||
scratch, and hand-written kernels may clobber them freely. The rule now
|
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
|
audits only the registers Go fixes across calls: the frame pointer and the
|
||||||
goroutine pointer (amd64 `BP`/`R14`, arm64 `R18`/`R28`/`R29`, riscv64
|
goroutine pointer (amd64 `BP`/`R14`, arm64 `R18`/`R28`/`R29`, riscv64
|
||||||
`X27`, loong64 `R22`), and the goroutine pointer is reported only when the
|
`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
|
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
|
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
|
may use it, exactly as the runtime's own assembly does. Both go-flac
|
||||||
kernels now lint with zero diagnostics.
|
kernels now lint with zero diagnostics.
|
||||||
@@ -879,21 +1034,21 @@ the encoder learns the legacy SSE moves.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the legacy (non-VEX) SSE moves — `MOVOU`/`MOVO` (the Plan 9 names
|
- `asm`: the legacy (non-VEX) SSE moves; `MOVOU`/`MOVO` (the Plan 9 names
|
||||||
for MOVDQU/MOVDQA), `MOVUPS`/`MOVAPS`/`MOVUPD`/`MOVAPD` and the scalar
|
for MOVDQU/MOVDQA), `MOVUPS`/`MOVAPS`/`MOVUPD`/`MOVAPD` and the scalar
|
||||||
`MOVSD`/`MOVSS` — and `VMOVDQU64` in the EVEX set. All verified byte for
|
`MOVSD`/`MOVSS`; and `VMOVDQU64` in the EVEX set. All verified byte for
|
||||||
byte against the Go assembler.
|
byte against the Go assembler.
|
||||||
|
|
||||||
## [0.5.0] — 2026-07-10
|
## [0.5.0] - 2026-07-10
|
||||||
|
|
||||||
EVEX / AVX-512: the go-flac AVX-512 kernel now assembles, byte-identically to
|
EVEX / AVX-512: the go-flac AVX-512 kernel now assembles, byte-identically to
|
||||||
the Go toolchain, completing the production-kernel coverage.
|
the Go toolchain, completing the production-kernel coverage.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: **EVEX (AVX-512) encoding** — the four-byte EVEX prefix with the
|
- `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
|
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
|
V'̄ shared between vvvv and the SIB index), opmask registers (K0-K7) as
|
||||||
operands and as mask destinations, and the compressed disp8×N displacement
|
operands and as mask destinations, and the compressed disp8×N displacement
|
||||||
(the multiplier follows the memory operand's size, as the Go assembler's
|
(the multiplier follows the memory operand's size, as the Go assembler's
|
||||||
opcode tables prescribe). Covers every AVX-512 instruction the go-flac
|
opcode tables prescribe). Covers every AVX-512 instruction the go-flac
|
||||||
@@ -903,20 +1058,20 @@ the Go toolchain, completing the production-kernel coverage.
|
|||||||
extracts VEXTRACTI64X4/VEXTRACTF64X4, VFMADD231PD, VADDPD, VMULPD, the
|
extracts VEXTRACTI64X4/VEXTRACTF64X4, VFMADD231PD, VADDPD, VMULPD, the
|
||||||
broadcasts VPBROADCASTD/Q (GPR and memory sources take different opcodes)
|
broadcasts VPBROADCASTD/Q (GPR and memory sources take different opcodes)
|
||||||
and the mask moves KMOVW/KTESTW. Masking/zeroing suffixes are out of scope
|
and the mask moves KMOVW/KTESTW. Masking/zeroing suffixes are out of scope
|
||||||
— the kernels use neither.
|
; the kernels use neither.
|
||||||
- `asm`: `AssembleFile` now accepts file-defined global (`non-<>`) symbols
|
- `asm`: `AssembleFile` now accepts file-defined global (`non-<>`) symbols
|
||||||
too; a reference is external only when no `GLOBL` in the file defines it.
|
too; a reference is external only when no `GLOBL` in the file defines it.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- `asm`: registers X16–Y31 force the EVEX encoding of dual-form mnemonics;
|
- `asm`: registers X16-Y31 force the EVEX encoding of dual-form mnemonics;
|
||||||
previously a `VPBROADCASTD AX, Y30` fell into the VEX encoder, which cannot
|
previously a `VPBROADCASTD AX, Y30` fell into the VEX encoder, which cannot
|
||||||
represent indices above 15 and silently truncated them.
|
represent indices above 15 and silently truncated them.
|
||||||
- `asm`: the VEX encoder now rejects vector register indices 16–31 instead of
|
- `asm`: the VEX encoder now rejects vector register indices 16-31 instead of
|
||||||
encoding a truncated (wrong) register.
|
encoding a truncated (wrong) register.
|
||||||
|
|
||||||
|
|
||||||
## [0.4.0] — 2026-07-09
|
## [0.4.0] - 2026-07-09
|
||||||
|
|
||||||
The standalone assembler reaches the whole go-flac AVX2 kernel: static
|
The standalone assembler reaches the whole go-flac AVX2 kernel: static
|
||||||
symbols assemble, and all 17 kernel functions now match the Go toolchain's
|
symbols assemble, and all 17 kernel functions now match the Go toolchain's
|
||||||
@@ -924,10 +1079,10 @@ machine code byte for byte.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: **file-level assembly** — `AssembleFile` turns a parsed file into an
|
- `asm`: **file-level assembly**; `AssembleFile` turns a parsed file into an
|
||||||
`Image`: the function bodies in source order followed by a data section
|
`Image`: the function bodies in source order followed by a data section
|
||||||
built from the file's `GLOBL`/`DATA` directives (each symbol 16-aligned).
|
built from the file's `GLOBL`/`DATA` directives (each symbol 16-aligned).
|
||||||
- `asm`: **static-symbol (`SB`) operands** — `mask<>(SB)` references encode as
|
- `asm`: **static-symbol (`SB`) operands**; `mask<>(SB)` references encode as
|
||||||
RIP-relative loads with a patched disp32, resolved against the image layout
|
RIP-relative loads with a patched disp32, resolved against the image layout
|
||||||
so the output is self-consistent and position-independent. External
|
so the output is self-consistent and position-independent. External
|
||||||
(non-file-local) symbols are rejected with a clear error: they need
|
(non-file-local) symbols are rejected with a clear error: they need
|
||||||
@@ -936,7 +1091,7 @@ machine code byte for byte.
|
|||||||
and writes the whole image (code + data) with `-o`.
|
and writes the whole image (code + data) with `-o`.
|
||||||
|
|
||||||
|
|
||||||
## [0.3.0] — 2026-07-08
|
## [0.3.0] - 2026-07-08
|
||||||
|
|
||||||
The assembler reaches byte-identical parity with the Go toolchain on the
|
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
|
production go-flac AVX2 kernels: every one of the 15 kernel functions that
|
||||||
@@ -946,18 +1101,18 @@ support).
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `asm`: the scalar instruction families the kernels use — `CMOVcc` and
|
- `asm`: the scalar instruction families the kernels use; `CMOVcc` and
|
||||||
`SETcc` (conditions spelled exactly like the jumps), `LZCNT`/`TZCNT`
|
`SETcc` (conditions spelled exactly like the jumps), `LZCNT`/`TZCNT`
|
||||||
(legacy `F3 0F BD/BC`), the sign/zero-extending moves (`MOVBLZX`, `MOVBQZX`,
|
(legacy `F3 0F BD/BC`), the sign/zero-extending moves (`MOVBLZX`, `MOVBQZX`,
|
||||||
`MOVWLZX`, `MOVWQZX`, `MOVWLSX`, `MOVLQSX`), `CVTSL2SD`/`CVTSQ2SD` (the
|
`MOVWLZX`, `MOVWQZX`, `MOVWLSX`, `MOVLQSX`), `CVTSL2SD`/`CVTSQ2SD` (the
|
||||||
legacy SSE encoding, as the Go assembler emits it), the traditional
|
legacy SSE encoding, as the Go assembler emits it), the traditional
|
||||||
three-operand `IMUL3{W,L,Q}`, and the variable-count vector shifts
|
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
|
(`VPSRLQ X0, Y8, Y8`; the count in an XMM register or memory takes the
|
||||||
ordinary NDS form).
|
ordinary NDS form).
|
||||||
- `asm`: **jump relaxation** — jumps start in the short (rel8) form and
|
- `asm`: **jump relaxation**; jumps start in the short (rel8) form and
|
||||||
expand to rel32 when the settled displacement does not fit, iterating the
|
expand to rel32 when the settled displacement does not fit, iterating the
|
||||||
layout to a fixed point (CALL is always rel32).
|
layout to a fixed point (CALL is always rel32).
|
||||||
- `asm`: **jump-to-jump folding** — a conditional jump to a label whose only
|
- `asm`: **jump-to-jump folding**; a conditional jump to a label whose only
|
||||||
instruction is an unconditional jump is redirected to the ultimate
|
instruction is an unconditional jump is redirected to the ultimate
|
||||||
target, replicating the Go toolchain's linker, which chases such chains
|
target, replicating the Go toolchain's linker, which chases such chains
|
||||||
before it encodes branches.
|
before it encodes branches.
|
||||||
@@ -969,12 +1124,12 @@ support).
|
|||||||
- `asm`: `CMP` with a register or memory operand computed **second − first**
|
- `asm`: `CMP` with a register or memory operand computed **second − first**
|
||||||
instead of first − second, silently inverting every condition that followed
|
instead of first − second, silently inverting every condition that followed
|
||||||
(`CMPQ SI, R10; JGE` tested R10 ≥ SI). The encoding now always records
|
(`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`
|
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.
|
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 =
|
- `asm`: register-to-register `MOV` now uses the `r/m ← r` opcode (reg =
|
||||||
source), the Go assembler's choice; the output is byte-identical.
|
source), the Go assembler's choice; the output is byte-identical.
|
||||||
|
|
||||||
## [0.2.0] — 2026-07-07
|
## [0.2.0] - 2026-07-07
|
||||||
|
|
||||||
The Phase 2 assembler grows the SIMD set: shuffles, extract/insert, permute
|
The Phase 2 assembler grows the SIMD set: shuffles, extract/insert, permute
|
||||||
and the moves, on top of the Phase 1 VEX forms.
|
and the moves, on top of the Phase 1 VEX forms.
|
||||||
@@ -987,10 +1142,10 @@ and the moves, on top of the Phase 1 VEX forms.
|
|||||||
- the immediate shuffle (`VPSHUFD`, `VPERMQ`),
|
- the immediate shuffle (`VPSHUFD`, `VPERMQ`),
|
||||||
- the three-operand-plus-immediate form (`VSHUFPD`, `VPERM2I128`,
|
- the three-operand-plus-immediate form (`VSHUFPD`, `VPERM2I128`,
|
||||||
`VINSERTI128`),
|
`VINSERTI128`),
|
||||||
- the lane extract (`VEXTRACTI128`, `VEXTRACTF128` — the YMM source occupies
|
- the lane extract (`VEXTRACTI128`, `VEXTRACTF128`; the YMM source occupies
|
||||||
the ModRM.reg field, the XMM/memory destination the r/m field),
|
the ModRM.reg field, the XMM/memory destination the r/m field),
|
||||||
- the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, `VMOVD`, `VMOVQ`,
|
- the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`, `VMOVD`, `VMOVQ`,
|
||||||
`VMOVSD` — each direction picks its own opcode and VEX.W; a vector→vector
|
`VMOVSD`; each direction picks its own opcode and VEX.W; a vector→vector
|
||||||
move uses the store-form layout, matching the Go assembler),
|
move uses the store-form layout, matching the Go assembler),
|
||||||
- the no-operand `VZEROUPPER`, and `VPERMD` in the NDS form,
|
- the no-operand `VZEROUPPER`, and `VPERMD` in the NDS form,
|
||||||
- the floating-point and FMA set (`VADDPD`, `VMULPD`, `VXORPD`,
|
- the floating-point and FMA set (`VADDPD`, `VMULPD`, `VXORPD`,
|
||||||
@@ -999,21 +1154,21 @@ and the moves, on top of the Phase 1 VEX forms.
|
|||||||
the encoder now covers every integer, shuffle and FP instruction the
|
the encoder now covers every integer, shuffle and FP instruction the
|
||||||
go-flac AVX2 kernels use.
|
go-flac AVX2 kernels use.
|
||||||
- `asm`: `CMP` accepts the immediate in the second operand position
|
- `asm`: `CMP` accepts the immediate in the second operand position
|
||||||
(`CMPL CX, $31`) — the spelling the Go assembler accepts — encoding it
|
(`CMPL CX, $31`), the spelling the Go assembler accepts, encoding it
|
||||||
identically to the immediate-first form.
|
identically to the immediate-first form.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- `asm`: an unused VEX.vvvv field is now stored as `1111` (v̄vvv = 1111), as
|
- `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
|
the hardware requires; the previous value (`0000`) made the two-operand
|
||||||
reg/rm forms (VPMOVSXWD, VPBROADCASTD, VMOVMSKPS, …) raise #UD on real CPUs
|
reg/rm forms (VPMOVSXWD, VPBROADCASTD, VMOVMSKPS, …) raise #UD on real CPUs
|
||||||
and differ from the Go assembler's bytes. The round-trip decoder ignores
|
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
|
the field on these instructions, which is why the byte-for-byte Go
|
||||||
comparison (added this release) is now part of the test suite.
|
comparison (added this release) is now part of the test suite.
|
||||||
|
|
||||||
## [0.1.0] — 2026-07-06
|
## [0.1.0] - 2026-07-06
|
||||||
|
|
||||||
Initial release — the Phase 1 foundation.
|
Initial release; the Phase 1 foundation.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -1026,11 +1181,11 @@ Initial release — the Phase 1 foundation.
|
|||||||
- `arch`: register files and **complete** instruction tables for amd64,
|
- `arch`: register files and **complete** instruction tables for amd64,
|
||||||
arm64, riscv64 and loong64, with the middle-dot symbol separator and static
|
arm64, riscv64 and loong64, with the middle-dot symbol separator and static
|
||||||
(`<>`) symbols. Instruction names are generated from the Go toolchain's own
|
(`<>`) symbols. Instruction names are generated from the Go toolchain's own
|
||||||
assembler source (`just gen`) — the `anames` opcode lists plus the common
|
assembler source (`just gen`); the `anames` opcode lists plus the common
|
||||||
opcodes and the per-architecture front-end aliases (arm64 `B`/`BL`, the
|
opcodes and the per-architecture front-end aliases (arm64 `B`/`BL`, the
|
||||||
`.P`/`.W` addressing suffixes, loong64 `JAL`, the x86 conditional-jump
|
`.P`/`.W` addressing suffixes, loong64 `JAL`, the x86 conditional-jump
|
||||||
spellings) — so every mnemonic the real assembler accepts is recognised.
|
spellings); so every mnemonic the real assembler accepts is recognised.
|
||||||
- `lint`: conservative rules — `unknown-instruction`, `operand-count`,
|
- `lint`: conservative rules; `unknown-instruction`, `operand-count`,
|
||||||
`undefined-label`, `duplicate-label`, `missing-ret`,
|
`undefined-label`, `duplicate-label`, `missing-ret`,
|
||||||
`missing-textflag-include`, `abi-argsize` and `unreachable-code`. Macro
|
`missing-textflag-include`, `abi-argsize` and `unreachable-code`. Macro
|
||||||
invocations are recognised (in-file `#define` names and underscore
|
invocations are recognised (in-file `#define` names and underscore
|
||||||
@@ -1052,9 +1207,9 @@ Initial release — the Phase 1 foundation.
|
|||||||
- `lsp`: a Language Server Protocol server over stdio providing completion,
|
- `lsp`: a Language Server Protocol server over stdio providing completion,
|
||||||
hover documentation, document symbols, publish-diagnostics and semantic-token
|
hover documentation, document symbols, publish-diagnostics and semantic-token
|
||||||
highlighting.
|
highlighting.
|
||||||
- `asm`: a standalone amd64 (x86-64) assembler — an instruction encoder (REX/
|
- `asm`: a standalone amd64 (x86-64) assembler; an instruction encoder (REX/
|
||||||
ModR-M/SIB/displacement/immediate plus the scalar instruction set, and VEX/
|
ModR-M/SIB/displacement/immediate plus the scalar instruction set, and VEX/
|
||||||
AVX2 SIMD across three operand forms — NDS, reg/rm and immediate-shift —
|
AVX2 SIMD across the three operand forms NDS, reg/rm and immediate-shift,
|
||||||
covering the bulk of the integer SIMD set) validated by round-trip decoding
|
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
|
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
|
into the encoder with local-label resolution and `FP`/`SP` frame mapping
|
||||||
|
|||||||
+97
-77
@@ -1,107 +1,127 @@
|
|||||||
# Contributing to gasm-devkit
|
# Contributing
|
||||||
|
|
||||||
Thanks for contributing to gasm-devkit.
|
Contributions to **gasm-devkit** are governed by the Contributor terms
|
||||||
|
below; submitting one means you accept them.
|
||||||
|
|
||||||
|
## Contributor terms
|
||||||
|
|
||||||
|
1. This project belongs to its owner alone. The owner decides what is
|
||||||
|
accepted, in what form and when; the decision is final and needs no
|
||||||
|
justification.
|
||||||
|
2. By submitting a contribution you assign to Petr Balvín
|
||||||
|
<opensource@petrbalvin.org> all present and future copyright and
|
||||||
|
related rights in it, worldwide, for the full term of the rights,
|
||||||
|
with the right to relicense and sublicense without restriction,
|
||||||
|
including under proprietary terms.
|
||||||
|
3. Where that assignment is not effective, it counts as a perpetual,
|
||||||
|
irrevocable, royalty-free licence with the same scope.
|
||||||
|
4. To the fullest extent permitted by law, you waive any right of
|
||||||
|
attribution and integrity in the contribution. The project names no
|
||||||
|
contributors and keeps no credits list.
|
||||||
|
5. By submitting you represent that the work is yours and that you
|
||||||
|
hold the rights to assign it as above.
|
||||||
|
|
||||||
## Development setup
|
## Development setup
|
||||||
|
|
||||||
Requirements: Go 1.27 or later, the [just](https://github.com/casey/just)
|
Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod`
|
||||||
command runner, and a Linux host on amd64, arm64, riscv64 or loong64.
|
declares, and [just](https://github.com/casey/just) for the recipes.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||||
cd gasm-devkit
|
cd gasm-devkit
|
||||||
just install # download module dependencies
|
just build
|
||||||
just build # go vet + gofmt check
|
just gates
|
||||||
just test # full suite, race detector, 80 % coverage gate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Branch from `development`; never commit directly to `main` (`main` is
|
1. Branch from `development`. Never commit directly to `main`, which is release-only.
|
||||||
release-only: merge from `development`, then tag).
|
2. Commit in [Conventional Commits](https://www.conventionalcommits.org/) form:
|
||||||
2. Commit with [Conventional Commits](https://www.conventionalcommits.org/):
|
`type(scope): description`, subject line only, imperative mood, lowercase after the
|
||||||
`type(scope): description`: subject line only, imperative mood,
|
colon, no trailing full stop. Allowed types: `feat`, `fix`, `docs`, `style`,
|
||||||
lowercase after the colon, no trailing dot. Allowed types: `feat`,
|
`refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert`.
|
||||||
`fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`,
|
3. One logical change per commit. A refactor, a behaviour change and a formatting pass
|
||||||
`build`, `revert`. The only line after the subject is the trailer:
|
are three commits, never one.
|
||||||
`Assisted-by: <model-name>`. No `Co-Authored-By`, no `Signed-off-by`,
|
4. Record every user-visible change in `CHANGELOG.md` under `## [development]`.
|
||||||
no other trailers.
|
5. Add or update tests. Coverage stays at 80 percent or more; it is a hard gate.
|
||||||
3. Record every user-visible change in `CHANGELOG.md` under
|
6. Update the documentation when the public API, the configuration or the behaviour
|
||||||
`## [development]` (categories: Added, Changed, Fixed, Removed,
|
changes.
|
||||||
Security).
|
7. Open a pull request against `development`.
|
||||||
4. Add or update tests; coverage must stay **at or above 80 %** (hard
|
|
||||||
gate, enforced by CI).
|
|
||||||
5. Update the documentation when behaviour, flags or the public surface
|
|
||||||
change.
|
|
||||||
6. Open a pull request against `development`.
|
|
||||||
|
|
||||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`;
|
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The release
|
||||||
CI builds and publishes the binaries for all four architectures.
|
workflow builds the assets and publishes the release and its notes.
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
`gofmt` and `go vet` via `just fmt` / `just build`; both must pass with
|
`gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero
|
||||||
zero output; `go fix -diff ./...` must report nothing on touched packages.
|
warnings tolerated. `just gates` is the definition of done in one command, and the recipe
|
||||||
|
file names what it contains. Errors are checked explicitly, wrapped as
|
||||||
|
`fmt.Errorf("context: %w", err)`, and nothing panics outside `main`. The `golang`
|
||||||
|
skill holds the rules the project follows; the recipe file holds the commands.
|
||||||
|
|
||||||
- Standard library only in production code; `golang.org/x/arch` is used
|
- `golang.org/x/arch` is the one module dependency, and it is linked into the binary:
|
||||||
in tests only (round-trip decoding) and is never linked into the `gasm`
|
`gasm dis` and the debugger's listings decode through it. Everything else is the
|
||||||
binary.
|
standard library.
|
||||||
- No cgo, no C, no external toolchains at runtime.
|
- No cgo, no C, no external toolchain at runtime.
|
||||||
- Explicit `if err != nil`; errors wrapped with
|
- The parser, lexer and formatter are hand-written; the `arch` instruction tables are
|
||||||
`fmt.Errorf("context: %w", err)`; no panics outside `main`.
|
generated only by `_gen/gen.go` (`just gen`) and never edited by hand.
|
||||||
- The parser, lexer and formatter are hand-written; the `arch` instruction
|
- Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a
|
||||||
tables are generated only via `_gen/gen.go` (`just gen`), never edited.
|
`.s` file that `gasm fmt -l .` lists is unfinished.
|
||||||
|
|
||||||
## Running a single test
|
New source files open with the project's two-line licence header, whose SPDX
|
||||||
|
identifier matches `LICENSE`. Configuration files, workflows and dotfiles do not carry
|
||||||
|
it.
|
||||||
|
|
||||||
```sh
|
## AI contribution policy
|
||||||
go test -run TestVexGroundTruth ./asm/
|
|
||||||
go test -run TestGroundTruthBasic ./verify/
|
AI tools are welcome as productivity aids and are a normal part of modern software
|
||||||
go test -run TestGOObjectLinkAndRun ./asm/
|
development. What matters is that the contribution stays understandable, reviewable and
|
||||||
go test -run TestFuzzWideCopy ./verify/
|
genuinely useful.
|
||||||
|
|
||||||
|
- **Disclose the assistance.** If AI helped draft any part of a commit, issue, pull
|
||||||
|
request or review, say so.
|
||||||
|
- **Commit messages carry exactly one trailer**, as a git trailer on the line after a
|
||||||
|
blank line that closes the subject:
|
||||||
|
|
||||||
|
```
|
||||||
|
Assisted-by: MODEL
|
||||||
```
|
```
|
||||||
|
|
||||||
The interactive debugger (`gasm debug`) requires a compiled binary on
|
Name the model that did the work, spelled the way its maker spells it, for example
|
||||||
`$PATH`; `go run` does not work for the traced child process. Install
|
`GLM 5.3`, `DeepSeek V4.1 Flash` or `Qwen 3.8 Flash`. No `Co-Authored-By`, no `Signed-off-by`,
|
||||||
first with `just install-bin`.
|
no other trailers, and no prose: the trailer is the disclosure.
|
||||||
|
- **Issues and pull requests** attribute the assistance in a comment, for example
|
||||||
|
`_Assisted-by: GLM 5.3_`. It does not belong in the pull request description.
|
||||||
|
- **Take responsibility.** You are accountable for the accuracy, completeness and
|
||||||
|
intent of everything you submit, whether or not AI produced it.
|
||||||
|
- **Review before marking ready.** Read the diff carefully, run it locally, and add the
|
||||||
|
tests it needs. Do not mark a pull request ready until you can defend every change in
|
||||||
|
it.
|
||||||
|
- **Quality over quantity.** Contributions that look like un-reviewed output, or whose
|
||||||
|
author cannot engage substantively during review, may be closed.
|
||||||
|
- **Preferred models.** Prefer open-weight models with transparent training data and
|
||||||
|
minimal output filtering.
|
||||||
|
|
||||||
## CI (Gitea Actions)
|
AI assists. It does not replace judgement.
|
||||||
|
|
||||||
Workflows live in `.gitea/workflows/` and run on self-hosted runners:
|
## Continuous integration
|
||||||
|
|
||||||
|
Workflows live in `.gitea/workflows/` and run on the project's own runners:
|
||||||
|
|
||||||
| Workflow | Trigger | What it does |
|
| Workflow | Trigger | What it does |
|
||||||
|----------|---------|--------------|
|
|---|---|---|
|
||||||
| Test | push / PR to `development` | gofmt check, `go vet`, `go test -race`, 80 % coverage gate |
|
| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor |
|
||||||
| Release | tag `v*` | cross-compiles binaries for linux/{amd64,arm64,riscv64,loong64} and publishes the Gitea release |
|
| Release | a `v*` tag | the same gates as Test, then the matrix build, the proven version and the release itself; the race detector runs locally in `just gates` before the tag is cut |
|
||||||
|
|
||||||
The Definition of Done (`just build` + `just test` + `just fmt`) must
|
The local equivalent is `just gates`, which is the same set plus the race detector. The
|
||||||
still pass locally before pushing.
|
race detector also has its own workflow, dispatched by hand; it never runs on a push or a
|
||||||
|
tag, where it would double the time and the memory a shared runner cannot spare.
|
||||||
## AI Contribution Policy
|
|
||||||
|
|
||||||
AI tools are welcome as productivity aids. What matters is that
|
|
||||||
contributions remain understandable, reviewable, and genuinely useful.
|
|
||||||
|
|
||||||
- **Disclose AI use.** If you used AI to draft or generate any part of a
|
|
||||||
commit, issue, pull request, or code review, say so clearly.
|
|
||||||
- **Commit messages:** end every commit with exactly one trailer:
|
|
||||||
`Assisted-by: <model-name>` (e.g. `Assisted-by: GLM 5.3`).
|
|
||||||
- **Pull requests and issues:** attribute AI assistance in one trailing
|
|
||||||
line, e.g. `_Assisted-by: GLM 5.3_`. Do not paste it into the PR
|
|
||||||
description as a section.
|
|
||||||
- **Take responsibility.** You remain accountable for the accuracy,
|
|
||||||
completeness, and intent of everything you submit.
|
|
||||||
- **Review before marking ready.** Read AI-generated diffs carefully, run
|
|
||||||
them locally, and add or update tests where appropriate.
|
|
||||||
- **Preferred models.** Prefer open-weight models with transparent
|
|
||||||
training data: **GLM**, **DeepSeek**, and **MiMo**.
|
|
||||||
|
|
||||||
## Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
Open an issue at
|
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-devkit/issues` with the
|
||||||
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues)
|
version, the operating system and architecture, the exact command, the full output,
|
||||||
with the version (`gasm --version`), OS and architecture, the exact
|
and the expected against the actual behaviour.
|
||||||
command, the full output, and the expected versus actual behaviour.
|
|
||||||
|
|
||||||
**Security issues:** email **opensource@petrbalvin.org** instead of opening
|
**Security issues do not go in the issue tracker.** Report them as
|
||||||
a public issue.
|
[SECURITY.md](SECURITY.md) describes, to **opensource@petrbalvin.org**.
|
||||||
|
|||||||
@@ -1,13 +1,63 @@
|
|||||||
# gasm-devkit
|
# Plan 9 assembly tooling, inside and outside Go
|
||||||
|
|
||||||
Developer tooling for **GAsm**, Go's built-in Plan 9 assembler.
|
> **Warning: this is an experiment.** gasm-devkit is under active
|
||||||
|
> development and is not stable. The version is 0.x.x: commands, flags,
|
||||||
|
> output formats and behaviour can change without warning at any time.
|
||||||
|
> A 1.0.0 release is light years away. Nothing in this document is a
|
||||||
|
> stability promise. For all of that, this is not a paper project: gasm
|
||||||
|
> is already in active use and is tested on real assembly work.
|
||||||
|
|
||||||
Go ships an assembler but no tooling for it: there is no syntax highlighting,
|
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
|
||||||
no autocomplete, no linter, no static analyser, no formatter, no standalone
|
there is no formatter, no linter, no static analyser, no standalone
|
||||||
assembler and no debugger for `.s` files. Developers write assembly blind,
|
assembler and no debugger for `.s` files. Developers write assembly
|
||||||
validate it by benchmark, and debug it by print statement. gasm-devkit is the
|
blind, validate it by benchmark, and debug it by print statement.
|
||||||
missing toolkit: a single, self-contained binary, `gasm`, that brings proper
|
gasm-devkit is the missing toolkit: a single, self-contained binary,
|
||||||
developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
`gasm`, that serves both purposes.
|
||||||
|
|
||||||
|
- **Help develop Plan 9 assembly.** Formatting, linting, disassembly,
|
||||||
|
dynamic verification, a source-level debugger and a language server,
|
||||||
|
for `.s` files in Go programs.
|
||||||
|
- **Use Plan 9 assembly outside the Go toolchain.** `gasm asm` encodes
|
||||||
|
on its own, with no Go installation in the loop, and writes raw
|
||||||
|
images, linkable ELF objects with DWARF5 debug sections, or the Go
|
||||||
|
toolchain's own GOOBJ format, which `go build` consumes in place of
|
||||||
|
the toolchain's output.
|
||||||
|
|
||||||
|
## Why Plan 9 assembly
|
||||||
|
|
||||||
|
Plan 9 assembly is the quiet triumph of the field. One syntax across
|
||||||
|
every architecture Go builds for: the same source-first operand order,
|
||||||
|
the same four pseudo-registers, the same frame convention, whether the
|
||||||
|
target is x86, ARM, RISC-V or LoongArch. Learn it once and you can
|
||||||
|
read a kernel on any of them.
|
||||||
|
|
||||||
|
Compare the alternatives. Intel syntax and AT&T syntax disagree on the
|
||||||
|
one question every instruction answers, which operand is the source
|
||||||
|
and which is the destination, so half the world writes it one way,
|
||||||
|
half the other, and every assembly programmer carries both in their
|
||||||
|
head forever. GNU as settles the argument with directives that switch
|
||||||
|
dialects mid-file (`.intel_syntax noprefix`), a percent sign on every
|
||||||
|
register and a dollar on every immediate: punctuation that carries
|
||||||
|
nothing the operand order did not already say. And the x86 family
|
||||||
|
fragments again underneath: NASM is not MASM is not GAS, each with its
|
||||||
|
own directive zoo and macro language, so every project picks a dialect
|
||||||
|
and every reader learns a different one by accident.
|
||||||
|
|
||||||
|
Plan 9 assembly has none of it. Registers are bare names. Memory is
|
||||||
|
one notation, `offset(base)`, extended by an index and a scale when
|
||||||
|
the instruction needs it. Arguments arrive named and offset-checked:
|
||||||
|
`x+0(FP)` is the argument x, on every architecture, and `go vet`
|
||||||
|
polices the offsets against the Go prototype.
|
||||||
|
|
||||||
|
```text
|
||||||
|
AT&T (GNU as): movq %rax, -16(%rbp)
|
||||||
|
Plan 9 (Go): MOVQ AX, total-16(SP)
|
||||||
|
```
|
||||||
|
|
||||||
|
The same lines, but only one of them tells you what the number is for.
|
||||||
|
The syntax is uppercase, regular and boring, which is the highest
|
||||||
|
compliment a language for machine code can earn. gasm-devkit exists
|
||||||
|
to give that syntax the tooling it deserves.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -16,7 +66,8 @@ developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
|||||||
directly.
|
directly.
|
||||||
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
||||||
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
||||||
operating recursively on directories the way `go fmt` does.
|
operating recursively on directories the way `go fmt` does. `-l` lists
|
||||||
|
files whose formatting differs and `-d` prints a unified diff.
|
||||||
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
|
- **Linter.** `gasm lint` runs 18 conservative static checks, among them
|
||||||
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
||||||
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
||||||
@@ -24,7 +75,11 @@ developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
|||||||
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
||||||
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
||||||
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
||||||
consumes in place of the toolchain's output.
|
consumes in place of the toolchain's output. Framed functions get the
|
||||||
|
stack-split guard and the morestack block, byte-identical to the
|
||||||
|
toolchain's, so split functions link too.
|
||||||
|
- **Disassembler.** `gasm dis` lists a `.s` file's functions at their real
|
||||||
|
offsets after assembling, or disassembles raw bytes from a file or stdin.
|
||||||
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
||||||
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
|
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
|
||||||
canary), differential fuzzing against the `go tool asm` build, and
|
canary), differential fuzzing against the `go tool asm` build, and
|
||||||
@@ -36,17 +91,17 @@ developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
|||||||
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
||||||
find references, rename, formatting, inlay hints, code actions, signature
|
find references, rename, formatting, inlay hints, code actions, signature
|
||||||
help, document highlights, workspace symbol search, #include document
|
help, document highlights, workspace symbol search, #include document
|
||||||
links and folding ranges over stdio.
|
links and folding ranges over stdio; definition, references and rename
|
||||||
|
work across every open document.
|
||||||
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
||||||
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
||||||
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
||||||
and `gasm scaffold` generates a differential test skeleton for a kernel.
|
and `gasm scaffold` generates a differential test skeleton for a kernel.
|
||||||
- **Complete instruction coverage.** The instruction tables are generated
|
|
||||||
from the Go toolchain's own assembler source, so the toolkit recognises
|
|
||||||
every mnemonic the real assembler accepts; `just gen` refreshes them.
|
|
||||||
|
|
||||||
### Architecture support
|
### Architecture support
|
||||||
|
|
||||||
|
Four architectures, the four that matter in practice:
|
||||||
|
|
||||||
| Architecture | GOARCH | File suffix | Instructions recognised |
|
| Architecture | GOARCH | File suffix | Instructions recognised |
|
||||||
|--------------|-------------|--------------|---------------------------------------------|
|
|--------------|-------------|--------------|---------------------------------------------|
|
||||||
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
||||||
@@ -57,27 +112,64 @@ developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
|||||||
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
||||||
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
||||||
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
|
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
|
||||||
...) that the assembler accepts as aliases. Regenerating the tables is one
|
...) that the assembler accepts as aliases. The tables are generated from
|
||||||
command (`just gen`) and requires only a Go installation; the committed output
|
the Go toolchain's own assembler source (`just gen` refreshes them), so
|
||||||
has no runtime dependency on the toolchain.
|
every mnemonic the real assembler accepts is recognised; what the encoder
|
||||||
|
can emit today is narrower, and a recognised but unencodable instruction is
|
||||||
|
reported as an explicit error, never as a wrong byte.
|
||||||
|
|
||||||
|
The same measurement runs over GOROOT's whole assembly corpus:
|
||||||
|
`gasm audit-instructions --corpus` reports 108 of 627 files (17.2 %)
|
||||||
|
assembling for every target architecture today, with the top failure
|
||||||
|
reasons per architecture; the number moves with every release.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
The plan, in the order it is being worked:
|
||||||
|
|
||||||
|
- **Extended instruction support.** Two layers. First, encoding
|
||||||
|
coverage for every mnemonic the Go toolchain itself accepts, closed in
|
||||||
|
order of how often real code needs each instruction;
|
||||||
|
`gasm audit-instructions` measures the gap. Second, the larger work:
|
||||||
|
an extended instruction set the toolchain does not know at all. The
|
||||||
|
toolchain-derived tables stay generated and untouched; only the
|
||||||
|
extended instructions are hand-maintained, with their own spellings
|
||||||
|
and encoders, verified by execution on real hardware because the
|
||||||
|
toolchain offers no ground truth to compare against. The gaps exist
|
||||||
|
on every architecture, amd64 included.
|
||||||
|
- **Full GOOBJ and ELF compilation.** The destination is a complete,
|
||||||
|
standalone compilation path: linkable ELF objects for consumers outside
|
||||||
|
Go, and GOOBJ objects that `go build` links directly. Through GOOBJ, a
|
||||||
|
Go program will be able to use machine instructions that the Go
|
||||||
|
toolchain itself does not support; through ELF, Plan 9 assembly becomes
|
||||||
|
usable outside Go entirely.
|
||||||
|
- **Platforms: Linux and FreeBSD.** Linux is supported today on all four
|
||||||
|
architectures and is where the binary builds. FreeBSD follows: the
|
||||||
|
JIT's executable-memory mapping and the ptrace debugger layer are the
|
||||||
|
two pieces of porting work. Other unix systems may follow those two.
|
||||||
|
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
||||||
|
No others are planned.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
||||||
linux/loong64 are on the
|
linux/loong64 are on the
|
||||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
||||||
From source (Go 1.27 or later):
|
From source (Go 1.27.1):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from a repository checkout, with the development version stamped:
|
Or from a repository checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
just install-bin
|
just install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The installed binary reports the version the toolchain recorded: the tag
|
||||||
|
on a tagged checkout, a pseudo-version naming the commit below one.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -102,9 +194,13 @@ gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with argumen
|
|||||||
```sh
|
```sh
|
||||||
gasm fmt # reformat every .s below here, like go fmt
|
gasm fmt # reformat every .s below here, like go fmt
|
||||||
gasm fmt -w kernel_amd64.s # canonicalise one file in place
|
gasm fmt -w kernel_amd64.s # canonicalise one file in place
|
||||||
|
gasm fmt -l *.s # list files whose formatting differs
|
||||||
|
gasm fmt -d kernel_amd64.s # print a unified diff instead
|
||||||
gasm lint *.s # static checks
|
gasm lint *.s # static checks
|
||||||
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
||||||
gasm asm --format goobj -p pkg/path -o k.o k.s # Go object, consumed by go build
|
gasm asm --format goobj -p pkg/path -o k.o k.s # Go object, consumed by go build
|
||||||
|
gasm dis k.s # assemble, then list each function
|
||||||
|
gasm dis -a amd64 - < dump.bin # disassemble raw bytes from stdin
|
||||||
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
||||||
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
|
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
|
||||||
gasm debug --func name k.s # interactive debugger
|
gasm debug --func name k.s # interactive debugger
|
||||||
@@ -132,9 +228,9 @@ infers the target architecture from the file-name suffix
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
just install # download module dependencies
|
just build # compile, zero errors and zero warnings
|
||||||
just build # go vet + gofmt check, zero errors and zero warnings
|
just test # the suite, no cache, the 80 % coverage floor
|
||||||
just test # full suite, race detector, 80 % coverage gate
|
just gates # build, fmt-check, vet, test, race: the definition of done
|
||||||
just fmt # gofmt the tree
|
just fmt # gofmt the tree
|
||||||
just gen # regenerate the instruction tables from the Go toolchain
|
just gen # regenerate the instruction tables from the Go toolchain
|
||||||
```
|
```
|
||||||
@@ -145,14 +241,16 @@ recipe.
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
|
||||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||||
|
- man pages: `just install-man` installs gasm(1) and one page per command
|
||||||
|
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
|
||||||
|
them
|
||||||
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||||
- [docs/DECISIONS.md](docs/DECISIONS.md): deferred design decisions
|
|
||||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
BSD-3-Clause — see [LICENSE](LICENSE).
|
BSD-3-Clause; see [LICENSE](LICENSE).
|
||||||
|
|
||||||
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Security policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
Security fixes go to the newest release and to the `development` branch. Older
|
||||||
|
releases do not receive them.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
|---|---|
|
||||||
|
| 0.33.0 | yes |
|
||||||
|
| older releases | no |
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
**Do not open a public issue for a security problem.** A public report tells everyone
|
||||||
|
about the flaw before there is a fix. Report it privately to
|
||||||
|
**opensource@petrbalvin.org**.
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- the version or commit you tested, and the platform
|
||||||
|
- what the problem is, and what an attacker gains from it
|
||||||
|
- the smallest reproducer you have, ideally a test or a single command
|
||||||
|
- a suggested fix, if you have one
|
||||||
|
|
||||||
|
## What to expect
|
||||||
|
|
||||||
|
- A human reads the report, and you get an acknowledgement.
|
||||||
|
- You are kept informed while the fix is being made, and told when it ships.
|
||||||
|
- The fix is released before the details are published, and the timing is agreed with
|
||||||
|
you.
|
||||||
|
- The reporter is credited in the release notes unless they ask otherwise.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Findings that require the attacker to already run code as the user, or to have local
|
||||||
|
access.
|
||||||
|
- Missing hardening with no demonstrated impact.
|
||||||
|
- Flaws in a third-party dependency: report them to that project, and to this one only
|
||||||
|
when this project's use of it makes them reachable.
|
||||||
+1
-1
@@ -29,7 +29,7 @@ func arm64Registers() []Register {
|
|||||||
regs = append(regs, Register{Name: name, Class: class, Desc: desc})
|
regs = append(regs, Register{Name: name, Class: class, Desc: desc})
|
||||||
}
|
}
|
||||||
|
|
||||||
// General-purpose integer registers R0–R30.
|
// General-purpose integer registers R0-R30.
|
||||||
for i := 0; i <= 30; i++ {
|
for i := 0; i <= 30; i++ {
|
||||||
add(fmt.Sprintf("R%d", i), GPR, "64-bit general-purpose register")
|
add(fmt.Sprintf("R%d", i), GPR, "64-bit general-purpose register")
|
||||||
}
|
}
|
||||||
|
|||||||
+148
-52
@@ -12,17 +12,21 @@ import (
|
|||||||
|
|
||||||
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
||||||
// code. Every instruction is 4 bytes; the MOV pseudo-instruction and the
|
// code. Every instruction is 4 bytes; the MOV pseudo-instruction and the
|
||||||
// immediate-arithmetic forms expand to 2–4 instructions when the immediate
|
// immediate-arithmetic forms expand to 2-4 instructions when the immediate
|
||||||
// does not fit, so the layout is computed in two passes (sizes, then encoding
|
// does not fit, so the layout is computed in two passes (sizes, then encoding
|
||||||
// with resolved branch targets).
|
// with resolved branch targets).
|
||||||
//
|
//
|
||||||
// The emitted bytes match the Go toolchain's arm64 assembler, which is the
|
// The emitted bytes match the Go toolchain's arm64 assembler, which is the
|
||||||
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
|
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
|
||||||
// encodings and the MOV immediate expansions all follow cmd/internal/obj/
|
// encodings and the MOV immediate expansions all follow cmd/internal/obj/
|
||||||
// arm64's asmout cases.
|
// arm64's asmout cases. One deliberate difference: the stack-growth guard
|
||||||
|
// (the morestack check in the prologue and the call back into the runtime in
|
||||||
|
// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions
|
||||||
|
// or zero-frame leaves, where the toolchain emits no guard either.
|
||||||
func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
||||||
fi := arm64ComputeFrame(t)
|
fi := arm64ComputeFrame(t)
|
||||||
prologue := arm64Prologue(fi)
|
prologue := arm64Prologue(fi)
|
||||||
|
guardLen := arm64GuardLen(fi)
|
||||||
chain := arm64JumpChain(t)
|
chain := arm64JumpChain(t)
|
||||||
resolve := func(name string) string {
|
resolve := func(name string) string {
|
||||||
if r, ok := chain[name]; ok {
|
if r, ok := chain[name]; ok {
|
||||||
@@ -35,14 +39,14 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
var spadj []SpadjStep
|
var spadj []SpadjStep
|
||||||
|
|
||||||
// The prologue (3 instructions when a small frame, 4 for large)
|
// The prologue (3 instructions when a small frame, 4 for large)
|
||||||
// raises the SP delta by autosize.
|
// raises the SP delta by autosize. The guard prefix shifts its PC.
|
||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
spadj = append(spadj, SpadjStep{PC: arm64PrologueSpadjPC(fi), Value: fi.autosize})
|
spadj = append(spadj, SpadjStep{PC: guardLen + arm64PrologueSpadjPC(fi), Value: fi.autosize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: label offsets from the instruction sizes.
|
// Pass 1: label offsets from the instruction sizes.
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pos := len(prologue)
|
pos := guardLen + len(prologue)
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -52,9 +56,25 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: encode. Relocation offsets are recorded function-relative.
|
// Pass 2: encode. The guard prefix precedes the prologue; its branches
|
||||||
out := append([]byte(nil), prologue...)
|
// target the morestack block at the end of the function, whose position
|
||||||
pc := len(prologue)
|
// the first pass has settled.
|
||||||
|
bodyLen := 0
|
||||||
|
{
|
||||||
|
p := guardLen + len(prologue)
|
||||||
|
for _, stmt := range t.Body {
|
||||||
|
if in, ok := stmt.(*ast.Instr); ok {
|
||||||
|
p += arm64InstrSize(in, fi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyLen = p - (guardLen + len(prologue))
|
||||||
|
}
|
||||||
|
var out []byte
|
||||||
|
if fi.needSplit {
|
||||||
|
out = append(out, arm64GuardBytes(fi, guardLen+len(prologue)+bodyLen)...)
|
||||||
|
}
|
||||||
|
out = append(out, prologue...)
|
||||||
|
pc := guardLen + len(prologue)
|
||||||
preCount := len(relocs)
|
preCount := len(relocs)
|
||||||
var lines []LineEntry
|
var lines []LineEntry
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
@@ -67,7 +87,12 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
for j := preCount; j < len(relocs); j++ {
|
for j := preCount; j < len(relocs); j++ {
|
||||||
relocs[j].Off += pc - len(prologue)
|
// Make the relocation offsets function-relative: each instruction
|
||||||
|
// records its reloc offset relative to its own start, and pc is
|
||||||
|
// that instruction's offset from the function start (prologue
|
||||||
|
// included). After shifts by the same amount.
|
||||||
|
relocs[j].Off += pc
|
||||||
|
relocs[j].After += pc
|
||||||
}
|
}
|
||||||
preCount = len(relocs)
|
preCount = len(relocs)
|
||||||
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
|
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
|
||||||
@@ -79,6 +104,12 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pc += len(code)
|
pc += len(code)
|
||||||
}
|
}
|
||||||
|
if fi.needSplit {
|
||||||
|
block, blReloc := arm64MoreStackBlock(pc)
|
||||||
|
out = append(out, block...)
|
||||||
|
relocs = append(relocs, blReloc)
|
||||||
|
pc += len(block)
|
||||||
|
}
|
||||||
return out, offsets, relocs, lines, spadj, nil
|
return out, offsets, relocs, lines, spadj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +222,7 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
|
|||||||
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
|
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
|
||||||
}
|
}
|
||||||
return a64wordLE(uint32(immFromOperand(ops[0]))), nil
|
return a64wordLE(uint32(immFromOperand(ops[0]))), nil
|
||||||
case "B":
|
case "B", "JMP":
|
||||||
return encodeARM64Branch(mnem, ops, pc, offsets, false, relocs, resolve)
|
return encodeARM64Branch(mnem, ops, pc, offsets, false, relocs, resolve)
|
||||||
case "BL", "CALL":
|
case "BL", "CALL":
|
||||||
return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve)
|
return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve)
|
||||||
@@ -205,6 +236,11 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
|
|||||||
return encodeARM64BranchCond(mnem, enc.op, ops, pc, offsets, resolve)
|
return encodeARM64BranchCond(mnem, enc.op, ops, pc, offsets, resolve)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unconditional register branches (BR, BLR).
|
||||||
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FUncondBranch {
|
||||||
|
return encodeARM64RegBranch(mnem, enc.op, ops)
|
||||||
|
}
|
||||||
|
|
||||||
// ADD/SUB immediate.
|
// ADD/SUB immediate.
|
||||||
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
||||||
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
||||||
@@ -306,8 +342,27 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
|||||||
}
|
}
|
||||||
op := ops[0]
|
op := ops[0]
|
||||||
|
|
||||||
// External symbol reference: BL sym(SB).
|
// Register-indirect: JMP (R0) is BR R0, CALL (R0) is BLR R0. The
|
||||||
if link && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
// toolchain's spelling carries no offset and no index; anything else
|
||||||
|
// is reported rather than silently dropped.
|
||||||
|
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||||
|
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||||
|
return nil, fmt.Errorf("%s: invalid indirect branch operand %q", mnem, op.Raw)
|
||||||
|
}
|
||||||
|
rn := arm64RegNum(op.Addr.Base)
|
||||||
|
if rn < 0 {
|
||||||
|
return nil, fmt.Errorf("%s: unknown branch register %q", mnem, op.Addr.Base)
|
||||||
|
}
|
||||||
|
opc := uint32(0) // BR
|
||||||
|
if link {
|
||||||
|
opc = 1 // BLR
|
||||||
|
}
|
||||||
|
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
||||||
|
// relocation (R_CALLARM64 either way).
|
||||||
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
||||||
if relocs != nil {
|
if relocs != nil {
|
||||||
*relocs = append(*relocs, Reloc{
|
*relocs = append(*relocs, Reloc{
|
||||||
Off: 0,
|
Off: 0,
|
||||||
@@ -317,8 +372,12 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
|||||||
Kind: RelArm64Branch,
|
Kind: RelArm64Branch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Emit BL with zero offset; the linker fills in the target.
|
// Emit B/BL with zero offset; the linker fills in the target.
|
||||||
return a64wordLE(a64Branch(1, 0)), nil
|
bop := uint32(0) // B
|
||||||
|
if link {
|
||||||
|
bop = 1 // BL
|
||||||
|
}
|
||||||
|
return a64wordLE(a64Branch(bop, 0)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
target := resolve(arm64Label(op))
|
target := resolve(arm64Label(op))
|
||||||
@@ -337,6 +396,19 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
|||||||
return a64wordLE(a64Branch(bop, int32(rel))), nil
|
return a64wordLE(a64Branch(bop, int32(rel))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeARM64RegBranch encodes BR/BLR through a register operand:
|
||||||
|
// BR Xn = 0xd61f0000 | Rn<<5, BLR Xn = 0xd63f0000 | Rn<<5.
|
||||||
|
func encodeARM64RegBranch(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
||||||
|
if len(ops) != 1 {
|
||||||
|
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||||
|
}
|
||||||
|
rn := arm64RegNum(operandRegName(ops[0]))
|
||||||
|
if rn < 0 {
|
||||||
|
return nil, fmt.Errorf("%s expects a register operand", mnem)
|
||||||
|
}
|
||||||
|
return a64wordLE(uint32(baseOp) | 31<<16 | uint32(rn)<<5), nil
|
||||||
|
}
|
||||||
|
|
||||||
// encodeARM64BranchCond encodes a conditional branch (B.cond) to a label.
|
// encodeARM64BranchCond encodes a conditional branch (B.cond) to a label.
|
||||||
func encodeARM64BranchCond(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
func encodeARM64BranchCond(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
||||||
if len(ops) != 1 {
|
if len(ops) != 1 {
|
||||||
@@ -462,7 +534,7 @@ func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
|
|||||||
|
|
||||||
// ---- MOV pseudo-instruction ----
|
// ---- MOV pseudo-instruction ----
|
||||||
|
|
||||||
// encodeARM64Mov encodes the MOV family — the load/store/immediate workhorse
|
// encodeARM64Mov encodes the MOV family, the load/store/immediate workhorse
|
||||||
// of Go's arm64 assembly. MOV is an alias of MOVD (the width mnemonics
|
// of Go's arm64 assembly. MOV is an alias of MOVD (the width mnemonics
|
||||||
// select the access width). The forms, mirroring the toolchain:
|
// select the access width). The forms, mirroring the toolchain:
|
||||||
//
|
//
|
||||||
@@ -562,9 +634,9 @@ func arm64MovSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
|
|||||||
}
|
}
|
||||||
_, off := arm64MemWithFrame(mem, fi)
|
_, off := arm64MemWithFrame(mem, fi)
|
||||||
// Scaled unsigned offset fits if aligned and in range.
|
// Scaled unsigned offset fits if aligned and in range.
|
||||||
lt := a64LoadTable[mnem]
|
lt, ok := a64LoadTable[mnem]
|
||||||
if lt.size == 0 {
|
if !ok {
|
||||||
lt.size = 3 // default to64-bit for MOV
|
lt = a64LoadTable["MOVD"] // the MOV pseudo is a 64-bit access
|
||||||
}
|
}
|
||||||
scale := int32(1) << uint(lt.size)
|
scale := int32(1) << uint(lt.size)
|
||||||
if off >= 0 && off%scale == 0 && off/scale < 4096 {
|
if off >= 0 && off%scale == 0 && off/scale < 4096 {
|
||||||
@@ -573,7 +645,10 @@ func arm64MovSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
|
|||||||
if off >= -256 && off <= 255 {
|
if off >= -256 && off <= 255 {
|
||||||
return 4 // unscaled
|
return 4 // unscaled
|
||||||
}
|
}
|
||||||
return 12 // materialise offset + LDR/STR
|
if _, _, _, ok := arm64SplitOffset(off, scale); ok {
|
||||||
|
return 8 // ADD base, REGTMP + access
|
||||||
|
}
|
||||||
|
return 12 // literal pool range: encoding reports it as unsupported
|
||||||
default:
|
default:
|
||||||
return 4 // register move
|
return 4 // register move
|
||||||
}
|
}
|
||||||
@@ -793,33 +868,53 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
|
|||||||
}
|
}
|
||||||
|
|
||||||
scale := int32(1) << uint(lt.size)
|
scale := int32(1) << uint(lt.size)
|
||||||
if load {
|
|
||||||
// Try scaled unsigned offset first.
|
|
||||||
if off >= 0 && off%scale == 0 {
|
|
||||||
imm12 := uint32(off / scale)
|
|
||||||
if imm12 < 4096 {
|
|
||||||
return a64wordLE(a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), imm12, uint32(rn), uint32(reg))), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Try unscaled (9-bit signed).
|
|
||||||
if off >= -256 && off <= 255 {
|
|
||||||
return a64wordLE(a64LSUnscaled(lt.size, lt.V, lt.opc, off, rn, reg)), nil
|
|
||||||
}
|
|
||||||
// Large offset: materialise in R20 (TMP) and use register-offset.
|
|
||||||
return nil, fmt.Errorf("%s: offset %d out of range", mnem, off)
|
|
||||||
}
|
|
||||||
// Store: same encoding but opc bits indicate store.
|
|
||||||
storeOpc := a64StoreOpc(lt)
|
storeOpc := a64StoreOpc(lt)
|
||||||
if off >= 0 && off%scale == 0 {
|
var opc int
|
||||||
imm12 := uint32(off / scale)
|
if load {
|
||||||
if imm12 < 4096 {
|
opc = lt.opc
|
||||||
return a64wordLE(a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), imm12, uint32(rn), uint32(reg))), nil
|
} else {
|
||||||
|
opc = storeOpc
|
||||||
}
|
}
|
||||||
|
// Scaled unsigned offset first, then the unscaled ±255 form.
|
||||||
|
if off >= 0 && off%scale == 0 && off/scale < 4096 {
|
||||||
|
return a64wordLE(a64LSU(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(off/scale), uint32(rn), uint32(reg))), nil
|
||||||
}
|
}
|
||||||
if off >= -256 && off <= 255 {
|
if off >= -256 && off <= 255 {
|
||||||
return a64wordLE(a64LSUnscaled(lt.size, lt.V, storeOpc, off, rn, reg)), nil
|
return a64wordLE(a64LSUnscaled(lt.size, lt.V, opc, off, rn, reg)), nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%s: offset %d out of range", mnem, off)
|
// Large offset: materialise the base in REGTMP (R27) the way the
|
||||||
|
// toolchain does and access what remains.
|
||||||
|
addImm, addShift, access, ok := arm64SplitOffset(off, scale)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
|
||||||
|
}
|
||||||
|
return a64WordsLE(
|
||||||
|
a64AddSub(1, 0, 0, addShift, uint32(addImm), 31, 27), // ADD $addImm<<shift, SP, R27
|
||||||
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(access/scale), 27, uint32(reg)),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64SplitOffset decomposes an out-of-range frame offset for a REGTMP
|
||||||
|
// base: an ADD (plain, or shifted left by 12) brings SP near the target and
|
||||||
|
// the access covers what remains. ok is false when no decomposition exists
|
||||||
|
// (offsets at or beyond 16 MiB, where the toolchain falls back to a literal
|
||||||
|
// pool).
|
||||||
|
func arm64SplitOffset(off int32, scale int32) (addImm, addShift uint32, access int32, ok bool) {
|
||||||
|
if off < 0 {
|
||||||
|
return 0, 0, 0, false
|
||||||
|
}
|
||||||
|
// Plain ADD: bring SP to within the largest scaled access.
|
||||||
|
l := min(off, 4095*scale)
|
||||||
|
l -= l % scale
|
||||||
|
if a := off - l; a <= 4095 {
|
||||||
|
return uint32(a), 0, l, true
|
||||||
|
}
|
||||||
|
// Shifted ADD: cover everything but the bits the access imm12 carries.
|
||||||
|
rest := off &^ (0xFFF * scale)
|
||||||
|
if rest >= 0 && rest>>12 <= 4095 {
|
||||||
|
return uint32(rest >> 12), 1, off - rest, true
|
||||||
|
}
|
||||||
|
return 0, 0, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- static symbol references (ADRP + offset) ----
|
// ---- static symbol references (ADRP + offset) ----
|
||||||
@@ -839,7 +934,9 @@ func encodeARM64SBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeARM64SBLoad emits ADRP R20, 0; LDR Rd, [R20, 0] with relocations.
|
// encodeARM64SBLoad emits ADRP R27, 0; LDR Rd, [R27, 0] with relocations,
|
||||||
|
// matching the toolchain: the scratch register is REGTMP (R27) and the pair
|
||||||
|
// carries R_ARM64_PCREL_LDST64.
|
||||||
func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
||||||
lt, ok := a64LoadTable[mnem]
|
lt, ok := a64LoadTable[mnem]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -847,17 +944,17 @@ func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([
|
|||||||
}
|
}
|
||||||
if relocs != nil {
|
if relocs != nil {
|
||||||
*relocs = append(*relocs,
|
*relocs = append(*relocs,
|
||||||
Reloc{Off: 0, After: 0, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
|
||||||
Reloc{Off: 4, After: 4, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return a64WordsLE(
|
return a64WordsLE(
|
||||||
a64ADR(1, 0, 0, 20), // ADRP R20, 0
|
a64ADR(1, 0, 0, 27), // ADRP R27, 0
|
||||||
a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), 0, 20, uint32(rd)), // LDR Rd, [R20, #0]
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), 0, 27, uint32(rd)), // LDR Rd, [R27, #0]
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeARM64SBStore emits ADRP R20, 0; STR Rs, [R20, 0] with relocations.
|
// encodeARM64SBStore emits ADRP R27, 0; STR Rs, [R27, 0] with relocations,
|
||||||
|
// matching the toolchain's R27 scratch and R_ARM64_PCREL_LDST64 pair.
|
||||||
func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
||||||
lt, ok := a64LoadTable[mnem]
|
lt, ok := a64LoadTable[mnem]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -866,13 +963,12 @@ func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) (
|
|||||||
storeOpc := a64StoreOpc(lt)
|
storeOpc := a64StoreOpc(lt)
|
||||||
if relocs != nil {
|
if relocs != nil {
|
||||||
*relocs = append(*relocs,
|
*relocs = append(*relocs,
|
||||||
Reloc{Off: 0, After: 0, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
|
||||||
Reloc{Off: 4, After: 4, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return a64WordsLE(
|
return a64WordsLE(
|
||||||
a64ADR(1, 0, 0, 20), // ADRP R20, 0
|
a64ADR(1, 0, 0, 27), // ADRP R27, 0
|
||||||
a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), 0, 20, uint32(rs)), // STR Rs, [R20, #0]
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), 0, 27, uint32(rs)), // STR Rs, [R27, #0]
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,7 +1188,7 @@ func encodeARM64CSEL(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
|
|||||||
return a64wordLE(baseOp | uint32(rn)<<16 | invCond<<12 | uint32(rn)<<5 | uint32(rd)), nil
|
return a64wordLE(baseOp | uint32(rn)<<16 | invCond<<12 | uint32(rn)<<5 | uint32(rd)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSEL cond, Rn, Rm, Rd (4 operands) — condition first.
|
// CSEL cond, Rn, Rm, Rd (4 operands), condition first.
|
||||||
// Go assembler syntax: CSEL cond, Rn, Rm, Rd
|
// Go assembler syntax: CSEL cond, Rn, Rm, Rd
|
||||||
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5], Rd in bits[4:0].
|
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5], Rd in bits[4:0].
|
||||||
if len(ops) != 4 {
|
if len(ops) != 4 {
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ package asm
|
|||||||
// an opcode constant, and the format selects the bit layout. The opcode
|
// an opcode constant, and the format selects the bit layout. The opcode
|
||||||
// constants and formats are transcribed from the Go toolchain's own arm64
|
// constants and formats are transcribed from the Go toolchain's own arm64
|
||||||
// backend (cmd/internal/obj/arm64), so the emitted bytes match `go tool asm`
|
// backend (cmd/internal/obj/arm64), so the emitted bytes match `go tool asm`
|
||||||
// exactly — the ground-truth oracle for the verify suite.
|
// exactly, the ground-truth oracle for the verify suite.
|
||||||
//
|
//
|
||||||
// All AArch64 instructions are 32 bits, little-endian. The formats used here
|
// All AArch64 instructions are 32 bits, little-endian. The formats used here
|
||||||
// (per the ARM Architecture Reference Manual):
|
// (per the ARM Architecture Reference Manual):
|
||||||
@@ -28,7 +28,7 @@ package asm
|
|||||||
// ADR/ADRP p<<31 | 0x10<<24 | immlo<<29 | immhi<<5 | Rd
|
// ADR/ADRP p<<31 | 0x10<<24 | immlo<<29 | immhi<<5 | Rd
|
||||||
|
|
||||||
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
// arm64RegNum returns the 5-bit register number for an AArch64 register name:
|
||||||
// R0–R30 (integer), F0–F31 (floating point), and the ABI aliases the
|
// R0-R30 (integer), F0-F31 (floating point), and the ABI aliases the
|
||||||
// runtime's assembly uses. Returns -1 for an unrecognised name.
|
// runtime's assembly uses. Returns -1 for an unrecognised name.
|
||||||
func arm64RegNum(name string) int {
|
func arm64RegNum(name string) int {
|
||||||
switch name {
|
switch name {
|
||||||
@@ -99,7 +99,7 @@ func arm64RegNum(name string) int {
|
|||||||
case "SP":
|
case "SP":
|
||||||
return 31 // SP and ZR share encoding 31; context determines meaning
|
return 31 // SP and ZR share encoding 31; context determines meaning
|
||||||
}
|
}
|
||||||
// F0–F31.
|
// F0-F31.
|
||||||
if len(name) >= 1 && name[0] == 'F' {
|
if len(name) >= 1 && name[0] == 'F' {
|
||||||
n := 0
|
n := 0
|
||||||
for i := 1; i < len(name); i++ {
|
for i := 1; i < len(name); i++ {
|
||||||
|
|||||||
@@ -572,3 +572,42 @@ func leWords(b []byte) []uint32 {
|
|||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestArm64IndirectBranch pins the indirect branch forms in a leaf function:
|
||||||
|
// JMP (Rn) lowers to BR Rn, matching the toolchain's spelling, and the raw
|
||||||
|
// BR/BLR mnemonics encode directly (a gasm superset the toolchain's front
|
||||||
|
// end does not accept). CALL (Rn) shares the BLR path and its non-leaf
|
||||||
|
// prologue parity is covered by the ground-truth kernel.
|
||||||
|
func TestArm64IndirectBranch(t *testing.T) {
|
||||||
|
src := `#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0-0
|
||||||
|
JMP (R0)
|
||||||
|
BR R5
|
||||||
|
BLR R6
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("test_arm64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileARM64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFileARM64: %v", err)
|
||||||
|
}
|
||||||
|
want := []uint32{
|
||||||
|
0xd61f0000, // BR R0
|
||||||
|
0xd61f00a0, // BR R5
|
||||||
|
0xd63f00c0, // BLR R6
|
||||||
|
0xd65f03c0, // RET (BR LR)
|
||||||
|
}
|
||||||
|
got := leWords(img.Code)
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("word count = %d, want %d", len(got), len(want))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+182
-10
@@ -63,6 +63,12 @@ type arm64FrameInfo struct {
|
|||||||
args int // the declared -argsize
|
args int // the declared -argsize
|
||||||
noSplit bool // the NOSPLIT flag
|
noSplit bool // the NOSPLIT flag
|
||||||
leaf bool // no call instructions in the body
|
leaf bool // no call instructions in the body
|
||||||
|
|
||||||
|
// Stack-split guard state: needSplit mirrors the toolchain, which skips
|
||||||
|
// the check for NOSPLIT functions and auto-marks leaf functions with an
|
||||||
|
// autosize below StackSmall as NOSPLIT.
|
||||||
|
needSplit bool
|
||||||
|
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
|
||||||
}
|
}
|
||||||
|
|
||||||
// arm64ComputeFrame derives the frame layout for a TEXT function.
|
// arm64ComputeFrame derives the frame layout for a TEXT function.
|
||||||
@@ -80,15 +86,68 @@ func arm64ComputeFrame(t *ast.Text) arm64FrameInfo {
|
|||||||
|
|
||||||
if fi.frame != 0 || !fi.leaf {
|
if fi.frame != 0 || !fi.leaf {
|
||||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||||
if fi.autosize%16 != 0 {
|
// The toolchain always adds an extrasize: 8 when the total leaves a
|
||||||
// The toolchain aligns to 16: if autosize%16 == 8, add 8;
|
// 16-byte alignment gap, another 16 when already aligned.
|
||||||
// otherwise add whatever is needed.
|
switch fi.autosize % 16 {
|
||||||
|
case 8:
|
||||||
|
fi.autosize += 8
|
||||||
|
case 0:
|
||||||
|
fi.autosize += 16
|
||||||
|
default:
|
||||||
|
// The toolchain rejects unaligned frames; round up so such
|
||||||
|
// sources still assemble.
|
||||||
fi.autosize += 16 - (fi.autosize % 16)
|
fi.autosize += 16 - (fi.autosize % 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
|
case fi.noSplit:
|
||||||
|
case fi.autosize < stackSmall && fi.leaf:
|
||||||
|
// Auto-NOSPLIT, as the toolchain's leaf mark concludes.
|
||||||
|
default:
|
||||||
|
fi.needSplit = true
|
||||||
|
switch {
|
||||||
|
case fi.autosize <= stackSmall:
|
||||||
|
fi.splitClass = 0
|
||||||
|
case fi.autosize <= stackBig:
|
||||||
|
fi.splitClass = 1
|
||||||
|
default:
|
||||||
|
fi.splitClass = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
return fi
|
return fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arm64GuardLen returns the byte length of the stack-split guard prefix
|
||||||
|
// (zero when the function needs no guard). The big class materialises
|
||||||
|
// framesize-StackSmall into REGTMP, whose MOVZ/MOVK sequence length varies.
|
||||||
|
func arm64GuardLen(fi arm64FrameInfo) int {
|
||||||
|
if !fi.needSplit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
return 12
|
||||||
|
case 1:
|
||||||
|
return 16
|
||||||
|
default:
|
||||||
|
n, err := arm64LoadImmLen(int64(fi.autosize - stackSmall))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 4 + n + 4 + 4 + 4 + 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64LoadImmLen returns the byte length of the MOVZ/MOVK sequence that
|
||||||
|
// loads v into a register.
|
||||||
|
func arm64LoadImmLen(v int64) (int, error) {
|
||||||
|
b, err := encodeARM64LoadImm(27, v, "MOVD")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
// arm64IsLeaf reports whether a function contains no call instructions
|
// arm64IsLeaf reports whether a function contains no call instructions
|
||||||
// (BL/CALL), matching the toolchain's LEAF mark.
|
// (BL/CALL), matching the toolchain's LEAF mark.
|
||||||
func arm64IsLeaf(t *ast.Text) bool {
|
func arm64IsLeaf(t *ast.Text) bool {
|
||||||
@@ -119,12 +178,47 @@ func arm64Prologue(fi arm64FrameInfo) []byte {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Large frame: SUB $autosize, SP, R20; STP (FP,LR), -8(R20); ADD $0, R20, SP; SUB $8, SP, FP
|
// Large frame: SUB $autosize, SP, R20; STP (FP,LR), -8(R20); ADD $0, R20, SP; SUB $8, SP, FP
|
||||||
return a64WordsLE(
|
ws := arm64SubImmWords(uint32(fi.autosize), 20)
|
||||||
a64AddSub(1, 1, 0, 0, uint32(fi.autosize), 31, 20), // SUB $autosize, SP, R20
|
ws = append(ws,
|
||||||
a64LSP(2, 0, 0, -1, 30, 20, 29), // STP FP, LR, [R20, #-8] (opc=2 for 64-bit pair)
|
a64LSP(2, 0, 0, -1, 30, 20, 29), // STP FP, LR, [R20, #-8] (opc=2 for 64-bit pair)
|
||||||
a64AddSub(1, 0, 0, 0, 0, 20, 31), // ADD $0, R20, SP (= MOV R20, SP)
|
a64AddSub(1, 0, 0, 0, 0, 20, 31), // ADD $0, R20, SP (= MOV R20, SP)
|
||||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||||
)
|
)
|
||||||
|
return a64WordsLE(ws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64SubImmWords emits SUB $imm, SP, Rd: the immediate form when the value
|
||||||
|
// fits the imm12 field (plain, or shifted left by 12 when it is a multiple
|
||||||
|
// of 4096); otherwise the toolchain materialises it into REGTMP (R27) and
|
||||||
|
// subtracts the register in the extended-register form.
|
||||||
|
func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
|
||||||
|
if imm <= 0xFFF {
|
||||||
|
return []uint32{a64AddSub(1, 1, 0, 0, imm, 31, rd)}
|
||||||
|
}
|
||||||
|
if imm <= 4095<<12 && imm&0xFFF == 0 {
|
||||||
|
return []uint32{a64AddSub(1, 1, 0, 1, imm>>12, 31, rd)}
|
||||||
|
}
|
||||||
|
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||||
|
if err != nil {
|
||||||
|
mov = nil
|
||||||
|
}
|
||||||
|
return append(wordsOf(mov), arm64DPExtWords(arm64OpSub, 27, 31, rd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64AddImmWords emits ADD $imm, SP, Rd with the same imm12, shifted-imm12
|
||||||
|
// and REGTMP fallback ladder.
|
||||||
|
func arm64AddImmWords(imm uint32, rd uint32) []uint32 {
|
||||||
|
if imm <= 0xFFF {
|
||||||
|
return []uint32{a64AddSub(1, 0, 0, 0, imm, 31, rd)}
|
||||||
|
}
|
||||||
|
if imm <= 4095<<12 && imm&0xFFF == 0 {
|
||||||
|
return []uint32{a64AddSub(1, 0, 0, 1, imm>>12, 31, rd)}
|
||||||
|
}
|
||||||
|
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||||
|
if err != nil {
|
||||||
|
mov = nil
|
||||||
|
}
|
||||||
|
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, rd))
|
||||||
}
|
}
|
||||||
|
|
||||||
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
|
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
|
||||||
@@ -134,10 +228,8 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
|||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
if fi.leaf {
|
if fi.leaf {
|
||||||
// Leaf with frame: ADD $autosize-8, SP, FP; ADD $autosize, SP, SP
|
// Leaf with frame: ADD $autosize-8, SP, FP; ADD $autosize, SP, SP
|
||||||
ws = append(ws,
|
ws = append(ws, arm64AddImmWords(uint32(fi.autosize-8), 29)...)
|
||||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize-8), 31, 29), // ADD $autosize-8, SP, FP
|
ws = append(ws, arm64AddImmWords(uint32(fi.autosize), 31)...)
|
||||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
|
||||||
)
|
|
||||||
} else if fi.autosize <= 0xf0 {
|
} else if fi.autosize <= 0xf0 {
|
||||||
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #autosize
|
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #autosize
|
||||||
ws = append(ws,
|
ws = append(ws,
|
||||||
@@ -148,8 +240,8 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
|||||||
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP
|
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP
|
||||||
ws = append(ws,
|
ws = append(ws,
|
||||||
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
||||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
|
||||||
)
|
)
|
||||||
|
ws = append(ws, arm64AddImmWords(uint32(fi.autosize), 31)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// RET: BR LR (0xd65f03c0)
|
// RET: BR LR (0xd65f03c0)
|
||||||
@@ -235,3 +327,83 @@ func arm64PostLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
|||||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||||
1<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
1<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data-processing (shifted register) base opcodes for the guard blocks.
|
||||||
|
const (
|
||||||
|
arm64OpAdd = 1<<31 | 0<<30 | 0<<29 | 0x0b<<24
|
||||||
|
arm64OpSub = 1<<31 | 1<<30 | 0<<29 | 0x0b<<24
|
||||||
|
arm64OpSubs = 1<<31 | 1<<30 | 1<<29 | 0x0b<<24
|
||||||
|
)
|
||||||
|
|
||||||
|
// arm64DPSRWords builds one data-processing (shifted register) word:
|
||||||
|
// OP Rm, Rn, Rd in the Go assembler's operand order.
|
||||||
|
func arm64DPSRWords(base uint32, rm, rn, rd uint32) uint32 {
|
||||||
|
return base | rm<<16 | rn<<5 | rd
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64DPExtWords builds one data-processing (extended register) word, the
|
||||||
|
// form the toolchain picks when a large immediate was materialised into
|
||||||
|
// REGTMP before the operation: base | 1<<21 | Rm<<16 | UXTX<<13 | Rn<<5 | Rd.
|
||||||
|
func arm64DPExtWords(base, rm, rn, rd uint32) uint32 {
|
||||||
|
return base | 1<<21 | rm<<16 | 3<<13 | rn<<5 | rd
|
||||||
|
}
|
||||||
|
|
||||||
|
// wordsOf converts little-endian instruction bytes back to words.
|
||||||
|
func wordsOf(b []byte) []uint32 {
|
||||||
|
ws := make([]uint32, 0, len(b)/4)
|
||||||
|
for i := 0; i+4 <= len(b); i += 4 {
|
||||||
|
ws = append(ws, uint32(b[i])|uint32(b[i+1])<<8|uint32(b[i+2])<<16|uint32(b[i+3])<<24)
|
||||||
|
}
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64GuardBytes emits the stack-split guard prefix; blockStart is the
|
||||||
|
// function-relative byte address of the morestack block the branches target.
|
||||||
|
func arm64GuardBytes(fi arm64FrameInfo, blockStart int) []byte {
|
||||||
|
// MOVD 16(R28), R16 (g.stackguard0)
|
||||||
|
ws := []uint32{a64LSU(3, 0, 1, 2, 28, 16)}
|
||||||
|
br := func(from int, cond uint32) uint32 {
|
||||||
|
return a64BranchCond(int32((blockStart-from)>>2), cond)
|
||||||
|
}
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
// CMP R16, RSP in the exact encoding go tool asm emits for it.
|
||||||
|
ws = append(ws, 0xeb3063ff)
|
||||||
|
ws = append(ws, br(8, a64CondLS))
|
||||||
|
case 1:
|
||||||
|
ws = append(ws, a64AddSub(1, 1, 0, 0, uint32(fi.autosize-stackSmall), 31, 17))
|
||||||
|
ws = append(ws, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17
|
||||||
|
ws = append(ws, br(12, a64CondLS))
|
||||||
|
default:
|
||||||
|
mov, err := encodeARM64LoadImm(27, int64(fi.autosize-stackSmall), "MOVD")
|
||||||
|
if err != nil {
|
||||||
|
mov = nil
|
||||||
|
}
|
||||||
|
ws = append(ws, wordsOf(mov)...)
|
||||||
|
ml := len(mov) / 4
|
||||||
|
ws = append(ws, arm64DPExtWords(arm64OpSubs, 27, 31, 17)) // SUBS R17, RSP, R27
|
||||||
|
ws = append(ws, br(8+ml, a64CondLO))
|
||||||
|
ws = append(ws, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17
|
||||||
|
ws = append(ws, br(8+ml+8, a64CondLS))
|
||||||
|
}
|
||||||
|
return a64WordsLE(ws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// arm64MoreStackBlock emits the trailing block: MOVD R30, R3 (save LR),
|
||||||
|
// BL runtime.morestack_noctxt, B back to the function start. The BL carries
|
||||||
|
// the R_CALLARM64 relocation.
|
||||||
|
func arm64MoreStackBlock(blockStart int) ([]byte, Reloc) {
|
||||||
|
ws := []uint32{
|
||||||
|
1<<31 | 1<<29 | 0x0a<<24 | 30<<16 | 31<<5 | 3, // MOVD R30, R3
|
||||||
|
a64Branch(1, 0), // BL, patched by the linker
|
||||||
|
}
|
||||||
|
bPC := blockStart + 8
|
||||||
|
ws = append(ws, a64Branch(0, int32(-bPC>>2))) // B back to the entry
|
||||||
|
reloc := Reloc{
|
||||||
|
Off: blockStart + 4,
|
||||||
|
After: blockStart + 8,
|
||||||
|
Name: "runtime\u00b7morestack_noctxt",
|
||||||
|
Kind: RelArm64Branch,
|
||||||
|
}
|
||||||
|
return a64WordsLE(ws...), reloc
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseArm64File is a helper assembling one arm64 source file.
|
||||||
|
func parseArm64File(t *testing.T, src string) *Image {
|
||||||
|
t.Helper()
|
||||||
|
f, errs := parser.Parse("k_arm64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileARM64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble: %v", err)
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestArm64RelocOffsetsIncludePrologue pins the function-relative relocation
|
||||||
|
// offsets of a framed function: the offsets used to exclude the prologue, so
|
||||||
|
// every relocation landed on a prologue instruction in the GOOBJ/ELF output.
|
||||||
|
// The function calls an external, so it is a non-leaf and carries the
|
||||||
|
// stack-split guard (12 bytes, small class) before the prologue.
|
||||||
|
func TestArm64RelocOffsetsIncludePrologue(t *testing.T) {
|
||||||
|
img := parseArm64File(t, "TEXT \u00b7f(SB), $16-0\n"+
|
||||||
|
"\tBL ext\u00b7foo(SB)\n"+
|
||||||
|
"\tMOVD $gdata(SB), R5\n"+
|
||||||
|
"\tMOVD $extsym(SB), R6\n"+
|
||||||
|
"\tRET\n"+
|
||||||
|
"GLOBL gdata(SB), $8\n")
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
|
||||||
|
// Layout: 12-byte guard, 12-byte prologue, BL (24), ADRP+ADD (28, 32),
|
||||||
|
// ADRP+ADD (36, 40), 12-byte epilogue with RET, 12-byte morestack block.
|
||||||
|
want := []struct {
|
||||||
|
off int
|
||||||
|
after int
|
||||||
|
name string
|
||||||
|
kind RelocKind
|
||||||
|
external bool
|
||||||
|
}{
|
||||||
|
{24, 28, "foo", RelArm64Branch, true},
|
||||||
|
{28, 28, "gdata", RelArm64Addr, false},
|
||||||
|
{32, 32, "gdata", RelArm64Addr, false},
|
||||||
|
{36, 36, "extsym", RelArm64Addr, true},
|
||||||
|
{40, 40, "extsym", RelArm64Addr, true},
|
||||||
|
{60, 64, "runtime\u00b7morestack_noctxt", RelArm64Branch, true},
|
||||||
|
}
|
||||||
|
if len(fn.Relocs) != len(want) {
|
||||||
|
t.Fatalf("relocs = %d, want %d", len(fn.Relocs), len(want))
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
r := fn.Relocs[i]
|
||||||
|
if r.Off != w.off || r.After != w.after || r.Name != w.name || r.Kind != w.kind || r.External != w.external {
|
||||||
|
t.Errorf("reloc %d = {off %d after %d name %q kind %d ext %v}, want {off %d after %d name %q kind %d ext %v}",
|
||||||
|
i, r.Off, r.After, r.Name, r.Kind, r.External, w.off, w.after, w.name, w.kind, w.external)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The BL with a zero offset sits exactly at the first reloc site.
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
if w := binary.LittleEndian.Uint32(code[24:28]); w != 0x94000000 {
|
||||||
|
t.Errorf("BL word = %08x, want 94000000", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestArm64SBLoadStoreMatchesToolchain pins the ADRP scratch register
|
||||||
|
// (REGTMP, R27) and the LDST64 relocation kind for sym loads and stores,
|
||||||
|
// against the bytes go tool asm emits for MOVD sym(SB), R5.
|
||||||
|
func TestArm64SBLoadStoreMatchesToolchain(t *testing.T) {
|
||||||
|
img := parseArm64File(t, "TEXT \u00b7ld(SB), NOSPLIT, $0\n"+
|
||||||
|
"\tMOVD sym(SB), R5\n"+
|
||||||
|
"\tMOVD R5, sym(SB)\n"+
|
||||||
|
"\tRET\n"+
|
||||||
|
"GLOBL sym(SB), $8\n")
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
|
||||||
|
// go tool asm: ADRP 0(PC), R27 (9000001b); MOVD (R27), R5 (f9400365);
|
||||||
|
// ADRP 0(PC), R27; MOVD R5, (R27) (f9000365).
|
||||||
|
for off, want := range map[int]uint32{0: 0x9000001b, 4: 0xf9400365, 8: 0x9000001b, 12: 0xf9000365} {
|
||||||
|
if got := binary.LittleEndian.Uint32(code[off : off+4]); got != want {
|
||||||
|
t.Errorf("word at %d = %08x, want %08x", off, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fn.Relocs) != 2 {
|
||||||
|
t.Fatalf("relocs = %d, want 2", len(fn.Relocs))
|
||||||
|
}
|
||||||
|
for i, w := range []struct{ off, after int }{{0, 8}, {8, 16}} {
|
||||||
|
r := fn.Relocs[i]
|
||||||
|
if r.Kind != RelArm64LDST64 {
|
||||||
|
t.Errorf("reloc %d kind = %d, want RelArm64LDST64 (%d)", i, r.Kind, RelArm64LDST64)
|
||||||
|
}
|
||||||
|
if r.Off != w.off || r.After != w.after {
|
||||||
|
t.Errorf("reloc %d = {off %d after %d}, want {off %d after %d}", i, r.Off, r.After, w.off, w.after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestArm64GOObjRelocTypes checks that GOOBJ emission succeeds with the new
|
||||||
|
// relocation kinds in play; the detailed layout is covered by the goobj tests.
|
||||||
|
func TestArm64GOObjRelocTypes(t *testing.T) {
|
||||||
|
img := parseArm64File(t, "TEXT \u00b7ld(SB), NOSPLIT, $0\n"+
|
||||||
|
"\tMOVD sym(SB), R5\n"+
|
||||||
|
"\tMOVD R5, sym(SB)\n"+
|
||||||
|
"\tRET\n"+
|
||||||
|
"GLOBL sym(SB), $8\n")
|
||||||
|
obj, err := img.GOObjectAARCH64("testpkg", "k_arm64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||||
|
}
|
||||||
|
if len(obj) == 0 {
|
||||||
|
t.Fatal("empty object")
|
||||||
|
}
|
||||||
|
// The detailed layout is covered by the goobj tests; here we only pin
|
||||||
|
// that emission succeeds with the new relocation kinds in play.
|
||||||
|
}
|
||||||
+365
-15
@@ -22,6 +22,11 @@ import (
|
|||||||
// FP/SP frame-relative operands, and local-label jumps. SB (global symbol)
|
// FP/SP frame-relative operands, and local-label jumps. SB (global symbol)
|
||||||
// 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.
|
||||||
|
//
|
||||||
|
// Like the other architectures, the stack-growth guard (the morestack check
|
||||||
|
// in the prologue and the call back into the runtime in the epilogue) is not
|
||||||
|
// emitted: the bytes match go tool asm only for NOSPLIT functions or
|
||||||
|
// zero-frame leaves, where the toolchain emits no guard either.
|
||||||
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
|
||||||
@@ -31,7 +36,7 @@ func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
|
|||||||
// the set of static symbols a GLOBL in the same file defines. A nil link
|
// the set of static symbols a GLOBL in the same file defines. A nil link
|
||||||
// rejects SB operands outright (single-function assembly cannot resolve
|
// rejects SB operands outright (single-function assembly cannot resolve
|
||||||
// them). When allowExternal is set, a reference to a symbol no GLOBL in the
|
// them). When allowExternal is set, a reference to a symbol no GLOBL in the
|
||||||
// file defines is recorded as an external relocation instead of failing —
|
// file defines is recorded as an external relocation instead of failing
|
||||||
// the object-file emitters resolve it at link time.
|
// the object-file emitters resolve it at link time.
|
||||||
type linkInfo struct {
|
type linkInfo struct {
|
||||||
symbols map[string]bool
|
symbols map[string]bool
|
||||||
@@ -46,6 +51,7 @@ type sbPatch struct {
|
|||||||
after int
|
after int
|
||||||
name string
|
name string
|
||||||
addend int64
|
addend int64
|
||||||
|
kind RelocKind
|
||||||
}
|
}
|
||||||
|
|
||||||
// spadjStep is one stack-adjustment boundary within a function: Value is the
|
// spadjStep is one stack-adjustment boundary within a function: Value is the
|
||||||
@@ -70,13 +76,18 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout: iterate jump sizes to a fixed point.
|
// Layout: iterate jump sizes to a fixed point. The stack-split guard
|
||||||
|
// prefix and the trailing morestack block participate in the iteration:
|
||||||
|
// their conditional branches relax from rel8 to rel32 when the body
|
||||||
|
// outgrows the short form.
|
||||||
long := make([]bool, len(t.Body))
|
long := make([]bool, len(t.Body))
|
||||||
sizes := make([]int, len(t.Body))
|
sizes := make([]int, len(t.Body))
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pcs := make([]int, len(t.Body))
|
pcs := make([]int, len(t.Body))
|
||||||
|
var guardJBlong, guardJBElong, moreJMPlong bool
|
||||||
for {
|
for {
|
||||||
pos := len(fi.prologue)
|
guard := fi.guardLen(guardJBlong, guardJBElong)
|
||||||
|
pos := guard + len(fi.prologue)
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -91,6 +102,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
pos += sz
|
pos += sz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bodyLen := pos - (guard + len(fi.prologue))
|
||||||
// Expand any short jump whose displacement no longer fits rel8.
|
// Expand any short jump whose displacement no longer fits rel8.
|
||||||
changed := false
|
changed := false
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
@@ -116,25 +128,75 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The guard's conditional branches target the morestack block, which
|
||||||
|
// starts right after the body: the JBE measures from the end of the
|
||||||
|
// guard, so its displacement is the prologue plus the body.
|
||||||
|
if !guardJBElong && !fits8(int64(len(fi.prologue)+bodyLen)) {
|
||||||
|
guardJBElong = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if fi.splitClass == 2 && !guardJBlong {
|
||||||
|
// The underflow JB sits before the CMPQ; its displacement spans
|
||||||
|
// the rest of the guard plus the prologue and the body.
|
||||||
|
jbLen := 2
|
||||||
|
if guardJBlong {
|
||||||
|
jbLen = 6
|
||||||
|
}
|
||||||
|
rest := fi.guardLen(guardJBlong, guardJBElong) - (9 + 3 + 7 + jbLen)
|
||||||
|
if !fits8(int64(rest + len(fi.prologue) + bodyLen)) {
|
||||||
|
guardJBlong = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The morestack JMP returns to the function start, so its
|
||||||
|
// displacement is the negated distance from its own end.
|
||||||
|
if !moreJMPlong {
|
||||||
|
jmpLen := 2
|
||||||
|
if moreJMPlong {
|
||||||
|
jmpLen = 5
|
||||||
|
}
|
||||||
|
if !fits8(-int64(guard + len(fi.prologue) + bodyLen + 5 + jmpLen)) {
|
||||||
|
moreJMPlong = true
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: emit.
|
// Pass 2: emit. The guard comes first, then the prologue, the body and
|
||||||
out := append([]byte(nil), fi.prologue...)
|
// the morestack block.
|
||||||
|
guardLen := fi.guardLen(guardJBlong, guardJBElong)
|
||||||
|
bodyLen := 0
|
||||||
|
{
|
||||||
|
pos := guardLen + len(fi.prologue)
|
||||||
|
for i, stmt := range t.Body {
|
||||||
|
if _, ok := stmt.(*ast.Instr); ok {
|
||||||
|
pos += sizes[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyLen = pos - (guardLen + len(fi.prologue))
|
||||||
|
}
|
||||||
|
var out []byte
|
||||||
var patches []sbPatch
|
var patches []sbPatch
|
||||||
|
if fi.needSplit {
|
||||||
|
guard, tlsPatch := buildGuard(fi, int32(len(fi.prologue)+bodyLen), int32(fi.guardLen(guardJBlong, guardJBElong)-(9+3+7+2)+len(fi.prologue)+bodyLen))
|
||||||
|
out = append(out, guard...)
|
||||||
|
patches = append(patches, tlsPatch)
|
||||||
|
}
|
||||||
|
out = append(out, fi.prologue...)
|
||||||
var steps []spadjStep
|
var steps []spadjStep
|
||||||
var lines []LineEntry
|
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.
|
||||||
steps = append(steps,
|
steps = append(steps,
|
||||||
spadjStep{1, 8},
|
spadjStep{guardLen + 1, 8},
|
||||||
spadjStep{len(fi.prologue), 8 + fi.size},
|
spadjStep{guardLen + len(fi.prologue), 8 + fi.size},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pos := len(fi.prologue)
|
pos := guardLen + len(fi.prologue)
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
s, ok := stmt.(*ast.Instr)
|
s, ok := stmt.(*ast.Instr)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -156,11 +218,32 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
if len(code) != sizes[i] {
|
if 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])
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||||
}
|
}
|
||||||
|
if strings.ToUpper(s.Mnemonic.Text) == "CALL" {
|
||||||
|
for k := range ps {
|
||||||
|
ps[k].kind = RelCall
|
||||||
|
}
|
||||||
|
}
|
||||||
patches = append(patches, ps...)
|
patches = append(patches, ps...)
|
||||||
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pos += len(code)
|
pos += len(code)
|
||||||
}
|
}
|
||||||
|
if fi.needSplit {
|
||||||
|
// The morestack block: CALL runtime.morestack_noctxt, then a JMP
|
||||||
|
// back to the function entry.
|
||||||
|
jmpLen := 2
|
||||||
|
if moreJMPlong {
|
||||||
|
jmpLen = 5
|
||||||
|
}
|
||||||
|
jmpDisp := -int64(pos + 5 + jmpLen)
|
||||||
|
suffix, callPatch := buildMoreStack(int32(jmpDisp))
|
||||||
|
callPatch.off += pos
|
||||||
|
callPatch.after = pos + 5
|
||||||
|
patches = append(patches, callPatch)
|
||||||
|
out = append(out, suffix...)
|
||||||
|
pos += len(suffix)
|
||||||
|
}
|
||||||
|
_ = pos
|
||||||
return out, patches, offsets, steps, lines, nil
|
return out, patches, offsets, steps, lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,16 +307,48 @@ type frameInfo struct {
|
|||||||
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
|
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
|
||||||
prologue []byte
|
prologue []byte
|
||||||
epilogue []byte
|
epilogue []byte
|
||||||
|
|
||||||
|
// Stack-split guard state (matching the toolchain's stacksplit): needSplit
|
||||||
|
// is false for NOSPLIT functions and for leaf functions whose frame is
|
||||||
|
// below StackSmall, which the toolchain auto-marks NOSPLIT.
|
||||||
|
needSplit bool
|
||||||
|
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
|
||||||
|
framesize int // the size the guard checks: frame+8 for framed functions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stack-frame size classes from runtime/stack.go.
|
||||||
|
const (
|
||||||
|
stackSmall = 128
|
||||||
|
stackBig = 4096
|
||||||
|
)
|
||||||
|
|
||||||
|
// sbPatch gains a kind so the emitters can tell CALL and TLS patches from
|
||||||
|
// plain PC-relative displacements.
|
||||||
|
|
||||||
// computeFrame derives the frame layout, matching the Go assembler's default
|
// computeFrame derives the frame layout, matching the Go assembler's default
|
||||||
// (a frame pointer is used whenever the function has a non-zero frame).
|
// (a frame pointer is used whenever the function has a non-zero frame). It
|
||||||
|
// also decides whether the function needs the stack-split guard, mirroring
|
||||||
|
// obj6: a NOSPLIT function never splits, and a leaf function whose frame is
|
||||||
|
// below StackSmall is auto-marked NOSPLIT. One deliberate deviation: the
|
||||||
|
// toolchain treats zero-argument runtime calls (duffcopy and friends) as
|
||||||
|
// leaf-compatible; here any CALL makes the function a non-leaf.
|
||||||
func computeFrame(t *ast.Text) frameInfo {
|
func computeFrame(t *ast.Text) frameInfo {
|
||||||
fi := frameInfo{}
|
fi := frameInfo{}
|
||||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||||
fi.size = int(t.Frame.Imm.Val)
|
fi.size = int(t.Frame.Imm.Val)
|
||||||
}
|
}
|
||||||
if fi.size > 0 {
|
if fi.size == 0 && hasCall(t) {
|
||||||
|
// The toolchain gives a frameless function containing a CALL an
|
||||||
|
// 8-byte frame for the pushed base pointer: the prologue saves BP
|
||||||
|
// with no stack adjustment, every RET pops it back, FP references
|
||||||
|
// pass one extra slot, and the virtual SP is the hardware SP.
|
||||||
|
fi.size = 8
|
||||||
|
fi.useFP = true
|
||||||
|
fi.fpAdjust = int64(fi.size) + 16 // return address + saved BP + args base
|
||||||
|
fi.spAdjust = 0
|
||||||
|
fi.prologue = []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
||||||
|
fi.epilogue = []byte{0x5D} // POPQ BP
|
||||||
|
} else if fi.size > 0 {
|
||||||
fi.useFP = true
|
fi.useFP = true
|
||||||
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
||||||
fi.spAdjust = int64(fi.size)
|
fi.spAdjust = int64(fi.size)
|
||||||
@@ -242,9 +357,141 @@ func computeFrame(t *ast.Text) frameInfo {
|
|||||||
} else {
|
} else {
|
||||||
fi.fpAdjust = 8 // return address only
|
fi.fpAdjust = 8 // return address only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noSplit := false
|
||||||
|
for _, f := range t.Flags {
|
||||||
|
if strings.EqualFold(f, "NOSPLIT") {
|
||||||
|
noSplit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The toolchain's autoffset: the frame plus the saved base pointer.
|
||||||
|
framesize := fi.size
|
||||||
|
if framesize > 0 {
|
||||||
|
framesize += 8
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case noSplit:
|
||||||
|
case framesize < stackSmall && !hasCall(t):
|
||||||
|
// Auto-NOSPLIT, as the toolchain's leaf search concludes.
|
||||||
|
default:
|
||||||
|
fi.needSplit = true
|
||||||
|
fi.framesize = framesize
|
||||||
|
switch {
|
||||||
|
case framesize <= stackSmall:
|
||||||
|
fi.splitClass = 0
|
||||||
|
case framesize <= stackBig:
|
||||||
|
fi.splitClass = 1
|
||||||
|
default:
|
||||||
|
fi.splitClass = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
return fi
|
return fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasCall reports whether the function body contains a CALL instruction.
|
||||||
|
func hasCall(t *ast.Text) bool {
|
||||||
|
for _, stmt := range t.Body {
|
||||||
|
in, ok := stmt.(*ast.Instr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.ToUpper(in.Mnemonic.Text) == "CALL" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// guardLen returns the byte length of the stack-split guard prefix. The
|
||||||
|
// final conditional branch (JBE, and JB in the big class) is 2 bytes in the
|
||||||
|
// short form and 6 in the long form.
|
||||||
|
func (fi frameInfo) guardLen(jbLong, jbeLong bool) int {
|
||||||
|
if !fi.needSplit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
jb, jbe := 2, 2
|
||||||
|
if jbLong {
|
||||||
|
jb = 6
|
||||||
|
}
|
||||||
|
if jbeLong {
|
||||||
|
jbe = 6
|
||||||
|
}
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
return 9 + 4 + jbe
|
||||||
|
case 1:
|
||||||
|
return 9 + 8 + 4 + jbe
|
||||||
|
default:
|
||||||
|
return 9 + 3 + 7 + jb + 4 + jbe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moreLen returns the byte length of the trailing morestack block: the CALL
|
||||||
|
// (always rel32) plus the JMP back to the function start.
|
||||||
|
func moreLen(jmpLong bool) int {
|
||||||
|
jmp := 2
|
||||||
|
if jmpLong {
|
||||||
|
jmp = 5
|
||||||
|
}
|
||||||
|
return 5 + jmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGuard emits the stack-split guard prefix. jbeDisp and jbDisp are the
|
||||||
|
// already-computed displacements of the conditional branches that jump to the
|
||||||
|
// morestack block (unused in classes without them). The TLS load carries a
|
||||||
|
// R_TLS_LE patch site at offset 5.
|
||||||
|
func buildGuard(fi frameInfo, jbeDisp, jbDisp int32) ([]byte, sbPatch) {
|
||||||
|
out := []byte{
|
||||||
|
0x64, 0x4c, 0x8b, 0x34, 0x25, // MOVQ FS:0, R14
|
||||||
|
0, 0, 0, 0, // TLS slot offset, filled by the linker
|
||||||
|
}
|
||||||
|
tls := sbPatch{off: 5, after: 9, kind: RelTLSLE}
|
||||||
|
jmp := func(op8, op32 byte, disp int32) []byte {
|
||||||
|
if disp >= -128 && disp <= 127 {
|
||||||
|
return []byte{op8, byte(disp)}
|
||||||
|
}
|
||||||
|
return append([]byte{0x0F, op32}, le32(int64(disp))...)
|
||||||
|
}
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
// CMPQ SP, 16(R14)
|
||||||
|
out = append(out, 0x49, 0x3b, 0x66, 0x10)
|
||||||
|
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||||
|
case 1:
|
||||||
|
// LEAQ -(framesize-StackSmall)(SP), R12; CMPQ R12, 16(R14)
|
||||||
|
out = append(out, 0x4c, 0x8d, 0xa4, 0x24)
|
||||||
|
out = append(out, le32(-int64(fi.framesize-stackSmall))...)
|
||||||
|
out = append(out, 0x4d, 0x3b, 0x66, 0x10)
|
||||||
|
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||||
|
default:
|
||||||
|
// MOVQ SP, R12; SUBQ $(framesize-StackSmall), R12; JB; CMPQ R12, 16(R14)
|
||||||
|
out = append(out, 0x49, 0x89, 0xe4)
|
||||||
|
out = append(out, 0x49, 0x81, 0xec)
|
||||||
|
out = append(out, le32(int64(fi.framesize-stackSmall))...)
|
||||||
|
out = append(out, jmp(0x72, 0x82, jbDisp)...)
|
||||||
|
out = append(out, 0x4d, 0x3b, 0x66, 0x10)
|
||||||
|
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||||
|
}
|
||||||
|
return out, tls
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMoreStack emits the trailing block: CALL runtime.morestack_noctxt
|
||||||
|
// (patched by the linker) and a JMP back to the function start.
|
||||||
|
func buildMoreStack(jmpDisp int32) ([]byte, sbPatch) {
|
||||||
|
out := []byte{0xE8, 0, 0, 0, 0}
|
||||||
|
call := sbPatch{off: 1, after: 5, name: "runtime\u00b7morestack_noctxt", kind: RelCall}
|
||||||
|
out = append(out, jmpBytes(jmpDisp)...)
|
||||||
|
return out, call
|
||||||
|
}
|
||||||
|
|
||||||
|
// jmpBytes encodes a near JMP in the short or long form.
|
||||||
|
func jmpBytes(disp int32) []byte {
|
||||||
|
if disp >= -128 && disp <= 127 {
|
||||||
|
return []byte{0xEB, byte(disp)}
|
||||||
|
}
|
||||||
|
return append([]byte{0xE9}, le32(int64(disp))...)
|
||||||
|
}
|
||||||
|
|
||||||
// prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP.
|
// prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP.
|
||||||
func prologueBytes(size int) []byte {
|
func prologueBytes(size int) []byte {
|
||||||
out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
||||||
@@ -258,14 +505,11 @@ func epilogueBytes(size int) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subSP(size int) []byte { // SUBQ $size, SP
|
func subSP(size int) []byte { // SUBQ $size, SP
|
||||||
|
// imm8 holds -128..127; anything larger takes the imm32 form, exactly as
|
||||||
|
// the Go assembler encodes it (verified for 8, 128, 200 and 255).
|
||||||
if size >= -128 && size <= 127 {
|
if size >= -128 && size <= 127 {
|
||||||
return []byte{0x48, 0x83, 0xEC, byte(int8(size))}
|
return []byte{0x48, 0x83, 0xEC, byte(int8(size))}
|
||||||
}
|
}
|
||||||
// 128..255 do not fit SUB's unsigned imm8, but the Go assembler
|
|
||||||
// switches to ADDQ $-size, SP whose sign-extended imm8 does.
|
|
||||||
if size >= -255 && size <= 255 {
|
|
||||||
return []byte{0x48, 0x83, 0xC4, byte(int8(-size))}
|
|
||||||
}
|
|
||||||
return append([]byte{0x48, 0x81, 0xEC}, le32(int64(size))...)
|
return append([]byte{0x48, 0x81, 0xEC}, le32(int64(size))...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +526,16 @@ func addSP(size int) []byte { // ADDQ $size, SP
|
|||||||
func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, error) {
|
func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, error) {
|
||||||
mnem := strings.ToUpper(s.Mnemonic.Text)
|
mnem := strings.ToUpper(s.Mnemonic.Text)
|
||||||
if isJumpMnemonic(mnem) {
|
if isJumpMnemonic(mnem) {
|
||||||
|
if (mnem == "CALL" || mnem == "JMP") && isSBCall(s) {
|
||||||
|
return 5, nil // opcode + rel32, always the long form
|
||||||
|
}
|
||||||
|
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||||
|
code, err := encodeIndirectJump(s, mnem)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(code), nil
|
||||||
|
}
|
||||||
return jumpSize(mnem, long), nil
|
return jumpSize(mnem, long), nil
|
||||||
}
|
}
|
||||||
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
||||||
@@ -331,6 +585,33 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
|||||||
var ps []sbPatch
|
var ps []sbPatch
|
||||||
var err error
|
var err error
|
||||||
if isJumpMnemonic(mnem) {
|
if isJumpMnemonic(mnem) {
|
||||||
|
if (mnem == "CALL" || mnem == "JMP") && isSBCall(s) {
|
||||||
|
// CALL/JMP sym(SB): a rel32 call (or tail call) against a
|
||||||
|
// static or external symbol, resolved by the file-level layout
|
||||||
|
// or the linker.
|
||||||
|
code, ps, err = encodeSBCall(s, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for i := range ps {
|
||||||
|
ps[i].kind = RelCall
|
||||||
|
}
|
||||||
|
body := pc + len(prefix)
|
||||||
|
for i := range ps {
|
||||||
|
ps[i].off += body
|
||||||
|
ps[i].after = body + len(code)
|
||||||
|
}
|
||||||
|
return append(prefix, code...), ps, nil
|
||||||
|
}
|
||||||
|
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||||
|
// JMP/CALL through a register or memory: no relocation and no
|
||||||
|
// label to resolve, the operand fully determines the bytes.
|
||||||
|
code, err = encodeIndirectJump(s, mnem)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return append(prefix, code...), nil, nil
|
||||||
|
}
|
||||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||||
} else {
|
} else {
|
||||||
code, ps, err = encodeNormal(s, fi, link)
|
code, ps, err = encodeNormal(s, fi, link)
|
||||||
@@ -412,6 +693,37 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSBCall reports whether the CALL operand is a symbol reference.
|
||||||
|
func isSBCall(s *ast.Instr) bool {
|
||||||
|
return len(s.Operands) == 1 && s.Operands[0].Kind == ast.OpAddr &&
|
||||||
|
s.Operands[0].Addr.Sym != nil && s.Operands[0].Addr.Sym.Pseudo == "SB"
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeSBCall encodes CALL sym(SB) as E8 rel32 with a patch site.
|
||||||
|
func encodeSBCall(s *ast.Instr, link *linkInfo) ([]byte, []sbPatch, error) {
|
||||||
|
o, err := operandFromAST(s.Operands[0], 8, frameInfo{}, link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
m, ok := o.(sbMem)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("CALL: unsupported operand")
|
||||||
|
}
|
||||||
|
opcode := []byte{0xE8}
|
||||||
|
if strings.ToUpper(s.Mnemonic.Text) == "JMP" {
|
||||||
|
opcode = []byte{0xE9} // a tail call, no return address pushed
|
||||||
|
}
|
||||||
|
e := &enc{}
|
||||||
|
if err := e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, disp: le32(0), sb: &sbRef{name: m.name, addend: m.addend}}); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ps := make([]sbPatch, len(e.patches))
|
||||||
|
for i, p := range e.patches {
|
||||||
|
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend, kind: RelCall}
|
||||||
|
}
|
||||||
|
return e.out, ps, nil
|
||||||
|
}
|
||||||
|
|
||||||
// labelName extracts a local-label name from a jump operand.
|
// labelName extracts a local-label name from a jump operand.
|
||||||
func labelName(op *ast.Operand) (string, bool) {
|
func labelName(op *ast.Operand) (string, bool) {
|
||||||
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
|
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
|
||||||
@@ -421,6 +733,44 @@ func labelName(op *ast.Operand) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||||
|
// register or a memory location rather than a label or a static symbol.
|
||||||
|
// A bare identifier is a register when the register table knows the name and
|
||||||
|
// a label otherwise, which is exactly how the parser cannot distinguish them.
|
||||||
|
func indirectJumpTarget(s *ast.Instr) bool {
|
||||||
|
if len(s.Operands) != 1 || s.Operands[0].Kind != ast.OpAddr {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a := s.Operands[0].Addr
|
||||||
|
if a.Base != "" || a.Index != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
||||||
|
if _, ok := ParseReg(a.Sym.Name); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeIndirectJump assembles a JMP/CALL through a register or memory
|
||||||
|
// operand, which carries no relocation and no label to resolve.
|
||||||
|
func encodeIndirectJump(s *ast.Instr, mnem string) ([]byte, error) {
|
||||||
|
ops := make([]Operand, len(s.Operands))
|
||||||
|
for i, op := range s.Operands {
|
||||||
|
o, err := operandFromAST(op, 8, frameInfo{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ops[i] = o
|
||||||
|
}
|
||||||
|
e := &enc{}
|
||||||
|
if err := e.encodeIndirectBranch(mnem, ops); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
|
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
|
||||||
var spReg = Reg{idx: 4, size: 8}
|
var spReg = Reg{idx: 4, size: 8}
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -4,6 +4,7 @@
|
|||||||
package asm
|
package asm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -201,8 +202,8 @@ TEXT ·withframe(SB), NOSPLIT, $16-16
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestAssembleVexKernel assembles the horizontal-sum reduction the go-flac
|
// TestAssembleVexKernel assembles the horizontal-sum reduction the go-flac
|
||||||
// kernels end with — exercising the VEX moves, shuffle and extract forms
|
// kernels end with; exercising the VEX moves, shuffle and extract forms
|
||||||
// through the full parser → encoder path — and checks the output is
|
// through the full parser → encoder path; and checks the output is
|
||||||
// byte-identical to the Go assembler's.
|
// byte-identical to the Go assembler's.
|
||||||
func TestAssembleVexKernel(t *testing.T) {
|
func TestAssembleVexKernel(t *testing.T) {
|
||||||
fn := firstText(t, `
|
fn := firstText(t, `
|
||||||
@@ -351,3 +352,26 @@ TEXT ·pf(SB), NOSPLIT, $0
|
|||||||
t.Errorf("PREFETCHT0 bytes: got %s, want 0f 18 0b", hex)
|
t.Errorf("PREFETCHT0 bytes: got %s, want 0f 18 0b", hex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSubSPEncodings pins the prologue SUB against the bytes go tool asm
|
||||||
|
// emits for SUBQ $size, SP: imm8 for -128..127, the imm32 form for anything
|
||||||
|
// larger. The intermediate 129..255 range used to encode an ADD with a
|
||||||
|
// truncated immediate, moving SP the wrong way.
|
||||||
|
func TestSubSPEncodings(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
size int
|
||||||
|
want []byte
|
||||||
|
}{
|
||||||
|
{8, []byte{0x48, 0x83, 0xEC, 0x08}},
|
||||||
|
{127, []byte{0x48, 0x83, 0xEC, 0x7F}},
|
||||||
|
{128, []byte{0x48, 0x81, 0xEC, 0x80, 0x00, 0x00, 0x00}},
|
||||||
|
{200, []byte{0x48, 0x81, 0xEC, 0xC8, 0x00, 0x00, 0x00}},
|
||||||
|
{255, []byte{0x48, 0x81, 0xEC, 0xFF, 0x00, 0x00, 0x00}},
|
||||||
|
{4096, []byte{0x48, 0x81, 0xEC, 0x00, 0x10, 0x00, 0x00}},
|
||||||
|
} {
|
||||||
|
got := subSP(tt.size)
|
||||||
|
if !bytes.Equal(got, tt.want) {
|
||||||
|
t.Errorf("subSP(%d) = %x, want %x", tt.size, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+13
-3
@@ -12,7 +12,7 @@ import (
|
|||||||
// Image: a .text section holding the function bodies, a .data section
|
// Image: a .text section holding the function bodies, a .data section
|
||||||
// holding the GLOBL initialisers, a symbol table with one symbol per TEXT
|
// holding the GLOBL initialisers, a symbol table with one symbol per TEXT
|
||||||
// and GLOBL (file-local <> symbols are STB_LOCAL, the rest STB_GLOBAL), and
|
// and GLOBL (file-local <> symbols are STB_LOCAL, the rest STB_GLOBAL), and
|
||||||
// a .rela.text relocation table — one R_X86_64_PC32 entry per static-symbol
|
// a .rela.text relocation table, one R_X86_64_PC32 entry per static-symbol
|
||||||
// reference, internal references resolving against the local data symbols
|
// reference, internal references resolving against the local data symbols
|
||||||
// and external ones against undefined globals. The output links with the
|
// and external ones against undefined globals. The output links with the
|
||||||
// system toolchain (cc/ld) the way a hand-assembled .o would.
|
// system toolchain (cc/ld) the way a hand-assembled .o would.
|
||||||
@@ -43,6 +43,7 @@ const (
|
|||||||
stInfoShift = 4
|
stInfoShift = 4
|
||||||
|
|
||||||
rX8664PC32 = 2
|
rX8664PC32 = 2
|
||||||
|
rX8664TPOFF32 = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// elfSym is one symbol-table entry in construction.
|
// elfSym is one symbol-table entry in construction.
|
||||||
@@ -71,7 +72,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
|||||||
|
|
||||||
// Build the symbol table: the null entry and the two section symbols
|
// Build the symbol table: the null entry and the two section symbols
|
||||||
// come first, then the local symbols (static TEXT and GLOBL), then the
|
// come first, then the local symbols (static TEXT and GLOBL), then the
|
||||||
// globals (exported TEXT and GLOBL, and the undefined externals) — ELF
|
// globals (exported TEXT and GLOBL, and the undefined externals), ELF
|
||||||
// requires every local to precede every global, and sh_info records the
|
// requires every local to precede every global, and sh_info records the
|
||||||
// boundary. symIdx maps a symbol name to its index for the relocations.
|
// boundary. symIdx maps a symbol name to its index for the relocations.
|
||||||
var locals, globals []elfSym
|
var locals, globals []elfSym
|
||||||
@@ -125,11 +126,19 @@ func (img *Image) ELFObject() ([]byte, error) {
|
|||||||
type elfRela struct {
|
type elfRela struct {
|
||||||
off uint64
|
off uint64
|
||||||
sym int
|
sym int
|
||||||
|
typ uint32
|
||||||
addend int64
|
addend int64
|
||||||
}
|
}
|
||||||
var relas []elfRela
|
var relas []elfRela
|
||||||
for _, fn := range img.Funcs {
|
for _, fn := range img.Funcs {
|
||||||
for _, r := range fn.Relocs {
|
for _, r := range fn.Relocs {
|
||||||
|
var typ uint32 = rX8664PC32
|
||||||
|
if r.Kind == RelTLSLE {
|
||||||
|
// R_X86_64_TPOFF32 resolves to the local-exec TLS offset and
|
||||||
|
// carries no symbol.
|
||||||
|
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), sym: 0, typ: rX8664TPOFF32})
|
||||||
|
continue
|
||||||
|
}
|
||||||
idx, ok := symIdx[r.Name]
|
idx, ok := symIdx[r.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||||
@@ -137,6 +146,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
|||||||
relas = append(relas, elfRela{
|
relas = append(relas, elfRela{
|
||||||
off: uint64(fn.Offset + r.Off),
|
off: uint64(fn.Offset + r.Off),
|
||||||
sym: idx,
|
sym: idx,
|
||||||
|
typ: typ,
|
||||||
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
||||||
// assembler measures the symbol from the instruction end,
|
// assembler measures the symbol from the instruction end,
|
||||||
// After − Off bytes past the field, so the addend carries
|
// After − Off bytes past the field, so the addend carries
|
||||||
@@ -218,7 +228,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
|||||||
shstrOff := len(out)
|
shstrOff := len(out)
|
||||||
out = append(out, stSections.bytes()...)
|
out = append(out, stSections.bytes()...)
|
||||||
|
|
||||||
// DWARF debug sections (no relocations — the linker resolves DWARF fixups).
|
// DWARF debug sections (no relocations, the linker resolves DWARF fixups).
|
||||||
dwAlign := func(n int) {
|
dwAlign := func(n int) {
|
||||||
for len(out)%n != 0 {
|
for len(out)%n != 0 {
|
||||||
out = append(out, 0)
|
out = append(out, 0)
|
||||||
|
|||||||
+1
-1
@@ -282,7 +282,7 @@ func dwarfBuildFrameSection(img *Image) []byte {
|
|||||||
// Patch CIE length.
|
// Patch CIE length.
|
||||||
le.PutUint32(b[cieStart:], uint32(len(b)-cieStart-4))
|
le.PutUint32(b[cieStart:], uint32(len(b)-cieStart-4))
|
||||||
|
|
||||||
// FDEs (Frame Description Entries) — one per function.
|
// FDEs (Frame Description Entries), one per function.
|
||||||
for _, fn := range img.Funcs {
|
for _, fn := range img.Funcs {
|
||||||
fdeStart := len(b)
|
fdeStart := len(b)
|
||||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ func elfTestImage(t *testing.T) *Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestAssembleFileExternals checks that a reference to a symbol no GLOBL
|
// TestAssembleFileExternals checks that a reference to a symbol no GLOBL
|
||||||
// defines is recorded as an external relocation instead of failing — the
|
// defines is recorded as an external relocation instead of failing; the
|
||||||
// raw image leaves the displacement zero, the object emitters carry it.
|
// raw image leaves the displacement zero, the object emitters carry it.
|
||||||
func TestAssembleFileExternals(t *testing.T) {
|
func TestAssembleFileExternals(t *testing.T) {
|
||||||
img := elfTestImage(t)
|
img := elfTestImage(t)
|
||||||
|
|||||||
+12
-3
@@ -15,8 +15,9 @@ const (
|
|||||||
|
|
||||||
// AArch64 relocation types (the ELF psABI).
|
// AArch64 relocation types (the ELF psABI).
|
||||||
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
|
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
|
||||||
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD/STR/LDR page offset)
|
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD page offset)
|
||||||
rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction)
|
rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction)
|
||||||
|
rArm64Ldst64Lo12NC = 286 // R_AARCH64_LDST64_ABS_LO12_NC (64-bit LDR/STR page offset)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ELFAARCH64Object returns the image as an ELF64 relocatable object file for
|
// ELFAARCH64Object returns the image as an ELF64 relocatable object file for
|
||||||
@@ -81,7 +82,13 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
|||||||
|
|
||||||
// Build relocations. Each SB reference is an ADRP pair:
|
// Build relocations. Each SB reference is an ADRP pair:
|
||||||
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21
|
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21
|
||||||
// ADD/LDR/STR → R_AARCH64_ADD_ABS_LO12_NC
|
// ADD → R_AARCH64_ADD_ABS_LO12_NC
|
||||||
|
// LDR/STR X → R_AARCH64_LDST64_ABS_LO12_NC
|
||||||
|
// BL → R_AARCH64_CALL26
|
||||||
|
// Addends stay raw: ADR_PREL_PG_HI21 and the ABS_LO12_NC forms resolve
|
||||||
|
// against S+A, and CALL26 branches take the branch instruction's own
|
||||||
|
// place as the PC-relative base, so subtracting the field width (the
|
||||||
|
// amd64 R_PCREL convention) would misplace every branch by 4 bytes.
|
||||||
type elfRela struct {
|
type elfRela struct {
|
||||||
off uint64
|
off uint64
|
||||||
typ uint32
|
typ uint32
|
||||||
@@ -99,6 +106,8 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
|||||||
switch {
|
switch {
|
||||||
case r.Kind == RelArm64Branch:
|
case r.Kind == RelArm64Branch:
|
||||||
typ = rArm64Call26
|
typ = rArm64Call26
|
||||||
|
case r.Kind == RelArm64LDST64 && r.Off%4 == 4:
|
||||||
|
typ = rArm64Ldst64Lo12NC
|
||||||
case r.Kind == RelArm64Addr && r.Off%4 == 4:
|
case r.Kind == RelArm64Addr && r.Off%4 == 4:
|
||||||
typ = rArm64AddAbsLo12NC
|
typ = rArm64AddAbsLo12NC
|
||||||
default:
|
default:
|
||||||
@@ -108,7 +117,7 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
|||||||
off: uint64(fn.Offset + r.Off),
|
off: uint64(fn.Offset + r.Off),
|
||||||
typ: typ,
|
typ: typ,
|
||||||
sym: idx,
|
sym: idx,
|
||||||
addend: r.Addend - int64(r.After-r.Off),
|
addend: r.Addend,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -16,6 +16,7 @@ const (
|
|||||||
// LoongArch relocation types (the ELF psABI).
|
// LoongArch relocation types (the ELF psABI).
|
||||||
rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i)
|
rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i)
|
||||||
rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st)
|
rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st)
|
||||||
|
rLarchB26 = 66 // R_LARCH_B26 (b/bl, matches the Go linker's mapping)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ELFLOONG64Object returns the image as an ELF64 relocatable object file for
|
// ELFLOONG64Object returns the image as an ELF64 relocatable object file for
|
||||||
@@ -95,14 +96,17 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||||
}
|
}
|
||||||
typ := uint32(rLarchPCALAHI20)
|
typ := uint32(rLarchPCALAHI20)
|
||||||
if r.Kind == RelLoong64AddrLo {
|
switch r.Kind {
|
||||||
|
case RelLoong64AddrLo:
|
||||||
typ = rLarchPCALALO12
|
typ = rLarchPCALALO12
|
||||||
|
case RelLoong64Branch:
|
||||||
|
typ = rLarchB26
|
||||||
}
|
}
|
||||||
relas = append(relas, elfRela{
|
relas = append(relas, elfRela{
|
||||||
off: uint64(fn.Offset + r.Off),
|
off: uint64(fn.Offset + r.Off),
|
||||||
typ: typ,
|
typ: typ,
|
||||||
sym: idx,
|
sym: idx,
|
||||||
addend: r.Addend - int64(r.After-r.Off),
|
addend: r.Addend,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,3 +198,45 @@ TEXT ·nop(SB), NOSPLIT, $0
|
|||||||
t.Error("function symbol nop not found")
|
t.Error("function symbol nop not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestELFLOONG64BranchRelocation checks that the morestack call and an
|
||||||
|
// internal CALL both carry R_LARCH_B26 in the emitted object, matching the
|
||||||
|
// Go linker's mapping of its call relocation.
|
||||||
|
func TestELFLOONG64BranchRelocation(t *testing.T) {
|
||||||
|
f, errs := parser.Parse("k_loong64.s", "TEXT \u00b7callbig(SB), $8192-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileLOONG64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||||
|
}
|
||||||
|
obj, err := img.ELFLOONG64Object()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ELFLOONG64Object: %v", err)
|
||||||
|
}
|
||||||
|
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse emitted object: %v", err)
|
||||||
|
}
|
||||||
|
defer ef.Close()
|
||||||
|
relaSec := ef.Section(".rela.text")
|
||||||
|
if relaSec == nil {
|
||||||
|
t.Fatal("missing .rela.text")
|
||||||
|
}
|
||||||
|
raw, err := relaSec.Data()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The guard's morestack call plus the body's CALL to other.
|
||||||
|
if len(raw)%24 != 0 || len(raw)/24 != 2 {
|
||||||
|
t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw))
|
||||||
|
}
|
||||||
|
le := binary.LittleEndian
|
||||||
|
for i := range 2 {
|
||||||
|
info := le.Uint64(raw[i*24+8:])
|
||||||
|
if elf.R_LARCH(info&0xffffffff) != elf.R_LARCH_B26 {
|
||||||
|
t.Errorf("relocation %d type = %v, want R_LARCH_B26", i, elf.R_LARCH(info&0xffffffff))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+15
-5
@@ -40,10 +40,20 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
|||||||
return e.encodeRet()
|
return e.encodeRet()
|
||||||
case upper == "NOP":
|
case upper == "NOP":
|
||||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||||
case upper == "CALL":
|
case upper == "CALL" || upper == "JMP":
|
||||||
return e.encodeJmpRel(ops, []byte{0xE8})
|
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||||
case upper == "JMP":
|
// Anything else is a rel32 against a label resolved by the assembler.
|
||||||
return e.encodeJmpRel(ops, []byte{0xE9})
|
if len(ops) == 1 {
|
||||||
|
switch ops[0].(type) {
|
||||||
|
case Reg, Mem:
|
||||||
|
return e.encodeIndirectBranch(upper, ops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opcode := []byte{0xE8}
|
||||||
|
if upper == "JMP" {
|
||||||
|
opcode = []byte{0xE9}
|
||||||
|
}
|
||||||
|
return e.encodeJmpRel(ops, opcode)
|
||||||
}
|
}
|
||||||
if cc, ok := condCode(upper); ok {
|
if cc, ok := condCode(upper); ok {
|
||||||
return e.encodeJcc(cc, ops)
|
return e.encodeJcc(cc, ops)
|
||||||
@@ -284,7 +294,7 @@ func setRM(i *instr, reg Reg, rm Operand, opSize int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setRMDigit fills in the ModR/M for an instruction whose reg field is an
|
// setRMDigit fills in the ModR/M for an instruction whose reg field is an
|
||||||
// opcode /digit extension (0–7), which carries none of the register REX rules.
|
// opcode /digit extension (0-7), which carries none of the register REX rules.
|
||||||
func setRMDigit(i *instr, digit int, rm Operand, opSize int) error {
|
func setRMDigit(i *instr, digit int, rm Operand, opSize int) error {
|
||||||
return setRMReg(i, digit, false, false, rm, opSize)
|
return setRMReg(i, digit, false, false, rm, opSize)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-2
@@ -181,6 +181,39 @@ func TestControl(t *testing.T) {
|
|||||||
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIndirectControlFlow pins the indirect JMP/CALL forms: FF /4 for JMP and
|
||||||
|
// FF /2 for CALL through a register or memory. A REX appears only for the
|
||||||
|
// extended registers, never REX.W: the branch operand size is fixed at 64
|
||||||
|
// bits in long mode.
|
||||||
|
func TestIndirectControlFlow(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
mnem string
|
||||||
|
ops []Operand
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"JMP AX", "JMP", []Operand{AX}, "ffe0"},
|
||||||
|
{"CALL AX", "CALL", []Operand{AX}, "ffd0"},
|
||||||
|
{"JMP (BX)", "JMP", []Operand{Ptr(BX, 0, 8)}, "ff23"},
|
||||||
|
{"CALL (BX)", "CALL", []Operand{Ptr(BX, 0, 8)}, "ff13"},
|
||||||
|
{"JMP 8(BX)", "JMP", []Operand{Ptr(BX, 8, 8)}, "ff6308"},
|
||||||
|
{"CALL -16(BX)", "CALL", []Operand{Ptr(BX, -16, 8)}, "ff53f0"},
|
||||||
|
{"JMP R8", "JMP", []Operand{Reg{idx: 8, size: 2}}, "41ffe0"},
|
||||||
|
{"CALL R9", "CALL", []Operand{Reg{idx: 9, size: 2}}, "41ffd1"},
|
||||||
|
{"JMP R15", "JMP", []Operand{Reg{idx: 15, size: 2}}, "41ffe7"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
code, err := Encode(c.mnem, c.ops...)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: %v", c.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||||
|
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
||||||
// against the Go assembler. wantOp is the decoder's name, which differs from
|
// against the Go assembler. wantOp is the decoder's name, which differs from
|
||||||
// the Plan 9 spelling for the octa moves (MOVOU = MOVDQU, MOVO = MOVDQA).
|
// the Plan 9 spelling for the octa moves (MOVOU = MOVDQU, MOVO = MOVDQA).
|
||||||
@@ -230,7 +263,7 @@ func TestSSEMoveGroundTruth(t *testing.T) {
|
|||||||
// TestGoFlacScalarTail encodes the scalar tail of an analyze kernel to confirm
|
// TestGoFlacScalarTail encodes the scalar tail of an analyze kernel to confirm
|
||||||
// the encoder handles a realistic instruction sequence.
|
// the encoder handles a realistic instruction sequence.
|
||||||
func TestGoFlacScalarTail(t *testing.T) {
|
func TestGoFlacScalarTail(t *testing.T) {
|
||||||
// MOVQ swin_base+0(FP), SI — modelled as MOVQ disp(reg), reg.
|
// MOVQ swin_base+0(FP), SI; modelled as MOVQ disp(reg), reg.
|
||||||
checkSyntax(t, "mov rsi, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), SI)
|
checkSyntax(t, "mov rsi, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), SI)
|
||||||
checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8})
|
checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8})
|
||||||
checkSyntax(t, "and r10, -0x8", "ANDQ", Imm(-8), Reg{idx: 10, size: 8})
|
checkSyntax(t, "and r10, -0x8", "ANDQ", Imm(-8), Reg{idx: 10, size: 8})
|
||||||
@@ -421,7 +454,7 @@ func TestSSEShuffleGroundTruth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestMOVQXMMGroundTruth pins the SSE2 packed-quadword move encodings:
|
// TestMOVQXMMGroundTruth pins the SSE2 packed-quadword move encodings:
|
||||||
// loads and register moves on F3 0F 7E, stores on 66 0F D6 — the forms
|
// loads and register moves on F3 0F 7E, stores on 66 0F D6; the forms
|
||||||
// the GPR-move fallback silently corrupted.
|
// the GPR-move fallback silently corrupted.
|
||||||
func TestMOVQXMMGroundTruth(t *testing.T) {
|
func TestMOVQXMMGroundTruth(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
|||||||
+82
-82
@@ -9,10 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// This file implements EVEX (AVX-512) instruction encoding: the four-byte
|
// This file implements EVEX (AVX-512) instruction encoding: the four-byte
|
||||||
// EVEX prefix with 5-bit vector register fields (Z0–Z31, X/Y 16–31), the
|
// EVEX prefix with 5-bit vector register fields (Z0-Z31, X/Y 16-31), the
|
||||||
// compressed disp8×N displacement, and the operand shapes the go-flac
|
// compressed disp8×N displacement, and the operand shapes the go-flac
|
||||||
// AVX-512 kernels use plus the common floating-point and conversion set.
|
// AVX-512 kernels use plus the common floating-point and conversion set.
|
||||||
// Masking follows the Go assembler's spelling: an explicit K1–K7 operand
|
// Masking follows the Go assembler's spelling: an explicit K1-K7 operand
|
||||||
// anywhere among the operands (merging) plus a ".Z" mnemonic suffix for
|
// anywhere among the operands (merging) plus a ".Z" mnemonic suffix for
|
||||||
// zeroing. K-register operands (mask destinations, KMOVW, KTESTW) are
|
// zeroing. K-register operands (mask destinations, KMOVW, KTESTW) are
|
||||||
// supported too.
|
// supported too.
|
||||||
@@ -36,7 +36,7 @@ type evexSpec struct {
|
|||||||
// are taken from the Go assembler's opcode tables, which are authoritative
|
// are taken from the Go assembler's opcode tables, which are authoritative
|
||||||
// for byte-for-byte agreement.
|
// for byte-for-byte agreement.
|
||||||
var evexTable = map[string]evexSpec{
|
var evexTable = map[string]evexSpec{
|
||||||
// EVEX.128/256/512.66.0F — integer arithmetic / logic, NDS form.
|
// EVEX.128/256/512.66.0F, integer arithmetic / logic, NDS form.
|
||||||
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPADDQ": {1, 0xD4, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPADDQ": {1, 0xD4, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
@@ -48,25 +48,25 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPCMPEQD": {1, 0x76, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPCMPEQD": {1, 0x76, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.128/256/512.66.0F.W1 — packed double arithmetic.
|
// EVEX.128/256/512.66.0F.W1, packed double arithmetic.
|
||||||
"VADDPD": {1, 0x58, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VADDPD": {1, 0x58, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMULPD": {1, 0x59, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMULPD": {1, 0x59, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VSUBPD": {1, 0x5C, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VSUBPD": {1, 0x5C, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VDIVPD": {1, 0x5E, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VDIVPD": {1, 0x5E, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMINPD": {1, 0x5D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMINPD": {1, 0x5D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMAXPD": {1, 0x5F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMAXPD": {1, 0x5F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.0F.W0 — packed single arithmetic.
|
// EVEX.128/256/512.0F.W0, packed single arithmetic.
|
||||||
"VADDPS": {1, 0x58, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VADDPS": {1, 0x58, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMULPS": {1, 0x59, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMULPS": {1, 0x59, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VSUBPS": {1, 0x5C, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VSUBPS": {1, 0x5C, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VDIVPS": {1, 0x5E, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VDIVPS": {1, 0x5E, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMINPS": {1, 0x5D, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMINPS": {1, 0x5D, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VMAXPS": {1, 0x5F, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VMAXPS": {1, 0x5F, 0, 0, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.66.0F.W1 — packed double unpack.
|
// EVEX.128/256/512.66.0F.W1, packed double unpack.
|
||||||
"VUNPCKLPD": {1, 0x14, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VUNPCKLPD": {1, 0x14, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VUNPCKHPD": {1, 0x15, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VUNPCKHPD": {1, 0x15, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.128.F2.0F.W1 — scalar double arithmetic (the packed opcodes with
|
// EVEX.128.F2.0F.W1, scalar double arithmetic (the packed opcodes with
|
||||||
// an F2 pp; the EVEX forms exist for masked and zeroing use). The
|
// an F2 pp; the EVEX forms exist for masked and zeroing use). The
|
||||||
// memory operand is a single double, so disp8×N = 8.
|
// memory operand is a single double, so disp8×N = 8.
|
||||||
"VADDSD": {1, 0x58, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VADDSD": {1, 0x58, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
@@ -76,7 +76,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VMINSD": {1, 0x5D, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VMINSD": {1, 0x5D, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
"VMAXSD": {1, 0x5F, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VMAXSD": {1, 0x5F, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
|
|
||||||
// EVEX.128.F3.0F.W0 — scalar single arithmetic (disp8×N = 4).
|
// EVEX.128.F3.0F.W0, scalar single arithmetic (disp8×N = 4).
|
||||||
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
"VMULSS": {1, 0x59, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VMULSS": {1, 0x59, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
@@ -84,38 +84,38 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
|
||||||
// EVEX.512.66.0F3A — align (NDS + imm8).
|
// EVEX.512.66.0F3A, align (NDS + imm8).
|
||||||
"VALIGND": {3, 0x03, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VALIGND": {3, 0x03, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.128/256/512.66.0F — immediate shift (VPSRAD /4).
|
// EVEX.128/256/512.66.0F, immediate shift (VPSRAD /4).
|
||||||
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm, [3]int{16, 32, 64}},
|
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.66.0F.W1 — variable shift with an XMM count (VPSRAQ;
|
// EVEX.128/256/512.66.0F.W1, variable shift with an XMM count (VPSRAQ;
|
||||||
// the W bit distinguishes it from VPSRAD's E2 form).
|
// the W bit distinguishes it from VPSRAD's E2 form).
|
||||||
"VPSRAQ": {1, 0xE2, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPSRAQ": {1, 0xE2, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.128/256/512.F3.0F.W1 — signed qword to packed double (reg=dst,
|
// EVEX.128/256/512.F3.0F.W1, signed qword to packed double (reg=dst,
|
||||||
// rm=src, no vvvv).
|
// rm=src, no vvvv).
|
||||||
"VCVTQQ2PD": {1, 0xE6, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTQQ2PD": {1, 0xE6, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.F2.0F.W1 — duplicate the low double (reg=dst,
|
// EVEX.128/256/512.F2.0F.W1, duplicate the low double (reg=dst,
|
||||||
// rm=src, no vvvv): a 128-bit destination reads a single double from
|
// rm=src, no vvvv): a 128-bit destination reads a single double from
|
||||||
// memory (disp8×8), the wider ones read the full operand.
|
// memory (disp8×8), the wider ones read the full operand.
|
||||||
"VMOVDDUP": {1, 0x12, 1, 3, -1, vexRM, [3]int{8, 32, 64}},
|
"VMOVDDUP": {1, 0x12, 1, 3, -1, vexRM, [3]int{8, 32, 64}},
|
||||||
// EVEX.128/256/512.0F.W0 — signed dword to packed single (reg=dst,
|
// EVEX.128/256/512.0F.W0, signed dword to packed single (reg=dst,
|
||||||
// rm=src, no vvvv, no mandatory prefix — as in the VEX form).
|
// rm=src, no vvvv, no mandatory prefix, as in the VEX form).
|
||||||
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.0F.W0 — packed single to packed double: the
|
// EVEX.128/256/512.0F.W0, packed single to packed double: the
|
||||||
// destination is twice the source width and sets the length; disp8×N
|
// destination is twice the source width and sets the length; disp8×N
|
||||||
// follows the narrow memory source. No F3 prefix: the Go assembler
|
// follows the narrow memory source. No F3 prefix: the Go assembler
|
||||||
// emits this instruction with pp = 00 (Intel's maps would call that
|
// emits this instruction with pp = 00 (Intel's maps would call that
|
||||||
// undefined) and gasm reproduces the Go assembler's bytes — its machine
|
// undefined) and gasm reproduces the Go assembler's bytes, its machine
|
||||||
// code is the oracle, not the manual.
|
// code is the oracle, not the manual.
|
||||||
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
// EVEX.128/256/512.F3.0F.W0 — signed dword to packed double (the EVEX
|
// EVEX.128/256/512.F3.0F.W0, signed dword to packed double (the EVEX
|
||||||
// form of the VEX instruction; the destination sets the length, disp8×N
|
// form of the VEX instruction; the destination sets the length, disp8×N
|
||||||
// follows the narrow memory source).
|
// follows the narrow memory source).
|
||||||
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
// EVEX packed double → dword conversions: the source is the wide
|
// EVEX packed double → dword conversions: the source is the wide
|
||||||
// operand and the mnemonic fixes the length — the bare names are
|
// operand and the mnemonic fixes the length, the bare names are
|
||||||
// 512-bit only (ZMM source, XMM destination), the X/Y spellings are
|
// 512-bit only (ZMM source, XMM destination), the X/Y spellings are
|
||||||
// EVEX-128/256. Exactly one slot of n is valid; it names the vector
|
// EVEX-128/256. Exactly one slot of n is valid; it names the vector
|
||||||
// length (and the disp8×N multiplier) a register or memory source
|
// length (and the disp8×N multiplier) a register or memory source
|
||||||
@@ -127,7 +127,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VCVTTPD2DQX": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
"VCVTTPD2DQX": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
||||||
"VCVTTPD2DQY": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
"VCVTTPD2DQY": {1, 0xE6, 1, 1, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — ternary logic and lane shuffles (NDS + imm8).
|
// EVEX.66.0F3A, ternary logic and lane shuffles (NDS + imm8).
|
||||||
"VPTERNLOGD": {3, 0x25, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VPTERNLOGD": {3, 0x25, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VPTERNLOGQ": {3, 0x25, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VPTERNLOGQ": {3, 0x25, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VSHUFI32X4": {3, 0x43, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VSHUFI32X4": {3, 0x43, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
@@ -136,11 +136,11 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VSHUFF64X2": {3, 0x23, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VSHUFF64X2": {3, 0x23, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VPALIGNR": {3, 0x0F, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VPALIGNR": {3, 0x0F, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F — the EVEX forms of the VEX two-source shuffle.
|
// EVEX.66.0F, the EVEX forms of the VEX two-source shuffle.
|
||||||
"VSHUFPD": {1, 0xC6, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VSHUFPD": {1, 0xC6, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VSHUFPS": {1, 0xC6, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VSHUFPS": {1, 0xC6, 0, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — lane insert ($imm, xsrc, zsrc1, zdst).
|
// EVEX.66.0F3A, lane insert ($imm, xsrc, zsrc1, zdst).
|
||||||
"VINSERTF32X4": {3, 0x18, 0, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
"VINSERTF32X4": {3, 0x18, 0, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
||||||
"VINSERTF32X8": {3, 0x1A, 0, 1, -1, vexNDS3Imm, [3]int{0, 0, 32}},
|
"VINSERTF32X8": {3, 0x1A, 0, 1, -1, vexNDS3Imm, [3]int{0, 0, 32}},
|
||||||
"VINSERTF64X2": {3, 0x18, 1, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
"VINSERTF64X2": {3, 0x18, 1, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
||||||
@@ -150,7 +150,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VINSERTI64X2": {3, 0x38, 1, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
"VINSERTI64X2": {3, 0x38, 1, 1, -1, vexNDS3Imm, [3]int{0, 16, 32}},
|
||||||
"VINSERTI64X4": {3, 0x3A, 1, 1, -1, vexNDS3Imm, [3]int{0, 0, 32}},
|
"VINSERTI64X4": {3, 0x3A, 1, 1, -1, vexNDS3Imm, [3]int{0, 0, 32}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — lane extract (reg=source, rm=XMM/YMM destination,
|
// EVEX.66.0F3A, lane extract (reg=source, rm=XMM/YMM destination,
|
||||||
// imm8).
|
// imm8).
|
||||||
"VEXTRACTF32X4": {3, 0x19, 0, 1, -1, vexExtract, [3]int{0, 16, 16}},
|
"VEXTRACTF32X4": {3, 0x19, 0, 1, -1, vexExtract, [3]int{0, 16, 16}},
|
||||||
"VEXTRACTF32X8": {3, 0x1B, 0, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
"VEXTRACTF32X8": {3, 0x1B, 0, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||||
@@ -159,14 +159,14 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VEXTRACTI32X8": {3, 0x3B, 0, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
"VEXTRACTI32X8": {3, 0x3B, 0, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||||
"VEXTRACTI64X2": {3, 0x39, 1, 1, -1, vexExtract, [3]int{0, 16, 16}},
|
"VEXTRACTI64X2": {3, 0x39, 1, 1, -1, vexExtract, [3]int{0, 16, 16}},
|
||||||
|
|
||||||
// EVEX.66.0F — compare with an opmask destination ($imm, src2, src1,
|
// EVEX.66.0F, compare with an opmask destination ($imm, src2, src1,
|
||||||
// kdst): NDS3Imm with the K register in the reg field.
|
// kdst): NDS3Imm with the K register in the reg field.
|
||||||
"VCMPPD": {1, 0xC2, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VCMPPD": {1, 0xC2, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VCMPPS": {1, 0xC2, 0, 0, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VCMPPS": {1, 0xC2, 0, 0, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VCMPSD": {1, 0xC2, 1, 3, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
"VCMPSD": {1, 0xC2, 1, 3, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
||||||
"VCMPSS": {1, 0xC2, 0, 2, -1, vexNDS3Imm, [3]int{4, 4, 4}},
|
"VCMPSS": {1, 0xC2, 0, 2, -1, vexNDS3Imm, [3]int{4, 4, 4}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — integer compares with an opmask destination, the same
|
// EVEX.66.0F3A, integer compares with an opmask destination, the same
|
||||||
// NDS3Imm-with-k-reg shape as the floating-point compares; W selects the
|
// NDS3Imm-with-k-reg shape as the floating-point compares; W selects the
|
||||||
// operand width (byte/word vs dword/qword), the opcode the signedness.
|
// operand width (byte/word vs dword/qword), the opcode the signedness.
|
||||||
// The memory form takes a full vector, so disp8×N is 16/32/64.
|
// The memory form takes a full vector, so disp8×N is 16/32/64.
|
||||||
@@ -179,7 +179,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPCMPQ": {3, 0x1F, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VPCMPQ": {3, 0x1F, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
"VPCMPUQ": {3, 0x1E, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
"VPCMPUQ": {3, 0x1E, 1, 1, -1, vexNDS3Imm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — permutes (NDS form).
|
// EVEX.66.0F38, permutes (NDS form).
|
||||||
"VPERMB": {2, 0x8D, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPERMB": {2, 0x8D, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPERMW": {2, 0x8D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPERMW": {2, 0x8D, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPERMI2D": {2, 0x76, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPERMI2D": {2, 0x76, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
@@ -188,7 +188,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPERMT2Q": {2, 0x7E, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPERMT2Q": {2, 0x7E, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPERMT2PD": {2, 0x7F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPERMT2PD": {2, 0x7F, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F — the wider integer set (NDS form).
|
// EVEX.66.0F, the wider integer set (NDS form).
|
||||||
"VPMADDWD": {1, 0xF5, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMADDWD": {1, 0xF5, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPMULHUW": {1, 0xE4, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMULHUW": {1, 0xE4, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPMADDUBSW": {2, 0x04, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMADDUBSW": {2, 0x04, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
@@ -199,34 +199,34 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPACKSSDW": {1, 0x6B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPACKSSDW": {1, 0x6B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPACKUSDW": {2, 0x2B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPACKUSDW": {2, 0x2B, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — absolute values and replicating moves (reg=dst,
|
// EVEX.66.0F38, absolute values and replicating moves (reg=dst,
|
||||||
// rm=src).
|
// rm=src).
|
||||||
"VPABSB": {2, 0x1C, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VPABSB": {2, 0x1C, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VPABSW": {2, 0x1D, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VPABSW": {2, 0x1D, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VPABSD": {2, 0x1E, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VPABSD": {2, 0x1E, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VPABSQ": {2, 0x1F, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VPABSQ": {2, 0x1F, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.F3.0F — replicate even/odd singles.
|
// EVEX.F3.0F, replicate even/odd singles.
|
||||||
"VMOVSLDUP": {1, 0x12, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VMOVSLDUP": {1, 0x12, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VMOVSHDUP": {1, 0x16, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VMOVSHDUP": {1, 0x16, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.66.0F38 — sign/zero-extending moves; the memory source is the
|
// EVEX.66.0F38, sign/zero-extending moves; the memory source is the
|
||||||
// narrow half (here byte to word).
|
// narrow half (here byte to word).
|
||||||
"VPMOVSXBW": {2, 0x20, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VPMOVSXBW": {2, 0x20, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
"VPMOVZXBW": {2, 0x30, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VPMOVZXBW": {2, 0x30, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
// EVEX.66.0F — packed single conversions (reg=dst, rm=src).
|
// EVEX.66.0F, packed single conversions (reg=dst, rm=src).
|
||||||
"VCVTPS2DQ": {1, 0x5B, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTPS2DQ": {1, 0x5B, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VCVTTPS2DQ": {1, 0x5B, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTTPS2DQ": {1, 0x5B, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.66.0F38 — broadcast a single/double to all lanes (reg=dst,
|
// EVEX.66.0F38, broadcast a single/double to all lanes (reg=dst,
|
||||||
// rm=scalar memory; disp8×N is the element size).
|
// rm=scalar memory; disp8×N is the element size).
|
||||||
"VBROADCASTSS": {2, 0x18, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
"VBROADCASTSS": {2, 0x18, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
||||||
"VBROADCASTSD": {2, 0x19, 1, 1, -1, vexRM, [3]int{0, 8, 8}},
|
"VBROADCASTSD": {2, 0x19, 1, 1, -1, vexRM, [3]int{0, 8, 8}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — expand loads (rm → vector register destination).
|
// EVEX.66.0F38, expand loads (rm → vector register destination).
|
||||||
"VEXPANDPD": {2, 0x88, 1, 1, -1, vexRM, [3]int{8, 8, 8}},
|
"VEXPANDPD": {2, 0x88, 1, 1, -1, vexRM, [3]int{8, 8, 8}},
|
||||||
"VEXPANDPS": {2, 0x88, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
"VEXPANDPS": {2, 0x88, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
||||||
"VPEXPANDD": {2, 0x89, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
"VPEXPANDD": {2, 0x89, 0, 1, -1, vexRM, [3]int{4, 4, 4}},
|
||||||
"VPEXPANDQ": {2, 0x89, 1, 1, -1, vexRM, [3]int{8, 8, 8}},
|
"VPEXPANDQ": {2, 0x89, 1, 1, -1, vexRM, [3]int{8, 8, 8}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — compress stores (vector register source → rm), and the
|
// EVEX.66.0F38, compress stores (vector register source → rm), and the
|
||||||
// remaining narrowing stores.
|
// remaining narrowing stores.
|
||||||
"VCOMPRESSPD": {2, 0x8A, 1, 1, -1, vexRMRev, [3]int{8, 8, 8}},
|
"VCOMPRESSPD": {2, 0x8A, 1, 1, -1, vexRMRev, [3]int{8, 8, 8}},
|
||||||
"VCOMPRESSPS": {2, 0x8A, 0, 1, -1, vexRMRev, [3]int{4, 4, 4}},
|
"VCOMPRESSPS": {2, 0x8A, 0, 1, -1, vexRMRev, [3]int{4, 4, 4}},
|
||||||
@@ -235,7 +235,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPMOVWB": {2, 0x30, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
"VPMOVWB": {2, 0x30, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
||||||
"VPMOVQB": {2, 0x32, 0, 2, -1, vexRMRev, [3]int{2, 4, 8}},
|
"VPMOVQB": {2, 0x32, 0, 2, -1, vexRMRev, [3]int{2, 4, 8}},
|
||||||
|
|
||||||
// EVEX.66.0F — rotates (immediate form: /0 right, /1 left).
|
// EVEX.66.0F, rotates (immediate form: /0 right, /1 left).
|
||||||
"VPRORD": {1, 0x72, 0, 1, 0, vexShiftImm, [3]int{16, 32, 64}},
|
"VPRORD": {1, 0x72, 0, 1, 0, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
"VPRORQ": {1, 0x72, 1, 1, 0, vexShiftImm, [3]int{16, 32, 64}},
|
"VPRORQ": {1, 0x72, 1, 1, 0, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
"VPROLD": {1, 0x72, 0, 1, 1, vexShiftImm, [3]int{16, 32, 64}},
|
"VPROLD": {1, 0x72, 0, 1, 1, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
@@ -248,14 +248,14 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPSRLQ": {1, 0x73, 1, 1, 2, vexShiftImm, [3]int{16, 32, 64}},
|
"VPSRLQ": {1, 0x73, 1, 1, 2, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
"VPSLLQ": {1, 0x73, 1, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
"VPSLLQ": {1, 0x73, 1, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — floating-point helpers, packed (reg=dst, rm=src).
|
// EVEX.66.0F38, floating-point helpers, packed (reg=dst, rm=src).
|
||||||
"VRCP14PD": {2, 0x4C, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VRCP14PD": {2, 0x4C, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VRCP14PS": {2, 0x4C, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VRCP14PS": {2, 0x4C, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VRSQRT14PD": {2, 0x4E, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VRSQRT14PD": {2, 0x4E, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VRSQRT14PS": {2, 0x4E, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VRSQRT14PS": {2, 0x4E, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VGETEXPPD": {2, 0x42, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VGETEXPPD": {2, 0x42, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VGETEXPPS": {2, 0x42, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VGETEXPPS": {2, 0x42, 0, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.66.0F38 — floating-point helpers, scalar (NDS form: src2 is
|
// EVEX.66.0F38, floating-point helpers, scalar (NDS form: src2 is
|
||||||
// rm, src1 is vvvv, the XMM destination is reg). Like the scalar 0F3A
|
// rm, src1 is vvvv, the XMM destination is reg). Like the scalar 0F3A
|
||||||
// forms, these take the 66 prefix; W selects double/single.
|
// forms, these take the 66 prefix; W selects double/single.
|
||||||
"VRCP14SD": {2, 0x4D, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VRCP14SD": {2, 0x4D, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
@@ -264,13 +264,13 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VRSQRT14SS": {2, 0x4F, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VRSQRT14SS": {2, 0x4F, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
"VGETEXPSD": {2, 0x43, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VGETEXPSD": {2, 0x43, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
"VGETEXPSS": {2, 0x43, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VGETEXPSS": {2, 0x43, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
// EVEX.66.0F38 — scale by a power of two (NDS form).
|
// EVEX.66.0F38, scale by a power of two (NDS form).
|
||||||
"VSCALEFPD": {2, 0x2C, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VSCALEFPD": {2, 0x2C, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VSCALEFPS": {2, 0x2C, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VSCALEFPS": {2, 0x2C, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VSCALEFSD": {2, 0x2D, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VSCALEFSD": {2, 0x2D, 1, 1, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
"VSCALEFSS": {2, 0x2D, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VSCALEFSS": {2, 0x2D, 0, 1, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — packed round/getmant/reduce ($imm, src, dst: reg=dst,
|
// EVEX.66.0F3A, packed round/getmant/reduce ($imm, src, dst: reg=dst,
|
||||||
// rm=src, imm8).
|
// rm=src, imm8).
|
||||||
"VRNDSCALEPD": {3, 0x09, 1, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VRNDSCALEPD": {3, 0x09, 1, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
"VRNDSCALEPS": {3, 0x08, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VRNDSCALEPS": {3, 0x08, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
@@ -278,7 +278,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VGETMANTPS": {3, 0x26, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VGETMANTPS": {3, 0x26, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
"VREDUCEPD": {3, 0x56, 1, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VREDUCEPD": {3, 0x56, 1, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
"VREDUCEPS": {3, 0x56, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VREDUCEPS": {3, 0x56, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
// EVEX.66.0F3A — scalar round/getmant/reduce and fixup/range (NDS +
|
// EVEX.66.0F3A, scalar round/getmant/reduce and fixup/range (NDS +
|
||||||
// imm8: $imm, src2, src1, dst). The scalar 0F3A forms all take the 66
|
// imm8: $imm, src2, src1, dst). The scalar 0F3A forms all take the 66
|
||||||
// prefix; W selects double/single.
|
// prefix; W selects double/single.
|
||||||
"VRNDSCALESD": {3, 0x0B, 1, 1, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
"VRNDSCALESD": {3, 0x0B, 1, 1, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
||||||
@@ -296,7 +296,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VRANGESD": {3, 0x51, 1, 1, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
"VRANGESD": {3, 0x51, 1, 1, -1, vexNDS3Imm, [3]int{8, 8, 8}},
|
||||||
"VRANGESS": {3, 0x51, 0, 1, -1, vexNDS3Imm, [3]int{4, 4, 4}},
|
"VRANGESS": {3, 0x51, 0, 1, -1, vexNDS3Imm, [3]int{4, 4, 4}},
|
||||||
|
|
||||||
// EVEX.66.0F3A — floating-point class test ($imm, src, kdst): the
|
// EVEX.66.0F3A, floating-point class test ($imm, src, kdst): the
|
||||||
// reg field carries the opmask destination. The packed forms carry an
|
// reg field carries the opmask destination. The packed forms carry an
|
||||||
// explicit length in the mnemonic (X/Y/Z).
|
// explicit length in the mnemonic (X/Y/Z).
|
||||||
"VFPCLASSPDX": {3, 0x66, 1, 1, -1, vexImmRM, [3]int{16, 0, 0}},
|
"VFPCLASSPDX": {3, 0x66, 1, 1, -1, vexImmRM, [3]int{16, 0, 0}},
|
||||||
@@ -308,7 +308,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VFPCLASSSD": {3, 0x67, 1, 1, -1, vexImmRM, [3]int{8, 0, 0}},
|
"VFPCLASSSD": {3, 0x67, 1, 1, -1, vexImmRM, [3]int{8, 0, 0}},
|
||||||
"VFPCLASSSS": {3, 0x67, 0, 1, -1, vexImmRM, [3]int{4, 0, 0}},
|
"VFPCLASSSS": {3, 0x67, 0, 1, -1, vexImmRM, [3]int{4, 0, 0}},
|
||||||
|
|
||||||
// EVEX — the remaining conversions. VCVTQQ2PS narrows (the 512-bit
|
// EVEX, the remaining conversions. VCVTQQ2PS narrows (the 512-bit
|
||||||
// source sets the length); the rest follow the destination.
|
// source sets the length); the rest follow the destination.
|
||||||
"VCVTQQ2PS": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{0, 0, 64}},
|
"VCVTQQ2PS": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{0, 0, 64}},
|
||||||
"VCVTPD2QQ": {1, 0x7B, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
"VCVTPD2QQ": {1, 0x7B, 1, 1, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
@@ -316,13 +316,13 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VCVTPS2QQ": {1, 0x7B, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTPS2QQ": {1, 0x7B, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
"VCVTUDQ2PD": {1, 0x7A, 0, 2, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTUDQ2PD": {1, 0x7A, 0, 2, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
"VCVTUDQ2PS": {1, 0x7A, 0, 0, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTUDQ2PS": {1, 0x7A, 0, 0, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
// EVEX.66.0F38 — half-precision convert (half-width source).
|
// EVEX.66.0F38, half-precision convert (half-width source).
|
||||||
"VCVTPH2PS": {2, 0x13, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VCVTPH2PS": {2, 0x13, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
// EVEX.66.0F3A — half-precision convert back ($imm, src, dst: reg=src,
|
// EVEX.66.0F3A, half-precision convert back ($imm, src, dst: reg=src,
|
||||||
// rm=dst, imm8 — the extract layout).
|
// rm=dst, imm8, the extract layout).
|
||||||
"VCVTPS2PH": {3, 0x1D, 0, 1, -1, vexExtract, [3]int{8, 16, 32}},
|
"VCVTPS2PH": {3, 0x1D, 0, 1, -1, vexExtract, [3]int{8, 16, 32}},
|
||||||
|
|
||||||
// EVEX — unsigned and truncating conversions. The PD sources are the
|
// EVEX, unsigned and truncating conversions. The PD sources are the
|
||||||
// wide operand (the bare names are 512-bit only, the X/Y spellings fix
|
// wide operand (the bare names are 512-bit only, the X/Y spellings fix
|
||||||
// the length); the PS/UQQ destinations are wide and follow the
|
// the length); the PS/UQQ destinations are wide and follow the
|
||||||
// destination.
|
// destination.
|
||||||
@@ -349,7 +349,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VCVTQQ2PSX": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
"VCVTQQ2PSX": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{16, 0, 0}},
|
||||||
"VCVTQQ2PSY": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
"VCVTQQ2PSY": {1, 0x5B, 1, 0, -1, vexRMSrcLen, [3]int{0, 32, 0}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — the remaining sign/zero-extending moves (narrow
|
// EVEX.66.0F38, the remaining sign/zero-extending moves (narrow
|
||||||
// source; disp8×N follows its size).
|
// source; disp8×N follows its size).
|
||||||
"VPMOVSXBD": {2, 0x21, 0, 1, -1, vexRM, [3]int{4, 8, 16}},
|
"VPMOVSXBD": {2, 0x21, 0, 1, -1, vexRM, [3]int{4, 8, 16}},
|
||||||
"VPMOVSXBQ": {2, 0x22, 0, 1, -1, vexRM, [3]int{2, 4, 8}},
|
"VPMOVSXBQ": {2, 0x22, 0, 1, -1, vexRM, [3]int{2, 4, 8}},
|
||||||
@@ -361,7 +361,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPMOVZXWQ": {2, 0x34, 0, 1, -1, vexRM, [3]int{4, 8, 16}},
|
"VPMOVZXWQ": {2, 0x34, 0, 1, -1, vexRM, [3]int{4, 8, 16}},
|
||||||
"VPMOVZXDQ": {2, 0x35, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VPMOVZXDQ": {2, 0x35, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
|
|
||||||
// EVEX.F3.0F38 — the remaining narrowing stores (vector source in reg,
|
// EVEX.F3.0F38, the remaining narrowing stores (vector source in reg,
|
||||||
// narrow destination in r/m): signed, unsigned and the D/Q truncations.
|
// narrow destination in r/m): signed, unsigned and the D/Q truncations.
|
||||||
"VPMOVSDB": {2, 0x21, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
"VPMOVSDB": {2, 0x21, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
||||||
"VPMOVSQB": {2, 0x22, 0, 2, -1, vexRMRev, [3]int{2, 4, 8}},
|
"VPMOVSQB": {2, 0x22, 0, 2, -1, vexRMRev, [3]int{2, 4, 8}},
|
||||||
@@ -378,7 +378,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPMOVDB": {2, 0x31, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
"VPMOVDB": {2, 0x31, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
||||||
"VPMOVQW": {2, 0x34, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
"VPMOVQW": {2, 0x34, 0, 2, -1, vexRMRev, [3]int{4, 8, 16}},
|
||||||
|
|
||||||
// EVEX.F3.0F38 — mask/vector conversions: M2* moves an opmask register
|
// EVEX.F3.0F38, mask/vector conversions: M2* moves an opmask register
|
||||||
// into a vector (rm = K source, reg = vector destination), *2M does the
|
// into a vector (rm = K source, reg = vector destination), *2M does the
|
||||||
// reverse (reg = K destination, rm = vector source, the length follows
|
// reverse (reg = K destination, rm = vector source, the length follows
|
||||||
// the vector).
|
// the vector).
|
||||||
@@ -391,7 +391,7 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPMOVD2M": {2, 0x39, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VPMOVD2M": {2, 0x39, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
"VPMOVQ2M": {2, 0x39, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
"VPMOVQ2M": {2, 0x39, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX — scalar conversions between vector and general-purpose
|
// EVEX, scalar conversions between vector and general-purpose
|
||||||
// registers. Vector to GPR (two operands: vec/mem source, GPR
|
// registers. Vector to GPR (two operands: vec/mem source, GPR
|
||||||
// destination, vvvv unused): the signed and truncated pair, and the
|
// destination, vvvv unused): the signed and truncated pair, and the
|
||||||
// unsigned forms (EVEX only).
|
// unsigned forms (EVEX only).
|
||||||
@@ -421,22 +421,22 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VCVTUSI2SDQ": {1, 0x7B, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VCVTUSI2SDQ": {1, 0x7B, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
"VCVTUSI2SSL": {1, 0x7B, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
"VCVTUSI2SSL": {1, 0x7B, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
|
||||||
"VCVTUSI2SSQ": {1, 0x7B, 1, 2, -1, vexNDS3, [3]int{8, 8, 8}},
|
"VCVTUSI2SSQ": {1, 0x7B, 1, 2, -1, vexNDS3, [3]int{8, 8, 8}},
|
||||||
// EVEX.128/256/512.66.0F38.W0 — sign-extend dwords to qwords; the memory
|
// EVEX.128/256/512.66.0F38.W0, sign-extend dwords to qwords; the memory
|
||||||
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
|
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
|
||||||
// the xmm/ymm/zmm destination lengths).
|
// the xmm/ymm/zmm destination lengths).
|
||||||
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM, [3]int{8, 16, 32}},
|
||||||
|
|
||||||
// EVEX.512.66.0F3A.W1 — lane extract (reg=ZMM source, rm=YMM/memory
|
// EVEX.512.66.0F3A.W1, lane extract (reg=ZMM source, rm=YMM/memory
|
||||||
// destination, imm8).
|
// destination, imm8).
|
||||||
"VEXTRACTI64X4": {3, 0x3B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
"VEXTRACTI64X4": {3, 0x3B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||||
"VEXTRACTF64X4": {3, 0x1B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
"VEXTRACTF64X4": {3, 0x1B, 1, 1, -1, vexExtract, [3]int{0, 0, 32}},
|
||||||
|
|
||||||
// EVEX.66.0F38 — more integer NDS forms (W distinguishes D/Q).
|
// EVEX.66.0F38, more integer NDS forms (W distinguishes D/Q).
|
||||||
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPMULLQ": {2, 0x40, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
|
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3, [3]int{0, 32, 64}},
|
||||||
|
|
||||||
// EVEX.128/256/512 — the wider integer set (AVX-512 F/BW): byte/word
|
// EVEX.128/256/512, the wider integer set (AVX-512 F/BW): byte/word
|
||||||
// arithmetic, the bitwise ops with D/Q suffixes, min/max, averages and
|
// arithmetic, the bitwise ops with D/Q suffixes, min/max, averages and
|
||||||
// variable shifts. All NDS form; W distinguishes element size.
|
// variable shifts. All NDS form; W distinguishes element size.
|
||||||
"VPADDB": {1, 0xFC, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPADDB": {1, 0xFC, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
@@ -474,21 +474,21 @@ var evexTable = map[string]evexSpec{
|
|||||||
"VPSRAVQ": {2, 0x46, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPSRAVQ": {2, 0x46, 1, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX forms of instructions that also exist in VEX (selected when a ZMM
|
// EVEX forms of instructions that also exist in VEX (selected when a ZMM
|
||||||
// or K register, or indices 16–31, demand EVEX).
|
// or K register, or indices 16-31, demand EVEX).
|
||||||
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM, [3]int{16, 32, 64}},
|
||||||
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.66.0F — immediate shift (VPSLLD /6).
|
// EVEX.66.0F, immediate shift (VPSLLD /6).
|
||||||
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm, [3]int{16, 32, 64}},
|
||||||
|
|
||||||
// EVEX.F3.0F38.W0 — narrowing stores: reg = wide source, rm = narrow
|
// EVEX.F3.0F38.W0, narrowing stores: reg = wide source, rm = narrow
|
||||||
// destination (VPMOVDW dword→word, VPMOVQD qword→dword).
|
// destination (VPMOVDW dword→word, VPMOVQD qword→dword).
|
||||||
"VPMOVDW": {2, 0x33, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
"VPMOVDW": {2, 0x33, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
||||||
"VPMOVQD": {2, 0x35, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
"VPMOVQD": {2, 0x35, 0, 2, -1, vexRMRev, [3]int{8, 16, 32}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// evexBcastSpec describes an EVEX broadcast (VPBROADCASTD/Q): the opcode
|
// evexBcastSpec describes an EVEX broadcast (VPBROADCASTD/Q): the opcode
|
||||||
// depends on the source kind — a GPR source uses opReg, a memory source uses
|
// depends on the source kind, a GPR source uses opReg, a memory source uses
|
||||||
// opMem with a disp8×N of n.
|
// opMem with a disp8×N of n.
|
||||||
type evexBcastSpec struct {
|
type evexBcastSpec struct {
|
||||||
mapSel int
|
mapSel int
|
||||||
@@ -499,10 +499,10 @@ type evexBcastSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var evexBcastTable = map[string]evexBcastSpec{
|
var evexBcastTable = map[string]evexBcastSpec{
|
||||||
// EVEX.128/256/512.66.0F38 — broadcast a dword/qword to all lanes.
|
// EVEX.128/256/512.66.0F38, broadcast a dword/qword to all lanes.
|
||||||
"VPBROADCASTD": {2, 0x7C, 0x58, 0, 4},
|
"VPBROADCASTD": {2, 0x7C, 0x58, 0, 4},
|
||||||
"VPBROADCASTQ": {2, 0x7C, 0x59, 1, 8},
|
"VPBROADCASTQ": {2, 0x7C, 0x59, 1, 8},
|
||||||
// EVEX.128/256/512.66.0F38 — broadcast a byte/word (GPR or memory
|
// EVEX.128/256/512.66.0F38, broadcast a byte/word (GPR or memory
|
||||||
// source) to all lanes.
|
// source) to all lanes.
|
||||||
"VPBROADCASTB": {2, 0x7A, 0x78, 0, 1},
|
"VPBROADCASTB": {2, 0x7A, 0x78, 0, 1},
|
||||||
"VPBROADCASTW": {2, 0x7B, 0x79, 0, 2},
|
"VPBROADCASTW": {2, 0x7B, 0x79, 0, 2},
|
||||||
@@ -521,26 +521,26 @@ type evexMoveSpec struct {
|
|||||||
|
|
||||||
// evexMoveTable maps an upper-case EVEX move mnemonic to its encoding.
|
// evexMoveTable maps an upper-case EVEX move mnemonic to its encoding.
|
||||||
var evexMoveTable = map[string]evexMoveSpec{
|
var evexMoveTable = map[string]evexMoveSpec{
|
||||||
// EVEX.128/256/512.F3.0F.W0 — unaligned integer move.
|
// EVEX.128/256/512.F3.0F.W0, unaligned integer move.
|
||||||
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
"VMOVDQU32": {1, 2, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.F3.0F.W1 — unaligned qword move.
|
// EVEX.128/256/512.F3.0F.W1, unaligned qword move.
|
||||||
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
"VMOVDQU64": {1, 2, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.F2.0F.W0 — unaligned byte move (byte/word moves use the
|
// EVEX.128/256/512.F2.0F.W0, unaligned byte move (byte/word moves use the
|
||||||
// F2 prefix, dword/qword moves F3; the element size only changes the tuple
|
// F2 prefix, dword/qword moves F3; the element size only changes the tuple
|
||||||
// semantics).
|
// semantics).
|
||||||
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
"VMOVDQU8": {1, 3, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.F2.0F.W1 — unaligned word move (shares the qword
|
// EVEX.128/256/512.F2.0F.W1, unaligned word move (shares the qword
|
||||||
// encoding).
|
// encoding).
|
||||||
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
"VMOVDQU16": {1, 3, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.66.0F.W1 — unaligned packed double move.
|
// EVEX.128/256/512.66.0F.W1, unaligned packed double move.
|
||||||
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
|
"VMOVUPD": {1, 1, 0x10, 0x11, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512 — aligned packed moves.
|
// EVEX.128/256/512, aligned packed moves.
|
||||||
"VMOVAPS": {1, 0, 0x28, 0x29, 0, [3]int{16, 32, 64}},
|
"VMOVAPS": {1, 0, 0x28, 0x29, 0, [3]int{16, 32, 64}},
|
||||||
"VMOVAPD": {1, 1, 0x28, 0x29, 1, [3]int{16, 32, 64}},
|
"VMOVAPD": {1, 1, 0x28, 0x29, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128/256/512.66.0F — aligned integer moves.
|
// EVEX.128/256/512.66.0F, aligned integer moves.
|
||||||
"VMOVDQA32": {1, 1, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
"VMOVDQA32": {1, 1, 0x6F, 0x7F, 0, [3]int{16, 32, 64}},
|
||||||
"VMOVDQA64": {1, 1, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
"VMOVDQA64": {1, 1, 0x6F, 0x7F, 1, [3]int{16, 32, 64}},
|
||||||
// EVEX.128.F3.0F.W0 — scalar single move, memory operands (the
|
// EVEX.128.F3.0F.W0, scalar single move, memory operands (the
|
||||||
// three-operand register form is not supported).
|
// three-operand register form is not supported).
|
||||||
"VMOVSS": {1, 2, 0x10, 0x11, 0, [3]int{4, 4, 4}},
|
"VMOVSS": {1, 2, 0x10, 0x11, 0, [3]int{4, 4, 4}},
|
||||||
}
|
}
|
||||||
@@ -559,7 +559,7 @@ func isEvex(mnemUpper string) bool {
|
|||||||
|
|
||||||
// evexRequired reports whether the operands force the EVEX encoding of a
|
// evexRequired reports whether the operands force the EVEX encoding of a
|
||||||
// mnemonic that also has a VEX form: ZMM and K registers do, and so do
|
// mnemonic that also has a VEX form: ZMM and K registers do, and so do
|
||||||
// register indices 16–31, which only EVEX can represent (X16–Y31 exist
|
// register indices 16-31, which only EVEX can represent (X16-Y31 exist
|
||||||
// solely under AVX-512).
|
// solely under AVX-512).
|
||||||
func evexRequired(upper string, ops []Operand) bool {
|
func evexRequired(upper string, ops []Operand) bool {
|
||||||
_, inVex := vexTable[upper]
|
_, inVex := vexTable[upper]
|
||||||
@@ -578,7 +578,7 @@ func evexRequired(upper string, ops []Operand) bool {
|
|||||||
// evexSuffix carries the EVEX mnemonic suffixes the Go assembler accepts:
|
// evexSuffix carries the EVEX mnemonic suffixes the Go assembler accepts:
|
||||||
// zeroing (.Z), a rounding mode (.RN_SAE, .RD_SAE, .RU_SAE, .RZ_SAE),
|
// zeroing (.Z), a rounding mode (.RN_SAE, .RD_SAE, .RU_SAE, .RZ_SAE),
|
||||||
// suppress-all-exceptions (.SAE) and memory broadcast (.BCST). Masking is
|
// suppress-all-exceptions (.SAE) and memory broadcast (.BCST). Masking is
|
||||||
// not a suffix — Go writes it as an explicit K operand.
|
// not a suffix, Go writes it as an explicit K operand.
|
||||||
type evexSuffix struct {
|
type evexSuffix struct {
|
||||||
zeroing bool
|
zeroing bool
|
||||||
sae bool
|
sae bool
|
||||||
@@ -668,7 +668,7 @@ var evexRound = map[string]bool{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// evexBcstN maps an instruction accepting .BCST to the broadcast element
|
// evexBcstN maps an instruction accepting .BCST to the broadcast element
|
||||||
// size — the disp8×N multiplier for its memory operand.
|
// size, the disp8×N multiplier for its memory operand.
|
||||||
var evexBcstN = map[string]int{
|
var evexBcstN = map[string]int{
|
||||||
"VADDPD": 8, "VSUBPD": 8, "VMULPD": 8, "VDIVPD": 8,
|
"VADDPD": 8, "VSUBPD": 8, "VMULPD": 8, "VDIVPD": 8,
|
||||||
"VMINPD": 8, "VMAXPD": 8,
|
"VMINPD": 8, "VMAXPD": 8,
|
||||||
@@ -689,7 +689,7 @@ var evexBcstN = map[string]int{
|
|||||||
"VCVTTPD2QQ": 8, "VCVTTPS2QQ": 4, "VCVTUQQ2PD": 8, "VCVTUQQ2PS": 8,
|
"VCVTTPD2QQ": 8, "VCVTTPS2QQ": 4, "VCVTUQQ2PD": 8, "VCVTUQQ2PS": 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitMask extracts an explicit mask register (K1–K7) from the operand list,
|
// splitMask extracts an explicit mask register (K1-K7) from the operand list,
|
||||||
// returning the remaining operands and the mask index. K0 is not a usable
|
// returning the remaining operands and the mask index. K0 is not a usable
|
||||||
// mask (aaa = 0 means "no mask"), matching the assembler.
|
// mask (aaa = 0 means "no mask"), matching the assembler.
|
||||||
func splitMask(ops []Operand) ([]Operand, int, error) {
|
func splitMask(ops []Operand) ([]Operand, int, error) {
|
||||||
@@ -712,7 +712,7 @@ func splitMask(ops []Operand) ([]Operand, int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order. The
|
// encodeEvex encodes an EVEX instruction with operands in Plan 9 order. The
|
||||||
// mask, when present, is an explicit K1–K7 operand anywhere among the
|
// mask, when present, is an explicit K1-K7 operand anywhere among the
|
||||||
// operands; the mnemonic suffix carries zeroing, rounding/SAE and
|
// operands; the mnemonic suffix carries zeroing, rounding/SAE and
|
||||||
// broadcast.
|
// broadcast.
|
||||||
func (e *enc) encodeEvex(mnemUpper string, ops []Operand, sfx evexSuffix) error {
|
func (e *enc) encodeEvex(mnemUpper string, ops []Operand, sfx evexSuffix) error {
|
||||||
@@ -1042,7 +1042,7 @@ func (e *enc) encodeEvexMove(mnem string, ms evexMoveSpec, ops []Operand, mask i
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeEvexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
// encodeEvexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
||||||
// the destination always XMM and the length fixed by the mnemonic — the
|
// the destination always XMM and the length fixed by the mnemonic, the
|
||||||
// single valid slot of spec.n names the vector length (and the disp8×N
|
// single valid slot of spec.n names the vector length (and the disp8×N
|
||||||
// multiplier) a register or memory source encodes.
|
// multiplier) a register or memory source encodes.
|
||||||
func (e *enc) encodeEvexRMSrcLen(spec evexSpec, ops []Operand, mask int, sfx evexSuffix) error {
|
func (e *enc) encodeEvexRMSrcLen(spec evexSpec, ops []Operand, mask int, sfx evexSuffix) error {
|
||||||
@@ -1061,7 +1061,7 @@ func (e *enc) encodeEvexRMSrcLen(spec evexSpec, ops []Operand, mask int, sfx eve
|
|||||||
return e.emitEvexFields(spec, ll, dstReg.idx, -1, src, mask, sfx)
|
return e.emitEvexFields(spec, ll, dstReg.idx, -1, src, mask, sfx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// soleLen returns the vector-length index of the single valid slot of n —
|
// soleLen returns the vector-length index of the single valid slot of n
|
||||||
// the length a length-fixed mnemonic (the EVEX conversion spellings) encodes
|
// the length a length-fixed mnemonic (the EVEX conversion spellings) encodes
|
||||||
// regardless of its operands.
|
// regardless of its operands.
|
||||||
func soleLen(n [3]int) (int, error) {
|
func soleLen(n [3]int) (int, error) {
|
||||||
@@ -1131,8 +1131,8 @@ func (e *enc) encodeEvexBcast(bs evexBcastSpec, ops []Operand, mask int, sfx eve
|
|||||||
|
|
||||||
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
|
// emitEvexFields emits the EVEX prefix, opcode, ModR/M, SIB and displacement
|
||||||
// (disp8×N compressed) for the given precomputed fields. regIdx is the
|
// (disp8×N compressed) for the given precomputed fields. regIdx is the
|
||||||
// unextended reg-field register index, or a /digit (0–7); vvvvIdx is the
|
// unextended reg-field register index, or a /digit (0-7); vvvvIdx is the
|
||||||
// vvvv register index, or -1 when unused. mask (K1–K7, 0 = unmasked) and
|
// vvvv register index, or -1 when unused. mask (K1-K7, 0 = unmasked) and
|
||||||
// zeroing fill the aaa and z bits of the P2 byte.
|
// zeroing fill the aaa and z bits of the P2 byte.
|
||||||
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand, mask int, sfx evexSuffix) error {
|
func (e *enc) emitEvexFields(spec evexSpec, ll, regIdx, vvvvIdx int, rm Operand, mask int, sfx evexSuffix) error {
|
||||||
if ll > 2 {
|
if ll > 2 {
|
||||||
@@ -1324,7 +1324,7 @@ func isScatter(upper string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// vsibLen validates a VSIB memory operand (the index must be a vector
|
// vsibLen validates a VSIB memory operand (the index must be a vector
|
||||||
// register) and returns it with the vector length the index selects — the
|
// register) and returns it with the vector length the index selects, the
|
||||||
// EVEX L'L field follows the index register, not the data register.
|
// EVEX L'L field follows the index register, not the data register.
|
||||||
func vsibLen(op Operand, what string) (Mem, int, error) {
|
func vsibLen(op Operand, what string) (Mem, int, error) {
|
||||||
m, ok := op.(Mem)
|
m, ok := op.(Mem)
|
||||||
@@ -1383,7 +1383,7 @@ func (e *enc) encodeGather(upper string, gs gatherSpec, ops []Operand, sfx evexS
|
|||||||
return e.emitVexFields(spec, dst.vecLenBit(), dst.idx&7, rBit, 15-maskReg.idx, vsib)
|
return e.emitVexFields(spec, dst.vecLenBit(), dst.idx&7, rBit, 15-maskReg.idx, vsib)
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeScatter encodes a scatter (EVEX only): OP src, K, vsib — reg = src,
|
// encodeScatter encodes a scatter (EVEX only): OP src, K, vsib, reg = src,
|
||||||
// rm = the VSIB memory operand, the K mask in aaa and L following the VSIB
|
// rm = the VSIB memory operand, the K mask in aaa and L following the VSIB
|
||||||
// index.
|
// index.
|
||||||
func (e *enc) encodeScatter(upper string, ss gatherSpec, ops []Operand, sfx evexSuffix) error {
|
func (e *enc) encodeScatter(upper string, ss gatherSpec, ops []Operand, sfx evexSuffix) error {
|
||||||
@@ -1411,15 +1411,15 @@ func (e *enc) encodeScatter(upper string, ss gatherSpec, ops []Operand, sfx evex
|
|||||||
|
|
||||||
// evexKOperand lists the instructions whose K register is a genuine operand
|
// evexKOperand lists the instructions whose K register is a genuine operand
|
||||||
// (the source or destination of a mask/vector conversion) rather than a
|
// (the source or destination of a mask/vector conversion) rather than a
|
||||||
// mask modifier — the M2 and 2M conversions. They take no masking.
|
// mask modifier, the M2 and 2M conversions. They take no masking.
|
||||||
var evexKOperand = map[string]bool{
|
var evexKOperand = map[string]bool{
|
||||||
"VPMOVM2B": true, "VPMOVM2W": true, "VPMOVM2D": true, "VPMOVM2Q": true,
|
"VPMOVM2B": true, "VPMOVM2W": true, "VPMOVM2D": true, "VPMOVM2Q": true,
|
||||||
"VPMOVB2M": true, "VPMOVW2M": true, "VPMOVD2M": true, "VPMOVQ2M": true,
|
"VPMOVB2M": true, "VPMOVW2M": true, "VPMOVD2M": true, "VPMOVQ2M": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// kmovSpec describes a KMOV width: the opcode depends on the operand
|
// kmovSpec describes a KMOV width: the opcode depends on the operand
|
||||||
// direction — kk (k/mem → K is 90, k → k uses the same), kmem (K → mem),
|
// direction, kk (k/mem → K is 90, k → k uses the same), kmem (K → mem),
|
||||||
// gprk (GPR/mem → K), kgpr (K → GPR) — and the GPR forms carry a mandatory
|
// gprk (GPR/mem → K), kgpr (K → GPR), and the GPR forms carry a mandatory
|
||||||
// prefix and W for the wider widths.
|
// prefix and W for the wider widths.
|
||||||
type kmovSpec struct {
|
type kmovSpec struct {
|
||||||
kk, kmem, gprk, kgpr byte
|
kk, kmem, gprk, kgpr byte
|
||||||
|
|||||||
+13
-13
@@ -16,7 +16,7 @@ import (
|
|||||||
// kernels use: NDS arithmetic, immediate and variable shifts, shuffles with
|
// kernels use: NDS arithmetic, immediate and variable shifts, shuffles with
|
||||||
// an immediate, lane extracts, narrowing stores, broadcasts from a GPR or
|
// an immediate, lane extracts, narrowing stores, broadcasts from a GPR or
|
||||||
// memory, mask destinations, mask moves, disp8×N compression and the 5-bit
|
// memory, mask destinations, mask moves, disp8×N compression and the 5-bit
|
||||||
// register fields (X/Y 16–31, Z 0–31).
|
// register fields (X/Y 16-31, Z 0-31).
|
||||||
func TestEvexGroundTruth(t *testing.T) {
|
func TestEvexGroundTruth(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -58,7 +58,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VMOVDQU32 16(SI)(R15*4),Z4", "VMOVDQU32", []Operand{Idx(SI, vreg(t, "R15"), 4, 16, 64), vreg(t, "Z4")}, "62b17e486fa4be10000000"},
|
{"VMOVDQU32 16(SI)(R15*4),Z4", "VMOVDQU32", []Operand{Idx(SI, vreg(t, "R15"), 4, 16, 64), vreg(t, "Z4")}, "62b17e486fa4be10000000"},
|
||||||
{"VMOVDQU32 Z0,4(SI)(AX*1)", "VMOVDQU32", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f17e487f840604000000"},
|
{"VMOVDQU32 Z0,4(SI)(AX*1)", "VMOVDQU32", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f17e487f840604000000"},
|
||||||
{"VMOVDQU32 Z3,(DI)(R15*4)", "VMOVDQU32", []Operand{vreg(t, "Z3"), Idx(DI, vreg(t, "R15"), 4, 0, 64)}, "62b17e487f1cbf"},
|
{"VMOVDQU32 Z3,(DI)(R15*4)", "VMOVDQU32", []Operand{vreg(t, "Z3"), Idx(DI, vreg(t, "R15"), 4, 0, 64)}, "62b17e487f1cbf"},
|
||||||
// VMOVDQU64 — the W1 qword variant.
|
// VMOVDQU64; the W1 qword variant.
|
||||||
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
|
{"VMOVDQU64 (SI)(R15*4),Z3", "VMOVDQU64", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b1fe486f1cbe"},
|
||||||
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
|
{"VMOVDQU64 Z0,4(SI)(AX*1)", "VMOVDQU64", []Operand{vreg(t, "Z0"), Idx(SI, AX, 1, 4, 64)}, "62f1fe487f840604000000"},
|
||||||
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
|
{"VMOVDQU64 Z1,Z2", "VMOVDQU64", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487fca"},
|
||||||
@@ -77,7 +77,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VPSHUFB Z1,Z2,Z3", "VPSHUFB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d4800d9"},
|
{"VPSHUFB Z1,Z2,Z3", "VPSHUFB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d4800d9"},
|
||||||
{"VMOVDQU8 Z1,Z2", "VMOVDQU8", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17f487fca"},
|
{"VMOVDQU8 Z1,Z2", "VMOVDQU8", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17f487fca"},
|
||||||
{"VMOVDQU16 Z1,Z2", "VMOVDQU16", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ff487fca"},
|
{"VMOVDQU16 Z1,Z2", "VMOVDQU16", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ff487fca"},
|
||||||
// Indices 16–31: rm[4] rides in X̄ for register operands.
|
// Indices 16-31: rm[4] rides in X̄ for register operands.
|
||||||
{"VPSHUFD $1,X16,X17", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}, "62a17d0870c801"},
|
{"VPSHUFD $1,X16,X17", "VPSHUFD", []Operand{Imm(1), vreg(t, "X16"), vreg(t, "X17")}, "62a17d0870c801"},
|
||||||
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
|
{"VMOVUPD (DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 0, 64), vreg(t, "Z14")}, "6271fd481037"},
|
||||||
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
|
{"VMOVUPD 64(DI),Z14", "VMOVUPD", []Operand{Ptr(DI, 64, 64), vreg(t, "Z14")}, "6271fd48107701"},
|
||||||
@@ -96,7 +96,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VPBROADCASTD 4(SI),Z10", "VPBROADCASTD", []Operand{Ptr(SI, 4, 4), vreg(t, "Z10")}, "62727d48585601"},
|
{"VPBROADCASTD 4(SI),Z10", "VPBROADCASTD", []Operand{Ptr(SI, 4, 4), vreg(t, "Z10")}, "62727d48585601"},
|
||||||
{"VPBROADCASTQ R8,X31", "VPBROADCASTQ", []Operand{vreg(t, "R8"), vreg(t, "X31")}, "6242fd087cf8"},
|
{"VPBROADCASTQ R8,X31", "VPBROADCASTQ", []Operand{vreg(t, "R8"), vreg(t, "X31")}, "6242fd087cf8"},
|
||||||
{"VPBROADCASTQ AX,Z9", "VPBROADCASTQ", []Operand{AX, vreg(t, "Z9")}, "6272fd487cc8"},
|
{"VPBROADCASTQ AX,Z9", "VPBROADCASTQ", []Operand{AX, vreg(t, "Z9")}, "6272fd487cc8"},
|
||||||
// Register indices 16–31 exist only in EVEX encodings.
|
// Register indices 16-31 exist only in EVEX encodings.
|
||||||
{"VPBROADCASTD AX,Y30", "VPBROADCASTD", []Operand{AX, vreg(t, "Y30")}, "62627d287cf0"},
|
{"VPBROADCASTD AX,Y30", "VPBROADCASTD", []Operand{AX, vreg(t, "Y30")}, "62627d287cf0"},
|
||||||
// Packed double arithmetic / unpack (EVEX forms carry W=1).
|
// Packed double arithmetic / unpack (EVEX forms carry W=1).
|
||||||
{"VSUBPD Z1,Z2,Z3", "VSUBPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed485cd9"},
|
{"VSUBPD Z1,Z2,Z3", "VSUBPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed485cd9"},
|
||||||
@@ -107,7 +107,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
{"VUNPCKHPD Z1,Z2,Z3", "VUNPCKHPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed4815d9"},
|
{"VUNPCKHPD Z1,Z2,Z3", "VUNPCKHPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed4815d9"},
|
||||||
{"VSUBPD 64(AX),Z1,Z2", "VSUBPD", []Operand{Ptr(AX, 64, 64), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1f5485c5001"},
|
{"VSUBPD 64(AX),Z1,Z2", "VSUBPD", []Operand{Ptr(AX, 64, 64), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1f5485c5001"},
|
||||||
{"VSUBPD Z17,Z18,Z19", "VSUBPD", []Operand{vreg(t, "Z17"), vreg(t, "Z18"), vreg(t, "Z19")}, "62a1ed405cd9"},
|
{"VSUBPD Z17,Z18,Z19", "VSUBPD", []Operand{vreg(t, "Z17"), vreg(t, "Z18"), vreg(t, "Z19")}, "62a1ed405cd9"},
|
||||||
// VMOVDDUP — duplicate the low double; disp8×N = 64 at 512 bits, and
|
// VMOVDDUP; duplicate the low double; disp8×N = 64 at 512 bits, and
|
||||||
// X16/X17 force EVEX (the mod=11 rm[4] extension rides in X̄).
|
// X16/X17 force EVEX (the mod=11 rm[4] extension rides in X̄).
|
||||||
{"VMOVDDUP Z1,Z2", "VMOVDDUP", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ff4812d1"},
|
{"VMOVDDUP Z1,Z2", "VMOVDDUP", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ff4812d1"},
|
||||||
{"VMOVDDUP 64(AX),Z1", "VMOVDDUP", []Operand{Ptr(AX, 64, 64), vreg(t, "Z1")}, "62f1ff48124801"},
|
{"VMOVDDUP 64(AX),Z1", "VMOVDDUP", []Operand{Ptr(AX, 64, 64), vreg(t, "Z1")}, "62f1ff48124801"},
|
||||||
@@ -150,7 +150,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEvexMasking checks the AVX-512 mask operand (K1–K7, placed freely among
|
// TestEvexMasking checks the AVX-512 mask operand (K1-K7, placed freely among
|
||||||
// the operands) and the .Z zeroing suffix, byte for byte against the Go
|
// the operands) and the .Z zeroing suffix, byte for byte against the Go
|
||||||
// assembler.
|
// assembler.
|
||||||
func TestEvexMasking(t *testing.T) {
|
func TestEvexMasking(t *testing.T) {
|
||||||
@@ -241,11 +241,11 @@ func TestEvexMasking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEvexExtendedGroundTruth covers the wider EVEX/AVX-512 set — ternary
|
// TestEvexExtendedGroundTruth covers the wider EVEX/AVX-512 set; ternary
|
||||||
// logic, lane shuffles/inserts/extracts, compares with a K destination,
|
// logic, lane shuffles/inserts/extracts, compares with a K destination,
|
||||||
// permutes, the wider integer families, expand/compress, broadcasts,
|
// permutes, the wider integer families, expand/compress, broadcasts,
|
||||||
// rotates and word shifts, the opmask instructions, the EVEX suffixes
|
// rotates and word shifts, the opmask instructions, the EVEX suffixes
|
||||||
// (rounding/SAE/broadcast) and the aligned/scalar moves — byte for byte
|
// (rounding/SAE/broadcast) and the aligned/scalar moves; byte for byte
|
||||||
// against the Go assembler.
|
// against the Go assembler.
|
||||||
func TestEvexExtendedGroundTruth(t *testing.T) {
|
func TestEvexExtendedGroundTruth(t *testing.T) {
|
||||||
mem64 := func(base Reg) Operand { return Ptr(base, 0, 64) }
|
mem64 := func(base Reg) Operand { return Ptr(base, 0, 64) }
|
||||||
@@ -275,7 +275,7 @@ func TestEvexExtendedGroundTruth(t *testing.T) {
|
|||||||
{"VMULPD.RZ_SAE.Z", "VMULPD.RZ_SAE.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f1edf959d9"},
|
{"VMULPD.RZ_SAE.Z", "VMULPD.RZ_SAE.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f1edf959d9"},
|
||||||
{"VMAXPD.SAE", "VMAXPD.SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed585fd9"},
|
{"VMAXPD.SAE", "VMAXPD.SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed585fd9"},
|
||||||
{"VADDPD.BCST", "VADDPD.BCST", []Operand{mem64(AX), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1f5585810"},
|
{"VADDPD.BCST", "VADDPD.BCST", []Operand{mem64(AX), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1f5585810"},
|
||||||
// Packed single arithmetic (same opcodes, no mandatory prefix) —
|
// Packed single arithmetic (same opcodes, no mandatory prefix);
|
||||||
// ZMM, YMM and XMM widths, rounding and broadcast.
|
// ZMM, YMM and XMM widths, rounding and broadcast.
|
||||||
{"VADDPS", "VADDPS", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16c4858d9"},
|
{"VADDPS", "VADDPS", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16c4858d9"},
|
||||||
{"VMULPS", "VMULPS", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ec59d9"},
|
{"VMULPS", "VMULPS", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ec59d9"},
|
||||||
@@ -399,8 +399,8 @@ func TestEvexExtendedGroundTruth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestEvexHelperGroundTruth covers the floating-point helper and conversion
|
// TestEvexHelperGroundTruth covers the floating-point helper and conversion
|
||||||
// tail of the EVEX set — reciprocals, rsqrt, getexp/getmant, scalef,
|
// tail of the EVEX set; reciprocals, rsqrt, getexp/getmant, scalef,
|
||||||
// rndscale, reduce, fixupimm, range, fpclass, the remaining conversions —
|
// rndscale, reduce, fixupimm, range, fpclass, the remaining conversions;
|
||||||
// plus gather/scatter with VSIB addressing, byte for byte against the Go
|
// plus gather/scatter with VSIB addressing, byte for byte against the Go
|
||||||
// assembler.
|
// assembler.
|
||||||
func TestEvexHelperGroundTruth(t *testing.T) {
|
func TestEvexHelperGroundTruth(t *testing.T) {
|
||||||
@@ -502,9 +502,9 @@ func TestEvexHelperGroundTruth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestEvexGprGroundTruth covers the scalar conversions between vector and
|
// TestEvexGprGroundTruth covers the scalar conversions between vector and
|
||||||
// general-purpose registers — the signed and truncated VCVT{,T}S{D,S}2SI
|
// general-purpose registers; the signed and truncated VCVT{,T}S{D,S}2SI
|
||||||
// forms (VEX and EVEX), the unsigned EVEX-only forms, and the GPR-to-vector
|
// forms (VEX and EVEX), the unsigned EVEX-only forms, and the GPR-to-vector
|
||||||
// VCVTSI2*/VCVTUSI2* forms with the preserved vector source in vvvv — byte
|
// VCVTSI2*/VCVTUSI2* forms with the preserved vector source in vvvv; byte
|
||||||
// for byte against the Go assembler, including memory sources and extended
|
// for byte against the Go assembler, including memory sources and extended
|
||||||
// GPRs.
|
// GPRs.
|
||||||
func TestEvexGprGroundTruth(t *testing.T) {
|
func TestEvexGprGroundTruth(t *testing.T) {
|
||||||
|
|||||||
+93
-11
@@ -14,8 +14,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This file emits GOOBJ — the Go toolchain's object format, which cmd/link
|
// This file emits GOOBJ, the Go toolchain's object format, which cmd/link
|
||||||
// consumes directly — so gasm-assembled functions drop into a go build
|
// consumes directly, so gasm-assembled functions drop into a go build
|
||||||
// without the Go assembler. The layout follows cmd/internal/goobj: a
|
// without the Go assembler. The layout follows cmd/internal/goobj: a
|
||||||
// toolchain preamble ("go object ...\n!\n"), the go120ld header with its
|
// toolchain preamble ("go object ...\n!\n"), the go120ld header with its
|
||||||
// block offsets, a string table, symbol definitions, the relocation /
|
// block offsets, a string table, symbol definitions, the relocation /
|
||||||
@@ -94,10 +94,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Relocation types (cmd/internal/objabi).
|
// Relocation types (cmd/internal/objabi).
|
||||||
// R_PCREL and R_ADDR are stable across Go versions.
|
// R_ADDR, R_CALL, R_PCREL and R_TLS_LE are stable across Go versions.
|
||||||
const (
|
const (
|
||||||
relocPCRel = 14 // R_PCREL
|
|
||||||
relocAddr = 1 // R_ADDR
|
relocAddr = 1 // R_ADDR
|
||||||
|
relocCall = 7 // R_CALL
|
||||||
|
relocPCRel = 14 // R_PCREL
|
||||||
|
relocTLSLE = 15 // R_TLS_LE
|
||||||
)
|
)
|
||||||
|
|
||||||
// relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the
|
// relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the
|
||||||
@@ -142,8 +144,28 @@ func isGo127OrLater() bool {
|
|||||||
const (
|
const (
|
||||||
pkgIdxNone = 0x7fffffff
|
pkgIdxNone = 0x7fffffff
|
||||||
pkgIdxSelf = 0x7ffffffb
|
pkgIdxSelf = 0x7ffffffb
|
||||||
|
pkgIdxBuiltin = 0x7ffffffc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// goobjBuiltinMorestackNoctxt is the index of runtime.morestack_noctxt in
|
||||||
|
// cmd/internal/goobj/builtinlist.go of the toolchain the object targets
|
||||||
|
// (246 since Go 1.25; the list is append-only).
|
||||||
|
const goobjBuiltinMorestackNoctxt = 246
|
||||||
|
|
||||||
|
// goobjBuiltinMorestack is the builtin reference the toolchain emits for the
|
||||||
|
// stack-guard call.
|
||||||
|
var goobjBuiltinMorestack = "runtime\u00b7morestack_noctxt"
|
||||||
|
|
||||||
|
// isCallReloc reports whether k is one of the per-arch call relocations a
|
||||||
|
// direct branch to a TEXT symbol carries.
|
||||||
|
func isCallReloc(k RelocKind) bool {
|
||||||
|
switch k {
|
||||||
|
case RelCall, RelRISCVJal, RelArm64Branch, RelLoong64Branch:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const goobjMagic = "\x00go120ld"
|
const goobjMagic = "\x00go120ld"
|
||||||
|
|
||||||
// goSym is one symbol definition under construction.
|
// goSym is one symbol definition under construction.
|
||||||
@@ -178,14 +200,24 @@ type dwarfRelocSet struct {
|
|||||||
// does with its -p flag). srcPath names the source file recorded in the
|
// does with its -p flag). srcPath names the source file recorded in the
|
||||||
// object's file table and line tables. The toolchain's object preamble is
|
// object's file table and line tables. The toolchain's object preamble is
|
||||||
// captured from the installed go tool asm, so the output links with the
|
// captured from the installed go tool asm, so the output links with the
|
||||||
// toolchain it was produced on — exactly like a real assembly object.
|
// toolchain it was produced on, exactly like a real assembly object.
|
||||||
func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||||
pre, err := toolchainObjectPreamble()
|
pre, err := toolchainObjectPreamble()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// amd64: MinLC 1, R_PCREL for the code relocations.
|
// amd64: MinLC 1, R_PCREL for displacements, R_CALL for calls and
|
||||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
|
// R_TLS_LE for the stack-guard TLS load.
|
||||||
|
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(r Reloc) (uint16, uint8) {
|
||||||
|
switch r.Kind {
|
||||||
|
case RelCall:
|
||||||
|
return relocCall, 4
|
||||||
|
case RelTLSLE:
|
||||||
|
return relocTLSLE, 4
|
||||||
|
default:
|
||||||
|
return relocPCRel, 4
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// emitGOObject assembles the GOOBJ payload for any architecture. pre is
|
// emitGOObject assembles the GOOBJ payload for any architecture. pre is
|
||||||
@@ -198,7 +230,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The non-package definitions first — the DWARF symbols reference the
|
// The non-package definitions first, the DWARF symbols reference the
|
||||||
// functions by these indices: per function the four pc-value tables
|
// functions by these indices: per function the four pc-value tables
|
||||||
// and the function itself, as cmd/asm lays them out.
|
// and the function itself, as cmd/asm lays them out.
|
||||||
type npSym struct {
|
type npSym struct {
|
||||||
@@ -247,7 +279,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||||
// zeroing r.Off would erase the opcode/register bits the linker
|
// zeroing r.Off would erase the opcode/register bits the linker
|
||||||
// preserves when it patches only the immediate.
|
// preserves when it patches only the immediate.
|
||||||
if r.Kind != RelPCRel32 {
|
if r.Kind != RelPCRel32 && r.Kind != RelCall {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||||
@@ -320,6 +352,13 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index the non-package TEXT definitions by short name for the internal
|
||||||
|
// call references.
|
||||||
|
textNpIdx := map[string]int{}
|
||||||
|
for i, fn := range img.Funcs {
|
||||||
|
textNpIdx[fn.Name] = fnNpIdx[i]
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve external symbol references (cross-package). Build the
|
// Resolve external symbol references (cross-package). Build the
|
||||||
// package index table and determine each external symbol's SymIdx
|
// package index table and determine each external symbol's SymIdx
|
||||||
// by reading the target package's export data.
|
// by reading the target package's export data.
|
||||||
@@ -327,12 +366,22 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
var extPkgIdx map[string]int
|
var extPkgIdx map[string]int
|
||||||
var extSymIdx map[string]int
|
var extSymIdx map[string]int
|
||||||
if len(img.Externals) > 0 {
|
if len(img.Externals) > 0 {
|
||||||
|
// The morestack call is a builtin reference, not a resolved external.
|
||||||
|
var need []string
|
||||||
|
for _, n := range img.Externals {
|
||||||
|
if n == goobjBuiltinMorestack {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
need = append(need, n)
|
||||||
|
}
|
||||||
|
if len(need) > 0 {
|
||||||
var err error
|
var err error
|
||||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Relocations, per defined symbol in definition order (package defs,
|
// Relocations, per defined symbol in definition order (package defs,
|
||||||
// then non-package defs).
|
// then non-package defs).
|
||||||
@@ -342,6 +391,31 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
si := len(defs) + fnNpIdx[i]
|
si := len(defs) + fnNpIdx[i]
|
||||||
for _, r := range fn.Relocs {
|
for _, r := range fn.Relocs {
|
||||||
typ, size := relocField(r)
|
typ, size := relocField(r)
|
||||||
|
if r.Kind == RelTLSLE {
|
||||||
|
// The TLS load has no symbol: {0, 0} is the nil ref.
|
||||||
|
var rec [23]byte
|
||||||
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||||
|
rec[4] = size
|
||||||
|
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||||
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||||
|
binary.LittleEndian.PutUint32(rec[15:], 0)
|
||||||
|
binary.LittleEndian.PutUint32(rec[19:], 0)
|
||||||
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.External && r.Name == goobjBuiltinMorestack {
|
||||||
|
// The stack-guard morestack call uses the toolchain's
|
||||||
|
// builtin reference.
|
||||||
|
var rec [23]byte
|
||||||
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||||
|
rec[4] = size
|
||||||
|
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||||
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||||
|
binary.LittleEndian.PutUint32(rec[15:], pkgIdxBuiltin)
|
||||||
|
binary.LittleEndian.PutUint32(rec[19:], goobjBuiltinMorestackNoctxt)
|
||||||
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if r.External {
|
if r.External {
|
||||||
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
||||||
pkg, name := splitQualified(r.Name)
|
pkg, name := splitQualified(r.Name)
|
||||||
@@ -366,16 +440,24 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
|||||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
pkg := uint32(pkgIdxSelf)
|
||||||
di, ok := defIdx[r.Name]
|
di, ok := defIdx[r.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// A call to a TEXT function of the same file references the
|
||||||
|
// non-package definition table.
|
||||||
|
ni, isText := textNpIdx[r.Name]
|
||||||
|
if !isText || !isCallReloc(r.Kind) {
|
||||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||||
}
|
}
|
||||||
|
pkg = pkgIdxNone
|
||||||
|
di = ni
|
||||||
|
}
|
||||||
var rec [23]byte
|
var rec [23]byte
|
||||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||||
rec[4] = size // field width
|
rec[4] = size // field width
|
||||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
binary.LittleEndian.PutUint32(rec[15:], pkg)
|
||||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func sortedPkgRefs(refs map[string][]string) []pkgRef {
|
|||||||
for pkg, syms := range refs {
|
for pkg, syms := range refs {
|
||||||
pkgs = append(pkgs, pkgRef{pkg, syms})
|
pkgs = append(pkgs, pkgRef{pkg, syms})
|
||||||
}
|
}
|
||||||
// Simple insertion sort — the list is tiny (usually 1–3 packages).
|
// Simple insertion sort, the list is tiny (usually 1-3 packages).
|
||||||
for i := 1; i < len(pkgs); i++ {
|
for i := 1; i < len(pkgs); i++ {
|
||||||
for j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- {
|
for j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- {
|
||||||
pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1]
|
pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1]
|
||||||
|
|||||||
+4
-4
@@ -158,8 +158,8 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
|||||||
t.Errorf("funcinfo bytes %x", fi)
|
t.Errorf("funcinfo bytes %x", fi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pc-value tables of addq (non-package indices 0–3, so global
|
// The pc-value tables of addq (non-package indices 0-3, so global
|
||||||
// indices 7–10): pcsp a flat zero over the whole function, pcinline a
|
// indices 7-10): pcsp a flat zero over the whole function, pcinline a
|
||||||
// flat -1, both with the pc delta in MinLC (1) units.
|
// flat -1, both with the pc delta in MinLC (1) units.
|
||||||
pcsp := data[le.Uint32(didx[4*7:]):]
|
pcsp := data[le.Uint32(didx[4*7:]):]
|
||||||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||||||
@@ -294,7 +294,7 @@ TEXT ·framed(SB), NOSPLIT, $8-0
|
|||||||
}
|
}
|
||||||
for i := range wantPCs {
|
for i := range wantPCs {
|
||||||
if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] {
|
if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] {
|
||||||
t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d) — all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals)
|
t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d); all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The last two steps unwind the epilogue to zero.
|
// The last two steps unwind the epilogue to zero.
|
||||||
@@ -333,7 +333,7 @@ TEXT ·useext(SB), NOSPLIT, $0-8
|
|||||||
|
|
||||||
// TestGOObjectLinkAndRun is the end-to-end check: assemble the test
|
// TestGOObjectLinkAndRun is the end-to-end check: assemble the test
|
||||||
// functions to a GOOBJ, swap it into a go build in place of the toolchain's
|
// functions to a GOOBJ, swap it into a go build in place of the toolchain's
|
||||||
// assembly object, link, and run — the output must match the baseline
|
// assembly object, link, and run; the output must match the baseline
|
||||||
// binary the Go assembler produced. Skipped when no Go toolchain is
|
// binary the Go assembler produced. Skipped when no Go toolchain is
|
||||||
// available.
|
// available.
|
||||||
func TestGOObjectLinkAndRun(t *testing.T) {
|
func TestGOObjectLinkAndRun(t *testing.T) {
|
||||||
|
|||||||
+13
-8
@@ -13,28 +13,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is
|
// GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is
|
||||||
// the shared one in goobj.go — the toolchain preamble, the go120ld header
|
// the shared one in goobj.go, the toolchain preamble, the go120ld header
|
||||||
// with its block offsets, the string table, the symbol definitions and the
|
// with its block offsets, the string table, the symbol definitions and the
|
||||||
// reloc/aux/data index arrays — with the arm64 preamble, the MinLC of 4
|
// reloc/aux/data index arrays, with the arm64 preamble, the MinLC of 4
|
||||||
// for the pc-value deltas, and R_ADDRARM64 relocation types for the
|
// for the pc-value deltas, and the arm64 relocation types for the ADRP
|
||||||
// ADRP+ADD/LDR/STR address pairs.
|
// pairs and BL calls.
|
||||||
func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) {
|
func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) {
|
||||||
pre, err := toolchainObjectPreambleAARCH64()
|
pre, err := toolchainObjectPreambleAARCH64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||||
if r.Kind == RelArm64Branch {
|
switch r.Kind {
|
||||||
|
case RelArm64Branch:
|
||||||
return relocArm64Branch, 4
|
return relocArm64Branch, 4
|
||||||
}
|
case RelArm64LDST64:
|
||||||
|
return relocArm64LDST64, 4
|
||||||
|
default:
|
||||||
return relocArm64Addr, 4
|
return relocArm64Addr, 4
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// arm64 relocation types (cmd/internal/objabi).
|
// arm64 relocation types (cmd/internal/objabi).
|
||||||
const (
|
const (
|
||||||
relocArm64Addr = 3 // R_ADDRARM64 — ADRP+ADD/LDR/STR pair
|
relocArm64Addr = 3 // R_ADDRARM64, ADRP+ADD pair
|
||||||
relocArm64Branch = 9 // R_CALLARM64 — BL instruction
|
relocArm64Branch = 9 // R_CALLARM64, BL instruction
|
||||||
|
relocArm64LDST64 = 40 // R_ARM64_PCREL_LDST64, ADRP+LDR/STR pair
|
||||||
)
|
)
|
||||||
|
|
||||||
// toolchainObjectPreambleAARCH64 returns the "go object ...\n!\n" header
|
// toolchainObjectPreambleAARCH64 returns the "go object ...\n!\n" header
|
||||||
|
|||||||
+11
-5
@@ -13,9 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GOObjectLOONG64 emits a GOOBJ object file for LoongArch. The layout is
|
// GOObjectLOONG64 emits a GOOBJ object file for LoongArch. The layout is
|
||||||
// the shared one in goobj.go — the toolchain preamble, the go120ld header
|
// the shared one in goobj.go, the toolchain preamble, the go120ld header
|
||||||
// with its block offsets, the string table, the symbol definitions and the
|
// with its block offsets, the string table, the symbol definitions and the
|
||||||
// reloc/aux/data index arrays — with the loong64 preamble, the MinLC of 4
|
// reloc/aux/data index arrays, with the loong64 preamble, the MinLC of 4
|
||||||
// for the pc-value deltas, and R_LOONG64_ADDR_HI/LO relocation types for
|
// for the pc-value deltas, and R_LOONG64_ADDR_HI/LO relocation types for
|
||||||
// the pcalau12i+addi.d address pairs.
|
// the pcalau12i+addi.d address pairs.
|
||||||
func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
||||||
@@ -25,11 +25,16 @@ func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||||
// A pcalau12i+addi.d pair: the high part carries
|
// A pcalau12i+addi.d pair: the high part carries
|
||||||
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO.
|
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO; the guard's
|
||||||
if r.Kind == RelLoong64AddrLo {
|
// morestack call carries R_CALLLOONG64.
|
||||||
|
switch {
|
||||||
|
case r.Kind == RelLoong64AddrLo:
|
||||||
return relocLoong64AddrLo, 4
|
return relocLoong64AddrLo, 4
|
||||||
}
|
case r.Kind == RelLoong64Branch:
|
||||||
|
return relocCallLoong64, 4
|
||||||
|
default:
|
||||||
return relocLoong64AddrHi, 4
|
return relocLoong64AddrHi, 4
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +44,7 @@ func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
|||||||
const (
|
const (
|
||||||
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
|
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
|
||||||
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
|
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
|
||||||
|
relocCallLoong64 = 84 // R_CALLLOONG64
|
||||||
)
|
)
|
||||||
|
|
||||||
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
|
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
|
||||||
|
|||||||
+2
-2
@@ -13,9 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GOObjectRISCV emits a GOOBJ object file for RISC-V. The layout is the
|
// GOObjectRISCV emits a GOOBJ object file for RISC-V. The layout is the
|
||||||
// shared one in goobj.go — the toolchain preamble, the go120ld header with
|
// shared one in goobj.go, the toolchain preamble, the go120ld header with
|
||||||
// its block offsets, the string table, the symbol definitions and the
|
// its block offsets, the string table, the symbol definitions and the
|
||||||
// reloc/aux/data index arrays — with the RISC-V preamble, the MinLC of 2 for
|
// reloc/aux/data index arrays, with the RISC-V preamble, the MinLC of 2 for
|
||||||
// the pc-value deltas, and the single R_RISCV_PCREL_ITYPE/STYPE relocation
|
// the pc-value deltas, and the single R_RISCV_PCREL_ITYPE/STYPE relocation
|
||||||
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
|
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
|
||||||
// relocation, not the ELF HI20/LO12 pair).
|
// relocation, not the ELF HI20/LO12 pair).
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64,
|
||||||
|
// verified with go tool objdump): the stack-split guard classes, the morestack
|
||||||
|
// block and the auto-NOSPLIT leaf behaviour.
|
||||||
|
func TestStackGuardBytes(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
|
||||||
|
"554889e54883ec104883c4105dc3"},
|
||||||
|
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
|
||||||
|
"644c8b3425000000004c8da42478ffffff4d3b66107614554889e54881ec000100004881c4000100005dc3e800000000ebce"},
|
||||||
|
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
|
||||||
|
"644c8b3425000000004989e44981ec881f0000721a4d3b66107614554889e54881ec002000004881c4002000005dc3e800000000ebca"},
|
||||||
|
{"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
|
||||||
|
"644c8b342500000000493b66107613554889e54883ec10e8000000004883c4105dc3e800000000ebd7"},
|
||||||
|
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
|
||||||
|
"554889e54883ec104883c4105dc3"},
|
||||||
|
} {
|
||||||
|
f, errs := parser.Parse("g_amd64.s", tt.src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.name, errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
// The toolchain's object leaves every relocation field zero for the
|
||||||
|
// linker, while the gasm image resolves file-internal references, so
|
||||||
|
// the comparison masks the patch sites the way verify's ground truth
|
||||||
|
// does.
|
||||||
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||||
|
code[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got := hex.EncodeToString(code)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStackGuardRelocs checks the guard's patch sites: the TLS slot and the
|
||||||
|
// morestack call.
|
||||||
|
func TestStackGuardRelocs(t *testing.T) {
|
||||||
|
f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tRET\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble: %v", err)
|
||||||
|
}
|
||||||
|
relocs := img.Funcs[0].Relocs
|
||||||
|
if len(relocs) != 2 {
|
||||||
|
t.Fatalf("relocs = %d, want 2", len(relocs))
|
||||||
|
}
|
||||||
|
tls, call := relocs[0], relocs[1]
|
||||||
|
if tls.Kind != RelTLSLE || tls.Off != 5 || tls.Name != "" || tls.External {
|
||||||
|
t.Errorf("tls reloc = %+v, want RelTLSLE at 5 with no symbol", tls)
|
||||||
|
}
|
||||||
|
if call.Kind != RelCall || call.Name != "runtime\u00b7morestack_noctxt" || !call.External {
|
||||||
|
t.Errorf("call reloc = %+v, want RelCall to runtime.morestack_noctxt", call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStackGuardGOObj emissions succeed with the guard's TLS and builtin
|
||||||
|
// references in play.
|
||||||
|
func TestStackGuardGOObj(t *testing.T) {
|
||||||
|
f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tCALL \u00b7helper(SB)\n\tRET\nTEXT \u00b7helper(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble: %v", err)
|
||||||
|
}
|
||||||
|
obj, err := img.GOObject("testpkg", "g_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GOObject: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(obj, []byte("go120ld")) {
|
||||||
|
t.Fatal("object lacks the GOOBJ magic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The arm64 stack-split guard, pinned from `go tool asm` (Go 1.27, arm64):
|
||||||
|
// the guard classes, the auto-NOSPLIT leaf behaviour and the morestack
|
||||||
|
// block. Relocation fields are masked: the toolchain's object leaves them
|
||||||
|
// zero for the linker, the gasm image resolves file-internal references.
|
||||||
|
func TestStackGuardBytesARM64(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
|
||||||
|
"fe0f1ef8fd831ff8fd2300d1fd630091ff830091c0035fd6"},
|
||||||
|
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
|
||||||
|
"900b40f9f14302d13f0210eb09010054f44304d19dfa3fa99f020091fd2300d1fd230491ff430491c0035fd6e3031eaa00000000f3ffff17"},
|
||||||
|
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
|
||||||
|
"900b40f91bf283d2f1633beba30100543f0210eb690100541b0284d2f4633bcb9dfa3fa99f020091fd2300d11b0184d2fd633b8b1b0284d2ff633b8bc0035fd6e3031eaa00000000eeffff17"},
|
||||||
|
{"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
|
||||||
|
"900b40f9ff6330eb09010054fe0f1ef8fd831ff8fd2300d100000000fd835ff8fe0742f8c0035fd6e3031eaa00000000f4ffff17"},
|
||||||
|
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
|
||||||
|
"fe0f1ef8fd831ff8fd2300d1fd630091ff830091c0035fd6"},
|
||||||
|
} {
|
||||||
|
f, errs := parser.Parse("g_arm64.s", tt.src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.name, errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileARM64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||||
|
code[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got := hex.EncodeToString(code)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The riscv64 stack-split guard, pinned from `go tool asm` (Go 1.27,
|
||||||
|
// riscv64): the morestack call sits between the guard and the body, and the
|
||||||
|
// guard branches forward over it. Relocation fields are masked.
|
||||||
|
func TestStackGuardBytesRISCV64(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
|
||||||
|
"03b30d0163662300000000006ff05fff233411fe211106e08260610167800000"},
|
||||||
|
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
|
||||||
|
"03b30d01930381f763667300000000006ff01fff233c11ee130181ef06e082601301811067800000"},
|
||||||
|
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
|
||||||
|
"03b30d0189639b8383f863697100f97f9b8f8f07b303f10163667300000000006ff01ffef97f8a9f23bc1ffef97fe13f7e9106e08260896fa12f7e9167800000"},
|
||||||
|
{"frameless", "TEXT \u00b7frameless(SB), $0-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
|
||||||
|
"03b30d0163662300000000006ff05fff233c11fe611106e0000000008260210167800000"},
|
||||||
|
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
|
||||||
|
"233411fe211106e08260610167800000"},
|
||||||
|
} {
|
||||||
|
f, errs := parser.Parse("g_riscv64.s", tt.src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.name, errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileRISCV(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||||
|
code[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got := hex.EncodeToString(code)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loong64 stack-split guard, pinned from `go tool asm` (Go 1.27,
|
||||||
|
// loong64): every guard class (including the medium class with the
|
||||||
|
// materialised constant and the big class with the ORI-less constants), the
|
||||||
|
// auto-NOSPLIT leaf behaviour, the large-frame R30 prologue/epilogue forms
|
||||||
|
// and the morestack block. Relocation fields are masked.
|
||||||
|
func TestStackGuardBytesLOONG64(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
src string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n",
|
||||||
|
"61a0ff2963a0ff026100c0296360c0022000004c"},
|
||||||
|
{"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n",
|
||||||
|
"d442c02878e0fd0294e21200801a004061e0fb2963e0fb026100c0296320c4022000004c3f00150000000000ffd7ff53"},
|
||||||
|
{"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n",
|
||||||
|
"61a0ff2963a0ff026100c0296360c0022000004c"},
|
||||||
|
// The LR store leaves the 12-bit store-offset range while the SP
|
||||||
|
// adjust immediate still fits, and the epilogue adjusts through a
|
||||||
|
// single ORI.
|
||||||
|
{"fit2048", "TEXT \u00b7fit2048(SB), $2040-0\n\tRET\n",
|
||||||
|
"d442c0287800e20294e21200802600401e000014de8f1000c103e0296300e0026100c0291e00a00363f810002000004c3f00150000000000ffcbff53"},
|
||||||
|
// Medium class at the materialisation boundary (off = 2048 still
|
||||||
|
// immediate, 2049+ goes through R30).
|
||||||
|
{"med2048off", "TEXT \u00b7med2048off(SB), $2168-0\n\tRET\n",
|
||||||
|
"d442c0287800e00294e21200802e0040feffff15de8f1000c103de29feffff15de039e0363f810006100c0291e00a20363f810002000004c3f00150000000000ffc3ff53"},
|
||||||
|
{"medmat", "TEXT \u00b7medmat(SB), $2176-0\n\tRET\n",
|
||||||
|
"d442c028feffff15dee39f0378f8100094e21200802e0040feffff15de8f1000c1e3dd29feffff15dee39d0363f810006100c0291e20a20363f810002000004c3f00150000000000ffbbff53"},
|
||||||
|
// Big class with the rounding-split store and the floor-split adjust.
|
||||||
|
{"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n",
|
||||||
|
"d442c0283e000014de23be0378f8120000470044deffff15dee3810378f8100094e2120080320040deffff15de8f1000c1e3ff29beffff15dee3bf0363f810006100c0295e000014de23800363f810002000004c3f00150000000000ffa7ff53"},
|
||||||
|
// Zero low 12 bits drop the ORI from the store, the adjust and the
|
||||||
|
// epilogue materialisation.
|
||||||
|
{"bigzero", "TEXT \u00b7bigzero(SB), $4088-0\n\tRET\n",
|
||||||
|
"d442c028feffff15de03820378f8100094e21200802a0040feffff15de8f1000c103c029feffff1563f810006100c0293e00001463f810002000004c3f00150000000000ffbfff53"},
|
||||||
|
// Big class whose first constant has a zero high part: a single ORI.
|
||||||
|
{"big3976", "TEXT \u00b7big3976(SB), $4096-0\n\tRET\n",
|
||||||
|
"d442c0281e20be0378f8120000470044feffff15dee3810378f8100094e2120080320040feffff15de8f1000c1e3ff29deffff15dee3bf0363f810006100c0293e000014de23800363f810002000004c3f00150000000000ffabff53"},
|
||||||
|
// Big class at a multiple of 4096: both guard constants lose their
|
||||||
|
// ORI word.
|
||||||
|
{"giantlo0", "TEXT \u00b7giantlo0(SB), $4216-0\n\tRET\n",
|
||||||
|
"d442c0283e00001478f8120000430044feffff1578f8100094e2120080320040feffff15de8f1000c103fe29deffff15de03be0363f810006100c0293e000014de03820363f810002000004c3f00150000000000ffafff53"},
|
||||||
|
// Non-leaf big frame: the body call plus the LR restore epilogue.
|
||||||
|
{"callbig", "TEXT \u00b7callbig(SB), $8192-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n",
|
||||||
|
"d442c0283e000014de23be0378f81200004f0044deffff15dee3810378f8100094e21200803a0040deffff15de8f1000c1e3ff29beffff15dee3bf0363f810006100c029000000006100c0285e000014de23800363f810002000004c3f00150000000000ff9fff53"},
|
||||||
|
} {
|
||||||
|
f, errs := parser.Parse("g_loong64.s", tt.src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.name, errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileLOONG64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||||
|
code[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got := hex.EncodeToString(code)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStackGuardGOObjInternalCall checks that GOOBJ emission succeeds when a
|
||||||
|
// guarded function calls a TEXT symbol of the same file, for every arch's
|
||||||
|
// call relocation kind.
|
||||||
|
func TestStackGuardGOObjInternalCall(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
src string
|
||||||
|
assemble func(*ast.File) (*Image, error)
|
||||||
|
}{
|
||||||
|
{"g_amd64.s", AssembleFile},
|
||||||
|
{"g_arm64.s", AssembleFileARM64},
|
||||||
|
{"g_riscv64.s", AssembleFileRISCV},
|
||||||
|
{"g_loong64.s", AssembleFileLOONG64},
|
||||||
|
} {
|
||||||
|
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.src, errs)
|
||||||
|
}
|
||||||
|
img, err := tt.assemble(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.src, err)
|
||||||
|
}
|
||||||
|
if _, err := img.GOObject("testpkg", tt.src); err != nil {
|
||||||
|
t.Errorf("%s: GOObject: %v", tt.src, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-14
@@ -21,7 +21,7 @@ var aluOp = map[string]struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// unaryOp maps INC/DEC/NEG/NOT to their /digit and base opcode. INC/DEC use
|
// unaryOp maps INC/DEC/NEG/NOT to their /digit and base opcode. INC/DEC use
|
||||||
// the 0xFE/0xFF group (the short 0x40–0x4F forms are REX prefixes in 64-bit
|
// the 0xFE/0xFF group (the short 0x40-0x4F forms are REX prefixes in 64-bit
|
||||||
// mode); NEG/NOT use the 0xF6/0xF7 group.
|
// mode); NEG/NOT use the 0xF6/0xF7 group.
|
||||||
var unaryOp = map[string]struct {
|
var unaryOp = map[string]struct {
|
||||||
digit int
|
digit int
|
||||||
@@ -33,7 +33,7 @@ var unaryOp = map[string]struct {
|
|||||||
"NEG": {3, 0xF7},
|
"NEG": {3, 0xF7},
|
||||||
}
|
}
|
||||||
|
|
||||||
// shiftOp maps SHL/SHR/SAR to their /digit in the 0xC0/0xC1/0xD0–0xD3 group.
|
// shiftOp maps SHL/SHR/SAR to their /digit in the 0xC0/0xC1/0xD0-0xD3 group.
|
||||||
var shiftOp = map[string]int{
|
var shiftOp = map[string]int{
|
||||||
"SHL": 4,
|
"SHL": 4,
|
||||||
"SHR": 5,
|
"SHR": 5,
|
||||||
@@ -50,7 +50,7 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
|||||||
|
|
||||||
// Integer scalar XMM moves: MOVQ with an XMM operand is the SSE2
|
// Integer scalar XMM moves: MOVQ with an XMM operand is the SSE2
|
||||||
// packed-quadword move, NOT a GPR move: mem→xmm encodes as F3 0F 7E
|
// packed-quadword move, NOT a GPR move: mem→xmm encodes as F3 0F 7E
|
||||||
// (reg = dst, no REX.W — the Go assembler's form), xmm→mem as
|
// (reg = dst, no REX.W, the Go assembler's form), xmm→mem as
|
||||||
// 66 0F D6 (rm = xmm). Register forms against a GPR use the MOVD
|
// 66 0F D6 (rm = xmm). Register forms against a GPR use the MOVD
|
||||||
// opcodes with REX.W instead: 66 REX.W 0F 6E (gpr→xmm) and
|
// opcodes with REX.W instead: 66 REX.W 0F 6E (gpr→xmm) and
|
||||||
// 66 REX.W 0F 7E (xmm→gpr); the memory opcodes with a register r/m
|
// 66 REX.W 0F 7E (xmm→gpr); the memory opcodes with a register r/m
|
||||||
@@ -103,7 +103,7 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
|||||||
switch src := src.(type) {
|
switch src := src.(type) {
|
||||||
case Reg:
|
case Reg:
|
||||||
if dstIsReg {
|
if dstIsReg {
|
||||||
// MOV r/m, r: 0x88/0x89, reg=src, rm=dst — the form the Go
|
// MOV r/m, r: 0x88/0x89, reg=src, rm=dst, the form the Go
|
||||||
// assembler emits for register-to-register moves.
|
// assembler emits for register-to-register moves.
|
||||||
i := newInstr(size, []byte{movRM(size)})
|
i := newInstr(size, []byte{movRM(size)})
|
||||||
if err := setRM(i, src, dst, size); err != nil {
|
if err := setRM(i, src, dst, size); err != nil {
|
||||||
@@ -147,7 +147,7 @@ func (e *enc) encodeMov(ops []Operand, size int) error {
|
|||||||
// a signed int32, choosing per sign:
|
// a signed int32, choosing per sign:
|
||||||
// v >= 0: B8+rd imm32 without REX.W (zero-extended by the
|
// v >= 0: B8+rd imm32 without REX.W (zero-extended by the
|
||||||
// hardware, REX.B still emitted for R8-R15);
|
// hardware, REX.B still emitted for R8-R15);
|
||||||
// v < 0: REX.W C7 /0 imm32 (sign-extended — the plain B8+rd
|
// v < 0: REX.W C7 /0 imm32 (sign-extended, the plain B8+rd
|
||||||
// form would zero-extend and corrupt the value).
|
// form would zero-extend and corrupt the value).
|
||||||
// Out-of-range immediates keep the B8+rd imm64 form.
|
// Out-of-range immediates keep the B8+rd imm64 form.
|
||||||
if size == 8 && v >= 0 && v <= (1<<31)-1 {
|
if size == 8 && v >= 0 && v <= (1<<31)-1 {
|
||||||
@@ -226,8 +226,8 @@ func (e *enc) encodeALU(op struct {
|
|||||||
return e.encodeALUImm(op.digit, dst, int64(imm), size)
|
return e.encodeALUImm(op.digit, dst, int64(imm), size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CMP accepts the immediate in the second position too — CMPL CX, $31 is
|
// CMP accepts the immediate in the second position too, CMPL CX, $31 is
|
||||||
// the form the Go assembler itself accepts — and encodes it identically
|
// the form the Go assembler itself accepts, and encodes it identically
|
||||||
// (CMP r/m, imm sets the flags as first − second). No other ALU op takes
|
// (CMP r/m, imm sets the flags as first − second). No other ALU op takes
|
||||||
// an immediate destination.
|
// an immediate destination.
|
||||||
if imm, ok := dst.(Imm); ok {
|
if imm, ok := dst.(Imm); ok {
|
||||||
@@ -313,7 +313,7 @@ func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
|
|||||||
i.imm = []byte{byte(int8(imm))}
|
i.imm = []byte{byte(int8(imm))}
|
||||||
return e.emit(i)
|
return e.emit(i)
|
||||||
}
|
}
|
||||||
// 0x81 /digit, imm16/imm32 — or the Go assembler's accumulator short
|
// 0x81 /digit, imm16/imm32, or the Go assembler's accumulator short
|
||||||
// form (opcode+5, no ModR/M) when the destination is AX/AL, which it
|
// form (opcode+5, no ModR/M) when the destination is AX/AL, which it
|
||||||
// prefers over the generic form exactly here.
|
// prefers over the generic form exactly here.
|
||||||
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
||||||
@@ -339,7 +339,7 @@ func (e *enc) encodeTest(ops []Operand, size int) error {
|
|||||||
}
|
}
|
||||||
src, dst := ops[0], ops[1]
|
src, dst := ops[0], ops[1]
|
||||||
if imm, ok := src.(Imm); ok {
|
if imm, ok := src.(Imm); ok {
|
||||||
// TEST r/m, imm: 0xF6 (8-bit) / 0xF7 /0 — but the Go assembler
|
// TEST r/m, imm: 0xF6 (8-bit) / 0xF7 /0, but the Go assembler
|
||||||
// always uses the accumulator forms (A8/A9, no ModR/M) when the
|
// always uses the accumulator forms (A8/A9, no ModR/M) when the
|
||||||
// register operand is AL/AX, whatever the immediate's width.
|
// register operand is AL/AX, whatever the immediate's width.
|
||||||
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
if r, ok := dst.(Reg); ok && r.idx == 0 {
|
||||||
@@ -575,6 +575,24 @@ func (e *enc) encodeJmpRel(ops []Operand, opcode []byte) error {
|
|||||||
return e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
return e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeIndirectBranch encodes JMP/CALL through a register or memory operand:
|
||||||
|
// FF /4 for JMP, FF /2 for CALL. The operand size is fixed at 64 bits in
|
||||||
|
// 64-bit mode, so no REX.W is emitted; a REX appears only for R8-R15 bases.
|
||||||
|
func (e *enc) encodeIndirectBranch(mnem string, ops []Operand) error {
|
||||||
|
if len(ops) != 1 {
|
||||||
|
return fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||||
|
}
|
||||||
|
digit := 4 // JMP r/m64
|
||||||
|
if mnem == "CALL" {
|
||||||
|
digit = 2 // CALL r/m64
|
||||||
|
}
|
||||||
|
i := &instr{opcode: []byte{0xFF}, modrm: -1, sib: -1}
|
||||||
|
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.emit(i)
|
||||||
|
}
|
||||||
|
|
||||||
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
||||||
func condCode(upper string) (int, bool) {
|
func condCode(upper string) (int, bool) {
|
||||||
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
||||||
@@ -678,7 +696,7 @@ func (e *enc) encodeCmov(upper string, ops []Operand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeSet encodes a conditional byte set: SET + condition (SETNE, SETEQ, …),
|
// encodeSet encodes a conditional byte set: SET + condition (SETNE, SETEQ, …),
|
||||||
// always a byte write — 0F 90+cc /0 into a register or memory operand.
|
// always a byte write, 0F 90+cc /0 into a register or memory operand.
|
||||||
func (e *enc) encodeSet(upper string, ops []Operand) error {
|
func (e *enc) encodeSet(upper string, ops []Operand) error {
|
||||||
if len(ops) != 1 {
|
if len(ops) != 1 {
|
||||||
return fmt.Errorf("SETcc expects 1 operand, got %d", len(ops))
|
return fmt.Errorf("SETcc expects 1 operand, got %d", len(ops))
|
||||||
@@ -711,8 +729,8 @@ var countOp = map[string]struct {
|
|||||||
"POPCNT": {0xB8, 0xF3},
|
"POPCNT": {0xB8, 0xF3},
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeCount encodes the bit-scan and bit-count family — BSF (0F BC),
|
// encodeCount encodes the bit-scan and bit-count family, BSF (0F BC),
|
||||||
// BSR (0F BD), TZCNT (F3 0F BC), LZCNT (F3 0F BD) and POPCNT (F3 0F B8) —
|
// BSR (0F BD), TZCNT (F3 0F BC), LZCNT (F3 0F BD) and POPCNT (F3 0F B8)
|
||||||
// with reg = dst and rm = src. The size suffix selects the operand width
|
// with reg = dst and rm = src. The size suffix selects the operand width
|
||||||
// (BSFQ, TZCNTL, …). Note BSF/BSR leave the destination undefined when the
|
// (BSFQ, TZCNTL, …). Note BSF/BSR leave the destination undefined when the
|
||||||
// source is zero (unlike their F3-prefixed counterparts); callers must
|
// source is zero (unlike their F3-prefixed counterparts); callers must
|
||||||
@@ -801,8 +819,8 @@ type sseMove struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sseMoveTable = map[string]sseMove{
|
var sseMoveTable = map[string]sseMove{
|
||||||
"MOVOU": {0xF3, 0x6F, 0x7F}, // MOVDQU — unaligned octa
|
"MOVOU": {0xF3, 0x6F, 0x7F}, // MOVDQU, unaligned octa
|
||||||
"MOVO": {0x66, 0x6F, 0x7F}, // MOVDQA — aligned octa
|
"MOVO": {0x66, 0x6F, 0x7F}, // MOVDQA, aligned octa
|
||||||
"MOVUPS": {0x00, 0x10, 0x11}, // unaligned packed single
|
"MOVUPS": {0x00, 0x10, 0x11}, // unaligned packed single
|
||||||
"MOVAPS": {0x00, 0x28, 0x29}, // aligned packed single
|
"MOVAPS": {0x00, 0x28, 0x29}, // aligned packed single
|
||||||
"MOVUPD": {0x66, 0x10, 0x11}, // unaligned packed double
|
"MOVUPD": {0x66, 0x10, 0x11}, // unaligned packed double
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import (
|
|||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel;
|
||||||
// all functions plus the file-local mask24 constant — and checks that every
|
// all functions plus the file-local mask24 constant; and checks that every
|
||||||
// static-symbol load resolves to the right bytes in the image.
|
// static-symbol load resolves to the right bytes in the image.
|
||||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||||
@@ -81,7 +81,7 @@ func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
// kernel, all functions plus the file-global idx16 constant, and checks
|
||||||
// that the static-symbol load resolves to the right bytes in the image.
|
// that the static-symbol load resolves to the right bytes in the image.
|
||||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ DATA ·table<>+0(SB)/8, $0x1122334455667788
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The debug_line program: LNE_set_address (the R_ADDR relocation
|
// The debug_line program: LNE_set_address (the R_ADDR relocation
|
||||||
// carries the function address), then one row per line change — the
|
// carries the function address), then one row per line change; the
|
||||||
// TEXT is on line 4 (a leading blank line precedes the include), the
|
// TEXT is on line 4 (a leading blank line precedes the include), the
|
||||||
// instructions on lines 5–9 — an advance to the 20-byte end and an
|
// instructions on lines 5-9; an advance to the 20-byte end and an
|
||||||
// end-of-sequence.
|
// end-of-sequence.
|
||||||
linesOff := le.Uint32(dataIdx[4*2:])
|
linesOff := le.Uint32(dataIdx[4*2:])
|
||||||
lines := dataBlk[linesOff : linesOff+21]
|
lines := dataBlk[linesOff : linesOff+21]
|
||||||
|
|||||||
+55
-22
@@ -6,6 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
)
|
)
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
// file-local static symbols are encoded RIP-relative and resolved within the
|
// file-local static symbols are encoded RIP-relative and resolved within the
|
||||||
// image, so the raw bytes are self-consistent and executable at any base
|
// image, so the raw bytes are self-consistent and executable at any base
|
||||||
// address; references to external symbols are recorded as relocations
|
// address; references to external symbols are recorded as relocations
|
||||||
// (Funcs[i].Relocs, Externals) and left unresolved — the object-file
|
// (Funcs[i].Relocs, Externals) and left unresolved, the object-file
|
||||||
// emitters turn them into linker relocations.
|
// emitters turn them into linker relocations.
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Code []byte // concatenated function bodies
|
Code []byte // concatenated function bodies
|
||||||
@@ -90,14 +91,18 @@ type RelocKind int
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
||||||
|
RelCall // R_CALL: CALL to a function symbol (amd64)
|
||||||
|
RelTLSLE // R_TLS_LE: local-exec TLS load, no symbol (amd64 guard)
|
||||||
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
||||||
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
||||||
RelRISCVJal // R_RISCV_JAL (J-type call)
|
RelRISCVJal // R_RISCV_JAL (J-type call)
|
||||||
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
||||||
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
|
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
|
||||||
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
|
RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st)
|
||||||
RelArm64Addr // R_ADDRARM64 (ADRP + ADD/LDR/STR pair)
|
RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair)
|
||||||
RelArm64Branch // R_CALLARM64 (BL instruction)
|
RelArm64Branch // R_CALLARM64 (BL instruction)
|
||||||
|
RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair)
|
||||||
|
RelLoong64Branch // R_CALLLOONG64 (BL instruction)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reloc struct {
|
type Reloc struct {
|
||||||
@@ -132,7 +137,7 @@ func (img *Image) Bytes() []byte {
|
|||||||
// reference to a file-local static symbol becomes a RIP-relative load whose
|
// reference to a file-local static symbol becomes a RIP-relative load whose
|
||||||
// displacement is resolved against that layout; a reference to a symbol no
|
// displacement is resolved against that layout; a reference to a symbol no
|
||||||
// GLOBL defines is recorded as an external relocation (Externals) with its
|
// GLOBL defines is recorded as an external relocation (Externals) with its
|
||||||
// displacement left zero — the object-file emitters resolve it at link
|
// displacement left zero, the object-file emitters resolve it at link
|
||||||
// time, while the raw image (Bytes) cannot represent it.
|
// time, while the raw image (Bytes) cannot represent it.
|
||||||
func AssembleFile(f *ast.File) (*Image, error) {
|
func AssembleFile(f *ast.File) (*Image, error) {
|
||||||
dataSyms, err := collectData(f)
|
dataSyms, err := collectData(f)
|
||||||
@@ -146,6 +151,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
link := &linkInfo{symbols: known, allowExternal: true}
|
link := &linkInfo{symbols: known, allowExternal: true}
|
||||||
|
|
||||||
img := &Image{Symbols: map[string]int{}}
|
img := &Image{Symbols: map[string]int{}}
|
||||||
|
textOff := map[string]int{}
|
||||||
type asmFunc struct {
|
type asmFunc struct {
|
||||||
name string
|
name string
|
||||||
patches []sbPatch
|
patches []sbPatch
|
||||||
@@ -183,6 +189,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
||||||
}
|
}
|
||||||
|
textOff[t.Name.Name] = len(img.Code)
|
||||||
img.Funcs = append(img.Funcs, fl)
|
img.Funcs = append(img.Funcs, fl)
|
||||||
img.Code = append(img.Code, code...)
|
img.Code = append(img.Code, code...)
|
||||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||||
@@ -215,13 +222,27 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
base := img.Funcs[i].Offset
|
base := img.Funcs[i].Offset
|
||||||
code := img.Code[base : base+img.Funcs[i].Size]
|
code := img.Code[base : base+img.Funcs[i].Size]
|
||||||
for _, p := range fn.patches {
|
for _, p := range fn.patches {
|
||||||
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend}
|
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend, Kind: p.kind}
|
||||||
|
if p.kind == RelTLSLE {
|
||||||
|
// The TLS slot has no symbol: the linker fills the offset
|
||||||
|
// from the runtime's TLS layout.
|
||||||
|
img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if imgOff, ok := img.Symbols[p.name]; ok {
|
if imgOff, ok := img.Symbols[p.name]; ok {
|
||||||
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||||
if rel < -1<<31 || rel >= 1<<31 {
|
if rel < -1<<31 || rel >= 1<<31 {
|
||||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
||||||
}
|
}
|
||||||
copy(code[p.off:p.off+4], le32(rel))
|
copy(code[p.off:p.off+4], le32(rel))
|
||||||
|
} else if imgOff, ok := textOff[p.name]; ok {
|
||||||
|
// A CALL to a TEXT function of the same file: resolve the
|
||||||
|
// displacement against the function's layout position.
|
||||||
|
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||||
|
if rel < -1<<31 || rel >= 1<<31 {
|
||||||
|
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
||||||
|
}
|
||||||
|
copy(code[p.off:p.off+4], le32(rel))
|
||||||
} else {
|
} else {
|
||||||
reloc.External = true
|
reloc.External = true
|
||||||
externals[p.name] = true
|
externals[p.name] = true
|
||||||
@@ -419,50 +440,63 @@ type dataSym struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectData gathers the file's static symbols (GLOBL) and their initial
|
// collectData gathers the file's static symbols (GLOBL) and their initial
|
||||||
// contents (DATA) into byte buffers, in declaration order.
|
// contents (DATA) into byte buffers. Two passes: the Plan 9 convention puts
|
||||||
|
// every DATA line before its symbol's GLOBL, so the symbols are registered
|
||||||
|
// before the initialisers are applied.
|
||||||
func collectData(f *ast.File) ([]dataSym, error) {
|
func collectData(f *ast.File) ([]dataSym, error) {
|
||||||
index := map[string]int{}
|
index := map[string]int{}
|
||||||
var syms []dataSym
|
var syms []dataSym
|
||||||
for _, d := range f.Decls {
|
for _, d := range f.Decls {
|
||||||
switch dd := d.(type) {
|
gd, ok := d.(*ast.Globl)
|
||||||
case *ast.Globl:
|
if !ok {
|
||||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := dd.Name.Name
|
if gd.Name == nil || gd.Name.Pseudo != "SB" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := gd.Name.Name
|
||||||
if _, dup := index[name]; dup {
|
if _, dup := index[name]; dup {
|
||||||
return nil, fmt.Errorf("duplicate GLOBL %q", name)
|
return nil, fmt.Errorf("duplicate GLOBL %q", name)
|
||||||
}
|
}
|
||||||
size := 0
|
size := 0
|
||||||
if dd.Size != nil && dd.Size.Imm.HasVal {
|
if gd.Size != nil && gd.Size.Imm.HasVal {
|
||||||
size = int(dd.Size.Imm.Val)
|
size = int(gd.Size.Imm.Val)
|
||||||
}
|
}
|
||||||
index[name] = len(syms)
|
index[name] = len(syms)
|
||||||
ds := dataSym{
|
ds := dataSym{
|
||||||
name: name,
|
name: name,
|
||||||
pkg: dd.Name.Pkg,
|
pkg: gd.Name.Pkg,
|
||||||
buf: make([]byte, size),
|
buf: make([]byte, size),
|
||||||
size: size,
|
size: size,
|
||||||
static: dd.Name.Static,
|
static: gd.Name.Static,
|
||||||
}
|
}
|
||||||
for _, f := range dd.Flags {
|
for _, f := range gd.Flags {
|
||||||
switch f {
|
switch f {
|
||||||
case "RODATA":
|
case "RODATA":
|
||||||
ds.rodata = true
|
ds.rodata = true
|
||||||
case "DUPOK":
|
case "DUPOK":
|
||||||
ds.dupok = true
|
ds.dupok = true
|
||||||
case "1":
|
default:
|
||||||
ds.dupok = true
|
// Legacy numeric flag constants (runtime/textflag.h):
|
||||||
case "8":
|
// DUPOK is 2, RODATA is 8; combinations arrive as one
|
||||||
ds.rodata = true
|
// number (e.g. 10 = RODATA|DUPOK).
|
||||||
case "9":
|
if n, err := strconv.Atoi(f); err == nil {
|
||||||
|
if n&2 != 0 {
|
||||||
ds.dupok = true
|
ds.dupok = true
|
||||||
|
}
|
||||||
|
if n&8 != 0 {
|
||||||
ds.rodata = true
|
ds.rodata = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
syms = append(syms, ds)
|
syms = append(syms, ds)
|
||||||
|
}
|
||||||
case *ast.Data:
|
for _, d := range f.Decls {
|
||||||
|
dd, ok := d.(*ast.Data)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -492,7 +526,6 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
|||||||
buf[off+int64(j)] = byte(v >> (8 * j))
|
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return syms, nil
|
return syms, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-2
@@ -10,8 +10,8 @@ import (
|
|||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAssembleFileStaticData checks the whole-image layout — code, padding
|
// TestAssembleFileStaticData checks the whole-image layout; code, padding
|
||||||
// and the data section — and that the RIP-relative displacements of static
|
// and the data section; and that the RIP-relative displacements of static
|
||||||
// symbol loads resolve to the right bytes.
|
// symbol loads resolve to the right bytes.
|
||||||
func TestAssembleFileStaticData(t *testing.T) {
|
func TestAssembleFileStaticData(t *testing.T) {
|
||||||
f, errs := parser.Parse("d_amd64.s", `
|
f, errs := parser.Parse("d_amd64.s", `
|
||||||
@@ -128,3 +128,41 @@ DATA x<>+0(SB)/4, $1
|
|||||||
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCollectDataNumericFlags pins the numeric GLOBL flag constants from
|
||||||
|
// runtime/textflag.h: DUPOK is 2, RODATA is 8, and combinations arrive as
|
||||||
|
// one number (9 = NOPROF|RODATA, 10 = RODATA|DUPOK).
|
||||||
|
func TestCollectDataNumericFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
flags string
|
||||||
|
rodata bool
|
||||||
|
dupok bool
|
||||||
|
}{
|
||||||
|
{"2", false, true},
|
||||||
|
{"8", true, false},
|
||||||
|
{"9", true, false}, // NOPROF|RODATA, not DUPOK
|
||||||
|
{"10", true, true}, // RODATA|DUPOK
|
||||||
|
{"RODATA", true, false},
|
||||||
|
{"DUPOK", false, true},
|
||||||
|
{"RODATA|DUPOK", true, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
src := "TEXT \u00b7f(SB), NOSPLIT, $0\n\tRET\nGLOBL sym(SB), " + tt.flags + ", $8\n"
|
||||||
|
f, errs := parser.Parse("f_amd64.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse %q: %v", tt.flags, errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble %q: %v", tt.flags, err)
|
||||||
|
}
|
||||||
|
if len(img.DataSyms) != 1 {
|
||||||
|
t.Fatalf("%q: data syms = %d, want 1", tt.flags, len(img.DataSyms))
|
||||||
|
}
|
||||||
|
d := img.DataSyms[0]
|
||||||
|
if d.Rodata != tt.rodata || d.Dupok != tt.dupok {
|
||||||
|
t.Errorf("flags %q: rodata=%v dupok=%v, want rodata=%v dupok=%v",
|
||||||
|
tt.flags, d.Rodata, d.Dupok, tt.rodata, tt.dupok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+110
-28
@@ -6,6 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
@@ -13,17 +14,19 @@ import (
|
|||||||
|
|
||||||
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
|
// assembleLOONG64 assembles a LoongArch (loong64) TEXT function body into
|
||||||
// machine code. Every instruction is 4 bytes; the MOV pseudo-instruction and
|
// machine code. Every instruction is 4 bytes; the MOV pseudo-instruction and
|
||||||
// the immediate-arithmetic forms expand to 2–5 instructions when the
|
// the immediate-arithmetic forms expand to 2-5 instructions when the
|
||||||
// immediate does not fit, so the layout is computed in two passes (sizes,
|
// immediate does not fit, so the layout is computed in two passes (sizes,
|
||||||
// then encoding with resolved branch targets).
|
// then encoding with resolved branch targets).
|
||||||
//
|
//
|
||||||
// The emitted bytes match the Go toolchain's loong64 assembler, which is the
|
// The emitted bytes match the Go toolchain's loong64 assembler, which is the
|
||||||
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
|
// ground-truth oracle: prologue/epilogue (including the large-frame R30
|
||||||
// encodings and the MOV immediate expansions all follow cmd/internal/obj/
|
// materialisations), FP/SP frame mapping, the stack-split guard classes, and
|
||||||
// loong64's asmout cases.
|
// branch encodings all follow cmd/internal/obj/loong64. The morestack block
|
||||||
|
// at the end of split functions carries the runtime.morestack_noctxt call.
|
||||||
func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
||||||
fi := loong64ComputeFrame(t)
|
fi := loong64ComputeFrame(t)
|
||||||
prologue := loong64Prologue(fi)
|
prologue := loong64Prologue(fi)
|
||||||
|
guardLen := loong64GuardLen(fi)
|
||||||
chain := loong64JumpChain(t)
|
chain := loong64JumpChain(t)
|
||||||
resolve := func(name string) string {
|
resolve := func(name string) string {
|
||||||
if r, ok := chain[name]; ok {
|
if r, ok := chain[name]; ok {
|
||||||
@@ -35,16 +38,17 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
|
|||||||
var relocs []Reloc
|
var relocs []Reloc
|
||||||
var spadj []SpadjStep
|
var spadj []SpadjStep
|
||||||
|
|
||||||
// The prologue (3 instructions when a frame is present) raises the SP
|
// The prologue raises the SP delta by autosize; the boundary is reported
|
||||||
// delta by autosize; the boundary is reported at the third instruction's
|
// after the SP adjust instruction, exactly as the toolchain's pctospadj
|
||||||
// pc, exactly as the toolchain's pctospadj does.
|
// does. The prologue (3 instructions when a frame is present) may
|
||||||
|
// materialise its store or adjust through R30, which widens it.
|
||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
spadj = append(spadj, SpadjStep{PC: 8, Value: fi.autosize})
|
spadj = append(spadj, SpadjStep{PC: guardLen + (loong64StoreWords(fi.autosize)+loong64AdjustWords(-int64(fi.autosize)))*4, Value: fi.autosize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: label offsets from the instruction sizes.
|
// Pass 1: label offsets from the instruction sizes.
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pos := len(prologue)
|
pos := guardLen + len(prologue)
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -54,9 +58,25 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: encode. Relocation offsets are recorded function-relative.
|
// Pass 2: encode. The guard prefix precedes the prologue; its branches
|
||||||
out := append([]byte(nil), prologue...)
|
// target the morestack block at the end of the function, which the first
|
||||||
pc := len(prologue)
|
// pass has sized.
|
||||||
|
bodyLen := 0
|
||||||
|
{
|
||||||
|
p := guardLen + len(prologue)
|
||||||
|
for _, stmt := range t.Body {
|
||||||
|
if in, ok := stmt.(*ast.Instr); ok {
|
||||||
|
p += loong64InstrSize(in, fi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyLen = p - (guardLen + len(prologue))
|
||||||
|
}
|
||||||
|
var out []byte
|
||||||
|
if fi.needSplit {
|
||||||
|
out = append(out, loong64GuardBytes(fi, guardLen+len(prologue)+bodyLen)...)
|
||||||
|
}
|
||||||
|
out = append(out, prologue...)
|
||||||
|
pc := guardLen + len(prologue)
|
||||||
preCount := len(relocs)
|
preCount := len(relocs)
|
||||||
var lines []LineEntry
|
var lines []LineEntry
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
@@ -69,23 +89,29 @@ func assembleLOONG64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry,
|
|||||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
|
||||||
}
|
}
|
||||||
for j := preCount; j < len(relocs); j++ {
|
for j := preCount; j < len(relocs); j++ {
|
||||||
relocs[j].Off += pc - len(prologue)
|
// Make the relocation offsets function-relative: each instruction
|
||||||
|
// records its reloc offset relative to its own start, and pc is
|
||||||
|
// that instruction's offset from the function start (prologue
|
||||||
|
// included). After shifts by the same amount.
|
||||||
|
relocs[j].Off += pc
|
||||||
|
relocs[j].After += pc
|
||||||
}
|
}
|
||||||
preCount = len(relocs)
|
preCount = len(relocs)
|
||||||
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
|
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
|
||||||
// The RET's epilogue closes the frame: the SP delta returns to zero
|
// The RET's epilogue closes the frame: the SP delta returns to zero
|
||||||
// after the addi.d (one instruction for a leaf, two for a non-leaf
|
// after the frame-deallocating ADDV.
|
||||||
// with the LR restore).
|
|
||||||
if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 {
|
if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 {
|
||||||
epi := 4
|
spadj = append(spadj, SpadjStep{PC: pc + loong64EpilogueWords(fi)*4, Value: 0})
|
||||||
if !fi.leaf {
|
|
||||||
epi = 8
|
|
||||||
}
|
|
||||||
spadj = append(spadj, SpadjStep{PC: pc + epi, Value: 0})
|
|
||||||
}
|
}
|
||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pc += len(code)
|
pc += len(code)
|
||||||
}
|
}
|
||||||
|
if fi.needSplit {
|
||||||
|
block, blReloc := loong64MoreStackBlock(pc)
|
||||||
|
out = append(out, block...)
|
||||||
|
relocs = append(relocs, blReloc)
|
||||||
|
pc += len(block)
|
||||||
|
}
|
||||||
return out, offsets, relocs, lines, spadj, nil
|
return out, offsets, relocs, lines, spadj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,15 +250,18 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
|||||||
}
|
}
|
||||||
return l64wordLE(uint32(immFromOperand(ops[0]))), nil
|
return l64wordLE(uint32(immFromOperand(ops[0]))), nil
|
||||||
case "JMP", "B":
|
case "JMP", "B":
|
||||||
return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve)
|
return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve, relocs)
|
||||||
case "JAL", "CALL", "BL":
|
case "JAL", "CALL", "BL":
|
||||||
return encodeLOONG64Branch(instr, mnem, pc, offsets, true, resolve)
|
return encodeLOONG64Branch(instr, mnem, pc, offsets, true, resolve, relocs)
|
||||||
case "MOV", "MOVB", "MOVH", "MOVW", "MOVV", "MOVBU", "MOVHU", "MOVWU", "MOVF", "MOVD":
|
case "MOV", "MOVB", "MOVH", "MOVW", "MOVV", "MOVBU", "MOVHU", "MOVWU", "MOVF", "MOVD":
|
||||||
return encodeLOONG64Mov(instr, mnem, fi, relocs)
|
return encodeLOONG64Mov(instr, mnem, fi, relocs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
|
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
|
||||||
if op, ok := l64branchTable[mnem]; ok {
|
if op, ok := l64branchTable[mnem]; ok {
|
||||||
|
if mnem == "JIRL" {
|
||||||
|
return encodeLOONG64Jirl(op, ops)
|
||||||
|
}
|
||||||
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
||||||
}
|
}
|
||||||
// Single-register branches with 21-bit offsets (BLTZ/BGEZ/BLEZ/BGTZ,
|
// Single-register branches with 21-bit offsets (BLTZ/BGEZ/BLEZ/BGTZ,
|
||||||
@@ -417,7 +446,7 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
|||||||
|
|
||||||
case l64Firrr:
|
case l64Firrr:
|
||||||
// ALSL: INSTR $sa, rj, rk, rd (the toolchain's optab places rj in
|
// ALSL: INSTR $sa, rj, rk, rd (the toolchain's optab places rj in
|
||||||
// the second register position); the source amount is 1–4, encoded
|
// the second register position); the source amount is 1-4, encoded
|
||||||
// as sa-1.
|
// as sa-1.
|
||||||
if len(ops) != 4 {
|
if len(ops) != 4 {
|
||||||
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
||||||
@@ -485,7 +514,7 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
|||||||
//
|
//
|
||||||
// JMP/B label → b label JMP/B (rj) → jirl r0, rj, 0
|
// JMP/B label → b label JMP/B (rj) → jirl r0, rj, 0
|
||||||
// JAL/CALL/BL label → bl label JAL/CALL/BL (rj) → jirl r1, rj, 0
|
// JAL/CALL/BL label → bl label JAL/CALL/BL (rj) → jirl r1, rj, 0
|
||||||
func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string) ([]byte, error) {
|
func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string, relocs *[]Reloc) ([]byte, error) {
|
||||||
if len(instr.Operands) != 1 {
|
if len(instr.Operands) != 1 {
|
||||||
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(instr.Operands))
|
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(instr.Operands))
|
||||||
}
|
}
|
||||||
@@ -502,6 +531,19 @@ func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[stri
|
|||||||
}
|
}
|
||||||
return l64wordLE(l64irr16(l64branchTable["JIRL"], 0, rj, rd)), nil
|
return l64wordLE(l64irr16(l64branchTable["JIRL"], 0, rj, rd)), nil
|
||||||
}
|
}
|
||||||
|
// Direct symbol: sym+off(SB) → b/bl with an R_CALLLOONG64 relocation
|
||||||
|
// (the linker fills the offset), as the toolchain does for CALL/BL/JAL
|
||||||
|
// and for tail-calling JMP.
|
||||||
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
||||||
|
opc := l64jumpTable["B"]
|
||||||
|
if link {
|
||||||
|
opc = l64jumpTable["BL"]
|
||||||
|
}
|
||||||
|
if relocs != nil {
|
||||||
|
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelLoong64Branch, Addend: op.Addr.Sym.Offset})
|
||||||
|
}
|
||||||
|
return l64wordLE(l64bbl(opc, 0)), nil
|
||||||
|
}
|
||||||
// Direct: label → b/bl.
|
// Direct: label → b/bl.
|
||||||
target := resolve(l64Label(op))
|
target := resolve(l64Label(op))
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
@@ -516,6 +558,46 @@ func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[stri
|
|||||||
return l64wordLE(l64bbl(opc, v)), nil
|
return l64wordLE(l64bbl(opc, v)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeLOONG64Jirl encodes the raw JIRL spelling, JIRL rd, rj, offset, the
|
||||||
|
// form the verify trampolines use. The (rj) indirect form without an offset
|
||||||
|
// is handled by encodeLOONG64Branch.
|
||||||
|
func encodeLOONG64Jirl(op uint32, ops []*ast.Operand) ([]byte, error) {
|
||||||
|
if len(ops) != 3 {
|
||||||
|
return nil, fmt.Errorf("JIRL expects 3 operands, got %d", len(ops))
|
||||||
|
}
|
||||||
|
rd := l64Reg(ops[0])
|
||||||
|
rj := l64Reg(ops[1])
|
||||||
|
if rd < 0 || rj < 0 {
|
||||||
|
return nil, fmt.Errorf("invalid register operand")
|
||||||
|
}
|
||||||
|
off, ok := l64offsetOperand(ops[2])
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("JIRL expects an immediate offset, got %q", ops[2].Raw)
|
||||||
|
}
|
||||||
|
if (int64(off)<<16)>>16 != int64(off) {
|
||||||
|
return nil, fmt.Errorf("JIRL offset %d out of the 16-bit range", off)
|
||||||
|
}
|
||||||
|
return l64wordLE(l64irr16(op, int(off), rj, rd)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// l64offsetOperand reads a bare numeric branch offset: an immediate ($n) or a
|
||||||
|
// plain number, which parses as an empty address carrying the digits in Raw.
|
||||||
|
func l64offsetOperand(op *ast.Operand) (int32, bool) {
|
||||||
|
if op.Imm.HasVal {
|
||||||
|
v := op.Imm.Val
|
||||||
|
if op.Imm.Neg {
|
||||||
|
v = -v
|
||||||
|
}
|
||||||
|
return int32(v), true
|
||||||
|
}
|
||||||
|
if op.Kind == ast.OpAddr && op.Addr.Sym == nil && op.Addr.Base == "" && op.Addr.Index == "" {
|
||||||
|
if v, err := strconv.ParseInt(op.Raw, 0, 64); err == nil {
|
||||||
|
return int32(v), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
// encodeLOONG64Branch16 encodes a 16-bit branch (BEQ/BNE/BLT/BGE/BLTU/BGEU):
|
// encodeLOONG64Branch16 encodes a 16-bit branch (BEQ/BNE/BLT/BGE/BLTU/BGEU):
|
||||||
// INSTR rj, rd, label, or INSTR rj, label with rd = R0, which the toolchain
|
// INSTR rj, rd, label, or INSTR rj, label with rd = R0, which the toolchain
|
||||||
// turns into the 21-bit BEQZ/BNEZ form when the register is the only operand.
|
// turns into the 21-bit BEQZ/BNEZ form when the register is the only operand.
|
||||||
@@ -578,8 +660,8 @@ func encodeLOONG64Branch16(mnem string, op uint32, ops []*ast.Operand, pc int, o
|
|||||||
|
|
||||||
// encodeLOONG64Branch21 encodes a single-register branch: BLTZ/BGEZ and
|
// encodeLOONG64Branch21 encodes a single-register branch: BLTZ/BGEZ and
|
||||||
// BFPT/BFPF use the 21-bit offset form (register in the rj field), while
|
// BFPT/BFPF use the 21-bit offset form (register in the rj field), while
|
||||||
// BGTZ/BLEZ — which the toolchain encodes with the register in the rd field
|
// BGTZ/BLEZ, which the toolchain encodes with the register in the rd field
|
||||||
// and a 16-bit offset — are handled separately.
|
// and a 16-bit offset, are handled separately.
|
||||||
func encodeLOONG64Branch21(mnem string, op uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
func encodeLOONG64Branch21(mnem string, op uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
||||||
if len(ops) != 2 {
|
if len(ops) != 2 {
|
||||||
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||||
@@ -692,7 +774,7 @@ func encodeLOONG64ImmArith(mnem string, de l64DualEnc, ops []*ast.Operand) ([]by
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isLoong64ShiftD reports whether a shift-immediate opcode constant is one of
|
// isLoong64ShiftD reports whether a shift-immediate opcode constant is one of
|
||||||
// the 6-bit (.d) variants — the toolchain distinguishes them by the bit
|
// the 6-bit (.d) variants, the toolchain distinguishes them by the bit
|
||||||
// position of the opcode field (bits [25:16]).
|
// position of the opcode field (bits [25:16]).
|
||||||
func isLoong64ShiftD(op uint32) bool {
|
func isLoong64ShiftD(op uint32) bool {
|
||||||
return op&0x03ff0000 != 0 && op>>25 == 0
|
return op&0x03ff0000 != 0 && op>>25 == 0
|
||||||
@@ -740,7 +822,7 @@ func l64MemOperands(ops []*ast.Operand, fi loong64FrameInfo) (rd, rj int, off in
|
|||||||
|
|
||||||
// ---- the MOV pseudo-instruction ----
|
// ---- the MOV pseudo-instruction ----
|
||||||
|
|
||||||
// encodeLOONG64Mov encodes the MOV family — the load/store/immediate
|
// encodeLOONG64Mov encodes the MOV family, the load/store/immediate
|
||||||
// workhorse of Go's loong64 assembly. MOV is an alias of MOVV (the width
|
// workhorse of Go's loong64 assembly. MOV is an alias of MOVV (the width
|
||||||
// mnemonics MOVB/MOVH/MOVW/MOVV/MOVBU/MOVHU/MOVWU/MOVF/MOVD select the
|
// mnemonics MOVB/MOVH/MOVW/MOVV/MOVBU/MOVHU/MOVWU/MOVF/MOVD select the
|
||||||
// access width). The forms, mirroring the toolchain:
|
// access width). The forms, mirroring the toolchain:
|
||||||
|
|||||||
+14
-14
@@ -9,7 +9,7 @@ package asm
|
|||||||
// an opcode constant, and the format selects the bit layout. The opcode
|
// an opcode constant, and the format selects the bit layout. The opcode
|
||||||
// constants and formats are transcribed from the Go toolchain's own loong64
|
// constants and formats are transcribed from the Go toolchain's own loong64
|
||||||
// backend (cmd/internal/obj/loong64), so the emitted bytes match `go tool asm`
|
// backend (cmd/internal/obj/loong64), so the emitted bytes match `go tool asm`
|
||||||
// exactly — the ground-truth oracle for the verify suite.
|
// exactly, the ground-truth oracle for the verify suite.
|
||||||
//
|
//
|
||||||
// All LoongArch instructions are 32 bits, little-endian. The formats used
|
// All LoongArch instructions are 32 bits, little-endian. The formats used
|
||||||
// here (per the LoongArch Volume I specification):
|
// here (per the LoongArch Volume I specification):
|
||||||
@@ -33,8 +33,8 @@ package asm
|
|||||||
import "maps"
|
import "maps"
|
||||||
|
|
||||||
// loong64RegNum returns the 5-bit register number for a LoongArch register
|
// loong64RegNum returns the 5-bit register number for a LoongArch register
|
||||||
// name: R0–R31 (integer), F0–F31 (floating point), FCC0–FCC7 (condition
|
// name: R0-R31 (integer), F0-F31 (floating point), FCC0-FCC7 (condition
|
||||||
// flags), FCSR0–FCSR31 (control/status) and the ABI aliases the runtime's
|
// flags), FCSR0-FCSR31 (control/status) and the ABI aliases the runtime's
|
||||||
// assembly uses. Returns -1 for an unrecognised name.
|
// assembly uses. Returns -1 for an unrecognised name.
|
||||||
func loong64RegNum(name string) int {
|
func loong64RegNum(name string) int {
|
||||||
switch name {
|
switch name {
|
||||||
@@ -103,7 +103,7 @@ func loong64RegNum(name string) int {
|
|||||||
case "R31", "S8":
|
case "R31", "S8":
|
||||||
return 31
|
return 31
|
||||||
}
|
}
|
||||||
// F0–F31, FCC0–FCC7, FCSR0–FCSR31.
|
// F0-F31, FCC0-FCC7, FCSR0-FCSR31.
|
||||||
if len(name) >= 4 && name[:4] == "FCSR" {
|
if len(name) >= 4 && name[:4] == "FCSR" {
|
||||||
return loong64RegSpecial(name[4:], 31)
|
return loong64RegSpecial(name[4:], 31)
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ func l64rrrr(op uint32, r1, r2, r3, r4 int) uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// l64irir encodes a BSTRINS/BSTRPICK instruction: op | msb<<16 | rj<<5 | lsb<<10 | rd.
|
// l64irir encodes a BSTRINS/BSTRPICK instruction: op | msb<<16 | rj<<5 | lsb<<10 | rd.
|
||||||
// The msb/lsb fields are 6 bits wide (0–63) and are validated by the caller.
|
// The msb/lsb fields are 6 bits wide (0-63) and are validated by the caller.
|
||||||
func l64irir(op uint32, msb, rj, lsb, rd int) uint32 {
|
func l64irir(op uint32, msb, rj, lsb, rd int) uint32 {
|
||||||
return op | uint32(msb)<<16 | uint32(rj&0x1f)<<5 | uint32(lsb)<<10 | uint32(rd&0x1f)
|
return op | uint32(msb)<<16 | uint32(rj&0x1f)<<5 | uint32(lsb)<<10 | uint32(rd&0x1f)
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ var l64DualTable = map[string]l64DualEnc{}
|
|||||||
var l64InstrTable = map[string]l64Enc{}
|
var l64InstrTable = map[string]l64Enc{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// 3R — integer.
|
// 3R, integer.
|
||||||
rrr := map[string]uint32{
|
rrr := map[string]uint32{
|
||||||
"ADD": 0x20 << 15, "ADDW": 0x20 << 15, "ADDV": 0x21 << 15, "ADDVU": 0x21 << 15,
|
"ADD": 0x20 << 15, "ADDW": 0x20 << 15, "ADDV": 0x21 << 15, "ADDVU": 0x21 << 15,
|
||||||
"SUB": 0x22 << 15, "SUBW": 0x22 << 15, "SUBV": 0x23 << 15, "SUBVU": 0x23 << 15,
|
"SUB": 0x22 << 15, "SUBW": 0x22 << 15, "SUBV": 0x23 << 15, "SUBVU": 0x23 << 15,
|
||||||
@@ -300,7 +300,7 @@ func init() {
|
|||||||
"CRCWBW": 0x48 << 15, "CRCWHW": 0x49 << 15, "CRCWWW": 0x4a << 15, "CRCWVW": 0x4b << 15,
|
"CRCWBW": 0x48 << 15, "CRCWHW": 0x49 << 15, "CRCWWW": 0x4a << 15, "CRCWVW": 0x4b << 15,
|
||||||
"CRCCWBW": 0x4c << 15, "CRCCWHW": 0x4d << 15, "CRCCWWW": 0x4e << 15, "CRCCWVW": 0x4f << 15,
|
"CRCCWBW": 0x4c << 15, "CRCCWHW": 0x4d << 15, "CRCCWWW": 0x4e << 15, "CRCCWVW": 0x4f << 15,
|
||||||
}
|
}
|
||||||
// 3R — floating point.
|
// 3R, floating point.
|
||||||
rrr["MULF"] = 0x209 << 15
|
rrr["MULF"] = 0x209 << 15
|
||||||
rrr["MULD"] = 0x20a << 15
|
rrr["MULD"] = 0x20a << 15
|
||||||
rrr["DIVF"] = 0x20d << 15
|
rrr["DIVF"] = 0x20d << 15
|
||||||
@@ -390,12 +390,12 @@ func init() {
|
|||||||
"ROTRV": {rrr: 0x37 << 15, imm: 0x004d << 16, shift: true},
|
"ROTRV": {rrr: 0x37 << 15, imm: 0x004d << 16, shift: true},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2RI12 — pure immediate arithmetic (LU52ID has no register form).
|
// 2RI12, pure immediate arithmetic (LU52ID has no register form).
|
||||||
l64InstrTable["LU52ID"] = l64Enc{format: l64Firr, op: 0x00c << 22}
|
l64InstrTable["LU52ID"] = l64Enc{format: l64Firr, op: 0x00c << 22}
|
||||||
// ADDV16 (addu16i.d): 2RI16 with the immediate shifted right by 16.
|
// ADDV16 (addu16i.d): 2RI16 with the immediate shifted right by 16.
|
||||||
l64InstrTable["ADDV16"] = l64Enc{format: l64Firr16, op: 0x4 << 26}
|
l64InstrTable["ADDV16"] = l64Enc{format: l64Firr16, op: 0x4 << 26}
|
||||||
|
|
||||||
// 2RI14 — LL/SC are aliased by the Go assembler to the pointer loads and
|
// 2RI14, LL/SC are aliased by the Go assembler to the pointer loads and
|
||||||
// stores (ldptr/stptr), with the offset scaled by 4.
|
// stores (ldptr/stptr), with the offset scaled by 4.
|
||||||
l64InstrTable["MOVWP"] = l64Enc{format: l64Firr14, op: 0x25 << 24} // stptr.w
|
l64InstrTable["MOVWP"] = l64Enc{format: l64Firr14, op: 0x25 << 24} // stptr.w
|
||||||
l64InstrTable["MOVVP"] = l64Enc{format: l64Firr14, op: 0x27 << 24} // stptr.d
|
l64InstrTable["MOVVP"] = l64Enc{format: l64Firr14, op: 0x27 << 24} // stptr.d
|
||||||
@@ -414,7 +414,7 @@ func init() {
|
|||||||
// LUI is the Plan 9 spelling of lu12i.w.
|
// LUI is the Plan 9 spelling of lu12i.w.
|
||||||
l64InstrTable["LUI"] = l64Enc{format: l64Fir20, op: 0x0a << 25}
|
l64InstrTable["LUI"] = l64Enc{format: l64Fir20, op: 0x0a << 25}
|
||||||
|
|
||||||
// 4R — fused multiply-add.
|
// 4R, fused multiply-add.
|
||||||
rrrr := map[string]uint32{
|
rrrr := map[string]uint32{
|
||||||
"FMADDF": 0x81 << 20, "FMADDD": 0x82 << 20,
|
"FMADDF": 0x81 << 20, "FMADDD": 0x82 << 20,
|
||||||
"FMSUBF": 0x85 << 20, "FMSUBD": 0x86 << 20,
|
"FMSUBF": 0x85 << 20, "FMSUBD": 0x86 << 20,
|
||||||
@@ -425,7 +425,7 @@ func init() {
|
|||||||
l64InstrTable[m] = l64Enc{format: l64Frrrr, op: op}
|
l64InstrTable[m] = l64Enc{format: l64Frrrr, op: op}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IRIR — bit-field insert/extract.
|
// IRIR, bit-field insert/extract.
|
||||||
irir := map[string]uint32{
|
irir := map[string]uint32{
|
||||||
"BSTRINSW": 0x3<<21 | 0x0<<15,
|
"BSTRINSW": 0x3<<21 | 0x0<<15,
|
||||||
"BSTRINSV": 0x2 << 22,
|
"BSTRINSV": 0x2 << 22,
|
||||||
@@ -436,7 +436,7 @@ func init() {
|
|||||||
l64InstrTable[m] = l64Enc{format: l64Firir, op: op}
|
l64InstrTable[m] = l64Enc{format: l64Firir, op: op}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3RI2 — ALSL.
|
// 3RI2, ALSL.
|
||||||
irrr := map[string]uint32{
|
irrr := map[string]uint32{
|
||||||
"ALSLW": 0x2 << 17, "ALSLWU": 0x3 << 17, "ALSLV": 0x16 << 17,
|
"ALSLW": 0x2 << 17, "ALSLWU": 0x3 << 17, "ALSLV": 0x16 << 17,
|
||||||
}
|
}
|
||||||
@@ -452,7 +452,7 @@ func init() {
|
|||||||
// PRELD.
|
// PRELD.
|
||||||
l64InstrTable["PRELD"] = l64Enc{format: l64Fpreld, op: 0x0ab << 22}
|
l64InstrTable["PRELD"] = l64Enc{format: l64Fpreld, op: 0x0ab << 22}
|
||||||
|
|
||||||
// Atomics — 3R with the AM field order (rk=value, rj=address, rd=result).
|
// Atomics, 3R with the AM field order (rk=value, rj=address, rd=result).
|
||||||
am := map[string]uint32{
|
am := map[string]uint32{
|
||||||
"AMSWAPB": 0x070B8 << 15, "AMSWAPH": 0x070B9 << 15,
|
"AMSWAPB": 0x070B8 << 15, "AMSWAPH": 0x070B9 << 15,
|
||||||
"AMSWAPW": 0x070C0 << 15, "AMSWAPV": 0x070C1 << 15,
|
"AMSWAPW": 0x070C0 << 15, "AMSWAPV": 0x070C1 << 15,
|
||||||
@@ -477,7 +477,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// l64FpMovTable maps (mnemonic, from-class, to-class) to the 2R opcode of the
|
// l64FpMovTable maps (mnemonic, from-class, to-class) to the 2R opcode of the
|
||||||
// register move between the integer and floating-point register banks — the
|
// register move between the integer and floating-point register banks, the
|
||||||
// MOVW/MOVV specials the Go assembler accepts.
|
// MOVW/MOVV specials the Go assembler accepts.
|
||||||
var l64FpMovTable = map[string]uint32{
|
var l64FpMovTable = map[string]uint32{
|
||||||
"MOVV.R.F": 0x452a << 10, // movgr2fr.d
|
"MOVV.R.F": 0x452a << 10, // movgr2fr.d
|
||||||
|
|||||||
@@ -291,3 +291,40 @@ done:
|
|||||||
t.Errorf("code = % x\nwant % x", code, want)
|
t.Errorf("code = % x\nwant % x", code, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLOONG64IndirectBranch pins the indirect branch encodings: JMP (Rj) and
|
||||||
|
// JAL (Rj) lower to jirl, and the raw JIRL spelling encodes the written
|
||||||
|
// offset (the Go loong64 assembler deletes raw JIRL instructions entirely,
|
||||||
|
// so this form is a gasm-only superset with faithful semantics).
|
||||||
|
func TestLOONG64IndirectBranch(t *testing.T) {
|
||||||
|
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0-0
|
||||||
|
JMP (R4)
|
||||||
|
JIRL R0, R4, 8
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleLOONG64Helper(t, fn)
|
||||||
|
wantWords(t, code,
|
||||||
|
0x4C000080, // jirl r0, r4, 0
|
||||||
|
0x4C002080, // jirl r0, r4, 8
|
||||||
|
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||||
|
)
|
||||||
|
|
||||||
|
// JAL (R5) links, so the toolchain gives the function its autosize-8
|
||||||
|
// prologue and epilogue around the call and the closing RET.
|
||||||
|
fn = firstTextLOONG64(t, `#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0-0
|
||||||
|
JAL (R5)
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code = assembleLOONG64Helper(t, fn)
|
||||||
|
wantWords(t, code,
|
||||||
|
0x29FFE061, // addi.d r1, r2, -8 (prologue)
|
||||||
|
0x02FFE063, // addi.d r3, r3, -8
|
||||||
|
0x29C00061, // st.d r1, r2, 0 (prologue saves RA)
|
||||||
|
0x4C0000A1, // jirl r1, r5, 0
|
||||||
|
0x28C00061, // ld.d r1, r2, 0 (epilogue restores RA)
|
||||||
|
0x02C02063, // addi.d r3, r3, 8
|
||||||
|
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+222
-11
@@ -20,14 +20,20 @@ import (
|
|||||||
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
|
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
|
||||||
// A leaf function (no calls) with a zero frame gets no prologue at all.
|
// A leaf function (no calls) with a zero frame gets no prologue at all.
|
||||||
//
|
//
|
||||||
// Prologue (autosize > 0), byte-identical to the toolchain:
|
// Prologue (autosize > 0, small), byte-identical to the toolchain:
|
||||||
//
|
//
|
||||||
// MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe)
|
// MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe)
|
||||||
// ADDV $-autosize, R3 // open the frame
|
// ADDV $-autosize, R3 // open the frame
|
||||||
// MOVV R1, 0(R3) // save LR again at SP (signal-safety)
|
// MOVV R1, 0(R3) // save LR again at SP (signal-safety)
|
||||||
//
|
//
|
||||||
|
// Large frames (autosize past the 12-bit offset or immediate ranges) expand
|
||||||
|
// the store and the adjust through REGTMP (R30) exactly as the toolchain's
|
||||||
|
// assembler does: the store via the rounding LU12IW split, the adjust via
|
||||||
|
// the floor LU12IW/ORI split.
|
||||||
|
//
|
||||||
// Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR
|
// Epilogue: MOVV 0(R3), R1; ADDV $autosize, R3 (non-leaf only for the LR
|
||||||
// restore); the RET's jirl r0, r1, 0 follows.
|
// restore; the adjust materialised when the immediate does not fit); the
|
||||||
|
// RET's jirl r0, r1, 0 follows.
|
||||||
|
|
||||||
// loong64FrameInfo holds the frame layout derived from a TEXT directive.
|
// loong64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||||
type loong64FrameInfo struct {
|
type loong64FrameInfo struct {
|
||||||
@@ -36,6 +42,11 @@ type loong64FrameInfo struct {
|
|||||||
args int // the declared -argsize
|
args int // the declared -argsize
|
||||||
noSplit bool // the NOSPLIT flag
|
noSplit bool // the NOSPLIT flag
|
||||||
leaf bool // no call instructions in the body
|
leaf bool // no call instructions in the body
|
||||||
|
|
||||||
|
// Stack-split guard state: like amd64 and arm64, a leaf function with a
|
||||||
|
// small autosize is auto-marked NOSPLIT by the toolchain.
|
||||||
|
needSplit bool
|
||||||
|
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
|
||||||
}
|
}
|
||||||
|
|
||||||
// loong64ComputeFrame derives the frame layout for a TEXT function.
|
// loong64ComputeFrame derives the frame layout for a TEXT function.
|
||||||
@@ -59,9 +70,157 @@ func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
|
|||||||
// A zero-frame non-leaf function still opens an 8-byte frame for LR.
|
// A zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||||
fi.autosize = 8
|
fi.autosize = 8
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
|
case fi.noSplit:
|
||||||
|
case fi.autosize < stackSmall && fi.leaf:
|
||||||
|
// Auto-NOSPLIT, as the toolchain's leaf mark concludes.
|
||||||
|
default:
|
||||||
|
fi.needSplit = true
|
||||||
|
switch {
|
||||||
|
case fi.autosize <= stackSmall:
|
||||||
|
fi.splitClass = 0
|
||||||
|
case fi.autosize <= stackBig:
|
||||||
|
fi.splitClass = 1
|
||||||
|
default:
|
||||||
|
fi.splitClass = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
return fi
|
return fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loong64GuardLen returns the byte length of the stack-split guard prefix
|
||||||
|
// (zero when the function needs no guard). The big class materialises two
|
||||||
|
// constants through R30; each materialisation shrinks by one word when the
|
||||||
|
// constant's low 12 bits are zero.
|
||||||
|
func loong64GuardLen(fi loong64FrameInfo) int {
|
||||||
|
if !fi.needSplit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
off := int64(fi.autosize - stackSmall)
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
return 12
|
||||||
|
case 1:
|
||||||
|
if off <= 2048 {
|
||||||
|
return 16 // ADDV $-off fits the signed 12-bit immediate
|
||||||
|
}
|
||||||
|
return 24 // MOVV + LU12IW + ORI + ADDV + SGTU + BEQ
|
||||||
|
default:
|
||||||
|
// MOVV + [mat] + SGTU + BNE + [mat] + ADDV + SGTU + BEQ
|
||||||
|
return (6 + loong64MatLen(off) + loong64MatLen(-off)) * 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64MatLen reports the word count of materialising v in R30: a value
|
||||||
|
// with a zero high part needs only the ORI (the toolchain's MOVW $v, R30),
|
||||||
|
// one with a zero low part only the LU12IW.
|
||||||
|
func loong64MatLen(v int64) int {
|
||||||
|
if v>>12 == 0 || v&0xFFF == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64MatWords appends the words that materialise v in R30, splitting it
|
||||||
|
// as v>>12 plus the zero-extended low 12 bits.
|
||||||
|
func loong64MatWords(ws []uint32, v int64) []uint32 {
|
||||||
|
hi := v >> 12
|
||||||
|
lo := v & 0xFFF
|
||||||
|
if hi == 0 {
|
||||||
|
return append(ws, l64irr(l64OriOp, int(v), 0, 30))
|
||||||
|
}
|
||||||
|
ws = append(ws, l64ir(l64Lu12iwOp, int(hi), 30))
|
||||||
|
if lo != 0 {
|
||||||
|
ws = append(ws, l64irr(l64OriOp, int(lo), 30, 30))
|
||||||
|
}
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// The LU12IW and ORI opcode bases (2RI20 and 2RI12 formats); the ORI reads
|
||||||
|
// and writes rd itself.
|
||||||
|
const (
|
||||||
|
l64Lu12iwOp = 0x0a << 25
|
||||||
|
l64OriOp = 0x0e << 22
|
||||||
|
)
|
||||||
|
|
||||||
|
// loong64Imm12 reports whether v fits a signed 12-bit immediate.
|
||||||
|
func loong64Imm12(v int64) bool { return v >= -2048 && v <= 2047 }
|
||||||
|
|
||||||
|
// loong64GuardBytes emits the stack-split guard prefix. blockStart is the
|
||||||
|
// function-relative address of the morestack call at the end of the function;
|
||||||
|
// branch displacements are in instructions and are computed from each
|
||||||
|
// branch's own position.
|
||||||
|
func loong64GuardBytes(fi loong64FrameInfo, blockStart int) []byte {
|
||||||
|
// MOVV 16(g), R20 (g.stackguard0), g = R22.
|
||||||
|
ws := []uint32{l64irr(l64loadStoreTable["MOVV"].ld, 16, 22, 20)}
|
||||||
|
off := int64(fi.autosize - stackSmall)
|
||||||
|
// beq appends BEQ R20, blockStart from the branch's own position.
|
||||||
|
beq := func() {
|
||||||
|
ws = append(ws, loong64Beqz(20, int32((blockStart-len(ws)*4)>>2)))
|
||||||
|
}
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
// SGTU SP, R20, R20; BEQ R20, more
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 3, 20, 20))
|
||||||
|
beq()
|
||||||
|
case 1:
|
||||||
|
ws = append(ws, loong64MediumWords(off)...)
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
|
||||||
|
beq()
|
||||||
|
default:
|
||||||
|
// SGTU $off, SP, R24 catches the SP underflow a huge frame would
|
||||||
|
// cause; BNE jumps to morestack in that case.
|
||||||
|
ws = append(ws, loong64MatWords(nil, off)...)
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 30, 3, 24))
|
||||||
|
ws = append(ws, loong64Bnez(24, int32((blockStart-len(ws)*4)>>2)))
|
||||||
|
ws = append(ws, loong64MatWords(nil, -off)...)
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24))
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["SGTU"].rrr, 24, 20, 20))
|
||||||
|
beq()
|
||||||
|
}
|
||||||
|
return l64WordsLE(ws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64MediumWords emits the medium-class stack check for offset off: the
|
||||||
|
// ADDV immediate when it fits, otherwise the same sequence with the constant
|
||||||
|
// materialised in R30.
|
||||||
|
func loong64MediumWords(off int64) []uint32 {
|
||||||
|
if off <= 2048 {
|
||||||
|
return []uint32{l64irr(l64DualTable["ADDV"].imm, int(-off), 3, 24)}
|
||||||
|
}
|
||||||
|
ws := loong64MatWords(nil, -off)
|
||||||
|
return append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 24))
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64Beqz/loong64Bnez build the 21-bit conditional branches against R0
|
||||||
|
// that the toolchain emits for its guard compares.
|
||||||
|
func loong64Beqz(rj int, dispInstr int32) uint32 {
|
||||||
|
return l64ir21(l64branch21Table["BEQZ"], int(dispInstr), rj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loong64Bnez(rj int, dispInstr int32) uint32 {
|
||||||
|
return l64ir21(l64branch21Table["BNEZ"], int(dispInstr), rj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64MoreStackBlock emits the trailing block: MOVV R1, R31 (save LR, the
|
||||||
|
// toolchain's OR R1, R0, R31 expansion), BL runtime.morestack_noctxt, B back
|
||||||
|
// to the function entry.
|
||||||
|
func loong64MoreStackBlock(blockStart int) ([]byte, Reloc) {
|
||||||
|
ws := []uint32{
|
||||||
|
l64rrr(l64DualTable["OR"].rrr, 0, 1, 31), // MOVV R1, R31 (OR R1, R0, R31)
|
||||||
|
l64bbl(l64jumpTable["BL"], 0), // BL, patched by the linker
|
||||||
|
}
|
||||||
|
disp := (-(blockStart + 8)) >> 2
|
||||||
|
ws = append(ws, l64bbl(l64jumpTable["B"], int(disp)))
|
||||||
|
reloc := Reloc{
|
||||||
|
Off: blockStart + 4,
|
||||||
|
After: blockStart + 8,
|
||||||
|
Name: "runtime\u00b7morestack_noctxt",
|
||||||
|
Kind: RelLoong64Branch,
|
||||||
|
}
|
||||||
|
return l64WordsLE(ws...), reloc
|
||||||
|
}
|
||||||
|
|
||||||
// loong64IsLeaf reports whether a function contains no call instructions
|
// loong64IsLeaf reports whether a function contains no call instructions
|
||||||
// (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame
|
// (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame
|
||||||
// and the epilogue shape.
|
// and the epilogue shape.
|
||||||
@@ -79,17 +238,37 @@ func loong64IsLeaf(t *ast.Text) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// loong64Prologue returns the prologue bytes for a loong64 function.
|
// loong64Prologue returns the prologue bytes for a loong64 function. When
|
||||||
|
// the LR store offset leaves the toolchain's 12-bit store range ([-2046,
|
||||||
|
// 2045], BIG_12 = 2046) or the SP adjust immediate its 12-bit immediate
|
||||||
|
// range, each switches to the R30 materialisation the assembler expands it
|
||||||
|
// to: the store uses the rounding %hi/%lo split (LU12IW of (v+2048)>>12,
|
||||||
|
// REGTMP += SP, store at the raw offset), the adjust the floor split
|
||||||
|
// (LU12IW, ORI when the low part is non-zero, REGTMP += SP).
|
||||||
func loong64Prologue(fi loong64FrameInfo) []byte {
|
func loong64Prologue(fi loong64FrameInfo) []byte {
|
||||||
if fi.autosize == 0 {
|
if fi.autosize == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
addiD := l64DualTable["ADDV"].imm
|
addiD := l64DualTable["ADDV"].imm
|
||||||
return l64WordsLE(
|
var ws []uint32
|
||||||
l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3)
|
storeBase := 3
|
||||||
l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3
|
if fi.autosize > 2046 {
|
||||||
l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3)
|
// The store goes through REGTMP: LU12IW of the rounding split,
|
||||||
)
|
// REGTMP += SP, then the store at REGTMP with the truncated offset.
|
||||||
|
v := -int64(fi.autosize)
|
||||||
|
ws = append(ws, l64ir(l64Lu12iwOp, int((v+2048)>>12), 30))
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 3, 30, 30))
|
||||||
|
storeBase = 30
|
||||||
|
}
|
||||||
|
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, storeBase, 1)) // MOVV R1, -autosize(base)
|
||||||
|
if loong64Imm12(-int64(fi.autosize)) {
|
||||||
|
ws = append(ws, l64irr(addiD, -fi.autosize, 3, 3)) // ADDV $-autosize, R3
|
||||||
|
} else {
|
||||||
|
ws = append(ws, loong64MatWords(nil, -int64(fi.autosize))...)
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 3))
|
||||||
|
}
|
||||||
|
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1)) // MOVV R1, 0(R3)
|
||||||
|
return l64WordsLE(ws...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loong64Return returns the bytes for a RET: the epilogue (restore LR and
|
// loong64Return returns the bytes for a RET: the epilogue (restore LR and
|
||||||
@@ -98,17 +277,49 @@ func loong64Return(fi loong64FrameInfo) []byte {
|
|||||||
var ws []uint32
|
var ws []uint32
|
||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
if !fi.leaf {
|
if !fi.leaf {
|
||||||
// MOVV 0(R3), R1 — restore the link register.
|
// MOVV 0(R3), R1, restore the link register.
|
||||||
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1))
|
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1))
|
||||||
}
|
}
|
||||||
// ADDV $autosize, R3 — close the frame.
|
// ADDV $autosize, R3, close the frame (materialised when the
|
||||||
|
// immediate does not fit).
|
||||||
|
if loong64Imm12(int64(fi.autosize)) {
|
||||||
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3))
|
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3))
|
||||||
|
} else {
|
||||||
|
ws = append(ws, loong64MatWords(nil, int64(fi.autosize))...)
|
||||||
|
ws = append(ws, l64rrr(l64DualTable["ADDV"].rrr, 30, 3, 3))
|
||||||
}
|
}
|
||||||
// jirl r0, r1, 0 — return.
|
}
|
||||||
|
// jirl r0, r1, 0, return.
|
||||||
ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0))
|
ws = append(ws, l64irr16(l64branchTable["JIRL"], 0, 1, 0))
|
||||||
return l64WordsLE(ws...)
|
return l64WordsLE(ws...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loong64StoreWords reports the prologue word count of the LR store, and
|
||||||
|
// loong64AdjustWords the word count of an SP adjust of v: the immediate
|
||||||
|
// forms when they fit, otherwise the R30 materialisation sequences.
|
||||||
|
func loong64StoreWords(autosize int) int {
|
||||||
|
if autosize > 2046 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func loong64AdjustWords(v int64) int {
|
||||||
|
if loong64Imm12(v) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return loong64MatLen(v) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// loong64EpilogueWords reports the epilogue word count the RET expands to.
|
||||||
|
func loong64EpilogueWords(fi loong64FrameInfo) int {
|
||||||
|
n := loong64AdjustWords(int64(fi.autosize))
|
||||||
|
if !fi.leaf {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
// loong64ResolvePseudo translates a pseudo-register memory reference into a
|
// loong64ResolvePseudo translates a pseudo-register memory reference into a
|
||||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||||
// x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable
|
// x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ done:
|
|||||||
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
||||||
// the prologue raises the SP delta by autosize (in effect from the third
|
// the prologue raises the SP delta by autosize (in effect from the third
|
||||||
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
||||||
// in MinLC (4) units — byte-identical to `go tool asm`.
|
// in MinLC (4) units; byte-identical to `go tool asm`.
|
||||||
func TestLOONG64_pcsp(t *testing.T) {
|
func TestLOONG64_pcsp(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLOONG64RelocOffsetsIncludePrologue pins the function-relative
|
||||||
|
// relocation offsets of a framed loong64 function: the offsets used to
|
||||||
|
// exclude the prologue, so every relocation landed on a prologue
|
||||||
|
// instruction in the GOOBJ/ELF output.
|
||||||
|
func TestLOONG64RelocOffsetsIncludePrologue(t *testing.T) {
|
||||||
|
f, errs := parser.Parse("k_loong64.s", "TEXT \u00b7f(SB), $16-0\n"+
|
||||||
|
"\tMOVV $gdata(SB), R4\n"+
|
||||||
|
"\tRET\n"+
|
||||||
|
"GLOBL gdata(SB), $8\n")
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFileLOONG64(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("assemble: %v", err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
|
||||||
|
// Layout: 12-byte prologue (autosize 32), pcalau12i+addi.d (12, 16),
|
||||||
|
// epilogue with RET.
|
||||||
|
if len(fn.Relocs) != 2 {
|
||||||
|
t.Fatalf("relocs = %d, want 2", len(fn.Relocs))
|
||||||
|
}
|
||||||
|
hi, lo := fn.Relocs[0], fn.Relocs[1]
|
||||||
|
if hi.Kind != RelLoong64AddrHi || hi.Off != 12 || hi.After != 12 {
|
||||||
|
t.Errorf("hi reloc = {off %d after %d kind %d}, want {off 12 after 12 kind RelLoong64AddrHi}", hi.Off, hi.After, hi.Kind)
|
||||||
|
}
|
||||||
|
if lo.Kind != RelLoong64AddrLo || lo.Off != 16 || lo.After != 16 {
|
||||||
|
t.Errorf("lo reloc = {off %d after %d kind %d}, want {off 16 after 16 kind RelLoong64AddrLo}", lo.Off, lo.After, lo.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-9
@@ -12,33 +12,33 @@ import "maps"
|
|||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// Reg is an x86-64 register. In Plan 9 assembly the classic names (AX, BX, …)
|
// Reg is an x86-64 register. In Plan 9 assembly the classic names (AX, BX, …)
|
||||||
// are size-agnostic — the instruction suffix (MOVQ vs MOVL) fixes the width —
|
// are size-agnostic, the instruction suffix (MOVQ vs MOVL) fixes the width
|
||||||
// so the encoder keys off the register's index and lets the mnemonic supply the
|
// so the encoder keys off the register's index and lets the mnemonic supply the
|
||||||
// size. The high flag marks the legacy high-byte registers AH/CH/DH/BH, which
|
// size. The high flag marks the legacy high-byte registers AH/CH/DH/BH, which
|
||||||
// occupy indices 4–7 yet take no REX prefix, unlike SPL/BPL/SIL/DIL that share
|
// occupy indices 4-7 yet take no REX prefix, unlike SPL/BPL/SIL/DIL that share
|
||||||
// those indices but require one. The mask flag marks the AVX-512 opmask
|
// those indices but require one. The mask flag marks the AVX-512 opmask
|
||||||
// registers K0–K7.
|
// registers K0-K7.
|
||||||
type Reg struct {
|
type Reg struct {
|
||||||
idx int
|
idx int
|
||||||
size int // informational width implied by the name; the mnemonic decides
|
size int // informational width implied by the name; the mnemonic decides
|
||||||
high bool // AH/CH/DH/BH
|
high bool // AH/CH/DH/BH
|
||||||
mask bool // K0–K7 opmask register
|
mask bool // K0-K7 opmask register
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index returns the register number (0–15 for GPRs, 0–31 for vectors).
|
// Index returns the register number (0-15 for GPRs, 0-31 for vectors).
|
||||||
func (r Reg) Index() int { return r.idx }
|
func (r Reg) Index() int { return r.idx }
|
||||||
|
|
||||||
// Size returns the width in bytes implied by the register's name.
|
// Size returns the width in bytes implied by the register's name.
|
||||||
func (r Reg) Size() int { return r.size }
|
func (r Reg) Size() int { return r.size }
|
||||||
|
|
||||||
// IsMask reports whether r is an AVX-512 opmask register (K0–K7).
|
// IsMask reports whether r is an AVX-512 opmask register (K0-K7).
|
||||||
func (r Reg) IsMask() bool { return r.mask }
|
func (r Reg) IsMask() bool { return r.mask }
|
||||||
|
|
||||||
func (r Reg) isOperand() {}
|
func (r Reg) isOperand() {}
|
||||||
|
|
||||||
// needsREX reports whether this register forces a REX prefix at the given
|
// needsREX reports whether this register forces a REX prefix at the given
|
||||||
// operand size: the extended registers R8–R15 always do, and at byte size the
|
// operand size: the extended registers R8-R15 always do, and at byte size the
|
||||||
// low registers SPL/BPL/SIL/DIL (indices 4–7, not high) do as well.
|
// low registers SPL/BPL/SIL/DIL (indices 4-7, not high) do as well.
|
||||||
func (r Reg) needsREX(opSize int) bool {
|
func (r Reg) needsREX(opSize int) bool {
|
||||||
if r.idx >= 8 {
|
if r.idx >= 8 {
|
||||||
return true
|
return true
|
||||||
@@ -133,7 +133,7 @@ func buildRegByName() map[string]Reg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vector: X0..X31 (128-bit, size 16), Y0..Y31 (256-bit, size 32),
|
// Vector: X0..X31 (128-bit, size 16), Y0..Y31 (256-bit, size 32),
|
||||||
// Z0..Z31 (512-bit, size 64). Indices 16–31 are only encodable in EVEX
|
// Z0..Z31 (512-bit, size 64). Indices 16-31 are only encodable in EVEX
|
||||||
// (AVX-512) instructions; the encoder validates that through its tables.
|
// (AVX-512) instructions; the encoder validates that through its tables.
|
||||||
for i := 0; i <= 31; i++ {
|
for i := 0; i <= 31; i++ {
|
||||||
m["X"+itoa(i)] = Reg{idx: i, size: 16}
|
m["X"+itoa(i)] = Reg{idx: i, size: 16}
|
||||||
|
|||||||
+264
-30
@@ -15,14 +15,16 @@ import (
|
|||||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
||||||
fi := riscvComputeFrame(t)
|
fi := riscvComputeFrame(t)
|
||||||
prologue := riscvPrologue(fi)
|
prologue := riscvPrologue(fi)
|
||||||
|
guardLen := riscvGuardLen(fi)
|
||||||
|
|
||||||
var relocs []Reloc
|
var relocs []Reloc
|
||||||
var spadj []SpadjStep
|
var spadj []SpadjStep
|
||||||
|
|
||||||
// The prologue raises the SP delta by autosize; the boundary is reported
|
// The prologue raises the SP delta by autosize; the boundary is reported
|
||||||
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
|
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
|
||||||
|
// The guard prefix shifts its PC.
|
||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
spadj = append(spadj, SpadjStep{PC: riscvPrologueSpadjPC(fi), Value: fi.autosize})
|
spadj = append(spadj, SpadjStep{PC: guardLen + riscvPrologueSpadjPC(fi), Value: fi.autosize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
|
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
|
||||||
@@ -34,7 +36,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
}
|
}
|
||||||
var recs []instrRec
|
var recs []instrRec
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pos := len(prologue)
|
pos := guardLen + len(prologue)
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -66,7 +68,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
|
|
||||||
// Pass 4: recompute offsets with actual sizes.
|
// Pass 4: recompute offsets with actual sizes.
|
||||||
offsets = map[string]int{}
|
offsets = map[string]int{}
|
||||||
pos = len(prologue)
|
pos = guardLen + len(prologue)
|
||||||
for _, stmt := range t.Body {
|
for _, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -82,9 +84,17 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pass 5: re-encode branches with corrected offsets. Record relocations
|
// Pass 5: re-encode branches with corrected offsets. Record relocations
|
||||||
// during this final pass (relocation offsets are relative to instruction start).
|
// during this final pass (relocation offsets are relative to instruction
|
||||||
out := append([]byte(nil), prologue...)
|
// start). The guard prefix precedes the prologue; its branches target
|
||||||
pc = len(prologue)
|
// the morestack block at the end of the function, which the previous
|
||||||
|
// passes have sized.
|
||||||
|
var out []byte
|
||||||
|
guardBytes, guardReloc := riscvGuard(fi)
|
||||||
|
if fi.needSplit {
|
||||||
|
out = append(out, guardBytes...)
|
||||||
|
}
|
||||||
|
out = append(out, prologue...)
|
||||||
|
pc = guardLen + len(prologue)
|
||||||
preCount := len(relocs)
|
preCount := len(relocs)
|
||||||
var lines []LineEntry
|
var lines []LineEntry
|
||||||
for _, r := range recs {
|
for _, r := range recs {
|
||||||
@@ -119,9 +129,49 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
pc += len(code)
|
pc += len(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fi.needSplit {
|
||||||
|
relocs = append(relocs, guardReloc)
|
||||||
|
}
|
||||||
return out, offsets, relocs, lines, spadj, nil
|
return out, offsets, relocs, lines, spadj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// riscvImmAlias maps the R-type ALU mnemonics onto their I-type immediate
|
||||||
|
// forms: the toolchain accepts ADD $imm, rj, rd and emits addi. Applied
|
||||||
|
// whenever the first operand is an immediate.
|
||||||
|
var riscvImmAlias = map[string]string{
|
||||||
|
"ADD": "ADDI",
|
||||||
|
"ADDW": "ADDIW",
|
||||||
|
"AND": "ANDI",
|
||||||
|
"OR": "ORI",
|
||||||
|
"XOR": "XORI",
|
||||||
|
"SLL": "SLLI",
|
||||||
|
"SRL": "SRLI",
|
||||||
|
"SRA": "SRAI",
|
||||||
|
"SLLW": "SLLIW",
|
||||||
|
"SRLW": "SRLIW",
|
||||||
|
"SRAW": "SRAIW",
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvNormaliseImmAlias rewrites the mnemonic to its immediate form when the
|
||||||
|
// first operand is an immediate: the toolchain accepts ADD $imm, rj, rd and
|
||||||
|
// emits addi, and SUB $imm becomes addi with the negated immediate. The
|
||||||
|
// second result reports that negation; the operand itself is left untouched
|
||||||
|
// because several passes normalise the same instruction.
|
||||||
|
func riscvNormaliseImmAlias(mnem string, ops []*ast.Operand) (string, bool) {
|
||||||
|
if len(ops) >= 2 && isImmOperand(ops[0]) {
|
||||||
|
switch strings.ToUpper(mnem) {
|
||||||
|
case "SUB":
|
||||||
|
return "ADDI", true
|
||||||
|
case "SUBW":
|
||||||
|
return "ADDIW", true
|
||||||
|
}
|
||||||
|
if alias, ok := riscvImmAlias[strings.ToUpper(mnem)]; ok {
|
||||||
|
return alias, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mnem, false
|
||||||
|
}
|
||||||
|
|
||||||
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
||||||
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
||||||
// arithmetic with a large immediate expand to several (possibly compressed)
|
// arithmetic with a large immediate expand to several (possibly compressed)
|
||||||
@@ -129,10 +179,12 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
|||||||
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||||
mnem := instr.Mnemonic.Text
|
mnem := instr.Mnemonic.Text
|
||||||
ops := instr.Operands
|
ops := instr.Operands
|
||||||
|
var immNeg bool
|
||||||
|
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
||||||
if mnem == "RET" {
|
if mnem == "RET" {
|
||||||
return len(riscvReturn(fi))
|
return len(riscvReturn(fi))
|
||||||
}
|
}
|
||||||
if mnem == "MOV" && len(ops) == 2 {
|
if strings.HasPrefix(mnem, "MOV") && len(ops) == 2 {
|
||||||
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
|
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
|
||||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
|
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
|
||||||
return 8
|
return 8
|
||||||
@@ -149,10 +201,22 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
|||||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil {
|
if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil {
|
||||||
return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0]))
|
return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0]))
|
||||||
}
|
}
|
||||||
|
// Frame-relative loads and stores: a frame offset beyond the signed
|
||||||
|
// 12-bit range materialises the address in X31 first.
|
||||||
|
if isMemOperand(ops[0]) && !isMemOperand(ops[1]) {
|
||||||
|
return riscvFrameMemSize(ops[0], fi)
|
||||||
|
}
|
||||||
|
if isMemOperand(ops[1]) && !isMemOperand(ops[0]) {
|
||||||
|
return riscvFrameMemSize(ops[1], fi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// I-type arithmetic with a large immediate expands to several instructions.
|
// I-type arithmetic with a large immediate expands to several instructions.
|
||||||
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
|
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
|
||||||
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
|
imm := immFromOperand(ops[0])
|
||||||
|
if immNeg {
|
||||||
|
imm = -imm
|
||||||
|
}
|
||||||
|
return riscvItypeImmediateSize(mnem, imm)
|
||||||
}
|
}
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
@@ -171,6 +235,8 @@ func isBranchLike(mnem string) bool {
|
|||||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||||
mnem := instr.Mnemonic.Text
|
mnem := instr.Mnemonic.Text
|
||||||
ops := instr.Operands
|
ops := instr.Operands
|
||||||
|
var immNeg bool
|
||||||
|
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
||||||
var word uint32
|
var word uint32
|
||||||
|
|
||||||
// Handle pseudo-instructions and special cases first.
|
// Handle pseudo-instructions and special cases first.
|
||||||
@@ -187,19 +253,51 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
}
|
}
|
||||||
op := ops[0]
|
op := ops[0]
|
||||||
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
||||||
|
// CALL (X5): an indirect call, the toolchain's JALR X1, 0(X5).
|
||||||
|
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||||
|
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||||
|
return nil, fmt.Errorf("CALL: invalid indirect operand %q", op.Raw)
|
||||||
|
}
|
||||||
|
rs1 := riscvRegNum(op.Addr.Base)
|
||||||
|
if rs1 < 0 {
|
||||||
|
return nil, fmt.Errorf("CALL: unknown branch register %q", op.Addr.Base)
|
||||||
|
}
|
||||||
|
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, 0)
|
||||||
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
||||||
}
|
}
|
||||||
if relocs != nil {
|
if relocs != nil {
|
||||||
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelRISCVJal, Addend: op.Addr.Sym.Offset})
|
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelRISCVJal, Addend: op.Addr.Sym.Offset})
|
||||||
}
|
}
|
||||||
word = riscvJType(1, 0) // JAL X1, 0 — the linker fills the offset
|
word = riscvJType(1, 0) // JAL X1, 0, the linker fills the offset
|
||||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
case "JMP":
|
case "JMP":
|
||||||
// JMP = JAL X0, target. The Go assembler never compresses this to
|
// JMP = JAL X0, target. The Go assembler never compresses this to
|
||||||
// C.J, so always emit the 32-bit JAL.
|
// C.J, so always emit the 32-bit JAL.
|
||||||
var target string
|
var target string
|
||||||
if len(ops) >= 1 {
|
if len(ops) >= 1 {
|
||||||
|
// JMP sym(SB): a tail call, JAL X0 against a symbol relocation.
|
||||||
|
if ops[0].Addr.Sym != nil && ops[0].Addr.Sym.Pseudo == "SB" {
|
||||||
|
if relocs != nil {
|
||||||
|
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: ops[0].Addr.Sym.Name, Kind: RelRISCVJal, Addend: ops[0].Addr.Sym.Offset})
|
||||||
|
}
|
||||||
|
word = riscvJType(0, 0)
|
||||||
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
|
}
|
||||||
target = labelFromOperand(ops[0])
|
target = labelFromOperand(ops[0])
|
||||||
|
// JMP (X5): an indirect branch, the toolchain's JALR X0, 0(X5).
|
||||||
|
if ops[0].Addr.Sym == nil && ops[0].Addr.Base != "" {
|
||||||
|
if ops[0].Addr.Offset != 0 || ops[0].Addr.Index != "" {
|
||||||
|
return nil, fmt.Errorf("JMP: invalid indirect operand %q", ops[0].Raw)
|
||||||
|
}
|
||||||
|
rs1 := riscvRegNum(ops[0].Addr.Base)
|
||||||
|
if rs1 < 0 {
|
||||||
|
return nil, fmt.Errorf("JMP: unknown branch register %q", ops[0].Addr.Base)
|
||||||
|
}
|
||||||
|
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0)
|
||||||
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -226,14 +324,50 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
|
|
||||||
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
||||||
// stores, register moves and immediate loads.
|
// stores, register moves and immediate loads. The width suffixes
|
||||||
case "MOV":
|
// (MOVB/MOVH/MOVW and unsigned forms) select the access width, and
|
||||||
|
// MOVD/MOVF address the FP registers.
|
||||||
|
case "MOV", "MOVB", "MOVBU", "MOVH", "MOVHU", "MOVW", "MOVWU", "MOVF", "MOVD":
|
||||||
return encodeRISCVMov(instr, fi, relocs)
|
return encodeRISCVMov(instr, fi, relocs)
|
||||||
|
|
||||||
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
||||||
case "JALR":
|
case "JALR":
|
||||||
return encodeRISCVJALR(instr, fi)
|
return encodeRISCVJALR(instr, fi)
|
||||||
|
|
||||||
|
// Branch-zero pseudos: BEQZ/BNEZ compare against X0, and BLTZ/BGEZ/
|
||||||
|
// BLEZ/BGTZ reorder the register operands of BLT/BGE accordingly.
|
||||||
|
case "BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
|
||||||
|
if len(ops) != 2 {
|
||||||
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||||
|
}
|
||||||
|
rs := regFromOperand(ops[0])
|
||||||
|
if rs < 0 {
|
||||||
|
return nil, fmt.Errorf("%s: invalid register", mnem)
|
||||||
|
}
|
||||||
|
target := labelFromOperand(ops[1])
|
||||||
|
targetOff, ok := offsets[target]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||||
|
}
|
||||||
|
var enc riscvEnc
|
||||||
|
rs1, rs2 := rs, 0
|
||||||
|
switch mnem {
|
||||||
|
case "BEQZ":
|
||||||
|
enc = riscvEnc{0x63, 0x0, 0x00} // beq rs, x0
|
||||||
|
case "BNEZ":
|
||||||
|
enc = riscvEnc{0x63, 0x1, 0x00} // bne rs, x0
|
||||||
|
case "BLTZ":
|
||||||
|
enc = riscvEnc{0x63, 0x4, 0x00} // blt rs, x0
|
||||||
|
case "BGEZ":
|
||||||
|
enc = riscvEnc{0x63, 0x5, 0x00} // bge rs, x0
|
||||||
|
case "BLEZ":
|
||||||
|
enc, rs1, rs2 = riscvEnc{0x63, 0x5, 0x00}, 0, rs // bge x0, rs
|
||||||
|
case "BGTZ":
|
||||||
|
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, 0, rs // blt x0, rs
|
||||||
|
}
|
||||||
|
word = riscvBType(enc, rs1, rs2, int32(targetOff-pc))
|
||||||
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
|
|
||||||
// System instructions with no operands.
|
// System instructions with no operands.
|
||||||
case "FENCE", "ECALL", "EBREAK":
|
case "FENCE", "ECALL", "EBREAK":
|
||||||
enc, ok := riscvInstrTable[mnem]
|
enc, ok := riscvInstrTable[mnem]
|
||||||
@@ -395,7 +529,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
}
|
}
|
||||||
word = riscvSType(enc, rs1, rs2, imm)
|
word = riscvSType(enc, rs1, rs2, imm)
|
||||||
|
|
||||||
// LR (load-reserved): INSTR (addr), dst — 2 operands.
|
// LR (load-reserved): INSTR (addr), dst, 2 operands.
|
||||||
case len(ops) == 2 && isLRInstr(mnem):
|
case len(ops) == 2 && isLRInstr(mnem):
|
||||||
rs1, _ := memFromOperandWithFrame(ops[0], fi)
|
rs1, _ := memFromOperandWithFrame(ops[0], fi)
|
||||||
rd := regFromOperand(ops[1])
|
rd := regFromOperand(ops[1])
|
||||||
@@ -404,7 +538,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
}
|
}
|
||||||
word = riscvAMOType(enc, rd, rs1, 0) // rs2=0 for LR
|
word = riscvAMOType(enc, rd, rs1, 0) // rs2=0 for LR
|
||||||
|
|
||||||
// SC (store-conditional): INSTR src, (addr), dst — 3 operands.
|
// SC (store-conditional): INSTR src, (addr), dst, 3 operands.
|
||||||
case len(ops) == 3 && isSCInstr(mnem):
|
case len(ops) == 3 && isSCInstr(mnem):
|
||||||
rs2 := regFromOperand(ops[0])
|
rs2 := regFromOperand(ops[0])
|
||||||
rs1, _ := memFromOperandWithFrame(ops[1], fi)
|
rs1, _ := memFromOperandWithFrame(ops[1], fi)
|
||||||
@@ -428,6 +562,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
// two-operand form INSTR $imm, rd uses rd as the source.
|
// two-operand form INSTR $imm, rd uses rd as the source.
|
||||||
case len(ops) == 3 && isITypeInstr(mnem):
|
case len(ops) == 3 && isITypeInstr(mnem):
|
||||||
imm := immFromOperand(ops[0]) // immediate
|
imm := immFromOperand(ops[0]) // immediate
|
||||||
|
if immNeg {
|
||||||
|
imm = -imm // SUB $imm arrived through the ADDI alias
|
||||||
|
}
|
||||||
rs1 := regFromOperand(ops[1]) // source register
|
rs1 := regFromOperand(ops[1]) // source register
|
||||||
rd := regFromOperand(ops[2]) // destination
|
rd := regFromOperand(ops[2]) // destination
|
||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
@@ -437,13 +574,16 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
|
|
||||||
case len(ops) == 2 && isITypeInstr(mnem):
|
case len(ops) == 2 && isITypeInstr(mnem):
|
||||||
imm := immFromOperand(ops[0])
|
imm := immFromOperand(ops[0])
|
||||||
|
if immNeg {
|
||||||
|
imm = -imm
|
||||||
|
}
|
||||||
rd := regFromOperand(ops[1])
|
rd := regFromOperand(ops[1])
|
||||||
if rd < 0 {
|
if rd < 0 {
|
||||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||||
}
|
}
|
||||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
||||||
|
|
||||||
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
|
// Loads: rd, offset(rs1), Plan 9 order is LD src, dst.
|
||||||
case len(ops) == 2 && isLoadInstr(mnem):
|
case len(ops) == 2 && isLoadInstr(mnem):
|
||||||
rd := regFromOperand(ops[1]) // destination (last operand)
|
rd := regFromOperand(ops[1]) // destination (last operand)
|
||||||
rs1, imm := memFromOperandWithFrame(ops[0], fi) // memory source (first operand)
|
rs1, imm := memFromOperandWithFrame(ops[0], fi) // memory source (first operand)
|
||||||
@@ -538,7 +678,7 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
|||||||
|
|
||||||
// Immediate → register.
|
// Immediate → register.
|
||||||
if isImmOperand(src) {
|
if isImmOperand(src) {
|
||||||
// MOV $sym(SB), rd — load address of a static symbol or external.
|
// MOV $sym(SB), rd, load address of a static symbol or external.
|
||||||
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
||||||
rd := regFromOperand(dst)
|
rd := regFromOperand(dst)
|
||||||
if rd < 0 {
|
if rd < 0 {
|
||||||
@@ -546,7 +686,7 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
|||||||
}
|
}
|
||||||
return encodeRISCVSBAddr(src.Imm.Sym, rd, relocs), nil
|
return encodeRISCVSBAddr(src.Imm.Sym, rd, relocs), nil
|
||||||
}
|
}
|
||||||
// MOV $sym(FP/SP), rd — not supported: immediate symbol references
|
// MOV $sym(FP/SP), rd, not supported: immediate symbol references
|
||||||
// other than SB cannot be encoded as a simple immediate.
|
// other than SB cannot be encoded as a simple immediate.
|
||||||
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo != "" {
|
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo != "" {
|
||||||
return nil, fmt.Errorf("MOV $%s(%s): unsupported immediate symbol reference (only SB is supported)", src.Imm.Sym.Name, src.Imm.Sym.Pseudo)
|
return nil, fmt.Errorf("MOV $%s(%s): unsupported immediate symbol reference (only SB is supported)", src.Imm.Sym.Name, src.Imm.Sym.Pseudo)
|
||||||
@@ -562,7 +702,7 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
|||||||
// Memory → register (load).
|
// Memory → register (load).
|
||||||
if isMemOperand(src) && !isMemOperand(dst) {
|
if isMemOperand(src) && !isMemOperand(dst) {
|
||||||
rd := regFromOperand(dst)
|
rd := regFromOperand(dst)
|
||||||
// MOV sym(SB), rd — load from static data.
|
// MOV sym(SB), rd, load from static data.
|
||||||
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" {
|
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" {
|
||||||
if rd < 0 {
|
if rd < 0 {
|
||||||
return nil, fmt.Errorf("MOV sym(SB): invalid destination register")
|
return nil, fmt.Errorf("MOV sym(SB): invalid destination register")
|
||||||
@@ -573,14 +713,13 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
|||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("MOV load: invalid operand")
|
return nil, fmt.Errorf("MOV load: invalid operand")
|
||||||
}
|
}
|
||||||
word := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rs1, off)
|
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), false), false, rd, rs1, off), nil
|
||||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register → memory (store).
|
// Register → memory (store).
|
||||||
if !isMemOperand(src) && isMemOperand(dst) {
|
if !isMemOperand(src) && isMemOperand(dst) {
|
||||||
rs2 := regFromOperand(src)
|
rs2 := regFromOperand(src)
|
||||||
// MOV rd, sym(SB) — store to static data.
|
// MOV rd, sym(SB), store to static data.
|
||||||
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
|
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
|
||||||
if rs2 < 0 {
|
if rs2 < 0 {
|
||||||
return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register")
|
return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register")
|
||||||
@@ -591,22 +730,108 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
|||||||
if rs2 < 0 || rs1 < 0 {
|
if rs2 < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("MOV store: invalid operand")
|
return nil, fmt.Errorf("MOV store: invalid operand")
|
||||||
}
|
}
|
||||||
word := riscvSType(riscvEnc{0x23, 0x3, 0x00}, rs1, rs2, off)
|
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), true), true, rs2, rs1, off), nil
|
||||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register → register (ADDI $0, src, dst).
|
// Register → register: MOVD/MOVF are FP moves (fsgnj with rs2 = rs1),
|
||||||
|
// everything else is ADDI $0, src, dst.
|
||||||
{
|
{
|
||||||
rs1 := regFromOperand(src)
|
rs1 := regFromOperand(src)
|
||||||
rd := regFromOperand(dst)
|
rd := regFromOperand(dst)
|
||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("MOV: invalid register operand")
|
return nil, fmt.Errorf("MOV: invalid register operand")
|
||||||
}
|
}
|
||||||
|
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
||||||
|
if mnem == "MOVD" || mnem == "MOVF" {
|
||||||
|
op := uint32(0x20000053) // FSGNJ.S
|
||||||
|
if mnem == "MOVD" {
|
||||||
|
op = 0x22000053 // FSGNJ.D
|
||||||
|
}
|
||||||
|
return wordLE(op | uint32(rs1)<<15 | uint32(rs1)<<20 | uint32(rd)<<7), nil
|
||||||
|
}
|
||||||
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
|
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
|
||||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// riscvMovEnc returns the load (store=false) or store (store=true) opcode for
|
||||||
|
// a MOV-family mnemonic: the suffix selects the access width, MOVD and MOVF
|
||||||
|
// select the FP load/store opcodes, and bare MOV is the 64-bit integer form.
|
||||||
|
func riscvMovEnc(mnem string, store bool) riscvEnc {
|
||||||
|
if store {
|
||||||
|
switch mnem {
|
||||||
|
case "MOVB":
|
||||||
|
return riscvEnc{0x23, 0x0, 0x00} // SB
|
||||||
|
case "MOVH":
|
||||||
|
return riscvEnc{0x23, 0x1, 0x00} // SH
|
||||||
|
case "MOVW":
|
||||||
|
return riscvEnc{0x23, 0x2, 0x00} // SW
|
||||||
|
case "MOVF":
|
||||||
|
return riscvEnc{0x27, 0x2, 0x00} // FSW
|
||||||
|
case "MOVD":
|
||||||
|
return riscvEnc{0x27, 0x3, 0x00} // FSD
|
||||||
|
}
|
||||||
|
return riscvEnc{0x23, 0x3, 0x00} // SD
|
||||||
|
}
|
||||||
|
switch mnem {
|
||||||
|
case "MOVB":
|
||||||
|
return riscvEnc{0x03, 0x0, 0x00} // LB
|
||||||
|
case "MOVBU":
|
||||||
|
return riscvEnc{0x03, 0x4, 0x00} // LBU
|
||||||
|
case "MOVH":
|
||||||
|
return riscvEnc{0x03, 0x1, 0x00} // LH
|
||||||
|
case "MOVHU":
|
||||||
|
return riscvEnc{0x03, 0x5, 0x00} // LHU
|
||||||
|
case "MOVW":
|
||||||
|
return riscvEnc{0x03, 0x2, 0x00} // LW
|
||||||
|
case "MOVWU":
|
||||||
|
return riscvEnc{0x03, 0x6, 0x00} // LWU
|
||||||
|
case "MOVF":
|
||||||
|
return riscvEnc{0x07, 0x2, 0x00} // FLW
|
||||||
|
case "MOVD":
|
||||||
|
return riscvEnc{0x07, 0x3, 0x00} // FLD
|
||||||
|
}
|
||||||
|
return riscvEnc{0x03, 0x3, 0x00} // LD
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvFrameMemOp encodes a register-relative load (store=false, I-type
|
||||||
|
// width 0x03) or store (store=true, S-type width 0x23) of the 64-bit width
|
||||||
|
// at off(rs1). Offsets beyond the signed 12-bit range materialise the
|
||||||
|
// address in X31 first: LUI hi (the rounding split), then ADD X31, rs1,
|
||||||
|
// matching the toolchain's large-frame addressing; the access uses the
|
||||||
|
// sign-extended low part, which always fits.
|
||||||
|
func riscvFrameMemOp(enc riscvEnc, store bool, reg, rs1 int, off int32) []byte {
|
||||||
|
if fits12(off) {
|
||||||
|
var word uint32
|
||||||
|
if store {
|
||||||
|
word = riscvSType(enc, rs1, reg, off)
|
||||||
|
} else {
|
||||||
|
word = riscvIType(enc, reg, rs1, off)
|
||||||
|
}
|
||||||
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||||
|
}
|
||||||
|
lo := off - (splitHi(off) << 12)
|
||||||
|
out := riscvAddressInX31WithBase(off, rs1)
|
||||||
|
var word uint32
|
||||||
|
if store {
|
||||||
|
word = riscvSType(enc, 31, reg, lo)
|
||||||
|
} else {
|
||||||
|
word = riscvIType(enc, reg, 31, lo)
|
||||||
|
}
|
||||||
|
return append(out, wordLE(word)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvFrameMemSize returns the encoded size of a frame-relative MOV for the
|
||||||
|
// layout pass: 4 bytes when the offset fits, otherwise the X31
|
||||||
|
// materialisation plus the access.
|
||||||
|
func riscvFrameMemSize(op *ast.Operand, fi riscvFrameInfo) int {
|
||||||
|
rs1, off := memFromOperandWithFrame(op, fi)
|
||||||
|
if fits12(off) {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return len(riscvAddressInX31WithBase(off, rs1)) + 4
|
||||||
|
}
|
||||||
|
|
||||||
// encodeRISCVLoadImm encodes loading an immediate into a register (MOV $imm,
|
// encodeRISCVLoadImm encodes loading an immediate into a register (MOV $imm,
|
||||||
// rd), matching the toolchain's instructionsForMOVConst. For 12-bit
|
// rd), matching the toolchain's instructionsForMOVConst. For 12-bit
|
||||||
// immediates it emits ADDI $imm, ZERO, rd (compressed to C.LI when it fits
|
// immediates it emits ADDI $imm, ZERO, rd (compressed to C.LI when it fits
|
||||||
@@ -821,25 +1046,34 @@ func word16(w uint16) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
// Plan 9: JALR rs1, rd (2 regs), JALR rd, offset(rs1) (the trampoline
|
||||||
|
// form), or JALR offset(rs1) (memory → rd=X1).
|
||||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||||
ops := instr.Operands
|
ops := instr.Operands
|
||||||
|
// JALR rd, offset(rs1): the memory operand's base is the jump-target
|
||||||
|
// register, not the destination.
|
||||||
|
if len(ops) == 2 && isMemOperand(ops[1]) {
|
||||||
|
rd := regFromOperand(ops[0])
|
||||||
|
rs1, imm := memFromOperandWithFrame(ops[1], fi)
|
||||||
|
if rd < 0 || rs1 < 0 {
|
||||||
|
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||||
|
}
|
||||||
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, imm)), nil
|
||||||
|
}
|
||||||
if len(ops) == 2 {
|
if len(ops) == 2 {
|
||||||
rs1 := regFromOperand(ops[0])
|
rs1 := regFromOperand(ops[0])
|
||||||
rd := regFromOperand(ops[1])
|
rd := regFromOperand(ops[1])
|
||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||||
}
|
}
|
||||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)), nil
|
||||||
return wordLE(word), nil
|
|
||||||
}
|
}
|
||||||
if len(ops) == 1 {
|
if len(ops) == 1 {
|
||||||
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
||||||
if rs1 < 0 {
|
if rs1 < 0 {
|
||||||
return nil, fmt.Errorf("JALR: invalid memory operand")
|
return nil, fmt.Errorf("JALR: invalid memory operand")
|
||||||
}
|
}
|
||||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)), nil
|
||||||
return wordLE(word), nil
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
||||||
}
|
}
|
||||||
@@ -1006,7 +1240,7 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "ADDW", "SUBW":
|
case "ADDW", "SUBW":
|
||||||
// C.ADDW (0x27,1) / C.SUBW (0x27,0) — CA-type, prime regs.
|
// C.ADDW (0x27,1) / C.SUBW (0x27,0), CA-type, prime regs.
|
||||||
if len(ops) == 3 {
|
if len(ops) == 3 {
|
||||||
funct2 := uint32(0x0)
|
funct2 := uint32(0x0)
|
||||||
if mnem == "ADDW" {
|
if mnem == "ADDW" {
|
||||||
@@ -1314,7 +1548,7 @@ func suggestLabel(target string, offsets map[string]int) string {
|
|||||||
}
|
}
|
||||||
// Only suggest if the distance is small enough.
|
// Only suggest if the distance is small enough.
|
||||||
if bestDist <= 3 && bestDist < len(target)/2+1 {
|
if bestDist <= 3 && bestDist < len(target)/2+1 {
|
||||||
return fmt.Sprintf(" — did you mean %q?", best)
|
return fmt.Sprintf("; did you mean %q?", best)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -154,7 +154,7 @@ type riscvEnc struct {
|
|||||||
|
|
||||||
// riscvInstrTable maps RISC-V mnemonics to their encoding.
|
// riscvInstrTable maps RISC-V mnemonics to their encoding.
|
||||||
var riscvInstrTable = map[string]riscvEnc{
|
var riscvInstrTable = map[string]riscvEnc{
|
||||||
// RV64I — R-type arithmetic/logic.
|
// RV64I, R-type arithmetic/logic.
|
||||||
"ADD": {0x33, 0x0, 0x00},
|
"ADD": {0x33, 0x0, 0x00},
|
||||||
"SUB": {0x33, 0x0, 0x20},
|
"SUB": {0x33, 0x0, 0x20},
|
||||||
"SLL": {0x33, 0x1, 0x00},
|
"SLL": {0x33, 0x1, 0x00},
|
||||||
@@ -165,20 +165,20 @@ var riscvInstrTable = map[string]riscvEnc{
|
|||||||
"SRA": {0x33, 0x5, 0x20},
|
"SRA": {0x33, 0x5, 0x20},
|
||||||
"OR": {0x33, 0x6, 0x00},
|
"OR": {0x33, 0x6, 0x00},
|
||||||
"AND": {0x33, 0x7, 0x00},
|
"AND": {0x33, 0x7, 0x00},
|
||||||
// RV64I — 32-bit variants (W suffix).
|
// RV64I, 32-bit variants (W suffix).
|
||||||
"ADDW": {0x3B, 0x0, 0x00},
|
"ADDW": {0x3B, 0x0, 0x00},
|
||||||
"SUBW": {0x3B, 0x0, 0x20},
|
"SUBW": {0x3B, 0x0, 0x20},
|
||||||
"SLLW": {0x3B, 0x1, 0x00},
|
"SLLW": {0x3B, 0x1, 0x00},
|
||||||
"SRLW": {0x3B, 0x5, 0x00},
|
"SRLW": {0x3B, 0x5, 0x00},
|
||||||
"SRAW": {0x3B, 0x5, 0x20},
|
"SRAW": {0x3B, 0x5, 0x20},
|
||||||
// RV64I — I-type shift-immediate (shamt in rs2 field).
|
// RV64I, I-type shift-immediate (shamt in rs2 field).
|
||||||
"SLLI": {0x13, 0x1, 0x00},
|
"SLLI": {0x13, 0x1, 0x00},
|
||||||
"SRLI": {0x13, 0x5, 0x00},
|
"SRLI": {0x13, 0x5, 0x00},
|
||||||
"SRAI": {0x13, 0x5, 0x20},
|
"SRAI": {0x13, 0x5, 0x20},
|
||||||
"SLLIW": {0x1B, 0x1, 0x00},
|
"SLLIW": {0x1B, 0x1, 0x00},
|
||||||
"SRLIW": {0x1B, 0x5, 0x00},
|
"SRLIW": {0x1B, 0x5, 0x00},
|
||||||
"SRAIW": {0x1B, 0x5, 0x20},
|
"SRAIW": {0x1B, 0x5, 0x20},
|
||||||
// RV64M — multiply/divide.
|
// RV64M, multiply/divide.
|
||||||
"MUL": {0x33, 0x0, 0x01},
|
"MUL": {0x33, 0x0, 0x01},
|
||||||
"MULH": {0x33, 0x1, 0x01},
|
"MULH": {0x33, 0x1, 0x01},
|
||||||
"MULHSU": {0x33, 0x2, 0x01},
|
"MULHSU": {0x33, 0x2, 0x01},
|
||||||
@@ -187,13 +187,13 @@ var riscvInstrTable = map[string]riscvEnc{
|
|||||||
"DIVU": {0x33, 0x5, 0x01},
|
"DIVU": {0x33, 0x5, 0x01},
|
||||||
"REM": {0x33, 0x6, 0x01},
|
"REM": {0x33, 0x6, 0x01},
|
||||||
"REMU": {0x33, 0x7, 0x01},
|
"REMU": {0x33, 0x7, 0x01},
|
||||||
// RV64M — 32-bit variants.
|
// RV64M, 32-bit variants.
|
||||||
"MULW": {0x3B, 0x0, 0x01},
|
"MULW": {0x3B, 0x0, 0x01},
|
||||||
"DIVW": {0x3B, 0x4, 0x01},
|
"DIVW": {0x3B, 0x4, 0x01},
|
||||||
"DIVUW": {0x3B, 0x5, 0x01},
|
"DIVUW": {0x3B, 0x5, 0x01},
|
||||||
"REMW": {0x3B, 0x6, 0x01},
|
"REMW": {0x3B, 0x6, 0x01},
|
||||||
"REMUW": {0x3B, 0x7, 0x01},
|
"REMUW": {0x3B, 0x7, 0x01},
|
||||||
// RV64I — I-type arithmetic.
|
// RV64I, I-type arithmetic.
|
||||||
"ADDI": {0x13, 0x0, 0x00},
|
"ADDI": {0x13, 0x0, 0x00},
|
||||||
"ADDIW": {0x1B, 0x0, 0x00},
|
"ADDIW": {0x1B, 0x0, 0x00},
|
||||||
"SLTI": {0x13, 0x2, 0x00},
|
"SLTI": {0x13, 0x2, 0x00},
|
||||||
@@ -228,10 +228,10 @@ var riscvInstrTable = map[string]riscvEnc{
|
|||||||
"ECALL": {0x73, 0x0, 0x00},
|
"ECALL": {0x73, 0x0, 0x00},
|
||||||
"EBREAK": {0x73, 0x0, 0x00},
|
"EBREAK": {0x73, 0x0, 0x00},
|
||||||
"FENCE": {0x0F, 0x0, 0x00},
|
"FENCE": {0x0F, 0x0, 0x00},
|
||||||
// JALR — indirect jump/call (I-type).
|
// JALR, indirect jump/call (I-type).
|
||||||
"JALR": {0x67, 0x0, 0x00},
|
"JALR": {0x67, 0x0, 0x00},
|
||||||
|
|
||||||
// RV64A — atomics (AMO opcode 0x2F).
|
// RV64A, atomics (AMO opcode 0x2F).
|
||||||
// funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27].
|
// funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27].
|
||||||
"AMOSWAPW": {0x2F, 0x2, 0x01 << 2},
|
"AMOSWAPW": {0x2F, 0x2, 0x01 << 2},
|
||||||
"AMOSWAPD": {0x2F, 0x3, 0x01 << 2},
|
"AMOSWAPD": {0x2F, 0x3, 0x01 << 2},
|
||||||
@@ -252,7 +252,7 @@ var riscvInstrTable = map[string]riscvEnc{
|
|||||||
"AMOMINUW": {0x2F, 0x2, 0x18 << 2},
|
"AMOMINUW": {0x2F, 0x2, 0x18 << 2},
|
||||||
"AMOMINUD": {0x2F, 0x3, 0x18 << 2},
|
"AMOMINUD": {0x2F, 0x3, 0x18 << 2},
|
||||||
|
|
||||||
// RV64F/D — floating-point arithmetic.
|
// RV64F/D, floating-point arithmetic.
|
||||||
"FADDS": {0x53, 0x0, 0x00},
|
"FADDS": {0x53, 0x0, 0x00},
|
||||||
"FSUBS": {0x53, 0x0, 0x04},
|
"FSUBS": {0x53, 0x0, 0x04},
|
||||||
"FMULS": {0x53, 0x0, 0x08},
|
"FMULS": {0x53, 0x0, 0x08},
|
||||||
@@ -274,13 +274,13 @@ var riscvInstrTable = map[string]riscvEnc{
|
|||||||
"FMIND": {0x53, 0x0, 0x15},
|
"FMIND": {0x53, 0x0, 0x15},
|
||||||
"FMAXD": {0x53, 0x1, 0x15},
|
"FMAXD": {0x53, 0x1, 0x15},
|
||||||
|
|
||||||
// RV64A — load-reserved / store-conditional (funct5 0x02 / 0x03).
|
// RV64A, load-reserved / store-conditional (funct5 0x02 / 0x03).
|
||||||
"LRW": {0x2F, 0x2, 0x02 << 2},
|
"LRW": {0x2F, 0x2, 0x02 << 2},
|
||||||
"LRD": {0x2F, 0x3, 0x02 << 2},
|
"LRD": {0x2F, 0x3, 0x02 << 2},
|
||||||
"SCW": {0x2F, 0x2, 0x03 << 2},
|
"SCW": {0x2F, 0x2, 0x03 << 2},
|
||||||
"SCD": {0x2F, 0x3, 0x03 << 2},
|
"SCD": {0x2F, 0x3, 0x03 << 2},
|
||||||
|
|
||||||
// FP compare — result in integer register (funct7 0x50/0x51).
|
// FP compare, result in integer register (funct7 0x50/0x51).
|
||||||
"FEQS": {0x53, 0x2, 0x50},
|
"FEQS": {0x53, 0x2, 0x50},
|
||||||
"FLTS": {0x53, 0x1, 0x50},
|
"FLTS": {0x53, 0x1, 0x50},
|
||||||
"FLES": {0x53, 0x0, 0x50},
|
"FLES": {0x53, 0x0, 0x50},
|
||||||
@@ -328,6 +328,8 @@ var riscvCvtTable = map[string]riscvCvtEnc{
|
|||||||
"FCVTSWU": {0x68, 0x1, 0x53}, // uint32 → float32
|
"FCVTSWU": {0x68, 0x1, 0x53}, // uint32 → float32
|
||||||
"FCVTSL": {0x68, 0x2, 0x53}, // int64 → float32
|
"FCVTSL": {0x68, 0x2, 0x53}, // int64 → float32
|
||||||
"FCVTSLU": {0x68, 0x3, 0x53}, // uint64 → float32
|
"FCVTSLU": {0x68, 0x3, 0x53}, // uint64 → float32
|
||||||
|
"FCLASSS": {0x70, 0x0, 0x53}, // classify float32 → GPR mask
|
||||||
|
"FCLASSD": {0x70, 0x0, 0x53}, // classify float64 → GPR mask
|
||||||
"FCVTDW": {0x69, 0x0, 0x53}, // int32 → float64
|
"FCVTDW": {0x69, 0x0, 0x53}, // int32 → float64
|
||||||
"FCVTDWU": {0x69, 0x1, 0x53}, // uint32 → float64
|
"FCVTDWU": {0x69, 0x1, 0x53}, // uint32 → float64
|
||||||
"FCVTDL": {0x69, 0x2, 0x53}, // int64 → float64
|
"FCVTDL": {0x69, 0x2, 0x53}, // int64 → float64
|
||||||
@@ -442,10 +444,10 @@ func riscvJType(rd int, offset int32) uint32 {
|
|||||||
// ---- RVC (compressed) encoding helpers ----
|
// ---- RVC (compressed) encoding helpers ----
|
||||||
|
|
||||||
// isRVCIntReg reports whether a register number can be encoded in the 3-bit
|
// isRVCIntReg reports whether a register number can be encoded in the 3-bit
|
||||||
// prime register field used by compressed instructions (x8–x15).
|
// prime register field used by compressed instructions (x8-x15).
|
||||||
func isRVCIntReg(r int) bool { return r >= 8 && r <= 15 }
|
func isRVCIntReg(r int) bool { return r >= 8 && r <= 15 }
|
||||||
|
|
||||||
// rvcReg3 returns the 3-bit encoding for registers x8–x15 (0–7).
|
// rvcReg3 returns the 3-bit encoding for registers x8-x15 (0-7).
|
||||||
func rvcReg3(r int) uint32 { return uint32(r - 8) }
|
func rvcReg3(r int) uint32 { return uint32(r - 8) }
|
||||||
|
|
||||||
// rvcCR encodes a CR-type (register) compressed instruction.
|
// rvcCR encodes a CR-type (register) compressed instruction.
|
||||||
@@ -455,7 +457,7 @@ func rvcCR(funct4, rd, rs2 uint32) uint16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rvcCI encodes a CI-type (immediate) compressed instruction.
|
// rvcCI encodes a CI-type (immediate) compressed instruction.
|
||||||
// Used for C.ADDI, C.LI, C.LUI, C.ADDIW — linear 6-bit immediate.
|
// Used for C.ADDI, C.LI, C.LUI, C.ADDIW, linear 6-bit immediate.
|
||||||
func rvcCI(funct3, rd uint32, imm uint32) uint16 {
|
func rvcCI(funct3, rd uint32, imm uint32) uint16 {
|
||||||
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1)
|
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ TEXT ·cmp(SB), NOSPLIT, $0
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRISCV_forwardBranch(t *testing.T) {
|
func TestRISCV_forwardBranch(t *testing.T) {
|
||||||
// Forward label reference — must not fail.
|
// Forward label reference; must not fail.
|
||||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
TEXT ·fwd(SB), NOSPLIT, $0
|
TEXT ·fwd(SB), NOSPLIT, $0
|
||||||
ADDI $1, X10, X10
|
ADDI $1, X10, X10
|
||||||
@@ -761,3 +761,24 @@ sub:
|
|||||||
t.Error("expected error for CALL to local label, got nil")
|
t.Error("expected error for CALL to local label, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRISCVIndirectBranch pins the indirect branch encodings: JMP (X5) is the
|
||||||
|
// toolchain's JALR X0, 0(X5), and the trampoline form JALR rd, offset(rs1)
|
||||||
|
// takes its destination from the first operand (regression: the base
|
||||||
|
// register was once read as the destination, silently jumping to X0).
|
||||||
|
func TestRISCVIndirectBranch(t *testing.T) {
|
||||||
|
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0-0
|
||||||
|
JMP (X5)
|
||||||
|
JALR X0, 0(X6)
|
||||||
|
JALR X28, 0(X9)
|
||||||
|
RET
|
||||||
|
`)
|
||||||
|
code := assembleRISCVHelper(t, fn)
|
||||||
|
wantWords(t, code,
|
||||||
|
0x00028067, // jalr x0, 5(x0), 0
|
||||||
|
0x00030067, // jalr x0, 6(x0), 0
|
||||||
|
0x00048e67, // jalr x28, 9(x0), 0
|
||||||
|
0x00008067, // jalr x0, 1(x0), 0 (RET)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+191
-12
@@ -32,6 +32,13 @@ import (
|
|||||||
// riscvFrameInfo holds the frame layout derived from a TEXT directive.
|
// riscvFrameInfo holds the frame layout derived from a TEXT directive.
|
||||||
type riscvFrameInfo struct {
|
type riscvFrameInfo struct {
|
||||||
autosize int // the real SP adjustment (locals + saved LR)
|
autosize int // the real SP adjustment (locals + saved LR)
|
||||||
|
|
||||||
|
// Stack-split guard state: the toolchain emits the check for every
|
||||||
|
// non-NOSPLIT function whose autosize is nonzero (a zero autosize is
|
||||||
|
// "effectively NOSPLIT"); unlike amd64 and arm64 there is no leaf
|
||||||
|
// auto-NOSPLIT.
|
||||||
|
needSplit bool
|
||||||
|
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
|
||||||
}
|
}
|
||||||
|
|
||||||
// riscvComputeFrame derives the frame layout for a TEXT function.
|
// riscvComputeFrame derives the frame layout for a TEXT function.
|
||||||
@@ -40,11 +47,34 @@ func riscvComputeFrame(t *ast.Text) riscvFrameInfo {
|
|||||||
if frame != 0 || !riscvIsLeaf(t) {
|
if frame != 0 || !riscvIsLeaf(t) {
|
||||||
// FixedFrameSize = 8: space for the saved link register. A
|
// FixedFrameSize = 8: space for the saved link register. A
|
||||||
// zero-frame non-leaf function still opens an 8-byte frame for LR.
|
// zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||||
return riscvFrameInfo{autosize: frame + 8}
|
autosize := frame + 8
|
||||||
|
fi := riscvFrameInfo{autosize: autosize}
|
||||||
|
if !hasNoSplitFlag(t) {
|
||||||
|
fi.needSplit = true
|
||||||
|
switch {
|
||||||
|
case autosize <= stackSmall:
|
||||||
|
fi.splitClass = 0
|
||||||
|
case autosize <= stackBig:
|
||||||
|
fi.splitClass = 1
|
||||||
|
default:
|
||||||
|
fi.splitClass = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fi
|
||||||
}
|
}
|
||||||
return riscvFrameInfo{}
|
return riscvFrameInfo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasNoSplitFlag reports whether the TEXT directive carries NOSPLIT.
|
||||||
|
func hasNoSplitFlag(t *ast.Text) bool {
|
||||||
|
for _, f := range t.Flags {
|
||||||
|
if strings.EqualFold(f, "NOSPLIT") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// riscvIsLeaf reports whether a function contains no call instructions.
|
// riscvIsLeaf reports whether a function contains no call instructions.
|
||||||
// CALL always links; JAL/JALR link only when their destination register is
|
// CALL always links; JAL/JALR link only when their destination register is
|
||||||
// the link register (X1), matching cmd/internal/obj/riscv's containsCall.
|
// the link register (X1), matching cmd/internal/obj/riscv's containsCall.
|
||||||
@@ -58,17 +88,24 @@ func riscvIsLeaf(t *ast.Text) bool {
|
|||||||
case "CALL":
|
case "CALL":
|
||||||
return false
|
return false
|
||||||
case "JAL":
|
case "JAL":
|
||||||
// JAL rd, target — a call only when rd is the link register.
|
// JAL rd, target, a call only when rd is the link register.
|
||||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[0]) == 1 {
|
if len(in.Operands) >= 2 && regFromOperand(in.Operands[0]) == 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case "JALR":
|
case "JALR":
|
||||||
// JALR rs1, rd — a call when rd is X1; JALR offset(rs1) always
|
// JALR rd, offset(rs1) links when the destination register (the
|
||||||
// links to X1.
|
// first operand) is X1; JALR rs1, rd links when the second
|
||||||
|
// register is X1; JALR offset(rs1) always links to X1.
|
||||||
if len(in.Operands) == 1 {
|
if len(in.Operands) == 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[1]) == 1 {
|
if isMemOperand(in.Operands[1]) {
|
||||||
|
if regFromOperand(in.Operands[0]) == 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if regFromOperand(in.Operands[1]) == 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,28 +121,99 @@ func riscvPrologue(fi riscvFrameInfo) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var out []byte
|
var out []byte
|
||||||
// MOV LR, -autosize(SP) — SD X1, -autosize(X2). The negative offset is
|
// MOV LR, -autosize(SP), SD X1, -autosize(X2). The negative offset is
|
||||||
// not compressible to C.SDSP (unsigned), so it stays 4 bytes.
|
// not compressible to C.SDSP (unsigned), so it stays 4 bytes. Beyond
|
||||||
|
// the imm12 range the toolchain materialises the address in X31.
|
||||||
|
if fits12(int32(-fi.autosize)) {
|
||||||
out = append(out, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 2, 1, int32(-fi.autosize)))...)
|
out = append(out, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 2, 1, int32(-fi.autosize)))...)
|
||||||
// ADDI $-autosize, SP, SP — open the frame (C.ADDI when it fits).
|
} else {
|
||||||
|
out = append(out, riscvAddressInX31(int32(-fi.autosize))...)
|
||||||
|
lo := int32(-fi.autosize) - (splitHi(int32(-fi.autosize)) << 12)
|
||||||
|
out = append(out, wordLE(riscvSType(riscvEnc{0x23, 0x3, 0x00}, 31, 1, lo))...)
|
||||||
|
}
|
||||||
|
// ADDI $-autosize, SP, SP, open the frame (C.ADDI when it fits; X31
|
||||||
|
// materialisation beyond imm12).
|
||||||
|
if fits12(int32(-fi.autosize)) {
|
||||||
out = append(out, riscvSPAdjust(int32(-fi.autosize))...)
|
out = append(out, riscvSPAdjust(int32(-fi.autosize))...)
|
||||||
// MOV LR, 0(SP) — SD X1, 0(X2) → C.SDSP X1, 0.
|
} else {
|
||||||
|
out = append(out, riscvAddToSP(int32(-fi.autosize))...)
|
||||||
|
}
|
||||||
|
// MOV LR, 0(SP), SD X1, 0(X2) → C.SDSP X1, 0.
|
||||||
c := rvcSSP(0x7, 1, 0)
|
c := rvcSSP(0x7, 1, 0)
|
||||||
out = append(out, byte(c), byte(c>>8))
|
out = append(out, byte(c), byte(c>>8))
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fits12(v int32) bool { return v >= -2048 && v <= 2047 }
|
||||||
|
|
||||||
|
// splitHi returns the LUI half of the hi/lo split of v (what remains is the
|
||||||
|
// sign-extended 12-bit low part).
|
||||||
|
func splitHi(v int32) int32 {
|
||||||
|
_, high := splitRISCV32Imm(v)
|
||||||
|
return high
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvAddressInX31 materialises hi(v) into X31 against the stack pointer,
|
||||||
|
// matching the toolchain's large-frame addressing: C.LUI (or LUI) X31, hi;
|
||||||
|
// C.ADD (or ADD) X31, SP.
|
||||||
|
func riscvAddressInX31(v int32) []byte {
|
||||||
|
return riscvAddressInX31WithBase(v, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvAddressInX31WithBase materialises hi(v) into X31 against an arbitrary
|
||||||
|
// base register: LUI (or C.LUI) X31, hi; C.ADD X31, rs1. The CR rs2 field
|
||||||
|
// carries the full 5-bit register, so the compressed form is always
|
||||||
|
// available.
|
||||||
|
func riscvAddressInX31WithBase(v int32, rs1 int) []byte {
|
||||||
|
hi := splitHi(v)
|
||||||
|
var out []byte
|
||||||
|
if hi >= -32 && hi <= 31 {
|
||||||
|
c := rvcCI(0x3, 31, uint32(hi)&0x3F)
|
||||||
|
out = append(out, byte(c), byte(c>>8))
|
||||||
|
} else {
|
||||||
|
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, 31, hi<<12))...)
|
||||||
|
}
|
||||||
|
c := rvcCR(0x9, 31, uint32(rs1))
|
||||||
|
return append(out, byte(c), byte(c>>8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvAddToSP adds v to SP through X31 for the values imm12 cannot carry:
|
||||||
|
// C.LUI X31, hi; C.ADDIW X31, lo; C.ADD SP, X31 (the toolchain's form).
|
||||||
|
func riscvAddToSP(v int32) []byte {
|
||||||
|
hi := splitHi(v)
|
||||||
|
lo := v - (hi << 12)
|
||||||
|
var out []byte
|
||||||
|
if hi >= -32 && hi <= 31 {
|
||||||
|
c := rvcCI(0x3, 31, uint32(hi)&0x3F)
|
||||||
|
out = append(out, byte(c), byte(c>>8))
|
||||||
|
} else {
|
||||||
|
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, 31, hi<<12))...)
|
||||||
|
}
|
||||||
|
if lo >= -32 && lo <= 31 {
|
||||||
|
c := rvcCI(0x1, 31, uint32(lo)&0x3F)
|
||||||
|
out = append(out, byte(c), byte(c>>8))
|
||||||
|
} else {
|
||||||
|
out = append(out, wordLE(riscvIType(riscvEnc{0x1b, 0x0, 0x00}, 31, 31, lo))...)
|
||||||
|
}
|
||||||
|
c := rvcCR(0x9, 2, 31)
|
||||||
|
return append(out, byte(c), byte(c>>8))
|
||||||
|
}
|
||||||
|
|
||||||
// riscvReturn returns the bytes for a RET: the epilogue (restore LR and
|
// riscvReturn returns the bytes for a RET: the epilogue (restore LR and
|
||||||
// deallocate the frame when present) followed by the uncompressed JALR X0,
|
// deallocate the frame when present) followed by the uncompressed JALR X0,
|
||||||
// 0(X1) the toolchain emits for RET (it never compresses RET to C.JR).
|
// 0(X1) the toolchain emits for RET (it never compresses RET to C.JR).
|
||||||
func riscvReturn(fi riscvFrameInfo) []byte {
|
func riscvReturn(fi riscvFrameInfo) []byte {
|
||||||
var out []byte
|
var out []byte
|
||||||
if fi.autosize != 0 {
|
if fi.autosize != 0 {
|
||||||
// MOV 0(SP), LR — LD X1, 0(X2) → C.LDSP X1, 0.
|
// MOV 0(SP), LR, LD X1, 0(X2) → C.LDSP X1, 0.
|
||||||
c := rvcLSP(0x3, 1, 0)
|
c := rvcLSP(0x3, 1, 0)
|
||||||
out = append(out, byte(c), byte(c>>8))
|
out = append(out, byte(c), byte(c>>8))
|
||||||
// ADDI $autosize, SP, SP — close the frame (C.ADDI when it fits).
|
// ADDI $autosize, SP, SP, close the frame (C.ADDI when it fits).
|
||||||
|
if fits12(int32(fi.autosize)) {
|
||||||
out = append(out, riscvSPAdjust(int32(fi.autosize))...)
|
out = append(out, riscvSPAdjust(int32(fi.autosize))...)
|
||||||
|
} else {
|
||||||
|
out = append(out, riscvAddToSP(int32(fi.autosize))...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// JALR X0, 0(X1).
|
// JALR X0, 0(X1).
|
||||||
return append(out, wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0))...)
|
return append(out, wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0))...)
|
||||||
@@ -143,7 +251,7 @@ func riscvPrologueSpadjPC(fi riscvFrameInfo) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// riscvReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
// riscvReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
||||||
// (but not including) the final JALR — the point where SP is restored.
|
// (but not including) the final JALR, the point where SP is restored.
|
||||||
func riscvReturnEpilogueLen(fi riscvFrameInfo) int {
|
func riscvReturnEpilogueLen(fi riscvFrameInfo) int {
|
||||||
if fi.autosize == 0 {
|
if fi.autosize == 0 {
|
||||||
return 0
|
return 0
|
||||||
@@ -180,3 +288,74 @@ func riscvResolvePseudo(sym *ast.Symbol, fi riscvFrameInfo) (base int, off int32
|
|||||||
}
|
}
|
||||||
return -1, 0
|
return -1, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// riscvGuardLen returns the byte length of the stack-split guard prefix
|
||||||
|
// including the inline morestack call (zero when the function needs no
|
||||||
|
// guard). Unlike amd64 and arm64, the toolchain places the morestack call
|
||||||
|
// between the guard and the body: the guard branches forward over it.
|
||||||
|
func riscvGuardLen(fi riscvFrameInfo) int {
|
||||||
|
_, reloc := riscvGuard(fi)
|
||||||
|
_ = reloc
|
||||||
|
return len(riscvGuardBytes(fi))
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvGuard emits the stack-split guard prefix with the inline morestack
|
||||||
|
// call: the branch skips forward over JAL X5 and JAL X0 straight into the
|
||||||
|
// body; the JAL X5 carries the R_RISCV_JAL relocation. All offsets are
|
||||||
|
// relative to the guard itself, which sits at function offset 0.
|
||||||
|
func riscvGuard(fi riscvFrameInfo) ([]byte, Reloc) {
|
||||||
|
if !fi.needSplit {
|
||||||
|
return nil, Reloc{}
|
||||||
|
}
|
||||||
|
// MOV 16(g), X6 (g.stackguard0), g = X27.
|
||||||
|
out := wordLE(riscvIType(riscvEnc{0x03, 0x3, 0x00}, 6, 27, 16))
|
||||||
|
jalBack := func() []byte {
|
||||||
|
// JAL X0 back to the function start: it sits right after the JAL X5,
|
||||||
|
// so its displacement is minus the current offset.
|
||||||
|
return wordLE(riscvJType(0, int32(-len(out))))
|
||||||
|
}
|
||||||
|
var reloc Reloc
|
||||||
|
switch fi.splitClass {
|
||||||
|
case 0:
|
||||||
|
// BLTU X6, SP, done (+8: over the CALL and the JMP back)
|
||||||
|
out = append(out, wordLE(riscvBType(riscvEnc{0x63, 0x06, 0x00}, 6, 2, 12))...)
|
||||||
|
call := len(out)
|
||||||
|
reloc = Reloc{Off: call, After: call + 4, Name: "runtime\u00b7morestack_noctxt", Kind: RelRISCVJal}
|
||||||
|
out = append(out, wordLE(riscvJType(5, 0))...)
|
||||||
|
out = append(out, jalBack()...)
|
||||||
|
case 1:
|
||||||
|
// ADDI $-(framesize-StackSmall), SP, X7; BLTU X6, X7, done (+8)
|
||||||
|
off := int32(fi.autosize - stackSmall)
|
||||||
|
out = append(out, wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, 7, 2, -off))...)
|
||||||
|
out = append(out, wordLE(riscvBType(riscvEnc{0x63, 0x06, 0x00}, 6, 7, 12))...)
|
||||||
|
call := len(out)
|
||||||
|
reloc = Reloc{Off: call, After: call + 4, Name: "runtime\u00b7morestack_noctxt", Kind: RelRISCVJal}
|
||||||
|
out = append(out, wordLE(riscvJType(5, 0))...)
|
||||||
|
out = append(out, jalBack()...)
|
||||||
|
default:
|
||||||
|
// MOV $(framesize-StackSmall), X7; BLTU SP, X7, call;
|
||||||
|
// ADD $-(framesize-StackSmall), SP, X7; BLTU X6, X7, call
|
||||||
|
off := int32(fi.autosize - stackSmall)
|
||||||
|
mov := encodeRISCVLoadImm(7, off)
|
||||||
|
out = append(out, mov...)
|
||||||
|
addiLen := riscvItypeImmediateSize("ADDI", -off)
|
||||||
|
out = append(out, wordLE(riscvBType(riscvEnc{0x63, 0x06, 0x00}, 2, 7, int32(addiLen+8)))...)
|
||||||
|
addi, err := encodeRISCVItypeImmediate("ADDI", riscvEnc{0x13, 0x0, 0x00}, 7, 2, -off)
|
||||||
|
if err != nil {
|
||||||
|
addi = nil
|
||||||
|
}
|
||||||
|
out = append(out, addi...)
|
||||||
|
out = append(out, wordLE(riscvBType(riscvEnc{0x63, 0x06, 0x00}, 6, 7, 12))...)
|
||||||
|
call := len(out)
|
||||||
|
reloc = Reloc{Off: call, After: call + 4, Name: "runtime\u00b7morestack_noctxt", Kind: RelRISCVJal}
|
||||||
|
out = append(out, wordLE(riscvJType(5, 0))...)
|
||||||
|
out = append(out, jalBack()...)
|
||||||
|
}
|
||||||
|
return out, reloc
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvGuardBytes emits the guard prefix bytes alone (sizing helper).
|
||||||
|
func riscvGuardBytes(fi riscvFrameInfo) []byte {
|
||||||
|
g, _ := riscvGuard(fi)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|||||||
+43
-43
@@ -36,11 +36,11 @@ const (
|
|||||||
vexNDS3Imm
|
vexNDS3Imm
|
||||||
// vexExtract is the lane-extract form `OP $imm, ysrc, xdst`: ModRM.reg =
|
// vexExtract is the lane-extract form `OP $imm, ysrc, xdst`: ModRM.reg =
|
||||||
// ysrc (op1), ModRM.rm = xdst or memory (op2), imm8 = op0. The YMM
|
// ysrc (op1), ModRM.rm = xdst or memory (op2), imm8 = op0. The YMM
|
||||||
// source lives in the reg field, the destination in r/m — the PEXTR-style
|
// source lives in the reg field, the destination in r/m, the PEXTR-style
|
||||||
// layout. VEXTRACTI128 and VEXTRACTF128 use this shape.
|
// layout. VEXTRACTI128 and VEXTRACTF128 use this shape.
|
||||||
vexExtract
|
vexExtract
|
||||||
// vexRMRev is the reversed two-operand form `OP src, dst` with the source
|
// vexRMRev is the reversed two-operand form `OP src, dst` with the source
|
||||||
// in ModRM.reg and the destination in r/m — the layout of the EVEX
|
// in ModRM.reg and the destination in r/m, the layout of the EVEX
|
||||||
// narrowing stores (VPMOVDW, VPMOVQD).
|
// narrowing stores (VPMOVDW, VPMOVQD).
|
||||||
vexRMRev
|
vexRMRev
|
||||||
// vexRMSrcLen is the two-operand conversion form `OP src, dst` whose
|
// vexRMSrcLen is the two-operand conversion form `OP src, dst` whose
|
||||||
@@ -68,7 +68,7 @@ type vexSpec struct {
|
|||||||
// incrementally; every entry is covered by a byte-for-byte ground-truth test
|
// incrementally; every entry is covered by a byte-for-byte ground-truth test
|
||||||
// against the Go assembler.
|
// against the Go assembler.
|
||||||
var vexTable = map[string]vexSpec{
|
var vexTable = map[string]vexSpec{
|
||||||
// VEX.128/256.66.0F.WIG — integer arithmetic / logic / compare.
|
// VEX.128/256.66.0F.WIG, integer arithmetic / logic / compare.
|
||||||
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3},
|
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3},
|
||||||
"VPADDQ": {1, 0xD4, 0, 1, -1, vexNDS3},
|
"VPADDQ": {1, 0xD4, 0, 1, -1, vexNDS3},
|
||||||
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3},
|
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3},
|
||||||
@@ -82,7 +82,7 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VPUNPCKHDQ": {1, 0x6A, 0, 1, -1, vexNDS3},
|
"VPUNPCKHDQ": {1, 0x6A, 0, 1, -1, vexNDS3},
|
||||||
"VPUNPCKLQDQ": {1, 0x6C, 0, 1, -1, vexNDS3},
|
"VPUNPCKLQDQ": {1, 0x6C, 0, 1, -1, vexNDS3},
|
||||||
"VPACKSSDW": {1, 0x6B, 0, 1, -1, vexNDS3},
|
"VPACKSSDW": {1, 0x6B, 0, 1, -1, vexNDS3},
|
||||||
// VEX.256.66.0F38.W0 — dword permute (three-operand NDS form).
|
// VEX.256.66.0F38.W0, dword permute (three-operand NDS form).
|
||||||
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3},
|
"VPERMD": {2, 0x36, 0, 1, -1, vexNDS3},
|
||||||
// VEX.128/256.66.0F38.WIG.
|
// VEX.128/256.66.0F38.WIG.
|
||||||
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3},
|
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3},
|
||||||
@@ -90,14 +90,14 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3},
|
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3},
|
||||||
"VPCMPGTQ": {2, 0x37, 0, 1, -1, vexNDS3},
|
"VPCMPGTQ": {2, 0x37, 0, 1, -1, vexNDS3},
|
||||||
|
|
||||||
// VEX.128/256.66.0F.WIG — packed double-precision arithmetic / logic.
|
// VEX.128/256.66.0F.WIG, packed double-precision arithmetic / logic.
|
||||||
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
|
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
|
||||||
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
|
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
|
||||||
"VSUBPD": {1, 0x5C, 0, 1, -1, vexNDS3},
|
"VSUBPD": {1, 0x5C, 0, 1, -1, vexNDS3},
|
||||||
"VDIVPD": {1, 0x5E, 0, 1, -1, vexNDS3},
|
"VDIVPD": {1, 0x5E, 0, 1, -1, vexNDS3},
|
||||||
"VMINPD": {1, 0x5D, 0, 1, -1, vexNDS3},
|
"VMINPD": {1, 0x5D, 0, 1, -1, vexNDS3},
|
||||||
"VMAXPD": {1, 0x5F, 0, 1, -1, vexNDS3},
|
"VMAXPD": {1, 0x5F, 0, 1, -1, vexNDS3},
|
||||||
// VEX.128/256.0F.WIG — packed single-precision arithmetic.
|
// VEX.128/256.0F.WIG, packed single-precision arithmetic.
|
||||||
"VADDPS": {1, 0x58, 0, 0, -1, vexNDS3},
|
"VADDPS": {1, 0x58, 0, 0, -1, vexNDS3},
|
||||||
"VMULPS": {1, 0x59, 0, 0, -1, vexNDS3},
|
"VMULPS": {1, 0x59, 0, 0, -1, vexNDS3},
|
||||||
"VSUBPS": {1, 0x5C, 0, 0, -1, vexNDS3},
|
"VSUBPS": {1, 0x5C, 0, 0, -1, vexNDS3},
|
||||||
@@ -107,7 +107,7 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VXORPD": {1, 0x57, 0, 1, -1, vexNDS3},
|
"VXORPD": {1, 0x57, 0, 1, -1, vexNDS3},
|
||||||
"VUNPCKHPD": {1, 0x15, 0, 1, -1, vexNDS3},
|
"VUNPCKHPD": {1, 0x15, 0, 1, -1, vexNDS3},
|
||||||
"VUNPCKLPD": {1, 0x14, 0, 1, -1, vexNDS3},
|
"VUNPCKLPD": {1, 0x14, 0, 1, -1, vexNDS3},
|
||||||
// VEX.128.F2.0F.WIG — scalar double-precision arithmetic (the packed
|
// VEX.128.F2.0F.WIG, scalar double-precision arithmetic (the packed
|
||||||
// opcodes with an F2 pp).
|
// opcodes with an F2 pp).
|
||||||
"VADDSD": {1, 0x58, 0, 3, -1, vexNDS3},
|
"VADDSD": {1, 0x58, 0, 3, -1, vexNDS3},
|
||||||
"VSUBSD": {1, 0x5C, 0, 3, -1, vexNDS3},
|
"VSUBSD": {1, 0x5C, 0, 3, -1, vexNDS3},
|
||||||
@@ -115,7 +115,7 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VDIVSD": {1, 0x5E, 0, 3, -1, vexNDS3},
|
"VDIVSD": {1, 0x5E, 0, 3, -1, vexNDS3},
|
||||||
"VMINSD": {1, 0x5D, 0, 3, -1, vexNDS3},
|
"VMINSD": {1, 0x5D, 0, 3, -1, vexNDS3},
|
||||||
"VMAXSD": {1, 0x5F, 0, 3, -1, vexNDS3},
|
"VMAXSD": {1, 0x5F, 0, 3, -1, vexNDS3},
|
||||||
// VEX.128.F3.0F.WIG — scalar single-precision arithmetic (the packed
|
// VEX.128.F3.0F.WIG, scalar single-precision arithmetic (the packed
|
||||||
// opcodes with an F3 pp).
|
// opcodes with an F3 pp).
|
||||||
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3},
|
"VADDSS": {1, 0x58, 0, 2, -1, vexNDS3},
|
||||||
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3},
|
"VSUBSS": {1, 0x5C, 0, 2, -1, vexNDS3},
|
||||||
@@ -123,10 +123,10 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VDIVSS": {1, 0x5E, 0, 2, -1, vexNDS3},
|
"VDIVSS": {1, 0x5E, 0, 2, -1, vexNDS3},
|
||||||
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3},
|
"VMINSS": {1, 0x5D, 0, 2, -1, vexNDS3},
|
||||||
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3},
|
"VMAXSS": {1, 0x5F, 0, 2, -1, vexNDS3},
|
||||||
// VEX.128/256.66.0F38.W1 — fused multiply-add (NDS form).
|
// VEX.128/256.66.0F38.W1, fused multiply-add (NDS form).
|
||||||
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3},
|
"VFMADD231PD": {2, 0xB8, 1, 1, -1, vexNDS3},
|
||||||
|
|
||||||
// VEX.128/256.66.0F38.WIG — sign/zero extend and broadcast (reg=dst, rm=src,
|
// VEX.128/256.66.0F38.WIG, sign/zero extend and broadcast (reg=dst, rm=src,
|
||||||
// no vvvv).
|
// no vvvv).
|
||||||
"VPMOVSXWD": {2, 0x23, 0, 1, -1, vexRM},
|
"VPMOVSXWD": {2, 0x23, 0, 1, -1, vexRM},
|
||||||
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM},
|
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM},
|
||||||
@@ -143,70 +143,70 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VPBROADCASTQ": {2, 0x59, 0, 1, -1, vexRM},
|
"VPBROADCASTQ": {2, 0x59, 0, 1, -1, vexRM},
|
||||||
"VPBROADCASTB": {2, 0x78, 0, 1, -1, vexRM},
|
"VPBROADCASTB": {2, 0x78, 0, 1, -1, vexRM},
|
||||||
"VPBROADCASTW": {2, 0x79, 0, 1, -1, vexRM},
|
"VPBROADCASTW": {2, 0x79, 0, 1, -1, vexRM},
|
||||||
// VEX.128/256.F3.0F.WIG — signed dword to packed double conversion
|
// VEX.128/256.F3.0F.WIG, signed dword to packed double conversion
|
||||||
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
||||||
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM},
|
"VCVTDQ2PD": {1, 0xE6, 0, 2, -1, vexRM},
|
||||||
// VEX.128/256.0F.WIG — signed dword to packed single conversion
|
// VEX.128/256.0F.WIG, signed dword to packed single conversion
|
||||||
// (reg=dst, rm=src, no vvvv, no mandatory prefix).
|
// (reg=dst, rm=src, no vvvv, no mandatory prefix).
|
||||||
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM},
|
"VCVTDQ2PS": {1, 0x5B, 0, 0, -1, vexRM},
|
||||||
// VEX.128/256.0F.WIG — packed single to packed double conversion
|
// VEX.128/256.0F.WIG, packed single to packed double conversion
|
||||||
// (reg=dst, rm=src; the destination is the wide operand and sets the
|
// (reg=dst, rm=src; the destination is the wide operand and sets the
|
||||||
// length). Intel's maps prescribe the F3 prefix here (VEX.pp = 10), but
|
// length). Intel's maps prescribe the F3 prefix here (VEX.pp = 10), but
|
||||||
// the Go assembler emits the instruction with pp = 00, and gasm follows
|
// the Go assembler emits the instruction with pp = 00, and gasm follows
|
||||||
// the Go assembler's bytes — its machine code is the oracle, not the
|
// the Go assembler's bytes, its machine code is the oracle, not the
|
||||||
// manual.
|
// manual.
|
||||||
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM},
|
"VCVTPS2PD": {1, 0x5A, 0, 0, -1, vexRM},
|
||||||
// VEX.128.F2.0F.WIG — duplicate the low double of each 128-bit lane
|
// VEX.128.F2.0F.WIG, duplicate the low double of each 128-bit lane
|
||||||
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
// (reg=dst, rm=src, no vvvv; the length follows the destination).
|
||||||
"VMOVDDUP": {1, 0x12, 0, 3, -1, vexRM},
|
"VMOVDDUP": {1, 0x12, 0, 3, -1, vexRM},
|
||||||
// VEX.128/256.66.0F.WIG — move mask to a GPR (reg=gpr dst, rm=vec src).
|
// VEX.128/256.66.0F.WIG, move mask to a GPR (reg=gpr dst, rm=vec src).
|
||||||
"VPMOVMSKB": {1, 0xD7, 0, 1, -1, vexRM},
|
"VPMOVMSKB": {1, 0xD7, 0, 1, -1, vexRM},
|
||||||
"VMOVMSKPS": {1, 0x50, 0, 0, -1, vexRM}, // no 66 prefix (that would be VMOVMSKPD)
|
"VMOVMSKPS": {1, 0x50, 0, 0, -1, vexRM}, // no 66 prefix (that would be VMOVMSKPD)
|
||||||
|
|
||||||
// VEX.128/256.66.0F.WIG — immediate shifts (opdigit selects the shift).
|
// VEX.128/256.66.0F.WIG, immediate shifts (opdigit selects the shift).
|
||||||
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm},
|
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm},
|
||||||
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm},
|
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm},
|
||||||
"VPSRLD": {1, 0x72, 0, 1, 2, vexShiftImm},
|
"VPSRLD": {1, 0x72, 0, 1, 2, vexShiftImm},
|
||||||
"VPSRLQ": {1, 0x73, 0, 1, 2, vexShiftImm},
|
"VPSRLQ": {1, 0x73, 0, 1, 2, vexShiftImm},
|
||||||
"VPSLLQ": {1, 0x73, 0, 1, 6, vexShiftImm},
|
"VPSLLQ": {1, 0x73, 0, 1, 6, vexShiftImm},
|
||||||
|
|
||||||
// VEX.128/256.66.0F.WIG — immediate shuffle (reg=dst, rm=src, imm8).
|
// VEX.128/256.66.0F.WIG, immediate shuffle (reg=dst, rm=src, imm8).
|
||||||
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM},
|
"VPSHUFD": {1, 0x70, 0, 1, -1, vexImmRM},
|
||||||
// VEX.256.66.0F3A.W1 — qword permute (reg=dst, rm=src, imm8).
|
// VEX.256.66.0F3A.W1, qword permute (reg=dst, rm=src, imm8).
|
||||||
"VPERMQ": {3, 0x00, 1, 1, -1, vexImmRM},
|
"VPERMQ": {3, 0x00, 1, 1, -1, vexImmRM},
|
||||||
|
|
||||||
// VEX.128/256.66.0F.WIG — two-source shuffle (reg=dst, vvvv=src1, rm=src2,
|
// VEX.128/256.66.0F.WIG, two-source shuffle (reg=dst, vvvv=src1, rm=src2,
|
||||||
// imm8).
|
// imm8).
|
||||||
"VSHUFPD": {1, 0xC6, 0, 1, -1, vexNDS3Imm},
|
"VSHUFPD": {1, 0xC6, 0, 1, -1, vexNDS3Imm},
|
||||||
// VEX.256.66.0F3A.W0 — permute / insert (same shape; VINSERTI128's rm is
|
// VEX.256.66.0F3A.W0, permute / insert (same shape; VINSERTI128's rm is
|
||||||
// the XMM or memory source).
|
// the XMM or memory source).
|
||||||
"VPERM2I128": {3, 0x46, 0, 1, -1, vexNDS3Imm},
|
"VPERM2I128": {3, 0x46, 0, 1, -1, vexNDS3Imm},
|
||||||
"VINSERTI128": {3, 0x38, 0, 1, -1, vexNDS3Imm},
|
"VINSERTI128": {3, 0x38, 0, 1, -1, vexNDS3Imm},
|
||||||
|
|
||||||
// VEX.256.66.0F3A.W0 — lane extract (reg=YMM src, rm=XMM/memory dst, imm8).
|
// VEX.256.66.0F3A.W0, lane extract (reg=YMM src, rm=XMM/memory dst, imm8).
|
||||||
"VEXTRACTI128": {3, 0x39, 0, 1, -1, vexExtract},
|
"VEXTRACTI128": {3, 0x39, 0, 1, -1, vexExtract},
|
||||||
"VEXTRACTF128": {3, 0x19, 0, 1, -1, vexExtract},
|
"VEXTRACTF128": {3, 0x19, 0, 1, -1, vexExtract},
|
||||||
// VEX.128/256.66.0F3A.W0 — half-precision convert back ($imm, src, dst:
|
// VEX.128/256.66.0F3A.W0, half-precision convert back ($imm, src, dst:
|
||||||
// reg=src, rm=XMM/memory dst, imm8 — the extract layout).
|
// reg=src, rm=XMM/memory dst, imm8, the extract layout).
|
||||||
"VCVTPS2PH": {3, 0x1D, 0, 1, -1, vexExtract},
|
"VCVTPS2PH": {3, 0x1D, 0, 1, -1, vexExtract},
|
||||||
|
|
||||||
// VEX.128.0F.W0 — no operands.
|
// VEX.128.0F.W0, no operands.
|
||||||
"VZEROUPPER": {1, 0x77, 0, 0, -1, vexZero},
|
"VZEROUPPER": {1, 0x77, 0, 0, -1, vexZero},
|
||||||
|
|
||||||
// VEX.128.0F.W0 — mask-register test (KTESTW k1, k2: reg = dst, rm = src).
|
// VEX.128.0F.W0, mask-register test (KTESTW k1, k2: reg = dst, rm = src).
|
||||||
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
|
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
|
||||||
|
|
||||||
// VEX.66.0F38.W0 — broadcast a single/double to all lanes (reg=dst,
|
// VEX.66.0F38.W0, broadcast a single/double to all lanes (reg=dst,
|
||||||
// rm=scalar memory; SD is 256-bit only).
|
// rm=scalar memory; SD is 256-bit only).
|
||||||
"VBROADCASTSS": {2, 0x18, 0, 1, -1, vexRM},
|
"VBROADCASTSS": {2, 0x18, 0, 1, -1, vexRM},
|
||||||
"VBROADCASTSD": {2, 0x19, 0, 1, -1, vexRM},
|
"VBROADCASTSD": {2, 0x19, 0, 1, -1, vexRM},
|
||||||
// VEX.66.0F38.W0 — half-precision convert (reg=dst, rm=half-width
|
// VEX.66.0F38.W0, half-precision convert (reg=dst, rm=half-width
|
||||||
// source).
|
// source).
|
||||||
"VCVTPH2PS": {2, 0x13, 0, 1, -1, vexRM},
|
"VCVTPH2PS": {2, 0x13, 0, 1, -1, vexRM},
|
||||||
// VEX.F3.0F.WIG — replicate even/odd singles (reg=dst, rm=src).
|
// VEX.F3.0F.WIG, replicate even/odd singles (reg=dst, rm=src).
|
||||||
"VMOVSLDUP": {1, 0x12, 0, 2, -1, vexRM},
|
"VMOVSLDUP": {1, 0x12, 0, 2, -1, vexRM},
|
||||||
"VMOVSHDUP": {1, 0x16, 0, 2, -1, vexRM},
|
"VMOVSHDUP": {1, 0x16, 0, 2, -1, vexRM},
|
||||||
// VEX.66.0F.WIG — packed double to packed single conversion, the X/Y
|
// VEX.66.0F.WIG, packed double to packed single conversion, the X/Y
|
||||||
// spellings: the destination is always XMM and the spelling fixes the
|
// spellings: the destination is always XMM and the spelling fixes the
|
||||||
// source length (X = 128, Y = 256).
|
// source length (X = 128, Y = 256).
|
||||||
"VCVTPD2PSX": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
|
"VCVTPD2PSX": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
|
||||||
@@ -230,14 +230,14 @@ var vexTable = map[string]vexSpec{
|
|||||||
"VCVTSI2SSL": {1, 0x2A, 0, 2, -1, vexNDS3},
|
"VCVTSI2SSL": {1, 0x2A, 0, 2, -1, vexNDS3},
|
||||||
"VCVTSI2SSQ": {1, 0x2A, 1, 2, -1, vexNDS3},
|
"VCVTSI2SSQ": {1, 0x2A, 1, 2, -1, vexNDS3},
|
||||||
|
|
||||||
// VEX.128/256.66.0F.WIG — word shifts (opdigit selects the shift).
|
// VEX.128/256.66.0F.WIG, word shifts (opdigit selects the shift).
|
||||||
"VPSRLW": {1, 0x71, 0, 1, 2, vexShiftImm},
|
"VPSRLW": {1, 0x71, 0, 1, 2, vexShiftImm},
|
||||||
"VPSRAW": {1, 0x71, 0, 1, 4, vexShiftImm},
|
"VPSRAW": {1, 0x71, 0, 1, 4, vexShiftImm},
|
||||||
"VPSLLW": {1, 0x71, 0, 1, 6, vexShiftImm},
|
"VPSLLW": {1, 0x71, 0, 1, 6, vexShiftImm},
|
||||||
|
|
||||||
// VEX.F2.0F — packed double to packed dword conversions, truncating and
|
// VEX.F2.0F, packed double to packed dword conversions, truncating and
|
||||||
// non-truncating. The destination is always XMM; the X/Y spellings fix
|
// non-truncating. The destination is always XMM; the X/Y spellings fix
|
||||||
// the source length (XMM/YMM), and VEX.L follows it — see vexSrcLen.
|
// the source length (XMM/YMM), and VEX.L follows it, see vexSrcLen.
|
||||||
"VCVTPD2DQX": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
"VCVTPD2DQX": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
||||||
"VCVTPD2DQY": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
"VCVTPD2DQY": {1, 0xE6, 0, 3, -1, vexRMSrcLen},
|
||||||
"VCVTTPD2DQX": {1, 0xE6, 0, 1, -1, vexRMSrcLen},
|
"VCVTTPD2DQX": {1, 0xE6, 0, 1, -1, vexRMSrcLen},
|
||||||
@@ -257,7 +257,7 @@ var vexSrcLen = map[string]int{
|
|||||||
"VCVTPD2PSY": 1,
|
"VCVTPD2PSY": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
// vexVarShift maps the shift mnemonics to their variable-count opcode — the
|
// vexVarShift maps the shift mnemonics to their variable-count opcode, the
|
||||||
// form whose count comes from an XMM register or memory (VPSRLQ X0, Y8, Y8),
|
// form whose count comes from an XMM register or memory (VPSRLQ X0, Y8, Y8),
|
||||||
// an ordinary NDS encoding rather than the /digit immediate form above.
|
// an ordinary NDS encoding rather than the /digit immediate form above.
|
||||||
var vexVarShift = map[string]byte{
|
var vexVarShift = map[string]byte{
|
||||||
@@ -288,20 +288,20 @@ type vexMoveSpec struct {
|
|||||||
|
|
||||||
// vexMoveTable maps an upper-case move mnemonic to its encoding.
|
// vexMoveTable maps an upper-case move mnemonic to its encoding.
|
||||||
var vexMoveTable = map[string]vexMoveSpec{
|
var vexMoveTable = map[string]vexMoveSpec{
|
||||||
// VEX.128/256.F3.0F.WIG — unaligned integer move.
|
// VEX.128/256.F3.0F.WIG, unaligned integer move.
|
||||||
"VMOVDQU": {1, 2, 0x6F, 0x7F, 0, 0, 0, 0, true, false, false},
|
"VMOVDQU": {1, 2, 0x6F, 0x7F, 0, 0, 0, 0, true, false, false},
|
||||||
// VEX.128/256.66.0F.WIG — unaligned packed double move.
|
// VEX.128/256.66.0F.WIG, unaligned packed double move.
|
||||||
"VMOVUPD": {1, 1, 0x10, 0x11, 0, 0, 0, 0, true, false, false},
|
"VMOVUPD": {1, 1, 0x10, 0x11, 0, 0, 0, 0, true, false, false},
|
||||||
// VEX.128.66.0F.W0 — 32-bit GPR/memory ↔ XMM.
|
// VEX.128.66.0F.W0, 32-bit GPR/memory ↔ XMM.
|
||||||
"VMOVD": {1, 1, 0x6E, 0x7E, 0, 0, 0, 0, false, true, true},
|
"VMOVD": {1, 1, 0x6E, 0x7E, 0, 0, 0, 0, false, true, true},
|
||||||
// VMOVQ — 66 6E W1 (r/m→xmm), 66 7E W1 (xmm→r/m), 66 D6 W0 (xmm→xmm).
|
// VMOVQ, 66 6E W1 (r/m→xmm), 66 7E W1 (xmm→r/m), 66 D6 W0 (xmm→xmm).
|
||||||
"VMOVQ": {1, 1, 0x6E, 0x7E, 1, 1, 0xD6, 0, true, true, true},
|
"VMOVQ": {1, 1, 0x6E, 0x7E, 1, 1, 0xD6, 0, true, true, true},
|
||||||
// VEX.128.F2.0F.WIG — scalar double move, memory operands only (the
|
// VEX.128.F2.0F.WIG, scalar double move, memory operands only (the
|
||||||
// register form takes three operands and is not supported yet).
|
// register form takes three operands and is not supported yet).
|
||||||
"VMOVSD": {1, 3, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
|
"VMOVSD": {1, 3, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
|
||||||
// VEX.128.F3.0F.WIG — scalar single move, memory operands only.
|
// VEX.128.F3.0F.WIG, scalar single move, memory operands only.
|
||||||
"VMOVSS": {1, 2, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
|
"VMOVSS": {1, 2, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
|
||||||
// VEX.128/256 — aligned packed moves.
|
// VEX.128/256, aligned packed moves.
|
||||||
"VMOVAPS": {1, 0, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
|
"VMOVAPS": {1, 0, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
|
||||||
"VMOVAPD": {1, 1, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
|
"VMOVAPD": {1, 1, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
|
||||||
}
|
}
|
||||||
@@ -317,7 +317,7 @@ func isVex(mnemUpper string) bool {
|
|||||||
|
|
||||||
// encodeVex encodes a VEX instruction with operands in Plan 9 order.
|
// encodeVex encodes a VEX instruction with operands in Plan 9 order.
|
||||||
func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
|
||||||
// Vector register indices 16–31 exist only in EVEX encodings; fail
|
// Vector register indices 16-31 exist only in EVEX encodings; fail
|
||||||
// loudly rather than silently truncating the index.
|
// loudly rather than silently truncating the index.
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
if r, ok := op.(Reg); ok && r.isVec() && r.idx >= 16 {
|
if r, ok := op.(Reg); ok && r.isVec() && r.idx >= 16 {
|
||||||
@@ -420,7 +420,7 @@ func (e *enc) encodeVexRM(spec vexSpec, ops []Operand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// encodeVexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
// encodeVexRMSrcLen encodes a length-narrowing conversion: OP src, dst with
|
||||||
// the destination always XMM and the VEX.L bit following the source — fixed
|
// the destination always XMM and the VEX.L bit following the source, fixed
|
||||||
// by the mnemonic's spelling (VCVTPD2DQX = 128, VCVTPD2DQY = 256) even when
|
// by the mnemonic's spelling (VCVTPD2DQX = 128, VCVTPD2DQY = 256) even when
|
||||||
// the source is memory.
|
// the source is memory.
|
||||||
func (e *enc) encodeVexRMSrcLen(mnem string, spec vexSpec, ops []Operand) error {
|
func (e *enc) encodeVexRMSrcLen(mnem string, spec vexSpec, ops []Operand) error {
|
||||||
|
|||||||
+5
-5
@@ -173,7 +173,7 @@ func TestVexGroundTruth(t *testing.T) {
|
|||||||
{"VPMULLD Y1,Y2,Y3", "VPMULLD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d40d9", ""},
|
{"VPMULLD Y1,Y2,Y3", "VPMULLD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d40d9", ""},
|
||||||
{"VPUNPCKLDQ Y4,Y3,Y5", "VPUNPCKLDQ", []Operand{vreg(t, "Y4"), vreg(t, "Y3"), vreg(t, "Y5")}, "c5e562ec", ""},
|
{"VPUNPCKLDQ Y4,Y3,Y5", "VPUNPCKLDQ", []Operand{vreg(t, "Y4"), vreg(t, "Y3"), vreg(t, "Y5")}, "c5e562ec", ""},
|
||||||
{"VPERMD Y1,Y2,Y3", "VPERMD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d36d9", ""},
|
{"VPERMD Y1,Y2,Y3", "VPERMD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c4e26d36d9", ""},
|
||||||
// Floating point (packed and scalar) and FMA — same NDS form, the pp
|
// Floating point (packed and scalar) and FMA; same NDS form, the pp
|
||||||
// bits and map select the operation.
|
// bits and map select the operation.
|
||||||
{"VADDPD Y9,Y8,Y8", "VADDPD", []Operand{vreg(t, "Y9"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d58c1", ""},
|
{"VADDPD Y9,Y8,Y8", "VADDPD", []Operand{vreg(t, "Y9"), vreg(t, "Y8"), vreg(t, "Y8")}, "c4413d58c1", ""},
|
||||||
{"VADDPD X1,X2,X3", "VADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e958d9", ""},
|
{"VADDPD X1,X2,X3", "VADDPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e958d9", ""},
|
||||||
@@ -217,7 +217,7 @@ func TestVexGroundTruth(t *testing.T) {
|
|||||||
{"VEXTRACTI128 $1,Y8,X9", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d39c101", ""},
|
{"VEXTRACTI128 $1,Y8,X9", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d39c101", ""},
|
||||||
{"VEXTRACTI128 $1,Y8,(DI)", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), Ptr(DI, 0, 16)}, "c4637d390701", ""},
|
{"VEXTRACTI128 $1,Y8,(DI)", "VEXTRACTI128", []Operand{Imm(1), vreg(t, "Y8"), Ptr(DI, 0, 16)}, "c4637d390701", ""},
|
||||||
{"VEXTRACTF128 $1,Y8,X9", "VEXTRACTF128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d19c101", ""},
|
{"VEXTRACTF128 $1,Y8,X9", "VEXTRACTF128", []Operand{Imm(1), vreg(t, "Y8"), vreg(t, "X9")}, "c4437d19c101", ""},
|
||||||
// Moves — each direction picks its own opcode and VEX.W.
|
// Moves; each direction picks its own opcode and VEX.W.
|
||||||
{"VMOVDQU (SI),Y1", "VMOVDQU", []Operand{Ptr(SI, 0, 32), vreg(t, "Y1")}, "c5fe6f0e", ""},
|
{"VMOVDQU (SI),Y1", "VMOVDQU", []Operand{Ptr(SI, 0, 32), vreg(t, "Y1")}, "c5fe6f0e", ""},
|
||||||
{"VMOVDQU Y3,(DI)", "VMOVDQU", []Operand{vreg(t, "Y3"), Ptr(DI, 0, 32)}, "c5fe7f1f", ""},
|
{"VMOVDQU Y3,(DI)", "VMOVDQU", []Operand{vreg(t, "Y3"), Ptr(DI, 0, 32)}, "c5fe7f1f", ""},
|
||||||
{"VMOVDQU X1,X2", "VMOVDQU", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fa7fca", ""},
|
{"VMOVDQU X1,X2", "VMOVDQU", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fa7fca", ""},
|
||||||
@@ -234,7 +234,7 @@ func TestVexGroundTruth(t *testing.T) {
|
|||||||
{"VMOVD AX,X0", "VMOVD", []Operand{AX, vreg(t, "X0")}, "c5f96ec0", ""},
|
{"VMOVD AX,X0", "VMOVD", []Operand{AX, vreg(t, "X0")}, "c5f96ec0", ""},
|
||||||
{"VMOVSD (SI),X8", "VMOVSD", []Operand{Ptr(SI, 0, 8), vreg(t, "X8")}, "c57b1006", ""},
|
{"VMOVSD (SI),X8", "VMOVSD", []Operand{Ptr(SI, 0, 8), vreg(t, "X8")}, "c57b1006", ""},
|
||||||
{"VMOVSD X8,(SI)", "VMOVSD", []Operand{vreg(t, "X8"), Ptr(SI, 0, 8)}, "c57b1106", ""},
|
{"VMOVSD X8,(SI)", "VMOVSD", []Operand{vreg(t, "X8"), Ptr(SI, 0, 8)}, "c57b1106", ""},
|
||||||
// Packed double arithmetic and unpack — the NDS form, the opcode
|
// Packed double arithmetic and unpack; the NDS form, the opcode
|
||||||
// selects the operation.
|
// selects the operation.
|
||||||
{"VSUBPD Y1,Y2,Y3", "VSUBPD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ed5cd9", ""},
|
{"VSUBPD Y1,Y2,Y3", "VSUBPD", []Operand{vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "c5ed5cd9", ""},
|
||||||
{"VDIVPD X1,X2,X3", "VDIVPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e95ed9", ""},
|
{"VDIVPD X1,X2,X3", "VDIVPD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e95ed9", ""},
|
||||||
@@ -255,12 +255,12 @@ func TestVexGroundTruth(t *testing.T) {
|
|||||||
{"VMINSS X6,X7,X8", "VMINSS", []Operand{vreg(t, "X6"), vreg(t, "X7"), vreg(t, "X8")}, "c5425dc6", ""},
|
{"VMINSS X6,X7,X8", "VMINSS", []Operand{vreg(t, "X6"), vreg(t, "X7"), vreg(t, "X8")}, "c5425dc6", ""},
|
||||||
{"VMAXSS X1,X2,X3", "VMAXSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea5fd9", ""},
|
{"VMAXSS X1,X2,X3", "VMAXSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5ea5fd9", ""},
|
||||||
{"VADDSD 8(AX),X1,X2", "VADDSD", []Operand{Ptr(AX, 8, 8), vreg(t, "X1"), vreg(t, "X2")}, "c5f3585008", ""},
|
{"VADDSD 8(AX),X1,X2", "VADDSD", []Operand{Ptr(AX, 8, 8), vreg(t, "X1"), vreg(t, "X2")}, "c5f3585008", ""},
|
||||||
// VMOVDDUP — duplicate the low double (reg=dst, rm=src, F2 pp).
|
// VMOVDDUP; duplicate the low double (reg=dst, rm=src, F2 pp).
|
||||||
{"VMOVDDUP X1,X2", "VMOVDDUP", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fb12d1", ""},
|
{"VMOVDDUP X1,X2", "VMOVDDUP", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fb12d1", ""},
|
||||||
{"VMOVDDUP Y1,Y2", "VMOVDDUP", []Operand{vreg(t, "Y1"), vreg(t, "Y2")}, "c5ff12d1", ""},
|
{"VMOVDDUP Y1,Y2", "VMOVDDUP", []Operand{vreg(t, "Y1"), vreg(t, "Y2")}, "c5ff12d1", ""},
|
||||||
{"VMOVDDUP 8(AX),X1", "VMOVDDUP", []Operand{Ptr(AX, 8, 8), vreg(t, "X1")}, "c5fb124808", ""},
|
{"VMOVDDUP 8(AX),X1", "VMOVDDUP", []Operand{Ptr(AX, 8, 8), vreg(t, "X1")}, "c5fb124808", ""},
|
||||||
// Conversions: DQ→PS (no prefix), PS→PD (Go emits it without the F3
|
// Conversions: DQ→PS (no prefix), PS→PD (Go emits it without the F3
|
||||||
// prefix — see the table comment), DQ→PD.
|
// prefix; see the table comment), DQ→PD.
|
||||||
{"VCVTDQ2PS X1,X2", "VCVTDQ2PS", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85bd1", ""},
|
{"VCVTDQ2PS X1,X2", "VCVTDQ2PS", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85bd1", ""},
|
||||||
{"VCVTDQ2PS Y3,Y4", "VCVTDQ2PS", []Operand{vreg(t, "Y3"), vreg(t, "Y4")}, "c5fc5be3", ""},
|
{"VCVTDQ2PS Y3,Y4", "VCVTDQ2PS", []Operand{vreg(t, "Y3"), vreg(t, "Y4")}, "c5fc5be3", ""},
|
||||||
{"VCVTPS2PD X1,X2", "VCVTPS2PD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85ad1", ""},
|
{"VCVTPS2PD X1,X2", "VCVTPS2PD", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f85ad1", ""},
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ type File struct {
|
|||||||
Orphans []Stmt // labels/instructions seen before any TEXT directive
|
Orphans []Stmt // labels/instructions seen before any TEXT directive
|
||||||
// Macros holds the names introduced by #define directives in this file.
|
// Macros holds the names introduced by #define directives in this file.
|
||||||
// The linter uses it to avoid flagging macro invocations as unknown
|
// The linter uses it to avoid flagging macro invocations as unknown
|
||||||
// instructions (macro expansion itself is out of scope — see the docs).
|
// instructions (macro expansion itself is out of scope, see the docs).
|
||||||
Macros map[string]bool
|
Macros map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+206
-1
@@ -37,17 +37,29 @@ import (
|
|||||||
// construction and are excluded from the diff; the other architectures list
|
// construction and are excluded from the diff; the other architectures list
|
||||||
// their conditional branches outright.
|
// their conditional branches outright.
|
||||||
func cmdAuditInstructions(args []string) error {
|
func cmdAuditInstructions(args []string) error {
|
||||||
fs := newCommand("audit-instructions", "gasm audit-instructions [amd64|arm64|riscv64|loong64]", `
|
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]", `
|
||||||
Compare the gasm encoder for the given architecture (default amd64) against
|
Compare the gasm encoder for the given architecture (default amd64) against
|
||||||
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
||||||
gasm asm --format goobj), known-but-unencodable names (the backlog) and go-
|
gasm asm --format goobj), known-but-unencodable names (the backlog) and go-
|
||||||
only names (feature gaps). The Go side is probed black-box with a battery
|
only names (feature gaps). The Go side is probed black-box with a battery
|
||||||
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||||
provides.
|
provides.
|
||||||
|
|
||||||
|
With --corpus the audit changes shape: it assembles every .s file under the
|
||||||
|
given directory (default GOROOT/src) with the gasm encoder only, no
|
||||||
|
toolchain probing. A file whose name carries a recognisable _arch suffix is
|
||||||
|
attempted for that architecture; a file without one is attempted for all
|
||||||
|
four, exactly as a GOARCH build would compile it. The report gives the
|
||||||
|
per-architecture pass rates and the most common failure reasons, which drive
|
||||||
|
the encodability backlog by frequency rather than by table order.
|
||||||
`)
|
`)
|
||||||
|
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if *corpus {
|
||||||
|
return cmdAuditCorpus(fs.Args())
|
||||||
|
}
|
||||||
archName := "amd64"
|
archName := "amd64"
|
||||||
switch n := len(fs.Args()); {
|
switch n := len(fs.Args()); {
|
||||||
case n > 1:
|
case n > 1:
|
||||||
@@ -305,3 +317,196 @@ func gasmAssembles(a arch.Arch, name, shape string) bool {
|
|||||||
func sanitize(name string) string {
|
func sanitize(name string) string {
|
||||||
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- corpus audit -----------------------------------------------------------
|
||||||
|
|
||||||
|
// corpusTarget is one architecture row of the corpus report.
|
||||||
|
type corpusTarget struct {
|
||||||
|
a arch.Arch
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// corpusTally accumulates one architecture's attempts over the corpus.
|
||||||
|
type corpusTally struct {
|
||||||
|
attempted int
|
||||||
|
assembled int
|
||||||
|
reasons map[string]int // failure reason → count
|
||||||
|
example map[string]string // failure reason → one representative file
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *corpusTally) fail(path, reason string) {
|
||||||
|
t.reasons[reason]++
|
||||||
|
if t.example[reason] == "" {
|
||||||
|
t.example[reason] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdAuditCorpus implements audit-instructions --corpus.
|
||||||
|
func cmdAuditCorpus(args []string) error {
|
||||||
|
if len(args) > 1 {
|
||||||
|
return fmt.Errorf("audit-instructions --corpus takes at most one directory argument")
|
||||||
|
}
|
||||||
|
root := ""
|
||||||
|
if len(args) == 1 {
|
||||||
|
root = args[0]
|
||||||
|
} else {
|
||||||
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("locate GOROOT: %w", err)
|
||||||
|
}
|
||||||
|
root = filepath.Join(strings.TrimSpace(string(out)), "src")
|
||||||
|
}
|
||||||
|
stats, err := runCorpusAudit(root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printCorpusStats(stats)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// corpusStats is the outcome of one corpus audit run.
|
||||||
|
type corpusStats struct {
|
||||||
|
root string
|
||||||
|
files int
|
||||||
|
generic int // files attempted for all four architectures
|
||||||
|
full int // files that assembled for every target architecture
|
||||||
|
targets []corpusTarget
|
||||||
|
tallies []*corpusTally
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCorpusAudit assembles every .s file under root and returns the stats.
|
||||||
|
func runCorpusAudit(root string) (*corpusStats, error) {
|
||||||
|
files, err := asmFiles(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := []corpusTarget{
|
||||||
|
{arch.AMD64, "amd64"},
|
||||||
|
{arch.ARM64, "arm64"},
|
||||||
|
{arch.RISCV, "riscv64"},
|
||||||
|
{arch.LOONG64, "loong64"},
|
||||||
|
}
|
||||||
|
tallies := make([]*corpusTally, len(targets))
|
||||||
|
for i := range tallies {
|
||||||
|
tallies[i] = &corpusTally{reasons: map[string]int{}, example: map[string]string{}}
|
||||||
|
}
|
||||||
|
// full is the north-star number: a file counts when every architecture
|
||||||
|
// its name allows assembles it.
|
||||||
|
full, generic := 0, 0
|
||||||
|
|
||||||
|
for _, path := range files {
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, src)
|
||||||
|
|
||||||
|
var wanted []int // indexes into targets
|
||||||
|
if a := arch.FromFilename(path); a != arch.Unknown {
|
||||||
|
for i, tg := range targets {
|
||||||
|
if tg.a == a {
|
||||||
|
wanted = append(wanted, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generic++
|
||||||
|
for i := range targets {
|
||||||
|
wanted = append(wanted, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
for _, i := range wanted {
|
||||||
|
tg, t := targets[i], tallies[i]
|
||||||
|
t.attempted++
|
||||||
|
var err error
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = errs[0] // a parse failure is a failure for every target
|
||||||
|
} else {
|
||||||
|
_, err = assembleFile(tg.a, f)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
ok = false
|
||||||
|
t.fail(path, corpusReason(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.assembled++
|
||||||
|
}
|
||||||
|
if ok && len(wanted) > 0 {
|
||||||
|
full++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &corpusStats{
|
||||||
|
root: root,
|
||||||
|
files: len(files),
|
||||||
|
generic: generic,
|
||||||
|
full: full,
|
||||||
|
targets: targets,
|
||||||
|
tallies: tallies,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// printCorpusStats renders the corpus audit report.
|
||||||
|
func printCorpusStats(s *corpusStats) {
|
||||||
|
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures)\n", s.root, s.files, s.generic)
|
||||||
|
fmt.Printf(" assemble for every target architecture: %d (%.1f%%)\n", s.full, 100*float64(s.full)/float64(max(s.files, 1)))
|
||||||
|
for i, tg := range s.targets {
|
||||||
|
t := s.tallies[i]
|
||||||
|
fmt.Printf(" %s: %d/%d attempted\n", tg.name, t.assembled, t.attempted)
|
||||||
|
for _, r := range topReasons(t) {
|
||||||
|
fmt.Printf(" %4d %s\n", t.reasons[r], r)
|
||||||
|
fmt.Printf(" e.g. %s\n", t.example[r])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// corpusReason buckets an assembly or parse failure for the histogram.
|
||||||
|
func corpusReason(err error) string {
|
||||||
|
msg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(msg, "unsupported"), strings.Contains(msg, "cannot encode"):
|
||||||
|
return "instruction not encodable"
|
||||||
|
case strings.Contains(msg, "undefined label"):
|
||||||
|
return "undefined label"
|
||||||
|
case strings.Contains(msg, "undefined symbol"), strings.Contains(msg, "external symbol"), strings.Contains(msg, "file-level assembly"):
|
||||||
|
return "undefined symbol or external"
|
||||||
|
case strings.Contains(msg, "operand"), strings.Contains(msg, "operand form"):
|
||||||
|
return "unsupported operand form"
|
||||||
|
default:
|
||||||
|
return "other: " + firstLine(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// topReasons returns at most five reasons, most frequent first.
|
||||||
|
func topReasons(t *corpusTally) []string {
|
||||||
|
type kv struct {
|
||||||
|
k string
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
var kvs []kv
|
||||||
|
for k, n := range t.reasons {
|
||||||
|
kvs = append(kvs, kv{k, n})
|
||||||
|
}
|
||||||
|
slices.SortFunc(kvs, func(a, b kv) int { return b.n - a.n })
|
||||||
|
if len(kvs) > 5 {
|
||||||
|
kvs = kvs[:5]
|
||||||
|
}
|
||||||
|
out := make([]string, len(kvs))
|
||||||
|
for i, kv := range kvs {
|
||||||
|
out[i] = kv.k
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstLine returns the first line of an error message, truncated.
|
||||||
|
func firstLine(msg string) string {
|
||||||
|
if i := strings.IndexByte(msg, '\n'); i >= 0 {
|
||||||
|
msg = msg[:i]
|
||||||
|
}
|
||||||
|
if len(msg) > 80 {
|
||||||
|
msg = msg[:80]
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ REPL commands:
|
|||||||
if *timeout > 0 {
|
if *timeout > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(*timeout)
|
time.Sleep(*timeout)
|
||||||
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s) — killing the debuggee\n", *timeout)
|
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s), killing the debuggee\n", *timeout)
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,6 @@ REPL commands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct the argument block with buffer pointers at the correct positions.
|
// Construct the argument block with buffer pointers at the correct positions.
|
||||||
bufIdx := 0
|
|
||||||
for _, arg := range layout {
|
for _, arg := range layout {
|
||||||
if !arg.IsPtr {
|
if !arg.IsPtr {
|
||||||
continue
|
continue
|
||||||
@@ -170,12 +169,10 @@ REPL commands:
|
|||||||
argBlock[off+16+j] = byte(size >> (j * 8))
|
argBlock[off+16+j] = byte(size >> (j * 8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bufIdx++
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = bufIdx
|
|
||||||
} else {
|
} else {
|
||||||
argBlock = make([]byte, fl.Args)
|
argBlock = make([]byte, fl.Args)
|
||||||
sess, err = debug.Launch("", path, *funcName, argBlock)
|
sess, err = debug.Launch("", path, *funcName, argBlock)
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdDis disassembles machine code: either a raw binary (standard input with
|
||||||
|
// "-") whose architecture is given with -a, or a .s file, which is assembled
|
||||||
|
// first so the listing shows the real function and label layout.
|
||||||
|
func cmdDis(args []string) int {
|
||||||
|
fs := newCommand("dis", "gasm dis [-a arch] <file>", `
|
||||||
|
Disassemble machine code to instruction text (via golang.org/x/arch).
|
||||||
|
|
||||||
|
With a .s file, the file is assembled first and the listing follows the
|
||||||
|
real layout: one block per TEXT function, local labels printed at their
|
||||||
|
offsets. The architecture comes from the file name suffix, or from -a.
|
||||||
|
|
||||||
|
With any other file, or "-" for standard input, the bytes are disassembled
|
||||||
|
linearly and -a selects the architecture (amd64, arm64, riscv64 or
|
||||||
|
loong64).
|
||||||
|
`)
|
||||||
|
archName := fs.String("a", "", "architecture for raw input: amd64, arm64, riscv64 or loong64")
|
||||||
|
fs.Parse(args)
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: gasm dis [-a arch] <file>")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
path := fs.Arg(0)
|
||||||
|
var target arch.Arch
|
||||||
|
if *archName != "" {
|
||||||
|
var err error
|
||||||
|
target, err = auditArch(*archName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, ".s") {
|
||||||
|
if target == arch.Unknown {
|
||||||
|
target = arch.FromFilename(path)
|
||||||
|
}
|
||||||
|
if target == arch.Unknown {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm dis: cannot infer the architecture from the file name; use -a")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return disSource(path, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == arch.Unknown {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm dis: raw input needs -a (amd64, arm64, riscv64 or loong64)")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm dis:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
printListing(target, []byte(src), 0, nil)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// disSource assembles a .s file and prints one listing block per function.
|
||||||
|
func disSource(path string, target arch.Arch) int {
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm dis:", 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 := assembleFile(target, f)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm dis: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if len(img.Funcs) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm dis: no assemblable TEXT functions found")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
fmt.Printf("%s: %d bytes\n", fn.Name, fn.Size)
|
||||||
|
labels := make(map[int][]string, len(fn.Labels))
|
||||||
|
for name, off := range fn.Labels {
|
||||||
|
labels[off] = append(labels[off], name)
|
||||||
|
}
|
||||||
|
for off := range labels {
|
||||||
|
sort.Strings(labels[off])
|
||||||
|
}
|
||||||
|
printListing(target, code, uint64(fn.Offset), labels)
|
||||||
|
}
|
||||||
|
if len(img.Data) > 0 {
|
||||||
|
fmt.Printf("data: %d bytes at 0x%x\n", len(img.Data), len(img.Code))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// printListing decodes code linearly from offset base, printing label lines
|
||||||
|
// (label name to offset within the block) as they are reached.
|
||||||
|
func printListing(a arch.Arch, code []byte, base uint64, labels map[int][]string) {
|
||||||
|
pc := 0
|
||||||
|
for pc < len(code) {
|
||||||
|
for _, name := range labels[pc] {
|
||||||
|
fmt.Printf("%s:\n", name)
|
||||||
|
}
|
||||||
|
ins, err := disasm.Decode(a, code[pc:], base+uint64(pc))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end := min(pc+ins.Len, len(code))
|
||||||
|
fmt.Printf(" %04x: %-16s %s\n", base+uint64(pc), hexBytes(code[pc:end]), ins.Text)
|
||||||
|
if ins.Len <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pc += ins.Len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hexBytes renders up to 8 bytes as contiguous hex.
|
||||||
|
func hexBytes(b []byte) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, c := range b {
|
||||||
|
if i == 8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%02x", c)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
+157
-291
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// Command gasm is the developer frontend for GAsm — Go's Plan 9 assembler.
|
// Command gasm is the developer frontend for GAsm, Go's Plan 9 assembler.
|
||||||
// It bundles a token dumper, a parser, a formatter, a linter and a language
|
// It bundles a token dumper, a parser, a formatter, a linter and a language
|
||||||
// server into one binary. Every subcommand works headlessly so it can be
|
// server into one binary. Every subcommand works headlessly so it can be
|
||||||
// driven from scripts and CI as well as from an editor.
|
// driven from scripts and CI as well as from an editor.
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -36,9 +37,16 @@ import (
|
|||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version reports the release the toolchain recorded for this build: the
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// tag on a tag, a pseudo-version below one, and (devel) outside version
|
||||||
var version = "0.32.0"
|
// control. Nothing is injected; the recorded value cannot go stale.
|
||||||
|
func version() string {
|
||||||
|
bi, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok || bi.Main.Version == "" {
|
||||||
|
return "(devel)"
|
||||||
|
}
|
||||||
|
return bi.Main.Version
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -56,6 +64,8 @@ func main() {
|
|||||||
os.Exit(cmdLint(os.Args[2:]))
|
os.Exit(cmdLint(os.Args[2:]))
|
||||||
case "asm":
|
case "asm":
|
||||||
os.Exit(cmdAsm(os.Args[2:]))
|
os.Exit(cmdAsm(os.Args[2:]))
|
||||||
|
case "dis":
|
||||||
|
os.Exit(cmdDis(os.Args[2:]))
|
||||||
case "verify":
|
case "verify":
|
||||||
os.Exit(cmdVerify(os.Args[2:]))
|
os.Exit(cmdVerify(os.Args[2:]))
|
||||||
case "debug":
|
case "debug":
|
||||||
@@ -81,18 +91,18 @@ func main() {
|
|||||||
case "help", "--help", "-h":
|
case "help", "--help", "-h":
|
||||||
usage(os.Stdout)
|
usage(os.Stdout)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
|
fmt.Fprintf(os.Stderr, "gasm: unknown command %q; run \"gasm --help\" for usage\n", os.Args[1])
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdVersion prints the release version.
|
// cmdVersion prints the recorded version.
|
||||||
func cmdVersion() int {
|
func cmdVersion() int {
|
||||||
fmt.Printf("gasm %s\n", version)
|
fmt.Printf("gasm %s\n", version())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ANSI color helpers for terminal output.
|
// ANSI colour helpers for terminal output.
|
||||||
const (
|
const (
|
||||||
colorReset = "\033[0m"
|
colorReset = "\033[0m"
|
||||||
colorBold = "\033[1m"
|
colorBold = "\033[1m"
|
||||||
@@ -101,7 +111,7 @@ const (
|
|||||||
colorGray = "\033[90m"
|
colorGray = "\033[90m"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isTTY reports whether the writer is a terminal (for color output).
|
// isTTY reports whether the writer is a terminal (for colour output).
|
||||||
func isTTY(w io.Writer) bool {
|
func isTTY(w io.Writer) bool {
|
||||||
if f, ok := w.(*os.File); ok {
|
if f, ok := w.(*os.File); ok {
|
||||||
stat, _ := f.Stat()
|
stat, _ := f.Stat()
|
||||||
@@ -117,7 +127,7 @@ func usage(w io.Writer) {
|
|||||||
bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset
|
bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, "%sgasm %s%s — developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset)
|
fmt.Fprintf(w, "%sgasm %s%s: developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version(), reset, reset)
|
||||||
fmt.Fprintf(w, "gasm bundles a lexer, parser, formatter, linter, standalone assembler and\n")
|
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")
|
fmt.Fprintf(w, "language server for Plan 9 assembly into one self-contained binary.\n\n")
|
||||||
|
|
||||||
@@ -132,6 +142,7 @@ func usage(w io.Writer) {
|
|||||||
{"fmt", "canonicalise formatting (gofmt for assembly)"},
|
{"fmt", "canonicalise formatting (gofmt for assembly)"},
|
||||||
{"lint", "run static checks"},
|
{"lint", "run static checks"},
|
||||||
{"asm", "assemble .s files to machine code (amd64, arm64, riscv64, loong64)"},
|
{"asm", "assemble .s files to machine code (amd64, arm64, riscv64, loong64)"},
|
||||||
|
{"dis", "disassemble machine code (raw bytes or an assembled .s file)"},
|
||||||
{"verify", "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)"},
|
{"verify", "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)"},
|
||||||
{"debug", "interactive source-level debugger (amd64, arm64, riscv64, loong64)"},
|
{"debug", "interactive source-level debugger (amd64, arm64, riscv64, loong64)"},
|
||||||
{"diff", "compare machine code of two .s files"},
|
{"diff", "compare machine code of two .s files"},
|
||||||
@@ -263,24 +274,34 @@ standard input.
|
|||||||
funcs++
|
funcs++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("%s: OK — %d declarations, %d functions\n", path, len(file.Decls), funcs)
|
fmt.Printf("%s: OK, %d declarations, %d functions\n", path, len(file.Decls), funcs)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdFmt(args []string) int {
|
func cmdFmt(args []string) int {
|
||||||
fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
|
fs := newCommand("fmt", "gasm fmt [-w|-l|-d] [path...]", `
|
||||||
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
|
||||||
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
spacing, per-function mnemonic alignment and blank-line layout (exactly one
|
||||||
blank line before each label, TEXT and GLOBL block). Formatting is
|
blank line before each label, TEXT and GLOBL block). Formatting is
|
||||||
idempotent and preserves every line, comments included.
|
idempotent and preserves every line, comments included.
|
||||||
|
|
||||||
With no paths — or a directory path — every .s file below it is reformatted
|
With no paths, or a directory path, every .s file below it is reformatted
|
||||||
in place and the changed files are listed, the way go fmt does; "." and "_"
|
in place and the changed files are listed, the way go fmt does; "." and "_"
|
||||||
directories are skipped. Explicit file paths print to stdout unless -w is
|
directories are skipped. Explicit file paths print to stdout unless -w is
|
||||||
given.
|
given.
|
||||||
|
|
||||||
|
-l and -d rewrite nothing: -l prints the paths whose formatting differs
|
||||||
|
from gasm's (empty output means everything is formatted, which is what a CI
|
||||||
|
check wants), -d prints the diffs. They are mutually exclusive.
|
||||||
`)
|
`)
|
||||||
write := fs.Bool("w", false, "write result to the source file")
|
write := fs.Bool("w", false, "write result to the source file")
|
||||||
|
list := fs.Bool("l", false, "list files whose formatting differs from gasm's")
|
||||||
|
diffMode := fs.Bool("d", false, "print diffs instead of rewriting files")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
|
if *list && *diffMode {
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm fmt: -l and -d are mutually exclusive")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
// Like go fmt: with no arguments, or with a directory argument, every .s
|
// Like go fmt: with no arguments, or with a directory argument, every .s
|
||||||
// file below the directory is formatted in place and the names of the
|
// file below the directory is formatted in place and the names of the
|
||||||
// changed files are listed; explicit file arguments keep the -w / stdout
|
// changed files are listed; explicit file arguments keep the -w / stdout
|
||||||
@@ -318,6 +339,16 @@ given.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out := format.Source(src)
|
out := format.Source(src)
|
||||||
|
if *list || *diffMode {
|
||||||
|
if out != src {
|
||||||
|
if *list {
|
||||||
|
fmt.Println(path)
|
||||||
|
} else {
|
||||||
|
fmt.Print(unifiedDiff(path, strings.Split(src, "\n"), strings.Split(out, "\n")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if dirMode || *write {
|
if dirMode || *write {
|
||||||
if out != src {
|
if out != src {
|
||||||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||||
@@ -337,7 +368,7 @@ given.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// asmFiles collects the .s files below dir, skipping directories whose name
|
// asmFiles collects the .s files below dir, skipping directories whose name
|
||||||
// starts with "." or "_" — as the go tooling does, which keeps .git and
|
// starts with "." or "_", as the go tooling does, which keeps .git and
|
||||||
// scratch or reference trees (e.g. _refs) untouched.
|
// scratch or reference trees (e.g. _refs) untouched.
|
||||||
func asmFiles(dir string) ([]string, error) {
|
func asmFiles(dir string) ([]string, error) {
|
||||||
var out []string
|
var out []string
|
||||||
@@ -419,6 +450,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
|
|||||||
`)
|
`)
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
srv := lsp.New(os.Stdin, os.Stdout)
|
srv := lsp.New(os.Stdin, os.Stdout)
|
||||||
|
srv.SetVersion(version())
|
||||||
if err := srv.Run(); err != nil {
|
if err := srv.Run(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
|
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -427,7 +459,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|goobj] [-p pkg] [-o out] <file>", `
|
fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>", `
|
||||||
Assemble FILE without the Go toolchain: every TEXT function is encoded to
|
Assemble FILE without the Go toolchain: every TEXT function is encoded to
|
||||||
machine code and printed as a hex dump. Supported architectures: amd64
|
machine code and printed as a hex dump. Supported architectures: amd64
|
||||||
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
|
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
|
||||||
@@ -445,13 +477,22 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
out := fs.String("o", "", "write the output to this file")
|
out := fs.String("o", "", "write the output to this file")
|
||||||
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
|
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)")
|
||||||
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
|
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
|
||||||
|
archName := fs.String("GOARCH", "", "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>")
|
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
targetArch := arch.FromFilename(path)
|
targetArch := arch.FromFilename(path)
|
||||||
|
if *archName != "" {
|
||||||
|
a, err := auditArch(*archName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm asm: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
targetArch = a
|
||||||
|
}
|
||||||
src, err := readSource(path)
|
src, err := readSource(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||||
@@ -470,8 +511,10 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(img.Funcs) == 0 {
|
if len(img.Funcs) == 0 && len(img.Data) == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions found")
|
// A file with neither code nor data assembles to nothing, which is
|
||||||
|
// almost always a wrong architecture rather than an intent.
|
||||||
|
fmt.Fprintln(os.Stderr, "gasm asm: no assemblable TEXT functions or GLOBL data found")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
for _, fn := range img.Funcs {
|
for _, fn := range img.Funcs {
|
||||||
@@ -562,7 +605,7 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
|
|
||||||
// cmdDiff compares the machine code of two assembly files.
|
// cmdDiff compares the machine code of two assembly files.
|
||||||
func cmdDiff(args []string) int {
|
func cmdDiff(args []string) int {
|
||||||
set := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
|
set := newCommand("diff", "gasm diff [-GOARCH arch] <file1.s> <file2.s>", `
|
||||||
Compare the machine code produced by assembling two files.
|
Compare the machine code produced by assembling two files.
|
||||||
Shows which functions differ and the byte-level differences.
|
Shows which functions differ and the byte-level differences.
|
||||||
Useful for verifying that two implementations produce identical code,
|
Useful for verifying that two implementations produce identical code,
|
||||||
@@ -572,12 +615,22 @@ Use --map to compare functions whose names differ between the files,
|
|||||||
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
||||||
`)
|
`)
|
||||||
mapSpec := set.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
mapSpec := set.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
||||||
|
archName := set.String("GOARCH", "", "target architecture for both files: amd64, arm64, riscv64 or loong64")
|
||||||
set.Parse(args)
|
set.Parse(args)
|
||||||
if set.NArg() != 2 {
|
if set.NArg() != 2 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
fmt.Fprintln(os.Stderr, "usage: gasm diff [-GOARCH arch] <file1.s> <file2.s>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path1, path2 := set.Arg(0), set.Arg(1)
|
path1, path2 := set.Arg(0), set.Arg(1)
|
||||||
|
forced := arch.Unknown
|
||||||
|
if *archName != "" {
|
||||||
|
a, err := auditArch(*archName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
forced = a
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the name mapping (file1 name → file2 name).
|
// Parse the name mapping (file1 name → file2 name).
|
||||||
nameMap := make(map[string]string)
|
nameMap := make(map[string]string)
|
||||||
@@ -593,12 +646,12 @@ e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assemble both files.
|
// Assemble both files.
|
||||||
img1, err := assemblePath(path1)
|
img1, err := assemblePath(path1, forced)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
img2, err := assemblePath(path2)
|
img2, err := assemblePath(path2, forced)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
||||||
return 1
|
return 1
|
||||||
@@ -673,8 +726,9 @@ func assembleFile(targetArch arch.Arch, f *ast.File) (*asm.Image, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// assemblePath reads, parses and assembles a file (used by cmdDiff).
|
// assemblePath reads, parses and assembles a file (used by cmdDiff). A
|
||||||
func assemblePath(path string) (*asm.Image, error) {
|
// non-Unknown forced architecture overrides the file-name suffix.
|
||||||
|
func assemblePath(path string, forced arch.Arch) (*asm.Image, error) {
|
||||||
src, err := readSource(path)
|
src, err := readSource(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -686,7 +740,11 @@ func assemblePath(path string) (*asm.Image, error) {
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return nil, fmt.Errorf("parse errors")
|
return nil, fmt.Errorf("parse errors")
|
||||||
}
|
}
|
||||||
return assembleFile(arch.FromFilename(path), f)
|
target := forced
|
||||||
|
if target == arch.Unknown {
|
||||||
|
target = arch.FromFilename(path)
|
||||||
|
}
|
||||||
|
return assembleFile(target, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printByteDiff shows the first few byte differences between two code blocks.
|
// printByteDiff shows the first few byte differences between two code blocks.
|
||||||
@@ -765,9 +823,11 @@ gasm verify --fuzz which exercises the code paths.
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
// cmdVerifyNonJIT handles the verify subcommand for files whose architecture
|
||||||
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
// the host cannot execute: only the ground-truth comparison and the static
|
||||||
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
// profile are available there. Relocation sites are masked before the byte
|
||||||
|
// comparison, as the toolchain leaves them zero for the linker.
|
||||||
|
func cmdVerifyNonJIT(path string, targetArch arch.Arch, groundTruth, profile bool) int {
|
||||||
src, err := readSource(path)
|
src, err := readSource(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)
|
||||||
@@ -780,19 +840,58 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
img, err := asm.AssembleFileRISCV(f)
|
img, err := assembleFile(targetArch, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if groundTruth {
|
if groundTruth {
|
||||||
gt, err := verify.GroundTruthRISCV(path)
|
var gt map[string][]byte
|
||||||
|
switch targetArch {
|
||||||
|
case arch.RISCV:
|
||||||
|
gt, err = verify.GroundTruthRISCV(path)
|
||||||
|
case arch.LOONG64:
|
||||||
|
gt, err = verify.GroundTruthLOONG64(path)
|
||||||
|
case arch.ARM64:
|
||||||
|
gt, err = verify.GroundTruthARM64(path)
|
||||||
|
default:
|
||||||
|
gt, err = verify.GroundTruth(path)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
matched, total := 0, 0
|
matched, total, diffs := compareGroundTruth(img, gt)
|
||||||
|
if diffs > 0 {
|
||||||
|
printCodeDiff(img, gt)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
||||||
|
if matched < total || diffs > 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareGroundTruth compares the image's functions against the go tool asm
|
||||||
|
// output byte-for-byte, masking relocation sites (disp32 fields the Go linker
|
||||||
|
// fills at link time). It prints one line per function and returns the
|
||||||
|
// matched and compared counts plus the number of functions with byte diffs.
|
||||||
|
func compareGroundTruth(img *asm.Image, gt map[string][]byte) (matched, total, diffs int) {
|
||||||
for _, fn := range img.Funcs {
|
for _, fn := range img.Funcs {
|
||||||
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
goCode, ok := gt[fn.Name]
|
goCode, ok := gt[fn.Name]
|
||||||
@@ -821,100 +920,22 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
|||||||
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
diffs++
|
||||||
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return matched, total, diffs
|
||||||
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
|
|
||||||
if matched < total {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile {
|
// printCodeDiff shows a 16-byte hex dump per function whose gasm bytes differ
|
||||||
for _, fn := range img.Funcs {
|
// from the go tool asm output.
|
||||||
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
|
func printCodeDiff(img *asm.Image, gt map[string][]byte) {
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// cmdVerifyLOONG64 verifies a loong64 source file against `go tool asm`
|
|
||||||
// (GOARCH=loong64) — the ground-truth oracle — since gasm cannot JIT-load
|
|
||||||
// LoongArch code on an amd64 host. Relocation sites are masked before the
|
|
||||||
// byte comparison, as the toolchain leaves them zero for the linker.
|
|
||||||
func cmdVerifyLOONG64(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.AssembleFileLOONG64(f)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if groundTruth {
|
|
||||||
gt, err := verify.GroundTruthLOONG64(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 {
|
for _, fn := range img.Funcs {
|
||||||
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
goCode, ok := gt[fn.Name]
|
goCode, ok := gt[fn.Name]
|
||||||
if !ok {
|
if !ok || bytes.Equal(gasmCode, goCode) {
|
||||||
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
|
|
||||||
continue
|
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 {
|
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
|
||||||
var gb, gs string
|
var gb, gs string
|
||||||
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
for j := i; j < i+16 && j < len(gasmCode); j++ {
|
||||||
@@ -928,121 +949,12 @@ func cmdVerifyLOONG64(path string, groundTruth, profile bool) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 cmdVerifyARM64(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.AssembleFileARM64(f)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if groundTruth {
|
|
||||||
gt, err := verify.GroundTruthARM64(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 {
|
||||||
set := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>", `
|
set := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <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
|
||||||
functions. This confirms the assembled image is self-consistent (no
|
functions. This confirms the assembled image is self-consistent (no
|
||||||
unresolved external symbols) and executable — the prerequisite for dynamic
|
unresolved external symbols) and executable, the prerequisite for dynamic
|
||||||
testing.
|
testing.
|
||||||
|
|
||||||
With -smoke, each NOSPLIT function is called with a zeroed argument block to
|
With -smoke, each NOSPLIT function is called with a zeroed argument block to
|
||||||
@@ -1100,17 +1012,12 @@ each entry reproduces.
|
|||||||
// under the available loong64 emulators), so those kernels take the
|
// under the available loong64 emulators), so those kernels take the
|
||||||
// toolchain-comparison path.
|
// toolchain-comparison path.
|
||||||
if targetArch != hostArch() || targetArch == arch.LOONG64 {
|
if targetArch != hostArch() || targetArch == arch.LOONG64 {
|
||||||
|
// No JIT on this host: ground truth and profile remain available.
|
||||||
|
// (loong64 is ground-truth-only everywhere for now: its trampoline
|
||||||
|
// is implemented but not yet validated against real hardware.)
|
||||||
switch targetArch {
|
switch targetArch {
|
||||||
case arch.RISCV:
|
case arch.RISCV, arch.LOONG64, arch.ARM64:
|
||||||
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
|
return cmdVerifyNonJIT(path, targetArch, *groundTruth, *profile)
|
||||||
return cmdVerifyRISCV(path, *groundTruth, *profile)
|
|
||||||
case arch.LOONG64:
|
|
||||||
// LoongArch: ground-truth only (trampoline not yet
|
|
||||||
// hardware-validated).
|
|
||||||
return cmdVerifyLOONG64(path, *groundTruth, *profile)
|
|
||||||
case arch.ARM64:
|
|
||||||
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
|
|
||||||
return cmdVerifyARM64(path, *groundTruth, *profile)
|
|
||||||
case arch.AMD64:
|
case arch.AMD64:
|
||||||
fmt.Fprintln(os.Stderr, "gasm verify: JIT-based checks need an amd64 host; use --ground-truth here")
|
fmt.Fprintln(os.Stderr, "gasm verify: JIT-based checks need an amd64 host; use --ground-truth here")
|
||||||
return 1
|
return 1
|
||||||
@@ -1224,50 +1131,13 @@ each entry reproduces.
|
|||||||
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
matched, total := 0, 0
|
matched, total, diffs := compareGroundTruth(k.Image(), gt)
|
||||||
for _, name := range names {
|
if diffs > 0 {
|
||||||
fl, _ := k.Func(name)
|
printCodeDiff(k.Image(), gt)
|
||||||
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
|
rc = 1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
|
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
|
||||||
if matched < total {
|
if matched < total || diffs > 0 {
|
||||||
rc = 1
|
rc = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1289,13 +1159,11 @@ each entry reproduces.
|
|||||||
sigs := verify.ExtractSignatures(src)
|
sigs := verify.ExtractSignatures(src)
|
||||||
fuzzed := 0
|
fuzzed := 0
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
sig, ok := sigs[name]
|
if _, ok := sigs[name]; !ok {
|
||||||
if !ok {
|
|
||||||
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
|
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
goCode, ok := gt[name]
|
if _, ok := gt[name]; !ok {
|
||||||
if !ok {
|
|
||||||
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1312,8 +1180,6 @@ each entry reproduces.
|
|||||||
rc = 1
|
rc = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = sig
|
|
||||||
_ = goCode
|
|
||||||
fuzzed++
|
fuzzed++
|
||||||
}
|
}
|
||||||
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
|
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
|
||||||
@@ -1338,7 +1204,7 @@ each entry reproduces.
|
|||||||
|
|
||||||
// Run smoke and ABI checks in parallel, each function in its own child
|
// Run smoke and ABI checks in parallel, each function in its own child
|
||||||
// process: the JIT'd code runs with zeroed or fuzzed arguments, and a
|
// process: the JIT'd code runs with zeroed or fuzzed arguments, and a
|
||||||
// function that dereferences them faults — the crash is reported as a
|
// function that dereferences them faults, the crash is reported as a
|
||||||
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
|
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
|
||||||
if *smoke || *abi {
|
if *smoke || *abi {
|
||||||
type checkResult struct {
|
type checkResult struct {
|
||||||
@@ -1479,7 +1345,7 @@ func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
|
|||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
ws := exitErr.Sys().(syscall.WaitStatus)
|
ws := exitErr.Sys().(syscall.WaitStatus)
|
||||||
if ws.Signaled() {
|
if ws.Signaled() {
|
||||||
return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal())
|
return fmt.Sprintf("%s: CRASH (%v; partial function, use --ground-truth)", funcName, ws.Signal())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Non-zero exit without a signal: the fuzz reported mismatches.
|
// Non-zero exit without a signal: the fuzz reported mismatches.
|
||||||
@@ -1504,12 +1370,12 @@ func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
|
|||||||
// sweepInSubprocess runs the smoke/abi checks for a single function in a
|
// sweepInSubprocess runs the smoke/abi checks for a single function in a
|
||||||
// child process. If the child is killed by a signal (e.g. SIGSEGV from a
|
// child process. If the child is killed by a signal (e.g. SIGSEGV from a
|
||||||
// function that dereferences its zeroed or fuzzed arguments), it returns a
|
// function that dereferences its zeroed or fuzzed arguments), it returns a
|
||||||
// CRASH report instead of dying — the same isolation fuzzInSubprocess
|
// CRASH report instead of dying, the same isolation fuzzInSubprocess
|
||||||
// provides for the fuzz sweep.
|
// provides for the fuzz sweep.
|
||||||
func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) {
|
func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) {
|
||||||
self, err := os.Executable()
|
self, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf(" smoke/abi: FAIL — cannot find self: %v", err), true
|
return fmt.Sprintf(" smoke/abi: FAIL: cannot find self: %v", err), true
|
||||||
}
|
}
|
||||||
args := []string{"verify"}
|
args := []string{"verify"}
|
||||||
if smoke {
|
if smoke {
|
||||||
@@ -1526,7 +1392,7 @@ func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string
|
|||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
||||||
if ok && ws.Signaled() {
|
if ok && ws.Signaled() {
|
||||||
return fmt.Sprintf(" smoke/abi: CRASH (%v — the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true
|
return fmt.Sprintf(" smoke/abi: CRASH (%v: the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Non-zero exit without a signal: the checks themselves failed and
|
// Non-zero exit without a signal: the checks themselves failed and
|
||||||
@@ -1550,7 +1416,7 @@ func sweepCheckLines(out []byte) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// runSweepChecks performs the in-process smoke and ABI checks for one
|
// runSweepChecks performs the in-process smoke and ABI checks for one
|
||||||
// function — the child half of sweepInSubprocess.
|
// function, the child half of sweepInSubprocess.
|
||||||
func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) {
|
func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) {
|
||||||
var msgs []string
|
var msgs []string
|
||||||
failed := false
|
failed := false
|
||||||
@@ -1559,7 +1425,7 @@ func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smok
|
|||||||
args := make([]byte, fl.Args)
|
args := make([]byte, fl.Args)
|
||||||
_, err := k.CallFunc(name, args)
|
_, err := k.CallFunc(name, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err))
|
msgs = append(msgs, fmt.Sprintf(" smoke: FAIL: %v", err))
|
||||||
failed = true
|
failed = true
|
||||||
} else {
|
} else {
|
||||||
msgs = append(msgs, " smoke: OK")
|
msgs = append(msgs, " smoke: OK")
|
||||||
@@ -1579,7 +1445,7 @@ func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smok
|
|||||||
args := make([]byte, fl.Args)
|
args := make([]byte, fl.Args)
|
||||||
_, report, err := k.CallFuncChecked(name, args)
|
_, report, err := k.CallFuncChecked(name, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err))
|
msgs = append(msgs, fmt.Sprintf(" abi: FAIL: %v", err))
|
||||||
failed = true
|
failed = true
|
||||||
} else if !report.OK() {
|
} else if !report.OK() {
|
||||||
msgs = append(msgs, fmt.Sprintf(" abi: %s", report))
|
msgs = append(msgs, fmt.Sprintf(" abi: %s", report))
|
||||||
@@ -1669,7 +1535,7 @@ func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec, scalarSpec string,
|
|||||||
for i := range repeat {
|
for i := range repeat {
|
||||||
out, err := k.CallFunc(funcName, args)
|
out, err := k.CallFunc(funcName, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
|
fmt.Printf(" call %d: FAIL: %v\n", i+1, err)
|
||||||
rc = 1
|
rc = 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-3
@@ -219,8 +219,9 @@ func TestCmdVersion(t *testing.T) {
|
|||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("code = %d", code)
|
t.Fatalf("code = %d", code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(out, version) {
|
got := version()
|
||||||
t.Errorf("version output %q does not mention %q", out, version)
|
if !strings.Contains(out, got) {
|
||||||
|
t.Errorf("version output %q does not mention %q", out, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ func TestVerifySmokeCrashIsolation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
|
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok && ws.Signaled() {
|
||||||
t.Fatalf("verify died from %v — the crash was not isolated:\n%s", ws.Signal(), out)
|
t.Fatalf("verify died from %v; the crash was not isolated:\n%s", ws.Signal(), out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(out), "CRASH") {
|
if !strings.Contains(string(out), "CRASH") {
|
||||||
@@ -292,3 +293,58 @@ func TestSweepCheckLines(t *testing.T) {
|
|||||||
t.Errorf("sweepCheckLines = %q, want %q", got, want)
|
t.Errorf("sweepCheckLines = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRunCorpusAudit drives the corpus audit over a small fixture tree: one
|
||||||
|
// suffixed amd64 file, one suffixed arm64 file whose body is not arm64, one
|
||||||
|
// generic file, and one file that does not parse.
|
||||||
|
func TestRunCorpusAudit(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
write := func(name, src string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("good_amd64.s", "#include \"textflag.h\"\nTEXT ·add(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||||
|
write("bad_arm64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||||
|
write("generic.s", "#include \"textflag.h\"\nTEXT ·g(SB), NOSPLIT, $0-0\n\tRET\n")
|
||||||
|
write("broken.s", "#include \"textflag.h\"\nTEXT ·b(SB), NOSPLIT, $0-0\n\tJMP nowhere\n\tRET\n")
|
||||||
|
|
||||||
|
stats, err := runCorpusAudit(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runCorpusAudit: %v", err)
|
||||||
|
}
|
||||||
|
if stats.files != 4 {
|
||||||
|
t.Errorf("files = %d, want 4", stats.files)
|
||||||
|
}
|
||||||
|
if stats.generic != 2 {
|
||||||
|
t.Errorf("generic = %d, want 2 (generic.s and broken.s)", stats.generic)
|
||||||
|
}
|
||||||
|
// good_amd64 and generic.s assemble everywhere they are attempted.
|
||||||
|
if stats.full != 2 {
|
||||||
|
t.Errorf("full = %d, want 2", stats.full)
|
||||||
|
}
|
||||||
|
get := func(name string) *corpusTally {
|
||||||
|
for i, tg := range stats.targets {
|
||||||
|
if tg.name == name {
|
||||||
|
return stats.tallies[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("no tally for %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// amd64: good_amd64 + generic.s + broken.s; the broken file fails to parse.
|
||||||
|
if a := get("amd64"); a.attempted != 3 || a.assembled != 2 {
|
||||||
|
t.Errorf("amd64 = %d/%d, want 2/3", a.assembled, a.attempted)
|
||||||
|
}
|
||||||
|
// arm64: bad_arm64 (MOVQ is not arm64) + generic.s + broken.s.
|
||||||
|
if a := get("arm64"); a.attempted != 3 || a.assembled != 1 {
|
||||||
|
t.Errorf("arm64 = %d/%d, want 1/3", a.assembled, a.attempted)
|
||||||
|
}
|
||||||
|
if r := get("amd64").reasons["instruction not encodable"]; r != 0 {
|
||||||
|
t.Errorf("amd64 unexpected unencodable reason: %d", r)
|
||||||
|
}
|
||||||
|
if r := get("arm64").reasons["instruction not encodable"]; r != 1 {
|
||||||
|
t.Errorf("arm64 unencodable reasons = %d, want 1", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestManPagesTrackTheCLI builds the binary once, then compares every
|
||||||
|
// command's live `-h` output with its docs/man/gasm-<command>.1 page: the
|
||||||
|
// flag sets must agree both ways, and the page's SYNOPSIS line must carry
|
||||||
|
// the command's usage line. A flag or a usage change that skips the man
|
||||||
|
// page fails here, so the pages cannot drift from the binary.
|
||||||
|
func TestManPagesTrackTheCLI(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("builds the gasm binary")
|
||||||
|
}
|
||||||
|
bin := filepath.Join(t.TempDir(), "gasm")
|
||||||
|
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("build gasm: %v\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range []string{
|
||||||
|
"tokens", "parse", "fmt", "lint", "asm", "dis", "verify",
|
||||||
|
"debug", "diff", "profile", "audit-instructions", "scaffold", "lsp",
|
||||||
|
} {
|
||||||
|
t.Run(cmd, func(t *testing.T) {
|
||||||
|
raw, err := os.ReadFile(filepath.Join("..", "..", "docs", "man", "gasm-"+cmd+".1"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read man page: %v", err)
|
||||||
|
}
|
||||||
|
page := string(raw)
|
||||||
|
|
||||||
|
out, _ := exec.Command(bin, cmd, "-h").CombinedOutput()
|
||||||
|
help := string(out)
|
||||||
|
|
||||||
|
binFlags := helpFlags(help)
|
||||||
|
pageFlags := roffFlags(page)
|
||||||
|
for f := range binFlags {
|
||||||
|
if !pageFlags[f] {
|
||||||
|
t.Errorf("flag -%s is in the binary's help but missing from the man page", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for f := range pageFlags {
|
||||||
|
if !binFlags[f] {
|
||||||
|
t.Errorf("flag -%s is in the man page but the binary does not accept it", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want := helpUsage(help)
|
||||||
|
got := roffSynopsis(page)
|
||||||
|
if want != "" && got != want {
|
||||||
|
t.Errorf("SYNOPSIS drift:\n page: %s\nbinary: %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpFlags extracts the flag names from a `gasm <cmd> -h` output.
|
||||||
|
func helpFlags(help string) map[string]bool {
|
||||||
|
m := map[string]bool{}
|
||||||
|
inFlags := false
|
||||||
|
for line := range strings.SplitSeq(help, "\n") {
|
||||||
|
if strings.TrimRight(line, " \t") == "Flags:" {
|
||||||
|
inFlags = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inFlags {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(line, " -") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token := strings.FieldsFunc(strings.TrimLeft(line, " "), func(r rune) bool {
|
||||||
|
return r == ' ' || r == '\t'
|
||||||
|
})
|
||||||
|
if len(token) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m[strings.TrimLeft(token[0], "-")] = true
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
var roffEscape = regexp.MustCompile(`\\f[BIRP]`)
|
||||||
|
|
||||||
|
// roffFlags extracts the flag names from a man page's OPTIONS section.
|
||||||
|
func roffFlags(page string) map[string]bool {
|
||||||
|
m := map[string]bool{}
|
||||||
|
inOptions := false
|
||||||
|
for line := range strings.SplitSeq(page, "\n") {
|
||||||
|
if strings.HasPrefix(line, ".SH ") {
|
||||||
|
inOptions = strings.HasPrefix(line, ".SH OPTIONS")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inOptions {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Flag entries are written as either `.B \-flag` or `\fB\-flag`.
|
||||||
|
var body string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, `.B \-`):
|
||||||
|
body = line[3:]
|
||||||
|
case strings.HasPrefix(line, `\fB\-`):
|
||||||
|
body = line[1:]
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := roffEscape.ReplaceAllString(body, "")
|
||||||
|
name = strings.ReplaceAll(name, `\-`, "-")
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if i := strings.IndexAny(name, " \t"); i >= 0 {
|
||||||
|
name = name[:i]
|
||||||
|
}
|
||||||
|
m[strings.TrimLeft(name, "-")] = true
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpUsage returns the command's usage line without the "Usage: " prefix.
|
||||||
|
func helpUsage(help string) string {
|
||||||
|
for line := range strings.SplitSeq(help, "\n") {
|
||||||
|
if strings.HasPrefix(line, "Usage: ") {
|
||||||
|
return normaliseUsage(line[len("Usage: "):])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// roffSynopsis returns the page's SYNOPSIS usage line, unescaped.
|
||||||
|
func roffSynopsis(page string) string {
|
||||||
|
inSyn := false
|
||||||
|
for line := range strings.SplitSeq(page, "\n") {
|
||||||
|
if strings.HasPrefix(line, ".SH ") {
|
||||||
|
inSyn = strings.HasPrefix(line, ".SH SYNOPSIS")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inSyn || !strings.HasPrefix(line, ".B ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return normaliseUsage(strings.ReplaceAll(line[3:], `\-`, "-"))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaliseUsage flattens whitespace and drops the roff font escapes so that
|
||||||
|
// the binary's usage line and the page's SYNOPSIS line compare equal.
|
||||||
|
func normaliseUsage(s string) string {
|
||||||
|
s = roffEscape.ReplaceAllString(s, "")
|
||||||
|
return strings.Join(strings.Fields(s), " ")
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// file: a Go test that seeds random states, drives both the assembly kernel
|
// file: a Go test that seeds random states, drives both the assembly kernel
|
||||||
// and a caller-provided portable reference, and compares the outputs
|
// and a caller-provided portable reference, and compares the outputs
|
||||||
// byte-for-byte. The lesson this encodes: a pipeline-level fuzz cannot see
|
// byte-for-byte. The lesson this encodes: a pipeline-level fuzz cannot see
|
||||||
// an unwired kernel — only a direct-call differential against the portable
|
// an unwired kernel, only a direct-call differential against the portable
|
||||||
// specification can, so every kernel ships with one.
|
// specification can, so every kernel ships with one.
|
||||||
//
|
//
|
||||||
// The generated file follows two conventions the caller fills in:
|
// The generated file follows two conventions the caller fills in:
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unifiedDiff renders a unified diff with three lines of context between the
|
||||||
|
// two line slices, in the form `gofmt -d` prints. An empty result means the
|
||||||
|
// inputs are identical.
|
||||||
|
func unifiedDiff(name string, a, b []string) string {
|
||||||
|
if slices.Equal(a, b) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
fmt.Fprintf(&out, "--- %s\n+++ %s\n", name, name)
|
||||||
|
|
||||||
|
// Longest common subsequence over the lines (assembly files are small
|
||||||
|
// enough for the quadratic table).
|
||||||
|
n, m := len(a), len(b)
|
||||||
|
lcs := make([][]int, n+1)
|
||||||
|
for i := range lcs {
|
||||||
|
lcs[i] = make([]int, m+1)
|
||||||
|
}
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
for j := m - 1; j >= 0; j-- {
|
||||||
|
if a[i] == b[j] {
|
||||||
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
||||||
|
} else if lcs[i+1][j] >= lcs[i][j+1] {
|
||||||
|
lcs[i][j] = lcs[i+1][j]
|
||||||
|
} else {
|
||||||
|
lcs[i][j] = lcs[i][j+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the LCS once, assigning every op its absolute position in both
|
||||||
|
// files (1-based, the position an insertion sits before).
|
||||||
|
type op struct {
|
||||||
|
kind byte // ' ', '-' or '+'
|
||||||
|
aLine, bLine int
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
var ops []op
|
||||||
|
aPos, bPos := 0, 0
|
||||||
|
emit := func(kind byte, text string) {
|
||||||
|
ops = append(ops, op{kind: kind, aLine: aPos + 1, bLine: bPos + 1, text: text})
|
||||||
|
switch kind {
|
||||||
|
case ' ':
|
||||||
|
aPos++
|
||||||
|
bPos++
|
||||||
|
case '-':
|
||||||
|
aPos++
|
||||||
|
case '+':
|
||||||
|
bPos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i, j := 0, 0
|
||||||
|
for i < n && j < m {
|
||||||
|
switch {
|
||||||
|
case a[i] == b[j]:
|
||||||
|
emit(' ', a[i])
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
case lcs[i+1][j] >= lcs[i][j+1]:
|
||||||
|
emit('-', a[i])
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
emit('+', b[j])
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; i < n; i++ {
|
||||||
|
emit('-', a[i])
|
||||||
|
}
|
||||||
|
for ; j < m; j++ {
|
||||||
|
emit('+', b[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group the edits into hunks: consecutive changes separated by more than
|
||||||
|
// twice the context lines start a new hunk.
|
||||||
|
const context = 3
|
||||||
|
var changes []int
|
||||||
|
for k, o := range ops {
|
||||||
|
if o.kind != ' ' {
|
||||||
|
changes = append(changes, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for g := 0; g < len(changes); {
|
||||||
|
last := g
|
||||||
|
for last+1 < len(changes) && changes[last+1]-changes[last]-1 <= 2*context {
|
||||||
|
last++
|
||||||
|
}
|
||||||
|
lo := max(0, changes[g]-context)
|
||||||
|
hi := min(len(ops), changes[last]+1+context)
|
||||||
|
// The header numbers are the first line of each side actually shown:
|
||||||
|
// the first context, deletion or insertion line. A hunk that shows
|
||||||
|
// no old lines is a pure insertion and reports the position it sits
|
||||||
|
// before (0 at the top of the file); the mirror rule holds for a
|
||||||
|
// pure deletion.
|
||||||
|
aStart := ops[lo].aLine - 1
|
||||||
|
bStart := ops[lo].bLine - 1
|
||||||
|
countA, countB := 0, 0
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
switch o.kind {
|
||||||
|
case ' ':
|
||||||
|
countA++
|
||||||
|
countB++
|
||||||
|
case '-':
|
||||||
|
countA++
|
||||||
|
case '+':
|
||||||
|
countB++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
if o.kind != '+' {
|
||||||
|
aStart = o.aLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
if o.kind != '-' {
|
||||||
|
bStart = o.bLine
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", aStart, countA, bStart, countB)
|
||||||
|
for _, o := range ops[lo:hi] {
|
||||||
|
out.WriteByte(o.kind)
|
||||||
|
out.WriteString(o.text)
|
||||||
|
out.WriteByte('\n')
|
||||||
|
}
|
||||||
|
g = last + 1
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lines(ss ...string) []string { return ss }
|
||||||
|
|
||||||
|
func TestUnifiedDiffIdentical(t *testing.T) {
|
||||||
|
if got := unifiedDiff("f", lines("a", "b"), lines("a", "b")); got != "" {
|
||||||
|
t.Errorf("identical inputs produced %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffSingleChange(t *testing.T) {
|
||||||
|
a := lines("1", "2", "3", "4", "5", "6", "7", "8")
|
||||||
|
b := lines("1", "2", "3!", "4", "5", "6", "7", "8")
|
||||||
|
want := "--- f\n+++ f\n" +
|
||||||
|
"@@ -1,6 +1,6 @@\n" +
|
||||||
|
" 1\n 2\n-3\n+3!\n 4\n 5\n 6\n"
|
||||||
|
if got := unifiedDiff("f", a, b); got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffInsertAtStart(t *testing.T) {
|
||||||
|
got := unifiedDiff("f", lines("x"), lines("new", "x"))
|
||||||
|
// The single existing line is shown as trailing context, so the hunk
|
||||||
|
// covers it.
|
||||||
|
want := "--- f\n+++ f\n@@ -1,1 +1,2 @@\n+new\n x\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffDeleteAtEnd(t *testing.T) {
|
||||||
|
got := unifiedDiff("f", lines("x", "y"), lines("x"))
|
||||||
|
want := "--- f\n+++ f\n@@ -1,2 +1,1 @@\n x\n-y\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnifiedDiffTwoHunks(t *testing.T) {
|
||||||
|
var a, b []string
|
||||||
|
for i := 1; i <= 20; i++ {
|
||||||
|
a = append(a, itoa(i))
|
||||||
|
b = append(b, itoa(i))
|
||||||
|
}
|
||||||
|
b[1] = "2!"
|
||||||
|
b[17] = "18!"
|
||||||
|
got := unifiedDiff("f", a, b)
|
||||||
|
if !strings.Contains(got, "@@ -1,5 +1,5 @@\n 1\n-2\n+2!\n 3\n 4\n 5\n") {
|
||||||
|
t.Errorf("first hunk wrong:\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "@@ -15,6 +15,6 @@\n 15\n 16\n 17\n-18\n+18!\n 19\n 20\n") {
|
||||||
|
t.Errorf("second hunk wrong:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnifiedDiffAdjacentHunks merges changes separated by exactly twice the
|
||||||
|
// context into one hunk.
|
||||||
|
func TestUnifiedDiffAdjacentHunks(t *testing.T) {
|
||||||
|
a := lines("1", "2", "3", "4", "5", "6", "7", "8")
|
||||||
|
b := slices.Clone(a)
|
||||||
|
b[0] = "1!"
|
||||||
|
b[7] = "8!"
|
||||||
|
got := unifiedDiff("f", a, b)
|
||||||
|
want := "--- f\n+++ f\n" +
|
||||||
|
"@@ -1,8 +1,8 @@\n" +
|
||||||
|
"-1\n+1!\n 2\n 3\n 4\n 5\n 6\n 7\n-8\n+8!\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("diff = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(n int) string {
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var buf [4]byte
|
||||||
|
i := len(buf)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
buf[i] = byte('0' + n%10)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(buf[i:])
|
||||||
|
}
|
||||||
+4
-5
@@ -36,7 +36,7 @@ type Condition struct {
|
|||||||
func (c *Condition) Eval(regs *Regs) bool {
|
func (c *Condition) Eval(regs *Regs) bool {
|
||||||
actual, ok := regs.RegValue(c.Reg)
|
actual, ok := regs.RegValue(c.Reg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true // unknown register — don't block
|
return true // unknown register, don't block
|
||||||
}
|
}
|
||||||
var expected uint64
|
var expected uint64
|
||||||
switch {
|
switch {
|
||||||
@@ -48,7 +48,7 @@ func (c *Condition) Eval(regs *Regs) bool {
|
|||||||
}
|
}
|
||||||
expected = v
|
expected = v
|
||||||
case c.MemAddr != 0:
|
case c.MemAddr != 0:
|
||||||
// Register-memory comparison — requires a Session, not available here.
|
// Register-memory comparison, requires a Session, not available here.
|
||||||
// Fall back to treating as constant (the caller should resolve).
|
// Fall back to treating as constant (the caller should resolve).
|
||||||
expected = c.Value
|
expected = c.Value
|
||||||
default:
|
default:
|
||||||
@@ -72,8 +72,7 @@ func (c *Condition) Eval(regs *Regs) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Breakpoints manages the set of breakpoints for a Session.
|
// Breakpoints manages the software breakpoints of one Session.
|
||||||
// Breakpoints manages software breakpoints for a debuggee.
|
|
||||||
type Breakpoints struct {
|
type Breakpoints struct {
|
||||||
t tracer
|
t tracer
|
||||||
bps map[uint64]*Breakpoint
|
bps map[uint64]*Breakpoint
|
||||||
@@ -200,7 +199,7 @@ func (bm *Breakpoints) HandleTrap(regs *Regs) *Breakpoint {
|
|||||||
}
|
}
|
||||||
// Check the condition (if any).
|
// Check the condition (if any).
|
||||||
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
if bp.Cond != nil && !bp.Cond.Eval(regs) {
|
||||||
// Condition not met — restore the byte but do NOT rewind RIP.
|
// Condition not met, restore the byte but do NOT rewind RIP.
|
||||||
// The process continues from the next instruction (past the INT3).
|
// The process continues from the next instruction (past the INT3).
|
||||||
word, err := bm.t.Peek(trapAddr)
|
word, err := bm.t.Peek(trapAddr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
+4
-5
@@ -275,8 +275,7 @@ func TestBreakpointInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWatchpointSlotTracking(t *testing.T) {
|
func TestWatchpointSlotTracking(t *testing.T) {
|
||||||
wpSlots = [4]bool{} // reset
|
s := &Session{} // per-session slots start free
|
||||||
s := &Session{}
|
|
||||||
|
|
||||||
// All four slots are free initially.
|
// All four slots are free initially.
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
@@ -289,8 +288,8 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
// Manually mark slots 0 and 2 as used (simulating successful SetWatchpoint).
|
||||||
wpSlots[0] = true
|
s.wpSlots[0] = true
|
||||||
wpSlots[2] = true
|
s.wpSlots[2] = true
|
||||||
|
|
||||||
if !s.IsWatchpointSlotUsed(0) {
|
if !s.IsWatchpointSlotUsed(0) {
|
||||||
t.Error("slot 0 should be in use")
|
t.Error("slot 0 should be in use")
|
||||||
@@ -318,7 +317,7 @@ func TestWatchpointSlotTracking(t *testing.T) {
|
|||||||
|
|
||||||
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
// Mark all slots used: FindFreeWatchpointSlot returns -1.
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
wpSlots[i] = true
|
s.wpSlots[i] = true
|
||||||
}
|
}
|
||||||
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
if got := s.FindFreeWatchpointSlot(); got != -1 {
|
||||||
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
t.Errorf("FindFreeWatchpointSlot() with all slots used = %d, want -1", got)
|
||||||
|
|||||||
@@ -9,27 +9,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
// memory and returns its text representation and length in bytes.
|
// memory and returns its text representation and length in bytes.
|
||||||
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
||||||
// Read up to 15 bytes (max x86 instruction length).
|
|
||||||
mem, err := s.ReadMemory(addr, 15)
|
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 {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
}
|
ins, err := disasm.Decode(arch.AMD64, mem, addr)
|
||||||
inst, err := x86asm.Decode(mem, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "???", 1, nil
|
return "", 0, err
|
||||||
}
|
}
|
||||||
text := x86asm.IntelSyntax(inst, addr, nil)
|
return ins.Text, ins.Len, nil
|
||||||
return text, inst.Len, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisassembleN decodes up to n instructions starting at addr and returns
|
// DisassembleN decodes up to n instructions starting at addr and returns
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.org/x/arch/arm64/arm64asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
@@ -18,12 +19,11 @@ func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
inst, err := arm64asm.Decode(mem)
|
ins, err := disasm.Decode(arch.ARM64, mem, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "???", 4, nil
|
return "", 0, err
|
||||||
}
|
}
|
||||||
text := arm64asm.GoSyntax(inst, addr, nil, nil)
|
return ins.Text, ins.Len, nil
|
||||||
return text, 4, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisassembleN decodes up to n instructions starting at addr.
|
// DisassembleN decodes up to n instructions starting at addr.
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.org/x/arch/loong64/loong64asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
@@ -18,12 +19,11 @@ func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
inst, err := loong64asm.Decode(mem)
|
ins, err := disasm.Decode(arch.LOONG64, mem, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "???", 4, nil
|
return "", 0, err
|
||||||
}
|
}
|
||||||
text := loong64asm.GoSyntax(inst, addr, nil)
|
return ins.Text, ins.Len, nil
|
||||||
return text, 4, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisassembleN decodes up to n instructions starting at addr.
|
// DisassembleN decodes up to n instructions starting at addr.
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.org/x/arch/riscv64/riscv64asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/disasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disassemble decodes the instruction at the given address in the debuggee's
|
// Disassemble decodes the instruction at the given address in the debuggee's
|
||||||
@@ -18,12 +19,11 @@ func (s *Session) Disassemble(addr uint64) (string, int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
inst, err := riscv64asm.Decode(mem)
|
ins, err := disasm.Decode(arch.RISCV, mem, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "???", 4, nil
|
return "", 0, err
|
||||||
}
|
}
|
||||||
text := riscv64asm.GoSyntax(inst, addr, nil, nil)
|
return ins.Text, ins.Len, nil
|
||||||
return text, inst.Len, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisassembleN decodes up to n instructions starting at addr.
|
// DisassembleN decodes up to n instructions starting at addr.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Session struct {
|
|||||||
stopped bool
|
stopped bool
|
||||||
exited bool
|
exited bool
|
||||||
codeBase uint64 // base address of the JIT code in the debuggee
|
codeBase uint64 // base address of the JIT code in the debuggee
|
||||||
|
wpSlots [16]bool // hardware watchpoint slots in use (DR0-DR3, arm64 BADVR0-15)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
// Launch starts the debuggee subprocess (gasm debug --target ...) and
|
||||||
|
|||||||
@@ -20,14 +20,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3 // trigger on read or write
|
WatchRead WatchpointType = 3 // trigger on read or write
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy (DR0-DR3).
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
// FindFreeWatchpointSlot returns the index of the first free watchpoint slot
|
||||||
// (0-3), or -1 if all four hardware watchpoints are in use.
|
// (0-3), or -1 if all four hardware watchpoints are in use.
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range 4 {
|
for i := range 4 {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +93,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return fmt.Errorf("debug: set DR7: %w", err)
|
return fmt.Errorf("debug: set DR7: %w", err)
|
||||||
}
|
}
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot > 3 {
|
if slot < 0 || slot > 3 {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
return fmt.Errorf("debug: watchpoint slot must be 0-3")
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
dr7, err := ptracePeekUser(s.pid, 0x38)
|
dr7, err := ptracePeekUser(s.pid, 0x38)
|
||||||
@@ -116,14 +113,14 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
if err := ptracePokeUser(s.pid, 0x38, dr7); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearAllWatchpoints removes all hardware watchpoints.
|
// ClearAllWatchpoints removes all hardware watchpoints.
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := range 4 {
|
for slot := range 4 {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [16]bool // arm64 supports up to 16 watchpoints
|
|
||||||
|
|
||||||
const maxWatchpoints = 16
|
const maxWatchpoints = 16
|
||||||
|
|
||||||
// hwBreakState mirrors the kernel's struct user_hwdebug_state.
|
// hwBreakState mirrors the kernel's struct user_hwdebug_state.
|
||||||
@@ -45,7 +42,7 @@ const (
|
|||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +53,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint on the given address.
|
// SetWatchpoint installs a hardware watchpoint on the given address.
|
||||||
@@ -64,7 +61,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +102,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint: %w", err)
|
return fmt.Errorf("debug: set watchpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +123,13 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := s.setHWBreakState(state); err != nil {
|
if err := s.setHWBreakState(state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
const maxWatchpoints = 4
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint.
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
if size != 1 && size != 2 && size != 4 && size != 8 {
|
if size != 1 && size != 2 && size != 4 && size != 8 {
|
||||||
@@ -85,7 +82,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,20 +90,20 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,11 @@ const (
|
|||||||
WatchRead WatchpointType = 3
|
WatchRead WatchpointType = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// wpSlots tracks watchpoint slot occupancy.
|
|
||||||
var wpSlots [4]bool
|
|
||||||
|
|
||||||
const maxWatchpoints = 4
|
const maxWatchpoints = 4
|
||||||
|
|
||||||
func (s *Session) FindFreeWatchpointSlot() int {
|
func (s *Session) FindFreeWatchpointSlot() int {
|
||||||
for i := range maxWatchpoints {
|
for i := range maxWatchpoints {
|
||||||
if !wpSlots[i] {
|
if !s.wpSlots[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +36,7 @@ func (s *Session) IsWatchpointSlotUsed(slot int) bool {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return wpSlots[slot]
|
return s.wpSlots[slot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWatchpoint installs a hardware watchpoint.
|
// SetWatchpoint installs a hardware watchpoint.
|
||||||
@@ -47,7 +44,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d already in use", slot)
|
||||||
}
|
}
|
||||||
if size != 1 && size != 2 && size != 4 && size != 8 {
|
if size != 1 && size != 2 && size != 4 && size != 8 {
|
||||||
@@ -87,7 +84,7 @@ func (s *Session) SetWatchpoint(slot int, addr uint64, typ WatchpointType, size
|
|||||||
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
return fmt.Errorf("debug: set watchpoint control: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wpSlots[slot] = true
|
s.wpSlots[slot] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +92,7 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if slot < 0 || slot >= maxWatchpoints {
|
if slot < 0 || slot >= maxWatchpoints {
|
||||||
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
return fmt.Errorf("debug: watchpoint slot must be 0-%d", maxWatchpoints-1)
|
||||||
}
|
}
|
||||||
if !wpSlots[slot] {
|
if !s.wpSlots[slot] {
|
||||||
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
return fmt.Errorf("debug: watchpoint slot %d is not in use", slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +100,13 @@ func (s *Session) ClearWatchpoint(slot int) error {
|
|||||||
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
if err := ptracePokeUser(s.pid, uintptr(0x1001+slot*8), 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
wpSlots[slot] = false
|
s.wpSlots[slot] = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) ClearAllWatchpoints() error {
|
func (s *Session) ClearAllWatchpoints() error {
|
||||||
for slot := 0; slot < maxWatchpoints; slot++ {
|
for slot := 0; slot < maxWatchpoints; slot++ {
|
||||||
if wpSlots[slot] {
|
if s.wpSlots[slot] {
|
||||||
if err := s.ClearWatchpoint(slot); err != nil {
|
if err := s.ClearWatchpoint(slot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Package disasm decodes machine code back to instruction text for the four
|
||||||
|
// architectures gasm assembles. It is a thin, platform-independent wrapper
|
||||||
|
// over golang.org/x/arch and backs both the `gasm dis` command and the live
|
||||||
|
// debugger views.
|
||||||
|
package disasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/arch/arm64/arm64asm"
|
||||||
|
"golang.org/x/arch/loong64/loong64asm"
|
||||||
|
"golang.org/x/arch/riscv64/riscv64asm"
|
||||||
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instruction is one decoded instruction: its text form, its length in bytes
|
||||||
|
// and the address it was decoded at.
|
||||||
|
type Instruction struct {
|
||||||
|
Addr uint64
|
||||||
|
Text string
|
||||||
|
Len int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes the instruction at the start of code, located at addr.
|
||||||
|
// code needs to hold at least the one instruction being decoded (amd64 may
|
||||||
|
// consume up to 15 bytes). Undecodable bytes yield the placeholder text "???"
|
||||||
|
// and a length of one word (four bytes, one on amd64) so that a listing can
|
||||||
|
// keep making progress, mirroring the debugger's behaviour.
|
||||||
|
func Decode(a arch.Arch, code []byte, addr uint64) (Instruction, error) {
|
||||||
|
if len(code) == 0 {
|
||||||
|
return Instruction{}, fmt.Errorf("disasm: empty input")
|
||||||
|
}
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64:
|
||||||
|
if len(code) < 4 {
|
||||||
|
return Instruction{}, fmt.Errorf("disasm: need 4 bytes, have %d", len(code))
|
||||||
|
}
|
||||||
|
inst, err := arm64asm.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
return Instruction{Addr: addr, Text: "???", Len: 4}, nil
|
||||||
|
}
|
||||||
|
return Instruction{Addr: addr, Text: arm64asm.GoSyntax(inst, addr, nil, nil), Len: 4}, nil
|
||||||
|
|
||||||
|
case arch.RISCV:
|
||||||
|
// The compressed extensions are decoded transparently; a 16-bit
|
||||||
|
// instruction only needs its two bytes.
|
||||||
|
inst, err := riscv64asm.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
return Instruction{Addr: addr, Text: "???", Len: 2}, nil
|
||||||
|
}
|
||||||
|
return Instruction{Addr: addr, Text: riscv64asm.GoSyntax(inst, addr, nil, nil), Len: inst.Len}, nil
|
||||||
|
|
||||||
|
case arch.LOONG64:
|
||||||
|
if len(code) < 4 {
|
||||||
|
return Instruction{}, fmt.Errorf("disasm: need 4 bytes, have %d", len(code))
|
||||||
|
}
|
||||||
|
inst, err := loong64asm.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
return Instruction{Addr: addr, Text: "???", Len: 4}, nil
|
||||||
|
}
|
||||||
|
return Instruction{Addr: addr, Text: loong64asm.GoSyntax(inst, addr, nil), Len: 4}, nil
|
||||||
|
|
||||||
|
default: // amd64
|
||||||
|
inst, err := x86asm.Decode(code, 64)
|
||||||
|
if err != nil {
|
||||||
|
return Instruction{Addr: addr, Text: "???", Len: 1}, nil
|
||||||
|
}
|
||||||
|
return Instruction{Addr: addr, Text: x86asm.IntelSyntax(inst, addr, nil), Len: inst.Len}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block decodes up to max instructions from code starting at addr and returns
|
||||||
|
// them in order. Decoding stops at the end of code or once an instruction
|
||||||
|
// would run past it.
|
||||||
|
func Block(a arch.Arch, code []byte, addr uint64, max int) []Instruction {
|
||||||
|
var out []Instruction
|
||||||
|
pc := 0
|
||||||
|
for len(out) < max && pc < len(code) {
|
||||||
|
ins, err := Decode(a, code[pc:], addr+uint64(pc))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ins.Len <= 0 || pc+ins.Len > len(code) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out = append(out, ins)
|
||||||
|
pc += ins.Len
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package disasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeKnownBytes(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
a arch.Arch
|
||||||
|
code []byte
|
||||||
|
text string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{arch.AMD64, []byte{0x55}, "push rbp", 1},
|
||||||
|
{arch.AMD64, []byte{0x48, 0x89, 0xE5}, "mov rbp, rsp", 3},
|
||||||
|
{arch.ARM64, []byte{0xc0, 0x03, 0x5f, 0xd6}, "RET", 4},
|
||||||
|
{arch.RISCV, []byte{0x67, 0x80, 0x00, 0x00}, "RET", 4},
|
||||||
|
{arch.LOONG64, []byte{0x20, 0x00, 0x00, 0x4c}, "RET", 4},
|
||||||
|
} {
|
||||||
|
ins, err := Decode(tt.a, tt.code, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: %v", tt.a, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ins.Text != tt.text || ins.Len != tt.want {
|
||||||
|
t.Errorf("%s: % x decoded to %q (%d bytes), want %q (%d)",
|
||||||
|
tt.a, tt.code, ins.Text, ins.Len, tt.text, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeUndecodable(t *testing.T) {
|
||||||
|
// Zero words do not encode a usable instruction on arm64 and loong64; the
|
||||||
|
// placeholder keeps a listing going. RISC-V is the exception: an all-zero
|
||||||
|
// word is the defined UNIMP instruction.
|
||||||
|
for _, a := range []arch.Arch{arch.ARM64, arch.LOONG64} {
|
||||||
|
ins, err := Decode(a, []byte{0, 0, 0, 0}, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", a, err)
|
||||||
|
}
|
||||||
|
if ins.Text != "???" {
|
||||||
|
t.Errorf("%s: text = %q, want ???", a, ins.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The compressed quadrant claims the zero halfword first, so the zero
|
||||||
|
// word decodes as the 2-byte compressed UNIMP.
|
||||||
|
if ins, err := Decode(arch.RISCV, []byte{0, 0, 0, 0}, 0); err != nil || ins.Text != "UNIMP" || ins.Len != 2 {
|
||||||
|
t.Errorf("riscv zero word: %q len %d err %v, want UNIMP with 2 bytes", ins.Text, ins.Len, err)
|
||||||
|
}
|
||||||
|
if _, err := Decode(arch.ARM64, []byte{0, 0}, 0); err == nil {
|
||||||
|
t.Error("short input: expected an error")
|
||||||
|
}
|
||||||
|
if _, err := Decode(arch.AMD64, nil, 0); err == nil {
|
||||||
|
t.Error("empty input: expected an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBlockRoundTrip assembles a small kernel with the gasm encoder for every
|
||||||
|
// architecture and disassembles it back: the listing must cover the whole
|
||||||
|
// function and end in RET.
|
||||||
|
func TestBlockRoundTrip(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
a arch.Arch
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{arch.AMD64, "k_amd64.s"},
|
||||||
|
{arch.ARM64, "k_arm64.s"},
|
||||||
|
{arch.RISCV, "k_riscv64.s"},
|
||||||
|
{arch.LOONG64, "k_loong64.s"},
|
||||||
|
} {
|
||||||
|
src := "TEXT \u00b7k(SB), NOSPLIT, $0\n\tMOVQ AX, CX\n\tRET\n"
|
||||||
|
if tt.a != arch.AMD64 {
|
||||||
|
src = "TEXT \u00b7k(SB), NOSPLIT, $0\n\tRET\n"
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(tt.name, src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: parse: %v", tt.a, errs)
|
||||||
|
}
|
||||||
|
img, err := assemble(t, tt.a, f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: assemble: %v", tt.a, err)
|
||||||
|
}
|
||||||
|
fn := img.Funcs[0]
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
|
||||||
|
ins := Block(tt.a, code, 0, 100)
|
||||||
|
if len(ins) == 0 {
|
||||||
|
t.Fatalf("%s: empty listing", tt.a)
|
||||||
|
}
|
||||||
|
consumed := 0
|
||||||
|
for _, in := range ins {
|
||||||
|
if in.Text == "" || in.Text == "???" {
|
||||||
|
t.Errorf("%s: undecoded instruction at %#x: %q", tt.a, in.Addr, in.Text)
|
||||||
|
}
|
||||||
|
consumed += in.Len
|
||||||
|
}
|
||||||
|
if consumed != len(code) {
|
||||||
|
t.Errorf("%s: listing consumed %d of %d bytes", tt.a, consumed, len(code))
|
||||||
|
}
|
||||||
|
if last := ins[len(ins)-1]; !strings.Contains(strings.ToLower(last.Text), "ret") {
|
||||||
|
t.Errorf("%s: last instruction = %q, want RET", tt.a, last.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockLimits(t *testing.T) {
|
||||||
|
code := []byte{0x55, 0x55, 0x55, 0x55, 0x55}
|
||||||
|
if got := Block(arch.AMD64, code, 0, 3); len(got) != 3 {
|
||||||
|
t.Errorf("max=3 produced %d instructions, want 3", len(got))
|
||||||
|
}
|
||||||
|
if got := Block(arch.AMD64, code, 0, 100); len(got) != 5 {
|
||||||
|
t.Errorf("code end produced %d instructions, want 5", len(got))
|
||||||
|
}
|
||||||
|
if got := Block(arch.AMD64, nil, 0, 3); len(got) != 0 {
|
||||||
|
t.Errorf("empty code produced %d instructions, want 0", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble assembles the parsed file with the encoder for a.
|
||||||
|
func assemble(t *testing.T, a arch.Arch, f *ast.File) (*asm.Image, error) {
|
||||||
|
t.Helper()
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64:
|
||||||
|
return asm.AssembleFileARM64(f)
|
||||||
|
case arch.RISCV:
|
||||||
|
return asm.AssembleFileRISCV(f)
|
||||||
|
case arch.LOONG64:
|
||||||
|
return asm.AssembleFileLOONG64(f)
|
||||||
|
default:
|
||||||
|
return asm.AssembleFile(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-14
@@ -4,7 +4,9 @@ How gasm-devkit is put together and why.
|
|||||||
|
|
||||||
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
||||||
|
|
||||||
## Design goals
|
## Overview
|
||||||
|
|
||||||
|
Three design goals shape everything below.
|
||||||
|
|
||||||
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
|
||||||
language server all need to *reason* about assembly, not just colour it.
|
language server all need to *reason* about assembly, not just colour it.
|
||||||
@@ -20,10 +22,10 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
|
|||||||
through two vendor-neutral interfaces: a CLI and an LSP server. No editor
|
through two vendor-neutral interfaces: a CLI and an LSP server. No editor
|
||||||
owns the toolkit; the toolkit is offered to editors on standard terms.
|
owns the toolkit; the toolkit is offered to editors on standard terms.
|
||||||
|
|
||||||
## Pipeline
|
The components, and how data moves between them:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
flowchart TD
|
||||||
SRC["source .s"] --> LEX["lexer<br/>token stream"]
|
SRC["source .s"] --> LEX["lexer<br/>token stream"]
|
||||||
LEX --> PAR["parser<br/>AST + diagnostics"]
|
LEX --> PAR["parser<br/>AST + diagnostics"]
|
||||||
LEX --> FMT["format<br/>re-space tokens"]
|
LEX --> FMT["format<br/>re-space tokens"]
|
||||||
@@ -42,9 +44,34 @@ graph TD
|
|||||||
|
|
||||||
The lexer is the shared foundation: the parser builds the AST from it, the
|
The lexer is the shared foundation: the parser builds the AST from it, the
|
||||||
formatter re-spaces its tokens directly, and the language server uses it for
|
formatter re-spaces its tokens directly, and the language server uses it for
|
||||||
semantic highlighting.
|
semantic highlighting. The phases follow a dependency chain: Phase 1 (static
|
||||||
|
analysis) builds only on the AST, Phase 2 (the standalone assembler) emits
|
||||||
|
object code, and Phases 3 (dynamic analysis) and 4 (the debugger) both consume
|
||||||
|
the execution substrate that the assembler provides.
|
||||||
|
|
||||||
## Components
|
## Packages
|
||||||
|
|
||||||
|
| Package | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `token` | token kinds and positions |
|
||||||
|
| `lexer` | hand-written scanner; permissive, and it never panics |
|
||||||
|
| `ast` | the typed syntax tree: declarations, lines, operands |
|
||||||
|
| `parser` | line-oriented parser producing the AST and its diagnostics |
|
||||||
|
| `arch` | register and instruction tables for the four architectures |
|
||||||
|
| `lint` | static checks over the AST |
|
||||||
|
| `format` | canonical formatter over the token stream |
|
||||||
|
| `lsp` | the language server |
|
||||||
|
| `asm` | standalone assembler: encoders, image layout, object emitters |
|
||||||
|
| `verify` | JIT execution, ABI checks, differential fuzzing |
|
||||||
|
| `debug` | interactive ptrace debugger |
|
||||||
|
| `cmd/gasm` | the CLI |
|
||||||
|
| `_gen` | rebuilds the `arch` tables from the Go toolchain source |
|
||||||
|
|
||||||
|
The boundaries matter as much as the responsibilities: `ast` records syntax
|
||||||
|
only, and whether a name is a register or a label is left to `arch`, so the
|
||||||
|
parser stays architecture-agnostic. `asm` and `verify` are the only packages
|
||||||
|
that touch machine code and executable memory, and `cmd/gasm` owns no logic
|
||||||
|
beyond flags and output.
|
||||||
|
|
||||||
### `token` and `lexer`
|
### `token` and `lexer`
|
||||||
|
|
||||||
@@ -134,7 +161,8 @@ Two deeper analyses sit on top of the AST:
|
|||||||
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
||||||
dead. The check is suppressed for any function whose reachability cannot be
|
dead. The check is suppressed for any function whose reachability cannot be
|
||||||
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
||||||
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`), or living in a
|
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`, or a `JMP`/`CALL`
|
||||||
|
through a register or memory operand), or living in a
|
||||||
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
||||||
code after it is occasionally intentional metadata.
|
code after it is occasionally intentional metadata.
|
||||||
- **`register-clobber` (register liveness).** The linter builds the function's
|
- **`register-clobber` (register liveness).** The linter builds the function's
|
||||||
@@ -206,7 +234,7 @@ The standalone assembler (Phase 2). Its core is an amd64 instruction encoder:
|
|||||||
a REX/ModR-M/SIB/displacement/immediate engine plus the scalar instruction set,
|
a REX/ModR-M/SIB/displacement/immediate engine plus the scalar instruction set,
|
||||||
with the Plan 9 operand order (source first) mapped onto the x86 encoding.
|
with the Plan 9 operand order (source first) mapped onto the x86 encoding.
|
||||||
Every encoding is validated by decoding it again with `golang.org/x/arch`, the
|
Every encoding is validated by decoding it again with `golang.org/x/arch`, the
|
||||||
one module dependency, used in tests only and never linked into the binary.
|
one module dependency, which also backs the `gasm dis` listings.
|
||||||
|
|
||||||
A **RISC-V encoder** (Phase 5, RV64IMAFDC + RVC compression) encodes the full
|
A **RISC-V encoder** (Phase 5, RV64IMAFDC + RVC compression) encodes the full
|
||||||
integer, atomic, float/double, FMA and CSR instruction sets with the MOV
|
integer, atomic, float/double, FMA and CSR instruction sets with the MOV
|
||||||
@@ -237,6 +265,18 @@ STP+SUB for large frames) and SB/global symbol references (ADRP+ADD pairs with
|
|||||||
R_ADDRARM64 relocations). Like the other encoders it is validated
|
R_ADDRARM64 relocations). Like the other encoders it is validated
|
||||||
byte-for-byte against `GOARCH=arm64 go tool asm`.
|
byte-for-byte against `GOARCH=arm64 go tool asm`.
|
||||||
|
|
||||||
|
On top of the per-architecture encoders, every framed function carries the
|
||||||
|
**stack-split guard**: the prologue check against `g.stackguard0` (small,
|
||||||
|
medium and large frame classes, the medium and large classes materialising
|
||||||
|
their offset through the architecture's temporary register and the large
|
||||||
|
class adding the SP-underflow branch) and the trailing morestack block
|
||||||
|
(save the link register, `CALL runtime.morestack_noctxt`, jump back to the
|
||||||
|
function entry). The auto-NOSPLIT rule, the frame classes, the large-frame
|
||||||
|
prologue and epilogue forms and the tail calls match the toolchain's
|
||||||
|
`stacksplit` and `preprocess` output byte for byte; a parity suite
|
||||||
|
assembles kernel files with gasm and the installed `go tool asm` and diffs
|
||||||
|
the bytes on all four architectures.
|
||||||
|
|
||||||
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
||||||
operand to an encoder operand, and lays the instructions out so local labels
|
operand to an encoder operand, and lays the instructions out so local labels
|
||||||
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
||||||
@@ -371,8 +411,8 @@ the Go ABI fixes across calls (amd64 `BP`/`R14`, arm64 `R29`/`R28`, riscv64
|
|||||||
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
raw return trampoline `leaveJITCheckedRaw` verifies them, restoring the
|
||||||
saved registers before Go code resumes. riscv64 is validated end to
|
saved registers before Go code resumes. riscv64 is validated end to
|
||||||
end under qemu-user emulation; arm64 shares the same stack convention and
|
end under qemu-user emulation; arm64 shares the same stack convention and
|
||||||
fix; loong64 stays ground-truth-only until hardware validation (see
|
fix; loong64 stays ground-truth-only until hardware validation.
|
||||||
docs/DECISIONS.md). `gasm verify` runs the JIT checks when the host
|
`gasm verify` runs the JIT checks when the host
|
||||||
matches the kernel's architecture and the toolchain comparisons
|
matches the kernel's architecture and the toolchain comparisons
|
||||||
elsewhere.
|
elsewhere.
|
||||||
|
|
||||||
@@ -424,14 +464,72 @@ watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot
|
|||||||
block it), and `--cover` runs to completion with a breakpoint on every
|
block it), and `--cover` runs to completion with a breakpoint on every
|
||||||
label and reports which blocks executed.
|
label and reports which blocks executed.
|
||||||
|
|
||||||
## Extension points
|
### Extending the toolkit
|
||||||
|
|
||||||
- **New architecture:** add an entry to the generator in `_gen`, run
|
- **New architecture:** add an entry to the generator in `_gen`, run
|
||||||
`just gen`, and add a `buildXXX()` register file plus a case in `ForArch`.
|
`just gen`, and add a `buildXXX()` register file plus a case in `ForArch`.
|
||||||
- **New lint rule:** add a function in `lint` and a rule-code constant.
|
- **New lint rule:** add a function in `lint` and a rule-code constant.
|
||||||
- **New LSP feature:** add a method case in `dispatch` and a handler.
|
- **New LSP feature:** add a method case in `dispatch` and a handler.
|
||||||
|
|
||||||
The phases follow a dependency chain. Phase 1 (static analysis) builds only on
|
## Data flow
|
||||||
the AST; Phase 2 (the standalone assembler) emits object code; Phases 3
|
|
||||||
(dynamic analysis) and 4 (the debugger) both consume the execution substrate
|
The main operation, assembling one file:
|
||||||
that the assembler provides.
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant CLI as gasm CLI
|
||||||
|
participant Parser as parser
|
||||||
|
participant Asm as asm
|
||||||
|
participant Go as go toolchain
|
||||||
|
User->>CLI: gasm asm --format goobj -p pkg -o k.o k_amd64.s
|
||||||
|
CLI->>Parser: Parse(path, src)
|
||||||
|
Parser-->>CLI: AST, diagnostics
|
||||||
|
CLI->>Asm: AssembleFile(AST)
|
||||||
|
Asm->>Asm: encode operands, settle label offsets, lay out data
|
||||||
|
Asm-->>CLI: Image, code and data and relocations
|
||||||
|
CLI->>Asm: GOObject(pkg, path)
|
||||||
|
Asm->>Go: go list -json -export, externals only
|
||||||
|
Go-->>Asm: package and symbol indices
|
||||||
|
Asm-->>CLI: Go object bytes
|
||||||
|
CLI-->>User: wrote N bytes to k.o
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors are produced where the parse or the encoding fails and become values at
|
||||||
|
the CLI boundary: the parser returns a diagnostic list and never aborts a file,
|
||||||
|
`AssembleFile` returns an error, and `cmd/gasm` prints what it has to stderr
|
||||||
|
and returns a non-zero exit code. The formatter and the linter take the same
|
||||||
|
AST by a different route: `gasm fmt` re-spaces the token stream and `gasm lint`
|
||||||
|
walks the parsed file, so neither depends on an encoding.
|
||||||
|
|
||||||
|
## State and lifetime
|
||||||
|
|
||||||
|
- The analysis packages (`lexer`, `parser`, `format`, `lint`, `arch`) hold only
|
||||||
|
read-only lookup tables and no mutable state: every call allocates its own
|
||||||
|
tokens and AST, and any number of goroutines may read the `arch` tables.
|
||||||
|
- A `verify.Kernel` owns one executable mapping, which `Close` releases. The
|
||||||
|
JIT trampolines keep the Go stack pointer and the checked-call sentinels in
|
||||||
|
package globals, so a call is a process-wide, one-at-a-time operation. The
|
||||||
|
`gasm verify` sweeps therefore run each function in a child process, which
|
||||||
|
contains a crash and keeps the globals unshared.
|
||||||
|
- `lsp.Server` is long-lived: it runs a single read and dispatch loop over the
|
||||||
|
stream and touches its document store only from that loop, so one server
|
||||||
|
serves one connection.
|
||||||
|
- A `debug.Session` owns a traced child process and pins its goroutine to the
|
||||||
|
forking OS thread, because ptrace requests must stay on that thread.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **`golang.org/x/arch`** (v0.30.0) is the one module dependency: it is the
|
||||||
|
disassembler backend (`gasm dis` and the debugger's listings) and the source
|
||||||
|
of the register metadata the encoder consults (`asm/reg.go`, `asm/vex.go`).
|
||||||
|
The tests additionally decode through it to validate the encodings.
|
||||||
|
- **The Go toolchain**, as an oracle and never as a library: `go tool asm`
|
||||||
|
supplies the object preamble and the ground truth for `gasm verify
|
||||||
|
--ground-truth`, `go list -json -export` locates the archives of the packages
|
||||||
|
a GOOBJ object references, and `_gen` parses
|
||||||
|
`$GOROOT/src/cmd/internal/obj/<arch>/anames.go` to rebuild the tables.
|
||||||
|
- **Linux process interfaces** for the dynamic work: `mmap` and `mprotect` for
|
||||||
|
the JIT mapping, ptrace with `/proc/pid/mem` for the debugger. That is why
|
||||||
|
`verify` runs a JIT check only when the host architecture matches the
|
||||||
|
kernel's, and why `debug` is Linux-only.
|
||||||
|
|||||||
+423
-149
@@ -1,198 +1,472 @@
|
|||||||
# CLI Reference
|
# Command line
|
||||||
|
|
||||||
Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit)
|
The reference below is taken from the program's own `--help`. If the two disagree, the
|
||||||
|
program is right and this file is a defect.
|
||||||
|
|
||||||
`gasm` is a single binary with subcommands. Run `gasm --help` for an
|
The same reference is installed as man pages: `just install-man` puts gasm(1) and one
|
||||||
overview, or `gasm <command> -h` for a command's usage and flags.
|
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each
|
||||||
|
page against the binary so the two cannot drift apart.
|
||||||
|
|
||||||
## Global Flags
|
## Synopsis
|
||||||
|
|
||||||
| Flag | Description |
|
```sh
|
||||||
|------|-------------|
|
gasm [global flags] <command> [command flags] [arguments]
|
||||||
| `-h`, `--help` | Show help |
|
```
|
||||||
| `-V`, `--version` | Print the version |
|
|
||||||
|
|
||||||
## `gasm tokens <file>`
|
## Commands
|
||||||
|
|
||||||
Print the lexical token stream of FILE: position, token kind, and text,
|
| Command | Purpose |
|
||||||
one token per line. FILE may be `-` to read standard input.
|
|---|---|
|
||||||
|
| `tokens` | print the lexical token stream |
|
||||||
|
| `parse` | parse a file and report syntax errors |
|
||||||
|
| `fmt` | canonicalise the formatting of `.s` files |
|
||||||
|
| `lint` | run the static checks |
|
||||||
|
| `asm` | assemble `.s` files to machine code |
|
||||||
|
| `dis` | disassemble machine code or an assembled file |
|
||||||
|
| `verify` | JIT-assemble and run the dynamic checks |
|
||||||
|
| `debug` | interactive source-level debugger |
|
||||||
|
| `diff` | compare the machine code of two `.s` files |
|
||||||
|
| `profile` | show the basic-block structure of the functions |
|
||||||
|
| `audit-instructions` | diff the encoder against the toolchain's name table |
|
||||||
|
| `scaffold` | generate a differential test skeleton for a kernel |
|
||||||
|
| `lsp` | run the language server over stdio |
|
||||||
|
| `version` | print the version |
|
||||||
|
|
||||||
## `gasm parse <file>`
|
## tokens
|
||||||
|
|
||||||
Parse FILE and report syntax errors on stderr. On success, prints how
|
```text
|
||||||
many declarations and TEXT functions the file contains.
|
Usage: gasm tokens <file>
|
||||||
|
```
|
||||||
|
|
||||||
## `gasm fmt [-w] [path...]`
|
Print the lexical token stream of FILE: position, token kind and text, one
|
||||||
|
token per line. FILE may be `-` to read standard input.
|
||||||
|
|
||||||
Canonicalise the formatting of Plan 9 assembly sources: indentation,
|
```sh
|
||||||
operand spacing, per-function mnemonic alignment, and blank-line layout.
|
gasm tokens hello_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
```text
|
||||||
|------|-------------|
|
1:1 # "#"
|
||||||
| `-w` | Write result to the source file (default: print to stdout) |
|
1:2 IDENT "include"
|
||||||
|
1:10 STRING "\"textflag.h\""
|
||||||
|
```
|
||||||
|
|
||||||
With no arguments, or with a directory argument, every `.s` file below
|
## parse
|
||||||
it is reformatted in place and the names of changed files are listed
|
|
||||||
(`go fmt` style). `.` and `_` directories are skipped.
|
|
||||||
|
|
||||||
## `gasm lint <file...>`
|
```text
|
||||||
|
Usage: gasm parse <file>
|
||||||
|
```
|
||||||
|
|
||||||
Run static checks and print diagnostics as
|
Parse FILE and report syntax errors on stderr. On success, print how many
|
||||||
`file:line:col: severity: message [code]`. Exit status is non-zero when
|
declarations and TEXT functions the file contains.
|
||||||
an error-severity diagnostic is found.
|
|
||||||
|
|
||||||
| Flag | Description |
|
```sh
|
||||||
|------|-------------|
|
gasm parse hello_amd64.s
|
||||||
| `-disable` | Comma-separated rule codes to disable |
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
hello_amd64.s: OK, 2 declarations, 1 functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## fmt
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm fmt [-w|-l|-d] [path...]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-w` | off | write the result back to the source file |
|
||||||
|
| `-l` | off | list the files whose formatting differs; write nothing |
|
||||||
|
| `-d` | off | print a unified diff of the canonical formatting instead |
|
||||||
|
|
||||||
|
`-l` and `-d` are mutually exclusive. With no arguments, or with a directory
|
||||||
|
argument, every `.s` file below it is reformatted in place and the names of the
|
||||||
|
changed files are listed, the way `go fmt` does; `.` and `_` directories are
|
||||||
|
skipped. Explicit file arguments print to stdout unless `-w` is given.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm fmt -l kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
Empty output means every file is formatted, which is the shape a CI check
|
||||||
|
wants; `-d` shows what would change:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm fmt -d ugly_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
--- ugly_amd64.s
|
||||||
|
+++ ugly_amd64.s
|
||||||
|
@@ -2,8 +2,8 @@
|
||||||
|
|
||||||
|
// func add(a, b int) int
|
||||||
|
TEXT ·add(SB), NOSPLIT, $0-24
|
||||||
|
- MOVQ a+0(FP), AX
|
||||||
|
- ADDQ b+8(FP), AX
|
||||||
|
+ MOVQ a+0(FP), AX
|
||||||
|
+ ADDQ b+8(FP), AX
|
||||||
|
```
|
||||||
|
|
||||||
|
## lint
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm lint <file...>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-disable` | empty | comma-separated rule codes to disable |
|
||||||
|
|
||||||
|
Diagnostics are printed as `file:line:col: severity: message [code]`. The exit
|
||||||
|
status is non-zero when an error-severity diagnostic is found; warnings (the
|
||||||
|
register-clobber audit, for example) do not affect it.
|
||||||
|
|
||||||
Rules: `unknown-instruction`, `operand-count`, `undefined-label`,
|
Rules: `unknown-instruction`, `operand-count`, `undefined-label`,
|
||||||
`duplicate-label`, `missing-ret`, `missing-textflag-include`,
|
`duplicate-label`, `missing-ret`, `missing-textflag-include`,
|
||||||
`abi-argsize`, `unreachable-code`, `register-clobber`,
|
`abi-argsize`, `unreachable-code`, `register-clobber`,
|
||||||
`funcdata-pcdata`, `unused-label`, `invalid-textflag`,
|
`funcdata-pcdata`, `unused-label`, `invalid-textflag`,
|
||||||
`stack-imbalance`, `register-width-mismatch`, `abi0-register-args`,
|
`stack-imbalance`, `register-width-mismatch`, `abi0-register-args`,
|
||||||
`nonportable-register-name` and `unencodable-instruction`.
|
`nonportable-register-name`, `unencodable-instruction` and
|
||||||
|
`reserved-register-write`.
|
||||||
|
|
||||||
## `gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>`
|
```sh
|
||||||
|
gasm lint kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
Assemble FILE to machine code (amd64, arm64, riscv64, loong64).
|
## asm
|
||||||
|
|
||||||
| Flag | Description |
|
```text
|
||||||
|------|-------------|
|
Usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-GOARCH arch] [-o out] <file>
|
||||||
| `--format` | Output format: `raw` (default), `elf`, `goobj` |
|
```
|
||||||
| `-p` | Package path (required for `--format goobj`) |
|
|
||||||
| `-o` | Write output to file (default: hex dump to stdout) |
|
|
||||||
|
|
||||||
## `gasm verify [flags] <file.s>`
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-format` | `raw` | output format: `raw` (concatenated image), `elf` or `goobj` (Go object) |
|
||||||
|
| `-p` | empty | package path for `--format goobj`, qualifying the exported symbols |
|
||||||
|
| `-GOARCH` | empty | target architecture: `amd64`, `arm64`, `riscv64` or `loong64`; overrides the file-name suffix |
|
||||||
|
| `-o` | empty | write the output to this file instead of a hex dump on stdout |
|
||||||
|
|
||||||
Assemble FILE, map it into executable memory, and run dynamic checks.
|
Supported architectures: amd64 (VEX/AVX2 and EVEX/AVX-512 included), arm64,
|
||||||
|
riscv64 (RV64IMAFDC and RVC) and loong64, taken from the file's `_arch.s`
|
||||||
|
suffix or from `-GOARCH`, which is how files whose names carry no
|
||||||
|
recognisable suffix (most of GOROOT's, for example `cpu_x86.s`) are
|
||||||
|
assembled. `raw` concatenates the functions and the data section into one
|
||||||
|
self-consistent image; `elf` emits a relocatable object that links with the
|
||||||
|
system toolchain; `goobj` emits the Go toolchain's own object format, which
|
||||||
|
`cmd/link` consumes directly.
|
||||||
|
|
||||||
| Flag | Description |
|
```sh
|
||||||
|------|-------------|
|
gasm asm hello_amd64.s
|
||||||
| `--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) |
|
|
||||||
| `--abi-n` | Number of ABI check iterations with varied inputs (default: 100) |
|
|
||||||
| `--profile` | List basic-block structure per function |
|
|
||||||
| `--smoke` | Call each NOSPLIT function with zeroed args |
|
|
||||||
| `--call <func>` | Invoke a single function with `--buf` instead of the sweeps |
|
|
||||||
| `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
|
|
||||||
| `--args <spec>` | Scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
|
|
||||||
| `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) |
|
|
||||||
| `--save-corpus <dir>` | With `--fuzz`: write each failing input to DIR as replayable JSON |
|
|
||||||
| `--replay <dir>` | Re-run saved corpus entries (JSON in DIR), one child process per entry |
|
|
||||||
|
|
||||||
The `--fuzz` mode runs each function in a subprocess; a partial function
|
```text
|
||||||
(e.g. a decoder that faults on malformed input) is reported as
|
add: 16 bytes
|
||||||
`CRASH` without killing the parent. Use `--call` with `--buf` to invoke
|
0000: 48 8b 44 24 08 48 03 44 24 10 48 89 44 24 18 c3
|
||||||
partial functions with valid data instead.
|
```
|
||||||
|
|
||||||
The `--call` mode parses the `// func` signature, allocates the requested
|
## dis
|
||||||
buffers (`zero`, `ones`, `seq`, or a hex blob), builds the ABI0 argument
|
|
||||||
block with buffer pointers/lengths/capacities at the matching parameter
|
|
||||||
offsets, and prints the arg block before and after the call, showing
|
|
||||||
return values and any output written to the buffers. Scalar parameters
|
|
||||||
are supplied with `--args` (decimal, or `0x` hex) at their ABI0 offsets.
|
|
||||||
|
|
||||||
The `--save-corpus` mode records the logical arguments (buffer contents and
|
```text
|
||||||
scalars, not raw pointers) of every failing fuzz input as JSON. `--replay`
|
Usage: gasm dis [-a arch] <file>
|
||||||
rebuilds a live argument block from each entry and calls it in its own child
|
```
|
||||||
process, reporting `OK`, `CRASH (reproduced)` or `FAIL` per entry and
|
|
||||||
exiting non-zero when any entry fails.
|
|
||||||
|
|
||||||
## `gasm debug [--func <name>] [--buf spec] [--script file] <file.s>`
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-a` | empty | architecture for raw input without a `_arch.s` name |
|
||||||
|
|
||||||
Interactive debugger for JIT-assembled functions (amd64, arm64, riscv64,
|
With a `.s` file the file is assembled first and the listing follows the real
|
||||||
loong64). Requires a compiled binary on `$PATH` (not `go run`).
|
layout: one block per `TEXT` function, local labels printed at their offsets.
|
||||||
|
With any other file, or `-` for standard input, the bytes are disassembled
|
||||||
|
linearly and `-a` selects the architecture (amd64, arm64, riscv64 or loong64).
|
||||||
|
|
||||||
| Flag | Description |
|
```sh
|
||||||
|------|-------------|
|
gasm dis hello_amd64.s
|
||||||
| `--func` | Function to debug (required) |
|
```
|
||||||
| `--buf` | Buffer spec: `name:size:pattern[,name:size:pattern]` |
|
|
||||||
| `--args <file>` | File containing the ABI0 argument block |
|
```text
|
||||||
| `--script <file>` | Run REPL commands from a file (one per line) and exit; `-` reads stdin |
|
add: 16 bytes
|
||||||
| `--timeout <dur>` | Kill the debuggee after this duration (e.g. `30s`); for headless `--script` runs |
|
0000: 48 8b 44 24 08 mov rax, qword ptr [rsp+0x8]
|
||||||
| `--cover` | Run to completion with a breakpoint on every instruction; report which executed, how often, and which labels were reached |
|
0005: 48 03 44 24 10 add rax, qword ptr [rsp+0x10]
|
||||||
|
000a: 48 89 44 24 18 mov qword ptr [rsp+0x18], rax
|
||||||
|
000f: c3 ret
|
||||||
|
```
|
||||||
|
|
||||||
|
## verify
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `--ground-truth` | off | compare the machine code byte-for-byte against `go tool asm` |
|
||||||
|
| `--fuzz` | off | differential fuzz against the `go tool asm` build |
|
||||||
|
| `-n` | 1000 | fuzz iterations per function |
|
||||||
|
| `--abi` | off | ABI-checking calls: sentinel registers and a red-zone canary |
|
||||||
|
| `--abi-n` | 100 | ABI check iterations with varied inputs |
|
||||||
|
| `--profile` | off | list the basic-block structure per function |
|
||||||
|
| `--smoke` | off | call each NOSPLIT function with zeroed arguments |
|
||||||
|
| `--call` | empty | invoke a single function with `--buf` instead of the sweeps |
|
||||||
|
| `--buf` | empty | buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
|
||||||
|
| `--args` | empty | scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
|
||||||
|
| `--repeat` | 1 | number of times to repeat a `--call` invocation |
|
||||||
|
| `--save-corpus` | empty | with `--fuzz`: write each failing input to this directory as replayable JSON |
|
||||||
|
| `--replay` | empty | re-run saved corpus entries, one child process per entry |
|
||||||
|
|
||||||
|
The JIT checks run when the host matches the file's architecture; the
|
||||||
|
toolchain comparison works everywhere. `--fuzz`, `--smoke` and `--abi` run each
|
||||||
|
function in its own child process, so a partial function that faults on random
|
||||||
|
input is reported as `CRASH` instead of ending the sweep; `--call` with `--buf`
|
||||||
|
invokes such a function with valid data. loong64 stays on the ground-truth path
|
||||||
|
until hardware validation.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm verify --ground-truth hello_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
hello_amd64.s: 1 functions JIT-loaded
|
||||||
|
add: MATCH (16 bytes)
|
||||||
|
ground truth: 1/1 functions byte-identical
|
||||||
|
add: 16 bytes, args=24, frame=0 NOSPLIT
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm verify --call add --args a=2,b=3 hello_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
add: 16 bytes, args=24
|
||||||
|
signature: func add(a int, b int) int
|
||||||
|
scalars:
|
||||||
|
a = 2
|
||||||
|
b = 3
|
||||||
|
args before: 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (24 bytes)
|
||||||
|
args after: 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 (24 bytes)
|
||||||
|
call 1: OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## debug
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm debug <file.s> --func <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-func` | empty | the function to debug, required |
|
||||||
|
| `-buf` | empty | buffer spec: `name:size:pattern[,name:size:pattern]` (zero, ones, seq or hex) |
|
||||||
|
| `-args` | empty | file containing the ABI0 argument block |
|
||||||
|
| `-script` | empty | run REPL commands from a file, one per line, and exit; `-` reads stdin |
|
||||||
|
| `-timeout` | 0 | kill the debuggee after this duration, for headless `-script` runs |
|
||||||
|
| `-cover` | off | run to completion with a breakpoint on every instruction and report which executed |
|
||||||
|
|
||||||
|
The debugger spawns the debuggee from the `gasm` binary on `$PATH`, so install
|
||||||
|
it first with `just install`; `go run` does not work for the traced child.
|
||||||
|
Requires Linux (ptrace) and all four architectures are supported.
|
||||||
|
|
||||||
REPL commands:
|
REPL commands:
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Effect |
|
||||||
|---------|-------------|
|
|---|---|
|
||||||
| `break <label\|addr> [if <reg> <op> <val>]` | Set a breakpoint, optionally conditional |
|
| `break <label\|addr> [if <reg> <op> <val>]` | set a breakpoint, optionally conditional |
|
||||||
| `delete <label\|addr>` | Remove a breakpoint |
|
| `delete <label\|addr>` | remove a breakpoint |
|
||||||
| `info break` | List all breakpoints |
|
| `info break` | list the breakpoints |
|
||||||
| `step [n]`, `s` | Single-step n instructions |
|
| `step [n]`, `s` | single-step n instructions |
|
||||||
| `next`, `n` | Step over CALL |
|
| `next`, `n` | step over a CALL |
|
||||||
| `finish`, `fin` | Run until the function returns |
|
| `finish`, `fin` | run until the function returns |
|
||||||
| `continue`, `c` | Run until breakpoint, watchpoint or exit |
|
| `continue`, `c` | run until a breakpoint, watchpoint or exit |
|
||||||
| `disas [n]`, `u` | Disassemble n instructions at PC |
|
| `disas [n]`, `u` | disassemble n instructions at the PC |
|
||||||
| `regs` | Print general-purpose + vector/FP registers |
|
| `regs` | print the general-purpose and vector/FP registers |
|
||||||
| `where` | Show source line and nearest label at PC |
|
| `where` | show the source line and the nearest label at the PC |
|
||||||
| `stack` | Show stack near RSP (return address + ABI0 args) |
|
| `stack` | show the stack near RSP, the return address and the ABI0 args |
|
||||||
| `bt`, `backtrace` | Backtrace (current frame + return address) |
|
| `bt`, `backtrace` | backtrace: the current frame and the return address |
|
||||||
| `x [addr] [len]` | Hex-dump memory |
|
| `x [addr] [len]` | hex-dump memory |
|
||||||
| `w <addr> <val...>` | Write bytes to memory |
|
| `w <addr> <val...>` | write bytes to memory |
|
||||||
| `set <reg> <value>` | Set a register |
|
| `set <reg> <value>` | set a register |
|
||||||
| `watch <addr> [r\|w] [size]` | Set a hardware watchpoint (write by default) |
|
| `watch <addr> [r\|w] [size]` | set a hardware watchpoint, write by default |
|
||||||
| `unwatch [<slot>]` | Clear one or all watchpoints |
|
| `unwatch [<slot>]` | clear one watchpoint or all of them |
|
||||||
| `labels`, `l` | List function labels and offsets |
|
| `labels`, `l` | list the function's labels and offsets |
|
||||||
| `help`, `h`, `?` | Show command help |
|
| `help`, `h`, `?` | show the command help |
|
||||||
| `quit`, `q` | Kill the debuggee and exit |
|
| `quit`, `q` | kill the debuggee and exit |
|
||||||
|
|
||||||
## `gasm diff [--map old=new,...] <file1.s> <file2.s>`
|
```sh
|
||||||
|
gasm debug --func add --cover hello_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
Compare the machine code produced by assembling two files. Shows which
|
## diff
|
||||||
functions differ and the first few differing bytes. Useful for verifying
|
|
||||||
that two implementations produce identical code, or for tracking encoding
|
|
||||||
changes between Go assembler versions.
|
|
||||||
|
|
||||||
| Flag | Description |
|
```text
|
||||||
|------|-------------|
|
Usage: gasm diff [-GOARCH arch] <file1.s> <file2.s>
|
||||||
| `--map` | Comma-separated `old=new` pairs to match functions with different names |
|
```
|
||||||
|
|
||||||
Without `--map`, functions are paired by exact name. With `--map`, a
|
| Flag | Default | Effect |
|
||||||
function named `old` in the first file is compared against the function
|
|---|---|---|
|
||||||
named `new` in the second file (e.g. `--map wideCopyAVX2=wideCopyAVX512`
|
| `-GOARCH` | empty | target architecture for both files, overriding the file-name suffixes |
|
||||||
pairs AVX2 and AVX-512 variants regardless of suffix).
|
| `-map` | empty | comma-separated `old=new` pairs to match functions with different names |
|
||||||
|
|
||||||
## `gasm profile <file.s>`
|
Functions are paired by exact name unless `--map` says otherwise, so
|
||||||
|
`--map wideCopyAVX2=wideCopyAVX512` pairs two variants regardless of suffix.
|
||||||
|
The exit status is non-zero when anything differs.
|
||||||
|
|
||||||
Show the basic-block structure of functions in an assembly file. Lists
|
```sh
|
||||||
each function's labels, their offsets, and the block boundaries. This is
|
gasm diff hello_amd64.s hello_amd64.s
|
||||||
the static structure; for runtime execution counts, use `gasm verify
|
```
|
||||||
--fuzz` which exercises the code paths.
|
|
||||||
|
|
||||||
## `gasm audit-instructions [amd64|arm64|riscv64|loong64]`
|
```text
|
||||||
|
add: identical (16 bytes)
|
||||||
|
all functions identical
|
||||||
|
```
|
||||||
|
|
||||||
Compare the gasm encoder for the given architecture (default amd64)
|
## profile
|
||||||
against the installed `go tool asm` and print the diff: superset
|
|
||||||
encodings (gasm-only spellings, shippable via `gasm asm --format goobj`),
|
|
||||||
known-but-unencodable names (the encoder backlog) and go-only names
|
|
||||||
(feature gaps). The Go side is probed black-box with a battery of operand
|
|
||||||
shapes per mnemonic, so the audit tracks whatever toolchain
|
|
||||||
`go env GOROOT` provides. On non-amd64 architectures the backlog is an
|
|
||||||
over-approximation: a name counts as encodable only when a probe shape
|
|
||||||
assembles cleanly, so a name whose real forms the battery misses lands
|
|
||||||
in the backlog.
|
|
||||||
|
|
||||||
## `gasm scaffold differential <file.s>`
|
```text
|
||||||
|
Usage: gasm profile <file.s>
|
||||||
|
```
|
||||||
|
|
||||||
Print a differential test skeleton for every `// func` signature in
|
Show the basic-block structure of each function: its labels, their offsets and
|
||||||
FILE. The generated test seeds random states, drives the kernel and a
|
the block boundaries. This is the static structure; for runtime execution
|
||||||
portable reference (`<name>Portable`), and compares outputs
|
counts use `gasm debug --cover`, and for input coverage `gasm verify --fuzz`.
|
||||||
byte-for-byte. Write the reference bodies, place the file in the
|
|
||||||
kernel's package, and run it in CI.
|
|
||||||
|
|
||||||
## `gasm lsp`
|
```sh
|
||||||
|
gasm profile hello_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
Run the language server over standard input/output (JSON-RPC 2.0 with
|
```text
|
||||||
Content-Length framing). Point an LSP-capable editor at the binary and
|
add: 16 bytes, args=24, frame=0 NOSPLIT
|
||||||
associate it with `.s` files. The target architecture is inferred from
|
basic blocks: 1
|
||||||
the file-name suffix (`_amd64.s`, `_arm64.s`, `_riscv64.s`,
|
```
|
||||||
`_loong64.s`).
|
|
||||||
|
|
||||||
Provides: completion, hover, document symbols, push and pull
|
## audit-instructions
|
||||||
diagnostics, semantic tokens, go-to-definition, find references, rename,
|
|
||||||
document formatting, inlay hints, code actions, signature help, document
|
```text
|
||||||
highlights, workspace symbol search, #include document links, and
|
Usage: gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]
|
||||||
folding ranges for function bodies.
|
```
|
||||||
|
|
||||||
|
Compare the gasm encoder for the given architecture (default amd64) against the
|
||||||
|
installed `go tool asm` and print the diff: superset encodings (gasm-only
|
||||||
|
spellings, shippable via `gasm asm --format goobj`), known-but-unencodable
|
||||||
|
names (the encoder backlog) and go-only names (feature gaps). The Go side is
|
||||||
|
probed black-box with a battery of operand shapes per mnemonic, so the audit
|
||||||
|
tracks whatever toolchain `go env GOROOT` provides. On non-amd64
|
||||||
|
architectures the backlog is an over-approximation: a name counts as encodable
|
||||||
|
only when a probe shape assembles cleanly, so a name whose real forms the
|
||||||
|
battery misses lands in the backlog.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm audit-instructions amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
gasm table (amd64, families excluded): 1542 mnemonics
|
||||||
|
gasm encodable: 580 go tool asm recognized: 1542
|
||||||
|
shared: 580
|
||||||
|
```
|
||||||
|
|
||||||
|
With `--corpus` the audit changes shape: it assembles every `.s` file under
|
||||||
|
DIR (default `GOROOT/src`) with the gasm encoder only, no toolchain probing.
|
||||||
|
A file whose name carries a recognisable `_arch` suffix is attempted for that
|
||||||
|
architecture; a file without one is attempted for all four, exactly as a
|
||||||
|
`GOARCH` build would compile it. The report gives the headline number (files
|
||||||
|
that assemble for every target architecture), the per-architecture pass rates
|
||||||
|
and the most common failure reasons with one representative file each, which
|
||||||
|
drive the encodability backlog by frequency rather than by table order. A run
|
||||||
|
over GOROOT takes under a second.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm audit-instructions --corpus
|
||||||
|
gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
|
||||||
|
assemble for every target architecture: 108 (17.2%)
|
||||||
|
amd64: 77/464 attempted
|
||||||
|
148 instruction not encodable
|
||||||
|
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## scaffold
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm scaffold differential <file.s>
|
||||||
|
```
|
||||||
|
|
||||||
|
Print a differential test skeleton for every `// func` signature in FILE. The
|
||||||
|
generated test seeds random states, drives the kernel and a portable reference
|
||||||
|
(`<name>Portable`), and compares the outputs byte-for-byte. Write the reference
|
||||||
|
bodies, place the file in the kernel's package, and run it in CI.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm scaffold differential kernel_amd64.s > kernel_differential_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## lsp
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: 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, push and pull diagnostics,
|
||||||
|
semantic tokens, go-to-definition, find references, rename, document
|
||||||
|
formatting, inlay hints, code actions, signature help, document highlights,
|
||||||
|
workspace symbol search, #include document links, and folding ranges for
|
||||||
|
function bodies. Definition, references and rename work across every open
|
||||||
|
document.
|
||||||
|
|
||||||
|
## version
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: gasm version
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the version the toolchain recorded for the build, the same string as
|
||||||
|
`gasm --version`: the tag on a tagged checkout, a pseudo-version naming the
|
||||||
|
commit below one, with `+dirty` appended on a dirty tree and `(devel)` outside
|
||||||
|
version control.
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
| Flag | Default | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-h`, `--help` | off | print the usage |
|
||||||
|
| `-V`, `--version` | off | print the version |
|
||||||
|
|
||||||
|
## Exit codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `0` | success |
|
||||||
|
| `1` | a failure the program detected: a parse or assembly error, an error-severity lint diagnostic, a mismatch in `verify`, a file that cannot be read |
|
||||||
|
| `2` | the arguments were wrong: a missing or extra argument, an unknown command or format, an invalid `--map` pair |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Assemble a kernel, check it, and run it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm lint kernel_amd64.s
|
||||||
|
gasm fmt -l kernel_amd64.s
|
||||||
|
gasm asm -o kernel.bin kernel_amd64.s
|
||||||
|
gasm verify --ground-truth kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
Link the kernel into a Go program through the toolchain's own object format:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm asm --format goobj -p example.com/kernel -o kernel.o kernel_amd64.s
|
||||||
|
```
|
||||||
|
|
||||||
|
Find which labels a failing kernel reaches, headlessly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gasm debug --func decodeBlockAVX2 --cover --script cmds.txt --timeout 30s kernel_amd64.s
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
# Deferred decisions
|
|
||||||
|
|
||||||
Design decisions deliberately postponed, with enough context to pick them up
|
|
||||||
again without re-deriving the analysis. Each entry records what is deferred,
|
|
||||||
why, the options on the table, and the trigger that should reopen it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GOOBJ external (cross-package) symbol references
|
|
||||||
|
|
||||||
**Status:** resolved (v0.29.0+, 2026-08-07).
|
|
||||||
|
|
||||||
**Approach taken.** Instead of parsing the compiler's iexport data (which
|
|
||||||
would have required either `golang.org/x/tools` or an in-house parser), the
|
|
||||||
resolver reads the **GOOBJ data directly** from the target package's `.a`
|
|
||||||
archive. The `.a` file contains a `_go_.o` member whose GOOBJ s is the
|
|
||||||
same one gasm writes; the parser reuses the same layout (`blkSymdef`,
|
|
||||||
`blkNonpkgdef`, the string table), so no new dependency was needed.
|
|
||||||
|
|
||||||
**How it works.**
|
|
||||||
|
|
||||||
1. `go list -json -export <pkg>` finds the target package's `.a` file.
|
|
||||||
2. `extractGOOBJ` reads the ar archive, finds the `_go_.o` member, skips
|
|
||||||
the `"go object …\n!\n"` preamble and parses the GOOBJ header.
|
|
||||||
3. `goobjFile.symbols()` walks `blkSymdef` and `blkNonpkgdef` in definition
|
|
||||||
order (the same order the linker uses) to build the symbol-to-index
|
|
||||||
mapping.
|
|
||||||
4. `resolveExternalSymbols` wires the resolved `{PkgIdx, SymIdx}` into the
|
|
||||||
GOOBJ emission.
|
|
||||||
|
|
||||||
The resolver is invoked automatically when `img.Externals` is non-empty; it
|
|
||||||
runs `go list` as a subprocess (consistent with `toolchainObjectPreamble`
|
|
||||||
which already calls `go tool asm`). All symbol data is cached per package
|
|
||||||
for the lifetime of the GOOBJ emission.
|
|
||||||
|
|
||||||
## 2026-08-30 non-amd64 JIT execution trampolines
|
|
||||||
|
|
||||||
**Status:** resolved for riscv64 (validated end to end under qemu-user)
|
|
||||||
and arm64 (fix in place, consistent with the observed frame convention);
|
|
||||||
open for loong64 until hardware validation.
|
|
||||||
|
|
||||||
**Root cause (found 2026-08-31).** The trampolines advanced SP past the
|
|
||||||
leave-address slot after loading it, while the assembled kernels read
|
|
||||||
their first argument at SP+8 per the frame convention (the amd64 path
|
|
||||||
already kept SP on that slot). Removing the advance fixed riscv64
|
|
||||||
immediately (plain and checked ABI tests pass under qemu-user); the
|
|
||||||
arm64 kernel's pre-fix trace showed exactly the same SP+8 reading. The
|
|
||||||
apparent arm64/loong64 "crashes in the JIT" turned out to be dominated
|
|
||||||
by an unrelated instability: the Go 1.26 and 1.27 runtimes crash under
|
|
||||||
qemu-user arm64 emulation (GC worker start, identical signature with the
|
|
||||||
JIT tests skipped, both qemu 7.2 and 10.2), and the Go loong64 runtime
|
|
||||||
does not start at all. `gasm verify` therefore keeps loong64 kernels on
|
|
||||||
the ground-truth path until hardware validation; the GOARCH-guarded
|
|
||||||
tests (`verify/jit_arch_test.go`, `verify/abi_arch_test.go`) are the
|
|
||||||
hardware validation entry point.
|
|
||||||
|
|
||||||
**State.** The per-architecture trampolines compile for all targets, the
|
|
||||||
kernels they execute are byte-for-byte correct against `go tool asm`, and
|
|
||||||
under `qemu-aarch64` the arm64 kernel demonstrably executes and stores its
|
|
||||||
result correctly. The failure is on the return path into Go code: arm64
|
|
||||||
and loong64 take a SIGSEGV after the kernel's RET (the Go-side unwind
|
|
||||||
through `leaveJIT` and its interposed ABIInternal wrapper is the suspect),
|
|
||||||
and riscv64 returns cleanly but with an untouched result area. amd64 is
|
|
||||||
unaffected (the checked trampoline saves and restores BP/R14 and the flow
|
|
||||||
is validated end to end).
|
|
||||||
|
|
||||||
**Evidence harness.** `verify/jit_arch_test.go` (plain call) and
|
|
||||||
`verify/abi_arch_test.go` (checked call) are GOARCH-guarded tests; build
|
|
||||||
the test binary per target (`GOARCH=arm64 go test -c -o v.test ./verify/`)
|
|
||||||
and run it under `qemu-aarch64-static` from the `verify/` directory. A
|
|
||||||
minimal reproducer pattern lives in the qemu exploration notes: verify
|
|
||||||
loads, the kernel executes, the fault follows the return.
|
|
||||||
|
|
||||||
**Fix direction.** Compare the amd64 checked trampoline (GLOBL/DATA raw
|
|
||||||
address, explicit SP/BP/R14 save-restore) against the arm64/riscv64/
|
|
||||||
loong64 `leaveJIT` unwind, in particular the interaction with the
|
|
||||||
ABIInternal wrapper that `reflect.ValueOf(leaveJIT).Pointer()` returns.
|
|
||||||
The plain-call path (no sentinels) fails the same way, so the checked
|
|
||||||
path is not the variable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-08-29 tooling round
|
|
||||||
|
|
||||||
- `lint abi0-register-args`: flags kernels whose `// func` parameters are
|
|
||||||
never read from the FP frame. Motivated by a real latent bug: kernels
|
|
||||||
reading arguments from registers pass every test while the autogenerated
|
|
||||||
`F.abi0` wrapper happens to leave the caller's register values intact, and
|
|
||||||
break on a toolchain upgrade.
|
|
||||||
- `lint nonportable-register-name`: the RAX/EAX register spellings are a gasm
|
|
||||||
extension; go tool asm rejects them, so files using them only link through
|
|
||||||
the gasm goobj path.
|
|
||||||
- `lint unencodable-instruction`: a mnemonic in the architecture table that
|
|
||||||
`asm.Encodable` rejects is flagged at edit time instead of failing at
|
|
||||||
assembly time.
|
|
||||||
- `audit-instructions`: black-box diff of the encoder against go tool asm.
|
|
||||||
As of this round the tables fully overlap on names; the audit exists to
|
|
||||||
catch drift in both directions (future supersets and future gaps).
|
|
||||||
- `scaffold differential`: generates the direct-call differential skeleton
|
|
||||||
(two independent seed sets, output and in-place buffer comparison) that a
|
|
||||||
pipeline-level fuzz can never replace.
|
|
||||||
- `verify --args`: scalar arguments for `-call`, closing the repro gap where
|
|
||||||
only buffers could be supplied.
|
|
||||||
- `debug --script/--timeout/--cover`: headless debugging with a watchdog
|
|
||||||
armed before the ptrace attach (untracing sandboxes hang the attach), and
|
|
||||||
label-level block coverage for the "did my test ever enter that branch"
|
|
||||||
question.
|
|
||||||
- Superset policy remains: gasm may accept spellings and encodings go tool
|
|
||||||
asm lacks, but such kernels ship only via `gasm asm --format goobj`; the
|
|
||||||
audit reports the superset surface. The register-alias superset is warned
|
|
||||||
about by lint because the default `go build` path cannot consume it.
|
|
||||||
+88
-66
@@ -4,79 +4,80 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Go** 1.27+ with `toolchain go1.27.0`
|
- **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares
|
||||||
- **just**, the command runner; every task below is a just recipe
|
- **just**, the command runner; every task below is a just recipe
|
||||||
|
- A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace
|
||||||
|
and the JIT checks of `gasm verify` need executable memory
|
||||||
- No external dependencies beyond the Go toolchain
|
- No external dependencies beyond the Go toolchain
|
||||||
|
|
||||||
## Quick Start
|
## Setup
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||||
cd gasm-devkit
|
cd gasm-devkit
|
||||||
just install # go mod download
|
just build # compile bin/gasm, zero errors and zero warnings
|
||||||
just build # go vet + gofmt, must pass with zero output
|
just gates # build, fmt-check, vet, test, race: the definition of done
|
||||||
just test # full suite, race detector, 80 % coverage gate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Just Recipes
|
## Recipes
|
||||||
|
|
||||||
### `just install`
|
Every recipe in the `justfile`, and what it does.
|
||||||
|
|
||||||
`go mod download`. The only module dependency, `golang.org/x/arch`, is
|
| Recipe | What it does |
|
||||||
used in tests only.
|
|---|---|
|
||||||
|
| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings |
|
||||||
### `just build`
|
| `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor |
|
||||||
|
| `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` |
|
||||||
Runs `go vet ./...` and checks `gofmt -l .` produces no output. This is
|
| `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly |
|
||||||
the minimum bar before any commit.
|
| `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one |
|
||||||
|
| `just bench [packages]` | benchmarks (`-benchmem -count=5`); on an idle machine only |
|
||||||
|
| `just fmt` | formats the tree in place with `gofmt` |
|
||||||
|
| `just fmt-check` | zero diff; prints nothing when everything is formatted, which is the shape the CI step wants |
|
||||||
|
| `just vet` | both static gates: `go vet` and `go fix -diff` |
|
||||||
|
| `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done |
|
||||||
|
| `just clean` | removes the build artefacts, `bin/` and `coverage.out` |
|
||||||
|
| `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`); `gasm debug` needs an installed binary, because it spawns the debuggee from `$PATH` |
|
||||||
|
| `just uninstall` | removes the installed binary from `bindir` |
|
||||||
|
| `just run` | runs the CLI with `go run -buildvcs=true`; the recipe takes no arguments, so flags go through the package instead |
|
||||||
|
| `just dev` | the same as `run`; the project has no watcher to add |
|
||||||
|
| `just gen` | regenerates the `arch` instruction tables from the Go toolchain source; not a gate |
|
||||||
|
|
||||||
### `just test`
|
### `just test`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go test -race -count=1 ./...
|
go test -count=1 -timeout 10m -coverprofile=coverage.out \
|
||||||
|
./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \
|
||||||
|
./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||||
```
|
```
|
||||||
|
|
||||||
Plus a coverage run over the ten analysable packages (arch, asm, ast,
|
The suite runs over the logic packages (`-count=1`, so no cached pass
|
||||||
format, lexer, lint, lsp, parser, token, verify; `debug` and `cmd/gasm`
|
counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser,
|
||||||
need hardware or are CLI glue) and an `awk` gate that fails if total
|
token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI
|
||||||
coverage is below 80 %.
|
glue, so both sit outside the sweep, and a thin `cmd/` in it would drag
|
||||||
|
the coverage total under the floor. The floor fails if the total is
|
||||||
|
below 80 %. CI runs the same command with the same ten-minute bound, so
|
||||||
|
the number is the same everywhere.
|
||||||
|
|
||||||
### `just fmt`
|
### `just run`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gofmt -w .
|
just run
|
||||||
|
go run -buildvcs=true ./cmd/gasm lint kernel_amd64.s
|
||||||
|
go run -buildvcs=true ./cmd/gasm verify --ground-truth kernel_amd64.s
|
||||||
```
|
```
|
||||||
|
|
||||||
Run after editing any Go source. The output must be idempotent.
|
The flag on `go run` is there because it does not stamp the build otherwise,
|
||||||
|
which `--version` would then report as `(devel)`.
|
||||||
### `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`
|
### `just gen`
|
||||||
|
|
||||||
Regenerates the architecture instruction tables in `arch/` by parsing
|
Regenerates the architecture instruction tables in `arch/` by parsing the Go
|
||||||
the Go toolchain's own assembler source
|
toolchain's own assembler source
|
||||||
(`$GOROOT/src/cmd/internal/obj/<arch>/anames.go`). Requires a Go
|
(`$GOROOT/src/cmd/internal/obj/<arch>/anames.go`). Requires a Go
|
||||||
installation. Output is committed, with no runtime dependency on the
|
installation. Output is committed, with no runtime dependency on the
|
||||||
toolchain.
|
toolchain.
|
||||||
|
|
||||||
### `just uninstall`
|
## Running a single test
|
||||||
|
|
||||||
Removes `coverage.out`, the `gasm` binary, and `*.test` artefacts.
|
|
||||||
|
|
||||||
## Running Individual Tests
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go test -run TestVexGroundTruth ./asm/
|
go test -run TestVexGroundTruth ./asm/
|
||||||
@@ -85,32 +86,53 @@ go test -run TestGOObjectLinkAndRun ./asm/
|
|||||||
go test -run TestFuzzWideCopy ./verify/
|
go test -run TestFuzzWideCopy ./verify/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debugger Note
|
Add `-v` for the sub-test names, and `-race` when the change touches
|
||||||
|
concurrency. `-count=1` defeats the test cache when a result looks stale.
|
||||||
|
|
||||||
`gasm debug` spawns a child process from the binary on `$PATH`. It does
|
## Coverage
|
||||||
not work with `go run`; install first:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
just install-bin
|
just test
|
||||||
gasm debug --func decodeBlockAVX2 path/to/kernel_amd64.s
|
go tool cover -func=coverage.out
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Layout
|
The `total:` line is the number that matters, and it stays at 80 percent or
|
||||||
|
more.
|
||||||
|
|
||||||
|
## Debugging the build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build -gcflags='-m' ./... # inlining decisions
|
||||||
|
go build -gcflags='-S' ./... # what the compiler generated
|
||||||
|
go tool asm -S kernel_amd64.s # how the toolchain's assembler encodes a kernel
|
||||||
|
gasm dis kernel_amd64.s # what gasm makes of the same kernel
|
||||||
|
gasm tokens kernel_amd64.s # the token stream
|
||||||
|
gasm profile kernel_amd64.s # the basic blocks of each function
|
||||||
```
|
```
|
||||||
cmd/gasm/ CLI entry point (subcommands)
|
|
||||||
token/ Lexical token kinds and positions
|
`gasm verify --ground-truth` is the differential check that ties the two
|
||||||
lexer/ Hand-written scanner
|
together: it compares gasm's bytes with `go tool asm`'s, with the relocation
|
||||||
ast/ Abstract syntax tree
|
sites masked, so an encoding drift shows up as a byte difference rather than a
|
||||||
parser/ Line-oriented parser
|
crash later.
|
||||||
arch/ Register and instruction tables (generated)
|
|
||||||
lint/ Static analysis rules
|
## Continuous integration
|
||||||
format/ Canonical formatter
|
|
||||||
lsp/ Language Server Protocol server
|
Workflows live in `.gitea/workflows/` and run on the project's own runners:
|
||||||
asm/ Standalone assembler, encoder, object emitters
|
Test on a push or pull request to `development`, race dispatched by hand, and
|
||||||
verify/ JIT execution, differential testing, ABI checks
|
the release on a `v*` tag. They are written by hand rather than through
|
||||||
debug/ Interactive ptrace debugger (all four architectures)
|
`just`, but they enforce the same set of gates minus the race detector, which
|
||||||
_gen/ Instruction table generator
|
the shared runner cannot afford on a push; a green `just gates` locally is
|
||||||
testdata/ Test fixtures
|
therefore the fastest way to a green pipeline.
|
||||||
docs/ Architecture, development, CLI reference
|
|
||||||
```
|
## Releases
|
||||||
|
|
||||||
|
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`,
|
||||||
|
which triggers the release workflow: it builds the portable Linux targets,
|
||||||
|
takes the notes from the matching `CHANGELOG.md` section and uploads the
|
||||||
|
assets.
|
||||||
|
|
||||||
|
The version is never injected. `gasm --version` prints what the
|
||||||
|
toolchain recorded in the build information: the tag on a tagged
|
||||||
|
checkout, a pseudo-version naming the commit below one, `+dirty` on a
|
||||||
|
dirty tree, and `(devel)` outside version control. There is no
|
||||||
|
`-ldflags "-X"` anywhere and no version constant in the source.
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
.TH GASM-ASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-asm \- assemble Plan 9 assembly without the Go toolchain
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm asm [\-\-format raw|elf|goobj] [\-p pkg] [\-GOARCH arch] [\-o out] <file>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Assemble FILE without the Go toolchain: every TEXT function is encoded
|
||||||
|
to machine code and printed as a hex dump. Supported architectures:
|
||||||
|
amd64 (including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer,
|
||||||
|
FP, conditional select, CRC32 and MOV pseudo), riscv64 (RV64IMAFDC and
|
||||||
|
RVC) and loong64 (LoongArch base ISA).
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.B \-o
|
||||||
|
the output is written to a file instead. The
|
||||||
|
.B \-\-format
|
||||||
|
flag selects what is written:
|
||||||
|
.B raw
|
||||||
|
(the default) concatenates the functions and the data section into one
|
||||||
|
self-consistent image;
|
||||||
|
.B elf
|
||||||
|
emits a relocatable object (.text/.data sections, a symbol table and
|
||||||
|
one PC32 relocation per static-symbol reference) that links with the
|
||||||
|
system toolchain;
|
||||||
|
.B goobj
|
||||||
|
emits the Go toolchain's own object format, which cmd/link consumes
|
||||||
|
directly (it requires
|
||||||
|
.BR \-p ,
|
||||||
|
the package path, and the installed Go toolchain).
|
||||||
|
.PP
|
||||||
|
Framed functions receive the stack-split guard and the trailing
|
||||||
|
morestack block, byte-identical to the toolchain's output, so split
|
||||||
|
functions link too.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-\-format \fIraw|elf|goobj\fR
|
||||||
|
Output format; the default is raw.
|
||||||
|
.TP
|
||||||
|
.B \-p \fIpkg\fR
|
||||||
|
Package path for --format goobj, qualifying the exported symbols.
|
||||||
|
.TP
|
||||||
|
.B \-GOARCH \fIarch\fR
|
||||||
|
Target architecture: amd64, arm64, riscv64 or loong64; overrides the
|
||||||
|
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||||
|
files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||||
|
.TP
|
||||||
|
.B \-o \fIfile\fR
|
||||||
|
Write the output to this file instead of a hex dump on stdout.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 on success, 1 when parsing or assembly fails, and 2 on a usage
|
||||||
|
error.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm asm \-o hello.bin hello_amd64.s raw image
|
||||||
|
gasm asm \-\-format elf \-o k.o k.s linkable ELF object
|
||||||
|
gasm asm \-\-format goobj \-p pkg/path \-o k.o k.s Go object for go build
|
||||||
|
gasm asm \-GOARCH amd64 cpu_x86.s arch override
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-dis (1),
|
||||||
|
.BR gasm\-verify (1)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.TH GASM-AUDIT-INSTRUCTIONS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-audit-instructions \- diff the encoder against the Go toolchain, or measure a corpus
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm audit\-instructions [\-\-corpus [\fIdir\fR]] [amd64|arm64|riscv64|loong64]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Compare the gasm encoder for the given architecture (default amd64)
|
||||||
|
against
|
||||||
|
.B go tool asm
|
||||||
|
and print the diff: superset encodings (gasm-only, shippable via
|
||||||
|
.BR "gasm asm \-\-format goobj" ),
|
||||||
|
known-but-unencodable names (the backlog) and go-only names (feature
|
||||||
|
gaps). The Go side is probed black-box with a battery of bare
|
||||||
|
mnemonics, so the audit tracks whatever toolchain
|
||||||
|
.B go env GOROOT
|
||||||
|
provides.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-\-corpus ,
|
||||||
|
the audit changes shape: it assembles every
|
||||||
|
.I .s
|
||||||
|
file under the given directory (default GOROOT/src) with the gasm
|
||||||
|
encoder only, no toolchain probing. A file whose name carries a
|
||||||
|
recognisable _arch suffix is attempted for that architecture; a file
|
||||||
|
without one is attempted for all four, exactly as a GOARCH build would
|
||||||
|
compile it. The report gives the headline number (files that assemble
|
||||||
|
for every target architecture), the per-architecture pass rates and the
|
||||||
|
most common failure reasons, which drive the encodability backlog by
|
||||||
|
frequency rather than by table order. A run over GOROOT takes under a
|
||||||
|
second.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-\-corpus [\fIdir\fR]
|
||||||
|
Assemble a corpus of .s files and report pass rates and failure
|
||||||
|
reasons.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
The mnemonic-diff mode reports through its output and exits 0; a failed
|
||||||
|
probe or an unknown architecture exits non-zero.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm audit\-instructions amd64
|
||||||
|
gasm audit\-instructions \-\-corpus
|
||||||
|
gasm audit\-instructions \-\-corpus "$(go env GOROOT)/src/crypto"
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-asm (1)
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
.TH GASM-DEBUG 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-debug \- interactive source-level debugger for JIT-assembled functions
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm debug <file.s> \-\-func <name>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Interactive debugger for JIT-assembled functions. Launches the function
|
||||||
|
in a traced subprocess (ptrace), then provides a REPL for
|
||||||
|
single-stepping, breakpoints, register and memory inspection.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.B \-\-script
|
||||||
|
the REPL commands run from a file and the session ends: the headless
|
||||||
|
mode CI and scripts use.
|
||||||
|
.B \-\-cover
|
||||||
|
runs to completion with a breakpoint on every instruction and reports
|
||||||
|
which executed and how often, the label-level coverage view.
|
||||||
|
.SH REPL COMMANDS
|
||||||
|
.TP
|
||||||
|
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR]
|
||||||
|
Set a breakpoint, optionally conditional on a register comparison
|
||||||
|
(register against register or immediate).
|
||||||
|
.TP
|
||||||
|
.B delete \fIlabel|addr\fR
|
||||||
|
Remove a breakpoint.
|
||||||
|
.TP
|
||||||
|
.B info break
|
||||||
|
List all breakpoints.
|
||||||
|
.TP
|
||||||
|
.BR step " [" n ], " s
|
||||||
|
Single-step n instructions; the default is 1.
|
||||||
|
.TP
|
||||||
|
.BR next ", " n
|
||||||
|
Step over a CALL.
|
||||||
|
.TP
|
||||||
|
.BR finish ", " fin
|
||||||
|
Run until the function returns.
|
||||||
|
.TP
|
||||||
|
.BR continue ", " c
|
||||||
|
Run until a breakpoint, watchpoint or exit.
|
||||||
|
.TP
|
||||||
|
.BR disas " [" n ], " u
|
||||||
|
Disassemble n instructions at PC.
|
||||||
|
.TP
|
||||||
|
.B regs
|
||||||
|
Print general-purpose and vector registers.
|
||||||
|
.TP
|
||||||
|
.B where
|
||||||
|
Show the source line and nearest label at PC.
|
||||||
|
.TP
|
||||||
|
.B stack
|
||||||
|
Show the stack near RSP (return address and ABI0 args).
|
||||||
|
.TP
|
||||||
|
.BR bt ", " backtrace
|
||||||
|
Backtrace: current frame plus return address.
|
||||||
|
.TP
|
||||||
|
.B x [\fIaddr\fR] [\fIlen\fR]
|
||||||
|
Hex-dump memory; the defaults are the current PC and 64 bytes.
|
||||||
|
.TP
|
||||||
|
.B w \fIaddr val...\fR
|
||||||
|
Write bytes to memory.
|
||||||
|
.TP
|
||||||
|
.B set \fIreg value\fR
|
||||||
|
Set a register.
|
||||||
|
.TP
|
||||||
|
.B watch \fIaddr\fR [\fBr|w\fR] [\fIsize\fR]
|
||||||
|
Set a hardware watchpoint; writes are watched by default.
|
||||||
|
.TP
|
||||||
|
.B unwatch [\fIslot\fR]
|
||||||
|
Clear one watchpoint, or all without an argument.
|
||||||
|
.TP
|
||||||
|
.BR labels ", " l
|
||||||
|
List function labels and offsets.
|
||||||
|
.TP
|
||||||
|
.BR help ", " h ", " ?
|
||||||
|
Show command help.
|
||||||
|
.TP
|
||||||
|
.BR quit ", " q
|
||||||
|
Kill the debuggee and exit.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-args \fIfile\fR
|
||||||
|
File containing the ABI0 argument block.
|
||||||
|
.TP
|
||||||
|
.B \-buf \fIspec\fR
|
||||||
|
Buffer specification: name:size:pattern[,name:size:pattern...] where
|
||||||
|
pattern is zero, ones, seq, or hex.
|
||||||
|
.TP
|
||||||
|
.B \-cover
|
||||||
|
Run to completion with a breakpoint on every instruction and report
|
||||||
|
which executed and how often.
|
||||||
|
.TP
|
||||||
|
.B \-func \fIname\fR
|
||||||
|
Function to debug.
|
||||||
|
.TP
|
||||||
|
.B \-script \fIfile\fR
|
||||||
|
Run REPL commands from a file (one per line) and exit; - reads stdin.
|
||||||
|
.TP
|
||||||
|
.B \-timeout \fIduration\fR
|
||||||
|
Kill the debuggee after this duration (e.g. 30s); for headless --script
|
||||||
|
runs.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when the scripted session completes and 1 when the debuggee
|
||||||
|
crashes or a check fails; the debugger is Linux-only.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm debug \-\-func name k.s
|
||||||
|
gasm debug \-\-func name \-\-script cmds.txt \-\-timeout 30s k.s
|
||||||
|
gasm debug \-\-func name \-\-cover k.s
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-verify (1)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.TH GASM-DIFF 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-diff \- compare the machine code of two assembly files
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm diff [\-GOARCH arch] <file1.s> <file2.s>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Compare the machine code produced by assembling two files. Shows which
|
||||||
|
functions differ and the byte-level differences. Useful for verifying
|
||||||
|
that two implementations produce identical code, or for tracking
|
||||||
|
encoding changes between Go assembler versions.
|
||||||
|
.PP
|
||||||
|
Functions are paired by exact name unless
|
||||||
|
.B \-\-map
|
||||||
|
says otherwise, so
|
||||||
|
.B \-\-map wideCopyAVX2=wideCopyAVX512
|
||||||
|
pairs two variants regardless of suffix.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-GOARCH \fIarch\fR
|
||||||
|
Target architecture for both files: amd64, arm64, riscv64 or loong64;
|
||||||
|
overrides the file-name suffixes.
|
||||||
|
.TP
|
||||||
|
.B \-\-map \fIspec\fR
|
||||||
|
Comma-separated old=new pairs to match functions with different names.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when every paired function is identical and 1 when anything
|
||||||
|
differs; a usage error exits 2.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm diff hello_amd64.s hello_amd64.s
|
||||||
|
gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-asm (1)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.TH GASM-DIS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-dis \- disassemble machine code to instruction text
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm dis [\-a arch] <file>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Disassemble machine code to instruction text, decoded through
|
||||||
|
golang.org/x/arch.
|
||||||
|
.PP
|
||||||
|
With a
|
||||||
|
.I .s
|
||||||
|
file, the file is assembled first and the listing follows the real
|
||||||
|
layout: one block per TEXT function, local labels printed at their
|
||||||
|
offsets. The architecture comes from the file-name suffix, or from
|
||||||
|
.BR \-a .
|
||||||
|
.PP
|
||||||
|
With any other file, or
|
||||||
|
.B \-
|
||||||
|
for standard input, the bytes are disassembled linearly and
|
||||||
|
.B \-a
|
||||||
|
selects the architecture (amd64, arm64, riscv64 or loong64).
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-a \fIarch\fR
|
||||||
|
Architecture for raw input: amd64, arm64, riscv64 or loong64.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
|
||||||
|
error.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm dis k.s assemble, then list each function
|
||||||
|
gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-asm (1)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.TH GASM-FMT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-fmt \- canonicalise the formatting of Plan 9 assembly sources
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm fmt [\-w|\-l|\-d] [path...]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Canonicalise the formatting of Plan 9 assembly sources: indentation,
|
||||||
|
operand spacing, per-function mnemonic alignment and blank-line layout
|
||||||
|
(exactly one blank line before each label, TEXT and GLOBL block).
|
||||||
|
Formatting is idempotent and preserves every line, comments included.
|
||||||
|
.PP
|
||||||
|
With no paths, or a directory path, every
|
||||||
|
.I .s
|
||||||
|
file below it is reformatted in place and the changed files are listed,
|
||||||
|
the way
|
||||||
|
.B go fmt
|
||||||
|
does;
|
||||||
|
.B .
|
||||||
|
and
|
||||||
|
.B _
|
||||||
|
directories are skipped. Explicit file paths print to stdout unless
|
||||||
|
.B \-w
|
||||||
|
is given.
|
||||||
|
.PP
|
||||||
|
.B \-l
|
||||||
|
and
|
||||||
|
.B \-d
|
||||||
|
rewrite nothing:
|
||||||
|
.B \-l
|
||||||
|
prints the paths whose formatting differs from gasm's (empty output
|
||||||
|
means everything is formatted, which is what a CI check wants),
|
||||||
|
.B \-d
|
||||||
|
prints the diffs. They are mutually exclusive.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-d
|
||||||
|
Print diffs instead of rewriting files.
|
||||||
|
.TP
|
||||||
|
.B \-l
|
||||||
|
List files whose formatting differs from gasm's.
|
||||||
|
.TP
|
||||||
|
.B \-w
|
||||||
|
Write the result to the source file.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm fmt reformat every .s below here
|
||||||
|
gasm fmt \-w kernel_amd64.s canonicalise one file in place
|
||||||
|
gasm fmt \-l *.s list files whose formatting differs
|
||||||
|
gasm fmt \-d kernel_amd64.s print a unified diff instead
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
.TH GASM-LINT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-lint \- run the static checks over assembly files
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm lint <file...>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Run the static checks over the given files and print diagnostics as
|
||||||
|
\fIfile:line:col: severity: message [code]\fR. The exit status is
|
||||||
|
non-zero when an error-severity diagnostic is found; warnings (e.g. the
|
||||||
|
register-clobber audit) do not affect it.
|
||||||
|
.PP
|
||||||
|
The checks are conservative: they report what can be proven wrong and
|
||||||
|
stay quiet otherwise, so a clean lint run is meaningful without
|
||||||
|
suppression lists.
|
||||||
|
.SH RULES
|
||||||
|
.TP
|
||||||
|
.B unknown-instruction
|
||||||
|
The mnemonic is not in the architecture's instruction table.
|
||||||
|
.TP
|
||||||
|
.B operand-count
|
||||||
|
The operand count disagrees with the instruction's declared arity.
|
||||||
|
.TP
|
||||||
|
.B undefined-label
|
||||||
|
A jump target names no label in the function.
|
||||||
|
.TP
|
||||||
|
.B duplicate-label
|
||||||
|
Two labels in one function share a name.
|
||||||
|
.TP
|
||||||
|
.B missing-ret
|
||||||
|
The function can fall off its end without a terminator.
|
||||||
|
.TP
|
||||||
|
.B missing-textflag-include
|
||||||
|
TEXT flags are used without including textflag.h.
|
||||||
|
.TP
|
||||||
|
.B abi-argsize
|
||||||
|
The declared frame or argument size disagrees with the
|
||||||
|
.B //\ function
|
||||||
|
signature.
|
||||||
|
.TP
|
||||||
|
.B unreachable-code
|
||||||
|
Code after RET and before the next label is dead; suppressed for
|
||||||
|
functions with PC-relative or register-indirect control flow.
|
||||||
|
.TP
|
||||||
|
.B register-clobber
|
||||||
|
A register the Go ABI fixes across calls is written without save and
|
||||||
|
restore, computed by liveness over the control-flow graph.
|
||||||
|
.TP
|
||||||
|
.B funcdata-pcdata
|
||||||
|
FUNCDATA and PCDATA indices are malformed.
|
||||||
|
.TP
|
||||||
|
.B unused-label
|
||||||
|
A label no jump reaches.
|
||||||
|
.TP
|
||||||
|
.B invalid-textflag
|
||||||
|
A TEXT flag combination the toolchain rejects.
|
||||||
|
.TP
|
||||||
|
.B stack-imbalance
|
||||||
|
The function does not restore the stack pointer on every path.
|
||||||
|
.TP
|
||||||
|
.B register-width-mismatch
|
||||||
|
An operand register has the wrong width for the instruction.
|
||||||
|
.TP
|
||||||
|
.B abi0-register-args
|
||||||
|
A call passes arguments in registers where ABI0 expects the stack
|
||||||
|
frame.
|
||||||
|
.TP
|
||||||
|
.B nonportable-register-name
|
||||||
|
A register spelling that does not exist on the target architecture.
|
||||||
|
.TP
|
||||||
|
.B unencodable-instruction
|
||||||
|
The mnemonic is known to the table but the encoder cannot assemble it
|
||||||
|
yet (amd64).
|
||||||
|
.TP
|
||||||
|
.B reserved-register-write
|
||||||
|
A write to the register the runtime reserves (arm64 R18).
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-disable \fIcodes\fR
|
||||||
|
Comma-separated rule codes to disable.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when no error-severity diagnostic is found, 1 otherwise, and 2
|
||||||
|
on a usage error.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm lint kernel_amd64.s
|
||||||
|
gasm lint \-disable register-clobber,unused-label *.s
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-asm (1)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.TH GASM-LSP 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-lsp \- run the Plan 9 assembly language server
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm lsp
|
||||||
|
.SH DESCRIPTION
|
||||||
|
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
|
||||||
|
.I .s
|
||||||
|
files; the target architecture is inferred from the file suffix
|
||||||
|
(_amd64.s, _arm64.s, _riscv64.s, _loong64.s).
|
||||||
|
.PP
|
||||||
|
Provides completion, hover, document symbols, push and pull
|
||||||
|
diagnostics, semantic-token highlighting, go-to-definition, find
|
||||||
|
references, rename, formatting, inlay hints, code actions, signature
|
||||||
|
help, document highlights, workspace symbol search, include document
|
||||||
|
links and folding ranges; definition, references and rename work across
|
||||||
|
every open document. Syntax highlighting is delivered as LSP semantic
|
||||||
|
tokens, so no editor-specific grammar is required.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Runs until the client closes the session; exits 0 on a clean shutdown.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.TH GASM-PARSE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-parse \- parse an assembly file and report syntax errors
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm parse <file>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Parse FILE and report syntax errors on stderr. The parser is
|
||||||
|
error-tolerant and line-oriented: a malformed line becomes a diagnostic
|
||||||
|
and parsing continues, so one run reports every syntax error in the
|
||||||
|
file rather than the first.
|
||||||
|
.PP
|
||||||
|
On success, print how many declarations and TEXT functions the file
|
||||||
|
contains. FILE may be
|
||||||
|
.B \-
|
||||||
|
to read standard input.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when the file parses without errors and 1 otherwise.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-tokens (1)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.TH GASM-PROFILE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-profile \- show the basic-block structure of functions
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm profile <file.s>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Show the basic-block structure of functions in an assembly file: each
|
||||||
|
function's labels, their offsets, and the block boundaries. This is
|
||||||
|
the static structure; for runtime execution counts, use
|
||||||
|
.BR "gasm verify \-fuzz" ,
|
||||||
|
which exercises the code paths.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 on success and 1 when the file cannot be assembled.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-verify (1)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.TH GASM-SCAFFOLD 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-scaffold \- generate a differential test skeleton for a kernel file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm scaffold differential <file.s>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Print a differential test skeleton for every
|
||||||
|
.B //\ func
|
||||||
|
signature in FILE. The test seeds random states, drives the kernel and
|
||||||
|
a portable reference (\fI<name>Portable\fR), and compares outputs
|
||||||
|
byte-for-byte. Write the reference bodies, place the file in the
|
||||||
|
kernel's package, and run it in CI.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when the skeleton is written to stdout and 1 when the file
|
||||||
|
cannot be parsed; a usage error exits 2.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm scaffold differential kernel_amd64.s > kernel_differential_test.go
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-verify (1)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.TH GASM-TOKENS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-tokens \- print the lexical token stream of an assembly file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm tokens <file>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Print the lexical token stream of FILE: position, token kind and text,
|
||||||
|
one token per line. FILE may be
|
||||||
|
.B \-
|
||||||
|
to read standard input.
|
||||||
|
.PP
|
||||||
|
This is the front end's raw view, for when the assembler's own
|
||||||
|
diagnostic is not enough: a mis-scanned operand or a swallowed comment
|
||||||
|
shows up here as the tokens the parser actually received.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 on success and 1 when the file cannot be read.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-parse (1)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
.TH GASM-VERIFY 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm-verify \- JIT-assemble a file and run dynamic checks against it
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm verify [\-smoke] [\-abi] [\-fuzz] [\-ground\-truth] [\-profile] [\-call] <file.s>
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Assemble FILE, map it into executable memory and report the available
|
||||||
|
functions. This confirms the assembled image is self-consistent (no
|
||||||
|
unresolved external symbols) and executable, the prerequisite for
|
||||||
|
dynamic testing.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-smoke ,
|
||||||
|
each NOSPLIT function is called with a zeroed argument block to confirm
|
||||||
|
the JIT trampoline works end-to-end. This is safe only for functions
|
||||||
|
that tolerate nil pointers and zero lengths in their arguments.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-abi ,
|
||||||
|
each function is called with sentinel values in the registers the Go
|
||||||
|
ABI fixes across calls (the frame pointer and the goroutine pointer)
|
||||||
|
plus a canary below SP; violations are reported. JIT-based checks run
|
||||||
|
when the host matches the file's architecture (all but loong64, which
|
||||||
|
is ground-truth only for now).
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-fuzz ,
|
||||||
|
each function with a
|
||||||
|
.B //\ func
|
||||||
|
signature is differentially fuzzed against the go-tool-asm version in a
|
||||||
|
subprocess (so a crash on a partial function is reported, not fatal).
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-ground\-truth ,
|
||||||
|
the assembled machine code is compared byte-for-byte against
|
||||||
|
.B go tool asm
|
||||||
|
(relocation sites masked), reporting any encoding drift.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-profile ,
|
||||||
|
the static basic-block structure is listed for each function.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.BR \-call ,
|
||||||
|
a single function is invoked with user-supplied buffers
|
||||||
|
.RB ( \-buf )
|
||||||
|
instead of the smoke/abi/fuzz sweeps. Useful for partial functions
|
||||||
|
(e.g. decoders) that crash on random input but should succeed on valid
|
||||||
|
data.
|
||||||
|
.PP
|
||||||
|
With
|
||||||
|
.B \-save\-corpus
|
||||||
|
(and
|
||||||
|
.BR \-fuzz ),
|
||||||
|
every input that crashes or mismatches is written to the directory as
|
||||||
|
replayable JSON.
|
||||||
|
.B \-replay
|
||||||
|
re-runs saved entries against the kernel, one child process per entry,
|
||||||
|
so an input that crashed the original run crashes only the child: the
|
||||||
|
report says whether each entry reproduces.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-abi
|
||||||
|
Run ABI-checking calls (sentinel registers and red zone).
|
||||||
|
.TP
|
||||||
|
.B \-abi\-n \fIn\fR
|
||||||
|
Number of ABI check iterations with varied inputs; the default is 100.
|
||||||
|
.TP
|
||||||
|
.B \-args \fIspec\fR
|
||||||
|
Scalar args for -call: name=value[,name=value] (decimal or 0x hex).
|
||||||
|
.TP
|
||||||
|
.B \-buf \fIspec\fR
|
||||||
|
Buffer spec for -call: name:size:pattern[,name:size:pattern] where
|
||||||
|
pattern is zero, ones, seq, or hex.
|
||||||
|
.TP
|
||||||
|
.B \-call \fIname\fR
|
||||||
|
Call a single function with -buf instead of the sweeps.
|
||||||
|
.TP
|
||||||
|
.B \-fuzz
|
||||||
|
Differential fuzz: JIT both the gasm and the go-tool-asm versions and
|
||||||
|
compare outputs.
|
||||||
|
.TP
|
||||||
|
.B \-ground\-truth
|
||||||
|
Compare machine code byte-for-byte against go tool asm.
|
||||||
|
.TP
|
||||||
|
.B \-n \fIn\fR
|
||||||
|
Number of fuzz iterations per function; the default is 1000.
|
||||||
|
.TP
|
||||||
|
.B \-profile
|
||||||
|
List basic-block structure per function.
|
||||||
|
.TP
|
||||||
|
.B \-repeat \fIn\fR
|
||||||
|
Number of times to repeat a -call invocation; the default is 1.
|
||||||
|
.TP
|
||||||
|
.B \-replay \fIdir\fR
|
||||||
|
Replay saved corpus entries (JSON files in this directory) against the
|
||||||
|
kernel.
|
||||||
|
.TP
|
||||||
|
.B \-save\-corpus \fIdir\fR
|
||||||
|
With -fuzz: write each failing input to this directory as replayable
|
||||||
|
JSON.
|
||||||
|
.TP
|
||||||
|
.B \-smoke
|
||||||
|
Call each NOSPLIT function with zeroed args.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 when every requested check passes and 1 when any check fails;
|
||||||
|
a file that cannot be assembled exits 1 and a usage error exits 2.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
|
||||||
|
gasm verify \-\-ground\-truth k.s
|
||||||
|
gasm verify \-\-fuzz \-n 500 k.s
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm (1),
|
||||||
|
.BR gasm\-asm (1),
|
||||||
|
.BR gasm\-debug (1)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
.TH GASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
gasm \- developer tooling for Go's Plan 9 assembler
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B gasm
|
||||||
|
.I command
|
||||||
|
.RI [ arguments ]
|
||||||
|
.br
|
||||||
|
.B gasm
|
||||||
|
.BR \-h | \-\-help
|
||||||
|
.br
|
||||||
|
.B gasm
|
||||||
|
.BR \-V | \-\-version
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B gasm
|
||||||
|
bundles a lexer, parser, formatter, linter, standalone assembler and
|
||||||
|
language server for Plan 9 assembly into one self-contained binary. It
|
||||||
|
serves two purposes: it brings developer tooling to the
|
||||||
|
.I .s
|
||||||
|
files of Go programs, and it assembles Plan 9 assembly without the Go
|
||||||
|
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug
|
||||||
|
sections, or the Go toolchain's own GOOBJ format, which
|
||||||
|
.B go build
|
||||||
|
consumes directly.
|
||||||
|
.PP
|
||||||
|
Four architectures are covered: amd64 (including VEX/AVX2 and
|
||||||
|
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
|
||||||
|
target architecture is inferred from the file-name suffix
|
||||||
|
(\fI_amd64.s\fR, \fI_arm64.s\fR, \fI_riscv64.s\fR, \fI_loong64.s\fR) or
|
||||||
|
named explicitly with \fB\-GOARCH\fR where the commands accept it.
|
||||||
|
.SH COMMANDS
|
||||||
|
.TP
|
||||||
|
.B gasm\-tokens(1)
|
||||||
|
Print the lexical token stream.
|
||||||
|
.TP
|
||||||
|
.B gasm\-parse(1)
|
||||||
|
Parse a file and report syntax errors.
|
||||||
|
.TP
|
||||||
|
.B gasm\-fmt(1)
|
||||||
|
Canonicalise formatting: gofmt for assembly.
|
||||||
|
.TP
|
||||||
|
.B gasm\-lint(1)
|
||||||
|
Run the static checks.
|
||||||
|
.TP
|
||||||
|
.B gasm\-asm(1)
|
||||||
|
Assemble \fI.s\fR files to machine code, raw images, ELF objects or GOOBJ.
|
||||||
|
.TP
|
||||||
|
.B gasm\-dis(1)
|
||||||
|
Disassemble machine code, raw bytes or an assembled \fI.s\fR file.
|
||||||
|
.TP
|
||||||
|
.B gasm\-verify(1)
|
||||||
|
JIT-assemble and run dynamic checks: smoke calls, ABI checks,
|
||||||
|
differential fuzzing, ground-truth comparison.
|
||||||
|
.TP
|
||||||
|
.B gasm\-debug(1)
|
||||||
|
Interactive source-level debugger.
|
||||||
|
.TP
|
||||||
|
.B gasm\-diff(1)
|
||||||
|
Compare the machine code of two files byte-for-byte.
|
||||||
|
.TP
|
||||||
|
.B gasm\-profile(1)
|
||||||
|
Show the basic-block structure of functions.
|
||||||
|
.TP
|
||||||
|
.B gasm\-audit\-instructions(1)
|
||||||
|
Diff the encoder against the Go toolchain's name table, or measure a
|
||||||
|
corpus of \fI.s\fR files.
|
||||||
|
.TP
|
||||||
|
.B gasm\-scaffold(1)
|
||||||
|
Generate a differential test skeleton for a kernel file.
|
||||||
|
.TP
|
||||||
|
.B gasm\-lsp(1)
|
||||||
|
Run the language server over standard input/output.
|
||||||
|
.TP
|
||||||
|
.B gasm version
|
||||||
|
Print the version, the same as \fB\-\-version\fR.
|
||||||
|
.SH GLOBAL FLAGS
|
||||||
|
.TP
|
||||||
|
.BR \-h ", " \-\-help
|
||||||
|
Show the command overview.
|
||||||
|
.TP
|
||||||
|
.BR \-V ", " \-\-version
|
||||||
|
Print the version the toolchain recorded at build time.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Exits 0 on success, 1 when a command fails, and 2 on a usage error. An
|
||||||
|
unknown command exits 2.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR gasm\-asm (1),
|
||||||
|
.BR gasm\-fmt (1),
|
||||||
|
.BR gasm\-lint (1),
|
||||||
|
.BR gasm\-verify (1),
|
||||||
|
.BR gasm\-debug (1)
|
||||||
|
.PP
|
||||||
|
The full command reference, with worked examples and every flag, is in
|
||||||
|
docs/CLI.md of the repository
|
||||||
|
.UR https://sourcedock.dev/petrbalvin/gasm-devkit
|
||||||
|
.UE .
|
||||||
+97
-10
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
// Package format implements a canonical formatter for GAsm source — the
|
// Package format implements a canonical formatter for GAsm source, the
|
||||||
// equivalent of gofmt for Plan 9 assembly. It works on the token stream
|
// equivalent of gofmt for Plan 9 assembly. It works on the token stream
|
||||||
// rather than the AST so that every line (including comments and blanks) is
|
// rather than the AST so that every line (including comments and blanks) is
|
||||||
// preserved; it only normalises indentation, operand spacing and per-function
|
// preserved; it only normalises indentation, operand spacing and per-function
|
||||||
@@ -50,15 +50,34 @@ func Source(src string) string {
|
|||||||
}
|
}
|
||||||
case len(line) >= 2 && line[1].Kind == token.Colon:
|
case len(line) >= 2 && line[1].Kind == token.Colon:
|
||||||
inf.kind = kLabel
|
inf.kind = kLabel
|
||||||
|
// Peel stacked labels exactly as the render pass does; the
|
||||||
|
// instruction after the last one is rendered at the
|
||||||
|
// function's alignment width, so its mnemonic counts here.
|
||||||
|
rest := line[2:]
|
||||||
|
for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon &&
|
||||||
|
!isDirective(rest[0].Text) {
|
||||||
|
rest = rest[2:]
|
||||||
|
}
|
||||||
|
if len(rest) > 0 && rest[0].Kind == token.Ident && !isDirective(rest[0].Text) {
|
||||||
|
inf.mnemLen = len(rest[0].Text)
|
||||||
|
if funcID >= 0 && inf.mnemLen > maxWidth[funcID] {
|
||||||
|
maxWidth[funcID] = inf.mnemLen
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
inf.kind = kInstr
|
inf.kind = kInstr
|
||||||
inf.funcID = funcID
|
inf.funcID = funcID
|
||||||
|
// Only an identifier mnemonic takes the alignment width; a
|
||||||
|
// line starting with anything else renders unpadded, so its
|
||||||
|
// length must not enter the width either.
|
||||||
|
if line[0].Kind == token.Ident {
|
||||||
inf.mnemLen = len(line[0].Text)
|
inf.mnemLen = len(line[0].Text)
|
||||||
if funcID >= 0 && inf.mnemLen > maxWidth[funcID] {
|
if funcID >= 0 && inf.mnemLen > maxWidth[funcID] {
|
||||||
maxWidth[funcID] = inf.mnemLen
|
maxWidth[funcID] = inf.mnemLen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
infos[i] = inf
|
infos[i] = inf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,16 +102,39 @@ func Source(src string) string {
|
|||||||
out = line[0].Text + " " + renderOps(line[1:])
|
out = line[0].Text + " " + renderOps(line[1:])
|
||||||
inBody = line[0].Text == "TEXT"
|
inBody = line[0].Text == "TEXT"
|
||||||
case kLabel:
|
case kLabel:
|
||||||
out = line[0].Text + ":"
|
// Every label, and a trailing instruction, becomes its own
|
||||||
// A label may share its line with an instruction; emit the
|
// output line: separate outLines keep the blank-line pass
|
||||||
// instruction on the following line.
|
// honest about what it is looking at.
|
||||||
if rest := line[2:]; len(rest) > 0 {
|
outs = append(outs, outLine{kind: kLabel, text: line[0].Text + ":"})
|
||||||
out += "\n" + renderInstr(rest, maxWidth[inf.funcID])
|
rest := line[2:]
|
||||||
|
for len(rest) >= 2 && rest[0].Kind == token.Ident && rest[1].Kind == token.Colon &&
|
||||||
|
!isDirective(rest[0].Text) {
|
||||||
|
outs = append(outs, outLine{kind: kLabel, text: rest[0].Text + ":"})
|
||||||
|
rest = rest[2:]
|
||||||
}
|
}
|
||||||
|
// A label may share its line with an instruction; the canonical
|
||||||
|
// form puts the instruction on the following line. Trailing
|
||||||
|
// content that does not start an instruction (a stray operand
|
||||||
|
// token) stays on the label line: splitting it off would produce
|
||||||
|
// a line the parser rejects.
|
||||||
|
if len(rest) > 0 && rest[0].Kind == token.Ident && isDirective(rest[0].Text) {
|
||||||
|
// A bare directive cannot start a line of its own (the
|
||||||
|
// parser wants a symbol per line), so a directive sharing
|
||||||
|
// the label's line stays there.
|
||||||
|
outs[len(outs)-1].text += " " + strings.TrimRight(renderOps(rest), " \t")
|
||||||
|
} else if len(rest) > 0 && rest[0].Kind == token.Ident {
|
||||||
|
outs = append(outs, outLine{kind: kInstr, text: strings.TrimRight(renderInstr(rest, maxWidth[inf.funcID]), " \t")})
|
||||||
|
if strings.EqualFold(rest[0].Text, "RET") {
|
||||||
|
inBody = false
|
||||||
|
}
|
||||||
|
} else if len(rest) > 0 {
|
||||||
|
outs[len(outs)-1].text += " " + renderOps(rest)
|
||||||
|
}
|
||||||
|
continue
|
||||||
case kInstr:
|
case kInstr:
|
||||||
out = renderInstr(line, maxWidth[inf.funcID])
|
out = renderInstr(line, maxWidth[inf.funcID])
|
||||||
// A RET ends the body for indentation purposes: comments that
|
// A RET ends the body for indentation purposes: comments that
|
||||||
// follow it — typically the next function's doc comment — belong
|
// follow it, typically the next function's doc comment, belong
|
||||||
// at column 0, not inside the finished function.
|
// at column 0, not inside the finished function.
|
||||||
if strings.EqualFold(line[0].Text, "RET") {
|
if strings.EqualFold(line[0].Text, "RET") {
|
||||||
inBody = false
|
inBody = false
|
||||||
@@ -120,8 +162,8 @@ type outLine struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// normalizeSpacing enforces the canonical blank-line layout: runs of blank
|
// normalizeSpacing enforces the canonical blank-line layout: runs of blank
|
||||||
// lines collapse to one, and a new block — a label, or a TEXT or GLOBL
|
// lines collapse to one, and a new block, a label, or a TEXT or GLOBL
|
||||||
// directive — is preceded by exactly one blank line. Comments immediately
|
// directive, is preceded by exactly one blank line. Comments immediately
|
||||||
// above a block belong to it, so the blank line is inserted before them. No
|
// above a block belong to it, so the blank line is inserted before them. No
|
||||||
// blank line is forced at the top of the file, right after a TEXT (the
|
// blank line is forced at the top of the file, right after a TEXT (the
|
||||||
// function's first label), or between stacked labels that share an address.
|
// function's first label), or between stacked labels that share an address.
|
||||||
@@ -192,6 +234,13 @@ func renderInstr(line []token.Token, width int) string {
|
|||||||
if ops == "" {
|
if ops == "" {
|
||||||
return "\t" + mnem
|
return "\t" + mnem
|
||||||
}
|
}
|
||||||
|
// Alignment is a mnemonic convention: a line that does not start with
|
||||||
|
// an identifier (a stray operand token the parser tolerates) renders
|
||||||
|
// unpadded, so that no alignment width can depend on it and the output
|
||||||
|
// stays stable across passes.
|
||||||
|
if line[0].Kind != token.Ident {
|
||||||
|
return "\t" + mnem + " " + ops
|
||||||
|
}
|
||||||
if width < len(mnem) {
|
if width < len(mnem) {
|
||||||
width = len(mnem)
|
width = len(mnem)
|
||||||
}
|
}
|
||||||
@@ -217,7 +266,19 @@ func renderPreproc(line []token.Token) string {
|
|||||||
func renderOps(toks []token.Token) string {
|
func renderOps(toks []token.Token) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, t := range toks {
|
for i, t := range toks {
|
||||||
if i > 0 && spaceBetween(toks[i-1], t) {
|
sp := i > 0 && spaceBetween(toks[i-1], t)
|
||||||
|
// The accumulated text ending in '/' must never meet a '/' or '*':
|
||||||
|
// the pair would re-lex as a comment and the next pass would see a
|
||||||
|
// different line, whatever the token boundaries were.
|
||||||
|
if !sp && i > 0 && (t.Kind == token.Slash || t.Kind == token.Star) && strings.HasSuffix(b.String(), "/") {
|
||||||
|
sp = true
|
||||||
|
}
|
||||||
|
if sp {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
} else if i > 0 && wouldMerge(toks[i-1], t) {
|
||||||
|
// The tight spelling would re-lex as something else ('/'
|
||||||
|
// before '*' opens a comment), which would make the next
|
||||||
|
// formatting pass see a different line.
|
||||||
b.WriteByte(' ')
|
b.WriteByte(' ')
|
||||||
}
|
}
|
||||||
b.WriteString(t.Text)
|
b.WriteString(t.Text)
|
||||||
@@ -225,8 +286,27 @@ func renderOps(toks []token.Token) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wouldMerge reports whether writing prev immediately before cur would
|
||||||
|
// re-lex as something other than those two tokens: a '/' before a '*' opens
|
||||||
|
// a comment, '>' before '>' shifts, and adjacent operators regroup.
|
||||||
|
func wouldMerge(prev, cur token.Token) bool {
|
||||||
|
var kinds []token.Kind
|
||||||
|
for _, t := range lexer.Tokenize(prev.Text + cur.Text) {
|
||||||
|
if t.Kind == token.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
kinds = append(kinds, t.Kind)
|
||||||
|
}
|
||||||
|
return len(kinds) != 2 || kinds[0] != prev.Kind || kinds[1] != cur.Kind
|
||||||
|
}
|
||||||
|
|
||||||
// spaceBetween decides whether a single space separates prev and cur.
|
// spaceBetween decides whether a single space separates prev and cur.
|
||||||
func spaceBetween(prev, cur token.Token) bool {
|
func spaceBetween(prev, cur token.Token) bool {
|
||||||
|
// '/' beside '/' or '*' would form a comment opener in the output and
|
||||||
|
// make the next pass see a different line; keep them separated.
|
||||||
|
if prev.Kind == token.Slash && (cur.Kind == token.Slash || cur.Kind == token.Star) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch cur.Kind {
|
switch cur.Kind {
|
||||||
case token.RParen:
|
case token.RParen:
|
||||||
return false
|
return false
|
||||||
@@ -274,6 +354,13 @@ func splitLines(toks []token.Token) [][]token.Token {
|
|||||||
if t.Kind == token.EOF {
|
if t.Kind == token.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if t.Kind == token.Illegal {
|
||||||
|
// Illegal tokens carry no canonical spelling: the parser
|
||||||
|
// reports them as errors where they matter, and the formatter
|
||||||
|
// drops them so that a stray character cannot survive into the
|
||||||
|
// output and make the next pass render a different file.
|
||||||
|
continue
|
||||||
|
}
|
||||||
if t.Kind == token.Newline {
|
if t.Kind == token.Newline {
|
||||||
lines = append(lines, cur)
|
lines = append(lines, cur)
|
||||||
cur = nil
|
cur = nil
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestGolden(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestDocCommentIndent checks that a doc comment preceding a TEXT directive
|
// TestDocCommentIndent checks that a doc comment preceding a TEXT directive
|
||||||
// sits at column 0 even when another function (ending in RET) precedes it —
|
// sits at column 0 even when another function (ending in RET) precedes it;
|
||||||
// the RET must terminate the previous body for indentation purposes.
|
// the RET must terminate the previous body for indentation purposes.
|
||||||
func TestDocCommentIndent(t *testing.T) {
|
func TestDocCommentIndent(t *testing.T) {
|
||||||
in := "#include \"textflag.h\"\n" +
|
in := "#include \"textflag.h\"\n" +
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzFormatIdempotency hammers the formatter with arbitrary input. The
|
||||||
|
// contract: formatting twice equals formatting once, and input that parses
|
||||||
|
// cleanly still parses cleanly after formatting. The seed corpus carries the
|
||||||
|
// repository's kernels, so a plain `go test` run replays every seed as a
|
||||||
|
// regression case and CI exercises them without any fuzzing budget.
|
||||||
|
func FuzzFormatIdempotency(f *testing.F) {
|
||||||
|
for _, pattern := range []string{
|
||||||
|
"../testdata/*.s",
|
||||||
|
"../testdata/verify/*.s",
|
||||||
|
} {
|
||||||
|
files, _ := filepath.Glob(pattern)
|
||||||
|
for _, path := range files {
|
||||||
|
if b, err := os.ReadFile(path); err == nil {
|
||||||
|
f.Add(string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ AX, BX\n\tRET\n")
|
||||||
|
f.Add("TEXT ·f(SB),NOSPLIT,$0\n\tMOVQ AX,BX\n\n\n\tRET\n")
|
||||||
|
f.Add("garbage ### ???\n")
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, src string) {
|
||||||
|
once := Source(src)
|
||||||
|
twice := Source(once)
|
||||||
|
if once != twice {
|
||||||
|
t.Fatalf("formatting is not idempotent:\nfirst: %q\nsecond: %q", once, twice)
|
||||||
|
}
|
||||||
|
if _, errs := parser.Parse("in.s", src); len(errs) == 0 {
|
||||||
|
if _, errs := parser.Parse("out.s", once); len(errs) > 0 {
|
||||||
|
t.Fatalf("formatted output of clean input does not parse: %v\n%s", errs[0], once)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("0:A:")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("$0/ *")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("A:TEXT")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("TEXT\n0:A:A0\nA 0")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("00/ /*")
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user