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 | ||
|
|
22226d59a5 | ||
|
|
a0ae0e4e37 | ||
|
|
94c4756d47 | ||
|
|
a5a59d6503 | ||
|
|
9cb1666b35 | ||
|
|
96cc70731f | ||
|
|
b4da13d0f6 | ||
|
|
41b387e54d | ||
|
|
4171e412b5 | ||
|
|
57c0ca8b09 | ||
|
|
75bd83fd52 | ||
|
|
62f6fb4faf | ||
|
|
8f84dac10b | ||
|
|
6d7f10f13e | ||
|
|
56f8babbce | ||
|
|
6c1c8d9d96 | ||
|
|
93794c02f7 | ||
|
|
56ad158772 | ||
|
|
15e8b88d32 | ||
|
|
9beff4ae85 | ||
|
|
eacf33d0f7 | ||
|
|
9a34733615 | ||
|
|
970df7c32a | ||
|
|
49572efe16 | ||
|
|
568c553986 | ||
|
|
7a30e902fc | ||
|
|
cb398d9498 | ||
|
|
e44162a749 | ||
|
|
6fb9629ab6 | ||
|
|
8bda4066e3 | ||
|
|
685b150ecf | ||
|
|
c92e6bed3a | ||
|
|
d75e6bcae6 | ||
|
|
6699ebd34f | ||
|
|
ba4d961b20 | ||
|
|
78b12dd427 | ||
|
|
19a26e049b | ||
|
|
163480e283 | ||
|
|
0d62818db7 | ||
|
|
be2ceaafb9 | ||
|
|
941f7fa990 | ||
|
|
eb3c79c3c8 | ||
|
|
eade875b53 | ||
|
|
b08005753e | ||
|
|
5e06d6a6aa | ||
|
|
e4c9d78968 | ||
|
|
f5fcaf9fa6 | ||
|
|
94b98468bb | ||
|
|
746cef8100 | ||
|
|
f153be8158 | ||
|
|
1bf95a169e | ||
|
|
f9eb4021d6 | ||
|
|
c4930438fd | ||
|
|
f372e2db75 | ||
|
|
ac02c83a86 | ||
|
|
ce5ec24fa8 | ||
|
|
181d8e508c | ||
|
|
1160c96427 | ||
|
|
de9e211ff1 | ||
|
|
3acbdd6533 | ||
|
|
ba502c9b79 | ||
|
|
874e054ecb | ||
|
|
f1960febdc | ||
|
|
ae550cc05a | ||
|
|
cc50035375 | ||
|
|
8054fff9ac | ||
|
|
6a79c35bf7 | ||
|
|
6f4f2096e9 | ||
|
|
56630f8624 | ||
|
|
459f4a2b6e | ||
|
|
5d66343488 | ||
|
|
48334c4d5a | ||
|
|
7629963cab | ||
|
|
97951cbeb6 | ||
|
|
6e73f59e78 | ||
|
|
4221ec5741 | ||
|
|
01dcc3b86e | ||
|
|
b08885bd31 | ||
|
|
c05c53452f | ||
|
|
681a449c01 | ||
|
|
31a2cee382 | ||
|
|
3bc7c18bc3 | ||
|
|
f0512a4e1c | ||
|
|
373c09f725 | ||
|
|
b78b6c5004 | ||
|
|
9b5c878f9e | ||
|
|
31ee8e7941 | ||
|
|
2d1176e045 | ||
|
|
2a27a3a52b | ||
|
|
cde7d0f96a | ||
|
|
d7ee1b78d4 | ||
|
|
30c53565a7 | ||
|
|
ebd8ab8a3c | ||
|
|
176d856f67 | ||
|
|
eace06bbd6 | ||
|
|
f97bea61c5 | ||
|
|
19b37569c0 | ||
|
|
23b3d3e152 | ||
|
|
b0c62be8ce | ||
|
|
ece0d3f127 | ||
|
|
ad6e3360df | ||
|
|
49566de7fb | ||
|
|
6228d77566 | ||
|
|
c0e280ee3c | ||
|
|
2d45dbf7ff | ||
|
|
8ddac0135e | ||
|
|
f58a4fe51d | ||
|
|
5cb7e3e231 | ||
|
|
36bbc0c13b | ||
|
|
32afa3449f | ||
|
|
2ab6b9eb84 | ||
|
|
f41a86b660 | ||
|
|
7721353d44 | ||
|
|
243b087116 | ||
|
|
eee7a6d4a4 | ||
|
|
801fb963c9 | ||
|
|
a2acc9b5a3 | ||
|
|
f860bf8ce6 | ||
|
|
e7df5e5225 | ||
|
|
d114b3412c | ||
|
|
c77d68018c | ||
|
|
89d633f4bb | ||
|
|
f20e0bf1e7 | ||
|
|
1a45b66139 | ||
|
|
382efe538a | ||
|
|
f8d28a42ba | ||
|
|
51a2854d7f | ||
|
|
c234c3dd5b | ||
|
|
9262990ce5 | ||
|
|
d9f6167a4d | ||
|
|
f5088c52fc | ||
|
|
f52e23f1bc | ||
|
|
c9775c2b95 | ||
|
|
5af12e15ac | ||
|
|
db8e3fc160 | ||
|
|
1312122a99 | ||
|
|
11f962fbcc | ||
|
|
ee68859beb | ||
|
|
0920edb092 |
No files matched your search
@@ -0,0 +1,300 @@
|
||||
# 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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- 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-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Validate the tag
|
||||
id: version
|
||||
env:
|
||||
VERSION: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
perl -e '
|
||||
my $v = $ENV{VERSION} // q{};
|
||||
$v =~ m{^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$}
|
||||
or die qq{ERROR: expected a semver tag like v1.2.3, got: $v\n};
|
||||
(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};
|
||||
'
|
||||
|
||||
- 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.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: |
|
||||
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
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Extract the CHANGELOG section
|
||||
env:
|
||||
VERSION: ${{ gitea.ref_name }}
|
||||
run: |
|
||||
# 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;
|
||||
'
|
||||
|
||||
- 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};
|
||||
'
|
||||
|
||||
- name: Create the release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
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);
|
||||
'
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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:
|
||||
push:
|
||||
branches: [development]
|
||||
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:
|
||||
test:
|
||||
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: 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: 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: 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: 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);
|
||||
'
|
||||
+14
-8
@@ -1,12 +1,18 @@
|
||||
# Binaries
|
||||
/gasm
|
||||
/bin/
|
||||
*.exe
|
||||
.idea/
|
||||
.zcode/
|
||||
|
||||
# Test and coverage artefacts
|
||||
# Build output
|
||||
/bin/
|
||||
/gasm
|
||||
coverage.out
|
||||
*.test
|
||||
|
||||
# Editor detritus
|
||||
*.swp
|
||||
.DS_Store
|
||||
# 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
|
||||
+1822
File diff suppressed because it is too large.
Load diff
+135
@@ -0,0 +1,135 @@
|
||||
# Contributing
|
||||
|
||||
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.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-sdk.git
|
||||
cd gasm-sdk
|
||||
just build
|
||||
just gates
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
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`. The release
|
||||
workflow builds the assets and publishes the release and its notes.
|
||||
|
||||
## Code style
|
||||
|
||||
`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.
|
||||
|
||||
- `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.
|
||||
|
||||
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.
|
||||
|
||||
## AI contribution policy
|
||||
|
||||
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.
|
||||
|
||||
- **Disclose the assistance.** If AI helped draft any part of a commit, issue, pull
|
||||
request or review, say so.
|
||||
- **Commit messages carry exactly one trailer**, as a git trailer on the line after a
|
||||
blank line that closes the subject:
|
||||
|
||||
```
|
||||
Assisted-by: MODEL
|
||||
```
|
||||
|
||||
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 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 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 `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 do not go in the issue tracker.** Report them as
|
||||
[SECURITY.md](SECURITY.md) describes, to **opensource@petrbalvin.org**.
|
||||
@@ -0,0 +1,339 @@
|
||||
# GAsm: Software Development Kit for Plan 9 Assembly
|
||||
|
||||
> **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)).
|
||||
|
||||
**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
|
||||
|
||||
- **Front end.** A hand-written lexer and an error-tolerant parser produce a
|
||||
typed AST with source positions; `gasm tokens` and `gasm parse` expose them
|
||||
directly.
|
||||
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
|
||||
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
|
||||
operating recursively on directories the way `go fmt` does. `-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 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 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 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; 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.
|
||||
|
||||
### 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` | 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. 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,
|
||||
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-sdk/cmd/gasm@latest
|
||||
```
|
||||
|
||||
Or from a repository checkout:
|
||||
|
||||
```sh
|
||||
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
|
||||
cat > hello_amd64.s <<'EOF'
|
||||
#include "textflag.h"
|
||||
|
||||
// func add(a, b int) int
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
ADDQ b+8(FP), AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
EOF
|
||||
|
||||
gasm lint hello_amd64.s # static checks
|
||||
gasm asm -o hello.bin hello_amd64.s # assemble to a raw image
|
||||
gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with arguments
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```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 # 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
|
||||
gasm audit-instructions # encoder vs go tool asm name diff
|
||||
gasm scaffold differential k.s # generate a differential test skeleton
|
||||
```
|
||||
|
||||
Run `gasm --help` for the command overview and `gasm <command> -h` for a
|
||||
command's flags. [docs/CLI.md](docs/CLI.md) is the full reference.
|
||||
|
||||
### Editor integration
|
||||
|
||||
`gasm lsp` speaks the Language Server Protocol over standard input/output, so
|
||||
any LSP-capable editor can use it: point your editor's LSP client at the
|
||||
binary and associate it with `.s` files. Syntax highlighting is delivered as
|
||||
LSP semantic tokens, so no editor-specific grammar is required. The server
|
||||
infers the target architecture from the file-name suffix
|
||||
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`).
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and
|
||||
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for setup details and every
|
||||
recipe.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [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
|
||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||
|
||||
## Licence
|
||||
|
||||
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.
|
||||
+113
-9
@@ -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,8 +119,11 @@ 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("// Source: cmd/internal/obj/util.go from the Go toolchain.\n\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")
|
||||
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\n\n")
|
||||
b.WriteString("package arch\n\n")
|
||||
b.WriteString("// commonGeneratedInstrs is the set of opcodes shared by every architecture\n")
|
||||
b.WriteString("// (RET, JMP, NOP, CALL, TEXT, FUNCDATA, PCDATA, …).\n")
|
||||
@@ -153,8 +190,11 @@ 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("// Source: cmd/internal/obj/" + sub + "/anames.go from the Go toolchain.\n\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")
|
||||
b.WriteString("// SPDX-License-Identifier: BSD-3-Clause\n\n")
|
||||
b.WriteString("package arch\n\n")
|
||||
b.WriteString("// " + arch + "GeneratedInstrs is the complete set of " + arch +
|
||||
" mnemonics accepted by\n// Go's Plan 9 assembler.\n")
|
||||
@@ -166,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 {
|
||||
@@ -174,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
|
||||
}
|
||||
|
||||
@@ -270,6 +274,8 @@ func amd64Curated() []Instr {
|
||||
"VMINPD", "VMINPS", "VMINSD", "VMINSS", "VMAXPD", "VMAXPS", "VMAXSD", "VMAXSS",
|
||||
"VXORPD", "VXORPS", "VANDPD", "VANDPS", "VANDNPD", "VANDNPS", "VORPD", "VORPS",
|
||||
"VUNPCKHPD", "VUNPCKLPD", "VUNPCKHPS", "VUNPCKLPS",
|
||||
"PSHUFD", "PSHUFHW", "PSHUFLW", "SHUFPS", "SHUFPD",
|
||||
"UNPCKLPS", "UNPCKHPS", "UNPCKLPD", "UNPCKHPD",
|
||||
"VSQRTPD", "VSQRTPS", "VSQRTSD", "VSQRTSS", "VRSQRTPS", "VRCPPS",
|
||||
"VCMPPD", "VCMPPS", "VCMPSD", "VCMPSS",
|
||||
} {
|
||||
|
||||
+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
+4
-1
@@ -1,5 +1,8 @@
|
||||
// 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)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
+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
+111
-1
@@ -1,5 +1,8 @@
|
||||
// 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)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
@@ -361,6 +364,8 @@ var arm64GeneratedInstrs = []string{
|
||||
"REVW",
|
||||
"ROR",
|
||||
"RORW",
|
||||
"RPRFM",
|
||||
"SB",
|
||||
"SBC",
|
||||
"SBCS",
|
||||
"SBCSW",
|
||||
@@ -474,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",
|
||||
@@ -499,11 +549,17 @@ var arm64GeneratedInstrs = []string{
|
||||
"VLD3R",
|
||||
"VLD4",
|
||||
"VLD4R",
|
||||
"VMLA",
|
||||
"VMLS",
|
||||
"VMOV",
|
||||
"VMOVD",
|
||||
"VMOVI",
|
||||
"VMOVQ",
|
||||
"VMOVS",
|
||||
"VMUL",
|
||||
"VNEG",
|
||||
"VNOT",
|
||||
"VORN",
|
||||
"VORR",
|
||||
"VPMULL",
|
||||
"VPMULL2",
|
||||
@@ -512,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",
|
||||
@@ -527,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",
|
||||
@@ -538,6 +646,8 @@ var arm64GeneratedInstrs = []string{
|
||||
"VUZP1",
|
||||
"VUZP2",
|
||||
"VXAR",
|
||||
"VXTN",
|
||||
"VXTN2",
|
||||
"VZIP1",
|
||||
"VZIP2",
|
||||
"WFE",
|
||||
|
||||
+4
-1
@@ -1,5 +1,8 @@
|
||||
// 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)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,5 +1,8 @@
|
||||
// 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)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
@@ -149,6 +152,8 @@ var loong64GeneratedInstrs = []string{
|
||||
"FNMADDF",
|
||||
"FNMSUBD",
|
||||
"FNMSUBF",
|
||||
"FRINTD",
|
||||
"FRINTF",
|
||||
"FSCALEBD",
|
||||
"FSCALEBF",
|
||||
"FSEL",
|
||||
@@ -174,7 +179,10 @@ var loong64GeneratedInstrs = []string{
|
||||
"FTINTWF",
|
||||
"JIRL",
|
||||
"LL",
|
||||
"LLACQV",
|
||||
"LLACQW",
|
||||
"LLV",
|
||||
"LLW",
|
||||
"LU12IW",
|
||||
"LU32ID",
|
||||
"LU52ID",
|
||||
@@ -245,7 +253,11 @@ var loong64GeneratedInstrs = []string{
|
||||
"ROTR",
|
||||
"ROTRV",
|
||||
"SC",
|
||||
"SCQ",
|
||||
"SCRELV",
|
||||
"SCRELW",
|
||||
"SCV",
|
||||
"SCW",
|
||||
"SGT",
|
||||
"SGTU",
|
||||
"SLL",
|
||||
|
||||
+35
-1
@@ -1,5 +1,8 @@
|
||||
// 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)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package arch
|
||||
|
||||
@@ -78,6 +81,9 @@ var riscvGeneratedInstrs = []string{
|
||||
"CLD",
|
||||
"CLDSP",
|
||||
"CLI",
|
||||
"CLMUL",
|
||||
"CLMULH",
|
||||
"CLMULR",
|
||||
"CLUI",
|
||||
"CLW",
|
||||
"CLWSP",
|
||||
@@ -92,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",
|
||||
@@ -256,6 +269,7 @@ var riscvGeneratedInstrs = []string{
|
||||
"ORCB",
|
||||
"ORI",
|
||||
"ORN",
|
||||
"PAUSE",
|
||||
"RDCYCLE",
|
||||
"RDINSTRET",
|
||||
"RDTIME",
|
||||
@@ -319,6 +333,8 @@ var riscvGeneratedInstrs = []string{
|
||||
"VADDVI",
|
||||
"VADDVV",
|
||||
"VADDVX",
|
||||
"VANDNVV",
|
||||
"VANDNVX",
|
||||
"VANDVI",
|
||||
"VANDVV",
|
||||
"VANDVX",
|
||||
@@ -326,8 +342,17 @@ var riscvGeneratedInstrs = []string{
|
||||
"VASUBUVX",
|
||||
"VASUBVV",
|
||||
"VASUBVX",
|
||||
"VBREV8V",
|
||||
"VBREVV",
|
||||
"VCLMULHVV",
|
||||
"VCLMULHVX",
|
||||
"VCLMULVV",
|
||||
"VCLMULVX",
|
||||
"VCLZV",
|
||||
"VCOMPRESSVM",
|
||||
"VCPOPM",
|
||||
"VCPOPV",
|
||||
"VCTZV",
|
||||
"VDIVUVV",
|
||||
"VDIVUVX",
|
||||
"VDIVVV",
|
||||
@@ -740,10 +765,16 @@ var riscvGeneratedInstrs = []string{
|
||||
"VREMUVX",
|
||||
"VREMVV",
|
||||
"VREMVX",
|
||||
"VREV8V",
|
||||
"VRGATHEREI16VV",
|
||||
"VRGATHERVI",
|
||||
"VRGATHERVV",
|
||||
"VRGATHERVX",
|
||||
"VROLVV",
|
||||
"VROLVX",
|
||||
"VRORVI",
|
||||
"VRORVV",
|
||||
"VRORVX",
|
||||
"VRSUBVI",
|
||||
"VRSUBVX",
|
||||
"VS1RV",
|
||||
@@ -947,6 +978,9 @@ var riscvGeneratedInstrs = []string{
|
||||
"VWMULVX",
|
||||
"VWREDSUMUVS",
|
||||
"VWREDSUMVS",
|
||||
"VWSLLVI",
|
||||
"VWSLLVV",
|
||||
"VWSLLVX",
|
||||
"VWSUBUVV",
|
||||
"VWSUBUVX",
|
||||
"VWSUBUWV",
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectAARCH64Structure checks the basic structure of the emitted
|
||||
// AArch64 GOOBJ: the preamble, the magic, the block offsets and the
|
||||
// non-package symbol definitions.
|
||||
func TestGOObjectAARCH64Structure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.GOObjectAARCH64("testpkg", "k_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
|
||||
// Check preamble.
|
||||
idx := strings.Index(string(obj), "\n!\n")
|
||||
if idx < 0 {
|
||||
t.Fatal("missing preamble separator")
|
||||
}
|
||||
preamble := string(obj[:idx])
|
||||
if !strings.HasPrefix(preamble, "go object") {
|
||||
t.Errorf("preamble = %q, want 'go object ...'", preamble)
|
||||
}
|
||||
|
||||
// Check GOOBJ magic.
|
||||
magicIdx := idx + 3
|
||||
if magicIdx+8 > len(obj) || string(obj[magicIdx:magicIdx+8]) != "\x00go120ld" {
|
||||
t.Error("missing GOOBJ magic")
|
||||
}
|
||||
|
||||
// The object should contain the function's code.
|
||||
if len(img.Code) == 0 {
|
||||
t.Error("no code generated")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// resulting binary. The binary is not executed (no arm64 host or qemu).
|
||||
// Skipped when no Go toolchain is available.
|
||||
func TestGOObjectAARCH64Link(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module a64link\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the cross build (GOARCH=arm64): the package archive and the
|
||||
// link line.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
st := parseBuildLog(t, buildLog, "main_arm64.s")
|
||||
defer os.RemoveAll(st.work)
|
||||
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"),
|
||||
gasmObj, "GOARCH=arm64")
|
||||
|
||||
// Verify the binary exists and contains the symbol.
|
||||
binPath := filepath.Join(dir, "prog2")
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
t.Fatalf("binary not found: %v", err)
|
||||
}
|
||||
binData, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read binary: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(binData), "add") && !strings.Contains(string(binData), "a64link") {
|
||||
t.Error("binary does not contain expected symbol")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+1589
File diff suppressed because it is too large.
Load diff
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
@@ -0,0 +1,578 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
// arm64 frame mapping, matching the Go toolchain's arm64 backend.
|
||||
//
|
||||
// Go's arm64 functions use R29 as the frame pointer (FP) and R30 as the link
|
||||
// register (LR). R31 is the stack pointer (SP). FP and SP in the source
|
||||
// are synthetic pseudo-registers resolved against the hardware SP and the
|
||||
// frame size.
|
||||
//
|
||||
// The autosize is the real stack adjustment: the declared local frame plus
|
||||
// 8 bytes for the saved link register, rounded up to a 16-byte multiple.
|
||||
// The toolchain adds an "extrasize" to align: if autosize%16 == 8, add 8;
|
||||
// if autosize%16 == 0, add 16.
|
||||
//
|
||||
// Prologue (autosize > 0, small frame ≤ 0xf0):
|
||||
//
|
||||
// MOVD.W LR, -autosize(SP) // pre-index: SP -= autosize, store LR at SP
|
||||
// MOVD FP, -8(SP) // store FP at SP-8
|
||||
// SUB $8, SP, FP // FP = SP - 8
|
||||
//
|
||||
// Prologue (autosize > 0, large frame > 0xf0):
|
||||
//
|
||||
// SUB $autosize, SP, R20 // R20 = SP - autosize
|
||||
// STP (FP, LR), -8(R20) // store FP,LR at R20-8
|
||||
// MOVD R20, SP // SP = R20
|
||||
// SUB $8, SP, FP // FP = SP - 8
|
||||
//
|
||||
// Epilogue (non-leaf, small frame):
|
||||
//
|
||||
// ADD $autosize-8, SP, FP // restore FP
|
||||
// ADD $autosize, SP, SP // deallocate frame
|
||||
// MOVD -8(SP), FP // (actually the reverse of prologue)
|
||||
// Actually:
|
||||
// MOVD -8(SP), FP // load FP from SP-8
|
||||
// MOVD.P autosize(SP), LR // post-index: load LR, SP += autosize
|
||||
//
|
||||
// Epilogue (non-leaf, large frame):
|
||||
// ADD $autosize-8, SP, FP
|
||||
// ADD $autosize, SP, SP
|
||||
// Actually:
|
||||
// LDP -8(SP), (FP, LR) // load FP,LR
|
||||
// ADD $autosize, SP, SP // deallocate frame
|
||||
//
|
||||
// Epilogue (leaf with frame):
|
||||
// ADD $autosize-8, SP, FP
|
||||
// ADD $autosize, SP, SP
|
||||
//
|
||||
// RET always emits as BR LR (0xd65f03c0).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// arm64FrameInfo holds the frame layout derived from a TEXT directive.
|
||||
type arm64FrameInfo struct {
|
||||
autosize int // the real SP adjustment (locals + saved LR + alignment)
|
||||
frame int // the declared $framesize
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
leaf bool // no call instructions in the body
|
||||
|
||||
// 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.
|
||||
func arm64ComputeFrame(t *ast.Text) arm64FrameInfo {
|
||||
fi := arm64FrameInfo{
|
||||
frame: frameSize(t),
|
||||
args: argsSize(t),
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
if f == "NOSPLIT" {
|
||||
fi.noSplit = true
|
||||
}
|
||||
}
|
||||
fi.leaf = arm64IsLeaf(t)
|
||||
|
||||
if fi.frame != 0 || !fi.leaf {
|
||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||
// 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 {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "BL", "CALL":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// arm64Prologue returns the prologue bytes for an arm64 function.
|
||||
func arm64Prologue(fi arm64FrameInfo) []byte {
|
||||
if fi.autosize == 0 {
|
||||
return nil
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
// Small frame: MOVD.W LR, -autosize(SP); MOVD FP, -8(SP); SUB $8, SP, FP
|
||||
return a64WordsLE(
|
||||
arm64PreStoreImm(3, 0, int32(-fi.autosize), 31, 30), // STR.W LR, -autosize(SP) (pre-index store)
|
||||
arm64UnscaledStore(3, 0, -8, 31, 29), // STUR FP, [SP, #-8]
|
||||
a64AddSub(1, 1, 0, 0, 8, 31, 29), // SUB $8, SP, FP (op=1 for SUB)
|
||||
)
|
||||
}
|
||||
// Large frame: SUB $autosize, SP, R20; STP (FP,LR), -8(R20); ADD $0, R20, SP; SUB $8, SP, FP
|
||||
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
|
||||
// deallocate the frame when present) followed by RET (BR LR).
|
||||
func arm64Return(fi arm64FrameInfo) []byte {
|
||||
var ws []uint32
|
||||
if fi.autosize != 0 {
|
||||
if fi.leaf {
|
||||
// Leaf with frame: ADD $autosize-8, SP, FP; ADD $autosize, SP, SP
|
||||
ws = append(ws, 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,
|
||||
arm64UnscaledLoad(3, 0, -8, 31, 29), // LDR FP, [SP, #-8]
|
||||
arm64PostLoad(3, 0, int32(fi.autosize), 31, 30), // LDR.P LR, [SP], #autosize
|
||||
)
|
||||
} else {
|
||||
// Large frame: LDP -8(SP), (FP, LR), then deallocate.
|
||||
ws = append(ws,
|
||||
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)
|
||||
ws = append(ws, a64UncondBranch(2, 30, 0)) // opc=2(RET), Rn=LR(30), Rd=0
|
||||
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 {
|
||||
if fi.autosize == 0 {
|
||||
return 0
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 4 // MOVD.W instruction decrements SP
|
||||
}
|
||||
// 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. 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 4 * (len(arm64AddImmWords(uint32(fi.autosize-8), 29)) +
|
||||
len(arm64AddImmWords(uint32(fi.autosize), 31)))
|
||||
}
|
||||
if fi.autosize <= 0xf0 {
|
||||
return 8 // LDR + LDR.P
|
||||
}
|
||||
// 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
|
||||
// hardware base register and offset. x+N(FP) → (N + autosize + 8)(SP);
|
||||
// x+N(SP) → (N + frame + 8)(SP). Returns base = -1 for an unresolvable
|
||||
// reference (SB: static data, handled by the relocation path).
|
||||
//
|
||||
// The Go toolchain resolves all pseudo-register references against the
|
||||
// hardware stack pointer (R31/SP): FP references add autosize+8 (the
|
||||
// distance from SP after the prologue to the caller's argument area),
|
||||
// SP references add frame+8 (the distance to the local area).
|
||||
func arm64ResolvePseudo(sym *ast.Symbol, fi arm64FrameInfo) (base int, off int32) {
|
||||
if sym == nil {
|
||||
return -1, 0
|
||||
}
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
return 31, int32(sym.Offset) + int32(fi.autosize) + 8
|
||||
case "SP":
|
||||
return 31, int32(sym.Offset) + int32(fi.frame) + 8
|
||||
case "SB":
|
||||
return -1, int32(sym.Offset)
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 0<<22 |
|
||||
3<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64UnscaledStore encodes an unscaled store (STUR):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 0<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64UnscaledStore(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 0<<22 |
|
||||
(uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64UnscaledLoad encodes an unscaled load (LDUR):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 0<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64UnscaledLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||
(uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// arm64PostLoad encodes a post-index load (LDR with post-increment):
|
||||
// size<<30 | 7<<27 | V<<26 | opc<<22 | 0<<11 | 1<<10 | imm9<<12 | Rn<<5 | Rt.
|
||||
func arm64PostLoad(size, V int, imm9 int32, rn, rt int) uint32 {
|
||||
return uint32(size)<<30 | 7<<27 | uint32(V)<<26 | 1<<22 |
|
||||
1<<10 | (uint32(imm9)&0x1FF)<<12 | uint32(rn&31)<<5 | uint32(rt&31)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
+1183
-52
File diff suppressed because it is too large.
Load diff
+333
-5
@@ -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.
|
||||
@@ -62,7 +63,7 @@ TEXT ·f(SB), NOSPLIT, $0
|
||||
XORQ AX, AX
|
||||
loop:
|
||||
ADDQ $1, AX
|
||||
CMPQ $10, AX
|
||||
CMPQ AX, $10
|
||||
JLT loop
|
||||
RET
|
||||
`)
|
||||
@@ -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, `
|
||||
@@ -317,3 +369,279 @@ end:
|
||||
t.Errorf("jump-folding mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
TEXT ·pf(SB), NOSPLIT, $0
|
||||
PREFETCHNTA (AX)
|
||||
PREFETCHT0 (BX)
|
||||
PREFETCHT1 8(CX)
|
||||
PREFETCHT2 -1(AX)(R12*1)
|
||||
RET
|
||||
`)
|
||||
code, _, err := Assemble(fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Assemble: %v", err)
|
||||
}
|
||||
got := strings.Join(disasm(t, code), "\n")
|
||||
want := strings.Join([]string{
|
||||
"prefetchnta zmmword ptr [rax]",
|
||||
"prefetcht0 zmmword ptr [rbx]",
|
||||
"prefetcht1 zmmword ptr [rcx+0x8]",
|
||||
"prefetcht2 zmmword ptr [rax+r12-0x1]",
|
||||
"ret",
|
||||
}, "\n")
|
||||
if got != want {
|
||||
t.Errorf("prefetch disassembly mismatch:\n got:\n%s\n want:\n%s", got, want)
|
||||
}
|
||||
// Byte-level expectations: 0F 18 with the variant in the reg field.
|
||||
if hex := hexBytes(code[:3]); hex != "0f 18 00" {
|
||||
t.Errorf("PREFETCHNTA bytes: got %s, want 0f 18 00", hex)
|
||||
}
|
||||
if hex := hexBytes(code[3:6]); hex != "0f 18 0b" {
|
||||
t.Errorf("PREFETCHT0 bytes: got %s, want 0f 18 0b", hex)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
+144
-17
@@ -12,12 +12,11 @@ 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.
|
||||
|
||||
// ELF constants (ELF64, little-endian, System V).
|
||||
const (
|
||||
elfClass64 = 2
|
||||
elfDataLSB = 1
|
||||
@@ -36,18 +35,20 @@ const (
|
||||
shfAlloc = 2
|
||||
shfExecInstr = 4
|
||||
|
||||
stbLocal = 0
|
||||
stbGlobal = 1
|
||||
|
||||
sttNotype = 0
|
||||
sttObject = 1
|
||||
sttFunc = 2
|
||||
sttSection = 3
|
||||
stInfoShift = 4
|
||||
|
||||
shnUndef = 0
|
||||
|
||||
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.
|
||||
@@ -76,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
|
||||
@@ -130,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)
|
||||
@@ -142,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
|
||||
@@ -151,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 {
|
||||
@@ -160,15 +214,12 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
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
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Lay the file out: header, section data, section headers.
|
||||
var out []byte
|
||||
@@ -204,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[:]...)
|
||||
}
|
||||
@@ -220,6 +282,34 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// 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, dwarfSourceName(img), symIdx, dwAlign, cfiAMD64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
// 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)
|
||||
shoff := len(out)
|
||||
|
||||
@@ -246,8 +336,45 @@ 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; 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// DWARF5 section generation for ELF output. Unlike the GOOBJ path (where
|
||||
// the linker assembles the final DWARF), the ELF path must emit complete,
|
||||
// self-contained sections because the system linker only performs fixup
|
||||
// relocations, not assembly.
|
||||
|
||||
// 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. 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 = 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 = 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)
|
||||
return b
|
||||
}
|
||||
|
||||
// dwarfSections holds the generated DWARF section payloads and their
|
||||
// relocations (byte offsets within .debug_info and .debug_line that need
|
||||
// fixup against .text symbols).
|
||||
type dwarfSections struct {
|
||||
debugAbbrev []byte
|
||||
debugInfo []byte
|
||||
debugLine []byte
|
||||
debugLineStr []byte
|
||||
debugFrame []byte
|
||||
// Relocations for .debug_info: (offset, symbol name, addend).
|
||||
infoRelocs []dwarfReloc
|
||||
// Relocations for .debug_line: (offset, symbol name, addend).
|
||||
lineRelocs []dwarfReloc
|
||||
// Relocations for .debug_frame: (offset, symbol name, addend), one per
|
||||
// FDE initial_location.
|
||||
frameRelocs []dwarfReloc
|
||||
}
|
||||
|
||||
type dwarfReloc struct {
|
||||
off uint64
|
||||
name string
|
||||
addend int64
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// Build the string table for .debug_line_str.
|
||||
lineStr := newElfStrtab()
|
||||
lineStr.add(srcFile)
|
||||
ds.debugLineStr = lineStr.bytes()
|
||||
|
||||
// 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, cfi, ds)
|
||||
return ds
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// We'll build the header first, then the programs, then patch the length.
|
||||
headerStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // unit_length (placeholder)
|
||||
b = le.AppendUint16(b, 5) // version (DWARF5)
|
||||
b = append(b, 8) // address_size
|
||||
b = append(b, 0) // segment_selector_size
|
||||
b = append(b, 0, 0, 0, 0) // header_length (placeholder)
|
||||
|
||||
// Line program parameters.
|
||||
b = append(b, 1) // minimum_instruction_length
|
||||
b = append(b, 1) // maximum_ops_per_instruction
|
||||
b = append(b, 1) // default_is_stmt
|
||||
b = append(b, byte(dwLineBase&0xFF)) // line_base (-4 as unsigned)
|
||||
b = append(b, uint8(dwLineRange)) // line_range
|
||||
b = append(b, uint8(dwOpcodeBase)) // opcode_base
|
||||
// Standard opcode lengths (opcode 1..opcode_base-1).
|
||||
b = append(b, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0)
|
||||
|
||||
// Directory table (DWARF5 §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)
|
||||
|
||||
// Per-function line programs.
|
||||
for _, fn := range img.Funcs {
|
||||
// LNE_set_address with the function's offset in .text.
|
||||
b = append(b, 0, 9, 2) // extended opcode, length 9, DW_LNE_set_address
|
||||
addrOff := len(b)
|
||||
b = le.AppendUint64(b, 0) // placeholder for address
|
||||
ds.lineRelocs = append(ds.lineRelocs, dwarfReloc{
|
||||
off: uint64(addrOff),
|
||||
name: fn.Name,
|
||||
addend: 0,
|
||||
})
|
||||
|
||||
// Build the line entries.
|
||||
pts := make([]LineEntry, 0, len(fn.Lines)+1)
|
||||
if len(fn.Lines) == 0 || fn.Lines[0].Offset > 0 {
|
||||
pts = append(pts, LineEntry{Offset: 0, Line: fn.Line})
|
||||
}
|
||||
pts = append(pts, fn.Lines...)
|
||||
|
||||
line := int64(1)
|
||||
pc := uint64(0)
|
||||
for _, p := range pts {
|
||||
if p.Line == 0 || uint64(p.Offset) < pc {
|
||||
continue
|
||||
}
|
||||
if int64(p.Line) == line {
|
||||
continue
|
||||
}
|
||||
deltaPC := uint64(p.Offset) - pc
|
||||
deltaLC := int64(p.Line) - line
|
||||
b = dwPutPCLCDelta(b, deltaPC, deltaLC)
|
||||
line, pc = int64(p.Line), uint64(p.Offset)
|
||||
}
|
||||
|
||||
// Advance to end of function.
|
||||
if end := uint64(fn.Size) - pc; end > 0 {
|
||||
b = append(b, 2) // DW_LNS_advance_pc
|
||||
b = appendUleb(b, end)
|
||||
}
|
||||
b = append(b, 0, 1, 1) // LNE_end_sequence
|
||||
}
|
||||
|
||||
// Patch unit_length.
|
||||
le.PutUint32(b[headerStart:], uint32(len(b)-headerStart-4))
|
||||
// Patch header_length. 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
|
||||
}
|
||||
|
||||
// dwarfBuildInfoSection builds a complete .debug_info section.
|
||||
func dwarfBuildInfoSection(img *Image, srcFile string, ds *dwarfSections) []byte {
|
||||
var b []byte
|
||||
le := binary.LittleEndian
|
||||
|
||||
cuStart := len(b)
|
||||
b = append(b, 0, 0, 0, 0) // unit_length (placeholder)
|
||||
b = le.AppendUint16(b, 5) // version (DWARF5)
|
||||
b = append(b, 0x01) // unit_type (DW_UT_compile)
|
||||
b = append(b, 8) // address_size
|
||||
b = le.AppendUint32(b, 0) // debug_abbrev_offset (0 since single CU)
|
||||
|
||||
// DW_TAG_compile_unit (abbrev 1).
|
||||
b = append(b, 1) // abbreviation code
|
||||
// DW_AT_low_pc: address of .text start. 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
|
||||
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).
|
||||
b = le.AppendUint32(b, 0)
|
||||
// DW_AT_name: source file name.
|
||||
b = append(b, srcFile...)
|
||||
b = append(b, 0)
|
||||
|
||||
// DW_TAG_subprogram entries (abbrev 2).
|
||||
for _, fn := range img.Funcs {
|
||||
b = append(b, 2) // abbreviation code
|
||||
// DW_AT_name.
|
||||
b = append(b, fn.Name...)
|
||||
b = append(b, 0)
|
||||
// DW_AT_low_pc.
|
||||
addrOff := len(b)
|
||||
b = le.AppendUint64(b, 0) // placeholder
|
||||
ds.infoRelocs = append(ds.infoRelocs, dwarfReloc{
|
||||
off: uint64(addrOff),
|
||||
name: fn.Name,
|
||||
addend: 0,
|
||||
})
|
||||
// DW_AT_high_pc: function size.
|
||||
b = le.AppendUint64(b, uint64(fn.Size))
|
||||
// DW_AT_frame_base: DW_OP_call_frame_cfa.
|
||||
b = append(b, 1, 0x9c)
|
||||
// DW_AT_decl_file: 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.
|
||||
if fn.Static {
|
||||
b = append(b, 0)
|
||||
} else {
|
||||
b = append(b, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// End of compile unit children.
|
||||
b = append(b, 0)
|
||||
|
||||
// Patch unit_length.
|
||||
le.PutUint32(b[cuStart:], uint32(len(b)-cuStart-4))
|
||||
return b
|
||||
}
|
||||
|
||||
func appendUleb(b []byte, v uint64) []byte {
|
||||
return binary.AppendUvarint(b, v)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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, 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, 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, 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.
|
||||
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, 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))
|
||||
|
||||
// Emit CFA rule changes at each Spadj boundary.
|
||||
for _, step := range fn.Spadj {
|
||||
if step.Value == 0 {
|
||||
continue
|
||||
}
|
||||
// DW_CFA_def_cfa_offset: set CFA = SP + |delta|.
|
||||
// The delta is negative (stack grows down), so CFA offset = -delta.
|
||||
offset := -step.Value
|
||||
if offset > 0 {
|
||||
b = append(b, 0x0e) // DW_CFA_def_cfa_offset
|
||||
b = appendUleb(b, uint64(offset))
|
||||
}
|
||||
}
|
||||
|
||||
// Pad to alignment.
|
||||
for len(b)%4 != 0 {
|
||||
b = append(b, 0) // DW_CFA_nop
|
||||
}
|
||||
|
||||
// Patch FDE length.
|
||||
le.PutUint32(b[fdeStart:], uint32(len(b)-fdeStart-4))
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
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 {
|
||||
abbrevOff, abbrevSize int
|
||||
infoOff, infoSize int
|
||||
lineOff, lineSize int
|
||||
lineStrOff, lineStrSize int
|
||||
frameOff, frameSize int
|
||||
// .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, section-relative.
|
||||
lineRelocs []elfDwarfReloc
|
||||
// Relocations for .debug_frame FDE initial locations, section-relative.
|
||||
frameRelocs []elfDwarfReloc
|
||||
}
|
||||
|
||||
type elfDwarfReloc struct {
|
||||
off uint64 // offset within the target section
|
||||
sym int // symbol index in .symtab
|
||||
addend int64
|
||||
}
|
||||
|
||||
// appendDWARFSections generates and appends DWARF5 debug sections to the ELF
|
||||
// output. It returns the section offsets/sizes and relocations for the caller
|
||||
// to emit section headers and relocation records.
|
||||
//
|
||||
// symIdx maps function names to their .symtab indices (needed for relocations
|
||||
// against .text symbols). The map uses objectName format (pkg.name); the
|
||||
// DWARF code uses bare function names, so we build a reverse lookup. 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 {
|
||||
// Strip package prefix: "pkg.name" → "name".
|
||||
if i := len(name) - 1; i >= 0 {
|
||||
for j := len(name) - 1; j >= 0; j-- {
|
||||
if name[j] == '.' {
|
||||
nameToIdx[name[j+1:]] = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
nameToIdx[name] = idx
|
||||
}
|
||||
ds := emitDWARF(img, srcFile, cfi)
|
||||
if ds == nil || len(ds.debugAbbrev) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &dwarfELFSections{}
|
||||
|
||||
// .debug_abbrev
|
||||
align(1)
|
||||
result.abbrevOff = len(*out)
|
||||
result.abbrevSize = len(ds.debugAbbrev)
|
||||
*out = append(*out, ds.debugAbbrev...)
|
||||
|
||||
// .debug_line_str
|
||||
align(1)
|
||||
result.lineStrOff = len(*out)
|
||||
result.lineStrSize = len(ds.debugLineStr)
|
||||
*out = append(*out, ds.debugLineStr...)
|
||||
|
||||
// .debug_line
|
||||
align(1)
|
||||
result.lineOff = len(*out)
|
||||
result.lineSize = len(ds.debugLine)
|
||||
*out = append(*out, ds.debugLine...)
|
||||
for _, dr := range ds.lineRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.lineRelocs = append(result.lineRelocs, elfDwarfReloc{
|
||||
off: dr.off,
|
||||
sym: idx,
|
||||
addend: dr.addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// .debug_info
|
||||
align(1)
|
||||
result.infoOff = len(*out)
|
||||
result.infoSize = len(ds.debugInfo)
|
||||
*out = append(*out, ds.debugInfo...)
|
||||
for _, dr := range ds.infoRelocs {
|
||||
if idx, ok := nameToIdx[dr.name]; ok {
|
||||
result.infoRelocs = append(result.infoRelocs, elfDwarfReloc{
|
||||
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",
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-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
|
||||
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)
|
||||
|
||||
// .debug_abbrev must not be empty and must start with abbrev code 1.
|
||||
if len(ds.debugAbbrev) == 0 {
|
||||
t.Fatal("empty .debug_abbrev")
|
||||
}
|
||||
if ds.debugAbbrev[0] != 1 {
|
||||
t.Fatalf(".debug_abbrev first byte = %d, want 1", ds.debugAbbrev[0])
|
||||
}
|
||||
|
||||
// .debug_info must have a compile unit header (DWARF5 version 5).
|
||||
if len(ds.debugInfo) < 12 {
|
||||
t.Fatalf(".debug_info too short: %d bytes", len(ds.debugInfo))
|
||||
}
|
||||
// Version field at offset 4 (after unit_length).
|
||||
if ds.debugInfo[4] != 5 || ds.debugInfo[5] != 0 {
|
||||
t.Fatalf(".debug_info version = %d, want 5", uint16(ds.debugInfo[4])|uint16(ds.debugInfo[5])<<8)
|
||||
}
|
||||
|
||||
// .debug_line must have a header.
|
||||
if len(ds.debugLine) < 20 {
|
||||
t.Fatalf(".debug_line too short: %d bytes", len(ds.debugLine))
|
||||
}
|
||||
// Version at offset 4.
|
||||
if ds.debugLine[4] != 5 || ds.debugLine[5] != 0 {
|
||||
t.Fatalf(".debug_line version = %d, want 5", uint16(ds.debugLine[4])|uint16(ds.debugLine[5])<<8)
|
||||
}
|
||||
|
||||
// .debug_line_str must contain the source file name.
|
||||
if len(ds.debugLineStr) == 0 {
|
||||
t.Fatal("empty .debug_line_str")
|
||||
}
|
||||
|
||||
// Relocations must reference the function.
|
||||
if len(ds.lineRelocs) == 0 {
|
||||
t.Fatal("no .debug_line relocations")
|
||||
}
|
||||
if len(ds.infoRelocs) == 0 {
|
||||
t.Fatal("no .debug_info relocations")
|
||||
}
|
||||
}
|
||||
+553
-3
@@ -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)
|
||||
@@ -187,7 +188,7 @@ func TestELFObject(t *testing.T) {
|
||||
end := bytes.IndexByte(strtabRaw[stName:], 0)
|
||||
return string(strtabRaw[stName : int(stName)+end])
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
e := raw[i*24 : (i+1)*24]
|
||||
off := binary.LittleEndian.Uint64(e[0:])
|
||||
info := binary.LittleEndian.Uint64(e[8:])
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AArch64 ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emAARCH64 = 183 // EM_AARCH64
|
||||
|
||||
// AArch64 relocation types (the ELF psABI).
|
||||
rArm64PrelPgHi21 = 275 // R_AARCH64_ADR_PREL_PG_HI21 (ADRP page)
|
||||
rArm64AddAbsLo12NC = 277 // R_AARCH64_ADD_ABS_LO12_NC (ADD 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, an
|
||||
// optional .rela.text and an optional .rela.data.
|
||||
func (img *Image) ELFAARCH64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference is an ADRP pair:
|
||||
// ADRP Rd, 0 → R_AARCH64_ADR_PREL_PG_HI21 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
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
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)
|
||||
}
|
||||
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:
|
||||
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 = 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,
|
||||
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 {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
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)
|
||||
}
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff, 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|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[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
// 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, dwarfSourceName(img), symIdx, dwAlign, cfiARM64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
// 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)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emAARCH64)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], 0)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// TestELFAARCH64Object checks the structure of the emitted AArch64 ELF64
|
||||
// relocatable object: sections, the symbol table (bindings, types, values,
|
||||
// sizes) and the .rela.text relocation pair for the static-symbol load,
|
||||
// parsed back with debug/elf.
|
||||
func TestELFAARCH64Object(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·getanswer(SB), NOSPLIT, $0-8
|
||||
MOVD answer<>(SB), R4
|
||||
MOVD $answer<>(SB), R5
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFAARCH64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFAARCH64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Type != elf.ET_REL || ef.Machine != elf.EM_AARCH64 {
|
||||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_AARCH64", ef.Type, ef.Machine)
|
||||
}
|
||||
|
||||
text := ef.Section(".text")
|
||||
data := ef.Section(".data")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing .text or .data section")
|
||||
}
|
||||
if text.Size == 0 {
|
||||
t.Error(".text section is empty")
|
||||
}
|
||||
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatalf("symbols: %v", err)
|
||||
}
|
||||
|
||||
foundAdd, foundGetanswer, foundAnswer := false, false, false
|
||||
for _, s := range syms {
|
||||
switch s.Name {
|
||||
case "add":
|
||||
foundAdd = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL {
|
||||
t.Errorf("add: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info)
|
||||
}
|
||||
case "getanswer":
|
||||
foundGetanswer = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_FUNC || elf.SymBind(s.Info>>4) != elf.STB_GLOBAL {
|
||||
t.Errorf("getanswer: info=0x%02x, want STT_FUNC|STB_GLOBAL", s.Info)
|
||||
}
|
||||
case "answer":
|
||||
foundAnswer = true
|
||||
if elf.SymType(s.Info&0xf) != elf.STT_OBJECT || elf.SymBind(s.Info>>4) != elf.STB_LOCAL {
|
||||
t.Errorf("answer: info=0x%02x, want STT_OBJECT|STB_LOCAL", s.Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundAdd {
|
||||
t.Error("symbol 'add' not found")
|
||||
}
|
||||
if !foundGetanswer {
|
||||
t.Error("symbol 'getanswer' not found")
|
||||
}
|
||||
if !foundAnswer {
|
||||
t.Error("symbol 'answer' not found")
|
||||
}
|
||||
|
||||
// Check that .rela.text exists (getanswer has SB reference).
|
||||
relaText := ef.Section(".rela.text")
|
||||
if relaText == nil {
|
||||
t.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
|
||||
// static-symbol references (no .rela.text section).
|
||||
func TestELFAARCH64ObjectNoRelocations(t *testing.T) {
|
||||
f, errs := parser.Parse("k_arm64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVD a+0(FP), R4
|
||||
MOVD b+8(FP), R5
|
||||
ADD R5, R4, R4
|
||||
MOVD R4, ret+16(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFAARCH64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFAARCH64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Section(".rela.text") != nil {
|
||||
t.Error("unexpected .rela.text section when there are no relocations")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// LoongArch ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emLOONGARCH = 258 // EM_LOONGARCH
|
||||
|
||||
// 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, an
|
||||
// optional .rela.text and an optional .rela.data.
|
||||
func (img *Image) ELFLOONG64Object() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference is a pcalau12i pair:
|
||||
// pcalau12i rd, 0 → R_LARCH_PCALA_HI20
|
||||
// addi.d/ld/st → R_LARCH_PCALA_LO12
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
typ := uint32(rLarchPCALAHI20)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
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)
|
||||
}
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff, 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|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[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiLOONG64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
// 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)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emLOONGARCH)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], efLarchAbiDoubleObjV1)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// TestELFLOONG64Object checks the structure of the emitted LoongArch ELF64
|
||||
// relocatable object: sections, the symbol table (bindings, types, values,
|
||||
// sizes) and the .rela.text relocation pair for the static-symbol load,
|
||||
// parsed back with debug/elf.
|
||||
func TestELFLOONG64Object(t *testing.T) {
|
||||
f, errs := parser.Parse("k_loong64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·getanswer(SB), NOSPLIT, $0-8
|
||||
MOVV answer<>(SB), R4
|
||||
MOVV R4, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL answer<>(SB), RODATA, $8
|
||||
DATA answer<>+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFLOONG64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFLOONG64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
|
||||
if ef.Type != elf.ET_REL || ef.Machine != elf.EM_LOONGARCH {
|
||||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_LOONGARCH", ef.Type, ef.Machine)
|
||||
}
|
||||
// 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")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing .text or .data section")
|
||||
}
|
||||
if text.Flags&elf.SHF_EXECINSTR == 0 || text.Flags&elf.SHF_ALLOC == 0 {
|
||||
t.Errorf(".text flags = %v", text.Flags)
|
||||
}
|
||||
if data.Flags&elf.SHF_WRITE == 0 {
|
||||
t.Errorf(".data flags = %v", data.Flags)
|
||||
}
|
||||
textData, err := text.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(textData, img.Code) {
|
||||
t.Errorf(".text contents differ from the image code")
|
||||
}
|
||||
dataData, err := data.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatalf("symbols: %v", err)
|
||||
}
|
||||
byName := map[string]elf.Symbol{}
|
||||
for _, s := range syms {
|
||||
byName[s.Name] = s
|
||||
}
|
||||
wantSym := func(name string, bind elf.SymBind, typ elf.SymType, section elf.SectionIndex, size uint64) {
|
||||
t.Helper()
|
||||
s, ok := byName[name]
|
||||
if !ok {
|
||||
t.Errorf("symbol %q not found", name)
|
||||
return
|
||||
}
|
||||
if elf.ST_BIND(s.Info) != bind || elf.ST_TYPE(s.Info) != typ {
|
||||
t.Errorf("%s: bind/type = %v/%v, want %v/%v", name, elf.ST_BIND(s.Info), elf.ST_TYPE(s.Info), bind, typ)
|
||||
}
|
||||
if s.Section != section {
|
||||
t.Errorf("%s: section = %v, want %v", name, s.Section, section)
|
||||
}
|
||||
if s.Size != size {
|
||||
t.Errorf("%s: size = %d, want %d", name, s.Size, size)
|
||||
}
|
||||
}
|
||||
if ef.Sections[1].Name != ".text" || ef.Sections[2].Name != ".data" {
|
||||
t.Fatalf("section layout = %s, %s; want .text, .data", ef.Sections[1].Name, ef.Sections[2].Name)
|
||||
}
|
||||
textIdx := elf.SectionIndex(1)
|
||||
dataIdx := elf.SectionIndex(2)
|
||||
wantSym("add", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 20)
|
||||
wantSym("getanswer", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 16)
|
||||
wantSym("answer", elf.STB_LOCAL, elf.STT_OBJECT, dataIdx, 8)
|
||||
|
||||
// The data section carries 16-byte alignment padding; the answer
|
||||
// symbol sits at its padded offset.
|
||||
ans := byName["answer"]
|
||||
if ans.Value+8 > uint64(len(dataData)) {
|
||||
t.Fatalf("answer value %d outside .data (%d bytes)", ans.Value, len(dataData))
|
||||
}
|
||||
if got := dataData[ans.Value : ans.Value+8]; !bytes.Equal(got, []byte{42, 0, 0, 0, 0, 0, 0, 0}) {
|
||||
t.Errorf("answer data = % x, want $42", got)
|
||||
}
|
||||
|
||||
// Relocations: the static-symbol load is a pcalau12i+ld.d pair, so one
|
||||
// R_LARCH_PCALA_HI20 and one R_LARCH_PCALA_LO12, both against the local
|
||||
// data symbol. debug/elf does not surface rela entries, so read the
|
||||
// section directly.
|
||||
relaSec := ef.Section(".rela.text")
|
||||
if relaSec == nil {
|
||||
t.Fatal("missing .rela.text")
|
||||
}
|
||||
raw, err := relaSec.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(raw)%24 != 0 || len(raw)/24 != 2 {
|
||||
t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw))
|
||||
}
|
||||
le := binary.LittleEndian
|
||||
for i := range 2 {
|
||||
e := raw[i*24 : (i+1)*24]
|
||||
off := le.Uint64(e[0:])
|
||||
info := le.Uint64(e[8:])
|
||||
typ := info & 0xffffffff
|
||||
sym := int(info >> 32)
|
||||
if i == 0 && (typ != uint64(elf.R_LARCH_PCALA_HI20) || off != 20) {
|
||||
t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_HI20 at 20", i, typ, off)
|
||||
}
|
||||
if i == 1 && (typ != uint64(elf.R_LARCH_PCALA_LO12) || off != 24) {
|
||||
t.Errorf("reloc %d: type %d off %d, want R_LARCH_PCALA_LO12 at 24", i, typ, off)
|
||||
}
|
||||
if sym != 3 { // NULL, .text, .data, then the first local: answer
|
||||
t.Errorf("reloc %d: symbol index %d, want 3 (answer)", i, sym)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFLOONG64ObjectNoRelocations checks a file with no static-symbol
|
||||
// references emits a valid object without a .rela.text section.
|
||||
func TestELFLOONG64ObjectNoRelocations(t *testing.T) {
|
||||
f, errs := parser.Parse("n_loong64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·nop(SB), NOSPLIT, $0
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.ELFLOONG64Object()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFLOONG64Object: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
if ef.Section(".rela.text") != nil {
|
||||
t.Error("unexpected .rela.text section")
|
||||
}
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, s := range syms {
|
||||
if s.Name == "nop" && elf.ST_TYPE(s.Info) == elf.STT_FUNC {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("function symbol nop not found")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RISC-V ELF64 relocatable object emission.
|
||||
|
||||
const (
|
||||
emRISCV = 243 // EM_RISCV
|
||||
|
||||
// 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.
|
||||
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, an optional .rela.text and
|
||||
// an optional .rela.data.
|
||||
func (img *Image) ELFRISCVObject() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
const (
|
||||
secText = 1
|
||||
secData = 2
|
||||
)
|
||||
|
||||
// Build symbol table.
|
||||
var locals, globals []elfSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := elfSym{
|
||||
name: objectName(fn.Pkg, fn.Name),
|
||||
info: sttFunc,
|
||||
shndx: secText,
|
||||
value: uint64(fn.Offset),
|
||||
size: uint64(fn.Size),
|
||||
}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := elfSym{
|
||||
name: objectName(d.Pkg, d.Name),
|
||||
info: sttObject,
|
||||
shndx: secData,
|
||||
value: uint64(d.Offset),
|
||||
size: uint64(d.Size),
|
||||
}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.info |= stbGlobal << stInfoShift
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
globals = append(globals, elfSym{name: name, info: stbGlobal << stInfoShift})
|
||||
}
|
||||
syms := []elfSym{
|
||||
{},
|
||||
{name: ".text", info: sttSection, shndx: secText},
|
||||
{name: ".data", info: sttSection, shndx: secData},
|
||||
}
|
||||
syms = append(syms, locals...)
|
||||
shInfo := len(syms)
|
||||
syms = append(syms, globals...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Build relocations. Each SB reference 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 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
|
||||
sym int
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
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: 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: secSymText, addend: auipc},
|
||||
)
|
||||
case RelRISCVJal:
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, 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 {
|
||||
stNames.add(s.name)
|
||||
}
|
||||
stSections := newElfStrtab()
|
||||
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)
|
||||
}
|
||||
|
||||
// Layout.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...)
|
||||
|
||||
align := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
|
||||
align(16)
|
||||
textOff := len(out)
|
||||
out = append(out, img.Code...)
|
||||
|
||||
align(16)
|
||||
dataOff := len(out)
|
||||
out = append(out, img.Data...)
|
||||
|
||||
align(8)
|
||||
symtabOff := len(out)
|
||||
for _, s := range syms {
|
||||
var b [24]byte
|
||||
le.PutUint32(b[0:], uint32(stNames.at(s.name)))
|
||||
b[4] = s.info
|
||||
b[5] = 0
|
||||
le.PutUint16(b[6:], s.shndx)
|
||||
le.PutUint64(b[8:], s.value)
|
||||
le.PutUint64(b[16:], s.size)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff, 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|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[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
|
||||
dwAlign := func(n int) {
|
||||
for len(out)%n != 0 {
|
||||
out = append(out, 0)
|
||||
}
|
||||
}
|
||||
dw := appendDWARFSections(&out, img, dwarfSourceName(img), symIdx, dwAlign, cfiRISCV64)
|
||||
dwarfStart := 0 // section index of .debug_abbrev, set when DWARF is present
|
||||
if dw != nil {
|
||||
// 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)
|
||||
shoff := len(out)
|
||||
|
||||
putSh := func(name string, typ int, flags uint64, off, size int, link, info int, alignV, entsize uint64) {
|
||||
var b [64]byte
|
||||
le.PutUint32(b[0:], uint32(stSections.at(name)))
|
||||
le.PutUint32(b[4:], uint32(typ))
|
||||
le.PutUint64(b[8:], flags)
|
||||
le.PutUint64(b[16:], 0)
|
||||
le.PutUint64(b[24:], uint64(off))
|
||||
le.PutUint64(b[32:], uint64(size))
|
||||
le.PutUint32(b[40:], uint32(link))
|
||||
le.PutUint32(b[44:], uint32(info))
|
||||
le.PutUint64(b[48:], alignV)
|
||||
le.PutUint64(b[56:], entsize)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
putSh("", shtNull, 0, 0, 0, 0, 0, 0, 0)
|
||||
putSh(".text", shtProgbits, shfAlloc|shfExecInstr, textOff, len(img.Code), 0, 0, 16, 0)
|
||||
putSh(".data", shtProgbits, shfAlloc|shfWrite, dataOff, len(img.Data), 0, 0, 16, 0)
|
||||
putSh(".symtab", shtSymtab, 0, symtabOff, 24*len(syms), secStrtab, shInfo, 8, 24)
|
||||
putSh(".strtab", shtStrtab, 0, strtabOff, len(stNames.bytes()), 0, 0, 1, 0)
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ELF header.
|
||||
hdr := out[:64]
|
||||
copy(hdr[0:], []byte{0x7f, 'E', 'L', 'F', elfClass64, elfDataLSB, elfVersion, 0})
|
||||
le.PutUint16(hdr[16:], etREL)
|
||||
le.PutUint16(hdr[18:], emRISCV)
|
||||
le.PutUint32(hdr[20:], elfVersion)
|
||||
le.PutUint64(hdr[24:], 0)
|
||||
le.PutUint64(hdr[32:], 0)
|
||||
le.PutUint64(hdr[40:], uint64(shoff))
|
||||
le.PutUint32(hdr[48:], efRISCVFloatAbiDouble)
|
||||
le.PutUint16(hdr[52:], 64)
|
||||
le.PutUint16(hdr[54:], 0)
|
||||
le.PutUint16(hdr[56:], 0)
|
||||
le.PutUint16(hdr[58:], 64)
|
||||
le.PutUint16(hdr[60:], uint16(nSections))
|
||||
le.PutUint16(hdr[62:], uint16(secShstr))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import "strings"
|
||||
|
||||
// Encodable reports whether the amd64 encoder knows how to encode the
|
||||
// mnemonic. It mirrors the dispatch in (*enc).encode: the fixed-name
|
||||
// instructions, conditional jumps, the CMOV/SET condition families, the
|
||||
// VEX/EVEX/opmask/gather/scatter vector paths, the legacy SSE tables and the
|
||||
// explicit scalar cases. A mnemonic that parses (is in the architecture
|
||||
// table) but is not encodable would otherwise surface only at assembly time,
|
||||
// deep inside a build; the linter uses this predicate to flag it at edit
|
||||
// time.
|
||||
func Encodable(mnemonic string) bool {
|
||||
upper := strings.ToUpper(mnemonic)
|
||||
|
||||
// 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",
|
||||
"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 {
|
||||
return true
|
||||
}
|
||||
|
||||
// VEX/EVEX and friends: the trailing B/W/L/Q/D is part of the mnemonic.
|
||||
base, _, err := parseEvexSuffix(upper)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if isVex(base) || isEvex(base) || isKOp(base) || isGather(base) || isScatter(base) ||
|
||||
base == "KMOVW" || base == "KMOVQ" || base == "KMOVB" || base == "KMOVD" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(upper, "SET"); ok {
|
||||
if _, ok := jccMap[rest]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy SSE shuffles and packed binaries dispatch on the full name; 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.
|
||||
base2, size := splitSize(upper)
|
||||
if size == 0 {
|
||||
size = 8
|
||||
}
|
||||
_ = size
|
||||
if base2 != upper {
|
||||
if _, ok := sseBinTable[base2]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
switch base2 {
|
||||
case "MOV", "MOVD",
|
||||
"ADD", "SUB", "AND", "OR", "XOR", "CMP", "ADC", "SBB",
|
||||
"TEST",
|
||||
"LEA",
|
||||
"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",
|
||||
"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
|
||||
}
|
||||
+598
-31
@@ -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,38 +64,188 @@ 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
|
||||
// before splitSize. A ".Z" suffix requests EVEX zeroing.
|
||||
base, zeroing, err := stripEvexSuffix(upper)
|
||||
// before splitSize. EVEX suffixes (.Z, .SAE, rounding, .BCST) split
|
||||
// off the mnemonic too.
|
||||
base, sfx, err := parseEvexSuffix(upper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isVex(base) || isEvex(base) || base == "KMOVW" {
|
||||
return e.encodeVec(base, ops, zeroing)
|
||||
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 zeroing {
|
||||
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", mnem)
|
||||
if sfx.any() {
|
||||
return fmt.Errorf("%s: the suffix requires an EVEX instruction", mnem)
|
||||
}
|
||||
|
||||
// CMOVcc and SETcc carry the condition in the mnemonic (CMOVLGT, SETNE).
|
||||
@@ -72,40 +257,365 @@ 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)
|
||||
}
|
||||
// Legacy SSE imm8 shuffles whose names end in W/H (PSHUFLW,
|
||||
// PSHUFHW) must dispatch BEFORE the size-suffix split, and the
|
||||
// others ride along.
|
||||
if m, ok := sseShufTable[upper]; ok {
|
||||
return e.encodeSSEShuf(m, ops)
|
||||
}
|
||||
// Legacy SSE packed binaries dispatch on the full name: the packed
|
||||
// integer mnemonics carry real width suffixes (PADDB/PCMPGTW/...),
|
||||
// which the size split must not eat. 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)
|
||||
case "LZCNT", "TZCNT":
|
||||
return e.encodePushPop(ops, size, false)
|
||||
case "BSF", "BSR", "LZCNT", "TZCNT", "POPCNT":
|
||||
return e.encodeCount(base, ops, size)
|
||||
case "MOVBLZX", "MOVBQZX", "MOVWLZX", "MOVWQZX", "MOVWLSX", "MOVLQSX":
|
||||
case "BSWAP":
|
||||
return e.encodeBswap(ops, size)
|
||||
case "PREFETCHNTA", "PREFETCHT0", "PREFETCHT1", "PREFETCHT2":
|
||||
return e.encodePrefetch(base, ops)
|
||||
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)
|
||||
}
|
||||
|
||||
// encodePrefetch emits the 0F 18 /r prefetch hints: the reg field selects
|
||||
// the locality (NTA=0, T0=1, T1=2, T2=3) and the single operand is memory.
|
||||
func (e *enc) encodePrefetch(base string, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects one memory operand", base)
|
||||
}
|
||||
m, ok := ops[0].(Mem)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s requires a memory operand", base)
|
||||
}
|
||||
i := newInstr(0, []byte{0x0F, 0x18})
|
||||
if err := setMem(i, prefetchVariant[base], m); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
var prefetchVariant = map[string]int{
|
||||
"PREFETCHNTA": 0,
|
||||
"PREFETCHT0": 1,
|
||||
"PREFETCHT1": 2,
|
||||
"PREFETCHT2": 3,
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
@@ -128,20 +638,32 @@ func splitSize(upper string) (base string, size int) {
|
||||
// its own direction-dependent opcodes; KTESTW is always VEX; everything else
|
||||
// takes EVEX when an operand demands it (a ZMM or K register, or an
|
||||
// EVEX-only mnemonic) and VEX otherwise.
|
||||
func (e *enc) encodeVec(upper string, ops []Operand, zeroing bool) error {
|
||||
if upper == "KMOVW" {
|
||||
if zeroing {
|
||||
return fmt.Errorf("KMOVW takes no .Z suffix")
|
||||
}
|
||||
return e.encodeKmovw(ops)
|
||||
func (e *enc) encodeVec(upper string, ops []Operand, sfx evexSuffix) error {
|
||||
if gs, ok := gatherTable[upper]; ok {
|
||||
return e.encodeGather(upper, gs, ops, sfx)
|
||||
}
|
||||
if upper == "KTESTW" || !evexRequired(upper, ops) {
|
||||
if zeroing {
|
||||
if ss, ok := scatterTable[upper]; ok {
|
||||
return e.encodeScatter(upper, ss, ops, sfx)
|
||||
}
|
||||
if upper == "KMOVW" || upper == "KMOVQ" || upper == "KMOVB" || upper == "KMOVD" {
|
||||
if sfx.any() {
|
||||
return fmt.Errorf("%s takes no EVEX suffixes", upper)
|
||||
}
|
||||
return e.encodeKmov(upper, ops)
|
||||
}
|
||||
if isKOp(upper) {
|
||||
if sfx.any() {
|
||||
return fmt.Errorf("%s takes no EVEX suffixes", upper)
|
||||
}
|
||||
return e.encodeKOp(upper, ops)
|
||||
}
|
||||
if upper == "KTESTW" || (!evexRequired(upper, ops) && !sfx.evexOnly()) {
|
||||
if sfx.any() {
|
||||
return fmt.Errorf("%s: the .Z suffix requires an EVEX instruction", upper)
|
||||
}
|
||||
return e.encodeVex(upper, ops)
|
||||
}
|
||||
return e.encodeEvex(upper, ops, zeroing)
|
||||
return e.encodeEvex(upper, ops, sfx)
|
||||
}
|
||||
|
||||
// --- instruction components -------------------------------------------------
|
||||
@@ -160,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
|
||||
@@ -202,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
|
||||
@@ -228,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)
|
||||
}
|
||||
@@ -256,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
|
||||
@@ -274,16 +818,39 @@ 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
|
||||
}
|
||||
|
||||
// The SIB scale field only encodes 1/2/4/8; the Go assembler rejects
|
||||
// anything else ("bad scale: 16"), so a silent fallback to scale 1 here
|
||||
// would mis-assemble the operand instead of reporting it.
|
||||
if m.HasIndex && m.Scale != 1 && m.Scale != 2 && m.Scale != 4 && m.Scale != 8 {
|
||||
return 0, -1, nil, 0, 0, fmt.Errorf("bad scale: %d", m.Scale)
|
||||
}
|
||||
|
||||
needSIB := m.HasIndex || (m.HasBase && m.Base.idx&7 == 4)
|
||||
|
||||
var mod int
|
||||
@@ -356,7 +923,7 @@ func le16(v int64) []byte {
|
||||
func le64(v int64) []byte {
|
||||
u := uint64(v)
|
||||
b := make([]byte, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
for i := range 8 {
|
||||
b[i] = byte(u >> (8 * i))
|
||||
}
|
||||
return b
|
||||
|
||||
+1097
-5
File diff suppressed because it is too large.
Load diff
+1608
-149
File diff suppressed because it is too large.
Load diff
+665
-84
@@ -4,13 +4,10 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// TestEvexGroundTruth checks the EVEX (AVX-512) encodings byte for byte
|
||||
@@ -19,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
|
||||
@@ -41,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"},
|
||||
@@ -55,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"},
|
||||
@@ -80,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"},
|
||||
@@ -99,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"},
|
||||
@@ -110,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"},
|
||||
@@ -153,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) {
|
||||
@@ -230,7 +246,11 @@ func TestEvexMasking(t *testing.T) {
|
||||
{"K0 mask", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K0"), vreg(t, "Z3")}},
|
||||
{"two masks", "VPADDD", []Operand{vreg(t, "Z1"), vreg(t, "K1"), vreg(t, "K2"), vreg(t, "Z3")}},
|
||||
{".Z on VEX-only", "VPSHUFD.Z", []Operand{Imm(1), vreg(t, "X0"), vreg(t, "X1")}},
|
||||
{"unsupported suffix", "VPADDD.BCST", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"broadcast unsupported", "VPXORD.BCST", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"rounding unsupported", "VPXORD.RN_SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"bcst with rounding", "VADDPD.BCST.RN_SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"Z not last", "VADDPD.Z.RN_SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"duplicate suffix", "VADDPD.Z.Z", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}},
|
||||
{"KMOVW.Z", "KMOVW.Z", []Operand{vreg(t, "K1"), vreg(t, "K2")}},
|
||||
}
|
||||
for _, c := range bad {
|
||||
@@ -240,6 +260,424 @@ func TestEvexMasking(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// against the Go assembler.
|
||||
func TestEvexExtendedGroundTruth(t *testing.T) {
|
||||
mem64 := func(base Reg) Operand { return Ptr(base, 0, 64) }
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
// Ternary logic and lane shuffles (NDS + imm8).
|
||||
{"VPTERNLOGD", "VPTERNLOGD", []Operand{Imm(0xE8), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f36d4825d9e8"},
|
||||
{"VPTERNLOGQ", "VPTERNLOGQ", []Operand{Imm(0x96), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f3ed4825d996"},
|
||||
{"VSHUFI32X4", "VSHUFI32X4", []Operand{Imm(0x4E), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "Y3")}, "62f36d2843d94e"},
|
||||
{"VSHUFF64X2", "VSHUFF64X2", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f3ed4823d901"},
|
||||
{"VPALIGNR", "VPALIGNR", []Operand{Imm(7), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f36d480fd907"},
|
||||
// Permutes.
|
||||
{"VPERMB", "VPERMB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d488dd9"},
|
||||
{"VPERMW", "VPERMW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed488dd9"},
|
||||
{"VPERMI2D", "VPERMI2D", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d4876d9"},
|
||||
{"VPERMT2PD", "VPERMT2PD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed487fd9"},
|
||||
// Compare with a K destination (and an immediate predicate).
|
||||
{"VCMPPD", "VCMPPD", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3")}, "62f1ed48c2d904"},
|
||||
{"VCMPPS", "VCMPPS", []Operand{Imm(0), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "K4")}, "62f16c28c2e100"},
|
||||
{"VCMPSD", "VCMPSD", []Operand{Imm(17), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K5")}, "62f1ef08c2e911"},
|
||||
// Rounding / SAE / broadcast suffixes.
|
||||
{"VADDPD.RN_SAE", "VADDPD.RN_SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f1ed1858d9"},
|
||||
{"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);
|
||||
// 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"},
|
||||
{"VMAXPS", "VMAXPS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "c5e85fd9"},
|
||||
{"VDIVPS.RD_SAE", "VDIVPS.RD_SAE", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16c385ed9"},
|
||||
{"VADDPS.BCST", "VADDPS.BCST", []Operand{mem64(AX), vreg(t, "Z1"), vreg(t, "Z2")}, "62f174585810"},
|
||||
// Compress / expand.
|
||||
{"VCOMPRESSPD", "VCOMPRESSPD", []Operand{vreg(t, "Z1"), mem64(DI)}, "62f2fd488a0f"},
|
||||
{"VEXPANDPS", "VEXPANDPS", []Operand{mem64(SI), vreg(t, "Y2")}, "62f27d288816"},
|
||||
{"VPCOMPRESSD.Z", "VPCOMPRESSD.Z", []Operand{vreg(t, "Z1"), vreg(t, "K2"), mem64(DI)}, "62f27dca8b0f"},
|
||||
// Broadcasts.
|
||||
{"VPBROADCASTB gpr", "VPBROADCASTB", []Operand{BX, vreg(t, "Z1")}, "62f27d487acb"},
|
||||
{"VPBROADCASTW mem", "VPBROADCASTW", []Operand{mem64(AX), vreg(t, "Z2")}, "62f27d487910"},
|
||||
{"VBROADCASTSS", "VBROADCASTSS", []Operand{mem64(AX), vreg(t, "Y3")}, "c4e27d1818"},
|
||||
{"VBROADCASTSD", "VBROADCASTSD", []Operand{mem64(AX), vreg(t, "Z4")}, "62f2fd481920"},
|
||||
// Wider integer families.
|
||||
{"VPMADDWD", "VPMADDWD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48f5d9"},
|
||||
{"VPMADDUBSW", "VPMADDUBSW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d4804d9"},
|
||||
{"VPMULHUW", "VPMULHUW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d48e4d9"},
|
||||
{"VPSLLVW", "VPSLLVW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed4812d9"},
|
||||
{"VPACKSSWB", "VPACKSSWB", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f16d4863d9"},
|
||||
{"VPACKUSDW", "VPACKUSDW", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d482bd9"},
|
||||
// Absolute values and replicating moves.
|
||||
{"VPABSD", "VPABSD", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f27d481ed1"},
|
||||
{"VPABSQ mem", "VPABSQ", []Operand{mem64(AX), vreg(t, "Z2")}, "62f2fd481f10"},
|
||||
{"VMOVSLDUP", "VMOVSLDUP", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5fa12d1"},
|
||||
{"VMOVSHDUP", "VMOVSHDUP", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17e4816d1"},
|
||||
// Rotates and word/qword shifts.
|
||||
{"VPROLD", "VPROLD", []Operand{Imm(5), vreg(t, "Z1"), vreg(t, "Z2")}, "62f16d4872c905"},
|
||||
{"VPRORQ", "VPRORQ", []Operand{Imm(63), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ed4872c13f"},
|
||||
{"VPSLLW", "VPSLLW", []Operand{Imm(9), vreg(t, "X1"), vreg(t, "X2")}, "c5e971f109"},
|
||||
{"VPSRLQ", "VPSRLQ", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "Z2")}, "62f1ed4873d103"},
|
||||
// Opmask instructions (VEX-encoded, the width in the L/W/pp bits).
|
||||
{"KANDW", "KANDW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec41d9"},
|
||||
{"KORD", "KORD", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d545f4"},
|
||||
{"KXNORQ", "KXNORQ", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ec46d9"},
|
||||
{"KNOTB", "KNOTB", []Operand{vreg(t, "K4"), vreg(t, "K5")}, "c5f944ec"},
|
||||
{"KUNPCKBW", "KUNPCKBW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ed4bd9"},
|
||||
{"KSHIFTLW", "KSHIFTLW", []Operand{Imm(2), vreg(t, "K1"), vreg(t, "K2")}, "c4e3f932d102"},
|
||||
{"KADDQ", "KADDQ", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ec4ad9"},
|
||||
{"KORTESTD", "KORTESTD", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c4e1f998d1"},
|
||||
{"KMOVQ k,k", "KMOVQ", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c4e1f890d1"},
|
||||
{"KMOVQ gpr,k", "KMOVQ", []Operand{BX, vreg(t, "K1")}, "c4e1fb92cb"},
|
||||
// Completed opmask families (ANDN, NOT, OR/XOR word+qword, TEST,
|
||||
// word-width shifts; byte-exact against go tool asm).
|
||||
{"KANDNW", "KANDNW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec42d9"},
|
||||
{"KANDNB", "KANDNB", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c5d542f4"},
|
||||
{"KANDND", "KANDND", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ed42d9"},
|
||||
{"KANDNQ", "KANDNQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d442f4"},
|
||||
{"KANDD", "KANDD", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c4e1ed41d9"},
|
||||
{"KADDD", "KADDD", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d54af4"},
|
||||
{"KNOTW", "KNOTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f844d1"},
|
||||
{"KNOTD", "KNOTD", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c4e1f944e3"},
|
||||
{"KNOTQ", "KNOTQ", []Operand{vreg(t, "K5"), vreg(t, "K6")}, "c4e1f844f5"},
|
||||
{"KORW", "KORW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec45d9"},
|
||||
{"KORQ", "KORQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d445f4"},
|
||||
{"KXNORB", "KXNORB", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ed46d9"},
|
||||
{"KXORW", "KXORW", []Operand{vreg(t, "K1"), vreg(t, "K2"), vreg(t, "K3")}, "c5ec47d9"},
|
||||
{"KXORQ", "KXORQ", []Operand{vreg(t, "K4"), vreg(t, "K5"), vreg(t, "K6")}, "c4e1d447f4"},
|
||||
{"KORTESTW", "KORTESTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f898d1"},
|
||||
{"KORTESTB", "KORTESTB", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c5f998e3"},
|
||||
{"KORTESTQ", "KORTESTQ", []Operand{vreg(t, "K5"), vreg(t, "K6")}, "c4e1f898f5"},
|
||||
{"KTESTW", "KTESTW", []Operand{vreg(t, "K1"), vreg(t, "K2")}, "c5f899d1"},
|
||||
{"KTESTD", "KTESTD", []Operand{vreg(t, "K3"), vreg(t, "K4")}, "c4e1f999e3"},
|
||||
{"KSHIFTLB", "KSHIFTLB", []Operand{Imm(1), vreg(t, "K1"), vreg(t, "K2")}, "c4e37932d101"},
|
||||
{"KSHIFTLD", "KSHIFTLD", []Operand{Imm(2), vreg(t, "K3"), vreg(t, "K4")}, "c4e37933e302"},
|
||||
{"KSHIFTLQ", "KSHIFTLQ", []Operand{Imm(3), vreg(t, "K5"), vreg(t, "K6")}, "c4e3f933f503"},
|
||||
{"KSHIFTRB", "KSHIFTRB", []Operand{Imm(4), vreg(t, "K1"), vreg(t, "K2")}, "c4e37930d104"},
|
||||
{"KSHIFTRW", "KSHIFTRW", []Operand{Imm(5), vreg(t, "K3"), vreg(t, "K4")}, "c4e3f930e305"},
|
||||
{"KSHIFTRQ", "KSHIFTRQ", []Operand{Imm(6), vreg(t, "K5"), vreg(t, "K6")}, "c4e3f931f506"},
|
||||
// Integer compares with an opmask destination (0F3A map, the
|
||||
// go-bzip2 partition kernel's classify instructions).
|
||||
{"VPCMPUB", "VPCMPUB", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X0"), vreg(t, "K1")}, "62f37d083ec901"},
|
||||
{"VPCMPB", "VPCMPB", []Operand{Imm(2), vreg(t, "Y2"), vreg(t, "Y3"), vreg(t, "K2")}, "62f365283fd202"},
|
||||
{"VPCMPUW", "VPCMPUW", []Operand{Imm(5), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "K3")}, "62f3ed483ed905"},
|
||||
{"VPCMPW", "VPCMPW", []Operand{Imm(6), vreg(t, "X3"), vreg(t, "X4"), vreg(t, "K4")}, "62f3dd083fe306"},
|
||||
{"VPCMPD", "VPCMPD", []Operand{Imm(0), vreg(t, "Y1"), vreg(t, "Y2"), vreg(t, "K1")}, "62f36d281fc900"},
|
||||
{"VPCMPUD", "VPCMPUD", []Operand{Imm(1), vreg(t, "Z2"), vreg(t, "Z3"), vreg(t, "K2")}, "62f365481ed201"},
|
||||
{"VPCMPQ", "VPCMPQ", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "K3")}, "62f3ed081fd902"},
|
||||
{"VPCMPUQ", "VPCMPUQ", []Operand{Imm(3), vreg(t, "Y3"), vreg(t, "Y4"), vreg(t, "K4")}, "62f3dd281ee303"},
|
||||
// Lane extract / insert.
|
||||
{"VEXTRACTF32X4", "VEXTRACTF32X4", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "X2")}, "62f37d2819ca01"},
|
||||
{"VEXTRACTI64X2", "VEXTRACTI64X2", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "X2")}, "62f3fd2839ca01"},
|
||||
{"VINSERTF32X8", "VINSERTF32X8", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f36d481ad901"},
|
||||
{"VINSERTI64X4", "VINSERTI64X4", []Operand{Imm(1), vreg(t, "Y1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f3ed483ad901"},
|
||||
// Aligned moves and the scalar single move.
|
||||
{"VMOVAPS", "VMOVAPS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17c4829ca"},
|
||||
{"VMOVDQA64 mem", "VMOVDQA64", []Operand{mem64(AX), vreg(t, "Z2")}, "62f1fd486f10"},
|
||||
{"VMOVSS mem", "VMOVSS", []Operand{mem64(AX), vreg(t, "X2")}, "c5fa1010"},
|
||||
// Conversions and extending/narrowing moves.
|
||||
{"VCVTPS2DQ", "VCVTPS2DQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17d485bd1"},
|
||||
{"VCVTTPS2DQ", "VCVTTPS2DQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17e485bd1"},
|
||||
{"VPMOVZXBW", "VPMOVZXBW", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d30d1"},
|
||||
{"VPMOVSXBW mem", "VPMOVSXBW", []Operand{mem64(AX), vreg(t, "Z2")}, "62f27d482010"},
|
||||
{"VPMOVWB", "VPMOVWB", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4830ca"},
|
||||
{"VPMOVQB", "VPMOVQB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4832ca"},
|
||||
}
|
||||
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: bytes %s, want %s", c.name, got, c.want)
|
||||
continue
|
||||
}
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
|
||||
continue
|
||||
}
|
||||
want := c.mnem
|
||||
if i := strings.IndexByte(want, '.'); i > 0 {
|
||||
want = want[:i]
|
||||
}
|
||||
if inst.Op.String() != want {
|
||||
t.Errorf("%s: decoded as %s", c.name, inst.Op.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
// plus gather/scatter with VSIB addressing, byte for byte against the Go
|
||||
// assembler.
|
||||
func TestEvexHelperGroundTruth(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
|
||||
}{
|
||||
// Reciprocals and rsqrt (packed RM, scalar NDS).
|
||||
{"VRCP14PD", "VRCP14PD", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f2fd484cd1"},
|
||||
{"VRCP14PS", "VRCP14PS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f27d484cd1"},
|
||||
{"VRCP14SD", "VRCP14SD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f2ed084dd9"},
|
||||
{"VRCP14SS", "VRCP14SS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f26d084dd9"},
|
||||
{"VRSQRT14PD", "VRSQRT14PD", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f2fd484ed1"},
|
||||
{"VRSQRT14PS", "VRSQRT14PS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f27d484ed1"},
|
||||
{"VRSQRT14SD", "VRSQRT14SD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f2ed084fd9"},
|
||||
{"VRSQRT14SS", "VRSQRT14SS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f26d084fd9"},
|
||||
// Getexp (packed RM, scalar NDS).
|
||||
{"VGETEXPPD", "VGETEXPPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f2fd4842d1"},
|
||||
{"VGETEXPPS", "VGETEXPPS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f27d4842d1"},
|
||||
{"VGETEXPSD", "VGETEXPSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f2ed0843d9"},
|
||||
{"VGETEXPSS", "VGETEXPSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f26d0843d9"},
|
||||
// Scalef (NDS).
|
||||
{"VSCALEFPD", "VSCALEFPD", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f2ed482cd9"},
|
||||
{"VSCALEFPS", "VSCALEFPS", []Operand{vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f26d482cd9"},
|
||||
{"VSCALEFSD", "VSCALEFSD", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f2ed082dd9"},
|
||||
{"VSCALEFSS", "VSCALEFSS", []Operand{vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f26d082dd9"},
|
||||
// Rndscale / getmant / reduce (packed $imm,src,dst; scalar NDS+imm).
|
||||
{"VRNDSCALEPD", "VRNDSCALEPD", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "Z2")}, "62f3fd4809d104"},
|
||||
{"VRNDSCALEPS", "VRNDSCALEPS", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "Z2")}, "62f37d4808d104"},
|
||||
{"VRNDSCALESD", "VRNDSCALESD", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f3ed080bd904"},
|
||||
{"VRNDSCALESS", "VRNDSCALESS", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f36d080ad904"},
|
||||
{"VGETMANTPD", "VGETMANTPD", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "Z2")}, "62f3fd4826d103"},
|
||||
{"VGETMANTPS", "VGETMANTPS", []Operand{Imm(3), vreg(t, "Z1"), vreg(t, "Z2")}, "62f37d4826d103"},
|
||||
{"VGETMANTSD", "VGETMANTSD", []Operand{Imm(3), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f3ed0827d903"},
|
||||
{"VGETMANTSS", "VGETMANTSS", []Operand{Imm(3), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f36d0827d903"},
|
||||
{"VREDUCEPD", "VREDUCEPD", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "Z2")}, "62f3fd4856d104"},
|
||||
{"VREDUCEPS", "VREDUCEPS", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "Z2")}, "62f37d4856d104"},
|
||||
{"VREDUCESD", "VREDUCESD", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f3ed0857d904"},
|
||||
{"VREDUCESS", "VREDUCESS", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f36d0857d904"},
|
||||
// Fixupimm / range (NDS + imm8).
|
||||
{"VFIXUPIMMPD", "VFIXUPIMMPD", []Operand{Imm(2), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f3ed4854d902"},
|
||||
{"VFIXUPIMMPS", "VFIXUPIMMPS", []Operand{Imm(2), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f36d4854d902"},
|
||||
{"VFIXUPIMMSD", "VFIXUPIMMSD", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f3ed0855d902"},
|
||||
{"VFIXUPIMMSS", "VFIXUPIMMSS", []Operand{Imm(2), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f36d0855d902"},
|
||||
{"VRANGEPD", "VRANGEPD", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f3ed4850d901"},
|
||||
{"VRANGEPS", "VRANGEPS", []Operand{Imm(1), vreg(t, "Z1"), vreg(t, "Z2"), vreg(t, "Z3")}, "62f36d4850d901"},
|
||||
{"VRANGESD", "VRANGESD", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f3ed0851d901"},
|
||||
{"VRANGESS", "VRANGESS", []Operand{Imm(1), vreg(t, "X1"), vreg(t, "X2"), vreg(t, "X3")}, "62f36d0851d901"},
|
||||
// FP class test ($imm, src, kdst; packed forms carry the length in
|
||||
// the X/Y/Z mnemonic suffix the decoder drops).
|
||||
{"VFPCLASSPDZ", "VFPCLASSPDZ", []Operand{Imm(4), vreg(t, "Z1"), vreg(t, "K2")}, "62f3fd4866d104"},
|
||||
{"VFPCLASSPSY", "VFPCLASSPSY", []Operand{Imm(4), vreg(t, "Y1"), vreg(t, "K2")}, "62f37d2866d104"},
|
||||
{"VFPCLASSSD", "VFPCLASSSD", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "K2")}, "62f3fd0867d104"},
|
||||
{"VFPCLASSSS", "VFPCLASSSS", []Operand{Imm(4), vreg(t, "X1"), vreg(t, "K2")}, "62f37d0867d104"},
|
||||
// Gather: VEX spelling (mask register, VSIB, destination) and EVEX
|
||||
// spelling (VSIB, K mask, destination; L'L follows the VSIB index).
|
||||
{"VGATHERDPS vex", "VGATHERDPS", []Operand{vreg(t, "X2"), vsib("SI", "X1", 4), vreg(t, "X3")}, "c4e269921c8e"},
|
||||
{"VPGATHERDD vex", "VPGATHERDD", []Operand{vreg(t, "Y2"), vsib("SI", "Y1", 4), vreg(t, "Y3")}, "c4e26d901c8e"},
|
||||
{"VGATHERDPS evex", "VGATHERDPS", []Operand{vsib("SI", "X1", 4), vreg(t, "K2"), vreg(t, "X3")}, "62f27d0a921c8e"},
|
||||
{"VPGATHERQD evex", "VPGATHERQD", []Operand{vsib("SI", "Z1", 8), vreg(t, "K2"), vreg(t, "Y3")}, "62f27d4a911cce"},
|
||||
// Scatter (EVEX only: source, K mask, VSIB).
|
||||
{"VSCATTERDPS", "VSCATTERDPS", []Operand{vreg(t, "X3"), vreg(t, "K1"), vsib("SI", "X1", 4)}, "62f27d09a21c8e"},
|
||||
{"VSCATTERQPD", "VSCATTERQPD", []Operand{vreg(t, "Z3"), vreg(t, "K1"), vsib("SI", "Z1", 8)}, "62f2fd49a31cce"},
|
||||
// The remaining conversions.
|
||||
{"VCVTDQ2PS", "VCVTDQ2PS", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17c485bd1"},
|
||||
{"VCVTQQ2PS", "VCVTQQ2PS", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1fc485bd1"},
|
||||
{"VCVTPD2QQ", "VCVTPD2QQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fd487bd1"},
|
||||
{"VCVTPS2QQ", "VCVTPS2QQ", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17d487bd1"},
|
||||
{"VCVTUDQ2PD", "VCVTUDQ2PD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "62f17e287ad1"},
|
||||
{"VCVTPH2PS", "VCVTPH2PS", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f27d4813d1"},
|
||||
{"VCVTPS2PH", "VCVTPS2PH", []Operand{Imm(4), vreg(t, "Y1"), vreg(t, "X2")}, "c4e37d1dca04"},
|
||||
}
|
||||
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: bytes %s, want %s", c.name, got, c.want)
|
||||
continue
|
||||
}
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
|
||||
continue
|
||||
}
|
||||
want := c.mnem
|
||||
got := inst.Op.String()
|
||||
if got != want && !(len(want) > len(got) && want[:len(got)] == got) {
|
||||
t.Errorf("%s: decoded as %s", c.name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexGprGroundTruth covers the scalar conversions between vector and
|
||||
// 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
|
||||
// for byte against the Go assembler, including memory sources and extended
|
||||
// GPRs.
|
||||
func TestEvexGprGroundTruth(t *testing.T) {
|
||||
mem := func(b Reg) Operand { return Ptr(b, 0, 8) }
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"VCVTSD2SI", "VCVTSD2SI", []Operand{vreg(t, "X1"), AX}, "c5fb2dc1"},
|
||||
{"VCVTSD2SIQ", "VCVTSD2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fb2dc1"},
|
||||
{"VCVTSS2SI", "VCVTSS2SI", []Operand{vreg(t, "X1"), AX}, "c5fa2dc1"},
|
||||
{"VCVTSS2SIQ", "VCVTSS2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fa2dc1"},
|
||||
{"VCVTTSD2SI", "VCVTTSD2SI", []Operand{vreg(t, "X1"), AX}, "c5fb2cc1"},
|
||||
{"VCVTTSD2SIQ", "VCVTTSD2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fb2cc1"},
|
||||
{"VCVTTSS2SI", "VCVTTSS2SI", []Operand{vreg(t, "X1"), AX}, "c5fa2cc1"},
|
||||
{"VCVTTSS2SIQ", "VCVTTSS2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fa2cc1"},
|
||||
{"VCVTSD2USIL", "VCVTSD2USIL", []Operand{vreg(t, "X1"), AX}, "62f17f0879c1"},
|
||||
{"VCVTSD2USIQ", "VCVTSD2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1ff0879c1"},
|
||||
{"VCVTSS2USIL", "VCVTSS2USIL", []Operand{vreg(t, "X1"), AX}, "62f17e0879c1"},
|
||||
{"VCVTSS2USIQ", "VCVTSS2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1fe0879c1"},
|
||||
{"VCVTTSD2USIL", "VCVTTSD2USIL", []Operand{vreg(t, "X1"), AX}, "62f17f0878c1"},
|
||||
{"VCVTTSD2USIQ", "VCVTTSD2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1ff0878c1"},
|
||||
{"VCVTTSS2USIL", "VCVTTSS2USIL", []Operand{vreg(t, "X1"), AX}, "62f17e0878c1"},
|
||||
{"VCVTTSS2USIQ", "VCVTTSS2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1fe0878c1"},
|
||||
{"VCVTSI2SDL", "VCVTSI2SDL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c5f32ad0"},
|
||||
{"VCVTSI2SDQ", "VCVTSI2SDQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c4e1f32ad0"},
|
||||
{"VCVTSI2SSL", "VCVTSI2SSL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c5f22ad0"},
|
||||
{"VCVTSI2SSQ", "VCVTSI2SSQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c4e1f22ad0"},
|
||||
{"VCVTUSI2SDL", "VCVTUSI2SDL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f177087bd0"},
|
||||
{"VCVTUSI2SDQ", "VCVTUSI2SDQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f1f7087bd0"},
|
||||
{"VCVTUSI2SSL", "VCVTUSI2SSL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f176087bd0"},
|
||||
{"VCVTUSI2SSQ", "VCVTUSI2SSQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f1f6087bd0"},
|
||||
{"VCVTSD2SI mem", "VCVTSD2SI", []Operand{mem(AX), BX}, "c5fb2d18"},
|
||||
{"VCVTSI2SDQ mem", "VCVTSI2SDQ", []Operand{mem(BX), vreg(t, "X1"), vreg(t, "X2")}, "c4e1f32a13"},
|
||||
{"VCVTSD2SIQ hi gpr", "VCVTSD2SIQ", []Operand{vreg(t, "X1"), vreg(t, "R9")}, "c461fb2dc9"},
|
||||
{"VCVTSI2SDQ hi gpr", "VCVTSI2SDQ", []Operand{vreg(t, "R10"), vreg(t, "X1"), vreg(t, "X2")}, "c4c1f32ad2"},
|
||||
}
|
||||
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: bytes %s, want %s", c.name, got, c.want)
|
||||
continue
|
||||
}
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
|
||||
continue
|
||||
}
|
||||
// The decoder does not distinguish the Plan 9 SIQ spelling (the
|
||||
// 64-bit GPR destination) from the base name; the W bit carries it.
|
||||
want := c.mnem
|
||||
got := inst.Op.String()
|
||||
if got != want && !(len(want) > len(got) && want[:len(got)] == got) {
|
||||
t.Errorf("%s: decoded as %s", c.name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexConversionGroundTruth covers the unsigned and truncating VCVT*
|
||||
// conversions, the remaining sign/zero-extending moves, the signed/unsigned
|
||||
// narrowing stores and the mask/vector conversions, byte for byte against
|
||||
// the Go assembler.
|
||||
func TestEvexConversionGroundTruth(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
// Unsigned and truncating conversions.
|
||||
{"VCVTPD2PS", "VCVTPD2PS", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1fd485ad1"},
|
||||
{"VCVTPD2PSX", "VCVTPD2PSX", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "c5f95ad1"},
|
||||
{"VCVTPD2PSY", "VCVTPD2PSY", []Operand{vreg(t, "Y1"), vreg(t, "X2")}, "c5fd5ad1"},
|
||||
{"VCVTPD2UDQ", "VCVTPD2UDQ", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1fc4879d1"},
|
||||
{"VCVTPD2UDQX", "VCVTPD2UDQX", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "62f1fc0879d1"},
|
||||
{"VCVTTPD2UDQ", "VCVTTPD2UDQ", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1fc4878d1"},
|
||||
{"VCVTTPD2UDQY", "VCVTTPD2UDQY", []Operand{vreg(t, "Y1"), vreg(t, "X2")}, "62f1fc2878d1"},
|
||||
{"VCVTTPD2UQQ", "VCVTTPD2UQQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fd4878d1"},
|
||||
{"VCVTPS2UDQ", "VCVTPS2UDQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17c4879d1"},
|
||||
{"VCVTTPS2UDQ", "VCVTTPS2UDQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f17c4878d1"},
|
||||
{"VCVTPS2UQQ", "VCVTPS2UQQ", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17d4879d1"},
|
||||
{"VCVTTPS2UQQ", "VCVTTPS2UQQ", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17d4878d1"},
|
||||
{"VCVTTPD2QQ", "VCVTTPD2QQ", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fd487ad1"},
|
||||
{"VCVTTPS2QQ", "VCVTTPS2QQ", []Operand{vreg(t, "Y1"), vreg(t, "Z2")}, "62f17d487ad1"},
|
||||
{"VCVTUQQ2PD", "VCVTUQQ2PD", []Operand{vreg(t, "Z1"), vreg(t, "Z2")}, "62f1fe487ad1"},
|
||||
{"VCVTUQQ2PS", "VCVTUQQ2PS", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f1ff487ad1"},
|
||||
{"VCVTUQQ2PSX", "VCVTUQQ2PSX", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "62f1ff087ad1"},
|
||||
{"VCVTQQ2PSX", "VCVTQQ2PSX", []Operand{vreg(t, "X1"), vreg(t, "X2")}, "62f1fc085bd1"},
|
||||
{"VCVTQQ2PSY", "VCVTQQ2PSY", []Operand{vreg(t, "Y1"), vreg(t, "X2")}, "62f1fc285bd1"},
|
||||
// The remaining sign/zero-extending moves.
|
||||
{"VPMOVSXBD", "VPMOVSXBD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d21d1"},
|
||||
{"VPMOVSXBQ evex", "VPMOVSXBQ", []Operand{vreg(t, "X1"), vreg(t, "Z2")}, "62f27d4822d1"},
|
||||
{"VPMOVSXWQ", "VPMOVSXWQ", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d24d1"},
|
||||
{"VPMOVSXWD", "VPMOVSXWD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d23d1"},
|
||||
{"VPMOVZXBD", "VPMOVZXBD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d31d1"},
|
||||
{"VPMOVZXBQ evex", "VPMOVZXBQ", []Operand{vreg(t, "X1"), vreg(t, "Z2")}, "62f27d4832d1"},
|
||||
{"VPMOVZXWD", "VPMOVZXWD", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d33d1"},
|
||||
{"VPMOVZXWQ", "VPMOVZXWQ", []Operand{vreg(t, "X1"), vreg(t, "Y2")}, "c4e27d34d1"},
|
||||
// Signed narrowing stores.
|
||||
{"VPMOVSDB", "VPMOVSDB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4821ca"},
|
||||
{"VPMOVSDW", "VPMOVSDW", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4823ca"},
|
||||
{"VPMOVSQB", "VPMOVSQB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4822ca"},
|
||||
{"VPMOVSQD", "VPMOVSQD", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4825ca"},
|
||||
{"VPMOVSQW", "VPMOVSQW", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4824ca"},
|
||||
{"VPMOVSWB", "VPMOVSWB", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4820ca"},
|
||||
// Unsigned narrowing stores.
|
||||
{"VPMOVUSDB", "VPMOVUSDB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4811ca"},
|
||||
{"VPMOVUSDW", "VPMOVUSDW", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4813ca"},
|
||||
{"VPMOVUSQB", "VPMOVUSQB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4812ca"},
|
||||
{"VPMOVUSQD", "VPMOVUSQD", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4815ca"},
|
||||
{"VPMOVUSQW", "VPMOVUSQW", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4814ca"},
|
||||
{"VPMOVUSWB", "VPMOVUSWB", []Operand{vreg(t, "Z1"), vreg(t, "Y2")}, "62f27e4810ca"},
|
||||
{"VPMOVDB", "VPMOVDB", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4831ca"},
|
||||
{"VPMOVQW", "VPMOVQW", []Operand{vreg(t, "Z1"), vreg(t, "X2")}, "62f27e4834ca"},
|
||||
// Mask/vector conversions (the K register is an operand, not a
|
||||
// mask).
|
||||
{"VPMOVM2B", "VPMOVM2B", []Operand{vreg(t, "K1"), vreg(t, "X2")}, "62f27e0828d1"},
|
||||
{"VPMOVM2W", "VPMOVM2W", []Operand{vreg(t, "K1"), vreg(t, "X2")}, "62f2fe0828d1"},
|
||||
{"VPMOVM2D", "VPMOVM2D", []Operand{vreg(t, "K1"), vreg(t, "X2")}, "62f27e0838d1"},
|
||||
{"VPMOVM2Q", "VPMOVM2Q", []Operand{vreg(t, "K1"), vreg(t, "Z2")}, "62f2fe4838d1"},
|
||||
{"VPMOVB2M", "VPMOVB2M", []Operand{vreg(t, "X1"), vreg(t, "K2")}, "62f27e0829d1"},
|
||||
{"VPMOVW2M", "VPMOVW2M", []Operand{vreg(t, "X1"), vreg(t, "K2")}, "62f2fe0829d1"},
|
||||
{"VPMOVD2M", "VPMOVD2M", []Operand{vreg(t, "Z1"), vreg(t, "K2")}, "62f27e4839d1"},
|
||||
{"VPMOVQ2M", "VPMOVQ2M", []Operand{vreg(t, "Z1"), vreg(t, "K2")}, "62f2fe4839d1"},
|
||||
}
|
||||
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: bytes %s, want %s", c.name, got, c.want)
|
||||
continue
|
||||
}
|
||||
inst, err := x86asm.Decode(code, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
|
||||
continue
|
||||
}
|
||||
want := c.mnem
|
||||
got := inst.Op.String()
|
||||
if got != want && !(len(want) > len(got) && want[:len(got)] == got) {
|
||||
t.Errorf("%s: decoded as %s", c.name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEvexErrors checks the EVEX-specific error paths.
|
||||
func TestEvexErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
@@ -256,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 {
|
||||
@@ -264,71 +711,6 @@ func TestEvexErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
||||
// that the static-symbol load resolves to the right bytes in the image.
|
||||
// Skipped when the sibling repository is not checked out.
|
||||
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")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 10 {
|
||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||
}
|
||||
|
||||
// idx16 as the DATA directives define it: dwords 1..16.
|
||||
idx := make([]byte, 0, 64)
|
||||
for i := 1; i <= 16; i++ {
|
||||
idx = append(idx, byte(i), 0, 0, 0)
|
||||
}
|
||||
image := img.Bytes()
|
||||
base := img.Symbols["idx16"]
|
||||
if base == 0 {
|
||||
t.Fatal("idx16 not laid out")
|
||||
}
|
||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||
}
|
||||
|
||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||
// resolve to idx16 within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||
for pos := 0; ; {
|
||||
i := indexOf(code[pos:], pat)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
i += pos
|
||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||
target := fn.Offset + i + 10 + int(rel)
|
||||
if target != base {
|
||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||
}
|
||||
loads++
|
||||
pos = i + 10
|
||||
}
|
||||
}
|
||||
if loads != 1 {
|
||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// hexCompact renders bytes as a lowercase hex string without separators.
|
||||
func hexCompact(b []byte) string {
|
||||
const hexdig = "0123456789abcdef"
|
||||
@@ -340,16 +722,215 @@ func hexCompact(b []byte) string {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||
func indexOf(b, pat []byte) int {
|
||||
for i := 0; i+len(pat) <= len(b); i++ {
|
||||
j := 0
|
||||
for j < len(pat) && b[i+j] == pat[j] {
|
||||
j++
|
||||
// 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 j == len(pat) {
|
||||
return i
|
||||
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)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+769
@@ -0,0 +1,769 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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
|
||||
// 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 /
|
||||
// aux / data index arrays, and the three blocks themselves.
|
||||
//
|
||||
// The object carries what the linker requires of an assembly object: the
|
||||
// functions (non-package symbols, as cmd/asm emits them), the GLOBL data,
|
||||
// one FuncInfo per function, the per-function DWARF symbols (the
|
||||
// .debug_line program and the subprogram DIE, which the linker's DWARF
|
||||
// pass reads verbatim), and the pc-value tables (pcsp, pcfile, pcline,
|
||||
// pcinline). The implicit funcdata symbols are omitted; the linker fills
|
||||
// their defaults.
|
||||
//
|
||||
// emitGOObject is architecture-agnostic; the per-architecture GOObject*
|
||||
// methods supply the toolchain preamble, the MinLC (pc-value delta unit)
|
||||
// and the relocation-type mapping for code relocations.
|
||||
|
||||
// GOOBJ block indices (cmd/internal/goobj). These MUST match the real
|
||||
// archive layout: the emitter writes the header offsets per index and the
|
||||
// reader (groundtruth, goobj_resolve) parses real Go archives with them.
|
||||
// blkAutolib is unused by the emitter but still defines index 0.
|
||||
const (
|
||||
blkAutolib = iota
|
||||
blkPkgIdx
|
||||
blkFile
|
||||
blkSymdef
|
||||
blkHashed64def
|
||||
blkHasheddef
|
||||
blkNonpkgdef
|
||||
blkNonpkgref
|
||||
blkRefFlags
|
||||
blkHash64
|
||||
blkHash
|
||||
blkRelocIdx
|
||||
blkAuxIdx
|
||||
blkDataIdx
|
||||
blkReloc
|
||||
blkAux
|
||||
blkData
|
||||
blkRefName
|
||||
blkEnd
|
||||
)
|
||||
|
||||
// Symbol kinds used by assembly objects (cmd/internal/objabi).
|
||||
const (
|
||||
kindSTEXT = 1
|
||||
kindSRODATA = 3
|
||||
kindSNOPTRDATA = 5
|
||||
kindSDATA = 7
|
||||
kindSDWARFFCN = 14
|
||||
kindSDWARFLINES = 20
|
||||
)
|
||||
|
||||
// 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
|
||||
symABIStatic = 0xffff
|
||||
)
|
||||
|
||||
// Aux entry types (cmd/internal/goobj).
|
||||
const (
|
||||
auxFuncInfo = 1
|
||||
auxDwarfInfo = 3
|
||||
auxDwarfLines = 6
|
||||
auxPcsp = 7
|
||||
auxPcfile = 8
|
||||
auxPcline = 9
|
||||
auxPcinline = 10
|
||||
)
|
||||
|
||||
// FuncInfo flags (internal/abi).
|
||||
const (
|
||||
funcFlagSPWrite = 2
|
||||
funcFlagAsm = 4
|
||||
)
|
||||
|
||||
// Relocation types (cmd/internal/objabi).
|
||||
// R_ADDR, R_CALL, R_PCREL and R_TLS_LE are stable across Go versions.
|
||||
const (
|
||||
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
|
||||
// installed Go toolchain. The value shifted between Go 1.26 (103) and
|
||||
// Go 1.27 (106) because new LoongArch relocations were inserted before it.
|
||||
func relocDWTXTADDRU4() uint16 {
|
||||
if isGo127OrLater() {
|
||||
return 106
|
||||
}
|
||||
return 103
|
||||
}
|
||||
|
||||
var (
|
||||
goVersionOnce sync.Once
|
||||
goVersionGT26 bool
|
||||
)
|
||||
|
||||
// isGo127OrLater reports whether the installed Go toolchain is 1.27 or later.
|
||||
func isGo127OrLater() bool {
|
||||
goVersionOnce.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err := exec.Command(goBin, "version").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// "go version go1.27rc1 linux/amd64"
|
||||
s := string(out)
|
||||
for _, prefix := range []string{"go version go1.27", "go version go1.28", "go version go1.29", "go version go2."} {
|
||||
if strings.Contains(s, prefix) {
|
||||
goVersionGT26 = true
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return goVersionGT26
|
||||
}
|
||||
|
||||
// Special package indices for symbol references.
|
||||
const (
|
||||
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.
|
||||
type goSym struct {
|
||||
name string
|
||||
abi uint16
|
||||
typ uint8
|
||||
flag uint8
|
||||
flag2 uint8
|
||||
size uint32
|
||||
align uint32
|
||||
}
|
||||
|
||||
func (s goSym) append(b []byte, strOff map[string]uint32) []byte {
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(len(s.name)))
|
||||
b = binary.LittleEndian.AppendUint32(b, strOff[s.name])
|
||||
b = binary.LittleEndian.AppendUint16(b, s.abi)
|
||||
b = append(b, s.typ, s.flag, s.flag2)
|
||||
b = binary.LittleEndian.AppendUint32(b, s.size)
|
||||
return binary.LittleEndian.AppendUint32(b, s.align)
|
||||
}
|
||||
|
||||
// dwarfRelocSet attaches emitter-generated relocations (the DWARF
|
||||
// lines/info symbols' address references) to a definition index.
|
||||
type dwarfRelocSet struct {
|
||||
si int
|
||||
relocs []goobjReloc
|
||||
}
|
||||
|
||||
// GOObject returns the image as a GOOBJ object file for the given package
|
||||
// path (the linker qualifies the exported symbols with it, the way cmd/asm
|
||||
// does with its -p flag). srcPath names the source file recorded in the
|
||||
// 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.
|
||||
func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreamble()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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
|
||||
// the toolchain's object preamble; minLC is the architecture's minimum
|
||||
// instruction length, the unit of the pc-value table deltas; relocField
|
||||
// maps a code relocation to its objabi relocation type and the width of
|
||||
// the instruction field the linker writes.
|
||||
func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, relocField func(Reloc) (uint16, uint8)) ([]byte, error) {
|
||||
if pkgPath == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||
}
|
||||
|
||||
// The non-package definitions first, the DWARF symbols reference the
|
||||
// functions by these indices: per function the four pc-value tables
|
||||
// and the function itself, as cmd/asm lays them out.
|
||||
type npSym struct {
|
||||
sym goSym
|
||||
data []byte
|
||||
}
|
||||
var nps []npSym
|
||||
type pcRefs struct{ sp, file, line, inl int }
|
||||
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||
fnNpIdx := make([]int, len(img.Funcs))
|
||||
for i, fn := range img.Funcs {
|
||||
tables := []struct {
|
||||
data []byte
|
||||
dst *int
|
||||
}{
|
||||
{pcspTable(fn, minLC), &pcIdx[i].sp},
|
||||
{pcValueFlat(0, fn.Size, minLC), &pcIdx[i].file},
|
||||
{pcValueFlat(int32(fn.Line), fn.Size, minLC), &pcIdx[i].line},
|
||||
{pcValueFlat(-1, fn.Size, minLC), &pcIdx[i].inl},
|
||||
}
|
||||
for _, t := range tables {
|
||||
*t.dst = len(nps)
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||
data: t.data,
|
||||
})
|
||||
}
|
||||
name := fn.Name
|
||||
abi := uint16(0)
|
||||
if fn.Static {
|
||||
abi = symABIStatic
|
||||
} else {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
flag := uint8(0)
|
||||
if fn.NoSplit {
|
||||
flag |= symFlagNoSplit
|
||||
}
|
||||
fnNpIdx[i] = len(nps)
|
||||
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||
for _, r := range fn.Relocs {
|
||||
// Only the amd64 encoder resolves file-local static symbols
|
||||
// into a disp32 field at assemble time; GOOBJ must leave that
|
||||
// field zero for the linker to fill. The RISC-V and LoongArch
|
||||
// encoders emit zero immediates with a relocation instead, and
|
||||
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||
// zeroing r.Off would erase the opcode/register bits the linker
|
||||
// preserves when it patches only the immediate.
|
||||
if r.Kind != RelPCRel32 && r.Kind != RelCall {
|
||||
continue
|
||||
}
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||
}
|
||||
}
|
||||
nps = append(nps, npSym{
|
||||
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, size: uint32(fn.Size)},
|
||||
data: code,
|
||||
})
|
||||
}
|
||||
|
||||
// The package definitions: the GLOBL symbols, then, per function, the
|
||||
// FuncInfo and the two DWARF symbols (the .debug_line program and the
|
||||
// subprogram DIE). defIdx maps a GLOBL's bare name to its definition
|
||||
// index for the code relocations.
|
||||
var defs []goSym
|
||||
var defData [][]byte
|
||||
defIdx := map[string]int{}
|
||||
for _, d := range img.DataSyms {
|
||||
name := d.Name
|
||||
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 {
|
||||
flag = symFlagDupok
|
||||
}
|
||||
abi := uint16(0)
|
||||
if d.Static {
|
||||
abi = symABIStatic
|
||||
}
|
||||
defIdx[d.Name] = len(defs)
|
||||
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, size: uint32(d.Size)})
|
||||
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||
}
|
||||
fnFiIdx := make([]int, len(img.Funcs))
|
||||
fnLinesIdx := make([]int, len(img.Funcs))
|
||||
fnDIEIdx := make([]int, len(img.Funcs))
|
||||
var dwarfRelocs []dwarfRelocSet
|
||||
for i, fn := range img.Funcs {
|
||||
data := marshalFuncInfo(fn)
|
||||
fnFiIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
||||
defData = append(defData, data)
|
||||
|
||||
name := fn.Name
|
||||
if !fn.Static {
|
||||
name = pkgPath + "." + name
|
||||
}
|
||||
|
||||
// The DWARF symbols: the .debug_line state-machine program and the
|
||||
// subprogram DIE, both referencing the function by its non-package
|
||||
// index (package definitions, like cmd/asm's).
|
||||
lines, lrel := goobjDwarfLines(fn, fnNpIdx[i])
|
||||
fnLinesIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDWARFLINES, size: uint32(len(lines))})
|
||||
defData = append(defData, lines)
|
||||
die, drel := goobjDwarfInfo(fn, name, fnNpIdx[i])
|
||||
fnDIEIdx[i] = len(defs)
|
||||
defs = append(defs, goSym{typ: kindSDWARFFCN, size: uint32(len(die))})
|
||||
defData = append(defData, die)
|
||||
dwarfRelocs = append(dwarfRelocs,
|
||||
dwarfRelocSet{si: fnLinesIdx[i], relocs: lrel},
|
||||
dwarfRelocSet{si: fnDIEIdx[i], relocs: drel},
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
var extPkgTable []string
|
||||
var extPkgIdx map[string]int
|
||||
var extSymIdx map[string]int
|
||||
if len(img.Externals) > 0 {
|
||||
// The morestack call is a builtin reference, not a resolved external.
|
||||
var need []string
|
||||
for _, n := range img.Externals {
|
||||
if n == goobjBuiltinMorestack {
|
||||
continue
|
||||
}
|
||||
need = append(need, n)
|
||||
}
|
||||
if len(need) > 0 {
|
||||
var err error
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Relocations, per defined symbol in definition order (package defs,
|
||||
// then non-package defs).
|
||||
nsyms := len(defs) + len(nps)
|
||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||
for i, fn := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
typ, size := relocField(r)
|
||||
if r.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)
|
||||
if pkg == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name)
|
||||
}
|
||||
pIdx, ok := extPkgIdx[pkg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg)
|
||||
}
|
||||
sIdx, ok := extSymIdx[pkg+"·"+name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
pkg := uint32(pkgIdxSelf)
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
// A call to a TEXT function of the same file references the
|
||||
// non-package definition table.
|
||||
ni, isText := textNpIdx[r.Name]
|
||||
if !isText || !isCallReloc(r.Kind) {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
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:], 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 {
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(r.off))
|
||||
rec[4] = r.siz
|
||||
binary.LittleEndian.PutUint16(rec[5:], r.typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.add))
|
||||
binary.LittleEndian.PutUint32(rec[15:], r.pkg)
|
||||
binary.LittleEndian.PutUint32(rec[19:], r.sym)
|
||||
symRelocs[ds.si] = append(symRelocs[ds.si], rec[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Aux entries per function: FuncInfo, the DWARF symbols, then the four
|
||||
// pc tables. References into the non-package table use pkgIdxNone.
|
||||
symAux := make([][]byte, nsyms)
|
||||
for i := range img.Funcs {
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
aux := func(typ uint8, pkg, idx uint32) {
|
||||
var rec [9]byte
|
||||
rec[0] = typ
|
||||
binary.LittleEndian.PutUint32(rec[1:], pkg)
|
||||
binary.LittleEndian.PutUint32(rec[5:], idx)
|
||||
symAux[si] = append(symAux[si], rec[:]...)
|
||||
}
|
||||
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
||||
aux(auxDwarfInfo, pkgIdxSelf, uint32(fnDIEIdx[i]))
|
||||
aux(auxDwarfLines, pkgIdxSelf, uint32(fnLinesIdx[i]))
|
||||
// The pc-table references are 0-based within the non-package
|
||||
// definitions; the loader adds the package-definition count itself.
|
||||
aux(auxPcsp, pkgIdxNone, uint32(pcIdx[i].sp))
|
||||
aux(auxPcfile, pkgIdxNone, uint32(pcIdx[i].file))
|
||||
aux(auxPcline, pkgIdxNone, uint32(pcIdx[i].line))
|
||||
aux(auxPcinline, pkgIdxNone, uint32(pcIdx[i].inl))
|
||||
}
|
||||
|
||||
// The string table. Absolute offsets: it starts right after the
|
||||
// 96-byte header (magic, fingerprint, flags, the 19 block offsets).
|
||||
const headerSize = 8 + 8 + 4 + 4*(blkEnd+1)
|
||||
var strTab []byte
|
||||
strOff := map[string]uint32{}
|
||||
addStr := func(s string) {
|
||||
if _, ok := strOff[s]; ok {
|
||||
return
|
||||
}
|
||||
strOff[s] = uint32(headerSize + len(strTab))
|
||||
strTab = append(strTab, s...)
|
||||
}
|
||||
addStr("")
|
||||
addStr(srcPath)
|
||||
for _, s := range defs {
|
||||
addStr(s.name)
|
||||
}
|
||||
for _, s := range nps {
|
||||
addStr(s.sym.name)
|
||||
}
|
||||
stringRef := func(b []byte, s string) []byte {
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(len(s)))
|
||||
return binary.LittleEndian.AppendUint32(b, strOff[s])
|
||||
}
|
||||
|
||||
// Serialise the block bodies.
|
||||
var symdefBlk, npdefBlk []byte
|
||||
for _, s := range defs {
|
||||
symdefBlk = s.append(symdefBlk, strOff)
|
||||
}
|
||||
for _, s := range nps {
|
||||
npdefBlk = s.sym.append(npdefBlk, strOff)
|
||||
}
|
||||
|
||||
// Package index table: index 0 is the dummy invalid package.
|
||||
// External packages follow, in pkgIdx order.
|
||||
for _, pkg := range extPkgTable {
|
||||
addStr(pkg)
|
||||
}
|
||||
pkgIdxBlk := stringRef(nil, "") // index 0: dummy
|
||||
for _, pkg := range extPkgTable {
|
||||
pkgIdxBlk = stringRef(pkgIdxBlk, pkg)
|
||||
}
|
||||
fileBlk := stringRef(nil, srcPath)
|
||||
|
||||
var relocBlk, auxBlk, dataBlk []byte
|
||||
relocIdxBlk := make([]byte, 0, 4*(nsyms+1))
|
||||
auxIdxBlk := make([]byte, 0, 4*(nsyms+1))
|
||||
dataIdxBlk := make([]byte, 0, 4*(nsyms+1))
|
||||
var nr, na, nd uint32
|
||||
for si := range nsyms {
|
||||
relocIdxBlk = binary.LittleEndian.AppendUint32(relocIdxBlk, nr)
|
||||
auxIdxBlk = binary.LittleEndian.AppendUint32(auxIdxBlk, na)
|
||||
dataIdxBlk = binary.LittleEndian.AppendUint32(dataIdxBlk, nd)
|
||||
relocBlk = append(relocBlk, symRelocs[si]...)
|
||||
auxBlk = append(auxBlk, symAux[si]...)
|
||||
var d []byte
|
||||
if si < len(defData) {
|
||||
d = defData[si]
|
||||
} else {
|
||||
d = nps[si-len(defData)].data
|
||||
}
|
||||
dataBlk = append(dataBlk, d...)
|
||||
nr += uint32(len(symRelocs[si])) / 23
|
||||
na += uint32(len(symAux[si])) / 9
|
||||
nd += uint32(len(d))
|
||||
}
|
||||
relocIdxBlk = binary.LittleEndian.AppendUint32(relocIdxBlk, nr)
|
||||
auxIdxBlk = binary.LittleEndian.AppendUint32(auxIdxBlk, na)
|
||||
dataIdxBlk = binary.LittleEndian.AppendUint32(dataIdxBlk, nd)
|
||||
|
||||
blocks := [blkEnd][]byte{
|
||||
blkPkgIdx: pkgIdxBlk,
|
||||
blkFile: fileBlk,
|
||||
blkSymdef: symdefBlk,
|
||||
blkNonpkgdef: npdefBlk,
|
||||
blkRelocIdx: relocIdxBlk,
|
||||
blkAuxIdx: auxIdxBlk,
|
||||
blkDataIdx: dataIdxBlk,
|
||||
blkReloc: relocBlk,
|
||||
blkAux: auxBlk,
|
||||
blkData: dataBlk,
|
||||
}
|
||||
|
||||
// Assemble the payload: header (offsets filled once known), string
|
||||
// table, blocks in order.
|
||||
payload := make([]byte, headerSize)
|
||||
copy(payload, goobjMagic)
|
||||
// The fingerprint stays zero, as cmd/asm leaves it.
|
||||
binary.LittleEndian.PutUint32(payload[16:], 4) // ObjFlagFromAssembly
|
||||
off := uint32(headerSize + len(strTab))
|
||||
for i := range blkEnd {
|
||||
binary.LittleEndian.PutUint32(payload[20+4*i:], off)
|
||||
off += uint32(len(blocks[i]))
|
||||
}
|
||||
binary.LittleEndian.PutUint32(payload[20+4*blkEnd:], off)
|
||||
payload = append(payload, strTab...)
|
||||
for _, blk := range blocks {
|
||||
payload = append(payload, blk...)
|
||||
}
|
||||
|
||||
out := make([]byte, 0, len(pre)+len(payload))
|
||||
out = append(out, pre...)
|
||||
return append(out, payload...), nil
|
||||
}
|
||||
|
||||
// marshalFuncInfo serialises a function's goobj.FuncInfo: sizes, flags,
|
||||
// start line, the one-element file table and an empty inline tree.
|
||||
func marshalFuncInfo(fn FuncLayout) []byte {
|
||||
flag := uint8(funcFlagAsm)
|
||||
if fn.SPWrite {
|
||||
flag |= funcFlagSPWrite
|
||||
}
|
||||
b := make([]byte, 0, 28)
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(fn.Args))
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(fn.Frame))
|
||||
b = append(b, 0, flag, 0, 0) // FuncID normal, flags, padding
|
||||
b = binary.LittleEndian.AppendUint32(b, uint32(int32(fn.Line)))
|
||||
b = binary.LittleEndian.AppendUint32(b, 1) // one file
|
||||
b = binary.LittleEndian.AppendUint32(b, 0) // file index 0
|
||||
b = binary.LittleEndian.AppendUint32(b, 0) // no inline tree
|
||||
return b
|
||||
}
|
||||
|
||||
// pcValueFlat encodes a pc-value table holding v over the whole function.
|
||||
// The pc deltas are in MinLC units (the runtime scales them by the
|
||||
// architecture's minimum instruction length).
|
||||
func pcValueFlat(v int32, size, minLC int) []byte {
|
||||
// The table is delta-encoded from an implicit value of -1: a varint
|
||||
// value delta, an unsigned pc delta to the end, and a zero terminator.
|
||||
out := binary.AppendVarint(nil, int64(v)+1)
|
||||
out = binary.AppendUvarint(out, uint64(size/minLC))
|
||||
return append(out, 0)
|
||||
}
|
||||
|
||||
// pcspTable encodes the stack-adjustment table: the SP delta in effect at
|
||||
// every pc, from the function's prologue and epilogue boundaries.
|
||||
func pcspTable(fn FuncLayout, minLC int) []byte {
|
||||
if len(fn.Spadj) == 0 {
|
||||
return pcValueFlat(0, fn.Size, minLC)
|
||||
}
|
||||
pts := make([]SpadjStep, 0, len(fn.Spadj)+1)
|
||||
pts = append(pts, SpadjStep{PC: 0, Value: 0})
|
||||
pts = append(pts, fn.Spadj...)
|
||||
out := binary.AppendVarint(nil, int64(pts[0].Value)+1)
|
||||
cur, old := pts[0].PC, pts[0].Value
|
||||
for _, p := range pts[1:] {
|
||||
out = binary.AppendUvarint(out, uint64((p.PC-cur)/minLC))
|
||||
out = binary.AppendVarint(out, int64(p.Value-old))
|
||||
cur, old = p.PC, p.Value
|
||||
}
|
||||
out = binary.AppendUvarint(out, uint64((fn.Size-cur)/minLC))
|
||||
return append(out, 0)
|
||||
}
|
||||
|
||||
// toolchainObjectPreamble returns the "go object ...\n!\n" header the
|
||||
// installed go tool asm writes, captured by assembling a one-instruction
|
||||
// probe. The linker compares this string verbatim against its own, so it
|
||||
// must come from the toolchain itself, not be reconstructed.
|
||||
var (
|
||||
preambleOnce sync.Once
|
||||
preamble []byte
|
||||
preambleErr error
|
||||
)
|
||||
|
||||
func toolchainObjectPreamble() ([]byte, error) {
|
||||
preambleOnce.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble")
|
||||
if err != nil {
|
||||
preambleErr = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_amd64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleErr = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=amd64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleErr = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleErr = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preamble = data[:i+3]
|
||||
})
|
||||
return preamble, preambleErr
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// This file generates the per-function DWARF symbols the linker's DWARF
|
||||
// pass requires of an assembly object, byte-identical to what cmd/asm
|
||||
// emits: the .debug_line state-machine program (SDWARFLINES) and the
|
||||
// subprogram DIE (SDWARFFCN). The linker copies the DIE and line-program
|
||||
// bytes verbatim into .debug_info and .debug_line, fixing up their
|
||||
// relocations, so the formats here must match cmd/internal/dwarf's
|
||||
// DW_ABRV_FUNCTION and generateDebugLinesSymbol exactly.
|
||||
//
|
||||
// DWARF5 is assumed throughout (the toolchain's default on Linux and the
|
||||
// other non-Darwin targets gasm supports).
|
||||
|
||||
// Line-program parameters (cmd/internal/obj/dwarf.go).
|
||||
const (
|
||||
dwLineBase = -4
|
||||
dwLineRange = 10
|
||||
dwOpcodeBase = 11
|
||||
dwPCRange = (255 - dwOpcodeBase) / dwLineRange
|
||||
)
|
||||
|
||||
// goobjReloc is one relocation attached to an emitter-generated symbol
|
||||
// (the DWARF lines/info symbols), in goobj's on-disk encoding fields.
|
||||
type goobjReloc struct {
|
||||
off int32
|
||||
siz uint8
|
||||
typ uint16
|
||||
add int64
|
||||
pkg uint32
|
||||
sym uint32
|
||||
}
|
||||
|
||||
// goobjDwarfLines builds the function's .debug_line state-machine program:
|
||||
// an LNE_set_address extended opcode establishing the function's start
|
||||
// address (carrying the R_ADDR relocation), one row per source line
|
||||
// change across the function's instructions, an advance to the end of the
|
||||
// function and an end-of-sequence opcode. The linker appends these bytes
|
||||
// after the unit's line header, so they must start with the address and
|
||||
// leave the state machine terminated.
|
||||
func goobjDwarfLines(fn FuncLayout, fnNpIdx int) ([]byte, []goobjReloc) {
|
||||
// Rows: the prologue, if any, then the body instructions (fn.Lines
|
||||
// covers the body only). The first body offset > 0 means a prologue
|
||||
// precedes it; the toolchain reports the prologue on the TEXT line.
|
||||
pts := make([]LineEntry, 0, len(fn.Lines)+1)
|
||||
if len(fn.Lines) == 0 || fn.Lines[0].Offset > 0 {
|
||||
pts = append(pts, LineEntry{Offset: 0, Line: fn.Line})
|
||||
}
|
||||
pts = append(pts, fn.Lines...)
|
||||
|
||||
out := []byte{0, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0} // LNE_set_address, address zeroed
|
||||
relocs := []goobjReloc{{
|
||||
off: 3, siz: 8, typ: relocAddr,
|
||||
pkg: pkgIdxNone, sym: uint32(fnNpIdx),
|
||||
}}
|
||||
|
||||
// The state machine starts at line 1, pc 0 (function-relative); the
|
||||
// implicit initial pc is the function entry, so the first pc delta is
|
||||
// against 0.
|
||||
line := int64(1)
|
||||
pc := uint64(0)
|
||||
for _, p := range pts {
|
||||
if p.Line == 0 || uint64(p.Offset) < pc {
|
||||
continue
|
||||
}
|
||||
// Rows mark source-line changes only; the pc delta is measured from
|
||||
// the previous row, not the previous instruction.
|
||||
if int64(p.Line) == line {
|
||||
continue
|
||||
}
|
||||
deltaPC := uint64(p.Offset) - pc
|
||||
deltaLC := int64(p.Line) - line
|
||||
out = dwPutPCLCDelta(out, deltaPC, deltaLC)
|
||||
line, pc = int64(p.Line), uint64(p.Offset)
|
||||
}
|
||||
|
||||
// Cover the rest of the function and close the sequence.
|
||||
if end := uint64(fn.Size) - pc; end > 0 {
|
||||
out = append(out, 2) // DW_LNS_advance_pc
|
||||
out = binary.AppendUvarint(out, end)
|
||||
}
|
||||
out = append(out, 0, 1, 1) // LNE_end_sequence
|
||||
return out, relocs
|
||||
}
|
||||
|
||||
// dwPutPCLCDelta encodes one (pcDelta, lineDelta) step as the shortest
|
||||
// special opcode plus any standard-opcode remainder, exactly like
|
||||
// cmd/internal/obj's putpclcdelta.
|
||||
func dwPutPCLCDelta(b []byte, deltaPC uint64, deltaLC int64) []byte {
|
||||
opcode := dwSelectOpcode(deltaPC, deltaLC)
|
||||
deltaPC -= uint64((opcode - dwOpcodeBase) / dwLineRange)
|
||||
deltaLC -= (opcode-dwOpcodeBase)%dwLineRange + dwLineBase
|
||||
|
||||
// The remainder: standard opcodes first, then the special opcode
|
||||
// (which emits the row).
|
||||
if deltaPC != 0 {
|
||||
switch {
|
||||
case deltaPC <= uint64(dwPCRange):
|
||||
opcode -= dwLineRange * int64(uint64(dwPCRange)-deltaPC)
|
||||
b = append(b, 8) // DW_LNS_const_add_pc
|
||||
case (1<<14) <= deltaPC && deltaPC < (1<<16):
|
||||
b = append(b, 9) // DW_LNS_fixed_advance_pc
|
||||
b = binary.LittleEndian.AppendUint16(b, uint16(deltaPC))
|
||||
default:
|
||||
b = append(b, 2) // DW_LNS_advance_pc
|
||||
b = binary.AppendUvarint(b, deltaPC)
|
||||
}
|
||||
}
|
||||
if deltaLC != 0 {
|
||||
b = append(b, 3) // DW_LNS_advance_line
|
||||
b = binary.AppendVarint(b, deltaLC)
|
||||
}
|
||||
return append(b, byte(opcode))
|
||||
}
|
||||
|
||||
// dwSelectOpcode picks the special opcode for (deltaPC, deltaLC) per
|
||||
// cmd/internal/obj's putpclcdelta selection logic.
|
||||
func dwSelectOpcode(deltaPC uint64, deltaLC int64) int64 {
|
||||
switch {
|
||||
case deltaLC < dwLineBase:
|
||||
if deltaPC >= uint64(dwPCRange) {
|
||||
return dwOpcodeBase + dwLineRange*dwPCRange
|
||||
}
|
||||
return dwOpcodeBase + dwLineRange*int64(deltaPC)
|
||||
case deltaLC < dwLineBase+dwLineRange:
|
||||
if deltaPC >= uint64(dwPCRange) {
|
||||
op := int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*dwPCRange
|
||||
if op > 255 {
|
||||
op -= dwLineRange
|
||||
}
|
||||
return op
|
||||
}
|
||||
return int64(dwOpcodeBase) + (deltaLC - dwLineBase) + dwLineRange*int64(deltaPC)
|
||||
default:
|
||||
if deltaPC <= uint64(dwPCRange) {
|
||||
op := min(int64(dwOpcodeBase)+(dwLineRange-1)+dwLineRange*int64(deltaPC), 255)
|
||||
return op
|
||||
}
|
||||
switch deltaPC - uint64(dwPCRange) {
|
||||
case uint64(dwPCRange), (1 << 7) - 1, (1 << 16) - 1, (1 << 21) - 1,
|
||||
(1 << 28) - 1, (1 << 35) - 1, (1 << 42) - 1, (1 << 49) - 1,
|
||||
(1 << 56) - 1, (1 << 63) - 1:
|
||||
return 255
|
||||
default:
|
||||
// 250: the toolchain's "249" comment is stale.
|
||||
return dwOpcodeBase + dwLineRange*dwPCRange - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// goobjDwarfInfo builds the function's DWARF5 subprogram DIE (abbrev
|
||||
// DW_ABRV_FUNCTION): name, low_pc as a .debug_addr index (the
|
||||
// R_DWTXTADDR_U4 relocation), high_pc as the size, the call-frame-CFA
|
||||
// frame base, the decl file/line and the external flag. name is the
|
||||
// symbol's object name (package-qualified unless static).
|
||||
func goobjDwarfInfo(fn FuncLayout, name string, fnNpIdx int) ([]byte, []goobjReloc) {
|
||||
out := []byte{3} // DW_ABRV_FUNCTION
|
||||
out = append(out, name...)
|
||||
out = append(out, 0)
|
||||
|
||||
addrx := len(out)
|
||||
out = append(out, 0, 0, 0, 0) // DW_AT_low_pc: addrx slot, zeroed
|
||||
out = binary.AppendUvarint(out, uint64(fn.Size))
|
||||
out = append(out, 1, 0x9c) // DW_AT_frame_base: block1, DW_OP_call_frame_cfa
|
||||
out = binary.LittleEndian.AppendUint32(out, 1)
|
||||
out = binary.AppendUvarint(out, uint64(fn.Line))
|
||||
if fn.Static {
|
||||
out = append(out, 0)
|
||||
} else {
|
||||
out = append(out, 1) // DW_AT_external
|
||||
}
|
||||
out = append(out, 0) // end of children
|
||||
|
||||
relocs := []goobjReloc{{
|
||||
off: int32(addrx), siz: 4, typ: relocDWTXTADDRU4(),
|
||||
pkg: pkgIdxNone, sym: uint32(fnNpIdx),
|
||||
}}
|
||||
return out, relocs
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDWSelectOpcode checks the special-opcode selection against
|
||||
// hand-computed values for the boundary cases: line deltas below, inside
|
||||
// and above the line range, and pc deltas at and beyond PC_RANGE (24).
|
||||
func TestDWSelectOpcode(t *testing.T) {
|
||||
cases := []struct {
|
||||
deltaPC uint64
|
||||
deltaLC int64
|
||||
want int64
|
||||
}{
|
||||
{0, 2, 17}, // the common single-instruction step
|
||||
{0, -4, 11}, // deltaLC == LINE_BASE
|
||||
{0, -5, 11}, // deltaLC below LINE_BASE: opcode adds nothing
|
||||
{0, 6, 20}, // deltaLC == LINE_BASE+LINE_RANGE, remainder via advance_line
|
||||
{4, 1, 56}, // the 4-byte loong64 instruction step
|
||||
{23, 1, 246}, // deltaPC == PC_RANGE-1
|
||||
{24, 1, 246}, // deltaPC == PC_RANGE: wraps past 255
|
||||
{25, 1, 246}, // deltaPC past PC_RANGE (the const_add_pc remainder adjusts it later)
|
||||
{100, 1, 246},
|
||||
{151, 10, 255}, // deltaPC-PC_RANGE == (1<<7)-1, large line delta
|
||||
{100, 10, 250}, // deltaPC-PC_RANGE not on a switch boundary
|
||||
{23, 10, 250}, // large line delta inside PC_RANGE
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := dwSelectOpcode(c.deltaPC, c.deltaLC); got != c.want {
|
||||
t.Errorf("dwSelectOpcode(%d, %d) = %d, want %d", c.deltaPC, c.deltaLC, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decodeDWLineProgram decodes a .debug_line state-machine program (as
|
||||
// emitted by goobjDwarfLines) into (pc, line) rows.
|
||||
func decodeDWLineProgram(t *testing.T, b []byte) (pcs []uint64, lines []int64) {
|
||||
t.Helper()
|
||||
pc, line := uint64(0), int64(1)
|
||||
emit := func() {
|
||||
if len(pcs) == 0 || pcs[len(pcs)-1] != pc || lines[len(lines)-1] != line {
|
||||
pcs = append(pcs, pc)
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
advancePC := func(delta uint64) { pc += delta }
|
||||
advanceLine := func(delta int64) { line += delta }
|
||||
for i := 0; i < len(b); {
|
||||
op := b[i]
|
||||
i++
|
||||
switch {
|
||||
case op == 0: // extended opcode
|
||||
ln, n := binary.Uvarint(b[i:])
|
||||
i += n
|
||||
sub := b[i]
|
||||
i++
|
||||
_ = ln
|
||||
switch sub {
|
||||
case 2: // DW_LNE_set_address: 8-byte address
|
||||
pc = binary.LittleEndian.Uint64(b[i:])
|
||||
i += 8
|
||||
case 1: // DW_LNE_end_sequence
|
||||
// terminates the sequence; no new row
|
||||
}
|
||||
case op == 2: // DW_LNS_advance_pc
|
||||
v, n := binary.Uvarint(b[i:])
|
||||
i += n
|
||||
advancePC(v)
|
||||
case op == 3: // DW_LNS_advance_line
|
||||
v, n := binary.Varint(b[i:])
|
||||
i += n
|
||||
advanceLine(v)
|
||||
case op == 8: // DW_LNS_const_add_pc
|
||||
advancePC(uint64(dwPCRange))
|
||||
case op == 9: // DW_LNS_fixed_advance_pc
|
||||
advancePC(uint64(binary.LittleEndian.Uint16(b[i:])))
|
||||
i += 2
|
||||
case op >= dwOpcodeBase: // special opcode
|
||||
advancePC(uint64((int64(op) - dwOpcodeBase) / dwLineRange))
|
||||
advanceLine((int64(op)-dwOpcodeBase)%dwLineRange + dwLineBase)
|
||||
emit()
|
||||
}
|
||||
}
|
||||
return pcs, lines
|
||||
}
|
||||
|
||||
// TestGoobjDwarfLinesRows checks the emitted line program's rows for
|
||||
// synthetic functions: a zero-frame function with one instruction per
|
||||
// line, a framed function (the prologue row is prepended on the TEXT
|
||||
// line), instructions sharing a line, and a function with a large pc gap
|
||||
// (the const_add_pc remainder path).
|
||||
func TestGoobjDwarfLinesRows(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fn FuncLayout
|
||||
want [][2]int64 // (pc, line)
|
||||
}{
|
||||
{
|
||||
"one instruction per line",
|
||||
FuncLayout{Size: 20, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7}},
|
||||
},
|
||||
{
|
||||
"framed: prologue row on the TEXT line",
|
||||
FuncLayout{Size: 24, Line: 2, Lines: []LineEntry{
|
||||
{12, 3}, {16, 4},
|
||||
}},
|
||||
[][2]int64{{0, 2}, {12, 3}, {16, 4}},
|
||||
},
|
||||
{
|
||||
"instructions sharing a line fold into one row",
|
||||
FuncLayout{Size: 16, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {4, 3}, {8, 4}, {12, 4},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {8, 4}},
|
||||
},
|
||||
{
|
||||
"large gap crosses PC_RANGE",
|
||||
FuncLayout{Size: 60, Line: 2, Lines: []LineEntry{
|
||||
{0, 3}, {40, 4},
|
||||
}},
|
||||
[][2]int64{{0, 3}, {40, 4}},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
prog, relocs := goobjDwarfLines(c.fn, 0)
|
||||
if len(relocs) != 1 || relocs[0].off != 3 || relocs[0].siz != 8 || relocs[0].typ != relocAddr || relocs[0].sym != 0 {
|
||||
t.Fatalf("relocs = %+v", relocs)
|
||||
}
|
||||
pcs, lines := decodeDWLineProgram(t, prog)
|
||||
if len(pcs) != len(c.want) {
|
||||
t.Fatalf("rows = %d (%v / %v), want %d", len(pcs), pcs, lines, len(c.want))
|
||||
}
|
||||
for i, w := range c.want {
|
||||
if pcs[i] != uint64(w[0]) || lines[i] != w[1] {
|
||||
t.Errorf("row %d = (%d, %d), want (%d, %d)", i, pcs[i], lines[i], w[0], w[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoobjDwarfInfo checks the subprogram DIE for an exported and a
|
||||
// static function: the abbrev, name, high_pc, frame base, decl file/line,
|
||||
// the external flag and the addrx relocation position.
|
||||
func TestGoobjDwarfInfo(t *testing.T) {
|
||||
fn := FuncLayout{Size: 20, Line: 2}
|
||||
die, relocs := goobjDwarfInfo(fn, "pkg.f", 3)
|
||||
want := []byte{
|
||||
0x03,
|
||||
'p', 'k', 'g', '.', 'f', 0,
|
||||
0, 0, 0, 0, // addrx slot at offset 7
|
||||
0x14, // high_pc: 20
|
||||
0x01, 0x9c, // frame_base
|
||||
0x01, 0, 0, 0, // decl_file 1
|
||||
0x02, // decl_line 2
|
||||
0x01, // external
|
||||
0x00, // end of children
|
||||
}
|
||||
if !bytes.Equal(die, want) {
|
||||
t.Errorf("DIE = %x, want %x", die, want)
|
||||
}
|
||||
if len(relocs) != 1 || relocs[0].off != 7 || relocs[0].siz != 4 || relocs[0].typ != relocDWTXTADDRU4() || relocs[0].sym != 3 {
|
||||
t.Errorf("relocs = %+v", relocs)
|
||||
}
|
||||
|
||||
// A static function carries no external flag and no package prefix.
|
||||
fn.Static = true
|
||||
die, _ = goobjDwarfInfo(fn, "f", 1)
|
||||
if die[len(die)-2] != 0 {
|
||||
t.Errorf("static external flag = %d, want 0", die[len(die)-2])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwPutPCLCDeltaRemainders checks the standard-opcode remainders:
|
||||
// const_add_pc and fixed_advance_pc after a special opcode.
|
||||
func TestDwPutPCLCDeltaRemainders(t *testing.T) {
|
||||
// deltaPC 25 past PC_RANGE: opcode 26 covers (1, 1), const_add_pc
|
||||
// covers the remaining 23 pc and 0 line.
|
||||
got := dwPutPCLCDelta(nil, 25, 1)
|
||||
if !bytes.Equal(got, []byte{8, 26}) {
|
||||
t.Errorf("25/1 = %x, want [8 1a]", got)
|
||||
}
|
||||
|
||||
// deltaPC 20000: opcode 246 covers 23, fixed_advance_pc covers the
|
||||
// remaining 19977.
|
||||
got = dwPutPCLCDelta(nil, 20000, 1)
|
||||
if got[0] != 9 || binary.LittleEndian.Uint16(got[1:]) != 19977 || got[3] != 246 {
|
||||
t.Errorf("20000/1 = %x, want fixed_advance_pc 19977 then 246", got)
|
||||
}
|
||||
|
||||
// Line remainder: deltaLC 10 leaves 5 past the opcode's reach, encoded
|
||||
// as advance_line 5 (zigzag 0x0a) before opcode 250.
|
||||
got = dwPutPCLCDelta(nil, 23, 10)
|
||||
if !bytes.Equal(got, []byte{3, 0x0a, 250}) {
|
||||
t.Errorf("23/10 = %x, want [03 0a fa]", got)
|
||||
}
|
||||
|
||||
// Negative line remainder: deltaLC -5 leaves advance_line -1 (zigzag
|
||||
// 0x01) after opcode 11.
|
||||
got = dwPutPCLCDelta(nil, 0, -5)
|
||||
if !bytes.Equal(got, []byte{3, 1, 11}) {
|
||||
t.Errorf("0/-5 = %x, want [03 01 0b]", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// exportPath returns the export file path for a given import path by running
|
||||
// "go list -export". The result is cached so repeated calls for the same
|
||||
// package are fast.
|
||||
func exportPath(importPath string) (string, error) {
|
||||
cmd := exec.Command("go", "list", "-json", "-export", importPath)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("go list %s: %w", importPath, err)
|
||||
}
|
||||
// Quick JSON extraction: find "Export": "…"
|
||||
const key = `"Export": "`
|
||||
i := bytes.Index(out, []byte(key))
|
||||
if i < 0 {
|
||||
return "", fmt.Errorf("go list %s: no Export field", importPath)
|
||||
}
|
||||
start := i + len(key)
|
||||
end := bytes.IndexByte(out[start:], '"')
|
||||
if end < 0 {
|
||||
return "", fmt.Errorf("go list %s: malformed Export field", importPath)
|
||||
}
|
||||
return string(out[start : start+end]), nil
|
||||
}
|
||||
|
||||
// resolveExternalGOOBJ resolves a set of external symbol references into
|
||||
// (package index, symbol index) pairs suitable for GOOBJ emission.
|
||||
//
|
||||
// refs maps package import paths to the symbol names referenced from that
|
||||
// package. The returned pkgIdx maps each import path to its position in
|
||||
// the blkPkgIdx table, 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)
|
||||
|
||||
// Assign package indices in sorted order for determinism.
|
||||
packages := sortedPkgRefs(refs)
|
||||
|
||||
for i, pkg := range packages {
|
||||
// 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
|
||||
}
|
||||
data, err := os.ReadFile(exp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
gobj, err := extractGOOBJ(data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", pkg.path, err)
|
||||
}
|
||||
for _, name := range pkg.syms {
|
||||
idx := gobj.findSymbol(pkg.path, name)
|
||||
if idx < 0 {
|
||||
return nil, nil, fmt.Errorf("symbol %s·%s not found in export data of %s", pkg.path, name, pkg.path)
|
||||
}
|
||||
symIdx[pkg.path+"·"+name] = idx
|
||||
}
|
||||
}
|
||||
return pkgIdx, symIdx, nil
|
||||
}
|
||||
|
||||
type pkgRef struct {
|
||||
path string
|
||||
syms []string
|
||||
}
|
||||
|
||||
func sortedPkgRefs(refs map[string][]string) []pkgRef {
|
||||
var pkgs []pkgRef
|
||||
for pkg, syms := range refs {
|
||||
pkgs = append(pkgs, pkgRef{pkg, syms})
|
||||
}
|
||||
// Simple insertion sort, the list is tiny (usually 1-3 packages).
|
||||
for i := 1; i < len(pkgs); i++ {
|
||||
for j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- {
|
||||
pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1]
|
||||
}
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// extractGOOBJ finds the GOOBJ data in an ar archive and returns a parsed
|
||||
// goobjFile. The archive member _go_.o contains the "go object …\n!\n"
|
||||
// preamble followed by the GOOBJ payload; __.PKGDEF is the compiler export
|
||||
// data (type information) and is not the GOOBJ object.
|
||||
func extractGOOBJ(data []byte) (*goobjFile, error) {
|
||||
if len(data) < 8 || string(data[:8]) != "!<arch>\n" {
|
||||
return nil, fmt.Errorf("not an ar archive")
|
||||
}
|
||||
pos := 8
|
||||
for pos+60 <= len(data) {
|
||||
hdr := data[pos : pos+60]
|
||||
pos += 60
|
||||
|
||||
// Parse ar header fields.
|
||||
name := strings.TrimRight(string(hdr[:16]), " /")
|
||||
size := parseArDecimal(hdr[48:58])
|
||||
if size < 0 {
|
||||
return nil, fmt.Errorf("invalid ar header: bad size")
|
||||
}
|
||||
if pos+size > len(data) {
|
||||
return nil, fmt.Errorf("ar entry %q extends past end of file", name)
|
||||
}
|
||||
body := data[pos : pos+size]
|
||||
pos += size
|
||||
// ar pads to even bytes.
|
||||
if pos%2 != 0 {
|
||||
pos++
|
||||
}
|
||||
|
||||
if name == "_go_.o" {
|
||||
return parseGOOBJ(body)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("archive contains no _go_.o member")
|
||||
}
|
||||
|
||||
// parseArDecimal parses a decimal number from a space-padded field.
|
||||
func parseArDecimal(b []byte) int {
|
||||
v := 0
|
||||
for _, c := range b {
|
||||
if c == ' ' {
|
||||
continue
|
||||
}
|
||||
if c < '0' || c > '9' {
|
||||
return -1
|
||||
}
|
||||
v = v*10 + int(c-'0')
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// goobjFile is a parsed GOOBJ file: the string table and the symbol-definition
|
||||
// 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
|
||||
hashed64 []byte // blkHashed64def raw block
|
||||
hashed []byte // blkHasheddef raw block
|
||||
npdef []byte // blkNonpkgdef raw block
|
||||
}
|
||||
|
||||
// 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 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
|
||||
for i, s := range f.defNames() {
|
||||
if s == qualified {
|
||||
return i
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// defNames returns names from blkSymdef only.
|
||||
func (f *goobjFile) defNames() []string {
|
||||
return f.readSymNames(f.symdef)
|
||||
}
|
||||
|
||||
// npdefNames returns names from blkNonpkgdef.
|
||||
func (f *goobjFile) npdefNames() []string {
|
||||
return f.readSymNames(f.npdef)
|
||||
}
|
||||
|
||||
// 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 = recSymSize
|
||||
if len(block) < recSize {
|
||||
return nil
|
||||
}
|
||||
n := len(block) / recSize
|
||||
names := make([]string, 0, n)
|
||||
for i := range n {
|
||||
rec := block[i*recSize : (i+1)*recSize]
|
||||
nameLen := binary.LittleEndian.Uint32(rec[0:4])
|
||||
nameOff := binary.LittleEndian.Uint32(rec[4:8])
|
||||
// nameOff is an absolute offset into the GOOBJ payload. The string
|
||||
// table we have starts at goobjHeaderSize, so we subtract that.
|
||||
if nameOff < goobjHeaderSize {
|
||||
continue
|
||||
}
|
||||
relOff := nameOff - goobjHeaderSize
|
||||
if relOff >= uint32(len(f.strTab)) || relOff+nameLen > uint32(len(f.strTab)) {
|
||||
continue
|
||||
}
|
||||
names = append(names, string(f.strTab[relOff:relOff+nameLen]))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
const goobjHeaderSize = 8 + 8 + 4 + 4*(blkEnd+1) // magic + fingerprint + flags + 19 block offsets
|
||||
|
||||
// parseGOOBJ parses a raw GOOBJ payload (the data after the "\n!\n" preamble).
|
||||
func parseGOOBJ(data []byte) (*goobjFile, error) {
|
||||
// Find the "\n!\n" separator.
|
||||
sep := []byte("\n!\n")
|
||||
i := bytes.Index(data, sep)
|
||||
if i < 0 {
|
||||
// Maybe the data has no preamble (e.g. a raw .o file).
|
||||
i = -3 // treat as if preamble starts before the data
|
||||
}
|
||||
payload := data[i+len(sep):]
|
||||
|
||||
if len(payload) < goobjHeaderSize {
|
||||
return nil, fmt.Errorf("GOOBJ payload too short (%d bytes)", len(payload))
|
||||
}
|
||||
if string(payload[:8]) != goobjMagic {
|
||||
return nil, fmt.Errorf("bad GOOBJ magic: %q", payload[:8])
|
||||
}
|
||||
|
||||
// Read block offsets. The header layout is:
|
||||
// [0:8] magic
|
||||
// [8:16] fingerprint
|
||||
// [16:20] flags
|
||||
// [20:96] 19 × uint32 offsets
|
||||
var offs [blkEnd + 1]uint32
|
||||
for i := 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),
|
||||
hashed64: blockSlice(payload, offs, blkHashed64def, blkHashed64def+1),
|
||||
hashed: blockSlice(payload, offs, blkHasheddef, blkHasheddef+1),
|
||||
npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1),
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// blockSlice extracts a block from the payload using its offset pair.
|
||||
func blockSlice(payload []byte, offs [blkEnd + 1]uint32, start, end int) []byte {
|
||||
if start < 0 || end > blkEnd || offs[end] < offs[start] {
|
||||
return nil
|
||||
}
|
||||
beg := offs[start]
|
||||
fin := offs[end]
|
||||
if int(fin) > len(payload) || int(beg) > int(fin) {
|
||||
return nil
|
||||
}
|
||||
return payload[beg:fin]
|
||||
}
|
||||
|
||||
// resolveExternalSymbols is the high-level entry point for GOOBJ emission.
|
||||
// Given a list of external symbol names (e.g. ["runtime·morestack",
|
||||
// "runtime·g0"]), it returns the package-index table entries and a map from
|
||||
// full symbol name to GOOBJ {pkgIdx, symIdx}.
|
||||
//
|
||||
// The package table entries should be written into blkPkgIdx, and the
|
||||
// returned indices should replace pkgIdxSelf / placeholder values in the
|
||||
// relocation records.
|
||||
func resolveExternalSymbols(externals []string) (pkgTable []string, pkgIdxMap map[string]int, symIdxMap map[string]int, err error) {
|
||||
// Group references by package.
|
||||
refs := make(map[string]map[string]bool)
|
||||
for _, full := range externals {
|
||||
pkg, name := splitQualified(full)
|
||||
if refs[pkg] == nil {
|
||||
refs[pkg] = make(map[string]bool)
|
||||
}
|
||||
refs[pkg][name] = true
|
||||
}
|
||||
|
||||
// Convert maps to slices.
|
||||
r := make(map[string][]string, len(refs))
|
||||
for pkg, names := range refs {
|
||||
for name := range names {
|
||||
r[pkg] = append(r[pkg], name)
|
||||
}
|
||||
}
|
||||
|
||||
pkgIdx1, symIdx1, err := resolveExternalGOOBJ(r)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Build the package table in pkgIdx order. 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-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),
|
||||
// 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 before, after, ok := strings.Cut(full, "\u00b7"); ok {
|
||||
return before, after
|
||||
}
|
||||
if before, after, ok := strings.Cut(full, "."); ok {
|
||||
return before, after
|
||||
}
|
||||
return "", full
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestReadRuntimeSymbols verifies the GOOBJ reader can extract and find
|
||||
// symbols from the runtime package's compiled archive.
|
||||
func TestReadRuntimeSymbols(t *testing.T) {
|
||||
exp, err := exportPath("runtime")
|
||||
if err != nil {
|
||||
t.Skipf("cannot find runtime export: %v (need Go toolchain)", err)
|
||||
}
|
||||
data, err := os.ReadFile(exp)
|
||||
if err != nil {
|
||||
t.Skipf("cannot read runtime export: %v", err)
|
||||
}
|
||||
gobj, err := extractGOOBJ(data)
|
||||
if err != nil {
|
||||
t.Fatalf("extractGOOBJ: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("runtime: %d symbols", len(gobj.symbols()))
|
||||
|
||||
// Verify we can find well-known runtime symbols.
|
||||
for _, tc := range []struct{ pkg, name string }{
|
||||
{"runtime", "g0"},
|
||||
{"runtime", "morestack"},
|
||||
{"runtime", "newstack"},
|
||||
} {
|
||||
idx := gobj.findSymbol(tc.pkg, tc.name)
|
||||
if idx < 0 {
|
||||
t.Errorf("findSymbol(%q, %q) = -1", tc.pkg, tc.name)
|
||||
} else {
|
||||
t.Logf("findSymbol(%q, %q) = %d", tc.pkg, tc.name, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveExternalSymbols verifies end-to-end resolution of external
|
||||
// symbol references.
|
||||
func TestResolveExternalSymbols(t *testing.T) {
|
||||
if 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")
|
||||
}
|
||||
|
||||
refs := map[string][]string{
|
||||
"runtime": {"g0"},
|
||||
}
|
||||
pkgIdx, symIdx, err := resolveExternalGOOBJ(refs)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveExternalGOOBJ: %v", err)
|
||||
}
|
||||
if len(pkgIdx) != 1 || pkgIdx["runtime"] != 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)
|
||||
}
|
||||
t.Logf("runtime·g0 → SymIdx=%d", symIdx["runtime·g0"])
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||||
// the emitter's output block by block.
|
||||
type goobjView struct {
|
||||
t *testing.T
|
||||
b []byte
|
||||
offs [blkEnd + 1]uint32
|
||||
strOff uint32
|
||||
}
|
||||
|
||||
func openGoobj(t *testing.T, data []byte) *goobjView {
|
||||
t.Helper()
|
||||
i := bytes.Index(data, []byte(goobjMagic))
|
||||
if i < 0 {
|
||||
t.Fatal("no GOOBJ magic in output")
|
||||
}
|
||||
v := &goobjView{t: t, b: data[i:], strOff: uint32(i + 96)}
|
||||
for j := 0; j <= blkEnd; j++ {
|
||||
v.offs[j] = binary.LittleEndian.Uint32(v.b[20+4*j:])
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *goobjView) blk(i int) []byte { return v.b[v.offs[i]:v.offs[i+1]] }
|
||||
|
||||
func (v *goobjView) str(off, ln uint32) string {
|
||||
return string(v.b[off : off+ln])
|
||||
}
|
||||
|
||||
type goobjSymView struct {
|
||||
name string
|
||||
abi uint16
|
||||
typ uint8
|
||||
flag uint8
|
||||
flag2 uint8
|
||||
size uint32
|
||||
align uint32
|
||||
}
|
||||
|
||||
func (v *goobjView) syms(i int) []goobjSymView {
|
||||
var out []goobjSymView
|
||||
for x := v.blk(i); len(x) >= 21; x = x[21:] {
|
||||
le := binary.LittleEndian
|
||||
out = append(out, goobjSymView{
|
||||
name: v.str(le.Uint32(x[4:]), le.Uint32(x[0:])),
|
||||
abi: le.Uint16(x[8:]),
|
||||
typ: x[10],
|
||||
flag: x[11],
|
||||
flag2: x[12],
|
||||
size: le.Uint32(x[13:]),
|
||||
align: le.Uint32(x[17:]),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestGOObjectStructure checks the emitted object's blocks against the
|
||||
// ground truth captured from go tool asm: the symbol tables, the FuncInfo
|
||||
// contents, the pc-value tables, the relocation and the aux wiring.
|
||||
func TestGOObjectStructure(t *testing.T) {
|
||||
f, errs := parser.Parse("t_amd64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), CX
|
||||
ADDQ CX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||||
VMOVDQU mask<>(SB), X0
|
||||
VPMOVMSKB X0, AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL mask<>(SB), RODATA, $16
|
||||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
obj, err := img.GOObject("testpkg", "t_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
|
||||
if flags := binary.LittleEndian.Uint32(v.b[16:]); flags != 4 {
|
||||
t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags)
|
||||
}
|
||||
|
||||
// Package defs: the static GLOBL, then per function the FuncInfo and the
|
||||
// two DWARF symbols (debug_line program, subprogram DIE).
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 7 {
|
||||
t.Fatalf("symdefs = %d, want 7", len(defs))
|
||||
}
|
||||
// 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 {
|
||||
t.Errorf("addq funcinfo symbol = %+v", defs[1])
|
||||
}
|
||||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||||
t.Errorf("addq lines symbol = %+v", defs[2])
|
||||
}
|
||||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||||
t.Errorf("addq DIE symbol = %+v", defs[3])
|
||||
}
|
||||
if defs[4].name != "" || defs[4].typ != kindSDATA || defs[4].size != 28 {
|
||||
t.Errorf("loadmask funcinfo symbol = %+v", defs[4])
|
||||
}
|
||||
|
||||
// Non-package defs: four pc tables and the function, per function.
|
||||
nps := v.syms(blkNonpkgdef)
|
||||
if len(nps) != 10 {
|
||||
t.Fatalf("nonpkgdefs = %d, want 10", len(nps))
|
||||
}
|
||||
fn := nps[4]
|
||||
if fn.name != "testpkg.addq" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 19 {
|
||||
t.Errorf("addq symbol = %+v", fn)
|
||||
}
|
||||
for i, s := range []int{0, 1, 2, 3, 5, 6, 7, 8} {
|
||||
if nps[s].typ != kindSRODATA || nps[s].align != 1 || nps[s].name != "" {
|
||||
t.Errorf("pc table %d = %+v", i, nps[s])
|
||||
}
|
||||
}
|
||||
|
||||
// FuncInfo: args 24, FuncFlag Asm, one file, no inline tree.
|
||||
le := binary.LittleEndian
|
||||
data := v.blk(blkData)
|
||||
didx := v.blk(blkDataIdx)
|
||||
fi := data[16:44]
|
||||
if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm ||
|
||||
le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 {
|
||||
t.Errorf("funcinfo bytes %x", fi)
|
||||
}
|
||||
|
||||
// The pc-value tables of addq (non-package indices 0-3, so global
|
||||
// indices 7-10): pcsp a flat zero over the whole function, pcinline a
|
||||
// flat -1, both with the pc delta in MinLC (1) units.
|
||||
pcsp := data[le.Uint32(didx[4*7:]):]
|
||||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||||
t.Errorf("pcsp = %x, want 021300", got)
|
||||
}
|
||||
pcinl := data[le.Uint32(didx[4*10:]):]
|
||||
if got := pcinl[:3]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
|
||||
t.Errorf("pcinline = %x, want 001300", got)
|
||||
}
|
||||
|
||||
// Relocations: the four DWARF address references (two per function, in
|
||||
// definition order), then the loadmask code's R_PCREL against the
|
||||
// GLOBL, with the field in the function code left zero. The loadmask
|
||||
// code's offset comes from the data index (7 defs + 9 non-package).
|
||||
relocs := v.blk(blkReloc)
|
||||
if len(relocs) != 5*23 {
|
||||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||||
}
|
||||
// addq's DWARF references (defs 2 and 3) against the function, which
|
||||
// is non-package index 4.
|
||||
lr := relocs[:23]
|
||||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||||
t.Errorf("addq lines reloc = %x", lr)
|
||||
}
|
||||
dr := relocs[23:46]
|
||||
if dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||||
t.Errorf("addq DIE reloc = %x", dr)
|
||||
}
|
||||
cr := relocs[4*23:]
|
||||
off := int32(le.Uint32(cr[0:]))
|
||||
if off != 4 || cr[4] != 4 || le.Uint16(cr[5:]) != relocPCRel ||
|
||||
le.Uint64(cr[7:]) != 0 || le.Uint32(cr[15:]) != pkgIdxSelf || le.Uint32(cr[19:]) != 0 {
|
||||
t.Errorf("loadmask reloc = %x", cr)
|
||||
}
|
||||
lm := le.Uint32(didx[4*16:])
|
||||
code := data[lm : lm+18]
|
||||
if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) {
|
||||
t.Errorf("relocated field = %x, want zeroed", code[4:8])
|
||||
}
|
||||
|
||||
// Aux wiring: FuncInfo, the two DWARF symbols (package symbols), then
|
||||
// the four pc tables (non-package symbols).
|
||||
auxs := v.blk(blkAux)
|
||||
if len(auxs) != 2*7*9 {
|
||||
t.Fatalf("aux = %d bytes, want 14 entries", len(auxs))
|
||||
}
|
||||
wantAux := []struct {
|
||||
typ uint8
|
||||
pkg uint32
|
||||
idx uint32
|
||||
}{
|
||||
{auxFuncInfo, pkgIdxSelf, 1},
|
||||
{auxDwarfInfo, pkgIdxSelf, 3},
|
||||
{auxDwarfLines, pkgIdxSelf, 2},
|
||||
{auxPcsp, pkgIdxNone, 0},
|
||||
{auxPcfile, pkgIdxNone, 1},
|
||||
{auxPcline, pkgIdxNone, 2},
|
||||
{auxPcinline, pkgIdxNone, 3},
|
||||
{auxFuncInfo, pkgIdxSelf, 4},
|
||||
{auxDwarfInfo, pkgIdxSelf, 6},
|
||||
{auxDwarfLines, pkgIdxSelf, 5},
|
||||
{auxPcsp, pkgIdxNone, 5},
|
||||
{auxPcfile, pkgIdxNone, 6},
|
||||
{auxPcline, pkgIdxNone, 7},
|
||||
{auxPcinline, pkgIdxNone, 8},
|
||||
}
|
||||
for i, w := range wantAux {
|
||||
e := auxs[i*9:]
|
||||
if e[0] != w.typ || le.Uint32(e[1:]) != w.pkg || le.Uint32(e[5:]) != w.idx {
|
||||
t.Errorf("aux[%d] = {%d,%d,%d}, want {%d,%d,%d}", i, e[0], le.Uint32(e[1:]), le.Uint32(e[5:]), w.typ, w.pkg, w.idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decodePCValues decodes a pc-value table into (pc, value) steps. The
|
||||
// table ends with a final unsigned pc delta covering the rest of the
|
||||
// function, followed by a zero byte that carries no value delta.
|
||||
func decodePCValues(b []byte) (pcs, vals []int64) {
|
||||
val, n := binary.Varint(b)
|
||||
b = b[n:]
|
||||
val-- // the first delta is against the implicit -1
|
||||
var pc int64
|
||||
pcs = append(pcs, pc)
|
||||
vals = append(vals, val)
|
||||
for {
|
||||
pcd, n := binary.Uvarint(b)
|
||||
b = b[n:]
|
||||
if pcd == 0 { // zero pc delta terminates the table
|
||||
break
|
||||
}
|
||||
pc += int64(pcd)
|
||||
if len(b) == 1 && b[0] == 0 { // final coverage, no value change
|
||||
break
|
||||
}
|
||||
vd, n := binary.Varint(b)
|
||||
b = b[n:]
|
||||
val += vd
|
||||
pcs = append(pcs, pc)
|
||||
vals = append(vals, val)
|
||||
}
|
||||
return pcs, vals
|
||||
}
|
||||
|
||||
// TestGOObjectPcspFrame checks the pcsp table of a frame-pointer function:
|
||||
// the prologue raises the stack delta to 8+frame, the RET's epilogue
|
||||
// restores it to zero.
|
||||
func TestGOObjectPcspFrame(t *testing.T) {
|
||||
f, errs := parser.Parse("frame_amd64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·framed(SB), NOSPLIT, $8-0
|
||||
MOVQ BP, AX
|
||||
RET
|
||||
`)
|
||||
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]
|
||||
pcs, vals := decodePCValues(pcspTable(fn, 1))
|
||||
// Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change),
|
||||
// SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds
|
||||
// ADDQ $8, SP (+8) then POPQ BP (0).
|
||||
wantPCs := []int64{0, 1, 8}
|
||||
wantVals := []int64{0, 8, 16}
|
||||
if len(pcs) < len(wantPCs) {
|
||||
t.Fatalf("pcsp pcs = %v vals = %v", pcs, vals)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
// The last two steps unwind the epilogue to zero.
|
||||
n := len(pcs)
|
||||
if vals[n-1] != 0 || vals[n-2] != 8 {
|
||||
t.Errorf("epilogue steps = %v %v, want …8, 0", pcs, vals)
|
||||
}
|
||||
// The table covers the whole function.
|
||||
if last := pcs[n-1]; last >= int64(fn.Size) {
|
||||
t.Errorf("last pc %d beyond function size %d", last, fn.Size)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectExternalRejected checks that a reference to a symbol no GLOBL
|
||||
// defines is reported: GOOBJ emission resolves only file-local symbols so
|
||||
// far.
|
||||
func TestGOObjectExternalRejected(t *testing.T) {
|
||||
f, errs := parser.Parse("ext_amd64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·useext(SB), NOSPLIT, $0-8
|
||||
MOVQ elsewhere(SB), AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if _, err := img.GOObject("p", "ext_amd64.s"); err == nil || !strings.Contains(err.Error(), "external") {
|
||||
t.Errorf("error = %v, want an external-symbol error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// binary the Go assembler produced. Skipped when no Go toolchain is
|
||||
// available.
|
||||
func TestGOObjectLinkAndRun(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
const asmSrc = `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), CX
|
||||
ADDQ CX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
|
||||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||||
VMOVDQU mask<>(SB), X0
|
||||
VPMOVMSKB X0, AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
|
||||
GLOBL mask<>(SB), RODATA, $16
|
||||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||||
`
|
||||
const mainSrc = `package main
|
||||
|
||||
func addq(a, b int64) int64
|
||||
func loadmask() int64
|
||||
|
||||
func main() {
|
||||
println(addq(41, 1))
|
||||
println(loadmask())
|
||||
}
|
||||
`
|
||||
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 goobjtest\n\ngo 1.27\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Baseline build with the toolchain's assembler; keep the work
|
||||
// directory and the commands the build used.
|
||||
cmd := exec.Command(goBin, "build", "-x", "-work", "-o", "app", ".")
|
||||
cmd.Dir = dir
|
||||
buildLog, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work string
|
||||
var asmObj, pkgArch, linkLine 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, "-o ") && 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.Fatalf("could not locate the build steps:\n%s", buildLog)
|
||||
}
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||||
|
||||
// The baseline's answer.
|
||||
baseOut, err := exec.Command(filepath.Join(dir, "app")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("run baseline: %v\n%s", err, baseOut)
|
||||
}
|
||||
|
||||
// Assemble the same source with gasm and swap the object in.
|
||||
pf, perrs := parser.Parse(filepath.Join(dir, "main_amd64.s"), asmSrc)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs)
|
||||
}
|
||||
img, err := AssembleFile(pf)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
obj, err := img.GOObject("main", filepath.Join(dir, "main_amd64.s"))
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
|
||||
// Rebuild the package archive with our object in place of the
|
||||
// toolchain's (go tool pack has no replace-in-place that dedupes, so
|
||||
// extract, substitute and repack). The archive member holding the
|
||||
// assembler's output is named after the asm object file, e.g.
|
||||
// main_amd64.o.
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// Link with our archive. The link line carries a GOROOT assignment
|
||||
// and $WORK placeholders; run it through the shell with the
|
||||
// GOEXPERIMENT the toolchain expects (the linker compares the object
|
||||
// header against its own, experiments included).
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)))
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
got, err := exec.Command(filepath.Join(dir, "app2")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("run gasm-linked binary: %v\n%s", err, got)
|
||||
}
|
||||
if !bytes.Equal(got, baseOut) {
|
||||
t.Errorf("gasm-linked output %q, want baseline %q", got, baseOut)
|
||||
}
|
||||
}
|
||||
|
||||
// fieldAfter returns the whitespace-delimited field following the first
|
||||
// occurrence of flag in line.
|
||||
func fieldAfter(line, flag string) string {
|
||||
fields := strings.Fields(line)
|
||||
for i, f := range fields {
|
||||
if f == flag && i+1 < len(fields) {
|
||||
return fields[i+1]
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectAARCH64 emits a GOOBJ object file for AArch64. The layout is
|
||||
// the shared one in goobj.go, the toolchain preamble, the go120ld header
|
||||
// with its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays, with the arm64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and 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
|
||||
}
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// arm64 relocation types (cmd/internal/objabi).
|
||||
const (
|
||||
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
|
||||
// the installed go tool asm writes for arm64, captured by assembling a
|
||||
// one-instruction probe.
|
||||
var (
|
||||
preambleAARCH64Once sync.Once
|
||||
preambleAARCH64 []byte
|
||||
preambleAARCH64Err error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleAARCH64() ([]byte, error) {
|
||||
preambleAARCH64Once.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleAARCH64Err = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-arm64")
|
||||
if err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_arm64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleAARCH64Err = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleAARCH64Err = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleAARCH64Err = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleAARCH64 = data[:i+3]
|
||||
})
|
||||
return preambleAARCH64, preambleAARCH64Err
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectLOONG64 emits a GOOBJ object file for LoongArch. The layout is
|
||||
// the shared one in goobj.go, the toolchain preamble, the go120ld header
|
||||
// with its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays, with the loong64 preamble, the MinLC of 4
|
||||
// for the pc-value deltas, and R_LOONG64_ADDR_HI/LO relocation types for
|
||||
// the pcalau12i+addi.d address pairs.
|
||||
func (img *Image) GOObjectLOONG64(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreambleLOONG64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) {
|
||||
// A pcalau12i+addi.d pair: the high part carries
|
||||
// R_LOONG64_ADDR_HI, the low part R_LOONG64_ADDR_LO; 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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Loong64 relocation types (cmd/internal/objabi). R_LOONG64_ADDR_HI
|
||||
// resolves the high 20 bits of a PC-relative address into pcalau12i;
|
||||
// R_LOONG64_ADDR_LO the low 12 bits into addi.d/ld/st.
|
||||
const (
|
||||
relocLoong64AddrHi = 77 // R_LOONG64_ADDR_HI
|
||||
relocLoong64AddrLo = 78 // R_LOONG64_ADDR_LO
|
||||
relocCallLoong64 = 84 // R_CALLLOONG64
|
||||
)
|
||||
|
||||
// toolchainObjectPreambleLOONG64 returns the "go object ...\n!\n" header
|
||||
// the installed go tool asm writes for loong64, captured by assembling a
|
||||
// one-instruction probe (see toolchainObjectPreamble).
|
||||
var (
|
||||
preambleLOONG64Once sync.Once
|
||||
preambleLOONG64 []byte
|
||||
preambleLOONG64Err error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleLOONG64() ([]byte, error) {
|
||||
preambleLOONG64Once.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleLOONG64Err = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-loong64")
|
||||
if err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_loong64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleLOONG64Err = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleLOONG64Err = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleLOONG64Err = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleLOONG64 = data[:i+3]
|
||||
})
|
||||
return preambleLOONG64, preambleLOONG64Err
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GOObjectRISCV emits a GOOBJ object file for RISC-V. The layout is the
|
||||
// shared one in goobj.go, the toolchain preamble, the go120ld header with
|
||||
// its block offsets, the string table, the symbol definitions and the
|
||||
// reloc/aux/data index arrays, with the RISC-V preamble, the MinLC of 2 for
|
||||
// the pc-value deltas, and the single R_RISCV_PCREL_ITYPE/STYPE relocation
|
||||
// per AUIPC pair, matching `go tool asm`'s model (each pair is one 8-byte
|
||||
// relocation, not the ELF HI20/LO12 pair).
|
||||
func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||
pre, err := toolchainObjectPreambleRISCV()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 2, func(r Reloc) (uint16, uint8) {
|
||||
switch r.Kind {
|
||||
case RelRISCVPCRELSType:
|
||||
return relocRISCVPcrelStype, 8
|
||||
case RelRISCVJal:
|
||||
return relocRISCVJal, 4
|
||||
case RelRISCVTLSLE:
|
||||
return relocRISCVTLSLE, 8
|
||||
default:
|
||||
return relocRISCVPcrelItype, 8
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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
|
||||
// the installed go tool asm writes for riscv64, captured by assembling a
|
||||
// one-instruction probe (see toolchainObjectPreamble).
|
||||
var (
|
||||
preambleRISCVOnce sync.Once
|
||||
preambleRISCV []byte
|
||||
preambleRISCVErr error
|
||||
)
|
||||
|
||||
func toolchainObjectPreambleRISCV() ([]byte, error) {
|
||||
preambleRISCVOnce.Do(func() {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
preambleRISCVErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||
return
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "gasm-preamble-riscv")
|
||||
if err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
src := filepath.Join(dir, "probe_riscv64.s")
|
||||
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
obj := filepath.Join(dir, "probe.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||
cmd.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
preambleRISCVErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(obj)
|
||||
if err != nil {
|
||||
preambleRISCVErr = err
|
||||
return
|
||||
}
|
||||
i := bytes.Index(data, []byte("\n!\n"))
|
||||
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||
preambleRISCVErr = fmt.Errorf("unrecognised assembler object layout")
|
||||
return
|
||||
}
|
||||
preambleRISCV = data[:i+3]
|
||||
})
|
||||
return preambleRISCV, preambleRISCVErr
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1446
-94
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build integration
|
||||
|
||||
// Package asm integration tests against the production go-libraries kernels.
|
||||
// These are excluded from the default test run (go test ./...) so that the
|
||||
// coverage numbers are identical locally and in CI, where go-libraries is
|
||||
// not checked out. Run them explicitly with: go test -tags=integration ./asm/
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"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
|
||||
// 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-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 17 {
|
||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||
}
|
||||
|
||||
// mask24 as the DATA directives define it.
|
||||
mask := []byte{
|
||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||
}
|
||||
image := img.Bytes()
|
||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||
}
|
||||
|
||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
for pc := 0; pc < len(code); {
|
||||
inst, err := x86asm.Decode(code[pc:], 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||
}
|
||||
// mod=00, rm=101 → RIP-relative.
|
||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||
target := fn.Offset + pc + 8 + int(rel)
|
||||
if !bytes.Equal(image[target:target+16], mask) {
|
||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||
}
|
||||
loads++
|
||||
}
|
||||
pc += inst.Len
|
||||
}
|
||||
}
|
||||
if loads != 2 {
|
||||
t.Errorf("mask loads found = %d, want 2", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||
// 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-sdk")
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse(path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 10 {
|
||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||
}
|
||||
|
||||
// idx16 as the DATA directives define it: dwords 1..16.
|
||||
idx := make([]byte, 0, 64)
|
||||
for i := 1; i <= 16; i++ {
|
||||
idx = append(idx, byte(i), 0, 0, 0)
|
||||
}
|
||||
image := img.Bytes()
|
||||
base := img.Symbols["idx16"]
|
||||
if base == 0 {
|
||||
t.Fatal("idx16 not laid out")
|
||||
}
|
||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||
}
|
||||
|
||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||
// resolve to idx16 within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||
for pos := 0; ; {
|
||||
i := indexOf(code[pos:], pat)
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
i += pos
|
||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||
target := fn.Offset + i + 10 + int(rel)
|
||||
if target != base {
|
||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||
}
|
||||
loads++
|
||||
pos = i + 10
|
||||
}
|
||||
}
|
||||
if loads != 1 {
|
||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||
}
|
||||
}
|
||||
|
||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||
func indexOf(b, pat []byte) int {
|
||||
for i := 0; i+len(pat) <= len(b); i++ {
|
||||
j := 0
|
||||
for j < len(pat) && b[i+j] == pat[j] {
|
||||
j++
|
||||
}
|
||||
if j == len(pat) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestGOObjectLOONG64Structure checks the emitted loong64 object's blocks:
|
||||
// the symbol tables, the function code bytes and the relocation wiring.
|
||||
func TestGOObjectLOONG64Structure(t *testing.T) {
|
||||
f, errs := parser.Parse("k_loong64.s", `
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
|
||||
GLOBL ·table<>(SB), RODATA, $8
|
||||
DATA ·table<>+0(SB)/8, $0x1122334455667788
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := img.GOObjectLOONG64("testpkg", "k_loong64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
|
||||
// Package defs: the static GLOBL, then the FuncInfo and the two DWARF
|
||||
// symbols (debug_line program, subprogram DIE).
|
||||
defs := v.syms(blkSymdef)
|
||||
if len(defs) != 4 {
|
||||
t.Fatalf("symdefs = %d, want 4", len(defs))
|
||||
}
|
||||
if defs[0].name != "table" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 8 {
|
||||
t.Errorf("table symbol = %+v", defs[0])
|
||||
}
|
||||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||||
t.Errorf("funcinfo symbol = %+v", defs[1])
|
||||
}
|
||||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||||
t.Errorf("lines symbol = %+v", defs[2])
|
||||
}
|
||||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||||
t.Errorf("DIE symbol = %+v", defs[3])
|
||||
}
|
||||
|
||||
// Non-package defs: four pc tables and the function.
|
||||
nps := v.syms(blkNonpkgdef)
|
||||
if len(nps) != 5 {
|
||||
t.Fatalf("nonpkgdefs = %d, want 5", len(nps))
|
||||
}
|
||||
fn := nps[4]
|
||||
if fn.name != "testpkg.add" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 20 {
|
||||
t.Errorf("add symbol = %+v", fn)
|
||||
}
|
||||
|
||||
// The function code: 20 bytes, the ground-truth encoding. It sits
|
||||
// after the GLOBL, FuncInfo, two DWARF symbols and four pc tables.
|
||||
dataIdx := v.blk(blkDataIdx)
|
||||
dataBlk := v.blk(blkData)
|
||||
le := binary.LittleEndian
|
||||
dOff := le.Uint32(dataIdx[8*4:])
|
||||
code := dataBlk[dOff : dOff+20]
|
||||
want := []byte{
|
||||
0x64, 0x20, 0xc0, 0x28, // ld.d r4, 8(r3)
|
||||
0x65, 0x40, 0xc0, 0x28, // ld.d r5, 16(r3)
|
||||
0x84, 0x94, 0x10, 0x00, // add.d r4, r4, r5
|
||||
0x64, 0x60, 0xc0, 0x29, // st.d r4, 24(r3)
|
||||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||||
}
|
||||
for i := range want {
|
||||
if code[i] != want[i] {
|
||||
t.Fatalf("code byte %d = %02x, want %02x", i, code[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The debug_line program: LNE_set_address (the R_ADDR relocation
|
||||
// carries the function address), then one row per line change; the
|
||||
// TEXT is on line 4 (a leading blank line precedes the include), the
|
||||
// instructions on lines 5-9; an advance to the 20-byte end and an
|
||||
// end-of-sequence.
|
||||
linesOff := le.Uint32(dataIdx[4*2:])
|
||||
lines := dataBlk[linesOff : linesOff+21]
|
||||
wantLines := []byte{
|
||||
0x00, 0x09, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LNE_set_address
|
||||
0x13, // pc 0, line 5
|
||||
0x38, // pc 4, line 6
|
||||
0x38, // pc 8, line 7
|
||||
0x38, // pc 12, line 8
|
||||
0x38, // pc 16, line 9
|
||||
0x02, 0x04, // advance_pc to 20
|
||||
0x00, 0x01, 0x01, // end_sequence
|
||||
}
|
||||
for i := range wantLines {
|
||||
if lines[i] != wantLines[i] {
|
||||
t.Fatalf("lines byte %d = %02x, want %02x", i, lines[i], wantLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The subprogram DIE: abbrev 3 (FUNCTION), the qualified name, the
|
||||
// addrx low_pc slot (R_DWTXTADDR_U4), the size as high_pc, the
|
||||
// call-frame-CFA frame base, decl file/line and the external flag.
|
||||
dieOff := le.Uint32(dataIdx[4*3:])
|
||||
die := dataBlk[dieOff : dieOff+27]
|
||||
wantDie := []byte{
|
||||
0x03,
|
||||
't', 'e', 's', 't', 'p', 'k', 'g', '.', 'a', 'd', 'd', 0,
|
||||
0x00, 0x00, 0x00, 0x00, // low_pc: addrx slot
|
||||
0x14, // high_pc: 20
|
||||
0x01, 0x9c, // frame_base: DW_OP_call_frame_cfa
|
||||
0x01, 0x00, 0x00, 0x00, // decl_file: 1
|
||||
0x04, // decl_line: 4
|
||||
0x01, // external
|
||||
0x00, // end of children
|
||||
}
|
||||
for i := range wantDie {
|
||||
if die[i] != wantDie[i] {
|
||||
t.Fatalf("DIE byte %d = %02x, want %02x", i, die[i], wantDie[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The DWARF symbols carry the function-address references: R_ADDR for
|
||||
// the line program's set_address, R_DWTXTADDR_U4 for the DIE's addrx
|
||||
// slot, both against the function's non-package index. The reloc
|
||||
// index counts relocations, not bytes.
|
||||
relocIdx := v.blk(blkRelocIdx)
|
||||
relocs := v.blk(blkReloc)
|
||||
if le.Uint32(relocIdx[4*2:]) != 0 || le.Uint32(relocIdx[4*3:]) != 1 || le.Uint32(relocIdx[4*4:]) != 2 {
|
||||
t.Fatalf("dwarf reloc index ranges: %d %d %d", le.Uint32(relocIdx[4*2:]), le.Uint32(relocIdx[4*3:]), le.Uint32(relocIdx[4*4:]))
|
||||
}
|
||||
lr := relocs[:23]
|
||||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||||
t.Errorf("lines reloc = %x", lr)
|
||||
}
|
||||
dr := relocs[23:46]
|
||||
if int32(le.Uint32(dr[0:])) != 13 || dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||||
t.Errorf("die reloc = %x", dr)
|
||||
}
|
||||
|
||||
// The pc-value deltas are in MinLC (4) units: the flat pcsp covers
|
||||
// the whole 20-byte function with a delta of 5.
|
||||
pcspOff := le.Uint32(dataIdx[4*4:])
|
||||
if got := dataBlk[pcspOff : pcspOff+3]; !bytes.Equal(got, []byte{0x02, 0x05, 0x00}) {
|
||||
t.Errorf("pcsp = %x, want 020500", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectLOONG64Link cross-compiles a Go program with the gasm-produced
|
||||
// object substituted into the package archive, proving cmd/link accepts the
|
||||
// emitted GOOBJ. The binary is not executed (no LoongArch host or qemu).
|
||||
// Skipped when no Go toolchain is available.
|
||||
func TestGOObjectLOONG64Link(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVV a+0(FP), R4
|
||||
MOVV b+8(FP), R5
|
||||
ADDV R5, R4, R4
|
||||
MOVV R4, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_loong64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func add(a, b int64) int64
|
||||
|
||||
func main() {
|
||||
if add(20, 22) != 42 {
|
||||
panic("bad add")
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module l64link\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the cross build (GOARCH=loong64): the package archive and the
|
||||
// link line.
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var pkgArch, work, linkLine, asmObj string
|
||||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "WORK="):
|
||||
work = strings.TrimPrefix(line, "WORK=")
|
||||
case strings.Contains(line, "/asm ") && strings.Contains(line, "main_loong64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if pkgArch == "" || linkLine == "" || asmObj == "" {
|
||||
t.Skip("could not locate the archive, asm output or link line in the build log")
|
||||
}
|
||||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||||
// The archive member holding the assembler's output is named after the
|
||||
// asm object file (main_loong64.o), as cmd/go packs it with `pack r`.
|
||||
asmMember := filepath.Base(strings.ReplaceAll(asmObj, "$WORK", work))
|
||||
|
||||
// Assemble the same source with gasm and swap the object in.
|
||||
pf, perrs := parser.Parse(filepath.Join(dir, "main_loong64.s"), asmSrc)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs)
|
||||
}
|
||||
pimg, err := AssembleFileLOONG64(pf)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
obj, err := pimg.GOObjectLOONG64("main", filepath.Join(dir, "main_loong64.s"))
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectLOONG64: %v", err)
|
||||
}
|
||||
|
||||
// Extract the archive, substitute the object member, repack.
|
||||
membersDir := filepath.Join(dir, "members")
|
||||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||||
extract.Dir = membersDir
|
||||
extract.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := extract.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack x: %v\n%s", err, out)
|
||||
}
|
||||
// Substitute the gasm object for the assembler's archive member (pack
|
||||
// extracts members read-only).
|
||||
member := filepath.Join(membersDir, asmMember)
|
||||
if err := os.Chmod(member, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||||
listCmd.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
listOut, err := listCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||||
}
|
||||
newArch := filepath.Join(dir, "pkg.a")
|
||||
args := []string{"tool", "pack", "c", newArch}
|
||||
seen := map[string]bool{}
|
||||
for m := range strings.FieldsSeq(string(listOut)) {
|
||||
if seen[m] {
|
||||
continue
|
||||
}
|
||||
seen[m] = true
|
||||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args = append(args, filepath.Join(membersDir, m))
|
||||
}
|
||||
pack := exec.Command(goBin, args...)
|
||||
pack.Dir = membersDir
|
||||
pack.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
if out, err := pack.CombinedOutput(); err != nil {
|
||||
t.Fatalf("pack c: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// Re-link with our archive in place of the toolchain's. The link line
|
||||
// carries a GOROOT assignment and $WORK placeholders; run it through the
|
||||
// shell with the GOEXPERIMENT and GOARCH the toolchain expects (the
|
||||
// linker compares the object header against its own, experiments
|
||||
// included).
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||||
link := exec.Command("sh", "-c", linkLine)
|
||||
link.Dir = dir
|
||||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)), "GOARCH=loong64")
|
||||
if out, err := link.CombinedOutput(); err != nil {
|
||||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// The binary is not executed: there is no LoongArch host or qemu here.
|
||||
// The link itself and the symbol table prove cmd/link accepted the gasm
|
||||
// object and laid out the function.
|
||||
nm := exec.Command(goBin, "tool", "nm", filepath.Join(dir, "app2"))
|
||||
nm.Env = append(os.Environ(), "GOARCH=loong64")
|
||||
nmOut, err := nm.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("nm gasm-linked binary: %v\n%s", err, nmOut)
|
||||
}
|
||||
if !strings.Contains(string(nmOut), "main.add") {
|
||||
t.Errorf("main.add not found in linked binary:\n%s", nmOut)
|
||||
}
|
||||
}
|
||||
+676
-62
@@ -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,30 +26,107 @@ 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.
|
||||
type FuncLayout struct {
|
||||
Name string
|
||||
Pkg string // explicit package prefix ("" = the current package)
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Offset int // start offset within the image (== offset within Code)
|
||||
Size int
|
||||
Labels map[string]int // local labels, function-relative
|
||||
Relocs []Reloc // static-symbol references, in emission order
|
||||
Name string
|
||||
Pkg string // explicit package prefix ("" = the current package)
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Offset int // start offset within the image (== offset within Code)
|
||||
Size int
|
||||
Args int // declared argument/result area (the TEXT size suffix)
|
||||
Frame int // local frame size (the TEXT $framesize)
|
||||
NoSplit bool // the NOSPLIT flag
|
||||
SPWrite bool // the SPWRITE flag: writes an arbitrary value to SP
|
||||
Line int // source line of the TEXT directive
|
||||
Labels map[string]int // local labels, function-relative
|
||||
Relocs []Reloc // static-symbol references, in emission order
|
||||
Spadj []SpadjStep // stack-adjustment boundaries, ascending by PC
|
||||
Lines []LineEntry // source-line table: byte offset → source line
|
||||
}
|
||||
|
||||
// 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.
|
||||
// SpadjStep is one stack-adjustment boundary: Value is the SP delta from the
|
||||
// entry state in effect from PC (function-relative) until the next step.
|
||||
type SpadjStep struct {
|
||||
PC int
|
||||
Value int
|
||||
}
|
||||
|
||||
// LineEntry maps a byte offset (function-relative) to a source line number.
|
||||
type LineEntry struct {
|
||||
Offset int
|
||||
Line int
|
||||
}
|
||||
|
||||
// LineAt returns the source line number for the given function-relative byte
|
||||
// offset, using a binary search on the line table. Returns 0 if the offset
|
||||
// is before the first instruction or the table is empty.
|
||||
func (fl *FuncLayout) LineAt(offset int) int {
|
||||
if len(fl.Lines) == 0 {
|
||||
return 0
|
||||
}
|
||||
// Binary search: find the last entry with Offset <= offset.
|
||||
lo, hi := 0, len(fl.Lines)-1
|
||||
for lo < hi {
|
||||
mid := (lo + hi + 1) / 2
|
||||
if fl.Lines[mid].Offset <= offset {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid - 1
|
||||
}
|
||||
}
|
||||
if fl.Lines[lo].Offset <= offset {
|
||||
return fl.Lines[lo].Line
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 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)
|
||||
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 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.
|
||||
@@ -57,6 +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 (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.
|
||||
@@ -66,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
|
||||
@@ -82,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
|
||||
@@ -95,18 +206,54 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, patches, labels, 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)
|
||||
}
|
||||
img.Funcs = append(img.Funcs, FuncLayout{
|
||||
// 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)
|
||||
}
|
||||
fl := FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Pkg: t.Name.Pkg,
|
||||
Static: t.Name.Static,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Frame: frameSize(t),
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
})
|
||||
Lines: lines,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
case "NOSPLIT":
|
||||
fl.NoSplit = true
|
||||
case "SPWRITE":
|
||||
fl.SPWrite = true
|
||||
}
|
||||
}
|
||||
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})
|
||||
}
|
||||
@@ -124,6 +271,9 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
Offset: len(img.Data),
|
||||
Size: len(d.buf),
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
}
|
||||
@@ -136,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
|
||||
@@ -150,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)
|
||||
}
|
||||
@@ -157,70 +337,488 @@ 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
|
||||
// immediates; the object-file emitters record relocations for the linker.
|
||||
func AssembleFileRISCV(f *ast.File) (*Image, error) {
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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{}, SourcePath: f.Path}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
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,
|
||||
Static: t.Name.Static,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Frame: frameSize(t),
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Lines: lines,
|
||||
Spadj: spadj,
|
||||
Relocs: relocs,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
case "NOSPLIT":
|
||||
fl.NoSplit = true
|
||||
case "SPWRITE":
|
||||
fl.SPWrite = true
|
||||
}
|
||||
}
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, d := range dataSyms {
|
||||
pos := dataStart + len(img.Data)
|
||||
for pos%16 != 0 {
|
||||
img.Data = append(img.Data, 0)
|
||||
pos++
|
||||
}
|
||||
img.Symbols[d.name] = pos
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||
Name: d.name,
|
||||
Pkg: d.pkg,
|
||||
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
|
||||
markExternals(img, dataSyms)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// AssembleFileLOONG64 assembles every TEXT function of a parsed loong64 file
|
||||
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
||||
// code. SB references in the code are encoded as pcalau12i pairs with zero
|
||||
// immediates; the object-file emitters record R_LOONG64_ADDR_HI/LO
|
||||
// relocations for the linker.
|
||||
func AssembleFileLOONG64(f *ast.File) (*Image, error) {
|
||||
dataSyms, err := collectData(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
||||
for _, d := range f.Decls {
|
||||
t, ok := d.(*ast.Text)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
code, labels, relocs, lines, spadj, err := assembleLOONG64(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
||||
}
|
||||
fl := FuncLayout{
|
||||
Name: t.Name.Name,
|
||||
Pkg: t.Name.Pkg,
|
||||
Static: t.Name.Static,
|
||||
Offset: len(img.Code),
|
||||
Size: len(code),
|
||||
Frame: frameSize(t),
|
||||
Args: argsSize(t),
|
||||
Line: t.Pos().Line,
|
||||
Labels: labels,
|
||||
Lines: lines,
|
||||
Spadj: spadj,
|
||||
Relocs: relocs,
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
switch f {
|
||||
case "NOSPLIT":
|
||||
fl.NoSplit = true
|
||||
case "SPWRITE":
|
||||
fl.SPWrite = true
|
||||
}
|
||||
}
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
}
|
||||
|
||||
// Lay out the data section behind the code, 16-aligned.
|
||||
dataStart := len(img.Code)
|
||||
for _, d := range dataSyms {
|
||||
pos := dataStart + len(img.Data)
|
||||
for pos%16 != 0 {
|
||||
img.Data = append(img.Data, 0)
|
||||
pos++
|
||||
}
|
||||
img.Symbols[d.name] = pos
|
||||
img.Data = append(img.Data, d.buf...)
|
||||
img.DataSyms = append(img.DataSyms, DataSymbol{
|
||||
Name: d.name,
|
||||
Pkg: d.pkg,
|
||||
Offset: len(img.Data) - len(d.buf), // relative to the data section
|
||||
Size: d.size,
|
||||
Static: d.static,
|
||||
Rodata: d.rodata,
|
||||
Noptr: d.noptr,
|
||||
Dupok: d.dupok,
|
||||
})
|
||||
}
|
||||
|
||||
markExternals(img, dataSyms)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// markExternals identifies relocations that reference symbols not defined in
|
||||
// the file (neither a GLOBL/DATA symbol nor a TEXT function) and records them
|
||||
// as external. The non-amd64 architectures emit relocations for every SB
|
||||
// reference; this post-processing step distinguishes file-local from external.
|
||||
func markExternals(img *Image, dataSyms []dataSym) {
|
||||
known := make(map[string]bool, len(dataSyms)+len(img.Funcs))
|
||||
for _, d := range dataSyms {
|
||||
known[d.name] = true
|
||||
}
|
||||
for _, fn := range img.Funcs {
|
||||
known[fn.Name] = true
|
||||
}
|
||||
externals := map[string]bool{}
|
||||
for i := range img.Funcs {
|
||||
for j := range img.Funcs[i].Relocs {
|
||||
r := &img.Funcs[i].Relocs[j]
|
||||
if !known[r.Name] {
|
||||
r.External = true
|
||||
externals[r.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
sort.Strings(img.Externals)
|
||||
}
|
||||
|
||||
// dataSym is one GLOBL symbol and its DATA initialiser.
|
||||
type dataSym struct {
|
||||
name string
|
||||
pkg string
|
||||
buf []byte
|
||||
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)
|
||||
syms = append(syms, dataSym{
|
||||
name: name,
|
||||
pkg: dd.Name.Pkg,
|
||||
buf: make([]byte, size),
|
||||
static: dd.Name.Static,
|
||||
})
|
||||
|
||||
case *ast.Data:
|
||||
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 || !dd.Value.Imm.HasVal {
|
||||
return nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
||||
}
|
||||
w := dd.Width
|
||||
switch w {
|
||||
case 1, 2, 4, 8:
|
||||
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:
|
||||
return nil, fmt.Errorf("DATA %q: invalid width %d (want 1, 2, 4 or 8)", dd.Name.Name, w)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
v := dd.Value.Imm.Val
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
num = -num
|
||||
}
|
||||
for j := 0; j < w; j++ {
|
||||
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
|
||||
@@ -230,3 +828,19 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
func align16(n int) int {
|
||||
return (n + 15) &^ 15
|
||||
}
|
||||
|
||||
// frameSize returns the local frame size declared on the TEXT directive.
|
||||
func frameSize(t *ast.Text) int {
|
||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||
return int(t.Frame.Imm.Val)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// argsSize returns the argument/result area declared on the TEXT directive.
|
||||
func argsSize(t *ast.Text) int {
|
||||
if t.Args != nil && t.Args.Imm.HasVal {
|
||||
return int(t.Args.Imm.Val)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+428
-59
@@ -4,18 +4,117 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/arch/x86/x86asm"
|
||||
|
||||
"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", `
|
||||
@@ -59,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",
|
||||
`
|
||||
@@ -133,20 +224,250 @@ DATA x<>+0(SB)/4, $1
|
||||
}
|
||||
}
|
||||
|
||||
// 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. Skipped when
|
||||
// the sibling repository is not checked out.
|
||||
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")
|
||||
// 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},
|
||||
}
|
||||
src, err := os.ReadFile(path)
|
||||
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(path, string(src))
|
||||
f, errs := parser.Parse("main_amd64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
@@ -154,43 +475,91 @@ func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
if len(img.Funcs) != 17 {
|
||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||
// 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)
|
||||
|
||||
// mask24 as the DATA directives define it.
|
||||
mask := []byte{
|
||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||
// 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)
|
||||
}
|
||||
image := img.Bytes()
|
||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||
loads := 0
|
||||
for _, fn := range img.Funcs {
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
for pc := 0; pc < len(code); {
|
||||
inst, err := x86asm.Decode(code[pc:], 64)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||
}
|
||||
// mod=00, rm=101 → RIP-relative.
|
||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||
target := fn.Offset + pc + 8 + int(rel)
|
||||
if !bytes.Equal(image[target:target+16], mask) {
|
||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||
}
|
||||
loads++
|
||||
}
|
||||
pc += inst.Len
|
||||
// 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 loads != 2 {
|
||||
t.Errorf("mask loads found = %d, want 2", loads)
|
||||
if _, err := AssembleFile(f); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||||
)
|
||||
|
||||
// Loong64 frame mapping, matching the Go toolchain's loong64 backend.
|
||||
//
|
||||
// Go's loong64 functions have no frame pointer: FP and SP are synthetic
|
||||
// registers resolved against the hardware stack pointer (R3) and the frame
|
||||
// size. The return address lives in R1 (the link register).
|
||||
//
|
||||
// The autosize is the real stack adjustment: the declared local frame plus
|
||||
// the 8 bytes for the saved link register, rounded up to a multiple of 8
|
||||
// (the toolchain aligns frames with `if autosize&4 != 0 { autosize += 4 }`).
|
||||
// A leaf function (no calls) with a zero frame gets no prologue at all.
|
||||
//
|
||||
// Prologue (autosize > 0, 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 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 {
|
||||
autosize int // the real SP adjustment (locals + saved LR, aligned)
|
||||
frame int // the declared $framesize
|
||||
args int // the declared -argsize
|
||||
noSplit bool // the NOSPLIT flag
|
||||
leaf bool // no call instructions in the body
|
||||
|
||||
// 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.
|
||||
func loong64ComputeFrame(t *ast.Text) loong64FrameInfo {
|
||||
fi := loong64FrameInfo{
|
||||
frame: frameSize(t),
|
||||
args: argsSize(t),
|
||||
}
|
||||
for _, f := range t.Flags {
|
||||
if f == "NOSPLIT" {
|
||||
fi.noSplit = true
|
||||
}
|
||||
}
|
||||
fi.leaf = loong64IsLeaf(t)
|
||||
if fi.frame != 0 {
|
||||
fi.autosize = fi.frame + 8 // space for the saved LR
|
||||
if fi.autosize&4 != 0 {
|
||||
fi.autosize += 4
|
||||
}
|
||||
} else if !fi.leaf {
|
||||
// A zero-frame non-leaf function still opens an 8-byte frame for LR.
|
||||
fi.autosize = 8
|
||||
}
|
||||
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.
|
||||
func loong64IsLeaf(t *ast.Text) bool {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||
case "JAL", "CALL", "BL":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// loong64Prologue returns the prologue bytes for a loong64 function. 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
|
||||
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
|
||||
// deallocate the frame when present) followed by jirl r0, r1, 0.
|
||||
func loong64Return(fi loong64FrameInfo) []byte {
|
||||
var ws []uint32
|
||||
if fi.autosize != 0 {
|
||||
if !fi.leaf {
|
||||
// MOVV 0(R3), R1, restore the link register.
|
||||
ws = append(ws, l64irr(l64loadStoreTable["MOVV"].ld, 0, 3, 1))
|
||||
}
|
||||
// ADDV $autosize, R3, close the frame (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.
|
||||
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
|
||||
// reference (SB: static data, handled by the relocation path).
|
||||
func loong64ResolvePseudo(sym *ast.Symbol, fi loong64FrameInfo) (base int, off int32) {
|
||||
if sym == nil {
|
||||
return -1, 0
|
||||
}
|
||||
switch sym.Pseudo {
|
||||
case "FP":
|
||||
return 3, int32(sym.Offset) + int32(fi.autosize) + 8
|
||||
case "SP":
|
||||
return 3, int32(fi.autosize) + int32(sym.Offset)
|
||||
case "SB":
|
||||
return -1, int32(sym.Offset)
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||||
// bare-data pseudo-instructions. The words match `go tool asm`
|
||||
// (GOARCH=loong64) for the same source.
|
||||
func TestLOONG64_sys(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·sys(SB), NOSPLIT, $0
|
||||
NOOP
|
||||
UNDEF
|
||||
WORD $0x12345678
|
||||
SYSCALL $0x10
|
||||
BREAK $0x20
|
||||
DBAR $1
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x03400000, // andi r0, r0, 0 (NOOP)
|
||||
0x002A0000, // break 0 (UNDEF)
|
||||
0x12345678, // WORD
|
||||
0x002B0010, // syscall 0x10
|
||||
0x002A0020, // break 0x20
|
||||
0x38720001, // dbar 1
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_branches21 exercises the single-register branch forms: the
|
||||
// 21-bit BEQZ/BNEZ/BLTZ/BGEZ and the rd-field BGTZ/BLEZ.
|
||||
func TestLOONG64_branches21(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·b21(SB), NOSPLIT, $0
|
||||
BEQZ R4, done
|
||||
BNEZ R5, done
|
||||
BLTZ R6, done
|
||||
BGEZ R7, done
|
||||
BGTZ R8, done
|
||||
BLEZ R9, done
|
||||
done:
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x40001880, // beqz r4, +6
|
||||
0x440014A0, // bnez r5, +5
|
||||
0x600010C0, // bltz r6, +4
|
||||
0x64000CE0, // bgez r7, +3
|
||||
0x60000808, // bgtz r8, +2 (register in the rd field)
|
||||
0x64000409, // blez r9, +1
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_fma exercises the four fused multiply-add forms (4 and 3
|
||||
// operand spellings).
|
||||
func TestLOONG64_fma(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·fma(SB), NOSPLIT, $0
|
||||
FMADDD F0, F1, F2, F3
|
||||
FMSUBD F4, F5, F6
|
||||
FNMADDD F7, F8, F9, F10
|
||||
FNMSUBD F11, F12, F13
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x08200443, // fmadd.d f3, f2, f1, f0
|
||||
0x086214C6, // fmsub.d f6, f5, f5, f4
|
||||
0x08A3A12A, // fnmadd.d f10, f9, f8, f7
|
||||
0x08E5B1AD, // fnmsub.d f13, f12, f12, f11
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_bitops exercises BSTRINS/BSTRPICK (the 6-bit msb/lsb fields)
|
||||
// and ALSL (the sa−1 shift field).
|
||||
func TestLOONG64_bitops(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·bits(SB), NOSPLIT, $0
|
||||
BSTRINSW $3, R4, $0, R5
|
||||
BSTRINSV $3, R4, $1, R6
|
||||
BSTRPICKW $3, R4, $0, R5
|
||||
BSTRPICKV $6, R7, $0, R8
|
||||
ALSLW $1, R4, R5, R6
|
||||
ALSLW $4, R7, R8, R9
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x00630085, // bstrins.w r5, r4, $3, $0
|
||||
0x00830486, // bstrins.d r6, r4, $3, $1
|
||||
0x00638085, // bstrpick.w r5, r4, $3, $0
|
||||
0x00C600E8, // bstrpick.d r8, r7, $6, $0
|
||||
0x00041486, // alsl.w r6, r5, r4, $1 (sa-1)
|
||||
0x0005A0E9, // alsl.w r9, r8, r7, $4
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_ptr exercises the 14-bit-offset memory forms (LL/SC/MOVWP/
|
||||
// MOVVP with the offset scaled by 4) and PRELD.
|
||||
func TestLOONG64_ptr(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·ptr(SB), NOSPLIT, $0
|
||||
LLW 8(R14), R15
|
||||
SCW R16, -4(R17)
|
||||
MOVWP 16(R18), R19
|
||||
MOVVP R20, 24(R21)
|
||||
PRELD 32(R22), $0
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x200009CF, // ll.w r15, 8(r14)
|
||||
0x21FFFE30, // sc.w r16, -4(r17)
|
||||
0x24001253, // ldptr.w r19, 16(r18)
|
||||
0x27001AB4, // stptr.d r20, 24(r21)
|
||||
0x2AC082C0, // preld 32(r22), 0
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_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) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·atoms(SB), NOSPLIT, $0
|
||||
AMADDW R4, (R5), R6
|
||||
RDTIMED R7, R8
|
||||
MOVV F1, R2
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x386110A6, // amadd.w r6, r5, r4
|
||||
0x000068E8, // rdtime.d r8, r7
|
||||
0x0114B822, // movfr2gr.d r2, f1
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_lu52 exercises the LU52I.D immediate form (a gasm extension
|
||||
// the toolchain reaches only through its MOVV expansion).
|
||||
func TestLOONG64_lu52(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·lu52(SB), NOSPLIT, $0
|
||||
LU52ID $0x345, R10
|
||||
LU52ID $0x123, R11, R12
|
||||
ADDV16 $0x10000, R13
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x030D154A, // lu52i.d r10, r10, 0x345
|
||||
0x03048D6C, // lu52i.d r12, r11, 0x123
|
||||
0x100005AD, // addu16i.d r13, r13, 0x10000>>16
|
||||
0x4C000020,
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_sbRefs checks the static-symbol reference forms through the
|
||||
// full file assembly: each pcalau12i+addi.d/ld/st pair carries the
|
||||
// R_LOONG64_ADDR_HI/LO relocation pair, and the immediate fields are left
|
||||
// zero for the linker.
|
||||
func TestLOONG64_sbRefs(t *testing.T) {
|
||||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||||
TEXT ·sb(SB), NOSPLIT, $0
|
||||
MOVV $·table(SB), R4
|
||||
MOVV ·table+8(SB), R5
|
||||
MOVV R6, ·table(SB)
|
||||
RET
|
||||
|
||||
GLOBL ·table(SB), RODATA, $8
|
||||
DATA ·table+0(SB)/8, $42
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
fn := img.Funcs[0]
|
||||
if fn.Size != 28 {
|
||||
t.Fatalf("function size = %d, want 28", fn.Size)
|
||||
}
|
||||
var hi, lo int
|
||||
// The three references: $·table (0), ·table+8 (8), ·table (0).
|
||||
wantAdd := []int64{0, 0, 8, 8, 0, 0}
|
||||
for i, r := range fn.Relocs {
|
||||
wantKind := RelLoong64AddrHi
|
||||
wantOff := (i / 2) * 8
|
||||
if i%2 == 1 {
|
||||
wantKind = RelLoong64AddrLo
|
||||
wantOff += 4
|
||||
}
|
||||
if r.Kind != wantKind || r.Off != wantOff || r.Name != "table" || r.Addend != wantAdd[i] {
|
||||
t.Errorf("reloc %d = {kind %v off %d name %q addend %d}", i, r.Kind, r.Off, r.Name, r.Addend)
|
||||
}
|
||||
if r.Kind == RelLoong64AddrHi {
|
||||
hi++
|
||||
} else {
|
||||
lo++
|
||||
}
|
||||
}
|
||||
if hi != 3 || lo != 3 {
|
||||
t.Errorf("relocs = %d hi + %d lo, want 3 + 3", hi, lo)
|
||||
}
|
||||
// The image carries the zero-immediate pair encodings (the linker
|
||||
// fills the immediate fields from the relocations).
|
||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||
wantWords(t, code,
|
||||
0x1A000004, // pcalau12i r4, 0
|
||||
0x02C00084, // addi.d r4, r4, 0
|
||||
0x1A00001E, // pcalau12i r30, 0
|
||||
0x28C003C5, // ld.d r5, 0(r30)
|
||||
0x1A00001E, // pcalau12i r30, 0
|
||||
0x29C003C6, // st.d r6, 0(r30)
|
||||
0x4C000020, // jirl r0, r1, 0
|
||||
)
|
||||
}
|
||||
|
||||
// TestLOONG64_errors checks the encoder's error paths: undefined labels,
|
||||
// invalid register operands and operand-count mismatches. 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
|
||||
JMP nowhere
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
BEQZ X0, done
|
||||
done:
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
ADDV R4
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
FMADDD F0, F1
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
AMADDW R4, R5
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
WORD
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
PRELD 32(R4)
|
||||
RET
|
||||
`,
|
||||
`TEXT ·e(SB), NOSPLIT, $0
|
||||
ALSLW $5, R4, R5, R6
|
||||
RET
|
||||
`,
|
||||
}
|
||||
for i, src := range cases {
|
||||
fn := firstTextLOONG64(t, src)
|
||||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||||
t.Errorf("case %d: expected an error, got none", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
||||
// the prologue raises the SP delta by autosize (in effect from the third
|
||||
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
||||
// in MinLC (4) units; byte-identical to `go tool asm`.
|
||||
func TestLOONG64_pcsp(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
"leaf",
|
||||
`#include "textflag.h"
|
||||
TEXT ·leaf(SB), NOSPLIT, $8-0
|
||||
MOVV R4, R5
|
||||
RET
|
||||
`,
|
||||
[]byte{0x02, 0x02, 0x20, 0x03, 0x1f, 0x01, 0x00},
|
||||
},
|
||||
{
|
||||
"nonleaf",
|
||||
`#include "textflag.h"
|
||||
TEXT ·nonleaf(SB), NOSPLIT, $8-0
|
||||
MOVV R4, R5
|
||||
JAL (R12)
|
||||
RET
|
||||
`,
|
||||
[]byte{0x02, 0x02, 0x20, 0x05, 0x1f, 0x01, 0x00},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
f, errs := parser.Parse("pcsp_loong64.s", c.src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
if got := pcspTable(img.Funcs[0], 4); !bytes.Equal(got, c.want) {
|
||||
t.Errorf("pcsp = % x, want % x", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_sbRefsUndefined checks that a reference to a symbol no GLOBL
|
||||
// defines assembles into a relocation and is rejected at object emission.
|
||||
func TestLOONG64_sbRefsUndefined(t *testing.T) {
|
||||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||||
TEXT ·sb(SB), NOSPLIT, $0
|
||||
MOVV missing(SB), R4
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileLOONG64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||||
}
|
||||
if len(img.Funcs[0].Relocs) != 2 {
|
||||
t.Fatalf("relocs = %d, want the HI/LO pair", len(img.Funcs[0].Relocs))
|
||||
}
|
||||
if _, err := img.GOObjectLOONG64("p", "sb_loong64.s"); err == nil {
|
||||
t.Error("expected an unknown-symbol error at emission")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64_movImmToFp checks the immediate-to-FP move: 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
|
||||
MOVW $0x1, F0
|
||||
MOVW $0x2, F4
|
||||
MOVW $-1, F4
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
want := []byte{
|
||||
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)
|
||||
}
|
||||
}
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// This file emits Mach-O x86-64 objects (MH_OBJECT) from an assembled
|
||||
// Image, in the shape the Darwin assembler produces: one unnamed segment
|
||||
// carrying a __TEXT,__text and a __DATA,__data section laid out back to
|
||||
// back at addresses zero and len(code), a symbol table (locals first, then
|
||||
// exported definitions, then undefined externals) and one relocation entry
|
||||
// per static-symbol reference, of type X86_64_RELOC_SIGNED.
|
||||
//
|
||||
// The image's own address space carries straight over — the data section
|
||||
// starts immediately after the code, and the layout padding already lives
|
||||
// inside Image.Data — so every symbol keeps its image address as its
|
||||
// n_value, and a local (non-external) relocation leaves the displacement
|
||||
// the assembler resolved in place: the linker only adjusts it by the
|
||||
// section's final movement.
|
||||
|
||||
// Mach-O constants.
|
||||
const (
|
||||
machoMagic64 = 0xfeedfacf
|
||||
machoCPUamd64 = 0x01000007 // CPU_TYPE_X86_64
|
||||
machoCPUSubAll = 3 // CPU_SUBTYPE_X86_64_ALL
|
||||
machoObj = 1 // MH_OBJECT
|
||||
|
||||
machoSegment64 = 0x19 // LC_SEGMENT_64
|
||||
machoSymtab = 0x2 // LC_SYMTAB
|
||||
|
||||
machoSectTextFlags = 0x80000400 // S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS
|
||||
|
||||
nUndf = 0x00 // undefined symbol
|
||||
nSect = 0x0e // defined in section number n_sect
|
||||
nExt = 0x01 // external (exported or undefined-global) bit
|
||||
|
||||
x8664RelocSigned = 1
|
||||
)
|
||||
|
||||
// MachOObject returns the image as a Mach-O x86-64 relocatable object
|
||||
// (MH_OBJECT), the shape the Darwin toolchain links. Symbol names follow
|
||||
// the same rules as the ELF output. Every static-symbol reference becomes
|
||||
// an X86_64_RELOC_SIGNED relocation: external references against their
|
||||
// undefined symbol, file-local ones against the __DATA section with the
|
||||
// resolved displacement carried in the instruction bytes.
|
||||
func (img *Image) MachOObject() ([]byte, error) {
|
||||
le := binary.LittleEndian
|
||||
|
||||
// Section ordinals (1-based, as Mach-O numbers them).
|
||||
const (
|
||||
sectText = 1
|
||||
sectData = 2
|
||||
)
|
||||
|
||||
// Object address space: code at 0, data immediately after (the layout
|
||||
// padding is already part of img.Data, so image addresses are object
|
||||
// addresses).
|
||||
textAddr := uint64(0)
|
||||
dataAddr := uint64(len(img.Code))
|
||||
vmsize := dataAddr + uint64(len(img.Data))
|
||||
|
||||
// The code, with external displacements primed to addend − 4: the
|
||||
// linker adds the symbol's address to the field as it stands. Local
|
||||
// displacements stay as the assembler resolved them.
|
||||
code := append([]byte(nil), img.Code...)
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
if r.External {
|
||||
// Prime the field to the addend measured from the patch
|
||||
// site: the assembler records it from the instruction end,
|
||||
// After − Off bytes past the field.
|
||||
copy(code[fn.Offset+r.Off:], le32(r.Addend-int64(r.After-r.Off)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Symbols: locals first, then exported definitions, then undefined
|
||||
// externals — the order the classic link editor expects.
|
||||
type machoSym struct {
|
||||
name string
|
||||
typ byte
|
||||
sect byte
|
||||
value uint64
|
||||
}
|
||||
var locals, globals, undefs []machoSym
|
||||
for _, fn := range img.Funcs {
|
||||
s := machoSym{name: objectName(fn.Pkg, fn.Name), typ: nSect, sect: sectText, value: textAddr + uint64(fn.Offset)}
|
||||
if fn.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.typ |= nExt
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
s := machoSym{name: objectName(d.Pkg, d.Name), typ: nSect, sect: sectData, value: dataAddr + uint64(d.Offset)}
|
||||
if d.Static {
|
||||
locals = append(locals, s)
|
||||
} else {
|
||||
s.typ |= nExt
|
||||
globals = append(globals, s)
|
||||
}
|
||||
}
|
||||
for _, name := range img.Externals {
|
||||
undefs = append(undefs, machoSym{name: name, typ: nUndf | nExt})
|
||||
}
|
||||
syms := append(append(locals, globals...), undefs...)
|
||||
symIdx := map[string]int{}
|
||||
for i, s := range syms {
|
||||
symIdx[s.name] = i
|
||||
}
|
||||
|
||||
// Relocations, attached to the __text section.
|
||||
type machoReloc struct {
|
||||
addr uint32
|
||||
symnum uint32
|
||||
extern bool
|
||||
}
|
||||
var relocs []machoReloc
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
rel := machoReloc{addr: uint32(fn.Offset + r.Off)}
|
||||
if r.External {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
rel.symnum = uint32(idx)
|
||||
rel.extern = true
|
||||
} else {
|
||||
// Section-relative: r_symbolnum carries the section number
|
||||
// and the resolved displacement stays in the bytes.
|
||||
rel.symnum = sectData
|
||||
}
|
||||
relocs = append(relocs, rel)
|
||||
}
|
||||
}
|
||||
|
||||
// The string table opens with the conventional " \0".
|
||||
strtab := []byte{' ', 0}
|
||||
strOff := map[string]int{}
|
||||
for _, s := range syms {
|
||||
if _, ok := strOff[s.name]; ok {
|
||||
continue
|
||||
}
|
||||
strOff[s.name] = len(strtab)
|
||||
strtab = append(strtab, s.name...)
|
||||
strtab = append(strtab, 0)
|
||||
}
|
||||
|
||||
// File layout: header, the two load commands, section data (code,
|
||||
// data), the relocation table, the symbol table, the string table.
|
||||
const (
|
||||
hdrSize = 32
|
||||
segCmdSize = 72 + 2*80 // segment command with two sections
|
||||
symCmdSize = 24
|
||||
)
|
||||
sizeofcmds := segCmdSize + symCmdSize
|
||||
dataOff := hdrSize + sizeofcmds
|
||||
reloff := dataOff + len(code) + len(img.Data)
|
||||
symoff := reloff + 8*len(relocs)
|
||||
stroff := symoff + 16*len(syms)
|
||||
|
||||
out := make([]byte, stroff+len(strtab))
|
||||
|
||||
// mach_header_64.
|
||||
le.PutUint32(out[0:], machoMagic64)
|
||||
le.PutUint32(out[4:], machoCPUamd64)
|
||||
le.PutUint32(out[8:], machoCPUSubAll)
|
||||
le.PutUint32(out[12:], machoObj)
|
||||
le.PutUint32(out[16:], 2) // ncmds
|
||||
le.PutUint32(out[20:], uint32(sizeofcmds))
|
||||
le.PutUint32(out[24:], 0) // flags
|
||||
le.PutUint32(out[28:], 0) // reserved
|
||||
|
||||
// LC_SEGMENT_64 with the two sections.
|
||||
p := hdrSize
|
||||
le.PutUint32(out[p:], machoSegment64)
|
||||
le.PutUint32(out[p+4:], segCmdSize)
|
||||
// segname: the empty string, zero-padded to 16 bytes.
|
||||
le.PutUint64(out[p+8:], 0)
|
||||
le.PutUint64(out[p+16:], 0)
|
||||
le.PutUint64(out[p+24:], 0) // vmaddr
|
||||
le.PutUint64(out[p+32:], vmsize)
|
||||
le.PutUint64(out[p+40:], uint64(dataOff))
|
||||
le.PutUint64(out[p+48:], vmsize)
|
||||
le.PutUint32(out[p+56:], 7) // maxprot rwx
|
||||
le.PutUint32(out[p+60:], 7) // initprot rwx
|
||||
le.PutUint32(out[p+64:], 2) // nsects
|
||||
le.PutUint32(out[p+68:], 0) // flags
|
||||
|
||||
// __TEXT,__text
|
||||
s := p + 72
|
||||
copy(out[s:], "__text")
|
||||
copy(out[s+16:], "__TEXT")
|
||||
le.PutUint64(out[s+32:], textAddr)
|
||||
le.PutUint64(out[s+40:], uint64(len(code)))
|
||||
le.PutUint32(out[s+48:], uint32(dataOff))
|
||||
le.PutUint32(out[s+52:], 4) // align 2^4
|
||||
le.PutUint32(out[s+56:], uint32(reloff))
|
||||
le.PutUint32(out[s+60:], uint32(len(relocs)))
|
||||
le.PutUint32(out[s+64:], machoSectTextFlags)
|
||||
|
||||
// __DATA,__data
|
||||
s += 80
|
||||
copy(out[s:], "__data")
|
||||
copy(out[s+16:], "__DATA")
|
||||
le.PutUint64(out[s+32:], dataAddr)
|
||||
le.PutUint64(out[s+40:], uint64(len(img.Data)))
|
||||
le.PutUint32(out[s+48:], uint32(dataOff+len(code)))
|
||||
le.PutUint32(out[s+52:], 4) // align 2^4
|
||||
|
||||
// LC_SYMTAB.
|
||||
p = hdrSize + segCmdSize
|
||||
le.PutUint32(out[p:], machoSymtab)
|
||||
le.PutUint32(out[p+4:], symCmdSize)
|
||||
le.PutUint32(out[p+8:], uint32(symoff))
|
||||
le.PutUint32(out[p+12:], uint32(len(syms)))
|
||||
le.PutUint32(out[p+16:], uint32(stroff))
|
||||
le.PutUint32(out[p+20:], uint32(len(strtab)))
|
||||
|
||||
// Section data.
|
||||
copy(out[dataOff:], code)
|
||||
copy(out[dataOff+len(code):], img.Data)
|
||||
|
||||
// Relocation entries.
|
||||
for i, r := range relocs {
|
||||
e := out[reloff+i*8:]
|
||||
le.PutUint32(e[0:], r.addr)
|
||||
bits := r.symnum & 0x00ffffff
|
||||
bits |= 1 << 24 // r_pcrel
|
||||
bits |= 2 << 25 // r_length = 4 bytes
|
||||
if r.extern {
|
||||
bits |= 1 << 27 // r_extern
|
||||
}
|
||||
bits |= x8664RelocSigned << 28
|
||||
le.PutUint32(e[4:], bits)
|
||||
}
|
||||
|
||||
// nlist_64 entries.
|
||||
for i, s := range syms {
|
||||
e := out[symoff+i*16:]
|
||||
le.PutUint32(e[0:], uint32(strOff[s.name]))
|
||||
e[4] = s.typ
|
||||
e[5] = s.sect
|
||||
le.PutUint16(e[6:], 0) // n_desc
|
||||
le.PutUint64(e[8:], s.value)
|
||||
}
|
||||
|
||||
// String table.
|
||||
copy(out[stroff:], strtab)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"debug/macho"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMachOObject checks the structure of the emitted MH_OBJECT: the two
|
||||
// sections and their addresses, the symbol table (types, sections, values)
|
||||
// and the __text relocation entries, parsed back with debug/macho. No
|
||||
// Darwin toolchain is available on the test hosts, so the check is
|
||||
// structural — the ELF output carries the end-to-end link-and-run proof of
|
||||
// the shared symbol and relocation model.
|
||||
func TestMachOObject(t *testing.T) {
|
||||
img := elfTestImage(t)
|
||||
obj, err := img.MachOObject()
|
||||
if err != nil {
|
||||
t.Fatalf("MachOObject: %v", err)
|
||||
}
|
||||
f, err := macho.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if f.Type != macho.TypeObj {
|
||||
t.Errorf("file type = %v, want MH_OBJECT", f.Type)
|
||||
}
|
||||
if f.Cpu != macho.CpuAmd64 {
|
||||
t.Errorf("cpu = %v, want CpuAmd64", f.Cpu)
|
||||
}
|
||||
|
||||
text := f.Section("__text")
|
||||
data := f.Section("__data")
|
||||
if text == nil || data == nil {
|
||||
t.Fatal("missing __text or __data section")
|
||||
}
|
||||
if text.Addr != 0 || text.Size != uint64(len(img.Code)) {
|
||||
t.Errorf("__text addr/size = %#x/%d, want 0/%d", text.Addr, text.Size, len(img.Code))
|
||||
}
|
||||
if data.Addr != uint64(len(img.Code)) {
|
||||
t.Errorf("__data addr = %#x, want %#x", data.Addr, len(img.Code))
|
||||
}
|
||||
|
||||
// Symbol table: locals, exported definitions, undefined externals.
|
||||
syms := f.Symtab.Syms
|
||||
byName := map[string]macho.Symbol{}
|
||||
for _, s := range syms {
|
||||
byName[s.Name] = s
|
||||
}
|
||||
wantSym := func(name string, typ, sect uint8, value uint64) {
|
||||
t.Helper()
|
||||
s, ok := byName[name]
|
||||
if !ok {
|
||||
t.Errorf("symbol %q not found", name)
|
||||
return
|
||||
}
|
||||
if s.Type != typ || s.Sect != sect || s.Value != value {
|
||||
t.Errorf("%s: type/sect/value = %#x/%d/%#x, want %#x/%d/%#x",
|
||||
name, s.Type, s.Sect, s.Value, typ, sect, value)
|
||||
}
|
||||
}
|
||||
const (
|
||||
defined = nSect | nExt
|
||||
local = nSect
|
||||
undefined = nUndf | nExt
|
||||
)
|
||||
wantSym("addq", defined, 1, 0)
|
||||
wantSym("getanswer", defined, 1, 5)
|
||||
wantSym("useextern", defined, 1, 13)
|
||||
answer := byName["answer"]
|
||||
if answer.Type != local || answer.Sect != 2 {
|
||||
t.Errorf("answer: type/sect = %#x/%d, want %#x/2", answer.Type, answer.Sect, local)
|
||||
}
|
||||
wantSym("extvar", undefined, 0, 0)
|
||||
|
||||
// Relocations: both X86_64_RELOC_SIGNED, PC-relative, 4 bytes wide.
|
||||
// The local one carries its section number in Value, the external one
|
||||
// its symbol number.
|
||||
if len(text.Relocs) != 2 {
|
||||
t.Fatalf("__text relocs = %d, want 2", len(text.Relocs))
|
||||
}
|
||||
var sawLocal, sawExternal bool
|
||||
for _, r := range text.Relocs {
|
||||
if !r.Pcrel || r.Len != 2 || r.Type != x8664RelocSigned {
|
||||
t.Errorf("reloc at %#x: pcrel/len/type = %v/%d/%d", r.Addr, r.Pcrel, r.Len, r.Type)
|
||||
}
|
||||
switch {
|
||||
case r.Extern:
|
||||
if name := syms[r.Value].Name; name != "extvar" {
|
||||
t.Errorf("external reloc at %#x names %q, want extvar", r.Addr, name)
|
||||
}
|
||||
sawExternal = true
|
||||
default:
|
||||
if r.Value != 2 { // __data, the second section
|
||||
t.Errorf("local reloc at %#x: section %d, want 2 (__data)", r.Addr, r.Value)
|
||||
}
|
||||
sawLocal = true
|
||||
}
|
||||
}
|
||||
if !sawLocal || !sawExternal {
|
||||
t.Errorf("relocs seen: local=%v external=%v, want both", sawLocal, sawExternal)
|
||||
}
|
||||
|
||||
// The __text bytes are the image code, with the external displacement
|
||||
// primed to addend − 4 and the local one left resolved.
|
||||
textData, err := text.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := append([]byte(nil), img.Code...)
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
if r.Name == "extvar" {
|
||||
binary.LittleEndian.PutUint32(want[fn.Offset+r.Off:], 0xfffffffc) // −4
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(textData, want) {
|
||||
t.Errorf("__text bytes %x, want %x", textData, want)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 503 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user