Compare commits

..
487 Commits
Author SHA1 Message Date
petrbalvin a77dd12d2d chore: prepare release v0.36.0
Test / test (push) Successful in 32s
Release / build (amd64, freebsd) (push) Successful in 52s
Release / build (amd64, linux) (push) Successful in 13s
Release / build (arm64, freebsd) (push) Successful in 51s
Release / build (arm64, linux) (push) Successful in 29s
Release / build (loong64, linux) (push) Successful in 29s
Release / build (riscv64, linux) (push) Successful in 31s
Release / release (push) Successful in 10s
Assisted-by: GLM 5.3 Flash
2026-10-07 22:37:42 +02:00
petrbalvin e5df0a9d84 fix(asm): adapt the flag list integration test to the paired oracle
Test / test (push) Successful in 31s
Assisted-by: GLM 5.3 Flash
2026-10-07 21:52:27 +02:00
petrbalvin a9ee2bc702 test(parser): pin the trailing comment of a TEXT and GLOBL header
Assisted-by: GLM 5.3 Flash
2026-10-07 21:50:20 +02:00
petrbalvin 6f3e054bab test(asm): pin the flag-list TEXT shapes against the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-07 21:50:20 +02:00
petrbalvin 9179d5cc7a test(format): pin the canonical rendering of the flags operand
Assisted-by: GLM 5.3 Flash
2026-10-07 21:50:20 +02:00
petrbalvin 431d0d9b0b feat(parser): read the TEXT and GLOBL operands the toolchain counts them
Assisted-by: GLM 5.3 Flash
2026-10-07 21:50:20 +02:00
petrbalvin cea5db6964 feat(parser): evaluate the TEXT and GLOBL flags operand as one expression
Assisted-by: GLM 5.3 Flash
2026-10-07 21:50:20 +02:00
petrbalvin 4eb9100def test(asm): walk the loong64 audit backlog under every operand shape
Assisted-by: GLM 5.3 Flash
2026-10-07 21:42:14 +02:00
petrbalvin 43494a30f0 test(disasm): pin the toolchain's own ADDU16I.D rendering
Assisted-by: GLM 5.3 Flash
2026-10-07 21:42:14 +02:00
petrbalvin f661c2fc78 test(asm): pin the ADDV16 immediate family the toolchain assembles
Assisted-by: GLM 5.3 Flash
2026-10-07 21:42:14 +02:00
petrbalvin a3eaa82f87 feat(asm): take the loong64 register-pair spellings the toolchain parses
Assisted-by: GLM 5.3 Flash
2026-10-07 21:42:14 +02:00
petrbalvin 1031cd9ae7 feat(arch): the same-size shift-immediate family
Assisted-by: GLM 5.3 Flash
2026-10-07 21:39:45 +02:00
petrbalvin eda8b16c5f feat(arch): the shift-immediate tsz:imm3 scheme
Assisted-by: GLM 5.3 Flash
2026-10-07 21:39:45 +02:00
petrbalvin 718181e7a7 feat(arch): the FP8-to-halfword conversion pairs
Assisted-by: GLM 5.3 Flash
2026-10-07 21:39:45 +02:00
petrbalvin 107ca512b2 feat(arch): the ZCNOT unary pair and the vector counter step
Assisted-by: GLM 5.3 Flash
2026-10-07 21:39:45 +02:00
petrbalvin 4066396226 feat(asm): encode the GETCALLERPC, REM, DWORD and half-FCVT shapes
Assisted-by: GLM 5.3 Flash
2026-10-07 21:34:30 +02:00
petrbalvin df7a5091e0 test(asm): pair the same-named GOROOT functions in definition order
Assisted-by: GLM 5.3 Flash
2026-10-07 21:34:30 +02:00
petrbalvin c27ba30862 feat(asm): encode the arm64 local-exec TLS load
Assisted-by: GLM 5.3 Flash
2026-10-07 21:34:30 +02:00
petrbalvin 5f6be4584d fix(asm): encode the flag-setting logicals to ZR and fold immediate expressions
Assisted-by: GLM 5.3 Flash
2026-10-07 21:34:30 +02:00
petrbalvin c1cef7b6e8 feat(asm): materialise frame-relative addresses the way the toolchain does
Assisted-by: GLM 5.3 Flash
2026-10-07 21:34:30 +02:00
petrbalvin 1120a52a54 test(asm): sweep every registered amd64 extension mnemonic from .s text
Assisted-by: GLM 5.3 Flash
2026-10-07 20:56:25 +02:00
petrbalvin fa50521619 feat(lint): surface the amd64 extension layer's refusals
Assisted-by: GLM 5.3 Flash
2026-10-07 20:56:25 +02:00
petrbalvin 5ee60c860b fix(cmd/gasm): probe the arm64 operand shapes the encoder accepts
Assisted-by: GLM 5.3 Flash
2026-10-07 20:39:44 +02:00
petrbalvin 76f8ba6403 feat(asm): give riscv64 the END and GETCALLERPC the toolchain accepts
Assisted-by: GLM 5.3 Flash
2026-10-07 20:39:44 +02:00
petrbalvin 0211d6672d feat(asm): expand riscv64 memory offsets beyond the 12-bit immediate
Assisted-by: GLM 5.3 Flash
2026-10-07 20:39:44 +02:00
petrbalvin 7aba29ac67 test(arch): pin the golden vectors of the last four families
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:42 +02:00
petrbalvin aeb109a64c feat(arch): the vector-length arithmetic pseudo group
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin bd85f8838d feat(arch): the SVE2.1 last-active vector and compare families
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin 12a5cfff52 feat(arch): the rest of the SVE2 BFloat16 wall
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin cdc3a75c88 feat(arch): the SVE multiple-structure loads and stores
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin 82dbf087a8 feat(arch): the SVE2 BFloat16 arithmetic core
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin 0760cc91af feat(arch): the SVE2 three-source and bitwise combine families
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin bd80499c74 feat(arch): the SVE2 shift-by-vector family
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin b93fccc075 feat(arch): the SVE2.1 pairwise and quadword-reduction families
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin 31ab584eee feat(arch): the SVE2.1 narrowing two-to-one family
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin ee2c6d51b3 fix(arch): keep Zdn out of the class bits of the predicated Z-alias source
Assisted-by: GLM 5.3 Flash
2026-10-07 20:35:41 +02:00
petrbalvin 6e49cbd099 feat(asm): assemble the extended instruction layer on amd64
Assisted-by: GLM 5.3 Flash
2026-10-07 20:24:51 +02:00
petrbalvin d4878524e8 test(asm): pin the clean GOROOT arm64 set against the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-07 19:54:22 +02:00
petrbalvin 07f4622ffe style(asm): range over the kernel generator's statement count
Assisted-by: GLM 5.3 Flash
2026-10-07 19:54:22 +02:00
petrbalvin b3ede5f952 test(asm): pin the mid-function pool flush against the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-07 19:54:22 +02:00
petrbalvin 5a5936d222 feat(asm): drain the arm64 literal pool mid-function at the distance bound
Assisted-by: GLM 5.3 Flash
2026-10-07 19:54:22 +02:00
petrbalvin 4632ac1bb9 feat(arch): add the AVX-VNNI-INT16 dot products to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 19:49:15 +02:00
petrbalvin 96000dd64d feat(arch): add the VEX encoder to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 19:49:15 +02:00
petrbalvin 47d561b229 refactor(arch): extract the displacement tail and SIB builders
Assisted-by: GLM 5.3 Flash
2026-10-07 19:49:15 +02:00
petrbalvin 27ef71859b feat(arch): add VMINMAXSH to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 19:49:15 +02:00
petrbalvin 03f9ef0ac6 feat(arch): add the FP16 complex fused multiply-add to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 19:49:15 +02:00
petrbalvin 86cb785e58 fix(asm): read the MOV family registers against their banks
Assisted-by: GLM 5.3 Flash
2026-10-07 19:40:32 +02:00
petrbalvin 4ff47fe17a fix(cmd/gasm): probe the riscv64 operand shapes the encoder accepts
Assisted-by: GLM 5.3 Flash
2026-10-07 19:40:32 +02:00
petrbalvin d18195f581 test(asm): pin the riscv64 error parity against the toolchain catalogues
Assisted-by: GLM 5.3 Flash
2026-10-07 19:40:32 +02:00
petrbalvin f0942ff7f3 fix(asm): reject the operand shapes the toolchain rejects on riscv64
Assisted-by: GLM 5.3 Flash
2026-10-07 19:40:32 +02:00
petrbalvin 6c66f5bbd9 test(asm): encode the new FP16 families through the registry
Assisted-by: GLM 5.3 Flash
2026-10-07 19:00:44 +02:00
petrbalvin 509afbb6c9 feat(arch): add the FP16 complex multiply and minimum-maximum to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 18:59:09 +02:00
petrbalvin 8080e0acef feat(arch): add the AVX512-FP16 FMA families to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 18:58:49 +02:00
petrbalvin 87493d9391 build(justfile): anchor the fuzz recipe to one exact target
Test / test (push) Successful in 1m2s
Assisted-by: GLM 5.3
2026-10-07 15:00:28 +02:00
petrbalvin 63ed141522 fix(parser): reject macro parameter lists the toolchain rejects
The C variadic spellings ("..." and the GNU "name..."), an empty or
trailing parameter, a missing comma, a stray token and an unterminated
list all parsed silently before: non-identifier tokens were skipped, so
"#define M(...)" defined a zero-parameter macro and invocations were
diagnosed only by argument count, if at all.  The toolchain rejects the
definition itself ("bad definition for macro"), and so does the
preprocessor now: the parameter list must be identifiers separated by
single commas and closed by the parenthesis, and a rejected definition
leaves the name unbound.  The spellings join the fuzz corpus.

Assisted-by: GLM 5.3
2026-10-07 14:04:15 +02:00
petrbalvin 373ec51061 fix(format): treat a selector-folded label as naming its macro
The lexer folds NAME.selector into one identifier, and an object macro
reached through the selector expands with it travelling along, so a
label spelled NAME.selector: restructures exactly like the bare name
would.  The macro-name checks now resolve the prefix before the first
period, and the crashing input joins the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin f932c5811c fix(format): never peel a stacked label that names a macro
Peeling a stacked label whose name is a macro moves it onto a line of
its own, where its expansion decides the line's shape: an empty body
leaves a bare colon behind, a line the parser rejects.  The peel loops
now hold such labels back with the rest of the line, and the crashing
input joins the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin b54d2b4520 fix(format): keep macro content behind a label on its line
The canonical form splits a label from the instruction that follows it,
but an identifier naming a macro may expand into any token at all: moved
to a line of its own it no longer parses, because the parser accepts a
non-mnemonic first token only behind a label.  The names #define'd in
the file now hold such content back, conservatively across the whole
file, and the crashing input joins the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin f452b8a995 fix(format): keep a macro-named label line whole
A label whose name is a macro expands into whatever the body is, so the
line's statement structure exists only after expansion; splitting the
label off changed clean input into a different statement sequence.  The
formatter now records the names #define'd above each line and renders
such a label line unsplit, and the crashing input joins the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 715298ed23 test(format): keep the expansion signature off the EOF comment newline
An unterminated block comment at the end of a file swallows the
formatter's mandatory final newline, an artifact the token view already
documents as layout; the preproc signature entry now trims it so the
invariant stays about tokens.  The triggering input joins the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin bbe14dd1ab fix(parser): diagnose GLOBL and DATA without a symbol name
A bare GLOBL or DATA parsed without a single diagnostic while leaving a
nil Name in the tree, a pointer every downstream tool dereferences; TEXT
keeps a placeholder beside its error for exactly that reason, and GLOBL
and DATA now do the same.  The crashing input enters the corpus.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 05de774c0a test(format): pin the macro-layout crashers as corpus seeds
The two inputs the expansion round-trip target was built for, the object
macro with a parenthesised body and the continuation-only macro body,
enter the seed corpus so every plain go test run replays them.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 6218023a63 style(parser): range over the include chain depth
Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 71cc65ea48 fix(format): separate an empty TEXT body from the next block
The one-blank rule before a new block skipped every line that followed a
TEXT directive, not just the function's first label, so a TEXT with an
empty body ran straight into the next declaration.  The exemption now
applies to labels only, and the directive shapes around GLOBL and DATA
ranges are pinned.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 21b6348dbe test(parser): cover self-includes, deep chains and diamonds
The include guard's scope is now pinned from three sides: a file
including itself is a cycle diagnostic, a chain of two thousand distinct
headers completes with the deepest content spliced, and a header reached
again through a separate branch splices a second time and is refused as
a redefinition.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 32c453d66e test(format): corpus invariants over GOROOT assembly
Three property tests walk every .s file under the installed GOROOT plus
the repository's kernels: formatting is idempotent and preserves the
token stream, a clean parse keeps its tree through a format pass, and a
cleanly expanding file expands to the same statements afterwards.  The
walks skip under -short so the push suite keeps its budget.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin 59637d35b1 test(format): fuzz the expansion round-trip
A second formatter target holds the assembly path's contract: beyond the
token view, a file the expander reads cleanly must expand to the same
statement sequence after formatting, because the macro language draws
distinctions from layout (the adjacency of a #define name and its '(',
continuation bodies) that a token count cannot see.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin e600fc00e7 fix(format): preserve macro adjacency and continuation bodies
The canonical spelling of a #define line glued a '(' to the macro name
whatever the input's spacing, turning an object macro whose body opens
with a parenthesis into a parameterised one, and it flattened the
backslash continuations of a multi-line body into one physical line,
fusing the statements the expansion splits at those boundaries.  Both
change what a valid file assembles to, so renderPreproc now keeps the
name's adjacency (the same column check the preprocessor applies) and
restores the continuation boundaries from the token positions.

