Compare commits

..
6 Commits
Author SHA1 Message Date
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
14 changed files with 2483 additions and 140 deletions
+35 -7
View File
@@ -23,7 +23,7 @@ import (
// operands require relocations and are not yet supported; the SIMD (VEX/AVX2)
// integer and shuffle/extract/permute/move set is in.
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
code, _, labels, err := assemble(t, nil)
code, _, labels, _, err := assemble(t, nil)
return code, labels, err
}
@@ -48,9 +48,19 @@ type sbPatch struct {
addend int64
}
// spadjStep is one stack-adjustment boundary within a function: Value is the
// SP delta from the entry state (just below the return address) in effect
// from PC (function-relative) until the next step. The steps feed the
// pcsp table of the object-file emitters.
type spadjStep struct {
pc int
value int
}
// assemble encodes a TEXT body, returning the machine code, the static-symbol
// patch sites (for the file-level layout to resolve) and the label table.
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, error) {
// patch sites (for the file-level layout to resolve), the label table and the
// stack-adjustment boundaries.
func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, []spadjStep, error) {
fi := computeFrame(t)
chain := jumpChain(t)
resolve := func(name string) string {
@@ -74,7 +84,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
case *ast.Instr:
sz, err := instrSize(s, fi, long[i], link)
if err != nil {
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
}
sizes[i] = sz
pcs[i] = pos
@@ -114,24 +124,42 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, e
// Pass 2: emit.
out := append([]byte(nil), fi.prologue...)
var patches []sbPatch
var steps []spadjStep
if fi.useFP {
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
// changes nothing; SUBQ $size, SP completes the frame.
steps = append(steps,
spadjStep{1, 8},
spadjStep{len(fi.prologue), 8 + fi.size},
)
}
pos := len(fi.prologue)
for i, stmt := range t.Body {
s, ok := stmt.(*ast.Instr)
if !ok {
continue
}
if strings.ToUpper(s.Mnemonic.Text) == "RET" && fi.useFP {
// The RET's epilogue prefix unwinds: ADDQ $size, SP restores
// the saved-BP-only stack, POPQ BP the entry state.
epi := len(fi.epilogue)
steps = append(steps,
spadjStep{pos + epi - 1, 8},
spadjStep{pos + epi, 0},
)
}
code, ps, err := encodeInstr(s, pos, offsets, fi, long[i], resolve, link)
if err != nil {
return nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
return nil, nil, nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
}
if len(code) != sizes[i] {
return nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
return nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
}
patches = append(patches, ps...)
out = append(out, code...)
pos += len(code)
}
return out, patches, offsets, nil
return out, patches, offsets, steps, nil
}
// jumpChain precomputes jump-to-jump folding: a label whose first instruction
+28 -15
View File
@@ -51,16 +51,17 @@ func (e *enc) encode(mnem string, ops []Operand) error {
// 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) || base == "KMOVW" || base == "KMOVQ" {
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).
@@ -128,20 +129,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" {
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 -------------------------------------------------
+837 -79
View File
File diff suppressed because it is too large Load Diff
+386 -1
View File
@@ -230,7 +230,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 +244,387 @@ 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"},
// 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 {
+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 (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"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, and the pc-value tables (pcsp, pcfile,
// pcline, pcinline). DWARF and the implicit funcdata symbols are omitted;
// the linker fills their defaults.
// GOOBJ block indices (cmd/internal/goobj).
const (
blkAutolib = iota
blkPkgIdx
blkFile
blkSymdef
blkHashed64def
blkHasheddef
blkNonpkgdef
blkNonpkgref
blkRefFlags
blkHash64
blkHash
blkRelocIdx
blkAuxIdx
blkDataIdx
blkReloc
blkAux
blkData
blkRefName
blkEnd
)
// Symbol kinds used by assembly objects (cmd/internal/objabi).
const (
kindSTEXT = 1
kindSRODATA = 3
kindSDATA = 7
)
// Symbol flags (cmd/internal/goobj).
const (
symFlagDupok = 0x01
symFlagNoSplit = 0x10
symFlag2Link = 0x10 // asm objects flag every named symbol as linkname
symABIStatic = 0xffff
)
// Aux entry types (cmd/internal/goobj).
const (
auxFuncInfo = 1
auxPcsp = 7
auxPcfile = 8
auxPcline = 9
auxPcinline = 10
)
// FuncInfo flags (internal/abi).
const (
funcFlagSPWrite = 2
funcFlagAsm = 4
)
// Relocation types (cmd/internal/objabi).
const relocPCRel = 14
// Special package indices for symbol references.
const (
pkgIdxNone = 0x7fffffff
pkgIdxSelf = 0x7ffffffb
)
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)
}
// 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) {
if pkgPath == "" {
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
}
pre, err := toolchainObjectPreamble()
if err != nil {
return nil, err
}
// The symbol tables. Package definitions: the GLOBL symbols, then one
// anonymous FuncInfo symbol per function. Non-package definitions: the
// pc-value tables and the functions themselves, as cmd/asm lays them
// out. defIdx maps a GLOBL's bare name to its definition index for the
// relocations; fnNpIdx maps a function to its non-package index.
var defs []goSym
var defData [][]byte
defIdx := map[string]int{}
for _, d := range img.DataSyms {
name := d.Name
if !d.Static {
name = pkgPath + "." + name
}
typ := uint8(kindSDATA)
if d.Rodata {
typ = kindSRODATA
}
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, flag2: symFlag2Link, size: uint32(d.Size)})
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
}
fnFiIdx := make([]int, len(img.Funcs))
for i := range img.Funcs {
data := marshalFuncInfo(img.Funcs[i])
fnFiIdx[i] = len(defs)
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
defData = append(defData, data)
}
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), &pcIdx[i].sp},
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
{pcValueFlat(-1, fn.Size), &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 {
// The linker writes the resolved displacement into the field;
// leave it zero, as cmd/asm's object does.
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, flag2: symFlag2Link, size: uint32(fn.Size)},
data: code,
})
}
// Relocations, per defined symbol in definition order (package defs,
// then non-package defs). Only file-local GLOBL references resolve;
// external symbols need the import machinery of a later increment.
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 {
if r.External {
return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name)
}
di, ok := defIdx[r.Name]
if !ok {
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
}
var rec [23]byte
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
rec[4] = 4 // field width
binary.LittleEndian.PutUint16(rec[5:], relocPCRel)
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
symRelocs[si] = append(symRelocs[si], rec[:]...)
}
}
// Aux entries per function: FuncInfo, 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(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+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)
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)
}
pkgIdxBlk := stringRef(nil, "") // index 0: the dummy invalid package
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 := 0; si < nsyms; si++ {
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 := 0; i < blkEnd; i++ {
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.
func pcValueFlat(v int32, size 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))
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) []byte {
if len(fn.Spadj) == 0 {
return pcValueFlat(0, fn.Size)
}
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))
out = binary.AppendVarint(out, int64(p.Value-old))
cur, old = p.PC, p.Value
}
out = binary.AppendUvarint(out, uint64(fn.Size-cur))
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
}
+477
View File
@@ -0,0 +1,477 @@
// 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-devkit/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 one anonymous FuncInfo per
// function.
defs := v.syms(blkSymdef)
if len(defs) != 3 {
t.Fatalf("symdefs = %d, want 3", len(defs))
}
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link {
t.Errorf("mask symbol = %+v", defs[0])
}
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
t.Errorf("funcinfo symbol = %+v", defs[1])
}
// 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)
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)
}
// pcsp: a flat zero over the whole function (zero-frame NOSPLIT).
if got := data[72:75]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
t.Errorf("pcsp = %x, want 021300", got)
}
// pcinline: a flat -1.
if got := data[81:84]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
t.Errorf("pcinline = %x, want 001300", got)
}
// The one relocation: R_PCREL, four bytes wide, against the GLOBL,
// with the field in the function code left zero. The loadmask code's
// offset comes from the data index (symbol 3 defs + 9 non-package).
relocs := v.blk(blkReloc)
if len(relocs) != 23 {
t.Fatalf("relocs = %d bytes, want one 23-byte entry", len(relocs))
}
off := int32(le.Uint32(relocs[0:]))
if off != 4 || relocs[4] != 4 || le.Uint16(relocs[5:]) != relocPCRel ||
le.Uint64(relocs[7:]) != 0 || le.Uint32(relocs[15:]) != pkgIdxSelf || le.Uint32(relocs[19:]) != 0 {
t.Errorf("reloc = %x", relocs)
}
didx := v.blk(blkDataIdx)
lm := le.Uint32(didx[4*(3+9):])
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 (package symbol), then the four pc tables
// (non-package symbols).
auxs := v.blk(blkAux)
if len(auxs) != 2*5*9 {
t.Fatalf("aux = %d bytes, want 10 entries", len(auxs))
}
wantAux := []struct {
typ uint8
pkg uint32
idx uint32
}{
{auxFuncInfo, pkgIdxSelf, 1},
{auxPcsp, pkgIdxNone, uint32(len(defs) + 0)},
{auxPcfile, pkgIdxNone, uint32(len(defs) + 1)},
{auxPcline, pkgIdxNone, uint32(len(defs) + 2)},
{auxPcinline, pkgIdxNone, uint32(len(defs) + 3)},
{auxFuncInfo, pkgIdxSelf, 2},
{auxPcsp, pkgIdxNone, uint32(len(defs) + 5)},
{auxPcfile, pkgIdxNone, uint32(len(defs) + 6)},
{auxPcline, pkgIdxNone, uint32(len(defs) + 7)},
{auxPcinline, pkgIdxNone, uint32(len(defs) + 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))
// 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.26\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.Split(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)
}
if err := os.WriteFile(asmObj, obj, 0o644); err != nil {
t.Fatal(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).
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)
}
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.Fields(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 ""
}
+78 -12
View File
@@ -28,13 +28,26 @@ type Image struct {
// 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
}
// 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
}
// Reloc is one static-symbol reference within a function body: the disp32
@@ -57,6 +70,8 @@ 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
Dupok bool // the DUPOK flag: duplicate-OK
}
// Bytes returns the whole image: code, then data.
@@ -95,18 +110,33 @@ func AssembleFile(f *ast.File) (*Image, error) {
if !ok {
continue
}
code, patches, labels, err := assemble(t, link)
code, patches, labels, steps, err := assemble(t, link)
if err != nil {
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
}
img.Funcs = append(img.Funcs, FuncLayout{
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,
})
}
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})
}
img.Funcs = append(img.Funcs, fl)
img.Code = append(img.Code, code...)
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
}
@@ -124,6 +154,8 @@ func AssembleFile(f *ast.File) (*Image, error) {
Offset: len(img.Data),
Size: len(d.buf),
Static: d.static,
Rodata: d.rodata,
Dupok: d.dupok,
})
img.Data = append(img.Data, d.buf...)
}
@@ -163,6 +195,8 @@ type dataSym struct {
pkg string
buf []byte
static bool
rodata bool
dupok bool
}
// collectData gathers the file's static symbols (GLOBL) and their initial
@@ -185,12 +219,28 @@ func collectData(f *ast.File) ([]dataSym, error) {
size = int(dd.Size.Imm.Val)
}
index[name] = len(syms)
syms = append(syms, dataSym{
ds := dataSym{
name: name,
pkg: dd.Name.Pkg,
buf: make([]byte, size),
static: dd.Name.Static,
})
}
for _, f := range dd.Flags {
switch f {
case "RODATA":
ds.rodata = true
case "DUPOK":
ds.dupok = true
case "1":
ds.dupok = true
case "8":
ds.rodata = true
case "9":
ds.dupok = true
ds.rodata = true
}
}
syms = append(syms, ds)
case *ast.Data:
if dd.Name == nil || dd.Name.Pseudo != "SB" {
@@ -230,3 +280,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
}
+70 -6
View File
@@ -91,12 +91,19 @@ var vexTable = map[string]vexSpec{
"VPCMPGTQ": {2, 0x37, 0, 1, -1, vexNDS3},
// VEX.128/256.66.0F.WIG — packed double-precision arithmetic / logic.
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
"VSUBPD": {1, 0x5C, 0, 1, -1, vexNDS3},
"VDIVPD": {1, 0x5E, 0, 1, -1, vexNDS3},
"VMINPD": {1, 0x5D, 0, 1, -1, vexNDS3},
"VMAXPD": {1, 0x5F, 0, 1, -1, vexNDS3},
"VADDPD": {1, 0x58, 0, 1, -1, vexNDS3},
"VMULPD": {1, 0x59, 0, 1, -1, vexNDS3},
"VSUBPD": {1, 0x5C, 0, 1, -1, vexNDS3},
"VDIVPD": {1, 0x5E, 0, 1, -1, vexNDS3},
"VMINPD": {1, 0x5D, 0, 1, -1, vexNDS3},
"VMAXPD": {1, 0x5F, 0, 1, -1, vexNDS3},
// VEX.128/256.0F.WIG — packed single-precision arithmetic.
"VADDPS": {1, 0x58, 0, 0, -1, vexNDS3},
"VMULPS": {1, 0x59, 0, 0, -1, vexNDS3},
"VSUBPS": {1, 0x5C, 0, 0, -1, vexNDS3},
"VDIVPS": {1, 0x5E, 0, 0, -1, vexNDS3},
"VMINPS": {1, 0x5D, 0, 0, -1, vexNDS3},
"VMAXPS": {1, 0x5F, 0, 0, -1, vexNDS3},
"VXORPD": {1, 0x57, 0, 1, -1, vexNDS3},
"VUNPCKHPD": {1, 0x15, 0, 1, -1, vexNDS3},
"VUNPCKLPD": {1, 0x14, 0, 1, -1, vexNDS3},
@@ -123,7 +130,15 @@ var vexTable = map[string]vexSpec{
// no vvvv).
"VPMOVSXWD": {2, 0x23, 0, 1, -1, vexRM},
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM},
"VPMOVSXBD": {2, 0x21, 0, 1, -1, vexRM},
"VPMOVSXBQ": {2, 0x22, 0, 1, -1, vexRM},
"VPMOVSXWQ": {2, 0x24, 0, 1, -1, vexRM},
"VPMOVZXDQ": {2, 0x35, 0, 1, -1, vexRM},
"VPMOVZXBW": {2, 0x30, 0, 1, -1, vexRM},
"VPMOVZXBD": {2, 0x31, 0, 1, -1, vexRM},
"VPMOVZXBQ": {2, 0x32, 0, 1, -1, vexRM},
"VPMOVZXWD": {2, 0x33, 0, 1, -1, vexRM},
"VPMOVZXWQ": {2, 0x34, 0, 1, -1, vexRM},
"VPBROADCASTD": {2, 0x58, 0, 1, -1, vexRM},
"VPBROADCASTQ": {2, 0x59, 0, 1, -1, vexRM},
// VEX.128/256.F3.0F.WIG — signed dword to packed double conversion
@@ -169,6 +184,9 @@ var vexTable = map[string]vexSpec{
// VEX.256.66.0F3A.W0 — lane extract (reg=YMM src, rm=XMM/memory dst, imm8).
"VEXTRACTI128": {3, 0x39, 0, 1, -1, vexExtract},
"VEXTRACTF128": {3, 0x19, 0, 1, -1, vexExtract},
// VEX.128/256.66.0F3A.W0 — half-precision convert back ($imm, src, dst:
// reg=src, rm=XMM/memory dst, imm8 — the extract layout).
"VCVTPS2PH": {3, 0x1D, 0, 1, -1, vexExtract},
// VEX.128.0F.W0 — no operands.
"VZEROUPPER": {1, 0x77, 0, 0, -1, vexZero},
@@ -176,6 +194,45 @@ var vexTable = map[string]vexSpec{
// VEX.128.0F.W0 — mask-register test (KTESTW k1, k2: reg = dst, rm = src).
"KTESTW": {1, 0x99, 0, 0, -1, vexRM},
// VEX.66.0F38.W0 — broadcast a single/double to all lanes (reg=dst,
// rm=scalar memory; SD is 256-bit only).
"VBROADCASTSS": {2, 0x18, 0, 1, -1, vexRM},
"VBROADCASTSD": {2, 0x19, 0, 1, -1, vexRM},
// VEX.66.0F38.W0 — half-precision convert (reg=dst, rm=half-width
// source).
"VCVTPH2PS": {2, 0x13, 0, 1, -1, vexRM},
// VEX.F3.0F.WIG — replicate even/odd singles (reg=dst, rm=src).
"VMOVSLDUP": {1, 0x12, 0, 2, -1, vexRM},
"VMOVSHDUP": {1, 0x16, 0, 2, -1, vexRM},
// VEX.66.0F.WIG — packed double to packed single conversion, the X/Y
// spellings: the destination is always XMM and the spelling fixes the
// source length (X = 128, Y = 256).
"VCVTPD2PSX": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
"VCVTPD2PSY": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
// VEX scalar conversions between vector and general-purpose registers.
// Vector to GPR (two operands: vec/mem source, GPR destination, vvvv
// unused; the length follows the source).
"VCVTSD2SI": {1, 0x2D, 0, 3, -1, vexRM},
"VCVTSD2SIQ": {1, 0x2D, 1, 3, -1, vexRM},
"VCVTSS2SI": {1, 0x2D, 0, 2, -1, vexRM},
"VCVTSS2SIQ": {1, 0x2D, 1, 2, -1, vexRM},
"VCVTTSD2SI": {1, 0x2C, 0, 3, -1, vexRM},
"VCVTTSD2SIQ": {1, 0x2C, 1, 3, -1, vexRM},
"VCVTTSS2SI": {1, 0x2C, 0, 2, -1, vexRM},
"VCVTTSS2SIQ": {1, 0x2C, 1, 2, -1, vexRM},
// GPR to vector (three operands: GPR/mem source in r/m, the preserved
// vector source in vvvv, vector destination in reg).
"VCVTSI2SDL": {1, 0x2A, 0, 3, -1, vexNDS3},
"VCVTSI2SDQ": {1, 0x2A, 1, 3, -1, vexNDS3},
"VCVTSI2SSL": {1, 0x2A, 0, 2, -1, vexNDS3},
"VCVTSI2SSQ": {1, 0x2A, 1, 2, -1, vexNDS3},
// VEX.128/256.66.0F.WIG — word shifts (opdigit selects the shift).
"VPSRLW": {1, 0x71, 0, 1, 2, vexShiftImm},
"VPSRAW": {1, 0x71, 0, 1, 4, vexShiftImm},
"VPSLLW": {1, 0x71, 0, 1, 6, vexShiftImm},
// VEX.F2.0F — packed double to packed dword conversions, truncating and
// non-truncating. The destination is always XMM; the X/Y spellings fix
// the source length (XMM/YMM), and VEX.L follows it — see vexSrcLen.
@@ -194,6 +251,8 @@ var vexSrcLen = map[string]int{
"VCVTPD2DQY": 1,
"VCVTTPD2DQX": 0,
"VCVTTPD2DQY": 1,
"VCVTPD2PSX": 0,
"VCVTPD2PSY": 1,
}
// vexVarShift maps the shift mnemonics to their variable-count opcode — the
@@ -238,6 +297,11 @@ var vexMoveTable = map[string]vexMoveSpec{
// VEX.128.F2.0F.WIG — scalar double move, memory operands only (the
// register form takes three operands and is not supported yet).
"VMOVSD": {1, 3, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
// VEX.128.F3.0F.WIG — scalar single move, memory operands only.
"VMOVSS": {1, 2, 0x10, 0x11, 0, 0, 0, 0, false, false, true},
// VEX.128/256 — aligned packed moves.
"VMOVAPS": {1, 0, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
"VMOVAPD": {1, 1, 0x28, 0x29, 0, 0, 0, 0, true, false, false},
}
// isVex reports whether the mnemonic is a VEX-encoded instruction we handle.
+6 -3
View File
@@ -39,11 +39,14 @@ func TestVexNDS3(t *testing.T) {
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(% x): %v", mnem, code, err)
t.Errorf("%s: Decode(% x): %v", mnem, err, code)
continue
}
if inst.Op.String() != mnem {
t.Errorf("%s: decoded as %s (% x)", mnem, inst.Op.String(), code)
// The decoder folds the Plan 9 L/Q GPR-width spellings (VCVTSI2SDL/
// SDQ, SSL/SSQ) onto the base name; the W bit carries the width.
got := inst.Op.String()
if got != mnem && !(len(mnem) > len(got) && mnem[:len(got)] == got) {
t.Errorf("%s: decoded as %s (% x)", mnem, got, code)
}
}
}
+13 -7
View File
@@ -28,7 +28,7 @@ import (
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.11.0"
var version = "0.16.0"
func main() {
if len(os.Args) < 2 {
@@ -94,6 +94,7 @@ Examples:
gasm lint go-flac/*.s run static checks over the kernels
gasm asm -o k.bin kern_amd64.s
gasm asm --format elf -o k.o kern_amd64.s
gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s
`, version)
}
@@ -340,7 +341,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
}
func cmdAsm(args []string) int {
fs := newCommand("asm", "gasm asm [--format raw|elf|macho] [-o out] <file>", `
fs := newCommand("asm", "gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>", `
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
@@ -350,14 +351,16 @@ With -o the output is written to a file instead. The --format flag selects
what is written: raw (the default) concatenates the functions and the data
section into one self-consistent image; elf and macho emit a relocatable
object (.text/.data sections, a symbol table and one PC32 relocation per
static-symbol reference) that links with the system toolchain — references
to symbols no GLOBL in the file defines become undefined external symbols.
static-symbol reference) that links with the system toolchain; goobj emits
the Go toolchain's own object format, which cmd/link consumes directly (it
requires -p, the package path, and the installed Go toolchain).
`)
out := fs.String("o", "", "write the output to this file")
format := fs.String("format", "raw", "output format: raw (concatenated image), elf or macho (relocatable object)")
format := fs.String("format", "raw", "output format: raw (concatenated image), elf, macho or goobj (Go object)")
pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho] [-o out] <file>")
fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|macho|goobj] [-p pkg] [-o out] <file>")
return 2
}
path := fs.Arg(0)
@@ -444,8 +447,11 @@ to symbols no GLOBL in the file defines become undefined external symbols.
case "macho":
obj, err = img.MachOObject()
kind = "Mach-O object"
case "goobj":
obj, err = img.GOObject(*pkg, path)
kind = "Go object"
default:
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf or macho)\n", *format)
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format)
return 2
}
if err != nil {
+38 -9
View File
@@ -228,11 +228,31 @@ explicit merging/zeroing masks — written the way Go writes them, as a K
operand among the operands plus a `.Z` mnemonic suffix), and the compressed
disp8×N displacement, whose multiplier follows the memory operand's size —
covering every instruction the go-flac and go-lz4 AVX2/AVX-512 kernels use,
plus the common AVX-512 F/BW integer set and the floating-point and
conversion set (the packed double arithmetic, the scalar SD/SS forms —
whose EVEX encodings serve masked and zeroing use — `VMOVDDUP`, and the
width-changing conversions, including the `VCVTPD2DQ`/`VCVTTPD2DQ` family
whose length follows the wider source operand). Every encoding is validated two ways: by
plus the common AVX-512 F/BW integer set, the floating-point and conversion
set (the packed double and single arithmetic, the scalar SD/SS forms —
whose EVEX encodings serve masked and zeroing use — `VMOVDDUP`, the
replicating moves, and the width-changing conversions, including the
`VCVTPD2DQ`/`VCVTTPD2DQ` family whose length follows the wider source
operand), and the wider AVX-512 set: ternary logic, lane shuffles, inserts
and extracts, compares with an opmask destination, the permutes, the
expand/compress family, the broadcasts, the opmask-register instructions
(KAND/KOR/KXNOR/KADD/KUNPCK/KNOT/KSHIFTL/KORTEST and KMOVQ), the aligned
moves and the remaining extending/narrowing moves, the floating-point
helper and conversion tail (VRCP14*, VRSQRT14*, VGETEXP*, VGETMANT*,
VSCALEF*, VRNDSCALE*, VREDUCE*, VFIXUPIMM*, VRANGE*, VFPCLASS* with an
opmask destination, and the VCVT* conversions — signed, unsigned and
truncating, including the length-suffixed X/Y spellings and the
mask/vector conversions VPMOVM2*/VPMOV*2M, and the scalar conversions
between vector and general-purpose registers (VCVT{,T}S{D,S}2SI{,Q} and
the unsigned forms, VCVTSI2*/VCVTUSI2*), and gather/scatter with VSIB addressing — both the
VEX spelling with a vector mask register and the EVEX spelling with an
explicit K mask, where the EVEX length follows the VSIB index register,
not the data register. The EVEX mnemonic
suffixes — rounding modes (.RN_SAE/.RD_SAE/.RU_SAE/.RZ_SAE),
suppress-all-exceptions (.SAE) and memory broadcast (.BCST) — set the EVEX
b bit and the L'L rounding-control field (broadcast keeps the vector length
and scales disp8 by the element size), and combine with the .Z zeroing
suffix. Every encoding is validated two ways: by
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
the machine code the real Go assembler emits — a comparison that holds for
whole functions: all 27 functions of both kernels assemble to exactly the Go
@@ -250,10 +270,19 @@ and Mach-O writers (`gasm asm --format elf|macho`) lay the code and data out
as `.text`/`.data` (or `__text`/`__data`) sections, export a symbol per
`TEXT` and `GLOBL` (the `<>` ones local, the rest global) and emit one
PC-relative relocation per static-symbol reference — undefined external
symbols included, so the output links with the system toolchain. GOOBJ
emission, the format the Go linker consumes directly, is the remaining
piece of Phase 2 (together with the rest of the EVEX set and the other
architectures).
symbols included, so the output links with the system toolchain. The GOOBJ
emitter (`gasm asm --format goobj`) writes the format the Go linker consumes
directly: the functions as non-package symbols (the way `cmd/asm` records
assembly symbols), the `GLOBL` data, one `FuncInfo` per function and the
pc-value tables — `pcsp` built from the prologue and epilogue stack
boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a
gasm-assembled object drops into a `go build` in place of the toolchain's.
The object preamble (the version-and-experiment header the linker compares
verbatim) is captured from the installed `go tool asm`, so the output is
always consistent with the toolchain that links it. External cross-package
references and the implicit funcdata/DWARF symbols remain future work (the
linker fills the latter's defaults); the rest of Phase 2 is those, the
remaining EVEX forms and the other architectures.
## Extension points
+56
View File
@@ -0,0 +1,56 @@
# Deferred decisions
Design decisions deliberately postponed, with enough context to pick them up
again without re-deriving the analysis. Each entry records what is deferred,
why, the options on the table, and the trigger that should reopen it.
---
## GOOBJ external (cross-package) symbol references
**Status:** deferred (v0.15.0, 2026-08-02). The GOOBJ emitter resolves only
symbols defined in the file being assembled; a reference to any other symbol
is rejected.
**Why it is deferred.** GOOBJ symbol references are *positional*: a
reference is a `{PkgIdx, SymIdx}` pair, where `SymIdx` is the index of the
symbol in the *referenced package's* symbol-definition table. That ordering
is not derivable from the reference site — it lives in the referenced
package's gc export data (the iexport binary format, which evolves with the
toolchain). `cmd/asm` reads it with `cmd/internal` readers gasm cannot
import, so emitting external references means either parsing export data
ourselves or taking a dependency that does.
**What works today.** Single-package objects: every symbol the file defines
(as `TEXT` or `GLOBL`, static or exported) and every reference to them.
This covers the production use case — the go-flac / go-lz4 kernels carry no
`FUNCDATA`/`PCDATA`, hence no references into `runtime`, and the Go side
references the assembly symbols, never the reverse. Such a package builds
with its assembly object replaced by a gasm-emitted one.
**The options, when we return.**
1. **`golang.org/x/tools/go/gcexportdata` as a production dependency.**
The straightforward path: read each imported package's export file
(paths from `-importcfg` or `go list -export`), assign symbol indices in
its symbol order, write `PkgIndex`/`Autolib` entries (fingerprints from
the export files' build IDs) and positional references. Robust across
toolchain versions — `x/tools` tracks the format. **Cost:** the first
production dependency beyond the standard library, an explicit deviation
from the "production code depends only on the standard library"
principle in the README. Requires the user's explicit agreement.
2. **A minimal iexport parser of our own.** Preserves self-containment.
Substantial effort and inherently fragile: the format is an internal
contract that changes with Go releases, so the parser needs a
version-gated fallback and regression tests against several toolchains.
3. **Shell out to the toolchain for symbol metadata.** Consistent with the
existing GOOBJ preamble probe (which already runs `go tool asm`), but no
toolchain command exposes a package's symbols *in definition-index
order* — `go tool nm` sorts differently — so this does not solve the
core problem on its own; it would only feed option 1 or 2.
**Trigger to reopen.** An assembly file that needs a cross-package
reference — in practice `FUNCDATA $…, runtime·…(SB)` (stack maps / GC
metadata written in assembly), or any kernel that calls into another
package directly. Until then, option 3's limitation is moot and the
single-package emitter suffices.
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.11.0"
version := "0.16.0"
default:
@just --list
+5
View File
@@ -375,6 +375,11 @@ func parseImmediate(g []token.Token) ast.Immediate {
if v, ok := tryInt(text); ok {
imm.Val = v
imm.HasVal = true
} else if u, err := strconv.ParseUint(text, 0, 64); err == nil && !imm.Neg {
// Unsigned 64-bit literals (DATA mask<>+8(SB)/8, $0x8000…)
// overflow int64; keep the bit pattern.
imm.Val = int64(u)
imm.HasVal = true
} else {
imm.Float = text
}