Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a77dd12d2d | ||
|
|
e5df0a9d84 | ||
|
|
a9ee2bc702 | ||
|
|
6f3e054bab | ||
|
|
9179d5cc7a | ||
|
|
431d0d9b0b | ||
|
|
cea5db6964 | ||
|
|
4eb9100def | ||
|
|
43494a30f0 | ||
|
|
f661c2fc78 | ||
|
|
a3eaa82f87 | ||
|
|
1031cd9ae7 | ||
|
|
eda8b16c5f | ||
|
|
718181e7a7 | ||
|
|
107ca512b2 | ||
|
|
4066396226 | ||
|
|
df7a5091e0 | ||
|
|
c27ba30862 | ||
|
|
5f6be4584d | ||
|
|
c1cef7b6e8 | ||
|
|
1120a52a54 | ||
|
|
fa50521619 | ||
|
|
5ee60c860b | ||
|
|
76f8ba6403 | ||
|
|
0211d6672d | ||
|
|
7aba29ac67 | ||
|
|
aeb109a64c | ||
|
|
bd85f8838d | ||
|
|
12a5cfff52 | ||
|
|
cdc3a75c88 | ||
|
|
82dbf087a8 | ||
|
|
0760cc91af | ||
|
|
bd80499c74 | ||
|
|
b93fccc075 | ||
|
|
31ab584eee | ||
|
|
ee2c6d51b3 | ||
|
|
6e49cbd099 | ||
|
|
d4878524e8 | ||
|
|
07f4622ffe | ||
|
|
b3ede5f952 | ||
|
|
5a5936d222 | ||
|
|
4632ac1bb9 | ||
|
|
96000dd64d | ||
|
|
47d561b229 | ||
|
|
27ef71859b | ||
|
|
03f9ef0ac6 | ||
|
|
86cb785e58 | ||
|
|
4ff47fe17a | ||
|
|
d18195f581 | ||
|
|
f0942ff7f3 | ||
|
|
6c66f5bbd9 | ||
|
|
509afbb6c9 | ||
|
|
8080e0acef | ||
|
|
87493d9391 | ||
|
|
63ed141522 | ||
|
|
373ec51061 | ||
|
|
f932c5811c | ||
|
|
b54d2b4520 | ||
|
|
f452b8a995 | ||
|
|
715298ed23 | ||
|
|
bbe14dd1ab | ||
|
|
05de774c0a | ||
|
|
6218023a63 | ||
|
|
71cc65ea48 | ||
|
|
21b6348dbe | ||
|
|
32c453d66e | ||
|
|
59637d35b1 | ||
|
|
e600fc00e7 | ||
|
|
acd30088af | ||
|
|
d03de62c07 | ||
|
|
8de1b371da | ||
|
|
fb6d01a7d0 | ||
|
|
e56c04e9ee | ||
|
|
104bea036b | ||
|
|
6faf850793 | ||
|
|
dac0a5b51b | ||
|
|
965b33e5b0 | ||
|
|
b9dfb48d79 | ||
|
|
41aa8edfd2 | ||
|
|
7f955f8bd1 | ||
|
|
8fdc511d0d | ||
|
|
e5035d92e9 | ||
|
|
b54547c08d | ||
|
|
e9510e8a68 | ||
|
|
9d50212a71 | ||
|
|
f2892e4f59 | ||
|
|
f8dbd4f017 | ||
|
|
e5aabf9801 | ||
|
|
36d1ac804a | ||
|
|
da35883633 | ||
|
|
f0318d2c99 | ||
|
|
a7f9d5eb67 | ||
|
|
0f6e008c9a | ||
|
|
3f35b2a718 | ||
|
|
2c70359ad0 | ||
|
|
d98aadbbbf | ||
|
|
8bded3ea39 | ||
|
|
98a562d8b3 | ||
|
|
33e7fdac98 | ||
|
|
7a69be8b59 | ||
|
|
2385bb7069 | ||
|
|
7bc80ccb54 | ||
|
|
405e2427ed | ||
|
|
4fc96decc4 | ||
|
|
4ccd3bb4c6 | ||
|
|
b5d6f1b46a | ||
|
|
409c8b348d | ||
|
|
2b2a72d54e | ||
|
|
8ab99c9b0c | ||
|
|
82d741514a | ||
|
|
f20aa156d0 | ||
|
|
d853432dba | ||
|
|
fbdad8424f | ||
|
|
a9b54b6868 | ||
|
|
d786b90fa1 | ||
|
|
9ef14bdb71 | ||
|
|
458cdd2066 | ||
|
|
a344399b81 | ||
|
|
a0fa7e802c | ||
|
|
71e8dd550d | ||
|
|
2bd52eb7ad | ||
|
|
daf7fad5b9 | ||
|
|
a6bd9c1ebe | ||
|
|
458d981f31 | ||
|
|
dd356b9e6a | ||
|
|
6c4932c4ec | ||
|
|
8231302bca | ||
|
|
83052ab466 | ||
|
|
9c951c232e | ||
|
|
c2adde948f | ||
|
|
631fb8a8d7 | ||
|
|
470639cd68 | ||
|
|
e7a1c20467 | ||
|
|
0cfed5516c | ||
|
|
a829b8f321 | ||
|
|
abc2d32b83 | ||
|
|
11cac26508 | ||
|
|
ccb155437e | ||
|
|
9c1392dfc3 | ||
|
|
8d611bfdaf | ||
|
|
454a21f5b7 | ||
|
|
d275dee3ae | ||
|
|
7d69dda874 | ||
|
|
d1030a1788 | ||
|
|
8e3b7f1caa | ||
|
|
b22abf512f | ||
|
|
ffc23929c6 | ||
|
|
a4a77b3168 | ||
|
|
380314a05f | ||
|
|
de2f28504a | ||
|
|
7878112b93 | ||
|
|
dd074dbafd | ||
|
|
14de1dd287 | ||
|
|
ea8b02b190 | ||
|
|
b394551af8 | ||
|
|
9b751f6e58 | ||
|
|
955bc6643e | ||
|
|
82e8919208 | ||
|
|
778c297214 | ||
|
|
5de9e985f8 | ||
|
|
e43fa39dc9 | ||
|
|
ddfa33ccc1 | ||
|
|
d82ef33fa9 | ||
|
|
c3540f0549 | ||
|
|
bffe408afa | ||
|
|
103864e8b2 | ||
|
|
aa9c7ca030 | ||
|
|
0354a1f4c1 | ||
|
|
127e52de69 | ||
|
|
22055b9bf3 | ||
|
|
a2301b52de | ||
|
|
bc37ea5b79 | ||
|
|
28bea95128 | ||
|
|
2d803e38d8 | ||
|
|
dadeda144a | ||
|
|
a90ec84bee | ||
|
|
c107b45933 | ||
|
|
a69f8cf4a8 | ||
|
|
cfc3abb752 | ||
|
|
6af3fd60d5 | ||
|
|
257feace6e | ||
|
|
f83bc8ddef | ||
|
|
f0b08ccea0 | ||
|
|
1040739fbb | ||
|
|
6cc6165c2b | ||
|
|
0768b4dc84 | ||
|
|
ae1baa0e61 | ||
|
|
5a8e9acbf3 | ||
|
|
2747fce7d3 | ||
|
|
e02918c17b | ||
|
|
02a6359c1f | ||
|
|
b4c1e133c0 | ||
|
|
1107928870 | ||
|
|
bc4ac93fd9 | ||
|
|
c1bca7ce7e | ||
|
|
fefb76beb9 | ||
|
|
42bc1669d7 | ||
|
|
69dcbec8ef | ||
|
|
f405cea5bc | ||
|
|
f57377abb9 | ||
|
|
dd1782c538 | ||
|
|
17cc49fee4 | ||
|
|
bafb2fd130 | ||
|
|
2f679326c2 | ||
|
|
96f2dd65b4 | ||
|
|
ca887d3927 | ||
|
|
6570709226 | ||
|
|
9a5217d9c1 | ||
|
|
4d01bb3ecf | ||
|
|
332c63e440 | ||
|
|
b306c210c6 | ||
|
|
b9015e1c2e | ||
|
|
26c5008136 | ||
|
|
74d6b90d69 | ||
|
|
7b11c62f53 | ||
|
|
8eed54b3da | ||
|
|
4be16dcdf5 | ||
|
|
ded9cabdf4 | ||
|
|
cf6bc6987e | ||
|
|
ff7b1452b1 | ||
|
|
517c1cea25 | ||
|
|
a3e3010e0f | ||
|
|
057c4eb545 | ||
|
|
f720381d43 | ||
|
|
2c9042d62c | ||
|
|
82ef289d3a | ||
|
|
7246b0e002 | ||
|
|
8cfd40aac8 | ||
|
|
5382c9a8e4 | ||
|
|
53de91b2df | ||
|
|
8a36af7c7d | ||
|
|
e9789ce3f4 | ||
|
|
837231c068 | ||
|
|
95025be1bc | ||
|
|
03a964bb2d | ||
|
|
123a16e346 | ||
|
|
9701812bee | ||
|
|
29ac03468e | ||
|
|
bfb7701db1 | ||
|
|
e8b6ff5d7c | ||
|
|
1456907000 | ||
|
|
ec1c521187 | ||
|
|
a7744c24bd | ||
|
|
522e6f2ae8 | ||
|
|
81d4bd81e4 | ||
|
|
687678a2ea | ||
|
|
b0f9071bf5 | ||
|
|
81e2673923 | ||
|
|
75e9fd771b | ||
|
|
863926abd6 | ||
|
|
241e7256f6 | ||
|
|
6556b85abf | ||
|
|
289cabe993 | ||
|
|
d6cf7cfa44 | ||
|
|
4cc2f0eba5 | ||
|
|
97dfaa7526 | ||
|
|
66aa4dbc8b | ||
|
|
dce5d31462 | ||
|
|
9dc3987e02 | ||
|
|
9b238a525a | ||
|
|
ad82aac663 | ||
|
|
0629f5e2df | ||
|
|
ecb203dcf5 | ||
|
|
6c672567f3 | ||
|
|
cc6e416c59 | ||
|
|
c66a47973a | ||
|
|
9629897202 | ||
|
|
5399a8a724 | ||
|
|
de5d9f358e | ||
|
|
ca3fdce0e0 | ||
|
|
fc2d92eabd | ||
|
|
39d2e80145 | ||
|
|
9f4f949c1f | ||
|
|
f0d5238c47 | ||
|
|
2931bbd6b2 | ||
|
|
63562a503a | ||
|
|
e836d6150d | ||
|
|
8a51b060da | ||
|
|
d3d47db727 | ||
|
|
0758556b7d | ||
|
|
ddb8440340 | ||
|
|
f15ff66fb1 | ||
|
|
187e4856d3 | ||
|
|
d315a998ce | ||
|
|
a6f3828c02 | ||
|
|
e3b35bb817 | ||
|
|
eb0a89e58d | ||
|
|
dd32d9e66e | ||
|
|
3a73acb20a | ||
|
|
7604a9443f | ||
|
|
b3908fc43d | ||
|
|
eb8b0cd316 | ||
|
|
a8bfd54ed2 | ||
|
|
375182ef1f | ||
|
|
87b1081c53 | ||
|
|
f3c8510a58 | ||
|
|
ebdf14939f | ||
|
|
79a2c16bac | ||
|
|
401386956c | ||
|
|
4258131a3a | ||
|
|
94e09e8070 | ||
|
|
7aefe6a42d | ||
|
|
ac1c05c793 | ||
|
|
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 |
No files matched your search
+230
-87
@@ -1,10 +1,39 @@
|
||||
# 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.
|
||||
#
|
||||
# No gate runs at the tag: the tagged tree was tested on every push to development,
|
||||
# the full suite is the suite workflow's business, and race never runs in CI at all.
|
||||
# This pipeline publishes and nothing else. The version contract has no injection
|
||||
# step: 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 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 matrix carries the platforms the project ships: Linux on amd64, arm64,
|
||||
# loong64 and riscv64, and FreeBSD on amd64 and arm64. Go cross-compiles
|
||||
# FreeBSD natively and the tree builds under CGO_ENABLED=0, which the
|
||||
# suite's cross-build gates prove for the whole module; the ptrace suite
|
||||
# itself still needs a FreeBSD machine, so nothing FreeBSD runs here.
|
||||
# Nothing is installed: the fedora job image carries git, perl and node
|
||||
# (verified on the runner, 2026-10-04).
|
||||
#
|
||||
# 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. Every
|
||||
# step is one command, and the scripted steps are Perl with builtins only: Perl drives
|
||||
# curl through a list, so no argument is ever word-split, globbed or quoted wrong.
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
env:
|
||||
# The box is shared with the forge, so parallelism is bounded on purpose.
|
||||
GOFLAGS: -p=1
|
||||
GOMAXPROCS: "2"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: fedora
|
||||
@@ -16,58 +45,91 @@ jobs:
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: riscv64
|
||||
- goos: linux
|
||||
goarch: loong64
|
||||
- goos: linux
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: amd64
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27"
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Validate tag and build
|
||||
id: build
|
||||
- name: Validate the tag
|
||||
id: version
|
||||
env:
|
||||
VERSION: ${{ gitea.ref_name }}
|
||||
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
|
||||
echo "ERROR: expected a semver tag like v1.2.3, got: '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
echo "version_no_v=${VERSION_NO_V}" >> "$GITEA_OUTPUT"
|
||||
|
||||
mkdir -p bin
|
||||
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 \
|
||||
go build -ldflags "-s -w -X main.version=${VERSION_NO_V}" \
|
||||
-o "bin/gasm-${VERSION_NO_V}-${{ matrix.goos }}-${{ matrix.goarch }}" \
|
||||
./cmd/gasm
|
||||
- name: Build
|
||||
env:
|
||||
VERSION_NO_V: ${{ steps.version.outputs.version_no_v }}
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: "0"
|
||||
run: |
|
||||
# Nothing is injected. The toolchain records the tag into the binary's build
|
||||
# information, so the version is right because this build happens at the tag, and
|
||||
# there is no path for anyone to get wrong. -s -w only strips symbols.
|
||||
go build -ldflags "-s -w" -o "bin/gasm-${VERSION_NO_V}-${GOOS}-${GOARCH}" ./cmd/gasm
|
||||
|
||||
# Artifacts stay on v3: v4 and later detect Gitea as GHES and abort.
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gasm-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: bin/gasm-${{ steps.version.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
if-no-files-found: error
|
||||
|
||||
- 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'
|
||||
env:
|
||||
TAG: ${{ gitea.ref_name }}
|
||||
BIN: bin/gasm-${{ steps.version.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
run: |
|
||||
chmod +x bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
./bin/gasm-${{ steps.build.outputs.version_no_v }}-${{ matrix.goos }}-${{ matrix.goarch }} --version
|
||||
perl -e '
|
||||
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:
|
||||
runs-on: fedora
|
||||
needs: build
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -77,81 +139,162 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Extract CHANGELOG section
|
||||
- name: Extract the CHANGELOG section
|
||||
env:
|
||||
VERSION: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION_NO_V="${VERSION#v}"
|
||||
# Each step derives what it needs from the tag, so no value has to travel between
|
||||
# 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 \
|
||||
| sed '$d' \
|
||||
| tail -n +2 \
|
||||
> release-body.md
|
||||
- name: Build the release request
|
||||
run: |
|
||||
perl -e '
|
||||
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
|
||||
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
|
||||
- name: Create the release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BODY=$(sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' -e 's/\r//g' release-body.md | sed ':a;N;$!ba;s/\n/\\n/g')
|
||||
BODY="\"${BODY}\""
|
||||
|
||||
response=$(curl -sS -w '\n%{http_code}' \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
||||
-d "{\"tag_name\":\"${GITEA_REF_NAME}\",\"name\":\"${GITEA_REF_NAME}\",\"body\":${BODY},\"draft\":false,\"prerelease\":false}")
|
||||
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
payload=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "HTTP ${http_code}"
|
||||
if [ "$http_code" != "201" ]; then
|
||||
echo "Failed to create release: ${payload}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RELEASE_ID=$(echo "$payload" | grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+')
|
||||
echo "Created release ID=${RELEASE_ID}"
|
||||
printf '%s' "${RELEASE_ID}" > release-id.txt
|
||||
perl -e '
|
||||
my @cmd = (q{curl}, q{-sS}, q{-o}, q{response.json}, q{-w}, q{%{http_code}},
|
||||
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
||||
q{-H}, q{Content-Type: application/json},
|
||||
q{-X}, q{POST},
|
||||
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases},
|
||||
q{--data-binary}, q{@release.json});
|
||||
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
||||
my $code = <$curl>;
|
||||
my $ok = close($curl);
|
||||
my $exit = $? >> 8;
|
||||
$code = defined $code ? $code : q{};
|
||||
$ok or die qq{ERROR: curl failed (exit $exit) calling $ENV{GITEA_SERVER_URL}\n};
|
||||
open(my $r, q{<:raw}, q{response.json}) or die qq{response.json: $!};
|
||||
my $body = do { local $/; <$r> };
|
||||
close($r);
|
||||
$code eq q{201} or die qq{ERROR: the release was not created, HTTP $code: $body\n};
|
||||
$body =~ m{"id"\s*:\s*([0-9]+)} or die qq{ERROR: no release id in the response: $body\n};
|
||||
open(my $o, q{>}, q{release-id.txt}) or die qq{release-id.txt: $!};
|
||||
print $o $1;
|
||||
close($o);
|
||||
print qq{release id $1\n};
|
||||
'
|
||||
|
||||
- name: Upload assets
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RELEASE_ID=$(cat release-id.txt)
|
||||
|
||||
for binary in dist/gasm-*/gasm-*; do
|
||||
[ -f "$binary" ] || continue
|
||||
fname=$(basename "$binary")
|
||||
echo "Uploading ${fname}..."
|
||||
http_code=$(curl -sS -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
-X POST \
|
||||
--data-binary "@${binary}" \
|
||||
"${GITEA_SERVER_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${fname}")
|
||||
echo " HTTP ${http_code}"
|
||||
if [ "$http_code" != "201" ]; then
|
||||
echo "Failed to upload ${fname}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Release ${GITEA_REF_NAME} is live."
|
||||
perl -e '
|
||||
open(my $f, q{<}, q{release-id.txt}) or die qq{release-id.txt: $!};
|
||||
my $id = <$f>;
|
||||
close($f);
|
||||
chomp $id;
|
||||
my @files = grep { -f $_ } glob(q{dist/*/*});
|
||||
@files or die qq{ERROR: no assets under dist/\n};
|
||||
# A file that arrived empty from the artifact step would be uploaded as an
|
||||
# empty attachment, every status would still be 201, and the run would go
|
||||
# green over a release nobody can install. Refuse it here, before the
|
||||
# upload, and verify what was stored afterwards.
|
||||
my %size;
|
||||
for my $path (@files) {
|
||||
my $n = -s $path // 0;
|
||||
(my $name = $path) =~ s{.*/}{};
|
||||
$n > 0 or die qq{ERROR: $path is empty, so there is nothing to upload\n};
|
||||
$size{$name} = $n;
|
||||
}
|
||||
my $bad = 0;
|
||||
for my $path (@files) {
|
||||
(my $name = $path) =~ s{.*/}{};
|
||||
my @cmd = (q{curl}, q{-sS}, q{-o}, q{/dev/null}, q{-w}, q{%{http_code}},
|
||||
q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}},
|
||||
q{-H}, q{Content-Type: application/octet-stream},
|
||||
# The @ must not sit inside a qq{} string: there it starts an
|
||||
# array interpolation and the upload body collapses to empty,
|
||||
# which Gitea stores as a 201-created zero-byte attachment.
|
||||
q{-X}, q{POST}, q{--data-binary}, q{@} . $path,
|
||||
qq{$ENV{GITEA_SERVER_URL}/api/v1/repos/$ENV{GITEA_REPOSITORY}/releases/$id/assets?name=$name});
|
||||
open(my $curl, q{-|}, @cmd) or die qq{curl: $!};
|
||||
my $code = <$curl>;
|
||||
my $ok = close($curl);
|
||||
my $exit = $? >> 8;
|
||||
$code = defined $code ? $code : q{};
|
||||
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};
|
||||
}
|
||||
# Read every asset back through the release download route and require the
|
||||
# served length to be the file that was sent: stored but empty is a broken
|
||||
# release however green the run looks.
|
||||
open(my $v, q{<}, q{version-no-v.txt}) or die qq{version-no-v.txt: $!};
|
||||
my $v = <$v>;
|
||||
close($v);
|
||||
chomp $v;
|
||||
for my $name (sort keys %size) {
|
||||
my $url = qq{$ENV{GITEA_SERVER_URL}/$ENV{GITEA_REPOSITORY}/releases/download/v$v/$name};
|
||||
my @head = (q{curl}, q{-sS}, q{-I}, q{-H}, qq{Authorization: token $ENV{GITEA_TOKEN}}, $url);
|
||||
open(my $h, q{-|}, @head) or die qq{curl: $!};
|
||||
my $len;
|
||||
my $status;
|
||||
while (my $l = <$h>) {
|
||||
$status = $1 if $l =~ m{^HTTP/\S+\s+(\d+)};
|
||||
$len = $1 if $l =~ m{^content-length:\s*(\d+)}i;
|
||||
}
|
||||
my $ok = close($h);
|
||||
$len = defined $len ? $len : 0;
|
||||
if (!$ok || $status != 200 || $len != $size{$name}) {
|
||||
printf qq{ERROR: %s serves %s bytes, expected %d\n}, $name, $len, $size{$name};
|
||||
$bad = 1;
|
||||
next;
|
||||
}
|
||||
printf qq{%s: serves %d bytes\n}, $name, $len;
|
||||
}
|
||||
exit($bad ? 1 : 0);
|
||||
'
|
||||
@@ -0,0 +1,86 @@
|
||||
# Suite, Go. Dispatched by hand, on development. Never a push gate.
|
||||
#
|
||||
# The complete gate set minus race: the build, the FreeBSD cross-builds the
|
||||
# port's compile proof needs (amd64 and arm64, the same two the release
|
||||
# matrix ships), both static gates, the full suite with every short-layer
|
||||
# skip unskipped, and the coverage floor. It is the pipeline form of the
|
||||
# local `just test`, for the moments when the tree must be proven end to end
|
||||
# and nobody is at the keyboard.
|
||||
#
|
||||
# Race never runs in CI. It roughly doubles the time and the memory on a box shared
|
||||
# with the forge, and the local `just gates` races the tree on the machine at the
|
||||
# keyboard, which is where that gate belongs.
|
||||
name: Suite
|
||||
|
||||
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:
|
||||
suite:
|
||||
runs-on: fedora
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
# The module is the source of truth for the version, so it cannot drift.
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
||||
- name: FreeBSD build (amd64)
|
||||
# The port is pure Go: the cross-build needs no C toolchain and the
|
||||
# release matrix ships the same two binaries.
|
||||
run: CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build ./...
|
||||
|
||||
- name: FreeBSD build (arm64)
|
||||
run: CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 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
|
||||
# Exits non-zero when it has something to rewrite, so it needs no output capture.
|
||||
run: go fix -diff ./...
|
||||
|
||||
- name: Tests
|
||||
# The full suite over the logic packages (`packages` in the justfile), every
|
||||
# short-layer skip lifted, with the coverage profile. No timeout: a run that
|
||||
# does not finish is a defect to find.
|
||||
run: go test -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||
|
||||
- name: Tests outside the coverage set
|
||||
# The same second invocation as the local gate: the CLI's exit codes and
|
||||
# manual-page guard, and the debugger's units, live ptrace sessions included.
|
||||
# They run without a profile, because a thin main and a ptrace-bound package
|
||||
# would drag the floor down rather than measure the product.
|
||||
run: go test -count=1 -timeout 0 ./cmd/... ./debug/...
|
||||
|
||||
- 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);
|
||||
'
|
||||
+66
-74
@@ -1,4 +1,20 @@
|
||||
# Test — gasm-devkit. Runs on push and pull request to development.
|
||||
# Test, Go. Push and pull request to development. Never on main.
|
||||
#
|
||||
# The push path owns a two-minute budget end to end, so it carries exactly the gates
|
||||
# that fit it: the format check, go vet, the suite's short layer and the coverage
|
||||
# floor. There is no build step (go test compiles what it runs) and the modernisation
|
||||
# gate (`go fix -diff`) belongs to the suite workflow, which is dispatched by hand.
|
||||
# Nothing is installed: the fedora job image carries git, perl and node (verified on
|
||||
# the runner, 2026-10-04), and no pipeline ever installs gcc or runs the race detector.
|
||||
#
|
||||
# The live ptrace sessions and the other deliberate-run categories skip under
|
||||
# testing.Short: they need the machine to themselves and belong to the suite workflow
|
||||
# and to the local `just test`, never to every push.
|
||||
#
|
||||
# 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 with
|
||||
# builtins only: Fedora packages the Perl modules separately, so nothing beyond `perl`
|
||||
# itself may be assumed present, and there is no bashism to trip over.
|
||||
name: Test
|
||||
|
||||
on:
|
||||
@@ -7,90 +23,66 @@ on:
|
||||
pull_request:
|
||||
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:
|
||||
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:
|
||||
runs-on: fedora
|
||||
needs: vet
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
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
|
||||
run: go mod download
|
||||
|
||||
- name: Install gcc
|
||||
run: dnf install -y gcc
|
||||
|
||||
- name: go test -race
|
||||
run: go test -race -count=1 ./...
|
||||
|
||||
- name: Coverage gate — 80 % minimum
|
||||
- name: Format
|
||||
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
|
||||
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);
|
||||
'
|
||||
|
||||
build:
|
||||
runs-on: fedora
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27"
|
||||
- name: Tests
|
||||
# The pattern is `packages` in the project's justfile, so the floor is the
|
||||
# same number the local gate reports: the logic packages, since a thin cmd/
|
||||
# and a ptrace-bound debug package would drag the total under it. No timeout
|
||||
# anywhere: `-timeout 0` disables go test's own ten-minute default, because a
|
||||
# run that does not finish is a defect to find and a timeout only hides it.
|
||||
run: go test -short -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
- name: Tests outside the coverage set
|
||||
# The CLI's exit codes and manual-page guard and the debugger's
|
||||
# architecture-neutral units, still tested but outside the profile the floor
|
||||
# is computed from. `-short` skips the debugger's live ptrace sessions.
|
||||
run: go test -short -count=1 -timeout 0 ./cmd/... ./debug/...
|
||||
|
||||
- name: Build
|
||||
run: go build -ldflags="-s -w" -o bin/gasm ./cmd/gasm
|
||||
|
||||
- name: Smoke test
|
||||
run: ./bin/gasm --version
|
||||
- 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);
|
||||
'
|
||||
+8
-14
@@ -1,24 +1,18 @@
|
||||
# Metadata (always first, per repo convention)
|
||||
.idea/
|
||||
.zcode/
|
||||
.mimocode/
|
||||
|
||||
# Binaries
|
||||
/gasm
|
||||
# Build output
|
||||
/bin/
|
||||
*.exe
|
||||
|
||||
# Test and coverage artefacts
|
||||
/gasm
|
||||
coverage.out
|
||||
*.test
|
||||
|
||||
# Crash dumps
|
||||
# The fuzz campaigns' cache: committed seeds live in the test file, crashes are
|
||||
# minimised into it, so everything go test -fuzz writes under testdata/fuzz is
|
||||
# transient. pending-* holds minimised reproductions awaiting their owner's fix.
|
||||
/asm/testdata/fuzz/
|
||||
|
||||
# Crash dumps from the emulator runs
|
||||
core
|
||||
core.*
|
||||
*.core
|
||||
|
||||
# Scratch / temporary work
|
||||
_scratch/
|
||||
|
||||
# ZCode workspace
|
||||
.zcode
|
||||
+905
-149
File diff suppressed because it is too large.
Load diff
+108
-80
@@ -1,107 +1,135 @@
|
||||
# Contributing to gasm-devkit
|
||||
# Contributing
|
||||
|
||||
Thanks for contributing to gasm-devkit.
|
||||
Contributions to **gasm-sdk** 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
|
||||
|
||||
Requirements: Go 1.27 or later, the [just](https://github.com/casey/just)
|
||||
command runner, and a Linux host on amd64, arm64, riscv64 or loong64.
|
||||
Requirements: Go 1.27.1, the exact version the `go` directive in `go.mod`
|
||||
declares, [just](https://github.com/casey/just) for the recipes, and a C
|
||||
compiler (gcc), because `just gates` includes `just race` and the race
|
||||
detector needs cgo.
|
||||
|
||||
```sh
|
||||
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
|
||||
cd gasm-devkit
|
||||
just install # download module dependencies
|
||||
just build # go vet + gofmt check
|
||||
just test # full suite, race detector, 80 % coverage gate
|
||||
git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git
|
||||
cd gasm-sdk
|
||||
just build
|
||||
just gates
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Branch from `development`; never commit directly to `main` (`main` is
|
||||
release-only: merge from `development`, then tag).
|
||||
2. Commit with [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
`type(scope): description`: subject line only, imperative mood,
|
||||
lowercase after the colon, no trailing dot. Allowed types: `feat`,
|
||||
`fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`,
|
||||
`build`, `revert`. The only line after the subject is the trailer:
|
||||
`Assisted-by: <model-name>`. No `Co-Authored-By`, no `Signed-off-by`,
|
||||
no other trailers.
|
||||
3. Record every user-visible change in `CHANGELOG.md` under
|
||||
`## [development]` (categories: Added, Changed, Fixed, Removed,
|
||||
Security).
|
||||
4. Add or update tests; coverage must stay **at or above 80 %** (hard
|
||||
gate, enforced by CI).
|
||||
5. Update the documentation when behaviour, flags or the public surface
|
||||
change.
|
||||
6. Open a pull request against `development`.
|
||||
1. Branch from `development`. Never commit directly to `main`, which is release-only.
|
||||
2. Commit in [Conventional Commits](https://www.conventionalcommits.org/) form:
|
||||
`type(scope): description`, subject line only, imperative mood, lowercase after the
|
||||
colon, no trailing full stop. Allowed types: `feat`, `fix`, `docs`, `style`,
|
||||
`refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert`.
|
||||
3. One logical change per commit. A refactor, a behaviour change and a formatting pass
|
||||
are three commits, never one.
|
||||
4. Record every user-visible change in `CHANGELOG.md` under `## [development]`.
|
||||
5. Add or update tests. Coverage stays at 80 percent or more; it is a hard gate.
|
||||
6. Update the documentation when the public API, the configuration or the behaviour
|
||||
changes.
|
||||
7. Open a pull request against `development`.
|
||||
|
||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`;
|
||||
CI builds and publishes the binaries for all four architectures.
|
||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The release
|
||||
workflow builds the assets and publishes the release and its notes.
|
||||
|
||||
## Code style
|
||||
|
||||
`gofmt` and `go vet` via `just fmt` / `just build`; both must pass with
|
||||
zero output; `go fix -diff ./...` must report nothing on touched packages.
|
||||
`gofmt` and `go vet` run through `just fmt` and `just vet`, with zero diff and zero
|
||||
warnings tolerated. `just vet` is two gates, `go vet ./...` and `go fix -diff ./...`,
|
||||
so the modernisation rewrites are enforced too. `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
|
||||
recipe file holds the commands, and the language and standard-library surface is the one
|
||||
the `go` directive in `go.mod` pins.
|
||||
|
||||
- Standard library only in production code; `golang.org/x/arch` is used
|
||||
in tests only (round-trip decoding) and is never linked into the `gasm`
|
||||
binary.
|
||||
- No cgo, no C, no external toolchains at runtime.
|
||||
- Explicit `if err != nil`; errors wrapped with
|
||||
`fmt.Errorf("context: %w", err)`; no panics outside `main`.
|
||||
- The parser, lexer and formatter are hand-written; the `arch` instruction
|
||||
tables are generated only via `_gen/gen.go` (`just gen`), never edited.
|
||||
- `golang.org/x/arch` is the one module dependency, and it is linked into the binary:
|
||||
`gasm dis` and the debugger's listings decode through it. Everything else is the
|
||||
standard library.
|
||||
- No cgo and no C. The standalone encoder paths (`gasm asm --format raw` and `--format
|
||||
elf`) need no Go installation; `gasm verify --ground-truth`, `gasm verify --fuzz`,
|
||||
`gasm audit-instructions` and `gasm asm --format goobj` resolve through the installed
|
||||
Go toolchain.
|
||||
- The parser, lexer and formatter are hand-written; the `arch` instruction tables are
|
||||
generated only by `_gen/gen.go` (`just gen`) and never edited by hand.
|
||||
- Assembly committed to the repository goes through `gasm fmt` and `gasm lint`, so a
|
||||
`.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
|
||||
go test -run TestVexGroundTruth ./asm/
|
||||
go test -run TestGroundTruthBasic ./verify/
|
||||
go test -run TestGOObjectLinkAndRun ./asm/
|
||||
go test -run TestFuzzWideCopy ./verify/
|
||||
```
|
||||
## AI contribution policy
|
||||
|
||||
The interactive debugger (`gasm debug`) requires a compiled binary on
|
||||
`$PATH`; `go run` does not work for the traced child process. Install
|
||||
first with `just install-bin`.
|
||||
AI tools are welcome as productivity aids and are a normal part of modern software
|
||||
development. What matters is that the contribution stays understandable, reviewable and
|
||||
genuinely useful.
|
||||
|
||||
## CI (Gitea Actions)
|
||||
- **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:
|
||||
|
||||
Workflows live in `.gitea/workflows/` and run on self-hosted runners:
|
||||
```
|
||||
Assisted-by: MODEL
|
||||
```
|
||||
|
||||
Name the model that did the work, spelled the way its maker spells it, for example
|
||||
`GLM 5.3`, `DeepSeek V4.1 Flash` or `Qwen 3.8 Flash`. No `Co-Authored-By`, no `Signed-off-by`,
|
||||
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.
|
||||
|
||||
AI assists. It does not replace judgement.
|
||||
|
||||
## Continuous integration
|
||||
|
||||
Workflows live in `.gitea/workflows/` and run on the project's own runners:
|
||||
|
||||
| Workflow | Trigger | What it does |
|
||||
|----------|---------|--------------|
|
||||
| Test | push / PR to `development` | gofmt check, `go vet`, `go test -race`, 80 % coverage gate |
|
||||
| Release | tag `v*` | cross-compiles binaries for linux/{amd64,arm64,riscv64,loong64} and publishes the Gitea release |
|
||||
|---|---|---|
|
||||
| Test | push or pull request to `development` | the format check, `go vet`, and the short layer of the test suite with the coverage profile and the 80 % floor, inside the two-minute budget |
|
||||
| Suite | dispatched by hand | the complete gate set minus race: the build, the two FreeBSD cross-build gates (amd64, arm64), both static gates, the full suite and the coverage floor |
|
||||
| Release | a `v*` tag | the matrix build (Linux on four architectures, FreeBSD on two), the version smoke test and the release with its assets; no gate runs at the tag |
|
||||
|
||||
The Definition of Done (`just build` + `just test` + `just fmt`) must
|
||||
still pass locally before pushing.
|
||||
|
||||
## 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**.
|
||||
The local equivalent is `just gates`, which is the same set plus the race detector. Race
|
||||
never runs in CI, on a push or a tag: it would double the time and the memory a shared
|
||||
runner cannot spare, so the local `just gates` runs it before the tag is cut.
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Open an issue at
|
||||
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues)
|
||||
with the version (`gasm --version`), OS and architecture, the exact
|
||||
command, the full output, and the expected versus actual behaviour.
|
||||
Open an issue at `https://sourcedock.dev/petrbalvin/gasm-sdk/issues` with the
|
||||
version, the operating system and architecture, the exact command, the full output,
|
||||
and the expected against the actual behaviour.
|
||||
|
||||
**Security issues:** email **opensource@petrbalvin.org** instead of opening
|
||||
a public issue.
|
||||
**Security issues do not go in the issue tracker.** Report them as
|
||||
[SECURITY.md](SECURITY.md) describes, to **opensource@petrbalvin.org**.
|
||||
@@ -1,13 +1,65 @@
|
||||
# gasm-devkit
|
||||
# GAsm: Software Development Kit for Plan 9 Assembly
|
||||
|
||||
Developer tooling for **GAsm**, Go's built-in Plan 9 assembler.
|
||||
> **Warning: this is an experiment.** gasm-sdk 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. Only
|
||||
> amd64 is validated on real hardware; the other three architectures run
|
||||
> under emulation ([Validation status](#validation-status)).
|
||||
|
||||
Go ships an assembler but no tooling for it: there is no syntax highlighting,
|
||||
no autocomplete, no linter, no static analyser, no formatter, no standalone
|
||||
assembler and no debugger for `.s` files. Developers write assembly blind,
|
||||
validate it by benchmark, and debug it by print statement. gasm-devkit is the
|
||||
missing toolkit: a single, self-contained binary, `gasm`, that brings proper
|
||||
developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
||||
**GAsm** is Go's Plan 9 assembler, and Go ships it without tooling:
|
||||
there is no formatter, no linter and no debugger for `.s` files, and no
|
||||
assembler that works without a Go installation. Developers write
|
||||
assembly blind, validate it by benchmark, and debug it by print
|
||||
statement. gasm-sdk is the missing toolkit: a single, self-contained
|
||||
binary, `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 and writes raw images or linkable ELF objects with DWARF5
|
||||
debug sections, with no Go installation in the loop; the Go
|
||||
toolchain's own GOOBJ format, which `go build` consumes in place of
|
||||
the toolchain's output, needs the installed toolchain.
|
||||
|
||||
## 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-sdk exists
|
||||
to give that syntax the tooling it deserves.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -16,68 +68,188 @@ developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
|
||||
directly.
|
||||
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
||||
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
||||
operating recursively on directories the way `go fmt` does.
|
||||
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
|
||||
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
|
||||
`register-clobber` (Go ABI register liveness over the control-flow graph),
|
||||
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`.
|
||||
`undefined-label`, `abi-argsize` (declared argument area vs the `// func`
|
||||
signature), `register-clobber` (Go ABI register liveness over the
|
||||
control-flow graph), `stack-imbalance`, `abi0-register-args` and
|
||||
`unencodable-instruction`.
|
||||
- **Standalone assembler.** `gasm asm` encodes all four architectures without
|
||||
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
|
||||
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
|
||||
consumes in place of the toolchain's output.
|
||||
the Go toolchain and writes raw images or linkable ELF objects (with DWARF5
|
||||
debug sections) with no Go installation needed, or the Go toolchain's own
|
||||
GOOBJ format, which needs the installed toolchain and which `go build`
|
||||
consumes in place of the toolchain's output. Framed functions get the
|
||||
stack-split guard and the morestack block, byte-identical to the
|
||||
toolchain's, so split functions link too. The assembler preprocesses
|
||||
like the toolchain (`#define`, `#include` with `-I`, `#ifdef`), generates
|
||||
`go_asm.h` from the package's Go files, and carries `PCALIGN`, the
|
||||
`LOCK`/`REP` prefixes and the literal-data pseudo-ops.
|
||||
- **Disassembler.** `gasm dis` lists a `.s` file's functions at their real
|
||||
offsets after assembling, or disassembles raw bytes from a file or stdin.
|
||||
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
|
||||
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
|
||||
canary), differential fuzzing against the `go tool asm` build, and
|
||||
byte-for-byte ground-truth comparison of the machine code.
|
||||
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
|
||||
breakpoints (optionally conditional), hardware watchpoints, register and
|
||||
memory inspection, and headless script runs with label-level coverage.
|
||||
memory inspection, and headless script runs that report instruction and
|
||||
label coverage; it runs on Linux (all four architectures) and FreeBSD
|
||||
(amd64, arm64).
|
||||
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
|
||||
push and pull diagnostics, semantic-token highlighting, go-to-definition,
|
||||
find references, rename, formatting, inlay hints, code actions, signature
|
||||
help, document highlights, workspace symbol search, #include document
|
||||
links and folding ranges over stdio.
|
||||
links and folding ranges over stdio; definition, references and rename
|
||||
work across every open document and the indexed workspace files beyond
|
||||
them, and the quick fixes add a missing textflag.h include and set the
|
||||
argument area from the // func signature.
|
||||
- **Comparators and audits.** `gasm diff` compares the machine code of two
|
||||
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
|
||||
`gasm audit-instructions` diffs the encoder against the installed toolchain,
|
||||
and `gasm scaffold` generates a differential test skeleton for a kernel.
|
||||
- **Complete instruction coverage.** The instruction tables are generated
|
||||
from the Go toolchain's own assembler source, so the toolkit recognises
|
||||
every mnemonic the real assembler accepts; `just gen` refreshes them.
|
||||
|
||||
### Architecture support
|
||||
|
||||
Four architectures, the four that matter in practice:
|
||||
|
||||
| Architecture | GOARCH | File suffix | Instructions recognised |
|
||||
|--------------|-------------|--------------|---------------------------------------------|
|
||||
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
|
||||
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
|
||||
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
|
||||
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
|
||||
| ARM64 | `arm64` | `_arm64.s` | 645 + common opcodes |
|
||||
| RISC-V | `riscv64` | `_riscv64.s` | 992 + common opcodes |
|
||||
| LoongArch | `loong64` | `_loong64.s` | 808 + common opcodes |
|
||||
|
||||
"Common opcodes" are the instructions shared by every architecture (`RET`,
|
||||
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
|
||||
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
|
||||
...) that the assembler accepts as aliases. Regenerating the tables is one
|
||||
command (`just gen`) and requires only a Go installation; the committed output
|
||||
has no runtime dependency on the toolchain.
|
||||
...) that the assembler accepts as aliases. The tables are generated from
|
||||
the Go toolchain's own assembler source (`just gen` refreshes them), so
|
||||
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 every real-code GOROOT assembly
|
||||
file (the tree without testdata) assembling for every target its build
|
||||
admits: 250 of 250, 100 %. Over the whole tree including testdata the
|
||||
measure is 271 of 322 attemptable (84.2 %); files named for other Go ports
|
||||
are counted but never attempted, and `//go:build` constraints decide which
|
||||
targets attempt a file at all, exactly as the build does. The number moves
|
||||
with every release.
|
||||
|
||||
### Validation status
|
||||
|
||||
**Only amd64 is validated on real hardware.** The other three
|
||||
architectures are validated under qemu-user emulation, because the
|
||||
project owns no arm64, riscv64 or loong64 machine, and emulation is the
|
||||
only substitute available for the hardware. The distinction matters and
|
||||
is stated rather than implied: everything below is a claim about what has
|
||||
actually been executed.
|
||||
|
||||
| Layer | amd64 | arm64, riscv64, loong64 |
|
||||
|---|---|---|
|
||||
| Encoding: byte-for-byte against `go tool asm` | native hardware | native hardware (the toolchain cross-assembles any GOARCH on any host) |
|
||||
| Execution: JIT calls, ABI checks, differential fuzzing | native hardware | qemu-user emulation |
|
||||
| Debugger: ptrace tracing, breakpoints, watchpoints, coverage | native hardware | emulation cannot run ptrace; the layer compiles and its architecture-neutral units run under `go test ./...`, nothing more. FreeBSD (amd64, arm64) is in the same position: the port compiles behind the cross-build gate and its integration test is ready, but no FreeBSD machine has executed it |
|
||||
|
||||
Consequences, stated plainly. An emulator is a model of a CPU, not the
|
||||
CPU: instruction semantics are implemented in software and can differ
|
||||
from silicon in ways a test suite does not reveal. A kernel that passes
|
||||
under qemu-user is therefore not proven correct on real hardware, and a
|
||||
discrepancy found on real hardware is a defect in gasm, reported like any
|
||||
other. Encoding parity is the exception: the byte comparison against the
|
||||
toolchain runs on the host for every architecture, so no emulator stands
|
||||
between the claim and the evidence. The debugger is the weakest case: on
|
||||
the three emulated architectures its per-architecture ptrace code has
|
||||
been compiled and read, never executed. Its units run under
|
||||
`go test ./...`, which `just test` sweeps in a second invocation beside
|
||||
the coverage profile; the live ptrace sessions are skipped in short
|
||||
mode, so they run locally or in the dispatched suite, never on a push.
|
||||
|
||||
## The documentation goal
|
||||
|
||||
The toolkit is the primary goal. The secondary one is documentation: a
|
||||
specification of the Plan 9 assembly language and of the GOOBJ object
|
||||
format that is 100 % complete, detailed enough to implement against,
|
||||
and written to a professional standard. These are the two subjects this
|
||||
project works with every day, and they are the two for which no usable
|
||||
documentation exists.
|
||||
|
||||
Go documents the language on a single page, "A Quick Guide to Go's
|
||||
Assembler", which carries no section for loong64, one of the four
|
||||
architectures gasm supports, and covers a fraction of what each
|
||||
assembler accepts. What exists beyond it lives as comments inside the
|
||||
toolchain's internal source: per-architecture reference manuals for
|
||||
arm64, ppc64, riscv64 and loong64, written for the toolchain's own
|
||||
maintainers rather than for an outside reader, and none at all for
|
||||
amd64. GOOBJ fares worst of all. The format that `go build` consumes
|
||||
has no specification anywhere: it is described by a comment in an
|
||||
internal package, it is not a stable interface, and it can change with
|
||||
any toolchain release.
|
||||
|
||||
The gap is therefore filled the only way it can be filled: by reverse
|
||||
engineering the toolchain itself, the same work the encoders already
|
||||
perform. Most of the documentation can come from nowhere else, and it
|
||||
is written as that knowledge is produced during development. It is
|
||||
verified the way the code is verified: an encoding documented here is
|
||||
one that differential tests against `go tool asm` confirm
|
||||
byte-for-byte, and a format field documented here is one the linker
|
||||
demonstrably reads. The work has begun: [docs/GOOBJ.md](docs/GOOBJ.md)
|
||||
specifies the object file format completely, and
|
||||
[docs/asm/README.md](docs/asm/README.md) opens the language reference
|
||||
with its common core. The per-architecture pages follow.
|
||||
|
||||
## 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 for amd64, under
|
||||
emulation for the rest, per the validation status above) because the
|
||||
toolchain offers no ground truth to compare against. The gaps exist
|
||||
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 on amd64
|
||||
and arm64: the JIT's executable-memory mapping and the ptrace debugger
|
||||
layer are ported, the release carries the two FreeBSD binaries, and the
|
||||
debugger's live validation awaits a FreeBSD machine (the validation
|
||||
status states it). Other unix systems may follow those two.
|
||||
- **Four architectures, no more.** amd64, arm64, riscv64 and loong64.
|
||||
No others are planned.
|
||||
|
||||
## Install
|
||||
|
||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
|
||||
linux/loong64 are on the
|
||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
|
||||
From source (Go 1.27 or later):
|
||||
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64,
|
||||
linux/loong64, freebsd/amd64 and freebsd/arm64 are on the
|
||||
[releases page](https://sourcedock.dev/petrbalvin/gasm-sdk/releases).
|
||||
From source (Go 1.27.1):
|
||||
|
||||
```sh
|
||||
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
|
||||
go install sourcedock.dev/petrbalvin/gasm-sdk/cmd/gasm@latest
|
||||
```
|
||||
|
||||
Or from a repository checkout, with the development version stamped:
|
||||
Or from a repository checkout:
|
||||
|
||||
```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
|
||||
|
||||
```sh
|
||||
@@ -102,14 +274,18 @@ gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with argumen
|
||||
```sh
|
||||
gasm fmt # reformat every .s below here, like go fmt
|
||||
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 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 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 --fuzz k.s # differential fuzz vs the go tool asm build
|
||||
gasm debug --func name k.s # interactive debugger
|
||||
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
|
||||
gasm debug --func name --cover k.s # which labels did execution reach?
|
||||
gasm debug --func name --cover k.s # instruction and label coverage
|
||||
gasm diff a.s b.s # compare machine code byte-for-byte
|
||||
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
gasm profile k.s # show basic-block structure
|
||||
@@ -132,9 +308,9 @@ infers the target architecture from the file-name suffix
|
||||
## Development
|
||||
|
||||
```sh
|
||||
just install # download module dependencies
|
||||
just build # go vet + gofmt check, zero errors and zero warnings
|
||||
just test # full suite, race detector, 80 % coverage gate
|
||||
just build # compile, zero errors and zero warnings
|
||||
just test # the suite, no cache, the 80 % coverage floor
|
||||
just gates # build, fmt-check, vet, test, race: the definition of done
|
||||
just fmt # gofmt the tree
|
||||
just gen # regenerate the instruction tables from the Go toolchain
|
||||
```
|
||||
@@ -145,14 +321,19 @@ recipe.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||
- man pages: `just install-man` installs gasm(1) and one page per command
|
||||
except `version`, which is documented inside gasm(1) instead, into
|
||||
~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
|
||||
them
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/GOOBJ.md](docs/GOOBJ.md): the GOOBJ object file format specification
|
||||
- [docs/asm/](docs/asm/README.md): the Plan 9 assembly language reference
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||
- [docs/DECISIONS.md](docs/DECISIONS.md): deferred design decisions
|
||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||
|
||||
## Licence
|
||||
|
||||
BSD-3-Clause — see [LICENSE](LICENSE).
|
||||
BSD-3-Clause; see [LICENSE](LICENSE).
|
||||
|
||||
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# 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.35.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 fix ships without naming you: the project keeps no credits list, so the release
|
||||
notes, the changelog and the commits name no reporter.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- 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.
|
||||
+105
-7
@@ -5,9 +5,13 @@
|
||||
// toolchain's own assembler source. Go's Plan 9 assembler defines the exact,
|
||||
// complete set of mnemonics it accepts for each architecture in
|
||||
// $GOROOT/src/cmd/internal/obj/<arch>/anames.go; this tool extracts those
|
||||
// names so gasm-devkit supports every instruction the real assembler does,
|
||||
// names so gasm-sdk supports every instruction the real assembler does,
|
||||
// with no hand-maintained (and therefore inevitably incomplete) lists.
|
||||
//
|
||||
// The same data feeds the generated instruction appendices of the assembly
|
||||
// language reference, docs/asm/INSTRUCTIONS-<ARCH>.md, so that the reference
|
||||
// cannot drift from the tables it documents.
|
||||
//
|
||||
// Usage (via the justfile):
|
||||
//
|
||||
// just gen
|
||||
@@ -26,9 +30,12 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
)
|
||||
|
||||
// archDirs maps a gasm-devkit architecture name to its obj sub-directory.
|
||||
// archDirs maps a gasm-sdk architecture name to its obj sub-directory.
|
||||
var archDirs = []struct {
|
||||
arch string
|
||||
sub string
|
||||
@@ -39,11 +46,30 @@ var archDirs = []struct {
|
||||
{"loong64", "loong64"},
|
||||
}
|
||||
|
||||
// docPages maps an architecture to its generated appendix in the language
|
||||
// reference. The amd64 page carries a per-mnemonic encodability column,
|
||||
// decided by asm.Encodable, which mirrors the encoder's own dispatch; the
|
||||
// other targets have no single cheap predicate, so their pages carry the
|
||||
// inventory and point at the live measurement instead.
|
||||
var docPages = []struct {
|
||||
arch arch.Arch
|
||||
title string
|
||||
file string
|
||||
anames string
|
||||
encodable bool
|
||||
}{
|
||||
{arch.AMD64, "AMD64", "INSTRUCTIONS-AMD64.md", "cmd/internal/obj/x86/anames.go", true},
|
||||
{arch.ARM64, "ARM64", "INSTRUCTIONS-ARM64.md", "cmd/internal/obj/arm64/anames.go", false},
|
||||
{arch.RISCV, "RISC-V 64", "INSTRUCTIONS-RISCV64.md", "cmd/internal/obj/riscv/anames.go", false},
|
||||
{arch.LOONG64, "LoongArch 64", "INSTRUCTIONS-LOONG64.md", "cmd/internal/obj/loong64/anames.go", false},
|
||||
}
|
||||
|
||||
func main() {
|
||||
goroot := strings.TrimSpace(runGoEnvGOROOT())
|
||||
if goroot == "" {
|
||||
fatal("could not determine GOROOT")
|
||||
}
|
||||
version := strings.TrimSpace(runGoEnv("GOVERSION"))
|
||||
// The common opcodes shared by every architecture (RET, JMP, NOP, CALL,
|
||||
// TEXT, FUNCDATA, …) live in cmd/internal/obj/util.go.
|
||||
commonPath := filepath.Join(goroot, "src", "cmd", "internal", "obj", "util.go")
|
||||
@@ -57,16 +83,24 @@ func main() {
|
||||
}
|
||||
fmt.Printf("%-8s %4d instructions -> arch/common_gen.go\n", "common", len(common))
|
||||
|
||||
names := map[string][]string{}
|
||||
for _, a := range archDirs {
|
||||
path := filepath.Join(goroot, "src", "cmd", "internal", "obj", a.sub, "anames.go")
|
||||
names, err := extractInstrs(path)
|
||||
names[a.arch], err = extractInstrs(path)
|
||||
if err != nil {
|
||||
fatal("extract %s: %v", a.arch, err)
|
||||
}
|
||||
if err := writeGen(a.arch, a.sub, names); err != nil {
|
||||
if err := writeGen(a.arch, a.sub, names[a.arch]); err != nil {
|
||||
fatal("write %s: %v", a.arch, err)
|
||||
}
|
||||
fmt.Printf("%-8s %4d instructions -> arch/%s_gen.go\n", a.arch, len(names), a.arch)
|
||||
fmt.Printf("%-8s %4d instructions -> arch/%s_gen.go\n", a.arch, len(names[a.arch]), a.arch)
|
||||
}
|
||||
|
||||
for _, p := range docPages {
|
||||
if err := writeDocPage(p.arch, p.title, p.file, p.anames, version, p.encodable); err != nil {
|
||||
fatal("write %s: %v", p.file, err)
|
||||
}
|
||||
fmt.Printf("%-8s -> docs/asm/%s\n", p.arch, p.file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +119,7 @@ func filterCommon(names []string) []string {
|
||||
// writeCommon emits arch/common_gen.go.
|
||||
func writeCommon(names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/util.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
@@ -156,7 +190,7 @@ func stringLit(elt ast.Expr) string {
|
||||
// writeGen emits arch/<arch>_gen.go.
|
||||
func writeGen(arch, sub string, names []string) error {
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by gasm-devkit _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Code generated by gasm-sdk _gen; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)\n")
|
||||
@@ -172,6 +206,61 @@ func writeGen(arch, sub string, names []string) error {
|
||||
return os.WriteFile(filepath.Join("arch", arch+"_gen.go"), []byte(b.String()), 0o644)
|
||||
}
|
||||
|
||||
// writeDocPage emits docs/asm/<file>, the generated instruction appendix of
|
||||
// the language reference for one architecture: every mnemonic the toolchain
|
||||
// accepts, with the curated summary where the architecture table carries one
|
||||
// and, on amd64, a per-mnemonic encodability column.
|
||||
func writeDocPage(a arch.Arch, title, file, anames, version string, encodable bool) error {
|
||||
table := arch.ForArch(a)
|
||||
instrs := table.Instructions()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("# " + title + ": instruction inventory\n\n")
|
||||
b.WriteString("Generated by gasm-sdk's `_gen` from the Go toolchain's instruction table\n")
|
||||
b.WriteString("(`" + anames + "`, " + version + "); DO NOT EDIT. This page lists every mnemonic\n")
|
||||
b.WriteString("`go tool asm` accepts on this target, which is the upper bound of the\n")
|
||||
b.WriteString("language on it: a name absent here is not an instruction of the target,\n")
|
||||
b.WriteString("and a name present here may still be one gasm's encoder cannot emit yet.\n\n")
|
||||
|
||||
encodableCount := 0
|
||||
if encodable {
|
||||
b.WriteString("The `gasm encodes` column reports whether gasm's encoder can emit the\n")
|
||||
b.WriteString("mnemonic today; the gap is the encoder backlog, measured live by\n")
|
||||
b.WriteString("`gasm audit-instructions`.\n\n")
|
||||
b.WriteString("| Mnemonic | gasm encodes | Notes |\n")
|
||||
b.WriteString("|---|---|---|\n")
|
||||
for _, in := range instrs {
|
||||
ok := asm.Encodable(in.Name)
|
||||
if ok {
|
||||
encodableCount++
|
||||
}
|
||||
b.WriteString("| `" + in.Name + "` | " + yesNo(ok) + " | " + in.Summary + " |\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
fmt.Fprintf(&b, "Recognised: %d mnemonics. gasm encodes: %d.\n", len(instrs), encodableCount)
|
||||
} else {
|
||||
b.WriteString("The inventory carries no per-mnemonic encoder column: on this target\n")
|
||||
b.WriteString("encodability is decided per operand shape, and the live measured\n")
|
||||
b.WriteString("coverage is reported by `gasm audit-instructions`.\n\n")
|
||||
b.WriteString("| Mnemonic | Notes |\n")
|
||||
b.WriteString("|---|---|\n")
|
||||
for _, in := range instrs {
|
||||
b.WriteString("| `" + in.Name + "` | " + in.Summary + " |\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
fmt.Fprintf(&b, "Recognised: %d mnemonics.\n", len(instrs))
|
||||
}
|
||||
return os.WriteFile(filepath.Join("docs", "asm", file), []byte(b.String()), 0o644)
|
||||
}
|
||||
|
||||
// yesNo renders a boolean as the word the appendix tables use.
|
||||
func yesNo(v bool) string {
|
||||
if v {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
|
||||
func runGoEnvGOROOT() string {
|
||||
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
@@ -180,6 +269,15 @@ func runGoEnvGOROOT() string {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// runGoEnv runs `go env` for a single variable.
|
||||
func runGoEnv(name string) string {
|
||||
out, err := exec.Command("go", "env", name).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "gen: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -70,6 +70,10 @@ func amd64Registers() []Register {
|
||||
for i := 0; i <= 7; i++ {
|
||||
add(fmt.Sprintf("K%d", i), Mask, "AVX-512 mask register")
|
||||
}
|
||||
// x87 stack registers (FMOVD and the other x87 moves).
|
||||
for i := 0; i <= 7; i++ {
|
||||
add(fmt.Sprintf("F%d", i), Float, "x87 stack register")
|
||||
}
|
||||
return regs
|
||||
}
|
||||
|
||||
|
||||
+1835
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The memory-operand mechanism: the base-plus-displacement validation and
|
||||
// the ModR/M, SIB and displacement byte choices. The expected words are
|
||||
// built on the VMOVSH memory-load template, whose rows the binutils-gdb
|
||||
// assembler testsuite quotes byte for byte; the rows marked GNU match the
|
||||
// listing bytes. Note on the quoted disp8 rows: binutils mainline encodes
|
||||
// EVEX displacements with the APX disp8*N scaling, so its source spellings
|
||||
// (254 for the m16 rows, 8128 for the m512 ones) are N times the plain SDM
|
||||
// displacement the disp8 bytes carry; the words here pin the bytes with the
|
||||
// plain displacement those bytes encode.
|
||||
func TestAmd64ExtMemoryEncoding(t *testing.T) {
|
||||
load := []byte{0x62, 0x05, 0x06, 0x00, 0x10, 0xC0}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
base int
|
||||
disp int64
|
||||
want string
|
||||
gnuSource string // the binutils source line the bytes serve, empty for a derived row
|
||||
}{
|
||||
{"zero displacement drops the disp bytes", 9, 0,
|
||||
"62457e081031", "vmovsh (%r9),%xmm30"},
|
||||
{"positive disp8", 1, 127,
|
||||
"62657e0810717f", "vmovsh 254(%rcx),%xmm30 (Disp8(7f) under the disp8*N scaling)"},
|
||||
{"negative disp8", 2, -128,
|
||||
"62657e08107280", "vmovsh -256(%rdx),%xmm30 (Disp8(80) under the disp8*N scaling)"},
|
||||
{"disp32 past the disp8 range", 2, 8128,
|
||||
"62657e0810b2c01f0000", ""},
|
||||
{"negative disp32", 13, -200,
|
||||
"62457e0810b538ffffff", ""},
|
||||
{"RSP base takes the SIB byte", 12, 0,
|
||||
"62457e08103424", ""},
|
||||
{"RSP base with a disp8", 12, 4,
|
||||
"62457e0810742404", ""},
|
||||
{"RBP base keeps a zero displacement explicit", 5, 0,
|
||||
"62657e08107500", ""},
|
||||
} {
|
||||
got := amd64EncodeMemory(load, 30, -1, tt.base, tt.disp)
|
||||
if hex.EncodeToString(got) != tt.want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64ExtMemoryRejects checks the validation around the memory operand:
|
||||
// the kinds and ranges the layer refuses before a byte is laid down.
|
||||
func TestAmd64ExtMemoryRejects(t *testing.T) {
|
||||
in := ExtInstr{Name: "TEST"}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
op ExtOperand
|
||||
quote string
|
||||
}{
|
||||
{"a register where the memory operand belongs", ExtXmm(3),
|
||||
"wants a memory operand"},
|
||||
{"base beyond r15", ExtMemory(16, 0),
|
||||
"outside 0-15"},
|
||||
{"base under r0", ExtMemory(-1, 0),
|
||||
"outside 0-15"},
|
||||
{"displacement past the signed 32-bit ceiling", ExtMemory(8, 1<<31),
|
||||
"outside the signed 32-bit range"},
|
||||
{"displacement past the signed 32-bit floor", ExtMemory(8, -1<<31-1),
|
||||
"outside the signed 32-bit range"},
|
||||
{"shift on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Imm: 4, Shift: 2, HasShift: true},
|
||||
"take none"},
|
||||
{"arrangement suffix on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Arr: ExtArrH},
|
||||
"arrangement"},
|
||||
{"predicate qualifier on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Qual: ExtQualZeroing},
|
||||
"predicate qualifier"},
|
||||
{"broadcast on an entry that takes none", ExtBroadcast(8, 0),
|
||||
"takes none"},
|
||||
} {
|
||||
_, _, err := in.amd64Memory(tt.op, 1)
|
||||
if err == nil {
|
||||
t.Errorf("%s: validation succeeded, want an error", tt.name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.quote) {
|
||||
t.Errorf("%s: error %q lacks %q", tt.name, err, tt.quote)
|
||||
}
|
||||
}
|
||||
// The broadcast spelling passes the flag gate on an entry that carries
|
||||
// Bcast and then meets the same base and displacement checks.
|
||||
bcast := ExtInstr{Name: "TEST", Bcast: true}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
op ExtOperand
|
||||
quote string
|
||||
}{
|
||||
{"broadcast base beyond r15", ExtOperand{Kind: ExtMem, Reg: 16, Imm: 0, Broadcast: true},
|
||||
"outside 0-15"},
|
||||
{"broadcast displacement past the signed 32-bit ceiling", ExtBroadcast(8, 1<<31),
|
||||
"outside the signed 32-bit range"},
|
||||
{"broadcast base under r0", ExtBroadcast(-1, 0),
|
||||
"outside 0-15"},
|
||||
} {
|
||||
if _, _, err := bcast.amd64Memory(tt.op, 1); err == nil {
|
||||
t.Errorf("%s: validation succeeded, want an error", tt.name)
|
||||
} else if !strings.Contains(err.Error(), tt.quote) {
|
||||
t.Errorf("%s: error %q lacks %q", tt.name, err, tt.quote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64ExtBroadcastEncoding pins the broadcast layer over the memory
|
||||
// encoding: EVEX.b, bit 4 of byte three, set on the VADDPH 512-bit template
|
||||
// while the ModR/M mod bits, the SIB byte and the displacement choices keep
|
||||
// the plain semantics amd64EncodeMemory chooses.
|
||||
func TestAmd64ExtBroadcastEncoding(t *testing.T) {
|
||||
add := []byte{0x62, 0x05, 0x04, 0x40, 0x58, 0xC0}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
base int
|
||||
disp int64
|
||||
want string
|
||||
}{
|
||||
{"zero displacement keeps the mod-00 form under the broadcast bit", 9, 0,
|
||||
"624514505831"},
|
||||
{"disp8 semantics unchanged", 1, 127,
|
||||
"6265145058717f"},
|
||||
{"disp32 semantics unchanged", 2, 8128,
|
||||
"6265145058b2c01f0000"},
|
||||
{"RBP keeps the forced displacement", 5, 0,
|
||||
"62651450587500"},
|
||||
{"RSP keeps the SIB byte", 12, 0,
|
||||
"62451450583424"},
|
||||
} {
|
||||
got := amd64EncodeBroadcast(add, 30, 29, tt.base, tt.disp)
|
||||
if hex.EncodeToString(got) != tt.want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64ExtScaledMemoryEncoding pins the SIB layer over the memory
|
||||
// encoding: the scale field, the index and the base in one byte, the r/m
|
||||
// field 100, EVEX.X clearing on an index above 7, and the ModR/M and
|
||||
// displacement choices keeping the plain semantics, RBP's forced
|
||||
// displacement included.
|
||||
func TestAmd64ExtScaledMemoryEncoding(t *testing.T) {
|
||||
add := []byte{0x62, 0x05, 0x04, 0x40, 0x58, 0xC0}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
base int
|
||||
index int
|
||||
scale int
|
||||
disp int64
|
||||
want string
|
||||
}{
|
||||
{"scale 1 encodes the scale field zero", 1, 2, 1, 0,
|
||||
"62651440583411"},
|
||||
{"scale 2", 1, 2, 2, 0,
|
||||
"62651440583451"},
|
||||
{"scale 4", 1, 2, 4, 0,
|
||||
"62651440583491"},
|
||||
{"scale 8", 1, 2, 8, 0,
|
||||
"626514405834d1"},
|
||||
{"an index above 7 clears EVEX.X", 1, 12, 2, 0,
|
||||
"62251440583461"},
|
||||
{"RBP base keeps the forced displacement", 5, 14, 8, 0,
|
||||
"622514405874f500"},
|
||||
{"RSP base takes the SIB byte with the index", 12, 3, 4, 0,
|
||||
"6245144058349c"},
|
||||
} {
|
||||
got := amd64EncodeScaledMemory(add, 30, 29, tt.base, tt.index, tt.scale, tt.disp)
|
||||
if hex.EncodeToString(got) != tt.want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64ExtMemoryVocabulary pins the names the shared layer gives the
|
||||
// memory operand and its two forms.
|
||||
func TestAmd64ExtMemoryVocabulary(t *testing.T) {
|
||||
if got := ExtMem.String(); got != "memory operand" {
|
||||
t.Errorf("ExtMem = %q, want %q", got, "memory operand")
|
||||
}
|
||||
if got := ExtFormAmdMemVec.String(); got != "memory into a vector" {
|
||||
t.Errorf("ExtFormAmdMemVec = %q, want %q", got, "memory into a vector")
|
||||
}
|
||||
if got := ExtFormAmdVecMem.String(); got != "a vector into memory" {
|
||||
t.Errorf("ExtFormAmdVecMem = %q, want %q", got, "a vector into memory")
|
||||
}
|
||||
for _, f := range []ExtForm{ExtFormAmdMemVec, ExtFormAmdVecMem} {
|
||||
if got := f.Arity(); got != 2 {
|
||||
t.Errorf("%s takes %d operands, want 2", f, got)
|
||||
}
|
||||
}
|
||||
if got := ExtMemory(9, 4096); got.Kind != ExtMem || got.Reg != 9 || got.Imm != 4096 {
|
||||
t.Errorf("ExtMemory(9, 4096) = %+v, want base 9 with displacement 4096", got)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/x86/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+34
-10
@@ -24,20 +24,41 @@ const (
|
||||
Unknown Arch = ""
|
||||
)
|
||||
|
||||
// FromFilename guesses the target architecture from a source file name. Go
|
||||
// assembly files conventionally carry a GOARCH suffix such as "_amd64.s",
|
||||
// "_arm64.s", "_riscv64.s" or "_loong64.s". It returns Unknown when no suffix
|
||||
// matches.
|
||||
// FromFilename guesses the target architecture from a source file name, by
|
||||
// go/build's goodOSArchFile rule: directories are stripped, the name is cut
|
||||
// at its first dot, everything before the first underscore is ignored and a
|
||||
// trailing _test segment is dropped; the name is per-architecture only when
|
||||
// its final underscore segment is an architecture name (foo_amd64.s,
|
||||
// sys_darwin_arm64.s, foo_amd64_test.s). Any other name narrows nothing:
|
||||
// not one whose last segment merely contains an architecture name
|
||||
// (x_loong64y.s), not one with no underscore at all (amd64.s, which
|
||||
// go/build also keeps generic), and not one differing in case
|
||||
// (foo_AMD64.s), because the segment match is case-sensitive as go/build's
|
||||
// is. A name carrying an operating system alone (foo_linux.s) narrows by
|
||||
// GOOS rather than architecture, and one carrying an architecture gasm
|
||||
// does not assemble for (vlop_arm.s) reports Unknown so the caller's own
|
||||
// port filter decides. It returns Unknown when the name narrows nothing.
|
||||
func FromFilename(name string) Arch {
|
||||
lower := strings.ToLower(name)
|
||||
switch {
|
||||
case strings.Contains(lower, "_amd64"):
|
||||
if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
||||
name = name[i+1:]
|
||||
}
|
||||
name, _, _ = strings.Cut(name, ".")
|
||||
i := strings.Index(name, "_")
|
||||
if i < 0 {
|
||||
return Unknown
|
||||
}
|
||||
segs := strings.Split(name[i:], "_")
|
||||
if last := len(segs) - 1; segs[last] == "test" {
|
||||
segs = segs[:last]
|
||||
}
|
||||
switch segs[len(segs)-1] {
|
||||
case "amd64":
|
||||
return AMD64
|
||||
case strings.Contains(lower, "_arm64"):
|
||||
case "arm64":
|
||||
return ARM64
|
||||
case strings.Contains(lower, "_riscv64"), strings.Contains(lower, "_riscv"):
|
||||
case "riscv64":
|
||||
return RISCV
|
||||
case strings.Contains(lower, "_loong64"), strings.Contains(lower, "_loong"):
|
||||
case "loong64":
|
||||
return LOONG64
|
||||
default:
|
||||
return Unknown
|
||||
@@ -55,6 +76,7 @@ const (
|
||||
Mask // AVX-512 mask register (K)
|
||||
Float // arm64 floating-point register (F)
|
||||
VecARM // arm64 SIMD/vector register (V)
|
||||
VecSIMD // architecture-neutral SIMD/vector register (LoongArch LSX/LASX)
|
||||
Special // architecture-special register
|
||||
)
|
||||
|
||||
@@ -73,6 +95,8 @@ func (c RegClass) String() string {
|
||||
return "float"
|
||||
case VecARM:
|
||||
return "vector (arm64)"
|
||||
case VecSIMD:
|
||||
return "vector"
|
||||
case Special:
|
||||
return "special"
|
||||
default:
|
||||
|
||||
+29
-2
@@ -7,13 +7,40 @@ import "testing"
|
||||
|
||||
func TestFromFilename(t *testing.T) {
|
||||
cases := map[string]Arch{
|
||||
// Positive: the final underscore segment is the architecture.
|
||||
"avx2_amd64.s": AMD64,
|
||||
"foo_arm64.s": ARM64,
|
||||
"portable.s": Unknown,
|
||||
"decode_ARM64.S": ARM64,
|
||||
"kernels_amd64.s": AMD64,
|
||||
"kernel_riscv64.s": RISCV,
|
||||
"kernel_loong64.s": LOONG64,
|
||||
"decode_arm64.S": ARM64, // the extension is cut with the first dot
|
||||
// OS before arch: the _<os>_<arch> pair form.
|
||||
"sys_darwin_arm64.s": ARM64,
|
||||
"rt0_linux_amd64.s": AMD64,
|
||||
// A trailing _test segment is dropped before the suffix rule.
|
||||
"foo_amd64_test.s": AMD64,
|
||||
// Trailing junk: a final segment that merely contains an
|
||||
// architecture name narrows nothing, exactly as go/build's
|
||||
// segment rule says.
|
||||
"x_loong64y.s": Unknown,
|
||||
"asm_amd64x.s": Unknown, // a real GOROOT name
|
||||
"asm_darwin_arm64_gc.s": Unknown, // the trailing _gc segment is junk
|
||||
"x_loong.s": Unknown,
|
||||
"foo_amd64x_test.s": Unknown,
|
||||
// Case sensitivity: the segment match is exact, as go/build's is.
|
||||
"foo_AMD64.s": Unknown,
|
||||
"decode_ARM64.S": Unknown,
|
||||
// No underscore at all: generic whatever the stem says.
|
||||
"amd64.s": Unknown,
|
||||
"portable.s": Unknown,
|
||||
// A GOOS-only name narrows by operating system, not architecture.
|
||||
"foo_linux.s": Unknown,
|
||||
// An architecture gasm does not assemble for: named, but Unknown.
|
||||
"vlop_arm.s": Unknown,
|
||||
"foo_riscv.s": Unknown,
|
||||
// Directories are stripped first, whatever dots they carry.
|
||||
"some/dir/kernel_loong64.s": LOONG64,
|
||||
"/a.b/x_amd64.s": AMD64,
|
||||
}
|
||||
for name, want := range cases {
|
||||
if got := FromFilename(name); got != want {
|
||||
|
||||
+30
-2
@@ -29,10 +29,11 @@ func arm64Registers() []Register {
|
||||
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++ {
|
||||
add(fmt.Sprintf("R%d", i), GPR, "64-bit general-purpose register")
|
||||
}
|
||||
add("R18_PLATFORM", GPR, "R18 under its toolchain-reserved Windows name (an alias of R18)")
|
||||
add("ZR", Special, "zero register (reads as 0)")
|
||||
add("SP", Special, "stack pointer")
|
||||
add("LR", Special, "link register (alias of R30)")
|
||||
@@ -145,11 +146,38 @@ func arm64Curated() []Instr {
|
||||
for _, op := range []string{
|
||||
"LDAXR", "LDAXRB", "LDAXRH", "LDAXRW", "STXR", "STXRB", "STXRH", "STXRW",
|
||||
"LDAR", "LDARB", "LDARH", "LDARW", "STLR", "STLRB", "STLRH", "STLRW",
|
||||
"LDADD", "LDCLR", "LDEOR", "LDSET", "SWP", "CAS", "CASAL", "CASL", "CASAL",
|
||||
"LDADD", "LDCLR", "LDEOR", "LDSET", "SWP", "CAS", "CASAL", "CASL",
|
||||
} {
|
||||
t = append(t, i(op, "Atomic memory operation"))
|
||||
}
|
||||
|
||||
// Register-pair loads and stores.
|
||||
for _, op := range []string{"LDP", "STP", "LDPW", "STPW", "FLDPD", "FSTPD"} {
|
||||
t = append(t, ic(op, "Register-pair load or store", 2, 2))
|
||||
}
|
||||
|
||||
// Cache maintenance and prefetch.
|
||||
t = append(t, i("DC", "Data cache maintenance"))
|
||||
t = append(t, i("PRFM", "Memory prefetch"))
|
||||
for _, op := range []string{"LDADDAL", "LDCLRAL", "LDORAL", "SWPAL"} {
|
||||
t = append(t, i(op, "Atomic memory operation with acquire and release semantics"))
|
||||
}
|
||||
|
||||
// Cryptographic extensions.
|
||||
for _, op := range []string{"AESE", "AESD", "AESMC", "AESIMC"} {
|
||||
t = append(t, i(op, "AES round"))
|
||||
}
|
||||
for _, op := range []string{
|
||||
"SHA1C", "SHA1P", "SHA1M", "SHA1H", "SHA1SU0", "SHA1SU1",
|
||||
"SHA256H", "SHA256H2", "SHA256SU0", "SHA256SU1",
|
||||
"SHA512H", "SHA512H2", "SHA512SU0", "SHA512SU1",
|
||||
} {
|
||||
t = append(t, i(op, "SHA round"))
|
||||
}
|
||||
for _, op := range []string{"VEOR3", "VBCAX", "VXAR", "VRAX1"} {
|
||||
t = append(t, i(op, "Three-way XOR / rotate crypto vector operation"))
|
||||
}
|
||||
|
||||
// Floating-point scalar.
|
||||
for _, op := range []string{
|
||||
"FADD", "FSUB", "FMUL", "FDIV", "FNEG", "FABS", "FSQRT", "FMIN", "FMAX",
|
||||
|
||||
+5294
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+108
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/arm64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
@@ -364,6 +364,8 @@ var arm64GeneratedInstrs = []string{
|
||||
"REVW",
|
||||
"ROR",
|
||||
"RORW",
|
||||
"RPRFM",
|
||||
"SB",
|
||||
"SBC",
|
||||
"SBCS",
|
||||
"SBCSW",
|
||||
@@ -477,23 +479,68 @@ var arm64GeneratedInstrs = []string{
|
||||
"UXTH",
|
||||
"UXTHW",
|
||||
"UXTW",
|
||||
"VABS",
|
||||
"VADD",
|
||||
"VADDP",
|
||||
"VADDV",
|
||||
"VAND",
|
||||
"VBCAX",
|
||||
"VBIC",
|
||||
"VBIF",
|
||||
"VBIT",
|
||||
"VBSL",
|
||||
"VCLS",
|
||||
"VCLZ",
|
||||
"VCMEQ",
|
||||
"VCMGE",
|
||||
"VCMGT",
|
||||
"VCMHI",
|
||||
"VCMHS",
|
||||
"VCMLE",
|
||||
"VCMLT",
|
||||
"VCMTST",
|
||||
"VCNT",
|
||||
"VDUP",
|
||||
"VEOR",
|
||||
"VEOR3",
|
||||
"VEXT",
|
||||
"VFABS",
|
||||
"VFADD",
|
||||
"VFADDP",
|
||||
"VFCMEQ",
|
||||
"VFCMGE",
|
||||
"VFCMGT",
|
||||
"VFCMLE",
|
||||
"VFCMLT",
|
||||
"VFCVTL",
|
||||
"VFCVTL2",
|
||||
"VFCVTN",
|
||||
"VFCVTN2",
|
||||
"VFCVTZS",
|
||||
"VFCVTZU",
|
||||
"VFDIV",
|
||||
"VFMAX",
|
||||
"VFMAXNM",
|
||||
"VFMAXNMP",
|
||||
"VFMAXNMV",
|
||||
"VFMAXP",
|
||||
"VFMAXV",
|
||||
"VFMIN",
|
||||
"VFMINNM",
|
||||
"VFMINNMP",
|
||||
"VFMINNMV",
|
||||
"VFMINP",
|
||||
"VFMINV",
|
||||
"VFMLA",
|
||||
"VFMLS",
|
||||
"VFMUL",
|
||||
"VFNEG",
|
||||
"VFRINTM",
|
||||
"VFRINTN",
|
||||
"VFRINTP",
|
||||
"VFRINTZ",
|
||||
"VFSQRT",
|
||||
"VFSUB",
|
||||
"VLD1",
|
||||
"VLD1R",
|
||||
"VLD2",
|
||||
@@ -502,11 +549,17 @@ var arm64GeneratedInstrs = []string{
|
||||
"VLD3R",
|
||||
"VLD4",
|
||||
"VLD4R",
|
||||
"VMLA",
|
||||
"VMLS",
|
||||
"VMOV",
|
||||
"VMOVD",
|
||||
"VMOVI",
|
||||
"VMOVQ",
|
||||
"VMOVS",
|
||||
"VMUL",
|
||||
"VNEG",
|
||||
"VNOT",
|
||||
"VORN",
|
||||
"VORR",
|
||||
"VPMULL",
|
||||
"VPMULL2",
|
||||
@@ -515,14 +568,47 @@ var arm64GeneratedInstrs = []string{
|
||||
"VREV16",
|
||||
"VREV32",
|
||||
"VREV64",
|
||||
"VSCVTF",
|
||||
"VSHADD",
|
||||
"VSHL",
|
||||
"VSHRN",
|
||||
"VSHRN2",
|
||||
"VSLI",
|
||||
"VSMAX",
|
||||
"VSMAXP",
|
||||
"VSMAXV",
|
||||
"VSMIN",
|
||||
"VSMINP",
|
||||
"VSMINV",
|
||||
"VSMLAL",
|
||||
"VSMLAL2",
|
||||
"VSMLSL",
|
||||
"VSMLSL2",
|
||||
"VSMULL",
|
||||
"VSMULL2",
|
||||
"VSQABS",
|
||||
"VSQADD",
|
||||
"VSQNEG",
|
||||
"VSQSHL",
|
||||
"VSQSUB",
|
||||
"VSQXTN",
|
||||
"VSQXTN2",
|
||||
"VSQXTUN",
|
||||
"VSQXTUN2",
|
||||
"VSRHADD",
|
||||
"VSRI",
|
||||
"VSRSHR",
|
||||
"VSSHL",
|
||||
"VSSHLL",
|
||||
"VSSHLL2",
|
||||
"VSSHR",
|
||||
"VST1",
|
||||
"VST2",
|
||||
"VST3",
|
||||
"VST4",
|
||||
"VSUB",
|
||||
"VSXTL",
|
||||
"VSXTL2",
|
||||
"VTBL",
|
||||
"VTBX",
|
||||
"VTRN1",
|
||||
@@ -530,8 +616,27 @@ var arm64GeneratedInstrs = []string{
|
||||
"VUADDLV",
|
||||
"VUADDW",
|
||||
"VUADDW2",
|
||||
"VUCVTF",
|
||||
"VUHADD",
|
||||
"VUMAX",
|
||||
"VUMAXP",
|
||||
"VUMAXV",
|
||||
"VUMIN",
|
||||
"VUMINP",
|
||||
"VUMINV",
|
||||
"VUMLAL",
|
||||
"VUMLAL2",
|
||||
"VUMLSL",
|
||||
"VUMLSL2",
|
||||
"VUMULL",
|
||||
"VUMULL2",
|
||||
"VUQADD",
|
||||
"VUQSHL",
|
||||
"VUQSUB",
|
||||
"VUQXTN",
|
||||
"VUQXTN2",
|
||||
"VURHADD",
|
||||
"VUSHL",
|
||||
"VUSHLL",
|
||||
"VUSHLL2",
|
||||
"VUSHR",
|
||||
@@ -541,6 +646,8 @@ var arm64GeneratedInstrs = []string{
|
||||
"VUZP1",
|
||||
"VUZP2",
|
||||
"VXAR",
|
||||
"VXTN",
|
||||
"VXTN2",
|
||||
"VZIP1",
|
||||
"VZIP2",
|
||||
"WFE",
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/util.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
||||
+2
-2
@@ -31,10 +31,10 @@ func loong64Registers() []Register {
|
||||
add(fmt.Sprintf("F%d", i), Float, "floating-point register")
|
||||
}
|
||||
for i := 0; i <= 31; i++ {
|
||||
add(fmt.Sprintf("V%d", i), VecARM, "LSX 128-bit vector register")
|
||||
add(fmt.Sprintf("V%d", i), VecSIMD, "LSX 128-bit vector register")
|
||||
}
|
||||
for i := 0; i <= 31; i++ {
|
||||
add(fmt.Sprintf("X%d", i), VecARM, "LASX 256-bit vector register")
|
||||
add(fmt.Sprintf("X%d", i), VecSIMD, "LASX 256-bit vector register")
|
||||
}
|
||||
return regs
|
||||
}
|
||||
|
||||
+10
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/loong64/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
@@ -152,6 +152,8 @@ var loong64GeneratedInstrs = []string{
|
||||
"FNMADDF",
|
||||
"FNMSUBD",
|
||||
"FNMSUBF",
|
||||
"FRINTD",
|
||||
"FRINTF",
|
||||
"FSCALEBD",
|
||||
"FSCALEBF",
|
||||
"FSEL",
|
||||
@@ -177,7 +179,10 @@ var loong64GeneratedInstrs = []string{
|
||||
"FTINTWF",
|
||||
"JIRL",
|
||||
"LL",
|
||||
"LLACQV",
|
||||
"LLACQW",
|
||||
"LLV",
|
||||
"LLW",
|
||||
"LU12IW",
|
||||
"LU32ID",
|
||||
"LU52ID",
|
||||
@@ -248,7 +253,11 @@ var loong64GeneratedInstrs = []string{
|
||||
"ROTR",
|
||||
"ROTRV",
|
||||
"SC",
|
||||
"SCQ",
|
||||
"SCRELV",
|
||||
"SCRELW",
|
||||
"SCV",
|
||||
"SCW",
|
||||
"SGT",
|
||||
"SGTU",
|
||||
"SLL",
|
||||
|
||||
+32
-1
@@ -1,4 +1,4 @@
|
||||
// Code generated by gasm-devkit _gen; DO NOT EDIT.
|
||||
// Code generated by gasm-sdk _gen; DO NOT EDIT.
|
||||
// Source: cmd/internal/obj/riscv/anames.go from the Go toolchain.
|
||||
//
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
@@ -81,6 +81,9 @@ var riscvGeneratedInstrs = []string{
|
||||
"CLD",
|
||||
"CLDSP",
|
||||
"CLI",
|
||||
"CLMUL",
|
||||
"CLMULH",
|
||||
"CLMULR",
|
||||
"CLUI",
|
||||
"CLW",
|
||||
"CLWSP",
|
||||
@@ -95,13 +98,20 @@ var riscvGeneratedInstrs = []string{
|
||||
"CSDSP",
|
||||
"CSLLI",
|
||||
"CSRAI",
|
||||
"CSRC",
|
||||
"CSRCI",
|
||||
"CSRLI",
|
||||
"CSRR",
|
||||
"CSRRC",
|
||||
"CSRRCI",
|
||||
"CSRRS",
|
||||
"CSRRSI",
|
||||
"CSRRW",
|
||||
"CSRRWI",
|
||||
"CSRS",
|
||||
"CSRSI",
|
||||
"CSRW",
|
||||
"CSRWI",
|
||||
"CSUB",
|
||||
"CSUBW",
|
||||
"CSW",
|
||||
@@ -259,6 +269,7 @@ var riscvGeneratedInstrs = []string{
|
||||
"ORCB",
|
||||
"ORI",
|
||||
"ORN",
|
||||
"PAUSE",
|
||||
"RDCYCLE",
|
||||
"RDINSTRET",
|
||||
"RDTIME",
|
||||
@@ -322,6 +333,8 @@ var riscvGeneratedInstrs = []string{
|
||||
"VADDVI",
|
||||
"VADDVV",
|
||||
"VADDVX",
|
||||
"VANDNVV",
|
||||
"VANDNVX",
|
||||
"VANDVI",
|
||||
"VANDVV",
|
||||
"VANDVX",
|
||||
@@ -329,8 +342,17 @@ var riscvGeneratedInstrs = []string{
|
||||
"VASUBUVX",
|
||||
"VASUBVV",
|
||||
"VASUBVX",
|
||||
"VBREV8V",
|
||||
"VBREVV",
|
||||
"VCLMULHVV",
|
||||
"VCLMULHVX",
|
||||
"VCLMULVV",
|
||||
"VCLMULVX",
|
||||
"VCLZV",
|
||||
"VCOMPRESSVM",
|
||||
"VCPOPM",
|
||||
"VCPOPV",
|
||||
"VCTZV",
|
||||
"VDIVUVV",
|
||||
"VDIVUVX",
|
||||
"VDIVVV",
|
||||
@@ -743,10 +765,16 @@ var riscvGeneratedInstrs = []string{
|
||||
"VREMUVX",
|
||||
"VREMVV",
|
||||
"VREMVX",
|
||||
"VREV8V",
|
||||
"VRGATHEREI16VV",
|
||||
"VRGATHERVI",
|
||||
"VRGATHERVV",
|
||||
"VRGATHERVX",
|
||||
"VROLVV",
|
||||
"VROLVX",
|
||||
"VRORVI",
|
||||
"VRORVV",
|
||||
"VRORVX",
|
||||
"VRSUBVI",
|
||||
"VRSUBVX",
|
||||
"VS1RV",
|
||||
@@ -950,6 +978,9 @@ var riscvGeneratedInstrs = []string{
|
||||
"VWMULVX",
|
||||
"VWREDSUMUVS",
|
||||
"VWREDSUMVS",
|
||||
"VWSLLVI",
|
||||
"VWSLLVV",
|
||||
"VWSLLVX",
|
||||
"VWSUBUVV",
|
||||
"VWSUBUVX",
|
||||
"VWSUBUWV",
|
||||
|
||||
+158
-39
@@ -4,13 +4,14 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
||||
@@ -61,6 +62,62 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectAARCH64PairReloc pins the ADRP-pair relocation shape against
|
||||
// the toolchain's own object for the same source: exactly one R_ADDRARM64
|
||||
// of Siz 8 at the ADRP word (cmd/internal/obj/arm64/asm7.go adds a single
|
||||
// Siz-8 relocation per pair and the linker patches both instructions from
|
||||
// it). gasm's assembler records the ADRP+ADD form as two word relocs; the
|
||||
// emitter must coalesce them, not emit two Siz-4 records.
|
||||
func TestGOObjectAARCH64PairReloc(t *testing.T) {
|
||||
f, errs := parser.Parse("gv_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·getv(SB), NOSPLIT, $0-8
|
||||
MOVD $v<>(SB), R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL v<>(SB), RODATA, $8
|
||||
DATA v<>+0(SB)/8, $7
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.GOObjectAARCH64("main", "gv_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
relocs := v.blk(blkReloc)
|
||||
le := binary.LittleEndian
|
||||
// Two DWARF relocs on the lines/DIE symbols, then the code's one pair
|
||||
// relocation.
|
||||
if len(relocs) != 3*23 {
|
||||
t.Fatalf("relocs = %d bytes, want three entries", len(relocs))
|
||||
}
|
||||
cr := relocs[2*23:]
|
||||
if off := int32(le.Uint32(cr[0:])); off != 0 {
|
||||
t.Errorf("pair reloc off = %d, want 0 (the ADRP word)", off)
|
||||
}
|
||||
if siz := cr[4]; siz != 8 {
|
||||
t.Errorf("pair reloc siz = %d, want 8", siz)
|
||||
}
|
||||
if typ := le.Uint16(cr[5:]); typ != relocArm64Addr {
|
||||
t.Errorf("pair reloc type = %d, want %d (R_ADDRARM64)", typ, relocArm64Addr)
|
||||
}
|
||||
if pkg := le.Uint32(cr[15:]); pkg != pkgIdxSelf {
|
||||
t.Errorf("pair reloc PkgIdx = %#x, want pkgIdxSelf", pkg)
|
||||
}
|
||||
// The GLOBL is the first package definition.
|
||||
if sym := le.Uint32(cr[19:]); sym != 0 {
|
||||
t.Errorf("pair reloc SymIdx = %d, want 0 (the GLOBL definition)", sym)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectAARCH64Link does an end-to-end link test: it cross-compiles a
|
||||
// Go program for arm64, substitutes the gasm-produced object into the package
|
||||
// archive, re-links with cmd/link, and verifies the symbol appears in the
|
||||
@@ -79,6 +136,14 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·getv(SB), NOSPLIT, $0-8
|
||||
MOVD $v<>(SB), R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL v<>(SB), RODATA, $8
|
||||
DATA v<>+0(SB)/8, $7
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_arm64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -86,11 +151,15 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
mainSrc := `package main
|
||||
|
||||
func add(a, b int64) int64
|
||||
func getv() *int64
|
||||
|
||||
func main() {
|
||||
if add(20, 22) != 42 {
|
||||
panic("bad add")
|
||||
}
|
||||
if getv() == nil {
|
||||
panic("bad getv")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
@@ -109,26 +178,10 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_arm64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
st := parseBuildLog(t, buildLog, "main_arm64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
// Expand $WORK in the object path.
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
|
||||
// Read the toolchain-produced object and assemble the same source with gasm.
|
||||
// Assemble the same source with gasm and substitute the object.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -141,30 +194,16 @@ func main() {
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("a64link", "main_arm64.s")
|
||||
// The package path is "main", the prefix the Go code's references carry.
|
||||
gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
|
||||
// Replace the toolchain-produced object with gasm's.
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
|
||||
// Re-link.
|
||||
if linkLine == "" {
|
||||
t.Skip("could not find link command in build log")
|
||||
}
|
||||
// Expand $WORK in the link command.
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
linkCmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"),
|
||||
gasmObj, "GOARCH=arm64")
|
||||
|
||||
// Verify the binary exists and contains the symbol.
|
||||
binPath := filepath.Join(dir, "prog")
|
||||
binPath := filepath.Join(dir, "prog2")
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
t.Fatalf("binary not found: %v", err)
|
||||
}
|
||||
@@ -176,3 +215,83 @@ func main() {
|
||||
t.Error("binary does not contain expected symbol")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectAARCH64DataSymbolLink does for symbol-valued DATA fields what
|
||||
// the rt0 files do ("DATA _rt0…lib+0(SB)/8, $_rt0…lib(SB)"): the gasm object
|
||||
// carries an R_ADDR against the file's own TEXT symbol, the toolchain links
|
||||
// it, and the binary is checked for the symbol (no arm64 host to run it).
|
||||
func TestGOObjectAARCH64DataSymbolLink(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
GLOBL entry(SB), NOPTR, $8
|
||||
DATA entry+0(SB)/8, $·keepme(SB)
|
||||
|
||||
TEXT ·keepme(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
|
||||
TEXT ·entryptr(SB), NOSPLIT, $0-8
|
||||
MOVD entry+0(SB), R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_arm64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func keepme()
|
||||
func entryptr() uintptr
|
||||
|
||||
func main() {
|
||||
if entryptr() == 0 {
|
||||
panic("the entry word is empty")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module a64dlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
st := parseBuildLog(t, buildLog, "main_arm64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse("main_arm64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"),
|
||||
gasmObj, "GOARCH=arm64")
|
||||
binData, err := os.ReadFile(filepath.Join(dir, "prog2"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(binData), "keepme") {
|
||||
t.Error("binary does not contain the keepme symbol")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The assembler's side of the amd64 extension layer: this file turns a parsed
|
||||
// amd64 statement into the operand form arch.ExtInstr.Encode consumes and
|
||||
// routes statements only the layer can encode through the registry. It sits
|
||||
// beside the main amd64 encoders, never inside them: the generated table, the
|
||||
// legacy SSE paths and the VEX and EVEX mechanisms are untouched, and a
|
||||
// statement reaches this file only when the mnemonic is registered in the
|
||||
// extension layer and the scalar paths cannot encode it.
|
||||
//
|
||||
// The spellings are the layer's own Plan 9 forms, the ones its metadata
|
||||
// documents: the vector registers carry the house names X0, Y0 and Z0 (the
|
||||
// EVEX 128, 256 and 512-bit classes, registers 16 to 31 included), the general
|
||||
// registers the width their spelling fixes (RAX through R15, EAX through EDI
|
||||
// and R8D through R15D), the opmask registers K0 through K7, and the memory
|
||||
// operand the base-relative form off(base) with the optional scaled index
|
||||
// off(base)(index*scale) the SIB byte carries. The decorations ride the
|
||||
// operand in braces: the write mask {k1} through {k7} and zeroing {z} on the
|
||||
// destination, the {1toN} broadcast on the memory source, and {sae} and
|
||||
// {rn-sae} through {rz-sae} beside the rounding-capable destinations. The
|
||||
// imm8-control forms take their control byte as the leading $ immediate the
|
||||
// reference listings write first.
|
||||
//
|
||||
// The Feature field stays metadata at assembly time: the assembler has no CPU,
|
||||
// the toolchain does not gate assembly on CPU features, and every registered
|
||||
// feature assembles, the behaviour the arm64 wiring established
|
||||
// (asm/arm64_ext.go). The field remains for the linter and the listing.
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// amd64ExtStatement converts one instruction's operands into the extended
|
||||
// layer's operand form. pinned reports that the statement belongs to the
|
||||
// layer: the mnemonic is registered in the amd64 registry and the scalar
|
||||
// paths cannot encode it. A pinned statement can only encode through the
|
||||
// layer, so every operand is read here and its diagnostic replaces whatever
|
||||
// the scalar paths would have said about operands they cannot read; err is
|
||||
// non-nil for a pinned statement whose operands the layer refuses, and
|
||||
// extops is complete only when err is nil. Unpinned means the statement is
|
||||
// nobody's: the caller falls through to the ordinary amd64 encoders, which
|
||||
// keep their exact behaviour for every statement they knew before.
|
||||
func amd64ExtStatement(mnem string, ops []*ast.Operand) (extops []arch.ExtOperand, pinned bool, err error) {
|
||||
if _, ok := LookupExtension(arch.AMD64, mnem); !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
if Encodable(mnem) {
|
||||
// A mnemonic the main encoder knows is never the layer's, whatever
|
||||
// the registry carries: the scalar paths keep the statement. No
|
||||
// registered mnemonic trips this today (the layer is sealed by
|
||||
// test), but the guard keeps the fall-through promise exact should
|
||||
// the toolchain ever learn one of these names.
|
||||
return nil, false, nil
|
||||
}
|
||||
out := make([]arch.ExtOperand, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
ext, convErr := amd64ExtOperand(mnem, op, i+1)
|
||||
if convErr != nil {
|
||||
return nil, true, convErr
|
||||
}
|
||||
out = append(out, ext)
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
|
||||
// EncodeAmd64Statement runs one parsed amd64 statement through the layer
|
||||
// exactly as the assembler does: the operands convert the amd64ExtOperand way
|
||||
// and the mnemonic resolves and encodes through the registry. pinned reports
|
||||
// that the statement belongs to the layer alone (the mnemonic is registered
|
||||
// and the scalar paths cannot encode it); err is the layer's own refusal of
|
||||
// the operands, the same text the assembler prints, so the linter surfaces
|
||||
// one diagnostic where assembly would fail. A statement the scalar paths own
|
||||
// returns pinned false, with nothing to report.
|
||||
func EncodeAmd64Statement(mnem string, ops []*ast.Operand) (code []byte, pinned bool, err error) {
|
||||
extops, pinned, err := amd64ExtStatement(mnem, ops)
|
||||
if !pinned || err != nil {
|
||||
return nil, pinned, err
|
||||
}
|
||||
code, err = EncodeExtension(arch.AMD64, mnem, extops...)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
return code, true, nil
|
||||
}
|
||||
|
||||
// amd64ExtOperand converts one parsed operand into the layer's form: a $ immediate,
|
||||
// a vector, general or opmask register, or a base-relative memory operand, each
|
||||
// with the brace decorations the spelling carries.
|
||||
func amd64ExtOperand(mnem string, op *ast.Operand, pos int) (arch.ExtOperand, error) {
|
||||
if op.Kind == ast.OpImmediate {
|
||||
return amd64ExtImmediate(mnem, op, pos)
|
||||
}
|
||||
body, dec, err := amd64ExtDecorations(mnem, op, pos)
|
||||
if err != nil {
|
||||
return arch.ExtOperand{}, err
|
||||
}
|
||||
if strings.ContainsRune(body, '(') {
|
||||
ext, ok := amd64ExtMemory(mnem, op, pos)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, fmt.Errorf("%s: operand %d (%s) is not an extended-layer operand: want a base-relative memory operand, off(base)(index*scale) shape", mnem, pos, op.Raw)
|
||||
}
|
||||
ext.Broadcast = dec.broadcast
|
||||
if dec.hasMask {
|
||||
ext.Mask, ext.HasMask = dec.mask, true
|
||||
}
|
||||
ext.Zeroing = dec.zeroing
|
||||
ext.Round = dec.round
|
||||
return ext, nil
|
||||
}
|
||||
ext, ok := amd64ExtRegister(mnem, op, pos, body)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, fmt.Errorf("%s: operand %d (%s) is not an extended-layer operand: want a vector, general or opmask register, a base-relative memory operand or an immediate", mnem, pos, op.Raw)
|
||||
}
|
||||
if dec.hasMask {
|
||||
ext.Mask, ext.HasMask = dec.mask, true
|
||||
}
|
||||
ext.Zeroing = dec.zeroing
|
||||
ext.Round = dec.round
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
// amd64ExtImmediate converts a $ immediate into the layer's form. The
|
||||
// parser folds a parenthesised constant expression in full and reads a bare
|
||||
// literal greedily, dropping any trailing operator tokens: $255<<8 parses as
|
||||
// 255 with the shift silently gone. Encoding that silent prefix would
|
||||
// assemble what the text did not say, so an unparenthesised immediate is
|
||||
// accepted only when its whole text reads back as one integer carrying the
|
||||
// parser's value.
|
||||
func amd64ExtImmediate(mnem string, op *ast.Operand, pos int) (arch.ExtOperand, error) {
|
||||
if !op.Imm.HasVal {
|
||||
return arch.ExtOperand{}, fmt.Errorf("%s: operand %d (%s) is not an immediate the layer can read", mnem, pos, op.Raw)
|
||||
}
|
||||
text := strings.Join(strings.Fields(strings.TrimPrefix(op.Raw, "$")), "")
|
||||
if !strings.HasPrefix(text, "(") {
|
||||
if _, parseErr := strconv.ParseInt(text, 0, 64); parseErr != nil {
|
||||
return arch.ExtOperand{}, fmt.Errorf("%s: operand %d (%s) is not an immediate the layer can read", mnem, pos, op.Raw)
|
||||
}
|
||||
}
|
||||
v := op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtImm, Imm: v}, nil
|
||||
}
|
||||
|
||||
// amd64ExtRegister parses a register operand off a normalised operand body:
|
||||
// the vector classes X0-X31, Y0-Y31 and Z0-Z31, the width-fixed general
|
||||
// spellings RAX through R15 and EAX through R15D, and the opmask registers
|
||||
// K0-K7. The register ranges are left to the encoding, whose diagnostics
|
||||
// name them.
|
||||
func amd64ExtRegister(mnem string, op *ast.Operand, pos int, body string) (arch.ExtOperand, bool) {
|
||||
if body == "" {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
r, ok := ParseReg(body)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
switch {
|
||||
case r.mask:
|
||||
return arch.ExtOperand{Kind: arch.ExtKReg, Reg: r.idx}, true
|
||||
case r.isVec():
|
||||
kind := arch.ExtXMM
|
||||
switch r.size {
|
||||
case 32:
|
||||
kind = arch.ExtYMM
|
||||
case 64:
|
||||
kind = arch.ExtZMM
|
||||
}
|
||||
return arch.ExtOperand{Kind: kind, Reg: r.idx}, true
|
||||
case r.size == 8:
|
||||
return arch.ExtOperand{Kind: arch.ExtR64, Reg: r.idx}, true
|
||||
case r.size == 4:
|
||||
return arch.ExtOperand{Kind: arch.ExtR32, Reg: r.idx}, true
|
||||
}
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
|
||||
// amd64ExtMemory parses a base-relative memory operand off the parsed
|
||||
// address: off(base) and off(base)(index*scale), the SIB shapes the layer's
|
||||
// entries carry. The base and the index are general registers spelled in any
|
||||
// width the house names offer, the displacement the leading signed term, and
|
||||
// a group whose scale is not written scales by one, the choice the main
|
||||
// amd64 paths make for the same spelling. Vector, opmask and segment
|
||||
// registers are refused as base and index, and so is every frame form: the
|
||||
// layer's memory operand is hardware addressing alone.
|
||||
func amd64ExtMemory(mnem string, op *ast.Operand, pos int) (arch.ExtOperand, bool) {
|
||||
a := op.Addr
|
||||
if a.Range != nil || a.Base == "" {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
if a.Sym != nil && a.Sym.Pseudo != "" {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
base, ok := amd64ExtGprNumber(a.Base)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
ext := arch.ExtOperand{Kind: arch.ExtMem, Reg: base, Imm: a.Offset}
|
||||
if a.Index != "" {
|
||||
index, ok := amd64ExtGprNumber(a.Index)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
scale := a.Scale
|
||||
if scale == 0 {
|
||||
scale = 1
|
||||
}
|
||||
ext.Index, ext.Scale, ext.HasIndex = index, scale, true
|
||||
}
|
||||
return ext, true
|
||||
}
|
||||
|
||||
// amd64ExtGprNumber resolves one general-register spelling to its number:
|
||||
// whatever the register table carries for indices 0-15, the vector, opmask,
|
||||
// x87, MMX, segment and control-debug classes refused, so a vector register
|
||||
// in a base or index position names itself rather than encoding as its
|
||||
// same-numbered general register.
|
||||
func amd64ExtGprNumber(name string) (int, bool) {
|
||||
r, ok := ParseReg(name)
|
||||
if !ok || r.mask || r.fp || r.mmx || r.seg != 0 || r.ctl != 0 || r.size > 8 {
|
||||
return 0, false
|
||||
}
|
||||
return r.idx, true
|
||||
}
|
||||
|
||||
// amd64ExtDecorations splits the brace decorations off a normalised operand
|
||||
// text and returns the body before the first brace and the decorations they
|
||||
// spell: the write mask {k1} through {k7}, zeroing {z}, the {1toN} broadcast
|
||||
// and the rounding controls {sae} and {rn-sae} through {rz-sae}, matched
|
||||
// case-insensitively the way the register spellings are. The mask, zeroing
|
||||
// and rounding fields land on the operand the conversion builds; whether the
|
||||
// position takes them is the encoding's judgement, whose diagnostics name the
|
||||
// entry. The broadcast factor N is checked as a number and otherwise left to
|
||||
// the entry: the layer's model carries the spelling, not the lane count.
|
||||
func amd64ExtDecorations(mnem string, op *ast.Operand, pos int) (body string, dec amd64ExtDecor, err error) {
|
||||
compact := strings.Join(strings.Fields(op.Raw), "")
|
||||
i := strings.IndexByte(compact, '{')
|
||||
if i < 0 {
|
||||
return compact, dec, nil
|
||||
}
|
||||
body = compact[:i]
|
||||
for i < len(compact) {
|
||||
if compact[i] != '{' {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s): text between brace decorations", mnem, pos, op.Raw)
|
||||
}
|
||||
end := strings.IndexByte(compact[i:], '}')
|
||||
if end < 0 {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s): brace decoration without a closing brace", mnem, pos, op.Raw)
|
||||
}
|
||||
content := strings.ToUpper(compact[i+1 : i+end])
|
||||
switch {
|
||||
case content == "Z":
|
||||
if dec.zeroing {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two zeroing decorations", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.zeroing = true
|
||||
case content == "SAE":
|
||||
if dec.round != arch.ExtRoundNone {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two rounding controls", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.round = arch.ExtRoundSAE
|
||||
case content == "RN-SAE":
|
||||
if dec.round != arch.ExtRoundNone {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two rounding controls", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.round = arch.ExtRoundNearest
|
||||
case content == "RD-SAE":
|
||||
if dec.round != arch.ExtRoundNone {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two rounding controls", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.round = arch.ExtRoundDown
|
||||
case content == "RU-SAE":
|
||||
if dec.round != arch.ExtRoundNone {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two rounding controls", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.round = arch.ExtRoundUp
|
||||
case content == "RZ-SAE":
|
||||
if dec.round != arch.ExtRoundNone {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two rounding controls", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.round = arch.ExtRoundTruncate
|
||||
case strings.HasPrefix(content, "K") && content != "K":
|
||||
n, convErr := strconv.Atoi(content[1:])
|
||||
if convErr != nil || n < 0 {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s): %q is not a mask decoration, want {k1} through {k7}", mnem, pos, op.Raw, content)
|
||||
}
|
||||
if dec.hasMask {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s) carries two write masks", mnem, pos, op.Raw)
|
||||
}
|
||||
dec.mask, dec.hasMask = n, true
|
||||
case strings.HasPrefix(content, "1TO"):
|
||||
if _, convErr := strconv.Atoi(content[3:]); convErr != nil {
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s): %q is not a broadcast decoration, want {1toN}", mnem, pos, op.Raw, content)
|
||||
}
|
||||
dec.broadcast = true
|
||||
default:
|
||||
return "", dec, fmt.Errorf("%s: operand %d (%s): {%s} is not a decoration the layer reads: want {k1} through {k7}, {z}, {1toN}, {sae} or {rn-sae} through {rz-sae}", mnem, pos, op.Raw, content)
|
||||
}
|
||||
i += end + 1
|
||||
}
|
||||
return body, dec, nil
|
||||
}
|
||||
|
||||
// amd64ExtDecor carries the brace decorations one operand's spelling names.
|
||||
type amd64ExtDecor struct {
|
||||
mask int
|
||||
hasMask bool
|
||||
zeroing bool
|
||||
broadcast bool
|
||||
round arch.ExtRounding
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// assembleAmd64ExtBody parses src, assembles it for amd64 and returns the
|
||||
// first function's body. Every statement must encode: a failure is the
|
||||
// test's.
|
||||
func assembleAmd64ExtBody(t *testing.T, src string) []byte {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("ext_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("got %d functions, want 1", len(img.Funcs))
|
||||
}
|
||||
return img.Code[img.Funcs[0].Offset:][:img.Funcs[0].Size]
|
||||
}
|
||||
|
||||
// assembleAmd64ExtError parses and assembles src and returns the assembler's
|
||||
// error text.
|
||||
func assembleAmd64ExtError(t *testing.T, src string) string {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("ext_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
_, err := AssembleFile(f)
|
||||
if err == nil {
|
||||
t.Fatal("assembled, want an error")
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
const amd64ExtProbeHead = "#include \"textflag.h\"\nTEXT ·t(SB), NOSPLIT, $0\n"
|
||||
|
||||
// amd64ExtProbe assembles one statement alone and returns the function body:
|
||||
// exactly the statement's bytes, no trailing RET.
|
||||
func amd64ExtProbe(t *testing.T, stmt string) []byte {
|
||||
t.Helper()
|
||||
return assembleAmd64ExtBody(t, amd64ExtProbeHead+"\t"+stmt+"\n")
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionGolden drives the wired layer through the full
|
||||
// assembler: text in, machine bytes out. One statement per family, the
|
||||
// decorations the layer spells beside them, and the memory mechanism's
|
||||
// canonical choices; each want is the byte string the registry's golden
|
||||
// vectors in arch/amd64_ext_test.go and arch/amd64_ext_mem_test.go already
|
||||
// pin, so these prove the text-to-bytes path lands on the same encoding the
|
||||
// metadata layer produces.
|
||||
func TestAmd64AssembleExtensionGolden(t *testing.T) {
|
||||
tests := []struct {
|
||||
stmt string
|
||||
want string
|
||||
}{
|
||||
// AVX512-BF16: the two converts and the dot product, the write mask
|
||||
// riding the destination in braces.
|
||||
{"VCVTNE2PS2BF16 Z5, Z4, Z6", "62f2574872f4"},
|
||||
{"VCVTNE2PS2BF16 Z21, Z20, Z23", "62a2574072fc"},
|
||||
{"VCVTNEPS2BF16 Z5, Y6", "62f27e4872f5"},
|
||||
{"VCVTNEPS2BF16 Y5, X6{K6}", "62f27e2e72f5"},
|
||||
{"VDPBF16PS Z5, Z4, Z6{K5}", "62f2564d52f4"},
|
||||
// AVX512-VP2INTERSECT: sources first, the opmask destination last.
|
||||
{"VP2INTERSECTD Y2, Y1, K2", "62f26f2868d1"},
|
||||
// AVX512-FP16, the scalar core: high registers, memory and the
|
||||
// general-register pairs in both directions.
|
||||
{"VADDSH X29, X28, X30", "6205160058f4"},
|
||||
{"VMINSH X5, X4, X6{SAE}", "62f556185df4"},
|
||||
{"VMOVSH (R9), X30", "62457e081031"},
|
||||
{"VCVTSH2SI (R9), R12", "6255fe082d21"},
|
||||
{"VCVTSH2SI X30, EDX", "62957e082dd6"},
|
||||
{"VCVTSI2SH X29, R12, X30", "624596002af4"},
|
||||
{"VMOVW R12, X30", "62457d086ef4"},
|
||||
{"VCMPSH $0x7b, X29, X28, K5", "62931600c2ec7b"},
|
||||
{"VGETMANTSH $0x0b, X29, X28, X30", "6203140027f40b"},
|
||||
// The packed FP16 arithmetic and the embedded rounding: {sae} and the
|
||||
// four rounding modes compose with the write mask and zeroing.
|
||||
{"VADDPH Z5, Z4, Z6{RN-SAE}", "62f5541858f4"},
|
||||
{"VADDPH Z5, Z4, Z6{RD-SAE}", "62f5543858f4"},
|
||||
{"VADDPH Z5, Z4, Z6{RU-SAE}", "62f5545858f4"},
|
||||
{"VADDPH Z5, Z4, Z6{RZ-SAE}", "62f5547858f4"},
|
||||
{"VADDPH Z29, Z28, Z30{K7}{Z}", "620514c758f4"},
|
||||
{"VADDPH Z5, Z4, Z6{K7}{RZ-SAE}", "62f5547f58f4"},
|
||||
{"VADDPH Z28, (R9), Z30{K7}{Z}", "62451cc75831"},
|
||||
{"VSQRTPH Z29, Z30{K3}{Z}", "62057ccb51f5"},
|
||||
{"VFMADD132PH Z29, Z28, Z30", "6206154098f4"},
|
||||
// The packed conversions: full-width sources and the {1toN} broadcast
|
||||
// over the integer sources.
|
||||
{"VCVTPH2W Z5, Z6", "62f57d487df5"},
|
||||
{"VCVTPH2QQ X5, Z6{RZ-SAE}", "62f57d787bf5"},
|
||||
{"VCVTPH2PD X5, Z6", "62f57c485af5"},
|
||||
{"VCVTDQ2PH (R9){1TO8}, Y30", "62457c585b31"},
|
||||
{"VRNDSCALEPH $0x7b, Z5, Z6", "62f37c4808f57b"},
|
||||
// The complex families and the imm8 minimum-or-maximum pair.
|
||||
{"VFCMULCPH Z29, Z28, Z30", "62061740d6f4"},
|
||||
{"VFMADDCPH Z29, Z28, Z30", "6206164056f4"},
|
||||
{"VFCMADDCPH Z5, Z4, Z6{RN-SAE}", "62f6571856f4"},
|
||||
{"VFMADDCSH X29, (R9), X30", "624616005731"},
|
||||
{"VMINMAXPH $0x88, Z29, (R9), Z30", "62431440523188"},
|
||||
{"VMINMAXSH $0x88, X28, (R9), X29", "62431c00532988"},
|
||||
// AVX-VNNI-INT16: the VEX word, its 256-bit length and the memory
|
||||
// source.
|
||||
{"VPDPWSUD X2, X1, X3", "c4e26ad2d9"},
|
||||
{"VPDPWUSDS Y10, Y15, Y8", "c4422dd3c7"},
|
||||
{"VPDPWSUD X2, 127(RCX), X1", "c4e26ad2497f"},
|
||||
// The memory mechanism through the ext statements: the disp8 and
|
||||
// disp32 choices, the RSP-base SIB byte, RBP's forced displacement
|
||||
// and the scaled index with its EVEX.X handling.
|
||||
{"VMOVSH 127(RCX), X30", "62657e0810717f"},
|
||||
{"VMOVSH 8128(RDX), X30", "62657e0810b2c01f0000"},
|
||||
{"VMOVSH (R12), X30", "62457e08103424"},
|
||||
{"VMOVSH (RBP), X30", "62657e08107500"},
|
||||
{"VADDPH Z29, (RCX)(DX*1), Z30", "62651440583411"},
|
||||
{"VADDPH Z29, (RCX)(R12*2), Z30", "62251440583461"},
|
||||
{"VADDPH Z29, (RBP)(R14*8), Z30", "622514405874f500"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
body := amd64ExtProbe(t, tt.stmt)
|
||||
if got := hex.EncodeToString(body); got != tt.want {
|
||||
t.Errorf("%s:\n got %s\n want %s", tt.stmt, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionRegistryParity pins the layer's contract over a
|
||||
// wider slice: every statement here assembles to exactly the bytes
|
||||
// EncodeExtension produces for the model operands the statement spells, so
|
||||
// the front end and the registry cannot drift apart unnoticed.
|
||||
func TestAmd64AssembleExtensionRegistryParity(t *testing.T) {
|
||||
tests := []struct {
|
||||
stmt string
|
||||
mnem string
|
||||
ops []arch.ExtOperand
|
||||
}{
|
||||
{"VCVTNE2PS2BF16 Z5, Z4, Z6", "VCVTNE2PS2BF16",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtZmm(6)}},
|
||||
{"VCVTNEPS2BF16 Y5, X6{K6}", "VCVTNEPS2BF16",
|
||||
[]arch.ExtOperand{arch.ExtYmm(5), arch.ExtWriteMasked(arch.ExtXmm(6), 6, false)}},
|
||||
{"VDPBF16PS Z5, Z4, Z6{K5}", "VDPBF16PS",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtWriteMasked(arch.ExtZmm(6), 5, false)}},
|
||||
{"VP2INTERSECTD Y2, Y1, K2", "VP2INTERSECTD",
|
||||
[]arch.ExtOperand{arch.ExtYmm(2), arch.ExtYmm(1), arch.ExtMask(2)}},
|
||||
{"VADDSH X29, X28, X30", "VADDSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)}},
|
||||
{"VMINSH X5, X4, X6{SAE}", "VMINSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtRounded(arch.ExtXmm(6), arch.ExtRoundSAE)}},
|
||||
{"VCVTSI2SH X29, R12, X30", "VCVTSI2SH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtGpr64(12), arch.ExtXmm(30)}},
|
||||
{"VCVTSH2SI X30, EDX", "VCVTSH2SI",
|
||||
[]arch.ExtOperand{arch.ExtXmm(30), arch.ExtGpr32(2)}},
|
||||
{"VCMPSH $0x7b, X29, X28, K5", "VCMPSH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x7b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtMask(5)}},
|
||||
{"VGETMANTSH $0x0b, X29, X28, X30", "VGETMANTSH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x0b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)}},
|
||||
{"VADDPH Z5, Z4, Z6{K7}{RZ-SAE}", "VADDPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4),
|
||||
arch.ExtRounded(arch.ExtWriteMasked(arch.ExtZmm(6), 7, false), arch.ExtRoundTruncate)}},
|
||||
{"VADDPH Z28, (R9), Z30{K7}{Z}", "VADDPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(28), arch.ExtMemory(9, 0),
|
||||
arch.ExtWriteMasked(arch.ExtZmm(30), 7, true)}},
|
||||
{"VCVTDQ2PH (R9){1TO8}, Y30", "VCVTDQ2PH",
|
||||
[]arch.ExtOperand{arch.ExtBroadcast(9, 0), arch.ExtYmm(30)}},
|
||||
{"VFMADD132PH Z29, Z28, Z30", "VFMADD132PH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)}},
|
||||
{"VFMADD231PH Y5, (RCX){1TO8}, Y6", "VFMADD231PH",
|
||||
[]arch.ExtOperand{arch.ExtYmm(5), arch.ExtBroadcast(1, 0), arch.ExtYmm(6)}},
|
||||
{"VFCMADDCPH Z5, Z4, Z6{RN-SAE}", "VFCMADDCPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundNearest)}},
|
||||
{"VFMADDCSH X29, (R9), X30", "VFMADDCSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtMemory(9, 0), arch.ExtXmm(30)}},
|
||||
{"VMINMAXPH $0x88, Z29, (R9), Z30", "VMINMAXPH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x88), arch.ExtZmm(29), arch.ExtMemory(9, 0), arch.ExtZmm(30)}},
|
||||
{"VPDPWSUD X2, 127(RCX), X1", "VPDPWSUD",
|
||||
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtMemory(1, 127), arch.ExtXmm(1)}},
|
||||
{"VPDPWSUD X2, (RCX)(R12*2), X1", "VPDPWSUD",
|
||||
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtScaledMemory(1, 12, 2, 0), arch.ExtXmm(1)}},
|
||||
{"VMOVSH X30, (R9)", "VMOVSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(30), arch.ExtMemory(9, 0)}},
|
||||
{"VPDPWUSDS Y10, Y15, Y8", "VPDPWUSDS",
|
||||
[]arch.ExtOperand{arch.ExtYmm(10), arch.ExtYmm(15), arch.ExtYmm(8)}},
|
||||
{"VMOVSH 8128(RDX), X30", "VMOVSH",
|
||||
[]arch.ExtOperand{arch.ExtMemory(2, 8128), arch.ExtXmm(30)}},
|
||||
{"VADDPH Z29, (RCX)(R12*2), Z30", "VADDPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtScaledMemory(1, 12, 2, 0), arch.ExtZmm(30)}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
body := amd64ExtProbe(t, tt.stmt)
|
||||
want, err := EncodeExtension(arch.AMD64, tt.mnem, tt.ops...)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: registry encode: %v", tt.stmt, err)
|
||||
}
|
||||
if !bytes.Equal(body, want) {
|
||||
t.Errorf("%s:\n got %x\n want %x (the registry encoding)", tt.stmt, body, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionRefusals pins the diagnostics a pinned statement
|
||||
// gets from the layer instead of a scalar path's complaint, and the
|
||||
// conversion's own diagnostics for spellings the layer cannot read. Where
|
||||
// the Go toolchain knows a family member the shape of the message is its
|
||||
// rejection bar; the FP16, BF16, VP2INTERSECT and VNNI-INT16 families take
|
||||
// the GNU assembler's.
|
||||
func TestAmd64AssembleExtensionRefusals(t *testing.T) {
|
||||
tests := []struct {
|
||||
stmt string
|
||||
want string
|
||||
}{
|
||||
{"VADDPH Z33, Z1, Z2", "not an extended-layer operand"},
|
||||
{"VADDPH Z5, Z4, Z6{K0}", "outside the masking registers k1-k7"},
|
||||
{"VADDPH Z5, Z4, Z6{Z}", "zeroing without a write mask"},
|
||||
{"VADDPH Y5, Y4, Y6{RZ-SAE}", "wants a ZMM register"},
|
||||
{"VADDPH Z5, Z4, Z6{SAE}", "spells {sae} without a mode"},
|
||||
{"VADDPH Z29, (RCX){RZ-SAE}, Z30", "the memory operand takes none"},
|
||||
{"VFMULCPH Z5, (RCX){1TO8}, Z6", "the entry's memory operand takes none"},
|
||||
{"VADDPH Z29, Z28, Z30{BOGUS}", "is not a decoration the layer reads"},
|
||||
{"VADDPH Z29, Z28, Z30{K7}{K3}", "carries two write masks"},
|
||||
{"VADDSH X5, X4, X6{K3}", "the entry's destination takes none"},
|
||||
{"VCMPSH $300, X29, X28, K5", "outside the unsigned byte range"},
|
||||
{"VGETMANTSH $0x20, X29, X28, X30", "the upper nibble of the mantissa control is reserved"},
|
||||
{"VCVTSI2SH X29, X12, X30", "general register"},
|
||||
{"VADDPH Z5, Z4", "got 2 operands"},
|
||||
{"VMOVSH (Z4), X30", "not an extended-layer operand"},
|
||||
{"VMOVSH foo+4(SB), X30", "not an extended-layer operand"},
|
||||
{"VMOVSH 8(RCX)(DX*3), X30", "outside the byte multipliers"},
|
||||
{"VMOVSH (K1), X30", "not an extended-layer operand"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := assembleAmd64ExtError(t, amd64ExtProbeHead+"\t"+tt.stmt+"\n")
|
||||
if !strings.Contains(got, tt.want) {
|
||||
t.Errorf("%s: error %q does not name %q", tt.stmt, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionLabelOffsets proves pass 1 and pass 2 agree on a
|
||||
// function mixing two ext statements with a backward jump: the label sits
|
||||
// exactly where the laid-down bytes put it, so the JMP's rel8 reaches it.
|
||||
func TestAmd64AssembleExtensionLabelOffsets(t *testing.T) {
|
||||
body := assembleAmd64ExtBody(t, amd64ExtProbeHead+`
|
||||
VADDPH Z1, Z2, Z3
|
||||
loop:
|
||||
VFMADD132PH Z1, Z2, Z3
|
||||
JMP loop
|
||||
`)
|
||||
// Two six-byte EVEX words, then the short JMP whose displacement
|
||||
// measures from its own end (14) back to the label (6).
|
||||
if len(body) != 14 {
|
||||
t.Fatalf("body is %d bytes, want 14", len(body))
|
||||
}
|
||||
if body[12] != 0xEB || body[13] != 0xF8 {
|
||||
t.Errorf("JMP encoded % x, want ebf8", body[12:14])
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionLeavesTheMainEncoderAlone pins the non-invasion
|
||||
// promise on the amd64 side: VPOPCNTD, an AVX-512 instruction the toolchain
|
||||
// knows and the layer deliberately does not carry, encodes through the main
|
||||
// EVEX path, byte for byte what that path produces on its own.
|
||||
func TestAmd64AssembleExtensionLeavesTheMainEncoderAlone(t *testing.T) {
|
||||
body := amd64ExtProbe(t, "VPOPCNTD Z1, Z2")
|
||||
e := &enc{}
|
||||
if err := e.encode("VPOPCNTD", []Operand{Reg{idx: 1, size: 64}, Reg{idx: 2, size: 64}}); err != nil {
|
||||
t.Fatalf("main encoder: %v", err)
|
||||
}
|
||||
if !bytes.Equal(body, e.out) {
|
||||
t.Errorf("VPOPCNTD Z1, Z2: got %x, want the main encoder's %x", body, e.out)
|
||||
}
|
||||
}
|
||||
|
||||
// amd64SweepClass recomputes the vector class an entry encodes, the read of
|
||||
// the template's length field the arch package keeps private: EVEX.L'L in
|
||||
// byte three, VEX.L in byte two.
|
||||
func amd64SweepClass(in arch.ExtInstr) arch.ExtOperandKind {
|
||||
if in.Vex {
|
||||
if in.Bytes[2]&0x04 != 0 {
|
||||
return arch.ExtYMM
|
||||
}
|
||||
return arch.ExtXMM
|
||||
}
|
||||
switch (in.Bytes[3] >> 5) & 3 {
|
||||
case 0:
|
||||
return arch.ExtXMM
|
||||
case 1:
|
||||
return arch.ExtYMM
|
||||
default:
|
||||
return arch.ExtZMM
|
||||
}
|
||||
}
|
||||
|
||||
// amd64SweepVec spells and models one vector register of the class, the
|
||||
// house names X, Y and Z the layer's text forms carry.
|
||||
func amd64SweepVec(kind arch.ExtOperandKind, n int) (string, arch.ExtOperand) {
|
||||
switch kind {
|
||||
case arch.ExtXMM:
|
||||
return fmt.Sprintf("X%d", n), arch.ExtXmm(n)
|
||||
case arch.ExtYMM:
|
||||
return fmt.Sprintf("Y%d", n), arch.ExtYmm(n)
|
||||
default:
|
||||
return fmt.Sprintf("Z%d", n), arch.ExtZmm(n)
|
||||
}
|
||||
}
|
||||
|
||||
// amd64SweepGpr spells and models the general register of an entry: the W bit
|
||||
// picks the width, and an entry that ignores W takes the 64-bit spelling.
|
||||
func amd64SweepGpr(in arch.ExtInstr) (string, arch.ExtOperand) {
|
||||
if in.Wig || in.Bytes[2]&0x80 != 0 {
|
||||
return "R12", arch.ExtGpr64(12)
|
||||
}
|
||||
return "R12D", arch.ExtGpr32(12)
|
||||
}
|
||||
|
||||
// amd64SweepImm spells and models the control immediate of an entry: the
|
||||
// mantissa control keeps its reserved upper nibble at zero, every other
|
||||
// layout takes a whole byte.
|
||||
func amd64SweepImm(in arch.ExtInstr) (string, arch.ExtOperand) {
|
||||
if in.Imm8 == arch.ExtImm8GetMant {
|
||||
return "$0x0b", arch.ExtImmediate(0x0b)
|
||||
}
|
||||
return "$0x7b", arch.ExtImmediate(0x7b)
|
||||
}
|
||||
|
||||
// amd64SweepDest spells and models the register destination with the
|
||||
// decorations the entry carries on its register form: the write mask beside
|
||||
// every masked entry, the rounding or the exception suppression beside every
|
||||
// entry that takes one.
|
||||
func amd64SweepDest(in arch.ExtInstr, kind arch.ExtOperandKind, n int) (string, arch.ExtOperand) {
|
||||
text, model := amd64SweepVec(kind, n)
|
||||
dec := ""
|
||||
if in.Mask {
|
||||
dec += "{K5}"
|
||||
}
|
||||
switch {
|
||||
case in.Er:
|
||||
dec += "{RZ-SAE}"
|
||||
case in.Sae:
|
||||
dec += "{SAE}"
|
||||
}
|
||||
if dec == "" {
|
||||
return text, model
|
||||
}
|
||||
if in.Mask {
|
||||
model = arch.ExtWriteMasked(model, 5, false)
|
||||
}
|
||||
switch {
|
||||
case in.Er:
|
||||
model = arch.ExtRounded(model, arch.ExtRoundTruncate)
|
||||
case in.Sae:
|
||||
model = arch.ExtRounded(model, arch.ExtRoundSAE)
|
||||
}
|
||||
return text + dec, model
|
||||
}
|
||||
|
||||
// amd64SweepMaskedDest spells and models the destination with the write mask
|
||||
// alone, the one decoration the memory shape keeps.
|
||||
func amd64SweepMaskedDest(in arch.ExtInstr, kind arch.ExtOperandKind, n int) (string, arch.ExtOperand) {
|
||||
text, model := amd64SweepVec(kind, n)
|
||||
if in.Mask {
|
||||
return text + "{K5}", arch.ExtWriteMasked(model, 5, false)
|
||||
}
|
||||
return text, model
|
||||
}
|
||||
|
||||
// amd64ExtSweepStatements builds, for the first registered entry of every
|
||||
// distinct amd64 mnemonic, the register-form statement the sweep drives and,
|
||||
// where the entry carries a memory position, the memory-form statement beside
|
||||
// it. Each statement comes back with its mnemonic and the model operands the
|
||||
// text spells, so the sweep can pin the assembled bytes against
|
||||
// EncodeExtension. The statements follow the registry: a mnemonic registered
|
||||
// on a form this builder knows lands in the sweep in the same change.
|
||||
func amd64ExtSweepStatements() (stmts, mnems []string, models [][]arch.ExtOperand, distinct int) {
|
||||
memText, memModel := "(R9)", arch.ExtMemory(9, 0)
|
||||
seen := make(map[string]bool)
|
||||
for _, in := range arch.Extensions(arch.AMD64) {
|
||||
if seen[in.Name] {
|
||||
continue
|
||||
}
|
||||
seen[in.Name] = true
|
||||
distinct++
|
||||
class := amd64SweepClass(in)
|
||||
// The two-vector forms narrow one side: the half form the
|
||||
// destination, the wide form the source, and the quarter forms pin
|
||||
// one side to the XMM class.
|
||||
srcClass, destClass := class, class
|
||||
switch in.Form {
|
||||
case arch.ExtFormAmdVec2Half:
|
||||
destClass = arch.ExtXMM
|
||||
if class == arch.ExtZMM {
|
||||
destClass = arch.ExtYMM
|
||||
}
|
||||
case arch.ExtFormAmdVec2Wide:
|
||||
srcClass = arch.ExtXMM
|
||||
if class == arch.ExtZMM {
|
||||
srcClass = arch.ExtYMM
|
||||
}
|
||||
case arch.ExtFormAmdVec2Quarter:
|
||||
srcClass = arch.ExtXMM
|
||||
case arch.ExtFormAmdVec2ToQuarter:
|
||||
destClass = arch.ExtXMM
|
||||
}
|
||||
srcText, srcModel := amd64SweepVec(srcClass, 1)
|
||||
src2Text, src2Model := amd64SweepVec(class, 2)
|
||||
gprText, gprModel := amd64SweepGpr(in)
|
||||
immText, immModel := amd64SweepImm(in)
|
||||
add := func(stmt, mnem string, ops ...arch.ExtOperand) {
|
||||
stmts = append(stmts, stmt)
|
||||
mnems = append(mnems, mnem)
|
||||
models = append(models, ops)
|
||||
}
|
||||
switch in.Form {
|
||||
case arch.ExtFormAmdVec3:
|
||||
dText, dModel := amd64SweepDest(in, class, 3)
|
||||
add(fmt.Sprintf("%s %s, %s, %s", in.Name, srcText, src2Text, dText), in.Name, srcModel, src2Model, dModel)
|
||||
if in.Mem == 2 {
|
||||
dText, dModel = amd64SweepMaskedDest(in, class, 3)
|
||||
add(fmt.Sprintf("%s %s, %s, %s", in.Name, srcText, memText, dText), in.Name, srcModel, memModel, dModel)
|
||||
}
|
||||
case arch.ExtFormAmdVec2, arch.ExtFormAmdVec2Half, arch.ExtFormAmdVec2Wide,
|
||||
arch.ExtFormAmdVec2Quarter, arch.ExtFormAmdVec2ToQuarter:
|
||||
dText, dModel := amd64SweepDest(in, destClass, 2)
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, srcText, dText), in.Name, srcModel, dModel)
|
||||
if in.Mem == 1 {
|
||||
dText, dModel = amd64SweepMaskedDest(in, destClass, 2)
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, memText, dText), in.Name, memModel, dModel)
|
||||
}
|
||||
case arch.ExtFormAmdMask2:
|
||||
add(fmt.Sprintf("%s %s, %s, K3", in.Name, srcText, src2Text), in.Name, srcModel, src2Model, arch.ExtMask(3))
|
||||
case arch.ExtFormAmdVecGprVec:
|
||||
dText, dModel := amd64SweepVec(class, 2)
|
||||
add(fmt.Sprintf("%s %s, %s, %s", in.Name, srcText, gprText, dText), in.Name, srcModel, gprModel, dModel)
|
||||
case arch.ExtFormAmdGprVec:
|
||||
dText, dModel := amd64SweepVec(class, 1)
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, gprText, dText), in.Name, gprModel, dModel)
|
||||
case arch.ExtFormAmdVecGpr:
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, srcText, gprText), in.Name, srcModel, gprModel)
|
||||
case arch.ExtFormAmdVec3Imm:
|
||||
dText, dModel := amd64SweepMaskedDest(in, class, 4)
|
||||
add(fmt.Sprintf("%s %s, %s, %s, %s", in.Name, immText, srcText, src2Text, dText), in.Name, immModel, srcModel, src2Model, dModel)
|
||||
if in.Mem == 3 {
|
||||
add(fmt.Sprintf("%s %s, %s, %s, %s", in.Name, immText, srcText, memText, dText), in.Name, immModel, srcModel, memModel, dModel)
|
||||
}
|
||||
case arch.ExtFormAmdMask2Imm:
|
||||
add(fmt.Sprintf("%s %s, %s, %s, K3", in.Name, immText, srcText, src2Text), in.Name, immModel, srcModel, src2Model, arch.ExtMask(3))
|
||||
if in.Mem == 3 {
|
||||
add(fmt.Sprintf("%s %s, %s, %s, K3", in.Name, immText, srcText, memText), in.Name, immModel, srcModel, memModel, arch.ExtMask(3))
|
||||
}
|
||||
case arch.ExtFormAmdVec2Imm:
|
||||
dText, dModel := amd64SweepMaskedDest(in, class, 3)
|
||||
add(fmt.Sprintf("%s %s, %s, %s", in.Name, immText, srcText, dText), in.Name, immModel, srcModel, dModel)
|
||||
if in.Mem == 2 {
|
||||
add(fmt.Sprintf("%s %s, %s, %s", in.Name, immText, memText, dText), in.Name, immModel, memModel, dModel)
|
||||
}
|
||||
case arch.ExtFormAmdMemVec:
|
||||
dText, dModel := amd64SweepVec(class, 1)
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, memText, dText), in.Name, memModel, dModel)
|
||||
case arch.ExtFormAmdVecMem:
|
||||
add(fmt.Sprintf("%s %s, %s", in.Name, srcText, memText), in.Name, srcModel, memModel)
|
||||
default:
|
||||
stmts = append(stmts, "")
|
||||
mnems = append(mnems, in.Name)
|
||||
models = append(models, nil)
|
||||
}
|
||||
}
|
||||
return stmts, mnems, models, distinct
|
||||
}
|
||||
|
||||
// TestAmd64AssembleExtensionSweep drives every distinct registered mnemonic
|
||||
// through the full assembler from .s text: the register-form statement of the
|
||||
// mnemonic's first entry, and the memory-form statement beside it where the
|
||||
// entry carries a memory position. Every statement must assemble, and every
|
||||
// body must equal EncodeExtension's encoding of the model operands the text
|
||||
// spells, so the front end and the registry cannot drift apart on any family.
|
||||
func TestAmd64AssembleExtensionSweep(t *testing.T) {
|
||||
stmts, mnems, models, distinct := amd64ExtSweepStatements()
|
||||
if distinct != 86 {
|
||||
t.Errorf("the amd64 layer registers %d distinct mnemonics, want 86", distinct)
|
||||
}
|
||||
for i, stmt := range stmts {
|
||||
if stmt == "" {
|
||||
t.Errorf("%s registers a form the sweep builder does not spell", mnems[i])
|
||||
continue
|
||||
}
|
||||
body := amd64ExtProbe(t, stmt)
|
||||
want, err := EncodeExtension(arch.AMD64, mnems[i], models[i]...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: registry encode: %v", stmt, err)
|
||||
continue
|
||||
}
|
||||
if !bytes.Equal(body, want) {
|
||||
t.Errorf("%s:\n got %x\n want %x (the registry encoding)", stmt, body, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "strings"
|
||||
|
||||
// encodeAmd64Family routes a mnemonic through the amd64 corpus families the
|
||||
// dedicated amd64_*.go files implement. The first family that owns the name
|
||||
// decides the outcome: its bytes, or its error. A name no family claims
|
||||
// falls through to the scalar dispatch in (*enc).encode untouched.
|
||||
func (e *enc) encodeAmd64Family(upper string, ops []Operand) (bool, error) {
|
||||
if ok, err := e.encodeX87(upper, ops); ok {
|
||||
return true, err
|
||||
}
|
||||
if ok, err := e.encodeSystem(upper, ops); ok {
|
||||
return true, err
|
||||
}
|
||||
if ok, err := e.encodeXsave(upper, ops); ok {
|
||||
return true, err
|
||||
}
|
||||
if ok, err := e.encodeSSEMore(upper, ops); ok {
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// amd64FamilyEncodable mirrors encodeAmd64Family for the lint-time
|
||||
// predicate: true when some family owns the mnemonic, whatever the operand
|
||||
// shapes. It must claim exactly the names encodeAmd64Family does.
|
||||
func amd64FamilyEncodable(upper string) bool {
|
||||
if _, ok := x87NoOperand[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := x87Arith[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := x87FCmov[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := x87Compare[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := x87MemUnary[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := x87Fxsav[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if upper == "FADDDP" {
|
||||
return true
|
||||
}
|
||||
if _, ok := systemNoOperand[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if b, size := splitSize(upper); size != 0 {
|
||||
switch upper[len(upper)-1] {
|
||||
case 'B', 'W', 'L', 'Q':
|
||||
if _, ok := stringOp[b]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if m, ok := sysRm[b]; ok && m.sized {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if _, ok := nopWidth[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := cacheControl[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := movbeSize[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := randSource[base(upper, 6)]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := fsGsBase[base(upper, 8)]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := descTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sysRm[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := selectorRead[base(upper, 3)]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := farSegLoad[base(upper, 3)]; ok {
|
||||
return true
|
||||
}
|
||||
if upper == "CMPXCHG8B" || upper == "CMPXCHG16B" {
|
||||
return true
|
||||
}
|
||||
if upper == "INVPCID" || upper == "XABORT" {
|
||||
return true
|
||||
}
|
||||
if _, ok := xsaveTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if before, ok := strings.CutSuffix(upper, "64"); ok {
|
||||
if _, ok := xsaveTable[before]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
switch upper {
|
||||
case "EMMS", "PSHUFW", "PSLLO", "PSRLO", "PMOVMSKB", "MOVQOZX", "MOVZWW", "MOVSWW":
|
||||
return true
|
||||
}
|
||||
if _, ok := sseMaskmov[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := mmxShiftImm[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := mmxShiftVar[upper]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// This file implements the amd64 SIMD pieces the main tables lack: the MMX
|
||||
// bank glue (EMMS, the masked stores, the MMX shifts and shuffle, the
|
||||
// byte-mask extract), the MOVQ bank crossings' odd spellings and the leaf
|
||||
// aliases. Every encoding here is pinned byte for byte against go tool asm
|
||||
// through the corpus lines in amd64_sse_test.go.
|
||||
|
||||
// sseMaskmov maps the masked cache-line stores to their prefix and opcode:
|
||||
// OP src, dst with the second operand in the reg field, no memory operand.
|
||||
var sseMaskmov = map[string]sseBin{
|
||||
"MASKMOVQ": {0, 0xF7, false},
|
||||
"MASKMOVOU": {0x66, 0xF7, false},
|
||||
}
|
||||
|
||||
// sseMoreBin maps the packed and scalar legacy binaries the main table
|
||||
// lacks, reg = destination and r/m = source: the float comparisons, square
|
||||
// roots and reciprocal estimates, the SSE3 horizontal arithmetic, the SSE4.1
|
||||
// packed integers and the SSSE3 sign and horizontal ops. The MMX twins of
|
||||
// the 0x66-prefixed members drop the prefix in the shared encoder.
|
||||
var sseMoreBin = map[string]sseBin{
|
||||
"COMISS": {0, 0x2F, false},
|
||||
"UCOMISS": {0, 0x2E, false},
|
||||
"UCOMISD": {0x66, 0x2E, false},
|
||||
"SQRTPS": {0, 0x51, false},
|
||||
"SQRTPD": {0x66, 0x51, false},
|
||||
"SQRTSS": {0xF3, 0x51, false},
|
||||
"RCPPS": {0, 0x53, false},
|
||||
"RCPSS": {0xF3, 0x53, false},
|
||||
"RSQRTPS": {0, 0x52, false},
|
||||
"RSQRTSS": {0xF3, 0x52, false},
|
||||
"ADDSUBPD": {0x66, 0xD0, false},
|
||||
"ADDSUBPS": {0xF2, 0xD0, false},
|
||||
"HADDPD": {0x66, 0x7C, false},
|
||||
"HADDPS": {0xF2, 0x7C, false},
|
||||
"HSUBPD": {0x66, 0x7D, false},
|
||||
"HSUBPS": {0xF2, 0x7D, false},
|
||||
"MOVDDUP": {0xF2, 0x12, false},
|
||||
"MOVSHDUP": {0xF3, 0x16, false},
|
||||
"MOVSLDUP": {0xF3, 0x12, false},
|
||||
"LDDQU": {0xF2, 0xF0, false},
|
||||
"MOVNTDQA": {0x66, 0x2A, true},
|
||||
"PTEST": {0x66, 0x17, true},
|
||||
"PABSB": {0x66, 0x1C, true},
|
||||
"PABSW": {0x66, 0x1D, true},
|
||||
"PABSD": {0x66, 0x1E, true},
|
||||
"PACKSSWB": {0x66, 0x63, false},
|
||||
"PACKUSWB": {0x66, 0x67, false},
|
||||
"PACKSSLW": {0x66, 0x6B, false},
|
||||
"PACKUSDW": {0x66, 0x2B, true},
|
||||
"PADDSB": {0x66, 0xEC, false},
|
||||
"PADDSW": {0x66, 0xED, false},
|
||||
"PADDUSB": {0x66, 0xDC, false},
|
||||
"PADDUSW": {0x66, 0xDD, false},
|
||||
"PAVGB": {0x66, 0xE0, false},
|
||||
"PAVGW": {0x66, 0xE3, false},
|
||||
"PCMPEQQ": {0x66, 0x29, true},
|
||||
"PCMPGTQ": {0x66, 0x37, true},
|
||||
"PHADDW": {0x66, 0x01, true},
|
||||
"PHADDD": {0x66, 0x02, true},
|
||||
"PHADDSW": {0x66, 0x03, true},
|
||||
"PHSUBW": {0x66, 0x05, true},
|
||||
"PHSUBD": {0x66, 0x06, true},
|
||||
"PHSUBSW": {0x66, 0x07, true},
|
||||
"PHMINPOSUW": {0x66, 0x41, true},
|
||||
"PMADDUBSW": {0x66, 0x04, true},
|
||||
"PMADDWL": {0x66, 0xF5, false},
|
||||
"PMAXSB": {0x66, 0x3C, true},
|
||||
"PMAXSD": {0x66, 0x3D, true},
|
||||
"PMAXSW": {0x66, 0xEE, false},
|
||||
"PMAXUB": {0x66, 0xDE, false},
|
||||
"PMAXUD": {0x66, 0x3F, true},
|
||||
"PMAXUW": {0x66, 0x3E, true},
|
||||
"PMINSB": {0x66, 0x38, true},
|
||||
"PMINSD": {0x66, 0x39, true},
|
||||
"PMINSW": {0x66, 0xEA, false},
|
||||
"PMINUB": {0x66, 0xDA, false},
|
||||
"PMINUD": {0x66, 0x3B, true},
|
||||
"PMINUW": {0x66, 0x3A, true},
|
||||
"PMULDQ": {0x66, 0x28, true},
|
||||
"PMULLD": {0x66, 0x40, true},
|
||||
"PMULHRSW": {0x66, 0x0B, true},
|
||||
"PMULHUW": {0x66, 0xE4, false},
|
||||
"PMULHW": {0x66, 0xE5, false},
|
||||
"PMULLW": {0x66, 0xD5, false},
|
||||
"PMULULQ": {0x66, 0xF4, false},
|
||||
"PCMPGTL": {0x66, 0x66, false},
|
||||
"PMOVSXBD": {0x66, 0x21, true},
|
||||
"PMOVSXBQ": {0x66, 0x22, true},
|
||||
"PMOVSXBW": {0x66, 0x20, true},
|
||||
"PMOVSXDQ": {0x66, 0x25, true},
|
||||
"PMOVSXWD": {0x66, 0x23, true},
|
||||
"PMOVSXWQ": {0x66, 0x24, true},
|
||||
"PMOVZXBD": {0x66, 0x31, true},
|
||||
"PMOVZXBQ": {0x66, 0x32, true},
|
||||
"PMOVZXBW": {0x66, 0x30, true},
|
||||
"PMOVZXDQ": {0x66, 0x35, true},
|
||||
"PMOVZXWD": {0x66, 0x33, true},
|
||||
"PMOVZXWQ": {0x66, 0x34, true},
|
||||
// The conversion aliases the Plan 9 table spells with an L: the dword
|
||||
// sources and destinations of the packed integer/float converts.
|
||||
"CVTPL2PD": {0xF3, 0xE6, false},
|
||||
"CVTPL2PS": {0, 0x5B, false},
|
||||
"CVTPD2PL": {0xF2, 0xE6, false},
|
||||
"CVTPS2PL": {0x66, 0x5B, false},
|
||||
"CVTTPD2PL": {0x66, 0xE6, false},
|
||||
"CVTTPS2PL": {0xF3, 0x5B, false},
|
||||
"PSADBW": {0x66, 0xF6, false},
|
||||
"PSUBSB": {0x66, 0xE8, false},
|
||||
"PSUBSW": {0x66, 0xE9, false},
|
||||
"PSUBUSB": {0x66, 0xD8, false},
|
||||
"PSUBUSW": {0x66, 0xD9, false},
|
||||
"PSIGNB": {0x66, 0x08, true},
|
||||
"PSIGNW": {0x66, 0x09, true},
|
||||
"PSIGND": {0x66, 0x0A, true},
|
||||
"PUNPCKHBW": {0x66, 0x68, false},
|
||||
"PUNPCKHLQ": {0x66, 0x6A, false},
|
||||
"PUNPCKHQDQ": {0x66, 0x6D, false},
|
||||
"PUNPCKHWL": {0x66, 0x69, false},
|
||||
"PUNPCKLLQ": {0x66, 0x62, false},
|
||||
"PUNPCKLQDQ": {0x66, 0x6C, false},
|
||||
"PUNPCKLWL": {0x66, 0x61, false},
|
||||
}
|
||||
|
||||
// sseMoreImm3 maps the imm8-controlled three-operand instructions the main
|
||||
// table lacks: OP $imm, src, dst.
|
||||
var sseMoreImm3 = map[string]sseImm3{
|
||||
"ROUNDPS": {0x66, 0x08, true},
|
||||
"ROUNDPD": {0x66, 0x09, true},
|
||||
"ROUNDSS": {0x66, 0x0A, true},
|
||||
"ROUNDSD": {0x66, 0x0B, true},
|
||||
"DPPS": {0x66, 0x40, true},
|
||||
"DPPD": {0x66, 0x41, true},
|
||||
"BLENDPS": {0x66, 0x0C, true},
|
||||
"BLENDPD": {0x66, 0x0D, true},
|
||||
"INSERTPS": {0x66, 0x21, true},
|
||||
"MPSADBW": {0x66, 0x42, true},
|
||||
"PCMPESTRM": {0x66, 0x60, true},
|
||||
"PCMPESTRI": {0x66, 0x61, true},
|
||||
"PCMPISTRM": {0x66, 0x62, true},
|
||||
"PCMPISTRI": {0x66, 0x63, true},
|
||||
}
|
||||
|
||||
// sseBlendv maps the variable blends whose implicit mask is X0: the first
|
||||
// operand must be the literal X0, the register the hardware reads.
|
||||
var sseBlendv = map[string]sseBin{
|
||||
"BLENDVPS": {0x66, 0x14, true},
|
||||
"BLENDVPD": {0x66, 0x15, true},
|
||||
"PBLENDVB": {0x66, 0x10, true},
|
||||
}
|
||||
|
||||
// sseHighLow maps the high/low half moves to their load/store opcode pair.
|
||||
// A memory source loads (reg = destination), a memory destination stores
|
||||
// (reg = the register source).
|
||||
var sseHighLow = map[string]sseMove{
|
||||
"MOVHPD": {0x66, 0x16, 0x17},
|
||||
"MOVHPS": {0, 0x16, 0x17},
|
||||
"MOVLPD": {0x66, 0x12, 0x13},
|
||||
"MOVLPS": {0, 0x12, 0x13},
|
||||
}
|
||||
|
||||
// sseRegReg maps the register-to-register half moves, register destination
|
||||
// and register source alone: MOVHLPS and MOVLHPS.
|
||||
var sseRegReg = map[string]sseBin{
|
||||
"MOVHLPS": {0, 0x12, false},
|
||||
"MOVLHPS": {0, 0x16, false},
|
||||
}
|
||||
|
||||
// sseMovmsk maps the sign-mask extractions to a GPR: OP vec, gpr.
|
||||
var sseMovmsk = map[string]sseBin{
|
||||
"MOVMSKPS": {0, 0x50, false},
|
||||
"MOVMSKPD": {0x66, 0x50, false},
|
||||
}
|
||||
|
||||
// sseMovnt maps the non-temporal stores, OP reg, mem, plus MOVNTDQA's load
|
||||
// (which rides sseMoreBin).
|
||||
var sseMovnt = map[string]sseBin{
|
||||
"MOVNTPS": {0, 0x2B, false},
|
||||
"MOVNTPD": {0x66, 0x2B, false},
|
||||
"MOVNTQ": {0, 0xE7, false},
|
||||
"MOVNTO": {0x66, 0xE7, false},
|
||||
"MOVNTIL": {0, 0xC3, false},
|
||||
"MOVNTIQ": {0, 0xC3, false},
|
||||
}
|
||||
|
||||
// mmxShiftImm lists the packed integer shifts whose immediate form the MMX
|
||||
// bank spells without the 0x66 prefix; the digit rides the 0F 71/72/73
|
||||
// group, the same /digits the XMM forms carry.
|
||||
var mmxShiftImm = map[string]sseShift{
|
||||
"PSLLW": {0x71, 6},
|
||||
"PSRLW": {0x71, 2},
|
||||
"PSRAW": {0x71, 4},
|
||||
"PSLLL": {0x72, 6},
|
||||
"PSRLL": {0x72, 2},
|
||||
"PSRAL": {0x72, 4},
|
||||
"PSLLQ": {0x73, 6},
|
||||
"PSRLQ": {0x73, 2},
|
||||
}
|
||||
|
||||
// mmxShiftVar lists the variable-count forms over the MMX bank, the same
|
||||
// opcodes the XMM variable shifts ride, prefix dropped.
|
||||
var mmxShiftVar = map[string]byte{
|
||||
"PSLLW": 0xF1,
|
||||
"PSRLW": 0xD1,
|
||||
"PSRAW": 0xE1,
|
||||
"PSLLL": 0xF2,
|
||||
"PSRLL": 0xD2,
|
||||
"PSRAL": 0xE2,
|
||||
"PSLLQ": 0xF3,
|
||||
"PSRLQ": 0xD3,
|
||||
}
|
||||
|
||||
// sseOctaShift lists the octa byte shifts' extra spellings: PSLLO and PSRLO
|
||||
// are the Plan 9 names of PSLLDQ/PSRLDQ, XMM only.
|
||||
var sseOctaShift = map[string]sseShift{
|
||||
"PSLLO": {0x73, 7},
|
||||
"PSRLO": {0x73, 3},
|
||||
}
|
||||
|
||||
// isMmx reports whether the operand is an MMX register.
|
||||
func isMmx(op Operand) bool {
|
||||
r, ok := op.(Reg)
|
||||
return ok && r.mmx
|
||||
}
|
||||
|
||||
// encodeSSEMore encodes the MMX glue and the leaf SIMD spellings. It
|
||||
// reports whether the mnemonic belongs to the layer; a false result hands
|
||||
// the mnemonic back to the caller.
|
||||
func (e *enc) encodeSSEMore(upper string, ops []Operand) (bool, error) {
|
||||
if upper == "EMMS" {
|
||||
if len(ops) != 0 {
|
||||
return true, fmt.Errorf("EMMS takes no operands, got %d", len(ops))
|
||||
}
|
||||
return true, e.emit(&instr{opcode: []byte{0x0F, 0x77}, modrm: -1, sib: -1})
|
||||
}
|
||||
if m, ok := sseMaskmov[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, ok1 := ops[0].(Reg)
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok1 || !ok2 || !srcReg.isVec() && !srcReg.mmx || !dstReg.isVec() && !dstReg.mmx {
|
||||
return true, fmt.Errorf("%s takes two vector or MMX registers", upper)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, srcReg, 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// The packed shifts over the MMX bank drop the 0x66 prefix the XMM forms
|
||||
// carry; only a shift name enters the MMX path, the XMM spellings of the
|
||||
// shifts and every other packed binary fall through to the main tables.
|
||||
if _, isShift := mmxShiftImm[upper]; isShift {
|
||||
if e.encodeMmxShiftGate(ops) {
|
||||
return e.encodeMmxShift(upper, ops)
|
||||
}
|
||||
} else if _, isVar := mmxShiftVar[upper]; isVar {
|
||||
if e.encodeMmxShiftGate(ops) {
|
||||
return e.encodeMmxShift(upper, ops)
|
||||
}
|
||||
}
|
||||
// PSHUFW is the MMX word shuffle, 0F 70 with no prefix: the XMM twins
|
||||
// (PSHUFD and friends) dispatch through the main shuffle table.
|
||||
if upper == "PSHUFW" {
|
||||
if len(ops) != 3 {
|
||||
return true, fmt.Errorf("PSHUFW expects 3 operands ($imm, src, dst), got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("PSHUFW needs an imm8 first operand")
|
||||
}
|
||||
immByte, err := imm8(int64(imm))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
dstReg, ok2 := ops[2].(Reg)
|
||||
if !ok2 || !dstReg.mmx {
|
||||
return true, fmt.Errorf("PSHUFW destination must be an MMX register")
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, 0x70}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, ops[1], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
i.imm = []byte{immByte}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
if spec, ok := sseOctaShift[upper]; ok {
|
||||
return e.encodeMmxShiftForm(upper, spec, 0x66, ops)
|
||||
}
|
||||
// The byte-mask extract over the MMX bank rides the same 0F D7 opcode
|
||||
// without the prefix; the XMM spelling falls through.
|
||||
if upper == "PMOVMSKB" && len(ops) == 2 {
|
||||
if srcReg, ok := ops[0].(Reg); ok && srcReg.mmx {
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok2 || dstReg.isVec() || dstReg.mmx {
|
||||
return true, fmt.Errorf("%s destination must be a general register", upper)
|
||||
}
|
||||
i := newInstr(4, []byte{0x0F, 0xD7})
|
||||
if err := setRM(i, dstReg, srcReg, 4); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
}
|
||||
// MOVQOZX is the octa-to-quad zero-extend load, F3 0F D6: an MMX or
|
||||
// memory source into an XMM destination, the MOVQ2DQ opcode.
|
||||
if upper == "MOVQOZX" {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("MOVQOZX expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return true, fmt.Errorf("MOVQOZX destination must be an XMM register")
|
||||
}
|
||||
switch ops[0].(type) {
|
||||
case Reg, Mem, sbMem:
|
||||
default:
|
||||
return true, fmt.Errorf("MOVQOZX source must be an MMX register or memory")
|
||||
}
|
||||
i := &instr{prefix: 0xF3, opcode: []byte{0x0F, 0xD6}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// MOVZWW and MOVSWW are the word zero/sign-extend moves under aliases:
|
||||
// the MOVWLZX and MOVWLSX opcodes carrying the word width's 0x66 prefix.
|
||||
switch upper {
|
||||
case "MOVZWW", "MOVSWW":
|
||||
op := []byte{0x0F, 0xB7}
|
||||
if upper == "MOVSWW" {
|
||||
op[1] = 0xBF
|
||||
}
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("%s destination must be a register", upper)
|
||||
}
|
||||
i := newInstr(2, op)
|
||||
if err := setRM(i, dstReg, ops[0], 2); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// The packed and scalar binaries: reg = destination, r/m = source, the
|
||||
// shared encoder carrying the MMX prefix drop.
|
||||
if m, ok := sseMoreBin[upper]; ok {
|
||||
return true, e.encodeSSEBin(m, ops)
|
||||
}
|
||||
// The imm8-controlled instructions: OP $imm, src, dst.
|
||||
if m, ok := sseMoreImm3[upper]; ok {
|
||||
return true, e.encodeSSEImm3(m, ops)
|
||||
}
|
||||
// The variable blends with their implicit X0 mask: the first operand is
|
||||
// the literal X0, the register the encoding leaves out.
|
||||
if m, ok := sseBlendv[upper]; ok {
|
||||
if len(ops) != 3 {
|
||||
return true, fmt.Errorf("%s expects 3 operands (X0, src, dst), got %d", upper, len(ops))
|
||||
}
|
||||
x0, ok := ops[0].(Reg)
|
||||
if !ok || !x0.isVec() || x0.idx != 0 || x0.size != 16 {
|
||||
return true, fmt.Errorf("%s first operand must be X0", upper)
|
||||
}
|
||||
return true, e.encodeSSEBin(m, ops[1:])
|
||||
}
|
||||
// The high/low half moves split by direction: a memory source loads, a
|
||||
// memory destination stores, both two-operand.
|
||||
if m, ok := sseHighLow[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, srcVec := vecReg(ops[0])
|
||||
dstReg, dstVec := vecReg(ops[1])
|
||||
var op byte
|
||||
var reg Reg
|
||||
var rm Operand
|
||||
switch {
|
||||
case srcVec && isX86Mem(ops[1]):
|
||||
op, reg, rm = m.store, srcReg, ops[1]
|
||||
case dstVec && isX86Mem(ops[0]):
|
||||
op, reg, rm = m.load, dstReg, ops[0]
|
||||
default:
|
||||
return true, fmt.Errorf("%s takes one vector register and one memory operand", upper)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, reg, rm, 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// The register-to-register half moves.
|
||||
if m, ok := sseRegReg[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, srcVec := vecReg(ops[0])
|
||||
dstReg, dstVec := vecReg(ops[1])
|
||||
if !srcVec || !dstVec {
|
||||
return true, fmt.Errorf("%s takes two XMM registers", upper)
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, srcReg, 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// The sign-mask extractions: the vector source's sign bits pack into a
|
||||
// general register.
|
||||
if m, ok := sseMovmsk[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, srcVec := vecReg(ops[0])
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !srcVec || !ok || dstReg.isVec() || dstReg.mmx {
|
||||
return true, fmt.Errorf("%s takes a vector register and a general register", upper)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, srcReg, 4); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// The non-temporal stores: the register source rides reg, the memory
|
||||
// destination r/m; MOVNTIL/IQ store from a general register and MOVNTIQ
|
||||
// carries REX.W.
|
||||
if m, ok := sseMovnt[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
var srcReg Reg
|
||||
switch r := ops[0].(type) {
|
||||
case Reg:
|
||||
if upper == "MOVNTIL" || upper == "MOVNTIQ" {
|
||||
if r.isVec() || r.mmx {
|
||||
return true, fmt.Errorf("%s source must be a general register", upper)
|
||||
}
|
||||
srcReg = r
|
||||
} else {
|
||||
if !r.isVec() && !r.mmx {
|
||||
return true, fmt.Errorf("%s source must be a vector or MMX register", upper)
|
||||
}
|
||||
srcReg = r
|
||||
}
|
||||
default:
|
||||
return true, fmt.Errorf("%s source must be a register", upper)
|
||||
}
|
||||
if !isX86Mem(ops[1]) {
|
||||
return true, fmt.Errorf("%s destination must be memory", upper)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1, rexW: upper == "MOVNTIQ"}
|
||||
if err := setRM(i, srcReg, ops[1], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// EXTRACTPS is the lane extract to a GPR or memory, the PEXTR layout.
|
||||
if upper == "EXTRACTPS" {
|
||||
return true, e.encodeSSEExtract(sseExtract{op: []byte{0x0F, 0x3A, 0x17}}, ops)
|
||||
}
|
||||
// CVTSL2SS and CVTSQ2SS are the integer-to-scalar-single converts, the
|
||||
// CVTSL2SD pair's F3 twin: F3 0F 2A with reg = XMM destination, REX.W
|
||||
// on the quad source spelling.
|
||||
if upper == "CVTSL2SS" || upper == "CVTSQ2SS" {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok || !dstReg.isVec() {
|
||||
return true, fmt.Errorf("%s destination must be a vector register", upper)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0x2A})
|
||||
i.rexW = upper == "CVTSQ2SS"
|
||||
i.prefix = 0xF3
|
||||
if err := setRM(i, dstReg, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// encodeMmxShiftGate reports whether the shift's destination operand is an
|
||||
// MMX register, the case the bank's own prefix-free forms cover.
|
||||
func (e *enc) encodeMmxShiftGate(ops []Operand) bool {
|
||||
if len(ops) != 2 {
|
||||
return false
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
return ok && dstReg.mmx
|
||||
}
|
||||
|
||||
// encodeMmxShift routes the MMX shift between its immediate form
|
||||
// (OP $imm, dst, the 0F 71/72/73 /digit group) and its variable-count form
|
||||
// (OP count, dst, the 0F D1-F3 row).
|
||||
func (e *enc) encodeMmxShift(upper string, ops []Operand) (bool, error) {
|
||||
if spec, ok := mmxShiftImm[upper]; ok {
|
||||
if _, isImm := ops[0].(Imm); isImm {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
immByte, err := imm8(int64(ops[0].(Imm)))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, spec.op}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, spec.digit, ops[1], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
i.imm = []byte{immByte}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
}
|
||||
if op, ok := mmxShiftVar[upper]; ok {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
if !vecOrMem(ops[0]) && !isMmx(ops[0]) {
|
||||
return true, fmt.Errorf("%s count must be an immediate, an MMX register or memory", upper)
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok || !dstReg.mmx {
|
||||
return true, fmt.Errorf("%s destination must be an MMX register", upper)
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
return true, fmt.Errorf("unsupported instruction %q", upper)
|
||||
}
|
||||
|
||||
// encodeMmxShiftForm emits one XMM octa shift: OP $imm, dst, the 0x66
|
||||
// prefix carried.
|
||||
func (e *enc) encodeMmxShiftForm(upper string, spec sseShift, prefix byte, ops []Operand) (bool, error) {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("%s needs an immediate count", upper)
|
||||
}
|
||||
immByte, err := imm8(int64(imm))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok2 || !dstReg.isVec() {
|
||||
return true, fmt.Errorf("%s destination must be the second, vector operand", upper)
|
||||
}
|
||||
i := &instr{prefix: prefix, opcode: []byte{0x0F, spec.op}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, spec.digit, dstReg, 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
i.imm = []byte{immByte}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,467 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// This file implements the system, flag, string and segment families the Go
|
||||
// assembler carries: the no-operand controls, the string primitives, the
|
||||
// sign-extension pair, the multi-byte no-ops, the cache controls, MOVBE, the
|
||||
// compare-exchange doubles, the random source pair, the FS/GS base pair, the
|
||||
// descriptor-table controls and the LAR/LSL selector reads. Every encoding
|
||||
// here is pinned byte for byte against go tool asm through the corpus lines
|
||||
// in amd64_system_test.go.
|
||||
|
||||
// systemNoOperand maps a fixed no-operand mnemonic to its opcode bytes, the
|
||||
// prefixes spelled out in full.
|
||||
var systemNoOperand = map[string][]byte{
|
||||
"CLC": {0xF8},
|
||||
"STC": {0xF9},
|
||||
"CMC": {0xF5},
|
||||
"CLI": {0xFA},
|
||||
"STI": {0xFB},
|
||||
"HLT": {0xF4},
|
||||
"ICEBP": {0xF1},
|
||||
"XLAT": {0xD7},
|
||||
"LAHF": {0x9F},
|
||||
"SAHF": {0x9E},
|
||||
"PUSHFW": {0x66, 0x9C},
|
||||
"POPFW": {0x66, 0x9D},
|
||||
"IRETW": {0x66, 0xCF},
|
||||
"IRETL": {0xCF},
|
||||
"IRETQ": {0x48, 0xCF},
|
||||
"UD1": {0x0F, 0xB9},
|
||||
"UD2": {0x0F, 0x0B},
|
||||
"CLAC": {0x0F, 0x01, 0xCA},
|
||||
"STAC": {0x0F, 0x01, 0xCB},
|
||||
"CLTS": {0x0F, 0x06},
|
||||
"INVD": {0x0F, 0x08},
|
||||
"WBINVD": {0x0F, 0x09},
|
||||
"SWAPGS": {0x0F, 0x01, 0xF8},
|
||||
"RSM": {0x0F, 0xAA},
|
||||
"MONITOR": {0x0F, 0x01, 0xC8},
|
||||
"MWAIT": {0x0F, 0x01, 0xC9},
|
||||
"RDMSR": {0x0F, 0x32},
|
||||
"WRMSR": {0x0F, 0x30},
|
||||
"RDPMC": {0x0F, 0x33},
|
||||
"RDPKRU": {0x0F, 0x01, 0xEE},
|
||||
"WRPKRU": {0x0F, 0x01, 0xEF},
|
||||
"XSETBV": {0x0F, 0x01, 0xD1},
|
||||
"SYSENTER": {0x0F, 0x34},
|
||||
"SYSENTER64": {0x48, 0x0F, 0x34},
|
||||
"SYSEXIT": {0x0F, 0x35},
|
||||
"SYSEXIT64": {0x48, 0x0F, 0x35},
|
||||
"SYSRET": {0x0F, 0x07},
|
||||
"LEAVE": {0xC9},
|
||||
"LEAVEQ": {0xC9},
|
||||
"XEND": {0x0F, 0x01, 0xD5},
|
||||
"XTEST": {0x0F, 0x01, 0xD6},
|
||||
"CBW": {0x66, 0x98},
|
||||
"CWDE": {0x98},
|
||||
"CDQE": {0x48, 0x98},
|
||||
"CWD": {0x66, 0x99},
|
||||
"CDQ": {0x99},
|
||||
"CQO": {0x48, 0x99},
|
||||
}
|
||||
|
||||
// stringOp maps the string-primitive bases to their 32-bit opcode; the byte
|
||||
// form is one lower, the word spelling carries 0x66 and the quad spelling
|
||||
// REX.W, exactly the prefix ladder newInstr applies.
|
||||
var stringOp = map[string]byte{
|
||||
"CMPS": 0xA7,
|
||||
"INS": 0x6D,
|
||||
"LODS": 0xAD,
|
||||
"OUTS": 0x6F,
|
||||
"SCAS": 0xAF,
|
||||
}
|
||||
|
||||
// nopWidth maps the multi-byte no-op spellings to their operand size.
|
||||
var nopWidth = map[string]int{
|
||||
"NOPW": 2,
|
||||
"NOPL": 4,
|
||||
"NOPQ": 8,
|
||||
}
|
||||
|
||||
// cacheControl maps the one-memory-operand cache controls to their mandatory
|
||||
// prefix, opcode group and /digit.
|
||||
var cacheControl = map[string]struct {
|
||||
prefix byte
|
||||
op []byte
|
||||
digit int
|
||||
}{
|
||||
"CLFLUSH": {0, []byte{0x0F, 0xAE}, 7},
|
||||
"CLFLUSHOPT": {0x66, []byte{0x0F, 0xAE}, 7},
|
||||
"INVLPG": {0, []byte{0x0F, 0x01}, 7},
|
||||
}
|
||||
|
||||
// movbeSize maps the MOVBE spellings to their operand size.
|
||||
var movbeSize = map[string]int{
|
||||
"MOVBEW": 2,
|
||||
"MOVBEL": 4,
|
||||
"MOVBEQ": 8,
|
||||
}
|
||||
|
||||
// randSource maps the random-source bases to their /digit (RDRAND /6,
|
||||
// RDSEED /7); the destination register rides r/m, mod 11.
|
||||
var randSource = map[string]int{
|
||||
"RDRAND": 6,
|
||||
"RDSEED": 7,
|
||||
}
|
||||
|
||||
// fsGsBase maps the FS/GS base accessors to their /digit in the F3-prefixed
|
||||
// 0F AE group; the L and Q spellings exist.
|
||||
var fsGsBase = map[string]int{
|
||||
"RDFSBASE": 0,
|
||||
"RDGSBASE": 1,
|
||||
"WRFSBASE": 2,
|
||||
"WRGSBASE": 3,
|
||||
}
|
||||
|
||||
// descTable maps the descriptor-table accesses to their /digit in 0F 01;
|
||||
// each takes one memory operand alone.
|
||||
var descTable = map[string]int{
|
||||
"LGDT": 2,
|
||||
"LIDT": 3,
|
||||
"SGDT": 0,
|
||||
"SIDT": 1,
|
||||
}
|
||||
|
||||
// sysRmEntry is one 0F 00/01 register-or-memory access. sized marks the
|
||||
// members whose trailing width letter (SLDTW, STRQ, SMSWL) carries the width
|
||||
// prefix ladder; the rest are fixed-width single names.
|
||||
type sysRmEntry struct {
|
||||
group byte
|
||||
digit int
|
||||
sized bool
|
||||
}
|
||||
|
||||
// sysRm maps the system register accesses LLDT/LTR/VERR/VERW/SLDT/STR (group
|
||||
// 0F 00), LMSW/SMSW (0F 01).
|
||||
var sysRm = map[string]sysRmEntry{
|
||||
"LLDT": {0x00, 2, false},
|
||||
"LTR": {0x00, 3, false},
|
||||
"VERR": {0x00, 4, false},
|
||||
"VERW": {0x00, 5, false},
|
||||
"SLDT": {0x00, 0, true},
|
||||
"STR": {0x00, 1, true},
|
||||
"LMSW": {0x01, 6, false},
|
||||
"SMSW": {0x01, 4, true},
|
||||
}
|
||||
|
||||
// selectorRead maps the selector reads LAR and LSL to their opcodes; both
|
||||
// load the destination register from an r/m selector, width prefixes per the
|
||||
// suffix.
|
||||
var selectorRead = map[string]byte{
|
||||
"LAR": 0x02,
|
||||
"LSL": 0x03,
|
||||
}
|
||||
|
||||
// farSegLoad maps the far-segment loads to their opcodes; memory source
|
||||
// alone, destination register, width prefixes per the suffix.
|
||||
var farSegLoad = map[string]byte{
|
||||
"LFS": 0xB4,
|
||||
"LGS": 0xB5,
|
||||
"LSS": 0xB2,
|
||||
}
|
||||
|
||||
// encodeSystem encodes the system, flag, string and segment families. It
|
||||
// reports whether the mnemonic belongs to the family.
|
||||
func (e *enc) encodeSystem(upper string, ops []Operand) (bool, error) {
|
||||
if op, ok := systemNoOperand[upper]; ok {
|
||||
if len(ops) != 0 {
|
||||
return true, fmt.Errorf("%s takes no operands, got %d", upper, len(ops))
|
||||
}
|
||||
return true, e.emit(&instr{opcode: append([]byte(nil), op...), modrm: -1, sib: -1})
|
||||
}
|
||||
// The string primitives carry a B/W/L/Q suffix only; CMPSD and friends
|
||||
// are the SSE compare family's names and must reach their own dispatch.
|
||||
if b, size := splitSize(upper); size != 0 {
|
||||
switch upper[len(upper)-1] {
|
||||
case 'B', 'W', 'L', 'Q':
|
||||
if op32, ok := stringOp[b]; ok {
|
||||
return true, e.encodeSystemString(upper, op32, ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
if size, ok := nopWidth[upper]; ok {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 operand, got %d", upper, len(ops))
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, 0x1F})
|
||||
if err := setRMDigit(i, 0, ops[0], size); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
if m, ok := cacheControl[upper]; ok {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := &instr{prefix: m.prefix, opcode: m.op, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
if _, ok := movbeSize[upper]; ok {
|
||||
return true, e.encodeSystemMovbe(upper, ops)
|
||||
}
|
||||
if digit, ok := randSource[base(upper, 6)]; ok {
|
||||
return true, e.encodeSystemRand(upper, digit, ops)
|
||||
}
|
||||
if digit, ok := fsGsBase[base(upper, 8)]; ok {
|
||||
return true, e.encodeSystemFsGsBase(upper, digit, ops)
|
||||
}
|
||||
if digit, ok := descTable[upper]; ok {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := &instr{opcode: []byte{0x0F, 0x01}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
if m, ok := sysRm[upper]; ok {
|
||||
return true, e.encodeSystemRm(upper, m, ops)
|
||||
}
|
||||
if b, size := splitSize(upper); size != 0 {
|
||||
if m, ok := sysRm[b]; ok && m.sized {
|
||||
return true, e.encodeSystemRm(upper, m, ops)
|
||||
}
|
||||
}
|
||||
if op, ok := selectorRead[base(upper, 3)]; ok {
|
||||
return true, e.encodeSystemSelectorRead(upper, op, ops)
|
||||
}
|
||||
if op, ok := farSegLoad[base(upper, 3)]; ok {
|
||||
return true, e.encodeSystemFarLoad(upper, op, ops)
|
||||
}
|
||||
if upper == "CMPXCHG8B" || upper == "CMPXCHG16B" {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0xC7})
|
||||
i.rexW = upper == "CMPXCHG16B"
|
||||
if err := setRMDigit(i, 1, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// INVPCID invalidates a translation-cache entry: 66 0F38 82 with the
|
||||
// type in a general register and the descriptor in memory.
|
||||
if upper == "INVPCID" {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("INVPCID expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("INVPCID requires a memory descriptor first")
|
||||
}
|
||||
srcReg, ok := ops[1].(Reg)
|
||||
if !ok || srcReg.isVec() {
|
||||
return true, fmt.Errorf("INVPCID: the second operand must be a general register")
|
||||
}
|
||||
i := &instr{prefix: 0x66, opcode: []byte{0x0F, 0x38, 0x82}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, srcReg, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
// XABORT carries its imm8 status byte in the C6 F8 group form.
|
||||
if upper == "XABORT" {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("XABORT expects 1 immediate operand, got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("XABORT requires an immediate")
|
||||
}
|
||||
immByte, err := imm8(int64(imm))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(&instr{opcode: []byte{0xC6, 0xF8}, modrm: -1, sib: -1, imm: []byte{immByte}})
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// base returns the first n characters of an upper-case mnemonic, or the empty
|
||||
// string when the mnemonic is shorter: the safe head lookup for the families
|
||||
// whose width suffix rides the tail (RDRANDW, RDFSBASEQ, LARW).
|
||||
func base(upper string, n int) string {
|
||||
if len(upper) <= n {
|
||||
return ""
|
||||
}
|
||||
return upper[:n]
|
||||
}
|
||||
|
||||
// encodeSystemString encodes a string primitive: no operands, the width
|
||||
// suffix picks the byte form, the 0x66 prefix or REX.W. Only the B/W/L/Q
|
||||
// suffixes belong to the family: CMPSD and friends are the SSE compare
|
||||
// family's names and must reach their own dispatch.
|
||||
func (e *enc) encodeSystemString(upper string, op32 byte, ops []Operand) error {
|
||||
switch upper[len(upper)-1] {
|
||||
case 'B', 'W', 'L', 'Q':
|
||||
default:
|
||||
return fmt.Errorf("unsupported instruction %q", upper)
|
||||
}
|
||||
b, size := splitSize(upper)
|
||||
if _, ok := stringOp[b]; !ok || size == 0 {
|
||||
return fmt.Errorf("unsupported instruction %q", upper)
|
||||
}
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("%s takes no operands, got %d", upper, len(ops))
|
||||
}
|
||||
// The byte spelling is the 32-bit opcode minus one; the W and Q forms
|
||||
// ride newInstr's prefix ladder, the L form the bare opcode.
|
||||
op := op32
|
||||
if size == 1 {
|
||||
op--
|
||||
}
|
||||
return e.emit(newInstr(size, []byte{op}))
|
||||
}
|
||||
|
||||
// encodeSystemMovbe encodes MOVBE: a register source stores (F1, reg = the
|
||||
// register, r/m = memory), a register destination loads (F0, same fields).
|
||||
func (e *enc) encodeSystemMovbe(mnem string, ops []Operand) error {
|
||||
size := movbeSize[mnem]
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
srcReg, srcIsReg := ops[0].(Reg)
|
||||
dstReg, dstIsReg := ops[1].(Reg)
|
||||
var op byte
|
||||
var reg Reg
|
||||
var rm Operand
|
||||
switch {
|
||||
case srcIsReg && isX86Mem(ops[1]):
|
||||
op, reg, rm = 0xF1, srcReg, ops[1] // store
|
||||
case dstIsReg && isX86Mem(ops[0]):
|
||||
op, reg, rm = 0xF0, dstReg, ops[0] // load
|
||||
default:
|
||||
return fmt.Errorf("%s takes one register and one memory operand", mnem)
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, 0x38, op})
|
||||
if err := setRM(i, reg, rm, size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSystemRand encodes RDRAND/RDSEED: the single register operand rides
|
||||
// r/m under the /digit, mod 11, with the width prefix the suffix picks.
|
||||
func (e *enc) encodeSystemRand(mnem string, digit int, ops []Operand) error {
|
||||
b, size := splitSize(mnem)
|
||||
if _, ok := randSource[b]; !ok || size == 0 {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 register operand, got %d", mnem, len(ops))
|
||||
}
|
||||
dstReg, ok := ops[0].(Reg)
|
||||
if !ok || dstReg.isVec() {
|
||||
return fmt.Errorf("%s destination must be a general register", mnem)
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, 0xC7})
|
||||
if err := setRMDigit(i, digit, dstReg, size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSystemFsGsBase encodes the FS/GS base accessors: F3-prefixed 0F AE
|
||||
// under the /digit, the register in r/m; the Q spellings add REX.W.
|
||||
func (e *enc) encodeSystemFsGsBase(mnem string, digit int, ops []Operand) error {
|
||||
b, size := splitSize(mnem)
|
||||
if _, ok := fsGsBase[b]; !ok || (size != 4 && size != 8) {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 register operand, got %d", mnem, len(ops))
|
||||
}
|
||||
dstReg, ok := ops[0].(Reg)
|
||||
if !ok || dstReg.isVec() {
|
||||
return fmt.Errorf("%s destination must be a general register", mnem)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0xAE})
|
||||
i.prefix = 0xF3
|
||||
i.rexW = size == 8
|
||||
if err := setRMDigit(i, digit, dstReg, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSystemRm encodes a 0F 00/01 r/m access: the operand is a register or
|
||||
// memory; the sized members carry the width prefix ladder the suffix fixes.
|
||||
func (e *enc) encodeSystemRm(mnem string, m sysRmEntry, ops []Operand) error {
|
||||
size := 0
|
||||
if m.sized {
|
||||
_, size = splitSize(mnem)
|
||||
if size == 0 {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
}
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, m.group})
|
||||
if err := setRMDigit(i, m.digit, ops[0], size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSystemSelectorRead encodes LAR/LSL: the destination register loads
|
||||
// from an r/m selector, width prefixes per the suffix.
|
||||
func (e *enc) encodeSystemSelectorRead(mnem string, op byte, ops []Operand) error {
|
||||
b, size := splitSize(mnem)
|
||||
if _, ok := selectorRead[b]; !ok || size == 0 {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok || dstReg.isVec() {
|
||||
return fmt.Errorf("%s destination must be a general register", mnem)
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, op})
|
||||
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSystemFarLoad encodes LFS/LGS/LSS: the destination register loads a
|
||||
// far pointer from memory, width prefixes per the suffix.
|
||||
func (e *enc) encodeSystemFarLoad(mnem string, op byte, ops []Operand) error {
|
||||
b, size := splitSize(mnem)
|
||||
if _, ok := farSegLoad[b]; !ok || size == 0 {
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
}
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return fmt.Errorf("%s requires a memory source", mnem)
|
||||
}
|
||||
dstReg, ok := ops[1].(Reg)
|
||||
if !ok || dstReg.isVec() {
|
||||
return fmt.Errorf("%s destination must be a general register", mnem)
|
||||
}
|
||||
i := newInstr(size, []byte{0x0F, op})
|
||||
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "testing"
|
||||
|
||||
// amd64SystemCorpus holds every line the Go toolchain's own
|
||||
// amd64enc.s carries for the system, flag, string and segment families, with the bytes go tool asm
|
||||
// emits for each: the differential ground truth the family is proven
|
||||
// against, line for line.
|
||||
var amd64SystemCorpus = []struct {
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{"CBW", "66 98"},
|
||||
{"CDQ", "99"},
|
||||
{"CDQE", "48 98"},
|
||||
{"CLAC", "0f 01 ca"},
|
||||
{"CLC", "f8"},
|
||||
{"CLFLUSH (BX)", "0f ae 3b"},
|
||||
{"CLFLUSH (R11)", "41 0f ae 3b"},
|
||||
{"CLFLUSHOPT (BX)", "66 0f ae 3b"},
|
||||
{"CLFLUSHOPT (R11)", "66 41 0f ae 3b"},
|
||||
{"CLI", "fa"},
|
||||
{"CLTS", "0f 06"},
|
||||
{"CMC", "f5"},
|
||||
{"CMPSB", "a6"},
|
||||
{"CMPSL", "a7"},
|
||||
{"CMPSQ", "48 a7"},
|
||||
{"CMPSW", "66 a7"},
|
||||
{"CMPXCHG16B (BX)", "48 0f c7 0b"},
|
||||
{"CMPXCHG16B (R11)", "49 0f c7 0b"},
|
||||
{"CMPXCHG8B (BX)", "0f c7 0b"},
|
||||
{"CMPXCHG8B (R11)", "41 0f c7 0b"},
|
||||
{"CQO", "48 99"},
|
||||
{"CWD", "66 99"},
|
||||
{"CWDE", "98"},
|
||||
{"HLT", "f4"},
|
||||
{"ICEBP", "f1"},
|
||||
{"INSB", "6c"},
|
||||
{"INSL", "6d"},
|
||||
{"INSW", "66 6d"},
|
||||
{"INVD", "0f 08"},
|
||||
{"INVLPG (BX)", "0f 01 3b"},
|
||||
{"INVLPG (R11)", "41 0f 01 3b"},
|
||||
{"IRETW", "66 cf"},
|
||||
{"IRETL", "cf"},
|
||||
{"IRETQ", "48 cf"},
|
||||
{"LAHF", "9f"},
|
||||
{"LARW (BX), DX", "66 0f 02 13"},
|
||||
{"LARW (R11), DX", "66 41 0f 02 13"},
|
||||
{"LARW DX, DX", "66 0f 02 d2"},
|
||||
{"LARW R11, DX", "66 41 0f 02 d3"},
|
||||
{"LARW (BX), R11", "66 44 0f 02 1b"},
|
||||
{"LARW (R11), R11", "66 45 0f 02 1b"},
|
||||
{"LARW DX, R11", "66 44 0f 02 da"},
|
||||
{"LARW R11, R11", "66 45 0f 02 db"},
|
||||
{"LARL (BX), DX", "0f 02 13"},
|
||||
{"LARL (R11), DX", "41 0f 02 13"},
|
||||
{"LARL DX, DX", "0f 02 d2"},
|
||||
{"LARL R11, DX", "41 0f 02 d3"},
|
||||
{"LARL (BX), R11", "44 0f 02 1b"},
|
||||
{"LARL (R11), R11", "45 0f 02 1b"},
|
||||
{"LARL DX, R11", "44 0f 02 da"},
|
||||
{"LARL R11, R11", "45 0f 02 db"},
|
||||
{"LARQ (BX), DX", "48 0f 02 13"},
|
||||
{"LARQ (R11), DX", "49 0f 02 13"},
|
||||
{"LARQ DX, DX", "48 0f 02 d2"},
|
||||
{"LARQ R11, DX", "49 0f 02 d3"},
|
||||
{"LARQ (BX), R11", "4c 0f 02 1b"},
|
||||
{"LARQ (R11), R11", "4d 0f 02 1b"},
|
||||
{"LARQ DX, R11", "4c 0f 02 da"},
|
||||
{"LARQ R11, R11", "4d 0f 02 db"},
|
||||
{"LFSW (BX), DX", "66 0f b4 13"},
|
||||
{"LFSW (R11), DX", "66 41 0f b4 13"},
|
||||
{"LFSW (BX), R11", "66 44 0f b4 1b"},
|
||||
{"LFSW (R11), R11", "66 45 0f b4 1b"},
|
||||
{"LFSL (BX), DX", "0f b4 13"},
|
||||
{"LFSL (R11), DX", "41 0f b4 13"},
|
||||
{"LFSL (BX), R11", "44 0f b4 1b"},
|
||||
{"LFSL (R11), R11", "45 0f b4 1b"},
|
||||
{"LFSQ (BX), DX", "48 0f b4 13"},
|
||||
{"LFSQ (R11), DX", "49 0f b4 13"},
|
||||
{"LFSQ (BX), R11", "4c 0f b4 1b"},
|
||||
{"LFSQ (R11), R11", "4d 0f b4 1b"},
|
||||
{"LGDT (BX)", "0f 01 13"},
|
||||
{"LGDT (R11)", "41 0f 01 13"},
|
||||
{"LGSW (BX), DX", "66 0f b5 13"},
|
||||
{"LGSW (R11), DX", "66 41 0f b5 13"},
|
||||
{"LGSW (BX), R11", "66 44 0f b5 1b"},
|
||||
{"LGSW (R11), R11", "66 45 0f b5 1b"},
|
||||
{"LGSL (BX), DX", "0f b5 13"},
|
||||
{"LGSL (R11), DX", "41 0f b5 13"},
|
||||
{"LGSL (BX), R11", "44 0f b5 1b"},
|
||||
{"LGSL (R11), R11", "45 0f b5 1b"},
|
||||
{"LGSQ (BX), DX", "48 0f b5 13"},
|
||||
{"LGSQ (R11), DX", "49 0f b5 13"},
|
||||
{"LGSQ (BX), R11", "4c 0f b5 1b"},
|
||||
{"LGSQ (R11), R11", "4d 0f b5 1b"},
|
||||
{"LIDT (BX)", "0f 01 1b"},
|
||||
{"LIDT (R11)", "41 0f 01 1b"},
|
||||
{"LLDT (BX)", "0f 00 13"},
|
||||
{"LLDT (R11)", "41 0f 00 13"},
|
||||
{"LLDT DX", "0f 00 d2"},
|
||||
{"LLDT R11", "41 0f 00 d3"},
|
||||
{"LMSW (BX)", "0f 01 33"},
|
||||
{"LMSW (R11)", "41 0f 01 33"},
|
||||
{"LMSW DX", "0f 01 f2"},
|
||||
{"LMSW R11", "41 0f 01 f3"},
|
||||
{"LODSB", "ac"},
|
||||
{"LODSL", "ad"},
|
||||
{"LODSQ", "48 ad"},
|
||||
{"LODSW", "66 ad"},
|
||||
{"LSLW (BX), DX", "66 0f 03 13"},
|
||||
{"LSLW (R11), DX", "66 41 0f 03 13"},
|
||||
{"LSLW DX, DX", "66 0f 03 d2"},
|
||||
{"LSLW R11, DX", "66 41 0f 03 d3"},
|
||||
{"LSLW (BX), R11", "66 44 0f 03 1b"},
|
||||
{"LSLW (R11), R11", "66 45 0f 03 1b"},
|
||||
{"LSLW DX, R11", "66 44 0f 03 da"},
|
||||
{"LSLW R11, R11", "66 45 0f 03 db"},
|
||||
{"LSLL (BX), DX", "0f 03 13"},
|
||||
{"LSLL (R11), DX", "41 0f 03 13"},
|
||||
{"LSLL DX, DX", "0f 03 d2"},
|
||||
{"LSLL R11, DX", "41 0f 03 d3"},
|
||||
{"LSLL (BX), R11", "44 0f 03 1b"},
|
||||
{"LSLL (R11), R11", "45 0f 03 1b"},
|
||||
{"LSLL DX, R11", "44 0f 03 da"},
|
||||
{"LSLL R11, R11", "45 0f 03 db"},
|
||||
{"LSLQ (BX), DX", "48 0f 03 13"},
|
||||
{"LSLQ (R11), DX", "49 0f 03 13"},
|
||||
{"LSLQ DX, DX", "48 0f 03 d2"},
|
||||
{"LSLQ R11, DX", "49 0f 03 d3"},
|
||||
{"LSLQ (BX), R11", "4c 0f 03 1b"},
|
||||
{"LSLQ (R11), R11", "4d 0f 03 1b"},
|
||||
{"LSLQ DX, R11", "4c 0f 03 da"},
|
||||
{"LSLQ R11, R11", "4d 0f 03 db"},
|
||||
{"LSSW (BX), DX", "66 0f b2 13"},
|
||||
{"LSSW (R11), DX", "66 41 0f b2 13"},
|
||||
{"LSSW (BX), R11", "66 44 0f b2 1b"},
|
||||
{"LSSW (R11), R11", "66 45 0f b2 1b"},
|
||||
{"LSSL (BX), DX", "0f b2 13"},
|
||||
{"LSSL (R11), DX", "41 0f b2 13"},
|
||||
{"LSSL (BX), R11", "44 0f b2 1b"},
|
||||
{"LSSL (R11), R11", "45 0f b2 1b"},
|
||||
{"LSSQ (BX), DX", "48 0f b2 13"},
|
||||
{"LSSQ (R11), DX", "49 0f b2 13"},
|
||||
{"LSSQ (BX), R11", "4c 0f b2 1b"},
|
||||
{"LSSQ (R11), R11", "4d 0f b2 1b"},
|
||||
{"LTR (BX)", "0f 00 1b"},
|
||||
{"LTR (R11)", "41 0f 00 1b"},
|
||||
{"LTR DX", "0f 00 da"},
|
||||
{"LTR R11", "41 0f 00 db"},
|
||||
{"MONITOR", "0f 01 c8"},
|
||||
{"MOVBEW DX, (BX)", "66 0f 38 f1 13"},
|
||||
{"MOVBEW R11, (BX)", "66 44 0f 38 f1 1b"},
|
||||
{"MOVBEW DX, (R11)", "66 41 0f 38 f1 13"},
|
||||
{"MOVBEW R11, (R11)", "66 45 0f 38 f1 1b"},
|
||||
{"MOVBEW (BX), DX", "66 0f 38 f0 13"},
|
||||
{"MOVBEW (R11), DX", "66 41 0f 38 f0 13"},
|
||||
{"MOVBEW (BX), R11", "66 44 0f 38 f0 1b"},
|
||||
{"MOVBEW (R11), R11", "66 45 0f 38 f0 1b"},
|
||||
{"MOVBEL DX, (BX)", "0f 38 f1 13"},
|
||||
{"MOVBEL R11, (BX)", "44 0f 38 f1 1b"},
|
||||
{"MOVBEL DX, (R11)", "41 0f 38 f1 13"},
|
||||
{"MOVBEL R11, (R11)", "45 0f 38 f1 1b"},
|
||||
{"MOVBEL (BX), DX", "0f 38 f0 13"},
|
||||
{"MOVBEL (R11), DX", "41 0f 38 f0 13"},
|
||||
{"MOVBEL (BX), R11", "44 0f 38 f0 1b"},
|
||||
{"MOVBEL (R11), R11", "45 0f 38 f0 1b"},
|
||||
{"MOVBEQ DX, (BX)", "48 0f 38 f1 13"},
|
||||
{"MOVBEQ R11, (BX)", "4c 0f 38 f1 1b"},
|
||||
{"MOVBEQ DX, (R11)", "49 0f 38 f1 13"},
|
||||
{"MOVBEQ R11, (R11)", "4d 0f 38 f1 1b"},
|
||||
{"MOVBEQ (BX), DX", "48 0f 38 f0 13"},
|
||||
{"MOVBEQ (R11), DX", "49 0f 38 f0 13"},
|
||||
{"MOVBEQ (BX), R11", "4c 0f 38 f0 1b"},
|
||||
{"MOVBEQ (R11), R11", "4d 0f 38 f0 1b"},
|
||||
{"MWAIT", "0f 01 c9"},
|
||||
{"NOPW (BX)", "66 0f 1f 03"},
|
||||
{"NOPW (R11)", "66 41 0f 1f 03"},
|
||||
{"NOPW DX", "66 0f 1f c2"},
|
||||
{"NOPW R11", "66 41 0f 1f c3"},
|
||||
{"NOPL (BX)", "0f 1f 03"},
|
||||
{"NOPL (R11)", "41 0f 1f 03"},
|
||||
{"NOPL DX", "0f 1f c2"},
|
||||
{"NOPL R11", "41 0f 1f c3"},
|
||||
{"OUTSB", "6e"},
|
||||
{"OUTSL", "6f"},
|
||||
{"OUTSW", "66 6f"},
|
||||
{"POPFW", "66 9d"},
|
||||
{"PUSHFW", "66 9c"},
|
||||
{"RDFSBASEL DX", "f3 0f ae c2"},
|
||||
{"RDFSBASEL R11", "f3 41 0f ae c3"},
|
||||
{"RDGSBASEL DX", "f3 0f ae ca"},
|
||||
{"RDGSBASEL R11", "f3 41 0f ae cb"},
|
||||
{"RDFSBASEQ DX", "f3 48 0f ae c2"},
|
||||
{"RDFSBASEQ R11", "f3 49 0f ae c3"},
|
||||
{"RDGSBASEQ DX", "f3 48 0f ae ca"},
|
||||
{"RDGSBASEQ R11", "f3 49 0f ae cb"},
|
||||
{"RDMSR", "0f 32"},
|
||||
{"RDPKRU", "0f 01 ee"},
|
||||
{"RDPMC", "0f 33"},
|
||||
{"RDRANDW DX", "66 0f c7 f2"},
|
||||
{"RDRANDW R11", "66 41 0f c7 f3"},
|
||||
{"RDRANDL DX", "0f c7 f2"},
|
||||
{"RDRANDL R11", "41 0f c7 f3"},
|
||||
{"RDRANDQ DX", "48 0f c7 f2"},
|
||||
{"RDRANDQ R11", "49 0f c7 f3"},
|
||||
{"RDSEEDW DX", "66 0f c7 fa"},
|
||||
{"RDSEEDW R11", "66 41 0f c7 fb"},
|
||||
{"RDSEEDL DX", "0f c7 fa"},
|
||||
{"RDSEEDL R11", "41 0f c7 fb"},
|
||||
{"RDSEEDQ DX", "48 0f c7 fa"},
|
||||
{"RDSEEDQ R11", "49 0f c7 fb"},
|
||||
{"RSM", "0f aa"},
|
||||
{"SAHF", "9e"},
|
||||
{"SCASB", "ae"},
|
||||
{"SCASL", "af"},
|
||||
{"SCASQ", "48 af"},
|
||||
{"SCASW", "66 af"},
|
||||
{"SGDT (BX)", "0f 01 03"},
|
||||
{"SGDT (R11)", "41 0f 01 03"},
|
||||
{"SIDT (BX)", "0f 01 0b"},
|
||||
{"SIDT (R11)", "41 0f 01 0b"},
|
||||
{"SLDTW (BX)", "66 0f 00 03"},
|
||||
{"SLDTW (R11)", "66 41 0f 00 03"},
|
||||
{"SLDTW DX", "66 0f 00 c2"},
|
||||
{"SLDTW R11", "66 41 0f 00 c3"},
|
||||
{"SLDTL (BX)", "0f 00 03"},
|
||||
{"SLDTL (R11)", "41 0f 00 03"},
|
||||
{"SLDTL DX", "0f 00 c2"},
|
||||
{"SLDTL R11", "41 0f 00 c3"},
|
||||
{"SLDTQ (BX)", "48 0f 00 03"},
|
||||
{"SLDTQ (R11)", "49 0f 00 03"},
|
||||
{"SLDTQ DX", "48 0f 00 c2"},
|
||||
{"SLDTQ R11", "49 0f 00 c3"},
|
||||
{"SMSWW (BX)", "66 0f 01 23"},
|
||||
{"SMSWW (R11)", "66 41 0f 01 23"},
|
||||
{"SMSWW DX", "66 0f 01 e2"},
|
||||
{"SMSWW R11", "66 41 0f 01 e3"},
|
||||
{"SMSWL (BX)", "0f 01 23"},
|
||||
{"SMSWL (R11)", "41 0f 01 23"},
|
||||
{"SMSWL DX", "0f 01 e2"},
|
||||
{"SMSWL R11", "41 0f 01 e3"},
|
||||
{"SMSWQ (BX)", "48 0f 01 23"},
|
||||
{"SMSWQ (R11)", "49 0f 01 23"},
|
||||
{"SMSWQ DX", "48 0f 01 e2"},
|
||||
{"SMSWQ R11", "49 0f 01 e3"},
|
||||
{"STAC", "0f 01 cb"},
|
||||
{"STC", "f9"},
|
||||
{"STI", "fb"},
|
||||
{"STRW (BX)", "66 0f 00 0b"},
|
||||
{"STRW (R11)", "66 41 0f 00 0b"},
|
||||
{"STRW DX", "66 0f 00 ca"},
|
||||
{"STRW R11", "66 41 0f 00 cb"},
|
||||
{"STRL (BX)", "0f 00 0b"},
|
||||
{"STRL (R11)", "41 0f 00 0b"},
|
||||
{"STRL DX", "0f 00 ca"},
|
||||
{"STRL R11", "41 0f 00 cb"},
|
||||
{"STRQ (BX)", "48 0f 00 0b"},
|
||||
{"STRQ (R11)", "49 0f 00 0b"},
|
||||
{"STRQ DX", "48 0f 00 ca"},
|
||||
{"STRQ R11", "49 0f 00 cb"},
|
||||
{"SWAPGS", "0f 01 f8"},
|
||||
{"SYSENTER", "0f 34"},
|
||||
{"SYSENTER64", "48 0f 34"},
|
||||
{"SYSEXIT", "0f 35"},
|
||||
{"SYSEXIT64", "48 0f 35"},
|
||||
{"SYSRET", "0f 07"},
|
||||
{"UD1", "0f b9"},
|
||||
{"UD2", "0f 0b"},
|
||||
{"VERR (BX)", "0f 00 23"},
|
||||
{"VERR (R11)", "41 0f 00 23"},
|
||||
{"VERR DX", "0f 00 e2"},
|
||||
{"VERR R11", "41 0f 00 e3"},
|
||||
{"VERW (BX)", "0f 00 2b"},
|
||||
{"VERW (R11)", "41 0f 00 2b"},
|
||||
{"VERW DX", "0f 00 ea"},
|
||||
{"VERW R11", "41 0f 00 eb"},
|
||||
{"WBINVD", "0f 09"},
|
||||
{"WRFSBASEL DX", "f3 0f ae d2"},
|
||||
{"WRFSBASEL R11", "f3 41 0f ae d3"},
|
||||
{"WRGSBASEL DX", "f3 0f ae da"},
|
||||
{"WRGSBASEL R11", "f3 41 0f ae db"},
|
||||
{"WRFSBASEQ DX", "f3 48 0f ae d2"},
|
||||
{"WRFSBASEQ R11", "f3 49 0f ae d3"},
|
||||
{"WRGSBASEQ DX", "f3 48 0f ae da"},
|
||||
{"WRGSBASEQ R11", "f3 49 0f ae db"},
|
||||
{"WRMSR", "0f 30"},
|
||||
{"WRPKRU", "0f 01 ef"},
|
||||
{"XLAT", "d7"},
|
||||
{"XSETBV", "0f 01 d1"},
|
||||
}
|
||||
|
||||
// TestAmd64SystemCorpus assembles every corpus line and requires the same bytes
|
||||
// go tool asm emits for it.
|
||||
func TestAmd64SystemCorpus(t *testing.T) {
|
||||
for _, tc := range amd64SystemCorpus {
|
||||
fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n")
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.line, err)
|
||||
continue
|
||||
}
|
||||
if got := hexBytes(code); got != tc.want {
|
||||
t.Errorf("%s: got %s, want %s", tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "testing"
|
||||
|
||||
// amd64VexCorpus holds every line the Go toolchain's own
|
||||
// amd64enc.s carries for the VEX (AVX/AVX2) families and the general-register count forms, with the bytes go
|
||||
// tool asm emits for each: the differential ground truth the layer is
|
||||
// proven against, line for line.
|
||||
var amd64VexCorpus = []struct {
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{"BEXTRL R9, (BX), DX", "c4 e2 30 f7 13"},
|
||||
{"BEXTRL R9, (BX), R11", "c4 62 30 f7 1b"},
|
||||
{"BEXTRL R9, (R11), DX", "c4 c2 30 f7 13"},
|
||||
{"BEXTRL R9, (R11), R11", "c4 42 30 f7 1b"},
|
||||
{"BEXTRL R9, DX, DX", "c4 e2 30 f7 d2"},
|
||||
{"BEXTRL R9, DX, R11", "c4 62 30 f7 da"},
|
||||
{"BEXTRL R9, R11, DX", "c4 c2 30 f7 d3"},
|
||||
{"BEXTRL R9, R11, R11", "c4 42 30 f7 db"},
|
||||
{"BEXTRQ R14, (BX), DX", "c4 e2 88 f7 13"},
|
||||
{"BEXTRQ R14, (BX), R11", "c4 62 88 f7 1b"},
|
||||
{"BEXTRQ R14, (R11), DX", "c4 c2 88 f7 13"},
|
||||
{"BEXTRQ R14, (R11), R11", "c4 42 88 f7 1b"},
|
||||
{"BEXTRQ R14, DX, DX", "c4 e2 88 f7 d2"},
|
||||
{"BEXTRQ R14, DX, R11", "c4 62 88 f7 da"},
|
||||
{"BEXTRQ R14, R11, DX", "c4 c2 88 f7 d3"},
|
||||
{"BEXTRQ R14, R11, R11", "c4 42 88 f7 db"},
|
||||
{"BZHIL R9, (BX), DX", "c4 e2 30 f5 13"},
|
||||
{"BZHIL R9, (BX), R11", "c4 62 30 f5 1b"},
|
||||
{"BZHIL R9, (R11), DX", "c4 c2 30 f5 13"},
|
||||
{"BZHIL R9, (R11), R11", "c4 42 30 f5 1b"},
|
||||
{"BZHIL R9, DX, DX", "c4 e2 30 f5 d2"},
|
||||
{"BZHIL R9, DX, R11", "c4 62 30 f5 da"},
|
||||
{"BZHIL R9, R11, DX", "c4 c2 30 f5 d3"},
|
||||
{"BZHIL R9, R11, R11", "c4 42 30 f5 db"},
|
||||
{"BZHIQ R14, (BX), DX", "c4 e2 88 f5 13"},
|
||||
{"BZHIQ R14, (BX), R11", "c4 62 88 f5 1b"},
|
||||
{"BZHIQ R14, (R11), DX", "c4 c2 88 f5 13"},
|
||||
{"BZHIQ R14, (R11), R11", "c4 42 88 f5 1b"},
|
||||
{"BZHIQ R14, DX, DX", "c4 e2 88 f5 d2"},
|
||||
{"BZHIQ R14, DX, R11", "c4 62 88 f5 da"},
|
||||
{"BZHIQ R14, R11, DX", "c4 c2 88 f5 d3"},
|
||||
{"BZHIQ R14, R11, R11", "c4 42 88 f5 db"},
|
||||
{"SARXL R9, (BX), DX", "c4 e2 32 f7 13"},
|
||||
{"SARXL R9, (BX), R11", "c4 62 32 f7 1b"},
|
||||
{"SARXL R9, (R11), DX", "c4 c2 32 f7 13"},
|
||||
{"SARXL R9, (R11), R11", "c4 42 32 f7 1b"},
|
||||
{"SARXL R9, DX, DX", "c4 e2 32 f7 d2"},
|
||||
{"SARXL R9, DX, R11", "c4 62 32 f7 da"},
|
||||
{"SARXL R9, R11, DX", "c4 c2 32 f7 d3"},
|
||||
{"SARXL R9, R11, R11", "c4 42 32 f7 db"},
|
||||
{"SARXQ R14, (BX), DX", "c4 e2 8a f7 13"},
|
||||
{"SARXQ R14, (BX), R11", "c4 62 8a f7 1b"},
|
||||
{"SARXQ R14, (R11), DX", "c4 c2 8a f7 13"},
|
||||
{"SARXQ R14, (R11), R11", "c4 42 8a f7 1b"},
|
||||
{"SARXQ R14, DX, DX", "c4 e2 8a f7 d2"},
|
||||
{"SARXQ R14, DX, R11", "c4 62 8a f7 da"},
|
||||
{"SARXQ R14, R11, DX", "c4 c2 8a f7 d3"},
|
||||
{"SARXQ R14, R11, R11", "c4 42 8a f7 db"},
|
||||
{"SHLXL R9, (BX), DX", "c4 e2 31 f7 13"},
|
||||
{"SHLXL R9, (BX), R11", "c4 62 31 f7 1b"},
|
||||
{"SHLXL R9, (R11), DX", "c4 c2 31 f7 13"},
|
||||
{"SHLXL R9, (R11), R11", "c4 42 31 f7 1b"},
|
||||
{"SHLXL R9, DX, DX", "c4 e2 31 f7 d2"},
|
||||
{"SHLXL R9, DX, R11", "c4 62 31 f7 da"},
|
||||
{"SHLXL R9, R11, DX", "c4 c2 31 f7 d3"},
|
||||
{"SHLXL R9, R11, R11", "c4 42 31 f7 db"},
|
||||
{"SHLXQ R14, (BX), DX", "c4 e2 89 f7 13"},
|
||||
{"SHLXQ R14, (BX), R11", "c4 62 89 f7 1b"},
|
||||
{"SHLXQ R14, (R11), DX", "c4 c2 89 f7 13"},
|
||||
{"SHLXQ R14, (R11), R11", "c4 42 89 f7 1b"},
|
||||
{"SHLXQ R14, DX, DX", "c4 e2 89 f7 d2"},
|
||||
{"SHLXQ R14, DX, R11", "c4 62 89 f7 da"},
|
||||
{"SHLXQ R14, R11, DX", "c4 c2 89 f7 d3"},
|
||||
{"SHLXQ R14, R11, R11", "c4 42 89 f7 db"},
|
||||
{"SHRXL R9, (BX), DX", "c4 e2 33 f7 13"},
|
||||
{"SHRXL R9, (BX), R11", "c4 62 33 f7 1b"},
|
||||
{"SHRXL R9, (R11), DX", "c4 c2 33 f7 13"},
|
||||
{"SHRXL R9, (R11), R11", "c4 42 33 f7 1b"},
|
||||
{"SHRXL R9, DX, DX", "c4 e2 33 f7 d2"},
|
||||
{"SHRXL R9, DX, R11", "c4 62 33 f7 da"},
|
||||
{"SHRXL R9, R11, DX", "c4 c2 33 f7 d3"},
|
||||
{"SHRXL R9, R11, R11", "c4 42 33 f7 db"},
|
||||
{"SHRXQ R14, (BX), DX", "c4 e2 8b f7 13"},
|
||||
{"SHRXQ R14, (BX), R11", "c4 62 8b f7 1b"},
|
||||
{"SHRXQ R14, (R11), DX", "c4 c2 8b f7 13"},
|
||||
{"SHRXQ R14, (R11), R11", "c4 42 8b f7 1b"},
|
||||
{"SHRXQ R14, DX, DX", "c4 e2 8b f7 d2"},
|
||||
{"SHRXQ R14, DX, R11", "c4 62 8b f7 da"},
|
||||
{"SHRXQ R14, R11, DX", "c4 c2 8b f7 d3"},
|
||||
{"SHRXQ R14, R11, R11", "c4 42 8b f7 db"},
|
||||
{"VADDSUBPD (BX), X9, X11", "c5 31 d0 1b"},
|
||||
{"VADDSUBPD (BX), X9, X2", "c5 b1 d0 13"},
|
||||
{"VADDSUBPD (BX), Y15, Y11", "c5 05 d0 1b"},
|
||||
{"VADDSUBPD (BX), Y15, Y2", "c5 85 d0 13"},
|
||||
{"VADDSUBPD (R11), X9, X11", "c4 41 31 d0 1b"},
|
||||
{"VADDSUBPD (R11), X9, X2", "c4 c1 31 d0 13"},
|
||||
{"VADDSUBPD (R11), Y15, Y11", "c4 41 05 d0 1b"},
|
||||
{"VADDSUBPD (R11), Y15, Y2", "c4 c1 05 d0 13"},
|
||||
{"VADDSUBPD X11, X9, X11", "c4 41 31 d0 db"},
|
||||
{"VADDSUBPD X11, X9, X2", "c4 c1 31 d0 d3"},
|
||||
{"VADDSUBPD X2, X9, X11", "c5 31 d0 da"},
|
||||
{"VADDSUBPD X2, X9, X2", "c5 b1 d0 d2"},
|
||||
{"VADDSUBPD Y11, Y15, Y11", "c4 41 05 d0 db"},
|
||||
{"VADDSUBPD Y11, Y15, Y2", "c4 c1 05 d0 d3"},
|
||||
{"VADDSUBPD Y2, Y15, Y11", "c5 05 d0 da"},
|
||||
{"VADDSUBPD Y2, Y15, Y2", "c5 85 d0 d2"},
|
||||
{"VADDSUBPS (BX), X9, X11", "c5 33 d0 1b"},
|
||||
{"VADDSUBPS (BX), X9, X2", "c5 b3 d0 13"},
|
||||
{"VADDSUBPS (BX), Y15, Y11", "c5 07 d0 1b"},
|
||||
{"VADDSUBPS (BX), Y15, Y2", "c5 87 d0 13"},
|
||||
{"VADDSUBPS (R11), X9, X11", "c4 41 33 d0 1b"},
|
||||
{"VADDSUBPS (R11), X9, X2", "c4 c1 33 d0 13"},
|
||||
{"VADDSUBPS (R11), Y15, Y11", "c4 41 07 d0 1b"},
|
||||
{"VADDSUBPS (R11), Y15, Y2", "c4 c1 07 d0 13"},
|
||||
{"VADDSUBPS X11, X9, X11", "c4 41 33 d0 db"},
|
||||
{"VADDSUBPS X11, X9, X2", "c4 c1 33 d0 d3"},
|
||||
{"VADDSUBPS X2, X9, X11", "c5 33 d0 da"},
|
||||
{"VADDSUBPS X2, X9, X2", "c5 b3 d0 d2"},
|
||||
{"VADDSUBPS Y11, Y15, Y11", "c4 41 07 d0 db"},
|
||||
{"VADDSUBPS Y11, Y15, Y2", "c4 c1 07 d0 d3"},
|
||||
{"VADDSUBPS Y2, Y15, Y11", "c5 07 d0 da"},
|
||||
{"VADDSUBPS Y2, Y15, Y2", "c5 87 d0 d2"},
|
||||
{"VAESIMC (BX), X11", "c4 62 79 db 1b"},
|
||||
{"VAESIMC (BX), X2", "c4 e2 79 db 13"},
|
||||
{"VAESIMC (R11), X11", "c4 42 79 db 1b"},
|
||||
{"VAESIMC (R11), X2", "c4 c2 79 db 13"},
|
||||
{"VAESIMC X11, X11", "c4 42 79 db db"},
|
||||
{"VAESIMC X11, X2", "c4 c2 79 db d3"},
|
||||
{"VAESIMC X2, X11", "c4 62 79 db da"},
|
||||
{"VAESIMC X2, X2", "c4 e2 79 db d2"},
|
||||
{"VBLENDPD $7, (BX), X9, X11", "c4 63 31 0d 1b 07"},
|
||||
{"VBLENDPD $7, (BX), X9, X2", "c4 e3 31 0d 13 07"},
|
||||
{"VBLENDPD $7, (BX), Y15, Y11", "c4 63 05 0d 1b 07"},
|
||||
{"VBLENDPD $7, (BX), Y15, Y2", "c4 e3 05 0d 13 07"},
|
||||
{"VBLENDPD $7, (R11), X9, X11", "c4 43 31 0d 1b 07"},
|
||||
{"VBLENDPD $7, (R11), X9, X2", "c4 c3 31 0d 13 07"},
|
||||
{"VBLENDPD $7, (R11), Y15, Y11", "c4 43 05 0d 1b 07"},
|
||||
{"VBLENDPD $7, (R11), Y15, Y2", "c4 c3 05 0d 13 07"},
|
||||
{"VBLENDPD $7, X11, X9, X11", "c4 43 31 0d db 07"},
|
||||
{"VBLENDPD $7, X11, X9, X2", "c4 c3 31 0d d3 07"},
|
||||
{"VBLENDPD $7, X2, X9, X11", "c4 63 31 0d da 07"},
|
||||
{"VBLENDPD $7, X2, X9, X2", "c4 e3 31 0d d2 07"},
|
||||
{"VBLENDPD $7, Y11, Y15, Y11", "c4 43 05 0d db 07"},
|
||||
{"VBLENDPD $7, Y11, Y15, Y2", "c4 c3 05 0d d3 07"},
|
||||
{"VBLENDPD $7, Y2, Y15, Y11", "c4 63 05 0d da 07"},
|
||||
{"VBLENDPD $7, Y2, Y15, Y2", "c4 e3 05 0d d2 07"},
|
||||
{"VBLENDPS $7, (BX), X9, X11", "c4 63 31 0c 1b 07"},
|
||||
{"VBLENDPS $7, (BX), X9, X2", "c4 e3 31 0c 13 07"},
|
||||
{"VBLENDPS $7, (BX), Y15, Y11", "c4 63 05 0c 1b 07"},
|
||||
{"VBLENDPS $7, (BX), Y15, Y2", "c4 e3 05 0c 13 07"},
|
||||
{"VBLENDPS $7, (R11), X9, X11", "c4 43 31 0c 1b 07"},
|
||||
{"VBLENDPS $7, (R11), X9, X2", "c4 c3 31 0c 13 07"},
|
||||
{"VBLENDPS $7, (R11), Y15, Y11", "c4 43 05 0c 1b 07"},
|
||||
{"VBLENDPS $7, (R11), Y15, Y2", "c4 c3 05 0c 13 07"},
|
||||
{"VBLENDPS $7, X11, X9, X11", "c4 43 31 0c db 07"},
|
||||
{"VBLENDPS $7, X11, X9, X2", "c4 c3 31 0c d3 07"},
|
||||
{"VBLENDPS $7, X2, X9, X11", "c4 63 31 0c da 07"},
|
||||
{"VBLENDPS $7, X2, X9, X2", "c4 e3 31 0c d2 07"},
|
||||
{"VBLENDPS $7, Y11, Y15, Y11", "c4 43 05 0c db 07"},
|
||||
{"VBLENDPS $7, Y11, Y15, Y2", "c4 c3 05 0c d3 07"},
|
||||
{"VBLENDPS $7, Y2, Y15, Y11", "c4 63 05 0c da 07"},
|
||||
{"VBLENDPS $7, Y2, Y15, Y2", "c4 e3 05 0c d2 07"},
|
||||
{"VBLENDVPD X12, (BX), X9, X11", "c4 63 31 4b 1b c0"},
|
||||
{"VBLENDVPD X12, (BX), X9, X2", "c4 e3 31 4b 13 c0"},
|
||||
{"VBLENDVPD X12, (R11), X9, X11", "c4 43 31 4b 1b c0"},
|
||||
{"VBLENDVPD X12, (R11), X9, X2", "c4 c3 31 4b 13 c0"},
|
||||
{"VBLENDVPD X12, X11, X9, X11", "c4 43 31 4b db c0"},
|
||||
{"VBLENDVPD X12, X11, X9, X2", "c4 c3 31 4b d3 c0"},
|
||||
{"VBLENDVPD X12, X2, X9, X11", "c4 63 31 4b da c0"},
|
||||
{"VBLENDVPD X12, X2, X9, X2", "c4 e3 31 4b d2 c0"},
|
||||
{"VBLENDVPD Y13, (BX), Y15, Y11", "c4 63 05 4b 1b d0"},
|
||||
{"VBLENDVPD Y13, (BX), Y15, Y2", "c4 e3 05 4b 13 d0"},
|
||||
{"VBLENDVPD Y13, (R11), Y15, Y11", "c4 43 05 4b 1b d0"},
|
||||
{"VBLENDVPD Y13, (R11), Y15, Y2", "c4 c3 05 4b 13 d0"},
|
||||
{"VBLENDVPD Y13, Y11, Y15, Y11", "c4 43 05 4b db d0"},
|
||||
{"VBLENDVPD Y13, Y11, Y15, Y2", "c4 c3 05 4b d3 d0"},
|
||||
{"VBLENDVPD Y13, Y2, Y15, Y11", "c4 63 05 4b da d0"},
|
||||
{"VBLENDVPD Y13, Y2, Y15, Y2", "c4 e3 05 4b d2 d0"},
|
||||
{"VBLENDVPS X12, (BX), X9, X11", "c4 63 31 4a 1b c0"},
|
||||
{"VBLENDVPS X12, (BX), X9, X2", "c4 e3 31 4a 13 c0"},
|
||||
{"VBLENDVPS X12, (R11), X9, X11", "c4 43 31 4a 1b c0"},
|
||||
{"VBLENDVPS X12, (R11), X9, X2", "c4 c3 31 4a 13 c0"},
|
||||
{"VBLENDVPS X12, X11, X9, X11", "c4 43 31 4a db c0"},
|
||||
{"VBLENDVPS X12, X11, X9, X2", "c4 c3 31 4a d3 c0"},
|
||||
{"VBLENDVPS X12, X2, X9, X11", "c4 63 31 4a da c0"},
|
||||
{"VBLENDVPS X12, X2, X9, X2", "c4 e3 31 4a d2 c0"},
|
||||
{"VBLENDVPS Y13, (BX), Y15, Y11", "c4 63 05 4a 1b d0"},
|
||||
{"VBLENDVPS Y13, (BX), Y15, Y2", "c4 e3 05 4a 13 d0"},
|
||||
{"VBLENDVPS Y13, (R11), Y15, Y11", "c4 43 05 4a 1b d0"},
|
||||
{"VBLENDVPS Y13, (R11), Y15, Y2", "c4 c3 05 4a 13 d0"},
|
||||
{"VBLENDVPS Y13, Y11, Y15, Y11", "c4 43 05 4a db d0"},
|
||||
{"VBLENDVPS Y13, Y11, Y15, Y2", "c4 c3 05 4a d3 d0"},
|
||||
{"VBLENDVPS Y13, Y2, Y15, Y11", "c4 63 05 4a da d0"},
|
||||
{"VBLENDVPS Y13, Y2, Y15, Y2", "c4 e3 05 4a d2 d0"},
|
||||
{"VBROADCASTF128 (BX), Y11", "c4 62 7d 1a 1b"},
|
||||
{"VBROADCASTF128 (BX), Y2", "c4 e2 7d 1a 13"},
|
||||
{"VBROADCASTF128 (R11), Y11", "c4 42 7d 1a 1b"},
|
||||
{"VBROADCASTF128 (R11), Y2", "c4 c2 7d 1a 13"},
|
||||
{"VDPPD $7, (BX), X9, X11", "c4 63 31 41 1b 07"},
|
||||
{"VDPPD $7, (BX), X9, X2", "c4 e3 31 41 13 07"},
|
||||
{"VDPPD $7, (R11), X9, X11", "c4 43 31 41 1b 07"},
|
||||
{"VDPPD $7, (R11), X9, X2", "c4 c3 31 41 13 07"},
|
||||
{"VDPPD $7, X11, X9, X11", "c4 43 31 41 db 07"},
|
||||
{"VDPPD $7, X11, X9, X2", "c4 c3 31 41 d3 07"},
|
||||
{"VDPPD $7, X2, X9, X11", "c4 63 31 41 da 07"},
|
||||
{"VDPPD $7, X2, X9, X2", "c4 e3 31 41 d2 07"},
|
||||
{"VDPPS $7, (BX), X9, X11", "c4 63 31 40 1b 07"},
|
||||
{"VDPPS $7, (BX), X9, X2", "c4 e3 31 40 13 07"},
|
||||
{"VDPPS $7, (BX), Y15, Y11", "c4 63 05 40 1b 07"},
|
||||
{"VDPPS $7, (BX), Y15, Y2", "c4 e3 05 40 13 07"},
|
||||
{"VDPPS $7, (R11), X9, X11", "c4 43 31 40 1b 07"},
|
||||
{"VDPPS $7, (R11), X9, X2", "c4 c3 31 40 13 07"},
|
||||
{"VDPPS $7, (R11), Y15, Y11", "c4 43 05 40 1b 07"},
|
||||
{"VDPPS $7, (R11), Y15, Y2", "c4 c3 05 40 13 07"},
|
||||
{"VDPPS $7, X11, X9, X11", "c4 43 31 40 db 07"},
|
||||
{"VDPPS $7, X11, X9, X2", "c4 c3 31 40 d3 07"},
|
||||
{"VDPPS $7, X2, X9, X11", "c4 63 31 40 da 07"},
|
||||
{"VDPPS $7, X2, X9, X2", "c4 e3 31 40 d2 07"},
|
||||
{"VDPPS $7, Y11, Y15, Y11", "c4 43 05 40 db 07"},
|
||||
{"VDPPS $7, Y11, Y15, Y2", "c4 c3 05 40 d3 07"},
|
||||
{"VDPPS $7, Y2, Y15, Y11", "c4 63 05 40 da 07"},
|
||||
{"VDPPS $7, Y2, Y15, Y2", "c4 e3 05 40 d2 07"},
|
||||
{"VHADDPD (BX), X9, X11", "c5 31 7c 1b"},
|
||||
{"VHADDPD (BX), X9, X2", "c5 b1 7c 13"},
|
||||
{"VHADDPD (BX), Y15, Y11", "c5 05 7c 1b"},
|
||||
{"VHADDPD (BX), Y15, Y2", "c5 85 7c 13"},
|
||||
{"VHADDPD (R11), X9, X11", "c4 41 31 7c 1b"},
|
||||
{"VHADDPD (R11), X9, X2", "c4 c1 31 7c 13"},
|
||||
{"VHADDPD (R11), Y15, Y11", "c4 41 05 7c 1b"},
|
||||
{"VHADDPD (R11), Y15, Y2", "c4 c1 05 7c 13"},
|
||||
{"VHADDPD X11, X9, X11", "c4 41 31 7c db"},
|
||||
{"VHADDPD X11, X9, X2", "c4 c1 31 7c d3"},
|
||||
{"VHADDPD X2, X9, X11", "c5 31 7c da"},
|
||||
{"VHADDPD X2, X9, X2", "c5 b1 7c d2"},
|
||||
{"VHADDPD Y11, Y15, Y11", "c4 41 05 7c db"},
|
||||
{"VHADDPD Y11, Y15, Y2", "c4 c1 05 7c d3"},
|
||||
{"VHADDPD Y2, Y15, Y11", "c5 05 7c da"},
|
||||
{"VHADDPD Y2, Y15, Y2", "c5 85 7c d2"},
|
||||
{"VHADDPS (BX), X9, X11", "c5 33 7c 1b"},
|
||||
{"VHADDPS (BX), X9, X2", "c5 b3 7c 13"},
|
||||
{"VHADDPS (BX), Y15, Y11", "c5 07 7c 1b"},
|
||||
{"VHADDPS (BX), Y15, Y2", "c5 87 7c 13"},
|
||||
{"VHADDPS (R11), X9, X11", "c4 41 33 7c 1b"},
|
||||
{"VHADDPS (R11), X9, X2", "c4 c1 33 7c 13"},
|
||||
{"VHADDPS (R11), Y15, Y11", "c4 41 07 7c 1b"},
|
||||
{"VHADDPS (R11), Y15, Y2", "c4 c1 07 7c 13"},
|
||||
{"VHADDPS X11, X9, X11", "c4 41 33 7c db"},
|
||||
{"VHADDPS X11, X9, X2", "c4 c1 33 7c d3"},
|
||||
{"VHADDPS X2, X9, X11", "c5 33 7c da"},
|
||||
{"VHADDPS X2, X9, X2", "c5 b3 7c d2"},
|
||||
{"VHADDPS Y11, Y15, Y11", "c4 41 07 7c db"},
|
||||
{"VHADDPS Y11, Y15, Y2", "c4 c1 07 7c d3"},
|
||||
{"VHADDPS Y2, Y15, Y11", "c5 07 7c da"},
|
||||
{"VHADDPS Y2, Y15, Y2", "c5 87 7c d2"},
|
||||
{"VHSUBPD (BX), X9, X11", "c5 31 7d 1b"},
|
||||
{"VHSUBPD (BX), X9, X2", "c5 b1 7d 13"},
|
||||
{"VHSUBPD (BX), Y15, Y11", "c5 05 7d 1b"},
|
||||
{"VHSUBPD (BX), Y15, Y2", "c5 85 7d 13"},
|
||||
{"VHSUBPD (R11), X9, X11", "c4 41 31 7d 1b"},
|
||||
{"VHSUBPD (R11), X9, X2", "c4 c1 31 7d 13"},
|
||||
{"VHSUBPD (R11), Y15, Y11", "c4 41 05 7d 1b"},
|
||||
{"VHSUBPD (R11), Y15, Y2", "c4 c1 05 7d 13"},
|
||||
{"VHSUBPD X11, X9, X11", "c4 41 31 7d db"},
|
||||
{"VHSUBPD X11, X9, X2", "c4 c1 31 7d d3"},
|
||||
{"VHSUBPD X2, X9, X11", "c5 31 7d da"},
|
||||
{"VHSUBPD X2, X9, X2", "c5 b1 7d d2"},
|
||||
{"VHSUBPD Y11, Y15, Y11", "c4 41 05 7d db"},
|
||||
{"VHSUBPD Y11, Y15, Y2", "c4 c1 05 7d d3"},
|
||||
{"VHSUBPD Y2, Y15, Y11", "c5 05 7d da"},
|
||||
{"VHSUBPD Y2, Y15, Y2", "c5 85 7d d2"},
|
||||
{"VHSUBPS (BX), X9, X11", "c5 33 7d 1b"},
|
||||
{"VHSUBPS (BX), X9, X2", "c5 b3 7d 13"},
|
||||
{"VHSUBPS (BX), Y15, Y11", "c5 07 7d 1b"},
|
||||
{"VHSUBPS (BX), Y15, Y2", "c5 87 7d 13"},
|
||||
{"VHSUBPS (R11), X9, X11", "c4 41 33 7d 1b"},
|
||||
{"VHSUBPS (R11), X9, X2", "c4 c1 33 7d 13"},
|
||||
{"VHSUBPS (R11), Y15, Y11", "c4 41 07 7d 1b"},
|
||||
{"VHSUBPS (R11), Y15, Y2", "c4 c1 07 7d 13"},
|
||||
{"VHSUBPS X11, X9, X11", "c4 41 33 7d db"},
|
||||
{"VHSUBPS X11, X9, X2", "c4 c1 33 7d d3"},
|
||||
{"VHSUBPS X2, X9, X11", "c5 33 7d da"},
|
||||
{"VHSUBPS X2, X9, X2", "c5 b3 7d d2"},
|
||||
{"VHSUBPS Y11, Y15, Y11", "c4 41 07 7d db"},
|
||||
{"VHSUBPS Y11, Y15, Y2", "c4 c1 07 7d d3"},
|
||||
{"VHSUBPS Y2, Y15, Y11", "c5 07 7d da"},
|
||||
{"VHSUBPS Y2, Y15, Y2", "c5 87 7d d2"},
|
||||
{"VINSERTF128 $7, (BX), Y15, Y11", "c4 63 05 18 1b 07"},
|
||||
{"VINSERTF128 $7, (BX), Y15, Y2", "c4 e3 05 18 13 07"},
|
||||
{"VINSERTF128 $7, (R11), Y15, Y11", "c4 43 05 18 1b 07"},
|
||||
{"VINSERTF128 $7, (R11), Y15, Y2", "c4 c3 05 18 13 07"},
|
||||
{"VINSERTF128 $7, X11, Y15, Y11", "c4 43 05 18 db 07"},
|
||||
{"VINSERTF128 $7, X11, Y15, Y2", "c4 c3 05 18 d3 07"},
|
||||
{"VINSERTF128 $7, X2, Y15, Y11", "c4 63 05 18 da 07"},
|
||||
{"VINSERTF128 $7, X2, Y15, Y2", "c4 e3 05 18 d2 07"},
|
||||
{"VINSERTPS $7, (BX), X9, X11", "c4 63 31 21 1b 07"},
|
||||
{"VINSERTPS $7, (BX), X9, X2", "c4 e3 31 21 13 07"},
|
||||
{"VINSERTPS $7, (R11), X9, X11", "c4 43 31 21 1b 07"},
|
||||
{"VINSERTPS $7, (R11), X9, X2", "c4 c3 31 21 13 07"},
|
||||
{"VINSERTPS $7, X11, X9, X11", "c4 43 31 21 db 07"},
|
||||
{"VINSERTPS $7, X11, X9, X2", "c4 c3 31 21 d3 07"},
|
||||
{"VINSERTPS $7, X2, X9, X11", "c4 63 31 21 da 07"},
|
||||
{"VINSERTPS $7, X2, X9, X2", "c4 e3 31 21 d2 07"},
|
||||
{"VLDDQU (BX), X11", "c5 7b f0 1b"},
|
||||
{"VLDDQU (BX), X2", "c5 fb f0 13"},
|
||||
{"VLDDQU (BX), Y11", "c5 7f f0 1b"},
|
||||
{"VLDDQU (BX), Y2", "c5 ff f0 13"},
|
||||
{"VLDDQU (R11), X11", "c4 41 7b f0 1b"},
|
||||
{"VLDDQU (R11), X2", "c4 c1 7b f0 13"},
|
||||
{"VLDDQU (R11), Y11", "c4 41 7f f0 1b"},
|
||||
{"VLDDQU (R11), Y2", "c4 c1 7f f0 13"},
|
||||
{"VLDMXCSR (BX)", "c5 f8 ae 13"},
|
||||
{"VLDMXCSR (R11)", "c4 c1 78 ae 13"},
|
||||
{"VMASKMOVDQU X11, X11", "c4 41 79 f7 db"},
|
||||
{"VMASKMOVDQU X11, X2", "c4 c1 79 f7 d3"},
|
||||
{"VMASKMOVDQU X2, X11", "c5 79 f7 da"},
|
||||
{"VMASKMOVDQU X2, X2", "c5 f9 f7 d2"},
|
||||
{"VMASKMOVPD (BX), X9, X11", "c4 62 31 2d 1b"},
|
||||
{"VMASKMOVPD (BX), X9, X2", "c4 e2 31 2d 13"},
|
||||
{"VMASKMOVPD (BX), Y15, Y11", "c4 62 05 2d 1b"},
|
||||
{"VMASKMOVPD (BX), Y15, Y2", "c4 e2 05 2d 13"},
|
||||
{"VMASKMOVPD (R11), X9, X11", "c4 42 31 2d 1b"},
|
||||
{"VMASKMOVPD (R11), X9, X2", "c4 c2 31 2d 13"},
|
||||
{"VMASKMOVPD (R11), Y15, Y11", "c4 42 05 2d 1b"},
|
||||
{"VMASKMOVPD (R11), Y15, Y2", "c4 c2 05 2d 13"},
|
||||
{"VMASKMOVPD X11, X9, (BX)", "c4 62 31 2f 1b"},
|
||||
{"VMASKMOVPD X11, X9, (R11)", "c4 42 31 2f 1b"},
|
||||
{"VMASKMOVPD X2, X9, (BX)", "c4 e2 31 2f 13"},
|
||||
{"VMASKMOVPD X2, X9, (R11)", "c4 c2 31 2f 13"},
|
||||
{"VMASKMOVPD Y11, Y15, (BX)", "c4 62 05 2f 1b"},
|
||||
{"VMASKMOVPD Y11, Y15, (R11)", "c4 42 05 2f 1b"},
|
||||
{"VMASKMOVPD Y2, Y15, (BX)", "c4 e2 05 2f 13"},
|
||||
{"VMASKMOVPD Y2, Y15, (R11)", "c4 c2 05 2f 13"},
|
||||
{"VMASKMOVPS (BX), X9, X11", "c4 62 31 2c 1b"},
|
||||
{"VMASKMOVPS (BX), X9, X2", "c4 e2 31 2c 13"},
|
||||
{"VMASKMOVPS (BX), Y15, Y11", "c4 62 05 2c 1b"},
|
||||
{"VMASKMOVPS (BX), Y15, Y2", "c4 e2 05 2c 13"},
|
||||
{"VMASKMOVPS (R11), X9, X11", "c4 42 31 2c 1b"},
|
||||
{"VMASKMOVPS (R11), X9, X2", "c4 c2 31 2c 13"},
|
||||
{"VMASKMOVPS (R11), Y15, Y11", "c4 42 05 2c 1b"},
|
||||
{"VMASKMOVPS (R11), Y15, Y2", "c4 c2 05 2c 13"},
|
||||
{"VMASKMOVPS X11, X9, (BX)", "c4 62 31 2e 1b"},
|
||||
{"VMASKMOVPS X11, X9, (R11)", "c4 42 31 2e 1b"},
|
||||
{"VMASKMOVPS X2, X9, (BX)", "c4 e2 31 2e 13"},
|
||||
{"VMASKMOVPS X2, X9, (R11)", "c4 c2 31 2e 13"},
|
||||
{"VMASKMOVPS Y11, Y15, (BX)", "c4 62 05 2e 1b"},
|
||||
{"VMASKMOVPS Y11, Y15, (R11)", "c4 42 05 2e 1b"},
|
||||
{"VMASKMOVPS Y2, Y15, (BX)", "c4 e2 05 2e 13"},
|
||||
{"VMASKMOVPS Y2, Y15, (R11)", "c4 c2 05 2e 13"},
|
||||
{"VMOVHLPS X11, X9, X11", "c4 41 30 12 db"},
|
||||
{"VMOVHLPS X11, X9, X2", "c4 c1 30 12 d3"},
|
||||
{"VMOVHLPS X2, X9, X11", "c5 30 12 da"},
|
||||
{"VMOVHLPS X2, X9, X2", "c5 b0 12 d2"},
|
||||
{"VMOVLPS (BX), X9, X11", "c5 30 12 1b"},
|
||||
{"VMOVLPS (BX), X9, X2", "c5 b0 12 13"},
|
||||
{"VMOVLPS (R11), X9, X11", "c4 41 30 12 1b"},
|
||||
{"VMOVLPS (R11), X9, X2", "c4 c1 30 12 13"},
|
||||
{"VMOVLPS X11, (BX)", "c5 78 13 1b"},
|
||||
{"VMOVLPS X11, (R11)", "c4 41 78 13 1b"},
|
||||
{"VMOVLPS X2, (BX)", "c5 f8 13 13"},
|
||||
{"VMOVLPS X2, (R11)", "c4 c1 78 13 13"},
|
||||
{"VMOVMSKPD X11, DX", "c4 c1 79 50 d3"},
|
||||
{"VMOVMSKPD X11, R11", "c4 41 79 50 db"},
|
||||
{"VMOVMSKPD X2, DX", "c5 f9 50 d2"},
|
||||
{"VMOVMSKPD X2, R11", "c5 79 50 da"},
|
||||
{"VMOVMSKPD Y11, DX", "c4 c1 7d 50 d3"},
|
||||
{"VMOVMSKPD Y11, R11", "c4 41 7d 50 db"},
|
||||
{"VMOVMSKPD Y2, DX", "c5 fd 50 d2"},
|
||||
{"VMOVMSKPD Y2, R11", "c5 7d 50 da"},
|
||||
{"VMOVSD (BX), X11", "c5 7b 10 1b"},
|
||||
{"VMOVSD (BX), X2", "c5 fb 10 13"},
|
||||
{"VMOVSD (R11), X11", "c4 41 7b 10 1b"},
|
||||
{"VMOVSD (R11), X2", "c4 c1 7b 10 13"},
|
||||
{"VMOVSD X11, (BX)", "c5 7b 11 1b"},
|
||||
{"VMOVSD X11, (R11)", "c4 41 7b 11 1b"},
|
||||
{"VMOVSD X11, X9, X11", "c4 41 33 11 db"},
|
||||
{"VMOVSD X11, X9, X2", "c5 33 11 da"},
|
||||
{"VMOVSD X2, (BX)", "c5 fb 11 13"},
|
||||
{"VMOVSD X2, (R11)", "c4 c1 7b 11 13"},
|
||||
{"VMOVSD X2, X9, X11", "c4 c1 33 11 d3"},
|
||||
{"VMOVSD X2, X9, X2", "c5 b3 11 d2"},
|
||||
{"VMOVSS (BX), X11", "c5 7a 10 1b"},
|
||||
{"VMOVSS (BX), X2", "c5 fa 10 13"},
|
||||
{"VMOVSS (R11), X11", "c4 41 7a 10 1b"},
|
||||
{"VMOVSS (R11), X2", "c4 c1 7a 10 13"},
|
||||
{"VMOVSS X11, (BX)", "c5 7a 11 1b"},
|
||||
{"VMOVSS X11, (R11)", "c4 41 7a 11 1b"},
|
||||
{"VMOVSS X11, X9, X11", "c4 41 32 11 db"},
|
||||
{"VMOVSS X11, X9, X2", "c5 32 11 da"},
|
||||
{"VMOVSS X2, (BX)", "c5 fa 11 13"},
|
||||
{"VMOVSS X2, (R11)", "c4 c1 7a 11 13"},
|
||||
{"VMOVSS X2, X9, X11", "c4 c1 32 11 d3"},
|
||||
{"VMOVSS X2, X9, X2", "c5 b2 11 d2"},
|
||||
{"VMPSADBW $7, (BX), X9, X11", "c4 63 31 42 1b 07"},
|
||||
{"VMPSADBW $7, (BX), X9, X2", "c4 e3 31 42 13 07"},
|
||||
{"VMPSADBW $7, (BX), Y15, Y11", "c4 63 05 42 1b 07"},
|
||||
{"VMPSADBW $7, (BX), Y15, Y2", "c4 e3 05 42 13 07"},
|
||||
{"VMPSADBW $7, (R11), X9, X11", "c4 43 31 42 1b 07"},
|
||||
{"VMPSADBW $7, (R11), X9, X2", "c4 c3 31 42 13 07"},
|
||||
{"VMPSADBW $7, (R11), Y15, Y11", "c4 43 05 42 1b 07"},
|
||||
{"VMPSADBW $7, (R11), Y15, Y2", "c4 c3 05 42 13 07"},
|
||||
{"VMPSADBW $7, X11, X9, X11", "c4 43 31 42 db 07"},
|
||||
{"VMPSADBW $7, X11, X9, X2", "c4 c3 31 42 d3 07"},
|
||||
{"VMPSADBW $7, X2, X9, X11", "c4 63 31 42 da 07"},
|
||||
{"VMPSADBW $7, X2, X9, X2", "c4 e3 31 42 d2 07"},
|
||||
{"VMPSADBW $7, Y11, Y15, Y11", "c4 43 05 42 db 07"},
|
||||
{"VMPSADBW $7, Y11, Y15, Y2", "c4 c3 05 42 d3 07"},
|
||||
{"VMPSADBW $7, Y2, Y15, Y11", "c4 63 05 42 da 07"},
|
||||
{"VMPSADBW $7, Y2, Y15, Y2", "c4 e3 05 42 d2 07"},
|
||||
{"VPBLENDVB X12, (BX), X9, X11", "c4 63 31 4c 1b c0"},
|
||||
{"VPBLENDVB X12, (BX), X9, X2", "c4 e3 31 4c 13 c0"},
|
||||
{"VPBLENDVB X12, (R11), X9, X11", "c4 43 31 4c 1b c0"},
|
||||
{"VPBLENDVB X12, (R11), X9, X2", "c4 c3 31 4c 13 c0"},
|
||||
{"VPBLENDVB X12, X11, X9, X11", "c4 43 31 4c db c0"},
|
||||
{"VPBLENDVB X12, X11, X9, X2", "c4 c3 31 4c d3 c0"},
|
||||
{"VPBLENDVB X12, X2, X9, X11", "c4 63 31 4c da c0"},
|
||||
{"VPBLENDVB X12, X2, X9, X2", "c4 e3 31 4c d2 c0"},
|
||||
{"VPBLENDVB Y13, (BX), Y15, Y11", "c4 63 05 4c 1b d0"},
|
||||
{"VPBLENDVB Y13, (BX), Y15, Y2", "c4 e3 05 4c 13 d0"},
|
||||
{"VPBLENDVB Y13, (R11), Y15, Y11", "c4 43 05 4c 1b d0"},
|
||||
{"VPBLENDVB Y13, (R11), Y15, Y2", "c4 c3 05 4c 13 d0"},
|
||||
{"VPBLENDVB Y13, Y11, Y15, Y11", "c4 43 05 4c db d0"},
|
||||
{"VPBLENDVB Y13, Y11, Y15, Y2", "c4 c3 05 4c d3 d0"},
|
||||
{"VPBLENDVB Y13, Y2, Y15, Y11", "c4 63 05 4c da d0"},
|
||||
{"VPBLENDVB Y13, Y2, Y15, Y2", "c4 e3 05 4c d2 d0"},
|
||||
{"VPBLENDW $7, (BX), X9, X11", "c4 63 31 0e 1b 07"},
|
||||
{"VPBLENDW $7, (BX), X9, X2", "c4 e3 31 0e 13 07"},
|
||||
{"VPBLENDW $7, (BX), Y15, Y11", "c4 63 05 0e 1b 07"},
|
||||
{"VPBLENDW $7, (BX), Y15, Y2", "c4 e3 05 0e 13 07"},
|
||||
{"VPBLENDW $7, (R11), X9, X11", "c4 43 31 0e 1b 07"},
|
||||
{"VPBLENDW $7, (R11), X9, X2", "c4 c3 31 0e 13 07"},
|
||||
{"VPBLENDW $7, (R11), Y15, Y11", "c4 43 05 0e 1b 07"},
|
||||
{"VPBLENDW $7, (R11), Y15, Y2", "c4 c3 05 0e 13 07"},
|
||||
{"VPBLENDW $7, X11, X9, X11", "c4 43 31 0e db 07"},
|
||||
{"VPBLENDW $7, X11, X9, X2", "c4 c3 31 0e d3 07"},
|
||||
{"VPBLENDW $7, X2, X9, X11", "c4 63 31 0e da 07"},
|
||||
{"VPBLENDW $7, X2, X9, X2", "c4 e3 31 0e d2 07"},
|
||||
{"VPBLENDW $7, Y11, Y15, Y11", "c4 43 05 0e db 07"},
|
||||
{"VPBLENDW $7, Y11, Y15, Y2", "c4 c3 05 0e d3 07"},
|
||||
{"VPBLENDW $7, Y2, Y15, Y11", "c4 63 05 0e da 07"},
|
||||
{"VPBLENDW $7, Y2, Y15, Y2", "c4 e3 05 0e d2 07"},
|
||||
{"VPERMILPD $7, (BX), X11", "c4 63 79 05 1b 07"},
|
||||
{"VPERMILPD $7, (BX), X2", "c4 e3 79 05 13 07"},
|
||||
{"VPERMILPD $7, (BX), Y11", "c4 63 7d 05 1b 07"},
|
||||
{"VPERMILPD $7, (BX), Y2", "c4 e3 7d 05 13 07"},
|
||||
{"VPERMILPD $7, (R11), X11", "c4 43 79 05 1b 07"},
|
||||
{"VPERMILPD $7, (R11), X2", "c4 c3 79 05 13 07"},
|
||||
{"VPERMILPD $7, (R11), Y11", "c4 43 7d 05 1b 07"},
|
||||
{"VPERMILPD $7, (R11), Y2", "c4 c3 7d 05 13 07"},
|
||||
{"VPERMILPD $7, X11, X11", "c4 43 79 05 db 07"},
|
||||
{"VPERMILPD $7, X11, X2", "c4 c3 79 05 d3 07"},
|
||||
{"VPERMILPD $7, X2, X11", "c4 63 79 05 da 07"},
|
||||
{"VPERMILPD $7, X2, X2", "c4 e3 79 05 d2 07"},
|
||||
{"VPERMILPD $7, Y11, Y11", "c4 43 7d 05 db 07"},
|
||||
{"VPERMILPD $7, Y11, Y2", "c4 c3 7d 05 d3 07"},
|
||||
{"VPERMILPD $7, Y2, Y11", "c4 63 7d 05 da 07"},
|
||||
{"VPERMILPD $7, Y2, Y2", "c4 e3 7d 05 d2 07"},
|
||||
{"VPERMILPD (BX), X9, X11", "c4 62 31 0d 1b"},
|
||||
{"VPERMILPD (BX), X9, X2", "c4 e2 31 0d 13"},
|
||||
{"VPERMILPD (BX), Y15, Y11", "c4 62 05 0d 1b"},
|
||||
{"VPERMILPD (BX), Y15, Y2", "c4 e2 05 0d 13"},
|
||||
{"VPERMILPD (R11), X9, X11", "c4 42 31 0d 1b"},
|
||||
{"VPERMILPD (R11), X9, X2", "c4 c2 31 0d 13"},
|
||||
{"VPERMILPD (R11), Y15, Y11", "c4 42 05 0d 1b"},
|
||||
{"VPERMILPD (R11), Y15, Y2", "c4 c2 05 0d 13"},
|
||||
{"VPERMILPD X11, X9, X11", "c4 42 31 0d db"},
|
||||
{"VPERMILPD X11, X9, X2", "c4 c2 31 0d d3"},
|
||||
{"VPERMILPD X2, X9, X11", "c4 62 31 0d da"},
|
||||
{"VPERMILPD X2, X9, X2", "c4 e2 31 0d d2"},
|
||||
{"VPERMILPD Y11, Y15, Y11", "c4 42 05 0d db"},
|
||||
{"VPERMILPD Y11, Y15, Y2", "c4 c2 05 0d d3"},
|
||||
{"VPERMILPD Y2, Y15, Y11", "c4 62 05 0d da"},
|
||||
{"VPERMILPD Y2, Y15, Y2", "c4 e2 05 0d d2"},
|
||||
{"VPERMILPS $7, (BX), X11", "c4 63 79 04 1b 07"},
|
||||
{"VPERMILPS $7, (BX), X2", "c4 e3 79 04 13 07"},
|
||||
{"VPERMILPS $7, (BX), Y11", "c4 63 7d 04 1b 07"},
|
||||
{"VPERMILPS $7, (BX), Y2", "c4 e3 7d 04 13 07"},
|
||||
{"VPERMILPS $7, (R11), X11", "c4 43 79 04 1b 07"},
|
||||
{"VPERMILPS $7, (R11), X2", "c4 c3 79 04 13 07"},
|
||||
{"VPERMILPS $7, (R11), Y11", "c4 43 7d 04 1b 07"},
|
||||
{"VPERMILPS $7, (R11), Y2", "c4 c3 7d 04 13 07"},
|
||||
{"VPERMILPS $7, X11, X11", "c4 43 79 04 db 07"},
|
||||
{"VPERMILPS $7, X11, X2", "c4 c3 79 04 d3 07"},
|
||||
{"VPERMILPS $7, X2, X11", "c4 63 79 04 da 07"},
|
||||
{"VPERMILPS $7, X2, X2", "c4 e3 79 04 d2 07"},
|
||||
{"VPERMILPS $7, Y11, Y11", "c4 43 7d 04 db 07"},
|
||||
{"VPERMILPS $7, Y11, Y2", "c4 c3 7d 04 d3 07"},
|
||||
{"VPERMILPS $7, Y2, Y11", "c4 63 7d 04 da 07"},
|
||||
{"VPERMILPS $7, Y2, Y2", "c4 e3 7d 04 d2 07"},
|
||||
{"VPERMILPS (BX), X9, X11", "c4 62 31 0c 1b"},
|
||||
{"VPERMILPS (BX), X9, X2", "c4 e2 31 0c 13"},
|
||||
{"VPERMILPS (BX), Y15, Y11", "c4 62 05 0c 1b"},
|
||||
{"VPERMILPS (BX), Y15, Y2", "c4 e2 05 0c 13"},
|
||||
{"VPERMILPS (R11), X9, X11", "c4 42 31 0c 1b"},
|
||||
{"VPERMILPS (R11), X9, X2", "c4 c2 31 0c 13"},
|
||||
{"VPERMILPS (R11), Y15, Y11", "c4 42 05 0c 1b"},
|
||||
{"VPERMILPS (R11), Y15, Y2", "c4 c2 05 0c 13"},
|
||||
{"VPERMILPS X11, X9, X11", "c4 42 31 0c db"},
|
||||
{"VPERMILPS X11, X9, X2", "c4 c2 31 0c d3"},
|
||||
{"VPERMILPS X2, X9, X11", "c4 62 31 0c da"},
|
||||
{"VPERMILPS X2, X9, X2", "c4 e2 31 0c d2"},
|
||||
{"VPERMILPS Y11, Y15, Y11", "c4 42 05 0c db"},
|
||||
{"VPERMILPS Y11, Y15, Y2", "c4 c2 05 0c d3"},
|
||||
{"VPERMILPS Y2, Y15, Y11", "c4 62 05 0c da"},
|
||||
{"VPERMILPS Y2, Y15, Y2", "c4 e2 05 0c d2"},
|
||||
{"VPHADDD (BX), X9, X11", "c4 62 31 02 1b"},
|
||||
{"VPHADDD (BX), X9, X2", "c4 e2 31 02 13"},
|
||||
{"VPHADDD (BX), Y15, Y11", "c4 62 05 02 1b"},
|
||||
{"VPHADDD (BX), Y15, Y2", "c4 e2 05 02 13"},
|
||||
{"VPHADDD (R11), X9, X11", "c4 42 31 02 1b"},
|
||||
{"VPHADDD (R11), X9, X2", "c4 c2 31 02 13"},
|
||||
{"VPHADDD (R11), Y15, Y11", "c4 42 05 02 1b"},
|
||||
{"VPHADDD (R11), Y15, Y2", "c4 c2 05 02 13"},
|
||||
{"VPHADDD X11, X9, X11", "c4 42 31 02 db"},
|
||||
{"VPHADDD X11, X9, X2", "c4 c2 31 02 d3"},
|
||||
{"VPHADDD X2, X9, X11", "c4 62 31 02 da"},
|
||||
{"VPHADDD X2, X9, X2", "c4 e2 31 02 d2"},
|
||||
{"VPHADDD Y11, Y15, Y11", "c4 42 05 02 db"},
|
||||
{"VPHADDD Y11, Y15, Y2", "c4 c2 05 02 d3"},
|
||||
{"VPHADDD Y2, Y15, Y11", "c4 62 05 02 da"},
|
||||
{"VPHADDD Y2, Y15, Y2", "c4 e2 05 02 d2"},
|
||||
{"VPHADDSW (BX), X9, X11", "c4 62 31 03 1b"},
|
||||
{"VPHADDSW (BX), X9, X2", "c4 e2 31 03 13"},
|
||||
{"VPHADDSW (BX), Y15, Y11", "c4 62 05 03 1b"},
|
||||
{"VPHADDSW (BX), Y15, Y2", "c4 e2 05 03 13"},
|
||||
{"VPHADDSW (R11), X9, X11", "c4 42 31 03 1b"},
|
||||
{"VPHADDSW (R11), X9, X2", "c4 c2 31 03 13"},
|
||||
{"VPHADDSW (R11), Y15, Y11", "c4 42 05 03 1b"},
|
||||
{"VPHADDSW (R11), Y15, Y2", "c4 c2 05 03 13"},
|
||||
{"VPHADDSW X11, X9, X11", "c4 42 31 03 db"},
|
||||
{"VPHADDSW X11, X9, X2", "c4 c2 31 03 d3"},
|
||||
{"VPHADDSW X2, X9, X11", "c4 62 31 03 da"},
|
||||
{"VPHADDSW X2, X9, X2", "c4 e2 31 03 d2"},
|
||||
{"VPHADDSW Y11, Y15, Y11", "c4 42 05 03 db"},
|
||||
{"VPHADDSW Y11, Y15, Y2", "c4 c2 05 03 d3"},
|
||||
{"VPHADDSW Y2, Y15, Y11", "c4 62 05 03 da"},
|
||||
{"VPHADDSW Y2, Y15, Y2", "c4 e2 05 03 d2"},
|
||||
{"VPHADDW (BX), X9, X11", "c4 62 31 01 1b"},
|
||||
{"VPHADDW (BX), X9, X2", "c4 e2 31 01 13"},
|
||||
{"VPHADDW (BX), Y15, Y11", "c4 62 05 01 1b"},
|
||||
{"VPHADDW (BX), Y15, Y2", "c4 e2 05 01 13"},
|
||||
{"VPHADDW (R11), X9, X11", "c4 42 31 01 1b"},
|
||||
{"VPHADDW (R11), X9, X2", "c4 c2 31 01 13"},
|
||||
{"VPHADDW (R11), Y15, Y11", "c4 42 05 01 1b"},
|
||||
{"VPHADDW (R11), Y15, Y2", "c4 c2 05 01 13"},
|
||||
{"VPHADDW X11, X9, X11", "c4 42 31 01 db"},
|
||||
{"VPHADDW X11, X9, X2", "c4 c2 31 01 d3"},
|
||||
{"VPHADDW X2, X9, X11", "c4 62 31 01 da"},
|
||||
{"VPHADDW X2, X9, X2", "c4 e2 31 01 d2"},
|
||||
{"VPHADDW Y11, Y15, Y11", "c4 42 05 01 db"},
|
||||
{"VPHADDW Y11, Y15, Y2", "c4 c2 05 01 d3"},
|
||||
{"VPHADDW Y2, Y15, Y11", "c4 62 05 01 da"},
|
||||
{"VPHADDW Y2, Y15, Y2", "c4 e2 05 01 d2"},
|
||||
{"VPHMINPOSUW (BX), X11", "c4 62 79 41 1b"},
|
||||
{"VPHMINPOSUW (BX), X2", "c4 e2 79 41 13"},
|
||||
{"VPHMINPOSUW (R11), X11", "c4 42 79 41 1b"},
|
||||
{"VPHMINPOSUW (R11), X2", "c4 c2 79 41 13"},
|
||||
{"VPHMINPOSUW X11, X11", "c4 42 79 41 db"},
|
||||
{"VPHMINPOSUW X11, X2", "c4 c2 79 41 d3"},
|
||||
{"VPHMINPOSUW X2, X11", "c4 62 79 41 da"},
|
||||
{"VPHMINPOSUW X2, X2", "c4 e2 79 41 d2"},
|
||||
{"VPHSUBD (BX), X9, X11", "c4 62 31 06 1b"},
|
||||
{"VPHSUBD (BX), X9, X2", "c4 e2 31 06 13"},
|
||||
{"VPHSUBD (BX), Y15, Y11", "c4 62 05 06 1b"},
|
||||
{"VPHSUBD (BX), Y15, Y2", "c4 e2 05 06 13"},
|
||||
{"VPHSUBD (R11), X9, X11", "c4 42 31 06 1b"},
|
||||
{"VPHSUBD (R11), X9, X2", "c4 c2 31 06 13"},
|
||||
{"VPHSUBD (R11), Y15, Y11", "c4 42 05 06 1b"},
|
||||
{"VPHSUBD (R11), Y15, Y2", "c4 c2 05 06 13"},
|
||||
{"VPHSUBD X11, X9, X11", "c4 42 31 06 db"},
|
||||
{"VPHSUBD X11, X9, X2", "c4 c2 31 06 d3"},
|
||||
{"VPHSUBD X2, X9, X11", "c4 62 31 06 da"},
|
||||
{"VPHSUBD X2, X9, X2", "c4 e2 31 06 d2"},
|
||||
{"VPHSUBD Y11, Y15, Y11", "c4 42 05 06 db"},
|
||||
{"VPHSUBD Y11, Y15, Y2", "c4 c2 05 06 d3"},
|
||||
{"VPHSUBD Y2, Y15, Y11", "c4 62 05 06 da"},
|
||||
{"VPHSUBD Y2, Y15, Y2", "c4 e2 05 06 d2"},
|
||||
{"VPHSUBSW (BX), X9, X11", "c4 62 31 07 1b"},
|
||||
{"VPHSUBSW (BX), X9, X2", "c4 e2 31 07 13"},
|
||||
{"VPHSUBSW (BX), Y15, Y11", "c4 62 05 07 1b"},
|
||||
{"VPHSUBSW (BX), Y15, Y2", "c4 e2 05 07 13"},
|
||||
{"VPHSUBSW (R11), X9, X11", "c4 42 31 07 1b"},
|
||||
{"VPHSUBSW (R11), X9, X2", "c4 c2 31 07 13"},
|
||||
{"VPHSUBSW (R11), Y15, Y11", "c4 42 05 07 1b"},
|
||||
{"VPHSUBSW (R11), Y15, Y2", "c4 c2 05 07 13"},
|
||||
{"VPHSUBSW X11, X9, X11", "c4 42 31 07 db"},
|
||||
{"VPHSUBSW X11, X9, X2", "c4 c2 31 07 d3"},
|
||||
{"VPHSUBSW X2, X9, X11", "c4 62 31 07 da"},
|
||||
{"VPHSUBSW X2, X9, X2", "c4 e2 31 07 d2"},
|
||||
{"VPHSUBSW Y11, Y15, Y11", "c4 42 05 07 db"},
|
||||
{"VPHSUBSW Y11, Y15, Y2", "c4 c2 05 07 d3"},
|
||||
{"VPHSUBSW Y2, Y15, Y11", "c4 62 05 07 da"},
|
||||
{"VPHSUBSW Y2, Y15, Y2", "c4 e2 05 07 d2"},
|
||||
{"VPHSUBW (BX), X9, X11", "c4 62 31 05 1b"},
|
||||
{"VPHSUBW (BX), X9, X2", "c4 e2 31 05 13"},
|
||||
{"VPHSUBW (BX), Y15, Y11", "c4 62 05 05 1b"},
|
||||
{"VPHSUBW (BX), Y15, Y2", "c4 e2 05 05 13"},
|
||||
{"VPHSUBW (R11), X9, X11", "c4 42 31 05 1b"},
|
||||
{"VPHSUBW (R11), X9, X2", "c4 c2 31 05 13"},
|
||||
{"VPHSUBW (R11), Y15, Y11", "c4 42 05 05 1b"},
|
||||
{"VPHSUBW (R11), Y15, Y2", "c4 c2 05 05 13"},
|
||||
{"VPHSUBW X11, X9, X11", "c4 42 31 05 db"},
|
||||
{"VPHSUBW X11, X9, X2", "c4 c2 31 05 d3"},
|
||||
{"VPHSUBW X2, X9, X11", "c4 62 31 05 da"},
|
||||
{"VPHSUBW X2, X9, X2", "c4 e2 31 05 d2"},
|
||||
{"VPHSUBW Y11, Y15, Y11", "c4 42 05 05 db"},
|
||||
{"VPHSUBW Y11, Y15, Y2", "c4 c2 05 05 d3"},
|
||||
{"VPHSUBW Y2, Y15, Y11", "c4 62 05 05 da"},
|
||||
{"VPHSUBW Y2, Y15, Y2", "c4 e2 05 05 d2"},
|
||||
{"VPINSRB $7, (BX), X9, X11", "c4 63 31 20 1b 07"},
|
||||
{"VPINSRB $7, (BX), X9, X2", "c4 e3 31 20 13 07"},
|
||||
{"VPINSRB $7, (R11), X9, X11", "c4 43 31 20 1b 07"},
|
||||
{"VPINSRB $7, (R11), X9, X2", "c4 c3 31 20 13 07"},
|
||||
{"VPINSRB $7, DX, X9, X11", "c4 63 31 20 da 07"},
|
||||
{"VPINSRB $7, DX, X9, X2", "c4 e3 31 20 d2 07"},
|
||||
{"VPINSRB $7, R11, X9, X11", "c4 43 31 20 db 07"},
|
||||
{"VPINSRB $7, R11, X9, X2", "c4 c3 31 20 d3 07"},
|
||||
{"VPINSRW $7, (BX), X9, X11", "c5 31 c4 1b 07"},
|
||||
{"VPINSRW $7, (BX), X9, X2", "c5 b1 c4 13 07"},
|
||||
{"VPINSRW $7, (R11), X9, X11", "c4 41 31 c4 1b 07"},
|
||||
{"VPINSRW $7, (R11), X9, X2", "c4 c1 31 c4 13 07"},
|
||||
{"VPINSRW $7, DX, X9, X11", "c5 31 c4 da 07"},
|
||||
{"VPINSRW $7, DX, X9, X2", "c5 b1 c4 d2 07"},
|
||||
{"VPINSRW $7, R11, X9, X11", "c4 41 31 c4 db 07"},
|
||||
{"VPINSRW $7, R11, X9, X2", "c4 c1 31 c4 d3 07"},
|
||||
{"VPMASKMOVD (BX), X9, X11", "c4 62 31 8c 1b"},
|
||||
{"VPMASKMOVD (BX), X9, X2", "c4 e2 31 8c 13"},
|
||||
{"VPMASKMOVD (BX), Y15, Y11", "c4 62 05 8c 1b"},
|
||||
{"VPMASKMOVD (BX), Y15, Y2", "c4 e2 05 8c 13"},
|
||||
{"VPMASKMOVD (R11), X9, X11", "c4 42 31 8c 1b"},
|
||||
{"VPMASKMOVD (R11), X9, X2", "c4 c2 31 8c 13"},
|
||||
{"VPMASKMOVD (R11), Y15, Y11", "c4 42 05 8c 1b"},
|
||||
{"VPMASKMOVD (R11), Y15, Y2", "c4 c2 05 8c 13"},
|
||||
{"VPMASKMOVD X11, X9, (BX)", "c4 62 31 8e 1b"},
|
||||
{"VPMASKMOVD X11, X9, (R11)", "c4 42 31 8e 1b"},
|
||||
{"VPMASKMOVD X2, X9, (BX)", "c4 e2 31 8e 13"},
|
||||
{"VPMASKMOVD X2, X9, (R11)", "c4 c2 31 8e 13"},
|
||||
{"VPMASKMOVD Y11, Y15, (BX)", "c4 62 05 8e 1b"},
|
||||
{"VPMASKMOVD Y11, Y15, (R11)", "c4 42 05 8e 1b"},
|
||||
{"VPMASKMOVD Y2, Y15, (BX)", "c4 e2 05 8e 13"},
|
||||
{"VPMASKMOVD Y2, Y15, (R11)", "c4 c2 05 8e 13"},
|
||||
{"VPMASKMOVQ (BX), X9, X11", "c4 62 b1 8c 1b"},
|
||||
{"VPMASKMOVQ (BX), X9, X2", "c4 e2 b1 8c 13"},
|
||||
{"VPMASKMOVQ (BX), Y15, Y11", "c4 62 85 8c 1b"},
|
||||
{"VPMASKMOVQ (BX), Y15, Y2", "c4 e2 85 8c 13"},
|
||||
{"VPMASKMOVQ (R11), X9, X11", "c4 42 b1 8c 1b"},
|
||||
{"VPMASKMOVQ (R11), X9, X2", "c4 c2 b1 8c 13"},
|
||||
{"VPMASKMOVQ (R11), Y15, Y11", "c4 42 85 8c 1b"},
|
||||
{"VPMASKMOVQ (R11), Y15, Y2", "c4 c2 85 8c 13"},
|
||||
{"VPMASKMOVQ X11, X9, (BX)", "c4 62 b1 8e 1b"},
|
||||
{"VPMASKMOVQ X11, X9, (R11)", "c4 42 b1 8e 1b"},
|
||||
{"VPMASKMOVQ X2, X9, (BX)", "c4 e2 b1 8e 13"},
|
||||
{"VPMASKMOVQ X2, X9, (R11)", "c4 c2 b1 8e 13"},
|
||||
{"VPMASKMOVQ Y11, Y15, (BX)", "c4 62 85 8e 1b"},
|
||||
{"VPMASKMOVQ Y11, Y15, (R11)", "c4 42 85 8e 1b"},
|
||||
{"VPMASKMOVQ Y2, Y15, (BX)", "c4 e2 85 8e 13"},
|
||||
{"VPMASKMOVQ Y2, Y15, (R11)", "c4 c2 85 8e 13"},
|
||||
{"VPSIGNB (BX), X9, X11", "c4 62 31 08 1b"},
|
||||
{"VPSIGNB (BX), X9, X2", "c4 e2 31 08 13"},
|
||||
{"VPSIGNB (BX), Y15, Y11", "c4 62 05 08 1b"},
|
||||
{"VPSIGNB (BX), Y15, Y2", "c4 e2 05 08 13"},
|
||||
{"VPSIGNB (R11), X9, X11", "c4 42 31 08 1b"},
|
||||
{"VPSIGNB (R11), X9, X2", "c4 c2 31 08 13"},
|
||||
{"VPSIGNB (R11), Y15, Y11", "c4 42 05 08 1b"},
|
||||
{"VPSIGNB (R11), Y15, Y2", "c4 c2 05 08 13"},
|
||||
{"VPSIGNB X11, X9, X11", "c4 42 31 08 db"},
|
||||
{"VPSIGNB X11, X9, X2", "c4 c2 31 08 d3"},
|
||||
{"VPSIGNB X2, X9, X11", "c4 62 31 08 da"},
|
||||
{"VPSIGNB X2, X9, X2", "c4 e2 31 08 d2"},
|
||||
{"VPSIGNB Y11, Y15, Y11", "c4 42 05 08 db"},
|
||||
{"VPSIGNB Y11, Y15, Y2", "c4 c2 05 08 d3"},
|
||||
{"VPSIGNB Y2, Y15, Y11", "c4 62 05 08 da"},
|
||||
{"VPSIGNB Y2, Y15, Y2", "c4 e2 05 08 d2"},
|
||||
{"VPSIGND (BX), X9, X11", "c4 62 31 0a 1b"},
|
||||
{"VPSIGND (BX), X9, X2", "c4 e2 31 0a 13"},
|
||||
{"VPSIGND (BX), Y15, Y11", "c4 62 05 0a 1b"},
|
||||
{"VPSIGND (BX), Y15, Y2", "c4 e2 05 0a 13"},
|
||||
{"VPSIGND (R11), X9, X11", "c4 42 31 0a 1b"},
|
||||
{"VPSIGND (R11), X9, X2", "c4 c2 31 0a 13"},
|
||||
{"VPSIGND (R11), Y15, Y11", "c4 42 05 0a 1b"},
|
||||
{"VPSIGND (R11), Y15, Y2", "c4 c2 05 0a 13"},
|
||||
{"VPSIGND X11, X9, X11", "c4 42 31 0a db"},
|
||||
{"VPSIGND X11, X9, X2", "c4 c2 31 0a d3"},
|
||||
{"VPSIGND X2, X9, X11", "c4 62 31 0a da"},
|
||||
{"VPSIGND X2, X9, X2", "c4 e2 31 0a d2"},
|
||||
{"VPSIGND Y11, Y15, Y11", "c4 42 05 0a db"},
|
||||
{"VPSIGND Y11, Y15, Y2", "c4 c2 05 0a d3"},
|
||||
{"VPSIGND Y2, Y15, Y11", "c4 62 05 0a da"},
|
||||
{"VPSIGND Y2, Y15, Y2", "c4 e2 05 0a d2"},
|
||||
{"VPSIGNW (BX), X9, X11", "c4 62 31 09 1b"},
|
||||
{"VPSIGNW (BX), X9, X2", "c4 e2 31 09 13"},
|
||||
{"VPSIGNW (BX), Y15, Y11", "c4 62 05 09 1b"},
|
||||
{"VPSIGNW (BX), Y15, Y2", "c4 e2 05 09 13"},
|
||||
{"VPSIGNW (R11), X9, X11", "c4 42 31 09 1b"},
|
||||
{"VPSIGNW (R11), X9, X2", "c4 c2 31 09 13"},
|
||||
{"VPSIGNW (R11), Y15, Y11", "c4 42 05 09 1b"},
|
||||
{"VPSIGNW (R11), Y15, Y2", "c4 c2 05 09 13"},
|
||||
{"VPSIGNW X11, X9, X11", "c4 42 31 09 db"},
|
||||
{"VPSIGNW X11, X9, X2", "c4 c2 31 09 d3"},
|
||||
{"VPSIGNW X2, X9, X11", "c4 62 31 09 da"},
|
||||
{"VPSIGNW X2, X9, X2", "c4 e2 31 09 d2"},
|
||||
{"VPSIGNW Y11, Y15, Y11", "c4 42 05 09 db"},
|
||||
{"VPSIGNW Y11, Y15, Y2", "c4 c2 05 09 d3"},
|
||||
{"VPSIGNW Y2, Y15, Y11", "c4 62 05 09 da"},
|
||||
{"VPSIGNW Y2, Y15, Y2", "c4 e2 05 09 d2"},
|
||||
{"VPSLLW $7, X11, X9", "c4 c1 31 71 f3 07"},
|
||||
{"VPSLLW $7, X2, X9", "c5 b1 71 f2 07"},
|
||||
{"VPSLLW $7, Y11, Y15", "c4 c1 05 71 f3 07"},
|
||||
{"VPSLLW $7, Y2, Y15", "c5 85 71 f2 07"},
|
||||
{"VPSLLW (BX), X9, X11", "c5 31 f1 1b"},
|
||||
{"VPSLLW (BX), X9, X2", "c5 b1 f1 13"},
|
||||
{"VPSLLW (BX), Y15, Y11", "c5 05 f1 1b"},
|
||||
{"VPSLLW (BX), Y15, Y2", "c5 85 f1 13"},
|
||||
{"VPSLLW (R11), X9, X11", "c4 41 31 f1 1b"},
|
||||
{"VPSLLW (R11), X9, X2", "c4 c1 31 f1 13"},
|
||||
{"VPSLLW (R11), Y15, Y11", "c4 41 05 f1 1b"},
|
||||
{"VPSLLW (R11), Y15, Y2", "c4 c1 05 f1 13"},
|
||||
{"VPSLLW X11, X9, X11", "c4 41 31 f1 db"},
|
||||
{"VPSLLW X11, X9, X2", "c4 c1 31 f1 d3"},
|
||||
{"VPSLLW X11, Y15, Y11", "c4 41 05 f1 db"},
|
||||
{"VPSLLW X11, Y15, Y2", "c4 c1 05 f1 d3"},
|
||||
{"VPSLLW X2, X9, X11", "c5 31 f1 da"},
|
||||
{"VPSLLW X2, X9, X2", "c5 b1 f1 d2"},
|
||||
{"VPSLLW X2, Y15, Y11", "c5 05 f1 da"},
|
||||
{"VPSLLW X2, Y15, Y2", "c5 85 f1 d2"},
|
||||
{"VPSRAW $7, X11, X9", "c4 c1 31 71 e3 07"},
|
||||
{"VPSRAW $7, X2, X9", "c5 b1 71 e2 07"},
|
||||
{"VPSRAW $7, Y11, Y15", "c4 c1 05 71 e3 07"},
|
||||
{"VPSRAW $7, Y2, Y15", "c5 85 71 e2 07"},
|
||||
{"VPSRAW (BX), X9, X11", "c5 31 e1 1b"},
|
||||
{"VPSRAW (BX), X9, X2", "c5 b1 e1 13"},
|
||||
{"VPSRAW (BX), Y15, Y11", "c5 05 e1 1b"},
|
||||
{"VPSRAW (BX), Y15, Y2", "c5 85 e1 13"},
|
||||
{"VPSRAW (R11), X9, X11", "c4 41 31 e1 1b"},
|
||||
{"VPSRAW (R11), X9, X2", "c4 c1 31 e1 13"},
|
||||
{"VPSRAW (R11), Y15, Y11", "c4 41 05 e1 1b"},
|
||||
{"VPSRAW (R11), Y15, Y2", "c4 c1 05 e1 13"},
|
||||
{"VPSRAW X11, X9, X11", "c4 41 31 e1 db"},
|
||||
{"VPSRAW X11, X9, X2", "c4 c1 31 e1 d3"},
|
||||
{"VPSRAW X11, Y15, Y11", "c4 41 05 e1 db"},
|
||||
{"VPSRAW X11, Y15, Y2", "c4 c1 05 e1 d3"},
|
||||
{"VPSRAW X2, X9, X11", "c5 31 e1 da"},
|
||||
{"VPSRAW X2, X9, X2", "c5 b1 e1 d2"},
|
||||
{"VPSRAW X2, Y15, Y11", "c5 05 e1 da"},
|
||||
{"VPSRAW X2, Y15, Y2", "c5 85 e1 d2"},
|
||||
{"VPSRLW $7, X11, X9", "c4 c1 31 71 d3 07"},
|
||||
{"VPSRLW $7, X2, X9", "c5 b1 71 d2 07"},
|
||||
{"VPSRLW $7, Y11, Y15", "c4 c1 05 71 d3 07"},
|
||||
{"VPSRLW $7, Y2, Y15", "c5 85 71 d2 07"},
|
||||
{"VPSRLW (BX), X9, X11", "c5 31 d1 1b"},
|
||||
{"VPSRLW (BX), X9, X2", "c5 b1 d1 13"},
|
||||
{"VPSRLW (BX), Y15, Y11", "c5 05 d1 1b"},
|
||||
{"VPSRLW (BX), Y15, Y2", "c5 85 d1 13"},
|
||||
{"VPSRLW (R11), X9, X11", "c4 41 31 d1 1b"},
|
||||
{"VPSRLW (R11), X9, X2", "c4 c1 31 d1 13"},
|
||||
{"VPSRLW (R11), Y15, Y11", "c4 41 05 d1 1b"},
|
||||
{"VPSRLW (R11), Y15, Y2", "c4 c1 05 d1 13"},
|
||||
{"VPSRLW X11, X9, X11", "c4 41 31 d1 db"},
|
||||
{"VPSRLW X11, X9, X2", "c4 c1 31 d1 d3"},
|
||||
{"VPSRLW X11, Y15, Y11", "c4 41 05 d1 db"},
|
||||
{"VPSRLW X11, Y15, Y2", "c4 c1 05 d1 d3"},
|
||||
{"VPSRLW X2, X9, X11", "c5 31 d1 da"},
|
||||
{"VPSRLW X2, X9, X2", "c5 b1 d1 d2"},
|
||||
{"VPSRLW X2, Y15, Y11", "c5 05 d1 da"},
|
||||
{"VPSRLW X2, Y15, Y2", "c5 85 d1 d2"},
|
||||
{"VRCPPS (BX), X11", "c5 78 53 1b"},
|
||||
{"VRCPPS (BX), X2", "c5 f8 53 13"},
|
||||
{"VRCPPS (BX), Y11", "c5 7c 53 1b"},
|
||||
{"VRCPPS (BX), Y2", "c5 fc 53 13"},
|
||||
{"VRCPPS (R11), X11", "c4 41 78 53 1b"},
|
||||
{"VRCPPS (R11), X2", "c4 c1 78 53 13"},
|
||||
{"VRCPPS (R11), Y11", "c4 41 7c 53 1b"},
|
||||
{"VRCPPS (R11), Y2", "c4 c1 7c 53 13"},
|
||||
{"VRCPPS X11, X11", "c4 41 78 53 db"},
|
||||
{"VRCPPS X11, X2", "c4 c1 78 53 d3"},
|
||||
{"VRCPPS X2, X11", "c5 78 53 da"},
|
||||
{"VRCPPS X2, X2", "c5 f8 53 d2"},
|
||||
{"VRCPPS Y11, Y11", "c4 41 7c 53 db"},
|
||||
{"VRCPPS Y11, Y2", "c4 c1 7c 53 d3"},
|
||||
{"VRCPPS Y2, Y11", "c5 7c 53 da"},
|
||||
{"VRCPPS Y2, Y2", "c5 fc 53 d2"},
|
||||
{"VRCPSS (BX), X9, X11", "c5 32 53 1b"},
|
||||
{"VRCPSS (BX), X9, X2", "c5 b2 53 13"},
|
||||
{"VRCPSS (R11), X9, X11", "c4 41 32 53 1b"},
|
||||
{"VRCPSS (R11), X9, X2", "c4 c1 32 53 13"},
|
||||
{"VRCPSS X11, X9, X11", "c4 41 32 53 db"},
|
||||
{"VRCPSS X11, X9, X2", "c4 c1 32 53 d3"},
|
||||
{"VRCPSS X2, X9, X11", "c5 32 53 da"},
|
||||
{"VRCPSS X2, X9, X2", "c5 b2 53 d2"},
|
||||
{"VROUNDSD $7, (BX), X9, X11", "c4 63 31 0b 1b 07"},
|
||||
{"VROUNDSD $7, (BX), X9, X2", "c4 e3 31 0b 13 07"},
|
||||
{"VROUNDSD $7, (R11), X9, X11", "c4 43 31 0b 1b 07"},
|
||||
{"VROUNDSD $7, (R11), X9, X2", "c4 c3 31 0b 13 07"},
|
||||
{"VROUNDSD $7, X11, X9, X11", "c4 43 31 0b db 07"},
|
||||
{"VROUNDSD $7, X11, X9, X2", "c4 c3 31 0b d3 07"},
|
||||
{"VROUNDSD $7, X2, X9, X11", "c4 63 31 0b da 07"},
|
||||
{"VROUNDSD $7, X2, X9, X2", "c4 e3 31 0b d2 07"},
|
||||
{"VROUNDSS $7, (BX), X9, X11", "c4 63 31 0a 1b 07"},
|
||||
{"VROUNDSS $7, (BX), X9, X2", "c4 e3 31 0a 13 07"},
|
||||
{"VROUNDSS $7, (R11), X9, X11", "c4 43 31 0a 1b 07"},
|
||||
{"VROUNDSS $7, (R11), X9, X2", "c4 c3 31 0a 13 07"},
|
||||
{"VROUNDSS $7, X11, X9, X11", "c4 43 31 0a db 07"},
|
||||
{"VROUNDSS $7, X11, X9, X2", "c4 c3 31 0a d3 07"},
|
||||
{"VROUNDSS $7, X2, X9, X11", "c4 63 31 0a da 07"},
|
||||
{"VROUNDSS $7, X2, X9, X2", "c4 e3 31 0a d2 07"},
|
||||
{"VRSQRTPS (BX), X11", "c5 78 52 1b"},
|
||||
{"VRSQRTPS (BX), X2", "c5 f8 52 13"},
|
||||
{"VRSQRTPS (BX), Y11", "c5 7c 52 1b"},
|
||||
{"VRSQRTPS (BX), Y2", "c5 fc 52 13"},
|
||||
{"VRSQRTPS (R11), X11", "c4 41 78 52 1b"},
|
||||
{"VRSQRTPS (R11), X2", "c4 c1 78 52 13"},
|
||||
{"VRSQRTPS (R11), Y11", "c4 41 7c 52 1b"},
|
||||
{"VRSQRTPS (R11), Y2", "c4 c1 7c 52 13"},
|
||||
{"VRSQRTPS X11, X11", "c4 41 78 52 db"},
|
||||
{"VRSQRTPS X11, X2", "c4 c1 78 52 d3"},
|
||||
{"VRSQRTPS X2, X11", "c5 78 52 da"},
|
||||
{"VRSQRTPS X2, X2", "c5 f8 52 d2"},
|
||||
{"VRSQRTPS Y11, Y11", "c4 41 7c 52 db"},
|
||||
{"VRSQRTPS Y11, Y2", "c4 c1 7c 52 d3"},
|
||||
{"VRSQRTPS Y2, Y11", "c5 7c 52 da"},
|
||||
{"VRSQRTPS Y2, Y2", "c5 fc 52 d2"},
|
||||
{"VRSQRTSS (BX), X9, X11", "c5 32 52 1b"},
|
||||
{"VRSQRTSS (BX), X9, X2", "c5 b2 52 13"},
|
||||
{"VRSQRTSS (R11), X9, X11", "c4 41 32 52 1b"},
|
||||
{"VRSQRTSS (R11), X9, X2", "c4 c1 32 52 13"},
|
||||
{"VRSQRTSS X11, X9, X11", "c4 41 32 52 db"},
|
||||
{"VRSQRTSS X11, X9, X2", "c4 c1 32 52 d3"},
|
||||
{"VRSQRTSS X2, X9, X11", "c5 32 52 da"},
|
||||
{"VRSQRTSS X2, X9, X2", "c5 b2 52 d2"},
|
||||
{"VSTMXCSR (BX)", "c5 f8 ae 1b"},
|
||||
{"VSTMXCSR (R11)", "c4 c1 78 ae 1b"},
|
||||
{"VTESTPD (BX), X11", "c4 62 79 0f 1b"},
|
||||
{"VTESTPD (BX), X2", "c4 e2 79 0f 13"},
|
||||
{"VTESTPD (BX), Y11", "c4 62 7d 0f 1b"},
|
||||
{"VTESTPD (BX), Y2", "c4 e2 7d 0f 13"},
|
||||
{"VTESTPD (R11), X11", "c4 42 79 0f 1b"},
|
||||
{"VTESTPD (R11), X2", "c4 c2 79 0f 13"},
|
||||
{"VTESTPD (R11), Y11", "c4 42 7d 0f 1b"},
|
||||
{"VTESTPD (R11), Y2", "c4 c2 7d 0f 13"},
|
||||
{"VTESTPD X11, X11", "c4 42 79 0f db"},
|
||||
{"VTESTPD X11, X2", "c4 c2 79 0f d3"},
|
||||
{"VTESTPD X2, X11", "c4 62 79 0f da"},
|
||||
{"VTESTPD X2, X2", "c4 e2 79 0f d2"},
|
||||
{"VTESTPD Y11, Y11", "c4 42 7d 0f db"},
|
||||
{"VTESTPD Y11, Y2", "c4 c2 7d 0f d3"},
|
||||
{"VTESTPD Y2, Y11", "c4 62 7d 0f da"},
|
||||
{"VTESTPD Y2, Y2", "c4 e2 7d 0f d2"},
|
||||
{"VTESTPS (BX), X11", "c4 62 79 0e 1b"},
|
||||
{"VTESTPS (BX), X2", "c4 e2 79 0e 13"},
|
||||
{"VTESTPS (BX), Y11", "c4 62 7d 0e 1b"},
|
||||
{"VTESTPS (BX), Y2", "c4 e2 7d 0e 13"},
|
||||
{"VTESTPS (R11), X11", "c4 42 79 0e 1b"},
|
||||
{"VTESTPS (R11), X2", "c4 c2 79 0e 13"},
|
||||
{"VTESTPS (R11), Y11", "c4 42 7d 0e 1b"},
|
||||
{"VTESTPS (R11), Y2", "c4 c2 7d 0e 13"},
|
||||
{"VTESTPS X11, X11", "c4 42 79 0e db"},
|
||||
{"VTESTPS X11, X2", "c4 c2 79 0e d3"},
|
||||
{"VTESTPS X2, X11", "c4 62 79 0e da"},
|
||||
{"VTESTPS X2, X2", "c4 e2 79 0e d2"},
|
||||
{"VTESTPS Y11, Y11", "c4 42 7d 0e db"},
|
||||
{"VTESTPS Y11, Y2", "c4 c2 7d 0e d3"},
|
||||
{"VTESTPS Y2, Y11", "c4 62 7d 0e da"},
|
||||
{"VTESTPS Y2, Y2", "c4 e2 7d 0e d2"},
|
||||
}
|
||||
|
||||
// TestAmd64VexCorpus assembles every corpus line and requires the same bytes
|
||||
// go tool asm emits for it.
|
||||
func TestAmd64VexCorpus(t *testing.T) {
|
||||
for _, tc := range amd64VexCorpus {
|
||||
fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n")
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.line, err)
|
||||
continue
|
||||
}
|
||||
if got := hexBytes(code); got != tc.want {
|
||||
t.Errorf("%s: got %s, want %s", tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,835 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// amd64VexParityCorpus holds every line the Go toolchain's own
|
||||
// amd64enc.s carries for the mnemonics the full-file byte sweep
|
||||
// flagged: the VEX forms the toolchain prefers for plain vector
|
||||
// registers, the compare-with-predicate family and the byte-level
|
||||
// corrections (PEXTRW's GPR fields, PUSHW and POPW widths, the double
|
||||
// shift's high register field, VCOMISS's prefix, RORX's destination
|
||||
// bit, the variable shifts' consolidated rows). Each entry lists the
|
||||
// byte strings go tool asm emits or accepts for the line; a line
|
||||
// passes when the assembled bytes match one of them.
|
||||
var amd64VexParityCorpus = []struct {
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{"PEXTRW $7, X11, (BX)", "66 44 0f 3a 15 1b 07"},
|
||||
{"PEXTRW $7, X11, (R11)", "66 45 0f 3a 15 1b 07"},
|
||||
{"PEXTRW $7, X11, DX", "66 41 0f c5 d3 07|66 44 0f 3a 15 da 07"},
|
||||
{"PEXTRW $7, X11, R11", "66 45 0f c5 db 07|66 45 0f 3a 15 db 07"},
|
||||
{"PEXTRW $7, X2, (BX)", "66 0f 3a 15 13 07"},
|
||||
{"PEXTRW $7, X2, (R11)", "66 41 0f 3a 15 13 07"},
|
||||
{"PEXTRW $7, X2, DX", "66 0f c5 d2 07|66 0f 3a 15 d2 07"},
|
||||
{"PEXTRW $7, X2, R11", "66 44 0f c5 da 07|66 41 0f 3a 15 d3 07"},
|
||||
{"POPW (BX)", "66 8f 03"},
|
||||
{"POPW (R11)", "66 41 8f 03"},
|
||||
{"POPW DX", "66 8f c2|66 5a"},
|
||||
{"POPW R11", "66 41 8f c3|66 41 5b"},
|
||||
{"PUSHW $61731", "66 68 23 f1"},
|
||||
{"PUSHW (BX)", "66 ff 33"},
|
||||
{"PUSHW (R11)", "66 41 ff 33"},
|
||||
{"PUSHW DX", "66 ff f2|66 52"},
|
||||
{"PUSHW R11", "66 41 ff f3|66 41 53"},
|
||||
{"RORXL $7, (BX), DX", "c4 e3 7b f0 13 07"},
|
||||
{"RORXL $7, (BX), R11", "c4 63 7b f0 1b 07"},
|
||||
{"RORXL $7, (R11), DX", "c4 c3 7b f0 13 07"},
|
||||
{"RORXL $7, (R11), R11", "c4 43 7b f0 1b 07"},
|
||||
{"RORXL $7, DX, DX", "c4 e3 7b f0 d2 07"},
|
||||
{"RORXL $7, DX, R11", "c4 63 7b f0 da 07"},
|
||||
{"RORXL $7, R11, DX", "c4 c3 7b f0 d3 07"},
|
||||
{"RORXL $7, R11, R11", "c4 43 7b f0 db 07"},
|
||||
{"RORXQ $7, (BX), DX", "c4 e3 fb f0 13 07"},
|
||||
{"RORXQ $7, (BX), R11", "c4 63 fb f0 1b 07"},
|
||||
{"RORXQ $7, (R11), DX", "c4 c3 fb f0 13 07"},
|
||||
{"RORXQ $7, (R11), R11", "c4 43 fb f0 1b 07"},
|
||||
{"RORXQ $7, DX, DX", "c4 e3 fb f0 d2 07"},
|
||||
{"RORXQ $7, DX, R11", "c4 63 fb f0 da 07"},
|
||||
{"RORXQ $7, R11, DX", "c4 c3 fb f0 d3 07"},
|
||||
{"RORXQ $7, R11, R11", "c4 43 fb f0 db 07"},
|
||||
{"SHLL $1, (BX)", "d1 23"},
|
||||
{"SHLL $1, (R11)", "41 d1 23"},
|
||||
{"SHLL $1, DX", "d1 e2"},
|
||||
{"SHLL $1, R11", "41 d1 e3"},
|
||||
{"SHLL $7, (BX)", "c1 23 07"},
|
||||
{"SHLL $7, (R11)", "41 c1 23 07"},
|
||||
{"SHLL $7, DX", "c1 e2 07"},
|
||||
{"SHLL $7, DX, (BX)", "0f a4 13 07"},
|
||||
{"SHLL $7, DX, (R11)", "41 0f a4 13 07"},
|
||||
{"SHLL $7, DX, DX", "0f a4 d2 07"},
|
||||
{"SHLL $7, DX, R11", "41 0f a4 d3 07"},
|
||||
{"SHLL $7, R11", "41 c1 e3 07"},
|
||||
{"SHLL $7, R11, (BX)", "44 0f a4 1b 07"},
|
||||
{"SHLL $7, R11, (R11)", "45 0f a4 1b 07"},
|
||||
{"SHLL $7, R11, DX", "44 0f a4 da 07"},
|
||||
{"SHLL $7, R11, R11", "45 0f a4 db 07"},
|
||||
{"SHLL CL, (BX)", "d3 23"},
|
||||
{"SHLL CL, (R11)", "41 d3 23"},
|
||||
{"SHLL CL, DX", "d3 e2"},
|
||||
{"SHLL CL, DX, (BX)", "0f a5 13"},
|
||||
{"SHLL CL, DX, (R11)", "41 0f a5 13"},
|
||||
{"SHLL CL, DX, DX", "0f a5 d2"},
|
||||
{"SHLL CL, DX, R11", "41 0f a5 d3"},
|
||||
{"SHLL CL, R11", "41 d3 e3"},
|
||||
{"SHLL CL, R11, (BX)", "44 0f a5 1b"},
|
||||
{"SHLL CL, R11, (R11)", "45 0f a5 1b"},
|
||||
{"SHLL CL, R11, DX", "44 0f a5 da"},
|
||||
{"SHLL CL, R11, R11", "45 0f a5 db"},
|
||||
{"SHLQ $1, (BX)", "48 d1 23"},
|
||||
{"SHLQ $1, (R11)", "49 d1 23"},
|
||||
{"SHLQ $1, DX", "48 d1 e2"},
|
||||
{"SHLQ $1, R11", "49 d1 e3"},
|
||||
{"SHLQ $7, (BX)", "48 c1 23 07"},
|
||||
{"SHLQ $7, (R11)", "49 c1 23 07"},
|
||||
{"SHLQ $7, DX", "48 c1 e2 07"},
|
||||
{"SHLQ $7, DX, (BX)", "48 0f a4 13 07"},
|
||||
{"SHLQ $7, DX, (R11)", "49 0f a4 13 07"},
|
||||
{"SHLQ $7, DX, DX", "48 0f a4 d2 07"},
|
||||
{"SHLQ $7, DX, R11", "49 0f a4 d3 07"},
|
||||
{"SHLQ $7, R11", "49 c1 e3 07"},
|
||||
{"SHLQ $7, R11, (BX)", "4c 0f a4 1b 07"},
|
||||
{"SHLQ $7, R11, (R11)", "4d 0f a4 1b 07"},
|
||||
{"SHLQ $7, R11, DX", "4c 0f a4 da 07"},
|
||||
{"SHLQ $7, R11, R11", "4d 0f a4 db 07"},
|
||||
{"SHLQ CL, (BX)", "48 d3 23"},
|
||||
{"SHLQ CL, (R11)", "49 d3 23"},
|
||||
{"SHLQ CL, DX", "48 d3 e2"},
|
||||
{"SHLQ CL, DX, (BX)", "48 0f a5 13"},
|
||||
{"SHLQ CL, DX, (R11)", "49 0f a5 13"},
|
||||
{"SHLQ CL, DX, DX", "48 0f a5 d2"},
|
||||
{"SHLQ CL, DX, R11", "49 0f a5 d3"},
|
||||
{"SHLQ CL, R11", "49 d3 e3"},
|
||||
{"SHLQ CL, R11, (BX)", "4c 0f a5 1b"},
|
||||
{"SHLQ CL, R11, (R11)", "4d 0f a5 1b"},
|
||||
{"SHLQ CL, R11, DX", "4c 0f a5 da"},
|
||||
{"SHLQ CL, R11, R11", "4d 0f a5 db"},
|
||||
{"SHLW $1, (BX)", "66 d1 23"},
|
||||
{"SHLW $1, (R11)", "66 41 d1 23"},
|
||||
{"SHLW $1, DX", "66 d1 e2"},
|
||||
{"SHLW $1, R11", "66 41 d1 e3"},
|
||||
{"SHLW $7, (BX)", "66 c1 23 07"},
|
||||
{"SHLW $7, (R11)", "66 41 c1 23 07"},
|
||||
{"SHLW $7, DX", "66 c1 e2 07"},
|
||||
{"SHLW $7, DX, (BX)", "66 0f a4 13 07"},
|
||||
{"SHLW $7, DX, (R11)", "66 41 0f a4 13 07"},
|
||||
{"SHLW $7, DX, DX", "66 0f a4 d2 07"},
|
||||
{"SHLW $7, DX, R11", "66 41 0f a4 d3 07"},
|
||||
{"SHLW $7, R11", "66 41 c1 e3 07"},
|
||||
{"SHLW $7, R11, (BX)", "66 44 0f a4 1b 07"},
|
||||
{"SHLW $7, R11, (R11)", "66 45 0f a4 1b 07"},
|
||||
{"SHLW $7, R11, DX", "66 44 0f a4 da 07"},
|
||||
{"SHLW $7, R11, R11", "66 45 0f a4 db 07"},
|
||||
{"SHLW CL, (BX)", "66 d3 23"},
|
||||
{"SHLW CL, (R11)", "66 41 d3 23"},
|
||||
{"SHLW CL, DX", "66 d3 e2"},
|
||||
{"SHLW CL, DX, (BX)", "66 0f a5 13"},
|
||||
{"SHLW CL, DX, (R11)", "66 41 0f a5 13"},
|
||||
{"SHLW CL, DX, DX", "66 0f a5 d2"},
|
||||
{"SHLW CL, DX, R11", "66 41 0f a5 d3"},
|
||||
{"SHLW CL, R11", "66 41 d3 e3"},
|
||||
{"SHLW CL, R11, (BX)", "66 44 0f a5 1b"},
|
||||
{"SHLW CL, R11, (R11)", "66 45 0f a5 1b"},
|
||||
{"SHLW CL, R11, DX", "66 44 0f a5 da"},
|
||||
{"SHLW CL, R11, R11", "66 45 0f a5 db"},
|
||||
{"VCMPPD $7, (BX), X9, X11", "c4 61 31 c2 1b 07|c5 31 c2 1b 07"},
|
||||
{"VCMPPD $7, (BX), X9, X2", "c4 e1 31 c2 13 07|c5 b1 c2 13 07"},
|
||||
{"VCMPPD $7, (BX), Y15, Y11", "c4 61 05 c2 1b 07|c5 05 c2 1b 07"},
|
||||
{"VCMPPD $7, (BX), Y15, Y2", "c4 e1 05 c2 13 07|c5 85 c2 13 07"},
|
||||
{"VCMPPD $7, (R11), X9, X11", "c4 41 31 c2 1b 07"},
|
||||
{"VCMPPD $7, (R11), X9, X2", "c4 c1 31 c2 13 07"},
|
||||
{"VCMPPD $7, (R11), Y15, Y11", "c4 41 05 c2 1b 07"},
|
||||
{"VCMPPD $7, (R11), Y15, Y2", "c4 c1 05 c2 13 07"},
|
||||
{"VCMPPD $7, X11, X9, X11", "c4 41 31 c2 db 07"},
|
||||
{"VCMPPD $7, X11, X9, X2", "c4 c1 31 c2 d3 07"},
|
||||
{"VCMPPD $7, X2, X9, X11", "c4 61 31 c2 da 07|c5 31 c2 da 07"},
|
||||
{"VCMPPD $7, X2, X9, X2", "c4 e1 31 c2 d2 07|c5 b1 c2 d2 07"},
|
||||
{"VCMPPD $7, Y11, Y15, Y11", "c4 41 05 c2 db 07"},
|
||||
{"VCMPPD $7, Y11, Y15, Y2", "c4 c1 05 c2 d3 07"},
|
||||
{"VCMPPD $7, Y2, Y15, Y11", "c4 61 05 c2 da 07|c5 05 c2 da 07"},
|
||||
{"VCMPPD $7, Y2, Y15, Y2", "c4 e1 05 c2 d2 07|c5 85 c2 d2 07"},
|
||||
{"VCMPPS $7, (BX), X9, X11", "c4 61 30 c2 1b 07|c5 30 c2 1b 07"},
|
||||
{"VCMPPS $7, (BX), X9, X2", "c4 e1 30 c2 13 07|c5 b0 c2 13 07"},
|
||||
{"VCMPPS $7, (BX), Y15, Y11", "c4 61 04 c2 1b 07|c5 04 c2 1b 07"},
|
||||
{"VCMPPS $7, (BX), Y15, Y2", "c4 e1 04 c2 13 07|c5 84 c2 13 07"},
|
||||
{"VCMPPS $7, (R11), X9, X11", "c4 41 30 c2 1b 07"},
|
||||
{"VCMPPS $7, (R11), X9, X2", "c4 c1 30 c2 13 07"},
|
||||
{"VCMPPS $7, (R11), Y15, Y11", "c4 41 04 c2 1b 07"},
|
||||
{"VCMPPS $7, (R11), Y15, Y2", "c4 c1 04 c2 13 07"},
|
||||
{"VCMPPS $7, X11, X9, X11", "c4 41 30 c2 db 07"},
|
||||
{"VCMPPS $7, X11, X9, X2", "c4 c1 30 c2 d3 07"},
|
||||
{"VCMPPS $7, X2, X9, X11", "c4 61 30 c2 da 07|c5 30 c2 da 07"},
|
||||
{"VCMPPS $7, X2, X9, X2", "c4 e1 30 c2 d2 07|c5 b0 c2 d2 07"},
|
||||
{"VCMPPS $7, Y11, Y15, Y11", "c4 41 04 c2 db 07"},
|
||||
{"VCMPPS $7, Y11, Y15, Y2", "c4 c1 04 c2 d3 07"},
|
||||
{"VCMPPS $7, Y2, Y15, Y11", "c4 61 04 c2 da 07|c5 04 c2 da 07"},
|
||||
{"VCMPPS $7, Y2, Y15, Y2", "c4 e1 04 c2 d2 07|c5 84 c2 d2 07"},
|
||||
{"VCMPSD $7, (BX), X9, X11", "c4 61 33 c2 1b 07|c5 33 c2 1b 07"},
|
||||
{"VCMPSD $7, (BX), X9, X2", "c4 e1 33 c2 13 07|c5 b3 c2 13 07"},
|
||||
{"VCMPSD $7, (R11), X9, X11", "c4 41 33 c2 1b 07"},
|
||||
{"VCMPSD $7, (R11), X9, X2", "c4 c1 33 c2 13 07"},
|
||||
{"VCMPSD $7, X11, X9, X11", "c4 41 33 c2 db 07"},
|
||||
{"VCMPSD $7, X11, X9, X2", "c4 c1 33 c2 d3 07"},
|
||||
{"VCMPSD $7, X2, X9, X11", "c4 61 33 c2 da 07|c5 33 c2 da 07"},
|
||||
{"VCMPSD $7, X2, X9, X2", "c4 e1 33 c2 d2 07|c5 b3 c2 d2 07"},
|
||||
{"VCMPSS $7, (BX), X9, X11", "c4 61 32 c2 1b 07|c5 32 c2 1b 07"},
|
||||
{"VCMPSS $7, (BX), X9, X2", "c4 e1 32 c2 13 07|c5 b2 c2 13 07"},
|
||||
{"VCMPSS $7, (R11), X9, X11", "c4 41 32 c2 1b 07"},
|
||||
{"VCMPSS $7, (R11), X9, X2", "c4 c1 32 c2 13 07"},
|
||||
{"VCMPSS $7, X11, X9, X11", "c4 41 32 c2 db 07"},
|
||||
{"VCMPSS $7, X11, X9, X2", "c4 c1 32 c2 d3 07"},
|
||||
{"VCMPSS $7, X2, X9, X11", "c4 61 32 c2 da 07|c5 32 c2 da 07"},
|
||||
{"VCMPSS $7, X2, X9, X2", "c4 e1 32 c2 d2 07|c5 b2 c2 d2 07"},
|
||||
{"VCOMISS (BX), X11", "c4 61 78 2f 1b|c5 78 2f 1b"},
|
||||
{"VCOMISS (BX), X2", "c4 e1 78 2f 13|c5 f8 2f 13"},
|
||||
{"VCOMISS (R11), X11", "c4 41 78 2f 1b"},
|
||||
{"VCOMISS (R11), X2", "c4 c1 78 2f 13"},
|
||||
{"VCOMISS X11, X11", "c4 41 78 2f db"},
|
||||
{"VCOMISS X11, X2", "c4 c1 78 2f d3"},
|
||||
{"VCOMISS X2, X11", "c4 61 78 2f da|c5 78 2f da"},
|
||||
{"VCOMISS X2, X2", "c4 e1 78 2f d2|c5 f8 2f d2"},
|
||||
{"VCVTPS2DQ (BX), X11", "c4 61 79 5b 1b|c5 79 5b 1b"},
|
||||
{"VCVTPS2DQ (BX), X2", "c4 e1 79 5b 13|c5 f9 5b 13"},
|
||||
{"VCVTPS2DQ (BX), Y11", "c4 61 7d 5b 1b|c5 7d 5b 1b"},
|
||||
{"VCVTPS2DQ (BX), Y2", "c4 e1 7d 5b 13|c5 fd 5b 13"},
|
||||
{"VCVTPS2DQ (R11), X11", "c4 41 79 5b 1b"},
|
||||
{"VCVTPS2DQ (R11), X2", "c4 c1 79 5b 13"},
|
||||
{"VCVTPS2DQ (R11), Y11", "c4 41 7d 5b 1b"},
|
||||
{"VCVTPS2DQ (R11), Y2", "c4 c1 7d 5b 13"},
|
||||
{"VCVTPS2DQ X11, X11", "c4 41 79 5b db"},
|
||||
{"VCVTPS2DQ X11, X2", "c4 c1 79 5b d3"},
|
||||
{"VCVTPS2DQ X2, X11", "c4 61 79 5b da|c5 79 5b da"},
|
||||
{"VCVTPS2DQ X2, X2", "c4 e1 79 5b d2|c5 f9 5b d2"},
|
||||
{"VCVTPS2DQ Y11, Y11", "c4 41 7d 5b db"},
|
||||
{"VCVTPS2DQ Y11, Y2", "c4 c1 7d 5b d3"},
|
||||
{"VCVTPS2DQ Y2, Y11", "c4 61 7d 5b da|c5 7d 5b da"},
|
||||
{"VCVTPS2DQ Y2, Y2", "c4 e1 7d 5b d2|c5 fd 5b d2"},
|
||||
{"VCVTTPS2DQ (BX), X11", "c4 61 7a 5b 1b|c5 7a 5b 1b"},
|
||||
{"VCVTTPS2DQ (BX), X2", "c4 e1 7a 5b 13|c5 fa 5b 13"},
|
||||
{"VCVTTPS2DQ (BX), Y11", "c4 61 7e 5b 1b|c5 7e 5b 1b"},
|
||||
{"VCVTTPS2DQ (BX), Y2", "c4 e1 7e 5b 13|c5 fe 5b 13"},
|
||||
{"VCVTTPS2DQ (R11), X11", "c4 41 7a 5b 1b"},
|
||||
{"VCVTTPS2DQ (R11), X2", "c4 c1 7a 5b 13"},
|
||||
{"VCVTTPS2DQ (R11), Y11", "c4 41 7e 5b 1b"},
|
||||
{"VCVTTPS2DQ (R11), Y2", "c4 c1 7e 5b 13"},
|
||||
{"VCVTTPS2DQ X11, X11", "c4 41 7a 5b db"},
|
||||
{"VCVTTPS2DQ X11, X2", "c4 c1 7a 5b d3"},
|
||||
{"VCVTTPS2DQ X2, X11", "c4 61 7a 5b da|c5 7a 5b da"},
|
||||
{"VCVTTPS2DQ X2, X2", "c4 e1 7a 5b d2|c5 fa 5b d2"},
|
||||
{"VCVTTPS2DQ Y11, Y11", "c4 41 7e 5b db"},
|
||||
{"VCVTTPS2DQ Y11, Y2", "c4 c1 7e 5b d3"},
|
||||
{"VCVTTPS2DQ Y2, Y11", "c4 61 7e 5b da|c5 7e 5b da"},
|
||||
{"VCVTTPS2DQ Y2, Y2", "c4 e1 7e 5b d2|c5 fe 5b d2"},
|
||||
{"VMOVLHPS X11, X9, X11", "c4 41 30 16 db"},
|
||||
{"VMOVLHPS X11, X9, X2", "c4 c1 30 16 d3"},
|
||||
{"VMOVLHPS X2, X9, X11", "c4 61 30 16 da|c5 30 16 da"},
|
||||
{"VMOVLHPS X2, X9, X2", "c4 e1 30 16 d2|c5 b0 16 d2"},
|
||||
{"VMOVUPS (BX), X11", "c4 61 78 10 1b|c5 78 10 1b"},
|
||||
{"VMOVUPS (BX), X2", "c4 e1 78 10 13|c5 f8 10 13"},
|
||||
{"VMOVUPS (BX), Y11", "c4 61 7c 10 1b|c5 7c 10 1b"},
|
||||
{"VMOVUPS (BX), Y2", "c4 e1 7c 10 13|c5 fc 10 13"},
|
||||
{"VMOVUPS (R11), X11", "c4 41 78 10 1b"},
|
||||
{"VMOVUPS (R11), X2", "c4 c1 78 10 13"},
|
||||
{"VMOVUPS (R11), Y11", "c4 41 7c 10 1b"},
|
||||
{"VMOVUPS (R11), Y2", "c4 c1 7c 10 13"},
|
||||
{"VMOVUPS X11, (BX)", "c4 61 78 11 1b|c5 78 11 1b"},
|
||||
{"VMOVUPS X11, (R11)", "c4 41 78 11 1b"},
|
||||
{"VMOVUPS X11, X11", "c4 41 78 10 db|c4 41 78 11 db"},
|
||||
{"VMOVUPS X11, X2", "c4 c1 78 10 d3|c4 61 78 11 da|c5 78 11 da"},
|
||||
{"VMOVUPS X2, (BX)", "c4 e1 78 11 13|c5 f8 11 13"},
|
||||
{"VMOVUPS X2, (R11)", "c4 c1 78 11 13"},
|
||||
{"VMOVUPS X2, X11", "c4 61 78 10 da|c5 78 10 da|c4 c1 78 11 d3"},
|
||||
{"VMOVUPS X2, X2", "c4 e1 78 10 d2|c5 f8 10 d2|c4 e1 78 11 d2|c5 f8 11 d2"},
|
||||
{"VMOVUPS Y11, (BX)", "c4 61 7c 11 1b|c5 7c 11 1b"},
|
||||
{"VMOVUPS Y11, (R11)", "c4 41 7c 11 1b"},
|
||||
{"VMOVUPS Y11, Y11", "c4 41 7c 10 db|c4 41 7c 11 db"},
|
||||
{"VMOVUPS Y11, Y2", "c4 c1 7c 10 d3|c4 61 7c 11 da|c5 7c 11 da"},
|
||||
{"VMOVUPS Y2, (BX)", "c4 e1 7c 11 13|c5 fc 11 13"},
|
||||
{"VMOVUPS Y2, (R11)", "c4 c1 7c 11 13"},
|
||||
{"VMOVUPS Y2, Y11", "c4 61 7c 10 da|c5 7c 10 da|c4 c1 7c 11 d3"},
|
||||
{"VMOVUPS Y2, Y2", "c4 e1 7c 10 d2|c5 fc 10 d2|c4 e1 7c 11 d2|c5 fc 11 d2"},
|
||||
{"VPABSB (BX), X11", "c4 62 79 1c 1b"},
|
||||
{"VPABSB (BX), X2", "c4 e2 79 1c 13"},
|
||||
{"VPABSB (BX), Y11", "c4 62 7d 1c 1b"},
|
||||
{"VPABSB (BX), Y2", "c4 e2 7d 1c 13"},
|
||||
{"VPABSB (R11), X11", "c4 42 79 1c 1b"},
|
||||
{"VPABSB (R11), X2", "c4 c2 79 1c 13"},
|
||||
{"VPABSB (R11), Y11", "c4 42 7d 1c 1b"},
|
||||
{"VPABSB (R11), Y2", "c4 c2 7d 1c 13"},
|
||||
{"VPABSB X11, X11", "c4 42 79 1c db"},
|
||||
{"VPABSB X11, X2", "c4 c2 79 1c d3"},
|
||||
{"VPABSB X2, X11", "c4 62 79 1c da"},
|
||||
{"VPABSB X2, X2", "c4 e2 79 1c d2"},
|
||||
{"VPABSB Y11, Y11", "c4 42 7d 1c db"},
|
||||
{"VPABSB Y11, Y2", "c4 c2 7d 1c d3"},
|
||||
{"VPABSB Y2, Y11", "c4 62 7d 1c da"},
|
||||
{"VPABSB Y2, Y2", "c4 e2 7d 1c d2"},
|
||||
{"VPABSD (BX), X11", "c4 62 79 1e 1b"},
|
||||
{"VPABSD (BX), X2", "c4 e2 79 1e 13"},
|
||||
{"VPABSD (BX), Y11", "c4 62 7d 1e 1b"},
|
||||
{"VPABSD (BX), Y2", "c4 e2 7d 1e 13"},
|
||||
{"VPABSD (R11), X11", "c4 42 79 1e 1b"},
|
||||
{"VPABSD (R11), X2", "c4 c2 79 1e 13"},
|
||||
{"VPABSD (R11), Y11", "c4 42 7d 1e 1b"},
|
||||
{"VPABSD (R11), Y2", "c4 c2 7d 1e 13"},
|
||||
{"VPABSD X11, X11", "c4 42 79 1e db"},
|
||||
{"VPABSD X11, X2", "c4 c2 79 1e d3"},
|
||||
{"VPABSD X2, X11", "c4 62 79 1e da"},
|
||||
{"VPABSD X2, X2", "c4 e2 79 1e d2"},
|
||||
{"VPABSD Y11, Y11", "c4 42 7d 1e db"},
|
||||
{"VPABSD Y11, Y2", "c4 c2 7d 1e d3"},
|
||||
{"VPABSD Y2, Y11", "c4 62 7d 1e da"},
|
||||
{"VPABSD Y2, Y2", "c4 e2 7d 1e d2"},
|
||||
{"VPABSW (BX), X11", "c4 62 79 1d 1b"},
|
||||
{"VPABSW (BX), X2", "c4 e2 79 1d 13"},
|
||||
{"VPABSW (BX), Y11", "c4 62 7d 1d 1b"},
|
||||
{"VPABSW (BX), Y2", "c4 e2 7d 1d 13"},
|
||||
{"VPABSW (R11), X11", "c4 42 79 1d 1b"},
|
||||
{"VPABSW (R11), X2", "c4 c2 79 1d 13"},
|
||||
{"VPABSW (R11), Y11", "c4 42 7d 1d 1b"},
|
||||
{"VPABSW (R11), Y2", "c4 c2 7d 1d 13"},
|
||||
{"VPABSW X11, X11", "c4 42 79 1d db"},
|
||||
{"VPABSW X11, X2", "c4 c2 79 1d d3"},
|
||||
{"VPABSW X2, X11", "c4 62 79 1d da"},
|
||||
{"VPABSW X2, X2", "c4 e2 79 1d d2"},
|
||||
{"VPABSW Y11, Y11", "c4 42 7d 1d db"},
|
||||
{"VPABSW Y11, Y2", "c4 c2 7d 1d d3"},
|
||||
{"VPABSW Y2, Y11", "c4 62 7d 1d da"},
|
||||
{"VPABSW Y2, Y2", "c4 e2 7d 1d d2"},
|
||||
{"VPACKSSWB (BX), X9, X11", "c4 61 31 63 1b|c5 31 63 1b"},
|
||||
{"VPACKSSWB (BX), X9, X2", "c4 e1 31 63 13|c5 b1 63 13"},
|
||||
{"VPACKSSWB (BX), Y15, Y11", "c4 61 05 63 1b|c5 05 63 1b"},
|
||||
{"VPACKSSWB (BX), Y15, Y2", "c4 e1 05 63 13|c5 85 63 13"},
|
||||
{"VPACKSSWB (R11), X9, X11", "c4 41 31 63 1b"},
|
||||
{"VPACKSSWB (R11), X9, X2", "c4 c1 31 63 13"},
|
||||
{"VPACKSSWB (R11), Y15, Y11", "c4 41 05 63 1b"},
|
||||
{"VPACKSSWB (R11), Y15, Y2", "c4 c1 05 63 13"},
|
||||
{"VPACKSSWB X11, X9, X11", "c4 41 31 63 db"},
|
||||
{"VPACKSSWB X11, X9, X2", "c4 c1 31 63 d3"},
|
||||
{"VPACKSSWB X2, X9, X11", "c4 61 31 63 da|c5 31 63 da"},
|
||||
{"VPACKSSWB X2, X9, X2", "c4 e1 31 63 d2|c5 b1 63 d2"},
|
||||
{"VPACKSSWB Y11, Y15, Y11", "c4 41 05 63 db"},
|
||||
{"VPACKSSWB Y11, Y15, Y2", "c4 c1 05 63 d3"},
|
||||
{"VPACKSSWB Y2, Y15, Y11", "c4 61 05 63 da|c5 05 63 da"},
|
||||
{"VPACKSSWB Y2, Y15, Y2", "c4 e1 05 63 d2|c5 85 63 d2"},
|
||||
{"VPACKUSDW (BX), X9, X11", "c4 62 31 2b 1b"},
|
||||
{"VPACKUSDW (BX), X9, X2", "c4 e2 31 2b 13"},
|
||||
{"VPACKUSDW (BX), Y15, Y11", "c4 62 05 2b 1b"},
|
||||
{"VPACKUSDW (BX), Y15, Y2", "c4 e2 05 2b 13"},
|
||||
{"VPACKUSDW (R11), X9, X11", "c4 42 31 2b 1b"},
|
||||
{"VPACKUSDW (R11), X9, X2", "c4 c2 31 2b 13"},
|
||||
{"VPACKUSDW (R11), Y15, Y11", "c4 42 05 2b 1b"},
|
||||
{"VPACKUSDW (R11), Y15, Y2", "c4 c2 05 2b 13"},
|
||||
{"VPACKUSDW X11, X9, X11", "c4 42 31 2b db"},
|
||||
{"VPACKUSDW X11, X9, X2", "c4 c2 31 2b d3"},
|
||||
{"VPACKUSDW X2, X9, X11", "c4 62 31 2b da"},
|
||||
{"VPACKUSDW X2, X9, X2", "c4 e2 31 2b d2"},
|
||||
{"VPACKUSDW Y11, Y15, Y11", "c4 42 05 2b db"},
|
||||
{"VPACKUSDW Y11, Y15, Y2", "c4 c2 05 2b d3"},
|
||||
{"VPACKUSDW Y2, Y15, Y11", "c4 62 05 2b da"},
|
||||
{"VPACKUSDW Y2, Y15, Y2", "c4 e2 05 2b d2"},
|
||||
{"VPACKUSWB (BX), X9, X11", "c4 61 31 67 1b|c5 31 67 1b"},
|
||||
{"VPACKUSWB (BX), X9, X2", "c4 e1 31 67 13|c5 b1 67 13"},
|
||||
{"VPACKUSWB (BX), Y15, Y11", "c4 61 05 67 1b|c5 05 67 1b"},
|
||||
{"VPACKUSWB (BX), Y15, Y2", "c4 e1 05 67 13|c5 85 67 13"},
|
||||
{"VPACKUSWB (R11), X9, X11", "c4 41 31 67 1b"},
|
||||
{"VPACKUSWB (R11), X9, X2", "c4 c1 31 67 13"},
|
||||
{"VPACKUSWB (R11), Y15, Y11", "c4 41 05 67 1b"},
|
||||
{"VPACKUSWB (R11), Y15, Y2", "c4 c1 05 67 13"},
|
||||
{"VPACKUSWB X11, X9, X11", "c4 41 31 67 db"},
|
||||
{"VPACKUSWB X11, X9, X2", "c4 c1 31 67 d3"},
|
||||
{"VPACKUSWB X2, X9, X11", "c4 61 31 67 da|c5 31 67 da"},
|
||||
{"VPACKUSWB X2, X9, X2", "c4 e1 31 67 d2|c5 b1 67 d2"},
|
||||
{"VPACKUSWB Y11, Y15, Y11", "c4 41 05 67 db"},
|
||||
{"VPACKUSWB Y11, Y15, Y2", "c4 c1 05 67 d3"},
|
||||
{"VPACKUSWB Y2, Y15, Y11", "c4 61 05 67 da|c5 05 67 da"},
|
||||
{"VPACKUSWB Y2, Y15, Y2", "c4 e1 05 67 d2|c5 85 67 d2"},
|
||||
{"VPADDB (BX), X9, X11", "c4 61 31 fc 1b|c5 31 fc 1b"},
|
||||
{"VPADDB (BX), X9, X2", "c4 e1 31 fc 13|c5 b1 fc 13"},
|
||||
{"VPADDB (BX), Y15, Y11", "c4 61 05 fc 1b|c5 05 fc 1b"},
|
||||
{"VPADDB (BX), Y15, Y2", "c4 e1 05 fc 13|c5 85 fc 13"},
|
||||
{"VPADDB (R11), X9, X11", "c4 41 31 fc 1b"},
|
||||
{"VPADDB (R11), X9, X2", "c4 c1 31 fc 13"},
|
||||
{"VPADDB (R11), Y15, Y11", "c4 41 05 fc 1b"},
|
||||
{"VPADDB (R11), Y15, Y2", "c4 c1 05 fc 13"},
|
||||
{"VPADDB X11, X9, X11", "c4 41 31 fc db"},
|
||||
{"VPADDB X11, X9, X2", "c4 c1 31 fc d3"},
|
||||
{"VPADDB X2, X9, X11", "c4 61 31 fc da|c5 31 fc da"},
|
||||
{"VPADDB X2, X9, X2", "c4 e1 31 fc d2|c5 b1 fc d2"},
|
||||
{"VPADDB Y11, Y15, Y11", "c4 41 05 fc db"},
|
||||
{"VPADDB Y11, Y15, Y2", "c4 c1 05 fc d3"},
|
||||
{"VPADDB Y2, Y15, Y11", "c4 61 05 fc da|c5 05 fc da"},
|
||||
{"VPADDB Y2, Y15, Y2", "c4 e1 05 fc d2|c5 85 fc d2"},
|
||||
{"VPADDW (BX), X9, X11", "c4 61 31 fd 1b|c5 31 fd 1b"},
|
||||
{"VPADDW (BX), X9, X2", "c4 e1 31 fd 13|c5 b1 fd 13"},
|
||||
{"VPADDW (BX), Y15, Y11", "c4 61 05 fd 1b|c5 05 fd 1b"},
|
||||
{"VPADDW (BX), Y15, Y2", "c4 e1 05 fd 13|c5 85 fd 13"},
|
||||
{"VPADDW (R11), X9, X11", "c4 41 31 fd 1b"},
|
||||
{"VPADDW (R11), X9, X2", "c4 c1 31 fd 13"},
|
||||
{"VPADDW (R11), Y15, Y11", "c4 41 05 fd 1b"},
|
||||
{"VPADDW (R11), Y15, Y2", "c4 c1 05 fd 13"},
|
||||
{"VPADDW X11, X9, X11", "c4 41 31 fd db"},
|
||||
{"VPADDW X11, X9, X2", "c4 c1 31 fd d3"},
|
||||
{"VPADDW X2, X9, X11", "c4 61 31 fd da|c5 31 fd da"},
|
||||
{"VPADDW X2, X9, X2", "c4 e1 31 fd d2|c5 b1 fd d2"},
|
||||
{"VPADDW Y11, Y15, Y11", "c4 41 05 fd db"},
|
||||
{"VPADDW Y11, Y15, Y2", "c4 c1 05 fd d3"},
|
||||
{"VPADDW Y2, Y15, Y11", "c4 61 05 fd da|c5 05 fd da"},
|
||||
{"VPADDW Y2, Y15, Y2", "c4 e1 05 fd d2|c5 85 fd d2"},
|
||||
{"VPAVGB (BX), X9, X11", "c4 61 31 e0 1b|c5 31 e0 1b"},
|
||||
{"VPAVGB (BX), X9, X2", "c4 e1 31 e0 13|c5 b1 e0 13"},
|
||||
{"VPAVGB (BX), Y15, Y11", "c4 61 05 e0 1b|c5 05 e0 1b"},
|
||||
{"VPAVGB (BX), Y15, Y2", "c4 e1 05 e0 13|c5 85 e0 13"},
|
||||
{"VPAVGB (R11), X9, X11", "c4 41 31 e0 1b"},
|
||||
{"VPAVGB (R11), X9, X2", "c4 c1 31 e0 13"},
|
||||
{"VPAVGB (R11), Y15, Y11", "c4 41 05 e0 1b"},
|
||||
{"VPAVGB (R11), Y15, Y2", "c4 c1 05 e0 13"},
|
||||
{"VPAVGB X11, X9, X11", "c4 41 31 e0 db"},
|
||||
{"VPAVGB X11, X9, X2", "c4 c1 31 e0 d3"},
|
||||
{"VPAVGB X2, X9, X11", "c4 61 31 e0 da|c5 31 e0 da"},
|
||||
{"VPAVGB X2, X9, X2", "c4 e1 31 e0 d2|c5 b1 e0 d2"},
|
||||
{"VPAVGB Y11, Y15, Y11", "c4 41 05 e0 db"},
|
||||
{"VPAVGB Y11, Y15, Y2", "c4 c1 05 e0 d3"},
|
||||
{"VPAVGB Y2, Y15, Y11", "c4 61 05 e0 da|c5 05 e0 da"},
|
||||
{"VPAVGB Y2, Y15, Y2", "c4 e1 05 e0 d2|c5 85 e0 d2"},
|
||||
{"VPAVGW (BX), X9, X11", "c4 61 31 e3 1b|c5 31 e3 1b"},
|
||||
{"VPAVGW (BX), X9, X2", "c4 e1 31 e3 13|c5 b1 e3 13"},
|
||||
{"VPAVGW (BX), Y15, Y11", "c4 61 05 e3 1b|c5 05 e3 1b"},
|
||||
{"VPAVGW (BX), Y15, Y2", "c4 e1 05 e3 13|c5 85 e3 13"},
|
||||
{"VPAVGW (R11), X9, X11", "c4 41 31 e3 1b"},
|
||||
{"VPAVGW (R11), X9, X2", "c4 c1 31 e3 13"},
|
||||
{"VPAVGW (R11), Y15, Y11", "c4 41 05 e3 1b"},
|
||||
{"VPAVGW (R11), Y15, Y2", "c4 c1 05 e3 13"},
|
||||
{"VPAVGW X11, X9, X11", "c4 41 31 e3 db"},
|
||||
{"VPAVGW X11, X9, X2", "c4 c1 31 e3 d3"},
|
||||
{"VPAVGW X2, X9, X11", "c4 61 31 e3 da|c5 31 e3 da"},
|
||||
{"VPAVGW X2, X9, X2", "c4 e1 31 e3 d2|c5 b1 e3 d2"},
|
||||
{"VPAVGW Y11, Y15, Y11", "c4 41 05 e3 db"},
|
||||
{"VPAVGW Y11, Y15, Y2", "c4 c1 05 e3 d3"},
|
||||
{"VPAVGW Y2, Y15, Y11", "c4 61 05 e3 da|c5 05 e3 da"},
|
||||
{"VPAVGW Y2, Y15, Y2", "c4 e1 05 e3 d2|c5 85 e3 d2"},
|
||||
{"VPMADDUBSW (BX), X9, X11", "c4 62 31 04 1b"},
|
||||
{"VPMADDUBSW (BX), X9, X2", "c4 e2 31 04 13"},
|
||||
{"VPMADDUBSW (BX), Y15, Y11", "c4 62 05 04 1b"},
|
||||
{"VPMADDUBSW (BX), Y15, Y2", "c4 e2 05 04 13"},
|
||||
{"VPMADDUBSW (R11), X9, X11", "c4 42 31 04 1b"},
|
||||
{"VPMADDUBSW (R11), X9, X2", "c4 c2 31 04 13"},
|
||||
{"VPMADDUBSW (R11), Y15, Y11", "c4 42 05 04 1b"},
|
||||
{"VPMADDUBSW (R11), Y15, Y2", "c4 c2 05 04 13"},
|
||||
{"VPMADDUBSW X11, X9, X11", "c4 42 31 04 db"},
|
||||
{"VPMADDUBSW X11, X9, X2", "c4 c2 31 04 d3"},
|
||||
{"VPMADDUBSW X2, X9, X11", "c4 62 31 04 da"},
|
||||
{"VPMADDUBSW X2, X9, X2", "c4 e2 31 04 d2"},
|
||||
{"VPMADDUBSW Y11, Y15, Y11", "c4 42 05 04 db"},
|
||||
{"VPMADDUBSW Y11, Y15, Y2", "c4 c2 05 04 d3"},
|
||||
{"VPMADDUBSW Y2, Y15, Y11", "c4 62 05 04 da"},
|
||||
{"VPMADDUBSW Y2, Y15, Y2", "c4 e2 05 04 d2"},
|
||||
{"VPMADDWD (BX), X9, X11", "c4 61 31 f5 1b|c5 31 f5 1b"},
|
||||
{"VPMADDWD (BX), X9, X2", "c4 e1 31 f5 13|c5 b1 f5 13"},
|
||||
{"VPMADDWD (BX), Y15, Y11", "c4 61 05 f5 1b|c5 05 f5 1b"},
|
||||
{"VPMADDWD (BX), Y15, Y2", "c4 e1 05 f5 13|c5 85 f5 13"},
|
||||
{"VPMADDWD (R11), X9, X11", "c4 41 31 f5 1b"},
|
||||
{"VPMADDWD (R11), X9, X2", "c4 c1 31 f5 13"},
|
||||
{"VPMADDWD (R11), Y15, Y11", "c4 41 05 f5 1b"},
|
||||
{"VPMADDWD (R11), Y15, Y2", "c4 c1 05 f5 13"},
|
||||
{"VPMADDWD X11, X9, X11", "c4 41 31 f5 db"},
|
||||
{"VPMADDWD X11, X9, X2", "c4 c1 31 f5 d3"},
|
||||
{"VPMADDWD X2, X9, X11", "c4 61 31 f5 da|c5 31 f5 da"},
|
||||
{"VPMADDWD X2, X9, X2", "c4 e1 31 f5 d2|c5 b1 f5 d2"},
|
||||
{"VPMADDWD Y11, Y15, Y11", "c4 41 05 f5 db"},
|
||||
{"VPMADDWD Y11, Y15, Y2", "c4 c1 05 f5 d3"},
|
||||
{"VPMADDWD Y2, Y15, Y11", "c4 61 05 f5 da|c5 05 f5 da"},
|
||||
{"VPMADDWD Y2, Y15, Y2", "c4 e1 05 f5 d2|c5 85 f5 d2"},
|
||||
{"VPMAXSB (BX), X9, X11", "c4 62 31 3c 1b"},
|
||||
{"VPMAXSB (BX), X9, X2", "c4 e2 31 3c 13"},
|
||||
{"VPMAXSB (BX), Y15, Y11", "c4 62 05 3c 1b"},
|
||||
{"VPMAXSB (BX), Y15, Y2", "c4 e2 05 3c 13"},
|
||||
{"VPMAXSB (R11), X9, X11", "c4 42 31 3c 1b"},
|
||||
{"VPMAXSB (R11), X9, X2", "c4 c2 31 3c 13"},
|
||||
{"VPMAXSB (R11), Y15, Y11", "c4 42 05 3c 1b"},
|
||||
{"VPMAXSB (R11), Y15, Y2", "c4 c2 05 3c 13"},
|
||||
{"VPMAXSB X11, X9, X11", "c4 42 31 3c db"},
|
||||
{"VPMAXSB X11, X9, X2", "c4 c2 31 3c d3"},
|
||||
{"VPMAXSB X2, X9, X11", "c4 62 31 3c da"},
|
||||
{"VPMAXSB X2, X9, X2", "c4 e2 31 3c d2"},
|
||||
{"VPMAXSB Y11, Y15, Y11", "c4 42 05 3c db"},
|
||||
{"VPMAXSB Y11, Y15, Y2", "c4 c2 05 3c d3"},
|
||||
{"VPMAXSB Y2, Y15, Y11", "c4 62 05 3c da"},
|
||||
{"VPMAXSB Y2, Y15, Y2", "c4 e2 05 3c d2"},
|
||||
{"VPMAXSD (BX), X9, X11", "c4 62 31 3d 1b"},
|
||||
{"VPMAXSD (BX), X9, X2", "c4 e2 31 3d 13"},
|
||||
{"VPMAXSD (BX), Y15, Y11", "c4 62 05 3d 1b"},
|
||||
{"VPMAXSD (BX), Y15, Y2", "c4 e2 05 3d 13"},
|
||||
{"VPMAXSD (R11), X9, X11", "c4 42 31 3d 1b"},
|
||||
{"VPMAXSD (R11), X9, X2", "c4 c2 31 3d 13"},
|
||||
{"VPMAXSD (R11), Y15, Y11", "c4 42 05 3d 1b"},
|
||||
{"VPMAXSD (R11), Y15, Y2", "c4 c2 05 3d 13"},
|
||||
{"VPMAXSD X11, X9, X11", "c4 42 31 3d db"},
|
||||
{"VPMAXSD X11, X9, X2", "c4 c2 31 3d d3"},
|
||||
{"VPMAXSD X2, X9, X11", "c4 62 31 3d da"},
|
||||
{"VPMAXSD X2, X9, X2", "c4 e2 31 3d d2"},
|
||||
{"VPMAXSD Y11, Y15, Y11", "c4 42 05 3d db"},
|
||||
{"VPMAXSD Y11, Y15, Y2", "c4 c2 05 3d d3"},
|
||||
{"VPMAXSD Y2, Y15, Y11", "c4 62 05 3d da"},
|
||||
{"VPMAXSD Y2, Y15, Y2", "c4 e2 05 3d d2"},
|
||||
{"VPMAXSW (BX), X9, X11", "c4 61 31 ee 1b|c5 31 ee 1b"},
|
||||
{"VPMAXSW (BX), X9, X2", "c4 e1 31 ee 13|c5 b1 ee 13"},
|
||||
{"VPMAXSW (BX), Y15, Y11", "c4 61 05 ee 1b|c5 05 ee 1b"},
|
||||
{"VPMAXSW (BX), Y15, Y2", "c4 e1 05 ee 13|c5 85 ee 13"},
|
||||
{"VPMAXSW (R11), X9, X11", "c4 41 31 ee 1b"},
|
||||
{"VPMAXSW (R11), X9, X2", "c4 c1 31 ee 13"},
|
||||
{"VPMAXSW (R11), Y15, Y11", "c4 41 05 ee 1b"},
|
||||
{"VPMAXSW (R11), Y15, Y2", "c4 c1 05 ee 13"},
|
||||
{"VPMAXSW X11, X9, X11", "c4 41 31 ee db"},
|
||||
{"VPMAXSW X11, X9, X2", "c4 c1 31 ee d3"},
|
||||
{"VPMAXSW X2, X9, X11", "c4 61 31 ee da|c5 31 ee da"},
|
||||
{"VPMAXSW X2, X9, X2", "c4 e1 31 ee d2|c5 b1 ee d2"},
|
||||
{"VPMAXSW Y11, Y15, Y11", "c4 41 05 ee db"},
|
||||
{"VPMAXSW Y11, Y15, Y2", "c4 c1 05 ee d3"},
|
||||
{"VPMAXSW Y2, Y15, Y11", "c4 61 05 ee da|c5 05 ee da"},
|
||||
{"VPMAXSW Y2, Y15, Y2", "c4 e1 05 ee d2|c5 85 ee d2"},
|
||||
{"VPMAXUB (BX), X9, X11", "c4 61 31 de 1b|c5 31 de 1b"},
|
||||
{"VPMAXUB (BX), X9, X2", "c4 e1 31 de 13|c5 b1 de 13"},
|
||||
{"VPMAXUB (BX), Y15, Y11", "c4 61 05 de 1b|c5 05 de 1b"},
|
||||
{"VPMAXUB (BX), Y15, Y2", "c4 e1 05 de 13|c5 85 de 13"},
|
||||
{"VPMAXUB (R11), X9, X11", "c4 41 31 de 1b"},
|
||||
{"VPMAXUB (R11), X9, X2", "c4 c1 31 de 13"},
|
||||
{"VPMAXUB (R11), Y15, Y11", "c4 41 05 de 1b"},
|
||||
{"VPMAXUB (R11), Y15, Y2", "c4 c1 05 de 13"},
|
||||
{"VPMAXUB X11, X9, X11", "c4 41 31 de db"},
|
||||
{"VPMAXUB X11, X9, X2", "c4 c1 31 de d3"},
|
||||
{"VPMAXUB X2, X9, X11", "c4 61 31 de da|c5 31 de da"},
|
||||
{"VPMAXUB X2, X9, X2", "c4 e1 31 de d2|c5 b1 de d2"},
|
||||
{"VPMAXUB Y11, Y15, Y11", "c4 41 05 de db"},
|
||||
{"VPMAXUB Y11, Y15, Y2", "c4 c1 05 de d3"},
|
||||
{"VPMAXUB Y2, Y15, Y11", "c4 61 05 de da|c5 05 de da"},
|
||||
{"VPMAXUB Y2, Y15, Y2", "c4 e1 05 de d2|c5 85 de d2"},
|
||||
{"VPMAXUD (BX), X9, X11", "c4 62 31 3f 1b"},
|
||||
{"VPMAXUD (BX), X9, X2", "c4 e2 31 3f 13"},
|
||||
{"VPMAXUD (BX), Y15, Y11", "c4 62 05 3f 1b"},
|
||||
{"VPMAXUD (BX), Y15, Y2", "c4 e2 05 3f 13"},
|
||||
{"VPMAXUD (R11), X9, X11", "c4 42 31 3f 1b"},
|
||||
{"VPMAXUD (R11), X9, X2", "c4 c2 31 3f 13"},
|
||||
{"VPMAXUD (R11), Y15, Y11", "c4 42 05 3f 1b"},
|
||||
{"VPMAXUD (R11), Y15, Y2", "c4 c2 05 3f 13"},
|
||||
{"VPMAXUD X11, X9, X11", "c4 42 31 3f db"},
|
||||
{"VPMAXUD X11, X9, X2", "c4 c2 31 3f d3"},
|
||||
{"VPMAXUD X2, X9, X11", "c4 62 31 3f da"},
|
||||
{"VPMAXUD X2, X9, X2", "c4 e2 31 3f d2"},
|
||||
{"VPMAXUD Y11, Y15, Y11", "c4 42 05 3f db"},
|
||||
{"VPMAXUD Y11, Y15, Y2", "c4 c2 05 3f d3"},
|
||||
{"VPMAXUD Y2, Y15, Y11", "c4 62 05 3f da"},
|
||||
{"VPMAXUD Y2, Y15, Y2", "c4 e2 05 3f d2"},
|
||||
{"VPMAXUW (BX), X9, X11", "c4 62 31 3e 1b"},
|
||||
{"VPMAXUW (BX), X9, X2", "c4 e2 31 3e 13"},
|
||||
{"VPMAXUW (BX), Y15, Y11", "c4 62 05 3e 1b"},
|
||||
{"VPMAXUW (BX), Y15, Y2", "c4 e2 05 3e 13"},
|
||||
{"VPMAXUW (R11), X9, X11", "c4 42 31 3e 1b"},
|
||||
{"VPMAXUW (R11), X9, X2", "c4 c2 31 3e 13"},
|
||||
{"VPMAXUW (R11), Y15, Y11", "c4 42 05 3e 1b"},
|
||||
{"VPMAXUW (R11), Y15, Y2", "c4 c2 05 3e 13"},
|
||||
{"VPMAXUW X11, X9, X11", "c4 42 31 3e db"},
|
||||
{"VPMAXUW X11, X9, X2", "c4 c2 31 3e d3"},
|
||||
{"VPMAXUW X2, X9, X11", "c4 62 31 3e da"},
|
||||
{"VPMAXUW X2, X9, X2", "c4 e2 31 3e d2"},
|
||||
{"VPMAXUW Y11, Y15, Y11", "c4 42 05 3e db"},
|
||||
{"VPMAXUW Y11, Y15, Y2", "c4 c2 05 3e d3"},
|
||||
{"VPMAXUW Y2, Y15, Y11", "c4 62 05 3e da"},
|
||||
{"VPMAXUW Y2, Y15, Y2", "c4 e2 05 3e d2"},
|
||||
{"VPMINSB (BX), X9, X11", "c4 62 31 38 1b"},
|
||||
{"VPMINSB (BX), X9, X2", "c4 e2 31 38 13"},
|
||||
{"VPMINSB (BX), Y15, Y11", "c4 62 05 38 1b"},
|
||||
{"VPMINSB (BX), Y15, Y2", "c4 e2 05 38 13"},
|
||||
{"VPMINSB (R11), X9, X11", "c4 42 31 38 1b"},
|
||||
{"VPMINSB (R11), X9, X2", "c4 c2 31 38 13"},
|
||||
{"VPMINSB (R11), Y15, Y11", "c4 42 05 38 1b"},
|
||||
{"VPMINSB (R11), Y15, Y2", "c4 c2 05 38 13"},
|
||||
{"VPMINSB X11, X9, X11", "c4 42 31 38 db"},
|
||||
{"VPMINSB X11, X9, X2", "c4 c2 31 38 d3"},
|
||||
{"VPMINSB X2, X9, X11", "c4 62 31 38 da"},
|
||||
{"VPMINSB X2, X9, X2", "c4 e2 31 38 d2"},
|
||||
{"VPMINSB Y11, Y15, Y11", "c4 42 05 38 db"},
|
||||
{"VPMINSB Y11, Y15, Y2", "c4 c2 05 38 d3"},
|
||||
{"VPMINSB Y2, Y15, Y11", "c4 62 05 38 da"},
|
||||
{"VPMINSB Y2, Y15, Y2", "c4 e2 05 38 d2"},
|
||||
{"VPMINSD (BX), X9, X11", "c4 62 31 39 1b"},
|
||||
{"VPMINSD (BX), X9, X2", "c4 e2 31 39 13"},
|
||||
{"VPMINSD (BX), Y15, Y11", "c4 62 05 39 1b"},
|
||||
{"VPMINSD (BX), Y15, Y2", "c4 e2 05 39 13"},
|
||||
{"VPMINSD (R11), X9, X11", "c4 42 31 39 1b"},
|
||||
{"VPMINSD (R11), X9, X2", "c4 c2 31 39 13"},
|
||||
{"VPMINSD (R11), Y15, Y11", "c4 42 05 39 1b"},
|
||||
{"VPMINSD (R11), Y15, Y2", "c4 c2 05 39 13"},
|
||||
{"VPMINSD X11, X9, X11", "c4 42 31 39 db"},
|
||||
{"VPMINSD X11, X9, X2", "c4 c2 31 39 d3"},
|
||||
{"VPMINSD X2, X9, X11", "c4 62 31 39 da"},
|
||||
{"VPMINSD X2, X9, X2", "c4 e2 31 39 d2"},
|
||||
{"VPMINSD Y11, Y15, Y11", "c4 42 05 39 db"},
|
||||
{"VPMINSD Y11, Y15, Y2", "c4 c2 05 39 d3"},
|
||||
{"VPMINSD Y2, Y15, Y11", "c4 62 05 39 da"},
|
||||
{"VPMINSD Y2, Y15, Y2", "c4 e2 05 39 d2"},
|
||||
{"VPMINSW (BX), X9, X11", "c4 61 31 ea 1b|c5 31 ea 1b"},
|
||||
{"VPMINSW (BX), X9, X2", "c4 e1 31 ea 13|c5 b1 ea 13"},
|
||||
{"VPMINSW (BX), Y15, Y11", "c4 61 05 ea 1b|c5 05 ea 1b"},
|
||||
{"VPMINSW (BX), Y15, Y2", "c4 e1 05 ea 13|c5 85 ea 13"},
|
||||
{"VPMINSW (R11), X9, X11", "c4 41 31 ea 1b"},
|
||||
{"VPMINSW (R11), X9, X2", "c4 c1 31 ea 13"},
|
||||
{"VPMINSW (R11), Y15, Y11", "c4 41 05 ea 1b"},
|
||||
{"VPMINSW (R11), Y15, Y2", "c4 c1 05 ea 13"},
|
||||
{"VPMINSW X11, X9, X11", "c4 41 31 ea db"},
|
||||
{"VPMINSW X11, X9, X2", "c4 c1 31 ea d3"},
|
||||
{"VPMINSW X2, X9, X11", "c4 61 31 ea da|c5 31 ea da"},
|
||||
{"VPMINSW X2, X9, X2", "c4 e1 31 ea d2|c5 b1 ea d2"},
|
||||
{"VPMINSW Y11, Y15, Y11", "c4 41 05 ea db"},
|
||||
{"VPMINSW Y11, Y15, Y2", "c4 c1 05 ea d3"},
|
||||
{"VPMINSW Y2, Y15, Y11", "c4 61 05 ea da|c5 05 ea da"},
|
||||
{"VPMINSW Y2, Y15, Y2", "c4 e1 05 ea d2|c5 85 ea d2"},
|
||||
{"VPMINUB (BX), X9, X11", "c4 61 31 da 1b|c5 31 da 1b"},
|
||||
{"VPMINUB (BX), X9, X2", "c4 e1 31 da 13|c5 b1 da 13"},
|
||||
{"VPMINUB (BX), Y15, Y11", "c4 61 05 da 1b|c5 05 da 1b"},
|
||||
{"VPMINUB (BX), Y15, Y2", "c4 e1 05 da 13|c5 85 da 13"},
|
||||
{"VPMINUB (R11), X9, X11", "c4 41 31 da 1b"},
|
||||
{"VPMINUB (R11), X9, X2", "c4 c1 31 da 13"},
|
||||
{"VPMINUB (R11), Y15, Y11", "c4 41 05 da 1b"},
|
||||
{"VPMINUB (R11), Y15, Y2", "c4 c1 05 da 13"},
|
||||
{"VPMINUB X11, X9, X11", "c4 41 31 da db"},
|
||||
{"VPMINUB X11, X9, X2", "c4 c1 31 da d3"},
|
||||
{"VPMINUB X2, X9, X11", "c4 61 31 da da|c5 31 da da"},
|
||||
{"VPMINUB X2, X9, X2", "c4 e1 31 da d2|c5 b1 da d2"},
|
||||
{"VPMINUB Y11, Y15, Y11", "c4 41 05 da db"},
|
||||
{"VPMINUB Y11, Y15, Y2", "c4 c1 05 da d3"},
|
||||
{"VPMINUB Y2, Y15, Y11", "c4 61 05 da da|c5 05 da da"},
|
||||
{"VPMINUB Y2, Y15, Y2", "c4 e1 05 da d2|c5 85 da d2"},
|
||||
{"VPMINUD (BX), X9, X11", "c4 62 31 3b 1b"},
|
||||
{"VPMINUD (BX), X9, X2", "c4 e2 31 3b 13"},
|
||||
{"VPMINUD (BX), Y15, Y11", "c4 62 05 3b 1b"},
|
||||
{"VPMINUD (BX), Y15, Y2", "c4 e2 05 3b 13"},
|
||||
{"VPMINUD (R11), X9, X11", "c4 42 31 3b 1b"},
|
||||
{"VPMINUD (R11), X9, X2", "c4 c2 31 3b 13"},
|
||||
{"VPMINUD (R11), Y15, Y11", "c4 42 05 3b 1b"},
|
||||
{"VPMINUD (R11), Y15, Y2", "c4 c2 05 3b 13"},
|
||||
{"VPMINUD X11, X9, X11", "c4 42 31 3b db"},
|
||||
{"VPMINUD X11, X9, X2", "c4 c2 31 3b d3"},
|
||||
{"VPMINUD X2, X9, X11", "c4 62 31 3b da"},
|
||||
{"VPMINUD X2, X9, X2", "c4 e2 31 3b d2"},
|
||||
{"VPMINUD Y11, Y15, Y11", "c4 42 05 3b db"},
|
||||
{"VPMINUD Y11, Y15, Y2", "c4 c2 05 3b d3"},
|
||||
{"VPMINUD Y2, Y15, Y11", "c4 62 05 3b da"},
|
||||
{"VPMINUD Y2, Y15, Y2", "c4 e2 05 3b d2"},
|
||||
{"VPMINUW (BX), X9, X11", "c4 62 31 3a 1b"},
|
||||
{"VPMINUW (BX), X9, X2", "c4 e2 31 3a 13"},
|
||||
{"VPMINUW (BX), Y15, Y11", "c4 62 05 3a 1b"},
|
||||
{"VPMINUW (BX), Y15, Y2", "c4 e2 05 3a 13"},
|
||||
{"VPMINUW (R11), X9, X11", "c4 42 31 3a 1b"},
|
||||
{"VPMINUW (R11), X9, X2", "c4 c2 31 3a 13"},
|
||||
{"VPMINUW (R11), Y15, Y11", "c4 42 05 3a 1b"},
|
||||
{"VPMINUW (R11), Y15, Y2", "c4 c2 05 3a 13"},
|
||||
{"VPMINUW X11, X9, X11", "c4 42 31 3a db"},
|
||||
{"VPMINUW X11, X9, X2", "c4 c2 31 3a d3"},
|
||||
{"VPMINUW X2, X9, X11", "c4 62 31 3a da"},
|
||||
{"VPMINUW X2, X9, X2", "c4 e2 31 3a d2"},
|
||||
{"VPMINUW Y11, Y15, Y11", "c4 42 05 3a db"},
|
||||
{"VPMINUW Y11, Y15, Y2", "c4 c2 05 3a d3"},
|
||||
{"VPMINUW Y2, Y15, Y11", "c4 62 05 3a da"},
|
||||
{"VPMINUW Y2, Y15, Y2", "c4 e2 05 3a d2"},
|
||||
{"VPMOVSXBW (BX), X11", "c4 62 79 20 1b"},
|
||||
{"VPMOVSXBW (BX), X2", "c4 e2 79 20 13"},
|
||||
{"VPMOVSXBW (BX), Y11", "c4 62 7d 20 1b"},
|
||||
{"VPMOVSXBW (BX), Y2", "c4 e2 7d 20 13"},
|
||||
{"VPMOVSXBW (R11), X11", "c4 42 79 20 1b"},
|
||||
{"VPMOVSXBW (R11), X2", "c4 c2 79 20 13"},
|
||||
{"VPMOVSXBW (R11), Y11", "c4 42 7d 20 1b"},
|
||||
{"VPMOVSXBW (R11), Y2", "c4 c2 7d 20 13"},
|
||||
{"VPMOVSXBW X11, X11", "c4 42 79 20 db"},
|
||||
{"VPMOVSXBW X11, X2", "c4 c2 79 20 d3"},
|
||||
{"VPMOVSXBW X11, Y11", "c4 42 7d 20 db"},
|
||||
{"VPMOVSXBW X11, Y2", "c4 c2 7d 20 d3"},
|
||||
{"VPMOVSXBW X2, X11", "c4 62 79 20 da"},
|
||||
{"VPMOVSXBW X2, X2", "c4 e2 79 20 d2"},
|
||||
{"VPMOVSXBW X2, Y11", "c4 62 7d 20 da"},
|
||||
{"VPMOVSXBW X2, Y2", "c4 e2 7d 20 d2"},
|
||||
{"VPMULHUW (BX), X9, X11", "c4 61 31 e4 1b|c5 31 e4 1b"},
|
||||
{"VPMULHUW (BX), X9, X2", "c4 e1 31 e4 13|c5 b1 e4 13"},
|
||||
{"VPMULHUW (BX), Y15, Y11", "c4 61 05 e4 1b|c5 05 e4 1b"},
|
||||
{"VPMULHUW (BX), Y15, Y2", "c4 e1 05 e4 13|c5 85 e4 13"},
|
||||
{"VPMULHUW (R11), X9, X11", "c4 41 31 e4 1b"},
|
||||
{"VPMULHUW (R11), X9, X2", "c4 c1 31 e4 13"},
|
||||
{"VPMULHUW (R11), Y15, Y11", "c4 41 05 e4 1b"},
|
||||
{"VPMULHUW (R11), Y15, Y2", "c4 c1 05 e4 13"},
|
||||
{"VPMULHUW X11, X9, X11", "c4 41 31 e4 db"},
|
||||
{"VPMULHUW X11, X9, X2", "c4 c1 31 e4 d3"},
|
||||
{"VPMULHUW X2, X9, X11", "c4 61 31 e4 da|c5 31 e4 da"},
|
||||
{"VPMULHUW X2, X9, X2", "c4 e1 31 e4 d2|c5 b1 e4 d2"},
|
||||
{"VPMULHUW Y11, Y15, Y11", "c4 41 05 e4 db"},
|
||||
{"VPMULHUW Y11, Y15, Y2", "c4 c1 05 e4 d3"},
|
||||
{"VPMULHUW Y2, Y15, Y11", "c4 61 05 e4 da|c5 05 e4 da"},
|
||||
{"VPMULHUW Y2, Y15, Y2", "c4 e1 05 e4 d2|c5 85 e4 d2"},
|
||||
{"VPMULLW (BX), X9, X11", "c4 61 31 d5 1b|c5 31 d5 1b"},
|
||||
{"VPMULLW (BX), X9, X2", "c4 e1 31 d5 13|c5 b1 d5 13"},
|
||||
{"VPMULLW (BX), Y15, Y11", "c4 61 05 d5 1b|c5 05 d5 1b"},
|
||||
{"VPMULLW (BX), Y15, Y2", "c4 e1 05 d5 13|c5 85 d5 13"},
|
||||
{"VPMULLW (R11), X9, X11", "c4 41 31 d5 1b"},
|
||||
{"VPMULLW (R11), X9, X2", "c4 c1 31 d5 13"},
|
||||
{"VPMULLW (R11), Y15, Y11", "c4 41 05 d5 1b"},
|
||||
{"VPMULLW (R11), Y15, Y2", "c4 c1 05 d5 13"},
|
||||
{"VPMULLW X11, X9, X11", "c4 41 31 d5 db"},
|
||||
{"VPMULLW X11, X9, X2", "c4 c1 31 d5 d3"},
|
||||
{"VPMULLW X2, X9, X11", "c4 61 31 d5 da|c5 31 d5 da"},
|
||||
{"VPMULLW X2, X9, X2", "c4 e1 31 d5 d2|c5 b1 d5 d2"},
|
||||
{"VPMULLW Y11, Y15, Y11", "c4 41 05 d5 db"},
|
||||
{"VPMULLW Y11, Y15, Y2", "c4 c1 05 d5 d3"},
|
||||
{"VPMULLW Y2, Y15, Y11", "c4 61 05 d5 da|c5 05 d5 da"},
|
||||
{"VPMULLW Y2, Y15, Y2", "c4 e1 05 d5 d2|c5 85 d5 d2"},
|
||||
{"VPSLLVD (BX), X9, X11", "c4 62 31 47 1b"},
|
||||
{"VPSLLVD (BX), X9, X2", "c4 e2 31 47 13"},
|
||||
{"VPSLLVD (BX), Y15, Y11", "c4 62 05 47 1b"},
|
||||
{"VPSLLVD (BX), Y15, Y2", "c4 e2 05 47 13"},
|
||||
{"VPSLLVD (R11), X9, X11", "c4 42 31 47 1b"},
|
||||
{"VPSLLVD (R11), X9, X2", "c4 c2 31 47 13"},
|
||||
{"VPSLLVD (R11), Y15, Y11", "c4 42 05 47 1b"},
|
||||
{"VPSLLVD (R11), Y15, Y2", "c4 c2 05 47 13"},
|
||||
{"VPSLLVD X11, X9, X11", "c4 42 31 47 db"},
|
||||
{"VPSLLVD X11, X9, X2", "c4 c2 31 47 d3"},
|
||||
{"VPSLLVD X2, X9, X11", "c4 62 31 47 da"},
|
||||
{"VPSLLVD X2, X9, X2", "c4 e2 31 47 d2"},
|
||||
{"VPSLLVD Y11, Y15, Y11", "c4 42 05 47 db"},
|
||||
{"VPSLLVD Y11, Y15, Y2", "c4 c2 05 47 d3"},
|
||||
{"VPSLLVD Y2, Y15, Y11", "c4 62 05 47 da"},
|
||||
{"VPSLLVD Y2, Y15, Y2", "c4 e2 05 47 d2"},
|
||||
{"VPSLLVQ (BX), X9, X11", "c4 62 b1 47 1b"},
|
||||
{"VPSLLVQ (BX), X9, X2", "c4 e2 b1 47 13"},
|
||||
{"VPSLLVQ (BX), Y15, Y11", "c4 62 85 47 1b"},
|
||||
{"VPSLLVQ (BX), Y15, Y2", "c4 e2 85 47 13"},
|
||||
{"VPSLLVQ (R11), X9, X11", "c4 42 b1 47 1b"},
|
||||
{"VPSLLVQ (R11), X9, X2", "c4 c2 b1 47 13"},
|
||||
{"VPSLLVQ (R11), Y15, Y11", "c4 42 85 47 1b"},
|
||||
{"VPSLLVQ (R11), Y15, Y2", "c4 c2 85 47 13"},
|
||||
{"VPSLLVQ X11, X9, X11", "c4 42 b1 47 db"},
|
||||
{"VPSLLVQ X11, X9, X2", "c4 c2 b1 47 d3"},
|
||||
{"VPSLLVQ X2, X9, X11", "c4 62 b1 47 da"},
|
||||
{"VPSLLVQ X2, X9, X2", "c4 e2 b1 47 d2"},
|
||||
{"VPSLLVQ Y11, Y15, Y11", "c4 42 85 47 db"},
|
||||
{"VPSLLVQ Y11, Y15, Y2", "c4 c2 85 47 d3"},
|
||||
{"VPSLLVQ Y2, Y15, Y11", "c4 62 85 47 da"},
|
||||
{"VPSLLVQ Y2, Y15, Y2", "c4 e2 85 47 d2"},
|
||||
{"VPSRAVD (BX), X9, X11", "c4 62 31 46 1b"},
|
||||
{"VPSRAVD (BX), X9, X2", "c4 e2 31 46 13"},
|
||||
{"VPSRAVD (BX), Y15, Y11", "c4 62 05 46 1b"},
|
||||
{"VPSRAVD (BX), Y15, Y2", "c4 e2 05 46 13"},
|
||||
{"VPSRAVD (R11), X9, X11", "c4 42 31 46 1b"},
|
||||
{"VPSRAVD (R11), X9, X2", "c4 c2 31 46 13"},
|
||||
{"VPSRAVD (R11), Y15, Y11", "c4 42 05 46 1b"},
|
||||
{"VPSRAVD (R11), Y15, Y2", "c4 c2 05 46 13"},
|
||||
{"VPSRAVD X11, X9, X11", "c4 42 31 46 db"},
|
||||
{"VPSRAVD X11, X9, X2", "c4 c2 31 46 d3"},
|
||||
{"VPSRAVD X2, X9, X11", "c4 62 31 46 da"},
|
||||
{"VPSRAVD X2, X9, X2", "c4 e2 31 46 d2"},
|
||||
{"VPSRAVD Y11, Y15, Y11", "c4 42 05 46 db"},
|
||||
{"VPSRAVD Y11, Y15, Y2", "c4 c2 05 46 d3"},
|
||||
{"VPSRAVD Y2, Y15, Y11", "c4 62 05 46 da"},
|
||||
{"VPSRAVD Y2, Y15, Y2", "c4 e2 05 46 d2"},
|
||||
{"VPSRLVD (BX), X9, X11", "c4 62 31 45 1b"},
|
||||
{"VPSRLVD (BX), X9, X2", "c4 e2 31 45 13"},
|
||||
{"VPSRLVD (BX), Y15, Y11", "c4 62 05 45 1b"},
|
||||
{"VPSRLVD (BX), Y15, Y2", "c4 e2 05 45 13"},
|
||||
{"VPSRLVD (R11), X9, X11", "c4 42 31 45 1b"},
|
||||
{"VPSRLVD (R11), X9, X2", "c4 c2 31 45 13"},
|
||||
{"VPSRLVD (R11), Y15, Y11", "c4 42 05 45 1b"},
|
||||
{"VPSRLVD (R11), Y15, Y2", "c4 c2 05 45 13"},
|
||||
{"VPSRLVD X11, X9, X11", "c4 42 31 45 db"},
|
||||
{"VPSRLVD X11, X9, X2", "c4 c2 31 45 d3"},
|
||||
{"VPSRLVD X2, X9, X11", "c4 62 31 45 da"},
|
||||
{"VPSRLVD X2, X9, X2", "c4 e2 31 45 d2"},
|
||||
{"VPSRLVD Y11, Y15, Y11", "c4 42 05 45 db"},
|
||||
{"VPSRLVD Y11, Y15, Y2", "c4 c2 05 45 d3"},
|
||||
{"VPSRLVD Y2, Y15, Y11", "c4 62 05 45 da"},
|
||||
{"VPSRLVD Y2, Y15, Y2", "c4 e2 05 45 d2"},
|
||||
{"VPSRLVQ (BX), X9, X11", "c4 62 b1 45 1b"},
|
||||
{"VPSRLVQ (BX), X9, X2", "c4 e2 b1 45 13"},
|
||||
{"VPSRLVQ (BX), Y15, Y11", "c4 62 85 45 1b"},
|
||||
{"VPSRLVQ (BX), Y15, Y2", "c4 e2 85 45 13"},
|
||||
{"VPSRLVQ (R11), X9, X11", "c4 42 b1 45 1b"},
|
||||
{"VPSRLVQ (R11), X9, X2", "c4 c2 b1 45 13"},
|
||||
{"VPSRLVQ (R11), Y15, Y11", "c4 42 85 45 1b"},
|
||||
{"VPSRLVQ (R11), Y15, Y2", "c4 c2 85 45 13"},
|
||||
{"VPSRLVQ X11, X9, X11", "c4 42 b1 45 db"},
|
||||
{"VPSRLVQ X11, X9, X2", "c4 c2 b1 45 d3"},
|
||||
{"VPSRLVQ X2, X9, X11", "c4 62 b1 45 da"},
|
||||
{"VPSRLVQ X2, X9, X2", "c4 e2 b1 45 d2"},
|
||||
{"VPSRLVQ Y11, Y15, Y11", "c4 42 85 45 db"},
|
||||
{"VPSRLVQ Y11, Y15, Y2", "c4 c2 85 45 d3"},
|
||||
{"VPSRLVQ Y2, Y15, Y11", "c4 62 85 45 da"},
|
||||
{"VPSRLVQ Y2, Y15, Y2", "c4 e2 85 45 d2"},
|
||||
{"VPSUBB (BX), X9, X11", "c4 61 31 f8 1b|c5 31 f8 1b"},
|
||||
{"VPSUBB (BX), X9, X2", "c4 e1 31 f8 13|c5 b1 f8 13"},
|
||||
{"VPSUBB (BX), Y15, Y11", "c4 61 05 f8 1b|c5 05 f8 1b"},
|
||||
{"VPSUBB (BX), Y15, Y2", "c4 e1 05 f8 13|c5 85 f8 13"},
|
||||
{"VPSUBB (R11), X9, X11", "c4 41 31 f8 1b"},
|
||||
{"VPSUBB (R11), X9, X2", "c4 c1 31 f8 13"},
|
||||
{"VPSUBB (R11), Y15, Y11", "c4 41 05 f8 1b"},
|
||||
{"VPSUBB (R11), Y15, Y2", "c4 c1 05 f8 13"},
|
||||
{"VPSUBB X11, X9, X11", "c4 41 31 f8 db"},
|
||||
{"VPSUBB X11, X9, X2", "c4 c1 31 f8 d3"},
|
||||
{"VPSUBB X2, X9, X11", "c4 61 31 f8 da|c5 31 f8 da"},
|
||||
{"VPSUBB X2, X9, X2", "c4 e1 31 f8 d2|c5 b1 f8 d2"},
|
||||
{"VPSUBB Y11, Y15, Y11", "c4 41 05 f8 db"},
|
||||
{"VPSUBB Y11, Y15, Y2", "c4 c1 05 f8 d3"},
|
||||
{"VPSUBB Y2, Y15, Y11", "c4 61 05 f8 da|c5 05 f8 da"},
|
||||
{"VPSUBB Y2, Y15, Y2", "c4 e1 05 f8 d2|c5 85 f8 d2"},
|
||||
{"VPSUBW (BX), X9, X11", "c4 61 31 f9 1b|c5 31 f9 1b"},
|
||||
{"VPSUBW (BX), X9, X2", "c4 e1 31 f9 13|c5 b1 f9 13"},
|
||||
{"VPSUBW (BX), Y15, Y11", "c4 61 05 f9 1b|c5 05 f9 1b"},
|
||||
{"VPSUBW (BX), Y15, Y2", "c4 e1 05 f9 13|c5 85 f9 13"},
|
||||
{"VPSUBW (R11), X9, X11", "c4 41 31 f9 1b"},
|
||||
{"VPSUBW (R11), X9, X2", "c4 c1 31 f9 13"},
|
||||
{"VPSUBW (R11), Y15, Y11", "c4 41 05 f9 1b"},
|
||||
{"VPSUBW (R11), Y15, Y2", "c4 c1 05 f9 13"},
|
||||
{"VPSUBW X11, X9, X11", "c4 41 31 f9 db"},
|
||||
{"VPSUBW X11, X9, X2", "c4 c1 31 f9 d3"},
|
||||
{"VPSUBW X2, X9, X11", "c4 61 31 f9 da|c5 31 f9 da"},
|
||||
{"VPSUBW X2, X9, X2", "c4 e1 31 f9 d2|c5 b1 f9 d2"},
|
||||
{"VPSUBW Y11, Y15, Y11", "c4 41 05 f9 db"},
|
||||
{"VPSUBW Y11, Y15, Y2", "c4 c1 05 f9 d3"},
|
||||
{"VPSUBW Y2, Y15, Y11", "c4 61 05 f9 da|c5 05 f9 da"},
|
||||
{"VPSUBW Y2, Y15, Y2", "c4 e1 05 f9 d2|c5 85 f9 d2"},
|
||||
{"VSHUFPS $7, (BX), X9, X11", "c4 61 30 c6 1b 07|c5 30 c6 1b 07"},
|
||||
{"VSHUFPS $7, (BX), X9, X2", "c4 e1 30 c6 13 07|c5 b0 c6 13 07"},
|
||||
{"VSHUFPS $7, (BX), Y15, Y11", "c4 61 04 c6 1b 07|c5 04 c6 1b 07"},
|
||||
{"VSHUFPS $7, (BX), Y15, Y2", "c4 e1 04 c6 13 07|c5 84 c6 13 07"},
|
||||
{"VSHUFPS $7, (R11), X9, X11", "c4 41 30 c6 1b 07"},
|
||||
{"VSHUFPS $7, (R11), X9, X2", "c4 c1 30 c6 13 07"},
|
||||
{"VSHUFPS $7, (R11), Y15, Y11", "c4 41 04 c6 1b 07"},
|
||||
{"VSHUFPS $7, (R11), Y15, Y2", "c4 c1 04 c6 13 07"},
|
||||
{"VSHUFPS $7, X11, X9, X11", "c4 41 30 c6 db 07"},
|
||||
{"VSHUFPS $7, X11, X9, X2", "c4 c1 30 c6 d3 07"},
|
||||
{"VSHUFPS $7, X2, X9, X11", "c4 61 30 c6 da 07|c5 30 c6 da 07"},
|
||||
{"VSHUFPS $7, X2, X9, X2", "c4 e1 30 c6 d2 07|c5 b0 c6 d2 07"},
|
||||
{"VSHUFPS $7, Y11, Y15, Y11", "c4 41 04 c6 db 07"},
|
||||
{"VSHUFPS $7, Y11, Y15, Y2", "c4 c1 04 c6 d3 07"},
|
||||
{"VSHUFPS $7, Y2, Y15, Y11", "c4 61 04 c6 da 07|c5 04 c6 da 07"},
|
||||
{"VSHUFPS $7, Y2, Y15, Y2", "c4 e1 04 c6 d2 07|c5 84 c6 d2 07"},
|
||||
}
|
||||
|
||||
// TestAmd64VexParityCorpus assembles every corpus line and requires the
|
||||
// bytes to match one of the alternatives go tool asm accepts.
|
||||
func TestAmd64VexParityCorpus(t *testing.T) {
|
||||
for _, tc := range amd64VexParityCorpus {
|
||||
fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n")
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.line, err)
|
||||
continue
|
||||
}
|
||||
got := hexBytes(code)
|
||||
if !slices.Contains(strings.Split(tc.want, "|"), got) {
|
||||
t.Errorf("%s: got %s, want one of %s", tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The byte forms of the suffixed scalar families answer to the register
|
||||
// operands as much as to the mnemonic: the toolchain's own disassembly
|
||||
// prints them with the L suffix or none at all (the rendered suffix rides
|
||||
// the operand-size attribute), the register names carrying the width. The
|
||||
// tests here pin that reconciliation: the renderer's spellings encode the
|
||||
// byte form byte for byte, the W and Q spellings never ride a byte
|
||||
// register, and the classic names stay size-agnostic.
|
||||
|
||||
// TestOperandWidthByteForms pins the renderer's spellings: an L suffix or
|
||||
// no suffix with a byte-spelled register encodes the 8-bit form, every
|
||||
// register joining at its low byte. Every row's bytes were cross-checked
|
||||
// against `go tool asm -S` over the B-suffixed spelling of the same
|
||||
// operands (the toolchain rejects the L spelling itself), and against the
|
||||
// toolchain's objdump text for the byte encodings.
|
||||
func TestOperandWidthByteForms(t *testing.T) {
|
||||
r11 := Reg{idx: 11, size: 8}
|
||||
r8 := Reg{idx: 8, size: 1}
|
||||
r9 := Reg{idx: 9, size: 1}
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
// The atomics pair: XADD and CMPXCHG drop to the 0F C0/0F B0 byte
|
||||
// opcodes the L spelling would otherwise widen past.
|
||||
{"XADDL DL, DL", []Operand{DL, DL}, "0fc0d2"},
|
||||
{"XADDB DL, DL", []Operand{DL, DL}, "0fc0d2"},
|
||||
{"XADDL R8B, R9B", []Operand{r8, r9}, "450fc0c1"},
|
||||
{"CMPXCHGL DL, DL", []Operand{DL, DL}, "0fb0d2"},
|
||||
{"XCHGL DL, DL", []Operand{DL, DL}, "86d2"},
|
||||
{"XCHGL DL, 0(BX)", []Operand{DL, Ptr(BX, 0, 1)}, "8613"},
|
||||
// The ALU immediates: the accumulator short form for the AL
|
||||
// spelling, the generic 0x80 /digit elsewhere.
|
||||
{"CMPL AL, $7", []Operand{AL, Imm(7)}, "3c07"},
|
||||
{"ADDL $7, AL", []Operand{Imm(7), AL}, "0407"},
|
||||
{"SUBL $7, AL", []Operand{Imm(7), AL}, "2c07"},
|
||||
{"ANDL $7, AL", []Operand{Imm(7), AL}, "2407"},
|
||||
{"SBBL $7, AL", []Operand{Imm(7), AL}, "1c07"},
|
||||
{"SBBL $7, DL", []Operand{Imm(7), DL}, "80da07"},
|
||||
{"ADDB $3, AX", []Operand{Imm(3), AX}, "80c003"},
|
||||
{"ORB $7, AX", []Operand{Imm(7), AX}, "80c807"},
|
||||
{"SBBB $7, AX", []Operand{Imm(7), AX}, "80d807"},
|
||||
// The register forms, the extended register riding REX.B and the
|
||||
// byte source in the reg field.
|
||||
{"SBBL DL, R11", []Operand{DL, r11}, "4118d3"},
|
||||
{"TESTL R11, DL", []Operand{r11, DL}, "4484da"},
|
||||
{"TESTB $7, AX", []Operand{Imm(7), AX}, "f6c007"},
|
||||
{"TESTB R11, DL", []Operand{r11, DL}, "4484da"},
|
||||
// CRC32 keeps the F0 byte opcode under the unsuffixed spelling.
|
||||
{"CRC32 DL, R11", []Operand{DL, r11}, "f2440f38f0da"},
|
||||
{"CRC32B DL, R11", []Operand{DL, r11}, "f2440f38f0da"},
|
||||
{"CRC32B AX, CX", []Operand{AX, CX}, "f20f38f0c8"},
|
||||
// The move and unary families follow the same rule.
|
||||
{"MOVL $7, DL", []Operand{Imm(7), DL}, "b207"},
|
||||
{"MOVB $7, DL", []Operand{Imm(7), DL}, "b207"},
|
||||
{"MOVB AX, AL", []Operand{AX, AL}, "88c0"},
|
||||
{"INCL DL", []Operand{DL}, "fec2"},
|
||||
{"NEGL DL", []Operand{DL}, "f6da"},
|
||||
{"IMULL DL", []Operand{DL}, "f6ea"},
|
||||
{"SHLL $2, DL", []Operand{Imm(2), DL}, "c0e202"},
|
||||
{"ROLL CL, DL", []Operand{CL, DL}, "d2c2"},
|
||||
// The shift count never narrows the shifted value.
|
||||
{"RCLW CL, 0(R11)", []Operand{CL, Ptr(r11, 0, 2)}, "6641d313"},
|
||||
{"RORQ CL, AX", []Operand{CL, AX}, "48d3c8"},
|
||||
// The size-agnostic spellings stay width-free: the L and Q forms
|
||||
// of the same families are untouched by the reconciliation.
|
||||
{"XADDL AX, CX", []Operand{AX, CX}, "0fc1c1"},
|
||||
{"ADDL $7, AX", []Operand{Imm(7), AX}, "83c007"},
|
||||
{"ADDL $256, AX", []Operand{Imm(256), AX}, "0500010000"},
|
||||
{"CRC32L AX, CX", []Operand{AX, CX}, "f20f38f1c8"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Encode(mnemonicOf(tt.name), tt.ops...)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tt.name, err)
|
||||
}
|
||||
if want := unhex(tt.want); !bytes.Equal(got, want) {
|
||||
t.Errorf("%s: % x, want % x", tt.name, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOperandWidthConflicts pins the refusals: the W and Q spellings never
|
||||
// ride a byte register (go tool asm rejects MOVQ AL, AX and its siblings
|
||||
// outright), and neither does the MOVD alias of the quad move.
|
||||
func TestOperandWidthConflicts(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
ops []Operand
|
||||
}{
|
||||
{"MOVQ AL, AX", []Operand{AL, AX}},
|
||||
{"MOVQ AX, AL", []Operand{AX, AL}},
|
||||
{"MOVQ DL, DL", []Operand{DL, DL}},
|
||||
{"MOVW $7, DL", []Operand{Imm(7), DL}},
|
||||
{"MOVD AL, AX", []Operand{AL, AX}},
|
||||
{"XADDQ DL, DL", []Operand{DL, DL}},
|
||||
{"CMPXCHGQ DL, DL", []Operand{DL, DL}},
|
||||
{"XCHGQ DL, DL", []Operand{DL, DL}},
|
||||
{"SHLQ $2, DL", []Operand{Imm(2), DL}},
|
||||
{"INCQ DL", []Operand{DL}},
|
||||
{"IMULQ DL", []Operand{DL}},
|
||||
{"TESTQ R11, DL", []Operand{Reg{idx: 11, size: 8}, DL}},
|
||||
{"CRC32Q DL, R11", []Operand{DL, Reg{idx: 11, size: 8}}},
|
||||
{"CRC32W DL, R11", []Operand{DL, Reg{idx: 11, size: 8}}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := Encode(mnemonicOf(tt.name), tt.ops...); err == nil {
|
||||
t.Errorf("%s: encoded, want the byte-register conflict refused", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOperandWidthDifferential is the byte-parity oracle for the same
|
||||
// reconciliation: the B-suffixed spellings, which go tool asm accepts, must
|
||||
// encode identically through both assemblers, the agnostic names included
|
||||
// (their low byte joins the byte form) and the accumulator division (AL
|
||||
// short, AX generic) with them.
|
||||
func TestOperandWidthDifferential(t *testing.T) {
|
||||
kernel := "#include \"textflag.h\"\n" +
|
||||
"TEXT \u00b7bytewidth(SB), NOSPLIT, $0\n" +
|
||||
"\tXADDB DL, DL\n" +
|
||||
"\tXADDB R8B, R9B\n" +
|
||||
"\tXADDL AX, CX\n" +
|
||||
"\tCMPXCHGB DL, DL\n" +
|
||||
"\tXCHGB DL, DL\n" +
|
||||
"\tXCHGB DL, 0(BX)\n" +
|
||||
"\tCMPB AL, $7\n" +
|
||||
"\tADDB $7, AL\n" +
|
||||
"\tADDB $3, AX\n" +
|
||||
"\tSUBB $7, AL\n" +
|
||||
"\tANDB $7, AL\n" +
|
||||
"\tSBBB $7, AL\n" +
|
||||
"\tSBBB $7, DL\n" +
|
||||
"\tSBBB DL, R11\n" +
|
||||
"\tORB $7, AX\n" +
|
||||
"\tTESTB R11, DL\n" +
|
||||
"\tTESTB $7, AX\n" +
|
||||
"\tCRC32B DL, R11\n" +
|
||||
"\tCRC32B AX, CX\n" +
|
||||
"\tCRC32B R8B, CX\n" +
|
||||
"\tCRC32L AX, CX\n" +
|
||||
"\tMOVB $7, DL\n" +
|
||||
"\tMOVB $3, AX\n" +
|
||||
"\tMOVB AX, AL\n" +
|
||||
"\tINCB DL\n" +
|
||||
"\tNEGB DL\n" +
|
||||
"\tIMULB DL\n" +
|
||||
"\tMULB CL\n" +
|
||||
"\tSHLB $2, DL\n" +
|
||||
"\tROLB CL, DL\n" +
|
||||
"\tRET\n"
|
||||
path := filepath.Join(t.TempDir(), "bytewidth_amd64.s")
|
||||
if err := os.WriteFile(path, []byte(kernel), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gt := oracleFuncCode(t, toolAsmObject(t, path, ""))
|
||||
goCode, ok := gt["bytewidth.bytewidth"]
|
||||
if !ok {
|
||||
t.Fatalf("oracle: bytewidth function missing (%d functions)", len(gt))
|
||||
}
|
||||
gasmCode := code(path, kernel)
|
||||
if gasmCode == nil {
|
||||
t.Fatal("gasm: assemble failed")
|
||||
}
|
||||
if !bytes.Equal(gasmCode, goCode) {
|
||||
t.Errorf("byte width kernel:\ngasm %x\ngo %x", gasmCode, goCode)
|
||||
}
|
||||
}
|
||||
|
||||
// mnemonicOf returns the first whitespace-free token of a rendered row.
|
||||
func mnemonicOf(text string) string {
|
||||
for i := 0; i < len(text); i++ {
|
||||
if text[i] == ' ' || text[i] == ',' {
|
||||
return text[:i]
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// unhex decodes a hex string, failing the test on malformed input.
|
||||
func unhex(s string) []byte {
|
||||
out, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic("unhex: " + err.Error())
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// This file implements the x87 floating-point family the Go assembler
|
||||
// carries: the no-operand stack controls, the two-register arithmetic pair,
|
||||
// the register compares, the memory loads and stores and the FXSAVE pair.
|
||||
// Every encoding here is pinned byte for byte against go tool asm through
|
||||
// the corpus lines in amd64_x87_test.go.
|
||||
|
||||
// x87NoOperand maps the no-operand x87 instruction to its postfix byte
|
||||
// inside the D9 escape: D9 <postfix>, no ModR/M, no operand.
|
||||
var x87NoOperand = map[string]byte{
|
||||
"F2XM1": 0xF0,
|
||||
"FABS": 0xE1,
|
||||
"FCHS": 0xE0,
|
||||
"FCOS": 0xFF,
|
||||
"FDECSTP": 0xF6,
|
||||
"FINCSTP": 0xF7,
|
||||
"FLD1": 0xE8,
|
||||
"FLDL2E": 0xEA,
|
||||
"FLDL2T": 0xE9,
|
||||
"FLDLG2": 0xEC,
|
||||
"FLDPI": 0xEB,
|
||||
"FNOP": 0xD0,
|
||||
"FPATAN": 0xF3,
|
||||
"FPREM": 0xF8,
|
||||
"FPREM1": 0xF5,
|
||||
"FPTAN": 0xF2,
|
||||
"FRNDINT": 0xFC,
|
||||
"FSCALE": 0xFD,
|
||||
"FSIN": 0xFE,
|
||||
"FSINCOS": 0xFB,
|
||||
"FSQRT": 0xFA,
|
||||
"FTST": 0xE4,
|
||||
"FXAM": 0xE5,
|
||||
"FXTRACT": 0xF4,
|
||||
"FYL2X": 0xF1,
|
||||
"FYL2XP1": 0xF9,
|
||||
}
|
||||
|
||||
// x87ArithSpec describes one member of the D8/DC two-register arithmetic
|
||||
// pair. The D8 form reads ST(0) as its second operand (FADDD F2, F0), the
|
||||
// DC form ST(0) as its first (FADDD F0, F2) or a memory source (FADDD (BX),
|
||||
// F0). FDIV is the odd member: with ST(0) as the first operand the toolchain
|
||||
// assembles the reversed register form (DC F8+i, the FDIVR digit), so the DC
|
||||
// digit differs from the memory digit there.
|
||||
type x87ArithSpec struct {
|
||||
d8Digit int // the /digit of the D8 form (dst = ST(0))
|
||||
dcDigit int // the /digit of the DC register form (src = ST(0))
|
||||
memDigit int // the /digit of the DC memory form (dst = ST(0))
|
||||
}
|
||||
|
||||
// x87Arith maps the arithmetic mnemonics to their digits.
|
||||
var x87Arith = map[string]x87ArithSpec{
|
||||
"FADDD": {0, 0, 0},
|
||||
"FCOMD": {2, 2, 2},
|
||||
"FDIVD": {6, 7, 6},
|
||||
}
|
||||
|
||||
// x87FCmov maps the conditional x87 moves to their escape byte and postfix
|
||||
// base (the C0/C8/D0/D8 group the condition selects); the compared register
|
||||
// rides the postfix's low three bits.
|
||||
var x87FCmov = map[string][2]byte{
|
||||
"FCMOVB": {0xDA, 0xC0},
|
||||
"FCMOVBE": {0xDA, 0xD0},
|
||||
"FCMOVE": {0xDA, 0xC8},
|
||||
"FCMOVNB": {0xDB, 0xC0},
|
||||
"FCMOVNBE": {0xDB, 0xD0},
|
||||
"FCMOVNE": {0xDB, 0xC8},
|
||||
"FCMOVNU": {0xDB, 0xD8},
|
||||
"FCMOVU": {0xDA, 0xD8},
|
||||
}
|
||||
|
||||
// x87Compare maps the register compare pair to their escape byte; the
|
||||
// register form is escape F0+i (mod 11, reg 110, rm = the compared stack
|
||||
// register), ST(0) fixed as the second operand.
|
||||
var x87Compare = map[string]byte{
|
||||
"FCOMI": 0xDB,
|
||||
"FCOMIP": 0xDF,
|
||||
}
|
||||
|
||||
// x87MemUnary maps the one-memory-operand x87 controls to their escape byte
|
||||
// and /digit.
|
||||
var x87MemUnary = map[string]struct {
|
||||
escape byte
|
||||
digit int
|
||||
}{
|
||||
"FBLD": {0xDF, 4},
|
||||
"FBSTP": {0xDF, 6},
|
||||
"FLDCW": {0xD9, 5},
|
||||
}
|
||||
|
||||
// x87Fxsav maps the FXSAVE pair to their /digit in the 0F AE group; the 64
|
||||
// spellings carry REX.W.
|
||||
var x87Fxsav = map[string]struct {
|
||||
digit int
|
||||
rexW bool
|
||||
}{
|
||||
"FXSAVE": {0, false},
|
||||
"FXSAVE64": {0, true},
|
||||
"FXRSTOR": {1, false},
|
||||
"FXRSTOR64": {1, true},
|
||||
}
|
||||
|
||||
// encodeX87 encodes the x87 family. It reports whether the mnemonic belongs
|
||||
// to the family; a false result hands the mnemonic back to the caller, an
|
||||
// error result a failed attempt to encode it.
|
||||
func (e *enc) encodeX87(upper string, ops []Operand) (bool, error) {
|
||||
if post, ok := x87NoOperand[upper]; ok {
|
||||
if len(ops) != 0 {
|
||||
return true, fmt.Errorf("%s takes no operands, got %d", upper, len(ops))
|
||||
}
|
||||
return true, e.emit(&instr{opcode: []byte{0xD9, post}, modrm: -1, sib: -1})
|
||||
}
|
||||
if spec, ok := x87Arith[upper]; ok {
|
||||
return true, e.encodeX87Arith(upper, spec, ops)
|
||||
}
|
||||
if esc, ok := x87FCmov[upper]; ok {
|
||||
src, _, err := x87PairOperands(upper, ops)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// The condition applies between the named register and ST(0), so the
|
||||
// second operand is always F0; the first rides the postfix's low bits.
|
||||
return true, e.emit(&instr{opcode: []byte{esc[0], esc[1] | byte(src.idx&7)}, modrm: -1, sib: -1})
|
||||
}
|
||||
if escape, ok := x87Compare[upper]; ok {
|
||||
if _, _, err := x87PairOperands(upper, ops); err != nil {
|
||||
return true, err
|
||||
}
|
||||
src, _ := ops[0].(Reg)
|
||||
return true, e.emit(&instr{opcode: []byte{escape, 0xF0 | byte(src.idx&7)}, modrm: -1, sib: -1})
|
||||
}
|
||||
if upper == "FADDDP" {
|
||||
if len(ops) != 2 {
|
||||
return true, fmt.Errorf("FADDDP expects 2 operands, got %d", len(ops))
|
||||
}
|
||||
first, ok1 := ops[0].(Reg)
|
||||
second, ok2 := ops[1].(Reg)
|
||||
if !ok1 || !ok2 || !first.fp || !second.fp {
|
||||
return true, fmt.Errorf("FADDDP takes two x87 stack registers")
|
||||
}
|
||||
if first.idx != 0 {
|
||||
return true, fmt.Errorf("FADDDP: the first operand must be F0")
|
||||
}
|
||||
return true, e.emit(&instr{opcode: []byte{0xDE, 0xC0 | byte(second.idx&7)}, modrm: -1, sib: -1})
|
||||
}
|
||||
if m, ok := x87MemUnary[upper]; ok {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := &instr{opcode: []byte{m.escape}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
if m, ok := x87Fxsav[upper]; ok {
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0xAE})
|
||||
i.rexW = m.rexW
|
||||
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// encodeX87Arith encodes one member of the D8/DC arithmetic pair. The tool-
|
||||
// chain's shape set: (Fn, F0) rides D8, (F0, Fn) rides DC, ((m), F0) rides
|
||||
// the DC memory digit; every other pairing is an error.
|
||||
func (e *enc) encodeX87Arith(mnem string, spec x87ArithSpec, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
src, dst := ops[0], ops[1]
|
||||
dstReg, dstIsF := dst.(Reg)
|
||||
if !dstIsF || !dstReg.fp {
|
||||
return fmt.Errorf("%s: the destination must be an x87 stack register", mnem)
|
||||
}
|
||||
switch s := src.(type) {
|
||||
case Reg:
|
||||
if !s.fp {
|
||||
return fmt.Errorf("%s: the source must be an x87 stack register", mnem)
|
||||
}
|
||||
switch {
|
||||
case dstReg.idx == 0:
|
||||
return e.emit(&instr{opcode: []byte{0xD8, 0xC0 | byte(spec.d8Digit)<<3 | byte(s.idx&7)}, modrm: -1, sib: -1})
|
||||
case s.idx == 0:
|
||||
return e.emit(&instr{opcode: []byte{0xDC, 0xC0 | byte(spec.dcDigit)<<3 | byte(dstReg.idx&7)}, modrm: -1, sib: -1})
|
||||
default:
|
||||
return fmt.Errorf("%s: one operand must be F0", mnem)
|
||||
}
|
||||
default:
|
||||
if !isX86Mem(src) {
|
||||
return fmt.Errorf("%s: the source must be an x87 stack register or memory", mnem)
|
||||
}
|
||||
if dstReg.idx != 0 {
|
||||
return fmt.Errorf("%s: the destination must be F0 with a memory source", mnem)
|
||||
}
|
||||
i := &instr{opcode: []byte{0xDC}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, spec.memDigit, src, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
}
|
||||
|
||||
// x87PairOperands validates the (register, F0) shape the conditional moves
|
||||
// and compares take and returns the two registers.
|
||||
func x87PairOperands(mnem string, ops []Operand) (Reg, Reg, error) {
|
||||
if len(ops) != 2 {
|
||||
return Reg{}, Reg{}, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
src, ok1 := ops[0].(Reg)
|
||||
dst, ok2 := ops[1].(Reg)
|
||||
if !ok1 || !ok2 || !src.fp || !dst.fp {
|
||||
return Reg{}, Reg{}, fmt.Errorf("%s takes two x87 stack registers", mnem)
|
||||
}
|
||||
if dst.idx != 0 {
|
||||
return Reg{}, Reg{}, fmt.Errorf("%s: the second operand must be F0", mnem)
|
||||
}
|
||||
return src, dst, nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "testing"
|
||||
|
||||
// amd64X87Corpus holds every line the Go toolchain's own
|
||||
// amd64enc.s carries for the x87 family (stack controls, the arithmetic pair, the compares, the memory loads and the FXSAVE pair), with the bytes go tool asm
|
||||
// emits for each: the differential ground truth the family is proven
|
||||
// against, line for line.
|
||||
var amd64X87Corpus = []struct {
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{"F2XM1", "d9 f0"},
|
||||
{"FABS", "d9 e1"},
|
||||
{"FADDD F2, F0", "d8 c2"},
|
||||
{"FADDD F3, F0", "d8 c3"},
|
||||
{"FADDD F0, F2", "dc c2"},
|
||||
{"FADDD F0, F3", "dc c3"},
|
||||
{"FADDD (BX), F0", "dc 03"},
|
||||
{"FADDD (R11), F0", "41 dc 03"},
|
||||
{"FADDDP F0, F2", "de c2"},
|
||||
{"FADDDP F0, F3", "de c3"},
|
||||
{"FBLD (BX)", "df 23"},
|
||||
{"FBLD (R11)", "41 df 23"},
|
||||
{"FBSTP (BX)", "df 33"},
|
||||
{"FBSTP (R11)", "41 df 33"},
|
||||
{"FCHS", "d9 e0"},
|
||||
{"FCMOVB F2, F0", "da c2"},
|
||||
{"FCMOVB F3, F0", "da c3"},
|
||||
{"FCMOVBE F2, F0", "da d2"},
|
||||
{"FCMOVBE F3, F0", "da d3"},
|
||||
{"FCMOVE F2, F0", "da ca"},
|
||||
{"FCMOVE F3, F0", "da cb"},
|
||||
{"FCMOVNB F2, F0", "db c2"},
|
||||
{"FCMOVNB F3, F0", "db c3"},
|
||||
{"FCMOVNBE F2, F0", "db d2"},
|
||||
{"FCMOVNBE F3, F0", "db d3"},
|
||||
{"FCMOVNE F2, F0", "db ca"},
|
||||
{"FCMOVNE F3, F0", "db cb"},
|
||||
{"FCMOVNU F2, F0", "db da"},
|
||||
{"FCMOVNU F3, F0", "db db"},
|
||||
{"FCMOVU F2, F0", "da da"},
|
||||
{"FCMOVU F3, F0", "da db"},
|
||||
{"FCOMD F2, F0", "d8 d2"},
|
||||
{"FCOMD F3, F0", "d8 d3"},
|
||||
{"FCOMD (BX), F0", "dc 13"},
|
||||
{"FCOMD (R11), F0", "41 dc 13"},
|
||||
{"FCOMI F2, F0", "db f2"},
|
||||
{"FCOMI F3, F0", "db f3"},
|
||||
{"FCOMIP F2, F0", "df f2"},
|
||||
{"FCOMIP F3, F0", "df f3"},
|
||||
{"FCOS", "d9 ff"},
|
||||
{"FDECSTP", "d9 f6"},
|
||||
{"FDIVD F2, F0", "d8 f2"},
|
||||
{"FDIVD F3, F0", "d8 f3"},
|
||||
{"FDIVD F0, F2", "dc fa"},
|
||||
{"FDIVD F0, F3", "dc fb"},
|
||||
{"FDIVD (BX), F0", "dc 33"},
|
||||
{"FDIVD (R11), F0", "41 dc 33"},
|
||||
{"FINCSTP", "d9 f7"},
|
||||
{"FLD1", "d9 e8"},
|
||||
{"FLDCW (BX)", "d9 2b"},
|
||||
{"FLDCW (R11)", "41 d9 2b"},
|
||||
{"FLDL2E", "d9 ea"},
|
||||
{"FLDL2T", "d9 e9"},
|
||||
{"FLDLG2", "d9 ec"},
|
||||
{"FLDPI", "d9 eb"},
|
||||
{"FNOP", "d9 d0"},
|
||||
{"FPATAN", "d9 f3"},
|
||||
{"FPREM", "d9 f8"},
|
||||
{"FPREM1", "d9 f5"},
|
||||
{"FPTAN", "d9 f2"},
|
||||
{"FRNDINT", "d9 fc"},
|
||||
{"FSCALE", "d9 fd"},
|
||||
{"FSIN", "d9 fe"},
|
||||
{"FSINCOS", "d9 fb"},
|
||||
{"FSQRT", "d9 fa"},
|
||||
{"FTST", "d9 e4"},
|
||||
{"FXAM", "d9 e5"},
|
||||
{"FXRSTOR (BX)", "0f ae 0b"},
|
||||
{"FXRSTOR (R11)", "41 0f ae 0b"},
|
||||
{"FXRSTOR64 (BX)", "48 0f ae 0b"},
|
||||
{"FXRSTOR64 (R11)", "49 0f ae 0b"},
|
||||
{"FXSAVE (BX)", "0f ae 03"},
|
||||
{"FXSAVE (R11)", "41 0f ae 03"},
|
||||
{"FXSAVE64 (BX)", "48 0f ae 03"},
|
||||
{"FXSAVE64 (R11)", "49 0f ae 03"},
|
||||
{"FXTRACT", "d9 f4"},
|
||||
{"FYL2X", "d9 f1"},
|
||||
{"FYL2XP1", "d9 f9"},
|
||||
}
|
||||
|
||||
// TestAmd64X87Corpus assembles every corpus line and requires the same bytes
|
||||
// go tool asm emits for it.
|
||||
func TestAmd64X87Corpus(t *testing.T) {
|
||||
for _, tc := range amd64X87Corpus {
|
||||
fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n")
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.line, err)
|
||||
continue
|
||||
}
|
||||
if got := hexBytes(code); got != tc.want {
|
||||
t.Errorf("%s: got %s, want %s", tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This file implements the XSAVE family: the extended-state save and restore
|
||||
// pair in its four generations (XSAVE/XRSTOR, XSAVEOPT, XSAVEC, XSAVES) and
|
||||
// their 64 spellings. Each takes one memory operand alone; the 64 spellings
|
||||
// carry REX.W. Every encoding here is pinned byte for byte against
|
||||
// go tool asm through the corpus lines in amd64_xsave_test.go.
|
||||
|
||||
// xsaveSpec is one XSAVE family member: the opcode group and the /digit.
|
||||
// XSAVEOPT carries no mandatory prefix in the toolchain's encoding despite
|
||||
// the manual's 0x66, so the family has none anywhere.
|
||||
type xsaveSpec struct {
|
||||
op []byte
|
||||
digit int
|
||||
}
|
||||
|
||||
// xsaveTable maps the save/restore mnemonics to their encodings. The 64
|
||||
// spellings share the base's digit with REX.W.
|
||||
var xsaveTable = map[string]xsaveSpec{
|
||||
"XSAVE": {[]byte{0x0F, 0xAE}, 4},
|
||||
"XRSTOR": {[]byte{0x0F, 0xAE}, 5},
|
||||
"XSAVEOPT": {[]byte{0x0F, 0xAE}, 6},
|
||||
"XSAVEC": {[]byte{0x0F, 0xC7}, 4},
|
||||
"XSAVES": {[]byte{0x0F, 0xC7}, 5},
|
||||
"XRSTORS": {[]byte{0x0F, 0xC7}, 3},
|
||||
}
|
||||
|
||||
// encodeXsave encodes the XSAVE family. It reports whether the mnemonic
|
||||
// belongs to the family.
|
||||
func (e *enc) encodeXsave(upper string, ops []Operand) (bool, error) {
|
||||
name, rexW := upper, false
|
||||
if base, ok := strings.CutSuffix(upper, "64"); ok {
|
||||
name, rexW = base, true
|
||||
}
|
||||
spec, ok := xsaveTable[name]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
if len(ops) != 1 {
|
||||
return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops))
|
||||
}
|
||||
if !isX86Mem(ops[0]) {
|
||||
return true, fmt.Errorf("%s requires a memory operand", upper)
|
||||
}
|
||||
i := &instr{opcode: append([]byte(nil), spec.op...), modrm: -1, sib: -1, rexW: rexW}
|
||||
if err := setRMDigit(i, spec.digit, ops[0], 8); err != nil {
|
||||
return true, err
|
||||
}
|
||||
return true, e.emit(i)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "testing"
|
||||
|
||||
// amd64XsaveCorpus holds every line the Go toolchain's own
|
||||
// amd64enc.s carries for the XSAVE family (XSAVE, XSAVEOPT, XSAVEC, XSAVES and the restores, plain and 64), with the bytes go tool asm
|
||||
// emits for each: the differential ground truth the family is proven
|
||||
// against, line for line.
|
||||
var amd64XsaveCorpus = []struct {
|
||||
line string
|
||||
want string
|
||||
}{
|
||||
{"XRSTOR (BX)", "0f ae 2b"},
|
||||
{"XRSTOR (R11)", "41 0f ae 2b"},
|
||||
{"XRSTOR64 (BX)", "48 0f ae 2b"},
|
||||
{"XRSTOR64 (R11)", "49 0f ae 2b"},
|
||||
{"XRSTORS (BX)", "0f c7 1b"},
|
||||
{"XRSTORS (R11)", "41 0f c7 1b"},
|
||||
{"XRSTORS64 (BX)", "48 0f c7 1b"},
|
||||
{"XRSTORS64 (R11)", "49 0f c7 1b"},
|
||||
{"XSAVE (BX)", "0f ae 23"},
|
||||
{"XSAVE (R11)", "41 0f ae 23"},
|
||||
{"XSAVE64 (BX)", "48 0f ae 23"},
|
||||
{"XSAVE64 (R11)", "49 0f ae 23"},
|
||||
{"XSAVEC (BX)", "0f c7 23"},
|
||||
{"XSAVEC (R11)", "41 0f c7 23"},
|
||||
{"XSAVEC64 (BX)", "48 0f c7 23"},
|
||||
{"XSAVEC64 (R11)", "49 0f c7 23"},
|
||||
{"XSAVEOPT (BX)", "0f ae 33"},
|
||||
{"XSAVEOPT (R11)", "41 0f ae 33"},
|
||||
{"XSAVEOPT64 (BX)", "48 0f ae 33"},
|
||||
{"XSAVEOPT64 (R11)", "49 0f ae 33"},
|
||||
{"XSAVES (BX)", "0f c7 2b"},
|
||||
{"XSAVES (R11)", "41 0f c7 2b"},
|
||||
{"XSAVES64 (BX)", "48 0f c7 2b"},
|
||||
{"XSAVES64 (R11)", "49 0f c7 2b"},
|
||||
}
|
||||
|
||||
// TestAmd64XsaveCorpus assembles every corpus line and requires the same bytes
|
||||
// go tool asm emits for it.
|
||||
func TestAmd64XsaveCorpus(t *testing.T) {
|
||||
for _, tc := range amd64XsaveCorpus {
|
||||
fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n")
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", tc.line, err)
|
||||
continue
|
||||
}
|
||||
if got := hexBytes(code); got != tc.want {
|
||||
t.Errorf("%s: got %s, want %s", tc.line, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5196
-254
File diff suppressed because it is too large.
Load diff
+1014
-112
File diff suppressed because it is too large.
Load diff
+2210
-8
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// arm64AcceptedErrorShapes lists the toolchain's arm64error.s spellings gasm
|
||||
// still accepts, each an acceptance superset with a known shape. The list
|
||||
// only shrinks: every tightening of the encoder moves spellings out of it,
|
||||
// and a spelling reappearing here means a regression. The catalogue is
|
||||
// empty as of Go 1.27: every line the toolchain's corpus rejects, gasm
|
||||
// rejects too.
|
||||
var arm64AcceptedErrorShapes = []string{}
|
||||
|
||||
// TestArm64ToolchainErrorParity walks the toolchain's arm64error.s (Go
|
||||
// 1.27, arm64) and requires gasm to reject every case the toolchain rejects.
|
||||
// A live Go toolchain is needed for the source file; the test skips without
|
||||
// one or in -short.
|
||||
func TestArm64ToolchainErrorParity(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live arm64error.s corpus: skipped in -short mode")
|
||||
}
|
||||
goroot := os.Getenv("GOROOT")
|
||||
if goroot == "" {
|
||||
t.Skip("no GOROOT")
|
||||
}
|
||||
path := filepath.Join(goroot, "src", "cmd", "asm", "internal", "asm", "testdata", "arm64error.s")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, s := range arm64AcceptedErrorShapes {
|
||||
allowed[s] = true
|
||||
}
|
||||
for raw := range strings.SplitSeq(string(data), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "TEXT") || !strings.Contains(line, "ERROR") {
|
||||
continue
|
||||
}
|
||||
body := line
|
||||
if i := strings.Index(body, "//"); i >= 0 {
|
||||
body = strings.TrimSpace(body[:i])
|
||||
}
|
||||
body = strings.ReplaceAll(body, "\t", " ")
|
||||
body = strings.Join(strings.Fields(body), " ")
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||||
f, perr := parser.Parse("errorparity.s", src)
|
||||
if len(perr) > 0 {
|
||||
continue // the parser already rejects the spelling
|
||||
}
|
||||
if _, aerr := AssembleFileARM64(f); aerr == nil && !allowed[body] {
|
||||
t.Errorf("gasm accepts what the toolchain rejects: %s", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The assembler's side of the extended-instruction layer: this file turns a
|
||||
// parsed arm64 statement into the operand form arch.ExtInstr.Encode consumes
|
||||
// and routes statements only the layer can encode through the registry. It
|
||||
// sits beside the main arm64 encoders, never inside them: the generated
|
||||
// tables and the scalar, NEON and FP paths are untouched, and a statement
|
||||
// reaches this file only when the mnemonic is registered in the extension
|
||||
// layer and at least one operand is a scalable vector or predicate register.
|
||||
//
|
||||
// The spellings are the layer's own Plan 9 forms, the ones its metadata
|
||||
// documents: Zn, Zm, Zd for the unpredicated three-vector class, Zm, Pg/M,
|
||||
// Zdn for the predicated class, imm{, LSL #8}, Zdn for the immediate
|
||||
// classes, and for the predicate family Pm.B, Pn.B, Pg/Z (or Pg.Z), Pd.B
|
||||
// for the logical operations, Pn.B, Pg.Z, Pd.B for the breaks, Pm.T, Pn.T,
|
||||
// Pd.T for the permutations, Rm, Rn, Pd.T for the while compares, PN8-PN15
|
||||
// for the counter destinations, and the bare SETFFR. Stage three adds the
|
||||
// crypto family (Zn.T, Zd.T, Zd.T read-back and the in-place Zd.T, Zd.T),
|
||||
// the predicate counters (Pn.T, Pg, Rd; Pn.T, ZR; Rd, Pn.T, Rd; ZR and R
|
||||
// terminators) and the reductions (Zn.T, Pg, Vd over the SIMD register
|
||||
// V0-V31, with ZR and RSP accepted where the classes take them).
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// arm64ExtStatement converts one instruction's operands into the extended
|
||||
// layer's operand form. pinned reports that the statement belongs to the
|
||||
// layer: the mnemonic is registered in the registry and the operand list
|
||||
// carries at least one scalable vector, predicate or predicate-as-counter
|
||||
// register, or no operands at all (the zero-operand forms such as SETFFR,
|
||||
// which no scalar path could mean instead). A pinned statement can only
|
||||
// encode through the layer, so every operand is read here and its
|
||||
// diagnostic replaces whatever the scalar paths would have said about
|
||||
// operands they cannot read; err is non-nil for a pinned statement whose
|
||||
// operands the layer refuses, and extops is complete only when err is nil.
|
||||
// Unpinned means the statement is nobody's: the caller falls through to the
|
||||
// ordinary arm64 encoders, which keep their exact behaviour for every
|
||||
// scalar, NEON and FP operand list.
|
||||
func arm64ExtStatement(mnem string, ops []*ast.Operand) (extops []arch.ExtOperand, pinned bool, err error) {
|
||||
if _, ok := LookupExtension(arch.ARM64, mnem); !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
if !arm64ExtPinned(mnem, ops) {
|
||||
return nil, false, nil
|
||||
}
|
||||
ops = arm64ExtMergeLists(ops)
|
||||
out := make([]arch.ExtOperand, 0, len(ops))
|
||||
for i, op := range ops {
|
||||
text := strings.Join(strings.Fields(op.Raw), "")
|
||||
|
||||
// The spelled shift of an immediate class: the shift is an attribute
|
||||
// of the preceding immediate operand (imm{, LSL #8}, Zdn), never an
|
||||
// operand of its own.
|
||||
if amount, ok := strings.CutPrefix(text, "LSL#"); ok {
|
||||
if len(out) == 0 || out[len(out)-1].Kind != arch.ExtImm || out[len(out)-1].HasShift {
|
||||
return nil, true, fmt.Errorf("%s: operand %d (%s): LSL belongs straight after an immediate", mnem, i+1, op.Raw)
|
||||
}
|
||||
n, convErr := strconv.Atoi(amount)
|
||||
if convErr != nil {
|
||||
return nil, true, fmt.Errorf("%s: operand %d (%s): %q is not an LSL amount", mnem, i+1, op.Raw, amount)
|
||||
}
|
||||
out[len(out)-1].Shift, out[len(out)-1].HasShift = n, true
|
||||
continue
|
||||
}
|
||||
|
||||
if op.Kind == ast.OpImmediate {
|
||||
ext, ok := arm64ExtImmediate(op)
|
||||
if !ok {
|
||||
return nil, true, fmt.Errorf("%s: operand %d (%s) is not an immediate the layer can read", mnem, i+1, op.Raw)
|
||||
}
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
// The load and store destination list, [Z13.B] or the multi-register
|
||||
// [Z13.B, Z14.B, Z15.B] of the multiple-structure shapes, its
|
||||
// arrangement part of the instruction's identity.
|
||||
if strings.HasPrefix(text, "[") && strings.HasSuffix(text, "]") {
|
||||
if ext, ok := arm64ExtVectorList(mnem, strings.Trim(text, "[]")); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// The gather/scatter memory operand: a parenthesised register pair,
|
||||
// an immediate-offset base or a lone vector base.
|
||||
if ext, ok := arm64ExtSveMem(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
if ext, ok := arm64ExtVector(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
if ext, ok := arm64ExtPredicate(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
if ext, ok := arm64ExtCounter(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
if text == "ZR" {
|
||||
out = append(out, arch.ExtZeroRegister())
|
||||
continue
|
||||
}
|
||||
if text == "RSP" {
|
||||
out = append(out, arch.ExtStackPointer())
|
||||
continue
|
||||
}
|
||||
if ext, ok := arm64ExtSIMD(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
if ext, ok := arm64ExtGeneral(text); ok {
|
||||
out = append(out, ext)
|
||||
continue
|
||||
}
|
||||
return nil, true, fmt.Errorf("%s: operand %d (%s) is not an extended-layer operand: want a scalable vector, predicate, general or counter register, or an immediate", mnem, i+1, op.Raw)
|
||||
}
|
||||
return out, true, nil
|
||||
}
|
||||
|
||||
// arm64ExtPinned reports whether the statement belongs to the layer. A
|
||||
// mnemonic the extension layer registers on its own, one the generated
|
||||
// arm64 table does not know, owns every one of its statements: no scalar
|
||||
// path could mean it instead, and the layer's diagnostics replace the
|
||||
// unsupported-instruction complaint. A mnemonic both tables carry (the
|
||||
// SVE aliases of ADD, SUB and MUL) keeps the operand-shape test: any
|
||||
// operand is a scalable vector, predicate or predicate-as-counter
|
||||
// register, the shapes only the extension layer reads, or the statement
|
||||
// carries no operands at all and the mnemonic's zero-operand forms claim
|
||||
// it. The shape test is deliberately loose about the suffixes: P0/B is
|
||||
// not a spelling the layer takes, but the P of it makes the statement the
|
||||
// layer's, and the conversion then diagnoses the operand precisely
|
||||
// instead of leaving it to a scalar path that would report an unrelated
|
||||
// register error.
|
||||
func arm64ExtPinned(mnem string, ops []*ast.Operand) bool {
|
||||
if len(ops) == 0 {
|
||||
return true
|
||||
}
|
||||
if _, shared := a64InstrTable[mnem]; !shared {
|
||||
return true
|
||||
}
|
||||
for _, op := range ops {
|
||||
if op.Kind == ast.OpImmediate {
|
||||
continue
|
||||
}
|
||||
text := strings.Join(strings.Fields(op.Raw), "")
|
||||
if _, ok := arm64ExtVector(text); ok {
|
||||
return true
|
||||
}
|
||||
if arm64ExtPredicateShape(text) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// arm64ExtPredicateShape reports whether text spells a predicate or
|
||||
// predicate-as-counter register at all: PN or P, digits, an optional
|
||||
// arrangement suffix and an optional qualifier after a slash, whatever the
|
||||
// qualifier says. The strict parses in arm64ExtPredicate and
|
||||
// arm64ExtCounter judge the suffix; this shape only decides who the operand
|
||||
// belongs to.
|
||||
func arm64ExtPredicateShape(text string) bool {
|
||||
if text == "" || text[0] != 'P' {
|
||||
return false
|
||||
}
|
||||
text = text[1:]
|
||||
if rest, found := strings.CutPrefix(text, "N"); found {
|
||||
text = rest
|
||||
}
|
||||
if i := strings.IndexByte(text, '/'); i >= 0 {
|
||||
text = text[:i]
|
||||
}
|
||||
if i := strings.IndexByte(text, '.'); i >= 0 {
|
||||
text = text[:i]
|
||||
}
|
||||
_, err := strconv.Atoi(text)
|
||||
return err == nil && text != ""
|
||||
}
|
||||
|
||||
// arm64ExtImmediate converts a $ immediate into the layer's form. The
|
||||
// parser folds a parenthesised constant expression in full ($(255<<8)) and
|
||||
// reads a bare literal greedily, dropping any trailing operator tokens:
|
||||
// $255<<8 parses as 255 with the shift silently gone. Encoding that silent
|
||||
// prefix would assemble what the text did not say, so an unparenthesised
|
||||
// immediate is accepted only when its whole text reads back as one integer
|
||||
// carrying the parser's value.
|
||||
func arm64ExtImmediate(op *ast.Operand) (arch.ExtOperand, bool) {
|
||||
if op.Kind != ast.OpImmediate || !op.Imm.HasVal {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
text := strings.Join(strings.Fields(strings.TrimPrefix(op.Raw, "$")), "")
|
||||
if !strings.HasPrefix(text, "(") {
|
||||
if _, parseErr := strconv.ParseInt(text, 0, 64); parseErr != nil {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
}
|
||||
v := op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtImm, Imm: v}, true
|
||||
}
|
||||
|
||||
// arm64ExtVector parses a scalable vector register operand: Z0..Z31 with an
|
||||
// optional element-size suffix, Z0.S. The arrangement is carried as written
|
||||
// and the encoding validates it against the form.
|
||||
func arm64ExtVector(text string) (arch.ExtOperand, bool) {
|
||||
reg, arr, ok := arm64ExtReg(text, 'Z')
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtZReg, Reg: reg, Arr: arr}, true
|
||||
}
|
||||
|
||||
// arm64ExtMergeLists rejoins the bracketed vector lists the parser reads as
|
||||
// separate operands: the comma inside [Z13.B, Z14.B, Z15.B] is an operand
|
||||
// boundary to the parser, so the list arrives as two or more pieces and the
|
||||
// multiple-structure loads and stores need it whole. Pieces from an opening
|
||||
// bracket to the one carrying the closing bracket rejoin over their commas;
|
||||
// everything else passes through untouched.
|
||||
func arm64ExtMergeLists(ops []*ast.Operand) []*ast.Operand {
|
||||
closed := func(op *ast.Operand) bool {
|
||||
return strings.HasSuffix(strings.Join(strings.Fields(op.Raw), ""), "]")
|
||||
}
|
||||
merged := make([]*ast.Operand, 0, len(ops))
|
||||
for i := 0; i < len(ops); i++ {
|
||||
text := strings.Join(strings.Fields(ops[i].Raw), "")
|
||||
if !strings.HasPrefix(text, "[") || closed(ops[i]) {
|
||||
merged = append(merged, ops[i])
|
||||
continue
|
||||
}
|
||||
parts := []string{ops[i].Raw}
|
||||
kind := ops[i].Kind
|
||||
for i+1 < len(ops) {
|
||||
i++
|
||||
parts = append(parts, ops[i].Raw)
|
||||
if closed(ops[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
merged = append(merged, &ast.Operand{Kind: kind, Raw: strings.Join(parts, ",")})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// arm64ExtVectorList parses the bracketed vector list of the loads and
|
||||
// stores: a single register, [Z13.B], or the multi-register lists of the
|
||||
// LD2-LD4 and ST2-ST4 multiple-structure shapes, [Z13.B, Z14.B, Z15.B],
|
||||
// consecutive registers under one arrangement. The instruction's own digit
|
||||
// names the list's length where it carries one, so a two-register list
|
||||
// under ZLD3 fails here. The encoding carries the first register alone;
|
||||
// the length rides the operand for the encode side.
|
||||
func arm64ExtVectorList(mnem, body string) (arch.ExtOperand, bool) {
|
||||
regs := strings.Split(body, ",")
|
||||
first, ok := arm64ExtVector(regs[0])
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
for i, reg := range regs[1:] {
|
||||
op, ok := arm64ExtVector(reg)
|
||||
if !ok || op.Arr != first.Arr || op.Reg != first.Reg+i+1 {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
}
|
||||
if count := arm64ExtListCount(mnem); count != len(regs) {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
if len(regs) > 1 {
|
||||
first.List = len(regs)
|
||||
}
|
||||
return first, true
|
||||
}
|
||||
|
||||
// arm64ExtListCount reads the list length a load or store mnemonic names,
|
||||
// the digit straight after its ZLD or ZST prefix; the loads and stores
|
||||
// without one carry a single register.
|
||||
func arm64ExtListCount(mnem string) int {
|
||||
rest, ok := strings.CutPrefix(mnem, "ZLD")
|
||||
if !ok {
|
||||
rest, ok = strings.CutPrefix(mnem, "ZST")
|
||||
}
|
||||
if !ok || rest == "" {
|
||||
return 1
|
||||
}
|
||||
if c := rest[0]; c >= '2' && c <= '4' {
|
||||
return int(c - '0')
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// arm64ExtPredicate parses a predicate register operand: P0..P15 with an
|
||||
// optional element-size suffix (P0.B) and an optional qualifier in either
|
||||
// spelling the corpus and the wired forms use, P0/M and P0.Z.
|
||||
func arm64ExtPredicate(text string) (arch.ExtOperand, bool) {
|
||||
qual := arch.ExtQualNone
|
||||
if base, suffix, found := strings.Cut(text, "/"); found {
|
||||
switch suffix {
|
||||
case "M":
|
||||
qual = arch.ExtQualMerging
|
||||
case "Z":
|
||||
qual = arch.ExtQualZeroing
|
||||
default:
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
text = base
|
||||
} else if base, suffix, found := strings.Cut(text, "."); found &&
|
||||
(suffix == "Z" || suffix == "M") {
|
||||
// The dot qualifier stands in place of an arrangement, the spelling
|
||||
// the toolchain's corpus writes (P1.Z, P14.M).
|
||||
qual = arch.ExtQualMerging
|
||||
if suffix == "Z" {
|
||||
qual = arch.ExtQualZeroing
|
||||
}
|
||||
text = base
|
||||
}
|
||||
reg, arr, ok := arm64ExtReg(text, 'P')
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtPReg, Reg: reg, Arr: arr, Qual: qual}, true
|
||||
}
|
||||
|
||||
// arm64ExtCounter parses a predicate-as-counter register operand: PN8..PN15
|
||||
// with an optional element-size suffix, PN14.S. The register range is the
|
||||
// counter range the layer's convention carries; the encoding validates it.
|
||||
func arm64ExtCounter(text string) (arch.ExtOperand, bool) {
|
||||
rest, ok := strings.CutPrefix(text, "PN")
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
reg, arr, ok := arm64ExtRegDigits(rest)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtPNReg, Reg: reg, Arr: arr}, true
|
||||
}
|
||||
|
||||
// arm64ExtGeneral parses a general register operand: R0..R30, the plain
|
||||
// spelling the while-compare forms take, beside the ZR and RSP spellings of
|
||||
// the thirty-first slot the conversion above reads. The register range is
|
||||
// left to the encoding, whose diagnostics name it.
|
||||
func arm64ExtGeneral(text string) (arch.ExtOperand, bool) {
|
||||
rest, ok := strings.CutPrefix(text, "R")
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
reg, arr, ok := arm64ExtRegDigits(rest)
|
||||
if !ok || arr != arch.ExtArrNone {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtGReg, Reg: reg}, true
|
||||
}
|
||||
|
||||
// arm64ExtSIMD parses a 128-bit SIMD register operand: V0..V31, written
|
||||
// bare, the scalar destination the reductions and the crypto read-back
|
||||
// forms take, or with the counted quadword suffix of the SVE2.1 QV class,
|
||||
// V5.S4 reading four 32-bit lanes (the spellings .B16, .H8, .S4 and .D2;
|
||||
// no other suffix parses). The register range is left to the encoding.
|
||||
func arm64ExtSIMD(text string) (arch.ExtOperand, bool) {
|
||||
rest, ok := strings.CutPrefix(text, "V")
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
arr := arch.ExtArrNone
|
||||
for _, q := range []struct {
|
||||
suffix string
|
||||
arr arch.ExtArrangement
|
||||
}{
|
||||
{"B16", arch.ExtArrB},
|
||||
{"H8", arch.ExtArrH},
|
||||
{"S4", arch.ExtArrS},
|
||||
{"D2", arch.ExtArrD},
|
||||
} {
|
||||
if s := "." + q.suffix; strings.HasSuffix(rest, s) {
|
||||
arr = q.arr
|
||||
rest = rest[:len(rest)-len(s)]
|
||||
break
|
||||
}
|
||||
}
|
||||
reg, bare, ok := arm64ExtRegDigits(rest)
|
||||
if !ok || bare != arch.ExtArrNone {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Kind: arch.ExtVReg, Reg: reg, Arr: arr}, true
|
||||
}
|
||||
|
||||
// arm64ExtSveMem parses the gather/scatter memory operand off a normalised
|
||||
// operand text: the parenthesised pair (R6)(R14), (Z23.D<<1)(R24) and
|
||||
// (Z4.S.UXTW)(R3), the immediate-offset base 6(Z7.S), and the lone vector
|
||||
// base (Z5.D) of the stores. The second parenthesis accepts the
|
||||
// stack-pointer spelling RSP; the ranges and the mode's own rules are left
|
||||
// to the encoding, whose diagnostics name them.
|
||||
func arm64ExtSveMem(text string) (arch.ExtOperand, bool) {
|
||||
// The immediate-offset spelling: digits straight before the parenthesis.
|
||||
if i := strings.IndexByte(text, '('); i > 0 && i == strings.LastIndexByte(text, '(') {
|
||||
disp, err := strconv.ParseUint(text[:i], 10, 32)
|
||||
if err == nil && strings.HasSuffix(text, ")") {
|
||||
op, ok := arm64ExtSveMemGroup(text[i+1 : len(text)-1])
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
if !op.BaseVec || op.Extend != 0 || op.Shift != 0 {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
op.Imm = int64(disp)
|
||||
return op, true
|
||||
}
|
||||
}
|
||||
// The parenthesised forms: one group or two.
|
||||
rest, ok := strings.CutPrefix(text, "(")
|
||||
if !ok || !strings.HasSuffix(text, ")") {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
rest = rest[:len(rest)-1]
|
||||
first := rest
|
||||
op := arch.ExtOperand{Off: -1}
|
||||
if base, second, found := strings.Cut(rest, ")("); found {
|
||||
first = base
|
||||
off, ok := arm64ExtSveMemOffset(second)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
op = off
|
||||
}
|
||||
group, ok := arm64ExtSveMemGroup(first)
|
||||
if !ok {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
group.Off = op.Off
|
||||
group.Reg31 = op.Reg31
|
||||
return group, true
|
||||
}
|
||||
|
||||
// arm64ExtSveMemGroup parses one parenthesised memory register: R6, R6<<3,
|
||||
// Z23.D, Z23.D<<1, Z4.S.UXTW or Z7.D.SXTW. The general registers run
|
||||
// R0-R30 and the scalable vectors Z0-Z31 with an .S or .D element size and
|
||||
// an optional UXTW or SXTW extension; the ranges are left to the encoding.
|
||||
func arm64ExtSveMemGroup(text string) (arch.ExtOperand, bool) {
|
||||
op := arch.ExtOperand{Kind: arch.ExtSveMem, Off: -1}
|
||||
if base, shift, found := strings.Cut(text, "<<"); found {
|
||||
n, err := strconv.Atoi(shift)
|
||||
if err != nil || n < 0 {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
op.Shift = n
|
||||
text = base
|
||||
}
|
||||
parts := strings.Split(text, ".")
|
||||
switch parts[0][0] {
|
||||
case 'R':
|
||||
reg, err := strconv.Atoi(parts[0][1:])
|
||||
if err != nil || len(parts) != 1 {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
op.Reg = reg
|
||||
case 'Z':
|
||||
reg, err := strconv.Atoi(parts[0][1:])
|
||||
if err != nil || len(parts) < 2 || len(parts) > 3 {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
switch parts[1] {
|
||||
case "S":
|
||||
op.Arr = arch.ExtArrS
|
||||
case "D":
|
||||
op.Arr = arch.ExtArrD
|
||||
default:
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
op.Reg = reg
|
||||
op.BaseVec = true
|
||||
if len(parts) == 3 {
|
||||
switch parts[2] {
|
||||
case "UXTW":
|
||||
op.Extend = 1
|
||||
case "SXTW":
|
||||
op.Extend = 2
|
||||
default:
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
}
|
||||
default:
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return op, true
|
||||
}
|
||||
|
||||
// arm64ExtSveMemOffset parses the second parenthesis of a gather/scatter
|
||||
// memory operand: a plain R0-R30 or the stack-pointer spelling RSP.
|
||||
func arm64ExtSveMemOffset(text string) (arch.ExtOperand, bool) {
|
||||
if text == "RSP" {
|
||||
return arch.ExtOperand{Off: 31, Reg31: 2}, true
|
||||
}
|
||||
if len(text) < 2 || text[0] != 'R' {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
reg, err := strconv.Atoi(text[1:])
|
||||
if err != nil {
|
||||
return arch.ExtOperand{}, false
|
||||
}
|
||||
return arch.ExtOperand{Off: reg}, true
|
||||
}
|
||||
|
||||
// arm64ExtRegDigits parses the digits and optional arrangement suffix of a
|
||||
// register spelling once the letter prefix is gone.
|
||||
func arm64ExtRegDigits(text string) (reg int, arr arch.ExtArrangement, ok bool) {
|
||||
if base, suffix, found := strings.Cut(text, "."); found {
|
||||
switch suffix {
|
||||
case "B":
|
||||
arr = arch.ExtArrB
|
||||
case "H":
|
||||
arr = arch.ExtArrH
|
||||
case "S":
|
||||
arr = arch.ExtArrS
|
||||
case "D":
|
||||
arr = arch.ExtArrD
|
||||
case "Q":
|
||||
arr = arch.ExtArrQ
|
||||
default:
|
||||
return 0, 0, false
|
||||
}
|
||||
text = base
|
||||
}
|
||||
n, err := strconv.Atoi(text)
|
||||
if err != nil || n < 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return n, arr, true
|
||||
}
|
||||
|
||||
// arm64ExtReg parses Pn or Zn with an optional arrangement suffix off a
|
||||
// normalised operand text. The register range is left to the encoding: the
|
||||
// layer's own diagnostics name the range a form carries.
|
||||
func arm64ExtReg(text string, letter byte) (reg int, arr arch.ExtArrangement, ok bool) {
|
||||
if len(text) < 2 || text[0] != letter {
|
||||
return 0, 0, false
|
||||
}
|
||||
digits := text[1:]
|
||||
if base, suffix, found := strings.Cut(digits, "."); found {
|
||||
switch suffix {
|
||||
case "B":
|
||||
arr = arch.ExtArrB
|
||||
case "H":
|
||||
arr = arch.ExtArrH
|
||||
case "S":
|
||||
arr = arch.ExtArrS
|
||||
case "D":
|
||||
arr = arch.ExtArrD
|
||||
case "Q":
|
||||
arr = arch.ExtArrQ
|
||||
default:
|
||||
return 0, 0, false
|
||||
}
|
||||
digits = base
|
||||
}
|
||||
n, err := strconv.Atoi(digits)
|
||||
if err != nil || n < 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return n, arr, true
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+362
-21
@@ -53,7 +53,7 @@ package asm
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||
@@ -63,6 +63,18 @@ type arm64FrameInfo struct {
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
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
|
||||
|
||||
// tls holds the names the file's own GLOBL declarations mark TLSBSS:
|
||||
// the toolchain's aclass keys the TLS-LE load off the symbol's type
|
||||
// (objabi.STLSBSS), which only the file's declarations reveal. A nil
|
||||
// map answers no, the plain static-symbol load.
|
||||
tls map[string]bool
|
||||
}
|
||||
|
||||
// arm64ComputeFrame derives the frame layout for a TEXT function.
|
||||
@@ -80,15 +92,78 @@ func arm64ComputeFrame(t *ast.Text) arm64FrameInfo {
|
||||
|
||||
if fi.frame != 0 || !fi.leaf {
|
||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||
if fi.autosize%16 != 0 {
|
||||
// The toolchain aligns to 16: if autosize%16 == 8, add 8;
|
||||
// otherwise add whatever is needed.
|
||||
fi.autosize += 16 - (fi.autosize % 16)
|
||||
// The toolchain always adds an extrasize: 8 when the total leaves a
|
||||
// 16-byte alignment gap, another 16 when already aligned. An
|
||||
// autosize of zero (the $-8 convention included) is frameless and
|
||||
// takes neither.
|
||||
if fi.autosize != 0 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if fi.autosize == 0 {
|
||||
// The NOFRAME shape: the toolchain forces the leaf mark on any
|
||||
// autosize-zero function (calls included), so nothing saves LR and
|
||||
// no stack-split guard runs.
|
||||
fi.leaf = true
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// (BL/CALL), matching the toolchain's LEAF mark.
|
||||
func arm64IsLeaf(t *ast.Text) bool {
|
||||
@@ -119,12 +194,99 @@ func arm64Prologue(fi arm64FrameInfo) []byte {
|
||||
)
|
||||
}
|
||||
// Large frame: SUB $autosize, SP, R20; STP (FP,LR), -8(R20); ADD $0, R20, SP; SUB $8, SP, FP
|
||||
return a64WordsLE(
|
||||
a64AddSub(1, 1, 0, 0, uint32(fi.autosize), 31, 20), // SUB $autosize, SP, R20
|
||||
a64LSP(2, 0, 0, -1, 30, 20, 29), // STP FP, LR, [R20, #-8] (opc=2 for 64-bit pair)
|
||||
a64AddSub(1, 0, 0, 0, 0, 20, 31), // ADD $0, R20, SP (= MOV R20, SP)
|
||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||
ws := arm64SubImmWords(uint32(fi.autosize), 20)
|
||||
ws = append(ws,
|
||||
a64LSP(2, 0, 0, -1, 30, 20, 29), // STP FP, LR, [R20, #-8] (opc=2 for 64-bit pair)
|
||||
a64AddSub(1, 0, 0, 0, 0, 20, 31), // ADD $0, R20, SP (= MOV R20, SP)
|
||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||
)
|
||||
return a64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// arm64SplitImm12 reports whether the toolchain decomposes ADD/SUB $imm into
|
||||
// two imm12 instructions instead of materialising it into REGTMP
|
||||
// (asm7.go case 48, the C_ADDCON2 class): the value must fit 24 bits
|
||||
// unsigned and be neither encodable as one imm12 (checked by the callers
|
||||
// first), nor loadable into a register in a single MOVZ/MOVN word, nor a
|
||||
// logical immediate, because conclass tests all three before C_ADDCON2.
|
||||
func arm64SplitImm12(imm uint32) bool {
|
||||
if imm > 0xFFFFFF {
|
||||
return false
|
||||
}
|
||||
if _, _, _, ok := arm64Bitmask(uint64(imm), 1); ok {
|
||||
return false
|
||||
}
|
||||
return arm64Movcon(int64(imm)) < 0 && arm64Movcon(^int64(imm)) < 0
|
||||
}
|
||||
|
||||
// arm64SubImmWords emits SUB $imm, SP, Rd with the toolchain's ladder for an
|
||||
// ADD/SUB constant (asm7.go conclass and cases 2, 48, 62 and 13): the
|
||||
// immediate form when the value fits imm12 (plain, or shifted left by 12
|
||||
// when it is a multiple of 4096); a value with a single 16-bit chunk, a
|
||||
// logical immediate, or one wider than 24 bits is materialised into REGTMP
|
||||
// (R27) and subtracted in the extended-register form; everything else up to
|
||||
// 0xFFFFFF is split into two imm12 instructions:
|
||||
//
|
||||
// SUB $(imm&0xfff), SP, Rd
|
||||
// SUB $((imm&0xfff000)>>12)<<12, Rd, Rd
|
||||
func arm64SubImmWords(imm uint32, rd uint32) []uint32 {
|
||||
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)}
|
||||
}
|
||||
if !arm64SplitImm12(imm) {
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpSub, 27, 31, rd))
|
||||
}
|
||||
return []uint32{
|
||||
a64AddSub(1, 1, 0, 0, imm&0xFFF, 31, rd),
|
||||
a64AddSub(1, 1, 0, 1, (imm&0xFFF000)>>12, rd, rd),
|
||||
}
|
||||
}
|
||||
|
||||
// arm64AddImmWords emits ADD $imm, SP, Rd with the same imm12, shifted-imm12,
|
||||
// split and REGTMP ladder as arm64SubImmWords.
|
||||
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)}
|
||||
}
|
||||
if !arm64SplitImm12(imm) {
|
||||
mov, err := encodeARM64LoadImm(27, int64(imm), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, rd))
|
||||
}
|
||||
return []uint32{
|
||||
a64AddSub(1, 0, 0, 0, imm&0xFFF, 31, rd),
|
||||
a64AddSub(1, 0, 0, 1, (imm&0xFFF000)>>12, rd, rd),
|
||||
}
|
||||
}
|
||||
|
||||
// arm64RetAddWords emits the frame deallocation of a non-leaf RET with a
|
||||
// large frame. The toolchain adds the frame back with a single instruction:
|
||||
// a plain imm12 ADD when autosize fits 12 bits, otherwise the value is
|
||||
// materialised into REGTMP and added as a register, so the epilogue never
|
||||
// leaves a partially deallocated frame (obj7.go ARET, issue 73259). The
|
||||
// shifted-imm12 and split-imm12 forms are therefore never used here, unlike
|
||||
// the leaf epilogue's plain ADD instructions.
|
||||
func arm64RetAddWords(autosize uint32) []uint32 {
|
||||
if autosize < 1<<12 {
|
||||
return []uint32{a64AddSub(1, 0, 0, 0, autosize, 31, 31)}
|
||||
}
|
||||
mov, err := encodeARM64LoadImm(27, int64(autosize), "MOVD")
|
||||
if err != nil {
|
||||
mov = nil
|
||||
}
|
||||
return append(wordsOf(mov), arm64DPExtWords(arm64OpAdd, 27, 31, 31))
|
||||
}
|
||||
|
||||
// arm64Return returns the bytes for a RET: the epilogue (restore FP/LR and
|
||||
@@ -134,10 +296,8 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
||||
if fi.autosize != 0 {
|
||||
if fi.leaf {
|
||||
// Leaf with frame: ADD $autosize-8, SP, FP; ADD $autosize, SP, SP
|
||||
ws = append(ws,
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize-8), 31, 29), // ADD $autosize-8, SP, FP
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
||||
)
|
||||
ws = append(ws, arm64AddImmWords(uint32(fi.autosize-8), 29)...)
|
||||
ws = append(ws, arm64AddImmWords(uint32(fi.autosize), 31)...)
|
||||
} else if fi.autosize <= 0xf0 {
|
||||
// Non-leaf small frame: LDR FP, [SP, #-8]; LDR.P LR, [SP], #autosize
|
||||
ws = append(ws,
|
||||
@@ -145,11 +305,11 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
||||
arm64PostLoad(3, 0, int32(fi.autosize), 31, 30), // LDR.P LR, [SP], #autosize
|
||||
)
|
||||
} else {
|
||||
// Large frame: LDP -8(SP), (FP, LR); ADD $autosize, SP, SP
|
||||
// Large frame: LDP -8(SP), (FP, LR), then deallocate.
|
||||
ws = append(ws,
|
||||
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
||||
a64AddSub(1, 0, 0, 0, uint32(fi.autosize), 31, 31), // ADD $autosize, SP, SP
|
||||
a64LSP(2, 0, 1, -1, 30, 31, 29), // LDP FP, LR, [SP, #-8] (opc=2 for 64-bit pair)
|
||||
)
|
||||
ws = append(ws, arm64RetAddWords(uint32(fi.autosize))...)
|
||||
}
|
||||
}
|
||||
// RET: BR LR (0xd65f03c0)
|
||||
@@ -157,6 +317,37 @@ func arm64Return(fi arm64FrameInfo) []byte {
|
||||
return a64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// arm64RetInstr encodes a RET. The plain form runs the frame epilogue and
|
||||
// branches to LR; RET Rn runs the epilogue and branches to the register
|
||||
// (asm7.go case 78); RET sym(SB) runs the epilogue and branches to the
|
||||
// symbol with the call relocation, the toolchain's retJMP tail call.
|
||||
func arm64RetInstr(fi arm64FrameInfo, ops []*ast.Operand, relocs *[]Reloc) []byte {
|
||||
out := arm64Return(fi)
|
||||
if len(ops) != 1 {
|
||||
return out
|
||||
}
|
||||
op := ops[0]
|
||||
switch {
|
||||
case op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" && op.Addr.Base == "" && op.Addr.Index == "":
|
||||
if rn := arm64RegNum(op.Addr.Sym.Name); rn >= 0 {
|
||||
// The operand form replaces the default BR LR word.
|
||||
return append(out[:len(out)-4], a64wordLE(0xd65f0000|uint32(rn)<<5)...)
|
||||
}
|
||||
case op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" && op.Addr.Base == "" && op.Addr.Index == "":
|
||||
if relocs != nil {
|
||||
*relocs = append(*relocs, Reloc{
|
||||
Off: 0,
|
||||
After: 4,
|
||||
Name: op.Addr.Sym.Name,
|
||||
Addend: op.Addr.Sym.Offset,
|
||||
Kind: RelArm64Branch,
|
||||
})
|
||||
}
|
||||
return append(out[:len(out)-4], a64wordLE(0x14000000)...) // B sym(SB)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// arm64PrologueSpadjPC returns the function-relative byte offset where the
|
||||
// prologue has finished decrementing SP (the delta becomes autosize).
|
||||
func arm64PrologueSpadjPC(fi arm64FrameInfo) int {
|
||||
@@ -166,22 +357,32 @@ func arm64PrologueSpadjPC(fi arm64FrameInfo) int {
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 4 // MOVD.W instruction decrements SP
|
||||
}
|
||||
return 8 // SUB + STP + MOVD (3 instructions, SP updated at the MOVD)
|
||||
// Large frame: [SUB words][STP][ADD R20, SP]; SP moves at the ADD, whose
|
||||
// position depends on how many words the SUB itself took (immediate,
|
||||
// shifted immediate, the two-word imm12 split, or a materialised REGTMP
|
||||
// sequence).
|
||||
return 4 * (len(arm64SubImmWords(uint32(fi.autosize), 20)) + 1)
|
||||
}
|
||||
|
||||
// arm64ReturnEpilogueLen returns the byte length of the RET's epilogue up to
|
||||
// (but not including) the final RET instruction.
|
||||
// (but not including) the final RET instruction. The lengths are read from
|
||||
// the same word-emitting helpers the epilogue uses rather than assumed: the
|
||||
// leaf path shares the prologue's immediate ladder, and a materialised
|
||||
// autosize costs its MOV words plus the ADD itself.
|
||||
func arm64ReturnEpilogueLen(fi arm64FrameInfo) int {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
if fi.leaf {
|
||||
return 8 // ADD + ADD
|
||||
return 4 * (len(arm64AddImmWords(uint32(fi.autosize-8), 29)) +
|
||||
len(arm64AddImmWords(uint32(fi.autosize), 31)))
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 8 // LDR + LDR.P
|
||||
}
|
||||
return 8 // LDP + ADD
|
||||
// LDP + the deallocation emitted by arm64RetAddWords, so the length
|
||||
// tracks whatever the MOVD ladder needs.
|
||||
return 4 + 4*len(arm64RetAddWords(uint32(fi.autosize)))
|
||||
}
|
||||
|
||||
// arm64ResolvePseudo translates a pseudo-register memory reference into a
|
||||
@@ -208,6 +409,59 @@ func arm64ResolvePseudo(sym *ast.Symbol, fi arm64FrameInfo) (base int, off int32
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// arm64FrameAddrValue returns the SP-relative displacement a $sym+off(FP)
|
||||
// or $sym+off(SP) immediate-address operand stands for, the toolchain's
|
||||
// aclass arithmetic (asm7.go): a parameter reference sits autosize+8 above
|
||||
// the hardware SP, and a pseudo-SP reference sits frame+8 above it, the
|
||||
// alignment padding cancelling out of the autosize.
|
||||
func arm64FrameAddrValue(sym *ast.Symbol, fi arm64FrameInfo) int64 {
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
return sym.Offset + int64(fi.autosize) + 8
|
||||
default: // SP
|
||||
return sym.Offset + int64(fi.frame) + 8
|
||||
}
|
||||
}
|
||||
|
||||
// arm64IsAddcon reports whether v is an addcon value (asm7.go isaddcon): an
|
||||
// unsigned imm12, or a multiple of 4096 whose shifted form fits imm12.
|
||||
func arm64IsAddcon(v int64) bool {
|
||||
if v < 0 {
|
||||
return false
|
||||
}
|
||||
if v&0xFFF == 0 {
|
||||
v >>= 12
|
||||
}
|
||||
return v <= 0xFFF
|
||||
}
|
||||
|
||||
// arm64FrameAddrWords returns the word sequence of the toolchain's optab
|
||||
// case 4 for a frame-relative address (the C_AACON and C_AACON2 rows): one
|
||||
// ADD/SUB imm12 word inside the addcon band, SUB carrying a negative
|
||||
// displacement, and otherwise the hi<<12 word from SP followed by the low
|
||||
// word added in place. The 24-bit band never reaches the REGTMP
|
||||
// materialisation the ADD/SUB immediate ladder uses: aclass classifies the
|
||||
// address straight into C_AACON2.
|
||||
func arm64FrameAddrWords(v int64, rd int) []uint32 {
|
||||
word := func(v int64, rn int) uint32 {
|
||||
op := uint32(0) // ADD
|
||||
if v < 0 {
|
||||
op = 1 // SUB
|
||||
v = -v
|
||||
}
|
||||
sh := uint32(0)
|
||||
if v&0xFFF000 != 0 { // asm7.go oaddi: the shift form when low 12 bits are clear
|
||||
sh = 1
|
||||
v >>= 12
|
||||
}
|
||||
return a64AddSub(1, op, 0, sh, uint32(v), uint32(rn), uint32(rd))
|
||||
}
|
||||
if arm64IsAddcon(v) || arm64IsAddcon(-v) {
|
||||
return []uint32{word(v, 31)}
|
||||
}
|
||||
return []uint32{word(v&^int64(0xFFF), 31), word(v&0xFFF, rd)}
|
||||
}
|
||||
|
||||
// arm64PreStoreImm encodes a pre-index store (STR with writeback):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 1<<11 | 1<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64PreStoreImm(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
@@ -235,3 +489,90 @@ func arm64PostLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||
1<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// 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
|
||||
// The branches sit at fixed byte offsets in the guard prefix: after
|
||||
// the LDR (4), the ml MOV words (4*ml) and the SUBS (4) for B.LO,
|
||||
// then a further B.LO word and the CMP for B.LS.
|
||||
ws = append(ws, br(8+4*ml, a64CondLO))
|
||||
ws = append(ws, arm64DPSRWords(arm64OpSubs, 16, 17, 31)) // CMP R16, R17
|
||||
ws = append(ws, br(16+4*ml, a64CondLS))
|
||||
}
|
||||
return a64WordsLE(ws...)
|
||||
}
|
||||
|
||||
// arm64MoreStackBlockLen is the byte length of arm64MoreStackBlock: the
|
||||
// saved LR, the BL and the branch back, three words whatever the target.
|
||||
const arm64MoreStackBlockLen = 12
|
||||
|
||||
// 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,236 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// assembleArm64Source parses src and assembles it for arm64, returning the
|
||||
// first function's words little-endian.
|
||||
func assembleArm64Source(t *testing.T, src string) []uint32 {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("got %d funcs, want 1", len(img.Funcs))
|
||||
}
|
||||
return wordsOf(img.Code[img.Funcs[0].Offset : img.Funcs[0].Offset+img.Funcs[0].Size])
|
||||
}
|
||||
|
||||
func TestArm64FrameAddrEncoding(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·fr(SB), NOSPLIT, $432-24
|
||||
MOVD $argframe+0(FP), R3
|
||||
MOVD $big+4096(FP), R4
|
||||
MOVD $ret-8(FP), R2
|
||||
MOVD $x-64(FP), R5
|
||||
MOVD RSP, R19
|
||||
MOVD R20, RSP
|
||||
RET
|
||||
`
|
||||
ws := assembleArm64Source(t, src)
|
||||
// Prologue (4: large frame) then the body at words 4..9, the toolchain's
|
||||
// own encodings for the same statements:
|
||||
// ADD $456, RSP, R3 (456 = 448 + 8 + 0)
|
||||
// ADD $(1<<12), RSP, R4 (4552, the hi<<12 half)
|
||||
// ADD $456, R4, R4 (then the lo half)
|
||||
// ADD $448, RSP, R2 (448 - 8 + 8)
|
||||
// ADD $392, RSP, R5 (448 - 64 + 8)
|
||||
// ADD $0, RSP, R19 (the SP register move)
|
||||
// ADD $0, R20, RSP
|
||||
want := []uint32{
|
||||
0xd10703f4, 0xa93ffa9d, 0x9100029f, 0xd10023fd,
|
||||
0x910723e3, 0x914007e4, 0x91072084, 0x910703e2,
|
||||
0x910623e5, 0x910003f3, 0x9100029f,
|
||||
}
|
||||
if len(ws) < len(want) {
|
||||
t.Fatalf("got %d words, want at least %d", len(ws), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if ws[i] != w {
|
||||
t.Errorf("word %d: got %08x, want %08x", i, ws[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64FrameAddrSizes(t *testing.T) {
|
||||
// One imm12 word inside the addcon band, two inside the 24-bit band.
|
||||
tests := []struct {
|
||||
v int64
|
||||
verb int
|
||||
}{
|
||||
{456, 1},
|
||||
{0xFFF, 1},
|
||||
{0x1000, 1}, // the shifted imm12 form
|
||||
{0x1005, 2}, // the hi<<12 plus lo pair
|
||||
{0xFFFFFF, 2},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := len(arm64FrameAddrWords(tt.v, 3))
|
||||
if got != tt.verb {
|
||||
t.Errorf("arm64FrameAddrWords(%d) took %d words, want %d", tt.v, got, tt.verb)
|
||||
}
|
||||
}
|
||||
if ws := arm64FrameAddrWords(0x1000, 3); len(ws) != 1 || ws[0] != 0x914007e3 {
|
||||
t.Errorf("arm64FrameAddrWords(0x1000) = %08x, want the shifted ADD 914007e3", ws[0])
|
||||
}
|
||||
if !arm64IsAddcon(0xFFF) || arm64IsAddcon(0x1001) || arm64IsAddcon(-1) {
|
||||
t.Error("arm64IsAddcon misclassifies the band edges")
|
||||
}
|
||||
fp := &ast.Symbol{Name: "x", Pseudo: "FP", Offset: 8}
|
||||
if v := arm64FrameAddrValue(fp, arm64FrameInfo{autosize: 448}); v != 464 {
|
||||
t.Errorf("FP address: got %d, want 464", v)
|
||||
}
|
||||
sp := &ast.Symbol{Name: "x", Pseudo: "SP", Offset: 8}
|
||||
if v := arm64FrameAddrValue(sp, arm64FrameInfo{frame: 432}); v != 448 {
|
||||
t.Errorf("SP address: got %d, want 448", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64FrameAddrRejects(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"width", `TEXT ·f(SB), NOSPLIT, $16-8
|
||||
MOVW $x+0(FP), R7
|
||||
RET
|
||||
`, "illegal combination",
|
||||
},
|
||||
{
|
||||
"pool band", `TEXT ·f(SB), NOSPLIT, $20000000-8
|
||||
MOVD $x+20000000(FP), R7
|
||||
RET
|
||||
`, "literal pool",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, errs := parser.Parse("test_arm64.s", tt.src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
_, err := AssembleFileARM64(f)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: no error, want one naming %q", tt.name, tt.want)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.want) {
|
||||
t.Errorf("%s: error %q, want it to name %q", tt.name, err, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64SPMoveEncoding(t *testing.T) {
|
||||
src := `TEXT ·f(SB), NOSPLIT, $0-8
|
||||
MOVD RSP, R19
|
||||
MOVD R20, RSP
|
||||
MOVD ZR, R4
|
||||
MOVD RSP, RSP
|
||||
RET
|
||||
`
|
||||
ws := assembleArm64Source(t, src)
|
||||
// The SP register moves ride ADD $0; the zero move stays ORR.
|
||||
want := []uint32{
|
||||
0x910003f3, // ADD $0, RSP, R19
|
||||
0x9100029f, // ADD $0, R20, RSP
|
||||
0xaa1f03e4, // ORR R4, ZR, ZR
|
||||
0x910003ff, // ADD $0, RSP, RSP
|
||||
0xd65f03c0, // RET
|
||||
}
|
||||
if len(ws) != len(want) {
|
||||
t.Fatalf("got %d words, want %d", len(ws), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if ws[i] != w {
|
||||
t.Errorf("word %d: got %08x, want %08x", i, ws[i], w)
|
||||
}
|
||||
}
|
||||
|
||||
rej := `TEXT ·f(SB), NOSPLIT, $0-8
|
||||
MOVW RSP, R7
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", rej)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if _, err := AssembleFileARM64(f); err == nil || !strings.Contains(err.Error(), "illegal combination") {
|
||||
t.Errorf("MOVW RSP: error %v, want an illegal-combination refusal", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64AliasLiveness(t *testing.T) {
|
||||
// A define inside a dead conditional branch must not become an alias:
|
||||
// go_tls.h's `#ifdef GOARCH_arm` block defines LR as R14, which would
|
||||
// silently renumber the link register on arm64, where LR is R30.
|
||||
src := `#define RARG R5
|
||||
#ifdef GOARCH_arm
|
||||
#define LR R14
|
||||
#endif
|
||||
TEXT ·f(SB), NOSPLIT, $0-8
|
||||
MOVD LR, R0
|
||||
MOVD RARG, R1
|
||||
MOVD R14, R2
|
||||
RET
|
||||
`
|
||||
ws := assembleArm64Source(t, src)
|
||||
want := []uint32{
|
||||
0xaa1e03e0, // ORR R0, ZR, R30: LR stayed the link register
|
||||
0xaa0503e1, // ORR R1, ZR, R5: the live alias applied
|
||||
0xaa0e03e2, // ORR R2, ZR, R14: R14 is R14
|
||||
0xd65f03c0,
|
||||
}
|
||||
if len(ws) != len(want) {
|
||||
t.Fatalf("got %d words, want %d", len(ws), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if ws[i] != w {
|
||||
t.Errorf("word %d: got %08x, want %08x", i, ws[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArm64ADRNoChainChase(t *testing.T) {
|
||||
// The toolchain's jump-to-jump collapse rewrites branch targets only:
|
||||
// a B to a chain-leading label is redirected, an ADR to the same label
|
||||
// resolves to the label itself.
|
||||
src := `TEXT ·adrchain(SB), NOSPLIT, $0-0
|
||||
B a
|
||||
a:
|
||||
B b
|
||||
b:
|
||||
ADR a, R0
|
||||
RET
|
||||
`
|
||||
ws := assembleArm64Source(t, src)
|
||||
want := []uint32{
|
||||
0x14000002, // B +2: chased through a to b
|
||||
0x14000001, // B +1: a's own jump to b
|
||||
0x10ffffe0, // ADR a, R0: -4, the label itself, unchased
|
||||
0xd65f03c0,
|
||||
}
|
||||
if len(ws) != len(want) {
|
||||
t.Fatalf("got %d words, want %d", len(ws), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
if ws[i] != w {
|
||||
t.Errorf("word %d: got %08x, want %08x", i, ws[i], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
var le = binary.LittleEndian
|
||||
|
||||
// kindSTEXTFIPS is objabi's STEXTFIPS: the fips140 packages' text kind.
|
||||
const kindSTEXTFIPS = 2
|
||||
|
||||
// gorootARM64Packages names the GOROOT packages whose arm64 assembly the
|
||||
// parity harness pins: every *_arm64.s of each package, as the real build
|
||||
// assembles it, the package's generated go_asm.h included. Together they
|
||||
// carry the heavy real-world shapes: the runtime's TLS and stack plumbing,
|
||||
// the cryptographic kernels, big-number arithmetic and the bytealg search
|
||||
// loops.
|
||||
var gorootARM64Packages = []string{
|
||||
"runtime",
|
||||
"internal/bytealg",
|
||||
"internal/cpu",
|
||||
"internal/chacha8rand",
|
||||
"internal/runtime/maps",
|
||||
"reflect",
|
||||
"math/big",
|
||||
"hash/crc32",
|
||||
"crypto/md5",
|
||||
"crypto/sha1",
|
||||
"crypto/internal/fips140/aes",
|
||||
"crypto/internal/fips140/aes/gcm",
|
||||
"crypto/internal/fips140/bigmod",
|
||||
"crypto/internal/fips140/nistec",
|
||||
"crypto/internal/fips140/sha256",
|
||||
"crypto/internal/fips140/sha512",
|
||||
"crypto/internal/fips140/sha3",
|
||||
"crypto/internal/fips140/subtle",
|
||||
}
|
||||
|
||||
// gorootARM64GapFiles names the files kept out of the byte parity set by
|
||||
// known, pre-existing gaps, each with the reason. A file here is skipped,
|
||||
// not silently dropped: the gaps are findings, and closing one is a matter
|
||||
// of removing its entry and watching the file pin itself.
|
||||
var gorootARM64GapFiles = map[string]string{
|
||||
"runtime/asm_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge",
|
||||
"runtime/sys_linux_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge (cgoSigtramp, clone)",
|
||||
"runtime/preempt_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue and guard shapes diverge (asyncPreempt)",
|
||||
"runtime/race_arm64.s": "the unparenthesised NOSPLIT|NOFRAME flag list is not recognised, so the prologue shape diverges (racecallbackthunk)",
|
||||
"runtime/rt0_linux_arm64.s": "the #ifdef GOOS selection diverges: gasm keeps a word the toolchain drops",
|
||||
}
|
||||
|
||||
// gorootOtherGOOS matches the file names of the ports the linux build never
|
||||
// assembles: the harness pins the linux arm64 set.
|
||||
var gorootOtherGOOS = regexp.MustCompile(`_(darwin|ios|freebsd|netbsd|openbsd|windows|android|plan9|aix|js|wasip1)_`)
|
||||
|
||||
// TestGOROOTARM64Parity assembles each package's arm64 files with gasm and
|
||||
// with the installed toolchain and holds the functions' bytes equal,
|
||||
// relocation sites masked. The toolchain side needs the go_asm.h the build
|
||||
// generates for the package, so the harness rebuilds it once with -work and
|
||||
// harvests the header the compiler wrote; the -gcflags flag exists only to
|
||||
// make that rebuild happen, the header's constants do not depend on it.
|
||||
func TestGOROOTARM64Parity(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live go tool asm oracle and per-package rebuild: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
out, err := exec.Command(goBin, "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go env GOROOT: %v", err)
|
||||
}
|
||||
goroot := strings.TrimSpace(string(out))
|
||||
include := filepath.Join(goroot, "pkg", "include")
|
||||
|
||||
totalFns, totalBytes := 0, 0
|
||||
for _, pkg := range gorootARM64Packages {
|
||||
t.Run(pkg, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
work := harvestGoAsm(t, goBin, pkg)
|
||||
defer os.RemoveAll(work)
|
||||
headers, err := filepath.Glob(filepath.Join(work, "b*", "go_asm.h"))
|
||||
if err != nil || len(headers) == 0 {
|
||||
t.Fatalf("no generated go_asm.h under %s", work)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go_asm.h"), mustRead(t, headers[0]), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files, err := filepath.Glob(filepath.Join(goroot, "src", pkg, "*_arm64.s"))
|
||||
if err != nil || len(files) == 0 {
|
||||
t.Fatalf("no arm64 assembly found for %s", pkg)
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
base := filepath.Base(f)
|
||||
if gorootOtherGOOS.MatchString(base) {
|
||||
continue // another port's file: the linux build never assembles it
|
||||
}
|
||||
t.Run(base, func(t *testing.T) {
|
||||
if reason, gap := gorootARM64GapFiles[pkg+"/"+base]; gap {
|
||||
t.Skip(reason)
|
||||
}
|
||||
// The parser side: the file's own directory resolves the
|
||||
// package's headers, the harvested directory carries
|
||||
// go_asm.h, and the platform conditionals read the same
|
||||
// predefines the go command drives go tool asm with.
|
||||
src := string(mustRead(t, f))
|
||||
af, errs := parser.ParseWithOptions(f, src, parser.Options{
|
||||
Expand: true,
|
||||
IncludeDirs: []string{dir, filepath.Join(goroot, "src", pkg), include},
|
||||
Predefines: map[string]string{
|
||||
"GOARCH_arm64": "1",
|
||||
"GOOS_linux": "1",
|
||||
},
|
||||
})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs[0])
|
||||
}
|
||||
img, err := AssembleFileARM64(af)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
|
||||
// The oracle side: the build's own invocation, the
|
||||
// generated header directory first.
|
||||
objPath := filepath.Join(t.TempDir(), "oracle.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm",
|
||||
"-I", dir, "-I", filepath.Join(goroot, "src", pkg), "-I", include,
|
||||
"-D", "GOOS_linux", "-D", "GOARCH_arm64", "-std",
|
||||
"-p", pkg, "-o", objPath, f)
|
||||
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64")
|
||||
if oout, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go tool asm %s: %v\n%s", base, err, oout)
|
||||
}
|
||||
byLocal := oracleFuncText(t, mustRead(t, objPath))
|
||||
|
||||
// The object's symdef order is the source order, gasm's
|
||||
// function list too, so same-named functions pair up in
|
||||
// definition order: a file-local kernel beside its
|
||||
// package-level twin (runtime·racefuncenter and
|
||||
// racefuncenter<>) carries the plain name twice in the
|
||||
// object and the reader cannot see the locality.
|
||||
seen := map[string]int{}
|
||||
for _, fn := range img.Funcs {
|
||||
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
bodies := byLocal[fn.Name]
|
||||
idx := seen[fn.Name]
|
||||
seen[fn.Name] = idx + 1
|
||||
if idx >= len(bodies) {
|
||||
keys := make([]string, 0, len(byLocal))
|
||||
for name := range byLocal {
|
||||
keys = append(keys, name)
|
||||
}
|
||||
t.Errorf("%s: not in the oracle output (%d functions: %s)",
|
||||
fn.Name, len(byLocal), strings.Join(keys, ", "))
|
||||
continue
|
||||
}
|
||||
goCode := maskCode(append([]byte(nil), bodies[idx]...), fn.Relocs)
|
||||
cmpLen := min(len(goCode), len(gasmCode))
|
||||
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
||||
for w := 0; w < cmpLen/4; w++ {
|
||||
g := le.Uint32(gasmCode[w*4:])
|
||||
o := le.Uint32(goCode[w*4:])
|
||||
if g != o {
|
||||
t.Errorf("%s: word %d (offset %d) differs: gasm %08x go %08x", fn.Name, w, w*4, g, o)
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(goCode) > len(gasmCode) {
|
||||
for _, b := range goCode[len(gasmCode):] {
|
||||
if b != 0 {
|
||||
t.Errorf("%s: non-zero trailing bytes in the oracle output", fn.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
totalFns++
|
||||
totalBytes += len(gasmCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Logf("GOROOT arm64 parity: %d functions, %d bytes identical", totalFns, totalBytes)
|
||||
}
|
||||
|
||||
// oracleFuncText extracts every TEXT function of a toolchain object, the
|
||||
// non-package and the hashed (file-local) definitions both, keyed by the
|
||||
// local name: GOROOT keeps several kernels file-local (cmpbody<>,
|
||||
// encryptBlockAsm<>), and those ride the hashed definition blocks the
|
||||
// non-package reader never sees. The value is the functions' bodies in
|
||||
// symdef order: a file-local kernel beside its package-level twin carries
|
||||
// the same plain name twice (the object reader cannot see the locality),
|
||||
// and the encoder pairs them up in definition order.
|
||||
func oracleFuncText(t *testing.T, obj []byte) map[string][][]byte {
|
||||
t.Helper()
|
||||
v := openGoobj(t, obj)
|
||||
data := v.blk(blkData)
|
||||
didx := v.blk(blkDataIdx)
|
||||
out := make(map[string][][]byte)
|
||||
di := 0
|
||||
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef, blkNonpkgdef} {
|
||||
for _, s := range v.syms(bi) {
|
||||
// STEXT and STEXTFIPS both: the fips140 packages' text carries
|
||||
// the FIPS kind in Go 1.27 and up.
|
||||
if (s.typ == kindSTEXT || s.typ == kindSTEXTFIPS) && s.size > 0 && 4*di+8 <= len(didx) {
|
||||
off := le.Uint32(didx[4*di:])
|
||||
if int(off)+int(s.size) <= len(data) {
|
||||
name := s.name
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
name = after
|
||||
}
|
||||
out[name] = append(out[name], data[off:int(off)+int(s.size)])
|
||||
}
|
||||
}
|
||||
di++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// harvestGoAsm rebuilds pkg once with -work and returns the work directory
|
||||
// holding the compiler's generated go_asm.h. A fully cached build leaves
|
||||
// the work directory empty, so the compile action is given a unique, inert
|
||||
// flag value each run (the inlining level never touches the header's
|
||||
// constants) and re-runs for the target package alone.
|
||||
func harvestGoAsm(t *testing.T, goBin, pkg string) string {
|
||||
t.Helper()
|
||||
nonce := time.Now().UnixNano() % 1000000
|
||||
cmd := exec.Command(goBin, "build", "-x", "-work",
|
||||
"-gcflags", pkg+"=-N", "-gcflags", pkg+"=-l=7"+strconv.FormatInt(nonce, 10),
|
||||
"-o", "/dev/null", pkg)
|
||||
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("rebuild %s: %v\n%s", pkg, err, out)
|
||||
}
|
||||
m := regexp.MustCompile(`WORK=(\S+)`).FindSubmatch(out)
|
||||
if m == nil {
|
||||
t.Fatalf("rebuild %s: no WORK directory in the build log", pkg)
|
||||
}
|
||||
return string(m[1])
|
||||
}
|
||||
|
||||
// mustRead reads path, failing the test when it cannot.
|
||||
func mustRead(t *testing.T, path string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The mid-function literal pool flush, pinned against the toolchain. A
|
||||
// kernel of distinct pooled stores walks the conservative displacement bound
|
||||
// (asm7.go's maxPCDisp) inside one body, so the pool must drain exactly
|
||||
// where cmd/internal/obj/arm64's checkpool drains it: the bytes, the flush
|
||||
// points and the reach of every load literal are the toolchain's own.
|
||||
|
||||
// pooledKernel renders a kernel of n distinct pooled stores: each offset
|
||||
// sits a step of 8 past the aligned split band (every fourth one aligned,
|
||||
// but 0x2000000+8i+4 never is), so every statement pools its own four-byte
|
||||
// word and expands to eight instruction bytes, the densest walk towards the
|
||||
// distance bound an arm64 body can make. head carries the TEXT line for the
|
||||
// splitting variant, tail the closing statements.
|
||||
func pooledKernel(head string, n int, tail string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#include \"textflag.h\"\n")
|
||||
if head != "" {
|
||||
b.WriteString(head)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("TEXT \u00b7poolmid(SB), NOSPLIT, $0-0\n")
|
||||
for i := range n {
|
||||
b.WriteString("\tMOVD\tR1, ")
|
||||
b.WriteString(decimal(0x2000000 + 8*i + 4))
|
||||
b.WriteString("(R2)\n")
|
||||
}
|
||||
b.WriteString(tail)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// decimal formats v in decimal.
|
||||
func decimal(v int) string {
|
||||
if v == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for v > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + v%10)
|
||||
v /= 10
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// assemblePooled parses and assembles a generated kernel, returning the
|
||||
// image and the single function's code words.
|
||||
func assemblePooled(t *testing.T, head string, n int, tail string) (*Image, []uint32) {
|
||||
t.Helper()
|
||||
src := pooledKernel(head, n, tail)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "poolmid_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("functions = %d, want 1", len(img.Funcs))
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
return img, leWords(img.Code[fn.Offset : fn.Offset+fn.Size])
|
||||
}
|
||||
|
||||
// a64BranchWord reports whether w is an unconditional B: op 000101 in bits
|
||||
// 31..26, the encoding the flush guards ride.
|
||||
func a64BranchWord(w uint32) bool {
|
||||
return w>>26 == 0x05
|
||||
}
|
||||
|
||||
// a64BranchTarget decodes a B word's target byte offset from its own pc.
|
||||
func a64BranchTarget(w uint32, pc int) int {
|
||||
d := int(w & 0x03FFFFFF)
|
||||
if d&(1<<25) != 0 {
|
||||
d |= ^0x03FFFFFF
|
||||
}
|
||||
return pc + 4*d
|
||||
}
|
||||
|
||||
// a64LoadLiteral reports whether w is a LDR W/X literal (the pool's loads
|
||||
// into REGTMP: word forms 0x18 and 0x58 in the top byte) and decodes its
|
||||
// target byte displacement.
|
||||
func a64LoadLiteral(w uint32) (int, bool) {
|
||||
if w>>24 != 0x18 && w>>24 != 0x58 {
|
||||
return 0, false
|
||||
}
|
||||
d := int(w>>5) & 0x7FFFF
|
||||
if d&(1<<18) != 0 {
|
||||
d |= ^0x7FFFF
|
||||
}
|
||||
return d * 4, true
|
||||
}
|
||||
|
||||
// TestArm64PoolFlushStructure assembles the distance-bound kernel and pins
|
||||
// the flush structure on gasm's own image: exactly one mid-body branch over
|
||||
// a drained segment, no code words inside it, every load literal inside the
|
||||
// conservative displacement bound, and the segment's byte range free of line
|
||||
// rows: the pool words carry the flushing statement's source line, so the
|
||||
// pc-line tables see no delta across them.
|
||||
func TestArm64PoolFlushStructure(t *testing.T) {
|
||||
const n = 44000 // one flush: the bound arrives at about 43690 statements
|
||||
img, words := assemblePooled(t, "", n, "\tRET\n")
|
||||
fn := img.Funcs[0]
|
||||
|
||||
var branches []int
|
||||
for i, w := range words {
|
||||
if a64BranchWord(w) {
|
||||
branches = append(branches, i*4)
|
||||
}
|
||||
}
|
||||
if len(branches) != 1 {
|
||||
t.Fatalf("branch words = %d, want exactly the one flush guard", len(branches))
|
||||
}
|
||||
branchPC := branches[0]
|
||||
target := a64BranchTarget(words[branchPC/4], branchPC)
|
||||
segStart, segEnd := branchPC+4, target
|
||||
if segEnd <= segStart || segEnd%4 != 0 {
|
||||
t.Fatalf("flush branch target %d leaves no legal word range after %d", target, branchPC)
|
||||
}
|
||||
if segEnd+4 > len(words)*4 {
|
||||
t.Fatalf("flush branch target %d runs past the image (%d words)", target, len(words))
|
||||
}
|
||||
// The drained segment holds pool words only: four-byte constants, no
|
||||
// branch opcodes, no literal loads.
|
||||
for off := segStart; off < segEnd; off += 4 {
|
||||
w := words[off/4]
|
||||
if a64BranchWord(w) {
|
||||
t.Fatalf("word at %d inside the drained segment is a branch: %08x", off, w)
|
||||
}
|
||||
if _, lit := a64LoadLiteral(w); lit {
|
||||
t.Fatalf("word at %d inside the drained segment is a literal load: %08x", off, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Every load literal resolves inside the conservative bound and inside
|
||||
// the image: the property the flush exists to keep.
|
||||
for i, w := range words {
|
||||
d, ok := a64LoadLiteral(w)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pc := i * 4
|
||||
if pc+d < 0 || pc+d >= len(words)*4 {
|
||||
t.Fatalf("literal load at %d targets %d, outside the image", pc, pc+d)
|
||||
}
|
||||
if d >= a64MaxPCDisp || d <= -a64MaxPCDisp {
|
||||
t.Fatalf("literal load at %d displaces %d, outside the conservative bound", pc, d)
|
||||
}
|
||||
}
|
||||
|
||||
// The drained words carry no line rows of their own: the toolchain gives
|
||||
// them the flushing statement's Pos so the pc-line tables see no delta.
|
||||
for _, l := range fn.Lines {
|
||||
if l.Offset >= segStart && l.Offset < segEnd {
|
||||
t.Fatalf("line row at %d sits inside the drained segment [%d, %d)", l.Offset, segStart, segEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestArm64PoolFlushDifferential assembles the kernel family with gasm and
|
||||
// with the installed toolchain and holds the bytes equal, the toolchain's
|
||||
// tail alignment padding excepted: one flush, a fall-through end behind the
|
||||
// UNDEF guard, two flushes, and a splitting function whose guard prefix and
|
||||
// morestack block sit behind a shifted pool.
|
||||
func TestArm64PoolFlushDifferential(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live go tool asm oracle: skipped in -short mode")
|
||||
}
|
||||
for _, k := range []struct {
|
||||
name string
|
||||
head string
|
||||
n int
|
||||
tail string
|
||||
}{
|
||||
{"one flush, RET end", "", 44000, "\tRET\n"},
|
||||
{"one flush, UNDEF end", "", 44000, ""},
|
||||
{"two flushes", "", 90000, "\tRET\n"},
|
||||
{"split function", "TEXT \u00b7poolmid(SB), $8-0", 44000, "\tRET\n"},
|
||||
} {
|
||||
t.Run(k.name, func(t *testing.T) {
|
||||
if k.head != "" {
|
||||
// The split variant spells its own TEXT: the generator's
|
||||
// NOSPLIT line must give way to it.
|
||||
runPoolFlushCase(t, pooledKernelFor(k.head, k.n, k.tail))
|
||||
return
|
||||
}
|
||||
runPoolFlushCase(t, pooledKernel("", k.n, k.tail))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// pooledKernelFor renders the kernel with an explicit TEXT line, the split
|
||||
// variant's shape: no NOSPLIT, so the frame forces the stack-split guard.
|
||||
func pooledKernelFor(text string, n int, tail string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#include \"textflag.h\"\n")
|
||||
b.WriteString(text)
|
||||
b.WriteString("\n")
|
||||
for i := range n {
|
||||
b.WriteString("\tMOVD\tR1, ")
|
||||
b.WriteString(decimal(0x2000000 + 8*i + 4))
|
||||
b.WriteString("(R2)\n")
|
||||
}
|
||||
b.WriteString(tail)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// runPoolFlushCase assembles one generated kernel both ways and compares
|
||||
// the function's bytes, relocation sites masked, the toolchain's trailing
|
||||
// alignment zeros excepted.
|
||||
func runPoolFlushCase(t *testing.T, src string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "poolmid_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gt := oracleFuncCode(t, toolAsmObject(t, path, "arm64"))
|
||||
byLocal := make(map[string][]byte, len(gt))
|
||||
for name, code := range gt {
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
name = after
|
||||
}
|
||||
byLocal[name] = code
|
||||
}
|
||||
for _, fn := range img.Funcs {
|
||||
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
goCode, ok := byLocal[fn.Name]
|
||||
if !ok {
|
||||
t.Fatalf("%s: not in the oracle output (%d functions)", fn.Name, len(gt))
|
||||
}
|
||||
goCode = maskCode(append([]byte(nil), goCode...), fn.Relocs)
|
||||
cmpLen := min(len(goCode), len(gasmCode))
|
||||
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
||||
for w := 0; w < cmpLen/4; w++ {
|
||||
g := binary.LittleEndian.Uint32(gasmCode[w*4:])
|
||||
o := binary.LittleEndian.Uint32(goCode[w*4:])
|
||||
if g != o {
|
||||
t.Fatalf("%s: word %d (offset %d) differs: gasm %08x go %08x", fn.Name, w, w*4, g, o)
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s: prefixes equal but lengths differ (gasm %d, oracle %d)", fn.Name, len(gasmCode), len(goCode))
|
||||
}
|
||||
for _, b := range goCode[len(gasmCode):] {
|
||||
if b != 0 {
|
||||
t.Fatalf("%s: non-zero trailing bytes in the oracle output", fn.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// 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-sdk/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.
|
||||
}
|
||||
|
||||
// TestArm64TLSLoad pins the local-exec TLS load: a symbol the file's own
|
||||
// GLOBL marks TLSBSS loads as one MOVZ word carrying the R_ARM64_TLS_LE
|
||||
// relocation (asm7.go case 69), the shape `go tool asm` emits for the
|
||||
// runtime's tls_g accesses. A non-TLS GLOBL keeps the ADRP+LDR pair.
|
||||
func TestArm64TLSLoad(t *testing.T) {
|
||||
img := parseArm64File(t, "#include \"textflag.h\"\n\n"+
|
||||
"TEXT \u00b7f(SB), NOSPLIT, $0-0\n"+
|
||||
"\tMOVD tlsvar(SB), R0\n"+
|
||||
"\tMOVD plain(SB), R1\n"+
|
||||
"\tRET\n"+
|
||||
"GLOBL tlsvar(SB), TLSBSS, $8\n"+
|
||||
"GLOBL plain(SB), NOPTR, $8\n")
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 4+8+4 {
|
||||
t.Fatalf("function size = %d, want 16", fn.Size)
|
||||
}
|
||||
if w := binary.LittleEndian.Uint32(img.Code[fn.Offset:]); w != 0xd2800000 {
|
||||
t.Errorf("TLS load word = %08x, want MOVZ 0 (d2800000)", w)
|
||||
}
|
||||
var tlsSeen, plainSeen bool
|
||||
for _, r := range fn.Relocs {
|
||||
if r.Name != "tlsvar" {
|
||||
continue
|
||||
}
|
||||
tlsSeen = true
|
||||
if r.Kind != RelArm64TLSLE {
|
||||
t.Errorf("tlsvar reloc kind = %v, want RelArm64TLSLE", r.Kind)
|
||||
}
|
||||
if r.Off != 0 {
|
||||
t.Errorf("tlsvar reloc off = %d, want 0", r.Off)
|
||||
}
|
||||
}
|
||||
for _, r := range fn.Relocs {
|
||||
if r.Name == "plain" && r.Kind == RelArm64LDST64 {
|
||||
plainSeen = true
|
||||
}
|
||||
}
|
||||
if !tlsSeen {
|
||||
t.Error("no tlsvar relocation recorded")
|
||||
}
|
||||
if !plainSeen {
|
||||
t.Error("the plain GLOBL load lost its ADRP+LDR relocation")
|
||||
}
|
||||
// The other widths have no TLS row: the toolchain refuses them.
|
||||
f, errs := parser.Parse("k_arm64.s", "TEXT \u00b7f(SB), NOSPLIT, $0-0\n"+
|
||||
"\tMOVW tlsvar(SB), R0\n"+
|
||||
"\tRET\n"+
|
||||
"GLOBL tlsvar(SB), TLSBSS, $8\n")
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
if _, err := AssembleFileARM64(f); err == nil {
|
||||
t.Error("MOVW of a TLS symbol assembled, want an illegal combination")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "strings"
|
||||
|
||||
// arm64 system registers and system-instruction aliases.
|
||||
//
|
||||
// The tables are transcribed from the data the Go toolchain itself carries
|
||||
// (cmd/internal/obj/arm64/sysRegEnc.go and the sysInstFields map of asm7.go),
|
||||
// which the ARM ARM defines: every system register is the packed field set
|
||||
// op0<<19 | op1<<16 | CRn<<12 | CRm<<8 | op2<<5, and the read/write flags are
|
||||
// the toolchain's own access classification. The encoding tables live here so
|
||||
// the encoder stays testable against the GOROOT testdata word for word.
|
||||
|
||||
// a64SysReg is one system register: the packed encoding fields and the
|
||||
// directions the register supports.
|
||||
type a64SysReg struct {
|
||||
v uint32
|
||||
read bool
|
||||
write bool
|
||||
}
|
||||
|
||||
// a64SysRegs maps the system register names the toolchain knows to their
|
||||
// encodings. MRS reads 0xd5300000 | v | Rd and MSR writes
|
||||
// 0xd5100000 | v | Rt.
|
||||
var a64SysRegs = map[string]a64SysReg{
|
||||
"ACTLR_EL1": a64SysReg{0x181020, true, true},
|
||||
"AFSR0_EL1": a64SysReg{0x185100, true, true},
|
||||
"AFSR1_EL1": a64SysReg{0x185120, true, true},
|
||||
"AIDR_EL1": a64SysReg{0x1900e0, true, false},
|
||||
"AMAIR_EL1": a64SysReg{0x18a300, true, true},
|
||||
"AMCFGR_EL0": a64SysReg{0x1bd220, true, false},
|
||||
"AMCGCR_EL0": a64SysReg{0x1bd240, true, false},
|
||||
"AMCNTENCLR0_EL0": a64SysReg{0x1bd280, true, true},
|
||||
"AMCNTENCLR1_EL0": a64SysReg{0x1bd300, true, true},
|
||||
"AMCNTENSET0_EL0": a64SysReg{0x1bd2a0, true, true},
|
||||
"AMCNTENSET1_EL0": a64SysReg{0x1bd320, true, true},
|
||||
"AMCR_EL0": a64SysReg{0x1bd200, true, true},
|
||||
"AMEVCNTR00_EL0": a64SysReg{0x1bd400, true, true},
|
||||
"AMEVCNTR01_EL0": a64SysReg{0x1bd420, true, true},
|
||||
"AMEVCNTR02_EL0": a64SysReg{0x1bd440, true, true},
|
||||
"AMEVCNTR03_EL0": a64SysReg{0x1bd460, true, true},
|
||||
"AMEVCNTR04_EL0": a64SysReg{0x1bd480, true, true},
|
||||
"AMEVCNTR05_EL0": a64SysReg{0x1bd4a0, true, true},
|
||||
"AMEVCNTR06_EL0": a64SysReg{0x1bd4c0, true, true},
|
||||
"AMEVCNTR07_EL0": a64SysReg{0x1bd4e0, true, true},
|
||||
"AMEVCNTR08_EL0": a64SysReg{0x1bd500, true, true},
|
||||
"AMEVCNTR09_EL0": a64SysReg{0x1bd520, true, true},
|
||||
"AMEVCNTR010_EL0": a64SysReg{0x1bd540, true, true},
|
||||
"AMEVCNTR011_EL0": a64SysReg{0x1bd560, true, true},
|
||||
"AMEVCNTR012_EL0": a64SysReg{0x1bd580, true, true},
|
||||
"AMEVCNTR013_EL0": a64SysReg{0x1bd5a0, true, true},
|
||||
"AMEVCNTR014_EL0": a64SysReg{0x1bd5c0, true, true},
|
||||
"AMEVCNTR015_EL0": a64SysReg{0x1bd5e0, true, true},
|
||||
"AMEVCNTR10_EL0": a64SysReg{0x1bdc00, true, true},
|
||||
"AMEVCNTR11_EL0": a64SysReg{0x1bdc20, true, true},
|
||||
"AMEVCNTR12_EL0": a64SysReg{0x1bdc40, true, true},
|
||||
"AMEVCNTR13_EL0": a64SysReg{0x1bdc60, true, true},
|
||||
"AMEVCNTR14_EL0": a64SysReg{0x1bdc80, true, true},
|
||||
"AMEVCNTR15_EL0": a64SysReg{0x1bdca0, true, true},
|
||||
"AMEVCNTR16_EL0": a64SysReg{0x1bdcc0, true, true},
|
||||
"AMEVCNTR17_EL0": a64SysReg{0x1bdce0, true, true},
|
||||
"AMEVCNTR18_EL0": a64SysReg{0x1bdd00, true, true},
|
||||
"AMEVCNTR19_EL0": a64SysReg{0x1bdd20, true, true},
|
||||
"AMEVCNTR110_EL0": a64SysReg{0x1bdd40, true, true},
|
||||
"AMEVCNTR111_EL0": a64SysReg{0x1bdd60, true, true},
|
||||
"AMEVCNTR112_EL0": a64SysReg{0x1bdd80, true, true},
|
||||
"AMEVCNTR113_EL0": a64SysReg{0x1bdda0, true, true},
|
||||
"AMEVCNTR114_EL0": a64SysReg{0x1bddc0, true, true},
|
||||
"AMEVCNTR115_EL0": a64SysReg{0x1bdde0, true, true},
|
||||
"AMEVTYPER00_EL0": a64SysReg{0x1bd600, true, false},
|
||||
"AMEVTYPER01_EL0": a64SysReg{0x1bd620, true, false},
|
||||
"AMEVTYPER02_EL0": a64SysReg{0x1bd640, true, false},
|
||||
"AMEVTYPER03_EL0": a64SysReg{0x1bd660, true, false},
|
||||
"AMEVTYPER04_EL0": a64SysReg{0x1bd680, true, false},
|
||||
"AMEVTYPER05_EL0": a64SysReg{0x1bd6a0, true, false},
|
||||
"AMEVTYPER06_EL0": a64SysReg{0x1bd6c0, true, false},
|
||||
"AMEVTYPER07_EL0": a64SysReg{0x1bd6e0, true, false},
|
||||
"AMEVTYPER08_EL0": a64SysReg{0x1bd700, true, false},
|
||||
"AMEVTYPER09_EL0": a64SysReg{0x1bd720, true, false},
|
||||
"AMEVTYPER010_EL0": a64SysReg{0x1bd740, true, false},
|
||||
"AMEVTYPER011_EL0": a64SysReg{0x1bd760, true, false},
|
||||
"AMEVTYPER012_EL0": a64SysReg{0x1bd780, true, false},
|
||||
"AMEVTYPER013_EL0": a64SysReg{0x1bd7a0, true, false},
|
||||
"AMEVTYPER014_EL0": a64SysReg{0x1bd7c0, true, false},
|
||||
"AMEVTYPER015_EL0": a64SysReg{0x1bd7e0, true, false},
|
||||
"AMEVTYPER10_EL0": a64SysReg{0x1bde00, true, true},
|
||||
"AMEVTYPER11_EL0": a64SysReg{0x1bde20, true, true},
|
||||
"AMEVTYPER12_EL0": a64SysReg{0x1bde40, true, true},
|
||||
"AMEVTYPER13_EL0": a64SysReg{0x1bde60, true, true},
|
||||
"AMEVTYPER14_EL0": a64SysReg{0x1bde80, true, true},
|
||||
"AMEVTYPER15_EL0": a64SysReg{0x1bdea0, true, true},
|
||||
"AMEVTYPER16_EL0": a64SysReg{0x1bdec0, true, true},
|
||||
"AMEVTYPER17_EL0": a64SysReg{0x1bdee0, true, true},
|
||||
"AMEVTYPER18_EL0": a64SysReg{0x1bdf00, true, true},
|
||||
"AMEVTYPER19_EL0": a64SysReg{0x1bdf20, true, true},
|
||||
"AMEVTYPER110_EL0": a64SysReg{0x1bdf40, true, true},
|
||||
"AMEVTYPER111_EL0": a64SysReg{0x1bdf60, true, true},
|
||||
"AMEVTYPER112_EL0": a64SysReg{0x1bdf80, true, true},
|
||||
"AMEVTYPER113_EL0": a64SysReg{0x1bdfa0, true, true},
|
||||
"AMEVTYPER114_EL0": a64SysReg{0x1bdfc0, true, true},
|
||||
"AMEVTYPER115_EL0": a64SysReg{0x1bdfe0, true, true},
|
||||
"AMUSERENR_EL0": a64SysReg{0x1bd260, true, true},
|
||||
"APDAKeyHi_EL1": a64SysReg{0x182220, true, true},
|
||||
"APDAKeyLo_EL1": a64SysReg{0x182200, true, true},
|
||||
"APDBKeyHi_EL1": a64SysReg{0x182260, true, true},
|
||||
"APDBKeyLo_EL1": a64SysReg{0x182240, true, true},
|
||||
"APGAKeyHi_EL1": a64SysReg{0x182320, true, true},
|
||||
"APGAKeyLo_EL1": a64SysReg{0x182300, true, true},
|
||||
"APIAKeyHi_EL1": a64SysReg{0x182120, true, true},
|
||||
"APIAKeyLo_EL1": a64SysReg{0x182100, true, true},
|
||||
"APIBKeyHi_EL1": a64SysReg{0x182160, true, true},
|
||||
"APIBKeyLo_EL1": a64SysReg{0x182140, true, true},
|
||||
"CCSIDR2_EL1": a64SysReg{0x190040, true, false},
|
||||
"CCSIDR_EL1": a64SysReg{0x190000, true, false},
|
||||
"CLIDR_EL1": a64SysReg{0x190020, true, false},
|
||||
"CNTFRQ_EL0": a64SysReg{0x1be000, true, true},
|
||||
"CNTKCTL_EL1": a64SysReg{0x18e100, true, true},
|
||||
"CNTP_CTL_EL0": a64SysReg{0x1be220, true, true},
|
||||
"CNTP_CVAL_EL0": a64SysReg{0x1be240, true, true},
|
||||
"CNTP_TVAL_EL0": a64SysReg{0x1be200, true, true},
|
||||
"CNTPCT_EL0": a64SysReg{0x1be020, true, false},
|
||||
"CNTPS_CTL_EL1": a64SysReg{0x1fe220, true, true},
|
||||
"CNTPS_CVAL_EL1": a64SysReg{0x1fe240, true, true},
|
||||
"CNTPS_TVAL_EL1": a64SysReg{0x1fe200, true, true},
|
||||
"CNTV_CTL_EL0": a64SysReg{0x1be320, true, true},
|
||||
"CNTV_CVAL_EL0": a64SysReg{0x1be340, true, true},
|
||||
"CNTV_TVAL_EL0": a64SysReg{0x1be300, true, true},
|
||||
"CNTVCT_EL0": a64SysReg{0x1be040, true, false},
|
||||
"CONTEXTIDR_EL1": a64SysReg{0x18d020, true, true},
|
||||
"CPACR_EL1": a64SysReg{0x181040, true, true},
|
||||
"CSSELR_EL1": a64SysReg{0x1a0000, true, true},
|
||||
"CTR_EL0": a64SysReg{0x1b0020, true, false},
|
||||
"CurrentEL": a64SysReg{0x184240, true, false},
|
||||
"DAIF": a64SysReg{0x1b4220, true, true},
|
||||
"DBGAUTHSTATUS_EL1": a64SysReg{0x107ec0, true, false},
|
||||
"DBGBCR0_EL1": a64SysReg{0x1000a0, true, true},
|
||||
"DBGBCR1_EL1": a64SysReg{0x1001a0, true, true},
|
||||
"DBGBCR2_EL1": a64SysReg{0x1002a0, true, true},
|
||||
"DBGBCR3_EL1": a64SysReg{0x1003a0, true, true},
|
||||
"DBGBCR4_EL1": a64SysReg{0x1004a0, true, true},
|
||||
"DBGBCR5_EL1": a64SysReg{0x1005a0, true, true},
|
||||
"DBGBCR6_EL1": a64SysReg{0x1006a0, true, true},
|
||||
"DBGBCR7_EL1": a64SysReg{0x1007a0, true, true},
|
||||
"DBGBCR8_EL1": a64SysReg{0x1008a0, true, true},
|
||||
"DBGBCR9_EL1": a64SysReg{0x1009a0, true, true},
|
||||
"DBGBCR10_EL1": a64SysReg{0x100aa0, true, true},
|
||||
"DBGBCR11_EL1": a64SysReg{0x100ba0, true, true},
|
||||
"DBGBCR12_EL1": a64SysReg{0x100ca0, true, true},
|
||||
"DBGBCR13_EL1": a64SysReg{0x100da0, true, true},
|
||||
"DBGBCR14_EL1": a64SysReg{0x100ea0, true, true},
|
||||
"DBGBCR15_EL1": a64SysReg{0x100fa0, true, true},
|
||||
"DBGBVR0_EL1": a64SysReg{0x100080, true, true},
|
||||
"DBGBVR1_EL1": a64SysReg{0x100180, true, true},
|
||||
"DBGBVR2_EL1": a64SysReg{0x100280, true, true},
|
||||
"DBGBVR3_EL1": a64SysReg{0x100380, true, true},
|
||||
"DBGBVR4_EL1": a64SysReg{0x100480, true, true},
|
||||
"DBGBVR5_EL1": a64SysReg{0x100580, true, true},
|
||||
"DBGBVR6_EL1": a64SysReg{0x100680, true, true},
|
||||
"DBGBVR7_EL1": a64SysReg{0x100780, true, true},
|
||||
"DBGBVR8_EL1": a64SysReg{0x100880, true, true},
|
||||
"DBGBVR9_EL1": a64SysReg{0x100980, true, true},
|
||||
"DBGBVR10_EL1": a64SysReg{0x100a80, true, true},
|
||||
"DBGBVR11_EL1": a64SysReg{0x100b80, true, true},
|
||||
"DBGBVR12_EL1": a64SysReg{0x100c80, true, true},
|
||||
"DBGBVR13_EL1": a64SysReg{0x100d80, true, true},
|
||||
"DBGBVR14_EL1": a64SysReg{0x100e80, true, true},
|
||||
"DBGBVR15_EL1": a64SysReg{0x100f80, true, true},
|
||||
"DBGCLAIMCLR_EL1": a64SysReg{0x1079c0, true, true},
|
||||
"DBGCLAIMSET_EL1": a64SysReg{0x1078c0, true, true},
|
||||
"DBGDTR_EL0": a64SysReg{0x130400, true, true},
|
||||
"DBGDTRRX_EL0": a64SysReg{0x130500, true, false},
|
||||
"DBGDTRTX_EL0": a64SysReg{0x130500, false, true},
|
||||
"DBGPRCR_EL1": a64SysReg{0x101480, true, true},
|
||||
"DBGWCR0_EL1": a64SysReg{0x1000e0, true, true},
|
||||
"DBGWCR1_EL1": a64SysReg{0x1001e0, true, true},
|
||||
"DBGWCR2_EL1": a64SysReg{0x1002e0, true, true},
|
||||
"DBGWCR3_EL1": a64SysReg{0x1003e0, true, true},
|
||||
"DBGWCR4_EL1": a64SysReg{0x1004e0, true, true},
|
||||
"DBGWCR5_EL1": a64SysReg{0x1005e0, true, true},
|
||||
"DBGWCR6_EL1": a64SysReg{0x1006e0, true, true},
|
||||
"DBGWCR7_EL1": a64SysReg{0x1007e0, true, true},
|
||||
"DBGWCR8_EL1": a64SysReg{0x1008e0, true, true},
|
||||
"DBGWCR9_EL1": a64SysReg{0x1009e0, true, true},
|
||||
"DBGWCR10_EL1": a64SysReg{0x100ae0, true, true},
|
||||
"DBGWCR11_EL1": a64SysReg{0x100be0, true, true},
|
||||
"DBGWCR12_EL1": a64SysReg{0x100ce0, true, true},
|
||||
"DBGWCR13_EL1": a64SysReg{0x100de0, true, true},
|
||||
"DBGWCR14_EL1": a64SysReg{0x100ee0, true, true},
|
||||
"DBGWCR15_EL1": a64SysReg{0x100fe0, true, true},
|
||||
"DBGWVR0_EL1": a64SysReg{0x1000c0, true, true},
|
||||
"DBGWVR1_EL1": a64SysReg{0x1001c0, true, true},
|
||||
"DBGWVR2_EL1": a64SysReg{0x1002c0, true, true},
|
||||
"DBGWVR3_EL1": a64SysReg{0x1003c0, true, true},
|
||||
"DBGWVR4_EL1": a64SysReg{0x1004c0, true, true},
|
||||
"DBGWVR5_EL1": a64SysReg{0x1005c0, true, true},
|
||||
"DBGWVR6_EL1": a64SysReg{0x1006c0, true, true},
|
||||
"DBGWVR7_EL1": a64SysReg{0x1007c0, true, true},
|
||||
"DBGWVR8_EL1": a64SysReg{0x1008c0, true, true},
|
||||
"DBGWVR9_EL1": a64SysReg{0x1009c0, true, true},
|
||||
"DBGWVR10_EL1": a64SysReg{0x100ac0, true, true},
|
||||
"DBGWVR11_EL1": a64SysReg{0x100bc0, true, true},
|
||||
"DBGWVR12_EL1": a64SysReg{0x100cc0, true, true},
|
||||
"DBGWVR13_EL1": a64SysReg{0x100dc0, true, true},
|
||||
"DBGWVR14_EL1": a64SysReg{0x100ec0, true, true},
|
||||
"DBGWVR15_EL1": a64SysReg{0x100fc0, true, true},
|
||||
"DCZID_EL0": a64SysReg{0x1b00e0, true, false},
|
||||
"DISR_EL1": a64SysReg{0x18c120, true, true},
|
||||
"DIT": a64SysReg{0x1b42a0, true, true},
|
||||
"DLR_EL0": a64SysReg{0x1b4520, true, true},
|
||||
"DSPSR_EL0": a64SysReg{0x1b4500, true, true},
|
||||
"ELR_EL1": a64SysReg{0x184020, true, true},
|
||||
"ERRIDR_EL1": a64SysReg{0x185300, true, false},
|
||||
"ERRSELR_EL1": a64SysReg{0x185320, true, true},
|
||||
"ERXADDR_EL1": a64SysReg{0x185460, true, true},
|
||||
"ERXCTLR_EL1": a64SysReg{0x185420, true, true},
|
||||
"ERXFR_EL1": a64SysReg{0x185400, true, false},
|
||||
"ERXMISC0_EL1": a64SysReg{0x185500, true, true},
|
||||
"ERXMISC1_EL1": a64SysReg{0x185520, true, true},
|
||||
"ERXMISC2_EL1": a64SysReg{0x185540, true, true},
|
||||
"ERXMISC3_EL1": a64SysReg{0x185560, true, true},
|
||||
"ERXPFGCDN_EL1": a64SysReg{0x1854c0, true, true},
|
||||
"ERXPFGCTL_EL1": a64SysReg{0x1854a0, true, true},
|
||||
"ERXPFGF_EL1": a64SysReg{0x185480, true, false},
|
||||
"ERXSTATUS_EL1": a64SysReg{0x185440, true, true},
|
||||
"ESR_EL1": a64SysReg{0x185200, true, true},
|
||||
"FAR_EL1": a64SysReg{0x186000, true, true},
|
||||
"FPCR": a64SysReg{0x1b4400, true, true},
|
||||
"FPSR": a64SysReg{0x1b4420, true, true},
|
||||
"GCR_EL1": a64SysReg{0x1810c0, true, true},
|
||||
"GMID_EL1": a64SysReg{0x31400, true, false},
|
||||
"ICC_AP0R0_EL1": a64SysReg{0x18c880, true, true},
|
||||
"ICC_AP0R1_EL1": a64SysReg{0x18c8a0, true, true},
|
||||
"ICC_AP0R2_EL1": a64SysReg{0x18c8c0, true, true},
|
||||
"ICC_AP0R3_EL1": a64SysReg{0x18c8e0, true, true},
|
||||
"ICC_AP1R0_EL1": a64SysReg{0x18c900, true, true},
|
||||
"ICC_AP1R1_EL1": a64SysReg{0x18c920, true, true},
|
||||
"ICC_AP1R2_EL1": a64SysReg{0x18c940, true, true},
|
||||
"ICC_AP1R3_EL1": a64SysReg{0x18c960, true, true},
|
||||
"ICC_ASGI1R_EL1": a64SysReg{0x18cbc0, false, true},
|
||||
"ICC_BPR0_EL1": a64SysReg{0x18c860, true, true},
|
||||
"ICC_BPR1_EL1": a64SysReg{0x18cc60, true, true},
|
||||
"ICC_CTLR_EL1": a64SysReg{0x18cc80, true, true},
|
||||
"ICC_DIR_EL1": a64SysReg{0x18cb20, false, true},
|
||||
"ICC_EOIR0_EL1": a64SysReg{0x18c820, false, true},
|
||||
"ICC_EOIR1_EL1": a64SysReg{0x18cc20, false, true},
|
||||
"ICC_HPPIR0_EL1": a64SysReg{0x18c840, true, false},
|
||||
"ICC_HPPIR1_EL1": a64SysReg{0x18cc40, true, false},
|
||||
"ICC_IAR0_EL1": a64SysReg{0x18c800, true, false},
|
||||
"ICC_IAR1_EL1": a64SysReg{0x18cc00, true, false},
|
||||
"ICC_IGRPEN0_EL1": a64SysReg{0x18ccc0, true, true},
|
||||
"ICC_IGRPEN1_EL1": a64SysReg{0x18cce0, true, true},
|
||||
"ICC_PMR_EL1": a64SysReg{0x184600, true, true},
|
||||
"ICC_RPR_EL1": a64SysReg{0x18cb60, true, false},
|
||||
"ICC_SGI0R_EL1": a64SysReg{0x18cbe0, false, true},
|
||||
"ICC_SGI1R_EL1": a64SysReg{0x18cba0, false, true},
|
||||
"ICC_SRE_EL1": a64SysReg{0x18cca0, true, true},
|
||||
"ICV_AP0R0_EL1": a64SysReg{0x18c880, true, true},
|
||||
"ICV_AP0R1_EL1": a64SysReg{0x18c8a0, true, true},
|
||||
"ICV_AP0R2_EL1": a64SysReg{0x18c8c0, true, true},
|
||||
"ICV_AP0R3_EL1": a64SysReg{0x18c8e0, true, true},
|
||||
"ICV_AP1R0_EL1": a64SysReg{0x18c900, true, true},
|
||||
"ICV_AP1R1_EL1": a64SysReg{0x18c920, true, true},
|
||||
"ICV_AP1R2_EL1": a64SysReg{0x18c940, true, true},
|
||||
"ICV_AP1R3_EL1": a64SysReg{0x18c960, true, true},
|
||||
"ICV_BPR0_EL1": a64SysReg{0x18c860, true, true},
|
||||
"ICV_BPR1_EL1": a64SysReg{0x18cc60, true, true},
|
||||
"ICV_CTLR_EL1": a64SysReg{0x18cc80, true, true},
|
||||
"ICV_DIR_EL1": a64SysReg{0x18cb20, false, true},
|
||||
"ICV_EOIR0_EL1": a64SysReg{0x18c820, false, true},
|
||||
"ICV_EOIR1_EL1": a64SysReg{0x18cc20, false, true},
|
||||
"ICV_HPPIR0_EL1": a64SysReg{0x18c840, true, false},
|
||||
"ICV_HPPIR1_EL1": a64SysReg{0x18cc40, true, false},
|
||||
"ICV_IAR0_EL1": a64SysReg{0x18c800, true, false},
|
||||
"ICV_IAR1_EL1": a64SysReg{0x18cc00, true, false},
|
||||
"ICV_IGRPEN0_EL1": a64SysReg{0x18ccc0, true, true},
|
||||
"ICV_IGRPEN1_EL1": a64SysReg{0x18cce0, true, true},
|
||||
"ICV_PMR_EL1": a64SysReg{0x184600, true, true},
|
||||
"ICV_RPR_EL1": a64SysReg{0x18cb60, true, false},
|
||||
"ID_AA64AFR0_EL1": a64SysReg{0x180580, true, false},
|
||||
"ID_AA64AFR1_EL1": a64SysReg{0x1805a0, true, false},
|
||||
"ID_AA64DFR0_EL1": a64SysReg{0x180500, true, false},
|
||||
"ID_AA64DFR1_EL1": a64SysReg{0x180520, true, false},
|
||||
"ID_AA64ISAR0_EL1": a64SysReg{0x180600, true, false},
|
||||
"ID_AA64ISAR1_EL1": a64SysReg{0x180620, true, false},
|
||||
"ID_AA64MMFR0_EL1": a64SysReg{0x180700, true, false},
|
||||
"ID_AA64MMFR1_EL1": a64SysReg{0x180720, true, false},
|
||||
"ID_AA64MMFR2_EL1": a64SysReg{0x180740, true, false},
|
||||
"ID_AA64PFR0_EL1": a64SysReg{0x180400, true, false},
|
||||
"ID_AA64PFR1_EL1": a64SysReg{0x180420, true, false},
|
||||
"ID_AA64ZFR0_EL1": a64SysReg{0x180480, true, false},
|
||||
"ID_AFR0_EL1": a64SysReg{0x180160, true, false},
|
||||
"ID_DFR0_EL1": a64SysReg{0x180140, true, false},
|
||||
"ID_ISAR0_EL1": a64SysReg{0x180200, true, false},
|
||||
"ID_ISAR1_EL1": a64SysReg{0x180220, true, false},
|
||||
"ID_ISAR2_EL1": a64SysReg{0x180240, true, false},
|
||||
"ID_ISAR3_EL1": a64SysReg{0x180260, true, false},
|
||||
"ID_ISAR4_EL1": a64SysReg{0x180280, true, false},
|
||||
"ID_ISAR5_EL1": a64SysReg{0x1802a0, true, false},
|
||||
"ID_ISAR6_EL1": a64SysReg{0x1802e0, true, false},
|
||||
"ID_MMFR0_EL1": a64SysReg{0x180180, true, false},
|
||||
"ID_MMFR1_EL1": a64SysReg{0x1801a0, true, false},
|
||||
"ID_MMFR2_EL1": a64SysReg{0x1801c0, true, false},
|
||||
"ID_MMFR3_EL1": a64SysReg{0x1801e0, true, false},
|
||||
"ID_MMFR4_EL1": a64SysReg{0x1802c0, true, false},
|
||||
"ID_PFR0_EL1": a64SysReg{0x180100, true, false},
|
||||
"ID_PFR1_EL1": a64SysReg{0x180120, true, false},
|
||||
"ID_PFR2_EL1": a64SysReg{0x180380, true, false},
|
||||
"ISR_EL1": a64SysReg{0x18c100, true, false},
|
||||
"LORC_EL1": a64SysReg{0x18a460, true, true},
|
||||
"LOREA_EL1": a64SysReg{0x18a420, true, true},
|
||||
"LORID_EL1": a64SysReg{0x18a4e0, true, false},
|
||||
"LORN_EL1": a64SysReg{0x18a440, true, true},
|
||||
"LORSA_EL1": a64SysReg{0x18a400, true, true},
|
||||
"MAIR_EL1": a64SysReg{0x18a200, true, true},
|
||||
"MDCCINT_EL1": a64SysReg{0x100200, true, true},
|
||||
"MDCCSR_EL0": a64SysReg{0x130100, true, false},
|
||||
"MDRAR_EL1": a64SysReg{0x101000, true, false},
|
||||
"MDSCR_EL1": a64SysReg{0x100240, true, true},
|
||||
"MIDR_EL1": a64SysReg{0x180000, true, false},
|
||||
"MPAM0_EL1": a64SysReg{0x18a520, true, true},
|
||||
"MPAM1_EL1": a64SysReg{0x18a500, true, true},
|
||||
"MPAMIDR_EL1": a64SysReg{0x18a480, true, false},
|
||||
"MPIDR_EL1": a64SysReg{0x1800a0, true, false},
|
||||
"MVFR0_EL1": a64SysReg{0x180300, true, false},
|
||||
"MVFR1_EL1": a64SysReg{0x180320, true, false},
|
||||
"MVFR2_EL1": a64SysReg{0x180340, true, false},
|
||||
"NZCV": a64SysReg{0x1b4200, true, true},
|
||||
"OSDLR_EL1": a64SysReg{0x101380, true, true},
|
||||
"OSDTRRX_EL1": a64SysReg{0x100040, true, true},
|
||||
"OSDTRTX_EL1": a64SysReg{0x100340, true, true},
|
||||
"OSECCR_EL1": a64SysReg{0x100640, true, true},
|
||||
"OSLAR_EL1": a64SysReg{0x101080, false, true},
|
||||
"OSLSR_EL1": a64SysReg{0x101180, true, false},
|
||||
"PAN": a64SysReg{0x184260, true, true},
|
||||
"PAR_EL1": a64SysReg{0x187400, true, true},
|
||||
"PMBIDR_EL1": a64SysReg{0x189ae0, true, false},
|
||||
"PMBLIMITR_EL1": a64SysReg{0x189a00, true, true},
|
||||
"PMBPTR_EL1": a64SysReg{0x189a20, true, true},
|
||||
"PMBSR_EL1": a64SysReg{0x189a60, true, true},
|
||||
"PMCCFILTR_EL0": a64SysReg{0x1befe0, true, true},
|
||||
"PMCCNTR_EL0": a64SysReg{0x1b9d00, true, true},
|
||||
"PMCEID0_EL0": a64SysReg{0x1b9cc0, true, false},
|
||||
"PMCEID1_EL0": a64SysReg{0x1b9ce0, true, false},
|
||||
"PMCNTENCLR_EL0": a64SysReg{0x1b9c40, true, true},
|
||||
"PMCNTENSET_EL0": a64SysReg{0x1b9c20, true, true},
|
||||
"PMCR_EL0": a64SysReg{0x1b9c00, true, true},
|
||||
"PMEVCNTR0_EL0": a64SysReg{0x1be800, true, true},
|
||||
"PMEVCNTR1_EL0": a64SysReg{0x1be820, true, true},
|
||||
"PMEVCNTR2_EL0": a64SysReg{0x1be840, true, true},
|
||||
"PMEVCNTR3_EL0": a64SysReg{0x1be860, true, true},
|
||||
"PMEVCNTR4_EL0": a64SysReg{0x1be880, true, true},
|
||||
"PMEVCNTR5_EL0": a64SysReg{0x1be8a0, true, true},
|
||||
"PMEVCNTR6_EL0": a64SysReg{0x1be8c0, true, true},
|
||||
"PMEVCNTR7_EL0": a64SysReg{0x1be8e0, true, true},
|
||||
"PMEVCNTR8_EL0": a64SysReg{0x1be900, true, true},
|
||||
"PMEVCNTR9_EL0": a64SysReg{0x1be920, true, true},
|
||||
"PMEVCNTR10_EL0": a64SysReg{0x1be940, true, true},
|
||||
"PMEVCNTR11_EL0": a64SysReg{0x1be960, true, true},
|
||||
"PMEVCNTR12_EL0": a64SysReg{0x1be980, true, true},
|
||||
"PMEVCNTR13_EL0": a64SysReg{0x1be9a0, true, true},
|
||||
"PMEVCNTR14_EL0": a64SysReg{0x1be9c0, true, true},
|
||||
"PMEVCNTR15_EL0": a64SysReg{0x1be9e0, true, true},
|
||||
"PMEVCNTR16_EL0": a64SysReg{0x1bea00, true, true},
|
||||
"PMEVCNTR17_EL0": a64SysReg{0x1bea20, true, true},
|
||||
"PMEVCNTR18_EL0": a64SysReg{0x1bea40, true, true},
|
||||
"PMEVCNTR19_EL0": a64SysReg{0x1bea60, true, true},
|
||||
"PMEVCNTR20_EL0": a64SysReg{0x1bea80, true, true},
|
||||
"PMEVCNTR21_EL0": a64SysReg{0x1beaa0, true, true},
|
||||
"PMEVCNTR22_EL0": a64SysReg{0x1beac0, true, true},
|
||||
"PMEVCNTR23_EL0": a64SysReg{0x1beae0, true, true},
|
||||
"PMEVCNTR24_EL0": a64SysReg{0x1beb00, true, true},
|
||||
"PMEVCNTR25_EL0": a64SysReg{0x1beb20, true, true},
|
||||
"PMEVCNTR26_EL0": a64SysReg{0x1beb40, true, true},
|
||||
"PMEVCNTR27_EL0": a64SysReg{0x1beb60, true, true},
|
||||
"PMEVCNTR28_EL0": a64SysReg{0x1beb80, true, true},
|
||||
"PMEVCNTR29_EL0": a64SysReg{0x1beba0, true, true},
|
||||
"PMEVCNTR30_EL0": a64SysReg{0x1bebc0, true, true},
|
||||
"PMEVTYPER0_EL0": a64SysReg{0x1bec00, true, true},
|
||||
"PMEVTYPER1_EL0": a64SysReg{0x1bec20, true, true},
|
||||
"PMEVTYPER2_EL0": a64SysReg{0x1bec40, true, true},
|
||||
"PMEVTYPER3_EL0": a64SysReg{0x1bec60, true, true},
|
||||
"PMEVTYPER4_EL0": a64SysReg{0x1bec80, true, true},
|
||||
"PMEVTYPER5_EL0": a64SysReg{0x1beca0, true, true},
|
||||
"PMEVTYPER6_EL0": a64SysReg{0x1becc0, true, true},
|
||||
"PMEVTYPER7_EL0": a64SysReg{0x1bece0, true, true},
|
||||
"PMEVTYPER8_EL0": a64SysReg{0x1bed00, true, true},
|
||||
"PMEVTYPER9_EL0": a64SysReg{0x1bed20, true, true},
|
||||
"PMEVTYPER10_EL0": a64SysReg{0x1bed40, true, true},
|
||||
"PMEVTYPER11_EL0": a64SysReg{0x1bed60, true, true},
|
||||
"PMEVTYPER12_EL0": a64SysReg{0x1bed80, true, true},
|
||||
"PMEVTYPER13_EL0": a64SysReg{0x1beda0, true, true},
|
||||
"PMEVTYPER14_EL0": a64SysReg{0x1bedc0, true, true},
|
||||
"PMEVTYPER15_EL0": a64SysReg{0x1bede0, true, true},
|
||||
"PMEVTYPER16_EL0": a64SysReg{0x1bee00, true, true},
|
||||
"PMEVTYPER17_EL0": a64SysReg{0x1bee20, true, true},
|
||||
"PMEVTYPER18_EL0": a64SysReg{0x1bee40, true, true},
|
||||
"PMEVTYPER19_EL0": a64SysReg{0x1bee60, true, true},
|
||||
"PMEVTYPER20_EL0": a64SysReg{0x1bee80, true, true},
|
||||
"PMEVTYPER21_EL0": a64SysReg{0x1beea0, true, true},
|
||||
"PMEVTYPER22_EL0": a64SysReg{0x1beec0, true, true},
|
||||
"PMEVTYPER23_EL0": a64SysReg{0x1beee0, true, true},
|
||||
"PMEVTYPER24_EL0": a64SysReg{0x1bef00, true, true},
|
||||
"PMEVTYPER25_EL0": a64SysReg{0x1bef20, true, true},
|
||||
"PMEVTYPER26_EL0": a64SysReg{0x1bef40, true, true},
|
||||
"PMEVTYPER27_EL0": a64SysReg{0x1bef60, true, true},
|
||||
"PMEVTYPER28_EL0": a64SysReg{0x1bef80, true, true},
|
||||
"PMEVTYPER29_EL0": a64SysReg{0x1befa0, true, true},
|
||||
"PMEVTYPER30_EL0": a64SysReg{0x1befc0, true, true},
|
||||
"PMINTENCLR_EL1": a64SysReg{0x189e40, true, true},
|
||||
"PMINTENSET_EL1": a64SysReg{0x189e20, true, true},
|
||||
"PMMIR_EL1": a64SysReg{0x189ec0, true, false},
|
||||
"PMOVSCLR_EL0": a64SysReg{0x1b9c60, true, true},
|
||||
"PMOVSSET_EL0": a64SysReg{0x1b9e60, true, true},
|
||||
"PMSCR_EL1": a64SysReg{0x189900, true, true},
|
||||
"PMSELR_EL0": a64SysReg{0x1b9ca0, true, true},
|
||||
"PMSEVFR_EL1": a64SysReg{0x1899a0, true, true},
|
||||
"PMSFCR_EL1": a64SysReg{0x189980, true, true},
|
||||
"PMSICR_EL1": a64SysReg{0x189940, true, true},
|
||||
"PMSIDR_EL1": a64SysReg{0x1899e0, true, false},
|
||||
"PMSIRR_EL1": a64SysReg{0x189960, true, true},
|
||||
"PMSLATFR_EL1": a64SysReg{0x1899c0, true, true},
|
||||
"PMSWINC_EL0": a64SysReg{0x1b9c80, false, true},
|
||||
"PMUSERENR_EL0": a64SysReg{0x1b9e00, true, true},
|
||||
"PMXEVCNTR_EL0": a64SysReg{0x1b9d40, true, true},
|
||||
"PMXEVTYPER_EL0": a64SysReg{0x1b9d20, true, true},
|
||||
"REVIDR_EL1": a64SysReg{0x1800c0, true, false},
|
||||
"RGSR_EL1": a64SysReg{0x1810a0, true, true},
|
||||
"RMR_EL1": a64SysReg{0x18c040, true, true},
|
||||
"RNDR": a64SysReg{0x1b2400, true, false},
|
||||
"RNDRRS": a64SysReg{0x1b2420, true, false},
|
||||
"RVBAR_EL1": a64SysReg{0x18c020, true, false},
|
||||
"SCTLR_EL1": a64SysReg{0x181000, true, true},
|
||||
"SCXTNUM_EL0": a64SysReg{0x1bd0e0, true, true},
|
||||
"SCXTNUM_EL1": a64SysReg{0x18d0e0, true, true},
|
||||
"SP_EL0": a64SysReg{0x184100, true, true},
|
||||
"SP_EL1": a64SysReg{0x1c4100, true, true},
|
||||
"SPSel": a64SysReg{0x184200, true, true},
|
||||
"SPSR_abt": a64SysReg{0x1c4320, true, true},
|
||||
"SPSR_EL1": a64SysReg{0x184000, true, true},
|
||||
"SPSR_fiq": a64SysReg{0x1c4360, true, true},
|
||||
"SPSR_irq": a64SysReg{0x1c4300, true, true},
|
||||
"SPSR_und": a64SysReg{0x1c4340, true, true},
|
||||
"SSBS": a64SysReg{0x1b42c0, true, true},
|
||||
"TCO": a64SysReg{0x1b42e0, true, true},
|
||||
"TCR_EL1": a64SysReg{0x182040, true, true},
|
||||
"TFSR_EL1": a64SysReg{0x185600, true, true},
|
||||
"TFSRE0_EL1": a64SysReg{0x185620, true, true},
|
||||
"TPIDR_EL0": a64SysReg{0x1bd040, true, true},
|
||||
"TPIDR_EL1": a64SysReg{0x18d080, true, true},
|
||||
"TPIDRRO_EL0": a64SysReg{0x1bd060, true, true},
|
||||
"TRFCR_EL1": a64SysReg{0x181220, true, true},
|
||||
"TTBR0_EL1": a64SysReg{0x182000, true, true},
|
||||
"TTBR1_EL1": a64SysReg{0x182020, true, true},
|
||||
"UAO": a64SysReg{0x184280, true, true},
|
||||
"VBAR_EL1": a64SysReg{0x18c000, true, true},
|
||||
"ZCR_EL1": a64SysReg{0x181200, true, true},
|
||||
}
|
||||
|
||||
// a64SysInst is one TLBI alias: the fields the SYS encoding carries beside
|
||||
// the fixed op0 = 01 and CRn = 8.
|
||||
type a64SysInst struct {
|
||||
op1, cm, op2 uint32
|
||||
}
|
||||
|
||||
// a64TLBIOps maps the TLBI operation names to their fields; the register
|
||||
// operand is optional and defaults to ZR.
|
||||
var a64TLBIOps = map[string]a64SysInst{
|
||||
"ALLE1": {0x4, 0x7, 0x4},
|
||||
"ALLE1IS": {0x4, 0x3, 0x4},
|
||||
"ALLE1OS": {0x4, 0x1, 0x4},
|
||||
"ALLE2": {0x4, 0x7, 0x0},
|
||||
"ALLE2IS": {0x4, 0x3, 0x0},
|
||||
"ALLE2OS": {0x4, 0x1, 0x0},
|
||||
"ALLE3": {0x6, 0x7, 0x0},
|
||||
"ALLE3IS": {0x6, 0x3, 0x0},
|
||||
"ALLE3OS": {0x6, 0x1, 0x0},
|
||||
"ASIDE1": {0x0, 0x7, 0x2},
|
||||
"ASIDE1IS": {0x0, 0x3, 0x2},
|
||||
"ASIDE1OS": {0x0, 0x1, 0x2},
|
||||
"IPAS2E1": {0x4, 0x4, 0x1},
|
||||
"IPAS2E1IS": {0x4, 0x0, 0x1},
|
||||
"IPAS2E1OS": {0x4, 0x4, 0x0},
|
||||
"IPAS2LE1": {0x4, 0x4, 0x5},
|
||||
"IPAS2LE1IS": {0x4, 0x0, 0x5},
|
||||
"IPAS2LE1OS": {0x4, 0x4, 0x4},
|
||||
"RIPAS2E1": {0x4, 0x4, 0x2},
|
||||
"RIPAS2E1IS": {0x4, 0x0, 0x2},
|
||||
"RIPAS2E1OS": {0x4, 0x4, 0x3},
|
||||
"RIPAS2LE1": {0x4, 0x4, 0x6},
|
||||
"RIPAS2LE1IS": {0x4, 0x0, 0x6},
|
||||
"RIPAS2LE1OS": {0x4, 0x4, 0x7},
|
||||
"RVAAE1": {0x0, 0x6, 0x3},
|
||||
"RVAAE1IS": {0x0, 0x2, 0x3},
|
||||
"RVAAE1OS": {0x0, 0x5, 0x3},
|
||||
"RVAALE1": {0x0, 0x6, 0x7},
|
||||
"RVAALE1IS": {0x0, 0x2, 0x7},
|
||||
"RVAALE1OS": {0x0, 0x5, 0x7},
|
||||
"RVAE1": {0x0, 0x6, 0x1},
|
||||
"RVAE1IS": {0x0, 0x2, 0x1},
|
||||
"RVAE1OS": {0x0, 0x5, 0x1},
|
||||
"RVAE2": {0x4, 0x6, 0x1},
|
||||
"RVAE2IS": {0x4, 0x2, 0x1},
|
||||
"RVAE2OS": {0x4, 0x5, 0x1},
|
||||
"RVAE3": {0x6, 0x6, 0x1},
|
||||
"RVAE3IS": {0x6, 0x2, 0x1},
|
||||
"RVAE3OS": {0x6, 0x5, 0x1},
|
||||
"RVALE1": {0x0, 0x6, 0x5},
|
||||
"RVALE1IS": {0x0, 0x2, 0x5},
|
||||
"RVALE1OS": {0x0, 0x5, 0x5},
|
||||
"RVALE2": {0x4, 0x6, 0x5},
|
||||
"RVALE2IS": {0x4, 0x2, 0x5},
|
||||
"RVALE2OS": {0x4, 0x5, 0x5},
|
||||
"RVALE3": {0x6, 0x6, 0x5},
|
||||
"RVALE3IS": {0x6, 0x2, 0x5},
|
||||
"RVALE3OS": {0x6, 0x5, 0x5},
|
||||
"VAAE1": {0x0, 0x7, 0x3},
|
||||
"VAAE1IS": {0x0, 0x3, 0x3},
|
||||
"VAAE1OS": {0x0, 0x1, 0x3},
|
||||
"VAALE1": {0x0, 0x7, 0x7},
|
||||
"VAALE1IS": {0x0, 0x3, 0x7},
|
||||
"VAALE1OS": {0x0, 0x1, 0x7},
|
||||
"VAE1": {0x0, 0x7, 0x1},
|
||||
"VAE1IS": {0x0, 0x3, 0x1},
|
||||
"VAE1OS": {0x0, 0x1, 0x1},
|
||||
"VAE2": {0x4, 0x7, 0x1},
|
||||
"VAE2IS": {0x4, 0x3, 0x1},
|
||||
"VAE2OS": {0x4, 0x1, 0x1},
|
||||
"VAE3": {0x6, 0x7, 0x1},
|
||||
"VAE3IS": {0x6, 0x3, 0x1},
|
||||
"VAE3OS": {0x6, 0x1, 0x1},
|
||||
"VALE1": {0x0, 0x7, 0x5},
|
||||
"VALE1IS": {0x0, 0x3, 0x5},
|
||||
"VALE1OS": {0x0, 0x1, 0x5},
|
||||
"VALE2": {0x4, 0x7, 0x5},
|
||||
"VALE2IS": {0x4, 0x3, 0x5},
|
||||
"VALE2OS": {0x4, 0x1, 0x5},
|
||||
"VALE3": {0x6, 0x7, 0x5},
|
||||
"VALE3IS": {0x6, 0x3, 0x5},
|
||||
"VALE3OS": {0x6, 0x1, 0x5},
|
||||
"VMALLE1": {0x0, 0x7, 0x0},
|
||||
"VMALLE1IS": {0x0, 0x3, 0x0},
|
||||
"VMALLE1OS": {0x0, 0x1, 0x0},
|
||||
"VMALLS12E1": {0x4, 0x7, 0x6},
|
||||
"VMALLS12E1IS": {0x4, 0x3, 0x6},
|
||||
"VMALLS12E1OS": {0x4, 0x1, 0x6},
|
||||
}
|
||||
|
||||
// arm64TLBITakesReg reports whether a TLBI operation spells a by-address
|
||||
// invalidation that carries the address in its optional second register
|
||||
// (asm7.go's sysInstFields hasOperand2). The whole-entry spellings are
|
||||
// exactly the VMALL-prefixed and the ALL-prefixed ones; everything else
|
||||
// (VAE*, VAAE*, VALE*, RVAA*, RVAE*, ASIDE1*, IPAS2*, RIPAS2*) is
|
||||
// by-address and takes the register.
|
||||
func arm64TLBITakesReg(name string) bool {
|
||||
return !strings.HasPrefix(name, "VMALL") && !strings.HasPrefix(name, "ALL")
|
||||
}
|
||||
|
||||
// a64DCOps2 maps the DC operation names to their fields; the register
|
||||
// operand is mandatory.
|
||||
var a64DCOps2 = map[string]a64SysInst{
|
||||
"CGDSW": {0x0, 0xa, 0x6},
|
||||
"CGDVAC": {0x3, 0xa, 0x5},
|
||||
"CGDVADP": {0x3, 0xd, 0x5},
|
||||
"CGDVAP": {0x3, 0xc, 0x5},
|
||||
"CGSW": {0x0, 0xa, 0x4},
|
||||
"CGVAC": {0x3, 0xa, 0x3},
|
||||
"CGVADP": {0x3, 0xd, 0x3},
|
||||
"CGVAP": {0x3, 0xc, 0x3},
|
||||
"CIGDSW": {0x0, 0xe, 0x6},
|
||||
"CIGDVAC": {0x3, 0xe, 0x5},
|
||||
"CIGSW": {0x0, 0xe, 0x4},
|
||||
"CIGVAC": {0x3, 0xe, 0x3},
|
||||
"CISW": {0x0, 0xe, 0x2},
|
||||
"CIVAC": {0x3, 0xe, 0x1},
|
||||
"CSW": {0x0, 0xa, 0x2},
|
||||
"CVAC": {0x3, 0xa, 0x1},
|
||||
"CVADP": {0x3, 0xd, 0x1},
|
||||
"CVAP": {0x3, 0xc, 0x1},
|
||||
"CVAU": {0x3, 0xb, 0x1},
|
||||
"GVA": {0x3, 0x4, 0x3},
|
||||
"GZVA": {0x3, 0x4, 0x4},
|
||||
"IGDSW": {0x0, 0x6, 0x6},
|
||||
"IGDVAC": {0x0, 0x6, 0x5},
|
||||
"IGSW": {0x0, 0x6, 0x4},
|
||||
"IGVAC": {0x0, 0x6, 0x3},
|
||||
"ISW": {0x0, 0x6, 0x2},
|
||||
"IVAC": {0x0, 0x6, 0x1},
|
||||
"ZVA": {0x3, 0x4, 0x1},
|
||||
}
|
||||
|
||||
// a64RPRFOps maps the range-prefetch operation names to their 6-bit values.
|
||||
var a64RPRFOps = map[string]uint32{
|
||||
"PLDKEEP": 0,
|
||||
"PLDSTRM": 4,
|
||||
"PSTKEEP": 1,
|
||||
"PSTSTRM": 5,
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestARM64SysRegsDifferential proves the whole system-register table against
|
||||
// the toolchain at once: one TEXT whose body reads every register the table
|
||||
// carries (and writes every writable one), assembled by gasm and by
|
||||
// go tool asm, must agree byte for byte. A single wrong op0/op1/CRn/CRm/op2
|
||||
// packing names its register through the first differing word.
|
||||
func TestARM64SysRegsDifferential(t *testing.T) {
|
||||
names := make([]string, 0, len(a64SysRegs))
|
||||
for name := range a64SysRegs {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
var body strings.Builder
|
||||
for i, name := range names {
|
||||
// R18 is the arm64 platform register and R29-R31 carry dedicated
|
||||
// meanings; a plain read/write destination keeps to R0-R17.
|
||||
reg := fmt.Sprintf("R%d", i%18)
|
||||
if a64SysRegs[name].read {
|
||||
body.WriteString(fmt.Sprintf("\tMRS %s, %s\n", name, reg))
|
||||
}
|
||||
if a64SysRegs[name].write {
|
||||
body.WriteString(fmt.Sprintf("\tMSR %s, %s\n", reg, name))
|
||||
}
|
||||
}
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·sysregs(SB), NOSPLIT, $0\n" + body.String() + "\tRET\n"
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "sysregs_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertARM64Differential(t, path, src, "sysregs")
|
||||
}
|
||||
|
||||
// TestARM64FamiliesDifferential pins the non-sysreg families the arm64
|
||||
// campaign added: the LSE compare-and-swap pairs, the VMOVI immediate, the
|
||||
// SIMD narrow/long shift pairs, the VLD2/VLD3/VLD4 and VST2/VST3/VST4
|
||||
// structure accesses with their post-index and replicate forms, LDPSW, the
|
||||
// pointer-authentication hint and the DC maintenance operation. Every
|
||||
// spelling is the toolchain's own, taken from its arm64 testdata, and the
|
||||
// bytes must agree word for word.
|
||||
func TestARM64FamiliesDifferential(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·families(SB), NOSPLIT, $0
|
||||
CASPD (R2, R3), (R2), (R8, R9)
|
||||
CASPW (R6, R7), (R8), (R4, R5)
|
||||
VMOVI $82, V0.B16
|
||||
VMOVI $146, V22.B16
|
||||
VSSHLL $0, V1.B8, V2.H8
|
||||
VSSHLL $7, V1.B8, V2.H8
|
||||
VSSHLL2 $0, V1.B16, V2.H8
|
||||
VSHRN $7, V1.H8, V0.B8
|
||||
VSHRN2 $31, V1.D2, V0.S4
|
||||
VLD2 (R29), [V23.H8, V24.H8]
|
||||
VLD2.P 16(R0), [V18.B8, V19.B8]
|
||||
VLD2.P (R1)(R2), [V15.S2, V16.S2]
|
||||
VLD3 (R27), [V11.S4, V12.S4, V13.S4]
|
||||
VLD3.P 48(RSP), [V11.S4, V12.S4, V13.S4]
|
||||
VLD4 (R15), [V10.H4, V11.H4, V12.H4, V13.H4]
|
||||
VLD4.P 32(R24), [V31.B8, V0.B8, V1.B8, V2.B8]
|
||||
VLD1R (R1), [V9.B8]
|
||||
VLD1R.P (R0), [V0.B16]
|
||||
VLD1R.P 2(R1), [V2.H4]
|
||||
VLD2R (R15), [V15.H4, V16.H4]
|
||||
VLD2R.P 16(R0), [V0.D2, V1.D2]
|
||||
VLD4R (R0), [V0.B8, V1.B8, V2.B8, V3.B8]
|
||||
VLD4R.P 16(RSP), [V31.S4, V0.S4, V1.S4, V2.S4]
|
||||
VST2 [V22.H8, V23.H8], (R23)
|
||||
VST2.P [V14.H4, V15.H4], 16(R17)
|
||||
VST2.P [V14.H4, V15.H4], (R3)(R17)
|
||||
VST3 [V1.D2, V2.D2, V3.D2], (R11)
|
||||
VST3.P [V18.S4, V19.S4, V20.S4], 48(R25)
|
||||
VST4 [V22.D2, V23.D2, V24.D2, V25.D2], (R3)
|
||||
VST4.P [V14.D2, V15.D2, V16.D2, V17.D2], 64(R15)
|
||||
LDPSW (R0), (R1, R2)
|
||||
LDPSW 4(R0), (R1, R2)
|
||||
LDPSW -4(R0), (R1, R2)
|
||||
PACIASP
|
||||
DC IVAC, R1
|
||||
RET
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "families_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertARM64Differential(t, path, src, "families")
|
||||
}
|
||||
|
||||
// assertARM64Differential assembles the same source with gasm and with the
|
||||
// toolchain for arm64 and requires the named function's code bytes to agree.
|
||||
// The live oracle is a deliberate-run comparison, so -short skips it (the
|
||||
// push pipeline's mode); the golden bytes of the individual encoders are
|
||||
// pinned separately in every mode.
|
||||
func assertARM64Differential(t *testing.T, path, src, fn string) {
|
||||
t.Helper()
|
||||
oracle := oracleFuncCode(t, toolAsmObject(t, path, "arm64"))
|
||||
|
||||
// The oracle keys its functions by the qualified object name
|
||||
// (pkg.name); match on the local part.
|
||||
want := map[string][]byte{}
|
||||
for name, code := range oracle {
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
want[after] = code
|
||||
} else {
|
||||
want[name] = code
|
||||
}
|
||||
}
|
||||
if want[fn] == nil {
|
||||
t.Fatalf("the oracle object carries no function %q (has %v)", fn, keysOf(want))
|
||||
}
|
||||
|
||||
f, perrs := parser.Parse(path, src)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs[0])
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
got := trimTrailingZeroWords(img.Code)
|
||||
wantB := trimTrailingZeroWords(want[fn])
|
||||
if len(got) != len(wantB) {
|
||||
t.Fatalf("gasm %d bytes, oracle %d bytes", len(got), len(wantB))
|
||||
}
|
||||
for i := range wantB {
|
||||
if got[i] != wantB[i] {
|
||||
t.Fatalf("word %d differs: gasm %08x, oracle %08x", i/4,
|
||||
binary.LittleEndian.Uint32(got[i:i+4]), binary.LittleEndian.Uint32(wantB[i:i+4]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trimTrailingZeroWords drops whole zero words off the end of a code span:
|
||||
// an object pads a function to its alignment, and the raw image does not.
|
||||
// A difference in the middle survives the trim untouched.
|
||||
func trimTrailingZeroWords(b []byte) []byte {
|
||||
for len(b) >= 4 {
|
||||
last := b[len(b)-4:]
|
||||
if last[0]|last[1]|last[2]|last[3] != 0 {
|
||||
break
|
||||
}
|
||||
b = b[:len(b)-4]
|
||||
}
|
||||
return b
|
||||
}
|
||||
+1154
-58
File diff suppressed because it is too large.
Load diff
+298
-4
@@ -4,13 +4,14 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// firstText parses src and returns its first TEXT function.
|
||||
@@ -159,6 +160,57 @@ TEXT ·loadarg(SB), NOSPLIT, $0-24
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFramelessCall verifies the forced base-pointer frame a $0-frame
|
||||
// function containing a CALL receives: the PUSHQ BP prologue with no stack
|
||||
// adjustment and the x+N(FP) → (N+16)(SP) translation, against the bytes the
|
||||
// Go assembler produces. The push is the frame, so the offset must not count
|
||||
// it twice.
|
||||
func TestAssembleFramelessCall(t *testing.T) {
|
||||
f, errs := parser.Parse("frameless_call_amd64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·withcall(SB), NOSPLIT, $0-16
|
||||
MOVQ x+0(FP), AX
|
||||
CALL ·other(SB)
|
||||
MOVQ AX, ret+8(FP)
|
||||
RET
|
||||
TEXT ·other(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
code := append([]byte(nil), img.Code[img.Funcs[0].Offset:img.Funcs[0].Offset+img.Funcs[0].Size]...)
|
||||
for _, r := range img.Funcs[0].Relocs {
|
||||
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||
code[j] = 0
|
||||
}
|
||||
}
|
||||
// From `go tool objdump` of the Go-assembled function:
|
||||
// PUSHQ BP 55
|
||||
// MOVQ SP, BP 4889e5
|
||||
// MOVQ 0x10(SP), AX 488b442410
|
||||
// CALL other e800000000
|
||||
// MOVQ AX, 0x18(SP) 4889442418
|
||||
// POPQ BP 5d
|
||||
// RET c3
|
||||
want := []byte{
|
||||
0x55,
|
||||
0x48, 0x89, 0xe5,
|
||||
0x48, 0x8b, 0x44, 0x24, 0x10,
|
||||
0xe8, 0x00, 0x00, 0x00, 0x00,
|
||||
0x48, 0x89, 0x44, 0x24, 0x18,
|
||||
0x5d,
|
||||
0xc3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("frameless CALL FP translation mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFrame verifies a function with a non-zero frame: the Go-style
|
||||
// prologue/epilogue and the x+N(FP) → (N+frame+16)(SP) translation, against
|
||||
// the bytes the Go assembler produces.
|
||||
@@ -201,8 +253,8 @@ TEXT ·withframe(SB), NOSPLIT, $16-16
|
||||
}
|
||||
|
||||
// TestAssembleVexKernel assembles the horizontal-sum reduction the go-flac
|
||||
// kernels end with — exercising the VEX moves, shuffle and extract forms
|
||||
// through the full parser → encoder path — and checks the output is
|
||||
// kernels end with; exercising the VEX moves, shuffle and extract forms
|
||||
// through the full parser → encoder path; and checks the output is
|
||||
// byte-identical to the Go assembler's.
|
||||
func TestAssembleVexKernel(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
@@ -318,6 +370,58 @@ end:
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleNumericPCJumps pins the numeric ±N(PC) branch operands: N
|
||||
// counts instruction statements, skipping labels, in both directions (the
|
||||
// runtime's exit loops write JMP -3(PC)), N = 0 parks on the jump itself.
|
||||
func TestAssembleNumericPCJumps(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·exit(SB), NOSPLIT, $0
|
||||
MOVB $1, AL
|
||||
lab:
|
||||
MOVB $2, AL
|
||||
MOVB $3, AL
|
||||
JMP -3(PC)
|
||||
MOVB $4, AL
|
||||
park:
|
||||
JMP 0(PC)
|
||||
MOVB $5, AL
|
||||
JMP 2(PC)
|
||||
MOVB $6, AL
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
// From the Go-assembled function:
|
||||
// MOVB $1, AL b001
|
||||
// MOVB $2, AL b002
|
||||
// MOVB $3, AL b003
|
||||
// JMP -3(PC) ebf8 (three instructions back, past lab:)
|
||||
// MOVB $4, AL b004
|
||||
// JMP 0(PC) ebfe (the park loop)
|
||||
// MOVB $5, AL b005
|
||||
// JMP 2(PC) eb02 (over MOVB $6 to the RET)
|
||||
// MOVB $6, AL b006
|
||||
// RET c3
|
||||
want := []byte{
|
||||
0xb0, 0x01,
|
||||
0xb0, 0x02,
|
||||
0xb0, 0x03,
|
||||
0xeb, 0xf8,
|
||||
0xb0, 0x04,
|
||||
0xeb, 0xfe,
|
||||
0xb0, 0x05,
|
||||
0xeb, 0x02,
|
||||
0xb0, 0x06,
|
||||
0xc3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("numeric-PC mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssemblePrefetch(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
@@ -351,3 +455,193 @@ TEXT ·pf(SB), NOSPLIT, $0
|
||||
t.Errorf("PREFETCHT0 bytes: got %s, want 0f 18 0b", hex)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleBareJump checks that a zero-operand jump (which parses, because
|
||||
// the parser does not arity-check mnemonics) is rejected with an error rather
|
||||
// than panicking in the layout loop, which indexes Operands[0] before the
|
||||
// emission pass gets a chance to diagnose the arity.
|
||||
func TestAssembleBareJump(t *testing.T) {
|
||||
for _, mnem := range []string{"JE", "JMP", "JLT", "CALL"} {
|
||||
fn := firstText(t, "TEXT ·bare(SB), $16-0\n\t"+mnem+"\n")
|
||||
if _, _, err := Assemble(fn); err == nil {
|
||||
t.Errorf("%s with no operand: expected an error, got none", mnem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssemblePseudoStatements runs LOCK/REP, BYTE/WORD and END through the
|
||||
// full statement pipeline, pinned against go tool asm (Go 1.27, amd64). It
|
||||
// asserts the three behaviours the toolchain shows: each prefix statement is
|
||||
// a standalone byte with a PC of its own (so a label placed on the LOCK
|
||||
// points at the F0), the data pseudo-ops write their literal bytes inline,
|
||||
// and END terminates nothing (the statements after it still belong to the
|
||||
// function and carry no trace of it).
|
||||
func TestAssemblePseudoStatements(t *testing.T) {
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·pseudo(SB), NOSPLIT, $0-0
|
||||
pfx:
|
||||
LOCK
|
||||
CMPXCHGQ AX, (BX)
|
||||
REP
|
||||
MOVSQ
|
||||
BYTE $0x0f
|
||||
BYTE $0x1f
|
||||
WORD $0x1234
|
||||
END
|
||||
BYTE $0x02
|
||||
RET
|
||||
`)
|
||||
code, labels, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
// go tool asm: f0 480fb103 f3 48a5 0f 1f 3412 02 c3
|
||||
want := []byte{
|
||||
0xf0,
|
||||
0x48, 0x0f, 0xb1, 0x03,
|
||||
0xf3, 0x48, 0xa5,
|
||||
0x0f, 0x1f, 0x34, 0x12,
|
||||
0x02, 0xc3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("pseudo statements:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
// The label sits on the LOCK byte, exactly where the toolchain's PC
|
||||
// listing puts it.
|
||||
if off := labels["pfx"]; off != 0 {
|
||||
t.Errorf("label pfx = %d, want 0 (the LOCK's own byte)", off)
|
||||
}
|
||||
// The trailing BYTE lands where the layout says: after the 8 bytes of
|
||||
// LOCK, CMPXCHGQ, REP and MOVSQ plus the 4 data bytes, END contributing
|
||||
// none.
|
||||
if code[12] != 0x02 {
|
||||
t.Errorf("byte at 12 = %02x, want 02 (the BYTE after END)", code[12])
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleAdjspBalance pins the toolchain's push/pop balance rule over
|
||||
// ADJSP: the straight-line sum of the adjustments must be zero at each
|
||||
// RET, branches in between counting for nothing (verified against go tool
|
||||
// asm: ADJSP $16 before a RET is reported as "unbalanced PUSH/POP", a
|
||||
// $16/$-16 pair with a JMP in between assembles).
|
||||
func TestAssembleAdjspBalance(t *testing.T) {
|
||||
// Balanced pair with a branch in between, bytes pinned from go tool asm.
|
||||
fn := firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·adjsp(SB), NOSPLIT, $0-0
|
||||
ADJSP $16
|
||||
JMP body
|
||||
body:
|
||||
ADJSP $-16
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
want := []byte{0x48, 0x83, 0xEC, 0x10, 0xEB, 0x00, 0x48, 0x83, 0xC4, 0x10, 0xC3}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("adjsp pair:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
|
||||
// Unbalanced at the RET: the toolchain diagnoses, so must we.
|
||||
_, _, err = Assemble(firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·unbalanced(SB), NOSPLIT, $0-0
|
||||
ADJSP $16
|
||||
RET
|
||||
`))
|
||||
if err == nil || !strings.Contains(err.Error(), "unbalanced PUSH/POP") {
|
||||
t.Errorf("unbalanced ADJSP: err = %v, want unbalanced PUSH/POP", err)
|
||||
}
|
||||
|
||||
// The check runs per RET: a closed pair before the first RET does not
|
||||
// excuse an open adjustment before the second.
|
||||
_, _, err = Assemble(firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·tworet(SB), NOSPLIT, $0-0
|
||||
ADJSP $8
|
||||
ADJSP $-8
|
||||
RET
|
||||
mid:
|
||||
ADJSP $8
|
||||
RET
|
||||
`))
|
||||
if err == nil || !strings.Contains(err.Error(), "unbalanced PUSH/POP") {
|
||||
t.Errorf("second RET with open ADJSP: err = %v, want unbalanced PUSH/POP", err)
|
||||
}
|
||||
|
||||
// A framed function: the assembler's own prologue and epilogue
|
||||
// contribute matching deltas, so the pair in the body still balances,
|
||||
// and the bytes match go tool asm end to end.
|
||||
fn = firstText(t, `
|
||||
#include "textflag.h"
|
||||
TEXT ·framed(SB), $16-8
|
||||
ADJSP $8
|
||||
ADJSP $-8
|
||||
RET
|
||||
`)
|
||||
code, _, err = Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble framed: %v", err)
|
||||
}
|
||||
want = []byte{
|
||||
0x55, 0x48, 0x89, 0xE5, 0x48, 0x83, 0xEC, 0x10, // prologue
|
||||
0x48, 0x83, 0xEC, 0x08, // ADJSP $8
|
||||
0x48, 0x83, 0xC4, 0x08, // ADJSP $-8
|
||||
0x48, 0x83, 0xC4, 0x10, 0x5D, // epilogue
|
||||
0xC3,
|
||||
}
|
||||
if hexBytes(code) != hexBytes(want) {
|
||||
t.Errorf("framed adjsp:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleRegRange pins the bracketed register range at the statement
|
||||
// level: exactly four consecutive same-width vector registers assemble, the
|
||||
// toolchain's rejected shapes all report an error.
|
||||
func TestAssembleRegRange(t *testing.T) {
|
||||
asm := func(t *testing.T, op string) ([]byte, error) {
|
||||
t.Helper()
|
||||
f, errs := parser.Parse("f_amd64.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n\tV4FMADDPS 17(SP), "+op+", K2, Z0\n\tRET\n")
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse %s: %v", op, errs)
|
||||
}
|
||||
code, _, err := Assemble(f.Decls[0].(*ast.Text))
|
||||
return code, err
|
||||
}
|
||||
for _, op := range []string{"[Z0-Z3]", "[Z4-Z7]", "[Z28-Z31]"} {
|
||||
if _, err := asm(t, op); err != nil {
|
||||
t.Errorf("%s: %v", op, err)
|
||||
}
|
||||
}
|
||||
for _, op := range []string{"[Z0-Z4]", "[Z0-Z2]", "[Z0-Z0]", "[Z4-Z0]", "[Z1-Z0]", "[AX-Z3]", "[Z0-AX]"} {
|
||||
if _, err := asm(t, op); err == nil {
|
||||
t.Errorf("%s: assembled, want an error", op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// fuzzIncludeDirs points the expansion path at the package's testdata include
|
||||
// directory, so a seed's #include resolves the way the CLI's -I list does.
|
||||
var fuzzIncludeDirs = []string{filepath.Join("testdata", "include")}
|
||||
|
||||
// corpusSeeds seeds every fuzz target with the repository's kernels, so a
|
||||
// plain `go test` run replays each seed as a regression case and CI exercises
|
||||
// them without any fuzzing budget. Kernels of a foreign architecture
|
||||
// exercise the rejection path (the fixed target reports them as diagnostics);
|
||||
// kernels of the target's own architecture reach its encoder.
|
||||
func corpusSeeds(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// archCorpusSeeds seeds a target with every assembly file of its architecture
|
||||
// seed directory, testdata/seeds/<dir>. The files hold the target-specific
|
||||
// corpus: the instruction families GOROOT's own assembler corpus exercises for
|
||||
// the architecture, minimalised, plus the boundary shapes the encoder's range
|
||||
// gates live on. They are committed assembly, so `gasm fmt` and `gasm lint`
|
||||
// gate them the way they gate every other .s file. A plain `go test` run
|
||||
// replays each one as a regression case.
|
||||
func archCorpusSeeds(f *testing.F, dir string) {
|
||||
files, _ := filepath.Glob(filepath.Join("testdata", "seeds", dir, "*.s"))
|
||||
for _, path := range files {
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
f.Add(string(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fuzzAssemble is the whole fuzz body, shared by every target and one line
|
||||
// apart between them: parse with macro and include expansion, assemble
|
||||
// through the target's file-level entry, and hold the invariants. The
|
||||
// contract:
|
||||
//
|
||||
// - no panic, however malformed the source (a crash fails the target);
|
||||
// - a rejected file yields a diagnostic and never a partial emission:
|
||||
// the assembler returns a nil image beside its error, and the
|
||||
// diagnostic is not empty;
|
||||
// - the output is deterministic: the same source, parsed and assembled
|
||||
// again from scratch, produces the same bytes;
|
||||
// - no unbounded memory: the fence around every test run kills a run that
|
||||
// amplifies its input, and the timebox turns a hang into a campaign
|
||||
// failure to bisect.
|
||||
//
|
||||
// A file the parser rejects still reaches the assembler: the parser is
|
||||
// line-oriented and tolerant, so it hands back a usable file either way, and
|
||||
// the assembler's own contract is to answer any file it is given with bytes
|
||||
// or with a diagnostic, never with a panic.
|
||||
func fuzzAssemble(t *testing.T, name string, src string, assemble func(*ast.File) (*Image, error)) {
|
||||
file, _ := parser.ParseWithOptions(name, src,
|
||||
parser.Options{Expand: true, IncludeDirs: fuzzIncludeDirs})
|
||||
if file == nil {
|
||||
t.Fatal("ParseWithOptions returned a nil file")
|
||||
}
|
||||
img, err := assemble(file)
|
||||
if err != nil {
|
||||
if img != nil {
|
||||
t.Fatal("the assembler returned an image beside its error: a rejected file must not emit")
|
||||
}
|
||||
if strings.TrimSpace(err.Error()) == "" {
|
||||
t.Fatal("rejection carries an empty diagnostic")
|
||||
}
|
||||
return
|
||||
}
|
||||
// Determinism: a second assembly of the same file must produce the same
|
||||
// bytes. One parse serves both runs, so any mutation the assembler makes
|
||||
// to the syntax tree it was handed shows up as differing bytes, and the
|
||||
// workers' footprint under the shared memory fence stays that of a single
|
||||
// parse.
|
||||
img2, err2 := assemble(file)
|
||||
if err2 != nil {
|
||||
t.Fatalf("the second assembly failed where the first succeeded: %v", err2)
|
||||
}
|
||||
if !bytes.Equal(img.Bytes(), img2.Bytes()) {
|
||||
t.Fatal("the same source assembled to different bytes")
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzAssembleAMD64 hammers the full parse-and-assemble pipeline for the
|
||||
// fixed amd64 target with arbitrary source: expansion (macros and includes)
|
||||
// included, matching the CLI's own pipeline.
|
||||
func FuzzAssembleAMD64(f *testing.F) {
|
||||
corpusSeeds(f)
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $16-8\n\tMOVQ x+0(FP), AX\n\tMOVQ AX, ret+8(FP)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $256-0\n\tCALL ·helper(SB)\n\tRET\nTEXT ·helper(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#define L(n) MOVQ $n, AX\nTEXT ·f(SB), NOSPLIT, $0\n\tL(7)\n\tRET\n")
|
||||
f.Add("#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#include \"fuzzdefs.h\"\nTEXT ·f(SB), $16-8\n\tMOVQ KONST, AX\n\tMOVQ ARG(x), BX\n\tRET\n")
|
||||
f.Add("DATA d<>+0(SB)/8, $0xf4f8fcff\nDATA d<>+4(SB)/4, $1\nGLOBL d<>(SB), RODATA, $8\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ d<>(SB), AX\n\tRET\n")
|
||||
f.Add("DATA s+0(SB)/8, $\"hi there\"\nGLOBL s(SB), $8\nDATA p+0(SB)/8, $s(SB)\nGLOBL p(SB), $8\n")
|
||||
f.Add("DATA d+0(SB)/8, $0xFFFFFFFFFFFFFFFF\nGLOBL d(SB), $8\n" +
|
||||
"TEXT ·f(SB), $0-8\n\tMOVQ $0xFFFFFFFFFFFFFFFF, AX\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\nL1:\n\tMOVQ AX, BX\n\tJMP L1\n\tJMP -3(PC)\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tLOOP L1\nL1:\n\tLOOPE L1\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tJMP *AX\n\tCALL (BX)\n\tJMP (R12)(R8*4)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ TLS, AX\n\tMOVQ 8(AX)(TLS*1), BX\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCALIGN $16\n\tMOVQ AX, BX\n\tPCALIGN $32\n\tMOVQ AX, BX\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADJSP $16\n\tMOVQ AX, -8(SP)\n\tADJSP $-16\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADDSD $1.5, X0\n\tMULSD $(-1.0), X1\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tSHLL CX, R11:AX\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCDATA $0, $1\n\tFUNCDATA $0, ·meta(SB)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $0\n\tCALL runtime·morestack_noctxt(SB)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $32-0\n\tMOVQ AX, x-8(SP)\n\tMOVQ BX, x-16(SP)(CX*1)\n\tRET\n")
|
||||
// Shapes that must be rejected: each pins a diagnostic path the seeds
|
||||
// above never reach.
|
||||
f.Add("TEXT ·f(SB), $0\n\tBOGUSINSTR AX, BX\n\tRET\n")
|
||||
f.Add("GLOBL d(SB), $-8\n")
|
||||
f.Add("GLOBL d(SB), $-1\n")
|
||||
f.Add("GLOBL d(SB), $0x4000001\n")
|
||||
f.Add("GLOBL d(SB), $0x7FFFFFFFFFFFFFFF\n")
|
||||
f.Add("DATA d+0(SB)/9, $1\nGLOBL d(SB), $8\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADJSP $16\n\tRET\n")
|
||||
f.Add("#define A A\nA\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
fuzzAssemble(t, "fuzz_amd64.s", src, func(f *ast.File) (*Image, error) {
|
||||
return AssembleFile(f)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzAssembleARM64 hammers the same pipeline for the fixed arm64 target,
|
||||
// whose encoder carries its own immediate classification, memory-offset
|
||||
// gates and literal pool. The seeds pin the recent encoder families: the
|
||||
// system registers and barriers, the LSE atomics and exclusive pairs, the
|
||||
// NEON structure loads and stores, the ADDCON2 offset split, the pooled
|
||||
// vector constants, and the macro and include expansion over arm64
|
||||
// spellings. The rejection shapes pin the diagnostic paths the accepted
|
||||
// seeds never reach.
|
||||
func FuzzAssembleARM64(f *testing.F) {
|
||||
corpusSeeds(f)
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $16-8\n\tMOVW x+0(FP), R0\n\tMOVW R0, ret+8(FP)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $256-0\n\tCALL ·helper(SB)\n\tRET\nTEXT ·helper(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#define L(n) MOVD $n, R0\nTEXT ·f(SB), NOSPLIT, $0\n\tL(7)\n\tRET\n")
|
||||
f.Add("#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#include \"fuzzdefs.h\"\nTEXT ·f(SB), $16-8\n\tMOVD KONST, R0\n\tMOVD ARG(x), R1\n\tRET\n")
|
||||
f.Add("DATA d<>+0(SB)/8, $0xf4f8fcff\nDATA d<>+4(SB)/4, $1\nGLOBL d<>(SB), RODATA, $8\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n\tMOVD d<>(SB), R0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMRS DCZID_EL0, R3\n\tMRS CNTVCT_EL0, R0\n\tMSR $3, SPSel\n" +
|
||||
"\tMSR $9, DAIFSet\n\tDMB $15\n\tDSB $4\n\tISB $1\n\tDC ZVA, R4\n\tSVC $0\n\tBRK $35943\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tLDADDB R2, (R1), R3\n\tLDADDD R2, (R1), ZR\n\tCASW R2, (R1), R3\n" +
|
||||
"\tSWPD R2, (R1), ZR\n\tLDXR (R1), R2\n\tLDAXRW (R1), R5\n\tSTXR R2, (R1), R6\n\tSTLXRB R3, (R1), R6\n" +
|
||||
"\tLDXP (R1), (R2, R3)\n\tSTXP (R2, R3), (R1), R6\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLD1 (R2), [V21.B16]\n\tVLD1.P 32(R1), [V2.B16, V3.B16]\n" +
|
||||
"\tVLD1R (R0), [V0.B16]\n\tVST1 [V2.S4, V3.S4], (R14)\n\tVST1.P [V2.B16], (R1)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD $0xaaaaaa, R2, R3\n\tSUB $0x186a0, R2, R3\n\tADDW $0x60060, R2\n\tCMP $40960, R0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVMOVD $0x123456789ABCDEF0, V0\n\tVMOVQ $0x12345678, $0x9ABCDEF0, V1\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0-8\n\tMOVW $-1, R0\n\tB after1\n\tMOVD $0x0001000200030004, R1\n" +
|
||||
"after1:\n\tMOVD R1, ret+0(FP)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\nL1:\n\tCBZ R1, L1\n\tTBZ $3, R2, L1\n\tBEQ L1\n\tJMP L1\n\tCALL (R5)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCALIGN $16\n\tMOVD R0, R1\n\tPCALIGN $32\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $0\n\tPCDATA $0, $1\n\tFUNCDATA $0, ·meta(SB)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $32-0\n\tMOVD R0, x-8(SP)\n\tRET\n")
|
||||
// Shapes that must be rejected: each pins a diagnostic path the seeds
|
||||
// above never reach.
|
||||
f.Add("TEXT ·f(SB), $0\n\tBOGUSINSTR R0, R1\n\tRET\n")
|
||||
f.Add("GLOBL d(SB), $-8\n")
|
||||
f.Add("GLOBL d(SB), $0x4000001\n")
|
||||
f.Add("DATA d+0(SB)/9, $1\nGLOBL d(SB), $8\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD R1, X99\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tMOVD $1, R1\n\tMOVD 0x1000000(R1), R2\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLD1 (R2), [V21.B17]\n\tRET\n")
|
||||
f.Add("#define A A\nA\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
fuzzAssemble(t, "fuzz_arm64.s", src, AssembleFileARM64)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzAssembleRISCV64 hammers the same pipeline for the fixed riscv64 target.
|
||||
// The seed corpus lives in testdata/seeds/riscv64: the instruction families
|
||||
// GOROOT's riscv64 assembler corpus exercises (the immediate-range ladder of
|
||||
// the I-type arithmetic, the load/store and branch offsets, the atomics, the
|
||||
// FP conversions and the fused multiply-adds), the RVV configuration and
|
||||
// arithmetic classes with their mask forms, the RVC-compressible shapes, the
|
||||
// CSR instructions and the Zbb/Zba/Zbs bit-manipulation set, minimalised.
|
||||
// The inline seeds below pin the shared file-level surface and the rejection
|
||||
// shapes, one diagnostic path each.
|
||||
func FuzzAssembleRISCV64(f *testing.F) {
|
||||
corpusSeeds(f)
|
||||
archCorpusSeeds(f, "riscv64")
|
||||
// Minimal seeds for the shared file-level surface, spelled the riscv64
|
||||
// way: the guard classes, the macro and include expansion, and the data
|
||||
// section with its symbol-valued fields.
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $16-8\n\tMOV x+0(FP), X10\n\tMOV X10, ret+8(FP)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $256-0\n\tCALL ·helper(SB)\n\tRET\nTEXT ·helper(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#define L(n) ADDI $n, X10, X10\nTEXT ·f(SB), NOSPLIT, $0\n\tL(7)\n\tRET\n")
|
||||
f.Add("#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#include \"fuzzdefs.h\"\nTEXT ·f(SB), $16-8\n\tMOV KONST, X10\n\tMOV ARG(x), X11\n\tRET\n")
|
||||
f.Add("DATA d<>+0(SB)/8, $0xf4f8fcff\nDATA d<>+4(SB)/4, $1\nGLOBL d<>(SB), RODATA, $8\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n\tMOV d<>(SB), X10\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCALIGN $16\n\tADD X11, X10, X10\n\tPCALIGN $2048\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $0\n\tPCDATA $0, $1\n\tFUNCDATA $0, ·meta(SB)\n\tRET\n")
|
||||
// Shapes that must be rejected: each pins a diagnostic path the accepted
|
||||
// seeds never reach.
|
||||
f.Add("TEXT ·f(SB), $0\n\tBOGUSINSTR X10, X11\n\tRET\n")
|
||||
f.Add("GLOBL d(SB), $-8\n")
|
||||
f.Add("GLOBL d(SB), $0x4000001\n")
|
||||
f.Add("DATA d+0(SB)/9, $1\nGLOBL d(SB), $8\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD X11, X32\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADDI $2048, X5, X6\n\tADD X11, X40, X6\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tCSRRW $0x1000, X5, X6\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tSLLI $64, X5, X6\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLE8V (X10), V32\n\tRET\n")
|
||||
// Operand-starved spellings that used to panic the layout and encode
|
||||
// passes; each must come back as a diagnostic.
|
||||
f.Add("TEXT ·f(SB), $0\n\tJALR\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tROR $3\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVLE8V (X10)\n\tRET\n")
|
||||
f.Add("#define A A\nA\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
fuzzAssemble(t, "fuzz_riscv64.s", src, AssembleFileRISCV)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzAssembleLOONG64 hammers the same pipeline for the fixed loong64 target.
|
||||
// The seed corpus lives in testdata/seeds/loong64: the LSX and LASX register
|
||||
// banks with their immediate forms and range gates (the si5 compares, the
|
||||
// biased shifts, the VSHUF4I/VPERMI/VEXTRINS immediates), the ll/sc offset
|
||||
// ladder with its three encoding spans, the pointer loads and stores, the
|
||||
// atomics with their dbar forms, the branches, the bit-field instructions and
|
||||
// the register-class moves, minimalised. The inline seeds below pin the
|
||||
// shared file-level surface and the rejection shapes, one diagnostic path
|
||||
// each.
|
||||
func FuzzAssembleLOONG64(f *testing.F) {
|
||||
corpusSeeds(f)
|
||||
archCorpusSeeds(f, "loong64")
|
||||
// Minimal seeds for the shared file-level surface, spelled the loong64
|
||||
// way.
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $16-8\n\tMOVV x+0(FP), R4\n\tMOVV R4, ret+8(FP)\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $256-0\n\tCALL ·helper(SB)\n\tRET\nTEXT ·helper(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#define L(n) ADDV $n, R4, R4\nTEXT ·f(SB), NOSPLIT, $0\n\tL(7)\n\tRET\n")
|
||||
f.Add("#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n")
|
||||
f.Add("#include \"fuzzdefs.h\"\nTEXT ·f(SB), $16-8\n\tMOVV KONST, R4\n\tMOVV ARG(x), R5\n\tRET\n")
|
||||
f.Add("DATA d<>+0(SB)/8, $0xf4f8fcff\nDATA d<>+4(SB)/4, $1\nGLOBL d<>(SB), RODATA, $8\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n\tMOVV d<>(SB), R4\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tPCALIGN $16\n\tADDV R5, R4, R4\n\tPCALIGN $2048\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), $0\n\tPCDATA $0, $1\n\tFUNCDATA $0, ·meta(SB)\n\tRET\n")
|
||||
// Shapes that must be rejected: each pins a diagnostic path the accepted
|
||||
// seeds never reach.
|
||||
f.Add("TEXT ·f(SB), $0\n\tBOGUSINSTR R4, R5\n\tRET\n")
|
||||
f.Add("GLOBL d(SB), $-8\n")
|
||||
f.Add("GLOBL d(SB), $0x4000001\n")
|
||||
f.Add("DATA d+0(SB)/9, $1\nGLOBL d(SB), $8\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tADD R5, R32\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tBEQZ V0, L1\nL1:\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVADDV V1, V2, X3\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVSEQV $32, V2, V3\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tVSHUF4IV $16, V2, V1\n\tRET\n")
|
||||
f.Add("TEXT ·f(SB), NOSPLIT, $0\n\tBSTRPICKV $64, R4, $5, R6\n\tRET\n")
|
||||
f.Add("#define A A\nA\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, src string) {
|
||||
fuzzAssemble(t, "fuzz_loong64.s", src, AssembleFileLOONG64)
|
||||
})
|
||||
}
|
||||
+124
-17
@@ -12,7 +12,7 @@ import (
|
||||
// Image: a .text section holding the function bodies, a .data section
|
||||
// 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
|
||||
// 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
|
||||
// and external ones against undefined globals. The output links with the
|
||||
// system toolchain (cc/ld) the way a hand-assembled .o would.
|
||||
@@ -43,6 +43,12 @@ const (
|
||||
stInfoShift = 4
|
||||
|
||||
rX8664PC32 = 2
|
||||
// R_X86_64_32 (debug/elf): the absolute 32-bit address of a symbol, the
|
||||
// R_ADDR shape a 4-byte DATA field carries.
|
||||
rX8664Abs32 = 10
|
||||
// R_X86_64_TPOFF32 (debug/elf): the local-exec TLS offset the stack
|
||||
// guard loads from FS. 20 is R_X86_64_TLSLD, a different relocation.
|
||||
rX8664TPOFF32 = 23
|
||||
)
|
||||
|
||||
// elfSym is one symbol-table entry in construction.
|
||||
@@ -71,7 +77,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
|
||||
// Build the symbol table: the null entry and the two section symbols
|
||||
// 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
|
||||
// boundary. symIdx maps a symbol name to its index for the relocations.
|
||||
var locals, globals []elfSym
|
||||
@@ -125,11 +131,19 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
sym int
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
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]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
@@ -137,6 +151,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
||||
// assembler measures the symbol from the instruction end,
|
||||
// After − Off bytes past the field, so the addend carries
|
||||
@@ -146,6 +161,50 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// The data symbols' symbol-valued DATA fields ("DATA s+0(SB)/8,
|
||||
// $other(SB)") become .rela.data entries: an absolute relocation of the
|
||||
// DATA line's width at the field's data-section offset, S + A with no
|
||||
// PC term. Widths 4 and 8 have ELF relocation shapes; narrower fields
|
||||
// cannot hold an address, so they are refused rather than truncated.
|
||||
var dataRelas []elfRela
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("data relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch r.Siz {
|
||||
case 8:
|
||||
typ = rX8664Abs64
|
||||
case 4:
|
||||
typ = rX8664Abs32
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: a symbol value of width %d has no ELF relocation", d.Name, r.Siz)
|
||||
}
|
||||
dataRelas = append(dataRelas, elfRela{
|
||||
off: uint64(d.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are code relocations,
|
||||
// .rela.data only when a DATA line holds a symbol value.
|
||||
hasRela := len(relas) > 0
|
||||
hasDataRela := len(dataRelas) > 0
|
||||
nSections := 6 // NULL, .text, .data, .symtab, .strtab, .shstrtab
|
||||
if hasRela {
|
||||
nSections++
|
||||
}
|
||||
if hasDataRela {
|
||||
nSections++
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Serialise the string tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
@@ -155,19 +214,13 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are relocations.
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6 // NULL, .text, .data, .symtab, .strtab, .shstrtab
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Lay the file out: header, section data, section headers.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...) // ELF header, filled last
|
||||
@@ -202,14 +255,25 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
var relaOff, relaDataOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
for _, r := range relas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|rX8664PC32)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
if hasDataRela {
|
||||
align(8)
|
||||
relaDataOff = len(out)
|
||||
for _, r := range dataRelas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
@@ -218,15 +282,32 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// DWARF debug sections (no relocations — the linker resolves DWARF fixups).
|
||||
// DWARF debug sections; the address placeholders they leave are carried
|
||||
// as .rela.debug_info/.rela.debug_line entries the system linker applies.
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiAMD64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
nSections += 4 // .debug_abbrev, .debug_info, .debug_line, .debug_line_str
|
||||
// Five DWARF sections: .debug_abbrev, .debug_info, .debug_line,
|
||||
// .debug_line_str and .debug_frame (the CIE is unconditional, so
|
||||
// the frame section is always present), plus the relocation
|
||||
// sections below when they carry entries.
|
||||
dwarfStart = nSections
|
||||
nSections += 5
|
||||
appendDWARFRelas(&out, dw, rX8664Abs64, dwAlign)
|
||||
if dw.infoRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.lineRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.frameRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
}
|
||||
|
||||
align(8)
|
||||
@@ -255,16 +336,42 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
if hasDataRela {
|
||||
putSh(".rela.data", shtRela, 0, relaDataOff, 24*len(dataRelas), secSymtab, secData, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
|
||||
// DWARF section headers.
|
||||
// DWARF section headers; their indices follow the write order.
|
||||
if dw != nil {
|
||||
// secIdx is a running section index: each putSh below emits the
|
||||
// next header, and the sh_info of a .rela section names the index
|
||||
// of the section it relocates.
|
||||
secIdx := dwarfStart
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
secInfoIdx := secIdx
|
||||
secIdx++
|
||||
if dw.infoRelaCount > 0 {
|
||||
putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
secLineIdx := secIdx
|
||||
secIdx++
|
||||
if dw.lineRelaCount > 0 {
|
||||
putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
secFrameIdx := secIdx
|
||||
secIdx++
|
||||
if dw.frameRelaCount > 0 {
|
||||
putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+162
-75
@@ -12,43 +12,74 @@ import (
|
||||
// self-contained sections because the system linker only performs fixup
|
||||
// relocations, not assembly.
|
||||
|
||||
// DWARF5 attribute, form and line-table constants (the values the
|
||||
// toolchain uses, cmd/internal/dwarf/dwarf_defs.go; the DIE streams below
|
||||
// are written against these forms).
|
||||
const (
|
||||
dwAtName = 0x03 // DW_AT_name
|
||||
dwAtStmtList = 0x10 // DW_AT_stmt_list
|
||||
dwAtLowPC = 0x11 // DW_AT_low_pc
|
||||
dwAtHighPC = 0x12 // DW_AT_high_pc
|
||||
dwAtDeclFile = 0x3a // DW_AT_decl_file
|
||||
dwAtDeclLine = 0x3b // DW_AT_decl_line
|
||||
dwAtExternal = 0x3f // DW_AT_external
|
||||
dwAtFrameBase = 0x40 // DW_AT_frame_base
|
||||
dwTagSubprog = 0x2e // DW_TAG_subprogram
|
||||
dwTagCompUnit = 0x11 // DW_TAG_compile_unit
|
||||
dwFormAddr = 0x01 // DW_FORM_addr
|
||||
dwFormData8 = 0x07 // DW_FORM_data8
|
||||
dwFormString = 0x08 // DW_FORM_string
|
||||
dwFormData1 = 0x0b // DW_FORM_data1
|
||||
dwFormUdata = 0x0f // DW_FORM_udata
|
||||
dwFormSecOff = 0x17 // DW_FORM_sec_offset
|
||||
dwFormExprloc = 0x18 // DW_FORM_exprloc
|
||||
dwFormLineStrp = 0x1f // DW_FORM_line_strp
|
||||
dwLnctPath = 0x01 // DW_LNCT_path
|
||||
dwLnctDirIndex = 0x02 // DW_LNCT_directory_index
|
||||
)
|
||||
|
||||
// dwarfAbbrevTable returns the .debug_abbrev content: a single compilation
|
||||
// unit with DW_TAG_compile_unit and DW_TAG_subprogram entries.
|
||||
// unit with DW_TAG_compile_unit and DW_TAG_subprogram entries. The
|
||||
// attribute/form pairs must match the DIE streams dwarfBuildInfoSection
|
||||
// writes byte for byte, in the same order, or every consumer's parse of
|
||||
// .debug_info desynchronises.
|
||||
func dwarfAbbrevTable() []byte {
|
||||
var b []byte
|
||||
// Abbrev 1: DW_TAG_compile_unit
|
||||
b = append(b, 1) // abbreviation code
|
||||
b = append(b, 0x11) // DW_TAG_compile_unit
|
||||
b = append(b, 1) // DW_CHILDREN_yes
|
||||
b = appendUleb(b, 0x1b) // DW_AT_low_pc
|
||||
b = appendUleb(b, 0x01) // DW_FORM_addr
|
||||
b = appendUleb(b, 0x29) // DW_AT_high_pc
|
||||
b = appendUleb(b, 0x07) // DW_FORM_data8
|
||||
b = appendUleb(b, 0x10) // DW_AT_stmt_list
|
||||
b = appendUleb(b, 0x25) // DW_FORM_sec_offset
|
||||
b = appendUleb(b, 0x01) // DW_AT_name
|
||||
b = appendUleb(b, 0x08) // DW_FORM_string
|
||||
b = appendUleb(b, 0) // end of attributes
|
||||
// Abbrev 1: DW_TAG_compile_unit.
|
||||
b = append(b, 1) // abbreviation code
|
||||
b = appendUleb(b, dwTagCompUnit) // DW_TAG_compile_unit
|
||||
b = append(b, 1) // DW_CHILDREN_yes
|
||||
b = appendUleb(b, dwAtLowPC) // DW_AT_low_pc
|
||||
b = appendUleb(b, dwFormAddr) // DW_FORM_addr
|
||||
b = appendUleb(b, dwAtHighPC) // DW_AT_high_pc
|
||||
b = appendUleb(b, dwFormData8) // DW_FORM_data8
|
||||
b = appendUleb(b, dwAtStmtList) // DW_AT_stmt_list
|
||||
b = appendUleb(b, dwFormSecOff) // DW_FORM_sec_offset (4 bytes here)
|
||||
b = appendUleb(b, dwAtName) // DW_AT_name
|
||||
b = appendUleb(b, dwFormString) // DW_FORM_string
|
||||
b = appendUleb(b, 0) // end of attributes: attr 0
|
||||
b = appendUleb(b, 0) // ... paired with form 0
|
||||
|
||||
// Abbrev 2: DW_TAG_subprogram
|
||||
b = append(b, 2) // abbreviation code
|
||||
b = append(b, 0x2e) // DW_TAG_subprogram
|
||||
b = append(b, 0) // DW_CHILDREN_no
|
||||
b = appendUleb(b, 0x03) // DW_AT_name
|
||||
b = appendUleb(b, 0x08) // DW_FORM_string
|
||||
b = appendUleb(b, 0x11) // DW_AT_low_pc
|
||||
b = appendUleb(b, 0x01) // DW_FORM_addr
|
||||
b = appendUleb(b, 0x29) // DW_AT_high_pc
|
||||
b = appendUleb(b, 0x07) // DW_FORM_data8
|
||||
b = appendUleb(b, 0x3f) // DW_AT_frame_base
|
||||
b = appendUleb(b, 0x18) // DW_FORM_exprloc
|
||||
b = appendUleb(b, 0x3b) // DW_AT_decl_file
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_data1
|
||||
b = appendUleb(b, 0x37) // DW_AT_decl_line
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_data1
|
||||
b = appendUleb(b, 0x63) // DW_AT_external
|
||||
b = appendUleb(b, 0x0b) // DW_FORM_flag
|
||||
b = appendUleb(b, 0) // end of attributes
|
||||
// Abbrev 2: DW_TAG_subprogram.
|
||||
b = append(b, 2) // abbreviation code
|
||||
b = appendUleb(b, dwTagSubprog) // DW_TAG_subprogram
|
||||
b = append(b, 0) // DW_CHILDREN_no
|
||||
b = appendUleb(b, dwAtName) // DW_AT_name
|
||||
b = appendUleb(b, dwFormString) // DW_FORM_string
|
||||
b = appendUleb(b, dwAtLowPC) // DW_AT_low_pc
|
||||
b = appendUleb(b, dwFormAddr) // DW_FORM_addr
|
||||
b = appendUleb(b, dwAtHighPC) // DW_AT_high_pc
|
||||
b = appendUleb(b, dwFormData8) // DW_FORM_data8
|
||||
b = appendUleb(b, dwAtFrameBase) // DW_AT_frame_base
|
||||
b = appendUleb(b, dwFormExprloc) // DW_FORM_exprloc
|
||||
b = appendUleb(b, dwAtDeclFile) // DW_AT_decl_file
|
||||
b = appendUleb(b, dwFormData1) // DW_FORM_data1
|
||||
b = appendUleb(b, dwAtDeclLine) // DW_AT_decl_line
|
||||
b = appendUleb(b, dwFormData1) // DW_FORM_data1
|
||||
b = appendUleb(b, dwAtExternal) // DW_AT_external
|
||||
b = appendUleb(b, 0x0c) // DW_FORM_flag (one byte, 0 or 1)
|
||||
b = appendUleb(b, 0) // end of attributes: attr 0
|
||||
b = appendUleb(b, 0) // ... paired with form 0
|
||||
|
||||
// End of table.
|
||||
b = append(b, 0)
|
||||
@@ -68,6 +99,9 @@ type dwarfSections struct {
|
||||
infoRelocs []dwarfReloc
|
||||
// Relocations for .debug_line: (offset, symbol name, addend).
|
||||
lineRelocs []dwarfReloc
|
||||
// Relocations for .debug_frame: (offset, symbol name, addend), one per
|
||||
// FDE initial_location.
|
||||
frameRelocs []dwarfReloc
|
||||
}
|
||||
|
||||
type dwarfReloc struct {
|
||||
@@ -76,8 +110,9 @@ type dwarfReloc struct {
|
||||
addend int64
|
||||
}
|
||||
|
||||
// emitDWARF generates complete DWARF5 sections for the image.
|
||||
func emitDWARF(img *Image, srcFile string) *dwarfSections {
|
||||
// emitDWARF generates complete DWARF5 sections for the image. cfi carries
|
||||
// the architecture's .debug_frame register conventions.
|
||||
func emitDWARF(img *Image, srcFile string, cfi cfiArch) *dwarfSections {
|
||||
ds := &dwarfSections{}
|
||||
ds.debugAbbrev = dwarfAbbrevTable()
|
||||
|
||||
@@ -86,19 +121,21 @@ func emitDWARF(img *Image, srcFile string) *dwarfSections {
|
||||
lineStr.add(srcFile)
|
||||
ds.debugLineStr = lineStr.bytes()
|
||||
|
||||
// Build .debug_line.
|
||||
ds.debugLine = dwarfBuildLineSection(img, ds)
|
||||
// Build .debug_line; the file table references the source name through
|
||||
// its offset in .debug_line_str.
|
||||
ds.debugLine = dwarfBuildLineSection(img, uint32(lineStr.at(srcFile)), ds)
|
||||
|
||||
// Build .debug_info.
|
||||
ds.debugInfo = dwarfBuildInfoSection(img, srcFile, ds)
|
||||
|
||||
// Build .debug_frame.
|
||||
ds.debugFrame = dwarfBuildFrameSection(img)
|
||||
ds.debugFrame = dwarfBuildFrameSection(img, cfi, ds)
|
||||
return ds
|
||||
}
|
||||
|
||||
// dwarfBuildLineSection builds a complete .debug_line section.
|
||||
func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte {
|
||||
// dwarfBuildLineSection builds a complete .debug_line section. srcStrOff is
|
||||
// the source file name's offset in .debug_line_str.
|
||||
func dwarfBuildLineSection(img *Image, srcStrOff uint32, ds *dwarfSections) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
@@ -120,15 +157,25 @@ func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte {
|
||||
// Standard opcode lengths (opcode 1..opcode_base-1).
|
||||
b = append(b, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0)
|
||||
|
||||
// Directory table (DWARF5 format).
|
||||
b = append(b, 0) // one directory entry (index 0 = empty)
|
||||
// File table.
|
||||
b = appendUleb(b, 1) // file count
|
||||
// File 1: name index into .debug_line_str, dir index, time, size.
|
||||
b = appendUleb(b, 0) // name (index 0 in line_str)
|
||||
b = appendUleb(b, 0) // directory index
|
||||
b = appendUleb(b, 0) // last modification time
|
||||
b = appendUleb(b, 0) // file size
|
||||
// Directory table (DWARF5 §6.2.4): entry format descriptors followed by
|
||||
// the entries. One directory, the compilation directory, whose path is
|
||||
// the empty string at .debug_line_str offset 0.
|
||||
b = append(b, 1) // directory_entry_format_count
|
||||
b = appendUleb(b, dwLnctPath) // DW_LNCT_path
|
||||
b = appendUleb(b, dwFormLineStrp) // DW_FORM_line_strp
|
||||
b = appendUleb(b, 1) // directories_count
|
||||
b = le.AppendUint32(b, 0) // .debug_line_str offset of ""
|
||||
|
||||
// File table (DWARF5 §6.2.5). v5 indexes files from 0, so the source
|
||||
// file is entry 0, matching the DW_AT_decl_file value 0 the DIEs carry.
|
||||
b = append(b, 2) // file_name_entry_format_count
|
||||
b = appendUleb(b, dwLnctPath) // DW_LNCT_path
|
||||
b = appendUleb(b, dwFormLineStrp) // DW_FORM_line_strp
|
||||
b = appendUleb(b, dwLnctDirIndex) // DW_LNCT_directory_index
|
||||
b = appendUleb(b, dwFormUdata) // DW_FORM_udata
|
||||
b = appendUleb(b, 1) // file_names_count
|
||||
b = le.AppendUint32(b, srcStrOff) // .debug_line_str offset of the source name
|
||||
b = appendUleb(b, 0) // directory index 0 (the compilation directory)
|
||||
|
||||
headerEnd := len(b)
|
||||
|
||||
@@ -176,8 +223,11 @@ func dwarfBuildLineSection(img *Image, ds *dwarfSections) []byte {
|
||||
|
||||
// Patch unit_length.
|
||||
le.PutUint32(b[headerStart:], uint32(len(b)-headerStart-4))
|
||||
// Patch header_length.
|
||||
le.PutUint32(b[headerStart+6:], uint32(headerEnd-headerStart-10))
|
||||
// Patch header_length. In the v5 header it follows the one-byte
|
||||
// address_size and segment_selector_size (offset 8, not the DWARF2-4
|
||||
// offset 6), and counts from just past itself to the first program
|
||||
// byte.
|
||||
le.PutUint32(b[headerStart+8:], uint32(headerEnd-headerStart-12))
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -195,14 +245,16 @@ func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte
|
||||
|
||||
// DW_TAG_compile_unit (abbrev 1).
|
||||
b = append(b, 1) // abbreviation code
|
||||
// DW_AT_low_pc: address of .text start.
|
||||
infoRelocBase := len(b)
|
||||
// DW_AT_low_pc: address of .text start. A data-only image has no
|
||||
// functions to relocate against; its CU covers no code, so the base
|
||||
// stays zero (the DWARF "no base address" value) with no relocation.
|
||||
b = le.AppendUint64(b, 0) // placeholder
|
||||
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
|
||||
off: uint64(infoRelocBase),
|
||||
name: img.Funcs[0].Name,
|
||||
addend: 0,
|
||||
})
|
||||
if len(img.Funcs) > 0 {
|
||||
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
|
||||
off: uint64(len(b) - 8),
|
||||
name: img.Funcs[0].Name,
|
||||
})
|
||||
}
|
||||
// DW_AT_high_pc: size of .text.
|
||||
b = le.AppendUint64(b, uint64(len(img.Code)))
|
||||
// DW_AT_stmt_list: offset into .debug_line (0).
|
||||
@@ -229,8 +281,9 @@ func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte
|
||||
b = le.AppendUint64(b, uint64(fn.Size))
|
||||
// DW_AT_frame_base: DW_OP_call_frame_cfa.
|
||||
b = append(b, 1, 0x9c)
|
||||
// DW_AT_decl_file: file index 1.
|
||||
b = append(b, 1)
|
||||
// DW_AT_decl_file: the single file-table entry, index 0 (v5 indexes
|
||||
// files from 0).
|
||||
b = append(b, 0)
|
||||
// DW_AT_decl_line.
|
||||
b = append(b, uint8(fn.Line))
|
||||
// DW_AT_external.
|
||||
@@ -253,41 +306,75 @@ func appendUleb(b []byte, v uint64) []byte {
|
||||
return binary.AppendUvarint(b, v)
|
||||
}
|
||||
|
||||
// appendSleb appends v in signed LEB128, the encoding DWARF specifies:
|
||||
// two's-complement sign extension, which is NOT Go's zigzag varint
|
||||
// (binary.AppendVarint(-8) encodes 15, where DWARF wants 0x78).
|
||||
func appendSleb(b []byte, v int64) []byte {
|
||||
return binary.AppendVarint(b, v)
|
||||
for {
|
||||
c := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if (v == 0 && c&0x40 == 0) || (v == -1 && c&0x40 != 0) {
|
||||
return append(b, c)
|
||||
}
|
||||
b = append(b, c|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
// cfiArch carries the .debug_frame CIE parameters that differ per
|
||||
// architecture: the DWARF register numbers of the stack pointer the initial
|
||||
// CFA rule names and of the return address. The values are the ones the Go
|
||||
// linker writes into its own CIE (cmd/link/internal/ld/dwarf.go uses
|
||||
// Dwarfregsp and Dwarfreglr; the per-architecture constants live in
|
||||
// cmd/link/internal/<arch>/l.go).
|
||||
type cfiArch struct {
|
||||
name string
|
||||
cfaReg byte // the stack-pointer register the initial CFA rule names
|
||||
raReg byte // the return-address register
|
||||
}
|
||||
|
||||
var (
|
||||
cfiAMD64 = cfiArch{"amd64", 7, 16} // RSP, RIP
|
||||
cfiARM64 = cfiArch{"arm64", 31, 30} // SP (X31), LR (X30)
|
||||
cfiRISCV64 = cfiArch{"riscv64", 2, 1} // X2 (sp), X1 (ra)
|
||||
cfiLOONG64 = cfiArch{"loong64", 3, 1} // $r3 (sp), $r1 (ra)
|
||||
)
|
||||
|
||||
// dwarfBuildFrameSection builds a .debug_frame section with CFI for stack
|
||||
// unwinding. It emits one CIE and one FDE per function, encoding the
|
||||
// CFA (Canonical Frame Address) rule changes at each stack-adjustment
|
||||
// boundary recorded in FuncLayout.Spadj.
|
||||
func dwarfBuildFrameSection(img *Image) []byte {
|
||||
func dwarfBuildFrameSection(img *Image, cfi cfiArch, ds *dwarfSections) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
// CIE (Common Information Entry).
|
||||
cieStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||
b = le.AppendUint32(b, 0xFFFFFFFF) // CIE marker
|
||||
b = append(b, 3) // version (DWARF3, widely supported)
|
||||
b = append(b, 0) // augmentation (empty)
|
||||
b = appendUleb(b, 1) // code alignment
|
||||
b = appendSleb(b, -8) // data alignment (-8 for 64-bit)
|
||||
b = appendUleb(b, 16) // return address register (LR on arm64, RIP on amd64)
|
||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||
b = le.AppendUint32(b, 0xFFFFFFFF) // CIE marker
|
||||
b = append(b, 3) // version (DWARF3, widely supported)
|
||||
b = append(b, 0) // augmentation (empty)
|
||||
b = appendUleb(b, 1) // code alignment
|
||||
b = appendSleb(b, -8) // data alignment (-8 for 64-bit)
|
||||
b = appendUleb(b, uint64(cfi.raReg)) // return address register
|
||||
// Initial CFA rule: DW_CFA_def_cfa (SP, 0)
|
||||
b = append(b, 0x0c) // DW_CFA_def_cfa
|
||||
b = appendUleb(b, 31) // register: SP (RSP=7 on amd64, SP=31 on arm64)
|
||||
b = appendUleb(b, 0) // offset: 0
|
||||
b = append(b, 0) // DW_CFA_nop (padding)
|
||||
b = append(b, 0x0c) // DW_CFA_def_cfa
|
||||
b = appendUleb(b, uint64(cfi.cfaReg)) // the architecture's stack pointer
|
||||
b = appendUleb(b, 0) // offset: 0
|
||||
b = append(b, 0) // DW_CFA_nop (padding)
|
||||
// Patch CIE length.
|
||||
le.PutUint32(b[cieStart:], uint32(len(b)-cieStart-4))
|
||||
|
||||
// FDEs (Frame Description Entries) — one per function.
|
||||
// FDEs (Frame Description Entries), one per function.
|
||||
for _, fn := range img.Funcs {
|
||||
fdeStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // length (placeholder)
|
||||
b = le.AppendUint32(b, uint32(cieStart)) // CIE pointer (offset from start)
|
||||
// Initial location: function offset in .text (relocated by linker).
|
||||
// Initial location: function offset in .text, referenced through
|
||||
// the function's symbol so the linker relocates it.
|
||||
ds.frameRelocs = append(ds.frameRelocs, dwarfReloc{
|
||||
off: uint64(fdeStart + 8),
|
||||
name: fn.Name,
|
||||
})
|
||||
b = le.AppendUint64(b, uint64(fn.Offset))
|
||||
// Address range: function size.
|
||||
b = le.AppendUint64(b, uint64(fn.Size))
|
||||
|
||||
+84
-24
@@ -3,6 +3,17 @@
|
||||
|
||||
package asm
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// Absolute 64-bit relocation types for the DWARF address fixups, one per
|
||||
// supported architecture (the numbers debug/elf carries).
|
||||
const (
|
||||
rX8664Abs64 = 1 // R_X86_64_64
|
||||
rAARCH64Abs64 = 257 // R_AARCH64_ABS64
|
||||
rRISCVAbs64 = 2 // R_RISCV_64
|
||||
rLarchAbs64 = 2 // R_LARCH_64
|
||||
)
|
||||
|
||||
// dwarfELFSections holds the laid-out DWARF sections ready for inclusion
|
||||
// in an ELF file.
|
||||
type dwarfELFSections struct {
|
||||
@@ -11,15 +22,23 @@ type dwarfELFSections struct {
|
||||
lineOff, lineSize int
|
||||
lineStrOff, lineStrSize int
|
||||
frameOff, frameSize int
|
||||
// Relocations for .debug_info address references.
|
||||
// .rela.debug_info and .rela.debug_line contents: file offsets and
|
||||
// entry counts (zero count: the section is absent).
|
||||
infoRelaOff, infoRelaCount int
|
||||
lineRelaOff, lineRelaCount int
|
||||
frameRelaOff, frameRelaCount int
|
||||
// Relocations for .debug_info address references, offsets relative to
|
||||
// the section start (what an r_offset in .rela.debug_info means).
|
||||
infoRelocs []elfDwarfReloc
|
||||
// Relocations for .debug_line address references.
|
||||
// Relocations for .debug_line address references, section-relative.
|
||||
lineRelocs []elfDwarfReloc
|
||||
// Relocations for .debug_frame FDE initial locations, section-relative.
|
||||
frameRelocs []elfDwarfReloc
|
||||
}
|
||||
|
||||
type elfDwarfReloc struct {
|
||||
off uint64
|
||||
sym int // symbol index in .symtab
|
||||
off uint64 // offset within the target section
|
||||
sym int // symbol index in .symtab
|
||||
addend int64
|
||||
}
|
||||
|
||||
@@ -29,8 +48,9 @@ type elfDwarfReloc struct {
|
||||
//
|
||||
// symIdx maps function names to their .symtab indices (needed for relocations
|
||||
// against .text symbols). The map uses objectName format (pkg.name); the
|
||||
// DWARF code uses bare function names, so we build a reverse lookup.
|
||||
func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[string]int, align func(int)) *dwarfELFSections {
|
||||
// DWARF code uses bare function names, so we build a reverse lookup. cfi
|
||||
// carries the architecture's .debug_frame register conventions.
|
||||
func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[string]int, align func(int), cfi cfiArch) *dwarfELFSections {
|
||||
// Build a lookup from bare function name to symbol index.
|
||||
nameToIdx := make(map[string]int, len(symIdx))
|
||||
for name, idx := range symIdx {
|
||||
@@ -45,7 +65,7 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str
|
||||
}
|
||||
nameToIdx[name] = idx
|
||||
}
|
||||
ds := emitDWARF(img, srcFile)
|
||||
ds := emitDWARF(img, srcFile, cfi)
|
||||
if ds == nil || len(ds.debugAbbrev) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -68,15 +88,11 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str
|
||||
align(1)
|
||||
result.lineOff = len(*out)
|
||||
result.lineSize = len(ds.debugLine)
|
||||
lineBase := len(*out)
|
||||
*out = append(*out, ds.debugLine...)
|
||||
|
||||
// Patch .debug_line relocations: replace placeholder addresses with
|
||||
// actual .text offsets via symbol lookup.
|
||||
for _, dr := range ds.lineRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.lineRelocs = append(result.lineRelocs, elfDwarfReloc{
|
||||
off: uint64(lineBase) + dr.off,
|
||||
off: dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
@@ -87,33 +103,77 @@ func appendDWARFSections(out *[]byte, img *Image, srcFile string, symIdx map[str
|
||||
align(1)
|
||||
result.infoOff = len(*out)
|
||||
result.infoSize = len(ds.debugInfo)
|
||||
infoBase := len(*out)
|
||||
*out = append(*out, ds.debugInfo...)
|
||||
|
||||
// .debug_frame
|
||||
if len(ds.debugFrame) > 0 {
|
||||
align(1)
|
||||
result.frameOff = len(*out)
|
||||
result.frameSize = len(ds.debugFrame)
|
||||
*out = append(*out, ds.debugFrame...)
|
||||
}
|
||||
|
||||
// Patch .debug_info relocations.
|
||||
for _, dr := range ds.infoRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.infoRelocs = append(result.infoRelocs, elfDwarfReloc{
|
||||
off: uint64(infoBase) + dr.off,
|
||||
off: dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// .debug_frame: the section header declares alignment 8, so the data is
|
||||
// padded to 8, matching it.
|
||||
if len(ds.debugFrame) > 0 {
|
||||
align(8)
|
||||
result.frameOff = len(*out)
|
||||
result.frameSize = len(ds.debugFrame)
|
||||
*out = append(*out, ds.debugFrame...)
|
||||
for _, dr := range ds.frameRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.frameRelocs = append(result.frameRelocs, elfDwarfReloc{
|
||||
off: dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// appendDWARFRelas writes the .rela.debug_info and .rela.debug_line section
|
||||
// bodies from the relocations appendDWARFSections recorded, with the
|
||||
// architecture's absolute 64-bit relocation type, and records their file
|
||||
// offsets and entry counts on dw. Called after the DWARF sections
|
||||
// themselves so the r_offsets (section-relative) need no adjustment.
|
||||
func appendDWARFRelas(out *[]byte, dw *dwarfELFSections, abs64 uint32, align func(int)) {
|
||||
le := binary.LittleEndian
|
||||
write := func(relas []elfDwarfReloc) (off, count int) {
|
||||
if len(relas) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
align(8)
|
||||
off = len(*out)
|
||||
for _, r := range relas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(abs64))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
*out = append(*out, b[:]...)
|
||||
}
|
||||
return off, len(relas)
|
||||
}
|
||||
dw.infoRelaOff, dw.infoRelaCount = write(dw.infoRelocs)
|
||||
dw.lineRelaOff, dw.lineRelaCount = write(dw.lineRelocs)
|
||||
dw.frameRelaOff, dw.frameRelaCount = write(dw.frameRelocs)
|
||||
}
|
||||
|
||||
// dwarfSourceName returns the source name the DWARF sections record: the
|
||||
// image's source path when the assembler captured one, "gasm.s" otherwise.
|
||||
func dwarfSourceName(img *Image) string {
|
||||
if img.SourcePath != "" {
|
||||
return img.SourcePath
|
||||
}
|
||||
return "gasm.s"
|
||||
}
|
||||
|
||||
// dwarfSectionNames returns the DWARF section names for the string table.
|
||||
var dwarfSectionNames = []string{
|
||||
".debug_abbrev", ".debug_info", ".debug_line", ".debug_line_str",
|
||||
".debug_frame", ".rela.debug_info", ".rela.debug_line",
|
||||
".rela.debug_frame",
|
||||
}
|
||||
+304
-13
@@ -4,11 +4,313 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// ulebIter reads ULEB128 values, the .debug_abbrev and line-header
|
||||
// encoding.
|
||||
type ulebIter struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
func (r *ulebIter) uleb(t *testing.T) uint64 {
|
||||
t.Helper()
|
||||
v, n := binary.Uvarint(r.b[r.i:])
|
||||
if n <= 0 {
|
||||
t.Fatalf("bad ULEB at %d", r.i)
|
||||
}
|
||||
r.i += n
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *ulebIter) byteAt(t *testing.T) byte {
|
||||
t.Helper()
|
||||
if r.i >= len(r.b) {
|
||||
t.Fatalf("read past end at %d", r.i)
|
||||
}
|
||||
c := r.b[r.i]
|
||||
r.i++
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *ulebIter) uint32At(t *testing.T) uint32 {
|
||||
t.Helper()
|
||||
v := binary.LittleEndian.Uint32(r.b[r.i:])
|
||||
r.i += 4
|
||||
return v
|
||||
}
|
||||
|
||||
// sleb reads a signed LEB128, the DWARF encoding (sign-extended two's
|
||||
// complement, not Go's zigzag varint).
|
||||
func (r *ulebIter) sleb(t *testing.T) int64 {
|
||||
t.Helper()
|
||||
var v int64
|
||||
var shift uint
|
||||
for {
|
||||
c := r.byteAt(t)
|
||||
v |= int64(c&0x7f) << shift
|
||||
shift += 7
|
||||
if c&0x80 == 0 {
|
||||
if c&0x40 != 0 {
|
||||
v |= -1 << shift
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dwarfAttr is one attribute/form pair of an abbreviation.
|
||||
type dwarfAttr struct{ attr, form uint64 }
|
||||
|
||||
// dwarfAbbrev is one parsed abbreviation declaration.
|
||||
type dwarfAbbrev struct {
|
||||
code uint64
|
||||
tag uint64
|
||||
children bool
|
||||
attrs []dwarfAttr
|
||||
}
|
||||
|
||||
// parseAbbrevs walks a .debug_abbrev table: abbreviation code, tag,
|
||||
// children flag, then attr/form ULEB pairs terminated by a double zero.
|
||||
func parseAbbrevs(t *testing.T, b []byte) map[uint64]dwarfAbbrev {
|
||||
t.Helper()
|
||||
out := map[uint64]dwarfAbbrev{}
|
||||
r := &ulebIter{b: b}
|
||||
for {
|
||||
code := r.uleb(t)
|
||||
if code == 0 {
|
||||
return out
|
||||
}
|
||||
ab := dwarfAbbrev{code: code, tag: r.uleb(t)}
|
||||
ab.children = r.byteAt(t) == 1
|
||||
for {
|
||||
attr := r.uleb(t)
|
||||
form := r.uleb(t)
|
||||
if attr == 0 && form == 0 {
|
||||
break
|
||||
}
|
||||
if attr == 0 || form == 0 {
|
||||
t.Fatalf("abbrev %d: half-terminated attr/form pair (%d, %d)", code, attr, form)
|
||||
}
|
||||
ab.attrs = append(ab.attrs, dwarfAttr{attr, form})
|
||||
}
|
||||
out[code] = ab
|
||||
}
|
||||
}
|
||||
|
||||
func eqAttrs(t *testing.T, ab dwarfAbbrev, want []dwarfAttr) {
|
||||
t.Helper()
|
||||
if len(ab.attrs) != len(want) {
|
||||
t.Fatalf("abbrev %d attrs = %v, want %v", ab.code, ab.attrs, want)
|
||||
}
|
||||
for i, w := range want {
|
||||
if ab.attrs[i] != w {
|
||||
t.Fatalf("abbrev %d attr %d = (%#x, %#x), want (%#x, %#x)", ab.code, i, ab.attrs[i].attr, ab.attrs[i].form, w.attr, w.form)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwarfAbbrevTable walks the abbreviation table as a consumer does and
|
||||
// checks the attribute/form sets against the constants the toolchain uses
|
||||
// (cmd/internal/dwarf/dwarf_defs.go). A wrong constant here renames an
|
||||
// attribute (0x1b is comp_dir, not low_pc; 0x29 and 0x37 are bounds and
|
||||
// count) and a wrong form desynchronises the DIE parse: 0x25 is strx1, one
|
||||
// byte, where the writer emits four for a section offset.
|
||||
func TestDwarfAbbrevTable(t *testing.T) {
|
||||
abbrev := dwarfAbbrevTable()
|
||||
if len(abbrev) == 0 {
|
||||
t.Fatal("empty abbrev table")
|
||||
}
|
||||
// Must end with a zero byte (end of table).
|
||||
if abbrev[len(abbrev)-1] != 0 {
|
||||
t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1])
|
||||
}
|
||||
abs := parseAbbrevs(t, abbrev)
|
||||
if len(abs) != 2 {
|
||||
t.Fatalf("abbreviations = %d, want 2", len(abs))
|
||||
}
|
||||
cu, ok := abs[1]
|
||||
if !ok {
|
||||
t.Fatal("missing abbreviation 1 (compile unit)")
|
||||
}
|
||||
if cu.tag != dwTagCompUnit || !cu.children {
|
||||
t.Errorf("abbrev 1: tag %#x children %v, want compile unit with children", cu.tag, cu.children)
|
||||
}
|
||||
eqAttrs(t, cu, []dwarfAttr{
|
||||
{dwAtLowPC, dwFormAddr},
|
||||
{dwAtHighPC, dwFormData8},
|
||||
{dwAtStmtList, dwFormSecOff},
|
||||
{dwAtName, dwFormString},
|
||||
})
|
||||
sp, ok := abs[2]
|
||||
if !ok {
|
||||
t.Fatal("missing abbreviation 2 (subprogram)")
|
||||
}
|
||||
if sp.tag != dwTagSubprog || sp.children {
|
||||
t.Errorf("abbrev 2: tag %#x children %v, want subprogram without children", sp.tag, sp.children)
|
||||
}
|
||||
eqAttrs(t, sp, []dwarfAttr{
|
||||
{dwAtName, dwFormString},
|
||||
{dwAtLowPC, dwFormAddr},
|
||||
{dwAtHighPC, dwFormData8},
|
||||
{dwAtFrameBase, dwFormExprloc},
|
||||
{dwAtDeclFile, dwFormData1},
|
||||
{dwAtDeclLine, dwFormData1},
|
||||
{dwAtExternal, 0x0c}, // DW_FORM_flag
|
||||
})
|
||||
}
|
||||
|
||||
// TestDwarfLineHeaderV5 parses the .debug_line header under DWARF5 rules:
|
||||
// the directory and file tables are format-descriptor lists, not the
|
||||
// DWARF2-4 shape of null-terminated strings, and the file entry references
|
||||
// the source name through .debug_line_str.
|
||||
func TestDwarfLineHeaderV5(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), BX
|
||||
ADDQ BX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
ds := emitDWARF(img, "test_amd64.s", cfiAMD64)
|
||||
r := &ulebIter{b: ds.debugLine}
|
||||
r.uint32At(t) // unit_length
|
||||
if v := binary.LittleEndian.Uint16(ds.debugLine[4:]); v != 5 {
|
||||
t.Fatalf("version = %d, want 5", v)
|
||||
}
|
||||
r.i = 6
|
||||
r.byteAt(t) // address_size
|
||||
r.byteAt(t) // segment_selector_size
|
||||
r.uint32At(t) // header_length
|
||||
r.byteAt(t) // minimum_instruction_length
|
||||
r.byteAt(t) // maximum_ops_per_instruction
|
||||
r.byteAt(t) // default_is_stmt
|
||||
r.byteAt(t) // line_base
|
||||
r.byteAt(t) // line_range
|
||||
opcodeBase := r.byteAt(t)
|
||||
for range int(opcodeBase) - 1 {
|
||||
r.byteAt(t) // standard opcode lengths
|
||||
}
|
||||
|
||||
// Directory table (DWARF5 §6.2.4).
|
||||
if n := r.byteAt(t); n != 1 {
|
||||
t.Fatalf("directory_entry_format_count = %d, want 1", n)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctPath {
|
||||
t.Errorf("directory content type = %#x, want DW_LNCT_path", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormLineStrp {
|
||||
t.Errorf("directory form = %#x, want DW_FORM_line_strp", form)
|
||||
}
|
||||
if n := r.uleb(t); n != 1 {
|
||||
t.Fatalf("directories_count = %d, want 1", n)
|
||||
}
|
||||
if off := r.uint32At(t); off != 0 {
|
||||
t.Errorf("compilation directory line_strp = %d, want 0 (the empty string)", off)
|
||||
}
|
||||
|
||||
// File table (DWARF5 §6.2.5).
|
||||
if n := r.byteAt(t); n != 2 {
|
||||
t.Fatalf("file_name_entry_format_count = %d, want 2", n)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctPath {
|
||||
t.Errorf("file content type = %#x, want DW_LNCT_path", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormLineStrp {
|
||||
t.Errorf("file path form = %#x, want DW_FORM_line_strp", form)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctDirIndex {
|
||||
t.Errorf("file content type = %#x, want DW_LNCT_directory_index", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormUdata {
|
||||
t.Errorf("file dir-index form = %#x, want DW_FORM_udata", form)
|
||||
}
|
||||
if n := r.uleb(t); n != 1 {
|
||||
t.Fatalf("file_names_count = %d, want 1", n)
|
||||
}
|
||||
strOff := r.uint32At(t)
|
||||
if dirIdx := r.uleb(t); dirIdx != 0 {
|
||||
t.Errorf("file directory index = %d, want 0", dirIdx)
|
||||
}
|
||||
|
||||
// The file entry's line_strp must resolve to the source name.
|
||||
end := int(strOff) + len("test_amd64.s")
|
||||
if int(strOff) >= len(ds.debugLineStr) || !bytes.Equal(ds.debugLineStr[strOff:end], []byte("test_amd64.s")) {
|
||||
t.Errorf("file entry line_strp %d does not name the source: %q", strOff, ds.debugLineStr)
|
||||
}
|
||||
|
||||
// The fixed header fields: address_size 8 and a header_length that
|
||||
// points just past the file table (the patch site is offset 8 in the
|
||||
// v5 header, and the field counts from its own end).
|
||||
if ds.debugLine[6] != 8 || ds.debugLine[7] != 0 {
|
||||
t.Errorf("address_size/segment_selector = %d/%d, want 8/0", ds.debugLine[6], ds.debugLine[7])
|
||||
}
|
||||
if hl := binary.LittleEndian.Uint32(ds.debugLine[8:]); hl != uint32(r.i-12) {
|
||||
t.Errorf("header_length = %d, want %d (the byte after the file table is %d)", hl, r.i-12, r.i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwarfFrameCIEArch checks the shared CIE carries each architecture's
|
||||
// stack-pointer and return-address registers: the values the Go linker
|
||||
// writes (cmd/link/internal/<arch>/l.go dwarfRegSP/dwarfRegLR).
|
||||
func TestDwarfFrameCIEArch(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cfi cfiArch
|
||||
}{
|
||||
{"amd64", cfiAMD64},
|
||||
{"arm64", cfiARM64},
|
||||
{"riscv64", cfiRISCV64},
|
||||
{"loong64", cfiLOONG64},
|
||||
} {
|
||||
frame := dwarfBuildFrameSection(&Image{}, tc.cfi, &dwarfSections{})
|
||||
r := &ulebIter{b: frame}
|
||||
r.uint32At(t) // length
|
||||
if cid := r.uint32At(t); cid != 0xFFFFFFFF {
|
||||
t.Errorf("%s: CIE id = %#x, want 0xffffffff", tc.name, cid)
|
||||
}
|
||||
if v := r.byteAt(t); v != 3 {
|
||||
t.Errorf("%s: CIE version = %d, want 3", tc.name, v)
|
||||
}
|
||||
if aug := r.byteAt(t); aug != 0 {
|
||||
t.Errorf("%s: CIE augmentation = %d, want 0", tc.name, aug)
|
||||
}
|
||||
if ca := r.uleb(t); ca != 1 {
|
||||
t.Errorf("%s: code alignment = %d, want 1", tc.name, ca)
|
||||
}
|
||||
if da := r.sleb(t); da != -8 {
|
||||
t.Errorf("%s: data alignment = %d, want -8 (signed LEB128, not zigzag)", tc.name, da)
|
||||
}
|
||||
if ra := r.uleb(t); ra != uint64(tc.cfi.raReg) {
|
||||
t.Errorf("%s: return-address register = %d, want %d", tc.name, ra, tc.cfi.raReg)
|
||||
}
|
||||
if op := r.byteAt(t); op != 0x0c {
|
||||
t.Errorf("%s: expected DW_CFA_def_cfa, got opcode %#x", tc.name, op)
|
||||
}
|
||||
if cfa := r.uleb(t); cfa != uint64(tc.cfi.cfaReg) {
|
||||
t.Errorf("%s: CFA register = %d, want %d", tc.name, cfa, tc.cfi.cfaReg)
|
||||
}
|
||||
if off := r.uleb(t); off != 0 {
|
||||
t.Errorf("%s: CFA offset = %d, want 0", tc.name, off)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitDWARF(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
@@ -27,7 +329,7 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
ds := emitDWARF(img, "test_amd64.s")
|
||||
ds := emitDWARF(img, "test_amd64.s", cfiAMD64)
|
||||
|
||||
// .debug_abbrev must not be empty and must start with abbrev code 1.
|
||||
if len(ds.debugAbbrev) == 0 {
|
||||
@@ -68,14 +370,3 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
t.Fatal("no .debug_info relocations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDwarfAbbrevTable(t *testing.T) {
|
||||
abbrev := dwarfAbbrevTable()
|
||||
if len(abbrev) == 0 {
|
||||
t.Fatal("empty abbrev table")
|
||||
}
|
||||
// Must end with a zero byte (end of table).
|
||||
if abbrev[len(abbrev)-1] != 0 {
|
||||
t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1])
|
||||
}
|
||||
}
|
||||
+552
-2
@@ -12,7 +12,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The object-file tests share one source: two exported functions, one
|
||||
@@ -52,7 +53,7 @@ func elfTestImage(t *testing.T) *Image {
|
||||
}
|
||||
|
||||
// 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.
|
||||
func TestAssembleFileExternals(t *testing.T) {
|
||||
img := elfTestImage(t)
|
||||
@@ -211,6 +212,75 @@ func TestELFObject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFObjectTLSGuardReloc checks that a non-NOSPLIT function's stack
|
||||
// guard carries an R_X86_64_TPOFF32 relocation against the null symbol in
|
||||
// .rela.text. The serialisation must honour the record's type field: a
|
||||
// hardcoded R_X86_64_PC32 mislinks the TLS load as an ordinary
|
||||
// PC-relative reference.
|
||||
func TestELFObjectTLSGuardReloc(t *testing.T) {
|
||||
f, errs := parser.Parse("g_amd64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·grow(SB), $0
|
||||
CALL ·other(SB)
|
||||
RET
|
||||
|
||||
TEXT ·other(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
var haveTLS bool
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
if r.Kind == RelTLSLE {
|
||||
haveTLS = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !haveTLS {
|
||||
t.Fatal("test source produced no RelTLSLE relocation")
|
||||
}
|
||||
obj, err := img.ELFObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFObject: %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)
|
||||
}
|
||||
found := false
|
||||
for i := 0; i+24 <= len(raw); i += 24 {
|
||||
e := raw[i:]
|
||||
info := binary.LittleEndian.Uint64(e[8:])
|
||||
typ := info & 0xffffffff
|
||||
sym := int(info >> 32)
|
||||
if typ == uint64(elf.R_X86_64_TPOFF32) {
|
||||
found = true
|
||||
if sym != 0 {
|
||||
t.Errorf("TPOFF32 relocation against symbol %d, want 0 (the null symbol)", sym)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("no R_X86_64_TPOFF32 relocation in .rela.text (%d bytes)", len(raw))
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFObjectNoRelocations checks a file with no static-symbol references
|
||||
// emits a valid object without a .rela.text section.
|
||||
func TestELFObjectNoRelocations(t *testing.T) {
|
||||
@@ -253,6 +323,238 @@ TEXT ·nop(SB), NOSPLIT, $0
|
||||
}
|
||||
}
|
||||
|
||||
// elfSectionHeaderCount returns the e_shnum the ELF header declares.
|
||||
func elfSectionHeaderCount(t *testing.T, obj []byte) int {
|
||||
t.Helper()
|
||||
return int(binary.LittleEndian.Uint16(obj[60:]))
|
||||
}
|
||||
|
||||
// checkELFSectionAccounting verifies the number of section headers the
|
||||
// writer physically laid out equals e_shnum: every DWARF section written
|
||||
// after .shstrtab must be counted, or the last ones (always .debug_frame)
|
||||
// are invisible to every consumer, debug/elf included.
|
||||
func checkELFSectionAccounting(t *testing.T, obj []byte) {
|
||||
t.Helper()
|
||||
shoff := int(binary.LittleEndian.Uint64(obj[40:]))
|
||||
shentsize := int(binary.LittleEndian.Uint16(obj[58:]))
|
||||
shnum := elfSectionHeaderCount(t, obj)
|
||||
if shentsize != 64 {
|
||||
t.Fatalf("e_shentsize = %d, want 64", shentsize)
|
||||
}
|
||||
if (len(obj)-shoff)%shentsize != 0 {
|
||||
t.Fatalf("section header table is not a whole number of entries: shoff=%d len=%d", shoff, len(obj))
|
||||
}
|
||||
if present := (len(obj) - shoff) / shentsize; present != shnum {
|
||||
t.Errorf("e_shnum = %d but %d section headers are laid out", shnum, present)
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFDWARFSectionAccounting runs the header accounting check over all
|
||||
// four architecture emitters, and additionally checks the .debug_frame
|
||||
// section is visible (its data aligned as its header declares).
|
||||
func TestELFDWARFSectionAccounting(t *testing.T) {
|
||||
parse := func(name, src string) *ast.File {
|
||||
f, errs := parser.Parse(name, src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse %s: %v", name, errs)
|
||||
}
|
||||
return f
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
img *Image
|
||||
emit func(*Image) ([]byte, error)
|
||||
}{
|
||||
{"amd64", elfTestImage(t), (*Image).ELFObject},
|
||||
{"arm64", mustImage(t, func() (*Image, error) {
|
||||
return AssembleFileARM64(parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`))
|
||||
}), (*Image).ELFAARCH64Object},
|
||||
{"riscv64", mustImage(t, func() (*Image, error) {
|
||||
return AssembleFileRISCV(parse("k_riscv64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·sb(SB), NOSPLIT, $0-0
|
||||
MOV $answer<>(SB), X10
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`))
|
||||
}), (*Image).ELFRISCVObject},
|
||||
{"loong64", mustImage(t, func() (*Image, error) {
|
||||
return AssembleFileLOONG64(parse("k_loong64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
`))
|
||||
}), (*Image).ELFLOONG64Object},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
obj, err := tc.emit(tc.img)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: emit: %v", tc.name, err)
|
||||
}
|
||||
checkELFSectionAccounting(t, obj)
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: parse emitted object: %v", tc.name, err)
|
||||
}
|
||||
frame := ef.Section(".debug_frame")
|
||||
if frame == nil {
|
||||
t.Errorf("%s: .debug_frame invisible to debug/elf (e_shnum too small?)", tc.name)
|
||||
ef.Close()
|
||||
continue
|
||||
}
|
||||
if frame.Offset%8 != 0 || frame.Addralign != 8 {
|
||||
t.Errorf("%s: .debug_frame offset %d align %d, want offset%%8==0 align 8", tc.name, frame.Offset, frame.Addralign)
|
||||
}
|
||||
ef.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func mustImage(t *testing.T, f func() (*Image, error)) *Image {
|
||||
t.Helper()
|
||||
img, err := f()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// TestELFDWARFRelocations checks the .rela.debug_info and .rela.debug_line
|
||||
// sections exist and carry absolute 64-bit relocations against the
|
||||
// function symbols, with r_offsets inside their target sections.
|
||||
func TestELFDWARFRelocations(t *testing.T) {
|
||||
img := elfTestImage(t)
|
||||
obj, err := img.ELFObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFObject: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
// The DWARF must record the assembled file's path (threaded through
|
||||
// Image.SourcePath), not a placeholder name.
|
||||
info, err := ef.Section(".debug_info").Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if img.SourcePath != "t_amd64.s" || !bytes.Contains(info, []byte(img.SourcePath)) {
|
||||
t.Errorf("DWARF compilation unit does not name the source %q", img.SourcePath)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
rela string
|
||||
target string
|
||||
want uint32
|
||||
}{
|
||||
{".rela.debug_info", ".debug_info", rX8664Abs64},
|
||||
{".rela.debug_line", ".debug_line", rX8664Abs64},
|
||||
{".rela.debug_frame", ".debug_frame", rX8664Abs64},
|
||||
} {
|
||||
rs := ef.Section(tc.rela)
|
||||
if rs == nil {
|
||||
t.Fatalf("missing %s", tc.rela)
|
||||
}
|
||||
if rs.Type != elf.SHT_RELA {
|
||||
t.Errorf("%s: type %v, want SHT_RELA", tc.rela, rs.Type)
|
||||
}
|
||||
target := ef.Section(tc.target)
|
||||
if target == nil {
|
||||
t.Fatalf("missing %s", tc.target)
|
||||
}
|
||||
if rs.Link == 0 || ef.Sections[rs.Info] != target {
|
||||
t.Errorf("%s: link %d info %d, want the symtab and %s", tc.rela, rs.Link, rs.Info, tc.target)
|
||||
}
|
||||
b, err := rs.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// .debug_line has one address per function; .debug_info adds the
|
||||
// compile unit's own low_pc.
|
||||
want := len(img.Funcs)
|
||||
if tc.target == ".debug_info" {
|
||||
want++
|
||||
}
|
||||
if len(b)/24 != want {
|
||||
t.Errorf("%s: %d entries, want %d", tc.rela, len(b)/24, want)
|
||||
}
|
||||
for i := 0; i+24 <= len(b); i += 24 {
|
||||
r_offset := binary.LittleEndian.Uint64(b[i:])
|
||||
info := binary.LittleEndian.Uint64(b[i+8:])
|
||||
typ := uint32(info)
|
||||
sym := int(info >> 32)
|
||||
if typ != tc.want {
|
||||
t.Errorf("%s entry %d: type %d, want R_X86_64_64 (%d)", tc.rela, i/24, typ, tc.want)
|
||||
}
|
||||
if r_offset >= uint64(target.Size) {
|
||||
t.Errorf("%s entry %d: r_offset %d outside %s (%d bytes)", tc.rela, i/24, r_offset, tc.target, target.Size)
|
||||
}
|
||||
if sym == 0 {
|
||||
t.Errorf("%s entry %d: against the null symbol", tc.rela, i/24)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFDataOnly checks a source with GLOBL data and no TEXT emits a valid
|
||||
// ELF object: the DWARF compilation unit of a code-less image has no
|
||||
// function to relocate against and must not reach for one.
|
||||
func TestELFDataOnly(t *testing.T) {
|
||||
f, errs := parser.Parse("d0_amd64.s", `
|
||||
GLOBL table<>(SB), RODATA, $8
|
||||
DATA table<>+0(SB)/8, $12345
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
obj, err := img.ELFObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFObject: %v", err)
|
||||
}
|
||||
checkELFSectionAccounting(t, obj)
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, s := range syms {
|
||||
if s.Name == "table" && s.Size == 8 {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("data symbol table missing: %v", syms)
|
||||
}
|
||||
if ef.Section(".rela.debug_info") != nil || ef.Section(".rela.debug_line") != nil {
|
||||
t.Error("data-only image must not emit DWARF address relocations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFLinkAndRun is the end-to-end check: assemble the test functions,
|
||||
// link the emitted object with a C driver that defines the external symbol,
|
||||
// and run the result. Skipped when no C compiler is available.
|
||||
@@ -307,4 +609,252 @@ int main(void) {
|
||||
if got := string(run); got != "42 42 7\n" {
|
||||
t.Errorf("output %q, want \"42 42 7\\n\"", got)
|
||||
}
|
||||
|
||||
// The DWARF addresses must have resolved at link time: the .debug_info
|
||||
// placeholders were carried by .rela.debug_info, so every subprogram's
|
||||
// low_pc must now equal its linked symbol address.
|
||||
bin, err := os.ReadFile(appPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lef, err := elf.NewFile(bytes.NewReader(bin))
|
||||
if err != nil {
|
||||
t.Fatalf("parse linked binary: %v", err)
|
||||
}
|
||||
defer lef.Close()
|
||||
syms, err := lef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addrByName := map[string]uint64{}
|
||||
for _, s := range syms {
|
||||
if elf.ST_TYPE(s.Info) == elf.STT_FUNC && s.Value != 0 {
|
||||
addrByName[s.Name] = s.Value
|
||||
}
|
||||
}
|
||||
lowPCs := dwarfSubprogramLowPCs(t, lef)
|
||||
if len(lowPCs) == 0 {
|
||||
t.Fatal("no subprogram DW_AT_low_pc parsed from the linked binary")
|
||||
}
|
||||
for name, pc := range lowPCs {
|
||||
addr, ok := addrByName[name]
|
||||
if !ok {
|
||||
t.Errorf("subprogram %q not in the linked symbol table", name)
|
||||
continue
|
||||
}
|
||||
if pc != addr {
|
||||
t.Errorf("subprogram %q: DW_AT_low_pc = %#x, linked address %#x (DWARF relocation unresolved)", name, pc, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dwarfSubprogramLowPCs walks the linked binary's .debug_info with its own
|
||||
// .debug_abbrev and returns each DW_TAG_subprogram's DW_AT_low_pc by name.
|
||||
func dwarfSubprogramLowPCs(t *testing.T, ef *elf.File) map[string]uint64 {
|
||||
t.Helper()
|
||||
abbrevSec := ef.Section(".debug_abbrev")
|
||||
infoSec := ef.Section(".debug_info")
|
||||
if abbrevSec == nil || infoSec == nil {
|
||||
t.Fatal("linked binary lacks .debug_abbrev or .debug_info")
|
||||
}
|
||||
abbrev, err := abbrevSec.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := infoSec.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
abs := parseAbbrevs(t, abbrev)
|
||||
le := binary.LittleEndian
|
||||
out := map[string]uint64{}
|
||||
r := &ulebIter{b: info}
|
||||
r.uint32At(t) // unit_length
|
||||
if v := le.Uint16(info[4:]); v != 5 {
|
||||
t.Fatalf(".debug_info version %d, want 5", v)
|
||||
}
|
||||
r.i = 6
|
||||
r.byteAt(t) // unit_type
|
||||
r.byteAt(t) // address_size
|
||||
r.uint32At(t) // debug_abbrev_offset
|
||||
var name string
|
||||
var lowPC uint64
|
||||
for r.i < len(r.b) {
|
||||
code := r.uleb(t)
|
||||
if code == 0 {
|
||||
continue // end of the CU's children
|
||||
}
|
||||
ab, ok := abs[code]
|
||||
if !ok {
|
||||
t.Fatalf("unknown abbreviation code %d", code)
|
||||
}
|
||||
name, lowPC = "", 0
|
||||
for _, a := range ab.attrs {
|
||||
switch a.attr {
|
||||
case dwAtName:
|
||||
readFormKeep(t, r, a.form, &name, nil)
|
||||
case dwAtLowPC:
|
||||
readFormKeep(t, r, a.form, nil, &lowPC)
|
||||
default:
|
||||
readFormSkip(t, r, a.form)
|
||||
}
|
||||
}
|
||||
if ab.tag == dwTagSubprog && name != "" {
|
||||
out[name] = lowPC
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// readFormKeep reads one DIE attribute value, keeping a string or an
|
||||
// address into the pointer it was given (nil keeps nothing).
|
||||
func readFormKeep(t *testing.T, r *ulebIter, form uint64, name *string, addr *uint64) {
|
||||
t.Helper()
|
||||
switch form {
|
||||
case dwFormString:
|
||||
end := r.i
|
||||
for end < len(r.b) && r.b[end] != 0 {
|
||||
end++
|
||||
}
|
||||
if name != nil {
|
||||
*name = string(r.b[r.i:end])
|
||||
}
|
||||
r.i = end + 1
|
||||
case dwFormAddr:
|
||||
if addr != nil {
|
||||
*addr = binary.LittleEndian.Uint64(r.b[r.i:])
|
||||
}
|
||||
r.i += 8
|
||||
default:
|
||||
readFormSkip(t, r, form)
|
||||
}
|
||||
}
|
||||
|
||||
func readFormSkip(t *testing.T, r *ulebIter, form uint64) {
|
||||
t.Helper()
|
||||
switch form {
|
||||
case dwFormString:
|
||||
for r.i < len(r.b) && r.b[r.i] != 0 {
|
||||
r.i++
|
||||
}
|
||||
r.i++
|
||||
case dwFormAddr, dwFormData8:
|
||||
r.i += 8
|
||||
case dwFormSecOff:
|
||||
r.i += 4
|
||||
case dwFormExprloc:
|
||||
r.i += int(r.uleb(t))
|
||||
case dwFormData1, 0x0c:
|
||||
r.i++
|
||||
default:
|
||||
t.Fatalf("unsupported form %#x", form)
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFObjectDataRelocation checks that a symbol-valued DATA field ("DATA
|
||||
// s+0(SB)/8, $other(SB)") reaches the ELF object as a .rela.data entry: an
|
||||
// absolute 64-bit relocation at the field's offset within .data, against
|
||||
// the named symbol, external targets included.
|
||||
func TestELFObjectDataRelocation(t *testing.T) {
|
||||
f, errs := parser.Parse("t_amd64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $24
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+8(SB)/8, $holder(SB)
|
||||
DATA holder+16(SB)/8, $extvar(SB)
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
obj, err := img.ELFObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFObject: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
relaData := ef.Section(".rela.data")
|
||||
if relaData == nil {
|
||||
t.Fatal("missing .rela.data section")
|
||||
}
|
||||
if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" {
|
||||
t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link)
|
||||
}
|
||||
if ef.Sections[relaData.Info].Name != ".data" {
|
||||
t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info)
|
||||
}
|
||||
relas, err := relaData.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
for i := 0; i+24 <= len(relas); i += 24 {
|
||||
got = append(got, struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}{
|
||||
off: binary.LittleEndian.Uint64(relas[i:]),
|
||||
// r_info packs the type in the low dword and the symbol index
|
||||
// in the high dword.
|
||||
typ: binary.LittleEndian.Uint32(relas[i+8:]),
|
||||
sym: binary.LittleEndian.Uint32(relas[i+12:]),
|
||||
addend: int64(binary.LittleEndian.Uint64(relas[i+16:])),
|
||||
})
|
||||
}
|
||||
// debug/elf hides the table's null entry, so raw index s names syms[s-1].
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := func(idx uint32) string {
|
||||
if idx >= 1 && int(idx) <= len(syms) {
|
||||
return syms[idx-1].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// The offsets are data-section-relative: the field's DATA offset plus
|
||||
// the symbol's position in .data (the layout aligns each symbol to 16).
|
||||
base := uint64(0)
|
||||
for _, d := range img.DataSyms {
|
||||
if d.Name == "holder" {
|
||||
base = uint64(d.Offset)
|
||||
}
|
||||
}
|
||||
want := []struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
addend int64
|
||||
target string
|
||||
}{
|
||||
{off: base + 0, typ: uint32(elf.R_X86_64_64), addend: 5, target: "Keep"},
|
||||
{off: base + 8, typ: uint32(elf.R_X86_64_64), addend: 0, target: "holder"},
|
||||
{off: base + 16, typ: uint32(elf.R_X86_64_64), addend: 0, target: "extvar"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := got[i]
|
||||
if g.off != w.off || g.typ != w.typ || g.addend != w.addend {
|
||||
t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}",
|
||||
i, g.off, g.typ, g.addend, w.off, w.typ, w.addend)
|
||||
}
|
||||
if n := name(g.sym); n != w.target {
|
||||
t.Errorf("entry %d names %q, want %q", i, n, w.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
-29
@@ -15,14 +15,22 @@ const (
|
||||
|
||||
// AArch64 relocation types (the ELF psABI).
|
||||
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
|
||||
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD/STR/LDR page offset)
|
||||
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD page offset)
|
||||
rArm64Call26 = 283 // R_AARCH64_CALL26 (BL instruction)
|
||||
rArm64Ldst64Lo12NC = 286 // R_AARCH64_LDST64_ABS_LO12_NC (64-bit LDR/STR page offset)
|
||||
// R_AARCH64_TLSLE_MOVW_TPREL_G0 (debug/elf 547): the local-exec TLS
|
||||
// load's MOVZ field, the module offset at bits [15:0].
|
||||
rArm64TLSLEMovwTprelG0 = 547
|
||||
// R_AARCH64_ABS32 (debug/elf 258): the absolute 32-bit address of a
|
||||
// symbol, the R_ADDR shape a 4-byte DATA field carries. ABS64 (257)
|
||||
// lives with the DWARF fixup constants as rAARCH64Abs64.
|
||||
rArm64Abs32 = 258
|
||||
)
|
||||
|
||||
// ELFAARCH64Object returns the image as an ELF64 relocatable object file for
|
||||
// AArch64 (EM_AARCH64, 64-bit, little-endian). The structure mirrors the
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab and an
|
||||
// optional .rela.text.
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab, an
|
||||
// optional .rela.text and an optional .rela.data.
|
||||
func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
@@ -80,8 +88,19 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference is an ADRP pair:
|
||||
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21
|
||||
// ADD/LDR/STR → R_AARCH64_ADD_ABS_LO12_NC
|
||||
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21 at the ADRP
|
||||
// ADD → R_AARCH64_ADD_ABS_LO12_NC at the ADD word
|
||||
// LDR/STR X → R_AARCH64_LDST64_ABS_LO12_NC at the LDR/STR word
|
||||
// BL → R_AARCH64_CALL26
|
||||
// cmd/link's own conversion emits the HI21 at sectoff and the LO12 at
|
||||
// sectoff+4 (cmd/link/internal/arm64/asm.go), so the ADD or load word
|
||||
// carries the page-offset relocation, never a second HI21. The
|
||||
// assembler records two RelArm64Addr relocs per ADRP+ADD pair (one per
|
||||
// word), so the second of the pair is consumed here.
|
||||
// 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 {
|
||||
off uint64
|
||||
typ uint32
|
||||
@@ -90,29 +109,86 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
for i := 0; i < len(fn.Relocs); i++ {
|
||||
r := fn.Relocs[i]
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch {
|
||||
case r.Kind == RelArm64Branch:
|
||||
typ = rArm64Call26
|
||||
case r.Kind == RelArm64Addr && r.Off%4 == 4:
|
||||
typ = rArm64AddAbsLo12NC
|
||||
switch r.Kind {
|
||||
case RelArm64Branch:
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off), typ: rArm64Call26, sym: idx, addend: r.Addend,
|
||||
})
|
||||
case RelArm64Addr:
|
||||
// ADRP+ADD: the pair's second reloc (at Off+4) is the
|
||||
// assembler's twin of the same pair; skip it.
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rArm64PrelPgHi21, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rArm64AddAbsLo12NC, sym: idx, addend: r.Addend},
|
||||
)
|
||||
i++
|
||||
case RelArm64LDST64:
|
||||
// ADRP+LDR/STR: one assembler reloc covers the pair.
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rArm64PrelPgHi21, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rArm64Ldst64Lo12NC, sym: idx, addend: r.Addend},
|
||||
)
|
||||
case RelArm64TLSLE:
|
||||
// The local-exec MOVZ: one relocation over the imm16 field.
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off), typ: rArm64TLSLEMovwTprelG0, sym: idx, addend: r.Addend,
|
||||
})
|
||||
default:
|
||||
typ = rArm64PrelPgHi21
|
||||
return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind)
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
}
|
||||
}
|
||||
|
||||
// The data symbols' symbol-valued DATA fields ("DATA s+0(SB)/8,
|
||||
// $other(SB)") become .rela.data entries: an absolute relocation of the
|
||||
// DATA line's width at the field's data-section offset, S + A with no
|
||||
// PC term. Widths 4 and 8 have ELF relocation shapes; narrower fields
|
||||
// cannot hold an address, so they are refused rather than truncated.
|
||||
var dataRelas []elfRela
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("data relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch r.Siz {
|
||||
case 8:
|
||||
typ = rAARCH64Abs64
|
||||
case 4:
|
||||
typ = rArm64Abs32
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: a symbol value of width %d has no ELF relocation", d.Name, r.Siz)
|
||||
}
|
||||
dataRelas = append(dataRelas, elfRela{
|
||||
off: uint64(d.Offset + r.Off),
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
typ: typ,
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are code relocations,
|
||||
// .rela.data only when a DATA line holds a symbol value.
|
||||
hasRela := len(relas) > 0
|
||||
hasDataRela := len(dataRelas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections++
|
||||
}
|
||||
if hasDataRela {
|
||||
nSections++
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
@@ -122,18 +198,13 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
@@ -168,7 +239,7 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
var relaOff, relaDataOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
@@ -180,19 +251,47 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
if hasDataRela {
|
||||
align(8)
|
||||
relaDataOff = len(out)
|
||||
for _, r := range dataRelas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// DWARF debug sections.
|
||||
// DWARF debug sections; the address placeholders they leave are carried
|
||||
// as .rela.debug_info/.rela.debug_line entries the system linker applies.
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiARM64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
// Five DWARF sections: .debug_abbrev, .debug_info, .debug_line,
|
||||
// .debug_line_str and .debug_frame (the CIE is unconditional, so
|
||||
// the frame section is always present), plus the relocation
|
||||
// sections below when they carry entries.
|
||||
dwarfStart = nSections
|
||||
nSections += 5
|
||||
appendDWARFRelas(&out, dw, rAARCH64Abs64, dwAlign)
|
||||
if dw.infoRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.lineRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.frameRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
}
|
||||
|
||||
align(8)
|
||||
@@ -220,14 +319,41 @@ func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
if hasDataRela {
|
||||
putSh(".rela.data", shtRela, 0, relaDataOff, 24*len(dataRelas), secSymtab, secData, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
// DWARF section headers; their indices follow the write order.
|
||||
if dw != nil {
|
||||
// secIdx is a running section index: each putSh below emits the
|
||||
// next header, and the sh_info of a .rela section names the index
|
||||
// of the section it relocates.
|
||||
secIdx := dwarfStart
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
secInfoIdx := secIdx
|
||||
secIdx++
|
||||
if dw.infoRelaCount > 0 {
|
||||
putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
secLineIdx := secIdx
|
||||
secIdx++
|
||||
if dw.lineRelaCount > 0 {
|
||||
putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
secFrameIdx := secIdx
|
||||
secIdx++
|
||||
if dw.frameRelaCount > 0 {
|
||||
putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+174
-2
@@ -6,9 +6,10 @@ package asm
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
||||
@@ -28,6 +29,7 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
|
||||
TEXT ·getanswer(SB), NOSPLIT, $0-8
|
||||
MOVD answer<>(SB), R4
|
||||
MOVD $answer<>(SB), R5
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
@@ -102,8 +104,63 @@ DATA answer<>+0(SB)/8, $42
|
||||
// Check that .rela.text exists (getanswer has SB reference).
|
||||
relaText := ef.Section(".rela.text")
|
||||
if relaText == nil {
|
||||
t.Error("missing .rela.text section")
|
||||
t.Fatal("missing .rela.text section")
|
||||
}
|
||||
|
||||
// The SB references of getanswer form two ADRP pairs: the load
|
||||
// (MOVD answer<>(SB), R4) is ADRP+LDR carrying HI21 at the ADRP and
|
||||
// LDST64_ABS_LO12_NC at the LDR word, and the address-of
|
||||
// (MOVD $answer<>(SB), R5) is ADRP+ADD carrying HI21 and
|
||||
// ADD_ABS_LO12_NC. cmd/link's own conversion emits exactly this
|
||||
// sectoff / sectoff+4 pairing; a second HI21 at the ADD or LDR word
|
||||
// corrupts the pair.
|
||||
raw, err := relaText.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(raw)%24 != 0 || len(raw)/24 != 4 {
|
||||
t.Fatalf(".rela.text has %d bytes, want four 24-byte entries", len(raw))
|
||||
}
|
||||
wantRela := []struct {
|
||||
typ elf.R_AARCH64
|
||||
off uint64 // relative to the getanswer function start
|
||||
}{
|
||||
{elf.R_AARCH64_ADR_PREL_PG_HI21, 0},
|
||||
{elf.R_AARCH64_LDST64_ABS_LO12_NC, 4},
|
||||
{elf.R_AARCH64_ADR_PREL_PG_HI21, 8},
|
||||
{elf.R_AARCH64_ADD_ABS_LO12_NC, 12},
|
||||
}
|
||||
getanswer := byNameElf(t, ef, "getanswer")
|
||||
for i, w := range wantRela {
|
||||
e := raw[i*24 : (i+1)*24]
|
||||
off := binary.LittleEndian.Uint64(e[0:])
|
||||
info := binary.LittleEndian.Uint64(e[8:])
|
||||
typ := elf.R_AARCH64(info & 0xffffffff)
|
||||
sym := int(info >> 32)
|
||||
if typ != w.typ || off != getanswer.Value+w.off {
|
||||
t.Errorf("reloc %d: type %v off %d, want %v at %d", i, typ, off, w.typ, getanswer.Value+w.off)
|
||||
}
|
||||
if sym != 3 { // NULL, .text, .data, then the first local: answer
|
||||
t.Errorf("reloc %d: symbol index %d, want 3 (answer)", i, sym)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// byNameElf returns the symbol table entry for name from the raw .symtab,
|
||||
// which carries every entry including the null and section symbols in order.
|
||||
func byNameElf(t *testing.T, ef *elf.File, name string) elf.Symbol {
|
||||
t.Helper()
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatalf("symbols: %v", err)
|
||||
}
|
||||
for _, s := range syms {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
t.Fatalf("symbol %q not found", name)
|
||||
return elf.Symbol{}
|
||||
}
|
||||
|
||||
// TestELFAARCH64ObjectNoRelocations checks the ELF output when there are no
|
||||
@@ -140,3 +197,118 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
t.Error("unexpected .rela.text section when there are no relocations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFAARCH64ObjectDataRelocation checks that a symbol-valued DATA field
|
||||
// ("DATA s+0(SB)/8, $other(SB)") reaches the AArch64 ELF object as a
|
||||
// .rela.data entry: an R_AARCH64_ABS64 (ABS32 for a width-4 field) at the
|
||||
// field's offset within .data, against the named symbol, external targets
|
||||
// included.
|
||||
func TestELFAARCH64ObjectDataRelocation(t *testing.T) {
|
||||
f, errs := parser.Parse("t_arm64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $32
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+8(SB)/8, $holder(SB)
|
||||
DATA holder+16(SB)/8, $extvar(SB)
|
||||
DATA holder+24(SB)/4, $Keep(SB)
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFAARCH64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFAARCH64Object: %v", err)
|
||||
}
|
||||
checkELFSectionAccounting(t, obj)
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
relaData := ef.Section(".rela.data")
|
||||
if relaData == nil {
|
||||
t.Fatal("missing .rela.data section")
|
||||
}
|
||||
if relaData.Type != elf.SHT_RELA {
|
||||
t.Errorf(".rela.data type = %v, want SHT_RELA", relaData.Type)
|
||||
}
|
||||
if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" {
|
||||
t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link)
|
||||
}
|
||||
if ef.Sections[relaData.Info].Name != ".data" {
|
||||
t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info)
|
||||
}
|
||||
relas, err := relaData.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
for i := 0; i+24 <= len(relas); i += 24 {
|
||||
got = append(got, struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}{
|
||||
off: binary.LittleEndian.Uint64(relas[i:]),
|
||||
// r_info packs the type in the low dword and the symbol index
|
||||
// in the high dword.
|
||||
typ: binary.LittleEndian.Uint32(relas[i+8:]),
|
||||
sym: binary.LittleEndian.Uint32(relas[i+12:]),
|
||||
addend: int64(binary.LittleEndian.Uint64(relas[i+16:])),
|
||||
})
|
||||
}
|
||||
// debug/elf hides the table's null entry, so raw index s names syms[s-1].
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := func(idx uint32) string {
|
||||
if idx >= 1 && int(idx) <= len(syms) {
|
||||
return syms[idx-1].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// The offsets are data-section-relative: the field's DATA offset plus
|
||||
// the symbol's position in .data (the layout aligns each symbol to 16).
|
||||
base := uint64(0)
|
||||
for _, d := range img.DataSyms {
|
||||
if d.Name == "holder" {
|
||||
base = uint64(d.Offset)
|
||||
}
|
||||
}
|
||||
want := []struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
addend int64
|
||||
target string
|
||||
}{
|
||||
{off: base + 0, typ: uint32(elf.R_AARCH64_ABS64), addend: 5, target: "Keep"},
|
||||
{off: base + 8, typ: uint32(elf.R_AARCH64_ABS64), addend: 0, target: "holder"},
|
||||
{off: base + 16, typ: uint32(elf.R_AARCH64_ABS64), addend: 0, target: "extvar"},
|
||||
{off: base + 24, typ: uint32(elf.R_AARCH64_ABS32), addend: 0, target: "Keep"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := got[i]
|
||||
if g.off != w.off || g.typ != w.typ || g.addend != w.addend {
|
||||
t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}",
|
||||
i, g.off, g.typ, g.addend, w.off, w.typ, w.addend)
|
||||
}
|
||||
if n := name(g.sym); n != w.target {
|
||||
t.Errorf("entry %d names %q, want %q", i, n, w.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
-16
@@ -13,15 +13,26 @@ import (
|
||||
const (
|
||||
emLOONGARCH = 258 // EM_LOONGARCH
|
||||
|
||||
// EF_LOONGARCH_ABI_DOUBLE_FLOAT | EF_LOONGARCH_OBJABI_V1: the flags the
|
||||
// Go toolchain writes (cmd/link/internal/ld/elf.go: Flags = 0x43 for
|
||||
// Loong64). System linkers refuse to merge ET_REL objects whose float
|
||||
// ABI differs, so 0 (soft-float) would make the object unlinkable.
|
||||
efLarchAbiDoubleObjV1 = 0x43
|
||||
|
||||
// LoongArch relocation types (the ELF psABI).
|
||||
rLarchPCALAHI20 = 71 // R_LARCH_PCALA_HI20 (pcalau12i)
|
||||
rLarchPCALALO12 = 72 // R_LARCH_PCALA_LO12 (addi.d/ld/st)
|
||||
rLarchB26 = 66 // R_LARCH_B26 (b/bl, matches the Go linker's mapping)
|
||||
// R_LARCH_32 (debug/elf 1): the absolute 32-bit address of a symbol,
|
||||
// the R_ADDR shape a 4-byte DATA field carries. R_LARCH_64 (2) lives
|
||||
// with the DWARF fixup constants as rLarchAbs64.
|
||||
rLarchAbs32 = 1
|
||||
)
|
||||
|
||||
// ELFLOONG64Object returns the image as an ELF64 relocatable object file for
|
||||
// LoongArch (EM_LOONGARCH, 64-bit, little-endian). The structure mirrors the
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab and an
|
||||
// optional .rela.text.
|
||||
// amd64 and RISC-V ELF emitters: .text, .data, .symtab, .strtab, an
|
||||
// optional .rela.text and an optional .rela.data.
|
||||
func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
@@ -95,18 +106,65 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
typ := uint32(rLarchPCALAHI20)
|
||||
if r.Kind == RelLoong64AddrLo {
|
||||
switch r.Kind {
|
||||
case RelLoong64AddrLo:
|
||||
typ = rLarchPCALALO12
|
||||
case RelLoong64Branch:
|
||||
typ = rLarchB26
|
||||
}
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
typ: typ,
|
||||
sym: idx,
|
||||
addend: r.Addend - int64(r.After-r.Off),
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The data symbols' symbol-valued DATA fields ("DATA s+0(SB)/8,
|
||||
// $other(SB)") become .rela.data entries: an absolute relocation of the
|
||||
// DATA line's width at the field's data-section offset, S + A with no
|
||||
// PC term. Widths 4 and 8 have ELF relocation shapes; narrower fields
|
||||
// cannot hold an address, so they are refused rather than truncated.
|
||||
var dataRelas []elfRela
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("data relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch r.Siz {
|
||||
case 8:
|
||||
typ = rLarchAbs64
|
||||
case 4:
|
||||
typ = rLarchAbs32
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: a symbol value of width %d has no ELF relocation", d.Name, r.Siz)
|
||||
}
|
||||
dataRelas = append(dataRelas, elfRela{
|
||||
off: uint64(d.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are code relocations,
|
||||
// .rela.data only when a DATA line holds a symbol value.
|
||||
hasRela := len(relas) > 0
|
||||
hasDataRela := len(dataRelas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections++
|
||||
}
|
||||
if hasDataRela {
|
||||
nSections++
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
@@ -116,18 +174,13 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
@@ -162,7 +215,7 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
var relaOff, relaDataOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
@@ -174,6 +227,17 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
if hasDataRela {
|
||||
align(8)
|
||||
relaDataOff = len(out)
|
||||
for _, r := range dataRelas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
@@ -183,9 +247,25 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiLOONG64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
// Five DWARF sections: .debug_abbrev, .debug_info, .debug_line,
|
||||
// .debug_line_str and .debug_frame (the CIE is unconditional, so
|
||||
// the frame section is always present), plus the relocation
|
||||
// sections below when they carry entries.
|
||||
dwarfStart = nSections
|
||||
nSections += 5
|
||||
appendDWARFRelas(&out, dw, rLarchAbs64, dwAlign)
|
||||
if dw.infoRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.lineRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.frameRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
}
|
||||
|
||||
align(8)
|
||||
@@ -213,14 +293,41 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
if hasDataRela {
|
||||
putSh(".rela.data", shtRela, 0, relaDataOff, 24*len(dataRelas), secSymtab, secData, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
// DWARF section headers; their indices follow the write order.
|
||||
if dw != nil {
|
||||
// secIdx is a running section index: each putSh below emits the
|
||||
// next header, and the sh_info of a .rela section names the index
|
||||
// of the section it relocates.
|
||||
secIdx := dwarfStart
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
secInfoIdx := secIdx
|
||||
secIdx++
|
||||
if dw.infoRelaCount > 0 {
|
||||
putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
secLineIdx := secIdx
|
||||
secIdx++
|
||||
if dw.lineRelaCount > 0 {
|
||||
putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
secFrameIdx := secIdx
|
||||
secIdx++
|
||||
if dw.frameRelaCount > 0 {
|
||||
putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +340,7 @@ func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint32(hdr[48:], efLarchAbiDoubleObjV1)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
|
||||
+163
-1
@@ -9,7 +9,7 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
||||
@@ -55,6 +55,11 @@ DATA answer<>+0(SB)/8, $42
|
||||
if ef.Type != elf.ET_REL || ef.Machine != elf.EM_LOONGARCH {
|
||||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_LOONGARCH", ef.Type, ef.Machine)
|
||||
}
|
||||
// The double-float ABI plus OBJABI_V1 flags the Go toolchain writes;
|
||||
// system linkers refuse ABI-mismatched merges.
|
||||
if flags := binary.LittleEndian.Uint32(obj[48:]); flags != efLarchAbiDoubleObjV1 {
|
||||
t.Errorf("e_flags = %#x, want %#x (double-float, OBJABI_V1)", flags, efLarchAbiDoubleObjV1)
|
||||
}
|
||||
|
||||
text := ef.Section(".text")
|
||||
data := ef.Section(".data")
|
||||
@@ -198,3 +203,160 @@ TEXT ·nop(SB), NOSPLIT, $0
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFLOONG64ObjectDataRelocation checks that a symbol-valued DATA field
|
||||
// ("DATA s+0(SB)/8, $other(SB)") reaches the LoongArch ELF object as a
|
||||
// .rela.data entry: an R_LARCH_64 (R_LARCH_32 for a width-4 field) at the
|
||||
// field's offset within .data, against the named symbol, external targets
|
||||
// included.
|
||||
func TestELFLOONG64ObjectDataRelocation(t *testing.T) {
|
||||
f, errs := parser.Parse("t_loong64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $32
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+8(SB)/8, $holder(SB)
|
||||
DATA holder+16(SB)/8, $extvar(SB)
|
||||
DATA holder+24(SB)/4, $Keep(SB)
|
||||
`)
|
||||
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)
|
||||
}
|
||||
checkELFSectionAccounting(t, obj)
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
relaData := ef.Section(".rela.data")
|
||||
if relaData == nil {
|
||||
t.Fatal("missing .rela.data section")
|
||||
}
|
||||
if relaData.Type != elf.SHT_RELA {
|
||||
t.Errorf(".rela.data type = %v, want SHT_RELA", relaData.Type)
|
||||
}
|
||||
if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" {
|
||||
t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link)
|
||||
}
|
||||
if ef.Sections[relaData.Info].Name != ".data" {
|
||||
t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info)
|
||||
}
|
||||
relas, err := relaData.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
for i := 0; i+24 <= len(relas); i += 24 {
|
||||
got = append(got, struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}{
|
||||
off: binary.LittleEndian.Uint64(relas[i:]),
|
||||
// r_info packs the type in the low dword and the symbol index
|
||||
// in the high dword.
|
||||
typ: binary.LittleEndian.Uint32(relas[i+8:]),
|
||||
sym: binary.LittleEndian.Uint32(relas[i+12:]),
|
||||
addend: int64(binary.LittleEndian.Uint64(relas[i+16:])),
|
||||
})
|
||||
}
|
||||
// debug/elf hides the table's null entry, so raw index s names syms[s-1].
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := func(idx uint32) string {
|
||||
if idx >= 1 && int(idx) <= len(syms) {
|
||||
return syms[idx-1].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// The offsets are data-section-relative: the field's DATA offset plus
|
||||
// the symbol's position in .data (the layout aligns each symbol to 16).
|
||||
base := uint64(0)
|
||||
for _, d := range img.DataSyms {
|
||||
if d.Name == "holder" {
|
||||
base = uint64(d.Offset)
|
||||
}
|
||||
}
|
||||
want := []struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
addend int64
|
||||
target string
|
||||
}{
|
||||
{off: base + 0, typ: uint32(elf.R_LARCH_64), addend: 5, target: "Keep"},
|
||||
{off: base + 8, typ: uint32(elf.R_LARCH_64), addend: 0, target: "holder"},
|
||||
{off: base + 16, typ: uint32(elf.R_LARCH_64), addend: 0, target: "extvar"},
|
||||
{off: base + 24, typ: uint32(elf.R_LARCH_32), addend: 0, target: "Keep"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := got[i]
|
||||
if g.off != w.off || g.typ != w.typ || g.addend != w.addend {
|
||||
t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}",
|
||||
i, g.off, g.typ, g.addend, w.off, w.typ, w.addend)
|
||||
}
|
||||
if n := name(g.sym); n != w.target {
|
||||
t.Errorf("entry %d names %q, want %q", i, n, w.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
-21
@@ -13,17 +13,29 @@ import (
|
||||
const (
|
||||
emRISCV = 243 // EM_RISCV
|
||||
|
||||
// EF_RISCV_FLOAT_ABI_DOUBLE: the double-precision float ABI the Go
|
||||
// toolchain targets (cmd/link/internal/ld/elf.go writes Flags = 0x4 for
|
||||
// RISCV64). System linkers refuse to merge ET_REL objects whose float
|
||||
// ABI differs, so 0 (soft-float) would make the object unlinkable.
|
||||
efRISCVFloatAbiDouble = 0x4
|
||||
|
||||
// RISC-V relocation types.
|
||||
rRISCV32 = 1
|
||||
rRISCVJAL = 17 // R_RISCV_JAL
|
||||
rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20
|
||||
rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I
|
||||
rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S
|
||||
rRISCVTPRELHI20 = 29 // R_RISCV_TPREL_HI20
|
||||
rRISCVTPRELLO12I = 30 // R_RISCV_TPREL_LO12_I
|
||||
// R_RISCV_32 (debug/elf 1): the absolute 32-bit address of a symbol,
|
||||
// the R_ADDR shape a 4-byte DATA field carries. R_RISCV_64 (2) lives
|
||||
// with the DWARF fixup constants as rRISCVAbs64.
|
||||
rRISVCAbs32 = 1
|
||||
)
|
||||
|
||||
// ELFRISCVObject returns the image as an ELF64 relocatable object file for
|
||||
// RISC-V (EM_RISCV, 64-bit, little-endian). The structure mirrors the amd64
|
||||
// ELF emission: .text, .data, .symtab, .strtab and optional .rela.text.
|
||||
// ELF emission: .text, .data, .symtab, .strtab, an optional .rela.text and
|
||||
// an optional .rela.data.
|
||||
func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
@@ -83,9 +95,14 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
// Build relocations. Each SB reference is an AUIPC + second-instruction
|
||||
// pair carrying a single relocation kind; the ELF writer expands it into
|
||||
// the R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I/S pair the psABI expects.
|
||||
// The HI20 carries the symbol addend; the LO12 addend is zero, matching
|
||||
// cmd/link's own ELF conversion (the LO12 resolves against the HI20's
|
||||
// AUIPC location).
|
||||
// The HI20 carries the symbol and its addend. The LO12's symbol must
|
||||
// denote the AUIPC site the HI20 relocates (psABI §8.4.9: the pair is
|
||||
// resolved against the label of the AUIPC, not the target symbol;
|
||||
// cmd/link generates one local text symbol per AUIPC for exactly this,
|
||||
// cmd/link/internal/riscv64/asm.go). The .text section symbol with the
|
||||
// AUIPC's section-relative offset as addend gives S + A = the AUIPC
|
||||
// address, which is that label.
|
||||
const secSymText = 1 // syms[1], the .text section symbol
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
@@ -99,27 +116,79 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
auipc := int64(fn.Offset + r.Off)
|
||||
switch r.Kind {
|
||||
case RelRISCVPCRELIType:
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: idx, addend: 0},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12I, sym: secSymText, addend: auipc},
|
||||
)
|
||||
case RelRISCVPCRELSType:
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVPCRELHI20, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: idx, addend: 0},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVPCRELLO12S, sym: secSymText, addend: auipc},
|
||||
)
|
||||
case RelRISCVJal:
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, sym: idx, addend: r.Addend})
|
||||
case RelPCRelAbs:
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCV32, sym: idx, addend: r.Addend})
|
||||
case RelRISCVTLSLE:
|
||||
// The local-exec pair splits into the TPREL HI20 on the LUI
|
||||
// and the TPREL LO12_I on the ADDIW, both against the symbol
|
||||
// (a thread offset, not PC-relative, so the LO12 needs no
|
||||
// label indirection).
|
||||
relas = append(relas,
|
||||
elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVTPRELHI20, sym: idx, addend: r.Addend},
|
||||
elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVTPRELLO12I, sym: idx, addend: r.Addend},
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The data symbols' symbol-valued DATA fields ("DATA s+0(SB)/8,
|
||||
// $other(SB)") become .rela.data entries: an absolute relocation of the
|
||||
// DATA line's width at the field's data-section offset, S + A with no
|
||||
// PC term. Widths 4 and 8 have ELF relocation shapes; narrower fields
|
||||
// cannot hold an address, so they are refused rather than truncated.
|
||||
var dataRelas []elfRela
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("data relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch r.Siz {
|
||||
case 8:
|
||||
typ = rRISCVAbs64
|
||||
case 4:
|
||||
typ = rRISVCAbs32
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: a symbol value of width %d has no ELF relocation", d.Name, r.Siz)
|
||||
}
|
||||
dataRelas = append(dataRelas, elfRela{
|
||||
off: uint64(d.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are code relocations,
|
||||
// .rela.data only when a DATA line holds a symbol value.
|
||||
hasRela := len(relas) > 0
|
||||
hasDataRela := len(dataRelas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections++
|
||||
}
|
||||
if hasDataRela {
|
||||
nSections++
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// String tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
@@ -129,18 +198,13 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
@@ -175,7 +239,7 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
var relaOff, relaDataOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
@@ -187,6 +251,17 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
if hasDataRela {
|
||||
align(8)
|
||||
relaDataOff = len(out)
|
||||
for _, r := range dataRelas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
@@ -196,9 +271,25 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, "gasm.s", symIdx, dwAlign)
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiRISCV64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
nSections += 4
|
||||
// Five DWARF sections: .debug_abbrev, .debug_info, .debug_line,
|
||||
// .debug_line_str and .debug_frame (the CIE is unconditional, so
|
||||
// the frame section is always present), plus the relocation
|
||||
// sections below when they carry entries.
|
||||
dwarfStart = nSections
|
||||
nSections += 5
|
||||
appendDWARFRelas(&out, dw, rRISCVAbs64, dwAlign)
|
||||
if dw.infoRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.lineRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
if dw.frameRelaCount > 0 {
|
||||
nSections++
|
||||
}
|
||||
}
|
||||
|
||||
align(8)
|
||||
@@ -226,14 +317,41 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
if hasDataRela {
|
||||
putSh(".rela.data", shtRela, 0, relaDataOff, 24*len(dataRelas), secSymtab, secData, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
// DWARF section headers; their indices follow the write order.
|
||||
if dw != nil {
|
||||
// secIdx is a running section index: each putSh below emits the
|
||||
// next header, and the sh_info of a .rela section names the index
|
||||
// of the section it relocates.
|
||||
secIdx := dwarfStart
|
||||
putSh(".debug_abbrev", shtProgbits, 0, dw.abbrevOff, dw.abbrevSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
putSh(".debug_info", shtProgbits, 0, dw.infoOff, dw.infoSize, 0, 0, 1, 0)
|
||||
secInfoIdx := secIdx
|
||||
secIdx++
|
||||
if dw.infoRelaCount > 0 {
|
||||
putSh(".rela.debug_info", shtRela, 0, dw.infoRelaOff, 24*dw.infoRelaCount, secSymtab, secInfoIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line", shtProgbits, 0, dw.lineOff, dw.lineSize, 0, 0, 1, 0)
|
||||
secLineIdx := secIdx
|
||||
secIdx++
|
||||
if dw.lineRelaCount > 0 {
|
||||
putSh(".rela.debug_line", shtRela, 0, dw.lineRelaOff, 24*dw.lineRelaCount, secSymtab, secLineIdx, 8, 24)
|
||||
secIdx++
|
||||
}
|
||||
putSh(".debug_line_str", shtProgbits, 0, dw.lineStrOff, dw.lineStrSize, 0, 0, 1, 0)
|
||||
secIdx++
|
||||
if dw.frameSize > 0 {
|
||||
putSh(".debug_frame", shtProgbits, 0, dw.frameOff, dw.frameSize, 0, 0, 8, 0)
|
||||
secFrameIdx := secIdx
|
||||
secIdx++
|
||||
if dw.frameRelaCount > 0 {
|
||||
putSh(".rela.debug_frame", shtRela, 0, dw.frameRelaOff, 24*dw.frameRelaCount, secSymtab, secFrameIdx, 8, 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +364,7 @@ func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint32(hdr[48:], efRISCVFloatAbiDouble)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestELFRISCVObjectDataRelocation checks that a symbol-valued DATA field
|
||||
// ("DATA s+0(SB)/8, $other(SB)") reaches the RISC-V ELF object as a
|
||||
// .rela.data entry: an R_RISCV_64 (R_RISCV_32 for a width-4 field) at the
|
||||
// field's offset within .data, against the named symbol, external targets
|
||||
// included.
|
||||
func TestELFRISCVObjectDataRelocation(t *testing.T) {
|
||||
f, errs := parser.Parse("t_riscv64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $32
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+8(SB)/8, $holder(SB)
|
||||
DATA holder+16(SB)/8, $extvar(SB)
|
||||
DATA holder+24(SB)/4, $Keep(SB)
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileRISCV(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileRISCV: %v", err)
|
||||
}
|
||||
obj, err := img.ELFRISCVObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFRISCVObject: %v", err)
|
||||
}
|
||||
checkELFSectionAccounting(t, obj)
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
relaData := ef.Section(".rela.data")
|
||||
if relaData == nil {
|
||||
t.Fatal("missing .rela.data section")
|
||||
}
|
||||
if relaData.Type != elf.SHT_RELA {
|
||||
t.Errorf(".rela.data type = %v, want SHT_RELA", relaData.Type)
|
||||
}
|
||||
if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" {
|
||||
t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link)
|
||||
}
|
||||
if ef.Sections[relaData.Info].Name != ".data" {
|
||||
t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info)
|
||||
}
|
||||
relas, err := relaData.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
for i := 0; i+24 <= len(relas); i += 24 {
|
||||
got = append(got, struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}{
|
||||
off: binary.LittleEndian.Uint64(relas[i:]),
|
||||
// r_info packs the type in the low dword and the symbol index
|
||||
// in the high dword.
|
||||
typ: binary.LittleEndian.Uint32(relas[i+8:]),
|
||||
sym: binary.LittleEndian.Uint32(relas[i+12:]),
|
||||
addend: int64(binary.LittleEndian.Uint64(relas[i+16:])),
|
||||
})
|
||||
}
|
||||
// debug/elf hides the table's null entry, so raw index s names syms[s-1].
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := func(idx uint32) string {
|
||||
if idx >= 1 && int(idx) <= len(syms) {
|
||||
return syms[idx-1].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// The offsets are data-section-relative: the field's DATA offset plus
|
||||
// the symbol's position in .data (the layout aligns each symbol to 16).
|
||||
base := uint64(0)
|
||||
for _, d := range img.DataSyms {
|
||||
if d.Name == "holder" {
|
||||
base = uint64(d.Offset)
|
||||
}
|
||||
}
|
||||
want := []struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
addend int64
|
||||
target string
|
||||
}{
|
||||
{off: base + 0, typ: uint32(elf.R_RISCV_64), addend: 5, target: "Keep"},
|
||||
{off: base + 8, typ: uint32(elf.R_RISCV_64), addend: 0, target: "holder"},
|
||||
{off: base + 16, typ: uint32(elf.R_RISCV_64), addend: 0, target: "extvar"},
|
||||
{off: base + 24, typ: uint32(elf.R_RISCV_32), addend: 0, target: "Keep"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := got[i]
|
||||
if g.off != w.off || g.typ != w.typ || g.addend != w.addend {
|
||||
t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}",
|
||||
i, g.off, g.typ, g.addend, w.off, w.typ, w.addend)
|
||||
}
|
||||
if n := name(g.sym); n != w.target {
|
||||
t.Errorf("entry %d names %q, want %q", i, n, w.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-12
@@ -16,9 +16,36 @@ import "strings"
|
||||
func Encodable(mnemonic string) bool {
|
||||
upper := strings.ToUpper(mnemonic)
|
||||
|
||||
// The corpus families first, mirroring encode()'s dispatch order: a
|
||||
// family owning the name decides encodability whatever the suffix split
|
||||
// would make of it.
|
||||
if amd64FamilyEncodable(upper) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fixed-name instructions (no size suffix).
|
||||
switch upper {
|
||||
case "RET", "NOP", "CALL", "JMP":
|
||||
case "RET", "NOP", "CALL", "JMP",
|
||||
"POPFQ", "PUSHFQ", "INT", "LDMXCSR", "STMXCSR", "CMPSD", "SHA256RNDS2",
|
||||
// The SSE compare family sharing CMPSD's predicate-last shape, the
|
||||
// far return with its stack pop, the loop family, the bank-crossing
|
||||
// MMX moves and the one-operand system controls.
|
||||
"CMPSS", "CMPPS", "CMPPD", "RETFL",
|
||||
"LOOP", "LOOPE", "LOOPNE",
|
||||
"MOVDQ2Q", "MOVQ2DQ",
|
||||
"ENDBR64", "CLWB", "TPAUSE", "UMONITOR", "UMWAIT", "RDPID", "CLDEMOTE",
|
||||
// The literal-data pseudo-ops, the accepted-and-ignored END and
|
||||
// bookkeeping statements, and the SP adjust.
|
||||
"BYTE", "WORD", "LONG", "QUAD", "END", "ADJSP", "FUNCDATA", "PCDATA":
|
||||
return true
|
||||
}
|
||||
if _, ok := sysUnaryTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseStoreOnly[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := noOperandTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := condCode(upper); ok {
|
||||
@@ -31,14 +58,17 @@ func Encodable(mnemonic string) bool {
|
||||
return false
|
||||
}
|
||||
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) ||
|
||||
base == "KMOVW" || base == "KMOVQ" {
|
||||
base == "KMOVW" || base == "KMOVQ" || base == "KMOVB" || base == "KMOVD" {
|
||||
return true
|
||||
}
|
||||
|
||||
// CMOV carries size then condition (CMOVLGT); SET carries the condition
|
||||
// alone (SETNE).
|
||||
if rest, ok := strings.CutPrefix(upper, "CMOV"); ok && len(rest) >= 2 {
|
||||
if _, ok := jccMap[rest[1:]]; ok {
|
||||
// CMOV carries size then condition (CMOVLGT), or the renderer's
|
||||
// condition alone (CMOVLE) with the width from the operand; SET carries
|
||||
// the condition alone (SETNE). cmovCondition checks the suffix exactly
|
||||
// as encodeCmov does, so a spelling like CMOVBGT is not reported
|
||||
// encodable when Encode would reject it.
|
||||
if rest, ok := strings.CutPrefix(upper, "CMOV"); ok {
|
||||
if _, _, ok := cmovCondition(rest); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -48,13 +78,28 @@ func Encodable(mnemonic string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy SSE shuffles and packed binaries dispatch on the full name.
|
||||
// Legacy SSE shuffles and packed binaries dispatch on the full name; so
|
||||
// do the imm8-controlled instructions, the lane extracts and inserts and
|
||||
// the packed integer shifts (their trailing width letters belong to the
|
||||
// mnemonic).
|
||||
if _, ok := sseShufTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseBinTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseImm3Table[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseExtractTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseInsertTable[upper]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := sseShiftImm[upper]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// The size-suffix split: retry the tables and the scalar switch on the
|
||||
// base.
|
||||
@@ -69,20 +114,32 @@ func Encodable(mnemonic string) bool {
|
||||
}
|
||||
}
|
||||
switch base2 {
|
||||
case "MOV",
|
||||
"ADD", "SUB", "AND", "OR", "XOR", "CMP",
|
||||
case "MOV", "MOVD",
|
||||
"ADD", "SUB", "AND", "OR", "XOR", "CMP", "ADC", "SBB",
|
||||
"TEST",
|
||||
"LEA",
|
||||
"INC", "DEC", "NEG", "NOT",
|
||||
"SHL", "SHR", "SAR",
|
||||
"INC", "DEC", "NEG", "NOT", "MUL", "DIV", "IDIV",
|
||||
"SHL", "SHR", "SAR", "SAL", "ROL", "ROR", "RCL", "RCR",
|
||||
"BT", "BTS", "BTR", "BTC",
|
||||
"XCHG", "CMPXCHG", "XADD", "CRC32", "ADCX", "ADOX",
|
||||
"MOVS", "STOS",
|
||||
"IMUL", "IMUL3",
|
||||
"PUSH", "POP",
|
||||
"BSF", "BSR", "LZCNT", "TZCNT", "POPCNT",
|
||||
"BSWAP",
|
||||
"PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2",
|
||||
"MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||
"MOVBWZX", "MOVBWSX", "MOVBLSX", "MOVBQSX", "MOVWQSX", "MOVLQZX",
|
||||
"CVTSL2SD", "CVTSQ2SD",
|
||||
"MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
"CVTSD2S", "CVTTSD2S", "CVTSS2S", "CVTTSS2S",
|
||||
"FMOVD",
|
||||
"MOVOU", "MOVO", "MOVOA", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
return true
|
||||
}
|
||||
// Full-name dispatches the size split would eat (a trailing width
|
||||
// letter that is part of the mnemonic).
|
||||
switch upper {
|
||||
case "PMOVMSKB":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
+521
-17
@@ -5,6 +5,8 @@ package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -21,6 +23,39 @@ func Encode(mnemonic string, ops ...Operand) ([]byte, error) {
|
||||
type enc struct {
|
||||
out []byte
|
||||
patches []encPatch // disp32 fields awaiting static-symbol resolution
|
||||
|
||||
// FloatPool collects the pooled constants the floating-point
|
||||
// immediates reference, in first-use order.
|
||||
floatPool []floatPoolEntry
|
||||
floatPoolSeen map[string]bool
|
||||
}
|
||||
|
||||
// floatPoolEntry is one pooled floating-point constant: the symbol name
|
||||
// the emitted RIP-relative load refers to and its IEEE-754 bytes.
|
||||
type floatPoolEntry struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
// addFloatPool records a pooled constant, deduplicated by symbol name.
|
||||
func (e *enc) addFloatPool(name string, bits uint64, width int) {
|
||||
if e.floatPoolSeen == nil {
|
||||
e.floatPoolSeen = map[string]bool{}
|
||||
}
|
||||
if e.floatPoolSeen[name] {
|
||||
return
|
||||
}
|
||||
e.floatPoolSeen[name] = true
|
||||
data := make([]byte, width)
|
||||
for i := range width {
|
||||
data[i] = byte(bits >> (8 * i))
|
||||
}
|
||||
e.floatPool = append(e.floatPool, floatPoolEntry{name: name, data: data})
|
||||
}
|
||||
|
||||
// floatPoolList returns the pooled constants in first-use order.
|
||||
func (e *enc) floatPoolList() []floatPoolEntry {
|
||||
return e.floatPool
|
||||
}
|
||||
|
||||
// encPatch marks a 4-byte displacement field in enc.out that must receive the
|
||||
@@ -29,25 +64,172 @@ type encPatch struct {
|
||||
off int
|
||||
name string
|
||||
addend int64
|
||||
tls bool // a TLS slot offset: the patch is R_TLSLE with no symbol
|
||||
}
|
||||
|
||||
func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
upper := strings.ToUpper(mnem)
|
||||
|
||||
// The corpus families (x87, the system and string controls, XSAVE)
|
||||
// dispatch on the full name from the amd64 family files before the
|
||||
// fixed-name switch, the way their tables spell the mnemonics out.
|
||||
if handled, err := e.encodeAmd64Family(upper, ops); handled {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fixed-name instructions (no size suffix).
|
||||
switch {
|
||||
case upper == "RET":
|
||||
// RET sym(SB), the absolute return: the toolchain encodes it as a
|
||||
// tail jump, E9 rel32 with a call relocation against the symbol.
|
||||
if len(ops) == 1 {
|
||||
if m, ok := ops[0].(sbMem); ok {
|
||||
return e.emit(&instr{opcode: []byte{0xE9}, modrm: -1, sib: -1, disp: le32(0), sb: &sbRef{name: m.name, addend: m.addend}})
|
||||
}
|
||||
return fmt.Errorf("RET: unsupported operand")
|
||||
}
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("RET expects no operands, got %d", len(ops))
|
||||
}
|
||||
return e.encodeRet()
|
||||
case upper == "NOP":
|
||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||
case upper == "CALL":
|
||||
return e.encodeJmpRel(ops, []byte{0xE8})
|
||||
case upper == "JMP":
|
||||
return e.encodeJmpRel(ops, []byte{0xE9})
|
||||
// The toolchain consumes every NOP statement as a pseudo and emits
|
||||
// nothing for it, operands included (a bare NOP, NOP AX and
|
||||
// NOP sym(SB) all vanish from the object).
|
||||
return nil
|
||||
case upper == "CALL" || upper == "JMP":
|
||||
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||
// Anything else is a rel32 against a label resolved by the assembler.
|
||||
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 {
|
||||
return e.encodeJcc(cc, ops)
|
||||
}
|
||||
// No-operand system and string-control instructions (CPUID, RDTSC,
|
||||
// SYSCALL, the fences, UNDEF, …).
|
||||
if op, ok := noOperandTable[upper]; ok {
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("%s takes no operands, got %d", upper, len(ops))
|
||||
}
|
||||
return e.emit(&instr{opcode: op, modrm: -1, sib: -1})
|
||||
}
|
||||
// One-operand system instructions whose reg field is a fixed digit:
|
||||
// the cache and wait controls under 0F AE/0F 1C and the RDPID read.
|
||||
if m, ok := sysUnaryTable[upper]; ok {
|
||||
return e.encodeSysUnary(upper, m, ops)
|
||||
}
|
||||
// The store-only SSE moves (the non-temporal store).
|
||||
if m, ok := sseStoreOnly[upper]; ok {
|
||||
return e.encodeSSEStoreOnly(upper, m, ops)
|
||||
}
|
||||
// POPFQ/PUSHFQ are exact names: the bare POPF/PUSHF and the L spellings
|
||||
// are rejected by go tool asm in 64-bit mode, so they stay unsupported.
|
||||
switch upper {
|
||||
case "POPFQ":
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("POPFQ takes no operands, got %d", len(ops))
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0x9D}, modrm: -1, sib: -1})
|
||||
case "PUSHFQ":
|
||||
if len(ops) != 0 {
|
||||
return fmt.Errorf("PUSHFQ takes no operands, got %d", len(ops))
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0x9C}, modrm: -1, sib: -1})
|
||||
case "INT":
|
||||
return e.encodeInt(ops)
|
||||
// The LOOP family outside the assembler's label settlement: the operand
|
||||
// is the already-computed rel8 (E0-E2).
|
||||
case "LOOP", "LOOPE", "LOOPNE":
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", upper, len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok || !fits8(int64(imm)) {
|
||||
return fmt.Errorf("%s: relative offset must be a signed byte", upper)
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{loopOpcode(upper)}, modrm: -1, sib: -1, imm: []byte{byte(int8(imm))}})
|
||||
case "LDMXCSR":
|
||||
return e.encodeMxcsr(2, ops)
|
||||
case "STMXCSR":
|
||||
return e.encodeMxcsr(3, ops)
|
||||
// CMPSD is the scalar double compare, whose predicate immediate comes
|
||||
// LAST in Plan 9 order (src, dst, $imm); the family shares the shape.
|
||||
case "CMPSD":
|
||||
return e.encodeSSECmp("CMPSD", 0xF2, ops)
|
||||
case "CMPSS":
|
||||
return e.encodeSSECmp("CMPSS", 0xF3, ops)
|
||||
case "CMPPS":
|
||||
return e.encodeSSECmp("CMPPS", 0x00, ops)
|
||||
case "CMPPD":
|
||||
return e.encodeSSECmp("CMPPD", 0x66, ops)
|
||||
// RETFL pops the immediate's worth of bytes after the far return
|
||||
// (LRET iw: CA imm16), the toolchain's RETF spelling with a stack
|
||||
// adjustment.
|
||||
case "RETFL":
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("RETFL expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("RETFL expects an immediate")
|
||||
}
|
||||
return e.emit(&instr{opcode: []byte{0xCA}, modrm: -1, sib: -1, imm: le16(int64(imm))})
|
||||
// MOVDQ2Q/MOVQ2DQ cross the MMX and XMM banks, and each direction
|
||||
// carries its own mandatory prefix: the toolchain renders F3 0F D6 as
|
||||
// MOVQ2DQ (the MMX source, XMM destination) and F2 0F D6 as MOVDQ2Q
|
||||
// (the XMM source, MMX destination), the register in the reg field, the
|
||||
// other bank's in r/m.
|
||||
case "MOVDQ2Q", "MOVQ2DQ":
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
srcReg, ok1 := ops[0].(Reg)
|
||||
dstReg, ok2 := ops[1].(Reg)
|
||||
if !ok1 || !ok2 {
|
||||
return fmt.Errorf("%s takes register operands alone", upper)
|
||||
}
|
||||
if upper == "MOVDQ2Q" && (!srcReg.isVec() || !dstReg.mmx) ||
|
||||
upper == "MOVQ2DQ" && (!srcReg.mmx || !dstReg.isVec()) {
|
||||
return fmt.Errorf("%s crosses the XMM and MMX banks in that order", upper)
|
||||
}
|
||||
prefix := byte(0xF2)
|
||||
if upper == "MOVQ2DQ" {
|
||||
prefix = 0xF3
|
||||
}
|
||||
i := &instr{prefix: prefix, opcode: []byte{0x0F, 0xD6}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dstReg, srcReg, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
// SHA256RNDS2 carries the round constant in a literal X0 first operand.
|
||||
case "SHA256RNDS2":
|
||||
return e.encodeSha256rnds2(ops)
|
||||
// BYTE, WORD, LONG and QUAD write the immediate into the text stream
|
||||
// itself: 1, 2, 4 or 8 literal bytes, little-endian. END is accepted
|
||||
// and ignored. ADJSP adjusts SP by the immediate, sign-chosen between
|
||||
// the SUBQ and ADDQ forms.
|
||||
case "BYTE", "WORD", "LONG", "QUAD":
|
||||
return e.encodeData(upper, ops)
|
||||
case "END":
|
||||
return e.encodeEnd(ops)
|
||||
case "ADJSP":
|
||||
return e.encodeAdjsp(ops)
|
||||
// The runtime's bookkeeping statements carry no text bytes: go tool asm
|
||||
// records FUNCDATA and PCDATA in the program list only, so the encoded
|
||||
// body shows nothing, on every architecture.
|
||||
case "FUNCDATA", "PCDATA":
|
||||
return e.encodeFuncdata(upper, ops)
|
||||
}
|
||||
|
||||
// VEX (AVX/AVX2) and EVEX (AVX-512) instructions: the trailing
|
||||
// B/W/L/Q/D is part of the mnemonic, not a size suffix, so dispatch
|
||||
@@ -57,7 +239,9 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) || base == "KMOVW" || base == "KMOVQ" {
|
||||
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) ||
|
||||
isEvexPrefGather(base) ||
|
||||
base == "KMOVW" || base == "KMOVQ" || base == "KMOVB" || base == "KMOVD" {
|
||||
return e.encodeVec(base, ops, sfx)
|
||||
}
|
||||
if sfx.any() {
|
||||
@@ -73,6 +257,19 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
}
|
||||
|
||||
base, size := splitSize(upper)
|
||||
// The scalar families that own byte forms settle the width against the
|
||||
// register operands here, before the unsuffixed 64-bit default applies,
|
||||
// so a literal Q suffix stays distinguishable from no suffix at all
|
||||
// (CRC32 DL, R11 is the byte form; CRC32Q DL, R11 is a conflict).
|
||||
if byteFormBase[base] {
|
||||
w, err := operandWidth(mnem, size, widthOperands(base, ops))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w != 0 {
|
||||
size = w
|
||||
}
|
||||
}
|
||||
if size == 0 {
|
||||
size = 8 // default operand size in 64-bit mode (e.g. PUSHQ)
|
||||
}
|
||||
@@ -84,43 +281,104 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
}
|
||||
// Legacy SSE packed binaries dispatch on the full name: the packed
|
||||
// integer mnemonics carry real width suffixes (PADDB/PCMPGTW/...),
|
||||
// which the size split must not eat.
|
||||
// which the size split must not eat. A floating-point immediate
|
||||
// rewrites into a pooled-constant read on the scalar members.
|
||||
if m, ok := sseBinTable[upper]; ok {
|
||||
if f, isFloat := floatImmOperand(ops); isFloat {
|
||||
return e.encodeSSEFloatBin(upper, m, f, ops)
|
||||
}
|
||||
return e.encodeSSEBin(m, ops)
|
||||
}
|
||||
if m, ok := sseBinTable[base]; ok {
|
||||
if f, isFloat := floatImmOperand(ops); isFloat {
|
||||
return e.encodeSSEFloatBin(upper, m, f, ops)
|
||||
}
|
||||
return e.encodeSSEBin(m, ops)
|
||||
}
|
||||
// The imm8-controlled legacy instructions, the lane extracts and inserts
|
||||
// and the packed integer shifts all dispatch on the full name: a trailing
|
||||
// width letter here belongs to the mnemonic, not to the size split.
|
||||
if m, ok := sseImm3Table[upper]; ok {
|
||||
return e.encodeSSEImm3(m, ops)
|
||||
}
|
||||
if m, ok := sseExtractTable[upper]; ok {
|
||||
return e.encodeSSEExtract(m, ops)
|
||||
}
|
||||
if m, ok := sseInsertTable[upper]; ok {
|
||||
return e.encodeSSEInsert(m, ops)
|
||||
}
|
||||
if _, ok := sseShiftImm[upper]; ok {
|
||||
return e.encodeSSEShift(upper, ops)
|
||||
}
|
||||
// PMOVMSKB ends in a width letter the size split would eat, so it
|
||||
// dispatches on the full name like the packed binaries above.
|
||||
if upper == "PMOVMSKB" {
|
||||
return e.encodePmovmskb(upper, ops)
|
||||
}
|
||||
switch base {
|
||||
case "MOV":
|
||||
return e.encodeMov(ops, size)
|
||||
case "ADD", "SUB", "AND", "OR", "XOR", "CMP":
|
||||
// MOVD is the Go assembler's alias of MOVQ: the same byte forms, 64-bit
|
||||
// REX.W and all. The alias takes no byte register either, the same
|
||||
// conflict rule the Q-suffixed spelling answers to.
|
||||
case "MOVD":
|
||||
if _, err := operandWidth(mnem, 8, ops); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.encodeMov(ops, 8)
|
||||
case "ADD", "SUB", "AND", "OR", "XOR", "CMP", "ADC", "SBB":
|
||||
return e.encodeALU(aluOp[base], ops, size)
|
||||
case "TEST":
|
||||
return e.encodeTest(ops, size)
|
||||
case "LEA":
|
||||
return e.encodeLea(ops, size)
|
||||
case "INC", "DEC", "NEG", "NOT":
|
||||
case "INC", "DEC", "NEG", "NOT", "MUL", "DIV", "IDIV":
|
||||
return e.encodeUnary(unaryOp[base], ops, size)
|
||||
case "SHL", "SHR", "SAR":
|
||||
return e.encodeShift(shiftOp[base], ops, size)
|
||||
case "SHL", "SHR", "SAR", "SAL", "ROL", "ROR", "RCL", "RCR":
|
||||
return e.encodeShift(base, ops, size)
|
||||
case "BT", "BTS", "BTR", "BTC":
|
||||
return e.encodeBitTest(base, ops, size)
|
||||
case "XCHG":
|
||||
return e.encodeExchange(ops, size)
|
||||
case "CMPXCHG":
|
||||
return e.encodeRegRegOp(0xB0, 0xB1, base, ops, size)
|
||||
case "XADD":
|
||||
return e.encodeRegRegOp(0xC0, 0xC1, base, ops, size)
|
||||
case "CRC32":
|
||||
return e.encodeCrc32(ops, size)
|
||||
case "ADCX":
|
||||
return e.encodeCarryExt(0x66, ops, size)
|
||||
case "ADOX":
|
||||
return e.encodeCarryExt(0xF3, ops, size)
|
||||
case "MOVS", "STOS":
|
||||
return e.encodeStringOp(base, ops, size)
|
||||
case "IMUL", "IMUL3":
|
||||
return e.encodeImul(ops, size)
|
||||
case "PUSH":
|
||||
return e.encodePushPop(ops, true)
|
||||
return e.encodePushPop(ops, size, true)
|
||||
case "POP":
|
||||
return e.encodePushPop(ops, false)
|
||||
return e.encodePushPop(ops, size, false)
|
||||
case "BSF", "BSR", "LZCNT", "TZCNT", "POPCNT":
|
||||
return e.encodeCount(base, ops, size)
|
||||
case "BSWAP":
|
||||
return e.encodeBswap(ops, size)
|
||||
case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2":
|
||||
return e.encodePrefetch(base, ops)
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX":
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX",
|
||||
"MOVBWZX", "MOVBWSX", "MOVBLSX", "MOVBQSX", "MOVWQSX", "MOVLQZX":
|
||||
return e.encodeMovExtend(base, ops)
|
||||
case "CVTSL2SD", "CVTSQ2SD":
|
||||
return e.encodeCvtsi2sd(base == "CVTSQ2SD", ops)
|
||||
case "MOVOU", "MOVO", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD", "MOVSD", "MOVSS":
|
||||
case "CVTSD2S", "CVTTSD2S", "CVTSS2S", "CVTTSS2S":
|
||||
return e.encodeCvtInt(base, ops, size)
|
||||
case "FMOVD":
|
||||
return e.encodeFmov(ops)
|
||||
case "MOVSD", "MOVSS":
|
||||
if f, isFloat := floatImmOperand(ops); isFloat {
|
||||
return e.encodeSSEFloatMove(upper, f, ops)
|
||||
}
|
||||
return e.encodeSSEMove(sseMoveTable[base], ops)
|
||||
case "MOVOU", "MOVO", "MOVOA", "MOVUPS", "MOVAPS", "MOVUPD", "MOVAPD":
|
||||
return e.encodeSSEMove(sseMoveTable[base], ops)
|
||||
}
|
||||
return fmt.Errorf("unsupported instruction %q", mnem)
|
||||
@@ -150,6 +408,214 @@ var prefetchVariant = map[string]int{
|
||||
"PREFETCHT2": 3,
|
||||
}
|
||||
|
||||
// dataWidth is the literal byte count of each data-emission pseudo-op.
|
||||
var dataWidth = map[string]int{
|
||||
"BYTE": 1,
|
||||
"WORD": 2,
|
||||
"LONG": 4,
|
||||
"QUAD": 8,
|
||||
}
|
||||
|
||||
// encodeData emits the literal-data pseudo-ops: BYTE, WORD, LONG and QUAD
|
||||
// write the immediate into the text stream as 1, 2, 4 or 8 bytes,
|
||||
// little-endian, with no opcode lookup. The value is truncated to the
|
||||
// width rather than range-checked, exactly as go tool asm behaves (BYTE
|
||||
// $0x1FF emits FF, WORD $0x12345 emits 45 23, both without an error), and
|
||||
// exactly one immediate is accepted: the toolchain rejects a list such as
|
||||
// BYTE $1, $2, $3.
|
||||
func (e *enc) encodeData(mnem string, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 immediate operand, got %d", mnem, len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s requires an integer immediate", mnem)
|
||||
}
|
||||
width := dataWidth[mnem]
|
||||
out := make([]byte, width)
|
||||
u := uint64(imm)
|
||||
for i := range width {
|
||||
out[i] = byte(u >> (8 * i))
|
||||
}
|
||||
e.out = append(e.out, out...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeFuncdata accepts-and-ignores the runtime bookkeeping statements:
|
||||
// FUNCDATA $n, sym(SB) and PCDATA $n, $m. go tool asm emits no text bytes
|
||||
// for either (the entries live in the object's ancillary tables, not the
|
||||
// function body), and the operand shapes it takes are exactly these: an
|
||||
// integer count first, then a symbol reference for FUNCDATA and an integer
|
||||
// value for PCDATA. The other architectures accept-and-ignore the same
|
||||
// statements; amd64 now matches.
|
||||
func (e *enc) encodeFuncdata(upper string, ops []Operand) error {
|
||||
if len(ops) != 2 {
|
||||
return fmt.Errorf("%s expects 2 operands, got %d", upper, len(ops))
|
||||
}
|
||||
if _, ok := ops[0].(Imm); !ok {
|
||||
return fmt.Errorf("%s: first operand must be an integer immediate", upper)
|
||||
}
|
||||
switch upper {
|
||||
case "FUNCDATA":
|
||||
if _, ok := ops[1].(sbMem); !ok {
|
||||
return fmt.Errorf("FUNCDATA: second operand must be a symbol reference")
|
||||
}
|
||||
case "PCDATA":
|
||||
if _, ok := ops[1].(Imm); !ok {
|
||||
return fmt.Errorf("PCDATA: second operand must be an integer immediate")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeEnd accepts-and-ignores END. go tool asm drops the statement
|
||||
// entirely: the AEND Prog is skipped when the program list is flushed, so
|
||||
// the statements after an END still belong to the same function and the
|
||||
// encoded body carries no trace of it, whatever operands follow the name
|
||||
// (the toolchain takes END $0 and END AX alike). Zero bytes, no effect.
|
||||
func (e *enc) encodeEnd(ops []Operand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeAdjsp emits ADJSP $imm: a positive value is SUBQ $imm, SP, a
|
||||
// negative one ADDQ $-imm, SP, in the imm8 or imm32 form the magnitude
|
||||
// picks (the same selection subSP and addSP make for the frame). go tool
|
||||
// asm refuses ADJSP $0 outright, so a zero value is an error here too; the
|
||||
// statement's effect on the SP balance is checked by the function-level
|
||||
// assembly (checkAdjspBalance), as the toolchain's push/pop walk does.
|
||||
func (e *enc) encodeAdjsp(ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("ADJSP expects 1 immediate operand, got %d", len(ops))
|
||||
}
|
||||
imm, ok := ops[0].(Imm)
|
||||
if !ok {
|
||||
return fmt.Errorf("ADJSP requires an integer immediate")
|
||||
}
|
||||
switch v := int(imm); {
|
||||
case v > 0:
|
||||
e.out = append(e.out, subSP(v)...)
|
||||
case v < 0:
|
||||
e.out = append(e.out, addSP(-v)...)
|
||||
default:
|
||||
return fmt.Errorf("ADJSP $0 has no encoding")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- floating-point immediates ----------------------------------------------
|
||||
|
||||
// sseFloatImm lists the mnemonics whose first operand may be a floating-point
|
||||
// immediate, the set go tool asm rewrites into a pooled-constant read: the
|
||||
// scalar moves, the four scalar arithmetic pairs and the scalar compares.
|
||||
// The packed members and the uniform forms (MAXSD, MINSD, SQRTSD, CMPSD)
|
||||
// reject the immediate in the toolchain and are absent here on purpose.
|
||||
var sseFloatImm = map[string]bool{
|
||||
"MOVSD": true, "MOVSS": true,
|
||||
"ADDSD": true, "ADDSS": true,
|
||||
"SUBSD": true, "SUBSS": true,
|
||||
"MULSD": true, "MULSS": true,
|
||||
"DIVSD": true, "DIVSS": true,
|
||||
"COMISD": true, "COMISS": true,
|
||||
"UCOMISD": true, "UCOMISS": true,
|
||||
}
|
||||
|
||||
// floatImmOperand reports whether the operand list opens with a
|
||||
// floating-point immediate in the two-operand spelling (imm, dst).
|
||||
func floatImmOperand(ops []Operand) (FloatImm, bool) {
|
||||
if len(ops) != 2 {
|
||||
return FloatImm{}, false
|
||||
}
|
||||
f, ok := ops[0].(FloatImm)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
// floatPoolValue evaluates a floating-point immediate at the width its
|
||||
// mnemonic encodes and names the pool constant the toolchain synthesises:
|
||||
// $f64.<16 hex> for the doubles, $f32.<8 hex> for the singles (the float32
|
||||
// rounding of the parsed value). The name carries the IEEE-754 bits; the
|
||||
// section holds them little-endian.
|
||||
func floatPoolValue(mnem string, f FloatImm) (bits uint64, name string, err error) {
|
||||
v, err := strconv.ParseFloat(f.Text, 64)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("invalid floating-point immediate %q", f.Text)
|
||||
}
|
||||
if f.Neg {
|
||||
v = -v
|
||||
}
|
||||
if strings.HasSuffix(mnem, "D") {
|
||||
bits = math.Float64bits(v)
|
||||
return bits, fmt.Sprintf("$f64.%016x", bits), nil
|
||||
}
|
||||
bits = uint64(math.Float32bits(float32(v)))
|
||||
return bits, fmt.Sprintf("$f32.%08x", bits), nil
|
||||
}
|
||||
|
||||
// encodeSSEFloatMove encodes MOVSD/MOVSS with a floating-point immediate
|
||||
// source. A positive zero needs no memory read: the toolchain emits
|
||||
// XORPS dst, dst. Anything else loads the pooled constant RIP-relative
|
||||
// ($f64.<hex>(SB) / $f32.<hex>(SB)), the displacement a patch site the
|
||||
// file-level layout or the linker resolves.
|
||||
func (e *enc) encodeSSEFloatMove(mnem string, f FloatImm, ops []Operand) error {
|
||||
if !sseFloatImm[mnem] {
|
||||
return fmt.Errorf("%s does not take a floating-point immediate", mnem)
|
||||
}
|
||||
dst, ok := ops[1].(Reg)
|
||||
if !ok || !dst.isVec() {
|
||||
return fmt.Errorf("%s: destination must be a vector register", mnem)
|
||||
}
|
||||
bits, name, err := floatPoolValue(mnem, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.addFloatPool(name, bits, mwidth(mnem))
|
||||
if bits == 0 {
|
||||
i := &instr{opcode: []byte{0x0F, 0x57}, modrm: -1, sib: -1} // XORPS
|
||||
if err := setRM(i, dst, dst, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
m := sseMoveTable[mnem]
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.load}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dst, sbMem{size: mwidth(mnem), name: name}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// encodeSSEFloatBin encodes the scalar arithmetic and compare mnemonics with
|
||||
// a floating-point immediate source: the constant is read from the pool into
|
||||
// the instruction's r/m side (reg = destination), the rewrite go tool asm
|
||||
// performs at the source level.
|
||||
func (e *enc) encodeSSEFloatBin(mnem string, m sseBin, f FloatImm, ops []Operand) error {
|
||||
if !sseFloatImm[mnem] {
|
||||
return fmt.Errorf("%s does not take a floating-point immediate", mnem)
|
||||
}
|
||||
dst, ok := ops[1].(Reg)
|
||||
if !ok || !dst.isVec() {
|
||||
return fmt.Errorf("%s: destination must be a vector register", mnem)
|
||||
}
|
||||
bits, name, err := floatPoolValue(mnem, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.addFloatPool(name, bits, mwidth(mnem))
|
||||
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, m.op}, modrm: -1, sib: -1}
|
||||
if err := setRM(i, dst, sbMem{size: mwidth(mnem), name: name}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// mwidth returns the operand width a scalar SSE mnemonic encodes: the double
|
||||
// spellings end in D, the single spellings in S.
|
||||
func mwidth(mnem string) int {
|
||||
if strings.HasSuffix(mnem, "D") {
|
||||
return 8
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
// splitSize separates a trailing B/W/L/Q size suffix from the mnemonic.
|
||||
func splitSize(upper string) (base string, size int) {
|
||||
if upper == "" {
|
||||
@@ -179,7 +645,7 @@ func (e *enc) encodeVec(upper string, ops []Operand, sfx evexSuffix) error {
|
||||
if ss, ok := scatterTable[upper]; ok {
|
||||
return e.encodeScatter(upper, ss, ops, sfx)
|
||||
}
|
||||
if upper == "KMOVW" || upper == "KMOVQ" {
|
||||
if upper == "KMOVW" || upper == "KMOVQ" || upper == "KMOVB" || upper == "KMOVD" {
|
||||
if sfx.any() {
|
||||
return fmt.Errorf("%s takes no EVEX suffixes", upper)
|
||||
}
|
||||
@@ -216,6 +682,7 @@ type instr struct {
|
||||
disp []byte
|
||||
imm []byte
|
||||
sb *sbRef // static-symbol displacement in disp, awaiting resolution
|
||||
tls bool // the displacement is a TLS slot offset, patched R_TLSLE
|
||||
}
|
||||
|
||||
// sbRef records that an instruction's displacement refers to a static symbol
|
||||
@@ -258,6 +725,9 @@ func (e *enc) emit(i *instr) error {
|
||||
if i.sb != nil {
|
||||
e.patches = append(e.patches, encPatch{off: len(e.out), name: i.sb.name, addend: i.sb.addend})
|
||||
}
|
||||
if i.tls {
|
||||
e.patches = append(e.patches, encPatch{off: len(e.out), tls: true})
|
||||
}
|
||||
e.out = append(e.out, i.disp...)
|
||||
e.out = append(e.out, i.imm...)
|
||||
return nil
|
||||
@@ -284,7 +754,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
|
||||
// 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 {
|
||||
return setRMReg(i, digit, false, false, rm, opSize)
|
||||
}
|
||||
@@ -312,12 +782,30 @@ func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize i
|
||||
i.disp = le32(0)
|
||||
i.sb = &sbRef{name: r.name, addend: r.addend}
|
||||
return nil
|
||||
case TLSMem:
|
||||
// off(TLS): the segment-prefixed absolute access, mod=00 with the
|
||||
// SIB escape's disp32 absolute form. The displacement is the TLS
|
||||
// slot offset, patched by the linker's TLS relocation.
|
||||
i.prefix = r.Seg
|
||||
i.modrm = 0x04 | regField<<3
|
||||
i.sib = 0x25
|
||||
i.disp = le32(r.Disp)
|
||||
i.tls = true
|
||||
return nil
|
||||
case SegAbs:
|
||||
// 0x30(GS): the segment override with the SIB escape's disp32
|
||||
// absolute form, no relocation.
|
||||
setSegAbs(i, regField, r)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid r/m operand %T", rm)
|
||||
}
|
||||
}
|
||||
|
||||
func setMem(i *instr, regField int, m Mem) error {
|
||||
if m.Seg != 0 {
|
||||
i.prefix = m.Seg
|
||||
}
|
||||
modrm, sib, disp, xBit, bBit, err := memComponents(regField, m)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -330,11 +818,27 @@ func setMem(i *instr, regField int, m Mem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSegAbs assembles a segment-absolute operand, 0x30(GS): the segment
|
||||
// override with the mod=00 SIB escape's disp32 absolute form and no
|
||||
// relocation.
|
||||
func setSegAbs(i *instr, regField int, m SegAbs) {
|
||||
i.prefix = m.Seg
|
||||
i.modrm = 0x04 | regField<<3
|
||||
i.sib = 0x25
|
||||
i.disp = le32(m.Disp)
|
||||
}
|
||||
|
||||
// memComponents computes the ModR/M byte (with the given reg field), the SIB
|
||||
// byte (-1 if none), the displacement bytes, and the high index/base bits, for
|
||||
// a memory operand. It is shared by the REX (scalar) and VEX (vector) paths.
|
||||
func memComponents(regField int, m Mem) (modrm, sib int, disp []byte, xBit, bBit int, err error) {
|
||||
sib = -1
|
||||
// A displacement wider than int32 fits no encoding form; truncating it
|
||||
// would address a different location, and go tool asm reports "offset
|
||||
// too large" for the same operand.
|
||||
if m.Disp < -(1<<31) || m.Disp > (1<<31)-1 {
|
||||
return 0, -1, nil, 0, 0, fmt.Errorf("displacement %d does not fit in 32 bits", m.Disp)
|
||||
}
|
||||
// RIP-relative: neither base nor index.
|
||||
if !m.HasBase && !m.HasIndex {
|
||||
return regField<<3 | 0x05, -1, le32(m.Disp), 0, 0, nil // mod=00, rm=101
|
||||
|
||||
+944
-3
File diff suppressed because it is too large.
Load diff
+789
-137
File diff suppressed because it is too large.
Load diff
+254
-13
@@ -16,7 +16,7 @@ import (
|
||||
// kernels use: NDS arithmetic, immediate and variable shifts, shuffles with
|
||||
// an immediate, lane extracts, narrowing stores, broadcasts from a GPR or
|
||||
// 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) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -38,6 +38,15 @@ func TestEvexGroundTruth(t *testing.T) {
|
||||
{"VADDPD Z11,Z10,Z10", "VADDPD", []Operand{vreg(t, "Z11"), vreg(t, "Z10"), vreg(t, "Z10")}, "6251ad4858d3"},
|
||||
{"VMULPD Z13,Z12,Z12", "VMULPD", []Operand{vreg(t, "Z13"), vreg(t, "Z12"), vreg(t, "Z12")}, "62519d4859e5"},
|
||||
{"VFMADD231PD Z14,Z12,Z10", "VFMADD231PD", []Operand{vreg(t, "Z14"), vreg(t, "Z12"), vreg(t, "Z10")}, "62529d48b8d6"},
|
||||
// The qword OR spelling always encodes through EVEX.
|
||||
{"VPORQ Y0,Y1,Y2", "VPORQ", []Operand{vreg(t, "Y0"), vreg(t, "Y1"), vreg(t, "Y2")}, "62f1f528ebd0"},
|
||||
{"VPORQ X0,X1,X2", "VPORQ", []Operand{vreg(t, "X0"), vreg(t, "X1"), vreg(t, "X2")}, "62f1f508ebd0"},
|
||||
// Byte permute and population count.
|
||||
{"VPERMI2B X0,X1,X2", "VPERMI2B", []Operand{vreg(t, "X0"), vreg(t, "X1"), vreg(t, "X2")}, "62f2750875d0"},
|
||||
{"VPOPCNTB X0,X1", "VPOPCNTB", []Operand{vreg(t, "X0"), vreg(t, "X1")}, "62f27d0854c8"},
|
||||
{"VPOPCNTD X0,X1", "VPOPCNTD", []Operand{vreg(t, "X0"), vreg(t, "X1")}, "62f27d0855c8"},
|
||||
{"VPOPCNTD Y0,Y1", "VPOPCNTD", []Operand{vreg(t, "Y0"), vreg(t, "Y1")}, "62f27d2855c8"},
|
||||
{"VPOPCNTQ X0,X1", "VPOPCNTQ", []Operand{vreg(t, "X0"), vreg(t, "X1")}, "62f2fd0855c8"},
|
||||
// Align (NDS + imm8).
|
||||
{"VALIGND $12,Z12,Z0,Z1", "VALIGND", []Operand{Imm(12), vreg(t, "Z12"), vreg(t, "Z0"), vreg(t, "Z1")}, "62d37d4803cc0c"},
|
||||
{"VALIGND $15,Z9,Z0,Z1", "VALIGND", []Operand{Imm(15), vreg(t, "Z9"), vreg(t, "Z0"), vreg(t, "Z1")}, "62d37d4803c90f"},
|
||||
@@ -52,13 +61,23 @@ func TestEvexGroundTruth(t *testing.T) {
|
||||
{"KMOVW K1,CX", "KMOVW", []Operand{vreg(t, "K1"), CX}, "c5f893c9"},
|
||||
{"KMOVW K1,R12", "KMOVW", []Operand{vreg(t, "K1"), vreg(t, "R12")}, "c57893e1"},
|
||||
{"KTESTW K1,K1", "KTESTW", []Operand{vreg(t, "K1"), vreg(t, "K1")}, "c5f899c9"},
|
||||
{"KMOVB K1,K2", "KMOVB", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f990d1"},
|
||||
{"KMOVB AX,K1", "KMOVB", []Operand{AX, vreg(t, "K1")}, "c5f992c8"},
|
||||
{"KMOVB K1,AX", "KMOVB", []Operand{vreg(t, "K1"), AX}, "c5f993c1"},
|
||||
{"KMOVB K1,(AX)", "KMOVB", []Operand{vreg(t, "K1"), Ptr(AX, 0, 1)}, "c5f99108"},
|
||||
{"KMOVD K1,K2", "KMOVD", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c4e1f990d1"},
|
||||
{"KMOVD AX,K1", "KMOVD", []Operand{AX, vreg(t, "K1")}, "c5fb92c8"},
|
||||
{"KMOVD K1,AX", "KMOVD", []Operand{vreg(t, "K1"), AX}, "c5fb93c1"},
|
||||
{"KMOVD K1,(AX)", "KMOVD", []Operand{vreg(t, "K1"), Ptr(AX, 0, 4)}, "c4e1f99108"},
|
||||
{"KMOVB (AX),K1", "KMOVB", []Operand{Ptr(AX, 0, 1), vreg(t, "K1")}, "c5f99008"},
|
||||
{"KMOVQ (AX),K1", "KMOVQ", []Operand{Ptr(AX, 0, 8), vreg(t, "K1")}, "c4e1f89008"},
|
||||
// Moves, incl. disp8×N (64 for a 512-bit operand).
|
||||
{"VMOVDQU32 (SI)(R15*4),Z3", "VMOVDQU32", []Operand{Idx(SI, vreg(t, "R15"), 4, 0, 64), vreg(t, "Z3")}, "62b17e486f1cbe"},
|
||||
{"VMOVDQU32 4(SI)(AX*1),Z4", "VMOVDQU32", []Operand{Idx(SI, AX, 1, 4, 64), vreg(t, "Z4")}, "62f17e486fa40604000000"},
|
||||
{"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 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 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"},
|
||||
@@ -77,7 +96,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
||||
{"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"},
|
||||
{"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"},
|
||||
{"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"},
|
||||
@@ -96,7 +115,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
||||
{"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 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"},
|
||||
// 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"},
|
||||
@@ -107,7 +126,7 @@ func TestEvexGroundTruth(t *testing.T) {
|
||||
{"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 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̄).
|
||||
{"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"},
|
||||
@@ -150,7 +169,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
|
||||
// assembler.
|
||||
func TestEvexMasking(t *testing.T) {
|
||||
@@ -241,11 +260,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,
|
||||
// permutes, the wider integer families, expand/compress, broadcasts,
|
||||
// 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.
|
||||
func TestEvexExtendedGroundTruth(t *testing.T) {
|
||||
mem64 := func(base Reg) Operand { return Ptr(base, 0, 64) }
|
||||
@@ -275,7 +294,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"},
|
||||
{"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"},
|
||||
// Packed single arithmetic (same opcodes, no mandatory prefix) —
|
||||
// Packed single arithmetic (same opcodes, no mandatory prefix);
|
||||
// ZMM, YMM and XMM widths, rounding and broadcast.
|
||||
{"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"},
|
||||
@@ -399,8 +418,8 @@ func TestEvexExtendedGroundTruth(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestEvexHelperGroundTruth covers the floating-point helper and conversion
|
||||
// tail of the EVEX set — reciprocals, rsqrt, getexp/getmant, scalef,
|
||||
// rndscale, reduce, fixupimm, range, fpclass, the remaining conversions —
|
||||
// tail of the EVEX set; reciprocals, rsqrt, getexp/getmant, scalef,
|
||||
// rndscale, reduce, fixupimm, range, fpclass, the remaining conversions;
|
||||
// plus gather/scatter with VSIB addressing, byte for byte against the Go
|
||||
// assembler.
|
||||
func TestEvexHelperGroundTruth(t *testing.T) {
|
||||
@@ -502,9 +521,9 @@ func TestEvexHelperGroundTruth(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// GPRs.
|
||||
func TestEvexGprGroundTruth(t *testing.T) {
|
||||
@@ -675,6 +694,15 @@ func TestEvexErrors(t *testing.T) {
|
||||
{"align arity", "VALIGND", []Operand{Imm(1), vreg(t, "Z0"), vreg(t, "Z1")}},
|
||||
// VEX-only mnemonics reject registers only EVEX can encode.
|
||||
{"VMOVMSKPS X16", "VMOVMSKPS", []Operand{vreg(t, "X16"), AX}},
|
||||
// The scalar EVEX move matches its VEX twin and the Go assembler:
|
||||
// XMM↔memory only, never reg-reg and never a wider register (the
|
||||
// toolchain rejects every one of these shapes).
|
||||
{"VMOVSS X1,X2", "VMOVSS", []Operand{vreg(t, "X1"), vreg(t, "X2")}},
|
||||
{"VMOVSS X16,X2", "VMOVSS", []Operand{vreg(t, "X16"), vreg(t, "X2")}},
|
||||
{"VMOVSS Y1,(AX)", "VMOVSS", []Operand{vreg(t, "Y1"), Ptr(AX, 0, 4)}},
|
||||
{"VMOVSS Z1,Z2", "VMOVSS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}},
|
||||
{"VMOVSS Z1,(AX)", "VMOVSS", []Operand{vreg(t, "Z1"), Ptr(AX, 0, 4)}},
|
||||
{"VMOVSS (AX),Z2", "VMOVSS", []Operand{Ptr(AX, 0, 4), vreg(t, "Z2")}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
||||
@@ -693,3 +721,216 @@ func hexCompact(b []byte) string {
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// TestAvx512CorpusFamilies pins representative encodings of the AVX-512
|
||||
// families the toolchain's avx512enc corpus exercises: the bytes are the
|
||||
// go tool asm output for exactly these operands, and the same families are
|
||||
// covered end to end by the avx512_amd64.s differential kernel.
|
||||
func TestAvx512CorpusFamilies(t *testing.T) {
|
||||
vsib := func(base, idx string, scale int) Operand {
|
||||
return Idx(vreg(t, base), vreg(t, idx), scale, 0, 0)
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
// AES rounds (EVEX NDS, VEX twin routed by operand width).
|
||||
{"VAESDEC Z", "VAESDEC", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d48ded9"},
|
||||
// Integer VNNI and the bit algorithm group.
|
||||
{"VPDPBUSD", "VPDPBUSD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K2"), vreg(t, "Z3")}, "62f26d4a50d9"},
|
||||
{"VPOPCNTW", "VPOPCNTW", []Operand{vreg(t, "Z1"), vreg(t, "K3"), vreg(t, "Z2")}, "62f2fd4b54d1"},
|
||||
{"VPCONFLICTD", "VPCONFLICTD", []Operand{vreg(t, "Z1"), vreg(t, "K1"), vreg(t, "Z2")}, "62f27d49c4d1"},
|
||||
{"VPLZCNTQ masked", "VPLZCNTQ", []Operand{vreg(t, "Z7"), vreg(t, "K1"), vreg(t, "Z8")}, "6272fd4944c7"},
|
||||
{"VPERMT2B", "VPERMT2B", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f26d497dd9"},
|
||||
{"VPMULTISHIFTQB", "VPMULTISHIFTQB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3"), vreg(t, "Z4")}, "62f2ed4b83e1"},
|
||||
{"VDBPSADBW", "VDBPSADBW", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3"), vreg(t, "Z3")}, "62f36d4b42d903"},
|
||||
{"VPSHUFBITQMB", "VPSHUFBITQMB", []Operand{vreg(t, "Z9"), vreg(t, "Z10"), vreg(t, "K3")}, "62d22d488fd9"},
|
||||
{"VPTESTNMQ", "VPTESTNMQ", []Operand{vreg(t, "Z13"), vreg(t, "Z14"), vreg(t, "K5")}, "62d28e4827ed"},
|
||||
// Permutations: immediate and register counts.
|
||||
{"VALIGNQ", "VALIGNQ", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f3ed4903d903"},
|
||||
{"VPERMQ imm", "VPERMQ", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z2")}, "62f3fd4a00d101"},
|
||||
{"VPERMQ reg", "VPERMQ", []Operand{vreg(t, "Z3"), vreg(t, "Z4"), vreg(t, "K2"), vreg(t, "Z5")}, "62f2dd4a36eb"},
|
||||
{"VPERMPD reg", "VPERMPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed4816d9"},
|
||||
{"VPERMILPS imm", "VPERMILPS", []Operand{Imm(5), vreg(t, "Z9"), vreg(t, "K2"), vreg(t, "Z10")}, "62537d4a04d105"},
|
||||
{"VPERMILPS reg", "VPERMILPS", []Operand{vreg(t, "Z11"), vreg(t, "Z12"), vreg(t, "K2"), vreg(t, "Z13")}, "62521d4a0ceb"},
|
||||
// Shifts: immediate, register-count and memory-count forms; the
|
||||
// count source carries its own XMM tuple width.
|
||||
{"VPSLLW imm mask", "VPSLLW", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z2")}, "62f16d4a71f103"},
|
||||
{"VPSLLD reg count", "VPSLLD", []Operand{vreg(t, "X1"), vreg(t, "Z2"), vreg(t, "K1"), vreg(t, "Z3")}, "62f16d49f2d9"},
|
||||
{"VPSLLDQ", "VPSLLDQ", []Operand{Imm(9), vreg(t, "Z7"), vreg(t, "Z8")}, "62f13d4873ff09"},
|
||||
{"VPSRLDQ mem", "VPSRLDQ", []Operand{Imm(11), Ptr(SI, 16, 16), vreg(t, "Z4")}, "62f15d48739e100000000b"},
|
||||
{"VPSRLVW", "VPSRLVW", []Operand{vreg(t, "Z3"), vreg(t, "Z4"), vreg(t, "K1"), vreg(t, "Z5")}, "62f2dd4910eb"},
|
||||
// Conversions and shuffles with the F2 prefix and no prefix.
|
||||
{"VCVTUDQ2PS", "VCVTUDQ2PS", []Operand{vreg(t, "Z1"), vreg(t, "K1"), vreg(t, "Z2")}, "62f17f497ad1"},
|
||||
{"VSHUFPS", "VSHUFPS", []Operand{Imm(2), vreg(t, "Z4"), vreg(t, "Z5"), vreg(t, "K1"), vreg(t, "Z6")}, "62f15449c6f402"},
|
||||
// Gather and scatter prefetch hints (memory-only, /digit in reg).
|
||||
{"VGATHERPF0DPD", "VGATHERPF0DPD", []Operand{vreg(t, "K5"), vsib("R10", "Y29", 8)}, "6292fd45c60cea"},
|
||||
{"VSCATTERPF1DPS", "VSCATTERPF1DPS", []Operand{vreg(t, "K2"), vsib("R10", "Z28", 4)}, "62927d42c634a2"},
|
||||
// Opmask broadcasts and the K logic.
|
||||
{"VPBROADCASTMB2Q", "VPBROADCASTMB2Q", []Operand{vreg(t, "K1"), vreg(t, "Z2")}, "62f2fe482ad1"},
|
||||
{"VPBROADCASTMW2D", "VPBROADCASTMW2D", []Operand{vreg(t, "K3"), vreg(t, "Z4")}, "62f27e483ae3"},
|
||||
{"KUNPCKWD", "KUNPCKWD", []Operand{vreg(t, "K6"), vreg(t, "K4"), vreg(t, "K1")}, "c5dc4bce"},
|
||||
{"KADDB", "KADDB", []Operand{vreg(t, "K2"), vreg(t, "K3"), vreg(t, "K5")}, "c5e54aea"},
|
||||
// Lane extracts to general registers (EVEX and VEX routes).
|
||||
{"VPEXTRB", "VPEXTRB", []Operand{Imm(3), vreg(t, "X26"), AX}, "62637d0814d003"},
|
||||
{"VPEXTRD", "VPEXTRD", []Operand{Imm(1), vreg(t, "X26"), vreg(t, "R9")}, "62437d0816d101"},
|
||||
{"VPEXTRD vex", "VPEXTRD", []Operand{Imm(1), vreg(t, "X2"), DI}, "c4e37916d701"},
|
||||
{"VPINSRQ", "VPINSRQ", []Operand{Imm(1), DI, vreg(t, "X3"), vreg(t, "X4")}, "c4e3e122e701"},
|
||||
// Moves: masked unaligned, masked scalar register form, half moves
|
||||
// and non-temporal stores.
|
||||
{"VMOVUPS mask", "VMOVUPS", []Operand{vreg(t, "Z1"), vreg(t, "K2"), vreg(t, "Z3")}, "62f17c4a11cb"},
|
||||
{"VMOVSD 3op", "VMOVSD", []Operand{vreg(t, "X14"), vreg(t, "X5"), vreg(t, "K3"), vreg(t, "X22")}, "6231d70b11f6"},
|
||||
{"VMOVSS 3op", "VMOVSS", []Operand{vreg(t, "X18"), vreg(t, "X3"), vreg(t, "K2"), vreg(t, "X25")}, "6281660a11d1"},
|
||||
{"VMOVHPS insert", "VMOVHPS", []Operand{Ptr(SI, 0, 8), vreg(t, "X18"), vreg(t, "X19")}, "62e16c00161e"},
|
||||
{"VMOVHPS store", "VMOVHPS", []Operand{vreg(t, "X20"), Ptr(SI, 8, 8)}, "62e17c08176601"},
|
||||
{"VMOVLHPS", "VMOVLHPS", []Operand{vreg(t, "X16"), vreg(t, "X5"), vreg(t, "X17")}, "62a1540816c8"},
|
||||
{"VMOVNTDQ", "VMOVNTDQ", []Operand{vreg(t, "Z7"), Ptr(SI, 0, 64)}, "62f17d48e73e"},
|
||||
{"VMOVNTDQA", "VMOVNTDQA", []Operand{Ptr(SI, 64, 64), vreg(t, "Z8")}, "62727d482a4601"},
|
||||
{"VMOVNTPS", "VMOVNTPS", []Operand{vreg(t, "Z9"), Ptr(SI, 0, 64)}, "62717c482b0e"},
|
||||
// Scalar compares with and without the 66 prefix.
|
||||
{"VCOMISD", "VCOMISD", []Operand{vreg(t, "X5"), vreg(t, "X6")}, "c5f92ff5"},
|
||||
{"VUCOMISS", "VUCOMISS", []Operand{vreg(t, "X7"), vreg(t, "X8")}, "c5782ec7"},
|
||||
// Floating point helpers.
|
||||
{"VSQRTSD", "VSQRTSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K1"), vreg(t, "X3")}, "62f1ef0951d9"},
|
||||
{"VEXP2PD", "VEXP2PD", []Operand{vreg(t, "Z5"), vreg(t, "K1"), vreg(t, "Z6")}, "62f2fd49c8f5"},
|
||||
{"VRCP28SD", "VRCP28SD", []Operand{vreg(t, "X9"), vreg(t, "X8"), vreg(t, "K1"), vreg(t, "X10")}, "6252bd09cbd1"},
|
||||
{"VBROADCASTF32X2", "VBROADCASTF32X2", []Operand{vreg(t, "X1"), vreg(t, "K1"), vreg(t, "Z2")}, "62f27d4919d1"},
|
||||
{"VPCOMPRESSB", "VPCOMPRESSB", []Operand{vreg(t, "Z1"), vreg(t, "K1"), Ptr(SI, 0, 64)}, "62f27d49630e"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Encode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := hexCompact(code); got != c.want {
|
||||
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexQuadRegisterGroundTruth pins the quad-register instructions (the
|
||||
// 4FMAPS and 4VNNIW families) byte for byte against go tool asm: the memory
|
||||
// source keeps r/m, the bracketed list's LOW register travels the inverted
|
||||
// 5-bit V'VVVV field, the destination sits in reg, the opmask rides aaa and
|
||||
// the vector length follows the destination (L'L=512 for the ZMM forms,
|
||||
// 128 for the scalar ones) while the disp8×N multiplier stays 16 for every
|
||||
// member. The x86 decoder has no view of these forms, so no decode check
|
||||
// runs.
|
||||
func TestEvexQuadRegisterGroundTruth(t *testing.T) {
|
||||
sp := vreg(t, "RSP")
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"V4FMADDPS 17(SP) [Z0-Z3] K2 Z0", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f27f4a9a842411000000"},
|
||||
{"V4FMADDPS [Z10-Z13]", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z10"), vreg(t, "Z13")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f22f4a9a842411000000"},
|
||||
{"V4FMADDPS [Z20-Z23]", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z20"), vreg(t, "Z23")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f25f429a842411000000"},
|
||||
{"V4FMADDPS Z8 dst", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "K2"), vreg(t, "Z8")},
|
||||
"62727f4a9a842411000000"},
|
||||
{"V4FMADDPS disp8x16", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 64, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f27f4a9a442404"},
|
||||
{"V4FMADDPS unmasked", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "Z0")},
|
||||
"62f27f489a842411000000"},
|
||||
{"V4FMADDSS 7(AX) [X0-X3] K5 X22", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X0"), vreg(t, "X3")}, vreg(t, "K5"), vreg(t, "X22")},
|
||||
"62e27f0d9bb007000000"},
|
||||
{"V4FMADDSS (DI)", "V4FMADDSS",
|
||||
[]Operand{Ptr(DI, 0, 8), RegList{vreg(t, "X0"), vreg(t, "X3")}, vreg(t, "K5"), vreg(t, "X22")},
|
||||
"62e27f0d9b37"},
|
||||
{"V4FMADDSS [X10-X13]", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X10"), vreg(t, "X13")}, vreg(t, "K5"), vreg(t, "X22")},
|
||||
"62e22f0d9bb007000000"},
|
||||
{"V4FMADDSS [X20-X23]", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X20"), vreg(t, "X23")}, vreg(t, "K5"), vreg(t, "X22")},
|
||||
"62e25f059bb007000000"},
|
||||
{"V4FMADDSS X30 dst", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X0"), vreg(t, "X3")}, vreg(t, "K5"), vreg(t, "X30")},
|
||||
"62627f0d9bb007000000"},
|
||||
{"V4FMADDSS X3 dst", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X0"), vreg(t, "X3")}, vreg(t, "K5"), vreg(t, "X3")},
|
||||
"62f27f0d9b9807000000"},
|
||||
{"V4FMADDSS disp8x16", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 16, 8), RegList{vreg(t, "X20"), vreg(t, "X23")}, vreg(t, "K5"), vreg(t, "X30")},
|
||||
"62625f059b7001"},
|
||||
{"V4FNMADDPS", "V4FNMADDPS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f27f4aaa842411000000"},
|
||||
{"V4FNMADDSS", "V4FNMADDSS",
|
||||
[]Operand{Ptr(AX, 7, 8), RegList{vreg(t, "X0"), vreg(t, "X3")}, vreg(t, "K5"), vreg(t, "X22")},
|
||||
"62e27f0dabb007000000"},
|
||||
{"VP4DPWSSD", "VP4DPWSSD",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "K2"), vreg(t, "Z0")},
|
||||
"62f27f4a52842411000000"},
|
||||
{"VP4DPWSSDS unmasked", "VP4DPWSSDS",
|
||||
[]Operand{Ptr(sp, 17, 8), RegList{vreg(t, "Z0"), vreg(t, "Z3")}, vreg(t, "Z0")},
|
||||
"62f27f4853842411000000"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Encode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := hexCompact(code); got != c.want {
|
||||
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexQuadRegisterErrors pins the operand shapes the toolchain rejects:
|
||||
// the register class the list and the destination take is fixed per
|
||||
// instruction, the source is memory only, the opmask slot is positional and
|
||||
// the list's low register owns V'VVVV.
|
||||
func TestEvexQuadRegisterErrors(t *testing.T) {
|
||||
sp := vreg(t, "RSP")
|
||||
list := func(lo, hi string) RegList {
|
||||
return RegList{vreg(t, lo), vreg(t, hi)}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
}{
|
||||
{"X list on the PS form", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("X0", "X3"), vreg(t, "K2"), vreg(t, "Z0")}},
|
||||
{"Z list on the SS form", "V4FMADDSS",
|
||||
[]Operand{Ptr(AX, 0, 8), list("Z0", "Z3"), vreg(t, "K5"), vreg(t, "X22")}},
|
||||
{"Y destination", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "K2"), vreg(t, "Y0")}},
|
||||
{"register source", "V4FMADDPS",
|
||||
[]Operand{vreg(t, "Z1"), list("Z0", "Z3"), vreg(t, "K2"), vreg(t, "Z0")}},
|
||||
{"non-mask third operand", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "Z4"), vreg(t, "Z0")}},
|
||||
{"k0 mask", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "K0"), vreg(t, "Z0")}},
|
||||
{"K after the destination", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "Z0"), vreg(t, "K2")}},
|
||||
{"zeroing without a mask", "V4FMADDPS.Z",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "Z0")}},
|
||||
{"SAE suffix", "V4FMADDPS.SAE",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3"), vreg(t, "K2"), vreg(t, "Z0")}},
|
||||
{"high index source", "VP4DPWSSD",
|
||||
[]Operand{Idx(DI, vreg(t, "X16"), 1, 0, 8), list("Z0", "Z3"), vreg(t, "K2"), vreg(t, "Z0")}},
|
||||
{"short operand list", "V4FMADDPS",
|
||||
[]Operand{Ptr(sp, 0, 8), list("Z0", "Z3")}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if _, err := Encode(c.mnem, c.ops...); err == nil {
|
||||
t.Errorf("%s: expected an error, got none", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The extended-instruction registry: the lookup over and above the generated
|
||||
// architecture tables. The generated tables (arch/*_gen.go) list the
|
||||
// mnemonics the Go toolchain knows; the extension layer carries the
|
||||
// instructions it does not, and this file indexes them per architecture so
|
||||
// the assembler and the linter can consult the layer without touching the
|
||||
// generated lists or the main encoders. A later hook wires
|
||||
// ExtensionEncodable into the Encodable mirror and EncodeExtension into the
|
||||
// per-architecture assembly paths; nothing existing changes until then.
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// extensionIndex is the per-architecture index of the extension layer, keyed
|
||||
// by upper-case mnemonic. One mnemonic registers several forms (the SVE ADD
|
||||
// carries unpredicated, predicated and immediate shapes), so the value is the
|
||||
// full candidate list in table order.
|
||||
type extensionIndex struct {
|
||||
byName map[string][]arch.ExtInstr
|
||||
}
|
||||
|
||||
// extensionIndexes builds one index per known architecture. Architectures
|
||||
// whose extension layer is not built yet get an empty index, which keeps the
|
||||
// queries answering false rather than failing on a missing entry.
|
||||
var extensionIndexes = buildExtensionIndexes()
|
||||
|
||||
func buildExtensionIndexes() map[arch.Arch]*extensionIndex {
|
||||
m := make(map[arch.Arch]*extensionIndex)
|
||||
for _, a := range []arch.Arch{arch.AMD64, arch.ARM64, arch.RISCV, arch.LOONG64} {
|
||||
idx := &extensionIndex{byName: make(map[string][]arch.ExtInstr)}
|
||||
for _, in := range arch.Extensions(a) {
|
||||
key := strings.ToUpper(in.Name)
|
||||
idx.byName[key] = append(idx.byName[key], in)
|
||||
}
|
||||
m[a] = idx
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// LookupExtension returns the extended instructions registered for the
|
||||
// mnemonic on a, outside the generated architecture table. It reports false
|
||||
// when a carries no extended layer or the mnemonic is not in it; a mnemonic
|
||||
// the base table knows is not thereby covered, the layers stay independent.
|
||||
func LookupExtension(a arch.Arch, mnemonic string) ([]arch.ExtInstr, bool) {
|
||||
idx, ok := extensionIndexes[a]
|
||||
if !ok || idx == nil {
|
||||
return nil, false
|
||||
}
|
||||
cands, ok := idx.byName[strings.ToUpper(mnemonic)]
|
||||
return cands, ok && len(cands) > 0
|
||||
}
|
||||
|
||||
// ExtensionNames returns the mnemonics the extension layer of a registers,
|
||||
// in table order, without duplicates.
|
||||
func ExtensionNames(a arch.Arch) []string {
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, in := range arch.Extensions(a) {
|
||||
key := strings.ToUpper(in.Name)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
names = append(names, in.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// EncodeExtension encodes one extended instruction on a: it resolves the
|
||||
// mnemonic through the extension registry, picks the registered form whose
|
||||
// arity matches the operands and encodes against it. The first form that
|
||||
// encodes wins. When every matching form rejects the operands, the error
|
||||
// comes from the form whose operand kinds the list points at (the one with
|
||||
// the most matching positions), so a mis-spelled predicate qualifier is
|
||||
// diagnosed as one, not as the unpredicated form's register complaint.
|
||||
func EncodeExtension(a arch.Arch, mnemonic string, ops ...arch.ExtOperand) ([]byte, error) {
|
||||
cands, ok := LookupExtension(a, mnemonic)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s registers no extended instruction %q", a, mnemonic)
|
||||
}
|
||||
var bestErr error
|
||||
var bestScore int
|
||||
var tried int
|
||||
for _, in := range cands {
|
||||
if in.Form.Arity() != len(ops) {
|
||||
continue
|
||||
}
|
||||
tried++
|
||||
b, err := in.Encode(ops)
|
||||
if err == nil {
|
||||
return b, nil
|
||||
}
|
||||
if score := kindScore(in.Form, ops); bestErr == nil || score > bestScore {
|
||||
bestErr, bestScore = err, score
|
||||
}
|
||||
}
|
||||
if tried == 0 {
|
||||
return nil, fmt.Errorf("%s: extended %q takes %s, got %d operands",
|
||||
a, mnemonic, extensionAritySummary(cands), len(ops))
|
||||
}
|
||||
return nil, bestErr
|
||||
}
|
||||
|
||||
// kindScore counts the positions whose operand kind matches what the form
|
||||
// wants, the tie-break that picks the most specific rejection.
|
||||
func kindScore(form arch.ExtForm, ops []arch.ExtOperand) int {
|
||||
kinds := form.Kinds()
|
||||
score := 0
|
||||
for i, op := range ops {
|
||||
if i < len(kinds) && op.Kind == kinds[i] {
|
||||
score++
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// ExtensionEncodable reports whether the extension layer of a encodes the
|
||||
// mnemonic with these operands. It mirrors asm.Encodable for the extension
|
||||
// layer: the predicate the linter consults once the hook wires it in.
|
||||
func ExtensionEncodable(a arch.Arch, mnemonic string, ops ...arch.ExtOperand) bool {
|
||||
_, err := EncodeExtension(a, mnemonic, ops...)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// extensionAritySummary describes the operand counts the candidate forms
|
||||
// take, "2 or 3" style, for the arity error.
|
||||
func extensionAritySummary(cands []arch.ExtInstr) string {
|
||||
counts := make([]int, 0, len(cands))
|
||||
seen := make(map[int]bool)
|
||||
for _, in := range cands {
|
||||
n := in.Form.Arity()
|
||||
if !seen[n] {
|
||||
seen[n] = true
|
||||
counts = append(counts, n)
|
||||
}
|
||||
}
|
||||
slices.Sort(counts)
|
||||
var b strings.Builder
|
||||
for i, n := range counts {
|
||||
if i > 0 {
|
||||
if i == len(counts)-1 {
|
||||
b.WriteString(" or ")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "%d", n)
|
||||
}
|
||||
b.WriteString(" operands")
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// TestAmd64ExtensionRegistry checks the mnemonic lookup for the amd64 layer:
|
||||
// one mnemonic across several vector lengths or W bits resolves to every
|
||||
// entry, the lookup is case-insensitive, and the counts match the registered
|
||||
// families.
|
||||
func TestAmd64ExtensionRegistry(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
mnem string
|
||||
forms int
|
||||
}{
|
||||
{"VCVTNE2PS2BF16", 3},
|
||||
{"VCVTNEPS2BF16", 3},
|
||||
{"VDPBF16PS", 3},
|
||||
{"VP2INTERSECTD", 3},
|
||||
{"VP2INTERSECTQ", 3},
|
||||
{"VMOVSH", 3},
|
||||
{"VMOVW", 4},
|
||||
{"VADDSH", 1},
|
||||
{"VSQRTSH", 1},
|
||||
{"VCOMISH", 1},
|
||||
{"VCVTSH2SS", 1},
|
||||
{"VCVTSI2SH", 2},
|
||||
{"VCVTSH2SI", 2},
|
||||
{"VADDPH", 3},
|
||||
{"VSQRTPH", 3},
|
||||
{"VSCALEFSH", 1},
|
||||
{"VGETEXPSH", 1},
|
||||
{"VCMPSH", 1},
|
||||
{"VGETMANTSH", 1},
|
||||
{"VREDUCESH", 1},
|
||||
{"VRNDSCALESH", 1},
|
||||
{"VCVTPH2W", 3},
|
||||
{"VCVTPH2UW", 3},
|
||||
{"VCVTW2PH", 3},
|
||||
{"VCVTUW2PH", 3},
|
||||
{"VCVTPH2DQ", 3},
|
||||
{"VCVTPH2UDQ", 3},
|
||||
{"VCVTDQ2PH", 3},
|
||||
{"VCVTUDQ2PH", 3},
|
||||
{"VCVTPH2QQ", 3},
|
||||
{"VCVTPH2UQQ", 3},
|
||||
{"VCVTQQ2PH", 3},
|
||||
{"VCVTUQQ2PH", 3},
|
||||
{"VCVTPH2PD", 3},
|
||||
{"VCVTPD2PH", 3},
|
||||
{"VRNDSCALEPH", 3},
|
||||
{"VREDUCEPH", 3},
|
||||
{"VGETMANTPH", 3},
|
||||
{"VFMADD132PH", 3},
|
||||
{"VFMADD213PH", 3},
|
||||
{"VFMADD231PH", 3},
|
||||
{"VFMSUB132PH", 3},
|
||||
{"VFMSUB213PH", 3},
|
||||
{"VFMSUB231PH", 3},
|
||||
{"VFMADDSUB132PH", 3},
|
||||
{"VFMADDSUB213PH", 3},
|
||||
{"VFMADDSUB231PH", 3},
|
||||
{"VFMSUBADD132PH", 3},
|
||||
{"VFMSUBADD213PH", 3},
|
||||
{"VFMSUBADD231PH", 3},
|
||||
{"VFMADD132SH", 1},
|
||||
{"VFMADD213SH", 1},
|
||||
{"VFMADD231SH", 1},
|
||||
{"VFMSUB132SH", 1},
|
||||
{"VFMSUB213SH", 1},
|
||||
{"VFMSUB231SH", 1},
|
||||
{"VFMULCPH", 3},
|
||||
{"VFCMULCPH", 3},
|
||||
{"VFMULCSH", 1},
|
||||
{"VFCMULCSH", 1},
|
||||
{"VFMADDCPH", 3},
|
||||
{"VFCMADDCPH", 3},
|
||||
{"VFMADDCSH", 1},
|
||||
{"VFCMADDCSH", 1},
|
||||
{"VMINMAXPH", 3},
|
||||
{"VMINMAXSH", 1},
|
||||
{"VPDPWSUD", 2},
|
||||
{"VPDPWSUDS", 2},
|
||||
{"VPDPWUSD", 2},
|
||||
{"VPDPWUSDS", 2},
|
||||
} {
|
||||
cands, ok := LookupExtension(arch.AMD64, tt.mnem)
|
||||
if !ok {
|
||||
t.Fatalf("LookupExtension(AMD64, %s) found nothing", tt.mnem)
|
||||
}
|
||||
if len(cands) != tt.forms {
|
||||
t.Errorf("%s registers %d forms, want %d", tt.mnem, len(cands), tt.forms)
|
||||
}
|
||||
lower, ok := LookupExtension(arch.AMD64, strings.ToLower(tt.mnem))
|
||||
if !ok || len(lower) != tt.forms {
|
||||
t.Errorf("the %s lookup is not case-insensitive", tt.mnem)
|
||||
}
|
||||
}
|
||||
if got := arch.Extensions(arch.AMD64); len(got) != 191 {
|
||||
t.Errorf("the amd64 layer registers %d instructions, want 191", len(got))
|
||||
}
|
||||
if _, ok := LookupExtension(arch.AMD64, "NOSUCHINSTR"); ok {
|
||||
t.Error("a non-extended mnemonic resolved")
|
||||
}
|
||||
// VPOPCNTD and VPOPCNTQ are toolchain instructions today: they stay in
|
||||
// the generated table and out of the extension layer.
|
||||
if _, ok := LookupExtension(arch.AMD64, "VPOPCNTD"); ok {
|
||||
t.Error("VPOPCNTD is an extension, want it in the generated table alone")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAmd64ExtensionAboveGeneratedTable pins the layering twice over: no
|
||||
// registered mnemonic sits in the generated amd64 table, and the encoder
|
||||
// mirror asm.Encodable answers false for every one of them, so the layer
|
||||
// stays out of the main encoders by test and not by promise.
|
||||
func TestAmd64ExtensionAboveGeneratedTable(t *testing.T) {
|
||||
for _, mnem := range ExtensionNames(arch.AMD64) {
|
||||
if _, found := arch.ForArch(arch.AMD64).Lookup(mnem); found {
|
||||
t.Errorf("%s leaked into the generated amd64 table", mnem)
|
||||
}
|
||||
if Encodable(mnem) {
|
||||
t.Errorf("%s is encodable through the main encoder, the layer is not sealed", mnem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionAmd64 encodes through the registry and pins the same
|
||||
// golden words the arch table tests pin, proving the registry resolves to the
|
||||
// right encoding.
|
||||
func TestEncodeExtensionAmd64(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []arch.ExtOperand
|
||||
want string
|
||||
}{
|
||||
{"bf16 convert", "VCVTNE2PS2BF16",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtZmm(6)},
|
||||
"62f2574872f4"},
|
||||
{"bf16 narrow convert", "VCVTNEPS2BF16",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtYmm(6)},
|
||||
"62f27e4872f5"},
|
||||
{"dot product", "VDPBF16PS",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtXmm(6)},
|
||||
"62f2560852f4"},
|
||||
{"intersect into a mask", "VP2INTERSECTD",
|
||||
[]arch.ExtOperand{arch.ExtYmm(2), arch.ExtYmm(1), arch.ExtMask(2)},
|
||||
"62f26f2868d1"},
|
||||
{"scalar fp16 add, high registers", "VADDSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)},
|
||||
"6205160058f4"},
|
||||
{"scalar compare", "VCOMISH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtXmm(30)},
|
||||
"62057c082ff5"},
|
||||
{"integer into a scalar fp16", "VCVTSI2SH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtGpr64(12), arch.ExtXmm(30)},
|
||||
"624596002af4"},
|
||||
{"scalar fp16 into an integer", "VCVTSH2SI",
|
||||
[]arch.ExtOperand{arch.ExtXmm(30), arch.ExtGpr32(2)},
|
||||
"62957e082dd6"},
|
||||
{"word move into an xmm", "VMOVW",
|
||||
[]arch.ExtOperand{arch.ExtGpr64(12), arch.ExtXmm(30)},
|
||||
"62457d086ef4"},
|
||||
{"scalar compare into a mask", "VCMPSH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x7b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtMask(5)},
|
||||
"62931600c2ec7b"},
|
||||
{"mantissa extract with a control byte", "VGETMANTSH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x0b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)},
|
||||
"6203140027f40b"},
|
||||
{"packed fp16 add under embedded rounding", "VADDPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundTruncate)},
|
||||
"62f5547858f4"},
|
||||
{"scalar fp16 minimum with exceptions suppressed", "VMINSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtRounded(arch.ExtXmm(6), arch.ExtRoundSAE)},
|
||||
"62f556185df4"},
|
||||
{"fp16 to signed words", "VCVTPH2W",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(6)},
|
||||
"62f57d487df5"},
|
||||
{"dwords to fp16 over a broadcast source", "VCVTDQ2PH",
|
||||
[]arch.ExtOperand{arch.ExtBroadcast(9, 0), arch.ExtYmm(30)},
|
||||
"62457c585b31"},
|
||||
{"fp16 to signed qwords, rounded", "VCVTPH2QQ",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundTruncate)},
|
||||
"62f57d787bf5"},
|
||||
{"fp16 scalar out of memory into an integer", "VCVTSH2SI",
|
||||
[]arch.ExtOperand{arch.ExtMemory(9, 0), arch.ExtGpr64(12)},
|
||||
"6255fe082d21"},
|
||||
{"fp16 to double-precision, widened", "VCVTPH2PD",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtZmm(6)},
|
||||
"62f57c485af5"},
|
||||
{"packed fp16 rounding to fraction bits", "VRNDSCALEPH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x7b), arch.ExtZmm(5), arch.ExtZmm(6)},
|
||||
"62f37c4808f57b"},
|
||||
{"packed fused multiply-add, high registers", "VFMADD132PH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
|
||||
"6206154098f4"},
|
||||
{"fused multiply-add out of a broadcast source", "VFMADD231PH",
|
||||
[]arch.ExtOperand{arch.ExtYmm(5), arch.ExtBroadcast(1, 0), arch.ExtYmm(6)},
|
||||
"62f65538b831"},
|
||||
{"scalar multiply-subtract under embedded rounding", "VFMSUB231SH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtRounded(arch.ExtXmm(6), arch.ExtRoundTruncate)},
|
||||
"62f65578bbf4"},
|
||||
{"complex multiply, conjugating the first source", "VFCMULCPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
|
||||
"62061740d6f4"},
|
||||
{"complex multiply-add, conjugating the second source", "VFMADDCPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
|
||||
"6206164056f4"},
|
||||
{"complex multiply-add, conjugated first source, rounded", "VFCMADDCPH",
|
||||
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundNearest)},
|
||||
"62f6571856f4"},
|
||||
{"scalar complex multiply-add out of memory", "VFMADDCSH",
|
||||
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtMemory(9, 0), arch.ExtXmm(30)},
|
||||
"624616005731"},
|
||||
{"minimum or maximum under a control byte", "VMINMAXPH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x88), arch.ExtZmm(29), arch.ExtMemory(9, 0), arch.ExtZmm(30)},
|
||||
"62431440523188"},
|
||||
{"scalar minimum or maximum out of memory", "VMINMAXSH",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(0x88), arch.ExtXmm(28), arch.ExtMemory(9, 0), arch.ExtXmm(29)},
|
||||
"62431c00532988"},
|
||||
{"vnni dot product through the VEX word", "VPDPWSUD",
|
||||
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtXmm(1), arch.ExtXmm(3)},
|
||||
"c4e26ad2d9"},
|
||||
{"saturating dot product, high registers", "VPDPWUSDS",
|
||||
[]arch.ExtOperand{arch.ExtYmm(10), arch.ExtYmm(15), arch.ExtYmm(8)},
|
||||
"c4422dd3c7"},
|
||||
{"dot product out of memory", "VPDPWSUD",
|
||||
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtMemory(1, 127), arch.ExtXmm(1)},
|
||||
"c4e26ad2497f"},
|
||||
} {
|
||||
got, err := EncodeExtension(arch.AMD64, tt.mnem, tt.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: encode: %v", tt.name, err)
|
||||
continue
|
||||
}
|
||||
if hex.EncodeToString(got) != tt.want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionAmd64Errors checks the registry's diagnostics on the
|
||||
// amd64 side: a wrong arity names the form's count and a mis-classed operand
|
||||
// surfaces the entry's own message.
|
||||
func TestEncodeExtensionAmd64Errors(t *testing.T) {
|
||||
if _, err := EncodeExtension(arch.AMD64, "VP2INTERSECTD", arch.ExtZmm(1)); err == nil {
|
||||
t.Error("one operand encoded, want an arity error")
|
||||
} else if !strings.Contains(err.Error(), "3 operands") {
|
||||
t.Errorf("arity error %q does not name the count", err)
|
||||
}
|
||||
_, err := EncodeExtension(arch.AMD64, "VCVTNEPS2BF16", arch.ExtZmm(1), arch.ExtZmm(2))
|
||||
if err == nil {
|
||||
t.Fatal("a ZMM destination encoded on the narrow convert, want an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "YMM register") {
|
||||
t.Errorf("error %q does not name the YMM destination", err)
|
||||
}
|
||||
if _, err := EncodeExtension(arch.AMD64, "VCVTNE2PS2BF16"); err == nil ||
|
||||
!strings.Contains(err.Error(), "takes 3 operands, got 0") {
|
||||
t.Errorf("zero-operand error = %v, want the operand-count diagnostic", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// TestExtensionRegistryARM64 checks the mnemonic lookup over and above the
|
||||
// generated arm64 table: one mnemonic, several forms, case-insensitive, and
|
||||
// nothing offered for a spelling the layer does not carry.
|
||||
func TestExtensionRegistryARM64(t *testing.T) {
|
||||
add, ok := LookupExtension(arch.ARM64, "ADD")
|
||||
if !ok {
|
||||
t.Fatal("LookupExtension(ARM64, ADD) found nothing")
|
||||
}
|
||||
var forms []arch.ExtForm
|
||||
for _, in := range add {
|
||||
if in.Name != "ADD" {
|
||||
t.Errorf("candidate %q leaked into the ADD lookup", in.Name)
|
||||
}
|
||||
forms = append(forms, in.Form)
|
||||
}
|
||||
if len(forms) != 3 ||
|
||||
forms[0] != arch.ExtFormVectors ||
|
||||
forms[1] != arch.ExtFormPredicated ||
|
||||
forms[2] != arch.ExtFormImmediate {
|
||||
t.Errorf("ADD registers forms %v, want unpredicated, predicated and immediate", forms)
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "add"); !ok {
|
||||
t.Error("the lookup is case-sensitive")
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "NOSUCHINSTR"); ok {
|
||||
t.Error("a non-extended mnemonic resolved")
|
||||
}
|
||||
sqadd, ok := LookupExtension(arch.ARM64, "SQADD")
|
||||
if !ok || len(sqadd) != 2 {
|
||||
t.Errorf("SQADD registers %d forms, want the unpredicated and immediate pair", len(sqadd))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionAboveGeneratedTable pins the layering: SQADD is nowhere in the
|
||||
// generated arm64 table (the toolchain knows only the NEON spelling VSQADD)
|
||||
// yet the extension layer carries it, while ADD sits in both layers
|
||||
// independently.
|
||||
func TestExtensionAboveGeneratedTable(t *testing.T) {
|
||||
if _, found := arch.ForArch(arch.ARM64).Lookup("SQADD"); found {
|
||||
t.Error("SQADD is in the generated table, the layering assumption broke")
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "SQADD"); !ok {
|
||||
t.Error("SQADD is missing from the extension layer")
|
||||
}
|
||||
if _, found := arch.ForArch(arch.ARM64).Lookup("ADD"); !found {
|
||||
t.Error("ADD vanished from the generated table")
|
||||
}
|
||||
if add, ok := LookupExtension(arch.ARM64, "ADD"); !ok || len(add) != 3 {
|
||||
t.Errorf("ADD carries %d extension forms, want 3", len(add))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionGolden encodes through the registry and pins the same
|
||||
// golden words the arch table tests pin, proving the registry resolves to the
|
||||
// right encoding.
|
||||
func TestEncodeExtensionGolden(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []arch.ExtOperand
|
||||
want uint32
|
||||
}{
|
||||
{"unpredicated add", "ADD",
|
||||
[]arch.ExtOperand{
|
||||
arch.ExtVector(2, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB),
|
||||
},
|
||||
0x04200040},
|
||||
{"predicated mul", "MUL",
|
||||
[]arch.ExtOperand{
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtPredicate(2, arch.ExtQualMerging), arch.ExtVector(0, arch.ExtArrB),
|
||||
},
|
||||
0x04100800},
|
||||
{"immediate add with derived shift", "ADD",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(32512), arch.ExtVector(0, arch.ExtArrH)},
|
||||
0x2560efe0},
|
||||
{"signed immediate mul", "MUL",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(-1), arch.ExtVector(0, arch.ExtArrB)},
|
||||
0x2530dfe0},
|
||||
} {
|
||||
got, err := EncodeExtension(arch.ARM64, tt.mnem, tt.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: encode: %v", tt.name, err)
|
||||
continue
|
||||
}
|
||||
if want := hex.EncodeToString([]byte{
|
||||
byte(tt.want), byte(tt.want >> 8), byte(tt.want >> 16), byte(tt.want >> 24),
|
||||
}); hex.EncodeToString(got) != want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionErrors checks the registry's diagnostics: a wrong arity
|
||||
// names every form's count, an operand the first candidate rejects surfaces
|
||||
// its own message once a later form takes over.
|
||||
func TestEncodeExtensionErrors(t *testing.T) {
|
||||
if _, err := EncodeExtension(arch.ARM64, "ADD", arch.ExtVector(0, arch.ExtArrB)); err == nil {
|
||||
t.Error("one operand encoded, want an arity error")
|
||||
} else if !strings.Contains(err.Error(), "2 or 3 operands") {
|
||||
t.Errorf("arity error %q does not name the counts", err)
|
||||
}
|
||||
// The predicated candidate must answer for its own operands: the /Z
|
||||
// qualifier is rejected with the merging message, not the unpredicated
|
||||
// form's register-kind complaint.
|
||||
_, err := EncodeExtension(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtPredicate(0, arch.ExtQualZeroing), arch.ExtVector(0, arch.ExtArrB))
|
||||
if err == nil {
|
||||
t.Fatal("/Z encoded, want an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/M") {
|
||||
t.Errorf("error %q does not name the merging qualifier", err)
|
||||
}
|
||||
if _, err := EncodeExtension(arch.ARM64, "NOSUCHINSTR", arch.ExtVector(0, arch.ExtArrB)); err == nil ||
|
||||
!strings.Contains(err.Error(), "registers no extended instruction") {
|
||||
t.Errorf("unknown mnemonic error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionEncodable checks the predicate the later Encodable hook will
|
||||
// call: true exactly when the registry encodes the operand list.
|
||||
func TestExtensionEncodable(t *testing.T) {
|
||||
if !ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrS), arch.ExtVector(1, arch.ExtArrS), arch.ExtVector(2, arch.ExtArrS)) {
|
||||
t.Error("an encodable unpredicated add reported false")
|
||||
}
|
||||
if !ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(1, arch.ExtArrS), arch.ExtPredicate(0, arch.ExtQualMerging), arch.ExtVector(0, arch.ExtArrS)) {
|
||||
t.Error("an encodable predicated add reported false")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrS), arch.ExtVector(0, arch.ExtArrB)) {
|
||||
t.Error("mismatched arrangements reported encodable")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "ADD", arch.ExtVector(0, arch.ExtArrB)) {
|
||||
t.Error("a one-operand add reported encodable")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "NOSUCHINSTR") {
|
||||
t.Error("an unregistered mnemonic reported encodable")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionArchIsolation is the architecture-binding negative case: no
|
||||
// architecture answers the arm64 mnemonics but arm64, and the amd64 layer
|
||||
// answers nothing of the arm64 family either (its own mnemonics live in
|
||||
// extension_amd64_test.go).
|
||||
func TestExtensionArchIsolation(t *testing.T) {
|
||||
ops := []arch.ExtOperand{
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB),
|
||||
}
|
||||
for _, a := range []arch.Arch{arch.RISCV, arch.LOONG64, arch.Unknown} {
|
||||
if cands, ok := LookupExtension(a, "ADD"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(%s, ADD) offered %d candidates", a, len(cands))
|
||||
}
|
||||
if cands, ok := LookupExtension(a, "MUL"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(%s, MUL) offered %d candidates", a, len(cands))
|
||||
}
|
||||
if got, err := EncodeExtension(a, "ADD", ops...); err == nil {
|
||||
t.Errorf("EncodeExtension(%s, ADD) encoded %x, want a refusal", a, got)
|
||||
} else if !strings.Contains(err.Error(), string(a)) {
|
||||
t.Errorf("EncodeExtension(%s) error %q does not name the architecture", a, err)
|
||||
}
|
||||
if ExtensionEncodable(a, "ADD", ops...) {
|
||||
t.Errorf("ExtensionEncodable(%s, ADD) reported true", a)
|
||||
}
|
||||
if names := ExtensionNames(a); len(names) != 0 {
|
||||
t.Errorf("ExtensionNames(%s) = %v, want none", a, names)
|
||||
}
|
||||
if got := arch.Extensions(a); len(got) != 0 {
|
||||
t.Errorf("arch.Extensions(%s) carries %d instructions", a, len(got))
|
||||
}
|
||||
}
|
||||
// The amd64 layer exists but stays silent about the arm64 family.
|
||||
if cands, ok := LookupExtension(arch.AMD64, "ADD"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(AMD64, ADD) offered %d candidates", len(cands))
|
||||
}
|
||||
if got, err := EncodeExtension(arch.AMD64, "ADD", ops...); err == nil {
|
||||
t.Errorf("EncodeExtension(AMD64, ADD) encoded %x, want a refusal", got)
|
||||
} else if !strings.Contains(err.Error(), string(arch.AMD64)) {
|
||||
t.Errorf("EncodeExtension(AMD64) error %q does not name the architecture", err)
|
||||
}
|
||||
if ExtensionEncodable(arch.AMD64, "ADD", ops...) {
|
||||
t.Error("ExtensionEncodable(AMD64, ADD) reported true")
|
||||
}
|
||||
if names := ExtensionNames(arch.AMD64); len(names) == 0 {
|
||||
t.Error("the amd64 layer registers no names")
|
||||
}
|
||||
if got := arch.Extensions(arch.AMD64); len(got) == 0 {
|
||||
t.Error("arch.Extensions(AMD64) is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionNamesARM64 checks the completion-facing name list: every
|
||||
// distinct mnemonic of the family, first-occurrence order, no duplicates.
|
||||
func TestExtensionNamesARM64(t *testing.T) {
|
||||
want := []string{
|
||||
// The SVE integer add/subtract/multiply family.
|
||||
"ADD", "SUB", "SQADD", "UQADD", "SQSUB", "UQSUB", "MUL", "SMULH", "UMULH", "SUBR",
|
||||
// The SVE and SVE2.1 predicate family: the logical operations, the
|
||||
// breaks, the permutations, the singles, the first-fault group and
|
||||
// the while compares.
|
||||
"PAND", "PANDS", "PBIC", "PBICS", "PEOR", "PEORS",
|
||||
"PNAND", "PNANDS", "PNOR", "PNORS", "PORN", "PORNS", "PORR", "PORRS",
|
||||
"PSEL",
|
||||
"PBRKA", "PBRKAS", "PBRKB", "PBRKBS", "PBRKN", "PBRKNS",
|
||||
"PBRKPA", "PBRKPAS", "PBRKPB", "PBRKPBS",
|
||||
"PTRN1", "PTRN2", "PUZP1", "PUZP2", "PZIP1", "PZIP2",
|
||||
"PPFALSE", "PPFIRST", "PPNEXT", "PPTEST", "PPTRUE", "PPUNPKHI", "PPUNPKLO",
|
||||
"PRDFFR", "PRDFFRS", "PWRFFR", "PREV", "SETFFR",
|
||||
"PWHILEGE", "PWHILEGT", "PWHILEHI", "PWHILEHS",
|
||||
"PWHILELE", "PWHILELO", "PWHILELS", "PWHILELT", "PWHILERW", "PWHILEWR",
|
||||
// The SVE2.1 Z-alias families, in first-occurrence order.
|
||||
"ZABS",
|
||||
"ZREVB",
|
||||
"ZREVH",
|
||||
"ZSXTB",
|
||||
"ZSXTH",
|
||||
"ZUXTB",
|
||||
"ZUXTH",
|
||||
"ZADD",
|
||||
"ZAND",
|
||||
"ZBIC",
|
||||
"ZCLS",
|
||||
"ZCLZ",
|
||||
"ZCNOT",
|
||||
"ZCNT",
|
||||
"ZCOMPACT",
|
||||
"ZDECP",
|
||||
"ZDUP",
|
||||
"ZEOR",
|
||||
"ZEXPAND",
|
||||
"ZINSR",
|
||||
"ZLASTA",
|
||||
"ZLASTB",
|
||||
"ZMOVPRFX",
|
||||
"ZREVD",
|
||||
"ZREVW",
|
||||
"ZSXTW",
|
||||
"ZUXTW",
|
||||
"ZNEG",
|
||||
"ZNOT",
|
||||
"ZORR",
|
||||
"ZRBIT",
|
||||
"ZREV",
|
||||
"ZSEL",
|
||||
"ZSQABS",
|
||||
"ZSQNEG",
|
||||
"ZSUB",
|
||||
"ZSUBR",
|
||||
"ZSUNPKHI",
|
||||
"ZSUNPKLO",
|
||||
"ZTBX",
|
||||
"ZTBXQ",
|
||||
"ZTRN1",
|
||||
"ZTRN2",
|
||||
"ZUUNPKHI",
|
||||
"ZUUNPKLO",
|
||||
"ZUZP1",
|
||||
"ZUZP2",
|
||||
"ZUZPQ1",
|
||||
"ZUZPQ2",
|
||||
"ZZIP1",
|
||||
"ZZIP2",
|
||||
"ZZIPQ1",
|
||||
"ZZIPQ2",
|
||||
// The stage-four families: the SVE2.1 narrowing two-to-one set, the
|
||||
// pairwise forms and the quadword reductions.
|
||||
"ZADDHNB",
|
||||
"ZADDHNT",
|
||||
"ZRADDHNB",
|
||||
"ZRADDHNT",
|
||||
"ZRSUBHNB",
|
||||
"ZRSUBHNT",
|
||||
"ZSUBHNB",
|
||||
"ZSUBHNT",
|
||||
"ZADDP",
|
||||
"ZADDPT",
|
||||
"ZADDQV",
|
||||
"ZANDQV",
|
||||
"ZEORQV",
|
||||
"ZFADDQV",
|
||||
"ZFMAXNMQV",
|
||||
"ZFMAXQV",
|
||||
"ZFMINNMQV",
|
||||
"ZFMINQV",
|
||||
"ZORQV",
|
||||
"ZSMAXQV",
|
||||
"ZSMINQV",
|
||||
"ZUMAXQV",
|
||||
"ZUMINQV",
|
||||
"ZASR",
|
||||
"ZASRR",
|
||||
"ZLSL",
|
||||
"ZLSLR",
|
||||
"ZLSR",
|
||||
"ZLSRR",
|
||||
"ZBCAX",
|
||||
"ZBDEP",
|
||||
"ZBEXT",
|
||||
"ZBGRP",
|
||||
"ZBSL",
|
||||
"ZBSL1N",
|
||||
"ZBSL2N",
|
||||
"ZEOR3",
|
||||
"ZEORBT",
|
||||
"ZEORTB",
|
||||
"ZNBSL",
|
||||
"ZBF1CVT",
|
||||
"ZBF1CVTLT",
|
||||
"ZBF2CVT",
|
||||
"ZBF2CVTLT",
|
||||
"ZF1CVT",
|
||||
"ZF1CVTLT",
|
||||
"ZF2CVT",
|
||||
"ZF2CVTLT",
|
||||
"ZBFADD",
|
||||
"ZBFCLAMP",
|
||||
"ZBFMAX",
|
||||
"ZBFMAXNM",
|
||||
"ZBFMIN",
|
||||
"ZBFMINNM",
|
||||
"ZBFMUL",
|
||||
"ZBFSUB",
|
||||
"ZBFCVT",
|
||||
"ZBFCVTNT",
|
||||
"ZBFDOT",
|
||||
"ZBFMLA",
|
||||
"ZBFMLALB",
|
||||
"ZBFMLALT",
|
||||
"ZBFMLS",
|
||||
"ZBFMLSLB",
|
||||
"ZBFMLSLT",
|
||||
"ZBFMMLA",
|
||||
"ZBFSCALE",
|
||||
"ZCLASTA",
|
||||
"ZCLASTB",
|
||||
"ZCMPEQ",
|
||||
"ZCMPGE",
|
||||
"ZCMPGT",
|
||||
"ZCMPHI",
|
||||
"ZCMPHS",
|
||||
"ZCMPNE",
|
||||
"ADDPL",
|
||||
"ADDVL",
|
||||
"RDVL",
|
||||
"ZLD2B",
|
||||
"ZLD2D",
|
||||
"ZLD2H",
|
||||
"ZLD2Q",
|
||||
"ZLD2W",
|
||||
"ZLD3B",
|
||||
"ZLD3D",
|
||||
"ZLD3H",
|
||||
"ZLD3Q",
|
||||
"ZLD3W",
|
||||
"ZLD4B",
|
||||
"ZLD4D",
|
||||
"ZLD4H",
|
||||
"ZLD4Q",
|
||||
"ZLD4W",
|
||||
"ZST2B",
|
||||
"ZST2D",
|
||||
"ZST2H",
|
||||
"ZST2Q",
|
||||
"ZST2W",
|
||||
"ZST3B",
|
||||
"ZST3D",
|
||||
"ZST3H",
|
||||
"ZST3Q",
|
||||
"ZST3W",
|
||||
"ZST4B",
|
||||
"ZST4D",
|
||||
"ZST4H",
|
||||
"ZST4Q",
|
||||
"ZST4W",
|
||||
// The stage-three families: the SVE2 crypto group, the predicate
|
||||
// counters and loop terminators with the 32-bit while compares, and
|
||||
// the reductions into a SIMD register.
|
||||
"ZADCLB",
|
||||
"ZADCLT",
|
||||
"ZSBCLB",
|
||||
"ZSBCLT",
|
||||
"ZRAX1",
|
||||
"ZSM4EKEY",
|
||||
"ZSM4E",
|
||||
"ZAESD",
|
||||
"ZAESE",
|
||||
"ZAESIMC",
|
||||
"ZAESMC",
|
||||
"CTERMEQ",
|
||||
"CTERMEQW",
|
||||
"CTERMNE",
|
||||
"CTERMNEW",
|
||||
"PCNTP",
|
||||
"PFIRSTP",
|
||||
"PLASTP",
|
||||
"PDECP",
|
||||
"PINCP",
|
||||
"PSQDECP",
|
||||
"PSQINCP",
|
||||
"PUQDECP",
|
||||
"PUQINCP",
|
||||
"PUQDECPW",
|
||||
"PUQINCPW",
|
||||
"PSQDECPW",
|
||||
"PSQINCPW",
|
||||
"PWHILEGEW",
|
||||
"PWHILEGTW",
|
||||
"PWHILEHIW",
|
||||
"PWHILEHSW",
|
||||
"PWHILELEW",
|
||||
"PWHILELOW",
|
||||
"PWHILELSW",
|
||||
"PWHILELTW",
|
||||
"ZSADDVD",
|
||||
"ZUADDVD",
|
||||
"ZANDVB",
|
||||
"ZANDVH",
|
||||
"ZANDVS",
|
||||
"ZANDVD",
|
||||
"ZEORVB",
|
||||
"ZEORVH",
|
||||
"ZEORVS",
|
||||
"ZEORVD",
|
||||
"ZORVB",
|
||||
"ZORVH",
|
||||
"ZORVS",
|
||||
"ZORVD",
|
||||
"ZSMAXVB",
|
||||
"ZSMAXVH",
|
||||
"ZSMAXVS",
|
||||
"ZSMAXVD",
|
||||
"ZSMINVB",
|
||||
"ZSMINVH",
|
||||
"ZSMINVS",
|
||||
"ZSMINVD",
|
||||
"ZUMAXVB",
|
||||
"ZUMAXVH",
|
||||
"ZUMAXVS",
|
||||
"ZUMAXVD",
|
||||
"ZUMINVB",
|
||||
"ZUMINVH",
|
||||
"ZUMINVS",
|
||||
"ZUMINVD",
|
||||
"ZFADDVH",
|
||||
"ZFADDVS",
|
||||
"ZFADDVD",
|
||||
"ZFMAXNMVH",
|
||||
"ZFMAXNMVS",
|
||||
"ZFMAXNMVD",
|
||||
"ZFMAXVH",
|
||||
"ZFMAXVS",
|
||||
"ZFMAXVD",
|
||||
"ZFMINNMVH",
|
||||
"ZFMINNMVS",
|
||||
"ZFMINNMVD",
|
||||
"ZFMINVH",
|
||||
"ZFMINVS",
|
||||
"ZFMINVD",
|
||||
"ZFADDAH",
|
||||
"ZFADDAS",
|
||||
"ZFADDAD",
|
||||
// The gather loads and scatter stores: plain, sign-extended and
|
||||
// first-fault loads, and the stores, in first-occurrence order.
|
||||
"ZLD1B",
|
||||
"ZLD1D",
|
||||
"ZLD1H",
|
||||
"ZLD1SB",
|
||||
"ZLD1SH",
|
||||
"ZLD1SW",
|
||||
"ZLD1W",
|
||||
"ZLDFF1B",
|
||||
"ZLDFF1D",
|
||||
"ZLDFF1H",
|
||||
"ZLDFF1SB",
|
||||
"ZLDFF1SH",
|
||||
"ZLDFF1SW",
|
||||
"ZLDFF1W",
|
||||
"ZST1B",
|
||||
"ZST1D",
|
||||
"ZST1H",
|
||||
"ZST1W",
|
||||
// The shift-immediate classes: the narrowing and widening
|
||||
// three-vector shifts and the predicated saturating left shifts,
|
||||
// in first-occurrence order.
|
||||
"ZSQSHRUNB",
|
||||
"ZSQSHRUNT",
|
||||
"ZSHRNB",
|
||||
"ZSHRNT",
|
||||
"ZRSHRNB",
|
||||
"ZRSHRNT",
|
||||
"ZSQSHRNB",
|
||||
"ZSQSHRNT",
|
||||
"ZSQRSHRNB",
|
||||
"ZSQRSHRNT",
|
||||
"ZUQSHRNB",
|
||||
"ZUQSHRNT",
|
||||
"ZUQRSHRNB",
|
||||
"ZUQRSHRNT",
|
||||
"ZSSHLLB",
|
||||
"ZSSHLLT",
|
||||
"ZUSHLLB",
|
||||
"ZUSHLLT",
|
||||
"ZSQSHL",
|
||||
"ZSQSHLU",
|
||||
"ZUQSHL",
|
||||
"ZSRI",
|
||||
"ZSSRA",
|
||||
"ZUSRA",
|
||||
"ZSRSRA",
|
||||
"ZURSRA",
|
||||
"ZASRD",
|
||||
"ZXAR",
|
||||
}
|
||||
got := ExtensionNames(arch.ARM64)
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Errorf("ExtensionNames(ARM64) = %v, want %v", got, want)
|
||||
}
|
||||
if n := len(arch.Extensions(arch.ARM64)); n != 537 {
|
||||
t.Errorf("the family registers %d instructions, want 537", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// flagListShapes are the TEXT shapes the flags operand decides: joined
|
||||
// names, the legacy numeric spellings, the arithmetic and immediate forms,
|
||||
// each with a frame size beside it. A flag the operand carries suppresses
|
||||
// the stack-split guard; the toolchain reads one integer from the operand,
|
||||
// and after the front end folds it the bytes must not tell the difference.
|
||||
// The bodies are leaf-shaped on purpose: the frame engine's open divergences
|
||||
// (the NOFRAME prologue for a non-leaf, the big-frame guard's shape) sit
|
||||
// outside the flags operand and are somebody else's gap list.
|
||||
var flagListShapes = []struct {
|
||||
name string // the subtest's name
|
||||
flags string // the flags operand as written
|
||||
frame string // the frame operand as written
|
||||
body string // the function body
|
||||
}{
|
||||
{"name", "NOSPLIT", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
|
||||
{"numeric", "4", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
|
||||
{"numericOR", "2|4", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
|
||||
{"joinedFrame", "DUPOK|NOSPLIT", "$4096-0", "\tMOVD R0, R1\n\tRET\n"},
|
||||
{"joinedFrameless", "NOSPLIT|TOPFRAME", "$0-0", "\tRET\n"},
|
||||
{"parenthesised", "(NOSPLIT|NOFRAME)", "$0-0", "\tMOVD R0, R1\n\tRET\n"},
|
||||
}
|
||||
|
||||
// TestFlagListAssemblyPathRejectsUnknown holds the front end's rejections
|
||||
// that mirror the toolchain's: an identifier outside the flag table
|
||||
// ("unexpected TYPO evaluating expression") and the immediate spelling of
|
||||
// the operand ("TEXT: expected integer constant"), each refused on the
|
||||
// assembly path before the encoder ever sees a tree.
|
||||
func TestFlagListAssemblyPathRejectsUnknown(t *testing.T) {
|
||||
for _, c := range []struct{ src, want string }{
|
||||
{"#include \"textflag.h\"\n\nTEXT f(SB), NOSPLIT|TYPO, $0-0\n\tRET\n",
|
||||
"unexpected TYPO evaluating expression"},
|
||||
{"#include \"textflag.h\"\n\nTEXT f(SB), $NOSPLIT, $0-0\n\tRET\n",
|
||||
"TEXT: expected integer constant; found $NOSPLIT"},
|
||||
{"#include \"textflag.h\"\n\nGLOBL g<>(SB), $8, $8\n",
|
||||
"GLOBL: expected integer constant; found $8"},
|
||||
} {
|
||||
_, errs := parser.ParseWithOptions("f.s", c.src, parser.Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), c.want) {
|
||||
t.Errorf("errors = %v, want %q", errs, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlagListByteParity assembles every flag-list shape through the fixed
|
||||
// front end and holds the function's bytes equal to the installed
|
||||
// toolchain's, no guard words where the flag suppresses them and none
|
||||
// missing where it demands one.
|
||||
func TestFlagListByteParity(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live go tool asm oracle: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
out, err := exec.Command(goBin, "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go env GOROOT: %v", err)
|
||||
}
|
||||
include := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
||||
|
||||
for _, c := range flagListShapes {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
src := "#include \"textflag.h\"\n\nTEXT f(SB), " + c.flags + ", " + c.frame + "\n" + c.body
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "f.s")
|
||||
if err := os.WriteFile(file, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
af, errs := parser.ParseWithOptions(file, src, parser.Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs[0])
|
||||
}
|
||||
img, err := AssembleFileARM64(af)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 1 {
|
||||
t.Fatalf("%d functions, want 1", len(img.Funcs))
|
||||
}
|
||||
|
||||
objPath := filepath.Join(t.TempDir(), "oracle.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-std", "-I", include,
|
||||
"-p", "flaglisttest", "-o", objPath, file)
|
||||
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64")
|
||||
if oout, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go tool asm: %v\n%s", err, oout)
|
||||
}
|
||||
blocks, ok := oracleFuncText(t, mustRead(t, objPath))["f"]
|
||||
if !ok {
|
||||
t.Fatal("the oracle output carries no f")
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("%d text blocks for f, want 1", len(blocks))
|
||||
}
|
||||
goCode := blocks[0]
|
||||
fn := img.Funcs[0]
|
||||
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
cmpLen := min(len(goCode), len(gasmCode))
|
||||
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
||||
t.Errorf("bytes differ:\ngasm: % x\ngo: % x", gasmCode[:cmpLen], goCode[:cmpLen])
|
||||
}
|
||||
if len(goCode) > len(gasmCode) {
|
||||
for _, b := range goCode[len(gasmCode):] {
|
||||
if b != 0 {
|
||||
t.Errorf("non-zero trailing bytes in the oracle output")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+167
-21
@@ -14,8 +14,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This file emits GOOBJ — the Go toolchain's object format, which cmd/link
|
||||
// consumes directly — so gasm-assembled functions drop into a go build
|
||||
// This file emits GOOBJ, the Go toolchain's object format, which cmd/link
|
||||
// consumes directly, so gasm-assembled functions drop into a go build
|
||||
// without the Go assembler. The layout follows cmd/internal/goobj: a
|
||||
// toolchain preamble ("go object ...\n!\n"), the go120ld header with its
|
||||
// block offsets, a string table, symbol definitions, the relocation /
|
||||
@@ -63,16 +63,18 @@ const (
|
||||
const (
|
||||
kindSTEXT = 1
|
||||
kindSRODATA = 3
|
||||
kindSNOPTRDATA = 5
|
||||
kindSDATA = 7
|
||||
kindSDWARFFCN = 14
|
||||
kindSDWARFLINES = 20
|
||||
)
|
||||
|
||||
// Symbol flags (cmd/internal/goobj).
|
||||
// Symbol flags (cmd/internal/goobj). The linkname flag is set only for
|
||||
// //go:linkname symbols (and main.main); ordinary assembly symbols carry
|
||||
// none, matching cmd/asm's output.
|
||||
const (
|
||||
symFlagDupok = 0x01
|
||||
symFlagNoSplit = 0x10
|
||||
symFlag2Link = 0x10 // asm objects flag every named symbol as linkname
|
||||
symABIStatic = 0xffff
|
||||
)
|
||||
|
||||
@@ -94,10 +96,12 @@ const (
|
||||
)
|
||||
|
||||
// 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 (
|
||||
relocPCRel = 14 // R_PCREL
|
||||
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
|
||||
@@ -140,10 +144,30 @@ func isGo127OrLater() bool {
|
||||
|
||||
// Special package indices for symbol references.
|
||||
const (
|
||||
pkgIdxNone = 0x7fffffff
|
||||
pkgIdxSelf = 0x7ffffffb
|
||||
pkgIdxNone = 0x7fffffff
|
||||
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"
|
||||
|
||||
// goSym is one symbol definition under construction.
|
||||
@@ -178,14 +202,24 @@ type dwarfRelocSet struct {
|
||||
// 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
|
||||
// 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) {
|
||||
pre, err := toolchainObjectPreamble()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// amd64: MinLC 1, R_PCREL for the code relocations.
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
|
||||
// amd64: MinLC 1, R_PCREL for displacements, R_CALL for calls and
|
||||
// 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
|
||||
@@ -198,7 +232,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
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
|
||||
// and the function itself, as cmd/asm lays them out.
|
||||
type npSym struct {
|
||||
@@ -247,7 +281,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||
// zeroing r.Off would erase the opcode/register bits the linker
|
||||
// preserves when it patches only the immediate.
|
||||
if r.Kind != RelPCRel32 {
|
||||
if r.Kind != RelPCRel32 && r.Kind != RelCall {
|
||||
continue
|
||||
}
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
@@ -255,7 +289,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
@@ -272,9 +306,16 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
if !d.Static {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
// RODATA implies no pointers, so it wins over NOPTR: the kind is
|
||||
// SRODATA either way, exactly as the toolchain chooses it. Plain
|
||||
// NOPTR data is SNOPTRDATA, which the linker keeps out of the GC's
|
||||
// type scan; a plain SDATA symbol would demand Go type information
|
||||
// no assembly file can supply, and the link would fail.
|
||||
typ := uint8(kindSDATA)
|
||||
if d.Rodata {
|
||||
typ = kindSRODATA
|
||||
} else if d.Noptr {
|
||||
typ = kindSNOPTRDATA
|
||||
}
|
||||
flag := uint8(0)
|
||||
if d.Dupok {
|
||||
@@ -285,7 +326,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
abi = symABIStatic
|
||||
}
|
||||
defIdx[d.Name] = len(defs)
|
||||
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)})
|
||||
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, size: uint32(d.Size)})
|
||||
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||
}
|
||||
fnFiIdx := make([]int, len(img.Funcs))
|
||||
@@ -320,6 +361,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
|
||||
// package index table and determine each external symbol's SymIdx
|
||||
// by reading the target package's export data.
|
||||
@@ -327,10 +375,20 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
var extPkgIdx map[string]int
|
||||
var extSymIdx map[string]int
|
||||
if len(img.Externals) > 0 {
|
||||
var err error
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||
// 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
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +400,31 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
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 {
|
||||
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
||||
pkg, name := splitQualified(r.Name)
|
||||
@@ -366,20 +449,83 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
pkg := uint32(pkgIdxSelf)
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
// 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)
|
||||
}
|
||||
pkg = pkgIdxNone
|
||||
di = ni
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkg)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
}
|
||||
// The data symbols' own relocations: the symbol-valued DATA fields
|
||||
// ("DATA s+0(SB)/8, $other(SB)"). The toolchain patches each field
|
||||
// with the target's absolute address through an R_ADDR of the DATA
|
||||
// line's width, on every architecture (the code relocations are
|
||||
// per-architecture PC-relative shapes; a data pointer word is not), so
|
||||
// this mapping bypasses relocField. The definitions were appended in
|
||||
// DataSyms order, so data symbol i is definition index i.
|
||||
for i, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
if r.Kind != RelAddr {
|
||||
return nil, fmt.Errorf("GOOBJ emission: data symbol %q carries a non-data relocation", d.Name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = r.Siz
|
||||
binary.LittleEndian.PutUint16(rec[5:], relocAddr)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
switch {
|
||||
case r.External && r.Name == goobjBuiltinMorestack:
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxBuiltin)
|
||||
binary.LittleEndian.PutUint32(rec[19:], goobjBuiltinMorestackNoctxt)
|
||||
case r.External:
|
||||
pkg, name := splitQualified(r.Name)
|
||||
if pkg == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name)
|
||||
}
|
||||
pIdx, ok := extPkgIdx[pkg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg)
|
||||
}
|
||||
sIdx, ok := extSymIdx[pkg+"·"+name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
|
||||
default:
|
||||
if di, ok := defIdx[r.Name]; ok {
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
break
|
||||
}
|
||||
// A DATA field may hold the address of a TEXT function of
|
||||
// the same file (the rt0 lib entry spelling), which is a
|
||||
// non-package definition.
|
||||
ni, isText := textNpIdx[r.Name]
|
||||
if !isText {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxNone)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(ni))
|
||||
}
|
||||
symRelocs[i] = append(symRelocs[i], rec[:]...)
|
||||
}
|
||||
}
|
||||
// The DWARF symbols' own relocations (the function address references).
|
||||
for _, ds := range dwarfRelocs {
|
||||
for _, r := range ds.relocs {
|
||||
|
||||
+73
-33
@@ -40,8 +40,11 @@ func exportPath(importPath string) (string, error) {
|
||||
//
|
||||
// refs maps package import paths to the symbol names referenced from that
|
||||
// package. The returned pkgIdx maps each import path to its position in
|
||||
// the blkPkgIdx table (0-based), and symIdx gives each symbol's index within
|
||||
// its package.
|
||||
// the blkPkgIdx table, which reserves index 0 for the dummy invalid
|
||||
// package (cmd/internal/obj/sym.go: "0 is invalid index"; the loader's
|
||||
// reader loop starts at 1), so package i sits at block index i+1 and its
|
||||
// relocations carry i+1. symIdx gives each symbol's index within its
|
||||
// package.
|
||||
func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symIdx map[string]int, err error) {
|
||||
pkgIdx = make(map[string]int, len(refs))
|
||||
symIdx = make(map[string]int)
|
||||
@@ -50,7 +53,9 @@ func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symI
|
||||
packages := sortedPkgRefs(refs)
|
||||
|
||||
for i, pkg := range packages {
|
||||
pkgIdx[pkg.path] = i
|
||||
// Block index 0 is the dummy invalid package; the first real
|
||||
// package starts at 1.
|
||||
pkgIdx[pkg.path] = i + 1
|
||||
exp, err := exportPath(pkg.path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -84,7 +89,7 @@ func sortedPkgRefs(refs map[string][]string) []pkgRef {
|
||||
for pkg, syms := range refs {
|
||||
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 j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- {
|
||||
pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1]
|
||||
@@ -145,40 +150,65 @@ func parseArDecimal(b []byte) int {
|
||||
}
|
||||
|
||||
// goobjFile is a parsed GOOBJ file: the string table and the symbol-definition
|
||||
// block.
|
||||
// blocks. The hashed blocks are kept raw: their symbols carry no names, only
|
||||
// the loader needs their counts.
|
||||
type goobjFile struct {
|
||||
strTab []byte // string table, at headerSize + n
|
||||
symdef []byte // blkSymdef raw block
|
||||
npdef []byte // blkNonpkgdef raw block
|
||||
strTab []byte // string table, at headerSize + n
|
||||
symdef []byte // blkSymdef raw block
|
||||
hashed64 []byte // blkHashed64def raw block
|
||||
hashed []byte // blkHasheddef raw block
|
||||
npdef []byte // blkNonpkgdef raw block
|
||||
}
|
||||
|
||||
// symbols returns all symbol names in definition order by scanning the
|
||||
// symdef and nonpkgdef blocks and resolving each name through the string
|
||||
// table. Package definitions (blkSymdef) use fully-qualified names like
|
||||
// "runtime.morestack"; non-package definitions (blkNonpkgdef) use bare
|
||||
// names like "morestack". This combined list matches the index the
|
||||
// linker expects for cross-package references.
|
||||
// loaderIndexBase returns the index the first nonpkgdef symbol occupies in the
|
||||
// loader's per-object symbol array. cmd/link lays the definition blocks out as
|
||||
// symdef, hashed64def, hasheddef, nonpkgdef, nonpkgref (loader.go: preloadSyms
|
||||
// fills r.syms in exactly that order, and resolve() indexes PkgIdxNone and
|
||||
// cross-package SymIdx into it), so a symbol found in blkNonpkgdef carries the
|
||||
// three leading blocks' symbol counts as its base.
|
||||
func (f *goobjFile) loaderIndexBase() int {
|
||||
return len(f.symdef)/recSymSize + len(f.hashed64)/recSymSize + len(f.hashed)/recSymSize
|
||||
}
|
||||
|
||||
// symbols returns the names of the symdef and nonpkgdef blocks in
|
||||
// definition order. Package definitions (blkSymdef) use fully-qualified
|
||||
// names like "runtime.morestack"; non-package definitions (blkNonpkgdef)
|
||||
// use bare names like "morestack". For lookups by index prefer
|
||||
// findSymbol: it adds the hashed blocks' count the loader's array
|
||||
// interleaves between the two.
|
||||
func (f *goobjFile) symbols() []string {
|
||||
return append(f.defNames(), f.npdefNames()...)
|
||||
}
|
||||
|
||||
// findSymbol returns the index of a symbol within the combined symbol list,
|
||||
// or -1 if not found. It first tries the fully-qualified name (pkg.name),
|
||||
// then the bare name.
|
||||
// findSymbol returns the index of a symbol within the loader's per-object
|
||||
// symbol array, or -1 if not found. It first tries the fully-qualified
|
||||
// name (pkg.name), then the bare name (assembly objects store dotless
|
||||
// names, e.g. runtime's "gogo", for symbols other packages reach through
|
||||
// a linkname).
|
||||
func (f *goobjFile) findSymbol(pkg, name string) int {
|
||||
base := f.loaderIndexBase()
|
||||
qualified := pkg + "." + name
|
||||
syms := f.symbols()
|
||||
for i, s := range syms {
|
||||
for i, s := range f.defNames() {
|
||||
if s == qualified {
|
||||
return i
|
||||
}
|
||||
}
|
||||
// Try bare name (for non-package definitions).
|
||||
for i, s := range syms {
|
||||
for i, s := range f.npdefNames() {
|
||||
if s == qualified {
|
||||
return base + i
|
||||
}
|
||||
}
|
||||
// Try bare name (for dotless assembly definitions).
|
||||
for i, s := range f.defNames() {
|
||||
if s == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
for i, s := range f.npdefNames() {
|
||||
if s == name {
|
||||
return base + i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -192,12 +222,16 @@ func (f *goobjFile) npdefNames() []string {
|
||||
return f.readSymNames(f.npdef)
|
||||
}
|
||||
|
||||
// recSymSize is the size of one Sym record in the definition blocks
|
||||
// (goobj.SymSize: stringRefSize + 2 + 1 + 1 + 1 + 4 + 4).
|
||||
const recSymSize = 21
|
||||
|
||||
// readSymNames reads symbol names from a symdef/nonpkgdef block. Each record
|
||||
// is 21 bytes: nameLen (u32), nameOff (u32), abi (u16), typ, flag, flag2,
|
||||
// size (u32), align (u32). nameOff is an absolute offset into the string
|
||||
// table.
|
||||
func (f *goobjFile) readSymNames(block []byte) []string {
|
||||
const recSize = 21
|
||||
const recSize = recSymSize
|
||||
if len(block) < recSize {
|
||||
return nil
|
||||
}
|
||||
@@ -247,16 +281,18 @@ func parseGOOBJ(data []byte) (*goobjFile, error) {
|
||||
// [16:20] flags
|
||||
// [20:96] 19 × uint32 offsets
|
||||
var offs [blkEnd + 1]uint32
|
||||
for i := 0; i <= blkEnd; i++ {
|
||||
for i := range blkEnd + 1 {
|
||||
offs[i] = binary.LittleEndian.Uint32(payload[20+4*i:])
|
||||
}
|
||||
// The string table lives at headerSize.
|
||||
strTabStart := uint32(goobjHeaderSize)
|
||||
|
||||
f := &goobjFile{
|
||||
strTab: payload[strTabStart:offs[0]],
|
||||
symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1),
|
||||
npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1),
|
||||
strTab: payload[strTabStart:offs[0]],
|
||||
symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1),
|
||||
hashed64: blockSlice(payload, offs, blkHashed64def, blkHashed64def+1),
|
||||
hashed: blockSlice(payload, offs, blkHasheddef, blkHasheddef+1),
|
||||
npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1),
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -306,22 +342,26 @@ func resolveExternalSymbols(externals []string) (pkgTable []string, pkgIdxMap ma
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Build the package table in pkgIdx order.
|
||||
// Build the package table in pkgIdx order. The indices are 1-based
|
||||
// (0 is the dummy invalid package, written by the emitter itself), so
|
||||
// the table without the dummy is indexed one below.
|
||||
pkgTable = make([]string, len(pkgIdx1))
|
||||
for pkg, idx := range pkgIdx1 {
|
||||
pkgTable[idx] = pkg
|
||||
pkgTable[idx-1] = pkg
|
||||
}
|
||||
|
||||
return pkgTable, pkgIdx1, symIdx1, nil
|
||||
}
|
||||
|
||||
// splitQualified splits a qualified Go symbol name (pkgpath·name) into its
|
||||
// package path and local name. The separator is the middle dot (U+00B7).
|
||||
// If no separator is found, the symbol is assumed to be in the current
|
||||
// package (empty pkg).
|
||||
// package path and local name. The separator is the middle dot (U+00B7),
|
||||
// whose UTF-8 encoding is two bytes, so the search must be string-based:
|
||||
// IndexByte would match only the second byte and leave the lead byte on
|
||||
// the package path. If no separator is found, the symbol is assumed to be
|
||||
// in the current package (empty pkg).
|
||||
func splitQualified(full string) (pkg, name string) {
|
||||
if idx := strings.IndexByte(full, '\u00b7'); idx >= 0 {
|
||||
return full[:idx], full[idx+len("\u00b7"):]
|
||||
if before, after, ok := strings.Cut(full, "\u00b7"); ok {
|
||||
return before, after
|
||||
}
|
||||
if before, after, ok := strings.Cut(full, "."); ok {
|
||||
return before, after
|
||||
|
||||
@@ -45,6 +45,9 @@ func TestReadRuntimeSymbols(t *testing.T) {
|
||||
// TestResolveExternalSymbols verifies end-to-end resolution of external
|
||||
// symbol references.
|
||||
func TestResolveExternalSymbols(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("resolves through a live go list -export: skipped in -short mode")
|
||||
}
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("go toolchain not available")
|
||||
}
|
||||
@@ -56,8 +59,12 @@ func TestResolveExternalSymbols(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("resolveExternalGOOBJ: %v", err)
|
||||
}
|
||||
if len(pkgIdx) != 1 || pkgIdx["runtime"] != 0 {
|
||||
t.Errorf("pkgIdx = %v, want runtime→0", pkgIdx)
|
||||
if len(pkgIdx) != 1 || pkgIdx["runtime"] != 1 {
|
||||
// Index 0 is the dummy invalid package in the blkPkgIdx table;
|
||||
// the loader's reader loop starts at 1 (cmd/link/internal/
|
||||
// loader/loader.go: "PkgIdx 0 is a dummy invalid package"), so
|
||||
// the first real package must carry index 1.
|
||||
t.Errorf("pkgIdx = %v, want runtime→1", pkgIdx)
|
||||
}
|
||||
if _, ok := symIdx["runtime·g0"]; !ok {
|
||||
t.Errorf("symIdx missing runtime·g0, got %v", symIdx)
|
||||
|
||||
+335
-6
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||||
@@ -117,7 +117,9 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
if len(defs) != 7 {
|
||||
t.Fatalf("symdefs = %d, want 7", len(defs))
|
||||
}
|
||||
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link {
|
||||
// The linkname flag stays clear: the toolchain sets it only for
|
||||
// //go:linkname symbols, and an ordinary static GLOBL is not one.
|
||||
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != 0 {
|
||||
t.Errorf("mask symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||||
@@ -158,8 +160,8 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
t.Errorf("funcinfo bytes %x", fi)
|
||||
}
|
||||
|
||||
// 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
|
||||
// The pc-value tables of addq (non-package indices 0-3, so global
|
||||
// indices 7-10): pcsp a flat zero over the whole function, pcinline a
|
||||
// flat -1, both with the pc delta in MinLC (1) units.
|
||||
pcsp := data[le.Uint32(didx[4*7:]):]
|
||||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||||
@@ -294,7 +296,7 @@ TEXT ·framed(SB), NOSPLIT, $8-0
|
||||
}
|
||||
for i := range wantPCs {
|
||||
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.
|
||||
@@ -333,7 +335,7 @@ TEXT ·useext(SB), NOSPLIT, $0-8
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// available.
|
||||
func TestGOObjectLinkAndRun(t *testing.T) {
|
||||
@@ -511,3 +513,330 @@ func fieldAfter(line, flag string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildLogSteps is what a substitution test needs from a `go build -x -work`
|
||||
// log: the work directory, the assembler's object, the package archive and
|
||||
// the link command line.
|
||||
type buildLogSteps struct {
|
||||
work string
|
||||
asmObj string // $WORK expanded
|
||||
pkgArch string // $WORK expanded
|
||||
linkLine string // still carries $WORK placeholders
|
||||
}
|
||||
|
||||
// parseBuildLog extracts the build steps from a `go build -x -work` log.
|
||||
// asmFile names the assembly file whose object the test substitutes. A
|
||||
// missing step is a failure, not a skip: the toolchain changed shape and the
|
||||
// substitution would silently test nothing.
|
||||
func parseBuildLog(t *testing.T, log []byte, asmFile string) buildLogSteps {
|
||||
t.Helper()
|
||||
var st buildLogSteps
|
||||
for line := range strings.SplitSeq(string(log), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
st.work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, asmFile) && !strings.Contains(line, "-gensymabis"):
|
||||
st.asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
rest := strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
st.pkgArch = strings.Fields(strings.SplitN(rest, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
st.linkLine = line
|
||||
}
|
||||
}
|
||||
if st.work == "" || st.asmObj == "" || st.pkgArch == "" || st.linkLine == "" {
|
||||
t.Fatalf("could not locate the build steps (work=%q asmObj=%q pkgArch=%q link=%q):\n%s",
|
||||
st.work, st.asmObj, st.pkgArch, st.linkLine, log)
|
||||
}
|
||||
st.asmObj = strings.ReplaceAll(st.asmObj, "$WORK", st.work)
|
||||
st.pkgArch = strings.ReplaceAll(st.pkgArch, "$WORK", st.work)
|
||||
return st
|
||||
}
|
||||
|
||||
// substituteAndRelink swaps the gasm object into the package archive the
|
||||
// baseline build produced and re-runs the captured link line against the
|
||||
// rebuilt archive, writing the binary to outBin (the -x log's link step
|
||||
// always targets the action graph's internal a.out, which the helper
|
||||
// redirects; the copy to the -o target is a separate build action the helper
|
||||
// does not need). The archive handed to the linker is proven to carry the
|
||||
// gasm object byte for byte, so a build-layout change that skipped the
|
||||
// substitution fails here instead of passing vacuously.
|
||||
func substituteAndRelink(t *testing.T, goBin, dir string, st buildLogSteps, outBin string, gasmObj []byte, extraEnv ...string) {
|
||||
t.Helper()
|
||||
|
||||
// The deliberate-run boundary: this path drives a real `go build` and
|
||||
// cmd/link per invocation, minutes-scale work on the small single-core
|
||||
// CI runner. Under -short (the push pipeline's mode) it skips; the
|
||||
// local test gate and the dispatched workflows run it in full.
|
||||
if testing.Short() {
|
||||
t.Skip("end-to-end go build and link: skipped in -short mode")
|
||||
}
|
||||
|
||||
// Extract the archive, overwrite the assembler's member with the gasm
|
||||
// object and repack (go tool pack has no replace-in-place).
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", st.pkgArch)
|
||||
extract.Dir = membersDir
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
member := filepath.Join(membersDir, filepath.Base(st.asmObj))
|
||||
if _, err := os.Stat(member); err != nil {
|
||||
t.Fatalf("the assembler's archive member was not extracted: %v", err)
|
||||
}
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, gasmObj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", st.pkgArch)
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, m)
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Prove the substitution: the archive the linker is about to consume
|
||||
// holds the gasm object, byte for byte.
|
||||
checkDir := filepath.Join(dir, "check")
|
||||
if err := os.MkdirAll(checkDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := exec.Command(goBin, "tool", "pack", "x", newArch)
|
||||
check.Dir = checkDir
|
||||
if out, err := check.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x (verification): %v\n%s", err, out)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(checkDir, filepath.Base(st.asmObj)))
|
||||
if err != nil {
|
||||
t.Fatalf("read the substituted member back: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, gasmObj) {
|
||||
t.Fatal("the repacked archive does not carry the gasm object")
|
||||
}
|
||||
|
||||
// Re-link. The line carries a GOROOT assignment and $WORK placeholders;
|
||||
// GOEXPERIMENT must match the toolchain's own, because the linker
|
||||
// compares the object header against its configuration.
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
linkLine := strings.ReplaceAll(st.linkLine, "$WORK", st.work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "exe", "a.out"), outBin)
|
||||
env := append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)))
|
||||
env = append(env, extraEnv...)
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
link.Env = env
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with the gasm object: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectExternalPackageLink is the cross-package end-to-end check: a
|
||||
// GOOBJ whose code references a real external package symbol (runtime's
|
||||
// morestack, a plain reference rather than the builtin noctxt form) must
|
||||
// carry a package index that points past the blkPkgIdx table's dummy entry
|
||||
// 0, and the object must link against the real runtime. Pre-fix, the
|
||||
// relocations carried block index 0, which the loader never fills, so the
|
||||
// reference resolved against whatever object was loaded first and the link
|
||||
// failed. The binary is not run: morestack returns to the call site's
|
||||
// stack check, which a hand-written caller has none of.
|
||||
func TestGOObjectExternalPackageLink(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("end-to-end go build and link: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
const asmSrc = `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·fn(SB), NOSPLIT, $0-0
|
||||
CALL ·helper(SB)
|
||||
RET
|
||||
|
||||
TEXT ·helper(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
`
|
||||
const mainSrc = `package main
|
||||
|
||||
func fn()
|
||||
func helper()
|
||||
|
||||
func main() {
|
||||
fn()
|
||||
helper()
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module extlink\n\ngo 1.27\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the build the toolchain performs and re-run only its link
|
||||
// step with our object swapped into the package archive, mirroring
|
||||
// TestGOObjectLinkAndRun.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj, pkgArch string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||||
|
||||
// Assemble the source with gasm, then retarget fn's internal call at
|
||||
// a real external package symbol: the reloc's qualified name drives
|
||||
// the export-data resolution the way a source-level runtime·sym(SB)
|
||||
// reference would.
|
||||
f, errs := parser.Parse("main_amd64.s", asmSrc)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
fn := &img.Funcs[0]
|
||||
for i := range fn.Relocs {
|
||||
fn.Relocs[i].Name = "runtime\u00b7morestack"
|
||||
fn.Relocs[i].External = true
|
||||
}
|
||||
img.Externals = []string{"runtime\u00b7morestack"}
|
||||
obj, err := img.GOObject("main", "main_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
|
||||
// Structural check: the blkPkgIdx block reserves entry 0 for the
|
||||
// dummy invalid package and places runtime at entry 1, and fn's call
|
||||
// relocation carries PkgIdx 1.
|
||||
v := openGoobj(t, obj)
|
||||
pkgBlk := v.blk(blkPkgIdx)
|
||||
if len(pkgBlk) != 2*8 {
|
||||
t.Fatalf("blkPkgIdx = %d bytes, want two entries", len(pkgBlk))
|
||||
}
|
||||
le := binary.LittleEndian
|
||||
strEntry := func(i int) string {
|
||||
e := pkgBlk[i*8 : (i+1)*8]
|
||||
return v.str(le.Uint32(e[4:]), le.Uint32(e[0:]))
|
||||
}
|
||||
if s := strEntry(0); s != "" {
|
||||
t.Errorf("blkPkgIdx[0] = %q, want the dummy empty package", s)
|
||||
}
|
||||
if s := strEntry(1); s != "runtime" {
|
||||
t.Errorf("blkPkgIdx[1] = %q, want runtime", s)
|
||||
}
|
||||
relocs := v.blk(blkReloc)
|
||||
// fn is the last non-package symbol (two functions, four pc tables
|
||||
// each); its one reloc is the final record.
|
||||
fnRec := relocs[len(relocs)-23:]
|
||||
if pIdx := le.Uint32(fnRec[15:]); pIdx != 1 {
|
||||
t.Errorf("external reloc PkgIdx = %d, want 1 (runtime)", pIdx)
|
||||
}
|
||||
|
||||
// Swap the object into the package archive and link with cmd/link;
|
||||
// the link line consumes the archive, not the loose object file.
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
extract.Dir = membersDir
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
member := filepath.Join(membersDir, filepath.Base(asmObj))
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, filepath.Join(membersDir, m))
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, pkgArch, newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "prog2"))
|
||||
linkCmd := exec.Command("sh", "-c", "cd "+dir+" && "+linkLine)
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// The call must have resolved to the real runtime symbol.
|
||||
dump, err := exec.Command(goBin, "tool", "objdump", "-s", "main.fn", filepath.Join(dir, "prog2")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("objdump main.fn: %v\n%s", err, dump)
|
||||
}
|
||||
if !bytes.Contains(dump, []byte("runtime.morestack")) {
|
||||
t.Errorf("main.fn does not call runtime.morestack:\n%s", dump)
|
||||
}
|
||||
}
|
||||
+38
-9
@@ -13,28 +13,57 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
// reloc/aux/data index arrays — with the arm64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and R_ADDRARM64 relocation types for the
|
||||
// ADRP+ADD/LDR/STR address pairs.
|
||||
// reloc/aux/data index arrays, with the arm64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and the arm64 relocation types for the ADRP
|
||||
// pairs and BL calls.
|
||||
//
|
||||
// The toolchain records one relocation per ADRP pair: a single R_ADDRARM64
|
||||
// or R_ARM64_PCREL_LDST64 of Siz 8 at the ADRP word, from which the linker
|
||||
// patches both instructions of the pair (cmd/internal/obj/arm64/asm7.go,
|
||||
// the ADRP cases: one AddRel with Off at the pair's pc and Siz 8). gasm's
|
||||
// assembler records the ADRP+ADD form as two word relocs, so the second
|
||||
// word's twin is dropped here before emission.
|
||||
func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreambleAARCH64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||
if r.Kind == RelArm64Branch {
|
||||
coalesced := *img
|
||||
coalesced.Funcs = append([]FuncLayout(nil), img.Funcs...)
|
||||
for i := range coalesced.Funcs {
|
||||
rs := coalesced.Funcs[i].Relocs
|
||||
var keep []Reloc
|
||||
for j := 0; j < len(rs); j++ {
|
||||
keep = append(keep, rs[j])
|
||||
if rs[j].Kind == RelArm64Addr && j+1 < len(rs) &&
|
||||
rs[j+1].Kind == RelArm64Addr && rs[j+1].Off == rs[j].Off+4 {
|
||||
j++ // the ADD word's twin: the Siz-8 pair reloc covers it
|
||||
}
|
||||
}
|
||||
coalesced.Funcs[i].Relocs = keep
|
||||
}
|
||||
return coalesced.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||
switch r.Kind {
|
||||
case RelArm64Branch:
|
||||
return relocArm64Branch, 4
|
||||
case RelArm64LDST64:
|
||||
return relocArm64LDST64, 8
|
||||
case RelArm64TLSLE:
|
||||
return relocArm64TLSLE, 4
|
||||
default:
|
||||
return relocArm64Addr, 8
|
||||
}
|
||||
return relocArm64Addr, 4
|
||||
})
|
||||
}
|
||||
|
||||
// arm64 relocation types (cmd/internal/objabi).
|
||||
const (
|
||||
relocArm64Addr = 3 // R_ADDRARM64 — ADRP+ADD/LDR/STR pair
|
||||
relocArm64Branch = 9 // R_CALLARM64 — BL instruction
|
||||
relocArm64Addr = 3 // R_ADDRARM64, ADRP+ADD pair
|
||||
relocArm64Branch = 9 // R_CALLARM64, BL instruction
|
||||
relocArm64TLSLE = 32 // R_ARM64_TLS_LE, MOVZ local-exec TLS load
|
||||
relocArm64LDST64 = 40 // R_ARM64_PCREL_LDST64, ADRP+LDR/STR pair
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// the pcalau12i+addi.d address pairs.
|
||||
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) {
|
||||
// A pcalau12i+addi.d pair: the high part carries
|
||||
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO.
|
||||
if r.Kind == RelLoong64AddrLo {
|
||||
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO; the guard's
|
||||
// morestack call carries R_CALLLOONG64.
|
||||
switch {
|
||||
case r.Kind == RelLoong64AddrLo:
|
||||
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 (
|
||||
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
|
||||
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
|
||||
relocCallLoong64 = 84 // R_CALLLOONG64
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
|
||||
|
||||
+7
-3
@@ -13,9 +13,9 @@ import (
|
||||
)
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
|
||||
// relocation, not the ELF HI20/LO12 pair).
|
||||
@@ -30,6 +30,8 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||
return relocRISCVPcrelStype, 8
|
||||
case RelRISCVJal:
|
||||
return relocRISCVJal, 4
|
||||
case RelRISCVTLSLE:
|
||||
return relocRISCVTLSLE, 8
|
||||
default:
|
||||
return relocRISCVPcrelItype, 8
|
||||
}
|
||||
@@ -38,11 +40,13 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||
|
||||
// RISC-V relocation types (cmd/internal/objabi). The Go linker applies
|
||||
// R_RISCV_PCREL_ITYPE/STYPE to an AUIPC + I/S-type instruction pair as a
|
||||
// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction.
|
||||
// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction;
|
||||
// R_RISCV_TLS_LE covers the LUI + I-type pair of a local-exec TLS reference.
|
||||
const (
|
||||
relocRISCVJal = 59 // R_RISCV_JAL
|
||||
relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE
|
||||
relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE
|
||||
relocRISCVTLSLE = 65 // R_RISCV_TLS_LE
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleRISCV returns the "go object ...\n!\n" header
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/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"},
|
||||
// Class 2 with a body long enough that the underflow JB relaxes to
|
||||
// rel32: its displacement must span the real 6-byte JB, else the
|
||||
// branch lands 4 bytes past the morestack block, inside the CALL
|
||||
// displacement field.
|
||||
{"leafbiglong", "TEXT \u00b7leafbiglong(SB), $8192-0\n" + strings.Repeat("\tMOVQ AX, BX\n", 40) + "\tRET\n",
|
||||
"644c8b3425000000004989e44981ec881f00000f82960000004d3b66100f868c000000554889e54881ec00200000" + strings.Repeat("4889c3", 40) + "4881c4002000005dc3e800000000e947ffffff"},
|
||||
{"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStackGuardBranchTargetsARM64 checks the class-2 guard's branch
|
||||
// positions for a frame whose guard constant needs two MOV words: the
|
||||
// displacements must be computed from byte offsets (8+4*ml and 16+4*ml), so
|
||||
// both branches land on the morestack block rather than inside the body.
|
||||
// The frame size makes the toolchain switch its own prologue decomposition,
|
||||
// so the assertion is on the branch targets, not pinned bytes.
|
||||
func TestStackGuardBranchTargetsARM64(t *testing.T) {
|
||||
f, errs := parser.Parse("g_arm64.s", "TEXT \u00b7f(SB), $65664-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 := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
if len(code)%4 != 0 {
|
||||
t.Fatalf("function size %d is not a word multiple", len(code))
|
||||
}
|
||||
// autosize = 65680, so the guard materialises 65552 = MOVZ+MOVK: ml = 2
|
||||
// and the branches sit at bytes 16 and 24 of the guard prefix.
|
||||
const morestackBlock = 12 // MOVD R30, R3; BL; B back
|
||||
blockStart := len(code) - morestackBlock
|
||||
check := func(name string, off int) {
|
||||
t.Helper()
|
||||
w := leWord(code[off:])
|
||||
imm19 := int32(w>>5) & 0x7FFFF
|
||||
if imm19&(1<<18) != 0 {
|
||||
imm19 -= 1 << 19
|
||||
}
|
||||
if target := off + int(imm19)*4; target != blockStart {
|
||||
t.Errorf("%s at byte %d targets byte %d, want the morestack block at %d", name, off, target, blockStart)
|
||||
}
|
||||
}
|
||||
check("B.LO", 16)
|
||||
check("B.LS", 24)
|
||||
}
|
||||
|
||||
// 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, ...AssembleOption) (*Image, error)
|
||||
}{
|
||||
{"g_amd64.s", AssembleFile},
|
||||
{"g_arm64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileARM64(f) }},
|
||||
{"g_riscv64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileRISCV(f) }},
|
||||
{"g_loong64.s", func(f *ast.File, _ ...AssembleOption) (*Image, error) { return AssembleFileLOONG64(f) }},
|
||||
} {
|
||||
f, errs := parser.Parse(tt.src, "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1217
-98
File diff suppressed because it is too large.
Load diff
@@ -16,16 +16,16 @@ import (
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
||||
// all functions plus the file-local mask24 constant — and checks that every
|
||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel;
|
||||
// all functions plus the file-local mask24 constant; and checks that every
|
||||
// static-symbol load resolves to the right bytes in the image.
|
||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -81,12 +81,12 @@ func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||
t.Skip("go-libraries repository not present next to gasm-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// The differential kernels for the DATA-path and front-end gaps are kept in
|
||||
// testdata/verify beside the campaign's other kernels; the verify package's
|
||||
// suites are not open to the asm package, so this test is their runner: each
|
||||
// kernel assembles through gasm and through go tool asm, and the functions'
|
||||
// bytes must agree with the relocation sites masked on both sides.
|
||||
|
||||
// toolAsmObject assembles path with the installed toolchain's assembler for
|
||||
// goarch ("" = the host) and returns the object bytes. Every live-oracle
|
||||
// comparison funnels through here, so this is also where the deliberate-run
|
||||
// boundary sits: under -short (the push pipeline's mode) the comparisons
|
||||
// skip, because each spawns a go tool asm subprocess and the small single-
|
||||
// core runner pays seconds per spawn. The encodings stay pinned by the
|
||||
// golden-byte tests in every mode; the live oracle runs in the local test
|
||||
// gate and the dispatched workflows.
|
||||
func toolAsmObject(t *testing.T, path, goarch string) []byte {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("live go tool asm oracle: skipped in -short mode")
|
||||
}
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
out, err := exec.Command(goBin, "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go env GOROOT: %v", err)
|
||||
}
|
||||
includeDir := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
||||
|
||||
pkg := strings.TrimSuffix(filepath.Base(path), ".s")
|
||||
pkg = strings.TrimSuffix(pkg, "_amd64")
|
||||
pkg = strings.TrimSuffix(pkg, "_arm64")
|
||||
|
||||
objPath := filepath.Join(t.TempDir(), "oracle.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||
if goarch != "" {
|
||||
environ := os.Environ()
|
||||
env := make([]string, 0, len(environ)+1)
|
||||
for _, e := range environ {
|
||||
if !strings.HasPrefix(e, "GOARCH=") {
|
||||
env = append(env, e)
|
||||
}
|
||||
}
|
||||
cmd.Env = append(env, "GOARCH="+goarch)
|
||||
}
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go tool asm %s: %v\n%s", filepath.Base(path), err, out)
|
||||
}
|
||||
obj, err := os.ReadFile(objPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// oracleFuncCode extracts the non-package TEXT functions' code bytes from a
|
||||
// toolchain object, keyed by the name the object records (pkg.name). Each
|
||||
// function's span is its own symbol size: a toolchain object that follows
|
||||
// the text with data symbols (the synthesised float-constant pool) would
|
||||
// otherwise fold them into the last function's bytes.
|
||||
func oracleFuncCode(t *testing.T, obj []byte) map[string][]byte {
|
||||
t.Helper()
|
||||
v := openGoobj(t, obj)
|
||||
le := binary.LittleEndian
|
||||
const symSize = 21
|
||||
nps := v.syms(blkNonpkgdef)
|
||||
data := v.blk(blkData)
|
||||
didx := v.blk(blkDataIdx)
|
||||
preceding := 0
|
||||
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
|
||||
preceding += len(v.blk(bi)) / symSize
|
||||
}
|
||||
out := make(map[string][]byte, len(nps))
|
||||
for i, s := range nps {
|
||||
if s.typ != kindSTEXT {
|
||||
continue
|
||||
}
|
||||
start := le.Uint32(didx[4*(preceding+i):])
|
||||
out[s.name] = data[start : start+s.size]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// maskCode zeroes every relocation field, the way the toolchain's object
|
||||
// leaves them for the linker.
|
||||
func maskCode(code []byte, relocs []Reloc) []byte {
|
||||
for _, r := range relocs {
|
||||
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||
code[j] = 0
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// code assembles src for amd64 and returns the image's code bytes.
|
||||
func code(path, src string) []byte {
|
||||
f, errs := parser.Parse(path, src)
|
||||
if len(errs) > 0 {
|
||||
return nil
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return img.Code
|
||||
}
|
||||
|
||||
// TestDifferentialKernels pins the new kernels against the oracle.
|
||||
func TestDifferentialKernels(t *testing.T) {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
t.Skip("the amd64 kernels assume an amd64 host assembler default")
|
||||
}
|
||||
for _, k := range []struct {
|
||||
path string
|
||||
goarch string
|
||||
arm64 bool
|
||||
}{
|
||||
{filepath.Join("..", "testdata", "verify", "datarel_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "divslash_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "semicolons_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "quadreg_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "floatimm_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "bookkeep_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "forms_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "datarel_arm64.s"), "arm64", true},
|
||||
{filepath.Join("..", "testdata", "verify", "divslash_arm64.s"), "arm64", true},
|
||||
} {
|
||||
t.Run(filepath.Base(k.path), func(t *testing.T) {
|
||||
src, err := os.ReadFile(k.path)
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
f, errs := parser.Parse(k.path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
var img *Image
|
||||
if k.arm64 {
|
||||
img, err = AssembleFileARM64(f)
|
||||
} else {
|
||||
img, err = AssembleFile(f)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
gt := oracleFuncCode(t, toolAsmObject(t, k.path, k.goarch))
|
||||
// The oracle keys its functions by the qualified object name
|
||||
// (pkg.name); match on the local part.
|
||||
byLocal := make(map[string][]byte, len(gt))
|
||||
for name, code := range gt {
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
name = after
|
||||
}
|
||||
byLocal[name] = code
|
||||
}
|
||||
|
||||
matched := 0
|
||||
for _, fn := range img.Funcs {
|
||||
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
goCode, ok := byLocal[fn.Name]
|
||||
if !ok {
|
||||
t.Errorf("%s: not in ground truth (%d functions: %v)", fn.Name, len(gt), keysOf(byLocal))
|
||||
continue
|
||||
}
|
||||
goCode = maskCode(append([]byte(nil), goCode...), fn.Relocs)
|
||||
cmpLen := min(len(goCode), len(gasmCode))
|
||||
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
||||
t.Errorf("%s: MISMATCH gasm=%d go=%d bytes\ngasm %x\ngo %x", fn.Name, len(gasmCode), len(goCode), gasmCode, goCode)
|
||||
continue
|
||||
}
|
||||
for _, b := range goCode[len(gasmCode):] {
|
||||
if b != 0 {
|
||||
t.Errorf("%s: non-zero trailing bytes in go tool asm output", fn.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
matched++
|
||||
t.Logf("%s: MATCH (%d bytes)", fn.Name, len(gasmCode))
|
||||
}
|
||||
if matched == 0 {
|
||||
t.Fatal("no functions matched")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(m map[string][]byte) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestSemicolonSpellingParity pins that the ';' statement separator changes
|
||||
// nothing about the encoding: the one-line spelling assembles to exactly the
|
||||
// bytes of the same statements written one per line.
|
||||
func TestSemicolonSpellingParity(t *testing.T) {
|
||||
for _, tt := range []struct{ one, two string }{
|
||||
{"\tROLQ $3, DI; ROLQ $13, DI\n", "\tROLQ $3, DI\n\tROLQ $13, DI\n"},
|
||||
{"\tREP; MOVSQ\n", "\tREP\n\tMOVSQ\n"},
|
||||
{"\tXORQ AX, AX; XORQ CX, CX\n", "\tXORQ AX, AX\n\tXORQ CX, CX\n"},
|
||||
} {
|
||||
one := code("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n"+tt.one+"\tRET\n")
|
||||
two := code("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n"+tt.two+"\tRET\n")
|
||||
if !bytes.Equal(one, two) {
|
||||
t.Errorf("semicolon spelling %q: %x, want the two-line bytes %x", tt.one, one, two)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||||
@@ -94,9 +94,9 @@ DATA ·table<>+0(SB)/8, $0x1122334455667788
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
linesOff := le.Uint32(dataIdx[4*2:])
|
||||
lines := dataBlk[linesOff : linesOff+21]
|
||||
|
||||
+402
-74
@@ -5,9 +5,11 @@ package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Image is an assembled file: the function bodies laid out in source order,
|
||||
@@ -15,7 +17,7 @@ import (
|
||||
// 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
|
||||
// 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.
|
||||
type Image struct {
|
||||
Code []byte // concatenated function bodies
|
||||
@@ -24,6 +26,10 @@ type Image struct {
|
||||
Symbols map[string]int // static symbol → byte offset within the image
|
||||
DataSyms []DataSymbol // GLOBL symbols, in layout order
|
||||
Externals []string // referenced but undefined symbols, sorted
|
||||
// SourcePath is the assembled file's path, recorded in the DWARF
|
||||
// sections in place of a placeholder name. Empty when the image was
|
||||
// not built from a named file.
|
||||
SourcePath string
|
||||
}
|
||||
|
||||
// FuncLayout describes one assembled function within an Image.
|
||||
@@ -80,33 +86,47 @@ func (fl *FuncLayout) LineAt(offset int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// RelocKind Reloc is one static-symbol reference within a function body: the disp32
|
||||
// field at Off (function-relative) must reach the symbol plus Addend,
|
||||
// measured from After, the address just past the instruction. An External
|
||||
// relocation names a symbol no GLOBL in the file defines; the object-file
|
||||
// emitters carry it into the output's relocation table.
|
||||
// RelocKind discriminates the type of relocation needed.
|
||||
// RelocKind discriminates the relocation a static-symbol reference needs;
|
||||
// the encoders record one per SB reference, and the object-file emitters map
|
||||
// it to their format's relocation type.
|
||||
type RelocKind int
|
||||
|
||||
const (
|
||||
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)
|
||||
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
||||
RelRISCVJal // R_RISCV_JAL (J-type call)
|
||||
RelPCRelAbs // 32-bit absolute (R_RISCV_32)
|
||||
RelRISCVTLSLE // R_RISCV_TLS_LE (LUI + I-type local-exec pair)
|
||||
RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i)
|
||||
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)
|
||||
RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair)
|
||||
RelLoong64Branch // R_CALLLOONG64 (BL instruction)
|
||||
RelAddr // R_ADDR: the absolute address of a symbol held in a DATA field
|
||||
RelArm64TLSLE // R_ARM64_TLS_LE (MOVZ local-exec TLS load)
|
||||
)
|
||||
|
||||
type Reloc struct {
|
||||
// Off is the function-relative offset of the field the linker patches
|
||||
// and After the address just past the instruction, the base the
|
||||
// assembler measures PC-relative displacements from. Name plus
|
||||
// Addend select the target: the symbol plus the byte offset. An
|
||||
// External relocation names a symbol no GLOBL in the file defines;
|
||||
// the object-file emitters carry it into the output's relocation
|
||||
// table. Siz is the width of the patched field and is set only for
|
||||
// data-field relocations (RelAddr, Off relative to the data symbol),
|
||||
// whose width is the DATA line's; code relocations take their width
|
||||
// from the architecture's instruction encoding.
|
||||
Off int
|
||||
After int
|
||||
Name string
|
||||
Addend int64
|
||||
External bool
|
||||
Kind RelocKind
|
||||
Siz uint8
|
||||
}
|
||||
|
||||
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
||||
@@ -116,8 +136,14 @@ type DataSymbol struct {
|
||||
Offset int // byte offset within Data
|
||||
Size int
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Rodata bool // the RODATA flag: read-only data
|
||||
Rodata bool // the RODATA flag: read-only data (implies no pointers)
|
||||
Noptr bool // the NOPTR flag: data with no pointers, kept out of GC scanning
|
||||
Dupok bool // the DUPOK flag: duplicate-OK
|
||||
// Relocs carries the symbol-valued DATA initialisers ("DATA s+0(SB)/8,
|
||||
// $other(SB)"): fields of this symbol's data that hold another symbol's
|
||||
// address, resolved by the linker. Off is relative to the symbol's
|
||||
// data start.
|
||||
Relocs []Reloc
|
||||
}
|
||||
|
||||
// Bytes returns the whole image: code, then data.
|
||||
@@ -127,14 +153,26 @@ func (img *Image) Bytes() []byte {
|
||||
return append(out, img.Data...)
|
||||
}
|
||||
|
||||
// AssembleOption adjusts the file-level assembly context.
|
||||
type AssembleOption func(*linkInfo)
|
||||
|
||||
// WithGOOS selects the target operating system for the forms that depend on
|
||||
// it, the TLS access shape above all: linux and freebsd take the
|
||||
// one-instruction form, windows and plan9 keep the two-instruction load.
|
||||
func WithGOOS(goos string) AssembleOption {
|
||||
return func(l *linkInfo) {
|
||||
l.goos = goos
|
||||
}
|
||||
}
|
||||
|
||||
// AssembleFile assembles every TEXT function of a parsed file and lays out
|
||||
// its static symbols (GLOBL/DATA) in a data section behind the code. Each
|
||||
// reference to a file-local static symbol becomes a RIP-relative load whose
|
||||
// displacement is resolved against that layout; a reference to a symbol no
|
||||
// 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.
|
||||
func AssembleFile(f *ast.File) (*Image, error) {
|
||||
func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) {
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -143,9 +181,21 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
for _, d := range dataSyms {
|
||||
known[d.name] = true
|
||||
}
|
||||
// TEXT symbols are file-level definitions too: a symbol immediate
|
||||
// ($fn(SB)) may name one, exactly as a data reference names a GLOBL.
|
||||
for _, d := range f.Decls {
|
||||
if t, ok := d.(*ast.Text); ok {
|
||||
known[t.Name.Name] = true
|
||||
}
|
||||
}
|
||||
link := &linkInfo{symbols: known, allowExternal: true}
|
||||
for _, o := range opts {
|
||||
o(link)
|
||||
}
|
||||
poolSeen := map[string]bool{}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
||||
textOff := map[string]int{}
|
||||
type asmFunc struct {
|
||||
name string
|
||||
patches []sbPatch
|
||||
@@ -156,7 +206,26 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, patches, labels, steps, lines, err := assemble(t, link)
|
||||
code, patches, labels, steps, lines, pool, err := assemble(t, link)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
// The pooled floating-point constants join the declared data as
|
||||
// read-only symbols, deduplicated across the file (the toolchain
|
||||
// synthesises the same symbols into its rodata).
|
||||
for _, entry := range pool {
|
||||
if poolSeen[entry.name] {
|
||||
continue
|
||||
}
|
||||
poolSeen[entry.name] = true
|
||||
dataSyms = append(dataSyms, dataSym{
|
||||
name: entry.name,
|
||||
buf: entry.data,
|
||||
size: len(entry.data),
|
||||
rodata: true,
|
||||
dupok: true,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
@@ -183,6 +252,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
for _, s := range steps {
|
||||
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.Code = append(img.Code, code...)
|
||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||
@@ -202,6 +272,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
Size: len(d.buf),
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
@@ -215,13 +286,27 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
base := img.Funcs[i].Offset
|
||||
code := img.Code[base : base+img.Funcs[i].Size]
|
||||
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 {
|
||||
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 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 {
|
||||
reloc.External = true
|
||||
externals[p.name] = true
|
||||
@@ -229,6 +314,22 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
// The data symbols' symbol-valued DATA fields resolve the same way the
|
||||
// code references do: a name the file defines (GLOBL or TEXT) stays an
|
||||
// internal reference the emitters resolve, anything else is external.
|
||||
// img.DataSyms was laid out in dataSyms order, so the indexes line up.
|
||||
for i := range img.DataSyms {
|
||||
for _, r := range dataSyms[i].relocs {
|
||||
reloc := r
|
||||
if _, ok := img.Symbols[reloc.Name]; !ok {
|
||||
if _, ok := textOff[reloc.Name]; !ok {
|
||||
reloc.External = true
|
||||
externals[reloc.Name] = true
|
||||
}
|
||||
}
|
||||
img.DataSyms[i].Relocs = append(img.DataSyms[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
for name := range externals {
|
||||
img.Externals = append(img.Externals, name)
|
||||
}
|
||||
@@ -236,6 +337,35 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// riscvTLSSymbols names the symbols the file declares with the TLSBSS flag
|
||||
// (or its legacy numeric constant 256 from textflag.h): the assembler gives
|
||||
// their SB references the local-exec TLS sequence.
|
||||
func riscvTLSSymbols(f *ast.File) map[string]bool {
|
||||
var tls map[string]bool
|
||||
for _, d := range f.Decls {
|
||||
gd, ok := d.(*ast.Globl)
|
||||
if !ok || gd.Name == nil || gd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
for _, fl := range gd.Flags {
|
||||
isTLS := fl == "TLSBSS"
|
||||
if !isTLS {
|
||||
if n, err := strconv.Atoi(fl); err == nil && n&256 != 0 {
|
||||
isTLS = true
|
||||
}
|
||||
}
|
||||
if isTLS {
|
||||
if tls == nil {
|
||||
tls = map[string]bool{}
|
||||
}
|
||||
tls[gd.Name.Name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return tls
|
||||
}
|
||||
|
||||
// AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file
|
||||
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
||||
// code. SB references in the code are encoded as AUIPC pairs with zero
|
||||
@@ -245,17 +375,38 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The symbols the file declares TLSBSS resolve through the local-exec
|
||||
// sequence (LUI + ADDIW + ADD of TP), exactly as the toolchain routes
|
||||
// every SB reference whose symbol carries the STLSBSS type.
|
||||
tlsSyms := riscvTLSSymbols(f)
|
||||
// The pooled $i64 constants the wide MOV immediate loads refer to join
|
||||
// the declared data as read-only symbols, deduplicated across the file
|
||||
// (the toolchain synthesises the same symbols into its rodata).
|
||||
litSeen := map[string]bool{}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, labels, relocs, lines, spadj, err := assembleRISCV(t)
|
||||
code, labels, relocs, lines, spadj, lits, err := assembleRISCV(t, tlsSyms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
for _, lit := range lits {
|
||||
if litSeen[lit.Name] {
|
||||
continue
|
||||
}
|
||||
litSeen[lit.Name] = true
|
||||
dataSyms = append(dataSyms, dataSym{
|
||||
name: lit.Name,
|
||||
buf: lit.Data,
|
||||
size: len(lit.Data),
|
||||
rodata: true,
|
||||
dupok: true,
|
||||
})
|
||||
}
|
||||
fl := FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Pkg: t.Name.Pkg,
|
||||
@@ -299,6 +450,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
@@ -318,7 +470,7 @@ func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
@@ -371,6 +523,7 @@ func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
@@ -401,6 +554,23 @@ func markExternals(img *Image, dataSyms []dataSym) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The declared data symbols carry the file's own relocations (the
|
||||
// symbol-valued DATA fields); the layouts appended img.DataSyms in
|
||||
// dataSyms order, so the indexes line up. The trailing entries (the
|
||||
// pooled arm64 literals) have no source relocations.
|
||||
for i := range img.DataSyms {
|
||||
if i >= len(dataSyms) {
|
||||
break
|
||||
}
|
||||
for _, r := range dataSyms[i].relocs {
|
||||
reloc := r
|
||||
if !known[reloc.Name] {
|
||||
reloc.External = true
|
||||
externals[reloc.Name] = true
|
||||
}
|
||||
img.DataSyms[i].Relocs = append(img.DataSyms[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
for name := range externals {
|
||||
img.Externals = append(img.Externals, name)
|
||||
}
|
||||
@@ -415,82 +585,240 @@ type dataSym struct {
|
||||
size int
|
||||
static bool
|
||||
rodata bool
|
||||
noptr bool
|
||||
dupok bool
|
||||
// relocs are the symbol-valued DATA fields, in declaration order; Off
|
||||
// is relative to the symbol's data start.
|
||||
relocs []Reloc
|
||||
}
|
||||
|
||||
// checkDuplicateDecls rejects a symbol the file declares twice, the
|
||||
// toolchain's OnList rule (cmd/internal/obj, InitTextSym and GloblPos,
|
||||
// measured with go tool asm): the second declaration of a symbol is
|
||||
// diagnosed as a redeclaration whether the pair is two TEXTs, two GLOBLs
|
||||
// or one of each, and a second TEXT carries the other declaration's line
|
||||
// the way the toolchain's note does. The DUPOK flag plays no part at
|
||||
// assembly time: it legalises duplicate definitions across files
|
||||
// (AttrDuplicateOK, which the linker resolves), never two declarations
|
||||
// inside one file, so a DUPOK pair here is rejected exactly like a plain
|
||||
// one. Symbol identity follows the object model: the package prefix and
|
||||
// the <> static marker are part of the name (foo(SB), foo<>(SB) and
|
||||
// other·foo(SB) are three symbols); the ABI selector is not, because
|
||||
// outside the runtime package, where alone it is legal, foo<ABIInternal>(SB)
|
||||
// resolves to foo(SB) and is a redeclaration.
|
||||
func checkDuplicateDecls(f *ast.File) error {
|
||||
type decl struct {
|
||||
text bool
|
||||
line int
|
||||
}
|
||||
seen := make(map[string]decl)
|
||||
symKey := func(s *ast.Symbol) string {
|
||||
key := s.Pkg + "\x00" + s.Name
|
||||
if s.Static {
|
||||
key += "\x00<>"
|
||||
}
|
||||
return key
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
switch t := d.(type) {
|
||||
case *ast.Text:
|
||||
if first, dup := seen[symKey(t.Name)]; dup {
|
||||
if first.text {
|
||||
return fmt.Errorf("symbol %q redeclared (other declaration on line %d)", t.Name.Name, first.line)
|
||||
}
|
||||
return fmt.Errorf("symbol %q redeclared", t.Name.Name)
|
||||
}
|
||||
seen[symKey(t.Name)] = decl{text: true, line: t.Pos().Line}
|
||||
case *ast.Globl:
|
||||
if t.Name == nil || t.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[symKey(t.Name)]; dup {
|
||||
return fmt.Errorf("symbol %q redeclared", t.Name.Name)
|
||||
}
|
||||
seen[symKey(t.Name)] = decl{line: t.Pos().Line}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The data section's size ceilings. The image materialises every GLOBL's
|
||||
// bytes at assembly time, where the toolchain defers the cost to its linker,
|
||||
// so gasm needs its own bound and a diagnostic in place of an allocation
|
||||
// failure. The toolchain's own limit (obj's "symbol too large", 2,000,000,000
|
||||
// bytes) would let a twenty-five byte input demand four gigabytes of resident
|
||||
// memory (measured: `GLOBL d(SB), $2000000000` peaks at 3.92 GiB RSS), which
|
||||
// starves the memory fence the test recipes run under when the fuzz workers
|
||||
// share it. 64 MiB per symbol and 128 MiB per file sit two orders above any
|
||||
// real assembly symbol (the runtime's largest GLOBL is KiB-scale) and keep a
|
||||
// worker's worst-case peak near its fence share.
|
||||
const (
|
||||
maxSymbolSize = 64 << 20
|
||||
maxDataSize = 128 << 20
|
||||
)
|
||||
|
||||
// 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. Every per-architecture entry point
|
||||
// collects data before assembling text, so the duplicate-declaration check
|
||||
// rides here and covers them all.
|
||||
func collectData(f *ast.File) ([]dataSym, error) {
|
||||
if err := checkDuplicateDecls(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index := map[string]int{}
|
||||
var syms []dataSym
|
||||
total := 0
|
||||
for _, d := range f.Decls {
|
||||
switch dd := d.(type) {
|
||||
case *ast.Globl:
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
gd, ok := d.(*ast.Globl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if gd.Name == nil || gd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
name := gd.Name.Name
|
||||
size := 0
|
||||
if gd.Size != nil && gd.Size.Imm.HasVal {
|
||||
size = int(gd.Size.Imm.Val)
|
||||
if gd.Size.Imm.Neg {
|
||||
size = -size
|
||||
}
|
||||
name := dd.Name.Name
|
||||
if _, dup := index[name]; dup {
|
||||
return nil, fmt.Errorf("duplicate GLOBL %q", name)
|
||||
if size < 0 || size > maxSymbolSize {
|
||||
return nil, fmt.Errorf("GLOBL %q: symbol too large (%d bytes > %d bytes)", name, size, maxSymbolSize)
|
||||
}
|
||||
size := 0
|
||||
if dd.Size != nil && dd.Size.Imm.HasVal {
|
||||
size = int(dd.Size.Imm.Val)
|
||||
if total+size > maxDataSize {
|
||||
return nil, fmt.Errorf("GLOBL %q: data section too large (%d bytes > %d bytes)", name, total+size, maxDataSize)
|
||||
}
|
||||
index[name] = len(syms)
|
||||
ds := dataSym{
|
||||
name: name,
|
||||
pkg: dd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
size: size,
|
||||
static: dd.Name.Static,
|
||||
}
|
||||
for _, f := range dd.Flags {
|
||||
switch f {
|
||||
case "RODATA":
|
||||
ds.rodata = true
|
||||
case "DUPOK":
|
||||
ds.dupok = true
|
||||
case "1":
|
||||
ds.dupok = true
|
||||
case "8":
|
||||
ds.rodata = true
|
||||
case "9":
|
||||
ds.dupok = true
|
||||
ds.rodata = true
|
||||
total += size
|
||||
}
|
||||
index[name] = len(syms)
|
||||
ds := dataSym{
|
||||
name: name,
|
||||
pkg: gd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
size: size,
|
||||
static: gd.Name.Static,
|
||||
}
|
||||
for _, f := range gd.Flags {
|
||||
switch f {
|
||||
case "RODATA":
|
||||
ds.rodata = true
|
||||
case "NOPTR":
|
||||
ds.noptr = true
|
||||
case "DUPOK":
|
||||
ds.dupok = true
|
||||
default:
|
||||
// Legacy numeric flag constants (runtime/textflag.h):
|
||||
// DUPOK is 2, RODATA is 8, NOPTR is 16; combinations arrive
|
||||
// as one number (e.g. 10 = RODATA|DUPOK).
|
||||
if n, err := strconv.Atoi(f); err == nil {
|
||||
if n&2 != 0 {
|
||||
ds.dupok = true
|
||||
}
|
||||
if n&8 != 0 {
|
||||
ds.rodata = true
|
||||
}
|
||||
if n&16 != 0 {
|
||||
ds.noptr = true
|
||||
}
|
||||
}
|
||||
}
|
||||
syms = append(syms, ds)
|
||||
|
||||
case *ast.Data:
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
syms = append(syms, ds)
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
dd, ok := d.(*ast.Data)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if dd.Name == nil || dd.Name.Pseudo != "SB" {
|
||||
continue
|
||||
}
|
||||
i, ok := index[dd.Name.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
||||
}
|
||||
if dd.Value == nil {
|
||||
return nil, fmt.Errorf("DATA %q: missing value", dd.Name.Name)
|
||||
}
|
||||
w := dd.Width
|
||||
off := dd.Name.Offset
|
||||
buf := syms[i].buf
|
||||
if off < 0 || off+int64(w) > int64(len(buf)) {
|
||||
return nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf))
|
||||
}
|
||||
// A symbol value ("DATA s+0(SB)/8, $other(SB)", the rt0 spelling)
|
||||
// leaves the field zero and records a relocation against the named
|
||||
// symbol: the linker patches the absolute address at this data
|
||||
// offset. The toolchain emits the same shape, an R_ADDR of the
|
||||
// DATA width with the value's offset as the addend, on every
|
||||
// architecture.
|
||||
if sym := dd.Value.Imm.Sym; !dd.Value.Imm.HasVal && sym != nil {
|
||||
syms[i].relocs = append(syms[i].relocs, Reloc{
|
||||
Off: int(off),
|
||||
Name: sym.Name,
|
||||
Addend: sym.Offset,
|
||||
Kind: RelAddr,
|
||||
Siz: uint8(w),
|
||||
})
|
||||
continue
|
||||
}
|
||||
// A string or rune value ("DATA s+0(SB)/20, $"text"") writes its
|
||||
// bytes into the field and leaves the rest zero, the toolchain's
|
||||
// WriteString: the declared width must hold every byte, and any
|
||||
// width is legal.
|
||||
if s := dd.Value.Imm.Str; s != "" && !dd.Value.Imm.HasVal {
|
||||
text, err := strconv.Unquote(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DATA %q: invalid string value %s", dd.Name.Name, s)
|
||||
}
|
||||
i, ok := index[dd.Name.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
||||
if len(text) > w {
|
||||
return nil, fmt.Errorf("DATA %q: string of %d bytes does not fit width %d", dd.Name.Name, len(text), w)
|
||||
}
|
||||
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
||||
return nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
||||
copy(buf[off:], text)
|
||||
continue
|
||||
}
|
||||
// A floating-point value stores its IEEE-754 bits: /4 the float32
|
||||
// rounding of the parsed double, /8 the full 64 bits, the
|
||||
// toolchain's WriteFloat32 and WriteFloat64.
|
||||
if f := dd.Value.Imm.Float; f != "" && !dd.Value.Imm.HasVal {
|
||||
num, err := strconv.ParseFloat(f, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DATA %q: invalid floating-point value %q", dd.Name.Name, f)
|
||||
}
|
||||
w := dd.Width
|
||||
switch w {
|
||||
case 1, 2, 4, 8:
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||
}
|
||||
off := dd.Name.Offset
|
||||
buf := syms[i].buf
|
||||
if off < 0 || off+int64(w) > int64(len(buf)) {
|
||||
return nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf))
|
||||
}
|
||||
v := dd.Value.Imm.Val
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
num = -num
|
||||
}
|
||||
var v uint64
|
||||
switch w {
|
||||
case 4:
|
||||
v = uint64(math.Float32bits(float32(num)))
|
||||
case 8:
|
||||
v = math.Float64bits(num)
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: invalid width %d for a float (want 4 or 8)", dd.Name.Name, w)
|
||||
}
|
||||
for j := range w {
|
||||
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !dd.Value.Imm.HasVal {
|
||||
return nil, fmt.Errorf("DATA %q: value must be an integer immediate or a symbol address", dd.Name.Name)
|
||||
}
|
||||
switch w {
|
||||
case 1, 2, 4, 8:
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||
}
|
||||
v := dd.Value.Imm.Val
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
for j := range w {
|
||||
buf[off+int64(j)] = byte(v >> (8 * j))
|
||||
}
|
||||
}
|
||||
return syms, nil
|
||||
|
||||
+449
-14
@@ -4,14 +4,117 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestAssembleFileStaticData checks the whole-image layout — code, padding
|
||||
// and the data section — and that the RIP-relative displacements of static
|
||||
// TestDuplicateDecls pins the duplicate-declaration rule against the
|
||||
// toolchain's, measured with go tool asm (Go 1.27.1): a symbol declared
|
||||
// twice in one file is rejected at the second declaration whether the pair
|
||||
// is two TEXTs, two GLOBLs or one of each, and DUPOK never legalises it
|
||||
// within a file (the toolchain's duperror.s testdata asserts the plain
|
||||
// pair). DUPOK legalises duplicates across files, so each file of a pair
|
||||
// assembles cleanly on its own, and symbol identity keeps the package
|
||||
// prefix and the <> static marker: foo(SB), foo<>(SB) and other·foo(SB)
|
||||
// are three symbols, not one. Every case runs through all four
|
||||
// per-architecture entry points.
|
||||
func TestDuplicateDecls(t *testing.T) {
|
||||
targets := []struct {
|
||||
goarch string
|
||||
file string
|
||||
asm func(*ast.File) (*Image, error)
|
||||
}{
|
||||
{"amd64", "dup_amd64.s", func(f *ast.File) (*Image, error) { return AssembleFile(f) }},
|
||||
{"arm64", "dup_arm64.s", AssembleFileARM64},
|
||||
{"riscv64", "dup_riscv64.s", AssembleFileRISCV},
|
||||
{"loong64", "dup_loong64.s", AssembleFileLOONG64},
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string // substring of the error, "" for no error
|
||||
}{
|
||||
{
|
||||
"plain TEXT duplicate rejected",
|
||||
"TEXT foo(SB), NOSPLIT, $0\n\tRET\nTEXT foo(SB), NOSPLIT, $0\n\tRET\n",
|
||||
`symbol "foo" redeclared (other declaration on line 1)`,
|
||||
},
|
||||
{
|
||||
"DUPOK pair in one file rejected",
|
||||
"TEXT foo(SB), DUPOK, $0\n\tRET\nTEXT foo(SB), DUPOK, $0\n\tRET\n",
|
||||
`symbol "foo" redeclared (other declaration on line 1)`,
|
||||
},
|
||||
{
|
||||
"TEXT then GLOBL rejected",
|
||||
"TEXT foo(SB), NOSPLIT, $0\n\tRET\nGLOBL foo(SB), NOPTR, $8\n",
|
||||
`symbol "foo" redeclared`,
|
||||
},
|
||||
{
|
||||
"GLOBL then TEXT rejected",
|
||||
"GLOBL foo(SB), NOPTR, $8\nTEXT foo(SB), NOSPLIT, $0\n\tRET\n",
|
||||
`symbol "foo" redeclared`,
|
||||
},
|
||||
{
|
||||
"plain GLOBL duplicate rejected",
|
||||
"GLOBL bar(SB), NOPTR, $8\nGLOBL bar(SB), NOPTR, $8\n",
|
||||
`symbol "bar" redeclared`,
|
||||
},
|
||||
{
|
||||
"static and package markers separate symbols",
|
||||
"TEXT foo(SB), NOSPLIT, $0\n\tRET\nTEXT foo<>(SB), NOSPLIT, $0\n\tRET\nTEXT other\u00b7foo(SB), NOSPLIT, $0\n\tRET\n",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"single DUPOK TEXT assembles",
|
||||
"TEXT foo(SB), DUPOK, $0\n\tRET\n",
|
||||
"",
|
||||
},
|
||||
}
|
||||
for _, tg := range targets {
|
||||
for _, c := range cases {
|
||||
f, errs := parser.Parse(tg.file, c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("%s/%s: parse: %v", tg.goarch, c.name, errs)
|
||||
}
|
||||
_, err := tg.asm(f)
|
||||
if c.want == "" {
|
||||
if err != nil {
|
||||
t.Errorf("%s/%s: error %v, want none", tg.goarch, c.name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s/%s: error %v, want substring %q", tg.goarch, c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A DUPOK pair across files is the case DUPOK exists for: the linker
|
||||
// resolves it, the assembler never sees both sides, so each file of the
|
||||
// pair assembles cleanly on its own.
|
||||
for _, tg := range targets {
|
||||
for _, name := range []string{"first", "second"} {
|
||||
f, errs := parser.Parse(tg.file, "TEXT foo(SB), DUPOK, $0\n\tRET\n")
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("%s/dupok cross-file %s: parse: %v", tg.goarch, name, errs)
|
||||
}
|
||||
if _, err := tg.asm(f); err != nil {
|
||||
t.Errorf("%s/dupok cross-file %s: error %v, want none", tg.goarch, name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFileStaticData checks the whole-image layout; code, padding
|
||||
// and the data section; and that the RIP-relative displacements of static
|
||||
// symbol loads resolve to the right bytes.
|
||||
func TestAssembleFileStaticData(t *testing.T) {
|
||||
f, errs := parser.Parse("d_amd64.s", `
|
||||
@@ -55,23 +158,15 @@ DATA small<>+0(SB)/4, $0x1234
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleFileErrors checks the static-symbol error paths.
|
||||
// TestAssembleFileErrors checks the static-symbol error paths. A reference
|
||||
// to a static symbol no GLOBL defines defers to the linker exactly as the
|
||||
// toolchain does (an external relocation), so it is not an error here.
|
||||
func TestAssembleFileErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string // substring of the error
|
||||
}{
|
||||
{
|
||||
"undefined symbol",
|
||||
`
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
VMOVDQU nope<>(SB), X0
|
||||
RET
|
||||
`,
|
||||
"undefined symbol",
|
||||
},
|
||||
{
|
||||
"DATA without GLOBL",
|
||||
`
|
||||
@@ -128,3 +223,343 @@ DATA x<>+0(SB)/4, $1
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectDataSymbolValue covers the symbol-valued DATA field ("DATA
|
||||
// s+0(SB)/8, $other(SB)", the rt0 spelling): the field stays zero in the
|
||||
// image and the relocation is recorded against the named symbol, whatever
|
||||
// the file defines (a TEXT function, a GLOBL) or leaves external.
|
||||
func TestCollectDataSymbolValue(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
MOVQ target+0(FP), AX
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $32
|
||||
DATA holder+0(SB)/8, $·Keep(SB)
|
||||
DATA holder+8(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+16(SB)/8, $holder(SB)
|
||||
GLOBL spare(SB), NOPTR, $8
|
||||
DATA spare+0(SB)/8, $extvar(SB)
|
||||
`
|
||||
f, errs := parser.Parse("f_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
byName := map[string]DataSymbol{}
|
||||
for _, d := range img.DataSyms {
|
||||
byName[d.Name] = d
|
||||
}
|
||||
want := []struct {
|
||||
sym string
|
||||
off int
|
||||
name string
|
||||
addend int64
|
||||
ext bool
|
||||
}{
|
||||
{"holder", 0, "Keep", 0, false},
|
||||
{"holder", 8, "Keep", 5, false},
|
||||
{"holder", 16, "holder", 0, false},
|
||||
{"spare", 0, "extvar", 0, true},
|
||||
}
|
||||
var flat []struct {
|
||||
sym string
|
||||
r Reloc
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
flat = append(flat, struct {
|
||||
sym string
|
||||
r Reloc
|
||||
}{d.Name, r})
|
||||
}
|
||||
}
|
||||
if len(flat) != len(want) {
|
||||
t.Fatalf("data relocations = %d, want %d", len(flat), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := flat[i]
|
||||
r := g.r
|
||||
if g.sym != w.sym {
|
||||
t.Errorf("relocation %d sits on %q, want %q", i, g.sym, w.sym)
|
||||
continue
|
||||
}
|
||||
if r.Off != w.off || r.Name != w.name || r.Addend != w.addend || r.External != w.ext {
|
||||
t.Errorf("relocation %d = {+%d %q addend %d ext %v}, want {+%d %q addend %d ext %v}",
|
||||
i, r.Off, r.Name, r.Addend, r.External, w.off, w.name, w.addend, w.ext)
|
||||
}
|
||||
if r.Kind != RelAddr {
|
||||
t.Errorf("relocation %d kind = %v, want RelAddr", i, r.Kind)
|
||||
}
|
||||
if r.Siz != 8 {
|
||||
t.Errorf("relocation %d siz = %d, want 8", i, r.Siz)
|
||||
}
|
||||
}
|
||||
// The fields themselves stay zero: only the linker fills them.
|
||||
for _, b := range img.Data {
|
||||
if b != 0 {
|
||||
t.Fatal("data section is not all zero before relocation")
|
||||
}
|
||||
}
|
||||
if len(img.Externals) != 1 || img.Externals[0] != "extvar" {
|
||||
t.Errorf("Externals = %v, want [extvar]", img.Externals)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectDataSymbolReloc pins the GOOBJ record a symbol-valued DATA
|
||||
// field produces, against the shape the toolchain emits for the same
|
||||
// source: an R_ADDR of the DATA width at the field offset, pkgIdxNone plus
|
||||
// the non-package definition index when the target is the file's own TEXT
|
||||
// function (the rt0 lib entry spelling).
|
||||
func TestGOObjectDataSymbolReloc(t *testing.T) {
|
||||
f, errs := parser.Parse("f_amd64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $16
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
`)
|
||||
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("main", "f_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
// Walk every relocation record; the data record is the one of Siz 8
|
||||
// and type R_ADDR.
|
||||
var off, add int64
|
||||
var pkg, sym uint32
|
||||
found := false
|
||||
for data := v.blk(blkReloc); len(data) >= 23; data = data[23:] {
|
||||
if data[4] != 8 || binary.LittleEndian.Uint16(data[5:]) != relocAddr {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
off = int64(int32(binary.LittleEndian.Uint32(data[0:])))
|
||||
add = int64(binary.LittleEndian.Uint64(data[7:]))
|
||||
pkg = binary.LittleEndian.Uint32(data[15:])
|
||||
sym = binary.LittleEndian.Uint32(data[19:])
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no data relocation record in the object")
|
||||
}
|
||||
if off != 0 || add != 5 {
|
||||
t.Errorf("data reloc = {off %d addend %d}, want {off 0 addend 5}", off, add)
|
||||
}
|
||||
if pkg != pkgIdxNone {
|
||||
t.Errorf("data reloc pkg = %#x, want pkgIdxNone (the TEXT function)", pkg)
|
||||
}
|
||||
// The function's non-package definition index: the four pc tables
|
||||
// precede it, so index 4.
|
||||
if sym != 4 {
|
||||
t.Errorf("data reloc sym = %d, want 4", sym)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectDataSymbolLink is the end-to-end proof for symbol-valued DATA
|
||||
// fields: the gasm object is substituted for the toolchain's and re-linked,
|
||||
// then executed, and the linked data word must hold the real address of the
|
||||
// function the DATA line named (runtime.FuncForPC identifies it).
|
||||
func TestGOObjectDataSymbolLink(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
GLOBL entry(SB), NOPTR, $8
|
||||
DATA entry+0(SB)/8, $·keepme(SB)
|
||||
|
||||
TEXT ·keepme(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
|
||||
TEXT ·entryptr(SB), NOSPLIT, $0-8
|
||||
MOVQ entry+0(SB), AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
import "runtime"
|
||||
|
||||
func keepme()
|
||||
func entryptr() uintptr
|
||||
|
||||
func main() {
|
||||
pc := entryptr()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
panic("the entry word does not point at a function")
|
||||
}
|
||||
if fn.Name() != "main.keepme" {
|
||||
panic("the entry word points at " + fn.Name())
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module dlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the build: the package archive's asm object and the link line.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
st := parseBuildLog(t, buildLog, "main_amd64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
// Assemble the same source with gasm and substitute the object.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_amd64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse("main_amd64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
// The package path is "main": the linker resolves the Go code's
|
||||
// references against main.<name>, so the object must define the symbols
|
||||
// under that prefix whatever the module is called.
|
||||
gasmObj, err := img.GOObject("main", "main_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"), gasmObj)
|
||||
|
||||
// The linked program must run and find the right function behind the
|
||||
// data word.
|
||||
out, err := exec.Command(filepath.Join(dir, "prog2")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("linked program failed: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectDataFloatAndStringValues covers the non-integer DATA values the
|
||||
// runtime's math and asm files use: floating-point initialisers store their
|
||||
// IEEE-754 bits (/4 the float32 rounding, /8 the full double) and string
|
||||
// initialisers write their bytes zero-padded within the declared width.
|
||||
func TestCollectDataFloatAndStringValues(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
GLOBL vals<>(SB), RODATA, $44
|
||||
DATA vals<>+0(SB)/8, $0.5
|
||||
DATA vals<>+8(SB)/8, $-1.0
|
||||
DATA vals<>+16(SB)/4, $1.5
|
||||
DATA vals<>+20(SB)/16, $"call frame too "
|
||||
DATA vals<>+36(SB)/4, $"hi"
|
||||
`
|
||||
f, errs := parser.Parse("fvals_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
byName := map[string]DataSymbol{}
|
||||
for _, d := range img.DataSyms {
|
||||
byName[d.Name] = d
|
||||
}
|
||||
d := byName["vals"]
|
||||
if d.Size != 44 {
|
||||
t.Fatalf("vals size = %d, want 44", d.Size)
|
||||
}
|
||||
buf := img.Data[d.Offset : d.Offset+44]
|
||||
// 0.5 = 0x3FE0000000000000, -1.0 = 0xBFF0000000000000 (float64);
|
||||
// 1.5 = 0x3FC00000 (float32).
|
||||
for _, c := range []struct {
|
||||
off int
|
||||
want []byte
|
||||
}{
|
||||
{0, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x3F}},
|
||||
{8, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xBF}},
|
||||
{16, []byte{0x00, 0x00, 0xC0, 0x3F}},
|
||||
{20, []byte("call frame too ")},
|
||||
{36, []byte{'h', 'i', 0x00, 0x00}},
|
||||
} {
|
||||
if string(buf[c.off:c.off+len(c.want)]) != string(c.want) {
|
||||
t.Errorf("vals+%d: got % x, want % x", c.off, buf[c.off:c.off+len(c.want)], c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectDataValueErrors pins the value-kind width rules: a float needs
|
||||
// width 4 or 8, a string must fit its declared width, and a bad float
|
||||
// literal is diagnosed rather than stored.
|
||||
func TestCollectDataValueErrors(t *testing.T) {
|
||||
cases := []string{
|
||||
`GLOBL v<>(SB), RODATA, $4
|
||||
DATA v<>+0(SB)/1, $0.5`,
|
||||
`GLOBL v<>(SB), RODATA, $2
|
||||
DATA v<>+0(SB)/2, $"toolarge"`,
|
||||
}
|
||||
for i, src := range cases {
|
||||
full := "#include \"textflag.h\"\nTEXT ·Keep(SB), NOSPLIT, $0-8\n\tRET\n" + src
|
||||
f, errs := parser.Parse(fmt.Sprintf("verr%d_amd64.s", i), full)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("case %d parse: %v", i, errs)
|
||||
}
|
||||
if _, err := AssembleFile(f); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1442
-91
File diff suppressed because it is too large.
Load diff
+681
-19
@@ -9,7 +9,7 @@ package asm
|
||||
// an opcode constant, and the format selects the bit layout. The opcode
|
||||
// constants and formats are transcribed from the Go toolchain's own loong64
|
||||
// backend (cmd/internal/obj/loong64), so the emitted bytes match `go tool asm`
|
||||
// exactly — the ground-truth oracle for the verify suite.
|
||||
// exactly, the ground-truth oracle for the verify suite.
|
||||
//
|
||||
// All LoongArch instructions are 32 bits, little-endian. The formats used
|
||||
// here (per the LoongArch Volume I specification):
|
||||
@@ -30,11 +30,14 @@ package asm
|
||||
// of the immediate and register fields), mirroring the toolchain's OP_*
|
||||
// helpers, so each l64* function only ORs its fields in.
|
||||
|
||||
import "maps"
|
||||
import (
|
||||
"maps"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// loong64RegNum returns the 5-bit register number for a LoongArch register
|
||||
// name: R0–R31 (integer), F0–F31 (floating point), FCC0–FCC7 (condition
|
||||
// flags), FCSR0–FCSR31 (control/status) and the ABI aliases the runtime's
|
||||
// name: R0-R31 (integer), F0-F31 (floating point), FCC0-FCC7 (condition
|
||||
// flags), FCSR0-FCSR31 (control/status) and the ABI aliases the runtime's
|
||||
// assembly uses. Returns -1 for an unrecognised name.
|
||||
func loong64RegNum(name string) int {
|
||||
switch name {
|
||||
@@ -103,7 +106,12 @@ func loong64RegNum(name string) int {
|
||||
case "R31", "S8":
|
||||
return 31
|
||||
}
|
||||
// F0–F31, FCC0–FCC7, FCSR0–FCSR31.
|
||||
// F0-F31, FCC0-FCC7, FCSR0-FCSR31. The LSX/LASX vector banks (V0-V31,
|
||||
// X0-X31) are deliberately NOT accepted here: they are a separate
|
||||
// register class, and the toolchain rejects V/X names wherever an
|
||||
// integer or FP register is expected (GOARCH=loong64 go tool asm reports
|
||||
// "unrecognized instruction" for `BEQZ X0`). Vector operands are
|
||||
// resolved only through loong64VecRegNum.
|
||||
if len(name) >= 4 && name[:4] == "FCSR" {
|
||||
return loong64RegSpecial(name[4:], 31)
|
||||
}
|
||||
@@ -148,6 +156,19 @@ func loong64RegSpecial(digits string, max int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// loong64VecRegNum resolves an LSX/LASX vector register name (V0-V31 or
|
||||
// X0-X31) to its 5-bit number, or -1. The vector banks are a register class
|
||||
// of their own: the toolchain accepts them only in the vector operands of the
|
||||
// LSX/LASX instructions (GOARCH=loong64 go tool asm assembles `VADDV V0, V1,
|
||||
// V2` and `XVADDV X0, X1, X2`, and rejects `VADDV R4, R5, R6`), so the V/X
|
||||
// spellings never reach the integer/FP resolver.
|
||||
func loong64VecRegNum(name string) int {
|
||||
if len(name) < 2 || (name[0] != 'V' && name[0] != 'X') {
|
||||
return -1
|
||||
}
|
||||
return loong64RegSpecial(name[1:], 31)
|
||||
}
|
||||
|
||||
// ---- format helpers ----
|
||||
|
||||
// l64rrr encodes a 3R instruction: op | rk<<10 | rj<<5 | rd.
|
||||
@@ -199,7 +220,9 @@ func l64rrrr(op uint32, r1, r2, r3, r4 int) uint32 {
|
||||
}
|
||||
|
||||
// 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 and are inserted unmasked: the caller
|
||||
// must have validated them (0..31 for the .w forms, 0..63 for the .d forms,
|
||||
// lsb <= msb), the same rule the toolchain enforces as "illegal bit number".
|
||||
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)
|
||||
}
|
||||
@@ -234,6 +257,37 @@ func l64WordsLE(ws ...uint32) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// l64firr14Words returns the words the 2RI14 families (LL/LLW/LLV, SC/SCW/
|
||||
// SCV, MOVWP/MOVVP) encode to for a 4-aligned byte offset. The toolchain
|
||||
// classifies the memory operand by span, with the class constants' -2 slop
|
||||
// included verbatim: a signed 16-bit offset (C_SOREG_16) rides in si14 alone;
|
||||
// a 32-bit one (C_LOREG_32) splits between addu16i.d (bits 31:16, materialised
|
||||
// in R30, the assembler temp) and si14 (bits 15:2); anything wider
|
||||
// (C_LOREG_64) builds the whole constant in R30 through lu12i.w + ori +
|
||||
// lu32i.d + lu52i.d and folds the base into it. The caller adjusts the
|
||||
// opcode for the 64-bit span's load-direction quirks before arriving here.
|
||||
func l64firr14Words(op uint32, off, rj, rd int) []byte {
|
||||
switch {
|
||||
case off >= -32766 && off < 32766:
|
||||
return l64wordLE(l64irr14(op, off>>2, rj, rd))
|
||||
case off >= -2147483650 && off < 2147483646:
|
||||
return l64WordsLE(
|
||||
l64irr16(l64InstrTable["ADDV16"].op, off>>16, 0, 30),
|
||||
l64rrr(l64DualTable["ADDV"].rrr, rj, 30, 30),
|
||||
l64irr14(op, off>>2, 30, rd),
|
||||
)
|
||||
default:
|
||||
return l64WordsLE(
|
||||
l64ir(l64InstrTable["LU12IW"].op, off>>12, 30),
|
||||
l64irr(l64DualTable["OR"].imm, off&0xFFF, 30, 30),
|
||||
l64ir(l64InstrTable["LU32ID"].op, off>>32, 30),
|
||||
l64irr(l64InstrTable["LU52ID"].op, off>>52, 30, 30),
|
||||
l64rrr(l64DualTable["ADDV"].rrr, 30, rj, rj),
|
||||
l64irr14(op, 0, rj, rd),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- instruction formats ----
|
||||
|
||||
type l64Format uint8
|
||||
@@ -245,7 +299,7 @@ const (
|
||||
l64Firr14 // 2RI14 (ldptr/stptr)
|
||||
l64Firr16 // 2RI16 (addu16i.d)
|
||||
l64Fir20 // 2RI20 (lu12i.w, lu32i.d, pcalau12i, pcaddu12i)
|
||||
l64Frrrr // 4R (fmadd/fmsub/fnmadd/fnmsub)
|
||||
l64Frrrr // 4R (fmadd/fmsub/fnmadd/fnmsub, fsel)
|
||||
l64Firir // bstrins/bstrpick
|
||||
l64Firrr // alsl
|
||||
l64Fi15 // syscall/break/dbar
|
||||
@@ -253,6 +307,11 @@ const (
|
||||
l64Frdtime // rdtime (rd at bits [9:5], rj at bits [4:0])
|
||||
l64Fshift // 2RI12 with a 5/6-bit shift immediate
|
||||
l64Fpreld // preld (2RI12 + 5-bit hint)
|
||||
l64Fvvv // 3R vector (LSX/LASX): op | vk<<10 | vj<<5 | vd
|
||||
l64Fvcf // vector-to-condition: op | subop<<10 | vj<<5 | fcc
|
||||
l64Fvvvv // 4R vector shuffle: op | va<<15 | vk<<10 | vj<<5 | vd
|
||||
l64Fllsc // acquire/release LL/SC (2R against a zero-offset memory operand)
|
||||
l64Fscq // sc.q: op | middle<<10 | base<<5 | first against a zero-offset memory operand
|
||||
)
|
||||
|
||||
// l64Enc is one instruction's encoding: its bit layout (format) and the
|
||||
@@ -275,17 +334,90 @@ type l64DualEnc struct {
|
||||
var l64DualTable = map[string]l64DualEnc{}
|
||||
|
||||
// l64InstrTable maps LoongArch mnemonics (as the Go assembler spells them)
|
||||
// to their encoding. SIMD (LSX/LASX: V*/XV*) instructions are not covered
|
||||
// yet; the base integer, memory and floating-point ISA is complete.
|
||||
// to their encoding.
|
||||
var l64InstrTable = map[string]l64Enc{}
|
||||
|
||||
// l64Vec3Enc pairs a vector opcode with its register bank: false = LSX
|
||||
// (V0-V31), true = LASX (X0-X31). The toolchain accepts one bank per
|
||||
// spelling: GOARCH=loong64 go tool asm assembles `VADDV V1, V2, V3` and
|
||||
// `XVADDV X1, X2, X3`, and rejects the crossed spellings.
|
||||
type l64Vec3Enc struct {
|
||||
op uint32
|
||||
lasx bool
|
||||
}
|
||||
|
||||
// l64VecImmEnc carries the immediate-form encoding of a vector mnemonic:
|
||||
// the opcode, the bank, the accepted immediate range, the bias the toolchain
|
||||
// adds (vsrai.b encodes imm+8) and the mask of the encoded field (vseqi.b
|
||||
// keeps a 5-bit two's-complement value, vseqi.d a 7-bit one).
|
||||
type l64VecImmEnc struct {
|
||||
op uint32
|
||||
lasx bool
|
||||
min, max int
|
||||
bias int
|
||||
mask int
|
||||
}
|
||||
|
||||
// l64VecBank marks the LSX/LASX mnemonics and records which register bank
|
||||
// each accepts; presence in the map routes the mnemonic through the vector
|
||||
// dispatcher rather than the integer/FP formats.
|
||||
var l64VecBank = map[string]bool{}
|
||||
|
||||
// l64VecImmInfo mirrors l64VecImmTable for the dispatcher.
|
||||
var l64VecImmInfo = map[string]l64VecImmEnc{}
|
||||
|
||||
// l64Vec2R marks the two-operand vector mnemonics (INSTR vj, vd, such as
|
||||
// vpcnt.v).
|
||||
var l64Vec2R = map[string]bool{}
|
||||
|
||||
// l64Vec4R marks the four-operand vector mnemonics (INSTR va, vk, vj, vd,
|
||||
// such as vshuf.b).
|
||||
var l64Vec4R = map[string]bool{}
|
||||
|
||||
// l64VmovqOps holds the VMOVQ/XVMOVQ opcode constants, each pre-shifted to
|
||||
// its exact bit range, read off `go tool objdump` of GOARCH=loong64
|
||||
// `go tool asm` kernels and the toolchain's specialLsxMovInst table.
|
||||
type l64VmovqEnc struct {
|
||||
ld, st, ldx, stx uint32 // plain and indexed load/store
|
||||
replB, replH, replW, replD uint32 // vldrepl: load and replicate element
|
||||
pickS, pickU uint32 // vpickve2gr.{,u} element extract
|
||||
ins uint32 // vinsgr2vr element insert
|
||||
dup uint32 // vreplgr2vr duplicate (width in [11:10])
|
||||
move uint32 // vori.b/xvori.b $0 register move
|
||||
rveiB, rveiH, rveiW, rveiD uint32 // vreplvei: broadcast one element (LSX)
|
||||
rve0B, rve0H, rve0W uint32 // xvreplve0 broadcast of element zero (LASX)
|
||||
rve0D, rve0Q uint32 // xvreplve0.{d,q}, ditto
|
||||
xinsW, xinsD uint32 // xvinsve0: insert element zero (LASX)
|
||||
xpickW, xpickD uint32 // xvpickve: extract element (LASX)
|
||||
}
|
||||
|
||||
var l64VmovqTable = map[bool]l64VmovqEnc{
|
||||
false: { // VMOVQ, the LSX (V) bank
|
||||
ld: 0x5800 << 15, st: 0x5880 << 15, ldx: 0x7080 << 15, stx: 0x7088 << 15,
|
||||
replB: 0x6100 << 15, replH: 0x6080 << 15, replW: 0x6040 << 15, replD: 0x6020 << 15,
|
||||
pickS: 0xE5DF << 15, pickU: 0xE5E7 << 15,
|
||||
ins: 0xE5D7 << 15, dup: 0xE53E << 15, move: 0xE65A << 15,
|
||||
rveiB: 0x01CBDE << 14, rveiH: 0x0397BE << 13, rveiW: 0x072F7E << 12, rveiD: 0x0E5EFE << 11,
|
||||
},
|
||||
true: { // XVMOVQ, the LASX (X) bank
|
||||
ld: 0x5900 << 15, st: 0x5980 << 15, ldx: 0x7090 << 15, stx: 0x7098 << 15,
|
||||
replB: 0x6500 << 15, replH: 0x6480 << 15, replW: 0x6440 << 15, replD: 0x6420 << 15,
|
||||
pickS: 0xEDDF << 15, pickU: 0xEDE7 << 15,
|
||||
ins: 0xEDD7 << 15, dup: 0xED3E << 15, move: 0xEE5A << 15,
|
||||
rve0B: 0x1DC1C0 << 10, rve0H: 0x1DC1E0 << 10, rve0W: 0x1DC1F0 << 10,
|
||||
rve0D: 0x1DC1F8 << 10, rve0Q: 0x1DC1FC << 10,
|
||||
xinsW: 0x03B7FE << 13, xinsD: 0x076FFE << 12,
|
||||
xpickW: 0x03B81E << 13, xpickD: 0x07703E << 12,
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 3R — integer.
|
||||
// 3R, integer.
|
||||
rrr := map[string]uint32{
|
||||
"ADD": 0x20 << 15, "ADDW": 0x20 << 15, "ADDV": 0x21 << 15, "ADDVU": 0x21 << 15,
|
||||
"SUB": 0x22 << 15, "SUBW": 0x22 << 15, "SUBV": 0x23 << 15, "SUBVU": 0x23 << 15,
|
||||
"SGT": 0x24 << 15, "SGTU": 0x25 << 15,
|
||||
"MASKEQZ": 0x26 << 15, "MASKNEZ": 0x27 << 15, "SCQ": 0x070AE << 15,
|
||||
"MASKEQZ": 0x26 << 15, "MASKNEZ": 0x27 << 15,
|
||||
"NOR": 0x28 << 15, "AND": 0x29 << 15, "OR": 0x2a << 15, "XOR": 0x2b << 15,
|
||||
"ORN": 0x2c << 15, "ANDN": 0x2d << 15,
|
||||
"SLL": 0x2e << 15, "SRL": 0x2f << 15, "SRA": 0x30 << 15,
|
||||
@@ -300,7 +432,7 @@ func init() {
|
||||
"CRCWBW": 0x48 << 15, "CRCWHW": 0x49 << 15, "CRCWWW": 0x4a << 15, "CRCWVW": 0x4b << 15,
|
||||
"CRCCWBW": 0x4c << 15, "CRCCWHW": 0x4d << 15, "CRCCWWW": 0x4e << 15, "CRCCWVW": 0x4f << 15,
|
||||
}
|
||||
// 3R — floating point.
|
||||
// 3R, floating point.
|
||||
rrr["MULF"] = 0x209 << 15
|
||||
rrr["MULD"] = 0x20a << 15
|
||||
rrr["DIVF"] = 0x20d << 15
|
||||
@@ -358,6 +490,18 @@ func init() {
|
||||
"FTINTRZVF": 0x46a9 << 10, "FTINTRZVD": 0x46aa << 10,
|
||||
"FTINTRNEWF": 0x46b1 << 10, "FTINTRNEWD": 0x46b2 << 10,
|
||||
"FTINTRNEVF": 0x46b9 << 10, "FTINTRNEVD": 0x46ba << 10,
|
||||
// LSX: convert a 64-bit integer lane to a double float. The operand
|
||||
// bank is the FP registers (the toolchain spells it `FFINTDV F0, F1`),
|
||||
// so the entry stays on the 2R integer/FP format.
|
||||
"FFINTDV": 0x474a << 10,
|
||||
// The rest of the scalar conversions (all F-bank, 2R).
|
||||
"FFINTFW": 0x4744 << 10, // ffint.s.w
|
||||
"FFINTFV": 0x4746 << 10, // ffint.s.l
|
||||
"FFINTDW": 0x4748 << 10, // ffint.d.w
|
||||
"FTINTWF": 0x46c1 << 10, // ftint.w.s
|
||||
"FTINTWD": 0x46c2 << 10, // ftint.w.d
|
||||
"FTINTVF": 0x46c9 << 10, // ftint.l.s
|
||||
"FTINTVD": 0x46ca << 10, // ftint.l.d
|
||||
}
|
||||
for m, op := range rr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frr, op: op}
|
||||
@@ -367,6 +511,20 @@ func init() {
|
||||
l64InstrTable["RDTIMEHW"] = l64Enc{format: l64Frdtime, op: 0x19 << 10}
|
||||
l64InstrTable["RDTIMED"] = l64Enc{format: l64Frdtime, op: 0x1a << 10}
|
||||
|
||||
// Acquire/release LL/SC (2R against a zero-offset memory operand):
|
||||
// LLACQV (Rj), Rd loads, SCRELV Rd, (Rj) stores, both encoding
|
||||
// op | rj<<5 | rd. Opcodes from cmd/internal/obj/loong64/instOp.go
|
||||
// (ll.acq.{w,d}, sc.rel.{w,d}).
|
||||
l64InstrTable["LLACQW"] = l64Enc{format: l64Fllsc, op: 0x0E15E0 << 10}
|
||||
l64InstrTable["SCRELW"] = l64Enc{format: l64Fllsc, op: 0x0E15E1 << 10}
|
||||
l64InstrTable["LLACQV"] = l64Enc{format: l64Fllsc, op: 0x0E15E2 << 10}
|
||||
l64InstrTable["SCRELV"] = l64Enc{format: l64Fllsc, op: 0x0E15E3 << 10}
|
||||
|
||||
// SCQ (sc.q first, middle, (base)) keeps its own operand order: the
|
||||
// encoding is op | middle<<10 | base<<5 | first, the memory operand's
|
||||
// base in the rj field, not the toolchain's generic 3R layout.
|
||||
l64InstrTable["SCQ"] = l64Enc{format: l64Fscq, op: 0x070AE << 15}
|
||||
|
||||
// The dual-form arithmetic mnemonics (register 3R + immediate 2RI12),
|
||||
// selected by the operand kind; the shift mnemonics pair the 3R form
|
||||
// with a 5/6-bit shift immediate.
|
||||
@@ -390,12 +548,12 @@ func init() {
|
||||
"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}
|
||||
// ADDV16 (addu16i.d): 2RI16 with the immediate shifted right by 16.
|
||||
l64InstrTable["ADDV16"] = l64Enc{format: l64Firr16, op: 0x4 << 26}
|
||||
|
||||
// 2RI14 — LL/SC are aliased by the Go assembler to the pointer loads and
|
||||
// 2RI14, LL/SC are aliased by the Go assembler to the pointer loads and
|
||||
// stores (ldptr/stptr), with the offset scaled by 4.
|
||||
l64InstrTable["MOVWP"] = l64Enc{format: l64Firr14, op: 0x25 << 24} // stptr.w
|
||||
l64InstrTable["MOVVP"] = l64Enc{format: l64Firr14, op: 0x27 << 24} // stptr.d
|
||||
@@ -414,18 +572,20 @@ func init() {
|
||||
// LUI is the Plan 9 spelling of lu12i.w.
|
||||
l64InstrTable["LUI"] = l64Enc{format: l64Fir20, op: 0x0a << 25}
|
||||
|
||||
// 4R — fused multiply-add.
|
||||
// 4R, fused multiply-add, and FSEL (fsel.d: the first operand is a FCC
|
||||
// condition flag, the layout matches the 4R shape).
|
||||
rrrr := map[string]uint32{
|
||||
"FMADDF": 0x81 << 20, "FMADDD": 0x82 << 20,
|
||||
"FMSUBF": 0x85 << 20, "FMSUBD": 0x86 << 20,
|
||||
"FNMADDF": 0x89 << 20, "FNMADDD": 0x8a << 20,
|
||||
"FNMSUBF": 0x8d << 20, "FNMSUBD": 0x8e << 20,
|
||||
"FSEL": 0x340 << 18,
|
||||
}
|
||||
for m, op := range rrrr {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frrrr, op: op}
|
||||
}
|
||||
|
||||
// IRIR — bit-field insert/extract.
|
||||
// IRIR, bit-field insert/extract.
|
||||
irir := map[string]uint32{
|
||||
"BSTRINSW": 0x3<<21 | 0x0<<15,
|
||||
"BSTRINSV": 0x2 << 22,
|
||||
@@ -436,7 +596,7 @@ func init() {
|
||||
l64InstrTable[m] = l64Enc{format: l64Firir, op: op}
|
||||
}
|
||||
|
||||
// 3RI2 — ALSL.
|
||||
// 3RI2, ALSL.
|
||||
irrr := map[string]uint32{
|
||||
"ALSLW": 0x2 << 17, "ALSLWU": 0x3 << 17, "ALSLV": 0x16 << 17,
|
||||
}
|
||||
@@ -452,7 +612,11 @@ func init() {
|
||||
// PRELD.
|
||||
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).
|
||||
// The toolchain's form is three operands, `AMADDW rk, (rj), rd`
|
||||
// (cmd/asm/internal/asm/testdata/loong64enc1.s and
|
||||
// internal/runtime/atomic/atomic_loong64.s); the two-register spelling
|
||||
// is rejected by the oracle.
|
||||
am := map[string]uint32{
|
||||
"AMSWAPB": 0x070B8 << 15, "AMSWAPH": 0x070B9 << 15,
|
||||
"AMSWAPW": 0x070C0 << 15, "AMSWAPV": 0x070C1 << 15,
|
||||
@@ -470,14 +634,512 @@ func init() {
|
||||
"AMSWAPDBW": 0x070D2 << 15, "AMSWAPDBV": 0x070D3 << 15,
|
||||
"AMCASDBB": 0x070B4 << 15, "AMCASDBH": 0x070B5 << 15,
|
||||
"AMCASDBW": 0x070B6 << 15, "AMCASDBV": 0x070B7 << 15,
|
||||
// The _dbar (acquire/release) add, and, or variants: opcodes read off
|
||||
// `go tool objdump` of `AMADDDBW R14, (R13), R12` and friends.
|
||||
"AMADDDBW": 0x070D4 << 15, "AMADDDBV": 0x070D5 << 15,
|
||||
"AMANDDBW": 0x070D6 << 15, "AMANDDBV": 0x070D7 << 15,
|
||||
"AMORDBW": 0x070D8 << 15, "AMORDBV": 0x070D9 << 15,
|
||||
// The remaining _dbar exchange variants (loong64enc1.s).
|
||||
"AMXORDBW": 0x070DA << 15, "AMXORDBV": 0x070DB << 15,
|
||||
"AMMAXDBW": 0x070DC << 15, "AMMAXDBV": 0x070DD << 15,
|
||||
"AMMINDBW": 0x070DE << 15, "AMMINDBV": 0x070DF << 15,
|
||||
"AMMAXDBWU": 0x070E0 << 15, "AMMAXDBVU": 0x070E1 << 15,
|
||||
"AMMINDBWU": 0x070E2 << 15, "AMMINDBVU": 0x070E3 << 15,
|
||||
}
|
||||
for m, op := range am {
|
||||
l64InstrTable[m] = l64Enc{format: l64Fam, op: op}
|
||||
}
|
||||
|
||||
// ---- LSX/LASX (V*/XV*) ----
|
||||
// Every opcode below was read off `go tool objdump` of a GOARCH=loong64
|
||||
// `go tool asm` kernel (the toolchain's own loong64enc1.s cross-checks
|
||||
// most of them), not assumed from the LoongArch manual.
|
||||
|
||||
// Three vector registers: INSTR vk, vj, vd (or INSTR vk, vd with
|
||||
// vj = vd). l64Vec3Enc.lasx selects the register bank the toolchain
|
||||
// accepts: LSX spellings take V0-V31, LASX spellings X0-X31.
|
||||
vec3 := map[string]l64Vec3Enc{
|
||||
"VADDW": {0xE016 << 15, false}, "VADDV": {0xE017 << 15, false},
|
||||
"VANDV": {0xE24C << 15, false}, "VXORV": {0xE24E << 15, false},
|
||||
"VSEQB": {0xE000 << 15, false}, "VSEQV": {0xE003 << 15, false},
|
||||
"VSRAB": {0xE1D8 << 15, false}, "VROTRW": {0xE1DE << 15, false},
|
||||
"XVADDV": {0xE817 << 15, true},
|
||||
"XVANDV": {0xEA4C << 15, true}, "XVXORV": {0xEA4E << 15, true},
|
||||
"XVSEQB": {0xE800 << 15, true}, "XVSEQV": {0xE803 << 15, true},
|
||||
}
|
||||
|
||||
// The integer and FP add/subtract families: [X]VADD and [X]VSUB by lane
|
||||
// width, plus the [X]VSADD/[X]VSSUB saturating pairs.
|
||||
// Opcodes transcribed from the toolchain's loong64enc1.s.
|
||||
addsub := map[string]l64Vec3Enc{
|
||||
"VADDB": {0xE014 << 15, false}, "VADDH": {0xE015 << 15, false},
|
||||
"VADDD": {0xE262 << 15, false}, "VADDF": {0xE261 << 15, false},
|
||||
"VADDQ": {0xE25A << 15, false},
|
||||
"VSUBB": {0xE018 << 15, false}, "VSUBH": {0xE019 << 15, false},
|
||||
"VSUBW": {0xE01A << 15, false}, "VSUBV": {0xE01B << 15, false},
|
||||
"VSUBQ": {0xE25B << 15, false},
|
||||
"VSUBF": {0xE265 << 15, false}, "VSUBD": {0xE266 << 15, false},
|
||||
"VSADDB": {0xE08C << 15, false}, "VSADDH": {0xE08D << 15, false},
|
||||
"VSADDW": {0xE08E << 15, false}, "VSADDV": {0xE08F << 15, false},
|
||||
"VSADDBU": {0xE094 << 15, false}, "VSADDHU": {0xE095 << 15, false},
|
||||
"VSADDWU": {0xE096 << 15, false}, "VSADDVU": {0xE097 << 15, false},
|
||||
"VSSUBB": {0xE090 << 15, false}, "VSSUBH": {0xE091 << 15, false},
|
||||
"VSSUBW": {0xE092 << 15, false}, "VSSUBV": {0xE093 << 15, false},
|
||||
"VSSUBBU": {0xE098 << 15, false}, "VSSUBHU": {0xE099 << 15, false},
|
||||
"VSSUBWU": {0xE09A << 15, false}, "VSSUBVU": {0xE09B << 15, false},
|
||||
"XVADDB": {0xE814 << 15, true}, "XVADDH": {0xE815 << 15, true},
|
||||
"XVADDW": {0xE816 << 15, true},
|
||||
"XVADDD": {0xEA62 << 15, true}, "XVADDF": {0xEA61 << 15, true},
|
||||
"XVADDQ": {0xEA5A << 15, true},
|
||||
"XVSUBB": {0xE818 << 15, true}, "XVSUBH": {0xE819 << 15, true},
|
||||
"XVSUBW": {0xE81A << 15, true}, "XVSUBV": {0xE81B << 15, true},
|
||||
"XVSUBQ": {0xEA5B << 15, true},
|
||||
"XVSUBF": {0xEA65 << 15, true}, "XVSUBD": {0xEA66 << 15, true},
|
||||
"XVSADDB": {0xE88C << 15, true}, "XVSADDH": {0xE88D << 15, true},
|
||||
"XVSADDW": {0xE88E << 15, true}, "XVSADDV": {0xE88F << 15, true},
|
||||
"XVSADDBU": {0xE894 << 15, true}, "XVSADDHU": {0xE895 << 15, true},
|
||||
"XVSADDWU": {0xE896 << 15, true}, "XVSADDVU": {0xE897 << 15, true},
|
||||
"XVSSUBB": {0xE890 << 15, true}, "XVSSUBH": {0xE891 << 15, true},
|
||||
"XVSSUBW": {0xE892 << 15, true}, "XVSSUBV": {0xE893 << 15, true},
|
||||
"XVSSUBBU": {0xE898 << 15, true}, "XVSSUBHU": {0xE899 << 15, true},
|
||||
"XVSSUBWU": {0xE89A << 15, true}, "XVSSUBVU": {0xE89B << 15, true},
|
||||
}
|
||||
|
||||
// The multiply families: plain and high-half [X]VMUL/[X]VMUH, the
|
||||
// widening [X]VMULW{EV,OD} ladder and its accumulating [X]VMADDW twins,
|
||||
// plus the [X]VMADD/[X]VMSUB fused multiply-add and the [X]VDIV/[X]VMOD
|
||||
// divide and modulo pairs.
|
||||
muldiv := map[string]l64Vec3Enc{
|
||||
"VMULB": {0xE108 << 15, false}, "VMULH": {0xE109 << 15, false},
|
||||
"VMULW": {0xE10A << 15, false}, "VMULV": {0xE10B << 15, false},
|
||||
"VMUHB": {0xE10C << 15, false}, "VMUHH": {0xE10D << 15, false},
|
||||
"VMUHW": {0xE10E << 15, false}, "VMUHV": {0xE10F << 15, false},
|
||||
"VMUHBU": {0xE110 << 15, false}, "VMUHHU": {0xE111 << 15, false},
|
||||
"VMUHWU": {0xE112 << 15, false}, "VMUHVU": {0xE113 << 15, false},
|
||||
"VMULWEVHB": {0xE120 << 15, false}, "VMULWEVWH": {0xE121 << 15, false},
|
||||
"VMULWEVVW": {0xE122 << 15, false}, "VMULWEVQV": {0xE123 << 15, false},
|
||||
"VMULWODHB": {0xE124 << 15, false}, "VMULWODWH": {0xE125 << 15, false},
|
||||
"VMULWODVW": {0xE126 << 15, false}, "VMULWODQV": {0xE127 << 15, false},
|
||||
"VMULWEVHBU": {0xE130 << 15, false}, "VMULWEVWHU": {0xE131 << 15, false},
|
||||
"VMULWEVVWU": {0xE132 << 15, false}, "VMULWEVQVU": {0xE133 << 15, false},
|
||||
"VMULWODHBU": {0xE134 << 15, false}, "VMULWODWHU": {0xE135 << 15, false},
|
||||
"VMULWODVWU": {0xE136 << 15, false}, "VMULWODQVU": {0xE137 << 15, false},
|
||||
"VMULWEVHBUB": {0xE140 << 15, false}, "VMULWEVWHUH": {0xE141 << 15, false},
|
||||
"VMULWEVVWUW": {0xE142 << 15, false}, "VMULWEVQVUV": {0xE143 << 15, false},
|
||||
"VMULWODHBUB": {0xE144 << 15, false}, "VMULWODWHUH": {0xE145 << 15, false},
|
||||
"VMULWODVWUW": {0xE146 << 15, false}, "VMULWODQVUV": {0xE147 << 15, false},
|
||||
"VMADDB": {0xE150 << 15, false}, "VMADDH": {0xE151 << 15, false},
|
||||
"VMADDW": {0xE152 << 15, false}, "VMADDV": {0xE153 << 15, false},
|
||||
"VMSUBB": {0xE154 << 15, false}, "VMSUBH": {0xE155 << 15, false},
|
||||
"VMSUBW": {0xE156 << 15, false}, "VMSUBV": {0xE157 << 15, false},
|
||||
"VMADDWEVHB": {0xE158 << 15, false}, "VMADDWEVWH": {0xE159 << 15, false},
|
||||
"VMADDWEVVW": {0xE15A << 15, false}, "VMADDWEVQV": {0xE15B << 15, false},
|
||||
"VMADDWODHB": {0xE15C << 15, false}, "VMADDWODWH": {0xE15D << 15, false},
|
||||
"VMADDWODVW": {0xE15E << 15, false}, "VMADDWODQV": {0xE15F << 15, false},
|
||||
"VMADDWEVHBU": {0xE168 << 15, false}, "VMADDWEVWHU": {0xE169 << 15, false},
|
||||
"VMADDWEVVWU": {0xE16A << 15, false}, "VMADDWEVQVU": {0xE16B << 15, false},
|
||||
"VMADDWODHBU": {0xE16C << 15, false}, "VMADDWODWHU": {0xE16D << 15, false},
|
||||
"VMADDWODVWU": {0xE16E << 15, false}, "VMADDWODQVU": {0xE16F << 15, false},
|
||||
"VMADDWEVHBUB": {0xE178 << 15, false}, "VMADDWEVWHUH": {0xE179 << 15, false},
|
||||
"VMADDWEVVWUW": {0xE17A << 15, false}, "VMADDWEVQVUV": {0xE17B << 15, false},
|
||||
"VMADDWODHBUB": {0xE17C << 15, false}, "VMADDWODWHUH": {0xE17D << 15, false},
|
||||
"VMADDWODVWUW": {0xE17E << 15, false}, "VMADDWODQVUV": {0xE17F << 15, false},
|
||||
"VDIVB": {0xE1C0 << 15, false}, "VDIVH": {0xE1C1 << 15, false},
|
||||
"VDIVW": {0xE1C2 << 15, false}, "VDIVV": {0xE1C3 << 15, false},
|
||||
"VMODB": {0xE1C4 << 15, false}, "VMODH": {0xE1C5 << 15, false},
|
||||
"VMODW": {0xE1C6 << 15, false}, "VMODV": {0xE1C7 << 15, false},
|
||||
"VDIVBU": {0xE1C8 << 15, false}, "VDIVHU": {0xE1C9 << 15, false},
|
||||
"VDIVWU": {0xE1CA << 15, false}, "VDIVVU": {0xE1CB << 15, false},
|
||||
"VMODBU": {0xE1CC << 15, false}, "VMODHU": {0xE1CD << 15, false},
|
||||
"VMODWU": {0xE1CE << 15, false}, "VMODVU": {0xE1CF << 15, false},
|
||||
"VMULF": {0xE271 << 15, false}, "VMULD": {0xE272 << 15, false},
|
||||
"VDIVF": {0xE275 << 15, false}, "VDIVD": {0xE276 << 15, false},
|
||||
"XVMULB": {0xE908 << 15, true}, "XVMULH": {0xE909 << 15, true},
|
||||
"XVMULW": {0xE90A << 15, true}, "XVMULV": {0xE90B << 15, true},
|
||||
"XVMUHB": {0xE90C << 15, true}, "XVMUHH": {0xE90D << 15, true},
|
||||
"XVMUHW": {0xE90E << 15, true}, "XVMUHV": {0xE90F << 15, true},
|
||||
"XVMUHBU": {0xE910 << 15, true}, "XVMUHHU": {0xE911 << 15, true},
|
||||
"XVMUHWU": {0xE912 << 15, true}, "XVMUHVU": {0xE913 << 15, true},
|
||||
"XVMULWEVHB": {0xE920 << 15, true}, "XVMULWEVWH": {0xE921 << 15, true},
|
||||
"XVMULWEVVW": {0xE922 << 15, true}, "XVMULWEVQV": {0xE923 << 15, true},
|
||||
"XVMULWODHB": {0xE924 << 15, true}, "XVMULWODWH": {0xE925 << 15, true},
|
||||
"XVMULWODVW": {0xE926 << 15, true}, "XVMULWODQV": {0xE927 << 15, true},
|
||||
"XVMULWEVHBU": {0xE930 << 15, true}, "XVMULWEVWHU": {0xE931 << 15, true},
|
||||
"XVMULWEVVWU": {0xE932 << 15, true}, "XVMULWEVQVU": {0xE933 << 15, true},
|
||||
"XVMULWODHBU": {0xE934 << 15, true}, "XVMULWODWHU": {0xE935 << 15, true},
|
||||
"XVMULWODVWU": {0xE936 << 15, true}, "XVMULWODQVU": {0xE937 << 15, true},
|
||||
"XVMULWEVHBUB": {0xE940 << 15, true}, "XVMULWEVWHUH": {0xE941 << 15, true},
|
||||
"XVMULWEVVWUW": {0xE942 << 15, true}, "XVMULWEVQVUV": {0xE943 << 15, true},
|
||||
"XVMULWODHBUB": {0xE944 << 15, true}, "XVMULWODWHUH": {0xE945 << 15, true},
|
||||
"XVMULWODVWUW": {0xE946 << 15, true}, "XVMULWODQVUV": {0xE947 << 15, true},
|
||||
"XVMADDB": {0xE950 << 15, true}, "XVMADDH": {0xE951 << 15, true},
|
||||
"XVMADDW": {0xE952 << 15, true}, "XVMADDV": {0xE953 << 15, true},
|
||||
"XVMSUBB": {0xE954 << 15, true}, "XVMSUBH": {0xE955 << 15, true},
|
||||
"XVMSUBW": {0xE956 << 15, true}, "XVMSUBV": {0xE957 << 15, true},
|
||||
"XVMADDWEVHB": {0xE958 << 15, true}, "XVMADDWEVWH": {0xE959 << 15, true},
|
||||
"XVMADDWEVVW": {0xE95A << 15, true}, "XVMADDWEVQV": {0xE95B << 15, true},
|
||||
"XVMADDWODHB": {0xE95C << 15, true}, "XVMADDWODWH": {0xE95D << 15, true},
|
||||
"XVMADDWODVW": {0xE95E << 15, true}, "XVMADDWODQV": {0xE95F << 15, true},
|
||||
"XVMADDWEVHBU": {0xE968 << 15, true}, "XVMADDWEVWHU": {0xE969 << 15, true},
|
||||
"XVMADDWEVVWU": {0xE96A << 15, true}, "XVMADDWEVQVU": {0xE96B << 15, true},
|
||||
"XVMADDWODHBU": {0xE96C << 15, true}, "XVMADDWODWHU": {0xE96D << 15, true},
|
||||
"XVMADDWODVWU": {0xE96E << 15, true}, "XVMADDWODQVU": {0xE96F << 15, true},
|
||||
"XVMADDWEVHBUB": {0xE978 << 15, true}, "XVMADDWEVWHUH": {0xE979 << 15, true},
|
||||
"XVMADDWEVVWUW": {0xE97A << 15, true}, "XVMADDWEVQVUV": {0xE97B << 15, true},
|
||||
"XVMADDWODHBUB": {0xE97C << 15, true}, "XVMADDWODWHUH": {0xE97D << 15, true},
|
||||
"XVMADDWODVWUW": {0xE97E << 15, true}, "XVMADDWODQVUV": {0xE97F << 15, true},
|
||||
"XVDIVB": {0xE9C0 << 15, true}, "XVDIVH": {0xE9C1 << 15, true},
|
||||
"XVDIVW": {0xE9C2 << 15, true}, "XVDIVV": {0xE9C3 << 15, true},
|
||||
"XVMODB": {0xE9C4 << 15, true}, "XVMODH": {0xE9C5 << 15, true},
|
||||
"XVMODW": {0xE9C6 << 15, true}, "XVMODV": {0xE9C7 << 15, true},
|
||||
"XVDIVBU": {0xE9C8 << 15, true}, "XVDIVHU": {0xE9C9 << 15, true},
|
||||
"XVDIVWU": {0xE9CA << 15, true}, "XVDIVVU": {0xE9CB << 15, true},
|
||||
"XVMODBU": {0xE9CC << 15, true}, "XVMODHU": {0xE9CD << 15, true},
|
||||
"XVMODWU": {0xE9CE << 15, true}, "XVMODVU": {0xE9CF << 15, true},
|
||||
"XVMULF": {0xEA71 << 15, true}, "XVMULD": {0xEA72 << 15, true},
|
||||
"XVDIVF": {0xEA75 << 15, true}, "XVDIVD": {0xEA76 << 15, true},
|
||||
}
|
||||
|
||||
// The lane-wise shifts and rotates (three-register forms; the immediate
|
||||
// forms live in l64VecImmInfo), the interleave families, the bit
|
||||
// clear/set/rev register forms, the remaining logic and compare
|
||||
// spellings, the widening add/subtract ladder and the vector FP
|
||||
// arithmetic.
|
||||
vecmisc := map[string]l64Vec3Enc{
|
||||
"VSLLB": {0xE1D0 << 15, false}, "VSLLH": {0xE1D1 << 15, false},
|
||||
"VSLLW": {0xE1D2 << 15, false}, "VSLLV": {0xE1D3 << 15, false},
|
||||
"VSRLB": {0xE1D4 << 15, false}, "VSRLH": {0xE1D5 << 15, false},
|
||||
"VSRLW": {0xE1D6 << 15, false}, "VSRLV": {0xE1D7 << 15, false},
|
||||
"VSRAH": {0xE1D9 << 15, false}, "VSRAW": {0xE1DA << 15, false},
|
||||
"VSRAV": {0xE1DB << 15, false},
|
||||
"VROTRB": {0xE1DC << 15, false}, "VROTRH": {0xE1DD << 15, false},
|
||||
"VROTRV": {0xE1DF << 15, false},
|
||||
"VILVLB": {0xE234 << 15, false}, "VILVLH": {0xE235 << 15, false},
|
||||
"VILVLW": {0xE236 << 15, false}, "VILVLV": {0xE237 << 15, false},
|
||||
"VILVHB": {0xE238 << 15, false}, "VILVHH": {0xE239 << 15, false},
|
||||
"VILVHW": {0xE23A << 15, false}, "VILVHV": {0xE23B << 15, false},
|
||||
"VBITCLRB": {0xE218 << 15, false}, "VBITCLRH": {0xE219 << 15, false},
|
||||
"VBITCLRW": {0xE21A << 15, false}, "VBITCLRV": {0xE21B << 15, false},
|
||||
"VBITSETB": {0xE21C << 15, false}, "VBITSETH": {0xE21D << 15, false},
|
||||
"VBITSETW": {0xE21E << 15, false}, "VBITSETV": {0xE21F << 15, false},
|
||||
"VBITREVB": {0xE220 << 15, false}, "VBITREVH": {0xE221 << 15, false},
|
||||
"VBITREVW": {0xE222 << 15, false}, "VBITREVV": {0xE223 << 15, false},
|
||||
"VORV": {0xE24D << 15, false}, "VNORV": {0xE24F << 15, false},
|
||||
"VANDNV": {0xE250 << 15, false}, "VORNV": {0xE251 << 15, false},
|
||||
"VSEQH": {0xE001 << 15, false}, "VSEQW": {0xE002 << 15, false},
|
||||
"VSLTB": {0xE00C << 15, false}, "VSLTH": {0xE00D << 15, false},
|
||||
"VSLTW": {0xE00E << 15, false}, "VSLTV": {0xE00F << 15, false},
|
||||
"VSLTBU": {0xE010 << 15, false}, "VSLTHU": {0xE011 << 15, false},
|
||||
"VSLTWU": {0xE012 << 15, false}, "VSLTVU": {0xE013 << 15, false},
|
||||
"VADDWEVHB": {0xE03C << 15, false}, "VADDWEVWH": {0xE03D << 15, false},
|
||||
"VADDWEVVW": {0xE03E << 15, false}, "VADDWEVQV": {0xE03F << 15, false},
|
||||
"VSUBWEVHB": {0xE040 << 15, false}, "VSUBWEVWH": {0xE041 << 15, false},
|
||||
"VSUBWEVVW": {0xE042 << 15, false}, "VSUBWEVQV": {0xE043 << 15, false},
|
||||
"VADDWODHB": {0xE044 << 15, false}, "VADDWODWH": {0xE045 << 15, false},
|
||||
"VADDWODVW": {0xE046 << 15, false}, "VADDWODQV": {0xE047 << 15, false},
|
||||
"VSUBWODHB": {0xE048 << 15, false}, "VSUBWODWH": {0xE049 << 15, false},
|
||||
"VSUBWODVW": {0xE04A << 15, false}, "VSUBWODQV": {0xE04B << 15, false},
|
||||
"VSUBWEVHBU": {0xE060 << 15, false}, "VSUBWEVWHU": {0xE061 << 15, false},
|
||||
"VSUBWEVVWU": {0xE062 << 15, false}, "VSUBWEVQVU": {0xE063 << 15, false},
|
||||
"VADDWEVHBU": {0xE05C << 15, false}, "VADDWEVWHU": {0xE05D << 15, false},
|
||||
"VADDWEVVWU": {0xE05E << 15, false}, "VADDWEVQVU": {0xE05F << 15, false},
|
||||
"VADDWODHBU": {0xE064 << 15, false}, "VADDWODWHU": {0xE065 << 15, false},
|
||||
"VADDWODVWU": {0xE066 << 15, false}, "VADDWODQVU": {0xE067 << 15, false},
|
||||
"VSUBWODHBU": {0xE068 << 15, false}, "VSUBWODWHU": {0xE069 << 15, false},
|
||||
"VSUBWODVWU": {0xE06A << 15, false}, "VSUBWODQVU": {0xE06B << 15, false},
|
||||
"VSHUFH": {0xE2F5 << 15, false}, "VSHUFW": {0xE2F6 << 15, false},
|
||||
"VSHUFV": {0xE2F7 << 15, false},
|
||||
"XVSLLB": {0xE9D0 << 15, true}, "XVSLLH": {0xE9D1 << 15, true},
|
||||
"XVSLLW": {0xE9D2 << 15, true}, "XVSLLV": {0xE9D3 << 15, true},
|
||||
"XVSRLB": {0xE9D4 << 15, true}, "XVSRLH": {0xE9D5 << 15, true},
|
||||
"XVSRLW": {0xE9D6 << 15, true}, "XVSRLV": {0xE9D7 << 15, true},
|
||||
"XVSRAB": {0xE9D8 << 15, true}, "XVSRAH": {0xE9D9 << 15, true},
|
||||
"XVSRAW": {0xE9DA << 15, true}, "XVSRAV": {0xE9DB << 15, true},
|
||||
"XVROTRB": {0xE9DC << 15, true}, "XVROTRH": {0xE9DD << 15, true},
|
||||
"XVROTRW": {0xE9DE << 15, true}, "XVROTRV": {0xE9DF << 15, true},
|
||||
"XVILVLB": {0xEA34 << 15, true}, "XVILVLH": {0xEA35 << 15, true},
|
||||
"XVILVLW": {0xEA36 << 15, true}, "XVILVLV": {0xEA37 << 15, true},
|
||||
"XVILVHB": {0xEA38 << 15, true}, "XVILVHH": {0xEA39 << 15, true},
|
||||
"XVILVHW": {0xEA3A << 15, true}, "XVILVHV": {0xEA3B << 15, true},
|
||||
"XVBITCLRB": {0xEA18 << 15, true}, "XVBITCLRH": {0xEA19 << 15, true},
|
||||
"XVBITCLRW": {0xEA1A << 15, true}, "XVBITCLRV": {0xEA1B << 15, true},
|
||||
"XVBITSETB": {0xEA1C << 15, true}, "XVBITSETH": {0xEA1D << 15, true},
|
||||
"XVBITSETW": {0xEA1E << 15, true}, "XVBITSETV": {0xEA1F << 15, true},
|
||||
"XVBITREVB": {0xEA20 << 15, true}, "XVBITREVH": {0xEA21 << 15, true},
|
||||
"XVBITREVW": {0xEA22 << 15, true}, "XVBITREVV": {0xEA23 << 15, true},
|
||||
"XVORV": {0xEA4D << 15, true}, "XVNORV": {0xEA4F << 15, true},
|
||||
"XVANDNV": {0xEA50 << 15, true}, "XVORNV": {0xEA51 << 15, true},
|
||||
"XVSEQH": {0xE801 << 15, true}, "XVSEQW": {0xE802 << 15, true},
|
||||
"XVSLTB": {0xE80C << 15, true}, "XVSLTH": {0xE80D << 15, true},
|
||||
"XVSLTW": {0xE80E << 15, true}, "XVSLTV": {0xE80F << 15, true},
|
||||
"XVSLTBU": {0xE810 << 15, true}, "XVSLTHU": {0xE811 << 15, true},
|
||||
"XVSLTWU": {0xE812 << 15, true}, "XVSLTVU": {0xE813 << 15, true},
|
||||
"XVADDWEVHB": {0xE83C << 15, true}, "XVADDWEVWH": {0xE83D << 15, true},
|
||||
"XVADDWEVVW": {0xE83E << 15, true}, "XVADDWEVQV": {0xE83F << 15, true},
|
||||
"XVSUBWEVHB": {0xE840 << 15, true}, "XVSUBWEVWH": {0xE841 << 15, true},
|
||||
"XVSUBWEVVW": {0xE842 << 15, true}, "XVSUBWEVQV": {0xE843 << 15, true},
|
||||
"XVADDWODHB": {0xE844 << 15, true}, "XVADDWODWH": {0xE845 << 15, true},
|
||||
"XVADDWODVW": {0xE846 << 15, true}, "XVADDWODQV": {0xE847 << 15, true},
|
||||
"XVSUBWODHB": {0xE848 << 15, true}, "XVSUBWODWH": {0xE849 << 15, true},
|
||||
"XVSUBWODVW": {0xE84A << 15, true}, "XVSUBWODQV": {0xE84B << 15, true},
|
||||
"XVADDWEVHBU": {0xE85C << 15, true}, "XVADDWEVWHU": {0xE85D << 15, true},
|
||||
"XVADDWEVVWU": {0xE85E << 15, true}, "XVADDWEVQVU": {0xE85F << 15, true},
|
||||
"XVSUBWEVHBU": {0xE860 << 15, true}, "XVSUBWEVWHU": {0xE861 << 15, true},
|
||||
"XVSUBWEVVWU": {0xE862 << 15, true}, "XVSUBWEVQVU": {0xE863 << 15, true},
|
||||
"XVADDWODHBU": {0xE864 << 15, true}, "XVADDWODWHU": {0xE865 << 15, true},
|
||||
"XVADDWODVWU": {0xE866 << 15, true}, "XVADDWODQVU": {0xE867 << 15, true},
|
||||
"XVSUBWODHBU": {0xE868 << 15, true}, "XVSUBWODWHU": {0xE869 << 15, true},
|
||||
"XVSUBWODVWU": {0xE86A << 15, true}, "XVSUBWODQVU": {0xE86B << 15, true},
|
||||
"XVSHUFH": {0xEAF5 << 15, true}, "XVSHUFW": {0xEAF6 << 15, true},
|
||||
"XVSHUFV": {0xEAF7 << 15, true},
|
||||
}
|
||||
for _, tab := range []map[string]l64Vec3Enc{addsub, muldiv, vecmisc} {
|
||||
for m, e := range tab {
|
||||
if _, dup := vec3[m]; dup {
|
||||
panic("loong64: duplicate vector mnemonic " + m)
|
||||
}
|
||||
vec3[m] = e
|
||||
}
|
||||
}
|
||||
for m, e := range vec3 {
|
||||
l64InstrTable[m] = l64Enc{format: l64Fvvv, op: e.op}
|
||||
l64VecBank[m] = e.lasx
|
||||
}
|
||||
|
||||
// Immediate forms: INSTR $imm, vj, vd (or INSTR $imm, vd). The immediate
|
||||
// range, bias and field mask are the ones the toolchain encodes: vandi.b
|
||||
// stores the raw 8-bit constant, vsrari.b stores imm+8 (lane-width
|
||||
// bias), the si5 compares store 5-bit two's-complement values and vseqi.d
|
||||
// a 7-bit field the toolchain range-checks down to si5.
|
||||
// The mnemonics that also have a register form (the shifts, the bit
|
||||
// clear/set/rev families, VSEQ and the logic immediates) keep their
|
||||
// three-register entry in l64InstrTable; the dispatcher picks the
|
||||
// immediate opcode from l64VecImmInfo by operand kind, so the immediate
|
||||
// entries must not overwrite the table.
|
||||
vecImm := map[string]l64VecImmEnc{
|
||||
"VANDB": {0xE7A0 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVANDB": {0xEFA0 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VORB": {0xE7A8 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVORB": {0xEFA8 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VXORB": {0xE7B0 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVXORB": {0xEFB0 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VNORB": {0xE7B8 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVNORB": {0xEFB8 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VSEQB": {0xE500 << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSEQB": {0xED00 << 15, true, -16, 15, 0, 0x1F},
|
||||
// vseqi.h/w accept the same si5 window as vseqi.b; vseqi.d carries a
|
||||
// 7-bit field, but the toolchain range-checks it down to si5 as well
|
||||
// (GOARCH=loong64 go tool asm rejects VSEQV $32 and VSEQV $-64).
|
||||
"VSEQH": {0xE501 << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSEQH": {0xED01 << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSEQW": {0xE502 << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSEQW": {0xED02 << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSEQV": {0xE503 << 15, false, -16, 15, 0, 0x7F},
|
||||
"XVSEQV": {0xED03 << 15, true, -16, 15, 0, 0x7F},
|
||||
// vslti compares against a signed (or, in the U spellings, unsigned)
|
||||
// si5/ui5 constant.
|
||||
"VSLTB": {0xE50C << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSLTB": {0xED0C << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSLTH": {0xE50D << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSLTH": {0xED0D << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSLTW": {0xE50E << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSLTW": {0xED0E << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSLTV": {0xE50F << 15, false, -16, 15, 0, 0x1F},
|
||||
"XVSLTV": {0xED0F << 15, true, -16, 15, 0, 0x1F},
|
||||
"VSLTBU": {0xE510 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSLTBU": {0xED10 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSLTHU": {0xE511 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSLTHU": {0xED11 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSLTWU": {0xE512 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSLTWU": {0xED12 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSLTVU": {0xE513 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSLTVU": {0xED13 << 15, true, 0, 31, 0, 0x1F},
|
||||
// vaddi/vsubi take ui5 constants for every width on this toolchain
|
||||
// (VADDVU $32 is rejected by the oracle although the field is ui8).
|
||||
"VADDBU": {0xE514 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVADDBU": {0xED14 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VADDHU": {0xE515 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVADDHU": {0xED15 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VADDWU": {0xE516 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVADDWU": {0xED16 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VADDVU": {0xE517 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVADDVU": {0xED17 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSUBBU": {0xE518 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSUBBU": {0xED18 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSUBHU": {0xE519 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSUBHU": {0xED19 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSUBWU": {0xE51A << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSUBWU": {0xED1A << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSUBVU": {0xE51B << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSUBVU": {0xED1B << 15, true, 0, 31, 0, 0x1F},
|
||||
// The shift/rotate immediates ride in a width-sized field whose upper
|
||||
// bits carry the lane-width code: vslli.b stores ui3 at [12:0] with
|
||||
// bits [14:13] inside the opcode, vslli.h ui4 under a 4 bit mask, and
|
||||
// the .w/.d spellings a raw ui5/ui6.
|
||||
"VSLLB": {0x732C2000, false, 0, 7, 0, 0x7},
|
||||
"XVSLLB": {0x772C2000, true, 0, 7, 0, 0x7},
|
||||
"VSLLH": {0x732C4000, false, 0, 15, 0, 0xF},
|
||||
"XVSLLH": {0x772C4000, true, 0, 15, 0, 0xF},
|
||||
"VSLLW": {0xE659 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSLLW": {0xEE59 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSLLV": {0xE65A << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVSLLV": {0xEE5A << 15, true, 0, 63, 0, 0x3F},
|
||||
"VSRLB": {0x73302000, false, 0, 7, 0, 0x7},
|
||||
"XVSRLB": {0x77302000, true, 0, 7, 0, 0x7},
|
||||
"VSRLH": {0x73304000, false, 0, 15, 0, 0xF},
|
||||
"XVSRLH": {0x77304000, true, 0, 15, 0, 0xF},
|
||||
"VSRLW": {0xE661 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSRLW": {0xEE61 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSRLV": {0xE662 << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVSRLV": {0xEE62 << 15, true, 0, 63, 0, 0x3F},
|
||||
// vsrari/vrotri bias the field so the lane-width code rides above the
|
||||
// shift amount (.b adds 8, .h 16, .w 32; .d is a raw ui6).
|
||||
"VSRAB": {0xE668 << 15, false, 0, 7, 8, 0x1F},
|
||||
"XVSRAB": {0xEE68 << 15, true, 0, 7, 8, 0x1F},
|
||||
"VSRAH": {0x73344000, false, 0, 15, 0, 0xF},
|
||||
"XVSRAH": {0x77344000, true, 0, 15, 0, 0xF},
|
||||
"VSRAW": {0xE669 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVSRAW": {0xEE69 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VSRAV": {0xE66A << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVSRAV": {0xEE6A << 15, true, 0, 63, 0, 0x3F},
|
||||
"VROTRB": {0x72A02000, false, 0, 7, 0, 0x7},
|
||||
"XVROTRB": {0x76A02000, true, 0, 7, 0, 0x7},
|
||||
"VROTRH": {0x72A04000, false, 0, 15, 0, 0xF},
|
||||
"XVROTRH": {0x76A04000, true, 0, 15, 0, 0xF},
|
||||
"VROTRW": {0xE541 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVROTRW": {0xED41 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VROTRV": {0xE542 << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVROTRV": {0xED42 << 15, true, 0, 63, 0, 0x3F},
|
||||
// vbitclri/vbitseti/vbitrevi follow the same width-coded layout.
|
||||
"VBITCLRB": {0x73102000, false, 0, 7, 0, 0x7},
|
||||
"XVBITCLRB": {0x77102000, true, 0, 7, 0, 0x7},
|
||||
"VBITCLRH": {0x73104000, false, 0, 15, 0, 0xF},
|
||||
"XVBITCLRH": {0x77104000, true, 0, 15, 0, 0xF},
|
||||
"VBITCLRW": {0xE621 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVBITCLRW": {0xEE21 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VBITCLRV": {0xE622 << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVBITCLRV": {0xEE22 << 15, true, 0, 63, 0, 0x3F},
|
||||
"VBITSETB": {0x73142000, false, 0, 7, 0, 0x7},
|
||||
"XVBITSETB": {0x77142000, true, 0, 7, 0, 0x7},
|
||||
"VBITSETH": {0x73144000, false, 0, 15, 0, 0xF},
|
||||
"XVBITSETH": {0x77144000, true, 0, 15, 0, 0xF},
|
||||
"VBITSETW": {0xE629 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVBITSETW": {0xEE29 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VBITSETV": {0xE62A << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVBITSETV": {0xEE2A << 15, true, 0, 63, 0, 0x3F},
|
||||
"VBITREVB": {0x73182000, false, 0, 7, 0, 0x7},
|
||||
"XVBITREVB": {0x77182000, true, 0, 7, 0, 0x7},
|
||||
"VBITREVH": {0x73184000, false, 0, 15, 0, 0xF},
|
||||
"XVBITREVH": {0x77184000, true, 0, 15, 0, 0xF},
|
||||
"VBITREVW": {0xE631 << 15, false, 0, 31, 0, 0x1F},
|
||||
"XVBITREVW": {0xEE31 << 15, true, 0, 31, 0, 0x1F},
|
||||
"VBITREVV": {0xE632 << 15, false, 0, 63, 0, 0x3F},
|
||||
"XVBITREVV": {0xEE32 << 15, true, 0, 63, 0, 0x3F},
|
||||
// The 4-bit-select shuffles and the byte-extract/insert permutations
|
||||
// take ui8 (the .d shuffle ui4 range-checked to 0..15 by the
|
||||
// toolchain) packing both position nibbles.
|
||||
"VSHUF4IB": {0xE720 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVSHUF4IB": {0xEF20 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VSHUF4IH": {0xE728 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVSHUF4IH": {0xEF28 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VSHUF4IW": {0xE730 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVSHUF4IW": {0xEF30 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VSHUF4IV": {0xE738 << 15, false, 0, 15, 0, 0xFF},
|
||||
"XVSHUF4IV": {0xEF38 << 15, true, 0, 15, 0, 0xFF},
|
||||
"VPERMIW": {0xE7C8 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVPERMIW": {0xEFC8 << 15, true, 0, 255, 0, 0xFF},
|
||||
"XVPERMIV": {0xEFD0 << 15, true, 0, 255, 0, 0xFF},
|
||||
"XVPERMIQ": {0xEFD8 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VEXTRINSB": {0xE718 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVEXTRINSB": {0xEF18 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VEXTRINSH": {0xE710 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVEXTRINSH": {0xEF10 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VEXTRINSW": {0xE708 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVEXTRINSW": {0xEF08 << 15, true, 0, 255, 0, 0xFF},
|
||||
"VEXTRINSV": {0xE700 << 15, false, 0, 255, 0, 0xFF},
|
||||
"XVEXTRINSV": {0xEF00 << 15, true, 0, 255, 0, 0xFF},
|
||||
}
|
||||
for m, e := range vecImm {
|
||||
l64VecImmInfo[m] = e
|
||||
l64VecBank[m] = e.lasx
|
||||
}
|
||||
|
||||
// Vector-to-condition flag: INSTR vj, FCCn (vsetnez.v, vsetanyeqz.*,
|
||||
// vsetallnez.*): the sub-op rides in the rk field.
|
||||
vecCf := map[string]uint32{
|
||||
"VSETNEV": 0xE539<<15 | 7<<10, "XVSETNEV": 0xED39<<15 | 7<<10,
|
||||
"VSETANYEQB": 0xE539<<15 | 8<<10, "XVSETANYEQB": 0xED39<<15 | 8<<10,
|
||||
"VSETANYEQV": 0xE539<<15 | 11<<10, "XVSETANYEQV": 0xED39<<15 | 11<<10,
|
||||
"VSETALLNEV": 0xE539<<15 | 15<<10, "XVSETALLNEV": 0xED39<<15 | 15<<10,
|
||||
"VSETEQV": 0xE539<<15 | 6<<10, "XVSETEQV": 0xED39<<15 | 6<<10,
|
||||
"VSETANYEQH": 0xE539<<15 | 9<<10, "XVSETANYEQH": 0xED39<<15 | 9<<10,
|
||||
"VSETANYEQW": 0xE539<<15 | 10<<10, "XVSETANYEQW": 0xED39<<15 | 10<<10,
|
||||
"VSETALLNEB": 0xE539<<15 | 12<<10, "XVSETALLNEB": 0xED39<<15 | 12<<10,
|
||||
"VSETALLNEH": 0xE539<<15 | 13<<10, "XVSETALLNEH": 0xED39<<15 | 13<<10,
|
||||
"VSETALLNEW": 0xE539<<15 | 14<<10, "XVSETALLNEW": 0xED39<<15 | 14<<10,
|
||||
}
|
||||
for m, op := range vecCf {
|
||||
l64InstrTable[m] = l64Enc{format: l64Fvcf, op: op}
|
||||
l64VecBank[m] = strings.HasPrefix(m, "XV")
|
||||
}
|
||||
|
||||
// Lane popcount and the two-operand vector FP/unary spellings: INSTR vj,
|
||||
// vd (the 2R layout with the opcode extending over the unused vk field;
|
||||
// the low byte of each constant is the instruction's own sub-op).
|
||||
vec2r := map[string]l64Vec3Enc{
|
||||
"VPCNTV": {0x1CA70B << 10, false}, "XVPCNTV": {0x1DA70B << 10, true},
|
||||
}
|
||||
// The rest of the lane popcounts, the vector negations and the vector FP
|
||||
// unary conversions (loong64enc1.s).
|
||||
vec2rMore := map[string]l64Vec3Enc{
|
||||
"VPCNTB": {0x1CA708 << 10, false}, "VPCNTH": {0x1CA709 << 10, false},
|
||||
"VPCNTW": {0x1CA70A << 10, false},
|
||||
"VNEGB": {0x1CA70C << 10, false}, "VNEGH": {0x1CA70D << 10, false},
|
||||
"VNEGW": {0x1CA70E << 10, false}, "VNEGV": {0x1CA70F << 10, false},
|
||||
"VFCLASSF": {0x1CA735 << 10, false}, "VFCLASSD": {0x1CA736 << 10, false},
|
||||
"VFSQRTF": {0x1CA739 << 10, false}, "VFSQRTD": {0x1CA73A << 10, false},
|
||||
"VFRECIPF": {0x1CA73D << 10, false}, "VFRECIPD": {0x1CA73E << 10, false},
|
||||
"VFRSQRTF": {0x1CA741 << 10, false}, "VFRSQRTD": {0x1CA742 << 10, false},
|
||||
"VFRINTF": {0x1CA74D << 10, false}, "VFRINTD": {0x1CA74E << 10, false},
|
||||
"VFRINTRMF": {0x1CA751 << 10, false}, "VFRINTRMD": {0x1CA752 << 10, false},
|
||||
"VFRINTRPF": {0x1CA755 << 10, false}, "VFRINTRPD": {0x1CA756 << 10, false},
|
||||
"VFRINTRZF": {0x1CA759 << 10, false}, "VFRINTRZD": {0x1CA75A << 10, false},
|
||||
"VFRINTRNEF": {0x1CA75D << 10, false}, "VFRINTRNED": {0x1CA75E << 10, false},
|
||||
"XVPCNTB": {0x1DA708 << 10, true}, "XVPCNTH": {0x1DA709 << 10, true},
|
||||
"XVPCNTW": {0x1DA70A << 10, true},
|
||||
"XVNEGB": {0x1DA70C << 10, true}, "XVNEGH": {0x1DA70D << 10, true},
|
||||
"XVNEGW": {0x1DA70E << 10, true}, "XVNEGV": {0x1DA70F << 10, true},
|
||||
"XVFCLASSF": {0x1DA735 << 10, true}, "XVFCLASSD": {0x1DA736 << 10, true},
|
||||
"XVFSQRTF": {0x1DA739 << 10, true}, "XVFSQRTD": {0x1DA73A << 10, true},
|
||||
"XVFRECIPF": {0x1DA73D << 10, true}, "XVFRECIPD": {0x1DA73E << 10, true},
|
||||
"XVFRSQRTF": {0x1DA741 << 10, true}, "XVFRSQRTD": {0x1DA742 << 10, true},
|
||||
"XVFRINTF": {0x1DA74D << 10, true}, "XVFRINTD": {0x1DA74E << 10, true},
|
||||
"XVFRINTRMF": {0x1DA751 << 10, true}, "XVFRINTRMD": {0x1DA752 << 10, true},
|
||||
"XVFRINTRPF": {0x1DA755 << 10, true}, "XVFRINTRPD": {0x1DA756 << 10, true},
|
||||
"XVFRINTRZF": {0x1DA759 << 10, true}, "XVFRINTRZD": {0x1DA75A << 10, true},
|
||||
"XVFRINTRNEF": {0x1DA75D << 10, true}, "XVFRINTRNED": {0x1DA75E << 10, true},
|
||||
}
|
||||
maps.Copy(vec2r, vec2rMore)
|
||||
for m, e := range vec2r {
|
||||
l64InstrTable[m] = l64Enc{format: l64Frr, op: e.op}
|
||||
l64VecBank[m] = e.lasx
|
||||
l64Vec2R[m] = true
|
||||
}
|
||||
|
||||
// The four-register byte shuffle: INSTR va, vk, vj, vd (the operand the
|
||||
// table reads in each field position, va at bits [19:15]).
|
||||
vec4r := map[string]l64Vec3Enc{
|
||||
"VSHUFB": {0x0D50 << 16, false}, "XVSHUFB": {0x0D60 << 16, true},
|
||||
}
|
||||
for m, e := range vec4r {
|
||||
l64InstrTable[m] = l64Enc{format: l64Fvvvv, op: e.op}
|
||||
l64VecBank[m] = e.lasx
|
||||
l64Vec4R[m] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
var l64FpMovTable = map[string]uint32{
|
||||
"MOVV.R.F": 0x452a << 10, // movgr2fr.d
|
||||
|
||||
+856
-2
@@ -8,8 +8,8 @@ import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
|
||||
@@ -263,6 +263,20 @@ func TestLOONG64_regNames(t *testing.T) {
|
||||
t.Errorf("loong64RegNum(%q) = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
// The X/V spellings name the LSX/LASX vector banks, a register class of
|
||||
// their own: the oracle (GOARCH=loong64 go tool asm) rejects `BEQZ X0`
|
||||
// with "unrecognized instruction" while assembling `VADDV V0, V1, V2`
|
||||
// and `XVADDV X0, X1, X2`, so loong64RegNum stays strict and the vector
|
||||
// operands resolve through loong64VecRegNum only.
|
||||
vecCases := map[string]int{
|
||||
"V0": 0, "V31": 31, "X0": 0, "X31": 31,
|
||||
"R4": -1, "F0": -1, "FCC0": -1, "V32": -1, "X32": -1, "V": -1, "X": -1,
|
||||
}
|
||||
for name, want := range vecCases {
|
||||
if got := loong64VecRegNum(name); got != want {
|
||||
t.Errorf("loong64VecRegNum(%q) = %d, want %d", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOONG64_bytesEqualGroundTruth(t *testing.T) {
|
||||
@@ -291,3 +305,843 @@ done:
|
||||
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, // st.d r1, -8(r3) (prologue saves RA below the new SP)
|
||||
0x02FFE063, // addi.d r3, r3, -8 (prologue opens the frame)
|
||||
0x29C00061, // st.d r1, 0(r3) (prologue saves RA at SP)
|
||||
0x4C0000A1, // jirl r1, r5, 0
|
||||
0x28C00061, // ld.d r1, 0(r3) (epilogue restores RA)
|
||||
0x02C02063, // addi.d r3, r3, 8
|
||||
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_vector pins the LSX/LASX slice against words read off
|
||||
// GOARCH=loong64 go tool asm (cross-checked against the toolchain's own
|
||||
// loong64enc1.s): the three-register forms, the immediate forms with their
|
||||
// biases, the vector-to-condition forms, lane popcount, the FP conversion,
|
||||
// FSEL and the VMOVQ move family.
|
||||
func TestLOONG64_vector(t *testing.T) {
|
||||
t.Run("three-register and immediate forms", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VADDV V1, V2, V3
|
||||
VADDW V1, V2, V3
|
||||
VADDV V2, V1
|
||||
VANDV V1, V2
|
||||
VXORV V1, V2, V3
|
||||
VSEQB V1, V2, V3
|
||||
VSEQV V1, V2, V3
|
||||
VSRAB V1, V2, V3
|
||||
VROTRW V1, V2, V3
|
||||
VANDB $0, V2, V3
|
||||
VANDB $255, V2
|
||||
VSEQB $3, V2, V3
|
||||
VSEQV $15, V2, V3
|
||||
VSEQV $-15, V2, V3
|
||||
VSRAB $7, V1, V2
|
||||
VROTRW $16, V1, V2
|
||||
VPCNTV V1, V2
|
||||
XVADDV X1, X2, X3
|
||||
XVXORV X1, X2, X3
|
||||
XVSEQB X1, X2, X3
|
||||
XVPCNTV X1, X2
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x700B8443, // vadd.v v3, v2, v1
|
||||
0x700B0443, // vadd.w
|
||||
0x700B8821, // vadd.v v1, v1, v2 (two-operand form)
|
||||
0x71260442, // vand.v v2, v2, v1
|
||||
0x71270443, // vxor.v
|
||||
0x70000443, // vseq.b
|
||||
0x70018443, // vseq.d
|
||||
0x70EC0443, // vsra.b
|
||||
0x70EF0443, // vrotr.w
|
||||
0x73D00043, // vandi.b v3, v2, 0
|
||||
0x73D3FC42, // vandi.b v2, v2, 255 (two-operand form)
|
||||
0x72800C43, // vseqi.b v3, v2, 3
|
||||
0x7281BC43, // vseqi.d v3, v2, 15
|
||||
0x7281C443, // vseqi.d v3, v2, -15 (7-bit two's complement)
|
||||
0x73343C22, // vsrai.b v2, v1, 7 (encoded as 7+8)
|
||||
0x72A0C022, // vrotri.w v2, v1, 16
|
||||
0x729C2C22, // vpcnt.d v2, v1
|
||||
0x740B8443, // xvadd.d x3, x2, x1
|
||||
0x75270443, // xvxor.d
|
||||
0x74000443, // xvseq.b
|
||||
0x769C2C22, // xvpcnt.d x2, x1
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("vector-to-condition", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VSETNEV V1, FCC0
|
||||
VSETANYEQB V1, FCC0
|
||||
VSETANYEQV V2, FCC0
|
||||
VSETALLNEV V0, FCC0
|
||||
XVSETNEV X1, FCC0
|
||||
XVSETALLNEV X1, FCC0
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x729C9C20, // vsetnez.d fcc0, v1
|
||||
0x729CA020, // vsetanyeqz.b
|
||||
0x729CAC40, // vsetanyeqz.d
|
||||
0x729CBC00, // vsetallnez.d
|
||||
0x769C9C20, // xvsetnez.d
|
||||
0x769CBC20, // xvsetallnez.d
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("FP convert and FSEL", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
FFINTDV F0, F1
|
||||
FSEL FCC0, F3, F4, F3
|
||||
FSEL FCC1, F1, F2
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x011D2801, // ffint.d.v f1, f0
|
||||
0x0D000C83, // fsel f3, f4, f3, fcc0
|
||||
0x0D008442, // fsel f2, f2, f1, fcc1
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("VMOVQ move family", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VMOVQ V1, V9
|
||||
VMOVQ (R4), V2
|
||||
VMOVQ 16(R4), V2
|
||||
VMOVQ V0, (R4)
|
||||
VMOVQ V0, 32(R4)
|
||||
VMOVQ (R4)(R7), V3
|
||||
VMOVQ V3, (R4)(R7)
|
||||
VMOVQ R6, V0.B16
|
||||
VMOVQ R6, V12.W4
|
||||
VMOVQ (R4), V4.W4
|
||||
XVMOVQ X3, X7
|
||||
XVMOVQ (R4), X2
|
||||
XVMOVQ X0, (R4)
|
||||
XVMOVQ (R4)(R7), X4
|
||||
XVMOVQ X0, (R4)(R7)
|
||||
XVMOVQ R6, X0.B32
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x732D0029, // vori.b v9, v1, 0 (register move)
|
||||
0x2C000082, // vld v2, r4, 0
|
||||
0x2C004082, // vld v2, r4, 16
|
||||
0x2C400080, // vst v0, r4, 0
|
||||
0x2C408080, // vst v0, r4, 32
|
||||
0x38401C83, // vldx v3, r4, r7
|
||||
0x38441C83, // vstx v3, r4, r7
|
||||
0x729F00C0, // vreplgr2vr.b v0, r6
|
||||
0x729F08CC, // vreplgr2vr.w v12, r6
|
||||
0x30200084, // vldrepl.w v4, r4, 0
|
||||
0x772D0067, // xvori.b x7, x3, 0
|
||||
0x2C800082, // xvld x2, r4, 0
|
||||
0x2CC00080, // xvst x0, r4, 0
|
||||
0x38481C84, // xvldx x4, r4, r7
|
||||
0x384C1C80, // xvstx x0, r4, r7
|
||||
0x769F00C0, // xvreplgr2vr.b x0, r6
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("element extract and insert", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VMOVQ V0.V[0], R10
|
||||
VMOVQ V6.V[1], R8
|
||||
VMOVQ R9, V1.V[0]
|
||||
XVMOVQ X0.V[0], R10
|
||||
XVMOVQ X5.W[7], R7
|
||||
XVMOVQ R4, X7.V[3]
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x72EFF00A, // vpickve2gr.d r10, v0, 0
|
||||
0x72EFF4C8, // vpickve2gr.d r8, v6, 1
|
||||
0x72EBF121, // vinsgr2vr.d v1, r9, 0
|
||||
0x76EFE00A, // xvpickve2gr.d r10, x0, 0
|
||||
0x76EFDCA7, // xvpickve2gr.w r7, x5, 7
|
||||
0x76EBEC87, // xvinsgr2vr.d x7, r4, 3
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
// The integer and FP add/subtract families with their saturating pairs
|
||||
// and immediate spellings (loong64enc1.s words).
|
||||
t.Run("add and subtract families", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VADDB V1, V2, V3
|
||||
VADDF V1, V2, V3
|
||||
VADDD V1, V2, V3
|
||||
VSUBD V1, V2, V3
|
||||
VSADDV V1, V2, V3
|
||||
VSSUBVU V1, V2, V3
|
||||
VADDBU $1, V2, V1
|
||||
VADDBU $1, V2
|
||||
VSUBVU $31, V2
|
||||
XVSADDV X3, X2, X1
|
||||
XVSUBD X1, X2, X3
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x700A0443, // vadd.b
|
||||
0x71308443, // vadd.f
|
||||
0x71310443, // vadd.d
|
||||
0x71330443, // vsub.d
|
||||
0x70478443, // vsadd.v
|
||||
0x704D8443, // vssub.u.d
|
||||
0x728A0441, // vaddi.bu v1, v2, 1
|
||||
0x728A0442, // vaddi.bu v2, v2, 1 (two-operand form)
|
||||
0x728DFC42, // vsubi.du v2, v2, 31 (two-operand form)
|
||||
0x74478C41, // xvsadd.d x1, x2, x3
|
||||
0x75330443, // xvsub.d x3, x2, x1
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
// The multiply, divide and accumulate families.
|
||||
t.Run("multiply and divide families", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VMULV V1, V2, V3
|
||||
VMUHHU V1, V2, V3
|
||||
VDIVBU V1, V2, V3
|
||||
VMODV V1, V2, V3
|
||||
VMADDB V1, V2, V3
|
||||
VMSUBV V1, V2, V3
|
||||
VMULWEVHB V1, V2, V3
|
||||
VMULWODQV V1, V2, V3
|
||||
VMADDWEVHBUB V1, V2, V3
|
||||
XVDIVD X1, X2, X3
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x70858443, // vmul.v
|
||||
0x70888443, // vmuh.u.d
|
||||
0x70E40443, // vdiv.u.b
|
||||
0x70E38443, // vmod.d
|
||||
0x70A80443, // vmadd.b
|
||||
0x70AB8443, // vmsub.d
|
||||
0x70900443, // vmulwev.h.b
|
||||
0x70938443, // vmulwod.q.d
|
||||
0x70BC0443, // vmaddwev.h.bu.b
|
||||
0x753B0443, // xvdiv.d
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
// The shift, bit and interleave families in register and immediate
|
||||
// spellings, with the width-coded shift immediates.
|
||||
t.Run("shift, bit and interleave families", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VSLLV V1, V2, V3
|
||||
VROTRB V1, V2, V3
|
||||
VBITCLRV V1, V2, V3
|
||||
VBITSETW V1, V2, V3
|
||||
VBITREVV V1, V2, V3
|
||||
VILVLB V1, V2, V3
|
||||
VILVHV V1, V2, V3
|
||||
VSLLB $7, V1, V2
|
||||
VSLLB $5, V1
|
||||
VSRLH $15, V1, V2
|
||||
VSRAW $31, V1, V2
|
||||
VSRAV $63, V1, V2
|
||||
VROTRV $63, V1, V2
|
||||
VBITCLRB $7, V2, V3
|
||||
VBITREVV $63, V2, V3
|
||||
VSEQH $-16, V2, V3
|
||||
VSLTB $1, V2, V3
|
||||
VSLTHU $31, V2, V3
|
||||
XVILVLV X3, X2, X1
|
||||
XVSLLB $7, X2, X1
|
||||
XVSRAV $63, X2, X1
|
||||
XVBITREVV $63, X2, X1
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x70E98443, // vsll.d
|
||||
0x70EE0443, // vrotr.b
|
||||
0x710D8443, // vbitclr.d
|
||||
0x710F0443, // vbitset.w
|
||||
0x71118443, // vbitrev.d
|
||||
0x711A0443, // vilvl.b
|
||||
0x711D8443, // vilvh.d
|
||||
0x732C3C22, // vslli.b v2, v1, 7
|
||||
0x732C3421, // vslli.b v1, v1, 5 (two-operand form)
|
||||
0x73307C22, // vsrli.h v2, v1, 15
|
||||
0x7334FC22, // vsrai.w v2, v1, 31
|
||||
0x7335FC22, // vsrai.d v2, v1, 63
|
||||
0x72A1FC22, // vrotri.d v2, v1, 63
|
||||
0x73103C43, // vbitclri.b v3, v2, 7
|
||||
0x7319FC43, // vbitrevi.d v3, v2, 63
|
||||
0x7280C043, // vseqi.h v3, v2, -16
|
||||
0x72860443, // vslti.b v3, v2, 1
|
||||
0x7288FC43, // vslti.hu v3, v2, 31
|
||||
0x751B8C41, // xvilvl.d x1, x2, x3
|
||||
0x772C3C41, // xvslli.b x1, x2, 7
|
||||
0x7735FC41, // xvsrai.d x1, x2, 63
|
||||
0x7719FC41, // xvbitrevi.d x1, x2, 63
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
// The shuffle, select and permutation families, including the
|
||||
// four-register byte shuffle.
|
||||
t.Run("shuffle and permutation families", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VSHUFH V1, V2, V3
|
||||
VSHUFW V1, V2, V3
|
||||
VSHUFV V1, V2, V3
|
||||
VSHUFB V1, V2, V3, V4
|
||||
XVSHUFB X1, X2, X3, X4
|
||||
VSHUF4IB $255, V2, V1
|
||||
VSHUF4IV $15, V2, V1
|
||||
XVSHUF4IV $15, X1, X2
|
||||
VEXTRINSB $0x18, V1, V2
|
||||
XVEXTRINSV $0x81, X1, X2
|
||||
VPERMIW $0x1B, V1, V2
|
||||
XVPERMIQ $0x4B, X1, X2
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x717A8443, // vshuf.h
|
||||
0x717B0443, // vshuf.w
|
||||
0x717B8443, // vshuf.d
|
||||
0x0D508864, // vshuf.b v4, v3, v2, v1
|
||||
0x0D608864, // xvshuf.b
|
||||
0x7393FC41, // vshuf4i.b v1, v2, 255
|
||||
0x739C3C41, // vshuf4i.d v1, v2, 15
|
||||
0x779C3C22, // xvshuf4i.d x2, x1, 15
|
||||
0x738C6022, // vextrins.b v2, v1, 0x18
|
||||
0x77820422, // xvextrins.d x2, x1, 0x81
|
||||
0x73E46C22, // vpermi.w v2, v1, 0x1b
|
||||
0x77ED2C22, // xvpermi.q x2, x1, 0x4b
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
|
||||
// The vector FP families, the unary spellings, the compare-to-flag
|
||||
// additions and the scalar int/float conversions.
|
||||
t.Run("FP and conversion families", func(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·v(SB), NOSPLIT, $0
|
||||
VADDF V1, V2, V3
|
||||
VMULF V1, V2, V3
|
||||
VFCLASSD V1, V2
|
||||
VFSQRTF V1, V2
|
||||
VFRECIPD V1, V2
|
||||
VFRSQRTF V1, V2
|
||||
VFRINTF V1, V2
|
||||
VFRINTRNED V1, V2
|
||||
VNEGB V1, V2
|
||||
VPCNTB V1, V2
|
||||
XVNEGV X2, X1
|
||||
XVPCNTW X3, X2
|
||||
XVFRINTRNEF X1, X2
|
||||
VSETEQV V1, FCC0
|
||||
VSETANYEQH V1, FCC0
|
||||
VSETALLNEB V1, FCC0
|
||||
XVSETALLNEW X1, FCC0
|
||||
FFINTFW F0, F1
|
||||
FTINTVD F0, F1
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x71308443, // vfadd.s
|
||||
0x71388443, // vfmul.s
|
||||
0x729CD822, // vfclass.d
|
||||
0x729CE422, // vfsqrt.s
|
||||
0x729CF822, // vfrecip.d
|
||||
0x729D0422, // vfrsqrt.s
|
||||
0x729D3422, // vfrint.s
|
||||
0x729D7822, // vfrintne.s
|
||||
0x729C3022, // vneg.b
|
||||
0x729C2022, // vpcnt.b
|
||||
0x769C3C41, // xvneg.d x1, x2
|
||||
0x769C2862, // xvpcnt.w x2, x3
|
||||
0x769D7422, // xvfrintne.s x2, x1
|
||||
0x729C9820, // vseteqz.d fcc0, v1
|
||||
0x729CA420, // vsetanyeqz.h
|
||||
0x729CB020, // vsetallnez.b
|
||||
0x769CB820, // xvsetallnez.w
|
||||
0x011D1001, // ffint.s.w f1, f0
|
||||
0x011B2801, // ftint.l.d f1, f0
|
||||
0x4C000020,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLOONG64_vectorErrors pins the register-class and range diagnostics of
|
||||
// the vector slice; each shape is rejected by the oracle as well
|
||||
// (GOARCH=loong64 go tool asm).
|
||||
func TestLOONG64_vectorErrors(t *testing.T) {
|
||||
cases := []string{
|
||||
// Integer registers in vector positions.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VADDV R4, R5, R6
|
||||
RET
|
||||
`,
|
||||
// Crossed banks: LSX spellings take V, LASX spellings X.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VADDV X1, X2, X3
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVADDV V1, V2, V3
|
||||
RET
|
||||
`,
|
||||
// The LASX bank has no .b/.h element forms.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ R4, X2.B[0]
|
||||
RET
|
||||
`,
|
||||
// Immediate ranges.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VANDB $256, V2
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSEQB $16, V2, V3
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VROTRW $32, V1, V2
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VADDVU $32, V2
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSEQV $32, V2, V3
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSHUF4IV $16, V2, V1
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VEXTRINSB $256, V1, V2
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSLTV $-17, V2, V3
|
||||
RET
|
||||
`,
|
||||
// VSHUFB wants four vector registers.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSHUFB V1, V2, V3
|
||||
RET
|
||||
`,
|
||||
// The FCC forms still refuse vector registers.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSETEQV V1, V2
|
||||
RET
|
||||
`,
|
||||
// VSET* wants an FCC flag, not a vector register.
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VSETNEV V1, V2
|
||||
RET
|
||||
`,
|
||||
}
|
||||
for i, src := range cases {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_dbarAtomics pins the _dbar (acquire/release) AMO variants.
|
||||
// The oracle words come from GOARCH=loong64 go tool objdump of kernels
|
||||
// assembled with go tool asm, and match the toolchain's loong64enc1.s.
|
||||
func TestLOONG64_dbarAtomics(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·atoms(SB), NOSPLIT, $0
|
||||
AMADDDBW R14, (R13), R12
|
||||
AMADDDBV R14, (R13), R12
|
||||
AMANDDBW R5, (R4), R6
|
||||
AMANDDBV R5, (R4), R6
|
||||
AMORDBW R5, (R4), R0
|
||||
AMORDBV R5, (R4), R6
|
||||
AMSWAPDBW R5, (R4), R6
|
||||
AMCASDBV R6, (R4), R5
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x386A39AC, // amadd_db.w r12, r13, r14
|
||||
0x386AB9AC, // amadd_db.d
|
||||
0x386B1486, // amand_db.w r6, r4, r5
|
||||
0x386B9486, // amand_db.d
|
||||
0x386C1480, // amor_db.w r0, r4, r5
|
||||
0x386C9486, // amor_db.d
|
||||
0x38691486, // amswap_db.w
|
||||
0x385B9885, // amcas_db.w
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_llacqScrel pins the acquire/release LL/SC pair. The oracle
|
||||
// words come from GOARCH=loong64 go tool objdump and the toolchain's own
|
||||
// loong64enc1.s golden bytes.
|
||||
func TestLOONG64_llacqScrel(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·llsc(SB), NOSPLIT, $0
|
||||
LLACQW (R5), R4
|
||||
LLACQV (R5), R4
|
||||
SCRELW R4, (R6)
|
||||
SCRELV R4, (R6)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x385780A4, // ll.acq.w r4, r5
|
||||
0x385788A4, // ll.acq.d r4, r5
|
||||
0x385784C4, // sc.rel.w r4, r6
|
||||
0x38578CC4, // sc.rel.d r4, r6
|
||||
0x4C000020,
|
||||
)
|
||||
// The toolchain accepts the zero-offset memory form alone.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
LLACQW 4(R5), R4
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
SCRELV R4, 8(R6)
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_vmovqSuffixed pins the element-broadcast and element-move
|
||||
// VMOVQ/XVMOVQ forms, with the oracle words lifted verbatim from the
|
||||
// toolchain's loong64enc1.s.
|
||||
func TestLOONG64_vmovqSuffixed(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·vmovq(SB), NOSPLIT, $0
|
||||
VMOVQ V1.B[3], V9.B16
|
||||
VMOVQ V2.H[2], V8.H8
|
||||
VMOVQ V3.W[1], V7.W4
|
||||
VMOVQ V4.V[0], V6.V2
|
||||
XVMOVQ X0, X31.B32
|
||||
XVMOVQ X1, X30.H16
|
||||
XVMOVQ X2, X29.W8
|
||||
XVMOVQ X3, X28.V4
|
||||
XVMOVQ X3, X27.Q2
|
||||
XVMOVQ X0, X31.W[7]
|
||||
XVMOVQ X1, X29.W[0]
|
||||
XVMOVQ X3, X28.V[3]
|
||||
XVMOVQ X4, X27.V[0]
|
||||
XVMOVQ X31.W[7], X0
|
||||
XVMOVQ X29.W[0], X1
|
||||
XVMOVQ X28.V[3], X8
|
||||
XVMOVQ X27.V[0], X9
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x72F78C29, // vreplvei.b v9, v1, 3
|
||||
0x72F7C848, // vreplvei.h v8, v2, 2
|
||||
0x72F7E467, // vreplvei.w v7, v3, 1
|
||||
0x72F7F086, // vreplvei.d v6, v4, 0
|
||||
0x7707001F, // xvreplve0.b x31, x0
|
||||
0x7707803E, // xvreplve0.h x30, x1
|
||||
0x7707C05D, // xvreplve0.w x29, x2
|
||||
0x7707E07C, // xvreplve0.d x28, x3
|
||||
0x7707F07B, // xvreplve0.q x27, x3
|
||||
0x76FFDC1F, // xvinsve0.w x31, x0, 7
|
||||
0x76FFC03D, // xvinsve0.w x29, x1, 0
|
||||
0x76FFEC7C, // xvinsve0.d x28, x3, 3
|
||||
0x76FFE09B, // xvinsve0.d x27, x4, 0
|
||||
0x7703DFE0, // xvpickve.w x0, x31, 7
|
||||
0x7703C3A1, // xvpickve.w x1, x29, 0
|
||||
0x7703EF88, // xvpickve.d x8, x28, 3
|
||||
0x7703E369, // xvpickve.d x9, x27, 0
|
||||
0x4C000020,
|
||||
)
|
||||
// The rejected shapes: a width mismatch between the element and the
|
||||
// arrangement, an element index past the lane count, a wrong-bank
|
||||
// vreplvei and an arrangement the LASX bank does not spell.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ V1.H[3], V9.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ V1.B[16], V9.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X1.B[3], X9.B32
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X0, X31.B16
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
XVMOVQ X0, X31.W[8]
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_parityFixes pins the operand forms whose encodings were found
|
||||
// diverging from the toolchain by the loong64enc1.s differential: the
|
||||
// $off(reg) address immediate (addi.d), the SCQ operand order, the scaled
|
||||
// vldrepl offsets (with their field masks), the XVSEQB/XVSEQV immediate
|
||||
// opcodes and the zero-register base the toolchain gives FP-relative
|
||||
// VMOVQ/XVMOVQ memory operands. Golden words from loong64enc1.s.
|
||||
func TestLOONG64_parityFixes(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·parity(SB), NOSPLIT, $0-32
|
||||
MOVW $4(R4), R5
|
||||
MOVV $4(R4), R5
|
||||
MOVW $65536(R4), R5
|
||||
MOVW $-4096(R4), R5
|
||||
SCQ R4, R5, (R6)
|
||||
VMOVQ 2(R4), V1.H8
|
||||
VMOVQ -6(R4), V1.H8
|
||||
VMOVQ -12(R4), V2.W4
|
||||
VMOVQ -16(R4), V3.V2
|
||||
XVMOVQ -10(R4), X1.H16
|
||||
XVSEQB $0, X2, X4
|
||||
XVSEQH $3, X2, X4
|
||||
XVSEQW $12, X2, X4
|
||||
XVSEQV $15, X2, X4
|
||||
XVSEQV $-15, X2, X4
|
||||
VMOVQ V2, y+16(FP)
|
||||
VMOVQ y+16(FP), V2
|
||||
VMOVQ V2, x+2030(FP)
|
||||
XVMOVQ X6, y+16(FP)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x02C01085, // addi.d $4, r4, r5
|
||||
0x02C01085, // addi.d $4, r4, r5 (MOVW keeps the 64-bit addi.d)
|
||||
0x1400021E, // lu12i.w $16, r30
|
||||
0x038003DE, // ori $0, r30, r30
|
||||
0x0010F885, // add.d r5, r4, r30
|
||||
0x15FFFFFE, // lu12i.w $-1, r30
|
||||
0x038003DE, // ori $0, r30, r30
|
||||
0x0010F885, // add.d r5, r4, r30
|
||||
0x385714C4, // sc.q r4, r5, (r6): middle<<10 | base<<5 | first
|
||||
0x30400481, // vldrepl.h v1, 2(r4)
|
||||
0x305FF481, // vldrepl.h v1, -6(r4)
|
||||
0x302FF482, // vldrepl.w v2, -12(r4)
|
||||
0x3017F883, // vldrepl.d v3, -16(r4)
|
||||
0x325FEC81, // xvldrepl.h x1, -10(r4)
|
||||
0x76800044, // xvseqi.b x4, x2, 0
|
||||
0x76808C44, // xvseqi.h x4, x2, 3
|
||||
0x76813044, // xvseqi.w x4, x2, 12
|
||||
0x7681BC44, // xvseqi.d x4, x2, 15
|
||||
0x7681C444, // xvseqi.d x4, x2, -15
|
||||
0x2C406002, // vst v2, 24(r0): FP-relative keeps the zero base
|
||||
0x2C006002, // vld v2, 24(r0)
|
||||
0x2C5FD802, // vst v2, 2038(r0)
|
||||
0x2CC06006, // xvst x6, 24(r0)
|
||||
0x4C000020,
|
||||
)
|
||||
// Misaligned vldrepl offsets are rejected, as the toolchain does.
|
||||
for i, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
VMOVQ 3(R4), V1.H8
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
MOVW $4(R4), F1
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_bytePseudo pins the BYTE literal-data pseudo-op, which the
|
||||
// loong64 toolchain does not spell but the arm64 and riscv64 encoders of
|
||||
// this package already accept for byte-exact data layout (a superset
|
||||
// spelling, shippable via the goobj path).
|
||||
func TestLOONG64_bytePseudo(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·bytes(SB), NOSPLIT, $0
|
||||
BYTE $2
|
||||
BYTE $1; BYTE $0
|
||||
BYTE $255
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
// Four literal bytes, then RET (jirl r0, r1, 0); the trailing bytes pad
|
||||
// the final word the way any sub-word tail does.
|
||||
want := []byte{2, 1, 0, 0xFF, 0x20, 0x00, 0x00, 0x4C}
|
||||
if !bytes.Equal(code[:len(want)], want) {
|
||||
t.Errorf("bytes = % x, want % x", code, want)
|
||||
}
|
||||
for _, src := range []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BYTE $256
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BYTE $-1
|
||||
RET
|
||||
`,
|
||||
} {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%q: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64PairSugar pins the toolchain's register-pair spellings: a
|
||||
// top-level colon between two registers splits the operand in two with the
|
||||
// halves swapped, so INSTR R4:R5, R6 encodes exactly as INSTR R5, R4, R6.
|
||||
// Each row is pinned two ways: against the word GOARCH=loong64 go tool asm
|
||||
// emits for the pair spelling, and against gasm's own encoding of the
|
||||
// direct spelling, which must be the same bytes.
|
||||
func TestLOONG64PairSugar(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
pair, direct string
|
||||
want uint32 // the word go tool asm emits for the pair
|
||||
}{
|
||||
{"MULV R4:R5, R6", "MULV R5, R4, R6", 0x001D9486},
|
||||
{"MULV R4:R5", "MULV R5, R4", 0x001D9484},
|
||||
{"MULV R6, R4:R5", "MULV R6, R5, R4", 0x001D98A4},
|
||||
{"DIVV R4:R5, R6", "DIVV R5, R4, R6", 0x00221486},
|
||||
{"MULHV R4:R5, R6", "MULHV R5, R4, R6", 0x001E1486},
|
||||
{"ADDV R4:R5, R6", "ADDV R5, R4, R6", 0x00109486},
|
||||
{"AND R4:R5, R6", "AND R5, R4, R6", 0x00149486},
|
||||
{"MOVV R4:R5", "MOVV R5, R4", 0x001500A4},
|
||||
{"MULD F4:F5, F6", "MULD F5, F4, F6", 0x01051486},
|
||||
{"VADDW V1:V2, V3", "VADDW V2, V1, V3", 0x700B0823},
|
||||
} {
|
||||
t.Run(tt.pair, func(t *testing.T) {
|
||||
pair := assembleLOONG64Helper(t, firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·p(SB), NOSPLIT, $0
|
||||
`+tt.pair+`
|
||||
RET
|
||||
`))
|
||||
direct := assembleLOONG64Helper(t, firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·d(SB), NOSPLIT, $0
|
||||
`+tt.direct+`
|
||||
RET
|
||||
`))
|
||||
if len(pair) < 4 {
|
||||
t.Fatalf("pair spelling produced %d bytes", len(pair))
|
||||
}
|
||||
got := binary.LittleEndian.Uint32(pair)
|
||||
if got != tt.want {
|
||||
t.Errorf("pair word = %08x, want %08x (go tool asm)", got, tt.want)
|
||||
}
|
||||
if len(direct) < 4 || !bytes.Equal(pair[:4], direct[:4]) {
|
||||
t.Errorf("pair bytes % x differ from the direct spelling's % x", pair[:4], direct[:4])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_addv16 pins the ADDV16 immediate family, the toolchain's
|
||||
// assembler name for the addu16i.d hardware instruction (loong64enc1.s
|
||||
// spells it ADDV16; the spelling ADDU16I.D is the decoder's name for the
|
||||
// encoding and no assembler input at all). The immediate must be a
|
||||
// multiple of 65536 and rides the instruction field shifted right by 16;
|
||||
// the oracle words are the toolchain's own loong64enc1.s rows, each also
|
||||
// confirmed against GOARCH=loong64 go tool asm.
|
||||
func TestLOONG64_addv16(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·a16(SB), NOSPLIT, $0
|
||||
ADDV16 $(-32768<<16), R4, R5
|
||||
ADDV16 $(0<<16), R4, R5
|
||||
ADDV16 $(8<<16), R4, R5
|
||||
ADDV16 $(32767<<16), R4, R5
|
||||
ADDV16 $(16<<16), R4
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x12000085, // addu16i.d r5, r4, -32768
|
||||
0x10000085, // addu16i.d r5, r4, 0
|
||||
0x10002085, // addu16i.d r5, r4, 8
|
||||
0x11FFFC85, // addu16i.d r5, r4, 32767
|
||||
0x10004084, // addu16i.d r4, r4, 16 (destination-only form)
|
||||
0x4C000020,
|
||||
)
|
||||
// The non-multiples the loong64error.s catalogue pins (its rows 3 and
|
||||
// 4): $1 and $65535 both refuse.
|
||||
for _, src := range []string{
|
||||
"ADDV16 $1, R4, R5",
|
||||
"ADDV16 $65535, R4, R5",
|
||||
"ADDV16 $(16<<16), F4",
|
||||
} {
|
||||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%q: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// loong64AcceptedErrorShapes lists the toolchain's loong64error.s spellings
|
||||
// gasm still accepts, each an acceptance superset with a known shape. The
|
||||
// list only shrinks: every tightening of the encoder moves spellings out of
|
||||
// it, and a spelling reappearing here means a regression.
|
||||
var loong64AcceptedErrorShapes = []string{}
|
||||
|
||||
// TestLoong64ToolchainErrorParity walks the toolchain's loong64error.s (Go
|
||||
// 1.27, loong64) and requires gasm to reject every case the toolchain rejects
|
||||
// with an equivalent diagnostic, the documented acceptance supersets above
|
||||
// excepted. A live Go toolchain is needed for the source file; the test
|
||||
// skips without one or in -short.
|
||||
func TestLoong64ToolchainErrorParity(t *testing.T) {
|
||||
goroot := loong64Goroot(t)
|
||||
path := filepath.Join(goroot, "src", "cmd", "asm", "internal", "asm", "testdata", "loong64error.s")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, s := range loong64AcceptedErrorShapes {
|
||||
allowed[s] = true
|
||||
}
|
||||
for raw := range strings.SplitSeq(string(data), "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "TEXT") || !strings.Contains(line, "ERROR") {
|
||||
continue
|
||||
}
|
||||
body := line
|
||||
if i := strings.Index(body, "//"); i >= 0 {
|
||||
body = strings.TrimSpace(body[:i])
|
||||
}
|
||||
want := ""
|
||||
if m := regexp.MustCompile(`ERROR "([^"]*)"`).FindStringSubmatch(line); m != nil {
|
||||
want = m[1]
|
||||
}
|
||||
body = strings.ReplaceAll(body, "\t", " ")
|
||||
body = strings.Join(strings.Fields(body), " ")
|
||||
// Label-relative and non-operand lines need their function context.
|
||||
if strings.Contains(body, "(PC)") || strings.Contains(body, "(SB)") || strings.Contains(body, "PCALIGN") ||
|
||||
strings.HasPrefix(body, "RET") || strings.HasPrefix(body, "NOP") || strings.HasPrefix(body, "BRK") ||
|
||||
strings.Contains(body, "again") || strings.Contains(body, "next") || strings.Contains(body, "loop") {
|
||||
continue
|
||||
}
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||||
f, perr := parser.Parse("errorparity.s", src)
|
||||
if len(perr) > 0 {
|
||||
continue // the parser already rejects the spelling
|
||||
}
|
||||
_, aerr := AssembleFileLOONG64(f)
|
||||
if aerr == nil {
|
||||
if !allowed[body] {
|
||||
t.Errorf("gasm accepts what the toolchain rejects: %s", body)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if want != "" && !loong64DiagEquivalent(want, aerr.Error()) {
|
||||
t.Errorf("gasm rejects %s with an inequivalent diagnostic:\n toolchain: %s\n gasm: %s", body, want, aerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loong64DiagEquivalent answers whether gasm's rejection carries the same
|
||||
// information as the toolchain's expected message. The two word the same
|
||||
// facts differently: the toolchain writes "operand out of range 0 to 15"
|
||||
// where gasm writes "immediate out of range [0, 15]", and both terminate
|
||||
// sentences the toolchain punctuates. Canonicalisation drops the subject
|
||||
// word and the connectives and compares the remaining token sequence.
|
||||
func loong64DiagEquivalent(want, got string) bool {
|
||||
return loong64DiagSubseq(loong64DiagTokens(want), loong64DiagTokens(got))
|
||||
}
|
||||
|
||||
// loong64DiagTokens lower-cases a diagnostic, strips punctuation and the
|
||||
// connective tokens, and returns its words. "to" survives only between
|
||||
// digits, where the toolchain's range wording uses it; gasm's bracketed
|
||||
// form never produces it.
|
||||
func loong64DiagTokens(msg string) []string {
|
||||
msg = strings.ToLower(msg)
|
||||
for _, r := range []string{"[", "]", ",", ".", ":", "\n"} {
|
||||
msg = strings.ReplaceAll(msg, r, " ")
|
||||
}
|
||||
fields := strings.Fields(msg)
|
||||
out := make([]string, 0, len(fields))
|
||||
for i, w := range fields {
|
||||
switch w {
|
||||
case "the", "a", "operand", "immediate":
|
||||
// subjects and articles carry no constraint
|
||||
case "to":
|
||||
if i > 0 && i+1 < len(fields) && isDigits(fields[i-1]) && isDigits(fields[i+1]) {
|
||||
continue // the range connective
|
||||
}
|
||||
out = append(out, w)
|
||||
default:
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// loong64DiagSubseq answers whether want is a subsequence of got.
|
||||
func loong64DiagSubseq(want, got []string) bool {
|
||||
i := 0
|
||||
for _, w := range got {
|
||||
if i < len(want) && w == want[i] {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i == len(want)
|
||||
}
|
||||
|
||||
func isDigits(s string) bool {
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
// loong64ForeignErrorCorpus lists the corpus directories whose .s files the
|
||||
// loong64 build attempts: the toolchain's own assembler testdata (the
|
||||
// foreign-architecture files the corpus audit indicts on loong64) and the
|
||||
// go/build malformed-source probes.
|
||||
var loong64ForeignErrorCorpus = []string{
|
||||
filepath.Join("cmd", "asm", "internal", "asm", "testdata"),
|
||||
filepath.Join("go", "build", "testdata", "bads"),
|
||||
}
|
||||
|
||||
// loong64ForeignAcceptedLines lists toolchain rejections gasm still
|
||||
// accepts, keyed by corpus-relative path and line, each with its reason.
|
||||
// The list only shrinks: every tightening moves lines out of it, and a line
|
||||
// reappearing here means a regression.
|
||||
var loong64ForeignAcceptedLines = map[string]string{
|
||||
// duperror.s rejections are whole-file semantics: the toolchain indicts
|
||||
// the second declaration against the first in the same file, and a
|
||||
// single TEXT or GLOBL line is legal on its own. gasm rejects the
|
||||
// whole file with the same redeclaration diagnostic.
|
||||
"cmd/asm/internal/asm/testdata/duperror.s:7": "symbol foo redeclared is a whole-file judgement",
|
||||
"cmd/asm/internal/asm/testdata/duperror.s:11": "symbol bar redeclared is a whole-file judgement",
|
||||
// The toolchain deletes the R22 spelling from the loong64 register map
|
||||
// (its comment: avoid unintentionally clobbering g) and keeps g alone;
|
||||
// gasm resolves R22 to register 22 as part of its deliberate ABI-alias
|
||||
// superset (A0, SP, T0, ... share the table), so the three mips64.s
|
||||
// spellings that name R22 assemble here.
|
||||
"cmd/asm/internal/asm/testdata/mips64.s:135": "R22 spelling: gasm's deliberate ABI-alias superset",
|
||||
"cmd/asm/internal/asm/testdata/mips64.s:138": "R22 spelling: gasm's deliberate ABI-alias superset",
|
||||
"cmd/asm/internal/asm/testdata/mips64.s:218": "R22 spelling: gasm's deliberate ABI-alias superset",
|
||||
}
|
||||
|
||||
// TestLoong64ForeignErrorParity takes go tool asm's own per-line rejections
|
||||
// on GOARCH=loong64 as the ground truth over the foreign corpus files and
|
||||
// requires gasm to reject every one of those lines too, the documented
|
||||
// acceptance supersets above excepted. The toolchain is run once per file;
|
||||
// the test skips without a live toolchain or in -short.
|
||||
func TestLoong64ForeignErrorParity(t *testing.T) {
|
||||
goroot := loong64Goroot(t)
|
||||
files := loong64CorpusFiles(t, goroot)
|
||||
if len(files) == 0 {
|
||||
t.Skip("no corpus files")
|
||||
}
|
||||
for _, path := range files {
|
||||
if filepath.Base(path) == "loong64error.s" {
|
||||
continue // the native catalogue above walks it deeply
|
||||
}
|
||||
rel := loong64CorpusRel(goroot, path)
|
||||
rejected := loong64ToolchainRejects(t, goroot, path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, ln := range rejected {
|
||||
if ln < 1 || ln > len(lines) {
|
||||
t.Fatalf("%s: diagnostic names line %d beyond the file", rel, ln)
|
||||
}
|
||||
body := strings.TrimSpace(lines[ln-1])
|
||||
if body == "" || strings.HasPrefix(body, "//") {
|
||||
continue
|
||||
}
|
||||
if i := strings.Index(body, "//"); i >= 0 {
|
||||
body = strings.TrimSpace(body[:i])
|
||||
}
|
||||
body = strings.Join(strings.Fields(strings.ReplaceAll(body, "\t", " ")), " ")
|
||||
key := rel + ":" + strconv.Itoa(ln)
|
||||
if _, ok := loong64ForeignAcceptedLines[key]; ok {
|
||||
continue
|
||||
}
|
||||
if loong64ProbeAssembles(body) {
|
||||
t.Errorf("gasm accepts what the toolchain rejects at %s: %s", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loong64RejectedNames pins the names the toolchain's loong64 table carries
|
||||
// but refuses to encode: each is an "illegal combination" under every
|
||||
// operand shape GOARCH=loong64 go tool asm accepts. gasm rejects them too,
|
||||
// and the pair of rejections is the parity this catalogue asserts.
|
||||
var loong64RejectedNames = []string{"RFE", "DUFFCOPY", "DUFFZERO", "PCALIGNMAX"}
|
||||
|
||||
// loong64PairErrorShapes walks the register-pair sugar's negative shapes,
|
||||
// every one measured against GOARCH=loong64 go tool asm. want carries the
|
||||
// toolchain's diagnostic where gasm's wording matches it (checked through
|
||||
// the same equivalence as the catalogue above); an empty want pins the
|
||||
// rejection alone, and the row's comment records the toolchain's own words
|
||||
// for the divergence.
|
||||
var loong64PairErrorShapes = []struct {
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
// A pair never splits inside an address; the toolchain's parse stage
|
||||
// refuses it outright.
|
||||
{"MULV (R4:R5), R6", "indirect through register pair"},
|
||||
// The reordered list fails the instruction's operand shape. The
|
||||
// toolchain words these "illegal combination ..." at the class match;
|
||||
// gasm words them at the operand count.
|
||||
{"MOVV R4:R5, R6", ""},
|
||||
{"BEQ R4:R5, R6", ""},
|
||||
{"JMP R4:R5", ""},
|
||||
{"MULV R4:R5, R7:R8, R6", ""},
|
||||
{"MULV R4:R5, (R6)", ""},
|
||||
{"BEQ R4:R5, 2(PC)", ""},
|
||||
// A right half outside the register table; the toolchain's parse stage
|
||||
// again ("illegal or missing addressing mode for symbol label").
|
||||
{"MULV R4:label", "illegal or missing addressing mode for symbol label"},
|
||||
// An immediate half on either side. The toolchain reorders the pair
|
||||
// first and rejects at the class match ("illegal combination MULV
|
||||
// U3CON ...", "MULV: expected register; found $4"); gasm rejects the
|
||||
// shape without the reorder.
|
||||
{"MOVV $4:R5", ""},
|
||||
{"MULV R4:$5", ""},
|
||||
}
|
||||
|
||||
// TestLoong64PairErrorParity requires gasm to refuse every register-pair
|
||||
// spelling the toolchain refuses, with an equivalent diagnostic where the
|
||||
// row carries one. The toolchain's accept side of the sugar is pinned
|
||||
// byte for byte by TestLOONG64PairSugar; the empty-want rows are documented
|
||||
// wording divergences, not acceptance divergences.
|
||||
func TestLoong64PairErrorParity(t *testing.T) {
|
||||
for _, tt := range loong64PairErrorShapes {
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + tt.body + "\n\tRET\n"
|
||||
f, perr := parser.Parse("pairparity.s", src)
|
||||
if len(perr) > 0 {
|
||||
continue // the parser already rejects the spelling
|
||||
}
|
||||
_, aerr := AssembleFileLOONG64(f)
|
||||
if aerr == nil {
|
||||
t.Errorf("gasm accepts what the toolchain rejects: %s", tt.body)
|
||||
continue
|
||||
}
|
||||
if tt.want != "" && !loong64DiagEquivalent(tt.want, aerr.Error()) {
|
||||
t.Errorf("gasm rejects %s with an inequivalent diagnostic:\n toolchain: %s\n gasm: %s", tt.body, tt.want, aerr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoong64BacklogNameParity walks the audit's known-but-unencodable
|
||||
// backlog: the names go tool asm recognises on loong64 yet refuses to
|
||||
// encode must be refused by gasm as well, and the one name the audit
|
||||
// misses because its probe battery lacks the shape (SCQ, whose only form
|
||||
// is the three-operand store-conditional) must encode to the toolchain's
|
||||
// own bytes. The refusal claim is walked under the full operand-shape
|
||||
// battery below, against the live toolchain and against gasm alike.
|
||||
func TestLoong64BacklogNameParity(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("live toolchain backlog: skipped in -short mode")
|
||||
}
|
||||
for _, name := range loong64RejectedNames {
|
||||
if loong64ProbeAssembles(name) {
|
||||
t.Errorf("gasm encodes %s, which the toolchain refuses under every shape", name)
|
||||
}
|
||||
}
|
||||
// The shapes the plain audit probes, walked here so the backlog's
|
||||
// claim holds under any operand shape, not only the bare mnemonic:
|
||||
// the toolchain must refuse every combination, and gasm must refuse
|
||||
// it too.
|
||||
goroot := loong64Goroot(t)
|
||||
for _, name := range loong64RejectedNames {
|
||||
for _, shape := range loong64BacklogShapes {
|
||||
body := strings.TrimSpace(name + " " + shape)
|
||||
if loong64ProbeAssembles(body) {
|
||||
t.Errorf("gasm encodes %s, which the toolchain refuses under every shape", body)
|
||||
}
|
||||
// The toolchain side of the same row: the file holds the one
|
||||
// instruction, and the live assembler must report it. A
|
||||
// spelling the toolchain accepts would open the name for
|
||||
// encoding and close nothing here but the parity.
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\np2:\n\t" + body + "\n\tRET\n"
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "backlogshape.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if lines := loong64ToolchainRejects(t, goroot, path); len(lines) == 0 {
|
||||
t.Errorf("the toolchain assembles %q, against the backlog's claim", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
// sc.q: loong64enc1.s pins the encoding as c4145738
|
||||
// (SCQ R4, R5, (R6): sc.q rd, rk, (rj)).
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\tSCQ R4, R5, (R6)\n\tRET\n"
|
||||
f, perr := parser.Parse("backlog.s", src)
|
||||
if len(perr) > 0 {
|
||||
t.Fatalf("parse SCQ: %v", perr[0])
|
||||
}
|
||||
img, aerr := AssembleFileLOONG64(f)
|
||||
if aerr != nil {
|
||||
t.Fatalf("assemble SCQ: %v", aerr)
|
||||
}
|
||||
want := []byte{0xc4, 0x14, 0x57, 0x38, 0x20, 0x00, 0x00, 0x4c}
|
||||
if !slices.Equal(img.Code, want) {
|
||||
t.Errorf("SCQ encodes to % X, want % X (RET included)", img.Code, want)
|
||||
}
|
||||
}
|
||||
|
||||
// loong64BacklogShapes is the operand-shape battery the backlog walk runs
|
||||
// every rejected name through: the register, immediate, memory, vector and
|
||||
// label positions the loong64 grammar has, mirroring the plain audit's
|
||||
// probe battery (cmd/gasm's probeShapes) so the two answer the same
|
||||
// question.
|
||||
var loong64BacklogShapes = []string{
|
||||
"", "R4", "R4, R5", "R4, R5, R6", "$1, R4", "R4, (R5)", "(R4), R5",
|
||||
"F0, F1", "F0, F1, F2", "V1, V2", "X1, X2", "V1, FCC0", "$65536, R4",
|
||||
"R4, R5, p2", "p2(SB)", "R5, (R4), R6", "$1, $0", "R1, R5, 0",
|
||||
"0(R7), $5, $0", "V1, V2, V3, V4", "R4, R5, (R6)", "$16", "(R4)",
|
||||
}
|
||||
|
||||
// loong64ProbeAssembles answers whether the single corpus line, wrapped in
|
||||
// its own function, parses and assembles on loong64.
|
||||
func loong64ProbeAssembles(body string) bool {
|
||||
src := "#include \"textflag.h\"\n\n"
|
||||
if strings.HasPrefix(body, "TEXT ") {
|
||||
src += body + "\n\tRET\n"
|
||||
} else if strings.HasPrefix(body, "DATA ") || strings.HasPrefix(body, "GLOBL ") {
|
||||
src += body + "\n\nTEXT ·probe(SB), NOSPLIT, $0-0\n\tRET\n"
|
||||
} else {
|
||||
src += "TEXT ·probe(SB), NOSPLIT, $0-0\n\t" + body + "\n\tRET\n"
|
||||
}
|
||||
f, perr := parser.Parse("foreignparity.s", src)
|
||||
if len(perr) > 0 {
|
||||
return false // the parser already rejects the line
|
||||
}
|
||||
_, aerr := AssembleFileLOONG64(f)
|
||||
return aerr == nil
|
||||
}
|
||||
|
||||
// loong64Goroot resolves the live toolchain root, the environment's own
|
||||
// value first, `go env GOROOT` as the fallback.
|
||||
func loong64Goroot(t *testing.T) string {
|
||||
t.Helper()
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
return goroot
|
||||
}
|
||||
out, err := exec.Command("go", "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
t.Skipf("no GOROOT: %v", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// loong64CorpusFiles lists every .s file under the corpus roots, the
|
||||
// testdata tree walked recursively.
|
||||
func loong64CorpusFiles(t *testing.T, goroot string) []string {
|
||||
t.Helper()
|
||||
var files []string
|
||||
for _, rel := range loong64ForeignErrorCorpus {
|
||||
root := filepath.Join(goroot, "src", rel)
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.HasSuffix(path, ".s") {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("walk %s: %v", root, err)
|
||||
}
|
||||
}
|
||||
slices.Sort(files)
|
||||
return files
|
||||
}
|
||||
|
||||
// loong64CorpusRel renders a corpus file's path relative to GOROOT/src, the
|
||||
// key the accepted-lines catalogue uses.
|
||||
func loong64CorpusRel(goroot, path string) string {
|
||||
rel, err := filepath.Rel(filepath.Join(goroot, "src"), path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
var (
|
||||
// loong64ParseDiag matches the parse-stage "file:line: message".
|
||||
loong64ParseDiag = regexp.MustCompile(`(?m)^(?:\S*asm: )?([^:\s]+\.s):(\d+):`)
|
||||
// loong64EncodeDiag matches the encode-stage
|
||||
// "asm: message: 003c (file.s:61)".
|
||||
loong64EncodeDiag = regexp.MustCompile(`\(([^:\s()]+\.s):(\d+)\)`)
|
||||
)
|
||||
|
||||
// loong64ToolchainRejects runs go tool asm on the file for GOARCH=loong64
|
||||
// and returns the line numbers it rejects, both diagnostic shapes
|
||||
// collected. An infrastructure failure fails the test: without the ground
|
||||
// truth there is no parity to assert.
|
||||
func loong64ToolchainRejects(t *testing.T, goroot, path string) []int {
|
||||
t.Helper()
|
||||
obj := filepath.Join(t.TempDir(), "probe.o")
|
||||
cmd := exec.Command("go", "tool", "asm", "-I", filepath.Join(goroot, "pkg", "include"), "-o", obj, path)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
out, err := cmd.CombinedOutput()
|
||||
var lines []int
|
||||
seen := map[int]bool{}
|
||||
for _, m := range loong64ParseDiag.FindAllStringSubmatch(string(out), -1) {
|
||||
if strings.HasSuffix(m[1], ".s") {
|
||||
ln, perr := strconv.Atoi(m[2])
|
||||
if perr == nil && !seen[ln] {
|
||||
seen[ln] = true
|
||||
lines = append(lines, ln)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range loong64EncodeDiag.FindAllStringSubmatch(string(out), -1) {
|
||||
ln, perr := strconv.Atoi(m[2])
|
||||
if perr == nil && !seen[ln] {
|
||||
seen[ln] = true
|
||||
lines = append(lines, ln)
|
||||
}
|
||||
}
|
||||
slices.Sort(lines)
|
||||
if err != nil && len(lines) == 0 {
|
||||
t.Fatalf("go tool asm %s: %v\n%s", filepath.Base(path), err, out)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
+224
-13
@@ -6,7 +6,7 @@ package asm
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
||||
@@ -20,14 +20,20 @@ import (
|
||||
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
|
||||
// A leaf function (no calls) with a zero frame gets no prologue at all.
|
||||
//
|
||||
// Prologue (autosize > 0), byte-identical to the toolchain:
|
||||
// Prologue (autosize > 0, small), byte-identical to the toolchain:
|
||||
//
|
||||
// MOVV R1, -autosize(R3) // save LR below the new SP (traceback-safe)
|
||||
// ADDV $-autosize, R3 // open the frame
|
||||
// MOVV R1, 0(R3) // save LR again at SP (signal-safety)
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
type loong64FrameInfo struct {
|
||||
@@ -36,6 +42,11 @@ type loong64FrameInfo struct {
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
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.
|
||||
@@ -59,9 +70,157 @@ func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
|
||||
// A zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// (JAL/BL/CALL), matching the toolchain's LEAF mark, which drives the frame
|
||||
// and the epilogue shape.
|
||||
@@ -79,17 +238,37 @@ func loong64IsLeaf(t *ast.Text) bool {
|
||||
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 {
|
||||
if fi.autosize == 0 {
|
||||
return nil
|
||||
}
|
||||
addiD := l64DualTable["ADDV"].imm
|
||||
return l64WordsLE(
|
||||
l64irr(l64loadStoreTable["MOVV"].st, -fi.autosize, 3, 1), // MOVV R1, -autosize(R3)
|
||||
l64irr(addiD, -fi.autosize, 3, 3), // ADDV $-autosize, R3
|
||||
l64irr(l64loadStoreTable["MOVV"].st, 0, 3, 1), // MOVV R1, 0(R3)
|
||||
)
|
||||
var ws []uint32
|
||||
storeBase := 3
|
||||
if fi.autosize > 2046 {
|
||||
// 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
|
||||
@@ -98,17 +277,49 @@ func loong64Return(fi loong64FrameInfo) []byte {
|
||||
var ws []uint32
|
||||
if fi.autosize != 0 {
|
||||
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))
|
||||
}
|
||||
// ADDV $autosize, R3 — close the frame.
|
||||
ws = append(ws, l64irr(l64DualTable["ADDV"].imm, fi.autosize, 3, 3))
|
||||
// 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))
|
||||
} 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))
|
||||
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
|
||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||
// x-N(SP) → (autosize - N)(SP). Returns base = -1 for an unresolvable
|
||||
|
||||
+175
-7
@@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||||
@@ -131,6 +131,98 @@ TEXT ·ptr(SB), NOSPLIT, $0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_firr14Spans pins the 2RI14 offset spans of the LL/SC/MOVWP
|
||||
// families against the toolchain's operand classes: an unaligned offset is
|
||||
// rejected ("offset must be a multiple of 4"), a signed 16-bit offset rides
|
||||
// in si14 alone, and anything wider materialises its high half in R30 with
|
||||
// addu16i.d before the base folds in, with si14 keeping bits 15:2. The
|
||||
// pinned words are GOARCH=loong64 go tool asm's own bytes for the same
|
||||
// sources, boundaries included (-32766 and 32766 are the class edges).
|
||||
func TestLOONG64_firr14Spans(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·spans(SB), NOSPLIT, $0
|
||||
SC R4, 32764(R5)
|
||||
SC R4, -32764(R5)
|
||||
SC R4, 32768(R5)
|
||||
SC R4, -32768(R5)
|
||||
SC R4, -32772(R5)
|
||||
LL 65540(R5), R4
|
||||
LLV 65540(R5), R4
|
||||
SCV R4, 65540(R5)
|
||||
MOVWP R4, 65540(R5)
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x217FFCA4, // sc.w r4, 8191(r5) — inside si14
|
||||
0x218004A4, // sc.w r4, -8191(r5)
|
||||
0x1000001E, // addu16i.d r30, r0, 0
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x218003C4, // sc.w r4, 8192(r30)
|
||||
0x13FFFC1E, // addu16i.d r30, r0, -1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x218003C4, // sc.w r4, 8192(r30)
|
||||
0x13FFFC1E, // addu16i.d r30, r0, -1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x217FFFC4, // sc.w r4, 8191(r30)
|
||||
0x1000041E, // addu16i.d r30, r0, 1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x200007C4, // ll.w r4, 1(r30)
|
||||
0x1000041E, // addu16i.d r30, r0, 1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x220007C4, // ldptr.d r4, 1(r30)
|
||||
0x1000041E, // addu16i.d r30, r0, 1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x230007C4, // sc.d r4, 1(r30)
|
||||
0x1000041E, // addu16i.d r30, r0, 1
|
||||
0x001097DE, // add.d r30, r30, r5
|
||||
0x250007C4, // stptr.w r4, 1(r30)
|
||||
0x4C000020,
|
||||
)
|
||||
|
||||
// The expansion participates in layout: the function size carries the
|
||||
// six three-word forms plus the single-word pair and the RET.
|
||||
f, errs := parser.Parse("spans_loong64.s", `#include "textflag.h"
|
||||
TEXT ·spans(SB), NOSPLIT, $0
|
||||
SC R4, 32768(R5)
|
||||
LL 65540(R5), R4
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
if img.Funcs[0].Size != 8*3+4 {
|
||||
t.Errorf("function size = %d, want %d", img.Funcs[0].Size, 8*3+4)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_firr14Alignment checks the LL/SC/MOVWP alignment rule the
|
||||
// toolchain enforces as "offset must be a multiple of 4": the LoongArch
|
||||
// ll/sc family requires a naturally scaled offset, and si14 cannot express
|
||||
// the remainder. GOARCH=loong64 go tool asm rejects every case here.
|
||||
func TestLOONG64_firr14Alignment(t *testing.T) {
|
||||
cases := []string{
|
||||
"SC R4, 1(R5)",
|
||||
"SCW R4, -1(R5)",
|
||||
"SCV R4, 2(R5)",
|
||||
"LL 1(R5), R4",
|
||||
"LLW 3(R5), R4",
|
||||
"LLV 6(R5), R4",
|
||||
"MOVWP R4, 1(R5)",
|
||||
"MOVVP R4, 2(R5)",
|
||||
}
|
||||
for _, src := range cases {
|
||||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%s: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_atomics exercises the AM* read-modify-write forms and
|
||||
// RDTIME, plus the MOVV FP→GP move.
|
||||
func TestLOONG64_atomics(t *testing.T) {
|
||||
@@ -232,7 +324,10 @@ DATA ·table+0(SB)/8, $42
|
||||
}
|
||||
|
||||
// TestLOONG64_errors checks the encoder's error paths: undefined labels,
|
||||
// invalid register operands and operand-count mismatches.
|
||||
// invalid register operands and operand-count mismatches. The X0 and
|
||||
// AMADDW cases follow the oracle: GOARCH=loong64 go tool asm rejects
|
||||
// `BEQZ X0` (the X bank is not an integer register) and the two-register
|
||||
// `AMADDW R4, R5` (the AM* family is strictly `val, (addr), result`).
|
||||
func TestLOONG64_errors(t *testing.T) {
|
||||
cases := []string{
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
@@ -280,7 +375,7 @@ done:
|
||||
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
||||
// the prologue raises the SP delta by autosize (in effect from the third
|
||||
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
||||
// in MinLC (4) units — byte-identical to `go tool asm`.
|
||||
// in MinLC (4) units; byte-identical to `go tool asm`.
|
||||
func TestLOONG64_pcsp(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -347,21 +442,94 @@ TEXT ·sb(SB), NOSPLIT, $0
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_movImmToFp checks the immediate-to-FP move forms.
|
||||
// TestLOONG64_movImmToFp checks the immediate-to-FP move: MOVW $c, Fd is the
|
||||
// only spelling the toolchain accepts, expanding to ori (or addi.w for the
|
||||
// negative span) into R30 plus movgr2fr.w. The pinned words are the
|
||||
// toolchain's own bytes; the other widths and out-of-range constants are
|
||||
// illegal combinations there and are diagnosed here.
|
||||
func TestLOONG64_movImmToFp(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·fpmov(SB), NOSPLIT, $0
|
||||
MOVV $0x1, F0
|
||||
MOVW $0x1, F0
|
||||
MOVW $0x2, F4
|
||||
MOVW $-1, F4
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
want := []byte{
|
||||
0x00, 0x04, 0x80, 0x03, // ori f0, r0, 1
|
||||
0x04, 0x08, 0x80, 0x03, // ori f4, r0, 2
|
||||
0x1e, 0x04, 0x80, 0x03, // ori r30, r0, 1
|
||||
0xc0, 0xa7, 0x14, 0x01, // movgr2fr.w f0, r30
|
||||
0x1e, 0x08, 0x80, 0x03, // ori r30, r0, 2
|
||||
0xc4, 0xa7, 0x14, 0x01, // movgr2fr.w f4, r30
|
||||
0x1e, 0xfc, 0xbf, 0x02, // addi.w r30, r0, -1
|
||||
0xc4, 0xa7, 0x14, 0x01, // movgr2fr.w f4, r30
|
||||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||||
}
|
||||
if !bytes.Equal(code, want) {
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_movImmToFpErrors checks the immediate-to-FP diagnostics: the
|
||||
// widths the toolchain rejects as illegal combinations, and constants beyond
|
||||
// the 12-bit ori/addi.w span (the toolchain never materialises a wider
|
||||
// constant on this path).
|
||||
func TestLOONG64_movImmToFpErrors(t *testing.T) {
|
||||
cases := []string{
|
||||
"MOVV $1, F0",
|
||||
"MOVF $2, F4",
|
||||
"MOVD $2, F4",
|
||||
"MOVW $100000, F1",
|
||||
"MOVW $-2049, F1",
|
||||
"MOVW $4096, F1",
|
||||
}
|
||||
for _, src := range cases {
|
||||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%s: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_branch16Unsigned pins the unsigned two-operand branches: with
|
||||
// one register BLTU/BGEU keep the register-register form against R0 (never
|
||||
// taken), the toolchain's encoding, where a beqz would test the wrong
|
||||
// condition; the three-operand forms are unchanged.
|
||||
func TestLOONG64_branch16Unsigned(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·u(SB), NOSPLIT, $0
|
||||
BLTU R4, done
|
||||
BGEU R5, done
|
||||
BLTU R6, R7, done
|
||||
BGEU R8, R9, done
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x68001080, // bltu r4, r0, +4
|
||||
0x6C000CA0, // bgeu r5, r0, +3
|
||||
0x680008C7, // bltu r6, r7, +2
|
||||
0x6C000509, // bgeu r8, r9, +1
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_bitFieldRange checks the BSTRINS/BSTRPICK bit-number
|
||||
// validation, mirroring the toolchain's "illegal bit number" rule: 0..31 for
|
||||
// the .w forms, 0..63 for the .d forms, and lsb <= msb.
|
||||
func TestLOONG64_bitFieldRange(t *testing.T) {
|
||||
cases := []string{
|
||||
"BSTRINSW $32, R4, $0, R5",
|
||||
"BSTRPICKW $31, R4, $32, R5",
|
||||
"BSTRINSV $64, R4, $0, R5",
|
||||
"BSTRPICKV $3, R4, $4, R5",
|
||||
"BSTRINSW $-1, R4, $0, R5",
|
||||
}
|
||||
for _, src := range cases {
|
||||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("%s: expected an error, got none", src)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-sdk/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)
|
||||
}
|
||||
}
|
||||
+142
@@ -3,6 +3,8 @@
|
||||
|
||||
package asm
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Operand is an instruction operand: a Reg, a Mem reference or an Imm value.
|
||||
type Operand interface {
|
||||
isOperand()
|
||||
@@ -14,6 +16,51 @@ type Imm int64
|
||||
|
||||
func (Imm) isOperand() {}
|
||||
|
||||
// RegList is a bracketed register range, [Z0-Z3]: the four-register source
|
||||
// of the 4FMAPS and 4VNNIW families. The EVEX emit path carries the list's
|
||||
// low register through the inverted 5-bit V'VVVV field; the three higher
|
||||
// registers are implied by the instruction, so only the pair travels here.
|
||||
type RegList struct {
|
||||
Lo Reg
|
||||
Hi Reg // implied by the encoding; Lo.idx+3 by construction
|
||||
}
|
||||
|
||||
func (RegList) isOperand() {}
|
||||
|
||||
// FloatImm is a floating-point immediate ($-1.0). The SSE mnemonics whose
|
||||
// encoding takes an XMM/memory source at that position rewrite it as a read
|
||||
// from a read-only pool constant ($f64.<hex> or $f32.<hex>), the toolchain's
|
||||
// own behaviour; every other instruction rejects it.
|
||||
type FloatImm struct {
|
||||
Text string // the numeric text as written, sign excluded
|
||||
Neg bool // a leading minus
|
||||
}
|
||||
|
||||
func (FloatImm) isOperand() {}
|
||||
|
||||
// TLSMem is a thread-local access, the source form off(base)(TLS*1) with the
|
||||
// base dropped: the toolchain's one-instruction TLS rewrite assembles it as
|
||||
// the segment-prefixed absolute whose disp32 carries an R_TLS_LE patch site
|
||||
// (the linker fills the TLS slot offset).
|
||||
type TLSMem struct {
|
||||
Disp int64
|
||||
Size int
|
||||
Seg byte // the segment override: FS (0x64) or GS (0x65) on windows
|
||||
}
|
||||
|
||||
func (TLSMem) isOperand() {}
|
||||
|
||||
// SegAbs is a segment-absolute access, 0x30(GS): the segment override
|
||||
// prefixes a disp32 absolute reference with no relocation. The base
|
||||
// register spellings GS and FS produce it.
|
||||
type SegAbs struct {
|
||||
Disp int64
|
||||
Size int
|
||||
Seg byte // 0x64 FS, 0x65 GS
|
||||
}
|
||||
|
||||
func (SegAbs) isOperand() {}
|
||||
|
||||
// Mem is a memory operand of the form disp(base)(index*scale).
|
||||
type Mem struct {
|
||||
Base Reg
|
||||
@@ -23,6 +70,7 @@ type Mem struct {
|
||||
Size int // operand width in bytes
|
||||
HasBase bool
|
||||
HasIndex bool
|
||||
Seg byte // segment override prefix (0x64 FS, 0x65 GS); 0 = none
|
||||
}
|
||||
|
||||
func (Mem) isOperand() {}
|
||||
@@ -48,3 +96,97 @@ type sbMem struct {
|
||||
}
|
||||
|
||||
func (sbMem) isOperand() {}
|
||||
|
||||
// isX86Mem reports whether the operand is an amd64 memory reference: a base
|
||||
// or indexed Mem, or an SB-relative sbMem. Encoders that gate on "memory in
|
||||
// this position" must accept both; the r/m emitters distinguish the two
|
||||
// themselves.
|
||||
func isX86Mem(o Operand) bool {
|
||||
switch o.(type) {
|
||||
case Mem, sbMem:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// byteReg reports whether r is a general-purpose register whose name spells
|
||||
// a byte width (AL, CL, DL, BL, AH-DH, SPL-DIL, R8B-R15B): the name itself
|
||||
// fixes an 8-bit access, unlike the size-agnostic spellings (AX, EAX, RAX,
|
||||
// R11) whose width the mnemonic supplies. The vector, opmask, x87, MMX,
|
||||
// control and segment registers never name a byte access.
|
||||
func byteReg(r Reg) bool {
|
||||
return r.size == 1 && !r.isVec() && !r.mask && !r.fp && !r.mmx && r.ctl == 0 && r.seg == 0
|
||||
}
|
||||
|
||||
// byteFormBase lists the scalar families that own an 8-bit encoding beside
|
||||
// the 16/32/64-bit ones: the arithmetic and logic group, TEST, the moves,
|
||||
// the unary group, the shifts, the exchanges and atomics, the one-operand
|
||||
// IMUL and CRC32. Families without a byte form (LEA, BT, the bit scans,
|
||||
// PUSH/POP, ADCX/ADOX, BSWAP) stay out, so their width comes from the
|
||||
// mnemonic alone and a byte register in them is the handler's own error to
|
||||
// make.
|
||||
var byteFormBase = map[string]bool{
|
||||
"MOV": true,
|
||||
"ADD": true, "OR": true, "ADC": true, "SBB": true,
|
||||
"AND": true, "SUB": true, "XOR": true, "CMP": true,
|
||||
"TEST": true,
|
||||
"INC": true, "DEC": true, "NEG": true, "NOT": true,
|
||||
"MUL": true, "DIV": true, "IDIV": true,
|
||||
"SHL": true, "SAL": true, "SHR": true, "SAR": true,
|
||||
"ROL": true, "ROR": true, "RCL": true, "RCR": true,
|
||||
"XCHG": true, "CMPXCHG": true, "XADD": true,
|
||||
"CRC32": true,
|
||||
"IMUL": true,
|
||||
}
|
||||
|
||||
// operandWidth settles the operand width of a suffixed scalar instruction
|
||||
// against the widths its register operands spell. A byte-spelled register
|
||||
// operand (AL, DL, R8B, ...) forces the 8-bit form whatever the mnemonic's
|
||||
// L suffix or lack of one says: that is the text the toolchain's own
|
||||
// disassembly prints for the byte encodings (the rendered suffix rides the
|
||||
// operand-size attribute, so an L or no suffix at all can name a byte
|
||||
// form), and every register operand joins the form at its low byte, the way
|
||||
// go tool asm reads the classic names there (TESTL R11, DL encodes TESTB
|
||||
// R11B, DL). A W or Q suffix never rides a byte form, so that mix is a
|
||||
// conflict rejected the way the toolchain rejects it (MOVQ AL, AX). With
|
||||
// no byte operand the mnemonic decides alone and 0 returns, keeping the
|
||||
// caller's width: the classic names are size-agnostic at every width.
|
||||
func operandWidth(mnem string, suffix int, ops []Operand) (int, error) {
|
||||
for _, o := range ops {
|
||||
r, ok := o.(Reg)
|
||||
if !ok || !byteReg(r) {
|
||||
continue
|
||||
}
|
||||
if suffix == 2 || suffix == 8 {
|
||||
kind := "quad"
|
||||
if suffix == 2 {
|
||||
kind = "word"
|
||||
}
|
||||
return 0, fmt.Errorf("%s: byte register cannot take the %s form", mnem, kind)
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// widthOperands selects the operands that carry the instruction's data
|
||||
// width for base: every operand of the scalar families but a shift's count,
|
||||
// which names the CL register without narrowing the shifted value (RCLW CL,
|
||||
// 0(R11) stays 16-bit), and nothing of IMUL's two- and three-operand forms,
|
||||
// which have no byte encoding at all.
|
||||
func widthOperands(base string, ops []Operand) []Operand {
|
||||
switch base {
|
||||
case "SHL", "SAL", "SHR", "SAR", "ROL", "ROR", "RCL", "RCR":
|
||||
if len(ops) > 1 {
|
||||
return ops[1:]
|
||||
}
|
||||
return nil
|
||||
case "IMUL":
|
||||
if len(ops) == 1 {
|
||||
return ops
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ops
|
||||
}
|
||||
+43
-9
@@ -12,33 +12,49 @@ import "maps"
|
||||
import "strings"
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// registers K0–K7.
|
||||
// registers K0-K7, the fp flag the x87 stack registers F0-F7, the mmx flag the
|
||||
// MMX registers M0-M7, the seg field a bare segment register (FS, GS) and the
|
||||
// ctl field the control and debug registers, whose number rides an
|
||||
// instruction's reg field rather than r/m.
|
||||
type Reg struct {
|
||||
idx int
|
||||
size int // informational width implied by the name; the mnemonic decides
|
||||
high bool // AH/CH/DH/BH
|
||||
mask bool // K0–K7 opmask register
|
||||
mask bool // K0-K7 opmask register
|
||||
fp bool // F0-F7 x87 stack register
|
||||
mmx bool // M0-M7 MMX register
|
||||
seg int // segment register number plus one (ES=1..GS=6); 0 = not one
|
||||
ctl byte // 0 none, 1 CRn control register, 2 DRn debug register
|
||||
}
|
||||
|
||||
// Index returns the register number (0–15 for GPRs, 0–31 for vectors).
|
||||
// segNumber returns the segment register number (ES=0..GS=5) when r names a
|
||||
// bare segment register.
|
||||
func (r Reg) segNumber() (int, bool) {
|
||||
if r.seg == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return r.seg - 1, true
|
||||
}
|
||||
|
||||
// Index returns the register number (0-15 for GPRs, 0-31 for vectors).
|
||||
func (r Reg) Index() int { return r.idx }
|
||||
|
||||
// Size returns the width in bytes implied by the register's name.
|
||||
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) isOperand() {}
|
||||
|
||||
// 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
|
||||
// low registers SPL/BPL/SIL/DIL (indices 4–7, not high) do as well.
|
||||
// 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.
|
||||
func (r Reg) needsREX(opSize int) bool {
|
||||
if r.idx >= 8 {
|
||||
return true
|
||||
@@ -133,7 +149,7 @@ func buildRegByName() map[string]Reg {
|
||||
}
|
||||
|
||||
// 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.
|
||||
for i := 0; i <= 31; i++ {
|
||||
m["X"+itoa(i)] = Reg{idx: i, size: 16}
|
||||
@@ -144,6 +160,24 @@ func buildRegByName() map[string]Reg {
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["K"+itoa(i)] = Reg{idx: i, size: 8, mask: true}
|
||||
}
|
||||
// x87 stack: F0..F7.
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["F"+itoa(i)] = Reg{idx: i, size: 8, fp: true}
|
||||
}
|
||||
// MMX: M0..M7.
|
||||
for i := 0; i <= 7; i++ {
|
||||
m["M"+itoa(i)] = Reg{idx: i, size: 8, mmx: true}
|
||||
}
|
||||
// Bare segment registers: ES, CS, SS, DS, FS, GS (the memory-base and
|
||||
// index spellings of FS and GS are handled before register lookup).
|
||||
for i, n := range []string{"ES", "CS", "SS", "DS", "FS", "GS"} {
|
||||
m[n] = Reg{idx: i, size: 2, seg: i + 1}
|
||||
}
|
||||
// Control and debug registers: CR0..CR15, DR0..DR15.
|
||||
for i := 0; i <= 15; i++ {
|
||||
m["CR"+itoa(i)] = Reg{idx: i, size: 8, ctl: 1}
|
||||
m["DR"+itoa(i)] = Reg{idx: i, size: 8, ctl: 2}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestRenderedTextEncodes pins the encoder against the disassembler's
|
||||
// renderings: every line here is a text the toolchain's own decoder prints
|
||||
// for bytes its assembler produced (the disasm package's parity fixtures),
|
||||
// so assembling the printed text must reproduce those bytes. Each row was
|
||||
// read back through the toolchain's decoder; the hex is the fixture's.
|
||||
func TestRenderedTextEncodes(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
text string
|
||||
want string
|
||||
}{
|
||||
// The segment-absolute rendering lowers to the FS-prefixed disp32
|
||||
// absolute, exactly what the 0(FS) spelling encodes.
|
||||
{"MOVQ FS:0, DX", "64488b142500000000"},
|
||||
// The bank-crossing quadword move takes the prefix by direction: the
|
||||
// MMX source carries F3, the XMM source F2.
|
||||
{"MOVQ2DQ M2, X11", "f3440fd6da"},
|
||||
} {
|
||||
src := "TEXT \u00b7k(SB), NOSPLIT, $0\n\t" + tt.text + "\n\tRET\n"
|
||||
f, errs := parser.Parse("k.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("%s: parse: %v", tt.text, errs[0])
|
||||
continue
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Errorf("%s: assemble: %v", tt.text, err)
|
||||
continue
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
// The no-fallthrough rule appends the RET; the pinned instruction is
|
||||
// the prefix.
|
||||
got := hex.EncodeToString(code)
|
||||
if !strings.HasPrefix(got, tt.want) {
|
||||
t.Errorf("%s: bytes %s, want prefix %s", tt.text, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4768
-230
File diff suppressed because it is too large.
Load diff
Loaded 100 of 488 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user