Assisted-by: GLM 5.3
2026-10-07 13:54:42 +02:00
petrbalvin acd30088af fix(asm): reject the operand-starved riscv64 spellings instead of panicking
Assisted-by: GLM 5.3
2026-10-07 13:53:37 +02:00
petrbalvin d03de62c07 feat(arch): add the amd64 fp16 packed imm8-control group
Assisted-by: GLM 5.3
2026-10-07 13:51:48 +02:00
petrbalvin 8de1b371da feat(arch): add the amd64 fp16 packed conversion family
Assisted-by: GLM 5.3
2026-10-07 13:51:48 +02:00
petrbalvin fb6d01a7d0 feat(arch): encode the amd64 embedded rounding and SAE decorations
Assisted-by: GLM 5.3
2026-10-07 13:51:48 +02:00
petrbalvin e56c04e9ee fix(asm): read three operands from the arm64 last-element form
Assisted-by: GLM 5.3
2026-10-07 13:51:10 +02:00
petrbalvin 104bea036b feat(asm): encode the arm64 SVE gather loads and scatter stores
Assisted-by: GLM 5.3
2026-10-07 13:51:10 +02:00
petrbalvin 6faf850793 feat(asm): encode the arm64 SVE2 crypto, counter and reduction families
Assisted-by: GLM 5.3
2026-10-07 13:51:10 +02:00
petrbalvin dac0a5b51b docs(asm): state the seed layout of the arch fuzz targets exactly
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 965b33e5b0 test(asm): seed the riscv64 and loong64 assemble fuzz targets
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin b9dfb48d79 fix(asm): encode the loong64 64-bit-span 2RI14 offsets like the toolchain
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 41aa8edfd2 fix(asm): match the toolchain's loong64 logical immediate expansion
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 7f955f8bd1 fix(cmd/gasm): probe the loong64 sc.q operand order in the encodability battery
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 8fdc511d0d test(asm): add the loong64 error-parity catalogue
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin e5035d92e9 fix(asm): reject the offset on the loong64 register-indexed memory form
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin b54547c08d fix(asm): reject the shifted-register compositions on loong64
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin e9510e8a68 test(asm): pin the riscv64 tail against the toolchain byte for byte
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 9d50212a71 fix(asm): refuse the riscv64 width moves across register banks
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin f2892e4f59 feat(asm): emit the riscv64 local-exec TLS sequence
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin f8dbd4f017 fix(asm): lower the riscv64 immediate CSR pseudos onto their opcode forms
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin e5aabf9801 fix(asm): encode the riscv64 FENCE predecessor and successor flags
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin 36d1ac804a fix(asm): route the riscv64 register moves through the toolchain forms
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin da35883633 fix(asm): compress the riscv64 two-operand arithmetic and immediate tail
Assisted-by: GLM 5.3
2026-10-07 13:51:02 +02:00
petrbalvin f0318d2c99 test(disasm): pin the vector spaces the decoder refuses
The RISC-V vector extension and the LoongArch LSX/LASX families carry
no x/arch decode tables, and the loong64 WORD directive is no
instruction: all three stay on the placeholder.  The pins record
today's refusal with the corpus rows that assemble the same bytes, so
a decoder bump that learns one of these spaces flips a row here and
asks for the naming pass to cover it.

Assisted-by: GLM 5.3
2026-10-07 13:50:54 +02:00
petrbalvin a7f9d5eb67 feat(disasm): render the loong64 families x/arch names Unknown
x/arch's Plan 9 renderer decodes thirty-odd scalar loong64 operations
perfectly but prints them as "Unknown OP args", and names the
sign-extension pair EXT.W.B/EXT.W.H "?".  The supplementary naming
pass re-renders them with the toolchain's own spellings: the families
whose operand order x/arch already prints the Plan 9 way trade only
the mnemonic, and the pointer loads and stores, the acquire loads,
the release stores, PRELD and ALSL are rebuilt from the decoded
arguments with the toolchain's operand order and its raw displacement
reading.  ADDU16I.D, a macro helper the assembler never takes as
input, keeps the decoder's Unknown render.

The loong64 parity fixture grows from 73 to 385 rows, pinning every
unique four-byte corpus word the decoder accepts, and the LL/SC
displacement divergence in the encoder is documented for asm.

Assisted-by: GLM 5.3
2026-10-07 13:50:54 +02:00
petrbalvin 0f6e008c9a feat(disasm): name the arm64 words arm64asm refuses
arm64asm rejects three exception-space words the toolchain's corpus
assembles: the hypervisor call HVC, the secure monitor call SMC and
the speculation barrier SB.  The supplementary naming table reads
the raw word in the error branch and renders the corpus spellings;
the corpus rows are pinned in the parity fixture and the boundary
test holds the unallocated neighbours on the placeholder.

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin 3f35b2a718 fix(disasm): drop the implicit GOARCH constraint the naming files carried
The names naming_amd64.go and naming_amd64_test.go carried the _amd64
filename suffix, which the go tool reads as an implicit GOARCH=amd64
build constraint: the tables disappeared from every non-amd64 build
and the package failed to compile for arm64, riscv64 and loong64, the
other three architectures the tool assembles.  Renaming to
amd64_naming.go removes the constraint; the module builds again for
all four GOARCH values.

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin 2c70359ad0 feat(disasm): name the amd64 encodings x86asm refuses
The toolchain's assembler corpus carries 195 amd64 encodings the
x/arch decoder rejects or degenerates: the BMI1/BMI2 VEX families
(ANDN, BEXTR, BLSI, BLSMSK, BLSR, BZHI, MULX, PDEP, PEXT, RORX,
SARX, SHLX, SHRX), the 0F 01 quartet CLAC, STAC, RDPKRU and WRPKRU,
the bare and REX-only RDSEED forms, and UD1.  The supplementary
naming table decodes the VEX prefix and the ModR/M shape and renders
the toolchain's own spellings; every corpus row is pinned in the
unlisted fixture and round-trips byte for byte through the encoder,
and the boundary test pins the prefix shapes no family carries.

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin d98aadbbbf test(disasm): drop the closed byte-width divergences from the parity map
The operand-width reconciliation closes the byte-register fixture lines
byte for byte: XADDL, XCHGL, CMPXCHGL and CRC32 with byte registers, the
ALU and TEST immediates against AL and DL, and the unlisted accumulator
short forms.  MOVL $0x7, DL stays mapped for the legal-encoding choice
alone: the toolchain's own table says "c6c207 or b207", go tool asm emits
b207, and the fixed-point invariant holds with it.

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin 8bded3ea39 test(asm): pin the byte-form width reconciliation and its oracle
Table-driven rows for the renderer's spellings (the L suffix or none with
a byte register encodes the byte form, every register joining at its low
byte), the refusals (W, Q and the MOVD alias take no byte register) and a
differential kernel of the B-suffixed spellings assembled through both
gasm and go tool asm, byte for byte.

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin 98a562d8b3 fix(asm): settle the byte-form width from the register operands
The suffixed scalar families derived the operand width from the mnemonic
alone, so a byte-spelled register under the L spelling or no suffix at all
encoded the widened form: XADDL DL, DL emitted 0F C1 where the byte form is
0F C0, CMPL AL, $7 emitted the 32-bit immediate form where the AL form is
3C 07, and CRC32 DL, R11 widened past the F0 byte opcode.  operandWidth now
reconciles the suffix with the operands: a byte register (AL, DL, R8B, ...)
forces the 8-bit form, which is the text the toolchain's own disassembly
prints for those encodings, while the W and Q spellings never ride a byte
register and are refused as go tool asm refuses them (MOVQ AL, AX).  The
shift count and the two- and three-operand IMUL forms stay out of the
reconciliation, and the byte accumulator short forms now belong to the AL
spelling alone, matching the toolchain's division (ADDB $3, AX is
80 c0 03, TESTB $7, AX is f6 c0 07).

Assisted-by: GLM 5.3
2026-10-07 13:49:58 +02:00
petrbalvin 33e7fdac98 fix(asm): enforce the arm64 TLBI, RPRFM, FCVT and integer-pair arities
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 7a69be8b59 fix(asm): reject the arm64 REGTMP spellings the toolchain refuses
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 2385bb7069 fix(asm): enforce the arm64 VLD/VST post-index contract
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 7bc80ccb54 fix(asm): emit nothing for the arm64 NOP pseudo-instruction
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 405e2427ed fix(asm): place the arm64 literal pool the way the toolchain flushes it
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 4fc96decc4 fix(asm): size the arm64 logical-immediate materialisation exactly
Assisted-by: GLM 5.3
2026-10-07 13:49:49 +02:00
petrbalvin 4ccd3bb4c6 test(debug): put the dead-debuggee and kill audits on deterministic ground
The launch-failure audit raced the clock: it asserted the dead debuggee
surfaced within 1.5 seconds, a bound the loaded machine behind a ten-way
test storm regularly starved past even though the poll detects the dead
notice within milliseconds of its appearance.  The audit now proves the
property itself: a stub debuggee that starts in single-digit milliseconds
marks itself dead, so the notice is always inside the poll's budget and
the error must come from the dead-file watch, while the real binary is
checked without any wall-clock bound.  A companion audit drives Kill
through a parked, a doubly killed and a run-to-exit session, the states
whose cleanup used to hang the package, under a watchdog.

Assisted-by: GLM 5.3
2026-10-07 13:46:29 +02:00
petrbalvin b5d6f1b46a fix(debug): target the traced thread and keep the kill from ever blocking
The Go runtime can migrate the debuggee's target-mode goroutine off the
process leader before PTRACE_TRACEME, which left the trace relation on a
thread the session never addressed: its stops starved the waits on the
leader, and a kill sequence that resumed nothing and then blocked in
Wait4 hung the whole package.  The debuggee now reports the traced thread
in the launch handshake and parks with a thread-directed stop, every
ptrace request and wait addresses that thread, a SIGURG arriving on a
single-step resumes it as a single-step again instead of letting the
tracee run uncontrolled, a resume rejected with ESRCH lifts a group-stop
with SIGCONT and retries once, and Kill resumes, kills and reaps through
non-blocking waits so it returns for a tracee in any state.

Assisted-by: GLM 5.3
2026-10-07 13:46:18 +02:00
petrbalvin 409c8b348d fix(asm): bound the arm64 VTBL table list before the destination read
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 2b2a72d54e fix(asm): encode the arm64 bitfield aliases with their own opc
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 8ab99c9b0c fix(asm): tighten the arm64 acceptance toward the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 82d741514a fix(asm): key the arm64 immediate class order on the ZR spelling
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin f20aa156d0 fix(asm): treat the arm64 $-8 frame as frameless and encode the RET forms
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin d853432dba fix(asm): route the arm64 logical immediates to ZR through REGTMP
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin fbdad8424f feat(asm): encode the arm64 FP immediate moves
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin a9b54b6868 fix(asm): carry the arm64 immediate to ZR through MOVZ
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin d786b90fa1 feat(asm): lower the arm64 con(register) form to the ADD chain
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 9ef14bdb71 feat(asm): encode the arm64 SIMD arrangement bits
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 458cdd2066 feat(asm): encode the arm64 register-offset addressing forms
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin a344399b81 fix(asm): set the CASALH opcode bit fifteen
The CASALH entry carried the CASB/CASH opcode pattern where the acquire
forms take the full fixed field, so the word differed from the
toolchain's in one opcode bit.  Pinned against the oracle word.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin a0fa7e802c feat(asm): pool the arm64 offsets the split bands cannot carry
Offsets beyond the split bands ride a per-function literal pool the way
the toolchain lays one out: a PC-relative literal load into REGTMP, then
the register-offset access (the pair family adds the base addition), the
pooled words appended after the last instruction behind the UNDEF guard,
deduplicated by value with the sign- and width-aware load selection.

The same differential pass against the corpus exposed three wrong-code
bugs and fixes them: the logical-immediate period marker rode the wrong
position for every element below 64 bits, so the 32-bit forms encoded a
different constant than written; the plain register operand of an
ADD/SUB against SP took the shifted-register form where the toolchain
uses the extended one with the identity extend, silently truncating
through UXTB; and the AUTIA1716 and AUTIB1716 hint constants were the
PACIA and PACIB encodings.  An offset sweep across every band boundary
now pins all three against the live oracle.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 71e8dd550d feat(asm): split wide arm64 load and store offsets into REGTMP
Offsets the single-instruction forms cannot carry lower the way the
toolchain lowers them: ADD or SUB moves the whole distance into REGTMP
within the ±4095 band, and the 24-bit band above it splits into an ADD of
the high half and an access of the low half, with the pair family taking
the two-ADD sequence.  The split band follows loadStoreClass per width,
byte accesses taking the full 24 bits and the Q width the widest, so an
offset the toolchain pools is never split instead.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 2bd52eb7ad fix(asm): scale the FLDPQ and FSTPQ pair offsets by sixteen
The pair encoder derived the imm7 divisor from the width suffix alone, so
the 128-bit FP pairs divided their offsets by eight and encoded twice the
distance.  The Q spellings scale by sixteen like every other 128-bit
access; the differential kernel carries them now.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin daf7fad5b9 feat(asm): encode the arm64 Q-width FP load and store
FMOVQ routes through the MOV load/store machinery in the plain, post-index,
pre-index and static-symbol forms.  The Q width carries its size in the opc
field, so the store spelling is opc=10 and the access scales by sixteen;
both come from helpers now instead of the size exponent.  The static-symbol
form takes the toolchain's twelve-byte ADRP + ADD + access fallback with the
R_ADDRARM64 pair.  The register-to-register and immediate forms stay
rejected, matching the toolchain's own table.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin a6bd9c1ebe feat(asm): encode the LDEORAL acquire variants
Assisted-by: GLM 5.3 Flash
2026-10-07 02:36:24 +02:00
petrbalvin 458d981f31 feat(disasm): name the CLDEMOTE encoding the decoder refuses
The hint NOP opcode 0F 1C /r with a memory operand is CLDEMOTE, a
memory-only instruction the toolchain's own table carries; the decoder
rejects the encoding instead of naming it.  The rejected-encoding side
of the supplementary table names it from the bytes, and the corpus row
0f1c03 pins the text in the unlisted fixture, round trip byte exact.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:27:51 +02:00
petrbalvin dd356b9e6a fix(disasm): name the amd64 families x/arch decodes to the zero opcode
x/arch reports the ADCX, ADOX, RDSEED, RDPID, TPAUSE, UMONITOR, UMWAIT
and ENDBR families with no error but the degenerate zero instruction,
which GoSyntax renders as Op(0) under its prefix decoration and with a
length of one.  A supplementary naming table keyed by the opcode
pattern restores the toolchain's own spellings and lengths; the parity
fixtures pin all 41 corpus rows (ENDBR32 alone, which the toolchain
cannot spell, pins as bytes and text in the focused naming test).

Assisted-by: GLM 5.3 Flash
2026-10-07 02:27:51 +02:00
petrbalvin 6c4932c4ec feat(arch): the SVE2.1 Z-alias permutations and copies
Assisted-by: GLM 5.3 Flash
2026-10-07 02:27:35 +02:00
petrbalvin 8231302bca feat(arch): the SVE predicate family in the extended layer
Assisted-by: GLM 5.3 Flash
2026-10-07 02:26:19 +02:00
petrbalvin 83052ab466 feat(lint): accept the wired extension mnemonics
Assisted-by: GLM 5.3 Flash
2026-10-07 02:23:37 +02:00
petrbalvin 9c951c232e feat(asm): assemble the extended instruction layer on arm64
Assisted-by: GLM 5.3 Flash
2026-10-07 02:23:37 +02:00
petrbalvin c2adde948f feat(arch): add the write mask to the packed amd64 destinations
Assisted-by: GLM 5.3 Flash
2026-10-07 02:21:57 +02:00
petrbalvin 631fb8a8d7 chore: keep the fuzz cache and pending reproductions out of the tree
Assisted-by: GLM 5.3 Flash
2026-10-07 02:14:37 +02:00
petrbalvin 470639cd68 test(asm): carry the fuzz pipeline to arm64
Assisted-by: GLM 5.3 Flash
2026-10-07 02:14:37 +02:00
petrbalvin e7a1c20467 fix(asm): prefix MOVQ2DQ with F3
The two bank-crossing quadword moves take the mandatory prefix by
direction: the toolchain renders F3 0F D6 as MOVQ2DQ with the MMX
source and F2 0F D6 as MOVDQ2Q with the XMM source, and the encoder
emitted F2 for both, so MOVQ2DQ encoded MOVDQ2Q.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:12:21 +02:00
petrbalvin 0cfed5516c fix(asm): carry the riscv64 U-type immediate raw
The toolchain writes the source immediate straight into imm[31:12]
(riscv64.s: AUIPC 24287, X10 encodes 7ffff517), and rejects values
beyond the signed 20-bit span; the encoder divided by 4096 instead and
truncated silently, so the high bits of every large AUIPC and LUI were
lost.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:12:21 +02:00
petrbalvin a829b8f321 fix(parser): read the segment-absolute rendering FS:0
The toolchain's disassembler prints the segment-prefixed disp32
absolute as FS:0, but the bare-name branch read the segment register
alone and dropped the offset, so MOVQ FS:0, DX silently encoded a
register move.  The colon-offset form now lowers to the same
segment-absolute operand the 0(FS) spelling takes.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:12:21 +02:00
petrbalvin abc2d32b83 fix(asm): encode the CMOV condition the renderer prints
The renderer spells a conditional move CMOV plus the condition alone
(CMOVLE, CMOVG), the width carried by the operand registers, so
CMOVLE parsed as the size L and the condition E and encoded CMOVE.
A suffix that is itself a condition name now reads as that condition
with the width from the destination register, and the Plan 9
size-prefixed spellings keep their parse.

Assisted-by: GLM 5.3 Flash
2026-10-07 02:12:21 +02:00
petrbalvin 11cac26508 feat(arch): add the scaled index to the amd64 memory operands
Assisted-by: GLM 5.3 Flash
2026-10-07 02:06:11 +02:00
petrbalvin ccb155437e feat(arch): add the {1toN} broadcast to the packed amd64 memory sources
Assisted-by: GLM 5.3 Flash
2026-10-07 02:06:11 +02:00
petrbalvin 9c1392dfc3 test(disasm): pin the amd64 lines the objdump listing fragments
Assisted-by: GLM 5.3 Flash
2026-10-07 01:57:04 +02:00
petrbalvin 8d611bfdaf feat(arch): add the remaining scalar FP16 memory forms to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 01:35:48 +02:00
petrbalvin 454a21f5b7 feat(arch): add the packed FP16 and BF16 memory forms to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 01:35:48 +02:00
petrbalvin d275dee3ae feat(arch): add the scalar FP16 memory forms to the amd64 extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 01:35:48 +02:00
petrbalvin 7d69dda874 feat(arch): add the amd64 memory-operand mechanism to the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 01:35:48 +02:00
petrbalvin d1030a1788 test(verify): prove the arm64 lse atomics through the link-parity kernel
Assisted-by: GLM 5.3 Flash
2026-10-07 01:31:28 +02:00
petrbalvin 8e3b7f1caa test(verify): prove the amd64 sse families through the link-parity kernel
Assisted-by: GLM 5.3 Flash
2026-10-07 01:31:28 +02:00
petrbalvin b22abf512f test(verify): prove the riscv64 vector families through the link-parity kernel
Assisted-by: GLM 5.3 Flash
2026-10-07 01:31:28 +02:00
petrbalvin ffc23929c6 refactor(verify): extract the shared goobj link-parity harness
Assisted-by: GLM 5.3 Flash
2026-10-07 01:31:28 +02:00
petrbalvin a4a77b3168 build(justfile): lower the fuzz worker default to the fence the assembler target holds
Assisted-by: GLM 5.3 Flash
2026-10-07 01:15:42 +02:00
petrbalvin 380314a05f test(asm): keep the data-section ceiling out of the seed corpus
Assisted-by: GLM 5.3 Flash
2026-10-07 01:14:58 +02:00
petrbalvin de2f28504a test(asm): parse once and assemble twice in the fuzz body
Assisted-by: GLM 5.3 Flash
2026-10-07 01:14:58 +02:00
petrbalvin 7878112b93 fix(asm): bound the data section to what the image can materialise
Assisted-by: GLM 5.3 Flash
2026-10-07 01:14:58 +02:00
petrbalvin dd074dbafd fix(asm): reject an out-of-range GLOBL size with a diagnostic
Assisted-by: GLM 5.3 Flash
2026-10-07 01:13:40 +02:00
petrbalvin 14de1dd287 test(asm): fuzz the parse-and-assemble pipeline for amd64
Assisted-by: GLM 5.3 Flash
2026-10-07 01:12:27 +02:00
petrbalvin ea8b02b190 test(disasm): pin decode parity with the toolchain and the round-trip invariant
Assisted-by: GLM 5.3 Flash
2026-10-07 01:00:45 +02:00
petrbalvin b394551af8 fix(disasm): render amd64 in the toolchain's Go syntax
Assisted-by: GLM 5.3 Flash
2026-10-07 01:00:45 +02:00
petrbalvin 9b751f6e58 feat(asm): encode the riscv64 quad-precision family and fix the fp cvt paths
Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin 955bc6643e fix(asm): carry the riscv64 scalar pseudos and the swapped branches
Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin 82e8919208 feat(asm): encode the riscv64 privileged instructions
Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin 778c297214 feat(asm): encode the riscv64 vector arithmetic families
Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin 5de9e985f8 feat(asm): encode the riscv64 vector load and store families
The vector memory section stopped at the three hand-written shapes the
GOROOT kernels use: every other spelling the toolchain accepts, the
width variants, the constant-stride and indexed accesses, the segment
families, the fault-only-first loads, the whole-register moves and the
bit-mask pair were names without an encoder.  The mnemonic now parses
into its own fields (direction, segment count, addressing mode, width,
fault-only-first and whole-register markers) and one encoder lays the
word down, with the optional V0 mask operand and the toolchain's
operand shapes.  VSETVL joins the configuration settings.  The
toolchain's whole vector memory section, six hundred and twenty-eight
statements of masked and unmasked forms, is a differential test against
the oracle, word for word.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin e43fa39dc9 feat(asm): encode the explicit riscv64 compressed instructions
The C extension's own spellings were names the table carried and the
encoder refused: CLWSP stopped the corpus audit's riscv64 file first.
Thirty-eight mnemonics now encode directly to their halfword, with the
toolchain's operand spellings and validation: the stack loads and stores
pin their base to SP, the register-based loads, stores and arithmetic
carry prime registers, CLUI refuses zero and SP, CADDI4SPN scales by
four, CADDI16SP by sixteen, and CJ, CBEQZ and CBNEZ resolve their N(PC)
targets against the final layout, taking a two-byte placeholder in the
early passes so the offsets stay honest.  CAND with an immediate is the
toolchain's C.ANDI spelling.  The toolchain's whole C extension testdata
block is a differential test, halfword for halfword, beside a range test
at the toolchain's own boundaries.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin ddfa33ccc1 feat(asm): encode the riscv64 bit-manipulation families
The Zba address generation, Zbb unary bit operations, Zbc carry-less
multiplication and Zbs single-bit families were names the table carried
and the encoder refused: thirty spellings plus RORI and XNOR fell over.
The register and immediate forms now encode as the toolchain does, the
unary operations carry their fixed rs2 constant, RORI lowers to ROR's
expansion (its reverse shift compressing like ROR's), XNOR XORs and
inverts in place, and ROL/ROLW rotate left through the same temporary
the toolchain uses, taking a register amount only as its own expansion
requires.  The toolchain's whole testdata block for these families is
now a differential test: every word must agree byte for byte.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin d82ef33fa9 fix(asm): bound the riscv64 shift immediate at the instruction width
SLLI $64 assembled with the amount silently masked into the six-bit
field where the toolchain rejects it, and the word forms took 0-63 where
they take 0-31.  Both families now validate against their own width and
the check reads the immediate at full width, so a value the source
spelled beyond int32 cannot wrap into the range; the boundary is pinned
in a test.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin c3540f0549 fix(asm): read the riscv64 raw-data immediates at full width
WORD and BYTE read their immediate through the truncating helper, so the
int32 wrap turned WORD $0xffffffff into -1 and rejected it, while WORD
$0x100000000 and BYTE $0x100000001 arrived pre-truncated and slipped
past the range check as small values.  Both statements now read the
immediate the source wrote and bound it at the toolchain's own limits:
[0, 0xffffffff] for WORD, [0, 0xff] for BYTE, with the bounds pinned in
a test.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin bffe408afa feat(asm): resolve every CSR name the toolchain knows
The riscv64 assembler carried fourteen hand-picked CSR names where the
toolchain resolves three hundred and twenty-nine: a CSRR/CSRW family
instruction naming any privileged register beyond the few base ones came
out as unknown CSR.  The table now carries the RISC-V privileged
specification's register set exactly as go tool asm spells it, and a
differential test assembles every name through both assemblers and
requires the words to agree byte for byte.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:47:27 +02:00
petrbalvin 103864e8b2 feat(arch): add the VL packed FP16 forms to the amd64 extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 00:37:41 +02:00
petrbalvin aa9c7ca030 feat(arch): add the imm8 scalar FP16 controls to the amd64 extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 00:33:30 +02:00
petrbalvin 0354a1f4c1 feat(arch): scale and exponent-extract the scalar FP16 in the extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin 127e52de69 test(verify): accumulate the BF16 dot product on the metal
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin 22055b9bf3 feat(arch): add the packed FP16 arithmetic to the amd64 extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin a2301b52de test(verify): run the amd64 extension encodings on the metal
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin bc37ea5b79 feat(arch): add the AVX512-FP16 scalar family to the amd64 extension layer
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin 28bea95128 feat(arch): add the amd64 extended-instruction layer with BF16 and VP2INTERSECT
Assisted-by: GLM 5.3 Flash
2026-10-07 00:07:58 +02:00
petrbalvin 2d803e38d8 feat(lsp): leave the extension verdicts to the registry
Diagnostics no longer repeat the generated table's ignorance of a
registered mnemonic: unknown-instruction and unencodable-instruction
against a statement the registry encodes are filtered from the server's
own presentation, driven by asm.LookupExtension directly.  The filter is
a no-op once lint learns the registry, so the two compose unchanged.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:06:05 +02:00
petrbalvin dadeda144a feat(lsp): offer and document the registered extended mnemonics
Completion merges the extension registry's mnemonics beside the toolchain
entries, with operand shapes measured against the layer's own encoder, and
hover documents a registered mnemonic from its metadata: the extension
notice, the forms, the features, the fixed encodings and the manual
references.

Assisted-by: GLM 5.3 Flash
2026-10-07 00:06:05 +02:00
petrbalvin a90ec84bee fix(arch): narrow file names by go/build's suffix rule
Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin c107b45933 fix(asm): reject duplicate symbol declarations like the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin a69f8cf4a8 fix(asm): match the toolchain's bytes across the corpus sweep
A line-for-line byte comparison of the whole amd64enc.s corpus against
go tool asm surfaced divergences the pass-only accounting never showed:
PEXTRW's GPR form swapped its fields, PUSHW took an imm32 where the
toolchain bounds the immediate to 16 bits, the double shift wrote the
unmasked register number into the reg field, VCOMISS carried a 0x66
prefix, RORX dropped the destination's R bit, and the variable bit
shifts used the manual's per-width opcodes where the toolchain
consolidates each row on one opcode with the W bit.  The VEX forms the
toolchain prefers for plain vector registers (the SSE2/SSSE3/SSE4.1
AVX twins, the compare-with-predicate family, VMOVUPS, VSHUFPS, the
variable shifts) now encode under VEX, with EVEX left to the ZMM,
opmask and index-16+ spellings, and the mnemonics whose rows never
offer the 2-byte prefix force it.  Every line is pinned through the new
corpus parity test (793 lines); the whole corpus file now assembles to
the toolchain's bytes at every commented line (10022 of 10022).

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin cfc3abb752 feat(asm): encode the remaining amd64 VEX families
The SSE3 horizontal and add-subtract pairs, the SSSE3 sign and
horizontal integers, the masked moves in both directions, the
reciprocity and test pairs, the AVX imm8 tail (blends, dot products,
inserts, rounds, MPSADBW, the string compares), the four-operand
variable blends with their /is4 mask byte, the scalar three-operand
moves, the MXCSR accessors, the VPERMIL register controls and the
variable word shifts, plus the BMI2 count forms over memory.  Every
encoding is pinned byte for byte against go tool asm through every
corpus line the toolchain's own amd64enc.s carries for the families
(852 lines); the /is4 byte carries the mask register number in its high
nibble, the layout the toolchain emits.

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin 6af3fd60d5 feat(asm): encode the legacy amd64 SSE and MMX families
The packed integer and float binaries, the imm8-controlled SSE4.1 forms,
the variable blends with their X0 mask, the high/low half moves, the
sign-mask extractions, the non-temporal stores, the MOVQ bank crossings
and their odd spellings, the MMX shifts and shuffle and the cache-line
mask stores, plus the scalar leaves LEAVE, INVPCID and the RTM controls.
Every encoding is pinned byte for byte against go tool asm through every
corpus line the toolchain's own amd64enc.s carries for the families
(1465 lines).  Two corpus-wide gaps fell out of the comparison: the
64-bit MOV immediate uses the zero-extending form across the unsigned
32-bit span, and the MMX-to-GPR MOVQ puts the bank register in reg.

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin 257feace6e feat(asm): encode the amd64 XSAVE family
XSAVE, XSAVEOPT, XSAVEC and XSAVES with their restore twins, plain and
64, each pinned byte for byte against go tool asm through every corpus
line the toolchain's own amd64enc.s carries for the family (24 lines).
The toolchain emits XSAVEOPT without the manual's 0x66 prefix; the bytes
are the oracle, so the family carries none.

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin f83bc8ddef feat(asm): encode the amd64 system, string and segment families
The no-operand flag and system controls, the sign-extension pair, the
string primitives, the multi-byte no-ops, the cache controls, MOVBE, the
compare-exchange doubles, the random source and FS/GS base pairs, the
descriptor-table accesses, the 0F 00/01 register controls and the
LAR/LSL selector reads and far-segment loads, each pinned byte for byte
against go tool asm through every corpus line the toolchain's own
amd64enc.s carries for the families (279 lines).

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin f0b08ccea0 feat(asm): encode the amd64 x87 family
The x87 stack controls, the D8/DC arithmetic pair, the conditional moves,
the register compares, FADDDP, the memory loads and the FXSAVE pair, each
pinned byte for byte against go tool asm through every corpus line the
toolchain's own amd64enc.s carries for the family (78 lines).

Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:47 +02:00
petrbalvin 1040739fbb fix(asm): validate the loong64 ll/sc offset span like the toolchain
Assisted-by: GLM 5.3 Flash
2026-10-06 23:59:27 +02:00
petrbalvin 6cc6165c2b build(justfile): cap the fuzz recipe's workers
Assisted-by: GLM 5.3 Flash
2026-10-06 21:13:31 +02:00
petrbalvin 0768b4dc84 fix(cmd): locate GOROOT when the build is trimmed
Assisted-by: GLM 5.3 Flash
2026-10-06 20:24:39 +02:00
petrbalvin ae1baa0e61 ci: run the affordable gate set on push and publish at the tag
Test / test (push) Successful in 1m45s
Assisted-by: DeepSeek V4.1 Flash
2026-10-04 21:17:40 +02:00
petrbalvin 5a8e9acbf3 feat(arch): add the extended-instruction layer with SVE arithmetic
Test / test (push) Successful in 3m38s
2026-10-02 20:39:33 +02:00
petrbalvin 2747fce7d3 feat(asm): encode the arm64 system registers and structure loads 2026-10-02 20:39:26 +02:00
petrbalvin e02918c17b fix(ci): keep the push suite inside the runner's memory and time budget
Test / test (push) Successful in 2m55s
Assisted-by: GLM 5.3 Flash
2026-10-02 17:20:31 +02:00
petrbalvin 02a6359c1f ci: shrink the push pipeline to the affordable gate set
Test / test (push) Failing after 5m26s
Assisted-by: GLM 5.3 Flash
2026-10-02 16:47:52 +02:00
petrbalvin b4c1e133c0 ci: keep the GOOBJ link parity gate off the push pipeline
Test / test (push) Failing after 12m18s
Assisted-by: GLM 5.3 Flash
2026-10-02 16:15:02 +02:00
petrbalvin 1107928870 build(justfile): run the test recipes under the memory fence
Assisted-by: GLM 5.3 Flash
2026-10-02 16:15:02 +02:00
petrbalvin bc4ac93fd9 style(asm): reindent the evex comment gofmt asks for
Test / test (push) Failing after 21m29s
Assisted-by: GLM 5.3
2026-10-02 00:41:46 +02:00
petrbalvin c1bca7ce7e docs: record the development deltas in the changelog
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin fefb76beb9 docs(asm): correct the reference against the assemblers' behaviour
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin 42bc1669d7 feat(lsp): document directives on hover and widen completion
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin 69dcbec8ef feat(lint): eleven new rules over directives, data and addressing
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin f405cea5bc fix(cmd): stop the coverage run on a stray in-place trap
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin f57377abb9 fix(debug): handle mapping edges, stray traps and dying debuggees
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin dd1782c538 test(verify): gate GOOBJ link parity with cmd/link
Assisted-by: GLM 5.3
2026-10-02 00:40:54 +02:00
petrbalvin 17cc49fee4 fix(asm): emit NOPTR data as its own symbol kind
Assisted-by: GLM 5.3
2026-10-02 00:40:43 +02:00
petrbalvin bafb2fd130 feat(asm): encode the amd64 and loong64 tails of the corpus testdata
Assisted-by: GLM 5.3
2026-10-02 00:40:43 +02:00
petrbalvin 2f679326c2 style(testdata): canonicalise forms_amd64.s
Assisted-by: GLM 5.3
2026-10-02 00:40:20 +02:00
petrbalvin 96f2dd65b4 test(lexer): fuzz the token stream invariants
Assisted-by: GLM 5.3
2026-10-02 00:40:20 +02:00
petrbalvin ca887d3927 fix(parser): bound folding depth and macro expansion work
Assisted-by: GLM 5.3
2026-10-02 00:40:20 +02:00
petrbalvin 6570709226 fix(parser): peel stacked labels the way the formatter renders them
Assisted-by: GLM 5.3
2026-10-02 00:40:20 +02:00
petrbalvin 9a5217d9c1 fix(format): keep every token of a line in the canonical output
Assisted-by: GLM 5.3
2026-10-02 00:40:20 +02:00
petrbalvin 4d01bb3ecf build: rename the module to sourcedock.dev/petrbalvin/gasm-sdk
Test / test (push) Successful in 4m18s
2026-09-26 11:08:43 +02:00
petrbalvin 332c63e440 ci: compile-gate FreeBSD in the test pipeline
Test / test (push) Successful in 4m10s
Assisted-by: GLM 5.3 Flash
2026-09-25 21:46:49 +02:00
petrbalvin b306c210c6 feat(debug): port the debugger to FreeBSD
Assisted-by: GLM 5.3 Flash
2026-09-25 21:46:40 +02:00
petrbalvin b9015e1c2e fix(verify): make the executable mapping build on FreeBSD
Assisted-by: GLM 5.3 Flash
2026-09-25 21:46:31 +02:00
petrbalvin 26c5008136 fix(cmd): honour //go:build in the corpus audit
Test / test (push) Successful in 2m32s
Assisted-by: GLM 5.3 Flash
2026-09-23 21:03:16 +02:00
petrbalvin 74d6b90d69 fix(asm): read the arm64 move-wide immediate as an unsigned pattern
Assisted-by: GLM 5.3 Flash
2026-09-23 21:03:03 +02:00
petrbalvin 7b11c62f53 fix(asm): resolve negative numeric PC-relative jumps
Assisted-by: GLM 5.3 Flash
2026-09-23 21:02:50 +02:00
petrbalvin 8eed54b3da feat(lsp): quick fixes for the textflag include and the argument area
Test / test (push) Successful in 2m56s
Assisted-by: GLM 5.3 Flash
2026-09-23 20:23:35 +02:00
petrbalvin 4be16dcdf5 feat(lsp): resolve symbols across workspace files
Assisted-by: GLM 5.3 Flash
2026-09-23 20:21:58 +02:00
petrbalvin ded9cabdf4 fix(lsp): apply each rename edit to its own document
Assisted-by: GLM 5.3 Flash
2026-09-23 20:19:21 +02:00
petrbalvin cf6bc6987e fix(ci): pass the upload file to curl, not its interpolation
Test / test (push) Successful in 2m26s
Release / gates (push) Successful in 2m25s
Release / build (amd64, linux) (push) Successful in 1m15s
Release / build (arm64, linux) (push) Successful in 1m16s
Release / build (loong64, linux) (push) Successful in 1m16s
Release / build (riscv64, linux) (push) Successful in 1m16s
Release / release (push) Successful in 34s
2026-09-22 01:31:49 +02:00
petrbalvin ff7b1452b1 docs: name 0.35.0 as the supported release
Test / test (push) Successful in 2m33s
Release / gates (push) Successful in 2m29s
Release / build (amd64, linux) (push) Successful in 1m18s
Release / build (arm64, linux) (push) Successful in 1m20s
Release / build (loong64, linux) (push) Successful in 1m17s
Release / build (riscv64, linux) (push) Successful in 1m26s
Release / release (push) Failing after 35s
2026-09-22 00:52:56 +02:00
petrbalvin 517c1cea25 chore: prepare release v0.35.0
Test / test (push) Successful in 2m33s
Release / gates (push) Failing after 46s
Release / build (amd64, linux) (push) Skipped
Release / build (arm64, linux) (push) Skipped
Release / build (loong64, linux) (push) Skipped
Release / build (riscv64, linux) (push) Skipped
Release / release (push) Skipped
2026-09-22 00:44:10 +02:00
petrbalvin a3e3010e0f fix(cmd): resolve the runtime header test GOROOT from the go command
Test / test (push) Successful in 2m39s
2026-09-21 22:46:07 +02:00
petrbalvin 057c4eb545 docs: complete the release delta in the changelog and readme 2026-09-21 22:45:56 +02:00
petrbalvin f720381d43 feat(asm): the segment-absolute and crash-store forms GOROOT writes
Test / test (push) Failing after 2m28s
Assisted-by: GLM 5.3 Flash
2026-09-21 22:19:53 +02:00
petrbalvin 2c9042d62c feat(asm): PCALIGN alignment on amd64
Assisted-by: GLM 5.3 Flash
2026-09-21 22:00:30 +02:00
petrbalvin 82ef289d3a feat(asm): the immediate multiply and arm64 indirect branches GOROOT writes
Assisted-by: GLM 5.3 Flash
2026-09-21 21:50:11 +02:00
petrbalvin 7246b0e002 feat(asm): the TLS access pair in the toolchain's one-instruction form
Assisted-by: GLM 5.3 Flash
2026-09-21 21:35:15 +02:00
petrbalvin 8cfd40aac8 feat(asm): the operand forms and defines GOROOT writes
Assisted-by: GLM 5.3 Flash
2026-09-21 21:17:34 +02:00
petrbalvin 5382c9a8e4 feat(audit): list every corpus failure per architecture 2026-09-21 21:17:34 +02:00
petrbalvin 53de91b2df docs(asm): describe the four target architectures
Test / test (push) Failing after 2m23s
Assisted-by: GLM 5.3 Flash
2026-09-21 20:15:55 +02:00
petrbalvin 8a36af7c7d docs(asm): generate the instruction appendices
Assisted-by: GLM 5.3 Flash
2026-09-21 20:15:55 +02:00
petrbalvin e9789ce3f4 chore(arch): regenerate the instruction tables 2026-09-21 20:15:55 +02:00
petrbalvin 837231c068 docs(asm): open the assembly language reference
Assisted-by: GLM 5.3 Flash
2026-09-21 19:49:04 +02:00
petrbalvin 95025be1bc docs(changelog): describe the encoder entries by content
Test / test (push) Failing after 2m33s
2026-09-21 19:20:01 +02:00
petrbalvin 03a964bb2d docs(goobj): document the GOOBJ object file format 2026-09-21 19:19:53 +02:00
petrbalvin 123a16e346 docs(readme): state the documentation goal 2026-09-21 18:35:27 +02:00
petrbalvin 9701812bee docs: changelog for the completeness waves
Test / test (push) Failing after 3m6s
Assisted-by: GLM 5.3 Flash
2026-09-21 02:04:44 +02:00
petrbalvin 29ac03468e feat(amd64): floating-point immediates through a synthesised pool
Assisted-by: GLM 5.3 Flash
2026-09-21 02:04:44 +02:00
petrbalvin bfb7701db1 feat(amd64): emit the quad-register EVEX families
Assisted-by: GLM 5.3 Flash
2026-09-21 02:02:19 +02:00
petrbalvin e8b6ff5d7c fix(parser): fold a signed parenthesised displacement expression
Test / test (push) Failing after 2m21s
Assisted-by: GLM 5.3 Flash
2026-09-21 00:45:33 +02:00
petrbalvin 1456907000 feat(riscv64,loong64): operand tail, float DATA and honest port classification
Assisted-by: GLM 5.3 Flash
2026-09-21 00:44:47 +02:00
petrbalvin ec1c521187 feat(cmd): GOOS-aware headers, audit battery shapes and semicolon spacing
Test / test (push) Failing after 2m21s
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:46 +02:00
petrbalvin a7744c24bd fix(parser): substitute macro parameters behind element selectors
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:19 +02:00
petrbalvin 522e6f2ae8 feat(parser): bracket register ranges, index-only VSIB and bare trailing immediates
Assisted-by: GLM 5.3 Flash
2026-09-20 22:02:19 +02:00
petrbalvin 81d4bd81e4 test(verify): register the wave kernels
Test / test (push) Failing after 2m20s
Assisted-by: GLM 5.3 Flash
2026-09-20 21:17:31 +02:00
petrbalvin 687678a2ea feat(elf): emit data relocations on arm64, riscv64 and loong64
Assisted-by: GLM 5.3 Flash
2026-09-20 21:17:20 +02:00
petrbalvin b0f9071bf5 feat(arm64): whole-vector moves, bookkeeping ops and truncating-move lowering
Assisted-by: GLM 5.3 Flash
2026-09-20 21:17:20 +02:00
petrbalvin 81e2673923 feat(amd64): encode the AVX-512 and BMI corpus families
Assisted-by: GLM 5.3 Flash
2026-09-20 21:17:20 +02:00
petrbalvin 75e9fd771b feat(parser): split plain statements on semicolons in the raw parse
Test / test (push) Failing after 2m30s
Assisted-by: GLM 5.3 Flash
2026-09-20 19:15:35 +02:00
petrbalvin 863926abd6 test(verify): register the loong64 vector kernels
Assisted-by: GLM 5.3 Flash
2026-09-20 19:15:05 +02:00
petrbalvin 241e7256f6 fix(arm64): reject bare BTI with a diagnostic and accept the full family
Assisted-by: GLM 5.3 Flash
2026-09-20 19:15:05 +02:00
petrbalvin 6556b85abf feat(asm): symbol-valued DATA, division slash in symbols and plain semicolons
Assisted-by: GLM 5.3 Flash
2026-09-20 19:15:05 +02:00
petrbalvin 289cabe993 feat(loong64): encode the full LSX and LASX table
Assisted-by: GLM 5.3 Flash
2026-09-20 19:14:43 +02:00
petrbalvin d6cf7cfa44 fix(format): keep statement separators and canonical macro bodies
Assisted-by: GLM 5.3 Flash
2026-09-20 19:14:43 +02:00
petrbalvin 4cc2f0eba5 feat(cmd): generate go_asm.h for package-context assembly
Assisted-by: GLM 5.3 Flash
2026-09-20 19:14:43 +02:00
petrbalvin 97dfaa7526 docs: changelog for macro expansion and the corrected corpus audit
Test / test (push) Successful in 2m14s
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin 66aa4dbc8b test(verify): register the campaign kernels in the ground-truth suites
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin dce5d31462 feat(amd64): LOCK and REP prefixes, literal data pseudo-ops and ADJSP
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin 9dc3987e02 feat(riscv64,loong64): PCALIGN, branch relaxation and operand shapes
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin 9b238a525a feat(arm64): wide immediates, SIMD compare and system operand forms
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin ad82aac663 feat(parser): macro expansion, conditionals and include splicing with -I
Assisted-by: GLM 5.3 Flash
2026-09-20 14:25:47 +02:00
petrbalvin 0629f5e2df feat(arm64): assemble PCALIGN padding and BYTE literal bytes
Test / test (push) Successful in 2m16s
Assisted-by: GLM 5.3 Flash
2026-09-20 11:49:05 +02:00
petrbalvin ecb203dcf5 fix(lexer): treat trailing CR as line end so comment text is idempotent
Test / test (push) Successful in 2m13s
Assisted-by: GLM 5.3 Flash
2026-09-20 11:40:39 +02:00
petrbalvin 6c672567f3 feat(amd64): assemble the double-shift and static-SB operand shapes
Assisted-by: GLM 5.3 Flash
2026-09-20 11:40:39 +02:00
petrbalvin cc6e416c59 fix(lint): exempt shift counts, SETcc and ABIInternal from false positives
Assisted-by: GLM 5.3 Flash
2026-09-20 11:40:39 +02:00
petrbalvin c66a47973a fix(format): preserve square brackets in SIMD operands
Test / test (push) Successful in 2m15s
Assisted-by: GLM 5.3 Flash
2026-09-20 09:58:25 +02:00
petrbalvin 9629897202 docs: changelog and readme for the instruction wave and the honest corpus rate
Test / test (push) Successful in 2m17s
Assisted-by: GLM 5.3 Flash
2026-09-20 06:45:03 +02:00
petrbalvin 5399a8a724 feat(audit): probe the new operand shapes and measure attemptable files
Assisted-by: GLM 5.3 Flash
2026-09-20 06:45:03 +02:00
petrbalvin de5d9f358e feat(riscv64,loong64): encode AMO atomics, vector slices and bit ops
Assisted-by: GLM 5.3 Flash
2026-09-20 06:44:51 +02:00
petrbalvin ca3fdce0e0 feat(arm64): encode pairs, atomics, crypto, system and NEON slices
Assisted-by: GLM 5.3 Flash
2026-09-20 06:44:51 +02:00
petrbalvin fc2d92eabd feat(amd64): encode the GOROOT instruction families
Assisted-by: GLM 5.3 Flash
2026-09-20 06:44:51 +02:00
petrbalvin 39d2e80145 ci(release): refuse empty assets and verify what the release serves
Test / test (push) Successful in 2m10s
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 02:01:36 +02:00
petrbalvin 9f4f949c1f chore: prepare release v0.34.0
Test / test (push) Successful in 2m11s
Release / gates (push) Successful in 2m11s
Release / build (amd64, linux) (push) Successful in 1m13s
Release / build (arm64, linux) (push) Successful in 1m10s
Release / build (loong64, linux) (push) Successful in 1m12s
Release / build (riscv64, linux) (push) Successful in 1m33s
Release / release (push) Successful in 58s
2026-09-20 01:44:23 +02:00
petrbalvin f0d5238c47 docs: state the validation status and correct claims the material contradicts
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin 2931bbd6b2 ci(release): refuse a tag the security policy does not name
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin 63562a503a test(justfile): run the CLI and debugger tests outside the coverage set
Assisted-by: DeepSeek V4.1 Flash
2026-09-20 01:40:51 +02:00
petrbalvin e836d6150d docs: changelog entry for the loong64 JIT enablement
Test / test (push) Successful in 2m7s
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin 8a51b060da feat(cmd): enable loong64 JIT execution, all trampolines qemu-validated
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin d3d47db727 test(verify): seed the arm64 ABI kernel arguments
Assisted-by: GLM 5.3
2026-09-20 00:57:02 +02:00
petrbalvin 0758556b7d docs: changelog entries for the parity round and corpus number
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin ddb8440340 fix(cmd): padding-aware ground-truth comparison
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin f15ff66fb1 fix(riscv64): accept the g spelling of the goroutine register
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin 187e4856d3 feat(amd64): encode the mixed-width extend family and PMOVMSKB
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin d315a998ce fix(arm64): store-exclusive operand order and large-frame parity
Assisted-by: GLM 5.3
2026-09-20 00:38:24 +02:00
petrbalvin a6f3828c02 docs: changelog entries for the review fixes
Test / test (push) Successful in 2m4s
Assisted-by: GLM 5.3
2026-09-19 23:49:27 +02:00
petrbalvin e3b35bb817 style(testdata): canonical gasm formatting for the verify kernels
Assisted-by: GLM 5.3
2026-09-19 23:49:27 +02:00
petrbalvin eb0a89e58d ci(release): state the version contract inline
Assisted-by: GLM 5.3
2026-09-19 23:49:27 +02:00
petrbalvin dd32d9e66e chore(justfile): one-line install-man comment and long flag forms
Assisted-by: GLM 5.3
2026-09-19 23:49:27 +02:00
petrbalvin 3a73acb20a docs: drop process labels and refresh the architecture and manual pages
Assisted-by: GLM 5.3
2026-09-19 23:49:27 +02:00
petrbalvin 7604a9443f fix(cmd): usage exit codes, asm output file and cross-arch ground truth
Assisted-by: GLM 5.3
2026-09-19 23:49:19 +02:00
petrbalvin b3908fc43d fix(lsp): parse-error survival, symbol ranges and UTF-16 positions
Assisted-by: GLM 5.3
2026-09-19 23:49:19 +02:00
petrbalvin eb8b0cd316 fix(lint): trailing-label CFG guard and the goroutine alias
Assisted-by: GLM 5.3
2026-09-19 23:49:19 +02:00
petrbalvin a8bfd54ed2 fix(debug): hardware watchpoints, signal stops and breakpoint restore
Assisted-by: GLM 5.3
2026-09-19 23:49:19 +02:00
petrbalvin 375182ef1f fix(verify): arm64 stack save, adaptive canary and host gating
Assisted-by: GLM 5.3
2026-09-19 23:49:19 +02:00
petrbalvin 87b1081c53 fix(goobj): external package and symbol indices and arm64 pair relocations
Assisted-by: GLM 5.3
2026-09-19 23:49:13 +02:00
petrbalvin f3c8510a58 fix(elf): relocation records, DWARF tables and per-architecture frame data
Assisted-by: GLM 5.3
2026-09-19 23:49:13 +02:00
petrbalvin ebdf14939f fix(loong64): FP immediates through R30 and unsigned branch forms
Assisted-by: GLM 5.3
2026-09-19 23:49:13 +02:00
petrbalvin 79a2c16bac fix(riscv64): compressed store offsets, FENCE and branch range checks
Assisted-by: GLM 5.3
2026-09-19 23:49:13 +02:00
petrbalvin 401386956c fix(arm64): encode shifts, divides and multiplies and align sizes with emission
Assisted-by: GLM 5.3
2026-09-19 23:49:07 +02:00
petrbalvin 4258131a3a fix(amd64): correct guard displacements, frameless FP offsets and immediate ranges
Assisted-by: GLM 5.3
2026-09-19 23:49:07 +02:00
petrbalvin 94e09e8070 fix(format): preserve flag separators and normalise CRLF input
Assisted-by: GLM 5.3
2026-09-19 23:48:47 +02:00
petrbalvin 7aefe6a42d fix(parser): parse ABI markers and keep TEXT decls usable on errors
Assisted-by: GLM 5.3
2026-09-19 23:48:47 +02:00
petrbalvin ac1c05c793 fix(lexer): tokenise the flag separator and handle NUL and invalid UTF-8
Assisted-by: GLM 5.3
2026-09-19 23:48:47 +02:00
petrbalvin 93c47a312a feat(docs): man pages for gasm and every command, guarded against CLI drift
Test / test (push) Successful in 2m4s
Assisted-by: GLM 5.3 Flash
2026-09-19 21:18:43 +02:00
petrbalvin 708d0a0a5e docs: trim the changelog entries to user-visible deltas
Assisted-by: GLM 5.3 Flash
2026-09-19 20:54:56 +02:00
petrbalvin 3c8f7cb411 test(format): pin the fuzz-found crashers as regression seeds
Assisted-by: GLM 5.3 Flash
2026-09-19 20:48:51 +02:00
petrbalvin 7c5b7a1419 docs: add the changelog entries and the corpus number to the readme
Assisted-by: GLM 5.3 Flash
2026-09-19 20:48:51 +02:00
petrbalvin bc3f448738 feat(format): fuzz targets for the parser and formatter
Assisted-by: GLM 5.3 Flash
2026-09-19 20:41:43 +02:00
petrbalvin f37f183577 feat(riscv64): GOROOT instruction shapes, DATA order and offset expressions
Assisted-by: GLM 5.3 Flash
2026-09-19 19:58:43 +02:00
petrbalvin 1e77e58250 feat(gasm): audit a .s corpus with audit-instructions --corpus
Assisted-by: GLM 5.3 Flash
2026-09-19 19:27:30 +02:00
petrbalvin 1d0969ed64 feat(gasm): select the asm and diff architecture with -GOARCH
Assisted-by: GLM 5.3 Flash
2026-09-19 19:20:47 +02:00
petrbalvin 23c001be51 feat(asm): encode indirect JMP and CALL on all four architectures
Assisted-by: GLM 5.3 Flash
2026-09-19 19:17:07 +02:00
petrbalvin 96e81cc98d docs: add the Plan 9 assembly case and real-use note to the README
Test / test (push) Successful in 2m6s
2026-09-19 18:06:18 +02:00
petrbalvin c834d98210 docs: bring the document set into the standard shape
Test / test (push) Successful in 2m28s
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:18 +02:00
petrbalvin 03d6d4da54 style: put the repository assembly in gasm fmt canonical form
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:18 +02:00
petrbalvin 0b42ce7952 style: use one spelling for colour across the CLI
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:18 +02:00
petrbalvin 288a64ccd2 ci: align the pipelines with the hand-written templates
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:18 +02:00
petrbalvin 5fddfa704b build: declare the exact toolchain and the canonical recipes
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:18 +02:00
petrbalvin a2bb5eeb4e chore: drop the stale comment from the ignore list
Assisted-by: GLM 5.3 Flash
2026-09-17 20:33:14 +02:00
petrbalvin 48449b7a7f build: declare the go1.27.1 toolchain
Assisted-by: GLM 5.3 Flash
2026-09-16 23:12:31 +02:00
petrbalvin 3de043c494 docs: add SECURITY.md and record the round in the CHANGELOG
Assisted-by: GLM 5.3 Flash
2026-09-16 23:12:31 +02:00
petrbalvin 0078f7be5c style: purge em dashes from the produced text
Assisted-by: GLM 5.3 Flash
2026-09-16 23:12:31 +02:00
petrbalvin 6a7317d141 chore: trim the ignore list to the convention
Assisted-by: GLM 5.3 Flash
2026-09-16 22:53:01 +02:00
petrbalvin d08523caa5 docs: move the recipe and version descriptions with the behaviour
Assisted-by: GLM 5.3 Flash
2026-09-16 22:53:01 +02:00
petrbalvin 20e4b8d9c4 ci: align the pipelines with the hand-written templates
Assisted-by: GLM 5.3 Flash
2026-09-16 22:53:01 +02:00
petrbalvin 61f4247cef refactor(gasm): report the toolchain-recorded version
Assisted-by: GLM 5.3 Flash
2026-09-16 22:53:01 +02:00
petrbalvin 049872ddff build: restore the canonical justfile recipe set
Assisted-by: GLM 5.3 Flash
2026-09-16 22:53:01 +02:00
petrbalvin 3669f64ff6 build: install the gasm binary into the user-local bin directory
Test / vet (push) Successful in 46s
Test / test (push) Successful in 2m44s
Test / build (push) Successful in 42s
2026-09-14 23:41:21 +02:00
petrbalvin fff9f75595 chore: prepare release v0.33.0
Release / build (amd64, linux) (push) Successful in 49s
Release / build (arm64, linux) (push) Successful in 43s
Release / build (loong64, linux) (push) Successful in 46s
Release / build (riscv64, linux) (push) Successful in 45s
Test / vet (push) Successful in 47s
Release / release (push) Successful in 18s
Test / test (push) Successful in 2m39s
Test / build (push) Successful in 43s
2026-09-14 23:36:19 +02:00
petrbalvin 40476546df fix(asm): close the oracle parity gaps in frame addressing and calls 2026-09-14 23:25:14 +02:00
petrbalvin 70218e84ba feat(asm): emit the loong64 stack-split guard for big frames 2026-09-14 22:38:58 +02:00
petrbalvin db50b98179 feat(asm): emit the loong64 stack-split guard for small and medium frames 2026-09-14 21:21:58 +02:00
petrbalvin 2e2c0b82a0 feat(asm): emit the riscv64 stack-split guard and fix large-frame addressing 2026-09-14 21:09:13 +02:00
petrbalvin 8dc1e98ca1 feat(asm): emit the arm64 stack-split guard and morestack block 2026-09-14 20:49:03 +02:00
petrbalvin 1d8e68c574 feat(asm): emit the amd64 stack-split guard and morestack block 2026-09-14 20:35:55 +02:00
petrbalvin 89fa6ea15e feat(lsp): resolve definition and references across open documents 2026-09-14 18:50:55 +02:00
petrbalvin edc20ffa97 feat(cmd): add gasm dis and share the decoder with the debugger 2026-09-14 18:47:08 +02:00
petrbalvin 50db6615b2 feat(cmd): add gofmt-style -l and -d modes to gasm fmt 2026-09-14 18:47:08 +02:00
petrbalvin 95f1d6f083 style: replace em dashes in the scaffold comments 2026-09-14 18:22:25 +02:00
petrbalvin f43e791e5a chore: add .qwen to the gitignore metadata block 2026-09-14 18:22:18 +02:00
petrbalvin 1691c81095 style: replace em and en dashes across sources 2026-09-14 18:22:18 +02:00
petrbalvin 2db563be07 refactor(cmd): consolidate cross-arch verify and drop dead code 2026-09-14 18:22:00 +02:00
petrbalvin 4f190ee1a2 refactor(debug): move watchpoint slot state into the session 2026-09-14 18:22:00 +02:00
petrbalvin 909f874797 fix(lsp): recover from handler panics and decode client uris 2026-09-14 18:22:00 +02:00
petrbalvin e307bf830f fix(lint): guard unnamed TEXT and refresh the textflag table 2026-09-14 18:22:00 +02:00
petrbalvin 953c258d6a fix(asm): make arm64 and loong64 relocations match the toolchain 2026-09-14 18:22:00 +02:00
petrbalvin c6f0286732 fix(asm): encode amd64 frame adjustments above 127 bytes with imm32 2026-09-14 18:22:00 +02:00
petrbalvin f5fc22d390 fix(parser): reject malformed TEXT frames and parse signed frame sizes 2026-09-14 18:22:00 +02:00
petrbalvin 22226d59a5 chore: prepare release v0.32.0
Release / build (amd64, linux) (push) Successful in 43s
Release / build (arm64, linux) (push) Successful in 42s
Release / build (loong64, linux) (push) Successful in 43s
Release / build (riscv64, linux) (push) Successful in 43s
Test / vet (push) Successful in 49s
Release / release (push) Successful in 18s
Test / test (push) Successful in 2m40s
Test / build (push) Successful in 42s
Assisted-by: GLM 5.3 Flash
2026-08-31 13:10:37 +02:00
petrbalvin a0ae0e4e37 docs: audit the development changelog against the release delta
Assisted-by: GLM 5.3 Flash
2026-08-31 12:53:36 +02:00
petrbalvin 94c4756d47 fix(verify): fix non-amd64 JIT trampolines and validate under qemu 2026-08-31 12:34:50 +02:00
petrbalvin a5a59d6503 fix(verify): gate JIT verification to amd64 until trampolines are hardened
Test / vet (push) Successful in 48s
Test / test (push) Successful in 2m34s
Test / build (push) Successful in 41s
2026-08-30 22:48:48 +02:00
petrbalvin 9cb1666b35 fix(debug): make ptrace sessions reliable on Go tracees 2026-08-30 22:35:22 +02:00
petrbalvin 96cc70731f docs: sync rule counts and feature lists with the new capabilities
Assisted-by: GLM 5.3 Flash
2026-08-30 21:42:12 +02:00
petrbalvin b4da13d0f6 feat(lsp): pull diagnostics, include links and folding ranges
Assisted-by: GLM 5.3 Flash
2026-08-30 21:42:12 +02:00
petrbalvin 41b387e54d feat(debug): instruction-level coverage and FP register display
Assisted-by: GLM 5.3 Flash
2026-08-30 21:42:12 +02:00
petrbalvin 4171e412b5 feat(verify): save and replay fuzz corpora
Assisted-by: GLM 5.3 Flash
2026-08-30 21:42:12 +02:00
petrbalvin 57c0ca8b09 feat(gasm): audit-instructions for arm64, riscv64 and loong64
Assisted-by: GLM 5.3 Flash
2026-08-30 21:42:12 +02:00
petrbalvin 75bd83fd52 feat(lint): flag writes to the platform-reserved register 2026-08-30 21:42:12 +02:00
petrbalvin 62f6fb4faf fix(debug): cross-compile for arm64, riscv64 and loong64
Test / vet (push) Successful in 47s
Test / test (push) Successful in 2m35s
Test / build (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
2026-08-30 11:27:54 +02:00
petrbalvin 8f84dac10b feat(verify): ABI checks on arm64, riscv64 and loong64
Assisted-by: GLM 5.3 Flash
2026-08-30 11:27:54 +02:00
petrbalvin 6d7f10f13e refactor(cmd): re-enter child modes via environment instead of hidden flags
Assisted-by: GLM 5.3 Flash
2026-08-30 11:00:40 +02:00
petrbalvin 56f8babbce docs: sync README, CHANGELOG and docs with the current state 2026-08-30 10:44:18 +02:00
petrbalvin 6c1c8d9d96 fix: point the coverage gate at the format package 2026-08-30 10:43:55 +02:00
petrbalvin 93794c02f7 chore: untrack .idea files 2026-08-30 10:43:45 +02:00
petrbalvin 56ad158772 fix: restore iota blocks, asm --format flag and prose after the syntax pass
Test / vet (push) Successful in 48s
Test / test (push) Successful in 2m35s
Test / build (push) Successful in 40s
2026-08-29 17:12:53 +02:00
petrbalvin 15e8b88d32 style: modernize the new tooling code to match the repo conventions 2026-08-29 16:15:14 +02:00
petrbalvin 9beff4ae85 style: modernize to splitseq, cut, min, maps.copy and range-over-int 2026-08-29 16:04:32 +02:00
petrbalvin eacf33d0f7 fix: staticcheck and deadcode findings repo-wide, modernize counting loops 2026-08-29 15:25:15 +02:00
petrbalvin 9a34733615 style(parser): cutprefix, default case, comma and tokens rename 2026-08-29 15:14:49 +02:00
petrbalvin 970df7c32a style(lint): drop duplicate rule code declaration 2026-08-29 15:14:49 +02:00
petrbalvin 49572efe16 style: gofmt the audit command 2026-08-29 14:25:02 +02:00
petrbalvin 568c553986 docs: document the audit, scaffold, scalar-args and headless debug additions 2026-08-29 14:24:52 +02:00
petrbalvin 7a30e902fc feat(debug): label coverage report for headless runs 2026-08-29 14:17:26 +02:00
petrbalvin cb398d9498 feat(debug): headless script mode with timeout watchdog 2026-08-29 14:16:04 +02:00
petrbalvin e44162a749 feat(verify): scalar arguments for -call invocations 2026-08-29 14:03:45 +02:00
petrbalvin 6fb9629ab6 fix(gasm): scaffold shared param names and two-sided seed sets 2026-08-29 13:57:04 +02:00
petrbalvin 8bda4066e3 feat(gasm): audit-instructions command and scaffold generator 2026-08-29 13:52:32 +02:00
petrbalvin 685b150ecf feat(lint): flag table-known instructions the encoder cannot emit 2026-08-29 13:42:47 +02:00
petrbalvin c92e6bed3a feat(lint): nonportable amd64 register name rule 2026-08-29 13:39:22 +02:00
petrbalvin d75e6bcae6 feat(lint): abi0 register-args rule and go-asm width model 2026-08-29 13:36:28 +02:00
petrbalvin 6699ebd34f feat(asm): add prefetch hint encoding
Test / vet (push) Successful in 51s
Test / test (push) Successful in 2m37s
Test / build (push) Successful in 41s
2026-08-29 10:42:15 +02:00
petrbalvin ba4d961b20 fix(asm): ADDQ imm8 frame adjust for 128-255 byte frames 2026-08-28 22:11:24 +02:00
petrbalvin 78b12dd427 fix(asm): match go tool asm encodings and strictness 2026-08-28 19:55:25 +02:00
petrbalvin 19a26e049b feat(asm): add vpcmp compare, full opmask set, legacy sse integers and bswap
Test / vet (push) Successful in 47s
Test / test (push) Successful in 2m35s
Test / build (push) Successful in 41s
Assisted-by: GLM 5.3
2026-08-27 22:41:03 +02:00
petrbalvin 163480e283 feat(amd64): encode scalar/double conversion ops (CVTSS2SD/CVTSD2SS/CVTPS2PD/CVTPD2PS) 2026-08-27 22:02:11 +02:00
petrbalvin 0d62818db7 feat(amd64): encode legacy SSE binaries, imm8 shuffles and MOVQ xmm moves 2026-08-27 22:02:11 +02:00
petrbalvin be2ceaafb9 feat(amd64): encode legacy SSE packed binaries and imm8 shuffles
Test / vet (push) Successful in 47s
Test / test (push) Successful in 2m34s
Test / build (push) Successful in 41s
2026-08-27 17:15:10 +02:00
petrbalvin 941f7fa990 chore: set development version to 0.32.0-dev
Test / vet (push) Successful in 48s
Test / test (push) Successful in 3m2s
Test / build (push) Successful in 47s
Assisted-by: GLM 5.3
2026-08-24 21:03:30 +02:00
petrbalvin eb3c79c3c8 fix(verify): isolate smoke and abi sweeps in a child process
Assisted-by: GLM 5.3
2026-08-24 21:01:32 +02:00
petrbalvin eade875b53 fix(asm): compress movq immediates to the go-tool-asm imm32 forms
Assisted-by: GLM 5.3
2026-08-24 20:23:39 +02:00
petrbalvin b08005753e fix(asm): encode BSF, BSR and POPCNT
Assisted-by: GLM 5.3
2026-08-24 20:19:34 +02:00
petrbalvin 5e06d6a6aa feat(lint): add register-width-mismatch rule
Test / vet (push) Successful in 48s
Test / test (push) Successful in 2m31s
Test / build (push) Successful in 44s
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:38 +02:00
petrbalvin e4c9d78968 feat(lsp): add workspace symbol search
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:35 +02:00
petrbalvin f5fcaf9fa6 perf(verify): parallelize smoke and ABI checks
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:31 +02:00
petrbalvin 94b98468bb feat(debug): support register-register conditional breakpoints
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:25 +02:00
petrbalvin 746cef8100 feat(asm): add .debug_frame CFI section for stack unwinding
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:21 +02:00
petrbalvin f153be8158 feat(lsp): add code actions, signature help, and document highlights
Assisted-by: MiMo V2.5 Pro
2026-08-21 01:20:14 +02:00
petrbalvin 1bf95a169e fix: resolve audit findings — stale text, dead code, build tags, docs
Test / vet (push) Successful in 47s
Test / test (push) Successful in 2m39s
Test / build (push) Successful in 42s
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:50:31 +02:00
petrbalvin f9eb4021d6 docs: update CHANGELOG and README for development changes
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:39:38 +02:00
petrbalvin c4930438fd refactor(lsp): use format package for document formatting
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin f372e2db75 test(lsp): add tests for references, rename, formatting, inlay hints
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin ac02c83a86 feat(lint): add stack-imbalance rule
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin ce5ec24fa8 feat(asm): integrate DWARF5 sections into all ELF emitters
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin 181d8e508c feat(verify): add Call trampolines for arm64, riscv64, loong64
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin 1160c96427 chore: remove stale debug_linux_amd64.go
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin de9e211ff1 feat(debug): multi-architecture debugger support for arm64, riscv64, loong64
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin 3acbdd6533 feat(asm): add DWARF5 debug info generation for ELF output
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin ba502c9b79 refactor(debug): make Regs and breakpoint arch-neutral for arm64
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin 874e054ecb feat(lsp): add references, rename, formatting, and inlay hints
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin f1960febdc feat(lint): add unused-label and invalid-textflag rules
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin ae550cc05a fix(asm): add cross-package GOOBJ resolution for riscv64, loong64, arm64
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin cc50035375 fix(cmd): update asm help text to list arm64 as supported
Assisted-by: MiMo V2.5 Pro
2026-08-21 00:35:21 +02:00
petrbalvin 8054fff9ac chore: prepare release v0.31.1
Test / vet (push) Successful in 47s
Release / build (amd64, linux) (push) Successful in 55s
Release / build (arm64, linux) (push) Successful in 51s
Release / build (loong64, linux) (push) Successful in 46s
Release / build (riscv64, linux) (push) Successful in 42s
Test / test (push) Successful in 2m29s
Release / release (push) Successful in 19s
Test / build (push) Successful in 42s
Assisted-by: MiMo V2.5 Pro
2026-08-20 22:35:11 +02:00
petrbalvin 6a79c35bf7 fix(version): bump version to 0.31.0 in justfile and main.go
Assisted-by: MiMo V2.5 Pro
2026-08-20 22:35:11 +02:00
petrbalvin 6f4f2096e9 chore: prepare release v0.31.0
Release / build (amd64, linux) (push) Successful in 42s
Release / build (arm64, linux) (push) Successful in 40s
Release / build (loong64, linux) (push) Successful in 42s
Release / build (riscv64, linux) (push) Successful in 45s
Release / release (push) Successful in 18s
2026-08-20 16:24:33 +02:00
petrbalvin 56630f8624 chore(toolchain): upgrade to Go 1.27
Test / vet (push) Successful in 1m5s
Test / test (push) Successful in 2m33s
Test / build (push) Successful in 40s
2026-08-20 16:03:26 +02:00
petrbalvin 459f4a2b6e fix(test): add arm64 encoding tests for Go 1.26 coverage compatibility
Assisted-by: MiMo V2.5 Pro
2026-08-20 15:44:23 +02:00
petrbalvin 5d66343488 fix(goobj): make R_DWTXTADDR_U4 relocation type Go-version-aware
The relocation type number shifted between Go 1.26 (103) and Go 1.27
(106)
because new LoongArch relocations were inserted. Detect the Go version
at
runtime and use the correct value.
2026-08-20 15:35:39 +02:00
petrbalvin 48334c4d5a docs: remove completed roadmap phases, fix licence description 2026-08-20 15:01:57 +02:00
petrbalvin 7629963cab chore: fix project conventions — .gitignore, CHANGELOG categories, docs naming
Assisted-by: MiMo V2.5 Pro
2026-08-20 14:47:39 +02:00
petrbalvin 97951cbeb6 feat(asm): extend arm64 encoder with atomics, bitfield, SIMD and more test kernels
Assisted-by: MiMo V2.5 Pro
2026-08-20 14:31:15 +02:00
petrbalvin 6e73f59e78 feat(asm): extend arm64 encoder with FP, conditional select, CRC32 and tests
Assisted-by: MiMo V2.5 Pro
2026-08-20 14:07:12 +02:00
petrbalvin 4221ec5741 feat(asm): add AArch64 arm64 encoder with ground-truth verification
Assisted-by: MiMo V2.5 Pro
2026-08-20 13:33:39 +02:00
petrbalvin 01dcc3b86e revert(toolchain): restore go1.26 in CI
Test / vet (push) Successful in 43s
Test / test (push) Failing after 1m55s
Test / build (push) Skipped
Release / build (amd64, linux) (push) Successful in 40s
Release / build (arm64, linux) (push) Successful in 37s
Release / build (loong64, linux) (push) Successful in 38s
Release / build (riscv64, linux) (push) Successful in 58s
Release / release (push) Successful in 22s
2026-08-13 18:34:20 +02:00
petrbalvin b08885bd31 chore(release): prepare v0.30.0
Test / vet (push) Failing after 17s
Test / test (push) Skipped
Test / build (push) Skipped
Assisted-by: DeepSeek V4 Pro
2026-08-13 18:27:12 +02:00
petrbalvin c05c53452f fix(asm): encode RISC-V CALL sym(SB) as JAL
Test / vet (push) Successful in 47s
Test / test (push) Failing after 2m9s
Test / build (push) Skipped
Assisted-by: DeepSeek V4 Pro
2026-08-13 18:12:22 +02:00
petrbalvin 681a449c01 fix(asm): match RISC-V branch and jump encodings 2026-08-13 17:57:10 +02:00
petrbalvin 31a2cee382 fix(asm): materialise RISC-V MOV immediates
Test / vet (push) Successful in 44s
Test / test (push) Failing after 1m56s
Test / build (push) Skipped
2026-08-13 17:41:16 +02:00
petrbalvin 3bc7c18bc3 fix(asm): materialise large RISC-V immediates
Assisted-by: DeepSeek V4 Pro
2026-08-13 16:04:08 +02:00
petrbalvin f0512a4e1c fix(asm): complete RISC-V compressed loads/stores and word arithmetic
Assisted-by: DeepSeek V4 Pro
2026-08-13 15:42:38 +02:00
petrbalvin 373c09f725 fix(asm): correct RISC-V operand order and complete RVC compression
Assisted-by: DeepSeek V4 Pro
2026-08-13 15:13:31 +02:00
petrbalvin b78b6c5004 fix(asm): correct RISC-V frame layout and RVC encodings
Assisted-by: GLM 5.2
2026-08-13 14:41:57 +02:00
petrbalvin 9b5c878f9e feat(asm): emit RISC-V GOOBJ with the shared emitter
Assisted-by: DeepSeek V4 Pro
2026-08-13 12:07:29 +02:00
petrbalvin 31ee8e7941 feat(asm): add LoongArch encoder with ELF and GOOBJ emission
Assisted-by: DeepSeek V4 Pro
2026-08-13 11:24:44 +02:00
petrbalvin 2d1176e045 feat(asm): resolve external GOOBJ symbols from archive data
Test / vet (push) Successful in 1m2s
Test / test (push) Failing after 2m5s
Test / build (push) Skipped
2026-08-08 16:18:56 +02:00
petrbalvin 2a27a3a52b docs: add BSD-3-Clause headers to generated files and update CI docs 2026-08-07 22:43:40 +02:00
petrbalvin cde7d0f96a docs: document watchpoint slot tracking and update debugger commands 2026-08-07 22:27:49 +02:00
petrbalvin d7ee1b78d4 feat: drop Mach-O and macOS support, Linux-only 2026-08-07 22:20:26 +02:00
petrbalvin 30c53565a7 build: align just test coverage gate with CI package list
Test / vet (push) Successful in 43s
Release / build (amd64, linux) (push) Successful in 41s
Release / build (arm64, linux) (push) Successful in 38s
Release / build (loong64, linux) (push) Successful in 39s
Release / build (riscv64, linux) (push) Successful in 38s
Test / test (push) Successful in 1m58s
Release / release (push) Successful in 18s
Test / build (push) Successful in 37s
Assisted-by: GLM 5.2
2026-08-07 21:34:09 +02:00
petrbalvin ebd8ab8a3c test: gate go-libraries integration tests behind -tags=integration
Test / vet (push) Successful in 44s
Test / test (push) Successful in 2m8s
Test / build (push) Successful in 40s
Assisted-by: GLM 5.2
2026-08-07 21:28:24 +02:00
petrbalvin 176d856f67 docs: remove go-libraries kernel references from README and CHANGELOG
Test / vet (push) Successful in 45s
Test / test (push) Failing after 2m1s
Test / build (push) Skipped
2026-08-07 21:10:16 +02:00
petrbalvin eace06bbd6 fix(verify): remove all go-libraries kernel dependencies from tests 2026-08-07 21:06:14 +02:00
petrbalvin f97bea61c5 test(verify): add FuzzResult.String and arrayLen tests to lift coverage over 80%
Test / vet (push) Successful in 49s
Test / test (push) Failing after 2m14s
Test / build (push) Skipped
Assisted-by: GLM 5.2
2026-08-05 23:16:52 +02:00
petrbalvin 19b37569c0 fix(verify): fix flaky JIT tests with global buffers and KeepAlive
Test / vet (push) Successful in 49s
Test / test (push) Failing after 2m9s
Test / build (push) Skipped
2026-08-05 23:09:54 +02:00
petrbalvin 23b3d3e152 docs: fold development changes into v0.29.0, document --call/--buf/--map 2026-08-05 21:15:05 +02:00
petrbalvin b0c62be8ce feat(verify): add --call and --buf flags for single-function invocation
Assisted-by: GLM 5.2
2026-08-05 20:46:39 +02:00
petrbalvin ece0d3f127 feat(cli): add --map flag to diff for comparing differently-named
functions
2026-08-05 20:20:47 +02:00
petrbalvin ad6e3360df feat: release v0.29.0 with RISC-V GOOBJ, debugger enhancements, and new
Test / vet (push) Successful in 47s
Test / test (push) Failing after 2m4s
Test / build (push) Skipped
CLI commands
2026-08-05 20:08:56 +02:00
petrbalvin 49566de7fb feat(asm): add did-you-mean suggestions for undefined labels
Assisted-by: DeepSeek V4 Pro
2026-08-05 18:52:00 +02:00
petrbalvin 6228d77566 feat(cli): add gasm profile command for basic-block structure
Assisted-by: DeepSeek V4 Pro
2026-08-05 16:41:00 +02:00
petrbalvin c0e280ee3c feat(cli): add gasm diff command for comparing assembly encodings
Assisted-by: DeepSeek V4 Pro
2026-08-05 14:23:00 +02:00
petrbalvin 2d45dbf7ff feat(lsp): add go-to-definition for labels 2026-08-05 11:08:00 +02:00
petrbalvin 8ddac0135e feat(asm): add GOOBJ emission for RISC-V 2026-08-05 09:27:00 +02:00
petrbalvin f58a4fe51d feat(debug): add named buffer allocation with pattern filling
Test / vet (push) Successful in 45s
Test / test (push) Successful in 1m58s
Test / build (push) Successful in 42s
2026-08-04 23:55:25 +02:00
petrbalvin 5cb7e3e231 feat(verify): combine ABI checks with fuzzing for deep-path testing 2026-08-04 22:26:04 +02:00
petrbalvin 36bbc0c13b feat(verify): store crashing input in FuzzResult for reproducibility
Test / vet (push) Successful in 44s
Test / test (push) Successful in 1m59s
Test / build (push) Successful in 36s
Assisted-by: DeepSeek V4 Pro
2026-08-04 21:58:53 +02:00
petrbalvin 32afa3449f feat(debug): add YMM vector register display via PTRACE_GETFPREGS
Assisted-by: DeepSeek V4 Pro
2026-08-04 21:55:28 +02:00
petrbalvin 2ab6b9eb84 ci: add Gitea CI workflows and release pipeline
Test / vet (push) Successful in 44s
Release / build (amd64, linux) (push) Successful in 39s
Release / build (arm64, linux) (push) Successful in 38s
Release / build (loong64, linux) (push) Successful in 39s
Release / build (riscv64, linux) (push) Successful in 39s
Test / test (push) Successful in 1m58s
Release / release (push) Successful in 18s
Test / build (push) Successful in 37s
Assisted-by: DeepSeek V4 Pro
2026-08-03 19:42:05 +02:00
petrbalvin f41a86b660 feat(asm): add RISC-V ELF relocatable object emission and SB relocation
support
2026-08-03 08:51:00 +02:00
petrbalvin 7721353d44 feat(asm): add RVC compression for branches, arithmetic, and FP
Assisted-by: DeepSeek V4 Pro
2026-08-03 01:08:00 +02:00
petrbalvin 243b087116 feat(riscv): add MOV pseudo-instruction and RVC compressed encoding
Assisted-by: DeepSeek V4 Pro
2026-08-02 18:22:00 +02:00
petrbalvin eee7a6d4a4 feat(riscv): add RISC-V RV64A, FP and CSR instruction support
Assisted-by: Kimi K3
2026-08-02 11:45:00 +02:00
petrbalvin 801fb963c9 fix(verify): enlarge fuzz buffers so slice-based kernels can be fuzzed
directly
2026-08-02 06:12:00 +02:00
petrbalvin a2acc9b5a3 feat(riscv): add prologue, epilogue and frame pseudo-register support
Assisted-by: Kimi K3
2026-08-02 00:18:00 +02:00
petrbalvin f860bf8ce6 feat(asm): add RISC-V encoder with RV64I/RV64M instruction formats
Assisted-by: Kimi K3
2026-08-01 19:51:00 +02:00
petrbalvin e7df5e5225 test(debug): add unit tests for condition evaluation, line lookup and
RFLAGS decoding

Assisted-by: MiniMax M3
2026-08-01 14:33:00 +02:00
petrbalvin d114b3412c feat(debug): complete the interactive debugger with disassembly, breakpoints, watchpoints and execution control
Assisted-by: DeepSeek V4 Pro
2026-08-01 09:47:00 +02:00
petrbalvin c77d68018c docs: add the full documentation surface — AGENTS, CONTRIBUTING, cli and development references
Assisted-by: DeepSeek V4 Flash
2026-08-01 05:22:00 +02:00
petrbalvin 89d633f4bb feat(debug): add interactive ptrace debugger MVP — single-step, regs, breakpoints, labels
Assisted-by: Qwen 3.8 Max Preview
2026-08-01 02:34:00 +02:00
petrbalvin f20e0bf1e7 fix(verify): subprocess isolation for --fuzz, partial functions report CRASH gracefully
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 1a45b66139 feat(verify): add universal --fuzz differential testing driven by // func signatures
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 382efe538a feat(verify): add universal --ground-truth verification against go tool asm
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f8d28a42ba feat(verify): complete the analyze family and add stereo16 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 51a2854d7f feat(verify): add analyzeO2/Res and decodeMono24 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin c234c3dd5b feat(verify): add analyzeO1Range and fastStereoSums differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 9262990ce5 feat(verify): extend differential tests to go-flac and AVX-512, add --abi/--profile CLI flags
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin d9f6167a4d feat(verify): add basic-block enumeration and path-diversity profiling
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f5088c52fc feat(verify): add runtime ABI checks with sentinel registers and red-zone canary
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f52e23f1bc feat(verify): add differential fuzz testing against a portable Go reference
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin c9775c2b95 feat(verify): add the JIT execution substrate and gasm verify subcommand
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 5af12e15ac feat(asm): add the GPR-interchanging conversions, completing the amd64 EVEX set
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin db8e3fc160 docs: record the deferred GOOBJ external-symbols decision 2026-08-02 23:11:30 +02:00
petrbalvin 1312122a99 feat(asm): complete the EVEX conversions, narrowing and mask-vector moves
Assisted-by: Qwen 3.8 Max Preview
2026-07-20 16:05:08 +02:00
petrbalvin 11f962fbcc feat(asm): add the EVEX FP helper tail and gather/scatter with VSIB
Assisted-by: Qwen 3.8 Max Preview
2026-07-19 15:58:48 +02:00
petrbalvin ee68859beb feat(asm): add the wider EVEX set and the rounding, SAE and broadcast suffixes
Assisted-by: Qwen 3.8 Max Preview
2026-07-18 15:47:59 +02:00
petrbalvin 0920edb092 feat(asm): emit GOOBJ objects that link directly with the Go toolchain
Assisted-by: Qwen 3.8 Max Preview
2026-07-17 18:57:04 +02:00
503 changed files with 128423 additions and 1872 deletions

No files matched your search

+300
View File
@@ -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);
'
+86
View File
@@ -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);
'
+88
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+135
View File
@@ -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**.
+339
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+201
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+111 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+297
View File
@@ -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")
}
}
+320
View File
@@ -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
}
+509
View File
@@ -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)
}
}
}
+122
View File
@@ -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
}
+566
View File
@@ -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
+467
View File
@@ -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)
}
+311
View File
@@ -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)
}
}
}
+884
View File
@@ -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)
}
}
}
+835
View File
@@ -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)
}
}
}
+202
View File
@@ -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
}
+237
View File
@@ -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
}
+110
View File
@@ -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)
}
}
}
+58
View File
@@ -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)
}
+56
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+64
View File
@@ -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)
}
}
}
+571
View File
@@ -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
+578
View File
@@ -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
}
+236
View File
@@ -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)
}
}
}
+269
View File
@@ -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
}
+279
View File
@@ -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)
}
}
}
}
+182
View File
@@ -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")
}
}
+600
View File
@@ -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,
}
+164
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+333 -5
View File
@@ -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)
}
}
}
+288
View File
@@ -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
View File
@@ -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})
+406
View File
@@ -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
}
+179
View File
@@ -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",
}
+372
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+314
View File
@@ -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)
}
}
}
+352
View File
@@ -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
}
+362
View File
@@ -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
View File
@@ -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
}
+128
View File
@@ -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)
}
}
}
+146
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+1608 -149
View File
File diff suppressed because it is too large. Load diff
+665 -84
View File
@@ -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
}
+159
View File
@@ -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()
}
+269
View File
@@ -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)
}
}
+534
View File
@@ -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)
}
}
+130
View File
@@ -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
View File
@@ -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
}
+185
View File
@@ -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
}
+214
View File
@@ -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)
}
}
+370
View File
@@ -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
}
+73
View File
@@ -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"])
}
+842
View File
@@ -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)
}
}
+116
View File
@@ -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
}
+97
View File
@@ -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
}
+99
View File
@@ -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
}
+329
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+159
View File
@@ -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
}
+228
View File
@@ -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)
}
}
}
+326
View File
@@ -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
View File
@@ -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
View File
@@ -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
+453
View File
@@ -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
}
+340
View File
@@ -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
}
+535
View File
@@ -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)
}
}
}
+42
View File
@@ -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
View File
@@ -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
}
-127
View File
@@ -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